Merge commit 'cb145e56cb7d530f9563b98d4f9920ff0372a67a' into torch/session-manager
No files matched your search
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
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.
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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 ?? [],
|
||||
|
||||
@@ -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: [] });
|
||||
});
|
||||
@@ -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/);
|
||||
|
||||