@imdeadpool/guardex 7.0.15 → 7.0.16
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 +6 -0
- package/bin/multiagent-safety.js +310 -55
- package/package.json +2 -1
- package/templates/scripts/agent-branch-merge.sh +421 -0
- package/templates/scripts/agent-branch-start.sh +43 -3
- package/templates/scripts/codex-agent.sh +47 -2
- package/templates/scripts/openspec/init-plan-workspace.sh +42 -0
package/README.md
CHANGED
|
@@ -529,6 +529,12 @@ npm pack --dry-run
|
|
|
529
529
|
<details>
|
|
530
530
|
<summary><strong>v7.x</strong></summary>
|
|
531
531
|
|
|
532
|
+
### v7.0.16
|
|
533
|
+
- `gx doctor` now keeps nested repo repair runs visibly progressing, and overlapping integration work stays off the protected base branch instead of trying to merge back on `main`.
|
|
534
|
+
- Cleanup and finish flows are less brittle: `codex-agent` no longer waits on PRs that can never exist, and prune cleanup now walks both managed worktree roots so stale sandboxes get removed consistently.
|
|
535
|
+
- Mirror-sync diagnostics are quieter: when the mirror PAT is unset, Guardex now skips the sync path instead of marking the run red, and shared `ralplan` lanes stay easier to identify during handoff/debugging.
|
|
536
|
+
- Bumped `@imdeadpool/guardex` from `7.0.15` → `7.0.16` after npm rejected a republish over the already-published `7.0.15`.
|
|
537
|
+
|
|
532
538
|
### v7.0.15
|
|
533
539
|
- `gx doctor` no longer blocks recursive nested protected-repo repairs on child PR merge waits; nested sandboxes now force `--no-wait-for-merge` so the parent repair loop can continue.
|
|
534
540
|
- `gx setup` can now refresh managed files from protected `main` through a temporary sandbox branch/worktree, sync the managed outputs back to the visible base checkout, and prune the sandbox afterward.
|
package/bin/multiagent-safety.js
CHANGED
|
@@ -89,6 +89,7 @@ const TEMPLATE_ROOT = path.resolve(__dirname, '..', 'templates');
|
|
|
89
89
|
const TEMPLATE_FILES = [
|
|
90
90
|
'scripts/agent-branch-start.sh',
|
|
91
91
|
'scripts/agent-branch-finish.sh',
|
|
92
|
+
'scripts/agent-branch-merge.sh',
|
|
92
93
|
'scripts/codex-agent.sh',
|
|
93
94
|
'scripts/guardex-docker-loader.sh',
|
|
94
95
|
'scripts/review-bot-watch.sh',
|
|
@@ -112,6 +113,7 @@ const TEMPLATE_FILES = [
|
|
|
112
113
|
const REQUIRED_WORKFLOW_FILES = [
|
|
113
114
|
'scripts/agent-branch-start.sh',
|
|
114
115
|
'scripts/agent-branch-finish.sh',
|
|
116
|
+
'scripts/agent-branch-merge.sh',
|
|
115
117
|
'scripts/guardex-docker-loader.sh',
|
|
116
118
|
'scripts/agent-worktree-prune.sh',
|
|
117
119
|
'scripts/agent-file-locks.py',
|
|
@@ -126,6 +128,7 @@ const REQUIRED_PACKAGE_SCRIPTS = {
|
|
|
126
128
|
'agent:codex': 'bash ./scripts/codex-agent.sh',
|
|
127
129
|
'agent:branch:start': 'bash ./scripts/agent-branch-start.sh',
|
|
128
130
|
'agent:branch:finish': 'bash ./scripts/agent-branch-finish.sh',
|
|
131
|
+
'agent:branch:merge': 'bash ./scripts/agent-branch-merge.sh',
|
|
129
132
|
'agent:cleanup': 'gx cleanup',
|
|
130
133
|
'agent:hooks:install': 'bash ./scripts/install-agent-git-hooks.sh',
|
|
131
134
|
'agent:locks:claim': 'python3 ./scripts/agent-file-locks.py claim',
|
|
@@ -149,6 +152,7 @@ const REQUIRED_PACKAGE_SCRIPTS = {
|
|
|
149
152
|
const EXECUTABLE_RELATIVE_PATHS = new Set([
|
|
150
153
|
'scripts/agent-branch-start.sh',
|
|
151
154
|
'scripts/agent-branch-finish.sh',
|
|
155
|
+
'scripts/agent-branch-merge.sh',
|
|
152
156
|
'scripts/codex-agent.sh',
|
|
153
157
|
'scripts/guardex-docker-loader.sh',
|
|
154
158
|
'scripts/review-bot-watch.sh',
|
|
@@ -171,6 +175,7 @@ const CRITICAL_GUARDRAIL_PATHS = new Set([
|
|
|
171
175
|
'.githooks/post-checkout',
|
|
172
176
|
'scripts/agent-branch-start.sh',
|
|
173
177
|
'scripts/agent-branch-finish.sh',
|
|
178
|
+
'scripts/agent-branch-merge.sh',
|
|
174
179
|
'scripts/agent-worktree-prune.sh',
|
|
175
180
|
'scripts/codex-agent.sh',
|
|
176
181
|
'scripts/agent-file-locks.py',
|
|
@@ -233,6 +238,7 @@ const SUGGESTIBLE_COMMANDS = [
|
|
|
233
238
|
'setup',
|
|
234
239
|
'doctor',
|
|
235
240
|
'agents',
|
|
241
|
+
'merge',
|
|
236
242
|
'finish',
|
|
237
243
|
'report',
|
|
238
244
|
'protect',
|
|
@@ -257,6 +263,7 @@ const CLI_COMMAND_DESCRIPTIONS = [
|
|
|
257
263
|
['setup', 'Install, repair, and verify guardrails (flags: --repair, --install-only, --target)'],
|
|
258
264
|
['doctor', 'Repair drift + verify (auto-sandboxes on protected main)'],
|
|
259
265
|
['protect', 'Manage protected branches (list/add/remove/set/reset)'],
|
|
266
|
+
['merge', 'Create/reuse an integration lane and merge overlapping agent branches'],
|
|
260
267
|
['sync', 'Sync agent branches with origin/<base>'],
|
|
261
268
|
['finish', 'Commit + PR + merge completed agent branches (--all, --branch)'],
|
|
262
269
|
['cleanup', 'Prune merged/stale agent branches and worktrees'],
|
|
@@ -280,6 +287,9 @@ const DEPRECATED_COMMAND_ALIASES = new Map([
|
|
|
280
287
|
const AGENT_BOT_DESCRIPTIONS = [
|
|
281
288
|
['agents', 'Start/stop review + cleanup bots for this repo'],
|
|
282
289
|
];
|
|
290
|
+
const DOCTOR_AUTO_FINISH_DETAIL_LIMIT = 6;
|
|
291
|
+
const DOCTOR_AUTO_FINISH_BRANCH_LABEL_MAX = 72;
|
|
292
|
+
const DOCTOR_AUTO_FINISH_MESSAGE_MAX = 160;
|
|
283
293
|
|
|
284
294
|
function envFlagIsTruthy(raw) {
|
|
285
295
|
const lowered = String(raw || '').trim().toLowerCase();
|
|
@@ -301,13 +311,14 @@ const AI_SETUP_PROMPT = `GitGuardex (gx) setup checklist for Codex/Claude in thi
|
|
|
301
311
|
3) Repair: gx doctor
|
|
302
312
|
4) Task loop: bash scripts/codex-agent.sh "<task>" "<agent>"
|
|
303
313
|
or branch-start -> python3 scripts/agent-file-locks.py claim -> branch-finish
|
|
304
|
-
5)
|
|
305
|
-
6)
|
|
306
|
-
7)
|
|
307
|
-
8)
|
|
308
|
-
9) Optional: gx
|
|
309
|
-
10)
|
|
310
|
-
11)
|
|
314
|
+
5) Integrate: gx merge --branch <agent-a> --branch <agent-b>
|
|
315
|
+
6) Finish: gx finish --all
|
|
316
|
+
7) Cleanup: gx cleanup
|
|
317
|
+
8) OpenSpec: /opsx:propose -> /opsx:apply -> /opsx:archive
|
|
318
|
+
9) Optional: gx protect add release staging
|
|
319
|
+
10) Optional: gx sync --check && gx sync
|
|
320
|
+
11) Review bot: install https://github.com/apps/cr-gpt + set OPENAI_API_KEY
|
|
321
|
+
12) Fork sync: install https://github.com/apps/pull + cp .github/pull.yml.example .github/pull.yml
|
|
311
322
|
`;
|
|
312
323
|
|
|
313
324
|
const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
|
|
@@ -316,6 +327,7 @@ gx setup
|
|
|
316
327
|
gx doctor
|
|
317
328
|
bash scripts/codex-agent.sh "<task>" "<agent>"
|
|
318
329
|
python3 scripts/agent-file-locks.py claim --branch "<agent-branch>" <file...>
|
|
330
|
+
gx merge --branch "<agent-a>" --branch "<agent-b>"
|
|
319
331
|
gx finish --all
|
|
320
332
|
gx cleanup
|
|
321
333
|
gx protect add release staging
|
|
@@ -504,6 +516,113 @@ function run(cmd, args, options = {}) {
|
|
|
504
516
|
});
|
|
505
517
|
}
|
|
506
518
|
|
|
519
|
+
function formatElapsedDuration(ms) {
|
|
520
|
+
const durationMs = Number.isFinite(ms) ? Math.max(0, ms) : 0;
|
|
521
|
+
if (durationMs < 1000) {
|
|
522
|
+
return `${Math.round(durationMs)}ms`;
|
|
523
|
+
}
|
|
524
|
+
if (durationMs < 10_000) {
|
|
525
|
+
return `${(durationMs / 1000).toFixed(1)}s`;
|
|
526
|
+
}
|
|
527
|
+
return `${Math.round(durationMs / 1000)}s`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function truncateMiddle(value, maxLength) {
|
|
531
|
+
const text = String(value || '');
|
|
532
|
+
const limit = Number.isFinite(maxLength) ? Math.max(4, maxLength) : 0;
|
|
533
|
+
if (!limit || text.length <= limit) {
|
|
534
|
+
return text;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const visible = limit - 1;
|
|
538
|
+
const headLength = Math.ceil(visible / 2);
|
|
539
|
+
const tailLength = Math.floor(visible / 2);
|
|
540
|
+
return `${text.slice(0, headLength)}…${text.slice(text.length - tailLength)}`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function truncateTail(value, maxLength) {
|
|
544
|
+
const text = String(value || '');
|
|
545
|
+
const limit = Number.isFinite(maxLength) ? Math.max(4, maxLength) : 0;
|
|
546
|
+
if (!limit || text.length <= limit) {
|
|
547
|
+
return text;
|
|
548
|
+
}
|
|
549
|
+
return `${text.slice(0, limit - 1)}…`;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function compactAutoFinishPathSegments(message) {
|
|
553
|
+
return String(message || '').replace(/\((\/[^)]+)\)/g, (_, rawPath) => {
|
|
554
|
+
if (
|
|
555
|
+
rawPath.includes(`${path.sep}.omx${path.sep}agent-worktrees${path.sep}`) ||
|
|
556
|
+
rawPath.includes(`${path.sep}.omc${path.sep}agent-worktrees${path.sep}`)
|
|
557
|
+
) {
|
|
558
|
+
return `(${path.basename(rawPath)})`;
|
|
559
|
+
}
|
|
560
|
+
return `(${truncateMiddle(rawPath, 72)})`;
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function summarizeAutoFinishDetail(detail) {
|
|
565
|
+
const trimmed = String(detail || '').trim();
|
|
566
|
+
const match = trimmed.match(/^\[(\w+)\]\s+([^:]+):\s*(.*)$/);
|
|
567
|
+
if (!match) {
|
|
568
|
+
return truncateTail(compactAutoFinishPathSegments(trimmed), DOCTOR_AUTO_FINISH_MESSAGE_MAX);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const [, status, rawBranch, rawMessage] = match;
|
|
572
|
+
const branch = truncateMiddle(rawBranch, DOCTOR_AUTO_FINISH_BRANCH_LABEL_MAX);
|
|
573
|
+
let message = String(rawMessage || '').trim();
|
|
574
|
+
|
|
575
|
+
if (status === 'fail') {
|
|
576
|
+
message = message.replace(/^auto-finish failed\.?\s*/i, '');
|
|
577
|
+
if (/\[agent-sync-guard\]/.test(message) && /Resolve conflicts/i.test(message)) {
|
|
578
|
+
message = 'rebase conflict in finish flow; run rebase --continue or rebase --abort in the source-probe worktree';
|
|
579
|
+
} else if (/unable to compute ahead\/behind/i.test(message)) {
|
|
580
|
+
const aheadBehindMatch = message.match(/unable to compute ahead\/behind(?: \([^)]+\))?/i);
|
|
581
|
+
if (aheadBehindMatch) {
|
|
582
|
+
message = aheadBehindMatch[0];
|
|
583
|
+
}
|
|
584
|
+
} else if (/remote ref does not exist/i.test(message)) {
|
|
585
|
+
message = 'branch merged, but the remote ref was already removed during cleanup';
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
message = compactAutoFinishPathSegments(message)
|
|
590
|
+
.replace(/\s+\|\s+/g, '; ')
|
|
591
|
+
.trim();
|
|
592
|
+
|
|
593
|
+
return `[${status}] ${branch}: ${truncateTail(message, DOCTOR_AUTO_FINISH_MESSAGE_MAX)}`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function printAutoFinishSummary(summary, options = {}) {
|
|
597
|
+
const enabled = Boolean(summary && summary.enabled);
|
|
598
|
+
const details = Array.isArray(summary && summary.details) ? summary.details : [];
|
|
599
|
+
const baseBranch = String(options.baseBranch || summary?.baseBranch || '').trim();
|
|
600
|
+
const verbose = Boolean(options.verbose);
|
|
601
|
+
const detailLimit = Number.isFinite(options.detailLimit)
|
|
602
|
+
? Math.max(0, options.detailLimit)
|
|
603
|
+
: DOCTOR_AUTO_FINISH_DETAIL_LIMIT;
|
|
604
|
+
|
|
605
|
+
if (enabled) {
|
|
606
|
+
console.log(
|
|
607
|
+
`[${TOOL_NAME}] Auto-finish sweep (base=${baseBranch}): attempted=${summary.attempted}, completed=${summary.completed}, skipped=${summary.skipped}, failed=${summary.failed}`,
|
|
608
|
+
);
|
|
609
|
+
const visibleDetails = verbose ? details : details.slice(0, detailLimit).map(summarizeAutoFinishDetail);
|
|
610
|
+
for (const detail of visibleDetails) {
|
|
611
|
+
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
612
|
+
}
|
|
613
|
+
if (!verbose && details.length > detailLimit) {
|
|
614
|
+
console.log(
|
|
615
|
+
`[${TOOL_NAME}] … ${details.length - detailLimit} more branch result(s). Re-run with --verbose-auto-finish for full details.`,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (details.length > 0) {
|
|
622
|
+
console.log(`[${TOOL_NAME}] ${verbose ? details[0] : summarizeAutoFinishDetail(details[0])}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
507
626
|
function gitRun(repoRoot, args, { allowFailure = false } = {}) {
|
|
508
627
|
const result = run('git', ['-C', repoRoot, ...args]);
|
|
509
628
|
if (!allowFailure && result.status !== 0) {
|
|
@@ -1121,7 +1240,7 @@ function parseSetupArgs(rawArgs, defaults) {
|
|
|
1121
1240
|
}
|
|
1122
1241
|
|
|
1123
1242
|
function parseDoctorArgs(rawArgs) {
|
|
1124
|
-
|
|
1243
|
+
const doctorDefaults = {
|
|
1125
1244
|
target: process.cwd(),
|
|
1126
1245
|
dropStaleLocks: true,
|
|
1127
1246
|
skipAgents: false,
|
|
@@ -1131,7 +1250,24 @@ function parseDoctorArgs(rawArgs) {
|
|
|
1131
1250
|
json: false,
|
|
1132
1251
|
allowProtectedBaseWrite: false,
|
|
1133
1252
|
waitForMerge: true,
|
|
1134
|
-
|
|
1253
|
+
verboseAutoFinish: false,
|
|
1254
|
+
};
|
|
1255
|
+
const forwardedArgs = [];
|
|
1256
|
+
|
|
1257
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
1258
|
+
const arg = rawArgs[index];
|
|
1259
|
+
if (arg === '--verbose-auto-finish') {
|
|
1260
|
+
doctorDefaults.verboseAutoFinish = true;
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
if (arg === '--compact-auto-finish') {
|
|
1264
|
+
doctorDefaults.verboseAutoFinish = false;
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
forwardedArgs.push(arg);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
return parseRepoTraversalArgs(forwardedArgs, doctorDefaults);
|
|
1135
1271
|
}
|
|
1136
1272
|
|
|
1137
1273
|
function normalizeWorkspacePath(relativePath) {
|
|
@@ -1309,6 +1445,7 @@ function buildSandboxDoctorArgs(options, sandboxTarget) {
|
|
|
1309
1445
|
if (options.skipGitignore) args.push('--no-gitignore');
|
|
1310
1446
|
if (!options.dropStaleLocks) args.push('--keep-stale-locks');
|
|
1311
1447
|
args.push(options.waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge');
|
|
1448
|
+
if (options.verboseAutoFinish) args.push('--verbose-auto-finish');
|
|
1312
1449
|
if (options.json) args.push('--json');
|
|
1313
1450
|
return args;
|
|
1314
1451
|
}
|
|
@@ -2207,6 +2344,7 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
2207
2344
|
postSandboxAutoFinishSummary = autoFinishReadyAgentBranches(blocked.repoRoot, {
|
|
2208
2345
|
baseBranch: blocked.branch,
|
|
2209
2346
|
dryRun: options.dryRun,
|
|
2347
|
+
waitForMerge: options.waitForMerge,
|
|
2210
2348
|
excludeBranches: [metadata.branch],
|
|
2211
2349
|
});
|
|
2212
2350
|
}
|
|
@@ -2307,16 +2445,10 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
2307
2445
|
console.log(`[${TOOL_NAME}] Auto-finish skipped: ${finishResult.note}.`);
|
|
2308
2446
|
}
|
|
2309
2447
|
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
for (const detail of postSandboxAutoFinishSummary.details) {
|
|
2315
|
-
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
2316
|
-
}
|
|
2317
|
-
} else if (postSandboxAutoFinishSummary.details.length > 0) {
|
|
2318
|
-
console.log(`[${TOOL_NAME}] ${postSandboxAutoFinishSummary.details[0]}`);
|
|
2319
|
-
}
|
|
2448
|
+
printAutoFinishSummary(postSandboxAutoFinishSummary, {
|
|
2449
|
+
baseBranch: blocked.branch,
|
|
2450
|
+
verbose: options.verboseAutoFinish,
|
|
2451
|
+
});
|
|
2320
2452
|
if (omxScaffoldSyncResult.status === 'synced') {
|
|
2321
2453
|
console.log(`[${TOOL_NAME}] Synced .omx scaffold back to protected branch workspace.`);
|
|
2322
2454
|
} else if (omxScaffoldSyncResult.status === 'unchanged') {
|
|
@@ -2871,6 +3003,7 @@ function hasSignificantWorkingTreeChanges(worktreePath) {
|
|
|
2871
3003
|
function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
2872
3004
|
const baseBranch = String(options.baseBranch || '').trim();
|
|
2873
3005
|
const dryRun = Boolean(options.dryRun);
|
|
3006
|
+
const waitForMerge = options.waitForMerge !== false;
|
|
2874
3007
|
const excludedBranches = new Set(
|
|
2875
3008
|
Array.isArray(options.excludeBranches)
|
|
2876
3009
|
? options.excludeBranches.map((branch) => String(branch || '').trim()).filter(Boolean)
|
|
@@ -2989,7 +3122,7 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
|
2989
3122
|
'--base',
|
|
2990
3123
|
baseBranch,
|
|
2991
3124
|
'--via-pr',
|
|
2992
|
-
'--wait-for-merge',
|
|
3125
|
+
waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge',
|
|
2993
3126
|
'--cleanup',
|
|
2994
3127
|
];
|
|
2995
3128
|
const finishResult = run('bash', finishArgs, { cwd: repoRoot });
|
|
@@ -3419,6 +3552,82 @@ function parseCleanupArgs(rawArgs) {
|
|
|
3419
3552
|
return options;
|
|
3420
3553
|
}
|
|
3421
3554
|
|
|
3555
|
+
function parseMergeArgs(rawArgs) {
|
|
3556
|
+
const options = {
|
|
3557
|
+
target: process.cwd(),
|
|
3558
|
+
base: '',
|
|
3559
|
+
into: '',
|
|
3560
|
+
branches: [],
|
|
3561
|
+
task: '',
|
|
3562
|
+
agent: '',
|
|
3563
|
+
};
|
|
3564
|
+
|
|
3565
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
3566
|
+
const arg = rawArgs[index];
|
|
3567
|
+
if (arg === '--target') {
|
|
3568
|
+
const next = rawArgs[index + 1];
|
|
3569
|
+
if (!next) {
|
|
3570
|
+
throw new Error('--target requires a path value');
|
|
3571
|
+
}
|
|
3572
|
+
options.target = next;
|
|
3573
|
+
index += 1;
|
|
3574
|
+
continue;
|
|
3575
|
+
}
|
|
3576
|
+
if (arg === '--base') {
|
|
3577
|
+
const next = rawArgs[index + 1];
|
|
3578
|
+
if (!next) {
|
|
3579
|
+
throw new Error('--base requires a branch value');
|
|
3580
|
+
}
|
|
3581
|
+
options.base = next;
|
|
3582
|
+
index += 1;
|
|
3583
|
+
continue;
|
|
3584
|
+
}
|
|
3585
|
+
if (arg === '--into') {
|
|
3586
|
+
const next = rawArgs[index + 1];
|
|
3587
|
+
if (!next) {
|
|
3588
|
+
throw new Error('--into requires an agent/* branch value');
|
|
3589
|
+
}
|
|
3590
|
+
options.into = next;
|
|
3591
|
+
index += 1;
|
|
3592
|
+
continue;
|
|
3593
|
+
}
|
|
3594
|
+
if (arg === '--branch') {
|
|
3595
|
+
const next = rawArgs[index + 1];
|
|
3596
|
+
if (!next) {
|
|
3597
|
+
throw new Error('--branch requires an agent/* branch value');
|
|
3598
|
+
}
|
|
3599
|
+
options.branches.push(next);
|
|
3600
|
+
index += 1;
|
|
3601
|
+
continue;
|
|
3602
|
+
}
|
|
3603
|
+
if (arg === '--task') {
|
|
3604
|
+
const next = rawArgs[index + 1];
|
|
3605
|
+
if (!next) {
|
|
3606
|
+
throw new Error('--task requires a task value');
|
|
3607
|
+
}
|
|
3608
|
+
options.task = next;
|
|
3609
|
+
index += 1;
|
|
3610
|
+
continue;
|
|
3611
|
+
}
|
|
3612
|
+
if (arg === '--agent') {
|
|
3613
|
+
const next = rawArgs[index + 1];
|
|
3614
|
+
if (!next) {
|
|
3615
|
+
throw new Error('--agent requires an agent value');
|
|
3616
|
+
}
|
|
3617
|
+
options.agent = next;
|
|
3618
|
+
index += 1;
|
|
3619
|
+
continue;
|
|
3620
|
+
}
|
|
3621
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
if (options.branches.length === 0) {
|
|
3625
|
+
throw new Error('merge requires at least one --branch <agent/*> input');
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
return options;
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3422
3631
|
function parseFinishArgs(rawArgs) {
|
|
3423
3632
|
const options = {
|
|
3424
3633
|
target: process.cwd(),
|
|
@@ -5127,31 +5336,38 @@ function doctor(rawArgs) {
|
|
|
5127
5336
|
|
|
5128
5337
|
const repoResults = [];
|
|
5129
5338
|
let aggregateExitCode = 0;
|
|
5130
|
-
for (
|
|
5339
|
+
for (let repoIndex = 0; repoIndex < discoveredRepos.length; repoIndex += 1) {
|
|
5340
|
+
const repoPath = discoveredRepos[repoIndex];
|
|
5341
|
+
const progressLabel = `${repoIndex + 1}/${discoveredRepos.length}`;
|
|
5131
5342
|
if (!options.json) {
|
|
5132
|
-
console.log(`[${TOOL_NAME}] ── Doctor target: ${repoPath} ──`);
|
|
5343
|
+
console.log(`[${TOOL_NAME}] ── Doctor target: ${repoPath} [${progressLabel}] ──`);
|
|
5133
5344
|
}
|
|
5134
5345
|
|
|
5135
|
-
const
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5346
|
+
const childArgs = [
|
|
5347
|
+
path.resolve(__filename),
|
|
5348
|
+
'doctor',
|
|
5349
|
+
'--single-repo',
|
|
5350
|
+
'--target',
|
|
5351
|
+
repoPath,
|
|
5352
|
+
...(options.dropStaleLocks ? [] : ['--keep-stale-locks']),
|
|
5353
|
+
...(options.skipAgents ? ['--skip-agents'] : []),
|
|
5354
|
+
...(options.skipPackageJson ? ['--skip-package-json'] : []),
|
|
5355
|
+
...(options.skipGitignore ? ['--no-gitignore'] : []),
|
|
5356
|
+
...(options.dryRun ? ['--dry-run'] : []),
|
|
5357
|
+
// Recursive child doctor runs should report pending PR state immediately instead of blocking the parent loop.
|
|
5358
|
+
'--no-wait-for-merge',
|
|
5359
|
+
...(options.verboseAutoFinish ? ['--verbose-auto-finish'] : []),
|
|
5360
|
+
...(options.json ? ['--json'] : []),
|
|
5361
|
+
...(options.allowProtectedBaseWrite ? ['--allow-protected-base-write'] : []),
|
|
5362
|
+
];
|
|
5363
|
+
const startedAt = Date.now();
|
|
5364
|
+
const nestedResult = options.json
|
|
5365
|
+
? run(process.execPath, childArgs, { cwd: topRepoRoot })
|
|
5366
|
+
: cp.spawnSync(process.execPath, childArgs, {
|
|
5367
|
+
cwd: topRepoRoot,
|
|
5368
|
+
encoding: 'utf8',
|
|
5369
|
+
stdio: 'inherit',
|
|
5370
|
+
});
|
|
5155
5371
|
if (isSpawnFailure(nestedResult)) {
|
|
5156
5372
|
throw nestedResult.error;
|
|
5157
5373
|
}
|
|
@@ -5181,9 +5397,12 @@ function doctor(rawArgs) {
|
|
|
5181
5397
|
},
|
|
5182
5398
|
);
|
|
5183
5399
|
} else {
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5400
|
+
console.log(
|
|
5401
|
+
`[${TOOL_NAME}] Doctor target complete: ${repoPath} [${progressLabel}] in ${formatElapsedDuration(Date.now() - startedAt)}.`,
|
|
5402
|
+
);
|
|
5403
|
+
if (repoIndex < discoveredRepos.length - 1) {
|
|
5404
|
+
process.stdout.write('\n');
|
|
5405
|
+
}
|
|
5187
5406
|
}
|
|
5188
5407
|
}
|
|
5189
5408
|
|
|
@@ -5232,6 +5451,7 @@ function doctor(rawArgs) {
|
|
|
5232
5451
|
: autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
5233
5452
|
baseBranch: currentBaseBranch,
|
|
5234
5453
|
dryRun: singleRepoOptions.dryRun,
|
|
5454
|
+
waitForMerge: singleRepoOptions.waitForMerge,
|
|
5235
5455
|
});
|
|
5236
5456
|
const safe = scanResult.guardexEnabled === false || (scanResult.errors === 0 && scanResult.warnings === 0);
|
|
5237
5457
|
const musafe = safe;
|
|
@@ -5273,16 +5493,10 @@ function doctor(rawArgs) {
|
|
|
5273
5493
|
setExitCodeFromScan(scanResult);
|
|
5274
5494
|
return;
|
|
5275
5495
|
}
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
for (const detail of autoFinishSummary.details) {
|
|
5281
|
-
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
5282
|
-
}
|
|
5283
|
-
} else if (autoFinishSummary.details.length > 0) {
|
|
5284
|
-
console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
|
|
5285
|
-
}
|
|
5496
|
+
printAutoFinishSummary(autoFinishSummary, {
|
|
5497
|
+
baseBranch: currentBaseBranch,
|
|
5498
|
+
verbose: singleRepoOptions.verboseAutoFinish,
|
|
5499
|
+
});
|
|
5286
5500
|
if (safe) {
|
|
5287
5501
|
console.log(`[${TOOL_NAME}] ✅ Repo is fully safe.`);
|
|
5288
5502
|
} else {
|
|
@@ -6458,6 +6672,46 @@ function cleanup(rawArgs) {
|
|
|
6458
6672
|
process.exitCode = 0;
|
|
6459
6673
|
}
|
|
6460
6674
|
|
|
6675
|
+
function merge(rawArgs) {
|
|
6676
|
+
const options = parseMergeArgs(rawArgs);
|
|
6677
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
6678
|
+
const mergeScript = path.join(repoRoot, 'scripts', 'agent-branch-merge.sh');
|
|
6679
|
+
|
|
6680
|
+
if (!fs.existsSync(mergeScript)) {
|
|
6681
|
+
throw new Error(`Missing merge script: ${mergeScript}. Run '${SHORT_TOOL_NAME} setup' first.`);
|
|
6682
|
+
}
|
|
6683
|
+
|
|
6684
|
+
const args = [mergeScript];
|
|
6685
|
+
if (options.base) {
|
|
6686
|
+
args.push('--base', options.base);
|
|
6687
|
+
}
|
|
6688
|
+
if (options.into) {
|
|
6689
|
+
args.push('--into', options.into);
|
|
6690
|
+
}
|
|
6691
|
+
if (options.task) {
|
|
6692
|
+
args.push('--task', options.task);
|
|
6693
|
+
}
|
|
6694
|
+
if (options.agent) {
|
|
6695
|
+
args.push('--agent', options.agent);
|
|
6696
|
+
}
|
|
6697
|
+
for (const branch of options.branches) {
|
|
6698
|
+
args.push('--branch', branch);
|
|
6699
|
+
}
|
|
6700
|
+
|
|
6701
|
+
const mergeResult = run('bash', args, { cwd: repoRoot, stdio: 'pipe' });
|
|
6702
|
+
if (mergeResult.stdout) {
|
|
6703
|
+
process.stdout.write(mergeResult.stdout);
|
|
6704
|
+
}
|
|
6705
|
+
if (mergeResult.stderr) {
|
|
6706
|
+
process.stderr.write(mergeResult.stderr);
|
|
6707
|
+
}
|
|
6708
|
+
if (mergeResult.status !== 0) {
|
|
6709
|
+
throw new Error(`merge command failed with status ${mergeResult.status}`);
|
|
6710
|
+
}
|
|
6711
|
+
|
|
6712
|
+
process.exitCode = 0;
|
|
6713
|
+
}
|
|
6714
|
+
|
|
6461
6715
|
function finish(rawArgs) {
|
|
6462
6716
|
const options = parseFinishArgs(rawArgs);
|
|
6463
6717
|
const repoRoot = resolveRepoRoot(options.target);
|
|
@@ -6944,6 +7198,7 @@ function main() {
|
|
|
6944
7198
|
if (command === 'prompt') return prompt(rest);
|
|
6945
7199
|
if (command === 'doctor') return doctor(rest);
|
|
6946
7200
|
if (command === 'agents') return agents(rest);
|
|
7201
|
+
if (command === 'merge') return merge(rest);
|
|
6947
7202
|
if (command === 'finish') return finish(rest);
|
|
6948
7203
|
if (command === 'report') return report(rest);
|
|
6949
7204
|
if (command === 'protect') return protect(rest);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imdeadpool/guardex",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.16",
|
|
4
4
|
"description": "GitGuardex: hardened multi-agent git guardrails for parallel agent work.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"preferGlobal": true,
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"agent:codex": "bash ./scripts/codex-agent.sh",
|
|
16
16
|
"agent:branch:start": "bash ./scripts/agent-branch-start.sh",
|
|
17
17
|
"agent:branch:finish": "bash ./scripts/agent-branch-finish.sh",
|
|
18
|
+
"agent:branch:merge": "bash ./scripts/agent-branch-merge.sh",
|
|
18
19
|
"agent:cleanup": "gx cleanup",
|
|
19
20
|
"agent:hooks:install": "bash ./scripts/install-agent-git-hooks.sh",
|
|
20
21
|
"agent:locks:claim": "python3 ./scripts/agent-file-locks.py claim",
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
BASE_BRANCH=""
|
|
5
|
+
BASE_BRANCH_EXPLICIT=0
|
|
6
|
+
TARGET_BRANCH=""
|
|
7
|
+
TASK_NAME=""
|
|
8
|
+
AGENT_NAME="${GUARDEX_MERGE_AGENT_NAME:-codex}"
|
|
9
|
+
declare -a SOURCE_BRANCHES=()
|
|
10
|
+
|
|
11
|
+
usage() {
|
|
12
|
+
cat <<'EOF'
|
|
13
|
+
Usage: scripts/agent-branch-merge.sh --branch <agent/...> [--branch <agent/...> ...] [--into <agent/...>] [--task <task>] [--agent <agent>] [--base <branch>]
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
bash scripts/agent-branch-merge.sh --branch agent/codex/ui-a --branch agent/codex/ui-b
|
|
17
|
+
bash scripts/agent-branch-merge.sh --into agent/codex/owner-lane --branch agent/codex/helper-a --branch agent/codex/helper-b
|
|
18
|
+
EOF
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
sanitize_slug() {
|
|
22
|
+
local raw="$1"
|
|
23
|
+
local fallback="${2:-merge-agent-branches}"
|
|
24
|
+
local slug
|
|
25
|
+
slug="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
|
|
26
|
+
if [[ -z "$slug" ]]; then
|
|
27
|
+
slug="$fallback"
|
|
28
|
+
fi
|
|
29
|
+
printf '%s' "$slug"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
resolve_base_branch() {
|
|
33
|
+
local repo="$1"
|
|
34
|
+
local explicit_target="$2"
|
|
35
|
+
local configured=""
|
|
36
|
+
local branch_base=""
|
|
37
|
+
|
|
38
|
+
if [[ -n "$explicit_target" ]]; then
|
|
39
|
+
branch_base="$(git -C "$repo" config --get "branch.${explicit_target}.guardexBase" || true)"
|
|
40
|
+
if [[ -n "$branch_base" ]]; then
|
|
41
|
+
printf '%s' "$branch_base"
|
|
42
|
+
return 0
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
configured="$(git -C "$repo" config --get multiagent.baseBranch || true)"
|
|
47
|
+
if [[ -n "$configured" ]]; then
|
|
48
|
+
printf '%s' "$configured"
|
|
49
|
+
return 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
for fallback in dev main master; do
|
|
53
|
+
if git -C "$repo" show-ref --verify --quiet "refs/heads/${fallback}" \
|
|
54
|
+
|| git -C "$repo" show-ref --verify --quiet "refs/remotes/origin/${fallback}"; then
|
|
55
|
+
printf '%s' "$fallback"
|
|
56
|
+
return 0
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
printf '%s' "dev"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get_worktree_for_branch() {
|
|
64
|
+
local repo="$1"
|
|
65
|
+
local branch="$2"
|
|
66
|
+
git -C "$repo" worktree list --porcelain | awk -v target="refs/heads/${branch}" '
|
|
67
|
+
$1 == "worktree" { wt = $2 }
|
|
68
|
+
$1 == "branch" && $2 == target { print wt; exit }
|
|
69
|
+
'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
is_clean_worktree() {
|
|
73
|
+
local wt="$1"
|
|
74
|
+
git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json" \
|
|
75
|
+
&& git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json" \
|
|
76
|
+
&& [[ -z "$(git -C "$wt" ls-files --others --exclude-standard)" ]]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
has_in_progress_git_op() {
|
|
80
|
+
local wt="$1"
|
|
81
|
+
local git_dir=""
|
|
82
|
+
git_dir="$(git -C "$wt" rev-parse --git-dir 2>/dev/null || true)"
|
|
83
|
+
if [[ -z "$git_dir" ]]; then
|
|
84
|
+
return 1
|
|
85
|
+
fi
|
|
86
|
+
if [[ "$git_dir" != /* ]]; then
|
|
87
|
+
git_dir="$(cd "$wt/$git_dir" 2>/dev/null && pwd -P || true)"
|
|
88
|
+
fi
|
|
89
|
+
if [[ -z "$git_dir" ]]; then
|
|
90
|
+
return 1
|
|
91
|
+
fi
|
|
92
|
+
[[ -f "${git_dir}/MERGE_HEAD" || -d "${git_dir}/rebase-merge" || -d "${git_dir}/rebase-apply" ]]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
select_unique_worktree_path() {
|
|
96
|
+
local root="$1"
|
|
97
|
+
local name="$2"
|
|
98
|
+
local candidate="${root}/${name}"
|
|
99
|
+
local suffix=2
|
|
100
|
+
while [[ -e "$candidate" ]]; do
|
|
101
|
+
candidate="${root}/${name}-${suffix}"
|
|
102
|
+
suffix=$((suffix + 1))
|
|
103
|
+
done
|
|
104
|
+
printf '%s' "$candidate"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
branch_exists() {
|
|
108
|
+
local repo="$1"
|
|
109
|
+
local branch="$2"
|
|
110
|
+
git -C "$repo" show-ref --verify --quiet "refs/heads/${branch}"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
branch_is_agent_lane() {
|
|
114
|
+
local branch="$1"
|
|
115
|
+
[[ "$branch" == agent/* ]]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
array_contains() {
|
|
119
|
+
local needle="$1"
|
|
120
|
+
shift || true
|
|
121
|
+
local item
|
|
122
|
+
for item in "$@"; do
|
|
123
|
+
if [[ "$item" == "$needle" ]]; then
|
|
124
|
+
return 0
|
|
125
|
+
fi
|
|
126
|
+
done
|
|
127
|
+
return 1
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
collect_branch_files() {
|
|
131
|
+
local repo="$1"
|
|
132
|
+
local base_ref="$2"
|
|
133
|
+
local branch="$3"
|
|
134
|
+
git -C "$repo" diff --name-only "${base_ref}...${branch}" -- . ":(exclude).omx/state/agent-file-locks.json" 2>/dev/null || true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
while [[ $# -gt 0 ]]; do
|
|
138
|
+
case "$1" in
|
|
139
|
+
--base)
|
|
140
|
+
BASE_BRANCH="${2:-}"
|
|
141
|
+
BASE_BRANCH_EXPLICIT=1
|
|
142
|
+
shift 2
|
|
143
|
+
;;
|
|
144
|
+
--into)
|
|
145
|
+
TARGET_BRANCH="${2:-}"
|
|
146
|
+
shift 2
|
|
147
|
+
;;
|
|
148
|
+
--branch)
|
|
149
|
+
SOURCE_BRANCHES+=("${2:-}")
|
|
150
|
+
shift 2
|
|
151
|
+
;;
|
|
152
|
+
--task)
|
|
153
|
+
TASK_NAME="${2:-}"
|
|
154
|
+
shift 2
|
|
155
|
+
;;
|
|
156
|
+
--agent)
|
|
157
|
+
AGENT_NAME="${2:-codex}"
|
|
158
|
+
shift 2
|
|
159
|
+
;;
|
|
160
|
+
-h|--help)
|
|
161
|
+
usage
|
|
162
|
+
exit 0
|
|
163
|
+
;;
|
|
164
|
+
*)
|
|
165
|
+
echo "[agent-branch-merge] Unknown argument: $1" >&2
|
|
166
|
+
usage >&2
|
|
167
|
+
exit 1
|
|
168
|
+
;;
|
|
169
|
+
esac
|
|
170
|
+
done
|
|
171
|
+
|
|
172
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
173
|
+
echo "[agent-branch-merge] Not inside a git repository." >&2
|
|
174
|
+
exit 1
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
repo_root="$(git rev-parse --show-toplevel)"
|
|
178
|
+
common_git_dir_raw="$(git -C "$repo_root" rev-parse --git-common-dir)"
|
|
179
|
+
if [[ "$common_git_dir_raw" == /* ]]; then
|
|
180
|
+
common_git_dir="$common_git_dir_raw"
|
|
181
|
+
else
|
|
182
|
+
common_git_dir="$(cd "$repo_root/$common_git_dir_raw" && pwd -P)"
|
|
183
|
+
fi
|
|
184
|
+
repo_common_root="$(cd "$common_git_dir/.." && pwd -P)"
|
|
185
|
+
agent_worktree_root="${repo_common_root}/.omx/agent-worktrees"
|
|
186
|
+
mkdir -p "$agent_worktree_root"
|
|
187
|
+
|
|
188
|
+
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
|
|
189
|
+
echo "[agent-branch-merge] --base requires a branch value." >&2
|
|
190
|
+
exit 1
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
if [[ -z "$TARGET_BRANCH" && "${#SOURCE_BRANCHES[@]}" -lt 1 ]]; then
|
|
194
|
+
echo "[agent-branch-merge] Provide at least one --branch <agent/...> source lane." >&2
|
|
195
|
+
exit 1
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
if [[ -n "$TARGET_BRANCH" ]] && ! branch_is_agent_lane "$TARGET_BRANCH"; then
|
|
199
|
+
echo "[agent-branch-merge] --into must reference an agent/* branch: ${TARGET_BRANCH}" >&2
|
|
200
|
+
exit 1
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
deduped_sources=()
|
|
204
|
+
for branch in "${SOURCE_BRANCHES[@]}"; do
|
|
205
|
+
if [[ -z "$branch" ]]; then
|
|
206
|
+
echo "[agent-branch-merge] --branch requires an agent/* branch value." >&2
|
|
207
|
+
exit 1
|
|
208
|
+
fi
|
|
209
|
+
if ! branch_is_agent_lane "$branch"; then
|
|
210
|
+
echo "[agent-branch-merge] Source branch must be agent/*: ${branch}" >&2
|
|
211
|
+
exit 1
|
|
212
|
+
fi
|
|
213
|
+
if ! branch_exists "$repo_root" "$branch"; then
|
|
214
|
+
echo "[agent-branch-merge] Local source branch not found: ${branch}" >&2
|
|
215
|
+
exit 1
|
|
216
|
+
fi
|
|
217
|
+
if ! array_contains "$branch" "${deduped_sources[@]}"; then
|
|
218
|
+
deduped_sources+=("$branch")
|
|
219
|
+
fi
|
|
220
|
+
done
|
|
221
|
+
SOURCE_BRANCHES=("${deduped_sources[@]}")
|
|
222
|
+
|
|
223
|
+
if [[ "${#SOURCE_BRANCHES[@]}" -eq 0 ]]; then
|
|
224
|
+
echo "[agent-branch-merge] No unique source branches were provided." >&2
|
|
225
|
+
exit 1
|
|
226
|
+
fi
|
|
227
|
+
|
|
228
|
+
if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
|
|
229
|
+
BASE_BRANCH="$(resolve_base_branch "$repo_root" "$TARGET_BRANCH")"
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
if [[ -z "$BASE_BRANCH" ]]; then
|
|
233
|
+
echo "[agent-branch-merge] Unable to resolve a base branch." >&2
|
|
234
|
+
exit 1
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
start_ref="$BASE_BRANCH"
|
|
238
|
+
if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
|
|
239
|
+
git -C "$repo_root" fetch origin "$BASE_BRANCH" --quiet
|
|
240
|
+
start_ref="origin/${BASE_BRANCH}"
|
|
241
|
+
elif ! git -C "$repo_root" show-ref --verify --quiet "refs/heads/${BASE_BRANCH}"; then
|
|
242
|
+
echo "[agent-branch-merge] Base branch not found locally or on origin: ${BASE_BRANCH}" >&2
|
|
243
|
+
exit 1
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
target_worktree=""
|
|
247
|
+
target_created=0
|
|
248
|
+
|
|
249
|
+
if [[ -z "$TARGET_BRANCH" ]]; then
|
|
250
|
+
if [[ -z "$TASK_NAME" ]]; then
|
|
251
|
+
first_hint="$(printf '%s' "${SOURCE_BRANCHES[0]}" | sed -E 's#^agent/[^/]+/##; s#^agent/##')"
|
|
252
|
+
source_count="${#SOURCE_BRANCHES[@]}"
|
|
253
|
+
if [[ "$source_count" -gt 1 ]]; then
|
|
254
|
+
TASK_NAME="$(sanitize_slug "merge-${first_hint}-and-$((source_count - 1))-more" "merge-agent-branches")"
|
|
255
|
+
else
|
|
256
|
+
TASK_NAME="$(sanitize_slug "merge-${first_hint}" "merge-agent-branches")"
|
|
257
|
+
fi
|
|
258
|
+
else
|
|
259
|
+
TASK_NAME="$(sanitize_slug "$TASK_NAME" "merge-agent-branches")"
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
start_output=""
|
|
263
|
+
if ! start_output="$(
|
|
264
|
+
cd "$repo_root"
|
|
265
|
+
env GUARDEX_OPENSPEC_AUTO_INIT=1 bash "scripts/agent-branch-start.sh" "$TASK_NAME" "$AGENT_NAME" "$BASE_BRANCH" 2>&1
|
|
266
|
+
)"; then
|
|
267
|
+
printf '%s\n' "$start_output" >&2
|
|
268
|
+
exit 1
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
printf '%s\n' "$start_output"
|
|
272
|
+
TARGET_BRANCH="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Created branch: //p' | head -n 1)"
|
|
273
|
+
target_worktree="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | head -n 1)"
|
|
274
|
+
if [[ -z "$TARGET_BRANCH" || -z "$target_worktree" ]]; then
|
|
275
|
+
echo "[agent-branch-merge] Unable to parse target branch/worktree from agent-branch-start output." >&2
|
|
276
|
+
exit 1
|
|
277
|
+
fi
|
|
278
|
+
target_created=1
|
|
279
|
+
else
|
|
280
|
+
if ! branch_exists "$repo_root" "$TARGET_BRANCH"; then
|
|
281
|
+
echo "[agent-branch-merge] Target branch not found: ${TARGET_BRANCH}" >&2
|
|
282
|
+
exit 1
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
target_worktree="$(get_worktree_for_branch "$repo_root" "$TARGET_BRANCH")"
|
|
286
|
+
if [[ -z "$target_worktree" ]]; then
|
|
287
|
+
target_worktree="$(select_unique_worktree_path "$agent_worktree_root" "${TARGET_BRANCH//\//__}")"
|
|
288
|
+
git -C "$repo_root" worktree add "$target_worktree" "$TARGET_BRANCH" >/dev/null
|
|
289
|
+
target_created=1
|
|
290
|
+
echo "[agent-branch-merge] Attached worktree for target branch '${TARGET_BRANCH}': ${target_worktree}"
|
|
291
|
+
fi
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
if [[ "$TARGET_BRANCH" == "$BASE_BRANCH" ]]; then
|
|
295
|
+
echo "[agent-branch-merge] Target branch must not equal the protected base branch '${BASE_BRANCH}'." >&2
|
|
296
|
+
exit 1
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
if ! is_clean_worktree "$target_worktree"; then
|
|
300
|
+
if [[ "$target_created" -eq 1 ]]; then
|
|
301
|
+
echo "[agent-branch-merge] Target worktree has freshly generated scaffold changes; continuing inside the new integration lane."
|
|
302
|
+
else
|
|
303
|
+
echo "[agent-branch-merge] Target worktree is not clean: ${target_worktree}" >&2
|
|
304
|
+
echo "[agent-branch-merge] Commit, stash, or discard local changes before merging agent lanes." >&2
|
|
305
|
+
exit 1
|
|
306
|
+
fi
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
if has_in_progress_git_op "$target_worktree"; then
|
|
310
|
+
echo "[agent-branch-merge] Target worktree has an in-progress merge/rebase: ${target_worktree}" >&2
|
|
311
|
+
echo "[agent-branch-merge] Resolve or abort that git operation before running the merge workflow." >&2
|
|
312
|
+
exit 1
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
for source_branch in "${SOURCE_BRANCHES[@]}"; do
|
|
316
|
+
if [[ "$source_branch" == "$TARGET_BRANCH" ]]; then
|
|
317
|
+
echo "[agent-branch-merge] Source branch list includes the target branch: ${source_branch}" >&2
|
|
318
|
+
exit 1
|
|
319
|
+
fi
|
|
320
|
+
source_worktree="$(get_worktree_for_branch "$repo_root" "$source_branch")"
|
|
321
|
+
if [[ -n "$source_worktree" ]] && ! is_clean_worktree "$source_worktree"; then
|
|
322
|
+
echo "[agent-branch-merge] Source worktree is not clean for '${source_branch}': ${source_worktree}" >&2
|
|
323
|
+
echo "[agent-branch-merge] Commit or stash source-lane changes before integration." >&2
|
|
324
|
+
exit 1
|
|
325
|
+
fi
|
|
326
|
+
done
|
|
327
|
+
|
|
328
|
+
pending_branches=()
|
|
329
|
+
for source_branch in "${SOURCE_BRANCHES[@]}"; do
|
|
330
|
+
if git -C "$repo_root" merge-base --is-ancestor "$source_branch" "$TARGET_BRANCH" >/dev/null 2>&1; then
|
|
331
|
+
echo "[agent-branch-merge] Skipping '${source_branch}' because it is already integrated into '${TARGET_BRANCH}'."
|
|
332
|
+
continue
|
|
333
|
+
fi
|
|
334
|
+
pending_branches+=("$source_branch")
|
|
335
|
+
done
|
|
336
|
+
|
|
337
|
+
if [[ "${#pending_branches[@]}" -eq 0 ]]; then
|
|
338
|
+
echo "[agent-branch-merge] No pending source branches remain for target '${TARGET_BRANCH}'."
|
|
339
|
+
echo "[agent-branch-merge] Target worktree: ${target_worktree}"
|
|
340
|
+
exit 0
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
declare -A file_to_branches=()
|
|
344
|
+
declare -a overlap_files=()
|
|
345
|
+
for source_branch in "${pending_branches[@]}"; do
|
|
346
|
+
while IFS= read -r changed_file; do
|
|
347
|
+
[[ -z "$changed_file" ]] && continue
|
|
348
|
+
existing="${file_to_branches[$changed_file]:-}"
|
|
349
|
+
if [[ -z "$existing" ]]; then
|
|
350
|
+
file_to_branches["$changed_file"]="$source_branch"
|
|
351
|
+
continue
|
|
352
|
+
fi
|
|
353
|
+
if [[ ",${existing}," == *",${source_branch},"* ]]; then
|
|
354
|
+
continue
|
|
355
|
+
fi
|
|
356
|
+
file_to_branches["$changed_file"]="${existing},${source_branch}"
|
|
357
|
+
if ! array_contains "$changed_file" "${overlap_files[@]}"; then
|
|
358
|
+
overlap_files+=("$changed_file")
|
|
359
|
+
fi
|
|
360
|
+
done < <(collect_branch_files "$repo_root" "$start_ref" "$source_branch")
|
|
361
|
+
done
|
|
362
|
+
|
|
363
|
+
echo "[agent-branch-merge] Target branch: ${TARGET_BRANCH}"
|
|
364
|
+
echo "[agent-branch-merge] Target worktree: ${target_worktree}"
|
|
365
|
+
echo "[agent-branch-merge] Base branch: ${BASE_BRANCH} (${start_ref})"
|
|
366
|
+
echo "[agent-branch-merge] Merge order: ${pending_branches[*]}"
|
|
367
|
+
|
|
368
|
+
if [[ "${#overlap_files[@]}" -gt 0 ]]; then
|
|
369
|
+
echo "[agent-branch-merge] Overlapping changed files detected across requested branches:"
|
|
370
|
+
for overlap_file in "${overlap_files[@]}"; do
|
|
371
|
+
branches_csv="${file_to_branches[$overlap_file]}"
|
|
372
|
+
branches_display="$(printf '%s' "$branches_csv" | sed 's/,/, /g')"
|
|
373
|
+
echo " - ${overlap_file} <- ${branches_display}"
|
|
374
|
+
done
|
|
375
|
+
else
|
|
376
|
+
echo "[agent-branch-merge] No overlapping changed files detected across requested branches."
|
|
377
|
+
fi
|
|
378
|
+
|
|
379
|
+
for index in "${!pending_branches[@]}"; do
|
|
380
|
+
source_branch="${pending_branches[$index]}"
|
|
381
|
+
echo "[agent-branch-merge] Merging '${source_branch}' into '${TARGET_BRANCH}'..."
|
|
382
|
+
if git -C "$target_worktree" merge --no-ff --no-edit "$source_branch"; then
|
|
383
|
+
echo "[agent-branch-merge] Merged '${source_branch}'."
|
|
384
|
+
continue
|
|
385
|
+
fi
|
|
386
|
+
|
|
387
|
+
conflict_files="$(git -C "$target_worktree" diff --name-only --diff-filter=U || true)"
|
|
388
|
+
echo "[agent-branch-merge] Merge conflict detected while merging '${source_branch}' into '${TARGET_BRANCH}'." >&2
|
|
389
|
+
echo "[agent-branch-merge] Target worktree: ${target_worktree}" >&2
|
|
390
|
+
if [[ -n "$conflict_files" ]]; then
|
|
391
|
+
echo "[agent-branch-merge] Conflicting files:" >&2
|
|
392
|
+
while IFS= read -r conflict_file; do
|
|
393
|
+
[[ -n "$conflict_file" ]] && echo " - ${conflict_file}" >&2
|
|
394
|
+
done <<< "$conflict_files"
|
|
395
|
+
fi
|
|
396
|
+
echo "[agent-branch-merge] Resolve or abort inside the integration worktree:" >&2
|
|
397
|
+
echo " cd \"${target_worktree}\"" >&2
|
|
398
|
+
echo " git status" >&2
|
|
399
|
+
echo " git add <resolved-files> && git commit" >&2
|
|
400
|
+
echo " # or: git merge --abort" >&2
|
|
401
|
+
|
|
402
|
+
remaining_branches=("${pending_branches[@]:$((index + 1))}")
|
|
403
|
+
if [[ "${#remaining_branches[@]}" -gt 0 ]]; then
|
|
404
|
+
echo "[agent-branch-merge] Remaining branches:" >&2
|
|
405
|
+
for remaining in "${remaining_branches[@]}"; do
|
|
406
|
+
echo " - ${remaining}" >&2
|
|
407
|
+
done
|
|
408
|
+
resume_cmd="gx merge --into ${TARGET_BRANCH} --base ${BASE_BRANCH}"
|
|
409
|
+
for remaining in "${remaining_branches[@]}"; do
|
|
410
|
+
resume_cmd="${resume_cmd} --branch ${remaining}"
|
|
411
|
+
done
|
|
412
|
+
echo "[agent-branch-merge] Resume after resolving with: ${resume_cmd}" >&2
|
|
413
|
+
fi
|
|
414
|
+
exit 1
|
|
415
|
+
done
|
|
416
|
+
|
|
417
|
+
echo "[agent-branch-merge] Merge sequence complete for '${TARGET_BRANCH}'."
|
|
418
|
+
if [[ "$target_created" -eq 1 ]]; then
|
|
419
|
+
echo "[agent-branch-merge] Review and verify in '${target_worktree}', then finish the integration branch when ready."
|
|
420
|
+
fi
|
|
421
|
+
echo "[agent-branch-merge] Next step: bash scripts/agent-branch-finish.sh --branch \"${TARGET_BRANCH}\" --base \"${BASE_BRANCH}\" --via-pr --wait-for-merge --cleanup"
|
|
@@ -11,6 +11,7 @@ OPENSPEC_AUTO_INIT_RAW="${GUARDEX_OPENSPEC_AUTO_INIT:-false}"
|
|
|
11
11
|
OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}"
|
|
12
12
|
OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}"
|
|
13
13
|
OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CAPABILITY_SLUG:-}"
|
|
14
|
+
OPENSPEC_MASTERPLAN_LABEL_RAW="${GUARDEX_OPENSPEC_MASTERPLAN_LABEL-masterplan}"
|
|
14
15
|
PRINT_NAME_ONLY=0
|
|
15
16
|
POSITIONAL_ARGS=()
|
|
16
17
|
|
|
@@ -226,13 +227,35 @@ normalize_bool() {
|
|
|
226
227
|
|
|
227
228
|
OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")"
|
|
228
229
|
|
|
230
|
+
resolve_openspec_masterplan_label() {
|
|
231
|
+
local raw="${OPENSPEC_MASTERPLAN_LABEL_RAW:-}"
|
|
232
|
+
local label
|
|
233
|
+
|
|
234
|
+
if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]] || [[ -z "$raw" ]]; then
|
|
235
|
+
printf ''
|
|
236
|
+
return 0
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
label="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
|
|
240
|
+
printf '%s' "$label"
|
|
241
|
+
}
|
|
242
|
+
|
|
229
243
|
resolve_openspec_plan_slug() {
|
|
230
244
|
local branch_name="$1"
|
|
231
|
-
local
|
|
245
|
+
local agent_slug="$2"
|
|
246
|
+
local task_slug="$3"
|
|
247
|
+
local masterplan_label=""
|
|
248
|
+
local branch_leaf=""
|
|
232
249
|
if [[ -n "$OPENSPEC_PLAN_SLUG_OVERRIDE" ]]; then
|
|
233
250
|
sanitize_slug "$OPENSPEC_PLAN_SLUG_OVERRIDE" "$task_slug"
|
|
234
251
|
return 0
|
|
235
252
|
fi
|
|
253
|
+
masterplan_label="$(resolve_openspec_masterplan_label)"
|
|
254
|
+
if [[ -n "$masterplan_label" ]] && [[ "$branch_name" == "agent/${agent_slug}/"* ]]; then
|
|
255
|
+
branch_leaf="${branch_name#agent/${agent_slug}/}"
|
|
256
|
+
sanitize_slug "agent-${agent_slug}-${masterplan_label}-${branch_leaf}" "$task_slug"
|
|
257
|
+
return 0
|
|
258
|
+
fi
|
|
236
259
|
sanitize_slug "${branch_name//\//-}" "$task_slug"
|
|
237
260
|
}
|
|
238
261
|
|
|
@@ -255,6 +278,22 @@ resolve_openspec_capability_slug() {
|
|
|
255
278
|
sanitize_slug "$task_slug" "general-behavior"
|
|
256
279
|
}
|
|
257
280
|
|
|
281
|
+
resolve_worktree_leaf() {
|
|
282
|
+
local branch_name="$1"
|
|
283
|
+
local agent_slug="$2"
|
|
284
|
+
local masterplan_label=""
|
|
285
|
+
local branch_leaf=""
|
|
286
|
+
|
|
287
|
+
masterplan_label="$(resolve_openspec_masterplan_label)"
|
|
288
|
+
if [[ -n "$masterplan_label" ]] && [[ "$branch_name" == "agent/${agent_slug}/"* ]]; then
|
|
289
|
+
branch_leaf="${branch_name#agent/${agent_slug}/}"
|
|
290
|
+
printf 'agent__%s__%s__%s' "$agent_slug" "$masterplan_label" "$branch_leaf"
|
|
291
|
+
return 0
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
printf '%s' "${branch_name//\//__}"
|
|
295
|
+
}
|
|
296
|
+
|
|
258
297
|
has_local_changes() {
|
|
259
298
|
local root="$1"
|
|
260
299
|
if ! git -C "$root" diff --quiet; then
|
|
@@ -497,8 +536,9 @@ done
|
|
|
497
536
|
|
|
498
537
|
worktree_root="${repo_root}/${WORKTREE_ROOT_REL}"
|
|
499
538
|
mkdir -p "$worktree_root"
|
|
500
|
-
|
|
501
|
-
|
|
539
|
+
worktree_leaf="$(resolve_worktree_leaf "$branch_name" "$agent_slug")"
|
|
540
|
+
worktree_path="${worktree_root}/${worktree_leaf}"
|
|
541
|
+
openspec_plan_slug="$(resolve_openspec_plan_slug "$branch_name" "$agent_slug" "$task_slug")"
|
|
502
542
|
openspec_change_slug="$(resolve_openspec_change_slug "$branch_name" "$task_slug")"
|
|
503
543
|
openspec_capability_slug="$(resolve_openspec_capability_slug "$task_slug")"
|
|
504
544
|
|
|
@@ -14,6 +14,7 @@ OPENSPEC_AUTO_INIT_RAW="${GUARDEX_OPENSPEC_AUTO_INIT:-true}"
|
|
|
14
14
|
OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}"
|
|
15
15
|
OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}"
|
|
16
16
|
OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CAPABILITY_SLUG:-}"
|
|
17
|
+
OPENSPEC_MASTERPLAN_LABEL_RAW="${GUARDEX_OPENSPEC_MASTERPLAN_LABEL-masterplan}"
|
|
17
18
|
|
|
18
19
|
normalize_bool() {
|
|
19
20
|
local raw="${1:-}"
|
|
@@ -34,6 +35,19 @@ AUTO_CLEANUP="$(normalize_bool "$AUTO_CLEANUP_RAW" "1")"
|
|
|
34
35
|
AUTO_WAIT_FOR_MERGE="$(normalize_bool "$AUTO_WAIT_FOR_MERGE_RAW" "1")"
|
|
35
36
|
OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")"
|
|
36
37
|
|
|
38
|
+
resolve_openspec_masterplan_label() {
|
|
39
|
+
local raw="${OPENSPEC_MASTERPLAN_LABEL_RAW:-}"
|
|
40
|
+
local label
|
|
41
|
+
|
|
42
|
+
if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]] || [[ -z "$raw" ]]; then
|
|
43
|
+
printf ''
|
|
44
|
+
return 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
label="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
|
|
48
|
+
printf '%s' "$label"
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
if [[ -n "$BASE_BRANCH" ]]; then
|
|
38
52
|
BASE_BRANCH_EXPLICIT=1
|
|
39
53
|
fi
|
|
@@ -161,11 +175,21 @@ sanitize_slug() {
|
|
|
161
175
|
resolve_openspec_plan_slug() {
|
|
162
176
|
local branch_name="$1"
|
|
163
177
|
local task_slug
|
|
178
|
+
local masterplan_label=""
|
|
179
|
+
local branch_role=""
|
|
180
|
+
local branch_leaf=""
|
|
164
181
|
task_slug="$(sanitize_slug "$TASK_NAME" "task")"
|
|
165
182
|
if [[ -n "$OPENSPEC_PLAN_SLUG_OVERRIDE" ]]; then
|
|
166
183
|
sanitize_slug "$OPENSPEC_PLAN_SLUG_OVERRIDE" "$task_slug"
|
|
167
184
|
return 0
|
|
168
185
|
fi
|
|
186
|
+
masterplan_label="$(resolve_openspec_masterplan_label)"
|
|
187
|
+
if [[ -n "$masterplan_label" ]] && [[ "$branch_name" =~ ^agent/([^/]+)/(.+)$ ]]; then
|
|
188
|
+
branch_role="${BASH_REMATCH[1]}"
|
|
189
|
+
branch_leaf="${BASH_REMATCH[2]}"
|
|
190
|
+
sanitize_slug "agent-${branch_role}-${masterplan_label}-${branch_leaf}" "$task_slug"
|
|
191
|
+
return 0
|
|
192
|
+
fi
|
|
169
193
|
sanitize_slug "${branch_name//\//-}" "$task_slug"
|
|
170
194
|
}
|
|
171
195
|
|
|
@@ -190,6 +214,23 @@ resolve_openspec_capability_slug() {
|
|
|
190
214
|
sanitize_slug "$task_slug" "general-behavior"
|
|
191
215
|
}
|
|
192
216
|
|
|
217
|
+
resolve_worktree_leaf() {
|
|
218
|
+
local branch_name="$1"
|
|
219
|
+
local masterplan_label=""
|
|
220
|
+
local branch_role=""
|
|
221
|
+
local branch_leaf=""
|
|
222
|
+
|
|
223
|
+
masterplan_label="$(resolve_openspec_masterplan_label)"
|
|
224
|
+
if [[ -n "$masterplan_label" ]] && [[ "$branch_name" =~ ^agent/([^/]+)/(.+)$ ]]; then
|
|
225
|
+
branch_role="${BASH_REMATCH[1]}"
|
|
226
|
+
branch_leaf="${BASH_REMATCH[2]}"
|
|
227
|
+
printf 'agent__%s__%s__%s' "$branch_role" "$masterplan_label" "$branch_leaf"
|
|
228
|
+
return 0
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
printf '%s' "${branch_name//\//__}"
|
|
232
|
+
}
|
|
233
|
+
|
|
193
234
|
hydrate_local_helper_in_worktree() {
|
|
194
235
|
local worktree="$1"
|
|
195
236
|
local relative_path="$2"
|
|
@@ -314,7 +355,7 @@ start_sandbox_fallback() {
|
|
|
314
355
|
|
|
315
356
|
worktree_root="${repo_root}/.omx/agent-worktrees"
|
|
316
357
|
mkdir -p "$worktree_root"
|
|
317
|
-
worktree_path="${worktree_root}/$
|
|
358
|
+
worktree_path="${worktree_root}/$(resolve_worktree_leaf "$branch_name")"
|
|
318
359
|
if [[ -e "$worktree_path" ]]; then
|
|
319
360
|
echo "[codex-agent] Fallback worktree path already exists: $worktree_path" >&2
|
|
320
361
|
return 1
|
|
@@ -346,7 +387,11 @@ initial_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/nu
|
|
|
346
387
|
start_output=""
|
|
347
388
|
start_status=0
|
|
348
389
|
set +e
|
|
349
|
-
start_output="$(
|
|
390
|
+
start_output="$(
|
|
391
|
+
GUARDEX_OPENSPEC_AUTO_INIT="$OPENSPEC_AUTO_INIT" \
|
|
392
|
+
GUARDEX_OPENSPEC_MASTERPLAN_LABEL="$OPENSPEC_MASTERPLAN_LABEL_RAW" \
|
|
393
|
+
bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1
|
|
394
|
+
)"
|
|
350
395
|
start_status=$?
|
|
351
396
|
set -e
|
|
352
397
|
|
|
@@ -54,6 +54,10 @@ write_if_missing "$PLAN_DIR/README.md" "# Plan Workspace: ${PLAN_SLUG}
|
|
|
54
54
|
|
|
55
55
|
Durable pre-implementation planning workspace.
|
|
56
56
|
|
|
57
|
+
Each role folder includes a copyable \`prompt.md\` for joined Codex helpers.
|
|
58
|
+
Helpers reuse the owner branch/worktree, claim the role files they touch, and
|
|
59
|
+
leave PR merge + sandbox cleanup to the owner change lane.
|
|
60
|
+
|
|
57
61
|
Use this command to update checkpoints:
|
|
58
62
|
|
|
59
63
|
\`\`\`bash
|
|
@@ -89,10 +93,39 @@ for role in "${ROLES[@]}"; do
|
|
|
89
93
|
write_if_missing "$ROLE_DIR/README.md" "# ${role}
|
|
90
94
|
|
|
91
95
|
Role workspace for \`${role}\`.
|
|
96
|
+
"
|
|
97
|
+
|
|
98
|
+
write_if_missing "$ROLE_DIR/prompt.md" "# ${role} prompt
|
|
99
|
+
|
|
100
|
+
You are the \`${role}\` lane for shared plan \`${PLAN_SLUG}\`.
|
|
101
|
+
|
|
102
|
+
## Scope
|
|
103
|
+
|
|
104
|
+
- Work inside \`openspec/plan/${PLAN_SLUG}/${role}/\` plus directly-related shared plan files you explicitly claim.
|
|
105
|
+
- Reuse the owner's branch/worktree instead of creating a separate sandbox unless the owner says otherwise.
|
|
106
|
+
|
|
107
|
+
## Ownership
|
|
108
|
+
|
|
109
|
+
- Before editing, claim this role's files in the shared owner lane:
|
|
110
|
+
\`python3 scripts/agent-file-locks.py claim --branch <owner-branch> openspec/plan/${PLAN_SLUG}/${role}/README.md openspec/plan/${PLAN_SLUG}/${role}/prompt.md openspec/plan/${PLAN_SLUG}/${role}/tasks.md openspec/plan/${PLAN_SLUG}/checkpoints.md\`
|
|
111
|
+
- Record branch, worktree, and scope in \`tasks.md\`.
|
|
112
|
+
- Do not change another role's files without reassignment.
|
|
113
|
+
|
|
114
|
+
## Deliverables
|
|
115
|
+
|
|
116
|
+
- Complete the role checklist in \`tasks.md\`.
|
|
117
|
+
- Leave a handoff with files changed, verification, and risks.
|
|
118
|
+
- The owner alone runs the change completion flow and sandbox cleanup after change tasks 4.1-4.3 are done.
|
|
92
119
|
"
|
|
93
120
|
|
|
94
121
|
write_if_missing "$ROLE_DIR/tasks.md" "# ${role} tasks
|
|
95
122
|
|
|
123
|
+
## Ownership
|
|
124
|
+
|
|
125
|
+
- [ ] Claim this role's files in the shared owner branch/worktree before editing.
|
|
126
|
+
- [ ] Record branch, worktree, and scope for this role.
|
|
127
|
+
- [ ] Copy or hand off \`prompt.md\` when another agent joins this role.
|
|
128
|
+
|
|
96
129
|
## 1. Spec
|
|
97
130
|
|
|
98
131
|
- [ ] Define requirements and scope for ${role}
|
|
@@ -111,6 +144,15 @@ Role workspace for \`${role}\`.
|
|
|
111
144
|
## 4. Checkpoints
|
|
112
145
|
|
|
113
146
|
- [ ] Publish checkpoint update for this role
|
|
147
|
+
|
|
148
|
+
## 5. Collaboration
|
|
149
|
+
|
|
150
|
+
- [ ] Leave a role handoff with files changed, verification, and risks.
|
|
151
|
+
- [ ] Owner records \`accept\`, \`revise\`, or \`reject\` for joined output, or marks \`N/A\` if no helper joined.
|
|
152
|
+
|
|
153
|
+
## 6. Completion
|
|
154
|
+
|
|
155
|
+
- [ ] Keep sandbox cleanup blocked until change tasks 4.1-4.3 are complete.
|
|
114
156
|
"
|
|
115
157
|
done
|
|
116
158
|
|