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.
- package/generated/.opencode/agdf-interaction-locales.json +148 -4
- package/generated/plugins/agdf/.claude-plugin/plugin.json +1 -1
- package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
- package/generated/plugins/agdf/companion/events.mjs +5 -3
- package/generated/plugins/agdf/companion/figure.mjs +241 -110
- package/generated/plugins/agdf/companion/localization.mjs +5 -0
- package/generated/plugins/agdf/companion/notice.mjs +10 -2
- package/generated/plugins/agdf/companion/register.mjs +90 -32
- package/generated/plugins/agdf/companion/run-state.mjs +13 -1
- package/generated/plugins/agdf/companion/view.mjs +149 -19
- package/generated/plugins/agdf/mcp/server/package.json +2 -2
- package/generated/plugins/agdf/mcp/server/src/generation-runtime.js +6 -4
- package/generated/plugins/agdf/mcp/server/src/generation.js +8 -2
- package/generated/plugins/agdf/mcp/server/src/main.js +4 -1
- package/generated/plugins/agdf/mcp/server/src/server.js +19 -5
- package/generated/plugins/agdf/mcp/server/ui/cockpit.html +130 -33
- package/generated/plugins/agdf/mcp/server/ui/decision-manifest.json +3 -3
- package/generated/plugins/agdf/mcp/server/ui/decision.html +3 -3
- package/generated/plugins/agdf/mcp/server/ui/manifest.json +3 -3
- package/generated/plugins/agdf/meta/agdf-interaction-locales.json +148 -4
- package/generated/plugins/agdf/meta/agdf-mcp-capability.json +1 -1
- package/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
- package/generated/plugins/agdf/meta/copilot-payload-baseline.json +3 -3
- package/generated/plugins/agdf/meta/distribution-profile-history.json +7 -0
- package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-interaction-locales.json +148 -4
- package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
- package/generated/plugins/agdf/runtime/create-agdf/lib/mcp-dispatch-runtime.js +4 -1
- package/generated/plugins/agdf/runtime/create-agdf/lib/mcp-worker-runtime.js +77 -77
- package/generated/plugins/agdf/runtime/create-agdf/package.json +1 -1
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/companion/events.js +4 -1
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/companion/figure-state.js +6 -4
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/companion/journal.js +6 -0
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/companion/run-view.js +54 -11
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-evaluation/decision-content-placeholder.js +4 -1
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-evaluation/gate-check.js +5 -1
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-inspect/cockpit-list.js +10 -8
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-inspect/cockpit-session.js +11 -3
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-inspect/cockpit-work.js +4 -2
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-inspect/cockpit.js +43 -13
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-inspect/work-situation.js +25 -0
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-read/cockpit-worker.js +1 -0
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-create-command.js +6 -1
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-inventory-reader.js +50 -3
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-inventory.js +12 -27
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-presentation-render.js +29 -0
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-title.js +43 -0
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/interaction-catalog-validation.js +2 -1
- package/generated/plugins/agdf/runtime/create-agdf/runtime/core/lib/interaction-presentation.js +5 -9
- package/generated/plugins/agdf/runtime/runtime-manifest.json +2 -2
- package/generated/plugins/agdf/submission/openai/availability.md +1 -1
- package/generated/plugins/agdf/submission/openai/capability-matrix.json +1 -1
- package/generated/plugins/agdf/submission/openai/release-notes.md +5 -8
- package/generated/plugins/agdf/submission/openai/reviewer-cases.json +1 -1
- package/generated/plugins/agdf-planning/.codex-plugin/plugin.json +1 -1
- package/generated/plugins/agdf-planning/plugin.json +1 -1
- package/generated/plugins/copilot/agdf/.agdf-payload-inventory.json +76 -58
- package/generated/plugins/copilot/agdf/mcp/server/package.json +2 -2
- package/generated/plugins/copilot/agdf/mcp/server/src/generation-runtime.js +6 -4
- package/generated/plugins/copilot/agdf/mcp/server/src/generation.js +8 -2
- package/generated/plugins/copilot/agdf/mcp/server/src/main.js +4 -1
- package/generated/plugins/copilot/agdf/mcp/server/src/server.js +19 -5
- package/generated/plugins/copilot/agdf/meta/agdf-plugin.definition.json +1 -1
- package/generated/plugins/copilot/agdf/meta/distribution-profile-history.json +7 -0
- package/generated/plugins/copilot/agdf/plugin.json +1 -1
- package/generated/plugins/copilot/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-interaction-locales.json +148 -4
- package/generated/plugins/copilot/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
- package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/mcp-dispatch-runtime.js +4 -1
- package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/mcp-worker-runtime.js +62 -62
- package/generated/plugins/copilot/agdf/runtime/create-agdf/package.json +1 -1
- package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/companion/events.js +4 -1
- package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/companion/figure-state.js +6 -4
- package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/companion/journal.js +6 -0
- package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/companion/run-view.js +54 -11
- package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/control-evaluation/decision-content-placeholder.js +4 -1
- package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/control-evaluation/gate-check.js +5 -1
- package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/control-inspect/work-situation.js +25 -0
- package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-create-command.js +6 -1
- package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-inventory-reader.js +50 -3
- package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-inventory.js +12 -27
- package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-presentation-render.js +29 -0
- package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/control-state/run-title.js +43 -0
- package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/interaction-catalog-validation.js +2 -1
- package/generated/plugins/copilot/agdf/runtime/create-agdf/runtime/core/lib/interaction-presentation.js +5 -9
- package/generated/plugins/copilot/agdf/runtime/runtime-manifest.json +2 -2
- package/generated/plugins/copilot/agdf/skills/agdf-interaction-locales.json +1 -1
- package/generated/submissions/openai/agdf/.codex-plugin/plugin.json +1 -1
- package/generated/submissions/openai/agdf/meta/agdf-interaction-locales.json +148 -4
- package/generated/submissions/openai/agdf/meta/agdf-plugin.definition.json +1 -1
- package/generated/submissions/openai/agdf/meta/copilot-payload-baseline.json +3 -3
- package/generated/submissions/openai/agdf/meta/distribution-profile-history.json +7 -0
- package/generated/submissions/openai/agdf/plugin.json +1 -1
- package/generated/submissions/openai/agdf/submission/openai/availability.md +1 -1
- package/generated/submissions/openai/agdf/submission/openai/capability-matrix.json +1 -1
- package/generated/submissions/openai/agdf/submission/openai/inventory.json +13 -13
- package/generated/submissions/openai/agdf/submission/openai/listing.json +1 -1
- package/generated/submissions/openai/agdf/submission/openai/readiness.json +2 -2
- package/generated/submissions/openai/agdf/submission/openai/readiness.md +2 -2
- package/generated/submissions/openai/agdf/submission/openai/release-notes.md +5 -8
- package/generated/submissions/openai/agdf/submission/openai/reviewer-cases.json +1 -1
- package/lib/mcp-dispatch-runtime.js +4 -1
- package/lib/mcp-worker-runtime.js +77 -77
- package/package.json +1 -1
- package/runtime/core/lib/companion/events.js +4 -1
- package/runtime/core/lib/companion/figure-state.js +6 -4
- package/runtime/core/lib/companion/journal.js +6 -0
- package/runtime/core/lib/companion/run-view.js +54 -11
- package/runtime/core/lib/control-evaluation/decision-content-placeholder.js +4 -1
- package/runtime/core/lib/control-evaluation/gate-check.js +5 -1
- package/runtime/core/lib/control-inspect/cockpit-list.js +10 -8
- package/runtime/core/lib/control-inspect/cockpit-session.js +11 -3
- package/runtime/core/lib/control-inspect/cockpit-work.js +4 -2
- package/runtime/core/lib/control-inspect/cockpit.js +43 -13
- package/runtime/core/lib/control-inspect/work-situation.js +25 -0
- package/runtime/core/lib/control-read/cockpit-worker.js +1 -0
- package/runtime/core/lib/control-state/run-create-command.js +6 -1
- package/runtime/core/lib/control-state/run-inventory-reader.js +50 -3
- package/runtime/core/lib/control-state/run-inventory.js +12 -27
- package/runtime/core/lib/control-state/run-presentation-render.js +29 -0
- package/runtime/core/lib/control-state/run-title.js +43 -0
- package/runtime/core/lib/interaction-catalog-validation.js +2 -1
- 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,
|
|
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
|
|
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
|
|
184
|
-
//
|
|
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)
|
|
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
|
|
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:
|
|
447
|
-
//
|
|
448
|
-
//
|
|
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:
|
|
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
|
|
480
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
195
|
+
gateBar: gateBar(marks, input.columns),
|
|
79
196
|
runId: run.id,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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) {
|
|
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')
|
|
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');
|