@imdeadpool/guardex 7.0.27 → 7.0.34
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 +171 -818
- package/package.json +1 -1
- package/src/cli/main.js +360 -23
- package/src/context.js +64 -21
- package/src/doctor/index.js +42 -20
- package/src/finish/index.js +1 -0
- package/src/output/index.js +151 -15
- package/src/toolchain/index.js +6 -0
- package/templates/AGENTS.multiagent-safety.md +5 -1
- package/templates/githooks/post-checkout +8 -2
- package/templates/scripts/agent-branch-finish.sh +36 -21
- package/templates/scripts/agent-branch-start.sh +15 -1
- package/templates/scripts/codex-agent.sh +16 -1
- package/templates/vscode/guardex-active-agents/extension.js +39 -4
- package/templates/vscode/guardex-active-agents/package.json +4 -4
- package/templates/vscode/guardex-active-agents/session-schema.js +23 -5
package/src/doctor/index.js
CHANGED
|
@@ -31,6 +31,7 @@ const {
|
|
|
31
31
|
} = require('../sandbox');
|
|
32
32
|
const { ensureOmxScaffold, configureHooks } = require('../scaffold');
|
|
33
33
|
const { detectRecoverableAutoFinishConflict, printAutoFinishSummary } = require('../output');
|
|
34
|
+
const { autoCommitWorktreeForFinish } = require('../finish');
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* @typedef {Object} SandboxMetadata
|
|
@@ -887,23 +888,25 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
|
887
888
|
return summary;
|
|
888
889
|
}
|
|
889
890
|
|
|
890
|
-
|
|
891
|
-
summary.enabled = false;
|
|
892
|
-
summary.details.push('Skipped auto-finish sweep (origin remote missing).');
|
|
893
|
-
return summary;
|
|
894
|
-
}
|
|
891
|
+
const originAvailable = hasOriginRemote(repoRoot);
|
|
895
892
|
const explicitGhBin = Boolean(String(process.env.GUARDEX_GH_BIN || '').trim());
|
|
896
|
-
if (!explicitGhBin && !originRemoteLooksLikeGithub(repoRoot)) {
|
|
897
|
-
summary.enabled = false;
|
|
898
|
-
summary.details.push('Skipped auto-finish sweep (origin remote is not GitHub).');
|
|
899
|
-
return summary;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
893
|
const ghBin = process.env.GUARDEX_GH_BIN || 'gh';
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
894
|
+
const ghAvailable =
|
|
895
|
+
originAvailable &&
|
|
896
|
+
(explicitGhBin || originRemoteLooksLikeGithub(repoRoot)) &&
|
|
897
|
+
run(ghBin, ['--version']).status === 0;
|
|
898
|
+
|
|
899
|
+
let fallbackMode = '';
|
|
900
|
+
if (!originAvailable) {
|
|
901
|
+
fallbackMode = 'local';
|
|
902
|
+
summary.details.push('origin remote missing; falling back to local direct merge (no push, no PR).');
|
|
903
|
+
} else if (!ghAvailable) {
|
|
904
|
+
fallbackMode = 'direct';
|
|
905
|
+
if (!explicitGhBin && !originRemoteLooksLikeGithub(repoRoot)) {
|
|
906
|
+
summary.details.push('origin remote is not GitHub; falling back to direct merge + push.');
|
|
907
|
+
} else {
|
|
908
|
+
summary.details.push(`${ghBin} not available; falling back to direct merge + push.`);
|
|
909
|
+
}
|
|
907
910
|
}
|
|
908
911
|
|
|
909
912
|
const branchWorktrees = mapWorktreePathsByBranch(repoRoot);
|
|
@@ -936,16 +939,29 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
|
936
939
|
continue;
|
|
937
940
|
}
|
|
938
941
|
|
|
942
|
+
const branchWorktree = branchWorktrees.get(branch) || '';
|
|
943
|
+
if (branchWorktree && hasSignificantWorkingTreeChanges(branchWorktree)) {
|
|
944
|
+
try {
|
|
945
|
+
const commitResult = autoCommitWorktreeForFinish(repoRoot, branchWorktree, branch, {});
|
|
946
|
+
if (commitResult.committed) {
|
|
947
|
+
counts = aheadBehind(repoRoot, branch, baseBranch);
|
|
948
|
+
}
|
|
949
|
+
} catch (error) {
|
|
950
|
+
summary.failed += 1;
|
|
951
|
+
summary.details.push(`[fail] ${branch}: auto-commit failed (${error.message}).`);
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
939
956
|
if (counts.ahead <= 0) {
|
|
940
957
|
summary.skipped += 1;
|
|
941
958
|
summary.details.push(`[skip] ${branch}: already merged into ${baseBranch}.`);
|
|
942
959
|
continue;
|
|
943
960
|
}
|
|
944
961
|
|
|
945
|
-
const branchWorktree = branchWorktrees.get(branch) || '';
|
|
946
962
|
if (branchWorktree && hasSignificantWorkingTreeChanges(branchWorktree)) {
|
|
947
963
|
summary.skipped += 1;
|
|
948
|
-
summary.details.push(`[skip] ${branch}: dirty worktree (${branchWorktree}).`);
|
|
964
|
+
summary.details.push(`[skip] ${branch}: dirty worktree after auto-commit (${branchWorktree}).`);
|
|
949
965
|
continue;
|
|
950
966
|
}
|
|
951
967
|
|
|
@@ -955,10 +971,16 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
|
955
971
|
branch,
|
|
956
972
|
'--base',
|
|
957
973
|
baseBranch,
|
|
958
|
-
'--via-pr',
|
|
959
|
-
waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge',
|
|
960
|
-
'--cleanup',
|
|
961
974
|
];
|
|
975
|
+
if (fallbackMode === 'local') {
|
|
976
|
+
finishArgs.push('--direct-only', '--no-push');
|
|
977
|
+
} else if (fallbackMode === 'direct') {
|
|
978
|
+
finishArgs.push('--direct-only');
|
|
979
|
+
} else {
|
|
980
|
+
finishArgs.push('--via-pr');
|
|
981
|
+
}
|
|
982
|
+
finishArgs.push(waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge');
|
|
983
|
+
finishArgs.push('--cleanup');
|
|
962
984
|
const finishResult = runPackageAsset('branchFinish', finishArgs, { cwd: repoRoot });
|
|
963
985
|
const combinedOutput = [finishResult.stdout || '', finishResult.stderr || ''].join('\n').trim();
|
|
964
986
|
|
package/src/finish/index.js
CHANGED
package/src/output/index.js
CHANGED
|
@@ -6,6 +6,8 @@ const {
|
|
|
6
6
|
LEGACY_NAMES,
|
|
7
7
|
GUARDEX_REPO_TOGGLE_ENV,
|
|
8
8
|
CLI_COMMAND_DESCRIPTIONS,
|
|
9
|
+
CLI_COMMAND_GROUPS,
|
|
10
|
+
CLI_QUICKSTART_STEPS,
|
|
9
11
|
AGENT_BOT_DESCRIPTIONS,
|
|
10
12
|
DOCTOR_AUTO_FINISH_DETAIL_LIMIT,
|
|
11
13
|
DOCTOR_AUTO_FINISH_BRANCH_LABEL_MAX,
|
|
@@ -166,6 +168,41 @@ function commandCatalogLines(indent = ' ') {
|
|
|
166
168
|
);
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
// groupedCommandCatalogLines renders CLI_COMMAND_GROUPS as a nested list with
|
|
172
|
+
// group headers separated by blank lines. It accepts an optional `colorize`
|
|
173
|
+
// callback so the caller can decide whether to decorate the group label (tty
|
|
174
|
+
// mode) or leave it plain (non-tty / NO_COLOR). Returns an array of lines;
|
|
175
|
+
// `null` entries mean "emit a blank line" so tree renderers can echo pipe
|
|
176
|
+
// characters on the separator rows.
|
|
177
|
+
function groupedCommandCatalogLines(indent = ' ', options = {}) {
|
|
178
|
+
const colorizeLabel = typeof options.colorizeLabel === 'function'
|
|
179
|
+
? options.colorizeLabel
|
|
180
|
+
: (text) => text;
|
|
181
|
+
const maxCommandLength = CLI_COMMAND_DESCRIPTIONS.reduce(
|
|
182
|
+
(max, [command]) => Math.max(max, command.length),
|
|
183
|
+
0,
|
|
184
|
+
);
|
|
185
|
+
const lines = [];
|
|
186
|
+
for (let groupIndex = 0; groupIndex < CLI_COMMAND_GROUPS.length; groupIndex += 1) {
|
|
187
|
+
const group = CLI_COMMAND_GROUPS[groupIndex];
|
|
188
|
+
const header = group.description
|
|
189
|
+
? `${colorizeLabel(group.label)} — ${group.description}`
|
|
190
|
+
: colorizeLabel(group.label);
|
|
191
|
+
lines.push(`${indent}${header}`);
|
|
192
|
+
for (const [command, description] of group.commands) {
|
|
193
|
+
lines.push(`${indent} ${command.padEnd(maxCommandLength + 2)}${description}`);
|
|
194
|
+
}
|
|
195
|
+
if (groupIndex < CLI_COMMAND_GROUPS.length - 1) {
|
|
196
|
+
lines.push(null);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return lines;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function quickstartLines(indent = ' ') {
|
|
203
|
+
return CLI_QUICKSTART_STEPS.map((step, index) => `${indent}${index + 1}. ${step}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
169
206
|
function agentBotCatalogLines(indent = ' ') {
|
|
170
207
|
const maxCommandLength = AGENT_BOT_DESCRIPTIONS.reduce(
|
|
171
208
|
(max, [command]) => Math.max(max, command.length),
|
|
@@ -182,19 +219,43 @@ function repoToggleLines(indent = ' ') {
|
|
|
182
219
|
];
|
|
183
220
|
}
|
|
184
221
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
222
|
+
const KNOWN_CLI_BASENAMES = new Set(['gx', 'gitguardex', 'guardex']);
|
|
223
|
+
|
|
224
|
+
function getInvokedCliName() {
|
|
225
|
+
const raw = path.basename(String(process.argv[1] || '')).replace(/\.js$/, '');
|
|
226
|
+
if (!KNOWN_CLI_BASENAMES.has(raw)) {
|
|
227
|
+
return SHORT_TOOL_NAME;
|
|
228
|
+
}
|
|
229
|
+
return raw;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function printToolLogsSummary(options = {}) {
|
|
233
|
+
const invoked = options.invokedBasename || getInvokedCliName();
|
|
234
|
+
const compact = Boolean(options.compact);
|
|
235
|
+
|
|
236
|
+
if (compact) {
|
|
237
|
+
const helpLine = `Try '${invoked} help' for commands, or '${invoked} status --verbose' for full service details.`;
|
|
238
|
+
console.log(`[${TOOL_NAME}] ${colorize(helpLine, '2')}`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const usageLine = ` $ ${invoked} <command> [options]`;
|
|
243
|
+
const quickstartDetails = quickstartLines(' ');
|
|
188
244
|
const agentBotDetails = agentBotCatalogLines(' ');
|
|
189
245
|
const repoToggleDetails = repoToggleLines(' ');
|
|
190
246
|
|
|
191
247
|
if (!supportsAnsiColors()) {
|
|
192
|
-
|
|
248
|
+
const commandDetails = groupedCommandCatalogLines(' ');
|
|
249
|
+
console.log(`${invoked} help:`);
|
|
193
250
|
console.log(' USAGE');
|
|
194
251
|
console.log(usageLine);
|
|
252
|
+
console.log(' QUICKSTART');
|
|
253
|
+
for (const line of quickstartDetails) {
|
|
254
|
+
console.log(line);
|
|
255
|
+
}
|
|
195
256
|
console.log(' COMMANDS');
|
|
196
257
|
for (const line of commandDetails) {
|
|
197
|
-
console.log(line);
|
|
258
|
+
console.log(line ?? '');
|
|
198
259
|
}
|
|
199
260
|
console.log(' AGENT BOT');
|
|
200
261
|
for (const line of agentBotDetails) {
|
|
@@ -204,24 +265,33 @@ function printToolLogsSummary() {
|
|
|
204
265
|
for (const line of repoToggleDetails) {
|
|
205
266
|
console.log(line);
|
|
206
267
|
}
|
|
268
|
+
console.log(` Try '${invoked} doctor' for one-step repair + verification.`);
|
|
207
269
|
return;
|
|
208
270
|
}
|
|
209
271
|
|
|
210
|
-
const title = colorize(`${
|
|
272
|
+
const title = colorize(`${invoked} help`, '1;36');
|
|
211
273
|
const usageHeader = colorize('USAGE', '1');
|
|
274
|
+
const quickstartHeader = colorize('QUICKSTART', '1');
|
|
212
275
|
const commandsHeader = colorize('COMMANDS', '1');
|
|
213
276
|
const agentBotHeader = colorize('AGENT BOT', '1');
|
|
214
277
|
const repoToggleHeader = colorize('REPO TOGGLE', '1');
|
|
215
278
|
const pipe = colorize('│', '90');
|
|
216
279
|
const tee = colorize('├', '90');
|
|
217
280
|
const corner = colorize('└', '90');
|
|
281
|
+
const commandDetails = groupedCommandCatalogLines(' ', {
|
|
282
|
+
colorizeLabel: (text) => colorize(text, '1;36'),
|
|
283
|
+
});
|
|
218
284
|
|
|
219
285
|
console.log(`${title}:`);
|
|
220
286
|
console.log(` ${tee}─ ${usageHeader}`);
|
|
221
287
|
console.log(` ${pipe}${usageLine}`);
|
|
288
|
+
console.log(` ${tee}─ ${quickstartHeader}`);
|
|
289
|
+
for (const line of quickstartDetails) {
|
|
290
|
+
console.log(` ${pipe}${line.slice(2)}`);
|
|
291
|
+
}
|
|
222
292
|
console.log(` ${tee}─ ${commandsHeader}`);
|
|
223
293
|
for (const line of commandDetails) {
|
|
224
|
-
if (
|
|
294
|
+
if (line == null) {
|
|
225
295
|
console.log(` ${pipe}`);
|
|
226
296
|
continue;
|
|
227
297
|
}
|
|
@@ -243,11 +313,18 @@ function printToolLogsSummary() {
|
|
|
243
313
|
}
|
|
244
314
|
console.log(` ${pipe}${line.slice(2)}`);
|
|
245
315
|
}
|
|
246
|
-
console.log(` ${corner}─ ${colorize(`Try '${
|
|
316
|
+
console.log(` ${corner}─ ${colorize(`Try '${invoked} doctor' for one-step repair + verification.`, '2')}`);
|
|
247
317
|
}
|
|
248
318
|
|
|
249
319
|
function usage(options = {}) {
|
|
250
320
|
const { outsideGitRepo = false } = options;
|
|
321
|
+
const invoked = options.invokedBasename || getInvokedCliName();
|
|
322
|
+
|
|
323
|
+
const groupedCommandLines = groupedCommandCatalogLines(' ', {
|
|
324
|
+
colorizeLabel: (text) => colorize(text, '1;36'),
|
|
325
|
+
})
|
|
326
|
+
.map((line) => (line == null ? '' : line))
|
|
327
|
+
.join('\n');
|
|
251
328
|
|
|
252
329
|
console.log(`A command-line tool that sets up hardened multi-agent safety for git repositories.
|
|
253
330
|
|
|
@@ -255,10 +332,13 @@ VERSION
|
|
|
255
332
|
${runtimeVersion()}
|
|
256
333
|
|
|
257
334
|
USAGE
|
|
258
|
-
$ ${
|
|
335
|
+
$ ${invoked} <command> [options]
|
|
336
|
+
|
|
337
|
+
QUICKSTART
|
|
338
|
+
${quickstartLines().join('\n')}
|
|
259
339
|
|
|
260
340
|
COMMANDS
|
|
261
|
-
${
|
|
341
|
+
${groupedCommandLines}
|
|
262
342
|
|
|
263
343
|
AGENT BOT
|
|
264
344
|
${agentBotCatalogLines().join('\n')}
|
|
@@ -267,19 +347,20 @@ REPO TOGGLE
|
|
|
267
347
|
${repoToggleLines().join('\n')}
|
|
268
348
|
|
|
269
349
|
NOTES
|
|
270
|
-
- No command = ${
|
|
350
|
+
- No command = ${invoked} status (compact in a TTY; pass --verbose for full services + help tree).
|
|
351
|
+
- ${invoked} init is an alias of ${invoked} setup.
|
|
271
352
|
- Global installs need Y/N approval; GitHub CLI (gh) is required for PR automation.
|
|
272
|
-
- Target another repo: ${
|
|
353
|
+
- Target another repo: ${invoked} <cmd> --target <repo-path>.
|
|
273
354
|
- On protected main, setup/install/fix/doctor auto-sandbox via agent branch + PR flow.
|
|
274
|
-
- Run '${
|
|
355
|
+
- Run '${invoked} cleanup' to prune merged agent branches/worktrees.
|
|
275
356
|
- Legacy aliases: ${LEGACY_NAMES.join(', ')}.`);
|
|
276
357
|
|
|
277
358
|
if (outsideGitRepo) {
|
|
278
359
|
console.log(`
|
|
279
360
|
[${TOOL_NAME}] No git repository detected in current directory.
|
|
280
361
|
[${TOOL_NAME}] Start from a repo root, or pass an explicit target:
|
|
281
|
-
${
|
|
282
|
-
${
|
|
362
|
+
${invoked} setup --target <path-to-git-repo>
|
|
363
|
+
${invoked} doctor --target <path-to-git-repo>`);
|
|
283
364
|
}
|
|
284
365
|
}
|
|
285
366
|
|
|
@@ -294,6 +375,59 @@ function formatElapsedDuration(ms) {
|
|
|
294
375
|
return `${Math.round(durationMs / 1000)}s`;
|
|
295
376
|
}
|
|
296
377
|
|
|
378
|
+
function startTransientSpinner(message, options = {}) {
|
|
379
|
+
const stream = options.stream || process.stdout;
|
|
380
|
+
if (!stream || !stream.isTTY || typeof stream.write !== 'function') {
|
|
381
|
+
return {
|
|
382
|
+
stop() {},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const frames = supportsAnsiColors()
|
|
387
|
+
? ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
388
|
+
: ['-', '\\', '|', '/'];
|
|
389
|
+
const intervalMs = Number.isFinite(options.intervalMs) ? Math.max(60, options.intervalMs) : 80;
|
|
390
|
+
const prefix = String(options.prefix || `[${TOOL_NAME}]`).trim();
|
|
391
|
+
const text = String(message || '').trim();
|
|
392
|
+
let frameIndex = 0;
|
|
393
|
+
let stopped = false;
|
|
394
|
+
|
|
395
|
+
const render = () => {
|
|
396
|
+
const frame = frames[frameIndex % frames.length];
|
|
397
|
+
frameIndex += 1;
|
|
398
|
+
const indicator = supportsAnsiColors() ? colorize(frame, '36') : frame;
|
|
399
|
+
stream.write(`\r${prefix} ${indicator} ${text}`);
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const clear = () => {
|
|
403
|
+
stream.write('\r');
|
|
404
|
+
if (typeof stream.clearLine === 'function') {
|
|
405
|
+
stream.clearLine(0);
|
|
406
|
+
}
|
|
407
|
+
if (typeof stream.cursorTo === 'function') {
|
|
408
|
+
stream.cursorTo(0);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
render();
|
|
413
|
+
const timer = setInterval(render, intervalMs);
|
|
414
|
+
if (typeof timer.unref === 'function') {
|
|
415
|
+
timer.unref();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
stop(finalLine = '') {
|
|
420
|
+
if (stopped) return;
|
|
421
|
+
stopped = true;
|
|
422
|
+
clearInterval(timer);
|
|
423
|
+
clear();
|
|
424
|
+
if (finalLine) {
|
|
425
|
+
stream.write(`${finalLine}\n`);
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
297
431
|
function truncateMiddle(value, maxLength) {
|
|
298
432
|
const text = String(value || '');
|
|
299
433
|
const limit = Number.isFinite(maxLength) ? Math.max(4, maxLength) : 0;
|
|
@@ -454,8 +588,10 @@ module.exports = {
|
|
|
454
588
|
agentBotCatalogLines,
|
|
455
589
|
repoToggleLines,
|
|
456
590
|
printToolLogsSummary,
|
|
591
|
+
getInvokedCliName,
|
|
457
592
|
usage,
|
|
458
593
|
formatElapsedDuration,
|
|
594
|
+
startTransientSpinner,
|
|
459
595
|
truncateMiddle,
|
|
460
596
|
truncateTail,
|
|
461
597
|
compactAutoFinishPathSegments,
|
package/src/toolchain/index.js
CHANGED
|
@@ -545,6 +545,10 @@ function installGlobalToolchain(options) {
|
|
|
545
545
|
};
|
|
546
546
|
}
|
|
547
547
|
|
|
548
|
+
return performCompanionInstall(missingPackages, missingLocalTools);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function performCompanionInstall(missingPackages, missingLocalTools) {
|
|
548
552
|
const installed = [];
|
|
549
553
|
if (missingPackages.length > 0) {
|
|
550
554
|
console.log(
|
|
@@ -593,6 +597,7 @@ module.exports = {
|
|
|
593
597
|
formatGlobalToolchainServiceName,
|
|
594
598
|
describeMissingGlobalDependencyWarnings,
|
|
595
599
|
describeCompanionInstallCommands,
|
|
600
|
+
buildMissingCompanionInstallPrompt,
|
|
596
601
|
detectGlobalToolchainPackages,
|
|
597
602
|
detectRequiredSystemTools,
|
|
598
603
|
detectOptionalLocalCompanionTools,
|
|
@@ -600,4 +605,5 @@ module.exports = {
|
|
|
600
605
|
maybeSelfUpdateBeforeStatus,
|
|
601
606
|
maybeOpenSpecUpdateBeforeStatus,
|
|
602
607
|
installGlobalToolchain,
|
|
608
|
+
performCompanionInstall,
|
|
603
609
|
};
|
|
@@ -36,10 +36,14 @@ Default: less word, same proof.
|
|
|
36
36
|
- Auto-clarity wins: switch back to `lite` or normal wording for security warnings, irreversible actions, privacy/compliance notes, ordered instructions where fragments may confuse, or when the user is confused and needs more detail.
|
|
37
37
|
- Boundaries stay normal/exact for code, commits, PR text, specs, logs, and blocker evidence.
|
|
38
38
|
|
|
39
|
-
**Isolation.** Every task runs on a dedicated `agent/*` branch + worktree. Start with `gx branch start "<task>" "<agent-name>"`. Treat the base branch (`main`/`dev`) as read-only while an agent branch is active.
|
|
39
|
+
**Isolation.** Every task runs on a dedicated `agent/*` branch + worktree. Start with `gx branch start "<task>" "<agent-name>"`. Treat the base branch (`main`/`dev`) as read-only while an agent branch is active. The `.githooks/post-checkout` hook auto-reverts primary-branch switches during agent sessions and auto-stashes a dirty tree before reverting - bypass only with `GUARDEX_ALLOW_PRIMARY_BRANCH_SWITCH=1`.
|
|
40
40
|
For every new task, including follow-up work in the same chat/session, if an assigned agent sub-branch/worktree is already open, continue in that sub-branch instead of creating a fresh lane unless the user explicitly redirects scope.
|
|
41
41
|
Never implement directly on the local/base branch checkout; keep it unchanged and perform all edits in the agent sub-branch/worktree.
|
|
42
42
|
|
|
43
|
+
**Primary-tree lock (blocking).** On the primary checkout, do NOT run any of: `git checkout <ref>`, `git switch <ref>`, `git switch -c ...`, `git checkout -b ...`, or `git worktree add <path> <existing-agent-branch>`. The only branch-changing commands allowed on primary are `git fetch` and `git pull --ff-only` against the protected branch itself. To work on any `agent/*` branch, run `gx branch start ...` first, then `cd` into the printed `.omc/agent-worktrees/...` path and run every subsequent git command from inside that worktree. If you find yourself typing `git checkout agent/...` or `git switch agent/...` from the primary cwd, stop - that is the mistake that flips primary onto an agent branch.
|
|
44
|
+
|
|
45
|
+
**Dirty-tree rule.** Finish or stash edits inside the worktree they belong to before any branch switch on primary. The post-checkout guard auto-stashes a dirty primary tree as `guardex-auto-revert <ts> <prev>-><new>` before reverting, but that is a safety net, not a workflow; do not rely on it routinely. Recover stashed changes with `git stash list | grep 'guardex-auto-revert'`.
|
|
46
|
+
|
|
43
47
|
**Ownership.** Before editing, claim files: `gx locks claim --branch "<agent-branch>" <file...>`. Before deleting, confirm the path is in your claim. Don't edit outside your scope unless reassigned.
|
|
44
48
|
|
|
45
49
|
**Handoff gate.** Post a one-line handoff note (plan/change, owned scope, intended action) before editing. Re-read the latest handoffs before replacing others' code.
|
|
@@ -73,8 +73,14 @@ if [[ "$is_agent" == "1" ]]; then
|
|
|
73
73
|
GUARDEX_ALLOW_PRIMARY_BRANCH_SWITCH=1 git checkout "$prev_branch" >/dev/null 2>&1 || true
|
|
74
74
|
echo "[agent-primary-branch-guard] Reverted to '$prev_branch'." >&2
|
|
75
75
|
else
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
stash_msg="guardex-auto-revert $(date +%s) ${prev_branch}->${new_branch}"
|
|
77
|
+
if git stash push --include-untracked -m "$stash_msg" >/dev/null 2>&1; then
|
|
78
|
+
GUARDEX_ALLOW_PRIMARY_BRANCH_SWITCH=1 git checkout "$prev_branch" >/dev/null 2>&1 || true
|
|
79
|
+
echo "[agent-primary-branch-guard] Dirty tree auto-stashed as '$stash_msg'; primary reverted to '$prev_branch'." >&2
|
|
80
|
+
echo "[agent-primary-branch-guard] Restore later with: git stash list | grep 'guardex-auto-revert'" >&2
|
|
81
|
+
else
|
|
82
|
+
echo "[agent-primary-branch-guard] Auto-stash failed; working tree left on '$new_branch'. Fix manually: git stash -u && git checkout $prev_branch" >&2
|
|
83
|
+
fi
|
|
78
84
|
fi
|
|
79
85
|
else
|
|
80
86
|
echo "[agent-primary-branch-guard] Bypass with GUARDEX_ALLOW_PRIMARY_BRANCH_SWITCH=1 if intentional." >&2
|
|
@@ -389,30 +389,45 @@ if [[ "$MERGE_MODE" == "pr" && "$PUSH_ENABLED" -eq 1 ]]; then
|
|
|
389
389
|
fi
|
|
390
390
|
|
|
391
391
|
if [[ "$should_create_integration_helper" -eq 1 ]]; then
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
integration_branch="$integration_branch_base"
|
|
397
|
-
integration_suffix=1
|
|
398
|
-
while [[ -e "$integration_worktree" ]] || git -C "$repo_root" show-ref --verify --quiet "refs/heads/${integration_branch}"; do
|
|
399
|
-
integration_worktree="${integration_worktree_base}-${integration_suffix}"
|
|
400
|
-
integration_branch="${integration_branch_base}_${integration_suffix}"
|
|
401
|
-
integration_suffix=$((integration_suffix + 1))
|
|
402
|
-
done
|
|
403
|
-
mkdir -p "$(dirname "$integration_worktree")"
|
|
392
|
+
existing_base_worktree=""
|
|
393
|
+
if [[ "$PUSH_ENABLED" -eq 0 ]]; then
|
|
394
|
+
existing_base_worktree="$(get_worktree_for_branch "$BASE_BRANCH")"
|
|
395
|
+
fi
|
|
404
396
|
|
|
405
|
-
|
|
406
|
-
|
|
397
|
+
if [[ -n "$existing_base_worktree" ]] && is_clean_worktree "$existing_base_worktree"; then
|
|
398
|
+
if ! git -C "$existing_base_worktree" merge --no-ff --no-edit "$SOURCE_BRANCH"; then
|
|
399
|
+
echo "[agent-branch-finish] Merge conflict detected while merging '${SOURCE_BRANCH}' into '${BASE_BRANCH}'." >&2
|
|
400
|
+
git -C "$existing_base_worktree" merge --abort >/dev/null 2>&1 || true
|
|
401
|
+
exit 1
|
|
402
|
+
fi
|
|
403
|
+
merge_completed=1
|
|
404
|
+
merge_status="direct"
|
|
405
|
+
else
|
|
406
|
+
integration_stamp="$(date +%Y%m%d-%H%M%S)"
|
|
407
|
+
integration_worktree_base="${temp_worktree_root}/__integrate-${BASE_BRANCH//\//__}-${integration_stamp}"
|
|
408
|
+
integration_branch_base="__agent_integrate_${BASE_BRANCH//\//_}_$(date +%Y%m%d_%H%M%S)"
|
|
409
|
+
integration_worktree="$integration_worktree_base"
|
|
410
|
+
integration_branch="$integration_branch_base"
|
|
411
|
+
integration_suffix=1
|
|
412
|
+
while [[ -e "$integration_worktree" ]] || git -C "$repo_root" show-ref --verify --quiet "refs/heads/${integration_branch}"; do
|
|
413
|
+
integration_worktree="${integration_worktree_base}-${integration_suffix}"
|
|
414
|
+
integration_branch="${integration_branch_base}_${integration_suffix}"
|
|
415
|
+
integration_suffix=$((integration_suffix + 1))
|
|
416
|
+
done
|
|
417
|
+
mkdir -p "$(dirname "$integration_worktree")"
|
|
418
|
+
|
|
419
|
+
git -C "$repo_root" worktree add "$integration_worktree" "$start_ref" >/dev/null
|
|
420
|
+
git -C "$integration_worktree" checkout -b "$integration_branch" >/dev/null
|
|
421
|
+
|
|
422
|
+
if ! git -C "$integration_worktree" merge --no-ff --no-edit "$SOURCE_BRANCH"; then
|
|
423
|
+
echo "[agent-branch-finish] Merge conflict detected while merging '${SOURCE_BRANCH}' into '${BASE_BRANCH}'." >&2
|
|
424
|
+
git -C "$integration_worktree" merge --abort >/dev/null 2>&1 || true
|
|
425
|
+
exit 1
|
|
426
|
+
fi
|
|
407
427
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
git -C "$integration_worktree" merge --abort >/dev/null 2>&1 || true
|
|
411
|
-
exit 1
|
|
428
|
+
merge_completed=1
|
|
429
|
+
merge_status="direct"
|
|
412
430
|
fi
|
|
413
|
-
|
|
414
|
-
merge_completed=1
|
|
415
|
-
merge_status="direct"
|
|
416
431
|
fi
|
|
417
432
|
|
|
418
433
|
is_local_branch_delete_error() {
|
|
@@ -340,16 +340,30 @@ resolve_openspec_capability_slug() {
|
|
|
340
340
|
sanitize_slug "$task_slug" "general-behavior"
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
resolve_repo_prefix() {
|
|
344
|
+
local root
|
|
345
|
+
root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
346
|
+
basename "$root"
|
|
347
|
+
}
|
|
348
|
+
|
|
343
349
|
resolve_worktree_leaf() {
|
|
344
350
|
local branch_name="$1"
|
|
345
351
|
local agent_slug="$2"
|
|
346
352
|
local masterplan_label=""
|
|
347
353
|
local branch_leaf=""
|
|
354
|
+
local repo_prefix
|
|
355
|
+
repo_prefix="$(resolve_repo_prefix)"
|
|
348
356
|
|
|
349
357
|
masterplan_label="$(resolve_openspec_masterplan_label)"
|
|
350
358
|
if [[ -n "$masterplan_label" ]] && [[ "$branch_name" == "agent/${agent_slug}/"* ]]; then
|
|
351
359
|
branch_leaf="${branch_name#agent/${agent_slug}/}"
|
|
352
|
-
printf '
|
|
360
|
+
printf '%s__%s__%s__%s' "$repo_prefix" "$agent_slug" "$masterplan_label" "$branch_leaf"
|
|
361
|
+
return 0
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
if [[ "$branch_name" == agent/*/* ]]; then
|
|
365
|
+
local without_agent="${branch_name#agent/}"
|
|
366
|
+
printf '%s__%s' "$repo_prefix" "${without_agent//\//__}"
|
|
353
367
|
return 0
|
|
354
368
|
fi
|
|
355
369
|
|
|
@@ -372,17 +372,32 @@ resolve_openspec_capability_slug() {
|
|
|
372
372
|
sanitize_slug "$task_slug" "general-behavior"
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
resolve_repo_prefix() {
|
|
376
|
+
local root
|
|
377
|
+
root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
378
|
+
basename "$root"
|
|
379
|
+
}
|
|
380
|
+
|
|
375
381
|
resolve_worktree_leaf() {
|
|
376
382
|
local branch_name="$1"
|
|
377
383
|
local masterplan_label=""
|
|
378
384
|
local branch_role=""
|
|
379
385
|
local branch_leaf=""
|
|
386
|
+
local repo_prefix
|
|
387
|
+
repo_prefix="$(resolve_repo_prefix)"
|
|
380
388
|
|
|
381
389
|
masterplan_label="$(resolve_openspec_masterplan_label)"
|
|
382
390
|
if [[ -n "$masterplan_label" ]] && [[ "$branch_name" =~ ^agent/([^/]+)/(.+)$ ]]; then
|
|
383
391
|
branch_role="${BASH_REMATCH[1]}"
|
|
384
392
|
branch_leaf="${BASH_REMATCH[2]}"
|
|
385
|
-
printf '
|
|
393
|
+
printf '%s__%s__%s__%s' "$repo_prefix" "$branch_role" "$masterplan_label" "$branch_leaf"
|
|
394
|
+
return 0
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
if [[ "$branch_name" =~ ^agent/([^/]+)/(.+)$ ]]; then
|
|
398
|
+
branch_role="${BASH_REMATCH[1]}"
|
|
399
|
+
branch_leaf="${BASH_REMATCH[2]}"
|
|
400
|
+
printf '%s__%s__%s' "$repo_prefix" "$branch_role" "$branch_leaf"
|
|
386
401
|
return 0
|
|
387
402
|
fi
|
|
388
403
|
|