tamperward 1.0.0 → 1.1.0
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 +20 -2
- package/dist/cli/index.js +248 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,8 +99,8 @@ with the same engine it ships.
|
|
|
99
99
|
- `src/engine.ts` — runs the enabled rules over `Change[]`; honours `policy.ignore`.
|
|
100
100
|
- `src/cli/` — `tamperward check --staged | --worktree | --diff <base>...<head>`,
|
|
101
101
|
exit 1 on any blocking finding.
|
|
102
|
-
- `test/` —
|
|
103
|
-
pre-go-live audit regression cases.
|
|
102
|
+
- `test/` — 211 tests, including the AST-vs-regex, self-hosting precision, and
|
|
103
|
+
pre-go-live audit regression cases, and the renderer accessibility contract.
|
|
104
104
|
|
|
105
105
|
- `src/adapters/claude/` + `src/cli/hook.ts` — the agent layer: `tamperward hook claude`
|
|
106
106
|
(PreToolUse deny, fail-closed) and `tamperward sweep claude` (Stop sweep, compared against
|
|
@@ -120,6 +120,24 @@ npx tamperward check --staged # pre-commit view
|
|
|
120
120
|
npx tamperward check --diff "main...HEAD" # CI view — the authority for main
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
### Reading the verdict
|
|
124
|
+
|
|
125
|
+
One verdict, rendered for whoever is reading it. `--format` picks the view; the default,
|
|
126
|
+
`auto`, picks `github` when `GITHUB_ACTIONS=true` and `text` otherwise, so the CI wiring
|
|
127
|
+
stays a single line.
|
|
128
|
+
|
|
129
|
+
| Format | Where it goes |
|
|
130
|
+
| --- | --- |
|
|
131
|
+
| `text` | The terminal. Blocking findings first, then by file and line, wrapped to the terminal width. |
|
|
132
|
+
| `github` | An inline annotation per finding — so it lands **on the line** in *Files changed*, not four clicks deep in a job log — plus a job-summary table on the run page. The full text output still goes to the log. |
|
|
133
|
+
| `json` | The findings verbatim, plus a summary count. |
|
|
134
|
+
|
|
135
|
+
Severity is always spelled out (`BLOCK` / `warn`) and never carried by colour or a glyph
|
|
136
|
+
alone, so the output reads the same piped, in a CI log, on a colour-blind reader's
|
|
137
|
+
terminal, and through a screen reader. Colour honours
|
|
138
|
+
[`NO_COLOR`](https://no-color.org) and `FORCE_COLOR`, and is off whenever stdout is not a
|
|
139
|
+
terminal.
|
|
140
|
+
|
|
123
141
|
From a clone:
|
|
124
142
|
|
|
125
143
|
```bash
|
package/dist/cli/index.js
CHANGED
|
@@ -182,7 +182,18 @@ function parseHunk(lines, start) {
|
|
|
182
182
|
|
|
183
183
|
// src/git/build.ts
|
|
184
184
|
function git(args, cwd) {
|
|
185
|
-
|
|
185
|
+
try {
|
|
186
|
+
return execFileSync("git", args, {
|
|
187
|
+
cwd,
|
|
188
|
+
encoding: "utf8",
|
|
189
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
190
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
191
|
+
});
|
|
192
|
+
} catch (e) {
|
|
193
|
+
const err = e;
|
|
194
|
+
const detail = String(err.stderr ?? "").trim();
|
|
195
|
+
throw new Error(detail ? `git ${args[0]}: ${detail}` : err.message ?? `git ${args[0]} failed`);
|
|
196
|
+
}
|
|
186
197
|
}
|
|
187
198
|
function blobAt(rev, path, cwd) {
|
|
188
199
|
try {
|
|
@@ -659,10 +670,14 @@ function underCoverageKey(node) {
|
|
|
659
670
|
}
|
|
660
671
|
return false;
|
|
661
672
|
}
|
|
673
|
+
function asExpression(src) {
|
|
674
|
+
const t = src.trim();
|
|
675
|
+
return t.startsWith("{") ? `(${t})` : src;
|
|
676
|
+
}
|
|
662
677
|
function parseThresholds(src) {
|
|
663
678
|
const res = { paths: /* @__PURE__ */ new Map(), present: false };
|
|
664
679
|
try {
|
|
665
|
-
const sf = ts2.createSourceFile("cfg.ts", src, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
|
|
680
|
+
const sf = ts2.createSourceFile("cfg.ts", asExpression(src), ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
|
|
666
681
|
const visit = (node) => {
|
|
667
682
|
if (ts2.isPropertyAssignment(node) && ts2.isObjectLiteralExpression(node.initializer)) {
|
|
668
683
|
const key2 = keyName(node.name);
|
|
@@ -1362,46 +1377,226 @@ function oobFromEnv(env = process.env) {
|
|
|
1362
1377
|
return (env.TAMPERWARD_OOB_SIGNOFF ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1363
1378
|
}
|
|
1364
1379
|
|
|
1365
|
-
// src/cli/
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1380
|
+
// src/cli/render/text.ts
|
|
1381
|
+
var ESC = "\x1B";
|
|
1382
|
+
var RESET = `${ESC}[0m`;
|
|
1383
|
+
var BOLD = `${ESC}[1m`;
|
|
1384
|
+
var DIM = `${ESC}[2m`;
|
|
1385
|
+
var RED = `${ESC}[31m`;
|
|
1386
|
+
var YELLOW = `${ESC}[33m`;
|
|
1387
|
+
var GREEN = `${ESC}[32m`;
|
|
1388
|
+
function colourEnabled(env = process.env, stream = process.stdout) {
|
|
1389
|
+
if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return false;
|
|
1390
|
+
if (env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "" && env.FORCE_COLOR !== "0") return true;
|
|
1391
|
+
if (env.TERM === "dumb") return false;
|
|
1392
|
+
return Boolean(stream.isTTY);
|
|
1393
|
+
}
|
|
1394
|
+
function terminalWidth(stream = process.stdout) {
|
|
1395
|
+
const c = stream.columns;
|
|
1396
|
+
if (!c || c < 40) return 80;
|
|
1397
|
+
return Math.min(c, 100);
|
|
1398
|
+
}
|
|
1399
|
+
function paint(s, code, on) {
|
|
1400
|
+
return on ? code + s + RESET : s;
|
|
1401
|
+
}
|
|
1402
|
+
function wrap(text, width) {
|
|
1403
|
+
const out = [];
|
|
1404
|
+
let cur = "";
|
|
1405
|
+
for (const w of text.split(/\s+/).filter(Boolean)) {
|
|
1406
|
+
if (cur === "") cur = w;
|
|
1407
|
+
else if (cur.length + 1 + w.length <= width) cur += " " + w;
|
|
1408
|
+
else {
|
|
1409
|
+
out.push(cur);
|
|
1410
|
+
cur = w;
|
|
1411
|
+
}
|
|
1371
1412
|
}
|
|
1372
|
-
|
|
1413
|
+
if (cur) out.push(cur);
|
|
1414
|
+
return out.length ? out : [""];
|
|
1415
|
+
}
|
|
1416
|
+
function clip(s, max) {
|
|
1417
|
+
const one = s.replace(/\s+/g, " ").trim();
|
|
1418
|
+
return one.length > max ? one.slice(0, Math.max(1, max - 1)) + "\u2026" : one;
|
|
1419
|
+
}
|
|
1420
|
+
function orderFindings(findings) {
|
|
1421
|
+
return [...findings].sort((a, b) => {
|
|
1422
|
+
if (a.severity !== b.severity) return a.severity === "block" ? -1 : 1;
|
|
1423
|
+
const fa = a.file ?? "";
|
|
1424
|
+
const fb = b.file ?? "";
|
|
1425
|
+
if (fa !== fb) return fa < fb ? -1 : 1;
|
|
1426
|
+
return (a.line ?? 0) - (b.line ?? 0);
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
function countsPhrase(blocks, warns) {
|
|
1430
|
+
const parts = [];
|
|
1431
|
+
if (blocks) parts.push(`${blocks} blocking`);
|
|
1432
|
+
if (warns) parts.push(`${warns} warning${warns === 1 ? "" : "s"}`);
|
|
1433
|
+
return parts.join(", ");
|
|
1434
|
+
}
|
|
1435
|
+
function scopePhrase(scanned, ignoredFiles) {
|
|
1436
|
+
const ignored = ignoredFiles > 0 ? `, ${ignoredFiles} file${ignoredFiles === 1 ? "" : "s"} ignored by policy` : "";
|
|
1437
|
+
return `${scanned} change${scanned === 1 ? "" : "s"} scanned${ignored}`;
|
|
1438
|
+
}
|
|
1439
|
+
var SIGNOFF_NOTE = "A blocking finding clears only with a human sign-off. In CI that sign-off is out-of-band \u2014 a PR label applied by a reviewer \u2014 never a file committed on the branch under review. See SPEC \xA75.4.";
|
|
1440
|
+
function renderText(input, opts) {
|
|
1441
|
+
const { scanned, ignoredFiles } = input;
|
|
1442
|
+
const findings = orderFindings(input.findings);
|
|
1443
|
+
const blocks = findings.filter((f) => f.severity === "block");
|
|
1444
|
+
const warns = findings.filter((f) => f.severity === "warn");
|
|
1445
|
+
const c = opts.colour;
|
|
1446
|
+
const out = [];
|
|
1373
1447
|
if (findings.length === 0) {
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1448
|
+
return `${paint("tamperward: clean", GREEN, c)} \u2014 no integrity findings (${scopePhrase(scanned, ignoredFiles)}).
|
|
1449
|
+
`;
|
|
1450
|
+
}
|
|
1451
|
+
const headColour = blocks.length ? RED : YELLOW;
|
|
1452
|
+
out.push(paint(`tamperward: ${countsPhrase(blocks.length, warns.length)}`, BOLD + headColour, c));
|
|
1453
|
+
out.push(paint(`(${scopePhrase(scanned, ignoredFiles)})`, DIM, c));
|
|
1454
|
+
out.push("");
|
|
1455
|
+
const LABEL = "sign-off".length;
|
|
1456
|
+
const pad = (k) => (k + " ".repeat(LABEL)).slice(0, LABEL);
|
|
1457
|
+
const body = Math.max(20, opts.width - LABEL - 6);
|
|
1458
|
+
for (const f of findings) {
|
|
1459
|
+
const isBlock = f.severity === "block";
|
|
1460
|
+
const mark = paint(isBlock ? "BLOCK" : "warn ", isBlock ? BOLD + RED : YELLOW, c);
|
|
1461
|
+
const loc = f.file ? `${f.file}${f.line ? `:${f.line}` : ""}` : "(command)";
|
|
1462
|
+
out.push(` ${mark} ${paint(f.rule, BOLD, c)} ${paint(loc, DIM, c)}`);
|
|
1463
|
+
for (const l of wrap(f.message, opts.width - 4)) out.push(` ${l}`);
|
|
1464
|
+
const row = (key2, lines) => {
|
|
1465
|
+
lines.forEach((l, i) => out.push(` ${paint(pad(i === 0 ? key2 : ""), DIM, c)} ${l}`));
|
|
1466
|
+
};
|
|
1467
|
+
row("evidence", [paint(clip(f.evidence, body), DIM, c)]);
|
|
1468
|
+
row("instead", wrap(f.remediation, body));
|
|
1469
|
+
if (f.signoff.required) row("sign-off", [f.signoff.command]);
|
|
1470
|
+
out.push("");
|
|
1377
1471
|
}
|
|
1472
|
+
if (blocks.length > 0) {
|
|
1473
|
+
out.push(
|
|
1474
|
+
wrap(SIGNOFF_NOTE, opts.width - 2).map((l) => paint(l, DIM, c)).join("\n")
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
return out.join("\n").replace(/\n+$/, "") + "\n";
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// src/cli/render/github.ts
|
|
1481
|
+
import { appendFileSync as appendFileSync2 } from "node:fs";
|
|
1482
|
+
function escData(s) {
|
|
1483
|
+
return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
1484
|
+
}
|
|
1485
|
+
function escProp(s) {
|
|
1486
|
+
return escData(s).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
1487
|
+
}
|
|
1488
|
+
function annotations(findings) {
|
|
1489
|
+
return orderFindings(findings).map((f) => {
|
|
1490
|
+
const kind = f.severity === "block" ? "error" : "warning";
|
|
1491
|
+
const props = [];
|
|
1492
|
+
if (f.file) {
|
|
1493
|
+
props.push(`file=${escProp(f.file)}`);
|
|
1494
|
+
if (f.line) props.push(`line=${f.line}`);
|
|
1495
|
+
}
|
|
1496
|
+
props.push(`title=${escProp(`tamperward: ${f.rule}`)}`);
|
|
1497
|
+
const body = `${f.message} Instead: ${f.remediation}` + (f.signoff.required ? ` Sign-off: ${f.signoff.command}` : "");
|
|
1498
|
+
return `::${kind} ${props.join(",")}::${escData(body)}`;
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
function cell(s) {
|
|
1502
|
+
return s.replace(/\s+/g, " ").replace(/\|/g, "\\|").trim();
|
|
1503
|
+
}
|
|
1504
|
+
function locationCell(f, env) {
|
|
1505
|
+
if (!f.file) return "`(command)`";
|
|
1506
|
+
const label = `${f.file}${f.line ? `:${f.line}` : ""}`;
|
|
1507
|
+
const server = env.GITHUB_SERVER_URL;
|
|
1508
|
+
const repo = env.GITHUB_REPOSITORY;
|
|
1509
|
+
const sha = env.GITHUB_SHA;
|
|
1510
|
+
if (!server || !repo || !sha) return `\`${cell(label)}\``;
|
|
1511
|
+
const frag = f.line ? `#L${f.line}` : "";
|
|
1512
|
+
return `[\`${cell(label)}\`](${server}/${repo}/blob/${sha}/${f.file}${frag})`;
|
|
1513
|
+
}
|
|
1514
|
+
function renderSummary(input, env = process.env) {
|
|
1515
|
+
const findings = orderFindings(input.findings);
|
|
1378
1516
|
const blocks = findings.filter((f) => f.severity === "block");
|
|
1379
1517
|
const warns = findings.filter((f) => f.severity === "warn");
|
|
1518
|
+
const scope = scopePhrase(input.scanned, input.ignoredFiles);
|
|
1519
|
+
if (findings.length === 0) {
|
|
1520
|
+
return `## Tamperward: clean
|
|
1521
|
+
|
|
1522
|
+
No integrity findings (${scope}).
|
|
1523
|
+
`;
|
|
1524
|
+
}
|
|
1525
|
+
const out = [
|
|
1526
|
+
`## Tamperward: ${countsPhrase(blocks.length, warns.length)}`,
|
|
1527
|
+
"",
|
|
1528
|
+
`${scope}. Every finding below is also annotated on the diff in **Files changed**.`,
|
|
1529
|
+
"",
|
|
1530
|
+
"| Severity | Rule | Location | Finding | Instead |",
|
|
1531
|
+
"| --- | --- | --- | --- | --- |"
|
|
1532
|
+
];
|
|
1380
1533
|
for (const f of findings) {
|
|
1381
|
-
const
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
`);
|
|
1386
|
-
process.stdout.write(` ${f.message}
|
|
1387
|
-
`);
|
|
1388
|
-
process.stdout.write(` evidence: ${f.evidence}
|
|
1389
|
-
`);
|
|
1390
|
-
process.stdout.write(` fix: ${f.remediation}
|
|
1391
|
-
`);
|
|
1392
|
-
if (f.signoff.required) process.stdout.write(` sign-off: ${f.signoff.command}
|
|
1393
|
-
`);
|
|
1534
|
+
const sev = f.severity === "block" ? "**BLOCK**" : "warn";
|
|
1535
|
+
out.push(
|
|
1536
|
+
`| ${sev} | \`${cell(f.rule)}\` | ${locationCell(f, env)} | ${cell(f.message)} | ${cell(f.remediation)} |`
|
|
1537
|
+
);
|
|
1394
1538
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1539
|
+
const needSignoff = findings.filter((f) => f.signoff.required);
|
|
1540
|
+
if (needSignoff.length) {
|
|
1541
|
+
out.push("", "### Clearing a blocking finding", "", SIGNOFF_NOTE, "");
|
|
1542
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1543
|
+
for (const f of needSignoff) {
|
|
1544
|
+
if (seen.has(f.signoff.command)) continue;
|
|
1545
|
+
seen.add(f.signoff.command);
|
|
1546
|
+
out.push(`- \`${cell(f.signoff.command)}\``);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return out.join("\n") + "\n";
|
|
1550
|
+
}
|
|
1551
|
+
function writeSummary(md, env = process.env) {
|
|
1552
|
+
const path = env.GITHUB_STEP_SUMMARY;
|
|
1553
|
+
if (!path) return false;
|
|
1554
|
+
try {
|
|
1555
|
+
appendFileSync2(path, md + "\n");
|
|
1556
|
+
return true;
|
|
1557
|
+
} catch {
|
|
1558
|
+
return false;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
function isGitHubActions(env = process.env) {
|
|
1562
|
+
return env.GITHUB_ACTIONS === "true";
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// src/cli/report.ts
|
|
1566
|
+
var FORMATS = ["auto", "text", "json", "github"];
|
|
1567
|
+
function isFormat(s) {
|
|
1568
|
+
return FORMATS.includes(s);
|
|
1569
|
+
}
|
|
1570
|
+
function resolveFormat(input, env = process.env) {
|
|
1571
|
+
if (input.json) return "json";
|
|
1572
|
+
if (input.format && input.format !== "auto") return input.format;
|
|
1573
|
+
return isGitHubActions(env) ? "github" : "text";
|
|
1574
|
+
}
|
|
1575
|
+
function report(input) {
|
|
1576
|
+
const { findings, scanned, ignoredFiles } = input;
|
|
1577
|
+
const format = resolveFormat(input);
|
|
1578
|
+
if (format === "json") {
|
|
1579
|
+
const blocks = findings.filter((f) => f.severity === "block").length;
|
|
1401
1580
|
process.stdout.write(
|
|
1402
|
-
|
|
1581
|
+
JSON.stringify(
|
|
1582
|
+
// `findings`, `scanned` and `ignoredFiles` keep their exact prior shape; `summary`
|
|
1583
|
+
// is additive, so an existing consumer reading only the old keys is unaffected.
|
|
1584
|
+
{ findings, scanned, ignoredFiles, summary: { block: blocks, warn: findings.length - blocks } },
|
|
1585
|
+
null,
|
|
1586
|
+
2
|
|
1587
|
+
) + "\n"
|
|
1403
1588
|
);
|
|
1589
|
+
return;
|
|
1404
1590
|
}
|
|
1591
|
+
const text = renderText(
|
|
1592
|
+
{ findings, scanned, ignoredFiles },
|
|
1593
|
+
{ colour: colourEnabled(), width: terminalWidth() }
|
|
1594
|
+
);
|
|
1595
|
+
if (format === "github") {
|
|
1596
|
+
for (const a of annotations(findings)) process.stdout.write(a + "\n");
|
|
1597
|
+
writeSummary(renderSummary({ findings, scanned, ignoredFiles }));
|
|
1598
|
+
}
|
|
1599
|
+
process.stdout.write(text);
|
|
1405
1600
|
}
|
|
1406
1601
|
|
|
1407
1602
|
// src/cli/check.ts
|
|
@@ -1439,7 +1634,7 @@ function check(opts) {
|
|
|
1439
1634
|
process.stderr.write(`tamperward: ${cleared.length} blocking finding(s) cleared by ${how}: ${cleared.map((f) => f.rule + (f.file ? `(${f.file})` : "")).join(", ")}
|
|
1440
1635
|
`);
|
|
1441
1636
|
}
|
|
1442
|
-
report({ findings, scanned: changes.length, ignoredFiles, json: opts.json });
|
|
1637
|
+
report({ findings, scanned: changes.length, ignoredFiles, json: opts.json, format: opts.format });
|
|
1443
1638
|
return hasBlocking(findings) ? 1 : 0;
|
|
1444
1639
|
}
|
|
1445
1640
|
function runCheck(opts) {
|
|
@@ -1456,7 +1651,7 @@ function runCheck(opts) {
|
|
|
1456
1651
|
}
|
|
1457
1652
|
|
|
1458
1653
|
// src/cli/hook.ts
|
|
1459
|
-
import { readFileSync as readFileSync6, appendFileSync as
|
|
1654
|
+
import { readFileSync as readFileSync6, appendFileSync as appendFileSync3 } from "node:fs";
|
|
1460
1655
|
|
|
1461
1656
|
// src/adapters/claude/changes.ts
|
|
1462
1657
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
@@ -1631,7 +1826,7 @@ function recordDenylog(blocks) {
|
|
|
1631
1826
|
const log = process.env.TAMPERWARD_DENYLOG;
|
|
1632
1827
|
if (!log) return;
|
|
1633
1828
|
try {
|
|
1634
|
-
|
|
1829
|
+
appendFileSync3(log, blocks.map((b) => b.rule).join(",") + "\n");
|
|
1635
1830
|
} catch {
|
|
1636
1831
|
}
|
|
1637
1832
|
}
|
|
@@ -1782,7 +1977,12 @@ function parseCheck(args) {
|
|
|
1782
1977
|
if (a === "--staged") o.staged = true;
|
|
1783
1978
|
else if (a === "--worktree") o.worktree = true;
|
|
1784
1979
|
else if (a === "--json") o.json = true;
|
|
1785
|
-
else if (a === "--
|
|
1980
|
+
else if (a === "--format") {
|
|
1981
|
+
const v = args[++i];
|
|
1982
|
+
if (v !== void 0 && isFormat(v)) o.format = v;
|
|
1983
|
+
else process.stderr.write(`tamperward: unknown --format "${v ?? ""}" (expected ${FORMATS.join(" | ")})
|
|
1984
|
+
`);
|
|
1985
|
+
} else if (a === "--diff") o.diff = args[++i];
|
|
1786
1986
|
else if (a === "--cwd") o.cwd = args[++i];
|
|
1787
1987
|
else {
|
|
1788
1988
|
process.stderr.write(`tamperward: unknown flag "${a}"
|
|
@@ -1798,7 +1998,18 @@ Usage:
|
|
|
1798
1998
|
tamperward check --staged check staged changes (pre-commit)
|
|
1799
1999
|
tamperward check --worktree check working-tree changes (stop sweep)
|
|
1800
2000
|
tamperward check --diff <base>...<head> check a commit range (CI authority)
|
|
1801
|
-
tamperward check ... --json machine-readable output
|
|
2001
|
+
tamperward check ... --json machine-readable output (alias for --format json)
|
|
2002
|
+
tamperward check ... --format <fmt> text | json | github | auto (default)
|
|
2003
|
+
|
|
2004
|
+
Formats:
|
|
2005
|
+
text grouped, wrapped, colour-optional terminal output. Severity is always
|
|
2006
|
+
spelled out ("BLOCK" / "warn"), never carried by colour alone. Honours
|
|
2007
|
+
NO_COLOR and FORCE_COLOR; colour is off when stdout is not a terminal.
|
|
2008
|
+
github GitHub Actions view: one inline annotation per finding (so it shows up
|
|
2009
|
+
on the line in "Files changed") plus a job-summary table on the run
|
|
2010
|
+
page, alongside the same text output in the log.
|
|
2011
|
+
json the findings verbatim, plus a summary count.
|
|
2012
|
+
auto github when GITHUB_ACTIONS=true, otherwise text.
|
|
1802
2013
|
tamperward hook claude PreToolUse gate (reads hook JSON on stdin)
|
|
1803
2014
|
tamperward sweep claude Stop sweep (re-scan the turn's working tree)
|
|
1804
2015
|
tamperward allow <rule> --reason "..." record a human sign-off (local audit ledger)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|