pqcheck 0.16.15 → 0.16.17
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 +1 -1
- package/bin/pqcheck.js +133 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](https://www.npmjs.com/package/pqcheck)
|
|
9
9
|
[](./LICENSE)
|
|
10
10
|
|
|
11
|
-
> **Latest: v0.16.
|
|
11
|
+
> **Latest: v0.16.17** — fail-loud AI guard now covers the no-baseline first-deploy fallback. `pqcheck deploy-check <new-domain> --ai` previously exited 3 with bare `error: /api/scan returned 429` if the very first scan got rate-limited — no `CIPHERWAKE_AI_GUARD_RESULT` block, leaving the calling AI agent with no `ship_decision` to route on. Now every error path in that fallback emits a `ship_decision=review` block with a status-specific `top_issue` code. [Full changelog →](./CHANGELOG.md)
|
|
12
12
|
|
|
13
13
|
## Two ways to use it
|
|
14
14
|
|
package/bin/pqcheck.js
CHANGED
|
@@ -3051,12 +3051,51 @@ async function runScanBasedDeployCheck(domain, args) {
|
|
|
3051
3051
|
try {
|
|
3052
3052
|
resp = await fetch(`${API_BASE}/api/scan?domain=${encodeURIComponent(domain)}`, { headers });
|
|
3053
3053
|
} catch (err) {
|
|
3054
|
+
// v0.16.17 — the v0.16.13 fail-loud AI guard fix was applied to the main
|
|
3055
|
+
// trust-diff deploy-check path but missed THIS fallback path (no-baseline
|
|
3056
|
+
// first-deploy), so a network blip on the very first `pqcheck deploy-check
|
|
3057
|
+
// <new-domain> --ai` exited 3 with no CIPHERWAKE_AI_GUARD_RESULT block.
|
|
3058
|
+
// The calling AI agent then had no ship_decision to route on and could
|
|
3059
|
+
// silently continue shipping — exactly the failure mode the protocol
|
|
3060
|
+
// exists to prevent. Same emit-block-and-exit pattern as trust-diff.
|
|
3054
3061
|
console.error(color("red", `error: network failure calling /api/scan: ${err.message}`));
|
|
3055
|
-
|
|
3062
|
+
return emitAiGuardReviewAndExit(args, {
|
|
3063
|
+
code: "deploy_check_fetch_failed",
|
|
3064
|
+
message: `Network failure calling /api/scan: ${err?.message || "fetch failed"}`,
|
|
3065
|
+
exitCode: 3,
|
|
3066
|
+
});
|
|
3056
3067
|
}
|
|
3057
3068
|
if (!resp.ok) {
|
|
3058
|
-
|
|
3059
|
-
|
|
3069
|
+
// v0.16.17 — same fix as above for the HTTP-non-OK path. The 429 case
|
|
3070
|
+
// is especially load-bearing: a brand new AI-coder workflow trying its
|
|
3071
|
+
// first deploy-check on a fresh project will commonly hit per-IP rate
|
|
3072
|
+
// limits, get a bare `error: /api/scan returned 429`, and exit 3 with no
|
|
3073
|
+
// guard block. The AI agent then has nothing to parse. Emitting a
|
|
3074
|
+
// ship_decision=review block with a quota-specific error code lets the
|
|
3075
|
+
// agent surface the rate-limit problem to the user instead of silently
|
|
3076
|
+
// continuing. Surface the body message + auth hint when available so
|
|
3077
|
+
// the user knows whether to wait or get an API key.
|
|
3078
|
+
const body = await safeJSON(resp);
|
|
3079
|
+
const statusLabel = resp.status === 429
|
|
3080
|
+
? "rate-limited"
|
|
3081
|
+
: resp.status === 401 || resp.status === 403
|
|
3082
|
+
? "auth failed"
|
|
3083
|
+
: `${resp.status}`;
|
|
3084
|
+
console.error(color("red", `error: /api/scan returned ${resp.status} (${statusLabel})`));
|
|
3085
|
+
if (body?.message) console.error(color("dim", body.message));
|
|
3086
|
+
if (body?.hint) console.error(color("dim", body.hint));
|
|
3087
|
+
if (resp.status === 429) {
|
|
3088
|
+
console.error(color("dim", "Higher quota via free API key (no card): https://cipherwake.io/account#api-keys"));
|
|
3089
|
+
}
|
|
3090
|
+
const code = resp.status === 429
|
|
3091
|
+
? "deploy_check_rate_limited"
|
|
3092
|
+
: resp.status === 401 || resp.status === 403
|
|
3093
|
+
? "deploy_check_auth_failed"
|
|
3094
|
+
: "deploy_check_scan_failed";
|
|
3095
|
+
const message = resp.status === 429
|
|
3096
|
+
? (body?.message || "Per-IP rate limit hit on /api/scan. Wait ~1 minute or use an API key for higher quota.")
|
|
3097
|
+
: body?.message || `Cipherwake /api/scan returned ${resp.status}.`;
|
|
3098
|
+
return emitAiGuardReviewAndExit(args, { code, message, exitCode: 3 });
|
|
3060
3099
|
}
|
|
3061
3100
|
const report = await resp.json();
|
|
3062
3101
|
const findings = Array.isArray(report.findings) ? report.findings : [];
|
|
@@ -4441,7 +4480,7 @@ function renderReleaseChecklist(domain, opts = {}) {
|
|
|
4441
4480
|
// `pqcheck init` — interactive workflow scaffold (habit-loop #4, locked 2026-05-16)
|
|
4442
4481
|
// =============================================================================
|
|
4443
4482
|
// Writes a ready-to-commit .github/workflows/cipherwake.yml that calls
|
|
4444
|
-
// cipherwakelabs/pqcheck
|
|
4483
|
+
// cipherwakelabs/pqcheck@v4 in trust-diff mode. Zero copy-paste docs friction.
|
|
4445
4484
|
//
|
|
4446
4485
|
// Flags:
|
|
4447
4486
|
// --domain <d> Skip the domain prompt
|
|
@@ -4618,7 +4657,7 @@ jobs:
|
|
|
4618
4657
|
runs-on: ubuntu-latest
|
|
4619
4658
|
steps:
|
|
4620
4659
|
- name: Run Cipherwake Trust Diff
|
|
4621
|
-
uses: cipherwakelabs/pqcheck
|
|
4660
|
+
uses: cipherwakelabs/pqcheck@v4
|
|
4622
4661
|
with:
|
|
4623
4662
|
mode: trust-diff
|
|
4624
4663
|
domain: ${domain}
|
|
@@ -5613,9 +5652,23 @@ async function runProtocolCommand(args) {
|
|
|
5613
5652
|
console.log("Here's what would be added:");
|
|
5614
5653
|
console.log("");
|
|
5615
5654
|
if (detected.length === 0) {
|
|
5655
|
+
// Default to creating project-local ./CLAUDE.md rather than global
|
|
5656
|
+
// ~/.claude/CLAUDE.md. Mirrors the cli/setup --scope=project default
|
|
5657
|
+
// (commit 4450e77): don't pollute global config when no signal exists
|
|
5658
|
+
// that the user wants machine-wide installation. Pass --scope=global
|
|
5659
|
+
// to override and create ~/.claude/CLAUDE.md instead.
|
|
5660
|
+
const scopeRaw = (parseFlag(args, "--scope") || "project").toLowerCase();
|
|
5661
|
+
const useGlobal = scopeRaw === "global";
|
|
5662
|
+
const fallbackPath = useGlobal
|
|
5663
|
+
? path.join(os.homedir(), ".claude", "CLAUDE.md")
|
|
5664
|
+
: path.join(process.cwd(), "CLAUDE.md");
|
|
5665
|
+
const fallbackDisplay = fallbackPath.replace(os.homedir(), "~");
|
|
5616
5666
|
console.log(color("dim", " No existing CLAUDE.md / .cursorrules / .aider.conf.yml found."));
|
|
5617
|
-
console.log(color("dim",
|
|
5618
|
-
|
|
5667
|
+
console.log(color("dim", ` Creating ${fallbackDisplay} with the protocol.`));
|
|
5668
|
+
if (!useGlobal) {
|
|
5669
|
+
console.log(color("dim", " (pass --scope global to create ~/.claude/CLAUDE.md instead)"));
|
|
5670
|
+
}
|
|
5671
|
+
detected.push({ label: useGlobal ? "Claude Code (will create global)" : "Claude Code (will create project)", path: fallbackPath });
|
|
5619
5672
|
}
|
|
5620
5673
|
for (const d of detected) {
|
|
5621
5674
|
console.log(` • Append a ~30-line "## Pre-deploy verification with Cipherwake" section to ${color("bold", d.path)}`);
|
|
@@ -5927,11 +5980,21 @@ async function runSetupCommand(args) {
|
|
|
5927
5980
|
const skipHook = args.includes("--skip-hook");
|
|
5928
5981
|
const skipStatusline = args.includes("--skip-statusline");
|
|
5929
5982
|
const skipVscode = args.includes("--skip-vscode");
|
|
5983
|
+
// Where to install Claude Code statusLine + hooks. Default 'project' keeps
|
|
5984
|
+
// Cipherwake scoped to the current repo (avoids the "Cipherwake badge
|
|
5985
|
+
// follows me into unrelated projects" UX bug). 'global' is opt-in for
|
|
5986
|
+
// power users who want one canonical domain badge across every project.
|
|
5987
|
+
const settingsScope = (parseFlag(args, "--scope") || "project").toLowerCase();
|
|
5988
|
+
if (!["project", "global"].includes(settingsScope)) {
|
|
5989
|
+
console.error(color("red", `error: --scope must be 'project' or 'global' (got '${settingsScope}')`));
|
|
5990
|
+
process.exit(3);
|
|
5991
|
+
}
|
|
5930
5992
|
|
|
5931
5993
|
if (!domain) {
|
|
5932
5994
|
console.error(color("red", "error: pqcheck setup requires --domain"));
|
|
5933
5995
|
console.error(color("dim", "Usage: npx pqcheck setup --auto --domain example.com"));
|
|
5934
|
-
console.error(color("dim", " --plan
|
|
5996
|
+
console.error(color("dim", " --plan Print the install plan without writing any files"));
|
|
5997
|
+
console.error(color("dim", " --scope project|global Where to install Claude Code hooks (default: project — this repo only)"));
|
|
5935
5998
|
console.error(color("dim", " --invoked-by=\"<name>\" --consent-phrase=\"<words>\" (audit trail)"));
|
|
5936
5999
|
console.error(color("dim", "Skip flags: --skip-workflow --skip-protocol --skip-hook --skip-statusline --skip-vscode"));
|
|
5937
6000
|
process.exit(3);
|
|
@@ -5977,11 +6040,17 @@ async function runSetupCommand(args) {
|
|
|
5977
6040
|
}
|
|
5978
6041
|
if (!skipHook) planEntries.push({ what: "git pre-push hook", to: path.join(process.cwd(), ".git", "hooks", "pre-push"), op: "create (if git repo)" });
|
|
5979
6042
|
if (!skipStatusline) {
|
|
5980
|
-
const settingsPath =
|
|
6043
|
+
const settingsPath = settingsScope === "global"
|
|
6044
|
+
? path.join(os.homedir(), ".claude", "settings.json")
|
|
6045
|
+
: path.join(process.cwd(), ".claude", "settings.json");
|
|
5981
6046
|
let exists = false;
|
|
5982
6047
|
try { await fs.access(settingsPath); exists = true; } catch { /* */ }
|
|
5983
|
-
|
|
6048
|
+
const scopeNote = settingsScope === "global"
|
|
6049
|
+
? " [GLOBAL — fires in every project on this machine]"
|
|
6050
|
+
: " [PROJECT-LOCAL — fires only when Claude Code runs in this directory]";
|
|
6051
|
+
planEntries.push({ what: `Claude Code statusLine${scopeNote}`, to: settingsPath, op: exists ? "deep-merge (backup first)" : "create" });
|
|
5984
6052
|
planEntries.push({ what: "Claude Code chat-hook (PostToolUse Bash)", to: settingsPath, op: exists ? "deep-merge into hooks.PostToolUse" : "create" });
|
|
6053
|
+
planEntries.push({ what: "Claude Code prompt-hook (UserPromptSubmit)", to: settingsPath, op: exists ? "deep-merge into hooks.UserPromptSubmit" : "create" });
|
|
5985
6054
|
}
|
|
5986
6055
|
if (!skipVscode) planEntries.push({ what: "VS Code / Cursor extension", to: "via `code --install-extension cipherwakelabs.cipherwake-statusbar`", op: "attempt (soft-fail if Marketplace listing missing)" });
|
|
5987
6056
|
for (const e of planEntries) {
|
|
@@ -6012,6 +6081,48 @@ async function runSetupCommand(args) {
|
|
|
6012
6081
|
console.log("");
|
|
6013
6082
|
}
|
|
6014
6083
|
|
|
6084
|
+
// Resolve where to write Claude Code statusLine + hook entries.
|
|
6085
|
+
// Default 'project' = ./.claude/settings.json (this repo only). 'global' = ~/.claude/settings.json
|
|
6086
|
+
// (fires in every Claude Code session on this machine). Project-local is the right default for
|
|
6087
|
+
// anyone with multiple projects; global is the legacy behavior and now an opt-in for the
|
|
6088
|
+
// "one canonical domain across everything" use case.
|
|
6089
|
+
const claudeSettingsPath = settingsScope === "global"
|
|
6090
|
+
? path.join(os.homedir(), ".claude", "settings.json")
|
|
6091
|
+
: path.join(process.cwd(), ".claude", "settings.json");
|
|
6092
|
+
|
|
6093
|
+
if (!skipStatusline) {
|
|
6094
|
+
const displayPath = claudeSettingsPath.replace(os.homedir(), "~");
|
|
6095
|
+
const scopeLabel = settingsScope === "global"
|
|
6096
|
+
? `global (${displayPath} — fires in EVERY project on this machine)`
|
|
6097
|
+
: `project (${displayPath} — fires only when Claude Code runs in this directory)`;
|
|
6098
|
+
console.log(color("dim", `Settings scope: ${scopeLabel}`));
|
|
6099
|
+
console.log(color("dim", settingsScope === "global"
|
|
6100
|
+
? ` (omit --scope or pass --scope project to install for this repo only)`
|
|
6101
|
+
: ` (pass --scope global to install for every project on this machine)`));
|
|
6102
|
+
console.log("");
|
|
6103
|
+
|
|
6104
|
+
// Detect existing GLOBAL Cipherwake install while doing a project install.
|
|
6105
|
+
// Warn so the user doesn't end up with two layers firing on top of each other.
|
|
6106
|
+
if (settingsScope === "project") {
|
|
6107
|
+
try {
|
|
6108
|
+
const globalPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
6109
|
+
const raw = await fs.readFile(globalPath, "utf8");
|
|
6110
|
+
const parsed = JSON.parse(raw);
|
|
6111
|
+
const hasGlobalCipherwake =
|
|
6112
|
+
(parsed.statusLine?.command && String(parsed.statusLine.command).includes("cipherwake")) ||
|
|
6113
|
+
JSON.stringify(parsed.hooks || {}).includes("cipherwake");
|
|
6114
|
+
if (hasGlobalCipherwake) {
|
|
6115
|
+
console.log(color("yellow", ` ⚠ A GLOBAL Cipherwake install already exists at ~/.claude/settings.json`));
|
|
6116
|
+
console.log(color("dim", ` It will continue firing across every project. To remove the global install,`));
|
|
6117
|
+
console.log(color("dim", ` edit ~/.claude/settings.json and delete the statusLine entry and the`));
|
|
6118
|
+
console.log(color("dim", ` cipherwake-chat-hook / cipherwake-prompt-hook commands. Backups are taken`));
|
|
6119
|
+
console.log(color("dim", ` automatically before any write, so changes are reversible.`));
|
|
6120
|
+
console.log("");
|
|
6121
|
+
}
|
|
6122
|
+
} catch { /* no global install — fine */ }
|
|
6123
|
+
}
|
|
6124
|
+
}
|
|
6125
|
+
|
|
6015
6126
|
const installSummary = [];
|
|
6016
6127
|
|
|
6017
6128
|
// -------------------------------------------------------------------------
|
|
@@ -6176,7 +6287,8 @@ async function runSetupCommand(args) {
|
|
|
6176
6287
|
}
|
|
6177
6288
|
|
|
6178
6289
|
if (!skipStatusline) {
|
|
6179
|
-
const settingsPath =
|
|
6290
|
+
const settingsPath = claudeSettingsPath;
|
|
6291
|
+
const displayPath = settingsPath.replace(os.homedir(), "~");
|
|
6180
6292
|
try {
|
|
6181
6293
|
let settings = {};
|
|
6182
6294
|
let existed = false;
|
|
@@ -6187,7 +6299,7 @@ async function runSetupCommand(args) {
|
|
|
6187
6299
|
} catch { /* will create */ }
|
|
6188
6300
|
if (existed && settings.statusLine && typeof settings.statusLine === "object") {
|
|
6189
6301
|
// Already has a statusLine config — don't overwrite.
|
|
6190
|
-
console.log(color("dim", ` ⊝
|
|
6302
|
+
console.log(color("dim", ` ⊝ ${displayPath} already has a statusLine entry — leaving alone`));
|
|
6191
6303
|
console.log(color("dim", ` To use the Cipherwake statusline instead, set: "command": "npx --package=pqcheck@latest cipherwake-statusline"`));
|
|
6192
6304
|
installSummary.push({ component: "Claude Code statusLine", path: settingsPath, status: "skipped-existing-config" });
|
|
6193
6305
|
} else {
|
|
@@ -6196,7 +6308,7 @@ async function runSetupCommand(args) {
|
|
|
6196
6308
|
settings.statusLine = { type: "command", command: "npx --package=pqcheck@latest cipherwake-statusline" };
|
|
6197
6309
|
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
6198
6310
|
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
6199
|
-
console.log(color("green", ` ✓ added statusLine config →
|
|
6311
|
+
console.log(color("green", ` ✓ added statusLine config → ${displayPath}`));
|
|
6200
6312
|
installSummary.push({ component: "Claude Code statusLine", path: settingsPath, status: existed ? "installed-updated" : "installed-created", backup: backupPath });
|
|
6201
6313
|
}
|
|
6202
6314
|
} catch (err) {
|
|
@@ -6212,7 +6324,8 @@ async function runSetupCommand(args) {
|
|
|
6212
6324
|
// doesn't clobber other hooks per CLAUDE.md Rule 17.
|
|
6213
6325
|
// -------------------------------------------------------------------------
|
|
6214
6326
|
if (!skipStatusline) {
|
|
6215
|
-
const settingsPath =
|
|
6327
|
+
const settingsPath = claudeSettingsPath;
|
|
6328
|
+
const displayPath = settingsPath.replace(os.homedir(), "~");
|
|
6216
6329
|
try {
|
|
6217
6330
|
let settings = {};
|
|
6218
6331
|
let existed = false;
|
|
@@ -6238,7 +6351,7 @@ async function runSetupCommand(args) {
|
|
|
6238
6351
|
);
|
|
6239
6352
|
|
|
6240
6353
|
if (alreadyInstalled) {
|
|
6241
|
-
console.log(color("dim", ` ⊝ chat-hook already configured in
|
|
6354
|
+
console.log(color("dim", ` ⊝ chat-hook already configured in ${displayPath} PostToolUse — skipping`));
|
|
6242
6355
|
installSummary.push({ component: "Claude Code chat-hook", path: settingsPath, status: "skipped-already-present" });
|
|
6243
6356
|
} else {
|
|
6244
6357
|
const backupPath = existed ? await backupSettingsJson(settingsPath) : null;
|
|
@@ -6246,7 +6359,7 @@ async function runSetupCommand(args) {
|
|
|
6246
6359
|
bashEntry.hooks.push({ type: "command", command: cipherwakeHookCmd });
|
|
6247
6360
|
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
6248
6361
|
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
6249
|
-
console.log(color("green", ` ✓ added chat-hook (PostToolUse Bash) →
|
|
6362
|
+
console.log(color("green", ` ✓ added chat-hook (PostToolUse Bash) → ${displayPath}`));
|
|
6250
6363
|
console.log(color("dim", ` Every \`pqcheck\` run will now push a live status message into Claude Code chat`));
|
|
6251
6364
|
installSummary.push({ component: "Claude Code chat-hook", path: settingsPath, status: existed ? "installed-updated" : "installed-created", backup: backupPath });
|
|
6252
6365
|
}
|
|
@@ -6265,7 +6378,8 @@ async function runSetupCommand(args) {
|
|
|
6265
6378
|
// / ship_decision=pass (no spam).
|
|
6266
6379
|
// -------------------------------------------------------------------------
|
|
6267
6380
|
if (!skipStatusline) {
|
|
6268
|
-
const settingsPath =
|
|
6381
|
+
const settingsPath = claudeSettingsPath;
|
|
6382
|
+
const displayPath = settingsPath.replace(os.homedir(), "~");
|
|
6269
6383
|
try {
|
|
6270
6384
|
let settings = {};
|
|
6271
6385
|
let existed = false;
|
|
@@ -6285,7 +6399,7 @@ async function runSetupCommand(args) {
|
|
|
6285
6399
|
);
|
|
6286
6400
|
|
|
6287
6401
|
if (alreadyInstalled) {
|
|
6288
|
-
console.log(color("dim", ` ⊝ prompt-hook already configured in
|
|
6402
|
+
console.log(color("dim", ` ⊝ prompt-hook already configured in ${displayPath} UserPromptSubmit — skipping`));
|
|
6289
6403
|
installSummary.push({ component: "Claude Code prompt-hook", path: settingsPath, status: "skipped-already-present" });
|
|
6290
6404
|
} else {
|
|
6291
6405
|
const backupPath = existed ? await backupSettingsJson(settingsPath) : null;
|
|
@@ -6293,7 +6407,7 @@ async function runSetupCommand(args) {
|
|
|
6293
6407
|
settings.hooks.UserPromptSubmit.push({ hooks: [{ type: "command", command: cipherwakeHookCmd }] });
|
|
6294
6408
|
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
6295
6409
|
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
6296
|
-
console.log(color("green", ` ✓ added prompt-hook (UserPromptSubmit) →
|
|
6410
|
+
console.log(color("green", ` ✓ added prompt-hook (UserPromptSubmit) → ${displayPath}`));
|
|
6297
6411
|
console.log(color("dim", ` Claude will see latest ship_decision in context on every prompt (when REVIEW/BLOCK)`));
|
|
6298
6412
|
installSummary.push({ component: "Claude Code prompt-hook", path: settingsPath, status: existed ? "installed-updated" : "installed-created", backup: backupPath });
|
|
6299
6413
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pqcheck",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.17",
|
|
4
4
|
"description": "Deploy gate for AI-coded web apps. `pqcheck deploy-check --ai` returns ship_decision=pass|review|block for Claude Code / Cursor / Copilot / Aider to gate deploys before they ship. Anonymous, no signup, free for first use.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-coder",
|