Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .air.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ tmp_dir = "tmp"

[build]
cmd = "./scripts/dev-backend-build.sh"
bin = "./tmp/agentsview"
bin = "./tmp/periscope"
args_bin = ["serve"]
delay = 500
exclude_dir = ["frontend", "internal/web/dist", "tmp", "vendor", ".git", "dist", "desktop", "jetbrains-plugin", "data", "sessions", "html", ".superpowers"]
Expand Down
9 changes: 8 additions & 1 deletion .cursor/rules/never-undo-attribution-expunge.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@ That is destructive for all collaborators. **Do not cause another rewrite.**
| Do | Do not |
|----|--------|
| Read `.cursor/private/agent-lesson-git-attribution.md` before any git write | Copy forbidden tokens into tracked files, commits, PRs, or LESSONS |
| Commit with `bash scripts/git/commit-clean.sh -m "..."` after `git add` | Raw `git commit` in Cursor cloud |
| `git add` → `verify-staged-for-commit.sh` → `commit-clean.sh` (in that order) | Run `commit-clean.sh` without staging or verify |
| Push with `bash scripts/git/publish-clean-branch.sh <branch> main origin` | Raw `git push` without audit |
| Run `bash scripts/git/daily-attribution-guard.sh` at session start | "Document" forbidden emails in repo docs so you remember |

```bash
git add <paths>
bash scripts/git/verify-staged-for-commit.sh # must print OK + non-empty stat
bash scripts/git/commit-clean.sh -m "type(scope): summary"
git show --stat --oneline HEAD # confirm file delta before push
```

## Session checklist (every day)

```bash
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-macos-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ jobs:
run: go run ./internal/pricing/cmd/litellm-snapshot -restore

- name: Run focused macOS FSEvents tests
run: CGO_ENABLED=1 go test -tags "fts5" ./internal/fsevents ./internal/sync ./internal/db ./cmd/agentsview -count=1 -timeout=20m
run: CGO_ENABLED=1 go test -tags "fts5" ./internal/fsevents ./internal/sync ./internal/db ./cmd/periscope -count=1 -timeout=20m
8 changes: 4 additions & 4 deletions .github/workflows/ci-pr.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
name: CI

# The PR dispatcher always calls the main-pinned workflow. That workflow
# independently routes same-repository Linux jobs to the managed public fleet
# and fork jobs to GitHub-hosted runners.
# Fork PRs must run the checked-in workflow so periscope-specific paths and
# CI fixes apply. Upstream kenn-io/agentsview still owns the canonical
# workflow for same-repo PRs on that repository.
on:
pull_request:

permissions: read-all

jobs:
run:
uses: kenn-io/agentsview/.github/workflows/ci.yml@main
uses: ./.github/workflows/ci.yml
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ jobs:
throw "could not determine Rust host triple"
}
New-Item -ItemType Directory -Force desktop/src-tauri/binaries | Out-Null
New-Item -ItemType File -Force "desktop/src-tauri/binaries/agentsview-$hostTriple.exe" | Out-Null
New-Item -ItemType File -Force "desktop/src-tauri/binaries/periscope-$hostTriple.exe" | Out-Null

- name: Fetch Windows desktop Rust dependencies
shell: pwsh
Expand Down Expand Up @@ -352,7 +352,7 @@ jobs:
CGO_ENABLED=1 go build -tags "fts5,kit_posthog_disabled" \
-o /tmp/testfixture ./cmd/testfixture
CGO_ENABLED=1 go build -tags "fts5,kit_posthog_disabled" \
-o /tmp/agentsview ./cmd/agentsview
-o /tmp/periscope ./cmd/periscope
env:
CGO_ENABLED: "1"

Expand All @@ -365,4 +365,4 @@ jobs:
working-directory: frontend
env:
E2E_PREBUILT_FIXTURE: /tmp/testfixture
E2E_PREBUILT_SERVER: /tmp/agentsview
E2E_PREBUILT_SERVER: /tmp/periscope
112 changes: 57 additions & 55 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build_script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestDevBackendBuildRestoresPricingSnapshotBeforeBuild(t *testing.T) {
"go run ./internal/pricing/cmd/litellm-snapshot -restore",
"go build -tags fts5",
)
require.FileExists(t, filepath.Join(root, "tmp", "agentsview"))
require.FileExists(t, filepath.Join(root, "tmp", "periscope"))
}

func TestDevBackendBuildStopsWhenPricingSnapshotRestoreFails(t *testing.T) {
Expand Down
32 changes: 32 additions & 0 deletions cmd/periscope/artifact_machine_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package main

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/latentsignal-org/periscope/internal/config"
"github.com/latentsignal-org/periscope/internal/db"
)

func TestOpenDBConfiguresArtifactLocalMachineOwnership(t *testing.T) {
cfg := config.Config{
DBPath: filepath.Join(t.TempDir(), "sessions.db"),
LocalMachineName: "workstation.example",
}
database, err := openDB(cfg)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, database.Close()) })
require.NoError(t, database.UpsertSession(db.Session{
ID: "hostname-local", Project: "project",
Machine: cfg.LocalMachineName, Agent: "claude",
}))
_, err = database.EnsureArtifactOrigin("desktop-a1b2c3")
require.NoError(t, err)

pending, err := database.PendingArtifactExports(t.Context(), 10)
require.NoError(t, err)
require.Len(t, pending, 1)
assert.Equal(t, "hostname-local", pending[0].SessionID)
}
6 changes: 5 additions & 1 deletion cmd/periscope/duckdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/latentsignal-org/periscope/internal/config"
duckdbsync "github.com/latentsignal-org/periscope/internal/duckdb"
"github.com/latentsignal-org/periscope/internal/pathutil"
"github.com/latentsignal-org/periscope/internal/server"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -515,7 +516,10 @@ func runDuckDBQuackServe(cfg DuckDBQuackServeConfig) {
fatal("duckdb quack serve: %v", err)
}
if cfg.Path != "" {
duckCfg.Path = cfg.Path
duckCfg.Path, err = pathutil.ExpandHome(cfg.Path)
if err != nil {
fatal("duckdb quack serve: expanding --path: %v", err)
}
}
if cfg.AllowInsecure {
duckCfg.AllowInsecure = true
Expand Down
7 changes: 7 additions & 0 deletions cmd/periscope/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/latentsignal-org/periscope/internal/config"
"github.com/latentsignal-org/periscope/internal/db"
"github.com/latentsignal-org/periscope/internal/importer"
"github.com/latentsignal-org/periscope/internal/pathutil"
)

type ImportConfig struct {
Expand All @@ -19,6 +20,12 @@ type ImportConfig struct {
}

func runImport(cfg ImportConfig) {
expandedPath, err := pathutil.ExpandHome(cfg.Path)
if err != nil {
log.Fatalf("expanding import path: %v", err)
}
cfg.Path = expandedPath

appCfg, err := config.LoadMinimal()
if err != nil {
log.Fatalf("loading config: %v", err)
Expand Down
8 changes: 8 additions & 0 deletions cmd/periscope/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,14 @@ func openDB(cfg config.Config) (*db.DB, error) {
if err != nil {
return nil, err
}
localMachine := cfg.LocalMachineName
if strings.TrimSpace(localMachine) == "" {
localMachine = "local"
}
if err := database.ConfigureArtifactLocalMachine(localMachine); err != nil {
database.Close()
return nil, fmt.Errorf("configuring artifact local machine: %w", err)
}
applyCustomPricing(database, cfg)
return database, nil
}
Expand Down
13 changes: 9 additions & 4 deletions cmd/periscope/periodic_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,26 @@ func TestScheduledReconcileTargetsSelectsOnlyOptedInProviders(t *testing.T) {
aiderDir := filepath.Join(home, "aider")
coworkDir := filepath.Join(home, "cowork")
claudeDir := filepath.Join(home, "claude")
omnigentDir := filepath.Join(home, "omnigent")
require.NoError(t, os.MkdirAll(aiderDir, 0o755))
require.NoError(t, os.MkdirAll(coworkDir, 0o755))
require.NoError(t, os.MkdirAll(claudeDir, 0o755))
require.NoError(t, os.MkdirAll(omnigentDir, 0o755))

cfg := config.Config{
AgentDirs: map[parser.AgentType][]string{
parser.AgentAider: {aiderDir},
parser.AgentCowork: {coworkDir},
parser.AgentClaude: {claudeDir},
parser.AgentAider: {aiderDir},
parser.AgentCowork: {coworkDir},
parser.AgentClaude: {claudeDir},
parser.AgentOmnigent: {omnigentDir},
},
}
targets := scheduledReconcileTargets(cfg)
require.Len(t, targets, 1, "only the opted-in provider is scheduled")
require.Len(t, targets, 2, "only opted-in providers are scheduled")
assert.Equal(t, parser.AgentAider, targets[0].Agent)
assert.Equal(t, []string{aiderDir}, targets[0].Roots)
assert.Equal(t, parser.AgentOmnigent, targets[1].Agent)
assert.Equal(t, []string{omnigentDir}, targets[1].Roots)
}

func TestScheduledReconcileDefersUnavailableOptedInRoots(t *testing.T) {
Expand Down
33 changes: 30 additions & 3 deletions cmd/periscope/recall.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/latentsignal-org/periscope/internal/config"
"github.com/latentsignal-org/periscope/internal/db"
"github.com/latentsignal-org/periscope/internal/pathutil"
corerecall "github.com/latentsignal-org/periscope/internal/recall"
"github.com/latentsignal-org/periscope/internal/service"
)
Expand Down Expand Up @@ -912,7 +913,11 @@ func newRecallImportCommand() *cobra.Command {
return err
}
}
f, err := os.Open(args[0])
path, err := pathutil.ExpandHome(args[0])
if err != nil {
return fmt.Errorf("expanding recall import path: %w", err)
}
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("opening recall import file: %w", err)
}
Expand Down Expand Up @@ -1050,7 +1055,7 @@ func addRecallFilterFlags(cmd *cobra.Command, f *service.RecallFilter) {
flags := cmd.Flags()
flags.StringVar(&f.Query, "query", "", "Filter by query text")
flags.StringVar(&f.Project, "project", "", "Filter by project")
flags.StringVar(&f.CWD, "cwd", "", "Filter by cwd")
flags.Var(&homePathValue{value: &f.CWD}, "cwd", "Filter by cwd")
flags.StringVar(&f.GitBranch, "git-branch", "", "Filter by git branch")
flags.StringVar(&f.Agent, "agent", "", "Filter by agent")
flags.StringVar(&f.Type, "type", "", "Filter by recall type")
Expand Down Expand Up @@ -1101,6 +1106,28 @@ func addRecallFilterFlags(cmd *cobra.Command, f *service.RecallFilter) {
flags.IntVar(&f.Limit, "limit", 0, "Maximum entries to return")
}

type homePathValue struct {
value *string
}

func (v *homePathValue) Set(path string) error {
expanded, err := pathutil.ExpandHome(path)
if err != nil {
return err
}
*v.value = expanded
return nil
}

func (v *homePathValue) String() string {
if v == nil || v.value == nil {
return ""
}
return *v.value
}

func (*homePathValue) Type() string { return "path" }

func addRecallEntryCurrentCWDFlag(cmd *cobra.Command, currentCWD *bool) {
cmd.Flags().BoolVar(
currentCWD,
Expand Down Expand Up @@ -1238,7 +1265,7 @@ func addRecallQueryFlags(cmd *cobra.Command, req *service.RecallQuery) {
"Retrieval mode: lexical, vector, or hybrid",
)
flags.StringVar(&req.Project, "project", "", "Filter by project")
flags.StringVar(&req.CWD, "cwd", "", "Filter by cwd")
flags.Var(&homePathValue{value: &req.CWD}, "cwd", "Filter by cwd")
flags.StringVar(&req.GitBranch, "git-branch", "", "Filter by git branch")
flags.StringVar(&req.Agent, "agent", "", "Filter by agent")
flags.StringVar(&req.Type, "type", "", "Filter by recall type")
Expand Down
13 changes: 13 additions & 0 deletions cmd/periscope/recall_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"strings"
"testing"

"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

Expand All @@ -22,6 +23,18 @@ import (
"github.com/latentsignal-org/periscope/internal/service"
)

func TestRecallCWDFlagExpandsHome(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
var filter service.RecallFilter
cmd := &cobra.Command{Use: "test"}
addRecallFilterFlags(cmd, &filter)

require.NoError(t, cmd.Flags().Parse([]string{"--cwd", "~/work"}))
assert.Equal(t, filepath.Join(home, "work"), filter.CWD)
}

func TestPrintRecallEntryReviewLineDefaultsReviewStateToUnreviewedAuto(
t *testing.T,
) {
Expand Down
5 changes: 5 additions & 0 deletions cmd/periscope/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/latentsignal-org/periscope/internal/config"
"github.com/latentsignal-org/periscope/internal/service"
"github.com/latentsignal-org/periscope/internal/timeutil"
"github.com/latentsignal-org/periscope/internal/pathutil"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
Expand Down Expand Up @@ -219,6 +220,10 @@ func explicitServerToken(cmd *cobra.Command) (string, error) {
}
path, err := cmd.Flags().GetString("server-token-file")
if err == nil && strings.TrimSpace(path) != "" {
path, err = pathutil.ExpandHome(path)
if err != nil {
return "", fmt.Errorf("expanding --server-token-file: %w", err)
}
b, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("reading --server-token-file: %w", err)
Expand Down
25 changes: 25 additions & 0 deletions cmd/periscope/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1518,6 +1518,31 @@ func TestSessionUsage_ServerTokenSendsBearer(t *testing.T) {
require.NotNil(t, out)
}

func TestSessionUsage_ServerTokenFileExpandsHome(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
tokenFile := filepath.Join(home, "remote-token")
require.NoError(t, os.WriteFile(
tokenFile, []byte("remote-secret\n"), 0o600,
))

ts, _ := newRemoteUsageServer(t, remoteUsageSpec{
canonicalID: "remote-session",
bearer: "remote-secret",
serverRunning: true,
})

cmd := sessionUsageCommand(t,
"session", "usage", "remote-session",
"--server", ts.URL,
"--server-token-file", "~/remote-token")

out, _, err := sessionUsageDataForCommand(cmd, "remote-session")
require.NoError(t, err)
require.NotNil(t, out)
}

func TestSessionUsage_ServerHTTPClientHasTimeout(t *testing.T) {
oldClient := sessionUsageHTTPClient
sessionUsageHTTPClient = &http.Client{Timeout: 20 * time.Millisecond}
Expand Down
14 changes: 14 additions & 0 deletions cmd/periscope/sync_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"runtime/pprof"
"runtime/trace"
"slices"

"github.com/latentsignal-org/periscope/internal/pathutil"
)

// startSyncProfile starts whichever of the hidden --cpuprofile,
Expand All @@ -19,6 +21,9 @@ import (
// profiling typo never aborts a real sync.
func startSyncProfile(cfg SyncConfig) func() {
var stoppers []func()
cfg.CPUProfile = expandSyncProfilePath("cpuprofile", cfg.CPUProfile)
cfg.MemProfile = expandSyncProfilePath("memprofile", cfg.MemProfile)
cfg.Trace = expandSyncProfilePath("trace", cfg.Trace)

if cfg.CPUProfile != "" {
f, err := os.Create(cfg.CPUProfile)
Expand Down Expand Up @@ -81,3 +86,12 @@ func startSyncProfile(cfg SyncConfig) func() {
}
}
}

func expandSyncProfilePath(name, path string) string {
expanded, err := pathutil.ExpandHome(path)
if err != nil {
log.Printf("%s: expand path: %v", name, err)
return ""
}
return expanded
}
4 changes: 2 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,5 +281,5 @@ on the next replay.
| [`periscope-upstream-sync-blueprint.md`](guides/periscope-upstream-sync-blueprint.md) | Canonical sync policy |
| [`periscope-modernization-status.md`](guides/periscope-modernization-status.md) | Current modernization snapshot |
| [`agentsview-to-periscope-rename-catalogue.md`](guides/agentsview-to-periscope-rename-catalogue.md) | Rename decision tree |
| [`superpowers/specs/2026-05-10-periscope-build-design.md`](superpowers/specs/2026-05-10-periscope-build-design.md) | Historical build design (May 2026) |
| [`superpowers/plans/2026-05-10-periscope-build-system.md`](superpowers/plans/2026-05-10-periscope-build-system.md) | Historical implementation plan |
| [`superpowers/specs/2026-05-10-periscope-build-design.md`](https://github.com/diazMelgarejo/periscope/blob/merged/docs/superpowers/specs/2026-05-10-periscope-build-design.md) | Historical build design (May 2026) |
| [`superpowers/plans/2026-05-10-periscope-build-system.md`](https://github.com/diazMelgarejo/periscope/blob/merged/docs/superpowers/plans/2026-05-10-periscope-build-system.md) | Historical implementation plan |
Loading