diff --git a/reports/test-audit/test-integrity-task-console-attention-actions-2026-10-01.md b/reports/test-audit/test-integrity-task-console-attention-actions-2026-10-01.md
new file mode 100644
index 0000000..f87842e
--- /dev/null
+++ b/reports/test-audit/test-integrity-task-console-attention-actions-2026-10-01.md
@@ -0,0 +1,37 @@
+# Test integrity note — TASK-console-attention-actions
+
+```yaml
+test_integrity_note:
+ change_type: [new_tests]
+ scenarios:
+ - id: SCN-console-attention-owner-shortcut
+ given: "A pending approval names the project owner and has an available decision form."
+ when: "The owner chooses an Approve or Reject shortcut from its attention card."
+ then: "Only the matching decision field is selected and the existing preview path starts; no decision is submitted directly."
+ - id: SCN-console-attention-shortcut-drafts
+ given: "The form is unavailable, has an active preview, or contains the opposite decision draft."
+ when: "An attention shortcut is selected."
+ then: "The action is declined with an explicit status and existing draft or preview values remain unchanged."
+ - id: SCN-console-attention-ownership
+ given: "An approval is pending for the owner or another named approver."
+ when: "Attention actions are projected."
+ then: "Only the owner-addressed item carries an owner decision shortcut identifier."
+ observable_outcomes:
+ - "Owner attention cards expose Approve and Reject controls only for pending requests addressed to owner."
+ - "A shortcut enters the existing revision/evidence preview and confirmation flow."
+ - "Missing forms, active previews, and conflicting drafts remain visible and unchanged."
+ - "Long evidence is available through a compact expandable disclosure."
+ determinism_controls:
+ - "Projection and shortcut scenarios use fixed in-memory fixtures without clock, network, or randomness."
+ anti_cheat_rationale:
+ prevents:
+ - "direct decision submission without preview and confirmation"
+ - "peer approval authority exposed as an owner control"
+ - "overwriting a pending decision draft or preview"
+ - "long evidence silently omitted from compact cards"
+ relaxation:
+ did_relax_any_assertion: false
+ if_true_explain_spec_basis: ""
+```
+
+Existing approval-console scenarios continue to cover stale revisions, named-approver enforcement, preview confirmation, and failed decisions. The attention tests add coverage at the UI projection and shortcut boundary; actual desktop/mobile visual acceptance remains separate and incomplete until a real browser capture is available.
diff --git a/site/attention-actions.js b/site/attention-actions.js
new file mode 100644
index 0000000..f89e8b7
--- /dev/null
+++ b/site/attention-actions.js
@@ -0,0 +1,25 @@
+(function (global) {
+ function prepareOwnerDecisionShortcut(form, decision, beginPreview) {
+ if (!['approved', 'rejected'].includes(decision)) {
+ return { started: false, reason: 'Choose approve or reject from an owner-only shortcut.' };
+ }
+ const choice = form?.elements?.namedItem('decision');
+ if (!form || !choice) {
+ return { started: false, reason: 'This owner decision form is no longer available. Refresh the current evidence before acting.' };
+ }
+ if (form.dataset.previewToken || choice.disabled) {
+ return { started: false, reason: 'An existing decision preview is preserved. Review or edit it in Flow watch before using another shortcut.' };
+ }
+ if (choice.value && choice.value !== decision) {
+ return { started: false, reason: 'Your existing decision draft is preserved. Review or change it in Flow watch before choosing another shortcut.' };
+ }
+ if (typeof beginPreview !== 'function') {
+ return { started: false, reason: 'The guarded decision preview is unavailable. Refresh before acting.' };
+ }
+ choice.value = decision;
+ beginPreview?.(form);
+ return { started: true };
+ }
+
+ global.TorchAttentionActions = Object.freeze({ prepareOwnerDecisionShortcut });
+})(globalThis);
diff --git a/site/attention-projection.js b/site/attention-projection.js
index 54c27e1..ba08c2a 100644
--- a/site/attention-projection.js
+++ b/site/attention-projection.js
@@ -27,6 +27,7 @@ function attentionGroups(snapshot) {
waitSince: approval.createdAt ?? null,
href: `#approval-${encodeURIComponent(approval.id)}`,
action: isOwner ? 'Review request and evidence' : 'Open approval details',
+ decisionApprovalId: isOwner ? approval.id : null,
});
}
diff --git a/site/console.html b/site/console.html
index 1b6921a..662156a 100644
--- a/site/console.html
+++ b/site/console.html
@@ -15,6 +15,7 @@
+
diff --git a/site/console.js b/site/console.js
index d502b4c..cba404d 100644
--- a/site/console.js
+++ b/site/console.js
@@ -743,8 +743,16 @@ function renderAttention(groups) {
${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))}` : ''} · ${escapeHtml(item.evidence)}
+
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)}
`}
`;
@@ -1829,6 +1837,32 @@ $('#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;
diff --git a/site/styles.css b/site/styles.css
index dbceea6..7ca7687 100644
--- a/site/styles.css
+++ b/site/styles.css
@@ -190,6 +190,13 @@ footer div { display: flex; gap: 1.5rem; }
.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/test/fleet/console-attention.test.mjs b/test/fleet/console-attention.test.mjs
index 0355af4..35b0377 100644
--- a/test/fleet/console-attention.test.mjs
+++ b/test/fleet/console-attention.test.mjs
@@ -1,6 +1,8 @@
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);
@@ -20,13 +22,74 @@ test('SCN-console-attention-ownership: only owner-addressed pending decisions en
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 },
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/);