@imdeadpool/guardex 5.0.12 → 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 +10 -4
- package/bin/multiagent-safety.js +458 -60
- package/package.json +1 -1
- package/templates/AGENTS.multiagent-safety.md +18 -6
- package/templates/githooks/post-merge +43 -0
- package/templates/githooks/pre-commit +24 -15
- package/templates/githooks/pre-push +3 -3
- 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,8 +50,10 @@ 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',
|
|
53
58
|
'codex/skills/guardex-merge-skills-to-dev/SKILL.md',
|
|
54
59
|
'claude/commands/guardex.md',
|
|
@@ -56,6 +61,29 @@ const TEMPLATE_FILES = [
|
|
|
56
61
|
'github/workflows/cr.yml',
|
|
57
62
|
];
|
|
58
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
|
+
|
|
59
87
|
const EXECUTABLE_RELATIVE_PATHS = new Set([
|
|
60
88
|
'scripts/agent-branch-start.sh',
|
|
61
89
|
'scripts/agent-branch-finish.sh',
|
|
@@ -65,14 +93,17 @@ const EXECUTABLE_RELATIVE_PATHS = new Set([
|
|
|
65
93
|
'scripts/agent-file-locks.py',
|
|
66
94
|
'scripts/install-agent-git-hooks.sh',
|
|
67
95
|
'scripts/openspec/init-plan-workspace.sh',
|
|
96
|
+
'scripts/openspec/init-change-workspace.sh',
|
|
68
97
|
'.githooks/pre-commit',
|
|
69
98
|
'.githooks/pre-push',
|
|
99
|
+
'.githooks/post-merge',
|
|
70
100
|
]);
|
|
71
101
|
|
|
72
102
|
const CRITICAL_GUARDRAIL_PATHS = new Set([
|
|
73
103
|
'AGENTS.md',
|
|
74
104
|
'.githooks/pre-commit',
|
|
75
105
|
'.githooks/pre-push',
|
|
106
|
+
'.githooks/post-merge',
|
|
76
107
|
'scripts/agent-branch-start.sh',
|
|
77
108
|
'scripts/agent-branch-finish.sh',
|
|
78
109
|
'scripts/agent-worktree-prune.sh',
|
|
@@ -96,8 +127,10 @@ const MANAGED_GITIGNORE_PATHS = [
|
|
|
96
127
|
'scripts/agent-file-locks.py',
|
|
97
128
|
'scripts/install-agent-git-hooks.sh',
|
|
98
129
|
'scripts/openspec/init-plan-workspace.sh',
|
|
130
|
+
'scripts/openspec/init-change-workspace.sh',
|
|
99
131
|
'.githooks/pre-commit',
|
|
100
132
|
'.githooks/pre-push',
|
|
133
|
+
'.githooks/post-merge',
|
|
101
134
|
'oh-my-codex/',
|
|
102
135
|
'.codex/skills/guardex/SKILL.md',
|
|
103
136
|
'.codex/skills/guardex-merge-skills-to-dev/SKILL.md',
|
|
@@ -160,7 +193,7 @@ const CLI_COMMAND_DESCRIPTIONS = [
|
|
|
160
193
|
['copy-commands', 'Print setup checklist as executable commands only'],
|
|
161
194
|
['protect', 'Manage protected branches (list/add/remove/set/reset)'],
|
|
162
195
|
['sync', 'Check or sync agent branches with origin/<base>'],
|
|
163
|
-
['cleanup', 'Cleanup agent branches/worktrees (
|
|
196
|
+
['cleanup', 'Cleanup agent branches/worktrees (watch mode defaults to 60-minute idle threshold)'],
|
|
164
197
|
['agents', 'Start/stop repo-scoped review + cleanup bots'],
|
|
165
198
|
['install', 'Install templates/locks/hooks without running full setup (supports --no-gitignore)'],
|
|
166
199
|
['fix', 'Repair broken or missing guardrail files/config (supports --no-gitignore)'],
|
|
@@ -201,10 +234,10 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
|
|
|
201
234
|
bash scripts/codex-agent.sh "task" "agent-name"
|
|
202
235
|
bash scripts/agent-branch-start.sh "task" "agent-name"
|
|
203
236
|
python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
|
|
204
|
-
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
|
|
205
238
|
- For every new user message/task, repeat the same cycle:
|
|
206
239
|
start isolated agent branch/worktree -> claim file locks -> implement/verify ->
|
|
207
|
-
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.
|
|
208
241
|
- Finished branches stay available by default for audit/follow-up.
|
|
209
242
|
Remove them explicitly when done:
|
|
210
243
|
gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
|
|
@@ -263,7 +296,7 @@ gx review --interval 30
|
|
|
263
296
|
bash scripts/codex-agent.sh "task" "agent-name"
|
|
264
297
|
bash scripts/agent-branch-start.sh "task" "agent-name"
|
|
265
298
|
python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
|
|
266
|
-
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
|
|
267
300
|
gx finish --all
|
|
268
301
|
gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
|
|
269
302
|
bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
|
|
@@ -685,6 +718,7 @@ function ensurePackageScripts(repoRoot, dryRun) {
|
|
|
685
718
|
'agent:locks:release': 'python3 ./scripts/agent-file-locks.py release',
|
|
686
719
|
'agent:locks:status': 'python3 ./scripts/agent-file-locks.py status',
|
|
687
720
|
'agent:plan:init': 'bash ./scripts/openspec/init-plan-workspace.sh',
|
|
721
|
+
'agent:change:init': 'bash ./scripts/openspec/init-change-workspace.sh',
|
|
688
722
|
'agent:protect:list': `${SHORT_TOOL_NAME} protect list`,
|
|
689
723
|
'agent:branch:sync': `${SHORT_TOOL_NAME} sync`,
|
|
690
724
|
'agent:branch:sync:check': `${SHORT_TOOL_NAME} sync --check`,
|
|
@@ -696,7 +730,7 @@ function ensurePackageScripts(repoRoot, dryRun) {
|
|
|
696
730
|
|
|
697
731
|
pkg.scripts = pkg.scripts || {};
|
|
698
732
|
let changed = false;
|
|
699
|
-
for (const [key, value] of Object.entries(
|
|
733
|
+
for (const [key, value] of Object.entries(REQUIRED_PACKAGE_SCRIPTS)) {
|
|
700
734
|
if (pkg.scripts[key] !== value) {
|
|
701
735
|
pkg.scripts[key] = value;
|
|
702
736
|
changed = true;
|
|
@@ -809,8 +843,8 @@ function parseCommonArgs(rawArgs, defaults) {
|
|
|
809
843
|
|
|
810
844
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
811
845
|
const arg = rawArgs[index];
|
|
812
|
-
if (arg === '--target') {
|
|
813
|
-
options.target = rawArgs
|
|
846
|
+
if (arg === '--target' || arg === '-t') {
|
|
847
|
+
options.target = requireValue(rawArgs, index, '--target');
|
|
814
848
|
index += 1;
|
|
815
849
|
continue;
|
|
816
850
|
}
|
|
@@ -1620,7 +1654,7 @@ function parseAgentsArgs(rawArgs) {
|
|
|
1620
1654
|
subcommand,
|
|
1621
1655
|
reviewIntervalSeconds: 30,
|
|
1622
1656
|
cleanupIntervalSeconds: 60,
|
|
1623
|
-
idleMinutes:
|
|
1657
|
+
idleMinutes: DEFAULT_SHADOW_CLEANUP_IDLE_MINUTES,
|
|
1624
1658
|
};
|
|
1625
1659
|
|
|
1626
1660
|
for (let index = 0; index < rest.length; index += 1) {
|
|
@@ -2367,10 +2401,6 @@ function parseSyncArgs(rawArgs) {
|
|
|
2367
2401
|
throw new Error(`Unknown option: ${arg}`);
|
|
2368
2402
|
}
|
|
2369
2403
|
|
|
2370
|
-
if (!options.target) {
|
|
2371
|
-
throw new Error('--target requires a path value');
|
|
2372
|
-
}
|
|
2373
|
-
|
|
2374
2404
|
return options;
|
|
2375
2405
|
}
|
|
2376
2406
|
|
|
@@ -2383,10 +2413,12 @@ function parseCleanupArgs(rawArgs) {
|
|
|
2383
2413
|
forceDirty: false,
|
|
2384
2414
|
keepRemote: false,
|
|
2385
2415
|
keepCleanWorktrees: false,
|
|
2416
|
+
includePrMerged: false,
|
|
2386
2417
|
idleMinutes: 0,
|
|
2387
2418
|
watch: false,
|
|
2388
2419
|
intervalSeconds: 60,
|
|
2389
2420
|
once: false,
|
|
2421
|
+
maxBranches: 0,
|
|
2390
2422
|
};
|
|
2391
2423
|
|
|
2392
2424
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
@@ -2434,6 +2466,10 @@ function parseCleanupArgs(rawArgs) {
|
|
|
2434
2466
|
options.keepCleanWorktrees = true;
|
|
2435
2467
|
continue;
|
|
2436
2468
|
}
|
|
2469
|
+
if (arg === '--include-pr-merged') {
|
|
2470
|
+
options.includePrMerged = true;
|
|
2471
|
+
continue;
|
|
2472
|
+
}
|
|
2437
2473
|
if (arg === '--idle-minutes') {
|
|
2438
2474
|
const next = rawArgs[index + 1];
|
|
2439
2475
|
if (!next) {
|
|
@@ -2468,11 +2504,24 @@ function parseCleanupArgs(rawArgs) {
|
|
|
2468
2504
|
options.once = true;
|
|
2469
2505
|
continue;
|
|
2470
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
|
+
}
|
|
2471
2520
|
throw new Error(`Unknown option: ${arg}`);
|
|
2472
2521
|
}
|
|
2473
2522
|
|
|
2474
2523
|
if (options.watch && options.idleMinutes === 0) {
|
|
2475
|
-
options.idleMinutes =
|
|
2524
|
+
options.idleMinutes = DEFAULT_SHADOW_CLEANUP_IDLE_MINUTES;
|
|
2476
2525
|
}
|
|
2477
2526
|
|
|
2478
2527
|
return options;
|
|
@@ -2766,27 +2815,16 @@ function branchExists(repoRoot, branch) {
|
|
|
2766
2815
|
return result.status === 0;
|
|
2767
2816
|
}
|
|
2768
2817
|
|
|
2769
|
-
function resolveFinishBaseBranch(repoRoot,
|
|
2818
|
+
function resolveFinishBaseBranch(repoRoot, _sourceBranch, explicitBase) {
|
|
2770
2819
|
if (explicitBase) {
|
|
2771
2820
|
return explicitBase;
|
|
2772
2821
|
}
|
|
2773
2822
|
|
|
2774
|
-
const branchSpecific = readGitConfig(repoRoot, `branch.${sourceBranch}.musafetyBase`);
|
|
2775
|
-
if (branchSpecific) {
|
|
2776
|
-
return branchSpecific;
|
|
2777
|
-
}
|
|
2778
|
-
|
|
2779
2823
|
const configured = readGitConfig(repoRoot, GIT_BASE_BRANCH_KEY);
|
|
2780
2824
|
if (configured) {
|
|
2781
2825
|
return configured;
|
|
2782
2826
|
}
|
|
2783
2827
|
|
|
2784
|
-
const current = gitRun(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD'], { allowFailure: true });
|
|
2785
|
-
const currentBranch = String(current.stdout || '').trim();
|
|
2786
|
-
if (current.status === 0 && currentBranch && currentBranch !== 'HEAD' && !currentBranch.startsWith('agent/')) {
|
|
2787
|
-
return currentBranch;
|
|
2788
|
-
}
|
|
2789
|
-
|
|
2790
2828
|
return DEFAULT_BASE_BRANCH;
|
|
2791
2829
|
}
|
|
2792
2830
|
|
|
@@ -3076,6 +3114,95 @@ function maybeSelfUpdateBeforeStatus() {
|
|
|
3076
3114
|
console.log(`[${TOOL_NAME}] ✅ Updated to latest published version.`);
|
|
3077
3115
|
}
|
|
3078
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
|
+
|
|
3079
3206
|
function promptYesNoStrict(question) {
|
|
3080
3207
|
while (true) {
|
|
3081
3208
|
process.stdout.write(`${question} [y/n] `);
|
|
@@ -3140,15 +3267,21 @@ function detectGlobalToolchainPackages() {
|
|
|
3140
3267
|
|
|
3141
3268
|
const installed = [];
|
|
3142
3269
|
const missing = [];
|
|
3270
|
+
const installedVersions = {};
|
|
3143
3271
|
for (const pkg of GLOBAL_TOOLCHAIN_PACKAGES) {
|
|
3144
3272
|
if (installedSet.has(pkg)) {
|
|
3145
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
|
+
}
|
|
3146
3279
|
} else {
|
|
3147
3280
|
missing.push(pkg);
|
|
3148
3281
|
}
|
|
3149
3282
|
}
|
|
3150
3283
|
|
|
3151
|
-
return { ok: true, installed, missing };
|
|
3284
|
+
return { ok: true, installed, missing, installedVersions };
|
|
3152
3285
|
}
|
|
3153
3286
|
|
|
3154
3287
|
function detectRequiredSystemTools() {
|
|
@@ -3937,37 +4070,60 @@ function agents(rawArgs) {
|
|
|
3937
4070
|
return;
|
|
3938
4071
|
}
|
|
3939
4072
|
|
|
3940
|
-
if (reviewRunning) {
|
|
3941
|
-
stopAgentProcessByPid(existingReviewPid, 'review-bot-watch.sh');
|
|
3942
|
-
}
|
|
3943
|
-
if (cleanupRunning) {
|
|
3944
|
-
stopAgentProcessByPid(existingCleanupPid, `${path.basename(__filename)} cleanup`);
|
|
3945
|
-
}
|
|
3946
|
-
|
|
3947
4073
|
const reviewLogPath = path.join(repoRoot, '.omx', 'logs', 'agent-review.log');
|
|
3948
4074
|
const cleanupLogPath = path.join(repoRoot, '.omx', 'logs', 'agent-cleanup.log');
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
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;
|
|
3971
4127
|
|
|
3972
4128
|
writeAgentsState(repoRoot, {
|
|
3973
4129
|
schemaVersion: 1,
|
|
@@ -3975,14 +4131,14 @@ function agents(rawArgs) {
|
|
|
3975
4131
|
startedAt: new Date().toISOString(),
|
|
3976
4132
|
review: {
|
|
3977
4133
|
pid: reviewPid,
|
|
3978
|
-
intervalSeconds:
|
|
4134
|
+
intervalSeconds: reviewIntervalSeconds,
|
|
3979
4135
|
script: reviewScriptPath,
|
|
3980
4136
|
logPath: reviewLogPath,
|
|
3981
4137
|
},
|
|
3982
4138
|
cleanup: {
|
|
3983
4139
|
pid: cleanupPid,
|
|
3984
|
-
intervalSeconds:
|
|
3985
|
-
idleMinutes
|
|
4140
|
+
intervalSeconds: cleanupIntervalSeconds,
|
|
4141
|
+
idleMinutes,
|
|
3986
4142
|
script: path.resolve(__filename),
|
|
3987
4143
|
logPath: cleanupLogPath,
|
|
3988
4144
|
},
|
|
@@ -3991,6 +4147,9 @@ function agents(rawArgs) {
|
|
|
3991
4147
|
console.log(
|
|
3992
4148
|
`[${TOOL_NAME}] Started repo agents in ${repoRoot} (review pid=${reviewPid}, cleanup pid=${cleanupPid}).`,
|
|
3993
4149
|
);
|
|
4150
|
+
if (reusedAny && startedAny) {
|
|
4151
|
+
console.log(`[${TOOL_NAME}] Reused healthy bot process(es) and started only missing ones.`);
|
|
4152
|
+
}
|
|
3994
4153
|
console.log(`[${TOOL_NAME}] Logs: ${reviewLogPath}, ${cleanupLogPath}`);
|
|
3995
4154
|
process.exitCode = 0;
|
|
3996
4155
|
return;
|
|
@@ -4272,6 +4431,238 @@ function release(rawArgs) {
|
|
|
4272
4431
|
process.exitCode = 0;
|
|
4273
4432
|
}
|
|
4274
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
|
+
|
|
4275
4666
|
function printAgentsSnippet() {
|
|
4276
4667
|
const snippetPath = path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md');
|
|
4277
4668
|
process.stdout.write(fs.readFileSync(snippetPath, 'utf8'));
|
|
@@ -4311,9 +4702,15 @@ function cleanup(rawArgs) {
|
|
|
4311
4702
|
if (!options.keepCleanWorktrees) {
|
|
4312
4703
|
args.push('--only-dirty-worktrees');
|
|
4313
4704
|
}
|
|
4705
|
+
if (options.includePrMerged) {
|
|
4706
|
+
args.push('--include-pr-merged');
|
|
4707
|
+
}
|
|
4314
4708
|
if (options.idleMinutes > 0) {
|
|
4315
4709
|
args.push('--idle-minutes', String(options.idleMinutes));
|
|
4316
4710
|
}
|
|
4711
|
+
if (options.maxBranches > 0) {
|
|
4712
|
+
args.push('--max-branches', String(options.maxBranches));
|
|
4713
|
+
}
|
|
4317
4714
|
args.push('--delete-branches');
|
|
4318
4715
|
if (!options.keepRemote) {
|
|
4319
4716
|
args.push('--delete-remote-branches');
|
|
@@ -4331,7 +4728,7 @@ function cleanup(rawArgs) {
|
|
|
4331
4728
|
while (true) {
|
|
4332
4729
|
cycle += 1;
|
|
4333
4730
|
console.log(
|
|
4334
|
-
`[${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"}).`,
|
|
4335
4732
|
);
|
|
4336
4733
|
runCleanupCycle();
|
|
4337
4734
|
if (options.once) {
|
|
@@ -4764,6 +5161,7 @@ function main() {
|
|
|
4764
5161
|
|
|
4765
5162
|
if (args.length === 0) {
|
|
4766
5163
|
maybeSelfUpdateBeforeStatus();
|
|
5164
|
+
maybeOpenSpecUpdateBeforeStatus();
|
|
4767
5165
|
status([]);
|
|
4768
5166
|
return;
|
|
4769
5167
|
}
|