diff --git a/docs/superpowers/plans/2026-07-12-alpha-activity-journal.md b/docs/superpowers/plans/2026-07-12-alpha-activity-journal.md new file mode 100644 index 0000000..88741fb --- /dev/null +++ b/docs/superpowers/plans/2026-07-12-alpha-activity-journal.md @@ -0,0 +1,240 @@ +# Alpha Activity And Journal Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking. + +**Goal:** Make Activity a durable, background-maintained session log that safely proposes Journal entries without creating a Дело automatically. + +**Architecture:** Desktop adds a plugin background-service lifecycle, unsubscribe-capable event bridge, and append-only Activity log. The Activity plugin owns session reconstruction and watermarks; Journal receives an explicit reviewed candidate and requires a destination for unassigned sessions. + +**Tech Stack:** Go, Wails, Svelte, plain JavaScript plugin bundles, Go tests, Node smoke tests, Playwright. + +## Global Constraints + +- Activity raw events are retained for 60 days, 10,000 events, or 8 MiB, whichever is reached first. +- Raw activity uses append/compaction, not settings.json rewrites. +- Session scope is workspaceId plus path cache or unassigned. +- Point-event duration is capped at 10 minutes per adjacent pair; gap over 20 minutes starts another session. +- Accepted/dismissed watermarks consume only the reviewed slice. +- Journal save is always an explicit user action. + +--- + +### Task 1: Add background services, event unsubscription, and an append-only Activity log + +**Files:** +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/plugin/plugin.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/plugin/plugin_test.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/events/bus.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/storage/api.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/storage/api_test.go +- Create: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-host/BackgroundPluginHost.svelte +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/App.svelte +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-host/VerstakPluginAPI.js +- Modify: /home/mirivlad/git/verstak2/verstak-sdk/src/types.ts +- Modify: /home/mirivlad/git/verstak2/verstak-sdk/schemas/contributions.json +- Modify: /home/mirivlad/git/verstak2/verstak-sdk/schemas/manifest.json +- Modify: /home/mirivlad/git/verstak2/verstak-sdk/src/plugin-api.test.ts + +**Interfaces:** +- Adds contributes.backgroundServices with id and component fields. +- Bus Subscribe returns subscription ID; Unsubscribe removes only that handler. +- Adds ActivityLogAppend, ActivityLogRead, ActivityLogDeleteWorkspace, and ActivityLogCompact backend methods. +- Background bundle registration exposes start(api) returning a cleanup function. + +- [ ] **Step 1: Write failing manifest, bus, and storage tests** + +Add a manifest validation test and isolated bus-unsubscribe test: + +~~~go +subA := bus.Subscribe("browser.activity.domain", handlerA) +subB := bus.Subscribe("browser.activity.domain", handlerB) +bus.Unsubscribe("browser.activity.domain", subA) +bus.Publish(events.Event{Name: "browser.activity.domain"}) +if gotA != 0 || gotB != 1 { t.Fatalf("wrong selective unsubscribe: %d %d", gotA, gotB) } +~~~ + +Add an append test asserting two appends create two NDJSON records and a +compaction test keeping only the newest record under a small test limit. + +- [ ] **Step 2: Run red** + +Run: + +~~~bash +cd /home/mirivlad/git/verstak2/verstak-desktop +GOCACHE=/tmp/verstak-go-cache go test ./internal/core/plugin ./internal/core/events ./internal/core/storage ./internal/api -run 'Test.*(Background|Unsubscribe|ActivityLog)' -count=1 +~~~ + +Expected: FAIL because subscriptions have no IDs and the log API does not exist. + +- [ ] **Step 3: Implement the platform boundary** + +Define: + +~~~go +type ContributionBackgroundService struct { + ID string `json:"id"` + Component string `json:"component"` +} + +type ActivityLogRecord struct { + ActivityID string `json:"activityId"` + Payload json.RawMessage `json:"payload"` +} +~~~ + +Append NDJSON under plugin-data/verstak.activity/events.ndjson using a locked +append. Compact to the declared retention limits by rewriting a temporary file. +Keep only compact candidate state in plugin data. BackgroundPluginHost loads +enabled plugins with a background contribution once at app startup and calls the +registered start function; it calls cleanup on disable/reload/destroy. + +- [ ] **Step 4: Run green and commit** + +Run: + +~~~bash +GOCACHE=/tmp/verstak-go-cache go test ./internal/core/plugin ./internal/core/events ./internal/core/storage ./internal/api -count=1 +git add internal/core/plugin internal/core/events internal/core/storage internal/api frontend/src/App.svelte frontend/src/lib/plugin-host +git commit -m "feat: add background activity runtime" +git -C /home/mirivlad/git/verstak2/verstak-sdk add src/types.ts schemas/contributions.json schemas/manifest.json src/plugin-api.test.ts +git -C /home/mirivlad/git/verstak2/verstak-sdk commit -m "feat: define plugin background services" +~~~ + +Expected: focused tests prove one subscription, selective cleanup, append, and compaction. + +### Task 2: Rebuild Activity sessions and persistent candidate watermarks + +**Files:** +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/plugin.json +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/frontend/src/index.js +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/locales/en.json +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/locales/ru.json +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-activity-plugin.js + +**Interfaces:** +- Activity background service registers ActivityService.start(api). +- Event record includes activityId, scope, occurredAt, durationSeconds, and sessionId. +- Candidate contains sessionId, handledThrough, estimatedMinutes, dateSlices, and source activities. + +- [ ] **Step 1: Write failing smoke scenarios** + +Add three explicit scenarios: + +~~~js +const nineMinutes = eventsAt('2026-07-12T10:00:00Z', '2026-07-12T10:09:00Z'); +assert.equal(buildSessions(nineMinutes)[0].estimatedMinutes, 9); +const late = appendLateEvent(existingSession, eventAt('2026-07-12T09:58:00Z')); +assert.equal(late.sessionId, existingSession.sessionId); +assert.equal(candidateAfterDismiss.sourceActivityIds.includes('a'), false); +~~~ + +Also assert a 23:50 to 00:30 session has one session ID and date slices for both +local dates. + +- [ ] **Step 2: Run red** + +Run: + +~~~bash +cd /home/mirivlad/git/verstak2/verstak-official-plugins +node scripts/smoke-activity-plugin.js +~~~ + +Expected: FAIL because Activity derives candidate IDs from first and last events. + +- [ ] **Step 3: Implement background session state** + +Replace view-mounted event recording with ActivityService.start(api). Persist +immutable generated session IDs and anchors in activity-state.json. Build scope +as either workspaceId plus root path or unassigned. Sum explicit browser +duration and only zero-duration adjacent point intervals, cap a point interval +at 10 minutes, split at 20 minutes, and cap a session at 120 minutes. + +For accept or dismiss, store the ordered handledThrough watermark. New events +after the watermark require another 10 minutes before a new candidate appears; +late events at or before the watermark remain diagnostic only. + +- [ ] **Step 4: Run green and commit** + +Run: + +~~~bash +node scripts/smoke-activity-plugin.js +./scripts/check.sh +git add plugins/activity scripts/smoke-activity-plugin.js +git commit -m "feat: persist activity sessions and watermarks" +~~~ + +Expected: session, late-event, dismissal, browser-duration, and midnight smoke tests pass. + +### Task 3: Review candidates safely in Journal and expose sessions in UI + +**Files:** +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/frontend/src/index.js +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/journal/frontend/src/index.js +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/journal/locales/en.json +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/journal/locales/ru.json +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-journal-plugin.js +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/activity.spec.js +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/todo.spec.js +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/test/wails-mock.js + +**Interfaces:** +- Journal candidate request accepts destinationWorkspaceId only for unassigned scope. +- Saved Journal entry stores sourceSessionId and handledThrough in addition to activity IDs. +- Activity UI has Sessions default and Events diagnostic view. + +- [ ] **Step 1: Write failing E2E and smoke assertions** + +Require an unassigned candidate to show a destination selector and reject Save +until an active workspace ID is selected: + +~~~js +await page.getByRole('button', { name: /review/i }).click(); +await expect(page.getByText(/choose.*дело/i)).toBeVisible(); +await expect(page.getByRole('button', { name: /save/i })).toBeDisabled(); +~~~ + +Require the mock WritePluginSetting path to retain sourceSessionId after reload. +Require Clear Activity and Journal Delete to show cancellation-safe confirmation +before changing stored rows. + +- [ ] **Step 2: Run red** + +Run: + +~~~bash +cd /home/mirivlad/git/verstak2/verstak-desktop/frontend +npm run test:e2e -- activity.spec.js todo.spec.js +~~~ + +Expected: FAIL because the current mock discards settings and Journal only accepts path-scoped candidates. + +- [ ] **Step 3: Implement review UI and local dates** + +Render domain rows as hostname plus duration without URLs. Keep raw event +details in the Events tab. For unassigned scope, list active workspace IDs and +require a selection before handing the candidate to Journal. Preselect the +largest date slice, show both date slices, use local date conversion, and save +the session watermark with the Journal entry. Show a visible action if Journal +is disabled instead of silently dropping the request. Put a confirm/cancel +dialog in front of Clear Activity and Journal Delete; cancelling leaves the +append log, candidate state, and entry list untouched. + +- [ ] **Step 4: Run green and commit** + +Run: + +~~~bash +cd /home/mirivlad/git/verstak2/verstak-official-plugins && node scripts/smoke-activity-plugin.js && node scripts/smoke-journal-plugin.js +cd /home/mirivlad/git/verstak2/verstak-desktop/frontend && npm run test:e2e -- activity.spec.js todo.spec.js +git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/activity plugins/journal scripts/smoke-activity-plugin.js scripts/smoke-journal-plugin.js +git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: review activity sessions in journal" +git -C /home/mirivlad/git/verstak2/verstak-desktop add frontend/e2e/activity.spec.js frontend/e2e/todo.spec.js frontend/src/lib/test/wails-mock.js +git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "test: cover activity journal review flow" +~~~ + +Expected: candidate-to-Journal, local date, and persisted mock workflows pass. diff --git a/docs/superpowers/plans/2026-07-12-alpha-browser-domain-activity.md b/docs/superpowers/plans/2026-07-12-alpha-browser-domain-activity.md new file mode 100644 index 0000000..b6866e3 --- /dev/null +++ b/docs/superpowers/plans/2026-07-12-alpha-browser-domain-activity.md @@ -0,0 +1,216 @@ +# Alpha Browser Domain Activity Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking. + +**Goal:** Add explicitly consented, privacy-minimal active-tab domain timing to the browser extension and deliver immutable activity batches to Desktop. + +**Architecture:** Manual captures remain unchanged. A new tracker stores mutable accumulators, immutable pending batches, and acknowledged IDs locally; Desktop accepts a separate authenticated activity endpoint and emits browser.activity.domain. + +**Tech Stack:** Plain WebExtension JavaScript, Node tests, Go HTTP handler tests, shared JSON vectors. + +## Global Constraints + +- Passive tracking defaults to disabled and requires explicit informed consent. +- Track only normalized hostname plus duration for an active tab in a focused window. +- Never send URL, title, text, page content, keystrokes, or history. +- A pending batch is byte-for-byte immutable; acknowledge only its own ID. +- Discard ambiguous or negative clock deltas and gaps over 10 minutes. +- Use hostname-normalization-v1 for extension, Desktop receiver, exclusions, and bindings. + +--- + +### Task 1: Define and test canonical hostname normalization + +**Files:** +- Create: /home/mirivlad/git/verstak2/verstak-sdk/schemas/hostname-normalization-v1.json +- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/hostname.js +- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/hostname-normalization-v1.json +- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/scripts/test-hostname.js +- Create: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/normalize.go +- Create: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/normalize_test.go +- Create: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/testdata/hostname-normalization-v1.json + +**Interfaces:** +- Produces JavaScript normalizeHostnameV1(value, mode). +- Produces Go hostname.NormalizeV1(value string, mode Mode) (string, error). +- Mode is URLSource for HTTP(S) URLs and BareHost for binding/exclusion input. + +- [ ] **Step 1: Write the shared vector corpus and failing tests** + +Include vectors for Unicode/punycode, trailing dot, port stripping/rejection, +IPv4, bracketed IPv6, localhost, one-label internal names, malformed labels, +and an overlong name: + +~~~json +[ + {"mode":"bare","input":"пример.рф.","want":"xn--e1afmkfd.xn--p1ai"}, + {"mode":"url","input":"https://Example.COM:8443/a","want":"example.com"}, + {"mode":"bare","input":"example.com:8443","error":"port"}, + {"mode":"bare","input":"[::1]","want":"::1"} +] +~~~ + +- [ ] **Step 2: Run red** + +Run: + +~~~bash +cd /home/mirivlad/git/verstak2/verstak-browser-extension +node scripts/test-hostname.js +cd /home/mirivlad/git/verstak2/verstak-desktop +GOCACHE=/tmp/verstak-go-cache go test ./internal/core/hostname -count=1 +~~~ + +Expected: both fail because the normalizers do not exist. + +- [ ] **Step 3: Implement both normalizers** + +Use the browser URL parser plus ASCII IDNA conversion in JavaScript. Use +net/url, net/netip, and golang.org/x/net/idna.Lookup.ToASCII in Go. Store +ASCII A-labels, lowercase names, no ports or IPv6 brackets, and reject malformed +or over-limit values. + +- [ ] **Step 4: Run green and commit** + +Run: + +~~~bash +cd /home/mirivlad/git/verstak2/verstak-browser-extension && npm test +cd /home/mirivlad/git/verstak2/verstak-desktop && GOCACHE=/tmp/verstak-go-cache go test ./internal/core/hostname -count=1 +cmp /home/mirivlad/git/verstak2/verstak-sdk/schemas/hostname-normalization-v1.json /home/mirivlad/git/verstak2/verstak-browser-extension/shared/hostname-normalization-v1.json +cmp /home/mirivlad/git/verstak2/verstak-sdk/schemas/hostname-normalization-v1.json /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/testdata/hostname-normalization-v1.json +git -C /home/mirivlad/git/verstak2/verstak-sdk add schemas/hostname-normalization-v1.json +git -C /home/mirivlad/git/verstak2/verstak-sdk commit -m "feat: define hostname normalization vectors" +git -C /home/mirivlad/git/verstak2/verstak-browser-extension add shared/hostname.js shared/hostname-normalization-v1.json scripts/test-hostname.js +git -C /home/mirivlad/git/verstak2/verstak-browser-extension commit -m "feat: normalize activity hostnames" +git -C /home/mirivlad/git/verstak2/verstak-desktop add internal/core/hostname +git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "feat: normalize browser hostnames" +~~~ + +Expected: every copied corpus passes in both implementations. + +### Task 2: Add a separate authenticated Desktop activity receiver + +**Files:** +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/browserreceiver/receiver.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/browserreceiver/receiver_test.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go + +**Interfaces:** +- Accepts POST /activities with BrowserDomainActivity. +- Emits browser.activity.domain with normalized hostname, duration, ID, and optional workspaceId. +- Returns status accepted and activityId for first delivery and duplicate delivery. + +- [ ] **Step 1: Write failing handler tests** + +Create a valid immutable request and then retry the same ID: + +~~~go +payload := "{\"schemaVersion\":1,\"activityId\":\"a-1\",\"hostname\":\"пример.рф\",\"durationSeconds\":300,\"startedAt\":\"2026-07-12T10:00:00Z\",\"endedAt\":\"2026-07-12T10:05:00Z\"}" +req := httptest.NewRequest(http.MethodPost, "/activities", strings.NewReader(payload)) +req.Header.Set("X-Verstak-Receiver-Token", "pair-token") +~~~ + +Assert invalid tokens, URL-like hostnames, zero or oversize duration, and a +duplicate ID do not publish a second event. + +- [ ] **Step 2: Run red** + +Run: + +~~~bash +cd /home/mirivlad/git/verstak2/verstak-desktop +GOCACHE=/tmp/verstak-go-cache go test ./internal/core/browserreceiver -run Activity -count=1 +~~~ + +Expected: FAIL because /activities is not routed. + +- [ ] **Step 3: Implement the bounded idempotent route** + +Add BrowserDomainActivity, a 30-day bounded receiver ID cache, and +handleActivity which invokes hostname.NormalizeV1. Do not reuse capture types +or persistence. In App, resolve an active exact hostname binding to workspaceId +and a path cache before Activity receives the event. + +- [ ] **Step 4: Run green and commit** + +Run: + +~~~bash +GOCACHE=/tmp/verstak-go-cache go test ./internal/core/browserreceiver ./internal/api -count=1 +git add internal/core/browserreceiver/receiver.go internal/core/browserreceiver/receiver_test.go internal/api/app.go internal/api/app_test.go +git commit -m "feat: receive browser domain activity" +~~~ + +Expected: authentication, normalization, idempotency, and binding tests pass. + +### Task 3: Build the opt-in extension tracker and immutable delivery queue + +**Files:** +- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/activity-tracker.js +- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/scripts/test-activity-tracker.js +- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/api.js +- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/background.js +- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.html +- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.js +- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.css +- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/locales/en.json +- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/locales/ru.json +- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/chromium/manifest.json +- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/firefox/manifest.json + +**Interfaces:** +- Persists activeAccumulator, pendingBatches, and acknowledgedIds under verstak.activityTracker. +- Produces sendActivity(receiverUrl, token, immutablePayload). +- Settings include passiveActivityEnabled false and excludedDomains empty. + +- [ ] **Step 1: Write failing state-machine tests** + +Cover consent, active-tab-only timing, long-gap discard, and the A/B batch case: + +~~~js +tracker.addElapsed('example.com', 600, start, end); +const batchA = tracker.freeze('example.com'); +tracker.addElapsed('example.com', 300, laterStart, laterEnd); +assert.equal(tracker.pendingBatches[0].payload.durationSeconds, 600); +tracker.acknowledge(batchA.activityId); +assert.equal(tracker.activeAccumulator['example.com'].durationSeconds, 300); +~~~ + +- [ ] **Step 2: Run red** + +Run: + +~~~bash +cd /home/mirivlad/git/verstak2/verstak-browser-extension +node scripts/test-activity-tracker.js +~~~ + +Expected: FAIL because the state machine is absent. + +- [ ] **Step 3: Implement tracker, disclosure, and listeners** + +Implement the tracker as pure functions. In background.js, register +tabs.onActivated, tabs.onUpdated, windows.onFocusChanged, idle.onStateChanged, +and a five-minute alarm only while opted in. Set idle detection to 600 seconds. +On a gap over 600 seconds, clock rollback, lock, or browser startup, reset the +active checkpoint without adding time. Retry frozen batches oldest-first and +remove only matching IDs. + +Show an unchecked Russian/English consent explanation in settings. Add windows, +alarms, and idle permissions to both manifests. Keep manual capture queue and +popup actions unchanged. + +- [ ] **Step 4: Run green and commit** + +Run: + +~~~bash +npm test +node scripts/test-activity-tracker.js +git add shared chromium/manifest.json firefox/manifest.json scripts/test-activity-tracker.js +git commit -m "feat: add opt-in browser domain tracker" +~~~ + +Expected: tracker unit tests and existing extension tests pass. diff --git a/docs/superpowers/plans/2026-07-12-alpha-browser-inbox.md b/docs/superpowers/plans/2026-07-12-alpha-browser-inbox.md new file mode 100644 index 0000000..bb673ce --- /dev/null +++ b/docs/superpowers/plans/2026-07-12-alpha-browser-inbox.md @@ -0,0 +1,229 @@ +# Alpha Browser Inbox Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking. + +**Goal:** Preserve a manually captured link when it leaves the global Inbox, give Browser Inbox a usable archive, and make links durable across case lifecycle changes. + +**Architecture:** A capture remains one canonical record. Its global archive state is independent of its workspaceId relation; domain bindings use the same stable identity. Browser Inbox owns capture UI and migration, while Desktop exposes a narrow URL-open capability for user-initiated links. + +**Tech Stack:** Plain JavaScript plugin bundle, Go Wails API, Node smoke tests, Playwright. + +## Global Constraints + +- Only manual extension actions create Browser Inbox captures. +- Captures have at most one optional case assignment in this alpha. +- Remove from global Inbox archives; only Delete everywhere is permanent. +- Assignment and binding use workspaceId, never path as identity. +- Create .url files through files.write and never overwrite a name silently. +- URL opening is direct HTTP(S) opening, not a Linux .url association. + +--- + +### Task 1: Migrate Browser Inbox records and implement archive state + +**Files:** +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/locales/en.json +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/locales/ru.json +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go + +**Interfaces:** +- Canonical capture fields: globalState, workspaceId, workspaceState, workspaceRootPath, workspaceTrashId. +- Domain binding value: workspaceId plus workspaceRootPath cache plus state. +- Mutations: archive, restore, assign, unassign, deleteEverywhere, migrate. + +- [ ] **Step 1: Write failing migration and archive smoke tests** + +Use a legacy path assignment and assert it resolves to a stable ID. Test the +original data-loss regression: + +~~~js +await inbox.assignWorkspace('capture-1', activeWorkspace); +await inbox.archiveCapture('capture-1'); +assert.equal(globalRows().some(row => row.captureId === 'capture-1'), false); +assert.equal(workspaceRows(activeWorkspace.workspaceId).some(row => row.captureId === 'capture-1'), true); +await inbox.restoreCapture('capture-1'); +assert.equal(globalRows().some(row => row.captureId === 'capture-1'), true); +~~~ + +- [ ] **Step 2: Run red** + +Run: + +~~~bash +cd /home/mirivlad/git/verstak2/verstak-official-plugins +node scripts/smoke-browser-inbox-plugin.js +~~~ + +Expected: FAIL because archive and assignment are currently one mutable path field. + +- [ ] **Step 3: Implement canonical state and idempotent migration** + +Normalize every capture on read. Set legacy captures globalState active. Resolve +legacy paths through Desktop identities: active marker gives workspaceId, +otherwise set workspaceState unavailable. Store global captures once; workspace +views filter the canonical collection by workspaceId rather than duplicate +storage keys. Reject permanent deletion without an explicit confirm token from +the UI. + +- [ ] **Step 4: Run green and commit** + +Run: + +~~~bash +node scripts/smoke-browser-inbox-plugin.js +./scripts/check.sh +git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/browser-inbox scripts/smoke-browser-inbox-plugin.js +git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: preserve archived browser inbox captures" +git -C /home/mirivlad/git/verstak2/verstak-desktop add internal/api/app.go internal/api/app_test.go +git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "feat: migrate browser inbox relations" +~~~ + +Expected: migration, archive, restore, and assignment tests pass. + +### Task 2: Handle rename, Trash, restore, purge, and archive UI + +**Files:** +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/browser-inbox.spec.js +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/test/wails-mock.js + +**Interfaces:** +- Receives workspace.renamed, workspace.trashed, workspace.restored, and workspace.purged. +- Status filter values are active, archive, all. +- Archive bulk action is restore; bulk permanent delete is not offered. + +- [ ] **Step 1: Write failing lifecycle and UI tests** + +Assert a rename changes only path cache, trash disables routing, restore restores +the same workspace ID, purge makes captures unassigned and bindings orphaned: + +~~~js +await emitWorkspaceEvent('workspace.trashed', { workspaceId: 'w-1', trashId: 't-1' }); +assert.equal(capture.workspaceState, 'trashed'); +assert.equal(domainBinding('client.example').state, 'trashed'); +await emitWorkspaceEvent('workspace.purged', { workspaceId: 'w-1', trashId: 't-1' }); +assert.equal(capture.workspaceState, 'unassigned'); +assert.equal(domainBinding('client.example').state, 'orphaned'); +~~~ + +In Playwright, select Archive, restore one visible item, and bulk-restore only +the filtered archive rows. Also click Delete everywhere for an assigned capture, +assert the dialog names its assigned Дело, cancel, and assert that the global +and assigned rows still exist. + +- [ ] **Step 2: Run red** + +Run: + +~~~bash +cd /home/mirivlad/git/verstak2/verstak-desktop/frontend +npm run test:e2e -- browser-inbox.spec.js +~~~ + +Expected: FAIL because the current UI has only processed filters and destructive Clear. + +- [ ] **Step 3: Implement lifecycle and labels** + +Subscribe once from the Browser Inbox background service. Update path caches on +rename only when workspaceId matches. On external unavailable, do not route to +the same path again. Add Active, Archive, and All filters; make search apply +inside the selected filter; show Archive badge inside a case; add Restore to +Inbox and visible filtered bulk restore. Rename Clear to an archive action with +count confirmation. Keep Delete everywhere separate and require a dialog that +names the assigned Дело; cancellation must perform no mutation. + +- [ ] **Step 4: Run green and commit** + +Run: + +~~~bash +cd /home/mirivlad/git/verstak2/verstak-official-plugins && node scripts/smoke-browser-inbox-plugin.js +cd /home/mirivlad/git/verstak2/verstak-desktop/frontend && npm run test:e2e -- browser-inbox.spec.js +git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/browser-inbox scripts/smoke-browser-inbox-plugin.js +git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: add browser inbox archive lifecycle" +git -C /home/mirivlad/git/verstak2/verstak-desktop add frontend/e2e/browser-inbox.spec.js frontend/src/lib/test/wails-mock.js +git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "test: cover browser inbox archive lifecycle" +~~~ + +Expected: archive filters, restore, rename, Trash, restore, and purge tests pass. + +### Task 3: Save and open durable links without file-association reliance + +**Files:** +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/permissions/registry.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-host/VerstakPluginAPI.js +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/plugin.json +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js +- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js + +**Interfaces:** +- Permission and API: urls.openExternal(url string) for user-initiated HTTP(S) URLs only. +- SaveLinkInCase(captureId, workspaceId, filename) creates Links/name.url with InternetShortcut URL field. +- Collision response offers a proposed unique filename and never overwrites. + +- [ ] **Step 1: Write failing API and plugin tests** + +Add tests for invalid URL, readonly failure, unique filename, collision, and +direct opener argument: + +~~~go +if errStr := app.OpenExternalURL("browser.plugin", "https://example.test/a"); errStr != "" { + t.Fatal(errStr) +} +if got := opener.Arguments[0]; got != "https://example.test/a" { + t.Fatalf("opened %q, want URL not .url path", got) +} +~~~ + +Add plugin smoke assertions for a title-derived name, Link fallback, and +proposal Link (2).url after a collision. + +- [ ] **Step 2: Run red** + +Run: + +~~~bash +cd /home/mirivlad/git/verstak2/verstak-desktop +GOCACHE=/tmp/verstak-go-cache go test ./internal/api -run 'Test.*OpenExternalURL' -count=1 +cd /home/mirivlad/git/verstak2/verstak-official-plugins +node scripts/smoke-browser-inbox-plugin.js +~~~ + +Expected: FAIL because only file-path external opening exists. + +- [ ] **Step 3: Implement safe save and open** + +Register urls.openExternal as dangerous. Validate HTTP(S), invoke the existing +OS opener with the URL string, and expose api.urls.openExternal. Browser Inbox +requests that permission, creates Links through files.createFolder and +files.writeText using: + +~~~text +[InternetShortcut] +URL=https://example.test/ +~~~ + +Sanitize a 96-character filename stem. On collision show an editable dialog +with proposed name (2).url and Cancel; leave capture state unchanged on errors. +When opening a saved link, parse and validate URL= before invoking the URL API. + +- [ ] **Step 4: Run green and commit** + +Run: + +~~~bash +cd /home/mirivlad/git/verstak2/verstak-desktop && GOCACHE=/tmp/verstak-go-cache go test ./internal/api -count=1 +cd /home/mirivlad/git/verstak2/verstak-official-plugins && node scripts/smoke-browser-inbox-plugin.js +git -C /home/mirivlad/git/verstak2/verstak-desktop add internal/core/permissions internal/api frontend/src/lib/plugin-host +git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "feat: open external URLs safely" +git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/browser-inbox scripts/smoke-browser-inbox-plugin.js +git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: save browser inbox links safely" +~~~ + +Expected: URL API tests and link save/collision smoke tests pass. diff --git a/docs/superpowers/plans/2026-07-12-alpha-shell-ux.md b/docs/superpowers/plans/2026-07-12-alpha-shell-ux.md new file mode 100644 index 0000000..ca63e41 --- /dev/null +++ b/docs/superpowers/plans/2026-07-12-alpha-shell-ux.md @@ -0,0 +1,207 @@ +# Alpha Shell UX Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking. + +**Goal:** Make the Desktop overview scoped and actionable, hide diagnostic plugin IDs by default, and remove alpha UI strings that expose implementation noise. + +**Architecture:** The shell consumes stable workspace IDs and plugin capabilities rather than hard-coded namespaces. Overview builds deterministic case-scoped projections; debug display is a presentation preference plus a session-only command-line override. + +**Tech Stack:** Svelte, Wails app settings, Go tests, Playwright E2E, existing i18n catalogs. + +## Global Constraints + +- Default UI labels are Russian and use Дела, Входящие, Активности, Журнал. +- A normal tab never shows a plugin ID. +- Settings Debug Show plugin IDs defaults false; Desktop --debug enables it only for that run. +- Overview never leaks an unscoped event into every case. +- Todo entries are included only if todo.workspace capability is loaded. +- No action opens or creates a Дело implicitly. + +--- + +### Task 1: Add effective debug display setting and human plugin tab labels + +**Files:** +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/main.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/appsettings/manager.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/appsettings/manager_test.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/shell/WorkspaceHost.svelte +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-manager/PluginManager.svelte +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/en.js +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/ru.js +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/plugin-api-bridge.spec.js + +**Interfaces:** +- Persisted app setting showPluginIds bool defaults false. +- GetAppSettings returns effectiveShowPluginIds and UpdateAppSettings accepts showPluginIds. +- Runtime --debug sets effectiveShowPluginIds true without persisting it. + +- [ ] **Step 1: Write failing app-setting and E2E tests** + +Add a Go test proving the setting preserves theme and language, plus a browser +test proving normal mode has no raw ID and debug mode has one: + +~~~js +await expect(page.locator('[role="tab"]').filter({ hasText: 'verstak.' })).toHaveCount(0); +await mockAppSettings({ showPluginIds: true }); +await expect(page.locator('[role="tab"]').filter({ hasText: 'verstak.default-editor' })).toHaveCount(1); +~~~ + +- [ ] **Step 2: Run red** + +Run: + +~~~bash +cd /home/mirivlad/git/verstak2/verstak-desktop +GOCACHE=/tmp/verstak-go-cache go test ./internal/core/appsettings ./internal/api -run 'Test.*PluginIds' -count=1 +cd frontend && npm run test:e2e -- plugin-api-bridge.spec.js +~~~ + +Expected: FAIL because no display preference exists. + +- [ ] **Step 3: Implement presentation-only debug state** + +Parse --debug before Wails startup and keep it in App runtime state. Merge it +with the persisted bool when returning effective settings. Render the localized +manifest title normally; append the plugin ID only when effectiveShowPluginIds +is true. Add the Debug settings checkbox and localized explanatory copy. + +- [ ] **Step 4: Run green and commit** + +Run: + +~~~bash +GOCACHE=/tmp/verstak-go-cache go test ./internal/core/appsettings ./internal/api -count=1 +cd frontend && npm run test:e2e -- plugin-api-bridge.spec.js +git add main.go internal/core/appsettings internal/api frontend/src frontend/e2e/plugin-api-bridge.spec.js +git commit -m "feat: hide plugin IDs outside debug mode" +~~~ + +Expected: normal and debug labels are both verified. + +### Task 2: Implement deterministic case-scoped Overview + +**Files:** +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/shell/TodaySurface.svelte +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/shell/WorkspaceHost.svelte +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/en.js +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/ru.js +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/ux-today.spec.js +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/ux-p0.spec.js +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/test/wails-mock.js + +**Interfaces:** +- Every overview row has workspaceId and lastMeaningfulAt. +- Needs attention maximum is five; Continue work maximum is four; Recent changes maximum is eight. +- Hidden recommendation state is keyed by entity ID and invalidated by later meaningful change. + +- [ ] **Step 1: Write failing scope and ranking tests** + +Add fixture events for two workspace IDs plus an unscoped event: + +~~~js +await seedOverview({ + selectedWorkspaceId: 'client-a', + events: [ + { activityId: 'a', workspaceId: 'client-a', type: 'note.saved', occurredAt: now }, + { activityId: 'b', workspaceId: 'client-b', type: 'file.changed', occurredAt: now }, + { activityId: 'global', type: 'workspace.selected', occurredAt: now } + ] +}); +await expect(page.getByText(/client-b/i)).toHaveCount(0); +await expect(page.locator('[data-overview-section="continue"] [data-overview-item]')).toHaveCount(4); +~~~ + +Also assert no selection shows only the select/create prompt and up to five +unassigned active Inbox captures. + +- [ ] **Step 2: Run red** + +Run: + +~~~bash +cd /home/mirivlad/git/verstak2/verstak-desktop/frontend +npm run test:e2e -- ux-today.spec.js ux-p0.spec.js +~~~ + +Expected: FAIL because rowsFor currently accepts untagged events for every workspace. + +- [ ] **Step 3: Implement exact overview projections** + +Replace path-only filtering with workspaceId equality. Build: + +1. Needs attention: ready Journal candidates, active unprocessed captures, then + urgent Todo rows only if todo.workspace capability exists; take five. +2. Continue work: distinct entities updated in 14 days; sort by + lastMeaningfulAt descending and the documented type tie-break; take four. +3. Recent changes: note save/create, file create/rename or last change per + file, saved link, and Journal create in seven days; take eight. + +Persist Hide recommendation without changing underlying data. Use localized +empty states and remove English technical strings. + +- [ ] **Step 4: Run green and commit** + +Run: + +~~~bash +npm run test:e2e -- ux-today.spec.js ux-p0.spec.js +git add src/lib/shell/TodaySurface.svelte src/lib/shell/WorkspaceHost.svelte src/lib/i18n frontend/e2e/ux-today.spec.js frontend/e2e/ux-p0.spec.js src/lib/test/wails-mock.js +git commit -m "feat: make overview case-scoped and actionable" +~~~ + +Expected: limits, scope, empty state, and recommendation hide tests pass. + +### Task 3: Verify alpha UX as a user sees it + +**Files:** +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/ux-followup.spec.js +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/scripts/check-gui.sh + +**Interfaces:** +- E2E fixture uses effectiveShowPluginIds false by default. +- GUI checker accepts a --debug-labels case. + +- [ ] **Step 1: Write the final failing alpha scenarios** + +Add a scenario that creates a capture, assigns it, archives it globally, opens +the assigned case, and verifies the capture is still present. Add a session +candidate path to Journal and assert normal tab labels are human-readable. + +~~~js +await archiveGlobalCapture('capture-1'); +await openWorkspace('client-a'); +await expect(page.getByTestId('browser-capture-capture-1')).toBeVisible(); +await expect(page.getByRole('tab', { name: /verstak\./i })).toHaveCount(0); +~~~ + +- [ ] **Step 2: Run red** + +Run: + +~~~bash +npm run test:e2e -- ux-followup.spec.js +~~~ + +Expected: FAIL until the prior plans are integrated. + +- [ ] **Step 3: Add deterministic GUI smoke commands** + +Extend check-gui.sh to start an isolated test vault, run normal and --debug +screens, and save screenshots under frontend/e2e-results. The script must not +read or mutate the user's normal vault. + +- [ ] **Step 4: Run green and commit** + +Run: + +~~~bash +npm run test:e2e -- ux-followup.spec.js +GOCACHE=/tmp/verstak-go-cache ./scripts/check-gui.sh +git add frontend/e2e/ux-followup.spec.js scripts/check-gui.sh +git commit -m "test: cover first alpha UX smoke flow" +~~~ + +Expected: E2E and isolated GUI smoke pass. diff --git a/docs/superpowers/plans/2026-07-12-alpha-workspace-identity.md b/docs/superpowers/plans/2026-07-12-alpha-workspace-identity.md new file mode 100644 index 0000000..666da10 --- /dev/null +++ b/docs/superpowers/plans/2026-07-12-alpha-workspace-identity.md @@ -0,0 +1,216 @@ +# Alpha Workspace Identity Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking. + +**Goal:** Give every managed Дело a durable UUID identity and publish lifecycle events that let alpha features follow a case across rename, Trash, restore, and path reuse. + +**Architecture:** The filesystem remains the source of truth for whether a case folder exists. A UUID marker inside each case is the relation identity; Desktop metadata indexes that marker and paths remain presentation caches. + +**Tech Stack:** Go, Wails bindings, JSON metadata, Go tests. + +## Global Constraints + +- A new Дело is created only by an explicit user action. +- workspaceId is UUID v4; a path is never relation identity. +- The marker is .verstak/workspace.json inside the case. +- Existing writable cases migrate without data loss; non-writable cases are view-only. +- Use TDD and make one commit per task. + +--- + +### Task 1: Store and resolve durable workspace identities + +**Files:** +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager_test.go + +**Interfaces:** +- Produces Workspace.ID string and Metadata.WorkspaceID string. +- Produces func (m *Manager) EnsureWorkspaceIdentity(name string) (WorkspaceIdentity, error). +- Produces func (m *Manager) ListWorkspaceIdentities() ([]WorkspaceIdentity, error). + +- [ ] **Step 1: Write the failing identity tests** + +Add tests proving creation writes a UUID marker, rename retains it, and a newly +created folder after external removal gets another UUID: + +~~~go +ws, err := m.CreateWorkspace("Clients", "default") +if err != nil { t.Fatal(err) } +first, err := m.EnsureWorkspaceIdentity(ws.Name) +if err != nil || first.WorkspaceID == "" { t.Fatalf("identity = %+v, %v", first, err) } +if err := m.RenameWorkspace("Clients", "Clients-2026"); err != nil { t.Fatal(err) } +renamed, _ := m.EnsureWorkspaceIdentity("Clients-2026") +if renamed.WorkspaceID != first.WorkspaceID { t.Fatal("rename changed workspace ID") } +~~~ + +- [ ] **Step 2: Run the focused test to verify it fails** + +Run: + +~~~bash +cd /home/mirivlad/git/verstak2/verstak-desktop +GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace -run 'Test.*Workspace.*Identity' -count=1 +~~~ + +Expected: FAIL because WorkspaceIdentity and EnsureWorkspaceIdentity do not exist. + +- [ ] **Step 3: Write the minimal marker implementation** + +Add these types and use them in CreateWorkspace, GetWorkspaceMetadata, and +workspace listing: + +~~~go +type WorkspaceIdentity struct { + WorkspaceID string `json:"workspaceId"` + RootPath string `json:"rootPath"` + State string `json:"state"` +} + +const workspaceIdentityRelativePath = ".verstak/workspace.json" + +func (m *Manager) EnsureWorkspaceIdentity(name string) (WorkspaceIdentity, error) { + // Validate the folder, read the in-folder marker, and atomically create + // a UUID-v4 marker only when absent and writable. +} +~~~ + +Write the same ID to central metadata only as an index. Never recover an ID from +stale path-keyed metadata when the in-folder marker is absent. + +- [ ] **Step 4: Run green and commit** + +Run: + +~~~bash +GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace -count=1 +git add internal/core/workspace/manager.go internal/core/workspace/manager_test.go +git commit -m "feat: add durable workspace identities" +~~~ + +Expected: tests pass and the commit contains only workspace identity changes. + +### Task 2: Expose identity and lifecycle events through Desktop + +**Files:** +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/App.svelte +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/wailsjs/go/api/App.d.ts +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/wailsjs/go/models.ts + +**Interfaces:** +- Produces ListWorkspaceIdentities() ([]workspace.WorkspaceIdentity, string). +- Lifecycle payloads include workspaceId and workspaceRootPath; Trash also includes trashId. +- Produces workspace.restored and workspace.purged events. + +- [ ] **Step 1: Add failing API tests** + +Extend TestWorkspaceAPIPublishesLifecycleEvents: + +~~~go +if got := received["workspace.created"]["workspaceId"]; got == "" { + t.Fatal("workspace.created must include workspaceId") +} +if got := received["workspace.renamed"]["workspaceId"]; got != createdID { + t.Fatalf("rename ID = %v, want %s", got, createdID) +} +~~~ + +Add restore and purge tests around RestoreVaultTrash and DeleteVaultTrash that +assert the original UUID and trash ID are emitted. + +- [ ] **Step 2: Run red** + +Run: + +~~~bash +GOCACHE=/tmp/verstak-go-cache go test ./internal/api -run 'TestWorkspaceAPI.*Lifecycle' -count=1 +~~~ + +Expected: FAIL because lifecycle payloads only identify a path. + +- [ ] **Step 3: Publish complete identity lifecycle** + +Resolve the marker before publishing create, rename, selected, and trashed +events. Detect workspace Trash metadata in RestoreVaultTrash and +DeleteVaultTrash, then publish: + +~~~go +map[string]interface{}{ + "operation": "restore", + "workspaceId": identity.WorkspaceID, + "workspaceRootPath": restoredRoot, + "trashId": trashID, +} +~~~ + +Change App.svelte workspace-node conversion to preserve workspace.id while using +rootPath only for selection and display. + +- [ ] **Step 4: Run green and commit** + +Run: + +~~~bash +GOCACHE=/tmp/verstak-go-cache go test ./internal/api ./internal/core/workspace -count=1 +git add internal/api/app.go internal/api/app_test.go frontend/src/App.svelte frontend/wailsjs/go/api/App.d.ts frontend/wailsjs/go/models.ts +git commit -m "feat: publish workspace identity lifecycle" +~~~ + +Expected: focused API and workspace tests pass. + +### Task 3: Migrate legacy references and repair duplicate IDs + +**Files:** +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager_test.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go +- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go + +**Interfaces:** +- Produces RepairWorkspaceIdentity(keepRootPath, regenerateRootPath string) string. +- Identity states are active, unavailable, or duplicate. +- Later Inbox and Activity plans consume workspaceId through ListWorkspaceIdentities. + +- [ ] **Step 1: Write failing duplicate and legacy tests** + +Create two folders with copied marker data and assert neither is an active +relation target. Create a legacy path-keyed capture fixture and assert a +resolved path gets an ID while a missing path becomes unavailable: + +~~~go +if errStr := app.RepairWorkspaceIdentity("Original", "Copied"); errStr != "" { + t.Fatal(errStr) +} +if originalID == copiedID { t.Fatal("repair must issue a new ID") } +~~~ + +- [ ] **Step 2: Run red** + +Run: + +~~~bash +GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace ./internal/api -run 'Test.*(Duplicate|Legacy).*Workspace' -count=1 +~~~ + +Expected: FAIL because copied markers are accepted as ordinary folders. + +- [ ] **Step 3: Implement deterministic repair** + +Group active roots by marker UUID during identity listing. Mark a group with +more than one root duplicate and exclude it from assignment targets. +RepairWorkspaceIdentity verifies the duplicate, keeps the first marker, creates +a new marker for the second root, and moves no relation data. + +- [ ] **Step 4: Run green and commit** + +Run: + +~~~bash +GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace ./internal/api -count=1 +git add internal/core/workspace/manager.go internal/core/workspace/manager_test.go internal/api/app.go internal/api/app_test.go +git commit -m "feat: repair duplicate workspace identities" +~~~ + +Expected: legacy, path-reuse, and duplicate-ID tests pass.