Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,27 @@ Versioning: [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
report grew by ~1.6 ms (vitest-shaped) to ~7.8 ms (pytest-shaped) per gate run
— noise against the ~50 s gate.

- **SARIF 2.1.0 output for `clad check`** (`F-acedface`) — `clad check --format
sarif` serializes the gate result as a SARIF 2.1.0 log, so every drift finding
surfaces inline on the PR diff and in the GitHub Security tab
(`github/codeql-action/upload-sarif`) and in any SARIF viewer, instead of
living only in the terminal/`--json` view. The mapping is 1:1 with the
existing finding shape — detector → rule, severity (`error`/`warn`/`info`) →
level (`error`/`warning`/`note`), `path`+`line` → `physicalLocation` — and each
result carries a deterministic `partialFingerprints` value so re-runs
de-duplicate alerts. A blocking stage that produced no findings (a type or test
failure) still surfaces as a result, so a RED gate never serializes to a
falsely-clean SARIF. Output is deterministic (no clock/PRNG); the default text
and `--json` outputs are unchanged.

### Fixed

- **Large `--json` / `--format sarif` output is no longer truncated when piped**
— `clad check`'s machine-output modes set `process.exitCode` and let stdout
drain instead of calling `process.exit()`, which could terminate before a
buffered stdout *pipe* (>64KB) flushed. Redirecting to a file was already safe;
piping to another process now is too.

## [0.6.3] — 2026-06-26 — Honest Status

**In one line:** the one-line-per-feature index that agents grep (and that feeds
Expand Down
475 changes: 238 additions & 237 deletions plugins/claude-code/dist/clad.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ project:

# Auto-maintained by `clad sync` (F-5b9f9f). Do not edit by hand.
inventory:
features: 181
features: 182
scenarios: 2
capabilities: 5
test_files: 149
test_files: 150
last_synced: "2026-06-29"
65 changes: 33 additions & 32 deletions spec/attestation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,17 @@ attested:
F-037: 7f811c5c8bc0e8e3
F-038: 1338100beadb15a6
F-039: 2e60f3d899b72d7f
F-040: c01ba9f1336b0580
F-040: e9d61033cb4ede20
F-041: cddb50fc41e49066
F-042: d1f661281bb9fb6e
F-043: 2c5f4a94e3e57e9b
F-044: c456621089a5b109
F-044: ee02a322f833f365
F-045: b494c9a80442ac20
F-046: 4b563fce74b6bb4b
F-047: 9172bb5bf584e5e1
F-048: 3d87c28993099f01
F-048: 1c90a415fbed614b
F-049: 444a75986c1c3430
F-051: 9ccc567a8f89437f
F-051: d0a79da7f07d6b25
F-052: 61e043371b9f7c71
F-053: b9f8a88e60fd5c6d
F-054: 5f5d30500bd8cd7f
Expand All @@ -68,7 +68,7 @@ attested:
F-061: c16123610e8fe7fc
F-062: 0ab83282a7f7b1ef
F-063: 76a719993cc71fa8
F-064: 3c8fea16a4289bcc
F-064: f96f37d53967d867
F-065: e6ed3ef916201947
F-066: d6c134dbb94025f5
F-067: 6e6dbd05bf314b56
Expand All @@ -77,106 +77,107 @@ attested:
F-070: e50bb5d3addc7720
F-071: 3183a483a8015d4c
F-072: 44e1d39139c816cc
F-073: 83ebc371980602d1
F-073: 7016909eabdc2a0f
F-074: 27db161619be249e
F-075: 70fe44939bcbda65
F-075: c65fa356b7a2c4c2
F-076: ef63ce344fe4a89a
F-077: 129e697604813bc2
F-078: 334bf94b687ccedb
F-079: 6ee7fe9680f5730e
F-079: 9a09e6bd538970e0
F-080: 1c19da74d32894e6
F-081: 248f9660cfb1b02b
F-098d3b: 42d61bf806ce462b
F-09d68b: bb62c8aab14d7ead
F-09d68b: 6530c667c7caf1ca
F-0ed2db: c6417aa133389b5d
F-12d740: 84ad71574d306c81
F-16746b: 2f98a1261b9b1fc2
F-17df0a: 915d13b33258d3fc
F-18e951: df7bd67676e86ef5
F-1c9166: 59221d71617a91ba
F-1d23a6: 6431b5a84f096b93
F-1d23a6: 54f72a91fbd9f969
F-1edb38: 64283112a3ab96ce
F-24062d: 48f26bb1c77234d2
F-245bd5: a8372aeb83acc411
F-2de65d: 84ad71574d306c81
F-315fd7: 5a555f824e4bc642
F-31eeb8: d88a9880d29ae411
F-32b1e0: 752c6fc6597a1711
F-32b1e0: 0faf5d044ec90add
F-3788c2: 0c1ae99c0cd756ab
F-37b4a8: e067655bad681488
F-3a5339: 0a1979737368613b
F-3a5339: 49ff6a7b58a0bbd6
F-3b3690: 66c098ef8c25ad29
F-40327b: 394b4ecbd20ed23c
F-417ff0: 2ed14fe2c70c8509
F-417ff0: be3631fd2af36858
F-42af48: d5c989097f3ba811
F-43d8e3: bbea25941e2b675d
F-4747ef: ae95a19aa0a0311d
F-4db939: 1ee1a30f68e6fb16
F-50ff43: fac674314685a912
F-551a1c: d5da7c6d3384c80d
F-56abaa: 2cfc34ff58be51f3
F-56abaa: b69d21e41d2751d2
F-570a3f: d782269396829529
F-59f093: 26735424fba6308c
F-5b9f9f: 9b66ee02892b2c8c
F-5b9f9f: d244ddac89151d01
F-5d3ed2: 9452eac28760fb99
F-5f6b45: 15323c4f5b619de7
F-65814a: 2136c8b8c94ef535
F-67d2e9: ca0b7b032b8e045a
F-67d2e9: 29253bba4778e163
F-67e33f: b75ed84e5ec41d29
F-6d943d: 16c54797409363c1
F-6f80e7: 0c0e5b71ae22cc26
F-7076f7: 0104e51794073d4a
F-78b50d: b15718ce8ae8b640
F-7afbd4: 18ce48352bee0fce
F-7ce18e: 699b2bbed18d9b07
F-7ce18e: 7e6b081ca15b638b
F-7fa4a7: 19b7709a0b2202e3
F-80d19d: 9db68e7621f0d615
F-80d19d: 906a0e169ffe4e4c
F-836a90: d975e600851e58f5
F-8f419e: f3473746f4e252bc
F-904495a5: 3c4ab94f03c49847
F-9064ff: db581e7d99186ea6
F-904495a5: aacfda12f2d70eaf
F-9064ff: a2e7e3b888d66c57
F-94dda4: 8dfb0267c45534f9
F-95a096: c6ca03ea8b16a112
F-96700032: 8a11572c62fcdfcb
F-99c6e5: ae91823dc74c17d8
F-9a3b61: 4baa26103a280acc
F-9b643e: 334970c0f773a0f0
F-9b643e: 6da12682d1f2622b
F-9d168287: 8f9a869b35db81b1
F-a04cd9: e65d87671306d305
F-a4b512: 34ccde833d13ab83
F-a5228c: b407fc818b113c71
F-a5228c: 02b4f1b13cd6ff73
F-aa7197: 7f561e4f3c902716
F-acedface: 538fcb4afbb95c38
F-ae61c1: 9bfd87053198f4a1
F-aee1da: a6d7525a6c547877
F-aee61f: e009b9eb07addd30
F-af96b1: e75ca2cb3412a7a5
F-b2094740: f379bf4feef6771a
F-b43066: 9402b630adcf1eae
F-b61449: 7095ce00662e987d
F-b84c38: f6f9872d6ebb6424
F-b99577: 169718adb64f1207
F-b84c38: c00151997892dae5
F-b99577: 7c87b1ef91da8fde
F-ba2e05: 103982ec4919449a
F-ba4b7a: c282e0e915ed547c
F-bb15e6: aa1c717495114d0b
F-bb15e6: 9b081521c741646e
F-bd07d7: 32e18ba16bebfb28
F-bdcd90: dd2e36b95d38e4cd
F-c037ae: 82b68cbd9f6d927c
F-c2c996: 2061234fb3d860b5
F-c48eb2: 2b35fe6581096524
F-bdcd90: d77cf8e680464ee4
F-c037ae: 4398f29c458185c9
F-c2c996: 53241b7b57bb6008
F-c48eb2: f2c9124fa2547a1d
F-c4c5ae: 18200d79542ae22e
F-c8aef8: 02e07f929a1d0ded
F-cd0415: 9cf6ce40e2a8b381
F-cfba0c: 077c03b8a96f562b
F-d12edf: 0bf470bf39dbeb53
F-d2c806: 407d9bc7c62c120e
F-d2c806: 0d7932184e1304d0
F-d3bde4: 915d13b33258d3fc
F-d49585: 7f0a55feda4a1357
F-d49585: 8488a0a5038e6de7
F-d7312b: a5c30a6e29fa450f
F-d8223c: 0501e9564231899b
F-d980359c: 8f1559276afc5c03
F-dd51b42c: 496eeffa2641169d
F-dddb89: f5625354e55eba9b
F-e0f6c7: 3a140f9430a7c550
F-e0f6c7: eff7db78ddb2312a
F-eb732f: d8abb536ff850a7a
F-ef2fd9: 4da05cead2099ba1
F-f334fa: 5207f35968a0c9b2
Expand Down
48 changes: 48 additions & 0 deletions spec/features/sarif-export-acedface.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
id: F-acedface
slug: sarif-export
title: "SARIF 2.1.0 output mode for clad check (--format sarif)"
status: done
modules:
- src/stages/sarif.ts
- src/cli/clad.ts
acceptance_criteria:
- id: AC-2dfce7a7
ears: event
condition: "when clad check is invoked with --format sarif"
action: "serialize the gate result as a SARIF 2.1.0 log to stdout"
response: "runCheckStages detects opts.format==='sarif' and writes toSarif(report) — a {$schema, version:'2.1.0', runs:[{tool.driver, results}]} document"
text: "When clad check is run with --format sarif, the system shall emit a SARIF 2.1.0 log of the gate findings."
test_refs: ["tests/stages/sarif.test.ts"]
- id: AC-4686f041
ears: ubiquitous
action: "map each DriftFinding onto a SARIF result and its detector onto a reportingDescriptor (rule)"
response: "toSarif maps detector→ruleId/rule, severity→level (error|warn|info → error|warning|note), and path+line→locations[].physicalLocation"
text: "The system shall map every drift finding's detector, severity, and file location onto the corresponding SARIF result and rule fields."
test_refs: ["tests/stages/sarif.test.ts"]
- id: AC-12ff7ffa
ears: ubiquitous
action: "attach a deterministic partialFingerprints value to each result"
response: "toSarif derives a sha256-based fingerprint from the finding's stable identity (detector+path+line+message) so re-runs do not create duplicate code-scanning alerts"
text: "The system shall attach a deterministic fingerprint to each SARIF result so repeated runs de-duplicate alerts."
test_refs: ["tests/stages/sarif.test.ts"]
- id: AC-0eabdc17
ears: unwanted
condition: "if a blocking stage produced no drift findings (e.g. a type or test failure)"
action: "still surface that failure as a SARIF result keyed on the stage id"
response: "toSarif emits a synthetic result (ruleId=stage id, level=error, message=stderr) for any blocking stage with zero findings, so a RED gate never serializes to a falsely-clean SARIF"
text: "If a blocking stage produced no findings, then the system shall still emit a SARIF result for it so a failing gate is never represented as clean."
test_refs: ["tests/stages/sarif.test.ts"]
- id: AC-ca59d7c7
ears: unwanted
condition: "if --format is given an unrecognized value"
action: "fail with a clear message and a non-zero exit code"
response: "runCheckStages returns {worst:2, anyFailed:true} and pulses 'unknown --format' when opts.format is set but not 'sarif'"
text: "If --format receives an unknown value, then the system shall report the error and exit non-zero."
test_refs: ["tests/stages/sarif.test.ts"]
- id: AC-e9bddf18
ears: state
condition: "while neither --json nor --format is supplied"
action: "leave the default human-readable gate output unchanged"
response: "the SARIF/JSON branches are additive; with no machine flag the Pulse summary path runs exactly as before"
text: "While no machine-output flag is set, the system shall keep the default gate output unchanged."
test_refs: ["tests/stages/sarif.test.ts"]
1 change: 1 addition & 0 deletions spec/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ features:
F-a4b512: {slug: dependency-cycle-detector, status: done, modules: 2}
F-a5228c: {slug: attestation-marker, status: done, modules: 4}
F-aa7197: {slug: scan-audit-residuals, status: done, modules: 2}
F-acedface: {slug: sarif-export, status: done, modules: 2}
F-ae61c1: {slug: ab-tm-query-domain-fix, status: done, modules: 3}
F-aee1da: {slug: scan-residuals, status: done, modules: 3}
F-aee61f: {slug: scan-roots-from-architecture, status: done, modules: 1}
Expand Down
43 changes: 33 additions & 10 deletions src/cli/clad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {runUnit} from '../stages/unit.js';
import {runVisual} from '../stages/visual.js';
import type {DriftFinding, Disposition} from '../stages/types.js';
import {gateStatusOf, isBlocking, worstContribution, type GateStatus} from '../stages/disposition.js';
import {toSarif} from '../stages/sarif.js';
import {staleSpecification} from '../stages/detectors/stale-specification.js';
import {findLatestCheckpoint, recordCheckpoint, recordRollback} from '../core/checkpoint.js';
import {maintainDeliverable} from '../spec/deliverable-detect.js';
Expand Down Expand Up @@ -417,11 +418,19 @@ export interface CheckOutcome {
* (which gates the status flip on it), so the two verify against the SAME stage
* pipeline.
*/
export function runCheckStages(opts: {internal?: boolean; strict?: boolean; tier?: string; json?: boolean; focusModules?: readonly string[]}): CheckOutcome {
export function runCheckStages(opts: {internal?: boolean; strict?: boolean; tier?: string; json?: boolean; format?: string; version?: string; focusModules?: readonly string[]}): CheckOutcome {
const tier = opts.tier ?? 'all';
// `--format sarif` is, like `--json`, a machine-readable mode: it suppresses
// the human Pulse output and emits a single structured document at the end.
const sarif = opts.format === 'sarif';
const machine = opts.json === true || sarif;
if (opts.format !== undefined && !sarif) {
pulse('fail', 'check', `unknown --format '${opts.format}' (expected: sarif)`);
return {worst: 2, anyFailed: true};
}
const allowed = TIER_STAGES[tier];
if (!allowed) {
if (opts.json) {
if (machine) {
process.stdout.write(`${JSON.stringify({tier, error: `unknown tier '${tier}'`, worst: 2, anyFailed: true, stages: []}, null, 2)}\n`);
} else {
pulse('fail', 'check', `unknown --tier '${tier}' (expected: pre-commit | pre-push | all)`);
Expand Down Expand Up @@ -479,7 +488,7 @@ export function runCheckStages(opts: {internal?: boolean; strict?: boolean; tier
worst = Math.max(worst, worstContribution(r, status));
}
collected.push({stage: name, label, status, exitCode: r.exitCode, stderr: r.stderr, findings: r.findings});
if (!opts.json) {
if (!machine) {
pulse(pulseKindOf(status), label);
if (isBlocking(status)) printStageDetails(r);
}
Expand All @@ -498,7 +507,7 @@ export function runCheckStages(opts: {internal?: boolean; strict?: boolean; tier
worst = Math.max(worst, 1);
anyFailed = true;
collected.push({stage: v.stage, label: v.label, status: 'fail', exitCode: 1, stderr: v.message});
if (!opts.json) pulse('fail', v.label, v.message);
if (!machine) pulse('fail', v.label, v.message);
}
} catch {
/* spec unreadable → other detectors own it; don't block here */
Expand Down Expand Up @@ -528,19 +537,23 @@ export function runCheckStages(opts: {internal?: boolean; strict?: boolean; tier
drift.stderr = 'stale attestation exempted — this run re-verified and re-attests';
anyFailed = collected.some((c) => isBlocking(c.status));
worst = anyFailed ? Math.max(1, worst) : 0;
if (!opts.json) pulse('note', 'attestation', 'stale entries re-verified by this run — re-attesting');
if (!machine) pulse('note', 'attestation', 'stale entries re-verified by this run — re-attesting');
}
if (!anyFailed) {
try {
if (writeAttestation('.', loadSpec())) {
if (!opts.json) pulse('note', 'attestation', 'spec/attestation.yaml refreshed (verified tree stamped)');
if (!machine) pulse('note', 'attestation', 'spec/attestation.yaml refreshed (verified tree stamped)');
}
} catch {
/* unloadable spec → nothing to attest */
}
}
}
if (opts.json) {
if (sarif) {
// SARIF 2.1.0 — every drift finding (and any blocking stage with no
// findings) projected onto the GitHub code-scanning / SARIF-viewer surface.
process.stdout.write(`${JSON.stringify(toSarif({tier, worst, anyFailed, stages: collected}, {version: opts.version}), null, 2)}\n`);
} else if (opts.json) {
// Machine-readable, UNTRUNCATED — findings carry file/line/suggestion so an
// agent fixes in one pass instead of re-running to discover where + what.
process.stdout.write(`${JSON.stringify({tier, worst, anyFailed, stages: collected}, null, 2)}\n`);
Expand Down Expand Up @@ -568,7 +581,7 @@ export function runContextCommand(query: string): void {
}
}

export function runCheckCommand(opts: {internal?: boolean; strict?: boolean; tier?: string; json?: boolean; feature?: string}): void {
export function runCheckCommand(opts: {internal?: boolean; strict?: boolean; tier?: string; json?: boolean; format?: string; version?: string; feature?: string}): void {
let focusModules: readonly string[] | undefined;
if (opts.feature) {
// Opt-in module scope: resolve the named feature's modules. clad check
Expand All @@ -588,7 +601,14 @@ export function runCheckCommand(opts: {internal?: boolean; strict?: boolean; tie
process.exit(1);
}
}
process.exit(runCheckStages({...opts, focusModules}).worst);
// Set exitCode rather than process.exit(): the machine-output modes
// (--json, --format sarif) can write >64KB to stdout, and process.exit()
// terminates before a buffered stdout PIPE flushes — truncating the document
// for any consumer that pipes (vs. redirects to a file). Letting the event
// loop drain guarantees the full payload is emitted, then Node exits with
// this code. The gate stages are all synchronous, so nothing keeps the loop
// alive past the flush.
process.exitCode = runCheckStages({...opts, focusModules}).worst;
}

/**
Expand Down Expand Up @@ -796,8 +816,11 @@ export function createProgram(): Command {
'run only the stages for a trigger: pre-commit (drift/arch/secret) | pre-push (+ type/lint/unit/cov/spec-conformance/deliverable-smoke) | all (default; full 15-stage gate, used by CI)',
)
.option('--json', 'emit structured per-stage results (machine-readable: findings with file/line/suggestion, untruncated) — for agents/CI; cuts RED→fix round-trips')
.option('--format <format>', 'output format for results: sarif (SARIF 2.1.0 JSON for GitHub code scanning / SARIF viewers). Suppresses the human summary like --json')
.option('--feature <id>', 'scope the gate to this feature\'s modules[] (Gradle monorepos): runs only :project: tasks instead of the root aggregate. No-op for non-Gradle repos or modules-less features')
.action(runCheckCommand);
.action((opts: {internal?: boolean; strict?: boolean; tier?: string; json?: boolean; format?: string; feature?: string}) =>
runCheckCommand({...opts, version: program.version()}),
);

program
.command('checkpoint <featureId>')
Expand Down
Binary file added src/stages/sarif.ts
Binary file not shown.
Loading
Loading