pi-crew 0.9.68 → 0.10.2

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.
Files changed (253) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/NOTICE.md +21 -0
  3. package/README.md +44 -2
  4. package/agents/analyst.md +1 -1
  5. package/agents/cold-verifier.md +3 -1
  6. package/agents/critic.md +1 -1
  7. package/agents/executor.md +1 -1
  8. package/agents/explorer.md +1 -1
  9. package/agents/planner.md +1 -1
  10. package/agents/reviewer.md +1 -1
  11. package/agents/security-reviewer.md +1 -1
  12. package/agents/test-engineer.md +1 -1
  13. package/agents/verifier.md +1 -1
  14. package/agents/writer.md +1 -1
  15. package/dist/index.mjs +68113 -60774
  16. package/docs/README.md +2 -0
  17. package/docs/actions-reference.md +31 -0
  18. package/docs/commands-reference.md +17 -6
  19. package/docs/resource-formats.md +13 -0
  20. package/package.json +4 -2
  21. package/schema.json +503 -91
  22. package/scripts/resource-sampler.mjs +36 -2
  23. package/skills/requirements-to-task-packet/SKILL.md +26 -0
  24. package/skills/widget-rendering/SKILL.md +7 -7
  25. package/src/agents/agent-config.ts +2 -1
  26. package/src/agents/discover-agents.ts +23 -14
  27. package/src/config/config-merge.ts +183 -0
  28. package/src/config/config-validation.ts +687 -0
  29. package/src/config/config.ts +22 -864
  30. package/src/config/defaults.ts +43 -2
  31. package/src/config/drift-detector.ts +1 -1
  32. package/src/config/env-vars.ts +691 -0
  33. package/src/config/role-tools.ts +11 -9
  34. package/src/config/sanitize-project-config.ts +172 -0
  35. package/src/config/types.ts +49 -1
  36. package/src/extension/async-notifier.ts +25 -2
  37. package/src/extension/crew-cleanup.ts +13 -0
  38. package/src/extension/crew-vibes/config.ts +2 -1
  39. package/src/extension/crew-vibes/footer.ts +19 -0
  40. package/src/extension/crew-vibes/index.ts +11 -1
  41. package/src/extension/plan-orchestrate.ts +132 -0
  42. package/src/extension/register.ts +8 -0
  43. package/src/extension/registration/command-registration.ts +1 -0
  44. package/src/extension/registration/commands/dashboard.ts +158 -0
  45. package/src/extension/registration/commands/index.ts +35 -0
  46. package/src/extension/registration/commands/manage.ts +303 -0
  47. package/src/extension/registration/commands/run.ts +228 -0
  48. package/src/extension/registration/commands/shared.ts +639 -0
  49. package/src/extension/registration/commands/status.ts +60 -0
  50. package/src/extension/registration/commands.ts +13 -1224
  51. package/src/extension/registration/foreground-run-controller.ts +10 -2
  52. package/src/extension/registration/lifecycle-handlers.ts +178 -17
  53. package/src/extension/registration/runtime-cleanup.ts +23 -5
  54. package/src/extension/registration/subagent-tools.ts +218 -9
  55. package/src/extension/registration/team-tool.ts +5 -1
  56. package/src/extension/registration/ui.ts +5 -4
  57. package/src/extension/rpc-hmac.ts +5 -3
  58. package/src/extension/team-tool/api/heartbeat.ts +47 -10
  59. package/src/extension/team-tool/api/plan-approval.ts +9 -0
  60. package/src/extension/team-tool/api/task-claims.ts +109 -40
  61. package/src/extension/team-tool/cancel.ts +84 -50
  62. package/src/extension/team-tool/dispatch/index.ts +1 -0
  63. package/src/extension/team-tool/dispatch/run.ts +4 -1
  64. package/src/extension/team-tool/doctor.ts +103 -1
  65. package/src/extension/team-tool/orchestrate.ts +66 -1
  66. package/src/extension/team-tool/plans.ts +192 -0
  67. package/src/extension/team-tool/respond.ts +197 -65
  68. package/src/extension/team-tool/run-deadline.ts +35 -3
  69. package/src/extension/team-tool/run-intent.ts +63 -0
  70. package/src/extension/team-tool/run.ts +74 -20
  71. package/src/extension/team-tool/status.ts +84 -26
  72. package/src/extension/team-tool.ts +11 -2
  73. package/src/hooks/registry.ts +1 -6
  74. package/src/i18n.ts +9 -0
  75. package/src/prompt/prompt-runtime.ts +521 -2
  76. package/src/prompt/worker-events-channel.ts +173 -0
  77. package/src/runtime/README.md +8 -8
  78. package/src/runtime/async-runner.ts +7 -3
  79. package/src/runtime/background-runner.ts +42 -14
  80. package/src/runtime/broker/broker-issuer.ts +9 -2
  81. package/src/runtime/broker/crew-broker-tokens.ts +43 -6
  82. package/src/runtime/broker/crew-broker.ts +838 -10
  83. package/src/runtime/broker/wait-status-cache.ts +157 -0
  84. package/src/runtime/budget-enforcement.ts +281 -0
  85. package/src/runtime/child-pi/child-pi-constants.ts +8 -0
  86. package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
  87. package/src/runtime/child-pi/child-pi-streams.ts +21 -1
  88. package/src/runtime/child-pi/child-pi-timers.ts +324 -0
  89. package/src/runtime/child-pi/child-pi.ts +97 -201
  90. package/src/runtime/child-pi/mock-fixtures.ts +16 -2
  91. package/src/runtime/crew-agent-records.ts +259 -14
  92. package/src/runtime/delegate-spawn.ts +148 -0
  93. package/src/runtime/detached-run-results.ts +90 -0
  94. package/src/runtime/deterministic-ast.ts +2 -1
  95. package/src/runtime/dispatch-batch.ts +945 -0
  96. package/src/runtime/finalize-run.ts +557 -0
  97. package/src/runtime/goal-workflow/adaptive-plan.ts +116 -15
  98. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
  99. package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
  100. package/src/runtime/group-join.ts +11 -125
  101. package/src/runtime/live-session/live-session-runtime.ts +26 -1
  102. package/src/runtime/merge-gate.ts +32 -10
  103. package/src/runtime/merge-loop.ts +130 -0
  104. package/src/runtime/model/model-budget-summary.ts +53 -0
  105. package/src/runtime/model/model-fallback.ts +36 -2
  106. package/src/runtime/model/pi-args.ts +10 -0
  107. package/src/runtime/model/provider-extensions.ts +10 -0
  108. package/src/runtime/orphan-worker-registry.ts +1 -1
  109. package/src/runtime/output/output-validator.ts +45 -0
  110. package/src/runtime/parent-guard.ts +3 -1
  111. package/src/runtime/peer-dep.ts +2 -1
  112. package/src/runtime/per-write-validator.ts +0 -5
  113. package/src/runtime/pi-spawn.ts +61 -15
  114. package/src/runtime/plan-approval.ts +125 -0
  115. package/src/runtime/plan-replan.ts +151 -0
  116. package/src/runtime/process-status.ts +16 -1
  117. package/src/runtime/recovery/checkpoint.ts +0 -18
  118. package/src/runtime/recovery/crash-recovery.ts +111 -46
  119. package/src/runtime/run-tracker.ts +77 -10
  120. package/src/runtime/scheduler-context.ts +98 -0
  121. package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
  122. package/src/runtime/scheduling/global-worker-cap.ts +2 -1
  123. package/src/runtime/scheduling/nested-slots.ts +70 -0
  124. package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
  125. package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
  126. package/src/runtime/settings-store.ts +219 -0
  127. package/src/runtime/spawn-policy.ts +217 -0
  128. package/src/runtime/stale-reconciler.ts +87 -6
  129. package/src/runtime/subagent-manager.ts +25 -1
  130. package/src/runtime/task-output-context.ts +230 -9
  131. package/src/runtime/task-packet.ts +23 -1
  132. package/src/runtime/task-runner/child-executor.ts +106 -7
  133. package/src/runtime/task-runner/post-execution.ts +125 -1
  134. package/src/runtime/task-runner/pre-execution.ts +39 -1
  135. package/src/runtime/task-runner/prompt-builder.ts +51 -1
  136. package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
  137. package/src/runtime/task-runner/spec-evidence.ts +403 -0
  138. package/src/runtime/task-runner/state-helpers.ts +26 -24
  139. package/src/runtime/task-runner.ts +11 -0
  140. package/src/runtime/team-runner.ts +132 -1673
  141. package/src/runtime/verification/spec-sandbox.ts +255 -0
  142. package/src/runtime/verification/verification-gates.ts +3 -2
  143. package/src/runtime/verification/verification-worktree.ts +2 -1
  144. package/src/runtime/workflow-phase-advance.ts +100 -0
  145. package/src/runtime/workspace-tree.ts +9 -0
  146. package/src/schema/config-schema.ts +66 -26
  147. package/src/schema/sensitive-config-paths.ts +64 -0
  148. package/src/schema/team-tool-schema.ts +13 -3
  149. package/src/state/README.md +4 -10
  150. package/src/state/atomic-write.ts +20 -3
  151. package/src/state/contracts.ts +38 -0
  152. package/src/state/coordination/mailbox.ts +12 -2
  153. package/src/state/event-log/cursor.ts +223 -0
  154. package/src/state/event-log/event-log-rotation.ts +12 -4
  155. package/src/state/event-log/event-log.ts +152 -369
  156. package/src/state/event-log/sequence-cache.ts +373 -0
  157. package/src/state/event-log/worker-atomic-writer.ts +2 -1
  158. package/src/state/stores/active-run-registry.ts +3 -2
  159. package/src/state/stores/manifest-io.ts +237 -0
  160. package/src/state/stores/ownership-map.ts +162 -0
  161. package/src/state/stores/plan-store.ts +241 -0
  162. package/src/state/stores/run-cache.ts +0 -90
  163. package/src/state/stores/spec-store.ts +189 -0
  164. package/src/state/stores/state-store.ts +139 -232
  165. package/src/state/types.ts +199 -0
  166. package/src/ui/dashboard-panes/plan-pane.ts +136 -0
  167. package/src/ui/dashboard-panes/progress-pane.ts +6 -0
  168. package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
  169. package/src/ui/dock-footer.ts +49 -0
  170. package/src/ui/heartbeat-aggregator.ts +9 -1
  171. package/src/ui/inline-panel/agent-pane.ts +375 -0
  172. package/src/ui/inline-panel/agent-transcript.ts +338 -0
  173. package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
  174. package/src/ui/inline-panel/crew-editor.ts +192 -0
  175. package/src/ui/inline-panel/index.ts +290 -0
  176. package/src/ui/inline-panel/panel-rows.ts +37 -0
  177. package/src/ui/inline-panel/panel-selection.ts +157 -0
  178. package/src/ui/inline-panel/panel-store.ts +111 -0
  179. package/src/ui/inline-panel/view-session-store.ts +36 -0
  180. package/src/ui/keybinding-map.ts +54 -13
  181. package/src/ui/pi-ui-compat.ts +9 -0
  182. package/src/ui/powerbar-publisher.ts +52 -1
  183. package/src/ui/run-dashboard.ts +31 -5
  184. package/src/ui/run-snapshot-cache.ts +57 -30
  185. package/src/ui/snapshot-types.ts +6 -1
  186. package/src/ui/widget/index.ts +176 -22
  187. package/src/ui/widget/task-list.ts +198 -0
  188. package/src/ui/widget/widget-formatters.ts +240 -4
  189. package/src/ui/widget/widget-renderer.ts +243 -38
  190. package/src/ui/widget/widget-types.ts +11 -0
  191. package/src/utils/child-process-shield.ts +106 -0
  192. package/src/utils/file-coalescer.ts +0 -4
  193. package/src/utils/fs-errno.ts +66 -0
  194. package/src/utils/fs-watch.ts +1 -1
  195. package/src/utils/internal-error.ts +3 -1
  196. package/src/utils/paths.ts +11 -3
  197. package/src/utils/redaction.ts +7 -0
  198. package/src/utils/safe-abort.ts +45 -0
  199. package/src/utils/task-name-generator.ts +1 -8
  200. package/src/workflows/discover-workflows.ts +20 -2
  201. package/src/workflows/validate-workflow.ts +7 -1
  202. package/src/workflows/workflow-config.ts +17 -0
  203. package/src/workflows/workflow-serializer.ts +3 -0
  204. package/src/worktree/worktree-manager.ts +22 -0
  205. package/workflows/default.workflow.md +36 -26
  206. package/workflows/strict-fast-fix.workflow.md +26 -0
  207. package/src/agents/agent-search.ts +0 -98
  208. package/src/benchmark/benchmark-runner.ts +0 -313
  209. package/src/benchmark/feedback-loop.ts +0 -73
  210. package/src/config/resilient-parser.ts +0 -117
  211. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  212. package/src/extension/result-watcher.ts +0 -139
  213. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  214. package/src/observability/metric-retention.ts +0 -64
  215. package/src/runtime/compaction/compaction-summary.ts +0 -278
  216. package/src/runtime/errors/crew-errors.ts +0 -162
  217. package/src/runtime/live-session/intercom-bridge.ts +0 -187
  218. package/src/runtime/loop-gates.ts +0 -128
  219. package/src/runtime/metric-parser.ts +0 -36
  220. package/src/runtime/output/stream-preview.ts +0 -184
  221. package/src/runtime/output/tool-progress.ts +0 -278
  222. package/src/runtime/phase-tracker.ts +0 -385
  223. package/src/runtime/pipeline-runner.ts +0 -523
  224. package/src/runtime/process/process-lifecycle.ts +0 -491
  225. package/src/runtime/recovery/retry-runner.ts +0 -330
  226. package/src/runtime/run-drift.ts +0 -219
  227. package/src/runtime/task-quality.ts +0 -199
  228. package/src/runtime/task-runner/run-projection.ts +0 -128
  229. package/src/runtime/verification/post-checks.ts +0 -142
  230. package/src/state/coordination/schedule.ts +0 -166
  231. package/src/state/event-log/jsonl-writer.ts +0 -115
  232. package/src/state/hook-instinct-bridge.ts +0 -94
  233. package/src/state/hook-integrations.ts +0 -51
  234. package/src/state/session-state-map.ts +0 -51
  235. package/src/state/stores/blob-store.ts +0 -308
  236. package/src/state/stores/instinct-store.ts +0 -275
  237. package/src/state/stores/observation-store.ts +0 -176
  238. package/src/state/tiered-eval.ts +0 -480
  239. package/src/state/types-eval.ts +0 -58
  240. package/src/tools/safe-bash-extension.ts +0 -54
  241. package/src/tools/safe-bash.ts +0 -505
  242. package/src/ui/agent-management-overlay.ts +0 -160
  243. package/src/ui/crew-footer.ts +0 -102
  244. package/src/ui/crew-select-list.ts +0 -114
  245. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  246. package/src/ui/transcript-entries.ts +0 -256
  247. package/src/utils/conflict-detect.ts +0 -721
  248. package/src/utils/fingerprint.ts +0 -180
  249. package/src/utils/gh-protocol.ts +0 -556
  250. package/src/utils/project-detector.ts +0 -160
  251. package/src/utils/sse-parser.ts +0 -131
  252. package/src/workflows/cost-estimator.ts +0 -34
  253. package/src/workflows/intermediate-store.ts +0 -166
@@ -14,11 +14,15 @@
14
14
  //
15
15
  // `__test__parseAdaptivePlan` and `__test__repairAdaptivePlan` are re-exported
16
16
  // from team-runner.ts so existing test imports keep working.
17
+
18
+ import { randomUUID } from "node:crypto";
17
19
  import * as fs from "node:fs";
20
+ import { getCrewEnv } from "../../config/env-vars.ts";
18
21
  import { appendEventAsync, appendEventFireAndForget } from "../../state/event-log/event-log.ts";
19
22
  import { writeArtifact } from "../../state/stores/artifact-store.ts";
20
- import { saveRunManifestAsync } from "../../state/stores/state-store.ts";
21
- import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
23
+ import { appendPlanRevision, getCurrentPlanRecord } from "../../state/stores/plan-store.ts";
24
+ import { saveRunManifestAsync, saveRunTasksAsync } from "../../state/stores/state-store.ts";
25
+ import type { PlanRecord, TeamRunManifest, TeamTaskState } from "../../state/types.ts";
22
26
  import type { TeamConfig } from "../../teams/team-config.ts";
23
27
  import type { WorkflowConfig, WorkflowStep } from "../../workflows/workflow-config.ts";
24
28
  import { refreshTaskGraphQueues } from "../scheduling/task-graph-scheduler.ts";
@@ -39,7 +43,15 @@ export interface AdaptivePlan {
39
43
  phases: AdaptivePlanPhase[];
40
44
  }
41
45
 
42
- const MAX_ADAPTIVE_TASKS = 12;
46
+ /**
47
+ * T2/R4 (ADR-4 §5): the cap is PER-PHASE, not global. Pre-v2 this was a
48
+ * global flatten cap (MAX_ADAPTIVE_TASKS = 12) applied across the whole plan —
49
+ * a 3-phase plan may now schedule up to 3 x this across its life, bounded and
50
+ * visible per phase in the persisted PlanRecord. Deliberately a module
51
+ * constant, NOT a config key (the `adaptive` config section belongs to
52
+ * T3/WP-5 — review finding F3 of the ADR round).
53
+ */
54
+ const ADAPTIVE_MAX_TASKS_PER_PHASE = 12;
43
55
 
44
56
  export function slug(value: string): string {
45
57
  return (
@@ -124,7 +136,6 @@ export function parseAdaptivePlan(text: string, allowedRoles: string[]): Adaptiv
124
136
  if (!phasesRaw) return undefined;
125
137
  const allowed = new Set(allowedRoles);
126
138
  const phases: AdaptivePlanPhase[] = [];
127
- let total = 0;
128
139
  for (const [phaseIndex, phaseRaw] of phasesRaw.entries()) {
129
140
  if (!phaseRaw || typeof phaseRaw !== "object" || Array.isArray(phaseRaw)) return undefined;
130
141
  const phaseObj = phaseRaw as { name?: unknown; tasks?: unknown };
@@ -139,13 +150,12 @@ export function parseAdaptivePlan(text: string, allowedRoles: string[]): Adaptiv
139
150
  };
140
151
  if (typeof taskObj.role !== "string" || !allowed.has(taskObj.role)) return undefined;
141
152
  if (typeof taskObj.task !== "string" || !taskObj.task.trim()) return undefined;
142
- if (total >= MAX_ADAPTIVE_TASKS) return undefined;
153
+ if (tasks.length >= ADAPTIVE_MAX_TASKS_PER_PHASE) return undefined; // per-phase (ADR-4 §5)
143
154
  tasks.push({
144
155
  role: taskObj.role,
145
156
  title: typeof taskObj.title === "string" ? taskObj.title : undefined,
146
157
  task: taskObj.task.trim(),
147
158
  });
148
- total++;
149
159
  }
150
160
  phases.push({
151
161
  name: typeof phaseObj.name === "string" && phaseObj.name.trim() ? phaseObj.name.trim() : `phase-${phaseIndex + 1}`,
@@ -289,7 +299,6 @@ export function repairAdaptivePlan(text: string, allowedRoles: string[]): { plan
289
299
  if (!phasesRaw) return { repaired: false, reason: "missing-phases" };
290
300
  const allowed = new Set(allowedRoles);
291
301
  const phases: AdaptivePlanPhase[] = [];
292
- let total = 0;
293
302
  let repaired = salvageUsed || raw !== closeResult.text;
294
303
  for (const [phaseIndex, phaseRaw] of phasesRaw.entries()) {
295
304
  if (!phaseRaw || typeof phaseRaw !== "object" || Array.isArray(phaseRaw)) continue;
@@ -297,7 +306,8 @@ export function repairAdaptivePlan(text: string, allowedRoles: string[]): { plan
297
306
  if (!Array.isArray(phaseObj.tasks)) continue;
298
307
  const tasks: AdaptivePlanTask[] = [];
299
308
  for (const taskRaw of phaseObj.tasks) {
300
- if (total >= MAX_ADAPTIVE_TASKS) {
309
+ if (tasks.length >= ADAPTIVE_MAX_TASKS_PER_PHASE) {
310
+ // Per-phase cap (ADR-4 §5): truncate THIS phase, keep later phases.
301
311
  repaired = true;
302
312
  break;
303
313
  }
@@ -321,14 +331,12 @@ export function repairAdaptivePlan(text: string, allowedRoles: string[]): { plan
321
331
  title: typeof taskObj.title === "string" ? taskObj.title : undefined,
322
332
  task: taskText,
323
333
  });
324
- total++;
325
334
  }
326
335
  if (tasks.length)
327
336
  phases.push({
328
337
  name: typeof phaseObj.name === "string" && phaseObj.name.trim() ? phaseObj.name.trim() : `phase-${phaseIndex + 1}`,
329
338
  tasks,
330
339
  });
331
- if (total >= MAX_ADAPTIVE_TASKS) break;
332
340
  }
333
341
  return phases.length
334
342
  ? {
@@ -360,6 +368,11 @@ export interface InjectAdaptivePlanInput {
360
368
  tasks: TeamTaskState[];
361
369
  workflow: WorkflowConfig;
362
370
  team: TeamConfig;
371
+ /** False for scaffold (dry-run) runs: the planner never executes, so there
372
+ * is no plan to inject — and no plan to be missing. Reports neutral
373
+ * (neither injected nor missing) so the preview completes instead of
374
+ * blocking on a plan a dry-run can never produce. */
375
+ executeWorkers?: boolean;
363
376
  }
364
377
 
365
378
  export interface InjectAdaptivePlanResult {
@@ -369,8 +382,18 @@ export interface InjectAdaptivePlanResult {
369
382
  missingPlan: boolean;
370
383
  }
371
384
 
385
+ /**
386
+ * True for workflows whose `assess` step emits a plan the runtime injects as
387
+ * concrete tasks. The `implementation` workflow is adaptive by name (historic
388
+ * hard-code, kept for backward compat); any other workflow opts in via
389
+ * frontmatter `adaptive: true`.
390
+ */
391
+ export function isAdaptiveWorkflow(workflow: WorkflowConfig): boolean {
392
+ return workflow.adaptive === true || workflow.name === "implementation";
393
+ }
394
+
372
395
  export async function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput): Promise<InjectAdaptivePlanResult> {
373
- if (input.workflow.name !== "implementation")
396
+ if (!isAdaptiveWorkflow(input.workflow) || input.executeWorkers === false)
374
397
  return {
375
398
  tasks: input.tasks,
376
399
  workflow: input.workflow,
@@ -409,6 +432,9 @@ export async function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput):
409
432
  };
410
433
  }
411
434
  const assessTask = completedAssess;
435
+ // Review R5: manifest variant tracked through the repair path — the plan
436
+ // save below must not clobber the repair artifact descriptor.
437
+ let manifestBase = input.manifest;
412
438
  const resultPath = completedAssess.resultArtifact.path;
413
439
  let text = "";
414
440
  try {
@@ -431,7 +457,7 @@ export async function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput):
431
457
  let plan = parseAdaptivePlan(text, allowedRoles);
432
458
  if (!plan) {
433
459
  const repair =
434
- process.env.PI_CREW_ADAPTIVE_REPAIR === "0" || process.env.PI_TEAMS_ADAPTIVE_REPAIR === "0"
460
+ getCrewEnv("PI_CREW_ADAPTIVE_REPAIR") === "0" || getCrewEnv("PI_TEAMS_ADAPTIVE_REPAIR") === "0"
435
461
  ? { repaired: false, reason: "disabled" }
436
462
  : repairAdaptivePlan(text, allowedRoles);
437
463
  if (repair.plan) {
@@ -442,11 +468,12 @@ export async function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput):
442
468
  producer: assessTask.id,
443
469
  content: `${JSON.stringify({ reason: repair.reason, phases: repair.plan.phases.map((phase) => ({ name: phase.name, count: phase.tasks.length, roles: phase.tasks.map((task) => task.role) })) }, null, 2)}\n`,
444
470
  });
445
- await saveRunManifestAsync({
471
+ manifestBase = {
446
472
  ...input.manifest,
447
473
  updatedAt: new Date().toISOString(),
448
474
  artifacts: [...input.manifest.artifacts, repairArtifact],
449
- });
475
+ };
476
+ await saveRunManifestAsync(manifestBase);
450
477
  appendEventFireAndForget(input.manifest.eventsPath, {
451
478
  type: "adaptive.plan_repaired",
452
479
  runId: input.manifest.runId,
@@ -478,14 +505,41 @@ export async function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput):
478
505
  }
479
506
  const steps: WorkflowStep[] = [];
480
507
  const tasks: TeamTaskState[] = [];
508
+ // T2/R4 (ADR-4 §6 producer 2): item ids are stable across revisions —
509
+ // phase/task POSITION based, independent of role renames.
510
+ const planPhases: PlanRecord["phases"] = [];
511
+ const planItems: PlanRecord["items"] = [];
481
512
  let previousStepIds = ["assess"];
482
513
  let counter = 0;
483
514
  for (const [phaseIndex, phase] of plan.phases.entries()) {
484
515
  const currentStepIds: string[] = [];
516
+ const phaseItemIds: string[] = [];
485
517
  for (const [taskIndex, planned] of phase.tasks.entries()) {
486
518
  counter++;
519
+ const itemId = `adaptive-p${phaseIndex + 1}-t${taskIndex + 1}`;
487
520
  const stepId = `adaptive-${phaseIndex + 1}-${taskIndex + 1}-${slug(planned.role)}`;
488
521
  const taskId = `adaptive-${String(counter).padStart(2, "0")}-${slug(planned.role)}`;
522
+ // Same display contract as static workflow steps: the planner's
523
+ // title (or the task text's first line) names the task; the full
524
+ // task text is the description for detail surfaces. Never the bare
525
+ // stepId — the task list is the plan, not an id roster.
526
+ const firstLine =
527
+ planned.task
528
+ .split("\n")
529
+ .map((line) => line.trim())
530
+ .filter(Boolean)
531
+ .find((line) => !line.startsWith("#")) ?? "";
532
+ const title = (planned.title ?? firstLine ?? stepId).slice(0, 120);
533
+ phaseItemIds.push(itemId);
534
+ planItems.push({
535
+ id: itemId,
536
+ ref: stepId,
537
+ title,
538
+ taskIds: [],
539
+ specIds: [],
540
+ acceptance: [],
541
+ status: "pending",
542
+ });
489
543
  steps.push({
490
544
  id: stepId,
491
545
  role: planned.role,
@@ -499,11 +553,13 @@ export async function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput):
499
553
  stepId,
500
554
  role: planned.role,
501
555
  agent: input.team.roles.find((role) => role.name === planned.role)?.agent ?? planned.role,
502
- title: planned.title ?? stepId,
556
+ title,
557
+ description: planned.task !== title ? planned.task : undefined,
503
558
  status: "queued",
504
559
  dependsOn: previousStepIds,
505
560
  cwd: input.manifest.cwd,
506
561
  adaptive: { phase: phase.name, task: planned.task },
562
+ planItem: itemId,
507
563
  graph: {
508
564
  taskId,
509
565
  dependencies: previousStepIds,
@@ -513,6 +569,12 @@ export async function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput):
513
569
  });
514
570
  currentStepIds.push(stepId);
515
571
  }
572
+ planPhases.push({
573
+ id: `adaptive-phase-${phaseIndex + 1}`,
574
+ title: phase.name,
575
+ itemIds: phaseItemIds,
576
+ status: "pending",
577
+ });
516
578
  previousStepIds = currentStepIds;
517
579
  }
518
580
  const dependencyTaskIdByStep = new Map<string, string>([
@@ -531,6 +593,45 @@ export async function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput):
531
593
  : task.graph,
532
594
  }));
533
595
  const allTasks = refreshTaskGraphQueues([...input.tasks, ...withGraph]);
596
+ // T2/R4 (ADR-4 §2/§6): persist the PlanRecord + manifest pointer (dual-write;
597
+ // crash between the two is benign — getCurrentPlanRecord falls back to the
598
+ // highest version). Producers never set taskIds (single-writer rule §3).
599
+ const planRecord: PlanRecord = {
600
+ id: randomUUID(),
601
+ runId: input.manifest.runId,
602
+ version: 1,
603
+ title: `Adaptive plan (${plan.phases.length} phase(s))`,
604
+ phases: planPhases,
605
+ items: planItems,
606
+ createdAt: new Date().toISOString(),
607
+ authorTaskId: assessTask.id,
608
+ };
609
+ // Review R1 (P1, crash-window): if a record ALREADY exists (the append
610
+ // survived a kill between the plans.json write and the tasks save below),
611
+ // REUSE it instead of re-appending — the fresh randomUUID would throw
612
+ // lineage-break on resume and permanently brick the run. Task/item ids are
613
+ // position-deterministic from the SAME parsed artifact, so the rebuilt
614
+ // linkage matches the surviving record exactly.
615
+ const existingRecord = getCurrentPlanRecord(input.manifest);
616
+ if (!existingRecord) appendPlanRevision(input.manifest, planRecord);
617
+ // Review round-2 N1: on the crash-window reuse path the pointer MUST name
618
+ // the SURVIVING record — the fresh planRecord was never persisted, so its
619
+ // randomUUID would dangle (masked today by the highest-version fallback,
620
+ // but it violates ADR-4 §2's pointer invariant).
621
+ const planPointer = existingRecord
622
+ ? { id: existingRecord.id, version: existingRecord.version }
623
+ : { id: planRecord.id, version: planRecord.version };
624
+ await saveRunManifestAsync({
625
+ ...manifestBase,
626
+ updatedAt: new Date().toISOString(),
627
+ plan: planPointer,
628
+ });
629
+ // FIX (T2/B2 case e, pre-existing): the injected tasks MUST be persisted at
630
+ // injection time. mergeUnitResult reloads tasks.json as the merge base and
631
+ // mergeTaskUpdatesPreservingTerminal DROPS updates for ids absent from the
632
+ // base — an injection that only lived in memory had its completed adaptive
633
+ // tasks silently erased from disk at the first merge.
634
+ await saveRunTasksAsync(manifestBase, allTasks);
534
635
  await appendEventAsync(input.manifest.eventsPath, {
535
636
  type: "adaptive.plan_injected",
536
637
  runId: input.manifest.runId,
@@ -20,11 +20,13 @@
20
20
  import { readFileSync } from "node:fs";
21
21
  import { join } from "node:path";
22
22
  import { transformSync } from "esbuild";
23
+ import { getCrewEnv } from "../../config/env-vars.ts";
23
24
  import { appendEvent } from "../../state/event-log/event-log.ts";
24
25
  import { writeArtifact } from "../../state/stores/artifact-store.ts";
25
26
  import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
26
27
  import { logInternalError } from "../../utils/internal-error.ts";
27
28
  import { packageRoot, projectCrewRoot, userPiRoot } from "../../utils/paths.ts";
29
+ import { safeAbort } from "../../utils/safe-abort.ts";
28
30
  import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
29
31
  import type { DynamicWorkflowConfig } from "../../workflows/workflow-config.ts";
30
32
  import { assertDeterministicScript, isDeterminismCheckEnabled } from "../deterministic-ast.ts";
@@ -151,7 +153,7 @@ export async function runDynamicWorkflow(input: RunDynamicWorkflowInput): Promis
151
153
  // require, fs, child_process). Cloning a hostile repo and running its workflow = arbitrary
152
154
  // code execution. Default-deny project workflows unless the user explicitly opts in via
153
155
  // PI_CREW_TRUST_PROJECT_DWF=1. Builtin and user workflows proceed without restriction.
154
- if (workflow.source === "project" && process.env.PI_CREW_TRUST_PROJECT_DWF !== "1") {
156
+ if (workflow.source === "project" && getCrewEnv("PI_CREW_TRUST_PROJECT_DWF") !== "1") {
155
157
  appendEvent(eventsPath, {
156
158
  type: "dwf.trust_denied",
157
159
  runId: manifest.runId,
@@ -223,14 +225,17 @@ export async function runDynamicWorkflow(input: RunDynamicWorkflowInput): Promis
223
225
  // it does NOT kill the script. Promise.race with a hard timeout at least returns an
224
226
  // error so the runner doesn't hang. The spawned child process is leaked, but the
225
227
  // dynamic-workflow returns failure promptly. (v1.5: use Worker threads to actually kill.)
226
- const SCRIPT_TIMEOUT_MS = Number.parseInt(process.env.PI_CREW_DWF_SCRIPT_TIMEOUT_MS ?? "", 10) || 1_800_000; // 30 min default (was 10 min; raised for distill-dwf + similar long pipelines)
228
+ const SCRIPT_TIMEOUT_MS = Number.parseInt(getCrewEnv("PI_CREW_DWF_SCRIPT_TIMEOUT_MS") ?? "", 10) || 1_800_000; // 30 min default (was 10 min; raised for distill-dwf + similar long pipelines)
227
229
  let timeoutHandle: NodeJS.Timeout | undefined;
228
230
  const timeoutPromise = new Promise<never>((_, reject) => {
229
231
  timeoutHandle = setTimeout(() => {
230
232
  // ERR-1: abort the timeout controller so the abort propagates to
231
233
  // runChildPi (via the combined signal passed to makeWorkflowCtx),
232
234
  // which kills spawned children instead of letting them run.
233
- timeoutController.abort();
235
+ // safeAbort: the combined signal may feed a signal-spawned child
236
+ // (abortChildProcess AbortError would otherwise reach the
237
+ // uncaughtException handler from this timer callback).
238
+ safeAbort(timeoutController, "dwf-script-timeout");
234
239
  reject(
235
240
  new Error(
236
241
  `Dynamic workflow script timed out after ${SCRIPT_TIMEOUT_MS}ms. The script may have spawned a child process that did not exit. Check for spawn/exec calls without proper stdio handling.`,
@@ -6,7 +6,7 @@
6
6
  * goal loop owns OUTER state) + §0c C10 (hardening: assertSafePathId + UUID goalId).
7
7
  *
8
8
  * Stores GoalLoopState as atomic JSON at <crewRoot>/state/goals/<goalId>.json.
9
- * Modeled on ScheduleStore (state/coordination/schedule.ts:86) but with atomicWriteJson +
9
+ * Modeled on the (removed in Phase 1.7) ScheduleStore coordination pattern but with atomicWriteJson +
10
10
  * path-traversal defense (assertSafePathId on every public method).
11
11
  *
12
12
  * Per §0c C2: budget lives here (budgetUsed accumulates collectRunMetrics across turns);
@@ -3,7 +3,7 @@ import { appendMailboxMessage, findMailboxMessageByRequestId, readDeliveryState
3
3
  import { appendEvent } from "../state/event-log/event-log.ts";
4
4
  import { writeArtifact } from "../state/stores/artifact-store.ts";
5
5
  import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../state/types.ts";
6
- import { aggregateTaskOutputs } from "./task-output-context.ts";
6
+ import { aggregateTaskOutputs, type ResultArtifactReadCache } from "./task-output-context.ts";
7
7
 
8
8
  export type CrewGroupJoinMode = "off" | "group" | "smart";
9
9
 
@@ -51,6 +51,11 @@ export function deliverGroupJoin(input: {
51
51
  batch: TeamTaskState[];
52
52
  allTasks: TeamTaskState[];
53
53
  partial?: boolean;
54
+ /** R10-1: optional per-run result-artifact read cache. The closeout
55
+ * aggregates the same settled batch for the batch-summary artifact
56
+ * immediately before this call — passing that cache here turns the
57
+ * group-join aggregation into cache hits (byte-identical output). */
58
+ cache?: ResultArtifactReadCache;
54
59
  }): CrewGroupJoinDelivery | undefined {
55
60
  if (!shouldGroupJoin(input.mode, input.batch)) return undefined;
56
61
  const taskIds = input.batch.map((task) => task.id);
@@ -63,7 +68,7 @@ export function deliverGroupJoin(input: {
63
68
  const remaining = latest.filter((task) => task.status === "queued" || task.status === "running").map((task) => task.id);
64
69
  const partial = input.partial ?? remaining.length > 0;
65
70
  const batchId = batchIdFor(input.manifest.runId, taskIds);
66
- const summary = aggregateTaskOutputs(latest, input.manifest);
71
+ const summary = aggregateTaskOutputs(latest, input.manifest, input.cache);
67
72
  const requestId = requestIdFor(input.manifest.runId, batchId, partial);
68
73
  const existingMailbox = findMailboxMessageByRequestId(input.manifest, requestId);
69
74
  const existingStatus = existingMailbox
@@ -141,127 +146,8 @@ export function deliverGroupJoin(input: {
141
146
  return { ...delivery, artifact, messageId: mailbox.id };
142
147
  }
143
148
 
144
- import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
145
-
149
+ /**
150
+ * Persisted settings vocabulary for join mode (imported by settings-store.ts
151
+ * for `defaultJoinMode` validation and config application).
152
+ */
146
153
  export type JoinMode = "async" | "group" | "smart";
147
- export type DeliveryCallback = (records: CrewAgentRecord[], partial: boolean) => void;
148
-
149
- interface AgentGroup {
150
- groupId: string;
151
- agentIds: Set<string>;
152
- completedRecords: Map<string, CrewAgentRecord>;
153
- timeoutHandle?: ReturnType<typeof setTimeout>;
154
- delivered: boolean;
155
- isStraggler: boolean;
156
- }
157
-
158
- const DEFAULT_TIMEOUT = 30_000;
159
- const STRAGGLER_TIMEOUT = 15_000;
160
-
161
- export class GroupJoinManager {
162
- private groups = new Map<string, AgentGroup>();
163
- private agentToGroup = new Map<string, string>();
164
-
165
- private deliverCb: DeliveryCallback;
166
- private groupTimeout: number;
167
-
168
- constructor(deliverCb: DeliveryCallback, groupTimeout = DEFAULT_TIMEOUT) {
169
- this.deliverCb = deliverCb;
170
- this.groupTimeout = groupTimeout;
171
- }
172
-
173
- registerGroup(groupId: string, agentIds: string[]): void {
174
- const group: AgentGroup = {
175
- groupId,
176
- agentIds: new Set(agentIds),
177
- completedRecords: new Map(),
178
- delivered: false,
179
- isStraggler: false,
180
- };
181
- this.groups.set(groupId, group);
182
- for (const id of agentIds) {
183
- this.agentToGroup.set(id, groupId);
184
- }
185
- }
186
-
187
- onAgentComplete(record: CrewAgentRecord): "delivered" | "held" | "pass" {
188
- const groupId = this.agentToGroup.get(record.taskId);
189
- if (!groupId) return "pass";
190
-
191
- const group = this.groups.get(groupId);
192
- if (!group || group.delivered) return "pass";
193
-
194
- group.completedRecords.set(record.taskId, record);
195
-
196
- if (group.completedRecords.size >= group.agentIds.size) {
197
- this.deliver(group, false);
198
- return "delivered";
199
- }
200
-
201
- if (!group.timeoutHandle) {
202
- const timeout = group.isStraggler ? STRAGGLER_TIMEOUT : this.groupTimeout;
203
- group.timeoutHandle = setTimeout(() => {
204
- this.onTimeout(group);
205
- }, timeout);
206
- }
207
-
208
- return "held";
209
- }
210
-
211
- private onTimeout(group: AgentGroup): void {
212
- if (group.delivered) return;
213
- group.timeoutHandle = undefined;
214
-
215
- const remaining = new Set<string>();
216
- for (const id of group.agentIds) {
217
- if (!group.completedRecords.has(id)) remaining.add(id);
218
- }
219
-
220
- for (const id of group.completedRecords.keys()) {
221
- this.agentToGroup.delete(id);
222
- }
223
-
224
- this.deliverCb([...group.completedRecords.values()], true);
225
-
226
- group.completedRecords.clear();
227
- group.agentIds = remaining;
228
- group.isStraggler = true;
229
-
230
- // Re-arm timer for remaining stragglers so they aren't silently abandoned
231
- if (remaining.size > 0) {
232
- group.timeoutHandle = setTimeout(() => this.onTimeout(group), STRAGGLER_TIMEOUT);
233
- group.timeoutHandle.unref();
234
- }
235
- }
236
-
237
- private deliver(group: AgentGroup, partial: boolean): void {
238
- if (group.timeoutHandle) {
239
- clearTimeout(group.timeoutHandle);
240
- group.timeoutHandle = undefined;
241
- }
242
- group.delivered = true;
243
- this.deliverCb([...group.completedRecords.values()], partial);
244
- this.cleanupGroup(group.groupId);
245
- }
246
-
247
- private cleanupGroup(groupId: string): void {
248
- const group = this.groups.get(groupId);
249
- if (!group) return;
250
- for (const id of group.agentIds) {
251
- this.agentToGroup.delete(id);
252
- }
253
- this.groups.delete(groupId);
254
- }
255
-
256
- isGrouped(agentId: string): boolean {
257
- return this.agentToGroup.has(agentId);
258
- }
259
-
260
- dispose(): void {
261
- for (const group of this.groups.values()) {
262
- if (group.timeoutHandle) clearTimeout(group.timeoutHandle);
263
- }
264
- this.groups.clear();
265
- this.agentToGroup.clear();
266
- }
267
- }
@@ -5,6 +5,7 @@ import { resolveToolPolicy } from "../../agents/agent-config.ts";
5
5
  import type { CrewRuntimeConfig } from "../../config/config.ts";
6
6
  import { loadConfig } from "../../config/config.ts";
7
7
  import { DEFAULT_LIVE_SESSION } from "../../config/defaults.ts";
8
+ import { getCrewEnv } from "../../config/env-vars.ts";
8
9
  import { appendEvent, appendEventFireAndForget } from "../../state/event-log/event-log.ts";
9
10
  import type { TeamRunManifest, TeamTaskState, UsageState } from "../../state/types.ts";
10
11
  import { logInternalError } from "../../utils/internal-error.ts";
@@ -81,6 +82,13 @@ import { collectLiveSessionHealth, formatLiveSessionDiagnostics } from "./live-s
81
82
  * explorer subagents launched together; 3 of 4 crashed.)
82
83
  */
83
84
  let liveSessionModulePromise: Promise<LiveSessionModule> | undefined;
85
+
86
+ /**
87
+ * Phase 4 (ADR 2026-08-15-runtime-convergence, decision (a) — freeze):
88
+ * warn-once flag so the experimental-path notice fires at most once per
89
+ * process, not per task.
90
+ */
91
+ let experimentalWarned = false;
84
92
  function loadLiveSessionModule(): Promise<LiveSessionModule> {
85
93
  if (!liveSessionModulePromise) {
86
94
  liveSessionModulePromise = import("@earendil-works/pi-coding-agent") as unknown as Promise<LiveSessionModule>;
@@ -549,13 +557,30 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
549
557
  // any module. Under tsx, concurrent first-imports race module-record
550
558
  // instantiation; awaiting the registration-time warmup eliminates the window.
551
559
  await awaitRuntimeWarmup();
560
+
561
+ // Phase 4 (ADR 2026-08-15-runtime-convergence, decision (a) — freeze):
562
+ // the live-session path is permanently EXPERIMENTAL and intentionally
563
+ // diverges from the supported child-process path (no worker-cap semaphore,
564
+ // no depth guard, permissive SDK tool surface, single-model-fallback
565
+ // report). Warn once per process so operators see the notice without
566
+ // per-task spam.
567
+ if (!experimentalWarned) {
568
+ experimentalWarned = true;
569
+ logInternalError(
570
+ "live-session.experimental",
571
+ new Error("experimental path"),
572
+ "runtime.mode=live-session is EXPERIMENTAL and frozen (decision (a)); it diverges from child-process semantics (no worker-cap/depth guard, permissive tools) — see docs/decisions/2026-08-15-runtime-convergence.md",
573
+ "warn",
574
+ );
575
+ }
576
+
552
577
  const isCurrent = input.isCurrent ?? (() => true);
553
578
  let streamOut: StreamingOutputHandle | undefined;
554
579
 
555
580
  // G1: Capture yield result from custom tool callback
556
581
  let customToolYieldResult: YieldResult | undefined;
557
582
  let customToolYieldResolved = false;
558
- if (process.env.PI_CREW_MOCK_LIVE_SESSION === "success") {
583
+ if (getCrewEnv("PI_CREW_MOCK_LIVE_SESSION") === "success") {
559
584
  const agentId = `${input.manifest.runId}:${input.task.id}`;
560
585
  const inherited =
561
586
  input.runtimeConfig?.inheritContext === true && input.parentContext ? ` with inherited context: ${input.parentContext}` : "";
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { TEAM_TASK_STATUSES, TEAM_TERMINAL_TASK_STATUSES, type TeamTaskStatus } from "../state/contracts.ts";
15
15
  import type { TeamTaskState } from "../state/types.ts";
16
+ import { logInternalError } from "../utils/internal-error.ts";
16
17
  import { refreshTaskGraphQueues } from "./scheduling/task-graph-scheduler.ts";
17
18
 
18
19
  export function isNonTerminalTaskStatus(status: TeamTaskState["status"]): boolean {
@@ -123,7 +124,12 @@ export function shouldMergeTaskUpdate(current: TeamTaskState, updated: TeamTaskS
123
124
  // Malformed finishedAt (NaN) is treated as Infinity — invalid state should be
124
125
  // replaced rather than persisting corruption. Log warning for visibility.
125
126
  if (!Number.isFinite(currentTime)) {
126
- console.warn(`[merge-gate] Task ${current.id} has malformed finishedAt: ${current.finishedAt}`);
127
+ logInternalError(
128
+ "merge-gate",
129
+ new Error(`Task ${current.id} has malformed finishedAt: ${current.finishedAt}`),
130
+ undefined,
131
+ "warn",
132
+ );
127
133
  }
128
134
  if (isMalformedFinishedAtReplacement(currentTime, updatedTime)) {
129
135
  return true;
@@ -175,15 +181,31 @@ export function mergeTaskUpdatesPreservingTerminal(base: TeamTaskState[], result
175
181
  const current = indexById.get(updated.id);
176
182
  if (!current) continue;
177
183
  if (!shouldMergeTaskUpdate(current, updated)) {
178
- // Log skipped merges for visibility into rejected parallel updates.
179
- // In distributed systems with parallel workers, rejected merges may
180
- // indicate bugs (wrong status, timestamp corruption) if they accumulate.
181
- console.debug("[merge-gate] Skipping stale merge for task", updated.id, {
182
- currentStatus: current.status,
183
- updatedStatus: updated.status,
184
- currentFinishedAt: current.finishedAt,
185
- updatedFinishedAt: updated.finishedAt,
186
- });
184
+ // Routine no-op skips (an unchanged queued/running snapshot
185
+ // arriving from a parallel worker) are NORMAL parallel-merge
186
+ // behaviour and must stay silent — pi renders extension
187
+ // console output inline in the session transcript, so logging
188
+ // every skip floods whatever session is open (main or an
189
+ // agent view). Only a REAL rejected change (status transition,
190
+ // completion time, or result) is surfaced, as a warning: in
191
+ // distributed systems, rejected transitions may indicate bugs
192
+ // (wrong status, timestamp corruption) if they accumulate.
193
+ const meaningfulDiff =
194
+ updated.status !== current.status ||
195
+ updated.finishedAt !== current.finishedAt ||
196
+ updated.startedAt !== current.startedAt ||
197
+ Boolean(updated.resultArtifact) !== Boolean(current.resultArtifact) ||
198
+ Boolean(updated.error) ||
199
+ Boolean(updated.modelAttempts?.length) ||
200
+ Boolean(updated.usage);
201
+ if (meaningfulDiff) {
202
+ console.warn("[merge-gate] Rejected update for task", updated.id, {
203
+ currentStatus: current.status,
204
+ updatedStatus: updated.status,
205
+ currentFinishedAt: current.finishedAt,
206
+ updatedFinishedAt: updated.finishedAt,
207
+ });
208
+ }
187
209
  skipped += 1;
188
210
  continue;
189
211
  }