From e21733295383375ffc817b1d26756a2e542017a1 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 1 Oct 2026 03:24:17 -0400 Subject: [PATCH] TASK-console-attention-actions: clarify attention ownership and actions --- ...e-attention-integrity-report-2026-10-01.md | 46 ++++++ site/attention-projection.js | 150 ++++++++++++++++++ site/console.html | 1 + site/console.js | 109 +++++-------- site/styles.css | 16 +- src/observability/snapshot.mjs | 3 +- test/fleet/console-attention.test.mjs | 92 +++++++++++ 7 files changed, 346 insertions(+), 71 deletions(-) create mode 100644 reports/test-audit/console-attention-integrity-report-2026-10-01.md create mode 100644 site/attention-projection.js create mode 100644 test/fleet/console-attention.test.mjs diff --git a/reports/test-audit/console-attention-integrity-report-2026-10-01.md b/reports/test-audit/console-attention-integrity-report-2026-10-01.md new file mode 100644 index 0000000..390e0f5 --- /dev/null +++ b/reports/test-audit/console-attention-integrity-report-2026-10-01.md @@ -0,0 +1,46 @@ +# Console attention test integrity + +```yaml +test_integrity_note: + change_type: ["new_tests"] + scenarios: + - id: SCN-console-attention-ownership + given: "Pending approvals name either the project owner or another approver" + when: "The Console projects attention groups" + then: "Only owner-addressed decisions appear under Waiting on you; peer approvals route to their named approver without approve or reject actions" + - id: SCN-console-attention-dedup + given: "Doctor and snapshot evidence describe the same unacknowledged message queue" + when: "Attention items are projected" + then: "One aggregate message item appears with the higher observed count and recipient-owned acknowledgement guidance" + - id: SCN-console-attention-worktrees + given: "A worktree has unique commits, local changes, or an active Git operation" + when: "Attention items are projected" + then: "Findings group by identity and branch/path; retained commits remain labeled as progress while unsafe operations remain review findings" + - id: SCN-console-attention-wait-reasons + given: "A task is blocked and repository recovery has no off-machine copy" + when: "The Console projects attention" + then: "The recorded block reason and single-disk recovery consequence remain visible with the responsible domain" + - id: SCN-console-attention-empty + given: "Approval evidence is unavailable and no queue or doctor finding is observed" + when: "Attention items are projected" + then: "No synthetic owner decision or operational issue is created" + observable_outcomes: + - "Attention groups distinguish owner decisions, Fleet work, and arbiter work" + - "Approval action ownership follows the named approver" + - "Repeated message queue evidence produces one item" + - "Worktree progress and unsafe Git state have distinct descriptions" + - "Unknown evidence stays explicit rather than gaining an invented owner" + determinism_controls: + - "Tests pass fixed in-memory snapshots; no clock, network, or filesystem is consulted" + anti_cheat_rationale: + prevents: + - "hard-coded return value" + - "over-mocking internal logic" + - "snapshot rubber-stamping" + - "retry/sleep-based flake masking" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +The focused attention scenarios passed. The existing owner approval scenario did not start because this worktree lacks the installed `zod` dependency; no assertion was changed or skipped in its source. diff --git a/site/attention-projection.js b/site/attention-projection.js new file mode 100644 index 0000000..77618f6 --- /dev/null +++ b/site/attention-projection.js @@ -0,0 +1,150 @@ +(function (global) { + const shortCommit = (value) => value ? String(value).slice(0, 9) : '—'; +const ownerDomainNames = { + 'release-self-host': 'Release and Self-host', + 'work-integration': 'Work and Integration', + 'project-kernel': 'Project Kernel', + 'session-manager': 'Session Manager', +}; + +function worktreeProblemLabel(problem) { + if (problem === 'worktree-dirty') return 'Uncommitted changes are present'; + if (problem === 'worktree-missing') return 'The managed worktree is missing'; + if (problem.startsWith('unique-commits:')) return `${problem.split(':')[1]} unique commit(s) are ahead`; + if (problem.startsWith('git-operation:')) return `Git operation needs resolution (${problem.slice('git-operation:'.length)})`; + if (problem.startsWith('branch-mismatch:')) return `Branch differs from its manifest (${problem.slice('branch-mismatch:'.length)})`; + return problem.replaceAll('-', ' '); +} + +function attentionGroups(snapshot) { + const groups = { owner: [], fleet: [], arbiter: [] }; + const agents = new Map((snapshot.agents ?? []).map((agent) => [agent.areaId, agent])); + const agentName = (id) => ownerDomainNames[id] ?? agents.get(id)?.title ?? id ?? 'Owner not recorded'; + const add = (group, item) => groups[group].push(item); + const approvals = (snapshot.approvalRequests?.items ?? []).filter((approval) => approval.status === 'pending'); + + for (const approval of approvals) { + const isOwner = approval.approver === 'owner'; + add(isOwner ? 'owner' : 'fleet', { + tone: isOwner ? 'decision' : 'review', + title: approval.title || 'Approval request', + owner: isOwner ? 'Project owner' : agentName(approval.approver), + detail: `${agentName(approval.requester)} is waiting on ${isOwner ? 'your decision' : agentName(approval.approver)}. ${approval.summary || 'Request consequence and options are not recorded.'}`, + evidence: [approval.task, approval.evidence].filter(Boolean).join(' · ') || 'Request evidence not recorded', + href: `#approval-${encodeURIComponent(approval.id)}`, + action: isOwner ? 'Review request and evidence' : 'Open approval details', + }); + } + + const pendingChanges = (snapshot.fleetChanges ?? []).filter((change) => change.state === 'proposed'); + const pendingHierarchy = (snapshot.hierarchyProposals ?? []).filter((proposal) => proposal.state === 'proposed'); + for (const proposal of [...pendingChanges, ...pendingHierarchy]) add('owner', { + tone: 'decision', title: proposal.title ?? proposal.proposal?.title ?? 'Organization proposal', + owner: 'Project owner', detail: `${proposal.rationale ?? proposal.proposal?.impact ?? 'The requested consequence is not recorded.'} This proposal does not change the active Fleet without a separate decision.`, + evidence: proposal.id, href: '#organization-proposals', action: 'Review proposal', + }); + + const findings = snapshot.doctor?.findings ?? []; + const messageFinding = findings.find((finding) => finding.code === 'MESSAGE_BACKLOG'); + const awaiting = Math.max(Number(snapshot.messages?.unacknowledged) || 0, Number(messageFinding?.unacknowledged) || 0); + if (awaiting) add('fleet', { + tone: 'review', title: `${awaiting} durable message${awaiting === 1 ? '' : 's'} awaiting acknowledgement`, + owner: 'Named recipients', detail: 'This aggregate covers the Fleet. Each named recipient owns their acknowledgement; other identities should not acknowledge it.', + evidence: 'Unread queues are summarized once; individual message ownership remains visible in Conversations.', + href: '#communications', action: 'Open Conversations', + }); + + const worktreeFindings = findings.filter((finding) => finding.code === 'WORKTREE_PROBLEM'); + const groupedWorktrees = new Map(); + for (const finding of worktreeFindings) { + const key = finding.area ?? finding.areaId ?? 'unknown-owner'; + const group = groupedWorktrees.get(key) ?? { area: key, path: finding.path, problems: [] }; + group.path ??= finding.path; + group.problems.push(finding.problem ?? finding.message ?? 'Worktree condition is not recorded'); + groupedWorktrees.set(key, group); + } + for (const item of groupedWorktrees.values()) { + const worktree = (snapshot.worktrees ?? []).find((entry) => entry.area === item.area); + const hasAhead = item.problems.some((problem) => problem.startsWith('unique-commits:')); + const hasUnsafeProblem = item.problems.some((problem) => !problem.startsWith('unique-commits:')); + const labels = item.problems.map(worktreeProblemLabel); + const identity = agentName(item.area); + add('fleet', { + tone: !hasUnsafeProblem ? 'info' : item.problems.some((problem) => problem.startsWith('git-operation:') || problem === 'worktree-missing') ? 'urgent' : 'review', + title: !hasUnsafeProblem ? `${identity} has work ahead of main` : `${identity} worktree needs review`, + owner: identity, + detail: `${labels.join('; ')}.${hasAhead ? ' Ahead commits are retained work progress, not a broken repository.' : ''} ${hasUnsafeProblem ? 'Review the exact worktree before deciding on recovery.' : 'Ask the owner to review a safe convergence plan.'}`, + evidence: [worktree?.branch, worktree?.path ?? item.path, worktree?.ahead != null ? `${worktree.ahead} ahead / ${worktree.behind ?? 0} behind` : null].filter(Boolean).join(' · ') || 'Branch and path not recorded', + href: `#worktree-${encodeURIComponent(item.area)}`, action: 'Inspect this worktree', + requestOwner: item.area !== 'unknown-owner' ? item.area : null, + }); + } + + for (const finding of findings.filter((entry) => ['error', 'warning'].includes(entry.severity) + && !['MESSAGE_BACKLOG', 'WORKTREE_PROBLEM'].includes(entry.code))) { + const recoveryGap = finding.code === 'RECOVERABILITY' && !finding.offMachine; + const humanTitle = recoveryGap + ? 'Off-machine recovery copy is not verified' + : finding.code.replaceAll('_', ' ').toLowerCase().replace(/^./, (letter) => letter.toUpperCase()); + add('fleet', { + tone: finding.severity === 'error' ? 'urgent' : 'review', title: humanTitle, + owner: recoveryGap ? 'Release and Self-host' : finding.area ? agentName(finding.area) : 'Responsible domain not recorded', + detail: recoveryGap + ? `${finding.recommendation ?? 'Configure and synchronize an off-machine canonical Git boundary.'} A single disk failure could remove the only recorded recovery copy.` + : finding.message ?? finding.recommendation ?? 'Review this condition with the responsible domain; its consequence is not recorded.', + evidence: [finding.level, finding.commit ? shortCommit(finding.commit) : null, finding.path, finding.problem].filter(Boolean).join(' · ') || 'Doctor finding', + href: finding.area ? `#worktree-${encodeURIComponent(finding.area)}` : '#evidence', + action: recoveryGap ? 'Review recovery evidence' : finding.area ? 'Inspect owner evidence' : 'Review evidence', + }); + } + + for (const task of (snapshot.backlog ?? []).filter((item) => item.state === 'blocked')) add('fleet', { + tone: 'review', title: task.title ?? task.id, owner: agentName(task.owner), + detail: task.blockedReason || 'This task is blocked; the reason is not recorded in the snapshot.', + evidence: task.id, href: '#backlog-board', action: 'Inspect task status', + }); + + const waitingIntegration = (snapshot.integration ?? []).filter((item) => !['landed', 'rejected', 'superseded'].includes(item.state)); + for (const item of waitingIntegration) add('arbiter', { + tone: 'review', title: `Integration ${item.state}`, owner: 'Session Manager / integration authority', + detail: 'Landing is serialized. The arbiter must check the exact candidate and required receipts before proceeding.', + evidence: `${item.sourceArea} · ${shortCommit(item.sourceCommit)} · ${item.reason ?? 'reason not recorded'}`, + href: '#delivery', action: 'Inspect integration queue', + }); + + const unknownWakes = snapshot.managerWakes?.blockingCount ?? 0; + if (unknownWakes) add('arbiter', { + tone: 'urgent', title: `${unknownWakes} manager launch${unknownWakes === 1 ? '' : 'es'} need inspection`, + owner: 'Session Manager', detail: 'An unknown launch blocks another scheduled wake. Inspect recorded runtime evidence; no automatic restart is available.', + evidence: 'A reserved launch does not prove the provider is still running.', + href: '#manager-wakes', action: 'Inspect manager launches', + }); + + const unresolvedDelivery = (snapshot.deliveryOperations ?? []).filter((operation) => ['running', 'unknown', 'succeeded'].includes(operation.state)); + if (unresolvedDelivery.length) add('arbiter', { + tone: 'urgent', title: `${unresolvedDelivery.length} delivery operation${unresolvedDelivery.length === 1 ? '' : 's'} need review`, + owner: 'Release and Self-host', detail: 'Inspect recorded attempts and external state before considering recovery. Do not repeat effects automatically.', + evidence: unresolvedDelivery.map((operation) => `${operation.operation} · ${operation.state} · ${shortCommit(operation.commit)}`).join('; '), + href: '#operation-outcomes', action: 'Inspect operation outcomes', + }); + + const neglected = (snapshot.backlogActivity?.tasks ?? []).filter((task) => task.status === 'stale' && task.ownerRequested); + if (neglected.length) add('fleet', { + tone: 'review', title: `${neglected.length} owner-requested task${neglected.length === 1 ? '' : 's'} need activity review`, + owner: 'Task owner not recorded', detail: 'No named commit appears within the configured review window. Blocked work may be waiting as expected.', + evidence: 'Review the recorded task activity before asking for a change.', href: '#activity-review', action: 'Open activity review', + }); + + const staleEvidence = (snapshot.backlogHealth?.findings ?? []).filter((finding) => + ['BACKLOG_OBSERVED_COMMIT_STALE', 'BACKLOG_OBSERVED_COMMIT_MISSING'].includes(finding.code)); + if (staleEvidence.length) add('fleet', { + tone: 'review', title: `${staleEvidence.length} task evidence record${staleEvidence.length === 1 ? '' : 's'} need refresh`, + owner: 'Task owner not recorded', detail: 'Some backlog evidence refers to an old or unreachable commit. Re-check it before dispatch.', + evidence: staleEvidence.map((finding) => `${finding.taskId ?? 'task not recorded'} · ${finding.code}`).join('; '), + href: '#backlog-board', action: 'Inspect backlog evidence', + }); + return groups; +} + + global.TorchAttentionProjection = Object.freeze({ groups: attentionGroups }); +})(globalThis); diff --git a/site/console.html b/site/console.html index bba7b05..1b6921a 100644 --- a/site/console.html +++ b/site/console.html @@ -14,6 +14,7 @@ + diff --git a/site/console.js b/site/console.js index 89e3f96..baf16e9 100644 --- a/site/console.js +++ b/site/console.js @@ -706,7 +706,7 @@ function renderFlowWatch(snapshot) { const waitClass = approval.approver === 'owner' ? 'wait-owner' : approval.approver === 'session-manager' ? 'wait-manager' : ''; const refs = [approval.task, approval.evidence].filter(Boolean).map((reference) => escapeHtml(reference)).join(' · '); - return `
+ return `

${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,30 @@ 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', 'Serialized execution and installation decisions stay with the 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)} · ${escapeHtml(item.evidence)} + ${escapeHtml(item.action)} + ${item.requestOwner ? `Request review from ${escapeHtml(item.owner)}` : ''} +
`).join('') : `

${escapeHtml(description)}

`} +
`; + }).join('')); } function render(snapshot) { @@ -789,7 +759,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 +777,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 +787,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 +814,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 +871,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 ?? []; @@ -1866,6 +1831,18 @@ $('#change-list')?.addEventListener('click', async (event) => { $('#attention-list')?.addEventListener('click', (event) => { 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..dbceea6 100644 --- a/site/styles.css +++ b/site/styles.css @@ -173,15 +173,23 @@ 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; } .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/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..3f3e266 --- /dev/null +++ b/test/fleet/console-attention.test.mjs @@ -0,0 +1,92 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import '../../site/attention-projection.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', 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.match(groups.owner[0].detail, /bounded scope/); + assert.equal(groups.fleet[0].owner, 'Independent QA'); + assert.equal(groups.fleet[0].action, 'Open approval details'); + assert.doesNotMatch(JSON.stringify(groups.fleet), /Approve|Reject/); +}); + +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: '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: 'qa', branch: 'qa/audit', path: '/work/qa', ahead: 0, behind: 0 }, + ], + }); + + assert.equal(groups.fleet.length, 2); + const runtime = groups.fleet.find((item) => item.owner === 'Provider Runtime'); + assert.equal(runtime.tone, 'review'); + assert.match(runtime.detail, /retained work progress, not a broken repository/); + assert.match(runtime.evidence, /runtime\/fix · \/work\/provider/); + assert.equal(runtime.requestOwner, 'provider-runtime'); + 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, 'Release and Self-host'); + 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-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: [] }); +});