Merge commit 'cb145e56cb7d530f9563b98d4f9920ff0372a67a' into torch/session-manager

This commit is contained in:
thePR0M3TH3AN committed 2026-10-01 05:33:16 -04:00
commit 11e23fa779
26 files changed
+714 -72

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

@@ -0,0 +1,164 @@
# Console UX audit: Owner Console evidence and coverage addendum
Task: `TASK-console-ux-audit` (blocked, revision 5)
Audit target: `http://127.0.0.1:4174/console`
Local branch: `torch/owner-console`
## Current status
This is a partial, evidence-bounded addendum to the arbiter's canonical report,
`reports/design-system/live-console-arbiter-audit-2026-09-30.md` at canonical
commit `7b7c6584b9d315d901133cff828f26f0fcc2003f`. Its measurements remain
attributed to that report, not to this audit.
The current TORCH backlog record is blocked at revision 5 and points to this
report and its exact viewport capture blocker. Desktop evidence is committed;
the audit is not complete or ready for integration.
I resumed the existing Brave browser binding and listed its existing Console
tab; attaching to that tab timed out twice. A fresh agent-created tab in the
same browser session worked. Live Console actions stayed read-only except for
unsent test strings in the local request form; neither string was sent and no
durable Console state was changed.
**Not complete:** mobile coverage beyond Overview, an exact 390 x 844 CSS
viewport, exhaustive keyboard order, empty/loading/failed-action states, and
task-owned artifact publication for every screen. The owner and peer guidance
identified a viewport transform: 351 x 760 at DPR 0.9 yields a 390 x 844 CSS
viewport. A fresh-tab attempt at that exact CSS size showed the Overview DOM,
but its single screenshot attempt still failed with the 5-second CDP timeout.
The earlier 433 x 938 CSS screenshot is retained as narrow-view evidence only;
it does not satisfy the exact viewport requirement. See the evidence below.
## Personally observed evidence
| View | URL fragment | Viewport | Viewed observation / evidence |
|---|---|---|---|
| Overview | `#overview` | 1237 x 677 | [Desktop screenshot](../../artifacts/TASK-console-ux-audit/desktop-overview.jpg). “Healthy” appears beside six attention items, including five generic doctor warnings; project facts also show `Clean at snapshot` and `ONE-DISK`. |
| Overview | `#overview` | 433 x 938 CSS, DPR 0.9 (requested 390 x 844) | [Narrow screenshot](../../artifacts/TASK-console-ux-audit/mobile-overview.jpg). Overview content fits the measured viewport width, but the horizontal nav clips after “Progress” and displays its own horizontal scrollbar. Screenshot dimensions are 462 x 1041 pixels. |
| Overview | `#overview` | 390 x 844 CSS, DPR 0.9 | Fresh-tab DOM was visible before the authorized screenshot attempt. The screenshot timed out with `Timed out after 5000ms waiting for CDP command Page.captureScreenshot.` No image was produced; this is blocker evidence, not visual coverage. |
| Briefing | `#owner-briefing` | 1237 x 677 | [Desktop screenshot](../../artifacts/TASK-console-ux-audit/desktop-briefing.jpg). Empty briefing state gives CLI build/publish instructions. On the first navigation, Work was highlighted while Briefing content was shown; repeated navigation selected Flow watch correctly. |
| Flow watch | `#flow-watch` | 1237 x 677 | [Desktop screenshot](../../artifacts/TASK-console-ux-audit/desktop-flow-watch.jpg). Check-in and approval panels sit side by side; timer installation is explicitly unverified and only named approvers can decide. |
| Work | `#work` | 1237 x 677 | [Desktop screenshot](../../artifacts/TASK-console-ux-audit/desktop-work.jpg). Filters form a dense block above five board columns; 44 active items make cards narrow and text wrap heavily. |
| Progress | `#initiative-progress` | 1237 x 677 | [Desktop screenshot](../../artifacts/TASK-console-ux-audit/desktop-progress.jpg). Feature/milestone summaries and the start of Fleet inventory share one long page. |
| Fleet | `#fleet` | 1237 x 677 | [Desktop screenshot](../../artifacts/TASK-console-ux-audit/desktop-fleet.jpg). Session state and ownership inventories sit side by side; repeated runtime/model/profile data and long paths dominate. |
| Conversations | `#communications` | 1237 x 677 | [Desktop screenshot](../../artifacts/TASK-console-ux-audit/desktop-conversations.jpg). Long messages fill two columns; an identical owner message appears in both columns. |
| Organization | `#organization` | 1237 x 677 | [Desktop screenshot](../../artifacts/TASK-console-ux-audit/desktop-organization.jpg). Three-column role cards repeat reporting/coordination links and show lengthy authority/path descriptions. |
| Evidence | `#evidence` | 1237 x 677 | [Desktop screenshot](../../artifacts/TASK-console-ux-audit/desktop-evidence.jpg). Check receipts use a narrow left column with repeated pass entries and large empty space to the right; entries are collapsed disclosures. |
| Release gates | `#delivery` | 1237 x 677 | [Desktop screenshot](../../artifacts/TASK-console-ux-audit/desktop-release-gates.jpg). Integration is “ready” while delivery, fetch, and operation receipts are absent; copy distinguishes landing from deployment. |
| Operations | `#delivery` (expanded disclosure) | 1237 x 677 | [Desktop screenshot](../../artifacts/TASK-console-ux-audit/desktop-operations.jpg). Expansion reveals Worktrees and Resources; details continue below the viewport. |
## Interaction evidence
- **Navigation/history:** section links update the hash. Back from Briefing returned
to Overview, and Forward returned to Briefing. The document remains one long
page rather than separate routes.
- **Keyboard:** pressing Tab after navigating to Overview moved focus to the
visible “Open relevant view” link near the bottom of Overview, with an orange
focus outline. This confirms visible focus for that control, not complete
tab-order coverage.
- **Manual refresh:** an unsent `AUDIT DRAFT ONLY — do not send.` string in the
request form survived clicking Refresh.
- **Automatic refresh:** an unsent `AUTO-REFRESH DRAFT ONLY — do not send.` string
remained after the 15-second update interval; the header reported “Updated just
now.”
- **Page reload:** the draft was gone after browser reload and reopening the
request disclosure. In-session refresh preserves it; a full reload does not.
- **Form validation:** Preview request with no recipient kept the form local and
displayed “Choose an identity and write the request before previewing.” No
message was sent. The form states that sending adds one durable inbox message.
- **Mobile:** the first viewport override did not apply to a newly created tab;
the owner-directed fresh-tab capture exposed that mismatch. On the fresh tab,
the 390 x 844 request measured 433 x 938 CSS pixels at DPR 0.9. DOM rectangles
showed the Overview section in view and no page-level horizontal overflow
(`documentWidth` 416, viewport width 433), while the visible navigation itself
had a horizontal scrollbar. The one authorized fresh-tab screenshot is
`mobile-overview.jpg` (462 x 1041 image pixels). The earlier
`mobile-release-gates.jpg` remains blank (3,157 bytes), followed by the exact
CDP screenshot timeout; it is not visual evidence of that view. QA and Provider
Runtime classified that failure as a browser/CDP capture limitation. Following
the owner's measured workaround, I created one new browser session, opened a
fresh tab, set 351 x 760 before navigation, then verified exact 390 x 844 CSS
dimensions at DPR 0.9. DOM geometry showed document width 373, Overview width
347.74, and nav scroll width 997 against client width 373; both header buttons
fit within 248.76 CSS pixels. The one screenshot attempt again returned the
exact 5-second CDP timeout. The override was reset immediately. No screenshot
was produced, so exact-size mobile visual coverage remains incomplete.
Selected screenshots are registered in private TORCH artifact storage with task,
session, and commit provenance:
| Image | Artifact ID | Commit |
|---|---|---|
| Desktop Overview | `d4c3bd6c-92b8-4791-9a3b-56c636bd6848` | `e0637f8255b79401fb483dddf3fe8d602aae687c` |
| Desktop Work | `703126cb-31b9-47ad-ba57-73d5f873b754` | `e0637f8255b79401fb483dddf3fe8d602aae687c` |
| Desktop Conversations | `c81acd53-da4e-4113-b5a8-ea3c75b0ac16` | `e0637f8255b79401fb483dddf3fe8d602aae687c` |
| Desktop Operations | `472d28b8-31f7-4130-abde-88f82c582970` | `e0637f8255b79401fb483dddf3fe8d602aae687c` |
| Desktop keyboard focus | `9e93f19d-f826-4d93-8ef7-c4a2474cf3bf` | `e0637f8255b79401fb483dddf3fe8d602aae687c` |
| Blank mobile capture (failure evidence) | `3bc724b8-a22f-4f93-8629-eec92ce06ce3` | `e0637f8255b79401fb483dddf3fe8d602aae687c` |
| Fresh-tab narrow Overview | `6d7848d9-a679-4005-8a5a-e5d8eb7d67de` | `412643da93df7df9e2da7a79cd2fd45b945decc0` |
The arbiter report provides the broader measured baseline: 50,288 px desktop
document, Work 36,662 px with 43 active tasks and 41 queue cards, Conversations
5,723 px, Organization 2,931 px, and a roughly 572 px narrow-screen filter
block. These are attributed to the arbiter report, not measurements from my
incomplete pass.
## Focused workspace proposal
Use separate, reloadable destinations with a persistent project/freshness
header and a concise overview. Suggested destinations:
- Overview: project outcome, owner decisions, blockers, recent events.
- Work: active tasks and a separately filtered proposed queue; compact rows
open exact task details.
- Attention: one card per underlying issue, with identity, branch/path, cause,
severity, and only actions supported by current authority.
- Agents: compact state/work/wait list; agent detail owns chat, profile, and
implementation ownership.
- Evidence: checks and artifacts linked to task, author, candidate commit,
and blocking reason. Preserve incomplete receipts as incomplete.
- Operations: resource/queue/timer and lifecycle evidence on its own screen.
- Organization and Releases: relationship/authority details and the separate
integration/delivery gates.
Keep the TORCH mark, dark mode, explicit approval previews, revision/evidence
checks, and honest unknown/incomplete states. Include the owner's requested
`TASK-console-attention-actions`: Approve/Reject only for an actual
owner-addressed approval; warnings should open the exact task, thread, or
worktree review. Deduplicate message counts and cards; do not add dismiss,
mass-acknowledge, merge, cleanup, or publication shortcuts.
## Measurable implementation acceptance criteria
1. Each primary destination has a distinct addressable URL. Reload, Back, and
Forward restore the expected view while retaining project identity and
freshness context.
2. At 390 x 844 CSS px, primary destinations have no horizontal page overflow;
work is a readable list or selected-column view, not a forced full-board
carousel. Secondary filters do not consume the first 572 px of the view.
3. Keyboard-only users can reach every primary destination and action, see focus,
and identify the current destination through an accessible current-page state.
4. A unique underlying attention issue appears once. Labels name the affected
identity and branch/path where available; approval actions remain hidden or
disabled for non-owner requests and use existing preview/revision/evidence
confirmation.
5. Automatic polling, explicit refresh, and route changes preserve an unsent
draft. A controlled failed or interrupted action displays pending/error/
unknown accurately and never announces success without a receipt.
6. Evidence links resolve to the exact task, candidate commit, author, receipt,
and blocker. A dashboard check that changes tracked files remains incomplete
even if its process exits zero.
7. Overview and agent lists stay compact as task/message counts grow; long
descriptions and message bodies open in contextual detail views. Duplicate
message cards are absent across recipient/group projections.
## Next evidence needed
Coordinate the capture behavior with Provider Runtime and QA. If a later task
authorization permits more mobile captures, inspect every navigation view at an
exact 390 x 844 CSS viewport for overflow, wrapping, nav behavior, and density.
Continue keyboard order and representative empty/loading/pending/failed/unknown
states, using the isolated demo for state-changing actions. Publish only
task-owned evidence after local commit. Live state stays read-only. No UI
implementation or release is proposed by this audit.
@@ -0,0 +1,51 @@
# 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"
- id: SCN-console-attention-authority
given: "Integration records may name a manager as authorizer while other runtime records lack an arbiter grant"
when: "Attention ownership is projected from the live roster"
then: "Roster titles supply display names, integration remains Fleet handling, and no arbiter-only authority is inferred"
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"
- "Manager-authorized integration is not mislabeled as an owner or arbiter decision"
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.
@@ -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.
+25
View File
@@ -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);
+151
View File
@@ -0,0 +1,151 @@
(function (global) {
const shortCommit = (value) => value ? String(value).slice(0, 9) : '—';
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) => 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',
waitSince: approval.createdAt ?? null,
href: `#approval-${encodeURIComponent(approval.id)}`,
action: isOwner ? 'Review request and evidence' : 'Open approval details',
decisionApprovalId: isOwner ? approval.id : null,
});
}
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 owning specialist or manager 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: 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('fleet', {
tone: 'review', title: `Integration ${item.state}`, owner: item.authorizedBy ? agentName(item.authorizedBy) : 'Integration authority not recorded',
detail: 'Review the exact candidate and required receipts under the current named landing authority. Queue state does not establish that a separate owner decision is required.',
evidence: [item.sourceArea, shortCommit(item.sourceCommit), item.authorizedBy ? `authorized by ${agentName(item.authorizedBy)}` : null, item.reason ?? 'reason not recorded'].filter(Boolean).join(' · '),
href: '#delivery', action: 'Inspect integration queue',
});
const unknownWakes = snapshot.managerWakes?.blockingCount ?? 0;
if (unknownWakes) {
const reservations = (snapshot.managerWakes?.reservations ?? []).filter((item) => item.blocksManagerWake);
const wakeOwners = [...new Set(reservations.map((item) => agentName(item.managerId)))];
add('fleet', {
tone: 'urgent', title: `${unknownWakes} scheduled manager launch${unknownWakes === 1 ? '' : 'es'} ${unknownWakes === 1 ? 'needs' : 'need'} inspection`,
owner: wakeOwners.join(', ') || 'Scheduled wake owner not recorded',
detail: 'A recorded reservation blocks another wake. Inspect current runtime evidence before acting; this record does not prove a provider is still running or establish who may restart it.',
evidence: reservations.map((item) => [item.scheduleId, item.reservedAt, item.outcome].filter(Boolean).join(' · ')).join('; ') || 'Reservation detail unavailable',
href: '#manager-wakes', action: 'Inspect manager launches',
});
}
const unresolvedDelivery = (snapshot.deliveryOperations ?? []).filter((operation) => ['running', 'unknown', 'succeeded'].includes(operation.state));
if (unresolvedDelivery.length) add('fleet', {
tone: 'urgent', title: `${unresolvedDelivery.length} delivery operation${unresolvedDelivery.length === 1 ? '' : 's'} need review`,
owner: [...new Set(unresolvedDelivery.map((operation) => operation.actor ? agentName(operation.actor) : 'Operation owner not recorded'))].join(', '),
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);
+2
View File
@@ -14,6 +14,8 @@
<script src="/check-evidence.js" defer></script>
<script src="/task-activity.js" defer></script>
<script src="/operation-outcomes.js" defer></script>
<script src="/attention-projection.js" defer></script>
<script src="/attention-actions.js" defer></script>
<script src="/live-refresh.js" defer></script>
<script src="/demo.js" defer></script>
<script src="/console.js" defer></script>
+77 -66
View File
@@ -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 `<article class="approval-item ${waitClass}">
return `<article class="approval-item ${waitClass}" id="approval-${escapeHtml(encodeURIComponent(approval.id))}">
<h4>${escapeHtml(approval.title)}</h4>
<p>${escapeHtml(requester?.title ?? approval.requester)} is waiting on <strong class="approval-target">${escapeHtml(approver)}</strong>${refs ? ` · ${refs}` : ''}</p>
<details><summary>Request details · ${escapeHtml(ageLabel(approval.createdAt))}</summary><p>${escapeHtml(approval.summary)}</p></details>
@@ -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 `<section class="attention-group attention-group-${key}" aria-labelledby="attention-${key}-title">
<div class="attention-group-heading"><h3 id="attention-${key}-title">${title}</h3><span>${items.length}</span></div>
${items.length ? items.map((item) => `<article class="attention-item tone-${escapeHtml(item.tone)}">
<span class="attention-mark" aria-hidden="true"></span><div class="attention-copy">
<strong>${escapeHtml(item.title)}</strong><p>${escapeHtml(item.detail)}</p>
<small>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))}` : ''}</small>
${item.evidence.length > 120
? `<details class="attention-evidence"><summary>Evidence and references</summary><p>${escapeHtml(item.evidence)}</p></details>`
: `<small class="attention-evidence-short">Evidence: ${escapeHtml(item.evidence)}</small>`}
<a href="${escapeHtml(item.href)}">${escapeHtml(item.action)}</a>
${item.decisionApprovalId ? `<div class="attention-quick-actions" role="group" aria-label="Owner decision shortcuts for ${escapeHtml(item.title)}">
<button type="button" data-attention-decision="approved" data-attention-approval="${escapeHtml(item.decisionApprovalId)}">Approve</button>
<button type="button" class="quiet-action" data-attention-decision="rejected" data-attention-approval="${escapeHtml(item.decisionApprovalId)}">Reject</button>
<p class="attention-action-status" role="status" aria-live="polite" hidden></p>
</div>` : ''}
${item.requestOwner ? `<a href="#owner-request-form" data-attention-recipient="${escapeHtml(item.requestOwner)}">Request review from ${escapeHtml(item.owner)}</a>` : ''}
</div></article>`).join('') : `<p class="attention-empty">${escapeHtml(description)}</p>`}
</section>`;
}).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) => `
<div class="attention-item tone-${escapeHtml(item.tone)}">
<span class="attention-mark" aria-hidden="true"></span>
<div><strong>${escapeHtml(item.title)}</strong><p>${escapeHtml(item.detail)}</p>
${item.href ? `<a href="${escapeHtml(item.href)}">Open relevant view</a>` : ''}</div>
</div>`).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 `<article class="conversation-item"><div class="conversation-meta"><strong>${escapeHtml(message.sender)} to ${escapeHtml(message.recipient)}</strong><span>${escapeHtml(ageLabel(message.createdAt))}</span></div><p>${escapeHtml(message.body)}</p><small>${escapeHtml(ack)}${refs.length ? ` · ${escapeHtml(refs.join(' · '))}` : ''}</small></article>`;
return `<article class="conversation-item" id="message-${escapeHtml(message.id)}"><div class="conversation-meta"><strong>${escapeHtml(message.sender)} to ${escapeHtml(message.recipient)}</strong><span>${escapeHtml(ageLabel(message.createdAt))}</span></div><p>${escapeHtml(message.body)}</p><small>${escapeHtml(ack)}${refs.length ? ` · ${escapeHtml(refs.join(' · '))}` : ''}</small></article>`;
}).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) => `
<div class="list-row"><div><strong>${escapeHtml(worktree.area)}</strong><small>${escapeHtml(worktree.branch)} · ${escapeHtml(worktree.path)}</small></div><span>${escapeHtml(`${worktree.ahead ?? 0} ahead / ${worktree.behind ?? 0} behind`)}</span></div>
<div class="list-row" id="worktree-${escapeHtml(encodeURIComponent(worktree.area))}"><div><strong>${escapeHtml(worktree.area)}</strong><small>${escapeHtml(worktree.branch)} · ${escapeHtml(worktree.path)}</small></div><span>${escapeHtml(`${worktree.ahead ?? 0} ahead / ${worktree.behind ?? 0} behind`)}</span></div>
`).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;
}
+19 -4
View File
@@ -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; }
+2
View File
@@ -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([
+2 -1
View File
@@ -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 ?? [],
+180
View File
@@ -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: [] });
});
+4 -1
View File
@@ -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, /<title>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/);