Repository navigation
Conversation
|
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. |
UX Review (Opus 5.5, fork) — 🔴 BLOCKUX-level review of 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. BlockersChoosing between goals without seeing the newer one, and no undo Notes
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 |
Design Review (Opus 5.5, fork) — ✅ PASSDesign-level review of 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
[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 |
GPT 6.1 Review (fork) — 🔴 changes requested (blocking)Reviewed 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
[BLOCK-MERGE] 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 |
First Principles Review (Opus 5.5, fork) — ✅ PASSPremise-level review of First-Principles-Verdict: PASS Nothing blocks: every item has a named harm, and I found no existing cross-device draft mechanism to reuse. Notes
What this change shipsInventory (10 items) — 8 justifiedIntent: keep the inactive goal form identical across devices so Start runs the latest confirmed text. ADDITION.
[FIRST-PRINCIPLES-REVIEWED] 00a9245 Description read[DESCRIPTION-READ] 12434bffb370ba730edf6db9b45b2f1efa3868c7f7fa5a4b3a86b732a84b4062 That sha256 names the PR description this verdict read. Recompute it with |
Opus 5.5 Review (fork) — ✅ no blocking findingsReviewed |
6fe8200 to
f5ccc36
Compare
|
self-added: yes
Fixed in |
|
self-added: yes
Fixed in |
|
self-added: yes
Fixed in |
Security Scope Review (fork) — ✅ PASSWhich legitimate operations does adjudicated, zero confirmed regressions Scope-Verdict: PASS Reason: The only stricter mechanism in this PR is one new TIGHTENING: 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 ( UNADJUDICATED:
[SCOPE-REVIEWED] 00a9245Security scope review -- adjudicated candidates
No candidate flipped from allowed to refused. |
f5ccc36 to
d7369c8
Compare
|
Disposition for an earlier head, kept for the record (no longer a ledger record):
|
|
Disposition for an earlier head, kept for the record (no longer a ledger record):
|
|
Disposition for an earlier head, kept for the record (no longer a ledger record):
|
|
Disposition for an earlier head, kept for the record (no longer a ledger record):
|
|
Disposition for an earlier head, kept for the record (no longer a ledger record):
|
|
d7369c8 to
03b45aa
Compare
|
|
|
|
Rebased onto
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:
|
|
Rebased onto
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
Main then moved to |
|
Rebased onto
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:
|
|
Rebased onto
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
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 |
|
Rebased onto
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
Main then moved to |
|
Fixed GPT's upheld F2 on f93d655 (
|
|
Fixed GPT's upheld finding on 8b39a9f (
|
|
Rebased onto
The other 47 paths have the same diff as before. Tests:
Main then moved to |
|
Rebased onto
The other 47 paths have the same diff as on Tests:
|
|
Pushed
The regressions are in a new Tests:
The description's Tests bullet now names the new file and gives the vitest count and the new base. |
|
Rebased onto
The rest of the patch merged without conflicts, including Tests, run on
Main then moved to |
|
Rebased onto
The other 49 paths have the same diff as on Tests:
|
|
Rebased onto 6a00426 to clear a conflict with #24392 in
|
|
Addressed the GPT 6.1 blocking finding on 729a068 ( 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.
|
|
Rebased onto main to resolve conflicts. Each resolution keeps both sides:
No code of this PR changed. The body's file table now lists |
|
Pushed dcb0c7c, rebased onto main
|
|
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. |
|
Pushed 80eb54a, rebased onto main
|
|
self-added: yes
|
|
self-added: no
|
|
Pushed ed71b22, rebased onto main
|
|
|
mechanism: Start sends a pending draft save at once and starts only from the store's confirmed answer
|
|
Pushed 17c89e3, rebased onto main
|
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. |
|
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.
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
What changed (motivation → approach → change)
Inactive goal forms share an owner-only canonical server record behind
GETandPUT /api/autonudge/draft/slot/{slot_key}. Live writes carry the adopted canonicalbase_updated_at; the store compares it under the write lock and returns409 goal_draft_conflictif 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
removedframe 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 localizedErrorNoticecopy 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.docs/feature-map/README.mdanddocs/system-specs/modules/learn-cron-dashboard.mdsrc/kiro_crew/dashboard/handlers/autonudge.py,server_runtime/mcp_routes.py, andsrc/kiro_crew/goal_drafts.pysrc/kiro_crew/constants.pyandsrc/kiro_crew/autonudge_authz.pysrc/kiro_crew/sandbox_leaves.py,src/kiro_crew/sandbox_precreate.py, andsrc/kiro_crew/security/paths_lists.pytest/link_screen_sites.pyandtest/test_*.py_MCP_TABLE_ROWS324 to 327: aiohttp'sHEAD, two lazy imports), security counters and sandbox behavior.website/src/components/AutoNudgePopover.tsx,website/src/utils/goalDrafts.tsandgoalDraftStartGate.tswebsite/src/i18n/locales/website/src/test/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
AutoNudgePopover.<name>.test.tsx).goalDrafts.test.tskeeps 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.goalSyncre-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).goalClearclears 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 theremovedframe remounts before the DELETE answers waits for the Clear, then closes or shows Retry.goalMigrationkeeps 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.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) andgoalConflictActions(both actions, no Retry; adopting writes nothing; a Clear's notice), also failing ondcb0c7c3b4(a cleared conflict offers Keep it cleared) and80eb54a677(a draft saved during Use newer goal stays a conflict).goalStartWaitfails oned71b22286: 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.3cdcfe893c), 1,736 backend and 1,364 vitest tests, TypeScript,i18n:checkand the static gates passed; one host-only failure is outside the diff.Manual verification
Synthetic-data screenshots below.
Screenshots / video
Related Issues
Part of #9052 (review follow-up)
Checklist
Contribution License Agreement
Contributions are accepted under the repository's Apache 2.0 license.