@mmnto/cli 1.101.2 → 1.103.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.
Files changed (45) hide show
  1. package/dist/commands/init-detect.js +1 -1
  2. package/dist/commands/init-detect.js.map +1 -1
  3. package/dist/commands/init-detect.test.js +2 -1
  4. package/dist/commands/init-detect.test.js.map +1 -1
  5. package/dist/commands/init-templates.d.ts.map +1 -1
  6. package/dist/commands/init-templates.js +118 -9
  7. package/dist/commands/init-templates.js.map +1 -1
  8. package/dist/commands/init.test.js +186 -5
  9. package/dist/commands/init.test.js.map +1 -1
  10. package/dist/commands/review-fan.d.ts +9 -3
  11. package/dist/commands/review-fan.d.ts.map +1 -1
  12. package/dist/commands/review-fan.js +131 -16
  13. package/dist/commands/review-fan.js.map +1 -1
  14. package/dist/commands/review-fan.test.js +246 -7
  15. package/dist/commands/review-fan.test.js.map +1 -1
  16. package/dist/commands/shield-nonreview.test.d.ts +24 -0
  17. package/dist/commands/shield-nonreview.test.d.ts.map +1 -0
  18. package/dist/commands/shield-nonreview.test.js +175 -0
  19. package/dist/commands/shield-nonreview.test.js.map +1 -0
  20. package/dist/commands/shield.d.ts +33 -7
  21. package/dist/commands/shield.d.ts.map +1 -1
  22. package/dist/commands/shield.js +96 -50
  23. package/dist/commands/shield.js.map +1 -1
  24. package/dist/commands/shield.test.js +69 -2
  25. package/dist/commands/shield.test.js.map +1 -1
  26. package/dist/hook/schema.d.ts +2 -2
  27. package/dist/orchestrators/orchestrator.d.ts +86 -1
  28. package/dist/orchestrators/orchestrator.d.ts.map +1 -1
  29. package/dist/orchestrators/orchestrator.js +277 -9
  30. package/dist/orchestrators/orchestrator.js.map +1 -1
  31. package/dist/orchestrators/orchestrator.test.js +240 -4
  32. package/dist/orchestrators/orchestrator.test.js.map +1 -1
  33. package/dist/orchestrators/shell-orchestrator.d.ts +12 -1
  34. package/dist/orchestrators/shell-orchestrator.d.ts.map +1 -1
  35. package/dist/orchestrators/shell-orchestrator.js +256 -49
  36. package/dist/orchestrators/shell-orchestrator.js.map +1 -1
  37. package/dist/orchestrators/shell-orchestrator.test.js +252 -4
  38. package/dist/orchestrators/shell-orchestrator.test.js.map +1 -1
  39. package/dist/utils.d.ts +18 -1
  40. package/dist/utils.d.ts.map +1 -1
  41. package/dist/utils.js +389 -108
  42. package/dist/utils.js.map +1 -1
  43. package/dist/utils.test.js +406 -14
  44. package/dist/utils.test.js.map +1 -1
  45. package/package.json +2 -2
@@ -0,0 +1,175 @@
1
+ /**
2
+ * CLI-level deterministic-skip NON-REVIEW tests (mmnto-ai/totem#2466).
3
+ *
4
+ * `totem review` has three deterministic skip paths that drop the ENTIRE diff
5
+ * without examining it: all-non-code, no-code-after-filtering, and all-generated.
6
+ * Each used to call `writeReviewedContentHash(...)` before returning, minting the
7
+ * `.reviewed-content-hash` push-gate stamp for content nothing reviewed.
8
+ *
9
+ * The danger is not uniform, which is why all three are pinned here rather than
10
+ * only the reported one:
11
+ *
12
+ * - all-non-code / filtered-empty fire only when no code file is in the diff,
13
+ * so the hash is unchanged and the stamp was a no-op. The defect is the
14
+ * dishonest clean-pass surface (Tenets 4/13).
15
+ * - all-generated can drop a TRACKED, HASHED source file, because
16
+ * `.gitattributes linguist-generated` can mark a `.ts` as generated. There the
17
+ * stamp genuinely authorized never-reviewed code — a push-gate bypass.
18
+ *
19
+ * Mirrors `shield-covariate.test.ts`: its own file to avoid mock contamination,
20
+ * mocking the heavy seams (config, engine bootstrap, hook installer, git diff) so
21
+ * the skip branches are exercised without a real repo, config, or network invoke.
22
+ */
23
+ import * as fs from 'node:fs';
24
+ import * as os from 'node:os';
25
+ import * as path from 'node:path';
26
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
27
+ import { cleanTmpDir } from '../test-utils.js';
28
+ // ── Seams that MUST NOT run once a skip path is taken ──
29
+ const bootstrapEngineSpy = vi.fn(async (..._args) => { });
30
+ const upgradePrePushHookSpy = vi.fn((..._args) => false);
31
+ const getDiffForReviewSpy = vi.fn(async (..._args) => null);
32
+ const TEST_CONFIG = {
33
+ totemDir: '.totem',
34
+ review: { sourceExtensions: ['.ts'] },
35
+ };
36
+ vi.mock('../utils/bootstrap-engine.js', async (importOriginal) => {
37
+ const actual = await importOriginal();
38
+ return { ...actual, bootstrapEngine: bootstrapEngineSpy };
39
+ });
40
+ vi.mock('./install-hooks.js', async (importOriginal) => {
41
+ const actual = await importOriginal();
42
+ return { ...actual, upgradePrePushHookIfNeeded: upgradePrePushHookSpy };
43
+ });
44
+ vi.mock('../utils.js', async (importOriginal) => {
45
+ const actual = await importOriginal();
46
+ return {
47
+ ...actual,
48
+ loadEnv: vi.fn(),
49
+ resolveConfigPath: (cwd) => path.join(cwd, 'totem.config.ts'),
50
+ loadConfig: vi.fn(async () => TEST_CONFIG),
51
+ };
52
+ });
53
+ vi.mock('../git.js', async (importOriginal) => {
54
+ const actual = await importOriginal();
55
+ return { ...actual, getDiffForReview: getDiffForReviewSpy };
56
+ });
57
+ /** A minimal single-file diff section for `file`. */
58
+ function diffFor(file) {
59
+ return [
60
+ `diff --git a/${file} b/${file}`,
61
+ 'index 1111111..2222222 100644',
62
+ `--- a/${file}`,
63
+ `+++ b/${file}`,
64
+ '@@ -1 +1 @@',
65
+ '-old',
66
+ '+new',
67
+ ].join('\n');
68
+ }
69
+ describe('deterministic skip paths are NON-REVIEWS and never stamp (#2466)', () => {
70
+ let tmpDir;
71
+ let warnings;
72
+ const stampPath = () => path.join(tmpDir, '.totem', 'cache', '.reviewed-content-hash');
73
+ beforeEach(() => {
74
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'shield-nonreview-'));
75
+ warnings = [];
76
+ vi.spyOn(process, 'cwd').mockReturnValue(tmpDir);
77
+ vi.spyOn(console, 'error').mockImplementation((...args) => {
78
+ warnings.push(args.map(String).join(' '));
79
+ });
80
+ vi.spyOn(console, 'log').mockImplementation((...args) => {
81
+ warnings.push(args.map(String).join(' '));
82
+ });
83
+ bootstrapEngineSpy.mockClear();
84
+ getDiffForReviewSpy.mockClear();
85
+ });
86
+ afterEach(() => {
87
+ vi.restoreAllMocks();
88
+ // The non-skip case (mixed diff) runs into the LLM path and aborts on the
89
+ // unmocked embedding config, which can leave a handle open under the temp
90
+ // dir — Windows then EPERMs the recursive remove. That is a harness artifact
91
+ // of driving the real command, not a product behavior, and it must not fail
92
+ // a suite whose assertions are all about the stamp and the emitted output.
93
+ try {
94
+ cleanTmpDir(tmpDir);
95
+ }
96
+ catch {
97
+ // Best-effort cleanup; the OS reclaims the temp dir.
98
+ }
99
+ });
100
+ /**
101
+ * Drive the real command with a diff whose files are all `files`.
102
+ *
103
+ * A run that does NOT take a skip path continues into the LLM review path and
104
+ * throws on the unmocked embedding config. That throw is expected and is itself
105
+ * evidence the skip did not fire, so it is swallowed here — every assertion
106
+ * below is made on the stamp and the emitted output, never on resolution.
107
+ */
108
+ async function runWithDiff(files) {
109
+ await runWithDiffRaw(files.map(diffFor).join('\n'), files);
110
+ }
111
+ /**
112
+ * Lower-level driver for cases where the diff BODY and the changed-file LIST
113
+ * must differ — the filtered-empty path needs a code file present in the list
114
+ * whose hunks are absent from the diff (a mode-only change has this shape).
115
+ */
116
+ async function runWithDiffRaw(diff, changedFiles) {
117
+ getDiffForReviewSpy.mockResolvedValueOnce({ diff, changedFiles });
118
+ const { shieldCommand } = await import('./shield.js');
119
+ try {
120
+ await shieldCommand({});
121
+ }
122
+ catch {
123
+ // See doc comment — a non-skip run is expected to fail downstream.
124
+ }
125
+ }
126
+ it('all-non-code: does not stamp, and says so', async () => {
127
+ await runWithDiff(['docs/plan.md', 'README.md']);
128
+ // The load-bearing assertion: no push authorization was minted.
129
+ expect(fs.existsSync(stampPath())).toBe(false);
130
+ // And the skip is LOUD — a caller cannot read it as a clean review.
131
+ expect(warnings.join('\n')).toMatch(/NON-REVIEW/);
132
+ expect(warnings.join('\n')).toMatch(/does not authorize a push/);
133
+ });
134
+ it('all-generated: does not stamp — the path that could authorize real code', async () => {
135
+ // A lockfile is generated by default glob, so the whole diff drops.
136
+ await runWithDiff(['pnpm-lock.yaml']);
137
+ expect(fs.existsSync(stampPath())).toBe(false);
138
+ expect(warnings.join('\n')).toMatch(/NON-REVIEW/);
139
+ expect(warnings.join('\n')).toMatch(/does not authorize a push/);
140
+ });
141
+ it('all-generated via .gitattributes on a TRACKED .ts: does not stamp (the bypass case)', async () => {
142
+ // The scenario the default-glob lockfile test does NOT reach, and the only
143
+ // one where the old stamp could authorize genuinely-changed, hashed source:
144
+ // `linguist-generated` marks a real `.ts`, so the file is dropped from review
145
+ // while still counting toward the content hash the push gate compares.
146
+ fs.writeFileSync(path.join(tmpDir, '.gitattributes'), 'src/generated-client.ts linguist-generated\n');
147
+ await runWithDiff(['src/generated-client.ts']);
148
+ expect(fs.existsSync(stampPath())).toBe(false);
149
+ // Assert the GENERATED-path wording specifically. A bare /NON-REVIEW/ match
150
+ // would also pass if this fell through to the all-non-code branch, which
151
+ // would mean the test proves nothing about the bypass it exists to cover.
152
+ expect(warnings.join('\n')).toMatch(/every changed file is a generated artifact/);
153
+ expect(warnings.join('\n')).toMatch(/does not authorize a push/);
154
+ });
155
+ it('filtered-empty: does not stamp when no code hunks survive filtering', async () => {
156
+ // Stage 2 fires when the diff is neither all-code nor all-non-code and the
157
+ // code side contributes no hunks — a mode-only change on a tracked source
158
+ // file has exactly this shape: listed as changed, absent from the diff body.
159
+ await runWithDiffRaw(diffFor('docs/plan.md'), ['docs/plan.md', 'src/index.ts']);
160
+ expect(fs.existsSync(stampPath())).toBe(false);
161
+ // Stage-2 wording specifically — distinguishes this from the Stage-1
162
+ // all-non-code branch, which a generic NON-REVIEW match would not.
163
+ expect(warnings.join('\n')).toMatch(/no code changes remain after filtering/);
164
+ expect(warnings.join('\n')).toMatch(/does not authorize a push/);
165
+ });
166
+ it('mixed code + prose does NOT take a skip path (the skip must not over-fire)', async () => {
167
+ // Guards the inverse error: tightening the skip must not start skipping real
168
+ // code. A surviving `.ts` file means this is a review, not a non-review.
169
+ await runWithDiff(['docs/plan.md', 'src/index.ts']);
170
+ expect(warnings.join('\n')).not.toMatch(/NON-REVIEW/);
171
+ // It also must not stamp here — this run never completed a review either.
172
+ expect(fs.existsSync(stampPath())).toBe(false);
173
+ });
174
+ });
175
+ //# sourceMappingURL=shield-nonreview.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shield-nonreview.test.js","sourceRoot":"","sources":["../../src/commands/shield-nonreview.test.ts"],"names":[],"mappings":"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"}
@@ -15,10 +15,32 @@ export declare function parseVerdict(content: string): {
15
15
  pass: boolean;
16
16
  reason: string;
17
17
  } | null;
18
+ export type StructuredVerdictExtractionLayer = 'xml' | 'fence' | 'bare-json';
19
+ export type StructuredVerdictExtractionFailureCause = 'empty-output' | 'no-candidate' | 'invalid-json' | 'schema-invalid';
20
+ export interface StructuredVerdictExtractionAttempt {
21
+ layer: StructuredVerdictExtractionLayer;
22
+ cause: Extract<StructuredVerdictExtractionFailureCause, 'invalid-json' | 'schema-invalid'>;
23
+ /** Bounded validation summaries; candidate/output text is never retained here. */
24
+ issues?: string[];
25
+ }
26
+ export type StructuredVerdictExtractionResult = {
27
+ ok: true;
28
+ verdict: ShieldStructuredVerdict;
29
+ layer: StructuredVerdictExtractionLayer;
30
+ } | {
31
+ ok: false;
32
+ cause: StructuredVerdictExtractionFailureCause;
33
+ attempts: StructuredVerdictExtractionAttempt[];
34
+ };
18
35
  /**
19
- * Three-layer JSON extraction from LLM plain-text response.
20
- * Layer 1: XML tags, Layer 2: markdown code fences, Layer 3: bare JSON.
21
- * Returns null if all layers fail (caller falls back to V1 regex parseVerdict).
36
+ * Three-layer JSON extraction with bounded, candidate-free failure diagnostics.
37
+ * A schema-invalid candidate takes precedence over malformed candidates because
38
+ * it is the most semantically advanced failure reached by the cascade.
39
+ */
40
+ export declare function extractStructuredVerdictDetailed(content: string): StructuredVerdictExtractionResult;
41
+ /**
42
+ * Compatibility wrapper for callers that only need the parsed verdict.
43
+ * Returns null when all XML, fenced, and bare-JSON layers fail.
22
44
  */
23
45
  export declare function extractStructuredVerdict(content: string): ShieldStructuredVerdict | null;
24
46
  /**
@@ -74,10 +96,14 @@ export declare function writeReviewedContentHashValue(precomputedHash: string, c
74
96
  * survives commits, amends, and rebases. Only breaks when source files change.
75
97
  *
76
98
  * Now a thin compose of the pure computer + explicit writer (Prop 304 R2): it
77
- * hashes the CURRENT tree and stamps it. Retained at its original signature
78
- * for the trivial fast-path stamps (no-changes / all-non-code / filtered-empty
79
- * none of which open a mid-run LLM window) and `recordShieldOverride`, where
80
- * there is no drift race to guard. The LLM review path does NOT use this — it
99
+ * hashes the CURRENT tree and stamps it. Retained at its original signature for
100
+ * the no-changes stamp (an empty diff opens no mid-run LLM window) and
101
+ * `recordShieldOverride`, where there is no drift race to guard.
102
+ *
103
+ * NO LONGER used by the deterministic skip paths (all-non-code / filtered-empty /
104
+ * all-generated). Those drop the entire diff without examining it, so they are
105
+ * non-reviews and must not stamp — see `NO_STAMP_NOTICE` (mmnto-ai/totem#2466).
106
+ * The LLM review path does NOT use this — it
81
107
  * captures the hash pre-fan and compare-and-stamps in `shieldCommand` /
82
108
  * `handleVerdictResult` so a mid-review edit can never be authorized.
83
109
  */
@@ -1 +1 @@
1
- {"version":3,"file":"shield.d.ts","sourceRoot":"","sources":["../../src/commands/shield.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"shield.d.ts","sourceRoot":"","sources":["../../src/commands/shield.ts"],"names":[],"mappings":"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"}
@@ -152,54 +152,84 @@ export function parseVerdict(content) {
152
152
  return null;
153
153
  return { pass: match[1] === 'PASS', reason: match[2].trim() };
154
154
  }
155
- // ─── V2 Structured verdict parsing ───────────────────
155
+ const MAX_EXTRACTION_ISSUES = 4;
156
+ const MAX_EXTRACTION_ISSUE_CHARS = 160;
157
+ function summarizeValidationIssues(issues) {
158
+ return issues.slice(0, MAX_EXTRACTION_ISSUES).map((issue) => {
159
+ const path = issue.path.length > 0 ? issue.path.join('.') : '<root>';
160
+ return `${path}: ${issue.code}`.slice(0, MAX_EXTRACTION_ISSUE_CHARS);
161
+ });
162
+ }
163
+ function tryStructuredVerdictCandidate(layer, candidate) {
164
+ let parsed;
165
+ try {
166
+ parsed = JSON.parse(candidate);
167
+ // totem-context: malformed candidates are expected during the extraction cascade and are converted into the typed invalid-json result below.
168
+ }
169
+ catch {
170
+ return { ok: false, attempt: { layer, cause: 'invalid-json' } };
171
+ }
172
+ const result = ShieldStructuredVerdictSchema.safeParse(parsed);
173
+ if (result.success)
174
+ return { ok: true, verdict: result.data, layer };
175
+ return {
176
+ ok: false,
177
+ attempt: {
178
+ layer,
179
+ cause: 'schema-invalid',
180
+ issues: summarizeValidationIssues(result.error.issues),
181
+ },
182
+ };
183
+ }
156
184
  /**
157
- * Three-layer JSON extraction from LLM plain-text response.
158
- * Layer 1: XML tags, Layer 2: markdown code fences, Layer 3: bare JSON.
159
- * Returns null if all layers fail (caller falls back to V1 regex parseVerdict).
185
+ * Three-layer JSON extraction with bounded, candidate-free failure diagnostics.
186
+ * A schema-invalid candidate takes precedence over malformed candidates because
187
+ * it is the most semantically advanced failure reached by the cascade.
160
188
  */
161
- export function extractStructuredVerdict(content) {
162
- // Layer 1 — XML tags (primary)
189
+ export function extractStructuredVerdictDetailed(content) {
190
+ if (content.trim().length === 0)
191
+ return { ok: false, cause: 'empty-output', attempts: [] };
192
+ const attempts = [];
193
+ const tryCandidate = (layer, candidate) => {
194
+ const result = tryStructuredVerdictCandidate(layer, candidate);
195
+ if (result.ok)
196
+ return result.verdict;
197
+ attempts.push(result.attempt);
198
+ return null;
199
+ };
163
200
  const xmlMatch = content.match(/<shield_verdict>([\s\S]*?)<\/shield_verdict>/);
164
201
  if (xmlMatch) {
165
- try {
166
- const parsed = JSON.parse(xmlMatch[1]);
167
- const result = ShieldStructuredVerdictSchema.safeParse(parsed);
168
- if (result.success)
169
- return result.data;
170
- }
171
- catch {
172
- // Move to next layer
173
- }
202
+ const verdict = tryCandidate('xml', xmlMatch[1]);
203
+ if (verdict)
204
+ return { ok: true, verdict, layer: 'xml' };
174
205
  }
175
- // Layer 2 — Markdown code fences (fallback)
176
206
  const fenceMatch = content.match(/(?:```|~~~)(?:json)?\s*\n([\s\S]*?)\n\s*(?:```|~~~)/);
177
207
  if (fenceMatch) {
178
- try {
179
- const parsed = JSON.parse(fenceMatch[1]);
180
- const result = ShieldStructuredVerdictSchema.safeParse(parsed);
181
- if (result.success)
182
- return result.data;
183
- }
184
- catch {
185
- // Move to next layer
186
- }
208
+ const verdict = tryCandidate('fence', fenceMatch[1]);
209
+ if (verdict)
210
+ return { ok: true, verdict, layer: 'fence' };
187
211
  }
188
- // Layer 3 — Bare JSON (last resort, guarded by "findings" keyword)
189
212
  const firstBrace = content.indexOf('{');
190
213
  const lastBrace = content.lastIndexOf('}');
191
214
  if (firstBrace !== -1 && lastBrace > firstBrace && content.includes('"findings"')) {
192
- try {
193
- const parsed = JSON.parse(content.slice(firstBrace, lastBrace + 1));
194
- const result = ShieldStructuredVerdictSchema.safeParse(parsed);
195
- if (result.success)
196
- return result.data;
197
- }
198
- catch {
199
- // All layers exhausted
200
- }
201
- }
202
- return null;
215
+ const verdict = tryCandidate('bare-json', content.slice(firstBrace, lastBrace + 1));
216
+ if (verdict)
217
+ return { ok: true, verdict, layer: 'bare-json' };
218
+ }
219
+ if (attempts.length === 0)
220
+ return { ok: false, cause: 'no-candidate', attempts };
221
+ const cause = attempts.some((attempt) => attempt.cause === 'schema-invalid')
222
+ ? 'schema-invalid'
223
+ : 'invalid-json';
224
+ return { ok: false, cause, attempts };
225
+ }
226
+ /**
227
+ * Compatibility wrapper for callers that only need the parsed verdict.
228
+ * Returns null when all XML, fenced, and bare-JSON layers fail.
229
+ */
230
+ export function extractStructuredVerdict(content) {
231
+ const result = extractStructuredVerdictDetailed(content);
232
+ return result.ok ? result.verdict : null;
203
233
  }
204
234
  /**
205
235
  * Deterministic pass/fail based on findings.
@@ -268,6 +298,17 @@ export function formatVerdictForDisplay(verdict, pass) {
268
298
  * (callers on the pre-#1527 signature) so behavior is preserved.
269
299
  */
270
300
  const LEGACY_REVIEW_SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
301
+ /**
302
+ * Shared tail for every deterministic skip that drops the ENTIRE diff
303
+ * (mmnto-ai/totem#2466). Such a run is a NON-REVIEW, not a pass: no lane ran and
304
+ * nothing was examined, so it must not stamp `.reviewed-content-hash` and must not
305
+ * read as a clean review.
306
+ *
307
+ * Single-sourced deliberately — the three skip sites must never drift into
308
+ * describing the same guarantee three different ways. Voice matches the existing
309
+ * worktree-drift notices, which are the other "NOT stamped" surface.
310
+ */
311
+ const NO_STAMP_NOTICE = 'Nothing was examined, so the reviewed-content-hash was NOT stamped — this run does not authorize a push.';
271
312
  /**
272
313
  * Refresh `<totemDir>/review-extensions.txt` if its contents do not match
273
314
  * the supplied extension set (or the file is missing). Closes the stale-
@@ -396,10 +437,14 @@ export async function writeReviewedContentHashValue(precomputedHash, cwd, totemD
396
437
  * survives commits, amends, and rebases. Only breaks when source files change.
397
438
  *
398
439
  * Now a thin compose of the pure computer + explicit writer (Prop 304 R2): it
399
- * hashes the CURRENT tree and stamps it. Retained at its original signature
400
- * for the trivial fast-path stamps (no-changes / all-non-code / filtered-empty
401
- * none of which open a mid-run LLM window) and `recordShieldOverride`, where
402
- * there is no drift race to guard. The LLM review path does NOT use this — it
440
+ * hashes the CURRENT tree and stamps it. Retained at its original signature for
441
+ * the no-changes stamp (an empty diff opens no mid-run LLM window) and
442
+ * `recordShieldOverride`, where there is no drift race to guard.
443
+ *
444
+ * NO LONGER used by the deterministic skip paths (all-non-code / filtered-empty /
445
+ * all-generated). Those drop the entire diff without examining it, so they are
446
+ * non-reviews and must not stamp — see `NO_STAMP_NOTICE` (mmnto-ai/totem#2466).
447
+ * The LLM review path does NOT use this — it
403
448
  * captures the hash pre-fan and compare-and-stamps in `shieldCommand` /
404
449
  * `handleVerdictResult` so a mid-review edit can never be authorized.
405
450
  */
@@ -1083,11 +1128,14 @@ export async function shieldCommand(options) {
1083
1128
  changedFiles = generated.keptFiles;
1084
1129
  generatedArtifactSummary = buildGeneratedArtifactSection(generated.summaries);
1085
1130
  // All changed files were generated artifacts — nothing left to review.
1086
- // Deterministic PASS: the summary was already surfaced above (never a
1087
- // silent skip). The artifacts' correctness is a gate concern, not the LLM's.
1131
+ // Not sending them to the LLM stays correct (their correctness is a gate
1132
+ // concern, not the LLM's) but that does NOT extend to stamping the push
1133
+ // gate on their behalf: this is the one skip path that can drop a TRACKED,
1134
+ // HASHED source file, because `.gitattributes linguist-generated` can mark
1135
+ // a `.ts` as generated. Stamping here would authorize code no reviewer saw
1136
+ // (mmnto-ai/totem#2466).
1088
1137
  if (!diff.trim()) {
1089
- log.info(DISPLAY_TAG, 'Deterministic fast-path: all changed files are generated artifacts');
1090
- await writeReviewedContentHash(cwd, config.totemDir, configRoot, config.review.sourceExtensions);
1138
+ log.warn(DISPLAY_TAG, `NON-REVIEW: every changed file is a generated artifact, so no lane ran. ${NO_STAMP_NOTICE}`);
1091
1139
  return;
1092
1140
  }
1093
1141
  }
@@ -1095,9 +1143,8 @@ export async function shieldCommand(options) {
1095
1143
  // Stage 1: Classify files — fast-path for non-code-only diffs
1096
1144
  const classification = classifyChangedFiles(changedFiles);
1097
1145
  if (classification.allNonCode) {
1098
- log.info(DISPLAY_TAG, 'Deterministic fast-path: all changed files are non-code');
1146
+ log.warn(DISPLAY_TAG, `NON-REVIEW: every changed file is non-code, so no lane ran. ${NO_STAMP_NOTICE}`);
1099
1147
  log.dim(DISPLAY_TAG, `Skipped: ${changedFiles.join(', ')}`);
1100
- await writeReviewedContentHash(cwd, config.totemDir, configRoot, config.review.sourceExtensions);
1101
1148
  return;
1102
1149
  }
1103
1150
  // Stage 2: Filter diff to code-only files for mixed diffs
@@ -1107,9 +1154,8 @@ export async function shieldCommand(options) {
1107
1154
  filteredDiff = await filterDiffByPatterns(diff, classification.nonCodeFiles);
1108
1155
  filteredFiles = classification.codeFiles;
1109
1156
  if (!filteredDiff.trim()) {
1110
- // After filtering, no code diff remains — fast-path PASS
1111
- log.info(DISPLAY_TAG, 'Deterministic fast-path: no code changes after filtering non-code files');
1112
- await writeReviewedContentHash(cwd, config.totemDir, configRoot, config.review.sourceExtensions);
1157
+ // After filtering non-code files, no code diff remains — nothing was examined.
1158
+ log.warn(DISPLAY_TAG, `NON-REVIEW: no code changes remain after filtering non-code files, so no lane ran. ${NO_STAMP_NOTICE}`);
1113
1159
  return;
1114
1160
  }
1115
1161
  log.dim(DISPLAY_TAG, `Filtered ${classification.nonCodeFiles.length} non-code file(s) from diff`);