clud-bug 0.7.0-rc.21 → 0.7.0-rc.23
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/data/canonical-v1.json +39 -34
- package/data/rulesets/baseline.json +29 -0
- package/data/rulesets/clud-bug.json +39 -0
- package/data/rulesets/public-guard.json +29 -0
- package/data/rulesets/skdd.json +42 -0
- package/dist/cli/configure-github.d.ts +13 -4
- package/dist/cli/configure-github.d.ts.map +1 -1
- package/dist/cli/configure-github.js +73 -27
- package/dist/cli/configure-github.js.map +1 -1
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +5 -1
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +33 -10
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/post-check-run.d.ts +2 -0
- package/dist/cli/post-check-run.d.ts.map +1 -1
- package/dist/cli/post-check-run.js +124 -5
- package/dist/cli/post-check-run.js.map +1 -1
- package/dist/cli/review-prompt.d.ts +11 -1
- package/dist/cli/review-prompt.d.ts.map +1 -1
- package/dist/cli/review-prompt.js +141 -37
- package/dist/cli/review-prompt.js.map +1 -1
- package/dist/core/check-verdict.d.ts +1 -1
- package/dist/core/check-verdict.d.ts.map +1 -1
- package/dist/core/check-verdict.js +14 -1
- package/dist/core/check-verdict.js.map +1 -1
- package/dist/core/configure-github.d.ts +184 -119
- package/dist/core/configure-github.d.ts.map +1 -1
- package/dist/core/configure-github.js +407 -209
- package/dist/core/configure-github.js.map +1 -1
- package/dist/core/index.d.ts +4 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +12 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/invariants.d.ts +46 -0
- package/dist/core/invariants.d.ts.map +1 -0
- package/dist/core/invariants.js +96 -0
- package/dist/core/invariants.js.map +1 -0
- package/dist/core/notary-bundle.d.ts +95 -0
- package/dist/core/notary-bundle.d.ts.map +1 -0
- package/dist/core/notary-bundle.js +129 -0
- package/dist/core/notary-bundle.js.map +1 -0
- package/dist/core/notary-validate.d.ts +86 -0
- package/dist/core/notary-validate.d.ts.map +1 -0
- package/dist/core/notary-validate.js +270 -0
- package/dist/core/notary-validate.js.map +1 -0
- package/dist/core/prompt-builder.js +2 -2
- package/dist/core/review-schema-zod.d.ts +48 -0
- package/dist/core/review-schema-zod.d.ts.map +1 -1
- package/dist/core/review-schema-zod.js +5 -0
- package/dist/core/review-schema-zod.js.map +1 -1
- package/dist/core/review-schema.d.ts +3 -0
- package/dist/core/review-schema.d.ts.map +1 -1
- package/dist/core/review-schema.js +9 -0
- package/dist/core/review-schema.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/package.json +1 -1
- package/src/cli/configure-github.ts +108 -31
- package/src/cli/hooks.ts +5 -1
- package/src/cli/main.ts +32 -10
- package/src/cli/post-check-run.ts +149 -5
- package/src/cli/review-prompt.ts +162 -36
- package/src/core/check-verdict.ts +14 -2
- package/src/core/configure-github.ts +591 -349
- package/src/core/index.ts +46 -0
- package/src/core/invariants.ts +123 -0
- package/src/core/notary-bundle.ts +196 -0
- package/src/core/notary-validate.ts +343 -0
- package/src/core/prompt-builder.ts +2 -2
- package/src/core/review-schema-zod.ts +5 -0
- package/src/core/review-schema.ts +12 -0
- package/src/core/version.ts +1 -1
- package/templates/skills/design/designing-elite-ui.md +57 -0
- package/templates/workflow-py.yml.tmpl +1 -1
- package/templates/workflow-ts.yml.tmpl +1 -1
- package/templates/workflow.yml.tmpl +1 -1
|
@@ -25,13 +25,23 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
25
25
|
import {
|
|
26
26
|
applyCanonicalRuleset,
|
|
27
27
|
loadCanonicalV1,
|
|
28
|
+
loadPreset,
|
|
29
|
+
isPresetName,
|
|
30
|
+
DEFAULT_PRESET,
|
|
31
|
+
PRESET_NAMES,
|
|
28
32
|
type OctokitLike,
|
|
29
33
|
type ApplyResult,
|
|
34
|
+
type PresetName,
|
|
30
35
|
} from '../core/configure-github.js';
|
|
31
36
|
|
|
32
37
|
export interface RunConfigureGithubOptions {
|
|
33
38
|
/** "owner/repo" target — required. */
|
|
34
39
|
target?: string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Ruleset preset: baseline · clud-bug · skdd · public-guard.
|
|
42
|
+
* Defaults to `skdd`. Invalid values exit 2 (usage error).
|
|
43
|
+
*/
|
|
44
|
+
preset?: string;
|
|
35
45
|
/** Target branch (default `main`). */
|
|
36
46
|
branch?: string;
|
|
37
47
|
/** Render diff only; skip PATCH calls. */
|
|
@@ -50,13 +60,22 @@ export interface RunConfigureGithubOptions {
|
|
|
50
60
|
stderr?: (msg: string) => void;
|
|
51
61
|
}
|
|
52
62
|
|
|
53
|
-
const HELP = `clud-bug configure-github —
|
|
63
|
+
const HELP = `clud-bug configure-github — one-stop repo setup (conveniences + SPEC §7 ruleset).
|
|
54
64
|
|
|
55
65
|
Usage:
|
|
56
66
|
clud-bug configure-github <owner>/<repo> [options]
|
|
57
67
|
|
|
68
|
+
Applies, in order, for the selected preset:
|
|
69
|
+
1. Repo conveniences (ALL presets): squash-only merges, auto-merge +
|
|
70
|
+
delete-branch-on-merge on, PR title/body as the squash commit message.
|
|
71
|
+
2. The canonical branch ruleset for the chosen preset.
|
|
72
|
+
|
|
58
73
|
Options:
|
|
59
|
-
--
|
|
74
|
+
--preset <name> Ruleset preset: baseline | clud-bug | skdd | public-guard.
|
|
75
|
+
Default: skdd. (baseline = structural only; clud-bug adds
|
|
76
|
+
the clud-bug-review gate; skdd adds SkDD derived-docs
|
|
77
|
+
checks; public-guard = 1 approval + code-owner.)
|
|
78
|
+
--dry-run Compute diff and print it, but do NOT create/update anything.
|
|
60
79
|
--branch <name> Target branch (default: main).
|
|
61
80
|
--quiet,-q Suppress progress chatter; emit only the final summary.
|
|
62
81
|
--json Emit the status payload as JSON (machine consumption).
|
|
@@ -68,7 +87,7 @@ Auth (resolved in order):
|
|
|
68
87
|
|
|
69
88
|
Exit codes:
|
|
70
89
|
0 success or already-canonical
|
|
71
|
-
1 auth missing,
|
|
90
|
+
1 auth missing, ruleset write error, or unrecoverable transport failure
|
|
72
91
|
2 CLI usage error (missing target, bad flag)
|
|
73
92
|
`;
|
|
74
93
|
|
|
@@ -76,6 +95,8 @@ Exit codes:
|
|
|
76
95
|
export interface ConfigureSummary {
|
|
77
96
|
owner: string;
|
|
78
97
|
repo: string;
|
|
98
|
+
/** The preset applied (baseline · clud-bug · skdd · public-guard). */
|
|
99
|
+
preset?: string;
|
|
79
100
|
/** True when the repo already matches canonical-v1 (idempotent no-op). */
|
|
80
101
|
alreadyCanonical: boolean;
|
|
81
102
|
/** True when this was a --dry-run (no PATCH calls made). */
|
|
@@ -90,20 +111,22 @@ export interface ConfigureSummary {
|
|
|
90
111
|
* consumption, key-colon-value for humans.
|
|
91
112
|
*/
|
|
92
113
|
export function formatConfigureSummary(summary: ConfigureSummary, json: boolean): string {
|
|
93
|
-
const { owner, repo, alreadyCanonical, dryRun, changes } = summary;
|
|
114
|
+
const { owner, repo, preset, alreadyCanonical, dryRun, changes } = summary;
|
|
94
115
|
if (json) {
|
|
95
116
|
return (
|
|
96
|
-
JSON.stringify({ owner, repo, alreadyCanonical, rulesetVersion: '
|
|
117
|
+
JSON.stringify({ owner, repo, preset, alreadyCanonical, rulesetVersion: 'v2', dryRun, changes }) + '\n'
|
|
97
118
|
);
|
|
98
119
|
}
|
|
120
|
+
const presetTag = preset ? ` preset: ${preset}` : '';
|
|
121
|
+
const presetParen = preset ? ` (${preset})` : '';
|
|
99
122
|
if (alreadyCanonical) {
|
|
100
|
-
return `ok configure-github: owner: ${owner} repo: ${repo} alreadyCanonical: true rulesetVersion:
|
|
123
|
+
return `ok configure-github: owner: ${owner} repo: ${repo}${presetTag} alreadyCanonical: true rulesetVersion: v2\n`;
|
|
101
124
|
}
|
|
102
125
|
const plural = changes === 1 ? '' : 's';
|
|
103
126
|
if (dryRun) {
|
|
104
|
-
return `ok configure-github: dry-run on ${owner}/${repo} — ${changes} change${plural} pending\n`;
|
|
127
|
+
return `ok configure-github: dry-run on ${owner}/${repo}${presetParen} — ${changes} change${plural} pending\n`;
|
|
105
128
|
}
|
|
106
|
-
return `ok configure-github: ${owner}/${repo} converged to canonical-v1 (${changes} change${plural})\n`;
|
|
129
|
+
return `ok configure-github: ${owner}/${repo} converged to canonical-v1${presetParen} (${changes} change${plural})\n`;
|
|
107
130
|
}
|
|
108
131
|
|
|
109
132
|
/**
|
|
@@ -117,6 +140,7 @@ export async function runConfigureGithub(
|
|
|
117
140
|
): Promise<number> {
|
|
118
141
|
const {
|
|
119
142
|
target,
|
|
143
|
+
preset: presetOpt,
|
|
120
144
|
branch = 'main',
|
|
121
145
|
dryRun = false,
|
|
122
146
|
quiet = false,
|
|
@@ -140,6 +164,15 @@ export async function runConfigureGithub(
|
|
|
140
164
|
}
|
|
141
165
|
const [, owner, repo] = match as unknown as [unknown, string, string];
|
|
142
166
|
|
|
167
|
+
// Resolve + validate the preset (usage error before any network call).
|
|
168
|
+
const preset: string = presetOpt ?? DEFAULT_PRESET;
|
|
169
|
+
if (!isPresetName(preset)) {
|
|
170
|
+
stderr(
|
|
171
|
+
`clud-bug configure-github: unknown preset "${preset}" (valid: ${PRESET_NAMES.join(', ')}).\n`,
|
|
172
|
+
);
|
|
173
|
+
return 2;
|
|
174
|
+
}
|
|
175
|
+
|
|
143
176
|
const token = await resolveToken();
|
|
144
177
|
if (!token) {
|
|
145
178
|
stderr(
|
|
@@ -151,7 +184,7 @@ export async function runConfigureGithub(
|
|
|
151
184
|
|
|
152
185
|
if (!quiet) {
|
|
153
186
|
stdout(
|
|
154
|
-
`\u{1F41B} configure-github: applying
|
|
187
|
+
`\u{1F41B} configure-github: applying "${preset}" preset (conveniences + ruleset) to ${owner}/${repo} (branch=${branch})\n`,
|
|
155
188
|
);
|
|
156
189
|
}
|
|
157
190
|
|
|
@@ -174,18 +207,19 @@ export async function runConfigureGithub(
|
|
|
174
207
|
owner,
|
|
175
208
|
repo,
|
|
176
209
|
branch,
|
|
210
|
+
preset,
|
|
177
211
|
dryRun,
|
|
178
212
|
});
|
|
179
213
|
} catch (err) {
|
|
180
214
|
stderr(
|
|
181
|
-
`clud-bug configure-github: ${dryRun ? 'failed to read current state' : '
|
|
215
|
+
`clud-bug configure-github: ${dryRun ? 'failed to read current state' : 'ruleset write failed'}: ${stringifyError(err)}\n`,
|
|
182
216
|
);
|
|
183
217
|
return 1;
|
|
184
218
|
}
|
|
185
219
|
|
|
186
220
|
if (result.alreadyCanonical) {
|
|
187
221
|
if (!quiet) stdout(' No changes — repo already matches canonical-v1.\n');
|
|
188
|
-
stdout(formatConfigureSummary({ owner, repo, alreadyCanonical: true, dryRun, changes: 0 }, json));
|
|
222
|
+
stdout(formatConfigureSummary({ owner, repo, preset, alreadyCanonical: true, dryRun, changes: 0 }, json));
|
|
189
223
|
return 0;
|
|
190
224
|
}
|
|
191
225
|
|
|
@@ -196,11 +230,11 @@ export async function runConfigureGithub(
|
|
|
196
230
|
}
|
|
197
231
|
|
|
198
232
|
if (dryRun) {
|
|
199
|
-
stdout(formatConfigureSummary({ owner, repo, alreadyCanonical: false, dryRun: true, changes: result.changes.length }, json));
|
|
233
|
+
stdout(formatConfigureSummary({ owner, repo, preset, alreadyCanonical: false, dryRun: true, changes: result.changes.length }, json));
|
|
200
234
|
return 0;
|
|
201
235
|
}
|
|
202
236
|
|
|
203
|
-
stdout(formatConfigureSummary({ owner, repo, alreadyCanonical: false, dryRun: false, changes: result.changes.length }, json));
|
|
237
|
+
stdout(formatConfigureSummary({ owner, repo, preset, alreadyCanonical: false, dryRun: false, changes: result.changes.length }, json));
|
|
204
238
|
return 0;
|
|
205
239
|
}
|
|
206
240
|
|
|
@@ -242,9 +276,9 @@ export function ghCliOctokit(token: string): OctokitLike {
|
|
|
242
276
|
args.push('--input', '-');
|
|
243
277
|
}
|
|
244
278
|
args.push(path);
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
279
|
+
// Pin the rulesets API media type; the wrapping error carries the
|
|
280
|
+
// parsed HTTP status (from `HTTP NNN` in gh's stderr) so transport
|
|
281
|
+
// failures surface with a real status code, not a shell hint.
|
|
248
282
|
args.push('-H', 'Accept: application/vnd.github+json');
|
|
249
283
|
const child = spawn('gh', args, {
|
|
250
284
|
env,
|
|
@@ -289,26 +323,67 @@ export function ghCliOctokit(token: string): OctokitLike {
|
|
|
289
323
|
|
|
290
324
|
return {
|
|
291
325
|
repos: {
|
|
292
|
-
async
|
|
326
|
+
async getRepoRulesets({ owner, repo, includes_parents }) {
|
|
327
|
+
// `?per_page=100` (not `--paginate`) to grab all rulesets in one
|
|
328
|
+
// call — a repo realistically has a handful, and --paginate emits
|
|
329
|
+
// multiple concatenated JSON arrays that our single JSON.parse can't
|
|
330
|
+
// consume (see the v0.6.30 --paginate → ?per_page=100 decision).
|
|
331
|
+
const parents = includes_parents === undefined ? false : includes_parents;
|
|
293
332
|
const data = await ghApi<unknown>(
|
|
294
333
|
'GET',
|
|
295
|
-
`/repos/${owner}/${repo}/
|
|
334
|
+
`/repos/${owner}/${repo}/rulesets?per_page=100&includes_parents=${parents}`,
|
|
296
335
|
);
|
|
297
|
-
return {
|
|
336
|
+
return {
|
|
337
|
+
data: (Array.isArray(data) ? data : []) as Awaited<
|
|
338
|
+
ReturnType<OctokitLike['repos']['getRepoRulesets']>
|
|
339
|
+
>['data'],
|
|
340
|
+
};
|
|
298
341
|
},
|
|
299
|
-
async
|
|
300
|
-
|
|
301
|
-
'
|
|
302
|
-
`/repos/${owner}/${repo}/
|
|
303
|
-
rest,
|
|
342
|
+
async getRepoRuleset({ owner, repo, ruleset_id }) {
|
|
343
|
+
const data = await ghApi<unknown>(
|
|
344
|
+
'GET',
|
|
345
|
+
`/repos/${owner}/${repo}/rulesets/${ruleset_id}`,
|
|
304
346
|
);
|
|
347
|
+
return {
|
|
348
|
+
data: data as Awaited<
|
|
349
|
+
ReturnType<OctokitLike['repos']['getRepoRuleset']>
|
|
350
|
+
>['data'],
|
|
351
|
+
};
|
|
305
352
|
},
|
|
306
|
-
async
|
|
353
|
+
async createRepoRuleset({ owner, repo, ...body }) {
|
|
307
354
|
const data = await ghApi<unknown>(
|
|
308
|
-
'
|
|
309
|
-
`/repos/${owner}/${repo}`,
|
|
355
|
+
'POST',
|
|
356
|
+
`/repos/${owner}/${repo}/rulesets`,
|
|
357
|
+
body,
|
|
358
|
+
);
|
|
359
|
+
return {
|
|
360
|
+
data: data as Awaited<
|
|
361
|
+
ReturnType<OctokitLike['repos']['createRepoRuleset']>
|
|
362
|
+
>['data'],
|
|
363
|
+
};
|
|
364
|
+
},
|
|
365
|
+
async updateRepoRuleset({ owner, repo, ruleset_id, ...body }) {
|
|
366
|
+
const data = await ghApi<unknown>(
|
|
367
|
+
'PUT',
|
|
368
|
+
`/repos/${owner}/${repo}/rulesets/${ruleset_id}`,
|
|
369
|
+
body,
|
|
310
370
|
);
|
|
311
|
-
return {
|
|
371
|
+
return {
|
|
372
|
+
data: data as Awaited<
|
|
373
|
+
ReturnType<OctokitLike['repos']['updateRepoRuleset']>
|
|
374
|
+
>['data'],
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
// Repo-level conveniences (universal hygiene): GET the repo settings,
|
|
378
|
+
// PATCH the drifted merge/branch-cleanup fields. Restored alongside the
|
|
379
|
+
// ruleset applier so `configure-github` is the one-stop setup command.
|
|
380
|
+
async get({ owner, repo }) {
|
|
381
|
+
const data = await ghApi<unknown>('GET', `/repos/${owner}/${repo}`);
|
|
382
|
+
return {
|
|
383
|
+
data: data as Awaited<
|
|
384
|
+
ReturnType<OctokitLike['repos']['get']>
|
|
385
|
+
>['data'],
|
|
386
|
+
};
|
|
312
387
|
},
|
|
313
388
|
async update({ owner, repo, ...rest }) {
|
|
314
389
|
return ghApi('PATCH', `/repos/${owner}/${repo}`, rest);
|
|
@@ -323,7 +398,9 @@ function stringifyError(err: unknown): string {
|
|
|
323
398
|
}
|
|
324
399
|
|
|
325
400
|
/**
|
|
326
|
-
* Re-exported so callers can preload
|
|
327
|
-
* flag in future).
|
|
401
|
+
* Re-exported so callers can preload a ruleset (e.g. for a `--show-ruleset`
|
|
402
|
+
* flag in future). `loadCanonicalV1` resolves the default (skdd) preset;
|
|
403
|
+
* `loadPreset(name)` picks a specific variant.
|
|
328
404
|
*/
|
|
329
|
-
export { loadCanonicalV1 };
|
|
405
|
+
export { loadCanonicalV1, loadPreset };
|
|
406
|
+
export type { PresetName };
|
package/src/cli/hooks.ts
CHANGED
|
@@ -57,7 +57,11 @@ export function buildCommitReviewCommand(pin: string = 'next'): string {
|
|
|
57
57
|
`if [ -n "$ev" ]; then case "$ev" in *'git commit'*|*'logmind log'*) ;; *) exit 0 ;; esac; fi`,
|
|
58
58
|
`sha=$(git rev-parse HEAD 2>/dev/null) || exit 0`,
|
|
59
59
|
`gitdir=$(git rev-parse --git-dir 2>/dev/null) || exit 0`,
|
|
60
|
-
|
|
60
|
+
// Pure LOOP-GUARD, not an attestation (§10.3.3): records only that the recipe
|
|
61
|
+
// was already SURFACED for this SHA (avoids a re-review on an amend / double
|
|
62
|
+
// fire). It never means "reviewed" and never yields a green check — the only
|
|
63
|
+
// authoritative "reviewed" state is the notary-issued `clud-bug-review` check.
|
|
64
|
+
`marker="$gitdir/clud-bug-hook-fired"`,
|
|
61
65
|
`[ "$(cat "$marker" 2>/dev/null)" = "$sha" ] && exit 0`,
|
|
62
66
|
// H4 — one retry on a transient npx/network blip (a stale lock, a slow
|
|
63
67
|
// registry) before giving up, so a hiccup doesn't silently skip the review.
|
package/src/cli/main.ts
CHANGED
|
@@ -71,7 +71,7 @@ function parseArgs(argv) {
|
|
|
71
71
|
withLocalReview: false,
|
|
72
72
|
withHooks: false,
|
|
73
73
|
// rc.16: `clud-bug init --with-design` installs the design-critic kit
|
|
74
|
-
// (
|
|
74
|
+
// (4 `kind: design` skills) and flips the off-by-default `design` block to
|
|
75
75
|
// enabled so the visual review lens runs (local recipe + hosted bot).
|
|
76
76
|
withDesign: false,
|
|
77
77
|
// rc.18: `clud-bug init --local-only` installs MAX MODE (skills + slash
|
|
@@ -82,8 +82,10 @@ function parseArgs(argv) {
|
|
|
82
82
|
localOnly: false,
|
|
83
83
|
// v0.7.0-rc.4: `clud-bug configure-github` flags.
|
|
84
84
|
// --dry-run prints the diff but skips PATCH; --branch overrides "main".
|
|
85
|
+
// --preset selects the vendored ruleset variant (default: skdd).
|
|
85
86
|
dryRun: false,
|
|
86
87
|
branch: null,
|
|
88
|
+
preset: null,
|
|
87
89
|
};
|
|
88
90
|
for (let i = 0; i < argv.length; i++) {
|
|
89
91
|
const a = argv[i];
|
|
@@ -112,6 +114,7 @@ function parseArgs(argv) {
|
|
|
112
114
|
else if (a === '--local-only') args.localOnly = true;
|
|
113
115
|
else if (a === '--dry-run') args.dryRun = true;
|
|
114
116
|
else if (a === '--branch') args.branch = argv[++i];
|
|
117
|
+
else if (a === '--preset') args.preset = argv[++i];
|
|
115
118
|
else if (a === '--trigger') args.trigger = argv[++i];
|
|
116
119
|
else if (a === '--diff-size') args.diffSizeBytes = Number(argv[++i]);
|
|
117
120
|
// H3: `clud-bug post-check-run` flags.
|
|
@@ -123,6 +126,8 @@ function parseArgs(argv) {
|
|
|
123
126
|
else if (a === '--no-strict') args.strict = false;
|
|
124
127
|
else if (a === '--owner') args.owner = argv[++i];
|
|
125
128
|
else if (a === '--details-url') args.detailsUrl = argv[++i];
|
|
129
|
+
// Phase Z: notary attestation bundle (JSON path) for `post-check-run`.
|
|
130
|
+
else if (a === '--bundle') args.bundle = argv[++i];
|
|
126
131
|
else args._.push(a);
|
|
127
132
|
}
|
|
128
133
|
return args;
|
|
@@ -145,11 +150,14 @@ Commands:
|
|
|
145
150
|
update Re-render workflows + refresh baseline specimens to the latest shipped
|
|
146
151
|
templates. Custom and skills.sh-installed specimens left alone.
|
|
147
152
|
configure-github <owner>/<repo>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
One-stop repo setup: repo conveniences (squash-only merges,
|
|
154
|
+
auto-merge, delete-branch-on-merge, PR title/body squash
|
|
155
|
+
message — ALL presets) + the SPEC §7 canonical branch
|
|
156
|
+
ruleset. \`--preset baseline|clud-bug|skdd|public-guard\`
|
|
157
|
+
picks the variant (default: skdd). Auth: GITHUB_TOKEN env
|
|
158
|
+
first, then \`gh auth token\`. Use --dry-run to print the
|
|
159
|
+
diff without writing; --branch to target a non-main branch.
|
|
160
|
+
Idempotent — already-canonical repos exit 0 with no changes.
|
|
153
161
|
edit-workflow Helper for editing .github/workflows/clud-bug-*.yml in an isolated
|
|
154
162
|
PR (the action refuses to review PRs that modify its own workflow).
|
|
155
163
|
usage Read recent clud-bug-review run JSON + normalize cost per LOC.
|
|
@@ -223,6 +231,10 @@ Commands:
|
|
|
223
231
|
clean|critical|failed --sha <sha> [--critical-count N]
|
|
224
232
|
[--source local|ci] [--strict|--no-strict] [--dry-run].
|
|
225
233
|
clean→success, critical+strict→failure, else neutral.
|
|
234
|
+
With CLUD_BUG_NOTARY_URL set + --bundle <file>, submits a
|
|
235
|
+
notary attestation bundle instead (Phase Z); the notary
|
|
236
|
+
issues the check. Falls back to the self-attested post if
|
|
237
|
+
the endpoint is unreachable.
|
|
226
238
|
|
|
227
239
|
Options:
|
|
228
240
|
--offline Skip skills.sh; pin only the bundled baseline specimens.
|
|
@@ -237,7 +249,7 @@ Options:
|
|
|
237
249
|
\`git commit\` / \`logmind log\`, it fetches a review recipe and
|
|
238
250
|
surfaces it to the agent (on this session's subscription)
|
|
239
251
|
via asyncRewake. Implies --with-local-review. Off by default.
|
|
240
|
-
--with-design (init) Install the design-critic kit (
|
|
252
|
+
--with-design (init) Install the design-critic kit (4 \`kind: design\`
|
|
241
253
|
skills) and enable the off-by-default visual review
|
|
242
254
|
lens — renders changed UI and critiques it. Off by default.
|
|
243
255
|
--local-only (init) MAX MODE: install the slash command + commit hook
|
|
@@ -253,9 +265,11 @@ Options:
|
|
|
253
265
|
required_conversation_resolution on the default
|
|
254
266
|
branch (init only). Use for repos that manage
|
|
255
267
|
branch protection via ruleset or org policy.
|
|
256
|
-
--dry-run Print the canonical-v1 diff without
|
|
268
|
+
--dry-run Print the canonical-v1 diff without writing
|
|
257
269
|
(configure-github only).
|
|
258
270
|
--branch <name> Target branch for configure-github (default: main).
|
|
271
|
+
--preset <name> configure-github ruleset preset: baseline | clud-bug |
|
|
272
|
+
skdd | public-guard (default: skdd).
|
|
259
273
|
--trigger <ctx> review-prompt context: commit (default) | push | pr.
|
|
260
274
|
--repo <owner/name> Restrict \`usage\` to a single repo. Default: all repos
|
|
261
275
|
with clud-bug-review.yml in the gh user's auth scope.
|
|
@@ -1380,7 +1394,7 @@ async function runInit(args) {
|
|
|
1380
1394
|
}
|
|
1381
1395
|
}
|
|
1382
1396
|
|
|
1383
|
-
// rc.16: --with-design installs the bundled design-critic kit (
|
|
1397
|
+
// rc.16: --with-design installs the bundled design-critic kit (4 `kind:
|
|
1384
1398
|
// design` skills) and enables the off-by-default design lens. writeSkills
|
|
1385
1399
|
// writes the SKILL.md files + merges the manifest entries; the `design` block
|
|
1386
1400
|
// is flipped on the manifest read below so the local recipe + hosted bot both
|
|
@@ -1498,6 +1512,13 @@ async function runInit(args) {
|
|
|
1498
1512
|
}
|
|
1499
1513
|
log(' • Opt out by setting "strictMode": false in .claude/skills/.clud-bug.json.');
|
|
1500
1514
|
|
|
1515
|
+
// Phase M — a quiet, on-voice support nudge for the free tiers (max mode +
|
|
1516
|
+
// self-hosted Action run on the user's own resources; no metered charge fits).
|
|
1517
|
+
// The GitHub "Sponsor" button + the Stripe "coffee" link both live in FUNDING.yml.
|
|
1518
|
+
log('');
|
|
1519
|
+
log('clud-bug forages on your own subscription — free, and meant to stay that way.');
|
|
1520
|
+
log('If the specimens earn their keep, you can feed the bug: github.com/sponsors/thrillmade');
|
|
1521
|
+
|
|
1501
1522
|
// v0.6.33 — opt-in unified install (mirror of logmind v0.6.8). When
|
|
1502
1523
|
// --with-skdd is passed, subprocess to `pip install logmind` + `logmind init`
|
|
1503
1524
|
// so Node-first users get the same one-command bootstrap as Python-first
|
|
@@ -1967,13 +1988,14 @@ async function runUpdateCmd(_args) {
|
|
|
1967
1988
|
// SPEC §7 canonical ruleset applier from src/cli/configure-github.ts;
|
|
1968
1989
|
// thin wrapper here just maps CLI args into the typed entry point. The
|
|
1969
1990
|
// command is idempotent — re-runs on a converged repo exit 0 with no
|
|
1970
|
-
//
|
|
1991
|
+
// create/update calls. See `src/core/configure-github.ts` for the diff + rule
|
|
1971
1992
|
// table.
|
|
1972
1993
|
async function runConfigureGithubCmd(args) {
|
|
1973
1994
|
const { runConfigureGithub } = await import('./configure-github.js');
|
|
1974
1995
|
const target = args._[1] ?? null;
|
|
1975
1996
|
const code = await runConfigureGithub({
|
|
1976
1997
|
target,
|
|
1998
|
+
preset: args.preset || undefined,
|
|
1977
1999
|
branch: args.branch || 'main',
|
|
1978
2000
|
dryRun: Boolean(args.dryRun),
|
|
1979
2001
|
quiet: QUIET,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// merge on those surfaces too (the hosted bot already posts it).
|
|
4
4
|
//
|
|
5
5
|
// Usage:
|
|
6
|
-
// clud-bug post-check-run --sha <sha> --verdict clean|critical|failed \
|
|
6
|
+
// clud-bug post-check-run --sha <sha> --verdict clean|critical|failed|unverified \
|
|
7
7
|
// [--critical-count N] [--source local|ci] [--strict|--no-strict] \
|
|
8
8
|
// [--owner O --repo R] [--details-url URL] [--dry-run]
|
|
9
9
|
//
|
|
@@ -14,8 +14,20 @@
|
|
|
14
14
|
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import { spawnSync } from 'node:child_process';
|
|
17
|
+
import { readFile } from 'node:fs/promises';
|
|
17
18
|
|
|
18
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
deriveCheck,
|
|
21
|
+
normalizeVerdict,
|
|
22
|
+
CLUD_BUG_CHECK_NAME,
|
|
23
|
+
parseBundle,
|
|
24
|
+
validateBundle,
|
|
25
|
+
validateConsistency,
|
|
26
|
+
splitUnifiedDiff,
|
|
27
|
+
notaryResponseIsRejection,
|
|
28
|
+
type NotaryBundle,
|
|
29
|
+
type DiffFile,
|
|
30
|
+
} from '../core/index.js';
|
|
19
31
|
import { readManifest } from './skills.js';
|
|
20
32
|
|
|
21
33
|
interface PostCheckRunArgs {
|
|
@@ -27,6 +39,8 @@ interface PostCheckRunArgs {
|
|
|
27
39
|
owner?: string;
|
|
28
40
|
repo?: string;
|
|
29
41
|
detailsUrl?: string;
|
|
42
|
+
/** Path to a notary attestation bundle (JSON). Activates the notary submit path. */
|
|
43
|
+
bundle?: string;
|
|
30
44
|
dryRun?: boolean;
|
|
31
45
|
cwd?: string;
|
|
32
46
|
_?: string[];
|
|
@@ -37,6 +51,115 @@ function sh(cmd: string, cmdArgs: string[], input?: string): { ok: boolean; out:
|
|
|
37
51
|
return { ok: r.status === 0, out: (r.stdout ?? '').trim(), err: (r.stderr ?? '').trim() };
|
|
38
52
|
}
|
|
39
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Best-effort load of the diff a bundle attests to, as `DiffFile[]`, for the
|
|
56
|
+
* LOCAL pre-check. Prefers the PR diff (matches GitHub's view); falls back to
|
|
57
|
+
* the commit diff. Returns `[]` when neither is obtainable — the caller then
|
|
58
|
+
* skips the diff-dependent checks (③④) and lets the SERVER do the authoritative
|
|
59
|
+
* validation against GitHub's ground truth (Z4).
|
|
60
|
+
*/
|
|
61
|
+
function loadDiffFiles(bundle: NotaryBundle): DiffFile[] {
|
|
62
|
+
let raw = '';
|
|
63
|
+
if (bundle.pr !== undefined) {
|
|
64
|
+
const r = sh('gh', ['pr', 'diff', String(bundle.pr), '--color', 'never']);
|
|
65
|
+
if (r.ok) raw = r.out;
|
|
66
|
+
}
|
|
67
|
+
if (!raw && bundle.head_sha) {
|
|
68
|
+
// -c core.quotepath=false → git emits non-ASCII paths as literal UTF-8
|
|
69
|
+
// (no octal quoting), so the splitter sees real filenames.
|
|
70
|
+
const r = sh('git', ['-c', 'core.quotepath=false', 'show', '--no-color', '--format=', bundle.head_sha]);
|
|
71
|
+
if (r.ok) raw = r.out;
|
|
72
|
+
}
|
|
73
|
+
return raw ? splitUnifiedDiff(raw) : [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type NotaryOutcome = 'posted' | 'rejected' | 'fallback';
|
|
77
|
+
|
|
78
|
+
interface NotaryResult {
|
|
79
|
+
outcome: NotaryOutcome;
|
|
80
|
+
/** The parsed + locally-validated bundle, when we got that far (for a bundle-derived fallback). */
|
|
81
|
+
bundle: NotaryBundle | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The notary submit path (Phase Z). Reads + parses the bundle, LOCALLY
|
|
86
|
+
* re-validates it (the handshake — a deterministic program refusing to certify
|
|
87
|
+
* an inconsistent/ungrounded review), then POSTs to the notary. The server (Z4)
|
|
88
|
+
* re-validates ①–⑤ against GitHub and — as SOLE issuer — posts the pinned check.
|
|
89
|
+
*
|
|
90
|
+
* Outcomes:
|
|
91
|
+
* 'posted' — the notary accepted; the SERVER owns the check, do not self-post.
|
|
92
|
+
* 'rejected' — the certification is definitively refused (malformed / inconsistent /
|
|
93
|
+
* ungrounded bundle, OR a server 4xx AUTHORITATIVELY declining). Post
|
|
94
|
+
* NO check — never a false green off a bad artifact or over a server "no".
|
|
95
|
+
* 'fallback' — the endpoint is DOWN (network error or 5xx), not a verdict; the caller
|
|
96
|
+
* may self-post the self-attested check (derived from THIS bundle) so local
|
|
97
|
+
* max mode keeps gating while Z4 is pending.
|
|
98
|
+
*/
|
|
99
|
+
async function submitToNotary(
|
|
100
|
+
notaryUrl: string,
|
|
101
|
+
bundlePath: string,
|
|
102
|
+
warn: (m: string) => void,
|
|
103
|
+
): Promise<NotaryResult> {
|
|
104
|
+
let bundle: NotaryBundle | null;
|
|
105
|
+
try {
|
|
106
|
+
bundle = parseBundle(JSON.parse(await readFile(bundlePath, 'utf8')));
|
|
107
|
+
} catch (e) {
|
|
108
|
+
warn(`could not read the bundle at ${bundlePath} (${e instanceof Error ? e.message : String(e)}); not certifying.`);
|
|
109
|
+
return { outcome: 'rejected', bundle: null };
|
|
110
|
+
}
|
|
111
|
+
if (!bundle) {
|
|
112
|
+
warn(`the bundle at ${bundlePath} is malformed; not certifying (fix the review artifact).`);
|
|
113
|
+
return { outcome: 'rejected', bundle: null };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Local pre-check: consistency is diff-free (always run); coverage + grounding
|
|
117
|
+
// need the diff (run only when one is obtainable — else defer to the server).
|
|
118
|
+
const diffFiles = loadDiffFiles(bundle);
|
|
119
|
+
const consistency = validateConsistency(bundle.verdict, bundle.findings);
|
|
120
|
+
if (!consistency.ok) {
|
|
121
|
+
warn(`bundle is internally inconsistent — ${consistency.reason}; not certifying.`);
|
|
122
|
+
return { outcome: 'rejected', bundle };
|
|
123
|
+
}
|
|
124
|
+
if (diffFiles.length > 0) {
|
|
125
|
+
const v = validateBundle(bundle, diffFiles);
|
|
126
|
+
if (!v.coverage.ok) {
|
|
127
|
+
warn(`bundle coverage is incomplete — unreviewed changed file(s): ${v.coverage.missingFiles.join(', ')}; not certifying.`);
|
|
128
|
+
return { outcome: 'rejected', bundle };
|
|
129
|
+
}
|
|
130
|
+
if (!v.grounding.ok) {
|
|
131
|
+
warn(`bundle has ungrounded critical finding(s): ${v.grounding.violations.map((x) => x.reason).join('; ')}; not certifying.`);
|
|
132
|
+
return { outcome: 'rejected', bundle };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Submit. TODO(Z4): the server mints/consumes the nonce, re-fetches GitHub's
|
|
137
|
+
// ground-truth diff, re-runs ①–⑤, signs, and posts the pinned check.
|
|
138
|
+
const url = notaryUrl.replace(/\/+$/, '') + '/notarize';
|
|
139
|
+
try {
|
|
140
|
+
const res = await fetch(url, {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: { 'content-type': 'application/json' },
|
|
143
|
+
body: JSON.stringify(bundle),
|
|
144
|
+
});
|
|
145
|
+
if (res.ok) {
|
|
146
|
+
process.stdout.write(`clud-bug: notarized ${bundle.repo}@${bundle.head_sha.slice(0, 12)} (verdict=${bundle.verdict}); the notary posts the check.\n`);
|
|
147
|
+
return { outcome: 'posted', bundle };
|
|
148
|
+
}
|
|
149
|
+
// A 4xx is the SOLE issuer authoritatively declining — terminal, no check.
|
|
150
|
+
// Only a 5xx (server down) is a fallback-able outage.
|
|
151
|
+
if (notaryResponseIsRejection(res.status)) {
|
|
152
|
+
warn(`notary declined the bundle (HTTP ${res.status}); not certifying.`);
|
|
153
|
+
return { outcome: 'rejected', bundle };
|
|
154
|
+
}
|
|
155
|
+
warn(`notary unavailable (HTTP ${res.status}); falling back to the self-attested check.`);
|
|
156
|
+
return { outcome: 'fallback', bundle };
|
|
157
|
+
} catch (e) {
|
|
158
|
+
warn(`notary endpoint unreachable (${e instanceof Error ? e.message : String(e)}); falling back to the self-attested check.`);
|
|
159
|
+
return { outcome: 'fallback', bundle };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
40
163
|
export async function runPostCheckRun(args: PostCheckRunArgs): Promise<void> {
|
|
41
164
|
const cwd = args.cwd ?? process.cwd();
|
|
42
165
|
const warn = (m: string) => process.stderr.write(`clud-bug post-check-run: ${m}\n`);
|
|
@@ -49,6 +172,21 @@ export async function runPostCheckRun(args: PostCheckRunArgs): Promise<void> {
|
|
|
49
172
|
sha = r.out;
|
|
50
173
|
}
|
|
51
174
|
|
|
175
|
+
// --- Notary submit path (Phase Z) — the un-forgeable route -------------
|
|
176
|
+
// When CLUD_BUG_NOTARY_URL is set and a --bundle is supplied, submit an
|
|
177
|
+
// attestation bundle: the CLI locally re-checks it (the handshake) and the
|
|
178
|
+
// notary (Z4) issues the pinned check. Only 'fallback' (endpoint unreachable)
|
|
179
|
+
// continues to the self-attested self-post below; 'posted'/'rejected' are terminal.
|
|
180
|
+
let fallbackBundle: NotaryBundle | null = null;
|
|
181
|
+
const notaryUrl = process.env.CLUD_BUG_NOTARY_URL?.trim();
|
|
182
|
+
if (notaryUrl && typeof args.bundle === 'string' && args.bundle && !args.dryRun) {
|
|
183
|
+
const { outcome, bundle } = await submitToNotary(notaryUrl, args.bundle, warn);
|
|
184
|
+
if (outcome !== 'fallback') return;
|
|
185
|
+
// Endpoint down → self-post below, derived from the ALREADY-VALIDATED bundle
|
|
186
|
+
// (not the raw --verdict flags, which a bundle-only invocation never passes).
|
|
187
|
+
fallbackBundle = bundle;
|
|
188
|
+
}
|
|
189
|
+
|
|
52
190
|
// --- strictMode: explicit flag wins, else the repo manifest -----------
|
|
53
191
|
let strictMode = false;
|
|
54
192
|
if (typeof args.strict === 'boolean') {
|
|
@@ -62,9 +200,15 @@ export async function runPostCheckRun(args: PostCheckRunArgs): Promise<void> {
|
|
|
62
200
|
}
|
|
63
201
|
}
|
|
64
202
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
203
|
+
// A bundle-fallback self-post reflects what was actually VALIDATED (bundle
|
|
204
|
+
// verdict + its critical count, local source); otherwise the raw flags.
|
|
205
|
+
const verdict = fallbackBundle
|
|
206
|
+
? fallbackBundle.verdict
|
|
207
|
+
: normalizeVerdict(typeof args.verdict === 'string' ? args.verdict : undefined);
|
|
208
|
+
const criticalCount = fallbackBundle
|
|
209
|
+
? fallbackBundle.findings.filter((f) => f.severity === 'critical').length
|
|
210
|
+
: Number(args.criticalCount ?? 0) || 0;
|
|
211
|
+
const source: 'local' | 'ci' = fallbackBundle ? 'local' : args.source === 'local' ? 'local' : 'ci';
|
|
68
212
|
const { conclusion, title, summary } = deriveCheck({ verdict, strictMode, criticalCount, source });
|
|
69
213
|
|
|
70
214
|
// --- resolve owner/repo (flags, else gh) ------------------------------
|