@imdeadpool/guardex 5.0.7 → 5.0.8
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 +18 -4
- package/bin/multiagent-safety.js +438 -11
- package/package.json +1 -1
- package/templates/AGENTS.multiagent-safety.md +1 -0
- package/templates/githooks/pre-commit +20 -6
- package/templates/githooks/pre-push +3 -3
- package/templates/scripts/agent-branch-start.sh +6 -39
- package/templates/scripts/codex-agent.sh +144 -3
package/README.md
CHANGED
|
@@ -69,6 +69,7 @@ gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
|
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
If you use `scripts/codex-agent.sh`, the finish flow is auto-run after the Codex session exits.
|
|
72
|
+
It auto-commits sandbox changes, retries once after syncing if the branch moved behind base during the run, then pushes/opens PR merge flow against the current base branch.
|
|
72
73
|
|
|
73
74
|
## Visual workflow
|
|
74
75
|
|
|
@@ -97,6 +98,9 @@ gx status
|
|
|
97
98
|
# setup and repair
|
|
98
99
|
gx setup
|
|
99
100
|
gx doctor
|
|
101
|
+
# setup + repair another repo without switching your current repo checkout
|
|
102
|
+
gx setup --target /path/to/repo
|
|
103
|
+
gx doctor --target /path/to/repo
|
|
100
104
|
|
|
101
105
|
# protected branch management
|
|
102
106
|
gx protect list
|
|
@@ -108,7 +112,7 @@ gx sync --check
|
|
|
108
112
|
gx sync
|
|
109
113
|
|
|
110
114
|
# continuously monitor open PRs targeting current branch and dispatch codex-agent review/merge tasks
|
|
111
|
-
|
|
115
|
+
gx review --interval 30
|
|
112
116
|
|
|
113
117
|
# cleanup merged agent branches and hide clean stale agent worktrees
|
|
114
118
|
gx cleanup
|
|
@@ -123,7 +127,7 @@ gx report scorecard --repo github.com/recodeecom/multiagent-safety
|
|
|
123
127
|
Run this in your local shell to keep watching PRs targeting the current branch (or `--base <branch>`):
|
|
124
128
|
|
|
125
129
|
```sh
|
|
126
|
-
|
|
130
|
+
gx review --interval 30
|
|
127
131
|
```
|
|
128
132
|
|
|
129
133
|
Useful flags:
|
|
@@ -143,10 +147,12 @@ Note: the monitor dispatches Codex through explicit `--task/--agent/--base` flag
|
|
|
143
147
|
- `gx setup` checks GitHub CLI (`gh`) and prints install guidance if missing.
|
|
144
148
|
- Interactive self-update prompt defaults to **No** (`[y/N]`).
|
|
145
149
|
- In initialized repos, `setup`/`install`/`fix` block protected-base writes unless explicitly overridden.
|
|
146
|
-
- Direct commits/pushes to protected branches are blocked by default
|
|
150
|
+
- Direct commits/pushes to protected branches are blocked by default.
|
|
151
|
+
- Exception: VS Code Source Control commits are allowed on protected branches that exist only locally (no upstream and no remote branch).
|
|
147
152
|
- Optional repo override for manual VS Code protected-branch writes: `git config multiagent.allowVscodeProtectedBranchWrites true`.
|
|
148
153
|
- Codex/agent sessions stay blocked on protected branches and must use `agent/*` branch + PR workflow.
|
|
149
154
|
- On protected `main`, `gx doctor` auto-runs in a sandbox agent branch/worktree.
|
|
155
|
+
- In-place agent branching is disabled; `scripts/agent-branch-start.sh` always creates a separate worktree to keep your visible local/base branch unchanged.
|
|
150
156
|
- `scripts/agent-branch-start.sh` hydrates `scripts/codex-agent.sh` into new sandbox worktrees when missing, so auto-finish launcher flow stays available.
|
|
151
157
|
|
|
152
158
|
## Configure protected branches
|
|
@@ -239,9 +245,17 @@ npm pack --dry-run
|
|
|
239
245
|
|
|
240
246
|
## Release notes
|
|
241
247
|
|
|
248
|
+
### v5.0.8
|
|
249
|
+
|
|
250
|
+
- Fixed `bin/multiagent-safety.js` syntax regressions in the doctor sandbox flow (`Unexpected identifier` / `Unexpected end of input`) that were breaking CLI execution and CI tests.
|
|
251
|
+
- Restored `scripts/codex-agent.sh` from `templates/scripts/codex-agent.sh` so critical runtime helper parity checks pass in clean CI clones.
|
|
252
|
+
- Bumped package version from `5.0.7` to `5.0.8` for the next npm publish.
|
|
253
|
+
|
|
242
254
|
### v5.0.7
|
|
255
|
+
### Unreleased (generated draft, not versioned yet)
|
|
243
256
|
|
|
244
|
-
-
|
|
257
|
+
- Add the user-facing changes for the next release here before assigning a version number.
|
|
258
|
+
- Keep this section focused on behavior changes (`Added`, `Changed`, `Fixed`) rather than version-bump-only notes.
|
|
245
259
|
|
|
246
260
|
### v5.0.6
|
|
247
261
|
|
package/bin/multiagent-safety.js
CHANGED
|
@@ -83,6 +83,7 @@ const AGENTS_MARKER_END = '<!-- multiagent-safety:END -->';
|
|
|
83
83
|
const GITIGNORE_MARKER_START = '# multiagent-safety:START';
|
|
84
84
|
const GITIGNORE_MARKER_END = '# multiagent-safety:END';
|
|
85
85
|
const MANAGED_GITIGNORE_PATHS = [
|
|
86
|
+
'.omx/',
|
|
86
87
|
'scripts/agent-branch-start.sh',
|
|
87
88
|
'scripts/agent-branch-finish.sh',
|
|
88
89
|
'scripts/codex-agent.sh',
|
|
@@ -98,6 +99,17 @@ const MANAGED_GITIGNORE_PATHS = [
|
|
|
98
99
|
'.claude/commands/guardex.md',
|
|
99
100
|
LOCK_FILE_RELATIVE,
|
|
100
101
|
];
|
|
102
|
+
const OMX_SCAFFOLD_DIRECTORIES = [
|
|
103
|
+
'.omx',
|
|
104
|
+
'.omx/state',
|
|
105
|
+
'.omx/logs',
|
|
106
|
+
'.omx/plans',
|
|
107
|
+
'.omx/agent-worktrees',
|
|
108
|
+
];
|
|
109
|
+
const OMX_SCAFFOLD_FILES = new Map([
|
|
110
|
+
['.omx/notepad.md', '\n\n## WORKING MEMORY\n'],
|
|
111
|
+
['.omx/project-memory.json', '{}\n'],
|
|
112
|
+
]);
|
|
101
113
|
const COMMAND_TYPO_ALIASES = new Map([
|
|
102
114
|
['relaese', 'release'],
|
|
103
115
|
['realaese', 'release'],
|
|
@@ -115,6 +127,7 @@ const SUGGESTIBLE_COMMANDS = [
|
|
|
115
127
|
'setup',
|
|
116
128
|
'init',
|
|
117
129
|
'doctor',
|
|
130
|
+
'review',
|
|
118
131
|
'report',
|
|
119
132
|
'copy-prompt',
|
|
120
133
|
'copy-commands',
|
|
@@ -149,8 +162,7 @@ const CLI_COMMAND_DESCRIPTIONS = [
|
|
|
149
162
|
['version', 'Print GuardeX version'],
|
|
150
163
|
];
|
|
151
164
|
const AGENT_BOT_DESCRIPTIONS = [
|
|
152
|
-
['review', '
|
|
153
|
-
['start', 'bash scripts/review-bot-watch.sh --interval 30'],
|
|
165
|
+
['review', 'Start PR monitor + codex-agent review flow (default interval: 30s)'],
|
|
154
166
|
];
|
|
155
167
|
|
|
156
168
|
const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-Rex for your repo) in this repository for Codex or Claude.
|
|
@@ -172,7 +184,10 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
|
|
|
172
184
|
3) If setup reports warnings/errors, repair + re-check:
|
|
173
185
|
gx doctor
|
|
174
186
|
|
|
175
|
-
4)
|
|
187
|
+
4) Optional: start continuous PR monitor from this repo:
|
|
188
|
+
gx review --interval 30
|
|
189
|
+
|
|
190
|
+
5) Confirm next safe agent workflow commands:
|
|
176
191
|
bash scripts/codex-agent.sh "task" "agent-name"
|
|
177
192
|
bash scripts/agent-branch-start.sh "task" "agent-name"
|
|
178
193
|
python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
|
|
@@ -184,17 +199,17 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
|
|
|
184
199
|
Remove them explicitly when done:
|
|
185
200
|
gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
|
|
186
201
|
|
|
187
|
-
|
|
202
|
+
6) Optional: create OpenSpec planning workspace:
|
|
188
203
|
bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
|
|
189
204
|
|
|
190
|
-
|
|
205
|
+
7) Optional: protect extra branches:
|
|
191
206
|
gx protect add release staging
|
|
192
207
|
|
|
193
|
-
|
|
208
|
+
8) Optional: sync your current agent branch with latest base branch:
|
|
194
209
|
gx sync --check
|
|
195
210
|
gx sync
|
|
196
211
|
|
|
197
|
-
|
|
212
|
+
9) Optional (GitHub remote cleanup): enable:
|
|
198
213
|
Settings -> General -> Pull Requests -> Automatically delete head branches
|
|
199
214
|
`;
|
|
200
215
|
|
|
@@ -202,6 +217,7 @@ const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
|
|
|
202
217
|
gh --version
|
|
203
218
|
gx setup
|
|
204
219
|
gx doctor
|
|
220
|
+
gx review --interval 30
|
|
205
221
|
bash scripts/codex-agent.sh "task" "agent-name"
|
|
206
222
|
bash scripts/agent-branch-start.sh "task" "agent-name"
|
|
207
223
|
python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
|
|
@@ -349,7 +365,9 @@ NOTES
|
|
|
349
365
|
- ${SHORT_TOOL_NAME} init is an alias of ${SHORT_TOOL_NAME} setup
|
|
350
366
|
- ${TOOL_NAME} setup asks for Y/N approval before global installs
|
|
351
367
|
- ${TOOL_NAME} setup checks GitHub CLI (gh) and prints install guidance if missing
|
|
368
|
+
- For other repos: ${SHORT_TOOL_NAME} setup --target <repo-path> then ${SHORT_TOOL_NAME} doctor --target <repo-path>
|
|
352
369
|
- In initialized repos, setup/install/fix block in-place writes on protected main by default
|
|
370
|
+
- setup/doctor auto-finish clean pending agent/* branches via PR flow into the current local base branch
|
|
353
371
|
- doctor auto-runs in a sandbox agent branch/worktree on protected main and tries auto-finish PR flow
|
|
354
372
|
- agent-branch-finish merges by default and keeps agent branches/worktrees until explicit cleanup
|
|
355
373
|
- use '${SHORT_TOOL_NAME} cleanup' to remove merged agent branches/worktrees (optionally remote refs too)
|
|
@@ -359,7 +377,8 @@ NOTES
|
|
|
359
377
|
console.log(`
|
|
360
378
|
[${TOOL_NAME}] No git repository detected in current directory.
|
|
361
379
|
[${TOOL_NAME}] Start from a repo root, or pass an explicit target:
|
|
362
|
-
${TOOL_NAME} setup --target <path-to-git-repo
|
|
380
|
+
${TOOL_NAME} setup --target <path-to-git-repo>
|
|
381
|
+
${TOOL_NAME} doctor --target <path-to-git-repo>`);
|
|
363
382
|
}
|
|
364
383
|
}
|
|
365
384
|
|
|
@@ -502,6 +521,45 @@ function lockFilePath(repoRoot) {
|
|
|
502
521
|
return path.join(repoRoot, LOCK_FILE_RELATIVE);
|
|
503
522
|
}
|
|
504
523
|
|
|
524
|
+
function ensureOmxScaffold(repoRoot, dryRun) {
|
|
525
|
+
const operations = [];
|
|
526
|
+
|
|
527
|
+
for (const relativeDir of OMX_SCAFFOLD_DIRECTORIES) {
|
|
528
|
+
const absoluteDir = path.join(repoRoot, relativeDir);
|
|
529
|
+
if (fs.existsSync(absoluteDir)) {
|
|
530
|
+
if (!fs.statSync(absoluteDir).isDirectory()) {
|
|
531
|
+
throw new Error(`Expected directory at ${relativeDir} but found a file.`);
|
|
532
|
+
}
|
|
533
|
+
operations.push({ status: 'unchanged', file: relativeDir });
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (!dryRun) {
|
|
538
|
+
fs.mkdirSync(absoluteDir, { recursive: true });
|
|
539
|
+
}
|
|
540
|
+
operations.push({ status: 'created', file: relativeDir });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
for (const [relativeFile, defaultContent] of OMX_SCAFFOLD_FILES.entries()) {
|
|
544
|
+
const absoluteFile = path.join(repoRoot, relativeFile);
|
|
545
|
+
if (fs.existsSync(absoluteFile)) {
|
|
546
|
+
if (!fs.statSync(absoluteFile).isFile()) {
|
|
547
|
+
throw new Error(`Expected file at ${relativeFile} but found a directory.`);
|
|
548
|
+
}
|
|
549
|
+
operations.push({ status: 'unchanged', file: relativeFile });
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!dryRun) {
|
|
554
|
+
fs.mkdirSync(path.dirname(absoluteFile), { recursive: true });
|
|
555
|
+
fs.writeFileSync(absoluteFile, defaultContent, 'utf8');
|
|
556
|
+
}
|
|
557
|
+
operations.push({ status: 'created', file: relativeFile });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return operations;
|
|
561
|
+
}
|
|
562
|
+
|
|
505
563
|
function ensureLockRegistry(repoRoot, dryRun) {
|
|
506
564
|
const absolutePath = lockFilePath(repoRoot);
|
|
507
565
|
if (fs.existsSync(absolutePath)) {
|
|
@@ -844,6 +902,33 @@ function isSpawnFailure(result) {
|
|
|
844
902
|
return Boolean(result?.error) && typeof result?.status !== 'number';
|
|
845
903
|
}
|
|
846
904
|
|
|
905
|
+
function ensureRepoBranch(repoRoot, branch) {
|
|
906
|
+
const current = currentBranchName(repoRoot);
|
|
907
|
+
if (current === branch) {
|
|
908
|
+
return { ok: true, changed: false };
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const checkoutResult = run('git', ['-C', repoRoot, 'checkout', branch], { timeout: 20_000 });
|
|
912
|
+
if (isSpawnFailure(checkoutResult)) {
|
|
913
|
+
return {
|
|
914
|
+
ok: false,
|
|
915
|
+
changed: false,
|
|
916
|
+
stdout: checkoutResult.stdout || '',
|
|
917
|
+
stderr: checkoutResult.stderr || '',
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
if (checkoutResult.status !== 0) {
|
|
921
|
+
return {
|
|
922
|
+
ok: false,
|
|
923
|
+
changed: false,
|
|
924
|
+
stdout: checkoutResult.stdout || '',
|
|
925
|
+
stderr: checkoutResult.stderr || '',
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return { ok: true, changed: true };
|
|
930
|
+
}
|
|
931
|
+
|
|
847
932
|
function doctorSandboxBranchPrefix() {
|
|
848
933
|
const now = new Date();
|
|
849
934
|
const stamp = [
|
|
@@ -945,12 +1030,26 @@ function startDoctorSandbox(blocked) {
|
|
|
945
1030
|
throw startResult.error;
|
|
946
1031
|
}
|
|
947
1032
|
if (startResult.status !== 0) {
|
|
948
|
-
|
|
1033
|
+
return startDoctorSandboxFallback(blocked);
|
|
949
1034
|
}
|
|
950
1035
|
|
|
951
1036
|
const metadata = extractAgentBranchStartMetadata(startResult.stdout);
|
|
952
|
-
|
|
953
|
-
|
|
1037
|
+
const currentBranch = currentBranchName(blocked.repoRoot);
|
|
1038
|
+
const worktreePath = metadata.worktreePath ? path.resolve(metadata.worktreePath) : '';
|
|
1039
|
+
const repoRootPath = path.resolve(blocked.repoRoot);
|
|
1040
|
+
const hasSafeWorktree = Boolean(worktreePath) && worktreePath !== repoRootPath;
|
|
1041
|
+
const branchChanged = Boolean(currentBranch) && currentBranch !== blocked.branch;
|
|
1042
|
+
|
|
1043
|
+
if (!hasSafeWorktree || branchChanged) {
|
|
1044
|
+
const restoreResult = ensureRepoBranch(blocked.repoRoot, blocked.branch);
|
|
1045
|
+
if (!restoreResult.ok) {
|
|
1046
|
+
const detail = [restoreResult.stderr, restoreResult.stdout].filter(Boolean).join('\n').trim();
|
|
1047
|
+
throw new Error(
|
|
1048
|
+
`doctor sandbox startup switched protected base checkout and could not restore '${blocked.branch}'.` +
|
|
1049
|
+
(detail ? `\n${detail}` : ''),
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
return startDoctorSandboxFallback(blocked);
|
|
954
1053
|
}
|
|
955
1054
|
|
|
956
1055
|
return {
|
|
@@ -1197,7 +1296,35 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1197
1296
|
status: 'skipped',
|
|
1198
1297
|
note: 'sandbox doctor did not complete successfully',
|
|
1199
1298
|
};
|
|
1299
|
+
let postSandboxAutoFinishSummary = {
|
|
1300
|
+
enabled: false,
|
|
1301
|
+
attempted: 0,
|
|
1302
|
+
completed: 0,
|
|
1303
|
+
skipped: 0,
|
|
1304
|
+
failed: 0,
|
|
1305
|
+
details: ['Skipped auto-finish sweep (sandbox doctor did not complete successfully).'],
|
|
1306
|
+
};
|
|
1307
|
+
let omxScaffoldSyncResult = {
|
|
1308
|
+
status: 'skipped',
|
|
1309
|
+
note: 'sandbox doctor did not complete successfully',
|
|
1310
|
+
};
|
|
1200
1311
|
if (nestedResult.status === 0) {
|
|
1312
|
+
const omxScaffoldOps = ensureOmxScaffold(blocked.repoRoot, Boolean(options.dryRun));
|
|
1313
|
+
const changedOmxPaths = omxScaffoldOps.filter((operation) => operation.status !== 'unchanged');
|
|
1314
|
+
if (changedOmxPaths.length === 0) {
|
|
1315
|
+
omxScaffoldSyncResult = {
|
|
1316
|
+
status: 'unchanged',
|
|
1317
|
+
note: '.omx scaffold already in sync',
|
|
1318
|
+
operations: omxScaffoldOps,
|
|
1319
|
+
};
|
|
1320
|
+
} else {
|
|
1321
|
+
omxScaffoldSyncResult = {
|
|
1322
|
+
status: options.dryRun ? 'would-sync' : 'synced',
|
|
1323
|
+
note: `${options.dryRun ? 'would sync' : 'synced'} ${changedOmxPaths.length} .omx path(s)`,
|
|
1324
|
+
operations: omxScaffoldOps,
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1201
1328
|
if (!options.dryRun) {
|
|
1202
1329
|
autoCommitResult = autoCommitDoctorSandboxChanges(metadata);
|
|
1203
1330
|
if (autoCommitResult.status === 'committed') {
|
|
@@ -1253,6 +1380,12 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1253
1380
|
};
|
|
1254
1381
|
}
|
|
1255
1382
|
}
|
|
1383
|
+
|
|
1384
|
+
postSandboxAutoFinishSummary = autoFinishReadyAgentBranches(blocked.repoRoot, {
|
|
1385
|
+
baseBranch: blocked.branch,
|
|
1386
|
+
dryRun: options.dryRun,
|
|
1387
|
+
excludeBranches: [metadata.branch],
|
|
1388
|
+
});
|
|
1256
1389
|
}
|
|
1257
1390
|
|
|
1258
1391
|
if (options.json) {
|
|
@@ -1264,9 +1397,11 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1264
1397
|
JSON.stringify(
|
|
1265
1398
|
{
|
|
1266
1399
|
...parsed,
|
|
1400
|
+
sandboxOmxScaffoldSync: omxScaffoldSyncResult,
|
|
1267
1401
|
sandboxLockSync: lockSyncResult,
|
|
1268
1402
|
sandboxAutoCommit: autoCommitResult,
|
|
1269
1403
|
sandboxFinish: finishResult,
|
|
1404
|
+
autoFinish: postSandboxAutoFinishSummary,
|
|
1270
1405
|
},
|
|
1271
1406
|
null,
|
|
1272
1407
|
2,
|
|
@@ -1332,6 +1467,26 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1332
1467
|
} else {
|
|
1333
1468
|
console.log(`[${TOOL_NAME}] Lock registry sync skipped: ${lockSyncResult.note}.`);
|
|
1334
1469
|
}
|
|
1470
|
+
|
|
1471
|
+
if (postSandboxAutoFinishSummary.enabled) {
|
|
1472
|
+
console.log(
|
|
1473
|
+
`[${TOOL_NAME}] Auto-finish sweep (base=${blocked.branch}): attempted=${postSandboxAutoFinishSummary.attempted}, completed=${postSandboxAutoFinishSummary.completed}, skipped=${postSandboxAutoFinishSummary.skipped}, failed=${postSandboxAutoFinishSummary.failed}`,
|
|
1474
|
+
);
|
|
1475
|
+
for (const detail of postSandboxAutoFinishSummary.details) {
|
|
1476
|
+
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
1477
|
+
}
|
|
1478
|
+
} else if (postSandboxAutoFinishSummary.details.length > 0) {
|
|
1479
|
+
console.log(`[${TOOL_NAME}] ${postSandboxAutoFinishSummary.details[0]}`);
|
|
1480
|
+
}
|
|
1481
|
+
if (omxScaffoldSyncResult.status === 'synced') {
|
|
1482
|
+
console.log(`[${TOOL_NAME}] Synced .omx scaffold back to protected branch workspace.`);
|
|
1483
|
+
} else if (omxScaffoldSyncResult.status === 'unchanged') {
|
|
1484
|
+
console.log(`[${TOOL_NAME}] .omx scaffold already aligned in protected branch workspace.`);
|
|
1485
|
+
} else if (omxScaffoldSyncResult.status === 'would-sync') {
|
|
1486
|
+
console.log(`[${TOOL_NAME}] Dry run: would sync .omx scaffold back to protected branch workspace.`);
|
|
1487
|
+
} else {
|
|
1488
|
+
console.log(`[${TOOL_NAME}] .omx scaffold sync skipped: ${omxScaffoldSyncResult.note}.`);
|
|
1489
|
+
}
|
|
1335
1490
|
}
|
|
1336
1491
|
}
|
|
1337
1492
|
|
|
@@ -1363,6 +1518,18 @@ function parseTargetFlag(rawArgs, defaultTarget = process.cwd()) {
|
|
|
1363
1518
|
return { target, args: remaining };
|
|
1364
1519
|
}
|
|
1365
1520
|
|
|
1521
|
+
function parseReviewArgs(rawArgs) {
|
|
1522
|
+
const parsed = parseTargetFlag(rawArgs, process.cwd());
|
|
1523
|
+
const passthroughArgs = [...parsed.args];
|
|
1524
|
+
if (passthroughArgs[0] === 'start') {
|
|
1525
|
+
passthroughArgs.shift();
|
|
1526
|
+
}
|
|
1527
|
+
return {
|
|
1528
|
+
target: parsed.target,
|
|
1529
|
+
passthroughArgs,
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1366
1533
|
function parseReportArgs(rawArgs) {
|
|
1367
1534
|
const options = {
|
|
1368
1535
|
target: process.cwd(),
|
|
@@ -1628,6 +1795,203 @@ function listLocalUserBranches(repoRoot) {
|
|
|
1628
1795
|
return [branchName];
|
|
1629
1796
|
}
|
|
1630
1797
|
|
|
1798
|
+
function listLocalAgentBranches(repoRoot) {
|
|
1799
|
+
const result = gitRun(
|
|
1800
|
+
repoRoot,
|
|
1801
|
+
['for-each-ref', '--format=%(refname:short)', 'refs/heads/agent/'],
|
|
1802
|
+
{ allowFailure: true },
|
|
1803
|
+
);
|
|
1804
|
+
if (result.status !== 0) {
|
|
1805
|
+
return [];
|
|
1806
|
+
}
|
|
1807
|
+
return uniquePreserveOrder(
|
|
1808
|
+
String(result.stdout || '')
|
|
1809
|
+
.split('\n')
|
|
1810
|
+
.map((item) => item.trim())
|
|
1811
|
+
.filter(Boolean),
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
function mapWorktreePathsByBranch(repoRoot) {
|
|
1816
|
+
const result = gitRun(repoRoot, ['worktree', 'list', '--porcelain'], { allowFailure: true });
|
|
1817
|
+
const map = new Map();
|
|
1818
|
+
if (result.status !== 0) {
|
|
1819
|
+
return map;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
const lines = String(result.stdout || '').split('\n');
|
|
1823
|
+
let currentWorktree = '';
|
|
1824
|
+
for (const line of lines) {
|
|
1825
|
+
if (line.startsWith('worktree ')) {
|
|
1826
|
+
currentWorktree = line.slice('worktree '.length).trim();
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
if (line.startsWith('branch refs/heads/')) {
|
|
1830
|
+
const branchName = line.slice('branch refs/heads/'.length).trim();
|
|
1831
|
+
if (currentWorktree && branchName) {
|
|
1832
|
+
map.set(branchName, currentWorktree);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
return map;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function hasSignificantWorkingTreeChanges(worktreePath) {
|
|
1840
|
+
const result = run('git', ['-C', worktreePath, 'status', '--porcelain']);
|
|
1841
|
+
if (result.status !== 0) {
|
|
1842
|
+
return true;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
const lines = String(result.stdout || '')
|
|
1846
|
+
.split('\n')
|
|
1847
|
+
.map((line) => line.trimEnd())
|
|
1848
|
+
.filter((line) => line.length > 0);
|
|
1849
|
+
|
|
1850
|
+
for (const line of lines) {
|
|
1851
|
+
const pathPart = (line.length > 3 ? line.slice(3) : '').trim();
|
|
1852
|
+
if (!pathPart) continue;
|
|
1853
|
+
if (pathPart === LOCK_FILE_RELATIVE) continue;
|
|
1854
|
+
if (pathPart.startsWith(`${LOCK_FILE_RELATIVE} -> `)) continue;
|
|
1855
|
+
if (pathPart.endsWith(` -> ${LOCK_FILE_RELATIVE}`)) continue;
|
|
1856
|
+
return true;
|
|
1857
|
+
}
|
|
1858
|
+
return false;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
1862
|
+
const baseBranch = String(options.baseBranch || '').trim();
|
|
1863
|
+
const dryRun = Boolean(options.dryRun);
|
|
1864
|
+
const excludedBranches = new Set(
|
|
1865
|
+
Array.isArray(options.excludeBranches)
|
|
1866
|
+
? options.excludeBranches.map((branch) => String(branch || '').trim()).filter(Boolean)
|
|
1867
|
+
: [],
|
|
1868
|
+
);
|
|
1869
|
+
|
|
1870
|
+
const summary = {
|
|
1871
|
+
enabled: true,
|
|
1872
|
+
baseBranch,
|
|
1873
|
+
attempted: 0,
|
|
1874
|
+
completed: 0,
|
|
1875
|
+
skipped: 0,
|
|
1876
|
+
failed: 0,
|
|
1877
|
+
details: [],
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1880
|
+
if (!baseBranch || baseBranch === 'HEAD' || baseBranch.startsWith('agent/')) {
|
|
1881
|
+
summary.enabled = false;
|
|
1882
|
+
summary.details.push('Skipped auto-finish sweep (base branch is missing or not a non-agent local branch).');
|
|
1883
|
+
return summary;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
if (String(process.env.MUSAFETY_DOCTOR_SANDBOX || '') === '1') {
|
|
1887
|
+
summary.enabled = false;
|
|
1888
|
+
summary.details.push('Skipped auto-finish sweep inside doctor sandbox pass.');
|
|
1889
|
+
return summary;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
if (String(process.env.MUSAFETY_SKIP_AUTO_FINISH_READY_BRANCHES || '') === '1') {
|
|
1893
|
+
summary.enabled = false;
|
|
1894
|
+
summary.details.push('Skipped auto-finish sweep (MUSAFETY_SKIP_AUTO_FINISH_READY_BRANCHES=1).');
|
|
1895
|
+
return summary;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
if (dryRun) {
|
|
1899
|
+
summary.enabled = false;
|
|
1900
|
+
summary.details.push('Skipped auto-finish sweep in dry-run mode.');
|
|
1901
|
+
return summary;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
const finishScript = path.join(repoRoot, 'scripts', 'agent-branch-finish.sh');
|
|
1905
|
+
if (!fs.existsSync(finishScript)) {
|
|
1906
|
+
summary.enabled = false;
|
|
1907
|
+
summary.details.push(`Skipped auto-finish sweep (missing ${path.relative(repoRoot, finishScript)}).`);
|
|
1908
|
+
return summary;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
const hasOrigin = gitRun(repoRoot, ['remote', 'get-url', 'origin'], { allowFailure: true }).status === 0;
|
|
1912
|
+
if (!hasOrigin) {
|
|
1913
|
+
summary.enabled = false;
|
|
1914
|
+
summary.details.push('Skipped auto-finish sweep (origin remote missing).');
|
|
1915
|
+
return summary;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
const ghBin = process.env.MUSAFETY_GH_BIN || 'gh';
|
|
1919
|
+
if (run(ghBin, ['--version']).status !== 0) {
|
|
1920
|
+
summary.enabled = false;
|
|
1921
|
+
summary.details.push(`Skipped auto-finish sweep (${ghBin} not available).`);
|
|
1922
|
+
return summary;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
const branchWorktrees = mapWorktreePathsByBranch(repoRoot);
|
|
1926
|
+
const agentBranches = listLocalAgentBranches(repoRoot);
|
|
1927
|
+
if (agentBranches.length === 0) {
|
|
1928
|
+
summary.enabled = false;
|
|
1929
|
+
summary.details.push('No local agent branches found for auto-finish sweep.');
|
|
1930
|
+
return summary;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
for (const branch of agentBranches) {
|
|
1934
|
+
if (excludedBranches.has(branch)) {
|
|
1935
|
+
summary.skipped += 1;
|
|
1936
|
+
summary.details.push(`[skip] ${branch}: excluded from this auto-finish sweep.`);
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
if (branch === baseBranch) {
|
|
1941
|
+
summary.skipped += 1;
|
|
1942
|
+
summary.details.push(`[skip] ${branch}: source branch equals base branch.`);
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
let counts;
|
|
1947
|
+
try {
|
|
1948
|
+
counts = aheadBehind(repoRoot, branch, baseBranch);
|
|
1949
|
+
} catch (error) {
|
|
1950
|
+
summary.failed += 1;
|
|
1951
|
+
summary.details.push(`[fail] ${branch}: unable to compute ahead/behind (${error.message}).`);
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
if (counts.ahead <= 0) {
|
|
1956
|
+
summary.skipped += 1;
|
|
1957
|
+
summary.details.push(`[skip] ${branch}: already merged into ${baseBranch}.`);
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
const branchWorktree = branchWorktrees.get(branch) || '';
|
|
1962
|
+
if (branchWorktree && hasSignificantWorkingTreeChanges(branchWorktree)) {
|
|
1963
|
+
summary.skipped += 1;
|
|
1964
|
+
summary.details.push(`[skip] ${branch}: dirty worktree (${branchWorktree}).`);
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
summary.attempted += 1;
|
|
1969
|
+
const finishArgs = [
|
|
1970
|
+
finishScript,
|
|
1971
|
+
'--branch',
|
|
1972
|
+
branch,
|
|
1973
|
+
'--base',
|
|
1974
|
+
baseBranch,
|
|
1975
|
+
'--via-pr',
|
|
1976
|
+
'--cleanup',
|
|
1977
|
+
];
|
|
1978
|
+
const finishResult = run('bash', finishArgs, { cwd: repoRoot });
|
|
1979
|
+
const combinedOutput = [finishResult.stdout || '', finishResult.stderr || ''].join('\n').trim();
|
|
1980
|
+
|
|
1981
|
+
if (finishResult.status === 0) {
|
|
1982
|
+
summary.completed += 1;
|
|
1983
|
+
summary.details.push(`[done] ${branch}: auto-finish completed.`);
|
|
1984
|
+
continue;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
summary.failed += 1;
|
|
1988
|
+
const tail = combinedOutput ? ` ${combinedOutput.split('\n').slice(-2).join(' | ')}` : '';
|
|
1989
|
+
summary.details.push(`[fail] ${branch}: auto-finish failed.${tail}`);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
return summary;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1631
1995
|
function ensureSetupProtectedBranches(repoRoot, dryRun) {
|
|
1632
1996
|
const localUserBranches = listLocalUserBranches(repoRoot);
|
|
1633
1997
|
if (localUserBranches.length === 0) {
|
|
@@ -2325,6 +2689,8 @@ function runInstallInternal(options) {
|
|
|
2325
2689
|
const repoRoot = resolveRepoRoot(options.target);
|
|
2326
2690
|
const operations = [];
|
|
2327
2691
|
|
|
2692
|
+
operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
|
|
2693
|
+
|
|
2328
2694
|
for (const templateFile of TEMPLATE_FILES) {
|
|
2329
2695
|
operations.push(copyTemplateFile(repoRoot, templateFile, Boolean(options.force), Boolean(options.dryRun)));
|
|
2330
2696
|
}
|
|
@@ -2351,6 +2717,8 @@ function runFixInternal(options) {
|
|
|
2351
2717
|
const repoRoot = resolveRepoRoot(options.target);
|
|
2352
2718
|
const operations = [];
|
|
2353
2719
|
|
|
2720
|
+
operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
|
|
2721
|
+
|
|
2354
2722
|
for (const templateFile of TEMPLATE_FILES) {
|
|
2355
2723
|
operations.push(ensureTemplateFilePresent(repoRoot, templateFile, Boolean(options.dryRun)));
|
|
2356
2724
|
}
|
|
@@ -2404,6 +2772,8 @@ function runScanInternal(options) {
|
|
|
2404
2772
|
const findings = [];
|
|
2405
2773
|
|
|
2406
2774
|
const requiredPaths = [
|
|
2775
|
+
...OMX_SCAFFOLD_DIRECTORIES,
|
|
2776
|
+
...Array.from(OMX_SCAFFOLD_FILES.keys()),
|
|
2407
2777
|
...TEMPLATE_FILES.map((entry) => toDestinationPath(entry)),
|
|
2408
2778
|
LOCK_FILE_RELATIVE,
|
|
2409
2779
|
];
|
|
@@ -2755,6 +3125,11 @@ function doctor(rawArgs) {
|
|
|
2755
3125
|
assertProtectedMainWriteAllowed(options, 'doctor');
|
|
2756
3126
|
const fixPayload = runFixInternal(options);
|
|
2757
3127
|
const scanResult = runScanInternal({ target: options.target, json: false });
|
|
3128
|
+
const currentBaseBranch = currentBranchName(scanResult.repoRoot);
|
|
3129
|
+
const autoFinishSummary = autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
3130
|
+
baseBranch: currentBaseBranch,
|
|
3131
|
+
dryRun: options.dryRun,
|
|
3132
|
+
});
|
|
2758
3133
|
const safe = scanResult.errors === 0 && scanResult.warnings === 0;
|
|
2759
3134
|
const musafe = safe;
|
|
2760
3135
|
|
|
@@ -2776,6 +3151,7 @@ function doctor(rawArgs) {
|
|
|
2776
3151
|
warnings: scanResult.warnings,
|
|
2777
3152
|
findings: scanResult.findings,
|
|
2778
3153
|
},
|
|
3154
|
+
autoFinish: autoFinishSummary,
|
|
2779
3155
|
},
|
|
2780
3156
|
null,
|
|
2781
3157
|
2,
|
|
@@ -2787,6 +3163,16 @@ function doctor(rawArgs) {
|
|
|
2787
3163
|
|
|
2788
3164
|
printOperations('Doctor/fix', fixPayload, options.dryRun);
|
|
2789
3165
|
printScanResult(scanResult, false);
|
|
3166
|
+
if (autoFinishSummary.enabled) {
|
|
3167
|
+
console.log(
|
|
3168
|
+
`[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
|
|
3169
|
+
);
|
|
3170
|
+
for (const detail of autoFinishSummary.details) {
|
|
3171
|
+
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
3172
|
+
}
|
|
3173
|
+
} else if (autoFinishSummary.details.length > 0) {
|
|
3174
|
+
console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
|
|
3175
|
+
}
|
|
2790
3176
|
if (safe) {
|
|
2791
3177
|
console.log(`[${TOOL_NAME}] ✅ Repo is fully safe.`);
|
|
2792
3178
|
} else {
|
|
@@ -2797,6 +3183,27 @@ function doctor(rawArgs) {
|
|
|
2797
3183
|
setExitCodeFromScan(scanResult);
|
|
2798
3184
|
}
|
|
2799
3185
|
|
|
3186
|
+
function review(rawArgs) {
|
|
3187
|
+
const options = parseReviewArgs(rawArgs);
|
|
3188
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
3189
|
+
const reviewScriptPath = path.join(repoRoot, 'scripts', 'review-bot-watch.sh');
|
|
3190
|
+
if (!fs.existsSync(reviewScriptPath)) {
|
|
3191
|
+
throw new Error(
|
|
3192
|
+
`Missing review bot script: ${reviewScriptPath}\n` +
|
|
3193
|
+
`Run '${SHORT_TOOL_NAME} setup --target ${repoRoot}' then '${SHORT_TOOL_NAME} doctor --target ${repoRoot}'.`,
|
|
3194
|
+
);
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
const result = run('bash', [reviewScriptPath, ...options.passthroughArgs], { cwd: repoRoot });
|
|
3198
|
+
if (isSpawnFailure(result)) {
|
|
3199
|
+
throw result.error;
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
3203
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
3204
|
+
process.exitCode = typeof result.status === 'number' ? result.status : 1;
|
|
3205
|
+
}
|
|
3206
|
+
|
|
2800
3207
|
function report(rawArgs) {
|
|
2801
3208
|
const options = parseReportArgs(rawArgs);
|
|
2802
3209
|
const subcommand = options.subcommand || 'help';
|
|
@@ -2955,7 +3362,22 @@ function setup(rawArgs) {
|
|
|
2955
3362
|
}
|
|
2956
3363
|
|
|
2957
3364
|
const scanResult = runScanInternal({ target: options.target, json: false });
|
|
3365
|
+
const currentBaseBranch = currentBranchName(scanResult.repoRoot);
|
|
3366
|
+
const autoFinishSummary = autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
3367
|
+
baseBranch: currentBaseBranch,
|
|
3368
|
+
dryRun: options.dryRun,
|
|
3369
|
+
});
|
|
2958
3370
|
printScanResult(scanResult, false);
|
|
3371
|
+
if (autoFinishSummary.enabled) {
|
|
3372
|
+
console.log(
|
|
3373
|
+
`[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
|
|
3374
|
+
);
|
|
3375
|
+
for (const detail of autoFinishSummary.details) {
|
|
3376
|
+
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
3377
|
+
}
|
|
3378
|
+
} else if (autoFinishSummary.details.length > 0) {
|
|
3379
|
+
console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
|
|
3380
|
+
}
|
|
2959
3381
|
|
|
2960
3382
|
if (scanResult.errors === 0 && scanResult.warnings === 0) {
|
|
2961
3383
|
console.log(`[${TOOL_NAME}] ✅ Setup complete.`);
|
|
@@ -3388,6 +3810,11 @@ function main() {
|
|
|
3388
3810
|
return;
|
|
3389
3811
|
}
|
|
3390
3812
|
|
|
3813
|
+
if (command === 'review') {
|
|
3814
|
+
review(rest);
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3391
3818
|
if (command === 'report') {
|
|
3392
3819
|
report(rest);
|
|
3393
3820
|
return;
|
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
- Before deleting/replacing code, each agent must read the latest session comments/handoffs first and confirm the target code is in their owned scope.
|
|
11
11
|
- If ownership is unclear or overlaps, stop that edit, post a blocker comment, and let the leader/integrator reassign scope.
|
|
12
12
|
- For git isolation, each agent must start on a dedicated branch via `scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"`.
|
|
13
|
+
- In-place branch mode is disallowed: never switch the active local/base checkout to an agent branch.
|
|
13
14
|
- Treat the base branch (`main` or the user's current local base branch) as read-only while the agent branch is active.
|
|
14
15
|
- Agent completion defaults to `scripts/codex-agent.sh`, which auto-finishes the branch (auto-commit changed files, push/create PR, attempt merge, and pull the local base branch after merge).
|
|
15
16
|
- Auto-finish now waits for required checks/merge and then cleans merged sandbox branch/worktree by default.
|
|
@@ -24,13 +24,13 @@ if [[ -n "${CODEX_THREAD_ID:-}" || -n "${OMX_SESSION_ID:-}" || "${CODEX_CI:-0}"
|
|
|
24
24
|
fi
|
|
25
25
|
|
|
26
26
|
is_vscode_git_context=0
|
|
27
|
-
if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n "${VSCODE_IPC_HOOK_CLI:-}"
|
|
27
|
+
if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n "${VSCODE_IPC_HOOK_CLI:-}" ]]; then
|
|
28
28
|
is_vscode_git_context=1
|
|
29
29
|
fi
|
|
30
30
|
|
|
31
31
|
allow_vscode_protected_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
|
|
32
32
|
if [[ -z "$allow_vscode_protected_raw" ]]; then
|
|
33
|
-
allow_vscode_protected_raw="
|
|
33
|
+
allow_vscode_protected_raw="true"
|
|
34
34
|
fi
|
|
35
35
|
allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"
|
|
36
36
|
|
|
@@ -55,6 +55,15 @@ for protected_branch in $protected_branches_raw; do
|
|
|
55
55
|
fi
|
|
56
56
|
done
|
|
57
57
|
|
|
58
|
+
is_local_only_branch=0
|
|
59
|
+
if [[ "$is_protected_branch" == "1" ]]; then
|
|
60
|
+
upstream_ref="$(git for-each-ref --format='%(upstream:short)' "refs/heads/${branch}" | head -n 1)"
|
|
61
|
+
remote_branch_ref="$(git for-each-ref --format='%(refname:short)' "refs/remotes/*/${branch}" | head -n 1)"
|
|
62
|
+
if [[ -z "$upstream_ref" && -z "$remote_branch_ref" ]]; then
|
|
63
|
+
is_local_only_branch=1
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
58
67
|
codex_require_agent_branch_raw="${MUSAFETY_CODEX_REQUIRE_AGENT_BRANCH:-$(git config --get multiagent.codexRequireAgentBranch || true)}"
|
|
59
68
|
if [[ -z "$codex_require_agent_branch_raw" ]]; then
|
|
60
69
|
codex_require_agent_branch_raw="true"
|
|
@@ -124,8 +133,10 @@ MSG
|
|
|
124
133
|
fi
|
|
125
134
|
|
|
126
135
|
if [[ "$is_protected_branch" == "1" ]]; then
|
|
127
|
-
if [[ "$is_codex_session" != "1" && "$is_vscode_git_context" == "1"
|
|
128
|
-
|
|
136
|
+
if [[ "$is_codex_session" != "1" && "$is_vscode_git_context" == "1" ]]; then
|
|
137
|
+
if [[ "$allow_vscode_protected_branch_writes" == "1" || "$is_local_only_branch" == "1" ]]; then
|
|
138
|
+
exit 0
|
|
139
|
+
fi
|
|
129
140
|
fi
|
|
130
141
|
|
|
131
142
|
if [[ "$is_unborn_branch" == "1" && "$is_codex_session" != "1" ]]; then
|
|
@@ -144,8 +155,11 @@ Use an agent branch first:
|
|
|
144
155
|
After finishing work:
|
|
145
156
|
bash scripts/agent-branch-finish.sh
|
|
146
157
|
|
|
147
|
-
Optional repo
|
|
148
|
-
git config multiagent.allowVscodeProtectedBranchWrites
|
|
158
|
+
Optional repo hard-block for VS Code protected-branch commits:
|
|
159
|
+
git config multiagent.allowVscodeProtectedBranchWrites false
|
|
160
|
+
|
|
161
|
+
VS Code Source Control commits on protected local-only branches
|
|
162
|
+
(no upstream and no remote branch) are allowed automatically.
|
|
149
163
|
|
|
150
164
|
Temporary bypass (not recommended):
|
|
151
165
|
ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
|
|
@@ -12,7 +12,7 @@ fi
|
|
|
12
12
|
|
|
13
13
|
allow_vscode_protected_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
|
|
14
14
|
if [[ -z "$allow_vscode_protected_raw" ]]; then
|
|
15
|
-
allow_vscode_protected_raw="
|
|
15
|
+
allow_vscode_protected_raw="true"
|
|
16
16
|
fi
|
|
17
17
|
allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"
|
|
18
18
|
|
|
@@ -77,8 +77,8 @@ if [[ "${#blocked_refs[@]}" -gt 0 ]]; then
|
|
|
77
77
|
echo "[agent-branch-guard] Push to protected branch blocked."
|
|
78
78
|
echo "[agent-branch-guard] Protected target(s): ${blocked_refs[*]}"
|
|
79
79
|
echo "[agent-branch-guard] Use an agent branch and merge via PR."
|
|
80
|
-
echo "[agent-branch-guard] Optional VS Code
|
|
81
|
-
echo " git config multiagent.allowVscodeProtectedBranchWrites
|
|
80
|
+
echo "[agent-branch-guard] Optional repo hard-block for VS Code protected-branch push:"
|
|
81
|
+
echo " git config multiagent.allowVscodeProtectedBranchWrites false"
|
|
82
82
|
echo
|
|
83
83
|
echo "Temporary bypass (not recommended):"
|
|
84
84
|
echo " ALLOW_PUSH_ON_PROTECTED_BRANCH=1 git push ..."
|
|
@@ -5,8 +5,6 @@ TASK_NAME="task"
|
|
|
5
5
|
AGENT_NAME="agent"
|
|
6
6
|
BASE_BRANCH=""
|
|
7
7
|
BASE_BRANCH_EXPLICIT=0
|
|
8
|
-
WORKTREE_MODE=1
|
|
9
|
-
ALLOW_IN_PLACE=0
|
|
10
8
|
WORKTREE_ROOT_REL=".omx/agent-worktrees"
|
|
11
9
|
POSITIONAL_ARGS=()
|
|
12
10
|
|
|
@@ -25,13 +23,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
25
23
|
BASE_BRANCH_EXPLICIT=1
|
|
26
24
|
shift 2
|
|
27
25
|
;;
|
|
28
|
-
--in-place)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
--allow-in-place)
|
|
33
|
-
ALLOW_IN_PLACE=1
|
|
34
|
-
shift
|
|
26
|
+
--in-place|--allow-in-place)
|
|
27
|
+
echo "[agent-branch-start] In-place branch mode is disabled." >&2
|
|
28
|
+
echo "[agent-branch-start] This command always creates an isolated worktree to keep your active checkout unchanged." >&2
|
|
29
|
+
exit 1
|
|
35
30
|
;;
|
|
36
31
|
--worktree-root)
|
|
37
32
|
WORKTREE_ROOT_REL="${2:-.omx/agent-worktrees}"
|
|
@@ -47,7 +42,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
47
42
|
;;
|
|
48
43
|
-*)
|
|
49
44
|
echo "[agent-branch-start] Unknown option: $1" >&2
|
|
50
|
-
echo "Usage: $0 [task] [agent] [base] [--
|
|
45
|
+
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
|
|
51
46
|
exit 1
|
|
52
47
|
;;
|
|
53
48
|
*)
|
|
@@ -59,7 +54,7 @@ done
|
|
|
59
54
|
|
|
60
55
|
if [[ "${#POSITIONAL_ARGS[@]}" -gt 3 ]]; then
|
|
61
56
|
echo "[agent-branch-start] Too many positional arguments." >&2
|
|
62
|
-
echo "Usage: $0 [task] [agent] [base] [--
|
|
57
|
+
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
|
|
63
58
|
exit 1
|
|
64
59
|
fi
|
|
65
60
|
|
|
@@ -237,34 +232,6 @@ while git show-ref --verify --quiet "refs/heads/${branch_name}"; do
|
|
|
237
232
|
branch_suffix=$((branch_suffix + 1))
|
|
238
233
|
done
|
|
239
234
|
|
|
240
|
-
if [[ "$WORKTREE_MODE" -eq 0 ]]; then
|
|
241
|
-
if [[ "$ALLOW_IN_PLACE" -ne 1 ]]; then
|
|
242
|
-
echo "[agent-branch-start] --in-place is blocked by default to prevent accidental edits on protected branches." >&2
|
|
243
|
-
echo "[agent-branch-start] If you really need it, pass both: --in-place --allow-in-place" >&2
|
|
244
|
-
exit 1
|
|
245
|
-
fi
|
|
246
|
-
|
|
247
|
-
if ! git diff --quiet || ! git diff --cached --quiet; then
|
|
248
|
-
echo "[agent-branch-start] Working tree is not clean. Commit/stash changes before starting an in-place branch." >&2
|
|
249
|
-
exit 1
|
|
250
|
-
fi
|
|
251
|
-
|
|
252
|
-
current_branch="$(git rev-parse --abbrev-ref HEAD)"
|
|
253
|
-
if [[ "$current_branch" != "$BASE_BRANCH" ]]; then
|
|
254
|
-
git checkout "$BASE_BRANCH"
|
|
255
|
-
fi
|
|
256
|
-
|
|
257
|
-
if git show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
|
|
258
|
-
git pull --ff-only origin "$BASE_BRANCH"
|
|
259
|
-
fi
|
|
260
|
-
|
|
261
|
-
git checkout -b "$branch_name"
|
|
262
|
-
git -C "$repo_root" config "branch.${branch_name}.musafetyBase" "$BASE_BRANCH" >/dev/null 2>&1 || true
|
|
263
|
-
echo "[agent-branch-start] Created in-place branch: ${branch_name}"
|
|
264
|
-
echo "$branch_name"
|
|
265
|
-
exit 0
|
|
266
|
-
fi
|
|
267
|
-
|
|
268
235
|
worktree_root="${repo_root}/${WORKTREE_ROOT_REL}"
|
|
269
236
|
mkdir -p "$worktree_root"
|
|
270
237
|
worktree_path="${worktree_root}/${branch_name//\//__}"
|
|
@@ -125,6 +125,106 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
|
125
125
|
fi
|
|
126
126
|
repo_root="$(git rev-parse --show-toplevel)"
|
|
127
127
|
|
|
128
|
+
sanitize_slug() {
|
|
129
|
+
local raw="$1"
|
|
130
|
+
local fallback="${2:-task}"
|
|
131
|
+
local slug
|
|
132
|
+
slug="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
|
|
133
|
+
if [[ -z "$slug" ]]; then
|
|
134
|
+
slug="$fallback"
|
|
135
|
+
fi
|
|
136
|
+
printf '%s' "$slug"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
resolve_start_base_branch() {
|
|
140
|
+
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
|
|
141
|
+
printf '%s' "$BASE_BRANCH"
|
|
142
|
+
return 0
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
local configured_base
|
|
146
|
+
configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
|
|
147
|
+
if [[ -n "$configured_base" ]]; then
|
|
148
|
+
printf '%s' "$configured_base"
|
|
149
|
+
return 0
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
local current_branch
|
|
153
|
+
current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
154
|
+
if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then
|
|
155
|
+
printf '%s' "$current_branch"
|
|
156
|
+
return 0
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
printf 'dev'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
resolve_start_ref() {
|
|
163
|
+
local base_branch="$1"
|
|
164
|
+
git -C "$repo_root" fetch origin "$base_branch" --quiet >/dev/null 2>&1 || true
|
|
165
|
+
if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
|
|
166
|
+
printf 'origin/%s' "$base_branch"
|
|
167
|
+
return 0
|
|
168
|
+
fi
|
|
169
|
+
if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${base_branch}"; then
|
|
170
|
+
printf '%s' "$base_branch"
|
|
171
|
+
return 0
|
|
172
|
+
fi
|
|
173
|
+
return 1
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
restore_repo_branch_if_changed() {
|
|
177
|
+
local expected_branch="$1"
|
|
178
|
+
if [[ -z "$expected_branch" || "$expected_branch" == "HEAD" ]]; then
|
|
179
|
+
return 0
|
|
180
|
+
fi
|
|
181
|
+
local current_branch
|
|
182
|
+
current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
183
|
+
if [[ -z "$current_branch" || "$current_branch" == "$expected_branch" ]]; then
|
|
184
|
+
return 0
|
|
185
|
+
fi
|
|
186
|
+
git -C "$repo_root" checkout "$expected_branch" >/dev/null 2>&1
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
start_sandbox_fallback() {
|
|
190
|
+
local base_branch start_ref timestamp task_slug agent_slug branch_name_base branch_name suffix
|
|
191
|
+
local worktree_root worktree_path
|
|
192
|
+
|
|
193
|
+
base_branch="$(resolve_start_base_branch)"
|
|
194
|
+
if ! start_ref="$(resolve_start_ref "$base_branch")"; then
|
|
195
|
+
echo "[codex-agent] Unable to resolve base ref for fallback sandbox start: ${base_branch}" >&2
|
|
196
|
+
return 1
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
timestamp="$(date +%Y%m%d-%H%M%S)"
|
|
200
|
+
task_slug="$(sanitize_slug "$TASK_NAME" "task")"
|
|
201
|
+
agent_slug="$(sanitize_slug "$AGENT_NAME" "agent")"
|
|
202
|
+
branch_name_base="agent/${agent_slug}/${timestamp}-${task_slug}"
|
|
203
|
+
branch_name="$branch_name_base"
|
|
204
|
+
suffix=2
|
|
205
|
+
while git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch_name}"; do
|
|
206
|
+
branch_name="${branch_name_base}-${suffix}"
|
|
207
|
+
suffix=$((suffix + 1))
|
|
208
|
+
done
|
|
209
|
+
|
|
210
|
+
worktree_root="${repo_root}/.omx/agent-worktrees"
|
|
211
|
+
mkdir -p "$worktree_root"
|
|
212
|
+
worktree_path="${worktree_root}/${branch_name//\//__}"
|
|
213
|
+
if [[ -e "$worktree_path" ]]; then
|
|
214
|
+
echo "[codex-agent] Fallback worktree path already exists: $worktree_path" >&2
|
|
215
|
+
return 1
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" "$start_ref" >/dev/null
|
|
219
|
+
git -C "$repo_root" config "branch.${branch_name}.musafetyBase" "$base_branch" >/dev/null 2>&1 || true
|
|
220
|
+
if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
|
|
221
|
+
git -C "$worktree_path" branch --set-upstream-to="origin/${base_branch}" "$branch_name" >/dev/null 2>&1 || true
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
printf '[agent-branch-start] Created branch: %s\n' "$branch_name"
|
|
225
|
+
printf '[agent-branch-start] Worktree: %s\n' "$worktree_path"
|
|
226
|
+
}
|
|
227
|
+
|
|
128
228
|
if [[ ! -x "${repo_root}/scripts/agent-branch-start.sh" ]]; then
|
|
129
229
|
echo "[codex-agent] Missing scripts/agent-branch-start.sh. Run: gx setup" >&2
|
|
130
230
|
exit 1
|
|
@@ -135,12 +235,53 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 ]]; then
|
|
|
135
235
|
start_args+=("$BASE_BRANCH")
|
|
136
236
|
fi
|
|
137
237
|
|
|
138
|
-
|
|
139
|
-
|
|
238
|
+
initial_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
239
|
+
start_output=""
|
|
240
|
+
start_status=0
|
|
241
|
+
set +e
|
|
242
|
+
start_output="$(bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1)"
|
|
243
|
+
start_status=$?
|
|
244
|
+
set -e
|
|
140
245
|
|
|
141
246
|
worktree_path="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | tail -n1)"
|
|
247
|
+
current_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
248
|
+
resolved_repo_root="$(cd "$repo_root" && pwd -P)"
|
|
249
|
+
resolved_worktree_path=""
|
|
250
|
+
if [[ -n "$worktree_path" && -d "$worktree_path" ]]; then
|
|
251
|
+
resolved_worktree_path="$(cd "$worktree_path" && pwd -P)"
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
fallback_reason=""
|
|
255
|
+
if [[ "$start_status" -ne 0 ]]; then
|
|
256
|
+
fallback_reason="starter exited with status ${start_status}"
|
|
257
|
+
elif [[ -z "$worktree_path" ]]; then
|
|
258
|
+
fallback_reason="starter did not report worktree path"
|
|
259
|
+
elif [[ -n "$resolved_worktree_path" && "$resolved_worktree_path" == "$resolved_repo_root" ]]; then
|
|
260
|
+
fallback_reason="starter pointed to active checkout path"
|
|
261
|
+
elif [[ -n "$initial_repo_branch" && -n "$current_repo_branch" && "$current_repo_branch" != "$initial_repo_branch" ]]; then
|
|
262
|
+
fallback_reason="starter switched active checkout branch"
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
if [[ -n "$fallback_reason" ]]; then
|
|
266
|
+
if ! restore_repo_branch_if_changed "$initial_repo_branch"; then
|
|
267
|
+
echo "[codex-agent] agent-branch-start changed the active checkout branch and restore failed." >&2
|
|
268
|
+
echo "[codex-agent] Run 'gx setup --target ${repo_root}' and 'gx doctor --target ${repo_root}', then retry." >&2
|
|
269
|
+
exit 1
|
|
270
|
+
fi
|
|
271
|
+
if [[ -n "$start_output" ]]; then
|
|
272
|
+
printf '%s\n' "$start_output" >&2
|
|
273
|
+
fi
|
|
274
|
+
echo "[codex-agent] Unsafe starter output (${fallback_reason}); creating sandbox worktree directly." >&2
|
|
275
|
+
start_output="$(start_sandbox_fallback)"
|
|
276
|
+
printf '%s\n' "$start_output"
|
|
277
|
+
worktree_path="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | tail -n1)"
|
|
278
|
+
else
|
|
279
|
+
printf '%s\n' "$start_output"
|
|
280
|
+
fi
|
|
281
|
+
|
|
142
282
|
if [[ -z "$worktree_path" ]]; then
|
|
143
|
-
echo "[codex-agent] Could not determine sandbox worktree path from
|
|
283
|
+
echo "[codex-agent] Could not determine sandbox worktree path from sandbox startup output." >&2
|
|
284
|
+
echo "[codex-agent] Run 'gx setup --target ${repo_root}' and 'gx doctor --target ${repo_root}', then retry." >&2
|
|
144
285
|
exit 1
|
|
145
286
|
fi
|
|
146
287
|
|