@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 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 `MUSAFETY_OPENSPEC_AUTO_INIT=true`.
353
- - Set `MUSAFETY_OPENSPEC_AUTO_INIT=false` (default for `agent-branch-start`) to skip branch-start auto-bootstrap.
354
- - Set `MUSAFETY_OPENSPEC_PLAN_SLUG=<kebab-case-slug>` to force a specific plan workspace name.
355
- - Set `MUSAFETY_OPENSPEC_CHANGE_SLUG=<kebab-case-slug>` to force a specific change workspace name.
356
- - Set `MUSAFETY_OPENSPEC_CAPABILITY_SLUG=<kebab-case-slug>` to override the default capability folder used for `spec.md` scaffolding.
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 `MUSAFETY_OPENSPEC_AUTO_INIT` plus `MUSAFETY_OPENSPEC_PLAN_SLUG`.
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
 
@@ -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 = ['musafety', 'multiagent-safety'];
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.MUSAFETY_GH_BIN || 'gh';
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.MUSAFETY_RELEASE_REPO || '/tmp/multiagent-safety',
29
+ process.env.GUARDEX_RELEASE_REPO || '/tmp/multiagent-safety',
30
30
  );
31
- const NPM_BIN = process.env.MUSAFETY_NPM_BIN || 'npm';
32
- const OPENSPEC_BIN = process.env.MUSAFETY_OPENSPEC_BIN || 'openspec';
33
- const SCORECARD_BIN = process.env.MUSAFETY_SCORECARD_BIN || 'scorecard';
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.MUSAFETY_GH_BIN || '').trim());
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.MUSAFETY_GH_BIN || 'gh';
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.MUSAFETY_FINISH_WAIT_TIMEOUT_SECONDS || '1800', 10);
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.MUSAFETY_DOCTOR_SANDBOX || '') === '1') {
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.MUSAFETY_SKIP_AUTO_FINISH_READY_BRANCHES || '') === '1') {
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 (MUSAFETY_SKIP_AUTO_FINISH_READY_BRANCHES=1).');
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.MUSAFETY_GH_BIN || '').trim());
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.MUSAFETY_GH_BIN || 'gh';
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 checkForMusafetyUpdate() {
3155
- if (envFlagEnabled('MUSAFETY_SKIP_UPDATE_CHECK')) {
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('MUSAFETY_FORCE_UPDATE_CHECK');
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 = checkForMusafetyUpdate();
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('MUSAFETY_AUTO_UPDATE_APPROVAL');
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('MUSAFETY_SKIP_OPENSPEC_UPDATE_CHECK')) {
3227
+ if (envFlagEnabled('GUARDEX_SKIP_OPENSPEC_UPDATE_CHECK')) {
3228
3228
  return { checked: false, reason: 'disabled' };
3229
3229
  }
3230
3230
 
3231
- const forceCheck = envFlagEnabled('MUSAFETY_FORCE_OPENSPEC_UPDATE_CHECK');
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('MUSAFETY_AUTO_OPENSPEC_UPDATE_APPROVAL');
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": "5.0.17",
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
- `MUSAFETY_OPENSPEC_AUTO_INIT=true` or scaffold manually before implementation:
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
- if [[ "${MUSAFETY_DISABLE_POST_MERGE_CLEANUP:-0}" == "1" ]]; then
5
- exit 0
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="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
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="${MUSAFETY_PROTECTED_BRANCHES:-$(git config --get multiagent.protectedBranches || true)}"
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="${MUSAFETY_CODEX_REQUIRE_AGENT_BRANCH:-$(git config --get multiagent.codexRequireAgentBranch || true)}"
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" && "${MUSAFETY_ALLOW_CODEX_ON_NON_AGENT:-0}" != "1" ]]; then
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
- MUSAFETY_ALLOW_CODEX_ON_NON_AGENT=1 git commit ...
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
- MUSAFETY_ALLOW_CODEX_ON_NON_AGENT=1 git commit ...
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 [[ "$is_protected_branch" == "1" ]]; then
127
- if [[ "$is_codex_session" != "1" && "$is_vscode_git_context" == "1" ]]; then
128
- if [[ "$allow_vscode_protected_branch_writes" == "1" ]]; then
129
- exit 0
130
- fi
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
- if [[ "$is_unborn_branch" == "1" && "$is_codex_session" != "1" ]]; then
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 opt-in for VS Code protected-branch commits:
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 [[ "${MUSAFETY_AUTOCLAIM_STAGED_LOCKS:-1}" == "1" ]]; then
173
- while IFS= read -r staged_file; do
174
- [[ -z "$staged_file" ]] && continue
175
- [[ "$staged_file" == ".omx/state/agent-file-locks.json" ]] && continue
176
- python3 scripts/agent-file-locks.py claim --branch "$branch" "$staged_file" >/dev/null 2>&1 || true
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="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
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="${MUSAFETY_PROTECTED_BRANCHES:-$(git config --get multiagent.protectedBranches || true)}"
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