@0xcraft/powershot 1.0.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.
Files changed (87) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +306 -0
  3. package/dist/agents.js +82 -0
  4. package/dist/bench.js +179 -0
  5. package/dist/budget.js +59 -0
  6. package/dist/bundle.js +173 -0
  7. package/dist/cache.js +155 -0
  8. package/dist/cli/agent-command.js +27 -0
  9. package/dist/cli/app.js +31 -0
  10. package/dist/cli/args.js +76 -0
  11. package/dist/cli/bench-command.js +89 -0
  12. package/dist/cli/dismiss-command.js +42 -0
  13. package/dist/cli/environment.js +32 -0
  14. package/dist/cli/reports.js +64 -0
  15. package/dist/cli/review-command.js +268 -0
  16. package/dist/cli/session-command.js +62 -0
  17. package/dist/cli.js +7 -0
  18. package/dist/config.js +130 -0
  19. package/dist/delegate.js +84 -0
  20. package/dist/dismissed.js +130 -0
  21. package/dist/fspolicy.js +62 -0
  22. package/dist/git.js +238 -0
  23. package/dist/ground.js +286 -0
  24. package/dist/judges/judge.js +85 -0
  25. package/dist/judges/llm.js +234 -0
  26. package/dist/judges/prompts.js +86 -0
  27. package/dist/judges/tools.js +125 -0
  28. package/dist/lang/packs.js +557 -0
  29. package/dist/lang/pyright.js +108 -0
  30. package/dist/lang/python-deps.js +174 -0
  31. package/dist/lang/ruby-deps.js +77 -0
  32. package/dist/langtest.js +248 -0
  33. package/dist/manifest.js +209 -0
  34. package/dist/otel.js +75 -0
  35. package/dist/package-meta.js +13 -0
  36. package/dist/package-smoke.js +110 -0
  37. package/dist/plan.js +134 -0
  38. package/dist/position.js +94 -0
  39. package/dist/report/ansi.js +18 -0
  40. package/dist/report/codequality.js +19 -0
  41. package/dist/report/compact.js +15 -0
  42. package/dist/report/highlight.js +54 -0
  43. package/dist/report/markdown.js +113 -0
  44. package/dist/report/sarif.js +66 -0
  45. package/dist/report/terminal.js +170 -0
  46. package/dist/report/viewer.js +148 -0
  47. package/dist/review.js +355 -0
  48. package/dist/scan.js +67 -0
  49. package/dist/selftest.js +1928 -0
  50. package/dist/session.js +140 -0
  51. package/dist/snapshot.js +101 -0
  52. package/dist/text.js +50 -0
  53. package/dist/types.js +2 -0
  54. package/dist/verifiers/assertion-drift.js +137 -0
  55. package/dist/verifiers/contract-drift.js +140 -0
  56. package/dist/verifiers/copy-paste-drift.js +106 -0
  57. package/dist/verifiers/dead-on-arrival.js +92 -0
  58. package/dist/verifiers/dropped-guard.js +144 -0
  59. package/dist/verifiers/foreign-contract-drift.js +114 -0
  60. package/dist/verifiers/foreign-copy-paste-drift.js +83 -0
  61. package/dist/verifiers/foreign-dropped-guard.js +78 -0
  62. package/dist/verifiers/foreign-phantom-api.js +36 -0
  63. package/dist/verifiers/foreign-phantom-config.js +40 -0
  64. package/dist/verifiers/foreign-phantom-dep.js +82 -0
  65. package/dist/verifiers/foreign-reinvented.js +65 -0
  66. package/dist/verifiers/foreign-scope-creep.js +42 -0
  67. package/dist/verifiers/foreign-swallowed-error.js +36 -0
  68. package/dist/verifiers/foreign-tests.js +143 -0
  69. package/dist/verifiers/foreign-tokens.js +94 -0
  70. package/dist/verifiers/foreign.js +16 -0
  71. package/dist/verifiers/index.js +38 -0
  72. package/dist/verifiers/lying-comment.js +90 -0
  73. package/dist/verifiers/phantom-api.js +88 -0
  74. package/dist/verifiers/phantom-config.js +93 -0
  75. package/dist/verifiers/phantom-dep.js +110 -0
  76. package/dist/verifiers/reinvented.js +74 -0
  77. package/dist/verifiers/scope-creep.js +77 -0
  78. package/dist/verifiers/swallowed-error.js +110 -0
  79. package/dist/verifiers/vacuous-test.js +138 -0
  80. package/docs/architecture.md +191 -0
  81. package/docs/assets/cli-preview.svg +68 -0
  82. package/docs/assets/powershot-logo.png +0 -0
  83. package/docs/ci.md +151 -0
  84. package/examples/github-actions/action.yml +23 -0
  85. package/examples/github-actions/cli.yml +43 -0
  86. package/examples/gitlab/.gitlab-ci.yml +21 -0
  87. package/package.json +65 -0
@@ -0,0 +1,76 @@
1
+ import { parseArgs } from 'node:util';
2
+ export const HELP = `
3
+ psh — code review for machine-written code
4
+
5
+ psh review staged + unstaged + untracked
6
+ psh review --from main --to feat/x branch range
7
+ psh review --commit <hash> a single commit
8
+ psh scan <path> audit existing files, no git history needed
9
+ psh delegate emit the judging work for an agent you already pay for
10
+ psh session list | view <id> runs that can be resumed, or replayed as a page
11
+ psh session diff <a> <b> what one review found that the other did not
12
+ psh dismiss <id> [--reason "..."] record that a finding is correct, and stop showing it
13
+ psh dismiss list | restore <fp> what has been waved through, and undoing it
14
+ psh agent <name> | list teach a coding agent to use this tool
15
+ psh bench --repo <path> replay real commits and count what the checks say
16
+
17
+ Options
18
+ --verify-only deterministic checks only — no model, no tokens
19
+ --checks a,b run only these checks
20
+ --min-severity <s> info | low | medium | high | critical (default: low)
21
+ --format <f> text · compact · markdown · json · sarif · codequality · manifest
22
+ --absorb <file> merge findings a delegated agent produced
23
+ --last <n> bench: how many commits to replay (default 50)
24
+ --cases <dir> bench: labelled .json cases to score against
25
+ --tools let judges read files and search before answering
26
+ --resume <id> continue a run, reusing judge results already paid for
27
+ --output <path> write the report to a file instead of stdout
28
+ --report <f>=<path> write this format to that file; repeatable, one run
29
+ --budget k=v,... requests · inputTokens · outputTokens · toolCalls · elapsedMs · units
30
+ --no-cache ask every judge again, ignoring answers already paid for
31
+ --max-bundle <n> lines per judge prompt (default 1200)
32
+ --help
33
+
34
+ Exit codes: 0 clean · 1 findings reported · 2 bad usage · 3 review incomplete.
35
+ A stage that failed makes the run non-zero even with nothing to report — "we found
36
+ nothing" and "we could not look" must not be the same answer to a pipeline.
37
+ `;
38
+ export function parseCliArgs(args = process.argv.slice(2)) {
39
+ return parseArgs({
40
+ args,
41
+ allowPositionals: true,
42
+ options: {
43
+ from: { type: 'string' },
44
+ to: { type: 'string' },
45
+ commit: { type: 'string' },
46
+ format: { type: 'string', default: 'text' },
47
+ checks: { type: 'string' },
48
+ 'min-severity': { type: 'string' },
49
+ 'verify-only': { type: 'boolean', default: false },
50
+ repo: { type: 'string' },
51
+ last: { type: 'string' },
52
+ cases: { type: 'string' },
53
+ tools: { type: 'boolean', default: false },
54
+ resume: { type: 'string' },
55
+ output: { type: 'string' },
56
+ 'no-cache': { type: 'boolean', default: false },
57
+ report: { type: 'string', multiple: true },
58
+ budget: { type: 'string' },
59
+ 'show-dismissed': { type: 'boolean', default: false },
60
+ reason: { type: 'string' },
61
+ 'max-bundle': { type: 'string' },
62
+ absorb: { type: 'string' },
63
+ help: { type: 'boolean', short: 'h', default: false },
64
+ },
65
+ });
66
+ }
67
+ export function positiveNumber(value, flag, fallback) {
68
+ if (value === undefined)
69
+ return fallback;
70
+ const parsed = Number(value);
71
+ if (Number.isFinite(parsed) && parsed > 0)
72
+ return parsed;
73
+ process.stderr.write(flag + ' must be a positive number, got "' + String(value) + '"\n');
74
+ return undefined;
75
+ }
76
+ //# sourceMappingURL=args.js.map
@@ -0,0 +1,89 @@
1
+ import { f1, loadCases, precision, recall, replayRepo, runCases } from '#app/bench.js';
2
+ import { repoRoot } from '#app/git.js';
3
+ import { dim, bold, steel, yellow } from '#app/report/ansi.js';
4
+ import { positiveNumber } from './args.js';
5
+ function percentage(value) {
6
+ return (value * 100).toFixed(1) + '%';
7
+ }
8
+ function total(scores) {
9
+ return scores.reduce((sum, score) => ({ tp: sum.tp + score.tp, fp: sum.fp + score.fp, fn: sum.fn + score.fn }), { tp: 0, fp: 0, fn: 0 });
10
+ }
11
+ export async function runBenchCommand(values) {
12
+ const root = values.repo !== undefined ? repoRoot(values.repo) : repoRoot(process.cwd());
13
+ const out = process.stdout;
14
+ if (values.cases !== undefined) {
15
+ const cases = loadCases(values.cases);
16
+ if (cases.length === 0) {
17
+ process.stderr.write('No .json cases found in ' + values.cases + '\n');
18
+ return 2;
19
+ }
20
+ const reports = await runCases(root, cases);
21
+ out.write('\n ' + bold('PowerShot bench') + dim(' · ' + cases.length + ' labelled case(s)') + '\n\n');
22
+ for (const report of reports) {
23
+ if (!report.complete) {
24
+ out.write(' ' + yellow(report.state) + ' ' + report.name + dim(' ' + report.reasons.join('; ')) + '\n');
25
+ continue;
26
+ }
27
+ const exact = report.score.fp === 0 && report.score.fn === 0;
28
+ out.write(' ' + (exact ? steel('pass') : yellow('diff')) + ' ' + report.name +
29
+ dim(' tp ' + report.score.tp + ' · fp ' + report.score.fp + ' · fn ' + report.score.fn) + '\n');
30
+ }
31
+ const complete = reports.filter((report) => report.complete);
32
+ const score = total(complete.map((report) => report.score));
33
+ if (complete.length > 0) {
34
+ out.write('\n ' + bold('precision ' + percentage(precision(score))) + dim(' · ') +
35
+ 'recall ' + percentage(recall(score)) + dim(' · ') + 'F1 ' + percentage(f1(score)) + '\n\n');
36
+ }
37
+ else {
38
+ out.write('\n ' + yellow('No complete case could be scored.') + '\n\n');
39
+ }
40
+ if (complete.length !== reports.length)
41
+ return 3;
42
+ return score.fp + score.fn > 0 ? 1 : 0;
43
+ }
44
+ const last = positiveNumber(values.last, '--last', 50);
45
+ if (last === undefined)
46
+ return 2;
47
+ process.stderr.write(dim(' ◇ replaying ' + last + ' commit(s) from ' + root) + '\n');
48
+ const report = await replayRepo(root, last, (line) => process.stderr.write(dim(' ◇ ' + line) + '\n'));
49
+ out.write('\n ' + bold('PowerShot bench') + dim(' · ' + report.commits + ' complete commit(s)') + '\n');
50
+ out.write(dim(' ' + '─'.repeat(70)) + '\n\n');
51
+ if (report.findings === 0 && report.partial === 0 && report.failed === 0 && report.falseClean === 0) {
52
+ out.write(' ' + steel('✔') + ' No findings across ' + report.commits + ' reviewed commits.\n');
53
+ out.write(dim(' On already-merged history that is the result to want: the checks stayed quiet.') + '\n\n');
54
+ return 0;
55
+ }
56
+ out.write(' ' + report.findings + ' finding(s) on ' + report.noisy.length + ' of ' + report.commits + ' complete commits\n');
57
+ out.write(dim(' ' + report.verified + ' verified · ' + report.judged + ' judged' +
58
+ (report.positions.added + report.positions.context > 0
59
+ ? ' · judged on a changed line: ' + report.positions.added + '/' + (report.positions.added + report.positions.context)
60
+ : '')) + '\n');
61
+ const coverage = report.coverage;
62
+ out.write(dim(' coverage ' + coverage.selected + ' reviewed · ' + coverage.limited + ' limited · ' +
63
+ coverage.waived + ' waived · ' + coverage.failed + ' failed') + '\n');
64
+ out.write((report.falseClean > 0 ? yellow(' ' + report.falseClean + ' commit(s) reported clean by an incomplete run')
65
+ : dim(' no commit was reported clean by an incomplete run')) + '\n');
66
+ if (report.usage.requests > 0) {
67
+ out.write(dim(' ' + report.usage.requests + ' request(s) · ' + report.usage.inputTokens + ' in · ' +
68
+ report.usage.outputTokens + ' out · ' + Math.round(report.usage.elapsedMs / 1000) + 's') + '\n');
69
+ }
70
+ out.write(dim(' Already-merged code — treat each as a likely false positive until read.') + '\n\n');
71
+ const rows = [...report.byCheck.entries()].sort((left, right) => right[1] - left[1]);
72
+ for (const [check, count] of rows) {
73
+ out.write(' ' + String(count).padStart(4) + ' ' + check + dim(' ' + (count / report.commits).toFixed(2) + ' per commit') + '\n');
74
+ }
75
+ out.write('\n');
76
+ for (const commit of report.noisy.slice(0, 12)) {
77
+ out.write(dim(' ' + commit.commit + ' ' + commit.subject.slice(0, 58)) + '\n');
78
+ for (const finding of commit.findings.slice(0, 4)) {
79
+ out.write(' ' + yellow(finding.check) + ' ' + dim(finding.file + ':' + finding.line) + ' ' + finding.title.slice(0, 60) + '\n');
80
+ }
81
+ }
82
+ if (report.partial > 0)
83
+ out.write(dim('\n ' + report.partial + ' commit(s) were partial and not scored') + '\n');
84
+ if (report.failed > 0)
85
+ out.write(dim(' ' + report.failed + ' commit(s) failed and were not scored') + '\n');
86
+ out.write('\n');
87
+ return report.partial > 0 || report.failed > 0 || report.falseClean > 0 ? 3 : 0;
88
+ }
89
+ //# sourceMappingURL=bench-command.js.map
@@ -0,0 +1,42 @@
1
+ import { Dismissals, lastReport } from '#app/dismissed.js';
2
+ import { repoRoot } from '#app/git.js';
3
+ import { dim, bold, steel } from '#app/report/ansi.js';
4
+ export function runDismissCommand(positionals, reason) {
5
+ const root = repoRoot(process.cwd());
6
+ const dismissals = Dismissals.open(root);
7
+ const subject = positionals[1];
8
+ if (!subject || subject === 'list') {
9
+ const all = dismissals.list();
10
+ if (all.length === 0) {
11
+ process.stdout.write(dim('\n Nothing has been dismissed.\n\n'));
12
+ return 0;
13
+ }
14
+ process.stdout.write('\n ' + bold('Dismissed') + dim(' · committed, so the team shares it') + '\n\n');
15
+ for (const dismissal of all) {
16
+ process.stdout.write(' ' + steel(dismissal.fingerprint.slice(0, 8)) + ' ' + dismissal.check + dim(' · ' + dismissal.file) + '\n');
17
+ process.stdout.write(dim(' ' + dismissal.code.slice(0, 68)) + '\n');
18
+ if (dismissal.reason)
19
+ process.stdout.write(dim(' because: ' + dismissal.reason) + '\n');
20
+ }
21
+ process.stdout.write('\n' + dim(' psh dismiss restore <fingerprint>') + '\n\n');
22
+ return 0;
23
+ }
24
+ if (subject === 'restore') {
25
+ const fingerprint = positionals[2];
26
+ if (!fingerprint || !dismissals.remove(fingerprint)) {
27
+ process.stderr.write('No dismissal starting with "' + (fingerprint ?? '') + '". Try: psh dismiss list\n');
28
+ return 2;
29
+ }
30
+ process.stdout.write(dim(' restored — it will be reported again\n'));
31
+ return 0;
32
+ }
33
+ const finding = lastReport(root).find((item) => item.id.toLowerCase() === subject.toLowerCase());
34
+ if (!finding) {
35
+ process.stderr.write('No finding "' + subject + '" in the last review. Run psh review first.\n');
36
+ return 2;
37
+ }
38
+ const added = dismissals.add(finding, reason, new Date().toISOString());
39
+ process.stdout.write((added ? ' dismissed ' : ' already dismissed ') + finding.check + ' at ' + finding.file + ':' + finding.line + '\n');
40
+ return 0;
41
+ }
42
+ //# sourceMappingURL=dismiss-command.js.map
@@ -0,0 +1,32 @@
1
+ import { join } from 'node:path';
2
+ import { repoRoot } from '#app/git.js';
3
+ import { dim, yellow } from '#app/report/ansi.js';
4
+ const REPOSITORY_ENV = /_API_KEY$/;
5
+ export function loadRepositoryEnv() {
6
+ try {
7
+ if (process.env.CI)
8
+ return;
9
+ const inherited = { ...process.env };
10
+ process.loadEnvFile(join(repoRoot(process.cwd()), '.env'));
11
+ const refused = [];
12
+ for (const key of Object.keys(process.env)) {
13
+ if (Object.prototype.hasOwnProperty.call(inherited, key))
14
+ continue;
15
+ if (REPOSITORY_ENV.test(key))
16
+ continue;
17
+ delete process.env[key];
18
+ refused.push(key);
19
+ }
20
+ for (const [key, value] of Object.entries(inherited)) {
21
+ if (value !== undefined)
22
+ process.env[key] = value;
23
+ }
24
+ if (refused.length > 0) {
25
+ process.stderr.write(yellow(' ◇ .env') + dim(' ignored ' + refused.join(', ') + ' — only *_API_KEY may come from the repository') + '\n');
26
+ }
27
+ }
28
+ catch {
29
+ // A missing .env and a directory outside Git are both normal startup states.
30
+ }
31
+ }
32
+ //# sourceMappingURL=environment.js.map
@@ -0,0 +1,64 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { dim } from '#app/report/ansi.js';
3
+ import { codeQuality } from '#app/report/codequality.js';
4
+ import { compact } from '#app/report/compact.js';
5
+ import { markdown } from '#app/report/markdown.js';
6
+ import { sarif } from '#app/report/sarif.js';
7
+ import { terminal } from '#app/report/terminal.js';
8
+ export const REPORT_FORMATS = ['text', 'compact', 'markdown', 'json', 'sarif', 'codequality', 'manifest'];
9
+ export function isReportFormat(value) {
10
+ return REPORT_FORMATS.includes(value);
11
+ }
12
+ export function parseReportSpecs(values = []) {
13
+ const reports = [];
14
+ for (const spec of values) {
15
+ const separator = spec.indexOf('=');
16
+ const format = separator === -1 ? spec : spec.slice(0, separator);
17
+ if (!isReportFormat(format))
18
+ return '--report format must be one of: ' + REPORT_FORMATS.join(', ');
19
+ const path = separator === -1 ? '' : spec.slice(separator + 1);
20
+ if (!path)
21
+ return '--report needs a path: --report ' + format + '=<file>';
22
+ reports.push({ format, path });
23
+ }
24
+ return reports;
25
+ }
26
+ function jsonResult(result) {
27
+ return JSON.stringify(result, (_key, value) => (value instanceof Set ? [...value] : value), 2) + '\n';
28
+ }
29
+ export function renderReport(format, result, manifest, target) {
30
+ if (format === 'manifest')
31
+ return JSON.stringify(manifest, null, 2) + '\n';
32
+ if (format === 'compact')
33
+ return compact(result.findings);
34
+ if (format === 'codequality')
35
+ return codeQuality(result.findings);
36
+ if (format === 'sarif')
37
+ return sarif(result.findings);
38
+ if (format === 'json')
39
+ return jsonResult(result);
40
+ if (format === 'markdown')
41
+ return markdown(result.findings, manifest);
42
+ return terminal(result.findings, {
43
+ subtitle: target,
44
+ ...result.stats,
45
+ state: manifest.state,
46
+ notLookedAt: manifest.notLookedAt,
47
+ });
48
+ }
49
+ export function publishReports(options) {
50
+ const { result, manifest, target } = options;
51
+ for (const report of options.additional) {
52
+ writeFileSync(report.path, renderReport(report.format, result, manifest, target));
53
+ process.stderr.write(dim(' written to ' + report.path) + '\n');
54
+ }
55
+ const primary = renderReport(options.format, result, manifest, target);
56
+ if (options.output !== undefined) {
57
+ writeFileSync(options.output, primary);
58
+ process.stderr.write(dim(' written to ' + options.output) + '\n');
59
+ }
60
+ else {
61
+ process.stdout.write(primary);
62
+ }
63
+ }
64
+ //# sourceMappingURL=reports.js.map
@@ -0,0 +1,268 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { Budget, parseLimits } from '#app/budget.js';
4
+ import { loadConfig, policyChanged } from '#app/config.js';
5
+ import { absorbDelegated, delegateBrief } from '#app/delegate.js';
6
+ import { baseRefOf, checkRange, headSha, repoRoot, shaOf } from '#app/git.js';
7
+ import { JUDGES } from '#app/judges/prompts.js';
8
+ import { RunManifest, coverageProblems, hashOf, writeManifest } from '#app/manifest.js';
9
+ import { Trace } from '#app/otel.js';
10
+ import { PACKAGE_VERSION } from '#app/package-meta.js';
11
+ import { dim, yellow } from '#app/report/ansi.js';
12
+ import { progress, stage } from '#app/report/terminal.js';
13
+ import { review } from '#app/review.js';
14
+ import { scanPaths } from '#app/scan.js';
15
+ import { Session } from '#app/session.js';
16
+ import { withTargetTree } from '#app/snapshot.js';
17
+ import { SEVERITIES } from '#app/types.js';
18
+ import { VERIFIERS } from '#app/verifiers/index.js';
19
+ import { positiveNumber } from './args.js';
20
+ import { REPORT_FORMATS, isReportFormat, parseReportSpecs, publishReports, } from './reports.js';
21
+ function targetName(command, values, positionals) {
22
+ if (command === 'scan')
23
+ return 'scan ' + (positionals[1] ?? '.');
24
+ if (values.commit)
25
+ return 'commit ' + values.commit;
26
+ if (values.from)
27
+ return values.from + '...' + values.to;
28
+ return 'workspace';
29
+ }
30
+ export async function runReviewCommand(command, values, positionals) {
31
+ if (!isReportFormat(values.format)) {
32
+ process.stderr.write('--format must be one of: ' + REPORT_FORMATS.join(', ') + '\n');
33
+ return 2;
34
+ }
35
+ if ((values.from && !values.to) || (values.to && !values.from)) {
36
+ process.stderr.write('--from and --to must be given together.\n');
37
+ return 2;
38
+ }
39
+ const root = repoRoot(process.cwd());
40
+ const range = { from: values.from, to: values.to, commit: values.commit };
41
+ // Ref validation must precede policy loading because the policy is read through Git.
42
+ try {
43
+ checkRange(root, range);
44
+ }
45
+ catch (error) {
46
+ process.stderr.write(error.message + '\n');
47
+ return 2;
48
+ }
49
+ const gated = values.from !== undefined || values.commit !== undefined;
50
+ const policyBase = gated ? baseRefOf(root, range) : undefined;
51
+ const config = loadConfig(root, {
52
+ verifiers: [...new Set(VERIFIERS.flatMap((verifier) => [verifier.name, verifier.id ?? verifier.name]))],
53
+ judges: JUDGES.map((judge) => judge.name),
54
+ }, policyBase);
55
+ const verifierChecks = new Set(VERIFIERS.flatMap((verifier) => [verifier.name, verifier.id ?? verifier.name]));
56
+ const judgeChecks = new Set(JUDGES.map((judge) => judge.name));
57
+ const requestedChecks = values.checks?.split(',').map((check) => check.trim()).filter(Boolean);
58
+ if (values.checks !== undefined && requestedChecks?.length === 0) {
59
+ process.stderr.write('--checks must name at least one check\n');
60
+ return 2;
61
+ }
62
+ if (requestedChecks) {
63
+ const knownChecks = new Set([...verifierChecks, ...judgeChecks]);
64
+ const unknown = requestedChecks.filter((name) => !knownChecks.has(name));
65
+ if (unknown.length > 0) {
66
+ process.stderr.write('--checks has unknown name(s): ' + unknown.join(', ') + '\n');
67
+ return 2;
68
+ }
69
+ if (command === 'delegate') {
70
+ const deterministic = requestedChecks.filter((name) => !judgeChecks.has(name));
71
+ if (deterministic.length > 0) {
72
+ process.stderr.write('delegate accepts judge names only, not: ' + deterministic.join(', ') + '\n');
73
+ return 2;
74
+ }
75
+ }
76
+ else if (values['verify-only']) {
77
+ const judges = requestedChecks.filter((name) => judgeChecks.has(name) && !verifierChecks.has(name));
78
+ if (judges.length > 0) {
79
+ process.stderr.write('--verify-only cannot run judge check(s): ' + judges.join(', ') + '\n');
80
+ return 2;
81
+ }
82
+ }
83
+ }
84
+ if (policyBase && policyChanged(root, policyBase)) {
85
+ process.stderr.write(yellow(' ◇ policy') + dim(' this change edits powershot.config.json — reviewed under the base policy') + '\n');
86
+ }
87
+ if (values['min-severity']) {
88
+ const minimum = values['min-severity'];
89
+ if (!SEVERITIES.includes(minimum)) {
90
+ process.stderr.write('--min-severity must be one of: ' + SEVERITIES.join(', ') + '\n');
91
+ return 2;
92
+ }
93
+ config.minSeverity = minimum;
94
+ }
95
+ const bundleLimit = positiveNumber(values['max-bundle'], '--max-bundle', 1200);
96
+ if (bundleLimit === undefined)
97
+ return 2;
98
+ const maxBundle = values['max-bundle'] === undefined ? undefined : bundleLimit;
99
+ const reportSpecs = parseReportSpecs(values.report);
100
+ if (typeof reportSpecs === 'string') {
101
+ process.stderr.write(reportSpecs + '\n');
102
+ return 2;
103
+ }
104
+ if (command === 'delegate' && values.absorb !== undefined) {
105
+ process.stderr.write('--absorb cannot be used with delegate\n');
106
+ return 2;
107
+ }
108
+ // Invalid delegated output must not leave behind a resumable session.
109
+ const absorbed = values.absorb !== undefined
110
+ ? absorbDelegated(readFileSync(values.absorb, 'utf8'))
111
+ : undefined;
112
+ let limits = {};
113
+ if (values.budget !== undefined) {
114
+ const parsed = parseLimits(values.budget);
115
+ if (typeof parsed === 'string') {
116
+ process.stderr.write(parsed + '\n');
117
+ return 2;
118
+ }
119
+ limits = parsed;
120
+ }
121
+ const quiet = values.format === 'json' || values.format === 'sarif' || values.format === 'compact';
122
+ const target = targetName(command, values, positionals);
123
+ let session;
124
+ if (values.resume !== undefined) {
125
+ session = Session.open(root, values.resume);
126
+ if (!session) {
127
+ process.stderr.write('No session ' + values.resume + '. Try: psh session list\n');
128
+ return 2;
129
+ }
130
+ if (!session.askedBy(config.provider, config.model)) {
131
+ const previous = session.asked;
132
+ process.stderr.write('Session ' + values.resume + ' was judged by ' + previous?.provider + '/' + previous?.model +
133
+ ', and this run would use ' + config.provider + '/' + config.model + '.\n' +
134
+ 'Resuming would mix two reviewers into one report. Start a fresh run, or switch back.\n');
135
+ return 2;
136
+ }
137
+ }
138
+ else if (!values['verify-only'] && command !== 'delegate') {
139
+ session = Session.create(root, target, { provider: config.provider, model: config.model });
140
+ }
141
+ if (command === 'delegate') {
142
+ const { buildGround } = await import('#app/ground.js');
143
+ const { collectChanges, statedIntent } = await import('#app/git.js');
144
+ const { matchesAny } = await import('#app/config.js');
145
+ const changes = collectChanges(root, range).filter((change) => !matchesAny(change.path, config.ignore));
146
+ if (changes.length === 0) {
147
+ process.stderr.write('Nothing to review.\n');
148
+ return 0;
149
+ }
150
+ const ground = await withTargetTree(root, range, (tree) => buildGround(tree, changes));
151
+ process.stdout.write(delegateBrief(ground, config, {
152
+ intent: statedIntent(root, range),
153
+ maxBundleLines: maxBundle,
154
+ checks: requestedChecks,
155
+ }));
156
+ return 0;
157
+ }
158
+ const canceller = new AbortController();
159
+ let interrupted = false;
160
+ const onInterrupt = () => {
161
+ if (interrupted)
162
+ process.exit(130);
163
+ interrupted = true;
164
+ canceller.abort();
165
+ process.stderr.write(dim('\n ◇ stopping after the current unit — press Ctrl-C again to exit now') + '\n');
166
+ };
167
+ const manifest = new RunManifest(session?.id ?? createHash('sha1').update(target + ':' + Date.now()).digest('hex').slice(0, 8));
168
+ const trace = new Trace();
169
+ const runSpan = trace.span('review', { target });
170
+ const result = await withTargetTree(root, command === 'scan' ? {} : range, (tree) => review({
171
+ root: tree,
172
+ stateRoot: root,
173
+ range,
174
+ changes: command === 'scan' ? scanPaths(root, positionals[1] ?? '.') : undefined,
175
+ config,
176
+ session,
177
+ tools: values.tools,
178
+ maxBundleLines: maxBundle,
179
+ signal: canceller.signal,
180
+ cache: !values['no-cache'],
181
+ budget: new Budget(limits),
182
+ manifest,
183
+ showDismissed: values['show-dismissed'],
184
+ absorbed,
185
+ onCancelable: (active) => {
186
+ if (!active) {
187
+ process.off('SIGINT', onInterrupt);
188
+ process.off('SIGTERM', onInterrupt);
189
+ return;
190
+ }
191
+ process.on('SIGINT', onInterrupt);
192
+ process.on('SIGTERM', onInterrupt);
193
+ },
194
+ verifyOnly: values['verify-only'],
195
+ checks: requestedChecks,
196
+ onProgress: quiet ? undefined : (line) => process.stderr.write(progress(line) + '\n'),
197
+ onStage: quiet ? undefined : stage,
198
+ }));
199
+ runSpan({
200
+ findings: result.findings.length,
201
+ verified: result.stats.verified,
202
+ judged: result.stats.judged,
203
+ files: result.stats.files,
204
+ });
205
+ await trace.flush();
206
+ const record = manifest.build({
207
+ operation: command,
208
+ repositoryHead: headSha(root),
209
+ target: {
210
+ requested: { from: values.from, to: values.to, commit: values.commit },
211
+ base: policyBase ? shaOf(root, policyBase) : undefined,
212
+ head: headSha(root),
213
+ },
214
+ policy: {
215
+ source: policyBase ? 'base' : 'head',
216
+ ref: policyBase,
217
+ hash: hashOf(JSON.stringify(config)),
218
+ },
219
+ engine: {
220
+ version: PACKAGE_VERSION,
221
+ provider: config.provider,
222
+ model: config.model,
223
+ tools: values.tools,
224
+ verifyOnly: values['verify-only'],
225
+ },
226
+ files: result.plan?.items() ?? [],
227
+ skippedChecks: result.skippedChecks ?? [],
228
+ findings: {
229
+ total: result.findings.length,
230
+ verified: result.stats.verified,
231
+ judged: result.stats.judged,
232
+ dismissed: result.stats.dismissed,
233
+ droppedPosition: result.droppedPosition ?? 0,
234
+ },
235
+ usage: result.usage ?? { requests: 0, inputTokens: 0, outputTokens: 0, toolCalls: 0, elapsedMs: 0, units: 0 },
236
+ failures: result.failures,
237
+ cancelled: result.cancelled,
238
+ budgetStop: result.budgetStop,
239
+ });
240
+ const gaps = coverageProblems(record);
241
+ if (gaps.length > 0) {
242
+ const failure = 'manifest: ' + gaps.join('; ');
243
+ result.failures.push(failure);
244
+ record.state = 'failed';
245
+ record.failures.push(failure);
246
+ record.notLookedAt.push(failure);
247
+ process.stderr.write(yellow(' ◇ manifest') + dim(' ' + gaps.join('; ')) + '\n');
248
+ }
249
+ session?.saveReport(result.findings, { state: record.state, notLookedAt: record.notLookedAt });
250
+ writeManifest(root, record);
251
+ publishReports({
252
+ format: values.format,
253
+ output: values.output,
254
+ additional: reportSpecs,
255
+ result,
256
+ manifest: record,
257
+ target,
258
+ });
259
+ if (values.format === 'text' && session && !values['verify-only']) {
260
+ process.stderr.write(dim(' session ' + session.id + ' · psh review --resume ' + session.id) + '\n');
261
+ }
262
+ if (interrupted)
263
+ return 130;
264
+ if (result.failures.length > 0 || gaps.length > 0 || record.state !== 'complete')
265
+ return 3;
266
+ return result.findings.length > 0 ? 1 : 0;
267
+ }
268
+ //# sourceMappingURL=review-command.js.map
@@ -0,0 +1,62 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { repoRoot } from '#app/git.js';
4
+ import { dim, bold, steel, yellow } from '#app/report/ansi.js';
5
+ import { viewer } from '#app/report/viewer.js';
6
+ import { Session } from '#app/session.js';
7
+ export function runSessionCommand(positionals) {
8
+ const root = repoRoot(process.cwd());
9
+ const operation = positionals[1];
10
+ if (operation === 'diff') {
11
+ const older = positionals[2] ? Session.open(root, positionals[2]) : undefined;
12
+ const newer = positionals[3] ? Session.open(root, positionals[3]) : undefined;
13
+ if (!older?.report || !newer?.report || older.report.state !== 'complete' || newer.report.state !== 'complete') {
14
+ process.stderr.write('Need two complete sessions: psh session diff <older> <newer>\n');
15
+ return 2;
16
+ }
17
+ const { fixed, introduced, remaining } = Session.compare(older, newer);
18
+ const out = process.stdout;
19
+ out.write('\n ' + bold('Between ') + older.id + dim(' and ') + newer.id + '\n\n');
20
+ const show = (label, list, paint) => {
21
+ out.write(' ' + paint(String(list.length).padStart(3) + ' ' + label) + '\n');
22
+ for (const finding of list.slice(0, 10)) {
23
+ out.write(dim(' ' + finding.check + ' ' + finding.file + ':' + finding.line + ' ') + finding.title.slice(0, 60) + '\n');
24
+ }
25
+ };
26
+ show('fixed', fixed, steel);
27
+ show('introduced', introduced, yellow);
28
+ out.write(' ' + dim(String(remaining.length).padStart(3) + ' still there') + '\n\n');
29
+ return introduced.length > 0 ? 1 : 0;
30
+ }
31
+ if (operation === 'view') {
32
+ const id = positionals[2];
33
+ const session = id ? Session.open(root, id) : undefined;
34
+ if (!session?.report) {
35
+ process.stderr.write('No finished session ' + (id ?? '') + '. Try: psh session list\n');
36
+ return 2;
37
+ }
38
+ const output = join(root, '.powershot', 'sessions', session.id + '.html');
39
+ writeFileSync(output, viewer(session.report.findings, {
40
+ id: session.id,
41
+ target: session.target,
42
+ started: session.started,
43
+ state: session.report.state ?? 'unknown',
44
+ notLookedAt: session.report.notLookedAt ?? ['session predates verdict recording'],
45
+ }));
46
+ process.stdout.write(output + '\n');
47
+ return 0;
48
+ }
49
+ const rows = Session.list(root);
50
+ if (rows.length === 0) {
51
+ process.stdout.write(dim('\n No resumable sessions.\n\n'));
52
+ return 0;
53
+ }
54
+ process.stdout.write('\n ' + bold('Sessions') + '\n\n');
55
+ for (const row of rows) {
56
+ process.stdout.write(' ' + steel(row.id) + ' ' + dim(row.started.slice(0, 19).replace('T', ' ')) +
57
+ ' ' + row.target + dim(' ' + row.done + ' unit(s) done') + '\n');
58
+ }
59
+ process.stdout.write('\n' + dim(' psh review --resume <id>') + '\n\n');
60
+ return 0;
61
+ }
62
+ //# sourceMappingURL=session-command.js.map
package/dist/cli.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from './cli/app.js';
3
+ runCli().then((code) => process.exit(code), (error) => {
4
+ process.stderr.write('psh: ' + error.message + '\n');
5
+ process.exit(2);
6
+ });
7
+ //# sourceMappingURL=cli.js.map