Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
d111802
docs: add versioned agent-client capability matrix and policy controls
roodboi Oct 12, 2026
58c485a
fix(mcp): harden run argv and tool input contract
roodboi Oct 12, 2026
c6c0db3
fix(env): harden legacy env key files and AES-GCM envelope parsing
roodboi Oct 12, 2026
de862d7
Merge remote-tracking branch 'origin/next' into w14-rework
roodboi Oct 12, 2026
dc88c6b
Merge remote-tracking branch 'origin/next' into w06-rework
roodboi Oct 12, 2026
ffeecce
fix(agents): generate capability matrix doc and parse all guidance co…
roodboi Oct 12, 2026
56bac3a
fix(mcp): make typed refusals value-free and keep unknown-tool in-band
roodboi Oct 12, 2026
12e80af
chore: merge origin/next into wf/w15-w5-6
roodboi Oct 12, 2026
cd76b26
test(env): pin AES-GCM IV, size and tag-length guards with discrimina…
roodboi Oct 12, 2026
bcf88ec
fix(init): stop writing repo-local agent files by default
roodboi Oct 12, 2026
c005262
fix(env): resolve --env base and default the same way on every command
roodboi Oct 12, 2026
6136946
fix(agents): count corrupted and unreadable integrations as drift
roodboi Oct 12, 2026
9854d1b
test(env): pin the console logger backend for CLI refusal tests
roodboi Oct 12, 2026
6e1ff63
test(mcp): assert closed ambiguous_project refusal in project-name suite
roodboi Oct 12, 2026
1f8b51e
chore(x01): temporary witness cost trace for hosted measurement
roodboi Oct 12, 2026
0911e6d
perf(storage): coalesce witness helper work per native command
roodboi Oct 12, 2026
eb9a678
chore(x01): revert temporary witness cost trace
roodboi Oct 12, 2026
7b2ccfc
test(e2e): restore the networks scenario budget after witness coalescing
roodboi Oct 12, 2026
fa7741c
Merge remote-tracking branch 'origin/next' into wf/x03-witness-probes
roodboi Oct 12, 2026
37f093a
perf(storage): make witness engine and executable fences cheaper
roodboi Oct 12, 2026
6a4e118
chore(x03): temporary witness probe trace for hosted measurement
roodboi Oct 12, 2026
2b11ed3
fix(storage): keep closed probe cancellation for direct engine-ID fences
roodboi Oct 12, 2026
a777bc4
test(storage): pin witness fence counts and refusal controls
roodboi Oct 12, 2026
994ddb8
chore(x03): revert temporary witness probe trace
roodboi Oct 12, 2026
3edbd54
feat(agents): package offline skill references and the HACK-P pattern…
roodboi Oct 12, 2026
726cc27
test(e2e): add agent-surface-freeze negative control for guidance writes
roodboi Oct 12, 2026
9b971fa
ci: add gated Bun crash experiment and read-only crash census
roodboi Oct 12, 2026
1342871
perf(storage): read engine identity directly for docker context selec…
roodboi Oct 12, 2026
c2ecf9c
chore(x04): temporary witness probe trace for hosted measurement
roodboi Oct 12, 2026
5cf9f0f
chore(x04): revert temporary witness probe trace
roodboi Oct 12, 2026
ebe0539
test(agents): cover reference generation and reference-style and HTML…
roodboi Oct 12, 2026
6b54980
test: keep the Bun crash loop tests free of real crash reports
roodboi Oct 12, 2026
7a05b84
Merge branch 'next' into wf/w15-w5-6
roodboi Oct 12, 2026
c00468c
Merge branch 'next' into wf/w06-w5-3
roodboi Oct 12, 2026
e9b1252
Merge branch 'next' into wf/w13-w5b-2
roodboi Oct 12, 2026
af786b5
Merge branch 'next' into wf/w10-w5b-0
roodboi Oct 12, 2026
9db4dba
Merge branch 'next' into wf/w14-w5-5
roodboi Oct 12, 2026
5a731cc
Merge remote-tracking branch 'origin/wf/w06-w5-3' into train/queue-pr…
roodboi Oct 12, 2026
38d9d7f
Merge remote-tracking branch 'origin/wf/w14-w5-5' into train/queue-pr…
roodboi Oct 12, 2026
d50e935
Merge remote-tracking branch 'origin/wf/w15-w5-6' into train/queue-pr…
roodboi Oct 12, 2026
8bab674
Merge remote-tracking branch 'origin/wf/w10-w5b-0' into train/queue-p…
roodboi Oct 12, 2026
26958c9
Merge remote-tracking branch 'origin/wf/w13-w5b-2' into train/queue-p…
roodboi Oct 12, 2026
07a1834
Merge remote-tracking branch 'origin/wf/w07-w6-3' into train/queue-pr…
roodboi Oct 12, 2026
a92e3ab
Merge remote-tracking branch 'origin/wf/w02-w6-1' into train/queue-pr…
roodboi Oct 12, 2026
3e261d5
Merge remote-tracking branch 'origin/wf/w05-w6-2' into train/queue-pr…
roodboi Oct 12, 2026
9f20366
fix(env): write .hack/.env and the encrypted store atomically with 0600
roodboi Oct 12, 2026
10b6d73
fix(agents): point the env matrix bullet at the offline env guide
roodboi Oct 12, 2026
77da332
Merge remote-tracking branch 'origin/wf/w13-w5b-2' into train/queue-p…
roodboi Oct 12, 2026
cb665d5
fix(env): scope the env-file symlink refusal doc to write paths
roodboi Oct 12, 2026
c2b41ff
chore: merge train/queue-preview into wf/y3-envfile-b6
roodboi Oct 12, 2026
5078855
feat(doctor): add value-free env key strength diagnostic
roodboi Oct 12, 2026
5f8cb79
fix(logger): pass messages to gum after an end-of-options marker
roodboi Oct 12, 2026
07413d0
Merge branch 'next' into wf/c4-keydiag-b7
roodboi Oct 12, 2026
6a043c0
Merge branch 'next' into wf/c6-gum-dash-b7
roodboi Oct 12, 2026
4243d60
Merge branch 'next' into wf/y3-envfile-b6
roodboi Oct 12, 2026
989fcea
Merge branch 'wf/c6-gum-dash-b7' into wf/y3-envfile-b6
roodboi Oct 12, 2026
c6055dd
Merge branch 'wf/y3-envfile-b6' into wf/c4-keydiag-b7
roodboi Oct 12, 2026
9cd7956
Merge branch 'next' into wf/c4-keydiag-b7
roodboi Oct 12, 2026
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
11 changes: 11 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,14 @@ That local archive is not included in a fresh clone.
The command reference is generated with `bun run docs:cli-reference`. Agent-facing
setup content is generated from `src/agents/instruction-source.ts` and
`src/agents/onboarding-prompt.ts`. End-to-end checks live in `tests/e2e/`.

### Dependabot alert inventory

To plan dependency updates, list open Dependabot alerts with
`bun scripts/dependabot-inventory.ts` (add `--repo owner/name` for another repository,
or `--json` for machine-readable output). The script prints each alert's package,
ecosystem, severity, manifest path and first patched version ("fixed-in"), sorted by
severity. It sends only `GET` requests through your existing `gh` authentication,
writes no files and changes no alerts or dependencies. Make the dependency updates in
separate, reviewed changes. You need read access to the repository's Dependabot
alerts.
29 changes: 29 additions & 0 deletions docs/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,35 @@ Write-path guarantees in a linked worktree (first secret added from any checkout
- if the shared location cannot be resolved (git unavailable) or written, Hack falls back to a checkout-local key and prints a loud divergence warning
- `hack doctor` flags divergent `.hack.secret.key` contents across checkouts with the exact paths

Key strength and exposure (`hack doctor` "env key" check):

`hack doctor` and `hack doctor --json` audit the checkout-local key, the shared key
under the git common dir and, in a linked worktree, the primary checkout's key. When
no key file exists, the check classifies `HACK_ENV_SECRET_KEY` instead. The check is
read-only. It never prints key bytes or absolute paths, and `--fix` does not act on it.
Each finding names a project-relative subject and the exact command to run from the
project root. `--json` adds an `envKey` object to the check with `status` and
`findings` (`code`, `status`, `subject`, `remedy`).

| Code | Status | Meaning |
| --- | --- | --- |
| `env-key-ok` | ok | key material is a regular, private, 256-bit-class file |
| `env-key-absent` | ok | no key file and no `HACK_ENV_SECRET_KEY` |
| `env-key-weak-permissions` | warn | group or others have access; suggests `chmod 600` |
| `env-key-wrong-owner` | warn | the file is not owned by the current user |
| `env-key-wrong-length` | warn | shorter than the 43-character base64url form of a 256-bit key, or empty |
| `env-key-low-entropy` | warn | long enough but repetitive (estimated under 128 bits) |
| `env-key-symlink` | error | the key path is a symlink; Hack refuses symlinked keys |
| `env-key-not-regular` | error | the key path is a directory or another non-file entry |
| `env-key-unreadable` | error | unreadable, oversized or not UTF-8 |
| `env-key-tracked` | error | a file named `.hack.secret.key` is tracked by git at any depth |
| `env-key-public-path` | error | a key copy sits in `public/`, `static/`, `dist/`, `build/`, `out/`, `www/`, `_site/`, `.next/` or `.output/` |

Hack derives the AES-256-GCM key by hashing the key text with SHA-256, so key
strength is the entropy of that text. Generated keys are 32 random bytes in base64url.
For a weak, tracked or published key, the remedy is rotation: move the key aside, then
re-add each secret with `hack env add --secret KEY VALUE` so Hack generates a new key.

CI and managed container fallback:

- if `.hack.secret.key` is missing, Hack falls back to `HACK_ENV_SECRET_KEY`
Expand Down
293 changes: 293 additions & 0 deletions scripts/dependabot-inventory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
#!/usr/bin/env bun
/**
* Read-only inventory of open Dependabot alerts for a GitHub repository.
*
* Lists each open alert's package, ecosystem, severity, manifest path,
* vulnerable range and first patched version through `gh api` with the
* caller's existing authentication. It never handles a token, sends only GET
* requests, writes no files and changes no alert, branch or dependency: use the
* output to plan dependency bumps in separate, reviewed changes.
*
* bun scripts/dependabot-inventory.ts [--repo owner/name] [--json]
*
* Diagnostics are closed and value-free: a refusal never echoes arguments or
* `gh` output.
*/

/** A closed, value-free diagnostic the inventory may print. */
export class InventoryError extends Error {}

/** Runs `gh` with the given arguments and resolves its stdout; rejects on failure. */
export type GhRunner = (args: readonly string[]) => Promise<string>;

export type AlertSeverity = "critical" | "high" | "medium" | "low" | "unknown";

export type DependabotAlert = {
readonly number: number;
readonly ecosystem: string;
readonly package: string;
readonly severity: AlertSeverity;
readonly manifest: string;
readonly vulnerableRange: string | null;
/** First patched version, or null when no fix is published. */
readonly fixedIn: string | null;
readonly advisory: string | null;
};

export type InventoryOptions = {
readonly repository: string;
readonly json: boolean;
};

export const DEFAULT_REPOSITORY = "hack-dance/hack";

const REPOSITORY_PATTERN =
/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]{1,100}$/;
const SEVERITY_ORDER: readonly AlertSeverity[] = [
"critical",
"high",
"medium",
"low",
"unknown",
];
/** Printable, bounded text from GitHub; anything else becomes a fixed placeholder. */
const PRINTABLE = /^[\x20-\x7e]{1,200}$/;

export const USAGE =
"Usage: bun scripts/dependabot-inventory.ts [--repo owner/name] [--json]\n";

export function parseInventoryArguments(
argv: readonly string[]
): InventoryOptions | "help" {
let repository = DEFAULT_REPOSITORY;
let json = false;
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--help" || arg === "-h") {
return "help";
}
if (arg === "--json") {
json = true;
continue;
}
if (arg === "--repo") {
const value = argv[index + 1];
if (value === undefined || !REPOSITORY_PATTERN.test(value)) {
throw new InventoryError("Invalid --repo value (expected owner/name)");
}
repository = value;
index += 1;
continue;
}
throw new InventoryError("Unknown argument");
}
return { repository, json };
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

function field(value: unknown, ...path: readonly string[]): unknown {
let current = value;
for (const key of path) {
if (!isRecord(current)) {
return undefined;
}
current = current[key];
}
return current;
}

function printable(value: unknown): string | null {
return typeof value === "string" && PRINTABLE.test(value) ? value : null;
}

function severityOf(value: unknown): AlertSeverity {
return value === "critical" ||
value === "high" ||
value === "medium" ||
value === "low"
? value
: "unknown";
}

/** Narrows one alert from the GitHub REST response; null when it is not an open alert. */
export function normalizeAlert(raw: unknown): DependabotAlert | null {
const number = field(raw, "number");
if (
typeof number !== "number" ||
!Number.isSafeInteger(number) ||
field(raw, "state") !== "open"
) {
return null;
}
return {
number,
ecosystem:
printable(field(raw, "dependency", "package", "ecosystem")) ?? "unknown",
package:
printable(field(raw, "dependency", "package", "name")) ?? "<unprintable>",
severity: severityOf(
field(raw, "security_vulnerability", "severity") ??
field(raw, "security_advisory", "severity")
),
manifest:
printable(field(raw, "dependency", "manifest_path")) ?? "<unprintable>",
vulnerableRange: printable(
field(raw, "security_vulnerability", "vulnerable_version_range")
),
fixedIn: printable(
field(
raw,
"security_vulnerability",
"first_patched_version",
"identifier"
)
),
advisory: printable(field(raw, "security_advisory", "ghsa_id")),
};
}

export function sortAlerts(
alerts: readonly DependabotAlert[]
): DependabotAlert[] {
return [...alerts].sort(
(left, right) =>
SEVERITY_ORDER.indexOf(left.severity) -
SEVERITY_ORDER.indexOf(right.severity) ||
left.package.localeCompare(right.package) ||
left.manifest.localeCompare(right.manifest) ||
left.number - right.number
);
}

/** Lists open alerts with GET-only, paginated `gh api` calls. */
export async function listOpenAlerts(opts: {
readonly repository: string;
readonly gh: GhRunner;
}): Promise<DependabotAlert[]> {
if (!REPOSITORY_PATTERN.test(opts.repository)) {
throw new InventoryError("Invalid repository");
}
let stdout: string;
try {
stdout = await opts.gh([
"api",
"--method",
"GET",
"--paginate",
"--jq",
".[]",
"-H",
"Accept: application/vnd.github+json",
`repos/${opts.repository}/dependabot/alerts?state=open&per_page=100`,
]);
} catch {
throw new InventoryError(
"gh api request failed (check `gh auth status` and Dependabot alert access)"
);
}
const alerts: DependabotAlert[] = [];
for (const line of stdout.split("\n")) {
if (line.trim().length === 0) {
continue;
}
let raw: unknown;
try {
raw = JSON.parse(line);
} catch {
throw new InventoryError("Unexpected GitHub response");
}
const alert = normalizeAlert(raw);
if (alert) {
alerts.push(alert);
}
}
return sortAlerts(alerts);
}

export function renderInventory(opts: {
readonly repository: string;
readonly alerts: readonly DependabotAlert[];
}): string {
const counts = SEVERITY_ORDER.map(
(severity) =>
`${severity}=${opts.alerts.filter((alert) => alert.severity === severity).length}`
).join(" ");
const lines = [
`Open Dependabot alerts for ${opts.repository}: ${opts.alerts.length} (${counts})`,
];
if (opts.alerts.length === 0) {
return `${lines.join("\n")}\n`;
}
const header = ["#", "severity", "package", "manifest", "fixed-in"];
const rows = opts.alerts.map((alert) => [
String(alert.number),
alert.severity,
`${alert.package} (${alert.ecosystem})`,
alert.manifest,
alert.fixedIn ?? "no fix",
]);
const widths = header.map((title, column) =>
Math.max(title.length, ...rows.map((row) => row[column]?.length ?? 0))
);
const format = (row: readonly string[]) =>
row
.map((cell, column) => cell.padEnd(widths[column] ?? 0))
.join(" ")
.trimEnd();
lines.push("", format(header), ...rows.map(format));
return `${lines.join("\n")}\n`;
}

/** Default runner: `gh` with the caller's authentication; stderr is discarded. */
export const ghCliRunner: GhRunner = async (args) => {
const proc = Bun.spawn(["gh", ...args], {
stdin: "ignore",
stdout: "pipe",
stderr: "ignore",
});
const text = await new Response(proc.stdout).text();
if ((await proc.exited) !== 0) {
throw new InventoryError("gh failed");
}
return text;
};

export async function runInventory(opts: {
readonly argv: readonly string[];
readonly gh: GhRunner;
readonly write: (text: string) => void;
}): Promise<number> {
const options = parseInventoryArguments(opts.argv);
if (options === "help") {
opts.write(USAGE);
return 0;
}
const alerts = await listOpenAlerts({
repository: options.repository,
gh: opts.gh,
});
opts.write(
options.json
? `${JSON.stringify({ repository: options.repository, alerts }, null, 2)}\n`
: renderInventory({ repository: options.repository, alerts })
);
return 0;
}

if (import.meta.main) {
try {
process.exitCode = await runInventory({
argv: process.argv.slice(2),
gh: ghCliRunner,
write: (text) => process.stdout.write(text),
});
} catch (error) {
process.stderr.write(
`${error instanceof InventoryError ? error.message : "Dependabot inventory failed"}\n`
);
process.exitCode = 1;
}
}
Loading
Loading