@chllming/wave-orchestration 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,40 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.8.3 - 2026-03-24
6
+
7
+ ### Changed
8
+
9
+ - Updated the shipped package metadata, release manifest, README, migration guide, sample-wave docs, current-state notes, and npm publishing runbook to advertise `0.8.3` as the current release surface.
10
+ - Documented that `wave feedback respond` is a canonical-state repair path, not a feedback-JSON-only update, and that ad-hoc reconciliation must keep the `--run <id>` context.
11
+
12
+ ### Fixed And Hardened
13
+
14
+ - Answered human-feedback requests now reconcile linked clarification, escalation, and helper-assignment state back into the canonical coordination log so reducer state, control surfaces, and launcher gates stop reading the wave as still `clarifying`.
15
+ - `wave feedback respond --run <id>` now carries the ad-hoc run id through the reconciliation helper, so answered human input repairs the isolated ad-hoc lane state instead of the roadmap state root.
16
+ - When a wave is stranded after a human answer arrives and no active attempt is still running, the human-input reconciliation path now writes a safe one-shot continuation request instead of leaving the wave waiting for manual relaunch bookkeeping.
17
+
18
+ ### Testing And Validation
19
+
20
+ - Added regression coverage for direct `wave feedback respond` reconciliation and for the ad-hoc `--run <id>` answer path.
21
+
22
+ ## 0.8.2 - 2026-03-24
23
+
24
+ ### Changed
25
+
26
+ - Updated the shipped package metadata, release manifest, README, migration guide, sample-wave docs, and npm publishing runbook to advertise `0.8.2` as the current release surface.
27
+
28
+ ### Fixed And Hardened
29
+
30
+ - `wave control status` now treats `phase=completed` as terminal in the control-status projection layer instead of replaying stale blocking edges from historical open coordination records.
31
+ - Completed waves now return `blockingEdge: null` and `nextTimer: null`, so stale overdue timers or request blockers stop leaking into an already-closed wave view.
32
+ - Successful logical-agent state is now preserved for completed waves, so agents that already finished cleanly stay `closed` or `satisfied` even when old request records remain visible in coordination history.
33
+
34
+ ### Testing And Validation
35
+
36
+ - Added regression coverage for completed-wave control-status projections so historical request records stay visible without reopening blocking state after closure.
37
+ - Revalidated the shipped release surface with the full Vitest suite, `wave doctor --json`, and `wave launch --lane main --dry-run --no-dashboard`.
38
+
5
39
  ## 0.8.1 - 2026-03-24
6
40
 
7
41
  ### Changed
package/README.md CHANGED
@@ -79,18 +79,18 @@ Wave is built to mitigate those failures with a canonical authority set, generat
79
79
 
80
80
  Current release:
81
81
 
82
- - `@chllming/wave-orchestration@0.8.1`
83
- - Release tag: [`v0.8.1`](https://github.com/chllming/agent-wave-orchestrator/releases/tag/v0.8.1)
82
+ - `@chllming/wave-orchestration@0.8.3`
83
+ - Release tag: [`v0.8.3`](https://github.com/chllming/agent-wave-orchestrator/releases/tag/v0.8.3)
84
84
  - Public install path: npmjs
85
85
  - Authenticated fallback: GitHub Packages
86
86
 
87
- Highlights in `0.8.1`:
87
+ Highlights in `0.8.3`:
88
88
 
89
- - `resolved-by-policy` helper follow-up now closes single-target helper assignments authoritatively instead of leaving stale helper-assignment barriers open.
90
- - Manual `wave coord post --kind resolved-by-policy` now defaults to `status=resolved`, which matches the intended policy-closure semantics.
91
- - Multi-target helper requests no longer over-close sibling assignments from a request-level policy note; assignment-specific evidence is required.
92
- - The architecture-hardening migration plan, reducer or envelope wiring, and aligned docs or skills from the prior release remain intact.
93
- - Upgrade and operator docs now cover the full `0.8.1` package surface end to end.
89
+ - Answering a human-feedback request now reconciles linked clarification, escalation, and helper-assignment state back into the canonical coordination log instead of only updating the feedback JSON.
90
+ - `wave feedback respond --run <id>` now applies that same reconciliation and safe continuation flow to ad-hoc runs instead of writing into the roadmap lane state root.
91
+ - When a stranded wave can safely continue after the answer arrives and no attempt is still running, Wave writes a one-shot continuation request automatically.
92
+ - The completed-wave control-status hardening from `0.8.2` remains intact.
93
+ - Upgrade and operator docs now cover the full `0.8.3` package surface end to end.
94
94
 
95
95
  Requirements:
96
96
 
@@ -1,6 +1,6 @@
1
1
  # Current State
2
2
 
3
- - The starter workspace in this source repo reflects the `0.8.1` package release surface.
3
+ - The starter workspace in this source repo reflects the `0.8.3` package release surface.
4
4
  - The staged architecture cutover from launcher-centric decisions to reducer and phase-engine ownership is tracked in `docs/plans/architecture-hardening-migration.md`.
5
5
  - The repository contains the published `@chllming/wave-orchestration` package plus the starter scaffold used by `wave init`.
6
6
  - The runtime is package-first and non-destructive for adopting repos: `wave init --adopt-existing` records existing repo-owned plans, waves, prompts, and config without overwriting them, and `wave upgrade` writes only `.wave/install-state.json` plus `.wave/upgrade-history/`.
@@ -15,6 +15,7 @@
15
15
  - `wave adhoc plan|run|list|show|promote` manage transient operator-driven work
16
16
  - requests, generated specs, rendered markdown, and final results live under `.wave/adhoc/runs/<run-id>/`
17
17
  - runtime state stays isolated under `.tmp/<lane>-wave-launcher/adhoc/<run-id>/`
18
+ - `wave feedback respond --run <run-id>` now reconciles answered human-input state inside that isolated ad-hoc state root and can queue a safe one-shot continuation request without touching roadmap state
18
19
  - ad-hoc runs always keep integration, documentation, and cont-QA closure, while `cont-EVAL` and security review are synthesized only when the request needs them
19
20
  - documentation closure still queues canonical shared-plan docs when a run reports a shared-plan delta, alongside the ad-hoc closure report
20
21
  - `wave adhoc promote` copies the stored ad-hoc spec into numbered roadmap artifacts instead of re-deriving it from the current project profile
@@ -39,6 +40,7 @@
39
40
  - hermetic `traceVersion: 2` per-attempt trace bundles with copied launched-agent summaries, copied component matrices for promoted waves, a hashed `outcome.json` replay baseline, run metadata, and cumulative quality metrics
40
41
  - an internal, read-only replay validator for trace bundles, with legacy `traceVersion: 1` bundles kept in best-effort warning mode
41
42
  - orchestrator-first clarification triage plus human escalation artifacts
43
+ - answered human-feedback responses that reconcile canonical coordination state, helper assignments, and safe continuation intent even when the launcher is no longer active
42
44
  - optional `--resident-orchestrator` support for a long-running, non-owning orchestrator session during live waves
43
45
  - persisted relaunch plans under `.tmp/<lane>-wave-launcher/status/` so targeted retry intent can survive a launcher restart
44
46
  - a canonical control-plane event log under `.tmp/<lane>-wave-launcher/control-plane/` that records operator tasks, rerun requests, proof bundles, attempt lifecycle, and human-input events as append-only JSONL; `wave control` materializes state from this log
@@ -2,7 +2,7 @@
2
2
 
3
3
  This is a showcase-first sample wave.
4
4
 
5
- Use it as the single reference example for the current `0.8.1` Wave surface.
5
+ Use it as the single reference example for the current `0.8.3` Wave surface.
6
6
 
7
7
  It intentionally combines more sections than a normal production wave so one file can demonstrate:
8
8
 
@@ -24,13 +24,13 @@ GitHub Packages remains available as an authenticated fallback path, and maintai
24
24
  - Fresh `wave init` seeds the starter `skills/` library. `wave init --adopt-existing` records existing repo-owned skill bundles when they are already present, but does not replace or rewrite them.
25
25
  - The current runtime expects the post-roadmap model: typed coordination, compiled inboxes, `A8` integration, staged closure, orchestrator-first clarification, and operational runtime policy.
26
26
 
27
- ## Upgrading From 0.6.x To 0.8.1
27
+ ## Upgrading From 0.6.x To 0.8.3
28
28
 
29
29
  Read `CHANGELOG.md` first, then treat this section as the repo-owned migration checklist for adopted `0.6.x` workspaces.
30
30
 
31
31
  `wave upgrade` updates the installed runtime only. It does not copy planner starter files into a repo that already owns its docs, skills, and Context7 bundles.
32
32
 
33
- `0.8.1` carries forward the `0.8.0` architecture-hardening release surface and tightens helper-assignment closure handling: `resolved-by-policy` records now default to `resolved`, single-target helper requests can close from policy follow-up without mutating the original request, and multi-target helper requests only close from assignment-specific policy evidence so sibling assignments stay honest.
33
+ `0.8.3` carries forward the `0.8.2` completed-wave control-status hardening and fixes the human-answer reconciliation path: answered feedback now closes the linked clarification or escalation chain in canonical coordination, re-syncs helper-assignment projections, and preserves ad-hoc `--run <id>` context when writing safe continuation intent.
34
34
 
35
35
  ### Required Repo Changes
36
36
 
@@ -42,7 +42,7 @@ If the repo adopted Wave before the planner corpus became a tracked required sur
42
42
  - `docs/reference/wave-planning-lessons.md`
43
43
  - the `planner-agentic` bundle entry in `docs/context7/bundles.json`
44
44
 
45
- If the repo copied the shipped starter architecture docs or skills and wants the `0.8.1` authority-model language, also sync:
45
+ If the repo copied the shipped starter architecture docs or skills and wants the `0.8.3` authority-model language, also sync:
46
46
 
47
47
  - `docs/agents/wave-launcher-role.md`
48
48
  - `docs/agents/wave-orchestrator-role.md`
@@ -57,6 +57,7 @@ After syncing those repo-owned files:
57
57
  1. Run `pnpm exec wave doctor`.
58
58
  2. Run `pnpm exec wave launch --lane main --dry-run --no-dashboard`.
59
59
  3. Use `pnpm exec wave dashboard --lane <lane> --attach current` or `--attach global` when you need to reattach to a live tmux-backed dashboard without reverse-engineering the socket or session name.
60
+ 4. If your operators answer human-input tickets through `wave feedback respond`, update any repo-local runbooks so ad-hoc runs always pass `--run <id>` when responding outside the main roadmap lane.
60
61
 
61
62
  ## Upgrading From 0.5.4 To 0.6.1
62
63
 
@@ -297,12 +297,14 @@ wave feedback ask \
297
297
 
298
298
  ```
299
299
  wave feedback respond \
300
- --id <request-id> --response "<text>" \
300
+ [--run <id>] --id <request-id> --response "<text>" \
301
301
  [--operator <name>] [--force]
302
302
  ```
303
303
 
304
304
  `--force` overrides a previously answered request.
305
305
 
306
+ When the answered request belongs to a live wave or ad-hoc run, `wave feedback respond` also reconciles the linked clarification, escalation, and helper-assignment state in canonical coordination. If no attempt is still running and the reducer can safely continue, it writes a one-shot continuation request instead of relaunching directly. Use `--run <id>` when answering an ad-hoc request so reconciliation targets the isolated ad-hoc state root.
307
+
306
308
  **List feedback requests:**
307
309
 
308
310
  ```
@@ -321,6 +323,8 @@ wave feedback watch [--lane <lane>] [--wave <n>] [--agent <id>] [--pending] [--r
321
323
  wave feedback show --id <request-id>
322
324
  ```
323
325
 
326
+ All `wave feedback` subcommands accept `--run <id>` for ad-hoc runs.
327
+
324
328
  ## wave dep
325
329
 
326
330
  Cross-lane dependency management.
@@ -250,6 +250,17 @@ pnpm exec wave control task act resolve --lane main --wave 10 --id escalation-cl
250
250
 
251
251
  That keeps clarification routing, dismissal, escalation, and human-answer handling inside the canonical coordination state instead of forcing ad hoc file edits.
252
252
 
253
+ When the operator answers through the feedback queue directly, the answer path now repairs the same canonical state:
254
+
255
+ ```bash
256
+ pnpm exec wave feedback respond \
257
+ --id 202603240000-main-w6-A3-abc123 \
258
+ --response "Use the 90-day compatibility window documented in docs/plans/migration.md." \
259
+ --operator ops-lead
260
+ ```
261
+
262
+ For ad-hoc runs, include `--run <id>` on that command. The response path will reconcile the linked clarification or escalation chain, re-sync helper-assignment projections, and write a safe one-shot continuation request when the reducer can resume but no active attempt is still running.
263
+
253
264
  ## End-To-End Example: Required Dependency
254
265
 
255
266
  Assume the wave needs another lane to land a required API first.
@@ -2,7 +2,7 @@
2
2
 
3
3
  This repo now includes a dedicated npmjs publish workflow at [publish-npm.yml](../../.github/workflows/publish-npm.yml).
4
4
 
5
- The current `0.8.1` release procedure publishes through a repository Actions secret named `NPM_TOKEN`.
5
+ The current `0.8.3` release procedure publishes through a repository Actions secret named `NPM_TOKEN`.
6
6
 
7
7
  ## What This Repo Already Does
8
8
 
@@ -47,6 +47,6 @@ If this repo later needs private npm dependencies during CI, consider a separate
47
47
  1. Confirm [publish-npm.yml](../../.github/workflows/publish-npm.yml) is on the default branch.
48
48
  2. Confirm `NPM_TOKEN` exists in the GitHub repo secrets.
49
49
  3. Confirm the package version has been bumped and committed.
50
- 4. Push the release commit and release tag, for example `v0.8.1`.
50
+ 4. Push the release commit and release tag, for example `v0.8.3`.
51
51
  5. Verify both `publish-npm.yml` and `publish-package.yml` start from the tag push.
52
52
  6. Verify the npmjs publish completes successfully for the tagged source.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  title: "Sample Waves"
3
- summary: "Showcase-first sample waves that demonstrate the current 0.8.1 Wave surface."
3
+ summary: "Showcase-first sample waves that demonstrate the current 0.8.3 Wave surface."
4
4
  ---
5
5
 
6
6
  # Sample Waves
7
7
 
8
- This guide points to showcase-first sample waves that demonstrate the current `0.8.1` authored Wave surface.
8
+ This guide points to showcase-first sample waves that demonstrate the current `0.8.3` authored Wave surface.
9
9
 
10
10
  The examples are intentionally denser than typical production waves. Their job is to teach the current authoring and runtime surface quickly, not to be the smallest possible launch-ready files.
11
11
 
@@ -15,7 +15,7 @@ The examples are intentionally denser than typical production waves. Their job i
15
15
  Shows what a good `repo-landed` outcome looks like when one promoted component only closes honestly if desired-state records, reconcile-loop substrate, and cluster-view surfaces land together. It emphasizes maturity discipline, explicit deliverables, and shared-plan closure without drifting into `pilot-live` claims.
16
16
 
17
17
  - [Full modern sample wave](../plans/examples/wave-example-live-proof.md)
18
- Shows the combined `0.8.1` authored surface in one file: closure roles, `E0`, optional security review, delegated and pinned benchmark targets, richer executor config, `### Skills`, `### Capabilities`, `### Deliverables`, `### Exit contract`, `### Proof artifacts`, sticky retry, deploy environments, and proof-first live-wave structure.
18
+ Shows the combined `0.8.3` authored surface in one file: closure roles, `E0`, optional security review, delegated and pinned benchmark targets, richer executor config, `### Skills`, `### Capabilities`, `### Deliverables`, `### Exit contract`, `### Proof artifacts`, sticky retry, deploy environments, and proof-first live-wave structure.
19
19
 
20
20
  ## What These Examples Teach
21
21
 
@@ -38,7 +38,7 @@ The examples are intentionally denser than typical production waves. Their job i
38
38
 
39
39
  ## Feature Coverage Map
40
40
 
41
- Together these samples cover the main surfaces added or hardened for `0.8.1`:
41
+ Together these samples cover the main surfaces added or hardened for `0.8.3`:
42
42
 
43
43
  - repo-landed maturity discipline and anti-overclaim framing
44
44
  - explicit shared-plan closure for future-wave safety
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chllming/wave-orchestration",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "license": "MIT",
5
5
  "description": "Generic wave-based multi-agent orchestration for repository work.",
6
6
  "repository": {
@@ -2,6 +2,45 @@
2
2
  "schemaVersion": 1,
3
3
  "packageName": "@chllming/wave-orchestration",
4
4
  "releases": [
5
+ {
6
+ "version": "0.8.3",
7
+ "date": "2026-03-24",
8
+ "summary": "Human-input reconciliation repair, ad-hoc feedback-run context hardening, and 0.8.3 release-surface alignment.",
9
+ "features": [
10
+ "Answered human-feedback requests now reconcile linked clarification, escalation, and helper-assignment state back into the canonical coordination log instead of only updating the feedback request JSON.",
11
+ "`wave feedback respond --run <id>` now preserves ad-hoc run context while writing reconciliation and safe continuation state, so ad-hoc answers stop mutating the roadmap lane state root.",
12
+ "When no attempt is still running and the reducer can safely continue, human-input reconciliation writes a one-shot continuation request instead of leaving the wave stranded after the operator answer arrives.",
13
+ "Regression coverage now exercises both direct feedback-response reconciliation and the ad-hoc `--run <id>` path.",
14
+ "Shipped package metadata, README, migration guidance, current-state notes, sample-wave docs, and npm publishing instructions now point at the `0.8.3` release surface."
15
+ ],
16
+ "manualSteps": [
17
+ "If your operators answer tickets through `wave feedback respond`, update any repo-local runbooks so ad-hoc runs always pass `--run <id>` when answering outside the main roadmap lane.",
18
+ "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.3` runtime and human-input reconciliation contract.",
19
+ "If an adopted repo fails `wave doctor` after the upgrade, sync the repo-owned planner starter surface (`docs/agents/wave-planner-role.md`, `skills/role-planner/`, `docs/context7/planner-agent/`, `docs/reference/wave-planning-lessons.md`, and the `planner-agentic` bundle entry) before relying on planner-aware validation.",
20
+ "If a repo copied the shipped starter architecture docs or skills and wants the `0.8.3` operator guidance, sync the updated launcher and orchestrator role prompts plus the relevant runtime and closure-role skill bundles."
21
+ ],
22
+ "breaking": false
23
+ },
24
+ {
25
+ "version": "0.8.2",
26
+ "date": "2026-03-24",
27
+ "summary": "Completed-wave control-status projection hardening and 0.8.2 release-surface alignment.",
28
+ "features": [
29
+ "`wave control status` now clears stale blocking edges after a wave has already reached `phase=completed`, instead of replaying historical open request records as live blockers.",
30
+ "Completed waves now suppress stale `nextTimer` deadlines in the control-status projection.",
31
+ "Successful logical-agent states are preserved for completed waves instead of being re-blocked by stale coordination history in the control-status surface.",
32
+ "Regression coverage now exercises the completed-wave stale-blocking projection path directly.",
33
+ "Shipped package metadata, README, migration guidance, sample-wave docs, and npm publishing instructions now point at the `0.8.2` release surface."
34
+ ],
35
+ "manualSteps": [
36
+ "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.2` runtime and completed-wave control-status contract.",
37
+ "If an adopted repo fails `wave doctor` after the upgrade, sync the repo-owned planner starter surface (`docs/agents/wave-planner-role.md`, `skills/role-planner/`, `docs/context7/planner-agent/`, `docs/reference/wave-planning-lessons.md`, and the `planner-agentic` bundle entry) before relying on planner-aware validation.",
38
+ "If a repo carries custom operator docs for `wave control status`, update that guidance so completed waves are described as terminal projections where historical coordination records may remain visible without reopening blockers.",
39
+ "If an adopted repo copied the starter architecture docs or skill bundles, sync the updated role prompts, `skills/wave-core/`, runtime skills, and closure-role skills so local guidance matches the `0.8.2` authority model and completed-wave control behavior.",
40
+ "Review `docs/plans/architecture-hardening-migration.md` before continuing reducer-authoritative cutover work in repos that extend the shipped starter surface."
41
+ ],
42
+ "breaking": false
43
+ },
5
44
  {
6
45
  "version": "0.8.1",
7
46
  "date": "2026-03-24",
@@ -6,6 +6,7 @@ import { readWaveHumanFeedbackRequests } from "./coordination.mjs";
6
6
  import { readWaveLedger } from "./ledger.mjs";
7
7
  import { buildDependencySnapshot, buildRequestAssignments } from "./routing-state.mjs";
8
8
  import { parseWaveFiles } from "./wave-files.mjs";
9
+ import { answerHumanInputAndReconcile } from "./human-input-resolution.mjs";
9
10
  import {
10
11
  buildLanePaths,
11
12
  DEFAULT_COORDINATION_ACK_TIMEOUT_MS,
@@ -270,6 +271,10 @@ function assignmentRelevantToAgent(assignment, agentId = "") {
270
271
  );
271
272
  }
272
273
 
274
+ function isCompletedPhase(phase) {
275
+ return String(phase || "").trim().toLowerCase() === "completed";
276
+ }
277
+
273
278
  function buildEffectiveSelection(lanePaths, wave, { activeAttempt = null, rerunRequest = null, relaunchPlan = null } = {}) {
274
279
  const activeAttemptSelected = Array.isArray(activeAttempt?.selectedAgentIds)
275
280
  ? Array.from(new Set(activeAttempt.selectedAgentIds.filter(Boolean)))
@@ -308,10 +313,20 @@ function buildEffectiveSelection(lanePaths, wave, { activeAttempt = null, rerunR
308
313
  };
309
314
  }
310
315
 
311
- function buildLogicalAgents({ lanePaths, wave, tasks, dependencySnapshot, capabilityAssignments, selection, proofRegistry }) {
316
+ function buildLogicalAgents({
317
+ lanePaths,
318
+ wave,
319
+ tasks,
320
+ dependencySnapshot,
321
+ capabilityAssignments,
322
+ selection,
323
+ proofRegistry,
324
+ phase,
325
+ }) {
312
326
  const selectedAgentIds = new Set(selection?.selectedAgentIds || []);
313
327
  const helperAssignments = Array.isArray(capabilityAssignments) ? capabilityAssignments : [];
314
328
  const openInbound = dependencySnapshot?.openInbound || [];
329
+ const completedPhase = isCompletedPhase(phase);
315
330
  return wave.agents.map((agent) => {
316
331
  const statusPath = statusPathForAgent(lanePaths, wave, agent);
317
332
  const statusRecord = readStatusRecordIfPresent(statusPath);
@@ -343,6 +358,11 @@ function buildLogicalAgents({ lanePaths, wave, tasks, dependencySnapshot, capabi
343
358
  (assignment) => assignment.blocking && assignment.assignedAgentId === agent.agentId,
344
359
  );
345
360
  const dependency = openInbound.find((record) => record.assignedAgentId === agent.agentId);
361
+ const satisfiedByStatus =
362
+ statusRecord?.code === 0 &&
363
+ (proofValidation.ok ||
364
+ isSecurityReviewAgent(agent) ||
365
+ isContEvalReportOnlyAgent(agent, { contEvalAgentId: lanePaths.contEvalAgentId }));
346
366
  let state = "planned";
347
367
  let reason = "";
348
368
  if (selection?.source === "active-attempt" && selectedAgentIds.has(agent.agentId)) {
@@ -354,6 +374,16 @@ function buildLogicalAgents({ lanePaths, wave, tasks, dependencySnapshot, capabi
354
374
  selection?.source === "relaunch-plan"
355
375
  ? "Selected by the persisted relaunch plan."
356
376
  : "Selected by active rerun request.";
377
+ } else if (completedPhase && satisfiedByStatus) {
378
+ state = [
379
+ lanePaths.contEvalAgentId || "E0",
380
+ lanePaths.integrationAgentId || "A8",
381
+ lanePaths.documentationAgentId || "A9",
382
+ lanePaths.contQaAgentId || "A0",
383
+ ].includes(agent.agentId) || isSecurityReviewAgent(agent)
384
+ ? "closed"
385
+ : "satisfied";
386
+ reason = "Completed wave preserves the latest satisfied agent state.";
357
387
  } else if (targetedBlockingTasks.some((task) => task.state === "working")) {
358
388
  state = "working";
359
389
  reason = targetedBlockingTasks.find((task) => task.state === "working")?.title || "";
@@ -365,7 +395,7 @@ function buildLogicalAgents({ lanePaths, wave, tasks, dependencySnapshot, capabi
365
395
  helperAssignment?.summary ||
366
396
  dependency?.summary ||
367
397
  "";
368
- } else if (statusRecord?.code === 0 && (proofValidation.ok || isSecurityReviewAgent(agent) || isContEvalReportOnlyAgent(agent, { contEvalAgentId: lanePaths.contEvalAgentId }))) {
398
+ } else if (satisfiedByStatus) {
369
399
  state = [
370
400
  lanePaths.contEvalAgentId || "E0",
371
401
  lanePaths.integrationAgentId || "A8",
@@ -401,7 +431,19 @@ function selectionTargetsAgent(agentId, selectionSet) {
401
431
  return Boolean(agentId) && selectionSet.has(agentId);
402
432
  }
403
433
 
404
- function buildBlockingEdge({ tasks, capabilityAssignments, dependencySnapshot, activeAttempt, rerunRequest, relaunchPlan, agentId = "" }) {
434
+ function buildBlockingEdge({
435
+ tasks,
436
+ capabilityAssignments,
437
+ dependencySnapshot,
438
+ activeAttempt,
439
+ rerunRequest,
440
+ relaunchPlan,
441
+ agentId = "",
442
+ phase,
443
+ }) {
444
+ if (isCompletedPhase(phase)) {
445
+ return null;
446
+ }
405
447
  const attemptSelection = new Set(activeAttempt?.selectedAgentIds || []);
406
448
  const scopeToActiveAttempt = !agentId && attemptSelection.size > 0;
407
449
  const scopedTasks = (agentId
@@ -546,6 +588,7 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
546
588
  const logPath = coordinationLogPath(lanePaths, wave.wave);
547
589
  const coordinationState = readMaterializedCoordinationState(logPath);
548
590
  const ledger = readWaveLedger(ledgerPath(lanePaths, wave.wave)) || { phase: "planned" };
591
+ const phase = ledger.phase || "unknown";
549
592
  const capabilityAssignments = buildRequestAssignments({
550
593
  coordinationState,
551
594
  agents: wave.agents,
@@ -596,7 +639,7 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
596
639
  return {
597
640
  lane: lanePaths.lane,
598
641
  wave: wave.wave,
599
- phase: ledger.phase || "unknown",
642
+ phase,
600
643
  agentId: agentId || null,
601
644
  blockingEdge: buildBlockingEdge({
602
645
  tasks,
@@ -606,6 +649,7 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
606
649
  rerunRequest,
607
650
  relaunchPlan,
608
651
  agentId,
652
+ phase,
609
653
  }),
610
654
  logicalAgents: buildLogicalAgents({
611
655
  lanePaths,
@@ -615,6 +659,7 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
615
659
  capabilityAssignments,
616
660
  selection,
617
661
  proofRegistry,
662
+ phase,
618
663
  }).filter((agent) => !agentId || agent.agentId === agentId),
619
664
  tasks,
620
665
  helperAssignments: (capabilityAssignments || []).filter(
@@ -634,7 +679,7 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
634
679
  selectionSource: selection.source,
635
680
  rerunRequest,
636
681
  relaunchPlan,
637
- nextTimer: nextTaskDeadline(tasks),
682
+ nextTimer: isCompletedPhase(phase) ? null : nextTaskDeadline(tasks),
638
683
  activeAttempt: controlState.activeAttempt,
639
684
  };
640
685
  }
@@ -973,6 +1018,13 @@ export async function runControlCli(argv) {
973
1018
  operator: options.operator,
974
1019
  force: true,
975
1020
  });
1021
+ answerHumanInputAndReconcile({
1022
+ lanePaths,
1023
+ wave,
1024
+ requestId: options.id,
1025
+ answeredPayload: answered,
1026
+ operator: options.operator,
1027
+ });
976
1028
  appendWaveControlEvent(lanePaths, wave.wave, {
977
1029
  entityType: "human_input",
978
1030
  entityId: options.id,
@@ -23,6 +23,7 @@ import {
23
23
  writeJsonArtifact,
24
24
  } from "./coordination-store.mjs";
25
25
  import { answerFeedbackRequest } from "./feedback.mjs";
26
+ import { answerHumanInputAndReconcile } from "./human-input-resolution.mjs";
26
27
  import { readWaveHumanFeedbackRequests } from "./coordination.mjs";
27
28
  import { readWaveProofRegistry } from "./proof-registry.mjs";
28
29
  import {
@@ -463,6 +464,13 @@ export async function runCoordinationCli(argv) {
463
464
  force: true,
464
465
  recordTelemetry: true,
465
466
  });
467
+ answerHumanInputAndReconcile({
468
+ lanePaths,
469
+ wave,
470
+ requestId: options.id,
471
+ answeredPayload: answered,
472
+ operator: options.operator,
473
+ });
466
474
  console.log(JSON.stringify(answered, null, 2));
467
475
  return;
468
476
  }
@@ -21,6 +21,7 @@ import {
21
21
  truncate,
22
22
  writeJsonAtomic,
23
23
  } from "./shared.mjs";
24
+ import { answerHumanInputByRequest } from "./human-input-resolution.mjs";
24
25
  import { safeQueueWaveControlEvent } from "./wave-control-client.mjs";
25
26
 
26
27
  function sanitizeToken(value) {
@@ -363,7 +364,7 @@ export async function runFeedbackCli(argv) {
363
364
  if (!options.id || !options.response) {
364
365
  throw new Error("respond requires --id and --response");
365
366
  }
366
- answerFeedbackRequest({
367
+ const answered = answerFeedbackRequest({
367
368
  feedbackStateDir: stateDir,
368
369
  feedbackRequestsDir: requestsDir,
369
370
  requestId: options.id,
@@ -372,6 +373,15 @@ export async function runFeedbackCli(argv) {
372
373
  force: options.force,
373
374
  recordTelemetry: true,
374
375
  });
376
+ if (answered?.lane && Number.isFinite(Number(answered.wave))) {
377
+ answerHumanInputByRequest({
378
+ lane: answered.lane,
379
+ waveNumber: Number(answered.wave),
380
+ requestId: options.id,
381
+ operator: options.operator,
382
+ runId: options.runId || null,
383
+ });
384
+ }
375
385
  console.log(`[wave-human-feedback] answered ${options.id}`);
376
386
  return;
377
387
  }
@@ -0,0 +1,344 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ appendCoordinationRecord,
5
+ clarificationClosureCondition,
6
+ clarificationLinkedRequests,
7
+ isOpenCoordinationStatus,
8
+ readMaterializedCoordinationState,
9
+ } from "./coordination-store.mjs";
10
+ import { readWaveHumanFeedbackRequests } from "./coordination.mjs";
11
+ import {
12
+ readControlPlaneEvents,
13
+ readWaveControlPlaneState,
14
+ syncWaveControlPlaneProjections,
15
+ waveControlPlaneLogPath,
16
+ } from "./control-plane.mjs";
17
+ import { readWaveLedger } from "./ledger.mjs";
18
+ import { readRunExecutionSummary } from "./launcher-gates.mjs";
19
+ import { buildResumePlan, clearWaveRelaunchPlan } from "./launcher-retry.mjs";
20
+ import { readWaveProofRegistry } from "./proof-registry.mjs";
21
+ import { buildDependencySnapshot, buildRequestAssignments, syncAssignmentRecords } from "./routing-state.mjs";
22
+ import { buildLanePaths } from "./shared.mjs";
23
+ import { reduceWaveState } from "./wave-state-reducer.mjs";
24
+ import { parseWaveFiles } from "./wave-files.mjs";
25
+ import { writeWaveRetryOverride } from "./retry-control.mjs";
26
+
27
+ function coordinationLogPath(lanePaths, waveNumber) {
28
+ return path.join(lanePaths.coordinationDir, `wave-${waveNumber}.jsonl`);
29
+ }
30
+
31
+ function ledgerPath(lanePaths, waveNumber) {
32
+ return path.join(lanePaths.ledgerDir, `wave-${waveNumber}.json`);
33
+ }
34
+
35
+ function coordinationTriagePath(lanePaths, waveNumber) {
36
+ return path.join(lanePaths.feedbackTriageDir, `wave-${waveNumber}.jsonl`);
37
+ }
38
+
39
+ function appendCoordinationStatusUpdate(logPath, record, status, options = {}) {
40
+ return appendCoordinationRecord(logPath, {
41
+ ...record,
42
+ status,
43
+ summary: options.summary || record.summary,
44
+ detail: options.detail || record.detail,
45
+ source: options.source || "operator",
46
+ });
47
+ }
48
+
49
+ function appendTriageEscalationUpdateIfPresent(lanePaths, waveNumber, record) {
50
+ const triagePath = coordinationTriagePath(lanePaths, waveNumber);
51
+ if (!fs.existsSync(triagePath) || record?.kind !== "human-escalation") {
52
+ return;
53
+ }
54
+ appendCoordinationRecord(triagePath, record);
55
+ }
56
+
57
+ function loadWave(lanePaths, waveNumber) {
58
+ const waves = parseWaveFiles(lanePaths.wavesDir, { laneProfile: lanePaths.laneProfile });
59
+ const wave = waves.find((entry) => entry.wave === waveNumber);
60
+ if (!wave) {
61
+ throw new Error(`Wave ${waveNumber} not found in ${lanePaths.wavesDir}`);
62
+ }
63
+ return wave;
64
+ }
65
+
66
+ function taskRunInfoForAgent(lanePaths, wave, agent, proofRegistry) {
67
+ const safeName = `wave-${wave.wave}-${agent.slug}`;
68
+ return {
69
+ agent,
70
+ logPath: path.join(lanePaths.logsDir, `${safeName}.log`),
71
+ statusPath: path.join(lanePaths.statusDir, `${safeName}.status`),
72
+ proofRegistry,
73
+ };
74
+ }
75
+
76
+ function feedbackLinkMatchesRecord(record, requestId) {
77
+ const normalizedRequestId = String(requestId || "").trim();
78
+ if (!normalizedRequestId) {
79
+ return false;
80
+ }
81
+ if (String(record?.id || "").trim() === normalizedRequestId) {
82
+ return true;
83
+ }
84
+ if (
85
+ Array.isArray(record?.artifactRefs) &&
86
+ record.artifactRefs.some((ref) => String(ref || "").trim() === normalizedRequestId)
87
+ ) {
88
+ return true;
89
+ }
90
+ if (
91
+ Array.isArray(record?.dependsOn) &&
92
+ record.dependsOn.some((value) => String(value || "").trim() === normalizedRequestId)
93
+ ) {
94
+ return true;
95
+ }
96
+ return false;
97
+ }
98
+
99
+ function linkedClarificationIdsForRecords(records) {
100
+ const ids = new Set();
101
+ for (const record of records || []) {
102
+ if (record?.kind === "clarification-request" && record?.id) {
103
+ ids.add(record.id);
104
+ continue;
105
+ }
106
+ const closureCondition = String(record?.closureCondition || "").trim();
107
+ if (closureCondition.startsWith("clarification:")) {
108
+ ids.add(closureCondition.slice("clarification:".length));
109
+ }
110
+ }
111
+ return [...ids].filter(Boolean);
112
+ }
113
+
114
+ export function resolveFeedbackLinkedCoordination({
115
+ lanePaths,
116
+ wave,
117
+ requestId,
118
+ operator = "human-operator",
119
+ detail = "",
120
+ }) {
121
+ const logPath = coordinationLogPath(lanePaths, wave.wave);
122
+ const state = readMaterializedCoordinationState(logPath);
123
+ const resolvedRecords = [];
124
+
125
+ const directlyLinked = state.latestRecords.filter((record) =>
126
+ isOpenCoordinationStatus(record.status) && feedbackLinkMatchesRecord(record, requestId),
127
+ );
128
+
129
+ for (const record of directlyLinked) {
130
+ const updated = appendCoordinationStatusUpdate(logPath, record, "resolved", {
131
+ detail: detail || `Resolved after answered human input ${requestId}.`,
132
+ summary: record.summary,
133
+ source: operator,
134
+ });
135
+ resolvedRecords.push(updated);
136
+ appendTriageEscalationUpdateIfPresent(lanePaths, wave.wave, updated);
137
+ }
138
+
139
+ const nextState = readMaterializedCoordinationState(logPath);
140
+ const clarificationIds = linkedClarificationIdsForRecords([
141
+ ...directlyLinked,
142
+ ...resolvedRecords,
143
+ ]);
144
+ for (const clarificationId of clarificationIds) {
145
+ const clarification = nextState.byId.get(clarificationId);
146
+ if (!clarification || !isOpenCoordinationStatus(clarification.status)) {
147
+ continue;
148
+ }
149
+ const updatedClarification = appendCoordinationStatusUpdate(logPath, clarification, "resolved", {
150
+ detail: detail || `Resolved after answered human input ${requestId}.`,
151
+ summary: clarification.summary,
152
+ source: operator,
153
+ });
154
+ resolvedRecords.push(updatedClarification);
155
+ }
156
+
157
+ const resolvedState = readMaterializedCoordinationState(logPath);
158
+ for (const clarificationId of clarificationIds) {
159
+ const linkedRequests = clarificationLinkedRequests(resolvedState, clarificationId).filter((entry) =>
160
+ isOpenCoordinationStatus(entry.status),
161
+ );
162
+ for (const linked of linkedRequests) {
163
+ const updatedLinked = appendCoordinationStatusUpdate(logPath, linked, "resolved", {
164
+ detail: `Resolved via clarification ${clarificationId}.`,
165
+ summary: linked.summary,
166
+ source: operator,
167
+ });
168
+ resolvedRecords.push(updatedLinked);
169
+ }
170
+ for (const escalation of (resolvedState.humanEscalations || []).filter(
171
+ (entry) =>
172
+ isOpenCoordinationStatus(entry.status) &&
173
+ entry.closureCondition === clarificationClosureCondition(clarificationId),
174
+ )) {
175
+ const updatedEscalation = appendCoordinationStatusUpdate(logPath, escalation, "resolved", {
176
+ detail: detail || `Resolved via clarification ${clarificationId}.`,
177
+ summary: escalation.summary,
178
+ source: operator,
179
+ });
180
+ resolvedRecords.push(updatedEscalation);
181
+ appendTriageEscalationUpdateIfPresent(lanePaths, wave.wave, updatedEscalation);
182
+ }
183
+ }
184
+
185
+ const assignmentState = readMaterializedCoordinationState(logPath);
186
+ const ledger = readWaveLedger(ledgerPath(lanePaths, wave.wave)) || { phase: "planned" };
187
+ const assignments = buildRequestAssignments({
188
+ coordinationState: assignmentState,
189
+ agents: wave.agents,
190
+ ledger,
191
+ capabilityRouting: lanePaths.capabilityRouting,
192
+ });
193
+ syncAssignmentRecords(logPath, {
194
+ lane: lanePaths.lane,
195
+ wave: wave.wave,
196
+ assignments,
197
+ });
198
+
199
+ return {
200
+ resolvedRecords,
201
+ clarificationIds,
202
+ };
203
+ }
204
+
205
+ export function buildResumePlanFromDisk({ lanePaths, wave }) {
206
+ const proofRegistry = readWaveProofRegistry(lanePaths, wave.wave);
207
+ const agentRuns = wave.agents.map((agent) =>
208
+ taskRunInfoForAgent(lanePaths, wave, agent, proofRegistry),
209
+ );
210
+ const agentResults = Object.fromEntries(
211
+ agentRuns
212
+ .map((runInfo) => [runInfo.agent.agentId, readRunExecutionSummary(runInfo, wave)])
213
+ .filter(([, summary]) => Boolean(summary)),
214
+ );
215
+ const coordinationState = readMaterializedCoordinationState(coordinationLogPath(lanePaths, wave.wave));
216
+ const feedbackRequests = readWaveHumanFeedbackRequests({
217
+ feedbackRequestsDir: lanePaths.feedbackRequestsDir,
218
+ lane: lanePaths.lane,
219
+ waveNumber: wave.wave,
220
+ agentIds: wave.agents.map((agent) => agent.agentId),
221
+ orchestratorId: "",
222
+ });
223
+ const ledger = readWaveLedger(ledgerPath(lanePaths, wave.wave)) || { phase: "planned" };
224
+ const dependencySnapshot = buildDependencySnapshot({
225
+ dirPath: lanePaths.crossLaneDependenciesDir,
226
+ lane: lanePaths.lane,
227
+ waveNumber: wave.wave,
228
+ agents: wave.agents,
229
+ ledger,
230
+ capabilityRouting: lanePaths.capabilityRouting,
231
+ });
232
+ const reducerState = reduceWaveState({
233
+ controlPlaneEvents: readControlPlaneEvents(waveControlPlaneLogPath(lanePaths, wave.wave)),
234
+ coordinationRecords: coordinationState.latestRecords || [],
235
+ agentResults,
236
+ waveDefinition: wave,
237
+ dependencyTickets: dependencySnapshot,
238
+ feedbackRequests,
239
+ laneConfig: {
240
+ lane: lanePaths.lane,
241
+ contQaAgentId: lanePaths.contQaAgentId || "A0",
242
+ contEvalAgentId: lanePaths.contEvalAgentId || "E0",
243
+ integrationAgentId: lanePaths.integrationAgentId || "A8",
244
+ documentationAgentId: lanePaths.documentationAgentId || "A9",
245
+ validationMode: "live",
246
+ evalTargets: wave.evalTargets,
247
+ benchmarkCatalogPath: lanePaths.laneProfile?.paths?.benchmarkCatalogPath,
248
+ laneProfile: lanePaths.laneProfile,
249
+ requireIntegrationStewardFromWave: lanePaths.requireIntegrationStewardFromWave,
250
+ capabilityRouting: lanePaths.capabilityRouting,
251
+ },
252
+ });
253
+ return buildResumePlan(reducerState, {
254
+ waveDefinition: wave,
255
+ lanePaths,
256
+ });
257
+ }
258
+
259
+ export function maybeWriteAutoResumeRequest({
260
+ lanePaths,
261
+ wave,
262
+ requestedBy = "human-operator",
263
+ reason = "",
264
+ }) {
265
+ const controlState = readWaveControlPlaneState(lanePaths, wave.wave);
266
+ if (controlState.activeAttempt || controlState.activeRerunRequest) {
267
+ return null;
268
+ }
269
+ const resumePlan = buildResumePlanFromDisk({ lanePaths, wave });
270
+ if (!resumePlan.canResume || resumePlan.reason === "human-request") {
271
+ return { resumePlan, request: null };
272
+ }
273
+ if (resumePlan.resumeFromPhase === "completed") {
274
+ return { resumePlan, request: null };
275
+ }
276
+ clearWaveRelaunchPlan(lanePaths, wave.wave);
277
+ const payload = {
278
+ requestedBy,
279
+ reason:
280
+ reason ||
281
+ `Auto continuation after answered human input; resume from ${resumePlan.resumeFromPhase}.`,
282
+ preserveReusableAgentIds: resumePlan.reusableAgentIds,
283
+ reuseProofBundleIds: resumePlan.reusableProofBundleIds,
284
+ applyOnce: true,
285
+ };
286
+ if (resumePlan.resumeFromPhase === "implementation") {
287
+ payload.selectedAgentIds = resumePlan.invalidatedAgentIds;
288
+ } else {
289
+ payload.resumePhase = resumePlan.resumeFromPhase;
290
+ }
291
+ const request = writeWaveRetryOverride(lanePaths, wave.wave, payload);
292
+ return { resumePlan, request };
293
+ }
294
+
295
+ export function answerHumanInputAndReconcile({
296
+ lanePaths,
297
+ wave,
298
+ requestId,
299
+ answeredPayload,
300
+ operator = "human-operator",
301
+ }) {
302
+ const detail =
303
+ `Resolved after human input ${requestId} was answered by ${operator}` +
304
+ (answeredPayload?.response?.text ? `: ${answeredPayload.response.text}` : ".");
305
+ const resolution = resolveFeedbackLinkedCoordination({
306
+ lanePaths,
307
+ wave,
308
+ requestId,
309
+ operator,
310
+ detail,
311
+ });
312
+ syncWaveControlPlaneProjections(
313
+ lanePaths,
314
+ wave.wave,
315
+ readWaveControlPlaneState(lanePaths, wave.wave),
316
+ );
317
+ const autoResume = maybeWriteAutoResumeRequest({
318
+ lanePaths,
319
+ wave,
320
+ requestedBy: operator,
321
+ reason: `Auto continuation after answered human input ${requestId}.`,
322
+ });
323
+ return {
324
+ resolution,
325
+ autoResume,
326
+ };
327
+ }
328
+
329
+ export function answerHumanInputByRequest({
330
+ lane,
331
+ waveNumber,
332
+ requestId,
333
+ operator = "human-operator",
334
+ runId = null,
335
+ }) {
336
+ const lanePaths = buildLanePaths(lane, { adhocRunId: runId || null });
337
+ const wave = loadWave(lanePaths, waveNumber);
338
+ return answerHumanInputAndReconcile({
339
+ lanePaths,
340
+ wave,
341
+ requestId,
342
+ operator,
343
+ });
344
+ }