Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions src/__tests__/cli-events.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Phase 125 Plan 02 Task 1 — CLI command-invocation event tests.
// CLI command-invocation event tests.
//
// CONTEXT.md §5: every real command emits `cli_command_invoked` on exit
// Contract: every real command emits `cli_command_invoked` on exit
// with `{ command, subcommand, exit_code, duration_ms, cli_version,
// node_version, platform }`; meta commands (`help`, `--help`, `--version`)
// are excluded; first-ever invocation emits `cli_first_run` once. Errors
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/errors-manifest.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Phase 123 Plan 10 — CLI error manifest drift test.
// CLI error manifest drift test.
//
// Asserts that every AppError subclass exported from `src/errors/index.ts`
// matches its corresponding entry in `src/errors/manifest.json` (which is
Expand Down Expand Up @@ -33,7 +33,7 @@ const manifest = manifestJson as {
// CLI-specific AppError subclasses that are intentionally NOT in the monorepo
// manifest (they exist only in the CLI surface area).
//
// `CliError` is the Phase 108 legacy base — a concrete class that extends
// `CliError` is the legacy base — a concrete class that extends
// `AppError` but has no severity-tier meaning of its own (default sev3). It
// stays in the CLI surface for `instanceof CliError` back-compat (sandbox-listen
// picker/binary, index.ts main()) and is NOT part of the monorepo taxonomy.
Expand Down Expand Up @@ -96,7 +96,7 @@ describe('CLI error manifest drift', () => {

it('the 15 canonical manifest classes are all present (sanity check on the mirrored JSON)', () => {
// This test fails if the monorepo manifest is ever sync-fetched in a
// reduced state. Keep it locked to the Phase 123 Plan 02 shape.
// reduced state. Keep it locked to this shape.
const names = Object.keys(manifest.classes).sort();
expect(names).toEqual(
[
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/flush-exit.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Phase 125 Plan 02 Task 1 — flushAndExit covers BOTH Sentry + PostHog.
// flushAndExit covers BOTH Sentry + PostHog.
//
// CONTEXT.md §125-02 must_haves: process exit awaits `posthog.shutdown(2000)`
// Contract: process exit awaits `posthog.shutdown(2000)`
// + `Sentry.flush(2000)` in parallel via Promise.allSettled — neither
// vendor's failure can block the other.
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/manifest-drift.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Phase 125 Plan 01 Task 2 — CLI-side event-manifest drift test.
// CLI-side event-manifest drift test.
//
// Guards the CLI-LOCAL side of the manifest mirror:
// 1. The JSON manifest parses with `version: '1'` + 24 unique event names.
Expand Down
8 changes: 4 additions & 4 deletions src/__tests__/posthog-alias.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
// Phase 125 Plan 02 Task 1 — PostHog alias-on-login contract tests.
// PostHog alias-on-login contract tests.
//
// CONTEXT.md §3 + RESEARCH §Pattern 3 + §Pitfall 1:
// Alias contract:
// The CLI captures pre-login events under a machine-id (`distinctId`).
// On the FIRST successful login per (machine, user) pair, we call
// `posthog.alias({ distinctId: workosSub, alias: machineId })` so PostHog
// stitches the anonymous machine-scoped events onto the user profile that
// the app + marketing already write.
//
// Direction matters (RESEARCH §Pitfall 1): `distinctId` is the canonical user
// Direction matters: `distinctId` is the canonical user
// id (workosSub); `alias` is the side identifier we want to merge in
// (machineId). Reversing the args — `{ distinctId: machineId, alias: sub }` —
// would silently merge the user profile into the machine, breaking app↔CLI
// stitching for every event from then on.
//
// Per-pair persistence (CONTEXT.md §3): `posthogAliasedUsers: [...]` in
// Per-pair persistence : `posthogAliasedUsers: [...]` in
// config.json prevents repeated alias calls on every login; a different
// workosSub on the same machine still fires alias once for that pair.
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/posthog-init.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Phase 125 Plan 02 Task 1 — PostHog lazy-init contract tests.
// PostHog lazy-init contract tests.
//
// Mirrors `sentry-init.test.ts` shape verbatim so failures land with the same
// debug ergonomics. Asserts:
Expand All @@ -8,7 +8,7 @@
// 3. `initPostHogLazy()` is a NO-OP when no token is baked (dev build).
// 4. `initPostHogLazy()` returns a real PostHog client when both telemetry
// is enabled AND a token is present, with `flushAt: 1, flushInterval: 0`
// (short-lived-process pattern from RESEARCH §Pattern 1).
// (short-lived-process pattern).
// 5. Subsequent calls reuse the same instance — no re-init.
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';

Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/sandbox-listen/version-check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('checkForNewerCli', () => {

expect(logSpy).toHaveBeenCalledOnce();
const msg = logSpy.mock.calls[0][0] as string;
// Locked prefix per CONTEXT.md §CLI Flow Step 2
// Locked prefix per the version-check contract
expect(msg.startsWith('A newer version of hookmyapp is available')).toBe(true);
expect(msg).toContain(pkg.version);
expect(msg).toContain('99.0.0');
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/sentry-init.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Phase 123 Plan 10 — Sentry lazy-init tests.
// Sentry lazy-init tests.
//
// Contract:
//
Expand All @@ -18,7 +18,7 @@
//
// 5. `shouldCaptureToSentry()` filters backend-response wrappers (any error
// with a non-undefined statusCode → false) — implements the
// single-capture-per-error rule from CONTEXT.md.
// single-capture-per-error rule.
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
initSentryLazy,
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/telemetry-consent.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Phase 123 Plan 10 — telemetry consent tests.
// telemetry consent tests.
//
// Exercises the three-layer override chain defined in
// `src/observability/telemetry.ts`:
Expand Down Expand Up @@ -217,8 +217,8 @@ afterEach(() => {
}
});

// Phase 125 Plan 02: HOOKMYAPP_TELEMETRY=off is the SINGLE kill-switch that
// covers both Sentry AND PostHog (CONTEXT.md §4 — "single CLI opt-out").
// an earlier release: HOOKMYAPP_TELEMETRY=off is the SINGLE kill-switch that
// covers both Sentry AND PostHog .
// This block lives here (not in posthog-init.test.ts) so the contract
// "isTelemetryEnabled is the one shared gate" is documented next to the gate
// itself.
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/workspace-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});

const CONFIG_PATH = path.join(TMP_HOME, '.hookmyapp', 'config.json');

// Phase 117: every workspace id fixture is a ws_ publicId.
// an earlier release: every workspace id fixture is a ws_ publicId.
// AIT-182: fixtures simulate an OLDER backend that still sends
// workosOrganizationId — the CLI must scrub it at the output boundary.
const fakeWorkspaces = [
Expand Down
12 changes: 6 additions & 6 deletions src/__tests__/workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const mockedApiClient = vi.mocked(apiClient);
const mockedOutput = vi.mocked(output);
const mockedRescopeWorkspaceToken = vi.mocked(rescopeWorkspaceToken);

// Phase 117 — every id fixture below is a publicId (ws_/ch_/ssn_/mem_/inv_ prefix, 8-char
// every id fixture below is a publicId (ws_/ch_/ssn_/mem_/inv_ prefix, 8-char
// alphanumeric body). Raw UUIDs are rejected with a typed ValidationError at every
// external flag/header/body surface; see workspace.ts resolveWorkspace + _helpers.ts.
// AIT-182 — the workspaces wire no longer carries workosOrganizationId.
Expand Down Expand Up @@ -93,8 +93,8 @@ describe('resolveWorkspace', () => {
await expect(resolveWorkspace('Nonexistent')).rejects.toThrow('not found');
});

// Phase 117 hard cutover — raw UUID input is not an accepted shape.
it('throws typed CliError when given a raw UUID (Phase 117 hard cutover)', async () => {
// an earlier release hard cutover — raw UUID input is not an accepted shape.
it('throws typed CliError when given a raw UUID (an earlier release hard cutover)', async () => {
mockedApiClient.mockResolvedValue(fakeWorkspaces);
const { CliError } = await import('../output/error.js');
await expect(resolveWorkspace('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa')).rejects.toThrow(CliError);
Expand Down Expand Up @@ -172,7 +172,7 @@ describe('writeWorkspaceConfig config-file merge', () => {
expect(ws.activeWorkspaceSlug).toBe('Another Workspace');
});

// Phase 117 — readWorkspaceConfig silently drops a stale UUID activeWorkspaceId
// readWorkspaceConfig silently drops a stale UUID activeWorkspaceId
// (pre-0.5.0 install artifact). Prevents the UUID from leaking back out to the
// backend which now 400s on every UUID input.
it('silently drops stale UUID activeWorkspaceId on read (pre-0.5.0 config)', async () => {
Expand Down Expand Up @@ -529,7 +529,7 @@ describe('workspace commands', () => {
}
});

// Phase 117 hard cutover — negative regression.
// an earlier release hard cutover — negative regression.
it('workspace use rejects raw UUID with typed ValidationError', async () => {
mockedApiClient.mockResolvedValue(fakeWorkspaces);

Expand Down Expand Up @@ -596,7 +596,7 @@ describe('resolveInviteByIdOrEmail', () => {
expect(invite).toEqual(fakeMembersResponse.invites[0]);
});

it('finds invite by inv_ publicId (Phase 117 — not raw UUID)', async () => {
it('finds invite by inv_ publicId (an earlier release — not raw UUID)', async () => {
const responseWithPublicIdInvite = {
members: [],
invites: [
Expand Down
2 changes: 1 addition & 1 deletion src/analytics/events.manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": "1",
"generatedAt": "2026-04-30T00:00:00Z",
"description": "CLI-side typed PostHog event registry (scoped subset). Declares only events the CLI binary actually emits — the 8 cli_* events. The cross-repo drift test in /Users/ordvir/COD/hookmyapp/packages/observability/src/analytics/__tests__/manifest-drift.spec.ts asserts every event here also appears in the monorepo's events.manifest.json with the same property set. Baseline properties (site, environment, workspace_id, days_since_signup) are implicit on every event and injected by super-properties.ts — not listed per-event below.",
"description": "CLI-side typed analytics event registry (scoped subset). Declares only events the CLI binary actually emits — the 8 cli_* events. A drift test asserts every event here also appears in the upstream registry with the same property set. Baseline properties (site, environment, workspace_id, days_since_signup) are implicit on every event and injected by super-properties.ts — not listed per-event below.",
"events": [
{
"name": "cli_first_run",
Expand Down
2 changes: 1 addition & 1 deletion src/analytics/events.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// CLI-side typed event registry — scoped subset of the monorepo's full
// registry at packages/observability/src/analytics/events.ts.
//
// History: from Phase 125 through Phase 130 this file was a byte-for-byte
// History: from an earlier release through an earlier release this file was a byte-for-byte
// mirror of the monorepo registry. That coupled CLI to every monorepo event
// addition (web, web-api, demo, marketing) even though CLI emits only the
// 7 `cli_*` events. With monorepo's surface growing (frontend + backend +
Expand Down
14 changes: 7 additions & 7 deletions src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,8 @@ export async function rescopeWorkspaceToken(workspaceId: string): Promise<void>
// consistent across commands. Keep this in sync with the error-hierarchy
// contract in output/error.ts (exit codes: 2 / 3 / 4 / 5 / 6).
//
// Return type is `CliError` — a Phase 108 alias that is identical at runtime
// to `AppError` under Phase 123 Plan 10. Existing callers see no change.
// Return type is `CliError` — a an earlier release alias that is identical at runtime
// to `AppError` under an earlier release. Existing callers see no change.
export async function mapApiError(res: Response): Promise<CliError> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body: any = await res.json().catch(() => ({ message: res.statusText }));
Expand Down Expand Up @@ -227,7 +227,7 @@ export async function mapApiError(res: Response): Promise<CliError> {
return new PermissionError(cfg.activeWorkspaceSlug ?? '<unknown>');
}
if (res.status === 409) return new ConflictError(msg, code ?? 'CONFLICT');
// Phase 122 — bootstrap-code exchange error mapping.
// bootstrap-code exchange error mapping.
// 404 and 410 collapse to the same user-facing message (oracle-attack
// defense — brute-force guessers can't distinguish "unknown code" from
// "already spent code"). ApiError.exitCode defaults to 1; we override
Expand Down Expand Up @@ -263,8 +263,8 @@ export async function mapApiError(res: Response): Promise<CliError> {
);
}
if (res.status === 429) {
// Phase 123 Plan 10 — use the new RateLimitError class (sev3, httpStatus
// 429). Exit code remains 6 so the Phase 108 exit-code contract for 429
// use the new RateLimitError class (sev3, httpStatus
// 429). Exit code remains 6 so the an earlier release exit-code contract for 429
// is preserved (historically this flowed through ConflictError → exit 6).
// The body.code 'RATE_LIMITED' matches the backend's
// UserIdThrottlerGuard structured 429 body.
Expand Down Expand Up @@ -319,7 +319,7 @@ export async function apiClient(
throw new AuthError('Not logged in. Run: hookmyapp login');
}

// Phase 123 Plan 10 — set Sentry user tag on every authenticated API call.
// set Sentry user tag on every authenticated API call.
// Idempotent + lazy (no-op when telemetry off or Sentry not initialized).
// Fire-and-forget so the API call's latency isn't gated on dynamic import.
void (async () => {
Expand Down Expand Up @@ -401,7 +401,7 @@ export async function apiClient(
}
}

// --- Phase 126 sandbox bind-code contract ---
// --- sandbox bind-code contract ---
//
// Mirrors backend/src/sandbox/bind-code.controller.ts (Plan 03 Wave 2 locked
// contract: `GET /sandbox/bind-code` returns the caller's available bind code
Expand Down
2 changes: 1 addition & 1 deletion src/api/version-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const OS = process.platform;
* Stainless-style headers used by the OpenAI and Anthropic SDKs
* (`x-stainless-package-version` + `x-stainless-lang/runtime/arch/os`).
*
* Contract per docs/superpowers/specs/2026-05-06-cli-and-skill-version-enforcement-design.md:
* The contract:
* - User-Agent: hookmyapp-cli/<version> (node/<runtime>; <arch>; <os>)
* - X-HookMyApp-CLI-Version: <semver> (always)
* - X-HookMyApp-Lang: node (always)
Expand Down
6 changes: 3 additions & 3 deletions src/auth/__tests__/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ describe('post-login wizard', () => {
logSpy.mockRestore();
});

it('--next sandbox --phone matches existing → delegates to listen (Phase 126 bind-code model)', async () => {
it('--next sandbox --phone matches existing → delegates to listen', async () => {
apiClientMock
// workspaces fetch
.mockResolvedValueOnce([
Expand All @@ -176,7 +176,7 @@ describe('post-login wizard', () => {
},
])
// sandbox sessions fetch — the session already exists (bound previously
// via `hookmyapp sandbox start`). Phase 126 no longer POSTs to
// via `hookmyapp sandbox start`). The current flow no longer POSTs to
// /sandbox/sessions; binding is inbound-message driven.
.mockResolvedValueOnce([
{
Expand Down Expand Up @@ -209,7 +209,7 @@ describe('post-login wizard', () => {
const [path, init] = call;
if (path === '/sandbox/sessions' && init?.method === 'POST') {
throw new Error(
'Regression: wizard POSTed to /sandbox/sessions which was deleted in Phase 126 Plan 03',
'Regression: wizard POSTed to /sandbox/sessions which was deleted',
);
}
}
Expand Down
16 changes: 8 additions & 8 deletions src/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { posthogAliasAndIdentify } from '../observability/posthog.js';
import { parseSandboxSessions, type WhatsAppSandboxSession } from '../api/sandbox-session.js';
import { maybeInstallClaudeMcp } from '../commands/mcp.js';

// --- Phase 122 bootstrap-code exchange DTO ---
// --- bootstrap-code exchange DTO ---
// Mirrors backend/src/auth/bootstrap/dto/exchange-bootstrap.dto.ts (Wave 1
// locked contract). The CLI does not import from the backend — the DTO is
// re-declared here verbatim so drift is caught by integration tests.
Expand Down Expand Up @@ -80,7 +80,7 @@ async function pollForTokens(opts: {
refreshToken: data.refresh_token,
expiresAt: Math.floor(Date.now() / 1000) + 900,
});
// Phase 125 — alias machineId → workosSub once per (machine, user) and
// alias machineId → workosSub once per (machine, user) and
// emit cli_logged_in. Fail-open: a posthog hiccup must never block the
// login UX. Pass email + name so the PostHog Person profile shows the
// human identity for CLI events.
Expand Down Expand Up @@ -238,7 +238,7 @@ export async function runWizard(opts: WizardOpts = {}): Promise<void> {
* Sandbox sub-flow. Invoked by the wizard when the user passes
* `hookmyapp login --next sandbox` (or `--phone <e164>` without `--next`).
*
* Phase 126 bind-code model — session creation is phone-initiated (user
* The bind-code model — session creation is phone-initiated (user
* sends a bind code from their WhatsApp into the env's sandbox number —
* staging IL or prod US, per env-profiles.ts), NOT CLI-flag-initiated.
* The wizard therefore no longer offers a
Expand All @@ -248,7 +248,7 @@ export async function runWizard(opts: WizardOpts = {}): Promise<void> {
* 0 sessions → runSandboxStart (prints bind code + QR + polls; on poll success,
* runSandboxListenFlow starts automatically because `--listen` is true).
* 1 session → direct listen (matches the "single active session" fast-path
* from before Phase 126 — preserved so repeated logins don't
* from an older CLI — preserved so repeated logins don't
* force the user through bind flow again).
* N sessions → picker (no "+ Create new" — bind flow handles that; the
* picker is purely for choosing which ALREADY-bound phone to
Expand Down Expand Up @@ -286,7 +286,7 @@ export async function runSandboxFlow(
);

// --phone is authoritative: match against existing active sessions only.
// Phase 126 — we do NOT POST /sandbox/sessions here (endpoint deleted);
// we do NOT POST /sandbox/sessions here (endpoint deleted);
// binding is phone-initiated via an inbound WhatsApp message matching
// the user's bind code, which the dedicated `sandbox start` command
// drives.
Expand Down Expand Up @@ -375,7 +375,7 @@ async function runChannelsConnectFlow(): Promise<void> {
}

/**
* Phase 122: bootstrap-code exchange branch. Invoked when the user (or their
* Bootstrap-code exchange branch. Invoked when the user (or their
* AI) runs `hookmyapp login --code hma_boot_<32>`. Bypasses the WorkOS device
* flow entirely — zero browser interaction, zero polling — then re-enters
* runWizard so the rest of the CLI (active workspace, --phone/--next hooks,
Expand Down Expand Up @@ -448,7 +448,7 @@ export async function runBootstrapCodeExchange(
});
maybeInstallClaudeMcp();

// Phase 125 — alias machineId → workosSub once per (machine, user) and
// alias machineId → workosSub once per (machine, user) and
// emit cli_logged_in. workspace publicId is already on disk above so
// baseline workspace_id resolves. Fail-open: posthog hiccup ≠ blocked login.
// Pass email so the PostHog Person profile shows the human identity for CLI
Expand Down Expand Up @@ -741,7 +741,7 @@ export function loginCommand(program: Command): void {
return;
}

// Phase 122 — bootstrap-code branch. MUST run BEFORE the wizard
// bootstrap-code branch. MUST run BEFORE the wizard
// fast-path and BEFORE device-flow initiation so --code --wizard is
// flagged as a programming error (mutually exclusive).
if (opts.code) {
Expand Down
Loading
Loading