Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
8683f2e
fix(cursor): hold discovery/decompose during optional keep-or-drop offer
jwaldrip May 28, 2026
f0ff907
fix(plan): single-source active stage + plan from intent main
jwaldrip May 28, 2026
3adc852
wip(optstage): Layer 1 filter fix + Layer 2 keystone + Layer 3/4 part…
jwaldrip May 29, 2026
fd90f92
fix(optstage): finish Layers 2-4 — keystone main-read + reader fallba…
jwaldrip May 29, 2026
38427f9
test(optstage): Layer 3 heal — diverged optional stage self-corrects …
jwaldrip May 29, 2026
7b6dc34
fix(optstage): correct Layer 1 filter + commit reader delegate + matt…
jwaldrip May 29, 2026
1e5c4b4
style(optstage): biome format + import order on changed files
jwaldrip May 29, 2026
497e15f
fix(optstage): convert lazy require() to static ESM imports + fix off…
jwaldrip May 29, 2026
42fb59c
fix(optstage): complete require→static ESM conversion (imports + body…
jwaldrip May 29, 2026
e94acd7
fix(optstage): add missing resolveActiveStageWithFallback import to s…
jwaldrip May 29, 2026
a6b7f45
fix(optstage): keep studio cycle imports + fix heal test fixture
jwaldrip May 29, 2026
9ee0bcb
fix(optstage): autopilot exempt from discovery-hold + fix two test fi…
jwaldrip May 29, 2026
a9f02b1
test(optstage): fix two fixtures to put the optional stage first
jwaldrip May 29, 2026
bfe3d4f
wip(optstage): holds-discovery test — pos.action fix, fixture still u…
jwaldrip May 29, 2026
1bd6b2a
test(optstage): holds-discovery GREEN — stamp elaboration_verified_at…
jwaldrip May 29, 2026
a89d18e
test(optstage): holds-discovery GREEN — stamp quoted verified_at to r…
jwaldrip May 29, 2026
74c6002
feat(browse): feedback deep links in the browse URL lib parser
jwaldrip May 29, 2026
b104e24
feat(statusline): OSC 8 clickable links to haikumethod.ai
jwaldrip May 29, 2026
a684096
chore(browse): drop orphaned url.test.ts from the reverted rewrite
jwaldrip May 29, 2026
14f0316
test(statusline): wave-membership assert ignores the new chip url field
jwaldrip May 29, 2026
8cae2bf
feat(browse): feedback deep-link route — statusline feedback chips la…
jwaldrip May 29, 2026
665a48c
feat(statusline): set refreshInterval:1 on install so the cursor walk…
jwaldrip May 29, 2026
e1bd1ce
fix(bin): resolve symlinked invocation so npx finds the bundle
jwaldrip May 29, 2026
b9bbfd3
feat(auth): OAuth Phase 1 — global provider-token store + auth status…
jwaldrip May 29, 2026
78b155e
fix(auth): re-export auth-tools + global-settings schemas from the ba…
jwaldrip May 29, 2026
3b15012
test(feedback): lock rejected_at terminal-skip — report 20260528
jwaldrip May 29, 2026
79ae53d
feat(auth-proxy): add CLI device-flow OAuth handshake + Firestore ses…
jwaldrip May 29, 2026
4db5c95
feat(engine): post-execution closing BRIEF rewrite (#17)
jwaldrip May 29, 2026
65caf1f
fix(auth): cast provider to enum in haiku_auth_logout (TS2345)
jwaldrip May 29, 2026
6234c96
Revert "feat(engine): post-execution closing BRIEF rewrite (#17)"
jwaldrip May 29, 2026
0393a0d
Reapply "feat(engine): post-execution closing BRIEF rewrite (#17)"
jwaldrip May 29, 2026
a52fd47
feat(engine): closing brief rewrites BRIEF.md at stage finish (#17)
jwaldrip May 29, 2026
3e554e7
feat(auth): broker login (#13) + provider proof upload (#15)
jwaldrip May 29, 2026
1d207a4
refactor(engine): closing brief via haiku_write_brief tool, engine-ow…
jwaldrip May 29, 2026
482f12e
feat(engine): route PR/MR create + mark-ready through stored provider…
jwaldrip May 29, 2026
f784739
docs(engine): sync surface for provider OAuth + write_brief (#16)
jwaldrip May 29, 2026
5f8ae9a
fix(engine): escape a dropped/dangling stage branch on tick (#22)
jwaldrip May 29, 2026
679899c
fix(engine): guard placeholder-grep quality gates + drop stale feedba…
jwaldrip May 29, 2026
9394d64
fix(engine): manual feedback reject/delete lands on the engine's read…
jwaldrip May 29, 2026
d2735dc
test(engine): feedback stays addressable through pending_seal until t…
jwaldrip May 29, 2026
a0e1f59
feat(auth): authenticate-when-needed for PR/MR + proof; fix broker pr…
jwaldrip May 29, 2026
0758ddd
style(engine): biome — format drift + drop two lint errors (shadowed …
jwaldrip May 29, 2026
6077048
fix(auth): address claude-review — GitLab proof upload uses Bearer; d…
jwaldrip May 29, 2026
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
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,14 @@ When modifying any component, check if other components need corresponding updat
| Hard Gates | Execution phase | exit code enforcement in quality-gate.sh | orchestrator.ts |
| Persistence | Context Preservation | Environment-detected via `isGitRepo()` (git or filesystem) | state-tools.ts, git-worktree.ts |
| Providers | Memory Providers section | `plugin/schemas/providers/*.json`, `plugin/providers/*.md` | config.sh |
| Provider OAuth (auth + token store) | N/A — implementation | Git-provider (github/gitlab) auth brokered through **haikumethod.ai** (NOT .com) — the Cloud Function in `deploy/auth-proxy/` runs a brokered authorization-code handshake (NOT the provider's native RFC-8628 device flow): `/cli/start` mints a **`session_id`** + `verification_url`, the browse-site callback POSTs the exchanged token to `/cli/complete`, the CLI polls `/cli/poll { session_id }` and reads the token bundle **spread at top level** of the `ready` response (matches `deploy/auth-proxy/src/cli.ts`). Token stored client-only in `~/.haiku/settings.json`. **Auth-when-needed (NOT auth-first):** `ensureProviderToken(provider)` (in `haiku_auth_login.ts`) returns a usable stored token or runs the handshake INLINE — the engine never tells the agent to "call haiku_auth_login first." Provider is the repo's (origin host). Tools: `haiku_auth_login` (explicit login), `haiku_auth_status`, `haiku_auth_logout`, `haiku_upload_proof` (upload proof to the delivery PR/MR over REST — auto-auths via `ensureProviderToken`; `proof_upload_auth_unavailable` only when auth genuinely can't be obtained). Token shape (`access_token`/`refresh_token`/`expires_at`/`scopes`/`account`/`host`) + read/write/clear live in `global-settings.ts`; provider detection (`parseGitRemote`/`providerFromHost`/`providerFromOrigin`/`readOriginRemoteUrl`) in `git-worktree.ts` | global-settings.ts, state/schemas/global-settings.ts, tools/orchestrator/haiku_auth_*.ts + haiku_upload_proof.ts, deploy/auth-proxy/ |
| PR/MR ops via stored token (Phase 4) | N/A — implementation | The engine drives PR/MR **create** + **mark-ready** over the provider REST API (`provider-rest.ts`, injectable fetch), **authenticating when needed**: `resolvePrRestContextEnsuringAuth` (origin host → provider → `ensureProviderToken`) obtains a token inline if none is stored — no "auth first." Any REST miss (incl. auth that couldn't be obtained — broker down / declined / headless) falls back to the `gh`/`glab` CLI (`openPullRequestCli`). The pre-open guard skips only when there's no CLI AND no recognized provider remote (`providerFromOrigin`). NO merge over REST — the human's merge is the approval signal (merge stays CLI/human-only). The two synchronous handler entry points (intent-main draft open in `haiku_intent_create`, repair PR in `haiku_repair`) stay CLI-only — a sync handler can't await REST/auth. REST contracts are doc-derived + mock-validated; CLI is the integration-proven path | provider-rest.ts, git-worktree.ts (`openPullRequest`/`markPullRequestReady` async + `resolvePrRestContextEnsuringAuth`), orchestrator/workflow/side-effects.ts |
| Closing brief (BRIEF.md) | Quality Enforcement | Per-stage user-facing `BRIEF.md` written twice via the engine-owned `haiku_write_brief { body }` tool — `pre` (the plan) on first write, `post` (what shipped) on the closing rewrite at stage finish. The tool takes ONLY the body; the engine resolves intent (from branch), stage (from cursor), and the `phase:` frontmatter (file absent → pre, present → post — the same signal `stageOwesClosingBrief` gates on, so frontmatter can't drift from the cursor). Frontmatter via gray-matter. The `write_brief` cursor action fires from `stageOwesBrief` (pre, BRIEF absent) and `stageOwesClosingBrief` (post, BRIEF exists + `phase != post`); two reachable surfaces (non-autopilot user-gate, autopilot/merge in `haiku_run_next`). Opt out with `brief: false` on intent FM | tools/orchestrator/haiku_write_brief.ts, orchestrator/workflow/cursor.ts (`stageOwesBrief`/`stageOwesClosingBrief`), prompts/stage/review/write_brief/ |
| Harness | N/A (implementation detail) | `--harness <name>` MCP arg or `HAIKU_HARNESS` env var; capability registry in `harness.ts`, instruction adaptation in `harness-instructions.ts` | harness.ts, harness-instructions.ts, orchestrator.ts, server.ts |
| Architecture (canonical) | N/A — implementation reference | `plugin/studios/ARCHITECTURE.md` — boundaries, lifecycle, hat patterns, FB-as-unit fix-loop semantics. Read before any structural change to studios, stages, hats, or workflow tools | ARCHITECTURE.md |
| Workflow-managed file boundary | Quality Enforcement | PreToolUse hook denies generic Read/Write/Edit on `units/*.md`, `feedback/*.md`, `intent.md`, `stages/*/state.json`. Agents go through MCP tools only; redirect messages name the right tool | hooks/guard-workflow-fields.ts |
| Unit CRUDL (MCP) | N/A — implementation | `haiku_unit_write` (create/rewrite, FM validators, DAG cycle detection, pending-only lifecycle), `haiku_unit_read` (body+title only — no FM exposed), `haiku_unit_get` (read ONE agent-authorable/corrective FM field — `quality_gates`/`outputs`/`inputs`/`depends_on`/`model`/`closes`/`title`; refuses FSM-driven fields with `unit_field_engine_only`; the read half of the corrective exemption so a gate-command fix is read→modify-one→write-back, not a whole-array clobber), `haiku_unit_set` (FM field update, lifecycle-enforced; `outputs`/`quality_gates` stay editable after a unit goes active — corrective exemption), `haiku_unit_delete` (pending only), `haiku_unit_list` | state-tools.ts |
| Feedback CRUDL (MCP) | N/A — implementation | `haiku_feedback_write` (body update, lifecycle-enforced), `haiku_feedback_read` (body+title only), `haiku_feedback` (create), `haiku_feedback_update` (status transitions, terminal-state-protected), `haiku_feedback_reject` (mark invalid), `haiku_feedback_delete`, `haiku_feedback_list`, `haiku_feedback_set_targets` / `haiku_feedback_set_severity` (classifier-hat backfill — write-once) | state-tools.ts |
| Feedback CRUDL (MCP) | N/A — implementation | `haiku_feedback_write` (body update, lifecycle-enforced), `haiku_feedback_read` (body+title only), `haiku_feedback` (create), `haiku_feedback_reject` (mark invalid/stale — stamps `rejected_at` so the open-feedback walk treats it as terminal; the v4 `haiku_feedback_update` status-transition tool was REMOVED — closure runs through the fix-loop's terminal hat `haiku_feedback_advance_hat`, and a stale `haiku_feedback_update` call returns `feedback_update_removed_in_v4`), `haiku_feedback_delete`, `haiku_feedback_list`, `haiku_feedback_set_targets` / `haiku_feedback_set_severity` (classifier-hat backfill — write-once) | state-tools.ts |
| Feedback severity | Quality Enforcement | `severity:` on feedback frontmatter — `blocker` \| `high` \| `medium` \| `low` (`FEEDBACK_SEVERITIES` in `state/schemas/feedback.ts`). **Required** on the agent-facing `haiku_feedback` create tool (review agents classify as they file). User/SPA findings land severity-less (the `writeFeedbackFile` HTTP path omits it) and the `classifier` fix-hat backfills via `haiku_feedback_set_severity` (write-once, `severity_already_set` on re-call). The fix-loop dispatches highest-severity-first: `feedbackSeverityRank` orders the initial pool in `collectFeedbackDispatches` (cursor.ts) AND the slot-replenishment pick in `pickUndispatchedFbBlock` (state-tools.ts) — unclassified ranks as `medium`. Engine-authored FBs set explicit severity at write time (gate-blockers → `blocker`, drift → `high`). Surfaced on the wire (`FeedbackSeveritySchema`) + SPA badge (`FeedbackItem.tsx`) | state/schemas/feedback.ts (`FEEDBACK_SEVERITIES`, `feedbackSeverityRank`), state-tools.ts (`haiku_feedback_set_severity`, `pickUndispatchedFbBlock`), orchestrator/workflow/cursor.ts (`collectFeedbackDispatches`) |
| Severity-gated fix-loop activation | Quality Enforcement | A finding is **blocking** (triggers a fix wave AND holds the stage gate) iff it's unclassified (null severity — the classifier must run) OR `feedbackSeverityRank(severity) <= fixSeverityThresholdRank()`. Threshold defaults to `high` (blocker+high block; medium+low ride along), overridable via **`HAIKU_FIX_SEVERITY_THRESHOLD`** env var (read at call time). `collectFeedbackDispatches` returns null (no wave) unless some open FB is `inFlight` OR blocking — so a lone reviewer nit never spins a worktree; the stage just advances and the nit stays open + advisory. Once a blocker DOES open a wave, the whole open set dispatches together (severity-ordered), sweeping the ridealong lows in the same pass. Agent findings carry severity at creation, so a reviewer's stream of `low`s is non-blocking from tick 1 | state/schemas/feedback.ts (`fixSeverityThresholdRank`, `isFixBlockingSeverity`), orchestrator/workflow/cursor.ts (`collectFeedbackDispatches` wave-active gate) |
| Feedback anti-churn (re-review) | Quality Enforcement | Two LIVE, reword-resistant mechanisms keep reviewers from re-litigating settled findings. **Preventive:** `buildExistingFeedbackBlock` hands every review/approval/intent-review subagent ALL prior findings on scope — incl. closed/rejected — now with `severity`, the closure_reply (`resolved: …`) and reject reason (`dismissed: …`), and tells it to audit the delta / respect the dismissal, not re-file. **Detective:** `detectSettledDuplicate` (create-time, Jaccard token-overlap ≥ 0.6 on same target_unit) returns a `duplicate_warning` on the `haiku_feedback` response when a finding restates an already-closed/rejected one — advisory (a bad fix can legitimately re-raise), pointing the agent at the prior resolution + `haiku_feedback_reject`. The dispatch_approval prompt also tells the reviewer its units came back because the WORK changed (only changed units re-sent — cursor filters `!approvals[role]`), so audit the fix delta. NOTE: the old `computeFeedbackSignature` stage-iteration loop detector is dormant in v4 (`appendStageIteration` never gets `feedbackTitles`); the per-FB bolt cap remains the hard backstop | _helpers.ts (`buildExistingFeedbackBlock`), state-tools.ts (`detectSettledDuplicate`, `findingSimilarity`), prompts/stage/approve/dispatch_approval/subagent.eta.md |
Expand Down
96 changes: 96 additions & 0 deletions deploy/auth-proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# auth-proxy

Node 22 GCP Cloud Function (entry `authProxy`) live at `auth.haikumethod.ai`.
Brokers GitHub/GitLab OAuth so neither the browse site nor the CLI ever holds the
OAuth client secret. The secret lives only in Secret Manager and is read by this
function.

Two surfaces share one provider exchange (`src/providers.ts`):

| Surface | Endpoints |
| --- | --- |
| Browse site (Phase 1) | `POST /github/token`, `POST /gitlab/token` — code→token exchange |
| CLI device flow (Phase 2) | `POST /cli/start`, `POST /cli/complete`, `POST /cli/poll`, `POST /cli/refresh` |

Phase 2 (`src/cli.ts`, `src/sessions.ts`) is wired into the same `authProxy`
entry: `index.ts` calls `handleCliRoute(req, res)` first; it owns any `/cli/*`
path and falls through to the existing browse-site routes otherwise.

## CLI device flow

```
CLI auth-proxy browse site / provider
| POST /cli/start ----------->| |
| <- { session_id, | store PENDING (Firestore, |
| verification_url } | keyed by session_id + state) |
| | |
| (open verification_url) --------------------------------------> human approves
| | provider callback -----> | /{provider}/callback
| | <- POST /cli/complete | exchanges code→token
| | { state, access_token, ... } | then POSTs token here
| | flip session -> READY |
| POST /cli/poll ------------>| |
| <- { status: ready, | release ONCE, mark consumed |
| access_token, ... } | |
```

### Endpoint contracts

**`POST /cli/start` `{ provider, host? }`** → `{ session_id, verification_url, expires_in }`.
Mints a 10-minute session + state, stores a PENDING record in Firestore keyed by
`session_id`, and returns the `verification_url` the human opens. The URL targets
the browse site's CLI authorize entry (`/oauth/cli/authorize`) carrying
`provider`, `host`, `state`, and `authorize_via` (the resolved provider authorize
endpoint). The browse site sends the human through the provider, and the existing
`/{provider}/callback` completes the exchange. Host-aware for enterprise GitHub /
self-managed GitLab.

**`POST /cli/complete` `{ state, access_token, refresh_token?, expires_at?, scopes?, account?, host? }`** → `{ status: "ready" }`.
**This is the single server endpoint the browse-site callback must POST to.** After
the existing `/{provider}/callback` exchanges the authorization code for a token,
it POSTs the captured token bundle here keyed by `state`. The session flips to
`ready`. `scopes` accepts a string (space/comma separated) or an array. An
enterprise `host` echoed here overrides the one captured at start.

> If the browse callback is purely client-side and cannot exchange the code
> server-side, it should instead POST the raw `code` to `/{provider}/token`
> (Phase 1) to get the bundle, then POST that bundle to `/cli/complete`.

**`POST /cli/poll` `{ session_id }`** →
- `{ status: "pending" }` while awaiting approval,
- `{ status: "ready", access_token, refresh_token?, expires_at?, scopes?, account, provider, host }` **exactly once**,
- `{ status: "consumed" }` / `{ status: "expired" }` thereafter.

The token is released a single time; the record is marked `consumed` and deleted
so it can never be replayed.

**`POST /cli/refresh` `{ provider, host?, refresh_token }`** → a fresh token bundle.
Re-runs the provider exchange with `grant_type=refresh_token` using the held
client secret. Persists nothing. Host-aware.

## Session store

Firestore collection `cli_sessions`. Every record carries `expires_at` (epoch
seconds). Reads opportunistically delete expired records; a Firestore TTL policy
on `expires_at` (see `deploy/terraform/modules/auth-proxy/firestore.tf`) is the
backstop sweep.

## Secrets / env

| Env var | Source |
| --- | --- |
| `HAIKU_GITHUB_OAUTH_CLIENT_ID` / `HAIKU_GITHUB_OAUTH_CLIENT_SECRET` | Secret Manager (Phase 1) |
| `HAIKU_GITLAB_OAUTH_CLIENT_ID` / `HAIKU_GITLAB_OAUTH_CLIENT_SECRET` | Secret Manager (Phase 1) |
| `HAIKU_<PROVIDER>_OAUTH_CLIENT_ID__<HOST>` / ... | optional per-enterprise-host overrides |
| `ALLOWED_ORIGIN` | CORS allowlist (Phase 1); first entry is the default browse origin |
| `BROWSE_ORIGIN` | optional override for the `verification_url` origin |

## Develop / test

```sh
npm install
npm test # tsc -> dist/ then node --test test/
```

Tests inject an in-memory session store and a fake `fetch`
(`setSessionStore`, `setFetchImpl`), so no GCP credentials or network are needed.
Loading
Loading