okstra 0.13.2 → 0.14.1
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/docs/kr/architecture.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/codex-worker.md +34 -15
- package/runtime/agents/workers/gemini-worker.md +34 -15
- package/runtime/bin/okstra-codex-exec.sh +25 -6
- package/runtime/bin/okstra-gemini-exec.sh +25 -6
- package/runtime/bin/okstra.sh +6 -3
- package/runtime/python/okstra_ctl/run.py +17 -9
- package/runtime/python/okstra_ctl/seeding.py +95 -14
- package/runtime/skills/okstra-setup/SKILL.md +31 -0
- package/runtime/skills/okstra-team-contract/SKILL.md +6 -0
- package/src/install.mjs +61 -0
- package/src/setup.mjs +64 -2
- package/src/uninstall.mjs +15 -0
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 세션 안에서 인계**
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -18,7 +18,7 @@ description: |
|
|
|
18
18
|
</example>
|
|
19
19
|
model: inherit
|
|
20
20
|
color: cyan
|
|
21
|
-
tools: ["Bash", "Read", "Write", "Glob", "Grep"]
|
|
21
|
+
tools: ["Bash", "BashOutput", "KillShell", "Read", "Write", "Glob", "Grep"]
|
|
22
22
|
---
|
|
23
23
|
|
|
24
24
|
You are a Codex worker agent. Your job is to execute the OpenAI Codex CLI and return the analysis result.
|
|
@@ -27,12 +27,14 @@ You are a Codex worker agent. Your job is to execute the OpenAI Codex CLI and re
|
|
|
27
27
|
|
|
28
28
|
**Required form (uses the okstra wrapper to avoid redirect-triggered permission prompts):**
|
|
29
29
|
```bash
|
|
30
|
-
$HOME/.okstra/bin/okstra-codex-exec.sh "<absolute-project-root>" "<assigned-model-execution-value>" "<absolute-prompt-history-path>"
|
|
30
|
+
$HOME/.okstra/bin/okstra-codex-exec.sh "<absolute-project-root>" "<assigned-model-execution-value>" "<absolute-prompt-history-path>" [<absolute-worktree-path>]
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
The fourth argument is **mandatory for implementation phase** and optional otherwise. It must be the literal `EXECUTOR_WORKTREE_PATH` recorded in the run context; the wrapper forwards it to codex as `--add-dir`, which grants the codex sandbox write access to the worktree (where all implementation-phase mutations occur). Without it, codex's `workspace-write` sandbox is anchored only at `<project-root>` and rejects every Edit/Write that targets the worktree (EPERM), which is the failure pattern that originally motivated this argument.
|
|
34
|
+
|
|
33
35
|
The wrapper internally runs:
|
|
34
36
|
```bash
|
|
35
|
-
codex exec -C "<project-root>" --model "<model>" --sandbox workspace-write - < "<prompt-path>" 2>/dev/null
|
|
37
|
+
codex exec -C "<project-root>" [--add-dir "<worktree-path>"] --model "<model>" --sandbox workspace-write - < "<prompt-path>" 2>/dev/null
|
|
36
38
|
```
|
|
37
39
|
|
|
38
40
|
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:*)`.
|
|
@@ -67,20 +69,34 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
|
|
|
67
69
|
- If neither is available, immediately return `CODEX_MODEL_MISSING: assigned Codex model execution value was not provided`. Do NOT fall back to training-data defaults — historical codex defaults like `o4-mini` are NOT acceptable substitutes for the assigned model. Returning the sentinel is the correct behavior; the lead is responsible for fixing its prompt and redispatching.
|
|
68
70
|
- This rule applies equally to convergence reverify rounds. The reverify prompt MUST carry the same `**Model:**` line as the initial run (see `okstra-convergence` skill, "Required reverify-prompt anchor headers"). If the line is absent in a reverify prompt, return `CODEX_MODEL_MISSING` rather than guessing.
|
|
69
71
|
|
|
70
|
-
7. If installed,
|
|
72
|
+
7. If installed, dispatch the wrapper as a **background** Bash command and poll for completion. The two-minute foreground Bash timeout is insufficient for implementation-phase Codex runs and forced workers into ad-hoc background dispatch with lost output. The polling contract below is the formal replacement.
|
|
73
|
+
|
|
74
|
+
**Dispatch (background, no foreground timeout):**
|
|
71
75
|
```bash
|
|
72
|
-
$HOME/.okstra/bin/okstra-codex-exec.sh "<absolute-project-root>" "<assigned-model-execution-value>" "<absolute-prompt-history-path>"
|
|
76
|
+
$HOME/.okstra/bin/okstra-codex-exec.sh "<absolute-project-root>" "<assigned-model-execution-value>" "<absolute-prompt-history-path>" "<absolute-worktree-path>"
|
|
73
77
|
```
|
|
74
|
-
Substitute the literal extracted Project Root, model execution value,
|
|
78
|
+
Call `Bash` with `run_in_background: true`. Capture the returned `bash_id` (a.k.a. `shell_id`). Pass the positional arguments verbatim — do NOT use environment variables, `cd`, `&&` chains, or pipes from `cat`. Substitute the literal extracted Project Root, model execution value, prompt-history path, and worktree path. The fourth argument is **mandatory for implementation phase** (extract from `EXECUTOR_WORKTREE_PATH` in the lead prompt's run context or the `**Worktree:**` / `cwd for every mutating command:` line) and **may be omitted only for non-implementation analysis phases** that do not mutate the worktree. Omitting it during implementation will cause every Edit/Write to fail with EPERM. The wrapper handles `-C`, `--add-dir`, `--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.
|
|
79
|
+
|
|
80
|
+
**Poll loop (60-second cadence, 30-minute hard cap):**
|
|
81
|
+
- Record `start_ts` at dispatch time.
|
|
82
|
+
- Repeat:
|
|
83
|
+
1. Foreground `Bash` call: `sleep 60` (or shorter on the first iteration if you expect a fast finish).
|
|
84
|
+
2. Call `BashOutput(bash_id: <shell_id>)`. Inspect `status`.
|
|
85
|
+
3. If `status == "completed"`: break out of the loop and proceed to step 8.
|
|
86
|
+
4. If `now - start_ts > 1800` seconds: call `KillShell(shell_id: <shell_id>)`, then record a `cli-failure` event with `--error-type cli-failure`, `--exit-code 124`, `--duration-ms 1800000`, `--message "okstra-codex-exec.sh exceeded 30m polling cap"`, and return `CODEX_CLI_TIMEOUT: codex exec exceeded 30-minute polling cap`.
|
|
87
|
+
5. Otherwise continue polling.
|
|
88
|
+
- Do NOT abort the loop on transient `running` status. Only `completed` or the 30-minute cap end it.
|
|
89
|
+
- Do NOT issue parallel `BashOutput` calls or speculate about progress between polls.
|
|
75
90
|
|
|
76
|
-
8.
|
|
91
|
+
8. Concatenate the wrapper's accumulated stdout from `BashOutput` and return it as-is without modification. If the final `BashOutput` reports a non-zero `exit_code`, follow the **CLI failure** rule in §"Error reporting" before returning.
|
|
77
92
|
|
|
78
93
|
## Stop Condition
|
|
79
94
|
|
|
80
95
|
This wrapper is a thin Bash-execution shell over the Codex CLI (via `okstra-codex-exec.sh`). The CLI process itself is the analysis engine; this subagent's only job is to dispatch it and forward output. Therefore:
|
|
81
96
|
|
|
82
|
-
- Return immediately after the
|
|
83
|
-
-
|
|
97
|
+
- Return immediately after the polling loop exits with `completed` (or after recording any required `cli-failure` event for a non-zero exit / 30-minute cap / rate-limit).
|
|
98
|
+
- The only tool calls permitted during the polling loop are `Bash` (for `sleep`), `BashOutput`, and — on the timeout path only — `KillShell`. Do NOT perform additional `Read`, `Grep`, `Glob` calls between polls; do NOT inspect intermediate wrapper output mid-run.
|
|
99
|
+
- Outside the polling loop, no `Read`, `Grep`, or `Glob` beyond what is strictly required by steps 1–7 (prompt persistence, Project Root extraction, model resolution).
|
|
84
100
|
- Do NOT re-invoke `okstra-codex-exec.sh` to "double-check" or "rerun for safety" — convergence (Phase 5.5) handles cross-worker reconciliation. A single CLI dispatch per dispatched-prompt is the contract.
|
|
85
101
|
|
|
86
102
|
The Codex CLI's own exit terminates the underlying analysis; this wrapper terminates by returning its captured output (or sentinel).
|
|
@@ -96,9 +112,9 @@ This wrapper does NOT invoke MCP tools directly. MCP availability inside the Cod
|
|
|
96
112
|
- The assigned model execution value is canonical for CLI execution. Do not substitute a different Codex model unless the task bundle explicitly changes it.
|
|
97
113
|
- Pass the prompt received from Lead directly to codex after persisting the exact prompt to the assigned path.
|
|
98
114
|
- Include context (code, diff, file paths) if provided.
|
|
99
|
-
- For long prompts, the wrapper script reads from the saved project-local prompt history file via stdin redirect internally. The caller invokes the wrapper with three positional args:
|
|
115
|
+
- For long prompts, the wrapper script reads from the saved project-local prompt history file via stdin redirect internally. The caller invokes the wrapper with three required positional args + the worktree path for implementation phase:
|
|
100
116
|
```bash
|
|
101
|
-
$HOME/.okstra/bin/okstra-codex-exec.sh "<literal-project-root>" "<assigned-model-execution-value>" "<literal-prompt-history-path>"
|
|
117
|
+
$HOME/.okstra/bin/okstra-codex-exec.sh "<literal-project-root>" "<assigned-model-execution-value>" "<literal-prompt-history-path>" "<literal-worktree-path>"
|
|
102
118
|
```
|
|
103
119
|
- If the parent directory does not exist yet, create it before writing the prompt file.
|
|
104
120
|
|
|
@@ -146,9 +162,12 @@ two kinds of errors via `scripts/okstra-error-log.py`:
|
|
|
146
162
|
then append. Lead will dump it to the run error log after this subagent
|
|
147
163
|
terminates.
|
|
148
164
|
|
|
149
|
-
2. **CLI failure (lead-observed)** — if
|
|
150
|
-
|
|
151
|
-
|
|
165
|
+
2. **CLI failure (lead-observed)** — if the wrapper's final `BashOutput`
|
|
166
|
+
reports a non-zero `exit_code`, the 30-minute polling cap is hit, or the
|
|
167
|
+
captured stdout/stderr carries a rate-limit/auth message, immediately
|
|
168
|
+
append a `cli-failure` event directly to the run error log. The
|
|
169
|
+
30-minute-cap path additionally requires a prior `KillShell` call against
|
|
170
|
+
the dispatched `bash_id`:
|
|
152
171
|
|
|
153
172
|
```bash
|
|
154
173
|
python3 scripts/okstra-error-log.py append-observed \
|
|
@@ -158,7 +177,7 @@ two kinds of errors via `scripts/okstra-error-log.py`:
|
|
|
158
177
|
--agent codex-worker --agent-role worker \
|
|
159
178
|
--model "<assigned-model-execution-value>" \
|
|
160
179
|
--error-type cli-failure \
|
|
161
|
-
--command "$HOME/.okstra/bin/okstra-codex-exec.sh <project-root> <m> <prompt-path>" \
|
|
180
|
+
--command "$HOME/.okstra/bin/okstra-codex-exec.sh <project-root> <m> <prompt-path> <worktree-path>" \
|
|
162
181
|
--command-kind cli-invoke \
|
|
163
182
|
--exit-code <N> --duration-ms <ms> \
|
|
164
183
|
--message "<one-line summary>" \
|
|
@@ -18,7 +18,7 @@ description: |
|
|
|
18
18
|
</example>
|
|
19
19
|
model: inherit
|
|
20
20
|
color: green
|
|
21
|
-
tools: ["Bash", "Read", "Write", "Glob", "Grep"]
|
|
21
|
+
tools: ["Bash", "BashOutput", "KillShell", "Read", "Write", "Glob", "Grep"]
|
|
22
22
|
---
|
|
23
23
|
|
|
24
24
|
You are a Gemini worker agent. Your job is to execute the Google Gemini CLI and return the analysis result.
|
|
@@ -27,12 +27,14 @@ You are a Gemini worker agent. Your job is to execute the Google Gemini CLI and
|
|
|
27
27
|
|
|
28
28
|
**Required form (uses the okstra wrapper to avoid redirect-triggered permission prompts):**
|
|
29
29
|
```bash
|
|
30
|
-
$HOME/.okstra/bin/okstra-gemini-exec.sh "<absolute-project-root>" "<assigned-model-execution-value>" "<absolute-prompt-history-path>"
|
|
30
|
+
$HOME/.okstra/bin/okstra-gemini-exec.sh "<absolute-project-root>" "<assigned-model-execution-value>" "<absolute-prompt-history-path>" [<absolute-worktree-path>]
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
The fourth argument is **mandatory for implementation phase** and optional otherwise. It must be the literal `EXECUTOR_WORKTREE_PATH` recorded in the run context; the wrapper appends it to gemini's `--include-directories` list so the model can both read and operate on the worktree alongside project-root.
|
|
34
|
+
|
|
33
35
|
The wrapper internally runs:
|
|
34
36
|
```bash
|
|
35
|
-
gemini -p - -m "<model>" -o text --include-directories "<project-root>" < "<prompt-path>" 2>/dev/null
|
|
37
|
+
gemini -p - -m "<model>" -o text --include-directories "<project-root>[,<worktree-path>]" < "<prompt-path>" 2>/dev/null
|
|
36
38
|
```
|
|
37
39
|
|
|
38
40
|
The wrapper exists because Claude Code's Bash permission matcher rejects simple-prefix matches when the command contains stdin/stderr redirects. Calling `gemini -p - ... < <path> 2>/dev/null` directly triggers a permission prompt every dispatch even when `Bash(gemini:*)` is allowlisted. The wrapper folds the redirects inside, so the harness sees a single non-redirect command that matches `Bash($HOME/.okstra/bin/okstra-gemini-exec.sh:*)`.
|
|
@@ -67,20 +69,34 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
|
|
|
67
69
|
- If no assigned model execution value can be determined, immediately return `GEMINI_MODEL_MISSING: assigned Gemini model execution value was not provided`. Do NOT fall back to training-data defaults — historical Gemini defaults (e.g. `gemini-1.5-flash`) are NOT acceptable substitutes for the assigned model. Returning the sentinel is the correct behavior; the lead is responsible for fixing its prompt and redispatching.
|
|
68
70
|
- This rule applies equally to convergence reverify rounds. The reverify prompt MUST carry the same `**Model:**` line as the initial run (see `okstra-convergence` skill, "Required reverify-prompt anchor headers"). If the line is absent in a reverify prompt, return `GEMINI_MODEL_MISSING` rather than guessing.
|
|
69
71
|
|
|
70
|
-
7. If installed,
|
|
72
|
+
7. If installed, dispatch the wrapper as a **background** Bash command and poll for completion. The two-minute foreground Bash timeout is insufficient for implementation-phase Gemini runs and forced workers into ad-hoc background dispatch with lost output. The polling contract below is the formal replacement.
|
|
73
|
+
|
|
74
|
+
**Dispatch (background, no foreground timeout):**
|
|
71
75
|
```bash
|
|
72
|
-
$HOME/.okstra/bin/okstra-gemini-exec.sh "<absolute-project-root>" "<assigned-model-execution-value>" "<absolute-prompt-history-path>"
|
|
76
|
+
$HOME/.okstra/bin/okstra-gemini-exec.sh "<absolute-project-root>" "<assigned-model-execution-value>" "<absolute-prompt-history-path>" "<absolute-worktree-path>"
|
|
73
77
|
```
|
|
74
|
-
Substitute the literal extracted Project Root, model execution value,
|
|
78
|
+
Call `Bash` with `run_in_background: true`. Capture the returned `bash_id` (a.k.a. `shell_id`). Pass the positional arguments verbatim — do NOT use environment variables, `cd`, `&&` chains, or pipes from `cat`. Substitute the literal extracted Project Root, model execution value, prompt-history path, and worktree path. The fourth argument is **mandatory for implementation phase** (extract from `EXECUTOR_WORKTREE_PATH` in the lead prompt's run context or the `**Worktree:**` / `cwd for every mutating command:` line) and **may be omitted only for non-implementation analysis phases** that do not mutate the worktree. The wrapper handles `-p -`, `-m`, `-o text`, `--include-directories`, the stdin redirect from the prompt file, and stderr suppression internally. Calling `gemini` directly (without the wrapper) is an error in this skill: the redirect tokens disqualify the prefix match against `Bash(gemini:*)` and produce a permission prompt every dispatch.
|
|
79
|
+
|
|
80
|
+
**Poll loop (60-second cadence, 30-minute hard cap):**
|
|
81
|
+
- Record `start_ts` at dispatch time.
|
|
82
|
+
- Repeat:
|
|
83
|
+
1. Foreground `Bash` call: `sleep 60` (or shorter on the first iteration if you expect a fast finish).
|
|
84
|
+
2. Call `BashOutput(bash_id: <shell_id>)`. Inspect `status`.
|
|
85
|
+
3. If `status == "completed"`: break out of the loop and proceed to step 8.
|
|
86
|
+
4. If `now - start_ts > 1800` seconds: call `KillShell(shell_id: <shell_id>)`, then record a `cli-failure` event with `--error-type cli-failure`, `--exit-code 124`, `--duration-ms 1800000`, `--message "okstra-gemini-exec.sh exceeded 30m polling cap"`, and return `GEMINI_CLI_TIMEOUT: gemini exec exceeded 30-minute polling cap`.
|
|
87
|
+
5. Otherwise continue polling.
|
|
88
|
+
- Do NOT abort the loop on transient `running` status. Only `completed` or the 30-minute cap end it.
|
|
89
|
+
- Do NOT issue parallel `BashOutput` calls or speculate about progress between polls.
|
|
75
90
|
|
|
76
|
-
8.
|
|
91
|
+
8. Concatenate the wrapper's accumulated stdout from `BashOutput` and return it as-is without modification. If the final `BashOutput` reports a non-zero `exit_code`, follow the **CLI failure** rule in §"Error reporting" before returning.
|
|
77
92
|
|
|
78
93
|
## Stop Condition
|
|
79
94
|
|
|
80
95
|
This wrapper is a thin Bash-execution shell over the Gemini CLI (via `okstra-gemini-exec.sh`). The CLI process itself is the analysis engine; this subagent's only job is to dispatch it and forward output. Therefore:
|
|
81
96
|
|
|
82
|
-
- Return immediately after the
|
|
83
|
-
-
|
|
97
|
+
- Return immediately after the polling loop exits with `completed` (or after recording any required `cli-failure` event for a non-zero exit / 30-minute cap / rate-limit).
|
|
98
|
+
- The only tool calls permitted during the polling loop are `Bash` (for `sleep`), `BashOutput`, and — on the timeout path only — `KillShell`. Do NOT perform additional `Read`, `Grep`, `Glob` calls between polls; do NOT inspect intermediate wrapper output mid-run.
|
|
99
|
+
- Outside the polling loop, no `Read`, `Grep`, or `Glob` beyond what is strictly required by steps 1–7 (prompt persistence, Project Root extraction, model resolution).
|
|
84
100
|
- Do NOT re-invoke `okstra-gemini-exec.sh` to "double-check" or "rerun for safety" — convergence (Phase 5.5) handles cross-worker reconciliation. A single CLI dispatch per dispatched-prompt is the contract.
|
|
85
101
|
|
|
86
102
|
The Gemini CLI's own exit terminates the underlying analysis; this wrapper terminates by returning its captured output (or sentinel).
|
|
@@ -96,9 +112,9 @@ This wrapper does NOT invoke MCP tools directly. MCP availability inside the Gem
|
|
|
96
112
|
- The assigned model execution value is canonical for CLI execution. Do not substitute a different Gemini model unless the task bundle explicitly changes it.
|
|
97
113
|
- Pass the prompt received from Lead directly to gemini after persisting the exact prompt to the assigned path.
|
|
98
114
|
- Include context (code, diff, file paths) if provided.
|
|
99
|
-
- For long prompts, dispatch through the wrapper with literal absolute paths:
|
|
115
|
+
- For long prompts, dispatch through the wrapper with literal absolute paths (plus the worktree path for implementation phase):
|
|
100
116
|
```bash
|
|
101
|
-
$HOME/.okstra/bin/okstra-gemini-exec.sh "<literal-project-root>" "<assigned-model-execution-value>" "<literal-prompt-history-path>"
|
|
117
|
+
$HOME/.okstra/bin/okstra-gemini-exec.sh "<literal-project-root>" "<assigned-model-execution-value>" "<literal-prompt-history-path>" "<literal-worktree-path>"
|
|
102
118
|
```
|
|
103
119
|
- If the parent directory does not exist yet, create it before writing the prompt file.
|
|
104
120
|
|
|
@@ -146,9 +162,12 @@ two kinds of errors via `scripts/okstra-error-log.py`:
|
|
|
146
162
|
then append. Lead will dump it to the run error log after this subagent
|
|
147
163
|
terminates.
|
|
148
164
|
|
|
149
|
-
2. **CLI failure (lead-observed)** — if
|
|
150
|
-
|
|
151
|
-
|
|
165
|
+
2. **CLI failure (lead-observed)** — if the wrapper's final `BashOutput`
|
|
166
|
+
reports a non-zero `exit_code`, the 30-minute polling cap is hit, or the
|
|
167
|
+
captured stdout/stderr carries a rate-limit/auth message, immediately
|
|
168
|
+
append a `cli-failure` event directly to the run error log. The
|
|
169
|
+
30-minute-cap path additionally requires a prior `KillShell` call against
|
|
170
|
+
the dispatched `bash_id`:
|
|
152
171
|
|
|
153
172
|
```bash
|
|
154
173
|
python3 scripts/okstra-error-log.py append-observed \
|
|
@@ -158,7 +177,7 @@ two kinds of errors via `scripts/okstra-error-log.py`:
|
|
|
158
177
|
--agent gemini-worker --agent-role worker \
|
|
159
178
|
--model "<assigned-model-execution-value>" \
|
|
160
179
|
--error-type cli-failure \
|
|
161
|
-
--command "$HOME/.okstra/bin/okstra-gemini-exec.sh <project-root> <m> <prompt-path>" \
|
|
180
|
+
--command "$HOME/.okstra/bin/okstra-gemini-exec.sh <project-root> <m> <prompt-path> <worktree-path>" \
|
|
162
181
|
--command-kind cli-invoke \
|
|
163
182
|
--exit-code <N> --duration-ms <ms> \
|
|
164
183
|
--message "<one-line summary>" \
|
|
@@ -13,20 +13,29 @@
|
|
|
13
13
|
# Bash($HOME/.okstra/bin/okstra-codex-exec.sh:*)
|
|
14
14
|
#
|
|
15
15
|
# Usage:
|
|
16
|
-
# okstra-codex-exec.sh <project-root> <model-execution-value> <prompt-path>
|
|
16
|
+
# okstra-codex-exec.sh <project-root> <model-execution-value> <prompt-path> [worktree-path]
|
|
17
17
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
18
|
+
# project-root / model-execution-value / prompt-path are required.
|
|
19
|
+
#
|
|
20
|
+
# worktree-path is optional and used for okstra implementation phase, where the
|
|
21
|
+
# executor must mutate files inside a git worktree that lives outside
|
|
22
|
+
# project-root. When supplied (non-empty), it is forwarded to codex as
|
|
23
|
+
# `--add-dir <worktree-path>` so the codex sandbox grants write access to that
|
|
24
|
+
# directory alongside the primary workspace anchored at project-root. When
|
|
25
|
+
# omitted or empty, no `--add-dir` is added (existing analysis-phase behavior).
|
|
26
|
+
#
|
|
27
|
+
# The wrapper exits non-zero on any preflight failure.
|
|
20
28
|
set -euo pipefail
|
|
21
29
|
|
|
22
|
-
if [[ $# -
|
|
23
|
-
printf 'usage: %s <project-root> <model-execution-value> <prompt-path
|
|
30
|
+
if [[ $# -lt 3 || $# -gt 4 ]]; then
|
|
31
|
+
printf 'usage: %s <project-root> <model-execution-value> <prompt-path> [worktree-path]\n' "$(basename "$0")" >&2
|
|
24
32
|
exit 64
|
|
25
33
|
fi
|
|
26
34
|
|
|
27
35
|
project_root="$1"
|
|
28
36
|
model="$2"
|
|
29
37
|
prompt_path="$3"
|
|
38
|
+
worktree_path="${4-}"
|
|
30
39
|
|
|
31
40
|
if [[ -z "$project_root" || ! -d "$project_root" ]]; then
|
|
32
41
|
printf 'okstra-codex-exec: project-root is missing or not a directory: %q\n' "$project_root" >&2
|
|
@@ -48,6 +57,16 @@ if ! command -v codex >/dev/null 2>&1; then
|
|
|
48
57
|
exit 127
|
|
49
58
|
fi
|
|
50
59
|
|
|
60
|
+
if [[ -n "$worktree_path" && ! -d "$worktree_path" ]]; then
|
|
61
|
+
printf 'okstra-codex-exec: worktree-path was provided but is not a directory: %q\n' "$worktree_path" >&2
|
|
62
|
+
exit 68
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
extra_args=()
|
|
66
|
+
if [[ -n "$worktree_path" ]]; then
|
|
67
|
+
extra_args+=(--add-dir "$worktree_path")
|
|
68
|
+
fi
|
|
69
|
+
|
|
51
70
|
# stdin redirect and stderr suppression are intentionally inside the wrapper —
|
|
52
71
|
# this is the entire reason this script exists.
|
|
53
|
-
exec codex exec -C "$project_root" --model "$model" --sandbox workspace-write - < "$prompt_path" 2>/dev/null
|
|
72
|
+
exec codex exec -C "$project_root" ${extra_args[@]+"${extra_args[@]}"} --model "$model" --sandbox workspace-write - < "$prompt_path" 2>/dev/null
|
|
@@ -13,20 +13,29 @@
|
|
|
13
13
|
# Bash($HOME/.okstra/bin/okstra-gemini-exec.sh:*)
|
|
14
14
|
#
|
|
15
15
|
# Usage:
|
|
16
|
-
# okstra-gemini-exec.sh <project-root> <model-execution-value> <prompt-path>
|
|
16
|
+
# okstra-gemini-exec.sh <project-root> <model-execution-value> <prompt-path> [worktree-path]
|
|
17
17
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
18
|
+
# project-root / model-execution-value / prompt-path are required.
|
|
19
|
+
#
|
|
20
|
+
# worktree-path is optional and used for okstra implementation phase, where the
|
|
21
|
+
# executor must mutate files inside a git worktree that lives outside
|
|
22
|
+
# project-root. When supplied (non-empty), it is appended to gemini's
|
|
23
|
+
# `--include-directories` list (comma-separated) so the model can see and
|
|
24
|
+
# operate on the worktree alongside the primary workspace. When omitted or
|
|
25
|
+
# empty, only project-root is included (existing analysis-phase behavior).
|
|
26
|
+
#
|
|
27
|
+
# The wrapper exits non-zero on any preflight failure.
|
|
20
28
|
set -euo pipefail
|
|
21
29
|
|
|
22
|
-
if [[ $# -
|
|
23
|
-
printf 'usage: %s <project-root> <model-execution-value> <prompt-path
|
|
30
|
+
if [[ $# -lt 3 || $# -gt 4 ]]; then
|
|
31
|
+
printf 'usage: %s <project-root> <model-execution-value> <prompt-path> [worktree-path]\n' "$(basename "$0")" >&2
|
|
24
32
|
exit 64
|
|
25
33
|
fi
|
|
26
34
|
|
|
27
35
|
project_root="$1"
|
|
28
36
|
model="$2"
|
|
29
37
|
prompt_path="$3"
|
|
38
|
+
worktree_path="${4-}"
|
|
30
39
|
|
|
31
40
|
if [[ -z "$project_root" || ! -d "$project_root" ]]; then
|
|
32
41
|
printf 'okstra-gemini-exec: project-root is missing or not a directory: %q\n' "$project_root" >&2
|
|
@@ -48,8 +57,18 @@ if ! command -v gemini >/dev/null 2>&1; then
|
|
|
48
57
|
exit 127
|
|
49
58
|
fi
|
|
50
59
|
|
|
60
|
+
if [[ -n "$worktree_path" && ! -d "$worktree_path" ]]; then
|
|
61
|
+
printf 'okstra-gemini-exec: worktree-path was provided but is not a directory: %q\n' "$worktree_path" >&2
|
|
62
|
+
exit 68
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
include_dirs="$project_root"
|
|
66
|
+
if [[ -n "$worktree_path" ]]; then
|
|
67
|
+
include_dirs="$project_root,$worktree_path"
|
|
68
|
+
fi
|
|
69
|
+
|
|
51
70
|
# stdin redirect and stderr suppression are intentionally inside the wrapper —
|
|
52
71
|
# this is the entire reason this script exists. Gemini CLI has no `--cd` flag,
|
|
53
72
|
# so workspace correctness is anchored via `--include-directories` plus the
|
|
54
73
|
# Project Root referenced in the prompt body itself.
|
|
55
|
-
exec gemini -p - -m "$model" -o text --include-directories "$
|
|
74
|
+
exec gemini -p - -m "$model" -o text --include-directories "$include_dirs" < "$prompt_path" 2>/dev/null
|
package/runtime/bin/okstra.sh
CHANGED
|
@@ -152,17 +152,20 @@ if [[ -z "$LAUNCH_JSON" ]]; then
|
|
|
152
152
|
fi
|
|
153
153
|
|
|
154
154
|
# Read fields via python (jq not assumed available).
|
|
155
|
-
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 < <(
|
|
156
156
|
okstra_py - "$LAUNCH_JSON" <<'PY'
|
|
157
157
|
import json, sys
|
|
158
158
|
d = json.loads(sys.argv[1])
|
|
159
|
-
print(d["claudeSessionId"], d["leadModelExecutionValue"], d["projectRoot"], d["
|
|
159
|
+
print(d["claudeSessionId"], d["leadModelExecutionValue"], d["projectRoot"], d["promptFile"])
|
|
160
160
|
PY
|
|
161
161
|
)
|
|
162
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.
|
|
163
167
|
PROMPT="$(cat "$PROMPT_FILE")"
|
|
164
168
|
cd "$PROJECT_ROOT_FROM_PY"
|
|
165
169
|
CLAUDE_COMMAND=(claude --model "$LEAD_MODEL_EXECUTION_VALUE" --session-id "$CLAUDE_SESSION_ID")
|
|
166
|
-
[[ -n "$OKSTRA_RUNTIME_SETTINGS_FILE" ]] && CLAUDE_COMMAND+=(--settings "$OKSTRA_RUNTIME_SETTINGS_FILE")
|
|
167
170
|
CLAUDE_COMMAND+=("$PROMPT")
|
|
168
171
|
exec "${CLAUDE_COMMAND[@]}"
|
|
@@ -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 (
|
|
@@ -102,7 +102,6 @@ class PrepareInputs:
|
|
|
102
102
|
class PrepareOutputs:
|
|
103
103
|
ctx: dict
|
|
104
104
|
prompt_text: str
|
|
105
|
-
runtime_settings_path: Optional[Path]
|
|
106
105
|
extras: dict = field(default_factory=dict)
|
|
107
106
|
|
|
108
107
|
|
|
@@ -764,16 +763,26 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
764
763
|
file=__import__("sys").stderr,
|
|
765
764
|
)
|
|
766
765
|
|
|
767
|
-
runtime_settings_path = None
|
|
768
766
|
if not inp.render_only:
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
+
)
|
|
772
782
|
|
|
773
783
|
return PrepareOutputs(
|
|
774
784
|
ctx=ctx,
|
|
775
785
|
prompt_text=prompt_text,
|
|
776
|
-
runtime_settings_path=runtime_settings_path,
|
|
777
786
|
extras={"profile_content": profile_content},
|
|
778
787
|
)
|
|
779
788
|
|
|
@@ -919,7 +928,6 @@ def main(argv: list[str]) -> int:
|
|
|
919
928
|
"claudeSessionId": ctx["CLAUDE_SESSION_ID"],
|
|
920
929
|
"leadModelExecutionValue": ctx["LEAD_MODEL_EXECUTION_VALUE"],
|
|
921
930
|
"projectRoot": ctx["PROJECT_ROOT"],
|
|
922
|
-
"runtimeSettingsFile": str(out.runtime_settings_path) if out.runtime_settings_path else "",
|
|
923
931
|
"promptFile": str(Path(ctx["INSTRUCTION_SET_DIR"]) / "claude-execution-prompt.md"),
|
|
924
932
|
}
|
|
925
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
|
|
@@ -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
|
|
@@ -270,6 +270,12 @@ Workers MUST omit `source` / `recordedAt` / `agent` / `agentRole` / `model` /
|
|
|
270
270
|
Workers MUST use only `errorType: "tool-failure"` in the **sidecar file**.
|
|
271
271
|
|
|
272
272
|
- `cli-failure` events are recorded by the wrapper subagent itself (Codex / Gemini), but **directly to the run-level error log** via `okstra-error-log.py append-observed --error-type cli-failure ...` — NOT via the sidecar. The sidecar is an in-process tool-failure channel only.
|
|
273
|
+
- **Wrapper invocation arity.** Both `okstra-codex-exec.sh` and `okstra-gemini-exec.sh` accept four positional arguments: `<project-root> <model> <prompt-path> [<worktree-path>]`. The fourth (worktree) argument is **mandatory for implementation phase** and optional otherwise. For codex it becomes `--add-dir <worktree>` (sandbox write access); for gemini it is appended to `--include-directories`. Omitting it during implementation causes the codex sandbox to reject every Edit/Write targeting the worktree with EPERM. Workers extract the path from the `**Worktree:**` / `EXECUTOR_WORKTREE_PATH` / `cwd for every mutating command:` line in the lead prompt.
|
|
274
|
+
- **Background dispatch + polling contract (Codex / Gemini wrappers).** Both wrapper subagents MUST dispatch `okstra-codex-exec.sh` / `okstra-gemini-exec.sh` via `Bash(run_in_background: true)` and poll with `BashOutput(bash_id)` on a 60-second cadence, capped at 30 minutes (1800s). The legacy "single foreground `Bash` with 120000ms timeout" rule is retired — it forced workers into ad-hoc background dispatch that lost stdout and silently broke Phase 5 synthesis. The new rule applies in **every phase** (analysis runs typically complete in 1–2 polls, so there is no regression for short jobs). Recording responsibilities:
|
|
275
|
+
- Successful completion: return the wrapper's accumulated stdout from the final `BashOutput`. No log entry.
|
|
276
|
+
- Non-zero `exit_code` reported by `BashOutput`: record a `cli-failure` to the run-level error log with the real `exit_code` and observed `duration-ms`.
|
|
277
|
+
- 30-minute polling cap exceeded: call `KillShell(shell_id)` first, then record `cli-failure` with `--exit-code 124 --duration-ms 1800000 --message "<wrapper> exceeded 30m polling cap"`, then return the language-specific `*_CLI_TIMEOUT` sentinel.
|
|
278
|
+
- Token-usage matching is unaffected: the wrapper subagent stays alive throughout polling, so the wrapper's jsonl timestamp window continues to cover the underlying CLI rollout's full duration (see §"Token-usage accounting" below).
|
|
273
279
|
- `contract-violation` events (C) are recorded by Lead via `okstra-error-log.py append-observed --error-type contract-violation ...` after inspecting worker outputs.
|
|
274
280
|
- Lead's responsibility regarding the sidecar is to dump it to the run-level error log via `okstra-error-log.py append-from-worker` after each worker terminates; Lead does not write into the sidecar.
|
|
275
281
|
|
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
|
|