diff --git a/review/sections/review-army.md b/review/sections/review-army.md
index 10f990caa..827029a6f 100644
--- a/review/sections/review-army.md
+++ b/review/sections/review-army.md
@@ -5,6 +5,7 @@
### Detect stack and scope
```bash
+if _SM=$(~/.claude/skills/gstack/bin/gstack-state-matrix detect --base ); then source <(printf '%s\n' "$_SM"); else echo "$_SM" | grep '^#'; SCOPE_STATE_MACHINE=unknown; fi
source <(~/.claude/skills/gstack/bin/gstack-diff-scope 2>/dev/null) || true
set | grep '^SCOPE_'
# Detect stack for specialist context
@@ -53,7 +54,12 @@ Based on the scope signals above, select which specialists to dispatch.
6. **API Contract** — if SCOPE_API=true. Read `~/.claude/skills/gstack/review/specialists/api-contract.md`
7. **Design** — if SCOPE_FRONTEND=true. Use the existing design review checklist at `~/.claude/skills/gstack/review/design-checklist.md` and run the mechanical pass at the top of that checklist (the user-installed design detector, when present) before the LLM items
8. **Simplification** — if DIFF_LINES > 100. Read `~/.claude/skills/gstack/review/specialists/simplification.md`. Advisory-only lens: hunts unrequested structure (hand-rolled stdlib, one-implementation abstractions, dependencies duplicating platform features), never coverage.
-9. **State Machines** — if `~/.claude/skills/gstack/bin/gstack-state-matrix detect --base ` says SCOPE_STATE_MACHINE=true: read `~/.claude/skills/gstack/review/specialists/state-machines.md`, then pipe output to `check -`.
+9. **State Machines** — if SCOPE_STATE_MACHINE is true or unknown (printed by the scope block): read `~/.claude/skills/gstack/review/specialists/state-machines.md`, then check its complete output:
+```bash
+~/.claude/skills/gstack/bin/gstack-state-matrix check - <<'STATE_MACHINES_OUTPUT'
+
+STATE_MACHINES_OUTPUT
+```
### Adaptive gating
diff --git a/scripts/resolvers/review-army.ts b/scripts/resolvers/review-army.ts
index 49faaf5d9..18cd35797 100644
--- a/scripts/resolvers/review-army.ts
+++ b/scripts/resolvers/review-army.ts
@@ -24,6 +24,7 @@ function generateSpecialistSelection(ctx: TemplateContext): string {
### Detect stack and scope
\`\`\`bash
+if _SM=$(${ctx.paths.binDir}/gstack-state-matrix detect --base ); then source <(printf '%s\\n' "$_SM"); else echo "$_SM" | grep '^#'; SCOPE_STATE_MACHINE=unknown; fi
source <(${ctx.paths.binDir}/gstack-diff-scope 2>/dev/null) || true
set | grep '^SCOPE_'
# Detect stack for specialist context
@@ -72,7 +73,12 @@ Based on the scope signals above, select which specialists to dispatch.
6. **API Contract** — if SCOPE_API=true. Read \`${ctx.paths.skillRoot}/review/specialists/api-contract.md\`
7. **Design** — if SCOPE_FRONTEND=true. Use the existing design review checklist at \`${ctx.paths.skillRoot}/review/design-checklist.md\` and run the mechanical pass at the top of that checklist (the user-installed design detector, when present) before the LLM items
8. **Simplification** — if DIFF_LINES > 100. Read \`${ctx.paths.skillRoot}/review/specialists/simplification.md\`. Advisory-only lens: hunts unrequested structure (hand-rolled stdlib, one-implementation abstractions, dependencies duplicating platform features), never coverage.
-9. **State Machines** — if \`${ctx.paths.binDir}/gstack-state-matrix detect --base \` says SCOPE_STATE_MACHINE=true: read \`${ctx.paths.skillRoot}/review/specialists/state-machines.md\`, then pipe output to \`check -\`.
+9. **State Machines** — if SCOPE_STATE_MACHINE is true or unknown (printed by the scope block): read \`${ctx.paths.skillRoot}/review/specialists/state-machines.md\`, then check its complete output:
+\`\`\`bash
+${ctx.paths.binDir}/gstack-state-matrix check - <<'STATE_MACHINES_OUTPUT'
+
+STATE_MACHINES_OUTPUT
+\`\`\`
### Adaptive gating
diff --git a/ship/sections/review-army.md b/ship/sections/review-army.md
index 106f92ef9..a767ce649 100644
--- a/ship/sections/review-army.md
+++ b/ship/sections/review-army.md
@@ -267,6 +267,7 @@ distinct defects stay separate and neither pass substitutes for the other.
### Detect stack and scope
```bash
+if _SM=$(~/.claude/skills/gstack/bin/gstack-state-matrix detect --base ); then source <(printf '%s\n' "$_SM"); else echo "$_SM" | grep '^#'; SCOPE_STATE_MACHINE=unknown; fi
source <(~/.claude/skills/gstack/bin/gstack-diff-scope 2>/dev/null) || true
set | grep '^SCOPE_'
# Detect stack for specialist context
@@ -315,7 +316,12 @@ Based on the scope signals above, select which specialists to dispatch.
6. **API Contract** — if SCOPE_API=true. Read `~/.claude/skills/gstack/review/specialists/api-contract.md`
7. **Design** — if SCOPE_FRONTEND=true. Use the existing design review checklist at `~/.claude/skills/gstack/review/design-checklist.md` and run the mechanical pass at the top of that checklist (the user-installed design detector, when present) before the LLM items
8. **Simplification** — if DIFF_LINES > 100. Read `~/.claude/skills/gstack/review/specialists/simplification.md`. Advisory-only lens: hunts unrequested structure (hand-rolled stdlib, one-implementation abstractions, dependencies duplicating platform features), never coverage.
-9. **State Machines** — if `~/.claude/skills/gstack/bin/gstack-state-matrix detect --base ` says SCOPE_STATE_MACHINE=true: read `~/.claude/skills/gstack/review/specialists/state-machines.md`, then pipe output to `check -`.
+9. **State Machines** — if SCOPE_STATE_MACHINE is true or unknown (printed by the scope block): read `~/.claude/skills/gstack/review/specialists/state-machines.md`, then check its complete output:
+```bash
+~/.claude/skills/gstack/bin/gstack-state-matrix check - <<'STATE_MACHINES_OUTPUT'
+
+STATE_MACHINES_OUTPUT
+```
### Adaptive gating
diff --git a/test/qa-exploratory-callers.test.ts b/test/qa-exploratory-callers.test.ts
index beac16aa7..8dacfcc56 100644
--- a/test/qa-exploratory-callers.test.ts
+++ b/test/qa-exploratory-callers.test.ts
@@ -196,6 +196,20 @@ describe('caller native-event observer controls', () => {
}
});
+ test('captured 39002269281 standalone state-machine detect stays outside the caller interface; the generated scope block runs it', async () => {
+ // PR gate run 39002269281 ship-exploratory-plan-checks, native Bash call 17: the selection prose named the detector, so the
+ // actor ran it alone. The detector now runs inside the generated scope block, which is the declared form.
+ const fixture = createQaCallerFixture('ship-exploratory-plan-checks', { installRuntime: false });
+ try {
+ const standalone = `${fixture.runtime}/bin/gstack-state-matrix detect --base main`;
+ expect(qaCallerCommandAllowed(standalone, fixture.workflowCommands)).toBe(false);
+ const block = fixture.workflowCommands.find(command => command.includes('/bin/gstack-state-matrix detect --base main'));
+ expect(block).toBeDefined();
+ expect(block).toContain('/gstack-diff-scope main 2>/dev/null) || true');
+ expect(qaCallerCommandAllowed(block!, fixture.workflowCommands)).toBe(true);
+ } finally { await fixture.close(); fs.rmSync(fixture.root, { recursive: true, force: true }); }
+ });
+
test.each(['pending-result', 'same-event', 'same-message-id'])('method Read %s cannot authorize a dependent probe', ordering => {
const observed = evidence();
const events = observed.result.transcript as any[];
diff --git a/test/review-workflow-clarity.test.ts b/test/review-workflow-clarity.test.ts
index c356d70fa..5206895e5 100644
--- a/test/review-workflow-clarity.test.ts
+++ b/test/review-workflow-clarity.test.ts
@@ -1,5 +1,7 @@
import { expect, test } from 'bun:test';
-import { readFileSync } from 'node:fs';
+import { spawnSync } from 'node:child_process';
+import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
+import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { generateReviewArmy } from '../scripts/resolvers/review-army';
import { generateCrossReviewDedup, generateSharedCodeReuse, generateScopeDrift } from '../scripts/resolvers/review-scope';
@@ -371,13 +373,46 @@ for (const skillName of ['review', 'ship']) {
const flags = between(readFileSync(join(root, 'bin/gstack-diff-scope'), 'utf8'), '_print_flags() {', '}');
for (const signal of used) {
if (flags.includes(`echo "${signal}=`)) continue;
- // A signal the scope block does not print must come from the helper its own selection line runs.
- const helper = /bin\/(gstack-[a-z-]+) detect\b/.exec(selection.split('\n').find(line => line.includes(signal))!)?.[1];
+ // PR gate run 39002269281: a signal gstack-diff-scope does not print comes from a detect helper the scope
+ // block itself runs, never from a command named only in selection prose.
+ const helper = /bin\/(gstack-[a-z-]+) detect\b/.exec(block.split('\n').find(line => line.includes(signal))!)?.[1];
expect(helper, signal).toBeDefined();
expect(readFileSync(join(root, 'bin', helper!), 'utf8'), signal).toContain(`${signal}=\${`);
+ expect(selection, signal).not.toMatch(/\bdetect\b/);
}
});
+ test(`${skillName} clarity: the state-machine signal checks the detector's exit status and fails toward dispatch`, () => {
+ const block = between(army, '### Detect stack and scope', '### Read specialist hit rates');
+ expect(block).toContain(`if _SM=$(${HOST_PATHS.claude.binDir}/gstack-state-matrix detect --base ); then source <(printf '%s\\n' "$_SM"); else echo "$_SM" | grep '^#'; SCOPE_STATE_MACHINE=unknown; fi\n`);
+ expect(block.indexOf('SCOPE_STATE_MACHINE=unknown')).toBeLessThan(block.indexOf("set | grep '^SCOPE_'"));
+ const selection = between(army, '### Select specialists', '### Adaptive gating').replace(/\s+/g, ' ');
+ expect(selection).toContain('9. **State Machines** — if SCOPE_STATE_MACHINE is true or unknown (printed by the scope block)');
+ expect(selection).toContain("gstack-state-matrix check - <<'STATE_MACHINES_OUTPUT' STATE_MACHINES_OUTPUT");
+ expect(selection).not.toContain('pipe output');
+ });
+
+ test(`${skillName} clarity: the scope block run against a failing detector prints unknown and keeps its diagnostics`, () => {
+ const stubs = mkdtempSync(join(realpathSync(tmpdir()), 'scope-block-'));
+ try {
+ const block = /\`\`\`bash\n([\s\S]*?)\n\`\`\`/.exec(between(generateReviewArmy({ ...ctx, paths: { ...ctx.paths, binDir: stubs } }),
+ '### Detect stack and scope', '### Read specialist hit rates'))![1].replaceAll('', 'main');
+ writeFileSync(join(stubs, 'gstack-diff-scope'), '#!/bin/sh\necho SCOPE_FRONTEND=false\n', { mode: 0o755 });
+ const run = (detector: string) => {
+ writeFileSync(join(stubs, 'gstack-state-matrix'), `#!/bin/sh\n${detector}\n`, { mode: 0o755 });
+ return spawnSync('bash', ['-c', block], { cwd: stubs, encoding: 'utf8', timeout: 10_000 });
+ };
+ const failed = run("echo SCOPE_STATE_MACHINE=false; echo '# no merge base with origin/main; run: git fetch origin main'; echo 'detector crashed' >&2; exit 2");
+ expect(failed.stdout).toMatch(/^SCOPE_STATE_MACHINE=unknown$/m);
+ expect(failed.stdout).not.toMatch(/^SCOPE_STATE_MACHINE=false$/m);
+ expect(failed.stdout).toContain('# no merge base with origin/main; run: git fetch origin main');
+ expect(failed.stderr).toContain('detector crashed');
+ const detected = run("echo SCOPE_STATE_MACHINE=true; echo '# state-machine: src/takes.ts:4 proposal,accept'");
+ expect(detected.stdout).toMatch(/^SCOPE_STATE_MACHINE=true$/m);
+ expect(detected.stdout).toMatch(/^SCOPE_FRONTEND=false$/m);
+ } finally { rmSync(stubs, { recursive: true, force: true }); }
+ });
+
test(`${skillName} clarity: terminal failure permits independent work but never certifies coverage`, () => {
expectMentions(flat, [['confirm', 'finished', 'stopped']], 'flat');
expectMentions(flat, [['does not', 'termination', 'timeout']], 'flat');