@chllming/wave-orchestration 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.8.2 - 2026-03-24
6
+
7
+ ### Changed
8
+
9
+ - 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.
10
+
11
+ ### Fixed And Hardened
12
+
13
+ - `wave control status` now stops surfacing stale blocking edges after a wave has already reached `phase=completed`.
14
+ - Completed waves now suppress stale `nextTimer` deadlines and preserve successful logical-agent states instead of re-blocking agents from historical open request records.
15
+
16
+ ### Testing And Validation
17
+
18
+ - Added regression coverage for completed-wave control-status projections so historical request records stay visible without reopening blocking state after closure.
19
+
20
+ ## 0.8.1 - 2026-03-24
21
+
22
+ ### Changed
23
+
24
+ - Updated the shipped package metadata, release manifest, README, migration guide, sample-wave docs, and npm publishing runbook to advertise `0.8.1` as the current release surface.
25
+
26
+ ### Fixed And Hardened
27
+
28
+ - Helper-assignment policy resolution now treats `resolved-by-policy` follow-up as authoritative closure without requiring the original request to be rewritten.
29
+ - Manual `wave coord post --kind resolved-by-policy` now defaults to `status=resolved`, so operator-authored policy closures stop reopening the assignment they are meant to close.
30
+ - Multi-target helper requests now require assignment-specific policy evidence before closure, preventing one request-level `resolved-by-policy` note from accidentally closing sibling assignments.
31
+
32
+ ### Testing And Validation
33
+
34
+ - Added regression coverage for default `resolved-by-policy` status handling and for multi-target assignment resolution that must not over-close sibling helper assignments.
35
+
5
36
  ## 0.8.0 - 2026-03-24
6
37
 
7
38
  ### 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.0`
83
- - Release tag: [`v0.8.0`](https://github.com/chllming/agent-wave-orchestrator/releases/tag/v0.8.0)
82
+ - `@chllming/wave-orchestration@0.8.2`
83
+ - Release tag: [`v0.8.2`](https://github.com/chllming/agent-wave-orchestrator/releases/tag/v0.8.2)
84
84
  - Public install path: npmjs
85
85
  - Authenticated fallback: GitHub Packages
86
86
 
87
- Highlights in `0.8.0`:
87
+ Highlights in `0.8.2`:
88
88
 
89
- - Reducer and task replay hardening now keeps coordination-derived task identity deterministic and strengthens authoritative replay of live wave state.
90
- - Gate evaluation, contradiction or fact schema wiring, and resume planning are aligned around control-plane state plus typed result-envelope reads.
91
- - Live launcher evaluation now computes reducer snapshots during real runs instead of leaving that path effectively test-only.
92
- - The package now ships a dedicated architecture hardening migration plan and aligns the active README, guides, role prompts, and starter skills to the canonical authority-set and thin-launcher model.
93
- - Upgrade and operator docs now cover the full `0.8.0` package surface end to end.
89
+ - `wave control status` now clears stale blocking edges once a wave is already completed instead of replaying historical open request records as live blockers.
90
+ - Completed waves now suppress stale `nextTimer` deadlines and preserve successful logical-agent states in the control-status projection.
91
+ - The helper-assignment policy-closure fixes from `0.8.1` remain intact.
92
+ - The architecture-hardening migration plan, reducer or envelope wiring, and aligned docs or skills from the prior releases remain intact.
93
+ - Upgrade and operator docs now cover the full `0.8.2` 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.0` package release surface.
3
+ - The starter workspace in this source repo reflects the `0.8.2` 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/`.
@@ -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.0` Wave surface.
5
+ Use it as the single reference example for the current `0.8.2` 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.0
27
+ ## Upgrading From 0.6.x To 0.8.2
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.0` carries forward the `0.7.3` proof-centric closure hardening and adds the first architecture-hardening release surface: reducer and retry parity fixes, result-envelope-first gate wiring, contradiction or fact schema alignment, a dedicated architecture migration plan, and starter docs or skills that now describe the canonical authority-set model instead of the old launcher-centric wording.
33
+ `0.8.2` carries forward the `0.8.1` helper-assignment fixes and hardens the control-status projection layer: completed waves no longer replay stale blocking edges or overdue timers from historical coordination records, and successful logical-agent state stays preserved once the wave is terminal.
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.0` authority-model language, also sync:
45
+ If the repo copied the shipped starter architecture docs or skills and wants the `0.8.2` authority-model language, also sync:
46
46
 
47
47
  - `docs/agents/wave-launcher-role.md`
48
48
  - `docs/agents/wave-orchestrator-role.md`
@@ -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.0` release procedure publishes through a repository Actions secret named `NPM_TOKEN`.
5
+ The current `0.8.2` 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.0`.
50
+ 4. Push the release commit and release tag, for example `v0.8.2`.
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.0 Wave surface."
3
+ summary: "Showcase-first sample waves that demonstrate the current 0.8.2 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.0` authored Wave surface.
8
+ This guide points to showcase-first sample waves that demonstrate the current `0.8.2` 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.0` 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.2` 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.0`:
41
+ Together these samples cover the main surfaces added or hardened for `0.8.2`:
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.0",
3
+ "version": "0.8.2",
4
4
  "license": "MIT",
5
5
  "description": "Generic wave-based multi-agent orchestration for repository work.",
6
6
  "repository": {
@@ -2,6 +2,46 @@
2
2
  "schemaVersion": 1,
3
3
  "packageName": "@chllming/wave-orchestration",
4
4
  "releases": [
5
+ {
6
+ "version": "0.8.2",
7
+ "date": "2026-03-24",
8
+ "summary": "Completed-wave control-status projection hardening and 0.8.2 release-surface alignment.",
9
+ "features": [
10
+ "`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.",
11
+ "Completed waves now suppress stale `nextTimer` deadlines in the control-status projection.",
12
+ "Successful logical-agent states are preserved for completed waves instead of being re-blocked by stale coordination history in the control-status surface.",
13
+ "Regression coverage now exercises the completed-wave stale-blocking projection path directly.",
14
+ "Shipped package metadata, README, migration guidance, sample-wave docs, and npm publishing instructions now point at the `0.8.2` release surface."
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.2` runtime and completed-wave control-status contract.",
18
+ "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.",
19
+ "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.",
20
+ "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.",
21
+ "Review `docs/plans/architecture-hardening-migration.md` before continuing reducer-authoritative cutover work in repos that extend the shipped starter surface."
22
+ ],
23
+ "breaking": false
24
+ },
25
+ {
26
+ "version": "0.8.1",
27
+ "date": "2026-03-24",
28
+ "summary": "Helper-assignment policy-closure hardening, multi-target assignment-safety repair, and 0.8.1 release-surface alignment.",
29
+ "features": [
30
+ "`resolved-by-policy` follow-up now closes single-target helper assignments authoritatively without forcing the original request record itself to be rewritten.",
31
+ "Manual `wave coord post --kind resolved-by-policy` now defaults to `status=resolved`, so operator-entered policy closures stop behaving like fresh open coordination records.",
32
+ "Multi-target helper requests now require assignment-specific policy evidence before closure, which prevents a request-level policy note from accidentally closing sibling assignments that were not actually completed.",
33
+ "Regression coverage now exercises default `resolved-by-policy` status handling and the multi-target helper-assignment over-closure edge case directly.",
34
+ "Shipped package metadata, README, migration guidance, sample-wave docs, and npm publishing instructions now point at the `0.8.1` release surface."
35
+ ],
36
+ "manualSteps": [
37
+ "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.1` runtime and helper-assignment closure contract.",
38
+ "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.",
39
+ "If a repo carries custom operator docs for `wave coord post --kind resolved-by-policy`, update that guidance so policy-closure examples either omit `--status` or use `--status resolved` explicitly.",
40
+ "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.1` authority model and helper-assignment behavior.",
41
+ "Review `docs/plans/architecture-hardening-migration.md` before continuing reducer-authoritative cutover work in repos that extend the shipped starter surface."
42
+ ],
43
+ "breaking": false
44
+ },
5
45
  {
6
46
  "version": "0.8.0",
7
47
  "date": "2026-03-24",
@@ -270,6 +270,10 @@ function assignmentRelevantToAgent(assignment, agentId = "") {
270
270
  );
271
271
  }
272
272
 
273
+ function isCompletedPhase(phase) {
274
+ return String(phase || "").trim().toLowerCase() === "completed";
275
+ }
276
+
273
277
  function buildEffectiveSelection(lanePaths, wave, { activeAttempt = null, rerunRequest = null, relaunchPlan = null } = {}) {
274
278
  const activeAttemptSelected = Array.isArray(activeAttempt?.selectedAgentIds)
275
279
  ? Array.from(new Set(activeAttempt.selectedAgentIds.filter(Boolean)))
@@ -308,10 +312,20 @@ function buildEffectiveSelection(lanePaths, wave, { activeAttempt = null, rerunR
308
312
  };
309
313
  }
310
314
 
311
- function buildLogicalAgents({ lanePaths, wave, tasks, dependencySnapshot, capabilityAssignments, selection, proofRegistry }) {
315
+ function buildLogicalAgents({
316
+ lanePaths,
317
+ wave,
318
+ tasks,
319
+ dependencySnapshot,
320
+ capabilityAssignments,
321
+ selection,
322
+ proofRegistry,
323
+ phase,
324
+ }) {
312
325
  const selectedAgentIds = new Set(selection?.selectedAgentIds || []);
313
326
  const helperAssignments = Array.isArray(capabilityAssignments) ? capabilityAssignments : [];
314
327
  const openInbound = dependencySnapshot?.openInbound || [];
328
+ const completedPhase = isCompletedPhase(phase);
315
329
  return wave.agents.map((agent) => {
316
330
  const statusPath = statusPathForAgent(lanePaths, wave, agent);
317
331
  const statusRecord = readStatusRecordIfPresent(statusPath);
@@ -343,6 +357,11 @@ function buildLogicalAgents({ lanePaths, wave, tasks, dependencySnapshot, capabi
343
357
  (assignment) => assignment.blocking && assignment.assignedAgentId === agent.agentId,
344
358
  );
345
359
  const dependency = openInbound.find((record) => record.assignedAgentId === agent.agentId);
360
+ const satisfiedByStatus =
361
+ statusRecord?.code === 0 &&
362
+ (proofValidation.ok ||
363
+ isSecurityReviewAgent(agent) ||
364
+ isContEvalReportOnlyAgent(agent, { contEvalAgentId: lanePaths.contEvalAgentId }));
346
365
  let state = "planned";
347
366
  let reason = "";
348
367
  if (selection?.source === "active-attempt" && selectedAgentIds.has(agent.agentId)) {
@@ -354,6 +373,16 @@ function buildLogicalAgents({ lanePaths, wave, tasks, dependencySnapshot, capabi
354
373
  selection?.source === "relaunch-plan"
355
374
  ? "Selected by the persisted relaunch plan."
356
375
  : "Selected by active rerun request.";
376
+ } else if (completedPhase && satisfiedByStatus) {
377
+ state = [
378
+ lanePaths.contEvalAgentId || "E0",
379
+ lanePaths.integrationAgentId || "A8",
380
+ lanePaths.documentationAgentId || "A9",
381
+ lanePaths.contQaAgentId || "A0",
382
+ ].includes(agent.agentId) || isSecurityReviewAgent(agent)
383
+ ? "closed"
384
+ : "satisfied";
385
+ reason = "Completed wave preserves the latest satisfied agent state.";
357
386
  } else if (targetedBlockingTasks.some((task) => task.state === "working")) {
358
387
  state = "working";
359
388
  reason = targetedBlockingTasks.find((task) => task.state === "working")?.title || "";
@@ -365,7 +394,7 @@ function buildLogicalAgents({ lanePaths, wave, tasks, dependencySnapshot, capabi
365
394
  helperAssignment?.summary ||
366
395
  dependency?.summary ||
367
396
  "";
368
- } else if (statusRecord?.code === 0 && (proofValidation.ok || isSecurityReviewAgent(agent) || isContEvalReportOnlyAgent(agent, { contEvalAgentId: lanePaths.contEvalAgentId }))) {
397
+ } else if (satisfiedByStatus) {
369
398
  state = [
370
399
  lanePaths.contEvalAgentId || "E0",
371
400
  lanePaths.integrationAgentId || "A8",
@@ -401,7 +430,19 @@ function selectionTargetsAgent(agentId, selectionSet) {
401
430
  return Boolean(agentId) && selectionSet.has(agentId);
402
431
  }
403
432
 
404
- function buildBlockingEdge({ tasks, capabilityAssignments, dependencySnapshot, activeAttempt, rerunRequest, relaunchPlan, agentId = "" }) {
433
+ function buildBlockingEdge({
434
+ tasks,
435
+ capabilityAssignments,
436
+ dependencySnapshot,
437
+ activeAttempt,
438
+ rerunRequest,
439
+ relaunchPlan,
440
+ agentId = "",
441
+ phase,
442
+ }) {
443
+ if (isCompletedPhase(phase)) {
444
+ return null;
445
+ }
405
446
  const attemptSelection = new Set(activeAttempt?.selectedAgentIds || []);
406
447
  const scopeToActiveAttempt = !agentId && attemptSelection.size > 0;
407
448
  const scopedTasks = (agentId
@@ -546,6 +587,7 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
546
587
  const logPath = coordinationLogPath(lanePaths, wave.wave);
547
588
  const coordinationState = readMaterializedCoordinationState(logPath);
548
589
  const ledger = readWaveLedger(ledgerPath(lanePaths, wave.wave)) || { phase: "planned" };
590
+ const phase = ledger.phase || "unknown";
549
591
  const capabilityAssignments = buildRequestAssignments({
550
592
  coordinationState,
551
593
  agents: wave.agents,
@@ -596,7 +638,7 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
596
638
  return {
597
639
  lane: lanePaths.lane,
598
640
  wave: wave.wave,
599
- phase: ledger.phase || "unknown",
641
+ phase,
600
642
  agentId: agentId || null,
601
643
  blockingEdge: buildBlockingEdge({
602
644
  tasks,
@@ -606,6 +648,7 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
606
648
  rerunRequest,
607
649
  relaunchPlan,
608
650
  agentId,
651
+ phase,
609
652
  }),
610
653
  logicalAgents: buildLogicalAgents({
611
654
  lanePaths,
@@ -615,6 +658,7 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
615
658
  capabilityAssignments,
616
659
  selection,
617
660
  proofRegistry,
661
+ phase,
618
662
  }).filter((agent) => !agentId || agent.agentId === agentId),
619
663
  tasks,
620
664
  helperAssignments: (capabilityAssignments || []).filter(
@@ -634,7 +678,7 @@ export function buildControlStatusPayload({ lanePaths, wave, agentId = "" }) {
634
678
  selectionSource: selection.source,
635
679
  rerunRequest,
636
680
  relaunchPlan,
637
- nextTimer: nextTaskDeadline(tasks),
681
+ nextTimer: isCompletedPhase(phase) ? null : nextTaskDeadline(tasks),
638
682
  activeAttempt: controlState.activeAttempt,
639
683
  };
640
684
  }
@@ -69,7 +69,7 @@ function parseArgs(argv) {
69
69
  priority: "normal",
70
70
  dependsOn: [],
71
71
  artifactRefs: [],
72
- status: "open",
72
+ status: "",
73
73
  id: "",
74
74
  to: "",
75
75
  response: "",
@@ -309,6 +309,10 @@ function appendCoordinationStatusUpdate(logPath, record, status, options = {}) {
309
309
  });
310
310
  }
311
311
 
312
+ function defaultStatusForKind(kind) {
313
+ return String(kind || "").trim().toLowerCase() === "resolved-by-policy" ? "resolved" : "open";
314
+ }
315
+
312
316
  function appendTriageEscalationUpdateIfPresent(lanePaths, waveNumber, record) {
313
317
  const triagePath = coordinationTriagePath(lanePaths, waveNumber);
314
318
  if (!fs.existsSync(triagePath) || record?.kind !== "human-escalation") {
@@ -436,7 +440,7 @@ export async function runCoordinationCli(argv) {
436
440
  priority: options.priority,
437
441
  dependsOn: options.dependsOn,
438
442
  artifactRefs: options.artifactRefs,
439
- status: options.status,
443
+ status: options.status || defaultStatusForKind(options.kind),
440
444
  source: "agent",
441
445
  });
442
446
  console.log(JSON.stringify(record, null, 2));
@@ -88,7 +88,9 @@ export function normalizeCoordinationRecord(rawRecord, defaults = {}) {
88
88
  const lane = normalizeString(rawRecord.lane || defaults.lane);
89
89
  const wave = Number.parseInt(String(rawRecord.wave ?? defaults.wave ?? ""), 10);
90
90
  const agentId = normalizeString(rawRecord.agentId || defaults.agentId);
91
- const status = normalizeString(rawRecord.status || defaults.status || "open").toLowerCase();
91
+ const status = normalizeString(
92
+ rawRecord.status || defaults.status || (kind === "resolved-by-policy" ? "resolved" : "open"),
93
+ ).toLowerCase();
92
94
  const priority = normalizeString(rawRecord.priority || defaults.priority || "normal").toLowerCase();
93
95
  const confidence = normalizeString(rawRecord.confidence || defaults.confidence || "medium").toLowerCase();
94
96
  const createdAt = normalizeString(rawRecord.createdAt || defaults.createdAt || now);
@@ -155,6 +155,82 @@ function assignmentStateForRecord(record) {
155
155
  return "open";
156
156
  }
157
157
 
158
+ function recordText(record) {
159
+ return `${String(record?.summary || "")}\n${String(record?.detail || "")}`.trim().toLowerCase();
160
+ }
161
+
162
+ function targetMatchesAgent(target, agentId) {
163
+ const normalizedTarget = String(target || "").trim();
164
+ const normalizedAgentId = String(agentId || "").trim();
165
+ if (!normalizedTarget || !normalizedAgentId) {
166
+ return false;
167
+ }
168
+ return normalizedTarget === normalizedAgentId || normalizedTarget === `agent:${normalizedAgentId}`;
169
+ }
170
+
171
+ function recordTargetsAgent(record, agentId) {
172
+ const normalizedAgentId = String(agentId || "").trim();
173
+ if (!normalizedAgentId) {
174
+ return false;
175
+ }
176
+ if (String(record?.agentId || "").trim() === normalizedAgentId) {
177
+ return true;
178
+ }
179
+ return Array.isArray(record?.targets)
180
+ ? record.targets.some((target) => targetMatchesAgent(target, normalizedAgentId))
181
+ : false;
182
+ }
183
+
184
+ function requestResolutionForAssignment({
185
+ coordinationState,
186
+ requestRecord,
187
+ assignmentId,
188
+ assignedAgentId,
189
+ target,
190
+ }) {
191
+ const requestId = String(requestRecord?.id || "").trim();
192
+ if (!requestId) {
193
+ return null;
194
+ }
195
+ const requestIdLower = requestId.toLowerCase();
196
+ const assignmentIdLower = String(assignmentId || "").trim().toLowerCase();
197
+ const requestTargets = Array.isArray(requestRecord?.targets)
198
+ ? requestRecord.targets.filter((entry) => String(entry || "").trim())
199
+ : [];
200
+ const requiresAssignmentSpecificMatch = requestTargets.length > 1;
201
+ const resolvedRecords = [...(coordinationState?.resolvedByPolicy || [])].reverse();
202
+ for (const record of resolvedRecords) {
203
+ const dependsOn = Array.isArray(record?.dependsOn)
204
+ ? record.dependsOn.map((value) => String(value || "").trim().toLowerCase())
205
+ : [];
206
+ const assignmentDependsOnMatch = assignmentIdLower && dependsOn.includes(assignmentIdLower);
207
+ const requestDependsOnMatch = dependsOn.includes(requestIdLower);
208
+ if (assignmentDependsOnMatch || (!requiresAssignmentSpecificMatch && requestDependsOnMatch)) {
209
+ return record;
210
+ }
211
+ const closureCondition = String(record?.closureCondition || "").trim().toLowerCase();
212
+ const assignmentClosureMatch =
213
+ assignmentIdLower && closureCondition.includes(assignmentIdLower);
214
+ const requestClosureMatch = closureCondition.includes(requestIdLower);
215
+ if (
216
+ assignmentClosureMatch ||
217
+ (!requiresAssignmentSpecificMatch && requestClosureMatch)
218
+ ) {
219
+ return record;
220
+ }
221
+ if (!recordTargetsAgent(record, assignedAgentId) && !targetMatchesAgent(target, record?.agentId)) {
222
+ continue;
223
+ }
224
+ const text = recordText(record);
225
+ const assignmentTextMatch = assignmentIdLower && text.includes(assignmentIdLower);
226
+ const requestTextMatch = text.includes(requestIdLower);
227
+ if (assignmentTextMatch || (!requiresAssignmentSpecificMatch && requestTextMatch)) {
228
+ return record;
229
+ }
230
+ }
231
+ return null;
232
+ }
233
+
158
234
  export function buildRequestAssignments({
159
235
  coordinationState,
160
236
  agents,
@@ -179,8 +255,18 @@ export function buildRequestAssignments({
179
255
  }
180
256
  for (const target of targets) {
181
257
  const resolution = resolveTargetAssignment(target, agents, ledger, capabilityRouting);
258
+ const assignmentId = `assignment:${record.id}:${targetSlug(target) || "target"}`;
259
+ const resolvedByPolicyRecord = requestResolutionForAssignment({
260
+ coordinationState,
261
+ requestRecord: record,
262
+ assignmentId,
263
+ assignedAgentId: resolution.assignedAgentId,
264
+ target,
265
+ });
266
+ const resolvedByPolicy = Boolean(resolvedByPolicyRecord);
267
+ const effectiveStatus = resolvedByPolicy ? "resolved" : record.status;
182
268
  assignments.push({
183
- id: `assignment:${record.id}:${targetSlug(target) || "target"}`,
269
+ id: assignmentId,
184
270
  requestId: record.id,
185
271
  recordId: record.id,
186
272
  sourceKind: record.kind,
@@ -188,20 +274,21 @@ export function buildRequestAssignments({
188
274
  summary: record.summary || "",
189
275
  detail: record.detail || "",
190
276
  priority: record.priority || "normal",
191
- requestStatus: record.status,
192
- state: assignmentStateForRecord(record),
277
+ requestStatus: effectiveStatus,
278
+ state: assignmentStateForRecord({ ...record, status: effectiveStatus }),
193
279
  target: resolution.target,
194
280
  targetType: resolution.targetType,
195
281
  capability: resolution.capability,
196
282
  assignedAgentId: resolution.assignedAgentId,
197
283
  assignmentReason: resolution.assignmentReason,
198
284
  assignmentDetail: resolution.detail,
199
- blocking: isOpenCoordinationStatus(record.status),
285
+ blocking: !resolvedByPolicy && isOpenCoordinationStatus(record.status),
200
286
  artifactRefs: Array.isArray(record.artifactRefs) ? record.artifactRefs : [],
201
287
  dependsOn: Array.isArray(record.dependsOn) ? record.dependsOn : [],
202
288
  closureCondition: String(record.closureCondition || ""),
203
289
  createdAt: record.createdAt,
204
290
  updatedAt: record.updatedAt,
291
+ resolvedByRecordId: resolvedByPolicyRecord?.id || null,
205
292
  });
206
293
  }
207
294
  }