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
2 changes: 2 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ Historical remote guides are excluded from the supported documentation.

- `--json` on `hack up`/`down`/`restart`/`doctor` emits a `{ok, data | error: {code, message}}`
envelope with stable `E_*` error codes. `--json` on `hack up` implies `--detach`.
- Known Hack errors and closed native runtime refusals print their message only, never a stack
trace; set `HACK_DEBUG=1` to add the stack. Unexpected exceptions always print their stack.
- `--no-interactive` (or `HACK_NO_INTERACTIVE=1`) is a global flag: commands never prompt — they
apply documented defaults or fail fast with `E_INTERACTIVE_REQUIRED`.
- `NO_COLOR` (or `HACK_NO_COLOR`) disables colored/decorated output.
Expand Down
8 changes: 7 additions & 1 deletion docs/guides/native-candidate.md
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,13 @@ A project with no admitted mapping reports not started. `hack ps` keeps containe
observations separate from the last graph receipt. For a ready foreground graph,
it also checks the live owner: `owner_unconfirmed` means the
owner could not be authenticated, while `runtime_degraded` means its runtime check
failed. Native `exec` and `run` refresh admitted host dependencies once before
failed. A stopped candidate runtime is an ordinary state for `ps`: with a mapping
present it exits 0 and reports `runtime_not_running` (no phase, no service rows),
and text output prints "Native runtime is not running; no services are running."
`hack ps --json` prints the standard `{ok: true, data}` envelope; `data` holds
`backend: "native"`, `status` (`not_started`, `runtime_not_running`, `observed`,
`owner_unconfirmed` or `runtime_degraded`), `run`, `phase` and `items`. Bounded
logs refuse a stopped runtime with `E_DOCKER_UNAVAILABLE` and exit 1. Native `exec` and `run` refresh admitted host dependencies once before
command selection and managed environment preparation. They proceed only with
fresh ownership and runtime proof, even if containers still appear running.
Unconfirmed ownership or an incomplete refresh requires inspection and explicit
Expand Down
10 changes: 8 additions & 2 deletions docs/reference/native-authored-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ Only fresh runtime observations supply state and health. Stored readiness and ph
do not prove that a service is still live.

A project with no saved attempt reports `not_started`; a retained startup intent
without a Ready mapping reports `pending` with no confirmed service rows. A dead
without a Ready mapping reports `pending` with no confirmed service rows. When a
Ready generation is saved but the candidate runtime is stopped (native code
`runtime_not_running`), `ps` exits 0 and reports `runtime_not_running` with the
saved `data.run`, a null `data.phase` and no items; text output prints
"Native runtime is not running; no services are running." A dead
owner, uncertain recovery, changed selection, or malformed or mismatched reply
refuses with unconfirmed status. The reader holds and rechecks project, private
directories, saved record identities and the selected executable across its single
Expand Down Expand Up @@ -259,7 +263,9 @@ guest checks, within the existing bounded control reply. Cancellation or incompl
child/output settlement
withholds the result; it never stops the foreground owner or retries a query. Logs
may contain application values and are returned only to the requesting caller;
they are never journaled or included in refusal diagnostics. Offline controls do
they are never journaled or included in refusal diagnostics. A stopped candidate
runtime refuses with `E_DOCKER_UNAVAILABLE` ("Native runtime is not running; no
service logs are available.") and exit 1. Offline controls do
not qualify a live provider or application log behavior.

## Finite service exec
Expand Down
2 changes: 2 additions & 0 deletions scripts/accept-native-frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ def up(self, label, previous_container=None, command="up"):
ps = self.cli(f"{label}-ps", "ps", "--path", str(self.project), "--branch", self.branch, "--json", timeout=20)
except Failure:
ps = {}
# `ps --json` prints the standard {ok, data} envelope; only its data is a native status.
ps = ps.get("data") if isinstance(ps, dict) and ps.get("ok") is True else None
ps = ps if isinstance(ps, dict) else {}
items = ps.get("items") or []
if (ps.get("status") == "observed" and ps.get("phase") == "ready-observed"
Expand Down
62 changes: 40 additions & 22 deletions src/backends/native-authored-project-logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "./native-authored-runtime-binary.ts";
import {
invokeNativeRuntime,
isNativeRuntimeNotRunning,
type NativeRuntimeSelection,
} from "./native-runtime-client.ts";

Expand Down Expand Up @@ -133,7 +134,11 @@ export async function nativeAuthoredProjectLogs(input: {
) {
return refused();
}
const result = await withStatus(scope, async (saved) => {
// The held saved-read owner turns any error raised inside it into its own
// refusal, so a stopped runtime leaves it as a value and is rethrown after.
const result = await withStatus<
{ readonly logs: NativeAuthoredLogs } | { readonly stopped: Error }
>(scope, async (saved) => {
active();
const receipt =
requireReadyServiceReceipt(saved, scope, service) ?? refused();
Expand All @@ -146,32 +151,45 @@ export async function nativeAuthoredProjectLogs(input: {
active();
};
await check();
const value = await invoke({
runtime,
cwd: scope.projectRoot,
args: [
"graph",
"native",
"logs",
"--run-id",
receipt.review.provenance.run,
"--service",
service,
"--tail",
String(tail),
"--json",
],
timeoutMs: 15_000,
signal,
boundNativeAuthoredLogsDrain: true,
});
let value: unknown;
try {
value = await invoke({
runtime,
cwd: scope.projectRoot,
args: [
"graph",
"native",
"logs",
"--run-id",
receipt.review.provenance.run,
"--service",
service,
"--tail",
String(tail),
"--json",
],
timeoutMs: 15_000,
signal,
boundNativeAuthoredLogsDrain: true,
});
} catch (error: unknown) {
if (!(isNativeRuntimeNotRunning(error) && error instanceof Error)) {
throw error;
}
// Reported only while the same saved selection and executable hold.
await check();
return { stopped: error };
}
await check();
const logs = parseNativeAuthoredLogs(value, receipt, service);
await check();
return logs;
return { logs };
}
);
});
active();
return result;
if ("stopped" in result) {
throw result.stopped;
}
return result.logs;
}
62 changes: 44 additions & 18 deletions src/backends/native-authored-project-observe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { withPinnedRuntimeBinary } from "./native-authored-runtime-binary.ts";
import {
invokeNativeRuntime,
isNativeRuntimeNotRunning,
type NativeRuntimeSelection,
} from "./native-runtime-client.ts";

Expand All @@ -29,7 +30,15 @@ type Item = {
};
export type NativeAuthoredProjectStatus = {
readonly backend: "native";
readonly status: "not_started" | "pending" | "observed";
/**
* `runtime_not_running`: a Ready generation is saved but the candidate
* runtime is stopped. Nothing was observed: `phase` is null, `items` empty.
*/
readonly status:
| "not_started"
| "pending"
| "runtime_not_running"
| "observed";
readonly run: string | null;
readonly phase: string | null;
readonly items: readonly Item[];
Expand Down Expand Up @@ -83,23 +92,40 @@ export async function nativeAuthoredProjectPs(input: {
active(signal);
};
await check();
const value = await invoke({
runtime,
cwd: scope.projectRoot,
args: [
"graph",
"native",
"control",
"--run-id",
receipt.review.provenance.run,
"--action",
"status",
"--json",
],
timeoutMs: 15_000,
signal,
boundNativeStatusDrain: true,
});
let value: unknown;
try {
value = await invoke({
runtime,
cwd: scope.projectRoot,
args: [
"graph",
"native",
"control",
"--run-id",
receipt.review.provenance.run,
"--action",
"status",
"--json",
],
timeoutMs: 15_000,
signal,
boundNativeStatusDrain: true,
});
} catch (error: unknown) {
if (!isNativeRuntimeNotRunning(error)) {
throw error;
}
// A stopped runtime is a status, not a refusal; the same saved
// selection and executable must still hold.
await check();
return {
backend: "native",
status: "runtime_not_running",
run: receipt.review.provenance.run,
phase: null,
items: [],
};
}
await check();
const current = parseNativeAuthoredControl(value, receipt, "status");
const items: Item[] = Object.entries(current.receipt.readiness).map(
Expand Down
75 changes: 64 additions & 11 deletions src/backends/native-project-observe.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ClosedCliError, HackCliError } from "../lib/cli-result.ts";
import { isRecord } from "../lib/guards.ts";
import {
loadNativeProjectRun,
Expand All @@ -6,6 +7,7 @@ import {
} from "./native-project-run.ts";
import {
invokeNativeRuntime,
isNativeRuntimeNotRunning,
type NativeRuntimeSelection,
} from "./native-runtime-client.ts";

Expand All @@ -25,10 +27,36 @@ type Options = {
readonly invoke?: typeof invokeNativeRuntime;
};
function refused(): Error {
return new Error(
return new ClosedCliError(
"Native graph observation refused: owned mapping or current graph identity is unavailable or changed."
);
}
/** Closed refusal for logs, which have nothing to read from a stopped runtime. */
function runtimeNotRunning(): HackCliError {
return new HackCliError({
code: "E_DOCKER_UNAVAILABLE",
message:
"Native runtime is not running; no service logs are available. Start the native runtime and retry.",
});
}
/**
* Native ps result. `runtime_not_running` means the project has an owned run
* mapping but the candidate runtime is stopped: nothing was observed, so
* `phase` is null and `items` is empty.
*/
export type NativeProjectPsStatus =
| "not_started"
| "runtime_not_running"
| "observed"
| "owner_unconfirmed"
| "runtime_degraded";
export type NativeProjectPsResult = {
readonly backend: "native";
readonly status: NativeProjectPsStatus;
readonly run: string | null;
readonly phase: string | null;
readonly items: Item[];
};
function inspected(
value: unknown,
expected: NativeProjectRun
Expand Down Expand Up @@ -111,19 +139,39 @@ async function inspect(opts: Options, run: NativeProjectRun) {
run
);
}
/** Historical receipt phase is separated from current per-service observations. */
export async function nativeProjectPs(opts: Options) {
/**
* Historical receipt phase is separated from current per-service observations.
* A stopped candidate runtime is an ordinary state for this read: it reports
* `runtime_not_running` instead of failing. Every other refusal still throws.
*/
export async function nativeProjectPs(
opts: Options
): Promise<NativeProjectPsResult> {
const run = await loadNativeProjectRun(opts.scope);
if (!run) {
return {
backend: "native" as const,
status: "not_started" as const,
backend: "native",
status: "not_started",
run: null,
phase: null,
items: [] as Item[],
items: [],
};
}
let snapshot: Awaited<ReturnType<typeof inspect>>;
try {
snapshot = await inspect(opts, run);
} catch (error: unknown) {
if (!isNativeRuntimeNotRunning(error)) {
throw error;
}
return {
backend: "native",
status: "runtime_not_running",
run: run.run,
phase: null,
items: [],
};
}
const snapshot = await inspect(opts, run);
let status: "observed" | "owner_unconfirmed" | "runtime_degraded" =
"observed";
if (snapshot.foreground && snapshot.phase === "ready-observed") {
Expand Down Expand Up @@ -153,7 +201,7 @@ export async function nativeProjectPs(opts: Options) {
}
}
return {
backend: "native" as const,
backend: "native",
status,
run: run.run,
items: snapshot.items,
Expand All @@ -176,17 +224,22 @@ export async function nativeProjectLogs(
opts.tail < 1 ||
opts.tail > 1000
) {
throw new Error(
throw new ClosedCliError(
"Native logs require one service, --no-follow and --tail 1..1000."
);
}
const run = await loadNativeProjectRun(opts.scope);
if (!run) {
throw new Error(
throw new ClosedCliError(
"Native project has not been started; no owned graph mapping exists."
);
}
const before = await inspect(opts, run);
let before: Awaited<ReturnType<typeof inspect>>;
try {
before = await inspect(opts, run);
} catch (error: unknown) {
throw isNativeRuntimeNotRunning(error) ? runtimeNotRunning() : error;
}
const service = before.items.find((item) => item.service === opts.service);
if (!service?.container) {
throw refused();
Expand Down
Loading
Loading