@chllming/wave-orchestration 0.8.6 → 0.8.7

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 (43) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +5 -5
  3. package/docs/README.md +1 -1
  4. package/docs/guides/author-and-run-waves.md +1 -1
  5. package/docs/guides/planner.md +1 -1
  6. package/docs/guides/terminal-surfaces.md +2 -0
  7. package/docs/plans/current-state.md +1 -1
  8. package/docs/plans/end-state-architecture.md +1 -1
  9. package/docs/plans/examples/wave-example-design-handoff.md +1 -1
  10. package/docs/plans/examples/wave-example-live-proof.md +1 -1
  11. package/docs/plans/migration.md +22 -8
  12. package/docs/plans/wave-orchestrator.md +8 -5
  13. package/docs/reference/cli-reference.md +11 -3
  14. package/docs/reference/coordination-and-closure.md +26 -5
  15. package/docs/reference/live-proof-waves.md +9 -0
  16. package/docs/reference/npmjs-trusted-publishing.md +2 -2
  17. package/docs/reference/runtime-config/README.md +9 -3
  18. package/docs/reference/sample-waves.md +5 -5
  19. package/docs/reference/skills.md +1 -1
  20. package/docs/reference/wave-control.md +16 -0
  21. package/docs/reference/wave-planning-lessons.md +7 -1
  22. package/docs/research/coordination-failure-review.md +6 -6
  23. package/package.json +1 -1
  24. package/releases/manifest.json +19 -0
  25. package/scripts/wave-orchestrator/agent-state.mjs +42 -0
  26. package/scripts/wave-orchestrator/autonomous.mjs +42 -6
  27. package/scripts/wave-orchestrator/clarification-triage.mjs +4 -3
  28. package/scripts/wave-orchestrator/control-cli.mjs +126 -11
  29. package/scripts/wave-orchestrator/control-plane.mjs +12 -1
  30. package/scripts/wave-orchestrator/coordination-store.mjs +124 -4
  31. package/scripts/wave-orchestrator/executors.mjs +11 -6
  32. package/scripts/wave-orchestrator/gate-engine.mjs +5 -5
  33. package/scripts/wave-orchestrator/launcher-runtime.mjs +1 -1
  34. package/scripts/wave-orchestrator/launcher.mjs +216 -0
  35. package/scripts/wave-orchestrator/ledger.mjs +14 -12
  36. package/scripts/wave-orchestrator/reducer-snapshot.mjs +8 -6
  37. package/scripts/wave-orchestrator/retry-engine.mjs +19 -11
  38. package/scripts/wave-orchestrator/routing-state.mjs +50 -3
  39. package/scripts/wave-orchestrator/session-supervisor.mjs +6 -10
  40. package/scripts/wave-orchestrator/task-entity.mjs +4 -4
  41. package/scripts/wave-orchestrator/terminals.mjs +14 -14
  42. package/scripts/wave-orchestrator/wave-files.mjs +15 -21
  43. package/scripts/wave-orchestrator/wave-state-reducer.mjs +72 -5
@@ -229,16 +229,16 @@ This is the central failure highlighted by `HiddenBench` and `Silo-Bench`, and t
229
229
 
230
230
  ### 3. Expertise routing is explicit, but shallow
231
231
 
232
- [scripts/wave-orchestrator/routing-state.mjs](../../scripts/wave-orchestrator/routing-state.mjs) is better than unconstrained self-organization, but it still routes mostly by:
232
+ [scripts/wave-orchestrator/routing-state.mjs](../../scripts/wave-orchestrator/routing-state.mjs) is better than unconstrained self-organization, and it now has a light same-wave success preference, but it still routes mostly by:
233
233
 
234
234
  - explicit target
235
235
  - configured preferred agents
236
236
  - declared capability ownership
237
+ - demonstrated same-wave completions on the capability
237
238
  - least-busy fallback
238
239
 
239
- It does not yet weight:
240
+ Beyond that light historical-success preference, it still does not weight:
240
241
 
241
- - historical success on a capability
242
242
  - evidence quality by agent
243
243
  - confidence calibration
244
244
  - expert-leverage metrics
@@ -247,14 +247,14 @@ So the repo partially addresses the concern from `Multi-Agent Teams Hold Experts
247
247
 
248
248
  ### 4. Clarification and contradiction handling are still somewhat heuristic
249
249
 
250
- Clarification triage and integration evidence aggregation are real safeguards, but they still lean heavily on:
250
+ Clarification triage, blocker taxonomy, operator downgrade controls, and integration evidence aggregation are real safeguards, but they still lean heavily on:
251
251
 
252
252
  - ownership mappings
253
253
  - artifact references
254
254
  - structured markers
255
255
  - text-level summaries and conflict extraction
256
256
 
257
- That is enough to make the runtime operationally safer, but it is not yet a richer semantic evidence-integration layer. Subtle contradictions or latent information asymmetries may still be missed.
257
+ That is enough to make the runtime operationally safer. The newer hard-vs-soft blocker split also removes some unnecessary terminal failures by letting stale or advisory coordination remain visible without owning closure. But it is not yet a richer semantic evidence-integration layer, and subtle contradictions or latent information asymmetries may still be missed.
258
258
 
259
259
  ### 5. DPBench-style simultaneous coordination is only indirectly addressed
260
260
 
@@ -283,7 +283,7 @@ So the design points in the right direction, but the claim is not yet validated.
283
283
 
284
284
  If the standard is "does this repo merely claim multi-agent coordination," the answer is no. It has real machinery for blackboard-like state sharing, evidence-based closure, clarification handling, and coordination diagnostics.
285
285
 
286
- If the standard is "has this repo already demonstrated that its design beats the core failure modes isolated by HiddenBench, Silo-Bench, DPBench, and related work," the answer is also no. The design is substantially more credible than most MAS stacks, but the empirical proof is still missing.
286
+ If the standard is "has this repo already demonstrated that its design beats the core failure modes isolated by HiddenBench, Silo-Bench, DPBench, and related work," the answer is also no. The design is substantially more credible than most MAS stacks, and it now also reduces avoidable failure through targeted recovery, blocker severity, and policy-safe downgrade paths, but the empirical proof is still missing.
287
287
 
288
288
  The most accurate claim today is:
289
289
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chllming/wave-orchestration",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
4
4
  "license": "MIT",
5
5
  "description": "Generic wave-based multi-agent orchestration for repository work.",
6
6
  "repository": {
@@ -2,6 +2,25 @@
2
2
  "schemaVersion": 1,
3
3
  "packageName": "@chllming/wave-orchestration",
4
4
  "releases": [
5
+ {
6
+ "version": "0.8.7",
7
+ "date": "2026-03-27",
8
+ "summary": "Policy-consistency hardening, capability-specific same-wave routing, stable per-wave tmux session reuse, and 0.8.7 release-surface alignment.",
9
+ "features": [
10
+ "Generic `budget.turns` is now documented and tested consistently as advisory metadata only; hard runtime turn ceilings come only from runtime-specific settings such as `claude.maxTurns` or `opencode.steps`.",
11
+ "Capability-targeted helper routing now prefers demonstrated same-wave success for the requested capability before falling back to the least-busy matching capability owner, and unrelated completed work no longer counts as routing evidence.",
12
+ "Advisory, stale, and other non-blocking clarification or human-input records stay visible in control and reducer projections without reopening hard blocked reducer state by themselves.",
13
+ "Wave-agent, resident-orchestrator, and per-wave dashboard tmux sessions now reuse stable per-wave session names, so stale launcher exits stop accumulating extra sessions for the same wave.",
14
+ "Structured signal extraction now also recognizes markers embedded inside JSON log lines, so wrapped executor transcripts still produce proof, doc-delta, and component evidence."
15
+ ],
16
+ "manualSteps": [
17
+ "Run `pnpm exec wave doctor` and `pnpm exec wave launch --lane main --dry-run --no-dashboard` after upgrading so the repo validates against the `0.8.7` routing, blocker-severity, signal-wrapper, and stable-session behavior.",
18
+ "If your repo copied starter scripts or operator docs, sync `scripts/wave-status.sh`, `scripts/wave-watch.sh`, `docs/guides/signal-wrappers.md`, `docs/guides/terminal-surfaces.md`, `docs/reference/cli-reference.md`, and any local tmux/session runbooks that still assume run-tagged session names.",
19
+ "If your repo copied planner or routing guidance, sync `docs/guides/planner.md`, `docs/reference/wave-planning-lessons.md`, `docs/plans/wave-orchestrator.md`, the `planner-agentic` bundle entry in `docs/context7/bundles.json`, and any local helper-assignment policy docs so they describe capability-specific same-wave routing evidence instead of generic prior completion.",
20
+ "If your repo relies on advisory `budget.turns` as if it were a hard ceiling, move that limit to the runtime-specific executor config (`claude.maxTurns` or `opencode.steps`) before you depend on deterministic turn enforcement."
21
+ ],
22
+ "breaking": false
23
+ },
5
24
  {
6
25
  "version": "0.8.6",
7
26
  "date": "2026-03-25",
@@ -160,6 +160,44 @@ function appendParsedStructuredSignalCandidates(lines, candidates, { requireAll
160
160
  candidates.push(...parsedCandidates);
161
161
  }
162
162
 
163
+ function collectEmbeddedStructuredSignalTexts(value, texts) {
164
+ if (!value || typeof value !== "object") {
165
+ return;
166
+ }
167
+ if (Array.isArray(value)) {
168
+ for (const item of value) {
169
+ collectEmbeddedStructuredSignalTexts(item, texts);
170
+ }
171
+ return;
172
+ }
173
+ if (typeof value.text === "string") {
174
+ texts.push(value.text);
175
+ }
176
+ if (typeof value.aggregated_output === "string") {
177
+ texts.push(value.aggregated_output);
178
+ }
179
+ for (const nestedValue of Object.values(value)) {
180
+ if (nestedValue && typeof nestedValue === "object") {
181
+ collectEmbeddedStructuredSignalTexts(nestedValue, texts);
182
+ }
183
+ }
184
+ }
185
+
186
+ function extractEmbeddedStructuredSignalTextsFromJsonLine(line) {
187
+ const trimmed = String(line || "").trim();
188
+ if (!trimmed || !/^[{\[]/.test(trimmed)) {
189
+ return [];
190
+ }
191
+ try {
192
+ const payload = JSON.parse(trimmed);
193
+ const texts = [];
194
+ collectEmbeddedStructuredSignalTexts(payload, texts);
195
+ return texts.filter(Boolean);
196
+ } catch {
197
+ return [];
198
+ }
199
+ }
200
+
163
201
  function collectStructuredSignalCandidates(text) {
164
202
  if (!text) {
165
203
  return [];
@@ -167,6 +205,10 @@ function collectStructuredSignalCandidates(text) {
167
205
  const candidates = [];
168
206
  let fenceLines = null;
169
207
  for (const rawLine of String(text || "").split(/\r?\n/)) {
208
+ const embeddedTexts = extractEmbeddedStructuredSignalTextsFromJsonLine(rawLine);
209
+ for (const embeddedText of embeddedTexts) {
210
+ candidates.push(...collectStructuredSignalCandidates(embeddedText));
211
+ }
170
212
  const trimmed = rawLine.trim();
171
213
  if (/^```/.test(trimmed)) {
172
214
  if (fenceLines === null) {
@@ -27,8 +27,13 @@ import {
27
27
  maybeAnnouncePackageUpdate,
28
28
  WAVE_SUPPRESS_UPDATE_NOTICE_ENV,
29
29
  } from "./package-update-notice.mjs";
30
+ import { buildTaskSnapshots } from "./control-plane.mjs";
31
+ import { readWaveHumanFeedbackRequests } from "./coordination.mjs";
30
32
  import { readRunState } from "./wave-files.mjs";
31
- import { readDependencyTickets } from "./coordination-store.mjs";
33
+ import {
34
+ readDependencyTickets,
35
+ readMaterializedCoordinationState,
36
+ } from "./coordination-store.mjs";
32
37
  import { readWaveLedger } from "./ledger.mjs";
33
38
 
34
39
  const AUTONOMOUS_EXECUTOR_MODES = SUPPORTED_EXECUTOR_MODES.filter((mode) => mode !== "local");
@@ -249,7 +254,38 @@ function requiredInboundDependenciesOpen(lanePaths, lane) {
249
254
  });
250
255
  }
251
256
 
252
- function pendingHumanItemsForWave(lanePaths, wave) {
257
+ function liveBlockingHumanItemsForWave(lanePaths, lane, wave) {
258
+ if (!lanePaths?.coordinationDir || !lanePaths?.feedbackRequestsDir) {
259
+ return null;
260
+ }
261
+ const coordinationState = readMaterializedCoordinationState(
262
+ path.join(lanePaths.coordinationDir, `wave-${wave}.jsonl`),
263
+ );
264
+ const feedbackRequests = readWaveHumanFeedbackRequests({
265
+ feedbackRequestsDir: lanePaths.feedbackRequestsDir,
266
+ lane,
267
+ waveNumber: wave,
268
+ agentIds: [],
269
+ orchestratorId: "",
270
+ });
271
+ return buildTaskSnapshots({
272
+ coordinationState,
273
+ feedbackRequests,
274
+ })
275
+ .filter(
276
+ (task) =>
277
+ ["human-input", "escalation"].includes(task.taskType) &&
278
+ task.blocking !== false &&
279
+ ["open", "working", "input-required"].includes(task.state),
280
+ )
281
+ .map((task) => task.taskId);
282
+ }
283
+
284
+ function pendingHumanItemsForWave(lanePaths, lane, wave) {
285
+ const liveItems = liveBlockingHumanItemsForWave(lanePaths, lane, wave);
286
+ if (Array.isArray(liveItems)) {
287
+ return liveItems;
288
+ }
253
289
  const existingLedger = readWaveLedger(path.join(lanePaths.ledgerDir, `wave-${wave}.json`));
254
290
  return [
255
291
  ...(existingLedger?.humanFeedback || []),
@@ -257,7 +293,7 @@ function pendingHumanItemsForWave(lanePaths, wave) {
257
293
  ];
258
294
  }
259
295
 
260
- function pendingHumanItemsForLane(lanePaths) {
296
+ function pendingHumanItemsForLane(lanePaths, lane) {
261
297
  if (!fs.existsSync(lanePaths.ledgerDir)) {
262
298
  return [];
263
299
  }
@@ -271,7 +307,7 @@ function pendingHumanItemsForLane(lanePaths) {
271
307
  .filter((item) => Number.isFinite(item.wave))
272
308
  .sort((left, right) => left.wave - right.wave)
273
309
  .flatMap((item) =>
274
- pendingHumanItemsForWave(lanePaths, item.wave).map((id) => ({
310
+ pendingHumanItemsForWave(lanePaths, lane, item.wave).map((id) => ({
275
311
  wave: item.wave,
276
312
  id,
277
313
  })),
@@ -292,7 +328,7 @@ export function readAutonomousBarrier(lanePaths, lane, wave = null) {
292
328
  };
293
329
  }
294
330
  if (wave === null) {
295
- const pendingHumanEntries = pendingHumanItemsForLane(lanePaths);
331
+ const pendingHumanEntries = pendingHumanItemsForLane(lanePaths, lane);
296
332
  if (pendingHumanEntries.length > 0) {
297
333
  return {
298
334
  kind: "human-input",
@@ -303,7 +339,7 @@ export function readAutonomousBarrier(lanePaths, lane, wave = null) {
303
339
  }
304
340
  return null;
305
341
  }
306
- const pendingHumanItems = pendingHumanItemsForWave(lanePaths, wave);
342
+ const pendingHumanItems = pendingHumanItemsForWave(lanePaths, lane, wave);
307
343
  if (pendingHumanItems.length > 0) {
308
344
  return {
309
345
  kind: "human-input",
@@ -4,6 +4,7 @@ import {
4
4
  appendCoordinationRecord,
5
5
  clarificationClosureCondition,
6
6
  clarificationLinkedRequests,
7
+ coordinationRecordBlocksWave,
7
8
  isOpenCoordinationStatus,
8
9
  readMaterializedCoordinationState,
9
10
  } from "./coordination-store.mjs";
@@ -467,14 +468,14 @@ export function triageClarificationRequests({
467
468
  ensureDirectory(lanePaths.feedbackTriageDir);
468
469
  const triagePath = triageLogPath(lanePaths, wave.wave);
469
470
  const openClarifications = (coordinationState?.clarifications || []).filter((record) =>
470
- isOpenCoordinationStatus(record.status),
471
+ coordinationRecordBlocksWave(record),
471
472
  );
472
473
  let changed = false;
473
474
 
474
475
  for (const record of openClarifications) {
475
476
  const linkedRequests = clarificationLinkedRequests(coordinationState, record.id);
476
477
  const openLinkedRequests = linkedRequests.filter((entry) =>
477
- isOpenCoordinationStatus(entry.status),
478
+ coordinationRecordBlocksWave(entry),
478
479
  );
479
480
  const openAckPendingLinkedRequests = openLinkedRequests.filter(
480
481
  (entry) => entry.status === "open",
@@ -487,7 +488,7 @@ export function triageClarificationRequests({
487
488
  const openEscalations = (coordinationState?.humanEscalations || []).filter(
488
489
  (entry) =>
489
490
  entry.closureCondition === clarificationClosureCondition(record.id) &&
490
- isOpenCoordinationStatus(entry.status),
491
+ coordinationRecordBlocksWave(entry),
491
492
  );
492
493
  if (resolvedLinkedRequest || resolvedEscalation) {
493
494
  if (openEscalations.length > 0) {
@@ -1,6 +1,13 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { appendCoordinationRecord, clarificationClosureCondition, clarificationLinkedRequests, isOpenCoordinationStatus, readMaterializedCoordinationState, updateSeedRecords } from "./coordination-store.mjs";
3
+ import {
4
+ appendCoordinationRecord,
5
+ clarificationClosureCondition,
6
+ clarificationLinkedRequests,
7
+ isOpenCoordinationStatus,
8
+ readMaterializedCoordinationState,
9
+ updateSeedRecords,
10
+ } from "./coordination-store.mjs";
4
11
  import { answerFeedbackRequest, createFeedbackRequest } from "./feedback.mjs";
5
12
  import { readWaveHumanFeedbackRequests } from "./coordination.mjs";
6
13
  import { readWaveLedger } from "./ledger.mjs";
@@ -18,6 +25,7 @@ import {
18
25
  REPO_ROOT,
19
26
  sanitizeAdhocRunId,
20
27
  sanitizeLaneName,
28
+ toIsoTimestamp,
21
29
  } from "./shared.mjs";
22
30
  import {
23
31
  appendWaveControlEvent,
@@ -50,7 +58,7 @@ function printUsage() {
50
58
  wave control task create --lane <lane> --wave <n> --agent <id> --kind <request|blocker|clarification|handoff|evidence|claim|decision|human-input> --summary <text> [options]
51
59
  wave control task list --lane <lane> --wave <n> [--agent <id>] [--json]
52
60
  wave control task get --lane <lane> --wave <n> --id <task-id> [--json]
53
- wave control task act <start|resolve|dismiss|cancel|reassign|answer|escalate> --lane <lane> --wave <n> --id <task-id> [options]
61
+ wave control task act <start|resolve|dismiss|cancel|reassign|answer|escalate|defer|mark-advisory|mark-stale|resolve-policy> --lane <lane> --wave <n> --id <task-id> [options]
54
62
 
55
63
  wave control rerun request --lane <lane> --wave <n> [--agent <id> ...] [--resume-cursor <cursor>] [--reuse-attempt <id> ...] [--reuse-proof <id> ...] [--reuse-derived-summaries <true|false>] [--invalidate-component <id> ...] [--clear-reuse <id> ...] [--preserve-reuse <id> ...] [--requested-by <name>] [--reason <text>] [--json]
56
64
  wave control rerun get --lane <lane> --wave <n> [--json]
@@ -94,6 +102,8 @@ function parseArgs(argv) {
94
102
  detail: "",
95
103
  targets: [],
96
104
  priority: "normal",
105
+ blocking: null,
106
+ blockerSeverity: "",
97
107
  dependsOn: [],
98
108
  artifactRefs: [],
99
109
  status: "open",
@@ -155,6 +165,10 @@ function parseArgs(argv) {
155
165
  options.targets.push(String(args[++i] || "").trim());
156
166
  } else if (arg === "--priority") {
157
167
  options.priority = String(args[++i] || "").trim();
168
+ } else if (arg === "--blocking") {
169
+ options.blocking = normalizeBooleanish(args[++i], true);
170
+ } else if (arg === "--severity") {
171
+ options.blockerSeverity = String(args[++i] || "").trim();
158
172
  } else if (arg === "--depends-on") {
159
173
  options.dependsOn.push(String(args[++i] || "").trim());
160
174
  } else if (arg === "--artifact") {
@@ -264,7 +278,10 @@ const BLOCKING_TASK_TYPES = new Set([
264
278
  ]);
265
279
 
266
280
  function taskBlocksAgent(task) {
267
- return BLOCKING_TASK_TYPES.has(String(task?.taskType || "").trim().toLowerCase());
281
+ return (
282
+ BLOCKING_TASK_TYPES.has(String(task?.taskType || "").trim().toLowerCase()) &&
283
+ task?.blocking !== false
284
+ );
268
285
  }
269
286
 
270
287
  function assignmentRelevantToAgent(assignment, agentId = "") {
@@ -462,7 +479,9 @@ function buildBlockingEdge({
462
479
  selectionTargetsAgent(task.assigneeAgentId, attemptSelection)
463
480
  );
464
481
  });
465
- const pendingHuman = scopedTasks.find((task) => task.state === "input-required");
482
+ const pendingHuman = scopedTasks.find(
483
+ (task) => task.state === "input-required" && task.blocking !== false,
484
+ );
466
485
  if (pendingHuman) {
467
486
  return {
468
487
  kind: "human-input",
@@ -472,7 +491,10 @@ function buildBlockingEdge({
472
491
  };
473
492
  }
474
493
  const escalation = scopedTasks.find(
475
- (task) => task.taskType === "escalation" && ["open", "working"].includes(task.state),
494
+ (task) =>
495
+ task.taskType === "escalation" &&
496
+ task.blocking !== false &&
497
+ ["open", "working"].includes(task.state),
476
498
  );
477
499
  if (escalation) {
478
500
  return {
@@ -483,7 +505,10 @@ function buildBlockingEdge({
483
505
  };
484
506
  }
485
507
  const clarification = scopedTasks.find(
486
- (task) => task.taskType === "clarification" && ["open", "working"].includes(task.state),
508
+ (task) =>
509
+ task.taskType === "clarification" &&
510
+ task.blocking !== false &&
511
+ ["open", "working"].includes(task.state),
487
512
  );
488
513
  if (clarification) {
489
514
  return {
@@ -564,7 +589,10 @@ function buildBlockingEdge({
564
589
  };
565
590
  }
566
591
  const blocker = scopedTasks.find(
567
- (task) => task.taskType === "blocker" && ["open", "working"].includes(task.state),
592
+ (task) =>
593
+ task.taskType === "blocker" &&
594
+ task.blocking !== false &&
595
+ ["open", "working"].includes(task.state),
568
596
  );
569
597
  if (blocker) {
570
598
  return {
@@ -575,7 +603,10 @@ function buildBlockingEdge({
575
603
  };
576
604
  }
577
605
  const request = scopedTasks.find(
578
- (task) => task.taskType === "request" && ["open", "working"].includes(task.state),
606
+ (task) =>
607
+ task.taskType === "request" &&
608
+ task.blocking !== false &&
609
+ ["open", "working"].includes(task.state),
579
610
  );
580
611
  if (request) {
581
612
  return {
@@ -738,7 +769,9 @@ function printStatus(payload) {
738
769
  function appendCoordinationStatusUpdate(logPath, record, status, options = {}) {
739
770
  return appendCoordinationRecord(logPath, {
740
771
  ...record,
772
+ ...(options.patch || {}),
741
773
  status,
774
+ updatedAt: options.updatedAt || toIsoTimestamp(),
742
775
  summary: options.summary || record.summary,
743
776
  detail: options.detail || record.detail,
744
777
  source: options.source || "operator",
@@ -823,6 +856,73 @@ function appendTaskCoordinationEvent(logPath, lanePaths, wave, record, action, o
823
856
  source: "operator",
824
857
  });
825
858
  }
859
+ if (action === "defer") {
860
+ return appendCoordinationStatusUpdate(logPath, record, record.status, {
861
+ detail:
862
+ options.detail ||
863
+ `${record.summary || record.id} deferred by operator; keep visible but do not block wave progression.`,
864
+ patch: {
865
+ blocking: false,
866
+ blockerSeverity: "soft",
867
+ },
868
+ });
869
+ }
870
+ if (action === "mark-advisory") {
871
+ return appendCoordinationStatusUpdate(logPath, record, record.status, {
872
+ detail:
873
+ options.detail ||
874
+ `${record.summary || record.id} marked advisory by operator; keep visible without blocking closure.`,
875
+ patch: {
876
+ blocking: false,
877
+ blockerSeverity: "advisory",
878
+ },
879
+ });
880
+ }
881
+ if (action === "mark-stale") {
882
+ return appendCoordinationStatusUpdate(logPath, record, record.status, {
883
+ detail:
884
+ options.detail ||
885
+ `${record.summary || record.id} marked stale by operator; historical context preserved without blocking.`,
886
+ patch: {
887
+ blocking: false,
888
+ blockerSeverity: "stale",
889
+ },
890
+ });
891
+ }
892
+ if (action === "resolve-policy") {
893
+ const resolvedRecord = appendCoordinationStatusUpdate(logPath, record, "resolved", {
894
+ detail: options.detail || `Resolved by operator policy: ${record.summary || record.id}.`,
895
+ patch: {
896
+ blocking: false,
897
+ blockerSeverity: "advisory",
898
+ },
899
+ });
900
+ const policyRecord = appendCoordinationRecord(logPath, {
901
+ id: `policy-${record.id}`,
902
+ lane: lanePaths.lane,
903
+ wave: wave.wave,
904
+ agentId: options.agent || "operator",
905
+ kind: "resolved-by-policy",
906
+ targets: record.targets,
907
+ priority: record.priority,
908
+ artifactRefs: record.artifactRefs,
909
+ dependsOn: Array.from(new Set([record.id, ...(record.dependsOn || [])])),
910
+ closureCondition:
911
+ record.kind === "clarification-request"
912
+ ? clarificationClosureCondition(record.id)
913
+ : record.closureCondition || "",
914
+ summary: record.summary,
915
+ detail: options.detail || `Operator resolved ${record.id} by policy.`,
916
+ status: "resolved",
917
+ source: "operator",
918
+ blocking: false,
919
+ blockerSeverity: "advisory",
920
+ });
921
+ return {
922
+ resolvedRecord,
923
+ policyRecord,
924
+ };
925
+ }
826
926
  throw new Error(`Unsupported task action: ${action}`);
827
927
  }
828
928
 
@@ -975,6 +1075,8 @@ export async function runControlCli(argv) {
975
1075
  artifactRefs: options.artifactRefs,
976
1076
  status: options.status,
977
1077
  source: "operator",
1078
+ ...(options.blocking !== null ? { blocking: options.blocking } : {}),
1079
+ ...(options.blockerSeverity ? { blockerSeverity: options.blockerSeverity } : {}),
978
1080
  });
979
1081
  console.log(JSON.stringify(record, null, 2));
980
1082
  return;
@@ -1067,14 +1169,27 @@ export async function runControlCli(argv) {
1067
1169
  throw new Error(`Task not found: ${options.id}`);
1068
1170
  }
1069
1171
  const updated = appendTaskCoordinationEvent(logPath, lanePaths, wave, record, action, options);
1070
- if (record.kind === "clarification-request" && ["resolve", "dismiss"].includes(action)) {
1172
+ if (record.kind === "clarification-request" && ["resolve", "dismiss", "resolve-policy"].includes(action)) {
1071
1173
  const nextStatus = action === "resolve" ? "resolved" : "cancelled";
1174
+ const linkedStatus = action === "resolve-policy" ? "resolved" : nextStatus;
1072
1175
  for (const linked of clarificationLinkedRequests(coordinationState, record.id).filter((entry) =>
1073
1176
  isOpenCoordinationStatus(entry.status),
1074
1177
  )) {
1075
- appendCoordinationStatusUpdate(logPath, linked, nextStatus, {
1076
- detail: `${action === "resolve" ? "Resolved" : "Cancelled"} via clarification ${record.id}.`,
1178
+ appendCoordinationStatusUpdate(logPath, linked, linkedStatus, {
1179
+ detail:
1180
+ action === "resolve"
1181
+ ? `Resolved via clarification ${record.id}.`
1182
+ : action === "resolve-policy"
1183
+ ? `Resolved by policy via clarification ${record.id}.`
1184
+ : `Cancelled via clarification ${record.id}.`,
1077
1185
  summary: linked.summary,
1186
+ patch:
1187
+ action === "resolve-policy"
1188
+ ? {
1189
+ blocking: false,
1190
+ blockerSeverity: "advisory",
1191
+ }
1192
+ : undefined,
1078
1193
  });
1079
1194
  }
1080
1195
  }
@@ -11,6 +11,8 @@ import {
11
11
  import {
12
12
  CLARIFICATION_CLOSURE_PREFIX,
13
13
  buildCoordinationResponseMetrics,
14
+ coordinationBlockerSeverity,
15
+ coordinationRecordBlocksWave,
14
16
  } from "./coordination-store.mjs";
15
17
  import {
16
18
  DEFAULT_COORDINATION_ACK_TIMEOUT_MS,
@@ -586,6 +588,8 @@ export function buildTaskSnapshots({
586
588
  const metrics = responseMetrics.recordMetricsById.get(record.id) || {};
587
589
  const feedbackRequest = feedbackById.get(record.id) || null;
588
590
  const taskState = taskStateForCoordinationRecord(record, feedbackRequest);
591
+ const blocking = coordinationRecordBlocksWave(record);
592
+ const blockerSeverity = coordinationBlockerSeverity(record);
589
593
  tasks.push({
590
594
  taskId: record.id,
591
595
  sourceRecordId: record.id,
@@ -598,6 +602,8 @@ export function buildTaskSnapshots({
598
602
  assigneeAgentId: firstTargetAgentId(record),
599
603
  leaseOwnerAgentId:
600
604
  ["acknowledged", "in_progress"].includes(record.status) ? firstTargetAgentId(record) : null,
605
+ blocking,
606
+ blockerSeverity,
601
607
  needsHuman:
602
608
  record.kind === "human-feedback" ||
603
609
  feedbackRequest?.status === "pending" ||
@@ -627,7 +633,7 @@ export function buildTaskSnapshots({
627
633
  ? feedbackRequest?.updatedAt || record.updatedAt || record.createdAt
628
634
  : null,
629
635
  overdueAck: metrics.overdueAck === true,
630
- stale: metrics.staleClarification === true,
636
+ stale: metrics.staleClarification === true || blockerSeverity === "stale",
631
637
  feedbackRequestId: feedbackRequest?.id || null,
632
638
  humanResponse: feedbackRequest?.responseText || null,
633
639
  humanOperator: feedbackRequest?.responseOperator || null,
@@ -648,6 +654,8 @@ export function buildTaskSnapshots({
648
654
  ownerAgentId: request.agentId || null,
649
655
  assigneeAgentId: request.agentId || null,
650
656
  leaseOwnerAgentId: null,
657
+ blocking: true,
658
+ blockerSeverity: "hard",
651
659
  needsHuman: request.status !== "answered",
652
660
  dependsOn: [],
653
661
  evidenceRefs: [],
@@ -676,6 +684,9 @@ export function buildTaskSnapshots({
676
684
  export function nextTaskDeadline(tasks) {
677
685
  const candidates = [];
678
686
  for (const task of tasks || []) {
687
+ if (task?.blocking === false) {
688
+ continue;
689
+ }
679
690
  for (const [kind, value] of [
680
691
  ["ack", task.ackDeadlineAt],
681
692
  ["resolve", task.resolveDeadlineAt],