Skip to content

feat(dashboard): sync goal drafts across devices - #10132

Open
rubencu wants to merge 1 commit into
kirodotdev:mainfrom
rubencu:fix/mobile-goal-state-consistency
Open

rubencu wants to merge 1 commit into
kirodotdev:mainfrom
rubencu:fix/mobile-goal-state-consistency

Conversation

@rubencu

@rubencu rubencu commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Problem / Motivation

Goal: Keep inactive Set a goal drafts consistent across devices so only the latest confirmed text can start autonomous work and failures remain recoverable.

The form stored executable goal instructions only in browser-local storage. The same session could show different drafts on desktop and mobile, and a stale copy could overwrite the latest edit.

Why it matters

Pressing Start turns this text into unattended agent instructions. A stale or silently lost draft can launch the wrong goal, while a sync or clear failure must leave the latest local edit recoverable.

Not a goal

  • Synchronize chat, comment, or other non-goal drafts.
  • Change live goal-loop scheduling, pause, resume, or stop behavior.
  • Replace main's server validation or raw number-field behavior.

What changed (motivation → approach → change)

Inactive goal forms share an owner-only canonical server record behind GET and PUT /api/autonudge/draft/slot/{slot_key}. Live writes carry the adopted canonical base_updated_at; the store compares it under the write lock and returns 409 goal_draft_conflict if another write won. On a conflict, Use newer goal (Keep it cleared after a Clear) adopts the stored record; only Replace with my text replaces it, on a freshly read base. An answer moves a queued edit's base only when it holds the submitted content, so another tab's newer save or a refused migration leaves that edit to meet the conflict. A migration fills only an empty slot: over a different draft or Clear it is the same 409, so a skewed browser clock cannot replace a confirmed goal. The 5-second deadline covers the body read.

Start sends a pending draft save at once and waits (form read-only, at most 11 s); a conflict or failure stops it. Clear reads the canonical stamp before removing the loop (an open on a live loop never reads the draft), so a second device's tombstone is accepted. It registers before its DELETE, as the removed frame can remount the editor first; that editor waits for it. Updated shows only when the canonical copy changes the visible draft.

A server-confirmed cache is not a pending edit: reopening adopts another device's newer copy as Updated with no write. A migration the full store accepts and evicts at once stays local-only, so Start runs that copy and its next edit saves on the empty slot.

Goal text is clamped to the server's 8,000-code-point limit (MAX_NUDGE_MESSAGE_CHARS). Sync failures show localized ErrorNotice copy beside Retry; a conflict shows its own notice and actions. An edit during a check, failure, failed clear or conflict keeps Start disabled, described by that notice. Text typed during the check or its upload saves on the draft the user saw (a newer goal is a conflict); a newer goal comes in around a typed number; a failure or conflict ends once the store confirms the edit.

Scope Purpose
docs/feature-map/README.md and docs/system-specs/modules/learn-cron-dashboard.md Map the route and modules; document canonical drafts, compare-and-set writes, retries, tombstones and limits.
src/kiro_crew/dashboard/handlers/autonudge.py, server_runtime/mcp_routes.py, and src/kiro_crew/goal_drafts.py Implement the authenticated routes, lazy composition, validation, conflicts and canonical storage.
src/kiro_crew/constants.py and src/kiro_crew/autonudge_authz.py Define the one loop-message bound the draft store and both loop authorizers read.
src/kiro_crew/sandbox_leaves.py, src/kiro_crew/sandbox_precreate.py, and src/kiro_crew/security/paths_lists.py Precreate the goal-draft store and protect it from agents.
test/link_screen_sites.py and test/test_*.py Pin routes, storage, bounded bodies, composition (_MCP_TABLE_ROWS 324 to 327: aiohttp's HEAD, two lazy imports), security counters and sandbox behavior.
website/src/components/AutoNudgePopover.tsx, website/src/utils/goalDrafts.ts and goalDraftStartGate.ts Reconcile drafts, render checking, updated, saving, failure, conflict, Retry and clear states, and keep the browser cache bounded for offline recovery.
website/src/i18n/locales/ Supply localized status, conflict, action and Retry copy.
website/src/test/ Cover sync, conflicts, Retry, Start, number fields, clears, cache and loop controls.

Backwards compatibility

Compatible: existing browser drafts migrate into the canonical store, live loops keep current-main behavior, and an unavailable server leaves the local edit visible instead of starting stale text.

Removes nothing: goal editing, loop controls and browser fallback stay.

Tests

  • Each review regression fails before its fix (popover suites: AutoNudgePopover.<name>.test.tsx). goalDrafts.test.ts keeps a queued edit, and text typed during the upload, on its own base when a migration meets another device's goal, and a stale tab's rejected edit on its base so its reload conflicts. goalSync re-reads on Retry before replacing a goal saved elsewhere, ends a stalled read as a failed check, gives a conflict its own notice, keeps an evicted cached draft and an uncacheable confirmed goal startable, and keeps Start disabled for text or a number edited during the read (typed text meets a newer goal as a conflict; a typed number stays over it until confirmed). goalClear clears from a second device opened on the live loop, keeps text typed during a clear across its remount and closes once it lands; an editor the removed frame remounts before the DELETE answers waits for the Clear, then closes or shows Retry. goalMigration keeps text typed during the opening upload: another device's goal is a conflict, a confirmed upload saves it. goalCacheResult: a superseded save blocks Start; a confirmed uncached edit ends a failed check.
  • New regressions fail on c12f01579b: test_goal_drafts.py (a skewed migration over a confirmed goal or Clear conflicts, store and HTTP 409), goalDrafts.test.ts (a refused browser-only copy stays pending until Replace) and goalConflictActions (both actions, no Retry; adopting writes nothing; a Clear's notice), also failing on dcb0c7c3b4 (a cleared conflict offers Keep it cleared) and 80eb54a677 (a draft saved during Use newer goal stays a conflict).
  • goalStartWait fails on ed71b22286: Start during a pending draft save created the loop before its 409 or failure; now it creates none and shows that notice, or creates the saved text on success.
  • On this patch (base 3cdcfe893c), 1,736 backend and 1,364 vitest tests, TypeScript, i18n:check and the static gates passed; one host-only failure is outside the diff.

Manual verification

Synthetic-data screenshots below.

Screenshots / video

State Desktop Mobile
Checking saved goal, Start disabled Desktop checking Mobile checking
Updated from another device Desktop updated Mobile updated
Sync failed, Retry Desktop sync failed Mobile sync failed
Clear failed, Retry Desktop clear failed Mobile clear failed
Newer goal elsewhere: Use newer goal or Replace with my text Desktop conflict Mobile conflict
Cleared elsewhere: Keep it cleared or Replace with my text Desktop cleared conflict Mobile cleared conflict
Start waiting on the draft save Desktop Start waiting Mobile Start waiting

Related Issues

Part of #9052 (review follow-up)

Checklist

  • At most two commits, with a Conventional Commits title
  • Existing tests pass and regression tests cover synchronization and recovery
  • Self-review completed and code follows project style guidelines
  • Documentation and locales updated
  • Current-head desktop and mobile evidence captured and attached by the maintainer
  • No secrets, credentials, or internal references in the diff

Contribution License Agreement

Contributions are accepted under the repository's Apache 2.0 license.

@rubencu
rubencu requested review from a team and cixuuz September 11, 2026 15:02
@github-actions github-actions Bot added the fork Pull request from a fork (external contributor) label Sep 11, 2026
@rubencu

rubencu commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Intent: Make one session’s Set a goal draft converge to the latest user edit across desktop and mobile while preserving the existing offline local fallback.
Not a goal: Change goal-loop scheduling, the popover’s visual design, or synchronize ordinary chat composer drafts.

@github-actions github-actions Bot added the readiness: action required A blocking check or review needs attention label Sep 11, 2026
@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

UX Review (Opus 5.5, fork) — 🔴 BLOCK

UX-level review of 00a9245cccd6d479c5f4fd1f3020356de0cc6cc7 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS does not.

UX-Verdict: BLOCK

The conflict notice makes the user choose between two goal texts while showing only one of them, and either choice permanently discards the other text with no way to undo it.

Blockers

Choosing between goals without seeing the newer one, and no undo
Trigger: the user edits a goal on one device after a newer goal was saved on another. The form then shows "A newer goal was saved on another device. Use it, or replace it with your text." (attachment-09 and attachment-10).
Outcome: the textarea shows only the local text, and the newer goal's wording appears nowhere. "Use newer goal" drops "this device's pending edit" (diff, goal-draft-use-newer onClick). "Replace with my text" overwrites the other device's goal. The user has to pick blind, and whichever text loses is gone, even though it may be a long set of instructions for an agent that will run unattended. "Keep it cleared" (attachment-11) has the same problem: the typed text is thrown away with no way back.
Severity: rare (it needs a conflict between two devices) × high impact (lost or wrong agent instructions) × permanent (no undo).
Fix: show the newer goal's text, for example quoted inside the notice. After either action, keep the discarded text recoverable, such as a "Restore my text" link next to the field until the next edit.

Notes

  • The notice says "Use it, or replace it with your text." and the two buttons right below it say the same thing. Change the notice to say what happened and what is at stake, for example: "A newer goal was saved on another device. Pick which one to keep."

No first-time reader has looked at these screens: this lane gets no blind read, so nobody has checked whether a new user understands them.

[UX-REVIEWED] 00a9245

Description read

[DESCRIPTION-READ] 3e21e61fea3aa64e7e2fd4ce6bff62eed104d9422d20b4421d682c7fc27111a9

That sha256 names everything this verdict read: the PR description, plus the 17 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with .github/scripts/pr-description-capture.sh over the same inputs; if it differs, the description or that evidence changed after this verdict was formed and any finding drawn from either is unproven.

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Design Review (Opus 5.5, fork) — ✅ PASS

Design-level review of 00a9245cccd6d479c5f4fd1f3020356de0cc6cc7 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS does not.

Design-Verdict: PASS

Nothing blocks: compare-and-set on a server record is the right shape for drafts that become unattended instructions, and the screenshots were committed.

Notes

  • The client reconciliation in goalDrafts.ts and AutoNudgePopover.tsx (states for pending, retained, superseded, conflict, clear in progress and migration) is far larger than the server's simple compare-and-set. A later simplification could drop the browser copy as a source of truth once migration has run, leaving it only as an offline cache.

[DESIGN-REVIEWED] 00a9245

Description read

[DESCRIPTION-READ] 14d9be764a5f7dffc4ef28e9fdbe881a48f0bf8b917bf50b9f0fbf0ec321e1cb

That sha256 names everything this verdict read: the PR description, plus the 1 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with .github/scripts/pr-description-capture.sh over the same inputs; if it differs, the description or that evidence changed after this verdict was formed and any finding drawn from either is unproven.

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

GPT 6.1 Review (fork) — 🔴 changes requested (blocking)

Reviewed 00a9245cccd6d479c5f4fd1f3020356de0cc6cc7 via the fork AI-review pipeline; updated in place on each push.

1 of 1 blocking finding(s) are security-class and were withheld from adjudication, so the blocking verdict stands.

BLOCKING -- website/src/components/AutoNudgePopover.tsx:1054 -- An unedited editor can start a superseded goal

if (flushed && (!(await saveDraftBeforeStart(setDraftSyncState, () => persistCurrentDraft(latest.current))) || !latest.current.open)) return
Trigger: Two devices open the same inactive session’s goal A. Device B saves goal B; device A remains open without editing and presses Start. Its open-time check is not repeated, hasEdited.current remains false, and startNow() posts A through /api/autonudge to authorize_and_add_nudge(), which does not check the canonical draft.
Outcome: The gateway arms autonomous work with superseded instructions A while the canonical draft holds B, without showing a conflict.
Anchor: defect
Fix: Include the adopted canonical draft version in Start and check it at the server’s arm/commit boundary, rejecting a changed version before creating the loop, including when the editor is unedited.

[BLOCK-MERGE] 00a9245
[GPT-REVIEWED] 00a9245

Adjudication (Opus 5.5) — is blocking on each finding proportionate?

F1 (fenced): the condition is plausible, so the finding stands. Two devices with the same inactive session's editor open is the exact case this PR adds sync for. The device that saved nothing keeps hasEdited.current false, which skips the flush at AutoNudgePopover.tsx:2083-2085 in the diff. Start then posts formFields(), the stale goal A. Nothing in authorize_and_add_nudge (src/kiro_crew/autonudge_authz.py:964, unchanged apart from the char-limit constant) checks the stored draft. The harm is limited: the user starts the goal text shown on their screen, and the loop is visible and can be stopped. But none of the conditions is extreme enough to support a FLAG.

[ADJUDICATION] 00a9245cccd6d479c5f4fd1f3020356de0cc6cc7 total=0 uphold=0 downgrade=0
[GPT-ADJUDICATED] 00a9245cccd6d479c5f4fd1f3020356de0cc6cc7

[ADJUDICATION-FENCED] 00a9245cccd6d479c5f4fd1f3020356de0cc6cc7 fenced=1 flagged=0
UPHOLD-FENCED F1 website/src/components/AutoNudgePopover.tsx:1054 -- Two open devices plus an unedited Start is an ordinary condition (hasEdited stays false, so formFields() posts the stale goal and authorize_and_add_nudge never checks the stored draft), not an extreme one, so the bar for a FLAG is not met.
[GPT-ADJUDICATED-FENCED] 00a9245cccd6d479c5f4fd1f3020356de0cc6cc7

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

First Principles Review (Opus 5.5, fork) — ✅ PASS

Premise-level review of 00a9245cccd6d479c5f4fd1f3020356de0cc6cc7 via the fork AI-review pipeline — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS does not.

First-Principles-Verdict: PASS

Nothing blocks: every item has a named harm, and I found no existing cross-device draft mechanism to reuse.

Notes

  • Item 9 — oversized: the browser protocol (tombstones, base stamps, queued-edit rebasing, two conflict actions, Retry variants) is far past "latest confirmed text starts the loop". Subtraction: drop the separate "Keep it cleared" and Clear-conflict wording and the migration clock-skew machinery; treat any 409 as one conflict with one Use-newer action.
  • Item 10 — rides along: the 15 s / 86,400 s / 2,147,483,647 numeric clamps in draftToPersist change what the number fields accept, and the "Not a goal" section excludes replacing raw number-field behavior. Subtraction: remove clampInteger and the GOAL_DRAFT_MIN_IDLE_SECS, GOAL_DRAFT_MAX_IDLE_SECS and GOAL_DRAFT_MAX_CYCLES constants.

What this change ships

Inventory (10 items) — 8 justified

Intent: keep the inactive goal form identical across devices so Start runs the latest confirmed text. ADDITION.

  1. Owner-only GET,PUT /api/autonudge/draft/slot/{slot_key} and the canonical goal_drafts.py store — justified
  2. A stale write now gets a 409 conflict instead of overwriting — justified
  3. Start sends a pending save first, waits up to 11 s, and stops on a conflict or failure — justified
  4. Clear writes a tombstone before removing the loop — justified
  5. Updated / checking / failed / conflict notices with Retry, plus localized copy — justified
  6. goal-drafts is hidden from the agent and precreated for the sandbox — justified
  7. MAX_NUDGE_MESSAGE_CHARS replaces the literal 8000 in both authorizers — justified
  8. Browser drafts move to the mc-goal-drafts-v2 key, with legacy migration — justified
  9. Conflict resolution UI and clock-skew handling — oversized
  10. Number-field clamps — rides along

[FIRST-PRINCIPLES-REVIEWED] 00a9245

Description read

[DESCRIPTION-READ] 12434bffb370ba730edf6db9b45b2f1efa3868c7f7fa5a4b3a86b732a84b4062

That sha256 names the PR description this verdict read. Recompute it with .github/scripts/pr-description-capture.sh; if it differs, the description changed after this verdict was formed and any finding drawn from the description is unproven.

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Opus 5.5 Review (fork) — ✅ no blocking findings

Reviewed 00a9245cccd6d479c5f4fd1f3020356de0cc6cc7 via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 00a9245

@rubencu
rubencu force-pushed the fix/mobile-goal-state-consistency branch 3 times, most recently from 6fe8200 to f5ccc36 Compare September 13, 2026 23:19
@rubencu

rubencu commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

self-added: yes
mechanism: agent-fenced canonical goal-draft directory

  • Fixed agent-writable goal store span=7a51058d7a46

Canonical goal text now lives under the top-level goal-drafts directory. That whole directory is on both the shared file-tool sensitive floor and every agent process sandbox hidden set.
The gateway opens the store directly, creates it owner-only, and still uses atomic owner-restricted writes. A prompt-injected same-UID agent can no longer forge the next goal through the file, lock, or atomic-write sibling.

Fixed in f5ccc36. Focused store and sandbox parity tests pass.

@rubencu

rubencu commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

self-added: yes
mechanism: explicit draft-record validation before tombstone dispatch

  • Fixed malformed draft erasure span=d9852f0154e8

A tombstone now requires the JSON draft member to be explicitly present and null. Every non-null draft must contain message, idle_secs, and max_cycles, and a null message is rejected before GoalDraftStore.put runs.
The regression test seeds a valid canonical draft, submits omitted, empty, and null-message objects, and proves all return 400 while the original goal remains.

Fixed in f5ccc36.

@rubencu

rubencu commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

self-added: yes
mechanism: structured goal-draft sync error notice

  • Fixed sync failures bypassing ErrorNotice span=f9eb25b1dcc6

The failed reconciliation branch now renders ErrorNotice with role=alert instead of a hand-styled status paragraph. The adjacent no-hand-off comment records that this popover owns the unsaved goal draft and recovery must preserve it in place.
Checking and updated remain non-error status messages. The focused component test proves the failure is an alert and the local draft remains unchanged.

Fixed in f5ccc36.

@github-actions

github-actions Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Security Scope Review (fork) — ✅ PASS

Which legitimate operations does 00a9245cccd6d479c5f4fd1f3020356de0cc6cc7 newly refuse? Model proposes, scripts/deny_diff.py decides — updated in place on each push. A script-confirmed regression blocks PR readiness.

adjudicated, zero confirmed regressions

Scope-Verdict: PASS

Reason: The only stricter mechanism in this PR is one new goal-drafts data-home leaf, added to the agent file-tool fence and to the sandbox hide/precreate lists. None of the three shell deny tiers matches paths in command text. So the 15 shell candidates probe the boundary, but the real guards are path and sandbox guards that no one could classify; that gap is named under UNADJUDICATED and does not block.

TIGHTENING: _CREW_SECRET_LEAVES (and through it _SENSITIVE_HOME_DIRS and _KEYSTONE_ARTIFACT_PARENTS), _CREW_HIDDEN_LEAVES, and _CREW_PRECREATE_HIDDEN_DIR_LEAVES each gain "goal-drafts" -- they refuse a prompt-injected same-UID agent that tries to forge the next Start prompt by writing the store file directly: + # A prompt-injected same-UID agent must not be able to forge that next prompt / + # by writing the backing file directly; ... + "goal-drafts", (src/kiro_crew/security/paths_lists.py), and + # Mask the directory so an agent cannot forge a prompt through its JSON file, lock, or atomic-write sibling temp. + "goal-drafts", (src/kiro_crew/sandbox_leaves.py).

ADJUDICATED: 15 candidates, 0 confirmed newly-refused

PLATFORM COVERAGE: Candidates cover posix and windows. The diff adds one path leaf under the crew data-home prefixes. The posix rows (~/.kiro/crew, / separators) probe the parent listing, the next-door config.json, a glob over the parent, and a same-name project directory outside the data home. The windows rows probe the same neighbours with %USERPROFILE%\.kiro\crew and \ separators in cmd (dir, type, findstr) and with $env:USERPROFILE in PowerShell (Get-ChildItem). The PR's own test and grep workflows have posix rows, and the pytest run also has a windows row. No any rows were written, because every row names a path or an executable. No row uses the legacy .kirocrew prefix, which is also a _CREW_HOME_PREFIXES entry.

UNADJUDICATED:

  • Guard: is_sensitive_path / is_sensitive_write_path (src/kiro_crew/security/paths.py), the agent file-tool gate in hooks.on_tool_call. Call path: an agent file read/write/edit tool call → GateFacts path target → _path_in_home_dirs(resolved, _SENSITIVE_HOME_DIRS), which now contains <crew-prefix>/goal-drafts. Also _is_keystone_publish_artifact → _KEYSTONE_ARTIFACT_PARENTS. This parent is the crew data-home root, which was already a parent through other root-level leaves, so .tmp/.lock files at the root should not newly match. Inputs I would have submitted: a file-tool read of <data home>/config.json, of <data home>/sessions.db, and of a project-local docs/goal-drafts/notes.md; the same reads with Windows drive-letter and \ spellings; and a read of <legacy .kirocrew home>/config.json. A false match is unlikely because the rule anchors on target + os.sep, but nothing gave a verdict on it.
  • Guard: the sandbox bind-mask and precreate of goal-drafts (_CREW_HIDDEN_LEAVES, _CREW_PRECREATE_HIDDEN_DIR_LEAVES), applied at namespace launch. Call path: session spawn → sandbox tier mount plan → empty placeholder mounted over <data home>/goal-drafts. Inputs I would have submitted: an in-sandbox ls ~/.kiro/crew and cat ~/.kiro/crew/config.json (these are in the candidates as shell rows, but the classifier judges command text, not the mount table), and a sandboxed process that legitimately writes a sibling data-home directory, to confirm the precreate does not shift sibling leaves. The leaf-count change (standard: 299→302, cc: 306→309, strict: 307→310) is three entries per tier, as the test comment states.
  • The new owner-gated GET,PUT /api/autonudge/draft/slot/{slot_key} validators (slot length ≤512, message ≤ MAX_NUDGE_MESSAGE_CHARS, idle_secs 1..86400) are new routes, not tightened ones. The refactor of the existing 8000-char bound in autonudge_authz.py into MAX_NUDGE_MESSAGE_CHARS = 8000 keeps the same value and the same len unit, so existing loop messages are not newly refused.

[SCOPE-REVIEWED] 00a9245

Security scope review -- adjudicated candidates

  • macos-latest (platform posix): 10 classified, 0 newly refused, 10 still allowed.
  • ubuntu-latest (platform posix): 10 classified, 0 newly refused, 10 still allowed.
  • windows-latest (platform windows): 5 classified, 0 newly refused, 5 still allowed.

No candidate flipped from allowed to refused.

@rubencu
rubencu force-pushed the fix/mobile-goal-state-consistency branch from f5ccc36 to d7369c8 Compare September 14, 2026 02:07
@rubencu

rubencu commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Disposition for an earlier head, kept for the record (no longer a ledger record):

  • fixed live-slot capacity pruning span=7ca377344eb3

The accepted live slot is now protected through count and byte pruning; capacity pressure evicts the oldest other slot, and regressions cover 50 future-skewed migrations. Fixed in d7369c8.

  • fixed uncached reconciliation span=1bda1fe2ef82

Goal-draft reads and writes now use React Query query/mutation state with one process-wide per-slot write queue and bounded requests. Fixed in d7369c8.

@rubencu

rubencu commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Disposition for an earlier head, kept for the record (no longer a ledger record):

  • fixed remotely-invalid local values span=2cbfa926034e

Every locally accepted idle/cycle value is normalized to the server integer bounds before persistence, so reopen cannot enter a permanent 400 loop. Fixed in d7369c8.

  • fixed pre-launch sandbox exposure span=356c4e6acf32

goal-drafts is precreated before namespace launch and remains on both hidden/sensitive floors, closing the absent-mask race. Fixed in d7369c8.

  • fixed remount write ordering and failed-write recovery span=2cbfa926034e

Per-slot tails live at module scope across remounts; failures render ErrorNotice with Retry while preserving the local copy. Fixed in d7369c8.

  • fixed timestamp/body rollback span=7b7188e85e08

If body persistence fails after the sidecar write, both disk and in-memory timestamps roll back, preventing an old body from carrying a failed write's newer conflict stamp. Fixed in d7369c8.

@rubencu

rubencu commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Disposition for an earlier head, kept for the record (no longer a ledger record):

  • fixed unbounded reconciliation gate span=732998da0365

Both GET and PUT abort after five seconds and enter the existing failed state, so Start cannot remain disabled on a stalled gateway. Fixed in d7369c8.

  • rebutted remove committed evidence span=08fa92f52fa4

temp-screenshots is this repository's established review-evidence convention, confirmed by First Principles against more than 80 existing files; the two bounded images document user-visible states and current raw links are in the PR body.

  • rebutted extract a reusable hook now span=732998da0365

This is the only synchronized draft form. Extracting a second abstraction before another consumer exists would widen the PR without reducing a concrete correctness risk; focused tests pin the state machine in place.

@rubencu

rubencu commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Disposition for an earlier head, kept for the record (no longer a ledger record):

  • fixed failed-state Retry span=c7dc47ca3b86

Failed live writes now use the popover's ErrorNotice plus in-place Retry pattern while retaining the local draft. Fixed in d7369c8.

  • fixed indefinite checking state span=c7dc47ca3b86

Remote reads and writes have a five-second abort bound and failures re-enable Start through the explicit failed state. Fixed in d7369c8.

  • rebutted reserve permanent status height span=c7dc47ca3b86

The checking line is intentionally immediate and transient; reserving empty height on every normal form open adds persistent whitespace to avoid a minor one-line transition.

  • rebutted attachment-only evidence requirement span=cde01ba37907

The repository-standard committed screenshots are linked from the current public fork head; the failed path uses the established ErrorNotice component and is directly regression-tested with the local draft preserved.

  • rebutted replace draft with goal in failure copy span=184517a7b0d1

The message distinguishes the unsent draft from the running goal and immediately states that this device's copy remains; changing that noun would blur the recovery contract.

@rubencu

rubencu commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Disposition for an earlier head, kept for the record (no longer a ledger record):

  • fixed feature framing span=01b392f0c13b

The single commit is typed feat and the refreshed body explicitly inventories the canonical store, endpoints, ordering, status UI, and recovery behavior.

  • fixed undeclared edit-time accessor span=82eea35ddf2d

The body and owning spec now state that first-sync migration uses bounded browser edit times while live writes use server arrival order; the accessor exists only to carry that declared conflict timestamp.

@rubencu

rubencu commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author
  • rebutted legitimate-operation regression span=23712bc79af1

goal-drafts is introduced by this PR specifically as executable prompt text whose only writer is the owner-gated dashboard route. Refusing agent file-tool and sandbox access is the declared security contract, while owner recovery remains available through the route; no pre-existing operation was removed.

@rubencu rubencu changed the title fix(dashboard): sync goal drafts across devices feat(dashboard): sync goal drafts across devices Sep 14, 2026
@rubencu
rubencu force-pushed the fix/mobile-goal-state-consistency branch from d7369c8 to 03b45aa Compare September 14, 2026 02:47
@rubencu

rubencu commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author
  • fixed live edit timestamp regression span=615a397dbf1e

A live browser edit now defaults to max(Date.now(), previous canonical/local timestamp + 1), while explicit server timestamps remain unchanged. Clock lag can no longer let stale remote text outrank a newer failed-PUT edit.
The monotonic regression fails when the max rule is removed and passes in 03b45aa.

@rubencu

rubencu commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author
  • fixed sidecar-write in-memory divergence span=53b12cb5cd39

A failed timestamp-sidecar write now restores the prior in-memory timestamp map before returning; body-write failure still restores both memory and persisted sidecar.
The sidecar-failure regression observes the advanced timestamp when this restore is removed and passes in 03b45aa.

@rubencu

rubencu commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author
  • fixed Retry erasing the only edit span=f9eb25b1dcc6

Failed local persistence records the current editor snapshot in a module-owned per-slot fallback. Retry/remount reconciles from that snapshot before localStorage and clears it only after the server accepts a canonical value.
The local+remote failure regression loses the second PUT when this preference is removed and passes in 03b45aa.

@rubencu

rubencu commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto 56b568a2be; the new head is 376630f658. There was one conflict, a count pin:

The other 48 paths have the same diff as before. The security package budget (32,533 lines) and the MCP route-table pin (312 rows) are unchanged.

Tests:

  • Backend: 5,603 scoped tests passed. The only failure was the boot-order pin, which needs a built website/dist and passes without one (65/65).
  • Frontend: 1,476 scoped vitest tests passed.
  • Other checks passed: TypeScript, ESLint, the i18n checks, pseudolocale freshness, gen:ui --check, the static gates, the line cap and docs lint.

@rubencu

rubencu commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto 1c57acaff3; the new head is 6d0a93128b. There was one conflict, a pin I re-measured on the rebased tree:

  • MCP route-table pin in test/test_dashboard_server_composition_contract_reach.py. Main's table now has 310 rows, and this PR adds the HEAD, GET and PUT draft routes. The pin is now 313 rows, digest 77f52ad4….

The other 48 paths have the same diff as before. The lazy-import pin (95) and the security package budget (32,533 lines) are unchanged. The description now names the new base and test counts, and its route-table numbers now read 310 to 313.

Tests, run on c24c7bc8d3:

  • Backend: 14,335 scoped tests passed. The only failure was the boot-order pin, which needs a built website/dist and passes without one (65/65).
  • Frontend: 2,141 vitest tests passed across 91 files. This host's npm registry token has expired, so the four dev dependencies main added in build(web): add React Compiler opt-in list with bailout test and per-file lint #23634 can't be installed here. vitest ran with main's vite.config.ts minus its @rolldown/plugin-babel import. Main's React Compiler file list is empty, so that plugin is never applied anyway. Main's reactCompilerAllowlist.test.ts was left out for the same reason.
  • Other checks passed: TypeScript, ESLint, the i18n checks, pseudolocale freshness, gen:ui --check, the static gates, the line cap and docs lint.

Main then moved to 1c57acaff3 (#23814, #23764: Telegram and tests only, no shared file). On that base, the tests those commits touched, plus the composition contracts, passed (120, and 65 without a built website/dist).

@rubencu

rubencu commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto c4ce1efa5d; the new head is f8dfd3cebe. Main #23645 (the gateway-only member-records leaf) touched two of this PR's pins:

  • Security package line budget in test/test_security_gate_liveness.py (conflict). Main raised it by 1, from 32,526 to 32,527. Main's note and this PR's note are both kept, and this PR's note now reads "Raised again, from 32,527". The package now measures 32,534 lines: main plus this PR's unchanged 7.
  • Leaf-only name count in test/test_sandbox_protected_name_holds.py. The conflict was only the comment, where main's member-records paragraph is now kept after this PR's goal-drafts one. The EXPECTED counts merged without a conflict, but were wrong after the merge: main and this PR each moved them from 290/297/298 to 293/300/301, so git kept one copy of the identical edit. The tree now holds 296/303/304 leaf-only names (main's 3 per tier plus this PR's 3), and the pin now says so. With the merged value the test fails ("standard now holds 296 … up from 293").

The other 47 paths have the same diff as before. The MCP route-table pin (313) and the lazy-import pin (95) are unchanged.

Tests:

  • Backend: 18,758 scoped tests passed. Besides the boot-order pin, which needs a built website/dist and passes without one (65/65), two test_masked_leaf_staged_publish.py cases for the ops-mission-control-* leaves failed under xdist. The same selection fails them the same way on a tree without this PR, and the file passes alone (8/8).
  • Frontend: 2,410 vitest tests passed across 104 files. They ran with main's vite.config.ts minus its unused @rolldown/plugin-babel import, because this host's npm registry token has expired.
  • Other checks passed: TypeScript, ESLint, the i18n checks, pseudolocale freshness, gen:ui --check, the static gates, the line cap and docs lint. The description now names the new base and counts.

@rubencu

rubencu commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto 03f3b2aad5; the new head is 80ce530b89. There was one conflict, a pin I re-measured on the rebased tree:

The other 48 paths have the same diff as before. The lazy-import pin (95), the leaf-only counts (296/303/304) and the security package budget (32,534) are unchanged.

Tests, run on d612350b69:

  • Backend: 20,343 scoped tests passed. Two kinds of failure, both recorded as not from this PR:
    • The boot-order pin, which needs a built website/dist and passes without one (65/65).
    • Two test_masked_leaf_staged_publish.py cases for ops-mission-control-policy under xdist, which fail the same way without this PR.
  • Frontend: 2,509 vitest tests passed across 108 files, run with main's config minus its unused @rolldown/plugin-babel import.
  • Other checks passed: TypeScript, ESLint, the i18n checks, pseudolocale freshness, gen:ui --check, the static gates, the line cap and docs lint.

Main then moved twice: #23907, website only, then #23912, #23913, #23915 and #23920, backend only. Neither touched a file this PR changes. On each base I re-ran what those commits touched: 3 vitest tests, then 219 backend tests. The composition contracts passed both times (65 without a built website/dist).

@rubencu

rubencu commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto 813e25c8fe; the new head is f93d6554ac. The rebase had no textual conflict, but one pin was wrong after the merge:

  • Lazy-import count pin in test/test_dashboard_server_composition_contract.py. Main's own count reached 95, and the line assert len(expected) == 95 was identical on both sides, so git kept it unchanged. The tree has 97 lazy imports: main's 95 plus this PR's api_goal_draft_get and api_goal_draft_put. With the merged value the test fails (assert 97 == 95), and the pin is now 97.

The other 48 paths have the same diff as before. The MCP route-table pin (314), the leaf-only counts (296/303/304) and the security package budget (32,534) are unchanged.

Tests, run on efc8f03203:

  • Backend: 28,027 scoped tests passed. The three failures were all recorded as not from this PR:
    • The boot-order pin, which needs a built website/dist and passes without one (65/65).
    • The enterprise-baseline case in test_governance_policy_viewer.py, which fails the same way on a clean main checkout here.
    • One test_masked_leaf_staged_publish.py xdist case.
  • Frontend: 3,945 vitest tests passed across 149 files, run with main's config minus its unused @rolldown/plugin-babel import.
  • Other checks passed: TypeScript, ESLint, the i18n checks, pseudolocale freshness, gen:ui --check, the static gates, the line cap and docs lint. The description now names the new base and counts.

Main then moved to 813e25c8fe (#23537, #23278). Its only shared file is learn-cron-dashboard.md, which merged cleanly. On that base, the tests those commits touched passed: 82 backend and 15 vitest, with the composition contracts at 65.

@rubencu

rubencu commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Fixed GPT's upheld F2 on f93d655 (AutoNudgePopover.tsx:509: a browser-cache failure blocked a server-confirmed goal), and rebased onto 6e94918705.

  • settle no longer maps retained to a sync failure. retained means the store confirmed the goal and only this browser's cache write failed. The module keeps that copy in memory, so the open check and Retry settle it like persisted (idle, or Updated when the canonical copy changes the visible draft), and Start stays available.
  • New regression in AutoNudgePopover.goalSync.test.tsx: keeps a server-confirmed goal startable when this browser cannot cache it. Storage refuses every goal-record write while the store answers the read. It fails on f93d655 (the sync error shows and Start is disabled) and passes here, and Start's create sends the confirmed text.
  • The Clear result handling is unchanged: keeps a local clear-write failure visible instead of closing as if both stores cleared still passes.
  • F1 (:963, downgraded on f93d655) is not in this push.
  • Main's learn-cron-dashboard.md and locale catalog changes merged cleanly. Pins unchanged: MCP table 314 rows, lazy imports 97, security budget 32,534.
  • Checks: 28,057 scoped backend tests, the dist-less composition contract (65), 3,947 vitest tests, TypeScript, zero-warning ESLint, gen:ui --check, pseudolocale freshness, i18n:check, lint:i18n, the file line cap and the static gates. The backend selection ran on 4fe3d2e879. 6e94918705 adds test: build the governance viewer context before switching to enterprise #23933's fix for test_governance_policy_viewer.py (its 33 tests pass here) and new website files this PR does not import. Four further failures also fail without this PR: the dist-built boot-order pin (passes dist-less) and 3 test_masked_leaf_staged_publish cases that fail only on this host. This host cannot install @rolldown/plugin-babel, so vitest ran without the React Compiler pass, which perf(web): compile ChatPaneNotices with the React Compiler #23899 applies only to ChatPaneNotices.tsx, a file this PR does not touch.

@rubencu

rubencu commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Fixed GPT's upheld finding on 8b39a9f (goalDrafts.ts:793: reopening during the close-flush save showed a false "another device" conflict), and rebased onto 09816d215f.

  • goalDrafts.ts exposes the slot's queued remote writes (pendingRemoteGoalDraftWrite), and the popover's open check waits for them before its GET. Read earlier, the GET returned the stamp the in-flight save was about to replace, and the reconciliation queued a second PUT on that stamp, which met the first save as a 409. The wait is bounded by the write's own five-second deadline, and Start stays disabled under the existing check while it runs.
  • New regression in AutoNudgePopover.goalMigration.test.tsx: reopening while the close flush still saves reads the copy that save confirms, not a conflict. The store mock applies the real compare-and-set. On 8b39a9f the reopened editor shows the conflict notice; here it shows the saved goal with Start enabled, and the close flush is the only PUT. It lives in that file because adding it to AutoNudgePopover.goalSync.test.tsx would put that file over the 2,000-line cap.
  • Checks: the 544-file backend selection (30,257 passed; the 2 failures are this host's dist-built boot-order pin, which passes dist-less, and a test_masked_leaf_staged_publish case that fails only here), the dist-less composition contract (65), 3,950 vitest tests including the goal-draft suites, TypeScript, ESLint, gen:ui --check, i18n:check, lint:i18n, the file line cap, the static gates and --check-body. This host cannot install @rolldown/plugin-babel, so vitest ran without the React Compiler pass, which perf(web): compile ChatPaneNotices with the React Compiler #23899 applies only to ChatPaneNotices.tsx, a file this PR does not touch. The body's Tests bullet names the new case.

@rubencu

rubencu commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto badd8f5aa8; the new head is 1b95b799b7. Main #23333 (Web Push, with its gateway-only web-push leaf) touched two of this PR's pins:

  • Security package line budget in test/test_security_gate_liveness.py (conflict). Main raised it by 3, from 32,527 to 32,530. Main's note and this PR's note are both kept, and this PR's note now reads "Raised again, from 32,530". The package now measures 32,537 lines: main plus this PR's unchanged 7.
  • Leaf-only name count in test/test_sandbox_protected_name_holds.py. The conflict was only the comment, where main's web-push paragraph is now kept. The EXPECTED counts merged without a conflict, but were wrong after the merge: main and this PR each moved them from 293/300/301 to 296/303/304, so git kept one copy of the identical edit. The tree now holds 299/306/307 leaf-only names (main's 3 per tier plus this PR's 3), and the pin now says so. With the merged value the test fails ("standard now holds 299 … up from 296").

The other 47 paths have the same diff as before. sandbox_leaves.py, sandbox_precreate.py and paths_lists.py merged cleanly and hold both web-push and goal-drafts; the JSON body guard register holds main's two push rows and this PR's goal-draft row.

Tests:

  • Backend: 33,619 scoped tests passed on 19877bc051. Five failed, all specific to this host. The boot-order pin needs a built website/dist and passes without one (65/65). Three test_cli.py::TestDoctor cases fail the same way without this PR. One test_masked_leaf_staged_publish.py case for an ops-mission-control-* leaf failed under xdist, and the file passes alone (8/8).
  • Frontend: 4,298 vitest tests passed across 160 files. They ran with main's vite.config.ts minus the React Compiler plugin, because this host's npm registry token has expired, so ChatPaneNotices.tsx ran uncompiled here.
  • Other checks passed: TypeScript, ESLint, the i18n checks, pseudolocale freshness, gen:ui --check, the static gates, the line cap and the description check. The description now names the new base and counts.

Main then moved to badd8f5aa8 (#24052, testing/harness.py only; no shared file). On that base, test/test_harness.py and this PR's tests passed (255, the boot-order pin aside, and 65 without a built website/dist).

@rubencu

rubencu commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto 9d31193afe; the new head is c276be41d5. Main touched two of this PR's pins:

  • Security package line budget in test/test_security_gate_liveness.py (conflict). Main perf(redaction): native single-pass credential gate, Python fallback #19628 (native single-pass credential gate) raised it by 71, from 32,530 to 32,601. Main's note and this PR's note are both kept, and this PR's note now reads "Raised again, from 32,601". The package now measures 32,608 lines: main plus this PR's unchanged 7.
  • Lazy-import count in test/test_dashboard_server_composition_contract.py (conflict). Main fix: exit an enterprise boot that has no kiro login or no governance rules #23968 added kiro_crew.enterprise_boot → require_kiro_login and moved the count from 95 to 96; this PR adds api_goal_draft_get and api_goal_draft_put and had moved it from 95 to 97. The table now holds 98 entries, and the pin now says so; the test passes at 98.

The other 47 paths have the same diff as on 1b95b799b7.

Tests:

@rubencu

rubencu commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 44f868f25d, rebased onto 533afdaa69. It fixes both findings GPT 6.1 upheld on c276be41d5 (comment), in applyCanonicalIfCurrent in AutoNudgePopover.tsx:

  • F1, a superseded save left stale text startable. If another tab saved a newer goal before this tab's live-edit answer arrived, the answer was dropped and the editor kept the older goal with Start enabled. When the editor still shows the submitted goal, it now enters the conflict state, as the open-time check already does when a record it did not adopt wins. Start stays disabled until Retry reconciles the editor.
  • F2, a confirmed edit this browser could not cache stayed failed. Only a persisted answer cleared a failed or conflict notice, so an edit the store confirmed but this browser kept only in memory (retained) left Start disabled. retained now clears it too, as settle already does.

The regressions are in a new website/src/test/AutoNudgePopover.goalCacheResult.test.tsx. Both fail on c276be41d5 and pass here: with the fix, another tab's newer save before the answer shows the conflict notice and disables Start, and a store-confirmed edit under full browser storage clears the failed check and enables Start. The rest of the patch has the same diff as on c276be41d5.

Tests:

  • Frontend: 4,765 vitest tests passed across 184 files, including the goal suites and the files main touched since the previous base. They ran with main's vite.config.ts minus the React Compiler plugin, because this host's npm registry token has expired, so ChatPaneNotices.tsx ran uncompiled here. TypeScript, ESLint, the i18n checks, pseudolocale freshness, gen:ui --check and the flake-pattern ratchet passed.
  • Backend, on 533afdaa69: the 70 test files main touched since the previous base, the memory-file pruning test test: date the recent history day from today in the prune seam test #24191 fixed, and this PR's tests passed (4,977). The one failure is the boot-order pin, which needs a built website/dist and passes without one (65/65). The static gates, the line cap and the description check passed.

The description's Tests bullet now names the new file and gives the vitest count and the new base.

@rubencu

rubencu commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto cca03bd528; the new head is c6e6660c02. There was one conflict, the security package line budget in test/test_security_gate_liveness.py:

  • Main fix(security): extend native config write tier #23935 (native config write tier) raised the budget by 12, from 32,601 to 32,613, for the crewmate folder's state files in paths_lists. Main's note and this PR's note are both kept, and this PR's note now reads "Raised again, from 32,613". The package now measures 32,620 lines: main's 32,613 plus this PR's unchanged 7. The pin is now 32,620.

The rest of the patch merged without conflicts, including src/kiro_crew/security/paths_lists.py beside main's edit: the other 49 paths have the same diff as on 44f868f25d. The hidden-leaf pin stays 299/306/307 (main's 296/303/304 plus this PR's three) and the lazy-import pin stays 98 (main's 96 plus this PR's two).

Tests, run on 2356a1ba1c:

Main then moved to cca03bd528 (#24474, slotDirtyBeacon.ts). On that base, its two vitest files passed (15) and the line cap passed.

@rubencu

rubencu commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto a3ee04f574; the new head is c60c11ce46. There was one conflict, the MCP route table pin in test/test_dashboard_server_composition_contract_reach.py:

  • Main feat(mcp): session_set_color tints a session the caller created #15428 (session_set_color) added POST /api/session-control/set-color to _register_mcp_routes and moved the pin to 312 rows. This PR adds GET, HEAD and PUT /api/autonudge/draft/slot/{slot_key}. server_runtime/mcp_routes.py merged without a conflict, and the pin now reads 315 rows with the digest of the merged table, measured from _register_mcp_routes on this tree. test_the_mcp_route_table_keeps_its_rows_and_order and test_a_literal_mcp_route_precedes_the_pattern_that_would_swallow_it pass.

The other 49 paths have the same diff as on c6e6660c02.

Tests:

@rubencu

rubencu commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto 6a00426 to clear a conflict with #24392 in test/test_security_gate_liveness.py. Main raised _PACKAGE_LINE_BUDGET from 32,613 to 32,616 for the enterprise install-tree write fence, and this PR raised it for the goal-draft hidden leaves. The resolution keeps both notes, restarts this PR's note from 32,616 and pins the measured total, 32,623 lines (this PR's 7 on top of main's 32,616). No code changed, and the rest of the patch is identical.

@rubencu

rubencu commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the GPT 6.1 blocking finding on 729a068 (AutoNudgePopover.tsx:404). A 409 goal_draft_conflict that arrived after the user typed again was dropped: persistDraft set the conflict state only while the editor still matched the submitted text, so Start stayed enabled on the rejected base.

The catch now shows a conflict for the same open editor session even when the text changed. The session is captured when the write is queued, so a late answer after a close and reopen is still ignored. A plain write failure keeps the text-match check.

  • Regression: website/src/test/AutoNudgePopover.goalConflictRace.test.tsx fails on 729a068 (no conflict notice) and passes on e03ac42.
  • Also rebased onto d8e04b9. Main's delta merged cleanly, locale files included.
  • Checks passed: all AutoNudge test files (161), main's new delta (906 Python, 156 vitest), typecheck, eslint, the i18n checks, the line cap and the static gates. The CI reds on 729a068 were main's strict XPASS in test_crew_log_fullscan_guard.py (backend shard 2) and the Coverage Gate after that shard failed.

@rubencu

rubencu commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto cd89e05 as 07a25de. This resolves the _PACKAGE_LINE_BUDGET conflict with #23840 in test/test_security_gate_liveness.py. Main's note stays first, and this PR's note now starts from 32,635. The measured package total is 32,642 (main + 7). The rest of the patch is identical to 3860ac3.

@rubencu

rubencu commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main to resolve conflicts. Each resolution keeps both sides:

No code of this PR changed. The body's file table now lists AutoNudgePopover.tsx and goalDrafts.ts in one row, to stay within eight rows. This PR's tests, the tests main changed since the last push, vitest, tsc, the composition contracts and the static gates pass. The only failures are ones main shows on this host too.

@rubencu

rubencu commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Pushed dcb0c7c, rebased onto main bf47e1598b without conflicts. It carries two review fixes.

  • Conflict state (UX lane). Retry no longer overwrites a newer goal it never showed. In the conflict state there are two actions, each with its own string: "Use newer goal" re-reads the server record, shows it in the editor and keeps it, dropping this device's pending edit; "Replace with my text" is the explicit compare-and-set that Retry used to do there. Retry keeps its label and behavior for a failed sync and a failed Clear. A Clear made on another device has its own notice. The new strings are in every catalog and en-XA, and components.autoNudgePopover.retry in bn.json and hi.json is main's wording again. The two conflict screenshots are re-captured as 05-conflict-two-actions-{desktop,mobile}.png.
  • Migration over a newer record (GPT goal_drafts.py:294). A first sync no longer replaces a confirmed goal or Clear on the server because the browser's clock says it is newer. GoalDraftStore.put answers a migration over an existing record with different content as a 409 goal_draft_conflict; an empty slot or an identical draft still migrates. The client keeps the browser copy as a pending edit and shows the conflict state above, so only "Replace with my text" sends it. The spec and the description no longer state the timestamp rule.
  • Regressions that fail on c12f015: 6 in test_goal_drafts.py (store and HTTP), and 18 in vitest across goalDrafts.test.ts, the new AutoNudgePopover.goalConflictActions.test.tsx and the conflict flows in the goalSync, goalMigration, goalConflictRace and goalCacheResult tests.
  • On bf47e1598b: 1,436 backend and contract tests, 767 vitest tests in 28 files, TypeScript, i18n:check and the static gates pass. The one failure, the dashboard boot-pin test, fails on main on this host too. The new non-English strings have not been checked by a native speaker.

@rubencu

rubencu commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Known minor point: when another device cleared the goal, the conflict notice says so, but the action that keeps it cleared still reads "Use newer goal". A separate label would add one more string to every catalog, so it is left out unless the UX lane raises it.

@rubencu

rubencu commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 80eb54a, rebased onto main 6f4cdf1f8b without conflicts. It fixes the UX lane block on dcb0c7c: when another device cleared the goal, the conflict notice said "Keep it cleared" but the first action still read "Use newer goal".

  • In that state the first action now reads "Keep it cleared" (new key components.autoNudgePopover.keep_it_cleared). It still adopts the cleared record. "Replace with my text" is unchanged, and the newer-goal conflict keeps "Use newer goal".
  • New screenshots of the cleared state: 06-conflict-cleared-{desktop,mobile}.png.
  • The translations for the new key and for the four conflict strings this PR added or changed now come from the repo pipeline (scripts/i18n-translate.mjs plan, emit, verify, merge) for all 11 locales, replacing the earlier hand translations. verify reported no findings. retry in bn and hi stays at main's wording.
  • AutoNudgePopover.goalConflictActions.test.tsx checks that the cleared conflict shows "Keep it cleared" and no "Use newer goal", and that it keeps the goal cleared with no extra write. It fails on dcb0c7c.
  • On 6f4cdf1f8b: 2,608 vitest tests in 104 files, TypeScript, i18n:check, 1,498 backend and contract tests and the static gates pass. The one failure, the dashboard boot-pin test, fails on main on this host too.

@rubencu

rubencu commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

self-added: yes
mechanism: adopt-canonical cache subject captured before the read

  • span=f9eb25b1dcc6 AutoNudgePopover.tsx:591: Delayed adoption erases another tab's pending draft

fixed in ed71b22: "Use newer goal" now caches the answer against the local copy read just before its GET, so a draft another tab saved during the GET makes the cache step return superseded and shows the conflict with both copies kept. "Use newer goal keeps another tab's draft saved during its read as a conflict" in AutoNudgePopover.goalConflictActions.test.tsx fails on 80eb54a.

@rubencu

rubencu commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

self-added: no

  • span=f9eb25b1dcc6 AutoNudgePopover.tsx:525: Start enables before text typed during migration is confirmed

Rebutted: the store's answer confirmed this tab's own submission, so the tab has no signal of the other device's save. Start in that window is the same pre-debounce window every idle edit has, runs exactly the visible typed text, and the later 409 keeps the other device's draft. Adjudication downgraded it as disproportionate.

@rubencu

rubencu commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Pushed ed71b22, rebased onto main 3cdcfe893c. The rebase resolved two pin conflicts: the MCP route table pin is now 325 rows (main's 322 plus this PR's 3), and _PACKAGE_LINE_BUDGET is 32,806 (main's 32,799 plus this PR's 7, equal to the measured total). This push also answers both GPT findings on 80eb54a.

  • AutoNudgePopover.tsx:591, fixed. "Use newer goal" now caches the answer against the local copy read just before its GET. A draft another tab saved during the GET makes the cache step return superseded, so the conflict notice shows and both copies are kept. "Use newer goal keeps another tab's draft saved during its read as a conflict" in AutoNudgePopover.goalConflictActions.test.tsx fails on 80eb54a.
  • AutoNudgePopover.tsx:525, no change. The store's answer confirmed this tab's own submission, so the tab has no signal that another device saved. Start in that window is the same window every edit has before its debounced write: it runs exactly the visible text, and the other device's draft is kept because its later write gets a 409. The adjudicator downgraded this finding as a disproportionate remedy, and the lane's fix would add a new waiting state to a file at 1,595 of its 1,600-line split limit.
  • On 9fb1d4aa52, 1,785 vitest tests, TypeScript, i18n:check, the backend tests the PR and main touch, the composition contracts and the static gates pass; the only failure also fails on main on this host. After the final re-sync onto 3cdcfe893c, main's newly changed test file and the composition contracts pass again.

@rubencu

rubencu commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author
  • span=f9eb25b1dcc6 AutoNudgePopover.tsx:992: Start bypasses the pending draft's conflict check

acknowledged: the window is real. With one owner on two devices, a Start pressed inside the edit's debounce window, after the other device saved, creates the loop before the stale save gets its 409. The loop runs the text the user can see, the create still passes the server's ownership, admission and message-cap checks, the 409 keeps the other device's goal on the server, and Clear removes the loop. There is no code change for it on this head.

@rubencu

rubencu commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

mechanism: Start sends a pending draft save at once and starts only from the store's confirmed answer

  • span=f9eb25b1dcc6 AutoNudgePopover.tsx:992: Start bypasses the pending draft's conflict check

fixed in 17c89e3: when the form has an edit the store has not confirmed, Start sends that save at once, keeps the form read-only and shows "Saving…", and creates the loop only from the text the store confirmed. A 409 creates no loop and leaves the existing conflict notice; a failure, or the 11-second bound, creates no loop and leaves the existing failure notice; with no pending save Start is unchanged. "creates no loop when the save Start waits on is refused as a conflict" and "creates no loop when the save Start waits on fails" in AutoNudgePopover.goalStartWait.test.tsx fail on ed71b22.

@rubencu

rubencu commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 17c89e3, rebased onto main 6b5bedad1b without conflicts. It fixes the GPT lane's finding on ed71b22 (AutoNudgePopover.tsx:992, Start before the pending draft save answers).

  • When the form has an edit the store has not confirmed, Start now sends that save at once and waits for its answer. Meanwhile Start is disabled, the goal and number fields are read-only, and the status line in Start's row reads "Saving…". Start then creates the loop from the text the store confirmed.
  • A 409 creates no loop and leaves the existing conflict notice and its actions. A failed save creates no loop and leaves the existing failure notice with Retry. The wait is bounded at 11 seconds (two 5-second request deadlines plus one second) and then ends as a failure. With no pending save, Start behaves as before.
  • The wait lives in a new website/src/utils/goalDraftStartGate.ts (49 lines), so AutoNudgePopover.tsx stays at 1,600 lines. "Saving…" reuses the existing components.markdownPanel.saving string, so there are no new strings. New screenshots 07-start-waiting-desktop.png and 07-start-waiting-mobile.png show the waiting state.
  • In AutoNudgePopover.goalStartWait.test.tsx, "creates no loop when the save Start waits on is refused as a conflict", "creates no loop when the save Start waits on fails" and "shows Start waiting with the form read-only, then creates the loop from the saved text" fail on ed71b22, and "starts at once with no pending save" pins the unchanged path. goalDraftStartGate.test.ts covers the result mapping and a save that never answers. Five existing tests now expect the draft save before the create POST instead of after it.
  • On 6b5bedad1b, 1,680 vitest tests (every AutoNudgePopover, goal-draft and SessionAutomationPopover suite, src/i18n, and the web tests main changed), TypeScript, i18n:check, the backend tests main changed, the composition contracts and the static gates pass; the only failure also fails on main on this host.

@rubencu

rubencu commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

acknowledged: the window is real. With one owner on two devices, a Start pressed inside the edit's debounce window, after the other device saved, creates the loop before the stale save gets its 409.

The fix in 17c89e3 supersedes this disposition: Start now waits for a pending draft save, and a conflict or a failure stops it before any loop is created.

@rubencu

rubencu commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author
  • span=f9eb25b1dcc6 AutoNudgePopover.tsx:1009: Start can launch a superseded canonical draft

Rebutted: with no unsaved edit, Start posts the goal the editor is showing, so the loop runs exactly the text the person saw when they pressed Start, and the loop is visible and can be cleared. Checking for a newer canonical draft here would need Start to send the draft version and POST /api/autonudge to check it under the draft store's lock, a new contract between the draft store and the loop handlers for a rare cross-device window. Adjudication downgraded it as disproportionate.

The inactive Set a goal form kept its draft only in browser storage, so
desktop and mobile could show different executable goal text for the
same session. Add an owner-only canonical goal-draft store behind
GET/PUT /api/autonudge/draft/slot/{slot_key}, masked from every agent
sandbox and refused by the file tools, and reconcile the popover with
it on open through React Query with a five-second bound.

Start stays disabled during that bounded check. A draft the check shows
becomes the form's pristine baseline. Start's create request keeps the
existing goal fencing unchanged and uploads the edited draft only after
the create succeeded. Clear writes a timestamped tombstone after the
loop record is gone and closes once it is canonical. Pause, Play, the
cycle count, the Done state and changed-field saves keep their current
behaviour; loop writes still send the typed values and adopt what the
loop service stored, while a saved draft is held to the store's bounds.

GoalDraftStore._read_unlocked joins the link-screen baseline as a site
that refuses every link: it reads a gateway-owned file in a directory
no sandboxed process can see, and it never traverses a link it found.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fork Pull request from a fork (external contributor) merge conflict Branch has merge conflicts with its base — author must resolve before merge needs-author-decision PR blocked on author input readiness: action required A blocking check or review needs attention

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants