@imdeadpool/guardex 5.0.12 → 5.0.15
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 +28 -4
- package/bin/multiagent-safety.js +547 -62
- 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',
|
|
@@ -151,7 +184,7 @@ const SUGGESTIBLE_COMMANDS = [
|
|
|
151
184
|
];
|
|
152
185
|
const CLI_COMMAND_DESCRIPTIONS = [
|
|
153
186
|
['status', 'Show GuardeX CLI + service health without modifying files'],
|
|
154
|
-
['setup', 'Install + repair guardrails in a git repo (supports --no-gitignore)'],
|
|
187
|
+
['setup', 'Install + repair guardrails in a git repo (supports --no-gitignore, --parent-workspace-view)'],
|
|
155
188
|
['init', 'Alias of setup (bootstrap + repair guardrails in a git repo)'],
|
|
156
189
|
['doctor', 'Repair safety setup drift, then verify repo safety'],
|
|
157
190
|
['report', 'Generate security/safety reports (for example: OpenSSF scorecard)'],
|
|
@@ -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>"
|
|
@@ -412,6 +445,7 @@ NOTES
|
|
|
412
445
|
- ${TOOL_NAME} setup asks for Y/N approval before global installs
|
|
413
446
|
- ${TOOL_NAME} setup checks GitHub CLI (gh) and prints install guidance if missing
|
|
414
447
|
- For other repos: ${SHORT_TOOL_NAME} setup --target <repo-path> then ${SHORT_TOOL_NAME} doctor --target <repo-path>
|
|
448
|
+
- Optional parent-folder Source Control view: ${SHORT_TOOL_NAME} setup --target <repo-path> --parent-workspace-view
|
|
415
449
|
- In initialized repos, setup/install/fix block in-place writes on protected main by default
|
|
416
450
|
- setup/doctor auto-finish clean pending agent/* branches via PR flow into the current local base branch
|
|
417
451
|
- doctor auto-runs in a sandbox agent branch/worktree on protected main and tries auto-finish PR flow
|
|
@@ -685,6 +719,7 @@ function ensurePackageScripts(repoRoot, dryRun) {
|
|
|
685
719
|
'agent:locks:release': 'python3 ./scripts/agent-file-locks.py release',
|
|
686
720
|
'agent:locks:status': 'python3 ./scripts/agent-file-locks.py status',
|
|
687
721
|
'agent:plan:init': 'bash ./scripts/openspec/init-plan-workspace.sh',
|
|
722
|
+
'agent:change:init': 'bash ./scripts/openspec/init-change-workspace.sh',
|
|
688
723
|
'agent:protect:list': `${SHORT_TOOL_NAME} protect list`,
|
|
689
724
|
'agent:branch:sync': `${SHORT_TOOL_NAME} sync`,
|
|
690
725
|
'agent:branch:sync:check': `${SHORT_TOOL_NAME} sync --check`,
|
|
@@ -696,7 +731,7 @@ function ensurePackageScripts(repoRoot, dryRun) {
|
|
|
696
731
|
|
|
697
732
|
pkg.scripts = pkg.scripts || {};
|
|
698
733
|
let changed = false;
|
|
699
|
-
for (const [key, value] of Object.entries(
|
|
734
|
+
for (const [key, value] of Object.entries(REQUIRED_PACKAGE_SCRIPTS)) {
|
|
700
735
|
if (pkg.scripts[key] !== value) {
|
|
701
736
|
pkg.scripts[key] = value;
|
|
702
737
|
changed = true;
|
|
@@ -804,13 +839,21 @@ function configureHooks(repoRoot, dryRun) {
|
|
|
804
839
|
return { status: 'set', key: 'core.hooksPath', value: '.githooks' };
|
|
805
840
|
}
|
|
806
841
|
|
|
842
|
+
function requireValue(rawArgs, index, flagName) {
|
|
843
|
+
const value = rawArgs[index + 1];
|
|
844
|
+
if (!value || value.startsWith('-')) {
|
|
845
|
+
throw new Error(`${flagName} requires a value`);
|
|
846
|
+
}
|
|
847
|
+
return value;
|
|
848
|
+
}
|
|
849
|
+
|
|
807
850
|
function parseCommonArgs(rawArgs, defaults) {
|
|
808
851
|
const options = { ...defaults };
|
|
809
852
|
|
|
810
853
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
811
854
|
const arg = rawArgs[index];
|
|
812
|
-
if (arg === '--target') {
|
|
813
|
-
options.target = rawArgs
|
|
855
|
+
if (arg === '--target' || arg === '-t') {
|
|
856
|
+
options.target = requireValue(rawArgs, index, '--target');
|
|
814
857
|
index += 1;
|
|
815
858
|
continue;
|
|
816
859
|
}
|
|
@@ -865,6 +908,76 @@ function parseCommonArgs(rawArgs, defaults) {
|
|
|
865
908
|
return options;
|
|
866
909
|
}
|
|
867
910
|
|
|
911
|
+
function parseSetupArgs(rawArgs, defaults) {
|
|
912
|
+
const setupDefaults = { ...defaults, parentWorkspaceView: false };
|
|
913
|
+
const forwardedArgs = [];
|
|
914
|
+
|
|
915
|
+
for (const arg of rawArgs) {
|
|
916
|
+
if (arg === '--parent-workspace-view') {
|
|
917
|
+
setupDefaults.parentWorkspaceView = true;
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
if (arg === '--no-parent-workspace-view') {
|
|
921
|
+
setupDefaults.parentWorkspaceView = false;
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
forwardedArgs.push(arg);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
return parseCommonArgs(forwardedArgs, setupDefaults);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function normalizeWorkspacePath(relativePath) {
|
|
931
|
+
return String(relativePath || '.').replace(/\\/g, '/');
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function buildParentWorkspaceView(repoRoot) {
|
|
935
|
+
const parentDir = path.dirname(repoRoot);
|
|
936
|
+
const workspaceFileName = `${path.basename(repoRoot)}-branches.code-workspace`;
|
|
937
|
+
const workspacePath = path.join(parentDir, workspaceFileName);
|
|
938
|
+
const repoRelativePath = normalizeWorkspacePath(path.relative(parentDir, repoRoot) || '.');
|
|
939
|
+
const worktreesRelativePath = normalizeWorkspacePath(
|
|
940
|
+
path.join(repoRelativePath === '.' ? '' : repoRelativePath, '.omx', 'agent-worktrees'),
|
|
941
|
+
);
|
|
942
|
+
|
|
943
|
+
return {
|
|
944
|
+
workspacePath,
|
|
945
|
+
payload: {
|
|
946
|
+
folders: [
|
|
947
|
+
{ path: repoRelativePath },
|
|
948
|
+
{ path: worktreesRelativePath },
|
|
949
|
+
],
|
|
950
|
+
settings: {
|
|
951
|
+
'scm.alwaysShowRepositories': true,
|
|
952
|
+
},
|
|
953
|
+
},
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function ensureParentWorkspaceView(repoRoot, dryRun) {
|
|
958
|
+
const { workspacePath, payload } = buildParentWorkspaceView(repoRoot);
|
|
959
|
+
const operationFile = path.relative(repoRoot, workspacePath) || path.basename(workspacePath);
|
|
960
|
+
const nextContent = `${JSON.stringify(payload, null, 2)}\n`;
|
|
961
|
+
const note = 'parent VS Code workspace view';
|
|
962
|
+
|
|
963
|
+
if (!fs.existsSync(workspacePath)) {
|
|
964
|
+
if (!dryRun) {
|
|
965
|
+
fs.writeFileSync(workspacePath, nextContent, 'utf8');
|
|
966
|
+
}
|
|
967
|
+
return { status: dryRun ? 'would-create' : 'created', file: operationFile, note };
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const currentContent = fs.readFileSync(workspacePath, 'utf8');
|
|
971
|
+
if (currentContent === nextContent) {
|
|
972
|
+
return { status: 'unchanged', file: operationFile, note };
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (!dryRun) {
|
|
976
|
+
fs.writeFileSync(workspacePath, nextContent, 'utf8');
|
|
977
|
+
}
|
|
978
|
+
return { status: dryRun ? 'would-update' : 'updated', file: operationFile, note };
|
|
979
|
+
}
|
|
980
|
+
|
|
868
981
|
function hasGuardexBootstrapFiles(repoRoot) {
|
|
869
982
|
const required = [
|
|
870
983
|
'AGENTS.md',
|
|
@@ -1620,7 +1733,7 @@ function parseAgentsArgs(rawArgs) {
|
|
|
1620
1733
|
subcommand,
|
|
1621
1734
|
reviewIntervalSeconds: 30,
|
|
1622
1735
|
cleanupIntervalSeconds: 60,
|
|
1623
|
-
idleMinutes:
|
|
1736
|
+
idleMinutes: DEFAULT_SHADOW_CLEANUP_IDLE_MINUTES,
|
|
1624
1737
|
};
|
|
1625
1738
|
|
|
1626
1739
|
for (let index = 0; index < rest.length; index += 1) {
|
|
@@ -2367,10 +2480,6 @@ function parseSyncArgs(rawArgs) {
|
|
|
2367
2480
|
throw new Error(`Unknown option: ${arg}`);
|
|
2368
2481
|
}
|
|
2369
2482
|
|
|
2370
|
-
if (!options.target) {
|
|
2371
|
-
throw new Error('--target requires a path value');
|
|
2372
|
-
}
|
|
2373
|
-
|
|
2374
2483
|
return options;
|
|
2375
2484
|
}
|
|
2376
2485
|
|
|
@@ -2383,10 +2492,12 @@ function parseCleanupArgs(rawArgs) {
|
|
|
2383
2492
|
forceDirty: false,
|
|
2384
2493
|
keepRemote: false,
|
|
2385
2494
|
keepCleanWorktrees: false,
|
|
2495
|
+
includePrMerged: false,
|
|
2386
2496
|
idleMinutes: 0,
|
|
2387
2497
|
watch: false,
|
|
2388
2498
|
intervalSeconds: 60,
|
|
2389
2499
|
once: false,
|
|
2500
|
+
maxBranches: 0,
|
|
2390
2501
|
};
|
|
2391
2502
|
|
|
2392
2503
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
@@ -2434,6 +2545,10 @@ function parseCleanupArgs(rawArgs) {
|
|
|
2434
2545
|
options.keepCleanWorktrees = true;
|
|
2435
2546
|
continue;
|
|
2436
2547
|
}
|
|
2548
|
+
if (arg === '--include-pr-merged') {
|
|
2549
|
+
options.includePrMerged = true;
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2437
2552
|
if (arg === '--idle-minutes') {
|
|
2438
2553
|
const next = rawArgs[index + 1];
|
|
2439
2554
|
if (!next) {
|
|
@@ -2468,11 +2583,24 @@ function parseCleanupArgs(rawArgs) {
|
|
|
2468
2583
|
options.once = true;
|
|
2469
2584
|
continue;
|
|
2470
2585
|
}
|
|
2586
|
+
if (arg === '--max-branches') {
|
|
2587
|
+
const next = rawArgs[index + 1];
|
|
2588
|
+
if (!next) {
|
|
2589
|
+
throw new Error('--max-branches requires an integer value');
|
|
2590
|
+
}
|
|
2591
|
+
const parsed = Number.parseInt(next, 10);
|
|
2592
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
2593
|
+
throw new Error('--max-branches must be an integer >= 1');
|
|
2594
|
+
}
|
|
2595
|
+
options.maxBranches = parsed;
|
|
2596
|
+
index += 1;
|
|
2597
|
+
continue;
|
|
2598
|
+
}
|
|
2471
2599
|
throw new Error(`Unknown option: ${arg}`);
|
|
2472
2600
|
}
|
|
2473
2601
|
|
|
2474
2602
|
if (options.watch && options.idleMinutes === 0) {
|
|
2475
|
-
options.idleMinutes =
|
|
2603
|
+
options.idleMinutes = DEFAULT_SHADOW_CLEANUP_IDLE_MINUTES;
|
|
2476
2604
|
}
|
|
2477
2605
|
|
|
2478
2606
|
return options;
|
|
@@ -2766,27 +2894,16 @@ function branchExists(repoRoot, branch) {
|
|
|
2766
2894
|
return result.status === 0;
|
|
2767
2895
|
}
|
|
2768
2896
|
|
|
2769
|
-
function resolveFinishBaseBranch(repoRoot,
|
|
2897
|
+
function resolveFinishBaseBranch(repoRoot, _sourceBranch, explicitBase) {
|
|
2770
2898
|
if (explicitBase) {
|
|
2771
2899
|
return explicitBase;
|
|
2772
2900
|
}
|
|
2773
2901
|
|
|
2774
|
-
const branchSpecific = readGitConfig(repoRoot, `branch.${sourceBranch}.musafetyBase`);
|
|
2775
|
-
if (branchSpecific) {
|
|
2776
|
-
return branchSpecific;
|
|
2777
|
-
}
|
|
2778
|
-
|
|
2779
2902
|
const configured = readGitConfig(repoRoot, GIT_BASE_BRANCH_KEY);
|
|
2780
2903
|
if (configured) {
|
|
2781
2904
|
return configured;
|
|
2782
2905
|
}
|
|
2783
2906
|
|
|
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
2907
|
return DEFAULT_BASE_BRANCH;
|
|
2791
2908
|
}
|
|
2792
2909
|
|
|
@@ -3076,6 +3193,95 @@ function maybeSelfUpdateBeforeStatus() {
|
|
|
3076
3193
|
console.log(`[${TOOL_NAME}] ✅ Updated to latest published version.`);
|
|
3077
3194
|
}
|
|
3078
3195
|
|
|
3196
|
+
function checkForOpenSpecPackageUpdate() {
|
|
3197
|
+
if (envFlagEnabled('MUSAFETY_SKIP_OPENSPEC_UPDATE_CHECK')) {
|
|
3198
|
+
return { checked: false, reason: 'disabled' };
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
const forceCheck = envFlagEnabled('MUSAFETY_FORCE_OPENSPEC_UPDATE_CHECK');
|
|
3202
|
+
if (!forceCheck && !isInteractiveTerminal()) {
|
|
3203
|
+
return { checked: false, reason: 'non-interactive' };
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
const detection = detectGlobalToolchainPackages();
|
|
3207
|
+
if (!detection.ok) {
|
|
3208
|
+
return { checked: false, reason: 'package-detect-failed' };
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
const current = String((detection.installedVersions || {})[OPENSPEC_PACKAGE] || '').trim();
|
|
3212
|
+
if (!current) {
|
|
3213
|
+
return { checked: false, reason: 'not-installed' };
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
const latestResult = run(NPM_BIN, ['view', OPENSPEC_PACKAGE, 'version', '--json'], { timeout: 5000 });
|
|
3217
|
+
if (latestResult.status !== 0) {
|
|
3218
|
+
return { checked: false, reason: 'lookup-failed' };
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
const latest = parseNpmVersionOutput(latestResult.stdout);
|
|
3222
|
+
if (!latest) {
|
|
3223
|
+
return { checked: false, reason: 'invalid-latest-version' };
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
return {
|
|
3227
|
+
checked: true,
|
|
3228
|
+
current,
|
|
3229
|
+
latest,
|
|
3230
|
+
updateAvailable: isNewerVersion(latest, current),
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
function printOpenSpecUpdateAvailableBanner(current, latest) {
|
|
3235
|
+
const title = colorize('OPENSPEC UPDATE AVAILABLE', '1;33');
|
|
3236
|
+
console.log(`[${TOOL_NAME}] ${title}`);
|
|
3237
|
+
console.log(`[${TOOL_NAME}] Current: ${current}`);
|
|
3238
|
+
console.log(`[${TOOL_NAME}] Latest : ${latest}`);
|
|
3239
|
+
console.log(`[${TOOL_NAME}] Command: ${NPM_BIN} i -g ${OPENSPEC_PACKAGE}@latest`);
|
|
3240
|
+
console.log(`[${TOOL_NAME}] Then : ${OPENSPEC_BIN} update`);
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
function maybeOpenSpecUpdateBeforeStatus() {
|
|
3244
|
+
const check = checkForOpenSpecPackageUpdate();
|
|
3245
|
+
if (!check.checked || !check.updateAvailable) {
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
printOpenSpecUpdateAvailableBanner(check.current, check.latest);
|
|
3250
|
+
|
|
3251
|
+
const autoApproval = parseAutoApproval('MUSAFETY_AUTO_OPENSPEC_UPDATE_APPROVAL');
|
|
3252
|
+
const interactive = isInteractiveTerminal();
|
|
3253
|
+
|
|
3254
|
+
if (!interactive && autoApproval == null) {
|
|
3255
|
+
console.log(`[${TOOL_NAME}] Non-interactive shell; skipping OpenSpec update prompt.`);
|
|
3256
|
+
return;
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
const shouldUpdate = interactive
|
|
3260
|
+
? promptYesNoStrict(
|
|
3261
|
+
`Update OpenSpec now? (${NPM_BIN} i -g ${OPENSPEC_PACKAGE}@latest && ${OPENSPEC_BIN} update)`,
|
|
3262
|
+
)
|
|
3263
|
+
: autoApproval;
|
|
3264
|
+
|
|
3265
|
+
if (!shouldUpdate) {
|
|
3266
|
+
console.log(`[${TOOL_NAME}] Skipped OpenSpec update.`);
|
|
3267
|
+
return;
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
const installResult = run(NPM_BIN, ['i', '-g', `${OPENSPEC_PACKAGE}@latest`], { stdio: 'inherit' });
|
|
3271
|
+
if (installResult.status !== 0) {
|
|
3272
|
+
console.log(`[${TOOL_NAME}] ⚠️ OpenSpec npm install failed. You can retry manually.`);
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
const toolUpdateResult = run(OPENSPEC_BIN, ['update'], { stdio: 'inherit' });
|
|
3277
|
+
if (toolUpdateResult.status !== 0) {
|
|
3278
|
+
console.log(`[${TOOL_NAME}] ⚠️ OpenSpec tool update failed. Run '${OPENSPEC_BIN} update' manually.`);
|
|
3279
|
+
return;
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
console.log(`[${TOOL_NAME}] ✅ OpenSpec updated to latest package and tool plugins refreshed.`);
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3079
3285
|
function promptYesNoStrict(question) {
|
|
3080
3286
|
while (true) {
|
|
3081
3287
|
process.stdout.write(`${question} [y/n] `);
|
|
@@ -3140,15 +3346,21 @@ function detectGlobalToolchainPackages() {
|
|
|
3140
3346
|
|
|
3141
3347
|
const installed = [];
|
|
3142
3348
|
const missing = [];
|
|
3349
|
+
const installedVersions = {};
|
|
3143
3350
|
for (const pkg of GLOBAL_TOOLCHAIN_PACKAGES) {
|
|
3144
3351
|
if (installedSet.has(pkg)) {
|
|
3145
3352
|
installed.push(pkg);
|
|
3353
|
+
const rawVersion = dependencyMap[pkg] && dependencyMap[pkg].version;
|
|
3354
|
+
const version = String(rawVersion || '').trim();
|
|
3355
|
+
if (version) {
|
|
3356
|
+
installedVersions[pkg] = version;
|
|
3357
|
+
}
|
|
3146
3358
|
} else {
|
|
3147
3359
|
missing.push(pkg);
|
|
3148
3360
|
}
|
|
3149
3361
|
}
|
|
3150
3362
|
|
|
3151
|
-
return { ok: true, installed, missing };
|
|
3363
|
+
return { ok: true, installed, missing, installedVersions };
|
|
3152
3364
|
}
|
|
3153
3365
|
|
|
3154
3366
|
function detectRequiredSystemTools() {
|
|
@@ -3937,37 +4149,60 @@ function agents(rawArgs) {
|
|
|
3937
4149
|
return;
|
|
3938
4150
|
}
|
|
3939
4151
|
|
|
3940
|
-
if (reviewRunning) {
|
|
3941
|
-
stopAgentProcessByPid(existingReviewPid, 'review-bot-watch.sh');
|
|
3942
|
-
}
|
|
3943
|
-
if (cleanupRunning) {
|
|
3944
|
-
stopAgentProcessByPid(existingCleanupPid, `${path.basename(__filename)} cleanup`);
|
|
3945
|
-
}
|
|
3946
|
-
|
|
3947
4152
|
const reviewLogPath = path.join(repoRoot, '.omx', 'logs', 'agent-review.log');
|
|
3948
4153
|
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
|
-
|
|
4154
|
+
|
|
4155
|
+
let reviewPid = existingReviewPid;
|
|
4156
|
+
let cleanupPid = existingCleanupPid;
|
|
4157
|
+
let startedAny = false;
|
|
4158
|
+
let reusedAny = false;
|
|
4159
|
+
|
|
4160
|
+
if (!reviewRunning) {
|
|
4161
|
+
reviewPid = spawnDetachedAgentProcess({
|
|
4162
|
+
command: 'bash',
|
|
4163
|
+
args: [reviewScriptPath, '--interval', String(options.reviewIntervalSeconds)],
|
|
4164
|
+
cwd: repoRoot,
|
|
4165
|
+
logPath: reviewLogPath,
|
|
4166
|
+
});
|
|
4167
|
+
startedAny = true;
|
|
4168
|
+
} else {
|
|
4169
|
+
reusedAny = true;
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
if (!cleanupRunning) {
|
|
4173
|
+
cleanupPid = spawnDetachedAgentProcess({
|
|
4174
|
+
command: process.execPath,
|
|
4175
|
+
args: [
|
|
4176
|
+
path.resolve(__filename),
|
|
4177
|
+
'cleanup',
|
|
4178
|
+
'--target',
|
|
4179
|
+
repoRoot,
|
|
4180
|
+
'--watch',
|
|
4181
|
+
'--interval',
|
|
4182
|
+
String(options.cleanupIntervalSeconds),
|
|
4183
|
+
'--idle-minutes',
|
|
4184
|
+
String(options.idleMinutes),
|
|
4185
|
+
],
|
|
4186
|
+
cwd: repoRoot,
|
|
4187
|
+
logPath: cleanupLogPath,
|
|
4188
|
+
});
|
|
4189
|
+
startedAny = true;
|
|
4190
|
+
} else {
|
|
4191
|
+
reusedAny = true;
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
const priorReviewInterval = Number.parseInt(String(existingState?.review?.intervalSeconds || ''), 10);
|
|
4195
|
+
const priorCleanupInterval = Number.parseInt(String(existingState?.cleanup?.intervalSeconds || ''), 10);
|
|
4196
|
+
const priorIdleMinutes = Number.parseInt(String(existingState?.cleanup?.idleMinutes || ''), 10);
|
|
4197
|
+
const reviewIntervalSeconds = reviewRunning && Number.isInteger(priorReviewInterval) && priorReviewInterval >= 5
|
|
4198
|
+
? priorReviewInterval
|
|
4199
|
+
: options.reviewIntervalSeconds;
|
|
4200
|
+
const cleanupIntervalSeconds = cleanupRunning && Number.isInteger(priorCleanupInterval) && priorCleanupInterval >= 5
|
|
4201
|
+
? priorCleanupInterval
|
|
4202
|
+
: options.cleanupIntervalSeconds;
|
|
4203
|
+
const idleMinutes = cleanupRunning && Number.isInteger(priorIdleMinutes) && priorIdleMinutes >= 1
|
|
4204
|
+
? priorIdleMinutes
|
|
4205
|
+
: options.idleMinutes;
|
|
3971
4206
|
|
|
3972
4207
|
writeAgentsState(repoRoot, {
|
|
3973
4208
|
schemaVersion: 1,
|
|
@@ -3975,14 +4210,14 @@ function agents(rawArgs) {
|
|
|
3975
4210
|
startedAt: new Date().toISOString(),
|
|
3976
4211
|
review: {
|
|
3977
4212
|
pid: reviewPid,
|
|
3978
|
-
intervalSeconds:
|
|
4213
|
+
intervalSeconds: reviewIntervalSeconds,
|
|
3979
4214
|
script: reviewScriptPath,
|
|
3980
4215
|
logPath: reviewLogPath,
|
|
3981
4216
|
},
|
|
3982
4217
|
cleanup: {
|
|
3983
4218
|
pid: cleanupPid,
|
|
3984
|
-
intervalSeconds:
|
|
3985
|
-
idleMinutes
|
|
4219
|
+
intervalSeconds: cleanupIntervalSeconds,
|
|
4220
|
+
idleMinutes,
|
|
3986
4221
|
script: path.resolve(__filename),
|
|
3987
4222
|
logPath: cleanupLogPath,
|
|
3988
4223
|
},
|
|
@@ -3991,6 +4226,9 @@ function agents(rawArgs) {
|
|
|
3991
4226
|
console.log(
|
|
3992
4227
|
`[${TOOL_NAME}] Started repo agents in ${repoRoot} (review pid=${reviewPid}, cleanup pid=${cleanupPid}).`,
|
|
3993
4228
|
);
|
|
4229
|
+
if (reusedAny && startedAny) {
|
|
4230
|
+
console.log(`[${TOOL_NAME}] Reused healthy bot process(es) and started only missing ones.`);
|
|
4231
|
+
}
|
|
3994
4232
|
console.log(`[${TOOL_NAME}] Logs: ${reviewLogPath}, ${cleanupLogPath}`);
|
|
3995
4233
|
process.exitCode = 0;
|
|
3996
4234
|
return;
|
|
@@ -4125,7 +4363,7 @@ function report(rawArgs) {
|
|
|
4125
4363
|
}
|
|
4126
4364
|
|
|
4127
4365
|
function setup(rawArgs) {
|
|
4128
|
-
const options =
|
|
4366
|
+
const options = parseSetupArgs(rawArgs, {
|
|
4129
4367
|
target: process.cwd(),
|
|
4130
4368
|
force: false,
|
|
4131
4369
|
skipAgents: false,
|
|
@@ -4172,6 +4410,9 @@ function setup(rawArgs) {
|
|
|
4172
4410
|
assertProtectedMainWriteAllowed(options, 'setup');
|
|
4173
4411
|
const installPayload = runInstallInternal(options);
|
|
4174
4412
|
installPayload.operations.push(ensureSetupProtectedBranches(installPayload.repoRoot, Boolean(options.dryRun)));
|
|
4413
|
+
if (options.parentWorkspaceView) {
|
|
4414
|
+
installPayload.operations.push(ensureParentWorkspaceView(installPayload.repoRoot, Boolean(options.dryRun)));
|
|
4415
|
+
}
|
|
4175
4416
|
printOperations('Setup/install', installPayload, options.dryRun);
|
|
4176
4417
|
|
|
4177
4418
|
const fixPayload = runFixInternal({
|
|
@@ -4190,6 +4431,11 @@ function setup(rawArgs) {
|
|
|
4190
4431
|
return;
|
|
4191
4432
|
}
|
|
4192
4433
|
|
|
4434
|
+
if (options.parentWorkspaceView) {
|
|
4435
|
+
const parentWorkspace = buildParentWorkspaceView(installPayload.repoRoot);
|
|
4436
|
+
console.log(`[${TOOL_NAME}] Parent workspace view: ${parentWorkspace.workspacePath}`);
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4193
4439
|
const scanResult = runScanInternal({ target: options.target, json: false });
|
|
4194
4440
|
const currentBaseBranch = currentBranchName(scanResult.repoRoot);
|
|
4195
4441
|
const autoFinishSummary = autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
@@ -4272,6 +4518,238 @@ function release(rawArgs) {
|
|
|
4272
4518
|
process.exitCode = 0;
|
|
4273
4519
|
}
|
|
4274
4520
|
|
|
4521
|
+
function installMany(rawArgs) {
|
|
4522
|
+
const options = parseInstallManyArgs(rawArgs);
|
|
4523
|
+
const targets = collectInstallManyTargets(options);
|
|
4524
|
+
|
|
4525
|
+
if (!targets.length) {
|
|
4526
|
+
throw new Error('install-many did not find any targets to process.');
|
|
4527
|
+
}
|
|
4528
|
+
|
|
4529
|
+
if (options.usedImplicitWorkspaceDefault) {
|
|
4530
|
+
console.log(
|
|
4531
|
+
`[multiagent-safety] No explicit targets provided. Defaulting to workspace scan: ${path.resolve(
|
|
4532
|
+
options.workspace,
|
|
4533
|
+
)} (max depth ${options.maxDepth})`,
|
|
4534
|
+
);
|
|
4535
|
+
}
|
|
4536
|
+
|
|
4537
|
+
console.log(
|
|
4538
|
+
`[multiagent-safety] install-many starting for ${targets.length} target path(s)${
|
|
4539
|
+
options.dryRun ? ' [dry-run]' : ''
|
|
4540
|
+
}`,
|
|
4541
|
+
);
|
|
4542
|
+
|
|
4543
|
+
let installed = 0;
|
|
4544
|
+
let duplicateRepos = 0;
|
|
4545
|
+
const seenRepoRoots = new Set();
|
|
4546
|
+
const failures = [];
|
|
4547
|
+
|
|
4548
|
+
for (const targetPath of targets) {
|
|
4549
|
+
let repoRoot;
|
|
4550
|
+
try {
|
|
4551
|
+
repoRoot = resolveRepoRoot(targetPath);
|
|
4552
|
+
} catch (error) {
|
|
4553
|
+
failures.push({ target: targetPath, message: error.message });
|
|
4554
|
+
if (options.failFast) {
|
|
4555
|
+
break;
|
|
4556
|
+
}
|
|
4557
|
+
continue;
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4560
|
+
if (seenRepoRoots.has(repoRoot)) {
|
|
4561
|
+
duplicateRepos += 1;
|
|
4562
|
+
console.log(`[multiagent-safety] Skipping duplicate repo target: ${targetPath} -> ${repoRoot}`);
|
|
4563
|
+
continue;
|
|
4564
|
+
}
|
|
4565
|
+
|
|
4566
|
+
seenRepoRoots.add(repoRoot);
|
|
4567
|
+
|
|
4568
|
+
try {
|
|
4569
|
+
const report = installIntoRepoRoot(repoRoot, options);
|
|
4570
|
+
printInstallReport(report);
|
|
4571
|
+
installed += 1;
|
|
4572
|
+
} catch (error) {
|
|
4573
|
+
failures.push({ target: repoRoot, message: error.message });
|
|
4574
|
+
if (options.failFast) {
|
|
4575
|
+
break;
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
|
|
4580
|
+
console.log(
|
|
4581
|
+
`[multiagent-safety] install-many summary: installed=${installed}, failures=${failures.length}, duplicate-targets=${duplicateRepos}`,
|
|
4582
|
+
);
|
|
4583
|
+
|
|
4584
|
+
if (failures.length > 0) {
|
|
4585
|
+
console.error('[multiagent-safety] Failed targets:');
|
|
4586
|
+
for (const failure of failures) {
|
|
4587
|
+
console.error(` - ${failure.target}`);
|
|
4588
|
+
console.error(` ${failure.message}`);
|
|
4589
|
+
}
|
|
4590
|
+
throw new Error(`install-many completed with ${failures.length} failure(s)`);
|
|
4591
|
+
}
|
|
4592
|
+
|
|
4593
|
+
if (options.dryRun) {
|
|
4594
|
+
console.log('[multiagent-safety] Dry run complete. No files were modified.');
|
|
4595
|
+
} else {
|
|
4596
|
+
console.log('[multiagent-safety] Installed multi-agent safety workflow across all targets.');
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
|
|
4600
|
+
function initWorkspace(rawArgs) {
|
|
4601
|
+
const options = parseInitWorkspaceArgs(rawArgs);
|
|
4602
|
+
const resolvedWorkspace = path.resolve(options.workspace);
|
|
4603
|
+
const repos = discoverGitRepos(resolvedWorkspace, options.maxDepth)
|
|
4604
|
+
.map((repoPath) => path.resolve(repoPath))
|
|
4605
|
+
.sort();
|
|
4606
|
+
|
|
4607
|
+
const outputPath = options.output
|
|
4608
|
+
? path.resolve(options.output)
|
|
4609
|
+
: path.join(resolvedWorkspace, DEFAULT_WORKSPACE_TARGETS_FILE);
|
|
4610
|
+
|
|
4611
|
+
if (fs.existsSync(outputPath) && !options.force) {
|
|
4612
|
+
throw new Error(`Refusing to overwrite existing file without --force: ${outputPath}`);
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
const headerLines = [
|
|
4616
|
+
'# multiagent-safety workspace targets',
|
|
4617
|
+
`# generated: ${new Date().toISOString()}`,
|
|
4618
|
+
`# workspace: ${resolvedWorkspace}`,
|
|
4619
|
+
`# max-depth: ${options.maxDepth}`,
|
|
4620
|
+
'#',
|
|
4621
|
+
'# Run:',
|
|
4622
|
+
`# multiagent-safety install-many --targets-file "${outputPath}"`,
|
|
4623
|
+
'',
|
|
4624
|
+
];
|
|
4625
|
+
const content = `${headerLines.join('\n')}${repos.join('\n')}${repos.length ? '\n' : ''}`;
|
|
4626
|
+
|
|
4627
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
4628
|
+
fs.writeFileSync(outputPath, content, 'utf8');
|
|
4629
|
+
|
|
4630
|
+
console.log(`[multiagent-safety] Workspace target file written: ${outputPath}`);
|
|
4631
|
+
console.log(`[multiagent-safety] Repos discovered: ${repos.length}`);
|
|
4632
|
+
if (repos.length === 0) {
|
|
4633
|
+
console.log('[multiagent-safety] No git repos found. You can add target paths manually to the file.');
|
|
4634
|
+
} else {
|
|
4635
|
+
console.log(`[multiagent-safety] Next step: multiagent-safety install-many --targets-file "${outputPath}"`);
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
|
|
4639
|
+
function doctor(rawArgs) {
|
|
4640
|
+
const options = parseDoctorArgs(rawArgs);
|
|
4641
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
4642
|
+
const failures = [];
|
|
4643
|
+
const warnings = [];
|
|
4644
|
+
|
|
4645
|
+
function ok(message) {
|
|
4646
|
+
console.log(` [ok] ${message}`);
|
|
4647
|
+
}
|
|
4648
|
+
function warn(message) {
|
|
4649
|
+
warnings.push(message);
|
|
4650
|
+
console.log(` [warn] ${message}`);
|
|
4651
|
+
}
|
|
4652
|
+
function fail(message) {
|
|
4653
|
+
failures.push(message);
|
|
4654
|
+
console.log(` [fail] ${message}`);
|
|
4655
|
+
}
|
|
4656
|
+
|
|
4657
|
+
console.log(`[multiagent-safety] doctor target: ${repoRoot}`);
|
|
4658
|
+
|
|
4659
|
+
const hooksPath = run('git', ['-C', repoRoot, 'config', '--get', 'core.hooksPath']);
|
|
4660
|
+
if (hooksPath.status !== 0) {
|
|
4661
|
+
fail('git core.hooksPath is not configured');
|
|
4662
|
+
} else if (hooksPath.stdout.trim() !== '.githooks') {
|
|
4663
|
+
fail(`git core.hooksPath is "${hooksPath.stdout.trim()}" (expected ".githooks")`);
|
|
4664
|
+
} else {
|
|
4665
|
+
ok('git core.hooksPath is .githooks');
|
|
4666
|
+
}
|
|
4667
|
+
|
|
4668
|
+
for (const relativePath of REQUIRED_WORKFLOW_FILES) {
|
|
4669
|
+
const absolutePath = path.join(repoRoot, relativePath);
|
|
4670
|
+
if (!fs.existsSync(absolutePath)) {
|
|
4671
|
+
fail(`missing ${relativePath}`);
|
|
4672
|
+
continue;
|
|
4673
|
+
}
|
|
4674
|
+
ok(`found ${relativePath}`);
|
|
4675
|
+
|
|
4676
|
+
if (EXECUTABLE_RELATIVE_PATHS.has(relativePath)) {
|
|
4677
|
+
try {
|
|
4678
|
+
fs.accessSync(absolutePath, fs.constants.X_OK);
|
|
4679
|
+
} catch {
|
|
4680
|
+
fail(`${relativePath} exists but is not executable`);
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
|
|
4685
|
+
const lockFilePath = path.join(repoRoot, '.omx/state/agent-file-locks.json');
|
|
4686
|
+
if (fs.existsSync(lockFilePath)) {
|
|
4687
|
+
try {
|
|
4688
|
+
const parsed = JSON.parse(fs.readFileSync(lockFilePath, 'utf8'));
|
|
4689
|
+
if (!parsed || typeof parsed !== 'object' || typeof parsed.locks !== 'object') {
|
|
4690
|
+
fail('.omx/state/agent-file-locks.json does not contain a valid { locks: {} } object');
|
|
4691
|
+
} else {
|
|
4692
|
+
ok('lock registry JSON is valid');
|
|
4693
|
+
}
|
|
4694
|
+
} catch (error) {
|
|
4695
|
+
fail(`lock registry JSON is invalid: ${error.message}`);
|
|
4696
|
+
}
|
|
4697
|
+
}
|
|
4698
|
+
|
|
4699
|
+
const packagePath = path.join(repoRoot, 'package.json');
|
|
4700
|
+
if (!fs.existsSync(packagePath)) {
|
|
4701
|
+
warn('package.json not found (npm helper scripts cannot be verified)');
|
|
4702
|
+
} else {
|
|
4703
|
+
try {
|
|
4704
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
4705
|
+
const scripts = pkg.scripts || {};
|
|
4706
|
+
for (const [name, expectedValue] of Object.entries(REQUIRED_PACKAGE_SCRIPTS)) {
|
|
4707
|
+
if (scripts[name] !== expectedValue) {
|
|
4708
|
+
fail(`package.json script mismatch for "${name}"`);
|
|
4709
|
+
} else {
|
|
4710
|
+
ok(`package.json script "${name}" is configured`);
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
} catch (error) {
|
|
4714
|
+
fail(`package.json is invalid JSON: ${error.message}`);
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4718
|
+
const agentsPath = path.join(repoRoot, 'AGENTS.md');
|
|
4719
|
+
if (!fs.existsSync(agentsPath)) {
|
|
4720
|
+
warn('AGENTS.md not found (multi-agent contract snippet not present)');
|
|
4721
|
+
} else {
|
|
4722
|
+
const agentsContent = fs.readFileSync(agentsPath, 'utf8');
|
|
4723
|
+
if (!agentsContent.includes(AGENTS_MARKER_START)) {
|
|
4724
|
+
warn('AGENTS.md exists but multiagent-safety snippet marker is missing');
|
|
4725
|
+
} else {
|
|
4726
|
+
ok('AGENTS.md contains multiagent-safety snippet marker');
|
|
4727
|
+
}
|
|
4728
|
+
}
|
|
4729
|
+
|
|
4730
|
+
if (warnings.length) {
|
|
4731
|
+
console.log(`[multiagent-safety] warnings: ${warnings.length}`);
|
|
4732
|
+
}
|
|
4733
|
+
if (failures.length) {
|
|
4734
|
+
console.log(`[multiagent-safety] failures: ${failures.length}`);
|
|
4735
|
+
}
|
|
4736
|
+
|
|
4737
|
+
if (failures.length === 0 && (!options.strict || warnings.length === 0)) {
|
|
4738
|
+
console.log('[multiagent-safety] doctor passed.');
|
|
4739
|
+
if (warnings.length > 0) {
|
|
4740
|
+
console.log('[multiagent-safety] tip: run with --strict to treat warnings as failures.');
|
|
4741
|
+
}
|
|
4742
|
+
return;
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4745
|
+
if (options.strict && warnings.length > 0 && failures.length === 0) {
|
|
4746
|
+
console.log('[multiagent-safety] strict mode failed due to warnings.');
|
|
4747
|
+
} else {
|
|
4748
|
+
console.log('[multiagent-safety] doctor failed.');
|
|
4749
|
+
}
|
|
4750
|
+
throw new Error('doctor detected configuration issues');
|
|
4751
|
+
}
|
|
4752
|
+
|
|
4275
4753
|
function printAgentsSnippet() {
|
|
4276
4754
|
const snippetPath = path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md');
|
|
4277
4755
|
process.stdout.write(fs.readFileSync(snippetPath, 'utf8'));
|
|
@@ -4311,9 +4789,15 @@ function cleanup(rawArgs) {
|
|
|
4311
4789
|
if (!options.keepCleanWorktrees) {
|
|
4312
4790
|
args.push('--only-dirty-worktrees');
|
|
4313
4791
|
}
|
|
4792
|
+
if (options.includePrMerged) {
|
|
4793
|
+
args.push('--include-pr-merged');
|
|
4794
|
+
}
|
|
4314
4795
|
if (options.idleMinutes > 0) {
|
|
4315
4796
|
args.push('--idle-minutes', String(options.idleMinutes));
|
|
4316
4797
|
}
|
|
4798
|
+
if (options.maxBranches > 0) {
|
|
4799
|
+
args.push('--max-branches', String(options.maxBranches));
|
|
4800
|
+
}
|
|
4317
4801
|
args.push('--delete-branches');
|
|
4318
4802
|
if (!options.keepRemote) {
|
|
4319
4803
|
args.push('--delete-remote-branches');
|
|
@@ -4331,7 +4815,7 @@ function cleanup(rawArgs) {
|
|
|
4331
4815
|
while (true) {
|
|
4332
4816
|
cycle += 1;
|
|
4333
4817
|
console.log(
|
|
4334
|
-
`[${TOOL_NAME}] Cleanup watch cycle=${cycle} (interval=${options.intervalSeconds}s, idleMinutes=${options.idleMinutes}).`,
|
|
4818
|
+
`[${TOOL_NAME}] Cleanup watch cycle=${cycle} (interval=${options.intervalSeconds}s, idleMinutes=${options.idleMinutes}, maxBranches=${options.maxBranches > 0 ? options.maxBranches : "unbounded"}).`,
|
|
4335
4819
|
);
|
|
4336
4820
|
runCleanupCycle();
|
|
4337
4821
|
if (options.once) {
|
|
@@ -4764,6 +5248,7 @@ function main() {
|
|
|
4764
5248
|
|
|
4765
5249
|
if (args.length === 0) {
|
|
4766
5250
|
maybeSelfUpdateBeforeStatus();
|
|
5251
|
+
maybeOpenSpecUpdateBeforeStatus();
|
|
4767
5252
|
status([]);
|
|
4768
5253
|
return;
|
|
4769
5254
|
}
|