mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 08:23:59 +01:00
v1.94.25.0 gstack-evidence: check a lane under the label it was recorded with (TRUT-7) (#3935)
* gstack-evidence: one ledger label spelling for run, record-ci and check (TRUT-7, truther#1170) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * gstack-evidence carried: list a CI row only when check's grading calls it FRESH (TRUT-7 stray ci_carried entry) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: evidence label spelling and carried's FRESH filter (l6ldocs1) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
1ccab0d8bc
commit
b2ba100781
8 files changed
+120
-65
No files matched your search
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## [1.94.25.0] - 2026-11-03
|
||||
|
||||
- A lane label with a space or a colon, such as `pnpm lint` or `db:test`, no longer goes MISSING right after it carries (TRUT-7, truther#1170). `gstack-evidence` writes a label to the ledger with unsafe characters turned into `_`, but `check`, which `gstack-ship-release finish` and `accept` call, looked it up under the raw label, so /ship stopped with "not FRESH at the pushed head". `run`, `record-ci` and `check` now share one label spelling.
|
||||
- A carry that doesn't reach FRESH no longer leaves a `ci_carried` entry in the ship receipt (TRUT-7: truther's receipt kept `pnpm_lint:41603968341/1` from the attempt that stopped). `gstack-evidence carried` now lists a CI row only when `check`'s own grading, at the 24-hour age /ship uses, calls it FRESH.
|
||||
|
||||
### Tests
|
||||
- `test/ship-ci-carry.test.ts`: `accept` with the lanes `pnpm lint` and `db:test` carries both and checks them FRESH, and `gstack-evidence check` finds both. It failed before the fix (`EVIDENCE: MISSING label=pnpm lint`).
|
||||
- `test/ship-ci-carry.test.ts`: a carried `pnpm lint` row aged past 24 hours grades STALE and leaves `carried` empty. It failed before the fix (the stale row was still listed).
|
||||
|
||||
## [1.94.24.0] - 2026-11-03
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.94.24.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.94.25.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
+73
-60
@@ -95,7 +95,7 @@ import { isUnavailable, loadPolicyFromBase, resolveBaseBranch } from "../lib/shi
|
||||
import { versionAtRef } from "../lib/restamp";
|
||||
import { bashCommandInvocation, runScriptSync, wtreeFingerprint } from "../lib/script-invocation";
|
||||
import { findCarry, laneMapping, mappingCoverage, perFileCommand } from "../lib/ci-carry";
|
||||
import { runChanged } from "../lib/run-evidence";
|
||||
import { DEFAULT_MAX_AGE_HOURS, runChanged } from "../lib/run-evidence";
|
||||
import { isConfigUnavailable, loadConfigFromBase } from "../lib/pregate/config";
|
||||
import { buildRunCheck } from "../lib/pregate/run-check";
|
||||
|
||||
@@ -163,6 +163,9 @@ class OutputTail {
|
||||
}
|
||||
}
|
||||
|
||||
/** The one spelling a lane label has in the ledger: run and record-ci write it, check reads it (`pnpm lint` is `pnpm_lint`, TRUT-7). */
|
||||
const ledgerLabel = (raw: string): string => raw.replace(/[^a-zA-Z0-9._-]/g, "_");
|
||||
|
||||
const ANSI = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
||||
const strip = (l: string) => l.replace(ANSI, "").replace(/\r$/, "");
|
||||
const count = (text: string, word: string): number => Number(new RegExp(`(\\d+) ${word}\\b`).exec(text)?.[1] ?? 0);
|
||||
@@ -428,7 +431,7 @@ async function cmdRun(argv: string[]): Promise<number> {
|
||||
const commandString = cmdArgv.length === 1 ? cmdArgv[0] : cmdArgv.join(" ");
|
||||
const shell = cmdArgv.length === 1 ? bashCommandInvocation(cmdArgv[0]) : null;
|
||||
const cmdSha = sha256(commandString);
|
||||
label = label.replace(/[^a-zA-Z0-9._-]/g, "_");
|
||||
label = ledgerLabel(label);
|
||||
|
||||
// Bookkeeping context — every piece is optional; failures only warn.
|
||||
let paths: ReturnType<typeof ledgerPath> | undefined;
|
||||
@@ -675,7 +678,8 @@ async function cmdRecordCi(argv: string[]): Promise<number> {
|
||||
const sep = argv.indexOf("--");
|
||||
const opts = sep < 0 ? argv : argv.slice(0, sep);
|
||||
const get = (name: string) => { const i = opts.indexOf(name); return i >= 0 ? opts[i + 1] : undefined; };
|
||||
const label = get("--label")?.replace(/[^a-zA-Z0-9._-]/g, "_");
|
||||
const raw = get("--label");
|
||||
const label = raw === undefined ? undefined : ledgerLabel(raw);
|
||||
const base = get("--base");
|
||||
const command = sep < 0 ? "" : argv.slice(sep + 1).join(" ");
|
||||
if (!label || !base || !command || opts.length !== 4) {
|
||||
@@ -754,13 +758,76 @@ function cmdCarried(argv: string[]): number {
|
||||
const wtree = currentWtree();
|
||||
const latest = new Map<string, EvidenceRecord>();
|
||||
for (const r of records) latest.set(r.label, r);
|
||||
const carried = [...latest.values()].filter((r) => r.source === "ci" && !!wtree && r.wtree === wtree && Array.isArray(r.runs) && typeof r.policy === "string" && r.policy.startsWith(policyRef))
|
||||
const carried = [...latest.values()].filter((r) => r.source === "ci" && !!wtree && r.wtree === wtree && Array.isArray(r.runs) && typeof r.policy === "string" && r.policy.startsWith(policyRef)
|
||||
&& gradeRecord(r, { maxAgeHours: DEFAULT_MAX_AGE_HOURS, allowPaths: [], wtreeNow: wtree }).verdict === "FRESH")
|
||||
.flatMap((r) => r.runs!.map((ref) => { const [run, attempt] = ref.split("/").map(Number); return { lane: r.label, run, attempt, tree: r.wtree! }; }));
|
||||
if (argv.includes("--json")) console.log(JSON.stringify(carried));
|
||||
else for (const c of carried) console.log(`CI_CARRIED: ${c.lane} run=${c.run}/${c.attempt} tree=${c.tree.slice(0, 12)}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** check's verdict for one ledger row; `carried` lists a CI row only when it grades FRESH here, so a carry that never reached FRESH leaves no receipt entry. */
|
||||
function gradeRecord(latest: EvidenceRecord, o: { maxAgeHours?: number; expectCmd?: string; allowPaths: string[]; wtreeNow: string | undefined }): { verdict: "FRESH" | "STALE" | "ZERO-RUN"; reason: string } {
|
||||
const { maxAgeHours, expectCmd, allowPaths, wtreeNow } = o;
|
||||
const testsRan = typeof latest.tests_ran === "number" || latest.tests_ran === "n/a" ? latest.tests_ran : "unknown";
|
||||
const changedDuringRun = Array.isArray(latest.wtree_changed) ? latest.wtree_changed.filter((p): p is string => typeof p === "string" && p !== "" && !/[\u0000-\u001f\u007f]/.test(p)).slice(0, 5) : [];
|
||||
let verdict: "FRESH" | "STALE" | "ZERO-RUN" = "FRESH";
|
||||
let reason = "";
|
||||
if (latest.exit !== 0) {
|
||||
verdict = "STALE";
|
||||
reason = "recorded run failed";
|
||||
} else if (testsRan === 0) {
|
||||
// Zero-run green: exit 0 with nothing executed (all skipped, empty
|
||||
// selection, "No tests found"). Graded like STALE under its own label;
|
||||
// "unknown" (no recognised runner summary) stays non-blocking.
|
||||
verdict = "ZERO-RUN";
|
||||
reason = "runner reported 0 executed tests";
|
||||
} else if (maxAgeHours !== undefined) {
|
||||
const ageMs = Date.now() - Date.parse(latest.ts);
|
||||
if (!(ageMs >= 0 && ageMs <= maxAgeHours * 3600 * 1000)) {
|
||||
verdict = "STALE";
|
||||
reason = `older than ${maxAgeHours}h`;
|
||||
}
|
||||
}
|
||||
if (verdict === "FRESH" && expectCmd !== undefined && sha256(expectCmd) !== latest.cmd_sha256) {
|
||||
verdict = "STALE";
|
||||
reason = "command changed (cmd_sha256 mismatch)";
|
||||
}
|
||||
if (verdict === "FRESH") {
|
||||
// Content binding: identical working-tree fingerprint, or a diff confined
|
||||
// to the allow-list. Any git failure (gc'd tree, not a repo) → STALE —
|
||||
// never an error into the calling flow.
|
||||
if (!latest.wtree || !/^[0-9a-f]{40}$/.test(latest.wtree) || !wtreeNow) {
|
||||
// Stored fingerprints are re-validated before reaching git argv — a
|
||||
// forged/corrupt ledger line must degrade, never inject options.
|
||||
verdict = "STALE";
|
||||
reason = !latest.wtree
|
||||
? changedDuringRun.length
|
||||
? `content changed during the run: ${changedDuringRun.join(", ")}`
|
||||
: "record has no content fingerprint"
|
||||
: !/^[0-9a-f]{40}$/.test(latest.wtree)
|
||||
? "record has malformed fingerprint"
|
||||
: "current fingerprint unavailable";
|
||||
} else if (latest.wtree !== wtreeNow) {
|
||||
const diff = git(["diff", "--name-only", latest.wtree, wtreeNow]);
|
||||
if (diff === undefined) {
|
||||
verdict = "STALE";
|
||||
reason = "content changed (fingerprint diff unavailable)";
|
||||
} else {
|
||||
const changed = diff.split("\n").map((s) => s.trim()).filter(Boolean);
|
||||
const outside = changed.filter((f) => !allowPaths.some((a) => f === a || f.startsWith(a.replace(/\/$/, "") + "/")));
|
||||
if (changed.length === 0 || outside.length === 0) {
|
||||
reason = changed.length ? `diff confined to allow-paths (${changed.length} file(s))` : "";
|
||||
} else {
|
||||
verdict = "STALE";
|
||||
reason = `content changed: ${outside.slice(0, 5).join(", ")}${outside.length > 5 ? ", ..." : ""}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { verdict, reason };
|
||||
}
|
||||
|
||||
function cmdCheck(argv: string[]): number {
|
||||
// Parse: repeated --label, each optionally followed (anywhere later) by its
|
||||
// own --expect-cmd; pairing is positional — an --expect-cmd binds to the most
|
||||
@@ -771,7 +838,7 @@ function cmdCheck(argv: string[]): number {
|
||||
let allowPaths: string[] = [];
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === "--label") wanted.push({ label: argv[++i] ?? "" });
|
||||
if (a === "--label") wanted.push({ label: ledgerLabel(argv[++i] ?? "") });
|
||||
else if (a === "--expect-cmd") {
|
||||
if (wanted.length === 0) {
|
||||
console.error("gstack-evidence: --expect-cmd requires a preceding --label");
|
||||
@@ -820,63 +887,9 @@ function cmdCheck(argv: string[]): number {
|
||||
continue;
|
||||
}
|
||||
const testsRan = typeof latest.tests_ran === "number" || latest.tests_ran === "n/a" ? latest.tests_ran : "unknown";
|
||||
const changedDuringRun = Array.isArray(latest.wtree_changed) ? latest.wtree_changed.filter((p): p is string => typeof p === "string" && p !== "" && !/[\u0000-\u001f\u007f]/.test(p)).slice(0, 5) : [];
|
||||
const ci = latest.source === "ci" && Array.isArray(latest.runs) ? ` source=ci runs=${latest.runs.filter((s) => /^\d+\/\d+$/.test(s)).join(",")}` : "";
|
||||
const detail = `label=${label} exit=${latest.exit} tests_ran=${testsRan}${latest.tests_unit === "files" ? " unit=files" : ""} ts=${latest.ts}${latest.log_path ? ` log=${latest.log_path}` : ""}${ci}`;
|
||||
let verdict: "FRESH" | "STALE" | "ZERO-RUN" = "FRESH";
|
||||
let reason = "";
|
||||
if (latest.exit !== 0) {
|
||||
verdict = "STALE";
|
||||
reason = "recorded run failed";
|
||||
} else if (testsRan === 0) {
|
||||
// Zero-run green: exit 0 with nothing executed (all skipped, empty
|
||||
// selection, "No tests found"). Graded like STALE under its own label;
|
||||
// "unknown" (no recognised runner summary) stays non-blocking.
|
||||
verdict = "ZERO-RUN";
|
||||
reason = "runner reported 0 executed tests";
|
||||
} else if (maxAgeHours !== undefined) {
|
||||
const ageMs = Date.now() - Date.parse(latest.ts);
|
||||
if (!(ageMs >= 0 && ageMs <= maxAgeHours * 3600 * 1000)) {
|
||||
verdict = "STALE";
|
||||
reason = `older than ${maxAgeHours}h`;
|
||||
}
|
||||
}
|
||||
if (verdict === "FRESH" && expectCmd !== undefined && sha256(expectCmd) !== latest.cmd_sha256) {
|
||||
verdict = "STALE";
|
||||
reason = "command changed (cmd_sha256 mismatch)";
|
||||
}
|
||||
if (verdict === "FRESH") {
|
||||
// Content binding: identical working-tree fingerprint, or a diff confined
|
||||
// to the allow-list. Any git failure (gc'd tree, not a repo) → STALE —
|
||||
// never an error into the calling flow.
|
||||
if (!latest.wtree || !/^[0-9a-f]{40}$/.test(latest.wtree) || !wtreeNow) {
|
||||
// Stored fingerprints are re-validated before reaching git argv — a
|
||||
// forged/corrupt ledger line must degrade, never inject options.
|
||||
verdict = "STALE";
|
||||
reason = !latest.wtree
|
||||
? changedDuringRun.length
|
||||
? `content changed during the run: ${changedDuringRun.join(", ")}`
|
||||
: "record has no content fingerprint"
|
||||
: !/^[0-9a-f]{40}$/.test(latest.wtree)
|
||||
? "record has malformed fingerprint"
|
||||
: "current fingerprint unavailable";
|
||||
} else if (latest.wtree !== wtreeNow) {
|
||||
const diff = git(["diff", "--name-only", latest.wtree, wtreeNow]);
|
||||
if (diff === undefined) {
|
||||
verdict = "STALE";
|
||||
reason = "content changed (fingerprint diff unavailable)";
|
||||
} else {
|
||||
const changed = diff.split("\n").map((s) => s.trim()).filter(Boolean);
|
||||
const outside = changed.filter((f) => !allowPaths.some((a) => f === a || f.startsWith(a.replace(/\/$/, "") + "/")));
|
||||
if (changed.length === 0 || outside.length === 0) {
|
||||
reason = changed.length ? `diff confined to allow-paths (${changed.length} file(s))` : "";
|
||||
} else {
|
||||
verdict = "STALE";
|
||||
reason = `content changed: ${outside.slice(0, 5).join(", ")}${outside.length > 5 ? ", ..." : ""}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const { verdict, reason } = gradeRecord(latest, { maxAgeHours, expectCmd, allowPaths, wtreeNow });
|
||||
console.log(`EVIDENCE: ${verdict} ${detail}${reason ? ` reason=${reason}` : ""}`);
|
||||
if (verdict !== "FRESH") allFresh = false;
|
||||
}
|
||||
|
||||
+8
-1
@@ -1123,6 +1123,9 @@ As a second guard, every `record-ci` row names the policy that mapped it
|
||||
(`origin/<base>@<sha>:.gstack/pregate.json`), and `gstack-evidence carried`, which
|
||||
the ship receipt reads, lists only rows whose policy is `origin/<the receipt's
|
||||
base>`. A row written by a run against another base never lands in a receipt.
|
||||
`carried` also lists a row only when `check`'s own grading, at the 24-hour age
|
||||
limit /ship uses, calls it FRESH, so a carry that never reached FRESH (a failed
|
||||
or zero-run row, or one older than 24 hours) leaves no `ci_carried` entry.
|
||||
|
||||
A carried lane prints `LANE: <label> carried run=<id>/<attempt> tests_ran=<n>`
|
||||
(see above for what `<n>` counts) and writes an explicit CI-sourced ledger row (`gstack-evidence record-ci
|
||||
@@ -1131,7 +1134,11 @@ itself, so no caller hands it run ids or a test count: `source: "ci"`, its runs
|
||||
and profiles, the run's finish time as `ts`, bound to the clean tree), never a
|
||||
fictitious local run. `gstack-evidence check` grades it
|
||||
like any row and names `source=ci runs=…`, so `finish`'s FRESH check and the
|
||||
24-hour age limit apply to the CI run's own time. A mapped lane that cannot
|
||||
24-hour age limit apply to the CI run's own time. The ledger stores a label
|
||||
with every character outside `A-Za-z0-9._-` turned into `_` (`pnpm lint` is
|
||||
`pnpm_lint`, `db:test` is `db_test`), and `run`, `record-ci` and `check` all
|
||||
use that one spelling, so `check --label 'pnpm lint'` finds the row; `ci_carried`
|
||||
and `CI_CARRIED:` name the stored spelling. A mapped lane that cannot
|
||||
carry prints `CI_CARRY: none label=<label> (<reason>)` and runs here. Every
|
||||
other lane runs through `gstack-evidence run --reuse`: the newest row for its
|
||||
label, command and working tree is reused when it passed with a counted run
|
||||
|
||||
@@ -18,7 +18,7 @@ Beyond the slash-command skills, gstack ships standalone CLIs for workflows that
|
||||
| `gstack-wtree` | **Working-tree fingerprint** — prints a content hash of what's actually on disk (temp index seeded from the stat cache, ~40x cheaper than a full re-hash; untracked source counts, gitignored scratch doesn't). Identical content fingerprints identically through commits, rebases, amends, and squashes — it's what binds reviews and test evidence to content instead of commit SHAs. |
|
||||
| `gstack-review-log` | **Review-pass receipts** — `--start <skill>` captures the working-tree fingerprint before a diff review; `'<JSON>' --finish <token>` consumes that single-use, repository/branch/skill-scoped receipt. Binding requires matching start/end content and reviewer-reported `completed:true` and `converged:true`; it is not independent proof that a model read the source. `--check-shared-libs <token>` reads a current finding as JSON on stdin and checks prior Skip decisions against the actual branch, capture and eligible raw source blobs. It returns `reusable:false` when proof is missing or unsafe, including older records without logger-versioned coverage. Final review logging computes shared-code fingerprints and coverage rather than trusting supplied proof. |
|
||||
| `gstack-review-read` | **Review freshness** — emits review records with computed `review_freshness.status` and `reason`: CURRENT, STALE, or UNVERIFIED for diff reviews. `/ship` and `/land-and-deploy` use the same grade; a matching commit alone never certifies a diff review. [Dashboard rules](skills.md#review-readiness-dashboard). |
|
||||
| `gstack-evidence` | **Verification-evidence ledger** — `run --label <lane> -- <cmd>` transparently wraps any test command (the child's exit code always passes through) and records what ran against which working-tree fingerprint (tracked and untracked files that are not gitignored, minus an untracked `.bun/` folder at the repo root, which Bun can create during a run); a run whose files changed while it ran is recorded without a fingerprint and with up to five of the changed paths, which `check` names in its STALE reason. `check` grades each label FRESH/STALE/MISSING with `--expect-cmd`, `--max-age`, and `--allow-paths` binding. /ship and /land-and-deploy cite fresh evidence instead of re-running suites. Per-run logs are 0600, capped at 2MB, pruned after 30 days; the ledger and logs stay machine-local by design. |
|
||||
| `gstack-evidence` | **Verification-evidence ledger** — `run --label <lane> -- <cmd>` transparently wraps any test command (the child's exit code always passes through) and records what ran against which working-tree fingerprint (tracked and untracked files that are not gitignored, minus an untracked `.bun/` folder at the repo root, which Bun can create during a run); a run whose files changed while it ran is recorded without a fingerprint and with up to five of the changed paths, which `check` names in its STALE reason. `check` grades each label FRESH/STALE/MISSING with `--expect-cmd`, `--max-age`, and `--allow-paths` binding. A label is stored and looked up with every character outside `A-Za-z0-9._-` turned into `_`, so `pnpm lint` and `pnpm_lint` name the same row. `carried` lists the CI-sourced rows at the current tree that grade FRESH, which the ship receipt records as `ci_carried`. /ship and /land-and-deploy cite fresh evidence instead of re-running suites. Per-run logs are 0600, capped at 2MB, pruned after 30 days; the ledger and logs stay machine-local by design. |
|
||||
| `gstack-flaky` | **Flaky-test rerun for any JUnit runner** — `run --junit <file.xml> -- <command>` runs your test command (Playwright, `bun test`, jest, vitest, pytest) and, when at most `--max-reruns` (3) tests failed and the runner made no retries of its own, reruns the whole command once (`--rerun-scope test` opts in to rerunning only the failed tests). A test that fails and then passes on that rerun is a flaky pass: the run stays green and one redacted line per flaky pass or failed rerun goes on a ledger (`GSTACK_FLAKE_LEDGER_JUNIT`, default `<state root>/flake-ledger-junit.jsonl`). A missing or malformed report, a crash or a killed runner never passes. `quarantine add|renew|release|check|report` keeps `.gstack/quarantine.json`: a quarantined test still runs, gets up to 3 attempts of the whole command, passes only on a full green attempt and is never counted as a pass (`run --base <ref>` reads the registry from the base branch). `ids --junit <file>` prints a report's test ids. Exit 0 pass, 1 a test failed, 2 usage, 3 no usable report. [Guide](flaky.md). |
|
||||
| `gstack-pr-wait` | **PR event watcher** — `gstack-pr-wait <pr> [--repo owner/name] [--until green\|ready\|any-change] [--interval 20] [--timeout 60] [--exclude CHECK,...]` polls one pull request through `gh` and prints one stdout line per real event (a check failed with its link, all checks finished, a comment or review from someone other than the author, a review bot finished on the head, the base moved and its new commits touch this PR's files, BEHIND or DIRTY, merged or closed), with progress on stderr, so a monitor wakes the agent once per event instead of on a timer. `green` gates on every check on the head (an advisory bot's review is never waited for, its checks gate like any other), as `gstack-ci-gate` does; a check the base branch requires (rulesets and classic branch protection) is pending until it appears, never green by omission; two identical polls confirm a check set is complete; `--exclude` leaves a named non-required check out of the verdict as `gstack-ci-gate --exclude` does (a required name is refused and gates). Exit 0 when the condition is met, 1 when it needs action or `--timeout` passes (`PR_WAIT_BLOCKED`), 2 on bad input, a second watcher for the same PR or a local error (`PR_WAIT_REFUSED`). State under `<state root>/pr-wait/`, baseline discarded after 6 hours. Review bots are configuration: `gstack-config set pr_wait_gating_bots greptile,macroscope`, `pr_wait_advisory_bots coderabbit` (advisory bots are reported, never waited on). `/land-and-deploy` Step 3 is one call of it. Design ported from tstack's pr-wait (MIT, Theo Browne; NOTICE.md). [Exit codes](troubleshooting.md#gstack-pr-wait). |
|
||||
| `gstack-review-bots` | **Review-bot comments** — `fetch [--pr <n>]` reads every root line comment, conversation comment and newest review body from each bot in `pr_wait_gating_bots` and `pr_wait_advisory_bots` (Bot accounts only, all pages, through `gh`) into a private directory with per-bot counts, or prints `REVIEW_BOTS: skip` or `REVIEW_BOTS: error` (never a false zero). `record --dir <dir> --id <comment id> --type fix\|fp\|already-fixed --category <category>` appends one triage outcome to the project and global `review-bot-history.md`, taking the bot and file from the fetched comment. `/review` Step 2.5 and `/ship` Step 10 run it. [Triage reference](../review/greptile-triage.md). |
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.94.24",
|
||||
"version": "1.94.25",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -122,6 +122,32 @@ describe('finish, push --early and accept (A1)', () => {
|
||||
// Value: protects=finish never runs a mapped lane locally on the stamped tree: it defers it to accept and runs only pregate's preflight stage; accept carries
|
||||
// it from the green run at the pushed head and reruns the pregate there; fails_when=the stamped tree reruns the suite, or accept trusts a run at another tree;
|
||||
// why_new=design fix A1; seam=fake gh, gstack-evidence, gstack-pregate
|
||||
test('accept checks a carried lane under the label it was recorded with: spaces and colons (TRUT-7)', () => {
|
||||
const f = fx();
|
||||
world(f);
|
||||
expect(f.run(RELEASE, ['push', '--branch', 'feat', '--early', '--base', 'main'], unattended).status).toBe(0);
|
||||
for (const label of ['pnpm lint', 'db:test']) {
|
||||
const ok = f.run(RELEASE, ['accept', '--base', 'main', '--lane', `${label}=${FREE}`, '--wait', '0']);
|
||||
expect([ok.status, ok.stdout], label).toEqual([0, expect.not.stringContaining('not FRESH')]);
|
||||
expect(ok.stdout).toMatch(/^EVIDENCE: FRESH /m);
|
||||
}
|
||||
for (const label of ['pnpm lint', 'db:test']) expect(f.run(path.join(BIN, 'gstack-evidence'), ['check', '--label', label]).stdout).toMatch(/^EVIDENCE: FRESH /m);
|
||||
});
|
||||
|
||||
test('a carried row that no longer grades FRESH leaves no ci_carried entry (TRUT-7: pnpm lint)', () => {
|
||||
const f = fx();
|
||||
world(f);
|
||||
expect(f.run(RELEASE, ['push', '--branch', 'feat', '--early', '--base', 'main'], unattended).status).toBe(0);
|
||||
expect(f.run(RELEASE, ['accept', '--base', 'main', '--lane', `pnpm lint=${FREE}`, '--wait', '0']).status).toBe(0);
|
||||
const carried = () => JSON.parse(f.run(path.join(BIN, 'gstack-evidence'), ['carried', '--base', 'main', '--json']).stdout);
|
||||
expect(carried()).toEqual([expect.objectContaining({ lane: 'pnpm_lint', run: 7, attempt: 1 })]);
|
||||
const [ledger] = findFilesBySuffix(path.join(f.state, 'projects'), '-evidence.jsonl');
|
||||
const rows = fs.readFileSync(ledger!, 'utf8').trim().split('\n').map(l => JSON.parse(l));
|
||||
fs.writeFileSync(ledger!, rows.map(r => JSON.stringify(r.source === 'ci' ? { ...r, ts: at(30 * 60) } : r)).join('\n') + '\n');
|
||||
expect(f.run(path.join(BIN, 'gstack-evidence'), ['check', '--label', 'pnpm lint', '--max-age', '24']).stdout).toMatch(/^EVIDENCE: STALE .* reason=older than 24h/m);
|
||||
expect(carried()).toEqual([]);
|
||||
});
|
||||
|
||||
test('finish defers the mapped lane; accept carries it from the green run at the pushed head', () => {
|
||||
const f = fx();
|
||||
world(f);
|
||||
|
||||
Reference in new issue
Block a user