@imdeadpool/guardex 5.0.5 → 5.0.8
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/README.md +29 -4
- package/bin/multiagent-safety.js +496 -12
- package/package.json +3 -2
- package/templates/AGENTS.multiagent-safety.md +3 -1
- package/templates/githooks/pre-commit +32 -2
- package/templates/githooks/pre-push +18 -3
- package/templates/scripts/agent-branch-finish.sh +1 -1
- package/templates/scripts/agent-branch-start.sh +6 -39
- package/templates/scripts/agent-worktree-prune.sh +103 -1
- package/templates/scripts/codex-agent.sh +219 -7
package/bin/multiagent-safety.js
CHANGED
|
@@ -79,9 +79,11 @@ const CRITICAL_GUARDRAIL_PATHS = new Set([
|
|
|
79
79
|
|
|
80
80
|
const LOCK_FILE_RELATIVE = '.omx/state/agent-file-locks.json';
|
|
81
81
|
const AGENTS_MARKER_START = '<!-- multiagent-safety:START -->';
|
|
82
|
+
const AGENTS_MARKER_END = '<!-- multiagent-safety:END -->';
|
|
82
83
|
const GITIGNORE_MARKER_START = '# multiagent-safety:START';
|
|
83
84
|
const GITIGNORE_MARKER_END = '# multiagent-safety:END';
|
|
84
85
|
const MANAGED_GITIGNORE_PATHS = [
|
|
86
|
+
'.omx/',
|
|
85
87
|
'scripts/agent-branch-start.sh',
|
|
86
88
|
'scripts/agent-branch-finish.sh',
|
|
87
89
|
'scripts/codex-agent.sh',
|
|
@@ -97,6 +99,17 @@ const MANAGED_GITIGNORE_PATHS = [
|
|
|
97
99
|
'.claude/commands/guardex.md',
|
|
98
100
|
LOCK_FILE_RELATIVE,
|
|
99
101
|
];
|
|
102
|
+
const OMX_SCAFFOLD_DIRECTORIES = [
|
|
103
|
+
'.omx',
|
|
104
|
+
'.omx/state',
|
|
105
|
+
'.omx/logs',
|
|
106
|
+
'.omx/plans',
|
|
107
|
+
'.omx/agent-worktrees',
|
|
108
|
+
];
|
|
109
|
+
const OMX_SCAFFOLD_FILES = new Map([
|
|
110
|
+
['.omx/notepad.md', '\n\n## WORKING MEMORY\n'],
|
|
111
|
+
['.omx/project-memory.json', '{}\n'],
|
|
112
|
+
]);
|
|
100
113
|
const COMMAND_TYPO_ALIASES = new Map([
|
|
101
114
|
['relaese', 'release'],
|
|
102
115
|
['realaese', 'release'],
|
|
@@ -114,6 +127,7 @@ const SUGGESTIBLE_COMMANDS = [
|
|
|
114
127
|
'setup',
|
|
115
128
|
'init',
|
|
116
129
|
'doctor',
|
|
130
|
+
'review',
|
|
117
131
|
'report',
|
|
118
132
|
'copy-prompt',
|
|
119
133
|
'copy-commands',
|
|
@@ -148,8 +162,7 @@ const CLI_COMMAND_DESCRIPTIONS = [
|
|
|
148
162
|
['version', 'Print GuardeX version'],
|
|
149
163
|
];
|
|
150
164
|
const AGENT_BOT_DESCRIPTIONS = [
|
|
151
|
-
['review', '
|
|
152
|
-
['start', 'bash scripts/review-bot-watch.sh --interval 30'],
|
|
165
|
+
['review', 'Start PR monitor + codex-agent review flow (default interval: 30s)'],
|
|
153
166
|
];
|
|
154
167
|
|
|
155
168
|
const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-Rex for your repo) in this repository for Codex or Claude.
|
|
@@ -171,7 +184,10 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
|
|
|
171
184
|
3) If setup reports warnings/errors, repair + re-check:
|
|
172
185
|
gx doctor
|
|
173
186
|
|
|
174
|
-
4)
|
|
187
|
+
4) Optional: start continuous PR monitor from this repo:
|
|
188
|
+
gx review --interval 30
|
|
189
|
+
|
|
190
|
+
5) Confirm next safe agent workflow commands:
|
|
175
191
|
bash scripts/codex-agent.sh "task" "agent-name"
|
|
176
192
|
bash scripts/agent-branch-start.sh "task" "agent-name"
|
|
177
193
|
python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
|
|
@@ -183,17 +199,17 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
|
|
|
183
199
|
Remove them explicitly when done:
|
|
184
200
|
gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
|
|
185
201
|
|
|
186
|
-
|
|
202
|
+
6) Optional: create OpenSpec planning workspace:
|
|
187
203
|
bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
|
|
188
204
|
|
|
189
|
-
|
|
205
|
+
7) Optional: protect extra branches:
|
|
190
206
|
gx protect add release staging
|
|
191
207
|
|
|
192
|
-
|
|
208
|
+
8) Optional: sync your current agent branch with latest base branch:
|
|
193
209
|
gx sync --check
|
|
194
210
|
gx sync
|
|
195
211
|
|
|
196
|
-
|
|
212
|
+
9) Optional (GitHub remote cleanup): enable:
|
|
197
213
|
Settings -> General -> Pull Requests -> Automatically delete head branches
|
|
198
214
|
`;
|
|
199
215
|
|
|
@@ -201,6 +217,7 @@ const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
|
|
|
201
217
|
gh --version
|
|
202
218
|
gx setup
|
|
203
219
|
gx doctor
|
|
220
|
+
gx review --interval 30
|
|
204
221
|
bash scripts/codex-agent.sh "task" "agent-name"
|
|
205
222
|
bash scripts/agent-branch-start.sh "task" "agent-name"
|
|
206
223
|
python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
|
|
@@ -348,7 +365,9 @@ NOTES
|
|
|
348
365
|
- ${SHORT_TOOL_NAME} init is an alias of ${SHORT_TOOL_NAME} setup
|
|
349
366
|
- ${TOOL_NAME} setup asks for Y/N approval before global installs
|
|
350
367
|
- ${TOOL_NAME} setup checks GitHub CLI (gh) and prints install guidance if missing
|
|
368
|
+
- For other repos: ${SHORT_TOOL_NAME} setup --target <repo-path> then ${SHORT_TOOL_NAME} doctor --target <repo-path>
|
|
351
369
|
- In initialized repos, setup/install/fix block in-place writes on protected main by default
|
|
370
|
+
- setup/doctor auto-finish clean pending agent/* branches via PR flow into the current local base branch
|
|
352
371
|
- doctor auto-runs in a sandbox agent branch/worktree on protected main and tries auto-finish PR flow
|
|
353
372
|
- agent-branch-finish merges by default and keeps agent branches/worktrees until explicit cleanup
|
|
354
373
|
- use '${SHORT_TOOL_NAME} cleanup' to remove merged agent branches/worktrees (optionally remote refs too)
|
|
@@ -358,7 +377,8 @@ NOTES
|
|
|
358
377
|
console.log(`
|
|
359
378
|
[${TOOL_NAME}] No git repository detected in current directory.
|
|
360
379
|
[${TOOL_NAME}] Start from a repo root, or pass an explicit target:
|
|
361
|
-
${TOOL_NAME} setup --target <path-to-git-repo
|
|
380
|
+
${TOOL_NAME} setup --target <path-to-git-repo>
|
|
381
|
+
${TOOL_NAME} doctor --target <path-to-git-repo>`);
|
|
362
382
|
}
|
|
363
383
|
}
|
|
364
384
|
|
|
@@ -501,6 +521,45 @@ function lockFilePath(repoRoot) {
|
|
|
501
521
|
return path.join(repoRoot, LOCK_FILE_RELATIVE);
|
|
502
522
|
}
|
|
503
523
|
|
|
524
|
+
function ensureOmxScaffold(repoRoot, dryRun) {
|
|
525
|
+
const operations = [];
|
|
526
|
+
|
|
527
|
+
for (const relativeDir of OMX_SCAFFOLD_DIRECTORIES) {
|
|
528
|
+
const absoluteDir = path.join(repoRoot, relativeDir);
|
|
529
|
+
if (fs.existsSync(absoluteDir)) {
|
|
530
|
+
if (!fs.statSync(absoluteDir).isDirectory()) {
|
|
531
|
+
throw new Error(`Expected directory at ${relativeDir} but found a file.`);
|
|
532
|
+
}
|
|
533
|
+
operations.push({ status: 'unchanged', file: relativeDir });
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (!dryRun) {
|
|
538
|
+
fs.mkdirSync(absoluteDir, { recursive: true });
|
|
539
|
+
}
|
|
540
|
+
operations.push({ status: 'created', file: relativeDir });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
for (const [relativeFile, defaultContent] of OMX_SCAFFOLD_FILES.entries()) {
|
|
544
|
+
const absoluteFile = path.join(repoRoot, relativeFile);
|
|
545
|
+
if (fs.existsSync(absoluteFile)) {
|
|
546
|
+
if (!fs.statSync(absoluteFile).isFile()) {
|
|
547
|
+
throw new Error(`Expected file at ${relativeFile} but found a directory.`);
|
|
548
|
+
}
|
|
549
|
+
operations.push({ status: 'unchanged', file: relativeFile });
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!dryRun) {
|
|
554
|
+
fs.mkdirSync(path.dirname(absoluteFile), { recursive: true });
|
|
555
|
+
fs.writeFileSync(absoluteFile, defaultContent, 'utf8');
|
|
556
|
+
}
|
|
557
|
+
operations.push({ status: 'created', file: relativeFile });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return operations;
|
|
561
|
+
}
|
|
562
|
+
|
|
504
563
|
function ensureLockRegistry(repoRoot, dryRun) {
|
|
505
564
|
const absolutePath = lockFilePath(repoRoot);
|
|
506
565
|
if (fs.existsSync(absolutePath)) {
|
|
@@ -608,6 +667,10 @@ function ensurePackageScripts(repoRoot, dryRun) {
|
|
|
608
667
|
function ensureAgentsSnippet(repoRoot, dryRun) {
|
|
609
668
|
const agentsPath = path.join(repoRoot, 'AGENTS.md');
|
|
610
669
|
const snippet = fs.readFileSync(path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md'), 'utf8').trimEnd();
|
|
670
|
+
const managedRegex = new RegExp(
|
|
671
|
+
`${AGENTS_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${AGENTS_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
|
|
672
|
+
'm',
|
|
673
|
+
);
|
|
611
674
|
|
|
612
675
|
if (!fs.existsSync(agentsPath)) {
|
|
613
676
|
if (!dryRun) {
|
|
@@ -617,8 +680,19 @@ function ensureAgentsSnippet(repoRoot, dryRun) {
|
|
|
617
680
|
}
|
|
618
681
|
|
|
619
682
|
const existing = fs.readFileSync(agentsPath, 'utf8');
|
|
683
|
+
if (managedRegex.test(existing)) {
|
|
684
|
+
const next = existing.replace(managedRegex, snippet);
|
|
685
|
+
if (next === existing) {
|
|
686
|
+
return { status: 'unchanged', file: 'AGENTS.md' };
|
|
687
|
+
}
|
|
688
|
+
if (!dryRun) {
|
|
689
|
+
fs.writeFileSync(agentsPath, next, 'utf8');
|
|
690
|
+
}
|
|
691
|
+
return { status: 'updated', file: 'AGENTS.md', note: 'refreshed guardex-managed block' };
|
|
692
|
+
}
|
|
693
|
+
|
|
620
694
|
if (existing.includes(AGENTS_MARKER_START)) {
|
|
621
|
-
return { status: 'unchanged', file: 'AGENTS.md' };
|
|
695
|
+
return { status: 'unchanged', file: 'AGENTS.md', note: 'existing marker found without managed end marker' };
|
|
622
696
|
}
|
|
623
697
|
|
|
624
698
|
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
@@ -828,6 +902,33 @@ function isSpawnFailure(result) {
|
|
|
828
902
|
return Boolean(result?.error) && typeof result?.status !== 'number';
|
|
829
903
|
}
|
|
830
904
|
|
|
905
|
+
function ensureRepoBranch(repoRoot, branch) {
|
|
906
|
+
const current = currentBranchName(repoRoot);
|
|
907
|
+
if (current === branch) {
|
|
908
|
+
return { ok: true, changed: false };
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const checkoutResult = run('git', ['-C', repoRoot, 'checkout', branch], { timeout: 20_000 });
|
|
912
|
+
if (isSpawnFailure(checkoutResult)) {
|
|
913
|
+
return {
|
|
914
|
+
ok: false,
|
|
915
|
+
changed: false,
|
|
916
|
+
stdout: checkoutResult.stdout || '',
|
|
917
|
+
stderr: checkoutResult.stderr || '',
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
if (checkoutResult.status !== 0) {
|
|
921
|
+
return {
|
|
922
|
+
ok: false,
|
|
923
|
+
changed: false,
|
|
924
|
+
stdout: checkoutResult.stdout || '',
|
|
925
|
+
stderr: checkoutResult.stderr || '',
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return { ok: true, changed: true };
|
|
930
|
+
}
|
|
931
|
+
|
|
831
932
|
function doctorSandboxBranchPrefix() {
|
|
832
933
|
const now = new Date();
|
|
833
934
|
const stamp = [
|
|
@@ -929,12 +1030,26 @@ function startDoctorSandbox(blocked) {
|
|
|
929
1030
|
throw startResult.error;
|
|
930
1031
|
}
|
|
931
1032
|
if (startResult.status !== 0) {
|
|
932
|
-
|
|
1033
|
+
return startDoctorSandboxFallback(blocked);
|
|
933
1034
|
}
|
|
934
1035
|
|
|
935
1036
|
const metadata = extractAgentBranchStartMetadata(startResult.stdout);
|
|
936
|
-
|
|
937
|
-
|
|
1037
|
+
const currentBranch = currentBranchName(blocked.repoRoot);
|
|
1038
|
+
const worktreePath = metadata.worktreePath ? path.resolve(metadata.worktreePath) : '';
|
|
1039
|
+
const repoRootPath = path.resolve(blocked.repoRoot);
|
|
1040
|
+
const hasSafeWorktree = Boolean(worktreePath) && worktreePath !== repoRootPath;
|
|
1041
|
+
const branchChanged = Boolean(currentBranch) && currentBranch !== blocked.branch;
|
|
1042
|
+
|
|
1043
|
+
if (!hasSafeWorktree || branchChanged) {
|
|
1044
|
+
const restoreResult = ensureRepoBranch(blocked.repoRoot, blocked.branch);
|
|
1045
|
+
if (!restoreResult.ok) {
|
|
1046
|
+
const detail = [restoreResult.stderr, restoreResult.stdout].filter(Boolean).join('\n').trim();
|
|
1047
|
+
throw new Error(
|
|
1048
|
+
`doctor sandbox startup switched protected base checkout and could not restore '${blocked.branch}'.` +
|
|
1049
|
+
(detail ? `\n${detail}` : ''),
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
return startDoctorSandboxFallback(blocked);
|
|
938
1053
|
}
|
|
939
1054
|
|
|
940
1055
|
return {
|
|
@@ -1069,6 +1184,19 @@ function isCommandAvailable(commandName) {
|
|
|
1069
1184
|
return run('which', [commandName]).status === 0;
|
|
1070
1185
|
}
|
|
1071
1186
|
|
|
1187
|
+
function extractAgentBranchFinishPrUrl(output) {
|
|
1188
|
+
const match = String(output || '').match(/\[agent-branch-finish\] PR:\s*(\S+)/);
|
|
1189
|
+
return match ? match[1] : '';
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function doctorFinishFlowIsPending(output) {
|
|
1193
|
+
return (
|
|
1194
|
+
/\[agent-branch-finish\] PR merge not completed yet; leaving PR open\./.test(output) ||
|
|
1195
|
+
/\[agent-branch-finish\] Merge pending review\/check policy\. Branch cleanup skipped for now\./.test(output) ||
|
|
1196
|
+
/\[agent-branch-finish\] PR auto-merge enabled; waiting for required checks\/reviews\./.test(output)
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1072
1200
|
function finishDoctorSandboxBranch(blocked, metadata) {
|
|
1073
1201
|
const finishScript = path.join(metadata.worktreePath, 'scripts', 'agent-branch-finish.sh');
|
|
1074
1202
|
if (!fs.existsSync(finishScript)) {
|
|
@@ -1122,6 +1250,17 @@ function finishDoctorSandboxBranch(blocked, metadata) {
|
|
|
1122
1250
|
};
|
|
1123
1251
|
}
|
|
1124
1252
|
|
|
1253
|
+
const combinedOutput = `${finishResult.stdout || ''}\n${finishResult.stderr || ''}`;
|
|
1254
|
+
if (doctorFinishFlowIsPending(combinedOutput)) {
|
|
1255
|
+
return {
|
|
1256
|
+
status: 'pending',
|
|
1257
|
+
note: 'PR created and waiting for merge policy/checks',
|
|
1258
|
+
prUrl: extractAgentBranchFinishPrUrl(combinedOutput),
|
|
1259
|
+
stdout: finishResult.stdout || '',
|
|
1260
|
+
stderr: finishResult.stderr || '',
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1125
1264
|
return {
|
|
1126
1265
|
status: 'completed',
|
|
1127
1266
|
note: 'doctor sandbox finish flow completed',
|
|
@@ -1157,7 +1296,35 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1157
1296
|
status: 'skipped',
|
|
1158
1297
|
note: 'sandbox doctor did not complete successfully',
|
|
1159
1298
|
};
|
|
1299
|
+
let postSandboxAutoFinishSummary = {
|
|
1300
|
+
enabled: false,
|
|
1301
|
+
attempted: 0,
|
|
1302
|
+
completed: 0,
|
|
1303
|
+
skipped: 0,
|
|
1304
|
+
failed: 0,
|
|
1305
|
+
details: ['Skipped auto-finish sweep (sandbox doctor did not complete successfully).'],
|
|
1306
|
+
};
|
|
1307
|
+
let omxScaffoldSyncResult = {
|
|
1308
|
+
status: 'skipped',
|
|
1309
|
+
note: 'sandbox doctor did not complete successfully',
|
|
1310
|
+
};
|
|
1160
1311
|
if (nestedResult.status === 0) {
|
|
1312
|
+
const omxScaffoldOps = ensureOmxScaffold(blocked.repoRoot, Boolean(options.dryRun));
|
|
1313
|
+
const changedOmxPaths = omxScaffoldOps.filter((operation) => operation.status !== 'unchanged');
|
|
1314
|
+
if (changedOmxPaths.length === 0) {
|
|
1315
|
+
omxScaffoldSyncResult = {
|
|
1316
|
+
status: 'unchanged',
|
|
1317
|
+
note: '.omx scaffold already in sync',
|
|
1318
|
+
operations: omxScaffoldOps,
|
|
1319
|
+
};
|
|
1320
|
+
} else {
|
|
1321
|
+
omxScaffoldSyncResult = {
|
|
1322
|
+
status: options.dryRun ? 'would-sync' : 'synced',
|
|
1323
|
+
note: `${options.dryRun ? 'would sync' : 'synced'} ${changedOmxPaths.length} .omx path(s)`,
|
|
1324
|
+
operations: omxScaffoldOps,
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1161
1328
|
if (!options.dryRun) {
|
|
1162
1329
|
autoCommitResult = autoCommitDoctorSandboxChanges(metadata);
|
|
1163
1330
|
if (autoCommitResult.status === 'committed') {
|
|
@@ -1213,6 +1380,12 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1213
1380
|
};
|
|
1214
1381
|
}
|
|
1215
1382
|
}
|
|
1383
|
+
|
|
1384
|
+
postSandboxAutoFinishSummary = autoFinishReadyAgentBranches(blocked.repoRoot, {
|
|
1385
|
+
baseBranch: blocked.branch,
|
|
1386
|
+
dryRun: options.dryRun,
|
|
1387
|
+
excludeBranches: [metadata.branch],
|
|
1388
|
+
});
|
|
1216
1389
|
}
|
|
1217
1390
|
|
|
1218
1391
|
if (options.json) {
|
|
@@ -1224,9 +1397,11 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1224
1397
|
JSON.stringify(
|
|
1225
1398
|
{
|
|
1226
1399
|
...parsed,
|
|
1400
|
+
sandboxOmxScaffoldSync: omxScaffoldSyncResult,
|
|
1227
1401
|
sandboxLockSync: lockSyncResult,
|
|
1228
1402
|
sandboxAutoCommit: autoCommitResult,
|
|
1229
1403
|
sandboxFinish: finishResult,
|
|
1404
|
+
autoFinish: postSandboxAutoFinishSummary,
|
|
1230
1405
|
},
|
|
1231
1406
|
null,
|
|
1232
1407
|
2,
|
|
@@ -1266,6 +1441,15 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1266
1441
|
console.log(`[${TOOL_NAME}] Auto-finish flow completed for sandbox branch '${metadata.branch}'.`);
|
|
1267
1442
|
if (finishResult.stdout) process.stdout.write(finishResult.stdout);
|
|
1268
1443
|
if (finishResult.stderr) process.stderr.write(finishResult.stderr);
|
|
1444
|
+
} else if (finishResult.status === 'pending') {
|
|
1445
|
+
console.log(
|
|
1446
|
+
`[${TOOL_NAME}] Auto-finish pending for sandbox branch '${metadata.branch}': ${finishResult.note}.`,
|
|
1447
|
+
);
|
|
1448
|
+
if (finishResult.prUrl) {
|
|
1449
|
+
console.log(`[${TOOL_NAME}] PR: ${finishResult.prUrl}`);
|
|
1450
|
+
}
|
|
1451
|
+
if (finishResult.stdout) process.stdout.write(finishResult.stdout);
|
|
1452
|
+
if (finishResult.stderr) process.stderr.write(finishResult.stderr);
|
|
1269
1453
|
} else if (finishResult.status === 'failed') {
|
|
1270
1454
|
console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
|
|
1271
1455
|
if (finishResult.stdout) process.stdout.write(finishResult.stdout);
|
|
@@ -1283,6 +1467,26 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1283
1467
|
} else {
|
|
1284
1468
|
console.log(`[${TOOL_NAME}] Lock registry sync skipped: ${lockSyncResult.note}.`);
|
|
1285
1469
|
}
|
|
1470
|
+
|
|
1471
|
+
if (postSandboxAutoFinishSummary.enabled) {
|
|
1472
|
+
console.log(
|
|
1473
|
+
`[${TOOL_NAME}] Auto-finish sweep (base=${blocked.branch}): attempted=${postSandboxAutoFinishSummary.attempted}, completed=${postSandboxAutoFinishSummary.completed}, skipped=${postSandboxAutoFinishSummary.skipped}, failed=${postSandboxAutoFinishSummary.failed}`,
|
|
1474
|
+
);
|
|
1475
|
+
for (const detail of postSandboxAutoFinishSummary.details) {
|
|
1476
|
+
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
1477
|
+
}
|
|
1478
|
+
} else if (postSandboxAutoFinishSummary.details.length > 0) {
|
|
1479
|
+
console.log(`[${TOOL_NAME}] ${postSandboxAutoFinishSummary.details[0]}`);
|
|
1480
|
+
}
|
|
1481
|
+
if (omxScaffoldSyncResult.status === 'synced') {
|
|
1482
|
+
console.log(`[${TOOL_NAME}] Synced .omx scaffold back to protected branch workspace.`);
|
|
1483
|
+
} else if (omxScaffoldSyncResult.status === 'unchanged') {
|
|
1484
|
+
console.log(`[${TOOL_NAME}] .omx scaffold already aligned in protected branch workspace.`);
|
|
1485
|
+
} else if (omxScaffoldSyncResult.status === 'would-sync') {
|
|
1486
|
+
console.log(`[${TOOL_NAME}] Dry run: would sync .omx scaffold back to protected branch workspace.`);
|
|
1487
|
+
} else {
|
|
1488
|
+
console.log(`[${TOOL_NAME}] .omx scaffold sync skipped: ${omxScaffoldSyncResult.note}.`);
|
|
1489
|
+
}
|
|
1286
1490
|
}
|
|
1287
1491
|
}
|
|
1288
1492
|
|
|
@@ -1314,6 +1518,18 @@ function parseTargetFlag(rawArgs, defaultTarget = process.cwd()) {
|
|
|
1314
1518
|
return { target, args: remaining };
|
|
1315
1519
|
}
|
|
1316
1520
|
|
|
1521
|
+
function parseReviewArgs(rawArgs) {
|
|
1522
|
+
const parsed = parseTargetFlag(rawArgs, process.cwd());
|
|
1523
|
+
const passthroughArgs = [...parsed.args];
|
|
1524
|
+
if (passthroughArgs[0] === 'start') {
|
|
1525
|
+
passthroughArgs.shift();
|
|
1526
|
+
}
|
|
1527
|
+
return {
|
|
1528
|
+
target: parsed.target,
|
|
1529
|
+
passthroughArgs,
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1317
1533
|
function parseReportArgs(rawArgs) {
|
|
1318
1534
|
const options = {
|
|
1319
1535
|
target: process.cwd(),
|
|
@@ -1579,6 +1795,203 @@ function listLocalUserBranches(repoRoot) {
|
|
|
1579
1795
|
return [branchName];
|
|
1580
1796
|
}
|
|
1581
1797
|
|
|
1798
|
+
function listLocalAgentBranches(repoRoot) {
|
|
1799
|
+
const result = gitRun(
|
|
1800
|
+
repoRoot,
|
|
1801
|
+
['for-each-ref', '--format=%(refname:short)', 'refs/heads/agent/'],
|
|
1802
|
+
{ allowFailure: true },
|
|
1803
|
+
);
|
|
1804
|
+
if (result.status !== 0) {
|
|
1805
|
+
return [];
|
|
1806
|
+
}
|
|
1807
|
+
return uniquePreserveOrder(
|
|
1808
|
+
String(result.stdout || '')
|
|
1809
|
+
.split('\n')
|
|
1810
|
+
.map((item) => item.trim())
|
|
1811
|
+
.filter(Boolean),
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
function mapWorktreePathsByBranch(repoRoot) {
|
|
1816
|
+
const result = gitRun(repoRoot, ['worktree', 'list', '--porcelain'], { allowFailure: true });
|
|
1817
|
+
const map = new Map();
|
|
1818
|
+
if (result.status !== 0) {
|
|
1819
|
+
return map;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
const lines = String(result.stdout || '').split('\n');
|
|
1823
|
+
let currentWorktree = '';
|
|
1824
|
+
for (const line of lines) {
|
|
1825
|
+
if (line.startsWith('worktree ')) {
|
|
1826
|
+
currentWorktree = line.slice('worktree '.length).trim();
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
if (line.startsWith('branch refs/heads/')) {
|
|
1830
|
+
const branchName = line.slice('branch refs/heads/'.length).trim();
|
|
1831
|
+
if (currentWorktree && branchName) {
|
|
1832
|
+
map.set(branchName, currentWorktree);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
return map;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function hasSignificantWorkingTreeChanges(worktreePath) {
|
|
1840
|
+
const result = run('git', ['-C', worktreePath, 'status', '--porcelain']);
|
|
1841
|
+
if (result.status !== 0) {
|
|
1842
|
+
return true;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
const lines = String(result.stdout || '')
|
|
1846
|
+
.split('\n')
|
|
1847
|
+
.map((line) => line.trimEnd())
|
|
1848
|
+
.filter((line) => line.length > 0);
|
|
1849
|
+
|
|
1850
|
+
for (const line of lines) {
|
|
1851
|
+
const pathPart = (line.length > 3 ? line.slice(3) : '').trim();
|
|
1852
|
+
if (!pathPart) continue;
|
|
1853
|
+
if (pathPart === LOCK_FILE_RELATIVE) continue;
|
|
1854
|
+
if (pathPart.startsWith(`${LOCK_FILE_RELATIVE} -> `)) continue;
|
|
1855
|
+
if (pathPart.endsWith(` -> ${LOCK_FILE_RELATIVE}`)) continue;
|
|
1856
|
+
return true;
|
|
1857
|
+
}
|
|
1858
|
+
return false;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
1862
|
+
const baseBranch = String(options.baseBranch || '').trim();
|
|
1863
|
+
const dryRun = Boolean(options.dryRun);
|
|
1864
|
+
const excludedBranches = new Set(
|
|
1865
|
+
Array.isArray(options.excludeBranches)
|
|
1866
|
+
? options.excludeBranches.map((branch) => String(branch || '').trim()).filter(Boolean)
|
|
1867
|
+
: [],
|
|
1868
|
+
);
|
|
1869
|
+
|
|
1870
|
+
const summary = {
|
|
1871
|
+
enabled: true,
|
|
1872
|
+
baseBranch,
|
|
1873
|
+
attempted: 0,
|
|
1874
|
+
completed: 0,
|
|
1875
|
+
skipped: 0,
|
|
1876
|
+
failed: 0,
|
|
1877
|
+
details: [],
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1880
|
+
if (!baseBranch || baseBranch === 'HEAD' || baseBranch.startsWith('agent/')) {
|
|
1881
|
+
summary.enabled = false;
|
|
1882
|
+
summary.details.push('Skipped auto-finish sweep (base branch is missing or not a non-agent local branch).');
|
|
1883
|
+
return summary;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
if (String(process.env.MUSAFETY_DOCTOR_SANDBOX || '') === '1') {
|
|
1887
|
+
summary.enabled = false;
|
|
1888
|
+
summary.details.push('Skipped auto-finish sweep inside doctor sandbox pass.');
|
|
1889
|
+
return summary;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
if (String(process.env.MUSAFETY_SKIP_AUTO_FINISH_READY_BRANCHES || '') === '1') {
|
|
1893
|
+
summary.enabled = false;
|
|
1894
|
+
summary.details.push('Skipped auto-finish sweep (MUSAFETY_SKIP_AUTO_FINISH_READY_BRANCHES=1).');
|
|
1895
|
+
return summary;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
if (dryRun) {
|
|
1899
|
+
summary.enabled = false;
|
|
1900
|
+
summary.details.push('Skipped auto-finish sweep in dry-run mode.');
|
|
1901
|
+
return summary;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
const finishScript = path.join(repoRoot, 'scripts', 'agent-branch-finish.sh');
|
|
1905
|
+
if (!fs.existsSync(finishScript)) {
|
|
1906
|
+
summary.enabled = false;
|
|
1907
|
+
summary.details.push(`Skipped auto-finish sweep (missing ${path.relative(repoRoot, finishScript)}).`);
|
|
1908
|
+
return summary;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
const hasOrigin = gitRun(repoRoot, ['remote', 'get-url', 'origin'], { allowFailure: true }).status === 0;
|
|
1912
|
+
if (!hasOrigin) {
|
|
1913
|
+
summary.enabled = false;
|
|
1914
|
+
summary.details.push('Skipped auto-finish sweep (origin remote missing).');
|
|
1915
|
+
return summary;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
const ghBin = process.env.MUSAFETY_GH_BIN || 'gh';
|
|
1919
|
+
if (run(ghBin, ['--version']).status !== 0) {
|
|
1920
|
+
summary.enabled = false;
|
|
1921
|
+
summary.details.push(`Skipped auto-finish sweep (${ghBin} not available).`);
|
|
1922
|
+
return summary;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
const branchWorktrees = mapWorktreePathsByBranch(repoRoot);
|
|
1926
|
+
const agentBranches = listLocalAgentBranches(repoRoot);
|
|
1927
|
+
if (agentBranches.length === 0) {
|
|
1928
|
+
summary.enabled = false;
|
|
1929
|
+
summary.details.push('No local agent branches found for auto-finish sweep.');
|
|
1930
|
+
return summary;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
for (const branch of agentBranches) {
|
|
1934
|
+
if (excludedBranches.has(branch)) {
|
|
1935
|
+
summary.skipped += 1;
|
|
1936
|
+
summary.details.push(`[skip] ${branch}: excluded from this auto-finish sweep.`);
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
if (branch === baseBranch) {
|
|
1941
|
+
summary.skipped += 1;
|
|
1942
|
+
summary.details.push(`[skip] ${branch}: source branch equals base branch.`);
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
let counts;
|
|
1947
|
+
try {
|
|
1948
|
+
counts = aheadBehind(repoRoot, branch, baseBranch);
|
|
1949
|
+
} catch (error) {
|
|
1950
|
+
summary.failed += 1;
|
|
1951
|
+
summary.details.push(`[fail] ${branch}: unable to compute ahead/behind (${error.message}).`);
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
if (counts.ahead <= 0) {
|
|
1956
|
+
summary.skipped += 1;
|
|
1957
|
+
summary.details.push(`[skip] ${branch}: already merged into ${baseBranch}.`);
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
const branchWorktree = branchWorktrees.get(branch) || '';
|
|
1962
|
+
if (branchWorktree && hasSignificantWorkingTreeChanges(branchWorktree)) {
|
|
1963
|
+
summary.skipped += 1;
|
|
1964
|
+
summary.details.push(`[skip] ${branch}: dirty worktree (${branchWorktree}).`);
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
summary.attempted += 1;
|
|
1969
|
+
const finishArgs = [
|
|
1970
|
+
finishScript,
|
|
1971
|
+
'--branch',
|
|
1972
|
+
branch,
|
|
1973
|
+
'--base',
|
|
1974
|
+
baseBranch,
|
|
1975
|
+
'--via-pr',
|
|
1976
|
+
'--cleanup',
|
|
1977
|
+
];
|
|
1978
|
+
const finishResult = run('bash', finishArgs, { cwd: repoRoot });
|
|
1979
|
+
const combinedOutput = [finishResult.stdout || '', finishResult.stderr || ''].join('\n').trim();
|
|
1980
|
+
|
|
1981
|
+
if (finishResult.status === 0) {
|
|
1982
|
+
summary.completed += 1;
|
|
1983
|
+
summary.details.push(`[done] ${branch}: auto-finish completed.`);
|
|
1984
|
+
continue;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
summary.failed += 1;
|
|
1988
|
+
const tail = combinedOutput ? ` ${combinedOutput.split('\n').slice(-2).join(' | ')}` : '';
|
|
1989
|
+
summary.details.push(`[fail] ${branch}: auto-finish failed.${tail}`);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
return summary;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1582
1995
|
function ensureSetupProtectedBranches(repoRoot, dryRun) {
|
|
1583
1996
|
const localUserBranches = listLocalUserBranches(repoRoot);
|
|
1584
1997
|
if (localUserBranches.length === 0) {
|
|
@@ -1818,6 +2231,7 @@ function parseCleanupArgs(rawArgs) {
|
|
|
1818
2231
|
dryRun: false,
|
|
1819
2232
|
forceDirty: false,
|
|
1820
2233
|
keepRemote: false,
|
|
2234
|
+
keepCleanWorktrees: false,
|
|
1821
2235
|
};
|
|
1822
2236
|
|
|
1823
2237
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
@@ -1861,6 +2275,10 @@ function parseCleanupArgs(rawArgs) {
|
|
|
1861
2275
|
options.keepRemote = true;
|
|
1862
2276
|
continue;
|
|
1863
2277
|
}
|
|
2278
|
+
if (arg === '--keep-clean-worktrees') {
|
|
2279
|
+
options.keepCleanWorktrees = true;
|
|
2280
|
+
continue;
|
|
2281
|
+
}
|
|
1864
2282
|
throw new Error(`Unknown option: ${arg}`);
|
|
1865
2283
|
}
|
|
1866
2284
|
|
|
@@ -2271,6 +2689,8 @@ function runInstallInternal(options) {
|
|
|
2271
2689
|
const repoRoot = resolveRepoRoot(options.target);
|
|
2272
2690
|
const operations = [];
|
|
2273
2691
|
|
|
2692
|
+
operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
|
|
2693
|
+
|
|
2274
2694
|
for (const templateFile of TEMPLATE_FILES) {
|
|
2275
2695
|
operations.push(copyTemplateFile(repoRoot, templateFile, Boolean(options.force), Boolean(options.dryRun)));
|
|
2276
2696
|
}
|
|
@@ -2297,6 +2717,8 @@ function runFixInternal(options) {
|
|
|
2297
2717
|
const repoRoot = resolveRepoRoot(options.target);
|
|
2298
2718
|
const operations = [];
|
|
2299
2719
|
|
|
2720
|
+
operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
|
|
2721
|
+
|
|
2300
2722
|
for (const templateFile of TEMPLATE_FILES) {
|
|
2301
2723
|
operations.push(ensureTemplateFilePresent(repoRoot, templateFile, Boolean(options.dryRun)));
|
|
2302
2724
|
}
|
|
@@ -2350,6 +2772,8 @@ function runScanInternal(options) {
|
|
|
2350
2772
|
const findings = [];
|
|
2351
2773
|
|
|
2352
2774
|
const requiredPaths = [
|
|
2775
|
+
...OMX_SCAFFOLD_DIRECTORIES,
|
|
2776
|
+
...Array.from(OMX_SCAFFOLD_FILES.keys()),
|
|
2353
2777
|
...TEMPLATE_FILES.map((entry) => toDestinationPath(entry)),
|
|
2354
2778
|
LOCK_FILE_RELATIVE,
|
|
2355
2779
|
];
|
|
@@ -2701,6 +3125,11 @@ function doctor(rawArgs) {
|
|
|
2701
3125
|
assertProtectedMainWriteAllowed(options, 'doctor');
|
|
2702
3126
|
const fixPayload = runFixInternal(options);
|
|
2703
3127
|
const scanResult = runScanInternal({ target: options.target, json: false });
|
|
3128
|
+
const currentBaseBranch = currentBranchName(scanResult.repoRoot);
|
|
3129
|
+
const autoFinishSummary = autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
3130
|
+
baseBranch: currentBaseBranch,
|
|
3131
|
+
dryRun: options.dryRun,
|
|
3132
|
+
});
|
|
2704
3133
|
const safe = scanResult.errors === 0 && scanResult.warnings === 0;
|
|
2705
3134
|
const musafe = safe;
|
|
2706
3135
|
|
|
@@ -2722,6 +3151,7 @@ function doctor(rawArgs) {
|
|
|
2722
3151
|
warnings: scanResult.warnings,
|
|
2723
3152
|
findings: scanResult.findings,
|
|
2724
3153
|
},
|
|
3154
|
+
autoFinish: autoFinishSummary,
|
|
2725
3155
|
},
|
|
2726
3156
|
null,
|
|
2727
3157
|
2,
|
|
@@ -2733,6 +3163,16 @@ function doctor(rawArgs) {
|
|
|
2733
3163
|
|
|
2734
3164
|
printOperations('Doctor/fix', fixPayload, options.dryRun);
|
|
2735
3165
|
printScanResult(scanResult, false);
|
|
3166
|
+
if (autoFinishSummary.enabled) {
|
|
3167
|
+
console.log(
|
|
3168
|
+
`[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
|
|
3169
|
+
);
|
|
3170
|
+
for (const detail of autoFinishSummary.details) {
|
|
3171
|
+
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
3172
|
+
}
|
|
3173
|
+
} else if (autoFinishSummary.details.length > 0) {
|
|
3174
|
+
console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
|
|
3175
|
+
}
|
|
2736
3176
|
if (safe) {
|
|
2737
3177
|
console.log(`[${TOOL_NAME}] ✅ Repo is fully safe.`);
|
|
2738
3178
|
} else {
|
|
@@ -2743,6 +3183,27 @@ function doctor(rawArgs) {
|
|
|
2743
3183
|
setExitCodeFromScan(scanResult);
|
|
2744
3184
|
}
|
|
2745
3185
|
|
|
3186
|
+
function review(rawArgs) {
|
|
3187
|
+
const options = parseReviewArgs(rawArgs);
|
|
3188
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
3189
|
+
const reviewScriptPath = path.join(repoRoot, 'scripts', 'review-bot-watch.sh');
|
|
3190
|
+
if (!fs.existsSync(reviewScriptPath)) {
|
|
3191
|
+
throw new Error(
|
|
3192
|
+
`Missing review bot script: ${reviewScriptPath}\n` +
|
|
3193
|
+
`Run '${SHORT_TOOL_NAME} setup --target ${repoRoot}' then '${SHORT_TOOL_NAME} doctor --target ${repoRoot}'.`,
|
|
3194
|
+
);
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
const result = run('bash', [reviewScriptPath, ...options.passthroughArgs], { cwd: repoRoot });
|
|
3198
|
+
if (isSpawnFailure(result)) {
|
|
3199
|
+
throw result.error;
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
3203
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
3204
|
+
process.exitCode = typeof result.status === 'number' ? result.status : 1;
|
|
3205
|
+
}
|
|
3206
|
+
|
|
2746
3207
|
function report(rawArgs) {
|
|
2747
3208
|
const options = parseReportArgs(rawArgs);
|
|
2748
3209
|
const subcommand = options.subcommand || 'help';
|
|
@@ -2901,7 +3362,22 @@ function setup(rawArgs) {
|
|
|
2901
3362
|
}
|
|
2902
3363
|
|
|
2903
3364
|
const scanResult = runScanInternal({ target: options.target, json: false });
|
|
3365
|
+
const currentBaseBranch = currentBranchName(scanResult.repoRoot);
|
|
3366
|
+
const autoFinishSummary = autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
3367
|
+
baseBranch: currentBaseBranch,
|
|
3368
|
+
dryRun: options.dryRun,
|
|
3369
|
+
});
|
|
2904
3370
|
printScanResult(scanResult, false);
|
|
3371
|
+
if (autoFinishSummary.enabled) {
|
|
3372
|
+
console.log(
|
|
3373
|
+
`[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
|
|
3374
|
+
);
|
|
3375
|
+
for (const detail of autoFinishSummary.details) {
|
|
3376
|
+
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
3377
|
+
}
|
|
3378
|
+
} else if (autoFinishSummary.details.length > 0) {
|
|
3379
|
+
console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
|
|
3380
|
+
}
|
|
2905
3381
|
|
|
2906
3382
|
if (scanResult.errors === 0 && scanResult.warnings === 0) {
|
|
2907
3383
|
console.log(`[${TOOL_NAME}] ✅ Setup complete.`);
|
|
@@ -2996,6 +3472,9 @@ function cleanup(rawArgs) {
|
|
|
2996
3472
|
if (options.dryRun) {
|
|
2997
3473
|
args.push('--dry-run');
|
|
2998
3474
|
}
|
|
3475
|
+
if (!options.keepCleanWorktrees) {
|
|
3476
|
+
args.push('--only-dirty-worktrees');
|
|
3477
|
+
}
|
|
2999
3478
|
args.push('--delete-branches');
|
|
3000
3479
|
if (!options.keepRemote) {
|
|
3001
3480
|
args.push('--delete-remote-branches');
|
|
@@ -3331,6 +3810,11 @@ function main() {
|
|
|
3331
3810
|
return;
|
|
3332
3811
|
}
|
|
3333
3812
|
|
|
3813
|
+
if (command === 'review') {
|
|
3814
|
+
review(rest);
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3334
3818
|
if (command === 'report') {
|
|
3335
3819
|
report(rest);
|
|
3336
3820
|
return;
|