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
6 changes: 3 additions & 3 deletions .codex/skills/hack-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ Use `hack` as the primary interface for local-first development.

## Integration freshness

- These instructions were generated by hack CLI v3.3.5; treat cached rules from another version as potentially stale.
- These instructions were generated by hack CLI v3.3.6; treat cached rules from another version as potentially stale.
- At session start, audit project and global integrations with `hack setup sync --all-scopes --check`.
- If anything is stale, missing, or deprecated, run `hack setup sync --all-scopes`, then reload the agent session so cached instructions are replaced.
- Never copy or hand-edit generated Hack rules to refresh them; update the CLI and run the sync command.
- Content revision: `b8663fad3ef4` (version alone is not a freshness guarantee).
- Content revision: `eef2b0142b95` (version alone is not a freshness guarantee).

## Product boundary

Expand Down Expand Up @@ -125,7 +125,7 @@ Use `hack` as the primary interface for local-first development.
- Inspect lifecycle status via `hack projects --details` and stream via `hack logs <service-or-process>`.
- Lifecycle session recovery is ownership-proven: Hack adopts healthy token-, definition-, and environment-matched sessions, replaces owned stale sessions, and refuses to kill same-name sessions without deterministic ownership proof.
- `hack doctor --fix` reaps an orphan lifecycle session only when mux ownership is proven and its Compose instance is absent; unverified same-name sessions are never modified.
- After `hack up` or `hack restart`, running services and successful one-shot services (`exited` with code 0) count as successful; other states return `E_STARTUP_INCOMPLETE`, and `hack doctor` warns about containers stuck in `Created`.
- After `hack up` or `hack restart`, running services and successful one-shot services (`exited` with code 0) count as successful. Hack recognizes dependency installers, `hack.service.one-shot=true`, and services referenced by Compose `condition: service_completed_successfully`; other states return `E_STARTUP_INCOMPLETE`, and `hack doctor` warns about containers stuck in `Created`.
- Detached startup is bounded; a hung Compose operation returns `E_STARTUP_TIMEOUT`, terminates its process group, and `hack doctor --fix` can start exact containers left in `Created`.
- Target only affected services with `hack up <service...> --detach`, `hack restart <service...>`, or `hack env apply --service <service>`; scoped operations skip project lifecycle hooks and implicit dependency startup.
- Use `hack env explain <KEY> --env <overlay> --service <service> --target <host|compose>` for redacted source, precedence, availability, and delivery diagnostics.
Expand Down
6 changes: 3 additions & 3 deletions .cursor/rules/hack.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ Prefer `hack` when shell access is available. Use MCP only when shell access is

## Integration freshness

- These instructions were generated by hack CLI v3.3.5; treat cached rules from another version as potentially stale.
- These instructions were generated by hack CLI v3.3.6; treat cached rules from another version as potentially stale.
- At session start, audit project and global integrations with `hack setup sync --all-scopes --check`.
- If anything is stale, missing, or deprecated, run `hack setup sync --all-scopes`, then reload the agent session so cached instructions are replaced.
- Never copy or hand-edit generated Hack rules to refresh them; update the CLI and run the sync command.
- Content revision: `b8663fad3ef4` (version alone is not a freshness guarantee).
- Content revision: `eef2b0142b95` (version alone is not a freshness guarantee).

## Product boundary

Expand Down Expand Up @@ -73,7 +73,7 @@ Prefer `hack` when shell access is available. Use MCP only when shell access is
- Inspect lifecycle status via `hack projects --details` and stream via `hack logs <service-or-process>`.
- Lifecycle session recovery is ownership-proven: Hack adopts healthy token-, definition-, and environment-matched sessions, replaces owned stale sessions, and refuses to kill same-name sessions without deterministic ownership proof.
- `hack doctor --fix` reaps an orphan lifecycle session only when mux ownership is proven and its Compose instance is absent; unverified same-name sessions are never modified.
- After `hack up` or `hack restart`, running services and successful one-shot services (`exited` with code 0) count as successful; other states return `E_STARTUP_INCOMPLETE`, and `hack doctor` warns about containers stuck in `Created`.
- After `hack up` or `hack restart`, running services and successful one-shot services (`exited` with code 0) count as successful. Hack recognizes dependency installers, `hack.service.one-shot=true`, and services referenced by Compose `condition: service_completed_successfully`; other states return `E_STARTUP_INCOMPLETE`, and `hack doctor` warns about containers stuck in `Created`.
- Detached startup is bounded; a hung Compose operation returns `E_STARTUP_TIMEOUT`, terminates its process group, and `hack doctor --fix` can start exact containers left in `Created`.
- Target only affected services with `hack up <service...> --detach`, `hack restart <service...>`, or `hack env apply --service <service>`; scoped operations skip project lifecycle hooks and implicit dependency startup.
- Use `hack env explain <KEY> --env <overlay> --service <service> --target <host|compose>` for redacted source, precedence, availability, and delivery diagnostics.
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,11 @@ Most formatting and common issues are automatically fixed by Biome. Run `bun x u
Use `hack` as the single interface for local-first runtime orchestration (compose, DNS/TLS, logs, env, and persistent project workspaces).

Integration freshness:
- These instructions were generated by hack CLI v3.3.5; treat cached rules from another version as potentially stale.
- These instructions were generated by hack CLI v3.3.6; treat cached rules from another version as potentially stale.
- At session start, audit project and global integrations with `hack setup sync --all-scopes --check`.
- If anything is stale, missing, or deprecated, run `hack setup sync --all-scopes`, then reload the agent session so cached instructions are replaced.
- Never copy or hand-edit generated Hack rules to refresh them; update the CLI and run the sync command.
- Content revision: `b8663fad3ef4` (version alone is not a freshness guarantee).
- Content revision: `eef2b0142b95` (version alone is not a freshness guarantee).

Product boundary:
- Supported v3 surface: project init, up/down/restart, open, logs, env, host exec/shell, sessions, doctor, and daemon.
Expand Down Expand Up @@ -310,7 +310,7 @@ Lifecycle + startup:
- Inspect lifecycle status via `hack projects --details` and stream via `hack logs <service-or-process>`.
- Lifecycle session recovery is ownership-proven: Hack adopts healthy token-, definition-, and environment-matched sessions, replaces owned stale sessions, and refuses to kill same-name sessions without deterministic ownership proof.
- `hack doctor --fix` reaps an orphan lifecycle session only when mux ownership is proven and its Compose instance is absent; unverified same-name sessions are never modified.
- After `hack up` or `hack restart`, running services and successful one-shot services (`exited` with code 0) count as successful; other states return `E_STARTUP_INCOMPLETE`, and `hack doctor` warns about containers stuck in `Created`.
- After `hack up` or `hack restart`, running services and successful one-shot services (`exited` with code 0) count as successful. Hack recognizes dependency installers, `hack.service.one-shot=true`, and services referenced by Compose `condition: service_completed_successfully`; other states return `E_STARTUP_INCOMPLETE`, and `hack doctor` warns about containers stuck in `Created`.
- Detached startup is bounded; a hung Compose operation returns `E_STARTUP_TIMEOUT`, terminates its process group, and `hack doctor --fix` can start exact containers left in `Created`.
- Target only affected services with `hack up <service...> --detach`, `hack restart <service...>`, or `hack env apply --service <service>`; scoped operations skip project lifecycle hooks and implicit dependency startup.
- Use `hack env explain <KEY> --env <overlay> --service <service> --target <host|compose>` for redacted source, precedence, availability, and delivery diagnostics.
Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ This project uses Obsidian for project context, specs, research, and progress tr
Use `hack` as the single interface for local-first runtime orchestration (compose, DNS/TLS, logs, env, and persistent project workspaces).

Integration freshness:
- These instructions were generated by hack CLI v3.3.5; treat cached rules from another version as potentially stale.
- These instructions were generated by hack CLI v3.3.6; treat cached rules from another version as potentially stale.
- At session start, audit project and global integrations with `hack setup sync --all-scopes --check`.
- If anything is stale, missing, or deprecated, run `hack setup sync --all-scopes`, then reload the agent session so cached instructions are replaced.
- Never copy or hand-edit generated Hack rules to refresh them; update the CLI and run the sync command.
- Content revision: `b8663fad3ef4` (version alone is not a freshness guarantee).
- Content revision: `eef2b0142b95` (version alone is not a freshness guarantee).

Product boundary:
- Supported v3 surface: project init, up/down/restart, open, logs, env, host exec/shell, sessions, doctor, and daemon.
Expand Down Expand Up @@ -180,7 +180,7 @@ Lifecycle + startup:
- Inspect lifecycle status via `hack projects --details` and stream via `hack logs <service-or-process>`.
- Lifecycle session recovery is ownership-proven: Hack adopts healthy token-, definition-, and environment-matched sessions, replaces owned stale sessions, and refuses to kill same-name sessions without deterministic ownership proof.
- `hack doctor --fix` reaps an orphan lifecycle session only when mux ownership is proven and its Compose instance is absent; unverified same-name sessions are never modified.
- After `hack up` or `hack restart`, running services and successful one-shot services (`exited` with code 0) count as successful; other states return `E_STARTUP_INCOMPLETE`, and `hack doctor` warns about containers stuck in `Created`.
- After `hack up` or `hack restart`, running services and successful one-shot services (`exited` with code 0) count as successful. Hack recognizes dependency installers, `hack.service.one-shot=true`, and services referenced by Compose `condition: service_completed_successfully`; other states return `E_STARTUP_INCOMPLETE`, and `hack doctor` warns about containers stuck in `Created`.
- Detached startup is bounded; a hung Compose operation returns `E_STARTUP_TIMEOUT`, terminates its process group, and `hack doctor --fix` can start exact containers left in `Created`.
- Target only affected services with `hack up <service...> --detach`, `hack restart <service...>`, or `hack env apply --service <service>`; scoped operations skip project lifecycle hooks and implicit dependency startup.
- Use `hack env explain <KEY> --env <overlay> --service <service> --target <host|compose>` for redacted source, precedence, availability, and delivery diagnostics.
Expand Down
3 changes: 3 additions & 0 deletions docs/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ its isolated derived branch instance, and `--branch <name>` targets only that ex
`E_COMPOSE_FAILED`. If Compose exits successfully but one or more containers remain `created`, exit
non-zero, or enter another failed runtime state, Hack returns `E_STARTUP_INCOMPLETE`. Successful
one-shot services (`exited` with code 0) are reported under `services.completed` and do not fail startup.
Hack recognizes dependency installers, services labeled `hack.service.one-shot=true`, and services
referenced by another service with Compose `condition: service_completed_successfully`. An unrelated
service that merely exits zero is still treated as incomplete startup.
Lifecycle failures are one of the primary consumers of the `--json` error envelope,
since hook and process startup are common failure points around `hack up`.

Expand Down
2 changes: 1 addition & 1 deletion src/agents/instruction-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export const INSTRUCTION_SECTIONS: readonly InstructionSection[] = [
"Inspect lifecycle status via `hack projects --details` and stream via `hack logs <service-or-process>`.",
"Lifecycle session recovery is ownership-proven: Hack adopts healthy token-, definition-, and environment-matched sessions, replaces owned stale sessions, and refuses to kill same-name sessions without deterministic ownership proof.",
"`hack doctor --fix` reaps an orphan lifecycle session only when mux ownership is proven and its Compose instance is absent; unverified same-name sessions are never modified.",
"After `hack up` or `hack restart`, running services and successful one-shot services (`exited` with code 0) count as successful; other states return `E_STARTUP_INCOMPLETE`, and `hack doctor` warns about containers stuck in `Created`.",
"After `hack up` or `hack restart`, running services and successful one-shot services (`exited` with code 0) count as successful. Hack recognizes dependency installers, `hack.service.one-shot=true`, and services referenced by Compose `condition: service_completed_successfully`; other states return `E_STARTUP_INCOMPLETE`, and `hack doctor` warns about containers stuck in `Created`.",
"Detached startup is bounded; a hung Compose operation returns `E_STARTUP_TIMEOUT`, terminates its process group, and `hack doctor --fix` can start exact containers left in `Created`.",
"Target only affected services with `hack up <service...> --detach`, `hack restart <service...>`, or `hack env apply --service <service>`; scoped operations skip project lifecycle hooks and implicit dependency startup.",
"Use `hack env explain <KEY> --env <overlay> --service <service> --target <host|compose>` for redacted source, precedence, availability, and delivery diagnostics.",
Expand Down
2 changes: 1 addition & 1 deletion src/agents/integration-revision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
* source test recomputes this value and fails whenever guidance changes
* without a revision update.
*/
export const HACK_AGENT_INTEGRATION_CONTENT_REVISION = "b8663fad3ef4";
export const HACK_AGENT_INTEGRATION_CONTENT_REVISION = "eef2b0142b95";
Comment thread
roodboi marked this conversation as resolved.
36 changes: 36 additions & 0 deletions src/commands/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,13 +755,15 @@ async function readComposeServiceStates(opts: {
readonly composeProjectName: string | null;
readonly profiles: readonly string[];
readonly services?: readonly string[];
readonly env?: Readonly<Record<string, string>>;
}): Promise<readonly ComposeServiceState[]> {
const res = await composeRuntimeBackend.psJson({
composeFiles: [opts.project.composeFile],
composeProject: opts.composeProjectName,
profiles: opts.profiles,
cwd: dirname(opts.project.composeFile),
all: true,
env: opts.env,
});
if (res.exitCode !== 0) {
return [];
Expand Down Expand Up @@ -6114,10 +6116,17 @@ async function runUpCommand({
composeProjectName,
profiles,
services: targetServices,
env: envOverrides.env,
});
const effectiveProfiles = resolveEffectiveComposeProfiles({
cliProfiles: profiles,
composeEnv: envOverrides.env,
});
const successfulCompletionServices = new Set(
await discoverSuccessfulCompletionServices({
composeFile: project.composeFile,
activeProfiles: effectiveProfiles,
selectedServices: serviceScoped ? targetServices : undefined,
})
);
const startup = classifyComposeStartupState(states, {
Expand Down Expand Up @@ -6786,10 +6795,17 @@ async function runTargetedServiceRestart(opts: {
composeProjectName: opts.composeProjectName,
profiles: opts.profiles,
services: opts.services,
env: envOverrides.env,
});
const effectiveProfiles = resolveEffectiveComposeProfiles({
cliProfiles: opts.profiles,
composeEnv: envOverrides.env,
});
const successfulCompletionServices = new Set(
await discoverSuccessfulCompletionServices({
composeFile: opts.project.composeFile,
activeProfiles: effectiveProfiles,
selectedServices: opts.services,
})
);
const startup = classifyComposeStartupState(states, {
Expand Down Expand Up @@ -7049,10 +7065,16 @@ async function runRestartCommand({
project,
composeProjectName,
profiles,
env: preflightEnv.env,
});
const effectiveProfiles = resolveEffectiveComposeProfiles({
cliProfiles: profiles,
composeEnv: preflightEnv.env,
});
const successfulCompletionServices = new Set(
await discoverSuccessfulCompletionServices({
composeFile: project.composeFile,
activeProfiles: effectiveProfiles,
})
);
const startup = classifyComposeStartupState(states, {
Expand Down Expand Up @@ -8064,6 +8086,20 @@ function parseCsvList(value: string | undefined): string[] {
return out;
}

/** Resolve the profiles Compose activates after CLI-over-environment precedence. */
function resolveEffectiveComposeProfiles(opts: {
readonly cliProfiles: readonly string[];
readonly composeEnv: Readonly<Record<string, string>>;
}): string[] {
if (opts.cliProfiles.length > 0) {
return [...opts.cliProfiles];
}
const composeProfiles = Object.hasOwn(opts.composeEnv, "COMPOSE_PROFILES")
? opts.composeEnv.COMPOSE_PROFILES
: process.env.COMPOSE_PROFILES;
return parseCsvList(composeProfiles);
}

function parseLogTimeRange(opts: {
readonly since: string | undefined;
readonly until: string | undefined;
Expand Down
38 changes: 36 additions & 2 deletions src/lib/registry-credential-preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ export async function discoverDependencyBootstrapServices(opts: {
/** Services that may legitimately exit zero after startup instead of staying running. */
export async function discoverSuccessfulCompletionServices(opts: {
readonly composeFile: string;
readonly activeProfiles?: readonly string[];
readonly selectedServices?: readonly string[];
}): Promise<readonly string[]> {
const bootstrapServices = await discoverDependencyBootstrapServices(opts);
const text = await readTextFile(opts.composeFile);
Expand All @@ -144,9 +146,41 @@ export async function discoverSuccessfulCompletionServices(opts: {
})
)
.map(([service]) => service);
return [...new Set([...bootstrapServices, ...explicitOneShots])].sort(
(left, right) => left.localeCompare(right)
const activeProfiles = new Set(opts.activeProfiles ?? []);
const allProfilesActive = activeProfiles.has("*");
const selectedServices = opts.selectedServices
? new Set(opts.selectedServices)
: null;
const completionDependencies = Object.entries(services).flatMap(
([service, value]) => {
if (!(isRecord(value) && isRecord(value.depends_on))) {
return [];
}
const dependentIsActive = selectedServices
? selectedServices.has(service)
: !Array.isArray(value.profiles) ||
allProfilesActive ||
value.profiles.some(
(profile) =>
typeof profile === "string" && activeProfiles.has(profile)
);
return Object.entries(value.depends_on)
.filter(
([dependencyService, dependency]) =>
isRecord(dependency) &&
dependency.condition === "service_completed_successfully" &&
(dependentIsActive || selectedServices?.has(dependencyService))
)
.map(([service]) => service);
}
);
return [
...new Set([
...bootstrapServices,
...explicitOneShots,
...completionDependencies,
]),
].sort((left, right) => left.localeCompare(right));
} catch {
return bootstrapServices;
}
Expand Down
Loading
Loading