@tea-agent/loop-agent 0.28.8 → 0.28.10
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 +25 -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/frontend-lint-baseline.js +3 -0
- package/dist/workflows/dag/init-hybrid.js +184 -46
- package/dist/workflows/dag/node-execution.js +176 -21
- package/dist/workflows/dag/project-governance-context.js +8 -2
- 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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { lstat } from "node:fs/promises";
|
|
2
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
3
3
|
import { ContractMutationError, applyTaskContract, logicalRevisionForState, observeTaskContractState, projectConstraintsMarkdown, projectRequirementMarkdown, sha256OfCanonicalJson, validateDraftForTask, } from "../contract/index.js";
|
|
4
|
+
import { TASK_CONTRACT_DRAFT_SCHEMA_VERSION } from "../contract/constants.js";
|
|
4
5
|
import { getTaskContractPaths } from "../contract/paths.js";
|
|
5
6
|
import { loadTaskConfig } from "../runtime.js";
|
|
6
7
|
import { buildPrepareDraft } from "./build-draft.js";
|
|
@@ -26,6 +27,84 @@ async function loadExistingTaskConfig(repoRoot, taskId) {
|
|
|
26
27
|
return null;
|
|
27
28
|
}
|
|
28
29
|
}
|
|
30
|
+
async function loadManagedProjectionDraft(input) {
|
|
31
|
+
const config = input.existingTaskConfig;
|
|
32
|
+
if (!config) {
|
|
33
|
+
return { ok: false, message: "managed task config could not be loaded" };
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const [requirement, constraints] = await Promise.all([
|
|
37
|
+
readFile(input.requirementPath, "utf-8"),
|
|
38
|
+
readFile(input.constraintsPath, "utf-8"),
|
|
39
|
+
]);
|
|
40
|
+
const requirementFacts = extractRequirementFactsFromMarkdown(requirement);
|
|
41
|
+
const constraintFacts = extractRequirementFactsFromMarkdown(constraints);
|
|
42
|
+
if (/^##\s+(?:References|Source Resolutions)\s*$/im.test(requirement)) {
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
message: "managed requirement projection contains unsupported References or Source Resolutions; use loop-agent task advance " +
|
|
46
|
+
input.taskId +
|
|
47
|
+
" --from-draft <reviewed-draft.json> --json",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (!requirementFacts.objective?.trim()) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
message: "managed requirement projection is missing objective",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (requirementFacts.acceptanceCriteria.length === 0) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
message: "managed requirement projection is missing acceptance criteria",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (!config.allowedPaths.length || !config.forbiddenPaths.length) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
message: "managed task config is missing path boundaries",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
draft: {
|
|
71
|
+
schemaVersion: TASK_CONTRACT_DRAFT_SCHEMA_VERSION,
|
|
72
|
+
taskId: input.taskId,
|
|
73
|
+
title: config.title,
|
|
74
|
+
taskKind: config.taskKind,
|
|
75
|
+
...(config.featureId ? { featureId: config.featureId } : {}),
|
|
76
|
+
requirement: {
|
|
77
|
+
objective: requirementFacts.objective,
|
|
78
|
+
scope: requirementFacts.scope,
|
|
79
|
+
nonGoals: requirementFacts.nonGoals,
|
|
80
|
+
acceptanceCriteria: requirementFacts.acceptanceCriteria,
|
|
81
|
+
},
|
|
82
|
+
constraints: {
|
|
83
|
+
invariants: constraintFacts.invariants,
|
|
84
|
+
allowedPaths: config.allowedPaths,
|
|
85
|
+
forbiddenPaths: config.forbiddenPaths,
|
|
86
|
+
...(config.allowedRoots !== undefined
|
|
87
|
+
? { allowedRoots: config.allowedRoots }
|
|
88
|
+
: {}),
|
|
89
|
+
},
|
|
90
|
+
verification: { commands: config.verifyCommands },
|
|
91
|
+
...(requirementFacts.openQuestions.length > 0
|
|
92
|
+
? { openQuestions: requirementFacts.openQuestions }
|
|
93
|
+
: {}),
|
|
94
|
+
...(requirementFacts.assumptions.length > 0
|
|
95
|
+
? { assumptions: requirementFacts.assumptions }
|
|
96
|
+
: {}),
|
|
97
|
+
hardConstraints: config.hardConstraints,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
message: `managed projection could not be reconstructed: ${error instanceof Error ? error.message : String(error)}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
29
108
|
function mutationOutcome(code) {
|
|
30
109
|
switch (code) {
|
|
31
110
|
case "REVISION_CONFLICT":
|
|
@@ -69,9 +148,8 @@ function buildNextSteps(input) {
|
|
|
69
148
|
next.push(`loop-agent task advance ${input.taskId} --profile auto --json`);
|
|
70
149
|
next.push("审查 DAG writeSet 后再 task advance --approve-gate ...");
|
|
71
150
|
}
|
|
72
|
-
if (input.gaps.some((g) => g.code === "DIRTY_SOURCE")) {
|
|
73
|
-
next.push(
|
|
74
|
-
next.push("或显式 --force-overwrite-source 后重跑 prepare");
|
|
151
|
+
if (input.gaps.some((g) => g.code === "DIRTY_SOURCE" || g.code === "MANAGED_PROJECTION_INCOMPLETE")) {
|
|
152
|
+
next.push(`loop-agent task advance ${input.taskId} --from-draft <reviewed-draft.json> --json`);
|
|
75
153
|
}
|
|
76
154
|
if (input.gaps.some((g) => g.code === "TRANSACTION_INCOMPLETE")) {
|
|
77
155
|
next.push(`loop-agent task advance ${input.taskId} --json`);
|
|
@@ -132,6 +210,24 @@ export async function prepareTaskSource(input) {
|
|
|
132
210
|
}
|
|
133
211
|
}
|
|
134
212
|
let baseDraft;
|
|
213
|
+
let managedProjectionError;
|
|
214
|
+
if ((input.intent.kind === "facts" &&
|
|
215
|
+
!input.intent.text &&
|
|
216
|
+
(state.effectiveStatus === "managed" ||
|
|
217
|
+
Boolean(state.ref && state.ref.revision > 0)))) {
|
|
218
|
+
const projection = await loadManagedProjectionDraft({
|
|
219
|
+
taskId,
|
|
220
|
+
existingTaskConfig,
|
|
221
|
+
requirementPath: contractPaths.requirementPath,
|
|
222
|
+
constraintsPath: contractPaths.constraintsPath,
|
|
223
|
+
});
|
|
224
|
+
if (projection.ok) {
|
|
225
|
+
baseDraft = projection.draft;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
managedProjectionError = projection.message;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
135
231
|
let facts;
|
|
136
232
|
let sourceIntegrity = null;
|
|
137
233
|
let hasImportedPrd = false;
|
|
@@ -272,6 +368,13 @@ export async function prepareTaskSource(input) {
|
|
|
272
368
|
sourceFilesPresent,
|
|
273
369
|
forceOverwriteSource: input.forceOverwriteSource,
|
|
274
370
|
});
|
|
371
|
+
if (managedProjectionError) {
|
|
372
|
+
gaps.push({
|
|
373
|
+
code: "MANAGED_PROJECTION_INCOMPLETE",
|
|
374
|
+
level: "blocking",
|
|
375
|
+
message: managedProjectionError,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
275
378
|
// If --use-imported-prd and no parseable requirement and no AC from flags/text → blocking
|
|
276
379
|
if (input.intent.kind === "facts" &&
|
|
277
380
|
input.intent.useImportedPrd &&
|
|
@@ -292,6 +292,9 @@ const writerChangeManifestSchema = z
|
|
|
292
292
|
.object({
|
|
293
293
|
schemaVersion: z.literal(1),
|
|
294
294
|
writerNodeId: z.string().min(1),
|
|
295
|
+
effectiveWriteSet: z.array(z.string()).optional(),
|
|
296
|
+
approvalSourceNodeId: z.string().min(1).optional(),
|
|
297
|
+
approvalDigest: z.string().min(1).optional(),
|
|
295
298
|
changedFiles: z.array(relativePathSchema),
|
|
296
299
|
beforeStatusSha256: sha256Schema,
|
|
297
300
|
afterStatusSha256: sha256Schema,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { access, readdir, readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
5
6
|
import { assertValidDagSpec } from "./validate.js";
|
|
@@ -958,7 +959,6 @@ function verifyCommandKey(command) {
|
|
|
958
959
|
cwdKey,
|
|
959
960
|
normalizedArgs.join("\0"),
|
|
960
961
|
envKey,
|
|
961
|
-
command.timeoutMs ?? "",
|
|
962
962
|
].join("\u0001");
|
|
963
963
|
}
|
|
964
964
|
function normalizeVerifyCommandArgs(args) {
|
|
@@ -1028,6 +1028,103 @@ function tokenizeSimpleShellCommand(command) {
|
|
|
1028
1028
|
tokens.push(token);
|
|
1029
1029
|
return tokens.length > 0 ? tokens : undefined;
|
|
1030
1030
|
}
|
|
1031
|
+
function isCheckRepoVerifyCommand(command) {
|
|
1032
|
+
return normalizeVerifyCommandArgs(command.args)
|
|
1033
|
+
.join(" ")
|
|
1034
|
+
.replaceAll("\\", "/")
|
|
1035
|
+
.includes("scripts/check-repo.sh");
|
|
1036
|
+
}
|
|
1037
|
+
const CHECK_REPO_COVERED_SCRIPTS = new Set([
|
|
1038
|
+
"scripts/check-doc-links.sh",
|
|
1039
|
+
"scripts/check-skill-entry.sh",
|
|
1040
|
+
"scripts/check-init-surface.sh",
|
|
1041
|
+
]);
|
|
1042
|
+
function localScriptTarget(command) {
|
|
1043
|
+
const args = normalizeVerifyCommandArgs(command.args);
|
|
1044
|
+
const candidate = args.find((arg) => arg.replaceAll("\\", "/").startsWith("scripts/"));
|
|
1045
|
+
return candidate?.replaceAll("\\", "/");
|
|
1046
|
+
}
|
|
1047
|
+
function isCheckRepoCoveredCommand(command) {
|
|
1048
|
+
const target = localScriptTarget(command);
|
|
1049
|
+
return target ? CHECK_REPO_COVERED_SCRIPTS.has(target) : false;
|
|
1050
|
+
}
|
|
1051
|
+
function canonicalizeVerificationCommands(commands) {
|
|
1052
|
+
const byKey = new Map();
|
|
1053
|
+
const merged = new Map();
|
|
1054
|
+
for (const command of commands) {
|
|
1055
|
+
const canonicalKey = verifyCommandKey(command);
|
|
1056
|
+
const retained = byKey.get(canonicalKey);
|
|
1057
|
+
if (!retained) {
|
|
1058
|
+
byKey.set(canonicalKey, { ...command, env: command.env ? { ...command.env } : undefined });
|
|
1059
|
+
merged.set(canonicalKey, {
|
|
1060
|
+
canonicalKey,
|
|
1061
|
+
keptLabel: command.label,
|
|
1062
|
+
mergedLabels: [],
|
|
1063
|
+
effectiveTimeoutMs: command.timeoutMs ?? null,
|
|
1064
|
+
});
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
const retainedTimeout = retained.timeoutMs ?? 0;
|
|
1068
|
+
const candidateTimeout = command.timeoutMs ?? 0;
|
|
1069
|
+
if (candidateTimeout > retainedTimeout)
|
|
1070
|
+
retained.timeoutMs = command.timeoutMs;
|
|
1071
|
+
const mergedEntry = merged.get(canonicalKey);
|
|
1072
|
+
mergedEntry.effectiveTimeoutMs = retained.timeoutMs ?? null;
|
|
1073
|
+
mergedEntry.mergedLabels.push(command.label);
|
|
1074
|
+
}
|
|
1075
|
+
const retained = [...byKey.values()];
|
|
1076
|
+
const aggregate = retained.find(isCheckRepoVerifyCommand);
|
|
1077
|
+
const covered = [];
|
|
1078
|
+
const commandsAfterCoverage = aggregate
|
|
1079
|
+
? retained.filter((command) => {
|
|
1080
|
+
if (!isCheckRepoCoveredCommand(command))
|
|
1081
|
+
return true;
|
|
1082
|
+
covered.push({ aggregateLabel: aggregate.label, coveredLabel: command.label });
|
|
1083
|
+
return false;
|
|
1084
|
+
})
|
|
1085
|
+
: retained;
|
|
1086
|
+
return {
|
|
1087
|
+
commands: commandsAfterCoverage,
|
|
1088
|
+
merged: [...merged.values()].filter((entry) => entry.mergedLabels.length > 0),
|
|
1089
|
+
covered,
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
function assertVerificationPlanPreflight(input) {
|
|
1093
|
+
const root = path.resolve(input.repoRoot);
|
|
1094
|
+
for (const command of input.commands) {
|
|
1095
|
+
const cwd = path.resolve(command.cwd);
|
|
1096
|
+
if (!isWithinRepo(root, cwd) || !existsSync(cwd)) {
|
|
1097
|
+
throw new Error(`verification preflight rejected cwd for ${command.label}: ${command.cwd}`);
|
|
1098
|
+
}
|
|
1099
|
+
const script = localScriptTarget(command);
|
|
1100
|
+
if (!script)
|
|
1101
|
+
continue;
|
|
1102
|
+
const scriptPath = path.resolve(cwd, script);
|
|
1103
|
+
if (existsSync(scriptPath))
|
|
1104
|
+
continue;
|
|
1105
|
+
const writerMayCreate = input.taskConfig.allowedPaths.some((allowed) => pathMatchesPattern(script, allowed.replace(/^\.\//, "")));
|
|
1106
|
+
if (!writerMayCreate) {
|
|
1107
|
+
throw new Error(`verification preflight missing local script for ${command.label}: ${script}`);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
function isWithinRepo(root, candidate) {
|
|
1112
|
+
const relative = path.relative(root, candidate);
|
|
1113
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
1114
|
+
}
|
|
1115
|
+
function taskVerifyCommands(repoRoot, taskConfig) {
|
|
1116
|
+
if (!repoRoot)
|
|
1117
|
+
return [];
|
|
1118
|
+
return taskConfig.verifyCommands.map((command) => ({
|
|
1119
|
+
args: ["bash", "-lc", command.command],
|
|
1120
|
+
cwd: repoRoot,
|
|
1121
|
+
label: command.label,
|
|
1122
|
+
timeoutMs: command.timeoutMs,
|
|
1123
|
+
}));
|
|
1124
|
+
}
|
|
1125
|
+
function verifyQuotaLimit(quota) {
|
|
1126
|
+
return quota === "full" ? undefined : Number.parseInt(quota, 10);
|
|
1127
|
+
}
|
|
1031
1128
|
function isFullSuiteVerifyCommand(command) {
|
|
1032
1129
|
const args = normalizeVerifyCommandArgs(command.args);
|
|
1033
1130
|
let index = 0;
|
|
@@ -1043,10 +1140,10 @@ function isFullSuiteVerifyCommand(command) {
|
|
|
1043
1140
|
(rest.length === 2 && rest[0] === "run" && rest[1] === "test"));
|
|
1044
1141
|
}
|
|
1045
1142
|
if (["bash", "sh"].includes(executable) && rest.length === 1) {
|
|
1046
|
-
return /(?:^|\/)scripts\/ci\.sh$/i.test(rest[0].replace(/\\/g, "/"));
|
|
1143
|
+
return /(?:^|\/)scripts\/(?:ci|check-repo)\.sh$/i.test(rest[0].replace(/\\/g, "/"));
|
|
1047
1144
|
}
|
|
1048
1145
|
return (rest.length === 0 &&
|
|
1049
|
-
/(?:^|\/)scripts\/ci\.sh$/i.test((args[index] ?? "").replace(/\\/g, "/")));
|
|
1146
|
+
/(?:^|\/)scripts\/(?:ci|check-repo)\.sh$/i.test((args[index] ?? "").replace(/\\/g, "/")));
|
|
1050
1147
|
}
|
|
1051
1148
|
function resolveDagVerifyStrategy(taskConfig, defaultIntermediateQuotaWhenFull = "full") {
|
|
1052
1149
|
const explicitIntermediateQuota = taskConfig.dagVerifyStrategy?.intermediateQuota;
|
|
@@ -1070,6 +1167,13 @@ function buildVerifyEvidence(input) {
|
|
|
1070
1167
|
commandLabels: selectedCommands?.map((command) => command.label) ??
|
|
1071
1168
|
input.fallbackCommands,
|
|
1072
1169
|
commandTexts: input.commandTexts ?? [],
|
|
1170
|
+
canonicalKeys: selectedCommands?.map(verifyCommandKey) ?? [],
|
|
1171
|
+
mergedCommands: input.plan?.merged ?? [],
|
|
1172
|
+
coveredCommands: input.plan?.covered ?? [],
|
|
1173
|
+
selectionReasons: selectedCommands?.map((command) => input.phase === "intermediate" && isFullSuiteVerifyCommand(command)
|
|
1174
|
+
? `${command.label}: deferred-heavy`
|
|
1175
|
+
: `${command.label}: kept`) ?? [],
|
|
1176
|
+
preflight: selectedCommands?.map((command) => ({ label: command.label, status: "ok" })) ?? [],
|
|
1073
1177
|
commandTimeoutMs: input.commandTimeoutMs,
|
|
1074
1178
|
totalTimeoutBudgetMs: commandCount * input.commandTimeoutMs,
|
|
1075
1179
|
finalFullRequired: input.finalFullRequired,
|
|
@@ -1388,6 +1492,10 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
|
1388
1492
|
catch {
|
|
1389
1493
|
throw new Error(`task.json not found for task "${taskId}": expected ${paths.taskConfigPath}`);
|
|
1390
1494
|
}
|
|
1495
|
+
const taskConfig = await loadTaskConfig(repoRoot, taskId);
|
|
1496
|
+
// Baseline boundaries are independently knowable from task.json and must
|
|
1497
|
+
// not be masked by source or adapter verification loading failures.
|
|
1498
|
+
assertTaskAllowedPathsPreflight(taskConfig);
|
|
1391
1499
|
let requirementMarkdown;
|
|
1392
1500
|
try {
|
|
1393
1501
|
requirementMarkdown = await readFile(requirementPath, "utf-8");
|
|
@@ -1402,7 +1510,6 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
|
1402
1510
|
catch {
|
|
1403
1511
|
// optional
|
|
1404
1512
|
}
|
|
1405
|
-
const taskConfig = await loadTaskConfig(repoRoot, taskId);
|
|
1406
1513
|
await materializeTaskReferenceDocs({
|
|
1407
1514
|
repoRoot,
|
|
1408
1515
|
taskId,
|
|
@@ -1415,19 +1522,40 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
|
1415
1522
|
try {
|
|
1416
1523
|
const adapter = await resolveAdapter(repoRoot);
|
|
1417
1524
|
const preset = resolveVerifyPreset(taskConfig.verifyPreset, taskConfig);
|
|
1525
|
+
const adapterIntermediate = adapter.getVerifyCommands(repoRoot, {
|
|
1526
|
+
preset,
|
|
1527
|
+
quota: strategy.intermediateQuota,
|
|
1528
|
+
phase: "intermediate",
|
|
1529
|
+
taskConfig,
|
|
1530
|
+
});
|
|
1531
|
+
const adapterFinal = adapter.getVerifyCommands(repoRoot, {
|
|
1532
|
+
preset,
|
|
1533
|
+
quota: "full",
|
|
1534
|
+
phase: "final",
|
|
1535
|
+
taskConfig,
|
|
1536
|
+
});
|
|
1537
|
+
const intermediatePlan = canonicalizeVerificationCommands([
|
|
1538
|
+
...taskVerifyCommands(repoRoot, taskConfig),
|
|
1539
|
+
...adapterIntermediate,
|
|
1540
|
+
]);
|
|
1541
|
+
const finalPlan = canonicalizeVerificationCommands([
|
|
1542
|
+
...taskVerifyCommands(repoRoot, taskConfig),
|
|
1543
|
+
...adapterFinal,
|
|
1544
|
+
]);
|
|
1545
|
+
assertVerificationPlanPreflight({
|
|
1546
|
+
repoRoot,
|
|
1547
|
+
commands: [...intermediatePlan.commands, ...finalPlan.commands],
|
|
1548
|
+
taskConfig,
|
|
1549
|
+
});
|
|
1418
1550
|
verifyCommands = {
|
|
1419
|
-
intermediate:
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
quota: "full",
|
|
1428
|
-
phase: "final",
|
|
1429
|
-
taskConfig,
|
|
1430
|
-
})),
|
|
1551
|
+
intermediate: [
|
|
1552
|
+
...taskVerifyCommands(repoRoot, taskConfig),
|
|
1553
|
+
...adapterIntermediate,
|
|
1554
|
+
],
|
|
1555
|
+
final: [
|
|
1556
|
+
...taskVerifyCommands(repoRoot, taskConfig),
|
|
1557
|
+
...adapterFinal,
|
|
1558
|
+
],
|
|
1431
1559
|
};
|
|
1432
1560
|
if (verifyCommands.final.length === 0) {
|
|
1433
1561
|
throw new Error("adapter returned no final verification commands");
|
|
@@ -1491,22 +1619,6 @@ async function prepareFrontendMockSources(sources, discoveredProjectCapability)
|
|
|
1491
1619
|
frontendRisk,
|
|
1492
1620
|
};
|
|
1493
1621
|
}
|
|
1494
|
-
function mergeFinalVerifyCommands(repoRoot, taskConfig, adapterCommands) {
|
|
1495
|
-
const taskCommands = taskConfig.verifyCommands.map((command) => ({
|
|
1496
|
-
args: ["bash", "-lc", command.command],
|
|
1497
|
-
cwd: repoRoot,
|
|
1498
|
-
label: command.label,
|
|
1499
|
-
timeoutMs: command.timeoutMs,
|
|
1500
|
-
}));
|
|
1501
|
-
const seen = new Set();
|
|
1502
|
-
return [...taskCommands, ...adapterCommands].filter((command) => {
|
|
1503
|
-
const key = verifyCommandKey(command);
|
|
1504
|
-
if (seen.has(key))
|
|
1505
|
-
return false;
|
|
1506
|
-
seen.add(key);
|
|
1507
|
-
return true;
|
|
1508
|
-
});
|
|
1509
|
-
}
|
|
1510
1622
|
export function buildStandardHybridDagFromTask(sources) {
|
|
1511
1623
|
const { taskConfig } = sources;
|
|
1512
1624
|
const forbiddenPaths = mergeForbiddenPaths(taskConfig);
|
|
@@ -1518,9 +1630,10 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1518
1630
|
const implementComplexity = resolveWriterComplexity(taskConfig);
|
|
1519
1631
|
const implementId = implementationNodeId();
|
|
1520
1632
|
const finalVerifyCommands = sources.verifyCommands?.final ?? [];
|
|
1633
|
+
const finalVerifyPlan = canonicalizeVerificationCommands(finalVerifyCommands);
|
|
1521
1634
|
const plannedFinal = applyMavenVerificationPlanning({
|
|
1522
1635
|
repoRoot: sources.repoRoot,
|
|
1523
|
-
commands:
|
|
1636
|
+
commands: finalVerifyPlan.commands,
|
|
1524
1637
|
taskConfig,
|
|
1525
1638
|
});
|
|
1526
1639
|
const verifyShellCommands = buildVerifyShellCommands({
|
|
@@ -1553,6 +1666,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1553
1666
|
fallbackCommands: [],
|
|
1554
1667
|
finalFullRequired: true,
|
|
1555
1668
|
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
1669
|
+
plan: finalVerifyPlan,
|
|
1556
1670
|
}),
|
|
1557
1671
|
cwd: ".",
|
|
1558
1672
|
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
@@ -5290,21 +5404,17 @@ export async function requireManagedTaskContractBinding(input) {
|
|
|
5290
5404
|
taskId: input.taskId,
|
|
5291
5405
|
});
|
|
5292
5406
|
if (state.effectiveStatus !== "managed" || !state.ref) {
|
|
5293
|
-
const expectedRevision = state.ref?.revision ?? 0;
|
|
5294
|
-
const observedCanonicalHash = state.observedCanonicalHash ?? "<observedCanonicalHash-from-show>";
|
|
5295
5407
|
const recovery = state.effectiveStatus === "transaction-incomplete"
|
|
5296
5408
|
? `Recover the unfinished transaction first: loop-agent task advance ${input.taskId} --json`
|
|
5297
5409
|
: [
|
|
5298
|
-
"
|
|
5299
|
-
`loop-agent task advance ${input.taskId} --from-draft
|
|
5300
|
-
"or apply a reviewed draft:",
|
|
5301
|
-
`loop-agent task advance ${input.taskId} --from-draft --input <reviewed-draft.json> --expected-revision ${expectedRevision} --expected-observed-hash ${observedCanonicalHash} --request-id <unique-request-id> --request-payload-sha256 <sha256-of-input-file> --json`,
|
|
5410
|
+
"Create or review a TaskContractDraftV1 JSON file, then apply it through the public lifecycle command:",
|
|
5411
|
+
`loop-agent task advance ${input.taskId} --from-draft <reviewed-draft.json> --json`,
|
|
5302
5412
|
].join(" ");
|
|
5303
5413
|
const err = new Error([
|
|
5304
5414
|
`dag generate requires managed Task Contract for task ${input.taskId} (effectiveStatus=${state.effectiveStatus})`,
|
|
5305
5415
|
`Inspect current state: loop-agent task status ${input.taskId} --json`,
|
|
5306
5416
|
recovery,
|
|
5307
|
-
"Do not auto-
|
|
5417
|
+
"Do not auto-apply before confirming the Task Contract facts.",
|
|
5308
5418
|
].join("; "));
|
|
5309
5419
|
err.code =
|
|
5310
5420
|
state.effectiveStatus === "externally-modified"
|
|
@@ -5728,11 +5838,12 @@ function buildWriteSetAuditFormatRepairNode(sources, options) {
|
|
|
5728
5838
|
writePolicy: "read-only",
|
|
5729
5839
|
allowedPaths: commonReadOnlyPaths(sources),
|
|
5730
5840
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
5731
|
-
outputContract: "Plain Markdown whose first non-empty line is exactly VERDICT: pass or VERDICT: request-revision,
|
|
5841
|
+
outputContract: "Plain Markdown whose first non-empty line is exactly VERDICT: pass or VERDICT: request-revision. For a final audit pass, preserve exactly one valid FINAL_WRITE_SET_APPROVAL_JSON from upstream; never infer, create, alter, or broaden approval data. No file writes.",
|
|
5732
5842
|
subtask_prompt: [
|
|
5733
5843
|
`Normalize the output format of ${options.auditNodeId}; this is the single read-only format-repair attempt for that audit.`,
|
|
5734
5844
|
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
5735
5845
|
"If the upstream audit already contains a valid verdict, preserve it exactly. If it omitted or malformed the verdict but states an unambiguous audit conclusion, add only the matching canonical verdict and preserve the findings.",
|
|
5846
|
+
"For final-write-set-audit-pi only: a pass requires exactly one upstream FINAL_WRITE_SET_APPROVAL_JSON block. Preserve it unchanged; never infer, synthesize, normalize, remove, alter, or broaden approval data. Otherwise emit VERDICT: request-revision.",
|
|
5736
5847
|
"Do not add, remove, or reclassify substantive findings. If the upstream conclusion is ambiguous or cannot be preserved safely, emit VERDICT: request-revision and report the format ambiguity.",
|
|
5737
5848
|
"Do not infer a pass from general prose, expand task allowedPaths, or edit files.",
|
|
5738
5849
|
buildSourceContextBlock(sources),
|
|
@@ -5802,11 +5913,12 @@ function buildFinalWriteSetAuditNode(sources) {
|
|
|
5802
5913
|
writePolicy: "read-only",
|
|
5803
5914
|
allowedPaths: commonReadOnlyPaths(sources),
|
|
5804
5915
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
5805
|
-
outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision
|
|
5916
|
+
outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision. A pass MUST include exactly one FINAL_WRITE_SET_APPROVAL_JSON fenced block with schemaVersion, writerNodeId, approvalSourceNodeId, auditedPlanNodeId, approvedWriteSet, taskContractSha256, auditedPlanSha256, and approvalDigest. No file writes.",
|
|
5806
5917
|
subtask_prompt: [
|
|
5807
5918
|
"Perform the final write-set audit after the single bounded plan-revision round.",
|
|
5808
5919
|
"When plan-revision-pi returned PASS_NO_REVISION_NEEDED, audit the original plan-pi output. Otherwise audit the complete revised plan.",
|
|
5809
5920
|
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
5921
|
+
"On VERDICT: pass emit exactly one ```FINAL_WRITE_SET_APPROVAL_JSON fenced JSON object containing schemaVersion: 1, writerNodeId: 'implement-pi', approvalSourceNodeId: 'final-write-set-audit-format-repair-pi', auditedPlanNodeId: 'plan-revision-pi', approvedWriteSet, taskContractSha256, auditedPlanSha256, and approvalDigest. approvedWriteSet is ordered, unique, exact, concrete, and never copied or expanded from allowedPaths. approvalDigest is SHA-256 of canonical JSON excluding approvalDigest.",
|
|
5810
5922
|
"Request revision if required files still lack a single exclusive owner, writeSet is broad/placeholder, forbidden paths overlap, or any initial finding remains unresolved.",
|
|
5811
5923
|
"Do not expand task allowedPaths or edit files.",
|
|
5812
5924
|
buildSourceContextBlock(sources),
|
|
@@ -5823,10 +5935,11 @@ function buildWriteSetGateNode(sources) {
|
|
|
5823
5935
|
writePolicy: "read-only",
|
|
5824
5936
|
allowedPaths: commonReadOnlyPaths(sources),
|
|
5825
5937
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
5826
|
-
outputContract: "Deterministic final write-set
|
|
5827
|
-
subtask_prompt: "Deterministic gate: block the implementation writer unless the normalized final
|
|
5938
|
+
outputContract: "Deterministic final write-set approval gate: exit 0 only when final-write-set-audit-format-repair-pi emits VERDICT: pass with one valid, integrity-bound FINAL_WRITE_SET_APPROVAL_JSON for implement-pi.",
|
|
5939
|
+
subtask_prompt: "Deterministic gate: block the implementation writer unless the normalized final audit emitted VERDICT: pass with a valid exact final write-set approval.",
|
|
5828
5940
|
shell: {
|
|
5829
5941
|
commands: [],
|
|
5942
|
+
finalWriteSetApprovalGate: { writerNodeId: "implement-pi" },
|
|
5830
5943
|
verdictGate: {
|
|
5831
5944
|
fromNodeId: "final-write-set-audit-format-repair-pi",
|
|
5832
5945
|
accept: ["VERDICT: pass"],
|
|
@@ -5844,7 +5957,15 @@ async function buildSoftVerifyNode(sources) {
|
|
|
5844
5957
|
const fallbackCommands = sources.repoRoot
|
|
5845
5958
|
? (await discoverFrontendFallbackVerifyCommands(sources.repoRoot)).staticCommands
|
|
5846
5959
|
: ["npm run typecheck"];
|
|
5847
|
-
const
|
|
5960
|
+
const candidatePlan = canonicalizeVerificationCommands([
|
|
5961
|
+
...taskVerifyCommands(sources.repoRoot, sources.taskConfig),
|
|
5962
|
+
...(sources.verifyCommands?.intermediate ?? []),
|
|
5963
|
+
]);
|
|
5964
|
+
const focusedCandidates = candidatePlan.commands.filter((command) => !isFullSuiteVerifyCommand(command));
|
|
5965
|
+
const quota = verifyQuotaLimit(strategy.intermediateQuota ?? "full");
|
|
5966
|
+
const focusedIntermediate = quota
|
|
5967
|
+
? focusedCandidates.slice(0, quota)
|
|
5968
|
+
: focusedCandidates;
|
|
5848
5969
|
const plannedIntermediate = applyMavenVerificationPlanning({
|
|
5849
5970
|
repoRoot: sources.repoRoot,
|
|
5850
5971
|
commands: focusedIntermediate,
|
|
@@ -5876,6 +5997,7 @@ async function buildSoftVerifyNode(sources) {
|
|
|
5876
5997
|
fallbackCommands,
|
|
5877
5998
|
commandTexts: commands,
|
|
5878
5999
|
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
6000
|
+
plan: candidatePlan,
|
|
5879
6001
|
}),
|
|
5880
6002
|
cwd: ".",
|
|
5881
6003
|
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
@@ -5977,9 +6099,13 @@ function buildHardVerifyNode(sources) {
|
|
|
5977
6099
|
const fallbackCommands = [
|
|
5978
6100
|
"HARNESS_ALLOW_ACTIVE_DAG_RUNS=1 bash scripts/check-repo.sh",
|
|
5979
6101
|
];
|
|
6102
|
+
const finalPlan = canonicalizeVerificationCommands([
|
|
6103
|
+
...taskVerifyCommands(sources.repoRoot, sources.taskConfig),
|
|
6104
|
+
...(sources.verifyCommands?.final ?? []),
|
|
6105
|
+
]);
|
|
5980
6106
|
const plannedFinal = applyMavenVerificationPlanning({
|
|
5981
6107
|
repoRoot: sources.repoRoot,
|
|
5982
|
-
commands:
|
|
6108
|
+
commands: finalPlan.commands,
|
|
5983
6109
|
taskConfig: sources.taskConfig,
|
|
5984
6110
|
});
|
|
5985
6111
|
const commands = buildVerifyShellCommands({
|
|
@@ -6009,6 +6135,7 @@ function buildHardVerifyNode(sources) {
|
|
|
6009
6135
|
commandTexts: commands,
|
|
6010
6136
|
finalFullRequired: true,
|
|
6011
6137
|
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
6138
|
+
plan: finalPlan,
|
|
6012
6139
|
}),
|
|
6013
6140
|
cwd: ".",
|
|
6014
6141
|
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
@@ -6136,6 +6263,12 @@ async function buildSupervisedHybridDag(standard, sources) {
|
|
|
6136
6263
|
"plan-revision-pi",
|
|
6137
6264
|
"final-write-set-audit-format-repair-pi",
|
|
6138
6265
|
],
|
|
6266
|
+
finalWriteSetApproval: {
|
|
6267
|
+
schemaVersion: 1,
|
|
6268
|
+
writerNodeId: "implement-pi",
|
|
6269
|
+
approvalSourceNodeId: "final-write-set-audit-format-repair-pi",
|
|
6270
|
+
auditedPlanNodeId: "plan-revision-pi",
|
|
6271
|
+
},
|
|
6139
6272
|
}),
|
|
6140
6273
|
await buildSoftVerifyNode(sources),
|
|
6141
6274
|
buildProcessSupervisorNode(sources),
|
|
@@ -6207,6 +6340,11 @@ export async function writeHybridDagDraft(sources, outputPath, options = {}) {
|
|
|
6207
6340
|
};
|
|
6208
6341
|
}
|
|
6209
6342
|
export async function initHybridDagFromTask(repoRoot, taskId, options = {}) {
|
|
6343
|
+
// These checks are independently knowable and must win over adapter
|
|
6344
|
+
// verification loading (which can fail for a missing local script).
|
|
6345
|
+
const taskConfig = await loadTaskConfig(repoRoot, taskId);
|
|
6346
|
+
assertTaskAllowedPathsPreflight(taskConfig);
|
|
6347
|
+
await requireManagedTaskContractBinding({ repoRoot, taskId });
|
|
6210
6348
|
const sources = await loadTaskHybridSources(repoRoot, taskId);
|
|
6211
6349
|
assertTaskAllowedPathsPreflight(sources.taskConfig);
|
|
6212
6350
|
const outputPath = options.outputPath ?? getTaskPaths(repoRoot, taskId).dagDraftPath;
|