@imdeadpool/guardex 7.0.12 → 7.0.14
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
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
# GitGuardex — Guardian T-Rex for your repo
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@imdeadpool/guardex)
|
|
4
|
-
[](https://www.npmjs.com/package/@imdeadpool/guardex)
|
|
4
|
+
[](https://www.npmjs.com/package/@imdeadpool/guardex)
|
|
5
|
+
[](https://github.com/recodeee/gitguardex/stargazers)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
[](https://github.com/recodeee/gitguardex/actions/workflows/ci.yml)
|
|
9
|
+
[](https://github.com/recodeee/gitguardex/actions/workflows/release.yml)
|
|
10
|
+
[](https://github.com/recodeee/gitguardex/actions/workflows/codeql.yml)
|
|
5
11
|
[](https://securityscorecards.dev/viewer/?uri=github.com/recodeee/gitguardex)
|
|
6
12
|
|
|
7
13
|
**GitGuardex is a safety layer for parallel agent work in git repos.** If you're running more than one Codex or Claude agent on the same codebase, this is what keeps them from deleting each other's work.
|
|
@@ -20,6 +26,8 @@ I was running ~30 Codex agents in parallel and hit a wall: they kept working on
|
|
|
20
26
|
|
|
21
27
|
GitGuardex exists to stop that loop. Every agent gets its own worktree, claims the files it's touching, and can't clobber files another agent has claimed. Your local branch stays clean; agents stay in their lanes.
|
|
22
28
|
|
|
29
|
+
### Solution
|
|
30
|
+
|
|
23
31
|
```mermaid
|
|
24
32
|
flowchart LR
|
|
25
33
|
A[Agent A adds assertions in a shared test] --> S[Several agents touch the same files]
|
|
@@ -34,6 +42,8 @@ flowchart LR
|
|
|
34
42
|
I --> S
|
|
35
43
|
```
|
|
36
44
|
|
|
45
|
+
### Dashboard
|
|
46
|
+
|
|
37
47
|

|
|
38
48
|
|
|
39
49
|
Coming soon: [recodee.com](https://recodee.com) — live account health, usage, routing, and capacity in one place.
|
|
@@ -277,6 +287,7 @@ npm i -g oh-my-codex
|
|
|
277
287
|
```
|
|
278
288
|
|
|
279
289
|
Repo: <https://github.com/Yeachan-Heo/oh-my-codex>
|
|
290
|
+
[](https://github.com/Yeachan-Heo/oh-my-codex)
|
|
280
291
|
|
|
281
292
|
### oh-my-claudecode — Claude Code equivalent
|
|
282
293
|
|
|
@@ -287,6 +298,7 @@ npm i -g oh-my-claude-sisyphus@latest
|
|
|
287
298
|
```
|
|
288
299
|
|
|
289
300
|
Repo: <https://github.com/Yeachan-Heo/oh-my-claudecode>
|
|
301
|
+
[](https://github.com/Yeachan-Heo/oh-my-claudecode)
|
|
290
302
|
|
|
291
303
|
### Caveman — output compression for long agent runs
|
|
292
304
|
|
|
@@ -297,6 +309,7 @@ npx skills add JuliusBrussee/caveman
|
|
|
297
309
|
```
|
|
298
310
|
|
|
299
311
|
Repo: <https://github.com/JuliusBrussee/caveman>
|
|
312
|
+
[](https://github.com/JuliusBrussee/caveman)
|
|
300
313
|
|
|
301
314
|
### Cavemem — local persistent memory for agents
|
|
302
315
|
|
|
@@ -309,6 +322,7 @@ cavemem status
|
|
|
309
322
|
```
|
|
310
323
|
|
|
311
324
|
Repo: <https://github.com/JuliusBrussee/cavemem>
|
|
325
|
+
[](https://github.com/JuliusBrussee/cavemem)
|
|
312
326
|
|
|
313
327
|
### Cavekit — spec-driven build loop
|
|
314
328
|
|
|
@@ -319,6 +333,7 @@ npx skills add JuliusBrussee/cavekit
|
|
|
319
333
|
```
|
|
320
334
|
|
|
321
335
|
Repo: <https://github.com/JuliusBrussee/cavekit>
|
|
336
|
+
[](https://github.com/JuliusBrussee/cavekit)
|
|
322
337
|
|
|
323
338
|
### OpenSpec — spec-driven workflows
|
|
324
339
|
|
|
@@ -329,6 +344,7 @@ npm i -g @fission-ai/openspec
|
|
|
329
344
|
```
|
|
330
345
|
|
|
331
346
|
Repo: <https://github.com/Fission-AI/OpenSpec>
|
|
347
|
+
[](https://github.com/Fission-AI/OpenSpec)
|
|
332
348
|
|
|
333
349
|
### codex-auth — multi-account switcher
|
|
334
350
|
|
|
@@ -344,6 +360,7 @@ codex-auth current
|
|
|
344
360
|
```
|
|
345
361
|
|
|
346
362
|
Repo: [recodeecom/codex-account-switcher-cli](https://github.com/recodeecom/codex-account-switcher-cli)
|
|
363
|
+
[](https://github.com/recodeecom/codex-account-switcher-cli)
|
|
347
364
|
|
|
348
365
|
### GitHub CLI (`gh`)
|
|
349
366
|
|
|
@@ -490,6 +507,15 @@ npm pack --dry-run
|
|
|
490
507
|
<details>
|
|
491
508
|
<summary><strong>v7.x</strong></summary>
|
|
492
509
|
|
|
510
|
+
### v7.0.14
|
|
511
|
+
- Bumped `@imdeadpool/guardex` from `7.0.13` → `7.0.14` after npm rejected a republish over the already-published `7.0.13`.
|
|
512
|
+
- No package payload changes beyond the release metadata bump; this release exists so `npm publish` can proceed with a fresh semver.
|
|
513
|
+
|
|
514
|
+
### v7.0.13
|
|
515
|
+
- `gx status` and `gx setup` now present the Claude companion as `oh-my-claudecode` while still installing the published npm package `oh-my-claude-sisyphus`.
|
|
516
|
+
- When that dependency is inactive or the user declines the optional install, Guardex now prints the upstream repo URL so the missing dependency is explicit instead of hidden behind the npm package name.
|
|
517
|
+
- Bumped `@imdeadpool/guardex` from `7.0.12` → `7.0.13` after npm rejected a republish over the already-published `7.0.12`.
|
|
518
|
+
|
|
493
519
|
### v7.0.12
|
|
494
520
|
- Fixed the self-update handoff after `gx` installs a newer global package. When the on-disk install advances, GitGuardex now restarts into the installed CLI instead of continuing in the old process and printing the stale in-memory version.
|
|
495
521
|
- This removes the confusing `Updated to latest published version` followed by `CLI: ...7.0.10` mismatch that happened when `7.0.11` finished installing during the same `gx` invocation.
|
package/bin/multiagent-safety.js
CHANGED
|
@@ -13,15 +13,26 @@ const SHORT_TOOL_NAME = 'gx';
|
|
|
13
13
|
const LEGACY_NAMES = ['guardex', 'multiagent-safety'];
|
|
14
14
|
const OPENSPEC_PACKAGE = '@fission-ai/openspec';
|
|
15
15
|
const OMC_PACKAGE = 'oh-my-claude-sisyphus';
|
|
16
|
+
const OMC_REPO_URL = 'https://github.com/Yeachan-Heo/oh-my-claudecode';
|
|
16
17
|
const CAVEMEM_PACKAGE = 'cavemem';
|
|
17
18
|
const NPX_BIN = process.env.GUARDEX_NPX_BIN || 'npx';
|
|
18
19
|
const GUARDEX_HOME_DIR = path.resolve(process.env.GUARDEX_HOME_DIR || os.homedir());
|
|
20
|
+
const GLOBAL_TOOLCHAIN_SERVICES = [
|
|
21
|
+
{ name: 'oh-my-codex', packageName: 'oh-my-codex' },
|
|
22
|
+
{
|
|
23
|
+
name: 'oh-my-claudecode',
|
|
24
|
+
packageName: OMC_PACKAGE,
|
|
25
|
+
dependencyUrl: OMC_REPO_URL,
|
|
26
|
+
},
|
|
27
|
+
{ name: OPENSPEC_PACKAGE, packageName: OPENSPEC_PACKAGE },
|
|
28
|
+
{ name: CAVEMEM_PACKAGE, packageName: CAVEMEM_PACKAGE },
|
|
29
|
+
{
|
|
30
|
+
name: '@imdeadpool/codex-account-switcher',
|
|
31
|
+
packageName: '@imdeadpool/codex-account-switcher',
|
|
32
|
+
},
|
|
33
|
+
];
|
|
19
34
|
const GLOBAL_TOOLCHAIN_PACKAGES = [
|
|
20
|
-
|
|
21
|
-
OMC_PACKAGE,
|
|
22
|
-
OPENSPEC_PACKAGE,
|
|
23
|
-
CAVEMEM_PACKAGE,
|
|
24
|
-
'@imdeadpool/codex-account-switcher',
|
|
35
|
+
...GLOBAL_TOOLCHAIN_SERVICES.map((service) => service.packageName),
|
|
25
36
|
];
|
|
26
37
|
const OPTIONAL_LOCAL_COMPANION_TOOLS = [
|
|
27
38
|
{
|
|
@@ -104,15 +115,26 @@ const REQUIRED_WORKFLOW_FILES = [
|
|
|
104
115
|
];
|
|
105
116
|
|
|
106
117
|
const REQUIRED_PACKAGE_SCRIPTS = {
|
|
118
|
+
'agent:codex': 'bash ./scripts/codex-agent.sh',
|
|
107
119
|
'agent:branch:start': 'bash ./scripts/agent-branch-start.sh',
|
|
108
120
|
'agent:branch:finish': 'bash ./scripts/agent-branch-finish.sh',
|
|
109
|
-
'agent:cleanup': '
|
|
121
|
+
'agent:cleanup': 'gx cleanup',
|
|
110
122
|
'agent:hooks:install': 'bash ./scripts/install-agent-git-hooks.sh',
|
|
111
123
|
'agent:locks:claim': 'python3 ./scripts/agent-file-locks.py claim',
|
|
124
|
+
'agent:locks:allow-delete': 'python3 ./scripts/agent-file-locks.py allow-delete',
|
|
112
125
|
'agent:locks:release': 'python3 ./scripts/agent-file-locks.py release',
|
|
113
126
|
'agent:locks:status': 'python3 ./scripts/agent-file-locks.py status',
|
|
114
127
|
'agent:plan:init': 'bash ./scripts/openspec/init-plan-workspace.sh',
|
|
115
128
|
'agent:change:init': 'bash ./scripts/openspec/init-change-workspace.sh',
|
|
129
|
+
'agent:protect:list': 'gx protect list',
|
|
130
|
+
'agent:branch:sync': 'gx sync',
|
|
131
|
+
'agent:branch:sync:check': 'gx sync --check',
|
|
132
|
+
'agent:safety:setup': 'gx setup',
|
|
133
|
+
'agent:safety:scan': 'gx status --strict',
|
|
134
|
+
'agent:safety:fix': 'gx setup --repair',
|
|
135
|
+
'agent:safety:doctor': 'gx doctor',
|
|
136
|
+
'agent:review:watch': 'bash ./scripts/review-bot-watch.sh',
|
|
137
|
+
'agent:finish': 'gx finish --all',
|
|
116
138
|
};
|
|
117
139
|
|
|
118
140
|
const EXECUTABLE_RELATIVE_PATHS = new Set([
|
|
@@ -154,26 +176,15 @@ const GITIGNORE_MARKER_END = '# multiagent-safety:END';
|
|
|
154
176
|
const MANAGED_GITIGNORE_PATHS = [
|
|
155
177
|
'.omx/',
|
|
156
178
|
'.omc/',
|
|
157
|
-
'scripts
|
|
158
|
-
'
|
|
159
|
-
'scripts/codex-agent.sh',
|
|
160
|
-
'scripts/review-bot-watch.sh',
|
|
161
|
-
'scripts/agent-worktree-prune.sh',
|
|
162
|
-
'scripts/agent-file-locks.py',
|
|
163
|
-
'scripts/guardex-env.sh',
|
|
164
|
-
'scripts/install-agent-git-hooks.sh',
|
|
165
|
-
'scripts/openspec/init-plan-workspace.sh',
|
|
166
|
-
'scripts/openspec/init-change-workspace.sh',
|
|
167
|
-
'.githooks/pre-commit',
|
|
168
|
-
'.githooks/pre-push',
|
|
169
|
-
'.githooks/post-merge',
|
|
170
|
-
'.githooks/post-checkout',
|
|
179
|
+
'scripts/*',
|
|
180
|
+
'.githooks',
|
|
171
181
|
'oh-my-codex/',
|
|
172
182
|
'.codex/skills/gitguardex/SKILL.md',
|
|
173
183
|
'.codex/skills/guardex-merge-skills-to-dev/SKILL.md',
|
|
174
184
|
'.claude/commands/gitguardex.md',
|
|
175
185
|
LOCK_FILE_RELATIVE,
|
|
176
186
|
];
|
|
187
|
+
const REPO_SCAFFOLD_DIRECTORIES = ['bin'];
|
|
177
188
|
const OMX_SCAFFOLD_DIRECTORIES = [
|
|
178
189
|
'.omx',
|
|
179
190
|
'.omx/state',
|
|
@@ -255,14 +266,14 @@ const AI_SETUP_PROMPT = `GitGuardex (gx) setup checklist for Codex/Claude in thi
|
|
|
255
266
|
2) Bootstrap: gx setup
|
|
256
267
|
3) Repair: gx doctor
|
|
257
268
|
4) Task loop: bash scripts/codex-agent.sh "<task>" "<agent>"
|
|
258
|
-
or branch-start -> claim -> branch-finish
|
|
269
|
+
or branch-start -> python3 scripts/agent-file-locks.py claim -> branch-finish
|
|
259
270
|
5) Finish: gx finish --all
|
|
260
271
|
6) Cleanup: gx cleanup
|
|
261
272
|
7) OpenSpec: /opsx:propose -> /opsx:apply -> /opsx:archive
|
|
262
273
|
8) Optional: gx protect add release staging
|
|
263
274
|
9) Optional: gx sync --check && gx sync
|
|
264
275
|
10) Review bot: install https://github.com/apps/cr-gpt + set OPENAI_API_KEY
|
|
265
|
-
11) Fork sync: cp .github/pull.yml.example .github/pull.yml
|
|
276
|
+
11) Fork sync: install https://github.com/apps/pull + cp .github/pull.yml.example .github/pull.yml
|
|
266
277
|
`;
|
|
267
278
|
|
|
268
279
|
const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
|
|
@@ -270,6 +281,7 @@ gh --version
|
|
|
270
281
|
gx setup
|
|
271
282
|
gx doctor
|
|
272
283
|
bash scripts/codex-agent.sh "<task>" "<agent>"
|
|
284
|
+
python3 scripts/agent-file-locks.py claim --branch "<agent-branch>" <file...>
|
|
273
285
|
gx finish --all
|
|
274
286
|
gx cleanup
|
|
275
287
|
gx protect add release staging
|
|
@@ -587,9 +599,27 @@ function toDestinationPath(relativeTemplatePath) {
|
|
|
587
599
|
throw new Error(`Unsupported template path: ${relativeTemplatePath}`);
|
|
588
600
|
}
|
|
589
601
|
|
|
590
|
-
function ensureParentDir(filePath, dryRun) {
|
|
602
|
+
function ensureParentDir(repoRoot, filePath, dryRun) {
|
|
591
603
|
if (dryRun) return;
|
|
592
|
-
|
|
604
|
+
|
|
605
|
+
const parentDir = path.dirname(filePath);
|
|
606
|
+
const relativeParentDir = path.relative(repoRoot, parentDir);
|
|
607
|
+
const segments = relativeParentDir.split(path.sep).filter(Boolean);
|
|
608
|
+
let currentPath = repoRoot;
|
|
609
|
+
|
|
610
|
+
for (const segment of segments) {
|
|
611
|
+
currentPath = path.join(currentPath, segment);
|
|
612
|
+
if (fs.existsSync(currentPath) && !fs.statSync(currentPath).isDirectory()) {
|
|
613
|
+
const blockingPath = path.relative(repoRoot, currentPath) || path.basename(currentPath);
|
|
614
|
+
const targetPath = path.relative(repoRoot, filePath) || path.basename(filePath);
|
|
615
|
+
throw new Error(
|
|
616
|
+
`Path conflict: ${blockingPath} exists as a file, but ${targetPath} needs it to be a directory. ` +
|
|
617
|
+
`Remove or rename ${blockingPath} and rerun '${SHORT_TOOL_NAME} setup'.`,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
593
623
|
}
|
|
594
624
|
|
|
595
625
|
function ensureExecutable(destinationPath, relativePath, dryRun) {
|
|
@@ -624,7 +654,7 @@ function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
|
|
|
624
654
|
}
|
|
625
655
|
}
|
|
626
656
|
|
|
627
|
-
ensureParentDir(destinationPath, dryRun);
|
|
657
|
+
ensureParentDir(repoRoot, destinationPath, dryRun);
|
|
628
658
|
if (!dryRun) {
|
|
629
659
|
fs.writeFileSync(destinationPath, sourceContent, 'utf8');
|
|
630
660
|
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
|
|
@@ -662,7 +692,7 @@ function ensureTemplateFilePresent(repoRoot, relativeTemplatePath, dryRun) {
|
|
|
662
692
|
return { status: 'skipped-conflict', file: destinationRelativePath };
|
|
663
693
|
}
|
|
664
694
|
|
|
665
|
-
ensureParentDir(destinationPath, dryRun);
|
|
695
|
+
ensureParentDir(repoRoot, destinationPath, dryRun);
|
|
666
696
|
if (!dryRun) {
|
|
667
697
|
fs.writeFileSync(destinationPath, sourceContent, 'utf8');
|
|
668
698
|
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
|
|
@@ -678,6 +708,22 @@ function lockFilePath(repoRoot) {
|
|
|
678
708
|
function ensureOmxScaffold(repoRoot, dryRun) {
|
|
679
709
|
const operations = [];
|
|
680
710
|
|
|
711
|
+
for (const relativeDir of REPO_SCAFFOLD_DIRECTORIES) {
|
|
712
|
+
const absoluteDir = path.join(repoRoot, relativeDir);
|
|
713
|
+
if (fs.existsSync(absoluteDir)) {
|
|
714
|
+
if (!fs.statSync(absoluteDir).isDirectory()) {
|
|
715
|
+
throw new Error(`Expected directory at ${relativeDir} but found a file.`);
|
|
716
|
+
}
|
|
717
|
+
operations.push({ status: 'unchanged', file: relativeDir });
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (!dryRun) {
|
|
722
|
+
fs.mkdirSync(absoluteDir, { recursive: true });
|
|
723
|
+
}
|
|
724
|
+
operations.push({ status: 'created', file: relativeDir });
|
|
725
|
+
}
|
|
726
|
+
|
|
681
727
|
for (const relativeDir of OMX_SCAFFOLD_DIRECTORIES) {
|
|
682
728
|
const absoluteDir = path.join(repoRoot, relativeDir);
|
|
683
729
|
if (fs.existsSync(absoluteDir)) {
|
|
@@ -965,10 +1011,9 @@ function parseCommonArgs(rawArgs, defaults) {
|
|
|
965
1011
|
return options;
|
|
966
1012
|
}
|
|
967
1013
|
|
|
968
|
-
function
|
|
969
|
-
const
|
|
1014
|
+
function parseRepoTraversalArgs(rawArgs, defaults) {
|
|
1015
|
+
const traversalDefaults = {
|
|
970
1016
|
...defaults,
|
|
971
|
-
parentWorkspaceView: false,
|
|
972
1017
|
recursive: true,
|
|
973
1018
|
nestedMaxDepth: NESTED_REPO_DEFAULT_MAX_DEPTH,
|
|
974
1019
|
nestedSkipDirs: [],
|
|
@@ -978,20 +1023,12 @@ function parseSetupArgs(rawArgs, defaults) {
|
|
|
978
1023
|
|
|
979
1024
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
980
1025
|
const arg = rawArgs[index];
|
|
981
|
-
if (arg === '--parent-workspace-view') {
|
|
982
|
-
setupDefaults.parentWorkspaceView = true;
|
|
983
|
-
continue;
|
|
984
|
-
}
|
|
985
|
-
if (arg === '--no-parent-workspace-view') {
|
|
986
|
-
setupDefaults.parentWorkspaceView = false;
|
|
987
|
-
continue;
|
|
988
|
-
}
|
|
989
1026
|
if (arg === '--no-recursive' || arg === '--no-nested' || arg === '--single-repo') {
|
|
990
|
-
|
|
1027
|
+
traversalDefaults.recursive = false;
|
|
991
1028
|
continue;
|
|
992
1029
|
}
|
|
993
1030
|
if (arg === '--recursive' || arg === '--nested') {
|
|
994
|
-
|
|
1031
|
+
traversalDefaults.recursive = true;
|
|
995
1032
|
continue;
|
|
996
1033
|
}
|
|
997
1034
|
if (arg === '--max-depth') {
|
|
@@ -1000,47 +1037,60 @@ function parseSetupArgs(rawArgs, defaults) {
|
|
|
1000
1037
|
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
1001
1038
|
throw new Error('--max-depth requires a positive integer');
|
|
1002
1039
|
}
|
|
1003
|
-
|
|
1040
|
+
traversalDefaults.nestedMaxDepth = parsed;
|
|
1004
1041
|
index += 1;
|
|
1005
1042
|
continue;
|
|
1006
1043
|
}
|
|
1007
1044
|
if (arg === '--skip-nested') {
|
|
1008
1045
|
const raw = requireValue(rawArgs, index, '--skip-nested');
|
|
1009
|
-
|
|
1046
|
+
traversalDefaults.nestedSkipDirs.push(raw);
|
|
1010
1047
|
index += 1;
|
|
1011
1048
|
continue;
|
|
1012
1049
|
}
|
|
1013
1050
|
if (arg === '--include-submodules') {
|
|
1014
|
-
|
|
1051
|
+
traversalDefaults.includeSubmodules = true;
|
|
1015
1052
|
continue;
|
|
1016
1053
|
}
|
|
1017
1054
|
forwardedArgs.push(arg);
|
|
1018
1055
|
}
|
|
1019
1056
|
|
|
1020
|
-
return parseCommonArgs(forwardedArgs,
|
|
1057
|
+
return parseCommonArgs(forwardedArgs, traversalDefaults);
|
|
1021
1058
|
}
|
|
1022
1059
|
|
|
1023
|
-
function
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1060
|
+
function parseSetupArgs(rawArgs, defaults) {
|
|
1061
|
+
const setupDefaults = {
|
|
1062
|
+
...defaults,
|
|
1063
|
+
parentWorkspaceView: false,
|
|
1027
1064
|
};
|
|
1065
|
+
const forwardedArgs = [];
|
|
1028
1066
|
|
|
1029
1067
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
1030
1068
|
const arg = rawArgs[index];
|
|
1031
|
-
if (arg === '--
|
|
1032
|
-
|
|
1033
|
-
index += 1;
|
|
1069
|
+
if (arg === '--parent-workspace-view') {
|
|
1070
|
+
setupDefaults.parentWorkspaceView = true;
|
|
1034
1071
|
continue;
|
|
1035
1072
|
}
|
|
1036
|
-
if (arg === '--
|
|
1037
|
-
|
|
1073
|
+
if (arg === '--no-parent-workspace-view') {
|
|
1074
|
+
setupDefaults.parentWorkspaceView = false;
|
|
1038
1075
|
continue;
|
|
1039
1076
|
}
|
|
1040
|
-
|
|
1077
|
+
forwardedArgs.push(arg);
|
|
1041
1078
|
}
|
|
1042
1079
|
|
|
1043
|
-
return
|
|
1080
|
+
return parseRepoTraversalArgs(forwardedArgs, setupDefaults);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function parseDoctorArgs(rawArgs) {
|
|
1084
|
+
return parseRepoTraversalArgs(rawArgs, {
|
|
1085
|
+
target: process.cwd(),
|
|
1086
|
+
dropStaleLocks: true,
|
|
1087
|
+
skipAgents: false,
|
|
1088
|
+
skipPackageJson: false,
|
|
1089
|
+
skipGitignore: false,
|
|
1090
|
+
dryRun: false,
|
|
1091
|
+
json: false,
|
|
1092
|
+
allowProtectedBaseWrite: false,
|
|
1093
|
+
});
|
|
1044
1094
|
}
|
|
1045
1095
|
|
|
1046
1096
|
function normalizeWorkspacePath(relativePath) {
|
|
@@ -1530,7 +1580,7 @@ function finishDoctorSandboxBranch(blocked, metadata) {
|
|
|
1530
1580
|
|
|
1531
1581
|
const finishResult = run(
|
|
1532
1582
|
'bash',
|
|
1533
|
-
[finishScript, '--branch', metadata.branch, '--via-pr', '--wait-for-merge'],
|
|
1583
|
+
[finishScript, '--branch', metadata.branch, '--base', blocked.branch, '--via-pr', '--wait-for-merge'],
|
|
1534
1584
|
{ cwd: metadata.worktreePath, timeout: finishTimeoutMs },
|
|
1535
1585
|
);
|
|
1536
1586
|
if (isSpawnFailure(finishResult)) {
|
|
@@ -1569,6 +1619,33 @@ function finishDoctorSandboxBranch(blocked, metadata) {
|
|
|
1569
1619
|
};
|
|
1570
1620
|
}
|
|
1571
1621
|
|
|
1622
|
+
function syncProtectedBaseDoctorRepairs(options, blocked) {
|
|
1623
|
+
const fixPayload = runFixInternal({
|
|
1624
|
+
...options,
|
|
1625
|
+
target: blocked.repoRoot,
|
|
1626
|
+
allowProtectedBaseWrite: true,
|
|
1627
|
+
});
|
|
1628
|
+
const changedOperations = fixPayload.operations.filter(
|
|
1629
|
+
(operation) => !['unchanged', 'skipped'].includes(operation.status),
|
|
1630
|
+
);
|
|
1631
|
+
const hookChanged = fixPayload.hookResult?.status && fixPayload.hookResult.status !== 'unchanged';
|
|
1632
|
+
const changedCount = changedOperations.length + (hookChanged ? 1 : 0);
|
|
1633
|
+
|
|
1634
|
+
if (changedCount === 0) {
|
|
1635
|
+
return {
|
|
1636
|
+
status: 'unchanged',
|
|
1637
|
+
note: 'managed repair files already aligned in protected branch workspace',
|
|
1638
|
+
fixPayload,
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
return {
|
|
1643
|
+
status: options.dryRun ? 'would-sync' : 'synced',
|
|
1644
|
+
note: `${options.dryRun ? 'would sync' : 'synced'} ${changedCount} managed repair item(s)`,
|
|
1645
|
+
fixPayload,
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1572
1649
|
function runDoctorInSandbox(options, blocked) {
|
|
1573
1650
|
const startResult = startDoctorSandbox(blocked);
|
|
1574
1651
|
const metadata = startResult.metadata;
|
|
@@ -1592,6 +1669,10 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1592
1669
|
note: 'sandbox doctor did not complete successfully',
|
|
1593
1670
|
};
|
|
1594
1671
|
|
|
1672
|
+
let protectedBaseRepairSyncResult = {
|
|
1673
|
+
status: 'skipped',
|
|
1674
|
+
note: 'sandbox doctor did not complete successfully',
|
|
1675
|
+
};
|
|
1595
1676
|
let lockSyncResult = {
|
|
1596
1677
|
status: 'skipped',
|
|
1597
1678
|
note: 'sandbox doctor did not complete successfully',
|
|
@@ -1609,6 +1690,7 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1609
1690
|
note: 'sandbox doctor did not complete successfully',
|
|
1610
1691
|
};
|
|
1611
1692
|
if (nestedResult.status === 0) {
|
|
1693
|
+
protectedBaseRepairSyncResult = syncProtectedBaseDoctorRepairs(options, blocked);
|
|
1612
1694
|
const omxScaffoldOps = ensureOmxScaffold(blocked.repoRoot, Boolean(options.dryRun));
|
|
1613
1695
|
const changedOmxPaths = omxScaffoldOps.filter((operation) => operation.status !== 'unchanged');
|
|
1614
1696
|
if (changedOmxPaths.length === 0) {
|
|
@@ -1697,6 +1779,7 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1697
1779
|
JSON.stringify(
|
|
1698
1780
|
{
|
|
1699
1781
|
...parsed,
|
|
1782
|
+
protectedBaseRepairSync: protectedBaseRepairSyncResult,
|
|
1700
1783
|
sandboxOmxScaffoldSync: omxScaffoldSyncResult,
|
|
1701
1784
|
sandboxLockSync: lockSyncResult,
|
|
1702
1785
|
sandboxAutoCommit: autoCommitResult,
|
|
@@ -1737,6 +1820,16 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1737
1820
|
console.log(`[${TOOL_NAME}] Doctor sandbox auto-commit skipped: ${autoCommitResult.note}.`);
|
|
1738
1821
|
}
|
|
1739
1822
|
|
|
1823
|
+
if (protectedBaseRepairSyncResult.status === 'synced') {
|
|
1824
|
+
console.log(`[${TOOL_NAME}] Synced repaired managed files back to protected branch workspace.`);
|
|
1825
|
+
} else if (protectedBaseRepairSyncResult.status === 'unchanged') {
|
|
1826
|
+
console.log(`[${TOOL_NAME}] Protected branch workspace already had the repaired managed files.`);
|
|
1827
|
+
} else if (protectedBaseRepairSyncResult.status === 'would-sync') {
|
|
1828
|
+
console.log(`[${TOOL_NAME}] Dry run: would sync repaired managed files back to protected branch workspace.`);
|
|
1829
|
+
} else {
|
|
1830
|
+
console.log(`[${TOOL_NAME}] Protected branch workspace repair sync skipped: ${protectedBaseRepairSyncResult.note}.`);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1740
1833
|
if (finishResult.status === 'completed') {
|
|
1741
1834
|
console.log(`[${TOOL_NAME}] Auto-finish flow completed for sandbox branch '${metadata.branch}'.`);
|
|
1742
1835
|
if (finishResult.stdout) process.stdout.write(finishResult.stdout);
|
|
@@ -3629,6 +3722,36 @@ function resolveGlobalInstallApproval(options) {
|
|
|
3629
3722
|
return { approved: true, source: 'prompt' };
|
|
3630
3723
|
}
|
|
3631
3724
|
|
|
3725
|
+
function getGlobalToolchainService(packageName) {
|
|
3726
|
+
const service = GLOBAL_TOOLCHAIN_SERVICES.find(
|
|
3727
|
+
(candidate) => candidate.packageName === packageName,
|
|
3728
|
+
);
|
|
3729
|
+
return service || { name: packageName, packageName };
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
function formatGlobalToolchainServiceName(packageName) {
|
|
3733
|
+
return getGlobalToolchainService(packageName).name;
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
function describeMissingGlobalDependencyWarnings(packageNames) {
|
|
3737
|
+
return packageNames
|
|
3738
|
+
.map((packageName) => getGlobalToolchainService(packageName))
|
|
3739
|
+
.filter((service) => service.dependencyUrl)
|
|
3740
|
+
.map(
|
|
3741
|
+
(service) =>
|
|
3742
|
+
`Guardex needs ${service.name} as a dependency: ${service.dependencyUrl}`,
|
|
3743
|
+
);
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
function buildMissingCompanionInstallPrompt(missingPackages, missingLocalTools) {
|
|
3747
|
+
const dependencyWarnings = describeMissingGlobalDependencyWarnings(missingPackages);
|
|
3748
|
+
const installCommands = describeCompanionInstallCommands(missingPackages, missingLocalTools);
|
|
3749
|
+
const dependencyPrefix = dependencyWarnings.length > 0
|
|
3750
|
+
? `${dependencyWarnings.join(' ')} `
|
|
3751
|
+
: '';
|
|
3752
|
+
return `${dependencyPrefix}Install missing companion tools now? (${installCommands.join(' && ')})`;
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3632
3755
|
function detectGlobalToolchainPackages() {
|
|
3633
3756
|
const result = run(NPM_BIN, ['list', '-g', '--depth=0', '--json']);
|
|
3634
3757
|
if (result.status !== 0) {
|
|
@@ -3721,17 +3844,15 @@ function describeCompanionInstallCommands(missingPackages, missingLocalTools) {
|
|
|
3721
3844
|
return commands;
|
|
3722
3845
|
}
|
|
3723
3846
|
|
|
3724
|
-
function askGlobalInstallForMissing(options, missingPackages) {
|
|
3847
|
+
function askGlobalInstallForMissing(options, missingPackages, missingLocalTools) {
|
|
3725
3848
|
const approval = resolveGlobalInstallApproval(options);
|
|
3726
3849
|
if (!approval.approved) {
|
|
3727
3850
|
return approval;
|
|
3728
3851
|
}
|
|
3729
3852
|
|
|
3730
|
-
const missingLocalTools = detectOptionalLocalCompanionTools().filter((tool) => tool.status !== 'active');
|
|
3731
|
-
const installCommands = describeCompanionInstallCommands(missingPackages, missingLocalTools);
|
|
3732
3853
|
if (approval.source === 'prompt') {
|
|
3733
3854
|
const approved = promptYesNoStrict(
|
|
3734
|
-
|
|
3855
|
+
buildMissingCompanionInstallPrompt(missingPackages, missingLocalTools),
|
|
3735
3856
|
);
|
|
3736
3857
|
return { approved, source: 'prompt' };
|
|
3737
3858
|
}
|
|
@@ -3750,7 +3871,10 @@ function installGlobalToolchain(options) {
|
|
|
3750
3871
|
console.log(`[${TOOL_NAME}] ⚠️ Could not detect global packages: ${detection.error}`);
|
|
3751
3872
|
} else {
|
|
3752
3873
|
if (detection.installed.length > 0) {
|
|
3753
|
-
console.log(
|
|
3874
|
+
console.log(
|
|
3875
|
+
`[${TOOL_NAME}] Already installed globally: ` +
|
|
3876
|
+
`${detection.installed.map((pkg) => formatGlobalToolchainServiceName(pkg)).join(', ')}`,
|
|
3877
|
+
);
|
|
3754
3878
|
}
|
|
3755
3879
|
const installedLocalTools = localCompanionTools
|
|
3756
3880
|
.filter((tool) => tool.status === 'active')
|
|
@@ -3765,9 +3889,14 @@ function installGlobalToolchain(options) {
|
|
|
3765
3889
|
|
|
3766
3890
|
const missingPackages = detection.ok ? detection.missing : [...GLOBAL_TOOLCHAIN_PACKAGES];
|
|
3767
3891
|
const missingLocalTools = localCompanionTools.filter((tool) => tool.status !== 'active');
|
|
3768
|
-
const approval = askGlobalInstallForMissing(options, missingPackages);
|
|
3892
|
+
const approval = askGlobalInstallForMissing(options, missingPackages, missingLocalTools);
|
|
3769
3893
|
if (!approval.approved) {
|
|
3770
|
-
return {
|
|
3894
|
+
return {
|
|
3895
|
+
status: 'skipped',
|
|
3896
|
+
reason: approval.source,
|
|
3897
|
+
missingPackages,
|
|
3898
|
+
missingLocalTools,
|
|
3899
|
+
};
|
|
3771
3900
|
}
|
|
3772
3901
|
|
|
3773
3902
|
const installed = [];
|
|
@@ -3850,6 +3979,10 @@ function runInstallInternal(options) {
|
|
|
3850
3979
|
}
|
|
3851
3980
|
const operations = [];
|
|
3852
3981
|
|
|
3982
|
+
if (!options.skipGitignore) {
|
|
3983
|
+
operations.push(ensureManagedGitignore(repoRoot, Boolean(options.dryRun)));
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3853
3986
|
operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
|
|
3854
3987
|
|
|
3855
3988
|
for (const templateFile of TEMPLATE_FILES) {
|
|
@@ -3857,9 +3990,6 @@ function runInstallInternal(options) {
|
|
|
3857
3990
|
}
|
|
3858
3991
|
|
|
3859
3992
|
operations.push(ensureLockRegistry(repoRoot, Boolean(options.dryRun)));
|
|
3860
|
-
if (!options.skipGitignore) {
|
|
3861
|
-
operations.push(ensureManagedGitignore(repoRoot, Boolean(options.dryRun)));
|
|
3862
|
-
}
|
|
3863
3993
|
|
|
3864
3994
|
if (!options.skipPackageJson) {
|
|
3865
3995
|
operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
|
|
@@ -3894,6 +4024,10 @@ function runFixInternal(options) {
|
|
|
3894
4024
|
}
|
|
3895
4025
|
const operations = [];
|
|
3896
4026
|
|
|
4027
|
+
if (!options.skipGitignore) {
|
|
4028
|
+
operations.push(ensureManagedGitignore(repoRoot, Boolean(options.dryRun)));
|
|
4029
|
+
}
|
|
4030
|
+
|
|
3897
4031
|
operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
|
|
3898
4032
|
|
|
3899
4033
|
for (const templateFile of TEMPLATE_FILES) {
|
|
@@ -3901,9 +4035,6 @@ function runFixInternal(options) {
|
|
|
3901
4035
|
}
|
|
3902
4036
|
|
|
3903
4037
|
operations.push(ensureLockRegistry(repoRoot, Boolean(options.dryRun)));
|
|
3904
|
-
if (!options.skipGitignore) {
|
|
3905
|
-
operations.push(ensureManagedGitignore(repoRoot, Boolean(options.dryRun)));
|
|
3906
|
-
}
|
|
3907
4038
|
|
|
3908
4039
|
const lockState = lockStateOrError(repoRoot);
|
|
3909
4040
|
if (!lockState.ok) {
|
|
@@ -4142,11 +4273,21 @@ function status(rawArgs) {
|
|
|
4142
4273
|
|
|
4143
4274
|
const toolchain = detectGlobalToolchainPackages();
|
|
4144
4275
|
const npmServices = GLOBAL_TOOLCHAIN_PACKAGES.map((pkg) => {
|
|
4276
|
+
const service = getGlobalToolchainService(pkg);
|
|
4145
4277
|
if (!toolchain.ok) {
|
|
4146
|
-
return {
|
|
4278
|
+
return {
|
|
4279
|
+
name: service.name,
|
|
4280
|
+
displayName: service.name,
|
|
4281
|
+
packageName: pkg,
|
|
4282
|
+
dependencyUrl: service.dependencyUrl || null,
|
|
4283
|
+
status: 'unknown',
|
|
4284
|
+
};
|
|
4147
4285
|
}
|
|
4148
4286
|
return {
|
|
4149
|
-
name:
|
|
4287
|
+
name: service.name,
|
|
4288
|
+
displayName: service.name,
|
|
4289
|
+
packageName: pkg,
|
|
4290
|
+
dependencyUrl: service.dependencyUrl || null,
|
|
4150
4291
|
status: toolchain.installed.includes(pkg) ? 'active' : 'inactive',
|
|
4151
4292
|
};
|
|
4152
4293
|
});
|
|
@@ -4224,6 +4365,13 @@ function status(rawArgs) {
|
|
|
4224
4365
|
console.log(
|
|
4225
4366
|
`[${TOOL_NAME}] Optional companion tools inactive: ${inactiveOptionalCompanions.join(', ')}`,
|
|
4226
4367
|
);
|
|
4368
|
+
for (const warning of describeMissingGlobalDependencyWarnings(
|
|
4369
|
+
npmServices
|
|
4370
|
+
.filter((service) => service.status === 'inactive')
|
|
4371
|
+
.map((service) => service.packageName),
|
|
4372
|
+
)) {
|
|
4373
|
+
console.log(`[${TOOL_NAME}] ${warning}`);
|
|
4374
|
+
}
|
|
4227
4375
|
console.log(
|
|
4228
4376
|
`[${TOOL_NAME}] Run '${SHORT_TOOL_NAME} setup' to install missing companions with an explicit Y/N prompt.`,
|
|
4229
4377
|
);
|
|
@@ -4357,26 +4505,116 @@ function scan(rawArgs) {
|
|
|
4357
4505
|
}
|
|
4358
4506
|
|
|
4359
4507
|
function doctor(rawArgs) {
|
|
4360
|
-
const options =
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4508
|
+
const options = parseDoctorArgs(rawArgs);
|
|
4509
|
+
const topRepoRoot = resolveRepoRoot(options.target);
|
|
4510
|
+
const discoveredRepos = options.recursive
|
|
4511
|
+
? discoverNestedGitRepos(topRepoRoot, {
|
|
4512
|
+
maxDepth: options.nestedMaxDepth,
|
|
4513
|
+
extraSkip: options.nestedSkipDirs,
|
|
4514
|
+
includeSubmodules: options.includeSubmodules,
|
|
4515
|
+
})
|
|
4516
|
+
: [topRepoRoot];
|
|
4517
|
+
|
|
4518
|
+
if (discoveredRepos.length > 1) {
|
|
4519
|
+
if (!options.json) {
|
|
4520
|
+
console.log(
|
|
4521
|
+
`[${TOOL_NAME}] Detected ${discoveredRepos.length} git repos under ${topRepoRoot}. ` +
|
|
4522
|
+
`Repairing each with doctor (use --single-repo to limit to the target).`,
|
|
4523
|
+
);
|
|
4524
|
+
}
|
|
4525
|
+
|
|
4526
|
+
const repoResults = [];
|
|
4527
|
+
let aggregateExitCode = 0;
|
|
4528
|
+
for (const repoPath of discoveredRepos) {
|
|
4529
|
+
if (!options.json) {
|
|
4530
|
+
console.log(`[${TOOL_NAME}] ── Doctor target: ${repoPath} ──`);
|
|
4531
|
+
}
|
|
4532
|
+
|
|
4533
|
+
const nestedResult = run(
|
|
4534
|
+
process.execPath,
|
|
4535
|
+
[
|
|
4536
|
+
path.resolve(__filename),
|
|
4537
|
+
'doctor',
|
|
4538
|
+
'--single-repo',
|
|
4539
|
+
'--target',
|
|
4540
|
+
repoPath,
|
|
4541
|
+
...(options.dropStaleLocks ? [] : ['--keep-stale-locks']),
|
|
4542
|
+
...(options.skipAgents ? ['--skip-agents'] : []),
|
|
4543
|
+
...(options.skipPackageJson ? ['--skip-package-json'] : []),
|
|
4544
|
+
...(options.skipGitignore ? ['--no-gitignore'] : []),
|
|
4545
|
+
...(options.dryRun ? ['--dry-run'] : []),
|
|
4546
|
+
...(options.json ? ['--json'] : []),
|
|
4547
|
+
...(options.allowProtectedBaseWrite ? ['--allow-protected-base-write'] : []),
|
|
4548
|
+
],
|
|
4549
|
+
{ cwd: topRepoRoot },
|
|
4550
|
+
);
|
|
4551
|
+
if (isSpawnFailure(nestedResult)) {
|
|
4552
|
+
throw nestedResult.error;
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4555
|
+
const exitCode = typeof nestedResult.status === 'number' ? nestedResult.status : 1;
|
|
4556
|
+
if (exitCode !== 0 && aggregateExitCode === 0) {
|
|
4557
|
+
aggregateExitCode = exitCode;
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4560
|
+
if (options.json) {
|
|
4561
|
+
let parsedResult = null;
|
|
4562
|
+
if (nestedResult.stdout) {
|
|
4563
|
+
try {
|
|
4564
|
+
parsedResult = JSON.parse(nestedResult.stdout);
|
|
4565
|
+
} catch {
|
|
4566
|
+
parsedResult = null;
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
repoResults.push(
|
|
4570
|
+
parsedResult
|
|
4571
|
+
? { repoRoot: repoPath, exitCode, result: parsedResult }
|
|
4572
|
+
: {
|
|
4573
|
+
repoRoot: repoPath,
|
|
4574
|
+
exitCode,
|
|
4575
|
+
stdout: nestedResult.stdout || '',
|
|
4576
|
+
stderr: nestedResult.stderr || '',
|
|
4577
|
+
},
|
|
4578
|
+
);
|
|
4579
|
+
} else {
|
|
4580
|
+
if (nestedResult.stdout) process.stdout.write(nestedResult.stdout);
|
|
4581
|
+
if (nestedResult.stderr) process.stderr.write(nestedResult.stderr);
|
|
4582
|
+
process.stdout.write('\n');
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4370
4585
|
|
|
4371
|
-
|
|
4586
|
+
if (options.json) {
|
|
4587
|
+
process.stdout.write(
|
|
4588
|
+
JSON.stringify(
|
|
4589
|
+
{
|
|
4590
|
+
repoRoot: topRepoRoot,
|
|
4591
|
+
recursive: true,
|
|
4592
|
+
repos: repoResults,
|
|
4593
|
+
},
|
|
4594
|
+
null,
|
|
4595
|
+
2,
|
|
4596
|
+
) + '\n',
|
|
4597
|
+
);
|
|
4598
|
+
}
|
|
4599
|
+
|
|
4600
|
+
process.exitCode = aggregateExitCode;
|
|
4601
|
+
return;
|
|
4602
|
+
}
|
|
4603
|
+
|
|
4604
|
+
const singleRepoOptions = {
|
|
4605
|
+
...options,
|
|
4606
|
+
target: topRepoRoot,
|
|
4607
|
+
};
|
|
4608
|
+
|
|
4609
|
+
const blocked = protectedBaseWriteBlock(singleRepoOptions, { requireBootstrap: false });
|
|
4372
4610
|
if (blocked) {
|
|
4373
|
-
runDoctorInSandbox(
|
|
4611
|
+
runDoctorInSandbox(singleRepoOptions, blocked);
|
|
4374
4612
|
return;
|
|
4375
4613
|
}
|
|
4376
4614
|
|
|
4377
|
-
assertProtectedMainWriteAllowed(
|
|
4378
|
-
const fixPayload = runFixInternal(
|
|
4379
|
-
const scanResult = runScanInternal({ target:
|
|
4615
|
+
assertProtectedMainWriteAllowed(singleRepoOptions, 'doctor');
|
|
4616
|
+
const fixPayload = runFixInternal(singleRepoOptions);
|
|
4617
|
+
const scanResult = runScanInternal({ target: singleRepoOptions.target, json: false });
|
|
4380
4618
|
const currentBaseBranch = currentBranchName(scanResult.repoRoot);
|
|
4381
4619
|
const autoFinishSummary = scanResult.guardexEnabled === false
|
|
4382
4620
|
? {
|
|
@@ -4389,12 +4627,12 @@ function doctor(rawArgs) {
|
|
|
4389
4627
|
}
|
|
4390
4628
|
: autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
4391
4629
|
baseBranch: currentBaseBranch,
|
|
4392
|
-
dryRun:
|
|
4630
|
+
dryRun: singleRepoOptions.dryRun,
|
|
4393
4631
|
});
|
|
4394
4632
|
const safe = scanResult.guardexEnabled === false || (scanResult.errors === 0 && scanResult.warnings === 0);
|
|
4395
4633
|
const musafe = safe;
|
|
4396
4634
|
|
|
4397
|
-
if (
|
|
4635
|
+
if (singleRepoOptions.json) {
|
|
4398
4636
|
process.stdout.write(
|
|
4399
4637
|
JSON.stringify(
|
|
4400
4638
|
{
|
|
@@ -4405,7 +4643,7 @@ function doctor(rawArgs) {
|
|
|
4405
4643
|
fix: {
|
|
4406
4644
|
operations: fixPayload.operations,
|
|
4407
4645
|
hookResult: fixPayload.hookResult,
|
|
4408
|
-
dryRun: Boolean(
|
|
4646
|
+
dryRun: Boolean(singleRepoOptions.dryRun),
|
|
4409
4647
|
},
|
|
4410
4648
|
scan: {
|
|
4411
4649
|
guardexEnabled: scanResult.guardexEnabled !== false,
|
|
@@ -4424,7 +4662,7 @@ function doctor(rawArgs) {
|
|
|
4424
4662
|
return;
|
|
4425
4663
|
}
|
|
4426
4664
|
|
|
4427
|
-
printOperations('Doctor/fix', fixPayload,
|
|
4665
|
+
printOperations('Doctor/fix', fixPayload, singleRepoOptions.dryRun);
|
|
4428
4666
|
printScanResult(scanResult, false);
|
|
4429
4667
|
if (scanResult.guardexEnabled === false) {
|
|
4430
4668
|
console.log(`[${TOOL_NAME}] Repo-local Guardex enforcement is intentionally disabled.`);
|
|
@@ -4881,6 +5119,13 @@ function setup(rawArgs) {
|
|
|
4881
5119
|
`[${TOOL_NAME}] Skipping companion installs (non-interactive mode). ` +
|
|
4882
5120
|
`Use --yes-global-install to force or run interactively for Y/N prompt.`,
|
|
4883
5121
|
);
|
|
5122
|
+
} else if (globalInstallStatus.status === 'skipped') {
|
|
5123
|
+
console.log(`[${TOOL_NAME}] ⚠️ Companion installs skipped by user choice.`);
|
|
5124
|
+
for (const warning of describeMissingGlobalDependencyWarnings(
|
|
5125
|
+
globalInstallStatus.missingPackages || [],
|
|
5126
|
+
)) {
|
|
5127
|
+
console.log(`[${TOOL_NAME}] ⚠️ ${warning}`);
|
|
5128
|
+
}
|
|
4884
5129
|
}
|
|
4885
5130
|
const requiredSystemTools = detectRequiredSystemTools();
|
|
4886
5131
|
const missingSystemTools = requiredSystemTools.filter((tool) => tool.status !== 'active');
|
package/package.json
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
<!-- multiagent-safety:START -->
|
|
2
|
-
## Multi-Agent
|
|
2
|
+
## Multi-Agent Execution Contract (GX)
|
|
3
3
|
|
|
4
|
-
**Repo toggle.** `GUARDEX_ON=0
|
|
4
|
+
**Repo toggle.** Guardex is enabled by default. If the repo root `.env` sets `GUARDEX_ON=0`, `false`, `no`, or `off`, treat this entire Guardex contract as disabled for the repo and do not require Guardex worktrees, lock claims, completion flow, or OpenSpec workflow until `GUARDEX_ON` is re-enabled.
|
|
5
5
|
|
|
6
|
-
**
|
|
6
|
+
**Repo toggle examples.** Add one of these lines to the repo-root `.env` file:
|
|
7
|
+
`GUARDEX_ON=0` disables Guardex for that repo.
|
|
8
|
+
`GUARDEX_ON=1` explicitly enables Guardex for that repo again.
|
|
7
9
|
|
|
8
|
-
**
|
|
10
|
+
**Isolation.** Every task runs on a dedicated `agent/*` branch + worktree. Start with `scripts/agent-branch-start.sh "<task>" "<agent-name>"`. Treat the base branch (`main`/`dev`) as read-only while an agent branch is active. Never `git checkout <branch>` on a primary working tree (including nested repos); use `git worktree add` instead. The `.githooks/post-checkout` hook auto-reverts primary-branch switches during agent sessions - bypass only with `GUARDEX_ALLOW_PRIMARY_BRANCH_SWITCH=1`.
|
|
11
|
+
For every new task, including follow-up work in the same chat/session, if an assigned agent sub-branch/worktree is already open, continue in that sub-branch instead of creating a fresh lane unless the user explicitly redirects scope.
|
|
12
|
+
Never implement directly on the local/base branch checkout; keep it unchanged and perform all edits in the agent sub-branch/worktree.
|
|
9
13
|
|
|
10
|
-
**
|
|
14
|
+
**Ownership.** Before editing, claim files: `scripts/agent-file-locks.py claim --branch "<agent-branch>" <file...>`. Before deleting, confirm the path is in your claim. Don't edit outside your scope unless reassigned.
|
|
11
15
|
|
|
12
|
-
**
|
|
16
|
+
**Handoff gate.** Post a one-line handoff note (plan/change, owned scope, intended action) before editing. Re-read the latest handoffs before replacing others' code.
|
|
13
17
|
|
|
14
|
-
**
|
|
18
|
+
**Completion.** Finish with `scripts/agent-branch-finish.sh --branch "<agent-branch>" --via-pr --wait-for-merge --cleanup` (or `gx finish --all`). Task is only complete when: commit pushed, PR URL recorded, state = `MERGED`, sandbox worktree pruned. If anything blocks, append a `BLOCKED:` note and stop - don't half-finish.
|
|
19
|
+
OMX completion policy: when a task is done, the agent must commit the task changes, push the agent branch, and create/update a PR before considering the branch complete.
|
|
15
20
|
|
|
16
|
-
**
|
|
21
|
+
**Parallel safety.** Assume other agents edit nearby. Never revert unrelated changes. Report conflicts in the handoff.
|
|
17
22
|
|
|
18
|
-
**
|
|
23
|
+
**Reporting.** Every completion handoff includes: files changed, behavior touched, verification commands + results, risks/follow-ups.
|
|
24
|
+
|
|
25
|
+
**OpenSpec (when change-driven).** Keep `openspec/changes/<slug>/tasks.md` checkboxes current during work, not batched at the end. Task scaffolds and manual task edits must include an explicit final completion/cleanup section that ends with PR merge + sandbox cleanup (`gx finish --via-pr --wait-for-merge --cleanup` or `scripts/agent-branch-finish.sh ... --cleanup`) and records PR URL + final `MERGED` evidence. Verify specs with `openspec validate --specs` before archive. Don't archive unverified.
|
|
19
26
|
|
|
20
27
|
**Version bumps.** If a change bumps a published version, the same PR updates release notes/changelog.
|
|
21
28
|
<!-- multiagent-safety:END -->
|
|
@@ -10,12 +10,18 @@ permissions:
|
|
|
10
10
|
|
|
11
11
|
jobs:
|
|
12
12
|
review:
|
|
13
|
-
if: ${{ secrets.OPENAI_API_KEY != '' }}
|
|
14
13
|
runs-on: ubuntu-latest
|
|
14
|
+
env:
|
|
15
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
15
16
|
steps:
|
|
16
|
-
-
|
|
17
|
+
- name: Skip when OPENAI_API_KEY is missing
|
|
18
|
+
if: ${{ env.OPENAI_API_KEY == '' }}
|
|
19
|
+
run: echo "OPENAI_API_KEY is not configured; skipping Code Review workflow."
|
|
20
|
+
|
|
21
|
+
- uses: anc95/ChatGPT-CodeReview@1e3df152c1b85c12da580b206c91ad343460c584 # v1.0.23
|
|
22
|
+
if: ${{ env.OPENAI_API_KEY != '' }}
|
|
17
23
|
env:
|
|
18
24
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
19
|
-
OPENAI_API_KEY: ${{
|
|
25
|
+
OPENAI_API_KEY: ${{ env.OPENAI_API_KEY }}
|
|
20
26
|
OPENAI_API_ENDPOINT: https://api.openai.com/v1
|
|
21
27
|
MODEL: gpt-4o-mini
|
|
@@ -8,11 +8,16 @@ FORCE_DIRTY=0
|
|
|
8
8
|
DELETE_BRANCHES=0
|
|
9
9
|
DELETE_REMOTE_BRANCHES=0
|
|
10
10
|
ONLY_DIRTY_WORKTREES=0
|
|
11
|
+
INCLUDE_PR_MERGED=0
|
|
11
12
|
TARGET_BRANCH=""
|
|
12
13
|
IDLE_MINUTES=0
|
|
13
14
|
NOW_EPOCH_RAW="${GUARDEX_PRUNE_NOW_EPOCH:-}"
|
|
14
15
|
IDLE_SECONDS=0
|
|
15
16
|
NOW_EPOCH=0
|
|
17
|
+
GH_BIN="${GUARDEX_GH_BIN:-gh}"
|
|
18
|
+
PR_MERGED_LOOKUP_DISABLED=0
|
|
19
|
+
PR_MERGED_LOOKUP_LOADED=0
|
|
20
|
+
declare -A MERGED_PR_BRANCHES=()
|
|
16
21
|
|
|
17
22
|
if [[ -n "$BASE_BRANCH" ]]; then
|
|
18
23
|
BASE_BRANCH_EXPLICIT=1
|
|
@@ -45,6 +50,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
45
50
|
ONLY_DIRTY_WORKTREES=1
|
|
46
51
|
shift
|
|
47
52
|
;;
|
|
53
|
+
--include-pr-merged)
|
|
54
|
+
INCLUDE_PR_MERGED=1
|
|
55
|
+
shift
|
|
56
|
+
;;
|
|
48
57
|
--branch)
|
|
49
58
|
TARGET_BRANCH="${2:-}"
|
|
50
59
|
shift 2
|
|
@@ -55,7 +64,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
55
64
|
;;
|
|
56
65
|
*)
|
|
57
66
|
echo "[agent-worktree-prune] Unknown argument: $1" >&2
|
|
58
|
-
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--branch <agent/...>] [--idle-minutes <minutes>]" >&2
|
|
67
|
+
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--include-pr-merged] [--branch <agent/...>] [--idle-minutes <minutes>]" >&2
|
|
59
68
|
exit 1
|
|
60
69
|
;;
|
|
61
70
|
esac
|
|
@@ -101,6 +110,44 @@ resolve_base_branch() {
|
|
|
101
110
|
printf '%s' ""
|
|
102
111
|
}
|
|
103
112
|
|
|
113
|
+
load_merged_pr_branches() {
|
|
114
|
+
if [[ "$INCLUDE_PR_MERGED" -ne 1 ]]; then
|
|
115
|
+
return 1
|
|
116
|
+
fi
|
|
117
|
+
if [[ "$PR_MERGED_LOOKUP_DISABLED" -eq 1 ]]; then
|
|
118
|
+
return 1
|
|
119
|
+
fi
|
|
120
|
+
if [[ "$PR_MERGED_LOOKUP_LOADED" -eq 1 ]]; then
|
|
121
|
+
return 0
|
|
122
|
+
fi
|
|
123
|
+
if ! command -v "$GH_BIN" >/dev/null 2>&1; then
|
|
124
|
+
PR_MERGED_LOOKUP_DISABLED=1
|
|
125
|
+
return 1
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
local merged_branches=""
|
|
129
|
+
merged_branches="$(
|
|
130
|
+
"$GH_BIN" pr list --state merged --base "$BASE_BRANCH" --limit 200 --json headRefName --jq '.[].headRefName' 2>/dev/null || true
|
|
131
|
+
)"
|
|
132
|
+
if [[ -n "$merged_branches" ]]; then
|
|
133
|
+
while IFS= read -r merged_branch; do
|
|
134
|
+
[[ -z "$merged_branch" ]] && continue
|
|
135
|
+
MERGED_PR_BRANCHES["$merged_branch"]=1
|
|
136
|
+
done <<< "$merged_branches"
|
|
137
|
+
fi
|
|
138
|
+
PR_MERGED_LOOKUP_LOADED=1
|
|
139
|
+
return 0
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
branch_has_merged_pr() {
|
|
143
|
+
local branch="$1"
|
|
144
|
+
if [[ "$INCLUDE_PR_MERGED" -ne 1 ]]; then
|
|
145
|
+
return 1
|
|
146
|
+
fi
|
|
147
|
+
load_merged_pr_branches || return 1
|
|
148
|
+
[[ -n "${MERGED_PR_BRANCHES[$branch]:-}" ]]
|
|
149
|
+
}
|
|
150
|
+
|
|
104
151
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
|
|
105
152
|
echo "[agent-worktree-prune] --base requires a non-empty branch name." >&2
|
|
106
153
|
exit 1
|
|
@@ -342,6 +389,7 @@ process_entry() {
|
|
|
342
389
|
fi
|
|
343
390
|
|
|
344
391
|
local remove_reason=""
|
|
392
|
+
local branch_delete_mode="safe"
|
|
345
393
|
|
|
346
394
|
if [[ -z "$branch_ref" ]]; then
|
|
347
395
|
remove_reason="detached-worktree"
|
|
@@ -352,6 +400,9 @@ process_entry() {
|
|
|
352
400
|
if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
|
|
353
401
|
remove_reason="merged-agent-branch"
|
|
354
402
|
fi
|
|
403
|
+
elif [[ "$DELETE_BRANCHES" -eq 1 ]] && branch_has_merged_pr "$branch"; then
|
|
404
|
+
remove_reason="merged-agent-pr"
|
|
405
|
+
branch_delete_mode="force"
|
|
355
406
|
elif [[ "$ONLY_DIRTY_WORKTREES" -eq 1 ]] && is_clean_worktree "$wt"; then
|
|
356
407
|
remove_reason="clean-agent-worktree"
|
|
357
408
|
fi
|
|
@@ -383,13 +434,19 @@ process_entry() {
|
|
|
383
434
|
|
|
384
435
|
if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch}" && ! branch_has_worktree "$branch"; then
|
|
385
436
|
if [[ "$branch" == agent/* && "$DELETE_BRANCHES" -eq 1 ]]; then
|
|
386
|
-
|
|
437
|
+
local delete_flag="-d"
|
|
438
|
+
local deleted_label="merged"
|
|
439
|
+
if [[ "$branch_delete_mode" == "force" ]]; then
|
|
440
|
+
delete_flag="-D"
|
|
441
|
+
deleted_label="merged PR"
|
|
442
|
+
fi
|
|
443
|
+
if run_cmd git -C "$repo_root" branch "$delete_flag" "$branch" >/dev/null 2>&1; then
|
|
387
444
|
removed_branches=$((removed_branches + 1))
|
|
388
|
-
echo "[agent-worktree-prune] Deleted
|
|
445
|
+
echo "[agent-worktree-prune] Deleted ${deleted_label} branch: ${branch}"
|
|
389
446
|
if [[ "$DELETE_REMOTE_BRANCHES" -eq 1 ]]; then
|
|
390
447
|
if git -C "$repo_root" ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
|
|
391
448
|
run_cmd git -C "$repo_root" push origin --delete "$branch" >/dev/null 2>&1 || true
|
|
392
|
-
echo "[agent-worktree-prune] Deleted
|
|
449
|
+
echo "[agent-worktree-prune] Deleted ${deleted_label} remote branch: ${branch}"
|
|
393
450
|
fi
|
|
394
451
|
fi
|
|
395
452
|
fi
|
|
@@ -420,7 +477,7 @@ while IFS= read -r line || [[ -n "$line" ]]; do
|
|
|
420
477
|
current_branch_ref="${line#branch }"
|
|
421
478
|
;;
|
|
422
479
|
esac
|
|
423
|
-
done < <(git -C "$repo_root" worktree list --porcelain)
|
|
480
|
+
done < <(git -C "$repo_root" worktree list --porcelain)
|
|
424
481
|
|
|
425
482
|
process_entry "$current_wt" "$current_branch_ref"
|
|
426
483
|
|
|
@@ -436,14 +493,27 @@ if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
|
|
|
436
493
|
if ! branch_idle_gate "$branch" "" "stale-merged-branch"; then
|
|
437
494
|
continue
|
|
438
495
|
fi
|
|
496
|
+
merged_by_ancestor=0
|
|
497
|
+
merged_by_pr=0
|
|
439
498
|
if git -C "$repo_root" merge-base --is-ancestor "$branch" "$BASE_BRANCH"; then
|
|
440
|
-
|
|
499
|
+
merged_by_ancestor=1
|
|
500
|
+
elif branch_has_merged_pr "$branch"; then
|
|
501
|
+
merged_by_pr=1
|
|
502
|
+
fi
|
|
503
|
+
if [[ "$merged_by_ancestor" -eq 1 || "$merged_by_pr" -eq 1 ]]; then
|
|
504
|
+
delete_flag="-d"
|
|
505
|
+
deleted_label="merged"
|
|
506
|
+
if [[ "$merged_by_pr" -eq 1 && "$merged_by_ancestor" -eq 0 ]]; then
|
|
507
|
+
delete_flag="-D"
|
|
508
|
+
deleted_label="merged PR"
|
|
509
|
+
fi
|
|
510
|
+
if run_cmd git -C "$repo_root" branch "$delete_flag" "$branch" >/dev/null 2>&1; then
|
|
441
511
|
removed_branches=$((removed_branches + 1))
|
|
442
|
-
echo "[agent-worktree-prune] Deleted stale
|
|
512
|
+
echo "[agent-worktree-prune] Deleted stale ${deleted_label} branch: ${branch}"
|
|
443
513
|
if [[ "$DELETE_REMOTE_BRANCHES" -eq 1 ]]; then
|
|
444
514
|
if git -C "$repo_root" ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
|
|
445
515
|
run_cmd git -C "$repo_root" push origin --delete "$branch" >/dev/null 2>&1 || true
|
|
446
|
-
echo "[agent-worktree-prune] Deleted stale
|
|
516
|
+
echo "[agent-worktree-prune] Deleted stale ${deleted_label} remote branch: ${branch}"
|
|
447
517
|
fi
|
|
448
518
|
fi
|
|
449
519
|
fi
|