feat(api): implement project routes with mock manager/store#47
Conversation
…ul shutdown (#10) Phase 1a of the Go HTTP daemon lane (#10). Stands up the loopback-only sidecar skeleton the later REST/SSE/WS/static surfaces build on: - config: env-driven (AO_HOST/PORT/ENV/timeouts/run-file) with zero-config defaults; binds 127.0.0.1:3001; validates and fails fast on bad input. - httpd: chi router with the recoverer → request-id → logger → real-ip middleware stack and /healthz + /readyz probes. Per-request timeout is carried in config but intentionally not global — it scopes to /api/v1 in Phase 1b so it never throttles SSE/WS/health. - runfile: atomic PID + port handshake (running.json) for the Electron supervisor, with a dead-PID stale check so a crashed predecessor doesn't block startup while a live one fails fast. - server: bind-before-publish (port conflict fails fast), graceful shutdown on SIGINT/SIGTERM via signal.NotifyContext with a 10s hard timeout, and run-file cleanup on exit. Why: the daemon must be safely supervisable as a child process — the supervisor needs a discoverable PID/port and the daemon must not leave a half-started process or stale handshake behind. Locking the lifecycle down now keeps the future port split a small change rather than a rewrite. Tests cover config defaults/overrides/validation, run-file round-trip and live/dead PID detection, health probes, full Run lifecycle, and port-conflict fail-fast. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per review on #14: AO_ENV / Config.Env / IsProduction() weren't load-bearing for Phase 1a — they only switched the slog handler. Removing them now keeps the surface minimal; the env knob can come back later when a real consumer needs it. - config: remove Env field, AO_ENV parsing, and IsProduction helper. - main: collapse newLogger to a single text-handler path. - httpd: drop the env field from the listening log line. - tests: drop the env assertions and AO_ENV fixture. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- config: drop AO_HOST entirely — the daemon is loopback-only by design, so making the bind host env-configurable was a security footgun - config: use net.JoinHostPort in Addr() so IPv6 literals stay valid - config: reject zero/negative AO_REQUEST_TIMEOUT and AO_SHUTDOWN_TIMEOUT (time.ParseDuration accepts both; either would silently break the daemon — instant request expiry / no graceful drain) - runfile: split processAlive into unix/windows build-tagged files so liveness detection is reliable on both platforms (Windows uses OpenProcess; POSIX keeps signal 0) - runfile: document os.Rename overwrite semantics (atomic on POSIX, REPLACE_EXISTING on Windows) so the temp-then-rename pattern's cross-platform behaviour is explicit - httpd tests: give probe/waitForHealth clients an explicit per-request timeout so a stalled connect can't hang the test on the outer deadline
gofmt CI was failing because removing the orphan processAlive doc comment left an extra newline at EOF.
- runfile: introduce build-tagged atomicReplace — POSIX rename(2) on Unix, MoveFileEx with MOVEFILE_REPLACE_EXISTING on Windows. The Go runtime happens to do the Windows call internally already, but invoking it directly makes the cross-platform contract explicit instead of a runtime implementation detail - runfile: tighten process_unix.go build tag from `!windows` to `unix` so plan9/js/wasm fail to build rather than silently using a broken signal-0 probe - runfile: add TestWriteOverwritesExisting covering the stale run-file replace path that none of the previous tests exercised - config: anchor the loopback-only decision in the LoopbackHost doc so the next contributor doesn't reintroduce AO_HOST without the security rationale
chi's middleware.Logger writes via stdlib log to stdout, but the daemon's slog logger writes to stderr — so REST traffic and daemon logs landed on different streams in different formats. Replace it with a small slog-backed requestLogger that: - Wraps the response writer via middleware.NewWrapResponseWriter so status/bytes are accurate even when handlers return without an explicit WriteHeader. - Reads the request id off the context set by middleware.RequestID (kept mounted just before this middleware so the id is available). - Emits one structured Info line per request with method, path, status, bytes, duration, and remote — same key=value shape as the rest of the daemon, one stream for the Electron supervisor to capture.
Mounts the /api/v1 surface on the skeleton router (#10·1a) and registers the 7 canonical project routes as 501 stubs that emit a structured PlannedRoute body documenting the future contract. Shared scaffolding landed here (api.go, errors.go, stubs/, controllers/) so #21/#22 plug in without re-touching the wiring. WHY: opens the route-shell PRs in the Go HTTP daemon lane. Doing it interface-first lets the dashboard team build against the contract before any handler logic exists; the locked APIError envelope and PlannedRoute shape become #19's OpenAPI source-of-truth. REST audit corrections vs the legacy TS surface: R3 PUT /projects/:id alias of PATCH: PUT not registered → 405. R4 POST /projects/:id repair overload: canonical /repair; legacy 405. R5 degraded GET returns 200 with error field: discriminator status. R6 ok/success flag flips: drop on 2xx; return affected resource. R9 bare {error: msg}: locked {error,code,message,requestId,details?}. Legacy paths are deliberately NOT registered; each canonical handler carries PlannedRoute.Legacy so consumers can discover the migration. Zod schemas (TrackerConfig, SCMConfig, AgentConfig, ReactionConfig, LocalProjectConfig, RoleAgentConfig) ported to typed Go structs with an Extra map reserved for .passthrough() round-tripping in later PRs. Closes part of #18; targets feat/issue-10 until #14 merges.
Controllers now depend on ONE inbound interface per resource — ports.ProjectManager — mirroring the existing ports.SessionManager + LifecycleManager pattern. Whether the manager impl reaches into the registry, the LCM, an outbound port, or all three is its own concern; the HTTP layer no longer has to know any of that. WHY: the original split named the boundary type "ProjectService" and put it in a sibling services.go. That implied a second category of port distinct from inbound.go's *Manager interfaces, even though they play the same role (things HTTP/CLI call into the core). Per review feedback, collapse them onto one Manager-per-resource pattern. Mechanical changes: - ports/inbound.go gains ProjectManager next to SessionManager. - ports/services.go renamed to projects.go; keeps only the DTOs the ProjectManager methods take/return. - ProjectsController.Svc renamed to Mgr; APIDeps.Projects type bumped to ports.ProjectManager. All tests pass unchanged; no behavioural change.
The first cut of the route shell duplicated each route's contract twice:
once as a Go literal (stubs.PlannedRoute{...}) in the controller, and
implicitly in the PR description. The Go literal was ~230 LoC of pure
throwaway that would be deleted in handler-impl PRs.
This commit eliminates the duplication:
- backend/internal/httpd/apispec/openapi.yaml: full OpenAPI 3.1 doc
covering the 7 project routes + shared schemas (Project, APIError,
config types). x-replaces records the legacy → canonical mapping
REST-audit corrections produced.
- apispec/apispec.go: //go:embed the YAML, expose Operation(method,
path) → the spec slice as a map, NotImplemented(w, r, method, path)
→ 501 with that slice embedded as `spec`.
- controllers/projects.go: each of 7 handlers is now a one-liner:
apispec.NotImplemented(w, r, "GET", "/api/v1/projects").
- /api/v1/openapi.yaml serves the embedded document so tooling
(SDK gen, the validator slated for #19, dashboard dev tools) can
fetch the whole spec from the same origin as the routes.
- stubs/ package deleted.
When a real handler lands, only the apispec.NotImplemented line goes
away — nothing else does. The spec stays as documentation; consumers
never had to know it was throwaway. #19 (OpenAPI follow-up) is now
half-folded into this PR; the validation middleware remains its own
follow-up.
Tests reshaped: assert envelope + spec.operationId + spec.x-replaces
(replaces the old planned.legacy assertion); add TestOpenAPIYAMLServed
to cover the static spec serve; add apispec_test.go for embed/lookup
behaviour.
Pilots the feature-package layout the backend is migrating toward: a resource's inbound interface and its DTOs live with the resource, not in a central ports/ catch-all. WHY: review flagged ports/ as vague. It conflates three jobs — the outbound capability seam (legit), single-impl inbound interfaces (Go idiom wants these consumer-side), and DTOs that aren't ports at all. This moves the projects contract out as the reference shape #21/#22 follow; the merged session/lifecycle/outbound contracts are left untouched and migrated separately. Scope: INTERFACE ONLY. No implementation — handlers still answer via apispec.NotImplemented and the injected project.Manager stays nil. The impl lands in a later handler-impl PR. Changes: - new internal/project: project.go (Manager interface, 7 endpoints) + dto.go (AddInput/GetResult/UpdateConfigInput/RemoveResult/ReloadResult, moved verbatim from ports/projects.go, Project-prefix dropped). - ports/projects.go deleted; ProjectManager removed from ports/inbound.go. outbound.go and facts.go untouched. - controllers/projects.go and httpd/api.go depend on project.Manager. Domain entities (Project, ProjectSummary, DegradedProject, config types) stay in domain/ as shared vocabulary. go build/vet/test/gofmt all clean; no behavioural change.
Addresses PR review: (1) "why are config_types required at the moment?" and (2) "project objects already defined in project/ — how do we differentiate?" Both had the same root cause: project types were split across domain/ and project/. Fix — keep ALL project types in the project package; only domain.ProjectID (shared with sessions/lifecycle/workspace) stays in domain. - domain/project.go → project/types.go: Project, Summary, Degraded (renamed from ProjectSummary/DegradedProject; the package name carries the "Project" prefix now). - domain/config_types.go deleted. Kept only the 4 shapes the projects API actually exposes — TrackerConfig, SCMConfig, SCMWebhookConfig, ReactionConfig — moved into project/types.go. Dropped AgentConfig, AgentPermission, RoleAgentConfig, LocalProjectConfig (zero references) and the speculative `Extra map[string]any` passthrough fields (no marshaller existed, so they silently dropped data — premature). - project/dto.go + project/project.go reference the local types; ids stay domain.ProjectID. Net: one home for project types, no dead code. go build/vet/test/gofmt clean; no behavioural change (handlers still 501 via apispec).
|
@copilot resolve the merge conflicts in this pull request |
There was a problem hiding this comment.
Pull request overview
Implements the /api/v1/projects REST surface in the Go daemon by replacing route-shell stubs with working handlers backed by an in-memory (mock) project manager/store, and rounds out the backend daemon skeleton (config, server lifecycle, run-file handshake, OpenAPI serving) with tests.
Changes:
- Implemented full projects API handlers (list/add/get/update/delete/repair/reload) backed by a mock
project.Manager+MemoryStore. - Added daemon lifecycle infrastructure: loopback bind config, graceful
Server.Run, and atomicrunning.jsonrun-file handshake (incl. Windows support). - Embedded/served OpenAPI YAML and updated/added unit tests for API behavior, server lifecycle, and spec helpers.
Reviewed changes
Copilot reviewed 29 out of 30 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents how to run/test the backend daemon and its env configuration. |
| backend/main.go | Turns main into a real daemon entrypoint (config, logger, run-file check, server run). |
| backend/internal/runfile/runfile.go | Implements atomic run-file write/read/remove + stale PID detection. |
| backend/internal/runfile/runfile_test.go | Adds run-file unit tests (round trip, overwrite, stale/live PID behavior). |
| backend/internal/runfile/rename_windows.go | Implements atomic replace on Windows via MoveFileExW. |
| backend/internal/runfile/rename_unix.go | Implements atomic replace on Unix via os.Rename. |
| backend/internal/runfile/process_windows.go | Windows PID liveness check via OpenProcess. |
| backend/internal/runfile/process_unix.go | Unix PID liveness check via kill(pid, 0) semantics. |
| backend/internal/project/types.go | Defines project entities and behavior-config DTO shapes exposed via API. |
| backend/internal/project/project.go | Introduces the project.Manager interface (service contract). |
| backend/internal/project/memory_store.go | Adds concurrency-safe in-memory project store implementing Store. |
| backend/internal/project/manager.go | Implements mock Manager behavior over the store (validation/conflicts/reload). |
| backend/internal/project/errors.go | Adds typed manager errors for controller translation to API envelopes. |
| backend/internal/project/dto.go | Adds manager request/response DTOs used by controllers. |
| backend/internal/httpd/server.go | Implements daemon HTTP server lifecycle (bind, serve, graceful shutdown, run-file). |
| backend/internal/httpd/server_test.go | Tests health probes, server lifecycle + run-file behavior, and port conflict. |
| backend/internal/httpd/router.go | Builds router + middleware stack, mounts health, mounts API surface. |
| backend/internal/httpd/log.go | Adds slog-based structured access logging middleware. |
| backend/internal/httpd/json.go | Adds shared JSON response writer helper for the httpd package. |
| backend/internal/httpd/errors.go | Defines the locked APIError envelope and writer for router-level errors. |
| backend/internal/httpd/controllers/projects.go | Implements /api/v1/projects handlers and error translation for projects routes. |
| backend/internal/httpd/controllers/projects_test.go | Replaces route-shell 501 tests with behavior tests for the projects API + OpenAPI serving. |
| backend/internal/httpd/apispec/openapi.yaml | Adds embedded OpenAPI 3.1 spec describing the /api/v1/projects surface. |
| backend/internal/httpd/apispec/apispec.go | Embeds/parses OpenAPI YAML and serves operation slices in 501 stubs + YAML serving. |
| backend/internal/httpd/apispec/apispec_test.go | Tests embedded spec loading, operation lookup behavior, and YAML serving. |
| backend/internal/httpd/api.go | Wires API surface and defaults missing deps to a memory project.Manager; mounts OpenAPI route. |
| backend/internal/config/config.go | Adds env-based daemon config with loopback-only binding and default timeouts/run-file path. |
| backend/internal/config/config_test.go | Adds config unit tests for defaults, overrides, and invalid env values. |
| backend/go.mod | Adds module requirements for chi and yaml. |
| backend/go.sum | Adds sums for new Go module dependencies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| type apiError struct { | ||
| Error string `json:"error"` | ||
| Code string `json:"code"` | ||
| Message string `json:"message"` | ||
| RequestID string `json:"requestId,omitempty"` | ||
| Details map[string]any `json:"details,omitempty"` | ||
| } | ||
|
|
||
| func writeAPIError(w http.ResponseWriter, r *http.Request, status int, kind, code, message string, details map[string]any) { | ||
| writeJSON(w, status, apiError{ | ||
| Error: kind, | ||
| Code: code, | ||
| Message: message, | ||
| RequestID: middleware.GetReqID(r.Context()), | ||
| Details: details, | ||
| }) | ||
| } |
| type apiError struct { | ||
| Error string `json:"error"` | ||
| Code string `json:"code"` | ||
| Message string `json:"message"` | ||
| RequestID string `json:"requestId,omitempty"` | ||
| Details map[string]any `json:"details,omitempty"` | ||
| } | ||
|
|
||
| func writeAPIError(w http.ResponseWriter, r *http.Request, status int, kind, code, message string, details map[string]any) { | ||
| writeJSON(w, status, apiError{ | ||
| Error: kind, | ||
| Code: code, | ||
| Message: message, | ||
| RequestID: middleware.GetReqID(r.Context()), | ||
| Details: details, | ||
| }) | ||
| } |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…-apis # Conflicts: # backend/go.mod # backend/go.sum # backend/internal/httpd/router.go # backend/internal/runfile/rename_windows.go
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
… lenovo/phase3-project-apis
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
… lenovo/phase3-project-apis
Resolved and pushed. I merged |
Greptile SummaryThis PR implements the Phase 3
Confidence Score: 3/5Not safe to merge without addressing the store mutation bugs: deleted projects permanently block re-registration at both the same path and the same ID, double-deletes silently succeed, and every PATCH request discards all supplied config fields. The HTTP wiring, routing, and error envelope layers are solid, but the in-memory store and manager that back every write operation carry four confirmed defects on the mutation paths: the paths index is never cleaned on archive (making path slots unrecoverable), archived entries still pass the id-conflict check (making id slots unrecoverable), Archive is idempotent (second delete returns 200 instead of 404), and UpdateConfig silently discards all patch fields. These are all exercised by the handlers added in this PR and will persist into the SQLite implementation if not corrected in the Store/Manager contract now. backend/internal/project/memory_store.go and backend/internal/project/manager.go — the archive/re-registration and silent-patch bugs live here and affect every project mutation route. Important Files Changed
Sequence DiagramsequenceDiagram
participant C as HTTP Client
participant R as chi Router
participant PC as ProjectsController
participant M as manager (Manager)
participant S as MemoryStore (Store)
C->>R: POST /api/v1/projects
R->>PC: add()
PC->>M: Add(ctx, AddInput)
M->>S: FindByPath(ctx, path)
S-->>M: (row, ok, err)
M->>S: Get(ctx, id)
S-->>M: (row, ok, err)
M->>S: Upsert(ctx, ProjectRow)
S-->>M: nil
M-->>PC: "Project{}"
PC-->>C: "201 {project}"
C->>R: "GET /api/v1/projects/{id}"
R->>PC: get()
PC->>M: Get(ctx, id)
M->>S: Get(ctx, id)
Note over S: Returns archived rows too
S-->>M: (row, true, nil)
M-->>PC: "GetResult{Status:ok}"
PC-->>C: "200 {status, project}"
C->>R: "DELETE /api/v1/projects/{id}"
R->>PC: remove()
PC->>M: Remove(ctx, id)
M->>S: Archive(ctx, id, now)
Note over S: paths index not cleared
Note over S: re-archive succeeds
S-->>M: (true, nil)
M-->>PC: "RemoveResult{}"
PC-->>C: "200 {projectId, removedStorageDir}"
C->>R: "PATCH /api/v1/projects/{id}"
R->>PC: updateConfig()
PC->>PC: containsFrozenIdentityField()
PC->>M: UpdateConfig(ctx, id, patch)
Note over M: patch arg ignored
M-->>PC: notImplemented error
PC-->>C: 501 PROJECT_CONFIG_NOT_IMPLEMENTED
Reviews (4): Last reviewed commit: "style(project): gofmt git repo validatio..." | Re-trigger Greptile |
illegalcall
left a comment
There was a problem hiding this comment.
We have 2 PRs that recommend the backend architecture and library choices:
Let’s try to follow those.
If you disagree with any part of those recommendations, let’s comment/contribute there and keep those PRs as the source of truth.
Based on that, I recommend these changes here:
- Keep the
internal/projectfeature-package direction, but make sure the mock store does not become the production source of truth by accident. - Keep HTTP as the protocol adapter: controllers should call
project.Manager, and storage/config/runtime side effects should stay behind the project feature layer. - If project registration validates a real git repo, use the recommended Git CLI path instead of filesystem-only
.gitchecks. - Make PATCH/DELETE semantics honest: either persist the requested mock state consistently, or return a clear not-implemented/unsupported response until the real project store lands.
- Align the OpenAPI spec with actual behavior, especially around config passthrough and archived/deleted project visibility.
| // per-request timeout so the REST group can apply it without re-reading the | ||
| // environment. | ||
| func NewAPI(cfg config.Config, deps APIDeps) *API { | ||
| if deps.Projects == nil { |
There was a problem hiding this comment.
Defaulting nil deps to project.NewMemoryManager() makes the production router expose a successful but process-local project API whenever callers forget to wire a real manager. That makes POST /projects look durable even though the data disappears on restart. I think the mock manager should be injected explicitly by tests/dev wiring, or nil should keep the 501 behavior until a real project manager is available.
| return Project{}, notFound("PROJECT_NOT_FOUND", "Unknown project") | ||
| } | ||
|
|
||
| // The PR #37 schema stores only registry identity columns. Behaviour config |
There was a problem hiding this comment.
This returns 200 for PATCH while deliberately dropping every requested config field. That is worse than a stub because clients will believe agent, runtime, tracker, etc. were accepted. For the mock phase, can we either persist these fields in the in-memory row/read-model, or return a clear unsupported/not-implemented error until a config store exists?
| if err := validateProjectID(id); err != nil { | ||
| return GetResult{}, err | ||
| } | ||
| row, ok, err := m.store.Get(ctx, string(id)) |
There was a problem hiding this comment.
Because Remove archives the row and Get does not check ArchivedAt, a project remains readable as status: ok after DELETE. That conflicts with the route contract’s “remove/unregister” semantics. Please either treat archived rows as not found/degraded at the API layer, or update the architecture/spec discussion to say deleted projects remain fetchable.
| return filepath.Clean(abs), nil | ||
| } | ||
|
|
||
| func isGitRepo(path string) bool { |
There was a problem hiding this comment.
The stack PR recommends using the real git CLI for Git behavior. A filesystem .git check accepts fake repos and misses some edge cases that git -C <path> rev-parse --is-inside-work-tree would handle. If this endpoint is doing real validation, it should go through the Git CLI path; otherwise keep this explicitly mock-only.
| degradedCount: { type: integer } | ||
|
|
||
| # ---- Behaviour config blobs (ported from the TS Zod schemas) ---- | ||
| # All carry .passthrough() semantics — extra keys are preserved on |
There was a problem hiding this comment.
This spec says extra config keys are preserved and re-emitted, but the Go DTOs do not carry an Extra/raw field and UpdateConfig currently drops the whole patch. Please align the spec with the implementation, or implement passthrough preservation before advertising it as part of the contract.
illegalcall
left a comment
There was a problem hiding this comment.
Rechecked latest head d46bf77. The earlier API semantics comments are partly addressed, but go test ./... now fails in backend/internal/httpd/controllers because valid git init fixtures are rejected as NOT_A_GIT_REPO on macOS.
Checked:
gofmt -l .passesgo vet ./...passesgo test ./...fails in controllers
| if err != nil { | ||
| return false | ||
| } | ||
| top := filepath.Clean(strings.TrimSpace(string(out))) |
There was a problem hiding this comment.
This equality check rejects valid repos on macOS when the input path is under /var/...: git rev-parse --show-toplevel returns the canonical /private/var/... path, while normalizePath keeps /var/..., so the same repo fails this comparison. That is why the latest git init fixtures fail with NOT_A_GIT_REPO. Please canonicalize both paths with filepath.EvalSymlinks before comparing, or treat a successful git -C path rev-parse --show-toplevel as sufficient for repo validation if nested worktrees are acceptable.
There was a problem hiding this comment.
i'll canonicalize both paths with filepath.EvalSymlinks before comparing.
|
@copilot resolve the merge conflicts in this pull request |
illegalcall
left a comment
There was a problem hiding this comment.
Rechecked latest head 0b0b136. The /var vs /private/var git repo fixture issue is fixed now: go test ./... and go vet ./... pass locally.
Still not ready to merge:
- GitHub currently marks this PR as
CONFLICTING/DIRTYagainst latestmainafter PR #37 merged. Please rebase/merge latestmainand resolve thebackend/go.mod/backend/go.sumconflict. gofmt -l .returnsbackend/internal/project/manager.go.
After that, rerun the backend checks on the rebased branch.
| top := filepath.Clean(strings.TrimSpace(string(out))) | ||
| path = filepath.Clean(path) | ||
| top, err = filepath.EvalSymlinks(top) | ||
| if err != nil { |
There was a problem hiding this comment.
This new block needs gofmt; gofmt -l . currently reports backend/internal/project/manager.go.
There was a problem hiding this comment.
yep working on that only
illegalcall
left a comment
There was a problem hiding this comment.
Rechecked latest head 5046ea8. The previous merge conflict and gofmt issues are resolved.
Checked locally:
gofmt -l .passesgo vet ./...passesgo test ./...passesgo test -race ./...passes
GitHub now reports the PR as MERGEABLE / CLEAN.
PR was building, leaving #24 conflicting on nearly every file. The route shell itself is now redundant, but two pieces of #24 are genuinely net-new and absent from main — salvage them here, rebuilt on top of #47's merged code: - Code-first OpenAPI: apispec/specgen reflects the controllers' request/response types and project DTOs (the same types the handlers use at runtime) into openapi.yaml via swaggest. `cmd/genspec` + `go:generate` regenerate the committed, embedded spec; a drift test (TestBuild_MatchesEmbedded) and a route parity test (TestRouteSpecParity) fail CI if the spec and the code disagree. This replaces main's hand-maintained openapi.yaml so the "single source of truth" claim is actually enforced, not aspirational. - Typed frontend client: frontend/src/api/schema.d.ts is generated from that spec via openapi-typescript (`npm run gen:api`), consumed by a small openapi-fetch client. The frontend now gets its types from the daemon contract instead of hand-maintaining them. specgen lives outside apispec (which controllers import for the 501 stub) to avoid an import cycle. Handlers now encode named response DTOs (controllers/dto.go) instead of map[string]any so the generator reflects the real wire shapes. A gen-verify CI job regenerates both artifacts and fails on a stale commit. Tradeoff: the generated spec drops the hand-authored examples / x-rest-audit notes from #47's openapi.yaml; those can be re-added as operation metadata in specgen if wanted. Behaviour-only patch (no handler logic changes). Supersedes the codegen + frontend parts of #24. Refs #20, #47.
Squashed feat/20 for a clean rebase onto main (which now carries #47's parallel implementation). Keeps our architecture: httpx transport, swaggest-generated openapi.yaml + frontend TS types, CI drift guard, 5 REST-corrected routes, 500 SERVICE_UNAVAILABLE while the Manager is nil.
Implement the existing 5-method project.Manager backed directly by the sqlite project store (no extra store/port/adapter, no memory store). Correcting #47: reuses our interface, drops reload/repair, and uses one error type. - project/manager.go: NewManager(*sqlite.Store) calling project_store.go directly. Add validates path (git rev-parse) + derives/validates the id + path/id conflict checks (suggestedProjectId); List/Get/Remove are real; Get always status:"ok" and UpdateConfig returns a typed error until config persistence exists (registry-only scope). - httpx.APIErr: one typed error (status+kind+code+message+details) with BadRequest/NotFound/Conflict/Internal constructors. The manager returns these; controllers translate via writeErr. Replaces #47's separate project/errors.go. - Wire the real Manager into main.go (httpd.New now takes APIDeps). - Tests: manager_test.go drives all 5 methods + validation/conflict cases against a real temp-dir sqlite store; controller test covers the typed→HTTP translation. Verified live: real 201/200/400/404/409 over the daemon. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR was building, leaving #24 conflicting on nearly every file. The route shell itself is now redundant, but two pieces of #24 are genuinely net-new and absent from main — salvage them here, rebuilt on top of #47's merged code: - Code-first OpenAPI: apispec/specgen reflects the controllers' request/response types and project DTOs (the same types the handlers use at runtime) into openapi.yaml via swaggest. `cmd/genspec` + `go:generate` regenerate the committed, embedded spec; a drift test (TestBuild_MatchesEmbedded) and a route parity test (TestRouteSpecParity) fail CI if the spec and the code disagree. This replaces main's hand-maintained openapi.yaml so the "single source of truth" claim is actually enforced, not aspirational. - Typed frontend client: frontend/src/api/schema.d.ts is generated from that spec via openapi-typescript (`npm run gen:api`), consumed by a small openapi-fetch client. The frontend now gets its types from the daemon contract instead of hand-maintaining them. specgen lives outside apispec (which controllers import for the 501 stub) to avoid an import cycle. Handlers now encode named response DTOs (controllers/dto.go) instead of map[string]any so the generator reflects the real wire shapes. A gen-verify CI job regenerates both artifacts and fails on a stale commit. Tradeoff: the generated spec drops the hand-authored examples / x-rest-audit notes from #47's openapi.yaml; those can be re-added as operation metadata in specgen if wanted. Behaviour-only patch (no handler logic changes). Supersedes the codegen + frontend parts of #24. Refs #20, #47.
…ory store The project Manager now runs only against the durable backend store: remove the process-local MemoryStore (and NewMemoryManager), and require a real Store. The daemon already wires the sqlite store; tests now build a real temp-dir sqlite store instead of the mock. - Move Row + the Store port to project/store.go. The Store interface stays because it is the dependency-inversion port that lets the manager reach the backend without an import cycle (storage imports project.Row), not an extra mock layer — there is no longer any in-memory implementation. - NewManager requires a non-nil Store (no in-memory fallback). - Add project/manager_test.go: List/Add/Get/Remove happy paths + PATH_REQUIRED/NOT_A_GIT_REPO/PATH_ALREADY_REGISTERED/ID_ALREADY_REGISTERED, PROJECT_NOT_FOUND/INVALID_PROJECT_ID, and UpdateConfig — all against a real sqlite store (the service-logic tests #47 lacked). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(project): manager talks to the sqlite store; drop the in-memory store The project Manager now runs only against the durable backend store: remove the process-local MemoryStore (and NewMemoryManager), and require a real Store. The daemon already wires the sqlite store; tests now build a real temp-dir sqlite store instead of the mock. - Move Row + the Store port to project/store.go. The Store interface stays because it is the dependency-inversion port that lets the manager reach the backend without an import cycle (storage imports project.Row), not an extra mock layer — there is no longer any in-memory implementation. - NewManager requires a non-nil Store (no in-memory fallback). - Add project/manager_test.go: List/Add/Get/Remove happy paths + PATH_REQUIRED/NOT_A_GIT_REPO/PATH_ALREADY_REGISTERED/ID_ALREADY_REGISTERED, PROJECT_NOT_FOUND/INVALID_PROJECT_ID, and UpdateConfig — all against a real sqlite store (the service-logic tests #47 lacked). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(project): trim routes, consolidate package, add code-first OpenAPI - Remove POST /reload, PATCH /{id}, POST /{id}/repair routes and their Manager methods (Reload, UpdateConfig, Repair) and DTOs (ReloadResult, UpdateConfigInput) — not needed at this stage - Merge Manager interface into manager.go; delete project.go (single-impl split served no purpose) - Remove dead notImplemented helper from errors.go - Port PR #59 code-first OpenAPI generation: controllers/dto.go named response types, specgen/build.go (4 routes), parity + drift tests, cmd/genspec, go generate wiring; regenerate openapi.yaml - Add swaggest deps; add YAML() method to apispec.Spec Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * fix(project): address PR review comments - t.Skipf → t.Fatalf in gitRepo helper: git failures now hard-fail instead of silently skipping manager tests on a misconfigured runner - FindProjectByPath: add AND archived_at IS NULL so archived paths don't permanently block re-registration (update queries/projects.sql and generated gen/projects.sql.go) - Add TestManager_ReaddAfterRemove to lock the fix Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * fixed lint and fmt * addressed greptile comments * Apply suggestions from code review Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * project tests fix * project_tests fix * fix: Linting and formatting fix * refactor: move project manager into service layer (#68) * refactor: split service package by resource (#68) * fix: ignore archived project id conflicts (#68) * refactor: move pr manager into service layer (#68) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: itrytoohard <ayetrytoohard@gmail.com>
Summary
Implements the Phase 3
/api/v1/projectsAPI surfaceon top of PR #24 route shell by replacing
501 Not Implementedstubs with working handlers backed by amock in-memory project manager/store.
What Changed
backend/ internal/httpd/controllers/projects.go:GET /api/v1/projectsPOST /api/v1/projectsGET /api/v1/projects/{id}PATCH /api/v1/projects/{id}DELETE /api/v1/projects/{id}POST /api/v1/projects/{id}/repairPOST /api/v1/projects/reloadbackend/internal/project/manager.gobackend/internal/project/memory_store.gobackend/internal/project/errors.gobackend/internal/ httpd/api.gowhen project deps are not provided.in
backend/internal/httpd/controllers/ projects_test.go.backend/internal/ runfile/rename_windows.go(MoveFileExWviakernel32.dll) so backend tests run on Windows.Behavior / Validation
INVALID_JSON,PATH_REQUIRED,NOT_A_GIT_REPO,INVALID_PROJECT_ID,PROJECT_NOT_FOUND,IDENTITY_FROZEN,PATH_ALREADY_REGISTERED,ID_ALREADY_REGISTERED,REPAIR_NOT_AVAILABLE).projectId,path,repo,defaultBranch.yet).
Testing
Ran locally: