@pellux/goodvibes-tui 0.19.22 → 0.19.24

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
@@ -4,6 +4,66 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.19.24] — 2026-04-22
8
+
9
+ Four correctness and honesty fixes from an external architecture review. No SDK change (pinned at 0.23.2).
10
+
11
+ ### Fixed
12
+
13
+ - **`/clear` now actually clears the display** (`src/core/conversation.ts`). The original `clearDisplay()` immediately rebuilt the display buffer from `this.messages`, so `getDisplayBlocks().length` was unchanged before and after the call — `/clear` was a no-op. The fix introduces a `_displayFromMessageIndex` watermark: `clearDisplay()` records the current message count and clears the buffer without re-rendering; `rebuildHistory()` now only renders messages added *after* the watermark, so the screen is blank until the next message arrives. The full LLM context (`getMessagesForLLM()`, `getMessageSnapshot()`) is untouched. `resetAll()` zeroes the watermark so a full reset continues to work. Regression tests added to `src/test/core/conversation.test.ts` (4 new tests).
14
+
15
+ - **Workspace tab bar shows active state on the unfocused pane** (`src/panels/panel-manager.ts`, `src/renderer/panel-workspace-bar.ts`). `getWorkspaceTabs()` previously set both `active` and `focused` from the single globally-focused panel, so the unfocused pane's selected tab lost its active marker entirely when focus moved to the other pane. The fix splits the two semantics: `active` is now true for the currently-selected tab in *its own pane* (derived from `pane.activeIndex`, independent of keyboard focus); `focused` is true only for the one tab in the globally-focused pane. The workspace bar renderer uses the `focused` flag to append a `▸` glyph to the focused tab's label, making keyboard-focus distinct from pane-level selection. Both panes now show their selected tab highlighted regardless of which has focus. Regression tests added to `src/test/panels/panel-manager.test.ts` (2 new tests).
16
+
17
+ - **Panel render cache race guard** (`src/renderer/panel-composite.ts`). The documented hazard in the R2 cache — where a mid-render `invalidate()` call would be silently clobbered by the trailing `markRendered()` — has been resolved. The fix wraps each panel's `invalidate()` the first time it enters `renderPanel()` to maintain a per-panel generation counter (stored in a module-level `WeakMap`). The generation is snapshotted before `render()` and compared after: `markRendered()` is only called when the generation is unchanged, meaning no concurrent invalidation fired. If the generation changed, `needsRender` stays `true` and the next frame re-renders with the fresh state. Backward-compatible: existing panels that don't invalidate during render are unaffected. New test file `src/test/renderer/panel-composite.test.ts` (4 tests).
18
+
19
+ - **Release gate filenames renamed to match what the tests actually verify** (`src/test/release-gates/`). `runtime-certification-gate.test.ts` was renamed to `runtime-contract-shape-gate.test.ts` and its `describe()` block updated to `'runtime contract shape gate'`. `foundation-surfaces-gate.test.ts` was renamed to `foundation-surface-stability-gate.test.ts` and its `describe()` block updated to `'foundation surface stability gate'`. The test content (assertions, helpers) is unchanged — this is an honesty rename only, removing the implication that the tests verify runtime behavior certification when they actually verify structural shape preservation.
20
+
21
+ ---
22
+
23
+ ## [0.19.23] — 2026-04-22
24
+
25
+ SDK 0.23.x constraint-propagation reconciliation. Surfaces the new per-chain constraint data across the WRFC panel, process modal, agent inspector, and agent detail modal; adds system-message notifications for constraint enumeration and violations; exposes the WRFC-injected `systemPromptAddendum` so operators can see the engineer addendum was applied.
26
+
27
+ ### Changed
28
+
29
+ - **SDK dep bumped to `@pellux/goodvibes-sdk@0.23.2`.** Picks up constraint-propagation additions from 0.23.0 (feature), the 0.23.1 cleanup (removed opt-in golden-prompt suite that was skipped by default), and the 0.23.2 docs bundle (the complete `docs/wrfc-constraint-propagation.md` + reference-runtime-events + observability + error-kinds + migration docs shipped with the feature). New types consumed by this TUI release: `Constraint` and `ConstraintFinding` from `platform/agents/completion-report`; three new fields on `WrfcChain` (`constraints`, `constraintsEnumerated`, `syntheticIssues`); `WORKFLOW_CONSTRAINTS_ENUMERATED` runtime event; optional constraint summary fields on `WORKFLOW_REVIEW_COMPLETED` (`constraintsSatisfied`, `constraintsTotal`, `unsatisfiedConstraintIds`) and `WORKFLOW_FIX_ATTEMPTED` (`targetConstraintIds`); optional `constraints?` on `EngineerReport`; optional `constraintFindings?` on `ReviewerReport`; `AgentRecord.systemPromptAddendum?`.
30
+
31
+ ### Added
32
+
33
+ - **`WORKFLOW_CONSTRAINTS_ENUMERATED` system message** (`src/runtime/bootstrap-core.ts`). When an engineer agent enumerates one or more constraints, a low-priority `[WRFC] Engineer enumerated N constraint(s) for chain <chainId>` message is routed to the SystemMessagesPanel. Zero-constraint chains produce no message (backward-compatible).
34
+ - **Constraint-violation system message on review failure** (`src/runtime/bootstrap-core.ts`). When `WORKFLOW_REVIEW_COMPLETED` fires with `passed: false` and `unsatisfiedConstraintIds` is non-empty, a high-priority `[WRFC] ✗ Chain <chainId>: N constraint violation(s) forced failure` message is emitted to both the main conversation and the SystemMessagesPanel. Complements the existing SDK-level score/threshold message.
35
+ - **`WORKFLOW_FIX_ATTEMPTED` constraint-targeting message** (`src/runtime/bootstrap-core.ts`). When a fix agent is spawned to address specific constraints (`targetConstraintIds` present), a low-priority `[WRFC] Fix #N targeting N constraint(s) on chain <chainId>` message is routed to the SystemMessagesPanel. Only fires when `targetConstraintIds` is non-empty.
36
+ - **Constraint count in Fix agent process-modal label** (`src/renderer/process-modal.ts`). When a running fix agent's WRFC chain has constraints, the Background Processes modal label gains a compact `[Nc]` suffix (e.g. `[Fix #2] my task (7.8 → 9.9/10) [3c]`). Chains with zero constraints are unchanged.
37
+ - **`systemPromptAddendum` indicator in Agent Inspector** (`src/panels/agent-inspector-panel.ts`). The per-agent summary line in the Inspector panel now includes an `Addendum yes` field when the selected agent's `AgentRecord.systemPromptAddendum` is set. This lets operators confirm that the WRFC constraint addendum was injected into the engineer's system prompt. Agents without an addendum are unchanged.
38
+ - **Constraint data in Agent Detail modal** (`src/renderer/agent-detail-modal.ts`). The agent detail modal (opened from the Background Processes modal with Enter) now surfaces three additional SDK 0.23.x fields when available: (1) an `Addendum: yes` line when `AgentRecord.systemPromptAddendum` is set; (2) a `Constraints (N):` block listing each constraint id/text/source from the agent's WRFC chain; (3) a `Findings: N checked, N unsatisfied` summary from the reviewer's `constraintFindings`. An optional `wrfcController` dep was added to `AgentDetailModalDeps` — backward-compatible, existing callers without it continue working unchanged. Zero-constraint chains and agents not in a WRFC chain render byte-identically to pre-0.23.
39
+ - **WRFC panel constraint badge** (`src/panels/wrfc-panel.ts`). Each chain row renders a compact `c:N/M` badge showing satisfied/total constraints when present, colored by aggregate state: green when all satisfied, red on any unsatisfied finding, grey when no findings yet, yellow for mixed verified/unverified. Omitted entirely for zero-constraint chains.
40
+ - **WRFC panel expanded constraint detail** (`src/panels/wrfc-panel.ts`). Expanding a chain now shows per-constraint lines with a severity-tagged status marker: `[SAT]` (satisfied), `[UNS CRIT]` / `[UNS MAJOR]` / `[UNS MINOR]` (unsatisfied with severity), or `[UNV]` (unverified). Inherited constraints — those carried from a parent chain after gate-failure retry — are suffixed with ` *`. List caps at 10 with a `(+N more)` tail and respects the panel's `maxLines` budget.
41
+ - **WRFC panel selected-chain summary** (`src/panels/wrfc-panel.ts`). The selected-chain summary row now shows `N sat / M total (K inherited)` when the focused chain has constraints, giving the operator at-a-glance status without expanding the row.
42
+ - **WRFC panel controller-flags block** (`src/panels/wrfc-panel.ts`). When the controller injects synthetic issues on the chain (e.g. fixer constraint-continuity violations), a `Controller flags` section renders above the reviewer Issues block with a `[CRITICAL]` prefix, so operators see why a chain went back to fixing even when the reviewer didn't flag anything.
43
+
44
+ ### Surface Audit — Per-Surface Verdict
45
+
46
+ | Surface | File | Verdict |
47
+ |---------|------|---------|
48
+ | WRFC chain panel | `src/panels/wrfc-panel.ts` | Reconciled — constraint badge `c:N/M`, expanded-detail severity-tagged markers (`[SAT]` / `[UNS CRIT|MAJOR|MINOR]` / `[UNV]`) with inherited ` *` suffix, selected-chain summary, controller-flags block, `WORKFLOW_CONSTRAINTS_ENUMERATED` subscription |
49
+ | System message router | `src/core/system-message-router.ts` | Not applicable — routing infrastructure, no direct event handling |
50
+ | Bootstrap runtime events | `src/runtime/bootstrap-core.ts` | Reconciled — 3 new subscriptions added |
51
+ | Process modal | `src/renderer/process-modal.ts` | Reconciled — Fix label shows constraint count |
52
+ | Agent Inspector panel | `src/panels/agent-inspector-panel.ts` | Reconciled — systemPromptAddendum indicator |
53
+ | Agent Logs panel | `src/panels/agent-logs-panel.ts` | Not applicable — log tail only, no report/constraint fields |
54
+ | Agent Detail modal | `src/renderer/agent-detail-modal.ts` | Reconciled — systemPromptAddendum indicator, constraint list, reviewer findings |
55
+ | Orchestration panel | `src/panels/orchestration-panel.ts` | Not applicable — reads `OrchestrationGraphRecord` from store domain, no constraint fields |
56
+ | Agent builtin panel registry | `src/panels/builtin/agent.ts` | Not applicable — registration only, no rendering |
57
+ | Tasks panel | `src/panels/builtin/operations.ts` | Not applicable — no WRFC chain rendering |
58
+ | Eval panel | `src/panels/eval-panel.ts` | Not applicable — eval suite scores, not WRFC constraint data |
59
+ | UI events plumbing | `src/runtime/ui-events.ts` | Not applicable — re-exports SDK; `WorkflowEvent` union already includes new event |
60
+ | Config command | `src/input/commands/config.ts` | Not applicable — no new SDK 0.23.0 config knobs |
61
+ | Runtime services | `src/runtime/services.ts` | Not applicable — no new service instantiation required |
62
+ | Bootstrap core | `src/runtime/bootstrap-core.ts` | Reconciled (see above) |
63
+ | Docs | `docs/` | Not applicable — TUI docs have minimal WRFC prose; no legacy content to update |
64
+
65
+ ---
66
+
7
67
  ## [0.19.22] — 2026-04-21
8
68
 
9
69
  Hotfix release: regenerates foundation artifacts against SDK 0.22.0 after 0.19.21's release pipeline failed the `foundation artifacts gate` test. No consumer-facing feature changes beyond what 0.19.21 carried.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Version](https://img.shields.io/badge/version-0.19.22-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-0.19.24-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
6
6
 
7
7
  A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
8
8
 
@@ -159,6 +159,12 @@ The TUI now consumes the extracted `@pellux/goodvibes-sdk` platform layer for sh
159
159
  - Archetype registry that supports built-ins and user-defined markdown archetypes
160
160
  - Task lifecycle tracking across exec, agent, MCP, plugin, integration, daemon, scheduler, and ACP work
161
161
  - Automated WRFC loops with review/fix/check chains, configurable gates, and explicit evidence in completion reports
162
+ - WRFC panel renders a constraint badge (`c:N/M`) per chain, colored by aggregate satisfaction status
163
+ - Expanded chain detail shows each constraint with status marker `[SAT]`, `[UNS CRIT|MAJOR|MINOR]` (unsatisfied, severity-tagged), or `[UNV]` (unverified), with ` *` suffix for inherited constraints
164
+ - Selected-chain summary shows satisfied/total/inherited counts at a glance
165
+ - Controller-flagged synthetic issues render above reviewer issues as `[CRITICAL]` "Controller flags"
166
+ - Agent-detail modal surfaces `systemPromptAddendum` (WRFC engineer addendum) when present on the agent record
167
+ - System-message router surfaces `WORKFLOW_CONSTRAINTS_ENUMERATED` as an operator-visible message when constraints are loaded
162
168
  - Built-in planning/strategy layer with execution plans, adaptive plan modes, and status/explain/override controls
163
169
 
164
170
  ### Tools And Intelligence
@@ -1235,7 +1241,7 @@ Those pieces cover conversation-noise routing, panel-health/performance budgets,
1235
1241
  | `/fork [name]` | `/branch-save` | Save a named snapshot of the current conversation |
1236
1242
  | `/merge <name>` | — | Append messages from a branch after the fork point |
1237
1243
  | `/agents` | — | List active and completed agents |
1238
- | `/wrfc` | — | Show WRFC chain status |
1244
+ | `/wrfc` | — | Show WRFC chain status, constraint satisfaction counts, and per-constraint `[SAT]`/`[UNS]`/`[UNV]` breakdown |
1239
1245
  | `/health [action]` | — | Unified runtime health review and repair entry point |
1240
1246
  | `/guidance [action]` | — | Contextual operational guidance without cluttering the conversation |
1241
1247
  | `/remote [action]` | — | Distributed peer, node-host contract, work-queue, and artifact control room |
@@ -3,7 +3,7 @@
3
3
  "product": {
4
4
  "id": "goodvibes",
5
5
  "surface": "operator",
6
- "version": "0.22.0"
6
+ "version": "0.23.2"
7
7
  },
8
8
  "auth": {
9
9
  "modes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.19.22",
3
+ "version": "0.19.24",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -90,7 +90,7 @@
90
90
  "@anthropic-ai/vertex-sdk": "^0.16.0",
91
91
  "@ast-grep/napi": "^0.42.0",
92
92
  "@aws/bedrock-token-generator": "^1.1.0",
93
- "@pellux/goodvibes-sdk": "0.22.0",
93
+ "@pellux/goodvibes-sdk": "0.23.2",
94
94
  "bash-language-server": "^5.6.0",
95
95
  "fuse.js": "^7.1.0",
96
96
  "graphql": "^16.13.2",
@@ -74,6 +74,13 @@ export class ConversationManager extends SdkConversationManager {
74
74
  private errorLineRegistry: number[] = [];
75
75
  /** Streaming block start line in history buffer (for incremental streaming update). */
76
76
  private streamingStartLine = -1;
77
+ /**
78
+ * Message index at the time of the last clearDisplay() call.
79
+ * rebuildHistory() renders only messages at or after this index, so the
80
+ * display stays blank for messages added before the clear while LLM history
81
+ * is fully preserved. Reset to 0 on resetAll() or rebuildHistory() width change.
82
+ */
83
+ private _displayFromMessageIndex = 0;
77
84
 
78
85
  public suppressSplash: boolean = false;
79
86
  public splashOptions: SplashOptions = {};
@@ -215,6 +222,7 @@ export class ConversationManager extends SdkConversationManager {
215
222
  this.messageLineRegistry = [];
216
223
  this.errorLineRegistry = [];
217
224
  this.streamingStartLine = -1;
225
+ this._displayFromMessageIndex = 0; // full reset — show everything on next render
218
226
  }
219
227
 
220
228
  /**
@@ -293,20 +301,27 @@ export class ConversationManager extends SdkConversationManager {
293
301
  this.lastRenderedWidth = width;
294
302
  this.dirty = false;
295
303
 
304
+ const snapshot = this.getMessageSnapshot();
305
+ // When _displayFromMessageIndex > 0, clearDisplay() was called. Only render
306
+ // messages added after the clear — the pre-clear history stays off-screen.
307
+ // On a full rebuild (e.g. width change), reset the display-start to 0 so the
308
+ // user can scroll back to the full history if needed.
309
+ const displayStart = this._displayFromMessageIndex;
310
+ const visibleSnapshot = displayStart > 0 ? snapshot.slice(displayStart) : snapshot;
311
+
296
312
  // Tool messages ARE rendered (as collapsed blocks); this filter is only
297
313
  // for determining whether to show the splash screen (tool-only messages
298
314
  // don't count as visible conversation content for splash purposes).
299
- const snapshot = this.getMessageSnapshot();
300
- const displayMessages = snapshot.filter(
315
+ const displayMessages = visibleSnapshot.filter(
301
316
  (m) => m.role !== 'tool' && m.role !== 'system',
302
317
  );
303
318
 
304
- if (displayMessages.length === 0 && !this.suppressSplash) {
319
+ if (displayMessages.length === 0 && displayStart === 0 && !this.suppressSplash) {
305
320
  this.addSplashScreen(width);
306
321
  return;
307
322
  }
308
323
 
309
- this.appendMessages(snapshot, width);
324
+ this.appendMessages(visibleSnapshot, width);
310
325
  this.appendedUpTo = snapshot.length;
311
326
  }
312
327
 
@@ -509,19 +524,27 @@ export class ConversationManager extends SdkConversationManager {
509
524
 
510
525
  /**
511
526
  * clearDisplay - Clear the visual history buffer without touching the LLM context messages.
512
- * The next render will show a blank conversation area.
527
+ * The next render will show a blank conversation area. Subsequent message additions
528
+ * rebuild the display incrementally from that point forward.
529
+ *
530
+ * Contract:
531
+ * - getDisplayBlocks() returns an empty array immediately after this call.
532
+ * - getMessageSnapshot() is unaffected — full LLM history is preserved.
533
+ * - resetAll() (which clears both display and messages) continues to work.
534
+ * - rebuildHistory() can be called by callers that need a full display rebuild.
513
535
  */
514
536
  public clearDisplay(): void {
515
537
  this.history.clear();
516
- this.appendedUpTo = 0;
517
- this.dirty = true;
518
- // Re-render from existing messages to rebuild buffer
519
- const width = this._getWidth();
520
- this.lastRenderedWidth = width;
538
+ this.blockRegistry = [];
539
+ this.messageLineRegistry = [];
540
+ this.errorLineRegistry = [];
541
+ // Advance _displayFromMessageIndex to exclude all current messages from display.
542
+ // rebuildHistory() will only render messages added AFTER this point.
543
+ this._displayFromMessageIndex = this.getMessageSnapshot().length;
544
+ this.appendedUpTo = this._displayFromMessageIndex;
521
545
  this.dirty = false;
522
- const snapshot = this.getMessageSnapshot();
523
- this.appendMessages(snapshot, width);
524
- this.appendedUpTo = snapshot.length;
546
+ // Do NOT re-render here — display stays blank until the next message is added.
547
+ // The lastRenderedWidth is kept so subsequent appends use the correct width.
525
548
  }
526
549
  }
527
550
 
@@ -207,6 +207,8 @@ export class InputHandler {
207
207
  agentManager: uiServices.agents.agentManager,
208
208
  agentMessageBus: uiServices.agents.agentMessageBus,
209
209
  sessionLogPathResolver: (agentId) => uiServices.environment.shellPaths.resolveProjectPath('tui', 'sessions', `${agentId}.jsonl`),
210
+ // SDK 0.23.0: supply wrfcController so the modal can show constraint data
211
+ wrfcController: uiServices.agents.wrfcController,
210
212
  });
211
213
  this.bookmarkModal = new BookmarkModal(uiServices.shell.bookmarkManager);
212
214
  this.sessionPickerModal = new SessionPickerModal(uiServices.sessions.sessionManager);
@@ -317,6 +317,10 @@ export class AgentInspectorPanel extends BasePanel {
317
317
  [formatMs(elapsed), DEFAULT_PANEL_PALETTE.value],
318
318
  [' Tools ', DEFAULT_PANEL_PALETTE.label],
319
319
  [String(rec.toolCallCount), DEFAULT_PANEL_PALETTE.info],
320
+ // SDK 0.23.0: show addendum indicator when WRFC injected a constraint addendum
321
+ ...(rec.systemPromptAddendum
322
+ ? [[' Addendum ', DEFAULT_PANEL_PALETTE.label] as [string, string], ['yes', DEFAULT_PANEL_PALETTE.info] as [string, string]]
323
+ : []),
320
324
  [' Task ', DEFAULT_PANEL_PALETTE.label],
321
325
  [taskDisplay, DEFAULT_PANEL_PALETTE.value],
322
326
  ]);
@@ -347,13 +347,17 @@ export class PanelManager {
347
347
 
348
348
  getWorkspaceTabs(): readonly WorkspaceTab[] {
349
349
  if (this._cachedWorkspaceTabs !== null) return this._cachedWorkspaceTabs;
350
+ // `active` = the currently selected tab in its own pane (independent of focus).
351
+ // `focused` = true only for the one tab in the globally focused pane that is active.
350
352
  const focusedPanelId = this.getActivePanel()?.id;
353
+ const topActivePanelId = this.topPane.panels[this.topPane.activeIndex]?.id;
354
+ const bottomActivePanelId = this.bottomPane.panels[this.bottomPane.activeIndex]?.id;
351
355
  const topTabs = this.topPane.panels.map((panel) => ({
352
356
  id: panel.id,
353
357
  name: panel.name,
354
358
  icon: panel.icon,
355
359
  pane: 'top' as const,
356
- active: panel.id === focusedPanelId,
360
+ active: panel.id === topActivePanelId,
357
361
  focused: panel.id === focusedPanelId,
358
362
  }));
359
363
  const bottomTabs = this.bottomPane.panels.map((panel) => ({
@@ -361,7 +365,7 @@ export class PanelManager {
361
365
  name: panel.name,
362
366
  icon: panel.icon,
363
367
  pane: 'bottom' as const,
364
- active: panel.id === focusedPanelId,
368
+ active: panel.id === bottomActivePanelId,
365
369
  focused: panel.id === focusedPanelId,
366
370
  }));
367
371
  const tabs = [...topTabs, ...bottomTabs] as WorkspaceTab[];
@@ -1,5 +1,6 @@
1
1
  import type { Line } from '../types/grid.ts';
2
2
  import type { WrfcChain, WrfcState, QualityGateResult } from '@pellux/goodvibes-sdk/platform/agents/wrfc-types';
3
+ import type { Constraint, ConstraintFinding } from '@pellux/goodvibes-sdk/platform/agents/completion-report';
3
4
  import type { WrfcController } from '@pellux/goodvibes-sdk/platform/agents/wrfc-controller';
4
5
  import { BasePanel } from './base-panel.ts';
5
6
  import type { WorkflowEvent } from '@pellux/goodvibes-sdk/platform/runtime/events/index';
@@ -48,6 +49,10 @@ const C = {
48
49
  issueSug: '#6b7280',
49
50
  gatePass: '#22c55e',
50
51
  gateFail: '#ef4444',
52
+ // constraint status
53
+ constraintSat: '#22c55e', // green — satisfied
54
+ constraintUnsat:'#ef4444', // red — unsatisfied
55
+ constraintUnv: '#4b5563', // grey — unverified (no finding yet)
51
56
  } as const;
52
57
 
53
58
  // ---------------------------------------------------------------------------
@@ -117,6 +122,37 @@ export function truncate(s: string, max: number): string {
117
122
  return s.slice(0, max - 3) + '...';
118
123
  }
119
124
 
125
+ // ---------------------------------------------------------------------------
126
+ // Constraint helpers
127
+ // ---------------------------------------------------------------------------
128
+
129
+ /**
130
+ * Returns display tag, foreground colour, and dim flag for a single constraint
131
+ * based on whether a reviewer finding exists for it.
132
+ */
133
+ export function constraintStatusMarker(
134
+ constraint: Constraint,
135
+ findings: ConstraintFinding[] | undefined,
136
+ ): { tag: string; fg: string; dim: boolean } {
137
+ const finding = findings?.find(f => f.constraintId === constraint.id);
138
+ if (!finding) {
139
+ return { tag: '[UNV]', fg: C.constraintUnv, dim: true };
140
+ }
141
+ if (finding.satisfied) {
142
+ return { tag: '[SAT]', fg: C.constraintSat, dim: false };
143
+ }
144
+ // Unsatisfied — use severity to pick colour and tag text
145
+ const sev = finding.severity ?? 'major';
146
+ let sevTag: string;
147
+ let fg: string;
148
+ switch (sev) {
149
+ case 'critical': sevTag = '[UNS CRIT]'; fg = C.issueCrit; break;
150
+ case 'minor': sevTag = '[UNS MINOR]'; fg = C.issueMin; break;
151
+ default: sevTag = '[UNS MAJOR]'; fg = C.issueMaj; break;
152
+ }
153
+ return { tag: sevTag, fg, dim: false };
154
+ }
155
+
120
156
  // ---------------------------------------------------------------------------
121
157
  // Panel
122
158
  // ---------------------------------------------------------------------------
@@ -228,6 +264,22 @@ export class WrfcPanel extends BasePanel {
228
264
  [' Scores ', DEFAULT_PANEL_PALETTE.label],
229
265
  [selectedChain.reviewScores.length > 0 ? selectedChain.reviewScores.map((score) => score.toFixed(0)).join(' -> ') : 'none', DEFAULT_PANEL_PALETTE.info],
230
266
  ]),
267
+ ...(selectedChain.constraints.length > 0 ? [
268
+ buildPanelLine(width, (() => {
269
+ const total = selectedChain.constraints.length;
270
+ const findings = selectedChain.reviewerReport?.constraintFindings;
271
+ const satisfied = findings ? findings.filter(f => f.satisfied).length : 0;
272
+ const inherited = selectedChain.constraints.filter(c => c.source === 'inherited').length;
273
+ const inheritedPart = inherited > 0 ? ` (${inherited} inherited)` : '';
274
+ const satFg = !findings || findings.length === 0
275
+ ? DEFAULT_PANEL_PALETTE.dim
276
+ : satisfied === total ? C.constraintSat : C.constraintUnsat;
277
+ return [
278
+ [' Constraints ', DEFAULT_PANEL_PALETTE.label],
279
+ [`${satisfied} sat / ${total} total${inheritedPart}`, satFg],
280
+ ] as Array<[string, string]>;
281
+ })()),
282
+ ] : []),
231
283
  ]
232
284
  : [];
233
285
 
@@ -300,7 +352,15 @@ export class WrfcPanel extends BasePanel {
300
352
  const latestScore = chain.reviewScores.length > 0
301
353
  ? ` ${chain.reviewScores[chain.reviewScores.length - 1].toFixed(1)}/10`
302
354
  : '';
303
- const rightInfo = `${latestScore}${fixes}${cycles} `;
355
+ // Constraint badge: c:sat/total — only when constraints exist
356
+ let constraintBadge = '';
357
+ if (chain.constraints.length > 0) {
358
+ const total = chain.constraints.length;
359
+ const findings = chain.reviewerReport?.constraintFindings;
360
+ const satisfied = findings ? findings.filter(f => f.satisfied).length : 0;
361
+ constraintBadge = ` c:${satisfied}/${total}`;
362
+ }
363
+ const rightInfo = `${latestScore}${fixes}${cycles}${constraintBadge} `;
304
364
 
305
365
  // Compute how much space the task text can use, then check if rightInfo fits.
306
366
  // If the terminal is narrow and rightInfo would overflow, omit it entirely
@@ -319,7 +379,32 @@ export class WrfcPanel extends BasePanel {
319
379
  ];
320
380
  if (remaining >= rightInfo.length + 1) {
321
381
  // Right-align rightInfo in the remaining space
322
- segments.push({ text: rightInfo.padStart(remaining), fg: isSelected ? fg : C.label });
382
+ // Colour the constraint badge separately when present
383
+ if (chain.constraints.length > 0 && !isSelected) {
384
+ const total = chain.constraints.length;
385
+ const findings = chain.reviewerReport?.constraintFindings;
386
+ const satisfied = findings ? findings.filter(f => f.satisfied).length : 0;
387
+ // Determine badge colour
388
+ let badgeFg: string;
389
+ if (!findings || findings.length === 0) {
390
+ badgeFg = C.constraintUnv;
391
+ } else if (satisfied === total) {
392
+ badgeFg = C.constraintSat;
393
+ } else if (findings.some(f => !f.satisfied)) {
394
+ badgeFg = C.constraintUnsat;
395
+ } else {
396
+ badgeFg = C.reviewing; // some unverified but none failed
397
+ }
398
+ // Split: everything before the badge, then the badge
399
+ const badgeText = ` c:${satisfied}/${total}`;
400
+ const beforeBadge = rightInfo.slice(0, rightInfo.length - badgeText.length - 1);
401
+ const padding = remaining - rightInfo.length;
402
+ segments.push({ text: beforeBadge.padStart(padding + beforeBadge.length), fg: isSelected ? fg : C.label });
403
+ segments.push({ text: badgeText, fg: badgeFg });
404
+ segments.push({ text: ' ', fg: '' });
405
+ } else {
406
+ segments.push({ text: rightInfo.padStart(remaining), fg: isSelected ? fg : C.label });
407
+ }
323
408
  }
324
409
  // else: no room — makeSegmentedLine will pad with spaces to fill width
325
410
 
@@ -358,6 +443,35 @@ export class WrfcPanel extends BasePanel {
358
443
  ]));
359
444
  }
360
445
 
446
+ // Constraints section (between Cycles and Gates)
447
+ if (chain.constraints.length > 0 && lines.length < maxLines) {
448
+ lines.push(buildStyledPanelLine(width, [{ text: `${indent}Constraints`, fg: C.label }]));
449
+ const MAX_CONSTRAINTS = 10;
450
+ const findings = chain.reviewerReport?.constraintFindings;
451
+ const displayed = chain.constraints.slice(0, MAX_CONSTRAINTS);
452
+ for (const constraint of displayed) {
453
+ if (lines.length >= maxLines) break;
454
+ const marker = constraintStatusMarker(constraint, findings);
455
+ const inheritedMark = constraint.source === 'inherited' ? ' *' : '';
456
+ const statusTag = `${marker.tag}${inheritedMark}`;
457
+ const rowPrefix = `${indent} ${statusTag} `;
458
+ const textMax = Math.max(8, width - rowPrefix.length);
459
+ const constraintText = truncate(constraint.text, textMax);
460
+ lines.push(buildStyledPanelLine(width, [
461
+ { text: `${indent} `, fg: C.dim },
462
+ { text: statusTag, fg: marker.fg, dim: marker.dim, bold: !marker.dim },
463
+ { text: ' ', fg: '' },
464
+ { text: constraintText, fg: C.value },
465
+ ]));
466
+ }
467
+ const remaining = chain.constraints.length - MAX_CONSTRAINTS;
468
+ if (remaining > 0 && lines.length < maxLines) {
469
+ lines.push(buildStyledPanelLine(width, [
470
+ { text: `${indent} (+${remaining} more)`, fg: C.dim, dim: true },
471
+ ]));
472
+ }
473
+ }
474
+
361
475
  // Quality gate results
362
476
  if (chain.gateResults && chain.gateResults.length > 0) {
363
477
  lines.push(buildStyledPanelLine(width, [{ text: `${indent}Gates`, fg: C.label }]));
@@ -374,6 +488,21 @@ export class WrfcPanel extends BasePanel {
374
488
  }
375
489
  }
376
490
 
491
+ // Synthetic issues injected by controller (continuity violations)
492
+ if (chain.syntheticIssues && chain.syntheticIssues.length > 0 && lines.length < maxLines) {
493
+ lines.push(buildStyledPanelLine(width, [{ text: `${indent}Controller flags`, fg: C.issueCrit, bold: true }]));
494
+ for (const synthetic of chain.syntheticIssues) {
495
+ if (lines.length >= maxLines) break;
496
+ const prefix = `${indent} [CRIT] `;
497
+ const descMax = Math.max(8, width - prefix.length);
498
+ const desc = truncate(synthetic.description, descMax);
499
+ lines.push(buildStyledPanelLine(width, [
500
+ { text: prefix, fg: C.issueCrit, bold: true },
501
+ { text: desc, fg: C.value },
502
+ ]));
503
+ }
504
+ }
505
+
377
506
  // Issues from reviewer
378
507
  const issues = chain.reviewerReport?.issues ?? [];
379
508
  if (issues.length > 0 && lines.length < maxLines) {
@@ -458,6 +587,7 @@ export class WrfcPanel extends BasePanel {
458
587
  this.workflowEvents.on('WORKFLOW_CHAIN_FAILED', refresh),
459
588
  this.workflowEvents.on('WORKFLOW_AUTO_COMMITTED', refresh),
460
589
  this.workflowEvents.on('WORKFLOW_CASCADE_ABORTED', refresh),
590
+ this.workflowEvents.on('WORKFLOW_CONSTRAINTS_ENUMERATED', refresh),
461
591
  );
462
592
  }
463
593
 
@@ -1,9 +1,9 @@
1
- import { join } from 'path';
2
1
  import { readFile } from 'fs/promises';
3
2
  import { type Line } from '../types/grid.ts';
4
3
  import { ModalFactory } from './modal-factory.ts';
5
4
  import type { AgentManager } from '@pellux/goodvibes-sdk/platform/tools/agent/index';
6
5
  import type { AgentMessageBus } from '@pellux/goodvibes-sdk/platform/agents/message-bus';
6
+ import type { WrfcController } from '@pellux/goodvibes-sdk/platform/agents/wrfc-controller';
7
7
  import { formatDuration } from './modal-utils.ts';
8
8
  import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
9
9
  import { getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
@@ -19,6 +19,8 @@ export interface AgentDetailModalDeps {
19
19
  readonly agentManager: Pick<AgentManager, 'getStatus'>;
20
20
  readonly agentMessageBus: Pick<AgentMessageBus, 'getMessages'>;
21
21
  readonly sessionLogPathResolver: (agentId: string) => string;
22
+ /** Optional — when supplied, constraint data from the agent's WRFC chain is shown (SDK 0.23.0). */
23
+ readonly wrfcController?: Pick<WrfcController, 'getChain'>;
22
24
  }
23
25
 
24
26
  // ─── AgentDetailModal ─────────────────────────────────────────────────────────
@@ -176,6 +178,51 @@ export function renderAgentDetailModal(
176
178
  sections.push({ type: 'text', content: `Tool calls : ${rec.toolCallCount}` });
177
179
  sections.push({ type: 'text', content: `Est tokens : ~${tokenEst.toLocaleString()}` });
178
180
 
181
+ // SDK 0.23.0: systemPromptAddendum indicator — confirms WRFC constraint addendum was injected
182
+ if (rec.systemPromptAddendum) {
183
+ sections.push({
184
+ type: 'text',
185
+ content: 'Addendum : yes (WRFC constraint layer injected)',
186
+ style: { fg: '#aaffee' },
187
+ });
188
+ }
189
+
190
+ // SDK 0.23.0: constraint data from WRFC chain (engineer constraints + reviewer findings)
191
+ if (rec.wrfcId && modal.deps.wrfcController) {
192
+ try {
193
+ const chain = modal.deps.wrfcController.getChain(rec.wrfcId);
194
+ if (chain && chain.constraints.length > 0) {
195
+ sections.push({ type: 'separator' });
196
+ sections.push({
197
+ type: 'text',
198
+ content: `Constraints (${chain.constraints.length}):`,
199
+ style: { dim: true },
200
+ });
201
+ for (const c of chain.constraints) {
202
+ const src = c.source === 'inherited' ? ' [inherited]' : '';
203
+ const text = c.text.length > 80 ? c.text.slice(0, 77) + '…' : c.text;
204
+ sections.push({
205
+ type: 'text',
206
+ content: ` [${c.id}]${src} ${text}`,
207
+ style: { fg: '246' },
208
+ });
209
+ }
210
+ // Reviewer constraint findings (if review has completed)
211
+ const findings = chain.reviewerReport?.constraintFindings;
212
+ if (findings && findings.length > 0) {
213
+ const unsatisfied = findings.filter((f) => !f.satisfied);
214
+ sections.push({
215
+ type: 'text',
216
+ content: `Findings : ${findings.length} checked, ${unsatisfied.length} unsatisfied`,
217
+ style: { fg: unsatisfied.length > 0 ? '#ff6666' : '#44ff88' },
218
+ });
219
+ }
220
+ }
221
+ } catch {
222
+ // wrfcController.getChain throws when chain not found — normal during teardown
223
+ }
224
+ }
225
+
179
226
  // Progress
180
227
  if (rec.progress) {
181
228
  sections.push({ type: 'separator' });
@@ -24,9 +24,9 @@ import { renderPanelWorkspaceBar } from './panel-workspace-bar.ts';
24
24
  * path (e.g. deferred lazy-load) should call `this.invalidate()` AFTER the
25
25
  * trailing work so the next frame picks it up.
26
26
  *
27
- * If this becomes a real bug, the fix is to snapshot `needsRender` before
28
- * calling `render()` and only `markRendered()` if the snapshot was true
29
- * preserving any concurrent invalidation. Deferred pending more evidence.
27
+ * The fix (now applied): snapshot `needsRender` before calling `render()` and
28
+ * only call `markRendered()` when no concurrent invalidation occurred during
29
+ * the render pass preserving any mid-render invalidation.
30
30
  */
31
31
  interface PanelRenderCache {
32
32
  lines: Line[];
@@ -34,15 +34,52 @@ interface PanelRenderCache {
34
34
  height: number;
35
35
  }
36
36
  const panelRenderCache = new WeakMap<Panel, PanelRenderCache>();
37
+ /**
38
+ * Per-panel render-generation counter. Incremented whenever invalidate() fires
39
+ * on the panel (tracked here externally since Panel does not expose a version).
40
+ * We monkey-patch each panel the first time it enters renderPanel() by wrapping
41
+ * its invalidate() to bump the counter stored in this map.
42
+ *
43
+ * Race-guard: snapshot the generation before render(), compare after. If it
44
+ * changed, a mid-render invalidation occurred — leave needsRender=true.
45
+ */
46
+ const panelRenderGen = new WeakMap<Panel, { gen: number }>();
47
+
48
+ function getRenderGenState(panel: Panel): { gen: number } {
49
+ let state = panelRenderGen.get(panel);
50
+ if (!state) {
51
+ state = { gen: 0 };
52
+ panelRenderGen.set(panel, state);
53
+ // Wrap invalidate() to bump generation counter.
54
+ const origInvalidate = panel.invalidate.bind(panel);
55
+ panel.invalidate = () => {
56
+ state!.gen++;
57
+ origInvalidate();
58
+ };
59
+ }
60
+ return state;
61
+ }
37
62
 
38
63
  /** R2: Render a panel, skipping if nothing changed. Returns cached lines on a skip. */
39
- function renderPanel(panel: Panel, width: number, height: number): Line[] {
64
+ export function renderPanel(panel: Panel, width: number, height: number): Line[] {
40
65
  const cached = panelRenderCache.get(panel);
41
66
  if (cached && !panel.needsRender && cached.width === width && cached.height === height) {
42
67
  return cached.lines;
43
68
  }
69
+ // Snapshot render-generation counter BEFORE calling render(). If an event
70
+ // listener fires during render() and calls panel.invalidate(), the generation
71
+ // counter will be bumped. We compare after render to detect mid-render races.
72
+ const genState = getRenderGenState(panel);
73
+ const genBefore = genState.gen;
44
74
  const lines = panel.render(width, height);
45
- panel.markRendered();
75
+ // Only call markRendered() when no mid-render invalidation occurred.
76
+ // If the generation changed during render(), a concurrent invalidate() fired —
77
+ // leave needsRender=true so the next frame re-renders with the new state.
78
+ if (genState.gen === genBefore) {
79
+ panel.markRendered();
80
+ }
81
+ // If gen changed, needsRender is already true (invalidate() set it); do not
82
+ // call markRendered() — the next frame will pick it up.
46
83
  panelRenderCache.set(panel, { lines, width, height });
47
84
  return lines;
48
85
  }
@@ -17,7 +17,11 @@ export function renderPanelWorkspaceBar(
17
17
  return renderTabStrip({
18
18
  width,
19
19
  tabs: tabs.map((tab) => ({
20
- label: `${tab.pane === 'bottom' ? 'v' : '^'} ${tab.icon} ${tab.name}`,
20
+ // tab.active = selected in its own pane (drives highlighted background).
21
+ // tab.focused = has keyboard focus (drives brighter text / focus indicator).
22
+ // A tab can be active-but-not-focused (selected in the unfocused pane) or
23
+ // active-and-focused (selected in the focused pane).
24
+ label: `${tab.pane === 'bottom' ? 'v' : '^'} ${tab.icon} ${tab.name}${tab.focused ? ' ▸' : ''}`,
21
25
  active: tab.active,
22
26
  })),
23
27
  prefixLabel: ' PANELS ',
@@ -60,7 +60,11 @@ function buildAgentLabel(rec: AgentRecord, deps: ProcessModalDeps): string {
60
60
  const attemptMatch = task.match(/Fix attempt:\s*(\d+)/);
61
61
  const attempt = attemptMatch ? attemptMatch[1] : '?';
62
62
  const desc = truncateFirst(originalTask ?? 'fix in progress', 45);
63
- return `[Fix #${attempt}] ${desc} (${fromScore} \u2192 ${toScore}/10)`;
63
+ // Show constraint count when the chain has constraints to target (SDK 0.23.0)
64
+ const chain = rec.wrfcId ? (() => { try { return deps.wrfcController.getChain(rec.wrfcId!); } catch { return null; } })() : null;
65
+ const constraintCount = chain && chain.constraints.length > 0 ? chain.constraints.length : 0;
66
+ const constraintSuffix = constraintCount > 0 ? ` [${constraintCount}c]` : '';
67
+ return `[Fix #${attempt}] ${desc} (${fromScore} \u2192 ${toScore}/10)${constraintSuffix}`;
64
68
  }
65
69
 
66
70
  // Regular agent — show template and truncated first line
@@ -292,6 +292,64 @@ export async function initializeBootstrapCore(
292
292
  wrfcController: services.wrfcController,
293
293
  });
294
294
 
295
+ // ── TUI-specific WRFC constraint-propagation event subscriptions (SDK 0.23.0) ──
296
+ // These supplement the SDK's registerBootstrapRuntimeEvents which handles the
297
+ // core WORKFLOW_REVIEW_COMPLETED / WORKFLOW_CHAIN_CREATED messages.
298
+ // The SDK does not surface constraint-specific system messages; the TUI layer
299
+ // adds them here so operators can observe constraint enumeration and violations
300
+ // in the SystemMessagesPanel and main conversation.
301
+ runtimeUnsubs.push(
302
+ runtimeBus.on<Extract<import('@pellux/goodvibes-sdk/platform/runtime/events/index').WorkflowEvent, { type: 'WORKFLOW_CONSTRAINTS_ENUMERATED' }>>(
303
+ 'WORKFLOW_CONSTRAINTS_ENUMERATED',
304
+ ({ payload }) => {
305
+ const router = systemMessageRouterRef.value;
306
+ if (!router) return;
307
+ const count = payload.constraints.length;
308
+ if (count > 0) {
309
+ router.wrfc(
310
+ `[WRFC] Engineer enumerated ${count} constraint${count !== 1 ? 's' : ''} for chain ${payload.chainId.slice(0, 12)}`,
311
+ 'low',
312
+ );
313
+ }
314
+ requestRender();
315
+ },
316
+ ),
317
+ );
318
+ runtimeUnsubs.push(
319
+ runtimeBus.on<Extract<import('@pellux/goodvibes-sdk/platform/runtime/events/index').WorkflowEvent, { type: 'WORKFLOW_FIX_ATTEMPTED' }>>(
320
+ 'WORKFLOW_FIX_ATTEMPTED',
321
+ ({ payload }) => {
322
+ const router = systemMessageRouterRef.value;
323
+ if (!router) return;
324
+ const targetIds = payload.targetConstraintIds;
325
+ if (targetIds && targetIds.length > 0) {
326
+ router.wrfc(
327
+ `[WRFC] Fix #${payload.attempt} targeting ${targetIds.length} constraint${targetIds.length !== 1 ? 's' : ''} on chain ${payload.chainId.slice(0, 12)}`,
328
+ 'low',
329
+ );
330
+ requestRender();
331
+ }
332
+ },
333
+ ),
334
+ );
335
+ runtimeUnsubs.push(
336
+ runtimeBus.on<Extract<import('@pellux/goodvibes-sdk/platform/runtime/events/index').WorkflowEvent, { type: 'WORKFLOW_REVIEW_COMPLETED' }>>(
337
+ 'WORKFLOW_REVIEW_COMPLETED',
338
+ ({ payload }) => {
339
+ const router = systemMessageRouterRef.value;
340
+ if (!router) return;
341
+ const unsatisfied = payload.unsatisfiedConstraintIds;
342
+ if (!payload.passed && unsatisfied && unsatisfied.length > 0) {
343
+ router.wrfc(
344
+ `[WRFC] ✗ Chain ${payload.chainId.slice(0, 12)}: ${unsatisfied.length} constraint violation${unsatisfied.length !== 1 ? 's' : ''} forced failure`,
345
+ 'high',
346
+ );
347
+ requestRender();
348
+ }
349
+ },
350
+ ),
351
+ );
352
+
295
353
  // Subscribe to companion main-chat messages received from the daemon's HTTP layer.
296
354
  // The daemon emits COMPANION_MESSAGE_RECEIVED on the runtime bus when a companion
297
355
  // POST /api/sessions/:id/messages with kind='message' arrives.
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.19.22';
9
+ let _version = '0.19.24';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;