instar 1.3.1663 → 1.3.1664

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 (39) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +9 -0
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/SafeGitExecutor.d.ts +33 -11
  5. package/dist/core/SafeGitExecutor.d.ts.map +1 -1
  6. package/dist/core/SafeGitExecutor.js +144 -116
  7. package/dist/core/SafeGitExecutor.js.map +1 -1
  8. package/dist/data/standards-guard-index.json +1 -1
  9. package/dist/data/standards-guard-index.meta.json +2 -2
  10. package/dist/data/standards-registry.meta.json +1 -1
  11. package/dist/native/InstarWedgeCanary.app/Contents/MacOS/InstarWedgeCanary +0 -0
  12. package/dist/native/instar-desktop-host +0 -0
  13. package/dist/recommended/catalog.js +1 -1
  14. package/dist/recommended/catalog.js.map +1 -1
  15. package/dist/recommended/recipes.d.ts.map +1 -1
  16. package/dist/recommended/recipes.js +3 -2
  17. package/dist/recommended/recipes.js.map +1 -1
  18. package/dist/recommended/setupState.d.ts +9 -0
  19. package/dist/recommended/setupState.d.ts.map +1 -1
  20. package/dist/recommended/setupState.js +17 -0
  21. package/dist/recommended/setupState.js.map +1 -1
  22. package/dist/server/AgentServer.d.ts.map +1 -1
  23. package/dist/server/AgentServer.js +5 -0
  24. package/dist/server/AgentServer.js.map +1 -1
  25. package/dist/server/recommendedServiceRoutes.d.ts +2 -0
  26. package/dist/server/recommendedServiceRoutes.d.ts.map +1 -1
  27. package/dist/server/recommendedServiceRoutes.js +41 -7
  28. package/dist/server/recommendedServiceRoutes.js.map +1 -1
  29. package/dist/server/routes.d.ts.map +1 -1
  30. package/dist/server/routes.js +7 -0
  31. package/dist/server/routes.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/data/builtin-manifest.json +47 -47
  34. package/src/data/standards-guard-index.json +1 -1
  35. package/src/data/standards-guard-index.meta.json +2 -2
  36. package/src/data/standards-registry.meta.json +1 -1
  37. package/upgrades/1.3.1664.md +32 -0
  38. package/upgrades/side-effects/recommended-services-stop-holds.md +64 -0
  39. package/upgrades/side-effects/safe-git-one-verb-classifier.md +117 -0
@@ -34,11 +34,19 @@
34
34
  import { type ChildProcess } from 'node:child_process';
35
35
  import { SourceTreeGuardError } from './SourceTreeGuard.js';
36
36
  /**
37
- * Verbs treated as destructive. Closed enumeration; additions are spec changes.
37
+ * How the funnel classes a verb.
38
+ * read — every form is treated as a read.
39
+ * write — every form is treated as a write.
40
+ * shape — the verb has both forms; isReadOnlyShape decides per call.
38
41
  */
42
+ export type GitVerbClass = 'read' | 'write' | 'shape';
43
+ export declare const GIT_VERB_CLASSES: ReadonlyMap<string, GitVerbClass>;
44
+ /** Verbs a write surface can run: `write` and `shape`. Derived from the table. */
39
45
  export declare const DESTRUCTIVE_GIT_VERBS: ReadonlySet<string>;
40
46
  /**
41
- * Read-only verbs explicitly safe to call. Closed enumeration.
47
+ * Verbs a read surface can run: `read` and `shape`. Derived from the table.
48
+ * Membership is the funnel's classification, NOT proof the verb writes nothing:
49
+ * some forms of these verbs can write or run a program (EVO-029 items 4 and 5).
42
50
  */
43
51
  export declare const READONLY_GIT_VERBS: ReadonlySet<string>;
44
52
  /**
@@ -81,6 +89,13 @@ declare function extractVerbAndTargets(args: readonly string[], cwd: string | un
81
89
  * membership).
82
90
  */
83
91
  declare function isReadOnlyShape(verb: string, verbArgs: readonly string[]): boolean | null;
92
+ export type GitInvocationClass = 'read' | 'write' | 'unclassified';
93
+ /**
94
+ * The one answer to "what does the funnel class this invocation as". `read`
95
+ * means the funnel classifies it as a read, not that git will write nothing.
96
+ * A verb outside GIT_VERB_CLASSES is `unclassified`, never read or write.
97
+ */
98
+ export declare function classifyGitInvocation(verb: string, verbArgs: readonly string[]): GitInvocationClass;
84
99
  interface AuditEntry {
85
100
  timestamp: string;
86
101
  executor: 'git' | 'fs';
@@ -234,13 +249,15 @@ export declare class SafeGitExecutor {
234
249
  * Synchronous destructive git execution.
235
250
  *
236
251
  * 1. Extract the verb and ALL target directories from args + opts.cwd.
237
- * 2. Canonicalize each target and call assertNotInstarSourceTree on each.
238
- * 3. Verify the verb is in DESTRUCTIVE_GIT_VERBS (or is an ambiguous verb
239
- * in destructive shape). Read-only verbs throw — callers must use readSync.
240
- * 4. Strip env denylist; inject GIT_CONFIG_GLOBAL/SYSTEM=/dev/null.
241
- * 5. Spawn git via execFileSync; return stdout.
252
+ * 2. Refuse an unclassified verb, before anything else, on every surface.
253
+ * 3. Canonicalize each target and call assertNotInstarSourceTree on each.
254
+ * 4. Verify the invocation classifies as a write. Reads throw — callers must
255
+ * use readSync.
256
+ * 5. Strip env denylist; inject GIT_CONFIG_GLOBAL/SYSTEM=/dev/null.
257
+ * 6. Spawn git via execFileSync; return stdout.
242
258
  *
243
- * Throws SourceTreeGuardError if any target is the instar source tree.
259
+ * Throws SourceTreeGuardError if any target is the instar source tree and the
260
+ * verb is classified.
244
261
  * Throws SafeGitExecutorError on classification mismatch or arg errors.
245
262
  */
246
263
  static execSync(args: readonly string[], opts: SafeGitOptions): string;
@@ -262,9 +279,13 @@ export declare class SafeGitExecutor {
262
279
  */
263
280
  static readSync(args: readonly string[], opts: SafeGitOptions): string;
264
281
  /**
265
- * Verb-aware dispatcher. Routes to readSync for known read-only verbs and to
266
- * execSync otherwise. Use this from generic git() helpers that take dynamic
267
- * args and don't know at the call site whether the verb is destructive.
282
+ * Verb-aware dispatcher. A write goes to execSync and everything else to
283
+ * readSync, which extracts the verb itself and makes the real decision. Use
284
+ * this from generic git() helpers that take dynamic args and don't know at the
285
+ * call site whether the verb is destructive.
286
+ *
287
+ * It reads args[0] as the verb, so a pre-verb flag such as `-c` classifies as
288
+ * unclassified here and lands on readSync (EVO-029 item 1).
268
289
  */
269
290
  static run(args: readonly string[], opts: SafeGitOptions): string;
270
291
  }
@@ -284,6 +305,7 @@ export declare function isLocalVerifyClonePath(target: string, verifyRoot: strin
284
305
  export declare const _internal: {
285
306
  extractVerbAndTargets: typeof extractVerbAndTargets;
286
307
  isReadOnlyShape: typeof isReadOnlyShape;
308
+ GIT_VERB_CLASS_ENTRIES: readonly (readonly [string, GitVerbClass])[];
287
309
  sanitizeEnv: typeof sanitizeEnv;
288
310
  GIT_ENV_DENYLIST: ReadonlySet<string>;
289
311
  repoHasLocalIdentity: typeof repoHasLocalIdentity;
@@ -1 +1 @@
1
- {"version":3,"file":"SafeGitExecutor.d.ts","sourceRoot":"","sources":["../../src/core/SafeGitExecutor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SafeGitExecutor.d.ts","sourceRoot":"","sources":["../../src/core/SafeGitExecutor.ts"],"names":[],"mappings":"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"}
@@ -39,92 +39,96 @@ import path from 'node:path';
39
39
  import { fileURLToPath } from 'node:url';
40
40
  import { withSyncOp } from './InFlightSyncOpMarker.js';
41
41
  import { assertNotInstarSourceTree, SourceTreeGuardError, } from './SourceTreeGuard.js';
42
- // ── Verb classification ─────────────────────────────────────────────
43
42
  /**
44
- * Verbs treated as destructive. Closed enumeration; additions are spec changes.
43
+ * The one verb classification every surface reads. Closed enumeration; an
44
+ * addition is a spec change. A verb that is not listed is unclassified, and no
45
+ * surface will run it. Kept as raw entries so a test can see a duplicate, which
46
+ * the Map built from them would hide.
45
47
  */
46
- export const DESTRUCTIVE_GIT_VERBS = new Set([
47
- 'add',
48
- 'am',
49
- 'apply',
50
- 'branch', // shape-checked: bare `branch <name>` is destructive
51
- 'checkout',
52
- 'cherry-pick',
53
- 'clean',
54
- 'clone',
55
- 'commit',
56
- 'fetch',
57
- 'gc',
58
- 'init',
59
- 'merge',
60
- 'mv',
61
- 'pull',
62
- 'push',
63
- 'rebase',
64
- 'reset',
65
- 'restore',
66
- 'revert',
67
- 'rm',
68
- 'stash',
69
- 'submodule',
70
- 'switch',
71
- 'tag',
72
- 'update-ref',
48
+ const GIT_VERB_CLASS_ENTRIES = [
49
+ // Writes.
50
+ ['add', 'write'],
51
+ ['am', 'write'],
52
+ ['apply', 'write'],
53
+ ['checkout', 'write'],
54
+ ['cherry-pick', 'write'],
55
+ ['clean', 'write'],
56
+ ['clone', 'write'],
57
+ ['commit', 'write'],
58
+ ['fetch', 'write'],
59
+ ['gc', 'write'],
60
+ ['init', 'write'],
61
+ ['merge', 'write'],
62
+ ['mv', 'write'],
63
+ ['pull', 'write'],
64
+ ['push', 'write'],
65
+ ['rebase', 'write'],
66
+ ['reset', 'write'],
67
+ ['restore', 'write'],
68
+ ['revert', 'write'],
69
+ ['rm', 'write'],
70
+ ['submodule', 'write'],
71
+ ['switch', 'write'],
72
+ ['tag', 'write'],
73
+ ['update-ref', 'write'],
73
74
  // Object-database writers (no ref or working-tree change); destructive-classified so
74
75
  // they route through execSync. local-verify's merge-tree/commit-tree path (§2 step 1).
75
- 'merge-tree',
76
- 'commit-tree',
77
- 'worktree', // shape-checked: `worktree list` is read-only
78
- 'prune',
79
- 'notes',
80
- 'replace',
81
- 'filter-branch',
82
- 'remote', // shape-checked: `remote get-url` is read-only
83
- 'config', // shape-checked: `config --get` is read-only
84
- 'format-patch', // shape-checked: --inline is destructive
85
- ]);
76
+ ['merge-tree', 'write'],
77
+ ['commit-tree', 'write'],
78
+ ['prune', 'write'],
79
+ ['notes', 'write'],
80
+ ['replace', 'write'],
81
+ ['filter-branch', 'write'],
82
+ // Reads.
83
+ ['status', 'read'],
84
+ ['log', 'read'],
85
+ ['diff', 'read'],
86
+ ['show', 'read'],
87
+ ['rev-parse', 'read'],
88
+ ['rev-list', 'read'],
89
+ ['ls-files', 'read'],
90
+ ['ls-tree', 'read'],
91
+ ['ls-remote', 'read'],
92
+ ['describe', 'read'],
93
+ ['name-rev', 'read'],
94
+ ['blame', 'read'],
95
+ ['cat-file', 'read'],
96
+ ['cherry', 'read'], // lists +/- patch-equivalence vs an upstream; no mutation
97
+ ['grep', 'read'],
98
+ ['shortlog', 'read'],
99
+ ['count-objects', 'read'],
100
+ ['fsck', 'read'],
101
+ ['var', 'read'],
102
+ ['version', 'read'],
103
+ ['--version', 'read'],
104
+ ['help', 'read'],
105
+ ['write-tree', 'read'],
106
+ ['interpret-trailers', 'read'],
107
+ ['check-ref-format', 'read'],
108
+ ['check-ignore', 'read'], // exit code is the answer: 0 matched, 1 no match, 128 git failed
109
+ ['symbolic-ref', 'read'],
110
+ ['show-ref', 'read'],
111
+ ['for-each-ref', 'read'],
112
+ ['merge-base', 'read'],
113
+ ['hash-object', 'read'],
114
+ // Both forms; isReadOnlyShape decides.
115
+ ['branch', 'shape'], // bare `branch <name>` writes; --list / -l / --show-current / -v read
116
+ ['stash', 'shape'], // `stash list` / `stash show` read
117
+ ['worktree', 'shape'], // `worktree list` reads
118
+ ['remote', 'shape'], // `remote get-url` / `remote show` read
119
+ ['config', 'shape'], // `config --get` reads
120
+ ['format-patch', 'shape'], // --inline writes
121
+ ['reflog', 'shape'], // bare / show / exists read; everything else goes through execSync
122
+ ];
123
+ export const GIT_VERB_CLASSES = new Map(GIT_VERB_CLASS_ENTRIES);
124
+ /** Verbs a write surface can run: `write` and `shape`. Derived from the table. */
125
+ export const DESTRUCTIVE_GIT_VERBS = new Set(GIT_VERB_CLASS_ENTRIES.filter(([, cls]) => cls !== 'read').map(([verb]) => verb));
86
126
  /**
87
- * Read-only verbs explicitly safe to call. Closed enumeration.
127
+ * Verbs a read surface can run: `read` and `shape`. Derived from the table.
128
+ * Membership is the funnel's classification, NOT proof the verb writes nothing:
129
+ * some forms of these verbs can write or run a program (EVO-029 items 4 and 5).
88
130
  */
89
- export const READONLY_GIT_VERBS = new Set([
90
- 'status',
91
- 'log',
92
- 'diff',
93
- 'show',
94
- 'rev-parse',
95
- 'rev-list',
96
- 'ls-files',
97
- 'ls-tree',
98
- 'ls-remote',
99
- 'describe',
100
- 'name-rev',
101
- 'blame',
102
- 'cat-file',
103
- 'cherry', // read-only: lists +/- patch-equivalence vs an upstream; no mutation
104
- 'grep',
105
- 'shortlog',
106
- 'count-objects',
107
- 'fsck',
108
- 'var',
109
- 'version',
110
- '--version',
111
- 'help',
112
- 'write-tree',
113
- 'interpret-trailers',
114
- 'check-ref-format',
115
- 'symbolic-ref',
116
- 'show-ref',
117
- 'for-each-ref',
118
- 'merge-base',
119
- 'reflog', // read-only by default; reflog expire/delete is destructive (caller must use execSync)
120
- 'hash-object',
121
- 'config', // overlap with destructive set; readSync shape-check enforces --get only
122
- 'remote', // overlap; readSync shape-check enforces list/get-url only
123
- 'branch', // overlap; readSync shape-check enforces --list / -l / --show-current / -v
124
- 'worktree', // overlap; readSync shape-check enforces list only
125
- 'format-patch', // overlap; readSync shape-check rejects --inline
126
- 'stash', // shape-check: `stash list` / `stash show` allowed
127
- ]);
131
+ export const READONLY_GIT_VERBS = new Set(GIT_VERB_CLASS_ENTRIES.filter(([, cls]) => cls !== 'write').map(([verb]) => verb));
128
132
  /**
129
133
  * Verbs that are "data-pull": they touch the object database and FETCH_HEAD
130
134
  * but DO NOT modify the working tree, committed refs (heads/tags/remotes),
@@ -156,6 +160,7 @@ export const SOURCE_TREE_READ_TIER_VERBS = new Set([
156
160
  'status',
157
161
  'cherry',
158
162
  'ls-files', // reaper checks ignored files before allowing worktree removal
163
+ 'check-ignore', // reads ignore rules; a caller may need the answer on the source tree itself
159
164
  ]);
160
165
  // ── Env denylist ────────────────────────────────────────────────────
161
166
  const GIT_ENV_DENYLIST = new Set([
@@ -657,6 +662,19 @@ function isReadOnlyShape(verb, verbArgs) {
657
662
  return null;
658
663
  }
659
664
  }
665
+ /**
666
+ * The one answer to "what does the funnel class this invocation as". `read`
667
+ * means the funnel classifies it as a read, not that git will write nothing.
668
+ * A verb outside GIT_VERB_CLASSES is `unclassified`, never read or write.
669
+ */
670
+ export function classifyGitInvocation(verb, verbArgs) {
671
+ const cls = GIT_VERB_CLASSES.get(verb);
672
+ if (cls === undefined)
673
+ return 'unclassified';
674
+ if (cls === 'shape')
675
+ return isReadOnlyShape(verb, verbArgs) === false ? 'write' : 'read';
676
+ return cls;
677
+ }
660
678
  /** Return the verbArgs (slice after the verb), accounting for pre-verb flags. */
661
679
  function sliceAfterVerb(args, verb) {
662
680
  const idx = args.indexOf(verb);
@@ -924,19 +942,23 @@ export class SafeGitExecutor {
924
942
  * Synchronous destructive git execution.
925
943
  *
926
944
  * 1. Extract the verb and ALL target directories from args + opts.cwd.
927
- * 2. Canonicalize each target and call assertNotInstarSourceTree on each.
928
- * 3. Verify the verb is in DESTRUCTIVE_GIT_VERBS (or is an ambiguous verb
929
- * in destructive shape). Read-only verbs throw — callers must use readSync.
930
- * 4. Strip env denylist; inject GIT_CONFIG_GLOBAL/SYSTEM=/dev/null.
931
- * 5. Spawn git via execFileSync; return stdout.
945
+ * 2. Refuse an unclassified verb, before anything else, on every surface.
946
+ * 3. Canonicalize each target and call assertNotInstarSourceTree on each.
947
+ * 4. Verify the invocation classifies as a write. Reads throw — callers must
948
+ * use readSync.
949
+ * 5. Strip env denylist; inject GIT_CONFIG_GLOBAL/SYSTEM=/dev/null.
950
+ * 6. Spawn git via execFileSync; return stdout.
932
951
  *
933
- * Throws SourceTreeGuardError if any target is the instar source tree.
952
+ * Throws SourceTreeGuardError if any target is the instar source tree and the
953
+ * verb is classified.
934
954
  * Throws SafeGitExecutorError on classification mismatch or arg errors.
935
955
  */
936
956
  static execSync(args, opts) {
937
957
  const { verb, targets: rawTargets } = extractVerbAndTargets(args, opts.cwd);
938
958
  const targets = withLocalVerifyTargets(rawTargets, opts);
939
959
  const verbArgs = sliceAfterVerb(args, verb);
960
+ const invocation = classifyGitInvocation(verb, verbArgs);
961
+ refuseUnclassified(invocation, verb, opts, targets);
940
962
  // Run source-tree assertion against every target — unless the caller
941
963
  // explicitly opted into the narrow data-pull allowlist (fetch / etc.)
942
964
  // for the documented LAYER B + LAYER C canonical-ref read path. See
@@ -948,14 +970,14 @@ export class SafeGitExecutor {
948
970
  audit('git', opts.operation, verb, targets[0], 'allowed', 'sourceTree-bypass');
949
971
  }
950
972
  // Verb classification.
951
- const ambiguousReadOnly = isReadOnlyShape(verb, verbArgs);
952
- if (ambiguousReadOnly === true) {
973
+ const shapeChecked = GIT_VERB_CLASSES.get(verb) === 'shape';
974
+ if (invocation === 'read' && shapeChecked) {
953
975
  // The verb is ambiguous and the shape is read-only — caller used the
954
976
  // wrong method. Fail loud rather than silently allow.
955
977
  audit('git', opts.operation, verb, targets[0], 'denied', 'read-only-shape-via-execSync');
956
978
  throw new SafeGitExecutorError(`SafeGitExecutor.execSync called with read-only shape '${verb} ${verbArgs.join(' ')}' — use SafeGitExecutor.readSync instead.`);
957
979
  }
958
- if (ambiguousReadOnly === null && !DESTRUCTIVE_GIT_VERBS.has(verb)) {
980
+ if (invocation === 'read') {
959
981
  // Pure read-only verb routed through execSync.
960
982
  audit('git', opts.operation, verb, targets[0], 'denied', 'readonly-verb-via-execSync');
961
983
  throw new SafeGitExecutorError(`SafeGitExecutor.execSync called with read-only verb '${verb}' — use SafeGitExecutor.readSync instead.`);
@@ -987,18 +1009,20 @@ export class SafeGitExecutor {
987
1009
  const { verb, targets: rawTargets } = extractVerbAndTargets(args, opts.cwd);
988
1010
  const targets = withLocalVerifyTargets(rawTargets, opts);
989
1011
  const verbArgs = sliceAfterVerb(args, verb);
1012
+ const invocation = classifyGitInvocation(verb, verbArgs);
1013
+ refuseUnclassified(invocation, verb, opts, targets);
990
1014
  if (!isSourceTreeCheckBypassed(verb, verbArgs, opts, targets)) {
991
1015
  runSourceTreeChecks(targets, opts.operation, 'git', verb);
992
1016
  }
993
1017
  else {
994
1018
  audit('git', opts.operation, verb, targets[0], 'allowed', 'sourceTree-bypass');
995
1019
  }
996
- const ambiguousReadOnly = isReadOnlyShape(verb, verbArgs);
997
- if (ambiguousReadOnly === true) {
1020
+ const shapeChecked = GIT_VERB_CLASSES.get(verb) === 'shape';
1021
+ if (invocation === 'read' && shapeChecked) {
998
1022
  audit('git', opts.operation, verb, targets[0], 'denied', 'read-only-shape-via-spawn');
999
1023
  throw new SafeGitExecutorError(`SafeGitExecutor.spawn called with read-only shape '${verb}' — use SafeGitExecutor.readSync instead.`);
1000
1024
  }
1001
- if (ambiguousReadOnly === null && !DESTRUCTIVE_GIT_VERBS.has(verb)) {
1025
+ if (invocation === 'read') {
1002
1026
  audit('git', opts.operation, verb, targets[0], 'denied', 'readonly-verb-via-spawn');
1003
1027
  throw new SafeGitExecutorError(`SafeGitExecutor.spawn called with read-only verb '${verb}' — use SafeGitExecutor.readSync instead.`);
1004
1028
  }
@@ -1020,12 +1044,14 @@ export class SafeGitExecutor {
1020
1044
  const { verb, targets: rawTargets } = extractVerbAndTargets(args, opts.cwd);
1021
1045
  const targets = withLocalVerifyTargets(rawTargets, opts);
1022
1046
  const verbArgs = sliceAfterVerb(args, verb);
1023
- const ambiguousReadOnly = isReadOnlyShape(verb, verbArgs);
1024
- if (ambiguousReadOnly === false) {
1047
+ const invocation = classifyGitInvocation(verb, verbArgs);
1048
+ refuseUnclassified(invocation, verb, opts, targets);
1049
+ const shapeChecked = GIT_VERB_CLASSES.get(verb) === 'shape';
1050
+ if (invocation === 'write' && shapeChecked) {
1025
1051
  audit('git', opts.operation, verb, targets[0], 'denied', 'destructive-shape-via-readStream');
1026
1052
  throw new SafeGitExecutorError(`SafeGitExecutor.readStream called with destructive shape '${verb} ${verbArgs.join(' ')}' — use SafeGitExecutor.spawn instead.`);
1027
1053
  }
1028
- if (ambiguousReadOnly === null && !READONLY_GIT_VERBS.has(verb)) {
1054
+ if (invocation === 'write') {
1029
1055
  audit('git', opts.operation, verb, targets[0], 'denied', 'destructive-verb-via-readStream');
1030
1056
  throw new SafeGitExecutorError(`SafeGitExecutor.readStream called with destructive verb '${verb}' — use SafeGitExecutor.spawn instead.`);
1031
1057
  }
@@ -1058,12 +1084,14 @@ export class SafeGitExecutor {
1058
1084
  // misusing this method get a clear "use execSync" error rather than
1059
1085
  // a guard error first.
1060
1086
  const verbArgs = sliceAfterVerb(args, verb);
1061
- const ambiguousReadOnly = isReadOnlyShape(verb, verbArgs);
1062
- if (ambiguousReadOnly === false) {
1087
+ const invocation = classifyGitInvocation(verb, verbArgs);
1088
+ refuseUnclassified(invocation, verb, opts, targets);
1089
+ const shapeChecked = GIT_VERB_CLASSES.get(verb) === 'shape';
1090
+ if (invocation === 'write' && shapeChecked) {
1063
1091
  audit('git', opts.operation, verb, targets[0], 'denied', 'destructive-shape-via-readSync');
1064
1092
  throw new SafeGitExecutorError(`SafeGitExecutor.readSync called with destructive shape '${verb} ${verbArgs.join(' ')}' — use SafeGitExecutor.execSync instead.`);
1065
1093
  }
1066
- if (ambiguousReadOnly === null && !READONLY_GIT_VERBS.has(verb)) {
1094
+ if (invocation === 'write') {
1067
1095
  audit('git', opts.operation, verb, targets[0], 'denied', 'destructive-verb-via-readSync');
1068
1096
  throw new SafeGitExecutorError(`SafeGitExecutor.readSync called with destructive verb '${verb}' — use SafeGitExecutor.execSync instead.`);
1069
1097
  }
@@ -1098,30 +1126,29 @@ export class SafeGitExecutor {
1098
1126
  return stdout || '';
1099
1127
  }
1100
1128
  /**
1101
- * Verb-aware dispatcher. Routes to readSync for known read-only verbs and to
1102
- * execSync otherwise. Use this from generic git() helpers that take dynamic
1103
- * args and don't know at the call site whether the verb is destructive.
1129
+ * Verb-aware dispatcher. A write goes to execSync and everything else to
1130
+ * readSync, which extracts the verb itself and makes the real decision. Use
1131
+ * this from generic git() helpers that take dynamic args and don't know at the
1132
+ * call site whether the verb is destructive.
1133
+ *
1134
+ * It reads args[0] as the verb, so a pre-verb flag such as `-c` classifies as
1135
+ * unclassified here and lands on readSync (EVO-029 item 1).
1104
1136
  */
1105
1137
  static run(args, opts) {
1106
1138
  const verb = args[0];
1107
- if (verb && READONLY_GIT_VERBS.has(verb)) {
1108
- const verbArgs = args.slice(1);
1109
- const shape = isReadOnlyShape(verb, verbArgs);
1110
- if (shape !== false)
1111
- return SafeGitExecutor.readSync(args, opts);
1112
- }
1113
- if (verb && !DESTRUCTIVE_GIT_VERBS.has(verb)) {
1114
- return SafeGitExecutor.readSync(args, opts);
1115
- }
1116
- if (verb) {
1117
- const verbArgs = args.slice(1);
1118
- const shape = isReadOnlyShape(verb, verbArgs);
1119
- if (shape === true)
1120
- return SafeGitExecutor.readSync(args, opts);
1139
+ if (verb && classifyGitInvocation(verb, args.slice(1)) === 'write') {
1140
+ return SafeGitExecutor.execSync(args, opts);
1121
1141
  }
1122
- return SafeGitExecutor.execSync(args, opts);
1142
+ return SafeGitExecutor.readSync(args, opts);
1123
1143
  }
1124
1144
  }
1145
+ /** The one refusal for a verb outside GIT_VERB_CLASSES. Every surface calls it first. */
1146
+ function refuseUnclassified(invocation, verb, opts, targets) {
1147
+ if (invocation !== 'unclassified')
1148
+ return;
1149
+ audit('git', opts.operation, verb, targets[0], 'denied', 'unclassified-verb');
1150
+ throw new SafeGitExecutorError(`SafeGitExecutor: git verb '${verb}' is not classified. It is not in GIT_VERB_CLASSES, so no surface will run it. Adding a verb is a reviewed change to that table.`);
1151
+ }
1125
1152
  function isSourceTreeCheckBypassed(verb, verbArgs, opts, targets = []) {
1126
1153
  if (opts.sourceTreeReadOk && SOURCE_TREE_READ_TIER_VERBS.has(verb))
1127
1154
  return true;
@@ -1372,6 +1399,7 @@ function audit(executor, operation, verb, target, outcome, reason) {
1372
1399
  export const _internal = {
1373
1400
  extractVerbAndTargets,
1374
1401
  isReadOnlyShape,
1402
+ GIT_VERB_CLASS_ENTRIES,
1375
1403
  sanitizeEnv,
1376
1404
  GIT_ENV_DENYLIST,
1377
1405
  repoHasLocalIdentity,