tamperward 2.23.0 → 2.23.2

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 +285 -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,180 @@ 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;
14821
+ const treeStatus = git5(cwd, ["status", "--porcelain", "--untracked-files=all"]) ?? "";
14822
+ const dirty = treeStatus.split("\n").filter(Boolean).map((line) => line.slice(3).split(" -> ").at(-1) ?? "");
14788
14823
  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) ?? "");
14793
- const owned = new Set(planInit(cwd).map((a) => a.path));
14794
- 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");
14803
- return 2;
14804
- }
14824
+ const count = dirty.length;
14825
+ status(
14826
+ "ACTION",
14827
+ count ? "No first commit yet; " + count + " path(s) are not part of a committed baseline. Create the initial commit before pristine verification or the safe demo." : "No first commit yet. Create the initial commit before pristine verification or the safe demo.",
14828
+ "warn"
14829
+ );
14805
14830
  } else {
14806
- out3("working tree: clean");
14831
+ const owned = new Set(planInit(cwd).map((a) => a.path));
14832
+ const foreign = dirty.filter((p) => !owned.has(p));
14833
+ if (dirty.length === 0) {
14834
+ status("OK", "Working tree is clean.", "ok");
14835
+ } else if (foreign.length === 0) {
14836
+ status("ACTION", dirty.length + " uncommitted setup file(s) from TamperWard; commit them when setup is complete.", "warn");
14837
+ } else {
14838
+ status("ACTION", dirty.length + " existing changed/untracked path(s). TamperWard will not stash or reset them.", "warn");
14839
+ if (!await confirm("Continue with the existing working-tree changes?", false, true)) {
14840
+ status("STOPPED", "Commit or stash your work, then re-run `tamperward onboard`.", "warn");
14841
+ return 2;
14842
+ }
14843
+ }
14807
14844
  }
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.");
14845
+ section(2);
14820
14846
  const plan = planInit(cwd);
14847
+ const names = {
14848
+ policy: "Policy",
14849
+ agent: "Claude hooks",
14850
+ "pre-commit": "Pre-commit",
14851
+ ci: "CI workflow",
14852
+ codeowners: "CODEOWNERS"
14853
+ };
14854
+ const renderPlan = (a) => {
14855
+ const name = (names[a.item] ?? a.item).padEnd(13);
14856
+ if (a.status === "ok") status("OK", name + " " + a.path, "ok");
14857
+ else if (a.status === "create") status("ADD", name + " " + a.path, "info");
14858
+ else if (a.status === "update") status("UPDATE", name + " " + a.path, "info");
14859
+ else if (a.status === "skip") status("ACTION", name + " " + a.path + " \u2014 " + a.detail, "warn");
14860
+ else status("ERROR", name + " " + a.path + " \u2014 " + a.detail, "bad");
14861
+ if (a.warning) status("ACTION", a.warning, "warn");
14862
+ };
14863
+ plan.forEach(renderPlan);
14821
14864
  const pending = plan.filter((a) => a.apply).length;
14822
- const errors = plan.filter((a) => a.status === "error").length;
14865
+ const planAttention = plan.filter((a) => a.status === "error" || a.status === "skip").length;
14823
14866
  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).");
14867
+ status(
14868
+ planAttention ? "ACTION" : "OK",
14869
+ planAttention ? "Local protection still needs the item(s) above." : "Local protection is already wired.",
14870
+ planAttention ? "warn" : "ok"
14871
+ );
14872
+ } else if (await confirm("Apply " + pending + " setup change(s)?", true, true)) {
14873
+ const code2 = runners.init({ cwd, quiet: true });
14874
+ const afterPlan = planInit(cwd);
14875
+ const remainingItems = new Set(afterPlan.filter((a) => a.status !== "ok").map((a) => a.item));
14876
+ wrote.push(
14877
+ ...plan.filter((a) => a.apply && !remainingItems.has(a.item)).map((a) => a.path)
14878
+ );
14879
+ const remaining = afterPlan.filter((a) => a.status !== "ok");
14880
+ if (code2 === 0 && remaining.length === 0) {
14881
+ status("OK", "Applied " + pending + " setup change(s).", "ok");
14882
+ } else {
14883
+ status("ACTION", "Applied what was safe; some setup items still need attention.", "warn");
14884
+ remaining.forEach(renderPlan);
14833
14885
  }
14886
+ } else {
14887
+ declined = true;
14888
+ status("SKIP", "No setup files were changed.", "dim");
14834
14889
  }
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;
14890
+ section(3);
14891
+ let configured;
14892
+ try {
14893
+ configured = loadPolicy(cwd).verify?.command?.trim() || void 0;
14894
+ } catch (e) {
14895
+ status("ERROR", POLICY_FILE + " does not load \u2014 " + errorMessage(e), "bad");
14896
+ }
14897
+ if (configured) {
14898
+ verifyCommand = configured;
14899
+ status("OK", "Trusted test command: " + configured, "ok");
14900
+ } else if (!declined) {
14901
+ const candidates = verifierCandidates(cwd);
14902
+ let chosen;
14903
+ if (scripted) {
14904
+ if (opts.verifyCommand?.trim()) {
14905
+ chosen = opts.verifyCommand.trim();
14874
14906
  } 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;
14907
+ status("ACTION", 'No trusted test command configured. Pass --verify-command "<suite command>".', "warn");
14877
14908
  }
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
- }
14909
+ } else if (candidates.length === 1) {
14910
+ status("FOUND", candidates[0], "info");
14911
+ if (await confirm("Trust `" + candidates[0] + "` as the verifier command?", false, false)) {
14912
+ chosen = candidates[0];
14913
+ } else {
14914
+ chosen = await ask("Test command to trust (Enter to skip): ") || void 0;
14915
+ }
14916
+ } else if (candidates.length > 1) {
14917
+ status("ACTION", "Choose the command that decides whether this repository passes:", "warn");
14918
+ candidates.forEach((candidate, i) => out3(" " + (i + 1) + ". " + terminalText(candidate)));
14919
+ const a = await ask("Choose 1-" + candidates.length + ", type a command, or press Enter to skip: ");
14920
+ const n = Number(a);
14921
+ chosen = a === "" ? void 0 : Number.isInteger(n) && n >= 1 && n <= candidates.length ? candidates[n - 1] : a;
14922
+ } else {
14923
+ status("ACTION", "No test command was detected automatically.", "warn");
14924
+ chosen = await ask("Test command to trust (Enter to skip): ") || void 0;
14925
+ }
14926
+ if (chosen) {
14927
+ const problem = writeVerifyCommand(cwd, chosen);
14928
+ if (problem) {
14929
+ status("ERROR", "verify.command was not written \u2014 " + problem, "bad");
14888
14930
  } 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\`.`);
14931
+ verifyCommand = chosen;
14932
+ wrote.push(POLICY_FILE);
14933
+ status("OK", "Saved verify.command: " + chosen + " (budget 300s).", "ok");
14891
14934
  }
14892
14935
  }
14893
14936
  }
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) => {
14937
+ if (!verifyCommand) {
14938
+ status("ACTION", "Verification is not configured; CI will fail closed until it is.", "warn");
14939
+ out3(paint2(' Set it later with: tamperward onboard --verify-command "<suite command>"', DIM2, colour));
14940
+ } else if (!localVerifySupported) {
14941
+ status("LIMITED", "Local verification is unavailable on " + platformLabel(platform) + "; use a digest-pinned container verifier.", "warn");
14942
+ } else if (!head) {
14943
+ status("ACTION", "Commit once, then run `tamperward verify`.", "warn");
14944
+ } else if (await confirm("Run the first verification now? (the suite runs twice)", true, true)) {
14945
+ let summary = null;
14946
+ const verifyOpts = {
14947
+ cwd,
14948
+ silent: true,
14949
+ onVerdict: (v) => {
14909
14950
  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.)`);
14917
- } else {
14918
- out3(`Skipped. Run it any time: tamperward verify${opts.base ? ` --base ${opts.base}` : ""}`);
14919
- }
14951
+ }
14952
+ };
14953
+ if (opts.base) verifyOpts.base = opts.base;
14954
+ const code2 = runners.verify(verifyOpts);
14955
+ verifyResult = { code: code2, summary };
14956
+ status(code2 === 0 ? "OK" : code2 === 1 ? "ACTION" : "ERROR", explainVerdict(summary, code2), code2 === 0 ? "ok" : code2 === 1 ? "warn" : "bad");
14957
+ } else {
14958
+ status("SKIP", "Verification skipped. Run `tamperward verify` when ready.", "dim");
14920
14959
  }
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.");
14960
+ if (!opts.skipDemo) {
14961
+ if (!head) {
14962
+ status("SKIP", "Safe demo becomes available after the first commit.", "dim");
14931
14963
  } else {
14932
- runDemo(cwd, head, runners, out3);
14964
+ const wanted = scripted ? Boolean(opts.demo) : opts.demo ? true : await confirm("Run the optional safe tamper demo? (temporary worktree only)", false, false);
14965
+ if (wanted) runDemo(cwd, head, runners, (line) => out3(terminalText(line)));
14933
14966
  }
14934
14967
  }
14935
- header(7);
14936
- out3("Local wiring steers the agent and gates commits; only the repository can make CI binding.");
14968
+ section(4);
14937
14969
  repo = opts.repo ?? inferGitHubRepo(cwd);
14938
- const doctorCommand = `tamperward doctor --github --repo ${repo ?? "OWNER/REPO"} --branch ${opts.branch ?? "<default-branch>"}`;
14970
+ const doctorCommand = "tamperward doctor --github --repo " + (repo ?? "OWNER/REPO") + " --branch " + (opts.branch ?? "<default-branch>");
14939
14971
  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}`);
14972
+ status("SKIP", "GitHub repository controls were not checked.", "dim");
14943
14973
  } 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}`);
14974
+ status("ACTION", "No github.com origin detected. Pass --repo OWNER/REPO to check repository protection.", "warn");
14947
14975
  } 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
- }
14976
+ status("INFO", repo + (opts.branch ? "#" + opts.branch : ""), "info");
14977
+ githubChecked = await confirm("Check GitHub branch protection now?", true, true);
14978
+ if (!githubChecked) status("SKIP", "GitHub repository controls were not checked.", "dim");
14958
14979
  }
14959
- header(8);
14980
+ section(5);
14960
14981
  const doctorOpts = { cwd };
14961
14982
  if (githubChecked && repo) {
14962
14983
  doctorOpts.github = true;
@@ -14964,48 +14985,62 @@ async function runOnboard(opts, io = {}) {
14964
14985
  if (opts.branch) doctorOpts.branch = opts.branch;
14965
14986
  }
14966
14987
  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);
14988
+ const seenFailure = /* @__PURE__ */ new Set();
14989
+ for (const check2 of doctorOutcome.checks) {
14990
+ if (check2.state === "OK" || check2.id === "observer") continue;
14991
+ seenFailure.add(check2.id);
14992
+ if (check2.id === "platform" && platform !== "linux") {
14993
+ continue;
14994
+ }
14995
+ if (check2.id === "verifier" && !verifyCommand && check2.state === "BROKEN") {
14996
+ status("ACTION", "Verification is not configured; choose the trusted test command.", "warn");
14997
+ } else {
14998
+ renderCheck(check2, out3, colour);
14999
+ }
15000
+ }
15001
+ if (doctorOutcome.failure && !seenFailure.has(doctorOutcome.failure.id)) {
15002
+ status("ACTION", doctorOutcome.failure.message, "warn");
15003
+ }
14970
15004
  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
- );
15005
+ if (posture === "READY") status("READY", "TamperWard is configured for this repository.", "ok");
15006
+ else if (posture === "READY WITH WARNINGS") status("READY", "Configured with the limitation(s) above.", "warn");
15007
+ else if (posture === "INCOMPLETE") status("INCOMPLETE", "Setup needs the action(s) above.", "warn");
15008
+ else status("BLOCKED", "Fix the broken item(s) above before relying on the gate.", "bad");
14977
15009
  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.");
15010
+ status("OK", "GitHub authority enforced for " + doctorOutcome.github.repo + "#" + doctorOutcome.github.branch + ".", "ok");
15011
+ } else if (!repo) {
15012
+ status("ACTION", "GitHub authority is not configured for this repository.", "warn");
15013
+ out3(paint2(" If GitHub will provide repository authority, run: " + terminalText(doctorCommand), DIM2, colour));
15014
+ out3(paint2(" If this repository will use GitHub CI authority, configure:", DIM2, colour));
15015
+ for (const line of MANUAL_CONTROLS) out3(paint2(" " + line, DIM2, colour));
14980
15016
  } 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
- }
15017
+ status("ACTION", "GitHub authority is not verified yet.", "warn");
15018
+ out3(paint2(" " + terminalText(doctorCommand), DIM2, colour));
15019
+ out3(paint2(" GitHub authority requires all three controls:", DIM2, colour));
15020
+ for (const line of MANUAL_CONTROLS) out3(paint2(" " + line, DIM2, colour));
15021
+ }
15022
+ const commitPaths = [...new Set(wrote)].filter((path) => committableSetupPath(cwd, path));
15023
+ if (commitPaths.length) {
15024
+ status("NEXT", "Commit repository setup files: " + commitPaths.join(", "), "info");
14986
15025
  }
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.`);
15026
+ if (!head) {
15027
+ status("NEXT", "Create the initial repository commit before verify or the safe demo.", "info");
15028
+ }
15029
+ if (!verifyCommand) {
15030
+ status("NEXT", "Choose the test command TamperWard should trust for verification.", "info");
15031
+ }
15032
+ if (verifyResult) {
15033
+ status("VERIFY", verifyResult.summary?.verdict ?? "exit " + verifyResult.code, verifyResult.code === 0 ? "ok" : "warn");
15034
+ }
15035
+ out3(paint2(" Daily: tamperward check --worktree \xB7 tamperward verify --base <base>", DIM2, colour));
15000
15036
  return posture === "READY" || posture === "READY WITH WARNINGS" ? 0 : 1;
15001
15037
  } catch (e) {
15002
15038
  if (e instanceof Aborted) {
15003
15039
  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.");
15040
+ status("STOPPED", "Onboarding was cancelled during " + (SECTIONS[sectionNo - 1] ?? "setup") + ".", "warn");
15041
+ if (wrote.length) status("INFO", "Already written: " + [...new Set(wrote)].join(", ") + ". Re-running is safe.", "dim");
15042
+ else status("INFO", "Nothing was written.", "dim");
15043
+ status("NEXT", "Re-run `tamperward onboard` to continue, or `tamperward doctor` to inspect the current setup.", "info");
15009
15044
  return 2;
15010
15045
  }
15011
15046
  throw e;
@@ -15013,14 +15048,18 @@ async function runOnboard(opts, io = {}) {
15013
15048
  prompt.asker?.close();
15014
15049
  }
15015
15050
  }
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}`);
15051
+ function renderCheck(check2, out3, colour) {
15052
+ const broken = check2.state === "BROKEN";
15053
+ const label = broken ? "ERROR" : "WARN";
15054
+ const code2 = broken ? BOLD2 + RED2 : YELLOW2;
15055
+ out3(paint2(label.padEnd(8), code2, colour) + " " + terminalText(check2.id + " \u2014 " + check2.detail));
15019
15056
  }
15020
15057
  function postureOf(outcome) {
15021
15058
  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";
15059
+ if (outcome.checks.some((c) => c.state === "BROKEN" && c.id !== "platform")) return "BROKEN";
15060
+ if (outcome.checks.some((c) => c.state === "WARN" || c.state === "BROKEN" && c.id === "platform")) {
15061
+ return "READY WITH WARNINGS";
15062
+ }
15024
15063
  return "READY";
15025
15064
  }
15026
15065
  function runDemo(cwd, head, runners, out3) {
@@ -15092,7 +15131,7 @@ function runDemo(cwd, head, runners, out3) {
15092
15131
  out3("changed it while the demo ran; inspect with `git status`.");
15093
15132
  }
15094
15133
  }
15095
- var Aborted, STEPS, MANUAL_CONTROLS, JS_TEST_PATH, JS_TEST_BLOCK;
15134
+ var Aborted, SECTIONS, MANUAL_CONTROLS, ESC2, RESET2, BOLD2, DIM2, RED2, YELLOW2, GREEN2, CYAN, JS_TEST_PATH, JS_TEST_BLOCK;
15096
15135
  var init_onboard = __esm({
15097
15136
  "src/cli/onboard.ts"() {
15098
15137
  "use strict";
@@ -15100,7 +15139,7 @@ var init_onboard = __esm({
15100
15139
  init_verify();
15101
15140
  init_doctor();
15102
15141
  init_check();
15103
- init_build();
15142
+ init_text();
15104
15143
  init_fingerprint();
15105
15144
  init_policy();
15106
15145
  init_policy_load();
@@ -15108,26 +15147,26 @@ var init_onboard = __esm({
15108
15147
  init_wiring();
15109
15148
  Aborted = class extends Error {
15110
15149
  };
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"
15150
+ SECTIONS = [
15151
+ "Environment",
15152
+ "Local protection",
15153
+ "Verification",
15154
+ "GitHub protection",
15155
+ "Summary"
15121
15156
  ];
15122
15157
  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."
15158
+ "required check: tamperward",
15159
+ "Code Owner review",
15160
+ "dismiss stale approvals when new commits are pushed"
15130
15161
  ];
15162
+ ESC2 = "\x1B";
15163
+ RESET2 = `${ESC2}[0m`;
15164
+ BOLD2 = `${ESC2}[1m`;
15165
+ DIM2 = `${ESC2}[2m`;
15166
+ RED2 = `${ESC2}[31m`;
15167
+ YELLOW2 = `${ESC2}[33m`;
15168
+ GREEN2 = `${ESC2}[32m`;
15169
+ CYAN = `${ESC2}[36m`;
15131
15170
  JS_TEST_PATH = /(?:^|\/)(?:[^/]+\.(?:test|spec)\.[cm]?[jt]sx?|__tests__\/[^/]+\.[cm]?[jt]sx?)$/;
15132
15171
  JS_TEST_BLOCK = /^(\s*)(it|test|describe)(\s*\()/m;
15133
15172
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.23.0",
3
+ "version": "2.23.2",
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",