okstra 0.8.0 → 0.10.0
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.kr.md +14 -3
- package/README.md +14 -3
- package/docs/kr/architecture.md +8 -3
- package/docs/kr/cli.md +55 -1
- package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +638 -0
- package/docs/superpowers/specs/2026-05-12-ticket-id-in-reports-design.md +131 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +13 -0
- package/runtime/agents/workers/claude-worker.md +2 -0
- package/runtime/agents/workers/codex-worker.md +2 -0
- package/runtime/agents/workers/gemini-worker.md +2 -0
- package/runtime/agents/workers/report-writer-worker.md +1 -0
- package/runtime/prompts/launch.template.md +11 -0
- package/runtime/prompts/profiles/_common-contract.md +27 -0
- package/runtime/prompts/profiles/error-analysis.md +4 -20
- package/runtime/prompts/profiles/final-verification.md +4 -20
- package/runtime/prompts/profiles/implementation-planning.md +4 -19
- package/runtime/prompts/profiles/implementation.md +34 -21
- package/runtime/prompts/profiles/release-handoff.md +89 -0
- package/runtime/prompts/profiles/requirements-discovery.md +2 -20
- package/runtime/python/lib/okstra/cli.sh +1 -1
- package/runtime/python/okstra_ctl/render.py +9 -2
- package/runtime/python/okstra_ctl/run.py +61 -1
- package/runtime/python/okstra_ctl/workers.py +6 -1
- package/runtime/python/okstra_ctl/workflow.py +30 -2
- package/runtime/python/okstra_ctl/worktree.py +298 -0
- package/runtime/skills/okstra-context-loader/SKILL.md +1 -1
- package/runtime/skills/okstra-convergence/SKILL.md +11 -5
- package/runtime/skills/okstra-report-finder/SKILL.md +1 -0
- package/runtime/skills/okstra-report-writer/SKILL.md +33 -1
- package/runtime/skills/okstra-run/SKILL.md +2 -1
- package/runtime/skills/okstra-status/SKILL.md +3 -1
- package/runtime/skills/okstra-team-contract/SKILL.md +19 -0
- package/runtime/skills/okstra-time-summary/SKILL.md +1 -0
- package/runtime/templates/reports/error-analysis-input.template.md +1 -0
- package/runtime/templates/reports/final-report.template.md +210 -28
- package/runtime/templates/reports/implementation-input.template.md +1 -0
- package/runtime/templates/reports/implementation-planning-input.template.md +1 -0
- package/runtime/templates/reports/quick-input.template.md +1 -0
- package/runtime/templates/reports/release-handoff-input.template.md +73 -0
- package/runtime/templates/reports/task-brief.template.md +5 -0
- package/runtime/validators/validate-run.py +6 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Release Handoff Profile
|
|
2
|
+
|
|
3
|
+
- Purpose: take an `accepted` final-verification verdict and turn it into a delivered commit and/or pull request, with explicit user selection at every mutating step
|
|
4
|
+
- Required workers:
|
|
5
|
+
- claude
|
|
6
|
+
- report-writer
|
|
7
|
+
{{INCLUDE:_common-contract.md}}
|
|
8
|
+
- Team contract (phase-specific overrides):
|
|
9
|
+
- `Claude lead` is the **executor of every git / gh command** in this phase. Workers never call mutating git commands themselves.
|
|
10
|
+
- `Claude worker` (drafter) is read-only and produces **commit message candidate(s) and a PR body candidate** in markdown; the lead presents these to the user, accepts edits, and only then runs git.
|
|
11
|
+
- Codex / Gemini workers are NOT part of this profile's roster (see `Required workers:` above); the shared contract's `Gemini worker must always be attempted` clause does not apply to release-handoff.
|
|
12
|
+
- Pre-handoff entry gate (mandatory — refuse to start if any item fails):
|
|
13
|
+
- the task brief MUST cite the originating `final-verification` final-report path under `## Source Verification Report`. The lead opens that file and confirms section `## 2. Final Verdict` contains exactly the token `accepted`.
|
|
14
|
+
- if the verdict is `conditional-accept`, `blocked`, or any other token (including ambiguous phrasing like "looks good"), the run MUST end immediately with status `blocked` and a routing recommendation back to `error-analysis` or `implementation-planning`. Do NOT prompt the user; do NOT run any git command.
|
|
15
|
+
- the lead MUST capture `git status --short` and confirm the working tree is clean OR contains only the files listed in the prior `implementation` run's `Out-of-plan edits` block. Unexpected dirty state aborts the run.
|
|
16
|
+
- the lead MUST capture `git rev-parse --abbrev-ref HEAD` and record it as the **feature branch**. If the current branch is itself `main`, `master`, `prod`, `preprod`, `staging`, or `dev`, the run MUST end immediately — release-handoff never operates on a base branch.
|
|
17
|
+
- User interaction protocol (Claude lead — performed in order, using `AskUserQuestion` or the equivalent interactive prompt):
|
|
18
|
+
1. **Action selection** — present three choices and capture exactly one:
|
|
19
|
+
- `commit only` — stage and commit the working-tree changes locally; no push, no PR.
|
|
20
|
+
- `commit + PR` — commit, push the feature branch, then open a pull request.
|
|
21
|
+
- `skip` — record the verified state and end the run without any git command.
|
|
22
|
+
If the user picks `skip`, route directly to the final-report self-review pass.
|
|
23
|
+
2. **PR base branch** (only when the user picked `commit + PR`) — present six options and capture exactly one:
|
|
24
|
+
- `staging`
|
|
25
|
+
- `preprod`
|
|
26
|
+
- `prod`
|
|
27
|
+
- `main`
|
|
28
|
+
- `dev`
|
|
29
|
+
- `직접 입력` (free-form branch name; lead validates the name exists on origin via `git ls-remote --heads origin <name>` and re-asks on failure)
|
|
30
|
+
The chosen base MUST NOT equal the feature branch. If it does, re-ask.
|
|
31
|
+
3. **Commit message + PR body confirmation** — show the `Claude worker` drafter's output verbatim and capture one of:
|
|
32
|
+
- `use as-is` — proceed with the drafter's text.
|
|
33
|
+
- `edit then proceed` — accept inline edits from the user, then proceed with the edited text.
|
|
34
|
+
- `cancel` — end the run without executing any git command; record the cancellation in the final report.
|
|
35
|
+
- Drafter worker contract (`Claude worker`):
|
|
36
|
+
- reads the run brief, the cited final-verification report, and `git diff <base>..HEAD --stat` to ground its output in actual changes.
|
|
37
|
+
- produces **two artifacts** in its worker result:
|
|
38
|
+
1. **Commit message candidate** — a single message in Conventional Commits style (`<type>(<scope>): <subject>` + optional body + optional footer). When `commit + PR` will be opened against a `release-please`-managed repo, the type MUST match a configured changelog section (`feat` / `fix` / `perf` / `revert` / `deps` / `docs` / `refactor` / `build` / `ci` / `chore` / `test`).
|
|
39
|
+
2. **PR body candidate** — markdown with sections `## Summary`, `## Changes`, `## Test plan`, `## Linked issues` (omit a section only if it is genuinely empty).
|
|
40
|
+
- is strictly **read-only**: MUST NOT run any git mutating command, MUST NOT call `gh pr create`, MUST NOT call `git push`.
|
|
41
|
+
- if the drafter cannot produce output (no diff exists, repo is bare, etc.), it returns terminal status `not-run` with a reason; the lead falls back to a manually-authored message captured directly from the user.
|
|
42
|
+
- Allowed actions during the run (Claude lead only — workers stay read-only):
|
|
43
|
+
- read-only inspection: `git status`, `git status --short`, `git diff`, `git log`, `git rev-parse`, `git ls-remote --heads origin <name>`, `gh pr list --head <branch>`, `gh pr view <url>`.
|
|
44
|
+
- local commit: `git add -- <path>...` (prefer explicit file paths over `git add -A` / `git add .`), `git commit -m "<message>"`. Re-use the user-confirmed message exactly.
|
|
45
|
+
- feature-branch push (only when the user picked `commit + PR`): `git push -u origin <current-branch>`. The pushed ref MUST be the feature branch — never the chosen base branch.
|
|
46
|
+
- PR creation (only when the user picked `commit + PR` AND no PR with the same head already exists on origin): `gh pr create --base <chosen-base> --head <current-branch> --title "<title>" --body "<body>"`. The title is the commit message subject by default; the body is the user-confirmed PR body.
|
|
47
|
+
- PR reuse: if `gh pr list --head <branch> --state open --json url --jq '.[0].url'` returns a URL, treat that PR as already existing — record the URL in the final report and SKIP `gh pr create`.
|
|
48
|
+
- Idempotency: if `git diff --cached` and `git diff` are both empty (nothing to commit), record "no staged changes; commit skipped" in the final report and skip `git commit` while still proceeding to the PR step if requested.
|
|
49
|
+
- Forbidden actions (any occurrence → terminal status `contract-violated`):
|
|
50
|
+
- any of the following git push variants, regardless of intent or whether the user said "force it":
|
|
51
|
+
- `git push --force`
|
|
52
|
+
- `git push --force-with-lease`
|
|
53
|
+
- `git push -f`
|
|
54
|
+
- `git push +<refspec>`
|
|
55
|
+
- any other invocation that rewrites remote history
|
|
56
|
+
- pushing directly to a base branch — i.e. `git push origin <branch>` where `<branch>` is `main`, `master`, `prod`, `preprod`, `staging`, `dev`, or the branch the user chose as the PR base in this run. The only permitted push target is the current feature branch.
|
|
57
|
+
- bypassing repo safeguards: `--no-verify` / `-n` on `git commit` or `git push`, disabling GPG signing via `-c commit.gpgsign=false` / `--no-gpg-sign`, or any equivalent flag-based hook bypass.
|
|
58
|
+
- release-publishing commands: `gh release create`, `gh release edit`, `npm publish`, `cargo publish`, `pip publish`, `twine upload`, `docker push`, `terraform apply`, `kubectl apply` against any non-local cluster.
|
|
59
|
+
- source-code edits, refactors, or any modification to files outside the run's own artifact directories (`reports/`, `prompts/`, `state/`, `manifests/`, `worker-results/`, `status/`, `sessions/`). The diff being shipped MUST be exactly what the prior `implementation` run produced; release-handoff packages it, it does not re-author it.
|
|
60
|
+
- executing any mutating command the user did NOT select. Examples: opening a PR when the user picked `commit only`; running `git commit` when the user picked `skip`; switching the PR base branch silently after the user already chose one.
|
|
61
|
+
- retrying a failed git / gh command with weaker safety flags. If `git push` fails with non-fast-forward, the lead MUST stop, explain the failure to the user, and ask for instructions — it MUST NOT add `--force`.
|
|
62
|
+
- dispatching parallel sub-agents beyond the required worker roster.
|
|
63
|
+
- silently treating an unrecognised user reply as one of the menu options. If the user's answer does not match a presented choice, re-ask the question verbatim.
|
|
64
|
+
- Required deliverable shape (final report, in addition to the standard sections):
|
|
65
|
+
- **Source Verification Report**: relative path of the originating `final-verification` final-report file plus the literal quoted `## 2. Final Verdict` line that read `accepted`.
|
|
66
|
+
- **Feature Branch & Working-Tree State**: branch name from `git rev-parse --abbrev-ref HEAD`, output of `git status --short` at run start.
|
|
67
|
+
- **User Selections**: a block recording each prompt and the user's verbatim answer.
|
|
68
|
+
- Q1 action: `commit only` | `commit + PR` | `skip`.
|
|
69
|
+
- Q2 PR base (if applicable): the chosen branch and how it was selected (menu pick vs free-form input).
|
|
70
|
+
- Q3 message/body: `use as-is` | `edit then proceed` (with a diff between the drafter's text and the final text) | `cancel`.
|
|
71
|
+
- **Executed Commands**: every git / gh command the lead actually ran, with its exit code and a one-line stdout/stderr summary. Read-only inspection commands MAY be summarised; mutating commands MUST be listed verbatim.
|
|
72
|
+
- **Commit List**: each commit SHA (short and full), its subject line, and the files it touched. If no commit was produced (idempotent no-op), state `- No commit was produced (working tree had no staged changes).`
|
|
73
|
+
- **Pull Request Outcome**: one of
|
|
74
|
+
- `- No PR action requested.` (user picked `commit only` or `skip`)
|
|
75
|
+
- `- PR created: <url>` with title and base branch
|
|
76
|
+
- `- PR reused: <url>` when an existing PR was found via `gh pr list`
|
|
77
|
+
- `- PR creation skipped: <reason>` for any user-driven cancellation
|
|
78
|
+
- **Routing recommendation**: explicit `done` token, since release-handoff is the terminal lifecycle phase. If the run ended in `skip` or `cancel`, the recommendation MUST also state whether re-entry into release-handoff is appropriate.
|
|
79
|
+
- Self-review pass before finalising the report (`Claude lead` runs this; do not delegate to a generic subagent):
|
|
80
|
+
1. **Entry-gate audit** — section 2 cites the originating final-verification report path and the literal `accepted` verdict line. If either is missing, the run is invalid and MUST be re-routed to `final-verification`.
|
|
81
|
+
2. **User-selection traceability** — every executed mutating command maps to a user selection captured in the report. Any mutating command without a corresponding user answer is a contract violation.
|
|
82
|
+
3. **Forbidden-action audit** — scan the run's session transcripts (`git`, `gh` invocations) for every entry in the Forbidden actions list above. Any occurrence means the run has crossed into unsafe territory and MUST be flagged as `contract-violated`.
|
|
83
|
+
4. **Push-target audit** — for every `git push` recorded, confirm the refspec resolves to the feature branch, not the base branch.
|
|
84
|
+
5. **Idempotency check** — if a PR with the same head already existed at run start, confirm the report records `PR reused` rather than a fresh `gh pr create` invocation.
|
|
85
|
+
- Non-goals:
|
|
86
|
+
- re-litigating the final-verification verdict — release-handoff trusts the cited `accepted` verdict and does not reopen acceptance checks.
|
|
87
|
+
- opening additional PRs, releases, or deployments beyond the single PR the user chose to create.
|
|
88
|
+
- merging the PR. Merging is a separate, manual step performed by the user (or by repo automation) after release-handoff ends; the lead MUST NOT call `gh pr merge`.
|
|
89
|
+
- escalating beyond the menu choices on user phrasing — every mutating action requires an explicit menu selection (the shared anti-escalation rule applies, with this phase-specific tightening).
|
|
@@ -6,16 +6,7 @@
|
|
|
6
6
|
- codex
|
|
7
7
|
- gemini
|
|
8
8
|
- report-writer
|
|
9
|
-
-
|
|
10
|
-
- `Claude lead` is synthesis-only and stays distinct from `Claude worker`
|
|
11
|
-
- required worker roles are `Claude worker`, `Codex worker`, `Gemini worker`, and `Report writer worker`
|
|
12
|
-
- `Report writer worker` is the **author** of the final-report file; `Claude lead` reviews and approves the produced draft and does NOT write the file itself (see `okstra-team-contract` and `okstra-report-writer` for the authoritative contract)
|
|
13
|
-
- default model assignments are resolved from centralized defaults; the fallback values are `Claude lead`/`Report writer worker`=`opus`, `Claude worker`=`sonnet`, `Codex worker`=`gpt-5.5`, `Gemini worker`=`auto`
|
|
14
|
-
- `Gemini worker` must always be attempted for this workflow
|
|
15
|
-
- the final verdict waits until each required worker has either a result or an explicit terminal status
|
|
16
|
-
- unnamed generic parallel workers must not replace the required role roster
|
|
17
|
-
- Tooling — read-only MCP availability:
|
|
18
|
-
- the read-only MCP servers declared in the task brief's `## Available MCP Servers` section may be queried when local schema or sample data clarifies the work category or routing decision; that section is the canonical source of which servers and tools exist for this run, and any MCP-derived finding MUST cite server, table, and the SELECT used
|
|
9
|
+
{{INCLUDE:_common-contract.md}}
|
|
19
10
|
- Primary focus areas:
|
|
20
11
|
- classify the work as bugfix, feature, improvement, refactor, or ops-change
|
|
21
12
|
- determine whether `error-analysis`, `implementation-planning`, or a direct implementation handoff is the next safe step
|
|
@@ -26,18 +17,9 @@
|
|
|
26
17
|
- evidence-backed routing decision
|
|
27
18
|
- uncertainty boundaries and missing inputs
|
|
28
19
|
- next recommended phase and safe resume guidance
|
|
29
|
-
- Clarification request policy:
|
|
20
|
+
- Clarification request policy (phase-specific addenda — shared policy is in `_common-contract.md`):
|
|
30
21
|
- if any blocking input is missing at the time of writing the final report, populate `## 5. Clarification Requests for the Next Run` in `final-report-template.md`
|
|
31
|
-
- section 5 must be split into two distinct sub-sections per the template — `5.1 추가 자료 요청 (Additional Materials Requested)` for files/screenshots/links the user must attach, and `5.2 사용자 확인 질문 (Questions for the User)` for routing or scoping decisions only the user can make. Never mix material requests and decision questions in the same row or list.
|
|
32
22
|
- prefer concrete questions whose answers map directly to a routing decision (`bugfix` vs `feature`, `error-analysis` vs `implementation-planning`, etc.). State each option in plain language with one sentence describing what choosing it would mean for the next phase.
|
|
33
|
-
- write every entry in full, descriptive sentences that a non-developer can act on without further context. Avoid abbreviations and internal jargon. For each material request, state *why* it is needed, *where* the user can find it, and *where* to place it. For each question, state *why* the answer changes the routing decision, *what* is being asked in a complete sentence, and *what shape of answer* is expected (예/아니오, 보기 중 하나, 숫자/날짜, 짧은 서술 등); supply concrete option choices when applicable.
|
|
34
|
-
- the same `final-report.md` file is the canonical artifact carried into the next run; the user appends answers inline before rerunning. The preferred turn-around is `scripts/okstra.sh --resume-clarification --task-key <project-id>:<task-group>:<task-id>` (opens the latest report in `$EDITOR`, then auto-reruns the same phase with `--clarification-response` carry-in). The lower-level form `--clarification-response <path>` remains available for scripted runs.
|
|
35
|
-
- if a clarification response was carried in for this run, reconcile each prior `A*` (material) and `Q*` (question) row in section 0 and update its `Status` (`resolved`, `obsolete`) before issuing the routing decision
|
|
36
|
-
- Authority & permissions assumption (HARD RULE — applies to every okstra task-type):
|
|
37
|
-
- **Assume the user (and their team) holds full authority and every permission required for the anticipated work.** Treat external approvals, third-party access grants, role/IAM permissions, organisational sign-off, legal/compliance review, vendor coordination, and "verify access exists" steps as already satisfied unless the user explicitly states otherwise in the task brief.
|
|
38
|
-
- Do NOT add such items to routing decisions, missing-materials lists, clarification questions, risk lists, dependencies, open questions, or day/effort estimates. They are not legitimate sources of schedule extension.
|
|
39
|
-
- Internal okstra phase handoffs (e.g. the `User Approval Request` block in `implementation-planning`) are unaffected — those are the user themselves approving and proceed without external coordination.
|
|
40
23
|
- Non-goals:
|
|
41
24
|
- full implementation design unless it is required to decide the next phase
|
|
42
25
|
- **source code edits, plan authoring, builds, or deployments** — this run only classifies the work and routes it; deeper analysis and planning belong to subsequent phases
|
|
43
|
-
- treating "다음 단계 진행해" or equivalent user phrases as authorisation to start `error-analysis`, `implementation-planning`, or `implementation` — the next phase begins only in a separate okstra run launched with the new `--task-type`
|
|
@@ -201,7 +201,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
201
201
|
printf 'unknown option: %s\n' "$__unknown_opt" >&2
|
|
202
202
|
case "$__opt_name" in
|
|
203
203
|
--phase|--phases|--lifecycle-phase)
|
|
204
|
-
printf ' hint: --phase is not a valid flag. Use --task-type <requirements-discovery|error-analysis|implementation-planning|implementation|final-verification>.\n' >&2
|
|
204
|
+
printf ' hint: --phase is not a valid flag. Use --task-type <requirements-discovery|error-analysis|implementation-planning|implementation|final-verification|release-handoff>.\n' >&2
|
|
205
205
|
printf ' "phase" is a manifest lifecycle field name, not a CLI option.\n' >&2
|
|
206
206
|
;;
|
|
207
207
|
--brief|--task-brief)
|
|
@@ -415,14 +415,15 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
415
415
|
catalog = _worker_catalog(ctx)
|
|
416
416
|
phase_sequence = [
|
|
417
417
|
"requirements-discovery", "error-analysis", "implementation-planning",
|
|
418
|
-
"implementation", "final-verification",
|
|
418
|
+
"implementation", "final-verification", "release-handoff",
|
|
419
419
|
]
|
|
420
420
|
default_next_phase = {
|
|
421
421
|
"requirements-discovery": "pending-routing-decision",
|
|
422
422
|
"error-analysis": "implementation-planning",
|
|
423
423
|
"implementation-planning": "implementation",
|
|
424
424
|
"implementation": "final-verification",
|
|
425
|
-
"final-verification": "
|
|
425
|
+
"final-verification": "pending-release-handoff",
|
|
426
|
+
"release-handoff": "done-or-follow-up",
|
|
426
427
|
}
|
|
427
428
|
required_worker_roles = _required_worker_roles(ctx, reviewers)
|
|
428
429
|
worker_prompt_paths = {item: catalog[item]["promptPath"] for item in reviewers}
|
|
@@ -809,6 +810,7 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
|
|
|
809
810
|
phase_order = [
|
|
810
811
|
"requirements-discovery", "error-analysis",
|
|
811
812
|
"implementation-planning", "implementation", "final-verification",
|
|
813
|
+
"release-handoff",
|
|
812
814
|
]
|
|
813
815
|
phase_state_lines = [
|
|
814
816
|
f"- `{phase}`: `{phase_states.get(phase, 'not-started')}`"
|
|
@@ -1077,6 +1079,11 @@ def render_template_file(template_path: str, output_path: str, ctx: dict) -> Non
|
|
|
1077
1079
|
"AVAILABLE_MCP_SERVERS",
|
|
1078
1080
|
build_available_mcp_servers_block(Path(ctx.get("PROJECT_ROOT", "."))),
|
|
1079
1081
|
),
|
|
1082
|
+
"{{EXECUTOR_WORKTREE_PATH}}": ctx.get("EXECUTOR_WORKTREE_PATH", ""),
|
|
1083
|
+
"{{EXECUTOR_WORKTREE_BRANCH}}": ctx.get("EXECUTOR_WORKTREE_BRANCH", ""),
|
|
1084
|
+
"{{EXECUTOR_WORKTREE_BASE_REF}}": ctx.get("EXECUTOR_WORKTREE_BASE_REF", ""),
|
|
1085
|
+
"{{EXECUTOR_WORKTREE_STATUS}}": ctx.get("EXECUTOR_WORKTREE_STATUS", ""),
|
|
1086
|
+
"{{EXECUTOR_WORKTREE_NOTE}}": ctx.get("EXECUTOR_WORKTREE_NOTE", ""),
|
|
1080
1087
|
}
|
|
1081
1088
|
rendered = template
|
|
1082
1089
|
for k, v in mapping.items():
|
|
@@ -54,6 +54,7 @@ from .seeding import (
|
|
|
54
54
|
from .session import generate_claude_session_id, write_claude_resume_command_file
|
|
55
55
|
from .workers import normalize_workers, resolve_profile_workers
|
|
56
56
|
from .workflow import compute_workflow_state
|
|
57
|
+
from .worktree import provision_implementation_worktree
|
|
57
58
|
|
|
58
59
|
APPROVED_PLAN_PATTERN = re.compile(
|
|
59
60
|
r"^[ \t]*(?:[-*+][ \t]+)?(APPROVED([ \t]|:|$)|\[x\][ \t]*Approved|"
|
|
@@ -339,6 +340,36 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
|
|
|
339
340
|
return argv
|
|
340
341
|
|
|
341
342
|
|
|
343
|
+
_INCLUDE_DIRECTIVE = re.compile(r"\{\{INCLUDE:([^}]+?)\}\}")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _expand_profile_includes(profile_path: Path, _depth: int = 0) -> str:
|
|
347
|
+
"""Resolve `{{INCLUDE:<name>}}` directives in a profile file.
|
|
348
|
+
|
|
349
|
+
Includes are resolved relative to the profile's directory. A maximum
|
|
350
|
+
recursion depth of 4 prevents accidental cycles; the included file
|
|
351
|
+
contents replace the directive line in-place. Missing include targets
|
|
352
|
+
raise PrepareError so a bad reference fails fast instead of silently
|
|
353
|
+
leaving a `{{INCLUDE:...}}` token in the rendered profile.
|
|
354
|
+
"""
|
|
355
|
+
if _depth > 4:
|
|
356
|
+
raise PrepareError(
|
|
357
|
+
f"profile include recursion exceeded depth 4 while resolving {profile_path}"
|
|
358
|
+
)
|
|
359
|
+
text = profile_path.read_text(encoding="utf-8")
|
|
360
|
+
|
|
361
|
+
def _sub(match: "re.Match[str]") -> str:
|
|
362
|
+
target_name = match.group(1).strip()
|
|
363
|
+
target = profile_path.parent / target_name
|
|
364
|
+
if not target.is_file():
|
|
365
|
+
raise PrepareError(
|
|
366
|
+
f"profile include target missing: {target} (referenced from {profile_path})"
|
|
367
|
+
)
|
|
368
|
+
return _expand_profile_includes(target, _depth + 1).rstrip("\n")
|
|
369
|
+
|
|
370
|
+
return _INCLUDE_DIRECTIVE.sub(_sub, text)
|
|
371
|
+
|
|
372
|
+
|
|
342
373
|
def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
343
374
|
"""Produce a complete okstra task bundle on disk. See module docstring."""
|
|
344
375
|
workspace_root = Path(inp.workspace_root)
|
|
@@ -482,10 +513,34 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
482
513
|
task_type=inp.task_type, run_seq_override=run_seq_override,
|
|
483
514
|
)
|
|
484
515
|
|
|
516
|
+
# ---- executor worktree provisioning (implementation phase only) ----
|
|
517
|
+
# The reports-seq is reused as the worktree-seq so the on-disk worktree
|
|
518
|
+
# path is colocated with the artefacts produced by this run.
|
|
519
|
+
try:
|
|
520
|
+
worktree = provision_implementation_worktree(
|
|
521
|
+
task_type=inp.task_type,
|
|
522
|
+
project_root=project_root,
|
|
523
|
+
project_id=inp.project_id,
|
|
524
|
+
task_group_segment=ctx["TASK_GROUP_SEGMENT"],
|
|
525
|
+
task_id_segment=ctx["TASK_ID_SEGMENT"],
|
|
526
|
+
run_seq=int(ctx["RUN_REPORTS_SEQ"]),
|
|
527
|
+
work_category=inp.work_category,
|
|
528
|
+
)
|
|
529
|
+
except RuntimeError as exc:
|
|
530
|
+
raise PrepareError(f"executor worktree provisioning failed: {exc}") from exc
|
|
531
|
+
|
|
532
|
+
ctx.update({
|
|
533
|
+
"EXECUTOR_WORKTREE_PATH": worktree.path,
|
|
534
|
+
"EXECUTOR_WORKTREE_BRANCH": worktree.branch,
|
|
535
|
+
"EXECUTOR_WORKTREE_BASE_REF": worktree.base_ref,
|
|
536
|
+
"EXECUTOR_WORKTREE_STATUS": worktree.status,
|
|
537
|
+
"EXECUTOR_WORKTREE_NOTE": worktree.note,
|
|
538
|
+
})
|
|
539
|
+
|
|
485
540
|
claude_session_id = "" if inp.render_only else generate_claude_session_id()
|
|
486
541
|
|
|
487
542
|
# ---- material + related-tasks ----
|
|
488
|
-
profile_content = profile_file
|
|
543
|
+
profile_content = _expand_profile_includes(profile_file)
|
|
489
544
|
review_material = build_analysis_material(inp.brief_path, inp.directive)
|
|
490
545
|
related_items = resolve_related_tasks(
|
|
491
546
|
task_manifest_path=Path(ctx["TASK_MANIFEST_FILE"]),
|
|
@@ -577,6 +632,11 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
577
632
|
"EXECUTOR_WORKER_AGENT",
|
|
578
633
|
"EXECUTOR_MODEL_DISPLAY",
|
|
579
634
|
"EXECUTOR_MODEL_EXECUTION_VALUE",
|
|
635
|
+
"EXECUTOR_WORKTREE_PATH",
|
|
636
|
+
"EXECUTOR_WORKTREE_BRANCH",
|
|
637
|
+
"EXECUTOR_WORKTREE_BASE_REF",
|
|
638
|
+
"EXECUTOR_WORKTREE_STATUS",
|
|
639
|
+
"EXECUTOR_WORKTREE_NOTE",
|
|
580
640
|
):
|
|
581
641
|
profile_rendered = profile_rendered.replace("{{" + key + "}}", ctx.get(key, ""))
|
|
582
642
|
(instruction_set / "analysis-profile.md").write_text(profile_rendered, encoding="utf-8")
|
|
@@ -9,7 +9,12 @@ from __future__ import annotations
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
11
|
ALLOWED_WORKERS = ["claude", "codex", "gemini", "report-writer"]
|
|
12
|
-
PROFILE_BULLET_HEADERS = {
|
|
12
|
+
PROFILE_BULLET_HEADERS = {
|
|
13
|
+
"- Workers:",
|
|
14
|
+
"- Required workers:",
|
|
15
|
+
"- Reviewers:",
|
|
16
|
+
"- Analysers:",
|
|
17
|
+
}
|
|
13
18
|
|
|
14
19
|
|
|
15
20
|
class WorkersError(Exception):
|
|
@@ -15,6 +15,7 @@ PHASE_SEQUENCE = [
|
|
|
15
15
|
"implementation-planning",
|
|
16
16
|
"implementation",
|
|
17
17
|
"final-verification",
|
|
18
|
+
"release-handoff",
|
|
18
19
|
]
|
|
19
20
|
|
|
20
21
|
DEFAULT_NEXT_PHASE = {
|
|
@@ -22,7 +23,11 @@ DEFAULT_NEXT_PHASE = {
|
|
|
22
23
|
"error-analysis": "implementation-planning",
|
|
23
24
|
"implementation-planning": "implementation",
|
|
24
25
|
"implementation": "final-verification",
|
|
25
|
-
|
|
26
|
+
# final-verification 의 다음 단계는 verdict 에 따라 갈리므로 정적 매핑은
|
|
27
|
+
# `pending-release-handoff` 로 둔다 (accepted 일 때만 release-handoff 로
|
|
28
|
+
# 진입; 그 외에는 error-analysis / implementation-planning 으로 리라우팅).
|
|
29
|
+
"final-verification": "pending-release-handoff",
|
|
30
|
+
"release-handoff": "done-or-follow-up",
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
# Phase 별 allowed outputs / forbidden actions. bash heredoc 원문 그대로 옮긴 값.
|
|
@@ -109,7 +114,7 @@ PHASE_RULES: dict[str, dict[str, str]] = {
|
|
|
109
114
|
"allowed": (
|
|
110
115
|
" - acceptance verdict with requirement coverage assessment\n"
|
|
111
116
|
" - residual risk and regression notes\n"
|
|
112
|
-
" - recommended follow-up routing (`error-analysis` / `implementation-planning`) for any defects detected"
|
|
117
|
+
" - recommended follow-up routing (`error-analysis` / `implementation-planning` / `release-handoff`) for any defects detected"
|
|
113
118
|
),
|
|
114
119
|
"forbidden": (
|
|
115
120
|
" - source code edits, follow-up bug fixes, or scope expansion\n"
|
|
@@ -117,6 +122,29 @@ PHASE_RULES: dict[str, dict[str, str]] = {
|
|
|
117
122
|
" - starting any follow-up phase inside this run; record findings and end the run"
|
|
118
123
|
),
|
|
119
124
|
},
|
|
125
|
+
"release-handoff": {
|
|
126
|
+
"allowed": (
|
|
127
|
+
" - entering this phase only when the cited final-verification report's verdict is exactly `accepted`\n"
|
|
128
|
+
" - asking the user (via `AskUserQuestion` / interactive prompt) which delivery action to take: `commit only`, `commit + PR`, or `skip` (end the run)\n"
|
|
129
|
+
" - asking the user to pick a PR base branch from `staging` | `preprod` | `prod` | `main` | `dev` | a user-supplied branch name\n"
|
|
130
|
+
" - dispatching the `Claude worker` (drafter) to produce candidate commit message(s) and PR body in markdown; the lead reviews and offers them to the user before any git command runs\n"
|
|
131
|
+
" - local git operations: `git status`, `git diff`, `git log`, `git add`, `git commit -m`\n"
|
|
132
|
+
" - pushing the current feature branch to its origin remote via `git push -u origin <current-branch>` (the feature branch only — NEVER the base branch)\n"
|
|
133
|
+
" - creating a pull request via `gh pr create --base <chosen-base> --head <current-branch>`; if a PR with the same head already exists, surface its URL and skip creation\n"
|
|
134
|
+
" - recording the executed actions, commit SHAs, PR URL, and user selections in the final report"
|
|
135
|
+
),
|
|
136
|
+
"forbidden": (
|
|
137
|
+
" - entering this phase when the cited final-verification verdict is `conditional-accept` or `blocked`, or when no final-verification report is cited\n"
|
|
138
|
+
" - any source-code edit, refactor, or scope expansion beyond what is strictly needed to author commit messages / PR descriptions (the changes themselves are inherited from prior `implementation` runs)\n"
|
|
139
|
+
" - `git push --force`, `git push --force-with-lease`, or any rewriting of remote history\n"
|
|
140
|
+
" - pushing directly to a base branch (`main`, `master`, `prod`, `preprod`, `staging`, `dev`, or any branch the user named as the PR base)\n"
|
|
141
|
+
" - bypassing git hooks (`--no-verify`, `-n`), bypassing GPG signing, or otherwise disabling repo-configured safeguards\n"
|
|
142
|
+
" - release-publishing commands: `gh release`, `npm publish`, `cargo publish`, `pip publish`, `docker push`, `terraform apply`, `kubectl apply` against non-local clusters\n"
|
|
143
|
+
" - executing any command the user did NOT select (e.g. if the user picked `commit only`, opening a PR is forbidden; if the user picked `skip`, the run ends without git commands)\n"
|
|
144
|
+
" - dispatching parallel sub-agents beyond the required worker roster (`Claude worker` drafter + `Report writer worker`)\n"
|
|
145
|
+
" - silently retrying a failed git/gh command with weaker flags (e.g. retrying `git push` with `--force` after a non-fast-forward rejection)"
|
|
146
|
+
),
|
|
147
|
+
},
|
|
120
148
|
}
|
|
121
149
|
|
|
122
150
|
PHASE_RULES_UNKNOWN = {
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Implementation-phase git worktree provisioning.
|
|
2
|
+
|
|
3
|
+
Implementation runs operate on an isolated git worktree rooted under
|
|
4
|
+
`~/.okstra/worktrees/<project_id>/<task_group_segment>/<task_id_segment>-<run_seq>`.
|
|
5
|
+
The executor mutates files there; verifiers read from the same path.
|
|
6
|
+
The worktree is always kept after the run for inspection, manual PR
|
|
7
|
+
authoring, and rollback verification.
|
|
8
|
+
|
|
9
|
+
Pre-conditions handled here:
|
|
10
|
+
- Skip non-`implementation` task-types entirely.
|
|
11
|
+
- Skip when `project_root` itself already sits inside a non-main git
|
|
12
|
+
worktree (the run reuses the caller's working tree).
|
|
13
|
+
- Refuse to clobber an existing path or branch — raise PrepareError.
|
|
14
|
+
|
|
15
|
+
Side effects: `git worktree add -b <branch> <path> <base_ref>` is invoked
|
|
16
|
+
in `project_root`. The function does NOT chdir.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import subprocess
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
OKSTRA_WORKTREES_RELATIVE = Path(".okstra/worktrees")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Project-root directories that hold okstra task state, ignored by git, or
|
|
31
|
+
# otherwise required for the executor to operate but NOT carried across by
|
|
32
|
+
# `git worktree add`. Each is symlinked from project_root into the new
|
|
33
|
+
# worktree at provision time. Symlinks (not copies) so the executor sees
|
|
34
|
+
# the live state and disk/CPU cost stays near zero; the trade-off is that
|
|
35
|
+
# any write through the link reaches the original project_root, which is
|
|
36
|
+
# acceptable because the executor only writes inside its own task-scoped
|
|
37
|
+
# subdirectory (e.g. `.project-docs/okstra/tasks/<task-id>/runs/...`).
|
|
38
|
+
#
|
|
39
|
+
# Override via the `OKSTRA_WORKTREE_SYNC_DIRS` env var: a colon-separated
|
|
40
|
+
# list of project-root-relative paths that REPLACES this default. Use an
|
|
41
|
+
# empty string to disable the feature entirely.
|
|
42
|
+
DEFAULT_WORKTREE_SYNC_DIRS: tuple[str, ...] = (
|
|
43
|
+
".project-docs",
|
|
44
|
+
".scratch",
|
|
45
|
+
"graphify-out",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Work-category → short branch prefix. Mirrors the values accepted by
|
|
50
|
+
# `--work-category` (bugfix / feature / refactor / ops / improvement) and
|
|
51
|
+
# falls back to `task` when the category is unset or unrecognised.
|
|
52
|
+
_WORK_CATEGORY_PREFIX = {
|
|
53
|
+
"feature": "feat",
|
|
54
|
+
"bugfix": "fix",
|
|
55
|
+
"refactor": "refactor",
|
|
56
|
+
"ops": "ops",
|
|
57
|
+
"improvement": "improve",
|
|
58
|
+
"docs": "doc",
|
|
59
|
+
"doc": "doc",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class WorktreeProvision:
|
|
65
|
+
"""Result of `provision_implementation_worktree`.
|
|
66
|
+
|
|
67
|
+
status:
|
|
68
|
+
- "created": fresh worktree at `path` on `branch`
|
|
69
|
+
- "skipped-non-implementation": task-type was not `implementation`
|
|
70
|
+
- "skipped-in-worktree": project_root is already inside a non-main
|
|
71
|
+
worktree; the run reuses `project_root` and no new worktree is
|
|
72
|
+
materialised
|
|
73
|
+
- "skipped-not-git": project_root has no `.git` (worktree path
|
|
74
|
+
cannot be provisioned; degrade gracefully)
|
|
75
|
+
"""
|
|
76
|
+
status: str
|
|
77
|
+
path: str = "" # absolute path of the executor worktree (or project_root when reused)
|
|
78
|
+
branch: str = "" # branch checked out in the worktree (empty when reused / not-git)
|
|
79
|
+
base_ref: str = "" # commit SHA the worktree was branched from (empty when not created)
|
|
80
|
+
note: str = "" # human-readable explanation, surfaced in team-state / manifests
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _work_category_prefix(work_category: str) -> str:
|
|
84
|
+
key = (work_category or "").strip().lower()
|
|
85
|
+
return _WORK_CATEGORY_PREFIX.get(key, "task")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _git(project_root: Path, *args: str) -> subprocess.CompletedProcess:
|
|
89
|
+
return subprocess.run(
|
|
90
|
+
["git", "-C", str(project_root), *args],
|
|
91
|
+
capture_output=True, text=True, check=False,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _is_inside_non_main_worktree(project_root: Path) -> bool:
|
|
96
|
+
"""True iff project_root is inside a git worktree that is NOT the
|
|
97
|
+
repository's main checkout. Detection rule: `--git-dir` (per-worktree
|
|
98
|
+
.git pointer) differs from `--git-common-dir` (shared object store).
|
|
99
|
+
"""
|
|
100
|
+
common = _git(project_root, "rev-parse", "--git-common-dir")
|
|
101
|
+
per_tree = _git(project_root, "rev-parse", "--git-dir")
|
|
102
|
+
if common.returncode != 0 or per_tree.returncode != 0:
|
|
103
|
+
return False
|
|
104
|
+
# Both paths can be relative to project_root; resolve before compare.
|
|
105
|
+
common_abs = (project_root / common.stdout.strip()).resolve()
|
|
106
|
+
per_tree_abs = (project_root / per_tree.stdout.strip()).resolve()
|
|
107
|
+
return common_abs != per_tree_abs
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _is_git_repo(project_root: Path) -> bool:
|
|
111
|
+
res = _git(project_root, "rev-parse", "--is-inside-work-tree")
|
|
112
|
+
return res.returncode == 0 and res.stdout.strip() == "true"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _branch_exists(project_root: Path, branch: str) -> bool:
|
|
116
|
+
res = _git(project_root, "rev-parse", "--verify", "--quiet", f"refs/heads/{branch}")
|
|
117
|
+
return res.returncode == 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _head_sha(project_root: Path) -> str:
|
|
121
|
+
res = _git(project_root, "rev-parse", "HEAD")
|
|
122
|
+
if res.returncode != 0:
|
|
123
|
+
return ""
|
|
124
|
+
return res.stdout.strip()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _resolve_sync_dirs() -> tuple[str, ...]:
|
|
128
|
+
"""Return the list of project-root-relative dirs to symlink into the
|
|
129
|
+
new worktree. Reads `OKSTRA_WORKTREE_SYNC_DIRS` if set (colon-separated,
|
|
130
|
+
empty string disables); otherwise returns the built-in default.
|
|
131
|
+
"""
|
|
132
|
+
raw = os.environ.get("OKSTRA_WORKTREE_SYNC_DIRS")
|
|
133
|
+
if raw is None:
|
|
134
|
+
return DEFAULT_WORKTREE_SYNC_DIRS
|
|
135
|
+
raw = raw.strip()
|
|
136
|
+
if not raw:
|
|
137
|
+
return ()
|
|
138
|
+
return tuple(part for part in (p.strip() for p in raw.split(":")) if part)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _link_sync_dirs(project_root: Path, worktree_path: Path) -> list[str]:
|
|
142
|
+
"""Symlink each configured project-root dir into the new worktree.
|
|
143
|
+
|
|
144
|
+
Skip rules:
|
|
145
|
+
- Source missing in project_root → silently skipped.
|
|
146
|
+
- Target path already exists in worktree (e.g. tracked content
|
|
147
|
+
checked out by `git worktree add`) → skipped to avoid clobbering
|
|
148
|
+
version-controlled files.
|
|
149
|
+
- Parent directories are created as needed for nested entries.
|
|
150
|
+
|
|
151
|
+
Returns a list of human-readable notes (one per linked entry) so the
|
|
152
|
+
caller can include them in the provisioning note.
|
|
153
|
+
"""
|
|
154
|
+
notes: list[str] = []
|
|
155
|
+
for rel in _resolve_sync_dirs():
|
|
156
|
+
src = (project_root / rel).resolve()
|
|
157
|
+
if not src.exists():
|
|
158
|
+
continue
|
|
159
|
+
dst = worktree_path / rel
|
|
160
|
+
if dst.exists() or dst.is_symlink():
|
|
161
|
+
continue
|
|
162
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
os.symlink(src, dst)
|
|
164
|
+
notes.append(rel)
|
|
165
|
+
return notes
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def compute_worktree_path(
|
|
169
|
+
*,
|
|
170
|
+
project_id: str,
|
|
171
|
+
task_group_segment: str,
|
|
172
|
+
task_id_segment: str,
|
|
173
|
+
run_seq: int,
|
|
174
|
+
) -> Path:
|
|
175
|
+
"""Pure path computation. Mirrors `okstra_root` location convention.
|
|
176
|
+
|
|
177
|
+
Uses `OKSTRA_HOME` when set (test hook), else `~/.okstra`.
|
|
178
|
+
"""
|
|
179
|
+
okstra_home = os.environ.get("OKSTRA_HOME", "").strip()
|
|
180
|
+
base = Path(okstra_home) if okstra_home else (Path.home() / ".okstra")
|
|
181
|
+
return (
|
|
182
|
+
base / "worktrees" / project_id
|
|
183
|
+
/ task_group_segment / f"{task_id_segment}-{int(run_seq):03d}"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def compute_branch_name(
|
|
188
|
+
*,
|
|
189
|
+
work_category: str,
|
|
190
|
+
task_id_segment: str,
|
|
191
|
+
run_seq: int,
|
|
192
|
+
) -> str:
|
|
193
|
+
return f"{_work_category_prefix(work_category)}-{task_id_segment}-{int(run_seq):03d}"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def provision_implementation_worktree(
|
|
197
|
+
*,
|
|
198
|
+
task_type: str,
|
|
199
|
+
project_root: Path,
|
|
200
|
+
project_id: str,
|
|
201
|
+
task_group_segment: str,
|
|
202
|
+
task_id_segment: str,
|
|
203
|
+
run_seq: int,
|
|
204
|
+
work_category: str,
|
|
205
|
+
) -> WorktreeProvision:
|
|
206
|
+
"""Materialise (or skip) the executor worktree for this run.
|
|
207
|
+
|
|
208
|
+
The caller passes the same `run_seq` used by the reports/manifests
|
|
209
|
+
artefacts so the worktree directory is colocated by sequence number.
|
|
210
|
+
|
|
211
|
+
Raises:
|
|
212
|
+
PrepareError-like RuntimeError when worktree creation fails
|
|
213
|
+
(path clash, branch clash, `git worktree add` non-zero). The
|
|
214
|
+
caller (`run.py`) catches and re-raises as PrepareError to keep
|
|
215
|
+
a single error surface.
|
|
216
|
+
"""
|
|
217
|
+
if task_type != "implementation":
|
|
218
|
+
return WorktreeProvision(
|
|
219
|
+
status="skipped-non-implementation",
|
|
220
|
+
path=str(project_root),
|
|
221
|
+
note="worktree provisioning skipped: task-type is not 'implementation'",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if not _is_git_repo(project_root):
|
|
225
|
+
return WorktreeProvision(
|
|
226
|
+
status="skipped-not-git",
|
|
227
|
+
path=str(project_root),
|
|
228
|
+
note=(
|
|
229
|
+
"worktree provisioning skipped: project_root is not inside a git "
|
|
230
|
+
"repository; executor will operate directly on project_root"
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if _is_inside_non_main_worktree(project_root):
|
|
235
|
+
return WorktreeProvision(
|
|
236
|
+
status="skipped-in-worktree",
|
|
237
|
+
path=str(project_root),
|
|
238
|
+
note=(
|
|
239
|
+
"worktree provisioning skipped: project_root is already inside a "
|
|
240
|
+
"non-main git worktree; executor reuses the caller's worktree"
|
|
241
|
+
),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
worktree_path = compute_worktree_path(
|
|
245
|
+
project_id=project_id,
|
|
246
|
+
task_group_segment=task_group_segment,
|
|
247
|
+
task_id_segment=task_id_segment,
|
|
248
|
+
run_seq=run_seq,
|
|
249
|
+
)
|
|
250
|
+
branch = compute_branch_name(
|
|
251
|
+
work_category=work_category,
|
|
252
|
+
task_id_segment=task_id_segment,
|
|
253
|
+
run_seq=run_seq,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if worktree_path.exists():
|
|
257
|
+
raise RuntimeError(
|
|
258
|
+
f"executor worktree path already exists: {worktree_path}. "
|
|
259
|
+
"Remove it with `git worktree remove <path>` (or `rm -rf` if it is "
|
|
260
|
+
"not a registered worktree) before retrying this implementation run."
|
|
261
|
+
)
|
|
262
|
+
if _branch_exists(project_root, branch):
|
|
263
|
+
raise RuntimeError(
|
|
264
|
+
f"executor worktree branch already exists: {branch}. "
|
|
265
|
+
"Delete it (`git branch -D <branch>`) or bump OKSTRA_RUN_SEQ_OVERRIDE "
|
|
266
|
+
"before retrying."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
base_ref = _head_sha(project_root)
|
|
270
|
+
if not base_ref:
|
|
271
|
+
raise RuntimeError(
|
|
272
|
+
"could not resolve HEAD sha in project_root; cannot create worktree"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
worktree_path.parent.mkdir(parents=True, exist_ok=True)
|
|
276
|
+
res = _git(
|
|
277
|
+
project_root,
|
|
278
|
+
"worktree", "add", "-b", branch, str(worktree_path), base_ref,
|
|
279
|
+
)
|
|
280
|
+
if res.returncode != 0:
|
|
281
|
+
raise RuntimeError(
|
|
282
|
+
f"`git worktree add` failed (exit={res.returncode}): "
|
|
283
|
+
f"{(res.stderr or res.stdout).strip()}"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
linked = _link_sync_dirs(project_root, worktree_path)
|
|
287
|
+
linked_suffix = f"; linked {', '.join(linked)}" if linked else ""
|
|
288
|
+
|
|
289
|
+
return WorktreeProvision(
|
|
290
|
+
status="created",
|
|
291
|
+
path=str(worktree_path),
|
|
292
|
+
branch=branch,
|
|
293
|
+
base_ref=base_ref,
|
|
294
|
+
note=(
|
|
295
|
+
f"executor worktree created at {worktree_path} on branch {branch} "
|
|
296
|
+
f"(base {base_ref[:12]}){linked_suffix}"
|
|
297
|
+
),
|
|
298
|
+
)
|