tamperward 2.14.1 → 2.15.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.
package/README.md CHANGED
@@ -225,6 +225,26 @@ but this generated GitHub-hosted authority refuses deterministically and tells y
225
225
  much outer time is required, so a custom runner/workflow can provide it instead of
226
226
  GitHub killing verification mid-verdict.
227
227
 
228
+ Since **2.15.0**, `tamperward doctor` is also the one-shot installation/posture
229
+ report promised by `init`'s security model. It reuses `init`'s canonical wiring
230
+ planner rather than maintaining a second definition of “installed correctly”, and
231
+ reports named `OK`, `WARN`, or `BROKEN` checks for policy/schema, Claude hooks,
232
+ pre-commit, CI authority wiring, CODEOWNERS, workflow permissions, binary/pin
233
+ alignment, verifier trust mode + declared inputs, platform residuals, CI verifier
234
+ outer time, observer health, and (with `--github`) repository authority. Use
235
+ `--json` for one machine-readable document with `authoritative: true|false`.
236
+
237
+ **2.15.1 closes an authority-reporting gap in that surface.** The permission check now
238
+ includes job-level overrides, not only workflow-root permissions; a policy schema newer
239
+ than the running binary is BROKEN rather than certifiable; and the exact workflow(s)
240
+ selected by `--workflow` or discovered as verifier authorities are also the workflow(s)
241
+ used for CI-wiring and token-permission posture. A custom verifier workflow therefore
242
+ does not get judged against an unrelated/missing generated file, and a second verifier
243
+ workflow with a write-scoped job cannot hide behind a safe canonical workflow.
244
+
245
+ Local early-layer gaps remain posture findings rather than silently changing the
246
+ existing generated-CI exit contract; hard CI/GitHub validation failures still exit 2.
247
+
228
248
  Legitimate exceptions are out-of-band PR labels, `tamperward:allow:<rule>@<head-sha>`.
229
249
  The workflow passes the head SHA to both steps through `TAMPERWARD_OOB_HEAD`, so an
230
250
  approval is bound to the exact commit it was granted for and a new push invalidates it.
@@ -364,7 +384,7 @@ option can never be reinterpreted as the agent command.
364
384
  | --- | --- |
365
385
  | `check` | one view — `--staged` · `--worktree` · `--diff <base>...<head>` — plus `--format text\|json\|github\|auto` (default `auto`) · `--json` (alias for `--format json`) · `--cwd <dir>` |
366
386
  | `verify` | `--base <rev>` (default `HEAD`) · `--cmd <suite command>` · `--budget <seconds>` · `--json` · `--keep` (keep the two materialised copies and report their paths) · `--require-ancestor` (refuse a base that is not an ancestor of `HEAD`) · `--cwd <dir>` |
367
- | `doctor` | `--base <rev>` (trusted policy revision) · `--workflow <path>` (default `.github/workflows/tamperward.yml`) · `--cwd <dir>` — validates every job that runs `tamperward verify` has a static outer timeout covering visible + pristine + authority reserve |
387
+ | `doctor` | `--base <rev>` (trusted policy revision) · `--workflow <path>` · `--cwd <dir>` · `--json` · `--github` · `--repo <owner/repo>` · `--branch <name>` — read-only installation/authority posture plus CI verifier outer-time validation |
368
388
  | `run` | `--base <rev>` · `--cmd <suite command>` · `--budget <seconds>` (per verifier suite) · `--agent-budget <seconds>` (optional wrapped-agent wall clock) · `--observe-transients` (start a session-scoped transient observer) · `--allow-dirty` · `--settle <seconds>` (wait before the final quiescence check) · `--allow-dep-drift` · `--cwd <dir>` · then `-- <agent command...>` |
369
389
  | `allow` | `<rule>` · `--file <path>` · `--reason "<why>"` (required) · `--cwd <dir>` |
370
390
  | `init` | `--cwd <dir>` · `--dry-run` · `--force-workflow` |
package/dist/cli/index.js CHANGED
@@ -10,6 +10,7 @@ import { join } from "node:path";
10
10
  // src/policy.ts
11
11
  import picomatch from "picomatch";
12
12
  var POLICY_FILE = ".tamperward.yml";
13
+ var POLICY_VERSION = 1;
13
14
  var BLOCK_SINCE = {};
14
15
  function applyVersionGates(rules, version, gates = BLOCK_SINCE) {
15
16
  const out2 = { ...rules };
@@ -8375,9 +8376,21 @@ import { existsSync as existsSync10, readFileSync as readFileSync13, readdirSync
8375
8376
  import { join as join13, resolve as resolve4 } from "node:path";
8376
8377
  import { parse as parse3 } from "yaml";
8377
8378
  var VERIFY_COMMAND = /\btamperward(?:@\S+)?\s+verify\b/;
8378
- function err(message) {
8379
- process.stderr.write(`tamperward doctor: ${message}
8379
+ function err(message, opts, id = "doctor", checks = []) {
8380
+ if (opts?.json) {
8381
+ const report2 = {
8382
+ command: "doctor",
8383
+ authoritative: false,
8384
+ checks: [
8385
+ ...checks.filter((check2) => check2.id !== id),
8386
+ { id, state: "BROKEN", detail: message }
8387
+ ]
8388
+ };
8389
+ process.stdout.write(JSON.stringify(report2) + "\n");
8390
+ } else {
8391
+ process.stderr.write(`tamperward doctor: ${message}
8380
8392
  `);
8393
+ }
8381
8394
  return 2;
8382
8395
  }
8383
8396
  function policyFor(opts, cwd) {
@@ -8388,6 +8401,219 @@ function policyFor(opts, cwd) {
8388
8401
  function asMapping(value) {
8389
8402
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
8390
8403
  }
8404
+ function wiringState(status, authority) {
8405
+ if (status === "ok") return "OK";
8406
+ if (status === "skip") return "WARN";
8407
+ return authority ? "BROKEN" : "WARN";
8408
+ }
8409
+ function pinsInFile(path) {
8410
+ if (!existsSync10(path)) return [];
8411
+ let src = "";
8412
+ try {
8413
+ src = readFileSync13(path, "utf8");
8414
+ } catch {
8415
+ return [];
8416
+ }
8417
+ return Array.from(src.matchAll(/\btamperward@((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))\b/g), (m) => m[1]);
8418
+ }
8419
+ function permissionWrites(value) {
8420
+ if (typeof value === "string") {
8421
+ if (/write/i.test(value)) return [value];
8422
+ if (/^(?:read-all|none)$/i.test(value)) return [];
8423
+ return null;
8424
+ }
8425
+ const mapping = asMapping(value);
8426
+ if (!mapping) return null;
8427
+ return Object.entries(mapping).filter(([, permission]) => typeof permission === "string" && /write/i.test(permission)).map(([name, permission]) => `${name}=${String(permission)}`);
8428
+ }
8429
+ function workflowPermissionCheck(cwd, workflowRels = [".github/workflows/tamperward.yml"]) {
8430
+ const broken = [];
8431
+ const warnings = [];
8432
+ for (const rel of workflowRels) {
8433
+ const path = resolve4(cwd, rel);
8434
+ if (!existsSync10(path)) {
8435
+ broken.push(`${rel} is missing`);
8436
+ continue;
8437
+ }
8438
+ let doc;
8439
+ try {
8440
+ doc = parse3(readFileSync13(path, "utf8"));
8441
+ } catch (e) {
8442
+ broken.push(`${rel} is not valid YAML (${e instanceof Error ? e.message : String(e)})`);
8443
+ continue;
8444
+ }
8445
+ const root = asMapping(doc);
8446
+ if (!root) {
8447
+ broken.push(`${rel} is not a YAML mapping`);
8448
+ continue;
8449
+ }
8450
+ const rootPermissions = root.permissions;
8451
+ if (rootPermissions === void 0) {
8452
+ broken.push(`${rel}: workflow does not declare least-privilege permissions; repository defaults could grant write access`);
8453
+ } else {
8454
+ const writes = permissionWrites(rootPermissions);
8455
+ if (writes === null) {
8456
+ broken.push(`${rel}: workflow permissions shape is unsupported`);
8457
+ } else if (writes.length) {
8458
+ broken.push(`${rel}: workflow grants write permission: ${writes.join(", ")}`);
8459
+ } else {
8460
+ const mapping = asMapping(rootPermissions);
8461
+ if (mapping && mapping.contents !== "read") {
8462
+ warnings.push(`${rel}: workflow permissions contain no writes but contents: read is not declared`);
8463
+ }
8464
+ }
8465
+ }
8466
+ const jobs = asMapping(root.jobs);
8467
+ if (!jobs) continue;
8468
+ for (const [name, rawJob] of Object.entries(jobs)) {
8469
+ const job = asMapping(rawJob);
8470
+ if (!job || job.permissions === void 0) continue;
8471
+ const writes = permissionWrites(job.permissions);
8472
+ if (writes === null) {
8473
+ broken.push(`${rel} job "${name}": permissions shape is unsupported`);
8474
+ } else if (writes.length) {
8475
+ broken.push(`${rel} job "${name}" grants write permission: ${writes.join(", ")}`);
8476
+ }
8477
+ }
8478
+ }
8479
+ if (broken.length) {
8480
+ return {
8481
+ id: "workflow-permissions",
8482
+ state: "BROKEN",
8483
+ detail: broken.join("; ")
8484
+ };
8485
+ }
8486
+ if (warnings.length) {
8487
+ return {
8488
+ id: "workflow-permissions",
8489
+ state: "WARN",
8490
+ detail: warnings.join("; ")
8491
+ };
8492
+ }
8493
+ return {
8494
+ id: "workflow-permissions",
8495
+ state: "OK",
8496
+ detail: `${workflowRels.join(", ")}: workflow/job token permissions contain no write grants and workflow contents access is read-only`
8497
+ };
8498
+ }
8499
+ function collectLocalPosture(cwd, policy, authorityWorkflows, explicitWorkflow = false) {
8500
+ const checks = [];
8501
+ checks.push(
8502
+ policy.version <= POLICY_VERSION ? { id: "policy", state: "OK", detail: `policy version ${policy.version} is understood (current schema ${POLICY_VERSION})` } : { id: "policy", state: "BROKEN", detail: `policy version ${policy.version} is newer than this binary's schema ${POLICY_VERSION}; authority cannot be certified for unknown policy semantics` }
8503
+ );
8504
+ let plan = [];
8505
+ try {
8506
+ plan = planInit(cwd);
8507
+ } catch (e) {
8508
+ checks.push({
8509
+ id: "installation-plan",
8510
+ state: "BROKEN",
8511
+ detail: `canonical wiring could not be evaluated: ${e instanceof Error ? e.message : String(e)}`
8512
+ });
8513
+ }
8514
+ const byItem = new Map(plan.map((x) => [x.item, x]));
8515
+ const addWiring = (id, item, authority) => {
8516
+ const action = byItem.get(item);
8517
+ if (!action) {
8518
+ checks.push({ id, state: authority ? "BROKEN" : "WARN", detail: `${item} wiring could not be evaluated` });
8519
+ return;
8520
+ }
8521
+ checks.push({
8522
+ id,
8523
+ state: wiringState(action.status, authority),
8524
+ detail: action.status === "ok" ? action.detail : `${action.status}: ${action.detail}`
8525
+ });
8526
+ };
8527
+ addWiring("claude-hooks", "agent", true);
8528
+ addWiring("pre-commit", "pre-commit", false);
8529
+ const canonicalWorkflow = ".github/workflows/tamperward.yml";
8530
+ if (authorityWorkflows?.length) {
8531
+ const normalized = authorityWorkflows.map((x) => x.replace(/\\/g, "/"));
8532
+ if (explicitWorkflow) {
8533
+ checks.push({
8534
+ id: "ci-wiring",
8535
+ state: "OK",
8536
+ detail: `explicit verifier authority workflow: ${normalized.join(", ")}; verifier step/timeout validated by doctor`
8537
+ });
8538
+ } else if (normalized.includes(canonicalWorkflow)) {
8539
+ addWiring("ci-wiring", "ci", true);
8540
+ } else {
8541
+ checks.push({
8542
+ id: "ci-wiring",
8543
+ state: "OK",
8544
+ detail: `discovered verifier authority workflow(s): ${normalized.join(", ")}; verifier step/timeout validated by doctor`
8545
+ });
8546
+ }
8547
+ } else {
8548
+ addWiring("ci-wiring", "ci", true);
8549
+ }
8550
+ addWiring("codeowners", "codeowners", true);
8551
+ checks.push(workflowPermissionCheck(cwd, authorityWorkflows));
8552
+ const pins = [
8553
+ ...pinsInFile(join13(cwd, ".claude", "settings.json")),
8554
+ ...pinsInFile(join13(cwd, ".git", "hooks", "pre-commit")),
8555
+ ...pinsInFile(join13(cwd, ".husky", "pre-commit"))
8556
+ ];
8557
+ const below = pins.filter((pin) => (compareVersions(pin, TW_VERSION) ?? 0) < 0);
8558
+ const above = pins.filter((pin) => (compareVersions(TW_VERSION, pin) ?? 0) < 0);
8559
+ checks.push(
8560
+ below.length ? { id: "binary-version", state: "BROKEN", detail: `wiring pins older TamperWard version(s): ${Array.from(new Set(below)).join(", ")}; running binary is ${TW_VERSION}` } : above.length ? { id: "binary-version", state: "WARN", detail: `repository wiring targets newer TamperWard version(s): ${Array.from(new Set(above)).join(", ")}; running binary is ${TW_VERSION}` } : pins.length ? { id: "binary-version", state: "OK", detail: `wiring pins agree with running TamperWard ${TW_VERSION}` } : { id: "binary-version", state: "WARN", detail: `no canonical TamperWard version pin was found; running binary is ${TW_VERSION}` }
8561
+ );
8562
+ if (!policy.verify?.command) {
8563
+ checks.push({ id: "verifier", state: "BROKEN", detail: "verify.command is not configured" });
8564
+ } else {
8565
+ const backend = policy.verify.backend ?? "local";
8566
+ const inputs = policy.verify.inputs?.length ?? 0;
8567
+ checks.push({
8568
+ id: "verifier",
8569
+ state: backend === "container" ? "OK" : "WARN",
8570
+ detail: backend === "container" ? `isolated-container verifier; budget ${policy.verify.budget}s/stage; ${inputs} declared verify.inputs glob(s); runtime/image availability is checked by verify` : `checkpointed-local verifier; budget ${policy.verify.budget}s/stage; ${inputs} declared verify.inputs glob(s); same-host/self-restoring mutation remains a documented residual`
8571
+ });
8572
+ }
8573
+ checks.push(
8574
+ process.platform === "linux" ? { id: "platform", state: "OK", detail: "Linux: POSIX generated wiring and /proc runtime/quiescence controls are available" } : process.platform === "win32" ? { id: "platform", state: "WARN", detail: "Windows: generated shell wiring has POSIX assumptions and Linux /proc survivor controls are unavailable" } : { id: "platform", state: "WARN", detail: `${process.platform}: POSIX wiring is available, but Linux /proc survivor controls are unavailable` }
8575
+ );
8576
+ return checks;
8577
+ }
8578
+ function observerCheck(cwd) {
8579
+ const observer = watcherTelemetry(defaultEventLog(cwd));
8580
+ if (observer.state === "healthy" && observer.health) {
8581
+ return {
8582
+ id: "observer",
8583
+ state: "OK",
8584
+ detail: `healthy ${observer.health.backend}; ${observer.health.watched_dirs} watched dir(s), ${observer.health.event_count} event(s); advisory telemetry only`
8585
+ };
8586
+ }
8587
+ if (observer.state === "degraded" && observer.health) {
8588
+ return {
8589
+ id: "observer",
8590
+ state: "WARN",
8591
+ detail: `degraded: ${observer.health.error_count} error(s), ${observer.health.dropped_events} dropped event(s); ${observer.reason ?? "telemetry may be incomplete"}; advisory only`
8592
+ };
8593
+ }
8594
+ return {
8595
+ id: "observer",
8596
+ state: "WARN",
8597
+ detail: `unavailable${observer.reason ? ": " + observer.reason : ""}; optional/advisory, and zero events are not evidence of no transient activity`
8598
+ };
8599
+ }
8600
+ function emitReport(opts, checks) {
8601
+ const authoritative = !checks.some((x) => x.state === "BROKEN");
8602
+ if (opts.json) {
8603
+ const report2 = { command: "doctor", authoritative, checks };
8604
+ process.stdout.write(JSON.stringify(report2) + "\n");
8605
+ return;
8606
+ }
8607
+ for (const check2 of checks) {
8608
+ if (check2.id === "observer") {
8609
+ process.stdout.write(`tamperward doctor: transient observer: ${check2.detail}
8610
+ `);
8611
+ } else {
8612
+ process.stdout.write(`tamperward doctor: [${check2.state}] ${check2.id} \u2014 ${check2.detail}
8613
+ `);
8614
+ }
8615
+ }
8616
+ }
8391
8617
  function evaluateGitHubProtection(snapshot2, requiredCheck = "tamperward") {
8392
8618
  let hasRequiredCheck = false;
8393
8619
  let hasCodeOwnerReview = false;
@@ -8570,10 +8796,21 @@ function runDoctor(opts = {}) {
8570
8796
  try {
8571
8797
  policy = policyFor(opts, cwd);
8572
8798
  } catch (e) {
8573
- return err(e instanceof PolicyError || e instanceof Error ? e.message : String(e));
8799
+ return err(
8800
+ e instanceof PolicyError || e instanceof Error ? e.message : String(e),
8801
+ opts,
8802
+ "policy"
8803
+ );
8574
8804
  }
8805
+ let postureWorkflows = opts.workflow ? [opts.workflow] : void 0;
8806
+ const fail = (id, message) => err(
8807
+ message,
8808
+ opts,
8809
+ id,
8810
+ collectLocalPosture(cwd, policy, postureWorkflows, Boolean(opts.workflow))
8811
+ );
8575
8812
  if (!policy.verify?.command) {
8576
- return err("trusted policy has no verify.command; generated CI cannot verify this repository");
8813
+ return fail("verifier", "trusted policy has no verify.command; generated CI cannot verify this repository");
8577
8814
  }
8578
8815
  const workflowRels = [];
8579
8816
  if (opts.workflow) {
@@ -8582,13 +8819,14 @@ function runDoctor(opts = {}) {
8582
8819
  const workflowDirRel = ".github/workflows";
8583
8820
  const workflowDir = resolve4(cwd, workflowDirRel);
8584
8821
  if (!existsSync10(workflowDir)) {
8585
- return err(`${workflowDirRel}: workflow directory does not exist`);
8822
+ return fail("ci-verifier", `${workflowDirRel}: workflow directory does not exist`);
8586
8823
  }
8587
8824
  let entries;
8588
8825
  try {
8589
8826
  entries = readdirSync3(workflowDir).filter((name) => /\.ya?ml$/i.test(name)).sort();
8590
8827
  } catch (e) {
8591
- return err(
8828
+ return fail(
8829
+ "ci-verifier",
8592
8830
  `${workflowDirRel}: could not enumerate workflows (${e instanceof Error ? e.message : String(e)})`
8593
8831
  );
8594
8832
  }
@@ -8597,50 +8835,62 @@ function runDoctor(opts = {}) {
8597
8835
  const requiredSecs = requiredVerifierAuthoritySeconds(policy.verify.budget);
8598
8836
  const requiredMinutes = Math.ceil(requiredSecs / 60);
8599
8837
  let verifyJobCount = 0;
8838
+ const authorityWorkflows = [];
8600
8839
  for (const workflowRel of workflowRels) {
8601
8840
  const workflowPath = resolve4(cwd, workflowRel);
8602
8841
  if (!existsSync10(workflowPath)) {
8603
- return err(`${workflowRel}: workflow does not exist`);
8842
+ return fail("ci-verifier", `${workflowRel}: workflow does not exist`);
8604
8843
  }
8605
8844
  let doc;
8606
8845
  try {
8607
8846
  doc = parse3(readFileSync13(workflowPath, "utf8"));
8608
8847
  } catch (e) {
8609
- return err(
8848
+ return fail(
8849
+ "ci-verifier",
8610
8850
  `${workflowRel}: workflow is not valid YAML (${e instanceof Error ? e.message : String(e)})`
8611
8851
  );
8612
8852
  }
8613
8853
  const jobs = verifyJobs(doc);
8614
8854
  verifyJobCount += jobs.length;
8855
+ if (jobs.length > 0) {
8856
+ authorityWorkflows.push(workflowRel);
8857
+ postureWorkflows = [...authorityWorkflows];
8858
+ }
8615
8859
  for (const { name, job } of jobs) {
8616
8860
  const timeout = job["timeout-minutes"];
8617
8861
  if (timeout === void 0 || timeout === null) {
8618
- return err(
8862
+ return fail(
8863
+ "ci-verifier",
8619
8864
  `${workflowRel} job "${name}": timeout-minutes is missing; requires at least ${requiredMinutes} minutes`
8620
8865
  );
8621
8866
  }
8622
8867
  if (typeof timeout === "string" && timeout.includes("${{")) {
8623
- return err(
8868
+ return fail(
8869
+ "ci-verifier",
8624
8870
  `${workflowRel} job "${name}": timeout-minutes must be a static numeric value; requires at least ${requiredMinutes} minutes`
8625
8871
  );
8626
8872
  }
8627
8873
  if (typeof timeout !== "number" || !Number.isFinite(timeout)) {
8628
- return err(
8874
+ return fail(
8875
+ "ci-verifier",
8629
8876
  `${workflowRel} job "${name}": timeout-minutes must be numeric; got ${JSON.stringify(timeout)}`
8630
8877
  );
8631
8878
  }
8632
8879
  if (timeout <= 0) {
8633
- return err(
8880
+ return fail(
8881
+ "ci-verifier",
8634
8882
  `${workflowRel} job "${name}": timeout-minutes must be positive; got ${timeout}`
8635
8883
  );
8636
8884
  }
8637
8885
  if (!Number.isInteger(timeout)) {
8638
- return err(
8886
+ return fail(
8887
+ "ci-verifier",
8639
8888
  `${workflowRel} job "${name}": timeout-minutes must be a whole number; got ${timeout}`
8640
8889
  );
8641
8890
  }
8642
8891
  if (timeout < requiredMinutes) {
8643
- return err(
8892
+ return fail(
8893
+ "ci-verifier",
8644
8894
  `${workflowRel} job "${name}": timeout-minutes ${timeout} is too small; trusted verify.budget ${policy.verify.budget}s requires at least ${requiredMinutes} minutes for visible + pristine + authority overhead`
8645
8895
  );
8646
8896
  }
@@ -8648,47 +8898,53 @@ function runDoctor(opts = {}) {
8648
8898
  }
8649
8899
  if (verifyJobCount === 0) {
8650
8900
  const scope = opts.workflow ?? ".github/workflows/*.yml|*.yaml";
8651
- return err(`${scope}: no job contains a tamperward verify step`);
8901
+ return fail("ci-verifier", `${scope}: no job contains a tamperward verify step`);
8652
8902
  }
8653
8903
  let github = null;
8654
8904
  if (opts.github) {
8655
8905
  try {
8656
8906
  github = githubAuthority(opts, cwd);
8657
8907
  } catch (e) {
8658
- return err(e instanceof Error ? e.message : String(e));
8908
+ return fail("github-authority", e instanceof Error ? e.message : String(e));
8659
8909
  }
8660
8910
  if (github.findings.length > 0) {
8661
- return err(
8911
+ return fail(
8912
+ "github-authority",
8662
8913
  "GitHub repository authority for " + github.repo + "#" + github.branch + " is incomplete: " + github.findings.join("; ")
8663
8914
  );
8664
8915
  }
8665
8916
  }
8666
- process.stdout.write(
8667
- `tamperward doctor: CI verifier envelope OK \u2014 ${verifyJobCount} verify job(s), trusted budget ${policy.verify.budget}s/stage, requires >=${requiredMinutes}m outer timeout.
8668
- `
8917
+ const checks = collectLocalPosture(
8918
+ cwd,
8919
+ policy,
8920
+ authorityWorkflows,
8921
+ Boolean(opts.workflow)
8669
8922
  );
8923
+ checks.push({
8924
+ id: "ci-verifier",
8925
+ state: "OK",
8926
+ detail: `${verifyJobCount} verify job(s); trusted budget ${policy.verify.budget}s/stage requires >=${requiredMinutes}m outer timeout`
8927
+ });
8670
8928
  if (github) {
8671
- process.stdout.write(
8672
- "tamperward doctor: GitHub repository authority OK \u2014 " + github.repo + "#" + github.branch + " requires tamperward, Code Owner review, and stale-review dismissal on new pushes.\n"
8673
- );
8929
+ checks.push({
8930
+ id: "github-authority",
8931
+ state: "OK",
8932
+ detail: github.repo + "#" + github.branch + " requires tamperward status, Code Owner review, and stale-review dismissal on new pushes"
8933
+ });
8674
8934
  }
8675
- const observer = watcherTelemetry(defaultEventLog(cwd));
8676
- if (observer.state === "healthy" && observer.health) {
8677
- process.stdout.write(
8678
- `tamperward doctor: transient observer: healthy \u2014 ${observer.health.backend}, ${observer.health.watched_dirs} watched dir(s), ${observer.health.event_count} event(s); advisory telemetry only, not verification authority.
8679
- `
8680
- );
8681
- } else if (observer.state === "degraded" && observer.health) {
8682
- process.stdout.write(
8683
- `tamperward doctor: transient observer: degraded \u2014 ${observer.health.error_count} error(s), ${observer.health.dropped_events} dropped event(s); ${observer.reason ?? "telemetry may be incomplete"}. Advisory only; zero events are not evidence of no transient activity.
8684
- `
8685
- );
8686
- } else {
8935
+ checks.push(observerCheck(cwd));
8936
+ if (!opts.json) {
8687
8937
  process.stdout.write(
8688
- `tamperward doctor: transient observer: unavailable${observer.reason ? " \u2014 " + observer.reason : ""}. Observer telemetry is optional/advisory; zero events are not evidence of no transient activity.
8938
+ `tamperward doctor: CI verifier envelope OK \u2014 ${verifyJobCount} verify job(s), trusted budget ${policy.verify.budget}s/stage, requires >=${requiredMinutes}m outer timeout.
8689
8939
  `
8690
8940
  );
8941
+ if (github) {
8942
+ process.stdout.write(
8943
+ "tamperward doctor: GitHub repository authority OK \u2014 " + github.repo + "#" + github.branch + " requires tamperward, Code Owner review, and stale-review dismissal on new pushes.\n"
8944
+ );
8945
+ }
8691
8946
  }
8947
+ emitReport(opts, checks);
8692
8948
  return 0;
8693
8949
  }
8694
8950
 
@@ -10852,6 +11108,7 @@ function parseDoctor(args) {
10852
11108
  else if (a === "--base") o.base = args[++i];
10853
11109
  else if (a === "--workflow") o.workflow = args[++i];
10854
11110
  else if (a === "--github") o.github = true;
11111
+ else if (a === "--json") o.json = true;
10855
11112
  else if (a === "--repo") o.repo = args[++i];
10856
11113
  else if (a === "--branch") o.branch = args[++i];
10857
11114
  }
@@ -10981,7 +11238,7 @@ function validateCliArgs(cmd, args) {
10981
11238
  }
10982
11239
  if (cmd === "doctor") {
10983
11240
  return validateFlatArgs(args, {
10984
- flags: ["--github"],
11241
+ flags: ["--github", "--json"],
10985
11242
  values: {
10986
11243
  "--cwd": "string",
10987
11244
  "--base": "string",
@@ -11068,8 +11325,9 @@ Formats:
11068
11325
  every tool the gate must see.
11069
11326
  --force-workflow replaces a workflow it
11070
11327
  did not write, or one you have edited.
11071
- tamperward doctor [--base R] validate the CI verifier's outer-time
11072
- [--workflow F] [--cwd D] envelope against the trusted policy.
11328
+ tamperward doctor [--base R] report installation + authority posture
11329
+ [--workflow F] [--cwd D] and validate the CI verifier's outer-time
11330
+ [--json] envelope against the trusted policy.
11073
11331
  [--github] [--repo O/R] --github also validates repository
11074
11332
  [--branch B] authority: required tamperward status,
11075
11333
  Code Owner review, and stale-review
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.14.1",
3
+ "version": "2.15.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",