@imdeadpool/guardex 5.0.17 → 6.0.1
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 +14 -6
- package/bin/multiagent-safety.js +22 -22
- package/package.json +3 -2
- package/templates/AGENTS.multiagent-safety.md +1 -1
- package/templates/githooks/post-merge +3 -39
- package/templates/githooks/pre-commit +199 -33
- package/templates/githooks/pre-push +2 -2
- package/templates/scripts/agent-branch-finish.sh +707 -75
- package/templates/scripts/agent-branch-start.sh +934 -73
- package/templates/scripts/agent-worktree-prune.sh +356 -68
- package/templates/scripts/codex-agent.sh +242 -630
- package/templates/scripts/install-agent-git-hooks.sh +27 -4
- package/templates/scripts/openspec/init-change-workspace.sh +50 -4
- package/templates/scripts/openspec/init-plan-workspace.sh +495 -48
package/README.md
CHANGED
|
@@ -349,11 +349,11 @@ openspec update
|
|
|
349
349
|
### OpenSpec in agent sub-branches
|
|
350
350
|
|
|
351
351
|
- `scripts/codex-agent.sh` enforces OpenSpec workspaces before it launches Codex in each sandbox branch/worktree.
|
|
352
|
-
- `scripts/agent-branch-start.sh` can scaffold both `openspec/changes/<agent-branch-slug>/` and `openspec/plan/<agent-branch-slug>/` when you set `
|
|
353
|
-
- Set `
|
|
354
|
-
- Set `
|
|
355
|
-
- Set `
|
|
356
|
-
- Set `
|
|
352
|
+
- `scripts/agent-branch-start.sh` can scaffold both `openspec/changes/<agent-branch-slug>/` and `openspec/plan/<agent-branch-slug>/` when you set `GUARDEX_OPENSPEC_AUTO_INIT=true`.
|
|
353
|
+
- Set `GUARDEX_OPENSPEC_AUTO_INIT=false` (default for `agent-branch-start`) to skip branch-start auto-bootstrap.
|
|
354
|
+
- Set `GUARDEX_OPENSPEC_PLAN_SLUG=<kebab-case-slug>` to force a specific plan workspace name.
|
|
355
|
+
- Set `GUARDEX_OPENSPEC_CHANGE_SLUG=<kebab-case-slug>` to force a specific change workspace name.
|
|
356
|
+
- Set `GUARDEX_OPENSPEC_CAPABILITY_SLUG=<kebab-case-slug>` to override the default capability folder used for `spec.md` scaffolding.
|
|
357
357
|
|
|
358
358
|
## Security and maintenance posture
|
|
359
359
|
|
|
@@ -372,6 +372,14 @@ npm pack --dry-run
|
|
|
372
372
|
|
|
373
373
|
## Release notes
|
|
374
374
|
|
|
375
|
+
### v6.0.0
|
|
376
|
+
|
|
377
|
+
- **Breaking** — removed the legacy `musafety` bin alias and all `MUSAFETY_*` environment variables. Callers must migrate to the `guardex` / `gx` bins and the `GUARDEX_*` env-var surface.
|
|
378
|
+
- **Breaking** — bootstrap manifest filename changed from `musafety-bootstrap-manifest.json` to `guardex-bootstrap-manifest.json`; existing sandbox worktrees must be pruned + re-bootstrapped (or have their manifest manually renamed).
|
|
379
|
+
- Rebranded all remaining `musafety` / `Musafety` / `MUSAFETY` codename tokens to `guardex` / `Guardex` / `GUARDEX` across scripts, templates, hooks, tests, and docs.
|
|
380
|
+
- The descriptive phrase `multiagent-safety` (including `bin/multiagent-safety.js` and `templates/AGENTS.multiagent-safety.md`) is preserved intentionally — only the short codename changed.
|
|
381
|
+
- Bumped package version from `5.0.17` to `6.0.0` for the next npm publish.
|
|
382
|
+
|
|
375
383
|
### v5.0.17
|
|
376
384
|
|
|
377
385
|
- Bumped package version from `5.0.16` to `5.0.17` for the next npm publish.
|
|
@@ -417,7 +425,7 @@ npm pack --dry-run
|
|
|
417
425
|
|
|
418
426
|
### v5.0.9
|
|
419
427
|
|
|
420
|
-
- Enforced OpenSpec workspace bootstrap for sandbox agent execution: `scripts/codex-agent.sh` now initializes `openspec/plan/<agent-branch-slug>/` before launching Codex, and `scripts/agent-branch-start.sh` supports `
|
|
428
|
+
- Enforced OpenSpec workspace bootstrap for sandbox agent execution: `scripts/codex-agent.sh` now initializes `openspec/plan/<agent-branch-slug>/` before launching Codex, and `scripts/agent-branch-start.sh` supports `GUARDEX_OPENSPEC_AUTO_INIT` plus `GUARDEX_OPENSPEC_PLAN_SLUG`.
|
|
421
429
|
- Tightened doctor auto-finish correctness: sandbox finish now waits for merge and exits non-zero if the PR closes without merge, so repair flows are not reported as complete when policy blocks merge.
|
|
422
430
|
- Updated package version from `5.0.8` to `5.0.9` for the next npm publish.
|
|
423
431
|
|
package/bin/multiagent-safety.js
CHANGED
|
@@ -9,14 +9,14 @@ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
|
9
9
|
|
|
10
10
|
const TOOL_NAME = 'guardex';
|
|
11
11
|
const SHORT_TOOL_NAME = 'gx';
|
|
12
|
-
const LEGACY_NAMES = ['
|
|
12
|
+
const LEGACY_NAMES = ['guardex', 'multiagent-safety'];
|
|
13
13
|
const OPENSPEC_PACKAGE = '@fission-ai/openspec';
|
|
14
14
|
const GLOBAL_TOOLCHAIN_PACKAGES = [
|
|
15
15
|
'oh-my-codex',
|
|
16
16
|
OPENSPEC_PACKAGE,
|
|
17
17
|
'@imdeadpool/codex-account-switcher',
|
|
18
18
|
];
|
|
19
|
-
const GH_BIN = process.env.
|
|
19
|
+
const GH_BIN = process.env.GUARDEX_GH_BIN || 'gh';
|
|
20
20
|
const REQUIRED_SYSTEM_TOOLS = [
|
|
21
21
|
{
|
|
22
22
|
name: 'gh',
|
|
@@ -26,11 +26,11 @@ const REQUIRED_SYSTEM_TOOLS = [
|
|
|
26
26
|
},
|
|
27
27
|
];
|
|
28
28
|
const MAINTAINER_RELEASE_REPO = path.resolve(
|
|
29
|
-
process.env.
|
|
29
|
+
process.env.GUARDEX_RELEASE_REPO || '/tmp/multiagent-safety',
|
|
30
30
|
);
|
|
31
|
-
const NPM_BIN = process.env.
|
|
32
|
-
const OPENSPEC_BIN = process.env.
|
|
33
|
-
const SCORECARD_BIN = process.env.
|
|
31
|
+
const NPM_BIN = process.env.GUARDEX_NPM_BIN || 'npm';
|
|
32
|
+
const OPENSPEC_BIN = process.env.GUARDEX_OPENSPEC_BIN || 'openspec';
|
|
33
|
+
const SCORECARD_BIN = process.env.GUARDEX_SCORECARD_BIN || 'scorecard';
|
|
34
34
|
const GIT_PROTECTED_BRANCHES_KEY = 'multiagent.protectedBranches';
|
|
35
35
|
const GIT_BASE_BRANCH_KEY = 'multiagent.baseBranch';
|
|
36
36
|
const GIT_SYNC_STRATEGY_KEY = 'multiagent.sync.strategy';
|
|
@@ -1405,7 +1405,7 @@ function finishDoctorSandboxBranch(blocked, metadata) {
|
|
|
1405
1405
|
note: 'origin remote missing; skipped auto-finish',
|
|
1406
1406
|
};
|
|
1407
1407
|
}
|
|
1408
|
-
const explicitGhBin = Boolean(String(process.env.
|
|
1408
|
+
const explicitGhBin = Boolean(String(process.env.GUARDEX_GH_BIN || '').trim());
|
|
1409
1409
|
if (!explicitGhBin && !originRemoteLooksLikeGithub(blocked.repoRoot)) {
|
|
1410
1410
|
return {
|
|
1411
1411
|
status: 'skipped',
|
|
@@ -1413,7 +1413,7 @@ function finishDoctorSandboxBranch(blocked, metadata) {
|
|
|
1413
1413
|
};
|
|
1414
1414
|
}
|
|
1415
1415
|
|
|
1416
|
-
const ghBin = process.env.
|
|
1416
|
+
const ghBin = process.env.GUARDEX_GH_BIN || 'gh';
|
|
1417
1417
|
if (!isCommandAvailable(ghBin)) {
|
|
1418
1418
|
return {
|
|
1419
1419
|
status: 'skipped',
|
|
@@ -1429,7 +1429,7 @@ function finishDoctorSandboxBranch(blocked, metadata) {
|
|
|
1429
1429
|
};
|
|
1430
1430
|
}
|
|
1431
1431
|
|
|
1432
|
-
const rawWaitTimeoutSeconds = Number.parseInt(process.env.
|
|
1432
|
+
const rawWaitTimeoutSeconds = Number.parseInt(process.env.GUARDEX_FINISH_WAIT_TIMEOUT_SECONDS || '1800', 10);
|
|
1433
1433
|
const waitTimeoutSeconds =
|
|
1434
1434
|
Number.isFinite(rawWaitTimeoutSeconds) && rawWaitTimeoutSeconds >= 30 ? rawWaitTimeoutSeconds : 1800;
|
|
1435
1435
|
const finishTimeoutMs = Math.max(180_000, (waitTimeoutSeconds + 60) * 1000);
|
|
@@ -2170,15 +2170,15 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
|
2170
2170
|
return summary;
|
|
2171
2171
|
}
|
|
2172
2172
|
|
|
2173
|
-
if (String(process.env.
|
|
2173
|
+
if (String(process.env.GUARDEX_DOCTOR_SANDBOX || '') === '1') {
|
|
2174
2174
|
summary.enabled = false;
|
|
2175
2175
|
summary.details.push('Skipped auto-finish sweep inside doctor sandbox pass.');
|
|
2176
2176
|
return summary;
|
|
2177
2177
|
}
|
|
2178
2178
|
|
|
2179
|
-
if (String(process.env.
|
|
2179
|
+
if (String(process.env.GUARDEX_SKIP_AUTO_FINISH_READY_BRANCHES || '') === '1') {
|
|
2180
2180
|
summary.enabled = false;
|
|
2181
|
-
summary.details.push('Skipped auto-finish sweep (
|
|
2181
|
+
summary.details.push('Skipped auto-finish sweep (GUARDEX_SKIP_AUTO_FINISH_READY_BRANCHES=1).');
|
|
2182
2182
|
return summary;
|
|
2183
2183
|
}
|
|
2184
2184
|
|
|
@@ -2201,14 +2201,14 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
|
2201
2201
|
summary.details.push('Skipped auto-finish sweep (origin remote missing).');
|
|
2202
2202
|
return summary;
|
|
2203
2203
|
}
|
|
2204
|
-
const explicitGhBin = Boolean(String(process.env.
|
|
2204
|
+
const explicitGhBin = Boolean(String(process.env.GUARDEX_GH_BIN || '').trim());
|
|
2205
2205
|
if (!explicitGhBin && !originRemoteLooksLikeGithub(repoRoot)) {
|
|
2206
2206
|
summary.enabled = false;
|
|
2207
2207
|
summary.details.push('Skipped auto-finish sweep (origin remote is not GitHub).');
|
|
2208
2208
|
return summary;
|
|
2209
2209
|
}
|
|
2210
2210
|
|
|
2211
|
-
const ghBin = process.env.
|
|
2211
|
+
const ghBin = process.env.GUARDEX_GH_BIN || 'gh';
|
|
2212
2212
|
if (run(ghBin, ['--version']).status !== 0) {
|
|
2213
2213
|
summary.enabled = false;
|
|
2214
2214
|
summary.details.push(`Skipped auto-finish sweep (${ghBin} not available).`);
|
|
@@ -3151,12 +3151,12 @@ function parseNpmVersionOutput(stdout) {
|
|
|
3151
3151
|
}
|
|
3152
3152
|
}
|
|
3153
3153
|
|
|
3154
|
-
function
|
|
3155
|
-
if (envFlagEnabled('
|
|
3154
|
+
function checkForGuardexUpdate() {
|
|
3155
|
+
if (envFlagEnabled('GUARDEX_SKIP_UPDATE_CHECK')) {
|
|
3156
3156
|
return { checked: false, reason: 'disabled' };
|
|
3157
3157
|
}
|
|
3158
3158
|
|
|
3159
|
-
const forceCheck = envFlagEnabled('
|
|
3159
|
+
const forceCheck = envFlagEnabled('GUARDEX_FORCE_UPDATE_CHECK');
|
|
3160
3160
|
if (!forceCheck && !isInteractiveTerminal()) {
|
|
3161
3161
|
return { checked: false, reason: 'non-interactive' };
|
|
3162
3162
|
}
|
|
@@ -3188,14 +3188,14 @@ function printUpdateAvailableBanner(current, latest) {
|
|
|
3188
3188
|
}
|
|
3189
3189
|
|
|
3190
3190
|
function maybeSelfUpdateBeforeStatus() {
|
|
3191
|
-
const check =
|
|
3191
|
+
const check = checkForGuardexUpdate();
|
|
3192
3192
|
if (!check.checked || !check.updateAvailable) {
|
|
3193
3193
|
return;
|
|
3194
3194
|
}
|
|
3195
3195
|
|
|
3196
3196
|
printUpdateAvailableBanner(check.current, check.latest);
|
|
3197
3197
|
|
|
3198
|
-
const autoApproval = parseAutoApproval('
|
|
3198
|
+
const autoApproval = parseAutoApproval('GUARDEX_AUTO_UPDATE_APPROVAL');
|
|
3199
3199
|
const interactive = isInteractiveTerminal();
|
|
3200
3200
|
|
|
3201
3201
|
if (!interactive && autoApproval == null) {
|
|
@@ -3224,11 +3224,11 @@ function maybeSelfUpdateBeforeStatus() {
|
|
|
3224
3224
|
}
|
|
3225
3225
|
|
|
3226
3226
|
function checkForOpenSpecPackageUpdate() {
|
|
3227
|
-
if (envFlagEnabled('
|
|
3227
|
+
if (envFlagEnabled('GUARDEX_SKIP_OPENSPEC_UPDATE_CHECK')) {
|
|
3228
3228
|
return { checked: false, reason: 'disabled' };
|
|
3229
3229
|
}
|
|
3230
3230
|
|
|
3231
|
-
const forceCheck = envFlagEnabled('
|
|
3231
|
+
const forceCheck = envFlagEnabled('GUARDEX_FORCE_OPENSPEC_UPDATE_CHECK');
|
|
3232
3232
|
if (!forceCheck && !isInteractiveTerminal()) {
|
|
3233
3233
|
return { checked: false, reason: 'non-interactive' };
|
|
3234
3234
|
}
|
|
@@ -3278,7 +3278,7 @@ function maybeOpenSpecUpdateBeforeStatus() {
|
|
|
3278
3278
|
|
|
3279
3279
|
printOpenSpecUpdateAvailableBanner(check.current, check.latest);
|
|
3280
3280
|
|
|
3281
|
-
const autoApproval = parseAutoApproval('
|
|
3281
|
+
const autoApproval = parseAutoApproval('GUARDEX_AUTO_OPENSPEC_UPDATE_APPROVAL');
|
|
3282
3282
|
const interactive = isInteractiveTerminal();
|
|
3283
3283
|
|
|
3284
3284
|
if (!interactive && autoApproval == null) {
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imdeadpool/guardex",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"description": "GuardeX: the Guardian T-Rex for your repo, with hardened multi-agent git guardrails.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"preferGlobal": true,
|
|
7
7
|
"bin": {
|
|
8
8
|
"guardex": "bin/multiagent-safety.js",
|
|
9
9
|
"gx": "bin/multiagent-safety.js",
|
|
10
|
-
"musafety": "bin/multiagent-safety.js",
|
|
11
10
|
"multiagent-safety": "bin/multiagent-safety.js"
|
|
12
11
|
},
|
|
13
12
|
"scripts": {
|
|
13
|
+
"prepack": "node pack-helpers/dereference-templates.mjs",
|
|
14
|
+
"postpack": "git checkout -- templates",
|
|
14
15
|
"test": "node --test test/*.test.js",
|
|
15
16
|
"agent:codex": "bash ./scripts/codex-agent.sh",
|
|
16
17
|
"agent:branch:start": "bash ./scripts/agent-branch-start.sh",
|
|
@@ -57,7 +57,7 @@ openspec/plan/<agent-branch-slug>/
|
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
For manual `scripts/agent-branch-start.sh` usage, enable auto-bootstrap with
|
|
60
|
-
`
|
|
60
|
+
`GUARDEX_OPENSPEC_AUTO_INIT=true` or scaffold manually before implementation:
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
63
|
bash scripts/openspec/init-change-workspace.sh "<change-slug>" "<capability-slug>"
|
|
@@ -1,43 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
# Auto-sync agent worktrees when the base branch is updated in this worktree.
|
|
5
|
+
if [[ -x "scripts/agent-sync-on-base-update.sh" ]]; then
|
|
6
|
+
bash scripts/agent-sync-on-base-update.sh --quiet || true
|
|
6
7
|
fi
|
|
7
|
-
|
|
8
|
-
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
9
|
-
if [[ -z "$repo_root" ]]; then
|
|
10
|
-
exit 0
|
|
11
|
-
fi
|
|
12
|
-
|
|
13
|
-
branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
14
|
-
if [[ -z "$branch" || "$branch" == "HEAD" ]]; then
|
|
15
|
-
exit 0
|
|
16
|
-
fi
|
|
17
|
-
|
|
18
|
-
base_branch="${MUSAFETY_BASE_BRANCH:-$(git -C "$repo_root" config --get multiagent.baseBranch || true)}"
|
|
19
|
-
if [[ -z "$base_branch" ]]; then
|
|
20
|
-
base_branch="dev"
|
|
21
|
-
fi
|
|
22
|
-
|
|
23
|
-
if [[ "$branch" != "$base_branch" ]]; then
|
|
24
|
-
exit 0
|
|
25
|
-
fi
|
|
26
|
-
|
|
27
|
-
cli_path="$repo_root/bin/multiagent-safety.js"
|
|
28
|
-
if [[ ! -f "$cli_path" ]]; then
|
|
29
|
-
exit 0
|
|
30
|
-
fi
|
|
31
|
-
|
|
32
|
-
node_bin="${MUSAFETY_NODE_BIN:-node}"
|
|
33
|
-
if ! command -v "$node_bin" >/dev/null 2>&1; then
|
|
34
|
-
exit 0
|
|
35
|
-
fi
|
|
36
|
-
|
|
37
|
-
"$node_bin" "$cli_path" cleanup \
|
|
38
|
-
--target "$repo_root" \
|
|
39
|
-
--base "$base_branch" \
|
|
40
|
-
--include-pr-merged \
|
|
41
|
-
--keep-clean-worktrees >/dev/null 2>&1 || true
|
|
42
|
-
|
|
43
|
-
exit 0
|
|
@@ -9,6 +9,12 @@ if [[ -z "$branch" ]]; then
|
|
|
9
9
|
exit 0
|
|
10
10
|
fi
|
|
11
11
|
|
|
12
|
+
git_dir="$(git rev-parse --git-dir 2>/dev/null || true)"
|
|
13
|
+
is_linked_worktree=0
|
|
14
|
+
if [[ -n "$git_dir" && "$git_dir" == *"/worktrees/"* ]]; then
|
|
15
|
+
is_linked_worktree=1
|
|
16
|
+
fi
|
|
17
|
+
|
|
12
18
|
if [[ "${ALLOW_COMMIT_ON_PROTECTED_BRANCH:-0}" == "1" ]]; then
|
|
13
19
|
exit 0
|
|
14
20
|
fi
|
|
@@ -24,11 +30,11 @@ if [[ -n "${CODEX_THREAD_ID:-}" || -n "${OMX_SESSION_ID:-}" || "${CODEX_CI:-0}"
|
|
|
24
30
|
fi
|
|
25
31
|
|
|
26
32
|
is_vscode_git_context=0
|
|
27
|
-
if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n "${VSCODE_IPC_HOOK_CLI:-}" ]]; then
|
|
33
|
+
if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n "${VSCODE_IPC_HOOK_CLI:-}" || "${TERM_PROGRAM:-}" == "vscode" ]]; then
|
|
28
34
|
is_vscode_git_context=1
|
|
29
35
|
fi
|
|
30
36
|
|
|
31
|
-
allow_vscode_protected_raw="${
|
|
37
|
+
allow_vscode_protected_raw="${GUARDEX_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
|
|
32
38
|
if [[ -z "$allow_vscode_protected_raw" ]]; then
|
|
33
39
|
allow_vscode_protected_raw="false"
|
|
34
40
|
fi
|
|
@@ -41,7 +47,7 @@ case "$allow_vscode_protected" in
|
|
|
41
47
|
*) allow_vscode_protected_branch_writes=0 ;;
|
|
42
48
|
esac
|
|
43
49
|
|
|
44
|
-
protected_branches_raw="${
|
|
50
|
+
protected_branches_raw="${GUARDEX_PROTECTED_BRANCHES:-$(git config --get multiagent.protectedBranches || true)}"
|
|
45
51
|
if [[ -z "$protected_branches_raw" ]]; then
|
|
46
52
|
protected_branches_raw="dev main master"
|
|
47
53
|
fi
|
|
@@ -55,7 +61,7 @@ for protected_branch in $protected_branches_raw; do
|
|
|
55
61
|
fi
|
|
56
62
|
done
|
|
57
63
|
|
|
58
|
-
codex_require_agent_branch_raw="${
|
|
64
|
+
codex_require_agent_branch_raw="${GUARDEX_CODEX_REQUIRE_AGENT_BRANCH:-$(git config --get multiagent.codexRequireAgentBranch || true)}"
|
|
59
65
|
if [[ -z "$codex_require_agent_branch_raw" ]]; then
|
|
60
66
|
codex_require_agent_branch_raw="true"
|
|
61
67
|
fi
|
|
@@ -68,6 +74,163 @@ case "$codex_require_agent_branch" in
|
|
|
68
74
|
*) should_require_codex_agent_branch=1 ;;
|
|
69
75
|
esac
|
|
70
76
|
|
|
77
|
+
sanitize_slug() {
|
|
78
|
+
local raw="$1"
|
|
79
|
+
local fallback="${2:-task}"
|
|
80
|
+
local slug
|
|
81
|
+
slug="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
|
|
82
|
+
if [[ -z "$slug" ]]; then
|
|
83
|
+
slug="$fallback"
|
|
84
|
+
fi
|
|
85
|
+
printf '%s' "$slug"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
resolve_agent_branch_base() {
|
|
89
|
+
local branch_name="$1"
|
|
90
|
+
git config --get "branch.${branch_name}.guardexBase" || true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
is_helper_agent_branch() {
|
|
94
|
+
local branch_name="$1"
|
|
95
|
+
local base_branch=""
|
|
96
|
+
base_branch="$(resolve_agent_branch_base "$branch_name")"
|
|
97
|
+
[[ "$base_branch" == agent/* ]]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ensure_agent_branch_openspec_workspace() {
|
|
101
|
+
local branch_name="$1"
|
|
102
|
+
local change_slug change_dir specs_dir capability_slug branch_base
|
|
103
|
+
local missing_workspace=0
|
|
104
|
+
local openspec_script="scripts/openspec/init-change-workspace.sh"
|
|
105
|
+
|
|
106
|
+
branch_base="$(git config --get "branch.${branch_name}.guardexBase" || true)"
|
|
107
|
+
if [[ "$branch_base" == agent/* ]]; then
|
|
108
|
+
echo "[agent-openspec-guard] Skipping OpenSpec change workspace bootstrap for helper branch '${branch_name}' (base '${branch_base}')."
|
|
109
|
+
return 0
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
change_slug="$(sanitize_slug "${branch_name//\//-}" "change")"
|
|
113
|
+
change_dir="openspec/changes/${change_slug}"
|
|
114
|
+
specs_dir="${change_dir}/specs"
|
|
115
|
+
|
|
116
|
+
if [[ ! -f "${change_dir}/.openspec.yaml" || ! -f "${change_dir}/proposal.md" || ! -f "${change_dir}/tasks.md" ]]; then
|
|
117
|
+
missing_workspace=1
|
|
118
|
+
elif [[ ! -d "$specs_dir" ]] || ! find "$specs_dir" -mindepth 2 -maxdepth 2 -type f -name spec.md | grep -q .; then
|
|
119
|
+
missing_workspace=1
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
if [[ "$missing_workspace" -ne 1 ]]; then
|
|
123
|
+
return 0
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
if [[ ! -f "$openspec_script" ]]; then
|
|
127
|
+
cat >&2 <<MSG
|
|
128
|
+
[agent-openspec-guard] Missing OpenSpec change workspace for '${branch_name}'.
|
|
129
|
+
Expected path:
|
|
130
|
+
${change_dir}
|
|
131
|
+
Cannot auto-initialize because '${openspec_script}' is missing.
|
|
132
|
+
Run:
|
|
133
|
+
gx setup --target "$(git rev-parse --show-toplevel)"
|
|
134
|
+
bash scripts/openspec/init-change-workspace.sh "${change_slug}" "<capability-slug>"
|
|
135
|
+
MSG
|
|
136
|
+
exit 1
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
if [[ ! -x "$openspec_script" ]]; then
|
|
140
|
+
chmod +x "$openspec_script" 2>/dev/null || true
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
capability_slug="$(sanitize_slug "${branch_name##*/}" "general-behavior")"
|
|
144
|
+
init_output=""
|
|
145
|
+
if ! init_output="$(bash "$openspec_script" "$change_slug" "$capability_slug" 2>&1)"; then
|
|
146
|
+
printf '%s\n' "$init_output" >&2
|
|
147
|
+
cat >&2 <<MSG
|
|
148
|
+
[agent-openspec-guard] OpenSpec auto-init failed for '${branch_name}'.
|
|
149
|
+
Run manually:
|
|
150
|
+
bash scripts/openspec/init-change-workspace.sh "${change_slug}" "${capability_slug}"
|
|
151
|
+
MSG
|
|
152
|
+
exit 1
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
if [[ -n "$init_output" ]]; then
|
|
156
|
+
printf '%s\n' "$init_output"
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
git add "$change_dir"
|
|
160
|
+
|
|
161
|
+
if [[ -x scripts/agent-file-locks.py ]]; then
|
|
162
|
+
staged_openspec="$(git diff --cached --name-only -- "$change_dir" | sed '/^$/d' || true)"
|
|
163
|
+
if [[ -n "$staged_openspec" ]]; then
|
|
164
|
+
mapfile -t openspec_files < <(printf '%s\n' "$staged_openspec")
|
|
165
|
+
python3 scripts/agent-file-locks.py claim --branch "$branch_name" "${openspec_files[@]}" >/dev/null 2>&1 || true
|
|
166
|
+
fi
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
echo "[agent-openspec-guard] Bootstrapped OpenSpec change workspace: ${change_dir}"
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
should_auto_reroute_protected_branch() {
|
|
173
|
+
local raw="${GUARDEX_AUTO_REROUTE_PROTECTED_BRANCH:-$(git config --get multiagent.autoRerouteProtectedBranch || true)}"
|
|
174
|
+
local lowered=""
|
|
175
|
+
if [[ -z "$raw" ]]; then
|
|
176
|
+
raw="true"
|
|
177
|
+
fi
|
|
178
|
+
lowered="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
|
|
179
|
+
case "$lowered" in
|
|
180
|
+
1|true|yes|on) return 0 ;;
|
|
181
|
+
*) return 1 ;;
|
|
182
|
+
esac
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
auto_reroute_protected_branch_commit() {
|
|
186
|
+
local branch_name="$1"
|
|
187
|
+
local starter_script="scripts/agent-branch-start.sh"
|
|
188
|
+
local task_name="${GUARDEX_AUTO_REROUTE_TASK_NAME:-protected-branch-commit-reroute}"
|
|
189
|
+
local agent_name="${GUARDEX_AUTO_REROUTE_AGENT_NAME:-auto-reroute}"
|
|
190
|
+
local changed_paths=""
|
|
191
|
+
local start_output=""
|
|
192
|
+
local start_status=0
|
|
193
|
+
local new_branch=""
|
|
194
|
+
local worktree_path=""
|
|
195
|
+
|
|
196
|
+
changed_paths="$({
|
|
197
|
+
git diff --name-only
|
|
198
|
+
git diff --cached --name-only
|
|
199
|
+
git ls-files --others --exclude-standard
|
|
200
|
+
} | sed '/^$/d' | sort -u)"
|
|
201
|
+
|
|
202
|
+
if [[ -z "$changed_paths" ]]; then
|
|
203
|
+
return 1
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
if [[ ! -x "$starter_script" ]]; then
|
|
207
|
+
return 1
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
set +e
|
|
211
|
+
start_output="$(bash "$starter_script" "$task_name" "$agent_name" "$branch_name" 2>&1)"
|
|
212
|
+
start_status=$?
|
|
213
|
+
set -e
|
|
214
|
+
|
|
215
|
+
if [[ "$start_status" -ne 0 ]]; then
|
|
216
|
+
printf '%s\n' "$start_output" >&2
|
|
217
|
+
return 1
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
new_branch="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Created branch: //p' | tail -n 1)"
|
|
221
|
+
worktree_path="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | tail -n 1)"
|
|
222
|
+
|
|
223
|
+
printf '%s\n' "$start_output" >&2
|
|
224
|
+
cat >&2 <<MSG
|
|
225
|
+
[agent-branch-guard] Protected-branch commit rerouted automatically.
|
|
226
|
+
Changes from '${branch_name}' were moved to:
|
|
227
|
+
branch: ${new_branch:-<see output>}
|
|
228
|
+
worktree: ${worktree_path:-<see output>}
|
|
229
|
+
Continue work and commit from that agent worktree.
|
|
230
|
+
MSG
|
|
231
|
+
return 0
|
|
232
|
+
}
|
|
233
|
+
|
|
71
234
|
is_codex_managed_only_commit_on_protected=0
|
|
72
235
|
if [[ "$is_codex_session" == "1" && "$is_protected_branch" == "1" ]]; then
|
|
73
236
|
deleted_paths="$(git diff --cached --name-only --diff-filter=D)"
|
|
@@ -86,7 +249,7 @@ if [[ "$is_codex_session" == "1" && "$is_protected_branch" == "1" ]]; then
|
|
|
86
249
|
fi
|
|
87
250
|
fi
|
|
88
251
|
|
|
89
|
-
if [[ "$should_require_codex_agent_branch" == "1" && "${
|
|
252
|
+
if [[ "$should_require_codex_agent_branch" == "1" && "${GUARDEX_ALLOW_CODEX_ON_NON_AGENT:-0}" != "1" ]]; then
|
|
90
253
|
if [[ "$is_codex_session" == "1" && "$branch" != agent/* ]]; then
|
|
91
254
|
if [[ "$is_protected_branch" == "1" ]]; then
|
|
92
255
|
if [[ "$is_codex_managed_only_commit_on_protected" == "1" ]]; then
|
|
@@ -103,7 +266,7 @@ Or manually:
|
|
|
103
266
|
Then commit from the created agent/* branch.
|
|
104
267
|
|
|
105
268
|
Temporary bypass (not recommended):
|
|
106
|
-
|
|
269
|
+
GUARDEX_ALLOW_CODEX_ON_NON_AGENT=1 git commit ...
|
|
107
270
|
MSG
|
|
108
271
|
exit 1
|
|
109
272
|
fi
|
|
@@ -115,7 +278,7 @@ Use isolated branch/worktree first:
|
|
|
115
278
|
Then commit from the created agent/* branch.
|
|
116
279
|
|
|
117
280
|
Temporary bypass (not recommended):
|
|
118
|
-
|
|
281
|
+
GUARDEX_ALLOW_CODEX_ON_NON_AGENT=1 git commit ...
|
|
119
282
|
Disable this rule for a repo (not recommended):
|
|
120
283
|
git config multiagent.codexRequireAgentBranch false
|
|
121
284
|
MSG
|
|
@@ -123,17 +286,32 @@ MSG
|
|
|
123
286
|
fi
|
|
124
287
|
fi
|
|
125
288
|
|
|
126
|
-
if [[ "$
|
|
127
|
-
if [[ "$
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
289
|
+
if [[ "$is_codex_session" == "1" && "$branch" == agent/* ]]; then
|
|
290
|
+
if [[ "$is_linked_worktree" != "1" && "${GUARDEX_ALLOW_CODEX_ON_PRIMARY_WORKTREE:-0}" != "1" ]]; then
|
|
291
|
+
cat >&2 <<'MSG'
|
|
292
|
+
[codex-worktree-guard] Codex agent commits are blocked from the primary checkout.
|
|
293
|
+
Use a linked agent worktree for agent/* branches:
|
|
294
|
+
bash scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"
|
|
295
|
+
Then commit from the printed worktree path.
|
|
296
|
+
|
|
297
|
+
Temporary bypass (not recommended):
|
|
298
|
+
GUARDEX_ALLOW_CODEX_ON_PRIMARY_WORKTREE=1 git commit ...
|
|
299
|
+
MSG
|
|
300
|
+
exit 1
|
|
131
301
|
fi
|
|
302
|
+
fi
|
|
132
303
|
|
|
133
|
-
|
|
304
|
+
if [[ "$is_protected_branch" == "1" ]]; then
|
|
305
|
+
if [[ "$is_codex_session" != "1" && "$is_vscode_git_context" == "1" && "$allow_vscode_protected_branch_writes" == "1" ]]; then
|
|
134
306
|
exit 0
|
|
135
307
|
fi
|
|
136
308
|
|
|
309
|
+
if should_auto_reroute_protected_branch; then
|
|
310
|
+
if auto_reroute_protected_branch_commit "$branch"; then
|
|
311
|
+
exit 1
|
|
312
|
+
fi
|
|
313
|
+
fi
|
|
314
|
+
|
|
137
315
|
git_dir="$(git rev-parse --git-dir)"
|
|
138
316
|
if [[ -f "$git_dir/MERGE_HEAD" ]]; then
|
|
139
317
|
exit 0
|
|
@@ -145,8 +323,10 @@ Use an agent branch first:
|
|
|
145
323
|
bash scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"
|
|
146
324
|
After finishing work:
|
|
147
325
|
bash scripts/agent-branch-finish.sh
|
|
326
|
+
Auto-reroute can be disabled (not recommended):
|
|
327
|
+
GUARDEX_AUTO_REROUTE_PROTECTED_BRANCH=0 git commit ...
|
|
148
328
|
|
|
149
|
-
Optional repo
|
|
329
|
+
Optional repo override for manual VS Code protected-branch commits:
|
|
150
330
|
git config multiagent.allowVscodeProtectedBranchWrites true
|
|
151
331
|
|
|
152
332
|
Temporary bypass (not recommended):
|
|
@@ -155,26 +335,12 @@ MSG
|
|
|
155
335
|
exit 1
|
|
156
336
|
fi
|
|
157
337
|
|
|
158
|
-
if [[ "$is_agent_context" == "1" && "$branch" != agent/* ]]; then
|
|
159
|
-
cat >&2 <<'MSG'
|
|
160
|
-
[agent-branch-guard] Agent commits must run on dedicated agent/* branches.
|
|
161
|
-
Start an agent branch first:
|
|
162
|
-
bash scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"
|
|
163
|
-
Then commit on that branch.
|
|
164
|
-
|
|
165
|
-
Temporary bypass (not recommended):
|
|
166
|
-
ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
|
|
167
|
-
MSG
|
|
168
|
-
exit 1
|
|
169
|
-
fi
|
|
170
|
-
|
|
171
338
|
if [[ "$branch" == agent/* ]]; then
|
|
172
|
-
if
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
done < <(git diff --cached --name-only --diff-filter=ACMRDTUXB)
|
|
339
|
+
if is_helper_agent_branch "$branch"; then
|
|
340
|
+
helper_base="$(resolve_agent_branch_base "$branch")"
|
|
341
|
+
echo "[agent-openspec-guard] Skipping OpenSpec change workspace bootstrap for helper branch '${branch}' (base '${helper_base}')."
|
|
342
|
+
else
|
|
343
|
+
ensure_agent_branch_openspec_workspace "$branch"
|
|
178
344
|
fi
|
|
179
345
|
|
|
180
346
|
if ! python3 scripts/agent-file-locks.py validate --branch "$branch" --staged; then
|
|
@@ -10,7 +10,7 @@ if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n
|
|
|
10
10
|
is_vscode_git_context=1
|
|
11
11
|
fi
|
|
12
12
|
|
|
13
|
-
allow_vscode_protected_raw="${
|
|
13
|
+
allow_vscode_protected_raw="${GUARDEX_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
|
|
14
14
|
if [[ -z "$allow_vscode_protected_raw" ]]; then
|
|
15
15
|
allow_vscode_protected_raw="false"
|
|
16
16
|
fi
|
|
@@ -28,7 +28,7 @@ if [[ -n "${CODEX_THREAD_ID:-}" || -n "${OMX_SESSION_ID:-}" || "${CODEX_CI:-0}"
|
|
|
28
28
|
is_codex_session=1
|
|
29
29
|
fi
|
|
30
30
|
|
|
31
|
-
protected_branches_raw="${
|
|
31
|
+
protected_branches_raw="${GUARDEX_PROTECTED_BRANCHES:-$(git config --get multiagent.protectedBranches || true)}"
|
|
32
32
|
if [[ -z "$protected_branches_raw" ]]; then
|
|
33
33
|
protected_branches_raw="dev main master"
|
|
34
34
|
fi
|