okstra 0.50.0 → 0.51.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 +8 -7
- package/README.md +8 -7
- package/bin/okstra +2 -0
- package/docs/kr/architecture.md +15 -16
- package/docs/kr/cli.md +5 -5
- package/docs/project-structure-overview.md +10 -6
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +15 -11
- package/runtime/agents/workers/claude-worker.md +3 -3
- package/runtime/agents/workers/codex-worker.md +2 -2
- package/runtime/agents/workers/gemini-worker.md +2 -2
- package/runtime/bin/lib/okstra/cli.sh +8 -1
- package/runtime/bin/lib/okstra/globals.sh +3 -0
- package/runtime/bin/lib/okstra/interactive.sh +14 -12
- package/runtime/bin/lib/okstra/usage.sh +6 -0
- package/runtime/bin/okstra-team-reconcile.sh +28 -0
- package/runtime/bin/okstra.sh +2 -0
- package/runtime/prompts/launch.template.md +3 -1
- package/runtime/prompts/profiles/_common-contract.md +4 -4
- package/runtime/prompts/profiles/_implementation-executor.md +2 -2
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +1 -0
- package/runtime/prompts/profiles/implementation.md +1 -1
- package/runtime/python/okstra_ctl/analysis_packet.py +259 -0
- package/runtime/python/okstra_ctl/context_cost.py +308 -0
- package/runtime/python/okstra_ctl/migrate.py +2 -12
- package/runtime/python/okstra_ctl/paths.py +22 -0
- package/runtime/python/okstra_ctl/render.py +284 -125
- package/runtime/python/okstra_ctl/render_final_report.py +31 -0
- package/runtime/python/okstra_ctl/run.py +507 -245
- package/runtime/python/okstra_ctl/sequence.py +2 -5
- package/runtime/python/okstra_ctl/team_reconcile.py +131 -0
- package/runtime/python/okstra_ctl/wizard.py +129 -133
- package/runtime/python/okstra_ctl/worktree.py +13 -5
- package/runtime/schemas/final-report-v1.0.schema.json +4 -0
- package/runtime/skills/okstra-coding-preflight/SKILL.md +69 -0
- package/runtime/skills/okstra-coding-preflight/architecture/hexagonal.md +116 -0
- package/runtime/skills/okstra-coding-preflight/clean-code.md +254 -0
- package/runtime/skills/okstra-coding-preflight/languages/java.md +64 -0
- package/runtime/skills/okstra-coding-preflight/languages/javascript-typescript.md +87 -0
- package/runtime/skills/okstra-coding-preflight/languages/kotlin.md +69 -0
- package/runtime/skills/okstra-coding-preflight/languages/nodejs.md +66 -0
- package/runtime/skills/okstra-coding-preflight/languages/python.md +179 -0
- package/runtime/skills/okstra-coding-preflight/languages/rust.md +105 -0
- package/runtime/skills/okstra-coding-preflight/languages/sql.md +68 -0
- package/runtime/skills/okstra-context-loader/SKILL.md +12 -6
- package/runtime/skills/okstra-inspect/SKILL.md +100 -1
- package/runtime/skills/okstra-report-writer/SKILL.md +5 -1
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/skills/okstra-team-contract/SKILL.md +7 -4
- package/runtime/templates/reports/final-report.template.md +1 -0
- package/runtime/templates/worker-prompt-preamble.md +3 -3
- package/src/_python-helper.mjs +3 -3
- package/src/context-cost.mjs +27 -0
- package/src/install.mjs +1 -0
- package/src/uninstall.mjs +1 -0
|
@@ -142,6 +142,13 @@ while [[ $# -gt 0 ]]; do
|
|
|
142
142
|
APPROVE_PLAN_ACK="true"
|
|
143
143
|
shift
|
|
144
144
|
;;
|
|
145
|
+
--implementation-option)
|
|
146
|
+
# 유저가 implementation-planning final-report 에서 고른 Option Candidate
|
|
147
|
+
# 이름. 런타임이 approved-plan frontmatter 의 implementation-option 라인을
|
|
148
|
+
# 이 값으로 채운다. 빈 값이면 implementation 이 Recommended Option 으로 폴백.
|
|
149
|
+
IMPLEMENTATION_OPTION="$(require_option_value --implementation-option "${2-}")"
|
|
150
|
+
shift 2
|
|
151
|
+
;;
|
|
145
152
|
--no-plan-verification)
|
|
146
153
|
# implementation-planning 의 Phase 6 plan-body verification 라운드를
|
|
147
154
|
# 끈다. 기본값은 활성화. 비활성 시 final-report 상단의 User Approval
|
|
@@ -178,7 +185,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
178
185
|
printf ' hint: did you mean --task-id?\n' >&2
|
|
179
186
|
;;
|
|
180
187
|
esac
|
|
181
|
-
printf ' valid options: --render-only --resume-clarification --yes --workers --lead-model --claude-model --codex-model --gemini-model --report-writer-model --related-tasks --task-type --project-id --project-root --task-group --task-id --task-brief --directive --clarification-response --approved-plan --approve --no-plan-verification -h|--help\n' >&2
|
|
188
|
+
printf ' valid options: --render-only --resume-clarification --yes --workers --lead-model --claude-model --codex-model --gemini-model --report-writer-model --related-tasks --task-type --project-id --project-root --task-group --task-id --task-brief --directive --clarification-response --approved-plan --approve --implementation-option --no-plan-verification -h|--help\n' >&2
|
|
182
189
|
usage
|
|
183
190
|
exit 1
|
|
184
191
|
;;
|
|
@@ -39,6 +39,9 @@ DIRECTIVE=""
|
|
|
39
39
|
CLARIFICATION_RESPONSE_PATH=""
|
|
40
40
|
APPROVED_PLAN_PATH=""
|
|
41
41
|
APPROVE_PLAN_ACK="false"
|
|
42
|
+
# implementation 전용: 유저가 고른 Option Candidate 이름. 빈 값이면 implementation
|
|
43
|
+
# 이 plan 의 Recommended Option 으로 폴백한다. --implementation-option 으로 설정.
|
|
44
|
+
IMPLEMENTATION_OPTION=""
|
|
42
45
|
# Phase 6 plan-body verification toggle. Default "true" (round runs).
|
|
43
46
|
# Flipped to "false" by --no-plan-verification on the CLI.
|
|
44
47
|
PLAN_VERIFICATION_ENABLED="true"
|
|
@@ -59,23 +59,25 @@ resolve_task_root_for_shortcut() {
|
|
|
59
59
|
local task_id="$4"
|
|
60
60
|
|
|
61
61
|
local resolved=""
|
|
62
|
-
resolved="$(python3 - "$project_root" "$project_id" "$task_group" "$task_id" <<'PY'
|
|
63
|
-
import json, os,
|
|
62
|
+
resolved="$(python3 - "$WORKSPACE_ROOT/scripts" "$project_root" "$project_id" "$task_group" "$task_id" <<'PY'
|
|
63
|
+
import json, os, sys
|
|
64
64
|
from pathlib import Path
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
# task root 의 slug 경로 구성은 okstra_ctl.paths.task_dir(SSOT) 에 위임한다.
|
|
67
|
+
# 과거 이 heredoc 은 slugify 와 `.okstra/tasks/<slug>/<slug>` 구조를 자체
|
|
68
|
+
# 재구현해 규칙 변경 시 silent drift 위험이 있었다. project-resolver.sh 와
|
|
69
|
+
# 동일하게 $WORKSPACE_ROOT/scripts 를 sys.path 에 올려 패키지를 import 한다.
|
|
70
|
+
sys.path.insert(0, sys.argv[1])
|
|
71
|
+
from okstra_ctl.paths import task_dir
|
|
72
|
+
|
|
73
|
+
project_root = Path(sys.argv[2])
|
|
74
|
+
project_id = sys.argv[3]
|
|
75
|
+
task_group = sys.argv[4]
|
|
76
|
+
task_id = sys.argv[5]
|
|
70
77
|
|
|
71
78
|
requested_key = f"{project_id}:{task_group}:{task_id}"
|
|
72
79
|
requested_key_ci = requested_key.lower()
|
|
73
80
|
|
|
74
|
-
def slugify(value: str) -> str:
|
|
75
|
-
value = value.lower()
|
|
76
|
-
value = re.sub(r"[^a-z0-9]+", "-", value).strip("-")
|
|
77
|
-
return value
|
|
78
|
-
|
|
79
81
|
candidates = []
|
|
80
82
|
|
|
81
83
|
catalog_path = project_root / ".okstra" / "discovery" / "task-catalog.json"
|
|
@@ -111,7 +113,7 @@ if catalog_path.is_file():
|
|
|
111
113
|
sys.exit(0)
|
|
112
114
|
candidates.append(str(abs_path))
|
|
113
115
|
|
|
114
|
-
slug_path = project_root
|
|
116
|
+
slug_path = task_dir(project_root, task_group, task_id)
|
|
115
117
|
if slug_path.is_dir():
|
|
116
118
|
print(f"OK\t{slug_path}")
|
|
117
119
|
sys.exit(0)
|
|
@@ -46,6 +46,12 @@ optional arguments:
|
|
|
46
46
|
\`- [ ] Approved\` to \`- [x] Approved\` and appends an approval audit line
|
|
47
47
|
(timestamp + "CLI --approve"). Use this for scripted/CI flows or when you want a
|
|
48
48
|
single command to both approve and launch the next phase.
|
|
49
|
+
--implementation-option <name>
|
|
50
|
+
Name of the Option Candidate the user chose from the implementation-planning
|
|
51
|
+
final-report. Only meaningful together with --approved-plan and
|
|
52
|
+
--task-type=implementation. The runtime fills the approved-plan frontmatter
|
|
53
|
+
\`implementation-option:\` line with <name>. When omitted, the implementation run
|
|
54
|
+
falls back to the plan's \`Recommended Option\`.
|
|
49
55
|
--no-plan-verification
|
|
50
56
|
Disable the Phase 6 plan-body verification round that runs after the report-writer
|
|
51
57
|
authors the implementation-planning draft. Default: enabled. Only meaningful with
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# okstra-team-reconcile.sh — flip dead-pane stale-active team members to
|
|
4
|
+
# inactive so the lead's `TeamDelete()` can disband the team in one shot.
|
|
5
|
+
#
|
|
6
|
+
# A Claude Code team member clears its own `isActive` flag in
|
|
7
|
+
# `~/.claude/teams/<team>/config.json` when its `Agent()` dispatch returns. A
|
|
8
|
+
# member whose tmux pane died WITHOUT that flip stays `isActive: true`, and
|
|
9
|
+
# `TeamDelete` then refuses the whole team ("active members remain") — an error
|
|
10
|
+
# no re-sent `shutdown_request` can clear, since the addressee is already gone.
|
|
11
|
+
# This reconciles exactly that case; it never touches a live-pane member, the
|
|
12
|
+
# lead, or a member with no recorded pane (those are left for graceful
|
|
13
|
+
# shutdown). It no-ops when tmux is unavailable or nothing is stale.
|
|
14
|
+
#
|
|
15
|
+
# Usage: okstra-team-reconcile.sh [--list] <team-name>
|
|
16
|
+
# --list report what WOULD be deactivated; do not write (alias --dry-run).
|
|
17
|
+
#
|
|
18
|
+
# Failures are non-fatal to the run — teardown must never block on this.
|
|
19
|
+
set -u
|
|
20
|
+
|
|
21
|
+
_dir="$(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
22
|
+
# Logic lives in okstra_ctl.team_reconcile. In the repo layout the package is a
|
|
23
|
+
# bin-sibling (scripts/okstra_ctl); in the installed layout it is under
|
|
24
|
+
# $OKSTRA_HOME/lib/python. Put both on PYTHONPATH so either resolves.
|
|
25
|
+
home="${OKSTRA_HOME:-$HOME/.okstra}"
|
|
26
|
+
export PYTHONPATH="${_dir}:${home}/lib/python${PYTHONPATH:+:$PYTHONPATH}"
|
|
27
|
+
|
|
28
|
+
exec python3 -c 'import sys; from okstra_ctl.team_reconcile import main; sys.exit(main(sys.argv[1:]))' "$@"
|
package/runtime/bin/okstra.sh
CHANGED
|
@@ -80,6 +80,7 @@ okstra execution summary:
|
|
|
80
80
|
executor (implementation only): ${EXECUTOR_OVERRIDE:-default(claude)}
|
|
81
81
|
approved plan: ${APPROVED_PLAN_PATH:-None}
|
|
82
82
|
approve ack (CLI 승인 의사): ${APPROVE_PLAN_ACK}
|
|
83
|
+
implementation option (선택 옵션): ${IMPLEMENTATION_OPTION:-None(fallback to Recommended Option)}
|
|
83
84
|
related tasks: ${RELATED_TASKS_RAW:-None}
|
|
84
85
|
CONFIRM_EOF
|
|
85
86
|
printf 'Continue? [y/yes]: ' >&2
|
|
@@ -117,6 +118,7 @@ PY_ARGS=(
|
|
|
117
118
|
[[ -n "${RELATED_TASKS_RAW-}" ]] && PY_ARGS+=(--related-tasks "$RELATED_TASKS_RAW")
|
|
118
119
|
[[ -n "${APPROVED_PLAN_PATH-}" ]] && PY_ARGS+=(--approved-plan "$APPROVED_PLAN_PATH")
|
|
119
120
|
[[ "$APPROVE_PLAN_ACK" == "true" ]] && PY_ARGS+=(--approve)
|
|
121
|
+
[[ -n "${IMPLEMENTATION_OPTION-}" ]] && PY_ARGS+=(--implementation-option "$IMPLEMENTATION_OPTION")
|
|
120
122
|
[[ -n "${CLARIFICATION_RESPONSE_PATH-}" ]] && PY_ARGS+=(--clarification-response "$CLARIFICATION_RESPONSE_PATH")
|
|
121
123
|
[[ -n "${WORK_CATEGORY-}" ]] && PY_ARGS+=(--work-category "$WORK_CATEGORY")
|
|
122
124
|
[[ -n "${BASE_REF-}" ]] && PY_ARGS+=(--base-ref "$BASE_REF")
|
|
@@ -39,6 +39,8 @@ Emit one `PROGRESS: <phase-id> <verb-phrase>` line as plain user-facing text at
|
|
|
39
39
|
|
|
40
40
|
- Task manifest: `{{TASK_MANIFEST_RELATIVE_PATH}}`
|
|
41
41
|
- Run manifest: `{{RUN_MANIFEST_RELATIVE_PATH}}`
|
|
42
|
+
- Active run context: `{{ACTIVE_RUN_CONTEXT_RELATIVE_PATH}}`
|
|
43
|
+
- Analysis packet: `{{ANALYSIS_PACKET_RELATIVE_PATH}}`
|
|
42
44
|
|
|
43
45
|
## Session
|
|
44
46
|
|
|
@@ -82,7 +84,7 @@ Emit one `PROGRESS: <phase-id> <verb-phrase>` line as plain user-facing text at
|
|
|
82
84
|
## Available MCP Servers
|
|
83
85
|
|
|
84
86
|
{{AVAILABLE_MCP_SERVERS}}
|
|
85
|
-
- The full usage policy and per-phase rules live in the
|
|
87
|
+
- The full usage policy and per-phase rules live in the analysis packet's `Available MCP Servers` extract. Inject only the one-line pointer below into each analysis-worker prompt: `**MCP servers:** follow the analysis packet's "Available MCP Servers" section (already in your Required reading).`
|
|
86
88
|
- **Invocation rule (forward to every worker prompt)**: MCP tools are addressed by their tool name through the host's tool interface — **never via `Bash`**. Claude-side workers call the tool directly (e.g. `mcp__<server>__<tool>`). Codex/Gemini workers call through their CLI's own MCP transport (e.g. `codex mcp call ...`). Running the tool name as a shell command is a contract violation and will always fail regardless of permission grants.
|
|
87
89
|
- Codex worker and Gemini worker run external CLIs; they can only use these MCP servers if their own CLI configs mirror them. If not, instruct the worker to record `MCP not available in this CLI` in its `Missing Information or Assumptions` block rather than guessing or shell-falling-back.
|
|
88
90
|
- MCP queries are evidence-grade. Cite server, table, and the SELECT used in worker output. MCP must NOT be used as a write path in any phase, including `implementation`.
|
|
@@ -39,11 +39,11 @@ profile document.
|
|
|
39
39
|
- Run-end team teardown (shared — runs AFTER Phase 7 persistence/token collection, BEFORE the pane disposition step below):
|
|
40
40
|
- The lead created the worker team in Phase 3 (`TeamCreate(team_name: "okstra-<task-key>")`). Worker teammates are NOT reclaimed on their own — without an explicit teardown they linger in the FleetView roster across this and later runs in the session. The lead MUST release them once the run's work is done.
|
|
41
41
|
- This step is **automatic and silent** — NO user prompt (workers are idle sessions that have already delivered their results; there is nothing for the user to preserve). It runs only when team-state's `teamCreate.status == "ok"` (Teams mode was actually used); in the no-`team_name` fallback there is no team to delete, so silent-skip.
|
|
42
|
+
- Why a reconcile step exists: each worker clears its own `isActive` flag in `~/.claude/teams/<team>/config.json` when its `Agent()` dispatch returns, so by Phase 7 every worker is normally already inactive and `TeamDelete()` succeeds immediately. The one failure mode is a worker whose tmux pane died WITHOUT clearing the flag (e.g. killed mid-turn): it stays `isActive: true`, and `TeamDelete` then refuses the entire team with an "active members" error that no amount of re-sending `shutdown_request` can clear — the addressee is already gone. `okstra-team-reconcile.sh` deterministically flips exactly those dead-pane stale-active members to inactive (never a live-pane member, never the lead).
|
|
42
43
|
- Sequence (token-usage collection MUST already be complete — `TeamDelete` removes `~/.claude/teams/<team>/` + `~/.claude/tasks/<team>/` but NOT the `~/.claude/projects/` jsonls Phase 7 reads, yet the read MUST precede teardown):
|
|
43
|
-
1.
|
|
44
|
-
2.
|
|
45
|
-
|
|
46
|
-
- Report it in one short line (e.g. `worker 6명 종료 + 팀 해제`) and proceed. Emit `PROGRESS: phase-7-teardown disbanding team` immediately before step 1.
|
|
44
|
+
1. Run `$HOME/.okstra/bin/okstra-team-reconcile.sh "okstra-<task-key>"` exactly once. It flips dead-pane stale-active members to inactive, and no-ops when tmux is unavailable or nothing is stale. Do NOT loop it.
|
|
45
|
+
2. Call `TeamDelete()` — the single synchronization point for teardown. If it STILL errors with an active-members message, one worker pane is genuinely still live (rare at Phase 7, since every `Agent()` dispatch has already returned): send that one member a structured `SendMessage(to: <name>, message: { type: "shutdown_request" })` — the `message` MUST be the object literal shown, NEVER a JSON string stuffed into a text field (a stringified payload is delivered as a plain message and the shutdown protocol never fires) — wait briefly, then retry `TeamDelete()` once and proceed regardless of the result. NEVER loop, never use `TaskStop` (teammates are not background tasks — `TaskStop` 404s on a member address), and never let teardown block run completion once the work and final report already exist.
|
|
46
|
+
- Report it in one short line (e.g. `stale 멤버 1명 정리 + 팀 해제`, or just `worker 팀 해제` when nothing was stale) and proceed. Emit `PROGRESS: phase-7-teardown disbanding team` immediately before step 1.
|
|
47
47
|
- Phase wrap-up — okstra pane disposition (shared, MUST be the *last* step before returning control to the user):
|
|
48
48
|
- At run end the only residual okstra panes are the LAST phase's (e.g. the `report-writer-worker` agent pane and any codex/gemini trace pane). `okstra-trace-cleanup.sh --list --run-dir "<RUN_DIR>"` returns one tab-separated `<pane_id>\t<pane_title>` line per residual okstra pane (worker-agent + trace) for this run.
|
|
49
49
|
- When `<RUN_DIR>/state/lead-pane.id` is non-empty, after the final-report file has been written and the routing recommendation has been issued, the lead MUST run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --list --run-dir "<RUN_DIR>"` exactly once. The output lists every residual okstra pane (worker-agent + trace) for this run, never the lead's own pane.
|
|
@@ -19,9 +19,9 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
|
|
|
19
19
|
## Pre-implementation context exploration (executor before first edit)
|
|
20
20
|
|
|
21
21
|
- **Coding-conventions preflight (BLOCKING — runs before the first `Edit` / `Write`, and binds the TDD loop below):** load the applicable coding conventions for every language the diff will touch, then state in ONE line which conventions apply (e.g. `Applying TS + hexagonal overlay; domain at src/domains/*/domain/`). Lint/test green is necessary but NOT sufficient — self-mocked tests, interaction-only assertions, and untruthful names all pass a green pipeline; this gate is what keeps them out of the diff.
|
|
22
|
-
- **Language-specific rules load per situation — never inline them here.** Detect each touched file's language (extension / project manifest) and load the matching reference
|
|
22
|
+
- **Language-specific rules load per situation — never inline them here.** Detect each touched file's language (extension / project manifest) and load the matching reference by reading okstra's installed coding-conventions files directly at `~/.claude/skills/okstra-coding-preflight/` (placed there by `okstra install`): read `languages/<lang>.md` (mock/spy API, idioms, test framework) + `clean-code.md` + any `architecture/*` overlay via the Read tool by absolute path. The skill is `user-invocable: false`, so do NOT rely on Skill-tool auto-invocation — read the files directly. For a ports-and-adapters / NestJS-hex layout (`domain/` + `ports/` + `adapters/`, `*.port.*`), load the hexagonal overlay too. This per-language split is the skill's job — the executor does not carry a multi-language block in context.
|
|
23
23
|
- **Language-agnostic principles that ALWAYS bind (the TDD loop below MUST satisfy them):** (1) no self-mocking of the SUT — stub/spy only injected collaborators, never the subject's own methods; (2) behavioral assertions on outcomes (return value, state, persisted rows, events, boundary calls) — never `toHaveBeenCalled*` on an internal helper as the only/primary assertion; (3) truthful names — a `get*` / `find*` that writes/inserts, or a name encoding the caller's use-case (`*ForInit`) or hiding a domain rule (`findValid*`), is a defect; (4) single-purpose functions ≤50 effective lines, plain-English readability.
|
|
24
|
-
- **Graceful degradation (
|
|
24
|
+
- **Graceful degradation (codex / gemini executor runtimes, or any runtime where the `~/.claude/skills/okstra-coding-preflight/` files are absent or unreadable):** do NOT skip the gate — apply the agnostic principles above plus the project's own `CLAUDE.md` / `CONTRIBUTING` / formatter+lint config, and record `coding-conventions: skill-unavailable → applied <project rules + agnostic principles>` in the final report. Never claim a skill read that did not happen.
|
|
25
25
|
- **Mandatory TDD loop**: BEFORE the first `Edit` or `Write` call, the executor MUST apply a red-green-refactor loop for every code change in this run. This is required; skipping it is a `contract-violated` outcome. This governs HOW each step is executed (failing test first → minimal implementation → refactor); it does not override the approved plan's WHAT/file scope.
|
|
26
26
|
- Order of operations per plan step: (1) write/extend the test that captures the step's acceptance criterion and confirm it fails for the right reason, (2) commit the failing test (`test(<scope>): ...`), (3) implement the minimum change to make it pass, (4) commit the implementation (`feat|fix(<scope>): ...`), (5) refactor without changing behaviour and commit separately if any cleanup is made (`refactor(<scope>): ...`). The failing-then-passing transition between steps (2) and (4) is the `TDD evidence` required by the final report.
|
|
27
27
|
- Doc-only / config-only / pure-rename steps that have no observable runtime behaviour are exempt from the failing-test requirement, but the executor MUST cite the exemption per step in the final report (`TDD exemption: <reason>`).
|
|
@@ -66,7 +66,7 @@ The final report keeps both — executor's `Validation evidence` AND each verifi
|
|
|
66
66
|
Re-running commands proves the diff *builds and passes*; it does NOT prove the diff is *well-designed*. Lint/test green is necessary but not sufficient — self-mocked tests, interaction-only assertions, and untruthful names all survive a green pipeline. This gate is the filter for exactly those defects, so the executor's design errors are caught here instead of in post-merge PR review. It is a real gate, not a checklist: it enumerates the full diff and a blocking hit forces `FAIL`.
|
|
67
67
|
|
|
68
68
|
- **Scope (no silent sampling).** Enumerate every changed source/test file via `git diff --name-only <base>...HEAD` and review each one. Skipping a changed file silently is a `contract-violated` outcome. If a file's language has no reference and is not covered by the agnostic checks below, record `design-review skipped: <file> (language=<x> no reference)` — never pass it silently.
|
|
69
|
-
- **Load the same conventions the executor used, per language.** For each touched language load the coding-conventions reference
|
|
69
|
+
- **Load the same conventions the executor used, per language.** For each touched language load the coding-conventions reference by reading `~/.claude/skills/okstra-coding-preflight/languages/<lang>.md` + `clean-code.md` + the `architecture/hexagonal.md` overlay when the layout matches; degrade to the agnostic checks below when those files are not readable. The verifier does NOT inline language rules — it loads them per situation, identical to the executor preflight.
|
|
70
70
|
- **Blocking checks (any hit → verdict `FAIL`, cited `path:line` + rule name, recommended fix recorded — the verifier does NOT apply it):**
|
|
71
71
|
- **Self-mocking:** a test for `Foo` stubs/spies a method on the `Foo` instance under test (`jest.spyOn(sut, ...)`, `spyOn(FooService.prototype, ...)` in `foo.*.spec.*`, `vi.mocked(sut)` + stub). Mocking injected collaborators is fine.
|
|
72
72
|
- **Interaction-only assertion:** a test whose only/primary assertion is `toHaveBeenCalled*` / `toHaveBeenCalledTimes` on an internal helper or a non-side-effecting collaborator, with no assertion on the returned value / resulting state / persisted row / emitted event.
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
- validation checklist (pre / mid / post) — each item is an exact command or observable outcome
|
|
77
77
|
- rollback strategy — exact revert path (commits, flags, migrations) and the signal that triggers rollback
|
|
78
78
|
- the YAML frontmatter MUST include the line `approved: false` (report-writer always emits the unflipped value). The user authorises the next `implementation` run by flipping it to `approved: true` (manual edit or `--approve` CLI). Do NOT recreate any `User Approval Request` body block — the validator fails reports that contain one (see `validators/validate-run.py` deprecated patterns).
|
|
79
|
+
- the YAML frontmatter MUST include the line `implementation-option:` directly under `approved:` (report-writer always emits it with an **empty value**). The user selects which Option Candidate the next `implementation` run executes by filling this line with that option's name (manual edit or `--implementation-option <name>` CLI). When left empty, the `implementation` run falls back to the `Recommended Option`.
|
|
79
80
|
- **the frontmatter `approved: false` line is rendered unconditionally; if the plan-body verification gate (§5.5.9) returns `blocked-by-disagreement` or `aborted-non-result`, the writer MUST keep `approved: false` and the validator refuses any report that ships with `approved: true` under such a gate result.**
|
|
80
81
|
- every ambiguity flagged during pre-planning that the user must resolve before approval registered as a `Blocks=approval` row in the `## 1. Clarification Items` table (do NOT create a separate `Open Questions` block under `4.5.x` — the unified table is the single home)
|
|
81
82
|
- **§5.5.9 Plan Body Verification (BLOCKING).** After report-writer finishes the draft, the lead MUST run a worker peer-review round on the consolidated plan body (sections 4.5.1 – 4.5.7) and populate `### 5.5.9 Plan Body Verification` in the final report. The round protocol, plan-item ID scheme (`P-Opt-*` / `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*`), verdict semantics, gate-result classification, and dissent log format are defined in `skills/okstra-convergence/SKILL.md` "Plan-body verification mode". The four gate-result values are `passed`, `passed-with-dissent`, `blocked-by-disagreement`, `aborted-non-result`. When the gate would have been `blocked-by-disagreement` or `aborted-non-result`, the lead MUST NOT silently flip it to one of the passing values to "unblock" the run — that is a contract violation. When `convergence.adversarial=true` (the default for this phase), this round uses the adversarial posture — verifiers confirm cited paths/commands and the burden of proof is on the plan — but the gate threshold stays `majority-disagree` (see that skill's §"Adversarial plan-body posture").
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
- the run brief MUST cite `--approved-plan <path>` pointing to a `final-report.md` produced by a prior `implementation-planning` run located under `runs/implementation-planning/.../reports/final-report.md`
|
|
20
20
|
- that file's YAML frontmatter MUST carry `approved: true`. report-writer emits `approved: false` by default; the user flips it to `true` to authorise this run. Free-form approvals such as "lgtm" / "go ahead" / paraphrased confirmations are NOT accepted; re-edit the plan file's frontmatter to `approved: true` before invoking implementation, or pass `--approve` so the CLI flips it on the user's behalf (`okstra_ctl.run._apply_cli_approval`).
|
|
21
21
|
- The `--approve` flag is meaningful ONLY with `--task-type implementation` and `--approved-plan <path>`; any other use raises `PrepareError`. Idempotent — re-running with `approved: true` already set appends an audit line but does NOT re-toggle.
|
|
22
|
-
- the
|
|
22
|
+
- the authoritative scope for this run is the Option Candidate named by the YAML frontmatter `implementation-option:` field. **If `implementation-option:` is empty, fall back to the plan's `Recommended Option`** (this is a soft fallback, not a hard block). The chosen option's bite-sized step list becomes the authoritative scope; deviations must be justified in the final report and routed back to a new `implementation-planning` run rather than silently expanded. If the chosen option name does not match any heading under `Option Candidates`, record it as a deviation.
|
|
23
23
|
- Task worktree (provisioned by `okstra-ctl` at the first phase's run-prep time, reused for every subsequent phase of this task-key):
|
|
24
24
|
- Status: `{{EXECUTOR_WORKTREE_STATUS}}` (one of: `created` | `reused` | `skipped-in-worktree` | `skipped-not-git`)
|
|
25
25
|
- Working tree path: `{{EXECUTOR_WORKTREE_PATH}}` — when status is `created` or `reused`, this is the task's `git worktree` rooted at `~/.okstra/worktrees/<project>/<task-group>/<task-id>/`. When skipped, this is the caller's `project_root`.
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Build the compact analysis-worker input packet for a task run."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
BRIEF_SECTIONS = (
|
|
9
|
+
"Identity",
|
|
10
|
+
"Request Summary",
|
|
11
|
+
"Current Context",
|
|
12
|
+
"Evidence and Source Materials",
|
|
13
|
+
"Task-Type Focus",
|
|
14
|
+
"Constraints and Risks",
|
|
15
|
+
"Out of Scope",
|
|
16
|
+
"Configuration References and Expected Values",
|
|
17
|
+
"Deployment Manifests and Expected Values",
|
|
18
|
+
"Questions for Workers",
|
|
19
|
+
"Expected Outputs",
|
|
20
|
+
"Task Continuity Notes",
|
|
21
|
+
"Available MCP Servers",
|
|
22
|
+
)
|
|
23
|
+
PROFILE_SECTIONS = (
|
|
24
|
+
"Primary focus areas",
|
|
25
|
+
"Expected output emphasis",
|
|
26
|
+
"Non-goals",
|
|
27
|
+
"Clarification request policy",
|
|
28
|
+
)
|
|
29
|
+
CLARIFICATION_SECTIONS = (
|
|
30
|
+
"Clarification Items",
|
|
31
|
+
"Clarification Response Carried In From Previous Run",
|
|
32
|
+
)
|
|
33
|
+
_FRONTMATTER_RE = re.compile(r"\A---\n(?P<body>.*?)\n---\n", re.DOTALL)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_analysis_packet(
|
|
37
|
+
*,
|
|
38
|
+
task_key: str,
|
|
39
|
+
task_type: str,
|
|
40
|
+
task_brief_path: Path,
|
|
41
|
+
analysis_profile_path: Path,
|
|
42
|
+
reference_expectations_path: Path,
|
|
43
|
+
clarification_response_path: Path | None,
|
|
44
|
+
directive: str,
|
|
45
|
+
instruction_set_relative_path: str,
|
|
46
|
+
) -> str:
|
|
47
|
+
"""Return the primary compact input for Claude/Codex/Gemini analysers."""
|
|
48
|
+
brief_text = task_brief_path.read_text(encoding="utf-8")
|
|
49
|
+
profile_text = analysis_profile_path.read_text(encoding="utf-8")
|
|
50
|
+
reference_text = _read_optional(reference_expectations_path)
|
|
51
|
+
clarification_text = (
|
|
52
|
+
_read_optional(clarification_response_path)
|
|
53
|
+
if clarification_response_path else ""
|
|
54
|
+
)
|
|
55
|
+
parts = [_packet_frontmatter(brief_text, task_key)]
|
|
56
|
+
parts.extend(
|
|
57
|
+
_intro_block(
|
|
58
|
+
task_key,
|
|
59
|
+
instruction_set_relative_path,
|
|
60
|
+
bool(clarification_response_path),
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
parts.extend(_brief_block(brief_text))
|
|
64
|
+
parts.extend(_profile_block(task_type, profile_text))
|
|
65
|
+
parts.extend(_reference_block(reference_text))
|
|
66
|
+
parts.extend(_clarification_block(clarification_text))
|
|
67
|
+
parts.extend(_directive_block(directive))
|
|
68
|
+
return "\n".join(part.rstrip() for part in parts).rstrip() + "\n"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _packet_frontmatter(brief_text: str, task_key: str) -> str:
|
|
72
|
+
frontmatter = _extract_frontmatter(brief_text)
|
|
73
|
+
if frontmatter:
|
|
74
|
+
return (
|
|
75
|
+
f"---\n{frontmatter}\n"
|
|
76
|
+
'packetVersion: "1.0"\n'
|
|
77
|
+
"packetRole: analysis-worker-primary\n---"
|
|
78
|
+
)
|
|
79
|
+
return (
|
|
80
|
+
"---\n"
|
|
81
|
+
f'title: OKSTRA Analysis Packet - {task_key}\n'
|
|
82
|
+
'packetVersion: "1.0"\n'
|
|
83
|
+
"packetRole: analysis-worker-primary\n"
|
|
84
|
+
"---"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _intro_block(
|
|
89
|
+
task_key: str,
|
|
90
|
+
instruction_set: str,
|
|
91
|
+
has_clarification: bool,
|
|
92
|
+
) -> list[str]:
|
|
93
|
+
lines = [
|
|
94
|
+
f"# OKSTRA Analysis Packet - {task_key}",
|
|
95
|
+
"",
|
|
96
|
+
"## Packet Role",
|
|
97
|
+
"",
|
|
98
|
+
"- This packet is the primary required reading for analysis workers.",
|
|
99
|
+
"- It extracts task-specific material from the source files listed below.",
|
|
100
|
+
"- Read source files only for evidence verification or missing detail.",
|
|
101
|
+
"",
|
|
102
|
+
"## Source Files",
|
|
103
|
+
"",
|
|
104
|
+
f"- Task brief: `{instruction_set}/task-brief.md`",
|
|
105
|
+
f"- Analysis profile: `{instruction_set}/analysis-profile.md`",
|
|
106
|
+
f"- Analysis material: `{instruction_set}/analysis-material.md`",
|
|
107
|
+
f"- Reference expectations: `{instruction_set}/reference-expectations.md`",
|
|
108
|
+
]
|
|
109
|
+
if has_clarification:
|
|
110
|
+
lines.append(
|
|
111
|
+
f"- Clarification response: `{instruction_set}/clarification-response.md`"
|
|
112
|
+
)
|
|
113
|
+
return lines
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _brief_block(brief_text: str) -> list[str]:
|
|
117
|
+
return [
|
|
118
|
+
"",
|
|
119
|
+
"## Task-Specific Brief Extract",
|
|
120
|
+
"",
|
|
121
|
+
_extract_sections(brief_text, BRIEF_SECTIONS),
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _profile_block(task_type: str, profile_text: str) -> list[str]:
|
|
126
|
+
profile_sections = _extract_sections(profile_text, PROFILE_SECTIONS)
|
|
127
|
+
profile_bullets = _extract_bullet_sections(profile_text, PROFILE_SECTIONS)
|
|
128
|
+
profile_focus = "\n\n".join(
|
|
129
|
+
part for part in (profile_sections, profile_bullets)
|
|
130
|
+
if part and not part.startswith("- No matching")
|
|
131
|
+
)
|
|
132
|
+
return [
|
|
133
|
+
"",
|
|
134
|
+
"## Phase Focus Extract",
|
|
135
|
+
"",
|
|
136
|
+
f"- Task Type: `{task_type}`",
|
|
137
|
+
"",
|
|
138
|
+
_extract_profile_prelude(profile_text),
|
|
139
|
+
"",
|
|
140
|
+
profile_focus or "- No matching source sections were available.",
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _reference_block(reference_text: str) -> list[str]:
|
|
145
|
+
body = reference_text.strip() or "- No reference expectations content was available."
|
|
146
|
+
return ["", "## Reference Expectations", "", body]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _clarification_block(clarification_text: str) -> list[str]:
|
|
150
|
+
if not clarification_text.strip():
|
|
151
|
+
return []
|
|
152
|
+
return [
|
|
153
|
+
"",
|
|
154
|
+
"## Clarification Carry-In Extract",
|
|
155
|
+
"",
|
|
156
|
+
_extract_sections(clarification_text, CLARIFICATION_SECTIONS),
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _directive_block(directive: str) -> list[str]:
|
|
161
|
+
if not directive:
|
|
162
|
+
return []
|
|
163
|
+
return ["", "## Directive", "", directive.strip()]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _extract_frontmatter(text: str) -> str:
|
|
167
|
+
match = _FRONTMATTER_RE.match(text)
|
|
168
|
+
return match.group("body").rstrip() if match else ""
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _extract_profile_prelude(text: str) -> str:
|
|
172
|
+
lines = []
|
|
173
|
+
for line in text.splitlines():
|
|
174
|
+
if line.startswith(("{{INCLUDE:", "## ", "<!--", "- Team contract")):
|
|
175
|
+
break
|
|
176
|
+
if _top_level_bullet_heading(line) in PROFILE_SECTIONS:
|
|
177
|
+
break
|
|
178
|
+
if line.strip():
|
|
179
|
+
lines.append(line)
|
|
180
|
+
return "\n".join(lines).strip() or "- No profile prelude was available."
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _extract_sections(text: str, headings: tuple[str, ...]) -> str:
|
|
184
|
+
sections = _section_map(text)
|
|
185
|
+
out = []
|
|
186
|
+
for heading in headings:
|
|
187
|
+
body = sections.get(heading, "").strip()
|
|
188
|
+
if body:
|
|
189
|
+
out.append(f"### {heading}\n\n{body}")
|
|
190
|
+
return "\n\n".join(out) or "- No matching source sections were available."
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _extract_bullet_sections(text: str, headings: tuple[str, ...]) -> str:
|
|
194
|
+
heading_set = set(headings)
|
|
195
|
+
captured: list[tuple[str, list[str]]] = []
|
|
196
|
+
current_heading = ""
|
|
197
|
+
current_lines: list[str] = []
|
|
198
|
+
|
|
199
|
+
def flush_current() -> None:
|
|
200
|
+
nonlocal current_heading, current_lines
|
|
201
|
+
if current_heading and current_lines:
|
|
202
|
+
captured.append((current_heading, current_lines))
|
|
203
|
+
current_heading = ""
|
|
204
|
+
current_lines = []
|
|
205
|
+
|
|
206
|
+
for line in _strip_frontmatter(text).splitlines():
|
|
207
|
+
bullet_heading = _top_level_bullet_heading(line)
|
|
208
|
+
if bullet_heading:
|
|
209
|
+
if bullet_heading in heading_set:
|
|
210
|
+
flush_current()
|
|
211
|
+
current_heading = bullet_heading
|
|
212
|
+
current_lines = [line]
|
|
213
|
+
continue
|
|
214
|
+
flush_current()
|
|
215
|
+
continue
|
|
216
|
+
if current_heading:
|
|
217
|
+
if line.startswith(" ") or not line.strip():
|
|
218
|
+
current_lines.append(line)
|
|
219
|
+
else:
|
|
220
|
+
flush_current()
|
|
221
|
+
flush_current()
|
|
222
|
+
|
|
223
|
+
out = []
|
|
224
|
+
for heading, lines in captured:
|
|
225
|
+
body = "\n".join(lines).strip()
|
|
226
|
+
if body:
|
|
227
|
+
out.append(f"### {heading}\n\n{body}")
|
|
228
|
+
return "\n\n".join(out)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _top_level_bullet_heading(line: str) -> str:
|
|
232
|
+
if not line.startswith("- ") or ":" not in line:
|
|
233
|
+
return ""
|
|
234
|
+
label = line[2:].split(":", 1)[0].strip().strip("*")
|
|
235
|
+
return label
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _section_map(text: str) -> dict[str, str]:
|
|
239
|
+
result: dict[str, list[str]] = {}
|
|
240
|
+
current = ""
|
|
241
|
+
for line in _strip_frontmatter(text).splitlines():
|
|
242
|
+
if line.startswith("## "):
|
|
243
|
+
current = line[3:].strip()
|
|
244
|
+
result.setdefault(current, [])
|
|
245
|
+
continue
|
|
246
|
+
if current:
|
|
247
|
+
result[current].append(line)
|
|
248
|
+
return {key: "\n".join(lines).strip() for key, lines in result.items()}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _strip_frontmatter(text: str) -> str:
|
|
252
|
+
match = _FRONTMATTER_RE.match(text)
|
|
253
|
+
return text[match.end():] if match else text
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _read_optional(path: Path | None) -> str:
|
|
257
|
+
if path and path.is_file():
|
|
258
|
+
return path.read_text(encoding="utf-8")
|
|
259
|
+
return ""
|