@tea-agent/loop-agent 0.39.0-beta.13 → 0.39.0-beta.15

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 CHANGED
@@ -1,6 +1,8 @@
1
1
  # 更新日志
2
2
 
3
3
  ## [Unreleased]
4
+ - 0.39.0-beta.15(frontend-node dogfood):修复自动前端路由修改 task.json 后未同步 Task Contract,导致批准阶段反复出现 externally-modified、contract binding drift 和 DAG 启动失败的问题;同时修复 adopt/import refresh 丢失 source fidelity ledger hash 的问题。
5
+ - 0.39.0-beta.14(frontend-node dogfood):plan-pi 三段分段会话 + coverage 需求批次切片(4/批、零进度减半)、finalize 收据修复环(聚合诊断+修复 JSON)、record_* 边界校验左移(VT 重复/writeSet/交互 VT 引用/规范需求 id/可疑 symbol)、编译装配 last-wins(组件按 purpose 去重修正)、review-context 授权面扩展至 VT 测试文件、设计评审修订降级为 admission advisory。
4
6
  - 前端 DAG 极端环境加固(16KB 输出 / 120KB 上下文真实冒烟,r2-r5 验证):`frontend-contract-pi` 改为消费编译后的 ledger 输入块(`<frontend_contract_input>`,12KB 上限、cap ladder、requirement id 永不丢)并移除 read 工具,以增量 `record_*` 提交为唯一输出模式(OUTPUT BUDGET DISCIPLINE),修复极端小输出窗口下模型反复 read 源文件空转 1h54m 超时的问题(r3 起 contract 3m54s 正常完成、37 次 record_* / 0 次 read);plan/contract 的 `record_*` 工具支持每 message 最多 5 条批量提交并加 1s 调用间隔,rate-limit 退避上限提高到 60s,缓解密集小调用触发第三方网关限流;修复 fragment-inventory 对 V4 标题式 AC(`#### AC-NNN`)的双重 emit 缺陷(同一标题行被 section 入口与正文 heading 分支各切一次,30 个 AC 结构性产生 30 个 duplicate)。
5
7
 
6
8
  - 修复连续 `dag rerun-task` 丢失前代评审意见的问题:直接父 run 未走到 review/design review 时,`deriveDagRerunFeedback` 沿 run-owned lineage 有界追溯最近 5 层并继承最近的 committed typed findings;较近的 `approve_review` / `approve_design` 作为对应 resolution barrier,防止已解决 finding 被复活。lineage run ID、生命周期目录与 `state.json.runId` 必须一致,损坏、越界、循环或超深链路 fail closed。
@@ -280,12 +280,12 @@ export async function generateTaskDagUseCase(input) {
280
280
  requestPayloadSha256: randomUUID().replaceAll("-", "").padEnd(64, "0"),
281
281
  });
282
282
  }
283
- const candidateResult = await initHybridDagFromTask(repoRoot, parsed.taskId, {
283
+ let candidateResult = await initHybridDagFromTask(repoRoot, parsed.taskId, {
284
284
  outputPath: parsed.outputPath,
285
285
  template: "standard-dag",
286
286
  rerunFeedback: parsed.rerunFeedback,
287
287
  });
288
- const candidateValidateSummary = await validateDagUseCase(buildValidateInput(repoRoot, candidateResult.outputPath, parsed));
288
+ let candidateValidateSummary = await validateDagUseCase(buildValidateInput(repoRoot, candidateResult.outputPath, parsed));
289
289
  const profileRouting = await resolveProfileRouting(repoRoot, parsed, candidateValidateSummary.governanceProfile ?? {
290
290
  profile: "standard",
291
291
  process: [],
@@ -322,6 +322,30 @@ export async function generateTaskDagUseCase(input) {
322
322
  taskKind: "frontend-implementation",
323
323
  dagFallbackReason: "Automatically classified as a high-confidence frontend implementation task.",
324
324
  });
325
+ // taskKind participates in task.json's canonical hash. Adopt the
326
+ // automatic route before exposing this DAG, then regenerate it so its
327
+ // frozen taskContractBinding points at the adopted revision.
328
+ const state = await observeTaskContract({
329
+ repoRoot,
330
+ taskId: parsed.taskId,
331
+ });
332
+ if (!state.ref || !state.observedCanonicalHash) {
333
+ throw new Error("automatic frontend routing could not reconcile the managed Task Contract");
334
+ }
335
+ await adoptTaskContract({
336
+ repoRoot,
337
+ taskId: parsed.taskId,
338
+ expectedRevision: state.ref.revision,
339
+ expectedObservedHash: state.observedCanonicalHash,
340
+ requestId: `automatic-frontend-route-${parsed.taskId}-${randomUUID()}`,
341
+ requestPayloadSha256: randomUUID().replaceAll("-", "").padEnd(64, "0"),
342
+ });
343
+ candidateResult = await initHybridDagFromTask(repoRoot, parsed.taskId, {
344
+ outputPath: parsed.outputPath,
345
+ template: "standard-dag",
346
+ rerunFeedback: parsed.rerunFeedback,
347
+ });
348
+ candidateValidateSummary = await validateDagUseCase(buildValidateInput(repoRoot, candidateResult.outputPath, parsed));
325
349
  }
326
350
  }
327
351
  const initResult = profileRouting.selectedTemplate === candidateResult.template
@@ -260,6 +260,10 @@ async function monitorRun(input) {
260
260
  failureCategory: "monitor-timeout",
261
261
  });
262
262
  }
263
+ function isStartupGateEligible(snapshot) {
264
+ const status = snapshot.source?.sourceFidelityStartup?.status;
265
+ return status === undefined || status === "complete" || status === "not-applicable";
266
+ }
263
267
  function nextMutationAction(actions) {
264
268
  for (const action of actions) {
265
269
  if (action.type !== "stop")
@@ -379,7 +383,10 @@ export async function advanceTaskLifecycle(input) {
379
383
  };
380
384
  }
381
385
  const token = parseGateToken(input.rejectGateToken);
382
- const currentGate = snapshot.gate ?? (await buildCurrentGate(input.repoRoot, input.taskId));
386
+ const currentGate = snapshot.gate ??
387
+ (isStartupGateEligible(snapshot)
388
+ ? await buildCurrentGate(input.repoRoot, input.taskId)
389
+ : null);
383
390
  if (!currentGate || !gateMatches(currentGate, token)) {
384
391
  return {
385
392
  taskId: input.taskId,
@@ -443,11 +450,12 @@ export async function advanceTaskLifecycle(input) {
443
450
  dryRun,
444
451
  skipFinalize: Boolean(input.skipFinalize),
445
452
  forcePrepareContract: Boolean(input.fromDraftPath) && !preparedContractThisCall,
453
+ startupRepairAttempted: preparedContractThisCall,
446
454
  });
447
455
  if (dryRun) {
448
456
  const plan = planOnce(snapshot);
449
457
  const plannedGate = snapshot.gate ??
450
- (snapshot.dagDraft?.exists
458
+ (isStartupGateEligible(snapshot) && snapshot.dagDraft?.exists
451
459
  ? await buildCurrentGate(input.repoRoot, input.taskId)
452
460
  : null);
453
461
  return {
@@ -586,6 +594,10 @@ export async function advanceTaskLifecycle(input) {
586
594
  continue;
587
595
  }
588
596
  if (action.type === "prepare-contract") {
597
+ const startupStatus = snapshot.source?.sourceFidelityStartup?.status;
598
+ const startupRepairAction = startupStatus === "missing" ||
599
+ startupStatus === "invalid" ||
600
+ startupStatus === "coverage-incomplete";
589
601
  // 生产 source fidelity Pi 接线(AC-PROD-001/005):仅 frontend-implementation
590
602
  // 任务注入 reconcile executor + 独立 reviewer;ineligible 返回 undefined 不接线。
591
603
  // 主路径与 semantic-intake retry 路径共用同一接线,保证任意一条生产路径都实际
@@ -731,7 +743,9 @@ export async function advanceTaskLifecycle(input) {
731
743
  message: "structured requirement draft via semantic intake; projected managed source",
732
744
  });
733
745
  preparedContractThisCall = true;
734
- if (refreshManagedContractThisCall || input.fromDraftPath) {
746
+ if (startupRepairAction ||
747
+ refreshManagedContractThisCall ||
748
+ input.fromDraftPath) {
735
749
  refreshManagedContractThisCall = false;
736
750
  regenerateDagThisCall = true;
737
751
  }
@@ -825,7 +839,9 @@ export async function advanceTaskLifecycle(input) {
825
839
  break;
826
840
  }
827
841
  preparedContractThisCall = true;
828
- if (refreshManagedContractThisCall || input.fromDraftPath) {
842
+ if (startupRepairAction ||
843
+ refreshManagedContractThisCall ||
844
+ input.fromDraftPath) {
829
845
  // The old DAG and gate were bound to the pre-mutation contract.
830
846
  refreshManagedContractThisCall = false;
831
847
  regenerateDagThisCall = true;
@@ -1,9 +1,11 @@
1
- import { access, readFile } from "node:fs/promises";
1
+ import { access, readFile, readdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { getTaskDir, getTaskPaths, getTaskStatus, loadTaskConfig, } from "../../task/runtime.js";
4
4
  import { logicalRevisionForState, observeTaskContractState, } from "../../task/contract/index.js";
5
5
  import { assessDagRunLiveness, listAllDagRunEntries, readDagRunSpec, readDagRunState, } from "../../workflows/dag/lifecycle.js";
6
6
  import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
7
+ import { parseLedgerJson, validateRequirementLedger, } from "../../task/source-prepare/ledger.js";
8
+ import { REQUIREMENT_LEDGER_FILE_NAME } from "../../task/source-prepare/types.js";
7
9
  import { loadLifecycleRecord } from "./record.js";
8
10
  import { buildGateBinding, buildWriteSetGate, buildWriteSetGateDelta, controllerIdentityAnchor, hashCanonicalPayload, normalizeWriteSet, sha256Hex, } from "./gates.js";
9
11
  async function exists(filePath) {
@@ -47,6 +49,140 @@ async function taskExists(repoRoot, taskId) {
47
49
  const paths = getTaskPaths(repoRoot, taskId);
48
50
  return exists(paths.taskConfigPath);
49
51
  }
52
+ async function assessReferenceEvidence(referencesDir) {
53
+ try {
54
+ const entries = await readdir(referencesDir);
55
+ const importedPrdCount = entries.filter((name) => name.endsWith(".md")).length;
56
+ return importedPrdCount > 0
57
+ ? { status: "present", importedPrdCount }
58
+ : { status: "absent-or-empty", importedPrdCount: 0 };
59
+ }
60
+ catch (error) {
61
+ if (error &&
62
+ typeof error === "object" &&
63
+ "code" in error &&
64
+ error.code === "ENOENT") {
65
+ return { status: "absent-or-empty", importedPrdCount: 0 };
66
+ }
67
+ return {
68
+ status: "unknown",
69
+ importedPrdCount: 0,
70
+ error: error instanceof Error ? error.message : String(error),
71
+ };
72
+ }
73
+ }
74
+ function hasLedgerStructure(value) {
75
+ if (!value || typeof value !== "object")
76
+ return false;
77
+ const ledger = value;
78
+ return (typeof ledger.taskId === "string" &&
79
+ typeof ledger.inputDigest === "string" &&
80
+ typeof ledger.requirementSha256 === "string" &&
81
+ Array.isArray(ledger.sourcePaths) &&
82
+ Array.isArray(ledger.fragments) &&
83
+ Array.isArray(ledger.canonicalRequirements) &&
84
+ Boolean(ledger.stats && typeof ledger.stats === "object") &&
85
+ Array.isArray(ledger.unresolved) &&
86
+ Array.isArray(ledger.conflicts) &&
87
+ Array.isArray(ledger.sourceResolutions) &&
88
+ typeof ledger.schemaReconcilerVersion === "string" &&
89
+ typeof ledger.builtAt === "string");
90
+ }
91
+ async function assessSourceFidelityStartup(input) {
92
+ if (!input.applicable) {
93
+ return {
94
+ status: "not-applicable",
95
+ ledgerPath: input.ledgerPath,
96
+ diagnostics: [],
97
+ };
98
+ }
99
+ if (input.referenceEvidenceError) {
100
+ return {
101
+ status: "invalid",
102
+ ledgerPath: input.ledgerPath,
103
+ diagnostics: [
104
+ {
105
+ code: "SOURCE_FIDELITY_LEDGER_STALE",
106
+ message: `imported PRD reference evidence is unreadable: ${input.referenceEvidenceError}`,
107
+ },
108
+ ],
109
+ };
110
+ }
111
+ let raw;
112
+ try {
113
+ raw = await readFile(input.ledgerPath, "utf-8");
114
+ }
115
+ catch (error) {
116
+ const missing = !(await exists(input.ledgerPath));
117
+ return {
118
+ status: missing ? "missing" : "invalid",
119
+ ledgerPath: input.ledgerPath,
120
+ diagnostics: [
121
+ {
122
+ code: missing
123
+ ? "SOURCE_FIDELITY_LEDGER_MISSING"
124
+ : "SOURCE_FIDELITY_LEDGER_STALE",
125
+ message: missing
126
+ ? "managed frontend task has no requirement-ledger.json"
127
+ : `requirement ledger is unreadable: ${error instanceof Error ? error.message : String(error)}`,
128
+ },
129
+ ],
130
+ };
131
+ }
132
+ try {
133
+ const ledger = parseLedgerJson(raw);
134
+ if (!hasLedgerStructure(ledger)) {
135
+ return {
136
+ status: "invalid",
137
+ ledgerPath: input.ledgerPath,
138
+ diagnostics: [
139
+ {
140
+ code: "SOURCE_FIDELITY_LEDGER_STALE",
141
+ message: "requirement ledger is missing required structural fields",
142
+ },
143
+ ],
144
+ };
145
+ }
146
+ const nonDecisionErrors = validateRequirementLedger(ledger, {
147
+ allowedUnresolved: true,
148
+ allowedConflicts: true,
149
+ });
150
+ if (nonDecisionErrors.length > 0) {
151
+ const coverageCodes = new Set([
152
+ "SOURCE_FIDELITY_HIGH_RISK_UNCOVERED",
153
+ "SOURCE_FIDELITY_REQUIREMENT_NOT_COVERED",
154
+ ]);
155
+ return {
156
+ status: nonDecisionErrors.every((error) => coverageCodes.has(error.code))
157
+ ? "coverage-incomplete"
158
+ : "invalid",
159
+ ledgerPath: input.ledgerPath,
160
+ diagnostics: nonDecisionErrors,
161
+ };
162
+ }
163
+ const decisionErrors = validateRequirementLedger(ledger);
164
+ if (decisionErrors.length > 0) {
165
+ return {
166
+ status: "human-decision-blocked",
167
+ ledgerPath: input.ledgerPath,
168
+ diagnostics: decisionErrors,
169
+ };
170
+ }
171
+ return { status: "complete", ledgerPath: input.ledgerPath, diagnostics: [] };
172
+ }
173
+ catch (error) {
174
+ return {
175
+ status: "invalid",
176
+ ledgerPath: input.ledgerPath,
177
+ diagnostics: [
178
+ {
179
+ code: "SOURCE_FIDELITY_LEDGER_STALE",
180
+ message: error instanceof Error ? error.message : String(error),
181
+ },
182
+ ],
183
+ };
184
+ }
185
+ }
50
186
  /**
51
187
  * Explicit Task↔DAG association only:
52
188
  * - lifecycle.json recorded runAssociations
@@ -182,10 +318,12 @@ export async function observeTaskLifecycle(repoRoot, taskId) {
182
318
  const record = await loadLifecycleRecord(repoRoot, taskId);
183
319
  const contractState = await observeTaskContractState(repoRoot, taskId);
184
320
  let title;
321
+ let taskKind;
185
322
  let taskConfigAllowed = [];
186
323
  try {
187
324
  const config = await loadTaskConfig(repoRoot, taskId);
188
325
  title = config.title;
326
+ taskKind = config.taskKind;
189
327
  taskConfigAllowed = config.allowedPaths ?? [];
190
328
  }
191
329
  catch {
@@ -199,19 +337,22 @@ export async function observeTaskLifecycle(repoRoot, taskId) {
199
337
  ...(requirementReady ? [] : [requirementPath]),
200
338
  ...(constraintsReady ? [] : [constraintsPath]),
201
339
  ];
202
- let importedPrdCount = 0;
340
+ const contractManaged = contractState.effectiveStatus === "managed" || Boolean(contractState.ref);
203
341
  const referencesDir = path.join(paths.sourceDir, "references");
204
- if (await exists(referencesDir)) {
205
- // Count is best-effort; absence is fine.
206
- try {
207
- const { readdir } = await import("node:fs/promises");
208
- const entries = await readdir(referencesDir);
209
- importedPrdCount = entries.filter((name) => name.endsWith(".md")).length;
210
- }
211
- catch {
212
- importedPrdCount = 0;
213
- }
214
- }
342
+ const referenceEvidence = await assessReferenceEvidence(referencesDir);
343
+ const importedPrdCount = referenceEvidence.importedPrdCount;
344
+ const ledgerPath = path.join(paths.sourceDir, REQUIREMENT_LEDGER_FILE_NAME);
345
+ const sourceFidelityStartup = await assessSourceFidelityStartup({
346
+ ledgerPath,
347
+ applicable: contractManaged &&
348
+ taskKind === "frontend-implementation" &&
349
+ referenceEvidence.status !== "absent-or-empty",
350
+ referenceEvidenceError: referenceEvidence.status === "unknown"
351
+ ? referenceEvidence.error
352
+ : undefined,
353
+ });
354
+ const sourceFidelityReady = sourceFidelityStartup.status === "complete" ||
355
+ sourceFidelityStartup.status === "not-applicable";
215
356
  const dagDraftPath = paths.dagDraftPath;
216
357
  const dagDraftExists = await exists(dagDraftPath);
217
358
  let specHash = null;
@@ -285,7 +426,11 @@ export async function observeTaskLifecycle(repoRoot, taskId) {
285
426
  // Open gate only when draft is strict-validated and the current binding is
286
427
  // not already approved/rejected. Rejected same-digest gates stay closed
287
428
  // until contract/source/DAG/writeSet/controller binding changes.
288
- const gate = gateBound && strictValidated && !approvedCurrent && !rejectedCurrent
429
+ const gate = gateBound &&
430
+ strictValidated &&
431
+ sourceFidelityReady &&
432
+ !approvedCurrent &&
433
+ !rejectedCurrent
289
434
  ? buildWriteSetGate({ taskId, bound: gateBound, delta: gateDelta })
290
435
  : null;
291
436
  const gateOpen = Boolean(gate);
@@ -310,11 +455,10 @@ export async function observeTaskLifecycle(repoRoot, taskId) {
310
455
  activeRun.failureCategory === "needs-attention" ||
311
456
  activeRun.failureCategory === "suspected-stall" ||
312
457
  activeRun.failureCategory === "monitor-timeout"));
313
- const contractManaged = contractState.effectiveStatus === "managed" || Boolean(contractState.ref);
314
458
  const lifecycleState = deriveLifecycleState({
315
459
  exists: true,
316
460
  contractManaged,
317
- sourceReady: requirementReady,
461
+ sourceReady: requirementReady && sourceFidelityReady,
318
462
  dagValidated: strictValidated,
319
463
  gateOpen: Boolean(gate),
320
464
  activeRun,
@@ -352,6 +496,7 @@ export async function observeTaskLifecycle(repoRoot, taskId) {
352
496
  constraintsReady,
353
497
  importedPrdCount,
354
498
  missing,
499
+ sourceFidelityStartup,
355
500
  },
356
501
  dagDraft: {
357
502
  path: dagDraftPath,
@@ -375,7 +520,16 @@ export async function observeTaskLifecycle(repoRoot, taskId) {
375
520
  exists: closeoutExists || Boolean(record?.closeout),
376
521
  },
377
522
  completedTransitions: record?.completedTransitions ?? [],
378
- blockers: [],
523
+ blockers: sourceFidelityReady
524
+ ? []
525
+ : sourceFidelityStartup.diagnostics.map((diagnostic) => ({
526
+ code: diagnostic.code,
527
+ message: diagnostic.message,
528
+ details: {
529
+ startupStatus: sourceFidelityStartup.status,
530
+ ledgerPath: sourceFidelityStartup.ledgerPath,
531
+ },
532
+ })),
379
533
  warnings: [],
380
534
  next,
381
535
  };
@@ -46,6 +46,37 @@ export function planTransitions(input) {
46
46
  actions.push({ type: "recover-contract" });
47
47
  expectedTransitions.push("contract-recovered");
48
48
  }
49
+ const startupStatus = snapshot.source?.sourceFidelityStartup?.status;
50
+ const startupRepairDetected = startupStatus === "missing" ||
51
+ startupStatus === "invalid" ||
52
+ startupStatus === "coverage-incomplete";
53
+ if (startupRepairDetected && input.startupRepairAttempted) {
54
+ actions.push({ type: "stop", reason: "blocker" });
55
+ return {
56
+ actions,
57
+ gate: null,
58
+ blockers,
59
+ next: {
60
+ kind: "advance",
61
+ command: `loop-agent task advance ${snapshot.taskId} --json`,
62
+ },
63
+ expectedTransitions,
64
+ };
65
+ }
66
+ const startupRepairRequired = startupRepairDetected;
67
+ if (startupStatus === "human-decision-blocked") {
68
+ actions.push({ type: "stop", reason: "blocker" });
69
+ return {
70
+ actions,
71
+ gate: null,
72
+ blockers,
73
+ next: {
74
+ kind: "external-action",
75
+ description: "Requirement ledger requires an existing human source decision; preserve sourceResolutions, resolve the blocker, then advance again.",
76
+ },
77
+ expectedTransitions,
78
+ };
79
+ }
49
80
  const sourceReady = snapshot.source?.requirementReady === true;
50
81
  const contractManaged = snapshot.contract?.effectiveStatus === "managed" ||
51
82
  Boolean(snapshot.contract?.revision && snapshot.contract.revision > 0);
@@ -53,7 +84,9 @@ export function planTransitions(input) {
53
84
  // Prefer prepare once PRDs are on disk; re-import only when caller still
54
85
  // signals hasPrdInput (advance.ts latches this after one import per call).
55
86
  const shouldImportPrd = input.hasPrdInput;
56
- if (input.forcePrepareContract || input.refreshManagedContract) {
87
+ if (input.forcePrepareContract ||
88
+ input.refreshManagedContract ||
89
+ startupRepairRequired) {
57
90
  if (shouldImportPrd) {
58
91
  actions.push({ type: "import-prd" });
59
92
  expectedTransitions.push("prd-imported");
@@ -124,7 +157,8 @@ export function planTransitions(input) {
124
157
  const strictValidated = snapshot.dagDraft?.strictValidated === true;
125
158
  const dagRefreshRequired = Boolean(input.forceDagRefresh ||
126
159
  input.refreshManagedContract ||
127
- input.forcePrepareContract);
160
+ input.forcePrepareContract ||
161
+ startupRepairRequired);
128
162
  if (!dagExists || dagRefreshRequired) {
129
163
  actions.push({ type: "generate-dag" });
130
164
  expectedTransitions.push("dag-generated");
@@ -145,10 +179,11 @@ export function planTransitions(input) {
145
179
  snapshot.lifecycleState === "evidence-closed" ||
146
180
  snapshot.lifecycleState === "needs-attention" ||
147
181
  snapshot.lifecycleState === "awaiting-decision";
148
- const approvedForCurrent = postRunState || (input.approveGate && gate !== null);
182
+ const approveCurrentGate = input.approveGate && !dagRefreshRequired;
183
+ const approvedForCurrent = postRunState || (approveCurrentGate && gate !== null);
149
184
  if (!approvedForCurrent && (strictValidated || !dagExists || dagRefreshRequired)) {
150
185
  // Gate opens after generate+validate in the advance loop; plan includes open.
151
- if (!input.approveGate) {
186
+ if (!approveCurrentGate) {
152
187
  actions.push({ type: "open-write-set-gate" });
153
188
  expectedTransitions.push("write-set-gate-opened");
154
189
  if (input.dryRun) {
@@ -177,14 +212,14 @@ export function planTransitions(input) {
177
212
  (snapshot.lifecycleState === "run-failed" && !failedRecoverable) ||
178
213
  snapshot.lifecycleState === "evidence-closed";
179
214
  if (!alreadyTerminalRun) {
180
- if (input.approveGate && !postRunState) {
215
+ if (approveCurrentGate && !postRunState) {
181
216
  expectedTransitions.push("write-set-approved");
182
217
  actions.push({ type: "start-or-monitor-run" });
183
218
  expectedTransitions.push("dag-run-started", "dag-run-monitored");
184
219
  }
185
220
  else if (snapshot.lifecycleState === "running" ||
186
221
  snapshot.activeRun ||
187
- (input.approveGate && Boolean(snapshot.activeRun))) {
222
+ (approveCurrentGate && Boolean(snapshot.activeRun))) {
188
223
  actions.push({ type: "start-or-monitor-run" });
189
224
  expectedTransitions.push("dag-run-monitored");
190
225
  }
@@ -218,7 +253,7 @@ export function planTransitions(input) {
218
253
  actions,
219
254
  gate,
220
255
  blockers,
221
- next: deriveNext(snapshot, gate, input.approveGate),
256
+ next: deriveNext(snapshot, gate, approveCurrentGate),
222
257
  expectedTransitions,
223
258
  };
224
259
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.39.0-beta.13",
4
- "gitSha": "5d72ad152e1f52f7001782af509a53f11c330aa4",
5
- "builtAt": "2026-08-29T13:28:15.172Z"
3
+ "version": "0.39.0-beta.15",
4
+ "gitSha": "188a3e304d6301cac9e02e309e47fae29bb05be2",
5
+ "builtAt": "2026-08-30T12:30:20.874Z"
6
6
  }