diff --git a/AGENTS.md b/AGENTS.md index 1d41226..b495f6e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -137,8 +137,10 @@ per-branch plan workspace automatically under: openspec/plan// ``` -For manual `scripts/agent-branch-start.sh` usage, enable auto-bootstrap with -`MUSAFETY_OPENSPEC_AUTO_INIT=true` or scaffold manually before implementation: +For manual `scripts/agent-branch-start.sh` usage, OpenSpec auto-bootstrap is +enabled by default. Set `MUSAFETY_OPENSPEC_AUTO_INIT=false` only when you +intentionally need to skip scaffold generation, or scaffold manually before +implementation: ```bash bash scripts/openspec/init-plan-workspace.sh "" diff --git a/README.md b/README.md index e72b12b..ca616b3 100644 --- a/README.md +++ b/README.md @@ -342,10 +342,12 @@ openspec update ### OpenSpec in agent sub-branches -- `scripts/codex-agent.sh` enforces an OpenSpec workspace before it launches Codex in each sandbox branch/worktree. -- `scripts/agent-branch-start.sh` can also scaffold `openspec/plan//` when you set `MUSAFETY_OPENSPEC_AUTO_INIT=true`. -- Set `MUSAFETY_OPENSPEC_AUTO_INIT=false` (default for `agent-branch-start`) to skip branch-start auto-bootstrap. +- `scripts/codex-agent.sh` enforces OpenSpec workspaces before it launches Codex in each sandbox branch/worktree. +- `scripts/agent-branch-start.sh` scaffolds both `openspec/changes//` and `openspec/plan//` by default. +- Set `MUSAFETY_OPENSPEC_AUTO_INIT=false` only when you intentionally need to skip branch-start auto-bootstrap. - Set `MUSAFETY_OPENSPEC_PLAN_SLUG=` to force a specific plan workspace name. +- Set `MUSAFETY_OPENSPEC_CHANGE_SLUG=` to force a specific change workspace name. +- Set `MUSAFETY_OPENSPEC_CAPABILITY_SLUG=` to override the default capability folder used for `spec.md` scaffolding. ## Security and maintenance posture diff --git a/scripts/agent-branch-start.sh b/scripts/agent-branch-start.sh index 4445d10..fa192c8 100755 --- a/scripts/agent-branch-start.sh +++ b/scripts/agent-branch-start.sh @@ -6,8 +6,10 @@ AGENT_NAME="agent" BASE_BRANCH="" BASE_BRANCH_EXPLICIT=0 WORKTREE_ROOT_REL=".omx/agent-worktrees" -OPENSPEC_AUTO_INIT_RAW="${MUSAFETY_OPENSPEC_AUTO_INIT:-false}" +OPENSPEC_AUTO_INIT_RAW="${MUSAFETY_OPENSPEC_AUTO_INIT:-true}" OPENSPEC_PLAN_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_PLAN_SLUG:-}" +OPENSPEC_CHANGE_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_CHANGE_SLUG:-}" +OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_CAPABILITY_SLUG:-}" POSITIONAL_ARGS=() while [[ $# -gt 0 ]]; do @@ -109,6 +111,25 @@ resolve_openspec_plan_slug() { sanitize_slug "${branch_name//\//-}" "$task_slug" } +resolve_openspec_change_slug() { + local branch_name="$1" + local task_slug="$2" + if [[ -n "$OPENSPEC_CHANGE_SLUG_OVERRIDE" ]]; then + sanitize_slug "$OPENSPEC_CHANGE_SLUG_OVERRIDE" "$task_slug" + return 0 + fi + sanitize_slug "${branch_name//\//-}" "$task_slug" +} + +resolve_openspec_capability_slug() { + local task_slug="$1" + if [[ -n "$OPENSPEC_CAPABILITY_SLUG_OVERRIDE" ]]; then + sanitize_slug "$OPENSPEC_CAPABILITY_SLUG_OVERRIDE" "$task_slug" + return 0 + fi + sanitize_slug "$task_slug" "general-behavior" +} + resolve_active_codex_snapshot_name() { local override="${MUSAFETY_CODEX_AUTH_SNAPSHOT:-}" if [[ -n "$override" ]]; then @@ -193,6 +214,33 @@ hydrate_local_helper_in_worktree() { echo "[agent-branch-start] Hydrated local helper in worktree: ${relative_path}" } +resolve_local_helper_script_path() { + local repo="$1" + local worktree="$2" + local relative_path="$3" + local candidate + + candidate="${worktree}/${relative_path}" + if [[ -f "$candidate" ]]; then + printf '%s' "$candidate" + return 0 + fi + + candidate="${repo}/${relative_path}" + if [[ -f "$candidate" ]]; then + printf '%s' "$candidate" + return 0 + fi + + candidate="${repo}/templates/${relative_path}" + if [[ -f "$candidate" ]]; then + printf '%s' "$candidate" + return 0 + fi + + return 1 +} + hydrate_dependency_dir_symlink_in_worktree() { local repo="$1" local worktree="$2" @@ -218,26 +266,21 @@ initialize_openspec_plan_workspace() { local worktree="$2" local plan_slug="$3" - hydrate_local_helper_in_worktree "$repo" "$worktree" "scripts/openspec/init-plan-workspace.sh" - if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then return 0 fi - local openspec_script="${worktree}/scripts/openspec/init-plan-workspace.sh" - if [[ ! -f "$openspec_script" ]]; then + local openspec_script + if ! openspec_script="$(resolve_local_helper_script_path "$repo" "$worktree" "scripts/openspec/init-plan-workspace.sh")"; then echo "[agent-branch-start] OpenSpec init script is missing in sandbox worktree." >&2 echo "[agent-branch-start] Run 'gx setup --target \"$repo\"' to repair templates, then retry." >&2 return 1 fi - if [[ ! -x "$openspec_script" ]]; then - chmod +x "$openspec_script" 2>/dev/null || true - fi local init_output="" if ! init_output="$( cd "$worktree" - bash "scripts/openspec/init-plan-workspace.sh" "$plan_slug" 2>&1 + bash "$openspec_script" "$plan_slug" 2>&1 )"; then printf '%s\n' "$init_output" >&2 echo "[agent-branch-start] OpenSpec workspace initialization failed for plan '${plan_slug}'." >&2 @@ -250,6 +293,38 @@ initialize_openspec_plan_workspace() { echo "[agent-branch-start] OpenSpec plan workspace: ${worktree}/openspec/plan/${plan_slug}" } +initialize_openspec_change_workspace() { + local repo="$1" + local worktree="$2" + local change_slug="$3" + local capability_slug="$4" + + if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then + return 0 + fi + + local openspec_script + if ! openspec_script="$(resolve_local_helper_script_path "$repo" "$worktree" "scripts/openspec/init-change-workspace.sh")"; then + echo "[agent-branch-start] OpenSpec change init script is missing in sandbox worktree." >&2 + echo "[agent-branch-start] Run 'gx setup --target \"$repo\"' to repair templates, then retry." >&2 + return 1 + fi + + local init_output="" + if ! init_output="$( + cd "$worktree" + bash "$openspec_script" "$change_slug" "$capability_slug" 2>&1 + )"; then + printf '%s\n' "$init_output" >&2 + echo "[agent-branch-start] OpenSpec workspace initialization failed for change '${change_slug}'." >&2 + return 1 + fi + + if [[ -n "$init_output" ]]; then + printf '%s\n' "$init_output" + fi + echo "[agent-branch-start] OpenSpec change workspace: ${worktree}/openspec/changes/${change_slug}" +} if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then echo "[agent-branch-start] Not inside a git repository." >&2 exit 1 @@ -312,6 +387,8 @@ worktree_root="${repo_root}/${WORKTREE_ROOT_REL}" mkdir -p "$worktree_root" worktree_path="${worktree_root}/${branch_name//\//__}" openspec_plan_slug="$(resolve_openspec_plan_slug "$branch_name" "$task_slug")" +openspec_change_slug="$(resolve_openspec_change_slug "$branch_name" "$task_slug")" +openspec_capability_slug="$(resolve_openspec_capability_slug "$task_slug")" if [[ -e "$worktree_path" ]]; then echo "[agent-branch-start] Worktree path already exists: ${worktree_path}" >&2 @@ -394,12 +471,16 @@ hydrate_local_helper_in_worktree "$repo_root" "$worktree_path" "scripts/codex-ag hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "node_modules" hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/frontend/node_modules" hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/backend/node_modules" +if ! initialize_openspec_change_workspace "$repo_root" "$worktree_path" "$openspec_change_slug" "$openspec_capability_slug"; then + exit 1 +fi if ! initialize_openspec_plan_workspace "$repo_root" "$worktree_path" "$openspec_plan_slug"; then exit 1 fi echo "[agent-branch-start] Created branch: ${branch_name}" echo "[agent-branch-start] Worktree: ${worktree_path}" +echo "[agent-branch-start] OpenSpec change: openspec/changes/${openspec_change_slug}" echo "[agent-branch-start] OpenSpec plan: openspec/plan/${openspec_plan_slug}" echo "[agent-branch-start] Next steps:" echo " cd \"${worktree_path}\"" diff --git a/scripts/codex-agent.sh b/scripts/codex-agent.sh index a4f734d..37b629a 100755 --- a/scripts/codex-agent.sh +++ b/scripts/codex-agent.sh @@ -12,6 +12,8 @@ AUTO_CLEANUP_RAW="${MUSAFETY_CODEX_AUTO_CLEANUP:-true}" AUTO_WAIT_FOR_MERGE_RAW="${MUSAFETY_CODEX_WAIT_FOR_MERGE:-true}" OPENSPEC_AUTO_INIT_RAW="${MUSAFETY_OPENSPEC_AUTO_INIT:-true}" OPENSPEC_PLAN_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_PLAN_SLUG:-}" +OPENSPEC_CHANGE_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_CHANGE_SLUG:-}" +OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_CAPABILITY_SLUG:-}" normalize_bool() { local raw="${1:-}" @@ -150,6 +152,27 @@ resolve_openspec_plan_slug() { sanitize_slug "${branch_name//\//-}" "$task_slug" } +resolve_openspec_change_slug() { + local branch_name="$1" + local task_slug + task_slug="$(sanitize_slug "$TASK_NAME" "task")" + if [[ -n "$OPENSPEC_CHANGE_SLUG_OVERRIDE" ]]; then + sanitize_slug "$OPENSPEC_CHANGE_SLUG_OVERRIDE" "$task_slug" + return 0 + fi + sanitize_slug "${branch_name//\//-}" "$task_slug" +} + +resolve_openspec_capability_slug() { + local task_slug + task_slug="$(sanitize_slug "$TASK_NAME" "task")" + if [[ -n "$OPENSPEC_CAPABILITY_SLUG_OVERRIDE" ]]; then + sanitize_slug "$OPENSPEC_CAPABILITY_SLUG_OVERRIDE" "$task_slug" + return 0 + fi + sanitize_slug "$task_slug" "general-behavior" +} + hydrate_local_helper_in_worktree() { local worktree="$1" local relative_path="$2" @@ -179,6 +202,32 @@ hydrate_local_helper_in_worktree() { echo "[codex-agent] Hydrated local helper in sandbox: ${relative_path}" } +resolve_local_helper_script_path() { + local worktree="$1" + local relative_path="$2" + local candidate="" + + candidate="${worktree}/${relative_path}" + if [[ -f "$candidate" ]]; then + printf '%s' "$candidate" + return 0 + fi + + candidate="${repo_root}/${relative_path}" + if [[ -f "$candidate" ]]; then + printf '%s' "$candidate" + return 0 + fi + + candidate="${repo_root}/templates/${relative_path}" + if [[ -f "$candidate" ]]; then + printf '%s' "$candidate" + return 0 + fi + + return 1 +} + resolve_start_base_branch() { if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then printf '%s' "$BASE_BRANCH" @@ -397,24 +446,19 @@ ensure_openspec_plan_workspace() { return 0 fi - hydrate_local_helper_in_worktree "$wt" "scripts/openspec/init-plan-workspace.sh" - - local openspec_script="${wt}/scripts/openspec/init-plan-workspace.sh" - if [[ ! -f "$openspec_script" ]]; then + local openspec_script + if ! openspec_script="$(resolve_local_helper_script_path "$wt" "scripts/openspec/init-plan-workspace.sh")"; then echo "[codex-agent] Missing OpenSpec init script in sandbox: ${openspec_script}" >&2 echo "[codex-agent] Run 'gx setup --target ${repo_root}' and retry." >&2 return 1 fi - if [[ ! -x "$openspec_script" ]]; then - chmod +x "$openspec_script" 2>/dev/null || true - fi local plan_slug plan_slug="$(resolve_openspec_plan_slug "$branch")" local init_output="" if ! init_output="$( cd "$wt" - bash "scripts/openspec/init-plan-workspace.sh" "$plan_slug" 2>&1 + bash "$openspec_script" "$plan_slug" 2>&1 )"; then printf '%s\n' "$init_output" >&2 echo "[codex-agent] OpenSpec workspace initialization failed for plan '${plan_slug}'." >&2 @@ -426,6 +470,37 @@ ensure_openspec_plan_workspace() { echo "[codex-agent] OpenSpec plan workspace: ${wt}/openspec/plan/${plan_slug}" } +ensure_openspec_change_workspace() { + local wt="$1" + local branch="$2" + + if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then + return 0 + fi + + local openspec_script + if ! openspec_script="$(resolve_local_helper_script_path "$wt" "scripts/openspec/init-change-workspace.sh")"; then + echo "[codex-agent] Missing OpenSpec change init script in sandbox: ${openspec_script}" >&2 + echo "[codex-agent] Run 'gx setup --target ${repo_root}' and retry." >&2 + return 1 + fi + + local change_slug capability_slug init_output="" + change_slug="$(resolve_openspec_change_slug "$branch")" + capability_slug="$(resolve_openspec_capability_slug)" + if ! init_output="$( + cd "$wt" + bash "$openspec_script" "$change_slug" "$capability_slug" 2>&1 + )"; then + printf '%s\n' "$init_output" >&2 + echo "[codex-agent] OpenSpec workspace initialization failed for change '${change_slug}'." >&2 + return 1 + fi + if [[ -n "$init_output" ]]; then + printf '%s\n' "$init_output" + fi + echo "[codex-agent] OpenSpec change workspace: ${wt}/openspec/changes/${change_slug}" +} worktree_has_changes() { local wt="$1" if ! git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then @@ -656,6 +731,10 @@ if [[ -z "$worktree_branch" || "$worktree_branch" == "HEAD" ]]; then exit 1 fi +if ! ensure_openspec_change_workspace "$worktree_path" "$worktree_branch"; then + exit 1 +fi + if ! ensure_openspec_plan_workspace "$worktree_path" "$worktree_branch"; then exit 1 fi diff --git a/templates/AGENTS.multiagent-safety.md b/templates/AGENTS.multiagent-safety.md index 6c4f187..3edbf34 100644 --- a/templates/AGENTS.multiagent-safety.md +++ b/templates/AGENTS.multiagent-safety.md @@ -55,8 +55,10 @@ per-branch plan workspace automatically under: openspec/plan// ``` -For manual `scripts/agent-branch-start.sh` usage, enable auto-bootstrap with -`MUSAFETY_OPENSPEC_AUTO_INIT=true` or scaffold manually before implementation: +For manual `scripts/agent-branch-start.sh` usage, OpenSpec auto-bootstrap is +enabled by default. Set `MUSAFETY_OPENSPEC_AUTO_INIT=false` only when you +intentionally need to skip scaffold generation, or scaffold manually before +implementation: ```bash bash scripts/openspec/init-plan-workspace.sh "" diff --git a/templates/scripts/agent-branch-start.sh b/templates/scripts/agent-branch-start.sh index ffd8db8..840ff78 100755 --- a/templates/scripts/agent-branch-start.sh +++ b/templates/scripts/agent-branch-start.sh @@ -6,8 +6,10 @@ AGENT_NAME="agent" BASE_BRANCH="" BASE_BRANCH_EXPLICIT=0 WORKTREE_ROOT_REL=".omx/agent-worktrees" -OPENSPEC_AUTO_INIT_RAW="${MUSAFETY_OPENSPEC_AUTO_INIT:-false}" +OPENSPEC_AUTO_INIT_RAW="${MUSAFETY_OPENSPEC_AUTO_INIT:-true}" OPENSPEC_PLAN_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_PLAN_SLUG:-}" +OPENSPEC_CHANGE_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_CHANGE_SLUG:-}" +OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_CAPABILITY_SLUG:-}" POSITIONAL_ARGS=() while [[ $# -gt 0 ]]; do @@ -109,6 +111,25 @@ resolve_openspec_plan_slug() { sanitize_slug "${branch_name//\//-}" "$task_slug" } +resolve_openspec_change_slug() { + local branch_name="$1" + local task_slug="$2" + if [[ -n "$OPENSPEC_CHANGE_SLUG_OVERRIDE" ]]; then + sanitize_slug "$OPENSPEC_CHANGE_SLUG_OVERRIDE" "$task_slug" + return 0 + fi + sanitize_slug "${branch_name//\//-}" "$task_slug" +} + +resolve_openspec_capability_slug() { + local task_slug="$1" + if [[ -n "$OPENSPEC_CAPABILITY_SLUG_OVERRIDE" ]]; then + sanitize_slug "$OPENSPEC_CAPABILITY_SLUG_OVERRIDE" "$task_slug" + return 0 + fi + sanitize_slug "$task_slug" "general-behavior" +} + resolve_active_codex_snapshot_name() { local override="${MUSAFETY_CODEX_AUTH_SNAPSHOT:-}" if [[ -n "$override" ]]; then @@ -193,6 +214,33 @@ hydrate_local_helper_in_worktree() { echo "[agent-branch-start] Hydrated local helper in worktree: ${relative_path}" } +resolve_local_helper_script_path() { + local repo="$1" + local worktree="$2" + local relative_path="$3" + local candidate + + candidate="${worktree}/${relative_path}" + if [[ -f "$candidate" ]]; then + printf '%s' "$candidate" + return 0 + fi + + candidate="${repo}/${relative_path}" + if [[ -f "$candidate" ]]; then + printf '%s' "$candidate" + return 0 + fi + + candidate="${repo}/templates/${relative_path}" + if [[ -f "$candidate" ]]; then + printf '%s' "$candidate" + return 0 + fi + + return 1 +} + hydrate_dependency_dir_symlink_in_worktree() { local repo="$1" local worktree="$2" @@ -218,26 +266,21 @@ initialize_openspec_plan_workspace() { local worktree="$2" local plan_slug="$3" - hydrate_local_helper_in_worktree "$repo" "$worktree" "scripts/openspec/init-plan-workspace.sh" - if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then return 0 fi - local openspec_script="${worktree}/scripts/openspec/init-plan-workspace.sh" - if [[ ! -f "$openspec_script" ]]; then + local openspec_script + if ! openspec_script="$(resolve_local_helper_script_path "$repo" "$worktree" "scripts/openspec/init-plan-workspace.sh")"; then echo "[agent-branch-start] OpenSpec init script is missing in sandbox worktree." >&2 echo "[agent-branch-start] Run 'gx setup --target \"$repo\"' to repair templates, then retry." >&2 return 1 fi - if [[ ! -x "$openspec_script" ]]; then - chmod +x "$openspec_script" 2>/dev/null || true - fi local init_output="" if ! init_output="$( cd "$worktree" - bash "scripts/openspec/init-plan-workspace.sh" "$plan_slug" 2>&1 + bash "$openspec_script" "$plan_slug" 2>&1 )"; then printf '%s\n' "$init_output" >&2 echo "[agent-branch-start] OpenSpec workspace initialization failed for plan '${plan_slug}'." >&2 @@ -250,6 +293,38 @@ initialize_openspec_plan_workspace() { echo "[agent-branch-start] OpenSpec plan workspace: ${worktree}/openspec/plan/${plan_slug}" } +initialize_openspec_change_workspace() { + local repo="$1" + local worktree="$2" + local change_slug="$3" + local capability_slug="$4" + + if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then + return 0 + fi + + local openspec_script + if ! openspec_script="$(resolve_local_helper_script_path "$repo" "$worktree" "scripts/openspec/init-change-workspace.sh")"; then + echo "[agent-branch-start] OpenSpec change init script is missing in sandbox worktree." >&2 + echo "[agent-branch-start] Run 'gx setup --target \"$repo\"' to repair templates, then retry." >&2 + return 1 + fi + + local init_output="" + if ! init_output="$( + cd "$worktree" + bash "$openspec_script" "$change_slug" "$capability_slug" 2>&1 + )"; then + printf '%s\n' "$init_output" >&2 + echo "[agent-branch-start] OpenSpec workspace initialization failed for change '${change_slug}'." >&2 + return 1 + fi + + if [[ -n "$init_output" ]]; then + printf '%s\n' "$init_output" + fi + echo "[agent-branch-start] OpenSpec change workspace: ${worktree}/openspec/changes/${change_slug}" +} if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then echo "[agent-branch-start] Not inside a git repository." >&2 exit 1 @@ -312,6 +387,8 @@ worktree_root="${repo_root}/${WORKTREE_ROOT_REL}" mkdir -p "$worktree_root" worktree_path="${worktree_root}/${branch_name//\//__}" openspec_plan_slug="$(resolve_openspec_plan_slug "$branch_name" "$task_slug")" +openspec_change_slug="$(resolve_openspec_change_slug "$branch_name" "$task_slug")" +openspec_capability_slug="$(resolve_openspec_capability_slug "$task_slug")" if [[ -e "$worktree_path" ]]; then echo "[agent-branch-start] Worktree path already exists: ${worktree_path}" >&2 @@ -364,12 +441,16 @@ hydrate_local_helper_in_worktree "$repo_root" "$worktree_path" "scripts/codex-ag hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "node_modules" hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/frontend/node_modules" hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/backend/node_modules" +if ! initialize_openspec_change_workspace "$repo_root" "$worktree_path" "$openspec_change_slug" "$openspec_capability_slug"; then + exit 1 +fi if ! initialize_openspec_plan_workspace "$repo_root" "$worktree_path" "$openspec_plan_slug"; then exit 1 fi echo "[agent-branch-start] Created branch: ${branch_name}" echo "[agent-branch-start] Worktree: ${worktree_path}" +echo "[agent-branch-start] OpenSpec change: openspec/changes/${openspec_change_slug}" echo "[agent-branch-start] OpenSpec plan: openspec/plan/${openspec_plan_slug}" echo "[agent-branch-start] Next steps:" echo " cd \"${worktree_path}\"" diff --git a/templates/scripts/codex-agent.sh b/templates/scripts/codex-agent.sh index a4f734d..37b629a 100755 --- a/templates/scripts/codex-agent.sh +++ b/templates/scripts/codex-agent.sh @@ -12,6 +12,8 @@ AUTO_CLEANUP_RAW="${MUSAFETY_CODEX_AUTO_CLEANUP:-true}" AUTO_WAIT_FOR_MERGE_RAW="${MUSAFETY_CODEX_WAIT_FOR_MERGE:-true}" OPENSPEC_AUTO_INIT_RAW="${MUSAFETY_OPENSPEC_AUTO_INIT:-true}" OPENSPEC_PLAN_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_PLAN_SLUG:-}" +OPENSPEC_CHANGE_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_CHANGE_SLUG:-}" +OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_CAPABILITY_SLUG:-}" normalize_bool() { local raw="${1:-}" @@ -150,6 +152,27 @@ resolve_openspec_plan_slug() { sanitize_slug "${branch_name//\//-}" "$task_slug" } +resolve_openspec_change_slug() { + local branch_name="$1" + local task_slug + task_slug="$(sanitize_slug "$TASK_NAME" "task")" + if [[ -n "$OPENSPEC_CHANGE_SLUG_OVERRIDE" ]]; then + sanitize_slug "$OPENSPEC_CHANGE_SLUG_OVERRIDE" "$task_slug" + return 0 + fi + sanitize_slug "${branch_name//\//-}" "$task_slug" +} + +resolve_openspec_capability_slug() { + local task_slug + task_slug="$(sanitize_slug "$TASK_NAME" "task")" + if [[ -n "$OPENSPEC_CAPABILITY_SLUG_OVERRIDE" ]]; then + sanitize_slug "$OPENSPEC_CAPABILITY_SLUG_OVERRIDE" "$task_slug" + return 0 + fi + sanitize_slug "$task_slug" "general-behavior" +} + hydrate_local_helper_in_worktree() { local worktree="$1" local relative_path="$2" @@ -179,6 +202,32 @@ hydrate_local_helper_in_worktree() { echo "[codex-agent] Hydrated local helper in sandbox: ${relative_path}" } +resolve_local_helper_script_path() { + local worktree="$1" + local relative_path="$2" + local candidate="" + + candidate="${worktree}/${relative_path}" + if [[ -f "$candidate" ]]; then + printf '%s' "$candidate" + return 0 + fi + + candidate="${repo_root}/${relative_path}" + if [[ -f "$candidate" ]]; then + printf '%s' "$candidate" + return 0 + fi + + candidate="${repo_root}/templates/${relative_path}" + if [[ -f "$candidate" ]]; then + printf '%s' "$candidate" + return 0 + fi + + return 1 +} + resolve_start_base_branch() { if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then printf '%s' "$BASE_BRANCH" @@ -397,24 +446,19 @@ ensure_openspec_plan_workspace() { return 0 fi - hydrate_local_helper_in_worktree "$wt" "scripts/openspec/init-plan-workspace.sh" - - local openspec_script="${wt}/scripts/openspec/init-plan-workspace.sh" - if [[ ! -f "$openspec_script" ]]; then + local openspec_script + if ! openspec_script="$(resolve_local_helper_script_path "$wt" "scripts/openspec/init-plan-workspace.sh")"; then echo "[codex-agent] Missing OpenSpec init script in sandbox: ${openspec_script}" >&2 echo "[codex-agent] Run 'gx setup --target ${repo_root}' and retry." >&2 return 1 fi - if [[ ! -x "$openspec_script" ]]; then - chmod +x "$openspec_script" 2>/dev/null || true - fi local plan_slug plan_slug="$(resolve_openspec_plan_slug "$branch")" local init_output="" if ! init_output="$( cd "$wt" - bash "scripts/openspec/init-plan-workspace.sh" "$plan_slug" 2>&1 + bash "$openspec_script" "$plan_slug" 2>&1 )"; then printf '%s\n' "$init_output" >&2 echo "[codex-agent] OpenSpec workspace initialization failed for plan '${plan_slug}'." >&2 @@ -426,6 +470,37 @@ ensure_openspec_plan_workspace() { echo "[codex-agent] OpenSpec plan workspace: ${wt}/openspec/plan/${plan_slug}" } +ensure_openspec_change_workspace() { + local wt="$1" + local branch="$2" + + if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then + return 0 + fi + + local openspec_script + if ! openspec_script="$(resolve_local_helper_script_path "$wt" "scripts/openspec/init-change-workspace.sh")"; then + echo "[codex-agent] Missing OpenSpec change init script in sandbox: ${openspec_script}" >&2 + echo "[codex-agent] Run 'gx setup --target ${repo_root}' and retry." >&2 + return 1 + fi + + local change_slug capability_slug init_output="" + change_slug="$(resolve_openspec_change_slug "$branch")" + capability_slug="$(resolve_openspec_capability_slug)" + if ! init_output="$( + cd "$wt" + bash "$openspec_script" "$change_slug" "$capability_slug" 2>&1 + )"; then + printf '%s\n' "$init_output" >&2 + echo "[codex-agent] OpenSpec workspace initialization failed for change '${change_slug}'." >&2 + return 1 + fi + if [[ -n "$init_output" ]]; then + printf '%s\n' "$init_output" + fi + echo "[codex-agent] OpenSpec change workspace: ${wt}/openspec/changes/${change_slug}" +} worktree_has_changes() { local wt="$1" if ! git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then @@ -656,6 +731,10 @@ if [[ -z "$worktree_branch" || "$worktree_branch" == "HEAD" ]]; then exit 1 fi +if ! ensure_openspec_change_workspace "$worktree_path" "$worktree_branch"; then + exit 1 +fi + if ! ensure_openspec_plan_workspace "$worktree_path" "$worktree_branch"; then exit 1 fi diff --git a/test/install.test.js b/test/install.test.js index fb3b342..05f68e7 100644 --- a/test/install.test.js +++ b/test/install.test.js @@ -980,7 +980,7 @@ test('setup agent-branch-start supports explicit snapshot override without codex assert.match(result.stdout, /Created branch: agent\/bot\/prod-snapshot-one-ship-fix(?:-\d+)?/); }); -test('setup agent-branch-start supports optional OpenSpec auto-bootstrap toggles', () => { +test('setup agent-branch-start bootstraps OpenSpec by default and supports disable toggle', () => { const repoDir = initRepo(); let result = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir); @@ -991,7 +991,6 @@ test('setup agent-branch-start supports optional OpenSpec auto-bootstrap toggles 'bash', ['scripts/agent-branch-start.sh', 'openspec-default', 'bot', 'dev'], repoDir, - { env: { MUSAFETY_OPENSPEC_AUTO_INIT: 'true' } }, ); assert.equal(result.status, 0, result.stderr || result.stdout); const defaultBranch = extractCreatedBranch(result.stdout); @@ -1020,6 +1019,59 @@ test('setup agent-branch-start supports optional OpenSpec auto-bootstrap toggles ); }); +test('agent-branch-start scaffolds OpenSpec from local helpers without copying helper scripts into legacy-base worktrees', () => { + const repoDir = initRepo(); + + let result = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + seedCommit(repoDir); + + result = runCmd('git', ['checkout', '-b', 'legacy-openspec-base'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + result = runCmd( + 'git', + ['rm', 'scripts/openspec/init-plan-workspace.sh', 'scripts/openspec/init-change-workspace.sh'], + repoDir, + ); + assert.equal(result.status, 0, result.stderr || result.stdout); + result = runCmd('git', ['commit', '-m', 'legacy base without openspec helper scripts'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + + result = runCmd('git', ['checkout', 'dev'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + + result = runCmd( + 'bash', + ['scripts/agent-branch-start.sh', 'legacy-bootstrap', 'bot', 'legacy-openspec-base'], + repoDir, + ); + assert.equal(result.status, 0, result.stderr || result.stdout); + + const createdWorktree = extractCreatedWorktree(result.stdout); + const createdPlanSlug = extractOpenSpecPlanSlug(result.stdout); + const createdChangeSlug = extractOpenSpecChangeSlug(result.stdout); + assert.equal( + fs.existsSync(path.join(createdWorktree, 'openspec', 'plan', createdPlanSlug, 'summary.md')), + true, + 'branch start should scaffold plan workspace even when base branch lacks helper scripts', + ); + assert.equal( + fs.existsSync(path.join(createdWorktree, 'openspec', 'changes', createdChangeSlug, 'proposal.md')), + true, + 'branch start should scaffold change workspace even when base branch lacks helper scripts', + ); + assert.equal( + fs.existsSync(path.join(createdWorktree, 'scripts', 'openspec', 'init-plan-workspace.sh')), + false, + 'branch start should not copy init-plan helper into sandbox branch when missing in base', + ); + assert.equal( + fs.existsSync(path.join(createdWorktree, 'scripts', 'openspec', 'init-change-workspace.sh')), + false, + 'branch start should not copy init-change helper into sandbox branch when missing in base', + ); +}); + test('setup agent-branch-start defaults base to current branch and stores per-branch base metadata', () => { const repoDir = initRepoOnBranch('main'); seedCommit(repoDir);