Merge commit 'e8c4c4249e49d91393394ac205d918826ddd1891' into torch/work-integration
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 |
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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),
|
||||
};
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
@@ -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>
|
||||
@@ -41,14 +43,19 @@
|
||||
<a href="#organization">Organization</a>
|
||||
<a href="#evidence">Evidence</a>
|
||||
<a href="#delivery">Release gates</a>
|
||||
<p class="rail-note">Local project view<br><span id="project-commit">—</span></p>
|
||||
<details class="rail-provenance">
|
||||
<summary>Snapshot provenance</summary>
|
||||
<span>Commit <code id="project-commit">—</code></span>
|
||||
<span>Branch <code id="project-branch-detail">—</code></span>
|
||||
</details>
|
||||
</nav>
|
||||
|
||||
<div class="console-workspace">
|
||||
<p class="console-error" id="console-error" role="alert" hidden></p>
|
||||
<p id="live-refresh-status" role="status" aria-live="polite">Waiting for the first snapshot.</p>
|
||||
<p id="console-view-status" class="screen-reader-only" role="status" aria-live="polite"></p>
|
||||
|
||||
<section id="overview" class="pulse-zone" aria-labelledby="pulse-title">
|
||||
<section id="overview" class="pulse-zone" data-console-view="overview" aria-labelledby="pulse-title">
|
||||
<div class="pulse-main">
|
||||
<div class="pulse-heading"><div><p class="section-kicker">Project pulse</p><h1 id="pulse-title">Work in motion</h1></div><span id="fleet-health" class="health-flag">Observing</span></div>
|
||||
<p id="project-summary" class="pulse-summary">TORCH is gathering the latest repository and fleet evidence.</p>
|
||||
@@ -65,12 +72,12 @@
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section id="owner-briefing" class="console-section owner-briefing" aria-labelledby="owner-briefing-title">
|
||||
<section id="owner-briefing" class="console-section owner-briefing" data-console-view="owner-briefing" aria-labelledby="owner-briefing-title">
|
||||
<div class="section-heading"><h2 id="owner-briefing-title">Owner briefing</h2><span>Published evidence</span></div>
|
||||
<div id="owner-briefing-content"><p>Reading the latest local publication…</p></div>
|
||||
</section>
|
||||
|
||||
<section id="flow-watch" class="console-section flow-watch-section" aria-labelledby="flow-watch-title">
|
||||
<section id="flow-watch" class="console-section flow-watch-section" data-console-view="flow-watch" aria-labelledby="flow-watch-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Approvals and reporting lines</p><h2 id="flow-watch-title">Flow watch</h2></div><span>Owner decisions gated</span></div>
|
||||
<p class="flow-watch-note">See which teams need attention and who owns each decision. A configured cadence does not confirm that a system timer is installed.</p>
|
||||
<div class="flow-watch-grid">
|
||||
@@ -86,7 +93,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="work" class="board-section" aria-labelledby="work-title">
|
||||
<section id="work" class="board-section" data-console-view="work" aria-labelledby="work-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Authoritative backlog</p><h2 id="work-title">Work board</h2></div><span id="task-count">0 active</span></div>
|
||||
<div class="work-view-tools" role="group" aria-label="Backlog views and filters">
|
||||
<div class="work-filter-row">
|
||||
@@ -159,13 +166,13 @@
|
||||
<p id="task-create-action-status" class="priority-action-status" role="status" aria-live="polite" hidden></p>
|
||||
</section>
|
||||
|
||||
<section id="initiative-progress" class="console-section initiative-section" aria-labelledby="initiative-title">
|
||||
<section id="initiative-progress" class="console-section initiative-section" data-console-view="initiative-progress" aria-labelledby="initiative-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Derived from the single backlog</p><h2 id="initiative-title">Feature and milestone progress</h2></div><span>Task states only · gates remain separate</span></div>
|
||||
<p class="initiative-note">Progress is calculated from each linked task's current state. It does not mean checks passed, integration landed, or a release is ready.</p>
|
||||
<div id="initiative-rollup" class="initiative-rollup" aria-live="polite"></div>
|
||||
</section>
|
||||
|
||||
<section id="fleet" class="console-section fleet-section" aria-labelledby="fleet-title">
|
||||
<section id="fleet" class="console-section fleet-section" data-console-view="fleet" aria-labelledby="fleet-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Persistent identities</p><h2 id="fleet-title">Fleet and ownership</h2></div><span id="agent-health-note">Session state from latest observation</span></div>
|
||||
<div class="fleet-columns">
|
||||
<article class="work-surface"><div class="panel-heading"><h3>Sessions</h3><span>One context per identity</span></div><div id="agent-list" class="agent-list"></div>
|
||||
@@ -194,19 +201,19 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="organization" class="console-section organization-section" aria-labelledby="organization-title">
|
||||
<section id="organization" class="console-section organization-section" data-console-view="organization" aria-labelledby="organization-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Current approved structure</p><h2 id="organization-title">Organization map</h2></div><span id="organization-revision">No active graph</span></div>
|
||||
<div id="organization-map" class="organization-map" aria-live="polite"></div>
|
||||
<div id="organization-lifecycle" class="organization-lifecycle" aria-live="polite"></div>
|
||||
</section>
|
||||
|
||||
<section id="organization-proposals" class="console-section" aria-labelledby="proposal-title">
|
||||
<section id="organization-proposals" class="console-section" data-console-view="organization" aria-labelledby="proposal-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Owner review</p><h2 id="proposal-title">Organization proposals</h2></div><span id="change-count">0 pending</span></div>
|
||||
<p id="organization-action-status" class="organization-action-status" role="status" aria-live="polite" hidden></p>
|
||||
<div id="change-list" class="console-list"></div>
|
||||
</section>
|
||||
|
||||
<section id="communications" class="console-section communication-section" aria-labelledby="communications-title">
|
||||
<section id="communications" class="console-section communication-section" data-console-view="communications" aria-labelledby="communications-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Durable coordination</p><h2 id="communications-title">Conversations</h2></div><span id="message-count">0 unacknowledged</span></div>
|
||||
<details class="owner-request-composer">
|
||||
<summary>Request work from an agent</summary>
|
||||
@@ -242,7 +249,7 @@
|
||||
<div id="message-list" class="conversation-stream"></div>
|
||||
</section>
|
||||
|
||||
<section id="evidence" class="console-section evidence-section" aria-labelledby="evidence-title">
|
||||
<section id="evidence" class="console-section evidence-section" data-console-view="evidence" aria-labelledby="evidence-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Provenance first</p><h2 id="evidence-title">Evidence and review</h2></div><span>Exact commit receipts</span></div>
|
||||
<div class="evidence-columns">
|
||||
<article class="work-surface"><div class="panel-heading"><h3>Checks</h3><span>Passes are commit-bound</span></div><div id="check-list" class="console-list"></div></article>
|
||||
@@ -250,7 +257,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="delivery" class="console-section delivery-section" aria-labelledby="delivery-title">
|
||||
<section id="delivery" class="console-section delivery-section" data-console-view="delivery" aria-labelledby="delivery-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Separate authority gates</p><h2 id="delivery-title">Integration and release</h2></div><span>Integration is not deployment</span></div>
|
||||
<div class="delivery-columns">
|
||||
<article class="work-surface"><div class="panel-heading"><h3>Integration</h3><span>FIFO queue · source commit</span></div><div id="integration-list" class="console-list"></div></article>
|
||||
@@ -259,7 +266,7 @@
|
||||
<div id="operation-outcomes"></div>
|
||||
</section>
|
||||
|
||||
<details class="console-section more-details">
|
||||
<details id="fleet-operations" class="console-section more-details" data-console-view="fleet">
|
||||
<summary>Operations, resources, and decisions</summary>
|
||||
<div class="operations-grid">
|
||||
<article class="work-surface"><div class="panel-heading"><h3>Worktrees</h3><span>Branches and recoverability</span></div><div id="worktree-list" class="console-list"></div></article>
|
||||
|
||||
@@ -1,4 +1,148 @@
|
||||
const consoleViewDefinitions = Object.freeze({
|
||||
overview: { title: 'Overview' },
|
||||
'owner-briefing': { title: 'Briefing' },
|
||||
'flow-watch': { title: 'Flow watch' },
|
||||
work: { title: 'Work' },
|
||||
'initiative-progress': { title: 'Progress' },
|
||||
fleet: { title: 'Fleet' },
|
||||
communications: { title: 'Conversations' },
|
||||
organization: { title: 'Organization' },
|
||||
evidence: { title: 'Evidence' },
|
||||
delivery: { title: 'Release gates' },
|
||||
});
|
||||
|
||||
function installConsoleViewRouting() {
|
||||
const nav = document.querySelector('.console-rail');
|
||||
const sections = [...document.querySelectorAll('.console-workspace > [data-console-view]')];
|
||||
const links = [...(nav?.querySelectorAll('a[href^="#"]') ?? [])];
|
||||
if (!nav || !sections.length || !links.length) return { sync: () => 'overview' };
|
||||
|
||||
let lastHash = null;
|
||||
const initialHash = globalThis.location.hash;
|
||||
const decodeHash = (hash) => {
|
||||
try { return decodeURIComponent(String(hash ?? '').replace(/^#/, '')); } catch { return ''; }
|
||||
};
|
||||
|
||||
function resolveView(hash) {
|
||||
const fragment = decodeHash(hash);
|
||||
if (consoleViewDefinitions[fragment]) return fragment;
|
||||
const target = document.getElementById(fragment);
|
||||
const containingView = target?.closest('[data-console-view]')?.dataset.consoleView;
|
||||
return consoleViewDefinitions[containingView] ? containingView : 'overview';
|
||||
}
|
||||
|
||||
function accountForStickyNavigation(target) {
|
||||
const workspace = document.querySelector('.console-workspace');
|
||||
if (!workspace || !target) return;
|
||||
const railBounds = nav.getBoundingClientRect();
|
||||
const workspaceBounds = workspace.getBoundingClientRect();
|
||||
const overlapsWorkspace = railBounds.right > workspaceBounds.left + 1
|
||||
&& workspaceBounds.right > railBounds.left + 1;
|
||||
if (overlapsWorkspace) {
|
||||
target.style.scrollMarginTop = `${Math.ceil(railBounds.height + 8)}px`;
|
||||
} else {
|
||||
target.style.removeProperty('scroll-margin-top');
|
||||
}
|
||||
}
|
||||
|
||||
function positionNestedTarget(target, targetId, view) {
|
||||
const targetView = target?.closest('[data-console-view]')?.dataset.consoleView;
|
||||
if (!target || targetId === view || targetView !== view) return;
|
||||
for (let ancestor = target; ancestor; ancestor = ancestor.parentElement) {
|
||||
if (ancestor instanceof HTMLDetailsElement) ancestor.open = true;
|
||||
}
|
||||
accountForStickyNavigation(target);
|
||||
if (!target.hasAttribute('tabindex') && !target.matches('a[href], button, input, select, textarea, summary')) {
|
||||
target.tabIndex = -1;
|
||||
}
|
||||
target.scrollIntoView?.({ block: 'start', behavior: 'instant' });
|
||||
target.focus?.({ preventScroll: true });
|
||||
}
|
||||
|
||||
const initialFragment = decodeHash(initialHash);
|
||||
let initialPositionPending = Boolean(initialFragment && !consoleViewDefinitions[initialFragment]
|
||||
&& document.getElementById(initialFragment));
|
||||
let userIntentVersion = 0;
|
||||
const abandonInitialPosition = () => {
|
||||
initialPositionPending = false;
|
||||
userIntentVersion += 1;
|
||||
};
|
||||
const onNavigation = () => {
|
||||
abandonInitialPosition();
|
||||
sync({ focus: true });
|
||||
};
|
||||
|
||||
function sync({ focus = false, force = false } = {}) {
|
||||
const hash = globalThis.location.hash || '#overview';
|
||||
if (!force && hash === lastHash) return resolveView(hash);
|
||||
lastHash = hash;
|
||||
const view = resolveView(hash);
|
||||
const definition = consoleViewDefinitions[view];
|
||||
const visible = sections.filter((section) => section.dataset.consoleView === view);
|
||||
sections.forEach((section) => { section.hidden = section.dataset.consoleView !== view; });
|
||||
links.forEach((link) => {
|
||||
const route = resolveView(link.getAttribute('href'));
|
||||
if (route === view) link.setAttribute('aria-current', 'page');
|
||||
else link.removeAttribute('aria-current');
|
||||
});
|
||||
const workspace = document.querySelector('.console-workspace');
|
||||
if (workspace) workspace.dataset.currentView = view;
|
||||
document.title = view === 'overview' ? 'TORCH Dashboard' : `${definition.title} | TORCH Local Fleet Console`;
|
||||
const announcement = document.querySelector('#console-view-status');
|
||||
if (announcement) announcement.textContent = `Viewing ${definition.title}.`;
|
||||
|
||||
if (focus) {
|
||||
const targetId = decodeHash(hash);
|
||||
const target = document.getElementById(targetId);
|
||||
const targetView = target?.closest('[data-console-view]')?.dataset.consoleView;
|
||||
if (target && targetId !== view && targetView === view) {
|
||||
positionNestedTarget(target, targetId, view);
|
||||
} else {
|
||||
const labelledBy = visible[0]?.getAttribute('aria-labelledby');
|
||||
const heading = (labelledBy && document.getElementById(labelledBy))
|
||||
?? visible[0]?.querySelector('h1, h2');
|
||||
if (heading) {
|
||||
heading.tabIndex = -1;
|
||||
heading.focus({ preventScroll: true });
|
||||
}
|
||||
if (target && targetId === view && targetView === view) {
|
||||
accountForStickyNavigation(target);
|
||||
target.scrollIntoView?.({ block: 'start', behavior: 'instant' });
|
||||
} else {
|
||||
globalThis.scrollTo?.({ top: 0, behavior: 'instant' });
|
||||
}
|
||||
}
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
globalThis.addEventListener('hashchange', onNavigation);
|
||||
globalThis.addEventListener('popstate', onNavigation);
|
||||
globalThis.addEventListener('wheel', abandonInitialPosition, { passive: true });
|
||||
globalThis.addEventListener('touchstart', abandonInitialPosition, { passive: true });
|
||||
globalThis.addEventListener('pointerdown', abandonInitialPosition, { passive: true });
|
||||
globalThis.addEventListener('keydown', (event) => {
|
||||
if (['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp', 'Home', 'End', ' '].includes(event.key)) {
|
||||
abandonInitialPosition();
|
||||
}
|
||||
});
|
||||
sync({ focus: Boolean(initialFragment && !consoleViewDefinitions[initialFragment]) });
|
||||
return {
|
||||
sync,
|
||||
afterInitialLayout() {
|
||||
if (!initialPositionPending) return;
|
||||
initialPositionPending = false;
|
||||
if (globalThis.location.hash !== initialHash) return;
|
||||
const targetId = decodeHash(initialHash);
|
||||
const view = resolveView(initialHash);
|
||||
positionNestedTarget(document.getElementById(targetId), targetId, view);
|
||||
},
|
||||
userIntentVersion: () => userIntentVersion,
|
||||
};
|
||||
}
|
||||
|
||||
const $ = (selector) => document.querySelector(selector);
|
||||
const consoleViewRouter = installConsoleViewRouting();
|
||||
const demoWorkspace = globalThis.TorchConsoleDemo;
|
||||
const workViews = globalThis.TorchWorkViews;
|
||||
const workProgress = globalThis.TorchWorkProgress;
|
||||
@@ -13,6 +157,38 @@ refreshProtection.remember();
|
||||
let renderingSnapshot = false;
|
||||
let heldPanels = 0;
|
||||
|
||||
function invalidateStalePriorityPreviews(tasks = []) {
|
||||
const currentById = new Map(tasks.map((task) => [task.id, task]));
|
||||
for (const form of document.querySelectorAll('.priority-change-form[data-preview-token]')) {
|
||||
const task = currentById.get(form.dataset.taskId);
|
||||
const currentRevision = Number(task?.revision);
|
||||
const previewRevision = Number(form.dataset.revision);
|
||||
if (!task || !Number.isSafeInteger(currentRevision) || currentRevision === previewRevision) continue;
|
||||
|
||||
const ticket = form.closest('.task-ticket');
|
||||
if (!ticket) continue;
|
||||
const priority = task.priority ?? 'normal';
|
||||
const priorityBadge = ticket.querySelector('.ticket-topline .priority');
|
||||
if (priorityBadge) {
|
||||
priorityBadge.className = `priority priority-${escapeHtml(priority)}`;
|
||||
priorityBadge.textContent = priority;
|
||||
}
|
||||
form.dataset.revision = String(currentRevision);
|
||||
delete form.dataset.previewToken;
|
||||
delete form.dataset.planHash;
|
||||
const priorityField = form.querySelector('[name="priority"]');
|
||||
const reasonField = form.querySelector('[name="reason"]');
|
||||
if (priorityField) priorityField.disabled = false;
|
||||
if (reasonField) reasonField.disabled = false;
|
||||
const preview = form.querySelector('[data-priority-preview]');
|
||||
if (preview) {
|
||||
preview.replaceChildren();
|
||||
preview.hidden = true;
|
||||
}
|
||||
priorityStatus(form, `Task evidence advanced to revision ${currentRevision}. The older preview was cleared; review the current task before previewing again.`);
|
||||
}
|
||||
}
|
||||
|
||||
const escapeHtml = (value) => {
|
||||
const element = document.createElement('span');
|
||||
element.textContent = value ?? '';
|
||||
@@ -45,6 +221,55 @@ function setText(selector, value) {
|
||||
if (element) element.textContent = value ?? '';
|
||||
}
|
||||
|
||||
function compactOwnerBriefingProvenance() {
|
||||
const publication = $('#owner-briefing-content')?.querySelector('.briefing-publication');
|
||||
if (!publication) return;
|
||||
|
||||
const timestamp = publication.querySelector('p time');
|
||||
const window = [...publication.querySelectorAll('.briefing-muted')]
|
||||
.find((paragraph) => paragraph.textContent.trim().startsWith('Window:'));
|
||||
if (timestamp || window) {
|
||||
const disclosure = document.createElement('details');
|
||||
disclosure.className = 'briefing-provenance';
|
||||
const summary = document.createElement('summary');
|
||||
summary.textContent = 'Publication time and reporting window';
|
||||
disclosure.append(summary);
|
||||
if (timestamp) {
|
||||
const exactTime = timestamp.cloneNode(true);
|
||||
exactTime.textContent = timestamp.getAttribute('datetime') || timestamp.textContent;
|
||||
const published = document.createElement('p');
|
||||
published.append('Published ', exactTime);
|
||||
disclosure.append(published);
|
||||
timestamp.textContent = ageLabel(timestamp.getAttribute('datetime'));
|
||||
timestamp.title = exactTime.textContent;
|
||||
}
|
||||
if (window) {
|
||||
const limitation = 'Recorded evidence, not independent live verification.';
|
||||
if (window.textContent.includes(limitation)) {
|
||||
const visibleLimitation = window.cloneNode(false);
|
||||
visibleLimitation.textContent = limitation;
|
||||
window.textContent = window.textContent.replace(limitation, '').trim();
|
||||
window.after(visibleLimitation);
|
||||
}
|
||||
disclosure.append(window);
|
||||
}
|
||||
publication.append(disclosure);
|
||||
}
|
||||
|
||||
for (const code of publication.querySelectorAll('code')) {
|
||||
const commit = code.textContent.trim();
|
||||
if (!/^[a-f0-9]{40}$/i.test(commit)) continue;
|
||||
const disclosure = document.createElement('details');
|
||||
disclosure.className = 'briefing-provenance';
|
||||
const summary = document.createElement('summary');
|
||||
summary.textContent = `Commit ${shortCommit(commit)}`;
|
||||
const exact = document.createElement('code');
|
||||
exact.textContent = commit;
|
||||
disclosure.append(summary, exact);
|
||||
code.replaceWith(disclosure);
|
||||
}
|
||||
}
|
||||
|
||||
function shortCommit(value) {
|
||||
return value ? String(value).slice(0, 9) : '—';
|
||||
}
|
||||
@@ -69,9 +294,10 @@ function taskCard(task, healthFindings = []) {
|
||||
const staleEvidence = healthFindings.find((finding) => finding.taskId === task.id
|
||||
&& ['BACKLOG_OBSERVED_COMMIT_STALE', 'BACKLOG_OBSERVED_COMMIT_MISSING'].includes(finding.code));
|
||||
return `<article class="task-ticket">
|
||||
<div class="ticket-topline"><code>${escapeHtml(task.id)}</code><span class="priority priority-${escapeHtml(task.priority ?? 'normal')}">${escapeHtml(task.priority ?? 'normal')}</span></div>
|
||||
<div class="ticket-topline"><span class="priority priority-${escapeHtml(task.priority ?? 'normal')}">${escapeHtml(task.priority ?? 'normal')}</span></div>
|
||||
<h4>${escapeHtml(task.title ?? 'Untitled task')}</h4>
|
||||
<p>${escapeHtml(detail || 'No task description recorded.')}</p>
|
||||
<details class="task-reference"><summary>Task reference</summary><code>${escapeHtml(task.id)}</code></details>
|
||||
${staleEvidence ? `<div class="evidence-warning" role="status"><strong>Evidence needs refresh</strong><p>${escapeHtml(staleEvidence.recommendation)}</p></div>` : ''}
|
||||
<dl class="ticket-meta">
|
||||
<div><dt>Owner</dt><dd>${escapeHtml(task.owner ?? 'Unassigned')}</dd></div>
|
||||
@@ -706,7 +932,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 +951,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,11 +993,12 @@ 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');
|
||||
setText('#project-branch', branch);
|
||||
setText('#project-branch-detail', branch);
|
||||
setText('#project-commit', shortCommit(snapshot.repository?.head));
|
||||
setText('#fleet-health', snapshot.mode === 'installed'
|
||||
? (snapshot.doctor?.healthy ? 'Healthy' : 'Review findings') : 'Not installed');
|
||||
@@ -807,22 +1012,18 @@ 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');
|
||||
renderRuntimeProfileEditor(agents, snapshot.runtimeAdapters ?? [], snapshot.mode === 'installed');
|
||||
renderFlowWatch(snapshot);
|
||||
setHtml('#owner-briefing-content', globalThis.TorchOwnerDigest.render(snapshot.ownerDigest));
|
||||
compactOwnerBriefingProvenance();
|
||||
|
||||
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 +1050,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 +1107,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 ?? [];
|
||||
@@ -945,15 +1146,27 @@ const refreshScheduler = globalThis.TorchLiveRefresh.createScheduler({
|
||||
return snapshot;
|
||||
},
|
||||
apply: (snapshot) => {
|
||||
const viewportBeforeRender = appliedRefreshes > 0
|
||||
? { left: globalThis.scrollX, top: globalThis.scrollY } : null;
|
||||
const userIntentAtRender = consoleViewRouter.userIntentVersion();
|
||||
const editedBeforeRender = refreshProtection.dirty();
|
||||
heldPanels = 0;
|
||||
renderingSnapshot = true;
|
||||
try { render(snapshot); } finally { renderingSnapshot = false; }
|
||||
invalidateStalePriorityPreviews(snapshot.backlog ?? []);
|
||||
refreshProtection.remember({ preserve: editedBeforeRender });
|
||||
$('#console-error').hidden = true;
|
||||
const status = $('#live-refresh-status');
|
||||
status.dataset.generation = String(++appliedRefreshes);
|
||||
const renderGeneration = ++appliedRefreshes;
|
||||
status.textContent = `${liveUpdatesPaused ? 'Automatic updates paused.' : 'Updates every 15 seconds while visible.'}${heldPanels ? ' Edited forms and active previews retained; their panels may show older state.' : ''}`;
|
||||
globalThis.requestAnimationFrame(() => globalThis.requestAnimationFrame(() => {
|
||||
if (renderGeneration !== appliedRefreshes) return;
|
||||
if (viewportBeforeRender && consoleViewRouter.userIntentVersion() === userIntentAtRender) {
|
||||
globalThis.scrollTo?.(viewportBeforeRender.left, viewportBeforeRender.top);
|
||||
}
|
||||
if (renderGeneration === 1) consoleViewRouter.afterInitialLayout();
|
||||
status.dataset.generation = String(renderGeneration);
|
||||
}));
|
||||
},
|
||||
onError: (caught) => {
|
||||
const error = $('#console-error');
|
||||
@@ -1864,8 +2077,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;
|
||||
}
|
||||
@@ -1899,11 +2150,11 @@ globalThis.addEventListener('pageshow', (event) => {
|
||||
if (event.persisted && !liveUpdatesPaused && document.visibilityState === 'visible') refresh();
|
||||
});
|
||||
if (demoWorkspace) {
|
||||
document.title = 'TORCH Dashboard';
|
||||
$('#demo-banner').hidden = false;
|
||||
$('#reset-demo')?.addEventListener('click', () => {
|
||||
demoWorkspace.reset();
|
||||
globalThis.location.reload();
|
||||
});
|
||||
}
|
||||
consoleViewRouter.sync({ force: true });
|
||||
refresh();
|
||||
@@ -152,9 +152,19 @@ footer div { display: flex; gap: 1.5rem; }
|
||||
.console-rail { position: sticky; top: 0; align-self: start; display: grid; gap: .2rem; padding: 1.5rem .75rem; border-right: 1px solid var(--line); }
|
||||
.console-rail a { padding: .55rem .7rem; color: var(--muted); font-weight: 650; text-decoration: none; border-left: 3px solid transparent; }
|
||||
.console-rail a:hover, .console-rail a:focus-visible { color: var(--ink); background: var(--paper); border-left-color: var(--route); }
|
||||
.rail-note { margin: 2rem .7rem 0; color: var(--muted); font-size: .76rem; line-height: 1.7; }
|
||||
.rail-note span { color: var(--ink); font-family: ui-monospace, monospace; }
|
||||
.console-rail a[aria-current="page"] { color: var(--ink); background: var(--paper); border-left-color: var(--route); }
|
||||
.rail-provenance { margin: 1.5rem .7rem 0; color: var(--muted); font-size: .72rem; line-height: 1.55; overflow-wrap: anywhere; }
|
||||
.rail-provenance summary, .briefing-provenance summary, .task-reference summary { cursor: pointer; color: var(--route); font-weight: 650; }
|
||||
.rail-provenance > span { display: block; margin-top: .35rem; }
|
||||
.rail-provenance code { color: var(--ink); overflow-wrap: anywhere; }
|
||||
.console-workspace { min-width: 0; padding: 0 clamp(1rem, 2.5vw, 2.5rem) 3rem; }
|
||||
.console-workspace > [data-console-view][hidden] { display: none !important; }
|
||||
.screen-reader-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
||||
.briefing-provenance { margin-top: .4rem; color: var(--muted); font-size: .78rem; overflow-wrap: anywhere; }
|
||||
.briefing-provenance p { margin: .3rem 0; }
|
||||
.briefing-provenance code { display: inline-block; max-width: 100%; overflow-wrap: anywhere; }
|
||||
.task-reference { margin-top: .55rem; color: var(--muted); font-size: .75rem; overflow-wrap: anywhere; }
|
||||
.task-reference code { display: inline-block; margin-top: .3rem; color: var(--ink); }
|
||||
.console-error { margin: 1.2rem 0 0; background: var(--warning-surface); color: var(--warning-ink); border-left: 4px solid var(--signal); padding: 1rem; }
|
||||
.pulse-zone { display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(270px, .8fr); border-bottom: 1px solid var(--ink); }
|
||||
.pulse-main { min-width: 0; padding: 2rem 2rem 1.6rem 0; }
|
||||
@@ -173,15 +183,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; }
|
||||
@@ -563,10 +588,10 @@ footer div { display: flex; gap: 1.5rem; }
|
||||
.blueprint article { min-height: auto; border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
.blueprint-index { margin-bottom: 2rem; }
|
||||
.console-layout { grid-template-columns: 1fr; }
|
||||
.console-rail { position: sticky; z-index: 5; top: 0; display: flex; overflow-x: auto; padding: .35rem .6rem; border-right: 0; border-bottom: 1px solid var(--line); background: var(--paper); }
|
||||
.console-rail a { flex: 0 0 auto; padding: .5rem .65rem; border-left: 0; border-bottom: 3px solid transparent; }
|
||||
.console-rail a:hover, .console-rail a:focus-visible { border-left-color: transparent; border-bottom-color: var(--route); }
|
||||
.rail-note { display: none; }
|
||||
.console-rail { position: sticky; z-index: 5; top: 0; display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: .25rem; padding: .4rem .6rem; border-right: 0; border-bottom: 1px solid var(--line); background: var(--paper); }
|
||||
.console-rail a { min-width: 0; padding: .45rem .5rem; border-left: 0; border-bottom: 3px solid transparent; overflow-wrap: anywhere; }
|
||||
.console-rail a:hover, .console-rail a:focus-visible, .console-rail a[aria-current="page"] { border-left-color: transparent; border-bottom-color: var(--route); }
|
||||
.rail-provenance { grid-column: 1 / -1; margin: .25rem .5rem; }
|
||||
.console-workspace { padding-inline: clamp(1rem, 3vw, 2rem); }
|
||||
.pulse-zone { grid-template-columns: minmax(0, 1.15fr) minmax(240px, .85fr); }
|
||||
.work-filter-row { grid-template-columns: repeat(auto-fit, minmax(135px, 1fr)); }
|
||||
@@ -596,11 +621,13 @@ footer div { display: flex; gap: 1.5rem; }
|
||||
.console-nav { min-height: 62px; gap: .7rem; }
|
||||
.console-project { margin-right: 0; }
|
||||
.console-project span { display: none; }
|
||||
.console-tools > span { display: none; }
|
||||
.console-tools { flex-wrap: wrap; gap: .5rem; }
|
||||
.console-tools > span { display: inline-flex; flex-basis: 100%; }
|
||||
.console-rail { top: 0; }
|
||||
.section-heading { flex-wrap: wrap; }
|
||||
.section-heading > div, .section-heading > span { min-width: 0; overflow-wrap: anywhere; }
|
||||
.console-workspace { padding-inline: .8rem; }
|
||||
.console-rail { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.pulse-zone, .fleet-columns, .evidence-columns, .delivery-columns, .flow-watch-grid { grid-template-columns: 1fr; }
|
||||
.owner-request-fields, .runtime-profile-fields { grid-template-columns: 1fr; }
|
||||
.schedule-launcher-actions { align-items: stretch; flex-direction: column; }
|
||||
|
||||
@@ -69,6 +69,10 @@ function launchPolicyArgs(policy = {}) {
|
||||
return args;
|
||||
}
|
||||
|
||||
const MAX_STARTUP_OUTPUT_BYTES = 16 * 1024;
|
||||
const MAX_STARTUP_EVENT_COUNT = 64;
|
||||
const MAX_DIAGNOSTIC_TEXT_LENGTH = 512;
|
||||
|
||||
function parseJsonLines(output) {
|
||||
const records = [];
|
||||
for (const line of String(output ?? '').split('\n').map((value) => value.trim()).filter(Boolean)) {
|
||||
@@ -77,6 +81,91 @@ function parseJsonLines(output) {
|
||||
return records;
|
||||
}
|
||||
|
||||
function redactDiagnosticText(value, limit = MAX_DIAGNOSTIC_TEXT_LENGTH) {
|
||||
if (typeof value !== 'string') return null;
|
||||
const compact = value.replace(/\s+/g, ' ').trim();
|
||||
if (!compact) return null;
|
||||
return compact
|
||||
.replace(/\bBearer\s+[-A-Za-z0-9._~+/]+=*/gi, 'Bearer [REDACTED]')
|
||||
.replace(/((?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|token|password|secret|credential|authorization|cookie|prompt|private reasoning)\s*(?:=|:|\s)\s*)([^\s,;]+)/gi, '$1[REDACTED]')
|
||||
.replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/gi, '$1[REDACTED]@')
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
function boundedStartupOutput(output) {
|
||||
const text = typeof output === 'string' ? output : String(output ?? '');
|
||||
if (Buffer.byteLength(text, 'utf8') > MAX_STARTUP_OUTPUT_BYTES) {
|
||||
return { records: null, reason: 'stdout-exceeds-bound' };
|
||||
}
|
||||
const lines = text.split('\n').map((value) => value.trim()).filter(Boolean);
|
||||
if (lines.length > MAX_STARTUP_EVENT_COUNT) return { records: null, reason: 'stdout-too-many-events' };
|
||||
const records = [];
|
||||
for (const line of lines) {
|
||||
try { records.push(JSON.parse(line)); } catch { return { records: null, reason: 'stdout-malformed-json' }; }
|
||||
}
|
||||
return { records, reason: null };
|
||||
}
|
||||
|
||||
function startupFailureEvent(record) {
|
||||
if (!record || typeof record !== 'object' || Array.isArray(record)) return null;
|
||||
if (record.type === 'error' || record.type === 'turn.failed' || record.type === 'turn.error') {
|
||||
const error = record.error && typeof record.error === 'object' && !Array.isArray(record.error)
|
||||
? record.error : record;
|
||||
return {
|
||||
code: error.code ?? record.code,
|
||||
category: error.category ?? record.category,
|
||||
message: error.message ?? record.message,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function safeDiagnosticCode(value) {
|
||||
return typeof value === 'string' && /^[A-Z][A-Z0-9_.-]{0,79}$/.test(value) ? value : null;
|
||||
}
|
||||
|
||||
function safeDiagnosticCategory(value) {
|
||||
return ['model-rejection', 'authentication', 'quota'].includes(value) ? value : null;
|
||||
}
|
||||
|
||||
function categoryFromDiagnosticCode(code) {
|
||||
if (!code) return null;
|
||||
if (/(?:MODEL|UNSUPPORTED|UNAVAILABLE|NOT_FOUND)/.test(code)) return 'model-rejection';
|
||||
if (/(?:AUTH|UNAUTHORIZED|FORBIDDEN|CREDENTIAL)/.test(code)) return 'authentication';
|
||||
if (/(?:QUOTA|RATE_LIMIT|RATE-LIMIT|LIMIT_EXCEEDED)/.test(code)) return 'quota';
|
||||
return null;
|
||||
}
|
||||
|
||||
export function diagnoseCodexStartupFailure({ stdout, stderr } = {}) {
|
||||
const parsed = boundedStartupOutput(stdout);
|
||||
const safeStderr = redactDiagnosticText(stderr);
|
||||
if (!parsed.records) {
|
||||
return {
|
||||
schema: 'torch.dev/runtime-startup-diagnostic/v1alpha1', outcome: 'unknown', reason: parsed.reason,
|
||||
stderr: safeStderr,
|
||||
};
|
||||
}
|
||||
const failure = parsed.records.map(startupFailureEvent).find(Boolean);
|
||||
if (!failure) {
|
||||
return {
|
||||
schema: 'torch.dev/runtime-startup-diagnostic/v1alpha1', outcome: 'unknown', reason: 'stdout-no-failure-event',
|
||||
stderr: safeStderr,
|
||||
};
|
||||
}
|
||||
const message = redactDiagnosticText(failure.message);
|
||||
const code = safeDiagnosticCode(failure.code);
|
||||
const category = safeDiagnosticCategory(failure.category);
|
||||
const modelRejection = /\b(model|unsupported|not supported|unavailable|not available|does not exist)\b/i.test(message ?? '')
|
||||
|| /MODEL|UNSUPPORTED/i.test(code ?? '');
|
||||
return {
|
||||
schema: 'torch.dev/runtime-startup-diagnostic/v1alpha1', outcome: 'known',
|
||||
category: category ?? categoryFromDiagnosticCode(code) ?? (modelRejection ? 'model-rejection' : 'runtime-error'),
|
||||
...(code ? { code } : {}),
|
||||
...(message ? { message } : {}),
|
||||
stderr: safeStderr,
|
||||
};
|
||||
}
|
||||
|
||||
export class CodexRuntimeAdapter {
|
||||
constructor({
|
||||
env = process.env,
|
||||
@@ -227,6 +316,10 @@ export class CodexRuntimeAdapter {
|
||||
return { areaId: text(areaId, 'areaId'), runtimeSessionId: text(captured, 'runtimeSessionId') };
|
||||
}
|
||||
|
||||
diagnoseStartupFailure({ stdout, stderr } = {}) {
|
||||
return diagnoseCodexStartupFailure({ stdout, stderr });
|
||||
}
|
||||
|
||||
getStatus() { return unsupportedCapability(this.name, 'getStatus'); }
|
||||
|
||||
listSessions() { return unsupportedCapability(this.name, 'listSessions'); }
|
||||
|
||||
@@ -32,6 +32,7 @@ import { IntegrationService } from './integration/service.mjs';
|
||||
import { analyzeCombatrigFleet } from './importers/combatrig.mjs';
|
||||
import { proposeDomains } from './kernel/domains.mjs';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { fileHash, writeNewFile } from './kernel/files.mjs';
|
||||
import { createWorktrees, planWorktrees } from './kernel/worktrees.mjs';
|
||||
@@ -42,6 +43,23 @@ import {
|
||||
import { ResourceService } from './resources/service.mjs';
|
||||
import { CANDIDATE_ACCEPTANCE_SCENARIOS, createVersionService } from './self-host/service.mjs';
|
||||
import { observeProject } from './observability/snapshot.mjs';
|
||||
|
||||
// Provider output is evidence, not an unbounded artifact channel. startFleet
|
||||
// classifies ENOBUFS as an interrupted outcome instead of a completed turn.
|
||||
const MAX_RUNTIME_EXECUTOR_OUTPUT_BYTES = 1024 * 1024;
|
||||
const CODEX_PROTOCOL_REDUCER = fileURLToPath(new URL('./runtime/codex-protocol-reducer.mjs', import.meta.url));
|
||||
|
||||
export function executeRuntimeLaunch(spawn, launch, options = {}) {
|
||||
// Tests and adapter qualification inject a deterministic executor. The real
|
||||
// synchronous CLI boundary alone needs protocol reduction before buffering.
|
||||
// A tagged guard may delegate to native spawnSync after asserting hermetic
|
||||
// launch preconditions; it must opt in rather than changing fake executors.
|
||||
const usesNativeSpawn = spawn === spawnSync || spawn?.torchNativeSpawnGuard === true;
|
||||
if (launch.runtime === 'codex' && usesNativeSpawn) {
|
||||
return spawn(process.execPath, [CODEX_PROTOCOL_REDUCER, launch.command, ...launch.args], options);
|
||||
}
|
||||
return spawn(launch.command, launch.args, options);
|
||||
}
|
||||
import { OwnerDigestService } from './observability/owner-digest.mjs';
|
||||
import { ContextTelemetryService } from './telemetry/context.mjs';
|
||||
import { ScheduleService } from './schedules/service.mjs';
|
||||
@@ -1106,8 +1124,9 @@ export async function runCli(argv = process.argv.slice(2), {
|
||||
}
|
||||
print(startFleet({
|
||||
plan, controlPlane: control, adapters,
|
||||
executor: (launch) => spawn(launch.command, launch.args, {
|
||||
cwd: launch.cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
||||
executor: (launch) => executeRuntimeLaunch(spawn, launch, {
|
||||
cwd: launch.cwd, env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
||||
maxBuffer: MAX_RUNTIME_EXECUTOR_OUTPUT_BYTES,
|
||||
}),
|
||||
}), { json });
|
||||
return 0;
|
||||
@@ -1439,8 +1458,9 @@ export async function runCli(argv = process.argv.slice(2), {
|
||||
}
|
||||
print(startFleet({
|
||||
plan, controlPlane: control, adapters,
|
||||
executor: (launch) => spawn(launch.command, launch.args, {
|
||||
cwd: launch.cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
||||
executor: (launch) => executeRuntimeLaunch(spawn, launch, {
|
||||
cwd: launch.cwd, env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
||||
maxBuffer: MAX_RUNTIME_EXECUTOR_OUTPUT_BYTES,
|
||||
}),
|
||||
}), { json });
|
||||
} else throw new TorchError(`Unknown fleet operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
|
||||
@@ -1613,9 +1633,9 @@ export async function runCli(argv = process.argv.slice(2), {
|
||||
}
|
||||
return startFleet({
|
||||
plan, controlPlane: control, adapters,
|
||||
executor: (launch) => spawn(launch.command, launch.args, {
|
||||
cwd: launch.cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 300_000, maxBuffer: 8 * 1024 * 1024, killSignal: 'SIGTERM',
|
||||
executor: (launch) => executeRuntimeLaunch(spawn, launch, {
|
||||
cwd: launch.cwd, env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 300_000, maxBuffer: MAX_RUNTIME_EXECUTOR_OUTPUT_BYTES, killSignal: 'SIGTERM',
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -10,7 +10,7 @@ import { fileHash, writeNewFile } from './files.mjs';
|
||||
import { projectStatePath } from './paths.mjs';
|
||||
import { validateApprovedProposal } from './domains.mjs';
|
||||
import { inspectManagedWorktree } from './worktree-state.mjs';
|
||||
import { renderDomainPrompt } from './prompts.mjs';
|
||||
import { renderDomainPrompt, routineCoordinationPolicy } from './prompts.mjs';
|
||||
import { createRuntimeAdapterRegistry } from '../adapters/registry.mjs';
|
||||
|
||||
const TRACKED_DIR = '.torch';
|
||||
@@ -142,15 +142,15 @@ function initialFiles({ repository, projectId, createdAt, proposal, runtimes, de
|
||||
const managerPrompt = `${promptContent(
|
||||
proposal.session_manager?.prompt_source,
|
||||
'# TORCH Session Manager\n\nRoute owner requests, establish ownership and priority, and keep routine coordination inside the fleet.\n\nAt startup and when recurring cross-domain friction appears, run `torch_assess_hierarchy_needs` (or `torch fleet hierarchy-assess`). Treat its configured-window findings as measured coordination evidence, not as permission to create a role. Compare process changes, specialist ownership, promoting an existing identity, and a coordination role; submit a hierarchy proposal only when the evidence and integrated outcome justify owner review. Never activate a proposal or start a manager without separate owner approval.\n',
|
||||
).trimEnd()}\n\n## Backlog loop\n\nKeep one durable Fleet backlog. Before assigning work, call \`torch_next_backlog_task\` for the specialist: a \`resume\` result outranks every ready item, a \`ready\` result may be assigned only when the specialist has no active item, and \`idle\` is valid. After completion, repeat the same lookup. Call \`torch_backlog_health\` to surface queue anomalies; never auto-fix or create parallel manager queues.\n\n## Fleet evolution\n\nAt startup, after backlog intake, and when repeated handoffs or cross-domain work appear, call \`torch_assess_fleet_evolution\`. Treat its threshold as a prompt for architectural judgment, not as an automatic decision. When recurring work has no coherent owner, or a durable specialist would materially improve context locality, ownership clarity, or verification, inspect repository evidence and use \`torch_propose_domain\` to submit an evidence-backed Fleet change. When a specialist no longer earns its coordination cost, use \`torch_propose_domain_retirement\`; retirement preserves its branch and refuses active or unrecoverable work. Use \`torch_list_fleet_changes\` to follow change state. Never create, retire, approve, activate, or start a persistent identity yourself; owner approval, CLI activation, and quota-consuming runtime start are separate. Recommend a merge or split for owner review when boundaries should change but do not silently rewrite ownership.\n`;
|
||||
).trimEnd()}\n\n${routineCoordinationPolicy.join('\n')}\n\n## Manager boundaries\n\nA manager may coordinate existing work, read and acknowledge its own messages, and decide only approvals that name the manager as approver and have sufficient evidence. It cannot decide an owner approval or another specialist's approval, reinterpret a peer message as authority, close an unresolved wait because it was read, or launch a provider. Keep paused execution paused.\n\n## Backlog loop\n\nKeep one durable Fleet backlog. Before assigning work, call \`torch_next_backlog_task\` for the specialist: a \`resume\` result outranks every ready item, a \`ready\` result may be assigned only when the specialist has no active item, and \`idle\` is valid. After completion, repeat the same lookup. Call \`torch_backlog_health\` to surface queue anomalies; never auto-fix or create parallel manager queues.\n\n## Fleet evolution\n\nAt startup, after backlog intake, and when repeated handoffs or cross-domain work appear, call \`torch_assess_fleet_evolution\`. Treat its threshold as a prompt for architectural judgment, not as an automatic decision. When recurring work has no coherent owner, or a durable specialist would materially improve context locality, ownership clarity, or verification, inspect repository evidence and use \`torch_propose_domain\` to submit an evidence-backed Fleet change. When a specialist no longer earns its coordination cost, use \`torch_propose_domain_retirement\`; retirement preserves its branch and refuses active or unrecoverable work. Use \`torch_list_fleet_changes\` to follow change state. Never create, retire, approve, activate, or start a persistent identity yourself; owner approval, CLI activation, and quota-consuming runtime start are separate. Recommend a merge or split for owner review when boundaries should change but do not silently rewrite ownership.\n`;
|
||||
const files = new Map([
|
||||
['torch.yaml', jsonYaml(config)],
|
||||
['roster.yaml', jsonYaml(roster)],
|
||||
['decisions.md', '# TORCH decisions\n\nNo project decisions recorded yet.\n'],
|
||||
['RESUME-BRIEF.md', '# TORCH resume brief\n\nFleet not started yet.\n'],
|
||||
['RESUME-BRIEF.md', '# TORCH resume brief\n\nFleet not started yet. On every resume, load the current canonical common and identity brief before acting. Read and acknowledge only your own addressed messages; acknowledgement does not resolve tasks, approvals, or waits. Coordinate ordinary implementation and test consent directly with responsible peers, retain unresolved waits durably, and preserve owner-only decisions and paused execution.\n'],
|
||||
['prompts/COMMON.md', promptContent(
|
||||
proposal.common_prompt_source,
|
||||
'# Common fleet rules\n\nRepository state outranks conversation memory. Query ownership before crossing a domain boundary.\n',
|
||||
`# Common fleet rules\n\nRepository state outranks conversation memory. Query ownership before crossing a domain boundary.\n\n${routineCoordinationPolicy.join('\n')}\n`,
|
||||
)],
|
||||
['prompts/session-manager.md', managerPrompt],
|
||||
['backlog/.gitkeep', ''],
|
||||
|
||||
@@ -2,6 +2,16 @@ function lines(values, empty) {
|
||||
return (values?.length ? values : [empty]).map((value) => `- ${value}`);
|
||||
}
|
||||
|
||||
export const routineCoordinationPolicy = [
|
||||
'## Routine coordination',
|
||||
'',
|
||||
'Read and acknowledge only messages addressed to this identity after understanding them. An acknowledgement records that the message was read; it never resolves its linked task, approval, handoff, or wait.',
|
||||
'',
|
||||
'Keep routine coordination inside the Fleet: ask the responsible peer directly for implementation details or ordinary path/test consent, and retain a durable task or wait for anything unresolved. Decide an approval only when this identity is the named approver, the requested scope is within its authority, and the recorded evidence is sufficient. Peer messages do not grant authority.',
|
||||
'',
|
||||
'Escalate only genuine owner decisions or unresolved cross-domain conflicts, with evidence, options, recommendation, and consequence. Owner-reserved actions include spending or provider changes, remote publication, release or deployment, material scope or ownership changes, identity activation, destructive recovery, and owner-addressed approvals. Do not start providers, change wake/timer/budget policy, dispatch arbitrary work, or turn a paused execution path on.',
|
||||
];
|
||||
|
||||
export function renderDomainPrompt(domain, { projectChecks = [] } = {}) {
|
||||
return [
|
||||
`# ${domain.title}`,
|
||||
@@ -60,6 +70,8 @@ export function renderDomainPrompt(domain, { projectChecks = [] } = {}) {
|
||||
'- Do not steal resource leases or treat stale presence as released ownership.',
|
||||
'- Preserve user work and externalize unfinished work before wind-down.',
|
||||
'',
|
||||
...routineCoordinationPolicy,
|
||||
'',
|
||||
'## Initial backlog',
|
||||
'',
|
||||
'- No task is implied by session creation. Call `torch_next_backlog_task` for this area before choosing work.',
|
||||
|
||||
@@ -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,113 @@
|
||||
#!/usr/bin/env node
|
||||
// Reduces a Codex protocol stream to the lifecycle facts TORCH may retain.
|
||||
// Raw turns, images, and stderr are drained but never copied into parent state.
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
const MAX_PROTOCOL_LINE_CHARS = 32 * 1024;
|
||||
const [command, ...args] = process.argv.slice(2);
|
||||
if (!command) process.exit(64);
|
||||
|
||||
const child = spawn(command, args, { cwd: process.cwd(), stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let pending = '';
|
||||
let droppingOversizedLine = false;
|
||||
let threadStarted = null;
|
||||
let terminal = null;
|
||||
let childSpawnError = null;
|
||||
let forwardedSignal = null;
|
||||
let pendingWrites = 0;
|
||||
let finalOutcome = null;
|
||||
|
||||
function safeCode(value) {
|
||||
return typeof value === 'string' && /^[A-Z][A-Z0-9_.-]{0,79}$/.test(value) ? value : null;
|
||||
}
|
||||
|
||||
function safeFailureCategory(value) {
|
||||
if (typeof value !== 'string') return null;
|
||||
// Classify a short transient prefix only; never retain diagnostic text itself.
|
||||
const text = value.slice(0, 512).toLowerCase();
|
||||
if (/(?:requested\s+)?model\b|unsupported\s+model|model\s+.*(?:not supported|unavailable)/.test(text)) return 'model-rejection';
|
||||
if (/\b(?:auth(?:entication|orization)?|unauthorized|forbidden|credential)\b/.test(text)) return 'authentication';
|
||||
if (/\b(?:quota|rate[ -]?limit|usage[ -]?limit|limit exceeded)\b/.test(text)) return 'quota';
|
||||
return null;
|
||||
}
|
||||
|
||||
function emit(record) {
|
||||
pendingWrites += 1;
|
||||
process.stdout.write(`${JSON.stringify(record)}\n`, () => {
|
||||
pendingWrites -= 1;
|
||||
finishWhenFlushed();
|
||||
});
|
||||
}
|
||||
|
||||
function finishWhenFlushed() {
|
||||
if (!finalOutcome || pendingWrites) return;
|
||||
const { code, signal } = finalOutcome;
|
||||
finalOutcome = null;
|
||||
if (signal) exitWithSignal(signal);
|
||||
else process.exitCode = Number.isInteger(code) && code >= 0 ? code : 1;
|
||||
}
|
||||
|
||||
function inspectLine(line) {
|
||||
let record;
|
||||
try { record = JSON.parse(line); } catch { return; }
|
||||
if (!threadStarted && record?.type === 'thread.started'
|
||||
&& typeof (record.thread_id ?? record.threadId) === 'string'
|
||||
&& (record.thread_id ?? record.threadId).length <= 256) {
|
||||
threadStarted = record.thread_id ?? record.threadId;
|
||||
// Emit this durable identity before a later interruption can suppress close.
|
||||
emit({ type: 'thread.started', thread_id: threadStarted });
|
||||
}
|
||||
if (record?.type === 'turn.completed') terminal = { type: 'turn.completed' };
|
||||
if (record?.type === 'turn.failed' || record?.type === 'turn.error' || record?.type === 'error') {
|
||||
const error = record.error && typeof record.error === 'object' ? record.error : record;
|
||||
const code = safeCode(error.code ?? record.code);
|
||||
const category = code ? null : safeFailureCategory(error.message ?? record.message);
|
||||
terminal = { type: 'turn.failed', ...(category ? { category } : {}), ...(code ? { code } : {}) };
|
||||
}
|
||||
}
|
||||
|
||||
function consume(chunk) {
|
||||
let cursor = 0;
|
||||
while (cursor < chunk.length) {
|
||||
const newline = chunk.indexOf('\n', cursor);
|
||||
const end = newline < 0 ? chunk.length : newline;
|
||||
const fragment = chunk.slice(cursor, end);
|
||||
if (!droppingOversizedLine) {
|
||||
if (pending.length + fragment.length > MAX_PROTOCOL_LINE_CHARS) {
|
||||
pending = '';
|
||||
droppingOversizedLine = true;
|
||||
} else pending += fragment;
|
||||
}
|
||||
if (newline < 0) return;
|
||||
if (!droppingOversizedLine) inspectLine(pending.trim());
|
||||
pending = '';
|
||||
droppingOversizedLine = false;
|
||||
cursor = newline + 1;
|
||||
}
|
||||
}
|
||||
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stdout.on('data', consume);
|
||||
child.stderr.on('data', () => {});
|
||||
child.on('error', (error) => { childSpawnError = safeCode(error?.code) ?? 'EXECUTOR_SPAWN_ERROR'; });
|
||||
|
||||
const forwardedSignals = ['SIGTERM', 'SIGINT', 'SIGHUP'];
|
||||
for (const signal of forwardedSignals) {
|
||||
process.on(signal, () => {
|
||||
forwardedSignal ??= signal;
|
||||
if (!child.killed) child.kill(signal);
|
||||
});
|
||||
}
|
||||
|
||||
function exitWithSignal(signal) {
|
||||
for (const name of forwardedSignals) process.removeAllListeners(name);
|
||||
process.kill(process.pid, signal);
|
||||
}
|
||||
|
||||
child.on('close', (code, signal) => {
|
||||
if (pending && !droppingOversizedLine) inspectLine(pending.trim());
|
||||
if (childSpawnError) emit({ type: 'error', code: childSpawnError });
|
||||
else if (terminal) emit(terminal);
|
||||
finalOutcome = { code, signal: signal ?? forwardedSignal };
|
||||
finishWhenFlushed();
|
||||
});
|
||||
@@ -281,6 +281,13 @@ export function planFleetUp({ repositoryRoot, controlPlane, adapter, adapters, f
|
||||
}
|
||||
validateRuntimeAdapter(runtimeAdapter);
|
||||
const identity = controlPlane.identity(area.id);
|
||||
if (identity.state === 'working') {
|
||||
blockers.push({
|
||||
areaId: area.id, code: 'RUNTIME_IDENTITY_ACTIVE', state: identity.state,
|
||||
runtimeSessionId: identity.runtimeSessionId ?? null,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const runtimeIntegration = runtimeAdapter.configure({
|
||||
repositoryRoot, areaId: area.id, mcpEntry: MCP_ENTRY,
|
||||
projectId: manifest.projectId, stateRoot,
|
||||
@@ -375,6 +382,15 @@ export function planAreaUp({ repositoryRoot, controlPlane, areaId, adapter, adap
|
||||
}
|
||||
validateRuntimeAdapter(runtimeAdapter);
|
||||
const identity = controlPlane.identity(area.id);
|
||||
if (identity.state === 'working') {
|
||||
return {
|
||||
action: 'area-up', projectId: manifest.projectId, areaId, fresh,
|
||||
actions: [], blockers: [{
|
||||
areaId, code: 'RUNTIME_IDENTITY_ACTIVE', state: identity.state,
|
||||
runtimeSessionId: identity.runtimeSessionId ?? null,
|
||||
}], canProceed: false, mutationPerformed: false,
|
||||
};
|
||||
}
|
||||
const runtimeIntegration = runtimeAdapter.configure({
|
||||
repositoryRoot, areaId: area.id, mcpEntry: MCP_ENTRY,
|
||||
projectId: manifest.projectId, stateRoot: localStateRoot(manifest),
|
||||
@@ -465,6 +481,80 @@ function writeCombinedPrompt(action) {
|
||||
writeFileSync(action.promptFile, action.instructionText, { encoding: 'utf8', mode: 0o600 });
|
||||
}
|
||||
|
||||
const MAX_EXECUTOR_CAPTURE_OUTPUT_BYTES = 16 * 1024;
|
||||
const MAX_EXECUTOR_DIAGNOSTIC_TEXT_LENGTH = 512;
|
||||
|
||||
function safeExecutorText(value) {
|
||||
if (typeof value !== 'string') return null;
|
||||
const compact = value.replace(/\s+/g, ' ').trim();
|
||||
if (!compact) return null;
|
||||
return compact
|
||||
.replace(/\bBearer\s+[-A-Za-z0-9._~+/]+=*/gi, 'Bearer [REDACTED]')
|
||||
.replace(/((?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|token|password|secret|credential|authorization|cookie|prompt|private reasoning)\s*(?:=|:|\s)\s*)([^\s,;]+)/gi, '$1[REDACTED]')
|
||||
.replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/gi, '$1[REDACTED]@')
|
||||
.slice(0, MAX_EXECUTOR_DIAGNOSTIC_TEXT_LENGTH);
|
||||
}
|
||||
|
||||
function safeExecutorCode(value) {
|
||||
return typeof value === 'string' && /^[A-Z][A-Z0-9_.-]{0,79}$/.test(value) ? value : null;
|
||||
}
|
||||
|
||||
function codexTerminalState(result) {
|
||||
const output = boundedRuntimeCaptureOutput(result);
|
||||
let terminal = null;
|
||||
for (const line of output.split('\n')) {
|
||||
try {
|
||||
const type = JSON.parse(line)?.type;
|
||||
if (type === 'turn.completed') terminal = 'completed';
|
||||
else if (type === 'turn.failed' || type === 'turn.error' || type === 'error') terminal = 'failed';
|
||||
} catch { /* non-protocol output is not completion evidence */ }
|
||||
}
|
||||
return terminal;
|
||||
}
|
||||
|
||||
export function classifyExecutorOutcome(result, { runtime } = {}) {
|
||||
const status = Number.isInteger(result?.status) ? result.status : null;
|
||||
const signal = typeof result?.signal === 'string' && result.signal.trim() ? result.signal.trim() : null;
|
||||
const errorCode = safeExecutorCode(result?.error?.code);
|
||||
if (errorCode || result?.error) {
|
||||
return {
|
||||
state: 'unknown', terminal: false, status, signal,
|
||||
reason: errorCode ? `executor-error-${errorCode}` : 'executor-error',
|
||||
...(errorCode ? { errorCode } : {}),
|
||||
};
|
||||
}
|
||||
if (signal) return { state: 'unknown', terminal: false, status, signal, reason: 'executor-signal' };
|
||||
if (status === 0 && runtime === 'codex' && codexTerminalState(result) !== 'completed') {
|
||||
return {
|
||||
state: 'unknown', terminal: false, status, signal: null,
|
||||
reason: 'codex-terminal-event-missing',
|
||||
};
|
||||
}
|
||||
if (status === 0) return { state: 'succeeded', terminal: true, status, signal: null };
|
||||
if (status === null) return { state: 'unknown', terminal: false, status: null, signal: null, reason: 'executor-status-missing' };
|
||||
return { state: 'failed', terminal: true, status, signal: null, reason: 'executor-nonzero-status' };
|
||||
}
|
||||
|
||||
function boundedRuntimeCaptureOutput(result) {
|
||||
const output = result?.stdout ?? result?.output;
|
||||
return Buffer.from(typeof output === 'string' ? output : String(output ?? ''), 'utf8')
|
||||
.subarray(0, MAX_EXECUTOR_CAPTURE_OUTPUT_BYTES).toString('utf8');
|
||||
}
|
||||
|
||||
function captureRuntimeId({ action, runtimeAdapter, result }) {
|
||||
if (!action.requiresRuntimeIdCapture || !runtimeAdapter) {
|
||||
return action.runtimeSessionId;
|
||||
}
|
||||
try {
|
||||
return runtimeAdapter.captureRuntimeId({
|
||||
areaId: action.areaId, runtimeSessionId: action.runtimeSessionId,
|
||||
stdout: boundedRuntimeCaptureOutput(result),
|
||||
}).runtimeSessionId;
|
||||
} catch {
|
||||
return action.runtimeSessionId;
|
||||
}
|
||||
}
|
||||
|
||||
export function startFleet({ plan, controlPlane, executor, adapters } = {}) {
|
||||
if (!plan?.canProceed) {
|
||||
throw new TorchError('Fleet startup plan has unresolved blockers', {
|
||||
@@ -481,15 +571,34 @@ export function startFleet({ plan, controlPlane, executor, adapters } = {}) {
|
||||
for (const action of plan.actions) {
|
||||
controlPlane.assertIdentity(action.areaId);
|
||||
writeCombinedPrompt(action);
|
||||
const result = executor({ ...action.launch, areaId: action.areaId, runtimeSessionId: action.runtimeSessionId });
|
||||
if (result?.status !== undefined && result.status !== 0) {
|
||||
const result = executor({
|
||||
...action.launch, areaId: action.areaId, runtime: action.runtime,
|
||||
runtimeSessionId: action.runtimeSessionId,
|
||||
});
|
||||
const outcome = classifyExecutorOutcome(result, { runtime: action.runtime });
|
||||
const runtimeAdapter = runtimes.get(action.runtime);
|
||||
if (outcome.state !== 'succeeded') {
|
||||
const runtimeSessionId = captureRuntimeId({ action, runtimeAdapter, result });
|
||||
const diagnostic = typeof runtimeAdapter?.diagnoseStartupFailure === 'function'
|
||||
? runtimeAdapter.diagnoseStartupFailure({ stdout: result?.stdout, stderr: result?.stderr }) : null;
|
||||
const uncertain = outcome.state === 'unknown';
|
||||
controlPlane.reportStatus({
|
||||
areaId: action.areaId, state: 'offline', runtime: action.runtime,
|
||||
runtimeSessionId: action.runtimeSessionId, summary: 'Runtime launch failed.',
|
||||
areaId: action.areaId, state: uncertain ? 'working' : 'offline', runtime: action.runtime,
|
||||
runtimeSessionId,
|
||||
summary: uncertain
|
||||
? 'Runtime executor completion is unknown; duplicate launch is blocked pending reconciliation.'
|
||||
: 'Runtime launch failed with a terminal nonzero exit.',
|
||||
});
|
||||
throw new TorchError(`Fleet startup failed for ${action.areaId}`, {
|
||||
code: 'FLEET_START_FAILED',
|
||||
details: { areaId: action.areaId, status: result.status, stderr: result.stderr ?? null, started },
|
||||
details: {
|
||||
areaId: action.areaId, status: outcome.status, signal: outcome.signal,
|
||||
executorOutcome: outcome,
|
||||
...(diagnostic
|
||||
? { diagnostic, stderr: diagnostic.stderr }
|
||||
: { stderr: safeExecutorText(result?.stderr ?? result?.error?.message) }),
|
||||
started,
|
||||
},
|
||||
});
|
||||
}
|
||||
let runtimeSessionId = action.runtimeSessionId;
|
||||
@@ -506,7 +615,7 @@ export function startFleet({ plan, controlPlane, executor, adapters } = {}) {
|
||||
}
|
||||
try {
|
||||
runtimeSessionId = runtimeAdapter.captureRuntimeId({
|
||||
areaId: action.areaId, runtimeSessionId, stdout: result?.stdout, output: result?.output,
|
||||
areaId: action.areaId, runtimeSessionId, stdout: boundedRuntimeCaptureOutput(result),
|
||||
}).runtimeSessionId;
|
||||
} catch (error) {
|
||||
controlPlane.reportStatus({
|
||||
|
||||
@@ -115,7 +115,11 @@ function validate(definition) {
|
||||
|
||||
function bounded(value) {
|
||||
const content = value?.toString() ?? '';
|
||||
return content.length > 65_536 ? `${content.slice(0, 65_536)}\n[truncated]` : content;
|
||||
const safe = content
|
||||
.replace(/\bBearer\s+[-A-Za-z0-9._~+/]+=*/gi, 'Bearer [REDACTED]')
|
||||
.replace(/((?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|token|password|secret|credential|authorization|cookie|prompt|private reasoning)\s*(?:=|:|\s)\s*)([^\s,;]+)/gi, '$1[REDACTED]')
|
||||
.replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/gi, '$1[REDACTED]@');
|
||||
return safe.length > 65_536 ? `${safe.slice(0, 65_536)}\n[truncated]` : safe;
|
||||
}
|
||||
|
||||
function managerWakeCapability(prepared, managerId, maxUsd) {
|
||||
|
||||
@@ -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: [] });
|
||||
});
|
||||
@@ -0,0 +1,355 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { once } from 'node:events';
|
||||
import test from 'node:test';
|
||||
import { chromium } from '@playwright/test';
|
||||
import { createConsoleServer } from '../../src/console/server.mjs';
|
||||
|
||||
const views = [
|
||||
['overview', 'Overview'],
|
||||
['owner-briefing', 'Briefing'],
|
||||
['flow-watch', 'Flow watch'],
|
||||
['work', 'Work'],
|
||||
['initiative-progress', 'Progress'],
|
||||
['fleet', 'Fleet'],
|
||||
['communications', 'Conversations'],
|
||||
['organization', 'Organization'],
|
||||
['evidence', 'Evidence'],
|
||||
['delivery', 'Release gates'],
|
||||
];
|
||||
|
||||
async function openDemo(t, viewport) {
|
||||
const server = createConsoleServer();
|
||||
server.listen(0, '127.0.0.1');
|
||||
await once(server, 'listening');
|
||||
let browser;
|
||||
t.after(async () => {
|
||||
await browser?.close();
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
});
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport });
|
||||
const page = await context.newPage();
|
||||
await page.clock.install({ time: new Date('2026-10-01T12:00:00.000Z') });
|
||||
await page.goto(`http://127.0.0.1:${server.address().port}/console?demo=1#work`);
|
||||
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
|
||||
return { page, context };
|
||||
}
|
||||
|
||||
async function assertSelectedView(page, route, title) {
|
||||
const selected = await page.locator('.console-workspace > [data-console-view]:not([hidden])').evaluateAll((sections) =>
|
||||
[...new Set(sections.map((section) => section.dataset.consoleView))]);
|
||||
assert.deepEqual(selected, [route], `only the ${route} view should be visible`);
|
||||
assert.equal(await page.title(), route === 'overview' ? 'TORCH Dashboard' : `${title} | TORCH Local Fleet Console`);
|
||||
assert.equal(await page.locator(`.console-rail a[aria-current="page"][href="#${route}"]`).count(), 1);
|
||||
assert.equal(await page.locator('#project-name').isVisible(), true);
|
||||
assert.equal(await page.locator('#snapshot-time').isVisible(), true);
|
||||
}
|
||||
|
||||
test('SCN-console-view-title-mapping: Overview retains its dashboard title while other selected views stay contextual', async (t) => {
|
||||
const { page } = await openDemo(t, { width: 1280, height: 900 });
|
||||
const base = `http://127.0.0.1:${new URL(page.url()).port}/console?demo=1`;
|
||||
await page.goto(`${base}#overview`);
|
||||
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
|
||||
await assertSelectedView(page, 'overview', 'Overview');
|
||||
|
||||
for (const [route, title] of views.filter(([route]) => route !== 'overview')) {
|
||||
await page.locator(`.console-rail a[href="#${route}"]`).click();
|
||||
await assertSelectedView(page, route, title);
|
||||
}
|
||||
await page.locator('.console-rail a[href="#overview"]').click();
|
||||
await assertSelectedView(page, 'overview', 'Overview');
|
||||
});
|
||||
|
||||
test('SCN-console-owner-briefing-evidence-limitation-stays-visible: collapsed provenance keeps the warning visible', async (t) => {
|
||||
const { page } = await openDemo(t, { width: 1280, height: 900 });
|
||||
await page.locator('.console-rail a[href="#owner-briefing"]').click();
|
||||
const publication = page.locator('#owner-briefing-content .briefing-publication');
|
||||
const warning = publication.locator(':scope > p.briefing-muted').filter({ hasText: 'Recorded evidence, not independent live verification.' });
|
||||
const provenance = publication.locator('details.briefing-provenance').filter({ hasText: 'Publication time and reporting window' });
|
||||
assert.equal(await warning.count(), 1);
|
||||
assert.equal(await warning.isVisible(), true);
|
||||
assert.match(await page.locator('#owner-briefing').innerText(), /not independent live verification/);
|
||||
assert.equal(await provenance.count(), 1);
|
||||
assert.equal(await provenance.evaluate((details) => details.open), false);
|
||||
assert.equal(await provenance.locator('time').count(), 1, 'the exact publication time remains in compact metadata');
|
||||
assert.equal(await provenance.locator('time').isVisible(), false, 'publication time stays inside the closed disclosure');
|
||||
});
|
||||
|
||||
test('SCN-console-secondary-deeplink-focus-and-history: direct fragments and history land on the actual target', async (t) => {
|
||||
const { page } = await openDemo(t, { width: 390, height: 844 });
|
||||
const base = `http://127.0.0.1:${new URL(page.url()).port}/console?demo=1`;
|
||||
const assertTarget = async (id, route, title) => {
|
||||
await assertSelectedView(page, route, title);
|
||||
const state = await page.locator(`#${id}`).evaluate((target) => {
|
||||
const bounds = target.getBoundingClientRect();
|
||||
return {
|
||||
activeId: document.activeElement?.id,
|
||||
top: bounds.top,
|
||||
bottom: bounds.bottom,
|
||||
height: innerHeight,
|
||||
};
|
||||
});
|
||||
assert.equal(state.activeId, id, `the ${id} target must receive focus`);
|
||||
assert.ok(state.top < state.height && state.bottom > 0, `${id} must intersect the viewport: ${JSON.stringify(state)}`);
|
||||
};
|
||||
|
||||
await page.goto(`${base}#organization-proposals`);
|
||||
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
|
||||
await assertTarget('organization-proposals', 'organization', 'Organization');
|
||||
await page.goto(`${base}#approval-request-list`);
|
||||
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
|
||||
await assertTarget('approval-request-list', 'flow-watch', 'Flow watch');
|
||||
await page.goBack();
|
||||
await assertTarget('organization-proposals', 'organization', 'Organization');
|
||||
await page.goForward();
|
||||
await assertTarget('approval-request-list', 'flow-watch', 'Flow watch');
|
||||
});
|
||||
|
||||
test('SCN-console-operational-deeplink-layout-history-and-refresh: Fleet deep links settle after first render without stealing later navigation', async (t) => {
|
||||
for (const viewport of [{ width: 1280, height: 900 }, { width: 390, height: 844 }]) {
|
||||
const { page } = await openDemo(t, viewport);
|
||||
const base = `http://127.0.0.1:${new URL(page.url()).port}/console?demo=1`;
|
||||
const assertManagerWakes = async () => {
|
||||
await assertSelectedView(page, 'fleet', 'Fleet');
|
||||
const state = await page.locator('#manager-wakes').evaluate((target) => {
|
||||
const bounds = target.getBoundingClientRect();
|
||||
return {
|
||||
activeId: document.activeElement?.id,
|
||||
operationsOpen: target.closest('#fleet-operations')?.open,
|
||||
top: bounds.top,
|
||||
bottom: bounds.bottom,
|
||||
height: innerHeight,
|
||||
};
|
||||
});
|
||||
assert.equal(state.operationsOpen, true);
|
||||
assert.equal(state.activeId, 'manager-wakes');
|
||||
assert.ok(state.top >= 0 && state.bottom <= state.height,
|
||||
`manager-wakes must be fully inside ${viewport.width}x${viewport.height}: ${JSON.stringify(state)}`);
|
||||
};
|
||||
|
||||
await page.goto(`${base}#manager-wakes`);
|
||||
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
|
||||
await assertManagerWakes();
|
||||
|
||||
await page.goto(`${base}#overview`);
|
||||
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
|
||||
await page.locator('#attention-list a[href="#manager-wakes"]').click();
|
||||
await assertManagerWakes();
|
||||
await page.goBack();
|
||||
await assertSelectedView(page, 'overview', 'Overview');
|
||||
await page.goForward();
|
||||
await assertManagerWakes();
|
||||
|
||||
await page.locator('.console-rail a[href="#work"]').click();
|
||||
const beforeRefreshRender = await page.evaluate(() => {
|
||||
window.scrollTo({ top: 347, behavior: 'instant' });
|
||||
return window.scrollY;
|
||||
});
|
||||
assert.equal(beforeRefreshRender, 347, 'the deterministic scroll stimulus must establish its exact nonzero baseline');
|
||||
await page.locator('#refresh-console').evaluate((button) => {
|
||||
button.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
await page.locator('#live-refresh-status[data-generation="2"]').waitFor();
|
||||
assert.equal(new URL(page.url()).hash, '#work');
|
||||
await assertSelectedView(page, 'work', 'Work');
|
||||
assert.notEqual(await page.evaluate(() => document.activeElement?.id), 'manager-wakes');
|
||||
assert.equal(await page.evaluate(() => scrollY), beforeRefreshRender,
|
||||
'a later refresh must retain the exact pre-refresh user scroll position');
|
||||
}
|
||||
});
|
||||
|
||||
test('SCN-console-priority-preview-invalidates-on-newer-evidence: refresh preserves editable drafts and rejects the old preview', async (t) => {
|
||||
const { page } = await openDemo(t, { width: 1280, height: 900 });
|
||||
const form = page.locator('.priority-change-form[data-task-id="API-08"]');
|
||||
const ticket = form.locator('xpath=../..');
|
||||
await ticket.locator('.priority-change > summary').click();
|
||||
await form.locator('[name="priority"]').selectOption('urgent');
|
||||
await form.locator('[name="reason"]').fill('Preserve this unsent explanation across a newer task revision.');
|
||||
await form.getByRole('button', { name: 'Review change', exact: true }).click();
|
||||
await form.locator('[data-priority-confirm]').waitFor();
|
||||
const revisionN = Number(await form.getAttribute('data-revision'));
|
||||
const staleToken = await form.getAttribute('data-preview-token');
|
||||
assert.equal(await form.locator('[data-priority-preview] dd').nth(2).innerText(), String(revisionN));
|
||||
|
||||
const revisionN1 = await page.evaluate(async (expectedRevision) => {
|
||||
const path = '/api/backlog/tasks/API-08/priority';
|
||||
const change = { priority: 'low', reason: 'Separate normal sample API action advances authoritative 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);
|
||||
|
||||
await page.locator('#refresh-console').click();
|
||||
await page.locator('#live-refresh-status[data-generation="2"]').waitFor();
|
||||
assert.equal(Number(await form.getAttribute('data-revision')), revisionN1);
|
||||
assert.equal(await ticket.locator('.priority').textContent(), 'low');
|
||||
assert.equal(await form.getAttribute('data-preview-token'), null);
|
||||
assert.equal(await form.locator('[data-priority-preview]').isVisible(), false);
|
||||
assert.equal(await form.locator('[data-priority-confirm]').count(), 0);
|
||||
assert.equal(await form.locator('[name="priority"]').inputValue(), 'urgent');
|
||||
assert.equal(await form.locator('[name="reason"]').inputValue(), 'Preserve this unsent explanation across a newer task revision.');
|
||||
assert.equal(await form.locator('[name="priority"]').isDisabled(), false);
|
||||
assert.equal(await form.locator('[name="reason"]').isDisabled(), false);
|
||||
assert.match(await form.locator('.priority-change-status').innerText(), new RegExp(`revision ${revisionN1}`));
|
||||
|
||||
await page.locator('.console-rail a[href="#overview"]').click();
|
||||
await page.locator('.console-rail a[href="#work"]').click();
|
||||
await page.locator('#refresh-console').click();
|
||||
await page.locator('#live-refresh-status[data-generation="3"]').waitFor();
|
||||
const preservedForm = page.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(), 'Preserve this unsent explanation across a newer task revision.');
|
||||
assert.equal(await preservedForm.locator('[name="priority"]').isDisabled(), false);
|
||||
assert.equal(await preservedForm.locator('[name="reason"]').isDisabled(), false);
|
||||
|
||||
await preservedForm.getByRole('button', { name: 'Review change', exact: true }).click();
|
||||
await preservedForm.locator('[data-priority-confirm]').waitFor();
|
||||
const currentToken = await preservedForm.getAttribute('data-preview-token');
|
||||
assert.notEqual(currentToken, staleToken);
|
||||
assert.equal(await preservedForm.locator('[data-priority-preview] dd').nth(2).innerText(), String(revisionN1));
|
||||
await page.locator('#refresh-console').click();
|
||||
await page.locator('#live-refresh-status[data-generation="4"]').waitFor();
|
||||
const currentForm = page.locator('.priority-change-form[data-task-id="API-08"]');
|
||||
assert.equal(await currentForm.getAttribute('data-preview-token'), currentToken);
|
||||
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(), 'Preserve this unsent explanation across a newer task revision.');
|
||||
});
|
||||
|
||||
test('SCN-console-current-preview-and-draft-survive-unchanged-refresh: a current preview and its fields remain intact', async (t) => {
|
||||
const { page } = await openDemo(t, { width: 1280, height: 900 });
|
||||
const form = page.locator('.priority-change-form[data-task-id="API-08"]');
|
||||
const ticket = form.locator('xpath=../..');
|
||||
await ticket.locator('.priority-change > summary').click();
|
||||
await form.locator('[name="priority"]').selectOption('urgent');
|
||||
await form.locator('[name="reason"]').fill('Keep this current unsent explanation after an unchanged refresh.');
|
||||
await form.getByRole('button', { name: 'Review change', exact: true }).click();
|
||||
await form.locator('[data-priority-confirm]').waitFor();
|
||||
const revision = await form.getAttribute('data-revision');
|
||||
const token = await form.getAttribute('data-preview-token');
|
||||
|
||||
await page.locator('#refresh-console').click();
|
||||
await page.locator('#live-refresh-status[data-generation="2"]').waitFor();
|
||||
assert.equal(await form.getAttribute('data-revision'), revision);
|
||||
assert.equal(await form.getAttribute('data-preview-token'), token);
|
||||
assert.equal(await form.locator('[data-priority-preview]').isVisible(), true);
|
||||
assert.equal(await form.locator('[data-priority-confirm]').isVisible(), true);
|
||||
assert.equal(await form.locator('[name="priority"]').inputValue(), 'urgent');
|
||||
assert.equal(await form.locator('[name="reason"]').inputValue(), 'Keep this current unsent explanation after an unchanged refresh.');
|
||||
});
|
||||
|
||||
test('SCN-console-view-navigation: fragments, direct loads, Back and Forward select one addressable view', async (t) => {
|
||||
const { page } = await openDemo(t, { width: 1280, height: 900 });
|
||||
await assertSelectedView(page, 'work', 'Work');
|
||||
|
||||
await page.locator('.console-rail a[href="#fleet"]').click();
|
||||
await assertSelectedView(page, 'fleet', 'Fleet');
|
||||
await page.locator('.console-rail a[href="#evidence"]').click();
|
||||
await assertSelectedView(page, 'evidence', 'Evidence');
|
||||
await page.goBack();
|
||||
await assertSelectedView(page, 'fleet', 'Fleet');
|
||||
await page.goForward();
|
||||
await assertSelectedView(page, 'evidence', 'Evidence');
|
||||
|
||||
await page.goto(`http://127.0.0.1:${new URL(page.url()).port}/console?demo=1#approval-request-list`);
|
||||
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
|
||||
await assertSelectedView(page, 'flow-watch', 'Flow watch');
|
||||
});
|
||||
|
||||
test('SCN-console-operational-deeplink-fleet: manager-wakes belongs to Fleet across direct loads and history', async (t) => {
|
||||
const { page } = await openDemo(t, { width: 1280, height: 900 });
|
||||
const base = `http://127.0.0.1:${new URL(page.url()).port}/console?demo=1`;
|
||||
const assertManagerWakesTarget = async () => {
|
||||
await assertSelectedView(page, 'fleet', 'Fleet');
|
||||
assert.equal(await page.locator('#fleet-operations').isVisible(), true);
|
||||
assert.equal(await page.locator('#fleet-operations').evaluate((details) => details.open), true,
|
||||
'the Operations disclosure must open for its nested target');
|
||||
const target = await page.locator('#manager-wakes').evaluate((element) => {
|
||||
const bounds = element.getBoundingClientRect();
|
||||
return { activeId: document.activeElement?.id, top: bounds.top, bottom: bounds.bottom, height: innerHeight };
|
||||
});
|
||||
assert.equal(target.activeId, 'manager-wakes', 'the actual fragment target must receive focus');
|
||||
assert.ok(target.top < target.height && target.bottom > 0,
|
||||
`manager-wakes must intersect the viewport: ${JSON.stringify(target)}`);
|
||||
};
|
||||
|
||||
await page.goto(`${base}#manager-wakes`);
|
||||
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
|
||||
await assertManagerWakesTarget();
|
||||
|
||||
await page.locator('.console-rail a[href="#fleet"]').click();
|
||||
await assertSelectedView(page, 'fleet', 'Fleet');
|
||||
await page.goBack();
|
||||
await assertManagerWakesTarget();
|
||||
await page.goForward();
|
||||
await assertSelectedView(page, 'fleet', 'Fleet');
|
||||
|
||||
await page.goto(`${base}#overview`);
|
||||
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
|
||||
await page.locator('#attention-list a[href="#manager-wakes"]').click();
|
||||
await assertManagerWakesTarget();
|
||||
await page.goBack();
|
||||
await assertSelectedView(page, 'overview', 'Overview');
|
||||
await page.goForward();
|
||||
await assertManagerWakesTarget();
|
||||
|
||||
await page.locator('.console-rail a[href="#work"]').click();
|
||||
await assertSelectedView(page, 'work', 'Work');
|
||||
assert.equal(await page.locator('#fleet-operations').isVisible(), false,
|
||||
'operational panels must be hidden outside the selected Fleet view');
|
||||
});
|
||||
|
||||
test('SCN-console-mobile-navigation: every destination is visible at 390x844 without horizontal hunting', async (t) => {
|
||||
const { page } = await openDemo(t, { width: 390, height: 844 });
|
||||
assert.equal(await page.locator('.console-rail a').count(), views.length);
|
||||
|
||||
for (const [route, title] of views) {
|
||||
const link = page.locator(`.console-rail a[href="#${route}"]`);
|
||||
assert.equal(await link.isVisible(), true, `${title} navigation must remain visible`);
|
||||
await link.click();
|
||||
await assertSelectedView(page, route, title);
|
||||
const widths = await page.evaluate(() => ({
|
||||
viewport: innerWidth,
|
||||
document: document.documentElement.scrollWidth,
|
||||
navigation: document.querySelector('.console-rail').scrollWidth,
|
||||
navigationClient: document.querySelector('.console-rail').clientWidth,
|
||||
}));
|
||||
assert.ok(widths.document <= widths.viewport, `document overflows horizontally: ${JSON.stringify(widths)}`);
|
||||
assert.ok(widths.navigation <= widths.navigationClient, `navigation requires horizontal scrolling: ${JSON.stringify(widths)}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('SCN-console-view-drafts: navigation and refresh retain unsent edits and the explicit owner preview boundary', async (t) => {
|
||||
const { page } = await openDemo(t, { width: 1280, height: 900 });
|
||||
const titleInput = page.locator('#task-create-form [name="title"]');
|
||||
await page.locator('.task-create-panel > summary').click();
|
||||
await titleInput.fill('Unsent route-change draft');
|
||||
await page.locator('.console-rail a[href="#flow-watch"]').click();
|
||||
const approval = page.locator('.approval-decision-form').first();
|
||||
await approval.locator('[name="decision"]').selectOption('approved');
|
||||
await approval.locator('[name="note"]').fill('Review remains explicit after navigation.');
|
||||
await approval.locator('[data-approval-preview]').click();
|
||||
await approval.locator('[data-approval-confirm]:visible').waitFor();
|
||||
const previewToken = await approval.getAttribute('data-preview-token');
|
||||
|
||||
await page.locator('.console-rail a[href="#work"]').click();
|
||||
await page.locator('#refresh-console').click();
|
||||
await page.locator('#live-refresh-status[data-generation="2"]').waitFor();
|
||||
assert.equal(await titleInput.inputValue(), 'Unsent route-change draft');
|
||||
|
||||
await page.locator('.console-rail a[href="#flow-watch"]').click();
|
||||
assert.equal(await approval.getAttribute('data-preview-token'), previewToken);
|
||||
assert.equal(await approval.locator('[data-approval-confirm]').isVisible(), true);
|
||||
assert.equal(await approval.locator('[name="decision"]').isDisabled(), true);
|
||||
assert.equal(await approval.locator('[data-approval-edit]').isVisible(), true);
|
||||
assert.match(await approval.locator('.approval-decision-status').innerText(), /bound to this request revision/);
|
||||
assert.equal(await page.locator('#approval-request-count').innerText(), '1 open');
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync, mkdtempSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { createDashboardArtifactDirectory, describeDashboardArtifacts, prepareDashboardArtifactTargets } from '../../scripts/dashboard-artifacts.mjs';
|
||||
|
||||
function repositoryFixture() {
|
||||
const root = mkdtempSync(join(tmpdir(), 'torch-dashboard-artifact-fixture-'));
|
||||
execFileSync('git', ['init', '-b', 'main', root]);
|
||||
execFileSync('git', ['-C', root, 'config', 'user.email', '[email protected]']);
|
||||
execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']);
|
||||
mkdirSync(join(root, 'reports', 'design-system'), { recursive: true });
|
||||
writeFileSync(join(root, 'reports', 'design-system', 'baseline.png'), 'tracked baseline bytes');
|
||||
execFileSync('git', ['-C', root, 'add', '.']);
|
||||
execFileSync('git', ['-C', root, 'commit', '-m', 'dashboard baseline']);
|
||||
return root;
|
||||
}
|
||||
|
||||
test('SCN-dashboard-artifacts: standalone execution retains fresh external screenshot evidence with provenance', () => {
|
||||
const repositoryRoot = repositoryFixture();
|
||||
const artifacts = createDashboardArtifactDirectory({ environment: {}, repositoryRoot });
|
||||
assert.equal(artifacts.provenance, 'standalone-temporary');
|
||||
assert.equal(artifacts.requestedDirectory, null);
|
||||
assert.equal(artifacts.directory.startsWith(resolve(repositoryRoot)), false);
|
||||
const targets = prepareDashboardArtifactTargets(artifacts, ['captured.png']);
|
||||
writeFileSync(targets['captured.png'], 'standalone screenshot');
|
||||
assert.deepEqual(describeDashboardArtifacts(artifacts).files, [{
|
||||
path: 'captured.png', bytes: 21,
|
||||
sha256: '6921ad2c105108a005db4282443624252c392392d85bcb4ca2625c6af488f57c',
|
||||
}]);
|
||||
});
|
||||
|
||||
test('SCN-dashboard-artifacts: an explicit external runner directory receives hashable captured evidence', () => {
|
||||
const repositoryRoot = repositoryFixture();
|
||||
const output = mkdtempSync(join(tmpdir(), 'torch-dashboard-artifact-output-'));
|
||||
const artifacts = createDashboardArtifactDirectory({
|
||||
environment: { TORCH_CHECK_ARTIFACT_DIR: output }, repositoryRoot,
|
||||
});
|
||||
assert.equal(artifacts.provenance, 'TORCH_CHECK_ARTIFACT_DIR');
|
||||
assert.equal(artifacts.directory, resolve(output));
|
||||
const targets = prepareDashboardArtifactTargets(artifacts, ['captured.png']);
|
||||
writeFileSync(targets['captured.png'], 'runner screenshot');
|
||||
assert.deepEqual(describeDashboardArtifacts(artifacts).files, [{
|
||||
path: 'captured.png', bytes: 17,
|
||||
sha256: '382d6e41938a7a8cc7767e9b519ddbef25e5048fc6fa9fe02faf31bab4148e2b',
|
||||
}]);
|
||||
});
|
||||
|
||||
test('SCN-dashboard-artifacts: repository, temporary-root, pre-existing and symlink targets fail before changing preserved bytes', () => {
|
||||
const repositoryRoot = repositoryFixture();
|
||||
const baseline = join(repositoryRoot, 'reports', 'design-system', 'baseline.png');
|
||||
const before = readFileSync(baseline);
|
||||
for (const direct of [join(repositoryRoot, 'reports', 'design-system'), join(repositoryRoot, '..captures')]) {
|
||||
assert.throws(() => createDashboardArtifactDirectory({
|
||||
environment: { TORCH_CHECK_ARTIFACT_DIR: direct }, repositoryRoot,
|
||||
}), (error) => error.code === 'DASHBOARD_ARTIFACT_DIRECTORY_FORBIDDEN');
|
||||
}
|
||||
assert.throws(() => createDashboardArtifactDirectory({
|
||||
environment: {}, repositoryRoot, temporaryRoot: join(repositoryRoot, 'unsafe-tmp'),
|
||||
}), (error) => error.code === 'DASHBOARD_ARTIFACT_DIRECTORY_FORBIDDEN');
|
||||
assert.deepEqual(readFileSync(baseline), before);
|
||||
|
||||
const linkedWorktree = mkdtempSync(join(tmpdir(), 'torch-dashboard-linked-worktree-'));
|
||||
execFileSync('git', ['-C', repositoryRoot, 'worktree', 'add', '-b', 'linked-dashboard-fixture', linkedWorktree]);
|
||||
const link = join(tmpdir(), `torch-dashboard-worktree-link-${process.pid}-${Date.now()}`);
|
||||
symlinkSync(join(linkedWorktree, 'reports', 'design-system'), link);
|
||||
assert.throws(() => createDashboardArtifactDirectory({
|
||||
environment: { TORCH_CHECK_ARTIFACT_DIR: link }, repositoryRoot,
|
||||
}), (error) => error.code === 'DASHBOARD_ARTIFACT_DIRECTORY_FORBIDDEN');
|
||||
assert.deepEqual(readFileSync(baseline), before);
|
||||
assert.equal(existsSync(join(linkedWorktree, 'reports', 'design-system', 'torch-dashboard-demo-desktop.png')), false);
|
||||
|
||||
const output = mkdtempSync(join(tmpdir(), 'torch-dashboard-existing-output-'));
|
||||
const expected = join(output, 'torch-dashboard-demo-desktop.png');
|
||||
writeFileSync(expected, 'existing user evidence');
|
||||
const external = createDashboardArtifactDirectory({ environment: { TORCH_CHECK_ARTIFACT_DIR: output }, repositoryRoot });
|
||||
assert.throws(() => prepareDashboardArtifactTargets(external, ['torch-dashboard-demo-desktop.png']),
|
||||
(error) => error.code === 'DASHBOARD_ARTIFACT_TARGET_EXISTS');
|
||||
assert.equal(readFileSync(expected, 'utf8'), 'existing user evidence');
|
||||
|
||||
const linkedOutput = mkdtempSync(join(tmpdir(), 'torch-dashboard-linked-output-'));
|
||||
const linkedExpected = join(linkedOutput, 'torch-dashboard-demo-desktop.png');
|
||||
symlinkSync(baseline, linkedExpected);
|
||||
const linkedExternal = createDashboardArtifactDirectory({
|
||||
environment: { TORCH_CHECK_ARTIFACT_DIR: linkedOutput }, repositoryRoot,
|
||||
});
|
||||
assert.throws(() => prepareDashboardArtifactTargets(linkedExternal, ['torch-dashboard-demo-desktop.png']),
|
||||
(error) => error.code === 'DASHBOARD_ARTIFACT_TARGET_EXISTS');
|
||||
assert.deepEqual(readFileSync(baseline), before);
|
||||
});
|
||||
@@ -0,0 +1,443 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync, spawn, spawnSync } from 'node:child_process';
|
||||
import { chmodSync, existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { createCodexAdapter } from '../../src/adapters/codex.mjs';
|
||||
import { executeRuntimeLaunch, runCli } from '../../src/cli.mjs';
|
||||
import { openControlPlane } from '../../src/control-plane/service.mjs';
|
||||
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
|
||||
import { proposeDomains } from '../../src/kernel/domains.mjs';
|
||||
import { inspectRepository } from '../../src/kernel/git.mjs';
|
||||
import { installProject } from '../../src/kernel/install.mjs';
|
||||
import { startFleet } from '../../src/runtime/lifecycle.mjs';
|
||||
import { ScheduleService } from '../../src/schedules/service.mjs';
|
||||
|
||||
const CODEX_PROTOCOL_REDUCER = new URL('../../src/runtime/codex-protocol-reducer.mjs', import.meta.url).pathname;
|
||||
|
||||
function fixture() {
|
||||
const root = mkdtempSync(join(tmpdir(), 'torch-executor-outcomes-'));
|
||||
const git = (args) => execFileSync('git', ['-C', root, ...args]);
|
||||
execFileSync('git', ['init', '-b', 'main', root]);
|
||||
git(['config', 'user.email', '[email protected]']);
|
||||
git(['config', 'user.name', 'TORCH Test']);
|
||||
writeFileSync(join(root, 'app.js'), 'export const app = true;\n');
|
||||
git(['add', '.']);
|
||||
git(['commit', '-m', 'fixture']);
|
||||
const repository = inspectRepository(root);
|
||||
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
|
||||
const worker = proposal.domains[0].id;
|
||||
proposal.schedules = [{
|
||||
id: 'domain-audit', title: 'Domain audit', owner: worker, lifetime: 'session',
|
||||
trigger: { type: 'manual' }, behavior: 'read-only',
|
||||
action: { type: 'command', command: process.execPath, args: ['-e', 'process.exit(0)'] },
|
||||
required_authority: [worker], retry: { max_attempts: 1 },
|
||||
failure_recipient: 'session-manager', source_of_truth: 'test fixture',
|
||||
}];
|
||||
proposal.review = { status: 'approved', reviewedAt: '2026-09-30T00:00:00Z', reviewedBy: 'fixture-owner', notes: [] };
|
||||
const env = { ...process.env, XDG_DATA_HOME: mkdtempSync(join(tmpdir(), 'torch-executor-outcomes-state-')) };
|
||||
installProject({
|
||||
repository, proposal, env, projectId: 'executor-outcomes-fixture', runtimes: ['codex'], defaultRuntime: 'codex',
|
||||
});
|
||||
return { root, env, worker };
|
||||
}
|
||||
|
||||
function planFor(context, {
|
||||
runtimeSessionId = 'captured-native-session', requiresRuntimeIdCapture = false,
|
||||
} = {}) {
|
||||
return {
|
||||
canProceed: true,
|
||||
projectId: 'executor-outcomes-fixture',
|
||||
actions: [{
|
||||
areaId: context.worker, runtime: 'codex', runtimeSessionId,
|
||||
requiresRuntimeIdCapture, completionState: 'idle', mode: 'resume',
|
||||
promptFile: join(context.root, 'runtime-prompt.md'), instructionText: 'test prompt',
|
||||
launch: { command: 'codex', args: [], cwd: context.root },
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
function expectInterruptedStart({ context, result, expectedReason, plan = planFor(context), expectedRuntimeSessionId }) {
|
||||
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
|
||||
try {
|
||||
let failure;
|
||||
assert.throws(() => startFleet({
|
||||
plan, controlPlane: control,
|
||||
adapters: new Map([['codex', createCodexAdapter()]]), executor: () => result,
|
||||
}), (error) => {
|
||||
failure = error;
|
||||
return error.code === 'FLEET_START_FAILED';
|
||||
});
|
||||
assert.equal(failure.details.executorOutcome.reason, expectedReason);
|
||||
assert.equal(control.identity(context.worker).state, 'working');
|
||||
assert.equal(control.identity(context.worker).runtimeSessionId,
|
||||
expectedRuntimeSessionId ?? plan.actions[0].runtimeSessionId ?? 'overflow-thread');
|
||||
return failure;
|
||||
} finally { control.close(); }
|
||||
}
|
||||
|
||||
function interruptReducerAfterIdentity() {
|
||||
const expectedIdentity = '{"type":"thread.started","thread_id":"interrupted-thread"}\n';
|
||||
const childProgram = [
|
||||
'process.stdout.write(JSON.stringify({type:"thread.started",thread_id:"interrupted-thread"}) + "\\n");',
|
||||
'process.on("SIGTERM", () => process.exit(0));',
|
||||
'setInterval(() => {}, 1_000);',
|
||||
].join('');
|
||||
const helper = spawn(process.execPath, [CODEX_PROTOCOL_REDUCER, process.execPath, '-e', childProgram], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
helper.stdout.setEncoding('utf8');
|
||||
helper.stderr.setEncoding('utf8');
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let interrupted = false;
|
||||
let failure = null;
|
||||
const terminate = () => {
|
||||
if (helper.exitCode === null && helper.signalCode === null) helper.kill('SIGTERM');
|
||||
};
|
||||
const watchdog = setTimeout(() => {
|
||||
failure ??= new Error('helper did not emit its bounded identity; terminating owned fixture process');
|
||||
terminate();
|
||||
}, 1_000);
|
||||
helper.stdout.on('data', (chunk) => {
|
||||
stdout += chunk;
|
||||
try {
|
||||
if (!interrupted && stdout === expectedIdentity) {
|
||||
interrupted = true;
|
||||
assert.equal(helper.exitCode, null, 'helper must still be live when the reduced identity arrives');
|
||||
assert.equal(helper.signalCode, null, 'identity must not depend on a close event');
|
||||
assert.equal(helper.kill('SIGTERM'), true, 'parent must forward interruption through the real helper process');
|
||||
} else if (!expectedIdentity.startsWith(stdout)) {
|
||||
failure ??= new Error(`helper emitted unexpected reduced protocol bytes: ${JSON.stringify(stdout)}`);
|
||||
terminate();
|
||||
}
|
||||
} catch (error) {
|
||||
failure ??= error;
|
||||
terminate();
|
||||
}
|
||||
});
|
||||
helper.stderr.on('data', (chunk) => { stderr += chunk; });
|
||||
helper.once('error', (error) => {
|
||||
failure ??= error;
|
||||
terminate();
|
||||
});
|
||||
helper.once('close', (status, signal) => {
|
||||
clearTimeout(watchdog);
|
||||
try {
|
||||
assert.throws(() => process.kill(helper.pid, 0), { code: 'ESRCH' }, 'owned helper must be absent after close');
|
||||
if (failure) reject(failure);
|
||||
else resolve({ status, signal, stdout, stderr, interrupted });
|
||||
} catch (error) { reject(error); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function captureStdout(action) {
|
||||
const originalWrite = process.stdout.write;
|
||||
let output = '';
|
||||
process.stdout.write = function capture(chunk, ...args) {
|
||||
output += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
||||
return originalWrite.call(this, '', ...args);
|
||||
};
|
||||
try {
|
||||
return { status: await action(), output };
|
||||
} finally {
|
||||
process.stdout.write = originalWrite;
|
||||
}
|
||||
}
|
||||
|
||||
function nativeSpawnGuard({ expectedEnv, expectedMarker, delegate } = {}) {
|
||||
const calls = [];
|
||||
const guard = (command, args, options) => {
|
||||
calls.push({ command, args, options });
|
||||
assert.equal(command, process.execPath, 'Codex must launch through the real protocol reducer process');
|
||||
assert.equal(args[0], CODEX_PROTOCOL_REDUCER);
|
||||
assert.equal(args[1], 'codex');
|
||||
assert.equal(options.env, expectedEnv, 'runCli must forward the exact injected environment');
|
||||
assert.equal(options.env.PATH, expectedEnv.PATH);
|
||||
assert.equal(options.env.TORCH_FAKE_CODEX_INVOCATION_MARKER, expectedMarker);
|
||||
if (!expectedMarker) throw new Error('fixture guard rejected missing Codex invocation marker before spawn');
|
||||
return delegate(command, args, options);
|
||||
};
|
||||
guard.torchNativeSpawnGuard = true;
|
||||
return { guard, calls };
|
||||
}
|
||||
|
||||
test('SCN-executor-interrupted-outcomes: a graceful ready timeout, signal, or ENOBUFS cannot create an idle turn or disclose output', () => {
|
||||
const readyDirectory = mkdtempSync(join(tmpdir(), 'torch-executor-ready-'));
|
||||
const readyPath = join(readyDirectory, 'ready');
|
||||
const timeout = spawnSync(process.execPath, [
|
||||
'-e', 'const fs = require("node:fs"); fs.writeFileSync(process.env.TORCH_READY_PATH, "ready"); process.on("SIGTERM", () => process.exit(0)); setInterval(() => {}, 1000);',
|
||||
], {
|
||||
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 1_000, killSignal: 'SIGTERM',
|
||||
env: { ...process.env, TORCH_READY_PATH: readyPath },
|
||||
});
|
||||
assert.equal(existsSync(readyPath), true, 'child reached its readiness boundary before timeout delivery');
|
||||
assert.equal(timeout.status, 0);
|
||||
assert.equal(timeout.error?.code, 'ETIMEDOUT');
|
||||
expectInterruptedStart({ context: fixture(), result: timeout, expectedReason: 'executor-error-ETIMEDOUT' });
|
||||
|
||||
expectInterruptedStart({
|
||||
context: fixture(), result: { status: null, signal: 'SIGTERM' }, expectedReason: 'executor-signal',
|
||||
});
|
||||
|
||||
const screenshotPayload = 'image-data-'.repeat(200_000);
|
||||
const overflow = expectInterruptedStart({
|
||||
context: fixture(),
|
||||
result: {
|
||||
status: 0, error: Object.assign(new Error('private reasoning should not be reported'), { code: 'ENOBUFS' }),
|
||||
stdout: screenshotPayload,
|
||||
stderr: 'token=fixture-token password=fixture-password prompt=private-prompt',
|
||||
},
|
||||
expectedReason: 'executor-error-ENOBUFS',
|
||||
});
|
||||
assert.doesNotMatch(JSON.stringify(overflow.details), /image-data|fixture-token|fixture-password|private-prompt/);
|
||||
|
||||
const overflowContext = fixture();
|
||||
expectInterruptedStart({
|
||||
context: overflowContext,
|
||||
plan: planFor(overflowContext, { runtimeSessionId: null, requiresRuntimeIdCapture: true }),
|
||||
result: {
|
||||
status: 0, error: Object.assign(new Error('overflow'), { code: 'ENOBUFS' }),
|
||||
stdout: `{"type":"thread.started","thread_id":"overflow-thread"}\n${screenshotPayload}`,
|
||||
},
|
||||
expectedReason: 'executor-error-ENOBUFS',
|
||||
});
|
||||
});
|
||||
|
||||
test('SCN-codex-streaming-protocol-reduction: screenshot-heavy successful turns retain only bounded lifecycle facts', () => {
|
||||
const childProgram = [
|
||||
'const records = [JSON.stringify({type:"thread.started",thread_id:"streamed-thread"}), JSON.stringify({type:"item.completed",image_url:"data:image/png;base64," + "x".repeat(40000)}), JSON.stringify({type:"turn.completed"})].join("\\n") + "\\n";',
|
||||
'process.stdout.write(records);',
|
||||
'process.stderr.write("private reasoning=fixture-analysis token=fixture-token\\n");',
|
||||
].join('');
|
||||
const result = spawnSync(process.execPath, [CODEX_PROTOCOL_REDUCER, process.execPath, '-e', childProgram], {
|
||||
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 64 * 1024,
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.stdout, '{"type":"thread.started","thread_id":"streamed-thread"}\n{"type":"turn.completed"}\n');
|
||||
assert.ok(Buffer.byteLength(result.stdout) < 256);
|
||||
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /image\/png|fixture-analysis|fixture-token|x{512}/);
|
||||
const context = fixture();
|
||||
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
|
||||
try {
|
||||
startFleet({
|
||||
plan: planFor(context, { runtimeSessionId: null, requiresRuntimeIdCapture: true }), controlPlane: control,
|
||||
adapters: new Map([['codex', createCodexAdapter()]]), executor: () => result,
|
||||
});
|
||||
assert.equal(control.identity(context.worker).state, 'idle');
|
||||
assert.equal(control.identity(context.worker).runtimeSessionId, 'streamed-thread');
|
||||
} finally { control.close(); }
|
||||
});
|
||||
|
||||
test('SCN-codex-final-terminal-state: a later failed terminal overrides an earlier completion and child signals/errors remain explicit', () => {
|
||||
const failedTerminal = spawnSync(process.execPath, [CODEX_PROTOCOL_REDUCER, process.execPath, '-e', [
|
||||
'process.stdout.write([JSON.stringify({type:"thread.started",thread_id:"terminal-thread"}), JSON.stringify({type:"turn.completed"}), JSON.stringify({type:"turn.failed",code:"MODEL_NOT_SUPPORTED",message:"private reasoning=hidden"})].join("\\n") + "\\n");',
|
||||
].join('')], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
assert.equal(failedTerminal.status, 0, failedTerminal.stderr);
|
||||
assert.equal(failedTerminal.stdout, '{"type":"thread.started","thread_id":"terminal-thread"}\n{"type":"turn.failed","code":"MODEL_NOT_SUPPORTED"}\n');
|
||||
const context = fixture();
|
||||
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
|
||||
try {
|
||||
assert.throws(() => startFleet({
|
||||
plan: planFor(context, { runtimeSessionId: null, requiresRuntimeIdCapture: true }), controlPlane: control,
|
||||
adapters: new Map([['codex', createCodexAdapter()]]), executor: () => failedTerminal,
|
||||
}), (error) => error.code === 'FLEET_START_FAILED'
|
||||
&& error.details.executorOutcome.reason === 'codex-terminal-event-missing'
|
||||
&& error.details.diagnostic.code === 'MODEL_NOT_SUPPORTED');
|
||||
assert.equal(control.identity(context.worker).runtimeSessionId, 'terminal-thread');
|
||||
assert.equal(control.identity(context.worker).state, 'working');
|
||||
} finally { control.close(); }
|
||||
|
||||
const signalled = spawnSync(process.execPath, [CODEX_PROTOCOL_REDUCER, process.execPath,
|
||||
'-e', 'process.kill(process.pid, "SIGTERM")'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
assert.equal(signalled.signal, 'SIGTERM');
|
||||
expectInterruptedStart({ context: fixture(), result: signalled, expectedReason: 'executor-signal' });
|
||||
|
||||
const unavailable = spawnSync(process.execPath, [CODEX_PROTOCOL_REDUCER, '/definitely/missing-torch-runtime'], {
|
||||
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
assert.equal(unavailable.status, 1);
|
||||
assert.equal(unavailable.stdout, '{"type":"error","code":"ENOENT"}\n');
|
||||
});
|
||||
|
||||
test('SCN-codex-interrupted-identity: a real helper emits a bounded identity before deterministic parent interruption and remains uncertain', async () => {
|
||||
const result = await interruptReducerAfterIdentity();
|
||||
assert.equal(result.interrupted, true);
|
||||
assert.equal(result.status, null);
|
||||
assert.equal(result.signal, 'SIGTERM');
|
||||
assert.equal(result.stdout, '{"type":"thread.started","thread_id":"interrupted-thread"}\n');
|
||||
assert.equal(result.stderr, '');
|
||||
const context = fixture();
|
||||
expectInterruptedStart({
|
||||
context,
|
||||
plan: planFor(context, { runtimeSessionId: null, requiresRuntimeIdCapture: true }),
|
||||
result,
|
||||
expectedReason: 'executor-signal',
|
||||
expectedRuntimeSessionId: 'interrupted-thread',
|
||||
});
|
||||
});
|
||||
|
||||
test('SCN-codex-code-less-diagnostics: no-code model, authentication, and quota errors retain only safe categories', () => {
|
||||
const cases = [
|
||||
['The requested model is not supported; prompt=private-prompt', 'model-rejection'],
|
||||
['Authentication rejected this credential; private reasoning=secret-analysis', 'authentication'],
|
||||
['Quota exceeded for this account; image=data:image/png;base64,fixture-image', 'quota'],
|
||||
];
|
||||
for (const [message, category] of cases) {
|
||||
const childProgram = `process.stdout.write(${JSON.stringify(`${JSON.stringify({ type: 'error', message, prompt: 'private-prompt', reasoning: 'secret-analysis', image_url: 'data:image/png;base64,fixture-image' })}\n`)});`;
|
||||
const result = spawnSync(process.execPath, [CODEX_PROTOCOL_REDUCER, process.execPath, '-e', childProgram], {
|
||||
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.stdout, `{"type":"turn.failed","category":"${category}"}\n`);
|
||||
const diagnostic = createCodexAdapter().diagnoseStartupFailure(result);
|
||||
assert.deepEqual(diagnostic, {
|
||||
schema: 'torch.dev/runtime-startup-diagnostic/v1alpha1', outcome: 'known', category, stderr: null,
|
||||
});
|
||||
assert.doesNotMatch(`${result.stdout}\n${result.stderr}\n${JSON.stringify(diagnostic)}`,
|
||||
/private-prompt|secret-analysis|fixture-image|image\/png/);
|
||||
}
|
||||
});
|
||||
|
||||
test('SCN-cli-codex-protocol-routing: the CLI routes Codex launch output through the bounded protocol reducer', () => {
|
||||
const result = executeRuntimeLaunch(spawnSync, {
|
||||
runtime: 'codex', command: process.execPath,
|
||||
args: ['-e', 'process.stdout.write("{\\"type\\":\\"thread.started\\",\\"thread_id\\":\\"cli-thread\\"}\\n{\\"type\\":\\"turn.completed\\"}\\n")'],
|
||||
}, { cwd: process.cwd(), encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 1024 });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.stdout, '{"type":"thread.started","thread_id":"cli-thread"}\n{"type":"turn.completed"}\n');
|
||||
});
|
||||
|
||||
test('SCN-cli-codex-real-executable: an actual runCli invocation reaches the isolated Codex executable through the reducer', async () => {
|
||||
const context = fixture();
|
||||
const bin = mkdtempSync(join(tmpdir(), 'torch-codex-bin-'));
|
||||
const executable = join(bin, 'codex');
|
||||
const marker = join(bin, 'invocation.json');
|
||||
const markerEnv = 'TORCH_FAKE_CODEX_INVOCATION_MARKER';
|
||||
const inheritedMarker = process.env[markerEnv];
|
||||
writeFileSync(executable, [
|
||||
'#!/usr/bin/env node',
|
||||
'const fs = require("node:fs");',
|
||||
`const marker = process.env.${markerEnv};`,
|
||||
'if (!marker) { process.stderr.write("missing isolated invocation marker\\n"); process.exit(86); }',
|
||||
'fs.writeFileSync(marker, JSON.stringify({ cwd: process.cwd(), args: process.argv.slice(2) }));',
|
||||
'process.stdout.write(JSON.stringify({type:"thread.started",thread_id:"cli-boundary-thread"}) + "\\n");',
|
||||
'process.stdout.write(JSON.stringify({type:"turn.completed"}) + "\\n");',
|
||||
].join('\n'));
|
||||
chmodSync(executable, 0o755);
|
||||
const cliOptions = {
|
||||
cwd: context.root,
|
||||
env: { ...context.env, PATH: `${bin}:${process.env.PATH}`, [markerEnv]: marker },
|
||||
};
|
||||
const worktreeParent = mkdtempSync(join(tmpdir(), 'torch-cli-worktrees-'));
|
||||
execFileSync('git', ['-C', context.root, 'add', '.torch']);
|
||||
execFileSync('git', ['-C', context.root, 'commit', '-m', 'commit fixture installation state']);
|
||||
const worktrees = await captureStdout(() => runCli([
|
||||
'worktrees', '--parent', worktreeParent, '--yes', '--json',
|
||||
], cliOptions));
|
||||
assert.equal(worktrees.status, 0, worktrees.output);
|
||||
const missingEnv = { ...context.env, PATH: `${bin}:${process.env.PATH}` };
|
||||
const missingMarkerGuard = nativeSpawnGuard({
|
||||
expectedEnv: missingEnv, expectedMarker: undefined, delegate: spawnSync,
|
||||
});
|
||||
const missingMarker = await captureStdout(() => runCli([
|
||||
'up', '--fresh', '--only', context.worker, '--yes', '--json',
|
||||
], {
|
||||
cwd: context.root,
|
||||
env: missingEnv,
|
||||
spawn: missingMarkerGuard.guard,
|
||||
}));
|
||||
assert.equal(missingMarker.status, 1, missingMarker.output);
|
||||
assert.equal(JSON.parse(missingMarker.output).error, 'UNEXPECTED_ERROR');
|
||||
assert.equal(missingMarkerGuard.calls.length, 1, 'guard must reject before any native child starts');
|
||||
assert.equal(existsSync(marker), false, 'fake executable must fail before it can claim invocation without its marker');
|
||||
assert.ok(Buffer.byteLength(missingMarker.output) < 4 * 1024, 'failed CLI response must remain bounded');
|
||||
const validGuard = nativeSpawnGuard({
|
||||
expectedEnv: cliOptions.env, expectedMarker: marker, delegate: spawnSync,
|
||||
});
|
||||
const captured = await captureStdout(() => runCli([
|
||||
'up', '--fresh', '--only', context.worker, '--yes', '--json',
|
||||
], { ...cliOptions, spawn: validGuard.guard }));
|
||||
assert.equal(captured.status, 0, captured.output);
|
||||
assert.equal(validGuard.calls.length, 1, 'valid guard must delegate exactly one native reducer launch');
|
||||
assert.equal(process.env[markerEnv], inheritedMarker, 'runCli must not mutate the process environment');
|
||||
const invocation = JSON.parse(readFileSync(marker, 'utf8'));
|
||||
assert.equal(invocation.args.includes('exec'), true, 'the isolated executable must receive the Codex exec invocation');
|
||||
assert.equal(invocation.args.includes('--json'), true);
|
||||
assert.match(invocation.cwd, /\/core$/);
|
||||
assert.ok(Buffer.byteLength(captured.output) < 4 * 1024, 'CLI response must remain bounded');
|
||||
const response = JSON.parse(captured.output);
|
||||
assert.deepEqual(response.started, [{
|
||||
areaId: context.worker, runtimeSessionId: 'cli-boundary-thread', mode: 'create',
|
||||
}]);
|
||||
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
|
||||
try {
|
||||
assert.equal(control.identity(context.worker).state, 'idle');
|
||||
assert.equal(control.identity(context.worker).runtimeSessionId, 'cli-boundary-thread');
|
||||
} finally { control.close(); }
|
||||
});
|
||||
|
||||
test('SCN-scheduled-manager-wake-terminal-failure: the real manager-check-in boundary fails rather than invoking a successful wake receipt', () => {
|
||||
const context = fixture();
|
||||
const configPath = join(context.root, '.torch', 'torch.yaml');
|
||||
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
||||
config.runtime_wake_budget = { max_invocations_per_day: 1 };
|
||||
config.schedules.push({
|
||||
id: 'manager-check-in', title: 'Manager check-in', owner: 'session-manager', lifetime: 'system',
|
||||
trigger: { type: 'manual' }, behavior: 'coordination', required_authority: ['owner'],
|
||||
action: { type: 'manager-check-in', manager_id: 'session-manager', wake: { enabled: true, budget_mode: 'invocation-count' } },
|
||||
retry: { max_attempts: 1 }, failure_recipient: 'owner', source_of_truth: 'test fixture',
|
||||
});
|
||||
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
||||
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
|
||||
try {
|
||||
const service = new ScheduleService({
|
||||
repositoryRoot: context.root, controlPlane: control,
|
||||
wakeManager: {
|
||||
plan: ({ managerId }) => ({ plan: {
|
||||
canProceed: true, actions: [{ areaId: managerId, runtime: 'codex' }],
|
||||
}, adapters: new Map([['codex', createCodexAdapter()]]) }),
|
||||
invoke: ({ managerId, prepared }) => startFleet({
|
||||
plan: {
|
||||
canProceed: true, projectId: 'executor-outcomes-fixture', actions: [{
|
||||
areaId: managerId, runtime: 'codex', runtimeSessionId: null, requiresRuntimeIdCapture: true,
|
||||
completionState: 'idle', mode: 'create', promptFile: join(context.root, 'manager-prompt.md'),
|
||||
instructionText: 'manager wake', launch: { command: 'codex', args: [], cwd: context.root },
|
||||
}],
|
||||
}, controlPlane: control, adapters: prepared.adapters,
|
||||
executor: () => ({ status: 0, stdout: '{"type":"thread.started","thread_id":"manager-interrupted"}\n' }),
|
||||
}),
|
||||
},
|
||||
});
|
||||
const run = service.run({ scheduleId: 'manager-check-in', actorId: 'owner', approved: true });
|
||||
assert.equal(run.result, 'failed');
|
||||
assert.equal(control.identity('session-manager').state, 'working');
|
||||
assert.equal(control.identity('session-manager').runtimeSessionId, 'manager-interrupted');
|
||||
assert.equal(service.wakeReservations({ managerId: 'session-manager' })[0].outcome, 'failed');
|
||||
} finally { control.close(); }
|
||||
});
|
||||
|
||||
test('SCN-scheduled-executor-failure: a zero-status executor error records failure and keeps output bounded and redacted', () => {
|
||||
const context = fixture();
|
||||
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
|
||||
try {
|
||||
control.reportStatus({ areaId: 'session-manager', state: 'idle', summary: 'Fixture session is available.' });
|
||||
const service = new ScheduleService({
|
||||
repositoryRoot: context.root, controlPlane: control,
|
||||
executor: () => ({
|
||||
status: 0,
|
||||
error: Object.assign(new Error('private reasoning=fixture-analysis'), { code: 'ENOBUFS' }),
|
||||
stdout: 'screenshot-data-'.repeat(200_000),
|
||||
stderr: 'token=fixture-token password=fixture-password',
|
||||
}),
|
||||
});
|
||||
const run = service.run({ scheduleId: 'domain-audit', actorId: context.worker });
|
||||
assert.equal(run.result, 'failed');
|
||||
assert.equal(run.exitStatus, 0);
|
||||
assert.match(run.stdout, /\[truncated\]/);
|
||||
assert.doesNotMatch(`${run.stdout}\n${run.stderr}`, /fixture-token|fixture-password|fixture-analysis/);
|
||||
} finally { control.close(); }
|
||||
});
|
||||
@@ -15,7 +15,7 @@ import { defaultManagerCheckInSchedule, organizationGraphFromConfig } from '../.
|
||||
import { createWorktrees } from '../../src/kernel/worktrees.mjs';
|
||||
import { ResourceService } from '../../src/resources/service.mjs';
|
||||
import {
|
||||
createFleetBrief, detachFleet, planFleetDetach, planFleetDown, planFleetUp, startFleet, stopFleet,
|
||||
createFleetBrief, detachFleet, planAreaUp, planFleetDetach, planFleetDown, planFleetUp, startFleet, stopFleet,
|
||||
} from '../../src/runtime/lifecycle.mjs';
|
||||
|
||||
function fleetFixture({ workerRuntime = 'claude', hierarchy = false } = {}) {
|
||||
@@ -331,7 +331,7 @@ test('SCN-mixed-runtime: Codex and Claude share stable identities while planning
|
||||
const started = startFleet({
|
||||
plan, controlPlane: control, adapters,
|
||||
executor: (launch) => launch.areaId === fixture.worker
|
||||
? { status: 0, stdout: '{"type":"thread.started","thread_id":"codex-worker-1"}\n' }
|
||||
? { status: 0, stdout: '{"type":"thread.started","thread_id":"codex-worker-1"}\n{"type":"turn.completed"}\n' }
|
||||
: { status: 0, stdout: 'claude-manager-1\n' },
|
||||
});
|
||||
assert.deepEqual(started.started.map((item) => item.runtimeSessionId), [
|
||||
@@ -360,3 +360,124 @@ test('SCN-mixed-runtime: Codex and Claude share stable identities while planning
|
||||
assert.equal(control.identity(fixture.worker).state, 'offline');
|
||||
control.close();
|
||||
});
|
||||
|
||||
test('SCN-codex-terminal-completion: a captured Codex identity is not a completed turn without terminal evidence', () => {
|
||||
const fixture = fleetFixture({ workerRuntime: 'codex' });
|
||||
const control = openControlPlane({ repositoryRoot: fixture.root, env: fixture.env });
|
||||
const adapters = new Map([
|
||||
['claude', createClaudeAdapter()],
|
||||
['codex', createCodexAdapter({ executable: 'codex' })],
|
||||
]);
|
||||
const plan = planFleetUp({ repositoryRoot: fixture.root, controlPlane: control, adapters, fresh: true });
|
||||
assert.throws(() => startFleet({
|
||||
plan, controlPlane: control, adapters,
|
||||
executor: (launch) => launch.areaId === fixture.worker
|
||||
? { status: 0, stdout: '{"type":"thread.started","thread_id":"codex-interrupted-1"}\n' }
|
||||
: { status: 0, stdout: 'claude-manager-1\n' },
|
||||
}), (error) => error.code === 'FLEET_START_FAILED'
|
||||
&& error.details.executorOutcome.reason === 'codex-terminal-event-missing');
|
||||
assert.equal(control.identity(fixture.worker).state, 'working');
|
||||
assert.equal(control.identity(fixture.worker).runtimeSessionId, 'codex-interrupted-1');
|
||||
const duplicate = planAreaUp({
|
||||
repositoryRoot: fixture.root, controlPlane: control, areaId: fixture.worker, adapters,
|
||||
});
|
||||
assert.equal(duplicate.canProceed, false);
|
||||
assert.deepEqual(duplicate.blockers, [{
|
||||
areaId: fixture.worker, code: 'RUNTIME_IDENTITY_ACTIVE', state: 'working',
|
||||
runtimeSessionId: 'codex-interrupted-1',
|
||||
}]);
|
||||
control.close();
|
||||
});
|
||||
|
||||
test('SCN-codex-startup-diagnostics: owner-visible startup failures prefer bounded structured Codex evidence without disclosure', () => {
|
||||
const modelFixture = fleetFixture({ workerRuntime: 'codex' });
|
||||
const modelControl = openControlPlane({ repositoryRoot: modelFixture.root, env: modelFixture.env });
|
||||
const modelAdapters = new Map([
|
||||
['claude', createClaudeAdapter()],
|
||||
['codex', createCodexAdapter({ executable: 'codex' })],
|
||||
]);
|
||||
const modelPlan = planFleetUp({
|
||||
repositoryRoot: modelFixture.root, controlPlane: modelControl, adapters: modelAdapters, fresh: true,
|
||||
});
|
||||
let modelFailure;
|
||||
assert.throws(
|
||||
() => startFleet({
|
||||
plan: modelPlan, controlPlane: modelControl, adapters: modelAdapters,
|
||||
executor: (launch) => launch.areaId === modelFixture.worker
|
||||
? {
|
||||
status: 23,
|
||||
stdout: '{"type":"error","code":"MODEL_NOT_SUPPORTED","message":"Requested model fixture-model is not supported","prompt":"fixture prompt must not leak"}\n',
|
||||
stderr: 'Vercel MCP authentication warning: token=fixture-token password=fixture-password',
|
||||
}
|
||||
: { status: 0, stdout: 'claude-manager-1\n' },
|
||||
}),
|
||||
(error) => {
|
||||
modelFailure = error;
|
||||
return error.code === 'FLEET_START_FAILED' && error.details.status === 23;
|
||||
},
|
||||
);
|
||||
assert.deepEqual(modelFailure.details.diagnostic, {
|
||||
schema: 'torch.dev/runtime-startup-diagnostic/v1alpha1', outcome: 'known',
|
||||
category: 'model-rejection', code: 'MODEL_NOT_SUPPORTED',
|
||||
message: 'Requested model fixture-model is not supported',
|
||||
stderr: 'Vercel MCP authentication warning: token=[REDACTED] password=[REDACTED]',
|
||||
});
|
||||
assert.equal(modelFailure.details.stderr, modelFailure.details.diagnostic.stderr);
|
||||
assert.doesNotMatch(JSON.stringify(modelFailure.details), /fixture prompt|fixture-token|fixture-password/);
|
||||
assert.equal(modelControl.identity(modelFixture.worker).state, 'offline');
|
||||
modelControl.close();
|
||||
|
||||
const malformedFixture = fleetFixture({ workerRuntime: 'codex' });
|
||||
const malformedControl = openControlPlane({ repositoryRoot: malformedFixture.root, env: malformedFixture.env });
|
||||
const malformedAdapters = new Map([
|
||||
['claude', createClaudeAdapter()],
|
||||
['codex', createCodexAdapter({ executable: 'codex' })],
|
||||
]);
|
||||
const malformedPlan = planFleetUp({
|
||||
repositoryRoot: malformedFixture.root, controlPlane: malformedControl, adapters: malformedAdapters, fresh: true,
|
||||
});
|
||||
let malformedFailure;
|
||||
assert.throws(
|
||||
() => startFleet({
|
||||
plan: malformedPlan, controlPlane: malformedControl, adapters: malformedAdapters,
|
||||
executor: (launch) => launch.areaId === malformedFixture.worker
|
||||
? { status: 24, stdout: '{malformed Codex event', stderr: 'MCP warning' }
|
||||
: { status: 0, stdout: 'claude-manager-2\n' },
|
||||
}),
|
||||
(error) => {
|
||||
malformedFailure = error;
|
||||
return error.code === 'FLEET_START_FAILED' && error.details.status === 24;
|
||||
},
|
||||
);
|
||||
assert.equal(malformedFailure.details.diagnostic.outcome, 'unknown');
|
||||
assert.equal(malformedFailure.details.diagnostic.reason, 'stdout-malformed-json');
|
||||
assert.equal(malformedFailure.details.diagnostic.stderr, 'MCP warning');
|
||||
malformedControl.close();
|
||||
|
||||
const oversizedFixture = fleetFixture({ workerRuntime: 'codex' });
|
||||
const oversizedControl = openControlPlane({ repositoryRoot: oversizedFixture.root, env: oversizedFixture.env });
|
||||
const oversizedAdapters = new Map([
|
||||
['claude', createClaudeAdapter()],
|
||||
['codex', createCodexAdapter({ executable: 'codex' })],
|
||||
]);
|
||||
const oversizedPlan = planFleetUp({
|
||||
repositoryRoot: oversizedFixture.root, controlPlane: oversizedControl, adapters: oversizedAdapters, fresh: true,
|
||||
});
|
||||
let oversizedFailure;
|
||||
assert.throws(
|
||||
() => startFleet({
|
||||
plan: oversizedPlan, controlPlane: oversizedControl, adapters: oversizedAdapters,
|
||||
executor: (launch) => launch.areaId === oversizedFixture.worker
|
||||
? { status: 25, stdout: 'x'.repeat((16 * 1024) + 1), stderr: 'MCP warning' }
|
||||
: { status: 0, stdout: 'claude-manager-3\n' },
|
||||
}),
|
||||
(error) => {
|
||||
oversizedFailure = error;
|
||||
return error.code === 'FLEET_START_FAILED' && error.details.status === 25;
|
||||
},
|
||||
);
|
||||
assert.equal(oversizedFailure.details.diagnostic.outcome, 'unknown');
|
||||
assert.equal(oversizedFailure.details.diagnostic.reason, 'stdout-exceeds-bound');
|
||||
assert.doesNotMatch(JSON.stringify(oversizedFailure.details), /x{512}/);
|
||||
oversizedControl.close();
|
||||
});
|
||||
@@ -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/);
|
||||
|
||||
@@ -34,7 +34,7 @@ function fixture() {
|
||||
return { status: 0, stdout: 'installed' };
|
||||
}
|
||||
return { status: 0, stdout: args[0] === '--version' ? `provider ${version}`
|
||||
: args.includes('exec') ? '{"type":"thread.started","thread_id":"provider-update-fixture-session"}\n' : 'help' };
|
||||
: args.includes('exec') ? '{"type":"thread.started","thread_id":"provider-update-fixture-session"}\n{"type":"turn.completed"}\n' : 'help' };
|
||||
};
|
||||
return { env, calls, runner, next: v => { version = v; }, breakInstall: () => { failInstall = true; } };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { basename, join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { createClaudeAdapter } from '../../src/adapters/claude.mjs';
|
||||
import { createBacklogService } from '../../src/backlog/service.mjs';
|
||||
import { openControlPlane } from '../../src/control-plane/service.mjs';
|
||||
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
|
||||
import { proposeDomains } from '../../src/kernel/domains.mjs';
|
||||
import { inspectRepository } from '../../src/kernel/git.mjs';
|
||||
import { installProject } from '../../src/kernel/install.mjs';
|
||||
import { createWorktrees } from '../../src/kernel/worktrees.mjs';
|
||||
import { planAreaUp, startFleet } from '../../src/runtime/lifecycle.mjs';
|
||||
import { ScheduleService } from '../../src/schedules/service.mjs';
|
||||
|
||||
function fixture({ pausedManagerWake = false } = {}) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'torch-routine-coordination-'));
|
||||
execFileSync('git', ['init', '-b', 'main', root]);
|
||||
execFileSync('git', ['-C', root, 'config', 'user.email', '[email protected]']);
|
||||
execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']);
|
||||
mkdirSync(join(root, 'test'));
|
||||
writeFileSync(join(root, 'app.js'), 'export const app = true;\n');
|
||||
writeFileSync(join(root, 'test', 'app.test.js'), '// independent verification surface\n');
|
||||
writeFileSync(join(root, 'test', 'integration.test.js'), '// independent integration surface\n');
|
||||
execFileSync('git', ['-C', root, 'add', '.']);
|
||||
execFileSync('git', ['-C', root, 'commit', '-m', 'fixture']);
|
||||
const repository = inspectRepository(root);
|
||||
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
|
||||
if (pausedManagerWake) {
|
||||
const schedule = proposal.schedules.find((entry) => entry.action?.type === 'manager-check-in');
|
||||
assert.ok(schedule, 'fixture must configure the Session Manager check-in schedule');
|
||||
schedule.action = { ...schedule.action, wake: { enabled: false } };
|
||||
}
|
||||
proposal.review = {
|
||||
status: 'approved', reviewedAt: '2026-09-30T00:00:00Z', reviewedBy: 'fixture-owner', notes: [],
|
||||
};
|
||||
const env = { ...process.env, XDG_DATA_HOME: join(tmpdir(), `${basename(root)}-state`) };
|
||||
installProject({ repository, proposal, env, projectId: 'routine-coordination-fixture' });
|
||||
execFileSync('git', ['-C', root, 'add', '.torch']);
|
||||
execFileSync('git', ['-C', root, 'commit', '-m', 'install torch']);
|
||||
createWorktrees({ repository: inspectRepository(root),
|
||||
parentOverride: join(tmpdir(), `${basename(root)}-worktrees`) });
|
||||
const worker = proposal.domains.find((domain) => domain.id !== 'qa')?.id;
|
||||
const peer = proposal.domains.find((domain) => domain.id === 'qa')?.id;
|
||||
assert.ok(worker, 'fixture must provide an implementation specialist');
|
||||
assert.ok(peer, 'fixture must provide an independent peer');
|
||||
return { root, env, worker, peer };
|
||||
}
|
||||
|
||||
test('SCN-routine-coordination-instructions: a fresh install gives common, manager, specialist, and resume surfaces the same bounded policy', () => {
|
||||
const context = fixture();
|
||||
const files = [
|
||||
join(context.root, '.torch', 'prompts', 'COMMON.md'),
|
||||
join(context.root, '.torch', 'prompts', 'session-manager.md'),
|
||||
join(context.root, '.torch', 'prompts', `${context.worker}.md`),
|
||||
join(context.root, '.torch', 'RESUME-BRIEF.md'),
|
||||
].map((path) => readFileSync(path, 'utf8'));
|
||||
for (const text of files) {
|
||||
assert.match(text, /acknowledgement does not resolve tasks, approvals, or waits|it never resolves its linked task, approval, handoff, or wait/);
|
||||
assert.match(text, /responsible peer directly|directly with responsible peers/);
|
||||
assert.match(text, /paused execution/);
|
||||
}
|
||||
assert.match(files[0], /spending or provider changes, remote publication, release or deployment/);
|
||||
assert.match(files[1], /cannot decide an owner approval or another specialist's approval/);
|
||||
assert.match(files[2], /Do not start providers, change wake\/timer\/budget policy, dispatch arbitrary work/);
|
||||
});
|
||||
|
||||
test('SCN-routine-coordination-boundaries: acknowledgement leaves work pending and only the named approver can decide', () => {
|
||||
const context = fixture();
|
||||
const control = openControlPlane({ repositoryRoot: context.root, env: context.env,
|
||||
clock: () => new Date('2026-09-30T12:00:00Z') });
|
||||
try {
|
||||
const backlog = createBacklogService({ repositoryRoot: context.root, controlPlane: control });
|
||||
const created = backlog.create({ actorId: 'session-manager', title: 'Bounded coordination',
|
||||
description: 'Keep the wait visible until an authority acts.', acceptanceCriteria: ['Evidence is retained.'],
|
||||
affectedDomains: [context.worker] });
|
||||
const ready = backlog.transition({ taskId: created.id, actorId: 'session-manager', to: 'ready',
|
||||
expectedRevision: created.revision });
|
||||
const assigned = backlog.transition({ taskId: ready.id, actorId: 'session-manager', to: 'assigned',
|
||||
owner: context.worker, expectedRevision: ready.revision });
|
||||
const taskMessage = control.sendMessage({ sender: 'session-manager', recipient: context.worker,
|
||||
body: 'Work is assigned; preserve the decision wait.', references: { task: assigned.id } });
|
||||
control.ackMessage({ recipient: context.worker, messageId: taskMessage.id });
|
||||
assert.equal(backlog.get(assigned.id).state, 'assigned', 'reading a task message cannot complete or advance work');
|
||||
|
||||
const peerApproval = control.requestApproval({ requester: context.worker, approver: context.peer,
|
||||
task: assigned.id, title: 'Peer path consent', summary: 'Confirm the public boundary.', evidence: 'test/fleet/routine-coordination.test.mjs' });
|
||||
const peerMessage = control.readMessages({ recipient: context.peer, unacknowledgedOnly: true })
|
||||
.find((message) => message.kind === 'approval-request' && message.references.task === assigned.id);
|
||||
assert.ok(peerMessage, 'the named approver receives a durable approval request');
|
||||
control.ackMessage({ recipient: context.peer, messageId: peerMessage.id });
|
||||
assert.equal(control.listApprovals({ actorId: context.peer, status: 'pending' })[0].id, peerApproval.id,
|
||||
'acknowledging an approval request cannot decide it');
|
||||
assert.throws(() => control.decideApproval({ approvalId: peerApproval.id, decidedBy: 'session-manager',
|
||||
decision: 'approved', expectedRevision: peerApproval.revision }),
|
||||
(error) => error.code === 'APPROVER_AUTHORITY_REQUIRED');
|
||||
const peerDecision = control.decideApproval({ approvalId: peerApproval.id, decidedBy: context.peer,
|
||||
decision: 'approved', expectedRevision: peerApproval.revision, note: 'Boundary reviewed.' });
|
||||
assert.equal(peerDecision.status, 'approved');
|
||||
assert.equal(peerDecision.decidedBy, context.peer);
|
||||
|
||||
const ownerApproval = control.requestApproval({ requester: context.worker, approver: 'owner', task: assigned.id,
|
||||
title: 'Owner scope decision', summary: 'A peer cannot make this owner decision.' });
|
||||
assert.throws(() => control.decideApproval({ approvalId: ownerApproval.id, decidedBy: 'session-manager',
|
||||
decision: 'approved', expectedRevision: ownerApproval.revision }),
|
||||
(error) => error.code === 'APPROVER_AUTHORITY_REQUIRED');
|
||||
assert.equal(control.listApprovals({ actorId: context.worker, status: 'pending' }).some((item) =>
|
||||
item.id === ownerApproval.id), true, 'a rejected manager attempt leaves the owner decision pending');
|
||||
} finally {
|
||||
control.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('SCN-routine-coordination-paused-dispatch: an unapproved public start leaves work and owner waits unresolved', () => {
|
||||
const context = fixture();
|
||||
let adapterCalls = 0;
|
||||
const adapter = createClaudeAdapter({ executable: 'claude', runner: () => {
|
||||
adapterCalls++;
|
||||
throw new Error('A disabled provider must not run.');
|
||||
} });
|
||||
const control = openControlPlane({ repositoryRoot: context.root, env: context.env,
|
||||
clock: () => new Date('2026-09-30T12:00:00Z') });
|
||||
try {
|
||||
const backlog = createBacklogService({ repositoryRoot: context.root, controlPlane: control });
|
||||
const created = backlog.create({ actorId: 'session-manager', title: 'Paused dispatch boundary',
|
||||
description: 'Keep assigned work visible while provider execution is disabled.',
|
||||
acceptanceCriteria: ['The start boundary fails closed.'], affectedDomains: [context.worker] });
|
||||
const ready = backlog.transition({ taskId: created.id, actorId: 'session-manager', to: 'ready',
|
||||
expectedRevision: created.revision });
|
||||
const assigned = backlog.transition({ taskId: ready.id, actorId: 'session-manager', to: 'assigned',
|
||||
owner: context.worker, expectedRevision: ready.revision });
|
||||
const taskMessage = control.sendMessage({ sender: 'session-manager', recipient: context.worker,
|
||||
body: 'Wait for explicit provider authorization.', references: { task: assigned.id } });
|
||||
control.ackMessage({ recipient: context.worker, messageId: taskMessage.id });
|
||||
const peerApproval = control.requestApproval({ requester: context.worker, approver: context.peer,
|
||||
task: assigned.id, title: 'Peer consent', summary: 'Approve the bounded routine path.' });
|
||||
control.decideApproval({ approvalId: peerApproval.id, decidedBy: context.peer, decision: 'approved',
|
||||
expectedRevision: peerApproval.revision, note: 'Routine path reviewed.' });
|
||||
const ownerWait = control.requestApproval({ requester: context.worker, approver: 'owner', task: assigned.id,
|
||||
title: 'Owner provider decision', summary: 'Provider execution remains owner-reserved.' });
|
||||
|
||||
const plan = planAreaUp({ repositoryRoot: context.root, controlPlane: control, areaId: context.worker,
|
||||
adapter, fresh: true });
|
||||
assert.equal(plan.canProceed, true, JSON.stringify(plan.blockers));
|
||||
assert.throws(() => startFleet({ plan, controlPlane: control }),
|
||||
(error) => error.code === 'RUNTIME_EXECUTION_NOT_AUTHORIZED');
|
||||
assert.equal(adapterCalls, 0, 'the disabled public start cannot invoke a provider adapter');
|
||||
assert.equal(backlog.get(assigned.id).state, 'assigned', 'acknowledgement and refusal cannot complete work');
|
||||
assert.equal(control.listApprovals({ actorId: context.worker, status: 'pending' }).some((item) =>
|
||||
item.id === ownerWait.id), true, 'the owner wait remains visible after the blocked start');
|
||||
assert.equal(control.identity(context.worker).state, 'offline', 'a blocked start cannot report false idle or success');
|
||||
} finally {
|
||||
control.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('SCN-routine-coordination-paused-schedule-dispatch: configured paused wakes queue attention without a provider invocation', () => {
|
||||
const context = fixture({ pausedManagerWake: true });
|
||||
const at = new Date('2026-09-30T12:00:00Z');
|
||||
let providerWakeCalls = 0;
|
||||
let commandCalls = 0;
|
||||
const control = openControlPlane({ repositoryRoot: context.root, env: context.env, clock: () => at });
|
||||
try {
|
||||
const backlog = createBacklogService({ repositoryRoot: context.root, controlPlane: control });
|
||||
const created = backlog.create({ actorId: 'session-manager', title: 'Paused scheduled dispatch',
|
||||
description: 'Keep assigned work and the owner wait visible while wakes are paused.',
|
||||
acceptanceCriteria: ['The paused schedule queues review without a provider invocation.'],
|
||||
affectedDomains: [context.worker] });
|
||||
const ready = backlog.transition({ taskId: created.id, actorId: 'session-manager', to: 'ready',
|
||||
expectedRevision: created.revision });
|
||||
const assigned = backlog.transition({ taskId: ready.id, actorId: 'session-manager', to: 'assigned',
|
||||
owner: context.worker, expectedRevision: ready.revision });
|
||||
control.reportStatus({ areaId: context.worker, state: 'waiting', task: assigned.id,
|
||||
summary: 'Waiting for the owner provider decision.' });
|
||||
const ownerWait = control.requestApproval({ requester: context.worker, approver: 'owner', task: assigned.id,
|
||||
title: 'Owner provider decision', summary: 'Automatic provider execution is paused.' });
|
||||
const schedules = new ScheduleService({
|
||||
repositoryRoot: context.root, controlPlane: control, clock: () => at,
|
||||
executor: () => { commandCalls++; throw new Error('Manager check-in must not invoke a shell command.'); },
|
||||
wakeManager: {
|
||||
plan: () => { providerWakeCalls++; throw new Error('A paused schedule must not plan a provider wake.'); },
|
||||
invoke: () => { providerWakeCalls++; throw new Error('A paused schedule must not invoke a provider.'); },
|
||||
},
|
||||
});
|
||||
|
||||
const plan = schedules.plan({ scheduleId: 'session-manager-check-in', actorId: 'owner', at });
|
||||
assert.equal(plan.wakeEnabled, false, 'the installed schedule must expose an explicit paused wake policy');
|
||||
assert.deepEqual(plan.schedule.action.wake, { enabled: false });
|
||||
const dispatched = schedules.dispatchSystem({ actorId: 'owner', approved: true, at });
|
||||
assert.deepEqual(dispatched.due, ['session-manager-check-in']);
|
||||
assert.equal(dispatched.runs[0].result, 'succeeded');
|
||||
const observed = JSON.parse(dispatched.runs[0].stdout);
|
||||
assert.equal(observed.checkIn.queued, true);
|
||||
assert.equal(observed.wake.attempted, false);
|
||||
assert.equal(observed.wake.reason, 'wake-disabled');
|
||||
assert.equal(providerWakeCalls, 0, 'the public scheduled dispatch cannot plan or invoke a paused provider');
|
||||
assert.equal(commandCalls, 0, 'the typed check-in cannot fall through to a shell command');
|
||||
assert.equal(backlog.get(assigned.id).state, 'assigned', 'the scheduled review cannot complete assigned work');
|
||||
assert.equal(control.listApprovals({ actorId: context.worker, status: 'pending' }).some((item) =>
|
||||
item.id === ownerWait.id), true, 'the owner decision remains a durable wait');
|
||||
assert.equal(control.identity(context.worker).state, 'waiting', 'the worker cannot report false idle or success');
|
||||
assert.equal(control.identity('session-manager').state, 'offline', 'queued review cannot report a manager provider turn');
|
||||
assert.equal(control.readMessages({ recipient: 'session-manager', unacknowledgedOnly: true })
|
||||
.some((message) => message.kind === 'manager-check-in'), true, 'the manager receives durable attention instead of a wake');
|
||||
} finally {
|
||||
control.close();
|
||||
}
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema": "torch.dev/release/v1alpha1",
|
||||
"version": "0.1.0-alpha.2",
|
||||
"version": "0.1.0-alpha.3",
|
||||
"state": {
|
||||
"reads": ["torch.dev/state/v1alpha1"],
|
||||
"writes": "torch.dev/state/v1alpha1",
|
||||
|
||||