${escapeHtml(approval.title)}
${escapeHtml(requester?.title ?? approval.requester)} is waiting on ${escapeHtml(approver)}${refs ? ` · ${refs}` : ''}
Request details · ${escapeHtml(ageLabel(approval.createdAt))}
${escapeHtml(approval.summary)}
@@ -725,60 +725,38 @@ function renderFlowWatch(snapshot) {
: empty(approvalState?.reason ?? 'Structured approval requests are not available.'));
}
-function attentionItems(snapshot) {
- const items = [];
- const unresolvedDelivery = (snapshot.deliveryOperations ?? []).filter((operation) => ['running', 'unknown', 'succeeded'].includes(operation.state));
- if (unresolvedDelivery.length) items.push({ tone: 'urgent', title: `${unresolvedDelivery.length} delivery operation${unresolvedDelivery.length === 1 ? ' needs' : 's need'} review`,
- detail: 'Inspect recorded attempts and external state before owner-approved recovery. Do not automatically repeat effects.', href: '#operation-outcomes' });
- const neglected = (snapshot.backlogActivity?.tasks ?? []).filter((task) => task.status === 'stale' && task.ownerRequested);
- if (neglected.length) items.push({ tone: 'review', title: `${neglected.length} owner request${neglected.length === 1 ? ' needs' : 's need'} activity review`,
- detail: 'No named commit within the configured review window. Blocked work may be waiting as expected.', href: '#activity-review' });
- const unknownWakes = snapshot.managerWakes?.blockingCount ?? 0;
- if (unknownWakes) items.push({
- tone: 'urgent', title: `${unknownWakes} manager ${unknownWakes === 1 ? 'launch needs' : 'launches need'} inspection`,
- detail: 'An unknown launch blocks another wake. Inspect the runtime before owner-approved recovery; no automatic restart occurs.',
- href: '#manager-wakes',
- });
- const findings = snapshot.doctor?.findings ?? [];
- for (const finding of findings.filter((entry) => entry.severity === 'error' || entry.severity === 'warning').slice(0, 5)) {
- items.push({
- tone: finding.severity === 'error' ? 'urgent' : 'review',
- title: finding.areaId ? `${finding.areaId}: ${finding.code}` : finding.code,
- detail: finding.message ?? finding.recommendation ?? 'Doctor reported a project condition to review.',
- });
- }
- const awaiting = snapshot.messages?.unacknowledged ?? 0;
- if (awaiting) items.push({
- tone: 'review', title: `${awaiting} unacknowledged ${awaiting === 1 ? 'message' : 'messages'}`,
- detail: 'Open Conversations to inspect the latest durable inter-session handoffs.',
- href: '#communications',
- });
- const ownerApprovals = (snapshot.approvalRequests?.items ?? []).filter((approval) =>
- approval.status === 'pending' && approval.approver === 'owner');
- if (ownerApprovals.length) items.push({
- tone: 'decision', title: `${ownerApprovals.length} approval ${ownerApprovals.length === 1 ? 'request' : 'requests'} need your decision`,
- detail: 'The named owner is the approver; review the request evidence in Flow watch.', href: '#flow-watch',
- });
- const pendingChanges = (snapshot.fleetChanges ?? []).filter((change) => !['active', 'rejected'].includes(change.state));
- const pendingHierarchy = (snapshot.hierarchyProposals ?? []).filter((proposal) => proposal.state === 'proposed');
- if (pendingChanges.length + pendingHierarchy.length) items.push({
- tone: 'decision', title: `${pendingChanges.length + pendingHierarchy.length} organization ${pendingChanges.length + pendingHierarchy.length === 1 ? 'proposal' : 'proposals'}`,
- detail: 'Organization proposals wait for owner review; they do not change the active Fleet.',
- href: '#organization-proposals',
- });
- const waitingIntegration = (snapshot.integration ?? []).filter((item) => !['landed', 'rejected'].includes(item.state));
- if (waitingIntegration.length) items.push({
- tone: 'decision', title: `${waitingIntegration.length} integration ${waitingIntegration.length === 1 ? 'request' : 'requests'}`,
- detail: 'Review exact source commits and required checks before landing.', href: '#delivery',
- });
- const staleEvidence = (snapshot.backlogHealth?.findings ?? []).filter((finding) =>
- ['BACKLOG_OBSERVED_COMMIT_STALE', 'BACKLOG_OBSERVED_COMMIT_MISSING'].includes(finding.code));
- if (staleEvidence.length) items.push({
- tone: 'review', title: `${staleEvidence.length} task ${staleEvidence.length === 1 ? 'evidence record needs' : 'evidence records need'} refresh`,
- detail: 'Some backlog items were observed against an old or unreachable commit. Re-check them before dispatch.',
- href: '#backlog-board',
- });
- return items;
+function attentionGroups(snapshot) {
+ return globalThis.TorchAttentionProjection.groups(snapshot);
+}
+function renderAttention(groups) {
+ const definitions = [
+ ['owner', 'Waiting on you', 'Only requests that name the project owner as decision-maker.'],
+ ['fleet', 'Fleet handling', 'A named domain owns the next step; unknown ownership stays explicit.'],
+ ['arbiter', 'Arbiter handling', 'No observed item has structured evidence assigning its next step exclusively to an arbiter.'],
+ ];
+ const total = Object.values(groups).reduce((sum, items) => sum + items.length, 0);
+ setText('#attention-count', `${total} ${total === 1 ? 'item' : 'items'}`);
+ setHtml('#attention-list', definitions.map(([key, title, description]) => {
+ const items = groups[key];
+ return `
+ ${title}
${items.length}
+ ${items.length ? items.map((item) => `
+
+
${escapeHtml(item.title)}${escapeHtml(item.detail)}
+
Severity: ${escapeHtml(({ urgent: 'Urgent', review: 'Review', decision: 'Decision required', info: 'Progress' })[item.tone] ?? 'Unknown')} · Owner: ${escapeHtml(item.owner)}${item.waitSince ? ` · Waiting ${escapeHtml(ageLabel(item.waitSince))}` : ''}
+ ${item.evidence.length > 120
+ ? `
Evidence and references
${escapeHtml(item.evidence)}
`
+ : `
Evidence: ${escapeHtml(item.evidence)}`}
+
${escapeHtml(item.action)}
+ ${item.decisionApprovalId ? `
` : ''}
+ ${item.requestOwner ? `
Request review from ${escapeHtml(item.owner)}` : ''}
+
`).join('') : `${escapeHtml(description)}
`}
+ `;
+ }).join(''));
}
function render(snapshot) {
@@ -789,7 +767,7 @@ function render(snapshot) {
const active = tasks.filter((task) => !['completed', 'cancelled'].includes(task.state));
const working = active.filter((task) => task.state === 'in_progress').length;
const blocked = active.filter((task) => task.state === 'blocked').length;
- const attention = attentionItems(snapshot);
+ const attention = attentionGroups(snapshot);
const branch = snapshot.repository?.branch ?? 'branch not recorded';
setText('#project-name', project?.name ?? project?.id ?? 'Not installed');
@@ -807,8 +785,9 @@ function render(snapshot) {
const parts = [`${agents.length} persistent ${agents.length === 1 ? 'identity' : 'identities'}`,
`${working} ${working === 1 ? 'task' : 'tasks'} in progress`];
if (blocked) parts.push(`${blocked} blocked`);
+ const attentionCount = Object.values(attention).reduce((sum, items) => sum + items.length, 0);
setText('#project-summary', snapshot.mode === 'installed'
- ? `${parts.join(' · ')}. ${attention.length ? `${attention.length} item${attention.length === 1 ? '' : 's'} may need your review.` : 'No owner decision is currently waiting in the observed queues.'}`
+ ? `${parts.join(' · ')}. ${attentionCount ? `${attentionCount} item${attentionCount === 1 ? '' : 's'} need a named next step.` : 'No owner decision is currently waiting in the observed queues.'}`
: 'This repository has not been installed as a TORCH Fleet. The view is showing repository intake only.');
renderTaskCreateOptions(agents, tasks, snapshot.mode === 'installed');
renderOwnerRequestOptions(agents, tasks, snapshot.mode === 'installed');
@@ -816,13 +795,7 @@ function render(snapshot) {
renderFlowWatch(snapshot);
setHtml('#owner-briefing-content', globalThis.TorchOwnerDigest.render(snapshot.ownerDigest));
- setText('#attention-count', `${attention.length} ${attention.length === 1 ? 'item' : 'items'}`);
- setHtml('#attention-list', attention.length ? attention.map((item) => `
-
-
-
${escapeHtml(item.title)}${escapeHtml(item.detail)}
- ${item.href ? `
Open relevant view` : ''}
-
`).join('') : empty('No owner decisions are waiting in the observed queues.'));
+ renderAttention(attention);
renderBacklog(tasks, snapshot.backlogHealth?.findings ?? []);
setHtml('#activity-review-content', globalThis.TorchTaskActivity.render(snapshot.backlogActivity));
@@ -849,7 +822,7 @@ function render(snapshot) {
const refs = [message.task, message.path, message.commit].filter(Boolean);
const ack = (message.acknowledgedBy ?? []).length
? `Acknowledged by ${message.acknowledgedBy.join(', ')}` : 'Awaiting acknowledgement';
- return `${escapeHtml(message.sender)} to ${escapeHtml(message.recipient)}${escapeHtml(ageLabel(message.createdAt))}
${escapeHtml(message.body)}
${escapeHtml(ack)}${refs.length ? ` · ${escapeHtml(refs.join(' · '))}` : ''}`;
+ return `${escapeHtml(message.sender)} to ${escapeHtml(message.recipient)}${escapeHtml(ageLabel(message.createdAt))}
${escapeHtml(message.body)}
${escapeHtml(ack)}${refs.length ? ` · ${escapeHtml(refs.join(' · '))}` : ''}`;
}).join('') : empty('No durable messages yet. Runtime-native private reasoning is not shown here.'));
setHtml('#check-list', globalThis.TorchCheckEvidence.render(snapshot.checks));
@@ -906,7 +879,7 @@ function render(snapshot) {
const worktrees = snapshot.worktrees ?? [];
setHtml('#worktree-list', worktrees.length ? worktrees.map((worktree) => `
- ${escapeHtml(worktree.area)}${escapeHtml(worktree.branch)} · ${escapeHtml(worktree.path)}
${escapeHtml(`${worktree.ahead ?? 0} ahead / ${worktree.behind ?? 0} behind`)}
+ ${escapeHtml(worktree.area)}${escapeHtml(worktree.branch)} · ${escapeHtml(worktree.path)}
${escapeHtml(`${worktree.ahead ?? 0} ahead / ${worktree.behind ?? 0} behind`)}
`).join('') : empty('No managed worktrees have been created.'));
const holders = snapshot.resources?.holders ?? [];
@@ -1864,8 +1837,46 @@ $('#change-list')?.addEventListener('click', async (event) => {
});
$('#attention-list')?.addEventListener('click', (event) => {
+ const decisionButton = event.target.closest('[data-attention-decision]');
+ if (decisionButton) {
+ const card = decisionButton.closest('.attention-item');
+ const status = card?.querySelector('.attention-action-status');
+ const approvalId = decisionButton.dataset.attentionApproval;
+ const decision = decisionButton.dataset.attentionDecision;
+ const form = [...document.querySelectorAll('#approval-request-list .approval-decision-form')]
+ .find((candidate) => candidate.dataset.approvalId === approvalId);
+ const previewButton = form?.querySelector('[data-approval-preview]');
+ const shortcut = globalThis.TorchAttentionActions.prepareOwnerDecisionShortcut(form, decision, previewButton ? (approvalForm) => {
+ approvalForm.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ previewButton.click();
+ } : null);
+ if (!shortcut.started) {
+ if (status) {
+ status.textContent = shortcut.reason;
+ status.hidden = false;
+ }
+ return;
+ }
+ if (status) {
+ status.textContent = `Opening the guarded ${decision} preview for this owner-addressed request. Confirm only after reviewing its current evidence.`;
+ status.hidden = false;
+ }
+ return;
+ }
const link = event.target.closest('a[href^="#"]');
const target = link && document.getElementById(link.getAttribute('href').slice(1));
+ const recipient = link?.dataset.attentionRecipient;
+ if (recipient) {
+ const form = $('#owner-request-form');
+ const recipientSelect = form?.elements.namedItem('recipient');
+ const body = form?.elements.namedItem('body');
+ const status = form?.querySelector('.owner-request-status');
+ const hasRecipient = [...(recipientSelect?.options ?? [])].some((option) => option.value === recipient);
+ if (form && recipientSelect && body && hasRecipient && !recipientSelect.value && !body.value.trim()) {
+ recipientSelect.value = recipient;
+ if (status) status.textContent = `Selected ${recipient}. Add context and review the preview before sending anything.`;
+ }
+ }
for (let parent = target?.parentElement; parent; parent = parent.parentElement) {
if (parent.tagName === 'DETAILS') parent.open = true;
}
diff --git a/site/styles.css b/site/styles.css
index 0a87989..7ca7687 100644
--- a/site/styles.css
+++ b/site/styles.css
@@ -173,15 +173,30 @@ footer div { display: flex; gap: 1.5rem; }
.panel-heading { padding-bottom: .65rem; border-bottom: 1px solid var(--line); }
.panel-heading h2, .panel-heading h3 { margin: 0; font: 650 1rem/1.2 var(--display); }
.panel-heading span, .section-heading > span { color: var(--muted); font-size: .76rem; text-align: right; }
-.attention-list { display: grid; }
-.attention-item { display: grid; grid-template-columns: 9px 1fr; gap: .7rem; padding: .8rem 0; border-bottom: 1px solid var(--line); }
+.attention-list { display: grid; gap: 1rem; }
+.attention-group { min-width: 0; padding-top: .6rem; border-top: 1px solid var(--line); }
+.attention-group-heading { display: flex; align-items: baseline; justify-content: space-between; gap: .8rem; }
+.attention-group-heading h3 { margin: 0; font: 650 .9rem/1.25 var(--display); }
+.attention-group-heading span, .attention-empty { color: var(--muted); font-size: .72rem; }
+.attention-empty { margin: .35rem 0 0; line-height: 1.4; }
+.attention-item { display: grid; grid-template-columns: 9px minmax(0, 1fr); gap: .7rem; padding: .75rem 0; border-bottom: 1px solid var(--line); }
.attention-mark { width: 7px; height: 7px; margin-top: .45rem; background: var(--field); }
.tone-urgent .attention-mark { background: var(--signal); }
.tone-review .attention-mark { background: var(--route); }
.tone-decision .attention-mark { background: #f3bd55; }
+.tone-info .attention-mark { background: var(--field); }
+.attention-copy { min-width: 0; }
.attention-item strong { display: block; font-size: .86rem; overflow-wrap: anywhere; }
-.attention-item p { margin: .18rem 0; color: var(--muted); font-size: .78rem; line-height: 1.4; }
-.attention-item a { color: var(--route); font-size: .76rem; font-weight: 650; }
+.attention-item p { margin: .18rem 0 .35rem; color: var(--muted); font-size: .78rem; line-height: 1.4; }
+.attention-item small { display: block; color: var(--muted); font-size: .69rem; line-height: 1.35; overflow-wrap: anywhere; }
+.attention-item a { display: inline-block; margin: .35rem .8rem 0 0; color: var(--route); font-size: .76rem; font-weight: 650; }
+.attention-evidence { margin-top: .28rem; color: var(--muted); font-size: .69rem; }
+.attention-evidence summary { width: fit-content; cursor: pointer; color: var(--route); font-weight: 650; }
+.attention-evidence p { margin: .25rem 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }
+.attention-quick-actions { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; margin-top: .4rem; }
+.attention-quick-actions button { min-height: 2rem; padding: .3rem .55rem; }
+.attention-action-status { flex-basis: 100%; margin: .1rem 0 0; color: var(--muted); font-size: .7rem; line-height: 1.4; }
+.attention-action-status[hidden] { display: none; }
.board-section, .console-section { padding-block: 1.65rem; border-bottom: 1px solid var(--line); scroll-margin-top: 1rem; }
.section-heading { margin-bottom: 1rem; }
.section-heading h2 { margin: 0; }
diff --git a/src/console/server.mjs b/src/console/server.mjs
index bfaf17c..596a39c 100644
--- a/src/console/server.mjs
+++ b/src/console/server.mjs
@@ -28,6 +28,8 @@ const ROUTES = new Map([
['/check-evidence.js', 'check-evidence.js'],
['/task-activity.js', 'task-activity.js'],
['/operation-outcomes.js', 'operation-outcomes.js'],
+ ['/attention-projection.js', 'attention-projection.js'],
+ ['/attention-actions.js', 'attention-actions.js'],
['/live-refresh.js', 'live-refresh.js'],
]);
const TYPES = new Map([
diff --git a/src/observability/snapshot.mjs b/src/observability/snapshot.mjs
index b8ff51a..12ab6d0 100644
--- a/src/observability/snapshot.mjs
+++ b/src/observability/snapshot.mjs
@@ -138,7 +138,8 @@ function backlog(repositoryRoot, manifest) {
const task = json(join(root, name));
tasks.push({
id: task.id, title: task.title, description: task.description ?? '',
- state: task.state, owner: task.owner ?? null, priority: task.priority ?? null,
+ state: task.state, owner: task.owner ?? null, blockedReason: task.blockedReason ?? null,
+ priority: task.priority ?? null,
affectedDomains: task.affectedDomains ?? [], dependencies: task.dependencies ?? [],
feature: task.feature ?? null, milestone: task.milestone ?? null,
acceptanceCriteria: task.acceptanceCriteria ?? [], evidence: task.evidence ?? [],
diff --git a/test/fleet/console-attention.test.mjs b/test/fleet/console-attention.test.mjs
new file mode 100644
index 0000000..35b0377
--- /dev/null
+++ b/test/fleet/console-attention.test.mjs
@@ -0,0 +1,180 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import '../../site/attention-projection.js';
+import '../../site/attention-actions.js';
+import '../../site/live-refresh.js';
+
+const project = (snapshot) => globalThis.TorchAttentionProjection.groups(snapshot);
+
+test('SCN-console-attention-ownership: only owner-addressed pending decisions enter Waiting on you', () => {
+ const groups = project({
+ agents: [
+ { areaId: 'provider-runtime', title: 'Provider Runtime' },
+ { areaId: 'qa', title: 'Independent QA' },
+ ],
+ approvalRequests: { items: [
+ { id: 'owner-review', status: 'pending', approver: 'owner', requester: 'provider-runtime',
+ title: 'Review executor boundary', task: 'TASK-RUNTIME', evidence: 'receipt:17', createdAt: '2026-10-01T06:00:00Z', summary: 'Confirm the bounded scope.' },
+ { id: 'qa-review', status: 'pending', approver: 'qa', requester: 'owner-console',
+ title: 'Confirm scenario path', summary: 'Review the owned test path.' },
+ ] },
+ });
+
+ assert.deepEqual(groups.owner.map((item) => item.title), ['Review executor boundary']);
+ assert.equal(groups.owner[0].href, '#approval-owner-review');
+ assert.equal(groups.owner[0].decisionApprovalId, 'owner-review');
+ assert.match(groups.owner[0].detail, /bounded scope/);
+ assert.equal(groups.owner[0].waitSince, '2026-10-01T06:00:00Z');
+ assert.equal(groups.fleet[0].owner, 'Independent QA');
+ assert.equal(groups.fleet[0].decisionApprovalId, null);
+ assert.equal(groups.fleet[0].action, 'Open approval details');
+ assert.doesNotMatch(JSON.stringify(groups.fleet), /Approve|Reject/);
+});
+
+test('SCN-console-attention-owner-shortcut: a shortcut selects only an owner decision and retains the preview-confirm boundary', () => {
+ const choice = { value: '', disabled: false };
+ const form = { dataset: {}, elements: { namedItem: () => choice } };
+ let previewCalls = 0;
+ const result = globalThis.TorchAttentionActions.prepareOwnerDecisionShortcut(form, 'approved', () => { previewCalls += 1; });
+
+ assert.equal(result.started, true);
+ assert.equal(choice.value, 'approved');
+ assert.equal(previewCalls, 1, 'the caller can only start the established preview handler');
+
+ const nonOwnerForm = { dataset: {}, elements: { namedItem: () => ({ value: '', disabled: false }) } };
+ const noOwnerShortcut = globalThis.TorchAttentionActions.prepareOwnerDecisionShortcut(nonOwnerForm, 'published', () => { previewCalls += 1; });
+ assert.equal(noOwnerShortcut.started, false);
+ assert.equal(nonOwnerForm.elements.namedItem().value, '');
+});
+
+test('SCN-console-attention-shortcut-drafts: missing forms, active previews, and opposite drafts remain unchanged', () => {
+ const missing = globalThis.TorchAttentionActions.prepareOwnerDecisionShortcut(null, 'rejected');
+ assert.equal(missing.started, false);
+ assert.match(missing.reason, /no longer available/);
+
+ const previewForm = { dataset: { previewToken: 'current-preview' },
+ elements: { namedItem: () => ({ value: 'approved', disabled: true }) } };
+ const preview = globalThis.TorchAttentionActions.prepareOwnerDecisionShortcut(previewForm, 'rejected');
+ assert.equal(preview.started, false);
+ assert.equal(previewForm.elements.namedItem().value, 'approved');
+
+ const draftChoice = { value: 'approved', disabled: false };
+ const draftForm = { dataset: {}, elements: { namedItem: () => draftChoice } };
+ const draft = globalThis.TorchAttentionActions.prepareOwnerDecisionShortcut(draftForm, 'rejected');
+ assert.equal(draft.started, false);
+ assert.equal(draftChoice.value, 'approved');
+ assert.match(draft.reason, /draft is preserved/);
+});
+
+test('SCN-console-attention-refresh-draft: an edited owner decision remains protected while newer evidence renders', () => {
+ const choice = { value: '', checked: false, tagName: 'SELECT', selectedOptions: [] };
+ const note = { value: '', checked: false, tagName: 'TEXTAREA' };
+ const fields = [choice, note];
+ const form = {
+ contains: () => false,
+ matches: () => false,
+ querySelector: () => null,
+ querySelectorAll: () => fields,
+ };
+ const document = { activeElement: null, querySelectorAll: () => fields };
+ const protection = globalThis.TorchLiveRefresh.createProtection(document);
+ protection.remember();
+ choice.value = 'rejected';
+ note.value = 'Need the exact impact first.';
+
+ assert.equal(protection.protects(form), true);
+ const dirty = protection.dirty();
+ protection.remember({ preserve: dirty });
+ assert.equal(protection.protects(form), true);
+ assert.equal(choice.value, 'rejected');
+ assert.equal(note.value, 'Need the exact impact first.');
+});
+
+test('SCN-console-attention-dedup: doctor and snapshot message counts become one honest queue item', () => {
+ const groups = project({
+ messages: { unacknowledged: 4 },
+ doctor: { findings: [
+ { severity: 'warning', code: 'MESSAGE_BACKLOG', unacknowledged: 3 },
+ { severity: 'warning', code: 'MESSAGE_BACKLOG', unacknowledged: 3 },
+ ] },
+ });
+
+ const queue = groups.fleet.filter((item) => item.href === '#communications');
+ assert.equal(queue.length, 1);
+ assert.equal(queue[0].title, '4 durable messages awaiting acknowledgement');
+ assert.match(queue[0].detail, /Each named recipient owns their acknowledgement/);
+});
+
+test('SCN-console-attention-worktrees: findings group by owning area and distinguish retained commits from unsafe operations', () => {
+ const groups = project({
+ agents: [{ areaId: 'provider-runtime', title: 'Provider Runtime' }],
+ doctor: { findings: [
+ { severity: 'warning', code: 'WORKTREE_PROBLEM', area: 'provider-runtime', path: '/work/provider', problem: 'unique-commits:2' },
+ { severity: 'warning', code: 'WORKTREE_PROBLEM', areaId: 'provider-runtime', path: '/work/provider', problem: 'worktree-dirty' },
+ { severity: 'warning', code: 'WORKTREE_PROBLEM', area: 'project-kernel', path: '/work/kernel', problem: 'unique-commits:1' },
+ { severity: 'error', code: 'WORKTREE_PROBLEM', area: 'qa', path: '/work/qa', problem: 'git-operation:REBASE_HEAD' },
+ ] },
+ worktrees: [
+ { area: 'provider-runtime', branch: 'runtime/fix', path: '/work/provider', ahead: 2, behind: 0 },
+ { area: 'project-kernel', branch: 'kernel/change', path: '/work/kernel', ahead: 1, behind: 0 },
+ { area: 'qa', branch: 'qa/audit', path: '/work/qa', ahead: 0, behind: 0 },
+ ],
+ });
+
+ assert.equal(groups.fleet.length, 3);
+ const runtime = groups.fleet.find((item) => item.owner === 'Provider Runtime');
+ assert.equal(runtime.tone, 'review');
+ assert.match(runtime.detail, /Ahead commits are retained work progress, not a broken repository/);
+ assert.match(runtime.evidence, /runtime\/fix · \/work\/provider/);
+ assert.equal(runtime.requestOwner, 'provider-runtime');
+ const kernel = groups.fleet.find((item) => item.owner === 'project-kernel');
+ assert.equal(kernel.tone, 'info');
+ assert.match(kernel.detail, /owning specialist or manager/);
+ const qa = groups.fleet.find((item) => item.owner === 'qa');
+ assert.equal(qa.tone, 'urgent');
+ assert.match(qa.title, /worktree needs review/);
+});
+
+test('SCN-console-attention-wait-reasons: recovery and blocked task evidence keep consequences and actual reason visible', () => {
+ const groups = project({
+ agents: [{ areaId: 'release-self-host', title: 'Release and Self-host' }],
+ doctor: { findings: [{ severity: 'warning', code: 'RECOVERABILITY', level: 'ONE-DISK',
+ offMachine: false, commit: 'abcdef0123456789', recommendation: 'Configure and synchronize an off-machine canonical Git boundary.' }] },
+ backlog: [{ id: 'TASK-BLOCKED', title: 'Capture mobile UI', state: 'blocked', owner: 'owner-console',
+ blockedReason: 'Browser CDP timed out; no image was saved.' }],
+ });
+
+ const recovery = groups.fleet.find((item) => item.title === 'Off-machine recovery copy is not verified');
+ assert.equal(recovery.owner, 'Responsible domain not recorded');
+ assert.match(recovery.detail, /single disk failure could remove/);
+ assert.match(recovery.evidence, /ONE-DISK · abcdef012/);
+ assert.match(groups.fleet.find((item) => item.evidence === 'TASK-BLOCKED').detail, /no image was saved/);
+});
+
+test('SCN-console-attention-authority: roster titles name owners and integration stays with Fleet unless arbiter authority is explicit', () => {
+ const groups = project({
+ agents: [{ areaId: 'coordinator-7', title: 'Delivery Coordinator' }],
+ integration: [
+ { state: 'awaiting-checks', sourceArea: 'build-team', sourceCommit: '1234567890abcdef', authorizedBy: 'coordinator-7' },
+ { state: 'queued', sourceArea: 'docs-team', sourceCommit: 'abcdef0123456789' },
+ ],
+ managerWakes: { blockingCount: 1, reservations: [{ managerId: 'coordinator-7', scheduleId: 'nightly-review',
+ outcome: 'reserved', blocksManagerWake: true }] },
+ });
+
+ assert.equal(groups.fleet.length, 3);
+ assert.equal(groups.fleet[0].owner, 'Delivery Coordinator');
+ assert.match(groups.fleet[0].evidence, /authorized by Delivery Coordinator/);
+ assert.equal(groups.fleet[1].owner, 'Integration authority not recorded');
+ assert.equal(groups.fleet[2].owner, 'Delivery Coordinator');
+ assert.deepEqual(groups.arbiter, []);
+});
+
+test('SCN-console-attention-empty: an unavailable signal does not manufacture owner work', () => {
+ const groups = project({
+ approvalRequests: { available: false, reason: 'Structured approval service unavailable.' },
+ messages: { unacknowledged: 0 }, doctor: { findings: [] },
+ });
+
+ assert.deepEqual(groups, { owner: [], fleet: [], arbiter: [] });
+});
diff --git a/test/fleet/product-surface.test.mjs b/test/fleet/product-surface.test.mjs
index 78cb14a..eb37b42 100644
--- a/test/fleet/product-surface.test.mjs
+++ b/test/fleet/product-surface.test.mjs
@@ -30,6 +30,7 @@ test('SCN-product-site / SCN-dashboard-demo / SCN-torch-brand-surface: public si
const html = readFileSync(new URL('../../site/index.html', import.meta.url), 'utf8');
const demoHtml = readFileSync(new URL('../../site/demo.html', import.meta.url), 'utf8');
const consoleHtml = readFileSync(new URL('../../site/console.html', import.meta.url), 'utf8');
+ const consoleScript = readFileSync(new URL('../../site/console.js', import.meta.url), 'utf8');
const styles = readFileSync(new URL('../../site/styles.css', import.meta.url), 'utf8');
const torchMark = readFileSync(new URL('../../site/torch-mark.svg', import.meta.url), 'utf8');
assert.match(html, /TORCH — an AI fleet shaped by your project<\/title>/);
@@ -70,6 +71,9 @@ test('SCN-product-site / SCN-dashboard-demo / SCN-torch-brand-surface: public si
assert.match(consoleHtml, /console-rail/);
assert.match(consoleHtml, /id="backlog-board"/);
assert.match(consoleHtml, /id="attention-list"/);
+ assert.match(consoleHtml, /src="\/attention-actions\.js" defer/);
+ assert.match(consoleScript, /data-attention-decision/);
+ assert.match(consoleScript, /data-approval-preview/);
assert.match(consoleHtml, /id="artifact-list"/);
assert.match(consoleHtml, /id="organization-map"/);
assert.match(consoleHtml, /id="organization-lifecycle"/);
@@ -93,7 +97,6 @@ test('SCN-product-site / SCN-dashboard-demo / SCN-torch-brand-surface: public si
assert.match(consoleHtml, /Create a task proposal/);
assert.match(consoleHtml, /Session Manager still owns triage and assignment/);
assert.match(consoleHtml, /gates remain separate/);
- const consoleScript = readFileSync(new URL('../../site/console.js', import.meta.url), 'utf8');
assert.match(consoleScript, /artifact-feedback/);
assert.match(consoleScript, /feedback\/preview/);
assert.match(consoleScript, /data-feedback-confirm/);