@imdeadpool/guardex 7.0.13 → 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
|
@@ -507,6 +507,10 @@ npm pack --dry-run
|
|
|
507
507
|
<details>
|
|
508
508
|
<summary><strong>v7.x</strong></summary>
|
|
509
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
|
+
|
|
510
514
|
### v7.0.13
|
|
511
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`.
|
|
512
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.
|
package/bin/multiagent-safety.js
CHANGED
|
@@ -115,15 +115,26 @@ const REQUIRED_WORKFLOW_FILES = [
|
|
|
115
115
|
];
|
|
116
116
|
|
|
117
117
|
const REQUIRED_PACKAGE_SCRIPTS = {
|
|
118
|
+
'agent:codex': 'bash ./scripts/codex-agent.sh',
|
|
118
119
|
'agent:branch:start': 'bash ./scripts/agent-branch-start.sh',
|
|
119
120
|
'agent:branch:finish': 'bash ./scripts/agent-branch-finish.sh',
|
|
120
|
-
'agent:cleanup': '
|
|
121
|
+
'agent:cleanup': 'gx cleanup',
|
|
121
122
|
'agent:hooks:install': 'bash ./scripts/install-agent-git-hooks.sh',
|
|
122
123
|
'agent:locks:claim': 'python3 ./scripts/agent-file-locks.py claim',
|
|
124
|
+
'agent:locks:allow-delete': 'python3 ./scripts/agent-file-locks.py allow-delete',
|
|
123
125
|
'agent:locks:release': 'python3 ./scripts/agent-file-locks.py release',
|
|
124
126
|
'agent:locks:status': 'python3 ./scripts/agent-file-locks.py status',
|
|
125
127
|
'agent:plan:init': 'bash ./scripts/openspec/init-plan-workspace.sh',
|
|
126
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',
|
|
127
138
|
};
|
|
128
139
|
|
|
129
140
|
const EXECUTABLE_RELATIVE_PATHS = new Set([
|
|
@@ -165,26 +176,15 @@ const GITIGNORE_MARKER_END = '# multiagent-safety:END';
|
|
|
165
176
|
const MANAGED_GITIGNORE_PATHS = [
|
|
166
177
|
'.omx/',
|
|
167
178
|
'.omc/',
|
|
168
|
-
'scripts
|
|
169
|
-
'
|
|
170
|
-
'scripts/codex-agent.sh',
|
|
171
|
-
'scripts/review-bot-watch.sh',
|
|
172
|
-
'scripts/agent-worktree-prune.sh',
|
|
173
|
-
'scripts/agent-file-locks.py',
|
|
174
|
-
'scripts/guardex-env.sh',
|
|
175
|
-
'scripts/install-agent-git-hooks.sh',
|
|
176
|
-
'scripts/openspec/init-plan-workspace.sh',
|
|
177
|
-
'scripts/openspec/init-change-workspace.sh',
|
|
178
|
-
'.githooks/pre-commit',
|
|
179
|
-
'.githooks/pre-push',
|
|
180
|
-
'.githooks/post-merge',
|
|
181
|
-
'.githooks/post-checkout',
|
|
179
|
+
'scripts/*',
|
|
180
|
+
'.githooks',
|
|
182
181
|
'oh-my-codex/',
|
|
183
182
|
'.codex/skills/gitguardex/SKILL.md',
|
|
184
183
|
'.codex/skills/guardex-merge-skills-to-dev/SKILL.md',
|
|
185
184
|
'.claude/commands/gitguardex.md',
|
|
186
185
|
LOCK_FILE_RELATIVE,
|
|
187
186
|
];
|
|
187
|
+
const REPO_SCAFFOLD_DIRECTORIES = ['bin'];
|
|
188
188
|
const OMX_SCAFFOLD_DIRECTORIES = [
|
|
189
189
|
'.omx',
|
|
190
190
|
'.omx/state',
|
|
@@ -266,14 +266,14 @@ const AI_SETUP_PROMPT = `GitGuardex (gx) setup checklist for Codex/Claude in thi
|
|
|
266
266
|
2) Bootstrap: gx setup
|
|
267
267
|
3) Repair: gx doctor
|
|
268
268
|
4) Task loop: bash scripts/codex-agent.sh "<task>" "<agent>"
|
|
269
|
-
or branch-start -> claim -> branch-finish
|
|
269
|
+
or branch-start -> python3 scripts/agent-file-locks.py claim -> branch-finish
|
|
270
270
|
5) Finish: gx finish --all
|
|
271
271
|
6) Cleanup: gx cleanup
|
|
272
272
|
7) OpenSpec: /opsx:propose -> /opsx:apply -> /opsx:archive
|
|
273
273
|
8) Optional: gx protect add release staging
|
|
274
274
|
9) Optional: gx sync --check && gx sync
|
|
275
275
|
10) Review bot: install https://github.com/apps/cr-gpt + set OPENAI_API_KEY
|
|
276
|
-
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
|
|
277
277
|
`;
|
|
278
278
|
|
|
279
279
|
const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
|
|
@@ -281,6 +281,7 @@ gh --version
|
|
|
281
281
|
gx setup
|
|
282
282
|
gx doctor
|
|
283
283
|
bash scripts/codex-agent.sh "<task>" "<agent>"
|
|
284
|
+
python3 scripts/agent-file-locks.py claim --branch "<agent-branch>" <file...>
|
|
284
285
|
gx finish --all
|
|
285
286
|
gx cleanup
|
|
286
287
|
gx protect add release staging
|
|
@@ -598,9 +599,27 @@ function toDestinationPath(relativeTemplatePath) {
|
|
|
598
599
|
throw new Error(`Unsupported template path: ${relativeTemplatePath}`);
|
|
599
600
|
}
|
|
600
601
|
|
|
601
|
-
function ensureParentDir(filePath, dryRun) {
|
|
602
|
+
function ensureParentDir(repoRoot, filePath, dryRun) {
|
|
602
603
|
if (dryRun) return;
|
|
603
|
-
|
|
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 });
|
|
604
623
|
}
|
|
605
624
|
|
|
606
625
|
function ensureExecutable(destinationPath, relativePath, dryRun) {
|
|
@@ -635,7 +654,7 @@ function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
|
|
|
635
654
|
}
|
|
636
655
|
}
|
|
637
656
|
|
|
638
|
-
ensureParentDir(destinationPath, dryRun);
|
|
657
|
+
ensureParentDir(repoRoot, destinationPath, dryRun);
|
|
639
658
|
if (!dryRun) {
|
|
640
659
|
fs.writeFileSync(destinationPath, sourceContent, 'utf8');
|
|
641
660
|
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
|
|
@@ -673,7 +692,7 @@ function ensureTemplateFilePresent(repoRoot, relativeTemplatePath, dryRun) {
|
|
|
673
692
|
return { status: 'skipped-conflict', file: destinationRelativePath };
|
|
674
693
|
}
|
|
675
694
|
|
|
676
|
-
ensureParentDir(destinationPath, dryRun);
|
|
695
|
+
ensureParentDir(repoRoot, destinationPath, dryRun);
|
|
677
696
|
if (!dryRun) {
|
|
678
697
|
fs.writeFileSync(destinationPath, sourceContent, 'utf8');
|
|
679
698
|
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
|
|
@@ -689,6 +708,22 @@ function lockFilePath(repoRoot) {
|
|
|
689
708
|
function ensureOmxScaffold(repoRoot, dryRun) {
|
|
690
709
|
const operations = [];
|
|
691
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
|
+
|
|
692
727
|
for (const relativeDir of OMX_SCAFFOLD_DIRECTORIES) {
|
|
693
728
|
const absoluteDir = path.join(repoRoot, relativeDir);
|
|
694
729
|
if (fs.existsSync(absoluteDir)) {
|
|
@@ -976,10 +1011,9 @@ function parseCommonArgs(rawArgs, defaults) {
|
|
|
976
1011
|
return options;
|
|
977
1012
|
}
|
|
978
1013
|
|
|
979
|
-
function
|
|
980
|
-
const
|
|
1014
|
+
function parseRepoTraversalArgs(rawArgs, defaults) {
|
|
1015
|
+
const traversalDefaults = {
|
|
981
1016
|
...defaults,
|
|
982
|
-
parentWorkspaceView: false,
|
|
983
1017
|
recursive: true,
|
|
984
1018
|
nestedMaxDepth: NESTED_REPO_DEFAULT_MAX_DEPTH,
|
|
985
1019
|
nestedSkipDirs: [],
|
|
@@ -989,20 +1023,12 @@ function parseSetupArgs(rawArgs, defaults) {
|
|
|
989
1023
|
|
|
990
1024
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
991
1025
|
const arg = rawArgs[index];
|
|
992
|
-
if (arg === '--parent-workspace-view') {
|
|
993
|
-
setupDefaults.parentWorkspaceView = true;
|
|
994
|
-
continue;
|
|
995
|
-
}
|
|
996
|
-
if (arg === '--no-parent-workspace-view') {
|
|
997
|
-
setupDefaults.parentWorkspaceView = false;
|
|
998
|
-
continue;
|
|
999
|
-
}
|
|
1000
1026
|
if (arg === '--no-recursive' || arg === '--no-nested' || arg === '--single-repo') {
|
|
1001
|
-
|
|
1027
|
+
traversalDefaults.recursive = false;
|
|
1002
1028
|
continue;
|
|
1003
1029
|
}
|
|
1004
1030
|
if (arg === '--recursive' || arg === '--nested') {
|
|
1005
|
-
|
|
1031
|
+
traversalDefaults.recursive = true;
|
|
1006
1032
|
continue;
|
|
1007
1033
|
}
|
|
1008
1034
|
if (arg === '--max-depth') {
|
|
@@ -1011,47 +1037,60 @@ function parseSetupArgs(rawArgs, defaults) {
|
|
|
1011
1037
|
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
1012
1038
|
throw new Error('--max-depth requires a positive integer');
|
|
1013
1039
|
}
|
|
1014
|
-
|
|
1040
|
+
traversalDefaults.nestedMaxDepth = parsed;
|
|
1015
1041
|
index += 1;
|
|
1016
1042
|
continue;
|
|
1017
1043
|
}
|
|
1018
1044
|
if (arg === '--skip-nested') {
|
|
1019
1045
|
const raw = requireValue(rawArgs, index, '--skip-nested');
|
|
1020
|
-
|
|
1046
|
+
traversalDefaults.nestedSkipDirs.push(raw);
|
|
1021
1047
|
index += 1;
|
|
1022
1048
|
continue;
|
|
1023
1049
|
}
|
|
1024
1050
|
if (arg === '--include-submodules') {
|
|
1025
|
-
|
|
1051
|
+
traversalDefaults.includeSubmodules = true;
|
|
1026
1052
|
continue;
|
|
1027
1053
|
}
|
|
1028
1054
|
forwardedArgs.push(arg);
|
|
1029
1055
|
}
|
|
1030
1056
|
|
|
1031
|
-
return parseCommonArgs(forwardedArgs,
|
|
1057
|
+
return parseCommonArgs(forwardedArgs, traversalDefaults);
|
|
1032
1058
|
}
|
|
1033
1059
|
|
|
1034
|
-
function
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1060
|
+
function parseSetupArgs(rawArgs, defaults) {
|
|
1061
|
+
const setupDefaults = {
|
|
1062
|
+
...defaults,
|
|
1063
|
+
parentWorkspaceView: false,
|
|
1038
1064
|
};
|
|
1065
|
+
const forwardedArgs = [];
|
|
1039
1066
|
|
|
1040
1067
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
1041
1068
|
const arg = rawArgs[index];
|
|
1042
|
-
if (arg === '--
|
|
1043
|
-
|
|
1044
|
-
index += 1;
|
|
1069
|
+
if (arg === '--parent-workspace-view') {
|
|
1070
|
+
setupDefaults.parentWorkspaceView = true;
|
|
1045
1071
|
continue;
|
|
1046
1072
|
}
|
|
1047
|
-
if (arg === '--
|
|
1048
|
-
|
|
1073
|
+
if (arg === '--no-parent-workspace-view') {
|
|
1074
|
+
setupDefaults.parentWorkspaceView = false;
|
|
1049
1075
|
continue;
|
|
1050
1076
|
}
|
|
1051
|
-
|
|
1077
|
+
forwardedArgs.push(arg);
|
|
1052
1078
|
}
|
|
1053
1079
|
|
|
1054
|
-
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
|
+
});
|
|
1055
1094
|
}
|
|
1056
1095
|
|
|
1057
1096
|
function normalizeWorkspacePath(relativePath) {
|
|
@@ -1541,7 +1580,7 @@ function finishDoctorSandboxBranch(blocked, metadata) {
|
|
|
1541
1580
|
|
|
1542
1581
|
const finishResult = run(
|
|
1543
1582
|
'bash',
|
|
1544
|
-
[finishScript, '--branch', metadata.branch, '--via-pr', '--wait-for-merge'],
|
|
1583
|
+
[finishScript, '--branch', metadata.branch, '--base', blocked.branch, '--via-pr', '--wait-for-merge'],
|
|
1545
1584
|
{ cwd: metadata.worktreePath, timeout: finishTimeoutMs },
|
|
1546
1585
|
);
|
|
1547
1586
|
if (isSpawnFailure(finishResult)) {
|
|
@@ -1580,6 +1619,33 @@ function finishDoctorSandboxBranch(blocked, metadata) {
|
|
|
1580
1619
|
};
|
|
1581
1620
|
}
|
|
1582
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
|
+
|
|
1583
1649
|
function runDoctorInSandbox(options, blocked) {
|
|
1584
1650
|
const startResult = startDoctorSandbox(blocked);
|
|
1585
1651
|
const metadata = startResult.metadata;
|
|
@@ -1603,6 +1669,10 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1603
1669
|
note: 'sandbox doctor did not complete successfully',
|
|
1604
1670
|
};
|
|
1605
1671
|
|
|
1672
|
+
let protectedBaseRepairSyncResult = {
|
|
1673
|
+
status: 'skipped',
|
|
1674
|
+
note: 'sandbox doctor did not complete successfully',
|
|
1675
|
+
};
|
|
1606
1676
|
let lockSyncResult = {
|
|
1607
1677
|
status: 'skipped',
|
|
1608
1678
|
note: 'sandbox doctor did not complete successfully',
|
|
@@ -1620,6 +1690,7 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1620
1690
|
note: 'sandbox doctor did not complete successfully',
|
|
1621
1691
|
};
|
|
1622
1692
|
if (nestedResult.status === 0) {
|
|
1693
|
+
protectedBaseRepairSyncResult = syncProtectedBaseDoctorRepairs(options, blocked);
|
|
1623
1694
|
const omxScaffoldOps = ensureOmxScaffold(blocked.repoRoot, Boolean(options.dryRun));
|
|
1624
1695
|
const changedOmxPaths = omxScaffoldOps.filter((operation) => operation.status !== 'unchanged');
|
|
1625
1696
|
if (changedOmxPaths.length === 0) {
|
|
@@ -1708,6 +1779,7 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1708
1779
|
JSON.stringify(
|
|
1709
1780
|
{
|
|
1710
1781
|
...parsed,
|
|
1782
|
+
protectedBaseRepairSync: protectedBaseRepairSyncResult,
|
|
1711
1783
|
sandboxOmxScaffoldSync: omxScaffoldSyncResult,
|
|
1712
1784
|
sandboxLockSync: lockSyncResult,
|
|
1713
1785
|
sandboxAutoCommit: autoCommitResult,
|
|
@@ -1748,6 +1820,16 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1748
1820
|
console.log(`[${TOOL_NAME}] Doctor sandbox auto-commit skipped: ${autoCommitResult.note}.`);
|
|
1749
1821
|
}
|
|
1750
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
|
+
|
|
1751
1833
|
if (finishResult.status === 'completed') {
|
|
1752
1834
|
console.log(`[${TOOL_NAME}] Auto-finish flow completed for sandbox branch '${metadata.branch}'.`);
|
|
1753
1835
|
if (finishResult.stdout) process.stdout.write(finishResult.stdout);
|
|
@@ -3897,6 +3979,10 @@ function runInstallInternal(options) {
|
|
|
3897
3979
|
}
|
|
3898
3980
|
const operations = [];
|
|
3899
3981
|
|
|
3982
|
+
if (!options.skipGitignore) {
|
|
3983
|
+
operations.push(ensureManagedGitignore(repoRoot, Boolean(options.dryRun)));
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3900
3986
|
operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
|
|
3901
3987
|
|
|
3902
3988
|
for (const templateFile of TEMPLATE_FILES) {
|
|
@@ -3904,9 +3990,6 @@ function runInstallInternal(options) {
|
|
|
3904
3990
|
}
|
|
3905
3991
|
|
|
3906
3992
|
operations.push(ensureLockRegistry(repoRoot, Boolean(options.dryRun)));
|
|
3907
|
-
if (!options.skipGitignore) {
|
|
3908
|
-
operations.push(ensureManagedGitignore(repoRoot, Boolean(options.dryRun)));
|
|
3909
|
-
}
|
|
3910
3993
|
|
|
3911
3994
|
if (!options.skipPackageJson) {
|
|
3912
3995
|
operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
|
|
@@ -3941,6 +4024,10 @@ function runFixInternal(options) {
|
|
|
3941
4024
|
}
|
|
3942
4025
|
const operations = [];
|
|
3943
4026
|
|
|
4027
|
+
if (!options.skipGitignore) {
|
|
4028
|
+
operations.push(ensureManagedGitignore(repoRoot, Boolean(options.dryRun)));
|
|
4029
|
+
}
|
|
4030
|
+
|
|
3944
4031
|
operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
|
|
3945
4032
|
|
|
3946
4033
|
for (const templateFile of TEMPLATE_FILES) {
|
|
@@ -3948,9 +4035,6 @@ function runFixInternal(options) {
|
|
|
3948
4035
|
}
|
|
3949
4036
|
|
|
3950
4037
|
operations.push(ensureLockRegistry(repoRoot, Boolean(options.dryRun)));
|
|
3951
|
-
if (!options.skipGitignore) {
|
|
3952
|
-
operations.push(ensureManagedGitignore(repoRoot, Boolean(options.dryRun)));
|
|
3953
|
-
}
|
|
3954
4038
|
|
|
3955
4039
|
const lockState = lockStateOrError(repoRoot);
|
|
3956
4040
|
if (!lockState.ok) {
|
|
@@ -4421,26 +4505,116 @@ function scan(rawArgs) {
|
|
|
4421
4505
|
}
|
|
4422
4506
|
|
|
4423
4507
|
function doctor(rawArgs) {
|
|
4424
|
-
const options =
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
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
|
+
}
|
|
4434
4525
|
|
|
4435
|
-
|
|
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
|
+
}
|
|
4585
|
+
|
|
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 });
|
|
4436
4610
|
if (blocked) {
|
|
4437
|
-
runDoctorInSandbox(
|
|
4611
|
+
runDoctorInSandbox(singleRepoOptions, blocked);
|
|
4438
4612
|
return;
|
|
4439
4613
|
}
|
|
4440
4614
|
|
|
4441
|
-
assertProtectedMainWriteAllowed(
|
|
4442
|
-
const fixPayload = runFixInternal(
|
|
4443
|
-
const scanResult = runScanInternal({ target:
|
|
4615
|
+
assertProtectedMainWriteAllowed(singleRepoOptions, 'doctor');
|
|
4616
|
+
const fixPayload = runFixInternal(singleRepoOptions);
|
|
4617
|
+
const scanResult = runScanInternal({ target: singleRepoOptions.target, json: false });
|
|
4444
4618
|
const currentBaseBranch = currentBranchName(scanResult.repoRoot);
|
|
4445
4619
|
const autoFinishSummary = scanResult.guardexEnabled === false
|
|
4446
4620
|
? {
|
|
@@ -4453,12 +4627,12 @@ function doctor(rawArgs) {
|
|
|
4453
4627
|
}
|
|
4454
4628
|
: autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
4455
4629
|
baseBranch: currentBaseBranch,
|
|
4456
|
-
dryRun:
|
|
4630
|
+
dryRun: singleRepoOptions.dryRun,
|
|
4457
4631
|
});
|
|
4458
4632
|
const safe = scanResult.guardexEnabled === false || (scanResult.errors === 0 && scanResult.warnings === 0);
|
|
4459
4633
|
const musafe = safe;
|
|
4460
4634
|
|
|
4461
|
-
if (
|
|
4635
|
+
if (singleRepoOptions.json) {
|
|
4462
4636
|
process.stdout.write(
|
|
4463
4637
|
JSON.stringify(
|
|
4464
4638
|
{
|
|
@@ -4469,7 +4643,7 @@ function doctor(rawArgs) {
|
|
|
4469
4643
|
fix: {
|
|
4470
4644
|
operations: fixPayload.operations,
|
|
4471
4645
|
hookResult: fixPayload.hookResult,
|
|
4472
|
-
dryRun: Boolean(
|
|
4646
|
+
dryRun: Boolean(singleRepoOptions.dryRun),
|
|
4473
4647
|
},
|
|
4474
4648
|
scan: {
|
|
4475
4649
|
guardexEnabled: scanResult.guardexEnabled !== false,
|
|
@@ -4488,7 +4662,7 @@ function doctor(rawArgs) {
|
|
|
4488
4662
|
return;
|
|
4489
4663
|
}
|
|
4490
4664
|
|
|
4491
|
-
printOperations('Doctor/fix', fixPayload,
|
|
4665
|
+
printOperations('Doctor/fix', fixPayload, singleRepoOptions.dryRun);
|
|
4492
4666
|
printScanResult(scanResult, false);
|
|
4493
4667
|
if (scanResult.guardexEnabled === false) {
|
|
4494
4668
|
console.log(`[${TOOL_NAME}] Repo-local Guardex enforcement is intentionally disabled.`);
|
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
|