Skip to content

Commit 644f818

Browse files
roodboiclaude
andcommitted
feat(agents): agent-assisted onboarding across CLI, skills, and MCP
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>
1 parent 1dd2b2f commit 644f818

18 files changed

Lines changed: 1934 additions & 51 deletions

docs/guides/agent-first-setup.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Agent-first setup
2+
3+
Let a coding agent (Claude Code, Codex) stand a project up under hack — or adopt an
4+
existing setup — instead of doing it by hand. One canonical onboarding prompt drives
5+
every entry point below; the content lives in the CLI, so it is always current for
6+
your installed version.
7+
8+
## Entry points (same prompt, four ways in)
9+
10+
| You have | Use |
11+
| --- | --- |
12+
| A fresh repo + an agent CLI installed | `hack init --with claude\|codex\|both` |
13+
| An existing hack project | `hack agent onboard` (prints the prompt) |
14+
| An agent session with the hack skills installed | `/hack-init` |
15+
| A no-shell MCP client | the `hack-init` MCP prompt (`hack setup mcp`) |
16+
17+
## New repo: `hack init --with`
18+
19+
```bash
20+
cd /path/to/repo
21+
hack init --auto --with claude # or codex, or both
22+
```
23+
24+
- Runs the normal init, then launches the agent CLI interactively with the full
25+
onboarding prompt (`claude "<prompt>"` / `codex "<prompt>"`).
26+
- If the agent CLI is not on PATH, the prompt is printed with copy-paste
27+
instructions instead.
28+
- If `.hack/` already exists, init is skipped and the handoff proceeds in
29+
existing-project mode.
30+
- With `--no-interactive` (or `HACK_NO_INTERACTIVE=1`, or no TTY) nothing is
31+
spawned — the prompt is always printed.
32+
33+
## Existing project: `hack agent onboard`
34+
35+
```bash
36+
hack agent onboard # prints the onboarding prompt for this project
37+
hack agent onboard | pbcopy # copy it for any agent session
38+
```
39+
40+
The prompt picks up the project name and dev host from `.hack/hack.config.json`
41+
when present.
42+
43+
## Agent-side skill: `/hack-init`
44+
45+
`hack setup claude` and `hack setup codex` install a thin `hack-init` skill
46+
(`.claude/skills/hack-init/SKILL.md` / `.codex/skills/hack-init/SKILL.md`).
47+
The skill tells the agent to run `hack agent onboard` (or fetch the `hack-init`
48+
MCP prompt) and follow it — the content stays in the CLI, so installed skills
49+
never go stale on substance.
50+
51+
## What the prompt covers
52+
53+
1. Inventory — package manager, workspaces, services and ports, databases/queues,
54+
`.env*` files (they become `hack env` candidates), scripts that need env.
55+
2. Setup — `hack init --auto` or config edits, compose services with Caddy labels
56+
on the `hack-dev` network, dev_host/subdomain design, `hack env add`
57+
(`--secret` for secrets; `.env` files get replaced by hack env +
58+
`hack host exec`).
59+
3. Platform nuances — deps container pattern (named `node_modules` volume so
60+
macOS-host installs never poison linux containers) and an ops/tooling
61+
container for migrations and one-off jobs.
62+
4. Running things — the `hack run` / `hack exec` / `hack host exec` /
63+
Caddy-hostname decision guide.
64+
5. Verification loop — `hack up --json``hack ps --json``hack open --json`
65+
→ curl or `hack logs`, iterating until `hack doctor` is clean.
66+
67+
## Copy-paste bootstrap
68+
69+
Paste this into a fresh Claude Code / Codex session started at the repo root:
70+
71+
```text
72+
Set this repository up to run under the hack CLI. Run `hack agent onboard`
73+
(pass --no-interactive to hack commands) and follow the printed onboarding
74+
prompt exactly, phase by phase. If the hack CLI is missing, stop and tell me.
75+
Finish only when `hack doctor` is clean and every routable service responds.
76+
```

src/agents/claude.ts

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,23 @@ import {
77
writeTextFileIfChanged,
88
} from "../lib/fs.ts";
99
import { isRecord } from "../lib/guards.ts";
10+
import {
11+
checkHackInitSkill,
12+
installHackInitSkill,
13+
removeHackInitSkill,
14+
} from "./hack-init-skill.ts";
1015

1116
export type ClaudeScope = "project" | "user";
1217

1318
export type ClaudeHookResult = {
1419
readonly scope: ClaudeScope;
15-
readonly status: "updated" | "noop" | "removed" | "missing" | "error";
20+
readonly status:
21+
| "updated"
22+
| "noop"
23+
| "stale"
24+
| "removed"
25+
| "missing"
26+
| "error";
1627
readonly path: string;
1728
readonly message?: string;
1829
};
@@ -21,7 +32,11 @@ const HOOK_COMMAND = "hack agent prime";
2132
const HOOK_EVENTS = ["SessionStart", "PreCompact"] as const;
2233

2334
/**
24-
* Install Claude Code hooks to run the hack agent primer.
35+
* Install the Claude Code integration: primer hooks in settings plus the
36+
* `/hack-init` onboarding skill (`.claude/skills/hack-init/SKILL.md`).
37+
*
38+
* The returned path points at the settings file; the status reflects both
39+
* artifacts (any change reports as updated).
2540
*/
2641
export async function installClaudeHooks(opts: {
2742
readonly scope: ClaudeScope;
@@ -57,9 +72,25 @@ export async function installClaudeHooks(opts: {
5772
const nextText = `${JSON.stringify(settings.value, null, 2)}\n`;
5873
const result = await writeTextFileIfChanged(path, nextText);
5974

75+
const initResult = await installHackInitSkill({
76+
client: "claude",
77+
scope: opts.scope,
78+
projectRoot: opts.projectRoot,
79+
});
80+
if (initResult.status === "error") {
81+
return {
82+
scope: opts.scope,
83+
status: "error",
84+
path: initResult.path,
85+
message: initResult.message,
86+
};
87+
}
88+
const initChanged =
89+
initResult.status === "created" || initResult.status === "updated";
90+
6091
return {
6192
scope: opts.scope,
62-
status: updated || result.changed ? "updated" : "noop",
93+
status: updated || result.changed || initChanged ? "updated" : "noop",
6394
path,
6495
};
6596
}
@@ -97,7 +128,27 @@ export async function checkClaudeHooks(opts: {
97128
}
98129

99130
const hasAll = hasHooks({ settings: settings.value, command: HOOK_COMMAND });
100-
return { scope: opts.scope, status: hasAll ? "noop" : "missing", path };
131+
if (!hasAll) {
132+
return { scope: opts.scope, status: "missing", path };
133+
}
134+
135+
const initResult = await checkHackInitSkill({
136+
client: "claude",
137+
scope: opts.scope,
138+
projectRoot: opts.projectRoot,
139+
});
140+
if (initResult.status !== "noop") {
141+
return {
142+
scope: opts.scope,
143+
status: initResult.status === "error" ? "error" : "stale",
144+
path: initResult.path,
145+
message:
146+
initResult.message ??
147+
"hack-init skill is missing or out of date. Run: hack setup claude",
148+
};
149+
}
150+
151+
return { scope: opts.scope, status: "noop", path };
101152
}
102153

103154
/**
@@ -118,8 +169,16 @@ export async function removeClaudeHooks(opts: {
118169
}
119170

120171
const path = resolved.path;
172+
const initResult = await removeHackInitSkill({
173+
client: "claude",
174+
scope: opts.scope,
175+
projectRoot: opts.projectRoot,
176+
});
177+
const initRemoved = initResult.status === "removed";
178+
121179
if (!(await pathExists(path))) {
122-
return { scope: opts.scope, status: "missing", path };
180+
const status = initRemoved ? "removed" : "missing";
181+
return { scope: opts.scope, status, path };
123182
}
124183

125184
const settings = await readSettingsFile({ path });
@@ -137,7 +196,11 @@ export async function removeClaudeHooks(opts: {
137196
command: HOOK_COMMAND,
138197
});
139198
if (!changed) {
140-
return { scope: opts.scope, status: "noop", path };
199+
return {
200+
scope: opts.scope,
201+
status: initRemoved ? "removed" : "noop",
202+
path,
203+
};
141204
}
142205

143206
const nextText = `${JSON.stringify(settings.value, null, 2)}\n`;

src/agents/codex-skill.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import {
77
readTextFile,
88
writeTextFileIfChanged,
99
} from "../lib/fs.ts";
10+
import {
11+
checkHackInitSkill,
12+
installHackInitSkill,
13+
removeHackInitSkill,
14+
} from "./hack-init-skill.ts";
1015
import {
1116
normalizeInstructionText,
1217
renderInstructionSections,
@@ -34,7 +39,11 @@ const SKILL_DIR = ".codex/skills";
3439
const HACK_CLI_MARKER_REGEX = /name:\s*hack-cli\b/i;
3540

3641
/**
37-
* Install or update the Codex skill for hack CLI usage.
42+
* Install or update the Codex skills for hack usage.
43+
*
44+
* Writes the main `hack-cli` skill and the companion `/hack-init` onboarding
45+
* skill. The returned path points at the main skill; the status reflects both
46+
* artifacts (any change reports as created/updated).
3847
*/
3948
export async function installCodexSkill(opts: {
4049
readonly scope: CodexSkillScope;
@@ -55,7 +64,26 @@ export async function installCodexSkill(opts: {
5564
const existed = await pathExists(path);
5665
const result = await writeTextFileIfChanged(path, renderCodexSkill());
5766

58-
const status = resolveInstallStatus({ changed: result.changed, existed });
67+
const initResult = await installHackInitSkill({
68+
client: "codex",
69+
scope: opts.scope,
70+
projectRoot: opts.projectRoot,
71+
});
72+
if (initResult.status === "error") {
73+
return {
74+
scope: opts.scope,
75+
status: "error",
76+
path: initResult.path,
77+
message: initResult.message,
78+
};
79+
}
80+
81+
const initChanged =
82+
initResult.status === "created" || initResult.status === "updated";
83+
const status = resolveInstallStatus({
84+
changed: result.changed || initChanged,
85+
existed,
86+
});
5987
return { scope: opts.scope, status, path };
6088
}
6189

@@ -99,6 +127,22 @@ export async function checkCodexSkill(opts: {
99127
};
100128
}
101129

130+
const initResult = await checkHackInitSkill({
131+
client: "codex",
132+
scope: opts.scope,
133+
projectRoot: opts.projectRoot,
134+
});
135+
if (initResult.status !== "noop") {
136+
return {
137+
scope: opts.scope,
138+
status: initResult.status === "error" ? "error" : "stale",
139+
path: initResult.path,
140+
message:
141+
initResult.message ??
142+
"hack-init skill is missing or out of date. Run: hack setup codex",
143+
};
144+
}
145+
102146
return { scope: opts.scope, status: "noop", path };
103147
}
104148

@@ -122,8 +166,15 @@ export async function removeCodexSkill(opts: {
122166
const path = resolved.path;
123167
const skillDir = resolve(path, "..");
124168

169+
const initResult = await removeHackInitSkill({
170+
client: "codex",
171+
scope: opts.scope,
172+
projectRoot: opts.projectRoot,
173+
});
174+
125175
if (!(await pathExists(path))) {
126-
return { scope: opts.scope, status: "missing", path };
176+
const status = initResult.status === "removed" ? "removed" : "missing";
177+
return { scope: opts.scope, status, path };
127178
}
128179

129180
await rm(skillDir, { recursive: true, force: true });

0 commit comments

Comments
 (0)