diff --git a/artifacts/TASK-console-ux-audit/desktop-briefing.jpg b/artifacts/TASK-console-ux-audit/desktop-briefing.jpg new file mode 100644 index 0000000..afdeb09 Binary files /dev/null and b/artifacts/TASK-console-ux-audit/desktop-briefing.jpg differ diff --git a/artifacts/TASK-console-ux-audit/desktop-conversations.jpg b/artifacts/TASK-console-ux-audit/desktop-conversations.jpg new file mode 100644 index 0000000..0b698ca Binary files /dev/null and b/artifacts/TASK-console-ux-audit/desktop-conversations.jpg differ diff --git a/artifacts/TASK-console-ux-audit/desktop-evidence.jpg b/artifacts/TASK-console-ux-audit/desktop-evidence.jpg new file mode 100644 index 0000000..8ad4ce8 Binary files /dev/null and b/artifacts/TASK-console-ux-audit/desktop-evidence.jpg differ diff --git a/artifacts/TASK-console-ux-audit/desktop-fleet.jpg b/artifacts/TASK-console-ux-audit/desktop-fleet.jpg new file mode 100644 index 0000000..310b47c Binary files /dev/null and b/artifacts/TASK-console-ux-audit/desktop-fleet.jpg differ diff --git a/artifacts/TASK-console-ux-audit/desktop-flow-watch.jpg b/artifacts/TASK-console-ux-audit/desktop-flow-watch.jpg new file mode 100644 index 0000000..fae23bb Binary files /dev/null and b/artifacts/TASK-console-ux-audit/desktop-flow-watch.jpg differ diff --git a/artifacts/TASK-console-ux-audit/desktop-keyboard-tab.jpg b/artifacts/TASK-console-ux-audit/desktop-keyboard-tab.jpg new file mode 100644 index 0000000..184f9a1 Binary files /dev/null and b/artifacts/TASK-console-ux-audit/desktop-keyboard-tab.jpg differ diff --git a/artifacts/TASK-console-ux-audit/desktop-operations.jpg b/artifacts/TASK-console-ux-audit/desktop-operations.jpg new file mode 100644 index 0000000..937c1d7 Binary files /dev/null and b/artifacts/TASK-console-ux-audit/desktop-operations.jpg differ diff --git a/artifacts/TASK-console-ux-audit/desktop-organization.jpg b/artifacts/TASK-console-ux-audit/desktop-organization.jpg new file mode 100644 index 0000000..92e7f1d Binary files /dev/null and b/artifacts/TASK-console-ux-audit/desktop-organization.jpg differ diff --git a/artifacts/TASK-console-ux-audit/desktop-overview.jpg b/artifacts/TASK-console-ux-audit/desktop-overview.jpg new file mode 100644 index 0000000..c193603 Binary files /dev/null and b/artifacts/TASK-console-ux-audit/desktop-overview.jpg differ diff --git a/artifacts/TASK-console-ux-audit/desktop-progress.jpg b/artifacts/TASK-console-ux-audit/desktop-progress.jpg new file mode 100644 index 0000000..cd0b033 Binary files /dev/null and b/artifacts/TASK-console-ux-audit/desktop-progress.jpg differ diff --git a/artifacts/TASK-console-ux-audit/desktop-release-gates.jpg b/artifacts/TASK-console-ux-audit/desktop-release-gates.jpg new file mode 100644 index 0000000..f123cdc Binary files /dev/null and b/artifacts/TASK-console-ux-audit/desktop-release-gates.jpg differ diff --git a/artifacts/TASK-console-ux-audit/desktop-work.jpg b/artifacts/TASK-console-ux-audit/desktop-work.jpg new file mode 100644 index 0000000..6787963 Binary files /dev/null and b/artifacts/TASK-console-ux-audit/desktop-work.jpg differ diff --git a/artifacts/TASK-console-ux-audit/mobile-overview.jpg b/artifacts/TASK-console-ux-audit/mobile-overview.jpg new file mode 100644 index 0000000..127cd27 Binary files /dev/null and b/artifacts/TASK-console-ux-audit/mobile-overview.jpg differ diff --git a/artifacts/TASK-console-ux-audit/mobile-release-gates.jpg b/artifacts/TASK-console-ux-audit/mobile-release-gates.jpg new file mode 100644 index 0000000..225804b Binary files /dev/null and b/artifacts/TASK-console-ux-audit/mobile-release-gates.jpg differ diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index a01d7bf..44c659d 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -1,5 +1,187 @@ # Test Integrity Notes +## 2026-09-30 — Bounded routine coordination + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-routine-coordination-instructions + given: A freshly installed Fleet using generated default prompts + when: Common, manager, specialist, and resume instructions are read + then: Each preserves own-inbox acknowledgement, direct peer routing, owner exceptions, and paused execution + - id: SCN-routine-coordination-boundaries + given: An assigned task and pending peer and owner approvals in a fixed-time local control plane + when: The named recipient acknowledges messages and the manager attempts a non-owned decision + then: The task and approvals remain pending until only the named peer decides its approval + - id: SCN-routine-coordination-paused-dispatch + given: An assigned task, an approved named-peer request, and a pending owner provider decision + when: A public runtime start is attempted without an authorized executor + then: The start is refused, no provider adapter runs, and the assigned task and owner wait remain unresolved + - id: SCN-routine-coordination-paused-schedule-dispatch + given: An assigned waiting task, a pending owner provider decision, and an installed manager check-in schedule with wake.enabled false + when: The public system scheduler dispatches the due owner-authorized coordination schedule + then: It queues a durable manager review without planning or invoking a provider, while the task and owner wait remain unresolved + observable_outcomes: + - Generated tracked instruction text for fresh-install and resume surfaces + - Durable task state, approval state, named-approver identity, and authority errors + - Public start refusal, zero adapter invocations, offline identity state, and retained owner wait + - Installed wake policy, public scheduled-dispatch receipt, zero provider or command invocations, retained assignment and owner wait + determinism_controls: + - Disposable local Git repository, SQLite state, and fixed clock + - No provider calls, network, retries, sleeps, or host timers + anti_cheat_rationale: + prevents: + - Treating acknowledgement as task or approval completion + - Letting a manager decide an owner or peer approval + - Omitting policy boundaries from one generated startup surface + - Re-enabling paused execution or granting starts, spending, publication, destructive recovery, or arbitrary dispatch + - Treating a queued manager check-in as authorization to start a provider or resolve the underlying wait + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-30 — Codex terminal completion correction + +```yaml +test_integrity_note: + change_type: spec_correction + scenarios: + - id: SCN-mixed-runtime + given: A status-zero Codex protocol stream containing thread.started and turn.completed + when: Fleet startup captures the provider-specific durable identity + then: The completed turn may record idle with the captured identity + - id: SCN-codex-terminal-completion + given: A status-zero Codex stream containing thread.started but no turn.completed + when: Fleet startup evaluates terminal evidence + then: Startup fails, retains the captured identity as working, and area startup refuses a duplicate + - id: SCN-provider-update-cli + given: The managed-provider fixture simulates a successful status-zero Codex exec stream + when: Startup follows an approved provider update + then: The fixture includes turn.completed after thread.started, preserving strict successful-turn evidence + observable_outcomes: + - Durable runtime state and captured Codex thread identifier + - FLEET_START_FAILED outcome and public area-start blocker + determinism_controls: + - Disposable local Git fixture and fixed JSON protocol records + - No provider calls, network, retries, sleeps, or timeout changes + anti_cheat_rationale: + prevents: + - Treating identity creation as successful turn completion + - Returning idle from a status-zero stream missing terminal evidence + - Launching a duplicate over an uncertain captured identity + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +Spec basis: a native successful Codex protocol probe emitted ten records ending +in `turn.completed`; `thread.started` establishes identity only, not completion. + +## 2026-09-30 — Bounded Codex executor qualification + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-codex-streaming-protocol-reduction + given: A successful Codex stream containing an initial identity, a multi-megabyte image record, private stderr, and terminal completion + when: The bounded reducer and lifecycle process the stream + then: Only identity and terminal records cross the boundary and idle is recorded without private output + - id: SCN-cli-codex-protocol-routing + given: A Codex launch planned by the CLI + when: The CLI invokes the executor + then: It launches the bounded reducer instead of collecting provider stdout directly + - id: SCN-codex-final-terminal-state + given: A stream whose final terminal failure follows an earlier completion, plus real child signal and spawn-error cases + when: The reducer and lifecycle classify the child outcome + then: Only the final failed terminal is retained, signals remain uncertain, and a safe spawn-error code is preserved + - id: SCN-scheduled-manager-wake-terminal-failure + given: An owner-authorized manager check-in with a planned Codex wake whose stream lacks terminal completion + when: The actual ScheduleService invokes the wake boundary + then: The schedule and wake reservation are failed while the manager identity remains working + observable_outcomes: + - Bounded reducer stdout, durable runtime state, schedule result, and reservation outcome + determinism_controls: + - Disposable Git/control-plane fixtures and fixed protocol records + - Local SIGTERM-aware child with a readiness marker; no provider, network, retries, or sleeps + anti_cheat_rationale: + prevents: + - Solving image overflow only by shrinking a parent buffer + - Marking a missing-terminal manager wake as invoked or successful + - Accepting an earlier completed event after a later failed terminal + - Retaining private image, token, or reasoning material in durable evidence + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-30 — Executor terminal-outcome integrity + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-executor-interrupted-outcomes + given: A real child that exits cleanly after SIGTERM plus deterministic signal and ENOBUFS executor results + when: Runtime startup receives a status-zero timeout, signal, or output-overflow error + then: No idle completion is recorded; captured identity remains working and failure evidence excludes private output + - id: SCN-scheduled-executor-failure + given: A scheduled command returns status zero together with ENOBUFS and screenshot-heavy output + when: The scheduler records its receipt + then: The receipt is failed, output is bounded, and credential or private-reasoning values are redacted + observable_outcomes: + - Durable runtime presence state and retained native session identifier + - Schedule result, exit status, bounded output, and redacted error evidence + determinism_controls: + - Disposable local Git fixtures and a bounded SIGTERM-aware child process + - Injected signal and ENOBUFS boundaries; no network, provider launch, retry, or sleep + anti_cheat_rationale: + prevents: + - Treating status zero as successful when Node reports timeout or output overflow + - Marking an interrupted runtime idle and permitting a duplicate launch + - Passing screenshot payloads, credentials, prompts, or private reasoning into durable receipts + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-30 — Early identity and safe no-code diagnostics + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-codex-interrupted-identity + given: A real local helper whose child emits thread.started then remains alive + when: The parent observes the reduced bounded identity and sends SIGTERM to the helper + then: The helper closes by SIGTERM without a terminal success, while lifecycle retains the identity as working and uncertain + - id: SCN-codex-code-less-diagnostics + given: Code-less structured model, authentication, or quota errors containing prompt, reasoning, and image adversarial fields + when: The bounded reducer and adapter classify startup failure + then: Only the allowlisted category crosses the boundary; no free-form diagnostic or adversarial field remains + - id: SCN-cli-codex-real-executable + given: An installed disposable project and an isolated executable named codex + when: The actual runCli up command starts the selected domain + then: The reducer carries identity and terminal completion through the CLI boundary and durable identity becomes idle + observable_outcomes: + - Real helper close signal, exact reduced protocol bytes, and working retained runtime identity + - Safe diagnostic category with no prompt, reasoning, image, or free-form message + - Actual CLI JSON response and persisted identity state from a fake executable on an isolated PATH + determinism_controls: + - Local child process signal handshake driven by receipt of thread.started; a bounded watchdog only terminates and awaits the owned fixture on failed emission, never retries or asserts product timing + - Disposable Git/XDG fixtures and a hermetic executable; a tagged guard rejects an absent or mismatched env before native spawn, then delegates unchanged to native spawnSync only for the validated fake path + anti_cheat_rationale: + prevents: + - Delaying identity output until child close and losing it on interruption + - Treating code-less model/auth/quota errors as successful or retaining their raw text + - Testing only an injected launch helper instead of the CLI execution boundary + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + ## 2026-09-30 — Generated exact-check artifacts ```yaml @@ -2322,6 +2504,138 @@ test_integrity_note: if_true_explain_spec_basis: "" ``` +# Codex startup runtime diagnostics (2026-09-30) + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-codex-startup-diagnostics + given: "A real Codex adapter and lifecycle plan receive deterministic nonzero child-process results" + when: "Codex stdout contains a model-rejection event, malformed JSON, or output above the diagnostic bound" + then: "Owner-visible failure details prefer the structured model rejection, redact stderr secrets and prompt fields, and label malformed or oversized output unknown with an explicit reason" + observable_outcomes: + - "FLEET_START_FAILED status, persisted offline identity state and owner-visible diagnostic details" + - "Structured model rejection over noisy MCP stderr; explicit unknown outcomes without retained oversized stdout" + - "No synthetic prompt, token or password value in serialized error details" + determinism_controls: + - "Disposable local Git/XDG fixtures and fixed injected child-process output" + - "Real createCodexAdapter and startFleet boundary; no provider calls, retries, sleeps or network" + anti_cheat_rationale: + prevents: + - "Returning a hard-coded success while losing the real nonzero execution status" + - "Displaying unrelated MCP noise instead of a structured model rejection" + - "Leaking raw prompt or credential values through diagnostics" + - "Treating malformed or oversized stdout as trusted structured evidence" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +# Standalone dashboard artifact isolation (2026-10-01) + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-dashboard-artifacts + given: "The dashboard test runs without an artifact environment, with an external runner directory, or with a repository/worktree target" + when: "It chooses the screenshot output boundary before the browser or screenshot writer starts" + then: "The default is a retained fresh external directory with provenance and hashes; explicit external output is honored; repository, temporary-root, pre-existing and symlink-resolved targets fail before protected bytes change" + observable_outcomes: + - "Printed artifact directory, provenance, relative names, byte counts and SHA-256 hashes" + - "Refusal code for direct, temporary-root, existing-file and symlink-resolved tracked-worktree destinations" + - "Unchanged tracked baseline and external user bytes after refusal" + determinism_controls: + - "Disposable Git worktrees and temporary directories" + - "Fixed fixture bytes and SHA-256 assertions; no browser, network, sleep or retry is needed for the boundary scenarios" + anti_cheat_rationale: + prevents: + - "Silently falling back to tracked golden-image paths" + - "Bypassing repository protection through a symlink" + - "Claiming runner output without exact retained evidence provenance" + - "Masking a protected-write failure by changing golden assertions" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +# Console dashboard navigation and preview freshness (2026-10-01) + +```yaml +test_integrity_note: + change_type: [new_tests, refactor_tests] + scenarios: + - id: SCN-dashboard-fragment-navigation-preserves-existing-captures + given: "The existing dashboard scenarios and eleven screenshot destinations run in their established order" + when: "The driver selects each target section's supported fragment immediately before its unchanged checks and captures" + then: "Each original assertion and capture executes against the selected view without changing capture order, viewport coverage, artifact destination, or artifact safeguards" + - id: SCN-console-priority-preview-invalidates-on-newer-evidence + given: "A task priority preview and unsent priority and reason values are created from task evidence revision N" + when: "A separate confirmed sample API action advances that task to authoritative revision N+1, then the reader refreshes again after navigating away and back" + then: "The revision N token, preview, and confirmation control remain absent; revision N+1 is visible, the unsent values remain intact and editable across both refreshes, a new preview is bound to N+1, and that current preview survives another unchanged refresh" + - id: SCN-console-current-preview-and-draft-survive-unchanged-refresh + given: "A task has an active priority preview and unsent priority and reason values bound to its current evidence revision" + when: "Console refresh returns the same task revision" + then: "The preview token, review panel, confirmation boundary, and unsent field values remain available unchanged" + - id: SCN-console-view-navigation + given: "The Console has addressable work, fleet, evidence, and flow-watch views" + when: "A reader opens fragments, follows navigation, and uses browser Back and Forward" + then: "Exactly one matching view is visible with the matching title, accessible current-page link, and persistent project and freshness context" + - id: SCN-console-mobile-navigation + given: "The Console is rendered at a 390x844 viewport" + when: "Each supported destination is selected" + then: "Every navigation destination remains visible and the document and navigation have no horizontal overflow" + - id: SCN-console-view-title-mapping + given: "The Console loads Overview and a reader selects each other supported view" + when: "The reader returns to Overview after visiting contextual views" + then: "Overview has the exact title TORCH Dashboard on initial and return navigation; all other views keep their exact contextual titles" + - id: SCN-console-owner-briefing-evidence-limitation-stays-visible + given: "The sample Console has a published owner briefing with reporting provenance" + when: "The owner briefing is rendered without opening compact provenance details" + then: "The warning that recorded evidence is not independent live verification remains visible, while exact publication time and reporting-window details stay in the collapsed disclosure" + - id: SCN-console-secondary-deeplink-focus-and-history + given: "The Console is loaded at the organization-proposals or approval-request-list fragment" + when: "The reader opens either direct link and uses browser Back and Forward" + then: "Exactly the matching view is selected and the actual secondary target is focused inside the viewport" + - id: SCN-console-operational-deeplink-fleet + given: "Operations, resources, schedules, providers, decisions, context, and manager-wakes are grouped in the Fleet workspace at 1280x900 and 390x844" + when: "The reader loads #manager-wakes directly, follows its existing attention link, or returns to it through Back and Forward, then later navigates, scrolls, and refreshes" + then: "After the first deterministic data/layout application Fleet is selected, the Operations disclosure opens, manager-wakes receives focus fully inside the viewport, a later refresh retains the user's selected view and scrolled position without refocusing manager-wakes, and Operations is hidden on unrelated views" + - id: SCN-console-view-drafts + given: "A task draft and an owner approval preview are present" + when: "The reader changes views and refreshes the sample Console" + then: "The unsent draft and same-revision preview remain available with their existing explicit owner confirmation boundary" + observable_outcomes: + - "Selected fragment, visible view, original assertion results, and the unchanged ordered screenshot set" + - "Revision N+1 displayed after refresh, with the old preview token and confirmation control unavailable" + - "Unsent priority and reason values retained and editable after newer evidence invalidates the old preview" + - "The same draft remains dirty and intact after another unchanged refresh and route transition, with the old token and confirmation control still absent" + - "An unchanged current-revision preview, confirmation boundary, and draft retained after refresh" + - "A regenerated preview token and displayed revision bound to N+1" + - "Exact Overview and contextual view titles, plus focused in-viewport secondary targets across direct loads and browser history" + - "The manager-wakes route selects Fleet and opens/focuses its nested target fully inside desktop and mobile viewports after first render and across browser history; later refresh retains the user's selected view and scrolled position without refocusing manager-wakes; operations stay hidden outside Fleet" + - "Visible recorded-evidence limitation beside owner briefing content while exact provenance remains compact and collapsed" + - "No browser requests to live project APIs and no changes to tracked screenshot expectations" + determinism_controls: + - "The existing isolated sample Console, fixed Playwright clock, and fixed desktop/mobile viewports" + - "Manager-wakes routing uses the existing isolated sample Console, a deterministic two-frame first-generation layout-completion marker, and browser fragment history without changing project state" + - "The test establishes a fixed nonzero numeric scroll baseline, observes that exact position, dispatches the actual non-scrolling Refresh button event, waits for generation two, and retains the exact scroll-position equality assertion" + - "Newer evidence is introduced through the sample's supported preview/confirm API before an explicit refresh" + - "The browser test advances only the isolated sample API; it never submits the priority form or mutates a live project" + - "No network, live project mutation, retry, sleep, timeout, golden update, or direct fixture-source change" + anti_cheat_rationale: + prevents: + - "Capturing a view other than the one named by the scenario while retaining a passing screenshot call" + - "Passing scroll-preservation coverage because an unsettled smooth-scroll or Playwright auto-scroll masks refresh movement" + - "Reusing a revision N preview after authoritative task evidence advances to N+1" + - "Treating an unchanged preview on refresh as proof of stale-preview invalidation" + - "Passing because of changed screenshot expectations, weakened assertions, or bypassed artifact provenance" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + # Candidate engine and artifact sealing Phase A (2026-10-01) ```yaml diff --git a/package-lock.json b/package-lock.json index 13acd7f..86045d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "torch-agent-fleet", - "version": "0.1.0-alpha.2", + "version": "0.1.0-alpha.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "torch-agent-fleet", - "version": "0.1.0-alpha.2", + "version": "0.1.0-alpha.3", "bundleDependencies": [ "@modelcontextprotocol/server", "typebox", diff --git a/package.json b/package.json index b3352f1..42324e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "torch-agent-fleet", - "version": "0.1.0-alpha.2", + "version": "0.1.0-alpha.3", "type": "module", "description": "TORCH — a portable, provider-independent AI development fleet for Git repositories", "main": "src/cli.mjs", diff --git a/reports/design-system/task-console-ux-audit-owner-console-2026-09-30.md b/reports/design-system/task-console-ux-audit-owner-console-2026-09-30.md new file mode 100644 index 0000000..51ad13f --- /dev/null +++ b/reports/design-system/task-console-ux-audit-owner-console-2026-09-30.md @@ -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. diff --git a/reports/test-audit/console-attention-integrity-report-2026-10-01.md b/reports/test-audit/console-attention-integrity-report-2026-10-01.md new file mode 100644 index 0000000..7c6c4d8 --- /dev/null +++ b/reports/test-audit/console-attention-integrity-report-2026-10-01.md @@ -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. diff --git a/reports/test-audit/test-integrity-task-console-attention-actions-2026-10-01.md b/reports/test-audit/test-integrity-task-console-attention-actions-2026-10-01.md new file mode 100644 index 0000000..f87842e --- /dev/null +++ b/reports/test-audit/test-integrity-task-console-attention-actions-2026-10-01.md @@ -0,0 +1,37 @@ +# Test integrity note — TASK-console-attention-actions + +```yaml +test_integrity_note: + change_type: [new_tests] + scenarios: + - id: SCN-console-attention-owner-shortcut + given: "A pending approval names the project owner and has an available decision form." + when: "The owner chooses an Approve or Reject shortcut from its attention card." + then: "Only the matching decision field is selected and the existing preview path starts; no decision is submitted directly." + - id: SCN-console-attention-shortcut-drafts + given: "The form is unavailable, has an active preview, or contains the opposite decision draft." + when: "An attention shortcut is selected." + then: "The action is declined with an explicit status and existing draft or preview values remain unchanged." + - id: SCN-console-attention-ownership + given: "An approval is pending for the owner or another named approver." + when: "Attention actions are projected." + then: "Only the owner-addressed item carries an owner decision shortcut identifier." + observable_outcomes: + - "Owner attention cards expose Approve and Reject controls only for pending requests addressed to owner." + - "A shortcut enters the existing revision/evidence preview and confirmation flow." + - "Missing forms, active previews, and conflicting drafts remain visible and unchanged." + - "Long evidence is available through a compact expandable disclosure." + determinism_controls: + - "Projection and shortcut scenarios use fixed in-memory fixtures without clock, network, or randomness." + anti_cheat_rationale: + prevents: + - "direct decision submission without preview and confirmation" + - "peer approval authority exposed as an owner control" + - "overwriting a pending decision draft or preview" + - "long evidence silently omitted from compact cards" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +Existing approval-console scenarios continue to cover stale revisions, named-approver enforcement, preview confirmation, and failed decisions. The attention tests add coverage at the UI projection and shortcut boundary; actual desktop/mobile visual acceptance remains separate and incomplete until a real browser capture is available. diff --git a/scripts/dashboard-artifacts.mjs b/scripts/dashboard-artifacts.mjs new file mode 100644 index 0000000..5a9c3e1 --- /dev/null +++ b/scripts/dashboard-artifacts.mjs @@ -0,0 +1,120 @@ +import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { existsSync, lstatSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, realpathSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'; + +function isWithin(path, root) { + const difference = relative(root, path); + return difference === '' || (!isAbsolute(difference) && difference !== '..' && !difference.startsWith(`..${sep}`)); +} + +function existingAncestor(path) { + let ancestor = resolve(path); + while (!existsSync(ancestor)) { + const parent = dirname(ancestor); + if (parent === ancestor) throw new Error(`No existing ancestor for ${path}`); + ancestor = parent; + } + return ancestor; +} + +function trackedWorktreeRoots(repositoryRoot) { + const roots = new Set([resolve(repositoryRoot)]); + const output = execFileSync('git', ['worktree', 'list', '--porcelain'], { + cwd: repositoryRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], + }); + for (const line of output.split('\n')) { + if (line.startsWith('worktree ')) roots.add(resolve(line.slice('worktree '.length))); + } + return [...roots].flatMap((root) => existsSync(root) ? [root, realpathSync(root)] : [root]); +} + +function refusal(target, protectedRoot) { + const error = new Error(`DASHBOARD_ARTIFACT_DIRECTORY_FORBIDDEN: ${target} resolves into tracked worktree ${protectedRoot}`); + error.code = 'DASHBOARD_ARTIFACT_DIRECTORY_FORBIDDEN'; + return error; +} + +function targetExists(target) { + try { + lstatSync(target); + return true; + } catch (error) { + if (error.code === 'ENOENT') return false; + throw error; + } +} + +function resolvePhysicalDirectory(directory) { + const requested = resolve(directory); + const ancestor = existingAncestor(requested); + return { requested, resolved: resolve(realpathSync(ancestor), relative(ancestor, requested)) }; +} + +function assertExternalDirectory({ requested, resolved }, repositoryRoot) { + for (const protectedRoot of trackedWorktreeRoots(repositoryRoot)) { + if (isWithin(resolved, protectedRoot)) throw refusal(requested, protectedRoot); + } +} + +export function createDashboardArtifactDirectory({ + environment = process.env, + repositoryRoot, + temporaryRoot = tmpdir(), +} = {}) { + if (!repositoryRoot) throw new Error('repositoryRoot is required for dashboard artifact isolation'); + const requestedDirectory = environment.TORCH_CHECK_ARTIFACT_DIR; + if (!requestedDirectory) { + const temporary = resolvePhysicalDirectory(temporaryRoot); + assertExternalDirectory(temporary, repositoryRoot); + const directory = mkdtempSync(join(temporary.resolved, 'torch-dashboard-artifacts-')); + return { directory, provenance: 'standalone-temporary', requestedDirectory: null }; + } + + const output = resolvePhysicalDirectory(requestedDirectory); + assertExternalDirectory(output, repositoryRoot); + mkdirSync(output.resolved, { recursive: true }); + return { directory: output.resolved, provenance: 'TORCH_CHECK_ARTIFACT_DIR', requestedDirectory: output.requested }; +} + +export function prepareDashboardArtifactTargets(artifacts, names) { + if (new Set(names).size !== names.length || names.some((name) => name !== basename(name))) { + throw new Error('Dashboard screenshot names must be unique plain filenames'); + } + const paths = {}; + for (const name of names) { + const target = join(artifacts.directory, name); + if (targetExists(target)) { + const error = new Error(`DASHBOARD_ARTIFACT_TARGET_EXISTS: refusing to replace ${target}`); + error.code = 'DASHBOARD_ARTIFACT_TARGET_EXISTS'; + throw error; + } + paths[name] = target; + } + return Object.freeze(paths); +} + +function artifactFiles(directory, prefix = '') { + return readdirSync(directory, { withFileTypes: true }) + .flatMap((entry) => { + const path = join(directory, entry.name); + const relativePath = join(prefix, entry.name); + if (entry.isDirectory()) return artifactFiles(path, relativePath); + if (!entry.isFile()) return []; + const bytes = readFileSync(path); + return [{ path: relativePath, bytes: bytes.length, + sha256: createHash('sha256').update(bytes).digest('hex') }]; + }) + .sort((left, right) => left.path.localeCompare(right.path)); +} + +export function describeDashboardArtifacts(artifacts) { + return { + kind: 'dashboard-artifacts', + directory: artifacts.directory, + provenance: artifacts.provenance, + requestedDirectory: artifacts.requestedDirectory, + files: artifactFiles(artifacts.directory), + }; +} diff --git a/scripts/test-dashboard-demo.mjs b/scripts/test-dashboard-demo.mjs index a36f3d8..258b330 100644 --- a/scripts/test-dashboard-demo.mjs +++ b/scripts/test-dashboard-demo.mjs @@ -1,13 +1,43 @@ import assert from 'node:assert/strict'; -import { mkdirSync } from 'node:fs'; import { once } from 'node:events'; import { chromium } from '@playwright/test'; -import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { createConsoleServer } from '../src/console/server.mjs'; +import { createDashboardArtifactDirectory, describeDashboardArtifacts, prepareDashboardArtifactTargets } from './dashboard-artifacts.mjs'; -const screenshotDirectory = process.env.TORCH_CHECK_ARTIFACT_DIR || 'reports/design-system'; +const screenshotNames = [ + 'torch-dashboard-operation-outcomes.png', 'torch-dashboard-activity-review.png', + 'torch-dashboard-check-evidence.png', 'torch-dashboard-owner-briefing-desktop.png', + 'torch-dashboard-pilot-conclusion.png', 'torch-dashboard-pilot-review.png', + 'torch-dashboard-manager-wakes.png', 'torch-dashboard-demo-desktop.png', + 'torch-dashboard-demo-mobile.png', 'torch-dashboard-owner-briefing-mobile.png', + 'torch-dashboard-live-refresh.png', +]; +const artifacts = createDashboardArtifactDirectory({ + repositoryRoot: fileURLToPath(new URL('..', import.meta.url)), +}); +const screenshotPaths = prepareDashboardArtifactTargets(artifacts, screenshotNames); function screenshotPath(name) { - return join(screenshotDirectory, name); + return screenshotPaths[name]; +} + +async function selectView(page, view) { + const link = page.locator(`.console-rail a[href="#${view}"]`); + assert.equal(await link.count(), 1, `the ${view} view must have one supported fragment link`); + if (await link.getAttribute('aria-current') !== 'page') await link.click(); + const viewSections = page.locator(`.console-workspace > [data-console-view="${view}"]`); + const visibleSections = page.locator(`.console-workspace > [data-console-view="${view}"]:not([hidden])`); + const sectionCount = await viewSections.count(); + assert.ok(sectionCount > 0, `${view} must have a fragment-addressable workspace section`); + await visibleSections.first().waitFor({ state: 'visible' }); + assert.equal(await visibleSections.count(), sectionCount, `all ${view} sections must be visible together`); +} + +async function selectViewFor(page, selector) { + const view = await page.locator(selector).first().evaluate((element) => + element.closest('[data-console-view]')?.dataset.consoleView); + assert.ok(view, `${selector} must belong to a fragment-addressable view`); + await selectView(page, view); } const server = createConsoleServer(); @@ -33,21 +63,25 @@ try { await page.waitForLoadState(); await page.locator('#project-name').filter({ hasText: 'Northstar' }).waitFor(); assert.equal(await page.title(), 'TORCH Dashboard'); + await selectView(page, 'work'); assert.equal(await page.locator('.agent-row').count(), 6); assert.equal(await page.locator('.task-ticket').count(), 5); assert.equal(await page.getByRole('region', { name: 'Queue', exact: true }).locator('.task-ticket').count(), 2); assert.equal(await page.locator('#demo-banner').isVisible(), true); + await selectViewFor(page, '#operation-outcomes'); assert.match(await page.locator('#operation-outcomes').innerText(), /Outcome unknown — review required/); assert.equal(await page.locator('#operation-outcomes button').count(), 0); await page.locator('#operation-outcomes .check-evidence').last().locator('summary').click(); assert.match(await page.locator('#operation-outcomes').innerText(), /transient failure.*effects unknown/s); - mkdirSync(screenshotDirectory, { recursive: true }); + await selectViewFor(page, '#operation-outcomes'); await page.locator('#operation-outcomes').screenshot({ path: screenshotPath('torch-dashboard-operation-outcomes.png') }); + await selectViewFor(page, '#activity-review'); await page.locator('#activity-review > summary').click(); assert.match(await page.locator('#activity-review').innerText(), /4 days without a named commit/); assert.match(await page.locator('#activity-review').innerText(), /waiting may be expected/); - mkdirSync(screenshotDirectory, { recursive: true }); + await selectViewFor(page, '#activity-review'); await page.locator('#activity-review').screenshot({ path: screenshotPath('torch-dashboard-activity-review.png') }); + await selectViewFor(page, '#check-list'); assert.match(await page.locator('#check-list').innerText(), /Running — completion unconfirmed/); await page.locator('#check-list .check-evidence').filter({ hasText: 'interrupted-browser' }).locator('summary').click(); assert.match(await page.locator('#check-list').innerText(), /Abandoned — not qualified/); @@ -57,14 +91,16 @@ try { await page.locator('#check-list .check-evidence').filter({ hasText: 'recovery-scenario' }).locator('summary').click(); assert.match(await page.locator('#check-list').innerText(), /clockMoving.*true/s); assert.match(await page.locator('#check-list').innerText(), /not independent sensor truth/); - mkdirSync(screenshotDirectory, { recursive: true }); + await selectViewFor(page, '#check-list'); await page.locator('#check-list').screenshot({ path: screenshotPath('torch-dashboard-check-evidence.png') }); + await selectViewFor(page, '#owner-briefing'); assert.match(await page.locator('#owner-briefing').innerText(), /Choose the project home layout/); assert.match(await page.locator('#owner-briefing').innerText(), /not independent live verification/); await page.locator('#owner-briefing .briefing-detail').first().locator('summary').click(); assert.match(await page.locator('#owner-briefing').innerText(), /Landed — not necessarily deployed/); - mkdirSync(screenshotDirectory, { recursive: true }); + await selectViewFor(page, '#owner-briefing'); await page.locator('#owner-briefing').screenshot({ path: screenshotPath('torch-dashboard-owner-briefing-desktop.png') }); + await selectViewFor(page, '#attention-list'); assert.match(await page.locator('#attention-list').innerText(), /manager launch needs inspection/); await page.locator('#attention-list a[href="#manager-wakes"]').click(); assert.equal(await page.locator('#manager-wakes').isVisible(), true); @@ -73,6 +109,7 @@ try { assert.equal(await page.locator('#manager-wake-list button').count(), 0, 'uncertain launches must not offer automatic restart or unlocking'); const pilotPanel = page.locator('.organization-proposal').filter({ hasText: 'Coordination pilot' }).locator('.pilot-review-history'); + await selectViewFor(page, '.organization-proposal'); await pilotPanel.locator(':scope > summary').click(); assert.match(await pilotPanel.innerText(), /Recommendation: inconclusive/); assert.match(await pilotPanel.innerText(), /reviewer-reported, not independently verified/); @@ -99,12 +136,13 @@ try { await conclusion.locator('[data-review-confirm]').waitFor(); await conclusion.getByText('Exact proposal and evidence in this decision', { exact: true }).click(); assert.match(await conclusion.locator('[data-review-scope]').innerText(), /schedulesRetained/); - mkdirSync(screenshotDirectory, { recursive: true }); + await selectViewFor(page, '[data-organization-review][data-type="hierarchy-conclusion"]'); await conclusion.screenshot({ path: screenshotPath('torch-dashboard-pilot-conclusion.png') }); await conclusion.locator('[data-review-confirm]').click(); await page.locator('#organization-action-status').filter({ hasText: 'recorded as reversed' }).waitFor(); assert.equal(await page.locator('.agent-row').count(), 6); + await selectViewFor(page, '.task-ticket'); const ticket = page.locator('.task-ticket').filter({ hasText: 'API-08' }); await ticket.locator('.priority-change > summary').click(); await ticket.locator('[name="priority"]').selectOption('urgent'); @@ -115,6 +153,7 @@ try { await ticket.locator('[data-priority-confirm]').click(); await ticket.locator('.priority-urgent').waitFor(); + await selectViewFor(page, '.owner-request-composer'); await page.locator('.owner-request-composer > summary').click(); await page.locator('#owner-request-recipient').selectOption('qa'); await page.locator('#owner-request-body').fill('Please review the home layout.'); @@ -123,6 +162,7 @@ try { await page.locator('[data-owner-request-confirm]').click(); await page.locator('#message-list .conversation-item').filter({ hasText: 'Please review the home layout.' }).waitFor(); + await selectViewFor(page, '.artifact-feedback-form'); const feedback = page.locator('.artifact-feedback-form').first(); await feedback.locator('textarea').fill('Give active work more room.'); await feedback.getByRole('button', { name: 'Review feedback' }).click(); @@ -130,6 +170,7 @@ try { await feedback.locator('[data-feedback-confirm]').click(); await page.locator('.artifact-feedback blockquote').filter({ hasText: 'Give active work more room.' }).waitFor(); + await selectViewFor(page, '.runtime-profile-tools'); await page.locator('.runtime-profile-tools > summary').click(); await page.locator('#runtime-profile-area').selectOption('interface'); await page.locator('#runtime-profile-runtime').selectOption('claude'); @@ -139,14 +180,17 @@ try { await page.locator('[data-runtime-profile-confirm]').click(); await page.locator('.agent-row').filter({ hasText: 'interface · claude / sonnet' }).waitFor(); + await selectViewFor(page, '.approval-decision-form'); await page.locator('.approval-decision-form [name="decision"]').selectOption('approved'); await page.locator('[data-approval-preview]').click(); await page.locator('[data-approval-confirm]').waitFor(); await page.locator('[data-approval-confirm]').click(); await page.getByText('No open approval requests.', { exact: true }).waitFor(); + await selectViewFor(page, '#owner-briefing'); assert.match(await page.locator('#owner-briefing').innerText(), /Choose the project home layout/, 'published briefing is historical even when live approval state changes'); + await selectViewFor(page, '.organization-review'); await page.locator('.organization-review > summary').click(); await page.locator('[data-organization-review] [name="action"]').selectOption('approve'); await page.locator('[data-review-preview]').click(); @@ -155,11 +199,15 @@ try { await page.locator('[data-review-confirm]').click(); await page.locator('#organization-action-status').filter({ hasText: 'recorded as approved' }).waitFor(); - mkdirSync(screenshotDirectory, { recursive: true }); + await selectViewFor(page, '.pilot-review-history'); await pilotPanel.locator(':scope > summary').click(); + await selectViewFor(page, '.pilot-review-history'); await pilotPanel.screenshot({ path: screenshotPath('torch-dashboard-pilot-review.png') }); + await selectViewFor(page, '#manager-wakes'); await page.locator('#manager-wakes').screenshot({ path: screenshotPath('torch-dashboard-manager-wakes.png') }); + await selectView(page, 'overview'); await page.evaluate(() => scrollTo(0, 0)); + await selectView(page, 'overview'); await page.screenshot({ path: screenshotPath('torch-dashboard-demo-desktop.png') }); await page.setViewportSize({ width: 390, height: 844 }); // Let responsive styles and layout finish before measuring the new viewport. @@ -171,7 +219,9 @@ try { const bounds = element.getBoundingClientRect(); return { tag: element.tagName, id: element.id, className: element.className, right: bounds.right, width: bounds.width }; }).filter((element) => element.right > innerWidth + 1).slice(-30) })))); + await selectView(page, 'overview'); await page.screenshot({ path: screenshotPath('torch-dashboard-demo-mobile.png') }); + await selectView(page, 'owner-briefing'); await page.locator('.console-rail a[href="#owner-briefing"]').click(); const briefingHeading = await page.locator('#owner-briefing-title').boundingBox(); const stickyRail = await page.locator('.console-rail').boundingBox(); @@ -181,6 +231,7 @@ try { await page.locator('#owner-briefing .briefing-detail').first().locator('summary').click(); assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false, 'expanded briefing evidence must fit mobile width'); await page.locator('#reset-demo').click(); + await selectViewFor(page, '#approval-request-count'); await page.locator('#approval-request-count').filter({ hasText: '1 open' }).waitFor(); assert.equal(await page.locator('.priority-urgent').count(), 0); assert.equal(await page.locator('.agent-row').count(), 6); @@ -189,6 +240,7 @@ try { await livePage.clock.install({ time: new Date('2026-09-30T12:00:00Z') }); await livePage.goto(base + '/console.html?demo=1'); await livePage.locator('#live-refresh-status[data-generation="1"]').waitFor(); + await selectView(livePage, 'fleet'); for (const model of ['external-model-1', 'external-model-2']) { await livePage.evaluate(async (model) => { const payload = { areaId: document.querySelector('#runtime-profile-area').value, runtime: 'codex', model, reasoning: 'high' }; @@ -200,10 +252,13 @@ try { } await livePage.reload(); await livePage.locator('#live-refresh-status[data-generation="1"]').waitFor(); + await selectView(livePage, 'fleet'); await livePage.locator('.runtime-profile-tools > summary').click(); await livePage.locator('#runtime-profile-model').fill('owner-draft-model'); + await selectViewFor(livePage, '#task-create-form'); await livePage.locator('#task-create-form').locator('..').locator(':scope > summary').click(); await livePage.locator('#task-create-form [name="title"]').fill('My unfinished task'); + await selectViewFor(livePage, '.approval-decision-form'); await livePage.locator('.approval-decision-form [name="decision"]').selectOption('approved'); await livePage.locator('[data-approval-preview]').click(); await livePage.locator('[data-approval-confirm]').waitFor(); @@ -236,18 +291,100 @@ try { await livePage.locator('#live-refresh-status[data-generation="5"]').waitFor(); await livePage.clock.runFor(15_000); await livePage.locator('#live-refresh-status[data-generation="6"]').waitFor(); + await selectViewFor(livePage, '.approval-decision-form'); await livePage.locator('[data-approval-confirm]').click(); await livePage.locator('#approval-request-count').filter({ hasText: '0 open' }).waitFor(); assert.equal(await livePage.locator('#runtime-profile-model').inputValue(), 'owner-draft-model', 'completing an approval must not clear another form'); assert.equal(await livePage.locator('#task-create-form [name="title"]').inputValue(), 'My unfinished task'); await livePage.locator('#refresh-console').click(); assert.equal(await livePage.locator('#runtime-profile-model').inputValue(), 'owner-draft-model'); + await selectView(livePage, 'flow-watch'); await livePage.screenshot({ path: screenshotPath('torch-dashboard-live-refresh.png') }); await livePage.close(); + + const stalePage = await context.newPage(); + await stalePage.clock.install({ time: new Date('2026-10-01T12:00:00.000Z') }); + await stalePage.goto(base + '/console.html?demo=1#work'); + await stalePage.locator('#live-refresh-status[data-generation="1"]').waitFor(); + await selectView(stalePage, 'work'); + const staleTicket = stalePage.locator('.task-ticket').filter({ hasText: 'API-08' }); + const staleForm = staleTicket.locator('.priority-change-form'); + const revisionN = Number(await staleForm.getAttribute('data-revision')); + await staleTicket.locator('.priority-change > summary').click(); + await staleForm.locator('[name="priority"]').selectOption('urgent'); + await staleForm.locator('[name="reason"]').fill('Review this change against the current task evidence.'); + await staleForm.getByRole('button', { name: 'Review change', exact: true }).click(); + await staleForm.locator('[data-priority-confirm]').waitFor(); + assert.equal(await staleForm.locator('[data-priority-preview]').locator('dd').nth(2).innerText(), String(revisionN)); + const staleToken = await staleForm.getAttribute('data-preview-token'); + const revisionN1 = await stalePage.evaluate(async (expectedRevision) => { + const path = '/api/backlog/tasks/API-08/priority'; + const change = { priority: 'low', reason: 'A separate confirmed sample change establishes newer task evidence.', expectedRevision }; + const preview = await globalThis.TorchConsoleDemo.request(`${path}/preview`, change); + const applied = await globalThis.TorchConsoleDemo.request(path, { ...change, token: preview.token, planHash: preview.planHash }); + return applied.task.revision; + }, revisionN); + assert.equal(revisionN1, revisionN + 1, 'new authoritative task evidence must advance from revision N to N+1'); + await selectView(stalePage, 'overview'); + await selectView(stalePage, 'work'); + await stalePage.locator('#refresh-console').click(); + await stalePage.locator('#live-refresh-status[data-generation="2"]').waitFor(); + const refreshedTicket = stalePage.locator('.task-ticket').filter({ hasText: 'API-08' }); + const refreshedForm = refreshedTicket.locator('.priority-change-form'); + assert.equal(Number(await refreshedForm.getAttribute('data-revision')), revisionN1); + assert.equal(await refreshedTicket.locator('.priority').textContent(), 'low'); + assert.equal(await refreshedForm.getAttribute('data-preview-token'), null, 'revision N preview token must be discarded'); + assert.equal(await refreshedForm.locator('[data-priority-preview]').isVisible(), false, + 'revision N preview must not remain presented after revision N+1 is refreshed'); + assert.equal(await refreshedForm.locator('[data-priority-confirm]').count(), 0, + 'the stale revision N preview cannot be confirmed as current'); + assert.equal(await refreshedForm.locator('[name="priority"]').inputValue(), 'urgent', 'the unsent priority choice must survive invalidation'); + assert.equal(await refreshedForm.locator('[name="reason"]').inputValue(), 'Review this change against the current task evidence.', + 'the unsent reason must survive invalidation'); + assert.equal(await refreshedForm.locator('[name="priority"]').isDisabled(), false, 'the preserved priority choice remains editable'); + assert.equal(await refreshedForm.locator('[name="reason"]').isDisabled(), false, 'the preserved reason remains editable'); + assert.match(await refreshedForm.locator('.priority-change-status').innerText(), new RegExp(`revision ${revisionN1}`), + 'the visible status identifies current task evidence'); + await selectView(stalePage, 'overview'); + await selectView(stalePage, 'work'); + await stalePage.locator('#refresh-console').click(); + await stalePage.locator('#live-refresh-status[data-generation="3"]').waitFor(); + const preservedForm = stalePage.locator('.priority-change-form[data-task-id="API-08"]'); + const preservedTicket = preservedForm.locator('xpath=../..'); + assert.equal(Number(await preservedForm.getAttribute('data-revision')), revisionN1); + assert.equal(await preservedTicket.locator('.priority').textContent(), 'low'); + assert.equal(await preservedForm.getAttribute('data-preview-token'), null); + assert.equal(await preservedForm.locator('[data-priority-preview]').isVisible(), false); + assert.equal(await preservedForm.locator('[data-priority-confirm]').count(), 0); + assert.equal(await preservedForm.locator('[name="priority"]').inputValue(), 'urgent'); + assert.equal(await preservedForm.locator('[name="reason"]').inputValue(), 'Review this change against the current task evidence.'); + assert.equal(await preservedForm.locator('[name="priority"]').isDisabled(), false); + assert.equal(await preservedForm.locator('[name="reason"]').isDisabled(), false); + const priorityDetails = preservedTicket.locator('.priority-change'); + if (!(await priorityDetails.evaluate((details) => details.open))) { + await priorityDetails.locator(':scope > summary').click(); + } + await preservedForm.getByRole('button', { name: 'Review change', exact: true }).click(); + await preservedForm.locator('[data-priority-confirm]').waitFor(); + assert.equal(await preservedForm.locator('[data-priority-preview]').locator('dd').nth(2).innerText(), String(revisionN1)); + assert.notEqual(await preservedForm.getAttribute('data-preview-token'), staleToken, + 'a current preview must be regenerated from revision N+1'); + const currentToken = await preservedForm.getAttribute('data-preview-token'); + await stalePage.locator('#refresh-console').click(); + await stalePage.locator('#live-refresh-status[data-generation="4"]').waitFor(); + const currentForm = stalePage.locator('.priority-change-form[data-task-id="API-08"]'); + assert.equal(await currentForm.getAttribute('data-preview-token'), currentToken, + 'unchanged current evidence must retain its active preview'); + assert.equal(await currentForm.locator('[data-priority-preview]').isVisible(), true); + assert.equal(await currentForm.locator('[data-priority-confirm]').isVisible(), true); + assert.equal(await currentForm.locator('[name="priority"]').inputValue(), 'urgent'); + assert.equal(await currentForm.locator('[name="reason"]').inputValue(), 'Review this change against the current task evidence.'); + await stalePage.close(); assert.deepEqual(errors, []); assert.deepEqual(apiRequests, [], 'demo must never contact live project APIs'); console.log('PASS: real Console demo; separate tab; priority, messages, feedback, profiles, approval and organization review; reset; mobile layout; zero live API calls.'); } finally { await browser?.close(); await new Promise((resolve) => server.close(resolve)); + console.log(JSON.stringify(describeDashboardArtifacts(artifacts))); } diff --git a/site/attention-actions.js b/site/attention-actions.js new file mode 100644 index 0000000..f89e8b7 --- /dev/null +++ b/site/attention-actions.js @@ -0,0 +1,25 @@ +(function (global) { + function prepareOwnerDecisionShortcut(form, decision, beginPreview) { + if (!['approved', 'rejected'].includes(decision)) { + return { started: false, reason: 'Choose approve or reject from an owner-only shortcut.' }; + } + const choice = form?.elements?.namedItem('decision'); + if (!form || !choice) { + return { started: false, reason: 'This owner decision form is no longer available. Refresh the current evidence before acting.' }; + } + if (form.dataset.previewToken || choice.disabled) { + return { started: false, reason: 'An existing decision preview is preserved. Review or edit it in Flow watch before using another shortcut.' }; + } + if (choice.value && choice.value !== decision) { + return { started: false, reason: 'Your existing decision draft is preserved. Review or change it in Flow watch before choosing another shortcut.' }; + } + if (typeof beginPreview !== 'function') { + return { started: false, reason: 'The guarded decision preview is unavailable. Refresh before acting.' }; + } + choice.value = decision; + beginPreview?.(form); + return { started: true }; + } + + global.TorchAttentionActions = Object.freeze({ prepareOwnerDecisionShortcut }); +})(globalThis); diff --git a/site/attention-projection.js b/site/attention-projection.js new file mode 100644 index 0000000..588f913 --- /dev/null +++ b/site/attention-projection.js @@ -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); diff --git a/site/console.html b/site/console.html index bba7b05..3ecc697 100644 --- a/site/console.html +++ b/site/console.html @@ -14,6 +14,8 @@ + + @@ -41,14 +43,19 @@ Organization Evidence Release gates -
Local project view
—
—
+ Branch —
+ Waiting for the first snapshot.
+ -Project pulse
TORCH is gathering the latest repository and fleet evidence.
@@ -65,12 +72,12 @@ -Reading the latest local publication…
Approvals and reporting lines
See which teams need attention and who owns each decision. A configured cadence does not confirm that a system timer is installed.
Authoritative backlog
Derived from the single backlog
Progress is calculated from each linked task's current state. It does not mean checks passed, integration landed, or a release is ready.
Persistent identities
Current approved structure
Owner review
Durable coordination
Provenance first
Separate authority gates
${escapeHtml(task.id)}${escapeHtml(task.priority ?? 'normal')}${escapeHtml(detail || 'No task description recorded.')}
+${escapeHtml(task.id)}${escapeHtml(staleEvidence.recommendation)}