@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.
- package/CHANGELOG.md +22 -0
- package/README.md +5 -5
- package/docs/README.md +1 -1
- package/docs/guides/author-and-run-waves.md +1 -1
- package/docs/guides/planner.md +1 -1
- package/docs/guides/terminal-surfaces.md +2 -0
- package/docs/plans/current-state.md +1 -1
- package/docs/plans/end-state-architecture.md +1 -1
- package/docs/plans/examples/wave-example-design-handoff.md +1 -1
- package/docs/plans/examples/wave-example-live-proof.md +1 -1
- package/docs/plans/migration.md +22 -8
- package/docs/plans/wave-orchestrator.md +8 -5
- package/docs/reference/cli-reference.md +11 -3
- package/docs/reference/coordination-and-closure.md +26 -5
- package/docs/reference/live-proof-waves.md +9 -0
- package/docs/reference/npmjs-trusted-publishing.md +2 -2
- package/docs/reference/runtime-config/README.md +9 -3
- package/docs/reference/sample-waves.md +5 -5
- package/docs/reference/skills.md +1 -1
- package/docs/reference/wave-control.md +16 -0
- package/docs/reference/wave-planning-lessons.md +7 -1
- package/docs/research/coordination-failure-review.md +6 -6
- package/package.json +1 -1
- package/releases/manifest.json +19 -0
- package/scripts/wave-orchestrator/agent-state.mjs +42 -0
- package/scripts/wave-orchestrator/autonomous.mjs +42 -6
- package/scripts/wave-orchestrator/clarification-triage.mjs +4 -3
- package/scripts/wave-orchestrator/control-cli.mjs +126 -11
- package/scripts/wave-orchestrator/control-plane.mjs +12 -1
- package/scripts/wave-orchestrator/coordination-store.mjs +124 -4
- package/scripts/wave-orchestrator/executors.mjs +11 -6
- package/scripts/wave-orchestrator/gate-engine.mjs +5 -5
- package/scripts/wave-orchestrator/launcher-runtime.mjs +1 -1
- package/scripts/wave-orchestrator/launcher.mjs +216 -0
- package/scripts/wave-orchestrator/ledger.mjs +14 -12
- package/scripts/wave-orchestrator/reducer-snapshot.mjs +8 -6
- package/scripts/wave-orchestrator/retry-engine.mjs +19 -11
- package/scripts/wave-orchestrator/routing-state.mjs +50 -3
- package/scripts/wave-orchestrator/session-supervisor.mjs +6 -10
- package/scripts/wave-orchestrator/task-entity.mjs +4 -4
- package/scripts/wave-orchestrator/terminals.mjs +14 -14
- package/scripts/wave-orchestrator/wave-files.mjs +15 -21
- 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
|
-
|
|
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
|
|
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
package/releases/manifest.json
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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(
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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,
|
|
1076
|
-
detail:
|
|
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],
|