Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ coverage/
tests/e2e/.env.test
.vitest-cache/
.claude/
.quality-local/
10 changes: 8 additions & 2 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
"rules": {
"no-unused-vars": "warn",
"no-console": "off"
"no-console": "off",
"no-debugger": "error",
"no-dupe-keys": "error"
},
"ignorePatterns": ["dist", "node_modules", "src/generated"]
"ignorePatterns": [
"dist",
"node_modules",
"src/generated"
]
}
18 changes: 18 additions & 0 deletions .quality/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# CLI quality checks

Install the CI development toolchain: Node 22, Bun 1.3.8, npm 11.12.1, then `bun install --frozen-lockfile`. These development requirements do not change the published CLI's Node >=20 engine. Runtime checks use only this checkout and its installed tools; no toolkit checkout or global TypeScript is needed.

| Command | Scope |
| --- | --- |
| `node .quality/run.mjs fast` | Changed JSON syntax and JS/TS Oxlint, read-only, maximum 30 seconds |
| `node .quality/run.mjs affected --base <SHA>` | Toolchain, runner regressions, types, lint, helper packaging validation and all top-level unit/integration tests |
| `node .quality/run.mjs gate` | All affected checks, Node build, standalone binary build and binary/package smoke tests |
| `node .quality/run.mjs risk` | Reports unavailable private release evidence; no production calls |

The `quality:*` Bun scripts call the same entry points. Without `--base`, changed paths are relative to HEAD and include index/worktree/untracked files. This single-package CLI conservatively checks its complete affected scope. Gate always runs anew, including in a clean checkout. `--check <id>` is a partial check and never issues a full gate receipt.

Existing Linux CI retains its native steps, matching every command in the local gate. The top-level Bun suite includes `tests/quality-loop.test.ts`, which runs the four real adapter regressions. Existing Windows Bun tests/builds/smoke remain native: quality-loop 1.0.0 process-group cancellation supports macOS/Linux. Existing helper validation and release evidence gates remain required. The runner does not send email, call models, publish packages or activate agent hooks. Private CLI/API coordinated evidence remains a separate release prerequisite.

Nonzero exits, missing tools, empty test discovery, zero passing tests, timeouts, busy locks and files changed during a run are incomplete results. Full logs and JSON receipts are in ignored `.quality-local/`; do not commit raw logs or use a historical receipt as today's proof. Source files are never autofixed. The gate retains existing Oxlint warnings and additionally rejects debugger statements and duplicate object keys.

The dependency-free runtime is a controlled copy of `@przeprogramowani/quality-loop` 1.0.0. `.quality/manifest.json` checks its hashes before execution; provenance is recorded in this change's source manifest. From an explicitly selected source package run `node sync.mjs /absolute/consumer` or add `--check` to compare bytes. The source package is separately prepared in Toolkit and has not yet landed on its master. The consumer is runnable without it. Updates require a reviewed source diff, matching runtime hashes and fresh gates. Revert the integration commit to remove it; no global configuration is installed.
44 changes: 44 additions & 0 deletions .quality/builtin.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as fs from "node:fs";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { rootFor, git } from "./run.mjs";
const root = rootFor();
const mode = process.argv[2] || "syntax";
let files = process.argv.slice(3);
if (!files.length) files = git(root, ["ls-files", "-z"]).toString().split("\0").filter(Boolean);
let checked = 0,
failed = 0;
for (const f of files) {
const full = path.resolve(process.cwd(), f);
if (!full.startsWith(root + path.sep)) throw new Error("Path outside worktree");
if (!fs.existsSync(full) || !fs.statSync(full).isFile()) continue;
const ext = path.extname(full);
try {
if (ext === ".json") JSON.parse(fs.readFileSync(full, "utf8"));
else if ([".mjs", ".cjs", ".js"].includes(ext) && mode === "syntax")
execFileSync(process.execPath, ["--check", full], { stdio: "pipe" });
else if (ext === ".py" && mode === "syntax")
execFileSync(
"python3",
[
"-c",
'import ast,sys; ast.parse(open(sys.argv[1], encoding="utf-8").read(), filename=sys.argv[1])',
full,
],
{ stdio: "pipe" },
);
else if (ext === ".sh" && mode === "syntax")
execFileSync("bash", ["-n", full], { stdio: "pipe" });
else if (ext === ".md" && mode === "docs") {
if (!fs.readFileSync(full, "utf8").trim()) throw new Error("Empty Markdown document");
} else continue;
checked++;
} catch (e) {
failed++;
console.error(`${f}: ${e.stderr?.toString() || e.message}`);
}
}
console.log(
`${checked} files structurally checked; ${failed} failed. This is syntax/document validation, not a test suite.`,
);
process.exitCode = failed ? 1 : checked ? 0 : 3;
173 changes: 173 additions & 0 deletions .quality/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
{
"schemaVersion": 1,
"repo": "10x-cli",
"nodeMajors": [
22
],
"units": [],
"checks": [
{
"id": "toolchain",
"command": [
"node",
".quality/native.mjs",
"toolchain"
],
"levels": [
"affected",
"gate"
],
"timeoutMs": 30000
},
{
"id": "config-changed",
"command": [
"node",
".quality/builtin.mjs",
"syntax"
],
"levels": [
"fast"
],
"filePattern": "\\.json$",
"appendFiles": true,
"timeoutMs": 30000
},
{
"id": "lint-changed",
"command": [
"bun",
"run",
"--bun",
"node_modules/oxlint/bin/oxlint"
],
"cwd": ".",
"levels": [
"fast"
],
"timeoutMs": 120000,
"next": "Fix the reported diagnostics and rerun this check.",
"filePattern": "\\.(?:[cm]?[jt]sx?)$",
"appendFiles": true
},
{
"id": "types",
"command": [
"node",
"node_modules/typescript/bin/tsc",
"--noEmit"
],
"cwd": ".",
"levels": [
"affected",
"gate"
],
"timeoutMs": 120000,
"next": "Fix the reported diagnostics and rerun this check."
},
{
"id": "lint",
"command": [
"bun",
"run",
"lint"
],
"cwd": ".",
"levels": [
"affected",
"gate"
],
"timeoutMs": 120000,
"next": "Fix the reported diagnostics and rerun this check."
},
{
"id": "cli-skills",
"command": [
"bun",
"run",
"validate:cli-skills"
],
"levels": [
"affected",
"gate"
],
"timeoutMs": 120000
},
{
"id": "unit",
"command": [
"node",
".quality/native.mjs",
"unit"
],
"cwd": ".",
"levels": [
"affected",
"gate"
],
"timeoutMs": 120000,
"next": "Fix the reported diagnostics and rerun this check.",
"test": true,
"countPattern": "^\\s*(\\d+) pass$"
},
{
"id": "build",
"command": [
"bun",
"run",
"build"
],
"cwd": ".",
"levels": [
"gate"
],
"timeoutMs": 120000,
"next": "Fix the reported diagnostics and rerun this check."
},
{
"id": "binary",
"command": [
"bun",
"run",
"build:binary"
],
"cwd": ".",
"levels": [
"gate"
],
"timeoutMs": 120000,
"next": "Fix the reported diagnostics and rerun this check."
},
{
"id": "smoke",
"command": [
"bun",
"test",
"tests/smoke/"
],
"cwd": ".",
"levels": [
"gate"
],
"timeoutMs": 120000,
"next": "Fix the reported diagnostics and rerun this check.",
"test": true,
"countPattern": "^\\s*(\\d+) pass$"
}
],
"coverage": [
{
"area": "formatter",
"status": "not_applicable",
"reason": "No existing formatter; retain repository oxlint rules."
},
{
"area": "live-auth-e2e",
"status": "unavailable",
"reason": "Private CLI/API release evidence is a separate existing workflow_dispatch gate. Offline checks never call production or send email.",
"levels": [
"risk"
]
}
]
}
109 changes: 109 additions & 0 deletions .quality/hook.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import * as fs from "node:fs";
import path from "node:path";
import { createHash, randomUUID } from "node:crypto";
import { fileURLToPath } from "node:url";
import { rootFor, snapshot, run, summary } from "./run.mjs";

const ownRoot = fs.realpathSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
function save(file, value) {
const tmp = file + "." + randomUUID();
fs.writeFileSync(tmp, JSON.stringify(value));
fs.renameSync(tmp, file);
}
function read(file) {
try {
return JSON.parse(fs.readFileSync(file));
} catch {
return null;
}
}
function feedback(event, text) {
return { hookSpecificOutput: { hookEventName: event, additionalContext: text } };
}
try {
let input = "";
for await (const chunk of process.stdin) {
input += chunk;
if (input.length > 1024 * 1024) throw new Error("Hook input exceeds 1MiB");
}
const payload = JSON.parse(input);
if (typeof payload.cwd !== "string" || typeof payload.session_id !== "string")
throw new Error("Missing cwd/session_id");
const root = rootFor(payload.cwd);
if (root !== ownRoot) throw new Error("Hook cwd belongs to another worktree");
const event = payload.hook_event_name;
if (!["SessionStart", "PostToolUse", "PostToolUseFailure", "Stop"].includes(event))
throw new Error(`Unsupported hook event ${event}`);
if (process.env.QUALITY_LOOP_RUNNING === "1") {
console.log("{}");
} else {
const dir = path.join(root, ".quality-local");
fs.mkdirSync(dir, { recursive: true });
const key = createHash("sha256").update(payload.session_id).digest("hex");
const sessionFile = path.join(dir, "session-" + key + ".json");
let session = read(sessionFile);
if (!session) {
session = { base: snapshot(root).head };
save(sessionFile, session);
}
const state = snapshot(root, session.base);
if (event === "SessionStart")
console.log(
JSON.stringify(
feedback(
event,
"Quality loop: fast after edits; affected for dependents; node .quality/run.mjs gate before completion. Risk checks remain explicit; source is never autoformatted.",
),
),
);
else if (event === "Stop") {
const receipt = read(path.join(dir, "gate.json"));
const fresh =
receipt &&
receipt.level === "gate" &&
receipt.status === "passed" &&
Date.now() - Date.parse(receipt.startedAt) < 600000 &&
snapshot(root, receipt.baseline).fingerprint === receipt.fingerprint;
if ((!state.files.length && state.head === session.base) || fresh) console.log("{}");
else if (payload.stop_hook_active)
console.log(
JSON.stringify({
systemMessage:
"Quality verification incomplete. Stop continuation limited; no passing gate is credited. Run node .quality/run.mjs gate and report failures/coverage gaps.",
}),
);
else
console.log(
JSON.stringify({
decision: "block",
reason:
"Quality verification incomplete: run node .quality/run.mjs gate before claiming completion. Report existing failures, unavailable coverage and risk checks honestly; do not weaken rules to pass.",
}),
);
} else {
const cacheFile = path.join(dir, "fast-hook-" + key + ".json");
const cached = read(cacheFile);
if (cached?.fingerprint === state.fingerprint && Date.now() - cached.time < 30000)
console.log("{}");
else {
const result = await run({ root, level: "fast", base: session.base, quiet: true });
const detail = result.checks
.filter((c) => !["passed", "not_selected"].includes(c.status))
.map((c) => `${c.id}: ${c.detail || c.status}\nNext: ${c.next || "retry"}`)
.join("\n");
if (["passed", "no_changes"].includes(result.status))
save(cacheFile, { fingerprint: state.fingerprint, time: Date.now() });
console.log(
JSON.stringify(feedback(event, (summary(result) + "\n" + detail).slice(0, 7000))),
);
}
}
}
} catch (e) {
console.log(
JSON.stringify({
systemMessage: `Quality hook unavailable: ${e.message}. No check credited; run node .quality/run.mjs gate manually.`,
}),
);
process.exitCode = 1;
}
10 changes: 10 additions & 0 deletions .quality/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"schemaVersion": 1,
"package": "@przeprogramowani/quality-loop",
"version": "1.0.0",
"files": {
"run.mjs": "24b84b9df60a043d3683b59c9d4e75d02cbfc6cf6d26df3493702fb22173af35",
"hook.mjs": "5b29bc88b42d1b9ed0c8008a24a3c27a275101f088a034e9e64cff894229e908",
"builtin.mjs": "1469de4e8c662bc376c1ae117e18367c007b677751b9d0d4a2ba0178f8c87c86"
}
}
19 changes: 19 additions & 0 deletions .quality/native.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { readdirSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
if (process.argv[2] === 'toolchain') {
for (const [command, expected] of [['bun', '1.3.8'], ['npm', '11.12.1']]) {
const result = spawnSync(command, ['--version'], { encoding: 'utf8', env: process.env });
if (result.error || result.status !== 0 || result.stdout.trim() !== expected) {
console.error(`Required ${command} ${expected}; install the CI toolchain before checking.`);
process.exit(1);
}
}
console.log('Bun 1.3.8 and npm 11.12.1 verified');
process.exit(0);
}
if (process.argv[2] !== 'unit') throw new Error('Expected unit');
const files = readdirSync('tests').filter(name => name.endsWith('.test.ts')).sort().map(name => `./tests/${name}`);
if (!files.length) throw new Error('No unit/integration test suites discovered in tests/');
const child = spawnSync('bun', ['test', ...files], { stdio: 'inherit', env: process.env });
if (child.error) throw child.error;
process.exit(child.status ?? 1);
Loading
Loading