Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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.
22 changes: 19 additions & 3 deletions src/lib/registry-credential-preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,25 @@ export async function discoverSuccessfulCompletionServices(opts: {
})
)
.map(([service]) => service);
return [...new Set([...bootstrapServices, ...explicitOneShots])].sort(
(left, right) => left.localeCompare(right)
);
const completionDependencies = Object.values(services).flatMap((value) => {
Comment thread
roodboi marked this conversation as resolved.
Outdated
if (!(isRecord(value) && isRecord(value.depends_on))) {
return [];
}
return Object.entries(value.depends_on)
.filter(
([, dependency]) =>
isRecord(dependency) &&
dependency.condition === "service_completed_successfully"
)
.map(([service]) => service);
});
return [
...new Set([
...bootstrapServices,
...explicitOneShots,
...completionDependencies,
]),
].sort((left, right) => left.localeCompare(right));
} catch {
return bootstrapServices;
}
Expand Down
7 changes: 4 additions & 3 deletions tests/project-up-startup-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ test("up returns failure when compose reports no services after exit zero", asyn
);
});

test("up accepts running and successfully completed one-shot services", async () => {
test("up accepts running services and successful Compose completion gates", async () => {
const projectRoot = await createProject();
psRows.push(
JSON.stringify({ Service: "api", State: "running", ExitCode: 0 }),
Expand Down Expand Up @@ -370,10 +370,11 @@ async function createProject(opts?: {
"services:",
" api:",
" image: alpine:3.20",
" depends_on:",
" migrate:",
" condition: service_completed_successfully",
" migrate:",
" image: alpine:3.20",
" labels:",
' hack.service.one-shot: "true"',
...(opts?.registryTokenScope
? [
" deps:",
Expand Down
10 changes: 9 additions & 1 deletion tests/registry-credential-preflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ test("dependency bootstrap detection is name agnostic", async () => {
]);
});

test("successful completion requires installer semantics or an explicit one-shot label", async () => {
test("successful completion recognizes installers, labels, and Compose completion gates", async () => {
const projectRoot = await createTempProject();
const composeFile = resolve(projectRoot, "compose.yml");
await writeFile(
Expand All @@ -80,13 +80,21 @@ test("successful completion requires installer semantics or an explicit one-shot
" image: app",
" labels:",
' hack.service.one-shot: "true"',
" database-init:",
" image: app",
" api:",
" image: app",
" depends_on:",
" database-init:",
" condition: service_completed_successfully",
" exited-api:",
" image: app",
"",
].join("\n")
);

expect(await discoverSuccessfulCompletionServices({ composeFile })).toEqual([
"database-init",
"installer",
"migrate",
]);
Expand Down
Loading