@imdeadpool/guardex 5.0.11 → 5.0.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/README.md +60 -13
- package/bin/multiagent-safety.js +485 -60
- package/package.json +4 -4
- package/templates/AGENTS.multiagent-safety.md +19 -7
- package/templates/codex/skills/guardex/SKILL.md +48 -0
- package/templates/codex/skills/guardex-merge-skills-to-dev/SKILL.md +58 -0
- package/templates/githooks/post-merge +43 -0
- package/templates/githooks/pre-commit +24 -15
- package/templates/githooks/pre-push +3 -3
- package/templates/github/pull.yml.example +6 -0
- package/templates/github/workflows/cr.yml +21 -0
- package/templates/scripts/agent-branch-finish.sh +0 -22
- package/templates/scripts/agent-branch-start.sh +66 -1
- package/templates/scripts/codex-agent.sh +82 -27
- package/templates/scripts/openspec/init-change-workspace.sh +87 -0
package/bin/multiagent-safety.js
CHANGED
|
@@ -10,9 +10,10 @@ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
|
10
10
|
const TOOL_NAME = 'guardex';
|
|
11
11
|
const SHORT_TOOL_NAME = 'gx';
|
|
12
12
|
const LEGACY_NAMES = ['musafety', 'multiagent-safety'];
|
|
13
|
+
const OPENSPEC_PACKAGE = '@fission-ai/openspec';
|
|
13
14
|
const GLOBAL_TOOLCHAIN_PACKAGES = [
|
|
14
15
|
'oh-my-codex',
|
|
15
|
-
|
|
16
|
+
OPENSPEC_PACKAGE,
|
|
16
17
|
'@imdeadpool/codex-account-switcher',
|
|
17
18
|
];
|
|
18
19
|
const GH_BIN = process.env.MUSAFETY_GH_BIN || 'gh';
|
|
@@ -28,6 +29,7 @@ const MAINTAINER_RELEASE_REPO = path.resolve(
|
|
|
28
29
|
process.env.MUSAFETY_RELEASE_REPO || '/tmp/multiagent-safety',
|
|
29
30
|
);
|
|
30
31
|
const NPM_BIN = process.env.MUSAFETY_NPM_BIN || 'npm';
|
|
32
|
+
const OPENSPEC_BIN = process.env.MUSAFETY_OPENSPEC_BIN || 'openspec';
|
|
31
33
|
const SCORECARD_BIN = process.env.MUSAFETY_SCORECARD_BIN || 'scorecard';
|
|
32
34
|
const GIT_PROTECTED_BRANCHES_KEY = 'multiagent.protectedBranches';
|
|
33
35
|
const GIT_BASE_BRANCH_KEY = 'multiagent.baseBranch';
|
|
@@ -35,6 +37,7 @@ const GIT_SYNC_STRATEGY_KEY = 'multiagent.sync.strategy';
|
|
|
35
37
|
const DEFAULT_PROTECTED_BRANCHES = ['dev', 'main', 'master'];
|
|
36
38
|
const DEFAULT_BASE_BRANCH = 'dev';
|
|
37
39
|
const DEFAULT_SYNC_STRATEGY = 'rebase';
|
|
40
|
+
const DEFAULT_SHADOW_CLEANUP_IDLE_MINUTES = 60;
|
|
38
41
|
|
|
39
42
|
const TEMPLATE_ROOT = path.resolve(__dirname, '..', 'templates');
|
|
40
43
|
|
|
@@ -47,12 +50,40 @@ const TEMPLATE_FILES = [
|
|
|
47
50
|
'scripts/agent-file-locks.py',
|
|
48
51
|
'scripts/install-agent-git-hooks.sh',
|
|
49
52
|
'scripts/openspec/init-plan-workspace.sh',
|
|
53
|
+
'scripts/openspec/init-change-workspace.sh',
|
|
50
54
|
'githooks/pre-commit',
|
|
51
55
|
'githooks/pre-push',
|
|
56
|
+
'githooks/post-merge',
|
|
52
57
|
'codex/skills/guardex/SKILL.md',
|
|
58
|
+
'codex/skills/guardex-merge-skills-to-dev/SKILL.md',
|
|
53
59
|
'claude/commands/guardex.md',
|
|
60
|
+
'github/pull.yml.example',
|
|
61
|
+
'github/workflows/cr.yml',
|
|
54
62
|
];
|
|
55
63
|
|
|
64
|
+
const REQUIRED_WORKFLOW_FILES = [
|
|
65
|
+
'scripts/agent-branch-start.sh',
|
|
66
|
+
'scripts/agent-branch-finish.sh',
|
|
67
|
+
'scripts/agent-worktree-prune.sh',
|
|
68
|
+
'scripts/agent-file-locks.py',
|
|
69
|
+
'scripts/install-agent-git-hooks.sh',
|
|
70
|
+
'.githooks/pre-commit',
|
|
71
|
+
'.githooks/post-merge',
|
|
72
|
+
'.omx/state/agent-file-locks.json',
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const REQUIRED_PACKAGE_SCRIPTS = {
|
|
76
|
+
'agent:branch:start': 'bash ./scripts/agent-branch-start.sh',
|
|
77
|
+
'agent:branch:finish': 'bash ./scripts/agent-branch-finish.sh',
|
|
78
|
+
'agent:cleanup': 'bash ./scripts/agent-worktree-prune.sh',
|
|
79
|
+
'agent:hooks:install': 'bash ./scripts/install-agent-git-hooks.sh',
|
|
80
|
+
'agent:locks:claim': 'python3 ./scripts/agent-file-locks.py claim',
|
|
81
|
+
'agent:locks:release': 'python3 ./scripts/agent-file-locks.py release',
|
|
82
|
+
'agent:locks:status': 'python3 ./scripts/agent-file-locks.py status',
|
|
83
|
+
'agent:plan:init': 'bash ./scripts/openspec/init-plan-workspace.sh',
|
|
84
|
+
'agent:change:init': 'bash ./scripts/openspec/init-change-workspace.sh',
|
|
85
|
+
};
|
|
86
|
+
|
|
56
87
|
const EXECUTABLE_RELATIVE_PATHS = new Set([
|
|
57
88
|
'scripts/agent-branch-start.sh',
|
|
58
89
|
'scripts/agent-branch-finish.sh',
|
|
@@ -62,14 +93,17 @@ const EXECUTABLE_RELATIVE_PATHS = new Set([
|
|
|
62
93
|
'scripts/agent-file-locks.py',
|
|
63
94
|
'scripts/install-agent-git-hooks.sh',
|
|
64
95
|
'scripts/openspec/init-plan-workspace.sh',
|
|
96
|
+
'scripts/openspec/init-change-workspace.sh',
|
|
65
97
|
'.githooks/pre-commit',
|
|
66
98
|
'.githooks/pre-push',
|
|
99
|
+
'.githooks/post-merge',
|
|
67
100
|
]);
|
|
68
101
|
|
|
69
102
|
const CRITICAL_GUARDRAIL_PATHS = new Set([
|
|
70
103
|
'AGENTS.md',
|
|
71
104
|
'.githooks/pre-commit',
|
|
72
105
|
'.githooks/pre-push',
|
|
106
|
+
'.githooks/post-merge',
|
|
73
107
|
'scripts/agent-branch-start.sh',
|
|
74
108
|
'scripts/agent-branch-finish.sh',
|
|
75
109
|
'scripts/agent-worktree-prune.sh',
|
|
@@ -93,10 +127,13 @@ const MANAGED_GITIGNORE_PATHS = [
|
|
|
93
127
|
'scripts/agent-file-locks.py',
|
|
94
128
|
'scripts/install-agent-git-hooks.sh',
|
|
95
129
|
'scripts/openspec/init-plan-workspace.sh',
|
|
130
|
+
'scripts/openspec/init-change-workspace.sh',
|
|
96
131
|
'.githooks/pre-commit',
|
|
97
132
|
'.githooks/pre-push',
|
|
133
|
+
'.githooks/post-merge',
|
|
98
134
|
'oh-my-codex/',
|
|
99
135
|
'.codex/skills/guardex/SKILL.md',
|
|
136
|
+
'.codex/skills/guardex-merge-skills-to-dev/SKILL.md',
|
|
100
137
|
'.claude/commands/guardex.md',
|
|
101
138
|
LOCK_FILE_RELATIVE,
|
|
102
139
|
];
|
|
@@ -156,7 +193,7 @@ const CLI_COMMAND_DESCRIPTIONS = [
|
|
|
156
193
|
['copy-commands', 'Print setup checklist as executable commands only'],
|
|
157
194
|
['protect', 'Manage protected branches (list/add/remove/set/reset)'],
|
|
158
195
|
['sync', 'Check or sync agent branches with origin/<base>'],
|
|
159
|
-
['cleanup', 'Cleanup agent branches/worktrees (
|
|
196
|
+
['cleanup', 'Cleanup agent branches/worktrees (watch mode defaults to 60-minute idle threshold)'],
|
|
160
197
|
['agents', 'Start/stop repo-scoped review + cleanup bots'],
|
|
161
198
|
['install', 'Install templates/locks/hooks without running full setup (supports --no-gitignore)'],
|
|
162
199
|
['fix', 'Repair broken or missing guardrail files/config (supports --no-gitignore)'],
|
|
@@ -197,10 +234,10 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
|
|
|
197
234
|
bash scripts/codex-agent.sh "task" "agent-name"
|
|
198
235
|
bash scripts/agent-branch-start.sh "task" "agent-name"
|
|
199
236
|
python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
|
|
200
|
-
bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)"
|
|
237
|
+
bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)" --base dev --via-pr --wait-for-merge
|
|
201
238
|
- For every new user message/task, repeat the same cycle:
|
|
202
239
|
start isolated agent branch/worktree -> claim file locks -> implement/verify ->
|
|
203
|
-
finish via PR/merge cleanup with scripts/agent-branch-finish.sh.
|
|
240
|
+
finish via PR/merge cleanup into dev with scripts/agent-branch-finish.sh.
|
|
204
241
|
- Finished branches stay available by default for audit/follow-up.
|
|
205
242
|
Remove them explicitly when done:
|
|
206
243
|
gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
|
|
@@ -230,6 +267,25 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
|
|
|
230
267
|
|
|
231
268
|
11) Optional (GitHub remote cleanup): enable:
|
|
232
269
|
Settings -> General -> Pull Requests -> Automatically delete head branches
|
|
270
|
+
|
|
271
|
+
12) Optional (fork sync with Pull app):
|
|
272
|
+
cp .github/pull.yml.example .github/pull.yml
|
|
273
|
+
# then edit .github/pull.yml:
|
|
274
|
+
# - set rules[].base to your fork branch (main/master/dev)
|
|
275
|
+
# - set rules[].upstream to upstream-owner:branch
|
|
276
|
+
# install app: https://github.com/apps/pull
|
|
277
|
+
# validate config: https://pull.git.ci/check/<owner>/<repo>
|
|
278
|
+
|
|
279
|
+
13) Optional (PR review bot with cr-gpt GitHub App):
|
|
280
|
+
- install app: https://github.com/apps/cr-gpt
|
|
281
|
+
- in GitHub repo Settings -> Secrets and variables -> Actions -> Variables:
|
|
282
|
+
add OPENAI_API_KEY (your API key)
|
|
283
|
+
- the app reviews new/updated pull requests automatically
|
|
284
|
+
|
|
285
|
+
14) Optional: test PR review action workflow
|
|
286
|
+
- gx setup installs .github/workflows/cr.yml
|
|
287
|
+
- open or update a PR
|
|
288
|
+
- check Actions -> "Code Review" run logs + PR timeline comments
|
|
233
289
|
`;
|
|
234
290
|
|
|
235
291
|
const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
|
|
@@ -240,7 +296,7 @@ gx review --interval 30
|
|
|
240
296
|
bash scripts/codex-agent.sh "task" "agent-name"
|
|
241
297
|
bash scripts/agent-branch-start.sh "task" "agent-name"
|
|
242
298
|
python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
|
|
243
|
-
bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)"
|
|
299
|
+
bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)" --base dev --via-pr --wait-for-merge
|
|
244
300
|
gx finish --all
|
|
245
301
|
gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
|
|
246
302
|
bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
|
|
@@ -249,6 +305,7 @@ openspec update
|
|
|
249
305
|
gx protect add release staging
|
|
250
306
|
gx sync --check
|
|
251
307
|
gx sync
|
|
308
|
+
cp .github/pull.yml.example .github/pull.yml
|
|
252
309
|
`;
|
|
253
310
|
|
|
254
311
|
const SCORECARD_RISK_BY_CHECK = {
|
|
@@ -452,6 +509,9 @@ function toDestinationPath(relativeTemplatePath) {
|
|
|
452
509
|
if (relativeTemplatePath.startsWith('claude/')) {
|
|
453
510
|
return `.${relativeTemplatePath}`;
|
|
454
511
|
}
|
|
512
|
+
if (relativeTemplatePath.startsWith('github/')) {
|
|
513
|
+
return `.${relativeTemplatePath}`;
|
|
514
|
+
}
|
|
455
515
|
throw new Error(`Unsupported template path: ${relativeTemplatePath}`);
|
|
456
516
|
}
|
|
457
517
|
|
|
@@ -658,6 +718,7 @@ function ensurePackageScripts(repoRoot, dryRun) {
|
|
|
658
718
|
'agent:locks:release': 'python3 ./scripts/agent-file-locks.py release',
|
|
659
719
|
'agent:locks:status': 'python3 ./scripts/agent-file-locks.py status',
|
|
660
720
|
'agent:plan:init': 'bash ./scripts/openspec/init-plan-workspace.sh',
|
|
721
|
+
'agent:change:init': 'bash ./scripts/openspec/init-change-workspace.sh',
|
|
661
722
|
'agent:protect:list': `${SHORT_TOOL_NAME} protect list`,
|
|
662
723
|
'agent:branch:sync': `${SHORT_TOOL_NAME} sync`,
|
|
663
724
|
'agent:branch:sync:check': `${SHORT_TOOL_NAME} sync --check`,
|
|
@@ -669,7 +730,7 @@ function ensurePackageScripts(repoRoot, dryRun) {
|
|
|
669
730
|
|
|
670
731
|
pkg.scripts = pkg.scripts || {};
|
|
671
732
|
let changed = false;
|
|
672
|
-
for (const [key, value] of Object.entries(
|
|
733
|
+
for (const [key, value] of Object.entries(REQUIRED_PACKAGE_SCRIPTS)) {
|
|
673
734
|
if (pkg.scripts[key] !== value) {
|
|
674
735
|
pkg.scripts[key] = value;
|
|
675
736
|
changed = true;
|
|
@@ -782,8 +843,8 @@ function parseCommonArgs(rawArgs, defaults) {
|
|
|
782
843
|
|
|
783
844
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
784
845
|
const arg = rawArgs[index];
|
|
785
|
-
if (arg === '--target') {
|
|
786
|
-
options.target = rawArgs
|
|
846
|
+
if (arg === '--target' || arg === '-t') {
|
|
847
|
+
options.target = requireValue(rawArgs, index, '--target');
|
|
787
848
|
index += 1;
|
|
788
849
|
continue;
|
|
789
850
|
}
|
|
@@ -1593,7 +1654,7 @@ function parseAgentsArgs(rawArgs) {
|
|
|
1593
1654
|
subcommand,
|
|
1594
1655
|
reviewIntervalSeconds: 30,
|
|
1595
1656
|
cleanupIntervalSeconds: 60,
|
|
1596
|
-
idleMinutes:
|
|
1657
|
+
idleMinutes: DEFAULT_SHADOW_CLEANUP_IDLE_MINUTES,
|
|
1597
1658
|
};
|
|
1598
1659
|
|
|
1599
1660
|
for (let index = 0; index < rest.length; index += 1) {
|
|
@@ -2340,10 +2401,6 @@ function parseSyncArgs(rawArgs) {
|
|
|
2340
2401
|
throw new Error(`Unknown option: ${arg}`);
|
|
2341
2402
|
}
|
|
2342
2403
|
|
|
2343
|
-
if (!options.target) {
|
|
2344
|
-
throw new Error('--target requires a path value');
|
|
2345
|
-
}
|
|
2346
|
-
|
|
2347
2404
|
return options;
|
|
2348
2405
|
}
|
|
2349
2406
|
|
|
@@ -2356,10 +2413,12 @@ function parseCleanupArgs(rawArgs) {
|
|
|
2356
2413
|
forceDirty: false,
|
|
2357
2414
|
keepRemote: false,
|
|
2358
2415
|
keepCleanWorktrees: false,
|
|
2416
|
+
includePrMerged: false,
|
|
2359
2417
|
idleMinutes: 0,
|
|
2360
2418
|
watch: false,
|
|
2361
2419
|
intervalSeconds: 60,
|
|
2362
2420
|
once: false,
|
|
2421
|
+
maxBranches: 0,
|
|
2363
2422
|
};
|
|
2364
2423
|
|
|
2365
2424
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
@@ -2407,6 +2466,10 @@ function parseCleanupArgs(rawArgs) {
|
|
|
2407
2466
|
options.keepCleanWorktrees = true;
|
|
2408
2467
|
continue;
|
|
2409
2468
|
}
|
|
2469
|
+
if (arg === '--include-pr-merged') {
|
|
2470
|
+
options.includePrMerged = true;
|
|
2471
|
+
continue;
|
|
2472
|
+
}
|
|
2410
2473
|
if (arg === '--idle-minutes') {
|
|
2411
2474
|
const next = rawArgs[index + 1];
|
|
2412
2475
|
if (!next) {
|
|
@@ -2441,11 +2504,24 @@ function parseCleanupArgs(rawArgs) {
|
|
|
2441
2504
|
options.once = true;
|
|
2442
2505
|
continue;
|
|
2443
2506
|
}
|
|
2507
|
+
if (arg === '--max-branches') {
|
|
2508
|
+
const next = rawArgs[index + 1];
|
|
2509
|
+
if (!next) {
|
|
2510
|
+
throw new Error('--max-branches requires an integer value');
|
|
2511
|
+
}
|
|
2512
|
+
const parsed = Number.parseInt(next, 10);
|
|
2513
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
2514
|
+
throw new Error('--max-branches must be an integer >= 1');
|
|
2515
|
+
}
|
|
2516
|
+
options.maxBranches = parsed;
|
|
2517
|
+
index += 1;
|
|
2518
|
+
continue;
|
|
2519
|
+
}
|
|
2444
2520
|
throw new Error(`Unknown option: ${arg}`);
|
|
2445
2521
|
}
|
|
2446
2522
|
|
|
2447
2523
|
if (options.watch && options.idleMinutes === 0) {
|
|
2448
|
-
options.idleMinutes =
|
|
2524
|
+
options.idleMinutes = DEFAULT_SHADOW_CLEANUP_IDLE_MINUTES;
|
|
2449
2525
|
}
|
|
2450
2526
|
|
|
2451
2527
|
return options;
|
|
@@ -2739,27 +2815,16 @@ function branchExists(repoRoot, branch) {
|
|
|
2739
2815
|
return result.status === 0;
|
|
2740
2816
|
}
|
|
2741
2817
|
|
|
2742
|
-
function resolveFinishBaseBranch(repoRoot,
|
|
2818
|
+
function resolveFinishBaseBranch(repoRoot, _sourceBranch, explicitBase) {
|
|
2743
2819
|
if (explicitBase) {
|
|
2744
2820
|
return explicitBase;
|
|
2745
2821
|
}
|
|
2746
2822
|
|
|
2747
|
-
const branchSpecific = readGitConfig(repoRoot, `branch.${sourceBranch}.musafetyBase`);
|
|
2748
|
-
if (branchSpecific) {
|
|
2749
|
-
return branchSpecific;
|
|
2750
|
-
}
|
|
2751
|
-
|
|
2752
2823
|
const configured = readGitConfig(repoRoot, GIT_BASE_BRANCH_KEY);
|
|
2753
2824
|
if (configured) {
|
|
2754
2825
|
return configured;
|
|
2755
2826
|
}
|
|
2756
2827
|
|
|
2757
|
-
const current = gitRun(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD'], { allowFailure: true });
|
|
2758
|
-
const currentBranch = String(current.stdout || '').trim();
|
|
2759
|
-
if (current.status === 0 && currentBranch && currentBranch !== 'HEAD' && !currentBranch.startsWith('agent/')) {
|
|
2760
|
-
return currentBranch;
|
|
2761
|
-
}
|
|
2762
|
-
|
|
2763
2828
|
return DEFAULT_BASE_BRANCH;
|
|
2764
2829
|
}
|
|
2765
2830
|
|
|
@@ -3049,6 +3114,95 @@ function maybeSelfUpdateBeforeStatus() {
|
|
|
3049
3114
|
console.log(`[${TOOL_NAME}] ✅ Updated to latest published version.`);
|
|
3050
3115
|
}
|
|
3051
3116
|
|
|
3117
|
+
function checkForOpenSpecPackageUpdate() {
|
|
3118
|
+
if (envFlagEnabled('MUSAFETY_SKIP_OPENSPEC_UPDATE_CHECK')) {
|
|
3119
|
+
return { checked: false, reason: 'disabled' };
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
const forceCheck = envFlagEnabled('MUSAFETY_FORCE_OPENSPEC_UPDATE_CHECK');
|
|
3123
|
+
if (!forceCheck && !isInteractiveTerminal()) {
|
|
3124
|
+
return { checked: false, reason: 'non-interactive' };
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
const detection = detectGlobalToolchainPackages();
|
|
3128
|
+
if (!detection.ok) {
|
|
3129
|
+
return { checked: false, reason: 'package-detect-failed' };
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
const current = String((detection.installedVersions || {})[OPENSPEC_PACKAGE] || '').trim();
|
|
3133
|
+
if (!current) {
|
|
3134
|
+
return { checked: false, reason: 'not-installed' };
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
const latestResult = run(NPM_BIN, ['view', OPENSPEC_PACKAGE, 'version', '--json'], { timeout: 5000 });
|
|
3138
|
+
if (latestResult.status !== 0) {
|
|
3139
|
+
return { checked: false, reason: 'lookup-failed' };
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
const latest = parseNpmVersionOutput(latestResult.stdout);
|
|
3143
|
+
if (!latest) {
|
|
3144
|
+
return { checked: false, reason: 'invalid-latest-version' };
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
return {
|
|
3148
|
+
checked: true,
|
|
3149
|
+
current,
|
|
3150
|
+
latest,
|
|
3151
|
+
updateAvailable: isNewerVersion(latest, current),
|
|
3152
|
+
};
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
function printOpenSpecUpdateAvailableBanner(current, latest) {
|
|
3156
|
+
const title = colorize('OPENSPEC UPDATE AVAILABLE', '1;33');
|
|
3157
|
+
console.log(`[${TOOL_NAME}] ${title}`);
|
|
3158
|
+
console.log(`[${TOOL_NAME}] Current: ${current}`);
|
|
3159
|
+
console.log(`[${TOOL_NAME}] Latest : ${latest}`);
|
|
3160
|
+
console.log(`[${TOOL_NAME}] Command: ${NPM_BIN} i -g ${OPENSPEC_PACKAGE}@latest`);
|
|
3161
|
+
console.log(`[${TOOL_NAME}] Then : ${OPENSPEC_BIN} update`);
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
function maybeOpenSpecUpdateBeforeStatus() {
|
|
3165
|
+
const check = checkForOpenSpecPackageUpdate();
|
|
3166
|
+
if (!check.checked || !check.updateAvailable) {
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
printOpenSpecUpdateAvailableBanner(check.current, check.latest);
|
|
3171
|
+
|
|
3172
|
+
const autoApproval = parseAutoApproval('MUSAFETY_AUTO_OPENSPEC_UPDATE_APPROVAL');
|
|
3173
|
+
const interactive = isInteractiveTerminal();
|
|
3174
|
+
|
|
3175
|
+
if (!interactive && autoApproval == null) {
|
|
3176
|
+
console.log(`[${TOOL_NAME}] Non-interactive shell; skipping OpenSpec update prompt.`);
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
const shouldUpdate = interactive
|
|
3181
|
+
? promptYesNoStrict(
|
|
3182
|
+
`Update OpenSpec now? (${NPM_BIN} i -g ${OPENSPEC_PACKAGE}@latest && ${OPENSPEC_BIN} update)`,
|
|
3183
|
+
)
|
|
3184
|
+
: autoApproval;
|
|
3185
|
+
|
|
3186
|
+
if (!shouldUpdate) {
|
|
3187
|
+
console.log(`[${TOOL_NAME}] Skipped OpenSpec update.`);
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
const installResult = run(NPM_BIN, ['i', '-g', `${OPENSPEC_PACKAGE}@latest`], { stdio: 'inherit' });
|
|
3192
|
+
if (installResult.status !== 0) {
|
|
3193
|
+
console.log(`[${TOOL_NAME}] ⚠️ OpenSpec npm install failed. You can retry manually.`);
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
const toolUpdateResult = run(OPENSPEC_BIN, ['update'], { stdio: 'inherit' });
|
|
3198
|
+
if (toolUpdateResult.status !== 0) {
|
|
3199
|
+
console.log(`[${TOOL_NAME}] ⚠️ OpenSpec tool update failed. Run '${OPENSPEC_BIN} update' manually.`);
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
console.log(`[${TOOL_NAME}] ✅ OpenSpec updated to latest package and tool plugins refreshed.`);
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3052
3206
|
function promptYesNoStrict(question) {
|
|
3053
3207
|
while (true) {
|
|
3054
3208
|
process.stdout.write(`${question} [y/n] `);
|
|
@@ -3113,15 +3267,21 @@ function detectGlobalToolchainPackages() {
|
|
|
3113
3267
|
|
|
3114
3268
|
const installed = [];
|
|
3115
3269
|
const missing = [];
|
|
3270
|
+
const installedVersions = {};
|
|
3116
3271
|
for (const pkg of GLOBAL_TOOLCHAIN_PACKAGES) {
|
|
3117
3272
|
if (installedSet.has(pkg)) {
|
|
3118
3273
|
installed.push(pkg);
|
|
3274
|
+
const rawVersion = dependencyMap[pkg] && dependencyMap[pkg].version;
|
|
3275
|
+
const version = String(rawVersion || '').trim();
|
|
3276
|
+
if (version) {
|
|
3277
|
+
installedVersions[pkg] = version;
|
|
3278
|
+
}
|
|
3119
3279
|
} else {
|
|
3120
3280
|
missing.push(pkg);
|
|
3121
3281
|
}
|
|
3122
3282
|
}
|
|
3123
3283
|
|
|
3124
|
-
return { ok: true, installed, missing };
|
|
3284
|
+
return { ok: true, installed, missing, installedVersions };
|
|
3125
3285
|
}
|
|
3126
3286
|
|
|
3127
3287
|
function detectRequiredSystemTools() {
|
|
@@ -3910,37 +4070,60 @@ function agents(rawArgs) {
|
|
|
3910
4070
|
return;
|
|
3911
4071
|
}
|
|
3912
4072
|
|
|
3913
|
-
if (reviewRunning) {
|
|
3914
|
-
stopAgentProcessByPid(existingReviewPid, 'review-bot-watch.sh');
|
|
3915
|
-
}
|
|
3916
|
-
if (cleanupRunning) {
|
|
3917
|
-
stopAgentProcessByPid(existingCleanupPid, `${path.basename(__filename)} cleanup`);
|
|
3918
|
-
}
|
|
3919
|
-
|
|
3920
4073
|
const reviewLogPath = path.join(repoRoot, '.omx', 'logs', 'agent-review.log');
|
|
3921
4074
|
const cleanupLogPath = path.join(repoRoot, '.omx', 'logs', 'agent-cleanup.log');
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
4075
|
+
|
|
4076
|
+
let reviewPid = existingReviewPid;
|
|
4077
|
+
let cleanupPid = existingCleanupPid;
|
|
4078
|
+
let startedAny = false;
|
|
4079
|
+
let reusedAny = false;
|
|
4080
|
+
|
|
4081
|
+
if (!reviewRunning) {
|
|
4082
|
+
reviewPid = spawnDetachedAgentProcess({
|
|
4083
|
+
command: 'bash',
|
|
4084
|
+
args: [reviewScriptPath, '--interval', String(options.reviewIntervalSeconds)],
|
|
4085
|
+
cwd: repoRoot,
|
|
4086
|
+
logPath: reviewLogPath,
|
|
4087
|
+
});
|
|
4088
|
+
startedAny = true;
|
|
4089
|
+
} else {
|
|
4090
|
+
reusedAny = true;
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
if (!cleanupRunning) {
|
|
4094
|
+
cleanupPid = spawnDetachedAgentProcess({
|
|
4095
|
+
command: process.execPath,
|
|
4096
|
+
args: [
|
|
4097
|
+
path.resolve(__filename),
|
|
4098
|
+
'cleanup',
|
|
4099
|
+
'--target',
|
|
4100
|
+
repoRoot,
|
|
4101
|
+
'--watch',
|
|
4102
|
+
'--interval',
|
|
4103
|
+
String(options.cleanupIntervalSeconds),
|
|
4104
|
+
'--idle-minutes',
|
|
4105
|
+
String(options.idleMinutes),
|
|
4106
|
+
],
|
|
4107
|
+
cwd: repoRoot,
|
|
4108
|
+
logPath: cleanupLogPath,
|
|
4109
|
+
});
|
|
4110
|
+
startedAny = true;
|
|
4111
|
+
} else {
|
|
4112
|
+
reusedAny = true;
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4115
|
+
const priorReviewInterval = Number.parseInt(String(existingState?.review?.intervalSeconds || ''), 10);
|
|
4116
|
+
const priorCleanupInterval = Number.parseInt(String(existingState?.cleanup?.intervalSeconds || ''), 10);
|
|
4117
|
+
const priorIdleMinutes = Number.parseInt(String(existingState?.cleanup?.idleMinutes || ''), 10);
|
|
4118
|
+
const reviewIntervalSeconds = reviewRunning && Number.isInteger(priorReviewInterval) && priorReviewInterval >= 5
|
|
4119
|
+
? priorReviewInterval
|
|
4120
|
+
: options.reviewIntervalSeconds;
|
|
4121
|
+
const cleanupIntervalSeconds = cleanupRunning && Number.isInteger(priorCleanupInterval) && priorCleanupInterval >= 5
|
|
4122
|
+
? priorCleanupInterval
|
|
4123
|
+
: options.cleanupIntervalSeconds;
|
|
4124
|
+
const idleMinutes = cleanupRunning && Number.isInteger(priorIdleMinutes) && priorIdleMinutes >= 1
|
|
4125
|
+
? priorIdleMinutes
|
|
4126
|
+
: options.idleMinutes;
|
|
3944
4127
|
|
|
3945
4128
|
writeAgentsState(repoRoot, {
|
|
3946
4129
|
schemaVersion: 1,
|
|
@@ -3948,14 +4131,14 @@ function agents(rawArgs) {
|
|
|
3948
4131
|
startedAt: new Date().toISOString(),
|
|
3949
4132
|
review: {
|
|
3950
4133
|
pid: reviewPid,
|
|
3951
|
-
intervalSeconds:
|
|
4134
|
+
intervalSeconds: reviewIntervalSeconds,
|
|
3952
4135
|
script: reviewScriptPath,
|
|
3953
4136
|
logPath: reviewLogPath,
|
|
3954
4137
|
},
|
|
3955
4138
|
cleanup: {
|
|
3956
4139
|
pid: cleanupPid,
|
|
3957
|
-
intervalSeconds:
|
|
3958
|
-
idleMinutes
|
|
4140
|
+
intervalSeconds: cleanupIntervalSeconds,
|
|
4141
|
+
idleMinutes,
|
|
3959
4142
|
script: path.resolve(__filename),
|
|
3960
4143
|
logPath: cleanupLogPath,
|
|
3961
4144
|
},
|
|
@@ -3964,6 +4147,9 @@ function agents(rawArgs) {
|
|
|
3964
4147
|
console.log(
|
|
3965
4148
|
`[${TOOL_NAME}] Started repo agents in ${repoRoot} (review pid=${reviewPid}, cleanup pid=${cleanupPid}).`,
|
|
3966
4149
|
);
|
|
4150
|
+
if (reusedAny && startedAny) {
|
|
4151
|
+
console.log(`[${TOOL_NAME}] Reused healthy bot process(es) and started only missing ones.`);
|
|
4152
|
+
}
|
|
3967
4153
|
console.log(`[${TOOL_NAME}] Logs: ${reviewLogPath}, ${cleanupLogPath}`);
|
|
3968
4154
|
process.exitCode = 0;
|
|
3969
4155
|
return;
|
|
@@ -4245,6 +4431,238 @@ function release(rawArgs) {
|
|
|
4245
4431
|
process.exitCode = 0;
|
|
4246
4432
|
}
|
|
4247
4433
|
|
|
4434
|
+
function installMany(rawArgs) {
|
|
4435
|
+
const options = parseInstallManyArgs(rawArgs);
|
|
4436
|
+
const targets = collectInstallManyTargets(options);
|
|
4437
|
+
|
|
4438
|
+
if (!targets.length) {
|
|
4439
|
+
throw new Error('install-many did not find any targets to process.');
|
|
4440
|
+
}
|
|
4441
|
+
|
|
4442
|
+
if (options.usedImplicitWorkspaceDefault) {
|
|
4443
|
+
console.log(
|
|
4444
|
+
`[multiagent-safety] No explicit targets provided. Defaulting to workspace scan: ${path.resolve(
|
|
4445
|
+
options.workspace,
|
|
4446
|
+
)} (max depth ${options.maxDepth})`,
|
|
4447
|
+
);
|
|
4448
|
+
}
|
|
4449
|
+
|
|
4450
|
+
console.log(
|
|
4451
|
+
`[multiagent-safety] install-many starting for ${targets.length} target path(s)${
|
|
4452
|
+
options.dryRun ? ' [dry-run]' : ''
|
|
4453
|
+
}`,
|
|
4454
|
+
);
|
|
4455
|
+
|
|
4456
|
+
let installed = 0;
|
|
4457
|
+
let duplicateRepos = 0;
|
|
4458
|
+
const seenRepoRoots = new Set();
|
|
4459
|
+
const failures = [];
|
|
4460
|
+
|
|
4461
|
+
for (const targetPath of targets) {
|
|
4462
|
+
let repoRoot;
|
|
4463
|
+
try {
|
|
4464
|
+
repoRoot = resolveRepoRoot(targetPath);
|
|
4465
|
+
} catch (error) {
|
|
4466
|
+
failures.push({ target: targetPath, message: error.message });
|
|
4467
|
+
if (options.failFast) {
|
|
4468
|
+
break;
|
|
4469
|
+
}
|
|
4470
|
+
continue;
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4473
|
+
if (seenRepoRoots.has(repoRoot)) {
|
|
4474
|
+
duplicateRepos += 1;
|
|
4475
|
+
console.log(`[multiagent-safety] Skipping duplicate repo target: ${targetPath} -> ${repoRoot}`);
|
|
4476
|
+
continue;
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
seenRepoRoots.add(repoRoot);
|
|
4480
|
+
|
|
4481
|
+
try {
|
|
4482
|
+
const report = installIntoRepoRoot(repoRoot, options);
|
|
4483
|
+
printInstallReport(report);
|
|
4484
|
+
installed += 1;
|
|
4485
|
+
} catch (error) {
|
|
4486
|
+
failures.push({ target: repoRoot, message: error.message });
|
|
4487
|
+
if (options.failFast) {
|
|
4488
|
+
break;
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4493
|
+
console.log(
|
|
4494
|
+
`[multiagent-safety] install-many summary: installed=${installed}, failures=${failures.length}, duplicate-targets=${duplicateRepos}`,
|
|
4495
|
+
);
|
|
4496
|
+
|
|
4497
|
+
if (failures.length > 0) {
|
|
4498
|
+
console.error('[multiagent-safety] Failed targets:');
|
|
4499
|
+
for (const failure of failures) {
|
|
4500
|
+
console.error(` - ${failure.target}`);
|
|
4501
|
+
console.error(` ${failure.message}`);
|
|
4502
|
+
}
|
|
4503
|
+
throw new Error(`install-many completed with ${failures.length} failure(s)`);
|
|
4504
|
+
}
|
|
4505
|
+
|
|
4506
|
+
if (options.dryRun) {
|
|
4507
|
+
console.log('[multiagent-safety] Dry run complete. No files were modified.');
|
|
4508
|
+
} else {
|
|
4509
|
+
console.log('[multiagent-safety] Installed multi-agent safety workflow across all targets.');
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
function initWorkspace(rawArgs) {
|
|
4514
|
+
const options = parseInitWorkspaceArgs(rawArgs);
|
|
4515
|
+
const resolvedWorkspace = path.resolve(options.workspace);
|
|
4516
|
+
const repos = discoverGitRepos(resolvedWorkspace, options.maxDepth)
|
|
4517
|
+
.map((repoPath) => path.resolve(repoPath))
|
|
4518
|
+
.sort();
|
|
4519
|
+
|
|
4520
|
+
const outputPath = options.output
|
|
4521
|
+
? path.resolve(options.output)
|
|
4522
|
+
: path.join(resolvedWorkspace, DEFAULT_WORKSPACE_TARGETS_FILE);
|
|
4523
|
+
|
|
4524
|
+
if (fs.existsSync(outputPath) && !options.force) {
|
|
4525
|
+
throw new Error(`Refusing to overwrite existing file without --force: ${outputPath}`);
|
|
4526
|
+
}
|
|
4527
|
+
|
|
4528
|
+
const headerLines = [
|
|
4529
|
+
'# multiagent-safety workspace targets',
|
|
4530
|
+
`# generated: ${new Date().toISOString()}`,
|
|
4531
|
+
`# workspace: ${resolvedWorkspace}`,
|
|
4532
|
+
`# max-depth: ${options.maxDepth}`,
|
|
4533
|
+
'#',
|
|
4534
|
+
'# Run:',
|
|
4535
|
+
`# multiagent-safety install-many --targets-file "${outputPath}"`,
|
|
4536
|
+
'',
|
|
4537
|
+
];
|
|
4538
|
+
const content = `${headerLines.join('\n')}${repos.join('\n')}${repos.length ? '\n' : ''}`;
|
|
4539
|
+
|
|
4540
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
4541
|
+
fs.writeFileSync(outputPath, content, 'utf8');
|
|
4542
|
+
|
|
4543
|
+
console.log(`[multiagent-safety] Workspace target file written: ${outputPath}`);
|
|
4544
|
+
console.log(`[multiagent-safety] Repos discovered: ${repos.length}`);
|
|
4545
|
+
if (repos.length === 0) {
|
|
4546
|
+
console.log('[multiagent-safety] No git repos found. You can add target paths manually to the file.');
|
|
4547
|
+
} else {
|
|
4548
|
+
console.log(`[multiagent-safety] Next step: multiagent-safety install-many --targets-file "${outputPath}"`);
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
function doctor(rawArgs) {
|
|
4553
|
+
const options = parseDoctorArgs(rawArgs);
|
|
4554
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
4555
|
+
const failures = [];
|
|
4556
|
+
const warnings = [];
|
|
4557
|
+
|
|
4558
|
+
function ok(message) {
|
|
4559
|
+
console.log(` [ok] ${message}`);
|
|
4560
|
+
}
|
|
4561
|
+
function warn(message) {
|
|
4562
|
+
warnings.push(message);
|
|
4563
|
+
console.log(` [warn] ${message}`);
|
|
4564
|
+
}
|
|
4565
|
+
function fail(message) {
|
|
4566
|
+
failures.push(message);
|
|
4567
|
+
console.log(` [fail] ${message}`);
|
|
4568
|
+
}
|
|
4569
|
+
|
|
4570
|
+
console.log(`[multiagent-safety] doctor target: ${repoRoot}`);
|
|
4571
|
+
|
|
4572
|
+
const hooksPath = run('git', ['-C', repoRoot, 'config', '--get', 'core.hooksPath']);
|
|
4573
|
+
if (hooksPath.status !== 0) {
|
|
4574
|
+
fail('git core.hooksPath is not configured');
|
|
4575
|
+
} else if (hooksPath.stdout.trim() !== '.githooks') {
|
|
4576
|
+
fail(`git core.hooksPath is "${hooksPath.stdout.trim()}" (expected ".githooks")`);
|
|
4577
|
+
} else {
|
|
4578
|
+
ok('git core.hooksPath is .githooks');
|
|
4579
|
+
}
|
|
4580
|
+
|
|
4581
|
+
for (const relativePath of REQUIRED_WORKFLOW_FILES) {
|
|
4582
|
+
const absolutePath = path.join(repoRoot, relativePath);
|
|
4583
|
+
if (!fs.existsSync(absolutePath)) {
|
|
4584
|
+
fail(`missing ${relativePath}`);
|
|
4585
|
+
continue;
|
|
4586
|
+
}
|
|
4587
|
+
ok(`found ${relativePath}`);
|
|
4588
|
+
|
|
4589
|
+
if (EXECUTABLE_RELATIVE_PATHS.has(relativePath)) {
|
|
4590
|
+
try {
|
|
4591
|
+
fs.accessSync(absolutePath, fs.constants.X_OK);
|
|
4592
|
+
} catch {
|
|
4593
|
+
fail(`${relativePath} exists but is not executable`);
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
}
|
|
4597
|
+
|
|
4598
|
+
const lockFilePath = path.join(repoRoot, '.omx/state/agent-file-locks.json');
|
|
4599
|
+
if (fs.existsSync(lockFilePath)) {
|
|
4600
|
+
try {
|
|
4601
|
+
const parsed = JSON.parse(fs.readFileSync(lockFilePath, 'utf8'));
|
|
4602
|
+
if (!parsed || typeof parsed !== 'object' || typeof parsed.locks !== 'object') {
|
|
4603
|
+
fail('.omx/state/agent-file-locks.json does not contain a valid { locks: {} } object');
|
|
4604
|
+
} else {
|
|
4605
|
+
ok('lock registry JSON is valid');
|
|
4606
|
+
}
|
|
4607
|
+
} catch (error) {
|
|
4608
|
+
fail(`lock registry JSON is invalid: ${error.message}`);
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
|
|
4612
|
+
const packagePath = path.join(repoRoot, 'package.json');
|
|
4613
|
+
if (!fs.existsSync(packagePath)) {
|
|
4614
|
+
warn('package.json not found (npm helper scripts cannot be verified)');
|
|
4615
|
+
} else {
|
|
4616
|
+
try {
|
|
4617
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
4618
|
+
const scripts = pkg.scripts || {};
|
|
4619
|
+
for (const [name, expectedValue] of Object.entries(REQUIRED_PACKAGE_SCRIPTS)) {
|
|
4620
|
+
if (scripts[name] !== expectedValue) {
|
|
4621
|
+
fail(`package.json script mismatch for "${name}"`);
|
|
4622
|
+
} else {
|
|
4623
|
+
ok(`package.json script "${name}" is configured`);
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
} catch (error) {
|
|
4627
|
+
fail(`package.json is invalid JSON: ${error.message}`);
|
|
4628
|
+
}
|
|
4629
|
+
}
|
|
4630
|
+
|
|
4631
|
+
const agentsPath = path.join(repoRoot, 'AGENTS.md');
|
|
4632
|
+
if (!fs.existsSync(agentsPath)) {
|
|
4633
|
+
warn('AGENTS.md not found (multi-agent contract snippet not present)');
|
|
4634
|
+
} else {
|
|
4635
|
+
const agentsContent = fs.readFileSync(agentsPath, 'utf8');
|
|
4636
|
+
if (!agentsContent.includes(AGENTS_MARKER_START)) {
|
|
4637
|
+
warn('AGENTS.md exists but multiagent-safety snippet marker is missing');
|
|
4638
|
+
} else {
|
|
4639
|
+
ok('AGENTS.md contains multiagent-safety snippet marker');
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4643
|
+
if (warnings.length) {
|
|
4644
|
+
console.log(`[multiagent-safety] warnings: ${warnings.length}`);
|
|
4645
|
+
}
|
|
4646
|
+
if (failures.length) {
|
|
4647
|
+
console.log(`[multiagent-safety] failures: ${failures.length}`);
|
|
4648
|
+
}
|
|
4649
|
+
|
|
4650
|
+
if (failures.length === 0 && (!options.strict || warnings.length === 0)) {
|
|
4651
|
+
console.log('[multiagent-safety] doctor passed.');
|
|
4652
|
+
if (warnings.length > 0) {
|
|
4653
|
+
console.log('[multiagent-safety] tip: run with --strict to treat warnings as failures.');
|
|
4654
|
+
}
|
|
4655
|
+
return;
|
|
4656
|
+
}
|
|
4657
|
+
|
|
4658
|
+
if (options.strict && warnings.length > 0 && failures.length === 0) {
|
|
4659
|
+
console.log('[multiagent-safety] strict mode failed due to warnings.');
|
|
4660
|
+
} else {
|
|
4661
|
+
console.log('[multiagent-safety] doctor failed.');
|
|
4662
|
+
}
|
|
4663
|
+
throw new Error('doctor detected configuration issues');
|
|
4664
|
+
}
|
|
4665
|
+
|
|
4248
4666
|
function printAgentsSnippet() {
|
|
4249
4667
|
const snippetPath = path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md');
|
|
4250
4668
|
process.stdout.write(fs.readFileSync(snippetPath, 'utf8'));
|
|
@@ -4284,9 +4702,15 @@ function cleanup(rawArgs) {
|
|
|
4284
4702
|
if (!options.keepCleanWorktrees) {
|
|
4285
4703
|
args.push('--only-dirty-worktrees');
|
|
4286
4704
|
}
|
|
4705
|
+
if (options.includePrMerged) {
|
|
4706
|
+
args.push('--include-pr-merged');
|
|
4707
|
+
}
|
|
4287
4708
|
if (options.idleMinutes > 0) {
|
|
4288
4709
|
args.push('--idle-minutes', String(options.idleMinutes));
|
|
4289
4710
|
}
|
|
4711
|
+
if (options.maxBranches > 0) {
|
|
4712
|
+
args.push('--max-branches', String(options.maxBranches));
|
|
4713
|
+
}
|
|
4290
4714
|
args.push('--delete-branches');
|
|
4291
4715
|
if (!options.keepRemote) {
|
|
4292
4716
|
args.push('--delete-remote-branches');
|
|
@@ -4304,7 +4728,7 @@ function cleanup(rawArgs) {
|
|
|
4304
4728
|
while (true) {
|
|
4305
4729
|
cycle += 1;
|
|
4306
4730
|
console.log(
|
|
4307
|
-
`[${TOOL_NAME}] Cleanup watch cycle=${cycle} (interval=${options.intervalSeconds}s, idleMinutes=${options.idleMinutes}).`,
|
|
4731
|
+
`[${TOOL_NAME}] Cleanup watch cycle=${cycle} (interval=${options.intervalSeconds}s, idleMinutes=${options.idleMinutes}, maxBranches=${options.maxBranches > 0 ? options.maxBranches : "unbounded"}).`,
|
|
4308
4732
|
);
|
|
4309
4733
|
runCleanupCycle();
|
|
4310
4734
|
if (options.once) {
|
|
@@ -4737,6 +5161,7 @@ function main() {
|
|
|
4737
5161
|
|
|
4738
5162
|
if (args.length === 0) {
|
|
4739
5163
|
maybeSelfUpdateBeforeStatus();
|
|
5164
|
+
maybeOpenSpecUpdateBeforeStatus();
|
|
4740
5165
|
status([]);
|
|
4741
5166
|
return;
|
|
4742
5167
|
}
|