create-agdf 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/generated/.opencode/agdf-interaction-locales.json +148 -4
  2. package/generated/plugins/agdf/.claude-plugin/plugin.json +1 -1
  3. package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
  4. package/generated/plugins/agdf/companion/events.mjs +5 -3
  5. package/generated/plugins/agdf/companion/figure.mjs +241 -110
  6. package/generated/plugins/agdf/companion/localization.mjs +5 -0
  7. package/generated/plugins/agdf/companion/notice.mjs +10 -2
  8. package/generated/plugins/agdf/companion/register.mjs +90 -32
  9. package/generated/plugins/agdf/companion/run-state.mjs +13 -1
  10. package/generated/plugins/agdf/companion/view.mjs +149 -19
  11. package/generated/plugins/agdf/mcp/server/package.json +2 -2
  12. package/generated/plugins/agdf/mcp/server/src/generation-runtime.js +6 -4
  13. package/generated/plugins/agdf/mcp/server/src/generation.js +8 -2
  14. package/generated/plugins/agdf/mcp/server/src/main.js +4 -1
  15. package/generated/plugins/agdf/mcp/server/src/server.js +19 -5
  16. package/generated/plugins/agdf/mcp/server/ui/cockpit.html +130 -33
  17. package/generated/plugins/agdf/mcp/server/ui/decision-manifest.json +3 -3
  18. package/generated/plugins/agdf/mcp/server/ui/decision.html +3 -3
  19. package/generated/plugins/agdf/mcp/server/ui/manifest.json +3 -3
  20. package/generated/plugins/agdf/meta/agdf-interaction-locales.json +148 -4
  21. package/generated/plugins/agdf/meta/agdf-mcp-capability.json +1 -1
  22. package/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  23. package/generated/plugins/agdf/meta/copilot-payload-baseline.json +3 -3
  24. package/generated/plugins/agdf/meta/distribution-profile-history.json +7 -0
  25. package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-interaction-locales.json +148 -4
  26. package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  27. package/generated/plugins/agdf/runtime/create-agdf/lib/mcp-dispatch-runtime.js +4 -1
  28. package/generated/plugins/agdf/runtime/create-agdf/lib/mcp-worker-runtime.js +77 -77
  29. package/generated/plugins/agdf/runtime/create-agdf/package.json +1 -1
  30. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/companion/events.js +4 -1
  31. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/companion/figure-state.js +6 -4
  32. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/companion/journal.js +6 -0
  33. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/companion/run-view.js +54 -11
  34. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-evaluation/decision-content-placeholder.js +4 -1
  35. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-evaluation/gate-check.js +5 -1
  36. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-inspect/cockpit-list.js +10 -8
  37. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-inspect/cockpit-session.js +11 -3
  38. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-inspect/cockpit-work.js +4 -2
  39. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-inspect/cockpit.js +43 -13
  40. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-inspect/work-situation.js +25 -0
  41. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-read/cockpit-worker.js +1 -0
  42. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-create-command.js +6 -1
  43. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-inventory-reader.js +50 -3
  44. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-inventory.js +12 -27
  45. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-presentation-render.js +29 -0
  46. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-title.js +43 -0
  47. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/interaction-catalog-validation.js +2 -1
  48. package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/interaction-presentation.js +5 -9
  49. package/generated/plugins/agdf/runtime/runtime-manifest.json +2 -2
  50. package/generated/plugins/agdf/submission/openai/availability.md +1 -1
  51. package/generated/plugins/agdf/submission/openai/capability-matrix.json +1 -1
  52. package/generated/plugins/agdf/submission/openai/release-notes.md +5 -8
  53. package/generated/plugins/agdf/submission/openai/reviewer-cases.json +1 -1
  54. package/generated/plugins/agdf-planning/.codex-plugin/plugin.json +1 -1
  55. package/generated/plugins/agdf-planning/plugin.json +1 -1
  56. package/generated/plugins/copilot/agdf/.agdf-payload-inventory.json +76 -58
  57. package/generated/plugins/copilot/agdf/mcp/server/package.json +2 -2
  58. package/generated/plugins/copilot/agdf/mcp/server/src/generation-runtime.js +6 -4
  59. package/generated/plugins/copilot/agdf/mcp/server/src/generation.js +8 -2
  60. package/generated/plugins/copilot/agdf/mcp/server/src/main.js +4 -1
  61. package/generated/plugins/copilot/agdf/mcp/server/src/server.js +19 -5
  62. package/generated/plugins/copilot/agdf/meta/agdf-plugin.definition.json +1 -1
  63. package/generated/plugins/copilot/agdf/meta/distribution-profile-history.json +7 -0
  64. package/generated/plugins/copilot/agdf/plugin.json +1 -1
  65. package/generated/plugins/copilot/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-interaction-locales.json +148 -4
  66. package/generated/plugins/copilot/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  67. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/mcp-dispatch-runtime.js +4 -1
  68. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/mcp-worker-runtime.js +62 -62
  69. package/generated/plugins/copilot/agdf/runtime/create-agdf/package.json +1 -1
  70. package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/companion/events.js +4 -1
  71. package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/companion/figure-state.js +6 -4
  72. package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/companion/journal.js +6 -0
  73. package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/companion/run-view.js +54 -11
  74. package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/control-evaluation/decision-content-placeholder.js +4 -1
  75. package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/control-evaluation/gate-check.js +5 -1
  76. package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/control-inspect/work-situation.js +25 -0
  77. package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-create-command.js +6 -1
  78. package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-inventory-reader.js +50 -3
  79. package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-inventory.js +12 -27
  80. package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-presentation-render.js +29 -0
  81. package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-title.js +43 -0
  82. package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/interaction-catalog-validation.js +2 -1
  83. package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/interaction-presentation.js +5 -9
  84. package/generated/plugins/copilot/agdf/runtime/runtime-manifest.json +2 -2
  85. package/generated/plugins/copilot/agdf/skills/agdf-interaction-locales.json +1 -1
  86. package/generated/submissions/openai/agdf/.codex-plugin/plugin.json +1 -1
  87. package/generated/submissions/openai/agdf/meta/agdf-interaction-locales.json +148 -4
  88. package/generated/submissions/openai/agdf/meta/agdf-plugin.definition.json +1 -1
  89. package/generated/submissions/openai/agdf/meta/copilot-payload-baseline.json +3 -3
  90. package/generated/submissions/openai/agdf/meta/distribution-profile-history.json +7 -0
  91. package/generated/submissions/openai/agdf/plugin.json +1 -1
  92. package/generated/submissions/openai/agdf/submission/openai/availability.md +1 -1
  93. package/generated/submissions/openai/agdf/submission/openai/capability-matrix.json +1 -1
  94. package/generated/submissions/openai/agdf/submission/openai/inventory.json +13 -13
  95. package/generated/submissions/openai/agdf/submission/openai/listing.json +1 -1
  96. package/generated/submissions/openai/agdf/submission/openai/readiness.json +2 -2
  97. package/generated/submissions/openai/agdf/submission/openai/readiness.md +2 -2
  98. package/generated/submissions/openai/agdf/submission/openai/release-notes.md +5 -8
  99. package/generated/submissions/openai/agdf/submission/openai/reviewer-cases.json +1 -1
  100. package/lib/mcp-dispatch-runtime.js +4 -1
  101. package/lib/mcp-worker-runtime.js +77 -77
  102. package/package.json +1 -1
  103. package/runtime/core/lib/companion/events.js +4 -1
  104. package/runtime/core/lib/companion/figure-state.js +6 -4
  105. package/runtime/core/lib/companion/journal.js +6 -0
  106. package/runtime/core/lib/companion/run-view.js +54 -11
  107. package/runtime/core/lib/control-evaluation/decision-content-placeholder.js +4 -1
  108. package/runtime/core/lib/control-evaluation/gate-check.js +5 -1
  109. package/runtime/core/lib/control-inspect/cockpit-list.js +10 -8
  110. package/runtime/core/lib/control-inspect/cockpit-session.js +11 -3
  111. package/runtime/core/lib/control-inspect/cockpit-work.js +4 -2
  112. package/runtime/core/lib/control-inspect/cockpit.js +43 -13
  113. package/runtime/core/lib/control-inspect/work-situation.js +25 -0
  114. package/runtime/core/lib/control-read/cockpit-worker.js +1 -0
  115. package/runtime/core/lib/control-state/run-create-command.js +6 -1
  116. package/runtime/core/lib/control-state/run-inventory-reader.js +50 -3
  117. package/runtime/core/lib/control-state/run-inventory.js +12 -27
  118. package/runtime/core/lib/control-state/run-presentation-render.js +29 -0
  119. package/runtime/core/lib/control-state/run-title.js +43 -0
  120. package/runtime/core/lib/interaction-catalog-validation.js +2 -1
  121. package/runtime/core/lib/interaction-presentation.js +5 -9
@@ -11,7 +11,7 @@
11
11
  import { assignRun, namedRun, openRuns, stepSelection } from './assignment.mjs';
12
12
  import { EVENT_POLL_MS, JOURNAL_FILE, JOURNAL_READ_LIMIT, JOURNAL_ROTATED_FILE, PANE_EVENTS, STICKY_KINDS, TOAST_KINDS, eventText,
13
13
  isConfirmed, isStillUncertain, journalDirectory, namedEventText, parseJournal, unseenEvents } from './events.mjs';
14
- import { figureSvg } from './figure.mjs';
14
+ import { figureSvg, observeFigure } from './figure.mjs';
15
15
  import { chatLocale, companionLabels, gateTitle, label } from './localization.mjs';
16
16
  import { CHIME, chimeClip, noticeText, observeNotice, observedRun, statusText } from './notice.mjs';
17
17
  import { STATE_FILE, defaultCompanionState, parseCompanionState, pluginDataDir, serializeCompanionState } from './persistence.mjs';
@@ -39,12 +39,14 @@ const BODY_RESERVE = 0;
39
39
  // stay beside the widest bar (owner request of 2026-10-28); a narrower pane lists the ids alone.
40
40
  const ID_ROOM = 18;
41
41
  const COLOR = Object.freeze({ laeuft: 'suggestion', baut: 'suggestion', wartet: 'warning', blockiert: 'error', fertig: 'success' });
42
+ // The colour of the "whose move" line: yours, the agent's, or a block.
43
+ const TURN_COLOR = Object.freeze({ you: 'warning', claude: 'suggestion', halt: 'error', next: undefined });
42
44
 
43
45
  function createSession() {
44
46
  return { startedAt: 0, root: '', registry: null, labels: null, locale: 'en', statePath: null, dataDir: null, version: null, icon: null,
45
- choices: defaultCompanionState(), isHidden: false, snapshot: null, isTurnActive: false, isObjectiveExpanded: false,
47
+ choices: defaultCompanionState(), isHidden: false, snapshot: null, isTurnActive: false, isInfoExpanded: false,
46
48
  isPreviewExpanded: false, isAboutShown: false, isMenuOpen: false, timer: null, eventTimer: null, reads: 0, appliedRead: 0,
47
- observation: null, chatMention: null, isBrowsing: false, conversation: null,
49
+ observation: null, figureObservation: null, chatMention: null, isBrowsing: false, conversation: null,
48
50
  // Companion core SDD-007: the run-file fingerprint of the last run-view call, the journal cursor and
49
51
  // stamp, this root's events since the last main-loop turn and the drift warnings already announced.
50
52
  fingerprint: null, journalCursor: null, journalStamp: null, eventsAvailable: null, events: [], driftSeen: null,
@@ -142,16 +144,23 @@ async function refresh($, { quiet } = {}) {
142
144
  const root = await $.session.root();
143
145
  await followConversation($);
144
146
  const remembered = session.choices.selections[root];
147
+ // Keep evaluating the run already followed in this project when it leaves the active list;
148
+ // its completed state must be confirmed before the companion celebrates.
149
+ const observed = root === session.root ? observedRun({ snapshot: session.snapshot, sessionStartedAt: session.startedAt,
150
+ selection: remembered, mention: session.chatMention }).runId : null;
151
+ const runIds = [...new Set([remembered, observed].filter(Boolean))];
145
152
  // Taken before the call: a change during the call shows up at the next check.
146
153
  const ids = fingerprintIds(root);
147
154
  const print = await fingerprint($, root, ids);
148
155
  const next = await readSnapshot(processes($), { pluginRoot: $.plugin.root, root, language: session.locale,
149
- runIds: remembered ? [remembered] : [], now: await $.clock.now(), clock: () => $.clock.now() });
156
+ runIds, now: await $.clock.now(), clock: () => $.clock.now() });
150
157
  if (read < session.appliedRead) return;
151
158
  session.appliedRead = read;
152
159
  if (root !== session.root) {
153
160
  // Another project root: its drift baseline, pane entries and preview start over (CR finding B3).
154
161
  session.driftSeen = null;
162
+ session.snapshot = null;
163
+ session.figureObservation = null;
155
164
  session.events = [];
156
165
  session.unconfirmed = [];
157
166
  session.isPreviewExpanded = false;
@@ -160,9 +169,10 @@ async function refresh($, { quiet } = {}) {
160
169
  session.snapshot = settleSnapshot(session.snapshot, next);
161
170
  if (next.status === 'ok') session.events = session.events.filter(event => !STICKY_KINDS.includes(event.kind) || isStillUncertain(event, next.runs));
162
171
  // A changed set of open runs is listed again, so the next check compares like with like. A failed
163
- // read keeps no fingerprint, so the next check reads again (CR finding B1).
172
+ // read keeps no fingerprint, so the next check reads again (CR finding B1); so does a view with a
173
+ // run that ran into the read budget, which may read on the next attempt.
164
174
  const after = fingerprintIds(root);
165
- session.fingerprint = next.status === 'failed' ? null
175
+ session.fingerprint = next.status === 'failed' || next.retry ? null
166
176
  : `${session.locale}|${after.join() === ids.join() ? print : await fingerprint($, root, after)}`;
167
177
  const chosen = selection();
168
178
  if (chosen && next.status === 'ok' && next.runs.length) {
@@ -180,8 +190,9 @@ async function refresh($, { quiet } = {}) {
180
190
  }
181
191
 
182
192
  // The run-file fingerprint (SDD-007): the run folders, `config.json` and, for each open or selected
183
- // run, its RUN_STATE and documents entries with size and modification time from `$.fs.list`. It reads
184
- // listings only, never a run file.
193
+ // run, its RUN_STATE, documents and evidence entries with size and modification time from `$.fs.list`:
194
+ // the files the run view compares for drift (bound documents and evidence reports). It reads listings
195
+ // only, never a run file.
185
196
  const fingerprintIds = root => [...new Set([...(session.snapshot?.runs ?? []).filter(run => run.lifecycle === 'active').map(run => run.id),
186
197
  session.choices.selections[root]].filter(Boolean))].sort();
187
198
 
@@ -192,7 +203,11 @@ async function fingerprint($, root, ids = fingerprintIds(root)) {
192
203
  const runs = await list(`${control}/runs`);
193
204
  const parts = [`config:${files((await list(control))?.filter(entry => entry.name === 'config.json'))}`,
194
205
  `runs:${(runs ?? []).map(entry => entry.name).sort().join(',')}`];
195
- for (const id of ids) parts.push(`${id}:${files(await list(`${control}/runs/${id}`))}|${files(await list(`${control}/runs/${id}/documents`))}`);
206
+ for (const id of ids) {
207
+ const folders = [];
208
+ for (const folder of ['', '/documents', '/evidence']) folders.push(files(await list(`${control}/runs/${id}${folder}`)));
209
+ parts.push(`${id}:${folders.join('|')}`);
210
+ }
196
211
  return parts.join(';');
197
212
  }
198
213
 
@@ -375,14 +390,24 @@ export function fitRunId(id, room) {
375
390
  return `${id.slice(0, room - 1 - tail)}…${id.slice(id.length - tail)}`;
376
391
  }
377
392
 
393
+ // A run name cut at its end to `room` characters; a name reads from its start, unlike an id.
394
+ export function fitRunName(name, room) {
395
+ return name.length <= room ? name : `${name.slice(0, Math.max(1, room - 1))}…`;
396
+ }
397
+
378
398
  const press = ($, action) => async () => {
379
399
  try { await action(); } catch { /* a failed action leaves the last drawing */ }
380
400
  redraw($);
381
401
  };
382
402
 
383
- function body($, e, view) {
403
+ function body($, e) {
384
404
  const { Box, Text, Button, Svg, Link, Select } = $.ui.resolve(e);
385
405
  const isTerminal = e.surface === 'terminal' || !Svg;
406
+ const view = buildPanelView({ snapshot: session.snapshot, sessionStartedAt: session.startedAt, selection: selection(),
407
+ mention: session.chatMention, isBrowsing: session.isBrowsing, showRunState: !isTerminal,
408
+ isTurnActive: session.isTurnActive, registry: session.registry, labels: session.labels, locale: session.locale,
409
+ columns: e.props?.bodyColumns ?? Infinity, isInfoExpanded: session.isInfoExpanded,
410
+ events: session.events, eventsAvailable: session.eventsAvailable ?? undefined });
386
411
  const rows = e.props?.scroll?.bodyRows;
387
412
  const fits = Number.isInteger(rows) && rows > 0;
388
413
  const columns = Number.isInteger(e.props?.bodyColumns) ? e.props.bodyColumns : 40;
@@ -443,14 +468,15 @@ function body($, e, view) {
443
468
  // The terminal keeps its six; the desktop lists as many as fit and gives the last row to "+ N more".
444
469
  const listed = isTerminal ? view.openRuns.slice(0, LISTED_RUNS)
445
470
  : view.openRuns.length <= capacity ? view.openRuns : view.openRuns.slice(0, capacity - 1);
446
- // One listed run: the id as a plain button and, where the pane has room for the widest listed bar, its
447
- // condensed gate bar at the right edge, every id shortened to the same room (the same rule for all rows,
448
- // live check of 2026-10-28). The bar keeps its width; an id that still runs over is cut by the surface.
471
+ // One listed run: its name as a plain button (owner request of 2026-11-11: a run id tells a person
472
+ // nothing) and, where the pane has room for the widest listed bar, its condensed gate bar at the right
473
+ // edge, every name cut to the same room (the same rule for all rows, live check of 2026-10-28). The bar
474
+ // keeps its width; a name that still runs over is cut by the surface.
449
475
  const widest = Math.max(0, ...listed.map(run => run.bar?.length ?? 0));
450
476
  const room = columns - widest - 2;
451
477
  const withBars = widest > 0 && room >= ID_ROOM;
452
478
  const runRow = run => {
453
- const id = h(Button, { key: `run-${run.id}`, plain: true, label: withBars ? fitRunId(run.id, room) : run.id, onPress: select(run.id) });
479
+ const id = h(Button, { key: `run-${run.id}`, plain: true, label: fitRunName(run.name ?? run.id, withBars ? room : columns), onPress: select(run.id) });
454
480
  return withBars && run.bar
455
481
  ? h(Box, { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 1 },
456
482
  h(Box, { flexShrink: 1 }, id), h(Box, { flexShrink: 0 }, h(Text, { dimColor: true, mono: true }, run.bar)))
@@ -472,34 +498,64 @@ function body($, e, view) {
472
498
  ...listed.map(runRow),
473
499
  !isTerminal && view.openRuns.length > listed.length ? h(Text, { dimColor: true }, t('fields.moreRuns', { count: view.openRuns.length - listed.length })) : null))
474
500
  : null;
501
+ // The (i) opens the run's details on purpose (owner request of 2026-11-12); closed by default and
502
+ // only offered when there is something behind it. The label names the action, so it reads alone.
503
+ const infoButton = view.info?.length ? h(Button, { key: 'info', plain: true,
504
+ label: view.isInfoExpanded ? t('actions.infoHide') : t('actions.infoShow'),
505
+ onPress: press($, async () => { session.isInfoExpanded = !session.isInfoExpanded; }) }) : null;
475
506
  if (isTerminal) {
476
507
  return h(Box, { flexDirection: 'column', gap: 1 },
477
- h(Text, { color: COLOR[view.mood] }, terminalText(view, { labels: session.labels, locale: session.locale })), runList, about, actions);
508
+ h(Text, { color: view.resting ? undefined : COLOR[view.mood] }, terminalText(view, { labels: session.labels, locale: session.locale })), infoButton, runList, about, actions);
478
509
  }
479
- const figure = h(Svg, { source: figureSvg(view.mood, { still: session.choices.still, title: `${view.title}: ${view.stateLabel}` }),
480
- alt: `${view.title}: ${view.stateLabel}`, width: 200, height: 140, isInteractive: !session.choices.still });
510
+ const scope = view.runId ? JSON.stringify([session.root, view.runId]) : null;
511
+ const quiet = !session.choices.enabled || session.choices.still || session.isHidden || Boolean(view.stale);
512
+ const still = quiet || !scope || ['unreadable', 'idle', 'completionUnconfirmed'].includes(view.figure?.reason.key);
513
+ // Without an assigned run a readable project smiles and keeps the idle motion (blinking) unless the
514
+ // still pose applies; it never gestures. The smile is a static face, so the still pose keeps it.
515
+ const idle = !scope && view.idle === true;
516
+ const idling = idle && !quiet;
517
+ session.figureObservation = observeFigure(session.figureObservation, { scope, figure: view.figure, current: !still });
518
+ const { gate, gesture, sequence } = session.figureObservation ?? {};
519
+ // With every run done the crew member checks the board: a tablet, never a finish flag (no run is shown).
520
+ const figure = h(Svg, { source: figureSvg(view.mood, { still: still && !idling, smile: idle, tablet: idle && view.allDone === true, invite: idle && view.invite === true, resting: view.resting === true, gate, workGate: view.workGate, gesture, sequence }),
521
+ alt: `${view.title}: ${view.stateLabel}`, width: 200, height: 140, isInteractive: !still });
481
522
  // The body is as tall as the pane's body rows less the probed reserve and the content grows, so the
482
523
  // footer sits at the bottom edge; a taller body scrolls with the footer last (live probes of
483
524
  // 2026-10-28: a cell count sizes the body, a percentage does not; SDD-08).
484
525
  return h(Box, { flexDirection: 'column', gap: 1, ...(fits ? { minHeight: rows - BODY_RESERVE } : {}) }, h(Box, { flexDirection: 'column', gap: 1, flexGrow: 1 },
485
526
  h(Box, { flexDirection: 'column', alignItems: 'center' },
486
527
  figure,
487
- h(Text, { color: COLOR[view.mood], size: 'heading', weight: 'bold' }, view.stateLabel),
488
- view.reason ? h(Text, { dimColor: true, wrap: 'truncate-end' }, view.reason) : null),
489
- view.message ? h(Text, {}, view.message) : null,
528
+ // A resting run carries no signal colour: nothing happens and nothing is asked of the person.
529
+ h(Text, { color: view.resting ? undefined : COLOR[view.mood], size: 'heading', weight: 'bold' }, view.stateLabel),
530
+ // The turn block below says what the reason would; without one the reason stays under the state.
531
+ view.reason && !view.turn ? h(Text, { dimColor: true, wrap: 'truncate-end' }, view.reason) : null),
532
+ // An invitation reads as the pane's headline; every other message stays plain.
533
+ view.message ? h(Text, view.invite ? { weight: 'semibold', wrap: 'wrap' } : {}, view.message) : null,
490
534
  view.hint ? h(Text, { dimColor: true }, view.hint) : null,
491
535
  view.unreadableHint && view.unreadableHint !== view.message ? h(Text, { color: 'warning' }, view.unreadableHint) : null,
536
+ // The run by its name, with its delivery path and the position on it.
537
+ view.name ? h(Box, { flexDirection: 'column' },
538
+ h(Text, { weight: 'semibold', wrap: 'wrap' }, view.name),
539
+ view.route ? h(Text, { dimColor: true, wrap: 'truncate-end' }, view.route) : null) : null,
540
+ // The run's goal in the chat language, before whose move it is: what an approval is measured against.
541
+ view.goal ? h(Box, { flexDirection: 'column' },
542
+ h(Text, { dimColor: true }, t('fields.objective')),
543
+ h(Text, { wrap: 'wrap' }, view.goal)) : null,
544
+ // Whose move it is, what it is and, for an approval, how it is given.
545
+ view.turn ? h(Box, { flexDirection: 'column', borderStyle: 'single', borderDimColor: true, paddingX: 1 },
546
+ h(Text, { color: TURN_COLOR[view.turn.tone], weight: 'semibold' }, view.turn.who),
547
+ h(Text, { wrap: 'wrap' }, view.turn.what),
548
+ view.turn.how ? h(Text, { dimColor: true, wrap: 'wrap' }, view.turn.how) : null) : null,
492
549
  view.gateBar ? h(Text, { mono: true, wrap: 'truncate-end' }, view.gateBar) : null,
493
- view.runId ? h(Box, { flexDirection: 'column' },
494
- h(Text, { weight: 'semibold', wrap: 'truncate-end' }, view.runId),
495
- view.origin ? h(Text, { dimColor: true }, view.origin) : null) : null,
496
- view.objective ? h(Box, { flexDirection: 'row', gap: 1, alignItems: 'center' },
497
- h(Text, { dimColor: true, wrap: view.isObjectiveExpanded ? 'wrap' : 'truncate-end' }, view.objective),
498
- h(Button, { key: 'objective', plain: true, label: view.isObjectiveExpanded ? t('actions.collapse') : t('actions.expand'),
499
- onPress: press($, async () => { session.isObjectiveExpanded = !session.isObjectiveExpanded; }) })) : null,
500
550
  view.nextStep ? h(Box, { flexDirection: 'column' },
501
551
  h(Text, { dimColor: true }, t('fields.nextStep')),
502
552
  h(Text, {}, view.nextStep)) : null,
553
+ // The (i) and, once opened, the details behind it: a goal that was cut, the gate's purpose, a repeated step.
554
+ infoButton,
555
+ view.isInfoExpanded && view.info?.length ? h(Box, { flexDirection: 'column', gap: 1, borderStyle: 'single', borderDimColor: true, paddingX: 1 },
556
+ ...view.info.map(item => h(Box, { flexDirection: 'column' },
557
+ h(Text, { dimColor: true }, item.label),
558
+ h(Text, { wrap: 'wrap' }, item.text)))) : null,
503
559
  ...(view.drift ?? []).map(text => h(Text, { color: 'warning' }, text)),
504
560
  view.events ? h(Box, { flexDirection: 'column' },
505
561
  h(Text, { dimColor: true }, t('fields.events')),
@@ -514,6 +570,10 @@ function body($, e, view) {
514
570
  view.review.truncated ? h(Text, { dimColor: true }, view.review.truncated) : null) : null,
515
571
  view.eventsHint ? h(Text, { dimColor: true }, view.eventsHint) : null,
516
572
  runList,
573
+ // The run id is a reference for commands, so it stands last and dim.
574
+ view.runId ? h(Box, { flexDirection: 'column' },
575
+ h(Text, { dimColor: true, mono: true, wrap: 'truncate-end' }, view.runId),
576
+ view.origin ? h(Text, { dimColor: true }, view.origin) : null) : null,
517
577
  view.stale ? h(Text, { color: 'warning' }, view.stale) : null,
518
578
  about),
519
579
  actions);
@@ -545,6 +605,7 @@ export const register = on => {
545
605
  // Reading, the status line and the pane stop before the choice is stored, so a failing write
546
606
  // leaves none of them running (CR-F03); the choice is stored even if closing the pane fails.
547
607
  stopRefreshing();
608
+ session.figureObservation = null;
548
609
  silence($);
549
610
  try {
550
611
  await $.ui.close({ id: PANE });
@@ -609,6 +670,7 @@ export const register = on => {
609
670
  // stamps `origin` as `{ kind: 'plugin' | 'person' | 'unload' }`.
610
671
  on('ui.close', { id: PANE }, async ($, e, next) => {
611
672
  try {
673
+ session.figureObservation = null;
612
674
  if (e.origin?.kind === 'person') session.isHidden = true;
613
675
  } catch { /* observation only */ }
614
676
  return next(e);
@@ -617,11 +679,7 @@ export const register = on => {
617
679
  // A logic failure draws a neutral line instead of the panel; it never throws into the host.
618
680
  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
619
681
  try {
620
- return body($, e, buildPanelView({ snapshot: session.snapshot, sessionStartedAt: session.startedAt, selection: selection(),
621
- mention: session.chatMention, isBrowsing: session.isBrowsing,
622
- isTurnActive: session.isTurnActive, registry: session.registry, labels: session.labels, locale: session.locale,
623
- columns: e.props?.bodyColumns ?? Infinity, isObjectiveExpanded: session.isObjectiveExpanded,
624
- events: session.events, eventsAvailable: session.eventsAvailable ?? undefined }));
682
+ return body($, e);
625
683
  } catch (error) {
626
684
  const { Text } = $.ui.resolve(e);
627
685
  return h(Text, { dimColor: true }, `AGDF Companion: ${String(error?.message ?? error).slice(0, 200)}`);
@@ -10,6 +10,8 @@ export const READ_FAILURE_REASONS = Object.freeze(['timeout', 'unavailable', 'ex
10
10
  // Store reasons of a read that may succeed on the next attempt (a write during the read, the read
11
11
  // budget); they keep the last good view, marked stale, like a failed read (CR delta review RV-1).
12
12
  export const TRANSIENT_STORE_REASONS = Object.freeze(['source_changed', 'timeout']);
13
+ // Evaluation errors of a single run that make the whole view incomplete (a write during its read).
14
+ export const TRANSIENT_RUN_REASONS = Object.freeze(['source_changed']);
13
15
  // The internal steps between an approved TP and QA, shown as the build phase in the gate bar.
14
16
  export const BUILD_GATES = Object.freeze(['Brownfield Analysis', 'CD+Tests', 'CR']);
15
17
  export const isStructured = route => route === 'structured_delivery' || route === 'structured_slice';
@@ -41,6 +43,11 @@ export function companionRun(entry) {
41
43
  drift: Object.freeze((entry.drift ?? []).map(item => Object.freeze({ ...item }))),
42
44
  // The authored objective, never the status card's run title or a template text.
43
45
  objective: clip(entry.objective ?? ''),
46
+ // The run's stable human title (registered UR heading, else the authored objective); empty while
47
+ // only the run id could name it. The goal is the objective in the display language where the UR
48
+ // states one.
49
+ title: clip(entry.run_title ?? ''),
50
+ goal: clip(entry.goal ?? entry.objective ?? ''),
44
51
  });
45
52
  }
46
53
 
@@ -57,7 +64,9 @@ export function snapshotFromRunView(stdout, { now }) {
57
64
  }
58
65
  // A run read while its files changed (after the run view's bounded retries) makes the view incomplete;
59
66
  // like a transient store failure it keeps the last good view instead of showing that run as blocked.
60
- const changedRun = view.runs.find(run => TRANSIENT_STORE_REASONS.includes(run?.evaluation_error));
67
+ // A single run that ran into the read budget is not transient: it stays that run's own reason, so the
68
+ // other runs are still shown.
69
+ const changedRun = view.runs.find(run => TRANSIENT_RUN_REASONS.includes(run?.evaluation_error));
61
70
  if (changedRun) return { status: 'failed', runs: [], unreadable: [], readAt: now, error: changedRun.evaluation_error };
62
71
  const status = view.store.status === 'ok' ? 'ok' : view.store.status === 'no_control' ? 'no_control' : 'unavailable';
63
72
  return {
@@ -69,6 +78,9 @@ export function snapshotFromRunView(stdout, { now }) {
69
78
  runs: view.runs.filter(entry => entry.valid !== false && entry.listed !== false).map(companionRun),
70
79
  unreadable: (view.diagnosis?.unreadable ?? []).map(({ run_id, reason }) => ({ id: run_id, reason })),
71
80
  diagnosis: view.diagnosis ?? null,
81
+ // A run that ran into the read budget may read on the next attempt: the adapter keeps no
82
+ // fingerprint for such a view, so the next check reads again instead of waiting for a file change.
83
+ retry: view.runs.some(run => run?.evaluation_error === 'timeout'),
72
84
  readAt: now,
73
85
  };
74
86
  }
@@ -3,12 +3,12 @@
3
3
  // host-independent; the adapter draws it on desktop or as text in the terminal.
4
4
 
5
5
  import { assignRun } from './assignment.mjs';
6
- import { FIGURE_TEXT } from './figure.mjs';
6
+ import { FIGURE_TEXT, INVITE_TEXT } from './figure.mjs';
7
7
  import { condensedGateBar, gateBar, gateMarks } from './gate-bar.mjs';
8
- import { gateTitle, label } from './localization.mjs';
9
- import { displayState } from './state.mjs';
10
- import { READ_FAILURE_REASONS } from './run-state.mjs';
11
- import { eventText, latestReview, PANE_EVENTS, shortDigest } from './events.mjs';
8
+ import { gateRationale, gateTitle, label } from './localization.mjs';
9
+ import { displayState, runState } from './state.mjs';
10
+ import { isStructured, READ_FAILURE_REASONS } from './run-state.mjs';
11
+ import { eventText, latestReview, PANE_EVENTS, shortDigest, shortRunId } from './events.mjs';
12
12
 
13
13
  const two = value => String(value).padStart(2, '0');
14
14
  // Preview lines the terminal text shows beneath the material line.
@@ -18,8 +18,81 @@ const folderName = root => String(root ?? '').replace(/[\\/]+$/u, '').split(/[\\
18
18
  // Host-local wall-clock time, HH:MM.
19
19
  export const clockText = ms => { const at = new Date(ms); return `${two(at.getHours())}:${two(at.getMinutes())}`; };
20
20
 
21
+ // The name a person reads for the shown run: its stable title, else the catalog's "new run". The run
22
+ // id never names a run; it stays a reference line at the panel's end.
23
+ export const runName = (run, t) => run.title || t('fields.newRun');
24
+ // A listed run needs a name that tells it from its neighbours, so an untitled one adds its short id.
25
+ export const listedRunName = (run, t) => run.title || `${t('fields.newRun')} · ${shortRunId(run.id)}`;
26
+
27
+ // Whose move it is (owner request of 2026-11-11): read from the figure state Core derived, so the
28
+ // block never decides anything itself. `tone` is you (an approval or decision is yours), claude (the
29
+ // agent works on the gate), next (the step is up next and nobody works on it) or halt (the run is
30
+ // blocked). This function words the moves that are yours and the block; activityOf below words the work.
31
+ // A state that concerns no gate work has no such block, and neither has the terminal's turn overlay
32
+ // beside a run that is not working: its reason already says that Claude works.
33
+ export function turnOf(state, run, t, gateName) {
34
+ const { key, gate, decision } = state.reason;
35
+ const at = gate ?? run.gate;
36
+ if (!at) return null;
37
+ if (key === 'waitingApproval') {
38
+ return { tone: 'you', who: t('turn.you'), what: t('turn.approve', { gate: gateName(at) }),
39
+ how: run.missing && run.missing !== 'none' ? t('turn.inChat', { approval: run.missing }) : null, gate: at };
40
+ }
41
+ if (key === 'waitingDecision') return { tone: 'you', who: t('turn.you'), what: t('turn.decide', { gate: gateName(at) }), how: null, gate: at };
42
+ if (key === 'blocked') {
43
+ return { tone: 'halt', who: t('turn.blocked'), what: t('reasons.blocked', { gate: gateName(at), decision: run.blockingText || decision || '' }), how: null, gate: at };
44
+ }
45
+ return null;
46
+ }
47
+
48
+ // Which kind of work a step is; the catalog's `activity` section words each kind once. A step the
49
+ // table does not know is worded by its name.
50
+ export const ACTIVITY_KINDS = Object.freeze({ UR: 'draft', PRD: 'draft', SD: 'draft', TP: 'draft',
51
+ 'Brownfield Review': 'analysis', 'Brownfield Analysis': 'analysis', 'Mode/Slice Decision': 'route',
52
+ 'CD+Tests': 'build', 'Quick Task Execution': 'build', 'Verified Change Execution': 'build',
53
+ CR: 'review', QA: 'quality', UAT: 'acceptance', OR: 'closeout' });
54
+
55
+ // What a run is busy with (owner request of 2026-11-12: "läuft" named no activity). Read from the run's
56
+ // own state, a step Claude works on or builds, never from the turn overlay. While a main-loop turn runs,
57
+ // the line says what is being done and the block asks for a moment. Otherwise the run rests: the line
58
+ // says what is up next and the block how to carry on. Returns { label, turn, resting } or null.
59
+ export function activityOf(derived, run, isTurnActive, t, title) {
60
+ const { key, gate } = derived.reason;
61
+ const at = gate ?? run.gate;
62
+ if ((key !== 'working' && key !== 'building') || !at) return null;
63
+ const kind = ACTIVITY_KINDS[at] ?? 'other';
64
+ const named = title(at);
65
+ // A document or approval gate keeps its short name beside its title; an internal step has only a title.
66
+ const step = named !== at && /^[A-Z]{2,3}$/u.test(at) ? `${named} (${at})` : named;
67
+ const label = t(`activity.${kind}.${isTurnActive ? 'working' : 'idle'}`, { gate: at, step: named });
68
+ const drafting = kind === 'draft';
69
+ const turn = isTurnActive
70
+ ? { tone: 'claude', who: t('turn.moment'), what: drafting ? t('turn.drafting', { step }) : t('turn.workingOn', { step }), how: null, gate: at }
71
+ : { tone: 'next', who: t('turn.next'), what: drafting ? t('turn.draft', { step }) : step, how: t('turn.resume'), gate: at };
72
+ return { label, turn, resting: !isTurnActive };
73
+ }
74
+
75
+ // About two pane lines of the goal, cut at a word, so the block that asks for a move stays in sight.
76
+ const GOAL_LINES = 2;
77
+ export function goalLine(goal, columns) {
78
+ const room = Math.max(40, (Number.isFinite(columns) ? columns : 60) * GOAL_LINES);
79
+ if (goal.length <= room) return goal;
80
+ const cut = goal.slice(0, room - 1), at = cut.lastIndexOf(' ');
81
+ return `${(at > room / 2 ? cut.slice(0, at) : cut).replace(/[\s,;:.–-]+$/u, '')}…`;
82
+ }
83
+
84
+ // The delivery path and the position on it, e.g. "Strukturierte Lieferung · Schritt 2 von 8". Before
85
+ // the path is decided only that is said; a path that names no current entry gives no position.
86
+ export function routeLine(run, marks, t) {
87
+ const key = isStructured(run.route) ? 'structured' : run.route === 'quick_task' || run.route === 'verified_change' ? run.route
88
+ : (run.route === 'undecided' || run.route == null) && run.evaluated && run.lifecycle === 'active' ? 'undecided' : null;
89
+ if (!key) return null;
90
+ const at = marks.findIndex(({ mark }) => mark === 'current');
91
+ return key === 'undecided' || at < 0 ? t(`routes.${key}`) : `${t(`routes.${key}`)} · ${t('fields.step', { position: at + 1, count: marks.length })}`;
92
+ }
93
+
21
94
  // input: { snapshot, sessionStartedAt, selection, mention, isBrowsing, isTurnActive, registry, labels,
22
- // locale, columns, isObjectiveExpanded, events, eventsAvailable }. Display only: the adapter persists
95
+ // locale, columns, isInfoExpanded, events, eventsAvailable }. Display only: the adapter persists
23
96
  // selection changes. `events` are this root's journal events since the last main-loop turn, newest first.
24
97
  export function buildPanelView(input) {
25
98
  const { snapshot, registry, labels, locale } = input;
@@ -46,7 +119,12 @@ export function buildPanelView(input) {
46
119
  : unreadable(reason);
47
120
  }
48
121
  if (snapshot.status === 'no_control') {
49
- return neutral(t('messages.noControl', { folder: folderName(snapshot.root) }), { hint: t('messages.noControlHint', { path: snapshot.root ?? '' }) });
122
+ // A project without AGDF is invited to start, not told about a missing folder (owner request of
123
+ // 2026-11-12): the figure holds the start sign, and the checked path and the finding sit behind the (i).
124
+ return neutral(t('messages.notSetUp'), { hint: t('messages.notSetUpHint'), invite: true, idle: true,
125
+ isInfoExpanded: Boolean(input.isInfoExpanded), info: [
126
+ { label: t('fields.checkedFolder'), text: snapshot.root ?? '' },
127
+ { label: t('fields.finding'), text: t('messages.noControl', { folder: folderName(snapshot.root) }) }] });
50
128
  }
51
129
 
52
130
  const { run, origin, open, chatRunId } = assignRun({ runs: snapshot.runs, sessionStartedAt: input.sessionStartedAt, selection: input.selection,
@@ -55,31 +133,77 @@ export function buildPanelView(input) {
55
133
  const reasons = [...new Set(snapshot.unreadable.map(item => t(`unreadable.${item.reason}`)))].join(', ');
56
134
  const unreadableHint = snapshot.unreadable.length ? t('messages.unreadableRuns', { count: snapshot.unreadable.length, reasons }) : null;
57
135
  // Each open run carries its condensed gate bar for the list (owner request of 2026-10-28).
58
- const openRuns = open.map(item => ({ id: item.id, objective: item.objective, bar: condensedGateBar(gateMarks(item)) }));
136
+ const openRuns = open.map(item => ({ id: item.id, name: listedRunName(item, t), objective: item.objective, bar: condensedGateBar(gateMarks(item)) }));
59
137
  if (!run) {
60
138
  const broken = input.selection && snapshot.unreadable.find(item => item.id === input.selection);
61
139
  if (broken) return { ...unreadable(broken.reason), openRuns, chatRunId, unreadableHint };
62
- if (!open.length) return neutral(unreadableHint ?? t('messages.noOpenRuns'));
63
- return neutral(t('messages.noRunAssigned'), { hint: t('messages.noRunAssignedHint'), openRuns, chatRunId, unreadableHint,
140
+ // A readable project without an assigned run idles: the figure blinks but shows no run state.
141
+ if (!open.length) {
142
+ if (unreadableHint) return neutral(unreadableHint, { idle: false });
143
+ const runs = snapshot.runs ?? [];
144
+ // "All done" only when every run completed; superseded or abandoned runs did not finish.
145
+ if (!runs.length) return neutral(t('messages.noRunsYet'), { idle: true, hint: t('messages.noRunsYetHint') });
146
+ if (runs.every(item => item.lifecycle === 'completed')) return neutral(t('messages.allRunsDone'), { idle: true, allDone: true, hint: t('messages.allRunsDoneHint') });
147
+ return neutral(t('messages.noOpenRuns'), { idle: true });
148
+ }
149
+ return neutral(t('messages.noRunAssigned'), { idle: true, hint: t('messages.noRunAssignedHint'), openRuns, chatRunId, unreadableHint,
64
150
  openCount: t('fields.openRuns', { count: open.length }) });
65
151
  }
66
152
 
67
153
  const position = open.findIndex(item => item.id === run.id);
68
154
  const openCount = position >= 0 ? t('fields.position', { position: position + 1, count: open.length }) : t('fields.openRuns', { count: open.length });
69
- const state = displayState(run, { isTurnActive: input.isTurnActive });
155
+ const derived = runState(run);
156
+ const state = input.showRunState ? derived : displayState(run, { isTurnActive: input.isTurnActive });
70
157
  const reasonParams = { gate: gateTitle(registry, locale, state.reason.gate), decision: run.blockingText || state.reason.decision,
71
158
  reason: state.reason.reason ? t(`unreadable.${state.reason.reason}`) : '' };
159
+ // A gate by its localized title with its short name, e.g. "Produktanforderungen (PRD)".
160
+ const gateName = gate => { const title = gateTitle(registry, locale, gate); return title === gate ? gate : `${title} (${gate})`; };
161
+ const marks = gateMarks(run);
162
+ const name = runName(run, t);
163
+ const activity = activityOf(derived, run, Boolean(input.isTurnActive), t, gate => gateTitle(registry, locale, gate));
164
+ const turn = activity?.turn ?? turnOf(state, run, t, gateName);
165
+ // The goal in the display language; a goal that only repeats the name is not said twice.
166
+ const goal = run.goal || run.objective;
167
+ const objective = goal && goal !== name ? goal : '';
168
+ // What the gate in question decides; a blocked run names its block instead.
169
+ const rationale = turn && turn.tone !== 'halt' ? gateRationale(registry, locale, turn.gate) : null;
170
+ // A completed run names no next step, unless it is blocked: then the step says what to do about it.
171
+ const step = run.lifecycle === 'completed' && run.figure?.reason?.key !== 'blocked' ? null : run.nextText || null;
172
+ // For an approval the turn block already says what to do and how, so the next step only repeats it.
173
+ const stepRepeats = Boolean(step) && state.reason.key === 'waitingApproval' && Boolean(turn);
174
+ // The goal stands under the run's name, before whose move it is: it is what an approval is measured
175
+ // against (owner request of 2026-11-12). About two pane lines of it; the rest is behind the (i).
176
+ const goalShown = objective ? goalLine(objective, input.columns) : '';
177
+ // Details a person opens on purpose behind the (i): a goal that was cut, what the gate decides, and
178
+ // a next step that only repeats the approval. Closed by default.
179
+ const info = [
180
+ objective && goalShown !== objective ? { label: t('fields.objective'), text: objective } : null,
181
+ rationale ? { label: t('fields.gatePurpose'), text: rationale } : null,
182
+ stepRepeats ? { label: t('fields.nextStep'), text: step } : null,
183
+ ].filter(Boolean);
72
184
  return {
73
185
  ...base,
74
186
  ...events(run.id),
187
+ ...(input.showRunState ? { figure: state } : {}),
75
188
  mood: state.mood,
76
- stateLabel: t(`states.${state.mood}`),
189
+ workGate: input.showRunState && run.evaluated && !snapshot.stale ? run.gate : undefined,
190
+ // A working run names its activity; every other state keeps its word.
191
+ stateLabel: activity?.label ?? t(`states.${state.mood}`),
192
+ // The run rests: the step is up next and no main-loop turn works on it.
193
+ resting: activity?.resting === true,
77
194
  reason: t(`reasons.${state.reason.key}`, reasonParams),
78
- gateBar: gateBar(gateMarks(run), input.columns),
195
+ gateBar: gateBar(marks, input.columns),
79
196
  runId: run.id,
80
- objective: run.objective,
81
- isObjectiveExpanded: Boolean(input.isObjectiveExpanded),
82
- nextStep: run.lifecycle === 'completed' ? null : run.nextText || null,
197
+ name,
198
+ route: routeLine(run, marks, t),
199
+ turn,
200
+ rationale,
201
+ objective,
202
+ goal: goalShown,
203
+ info,
204
+ isInfoExpanded: Boolean(input.isInfoExpanded),
205
+ // The next step stays in sight where it says something the turn block does not.
206
+ nextStep: stepRepeats ? null : step,
83
207
  unreadableHint,
84
208
  origin: t(`origin.${origin}`),
85
209
  originKind: origin,
@@ -107,14 +231,18 @@ function eventFields(input, t, naming) {
107
231
  // Text fallback for surfaces without Svg: state, gate bar and next step on separate lines.
108
232
  export function terminalText(view, { labels, locale }) {
109
233
  const t = path => label(labels, locale, path);
110
- const lines = [view.title, `${FIGURE_TEXT[view.mood]} ${view.stateLabel}${view.reason ? ` · ${view.reason}` : ''}`];
234
+ // The turn line says what the reason would; without one the reason stays beside the state.
235
+ const lines = [view.title, `${view.invite ? INVITE_TEXT : FIGURE_TEXT[view.mood]} ${view.stateLabel}${view.reason && !view.turn ? ` · ${view.reason}` : ''}`];
111
236
  if (view.message) lines.push(view.message);
112
237
  if (view.hint) lines.push(view.hint);
113
238
  if (view.unreadableHint && view.unreadableHint !== view.message) lines.push(view.unreadableHint);
239
+ if (view.name) lines.push(view.route ? `${view.name} · ${view.route}` : view.name);
240
+ if (view.goal) lines.push(`${t('fields.objective')}: ${view.goal}`);
241
+ if (view.turn) lines.push(`${view.turn.who}: ${view.turn.what}${view.turn.how ? ` · ${view.turn.how}` : ''}`);
114
242
  if (view.gateBar) lines.push(view.gateBar);
115
- if (view.runId) lines.push(view.origin ? `${view.runId} · ${view.origin}` : view.runId);
116
- if (view.objective) lines.push(`${t('fields.objective')}: ${view.objective}`);
117
243
  if (view.nextStep) lines.push(`${t('fields.nextStep')}: ${view.nextStep}`);
244
+ // The (i) details appear only once opened; its button is drawn beside this text.
245
+ if (view.isInfoExpanded) for (const item of view.info ?? []) lines.push(`${item.label}: ${item.text}`);
118
246
  for (const warning of view.drift ?? []) lines.push(warning);
119
247
  if (view.events) lines.push(`${t('fields.events')}:`, ...view.events);
120
248
  if (view.review) {
@@ -123,6 +251,8 @@ export function terminalText(view, { labels, locale }) {
123
251
  if (view.review.preview) lines.push(...view.review.preview.split('\n').slice(0, TERMINAL_PREVIEW_LINES).map(line => ` ${line}`));
124
252
  }
125
253
  if (view.eventsHint) lines.push(view.eventsHint);
254
+ // The run id is a reference, so it stands after what a person reads.
255
+ if (view.runId) lines.push(view.origin ? `${view.runId} · ${view.origin}` : view.runId);
126
256
  if (view.stale) lines.push(view.stale);
127
257
  if (view.readAt) lines.push(view.readAt);
128
258
  return lines.join('\n');
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agdf/mcp-server",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "Local MCP adapter for version-matched AGDF dispatch and control inspection.",
5
5
  "type": "module",
6
6
  "files": [
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@modelcontextprotocol/server": "2.0.0",
39
- "create-agdf": "0.31.0"
39
+ "create-agdf": "0.32.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@modelcontextprotocol/client": "2.0.0"
@@ -15,7 +15,7 @@ export function startGenerationRuntime({ epoch, generation, channel }) {
15
15
  // Companion events (SD SDD-006): this generation executed the call, so it journals the final result
16
16
  // once, before the result leaves; a failing journal never changes or delays the result.
17
17
  function journal(name, args, projected) {
18
- try { runtime.observeOutcome?.(name, args, projected); } catch { /* notification only */ }
18
+ try { return runtime.observeOutcome?.(name, args, projected) === true; } catch { return false; /* notification only */ }
19
19
  }
20
20
  function send(input, kind, payload) {
21
21
  const reply = generationMessage(epoch, generation, kind, payload, {}, input.original_operation_id, input.request_id);
@@ -41,6 +41,7 @@ export function startGenerationRuntime({ epoch, generation, channel }) {
41
41
  runtime = await createMcpDecisionRuntime({ runtime, ui });
42
42
  }
43
43
  if (runtime.ui) uiDigest = loadCockpitResource(runtime.ui).digest;
44
+ if (runtime.mode === 'cockpit' && uiDigest) runtime.bindResourceIdentity({ digest: uiDigest, version: inspected.expectedVersion });
44
45
  // Inspect reports package-owned immutable identities, not caller evidence.
45
46
  sdkDigest = inspected.generationIdentity.sdk_digest; serverDigest = inspected.generationIdentity.server_digest;
46
47
  executor = runtime.mode === 'cockpit' ? null : createAgdfWorkerExecutor(runtime, { releaseOwnedLock: releaseAbandonedOwnedLock });
@@ -68,7 +69,7 @@ export function startGenerationRuntime({ epoch, generation, channel }) {
68
69
  ++inflight;
69
70
  const parentOwned = typeof tool.parentOwned === 'function' ? tool.parentOwned(args) : tool.parentOwned;
70
71
  const started = performance.now();
71
- let result, phase = parentOwned || !executor ? 'parse' : 'execute';
72
+ let result, journaled = false, phase = parentOwned || !executor ? 'parse' : 'execute';
72
73
  try {
73
74
  phase = 'parse';
74
75
  args = runtime.prepareInput?.(name, args) ?? args;
@@ -82,13 +83,14 @@ export function startGenerationRuntime({ epoch, generation, channel }) {
82
83
  if (name === 'agdf_inspect' && args.operation === 'operation-status' && result.report?.status === 'recorded'
83
84
  && (result.report.receipt?.operation_id ?? result.report.receipt?.operation?.operation_id) === args.module.split('/').at(-1)) unknown.delete(args.module.split('/').at(-1));
84
85
  const projected = runtime.projectToolResult(name, args, result);
85
- journal(name, args, projected);
86
+ journaled = journal(name, args, projected);
86
87
  send(input, 'result', projected);
87
88
  } catch (error) {
88
89
  const facts = runtime.normalizeFailure(error, { phase });
89
90
  if (input.original_operation_id && facts.dispatch !== 'not_dispatched') unknown.add(input.original_operation_id);
90
91
  const projected = runtime.projectToolResult(name, args, runtime.failure(facts.code, name, args, { ...facts, elapsed_ms: performance.now() - started }), facts);
91
- journal(name, args, projected);
92
+ // One call leaves at most one event: a failure after its result left an event adds none.
93
+ if (!journaled) journal(name, args, projected);
92
94
  send(input, 'result', projected);
93
95
  } finally { --inflight; }
94
96
  }
@@ -211,8 +211,9 @@ export async function createOwnedGenerationFacade({ runtime, publisher, createCo
211
211
  operations.set(key, row);
212
212
  }
213
213
  ++row.inflight; event(row, 'invoke', name);
214
+ let replied;
214
215
  try {
215
- const result = (await request(row, 'invoke', { name, arguments: args }, actualContext, canonical ? args.operation_id : null, invokeMs)).payload;
216
+ const result = replied = (await request(row, 'invoke', { name, arguments: args }, actualContext, canonical ? args.operation_id : null, invokeMs)).payload;
216
217
  if (canonical && result.outcome === 'effect_unknown') row.unknown.add(key);
217
218
  if (name === 'agdf_inspect' && args.operation === 'operation-status' && result.report?.status === 'recorded'
218
219
  && (result.report.receipt?.operation_id ?? result.report.receipt?.operation?.operation_id) === id) row.unknown.delete(key);
@@ -228,7 +229,12 @@ export async function createOwnedGenerationFacade({ runtime, publisher, createCo
228
229
  }
229
230
  }
230
231
  return result;
231
- } catch (error) { if (canonical && error.dispatch !== 'not_dispatched') row.unknown.add(key); throw error; }
232
+ } catch (error) {
233
+ // The generation journaled the reply it sent; a failure raised here afterwards carries it, so the
234
+ // server adds no second companion event for the same call.
235
+ if (replied && error && typeof error === 'object') error.generationResult = replied;
236
+ if (canonical && error.dispatch !== 'not_dispatched') row.unknown.add(key); throw error;
237
+ }
232
238
  finally { --row.inflight; if (closed && !row.inflight && !row.unknown.size) void retire(row, true); }
233
239
  }
234
240
  current = await reserve(publisher.initial, {}); current.state = 'current'; event(current, 'admitted');
@@ -41,7 +41,10 @@ export async function runMcpServer({ surface, cockpitDir, cockpitMode, runtime,
41
41
  runtime = thin ? await dispatcherRuntime.createMcpDecisionTransportRuntime({ runtime, ui })
42
42
  : await dispatcherRuntime.createMcpDecisionRuntime({ runtime, ui });
43
43
  }
44
- if (runtime.mode === 'cockpit') loadCockpitResource(runtime.ui);
44
+ if (runtime.mode === 'cockpit') {
45
+ const resource = loadCockpitResource(runtime.ui);
46
+ runtime.bindResourceIdentity?.({ digest: resource.digest, version: serverVersion });
47
+ }
45
48
  let transport;
46
49
  if (ownedPublication) {
47
50
  const { createOwnedGenerationFacade } = await import('./generation.js');