okstra 0.116.0 → 0.118.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.
@@ -0,0 +1,261 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { createInterface } from "node:readline/promises";
6
+ import { stdin as input, stdout as output } from "node:process";
7
+ import { resolveOkstraHome } from "../../lib/paths.mjs";
8
+
9
+ const NAME_RE = /^[A-Za-z0-9._-]+$/;
10
+ const DEFAULT_TEMPLATE_PATH = join(
11
+ dirname(fileURLToPath(import.meta.url)),
12
+ "default.md",
13
+ );
14
+
15
+ const USAGE = `okstra pr — PR body templates and PR generation
16
+
17
+ Usage:
18
+ okstra pr template list [--json]
19
+ okstra pr template show <name|default> [--json]
20
+ okstra pr template add --name <name> (--content <text> | --file <path>) [--yes]
21
+ okstra pr template path
22
+ okstra pr branches [--json]
23
+ okstra pr gen --base <ref> [--template <name|default>] [--json]
24
+ `;
25
+
26
+ export function isValidTemplateName(name) {
27
+ return (
28
+ typeof name === "string" &&
29
+ NAME_RE.test(name) &&
30
+ name !== "." &&
31
+ name !== ".."
32
+ );
33
+ }
34
+
35
+ export function templateDir(home) {
36
+ return join(home, "template", "pr");
37
+ }
38
+
39
+ export async function listTemplates(home) {
40
+ let entries;
41
+ try {
42
+ entries = await fs.readdir(templateDir(home));
43
+ } catch (err) {
44
+ if (err.code === "ENOENT") return [];
45
+ throw err;
46
+ }
47
+ return entries
48
+ .filter((f) => f.endsWith(".md"))
49
+ .map((f) => f.slice(0, -3))
50
+ .sort();
51
+ }
52
+
53
+ async function readBundledDefault() {
54
+ return fs.readFile(DEFAULT_TEMPLATE_PATH, "utf8");
55
+ }
56
+
57
+ export async function readTemplate(home, name) {
58
+ if (!name || name === "default") return readBundledDefault();
59
+ if (!isValidTemplateName(name)) throw new Error(`invalid template name: ${name}`);
60
+ try {
61
+ return await fs.readFile(join(templateDir(home), `${name}.md`), "utf8");
62
+ } catch (err) {
63
+ if (err.code === "ENOENT") return readBundledDefault();
64
+ throw err;
65
+ }
66
+ }
67
+
68
+ export async function addTemplate(home, name, content, opts = {}) {
69
+ if (!isValidTemplateName(name)) throw new Error(`invalid template name: ${name}`);
70
+ const dir = templateDir(home);
71
+ await fs.mkdir(dir, { recursive: true });
72
+ const target = join(dir, `${name}.md`);
73
+ if (!opts.force) {
74
+ try {
75
+ await fs.access(target);
76
+ throw new Error(`template exists: ${name} (use --yes to overwrite)`);
77
+ } catch (err) {
78
+ if (err.code !== "ENOENT") throw err;
79
+ }
80
+ }
81
+ await fs.writeFile(target, content, "utf8");
82
+ return target;
83
+ }
84
+
85
+ function takeValue(args, i, flag) {
86
+ const v = args[i + 1];
87
+ if (!v || v.startsWith("--")) throw new Error(`${flag} requires a value`);
88
+ return v;
89
+ }
90
+
91
+ async function readAddContent(opts) {
92
+ if (opts.content !== null) return opts.content;
93
+ if (opts.file !== null) return fs.readFile(resolve(opts.file), "utf8");
94
+ throw new Error("template add requires --content or --file");
95
+ }
96
+
97
+ async function confirm(question) {
98
+ const rl = createInterface({ input, output });
99
+ try {
100
+ const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
101
+ return answer === "y" || answer === "yes";
102
+ } finally {
103
+ rl.close();
104
+ }
105
+ }
106
+
107
+ async function cmdTemplate(args, home) {
108
+ const sub = args[0];
109
+ const rest = args.slice(1);
110
+ const json = rest.includes("--json");
111
+ if (sub === "list") {
112
+ const names = await listTemplates(home);
113
+ if (json) output.write(JSON.stringify({ templates: names }) + "\n");
114
+ else output.write(names.length ? names.join("\n") + "\n" : "(no templates)\n");
115
+ return 0;
116
+ }
117
+ if (sub === "show") {
118
+ const name = rest.find((a) => !a.startsWith("--"));
119
+ if (!name) throw new Error("template show requires a name");
120
+ const body = await readTemplate(home, name);
121
+ output.write(json ? JSON.stringify({ name, body }) + "\n" : body);
122
+ return 0;
123
+ }
124
+ if (sub === "path") {
125
+ output.write(templateDir(home) + "\n");
126
+ return 0;
127
+ }
128
+ if (sub === "add") {
129
+ const opts = { name: null, content: null, file: null, yes: false };
130
+ for (let i = 0; i < rest.length; i++) {
131
+ const flag = rest[i];
132
+ if (flag === "--name") opts.name = takeValue(rest, i++, flag);
133
+ else if (flag === "--content") opts.content = takeValue(rest, i++, flag);
134
+ else if (flag === "--file") opts.file = takeValue(rest, i++, flag);
135
+ else if (flag === "--yes") opts.yes = true;
136
+ else throw new Error(`unknown flag ${flag}`);
137
+ }
138
+ if (!opts.name) throw new Error("template add requires --name");
139
+ const content = await readAddContent(opts);
140
+ const dir = templateDir(home);
141
+ const target = join(dir, `${opts.name}.md`);
142
+ let force = opts.yes;
143
+ if (!force && isValidTemplateName(opts.name)) {
144
+ let exists = false;
145
+ try {
146
+ await fs.access(target);
147
+ exists = true;
148
+ } catch (err) {
149
+ if (err.code !== "ENOENT") throw err;
150
+ }
151
+ if (exists) {
152
+ if (!process.stdin.isTTY) {
153
+ throw new Error(`template exists: ${opts.name} (use --yes to overwrite)`);
154
+ }
155
+ force = await confirm(`Overwrite existing template ${opts.name}?`);
156
+ if (!force) { output.write("aborted\n"); return 1; }
157
+ }
158
+ }
159
+ const saved = await addTemplate(home, opts.name, content, { force: true });
160
+ output.write(`saved: ${saved}\n`);
161
+ return 0;
162
+ }
163
+ throw new Error(`unknown template subcommand: ${sub ?? "(none)"}`);
164
+ }
165
+
166
+ function tryGit(cwd, args) {
167
+ try {
168
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+
174
+ export function recommendBranches(cwd) {
175
+ const currentBranch = tryGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
176
+ if (currentBranch === null) return { currentBranch: null, recommended: [] };
177
+ const ordered = [];
178
+ const remoteDefault = tryGit(cwd, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
179
+ if (remoteDefault) {
180
+ const bare = remoteDefault.replace(/^origin\//, "");
181
+ if (tryGit(cwd, ["rev-parse", "--verify", "--quiet", `${bare}^{commit}`]) !== null) ordered.push(bare);
182
+ else if (tryGit(cwd, ["rev-parse", "--verify", "--quiet", `${remoteDefault}^{commit}`]) !== null) ordered.push(remoteDefault);
183
+ }
184
+ for (const b of ["main", "master", "develop"]) {
185
+ const local = tryGit(cwd, ["show-ref", "--verify", "--quiet", `refs/heads/${b}`]);
186
+ if (local !== null) ordered.push(b);
187
+ }
188
+ const recent = tryGit(cwd, [
189
+ "for-each-ref", "--sort=-committerdate", "--format=%(refname:short)", "refs/heads",
190
+ ]);
191
+ if (recent) ordered.push(...recent.split("\n").filter(Boolean));
192
+ const recommended = [];
193
+ for (const b of ordered) {
194
+ if (b && b !== currentBranch && !recommended.includes(b)) recommended.push(b);
195
+ }
196
+ return { currentBranch, recommended };
197
+ }
198
+
199
+ function cmdBranches(args, cwd) {
200
+ const json = args.includes("--json");
201
+ const r = recommendBranches(cwd);
202
+ if (json) output.write(JSON.stringify(r) + "\n");
203
+ else output.write((r.recommended.join("\n") || "(no branches)") + "\n");
204
+ return 0;
205
+ }
206
+
207
+ export async function buildGenBundle({ cwd, home, base, templateName }) {
208
+ if (!base) throw new Error("gen requires --base");
209
+ const verified = tryGit(cwd, ["rev-parse", "--verify", "--quiet", `${base}^{commit}`]);
210
+ if (verified === null) throw new Error(`base ref not found: ${base}`);
211
+ const currentBranch = tryGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
212
+ const logRaw = tryGit(cwd, [
213
+ "log", "--reverse", "--format=%h%x09%s", `${base}..HEAD`,
214
+ ]) || "";
215
+ const commits = logRaw
216
+ .split("\n")
217
+ .filter(Boolean)
218
+ .map((line) => {
219
+ const tab = line.indexOf("\t");
220
+ return { hash: line.slice(0, tab), subject: line.slice(tab + 1) };
221
+ });
222
+ const diffStat = tryGit(cwd, ["diff", "--stat", `${base}...HEAD`]) || "";
223
+ const template = await readTemplate(home, templateName);
224
+ const resolvedName = templateName && templateName !== "default"
225
+ ? templateName
226
+ : "default";
227
+ return { base, currentBranch, templateName: resolvedName, template, commits, diffStat };
228
+ }
229
+
230
+ async function cmdGen(args, cwd, home) {
231
+ const opts = { base: null, template: "default" };
232
+ for (let i = 0; i < args.length; i++) {
233
+ const flag = args[i];
234
+ if (flag === "--base") opts.base = takeValue(args, i++, flag);
235
+ else if (flag === "--template") opts.template = takeValue(args, i++, flag);
236
+ else if (flag === "--json") continue;
237
+ else throw new Error(`unknown flag ${flag}`);
238
+ }
239
+ const bundle = await buildGenBundle({ cwd, home, base: opts.base, templateName: opts.template });
240
+ output.write(JSON.stringify(bundle) + "\n");
241
+ return 0;
242
+ }
243
+
244
+ export async function run(args) {
245
+ if (args.length === 0 || args.includes("--help")) {
246
+ output.write(USAGE);
247
+ return 0;
248
+ }
249
+ const home = resolveOkstraHome();
250
+ const sub = args[0];
251
+ const rest = args.slice(1);
252
+ try {
253
+ if (sub === "template") return await cmdTemplate(rest, home);
254
+ if (sub === "branches") return cmdBranches(rest, process.cwd());
255
+ if (sub === "gen") return await cmdGen(rest, process.cwd(), home);
256
+ throw new Error(`unknown pr subcommand: ${sub}`);
257
+ } catch (err) {
258
+ process.stderr.write(`error: ${err.message}\n`);
259
+ return 1;
260
+ }
261
+ }
@@ -15,6 +15,7 @@ export const USER_SKILL_NAMES = Object.freeze([
15
15
  "okstra-schedule",
16
16
  "okstra-container-build",
17
17
  "okstra-graphify",
18
+ "okstra-pr-gen",
18
19
  ]);
19
20
 
20
21
  // Names okstra used to install as skills before they were renamed or moved
package/README.kr.md DELETED
@@ -1,231 +0,0 @@
1
- # okstra
2
-
3
- > npm: [`okstra`](https://www.npmjs.com/package/okstra) · 설치: `npx -y okstra@latest install`
4
- >
5
- > English: [`README.md`](README.md)
6
-
7
- ## 인덱스
8
-
9
- - [1. 용도](#1-용도)
10
- - [2. 구조](#2-구조)
11
- - [2.1 repo 레이아웃](#21-repo-레이아웃)
12
- - [2.2 설치 후 사용자 머신 레이아웃](#22-설치-후-사용자-머신-레이아웃)
13
- - [2.3 단일 권위 요약](#23-단일-권위-요약)
14
- - [3. 사용 매뉴얼](#3-사용-매뉴얼)
15
- - [3.1 최초 셋업 (머신당 1회)](#31-최초-셋업-머신당-1회)
16
- - [3.2 프로젝트 등록 (프로젝트당 1회)](#32-프로젝트-등록-프로젝트당-1회)
17
- - [3.3 일상 명령](#33-일상-명령)
18
- - [3.4 CLI 모드 (선택)](#34-cli-모드-선택)
19
- - [3.5 운영 명령](#35-운영-명령)
20
- - [4. 더 읽을 자료](#4-더-읽을-자료)
21
-
22
- ## 1. 용도
23
-
24
- `okstra` 는 **Claude Code 안에서 lead + worker 모델로 작업을 cross-verify 하기 위한 정형화된 task 실행 러너**입니다. Claude lead 가 phase 진행을 주도하고, 독립된 분석 worker — **기본 Claude · Codex** (Antigravity 는 옵션으로 명시할 때만 추가) — 와 최종 보고서 작성을 전담하는 report-writer 를 dispatch 합니다.
25
-
26
- 설계의 세 가지 원칙:
27
-
28
- - **단일 진입점 (`prepare_task_bundle`)**: 어떤 호출자(스킬, bash CLI, in-session) 가 들어와도 같은 task 디렉터리 구조 · 매니페스트 · 검증 경로를 산출합니다.
29
- - **task 정체성의 영구화**: `<project-id>/<task-group>/<task-id>` stable task key 가 phase 간 / 세션 간 / 모델 업그레이드 사이의 컨텍스트를 잇습니다.
30
- - **lead/worker 계약 강제**: 산출 schema 와 검증 절차가 `templates/` + `validators/` 에 묶여 있고, worker roster 는 phase 별로 고정되어 있습니다.
31
-
32
- okstra 는 단발성 코드 리뷰 도구가 **아닙니다**. **여러 phase 로 나뉘고, 여러 에이전트가 의견을 내야 하며, 각 phase 의 출력이 다음 phase 의 입력이 되는** 작업을 위한 도구입니다.
33
-
34
- ## 2. 구조
35
-
36
- ### 2.1 repo 레이아웃
37
-
38
- ```
39
- okstra/ npm 패키지 = repo 루트
40
- ├── package.json name: "okstra"
41
- ├── bin/okstra 노드 CLI 진입점
42
- ├── src/ Node CLI 명령 모듈 (install, wizard, config, render, token usage, ...)
43
- ├── tools/build.mjs runtime/ 동기화 스크립트 (prepack 에서 호출)
44
- ├── runtime/ gitignored 설치 payload; ~/.okstra 로 복사
45
- ├── scripts/ python + bash 런타임 소스
46
- ├── skills/ public 스킬 마크다운 소스 (사용자 노출 8종)
47
- ├── agents/ worker agent 마크다운 소스
48
- ├── prompts/, schemas/, templates/, validators/
49
- ├── docs/kr/ 한국어 상세 매뉴얼 (architecture.md, cli.md)
50
- ├── tests/, tests-e2e/
51
- ├── .claude-plugin/plugin.json 보조 skills-CLI 채널 매니페스트
52
- ├── .github/workflows/ release-please.yml, release.yml
53
- └── RELEASING.md, CHANGELOG.md, README.md, README.kr.md
54
- ```
55
-
56
- `runtime/` 은 `prepack` 시점에 `tools/build.mjs` 가 `scripts/`, `skills/`, `agents/`, `prompts/`, `schemas/`, `templates/`, `validators/` 로부터 다시 빌드합니다. `~/.okstra` 로 복사되는 설치 payload이며, npm package는 이 payload와 함께 Node CLI(`bin/`, `src/`), docs, README를 배포합니다.
57
-
58
- ### 2.2 설치 후 사용자 머신 레이아웃
59
-
60
- ```
61
- ~/.okstra/ 런타임 홈, `okstra install` 이 생성
62
- ├── version 패키지 버전 stamp
63
- ├── lib/python/ okstra_project/, okstra_ctl/, okstra_token_usage/, lib/
64
- ├── bin/ okstra.sh, codex-exec, antigravity-exec, ...
65
- ├── templates/ report asset, settings template
66
- ├── prompts/ lead 운영 계약(prompts/lead/*) + coding-preflight 리소스 팩
67
- ├── installed-runtimes.json 설치된 runtime adapter 매니페스트
68
- ├── installed-skills.json 설치된 스킬 매니페스트 (uninstall 이 사용)
69
- ├── installed-agents.json 설치된 워커 에이전트 매니페스트 (uninstall 이 사용)
70
- ├── recent.jsonl, active.jsonl run 인덱스
71
- ├── memory-book/ 전역 대화 메모리 (`okstra memory`)
72
- ├── projects/ 프로젝트별 메타데이터 미러
73
- ├── worktrees/ task-key 당 하나의 격리 git worktree
74
- │ (모든 phase가 공유; run 종료 후 자동 삭제하지 않음)
75
- ├── archive/ 완료된 run
76
- └── .locks/ central/task mutex 파일
77
-
78
- ~/.claude/skills/ Claude Code 가 자동 인식 (`~/.claude` 가 있을 때)
79
- ~/.agents/skills/ Agent 호환 host 가 인식 (`install` 이 생성)
80
- └── okstra-*/SKILL.md 사용자 노출 스킬 8종만 (setup/brief/run/memory/inspect/schedule/container/manager)
81
-
82
- ~/.claude/agents/ Claude Code 가 자동 인식 (`~/.claude` 가 있을 때)
83
- └── {claude,codex,antigravity,report-writer}-worker.md worker subagent 정의
84
- (Claude Code multi-agent dispatch 필수)
85
-
86
- <프로젝트 루트>/.okstra/
87
- ├── project.json {projectId, projectRoot, ...} (`/okstra-setup` 이 작성)
88
- ├── discovery/{task-catalog,latest-task}.json
89
- ├── glossary.md okstra-owned project terminology
90
- ├── decisions/<NNNN>-<slug>.md okstra-owned decision records
91
- └── tasks/<task-group>/<task-id>/ task bundle (runs, manifest, reports)
92
- ```
93
-
94
- ### 2.3 단일 권위 요약
95
-
96
- | 리소스 | 위치 | 소유자 |
97
- |---|---|---|
98
- | 런타임 코드 (python + bash) | `~/.okstra/{lib/python, bin}` | `okstra install` |
99
- | agents/prompts/schemas/templates/validators | npm 패키지의 `runtime/` | `okstra` 패키지 자체 (`okstra paths` 로 해석) |
100
- | 스킬 마크다운 | `~/.claude/skills` 또는 `~/.agents/skills` | `okstra install` (`installed-skills.json` 에 target별 트래킹) |
101
- | 워커 에이전트 마크다운 | `~/.claude/agents/<worker>.md` | `~/.claude` 가 있을 때 `okstra install` (`installed-agents.json` 에 트래킹) |
102
- | 프로젝트 메타데이터 | `<project>/.okstra/` | `/okstra-setup` + 프로젝트 자체 |
103
- | Run 인덱스 | `~/.okstra/{recent,active}.jsonl` | `prepare_task_bundle` |
104
-
105
- ## 3. 사용 매뉴얼
106
-
107
- ### 3.1 최초 셋업 (머신당 1회)
108
-
109
- ```bash
110
- npx -y okstra@latest install
111
- ```
112
-
113
- `~/.okstra/{lib/python, bin, templates, prompts, version}` 과 `~/.okstra/` 의 설치 asset manifest 를 생성합니다. 스킬 설치는 host home 기준입니다. `~/.claude` 가 있으면 public skill 을 `~/.claude/skills/` 에, worker agent 를 `~/.claude/agents/` 에 설치하고, `~/.agents/skills/` 는 항상 생성해 Agent 호환 host 용 public skill 을 설치합니다. 사용자 진입점 스킬만 목록에 보이고, lead/support 운영 계약은 `~/.okstra/prompts/` 아래 runtime resource 로 설치되어 skill discovery 대상이 아닙니다. 재실행은 idempotent — 파일별 hash 를 비교하고 바뀐 파일만 갱신합니다.
114
-
115
- 검증:
116
-
117
- ```bash
118
- npx -y okstra@latest doctor
119
- ```
120
-
121
- `result: OK` 라인이 보이면 준비 완료입니다. FAIL row 가 있으면 그 줄에 바로 복구 방법이 인쇄됩니다 (대개는 install 재실행).
122
-
123
- #### (선택) `okstra` 명령을 글로벌로 설치
124
-
125
- 이 README 의 모든 예제는 `npx -y okstra@latest <cmd>` 를 씁니다 — 글로벌 설치 없이도 동작합니다. 매번 npx 의 패키지 fetch / 버전 체크를 거치지 않고 `okstra` 한 단어로 호출하고 싶다면 글로벌 설치:
126
-
127
- ```bash
128
- npm i -g okstra
129
- okstra --version # CLI 가 PATH 에 잡혔는지 확인
130
- okstra install # 'npx -y okstra@latest install' 과 동일
131
- ```
132
-
133
- 글로벌 설치는 Node CLI 를 PATH 에 등록할 뿐입니다. 런타임(`~/.okstra/`) 과 host skill directory(`~/.claude/skills/` 또는 `~/.agents/skills/`) 는 여전히 `okstra install` 이 생성합니다 — `npm i -g` 에 포함되지 않습니다. 이후 업그레이드: `npm i -g okstra@latest && okstra install`. 글로벌 바이너리 제거: `npm uninstall -g okstra` (`~/.okstra/` 는 그대로; 그것까지 지우려면 `okstra uninstall`).
134
-
135
- **글로벌 설치 시 스킬 동작.** 모든 okstra 스킬은 PATH 에 잡힌 `okstra` 를 자동 감지하여 `npx -y okstra@latest` 대신 우선 사용합니다. 즉 글로벌 설치를 해두면 매 스킬 호출(`okstra-run`, `okstra-inspect`, `okstra-schedule`, `okstra-setup` Step 2 의 Step 0) 마다 npx 가 패키지 fetch / 버전 체크하던 비용이 사라집니다. 스킬이 본인이 설치한 버전을 그대로 쓰므로 **업그레이드 타이밍은 사용자가 통제** 합니다 — 더 이상 호출마다 `@latest` 가 강제되지 않습니다. 새 릴리스를 받으려면 원하는 시점에 `npm i -g okstra@latest && okstra install` 을 실행하세요. `okstra` 가 PATH 에 없으면 스킬은 자동으로 npx fallback 으로 동작하므로 글로벌 설치가 없는 환경에서도 변경 없이 그대로 동작합니다.
136
-
137
- ### 3.2 프로젝트 등록 (프로젝트당 1회)
138
-
139
- CLI 에서:
140
-
141
- ```bash
142
- cd <대상 프로젝트>
143
- npx -y okstra@latest setup --project-id <id> # 예: INV-1234, my-app, okstra
144
- ```
145
-
146
- 또는 Claude Code 세션 안에서 동일한 슬래시 커맨드:
147
-
148
- ```
149
- /okstra-setup
150
- ```
151
-
152
- 둘 다 `<project>/.okstra/project.json` 을 작성합니다. CLI 는 `--project-id` 가 주어지면 non-interactive 로 동작하므로 CI 에서 사용할 수 있고, 슬래시 커맨드는 `AskUserQuestion` 으로 prompt 합니다. 이 파일이 존재하기 전에는 다른 모든 사용자 스킬이 실행을 거부합니다.
153
-
154
- ### 3.3 일상 명령
155
-
156
- Claude Code 세션 안에서 사용하는 슬래시 커맨드:
157
-
158
- | 커맨드 | 용도 |
159
- |---|---|
160
- | `/okstra-brief-gen` | ticket, 요구사항 문서, 링크, 대화 내용을 `okstra-run`용 task brief로 변환 |
161
- | `/okstra-run` | 새 task 시작 (또는 기존 task 의 다음 phase 이어가기) |
162
- | `/okstra-memory` | `~/.okstra/memory-book` 전역 대화 메모리 저장·검색·보관 |
163
- | `/okstra-inspect` | 통합 read-side 스킬. sub-command: `status` (phase / 상태, workStatus 설정), `history` (과거 task / re-run / resume), `report` (final-report 조회·읽기), `time` (소요 시간 breakdown), `logs` (wrapper log sidecar 조회·정리 제안), `cost` (task bundle 컨텍스트/읽기 비용), `errors` (run 에러 로그를 리포트로 집계), `error-zip` (cross-project 에러 로그를 익명화 zip 으로 수집·클러스터 요약), `recap` (run 간 전/후 요약 + task 의 `.okstra` 산출물 위 자유 Q&A) |
164
- | `/okstra-rollup` | task-group(또는 프로젝트 전체)의 모든 task run 결과를 모아 task별 run수/소요시간/에러와 그룹 합계를 집계하고, report 들을 묶어 task 횡단 종합 요약 작성 |
165
- | `/okstra-schedule` | task-group 전체에 대한 작업 계획표 생성 |
166
- | `/okstra-container-build` | 검증 완료된 task 의 코드를 로컬 docker compose 그룹으로 배포하고 컨테이너별 로그를 감시 (sub-command: `up` / `status` / `logs` / `stop-watcher` / `down`) |
167
- | `/okstra-graphify` | 프로젝트 자체 `.okstra/` 메모리(final report, `decisions/*.md`, `glossary.md`)를 지식그래프로 빌드·질의 (범위는 `.okstra/` 로 한정, 산출물은 `.okstra/graph/` 아래; sub-command: `build` / `query` / `path` / `explain` / `mcp` / `wiki`) |
168
- | `/okstra-manager` | 여러 프로젝트에 걸친 okstra task 를 manager-owned plan, assignment, 단방향 project sync snapshot, status, child launch context packet 으로 조정 |
169
- | `/okstra-setup` | 프로젝트별 부트스트랩 (§3.2) |
170
-
171
- lead 운영 계약과 지원 계약 — context-loader, team-contract, convergence, report-writer, 그리고 coding-preflight 팩 — 은 더 이상 agent 스킬로 설치되지 않습니다. okstra 런타임 리소스로 `~/.okstra/prompts/` (`prompts/lead/*.md`, `prompts/coding-preflight/*`) 아래에 배치되고, generated launch prompt 가 lead 에게 절대 경로를 전달합니다. 재설치 시 agent skill home 에 남은 과거 복사본은 prune 되므로 슬래시 커맨드로 노출되지 않습니다.
172
-
173
- ### 3.4 CLI 모드 (선택)
174
-
175
- Claude Code 세션 밖에서 task 를 시작하려면:
176
-
177
- ```bash
178
- ~/.okstra/bin/okstra.sh \
179
- --project-id <id> \
180
- --task-group <group> \
181
- --task-id <id> \
182
- --task-type <requirements-discovery|improvement-discovery|error-analysis|implementation-planning|implementation|final-verification|release-handoff> \
183
- --base-ref <branch|tag|sha> \
184
- --task-brief ./brief.md
185
- ```
186
-
187
- 새 `claude` 프로세스를 띄워 lead 역할로 동작시킵니다. 전체 인자 목록은 `okstra.sh --help` 또는 [`docs/kr/cli.md`](docs/kr/cli.md) 참조.
188
-
189
- 0.7.0 / 0.8.0 에서 추가된 주요 플래그:
190
-
191
- - `--executor claude|codex|antigravity` — `--task-type implementation` 에서 파일을 mutate 할 provider 를 선택합니다. 나머지 두 provider 는 같은 run 에서 strict read-only verifier 로 dispatch 됩니다 ([`docs/kr/cli.md`](docs/kr/cli.md#--executor)).
192
- - `--work-category bugfix|feature|refactor|ops|improvement` — `requirements-discovery` phase 를 건너뛰는 경우 작업 분류를 직접 지정합니다.
193
- - `--approve` — `--approved-plan` 과 함께 사용하면 plan 의 YAML frontmatter `approved` field 를 `false` → `true` 로 toggle 합니다 (제거된 `--ack-approved` alias 와 구 `[ ] Approved` 체크박스 마커 대체).
194
-
195
- 0.8.0 이후 `main` 에 추가된 workflow 변경:
196
-
197
- - **모든 task-type 격리 worktree 자동 provisioning** — prepare 단계에서 `okstra-ctl` 이 task-key 당 한 번 `git worktree add ~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>` 를 수행해 격리된 working tree 와 브랜치 `<work-category-namespace>/<task-id-segment>` (예: `feature/dev-9436`, `fix/dev-7311`) 를 만듭니다. base ref 는 사용자가 `--base-ref` 로 지정 (release-handoff PR base picker 와 동일한 메뉴: `main` / `dev` / `staging` / `preprod` / `prod` / 직접 입력). 첫 phase 에서는 필수이며, okstra-run skill 이 `AskUserQuestion` 으로 수집합니다 — 비대화형 호출자는 `--base-ref` 플래그를 직접 전달해야 prepare 가 통과합니다. 같은 task-key 의 후속 **비-`implementation`** phase(`requirements-discovery` → `error-analysis` → `implementation-planning` → `final-verification` → `release-handoff`)는 같은 path/branch를 재사용합니다. 반면 `implementation` run 은 **stage 격리** 로 동작합니다 — 각 run 이 자신의 `.../<task>/stage-<N>/` worktree(브랜치 `<work-category-namespace>/<task>-s<N>`)에서 한 stage 만 실행하므로, 독립(`depends-on (none)`) stage 를 별도 run 으로 **동시에 병렬 구현** 할 수 있습니다(트리 공유 없음). registry 는 task-key 와 **stage-key** 를 함께 flock 예약합니다. caller 가 이미 다른 worktree 안에 있거나 project_root 가 git repo 가 아니면 provisioning 은 skip 됩니다(stage 격리도 평면 경로로 degrade). 수동 cleanup: `git worktree remove <path>` → `git branch -D <branch>` + registry 항목 release/remove. 상세: [`docs/kr/architecture.md`](docs/kr/architecture.md) *Task type* 섹션, [`docs/kr/cli.md#--executor`](docs/kr/cli.md#--executor).
198
- - **`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).
199
- - **PR 본문 템플릿 설정** (release-handoff) — PR 본문은 마크다운 템플릿에서 채워집니다. 해석 우선순위: 1회성 override (`--pr-template-path` 또는 okstra-run Step 6 prompt) → `<project_root>/.okstra/project.json` 의 `prTemplatePath` → `~/.okstra/config.json` 의 `prTemplatePath` → 스킬 디폴트 `~/.claude/skills/templates/prd/pr-body.template.md`. 템플릿 등록 명령: `okstra config set pr-template-path <path> [--scope project|global]` (project 스코프는 project root 기준 상대경로 허용, global 스코프는 절대경로 또는 `~/` 시작 경로만 허용). 현재 설정 확인: `okstra config get pr-template-path --scope all` 은 각 스코프 값 + 실제로 우승하는 경로(effective) 까지 보여줍니다. 디폴트 템플릿은 `## Summary` / `## Changes` / `## Test plan` / `## Linked issues` 4 섹션 + HTML 주석으로 lead 작성 가이드를 포함하며, PR 생성 직전에 lead 가 주석을 제거합니다.
200
- - **프로파일 워커 로스터 검증** — `--workers <csv>` 와 okstra-run Step 6 의 워커 prompt 는 해당 프로파일의 `Required workers:` 블록에 선언된 워커 ID 만 허용합니다. 프로파일에 없는 워커 (예: `release-handoff` 에서 `codex` / `antigravity`) 를 요청하면 명확한 에러로 거절되고, 인터랙티브 prompt 도 프로파일이 실제로 받는 워커만 보여줍니다.
201
- - **실험적 Codex lead adapter** — `okstra codex-run <render-bundle args...>` 가 Claude Code 를 띄우지 않고 `leadRuntime=codex` task bundle 을 준비합니다. 이어서 `okstra codex-dispatch --project-root <dir> --run-manifest <path>` 로 roster 중 Codex-side 지원 subset 을 실행합니다(Codex/Antigravity CLI worker 기본, Codex report-writer 는 `--enable-codex-report-writer --report-writer-codex-model <model>` opt-in 필요). 성공한 Codex report-writer dispatch 는 token/cost substitution, HTML view 렌더, follow-up stub 생성, run validation 을 순서대로 자동 실행합니다. Claude lead 경로와 같은 manifest/schema 를 공유하며, 프로젝트를 Codex 전용 fork 로 복제하지 않습니다.
202
- - **다단계 `implementation-planning` / `implementation`** — `implementation-planning` 은 항상 Stage Map + N 개 stage 섹션으로 산출합니다. 각 stage 의 step 은 ≤ 6 이며 `depends-on (none)` 인 stage 들은 별도 `implementation` run 으로 병렬 실행할 수 있습니다. 각 `implementation` 호출은 한 stage 만 실행하고 (`--stage <auto|N>`), 자동 생성되는 evidence sidecar (`carry/stage-<N>.json`) 가 다음 stage 의 carry-in 으로 흡수됩니다. `implementation-planning` run 디렉터리에 `consumers.jsonl` 역링크가 누적되어 어느 run 이 어느 stage 를 소비했는지 추적됩니다.
203
- - **Phase 6 plan-body verification (implementation-planning 전용)** — Report writer worker 가 final-report draft 를 작성한 직후, 사용자 승인 gate 직전에 lead 가 1 라운드의 사후 검증을 추가로 돌립니다. 합성된 `## 5.5` implementation plan deliverables 본문에서 `P-Opt-*` / `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*` plan-item 을 추출해 모든 analyser 워커에게 `AGREE` / `DISAGREE(a-e)` / `SUPPLEMENT` 평결을 요청합니다. 집계된 gate 결과는 `passed` / `passed-with-dissent` / `blocked-by-disagreement` / `aborted-non-result` 중 하나입니다. frontmatter `approved` 필드는 항상 `false` 로 발행되며, blocking gate 결과는 이를 false 로 유지하고 `## 1. Clarification Items` row 로 변환합니다. 빠른 반복용 opt-out 은 `--no-plan-verification` 입니다. 세부 계약: [`prompts/lead/convergence.md`](prompts/lead/convergence.md) "Plan-body verification mode", [`docs/kr/cli.md#--no-plan-verification`](docs/kr/cli.md#--no-plan-verification).
204
- - **Brief = translation layer + Step 6.5 reporter batch confirmation** — `okstra-brief-gen` 가 외부 입력 (이슈 ticket, 요구사항 문서, 사용자 메시지) 을 verbatim 으로 옮기되 okstra 가 추가한 부분은 labelled augmentation 으로 구분하는 translation layer 가 됐습니다. Step 6.5 가 brief 가 옮기는 과정에서 의미 변화가 발생했는지 사용자에게 일괄 확인받아 `Reporter Confirmations` 섹션에 기록하고, 모든 분석 profile 은 이 섹션의 존재를 phase 분석 진입 precondition 으로 강제합니다 (validator: `validators/validate-brief.py`).
205
- - **Artifact-home rule (`.okstra/`)** — okstra 의 project artifact root 는 `<project>/.okstra/` 하나뿐입니다. 이 root 밖은 okstra memory 가 아니며, Source Material 또는 Reporter Confirmations 가 명시적으로 cite 한 경우에만 read-only 로 읽습니다. 쓰기는 같은 explicit request path 를 요구합니다. okstra-internal 등가물: 용어집 `glossary.md`, 결정 기록 `decisions/<NNNN>-<slug>.md` (`implementation-planning` phase 에서 평가).
206
- - **Self-contained HTML final-report view** — Phase 7 가 `final-report-<task-type>-<seq>.md` 를 쓰면 `okstra render-views` 가 같은 `reports/` 폴더에 사람 reviewer 용 self-contained HTML view (CSS/JS 인라인, 외부 URL 0) 를 자동 생성합니다. HTML 의 `Export user response` 버튼은 `## 1. Clarification Items` 입력을 `runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md` 사이드카로 직렬화해 다음 phase 가 소비합니다. 원본 MD 는 어떤 경우에도 view 생성으로 인해 수정되지 않습니다.
207
- - **`improvement-discovery` task-type (sidetrack entry-point)** — 코드베이스 범위 + 우선순위 lens 화이트리스트 안에서 multi-worker 합의 기반 개선 후보 N개 (기본 8, 절대 cap 12) 도출. `PHASE_SEQUENCE` 외부 sidetrack entry-point — 사용자가 후보를 골라 각각 새 task-id 로 `requirements-discovery` / `implementation-planning` / `error-analysis` 진입. lens enum SSOT: [`scripts/okstra_ctl/improvement_lenses.py`](scripts/okstra_ctl/improvement_lenses.py). 출력 섹션: `## 4.9 Improvement Candidates` (10-column 표). validator: [`validators/validate_improvement_report.py`](validators/validate_improvement_report.py).
208
-
209
- ### 3.5 운영 명령
210
-
211
- | 커맨드 | 용도 |
212
- |---|---|
213
- | `npx -y okstra@latest paths` | 런타임 경로 출력 (`--field <name>` 또는 `--shell`) |
214
- | `npx -y okstra@latest doctor [--runtime claude-code\|codex\|all] [--phase <phase>]` | 런타임 + 스킬 + python import 진단. `codex` 는 Claude skill checks 를 제외하고, `--phase` 는 implementation / final-verification / release-handoff / improvement-discovery readiness 체크를 추가 |
215
- | `npx -y okstra@latest ensure-installed` | Idempotent 체크, stale 이면 자동 재설치 (스킬이 내부적으로 호출) |
216
- | `npx -y okstra@latest setup --project-id <id>` | 현재 프로젝트를 등록 (`.okstra/project.json`) |
217
- | `npx -y okstra@latest check-project` | 현재 프로젝트가 `setup` 으로 등록됐는지 검증 |
218
- | `npx -y okstra@latest config <get\|set\|unset\|show> [key] [value] [--scope project\|global\|all]` | okstra 설정 읽기/쓰기. 현재 지원 키: `pr-template-path` (project.json 또는 `~/.okstra/config.json` 의 `prTemplatePath` 갱신) |
219
- | `npx -y okstra@latest memory <add\|list\|search\|show\|archive>` | `~/.okstra/memory-book` 전역 대화 메모리 저장·검색·보관 |
220
- | `npx -y okstra@latest context-cost <task-key\|task-root> [--project-root <path>]` | task bundle 의 lead/worker/report-writer 컨텍스트·읽기 비용 추정 |
221
- | `npx -y okstra@latest render-views <final-report.md>` | final-report MD 한 본을 입력으로 self-contained HTML view 를 (재)생성 (Phase 7 step 1.5; 멱등) |
222
- | `npx -y okstra@latest token-usage ...` | run token usage 수집/치환. 설치된 Python token usage CLI를 감싼 Node wrapper |
223
- | `npx -y okstra@latest uninstall` | 런타임 + 스킬 제거; 사용자 데이터(`recent.jsonl`, `projects/`, …)는 보존 |
224
- | `npx -y okstra@latest uninstall --purge -y` | 사용자 데이터까지 모두 제거 |
225
-
226
- ## 4. 더 읽을 자료
227
-
228
- - [`docs/kr/architecture.md`](docs/kr/architecture.md) — 한국어 상세 매뉴얼: prompt contract, team contract, storage model, task type 별 phase 규칙, lifecycle, workflow.
229
- - [`docs/kr/cli.md`](docs/kr/cli.md) — `okstra.sh` 의 모든 인자 / 옵션과 인터랙티브 입력 흐름.
230
- - [`RELEASING.md`](RELEASING.md) — 버전 컷과 publish 절차 (release-please + 수동 fallback).
231
- - [`CHANGELOG.md`](CHANGELOG.md) — release 별 변경 이력.