diff --git a/changelog.d/gst-969-qa-tree-writes.md b/changelog.d/gst-969-qa-tree-writes.md index 604321faa..8962ded61 100644 --- a/changelog.d/gst-969-qa-tree-writes.md +++ b/changelog.d/gst-969-qa-tree-writes.md @@ -1,3 +1,3 @@ ### Fixed -- **/ship's exploratory QA no longer writes reports into your project (GST-969).** Step 9.2.1 used to put its QA reports, deadline file and captured evidence in `.gstack/qa-reports/` inside the repository under review. In a project that doesn't ignore `.gstack/`, those files showed up as untracked changes, so they could land in the PR and change the tree the review certified. The new `gstack-qa-evidence report-dir` command creates a fresh run directory under `.gstack/tmp/qa-reports/` and adds `/.gstack/tmp/` to the repository's git exclude file, and /ship's QA step now uses it. It refuses a symlinked `.gstack`, a `.gitignore` rule that un-ignores `.gstack/tmp`, and a directory outside a git repository. /review's Step 4.7 still uses `.gstack/qa-reports`: /review's prompt is already at its size cap. +- **/ship and /review exploratory QA no longer write reports into your project (GST-969).** /ship Step 9.2.1 and /review Step 4.7 used to put its QA reports, deadline file and captured evidence in `.gstack/qa-reports/` inside the repository under review. In a project that doesn't ignore `.gstack/`, those files showed up as untracked changes, so they could land in the PR and change the tree the review certified. The new `gstack-qa-evidence report-dir` command creates a fresh run directory under `.gstack/tmp/qa-reports/` and adds `/.gstack/tmp/` to the repository's git exclude file, and both QA steps now use it. It refuses a symlinked `.gstack`, a `.gitignore` rule that un-ignores `.gstack/tmp`, and a directory outside a git repository. /qa keeps its `.gstack/qa-reports` output directory. diff --git a/lib/qa-evidence.ts b/lib/qa-evidence.ts index 111872c4c..841cb2354 100644 --- a/lib/qa-evidence.ts +++ b/lib/qa-evidence.ts @@ -388,7 +388,7 @@ function materialize(root: string, source: string) { next: `Include every reportLinks entry in the Markdown report, and report the overall status as ${verdict.status}${verdict.open.length ? ` (open: ${verdict.open.join('; ')})` : ''}; this verdict is final. Next, write the Markdown report beside evidence.json.` }; } -/** A fresh report root for /review and /ship QA under the repository's git-excluded .gstack/tmp, so probes never change the reviewed tree. */ +/** A fresh report root for caller QA (/review and /ship) under the repository's git-excluded .gstack/tmp, so probes never change the reviewed tree. */ function reportDir(): Record { const top = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8', timeout: 5000 }); if (top.status !== 0 || !top.stdout.trim()) throw new QaEvidenceError('report-dir needs a git work tree'); @@ -418,7 +418,7 @@ function reportDir(): Record { } } -const QA_EVIDENCE_USAGE = 'report-dir (prints a fresh git-excluded report root for /ship QA) | capture ROOT ID [--public] --deadline FILE|--timeout-ms MS [--after PREVIOUS_CAPTURE --hypothesis TEXT] -- COMMAND ARGS (--after publishes checkpoint ID linking PREVIOUS_CAPTURE to this probe; required after the first complete capture unless a checkpoint was published) | checkpoint ROOT ID CAPTURE OBSERVATION_COMMAND HYPOTHESIS NEXT_COMMAND | checkpoint ROOT ID INTENT_FILE | materialize ROOT ANNOTATIONS (annotations: {evidence: [{capture, command, contract, expected, classification: pass|superseded|product-defect|fail|setup-blocked|blocked|inconclusive}], limits: [..]}; revision, runtime, cwd and learning are filled in)'; +const QA_EVIDENCE_USAGE = 'report-dir (prints a fresh git-excluded report root for /review and /ship QA) | capture ROOT ID [--public] --deadline FILE|--timeout-ms MS [--after PREVIOUS_CAPTURE --hypothesis TEXT] -- COMMAND ARGS (--after publishes checkpoint ID linking PREVIOUS_CAPTURE to this probe; required after the first complete capture unless a checkpoint was published) | checkpoint ROOT ID CAPTURE OBSERVATION_COMMAND HYPOTHESIS NEXT_COMMAND | checkpoint ROOT ID INTENT_FILE | materialize ROOT ANNOTATIONS (annotations: {evidence: [{capture, command, contract, expected, classification: pass|superseded|product-defect|fail|setup-blocked|blocked|inconclusive}], limits: [..]}; revision, runtime, cwd and learning are filled in)'; export async function qaEvidenceMain(args: string[]): Promise { return withQaReceiptOutput(false, 'qa-evidence-receipt', value => value.event === 'observation' diff --git a/qa/sections/exploratory.md b/qa/sections/exploratory.md index 0c62850b0..1364afdb9 100644 --- a/qa/sections/exploratory.md +++ b/qa/sections/exploratory.md @@ -19,7 +19,7 @@ Missing or unreadable assets, prerequisites or permission block affected probes, ## 1. Charter and preflight -Reuse resolved REPORT_DIR; otherwise own a fresh `.gstack/qa-reports` subdirectory. +Reuse resolved REPORT_DIR; otherwise use the `reportDir` `EVIDENCE_TOOL report-dir` prints. Write a **charter** per behavior: contract, risk, entrypoint, isolation, exit condition, source, commands and inputs. Save charters as Markdown in the report. For /review and /ship, no plan/server is required. diff --git a/scripts/resolvers/qa.ts b/scripts/resolvers/qa.ts index 7ff67ecb3..76be4da89 100644 --- a/scripts/resolvers/qa.ts +++ b/scripts/resolvers/qa.ts @@ -66,7 +66,7 @@ full contents. If either required Read is missing, complete it now before Charte ## 1. Charter and preflight -Reuse resolved REPORT_DIR; otherwise own a fresh \`.gstack/qa-reports\` subdirectory. +${reportOnly ? 'Reuse resolved REPORT_DIR; otherwise own a fresh \`.gstack/qa-reports\` subdirectory.' : 'Reuse resolved REPORT_DIR; otherwise use the \`reportDir\` \`EVIDENCE_TOOL report-dir\` prints.'} Write a **charter** per behavior: contract, risk, entrypoint, isolation, exit condition, source, commands and inputs. Save charters as Markdown in the report. ${reportOnly ? '' : `For /review and /ship, no plan/server is required. @@ -244,7 +244,7 @@ export function generateQAReview(ctx: TemplateContext): string { Only the parent runs report-only discovery. Never overwrite another run's reports. Batch only independent Reads. -${ship ? `Without a caller REPORT_DIR, set it to the \`reportDir\` that \`${quoteSafePath(ctx.paths.binDir)}/gstack-qa-evidence report-dir\` prints: git-excluded, so QA never changes the reviewed tree.\n` : ''} + ${ship ? `**1. Load methods before any QA or explicit-verification probe.** ${generateQAReviewPreflight(ctx)}` : `**1. Set the charter and isolation.** diff --git a/ship/sections/review-army.md b/ship/sections/review-army.md index 81040c5f8..f78dda7d1 100644 --- a/ship/sections/review-army.md +++ b/ship/sections/review-army.md @@ -594,7 +594,6 @@ If the Red Team fails or times out, confirm it stopped and record its review as Only the parent runs report-only discovery. Never overwrite another run's reports. Batch only independent Reads. -Without a caller REPORT_DIR, set it to the `reportDir` that `$HOME/.claude/skills/gstack/bin/gstack-qa-evidence report-dir` prints: git-excluded, so QA never changes the reviewed tree. **1. Load methods before any QA or explicit-verification probe.** diff --git a/test/gstack-clean-producers.test.ts b/test/gstack-clean-producers.test.ts index daad0df41..937eb19cb 100644 --- a/test/gstack-clean-producers.test.ts +++ b/test/gstack-clean-producers.test.ts @@ -42,6 +42,7 @@ const KEPT: Record = { 'review-*': '/ship review loop records', 'fix-check-*': '/ship fix-check inputs', 'tasks-*': 'gstack-tasks rows files (bin/gstack-tasks)', + 'qa-reports': 'caller QA report runs from gstack-qa-evidence report-dir (lib/qa-evidence.ts)', 'autoplan': 'autoplan phase artifacts (autoplan/SKILL.md.tmpl)', 'eval-bisect': '/investigate eval bisect cache', 'price-gate': 'gstack-eval-plan price receipts', diff --git a/test/qa-report-dir.test.ts b/test/qa-report-dir.test.ts index e084b8830..97a121703 100644 --- a/test/qa-report-dir.test.ts +++ b/test/qa-report-dir.test.ts @@ -95,9 +95,10 @@ test('report-dir refuses outside a git work tree instead of guessing a location' expect(fs.readdirSync(outside)).toEqual([]); }); -test('/ship Step 9.2.1 resolves REPORT_DIR through report-dir, not the product tree', () => { - const ctx: TemplateContext = { host: 'claude', skillName: 'ship', tmplPath: '', paths: HOST_PATHS.claude }; - expect(RESOLVERS.QA_REVIEW(ctx)).toContain('set it to the `reportDir` that `$HOME/.claude/skills/gstack/bin/gstack-qa-evidence report-dir` prints: git-excluded'); - const rendered = fs.readFileSync(path.resolve(import.meta.dir, '../ship/sections/review-army.md'), 'utf8'); - expect(rendered).toContain('gstack-qa-evidence report-dir` prints: git-excluded'); +test('caller QA without a resolved REPORT_DIR (/review, /ship) falls back to report-dir, not the product tree', () => { + const line = 'Reuse resolved REPORT_DIR; otherwise use the `reportDir` `EVIDENCE_TOOL report-dir` prints.'; + const ctx: TemplateContext = { host: 'claude', skillName: 'qa', tmplPath: '', paths: HOST_PATHS.claude }; + expect(RESOLVERS.QA_EXPLORATORY(ctx)).toContain(line); + expect(fs.readFileSync(path.resolve(import.meta.dir, '../qa/sections/exploratory.md'), 'utf8')).toContain(line); + expect(fs.readFileSync(path.resolve(import.meta.dir, '../qa/SKILL.md.tmpl'), 'utf8')).toContain('Use the directory as `REPORT_DIR`'); }); diff --git a/test/size-ledger/ship/gst-969-qa-tree-writes.json b/test/size-ledger/ship/gst-969-qa-tree-writes.json deleted file mode 100644 index b64cc6568..000000000 --- a/test/size-ledger/ship/gst-969-qa-tree-writes.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "bytes": 185, - "why": "GST-969: /ship Step 9.2.1 QA writes its reports under gstack-qa-evidence report-dir (git-excluded .gstack/tmp), not the product tree" -}