Skip to content

Worktree-first development + agent experience overhaul#54

Merged
roodboi merged 25 commits into
mainfrom
feat/worktree-ax-overhaul
Jul 7, 2026
Merged

Worktree-first development + agent experience overhaul#54
roodboi merged 25 commits into
mainfrom
feat/worktree-ax-overhaul

Conversation

@roodboi

@roodboi roodboi commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What this is

A full pass on making hack excellent for worktree-based, agent-driven development: worktree workflows that just work, a single drift-enforced source for agent instructions, a machine-first CLI surface, agent-assisted onboarding, tickets phase-out, a real e2e harness, and a docs overhaul with generated reference.

Highlights

Linked worktrees are first-class

  • Secret keys never silently diverge: shared git-common-dir location, primary-key adoption, loud fallback warnings, HACK_ENV_SECRET_KEY for CI
  • hack up/down/restart/ps/logs/open/run/exec in a linked worktree default to a branch instance named for the worktree branch (worktree.auto_branch=false or explicit --branch to opt out)
  • hack doctor flags divergent keys, cross-checkout dev_host collisions, and generated files tracked in git (doctor --fix untracks)
  • Registry dedupes by repo family and records sibling checkouts (hack projects --details)

Machine-first CLI (AX)

  • --json on up/down/restart/doctor with a {ok, data, error: {code, message}} envelope and typed E_* codes
  • Global --no-interactive / HACK_NO_INTERACTIVE; safe defaults or fail-fast E_INTERACTIVE_REQUIRED
  • Daemon autostart no longer hangs non-TTY callers (>120s → bounded, detached, docker fallback)
  • NO_COLOR honored; no ANSI leaks to piped stdout
  • Experimental commands (node/dispatch/gateway/remote) hidden behind hack help --all + warning banner

One source of truth for agent instructions

  • src/agents/instruction-source.ts renders AGENTS.md/CLAUDE.md snippets, Codex skill, Cursor rules, and the primer
  • setup sync --check now detects stale content, not just marker presence; drift tests validate every documented command against CLI_SPEC

Agent-assisted onboarding

  • hack init --with claude|codex|both, hack agent onboard, /hack-init skills, and an MCP prompt — all from one prompt module (inventory → compose+caddy setup → deps-container/ops-container patterns → running-things guide → verification loop)

Housekeeping

  • Committed self-healing .hack/.gitignore ends generated-file leaks into git history
  • Tickets integration is opt-in (extension must be enabled); removed from default instructions
  • HACK_HOME override for full global-state isolation; hack projects prune; unit suite made order-independent (642/41 → 783/0); deterministic CI runner deleted
  • Docs: audit-driven accuracy pass on 27 files + generated docs/reference/cli.md with a drift test + non-negotiable docs-currency rule

Verification

  • Unit: 783 pass / 0 fail (order-independent, shuffled-order verified)
  • Typecheck, ultracite, build (compiled binary): clean
  • E2E (new harness, tests/e2e/): 11/11 including docker tier — real up/down, lifecycle singleton adoption, and primary+worktree running in parallel as separate branch instances — against the dev wrapper installed globally
  • Live smoke on this repo: doctor --json envelope, 0 errors; projects --json non-TTY 0.37s
  • hack setup sync --all-scopes --check: clean

Follow-ups (non-blocking)

  • docs/guides/codex-managed-environments.md minor-stale refresh (task chip filed)
  • Doctor tickets git check should skip when the extension is disabled
  • Reserved error codes (E_CONFIG_PARSE, E_SERVICE_NOT_FOUND, …) wired as --json spreads to more commands

🤖 Generated with Claude Code

roodboi and others added 4 commits July 7, 2026 10:50
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All agent instruction surfaces (AGENTS.md/CLAUDE.md snippet, Codex skill,
Cursor rules, primer) now render from src/agents/instruction-source.ts.
setup sync --check detects stale marker content, not just marker presence.
Tickets removed from default instructions; linked-worktree guidance added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Secret keys never silently diverge (shared-location writes, primary-key
adoption, loud fallback warnings). hack up/down/restart/ps/logs/open
default to a branch instance named for the worktree branch (opt out:
worktree.auto_branch=false or explicit --branch). hack doctor gains
divergent-key and cross-checkout instance checks. Registry tracks
sibling worktree checkouts, shown in hack projects --details/--json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Isolated (HACK_HOME) end-to-end suite driving the working-tree CLI:
tier 1 (init, env secrets, worktree key sharing, registry dedupe,
branch defaulting, docs sync drift, doctor, non-TTY hygiene) and an
opt-in docker tier (up/down, lifecycle hooks, parallel primary+worktree
branch instances). bun run test:e2e:local / test:e2e:local:docker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9873feb6eb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/commands/doctor.ts
return;
}

await ensureHackDirGitignore({ projectDir: project.projectDir });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the root ignore when untracking the env key

When hack doctor --fix finds .hack.secret.key tracked, this fix only untracks it and ensures the nested .hack/.gitignore; that file cannot ignore the root-level secret key. The git rm --cached leaves the leaked key on disk as an unignored ?? .hack.secret.key, so after committing the removal it can be accidentally re-added. The repair should also add the root .gitignore entry for .hack.secret.key before reporting the generated-file fix complete.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d6456a1: doctor --fix now ensures the .hack.secret.key entry in the root .gitignore when untracking a leaked key (the nested .hack/.gitignore can't cover repo-root files), with a regression test asserting git check-ignore passes and the key no longer appears as untracked.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e9e9ae0fbf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/cli/package.json
"macos:ghostty:bundle": "cd ../.. && bun run scripts/macos-ghostty-bundle.ts",
"typecheck": "bunx tsc -p tsconfig.json --noEmit",
"test": "cd ../.. && bun run scripts/run-tests-deterministic.ts",
"test": "cd ../.. && bun test",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore deterministic test isolation

Changing the package test script to raw bun test makes the suite run shared-process tests concurrently; bun test --help reports --max-concurrency defaults to 20, while many existing tests mutate process.env, PATH, HOME, and stdout/stderr capture globally. I reproduced this by running bun test tests/cli-result.test.ts tests/lifecycle-json.test.ts tests/interactivity.test.ts tests/hack-gitignore.test.ts tests/run-exec-branch-default.test.ts tests/doctor-generated-files.test.ts tests/projects-registry-worktrees.test.ts, which fails the linked-worktree hack run tests because their stubbed PATH/HOME are raced by other files. The deleted deterministic runner existed to isolate exactly this kind of cross-file bleed, so bun run test/CI is now flaky or failing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked and refuting this one: --max-concurrency (default 20) only applies to test.concurrent() tests — plain bun test runs files and tests serially in a single process (bun --help: '--concurrent: Treat all tests as test.concurrent() tests'; the repo uses no test.concurrent anywhere). The serial single-process model is exactly why file-scope mock.module used to poison the whole run. The deterministic runner existed to isolate those mock.module files into separate processes; that root cause was removed (scoped dispatching mocks in tests/helpers/scoped-module-mock.ts), and the full suite now passes repeatedly under shuffled file order (783+/0). Env-mutation hygiene is enforced by order-independence, not process isolation.

Comment thread src/cli/run.ts
Comment on lines +113 to +115
if (jsonRequested) {
emitCliResult({ result: errorResultFromUnknown({ error }) });
return 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve JSON envelopes for usage errors

This JSON fallback is bypassed for CliUsageError because the earlier catch branch prints the styled error/help and returns first. For example, hack up --json --definitely-bad writes non-JSON help text to stdout, even though the CLI reference says commands with --json emit a {ok, data, error} envelope on stdout. Automation that parses JSON from up/down/restart/doctor --json will break on option typos or validation errors, so usage errors need to be rendered through the JSON path when --json was requested.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7da751e: usage errors under --json now emit {ok:false, error:{code:"E_USAGE", message}} on stdout (help text skipped, message still on stderr), with a regression test asserting pure-JSON stdout for hack up --json --definitely-not-a-flag.

roodboi and others added 18 commits July 7, 2026 15:28
resolveGlobalHackDir() in config-paths is now the single seam for
~/.hack resolution (HACK_HOME env > HOME > homedir), migrated across
26 files: registries, daemon paths, local CA/PKI, secret store,
bundled binaries, caddy/cloudflared state, logs. Non-hack home paths
(ssh, tmux, ~/.claude, ~/.cursor, launchd plist) intentionally
untouched. Unset HACK_HOME is byte-identical to prior behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… docs phase)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
File-scope mock.module poisons every file in a bun test run (module
scopes all evaluate before any test executes). Replaced all 10 uses
with a scoped dispatching mock helper (activate/deactivate per file),
fixed isTTY stub restoration leaks, and reset the gum path cache
between tests. Full suite: 642 pass/41 fail -> 683 pass/0 fail,
stable under shuffled file order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ting

--json on up/down/restart/doctor with {ok,data,error:{code,message}}
envelope and typed HackErrorCode union. Global --no-interactive flag +
HACK_NO_INTERACTIVE with fail-fast E_INTERACTIVE_REQUIRED and safe
defaults. Daemon autostart no longer hangs non-TTY callers (detached
spawn, bounded wait, docker fallback). NO_COLOR honored across gum/
logger/help; doctor spinner TTY-only. Experimental (Beta) commands
hidden behind 'hack help --all' with stderr banner. hack run/exec get
linked-worktree branch defaulting. Registry records worktree siblings
on the projects/status touch path. Instruction source gains the
'Running things' decision guide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AGENTS.md/CLAUDE.md marker blocks re-synced from the instruction
source (worktrees + running-things guides, no tickets). Tickets
extension disabled in project config; tickets doc blocks and project
codex skill removed. Default-scope phase-out of the tickets
integration lands separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erated files

Machine-local generated files (.internal/, .branch/, .env,
.env.state.json, local env overlays) are ignored via a committed,
self-healing .hack/.gitignore that every clone and worktree inherits.
Env-local ignores no longer write to machine-local .git/info/exclude.
hack doctor flags generated files tracked in git (tracked secret key
escalates with rotation advice) and doctor --fix untracks them. E2E
scenarios updated: nested gitignore assertion, worktree registry
recording now a hard assertion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tickets integrations (skill install/check, doc upsert) now apply only
when the tickets extension is enabled for the scope; hack setup
tickets notes optional/legacy status. hack projects prune removes
registry entries with missing repo roots and works with docker down;
--json emits the standard envelope. findExecutableInPath honors
runtime PATH. Deterministic test runner deleted (plain bun test is
order-independent now). Planet animation data lazily imported out of
the startup module graph. Stale github/linear extension entries
disabled in this repo's config.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hack init --with claude|codex|both hands a canonical onboarding prompt
to the agent CLI (or prints a copy-paste block when unavailable or
non-interactive). hack agent onboard emits it for existing projects.
Thin /hack-init skills install with hack setup claude|codex; MCP
serves a hack-init prompt. Content is one module: inventory, compose +
caddy setup, deps-container and ops-container patterns, running-things
guide (referenced from the instruction source), verification loop.
docs/guides/agent-first-setup.md carries the bootstrap block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs/reference/cli.md renders from CLI_SPEC (bun run
docs:cli-reference); a test fails whenever the committed reference no
longer matches the spec, so flag/command docs cannot rot. Experimental
commands listed but not expanded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Interface/behavior changes must update docs in the same patch;
CLI reference regenerates via bun run docs:cli-reference; agent-facing
phrasing changes go through instruction-source + setup sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Audit-driven rewrite of 27 files: docs-ia and cli.md rebuilt (cli.md
defers exhaustive flags to the generated reference), 7 architecture
stale claims fixed, env/sessions/lifecycle corrected (secret-key
inheritance order, tmux-only daemon sessions, envelope error codes),
experimental banner standardized across all remote/gateway docs,
tickets reframed as opt-in. Every relative link and documented
command verified against the generated CLI reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lead with the concrete promise (every project at its own local HTTPS
URL, one command to run any repo), five what-you-get pillars, tight
quick start and everyday commands. Deep material moves to docs/;
tickets removed from the README entirely (opt-in extension, covered
in docs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comparison table: the duct-tape stack hack replaces (domains+TLS,
secrets, run scripts, tunnels, persisted logs, parallel branches,
workspaces) and why the pieces composing around one project model is
the point.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Portless framing woven in ('the URL is just the front door'), the
one-project-model claim as the table capstone, and a new 'Your
environment, anywhere' section: committed + isolated environments make
remote coding sandboxes (Codex, Claude Code, Cursor, custom) zero-work
— install hack, pass HACK_ENV_SECRET_KEY, hack up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Audit fixes: tickets opt-in (setup enables it; removed from the intro
promise), smoke-script mechanics corrected (temp fixture + CI-built
image), installer described accurately (two-stage, pinning knobs,
HACK_ASSETS_DIR, image env defaults), remote-node pointer marked
unsupported experimental. Added --no-interactive/NO_COLOR/--json
guidance for container scripts, worktree key-inheritance cross-link,
full slim-gated global list, and bun version read from .tool-versions
instead of a hardcoded pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t key

Bun.spawn without an explicit env resolves argv[0] against the
startup PATH snapshot — CI (no real docker) failed where dev machines
masked it. buildSpawnEnv now always passes the live process env.
That exposed a real test leak: remote-caddy-routes restored HOME from
an unset per-test capture, unsetting HOME for the rest of the run.
doctor --fix now also ensures the root .gitignore entry when
untracking a leaked .hack.secret.key (nested .hack/.gitignore cannot
cover a repo-root file) — from PR review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Headline no longer leads with the URL (which invited the port-tool
pigeonhole); the isolation frame moves into the intro.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@roodboi roodboi force-pushed the feat/worktree-ax-overhaul branch from 8760e9f to d82ae58 Compare July 7, 2026 19:28
roodboi and others added 3 commits July 7, 2026 15:32
CliUsageError returned before the jsonRequested check, printing styled
help to stdout. Usage errors now emit {ok:false, error:{code:E_USAGE}}
with the message on stderr; error handling extracted to a helper.
From PR review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Logger backends write to stdout on hosts without gum, corrupting the
parseable envelope (caught by CI, invisible on dev machines). The
envelope already carries the message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed08dc0d79

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/commands/project.ts
const detach = args.options.detach;
// `--json` implies detach: machine-readable output requires the command to
// return instead of streaming compose logs on stdout.
const detach = args.options.detach || json;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep compose output out of JSON stdout

When hack up --json reaches this new detached path, it still calls composeRuntimeBackend.up, and the detached backend delegates to run() whose stdout is inherited (src/backends/runtime-backend.ts -> src/lib/shell.ts). Docker Compose's normal progress/status lines are therefore written to stdout before emitLifecycleResult prints the envelope; down --json and restart --json take the same inherited-output paths. Any automation expecting stdout to be a single JSON object will fail even on successful lifecycle commands.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in PR #59 (or as noted): src/lib/shell.ts run() gains fd-2 stdout routing, threaded through composeRuntimeBackend and all four lifecycle call sites so up/down/restart --json keep stdout as a single parseable envelope. Plumbing pinned by tests in tests/runtime-backend.test.ts.

Comment thread src/lib/branches.ts
return { branch: null, source: "none", gitBranch: null };
}

const slug = sanitizeBranchSlug(gitBranch);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid colliding auto branch slugs

When two linked worktrees are on branches such as feature/api and feature-api, both sanitize to feature-api here, so hack up/run/exec defaults them to the same compose project and branch host instead of separate branch instances. That breaks parallel worktree usage and can target or stop the other checkout's containers; the automatic default needs a collision-resistant suffix/detection path, or it should require an explicit unique --branch when the sanitized slug collides.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in PR #59 (or as noted): resolveEffectiveBranch now detects sanitization collisions against sibling worktrees' branches and deterministically suffixes both sides with a 4-hex hash of the raw branch name (feature/api -> feature-api-XXXX); non-colliding slugs unchanged, stable across invocations. Tested with a real two-worktree fixture.

@roodboi roodboi merged commit 1fc0715 into main Jul 7, 2026
4 checks passed
@roodboi roodboi deleted the feat/worktree-ax-overhaul branch July 7, 2026 19:57
roodboi pushed a commit that referenced this pull request Jul 7, 2026
## 3.1.0 (2026-07-07)

* Merge pull request #52 from hack-dance/blacksmith-migration-6707f0f ([6774244](6774244)), closes [#52](#52)
* Merge pull request #54 from hack-dance/feat/worktree-ax-overhaul ([1fc0715](1fc0715)), closes [#54](#54)
* Migrate workflows to Blacksmith ([fa30afb](fa30afb))
* Rewrite README for the v3 local-first product ([7da1cc2](7da1cc2))
* fix: spawn resolves runtime PATH; doctor --fix root-ignores the secret key ([fd7eb27](fd7eb27))
* fix(cli): usage errors emit the JSON envelope under --json ([7da751e](7da751e))
* fix(cli): usage-error JSON envelope emits without logger output ([ed08dc0](ed08dc0))
* docs: carry the positioning line into the docs index ([99a4b96](99a4b96))
* docs: extend overhaul plan (run/exec defaulting, onboarding surfaces, docs phase) ([edf7de9](edf7de9))
* docs: full accuracy pass against the v3 surface ([93ae8c0](93ae8c0))
* docs: non-negotiable docs-currency rule across instruction surfaces ([e9e013e](e9e013e))
* docs: plan for worktree DX + agent experience overhaul ([d32e743](d32e743))
* docs(cli): generated command reference with drift enforcement ([4709d89](4709d89))
* docs(env): deprecation marker on legacy v2 env migrator ([aa1d305](aa1d305))
* docs(guides): refresh managed-environments guide for the v3 surface ([47ca25a](47ca25a))
* docs(readme): differentiate the environment, not the port ([3c3f502](3c3f502))
* docs(readme): lead with the whole-environment claim ([d82ae58](d82ae58))
* docs(readme): rewrite around the core value proposition ([a83c0fb](a83c0fb))
* docs(readme): sharpen differentiation and add the portability story ([99e08fd](99e08fd))
* feat(agents): agent-assisted onboarding across CLI, skills, and MCP ([644f818](644f818))
* feat(agents): single instruction source with drift detection ([3660c27](3660c27))
* feat(cli): machine-first surface — JSON envelope, non-interactive, gating ([346b2c8](346b2c8))
* feat(core): add HACK_HOME override for the global hack directory ([58a023c](58a023c))
* feat(project): committed .hack/.gitignore + doctor untracking for generated files ([b575b78](b575b78))
* feat(worktree): first-class linked-worktree development ([abc695d](abc695d))
* chore: refresh agent docs and de-register tickets for this repo ([9c150b6](9c150b6))
* chore: tickets opt-in by default, registry prune, CI + perf cleanup ([1dd2b2f](1dd2b2f))
* test: make suite order-independent via scoped module mocks ([b36a312](b36a312))
* test(e2e): real CLI harness with turborepo fixture + worktree scenarios ([83f3e17](83f3e17))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant