Skip to content

Commit 3660c27

Browse files
roodboiclaude
andcommitted
feat(agents): single instruction source with drift detection
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>
1 parent d32e743 commit 3660c27

8 files changed

Lines changed: 727 additions & 507 deletions

File tree

src/agents/codex-skill.ts

Lines changed: 29 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import {
77
readTextFile,
88
writeTextFileIfChanged,
99
} from "../lib/fs.ts";
10+
import {
11+
normalizeInstructionText,
12+
renderInstructionSections,
13+
} from "./instruction-source.ts";
1014

1115
export type CodexSkillScope = "project" | "user";
1216

@@ -16,6 +20,7 @@ export type CodexSkillResult = {
1620
| "created"
1721
| "updated"
1822
| "noop"
23+
| "stale"
1924
| "removed"
2025
| "missing"
2126
| "error";
@@ -55,7 +60,10 @@ export async function installCodexSkill(opts: {
5560
}
5661

5762
/**
58-
* Check whether the Codex skill is installed.
63+
* Check whether the Codex skill is installed and current.
64+
*
65+
* Reports `stale` when the skill file exists but its content no longer
66+
* matches the current render (normalized comparison).
5967
*/
6068
export async function checkCodexSkill(opts: {
6169
readonly scope: CodexSkillScope;
@@ -77,8 +85,21 @@ export async function checkCodexSkill(opts: {
7785
return { scope: opts.scope, status: "missing", path };
7886
}
7987

80-
const hasMarker = HACK_CLI_MARKER_REGEX.test(content);
81-
return { scope: opts.scope, status: hasMarker ? "noop" : "error", path };
88+
if (!HACK_CLI_MARKER_REGEX.test(content)) {
89+
return { scope: opts.scope, status: "error", path };
90+
}
91+
92+
const current = normalizeInstructionText({ text: renderCodexSkill() });
93+
if (normalizeInstructionText({ text: content }) !== current) {
94+
return {
95+
scope: opts.scope,
96+
status: "stale",
97+
path,
98+
message: "Codex skill content is out of date. Run: hack setup codex",
99+
};
100+
}
101+
102+
return { scope: opts.scope, status: "noop", path };
82103
}
83104

84105
/**
@@ -111,13 +132,16 @@ export async function removeCodexSkill(opts: {
111132

112133
/**
113134
* Render the Codex skill template for hack CLI usage.
135+
*
136+
* Frontmatter and top-level framing live here; the instructional content is
137+
* the `skill`-tagged sections from the shared instruction source.
114138
*/
115139
export function renderCodexSkill(): string {
116140
const lines = [
117141
"---",
118142
"name: hack-cli",
119143
"description: >",
120-
" Use the hack CLI for local runtime orchestration (compose, DNS/TLS, logs, persistent project workspaces, tickets) and agent setup.",
144+
" Use the hack CLI for local runtime orchestration (compose, DNS/TLS, logs, env, persistent project workspaces) and agent setup.",
121145
" Trigger when asked to run/start/stop services, inspect logs, manage lifecycle/workspace workflows, or update",
122146
" agent integrations. Prefer CLI over MCP when shell access is available.",
123147
"---",
@@ -126,169 +150,7 @@ export function renderCodexSkill(): string {
126150
"",
127151
"Use `hack` as the primary interface for local-first development.",
128152
"",
129-
"## Product Boundary",
130-
"",
131-
"- Supported v3 surface: project init, up/down/restart, open, logs, env, host exec/shell, sessions, doctor, daemon, and optional local tickets.",
132-
"- Removed surfaces: hosted auth/account/org/team flows, web dashboard, built-in GitHub workflows, and built-in Linear sync.",
133-
"- Remote/gateway/node/dispatch code is unsupported experimental. Do not put it on the critical path for local dev unless explicitly requested.",
134-
"",
135-
"## Operating Rules",
136-
"",
137-
"- Prefer `hack` over raw `docker` / `docker compose`.",
138-
"- Do not start/stop project services from Docker Desktop UI for `hack`-managed repos.",
139-
"- Treat `.hack/.internal` and `.hack/.branch` as hack-managed artifacts; avoid hand-editing generated files.",
140-
"- Use MCP only when shell access is unavailable.",
141-
"- Run `hack doctor` (and `hack doctor --fix`) before manual runtime/network repair.",
142-
"",
143-
"## Config + Schema",
144-
"",
145-
"- Project config: `.hack/hack.config.json`",
146-
"- Global config: `~/.hack/hack.config.json`",
147-
"- Schema URL: `https://schemas.hack/hack.config.schema.json`",
148-
"- Prefer CLI config edits via `hack config get/set`.",
149-
"",
150-
"## Hostname Routing",
151-
"",
152-
"- Primary host is `dev_host` (default: `<project>.hack`).",
153-
"- Subdomains use `<sub>.<dev_host>` (for example: `api.myapp.hack`).",
154-
"- OAuth alias can add `<dev_host>.<tld>` and `<sub>.<dev_host>.<tld>` (default: `gy`).",
155-
"- Only HTTP services with Caddy labels and `hack-dev` network attachment are routable.",
156-
"- Required labels: `caddy`, `caddy.reverse_proxy`, `caddy.tls=internal`.",
157-
"- Quick checks: `hack open`, `hack open <sub>`, `hack open --json`.",
158-
"",
159-
"## TLS + OAuth Host Rules",
160-
"",
161-
"- Caddy internal PKI provides HTTPS for routed hosts; trust CA with `hack global trust`.",
162-
"- `.hack` is local-first and not a public suffix.",
163-
"- Use alias hosts like `*.hack.gy` when provider callback validation rejects non-public-suffix hosts.",
164-
"- Alias hosts are local routes unless you explicitly add remote ingress/tunnel plumbing.",
165-
"",
166-
"## Managed Files",
167-
"",
168-
"- Source-of-truth files: `.hack/docker-compose.yml`, `.hack/hack.config.json`, `.hack/hack.env.default.yaml`, and optional `.hack/hack.env.<overlay>.yaml`.",
169-
"- Worktree-local override files: `.hack/hack.env.local.yaml` and `.hack/hack.env.<overlay>.local.yaml`.",
170-
"- Local-only files: `.hack.secret.key`, optional `.hack/.env` compatibility output, `.hack/.env.state.json`, and `.hack/.internal/` (gitignored; machine-specific state).",
171-
"- Generated by hack: `.hack/.internal/compose.override.yml`, `.hack/.internal/compose.env.override.yml`, `.hack/.branch/compose.<branch>.override.yml`.",
172-
"- Managed via CLI: `.hack/.internal/extra-hosts.json` using `hack internal extra-hosts ...` commands.",
173-
"- Lifecycle runtime files: `.hack/.internal/lifecycle/state.json`, `.hack/.internal/lifecycle/*.log`.",
174-
"",
175-
"## Advanced Networking",
176-
"",
177-
"- Static host mappings: set `internal.extra_hosts` in `.hack/hack.config.json`.",
178-
"- Dynamic mappings for local proxies/tunnels: `hack internal extra-hosts set <hostname> <target>`.",
179-
"- List/remove mappings: `hack internal extra-hosts list` / `hack internal extra-hosts unset <hostname>`.",
180-
"- Prefer `host-gateway` for host-local proxy targets when possible.",
181-
"- Apply changes with `hack restart`; verify with `hack doctor`.",
182-
"",
183-
"## Quick Start",
184-
"",
185-
"- Bootstrap project config: `hack init`",
186-
"- Start services: `hack up --detach`",
187-
"- Alternate shorthand: `hack up -d`",
188-
"- Restart services: `hack restart`",
189-
"- Open app: `hack open --json`",
190-
"- Tail logs (compose): `hack logs --pretty`",
191-
"- Per-service logs: `hack logs <service>`",
192-
"- Snapshot logs: `hack logs --json --no-follow`",
193-
"- Loki history/query: `hack logs --loki --since 2h --pretty`",
194-
"- Run commands: `hack run <service> <cmd...>`",
195-
"- Stop services: `hack down`",
196-
"",
197-
"## Global Infra",
198-
"",
199-
"- Install once: `hack global install`",
200-
"- Start/stop/status: `hack global up`, `hack global down`, `hack global status`",
201-
"- Global logs: `hack global logs <service> --no-follow --tail 200`",
202-
"",
203-
"## Unsupported Experimental Remote",
204-
"",
205-
"These commands are source-available but outside the supported v3 product contract:",
206-
"",
207-
"- Pair/register a node: `hack node pair ...`, then verify via `hack node list` and `hack node status --watch`.",
208-
"- Repair SSH access for remote Git/mutagen: `hack node ssh setup --node <id>`.",
209-
"- Inspect node workspace map on the node host: `ssh <user@host> 'hack node workspace list --json'`.",
210-
"- Inspect/repair controller route bridge: `hack node routes status`, `hack node routes repair`.",
211-
'- Dispatch command to remote workspace: `hack dispatch run --project <name|id> --node default --branch <branch> --runner generic -- "pwd"`.',
212-
"",
213-
"## Lifecycle + Startup",
214-
"",
215-
"- Put host setup in `.hack/hack.config.json` under `startup` / `lifecycle`.",
216-
"- Use lifecycle processes for long-running host tasks, not ad-hoc terminals.",
217-
'- For fixed-port host helpers such as SSM tunnels or local proxies, set `singleton.ports` and usually `onConflict: "adopt"` so Hack reuses a healthy existing listener instead of starting duplicate tunnel stacks.',
218-
"- `singleton` is a listener guard, not process ownership transfer; adopted external processes are left running on `hack down`.",
219-
"- Inspect via `hack projects --details` and `hack logs <service-or-process>`.",
220-
"",
221-
"## Verification Loops",
222-
"",
223-
"- For `hack run` / `hack exec` / env-resolution changes, verify the effective env transition matrix in",
224-
" `tests/project-run-command.test.ts`.",
225-
"- Cover omitted env, explicit overlay, explicit `base`, default-overlay resolution, cached runtime-state env,",
226-
" target-service running/not-running, worktree-local overrides, and host-vs-compose target mode.",
227-
"- For lifecycle or startup-process changes, verify `tests/project-lifecycle-processes.test.ts` and",
228-
" `tests/project-lifecycle-singleton.test.ts`.",
229-
"- Preserve `sh -c` semantics, process-group cleanup, stale pane-metadata reconciliation, singleton listener",
230-
" behavior, and interactive stdin behavior.",
231-
"- When semantics change, update `docs/env.md` or `docs/lifecycle.md` in the same patch so future agent work starts",
232-
" from the current contract.",
233-
"",
234-
"## Branch Instances",
235-
"",
236-
"Use branch instances to run parallel environments:",
237-
"",
238-
"- `hack up --branch <name> --detach`",
239-
"- `hack open --branch <name>`",
240-
"- `hack logs --branch <name>`",
241-
"- `hack down --branch <name>`",
242-
"",
243-
"## Workspaces",
244-
"",
245-
"- Picker: `hack session` for persistent project workspaces.",
246-
"- Reuse/create: `hack session start <project>`",
247-
"- Env-scoped workspace: `hack session start <project> --env qa --service api --detach`",
248-
"- Isolated agent workspace: `hack session start <project> --new --name agent-1` (`<project>--agent-1`).",
249-
'- Exec in workspace: `hack session exec <workspace> "<command>"`',
250-
'- Exec in workspace with injected env: `hack session exec <workspace> --env qa --service api "bun db:migrate"`',
251-
"- Stop workspace: `hack session stop <workspace>`",
252-
"",
253-
"## Host-side env helpers",
254-
"",
255-
"- One-off host command with injected env: `hack host exec --env qa --scope api -- bun db:migrate`",
256-
"- Host commands default to a host-local env view; use `--target compose` when you explicitly want container-oriented addresses.",
257-
"- `--scope` selects which env scope to inject; it does not move execution into that service container.",
258-
"- Interactive host shell with injected env: `hack host shell --env qa --scope api`",
259-
"- Run inside an already-running service container: `hack exec api -- bun test`",
260-
"",
261-
"## Tickets",
262-
"",
263-
'- Create: `hack tickets create --title "..." --body-stdin`',
264-
"- List/show: `hack tickets list`, `hack tickets show T-AB12CD34EF`",
265-
"- Status/sync: `hack tickets status T-AB12CD34EF in_progress`, `hack tickets sync`",
266-
"",
267-
"## Project Targeting",
268-
"",
269-
"- Run from repo root when possible.",
270-
"- Otherwise use `--project <name>` or `--path <repo-root>`.",
271-
"- List projects: `hack projects --json`.",
272-
"",
273-
"## Agent Maintenance",
274-
"",
275-
"- Project-level hack commands auto-check integration drift and attempt auto-sync.",
276-
"- Set `HACK_SETUP_SYNC_MODE=warn` to warn-only, or `HACK_SETUP_SYNC_MODE=off` to disable.",
277-
"- Refresh project + global integrations: `hack setup sync --all-scopes`",
278-
"- Check generated integrations: `hack setup sync --all-scopes --check`",
279-
"- Remove generated integrations: `hack setup sync --all-scopes --remove`",
280-
"- After self-update: `hack update` then `hack setup sync --all-scopes`",
281-
"",
282-
"## Agent Setup",
283-
"",
284-
"- Cursor rules: `hack setup cursor`",
285-
"- Claude hooks: `hack setup claude`",
286-
"- Codex skill: `hack setup codex`",
287-
"- Tickets skill: `hack setup tickets`",
288-
"- Init prompt: `hack agent init` (use --client cursor|claude|codex to open)",
289-
"- Init patterns: `hack agent patterns`",
290-
"- MCP (no shell only): `hack setup mcp`",
291-
"- MCP install (explicit): `hack mcp install --all --scope project`",
153+
renderInstructionSections({ surface: "skill", headingStyle: "markdown" }),
292154
"",
293155
];
294156

src/agents/cursor.ts

Lines changed: 24 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import {
77
readTextFile,
88
writeTextFileIfChanged,
99
} from "../lib/fs.ts";
10+
import {
11+
normalizeInstructionText,
12+
renderInstructionSections,
13+
} from "./instruction-source.ts";
1014

1115
export type CursorScope = "project" | "user";
1216

@@ -16,6 +20,7 @@ export type CursorRulesResult = {
1620
| "created"
1721
| "updated"
1822
| "noop"
23+
| "stale"
1924
| "removed"
2025
| "missing"
2126
| "error";
@@ -55,7 +60,10 @@ export async function installCursorRules(opts: {
5560
}
5661

5762
/**
58-
* Check whether Cursor rules are installed.
63+
* Check whether Cursor rules are installed and current.
64+
*
65+
* Reports `stale` when the rules file exists but its content no longer
66+
* matches the current render (normalized comparison).
5967
*/
6068
export async function checkCursorRules(opts: {
6169
readonly scope: CursorScope;
@@ -86,6 +94,16 @@ export async function checkCursorRules(opts: {
8694
};
8795
}
8896

97+
const current = normalizeInstructionText({ text: renderCursorRules() });
98+
if (normalizeInstructionText({ text: content }) !== current) {
99+
return {
100+
scope: opts.scope,
101+
status: "stale",
102+
path,
103+
message: "Cursor rules content is out of date. Run: hack setup cursor",
104+
};
105+
}
106+
89107
return { scope: opts.scope, status: "noop", path };
90108
}
91109

@@ -117,81 +135,20 @@ export async function removeCursorRules(opts: {
117135

118136
/**
119137
* Render the Cursor rules template for hack CLI usage.
138+
*
139+
* Marker framing lives here; the instructional content is the `rules`-tagged
140+
* sections from the shared instruction source.
120141
*/
121142
export function renderCursorRules(): string {
122143
const lines = [
123144
"# hack CLI",
124145
"# Auto-generated by 'hack setup cursor' - do not remove these markers",
125146
MARKER_START,
126147
"",
127-
"This project uses `hack` for local runtime orchestration (compose + DNS/TLS + logs + sessions).",
148+
"Hack v3 is local-first. This project uses `hack` for local runtime orchestration (compose + DNS/TLS + logs + sessions).",
128149
"Prefer `hack` when shell access is available. Use MCP only when shell access is unavailable.",
129150
"",
130-
"Hack v3 is local-first. Supported core commands are `hack init`, `up/down/restart`, `open`, `logs`, `env`, `host exec/shell`, `session`, `doctor`, `daemon`, and optional local `tickets`.",
131-
"Do not assume hosted auth, a web dashboard, built-in GitHub workflows, or built-in Linear sync exist. Remote/gateway/node/dispatch commands are unsupported experimental unless explicitly requested.",
132-
"",
133-
"## Core Rules",
134-
"- Use `hack` CLI for start/stop/open/logs/run/session/tickets workflows.",
135-
"- Do not run raw `docker compose up/down/logs` for normal project operations.",
136-
"- Treat `.hack/.internal` and `.hack/.branch` as hack-managed artifacts; avoid manual edits there.",
137-
"- Use lifecycle/startup config in `.hack/hack.config.json` instead of ad-hoc host process tabs.",
138-
"- Use `--json` for machine-readable output when available.",
139-
"- If state is inconsistent, run `hack doctor` then `hack doctor --fix` before manual repairs.",
140-
"- For hostname routing, only services with Caddy labels on `hack-dev` are exposed (not every compose service).",
141-
"- Use `.hack.gy` alias hosts for OAuth-style callback validation when `.hack` is rejected by providers.",
142-
"",
143-
"## Managed Files",
144-
"- Source-of-truth: `.hack/docker-compose.yml`, `.hack/hack.config.json`, `.hack/hack.env.default.yaml`, optional `.hack/hack.env.<overlay>.yaml`.",
145-
"- Worktree-local env overrides: `.hack/hack.env.local.yaml`, `.hack/hack.env.<overlay>.local.yaml`.",
146-
"- Generated/local-only: `.hack.secret.key`, `.hack/.env`, `.hack/.env.state.json`, `.hack/.internal/compose.override.yml`, `.hack/.internal/compose.env.override.yml`, `.hack/.branch/compose.<branch>.override.yml`, `.hack/.internal/lifecycle/*`.",
147-
"- Manage dynamic host mappings with `hack internal extra-hosts ...` (not manual file edits).",
148-
"",
149-
"## Lifecycle",
150-
"- Put long-running host helpers in `.hack/hack.config.json` under `lifecycle.processes` or `startup` with `persistent: true`.",
151-
'- For fixed-port helpers such as SSM tunnels or local proxies, use `singleton.ports` with `onConflict: "adopt"` when Hack should reuse an already-running equivalent listener.',
152-
"- `singleton` adoption is not process ownership transfer; Hack leaves adopted external processes alone on `hack down`.",
153-
"",
154-
"## Quick Reference",
155-
"```bash",
156-
"hack init",
157-
"hack up --detach",
158-
"hack up -d",
159-
"hack open --json",
160-
"hack open <sub>",
161-
"hack logs --pretty",
162-
"hack logs <service>",
163-
"hack logs --json --no-follow",
164-
"hack logs --loki --since 2h --pretty",
165-
"hack run <service> <cmd...>",
166-
"hack restart",
167-
"hack down",
168-
"hack global status",
169-
"hack session",
170-
"hack tickets list",
171-
"hack host exec --env qa --scope api -- bun db:migrate",
172-
"hack host shell --env qa --scope api",
173-
"# unsupported experimental remote commands:",
174-
"hack node list",
175-
"hack node status --watch",
176-
"hack node ssh setup --node <id>",
177-
"hack node routes status",
178-
"hack node routes repair",
179-
'hack dispatch run --project <name|id> --node default --branch <branch> --runner generic -- "pwd"',
180-
"hack internal extra-hosts list",
181-
"hack internal extra-hosts set <hostname> <target>",
182-
"hack internal extra-hosts unset <hostname>",
183-
"hack up --branch <name> --detach",
184-
"hack open --branch <name>",
185-
"hack logs --branch <name>",
186-
"hack down --branch <name>",
187-
"hack setup sync --all-scopes",
188-
"```",
189-
"",
190-
"## MCP (optional)",
191-
"- Install only for no-shell clients: `hack setup mcp`",
192-
"- Project-level hack commands auto-check integration drift and attempt auto-sync by default.",
193-
"- Use `HACK_SETUP_SYNC_MODE=warn` (warn only) or `HACK_SETUP_SYNC_MODE=off` (disable).",
194-
"- Refresh generated MCP/docs/instructions: `hack setup sync --all-scopes`",
151+
renderInstructionSections({ surface: "rules", headingStyle: "markdown" }),
195152
"",
196153
MARKER_END,
197154
"",

0 commit comments

Comments
 (0)