okstra 0.7.0 → 0.9.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 +20 -3
- package/README.md +20 -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/bin/okstra.sh +3 -0
- package/runtime/prompts/launch.template.md +11 -0
- package/runtime/prompts/profiles/implementation-planning.md +2 -2
- package/runtime/prompts/profiles/implementation.md +15 -1
- package/runtime/prompts/profiles/release-handoff.md +97 -0
- package/runtime/python/lib/okstra/cli.sh +13 -2
- package/runtime/python/lib/okstra/globals.sh +2 -0
- package/runtime/python/lib/okstra/usage.sh +11 -0
- package/runtime/python/okstra_ctl/render.py +21 -5
- package/runtime/python/okstra_ctl/run.py +135 -8
- package/runtime/python/okstra_ctl/workflow.py +34 -3
- package/runtime/python/okstra_ctl/worktree.py +235 -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 +6 -0
- package/runtime/skills/okstra-run/SKILL.md +2 -1
- package/runtime/skills/okstra-status/SKILL.md +14 -3
- 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 +144 -21
- 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 +136 -4
- package/src/install.mjs +133 -2
- package/src/uninstall.mjs +46 -9
|
@@ -21,6 +21,7 @@ import re
|
|
|
21
21
|
import shutil
|
|
22
22
|
import subprocess
|
|
23
23
|
from dataclasses import dataclass, field
|
|
24
|
+
from datetime import datetime, timezone
|
|
24
25
|
from pathlib import Path
|
|
25
26
|
from typing import Optional
|
|
26
27
|
|
|
@@ -53,9 +54,10 @@ from .seeding import (
|
|
|
53
54
|
from .session import generate_claude_session_id, write_claude_resume_command_file
|
|
54
55
|
from .workers import normalize_workers, resolve_profile_workers
|
|
55
56
|
from .workflow import compute_workflow_state
|
|
57
|
+
from .worktree import provision_implementation_worktree
|
|
56
58
|
|
|
57
59
|
APPROVED_PLAN_PATTERN = re.compile(
|
|
58
|
-
r"^[ \t]*(APPROVED([ \t]|:|$)|\[x\][ \t]*Approved|"
|
|
60
|
+
r"^[ \t]*(?:[-*+][ \t]+)?(APPROVED([ \t]|:|$)|\[x\][ \t]*Approved|"
|
|
59
61
|
r"User[ \t]+Approval[ \t]*:[ \t]*(APPROVED|granted|yes))",
|
|
60
62
|
re.IGNORECASE | re.MULTILINE,
|
|
61
63
|
)
|
|
@@ -83,10 +85,12 @@ class PrepareInputs:
|
|
|
83
85
|
report_writer_model: str = ""
|
|
84
86
|
executor: str = ""
|
|
85
87
|
related_tasks_raw: str = ""
|
|
88
|
+
work_category: str = ""
|
|
86
89
|
approved_plan_path: str = ""
|
|
87
90
|
clarification_response_path: str = "" # absolute or empty
|
|
88
91
|
render_only: bool = False
|
|
89
92
|
refresh_assets: bool = False
|
|
93
|
+
approve_plan_ack: bool = False
|
|
90
94
|
|
|
91
95
|
|
|
92
96
|
@dataclass
|
|
@@ -108,11 +112,65 @@ def _validate_approved_plan(path: str) -> None:
|
|
|
108
112
|
if not APPROVED_PLAN_PATTERN.search(p.read_text(encoding="utf-8", errors="replace")):
|
|
109
113
|
raise PrepareError(
|
|
110
114
|
f"approved plan has no recognised user-approval marker: {path}\n"
|
|
111
|
-
'
|
|
112
|
-
'
|
|
115
|
+
' canonical form (single line, top-of-report block): "- [x] Approved"\n'
|
|
116
|
+
' also accepted (case-insensitive, line-anchored, optional leading bullet): '
|
|
117
|
+
'"APPROVED", "[x] Approved", "User Approval: APPROVED|granted|yes"\n'
|
|
118
|
+
" shortcut: re-run okstra with --approve to have the CLI itself "
|
|
119
|
+
"record the approval marker on this file."
|
|
113
120
|
)
|
|
114
121
|
|
|
115
122
|
|
|
123
|
+
# `- [ ] Approved` 라인을 정확히 한 번만 매치한다. 좌측 leading whitespace 와
|
|
124
|
+
# 옵션 bullet 은 보존된 채 체크박스 안쪽 공백만 `x` 로 갱신된다.
|
|
125
|
+
APPROVAL_UNCHECKED_PATTERN = re.compile(
|
|
126
|
+
r"^([ \t]*(?:[-*+][ \t]+)?)\[[ \t]\][ \t]*Approved[ \t]*$",
|
|
127
|
+
re.IGNORECASE | re.MULTILINE,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _apply_cli_approval(path: str) -> str:
|
|
132
|
+
"""`--approve` 가 지정된 경우 approved-plan 파일에 사용자 승인 마커를 새겨 넣는다.
|
|
133
|
+
|
|
134
|
+
Returns a short human-readable description of the action taken (used in the
|
|
135
|
+
runtime audit line). Idempotent: if the file already carries a valid
|
|
136
|
+
approval marker, no edits are written and `"already-approved"` is returned.
|
|
137
|
+
"""
|
|
138
|
+
p = Path(path)
|
|
139
|
+
if not p.is_file():
|
|
140
|
+
raise PrepareError(f"approved plan file not found: {path}")
|
|
141
|
+
body = p.read_text(encoding="utf-8", errors="replace")
|
|
142
|
+
|
|
143
|
+
audit_iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
144
|
+
audit_line = (
|
|
145
|
+
f"- 승인 일시 (CLI ack): {audit_iso} — recorded by `okstra --approve` "
|
|
146
|
+
"(user CLI invocation treated as approval signal)"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if APPROVED_PLAN_PATTERN.search(body):
|
|
150
|
+
# 이미 사용자(또는 이전 --approve 호출)가 마커를 남긴 상태. audit 라인이
|
|
151
|
+
# 없으면 보조적으로 한 줄만 추가하고 마커 자체는 건드리지 않는다.
|
|
152
|
+
if audit_line.split(" — ")[1] in body:
|
|
153
|
+
return "already-approved"
|
|
154
|
+
new_body = body.rstrip("\n") + "\n" + audit_line + "\n"
|
|
155
|
+
p.write_text(new_body, encoding="utf-8")
|
|
156
|
+
return "already-approved-audit-appended"
|
|
157
|
+
|
|
158
|
+
if APPROVAL_UNCHECKED_PATTERN.search(body):
|
|
159
|
+
new_body, count = APPROVAL_UNCHECKED_PATTERN.subn(
|
|
160
|
+
lambda m: f"{m.group(1)}[x] Approved", body, count=1,
|
|
161
|
+
)
|
|
162
|
+
new_body = new_body.rstrip("\n") + "\n" + audit_line + "\n"
|
|
163
|
+
p.write_text(new_body, encoding="utf-8")
|
|
164
|
+
return "checkbox-flipped"
|
|
165
|
+
|
|
166
|
+
raise PrepareError(
|
|
167
|
+
f"--approve was given but the approved-plan file has no `User Approval Request` "
|
|
168
|
+
f"checkbox to flip: {path}\n"
|
|
169
|
+
" expected a line of the form `- [ ] Approved` near the top of the report "
|
|
170
|
+
"(see templates/reports/final-report.template.md)."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
116
174
|
def _ensure_task_directories(ctx: dict) -> None:
|
|
117
175
|
for key in (
|
|
118
176
|
"TASK_ROOT", "INSTRUCTION_SET_DIR", "RUNS_DIR", "HISTORY_DIR",
|
|
@@ -268,6 +326,7 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
|
|
|
268
326
|
("--report-writer-model", inp.report_writer_model or ctx.get("REPORT_WRITER_MODEL_DISPLAY", "")),
|
|
269
327
|
("--executor", inp.executor or ctx.get("EXECUTOR_PROVIDER", "")),
|
|
270
328
|
("--related-tasks", inp.related_tasks_raw),
|
|
329
|
+
("--work-category", inp.work_category),
|
|
271
330
|
]
|
|
272
331
|
argv: list[str] = []
|
|
273
332
|
for flag, val in pairs:
|
|
@@ -330,7 +389,19 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
330
389
|
raise PrepareError(
|
|
331
390
|
"task-type implementation requires --approved-plan <path-to-final-report.md>"
|
|
332
391
|
)
|
|
392
|
+
if inp.approve_plan_ack:
|
|
393
|
+
# 사용자가 직접 `--approve` 를 입력한 행위 자체를 승인 의사로 모델링한다.
|
|
394
|
+
# 파일을 먼저 갱신한 뒤 동일한 검증 경로를 그대로 통과시킨다 — 검증
|
|
395
|
+
# 책임을 단일 지점(`_validate_approved_plan`)으로 유지한다.
|
|
396
|
+
_apply_cli_approval(inp.approved_plan_path)
|
|
333
397
|
_validate_approved_plan(inp.approved_plan_path)
|
|
398
|
+
elif inp.approve_plan_ack:
|
|
399
|
+
# implementation 외 task-type 에서 `--approve` 는 의미가 없다. 사용자에게
|
|
400
|
+
# 정확한 시점을 알려주기 위해 조용히 무시하지 않고 즉시 거부한다.
|
|
401
|
+
raise PrepareError(
|
|
402
|
+
"--approve is only meaningful with --task-type implementation "
|
|
403
|
+
"and --approved-plan <path>"
|
|
404
|
+
)
|
|
334
405
|
if inp.clarification_response_path and not Path(inp.clarification_response_path).is_file():
|
|
335
406
|
raise PrepareError(
|
|
336
407
|
f"clarification response file not found: {inp.clarification_response_path}"
|
|
@@ -412,6 +483,30 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
412
483
|
task_type=inp.task_type, run_seq_override=run_seq_override,
|
|
413
484
|
)
|
|
414
485
|
|
|
486
|
+
# ---- executor worktree provisioning (implementation phase only) ----
|
|
487
|
+
# The reports-seq is reused as the worktree-seq so the on-disk worktree
|
|
488
|
+
# path is colocated with the artefacts produced by this run.
|
|
489
|
+
try:
|
|
490
|
+
worktree = provision_implementation_worktree(
|
|
491
|
+
task_type=inp.task_type,
|
|
492
|
+
project_root=project_root,
|
|
493
|
+
project_id=inp.project_id,
|
|
494
|
+
task_group_segment=ctx["TASK_GROUP_SEGMENT"],
|
|
495
|
+
task_id_segment=ctx["TASK_ID_SEGMENT"],
|
|
496
|
+
run_seq=int(ctx["RUN_REPORTS_SEQ"]),
|
|
497
|
+
work_category=inp.work_category,
|
|
498
|
+
)
|
|
499
|
+
except RuntimeError as exc:
|
|
500
|
+
raise PrepareError(f"executor worktree provisioning failed: {exc}") from exc
|
|
501
|
+
|
|
502
|
+
ctx.update({
|
|
503
|
+
"EXECUTOR_WORKTREE_PATH": worktree.path,
|
|
504
|
+
"EXECUTOR_WORKTREE_BRANCH": worktree.branch,
|
|
505
|
+
"EXECUTOR_WORKTREE_BASE_REF": worktree.base_ref,
|
|
506
|
+
"EXECUTOR_WORKTREE_STATUS": worktree.status,
|
|
507
|
+
"EXECUTOR_WORKTREE_NOTE": worktree.note,
|
|
508
|
+
})
|
|
509
|
+
|
|
415
510
|
claude_session_id = "" if inp.render_only else generate_claude_session_id()
|
|
416
511
|
|
|
417
512
|
# ---- material + related-tasks ----
|
|
@@ -487,11 +582,15 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
487
582
|
cleanup_obsolete_generated_docs(
|
|
488
583
|
project_root=project_root, instruction_set_dir=Path(ctx["INSTRUCTION_SET_DIR"]),
|
|
489
584
|
)
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
585
|
+
# Always materialise the resume command script. Even in --render-only
|
|
586
|
+
# preparation flows the user (or a later non-interactive runner) may
|
|
587
|
+
# invoke it manually; deferring its creation until interactive launch
|
|
588
|
+
# leaves runs/<phase>/sessions/ empty and the manifest pointing at a
|
|
589
|
+
# path that does not exist.
|
|
590
|
+
write_claude_resume_command_file(
|
|
591
|
+
resume_command_path=Path(ctx["CLAUDE_RESUME_COMMAND_FILE"]),
|
|
592
|
+
project_root=project_root, claude_session_id=claude_session_id,
|
|
593
|
+
)
|
|
495
594
|
|
|
496
595
|
# ---- write instruction-set scaffolding ----
|
|
497
596
|
instruction_set = Path(ctx["INSTRUCTION_SET_DIR"])
|
|
@@ -503,6 +602,11 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
503
602
|
"EXECUTOR_WORKER_AGENT",
|
|
504
603
|
"EXECUTOR_MODEL_DISPLAY",
|
|
505
604
|
"EXECUTOR_MODEL_EXECUTION_VALUE",
|
|
605
|
+
"EXECUTOR_WORKTREE_PATH",
|
|
606
|
+
"EXECUTOR_WORKTREE_BRANCH",
|
|
607
|
+
"EXECUTOR_WORKTREE_BASE_REF",
|
|
608
|
+
"EXECUTOR_WORKTREE_STATUS",
|
|
609
|
+
"EXECUTOR_WORKTREE_NOTE",
|
|
506
610
|
):
|
|
507
611
|
profile_rendered = profile_rendered.replace("{{" + key + "}}", ctx.get(key, ""))
|
|
508
612
|
(instruction_set / "analysis-profile.md").write_text(profile_rendered, encoding="utf-8")
|
|
@@ -569,6 +673,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
569
673
|
current_run_status=ctx["CURRENT_RUN_STATUS"],
|
|
570
674
|
current_task_status=ctx["CURRENT_TASK_STATUS"],
|
|
571
675
|
render_only=inp.render_only,
|
|
676
|
+
work_category=inp.work_category,
|
|
572
677
|
))
|
|
573
678
|
render_team_state(ctx["TEAM_STATE_FILE"], ctx)
|
|
574
679
|
render_task_manifest(ctx["TASK_MANIFEST_FILE"], ctx)
|
|
@@ -639,9 +744,29 @@ def main(argv: list[str]) -> int:
|
|
|
639
744
|
p.add_argument("--executor", default="")
|
|
640
745
|
p.add_argument("--related-tasks", default="", dest="related_tasks_raw")
|
|
641
746
|
p.add_argument("--approved-plan", default="", dest="approved_plan_path")
|
|
747
|
+
p.add_argument(
|
|
748
|
+
"--approve",
|
|
749
|
+
action="store_true",
|
|
750
|
+
dest="approve_plan_ack",
|
|
751
|
+
help=(
|
|
752
|
+
"Treat the CLI invocation itself as the plan approval signal. "
|
|
753
|
+
"Flips `- [ ] Approved` to `- [x] Approved` in the --approved-plan file "
|
|
754
|
+
"and appends an audit line."
|
|
755
|
+
),
|
|
756
|
+
)
|
|
642
757
|
p.add_argument("--clarification-response", default="", dest="clarification_response_path")
|
|
643
758
|
p.add_argument("--render-only", action="store_true", dest="render_only")
|
|
644
759
|
p.add_argument("--refresh-assets", action="store_true", dest="refresh_assets")
|
|
760
|
+
p.add_argument(
|
|
761
|
+
"--work-category",
|
|
762
|
+
default="",
|
|
763
|
+
dest="work_category",
|
|
764
|
+
help=(
|
|
765
|
+
"Work-category classification for this task "
|
|
766
|
+
"(bugfix / feature / refactor / ops / improvement). "
|
|
767
|
+
"Falls back to `unknown` when omitted."
|
|
768
|
+
),
|
|
769
|
+
)
|
|
645
770
|
args = p.parse_args(argv)
|
|
646
771
|
|
|
647
772
|
project_root = Path(args.project_root).expanduser().resolve()
|
|
@@ -677,10 +802,12 @@ def main(argv: list[str]) -> int:
|
|
|
677
802
|
report_writer_model=args.report_writer_model,
|
|
678
803
|
executor=args.executor,
|
|
679
804
|
related_tasks_raw=args.related_tasks_raw,
|
|
805
|
+
work_category=args.work_category,
|
|
680
806
|
approved_plan_path=args.approved_plan_path,
|
|
681
807
|
clarification_response_path=clarification_abs,
|
|
682
808
|
render_only=args.render_only,
|
|
683
809
|
refresh_assets=args.refresh_assets,
|
|
810
|
+
approve_plan_ack=args.approve_plan_ack,
|
|
684
811
|
)
|
|
685
812
|
try:
|
|
686
813
|
out = prepare_task_bundle(inputs)
|
|
@@ -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 = {
|
|
@@ -138,6 +166,7 @@ def compute_workflow_state(
|
|
|
138
166
|
current_run_status: str,
|
|
139
167
|
current_task_status: str,
|
|
140
168
|
render_only: bool,
|
|
169
|
+
work_category: str = "",
|
|
141
170
|
) -> dict:
|
|
142
171
|
"""WORKFLOW_* + PHASE_* 값을 dict 로 돌려준다."""
|
|
143
172
|
if current_run_status == "in-progress":
|
|
@@ -170,8 +199,10 @@ def compute_workflow_state(
|
|
|
170
199
|
rules = PHASE_RULES.get(task_type, PHASE_RULES_UNKNOWN)
|
|
171
200
|
last_completed = task_type if current_run_status == "completed" else ""
|
|
172
201
|
|
|
202
|
+
resolved_work_category = (work_category or "").strip() or "unknown"
|
|
203
|
+
|
|
173
204
|
return {
|
|
174
|
-
"WORKFLOW_WORK_CATEGORY":
|
|
205
|
+
"WORKFLOW_WORK_CATEGORY": resolved_work_category,
|
|
175
206
|
"WORKFLOW_CURRENT_PHASE": task_type,
|
|
176
207
|
"WORKFLOW_CURRENT_PHASE_STATE": phase_state,
|
|
177
208
|
"WORKFLOW_NEXT_RECOMMENDED_PHASE": default_next_phase_for(task_type),
|
|
@@ -0,0 +1,235 @@
|
|
|
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
|
+
# Work-category → short branch prefix. Mirrors the values accepted by
|
|
31
|
+
# `--work-category` (bugfix / feature / refactor / ops / improvement) and
|
|
32
|
+
# falls back to `task` when the category is unset or unrecognised.
|
|
33
|
+
_WORK_CATEGORY_PREFIX = {
|
|
34
|
+
"feature": "feat",
|
|
35
|
+
"bugfix": "fix",
|
|
36
|
+
"refactor": "refactor",
|
|
37
|
+
"ops": "ops",
|
|
38
|
+
"improvement": "improve",
|
|
39
|
+
"docs": "doc",
|
|
40
|
+
"doc": "doc",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class WorktreeProvision:
|
|
46
|
+
"""Result of `provision_implementation_worktree`.
|
|
47
|
+
|
|
48
|
+
status:
|
|
49
|
+
- "created": fresh worktree at `path` on `branch`
|
|
50
|
+
- "skipped-non-implementation": task-type was not `implementation`
|
|
51
|
+
- "skipped-in-worktree": project_root is already inside a non-main
|
|
52
|
+
worktree; the run reuses `project_root` and no new worktree is
|
|
53
|
+
materialised
|
|
54
|
+
- "skipped-not-git": project_root has no `.git` (worktree path
|
|
55
|
+
cannot be provisioned; degrade gracefully)
|
|
56
|
+
"""
|
|
57
|
+
status: str
|
|
58
|
+
path: str = "" # absolute path of the executor worktree (or project_root when reused)
|
|
59
|
+
branch: str = "" # branch checked out in the worktree (empty when reused / not-git)
|
|
60
|
+
base_ref: str = "" # commit SHA the worktree was branched from (empty when not created)
|
|
61
|
+
note: str = "" # human-readable explanation, surfaced in team-state / manifests
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _work_category_prefix(work_category: str) -> str:
|
|
65
|
+
key = (work_category or "").strip().lower()
|
|
66
|
+
return _WORK_CATEGORY_PREFIX.get(key, "task")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _git(project_root: Path, *args: str) -> subprocess.CompletedProcess:
|
|
70
|
+
return subprocess.run(
|
|
71
|
+
["git", "-C", str(project_root), *args],
|
|
72
|
+
capture_output=True, text=True, check=False,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _is_inside_non_main_worktree(project_root: Path) -> bool:
|
|
77
|
+
"""True iff project_root is inside a git worktree that is NOT the
|
|
78
|
+
repository's main checkout. Detection rule: `--git-dir` (per-worktree
|
|
79
|
+
.git pointer) differs from `--git-common-dir` (shared object store).
|
|
80
|
+
"""
|
|
81
|
+
common = _git(project_root, "rev-parse", "--git-common-dir")
|
|
82
|
+
per_tree = _git(project_root, "rev-parse", "--git-dir")
|
|
83
|
+
if common.returncode != 0 or per_tree.returncode != 0:
|
|
84
|
+
return False
|
|
85
|
+
# Both paths can be relative to project_root; resolve before compare.
|
|
86
|
+
common_abs = (project_root / common.stdout.strip()).resolve()
|
|
87
|
+
per_tree_abs = (project_root / per_tree.stdout.strip()).resolve()
|
|
88
|
+
return common_abs != per_tree_abs
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _is_git_repo(project_root: Path) -> bool:
|
|
92
|
+
res = _git(project_root, "rev-parse", "--is-inside-work-tree")
|
|
93
|
+
return res.returncode == 0 and res.stdout.strip() == "true"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _branch_exists(project_root: Path, branch: str) -> bool:
|
|
97
|
+
res = _git(project_root, "rev-parse", "--verify", "--quiet", f"refs/heads/{branch}")
|
|
98
|
+
return res.returncode == 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _head_sha(project_root: Path) -> str:
|
|
102
|
+
res = _git(project_root, "rev-parse", "HEAD")
|
|
103
|
+
if res.returncode != 0:
|
|
104
|
+
return ""
|
|
105
|
+
return res.stdout.strip()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def compute_worktree_path(
|
|
109
|
+
*,
|
|
110
|
+
project_id: str,
|
|
111
|
+
task_group_segment: str,
|
|
112
|
+
task_id_segment: str,
|
|
113
|
+
run_seq: int,
|
|
114
|
+
) -> Path:
|
|
115
|
+
"""Pure path computation. Mirrors `okstra_root` location convention.
|
|
116
|
+
|
|
117
|
+
Uses `OKSTRA_HOME` when set (test hook), else `~/.okstra`.
|
|
118
|
+
"""
|
|
119
|
+
okstra_home = os.environ.get("OKSTRA_HOME", "").strip()
|
|
120
|
+
base = Path(okstra_home) if okstra_home else (Path.home() / ".okstra")
|
|
121
|
+
return (
|
|
122
|
+
base / "worktrees" / project_id
|
|
123
|
+
/ task_group_segment / f"{task_id_segment}-{int(run_seq):03d}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def compute_branch_name(
|
|
128
|
+
*,
|
|
129
|
+
work_category: str,
|
|
130
|
+
task_id_segment: str,
|
|
131
|
+
run_seq: int,
|
|
132
|
+
) -> str:
|
|
133
|
+
return f"{_work_category_prefix(work_category)}-{task_id_segment}-{int(run_seq):03d}"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def provision_implementation_worktree(
|
|
137
|
+
*,
|
|
138
|
+
task_type: str,
|
|
139
|
+
project_root: Path,
|
|
140
|
+
project_id: str,
|
|
141
|
+
task_group_segment: str,
|
|
142
|
+
task_id_segment: str,
|
|
143
|
+
run_seq: int,
|
|
144
|
+
work_category: str,
|
|
145
|
+
) -> WorktreeProvision:
|
|
146
|
+
"""Materialise (or skip) the executor worktree for this run.
|
|
147
|
+
|
|
148
|
+
The caller passes the same `run_seq` used by the reports/manifests
|
|
149
|
+
artefacts so the worktree directory is colocated by sequence number.
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
PrepareError-like RuntimeError when worktree creation fails
|
|
153
|
+
(path clash, branch clash, `git worktree add` non-zero). The
|
|
154
|
+
caller (`run.py`) catches and re-raises as PrepareError to keep
|
|
155
|
+
a single error surface.
|
|
156
|
+
"""
|
|
157
|
+
if task_type != "implementation":
|
|
158
|
+
return WorktreeProvision(
|
|
159
|
+
status="skipped-non-implementation",
|
|
160
|
+
path=str(project_root),
|
|
161
|
+
note="worktree provisioning skipped: task-type is not 'implementation'",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if not _is_git_repo(project_root):
|
|
165
|
+
return WorktreeProvision(
|
|
166
|
+
status="skipped-not-git",
|
|
167
|
+
path=str(project_root),
|
|
168
|
+
note=(
|
|
169
|
+
"worktree provisioning skipped: project_root is not inside a git "
|
|
170
|
+
"repository; executor will operate directly on project_root"
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if _is_inside_non_main_worktree(project_root):
|
|
175
|
+
return WorktreeProvision(
|
|
176
|
+
status="skipped-in-worktree",
|
|
177
|
+
path=str(project_root),
|
|
178
|
+
note=(
|
|
179
|
+
"worktree provisioning skipped: project_root is already inside a "
|
|
180
|
+
"non-main git worktree; executor reuses the caller's worktree"
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
worktree_path = compute_worktree_path(
|
|
185
|
+
project_id=project_id,
|
|
186
|
+
task_group_segment=task_group_segment,
|
|
187
|
+
task_id_segment=task_id_segment,
|
|
188
|
+
run_seq=run_seq,
|
|
189
|
+
)
|
|
190
|
+
branch = compute_branch_name(
|
|
191
|
+
work_category=work_category,
|
|
192
|
+
task_id_segment=task_id_segment,
|
|
193
|
+
run_seq=run_seq,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if worktree_path.exists():
|
|
197
|
+
raise RuntimeError(
|
|
198
|
+
f"executor worktree path already exists: {worktree_path}. "
|
|
199
|
+
"Remove it with `git worktree remove <path>` (or `rm -rf` if it is "
|
|
200
|
+
"not a registered worktree) before retrying this implementation run."
|
|
201
|
+
)
|
|
202
|
+
if _branch_exists(project_root, branch):
|
|
203
|
+
raise RuntimeError(
|
|
204
|
+
f"executor worktree branch already exists: {branch}. "
|
|
205
|
+
"Delete it (`git branch -D <branch>`) or bump OKSTRA_RUN_SEQ_OVERRIDE "
|
|
206
|
+
"before retrying."
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
base_ref = _head_sha(project_root)
|
|
210
|
+
if not base_ref:
|
|
211
|
+
raise RuntimeError(
|
|
212
|
+
"could not resolve HEAD sha in project_root; cannot create worktree"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
worktree_path.parent.mkdir(parents=True, exist_ok=True)
|
|
216
|
+
res = _git(
|
|
217
|
+
project_root,
|
|
218
|
+
"worktree", "add", "-b", branch, str(worktree_path), base_ref,
|
|
219
|
+
)
|
|
220
|
+
if res.returncode != 0:
|
|
221
|
+
raise RuntimeError(
|
|
222
|
+
f"`git worktree add` failed (exit={res.returncode}): "
|
|
223
|
+
f"{(res.stderr or res.stdout).strip()}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return WorktreeProvision(
|
|
227
|
+
status="created",
|
|
228
|
+
path=str(worktree_path),
|
|
229
|
+
branch=branch,
|
|
230
|
+
base_ref=base_ref,
|
|
231
|
+
note=(
|
|
232
|
+
f"executor worktree created at {worktree_path} on branch {branch} "
|
|
233
|
+
f"(base {base_ref[:12]})"
|
|
234
|
+
),
|
|
235
|
+
)
|
|
@@ -40,7 +40,7 @@ task-manifest.json is the canonical metadata source. The following fields must b
|
|
|
40
40
|
| `projectId` | Project ID |
|
|
41
41
|
| `taskGroup` | Task group |
|
|
42
42
|
| `taskId` | Task ID |
|
|
43
|
-
| `taskType` | Analysis type (requirements-discovery, error-analysis, final-verification,
|
|
43
|
+
| `taskType` | Analysis type (requirements-discovery, error-analysis, implementation-planning, implementation, final-verification, release-handoff) |
|
|
44
44
|
| `workCategory` | bugfix / feature / improvement / refactor / ops-change / unknown |
|
|
45
45
|
| `recommendedWorkers` | List of selected workers |
|
|
46
46
|
| `currentStatus` | Current task status |
|
|
@@ -37,12 +37,18 @@ Configure this in the `convergence` block of `task-manifest.json`. If the block
|
|
|
37
37
|
|
|
38
38
|
Read the worker result files generated in Phase 4/5 and extract individual findings.
|
|
39
39
|
|
|
40
|
-
1. In the "Findings" section of each worker's results, identify individual items by number (F-001, F-002, ...)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
1. In the "Findings" section of each worker's results, identify individual items by number (F-001, F-002, ...) and parse the ticket identifier attached to each item:
|
|
41
|
+
- For table-form findings, read the `Ticket ID` column.
|
|
42
|
+
- For bullet/numbered findings, parse `[TICKETID: <id>]` from the item title.
|
|
43
|
+
- Items with multiple tickets (e.g. `TICKET-123, TICKET-456`) expand to a set of ticket keys.
|
|
44
|
+
- Items tagged `unknown` keep the literal `unknown` as their ticket key.
|
|
45
|
+
2. For each finding, record the summary, evidence (file path, line number, basis), the worker who identified it, and the parsed ticket set.
|
|
46
|
+
3. Claude Lead groups findings based on semantic similarity AND ticket-set equality:
|
|
47
|
+
- Same semantics + same ticket set across 2+ workers → immediately reach `full consensus`.
|
|
48
|
+
- Same semantics but disjoint ticket sets → keep as separate groups (do NOT over-merge across tickets).
|
|
49
|
+
- Only one worker confirms a finding → `unique`, enter the verification queue.
|
|
45
50
|
4. When grouping is ambiguous, prefer splitting over merging (avoid over-merging).
|
|
51
|
+
5. Persist each finding's ticket set in the convergence state artifact under a `ticketIds` field on the finding record. Re-verification rounds carry the same field forward.
|
|
46
52
|
|
|
47
53
|
### Round 1-N: Re-verification Loop
|
|
48
54
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: okstra-report-finder
|
|
3
3
|
description: Use when the user provides a task key and needs to find the final report path, or wants to read a previous okstra report to continue work based on its findings. Trigger words include "find report", "show report for", "read the okstra report", "continue from report".
|
|
4
|
+
user-invocable: false
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# OKSTRA Report Finder
|
|
@@ -172,6 +172,12 @@ The Korean translation in parentheses is optional but the English keyword is man
|
|
|
172
172
|
|
|
173
173
|
The final-report template `okstra-final-report.template.md` Section 4.5 already encodes this contract — copy that block verbatim and fill in.
|
|
174
174
|
|
|
175
|
+
### Release-handoff section contract (release-handoff runs only)
|
|
176
|
+
|
|
177
|
+
When the run's `task-type` is `release-handoff`, the final report MUST include Section `## 4.6 Release Handoff Deliverables` with all seven sub-sections (`4.6.1` Source Verification Report, `4.6.2` Feature Branch & Working-Tree State, `4.6.3` User Selections, `4.6.4` Executed Commands, `4.6.5` Commit List, `4.6.6` Pull Request Outcome, `4.6.7` Routing Recommendation). The drafter does **not** invent values for these sub-sections — every entry is dictated by the lead's recorded git/gh command log and the user's verbatim answers to the H1/H2/H3 menu prompts. If the user picked `skip` (H1) or `cancel` (H3), keep 4.6.3 populated but leave 4.6.4–4.6.6 explicitly empty per the template's empty-state lines.
|
|
178
|
+
|
|
179
|
+
The final-report template `okstra-final-report.template.md` Section 4.6 already encodes this contract — copy that block verbatim and fill in. For non-`release-handoff` runs, omit Section 4.6 entirely.
|
|
180
|
+
|
|
175
181
|
### Mandatory worker-results file (BLOCKING)
|
|
176
182
|
|
|
177
183
|
You (the report-writer-worker subagent) MUST also write a worker-results audit file at the path the lead provides as `**Worker Result Path:**`, defaulting to:
|
|
@@ -116,7 +116,7 @@ Validate that slugified `task_group` and `task_id` each contain at least one alp
|
|
|
116
116
|
|
|
117
117
|
## Step 4: Choose task-type
|
|
118
118
|
|
|
119
|
-
`AskUserQuestion` with
|
|
119
|
+
`AskUserQuestion` with six fixed options:
|
|
120
120
|
|
|
121
121
|
| Option | Description |
|
|
122
122
|
|---|---|
|
|
@@ -125,6 +125,7 @@ Validate that slugified `task_group` and `task_id` each contain at least one alp
|
|
|
125
125
|
| `implementation-planning` | Plan options + request user approval |
|
|
126
126
|
| `implementation` | Execute approved plan (requires `--approved-plan`) |
|
|
127
127
|
| `final-verification` | Acceptance + residual-risk review |
|
|
128
|
+
| `release-handoff` | Drive commit / push / PR with user-selected actions after `accepted` final-verification |
|
|
128
129
|
|
|
129
130
|
For existing tasks, present `nextRecommendedPhase` as the first option (recommended default).
|
|
130
131
|
|