Skip to content

Commit e5d5c78

Browse files
committed
docs: design activity project reclassification
- docs: plan activity project reclassification - fix(parser): recognize generic GitHub worktrees - feat(settings): manage worktree mappings by machine - fix(db): preserve mapping origin during resync - fix(db): retain mapping context during pre-copy - feat(activity): add atomic project reclassification - fix(db): unify scoped worktree mapping evaluation - feat(api): expose project reclassification workflow - fix(sync): preserve mapped project moves across mirrors - fix(sync): retain source labels in identity snapshots - fix(sync): qualify snapshot tombstones by project - fix(sync): bound incremental identity reconciliation - feat(settings): manage remote worktree mappings - fix(settings): isolate mapping mutations by machine - feat(activity): reclassify projects from breakdowns - fix(activity): harden reclassification state transitions - test(e2e): cover activity project reclassification - fix(sidebar): align root totals across backends - test(e2e): gate DuckDB reclassification - test(e2e): allow isolated server ports - fix(sync): preserve source project in identity snapshots - fix(sync): complete mapped identity ingestion - fix(sync): retain snapshots across empty reparses - fix(sync): make identity ingestion atomic - fix(sync): preserve source identity on reprocessing - test(e2e): validate isolated server ports - chore(frontend): pin merged kit-ui contracts - fix(activity): polish reclassification UI review findings - docs: document activity project reclassification - docs: close out completed superpowers plans - fix(sync): harden project snapshot upgrades - fix(sync): preserve reparsed snapshots on upgrade
1 parent bf5ad3b commit e5d5c78

117 files changed

Lines changed: 10716 additions & 881 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ jobs:
118118
run: |
119119
bash scripts/install_test.sh
120120
bash scripts/retry_test.sh
121+
bash scripts/e2e-server_test.sh
121122
bash scripts/check_desktop_release_health_test.sh
122123
bash scripts/check_desktop_release_health_from_event_test.sh
123124
bash desktop/scripts/test-repair-appimage-diricon.sh
@@ -362,3 +363,9 @@ jobs:
362363
env:
363364
E2E_PREBUILT_FIXTURE: /tmp/testfixture
364365
E2E_PREBUILT_SERVER: /tmp/agentsview
366+
367+
- name: Run DuckDB E2E tests
368+
run: make e2e-duckdb
369+
env:
370+
E2E_PREBUILT_FIXTURE: /tmp/testfixture
371+
E2E_PREBUILT_SERVER: /tmp/agentsview

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ e2e:
378378
# Run focused Playwright smoke tests against duckdb serve.
379379
e2e-duckdb:
380380
cd frontend && AGENTSVIEW_E2E_BACKEND=duckdb npx playwright test \
381+
e2e/activity-project-reclassification.spec.ts \
381382
e2e/duckdb-backend.spec.ts e2e/session-list.spec.ts --project=chromium
382383

383384
# Vet

cmd/testfixture/main.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"go.kenn.io/agentsview/internal/db"
1414
duckdbsync "go.kenn.io/agentsview/internal/duckdb"
15+
"go.kenn.io/agentsview/internal/export"
1516
)
1617

1718
type sessionSpec struct {
@@ -118,6 +119,12 @@ func main() {
118119
log.Fatalf("creating recent-edits fixture: %v", err)
119120
}
120121

122+
if err := createProjectReclassificationFixture(
123+
database, base.Add(120*time.Hour),
124+
); err != nil {
125+
log.Fatalf("creating project-reclassification fixture: %v", err)
126+
}
127+
121128
fmt.Printf("Fixture DB written to %s\n", *out)
122129
if *duckDBOut != "" {
123130
if err := writeDuckDBMirror(database, *duckDBOut); err != nil {
@@ -127,6 +134,80 @@ func main() {
127134
}
128135
}
129136

137+
func createProjectReclassificationFixture(
138+
database *db.DB, start time.Time,
139+
) error {
140+
const (
141+
machine = "remote-example-host"
142+
project = "wrong_branch_label"
143+
worktreeRoot = "/srv/worktrees/github.com/example-org/sample-service/example-worktree"
144+
model = "claude-sonnet-4-20250514"
145+
)
146+
cwds := []struct {
147+
suffix string
148+
cwd string
149+
}{
150+
{suffix: "root", cwd: worktreeRoot},
151+
{suffix: "nested", cwd: worktreeRoot + "/cmd/server"},
152+
}
153+
ctx := context.Background()
154+
for index, item := range cwds {
155+
sessionID := "test-session-project-reclassification-" + item.suffix
156+
startedAt := start.Add(time.Duration(index) * time.Hour)
157+
endedAt := startedAt.Add(12 * time.Minute)
158+
firstMessage := "Inspect the sample service worktree."
159+
session := db.Session{
160+
ID: sessionID,
161+
Project: project,
162+
Machine: machine,
163+
Agent: "claude",
164+
StartedAt: new(startedAt.Format(time.RFC3339Nano)),
165+
EndedAt: new(endedAt.Format(time.RFC3339Nano)),
166+
MessageCount: 2,
167+
UserMessageCount: 1,
168+
FirstMessage: new(firstMessage),
169+
Cwd: item.cwd,
170+
}
171+
if err := database.UpsertSession(session); err != nil {
172+
return fmt.Errorf(
173+
"upserting project-reclassification session: %w", err,
174+
)
175+
}
176+
if err := database.InsertMessages(generateMessages(
177+
sessionID, session.MessageCount, startedAt, model,
178+
)); err != nil {
179+
return fmt.Errorf(
180+
"inserting project-reclassification messages: %w", err,
181+
)
182+
}
183+
if err := database.UpsertProjectIdentityObservation(
184+
ctx,
185+
export.ProjectIdentityObservation{
186+
SessionID: sessionID,
187+
Project: project,
188+
Machine: machine,
189+
RootPath: worktreeRoot,
190+
RepositoryPath: "/srv/worktrees/github.com/example-org/sample-service",
191+
WorktreeName: "example-worktree",
192+
WorktreeRootPath: worktreeRoot,
193+
WorktreeRelationship: export.WorktreeLinked,
194+
CheckoutState: export.CheckoutBranch,
195+
GitBranch: "example-worktree",
196+
ObservedAt: startedAt,
197+
},
198+
); err != nil {
199+
return fmt.Errorf(
200+
"upserting project-reclassification identity: %w", err,
201+
)
202+
}
203+
fmt.Printf(
204+
" %s: %d messages (project reclassification)\n",
205+
sessionID, session.MessageCount,
206+
)
207+
}
208+
return nil
209+
}
210+
130211
func writeDuckDBMirror(database *db.DB, path string) error {
131212
if err := os.Remove(path); err != nil &&
132213
!errors.Is(err, os.ErrNotExist) {

cmd/testfixture/main_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"path/filepath"
6+
"testing"
7+
"time"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
"go.kenn.io/agentsview/internal/db"
13+
)
14+
15+
func TestCreateProjectReclassificationFixture(t *testing.T) {
16+
database, err := db.Open(filepath.Join(t.TempDir(), "sessions.db"))
17+
require.NoError(t, err)
18+
t.Cleanup(func() { require.NoError(t, database.Close()) })
19+
20+
base := time.Date(2026, 7, 1, 10, 0, 0, 0, time.UTC)
21+
require.NoError(t, createProjectReclassificationFixture(database, base))
22+
23+
const (
24+
machine = "remote-example-host"
25+
wrongProject = "wrong_branch_label"
26+
worktreeRoot = "/srv/worktrees/github.com/example-org/sample-service/example-worktree"
27+
)
28+
wantCwds := map[string]string{
29+
"test-session-project-reclassification-root": worktreeRoot,
30+
"test-session-project-reclassification-nested": worktreeRoot + "/cmd/server",
31+
}
32+
for sessionID, wantCwd := range wantCwds {
33+
session, getErr := database.GetSession(context.Background(), sessionID)
34+
require.NoError(t, getErr)
35+
require.NotNil(t, session)
36+
assert.Equal(t, machine, session.Machine)
37+
assert.Equal(t, wrongProject, session.Project)
38+
assert.Equal(t, wantCwd, session.Cwd)
39+
}
40+
41+
snapshots, err := database.ListSessionProjectIdentitySnapshots(
42+
context.Background(),
43+
)
44+
require.NoError(t, err)
45+
require.Len(t, snapshots, 2)
46+
for _, snapshot := range snapshots {
47+
assert.Equal(t, machine, snapshot.Machine)
48+
assert.Equal(t, wrongProject, snapshot.Project)
49+
assert.Equal(t, worktreeRoot, snapshot.RootPath)
50+
assert.Equal(t, worktreeRoot, snapshot.WorktreeRootPath)
51+
assert.NotEmpty(t, snapshot.Key)
52+
}
53+
}

docs/activity.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,40 @@ stacked bars to compare interactive and automated contributions.
9999
Rows with no value for the selected metric are omitted from that view, so
100100
cost-only untimed sessions appear in **Cost** but not **Agent-min**.
101101

102+
## Reclassify A Project
103+
104+
Worktree layouts the parser does not recognize can surface a branch or
105+
worktree directory name as a project. When that happens, hover or
106+
keyboard-focus a row in the **Project** breakdown and use the pencil action to
107+
open **Reclassify project**. On touch devices the action is always visible.
108+
109+
The dialog works through the existing
110+
[worktree project mapping](/configuration/#worktree-project-mappings) system:
111+
112+
- It lists the worktrees that produced the clicked row in the current Activity
113+
view, grouped by machine and worktree evidence. A single group is
114+
preselected; several groups require an explicit choice, because different
115+
worktrees usually need different target projects.
116+
- The suggested **path prefix** covers the selected group's working
117+
directories. You can edit it — for example, shorten it to cover sibling
118+
worktrees of the same repository.
119+
- The **target project** typeahead suggests known projects and accepts a new
120+
name. When the server normalizes the name (for example `sample-service`
121+
becomes `sample_service`), the dialog shows the stored form before you
122+
apply.
123+
- The **full archive impact** preview is live and authoritative: it counts
124+
matching sessions across all dates for that machine, not just the current
125+
Activity range. A prefix that touches more than one existing project shows a
126+
warning with per-project counts — usually a sign the prefix is too broad. A
127+
prefix matching zero sessions cannot be applied.
128+
129+
**Apply** saves the rule and rewrites the matching sessions in one atomic
130+
step, then reloads the report. The rule stays active for future syncs and is
131+
managed under **Settings → Worktree mappings**, which records the label the
132+
row originally showed. On a read-only server (`pg serve` or `duckdb serve`)
133+
the action explains that reclassification happens on the writable archive that
134+
syncs the machine's sessions.
135+
102136
## Activity Insight
103137

104138
At the bottom of the page, **Activity Insight** shows an existing global

docs/configuration.md

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -616,27 +616,42 @@ manual sync, and the periodic directory scan.
616616

617617
### Worktree Project Mappings
618618

619-
The parser infers a session's project from its `cwd`, which works for standard
620-
layouts but not custom worktree conventions like
621-
`~/code/{project}.worktrees/feat/<branch>/` — those sessions otherwise group
622-
under `<branch>` rather than `{project}`. As of 0.29.0, you can register manual
623-
**path-prefix → project** rules from the **Worktree Project Mappings** section
624-
in Settings:
619+
The parser infers a session's project from its `cwd`. It recognizes common
620+
worktree manager layouts, including the generic
621+
`worktrees/github.com/<owner>/<repository>/<worktree>` convention, where the
622+
repository segment becomes the project. Layouts it does not recognize — such
623+
as `~/code/{project}.worktrees/feat/<branch>/` — otherwise group sessions
624+
under `<branch>` rather than `{project}`. For those, register manual
625+
**path-prefix → project** rules from the **Worktree mappings** section in
626+
Settings, or directly from the Activity project breakdown (see
627+
[Reclassify a project](/activity/#reclassify-a-project)):
625628

626629
![Worktree Project Mappings settings section](/assets/generated/screenshots/worktree-mappings.png)
627630

628631
- Mappings are explicit; there is no auto-discovery.
632+
- Each rule is scoped to one machine. The machine selector manages rules for
633+
the local machine and for any remotely synced machine. Rules live in the
634+
writable archive that ingests that machine's sessions, which may be the
635+
source machine's local SQLite archive or a separate collector archive.
629636
- Each rule applies whenever a session's `cwd` falls under the configured
630637
prefix, on both new sessions as they sync and (via the **Apply** button)
631-
already-imported sessions.
638+
already-imported sessions. Prefixes match on directory boundaries, so
639+
`/worktrees/service` does not match `/worktrees/service-old`.
640+
- Enabled mappings run after parser inference, so an explicit rule always wins
641+
when the two disagree.
632642
- The default `explicit` layout maps every matching path to the project name
633643
stored on the rule. The `repo_dot_worktrees` layout derives the project from
634644
the first path segment under the prefix when it is named `<repo>.worktrees`,
635645
so a path like `/code/agentsview.worktrees/feature/frontend` resolves to
636646
project `agentsview`.
637-
- Rules are stored in a `worktree_project_mappings` SQLite table scoped to the
638-
host machine, so a mapping created on one machine does not leak into another
639-
machine's view of synced sessions.
647+
- Rules created from the Activity dialog record the mislabeled project they
648+
corrected, shown as **Originally shown as …**. The value is informational
649+
and set once; to manually revert a reclassification, edit the rule's target
650+
back to that original label and apply again.
651+
- Disabling or deleting a rule does not rewrite sessions by itself. Sessions
652+
whose source files still exist revert to parser-derived names on a later
653+
reparse or full resync, while orphaned sessions keep their stored
654+
classification.
640655
- Excluded, trashed, and skipped session files are left alone.
641656

642657
Mappings only mutate the session's `project` field; the rest of the session

docs/superpowers/plans/2026-07-12-ci-docker-build-retry.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# CI Docker Build Retry Implementation Plan
22

3+
> **Status:** Complete. Shipped via scripts/retry.sh and PR #1111 (fix(ci):
4+
> retry transient SSH image builds); verified against origin/main 2026-07-17.
5+
36
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development
47
> (if subagents available) or superpowers:executing-plans to implement this
58
> plan. Steps use checkbox (`- [ ]`) syntax for tracking.
@@ -22,7 +25,7 @@ ______________________________________________________________________
2225

2326
- Create: `scripts/retry_test.sh`
2427

25-
- [ ] **Step 1: Write the failing behavioral test**
28+
- [x] **Step 1: Write the failing behavioral test**
2629

2730
Create a temporary fake command that records its arguments and attempt count,
2831
fails twice, and succeeds on its third invocation. Assert that
@@ -36,7 +39,7 @@ ______________________________________________________________________
3639
Add a second fake command that always exits 17. Assert that the helper invokes
3740
it exactly three times and returns exit status 17.
3841

39-
- [ ] **Step 2: Run the test to verify it fails for the missing helper**
42+
- [x] **Step 2: Run the test to verify it fails for the missing helper**
4043

4144
Run: `bash scripts/retry_test.sh`
4245

@@ -50,15 +53,15 @@ ______________________________________________________________________
5053

5154
- Test: `scripts/retry_test.sh`
5255

53-
- [ ] **Step 1: Add the minimal retry loop**
56+
- [x] **Step 1: Add the minimal retry loop**
5457

5558
Accept a maximum-attempt count and delay in seconds followed by the command
5659
and its arguments. Run the command until it succeeds or reaches the limit,
5760
sleep for `base delay * failed-attempt number` between failures, emit a
5861
concise retry message to standard error, and return the final command's exit
5962
status when exhausted.
6063

61-
- [ ] **Step 2: Run the behavioral test to verify it passes**
64+
- [x] **Step 2: Run the behavioral test to verify it passes**
6265

6366
Run: `bash scripts/retry_test.sh`
6467

@@ -72,16 +75,16 @@ ______________________________________________________________________
7275

7376
- Modify: `.github/workflows/ci.yml:282-283`
7477

75-
- [ ] **Step 1: Add the retry test to the scripts job**
78+
- [x] **Step 1: Add the retry test to the scripts job**
7679

7780
Run `bash scripts/retry_test.sh` alongside the existing shell-script tests.
7881

79-
- [ ] **Step 2: Wrap the SSH image build**
82+
- [x] **Step 2: Wrap the SSH image build**
8083

8184
Replace the direct build invocation with
8285
`bash scripts/retry.sh 3 10 docker build -t agentsview-sshd -f testdata/ssh/Dockerfile .`.
8386

84-
- [ ] **Step 3: Run focused validation**
87+
- [x] **Step 3: Run focused validation**
8588

8689
Run: `bash scripts/retry_test.sh`
8790

@@ -97,17 +100,17 @@ ______________________________________________________________________
97100

98101
- Commit the design, plan, helper, test, and workflow changes.
99102

100-
- [ ] **Step 1: Review and scrub the outgoing diff and messages**
103+
- [x] **Step 1: Review and scrub the outgoing diff and messages**
101104

102105
Verify that no private paths, identities, hostnames, or unrelated changes are
103106
present.
104107

105-
- [ ] **Step 2: Commit the implementation**
108+
- [x] **Step 2: Commit the implementation**
106109

107110
Use a focused conventional commit explaining why transient registry
108111
availability should not invalidate successful integration work.
109112

110-
- [ ] **Step 3: Push and open a pull request**
113+
- [x] **Step 3: Push and open a pull request**
111114

112115
As explicitly requested by the user, push the current feature branch and open
113116
a rationale-first PR whose description is a summary only, without a

0 commit comments

Comments
 (0)