From de534c10184878576ac6021afc2c2c7fe82dbb42 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Tue, 21 Apr 2026 15:06:03 +0200 Subject: [PATCH] Keep shared ralplan lanes identifiable and handoff-ready Plan-backed sandboxes now add a masterplan label to the worktree path and plan slug while keeping the branch and change slug stable. The plan scaffold now creates role prompt.md files plus ownership, collaboration, and cleanup-blocking checklist sections, and codex-agent preserves the same naming in both the normal and fallback launch paths. Constraint: Joined agents must reuse the owner worktree without stealing cleanup ownership from the change lane Rejected: Prefix the git branch name itself with masterplan | branch identities stay stable while the worktree and plan workspace carry the planning label Confidence: high Scope-risk: moderate Directive: Keep agent-branch-start, codex-agent, and init-plan-workspace naming logic aligned across runtime and template copies Tested: bash -n scripts/agent-branch-start.sh templates/scripts/agent-branch-start.sh scripts/codex-agent.sh templates/scripts/codex-agent.sh scripts/openspec/init-plan-workspace.sh templates/scripts/openspec/init-plan-workspace.sh; node --test --test-name-pattern "setup agent-branch-start supports optional OpenSpec auto-bootstrap toggles|codex-agent launches codex inside a fresh sandbox worktree and keeps branch/worktree by default|codex-agent restores local branch and falls back to safe worktree start when starter script switches in-place|OpenSpec plan workspace scaffold creates expected role/task structure" test/install.test.js; node --test --test-name-pattern "critical runtime helper scripts stay in sync with templates" test/metadata.test.js; node --test --test-name-pattern "merge branches replays non-conflicting changes onto a dedicated target lane" test/merge-workflow.test.js; openspec validate agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48 --type change --strict; openspec validate --specs Not-tested: live GitHub PR merge flow before this commit --- .../.openspec.yaml | 2 + .../proposal.md | 25 ++++++++++ .../specs/ralplan-masterplan/spec.md | 40 +++++++++++++++ .../tasks.md | 29 +++++++++++ scripts/agent-branch-start.sh | 46 +++++++++++++++-- scripts/codex-agent.sh | 49 ++++++++++++++++++- scripts/openspec/init-plan-workspace.sh | 42 ++++++++++++++++ templates/scripts/agent-branch-start.sh | 46 +++++++++++++++-- templates/scripts/codex-agent.sh | 49 ++++++++++++++++++- .../scripts/openspec/init-plan-workspace.sh | 42 ++++++++++++++++ test/install.test.js | 34 ++++++++++--- test/merge-workflow.test.js | 2 +- test/metadata.test.js | 2 + 13 files changed, 391 insertions(+), 17 deletions(-) create mode 100644 openspec/changes/agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48/.openspec.yaml create mode 100644 openspec/changes/agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48/proposal.md create mode 100644 openspec/changes/agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48/specs/ralplan-masterplan/spec.md create mode 100644 openspec/changes/agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48/tasks.md diff --git a/openspec/changes/agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48/.openspec.yaml b/openspec/changes/agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48/.openspec.yaml new file mode 100644 index 0000000..4b8c565 --- /dev/null +++ b/openspec/changes/agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-21 diff --git a/openspec/changes/agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48/proposal.md b/openspec/changes/agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48/proposal.md new file mode 100644 index 0000000..71a3dc7 --- /dev/null +++ b/openspec/changes/agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48/proposal.md @@ -0,0 +1,25 @@ +## Why + +- Ralplan-created planning lanes need a visible `masterplan` identity in the + sandbox worktree and OpenSpec plan folder so the owner branch is easy to find + in VS Code and hand off to joined agents. +- Plan role folders need copyable prompts and explicit ownership/completion + checklists so helpers can reuse the same owner worktree instead of spinning up + unrelated branches. + +## What Changes + +- Prefix plan-backed worktree and plan-workspace identities with + `masterplan` while leaving branch names and change slugs stable. +- Extend the plan scaffold so each role gets a default `prompt.md` plus + ownership, collaboration, and cleanup-blocking checklist items. +- Keep `codex-agent.sh` aligned with the new naming so both the normal launch + path and the fallback sandbox path preserve the same `masterplan` labeling. + +## Impact + +- Touches Guardex runtime/template helper scripts and their regression suite. +- Existing branches are unaffected; only newly created plan-backed sandboxes get + the new worktree/plan naming. +- Cleanup still stays on the owner change lane; role prompts now make that + explicit for joined helpers. diff --git a/openspec/changes/agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48/specs/ralplan-masterplan/spec.md b/openspec/changes/agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48/specs/ralplan-masterplan/spec.md new file mode 100644 index 0000000..278f618 --- /dev/null +++ b/openspec/changes/agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48/specs/ralplan-masterplan/spec.md @@ -0,0 +1,40 @@ +## ADDED Requirements + +### Requirement: Plan-backed sandboxes expose a masterplan identity +GuardeX plan-backed sandboxes SHALL include a `masterplan` label in the +generated worktree path and OpenSpec plan workspace slug whenever OpenSpec plan +bootstrap is enabled. + +#### Scenario: agent-branch-start auto-bootstraps a shared planning lane +- **GIVEN** `scripts/agent-branch-start.sh` runs with `GUARDEX_OPENSPEC_AUTO_INIT=true` +- **WHEN** it creates the sandbox worktree and OpenSpec plan slug +- **THEN** the worktree path includes `agent____masterplan__` +- **AND** the plan folder slug starts with `agent--masterplan-` +- **AND** the change slug remains based on the original branch name. + +### Requirement: Plan role folders ship with shareable helper prompts +Guardex plan workspaces SHALL scaffold each role folder with a default +`prompt.md` and ownership-oriented checklist sections so joined Codex helpers +can work inside the same owner branch/worktree safely. + +#### Scenario: init-plan-workspace scaffolds role collaboration defaults +- **GIVEN** the user runs `scripts/openspec/init-plan-workspace.sh ` +- **WHEN** role folders are created for `planner`, `architect`, `critic`, + `executor`, `writer`, and `verifier` +- **THEN** each role folder includes `prompt.md` +- **AND** each role `tasks.md` includes ownership, collaboration, and completion + guidance in addition to the visible Spec/Tests/Implementation/Checkpoints sections +- **AND** the prompt instructs helpers to claim files in the owner lane and to + leave cleanup to the owner change tasks 4.1-4.3. + +### Requirement: codex-agent preserves masterplan labeling across safe fallback +`scripts/codex-agent.sh` SHALL preserve the same `masterplan` worktree/plan +labeling whether sandbox creation uses `agent-branch-start.sh` directly or the +safe fallback path. + +#### Scenario: codex-agent falls back after an unsafe starter +- **GIVEN** the starter script switches the primary checkout or otherwise fails + the safe sandbox checks +- **WHEN** `scripts/codex-agent.sh` creates the sandbox directly +- **THEN** the fallback worktree path still includes `agent____masterplan__` +- **AND** the fallback OpenSpec plan slug still starts with `agent--masterplan-`. diff --git a/openspec/changes/agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48/tasks.md b/openspec/changes/agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48/tasks.md new file mode 100644 index 0000000..e4b416c --- /dev/null +++ b/openspec/changes/agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48/tasks.md @@ -0,0 +1,29 @@ +## 1. Specification + +- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48`. +- [x] 1.2 Define normative requirements in `specs/ralplan-masterplan/spec.md`. + +## 2. Implementation + +- [x] 2.1 Implement scoped behavior changes. +- [x] 2.2 Add/update focused regression coverage. + +## 3. Verification + +- [x] 3.1 Run targeted project verification commands. +- [x] 3.2 Run `openspec validate agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48 --type change --strict`. +- [x] 3.3 Run `openspec validate --specs`. + +Verification evidence: +- `bash -n scripts/agent-branch-start.sh templates/scripts/agent-branch-start.sh scripts/codex-agent.sh templates/scripts/codex-agent.sh scripts/openspec/init-plan-workspace.sh templates/scripts/openspec/init-plan-workspace.sh` +- `node --test --test-name-pattern "setup agent-branch-start supports optional OpenSpec auto-bootstrap toggles|codex-agent launches codex inside a fresh sandbox worktree and keeps branch/worktree by default|codex-agent restores local branch and falls back to safe worktree start when starter script switches in-place|OpenSpec plan workspace scaffold creates expected role/task structure" test/install.test.js` +- `node --test --test-name-pattern "critical runtime helper scripts stay in sync with templates" test/metadata.test.js` +- `node --test --test-name-pattern "merge branches replays non-conflicting changes onto a dedicated target lane" test/merge-workflow.test.js` +- `openspec validate agent-codex-ralplan-masterplan-role-prompts-and-owne-2026-04-21-14-48 --type change --strict` +- `openspec validate --specs` -> exit 0 with `No items found to validate.` + +## 4. Completion + +- [ ] 4.1 Finish the agent branch via PR merge + cleanup (`gx finish --via-pr --wait-for-merge --cleanup` or `bash scripts/agent-branch-finish.sh --branch --base --via-pr --wait-for-merge --cleanup`). +- [ ] 4.2 Record PR URL + final `MERGED` state in the completion handoff. +- [ ] 4.3 Confirm sandbox cleanup (`git worktree list`, `git branch -a`) or capture a `BLOCKED:` handoff if merge/cleanup is pending. diff --git a/scripts/agent-branch-start.sh b/scripts/agent-branch-start.sh index 7e3024b..ef8cc11 100755 --- a/scripts/agent-branch-start.sh +++ b/scripts/agent-branch-start.sh @@ -11,6 +11,7 @@ OPENSPEC_AUTO_INIT_RAW="${GUARDEX_OPENSPEC_AUTO_INIT:-false}" OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}" OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}" OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CAPABILITY_SLUG:-}" +OPENSPEC_MASTERPLAN_LABEL_RAW="${GUARDEX_OPENSPEC_MASTERPLAN_LABEL-masterplan}" PRINT_NAME_ONLY=0 POSITIONAL_ARGS=() @@ -226,13 +227,35 @@ normalize_bool() { OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")" +resolve_openspec_masterplan_label() { + local raw="${OPENSPEC_MASTERPLAN_LABEL_RAW:-}" + local label + + if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]] || [[ -z "$raw" ]]; then + printf '' + return 0 + fi + + label="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')" + printf '%s' "$label" +} + resolve_openspec_plan_slug() { local branch_name="$1" - local task_slug="$2" + local agent_slug="$2" + local task_slug="$3" + local masterplan_label="" + local branch_leaf="" if [[ -n "$OPENSPEC_PLAN_SLUG_OVERRIDE" ]]; then sanitize_slug "$OPENSPEC_PLAN_SLUG_OVERRIDE" "$task_slug" return 0 fi + masterplan_label="$(resolve_openspec_masterplan_label)" + if [[ -n "$masterplan_label" ]] && [[ "$branch_name" == "agent/${agent_slug}/"* ]]; then + branch_leaf="${branch_name#agent/${agent_slug}/}" + sanitize_slug "agent-${agent_slug}-${masterplan_label}-${branch_leaf}" "$task_slug" + return 0 + fi sanitize_slug "${branch_name//\//-}" "$task_slug" } @@ -255,6 +278,22 @@ resolve_openspec_capability_slug() { sanitize_slug "$task_slug" "general-behavior" } +resolve_worktree_leaf() { + local branch_name="$1" + local agent_slug="$2" + local masterplan_label="" + local branch_leaf="" + + masterplan_label="$(resolve_openspec_masterplan_label)" + if [[ -n "$masterplan_label" ]] && [[ "$branch_name" == "agent/${agent_slug}/"* ]]; then + branch_leaf="${branch_name#agent/${agent_slug}/}" + printf 'agent__%s__%s__%s' "$agent_slug" "$masterplan_label" "$branch_leaf" + return 0 + fi + + printf '%s' "${branch_name//\//__}" +} + has_local_changes() { local root="$1" if ! git -C "$root" diff --quiet; then @@ -497,8 +536,9 @@ done 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")" +worktree_leaf="$(resolve_worktree_leaf "$branch_name" "$agent_slug")" +worktree_path="${worktree_root}/${worktree_leaf}" +openspec_plan_slug="$(resolve_openspec_plan_slug "$branch_name" "$agent_slug" "$task_slug")" openspec_change_slug="$(resolve_openspec_change_slug "$branch_name" "$task_slug")" openspec_capability_slug="$(resolve_openspec_capability_slug "$task_slug")" diff --git a/scripts/codex-agent.sh b/scripts/codex-agent.sh index e9e4166..a8a53c8 100755 --- a/scripts/codex-agent.sh +++ b/scripts/codex-agent.sh @@ -14,6 +14,7 @@ OPENSPEC_AUTO_INIT_RAW="${GUARDEX_OPENSPEC_AUTO_INIT:-true}" OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}" OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}" OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CAPABILITY_SLUG:-}" +OPENSPEC_MASTERPLAN_LABEL_RAW="${GUARDEX_OPENSPEC_MASTERPLAN_LABEL-masterplan}" normalize_bool() { local raw="${1:-}" @@ -34,6 +35,19 @@ AUTO_CLEANUP="$(normalize_bool "$AUTO_CLEANUP_RAW" "1")" AUTO_WAIT_FOR_MERGE="$(normalize_bool "$AUTO_WAIT_FOR_MERGE_RAW" "1")" OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")" +resolve_openspec_masterplan_label() { + local raw="${OPENSPEC_MASTERPLAN_LABEL_RAW:-}" + local label + + if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]] || [[ -z "$raw" ]]; then + printf '' + return 0 + fi + + label="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')" + printf '%s' "$label" +} + if [[ -n "$BASE_BRANCH" ]]; then BASE_BRANCH_EXPLICIT=1 fi @@ -161,11 +175,21 @@ sanitize_slug() { resolve_openspec_plan_slug() { local branch_name="$1" local task_slug + local masterplan_label="" + local branch_role="" + local branch_leaf="" task_slug="$(sanitize_slug "$TASK_NAME" "task")" if [[ -n "$OPENSPEC_PLAN_SLUG_OVERRIDE" ]]; then sanitize_slug "$OPENSPEC_PLAN_SLUG_OVERRIDE" "$task_slug" return 0 fi + masterplan_label="$(resolve_openspec_masterplan_label)" + if [[ -n "$masterplan_label" ]] && [[ "$branch_name" =~ ^agent/([^/]+)/(.+)$ ]]; then + branch_role="${BASH_REMATCH[1]}" + branch_leaf="${BASH_REMATCH[2]}" + sanitize_slug "agent-${branch_role}-${masterplan_label}-${branch_leaf}" "$task_slug" + return 0 + fi sanitize_slug "${branch_name//\//-}" "$task_slug" } @@ -190,6 +214,23 @@ resolve_openspec_capability_slug() { sanitize_slug "$task_slug" "general-behavior" } +resolve_worktree_leaf() { + local branch_name="$1" + local masterplan_label="" + local branch_role="" + local branch_leaf="" + + masterplan_label="$(resolve_openspec_masterplan_label)" + if [[ -n "$masterplan_label" ]] && [[ "$branch_name" =~ ^agent/([^/]+)/(.+)$ ]]; then + branch_role="${BASH_REMATCH[1]}" + branch_leaf="${BASH_REMATCH[2]}" + printf 'agent__%s__%s__%s' "$branch_role" "$masterplan_label" "$branch_leaf" + return 0 + fi + + printf '%s' "${branch_name//\//__}" +} + hydrate_local_helper_in_worktree() { local worktree="$1" local relative_path="$2" @@ -314,7 +355,7 @@ start_sandbox_fallback() { worktree_root="${repo_root}/.omx/agent-worktrees" mkdir -p "$worktree_root" - worktree_path="${worktree_root}/${branch_name//\//__}" + worktree_path="${worktree_root}/$(resolve_worktree_leaf "$branch_name")" if [[ -e "$worktree_path" ]]; then echo "[codex-agent] Fallback worktree path already exists: $worktree_path" >&2 return 1 @@ -346,7 +387,11 @@ initial_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/nu start_output="" start_status=0 set +e -start_output="$(GUARDEX_OPENSPEC_AUTO_INIT=0 bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1)" +start_output="$( + GUARDEX_OPENSPEC_AUTO_INIT="$OPENSPEC_AUTO_INIT" \ + GUARDEX_OPENSPEC_MASTERPLAN_LABEL="$OPENSPEC_MASTERPLAN_LABEL_RAW" \ + bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1 +)" start_status=$? set -e diff --git a/scripts/openspec/init-plan-workspace.sh b/scripts/openspec/init-plan-workspace.sh index abf8c7f..f05f28c 100755 --- a/scripts/openspec/init-plan-workspace.sh +++ b/scripts/openspec/init-plan-workspace.sh @@ -54,6 +54,10 @@ write_if_missing "$PLAN_DIR/README.md" "# Plan Workspace: ${PLAN_SLUG} Durable pre-implementation planning workspace. +Each role folder includes a copyable \`prompt.md\` for joined Codex helpers. +Helpers reuse the owner branch/worktree, claim the role files they touch, and +leave PR merge + sandbox cleanup to the owner change lane. + Use this command to update checkpoints: \`\`\`bash @@ -89,10 +93,39 @@ for role in "${ROLES[@]}"; do write_if_missing "$ROLE_DIR/README.md" "# ${role} Role workspace for \`${role}\`. +" + + write_if_missing "$ROLE_DIR/prompt.md" "# ${role} prompt + +You are the \`${role}\` lane for shared plan \`${PLAN_SLUG}\`. + +## Scope + +- Work inside \`openspec/plan/${PLAN_SLUG}/${role}/\` plus directly-related shared plan files you explicitly claim. +- Reuse the owner's branch/worktree instead of creating a separate sandbox unless the owner says otherwise. + +## Ownership + +- Before editing, claim this role's files in the shared owner lane: + \`python3 scripts/agent-file-locks.py claim --branch openspec/plan/${PLAN_SLUG}/${role}/README.md openspec/plan/${PLAN_SLUG}/${role}/prompt.md openspec/plan/${PLAN_SLUG}/${role}/tasks.md openspec/plan/${PLAN_SLUG}/checkpoints.md\` +- Record branch, worktree, and scope in \`tasks.md\`. +- Do not change another role's files without reassignment. + +## Deliverables + +- Complete the role checklist in \`tasks.md\`. +- Leave a handoff with files changed, verification, and risks. +- The owner alone runs the change completion flow and sandbox cleanup after change tasks 4.1-4.3 are done. " write_if_missing "$ROLE_DIR/tasks.md" "# ${role} tasks +## Ownership + +- [ ] Claim this role's files in the shared owner branch/worktree before editing. +- [ ] Record branch, worktree, and scope for this role. +- [ ] Copy or hand off \`prompt.md\` when another agent joins this role. + ## 1. Spec - [ ] Define requirements and scope for ${role} @@ -111,6 +144,15 @@ Role workspace for \`${role}\`. ## 4. Checkpoints - [ ] Publish checkpoint update for this role + +## 5. Collaboration + +- [ ] Leave a role handoff with files changed, verification, and risks. +- [ ] Owner records \`accept\`, \`revise\`, or \`reject\` for joined output, or marks \`N/A\` if no helper joined. + +## 6. Completion + +- [ ] Keep sandbox cleanup blocked until change tasks 4.1-4.3 are complete. " done diff --git a/templates/scripts/agent-branch-start.sh b/templates/scripts/agent-branch-start.sh index 7e3024b..ef8cc11 100755 --- a/templates/scripts/agent-branch-start.sh +++ b/templates/scripts/agent-branch-start.sh @@ -11,6 +11,7 @@ OPENSPEC_AUTO_INIT_RAW="${GUARDEX_OPENSPEC_AUTO_INIT:-false}" OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}" OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}" OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CAPABILITY_SLUG:-}" +OPENSPEC_MASTERPLAN_LABEL_RAW="${GUARDEX_OPENSPEC_MASTERPLAN_LABEL-masterplan}" PRINT_NAME_ONLY=0 POSITIONAL_ARGS=() @@ -226,13 +227,35 @@ normalize_bool() { OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")" +resolve_openspec_masterplan_label() { + local raw="${OPENSPEC_MASTERPLAN_LABEL_RAW:-}" + local label + + if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]] || [[ -z "$raw" ]]; then + printf '' + return 0 + fi + + label="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')" + printf '%s' "$label" +} + resolve_openspec_plan_slug() { local branch_name="$1" - local task_slug="$2" + local agent_slug="$2" + local task_slug="$3" + local masterplan_label="" + local branch_leaf="" if [[ -n "$OPENSPEC_PLAN_SLUG_OVERRIDE" ]]; then sanitize_slug "$OPENSPEC_PLAN_SLUG_OVERRIDE" "$task_slug" return 0 fi + masterplan_label="$(resolve_openspec_masterplan_label)" + if [[ -n "$masterplan_label" ]] && [[ "$branch_name" == "agent/${agent_slug}/"* ]]; then + branch_leaf="${branch_name#agent/${agent_slug}/}" + sanitize_slug "agent-${agent_slug}-${masterplan_label}-${branch_leaf}" "$task_slug" + return 0 + fi sanitize_slug "${branch_name//\//-}" "$task_slug" } @@ -255,6 +278,22 @@ resolve_openspec_capability_slug() { sanitize_slug "$task_slug" "general-behavior" } +resolve_worktree_leaf() { + local branch_name="$1" + local agent_slug="$2" + local masterplan_label="" + local branch_leaf="" + + masterplan_label="$(resolve_openspec_masterplan_label)" + if [[ -n "$masterplan_label" ]] && [[ "$branch_name" == "agent/${agent_slug}/"* ]]; then + branch_leaf="${branch_name#agent/${agent_slug}/}" + printf 'agent__%s__%s__%s' "$agent_slug" "$masterplan_label" "$branch_leaf" + return 0 + fi + + printf '%s' "${branch_name//\//__}" +} + has_local_changes() { local root="$1" if ! git -C "$root" diff --quiet; then @@ -497,8 +536,9 @@ done 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")" +worktree_leaf="$(resolve_worktree_leaf "$branch_name" "$agent_slug")" +worktree_path="${worktree_root}/${worktree_leaf}" +openspec_plan_slug="$(resolve_openspec_plan_slug "$branch_name" "$agent_slug" "$task_slug")" openspec_change_slug="$(resolve_openspec_change_slug "$branch_name" "$task_slug")" openspec_capability_slug="$(resolve_openspec_capability_slug "$task_slug")" diff --git a/templates/scripts/codex-agent.sh b/templates/scripts/codex-agent.sh index e9e4166..a8a53c8 100755 --- a/templates/scripts/codex-agent.sh +++ b/templates/scripts/codex-agent.sh @@ -14,6 +14,7 @@ OPENSPEC_AUTO_INIT_RAW="${GUARDEX_OPENSPEC_AUTO_INIT:-true}" OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}" OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}" OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CAPABILITY_SLUG:-}" +OPENSPEC_MASTERPLAN_LABEL_RAW="${GUARDEX_OPENSPEC_MASTERPLAN_LABEL-masterplan}" normalize_bool() { local raw="${1:-}" @@ -34,6 +35,19 @@ AUTO_CLEANUP="$(normalize_bool "$AUTO_CLEANUP_RAW" "1")" AUTO_WAIT_FOR_MERGE="$(normalize_bool "$AUTO_WAIT_FOR_MERGE_RAW" "1")" OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")" +resolve_openspec_masterplan_label() { + local raw="${OPENSPEC_MASTERPLAN_LABEL_RAW:-}" + local label + + if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]] || [[ -z "$raw" ]]; then + printf '' + return 0 + fi + + label="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')" + printf '%s' "$label" +} + if [[ -n "$BASE_BRANCH" ]]; then BASE_BRANCH_EXPLICIT=1 fi @@ -161,11 +175,21 @@ sanitize_slug() { resolve_openspec_plan_slug() { local branch_name="$1" local task_slug + local masterplan_label="" + local branch_role="" + local branch_leaf="" task_slug="$(sanitize_slug "$TASK_NAME" "task")" if [[ -n "$OPENSPEC_PLAN_SLUG_OVERRIDE" ]]; then sanitize_slug "$OPENSPEC_PLAN_SLUG_OVERRIDE" "$task_slug" return 0 fi + masterplan_label="$(resolve_openspec_masterplan_label)" + if [[ -n "$masterplan_label" ]] && [[ "$branch_name" =~ ^agent/([^/]+)/(.+)$ ]]; then + branch_role="${BASH_REMATCH[1]}" + branch_leaf="${BASH_REMATCH[2]}" + sanitize_slug "agent-${branch_role}-${masterplan_label}-${branch_leaf}" "$task_slug" + return 0 + fi sanitize_slug "${branch_name//\//-}" "$task_slug" } @@ -190,6 +214,23 @@ resolve_openspec_capability_slug() { sanitize_slug "$task_slug" "general-behavior" } +resolve_worktree_leaf() { + local branch_name="$1" + local masterplan_label="" + local branch_role="" + local branch_leaf="" + + masterplan_label="$(resolve_openspec_masterplan_label)" + if [[ -n "$masterplan_label" ]] && [[ "$branch_name" =~ ^agent/([^/]+)/(.+)$ ]]; then + branch_role="${BASH_REMATCH[1]}" + branch_leaf="${BASH_REMATCH[2]}" + printf 'agent__%s__%s__%s' "$branch_role" "$masterplan_label" "$branch_leaf" + return 0 + fi + + printf '%s' "${branch_name//\//__}" +} + hydrate_local_helper_in_worktree() { local worktree="$1" local relative_path="$2" @@ -314,7 +355,7 @@ start_sandbox_fallback() { worktree_root="${repo_root}/.omx/agent-worktrees" mkdir -p "$worktree_root" - worktree_path="${worktree_root}/${branch_name//\//__}" + worktree_path="${worktree_root}/$(resolve_worktree_leaf "$branch_name")" if [[ -e "$worktree_path" ]]; then echo "[codex-agent] Fallback worktree path already exists: $worktree_path" >&2 return 1 @@ -346,7 +387,11 @@ initial_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/nu start_output="" start_status=0 set +e -start_output="$(GUARDEX_OPENSPEC_AUTO_INIT=0 bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1)" +start_output="$( + GUARDEX_OPENSPEC_AUTO_INIT="$OPENSPEC_AUTO_INIT" \ + GUARDEX_OPENSPEC_MASTERPLAN_LABEL="$OPENSPEC_MASTERPLAN_LABEL_RAW" \ + bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1 +)" start_status=$? set -e diff --git a/templates/scripts/openspec/init-plan-workspace.sh b/templates/scripts/openspec/init-plan-workspace.sh index abf8c7f..f05f28c 100644 --- a/templates/scripts/openspec/init-plan-workspace.sh +++ b/templates/scripts/openspec/init-plan-workspace.sh @@ -54,6 +54,10 @@ write_if_missing "$PLAN_DIR/README.md" "# Plan Workspace: ${PLAN_SLUG} Durable pre-implementation planning workspace. +Each role folder includes a copyable \`prompt.md\` for joined Codex helpers. +Helpers reuse the owner branch/worktree, claim the role files they touch, and +leave PR merge + sandbox cleanup to the owner change lane. + Use this command to update checkpoints: \`\`\`bash @@ -89,10 +93,39 @@ for role in "${ROLES[@]}"; do write_if_missing "$ROLE_DIR/README.md" "# ${role} Role workspace for \`${role}\`. +" + + write_if_missing "$ROLE_DIR/prompt.md" "# ${role} prompt + +You are the \`${role}\` lane for shared plan \`${PLAN_SLUG}\`. + +## Scope + +- Work inside \`openspec/plan/${PLAN_SLUG}/${role}/\` plus directly-related shared plan files you explicitly claim. +- Reuse the owner's branch/worktree instead of creating a separate sandbox unless the owner says otherwise. + +## Ownership + +- Before editing, claim this role's files in the shared owner lane: + \`python3 scripts/agent-file-locks.py claim --branch openspec/plan/${PLAN_SLUG}/${role}/README.md openspec/plan/${PLAN_SLUG}/${role}/prompt.md openspec/plan/${PLAN_SLUG}/${role}/tasks.md openspec/plan/${PLAN_SLUG}/checkpoints.md\` +- Record branch, worktree, and scope in \`tasks.md\`. +- Do not change another role's files without reassignment. + +## Deliverables + +- Complete the role checklist in \`tasks.md\`. +- Leave a handoff with files changed, verification, and risks. +- The owner alone runs the change completion flow and sandbox cleanup after change tasks 4.1-4.3 are done. " write_if_missing "$ROLE_DIR/tasks.md" "# ${role} tasks +## Ownership + +- [ ] Claim this role's files in the shared owner branch/worktree before editing. +- [ ] Record branch, worktree, and scope for this role. +- [ ] Copy or hand off \`prompt.md\` when another agent joins this role. + ## 1. Spec - [ ] Define requirements and scope for ${role} @@ -111,6 +144,15 @@ Role workspace for \`${role}\`. ## 4. Checkpoints - [ ] Publish checkpoint update for this role + +## 5. Collaboration + +- [ ] Leave a role handoff with files changed, verification, and risks. +- [ ] Owner records \`accept\`, \`revise\`, or \`reject\` for joined output, or marks \`N/A\` if no helper joined. + +## 6. Completion + +- [ ] Keep sandbox cleanup blocked until change tasks 4.1-4.3 are complete. " done diff --git a/test/install.test.js b/test/install.test.js index ce6d2f1..cab67ec 100644 --- a/test/install.test.js +++ b/test/install.test.js @@ -326,6 +326,14 @@ function extractOpenSpecChangeSlug(output) { return match[1].trim(); } +function expectedMasterplanPlanSlug(branchName, fallback) { + const match = String(branchName || '').match(/^agent\/([^/]+)\/(.+)$/); + if (!match) { + return sanitizeSlug(branchName, fallback); + } + return sanitizeSlug(`agent-${match[1]}-masterplan-${match[2]}`, fallback); +} + function extractHookCommands(settings) { const hooks = settings && typeof settings === 'object' ? settings.hooks : null; if (!hooks || typeof hooks !== 'object') { @@ -1753,7 +1761,7 @@ test('setup agent-branch-start supports optional OpenSpec auto-bootstrap toggles const defaultWorktree = extractCreatedWorktree(result.stdout); const defaultPlanSlug = extractOpenSpecPlanSlug(result.stdout); const defaultChangeSlug = extractOpenSpecChangeSlug(result.stdout); - assert.equal(defaultPlanSlug, sanitizeSlug(defaultBranch, 'openspec-default')); + assert.equal(defaultPlanSlug, expectedMasterplanPlanSlug(defaultBranch, 'openspec-default')); assert.equal(defaultChangeSlug, sanitizeSlug(defaultBranch, 'openspec-default')); assert.equal( fs.existsSync(path.join(defaultWorktree, 'openspec', 'plan', defaultPlanSlug, 'summary.md')), @@ -3120,7 +3128,7 @@ test('codex-agent launches codex inside a fresh sandbox worktree and keeps branc const launchedCwd = fs.readFileSync(cwdMarker, 'utf8').trim(); assert.match( launchedCwd, - new RegExp(`${escapeRegexLiteral(repoDir)}/\\.omx/agent-worktrees/agent__planner__`), + new RegExp(`${escapeRegexLiteral(repoDir)}/\\.omx/agent-worktrees/agent__planner__masterplan__`), ); const launchedArgs = fs.readFileSync(argsMarker, 'utf8').trim(); @@ -3130,10 +3138,10 @@ test('codex-agent launches codex inside a fresh sandbox worktree and keeps branc assert.match(launch.stdout, /\[codex-agent\] OpenSpec change workspace:/); assert.match(launch.stdout, /\[codex-agent\] OpenSpec plan workspace:/); const launchedBranch = extractCreatedBranch(launch.stdout); + const openspecPlanSlug = extractOpenSpecPlanSlug(launch.stdout); + const openspecChangeSlug = extractOpenSpecChangeSlug(launch.stdout); const branchResult = runCmd('git', ['show-ref', '--verify', '--quiet', `refs/heads/${launchedBranch}`], repoDir); assert.equal(branchResult.status, 0, 'agent branch should remain after default codex-agent run'); - const openspecPlanSlug = sanitizeSlug(launchedBranch, 'launch-task'); - const openspecChangeSlug = sanitizeSlug(launchedBranch, 'launch-task'); assert.equal( fs.existsSync(path.join(launchedCwd, 'openspec', 'plan', openspecPlanSlug, 'summary.md')), true, @@ -3215,13 +3223,13 @@ test('codex-agent restores local branch and falls back to safe worktree start wh const launchedCwd = fs.readFileSync(cwdMarker, 'utf8').trim(); assert.match( launchedCwd, - new RegExp(`${escapeRegexLiteral(repoDir)}/\\.omx/agent-worktrees/agent__planner__`), + new RegExp(`${escapeRegexLiteral(repoDir)}/\\.omx/agent-worktrees/agent__planner__masterplan__`), ); assert.notEqual(launchedCwd, repoDir); assert.match(combinedOutput, /\[codex-agent\] OpenSpec change workspace:/); assert.match(combinedOutput, /\[codex-agent\] OpenSpec plan workspace:/); const launchedBranch = extractCreatedBranch(combinedOutput); - const openspecPlanSlug = sanitizeSlug(launchedBranch, 'fallback-task'); + const openspecPlanSlug = expectedMasterplanPlanSlug(launchedBranch, 'fallback-task'); const openspecChangeSlug = sanitizeSlug(launchedBranch, 'fallback-task'); assert.equal( fs.existsSync(path.join(launchedCwd, 'openspec', 'plan', openspecPlanSlug, 'summary.md')), @@ -4041,11 +4049,17 @@ test('OpenSpec plan workspace scaffold creates expected role/task structure', () 'summary.md', 'checkpoints.md', 'planner/plan.md', + 'planner/prompt.md', 'planner/tasks.md', + 'architect/prompt.md', 'architect/tasks.md', + 'critic/prompt.md', 'critic/tasks.md', + 'executor/prompt.md', 'executor/tasks.md', + 'writer/prompt.md', 'writer/tasks.md', + 'verifier/prompt.md', 'verifier/tasks.md', ]; for (const rel of expected) { @@ -4053,10 +4067,18 @@ test('OpenSpec plan workspace scaffold creates expected role/task structure', () } const plannerTasks = fs.readFileSync(path.join(planDir, 'planner', 'tasks.md'), 'utf8'); + assert.match(plannerTasks, /## Ownership/); assert.match(plannerTasks, /## 1\. Spec/); assert.match(plannerTasks, /## 2\. Tests/); assert.match(plannerTasks, /## 3\. Implementation/); assert.match(plannerTasks, /## 4\. Checkpoints/); + assert.match(plannerTasks, /## 5\. Collaboration/); + assert.match(plannerTasks, /## 6\. Completion/); + assert.match(plannerTasks, /Claim this role's files in the shared owner branch\/worktree before editing/); + + const plannerPrompt = fs.readFileSync(path.join(planDir, 'planner', 'prompt.md'), 'utf8'); + assert.match(plannerPrompt, /claim --branch /); + assert.match(plannerPrompt, /change tasks 4\.1-4\.3/); }); test('OpenSpec change workspace scaffold creates proposal/tasks/spec defaults', () => { diff --git a/test/merge-workflow.test.js b/test/merge-workflow.test.js index 62e5216..797c180 100644 --- a/test/merge-workflow.test.js +++ b/test/merge-workflow.test.js @@ -201,7 +201,7 @@ test('merge command creates an integration lane, reports overlaps, and merges cl assert.equal(ancestry.status, 0, ancestry.stderr || ancestry.stdout); assert.match(output, /OpenSpec change workspace: .+openspec\/changes\/agent-codex-merge-shared-smoke-/); - assert.match(output, /OpenSpec plan workspace: .+openspec\/plan\/agent-codex-merge-shared-smoke-/); + assert.match(output, /OpenSpec plan workspace: .+openspec\/plan\/agent-codex-masterplan-merge-shared-smoke-/); }); test('merge command reuses an owner lane and stops with resumable guidance on conflict', () => { diff --git a/test/metadata.test.js b/test/metadata.test.js index db5c8b5..e2b91a6 100644 --- a/test/metadata.test.js +++ b/test/metadata.test.js @@ -94,7 +94,9 @@ test('frontend mirror workflow skips cleanly when the mirror PAT is missing', () test('critical runtime helper scripts stay in sync with templates', () => { const pairs = [ + ['templates/scripts/agent-branch-start.sh', 'scripts/agent-branch-start.sh'], ['templates/scripts/codex-agent.sh', 'scripts/codex-agent.sh'], + ['templates/scripts/openspec/init-plan-workspace.sh', 'scripts/openspec/init-plan-workspace.sh'], ['templates/scripts/openspec/init-change-workspace.sh', 'scripts/openspec/init-change-workspace.sh'], ];