@cratis/pi 7.73.1 → 7.75.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.
- package/package/corpus/harnesses/pi/extensions/cratis-hooks/index.ts +5 -2
- package/package/corpus/hooks/README.md +27 -3
- package/package/corpus/hooks/scripts/cratis-quality-gate.sh +67 -4
- package/package/corpus/hooks/scripts/quality-gates.json +7 -1
- package/package/corpus/rules/general.md +1 -0
- package/package.json +1 -1
|
@@ -332,7 +332,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
332
332
|
label: "Cratis quality gate",
|
|
333
333
|
description:
|
|
334
334
|
"Run the Cratis quality gate (.cratis/ai/hooks/scripts/cratis-quality-gate.sh): the build, spec and lint gates the current " +
|
|
335
|
-
"working-tree changes touch
|
|
335
|
+
"working-tree changes touch. Shipped defaults: .cratis/ai/hooks/scripts/quality-gates.json; project overrides: " +
|
|
336
|
+
".cratis/ai/quality-gates.project.json, shallow-merged by gate id (command without requires drops inherited packageScripts). " +
|
|
337
|
+
"CRATIS_HOOKS_GATES replaces the defaults; project overrides still merge over it. " +
|
|
338
|
+
"Runs in the foreground with a whole-tool deadline and optional per-gate timeoutSeconds; can be cancelled. " +
|
|
336
339
|
"A failure, a timeout or a cancellation is returned as an error and means the change is NOT verified. " +
|
|
337
340
|
"Run it explicitly at a verification checkpoint, not after every prompt. It selects gates for all current " +
|
|
338
341
|
"working-tree changes, not the complete CI matrix; run CI-equivalent gates before claiming completion or pushing/opening a PR. " +
|
|
@@ -378,7 +381,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
378
381
|
const last = latestGateLog(gateLogDirectory(sessionId), started);
|
|
379
382
|
const tail = last ? tailLines(last.file, TAIL_LINES) : "";
|
|
380
383
|
throw new Error(`The Cratis quality gate TIMED OUT after ${duration(timeoutSeconds * 1000)}${last ? ` while running '${last.gate}'${last.cwd ? ` (cwd: ${last.cwd})` : ""}` : ""}. This is not a pass: nothing was verified.` +
|
|
381
|
-
(last ? `\n\nGate log: ${last.file}` : "") +
|
|
384
|
+
(last ? `\nRun a narrower command instead: ${last.gate === "backend-specs" ? "dotnet test <affected.Specs.csproj-or-Tests.csproj> --configuration Debug --no-build (replace the placeholder; repeat for affected dependents)." : "run the affected project alone using this gate's configured command."}\nConfigure it in .cratis/ai/quality-gates.project.json.\n\nGate log: ${last.file}` : "") +
|
|
382
385
|
(tail ? `\n\n--- last ${TAIL_LINES} lines ---\n${tail}\n--- end ---` : ""));
|
|
383
386
|
};
|
|
384
387
|
const reportInterrupted = () => {
|
|
@@ -277,6 +277,9 @@ their explicit consent. Slice-only scope checks do not replace the repository's
|
|
|
277
277
|
|
|
278
278
|
### Project-specific gate configuration
|
|
279
279
|
|
|
280
|
+
Shipped defaults live in `.cratis/ai/hooks/scripts/quality-gates.json`, not `.cratis/ai/quality-gates.json`.
|
|
281
|
+
Project overrides live in `.cratis/ai/quality-gates.project.json`. `CRATIS_HOOKS_GATES=<path>` replaces
|
|
282
|
+
the defaults file; the project overrides still merge over that replacement by gate id.
|
|
280
283
|
The shipped gates discover the repository's own solution and package, so most repositories need no
|
|
281
284
|
configuration at all. An eligible root workspace or solution keeps repository-wide dispatch. Without
|
|
282
285
|
one, discovery uses only each gate's matching, non-excluded changes and runs it once per affected
|
|
@@ -547,9 +550,30 @@ inapplicable gate remains a no-op. Pi preserves the script's diagnostics when pl
|
|
|
547
550
|
The same shipped file therefore activates in application and framework repositories, and reports
|
|
548
551
|
an empty plan without running gates in a repository that has no project at all.
|
|
549
552
|
|
|
550
|
-
**Overriding it
|
|
551
|
-
several candidate projects
|
|
552
|
-
|
|
553
|
+
**Overriding it.** Set `workingDirectory` on a gate in `.cratis/ai/quality-gates.project.json` to pin
|
|
554
|
+
one of several candidate projects. For a replacement defaults file, point `CRATIS_HOOKS_GATES` at a
|
|
555
|
+
file anywhere instead of editing the managed file. Neither requires forking the script.
|
|
556
|
+
A missing defaults file reports both real configuration paths and runs no gates.
|
|
557
|
+
|
|
558
|
+
**Per-gate deadlines.** `timeoutSeconds` is an optional integer from 1 to 86400, applied separately
|
|
559
|
+
to each command/directory. Unset means no per-gate deadline; Pi's whole-tool deadline still applies
|
|
560
|
+
and may expire first. The shipped `backend-specs` gate allows 120 seconds so a long solution-wide
|
|
561
|
+
spec run stops with a named diagnostic rather than consuming the entire tool budget. On timeout,
|
|
562
|
+
the script stops the command's process group (TERM, then KILL after one second), exits **2**, and
|
|
563
|
+
reports **NOT VERIFIED**, the gate, its directory, log tail, and `timeoutHint` (advice, never executed).
|
|
564
|
+
Commands that create separate sessions/process groups can escape this cleanup; this is not a sandbox.
|
|
565
|
+
|
|
566
|
+
For a large solution, replace the placeholder below with the affected test project and repeat for
|
|
567
|
+
its affected dependents. Override `command` with that narrower argv array (or a repository-owned
|
|
568
|
+
runner covering all affected projects), rather than raising the timeout without diagnosing the delay:
|
|
569
|
+
|
|
570
|
+
```json
|
|
571
|
+
{ "gates": [{ "id": "backend-specs", "command": ["dotnet", "test", "<affected.Specs.csproj>", "--configuration", "Debug", "--no-build"], "timeoutSeconds": 120, "timeoutHint": "Run dotnet test for each affected Specs/Tests project separately." }] }
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
This is not automatic affected-project selection: imported/conditional MSBuild references and
|
|
575
|
+
linked or shared sources require evaluation to establish coverage safely. The default retains
|
|
576
|
+
solution-wide tests rather than silently missing dependents, and a timeout is never a pass.
|
|
553
577
|
|
|
554
578
|
**Profile note.** The C# patterns are application-profile and scoped to `Source/**/*.cs`, which is
|
|
555
579
|
the application source root [`../rules/general.md`](../rules/general.md) documents. A
|
|
@@ -35,7 +35,10 @@ here="$(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
|
35
35
|
config="${CRATIS_HOOKS_GATES:-$here/quality-gates.json}"
|
|
36
36
|
dryrun="${CRATIS_HOOKS_GATE_DRYRUN:-0}"
|
|
37
37
|
|
|
38
|
-
[ -f "$config" ] ||
|
|
38
|
+
[ -f "$config" ] || {
|
|
39
|
+
printf 'cratis-quality-gate: configuration not found: %s — no gates ran. Shipped defaults: .cratis/ai/hooks/scripts/quality-gates.json; project overrides: .cratis/ai/quality-gates.project.json (merged by gate id). CRATIS_HOOKS_GATES replaces the defaults.\n' "$config" >&2
|
|
40
|
+
exit 0
|
|
41
|
+
}
|
|
39
42
|
jq -e . "$config" >/dev/null 2>&1 || {
|
|
40
43
|
printf 'cratis-quality-gate: %s is not valid JSON — gate skipped.\n' "$config" >&2
|
|
41
44
|
exit 0
|
|
@@ -265,7 +268,8 @@ discover_workdirs() {
|
|
|
265
268
|
|
|
266
269
|
gate_count="$(jq -r '.gates | length' "$config")"
|
|
267
270
|
[ "${gate_count:-0}" -gt 0 ] || exit 0
|
|
268
|
-
|
|
271
|
+
# jq's // treats false as absent; failFast=false must keep running after a failed gate.
|
|
272
|
+
fail_fast="$(jq -r '.failFast | if . == null then true else . end' "$config")"
|
|
269
273
|
max_lines="$(jq -r '.maxOutputLines // 60' "$config")"
|
|
270
274
|
|
|
271
275
|
tmp_root="$(hook_state_dir "$(hook_json "$input" '.session_id')")" || tmp_root="${TMPDIR:-/tmp}"
|
|
@@ -335,6 +339,46 @@ EOF
|
|
|
335
339
|
return 0
|
|
336
340
|
}
|
|
337
341
|
|
|
342
|
+
# Bash job control gives the command and watchdog separate process groups without GNU timeout
|
|
343
|
+
# or a language runtime. Keep the supervisor in the caller's group so Pi cancellation reaches
|
|
344
|
+
# its trap too. Wait for escalation even if the command leader exits on TERM: its test hosts
|
|
345
|
+
# may still be running. Only configured gates use this wrapper; other gates remain unchanged.
|
|
346
|
+
run_timed_gate() (
|
|
347
|
+
set -m
|
|
348
|
+
local worker timer rc=0 marker="$log.timeout"
|
|
349
|
+
rm -f "$marker"
|
|
350
|
+
stop_group() {
|
|
351
|
+
kill -TERM -- "-$1" 2>/dev/null || return 0
|
|
352
|
+
sleep 1
|
|
353
|
+
kill -KILL -- "-$1" 2>/dev/null || true
|
|
354
|
+
}
|
|
355
|
+
cleanup() {
|
|
356
|
+
trap '' TERM INT
|
|
357
|
+
kill -TERM -- "-$timer" 2>/dev/null || true
|
|
358
|
+
stop_group "$worker"
|
|
359
|
+
wait "$timer" 2>/dev/null || true
|
|
360
|
+
wait "$worker" 2>/dev/null || true
|
|
361
|
+
}
|
|
362
|
+
(cd -P "$root/$wd" && exec "${cmd[@]}") </dev/null &
|
|
363
|
+
worker=$!
|
|
364
|
+
(
|
|
365
|
+
sleep "$gate_timeout"
|
|
366
|
+
printf 'timed out\n' >"$marker"
|
|
367
|
+
stop_group "$worker"
|
|
368
|
+
) &
|
|
369
|
+
timer=$!
|
|
370
|
+
trap 'cleanup; exit 2' TERM INT
|
|
371
|
+
wait "$worker" || rc=$?
|
|
372
|
+
if [ -f "$marker" ]; then
|
|
373
|
+
wait "$timer" || true
|
|
374
|
+
rc=124
|
|
375
|
+
else
|
|
376
|
+
cleanup
|
|
377
|
+
fi
|
|
378
|
+
trap - TERM INT
|
|
379
|
+
exit "$rc"
|
|
380
|
+
)
|
|
381
|
+
|
|
338
382
|
idx=0
|
|
339
383
|
ran=0
|
|
340
384
|
planned_gates=0
|
|
@@ -383,6 +427,15 @@ while [ "$idx" -lt "$gate_count" ]; do
|
|
|
383
427
|
$(jq -r --argjson i "$idx" '.gates[$i].command // [] | .[]' "$config")
|
|
384
428
|
EOF
|
|
385
429
|
|
|
430
|
+
gate_timeout="$(jq -r --argjson i "$idx" 'if .gates[$i].timeoutSeconds == null then "" else .gates[$i].timeoutSeconds end' "$config")"
|
|
431
|
+
timeout_hint="$(jq -r --argjson i "$idx" '.gates[$i].timeoutHint // empty' "$config")"
|
|
432
|
+
if ! jq -e --argjson i "$idx" '
|
|
433
|
+
.gates[$i].timeoutSeconds | . == null or (type == "number" and . >= 1 and . <= 86400 and floor == .)
|
|
434
|
+
' "$config" >/dev/null; then
|
|
435
|
+
printf 'cratis-quality-gate: UNVERIFIED %s — timeoutSeconds must be an integer from 1 to 86400.\n' "$id" >&2
|
|
436
|
+
exit 2
|
|
437
|
+
fi
|
|
438
|
+
|
|
386
439
|
project_index=0
|
|
387
440
|
gate_ran=0
|
|
388
441
|
gate_noops=0
|
|
@@ -424,13 +477,23 @@ EOF
|
|
|
424
477
|
rc=0
|
|
425
478
|
{
|
|
426
479
|
printf 'cratis-quality-gate: LOG %s (cwd: %s)\n' "$id" "$wd"
|
|
427
|
-
|
|
480
|
+
if [ -n "$gate_timeout" ]; then
|
|
481
|
+
run_timed_gate
|
|
482
|
+
else
|
|
483
|
+
(cd -P "$root/$wd" && "${cmd[@]}") </dev/null
|
|
484
|
+
fi
|
|
428
485
|
} >"$log" 2>&1 || rc=$?
|
|
429
486
|
ran=$((ran + 1))
|
|
430
487
|
|
|
431
488
|
if [ "$rc" -ne 0 ]; then
|
|
432
489
|
{
|
|
433
|
-
|
|
490
|
+
if [ -n "$gate_timeout" ] && [ -f "$log.timeout" ]; then
|
|
491
|
+
printf 'QUALITY GATE TIMED OUT: %s after %ss (cwd: %s) — NOT VERIFIED.\n' "$id" "$gate_timeout" "$wd"
|
|
492
|
+
printf 'Run a narrower command instead: %s\n' "${timeout_hint:-run the affected project with the command below, not the whole workspace/solution.}"
|
|
493
|
+
printf 'Configure command/workingDirectory/timeoutSeconds in .cratis/ai/quality-gates.project.json.\n'
|
|
494
|
+
else
|
|
495
|
+
printf 'QUALITY GATE FAILED: %s (exit %s)\n' "$id" "$rc"
|
|
496
|
+
fi
|
|
434
497
|
printf ' %s\n' "$desc"
|
|
435
498
|
printf ' $ %s (cwd: %s)\n\n' "${cmd[*]}" "$wd"
|
|
436
499
|
printf -- '--- last %s lines ---\n' "$max_lines"
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": [
|
|
3
3
|
"Quality gates run by the Stop hook (cratis-quality-gate.sh).",
|
|
4
|
-
"
|
|
4
|
+
"Shipped defaults: .cratis/ai/hooks/scripts/quality-gates.json; project overrides: .cratis/ai/quality-gates.project.json.",
|
|
5
|
+
"Data, not code: a consuming repository patches gates by id (or points CRATIS_HOOKS_GATES at",
|
|
5
6
|
"its own) without touching the script. Gates run in order and stop at the first failure.",
|
|
6
7
|
"",
|
|
7
8
|
"A gate runs only when the working tree contains a change matching one of its \"changed\" globs,",
|
|
@@ -41,6 +42,9 @@
|
|
|
41
42
|
" Mixed RUN/NO-OP directories report UNVERIFIED and exit 2, not a partial success.",
|
|
42
43
|
" Gate commands read /dev/null so they cannot consume remaining directories.",
|
|
43
44
|
" command argv array \u2014 executed directly, never through a shell (no eval)",
|
|
45
|
+
" timeoutSeconds optional integer 1..86400; deadline per command/directory, plus 1s TERM-to-KILL cleanup",
|
|
46
|
+
" (Pi's whole-tool deadline still applies); timeout exits 2, never a pass",
|
|
47
|
+
" timeoutHint narrower command to run when the gate times out; diagnostic text, never executed",
|
|
44
48
|
"",
|
|
45
49
|
"Managed corpus and adapter trees (.cratis/**, .pi/**, .claude/**) never count as a relevant change:",
|
|
46
50
|
"a corpus update must not run a repository's application gates over files that are not its application.",
|
|
@@ -114,6 +118,8 @@
|
|
|
114
118
|
{
|
|
115
119
|
"id": "backend-specs",
|
|
116
120
|
"description": "dotnet test (Debug, --no-build) \u2014 zero failures",
|
|
121
|
+
"timeoutSeconds": 120,
|
|
122
|
+
"timeoutHint": "dotnet test <affected.Specs.csproj-or-Tests.csproj> --configuration Debug --no-build (replace the placeholder with the affected test project; repeat for its affected dependents)",
|
|
117
123
|
"changed": [
|
|
118
124
|
"**/*.cs",
|
|
119
125
|
"**/*.csproj",
|
|
@@ -95,6 +95,7 @@ A claim is only as good as the signal behind it — a build result, a test run,
|
|
|
95
95
|
|
|
96
96
|
- **Confirm "done"/"fixed"/"correct" against a fresh signal — never self-assessment.** Run the relevant gate and observe it pass *this time*.
|
|
97
97
|
- **Check at the right frequency.** Run affected-project checks while iterating; a prompt ending is not task completion. Before claiming a task complete or pushing/opening a PR, run the full CI-equivalent gates required for that repository (including its Tier 1/2 policy where applicable). A change-selected hook gate is not a substitute for the repository's full CI matrix. After a fix, re-run the gate that failed. Don't argue yourself to green.
|
|
98
|
+
- **Gate configuration:** shipped defaults `.cratis/ai/hooks/scripts/quality-gates.json`; project overrides `.cratis/ai/quality-gates.project.json` shallow-merge by gate id (command without requires drops inherited packageScripts). `CRATIS_HOOKS_GATES` replaces defaults; project overrides still apply.
|
|
98
99
|
- **A green build is not behavioral correctness.** Compilation proves it builds, not that the slice does the right thing — that's what specs and exercising the UI are for.
|
|
99
100
|
- **Report the conclusion and what you didn't verify, in a line or two.** Show the output when asked, when a claim is contested, or when the check failed — see [verification-discipline.md](./verification-discipline.md).
|
|
100
101
|
|