@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.
@@ -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: adapter.getVerifyCommands(repoRoot, {
1420
- preset,
1421
- quota: strategy.intermediateQuota,
1422
- phase: "intermediate",
1423
- taskConfig,
1424
- }),
1425
- final: mergeFinalVerifyCommands(repoRoot, taskConfig, adapter.getVerifyCommands(repoRoot, {
1426
- preset,
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: finalVerifyCommands,
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
- "After reviewing the show JSON, either adopt the current on-disk facts:",
5299
- `loop-agent task advance ${input.taskId} --from-draft --expected-revision ${expectedRevision} --expected-observed-hash ${observedCanonicalHash} --request-id <unique-request-id> --request-payload-sha256 <sha256-of-this-adopt-request> --json`,
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-adopt or auto-apply before confirming the Task Contract facts and concurrency fields.",
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, followed by the original audit findings without substantive changes. No file writes.",
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; includes final writeSet coverage findings after the single plan-revision round. No file writes.",
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 audit verdict gate: exit 0 only when final-write-set-audit-format-repair-pi emits VERDICT: pass after the bounded revision round.",
5827
- subtask_prompt: "Deterministic gate: block the implementation writer unless the normalized final write-set audit emitted VERDICT: pass.",
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 focusedIntermediate = sources.verifyCommands?.intermediate.filter((command) => !isFullSuiteVerifyCommand(command));
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: sources.verifyCommands?.final,
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;