tamperward 2.23.0 → 2.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli/index.js +267 -246
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -10436,7 +10436,7 @@ function planInit(cwd, opts = {}) {
10436
10436
  function runInit(opts) {
10437
10437
  const cwd = opts.cwd ?? process.cwd();
10438
10438
  const plan = planInit(cwd, { forceWorkflow: opts.forceWorkflow });
10439
- const w = process.stdout;
10439
+ const w = opts.quiet ? { write: (_text) => true } : process.stdout;
10440
10440
  let applied = 0;
10441
10441
  for (const a of plan) {
10442
10442
  if (!opts.dryRun && a.apply) {
@@ -14621,7 +14621,7 @@ import { execFileSync as execFileSync12 } from "node:child_process";
14621
14621
  import { randomBytes as randomBytes2 } from "node:crypto";
14622
14622
  import { lstatSync as lstatSync9, mkdtempSync as mkdtempSync6, readFileSync as readFileSync20, realpathSync as realpathSync9, renameSync as renameSync2, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "node:fs";
14623
14623
  import { tmpdir as tmpdir7 } from "node:os";
14624
- import { join as join22, resolve as resolve10, sep as sep5 } from "node:path";
14624
+ import { join as join22, relative as relative7, resolve as resolve10, sep as sep5 } from "node:path";
14625
14625
  import { createInterface } from "node:readline";
14626
14626
  import { parseDocument } from "yaml";
14627
14627
  function readlineAsker(input, output) {
@@ -14651,6 +14651,23 @@ function readlineAsker(input, output) {
14651
14651
  };
14652
14652
  return ask;
14653
14653
  }
14654
+ function paint2(text, code2, on) {
14655
+ return on ? code2 + text + RESET2 : text;
14656
+ }
14657
+ function terminalText(text) {
14658
+ return stripControl(text).replace(/\s+/g, " ").trim();
14659
+ }
14660
+ function committableSetupPath(cwd, path) {
14661
+ const rel = relative7(cwd, resolve10(cwd, path));
14662
+ if (!rel || rel === ".." || rel.startsWith(".." + sep5)) return false;
14663
+ return rel !== ".git" && !rel.startsWith(".git" + sep5);
14664
+ }
14665
+ function platformLabel(platform) {
14666
+ if (platform === "darwin") return "macOS";
14667
+ if (platform === "win32") return "Windows";
14668
+ if (platform === "linux") return "Linux";
14669
+ return platform;
14670
+ }
14654
14671
  function git5(cwd, args) {
14655
14672
  try {
14656
14673
  return execFileSync12("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -14662,27 +14679,13 @@ function explainVerdict(v, code2) {
14662
14679
  const verdict2 = v?.verdict ?? (code2 === 0 ? "VERIFIED" : code2 === 1 ? "SUITE_RED" : "CANNOT_VERIFY");
14663
14680
  switch (verdict2) {
14664
14681
  case "VERIFIED":
14665
- return [
14666
- "VERIFIED: your suite passes, and still passes with every protected test, snapshot and",
14667
- "config file restored from the base. Nothing was masked. Exit 0."
14668
- ];
14682
+ return "Verification passed \u2014 visible and pristine suites are green.";
14669
14683
  case "SUITE_RED":
14670
- return [
14671
- "SUITE_RED: your suite fails as it is. That is not a TamperWard finding \u2014 fix the code",
14672
- "(or the suite) first; verify exits 1 until the visible run is green."
14673
- ];
14684
+ return "Your test suite is red. Fix it, then run verification again.";
14674
14685
  case "MASKED_FAILURE":
14675
- return [
14676
- "MASKED_FAILURE: your suite passes as it is, but FAILS once the protected files are restored",
14677
- "from the base. Something weakened the checks; the code does not pass the original suite.",
14678
- "Exit 1. In CI only a reviewer label (`tamperward:allow:verify@<head-sha>`) clears it."
14679
- ];
14686
+ return "Verification blocked \u2014 the visible suite passes, but the pristine suite fails.";
14680
14687
  default:
14681
- return [
14682
- `CANNOT VERIFY${v?.reason ? ` (${v.reason})` : ""}: TamperWard could not verify this repository and fails closed`,
14683
- "(exit 2) rather than reporting success. Nothing above is a verdict about your code.",
14684
- ...v?.detail ? [` ${v.detail}`] : []
14685
- ];
14688
+ return `Could not verify${v?.reason ? ` (${v.reason})` : ""} \u2014 TamperWard failed closed${v?.detail ? `: ${v.detail}` : "."}`;
14686
14689
  }
14687
14690
  }
14688
14691
  function atomicReplaceFile(path, content, mode) {
@@ -14724,10 +14727,11 @@ function writeVerifyCommand(cwd, command) {
14724
14727
  }
14725
14728
  }
14726
14729
  async function runOnboard(opts, io = {}) {
14727
- const cwd = resolve10(opts.cwd ?? process.cwd());
14730
+ const requestedCwd = resolve10(opts.cwd ?? process.cwd());
14728
14731
  const out3 = io.out ?? ((line) => void process.stdout.write(line + "\n"));
14729
- const err3 = (line) => void process.stderr.write(line + "\n");
14732
+ const rawErr = (line) => void process.stderr.write(terminalText(line) + "\n");
14730
14733
  const platform = io.platform ?? process.platform;
14734
+ const colour = io.colour ?? colourEnabled(process.env, process.stdout);
14731
14735
  const runners = {
14732
14736
  init: runInit,
14733
14737
  verify: runVerify,
@@ -14737,35 +14741,60 @@ async function runOnboard(opts, io = {}) {
14737
14741
  };
14738
14742
  const interactive = io.interactive ?? (Boolean(process.stdin.isTTY) && !process.env.CI && !process.env.GITHUB_ACTIONS);
14739
14743
  const scripted = Boolean(opts.yes);
14744
+ const tone = (kind) => {
14745
+ if (kind === "ok") return GREEN2;
14746
+ if (kind === "warn") return YELLOW2;
14747
+ if (kind === "bad") return RED2;
14748
+ if (kind === "info") return CYAN;
14749
+ return DIM2;
14750
+ };
14751
+ const status = (label, text, kind = "info") => {
14752
+ out3(paint2(label.padEnd(8), (kind === "bad" ? BOLD2 : "") + tone(kind), colour) + " " + terminalText(text));
14753
+ };
14754
+ const fail = (text) => rawErr(paint2("ERROR ", BOLD2 + RED2, colour) + " " + text);
14740
14755
  if (!interactive && !scripted) {
14741
- err3("tamperward onboard: stdin is not interactive (or a CI environment was detected), so the guided");
14742
- err3("setup cannot ask questions. Re-run with --yes for the scripted mode (add --verify-command");
14743
- err3('"<suite command>" to configure verification; a detected command is never written unscripted),');
14744
- err3("or run the deterministic primitive directly: tamperward init.");
14756
+ fail("onboard needs an interactive terminal.");
14757
+ rawErr('Use `tamperward onboard --yes --verify-command "<suite command>"` for scripted setup,');
14758
+ rawErr("or run `tamperward init` for the deterministic wiring step only.");
14745
14759
  return 2;
14746
14760
  }
14747
- if (!isGitRepo(cwd)) {
14748
- err3(`tamperward onboard: ${cwd} is not inside a git repository \u2014 run \`git init\` first; every enforcement point is git-anchored`);
14761
+ const rootText = git5(requestedCwd, ["rev-parse", "--show-toplevel"])?.trim() ?? "";
14762
+ if (!rootText) {
14763
+ fail(requestedCwd + " is not inside a Git repository. Run `git init` first.");
14764
+ return 2;
14765
+ }
14766
+ const cwd = resolve10(rootText);
14767
+ let sameRoot = cwd === requestedCwd;
14768
+ try {
14769
+ sameRoot = realpathSync9(cwd) === realpathSync9(requestedCwd);
14770
+ } catch {
14771
+ }
14772
+ if (!sameRoot) {
14773
+ fail("current directory is not the Git repository root.");
14774
+ rawErr("Current: " + requestedCwd);
14775
+ rawErr("Git root: " + cwd);
14776
+ rawErr("TamperWard installs repository-wide hooks and CI, so it will not mix a child directory with its parent repository.");
14777
+ rawErr("Run from the Git root, or run `git init` in the child directory if it should be a separate repository.");
14749
14778
  return 2;
14750
14779
  }
14751
14780
  const prompt = { asker: null };
14752
14781
  const rawAsk = io.ask ?? ((q) => (prompt.asker ??= readlineAsker(process.stdin, process.stdout))(q));
14753
14782
  const ask = async (question) => {
14754
- const answer = await rawAsk(question);
14783
+ const answer = await rawAsk(terminalText(question) + (question.endsWith(" ") ? " " : ""));
14755
14784
  if (answer === null) throw new Aborted();
14756
14785
  return answer.trim();
14757
14786
  };
14758
14787
  const confirm = async (question, fallback, scriptedAnswer) => {
14759
14788
  if (scripted) return scriptedAnswer;
14760
- const a = (await ask(`${question} ${fallback ? "[Y/n]" : "[y/N]"} `)).toLowerCase();
14789
+ const a = (await ask(question + " " + (fallback ? "[Y/n]" : "[y/N]") + " ")).toLowerCase();
14761
14790
  if (a === "") return fallback;
14762
14791
  return a === "y" || a === "yes";
14763
14792
  };
14764
- let step = 0;
14765
- const header = (n) => {
14766
- step = n;
14793
+ let sectionNo = 0;
14794
+ const section = (n) => {
14795
+ sectionNo = n;
14767
14796
  out3("");
14768
- out3(`== ${n}/${STEPS.length} ${STEPS[n - 1]} ==`);
14797
+ out3(paint2(n + "/" + SECTIONS.length + " " + SECTIONS[n - 1], BOLD2 + CYAN, colour));
14769
14798
  };
14770
14799
  const wrote = [];
14771
14800
  let verifyCommand;
@@ -14775,188 +14804,172 @@ async function runOnboard(opts, io = {}) {
14775
14804
  let declined = false;
14776
14805
  let doctorOutcome = null;
14777
14806
  try {
14778
- header(1);
14779
- out3(`TamperWard ${TW_VERSION} on Node ${process.versions.node} (${platform}/${process.arch}), repository ${cwd}`);
14807
+ out3(paint2("TamperWard onboarding", BOLD2, colour));
14808
+ out3(paint2("v" + TW_VERSION + " \xB7 Node " + process.versions.node + " \xB7 " + platformLabel(platform) + "/" + process.arch, DIM2, colour));
14809
+ out3(paint2(terminalText(cwd), DIM2, colour));
14810
+ section(1);
14780
14811
  const lifecycle = lifecyclePlatformCheck(platform);
14781
- out3(`platform: [${lifecycle.state}] ${lifecycle.detail}`);
14782
14812
  const localVerifySupported = localVerifierShell(platform, "true") !== null;
14783
- if (!localVerifySupported) {
14784
- out3("platform: checkpointed-local verify is unsupported here; only a digest-pinned container verifier");
14785
- out3(" (verify.backend: container) can verify on this platform, exactly as the support contract states.");
14813
+ if (platform === "linux" && lifecycle.state === "OK") {
14814
+ status("OK", "Full check / verify / run support is available.", "ok");
14815
+ } else if (localVerifySupported) {
14816
+ status("LIMITED", platformLabel(platform) + ": check + verify work here; `tamperward run` requires Linux in this release.", "warn");
14817
+ } else {
14818
+ status("LIMITED", platformLabel(platform) + ": check works here; local verify and `run` are unavailable. Use the container verifier for final verification.", "warn");
14786
14819
  }
14787
14820
  const head = git5(cwd, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"])?.trim() ?? null;
14788
- if (!head) {
14789
- out3("history: no commit yet. Verification and the demonstration need a committed base; the wiring can still be written.");
14790
- }
14791
- const status = git5(cwd, ["status", "--porcelain", "--untracked-files=all"]) ?? "";
14792
- const dirty = status.split("\n").filter(Boolean).map((line) => line.slice(3).split(" -> ").at(-1) ?? "");
14821
+ if (!head) status("ACTION", "No commit yet. Commit once before pristine verification or the safe demo.", "warn");
14822
+ const treeStatus = git5(cwd, ["status", "--porcelain", "--untracked-files=all"]) ?? "";
14823
+ const dirty = treeStatus.split("\n").filter(Boolean).map((line) => line.slice(3).split(" -> ").at(-1) ?? "");
14793
14824
  const owned = new Set(planInit(cwd).map((a) => a.path));
14794
14825
  const foreign = dirty.filter((p) => !owned.has(p));
14795
- if (dirty.length && foreign.length === 0) {
14796
- out3(`working tree: ${dirty.length} uncommitted path(s), all written by init \u2014 commit them so the gate travels with the repository.`);
14797
- } else if (dirty.length) {
14798
- out3(`working tree is not clean: ${dirty.length} changed or untracked path(s). Onboarding never stashes,`);
14799
- out3("resets or edits your changes, but a clean tree makes the first verification easier to read:");
14800
- out3("commit or stash your work first for the cleanest result.");
14801
- if (!await confirm("Continue with a dirty working tree?", false, true)) {
14802
- out3("Stopped before writing anything. Commit or stash your work, then re-run: tamperward onboard");
14826
+ if (dirty.length === 0) {
14827
+ status("OK", "Working tree is clean.", "ok");
14828
+ } else if (foreign.length === 0) {
14829
+ status("ACTION", dirty.length + " uncommitted setup file(s) from TamperWard; commit them when setup is complete.", "warn");
14830
+ } else {
14831
+ status("ACTION", dirty.length + " existing changed/untracked path(s). TamperWard will not stash or reset them.", "warn");
14832
+ if (!await confirm("Continue with the existing working-tree changes?", false, true)) {
14833
+ status("STOPPED", "Commit or stash your work, then re-run `tamperward onboard`.", "warn");
14803
14834
  return 2;
14804
14835
  }
14805
- } else {
14806
- out3("working tree: clean");
14807
14836
  }
14808
- header(2);
14809
- out3("This is the plan `tamperward init` would apply (the same planner as `init --dry-run`):");
14810
- out3("");
14811
- runners.init({ cwd, dryRun: true });
14812
- out3("");
14813
- out3("What each item is for:");
14814
- out3(` policy \u2014 ${POLICY_FILE} holds your overrides; the baseline rules apply even without it.`);
14815
- out3(" agent hooks \u2014 Claude Code PreToolUse deny + Stop sweep: the steering layer, judged in the loop.");
14816
- out3(" pre-commit \u2014 the same engine over the staged diff, so a shortcut cannot be committed quietly.");
14817
- out3(" CI \u2014 the authority for the protected branch: check --diff over the PR range plus");
14818
- out3(" pristine verify; cleared only by a reviewer label bound to the head SHA.");
14819
- out3(" CODEOWNERS \u2014 a human requirement on the paths that decide whether the gate runs at all.");
14837
+ section(2);
14820
14838
  const plan = planInit(cwd);
14839
+ const names = {
14840
+ policy: "Policy",
14841
+ agent: "Claude hooks",
14842
+ "pre-commit": "Pre-commit",
14843
+ ci: "CI workflow",
14844
+ codeowners: "CODEOWNERS"
14845
+ };
14846
+ const renderPlan = (a) => {
14847
+ const name = (names[a.item] ?? a.item).padEnd(13);
14848
+ if (a.status === "ok") status("OK", name + " " + a.path, "ok");
14849
+ else if (a.status === "create") status("ADD", name + " " + a.path, "info");
14850
+ else if (a.status === "update") status("UPDATE", name + " " + a.path, "info");
14851
+ else if (a.status === "skip") status("ACTION", name + " " + a.path + " \u2014 " + a.detail, "warn");
14852
+ else status("ERROR", name + " " + a.path + " \u2014 " + a.detail, "bad");
14853
+ if (a.warning) status("ACTION", a.warning, "warn");
14854
+ };
14855
+ plan.forEach(renderPlan);
14821
14856
  const pending = plan.filter((a) => a.apply).length;
14822
- const errors = plan.filter((a) => a.status === "error").length;
14857
+ const planAttention = plan.filter((a) => a.status === "error" || a.status === "skip").length;
14823
14858
  if (pending === 0) {
14824
- out3("");
14825
- out3(errors ? "Nothing to write, but item(s) above need your attention." : "Everything is already wired \u2014 nothing to write.");
14826
- } else {
14827
- out3("");
14828
- out3("init never overwrites a file you wrote: it creates what is absent and merges what is shared.");
14829
- if (!await confirm(`Write these ${pending} change(s) now?`, false, true)) {
14830
- declined = true;
14831
- out3("Declined: nothing was written. Re-run `tamperward onboard` when ready, or apply the plan");
14832
- out3("yourself with `tamperward init` (`--dry-run` prints it again).");
14859
+ status(
14860
+ planAttention ? "ACTION" : "OK",
14861
+ planAttention ? "Local protection still needs the item(s) above." : "Local protection is already wired.",
14862
+ planAttention ? "warn" : "ok"
14863
+ );
14864
+ } else if (await confirm("Apply " + pending + " setup change(s)?", true, true)) {
14865
+ const code2 = runners.init({ cwd, quiet: true });
14866
+ const afterPlan = planInit(cwd);
14867
+ const remainingItems = new Set(afterPlan.filter((a) => a.status !== "ok").map((a) => a.item));
14868
+ wrote.push(
14869
+ ...plan.filter((a) => a.apply && !remainingItems.has(a.item)).map((a) => a.path)
14870
+ );
14871
+ const remaining = afterPlan.filter((a) => a.status !== "ok");
14872
+ if (code2 === 0 && remaining.length === 0) {
14873
+ status("OK", "Applied " + pending + " setup change(s).", "ok");
14874
+ } else {
14875
+ status("ACTION", "Applied what was safe; some setup items still need attention.", "warn");
14876
+ remaining.forEach(renderPlan);
14833
14877
  }
14878
+ } else {
14879
+ declined = true;
14880
+ status("SKIP", "No setup files were changed.", "dim");
14834
14881
  }
14835
- if (!declined && pending > 0) {
14836
- header(3);
14837
- const code2 = runners.init({ cwd });
14838
- wrote.push(...plan.filter((a) => a.apply).map((a) => a.path));
14839
- if (code2 !== 0) out3("init reported item(s) that need your attention (above); onboarding continues so you can see the rest.");
14840
- }
14841
- if (!declined) {
14842
- header(4);
14843
- let configured;
14844
- try {
14845
- configured = loadPolicy(cwd).verify?.command?.trim() || void 0;
14846
- } catch (e) {
14847
- out3(`${POLICY_FILE} does not load (${errorMessage(e)}); fix it, then re-run. verify.command was not configured.`);
14848
- }
14849
- if (configured) {
14850
- out3(`verification configured \u2014 ${configured}`);
14851
- verifyCommand = configured;
14852
- } else {
14853
- out3("The verifier command is part of the trust anchor: `verify` re-runs it against a pristine copy of");
14854
- out3("your suite, and CI fails closed without it. TamperWard never chooses it for you.");
14855
- const candidates = verifierCandidates(cwd);
14856
- let chosen;
14857
- if (scripted) {
14858
- if (opts.verifyCommand?.trim()) {
14859
- chosen = opts.verifyCommand.trim();
14860
- } else {
14861
- out3(candidates.length ? `Detected candidate(s): ${candidates.join(", ")}.` : "No suite command was detected.");
14862
- out3('Scripted mode: verify.command was not written. Pass --verify-command "<suite command>" to set it explicitly.');
14863
- }
14864
- } else if (candidates.length === 1) {
14865
- out3(`Detected one high-confidence suite command: ${candidates[0]}`);
14866
- if (await confirm(`Use \`${candidates[0]}\` as verify.command?`, false, false)) chosen = candidates[0];
14867
- else chosen = await ask("Enter the suite command to trust (empty to skip): ") || void 0;
14868
- } else if (candidates.length > 1) {
14869
- out3("Several suite commands were detected; choose the one that defines merge authority:");
14870
- candidates.forEach((c, i) => out3(` ${i + 1}. ${c}`));
14871
- const a = await ask(`Choose 1-${candidates.length} (${candidates.join(" / ")}), type a command, or press Enter to skip: `);
14872
- const n = Number(a);
14873
- chosen = a === "" ? void 0 : Number.isInteger(n) && n >= 1 && n <= candidates.length ? candidates[n - 1] : a;
14882
+ section(3);
14883
+ let configured;
14884
+ try {
14885
+ configured = loadPolicy(cwd).verify?.command?.trim() || void 0;
14886
+ } catch (e) {
14887
+ status("ERROR", POLICY_FILE + " does not load \u2014 " + errorMessage(e), "bad");
14888
+ }
14889
+ if (configured) {
14890
+ verifyCommand = configured;
14891
+ status("OK", "Trusted test command: " + configured, "ok");
14892
+ } else if (!declined) {
14893
+ const candidates = verifierCandidates(cwd);
14894
+ let chosen;
14895
+ if (scripted) {
14896
+ if (opts.verifyCommand?.trim()) {
14897
+ chosen = opts.verifyCommand.trim();
14874
14898
  } else {
14875
- out3("No suite command was detected (no test script, pytest/tox config, Cargo or Go module root).");
14876
- chosen = await ask("Enter the suite command to trust (empty to skip): ") || void 0;
14899
+ status("ACTION", 'No trusted test command configured. Pass --verify-command "<suite command>".', "warn");
14877
14900
  }
14878
- if (chosen) {
14879
- const problem = writeVerifyCommand(cwd, chosen);
14880
- if (problem) {
14881
- out3(`verify.command was not written: ${problem}`);
14882
- } else {
14883
- out3(`Wrote verify.command: ${chosen} (budget 300s) to ${POLICY_FILE}. Commit it: with a --base, verify reads`);
14884
- out3("the policy from that commit, so an uncommitted command is invisible to CI.");
14885
- verifyCommand = chosen;
14886
- wrote.push(POLICY_FILE);
14887
- }
14901
+ } else if (candidates.length === 1) {
14902
+ status("FOUND", candidates[0], "info");
14903
+ if (await confirm("Trust `" + candidates[0] + "` as the verifier command?", false, false)) {
14904
+ chosen = candidates[0];
14888
14905
  } else {
14889
- out3("verify.command was not configured. CI will fail closed (exit 2) until it is; add it to");
14890
- out3(`${POLICY_FILE} under verify: and re-run \`tamperward doctor\`.`);
14906
+ chosen = await ask("Test command to trust (Enter to skip): ") || void 0;
14891
14907
  }
14892
- }
14893
- }
14894
- if (!declined) {
14895
- header(5);
14896
- out3("`verify` runs your suite twice: as-is (visible), and with every protected test/snapshot/config");
14897
- out3("file restored from the trusted base (pristine). The pair is what no diff rule can fake.");
14898
- if (!verifyCommand) {
14899
- out3("Skipped: no verify.command. Standalone `tamperward verify` would report CANNOT_VERIFY");
14900
- out3("(exit 2) here \u2014 it fails closed rather than pretending to verify.");
14901
- } else if (!localVerifySupported) {
14902
- out3(`Skipped: checkpointed-local verify is unsupported on ${platform}; \`tamperward verify\` fails closed`);
14903
- out3("(exit 2) before running anything. Configure a digest-pinned container verifier for this platform.");
14904
- } else if (!head) {
14905
- out3("Skipped: no commit to anchor the pristine copy to. Commit, then run: tamperward verify");
14906
- } else if (await confirm(`Run the first verification now (${verifyCommand}, twice)?`, true, true)) {
14907
- let summary = null;
14908
- const verifyOpts = { cwd, onVerdict: (v) => {
14909
- summary = v;
14910
- } };
14911
- if (opts.base) verifyOpts.base = opts.base;
14912
- const code2 = runners.verify(verifyOpts);
14913
- verifyResult = { code: code2, summary };
14914
- out3("");
14915
- for (const line of explainVerdict(summary, code2)) out3(line);
14916
- out3(`(verify exited ${code2}; onboarding reports it and changes nothing about it.)`);
14908
+ } else if (candidates.length > 1) {
14909
+ status("ACTION", "Choose the command that decides whether this repository passes:", "warn");
14910
+ candidates.forEach((candidate, i) => out3(" " + (i + 1) + ". " + terminalText(candidate)));
14911
+ const a = await ask("Choose 1-" + candidates.length + ", type a command, or press Enter to skip: ");
14912
+ const n = Number(a);
14913
+ chosen = a === "" ? void 0 : Number.isInteger(n) && n >= 1 && n <= candidates.length ? candidates[n - 1] : a;
14917
14914
  } else {
14918
- out3(`Skipped. Run it any time: tamperward verify${opts.base ? ` --base ${opts.base}` : ""}`);
14915
+ status("ACTION", "No test command was detected automatically.", "warn");
14916
+ chosen = await ask("Test command to trust (Enter to skip): ") || void 0;
14917
+ }
14918
+ if (chosen) {
14919
+ const problem = writeVerifyCommand(cwd, chosen);
14920
+ if (problem) {
14921
+ status("ERROR", "verify.command was not written \u2014 " + problem, "bad");
14922
+ } else {
14923
+ verifyCommand = chosen;
14924
+ wrote.push(POLICY_FILE);
14925
+ status("OK", "Saved verify.command: " + chosen + " (budget 300s).", "ok");
14926
+ }
14919
14927
  }
14920
14928
  }
14921
- if (!declined && !opts.skipDemo) {
14922
- header(6);
14923
- out3("Optional: see a finding without risking anything. TamperWard creates a detached temporary");
14924
- out3("worktree of HEAD, adds `.skip` to one test block THERE, runs `check --worktree` on it, then");
14925
- out3("removes the worktree. Your working tree is not touched; its fingerprint is shown before and after.");
14926
- const wanted = scripted ? Boolean(opts.demo) : opts.demo ? true : await confirm("Run the demonstration?", false, false);
14927
- if (!wanted) {
14928
- out3("Demo skipped.");
14929
- } else if (!head) {
14930
- out3("Demo skipped: no commit to build disposable state from.");
14931
- } else {
14932
- runDemo(cwd, head, runners, out3);
14929
+ if (!verifyCommand) {
14930
+ status("ACTION", "Verification is not configured; CI will fail closed until it is.", "warn");
14931
+ out3(paint2(' Set it later with: tamperward onboard --verify-command "<suite command>"', DIM2, colour));
14932
+ } else if (!localVerifySupported) {
14933
+ status("LIMITED", "Local verification is unavailable on " + platformLabel(platform) + "; use a digest-pinned container verifier.", "warn");
14934
+ } else if (!head) {
14935
+ status("ACTION", "Commit once, then run `tamperward verify`.", "warn");
14936
+ } else if (await confirm("Run the first verification now? (the suite runs twice)", true, true)) {
14937
+ let summary = null;
14938
+ const verifyOpts = {
14939
+ cwd,
14940
+ silent: true,
14941
+ onVerdict: (v) => {
14942
+ summary = v;
14943
+ }
14944
+ };
14945
+ if (opts.base) verifyOpts.base = opts.base;
14946
+ const code2 = runners.verify(verifyOpts);
14947
+ verifyResult = { code: code2, summary };
14948
+ status(code2 === 0 ? "OK" : code2 === 1 ? "ACTION" : "ERROR", explainVerdict(summary, code2), code2 === 0 ? "ok" : code2 === 1 ? "warn" : "bad");
14949
+ } else {
14950
+ status("SKIP", "Verification skipped. Run `tamperward verify` when ready.", "dim");
14951
+ }
14952
+ if (!opts.skipDemo) {
14953
+ const wanted = scripted ? Boolean(opts.demo) : opts.demo ? true : await confirm("Run the optional safe tamper demo? (temporary worktree only)", false, false);
14954
+ if (wanted && head) {
14955
+ runDemo(cwd, head, runners, (line) => out3(terminalText(line)));
14956
+ } else if (wanted) {
14957
+ status("SKIP", "Demo needs at least one commit.", "dim");
14933
14958
  }
14934
14959
  }
14935
- header(7);
14936
- out3("Local wiring steers the agent and gates commits; only the repository can make CI binding.");
14960
+ section(4);
14937
14961
  repo = opts.repo ?? inferGitHubRepo(cwd);
14938
- const doctorCommand = `tamperward doctor --github --repo ${repo ?? "OWNER/REPO"} --branch ${opts.branch ?? "<default-branch>"}`;
14962
+ const doctorCommand = "tamperward doctor --github --repo " + (repo ?? "OWNER/REPO") + " --branch " + (opts.branch ?? "<default-branch>");
14939
14963
  if (opts.noGithub) {
14940
- out3("--no-github: the GitHub API is not consulted, so nothing below is reported as enforced.");
14941
- for (const line of MANUAL_CONTROLS) out3(line);
14942
- out3(`Then verify it with: ${doctorCommand}`);
14964
+ status("SKIP", "GitHub repository controls were not checked.", "dim");
14943
14965
  } else if (!repo) {
14944
- out3("No GitHub repository could be determined (no github.com origin; pass --repo OWNER/REPO).");
14945
- for (const line of MANUAL_CONTROLS) out3(line);
14946
- out3(`Then verify it with: ${doctorCommand}`);
14966
+ status("ACTION", "No github.com origin detected. Pass --repo OWNER/REPO to check repository protection.", "warn");
14947
14967
  } else {
14948
- out3(`Repository: ${repo}${opts.branch ? `, branch ${opts.branch}` : ""}.`);
14949
- githubChecked = await confirm(
14950
- "Check the live GitHub controls now with `doctor --github`? (reads the GitHub API; set GH_TOKEN/GITHUB_TOKEN if needed)",
14951
- true,
14952
- true
14953
- );
14954
- if (!githubChecked) {
14955
- for (const line of MANUAL_CONTROLS) out3(line);
14956
- out3(`Then verify it with: ${doctorCommand}`);
14957
- }
14968
+ status("INFO", repo + (opts.branch ? "#" + opts.branch : ""), "info");
14969
+ githubChecked = await confirm("Check GitHub branch protection now?", true, true);
14970
+ if (!githubChecked) status("SKIP", "GitHub repository controls were not checked.", "dim");
14958
14971
  }
14959
- header(8);
14972
+ section(5);
14960
14973
  const doctorOpts = { cwd };
14961
14974
  if (githubChecked && repo) {
14962
14975
  doctorOpts.github = true;
@@ -14964,48 +14977,51 @@ async function runOnboard(opts, io = {}) {
14964
14977
  if (opts.branch) doctorOpts.branch = opts.branch;
14965
14978
  }
14966
14979
  doctorOutcome = runners.doctor(doctorOpts);
14967
- for (const line of doctorOutcome.summary) out3(line);
14968
- if (doctorOutcome.failure) out3(`tamperward doctor: ${doctorOutcome.failure.message}`);
14969
- for (const check2 of doctorOutcome.checks) renderCheck(check2, out3);
14980
+ const seenFailure = /* @__PURE__ */ new Set();
14981
+ for (const check2 of doctorOutcome.checks) {
14982
+ if (check2.state === "OK" || check2.id === "observer") continue;
14983
+ seenFailure.add(check2.id);
14984
+ if (check2.id === "platform" && platform !== "linux") {
14985
+ const detail = localVerifySupported ? platformLabel(platform) + ": `tamperward run` requires Linux; check + verify remain available." : platformLabel(platform) + ": `tamperward run` and local verify are unavailable; check remains available.";
14986
+ status("LIMITED", detail, "warn");
14987
+ } else {
14988
+ renderCheck(check2, out3, colour);
14989
+ }
14990
+ }
14991
+ if (doctorOutcome.failure && !seenFailure.has(doctorOutcome.failure.id)) {
14992
+ status("ACTION", doctorOutcome.failure.message, "warn");
14993
+ }
14970
14994
  const posture = postureOf(doctorOutcome);
14971
- const warnings = doctorOutcome.checks.filter((c) => c.state === "WARN").length;
14972
- const broken = doctorOutcome.checks.filter((c) => c.state === "BROKEN");
14973
- out3("");
14974
- out3(
14975
- `POSTURE: ${posture}` + (posture === "READY WITH WARNINGS" ? ` \u2014 ${warnings} warning(s) above` : "") + (posture === "BROKEN" ? ` \u2014 ${broken.map((c) => c.id).join(", ")}` : "") + (posture === "INCOMPLETE" && doctorOutcome.failure ? ` \u2014 doctor could not certify: ${doctorOutcome.failure.id}` : "")
14976
- );
14995
+ if (posture === "READY") status("READY", "TamperWard is configured for this repository.", "ok");
14996
+ else if (posture === "READY WITH WARNINGS") status("READY", "Configured with the limitation(s) above.", "warn");
14997
+ else if (posture === "INCOMPLETE") status("INCOMPLETE", "Setup needs the action(s) above.", "warn");
14998
+ else status("BLOCKED", "Fix the broken item(s) above before relying on the gate.", "bad");
14977
14999
  if (doctorOutcome.github) {
14978
- out3(`GitHub repository authority: ENFORCED \u2014 doctor --github verified ${doctorOutcome.github.repo}#${doctorOutcome.github.branch}`);
14979
- out3(" requires the tamperward check, Code Owner review, and stale-approval dismissal.");
15000
+ status("OK", "GitHub authority enforced for " + doctorOutcome.github.repo + "#" + doctorOutcome.github.branch + ".", "ok");
14980
15001
  } else {
14981
- out3("GitHub repository authority: NOT VERIFIED \u2014 locally configured only. Until doctor --github");
14982
- out3(` confirms the three controls, CI is advisory. Run: ${doctorCommand}`);
14983
- if (githubChecked && doctorOutcome.failure?.id === "github-authority") {
14984
- for (const line of MANUAL_CONTROLS) out3(line);
14985
- }
15002
+ status("ACTION", "GitHub authority is not verified yet.", "warn");
15003
+ out3(paint2(" " + terminalText(doctorCommand), DIM2, colour));
15004
+ for (const line of MANUAL_CONTROLS) out3(paint2(" " + line, DIM2, colour));
15005
+ }
15006
+ const commitPaths = [...new Set(wrote)].filter((path) => committableSetupPath(cwd, path));
15007
+ if (commitPaths.length) {
15008
+ status("NEXT", "Commit repository setup files: " + commitPaths.join(", "), "info");
15009
+ }
15010
+ if (!verifyCommand) {
15011
+ status("NEXT", "Choose the test command TamperWard should trust for verification.", "info");
14986
15012
  }
14987
- if (declined) out3("Nothing was written this run; the posture above describes the repository as it was.");
14988
- if (verifyResult) out3(`First verification: ${verifyResult.summary?.verdict ?? `exit ${verifyResult.code}`}.`);
14989
- header(9);
14990
- out3("Day to day:");
14991
- out3(" tamperward check --worktree the stop-sweep view of everything changed since HEAD");
14992
- out3(` tamperward verify --base ${opts.base ?? "<base>"}${opts.base ? "" : " "}pristine re-execution against a base the agent cannot rewrite`);
14993
- out3(` tamperward run --base ${opts.base ?? "<base>"} -- <agent...> the enforcement envelope (Linux; fails closed elsewhere)`);
14994
- out3(` ${doctorCommand}`);
14995
- out3("When the final verification matters most \u2014 a release, a merge into the protected branch \u2014 prefer");
14996
- out3("the digest-pinned container verifier (verify.backend: container, image: <ref>@sha256:<digest>):");
14997
- out3("it owns the runtime and dependencies, mounts the tree read-only and disables the network, so the");
14998
- out3("candidate cannot reach the verifier from the same host. `run` deliberately does not offer it.");
14999
- if (wrote.length) out3(`Commit what onboarding wrote (${[...new Set(wrote)].join(", ")}) so the gate travels with the repository.`);
15013
+ if (verifyResult) {
15014
+ status("VERIFY", verifyResult.summary?.verdict ?? "exit " + verifyResult.code, verifyResult.code === 0 ? "ok" : "warn");
15015
+ }
15016
+ out3(paint2(" Daily: tamperward check --worktree \xB7 tamperward verify --base <base>", DIM2, colour));
15000
15017
  return posture === "READY" || posture === "READY WITH WARNINGS" ? 0 : 1;
15001
15018
  } catch (e) {
15002
15019
  if (e instanceof Aborted) {
15003
15020
  out3("");
15004
- out3(`tamperward onboard: aborted during step ${step} (${STEPS[step - 1]}).`);
15005
- out3(
15006
- wrote.length ? `Written so far: ${[...new Set(wrote)].join(", ")} \u2014 all through init, all idempotent; nothing is half-applied.` : "Nothing was written."
15007
- );
15008
- out3("Re-run `tamperward onboard` to continue from the same place, or `tamperward doctor` to see the current posture.");
15021
+ status("STOPPED", "Onboarding was cancelled during " + (SECTIONS[sectionNo - 1] ?? "setup") + ".", "warn");
15022
+ if (wrote.length) status("INFO", "Already written: " + [...new Set(wrote)].join(", ") + ". Re-running is safe.", "dim");
15023
+ else status("INFO", "Nothing was written.", "dim");
15024
+ status("NEXT", "Re-run `tamperward onboard` to continue, or `tamperward doctor` to inspect the current setup.", "info");
15009
15025
  return 2;
15010
15026
  }
15011
15027
  throw e;
@@ -15013,14 +15029,18 @@ async function runOnboard(opts, io = {}) {
15013
15029
  prompt.asker?.close();
15014
15030
  }
15015
15031
  }
15016
- function renderCheck(check2, out3) {
15017
- if (check2.id === "observer") out3(`tamperward doctor: transient observer: ${check2.detail}`);
15018
- else out3(`tamperward doctor: [${check2.state}] ${check2.id} \u2014 ${check2.detail}`);
15032
+ function renderCheck(check2, out3, colour) {
15033
+ const broken = check2.state === "BROKEN";
15034
+ const label = broken ? "ERROR" : "WARN";
15035
+ const code2 = broken ? BOLD2 + RED2 : YELLOW2;
15036
+ out3(paint2(label.padEnd(8), code2, colour) + " " + terminalText(check2.id + " \u2014 " + check2.detail));
15019
15037
  }
15020
15038
  function postureOf(outcome) {
15021
15039
  if (outcome.failure) return "INCOMPLETE";
15022
- if (outcome.checks.some((c) => c.state === "BROKEN")) return "BROKEN";
15023
- if (outcome.checks.some((c) => c.state === "WARN")) return "READY WITH WARNINGS";
15040
+ if (outcome.checks.some((c) => c.state === "BROKEN" && c.id !== "platform")) return "BROKEN";
15041
+ if (outcome.checks.some((c) => c.state === "WARN" || c.state === "BROKEN" && c.id === "platform")) {
15042
+ return "READY WITH WARNINGS";
15043
+ }
15024
15044
  return "READY";
15025
15045
  }
15026
15046
  function runDemo(cwd, head, runners, out3) {
@@ -15092,7 +15112,7 @@ function runDemo(cwd, head, runners, out3) {
15092
15112
  out3("changed it while the demo ran; inspect with `git status`.");
15093
15113
  }
15094
15114
  }
15095
- var Aborted, STEPS, MANUAL_CONTROLS, JS_TEST_PATH, JS_TEST_BLOCK;
15115
+ var Aborted, SECTIONS, MANUAL_CONTROLS, ESC2, RESET2, BOLD2, DIM2, RED2, YELLOW2, GREEN2, CYAN, JS_TEST_PATH, JS_TEST_BLOCK;
15096
15116
  var init_onboard = __esm({
15097
15117
  "src/cli/onboard.ts"() {
15098
15118
  "use strict";
@@ -15100,7 +15120,7 @@ var init_onboard = __esm({
15100
15120
  init_verify();
15101
15121
  init_doctor();
15102
15122
  init_check();
15103
- init_build();
15123
+ init_text();
15104
15124
  init_fingerprint();
15105
15125
  init_policy();
15106
15126
  init_policy_load();
@@ -15108,26 +15128,27 @@ var init_onboard = __esm({
15108
15128
  init_wiring();
15109
15129
  Aborted = class extends Error {
15110
15130
  };
15111
- STEPS = [
15112
- "Preflight",
15113
- "Preview the installation",
15114
- "Initialize",
15115
- "Configure verification",
15116
- "First verification",
15117
- "Safe demonstration",
15118
- "GitHub repository authority",
15119
- "Posture",
15120
- "Next steps"
15131
+ SECTIONS = [
15132
+ "Environment",
15133
+ "Local protection",
15134
+ "Verification",
15135
+ "GitHub protection",
15136
+ "Summary"
15121
15137
  ];
15122
15138
  MANUAL_CONTROLS = [
15123
- "In the branch-protection or ruleset settings of the protected branch, configure ALL THREE:",
15124
- " 1. require the `tamperward` status check;",
15125
- ' 2. enable "Require review from Code Owners";',
15126
- ' 3. enable "Dismiss stale pull request approvals when new commits are pushed".',
15127
- "Until all three are enforced the generated CI gate is advisory: a pull request runs the",
15128
- "workflow from its own head and a required check is matched by job name, so it could keep",
15129
- "the job called `tamperward`, replace the gate with `true`, and report green."
15139
+ "GitHub still needs three repository controls:",
15140
+ " required check: tamperward",
15141
+ " Code Owner review",
15142
+ " dismiss stale approvals when new commits are pushed"
15130
15143
  ];
15144
+ ESC2 = "\x1B";
15145
+ RESET2 = `${ESC2}[0m`;
15146
+ BOLD2 = `${ESC2}[1m`;
15147
+ DIM2 = `${ESC2}[2m`;
15148
+ RED2 = `${ESC2}[31m`;
15149
+ YELLOW2 = `${ESC2}[33m`;
15150
+ GREEN2 = `${ESC2}[32m`;
15151
+ CYAN = `${ESC2}[36m`;
15131
15152
  JS_TEST_PATH = /(?:^|\/)(?:[^/]+\.(?:test|spec)\.[cm]?[jt]sx?|__tests__\/[^/]+\.[cm]?[jt]sx?)$/;
15132
15153
  JS_TEST_BLOCK = /^(\s*)(it|test|describe)(\s*\()/m;
15133
15154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.23.0",
3
+ "version": "2.23.1",
4
4
  "description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "hexrift",