okstra 0.13.1 → 0.14.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 +1 -1
- package/README.md +1 -1
- package/docs/kr/architecture.md +8 -5
- package/docs/kr/cli.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +1 -1
- package/runtime/agents/workers/codex-worker.md +2 -2
- package/runtime/bin/okstra-codex-exec.sh +1 -1
- package/runtime/bin/okstra.sh +7 -3
- package/runtime/prompts/profiles/implementation.md +10 -9
- package/runtime/python/lib/okstra/cli.sh +4 -0
- package/runtime/python/lib/okstra/globals.sh +2 -1
- package/runtime/python/lib/okstra/usage.sh +8 -1
- package/runtime/python/okstra_ctl/run.py +43 -9
- package/runtime/python/okstra_ctl/seeding.py +95 -14
- package/runtime/python/okstra_ctl/worktree.py +50 -7
- package/runtime/skills/okstra-run/SKILL.md +55 -1
- package/runtime/skills/okstra-setup/SKILL.md +31 -0
- package/src/install.mjs +61 -0
- package/src/setup.mjs +64 -2
- package/src/uninstall.mjs +15 -0
package/README.kr.md
CHANGED
|
@@ -165,7 +165,7 @@ Claude Code 세션 밖에서 task 를 시작하려면:
|
|
|
165
165
|
|
|
166
166
|
0.8.0 이후 `main` 에 추가된 workflow 변경:
|
|
167
167
|
|
|
168
|
-
- **`--task-type implementation` 격리 worktree 자동 provisioning** — prepare 단계에서 `okstra-ctl` 이 `git worktree add ~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>-<run-seq>` 를 수행해 격리된 working tree 와 브랜치 `<work-category-prefix>-<task-id-segment>-<run-seq>` (예: `feat-dev-9436-001`, `fix-dev-7311-002`) 를 만듭니다. base ref 는
|
|
168
|
+
- **`--task-type implementation` 격리 worktree 자동 provisioning** — prepare 단계에서 `okstra-ctl` 이 `git worktree add ~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>-<run-seq>` 를 수행해 격리된 working tree 와 브랜치 `<work-category-prefix>-<task-id-segment>-<run-seq>` (예: `feat-dev-9436-001`, `fix-dev-7311-002`) 를 만듭니다. base ref 는 사용자가 `--base-ref` 로 지정 (release-handoff PR base picker 와 동일한 메뉴: `main` / `dev` / `staging` / `preprod` / `prod` / 직접 입력). 첫 phase 에서는 필수이며, okstra-run skill 이 `AskUserQuestion` 으로 수집합니다 — 비대화형 호출자는 `--base-ref` 플래그를 직접 전달해야 prepare 가 통과합니다. Executor 와 verifier 모두 이 worktree 안에서 동작하므로 caller 의 작업 디렉터리는 깨끗하게 유지되고, worktree 는 PR 작성 · rollback 검증의 권위 artefact 로 남습니다. caller 가 이미 다른 worktree 안에 있거나 project_root 가 git repo 가 아니면 provisioning 은 skip 됩니다. 수동 cleanup: `git worktree remove <path>` → `git branch -D <branch>`. 상세: [`docs/kr/architecture.md`](docs/kr/architecture.md) *Task type* 섹션, [`docs/kr/cli.md#--executor`](docs/kr/cli.md#--executor).
|
|
169
169
|
- **`release-handoff` lifecycle phase** — `final-verification` 이 `verdict=accepted` 를 반환한 직후에 실행되는 신규 phase. lead 가 Claude worker (drafter) 를 통해 commit message · PR body 후보를 만들고, `AskUserQuestion` 으로 사용자에게 (1) action (`commit only` / `commit + PR` / `skip`), (2) PR base branch (`staging` / `preprod` / `prod` / `main` / `dev` / 직접 입력), (3) message handling (`use as-is` / `edit then proceed` / `cancel`) 세 가지를 순서대로 묻습니다. 사용자가 메뉴로 선택한 git / gh 명령만 실행되고, force-push, base 브랜치 직접 push, hook bypass (`--no-verify`), release publish (`gh release`, `npm publish`, ...) 는 금지됩니다. 이 phase 에서는 소스 코드를 수정하지 않습니다. profile: [`prompts/profiles/release-handoff.md`](prompts/profiles/release-handoff.md).
|
|
170
170
|
|
|
171
171
|
### 3.5 운영 명령
|
package/README.md
CHANGED
|
@@ -164,7 +164,7 @@ Notable flags added in 0.7.0 / 0.8.0:
|
|
|
164
164
|
|
|
165
165
|
Recent workflow additions (post-0.8.0, on `main`):
|
|
166
166
|
|
|
167
|
-
- **Isolated task worktree for every task-type** — prepare automatically runs `git worktree add ~/.okstra/worktrees/<project>/<group>/<task>/` on a fresh branch `<work-category-prefix>-<task-id-segment>` branched from the main
|
|
167
|
+
- **Isolated task worktree for every task-type** — prepare automatically runs `git worktree add ~/.okstra/worktrees/<project>/<group>/<task>/` on a fresh branch `<work-category-prefix>-<task-id-segment>` branched from the **user-chosen base ref** (`--base-ref`, mirroring the `release-handoff` PR-base picker: `main` / `dev` / `staging` / `preprod` / `prod` / any local ref) the first time a task-key is seen. `--base-ref` is required on first phase; the okstra-run skill collects it via `AskUserQuestion`, non-interactive callers must pass the flag explicitly. Every subsequent phase of the same task-key (`requirements-discovery` → `error-analysis` → `implementation-planning` → `implementation`) reuses the same path and branch, so phase N inherits the working-tree state phase N-1 left behind. A global registry at `~/.okstra/worktrees/registry.json` (flock-guarded) reserves task-keys and branches across concurrent runs; all path/branch segments are sanitised (`/`, `:`, etc. → `-`). The worktree is preserved after every run for follow-up phases, PR authoring, and rollback. Skip paths: when the caller is already inside another worktree or `project_root` is not a git repo, provisioning no-ops. Manual cleanup: `git worktree remove <path>` → `git branch -D <branch>` plus removing the task-key entry from the registry. Details: [`docs/kr/architecture.md`](docs/kr/architecture.md) (*Task type* section) and [`docs/kr/cli.md#--executor`](docs/kr/cli.md#--executor).
|
|
168
168
|
- **`release-handoff` lifecycle phase** — runs after `final-verification` returns `verdict=accepted`. The lead drafts a commit message and PR body via a Claude worker, then prompts the user with `AskUserQuestion` for three choices: action (`commit only` / `commit + PR` / `skip`), PR base branch (`staging` / `preprod` / `prod` / `main` / `dev` / free-form), and message handling (`use as-is` / `edit then proceed` / `cancel`). Only user-selected mutating git/gh commands run. Force-push, base-branch direct push, hook bypass (`--no-verify`), and release publishing (`gh release`, `npm publish`, ...) are forbidden. Source code is not edited in this phase. Profile: [`prompts/profiles/release-handoff.md`](prompts/profiles/release-handoff.md).
|
|
169
169
|
|
|
170
170
|
### 3.5 Ops commands
|
package/docs/kr/architecture.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
- **Single python authority**: 모든 prepare wiring(profile/workers/model 해소, path 계산, 9개 render, central record_start)이 [`okstra_ctl.run.prepare_task_bundle()`](scripts/okstra_ctl/run.py) 한 함수에 모여 있습니다. `okstra.sh` 와 `okstra-run` skill 은 같은 함수를 호출하는 thin caller 이며, 환경 변수로 상태를 전달하지 않습니다 — task 정체성·경로·workflow 상태는 모두 디스크 권위 파일에서 매번 계산됩니다.
|
|
19
19
|
- **Claude handoff (두 모드)**: (a) `okstra.sh` 가 새 `claude` 프로세스를 띄우는 전통 방식, (b) `okstra-run` skill 이 현재 claude 세션 안에서 prepare 후 lead 역할을 그대로 인계받는 in-session 모드. 둘 다 `prepare_task_bundle` 의 산출물(instruction-set 등)을 그대로 사용합니다.
|
|
20
20
|
- **Required team contract**: `Claude lead` + `Claude worker` · `Codex worker` · `Gemini worker` · `Report writer worker`의 필수 구성과 Agent Teams 우선 시도를 강제합니다.
|
|
21
|
-
- **User-home install + project-local task bundles**: `npx okstra@latest install` 한 명령이 런타임(`~/.okstra/{lib/python, bin}`) + 스킬 마크다운(`~/.claude/skills/<name>/SKILL.md`) 을 모두 깐다. 대상 프로젝트에는 task bundle 과 discovery metadata
|
|
21
|
+
- **User-home install + project-local task bundles**: `npx okstra@latest install` 한 명령이 런타임(`~/.okstra/{lib/python, bin, templates}`) + 스킬 마크다운(`~/.claude/skills/<name>/SKILL.md`) 을 모두 깐다. 대상 프로젝트에는 task bundle 과 discovery metadata 가 `.project-docs/okstra/` 아래 저장되고, **추가로 `<PROJECT_ROOT>/.claude/settings.local.json` 이 `~/.okstra/templates/settings.local.json` 을 가리키는 symlink 로 provisioning** 됩니다 (`okstra setup` 또는 `okstra-ctl` prepare 가 idempotent 하게 관리; 기존에 일반 파일이 있었다면 `.bak.<timestamp>` 로 백업 후 교체). 이 symlink 가 host Claude Code 세션에 자동 로드되어 codex/gemini worker wrapper 호출 권한을 부여하므로, 사용자의 글로벌 `~/.claude/settings.json` 은 건드리지 않으며 별도 `--settings` CLI 주입도 필요 없습니다. (개발용으로는 `okstra-install.sh` 가 `--link` 모드 symlink 설치를 제공합니다.)
|
|
22
22
|
- **Resume and clarification**: `--task-key`, `--resume-clarification`, `--clarification-response`로 같은 task 재개와 lead의 추가 질문 응답 흐름을 지원합니다.
|
|
23
23
|
- **Optional integrations**: worker error sidecar, token usage / cost accounting을 옵션으로 제공합니다.
|
|
24
24
|
|
|
@@ -213,7 +213,7 @@ per-process 환경 변수에 task 정체성·경로·workflow 상태를 보관
|
|
|
213
213
|
**Mode A — `okstra.sh` 가 새 claude 프로세스를 띄움**
|
|
214
214
|
- `--render-only`를 사용하면 Claude를 실행하지 않고 instruction-set 만 만든 뒤 종료합니다.
|
|
215
215
|
- `--render-only`가 없으면 prepare 단계가 Claude session ID 를 선할당하고 current run 의 `sessions/` 아래에 `claude-resume-<task-type>-<seq>.sh` 를 생성합니다.
|
|
216
|
-
- 이후 대상 프로젝트 루트에서 resolved `Claude lead` model execution value 로 `claude --model <lead> --session-id "$CLAUDE_SESSION_ID"
|
|
216
|
+
- 이후 대상 프로젝트 루트에서 resolved `Claude lead` model execution value 로 `claude --model <lead> --session-id "$CLAUDE_SESSION_ID" "$PROMPT"` 를 `exec` 합니다. (이전 버전의 `--settings <runtime-settings>` 인자는 0.14.0 부터 제거됨 — 권한은 `<PROJECT_ROOT>/.claude/settings.local.json` symlink 가 담당.)
|
|
217
217
|
- `okstra.sh` 는 handoff 까지만 수행하고, 최종 보고서 저장과 run/task 상태 갱신은 Claude lead 가 이어서 수행합니다.
|
|
218
218
|
|
|
219
219
|
**Mode B — `okstra-run` skill 이 현재 claude 세션 안에서 인계**
|
|
@@ -286,11 +286,14 @@ Claude launch prompt 본문은 항상 `prompts/launch.template.md` 템플릿에
|
|
|
286
286
|
"projectId": "sample-project-v2-api",
|
|
287
287
|
"projectRoot": "/Volumes/Workspaces/workspace/projects/sample-project",
|
|
288
288
|
"createdAt": "2026-05-10T00:00:00Z",
|
|
289
|
-
"updatedAt": "2026-05-10T00:00:00Z"
|
|
289
|
+
"updatedAt": "2026-05-10T00:00:00Z",
|
|
290
|
+
"worktreeSyncDirs": [".project-docs", ".scratch", "graphify-out", ".claude"]
|
|
290
291
|
}
|
|
291
292
|
```
|
|
292
293
|
|
|
293
|
-
처음 실행이면 위 4
|
|
294
|
+
처음 실행이면 위 4-필드(`projectId`, `projectRoot`, `createdAt`, `updatedAt`)를 새로 작성합니다. 이미 존재하면 `--project-id` 인자값과 저장된 `projectId` 가 일치하는지 검증한 뒤 `projectRoot`/`updatedAt` 만 갱신하고, 사용자가 추가한 알 수 없는 필드(`worktreeSyncDirs`, 향후 `mcpServers` 등)는 upsert 과정에서 보존됩니다. 불일치 시 즉시 종료해 동일 디렉토리에서 두 개의 ID 가 혼용되는 것을 막습니다.
|
|
295
|
+
|
|
296
|
+
`worktreeSyncDirs` (선택) 는 task worktree 로 symlink 할 project-root-relative 디렉토리 목록을 per-project 로 override 합니다. 해석 우선순위는 `OKSTRA_WORKTREE_SYNC_DIRS` 환경변수 → `project.json` → built-in default (`.project-docs`, `.scratch`, `graphify-out`, `.claude`). 빈 배열을 지정하면 sync 자체를 비활성화합니다.
|
|
294
297
|
|
|
295
298
|
`okstra-ctl` 의 reindex/backfill 도 신규 모델에서 권위 소스를 변경했습니다. 과거에는 `examples/projects/*.conf.sh` 를 source 했지만, 지금은 `~/.okstra/projects/<projectId>/meta.json` (record_start 가 위 project.json 정보를 mirror 한 결과) 을 스캔하여 (projectId, projectRoot) 매핑을 복원합니다. `OKSTRA_PROJECT_DEFINITION_DIR_OVERRIDE` 환경변수도 함께 폐기되었습니다.
|
|
296
299
|
|
|
@@ -321,7 +324,7 @@ Claude launch prompt 본문은 항상 `prompts/launch.template.md` 템플릿에
|
|
|
321
324
|
공통 제약:
|
|
322
325
|
|
|
323
326
|
- `implementation`을 제외한 모든 phase는 source code edit, build, migration, deployment, 그 밖의 state-mutating 명령을 금지합니다(`final-verification`은 read-only 테스트 명령만 허용). `implementation`은 승인된 plan의 파일 목록 안에서만 edit/commit이 허용되며, `git push`·publish·deploy·실제 migration·third-party write API는 여전히 금지됩니다.
|
|
324
|
-
- **모든 task-type 격리 worktree (BLOCKING)**: 모든 task-type 의 첫 번째 phase prepare 단계에서 `okstra-ctl` 이 자동으로 task-key 단위 `git worktree` 를 생성하고, 같은 task-key 의 이후 phase (`requirements-discovery` → `error-analysis` → `implementation-planning` → `implementation`) 는 동일한 worktree·브랜치를 재사용합니다. 위치는 `~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/` (segment 의 `/`·`:` 등 특수문자는 `-` 로 정규화) 이고, 브랜치 이름은 `<work-category-prefix>-<task-id-segment>` (예: `feat-dev-9436`, `fix-dev-7311`) 입니다. base ref 는 첫 phase prepare 시점의 main worktree `HEAD`. `~/.okstra/worktrees/registry.json` (flock-guarded) 가 task-key → path/branch 매핑을 전역 관리해 동시 실행 시 path·branch 충돌을 방지합니다. `.project-docs/`, `.scratch/`, `graphify-out/`
|
|
327
|
+
- **모든 task-type 격리 worktree (BLOCKING)**: 모든 task-type 의 첫 번째 phase prepare 단계에서 `okstra-ctl` 이 자동으로 task-key 단위 `git worktree` 를 생성하고, 같은 task-key 의 이후 phase (`requirements-discovery` → `error-analysis` → `implementation-planning` → `implementation`) 는 동일한 worktree·브랜치를 재사용합니다. 위치는 `~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/` (segment 의 `/`·`:` 등 특수문자는 `-` 로 정규화) 이고, 브랜치 이름은 `<work-category-prefix>-<task-id-segment>` (예: `feat-dev-9436`, `fix-dev-7311`) 입니다. base ref 는 첫 phase prepare 시점의 main worktree `HEAD`. `~/.okstra/worktrees/registry.json` (flock-guarded) 가 task-key → path/branch 매핑을 전역 관리해 동시 실행 시 path·branch 충돌을 방지합니다. `.project-docs/`, `.scratch/`, `graphify-out/`, `.claude/` 는 main worktree 에서 symlink 로 연결되어 모든 task 가 동일한 shared state 를 봅니다 (sync 대상 목록은 `project.json` 의 `worktreeSyncDirs` 또는 `OKSTRA_WORKTREE_SYNC_DIRS` 환경변수로 override 가능; 빈 배열이면 sync 비활성화). caller 가 이미 다른 worktree 안에 있거나 project_root 가 git repo 가 아니면 provisioning 은 skip 되고 executor 는 project_root 에서 그대로 작업합니다. worktree 는 run 종료 후 자동 삭제되지 않으며 후속 phase·PR 작성·rollback 검증의 권위 artefact 입니다. 수동 cleanup: `git -C <main-worktree> worktree remove <path>` → `git -C <main-worktree> branch -D <branch>` + registry 항목 삭제. 자세한 동작은 `prompts/profiles/implementation.md` 의 *Task worktree* 블록과 `agents/SKILL.md` 의 *Task worktree (BLOCKING for every task-type)* 섹션 참고.
|
|
325
328
|
- `implementation` 과 `release-handoff` 를 제외한 모든 phase 는 source code edit, build, migration, deployment, 그 밖의 state-mutating 명령을 금지합니다 (`final-verification` 은 read-only 테스트 명령만 허용). `implementation` 은 승인된 plan 의 파일 목록 안에서만 edit/commit 이 허용되며, `git push`·publish·deploy·실제 migration·third-party write API 는 여전히 금지됩니다. `release-handoff` 는 source code 자체는 수정하지 않고, 사용자가 메뉴로 선택한 commit / push / PR 명령만 실행합니다 (force push, base 브랜치 직접 push, hook bypass, release publish 는 여전히 금지).
|
|
326
329
|
- 사용자가 "다음 단계 진행해" 같은 표현을 보내도, 그 발화만으로 다음 phase가 자동 시작되지 않습니다. 다음 phase는 새 `okstra.sh` 실행으로만 시작합니다.
|
|
327
330
|
- **Authority & permissions assumption (HARD RULE — 모든 task-type 및 `okstra-schedule` 공통)**: 사용자(및 팀)는 예상되는 모든 작업에 대해 완전한 권한·승인 권한을 보유한다고 가정합니다. 외부 승인, 서드파티 액세스, 역할/IAM 권한, 조직적 sign-off, 법무·보안 검토, 벤더 협의, "권한 보유 여부 확인" 같은 항목을 routing 결정·missing inputs·clarification questions·risk·dependency·open questions·effort/day 추정에 포함하지 않습니다. okstra 내부 phase 핸드오프(`User Approval Request` 등)는 사용자 본인이 즉시 승인 가능한 내부 게이트이므로 영향 없으며, `implementation`의 forbidden actions(`git push`, prod deploy, shared-DB migration 등)도 권한 사유가 아닌 **안전 사유**로 계속 적용됩니다.
|
package/docs/kr/cli.md
CHANGED
|
@@ -227,7 +227,7 @@ scripts/okstra.sh --task-type implementation-planning ... \
|
|
|
227
227
|
### `--workers`
|
|
228
228
|
|
|
229
229
|
이번 run에서 사용할 worker 목록을 직접 지정합니다.
|
|
230
|
-
생략하면 기본값 `claude,codex,gemini,report-writer
|
|
230
|
+
생략하면 기본값 `claude,codex,report-writer`를 사용합니다. Gemini worker는 옵션이며 필요할 때 `--workers claude,codex,gemini,report-writer` 와 같이 명시적으로 추가하세요.
|
|
231
231
|
지정하면 전달한 worker subset만 dedupe 후 기록됩니다.
|
|
232
232
|
|
|
233
233
|
예:
|
|
@@ -287,7 +287,7 @@ fallback 기본값은 아래와 같습니다.
|
|
|
287
287
|
- Executor 는 이 run 에서 **유일하게 프로젝트 파일을 mutate 할 수 있는 worker** 입니다. 나머지 두 provider 는 같은 run 에서 strict read-only verifier 로 dispatch 됩니다.
|
|
288
288
|
- Executor 의 모델은 provider 별 worker 모델 플래그를 그대로 재사용합니다. 즉 `--executor codex` 이면 Executor 의 모델은 `--codex-model` (기본 `gpt-5.5`), `--executor gemini` 이면 `--gemini-model` (기본 `auto`) 가 됩니다.
|
|
289
289
|
- Claude/Codex/Gemini 세 verifier 는 executor provider 와 관계없이 항상 dispatch 됩니다. Executor 와 같은 provider 라도 별도 CLI 세션으로 verifier 가 호출되어 context 가 분리되므로 self-review 안전장치는 유지됩니다.
|
|
290
|
-
- 실제 파일 변경은 Codex/Gemini 의 경우 각 CLI 의 auto-edit 모드 (예: `codex exec --
|
|
290
|
+
- 실제 파일 변경은 Codex/Gemini 의 경우 각 CLI 의 auto-edit 모드 (예: `codex exec --sandbox workspace-write`) 를 통해 일어나며, Claude-side Edit/Write tool 을 거치지 않습니다.
|
|
291
291
|
- **Task worktree (모든 task-type 자동 격리)**: 모든 task-type 의 첫 번째 phase prepare 단계에서 `okstra-ctl` 이 `~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/` 에 `git worktree` 를 생성하고, 브랜치 `<work-category-prefix>-<task-id-segment>` 를 main worktree `HEAD` 에서 분기합니다. 같은 task-key 의 이후 phase 는 동일한 path/branch 를 재사용하므로 status 가 `reused` 로 기록됩니다 (run-prep 시점에 새 `git worktree add` 가 일어나지 않음). 모든 segment 의 `/`·`:` 등 특수문자는 `-` 로 정규화되며, `~/.okstra/worktrees/registry.json` 가 task-key → path/branch 매핑을 전역 관리합니다 (flock-guarded). Executor 의 Edit/Write/build/test/commit, verifier 의 read 는 모두 이 worktree 안에서 수행됩니다. caller 가 이미 다른 worktree 안에 있거나 project_root 가 git repo 가 아니면 provisioning 은 skip 되고 status 가 `skipped-in-worktree` / `skipped-not-git` 로 기록됩니다. 경로·브랜치 충돌은 `PrepareError` 로 즉시 실패시키며, run 종료 후 worktree 는 자동 삭제하지 않습니다 (수동: `git worktree remove` → `git branch -D` + registry 항목 삭제).
|
|
292
292
|
|
|
293
293
|
예:
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
package/runtime/agents/SKILL.md
CHANGED
|
@@ -105,7 +105,7 @@ For `--task-type implementation` runs, the task bundle additionally pins one of
|
|
|
105
105
|
|
|
106
106
|
Lead MUST dispatch Edit/Write-bearing work only through the `workerAgent` declared there. The other two providers still run as read-only verifiers in the same run; the executor's own provider is *also* dispatched separately as a verifier (a fresh CLI session) so the diff is reviewed by a context-isolated session. Session isolation is the primary self-review safeguard — same-model executor and same-provider verifier is acceptable when running in distinct sessions. Selecting a different model variant (e.g. executor=opus / Claude verifier=sonnet) is recommended but no longer mandatory.
|
|
107
107
|
|
|
108
|
-
Executor is chosen at run-prep time via `--executor <claude|codex|gemini>` (or `OKSTRA_DEFAULT_EXECUTOR`, fallback `claude`); the model used by the executor is taken from the corresponding worker model flag (`--claude-model` / `--codex-model` / `--gemini-model`). For Codex/Gemini executors, the underlying file mutation happens inside the executor CLI's own auto-edit mode (e.g. `codex exec --
|
|
108
|
+
Executor is chosen at run-prep time via `--executor <claude|codex|gemini>` (or `OKSTRA_DEFAULT_EXECUTOR`, fallback `claude`); the model used by the executor is taken from the corresponding worker model flag (`--claude-model` / `--codex-model` / `--gemini-model`). For Codex/Gemini executors, the underlying file mutation happens inside the executor CLI's own auto-edit mode (e.g. `codex exec --sandbox workspace-write`), not through Claude-side Edit/Write tools.
|
|
109
109
|
|
|
110
110
|
#### Task worktree (BLOCKING for every task-type)
|
|
111
111
|
|
|
@@ -32,7 +32,7 @@ $HOME/.okstra/bin/okstra-codex-exec.sh "<absolute-project-root>" "<assigned-mode
|
|
|
32
32
|
|
|
33
33
|
The wrapper internally runs:
|
|
34
34
|
```bash
|
|
35
|
-
codex exec -C "<project-root>" --model "<model>" --
|
|
35
|
+
codex exec -C "<project-root>" --model "<model>" --sandbox workspace-write - < "<prompt-path>" 2>/dev/null
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
The wrapper exists because Claude Code's Bash permission matcher rejects simple-prefix matches when the command contains stdin/stderr redirects. Calling `codex exec ... - < <path> 2>/dev/null` directly triggers a permission prompt every dispatch even when `Bash(codex exec:*)` is allowlisted. The wrapper folds the redirects inside, so the harness sees a single non-redirect command that matches `Bash($HOME/.okstra/bin/okstra-codex-exec.sh:*)`.
|
|
@@ -71,7 +71,7 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
|
|
|
71
71
|
```bash
|
|
72
72
|
$HOME/.okstra/bin/okstra-codex-exec.sh "<absolute-project-root>" "<assigned-model-execution-value>" "<absolute-prompt-history-path>"
|
|
73
73
|
```
|
|
74
|
-
Substitute the literal extracted Project Root, model execution value, and prompt-history path in place of the placeholders above. The wrapper handles `-C`, `--model`, `--
|
|
74
|
+
Substitute the literal extracted Project Root, model execution value, and prompt-history path in place of the placeholders above. The wrapper handles `-C`, `--model`, `--sandbox workspace-write`, the stdin redirect from the prompt file, and stderr suppression internally. Calling `codex exec` directly (without the wrapper) is an error in this skill: the redirect tokens disqualify the prefix match against `Bash(codex exec:*)` and produce a permission prompt every dispatch.
|
|
75
75
|
|
|
76
76
|
8. Return the codex output as-is without modification.
|
|
77
77
|
|
|
@@ -50,4 +50,4 @@ fi
|
|
|
50
50
|
|
|
51
51
|
# stdin redirect and stderr suppression are intentionally inside the wrapper —
|
|
52
52
|
# this is the entire reason this script exists.
|
|
53
|
-
exec codex exec -C "$project_root" --model "$model" --
|
|
53
|
+
exec codex exec -C "$project_root" --model "$model" --sandbox workspace-write - < "$prompt_path" 2>/dev/null
|
package/runtime/bin/okstra.sh
CHANGED
|
@@ -119,6 +119,7 @@ PY_ARGS=(
|
|
|
119
119
|
[[ "$APPROVE_PLAN_ACK" == "true" ]] && PY_ARGS+=(--approve)
|
|
120
120
|
[[ -n "${CLARIFICATION_RESPONSE_PATH-}" ]] && PY_ARGS+=(--clarification-response "$CLARIFICATION_RESPONSE_PATH")
|
|
121
121
|
[[ -n "${WORK_CATEGORY-}" ]] && PY_ARGS+=(--work-category "$WORK_CATEGORY")
|
|
122
|
+
[[ -n "${BASE_REF-}" ]] && PY_ARGS+=(--base-ref "$BASE_REF")
|
|
122
123
|
[[ "$RENDER_ONLY" == "true" ]] && PY_ARGS+=(--render-only)
|
|
123
124
|
[[ "$REFRESH_OKSTRA_ASSETS" == "true" ]] && PY_ARGS+=(--refresh-assets)
|
|
124
125
|
|
|
@@ -151,17 +152,20 @@ if [[ -z "$LAUNCH_JSON" ]]; then
|
|
|
151
152
|
fi
|
|
152
153
|
|
|
153
154
|
# Read fields via python (jq not assumed available).
|
|
154
|
-
read -r CLAUDE_SESSION_ID LEAD_MODEL_EXECUTION_VALUE PROJECT_ROOT_FROM_PY
|
|
155
|
+
read -r CLAUDE_SESSION_ID LEAD_MODEL_EXECUTION_VALUE PROJECT_ROOT_FROM_PY PROMPT_FILE < <(
|
|
155
156
|
okstra_py - "$LAUNCH_JSON" <<'PY'
|
|
156
157
|
import json, sys
|
|
157
158
|
d = json.loads(sys.argv[1])
|
|
158
|
-
print(d["claudeSessionId"], d["leadModelExecutionValue"], d["projectRoot"], d["
|
|
159
|
+
print(d["claudeSessionId"], d["leadModelExecutionValue"], d["projectRoot"], d["promptFile"])
|
|
159
160
|
PY
|
|
160
161
|
)
|
|
161
162
|
|
|
163
|
+
# Note: per-session --settings injection was removed. okstra-ctl prepare
|
|
164
|
+
# provisions <PROJECT_ROOT>/.claude/settings.local.json as a symlink to
|
|
165
|
+
# ~/.okstra/templates/settings.local.json, which Claude Code auto-loads
|
|
166
|
+
# whenever it runs inside that project — no CLI flag required.
|
|
162
167
|
PROMPT="$(cat "$PROMPT_FILE")"
|
|
163
168
|
cd "$PROJECT_ROOT_FROM_PY"
|
|
164
169
|
CLAUDE_COMMAND=(claude --model "$LEAD_MODEL_EXECUTION_VALUE" --session-id "$CLAUDE_SESSION_ID")
|
|
165
|
-
[[ -n "$OKSTRA_RUNTIME_SETTINGS_FILE" ]] && CLAUDE_COMMAND+=(--settings "$OKSTRA_RUNTIME_SETTINGS_FILE")
|
|
166
170
|
CLAUDE_COMMAND+=("$PROMPT")
|
|
167
171
|
exec "${CLAUDE_COMMAND[@]}"
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
- Required workers:
|
|
5
5
|
- claude
|
|
6
6
|
- codex
|
|
7
|
-
- gemini
|
|
8
7
|
- report-writer
|
|
8
|
+
- Optional workers (opt-in via `--workers`):
|
|
9
|
+
- gemini — when added to the roster it joins the verifier set; when omitted only the Claude+Codex verifiers participate (`--executor gemini` is therefore not selectable without explicitly listing `gemini` in `--workers`)
|
|
9
10
|
- **Executor binding (resolved at run-prep time, fixed for this run):**
|
|
10
11
|
- Executor display name: `{{EXECUTOR_DISPLAY_NAME}}`
|
|
11
12
|
- Executor provider: `{{EXECUTOR_PROVIDER}}` (one of: `claude` | `codex` | `gemini`; chosen via `--executor` or `OKSTRA_DEFAULT_EXECUTOR`, default `claude`)
|
|
@@ -13,12 +14,12 @@
|
|
|
13
14
|
- Executor model: `{{EXECUTOR_MODEL_DISPLAY}}` (launch value: `{{EXECUTOR_MODEL_EXECUTION_VALUE}}`)
|
|
14
15
|
- Wherever this profile mentions the `Executor`, it refers to the role bound above. The other two providers in the roster (`claude` / `codex` / `gemini` minus the executor) are dispatched as **verifiers only** for this run and remain strictly read-only.
|
|
15
16
|
{{INCLUDE:_common-contract.md}}
|
|
16
|
-
- Team contract (phase-specific overrides — `Claude worker` is replaced by `Executor` + verifier
|
|
17
|
-
- **Executor role:** the `Executor` (bound above) is the **only worker permitted to use Edit / Write / state-mutating Bash commands** on project files. All other workers run read-only. When the executor provider is `codex` or `gemini`, the actual file mutation happens inside the executor CLI's own auto-edit mode (e.g. `codex exec --
|
|
18
|
-
- **Verifier roles:** the
|
|
17
|
+
- Team contract (phase-specific overrides — `Claude worker` is replaced by `Executor` + verifier set in this phase):
|
|
18
|
+
- **Executor role:** the `Executor` (bound above) is the **only worker permitted to use Edit / Write / state-mutating Bash commands** on project files. All other workers run read-only. When the executor provider is `codex` or `gemini`, the actual file mutation happens inside the executor CLI's own auto-edit mode (e.g. `codex exec --sandbox workspace-write`, gemini's equivalent) — not through Claude-side Edit/Write tools — but the safety rules in this profile still apply identically.
|
|
19
|
+
- **Verifier roles:** the verifier slots are `Claude verifier` and `Codex verifier`, plus `Gemini verifier` **only when `gemini` is in the resolved `--workers` roster**. Every verifier in the resolved roster is dispatched regardless of which provider holds the executor role; the executor's own provider is run *separately* as a verifier (a fresh CLI session with no shared context) so that no verdict is produced from the same session that wrote the diff. Verifiers MUST NOT call Edit, Write, or any Bash command that mutates files outside the run's artifact directories. If a verifier wants a fix, it records the recommendation in its worker result; it does not apply the fix itself.
|
|
19
20
|
- Session isolation — not model-variant divergence — is the primary self-review safeguard: each verifier is a separate CLI invocation with its own context window, so reusing the same model variant for executor and same-provider verifier is acceptable. Different model variants (e.g. executor=opus / Claude verifier=sonnet) remain recommended when available.
|
|
20
|
-
- Phase-specific model defaults override the shared defaults: `Claude verifier`=`sonnet`, `Codex verifier`=`gpt-5.5`, `Gemini verifier`=`auto
|
|
21
|
-
- **All-verifier-failure policy**: if every verifier (`
|
|
21
|
+
- Phase-specific model defaults override the shared defaults: `Claude verifier`=`sonnet`, `Codex verifier`=`gpt-5.5`, `Gemini verifier`=`auto` (only when present in the roster). The `Executor`'s model is taken from the provider-specific worker model corresponding to `--executor`: claude→`--claude-model` (default `sonnet`, override to `opus` recommended when this run's executor is claude), codex→`--codex-model` (default `gpt-5.5`), gemini→`--gemini-model` (default `auto`).
|
|
22
|
+
- **All-verifier-failure policy**: if every verifier present in the resolved roster (`Claude verifier`, `Codex verifier`, and `Gemini verifier` when opted in) ends with a non-result terminal status (`timeout`, `error`, `not-run`) — i.e. zero independent verdicts were produced — the run MUST end with status `blocked` and route to a follow-up `error-analysis` run. `Claude lead` MUST NOT substitute its own verdict in place of the missing verifier outputs; synthesis requires at least one independent verifier's verdict. If one or more verifiers fail but at least one returns a verdict, the run proceeds with the surviving verdict(s) and the final report MUST explicitly notate which verifiers were unavailable, with the captured error / timeout evidence per failed verifier.
|
|
22
23
|
- Pre-implementation gate (mandatory — refuse to start if any item fails):
|
|
23
24
|
- 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`
|
|
24
25
|
- that file MUST contain a `User Approval Request` block (canonically placed at the **top of the report**, immediately under the metadata header) AND a recorded user approval marker. The canonical, recommended form is the single markdown checkbox line `- [x] Approved`. The runtime regex in `okstra_ctl.run._validate_approved_plan` also accepts (case-insensitive, line-anchored, optional leading `-`/`*`/`+` bullet): `APPROVED` (alone, followed by `:`, or end-of-line), `[x] Approved`, or `User Approval: APPROVED|granted|yes`. Free-form approvals such as "lgtm", "go ahead", or paraphrased confirmations are intentionally NOT accepted; if the user's approval is informal, re-edit the plan file to flip the top checkbox to `- [x] Approved` before invoking the implementation run.
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
- Base ref: `{{EXECUTOR_WORKTREE_BASE_REF}}` — commit SHA the worktree was branched from at the first phase; canonical `<base>` for every `git diff` / `git log` in this run.
|
|
35
36
|
- Provisioning note: `{{EXECUTOR_WORKTREE_NOTE}}`
|
|
36
37
|
- **Executor behaviour**: when status is `created` or `reused`, the Executor MUST run every Edit / Write / build / test / commit command with the working tree path above as cwd. Treat it as `project_root` for the duration of this run. Do NOT mutate the caller's original checkout. Do NOT `cd` out of the worktree to reach files; if a file outside the worktree is needed, the dependency is a planning gap — record it in `Out-of-plan edits` and continue.
|
|
37
|
-
- **Verifier behaviour**: all
|
|
38
|
+
- **Verifier behaviour**: all verifier roles in the resolved roster read from the SAME working tree path so they observe the exact diff the Executor produced. Verifiers remain strictly read-only there.
|
|
38
39
|
- **Lifecycle**: the worktree is kept after the run completes (no automatic cleanup) and is reused by every subsequent phase of the same task-key. Cleanup, when the task is fully done, is manual: `git -C <main-worktree> worktree remove <path>` followed by `git -C <main-worktree> branch -D <branch>`, plus removing the task-key entry from `~/.okstra/worktrees/registry.json`.
|
|
39
40
|
- **Skipped paths**: when status is `skipped-in-worktree` or `skipped-not-git`, the executor operates in `project_root` as before. Cite the status in the final report's metadata header so reviewers know which path was taken.
|
|
40
41
|
- **Synced state directories (symlinks into the MAIN worktree)**: at provision time `okstra-ctl` symlinks `.project-docs/`, `.scratch/`, and `graphify-out/` from the repo's **main worktree** into the task worktree (override via `OKSTRA_WORKTREE_SYNC_DIRS`; empty string disables). These are NOT independent copies — writes through them land in the main worktree. Inside this run the executor MUST confine writes under these paths to its own task scope (i.e. only `.project-docs/okstra/tasks/<this-task-id>/...`). Reading from elsewhere under the symlinks (other tasks, `graphify-out/GRAPH_REPORT.md`, `.scratch/` issues) is allowed and expected for context.
|
|
@@ -84,7 +85,7 @@
|
|
|
84
85
|
- **Out-of-plan edits block**: every file edited that was not in the approved plan's file list, with rationale (empty block is acceptable and preferred)
|
|
85
86
|
- **Validation evidence**: actual command output (stdout/stderr) for every `pre / mid / post` validation command from the plan. Truncated output is acceptable but the command line and exit code MUST be exact. No paraphrasing of test results.
|
|
86
87
|
- **TDD evidence (when applicable)**: for steps that should be TDD-ordered, show the failing-test output BEFORE the implementation commit and the passing-test output AFTER, with commit SHAs framing the transition.
|
|
87
|
-
- **Verifier results**: a section per verifier (`
|
|
88
|
+
- **Verifier results**: a section per verifier present in the resolved roster (`Claude verifier`, `Codex verifier`, and `Gemini verifier` when opted in) containing their independent verdict (PASS / CONCERNS / FAIL), their cited diff snippets, and any fix recommendations they declined to apply. `Claude lead` synthesises a unified verdict but MUST preserve dissent — do not collapse opinions into one paragraph.
|
|
88
89
|
- **Rollback verification**: confirmation that the plan's rollback path is still valid after the changes. Strength of verification depends on the change category:
|
|
89
90
|
- **Pure code changes** (no persisted state, no infra mutation): a reachable revert SHA is sufficient. Record the exact `git revert <SHA>` command that would undo the change, and confirm `git rev-parse <SHA>` resolves.
|
|
90
91
|
- **Feature-flag-gated changes**: confirm the off-switch path was exercised in this run's validation evidence (i.e. one of the validation commands ran with the flag off and succeeded). A plan that ships a flag without exercising the off-path does NOT satisfy this requirement.
|
|
@@ -95,7 +96,7 @@
|
|
|
95
96
|
1. **Plan coverage** — every step in the approved plan's recommended option must point to a commit (or an explicit `Skipped: <reason>` entry). List gaps.
|
|
96
97
|
2. **Evidence completeness** — every `Validation evidence` and `TDD evidence` claim has the actual command line and exit code? No paraphrased "tests pass" without output?
|
|
97
98
|
3. **Out-of-plan honesty** — files in the diff that are NOT in the plan list must appear in the `Out-of-plan edits` block. Cross-check with `git diff --name-only`.
|
|
98
|
-
4. **Verifier dissent preserved** — if the
|
|
99
|
+
4. **Verifier dissent preserved** — if the verifiers in the resolved roster disagree, the disagreement is visible in the report? Synthesis hides nothing?
|
|
99
100
|
5. **Forbidden action audit** — `git push`, publish, deploy, migration, third-party write commands: scan the run's session transcripts for any occurrence and confirm none happened.
|
|
100
101
|
6. **Placeholder scan** — restrict the scan to lines this run actually introduced; pre-existing placeholders in unchanged regions of touched files are out of scope. Required command (substitute `<base>` with the parent of the first commit in this run's commit list):
|
|
101
102
|
```
|
|
@@ -144,6 +144,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
144
144
|
WORK_CATEGORY="$(require_option_value --work-category "${2-}")"
|
|
145
145
|
shift 2
|
|
146
146
|
;;
|
|
147
|
+
--base-ref)
|
|
148
|
+
BASE_REF="$(require_option_value --base-ref "${2-}")"
|
|
149
|
+
shift 2
|
|
150
|
+
;;
|
|
147
151
|
--task-type)
|
|
148
152
|
ANALYSIS_TYPE="$(require_option_value --task-type "${2-}")"
|
|
149
153
|
shift 2
|
|
@@ -26,6 +26,7 @@ GEMINI_MODEL_OVERRIDE=""
|
|
|
26
26
|
REPORT_WRITER_MODEL_OVERRIDE=""
|
|
27
27
|
EXECUTOR_OVERRIDE=""
|
|
28
28
|
WORK_CATEGORY=""
|
|
29
|
+
BASE_REF=""
|
|
29
30
|
RELATED_TASKS_RAW=""
|
|
30
31
|
ANALYSIS_TYPE=""
|
|
31
32
|
BRIEF_PATH=""
|
|
@@ -150,7 +151,7 @@ GEMINI_WORKER_MODEL_DISPLAY=""
|
|
|
150
151
|
GEMINI_WORKER_MODEL_EXECUTION_VALUE=""
|
|
151
152
|
REPORT_WRITER_MODEL_DISPLAY=""
|
|
152
153
|
REPORT_WRITER_MODEL_EXECUTION_VALUE=""
|
|
153
|
-
DEFAULT_WORKERS="claude,codex,
|
|
154
|
+
DEFAULT_WORKERS="claude,codex,report-writer"
|
|
154
155
|
DEFAULT_LEAD_MODEL_NAME="${OKSTRA_DEFAULT_LEAD_MODEL:-opus}"
|
|
155
156
|
DEFAULT_CLAUDE_WORKER_MODEL_NAME="${OKSTRA_DEFAULT_CLAUDE_MODEL:-sonnet}"
|
|
156
157
|
DEFAULT_CODEX_WORKER_MODEL_NAME="${OKSTRA_DEFAULT_CODEX_MODEL:-gpt-5.5}"
|
|
@@ -65,7 +65,8 @@ options:
|
|
|
65
65
|
--refresh-assets Deprecated. okstra now installs skills/agents into ~/.claude and the codex
|
|
66
66
|
wrapper into ~/.okstra/bin via scripts/okstra-install.sh. Re-run that
|
|
67
67
|
installer with --refresh to update installed assets.
|
|
68
|
-
--workers Comma-separated worker list for this run. Default: claude,codex,
|
|
68
|
+
--workers Comma-separated worker list for this run. Default: claude,codex,report-writer
|
|
69
|
+
(Gemini worker is optional; add `gemini` explicitly, e.g. --workers claude,codex,gemini,report-writer)
|
|
69
70
|
--lead-model Model for Claude lead. Default: OKSTRA_DEFAULT_LEAD_MODEL or opus
|
|
70
71
|
--claude-model Model for Claude worker. Default: OKSTRA_DEFAULT_CLAUDE_MODEL or sonnet
|
|
71
72
|
--codex-model Model for Codex worker. Default: OKSTRA_DEFAULT_CODEX_MODEL or gpt-5.5
|
|
@@ -83,6 +84,12 @@ options:
|
|
|
83
84
|
Defaults to 'unknown' when omitted. Use this when the
|
|
84
85
|
lifecycle skipped --task-type=requirements-discovery
|
|
85
86
|
(where work-category would otherwise be inferred).
|
|
87
|
+
--base-ref Git ref (branch name, tag, or commit SHA) used as the
|
|
88
|
+
base of the per-task worktree on the FIRST phase of a
|
|
89
|
+
task-key. Required on first phase; ignored on
|
|
90
|
+
subsequent phases (the registered worktree is reused).
|
|
91
|
+
Typical values mirror the release-handoff PR-base picker:
|
|
92
|
+
main | dev | staging | preprod | prod | any local ref.
|
|
86
93
|
--task-type Set the task purpose for this run and select the matching profile file.
|
|
87
94
|
-h, --help Show this help.
|
|
88
95
|
|
|
@@ -23,7 +23,6 @@ import subprocess
|
|
|
23
23
|
from dataclasses import dataclass, field
|
|
24
24
|
from datetime import datetime, timezone
|
|
25
25
|
from pathlib import Path
|
|
26
|
-
from typing import Optional
|
|
27
26
|
|
|
28
27
|
from okstra_project import upsert_project_json
|
|
29
28
|
from .material import (
|
|
@@ -47,8 +46,9 @@ from .render import (
|
|
|
47
46
|
)
|
|
48
47
|
from .run_context import compute_and_write_run_context, write_run_inputs
|
|
49
48
|
from .seeding import (
|
|
49
|
+
SettingsLinkError,
|
|
50
50
|
cleanup_obsolete_generated_docs,
|
|
51
|
-
|
|
51
|
+
ensure_project_settings_symlink,
|
|
52
52
|
verify_installation,
|
|
53
53
|
)
|
|
54
54
|
from .session import (
|
|
@@ -90,6 +90,7 @@ class PrepareInputs:
|
|
|
90
90
|
executor: str = ""
|
|
91
91
|
related_tasks_raw: str = ""
|
|
92
92
|
work_category: str = ""
|
|
93
|
+
base_ref: str = ""
|
|
93
94
|
approved_plan_path: str = ""
|
|
94
95
|
clarification_response_path: str = "" # absolute or empty
|
|
95
96
|
render_only: bool = False
|
|
@@ -101,7 +102,6 @@ class PrepareInputs:
|
|
|
101
102
|
class PrepareOutputs:
|
|
102
103
|
ctx: dict
|
|
103
104
|
prompt_text: str
|
|
104
|
-
runtime_settings_path: Optional[Path]
|
|
105
105
|
extras: dict = field(default_factory=dict)
|
|
106
106
|
|
|
107
107
|
|
|
@@ -504,6 +504,15 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
504
504
|
raise PrepareError(
|
|
505
505
|
f"--executor must be one of: claude, codex, gemini (got: {executor_provider!r})"
|
|
506
506
|
)
|
|
507
|
+
if (
|
|
508
|
+
inp.task_type == "implementation"
|
|
509
|
+
and executor_provider not in workers
|
|
510
|
+
):
|
|
511
|
+
raise PrepareError(
|
|
512
|
+
f"--executor {executor_provider} requires {executor_provider!r} in "
|
|
513
|
+
f"--workers, but resolved roster is {workers!r}. "
|
|
514
|
+
f"Add it explicitly, e.g. --workers {','.join(sorted(set(workers + [executor_provider])))}."
|
|
515
|
+
)
|
|
507
516
|
executor_provider_to_meta = {
|
|
508
517
|
"claude": ("Claude executor", "claude-worker", cw),
|
|
509
518
|
"codex": ("Codex executor", "codex-worker", co),
|
|
@@ -537,6 +546,8 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
537
546
|
task_group_segment=ctx["TASK_GROUP_SEGMENT"],
|
|
538
547
|
task_id_segment=ctx["TASK_ID_SEGMENT"],
|
|
539
548
|
work_category=inp.work_category,
|
|
549
|
+
base_ref=inp.base_ref,
|
|
550
|
+
require_base_ref=True,
|
|
540
551
|
)
|
|
541
552
|
except RuntimeError as exc:
|
|
542
553
|
raise PrepareError(f"task worktree provisioning failed: {exc}") from exc
|
|
@@ -752,16 +763,26 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
752
763
|
file=__import__("sys").stderr,
|
|
753
764
|
)
|
|
754
765
|
|
|
755
|
-
runtime_settings_path = None
|
|
756
766
|
if not inp.render_only:
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
767
|
+
try:
|
|
768
|
+
link = ensure_project_settings_symlink(project_root=Path(inp.project_root))
|
|
769
|
+
except SettingsLinkError as exc:
|
|
770
|
+
print(
|
|
771
|
+
f"okstra-settings: failed to provision project settings symlink — "
|
|
772
|
+
f"worker dispatch may be blocked by Claude Code permissions. ({exc})",
|
|
773
|
+
file=__import__("sys").stderr,
|
|
774
|
+
)
|
|
775
|
+
else:
|
|
776
|
+
if link is None:
|
|
777
|
+
print(
|
|
778
|
+
"okstra-settings: ~/.okstra/templates/settings.local.json missing — "
|
|
779
|
+
"re-run 'npx okstra@latest install' (0.14.0+) to provision the symlink target.",
|
|
780
|
+
file=__import__("sys").stderr,
|
|
781
|
+
)
|
|
760
782
|
|
|
761
783
|
return PrepareOutputs(
|
|
762
784
|
ctx=ctx,
|
|
763
785
|
prompt_text=prompt_text,
|
|
764
|
-
runtime_settings_path=runtime_settings_path,
|
|
765
786
|
extras={"profile_content": profile_content},
|
|
766
787
|
)
|
|
767
788
|
|
|
@@ -818,6 +839,19 @@ def main(argv: list[str]) -> int:
|
|
|
818
839
|
"Falls back to `unknown` when omitted."
|
|
819
840
|
),
|
|
820
841
|
)
|
|
842
|
+
p.add_argument(
|
|
843
|
+
"--base-ref",
|
|
844
|
+
default="",
|
|
845
|
+
dest="base_ref",
|
|
846
|
+
help=(
|
|
847
|
+
"Git ref (branch name, tag, or commit SHA) used as the base of "
|
|
848
|
+
"the task worktree on the first phase of this task-key. "
|
|
849
|
+
"Required for first-phase prepare; ignored on subsequent phases "
|
|
850
|
+
"(the registered worktree is reused). Mirrors the PR-base picker "
|
|
851
|
+
"used by the `release-handoff` phase: typical values are "
|
|
852
|
+
"`main` / `dev` / `staging` / `preprod` / `prod` or any local ref."
|
|
853
|
+
),
|
|
854
|
+
)
|
|
821
855
|
args = p.parse_args(argv)
|
|
822
856
|
|
|
823
857
|
project_root = Path(args.project_root).expanduser().resolve()
|
|
@@ -854,6 +888,7 @@ def main(argv: list[str]) -> int:
|
|
|
854
888
|
executor=args.executor,
|
|
855
889
|
related_tasks_raw=args.related_tasks_raw,
|
|
856
890
|
work_category=args.work_category,
|
|
891
|
+
base_ref=args.base_ref,
|
|
857
892
|
approved_plan_path=args.approved_plan_path,
|
|
858
893
|
clarification_response_path=clarification_abs,
|
|
859
894
|
render_only=args.render_only,
|
|
@@ -893,7 +928,6 @@ def main(argv: list[str]) -> int:
|
|
|
893
928
|
"claudeSessionId": ctx["CLAUDE_SESSION_ID"],
|
|
894
929
|
"leadModelExecutionValue": ctx["LEAD_MODEL_EXECUTION_VALUE"],
|
|
895
930
|
"projectRoot": ctx["PROJECT_ROOT"],
|
|
896
|
-
"runtimeSettingsFile": str(out.runtime_settings_path) if out.runtime_settings_path else "",
|
|
897
931
|
"promptFile": str(Path(ctx["INSTRUCTION_SET_DIR"]) / "claude-execution-prompt.md"),
|
|
898
932
|
}
|
|
899
933
|
print(f"__OKSTRA_LAUNCH__ {json.dumps(machine)}")
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
"""okstra runtime asset verification +
|
|
1
|
+
"""okstra runtime asset verification + project settings provisioning.
|
|
2
2
|
|
|
3
3
|
okstra 가 깔아둔 런타임(`~/.okstra/lib/python`, `~/.okstra/bin`,
|
|
4
4
|
`~/.okstra/version`) 이 있는지 확인하고, 누락 시 InstallationError 로
|
|
5
|
-
surface 한다. 또한
|
|
6
|
-
|
|
5
|
+
surface 한다. 또한 대상 프로젝트의 `.claude/settings.local.json` 을
|
|
6
|
+
`~/.okstra/templates/settings.local.json` 으로 가리키는 symlink 로
|
|
7
|
+
provision 해서, host Claude Code 세션이 같은 프로젝트에서 일하는 동안
|
|
8
|
+
okstra worker wrapper 호출이 자동 허용되도록 한다.
|
|
7
9
|
"""
|
|
8
10
|
from __future__ import annotations
|
|
9
11
|
|
|
10
|
-
import
|
|
12
|
+
import os
|
|
13
|
+
import time
|
|
11
14
|
from pathlib import Path
|
|
12
15
|
from typing import Optional
|
|
13
16
|
|
|
@@ -16,6 +19,10 @@ class InstallationError(Exception):
|
|
|
16
19
|
"""okstra 가 깔아둔 런타임 자산이 누락됨."""
|
|
17
20
|
|
|
18
21
|
|
|
22
|
+
class SettingsLinkError(Exception):
|
|
23
|
+
"""`<project>/.claude/settings.local.json` symlink provisioning 실패."""
|
|
24
|
+
|
|
25
|
+
|
|
19
26
|
def required_install_paths() -> list[Path]:
|
|
20
27
|
"""okstra install 이 채워야 하는 최소 자산 경로."""
|
|
21
28
|
okstra_home = Path.home() / ".okstra"
|
|
@@ -82,16 +89,90 @@ def cleanup_obsolete_generated_docs(
|
|
|
82
89
|
pass
|
|
83
90
|
|
|
84
91
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
def _okstra_home() -> Path:
|
|
93
|
+
"""`~/.okstra` 의 절대경로. 테스트에서 `OKSTRA_HOME` 으로 override 가능."""
|
|
94
|
+
override = os.environ.get("OKSTRA_HOME", "").strip()
|
|
95
|
+
if override:
|
|
96
|
+
return Path(override)
|
|
97
|
+
return Path.home() / ".okstra"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def installed_settings_template_path() -> Path:
|
|
101
|
+
"""okstra install 이 만들어 둔 settings.local.json template 의 절대경로."""
|
|
102
|
+
return _okstra_home() / "templates" / "settings.local.json"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def ensure_project_settings_symlink(*, project_root: Path) -> Optional[Path]:
|
|
106
|
+
"""`<project_root>/.claude/settings.local.json` 을
|
|
107
|
+
`~/.okstra/templates/settings.local.json` 으로 가리키는 symlink 로
|
|
108
|
+
provisioning 한다.
|
|
109
|
+
|
|
110
|
+
Claude Code 가 그 프로젝트에서 host 세션으로 실행될 때 이 파일을
|
|
111
|
+
자동으로 로드하므로, okstra worker wrapper 호출(`okstra-codex-exec.sh`,
|
|
112
|
+
`okstra-gemini-exec.sh`) 이 별도 `--settings` 인자 없이도 허용된다.
|
|
113
|
+
|
|
114
|
+
반환값:
|
|
115
|
+
- target Path: symlink 가 새로 생성되었거나 이미 올바른 위치를
|
|
116
|
+
가리키고 있을 때.
|
|
117
|
+
- None: install 이 아직 settings template 을 깔지 않았을 때
|
|
118
|
+
(구버전 okstra install 등). 상위에서 경고로 흘려보낸다.
|
|
119
|
+
|
|
120
|
+
상위 호출자는 `SettingsLinkError` 만 처리하면 된다 — symlink target
|
|
121
|
+
의 dangling 여부, regular 파일 충돌, 사용자가 직접 만든 다른
|
|
122
|
+
symlink 등 의도된 boundary error 만 발생한다.
|
|
90
123
|
"""
|
|
91
|
-
|
|
92
|
-
|
|
124
|
+
project_root = Path(project_root)
|
|
125
|
+
template = installed_settings_template_path()
|
|
126
|
+
if not template.exists():
|
|
127
|
+
# install 이 0.13.x 이전 버전이면 templates/ 가 깔리지 않았을 수 있다.
|
|
128
|
+
# 상위에서 안내 메시지로 처리.
|
|
93
129
|
return None
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
130
|
+
|
|
131
|
+
claude_dir = project_root / ".claude"
|
|
132
|
+
target = claude_dir / "settings.local.json"
|
|
133
|
+
claude_dir.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
|
|
135
|
+
# idempotent: 이미 올바른 target 을 가리키는 symlink 면 no-op.
|
|
136
|
+
if target.is_symlink():
|
|
137
|
+
try:
|
|
138
|
+
current = os.readlink(target)
|
|
139
|
+
except OSError as exc:
|
|
140
|
+
raise SettingsLinkError(
|
|
141
|
+
f"failed to read existing symlink {target}: {exc}"
|
|
142
|
+
) from exc
|
|
143
|
+
if Path(current) == template or (claude_dir / current).resolve() == template.resolve():
|
|
144
|
+
return target
|
|
145
|
+
# okstra 가 관리하지 않는 다른 symlink 였으면 backup 후 교체.
|
|
146
|
+
_backup_and_replace(target, template)
|
|
147
|
+
return target
|
|
148
|
+
|
|
149
|
+
if target.exists():
|
|
150
|
+
# 일반 파일이 있으면 사용자 작성물일 가능성이 높다 — 손실 방지 backup.
|
|
151
|
+
_backup_and_replace(target, template)
|
|
152
|
+
return target
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
target.symlink_to(template)
|
|
156
|
+
except OSError as exc:
|
|
157
|
+
raise SettingsLinkError(
|
|
158
|
+
f"failed to create symlink {target} -> {template}: {exc}"
|
|
159
|
+
) from exc
|
|
97
160
|
return target
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _backup_and_replace(target: Path, template: Path) -> None:
|
|
164
|
+
"""기존 파일/심볼릭링크를 timestamped backup 으로 옮기고 새 symlink 생성."""
|
|
165
|
+
stamp = time.strftime("%Y%m%d-%H%M%S")
|
|
166
|
+
backup = target.with_name(f"{target.name}.bak.{stamp}")
|
|
167
|
+
try:
|
|
168
|
+
target.rename(backup)
|
|
169
|
+
except OSError as exc:
|
|
170
|
+
raise SettingsLinkError(
|
|
171
|
+
f"failed to back up existing {target} to {backup}: {exc}"
|
|
172
|
+
) from exc
|
|
173
|
+
try:
|
|
174
|
+
target.symlink_to(template)
|
|
175
|
+
except OSError as exc:
|
|
176
|
+
raise SettingsLinkError(
|
|
177
|
+
f"failed to create symlink {target} -> {template} after backup: {exc}"
|
|
178
|
+
) from exc
|
|
@@ -161,6 +161,17 @@ def _head_sha(cwd: Path) -> str:
|
|
|
161
161
|
return res.stdout.strip()
|
|
162
162
|
|
|
163
163
|
|
|
164
|
+
def _resolve_commit_sha(cwd: Path, ref: str) -> str:
|
|
165
|
+
"""Resolve a user-supplied ref (branch, tag, short/long SHA) to a full
|
|
166
|
+
commit SHA in `cwd`. Returns empty string when the ref is not resolvable
|
|
167
|
+
so the caller can raise a contextual error.
|
|
168
|
+
"""
|
|
169
|
+
res = _git(cwd, "rev-parse", "--verify", "--quiet", f"{ref}^{{commit}}")
|
|
170
|
+
if res.returncode != 0:
|
|
171
|
+
return ""
|
|
172
|
+
return res.stdout.strip()
|
|
173
|
+
|
|
174
|
+
|
|
164
175
|
def _main_worktree_path(project_root: Path) -> Path:
|
|
165
176
|
"""Locate the repository's MAIN worktree (the original checkout).
|
|
166
177
|
|
|
@@ -292,6 +303,8 @@ def provision_task_worktree(
|
|
|
292
303
|
task_group_segment: str,
|
|
293
304
|
task_id_segment: str,
|
|
294
305
|
work_category: str,
|
|
306
|
+
base_ref: str = "",
|
|
307
|
+
require_base_ref: bool = False,
|
|
295
308
|
) -> WorktreeProvision:
|
|
296
309
|
"""Materialise (or reuse) the task worktree for this run.
|
|
297
310
|
|
|
@@ -299,6 +312,13 @@ def provision_task_worktree(
|
|
|
299
312
|
Subsequent phases of the same task-key look up the registry and
|
|
300
313
|
return the existing path + branch unchanged.
|
|
301
314
|
|
|
315
|
+
``base_ref`` is the ref (branch name, tag, or commit SHA) to branch
|
|
316
|
+
the new worktree from on first phase. When empty, the main worktree's
|
|
317
|
+
current ``HEAD`` is used (legacy default; the CLI enforces a
|
|
318
|
+
non-empty value on first phase so callers go through the
|
|
319
|
+
AskUserQuestion menu in the okstra-run skill). Subsequent phases
|
|
320
|
+
ignore ``base_ref`` — the registered entry's base is reused.
|
|
321
|
+
|
|
302
322
|
Raises:
|
|
303
323
|
RuntimeError when worktree creation fails (path clash on disk
|
|
304
324
|
that the registry does not know about, branch clash with a
|
|
@@ -369,16 +389,34 @@ def provision_task_worktree(
|
|
|
369
389
|
)
|
|
370
390
|
|
|
371
391
|
main_root = _main_worktree_path(project_root)
|
|
372
|
-
|
|
373
|
-
if not
|
|
392
|
+
requested_base = (base_ref or "").strip()
|
|
393
|
+
if not requested_base and require_base_ref:
|
|
374
394
|
raise RuntimeError(
|
|
375
|
-
"
|
|
395
|
+
"first-phase task worktree requires an explicit base ref; "
|
|
396
|
+
"pass `--base-ref <branch|tag|sha>` (or invoke through the "
|
|
397
|
+
"okstra-run skill which collects this interactively)"
|
|
376
398
|
)
|
|
399
|
+
if requested_base:
|
|
400
|
+
resolved_sha = _resolve_commit_sha(main_root, requested_base)
|
|
401
|
+
if not resolved_sha:
|
|
402
|
+
raise RuntimeError(
|
|
403
|
+
f"could not resolve base ref `{requested_base}` in main worktree "
|
|
404
|
+
f"({main_root}); ensure the branch/tag/SHA exists locally"
|
|
405
|
+
)
|
|
406
|
+
resolved_base_ref = resolved_sha
|
|
407
|
+
base_origin = requested_base
|
|
408
|
+
else:
|
|
409
|
+
resolved_base_ref = _head_sha(main_root)
|
|
410
|
+
if not resolved_base_ref:
|
|
411
|
+
raise RuntimeError(
|
|
412
|
+
"could not resolve HEAD sha in main worktree; cannot create task worktree"
|
|
413
|
+
)
|
|
414
|
+
base_origin = "HEAD"
|
|
377
415
|
|
|
378
416
|
worktree_path.parent.mkdir(parents=True, exist_ok=True)
|
|
379
417
|
res = _git(
|
|
380
418
|
main_root,
|
|
381
|
-
"worktree", "add", "-b", branch, str(worktree_path),
|
|
419
|
+
"worktree", "add", "-b", branch, str(worktree_path), resolved_base_ref,
|
|
382
420
|
)
|
|
383
421
|
if res.returncode != 0:
|
|
384
422
|
raise RuntimeError(
|
|
@@ -398,7 +436,7 @@ def provision_task_worktree(
|
|
|
398
436
|
task_id=safe_task,
|
|
399
437
|
worktree_path=str(worktree_path),
|
|
400
438
|
branch=branch,
|
|
401
|
-
base_ref=
|
|
439
|
+
base_ref=resolved_base_ref,
|
|
402
440
|
phase=task_type,
|
|
403
441
|
)
|
|
404
442
|
except RuntimeError:
|
|
@@ -408,13 +446,18 @@ def provision_task_worktree(
|
|
|
408
446
|
_git(main_root, "branch", "-D", branch)
|
|
409
447
|
raise
|
|
410
448
|
|
|
449
|
+
base_label = (
|
|
450
|
+
f"{base_origin} @ {resolved_base_ref[:12]}"
|
|
451
|
+
if base_origin != "HEAD"
|
|
452
|
+
else f"HEAD @ {resolved_base_ref[:12]}"
|
|
453
|
+
)
|
|
411
454
|
return WorktreeProvision(
|
|
412
455
|
status="created",
|
|
413
456
|
path=str(worktree_path),
|
|
414
457
|
branch=branch,
|
|
415
|
-
base_ref=
|
|
458
|
+
base_ref=resolved_base_ref,
|
|
416
459
|
note=(
|
|
417
460
|
f"task worktree created at {worktree_path} on branch {branch} "
|
|
418
|
-
f"(base {
|
|
461
|
+
f"(base {base_label}; phase {task_type}){linked_suffix}"
|
|
419
462
|
),
|
|
420
463
|
)
|
|
@@ -141,6 +141,58 @@ If `implementation` chosen, ask two more `AskUserQuestion` in order:
|
|
|
141
141
|
- `"Path to the approved final-report.md (must contain APPROVED marker)"` — the underlying python `prepare_task_bundle` re-validates the marker, but you can pre-check with `grep`.
|
|
142
142
|
- `"Executor provider for this run (claude | codex | gemini)?"` — only this provider mutates project files; the other two run as read-only verifiers. Default `claude` (or `OKSTRA_DEFAULT_EXECUTOR` if set). Pass the answer through `PrepareInputs.executor`.
|
|
143
143
|
|
|
144
|
+
## Step 4.6: Base ref for the task worktree (first phase only)
|
|
145
|
+
|
|
146
|
+
`okstra prepare` provisions a per-task git worktree on first phase of a task-key
|
|
147
|
+
and reuses it on every subsequent phase. The base ref of that worktree is the
|
|
148
|
+
**user's choice**, not the caller's current `HEAD`, so the worktree never
|
|
149
|
+
silently inherits an unrelated branch you happen to be checked out on.
|
|
150
|
+
|
|
151
|
+
First, decide whether to ask:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
python3 - <<PY
|
|
155
|
+
import sys
|
|
156
|
+
from pathlib import Path
|
|
157
|
+
sys.path.insert(0, "$OKSTRA_PYTHONPATH".split(":")[0])
|
|
158
|
+
from okstra_ctl import worktree_registry
|
|
159
|
+
from okstra_ctl.ids import _safe_fs_segment
|
|
160
|
+
entry = worktree_registry.lookup(
|
|
161
|
+
_safe_fs_segment("<project-id>"),
|
|
162
|
+
_safe_fs_segment("<task-group>"),
|
|
163
|
+
_safe_fs_segment("<task-id>"),
|
|
164
|
+
)
|
|
165
|
+
print("REUSE" if (entry and entry.status == "active") else "ASK")
|
|
166
|
+
PY
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- `REUSE` → the registered worktree is reused; set `base_ref=""` and skip the
|
|
170
|
+
question (the registered base is authoritative).
|
|
171
|
+
- `ASK` → this is the first phase for this task-key. Continue.
|
|
172
|
+
|
|
173
|
+
`AskUserQuestion` (mirrors the `release-handoff` PR-base picker):
|
|
174
|
+
|
|
175
|
+
- **Label**: `"이 task worktree 의 base branch?"`
|
|
176
|
+
- **Options** (single-select):
|
|
177
|
+
1. `main` (recommended)
|
|
178
|
+
2. `dev`
|
|
179
|
+
3. `staging`
|
|
180
|
+
4. `preprod`
|
|
181
|
+
5. `prod`
|
|
182
|
+
6. Free text — let the user type any local ref (branch, tag, or full/short SHA).
|
|
183
|
+
|
|
184
|
+
Validate the chosen ref exists in the MAIN worktree before continuing:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
git -C "$(git -C "$PROJECT_ROOT" rev-parse --path-format=absolute --git-common-dir | xargs dirname)" \
|
|
188
|
+
rev-parse --verify --quiet "<chosen-ref>^{commit}" >/dev/null \
|
|
189
|
+
|| { echo "ref not found locally: <chosen-ref>"; exit 1; }
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Re-ask on failure. Echo the resolved short SHA back to the user
|
|
193
|
+
(`base 확정: <ref> (<short-sha>)`) and capture `base_ref=<chosen-ref>` for
|
|
194
|
+
Step 7.
|
|
195
|
+
|
|
144
196
|
## Step 5: Brief path
|
|
145
197
|
|
|
146
198
|
- New task: `AskUserQuestion` (free text) `"Path to the task brief markdown (relative to project root)"`. Verify file exists; re-ask on failure.
|
|
@@ -169,7 +221,7 @@ Do NOT ask for `workers_override` in implementation — the profile's required r
|
|
|
169
221
|
|
|
170
222
|
Ask each in turn (free text, blank = default):
|
|
171
223
|
|
|
172
|
-
1. `"참여 워커 목록 (쉼표 구분, 빈 칸 = 프로필 기본값). 선택지: claude, codex, gemini, report-writer"` → `workers_override`
|
|
224
|
+
1. `"참여 워커 목록 (쉼표 구분, 빈 칸 = 프로필 기본값 claude,codex,report-writer). 선택지: claude, codex, gemini, report-writer — gemini는 옵션이므로 필요할 때 명시"` → `workers_override`
|
|
173
225
|
2. `"리더(Claude lead) 모델? (빈 칸 = 기본값)"` → `lead_model`
|
|
174
226
|
3. `"claude 워커 모델? (빈 칸 = 기본값)"` → `claude_model`
|
|
175
227
|
4. `"codex 워커 모델? (빈 칸 = 기본값)"` → `codex_model`
|
|
@@ -189,6 +241,7 @@ Before calling `prepare_task_bundle`, echo the resolved selections back to the u
|
|
|
189
241
|
선택 확인:
|
|
190
242
|
task-type : implementation
|
|
191
243
|
task-key : <group>/<id>
|
|
244
|
+
base-ref : main (resolved <short-sha>) ← worktree base, first phase only
|
|
192
245
|
executor : codex
|
|
193
246
|
workers : (프로필 기본 — executor + verifier 2 + report-writer)
|
|
194
247
|
lead-model : default (opus)
|
|
@@ -231,6 +284,7 @@ out = prepare_task_bundle(PrepareInputs(
|
|
|
231
284
|
gemini_model="...", report_writer_model="...",
|
|
232
285
|
related_tasks_raw="...",
|
|
233
286
|
executor="<claude|codex|gemini or empty>", # implementation only; empty → default (claude / OKSTRA_DEFAULT_EXECUTOR)
|
|
287
|
+
base_ref="<chosen-ref-from-step-4.6 or empty when reusing existing worktree>",
|
|
234
288
|
approved_plan_path="<approved-plan-or-empty>",
|
|
235
289
|
clarification_response_path=str(clarification_abs) if clarification_abs else "",
|
|
236
290
|
render_only=True,
|
|
@@ -142,6 +142,37 @@ field → built-in default. Only edit when defaults don't cover the
|
|
|
142
142
|
project's working files (e.g. additional cache or local-config dirs
|
|
143
143
|
that must follow the executor into the worktree).
|
|
144
144
|
|
|
145
|
+
## Step 4.6 (automatic): project-local Claude settings symlink
|
|
146
|
+
|
|
147
|
+
`okstra setup` (and `okstra run` on its first invocation per project)
|
|
148
|
+
provisions `<PROJECT_ROOT>/.claude/settings.local.json` as a symlink to
|
|
149
|
+
`~/.okstra/templates/settings.local.json`. The template is installed
|
|
150
|
+
by `okstra install` 0.14.0+ and contains the Bash permission rules
|
|
151
|
+
required for the codex/gemini worker wrappers:
|
|
152
|
+
|
|
153
|
+
- `Bash($HOME/.okstra/bin/okstra-codex-exec.sh:*)`
|
|
154
|
+
- `Bash($HOME/.okstra/bin/okstra-gemini-exec.sh:*)`
|
|
155
|
+
|
|
156
|
+
Claude Code automatically loads `.claude/settings.local.json` whenever
|
|
157
|
+
it operates inside that project, so okstra workers dispatched from
|
|
158
|
+
**any** Claude Code session (host or okstra.sh-spawned) are allowed to
|
|
159
|
+
run their wrapper scripts without further configuration.
|
|
160
|
+
|
|
161
|
+
This replaces the previous per-run `--settings` injection model
|
|
162
|
+
(`<run-dir>/okstra-runtime-settings.json`) and the earlier guidance to
|
|
163
|
+
modify the user's global `~/.claude/settings.json`.
|
|
164
|
+
|
|
165
|
+
If a non-symlink `.claude/settings.local.json` already exists, the
|
|
166
|
+
setup step backs it up to `.claude/settings.local.json.bak.<timestamp>`
|
|
167
|
+
before installing the symlink — surface that to the user so they can
|
|
168
|
+
merge any project-specific rules back into a downstream file (the
|
|
169
|
+
symlinked template is okstra-owned and gets refreshed when okstra
|
|
170
|
+
updates).
|
|
171
|
+
|
|
172
|
+
To opt out (advanced): replace the symlink with a regular file. okstra
|
|
173
|
+
will detect that it is no longer a symlink on its next setup call and
|
|
174
|
+
back it up as `.bak.<timestamp>` rather than overwriting silently.
|
|
175
|
+
|
|
145
176
|
## Step 5: Verify
|
|
146
177
|
|
|
147
178
|
```bash
|
package/src/install.mjs
CHANGED
|
@@ -10,6 +10,11 @@ const AGENTS_MANIFEST_REL = "installed-agents.json";
|
|
|
10
10
|
const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
|
|
11
11
|
const CLAUDE_AGENTS_DIR = join(homedir(), ".claude", "agents");
|
|
12
12
|
|
|
13
|
+
// Source template (relative to runtime root in copy mode, or repo root in link mode).
|
|
14
|
+
const SETTINGS_TEMPLATE_SRC_REL = ["templates", "reports", "settings.template.json"];
|
|
15
|
+
// Destination under ~/.okstra/. Project-local .claude/settings.local.json symlinks here.
|
|
16
|
+
const SETTINGS_TEMPLATE_DST_REL = ["templates", "settings.local.json"];
|
|
17
|
+
|
|
13
18
|
const PYTHON_PACKAGES = ["okstra_project", "okstra_ctl", "okstra_token_usage", "lib"];
|
|
14
19
|
const BIN_ENTRYPOINTS = [
|
|
15
20
|
"okstra.sh",
|
|
@@ -33,6 +38,7 @@ Usage:
|
|
|
33
38
|
Effect (copy mode):
|
|
34
39
|
${"$HOME"}/.okstra/lib/python <- runtime/python
|
|
35
40
|
${"$HOME"}/.okstra/bin <- runtime/bin
|
|
41
|
+
${"$HOME"}/.okstra/templates/settings.local.json <- runtime/templates/reports/settings.template.json
|
|
36
42
|
${"$HOME"}/.claude/skills/<name> <- runtime/skills/<name> (per skill)
|
|
37
43
|
${"$HOME"}/.claude/agents/<worker>.md <- runtime/agents/workers/<worker>.md
|
|
38
44
|
${"$HOME"}/.okstra/installed-skills.json <- manifest of installed skills
|
|
@@ -42,11 +48,17 @@ Effect (copy mode):
|
|
|
42
48
|
Effect (link mode):
|
|
43
49
|
${"$HOME"}/.okstra/lib/python/<pkg> -> <repo>/scripts/<pkg> (symlink)
|
|
44
50
|
${"$HOME"}/.okstra/bin/<name>.sh -> <repo>/scripts/<name>.sh
|
|
51
|
+
${"$HOME"}/.okstra/templates/settings.local.json -> <repo>/templates/reports/settings.template.json
|
|
45
52
|
${"$HOME"}/.claude/skills/<name> -> <repo>/skills/<name> (symlink dir)
|
|
46
53
|
${"$HOME"}/.claude/agents/<worker>.md -> <repo>/agents/workers/<worker>.md
|
|
47
54
|
${"$HOME"}/.okstra/dev-link <- <repo> path stamp
|
|
48
55
|
${"$HOME"}/.okstra/version <- installed package version stamp
|
|
49
56
|
|
|
57
|
+
The settings.local.json file is the symlink target referenced by every
|
|
58
|
+
project-local <project>/.claude/settings.local.json that okstra-setup
|
|
59
|
+
provisions, granting per-project Claude Code permissions for okstra
|
|
60
|
+
worker wrapper scripts without modifying the user's global settings.
|
|
61
|
+
|
|
50
62
|
Worker agent definitions are installed into ${"$HOME"}/.claude/agents/ so
|
|
51
63
|
that Claude Code's subagent discovery picks them up; they cannot live
|
|
52
64
|
inside the package alone because the harness only scans ~/.claude/agents/
|
|
@@ -228,6 +240,8 @@ async function installLinkMode(repoPath, paths, opts) {
|
|
|
228
240
|
const agentResult = await installAgentsLink(repoAbs, { dryRun, quiet });
|
|
229
241
|
await writeAgentsManifest(paths.home, agentResult.installed, { dryRun });
|
|
230
242
|
|
|
243
|
+
await installSettingsTemplate(repoAbs, paths, { mode: "link", dryRun, quiet });
|
|
244
|
+
|
|
231
245
|
if (!dryRun) {
|
|
232
246
|
await writeFileAtomic(join(paths.home, "dev-link"), repoAbs + "\n", 0o644);
|
|
233
247
|
await writeFileAtomic(join(paths.home, "version"), paths.package + "\n", 0o644);
|
|
@@ -376,6 +390,51 @@ async function installAgentsLink(repoAbs, opts) {
|
|
|
376
390
|
return { installed: names };
|
|
377
391
|
}
|
|
378
392
|
|
|
393
|
+
async function installSettingsTemplate(srcRoot, paths, opts) {
|
|
394
|
+
const { mode, refresh = false, dryRun = false, quiet = false } = opts;
|
|
395
|
+
const src = join(srcRoot, ...SETTINGS_TEMPLATE_SRC_REL);
|
|
396
|
+
const dst = join(paths.home, ...SETTINGS_TEMPLATE_DST_REL);
|
|
397
|
+
|
|
398
|
+
if (!(await fileExists(src))) {
|
|
399
|
+
if (!quiet) process.stdout.write(` settings template: source missing — skipped (${src})\n`);
|
|
400
|
+
return { installed: false };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!dryRun) await fs.mkdir(join(dst, ".."), { recursive: true });
|
|
404
|
+
|
|
405
|
+
if (mode === "link") {
|
|
406
|
+
const action = await ensureSymlink(src, dst, { dryRun });
|
|
407
|
+
if (!quiet) process.stdout.write(` settings template: ${action} (${dst} -> ${src})\n`);
|
|
408
|
+
return { installed: action !== "skipped" };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// copy mode — hash-skip mirrors copyTreeIfChanged behavior for a single file.
|
|
412
|
+
let needsCopy = refresh;
|
|
413
|
+
if (!needsCopy) {
|
|
414
|
+
try {
|
|
415
|
+
await fs.access(dst);
|
|
416
|
+
const [srcHash, dstHash] = await Promise.all([hashFile(src), hashFile(dst)]);
|
|
417
|
+
needsCopy = srcHash !== dstHash;
|
|
418
|
+
} catch {
|
|
419
|
+
needsCopy = true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!needsCopy) {
|
|
424
|
+
if (!quiet) process.stdout.write(` settings template: skipped (hash match)\n`);
|
|
425
|
+
return { installed: false };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (dryRun) {
|
|
429
|
+
process.stdout.write(`[dry-run] copy ${src} -> ${dst}\n`);
|
|
430
|
+
} else {
|
|
431
|
+
const buf = await fs.readFile(src);
|
|
432
|
+
await writeFileAtomic(dst, buf, 0o644);
|
|
433
|
+
}
|
|
434
|
+
if (!quiet) process.stdout.write(` settings template: copied -> ${dst}\n`);
|
|
435
|
+
return { installed: true };
|
|
436
|
+
}
|
|
437
|
+
|
|
379
438
|
async function installSkillsCopy(runtimeRoot, opts) {
|
|
380
439
|
const { refresh, dryRun, quiet } = opts;
|
|
381
440
|
const srcRoot = join(runtimeRoot, "skills");
|
|
@@ -507,6 +566,8 @@ export async function runInstall(args) {
|
|
|
507
566
|
const agentResult = await installAgentsCopy(runtimeRoot, opts);
|
|
508
567
|
await writeAgentsManifest(paths.home, agentResult.installed, { dryRun: opts.dryRun });
|
|
509
568
|
|
|
569
|
+
await installSettingsTemplate(runtimeRoot, paths, { mode: "copy", ...opts });
|
|
570
|
+
|
|
510
571
|
if (!opts.dryRun) {
|
|
511
572
|
await writeFileAtomic(join(paths.home, "version"), paths.package + "\n", 0o644);
|
|
512
573
|
}
|
package/src/setup.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { promises as fs } from "node:fs";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { resolve as resolvePath } from "node:path";
|
|
5
|
+
import { join, resolve as resolvePath } from "node:path";
|
|
6
6
|
import { resolvePaths } from "./paths.mjs";
|
|
7
7
|
|
|
8
8
|
const USAGE = `okstra setup — register the current project with okstra
|
|
@@ -283,6 +283,68 @@ export async function run(args) {
|
|
|
283
283
|
return 1;
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
-
|
|
286
|
+
let settingsSymlink = null;
|
|
287
|
+
try {
|
|
288
|
+
settingsSymlink = await ensureProjectSettingsSymlink(projectRoot);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
process.stderr.write(
|
|
291
|
+
`warning: failed to provision .claude/settings.local.json symlink — ` +
|
|
292
|
+
`host Claude Code sessions in this project may need to add wrapper permissions manually. (${err.message})\n`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
process.stdout.write(
|
|
297
|
+
JSON.stringify(
|
|
298
|
+
{ ok: true, ...result, projectJsonPath, settingsLocalJson: settingsSymlink },
|
|
299
|
+
null,
|
|
300
|
+
2,
|
|
301
|
+
) + "\n",
|
|
302
|
+
);
|
|
287
303
|
return 0;
|
|
288
304
|
}
|
|
305
|
+
|
|
306
|
+
async function ensureProjectSettingsSymlink(projectRoot) {
|
|
307
|
+
const template = join(homedir(), ".okstra", "templates", "settings.local.json");
|
|
308
|
+
try {
|
|
309
|
+
await fs.access(template);
|
|
310
|
+
} catch {
|
|
311
|
+
return null; // install hasn't provisioned the template yet (pre-0.14 install)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const claudeDir = join(projectRoot, ".claude");
|
|
315
|
+
const target = join(claudeDir, "settings.local.json");
|
|
316
|
+
await fs.mkdir(claudeDir, { recursive: true });
|
|
317
|
+
|
|
318
|
+
let existingStat;
|
|
319
|
+
try {
|
|
320
|
+
existingStat = await fs.lstat(target);
|
|
321
|
+
} catch {
|
|
322
|
+
existingStat = null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (existingStat?.isSymbolicLink()) {
|
|
326
|
+
const current = await fs.readlink(target);
|
|
327
|
+
const resolved = current.startsWith("/") ? current : join(claudeDir, current);
|
|
328
|
+
if (resolved === template) return target;
|
|
329
|
+
await backupAndReplace(target, template);
|
|
330
|
+
return target;
|
|
331
|
+
}
|
|
332
|
+
if (existingStat) {
|
|
333
|
+
await backupAndReplace(target, template);
|
|
334
|
+
return target;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
await fs.symlink(template, target);
|
|
338
|
+
return target;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function backupAndReplace(target, template) {
|
|
342
|
+
const stamp = new Date()
|
|
343
|
+
.toISOString()
|
|
344
|
+
.replace(/[-:]/g, "")
|
|
345
|
+
.replace(/\..*/, "")
|
|
346
|
+
.replace("T", "-");
|
|
347
|
+
const backup = `${target}.bak.${stamp}`;
|
|
348
|
+
await fs.rename(target, backup);
|
|
349
|
+
await fs.symlink(template, target);
|
|
350
|
+
}
|
package/src/uninstall.mjs
CHANGED
|
@@ -180,6 +180,21 @@ export async function runUninstall(args) {
|
|
|
180
180
|
}
|
|
181
181
|
await removePath(join(paths.home, AGENTS_MANIFEST_REL), opts);
|
|
182
182
|
|
|
183
|
+
await removePath(join(paths.home, "templates", "settings.local.json"), opts);
|
|
184
|
+
// Remove templates/ if now empty.
|
|
185
|
+
const templatesDir = join(paths.home, "templates");
|
|
186
|
+
if (await pathExists(templatesDir)) {
|
|
187
|
+
try {
|
|
188
|
+
const entries = await fs.readdir(templatesDir);
|
|
189
|
+
if (entries.length === 0) {
|
|
190
|
+
if (!opts.dryRun) await fs.rmdir(templatesDir);
|
|
191
|
+
if (!opts.quiet) process.stdout.write(` removed empty: ${templatesDir}\n`);
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
/* ignore */
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
183
198
|
await removePath(join(paths.home, "version"), opts);
|
|
184
199
|
await removePath(join(paths.home, "dev-link"), opts);
|
|
185
200
|
|