@goodandready/dsh-approval-gate 0.1.11 → 0.1.13

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.
package/lib/messages.js CHANGED
@@ -32,12 +32,18 @@ export const MESSAGES = Object.freeze({
32
32
  askShellStdin: "Shell input is not fully available to the guard; review it before allowing",
33
33
  askInterpreterPayload: "Interpreter side effects are not fully analyzed; review it before allowing",
34
34
  askCredentialInArgv: "Expanded authorization values become process arguments; use a credential-safe API helper or review before allowing",
35
+ askGitRemotePush: "Publishing commits to a remote repository via git push requires confirmation",
36
+ askSilentErrorMasked: "The command suppresses errors or masks non-zero exit codes; review before allowing",
35
37
  ruleUnknown: "A blocking safety rule matched",
36
38
  ruleRecursiveRm: "Recursive file deletion",
37
39
  ruleProcessSignal: "Process termination",
38
40
  ruleServiceControl: "Service state change",
39
41
  ruleGitClean: "Destructive git clean",
40
42
  ruleGitResetHard: "Destructive git reset --hard",
43
+ ruleGitForcePush: "Forceful git push",
44
+ ruleGitBranchForceDelete: "Forceful git branch deletion",
45
+ ruleGitCheckoutForce: "Forceful checkout discarding local modifications",
46
+ ruleGitRestoreWorktree: "Restoring working tree files discarding modifications",
41
47
  ruleFindDelete: "find -delete",
42
48
  ruleDestructiveSql: "Destructive SQL",
43
49
  ruleSecretWrite: "Write to a protected secret file",
@@ -51,6 +57,9 @@ export const MESSAGES = Object.freeze({
51
57
  rulePathTraversal: "Directory traversal attack detected",
52
58
  ruleBandHardDeny: "Command matched Band 0 critical security blocklist",
53
59
  ruleCircuitBreakerTripped: "Circuit breaker tripped due to repeated security denials",
60
+ ruleSilentErrorMasked: "Suppression of command failure or error stream",
61
+ ruleChmodChown: "Permission or ownership mutation",
62
+ ruleReviewerReadOnly: "Reviewer agent is strictly isolated in read-only mode",
54
63
  hintRecursiveRm: "Specify exact target files instead of recursive deletion",
55
64
  hintPathTraversal: "Keep operations strictly within the project directory",
56
65
  hintSecretWrite: "Modify configuration via settings or environment variables, not raw file overwrite",
@@ -58,6 +67,11 @@ export const MESSAGES = Object.freeze({
58
67
  hintCircuitBreakerTripped: "Request manual operator approval before attempting new system commands",
59
68
  hintDestructiveSql: "Use migrations or execute non-destructive database operations",
60
69
  hintCurlPipe: "Download the script, inspect its content, and execute locally",
70
+ hintGitForcePush: "Push branch without force or check with remote administrator",
71
+ hintGitRemotePush: "Confirm remote target branch and repository before pushing",
72
+ hintSilentErrorMasked: "Allow standard error output to be captured and handle non-zero exit codes explicitly",
73
+ hintChmodChown: "Avoid modifying file permissions or system directory ownership directly",
74
+ hintReviewerReadOnly: "Reviewer agents may only read repository files, not run commands or modify files",
61
75
  },
62
76
  zh: {
63
77
  blockedPrefix: "dsh-approval-gate 规则已阻止",
@@ -89,12 +103,18 @@ export const MESSAGES = Object.freeze({
89
103
  askShellStdin: "卫兵无法完整获取 Shell 输入;请在允许前进行审查",
90
104
  askInterpreterPayload: "解释器副作用未完全分析;请在允许前进行审查",
91
105
  askCredentialInArgv: "展开后的授权凭据作为进程参数传递;请使用凭据安全的 API 辅助工具,或在允许前进行审查",
106
+ askGitRemotePush: "通过 git push 向远程仓库发布代码需要确认",
107
+ askSilentErrorMasked: "命令屏蔽了错误输出或强制将退出码设为 0;请在允许前审查",
92
108
  ruleUnknown: "命中阻止性安全规则",
93
109
  ruleRecursiveRm: "递归删除文件",
94
110
  ruleProcessSignal: "终止进程",
95
111
  ruleServiceControl: "更改服务状态",
96
112
  ruleGitClean: "破坏性 git clean",
97
113
  ruleGitResetHard: "破坏性 git reset --hard",
114
+ ruleGitForcePush: "强制 git push",
115
+ ruleGitBranchForceDelete: "强制删除 git 分支",
116
+ ruleGitCheckoutForce: "强制 checkout 丢弃本地修改",
117
+ ruleGitRestoreWorktree: "还原工作区文件并丢弃修改",
98
118
  ruleFindDelete: "find -delete",
99
119
  ruleDestructiveSql: "破坏性 SQL",
100
120
  ruleSecretWrite: "写入受保护的敏感文件",
@@ -108,6 +128,9 @@ export const MESSAGES = Object.freeze({
108
128
  rulePathTraversal: "检测到路径穿越攻击",
109
129
  ruleBandHardDeny: "命令匹配 Band 0 关键安全黑名单",
110
130
  ruleCircuitBreakerTripped: "因连续多次安全拒绝,断路熔断器已触发",
131
+ ruleSilentErrorMasked: "屏蔽错误输出或强制忽略退出码",
132
+ ruleChmodChown: "修改权限或所有权",
133
+ ruleReviewerReadOnly: "审查者代理处于只读隔离模式",
111
134
  hintRecursiveRm: "请指定具体目标文件,避免递归删除",
112
135
  hintPathTraversal: "请将操作限制在项目根目录内",
113
136
  hintSecretWrite: "请通过设置或环境变量修改配置,不要直接覆盖敏感配置文件",
@@ -115,63 +138,11 @@ export const MESSAGES = Object.freeze({
115
138
  hintCircuitBreakerTripped: "请在尝试新的系统命令前请求操作员人工授权",
116
139
  hintDestructiveSql: "请使用版本化数据库迁移,或执行非破坏性查询",
117
140
  hintCurlPipe: "先将脚本下载到本地检查内容,再安全执行",
118
- },
119
- ru: {
120
- blockedPrefix: "Заблокировано правилом dsh-approval-gate",
121
- detectedLabel: "Обнаружено",
122
- commandLabel: "Команда",
123
- parseReasonLabel: "Причина разбора",
124
- hintPrefix: "Подсказка",
125
- detailProcessSubstitution: "подстановка процесса требует подтверждения",
126
- detailUnclosedProcessSubstitution: "незакрытая подстановка процесса",
127
- detailUnclosedCommandSubstitution: "незакрытая подстановка команды",
128
- detailUnclosedBacktick: "незакрытая подстановка в обратных кавычках",
129
- detailUnclosedParameterExpansion: "незакрытое расширение параметра",
130
- detailUnclosedSingleQuote: "незакрытая одинарная кавычка",
131
- detailUnclosedDoubleQuote: "незакрытая двойная кавычка",
132
- detailDanglingEscape: "незавершённое экранирование",
133
- detailInvalidHeredoc: "неверный разделитель here-document",
134
- detailUnterminatedHeredoc: "ограничитель here-document не найден",
135
- detailUnsupportedGrouping: "группировка shell не поддерживается анализатором",
136
- detailUnsupported: "неподдерживаемый синтаксис shell",
137
- approvalRequired: "Требуется подтверждение",
138
- askUnknown: "Команда не может быть проверена полностью; проверьте её перед подтверждением",
139
- askUnparseable: "Синтаксис shell не может быть проверен полностью; проверьте его перед подтверждением",
140
- askUnknownFormat: "Формат вызова инструмента не может быть проверен; проверьте его перед подтверждением",
141
- askUnknownWriteTarget: "Цель записи файла не может быть определена; проверьте её перед подтверждением",
142
- askDynamicCommand: "Развёрнутое имя команды не может быть проверено; проверьте его перед подтверждением",
143
- askDynamicArguments: "Развёрнутые аргументы чувствительной команды не могут быть проверены; проверьте их перед подтверждением",
144
- askDynamicRedirect: "Развёрнутая цель перенаправления не может быть проверена; проверьте её перед подтверждением",
145
- askScriptFile: "Содержимое файла скрипта недоступно для гейта; проверьте его перед подтверждением",
146
- askShellStdin: "Ввод шелла недоступен для гейта в полном объёме; проверьте его перед подтверждением",
147
- askInterpreterPayload: "Побочные эффекты интерпретатора не могут быть полностью проанализированы; проверьте их перед подтверждением",
148
- askCredentialInArgv: "Развёрнутые учётные данные передаются как аргументы процесса; используйте безопасный API или проверьте перед подтверждением",
149
- ruleUnknown: "Сработало защитное правило блокировки",
150
- ruleRecursiveRm: "Рекурсивное удаление файлов",
151
- ruleProcessSignal: "Завершение процесса",
152
- ruleServiceControl: "Изменение состояния службы",
153
- ruleGitClean: "Разрушительный git clean",
154
- ruleGitResetHard: "Разрушительный git reset --hard",
155
- ruleFindDelete: "find -delete",
156
- ruleDestructiveSql: "Разрушительный SQL",
157
- ruleSecretWrite: "Запись в защищённый файл с секретами",
158
- ruleDeviceWrite: "Запись в системное устройство или защищённый файл",
159
- ruleMkfsDevice: "Форматирование файловой системы",
160
- ruleCurlPipe: "Передача скачанного содержимого в шелл",
161
- ruleEval: "Динамическое выполнение шелла (eval)",
162
- ruleNestedShell: "Превышен лимит вложенности шелла",
163
- ruleInterpreterPayload: "Небезопасный исполняемый код интерпретатора",
164
- ruleEnvDumpLeak: "Попытка утечки переменных окружения и секретов",
165
- rulePathTraversal: "Обнаружена попытка обхода каталогов (path traversal)",
166
- ruleBandHardDeny: "Команда заблокирована критическим списком безопасности Band 0",
167
- ruleCircuitBreakerTripped: "Предохранитель сессии сработал из-за повторяющихся блокировок",
168
- hintRecursiveRm: "Указывайте конкретные файлы вместо рекурсивного удаления",
169
- hintPathTraversal: "Ограничьте операции пределами рабочего каталога проекта",
170
- hintSecretWrite: "Изменяйте настройки через UI или переменные окружения, не перезаписывайте секретные файлы напрямую",
171
- hintEnvDumpLeak: "Запрашивайте конкретные несекретные переменные по отдельности",
172
- hintCircuitBreakerTripped: "Запросите подтверждение оператора перед повторной отправкой системных команд",
173
- hintDestructiveSql: "Используйте версионированные миграции или выполняйте неразрушающие запросы",
174
- hintCurlPipe: "Скачайте скрипт локально, проверьте его содержимое и выполните безопасно",
141
+ hintGitForcePush: "请勿使用 force 参数推送分支,或与远程仓库管理员确认",
142
+ hintGitRemotePush: "推送前请核对目标分支与远程仓库地址",
143
+ hintSilentErrorMasked: "请保留标准错误输出,并明确处理非零退出码",
144
+ hintChmodChown: "避免直接修改系统目录所有权或文件权限",
145
+ hintReviewerReadOnly: "审查者代理仅可读取项目文件,禁止执行命令或修改文件",
175
146
  },
176
147
  });
177
148
 
@@ -211,6 +182,10 @@ const RULE_KEYS = Object.freeze({
211
182
  "service-control": "ruleServiceControl",
212
183
  "git-clean": "ruleGitClean",
213
184
  "git-reset-hard": "ruleGitResetHard",
185
+ "git-force-push": "ruleGitForcePush",
186
+ "git-branch-force-delete": "ruleGitBranchForceDelete",
187
+ "git-checkout-force": "ruleGitCheckoutForce",
188
+ "git-restore-worktree": "ruleGitRestoreWorktree",
214
189
  "find-delete": "ruleFindDelete",
215
190
  "destructive-sql": "ruleDestructiveSql",
216
191
  "secret-write": "ruleSecretWrite",
@@ -224,6 +199,9 @@ const RULE_KEYS = Object.freeze({
224
199
  "path-traversal": "rulePathTraversal",
225
200
  "band-hard-deny": "ruleBandHardDeny",
226
201
  "circuit-breaker-tripped": "ruleCircuitBreakerTripped",
202
+ "silent-error-masked": "ruleSilentErrorMasked",
203
+ "chmod-chown": "ruleChmodChown",
204
+ "reviewer-read-only": "ruleReviewerReadOnly",
227
205
  });
228
206
 
229
207
  const HINT_KEYS = Object.freeze({
@@ -234,6 +212,11 @@ const HINT_KEYS = Object.freeze({
234
212
  "circuit-breaker-tripped": "hintCircuitBreakerTripped",
235
213
  "destructive-sql": "hintDestructiveSql",
236
214
  "curl-pipe": "hintCurlPipe",
215
+ "git-force-push": "hintGitForcePush",
216
+ "git-remote-push": "hintGitRemotePush",
217
+ "silent-error-masked": "hintSilentErrorMasked",
218
+ "chmod-chown": "hintChmodChown",
219
+ "reviewer-read-only": "hintReviewerReadOnly",
237
220
  });
238
221
 
239
222
  const ASK_KEYS = Object.freeze({
@@ -247,6 +230,8 @@ const ASK_KEYS = Object.freeze({
247
230
  "shell-stdin": "askShellStdin",
248
231
  "interpreter-payload": "askInterpreterPayload",
249
232
  "credential-in-argv": "askCredentialInArgv",
233
+ "git-remote-push": "askGitRemotePush",
234
+ "silent-error-masked": "askSilentErrorMasked",
250
235
  });
251
236
 
252
237
  const DETAIL_KEYS = Object.freeze({
@@ -288,4 +273,4 @@ export function askMessage(hit, who, translate, logger) {
288
273
  if (snippet) parts.push(translated(translate, "commandLabel", logger) + ": " + snippet);
289
274
  if (who) parts.push(who.trim());
290
275
  return parts.join(" — ") + ".";
291
- }
276
+ }
@@ -0,0 +1,321 @@
1
+ // lib/parsers/git.js
2
+ // Specialized Git AST and semantics parser with classification of read, local mutation,
3
+ // remote operations, and destructive commands.
4
+ // Resolves: #125, #103
5
+
6
+ export const GIT_CATEGORY_READ = "read";
7
+ export const GIT_CATEGORY_MUTATE_LOCAL = "mutate_local";
8
+ export const GIT_CATEGORY_REMOTE = "remote";
9
+ export const GIT_CATEGORY_DESTRUCTIVE = "destructive";
10
+
11
+ const GLOBAL_FLAGS_WITH_VALUE = new Set([
12
+ "-C",
13
+ "-c",
14
+ "--git-dir",
15
+ "--work-tree",
16
+ "--namespace",
17
+ "--exec-path",
18
+ "--super-prefix",
19
+ "--config-env",
20
+ ]);
21
+
22
+ const READ_ONLY_VERBS = new Set([
23
+ "status",
24
+ "log",
25
+ "diff",
26
+ "show",
27
+ "rev-parse",
28
+ "describe",
29
+ "cat-file",
30
+ "ls-files",
31
+ "ls-tree",
32
+ "shortlog",
33
+ "blame",
34
+ "check-ignore",
35
+ "check-ref-format",
36
+ "var",
37
+ "version",
38
+ "help",
39
+ "symbolic-ref",
40
+ "name-rev",
41
+ "merge-base",
42
+ "count-objects",
43
+ "diff-index",
44
+ "diff-tree",
45
+ "for-each-ref",
46
+ "rev-list",
47
+ ]);
48
+
49
+ const LOCAL_MUTATE_VERBS = new Set([
50
+ "add",
51
+ "commit",
52
+ "checkout",
53
+ "switch",
54
+ "branch",
55
+ "tag",
56
+ "stash",
57
+ "merge",
58
+ "rebase",
59
+ "cherry-pick",
60
+ "revert",
61
+ "mv",
62
+ "rm",
63
+ "init",
64
+ "apply",
65
+ "am",
66
+ ]);
67
+
68
+ const REMOTE_VERBS = new Set([
69
+ "push",
70
+ "pull",
71
+ "fetch",
72
+ "clone",
73
+ "remote",
74
+ "submodule",
75
+ ]);
76
+
77
+ /**
78
+ * Extracts global git options and isolates the git subcommand verb and its arguments.
79
+ *
80
+ * @param {string[]} argv - Array of tokens starting with 'git' or the first flag/verb.
81
+ * @returns {{ verb: string, args: string[], globalOptions: Record<string, string>, rawArgv: string[] }}
82
+ */
83
+ export function parseGitArgv(argv) {
84
+ if (!Array.isArray(argv) || argv.length === 0) {
85
+ return { verb: "", args: [], globalOptions: {}, rawArgv: [] };
86
+ }
87
+
88
+ const rawArgv = [...argv];
89
+ let idx = rawArgv[0] === "git" ? 1 : 0;
90
+ const globalOptions = {};
91
+
92
+ while (idx < rawArgv.length && rawArgv[idx].startsWith("-")) {
93
+ const flag = rawArgv[idx];
94
+ if (flag === "--") {
95
+ idx += 1;
96
+ break;
97
+ }
98
+ if (GLOBAL_FLAGS_WITH_VALUE.has(flag)) {
99
+ globalOptions[flag] = rawArgv[idx + 1] || "";
100
+ idx += 2;
101
+ } else if (flag.startsWith("--") && flag.includes("=")) {
102
+ const eqIdx = flag.indexOf("=");
103
+ globalOptions[flag.slice(0, eqIdx)] = flag.slice(eqIdx + 1);
104
+ idx += 1;
105
+ } else if (flag.length > 2 && flag[1] !== "-" && flag.startsWith("-C")) {
106
+ globalOptions["-C"] = flag.slice(2);
107
+ idx += 1;
108
+ } else {
109
+ globalOptions[flag] = "true";
110
+ idx += 1;
111
+ }
112
+ }
113
+
114
+ const verb = rawArgv[idx] || "";
115
+ const args = rawArgv.slice(idx + 1);
116
+
117
+ return { verb, args, globalOptions, rawArgv };
118
+ }
119
+
120
+ /**
121
+ * Checks whether an argument contains a specific short flag (e.g. 'f' in '-fdx').
122
+ *
123
+ * @param {string[]} args
124
+ * @param {string} flagChar
125
+ * @returns {boolean}
126
+ */
127
+ function hasShortFlag(args, flagChar) {
128
+ for (const a of args) {
129
+ if (a === "--") break;
130
+ if (a.length > 1 && a[0] === "-" && a[1] !== "-" && a.includes(flagChar)) {
131
+ return true;
132
+ }
133
+ }
134
+ return false;
135
+ }
136
+
137
+ /**
138
+ * Analyzes and classifies a git subcommand and its arguments.
139
+ *
140
+ * @param {string} verb
141
+ * @param {string[]} args
142
+ * @returns {{
143
+ * verb: string,
144
+ * category: "read" | "mutate_local" | "remote" | "destructive",
145
+ * isDestructive: boolean,
146
+ * isRemote: boolean,
147
+ * isForce: boolean,
148
+ * reason?: string,
149
+ * detail?: string
150
+ * }}
151
+ */
152
+ export function classifyGitOperation(verb, args = []) {
153
+ if (!verb) {
154
+ return {
155
+ verb: "",
156
+ category: GIT_CATEGORY_READ,
157
+ isDestructive: false,
158
+ isRemote: false,
159
+ isForce: false,
160
+ };
161
+ }
162
+
163
+ const subArgs = Array.isArray(args) ? args : [];
164
+
165
+ // 1. Destructive operations check
166
+ if (verb === "reset" && subArgs.includes("--hard")) {
167
+ return {
168
+ verb,
169
+ category: GIT_CATEGORY_DESTRUCTIVE,
170
+ isDestructive: true,
171
+ isRemote: false,
172
+ isForce: true,
173
+ reason: "git-reset-hard",
174
+ detail: "Destructive git reset --hard discards all uncommitted modifications",
175
+ };
176
+ }
177
+
178
+ if (verb === "clean" && (subArgs.includes("-f") || subArgs.includes("--force") || hasShortFlag(subArgs, "f"))) {
179
+ return {
180
+ verb,
181
+ category: GIT_CATEGORY_DESTRUCTIVE,
182
+ isDestructive: true,
183
+ isRemote: false,
184
+ isForce: true,
185
+ reason: "git-clean",
186
+ detail: "Destructive git clean removes untracked files irreversibly",
187
+ };
188
+ }
189
+
190
+ if (verb === "branch" && (subArgs.includes("-D") || (subArgs.includes("-d") && (subArgs.includes("-f") || subArgs.includes("--force"))))) {
191
+ return {
192
+ verb,
193
+ category: GIT_CATEGORY_DESTRUCTIVE,
194
+ isDestructive: true,
195
+ isRemote: false,
196
+ isForce: true,
197
+ reason: "git-branch-force-delete",
198
+ detail: "Forceful git branch deletion (-D) risks losing unmerged commits",
199
+ };
200
+ }
201
+
202
+ // 2. Remote operations (#103: git push vs local git operations)
203
+ if (verb === "push") {
204
+ const isForce = subArgs.some((a) => (
205
+ a === "--force" ||
206
+ a === "-f" ||
207
+ a.startsWith("--force-with-lease") ||
208
+ a === "--delete" ||
209
+ (a[0] === "+" && a.length > 1)
210
+ )) || hasShortFlag(subArgs, "f");
211
+
212
+ if (isForce) {
213
+ return {
214
+ verb,
215
+ category: GIT_CATEGORY_DESTRUCTIVE,
216
+ isDestructive: true,
217
+ isRemote: true,
218
+ isForce: true,
219
+ reason: "git-force-push",
220
+ detail: "Forceful git push overwrites remote repository commit history",
221
+ };
222
+ }
223
+
224
+ return {
225
+ verb,
226
+ category: GIT_CATEGORY_REMOTE,
227
+ isDestructive: false,
228
+ isRemote: true,
229
+ isForce: false,
230
+ reason: "git-remote-push",
231
+ detail: "Publishing commits to a remote repository via git push requires confirmation",
232
+ };
233
+ }
234
+
235
+ if (verb === "checkout" && (subArgs.includes("-f") || subArgs.includes("--force") || (subArgs.includes("--") && subArgs.includes(".")))) {
236
+ return {
237
+ verb,
238
+ category: GIT_CATEGORY_DESTRUCTIVE,
239
+ isDestructive: true,
240
+ isRemote: false,
241
+ isForce: true,
242
+ reason: "git-checkout-force",
243
+ detail: "Forceful checkout discards local working tree changes",
244
+ };
245
+ }
246
+
247
+ if (verb === "restore" && subArgs.includes("--worktree") && (subArgs.includes(".") || subArgs.includes("--staged"))) {
248
+ return {
249
+ verb,
250
+ category: GIT_CATEGORY_DESTRUCTIVE,
251
+ isDestructive: true,
252
+ isRemote: false,
253
+ isForce: true,
254
+ reason: "git-restore-worktree",
255
+ detail: "Restoring working tree files discards local changes",
256
+ };
257
+ }
258
+
259
+ if (REMOTE_VERBS.has(verb)) {
260
+ return {
261
+ verb,
262
+ category: GIT_CATEGORY_REMOTE,
263
+ isDestructive: false,
264
+ isRemote: true,
265
+ isForce: false,
266
+ };
267
+ }
268
+
269
+ if (LOCAL_MUTATE_VERBS.has(verb)) {
270
+ return {
271
+ verb,
272
+ category: GIT_CATEGORY_MUTATE_LOCAL,
273
+ isDestructive: false,
274
+ isRemote: false,
275
+ isForce: false,
276
+ };
277
+ }
278
+
279
+ if (READ_ONLY_VERBS.has(verb)) {
280
+ return {
281
+ verb,
282
+ category: GIT_CATEGORY_READ,
283
+ isDestructive: false,
284
+ isRemote: false,
285
+ isForce: false,
286
+ };
287
+ }
288
+
289
+ return {
290
+ verb,
291
+ category: GIT_CATEGORY_MUTATE_LOCAL,
292
+ isDestructive: false,
293
+ isRemote: false,
294
+ isForce: false,
295
+ };
296
+ }
297
+
298
+ /**
299
+ * Full inspection of a git command invocation.
300
+ *
301
+ * @param {string[]} argv - Tokenized arguments
302
+ * @returns {{
303
+ * verb: string,
304
+ * args: string[],
305
+ * category: "read" | "mutate_local" | "remote" | "destructive",
306
+ * isDestructive: boolean,
307
+ * isRemote: boolean,
308
+ * isForce: boolean,
309
+ * reason?: string,
310
+ * detail?: string
311
+ * }}
312
+ */
313
+ export function inspectGitCommand(argv) {
314
+ const parsed = parseGitArgv(argv);
315
+ const classification = classifyGitOperation(parsed.verb, parsed.args);
316
+ return {
317
+ ...classification,
318
+ args: parsed.args,
319
+ globalOptions: parsed.globalOptions,
320
+ };
321
+ }