okstra 0.74.0 → 0.75.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,108 @@
1
+ # Stage discipline 규칙 — 선행 stage 동작 동결 설계
2
+
3
+ - 날짜: 2026-06-12
4
+ - 상태: 설계 승인됨 (구현 전)
5
+ - 대상: `implementation` · `implementation-planning` 프로파일
6
+
7
+ ## 1. 배경과 목표
8
+
9
+ implementation 은 stage-isolated 다 — 한 run = 한 stage 이고, dependent stage 는 선행(`depends-on`) stage 의 done commit 을 base 로 시작한다(`prompts/profiles/implementation.md:27`). 선행 stage 는 이미 conformance 를 통과해 `done` 으로 닫힌 상태인데, 이후 stage 가 그 코드의 **동작을 바꾸면** "verified-stays-verified" 불변식이 깨진다 — 닫힌 stage 가 조용히 회귀한다.
10
+
11
+ 목표: "선행 stage 가 추가·수정한 코드의 기능(동작)은 이후 stage 에서 변경하지 않는다" 라는 규율을 `implementation` 과 `implementation-planning` 양쪽에 도입하고, 이런 stage 규율을 **별도 단일 파일**로 관리한다.
12
+
13
+ ## 2. 결정 사항 (brainstorming 합의)
14
+
15
+ | 항목 | 결정 |
16
+ |---|---|
17
+ | 규칙 파일 소유권 | okstra 내장 공유 파일 (모든 프로젝트 적용, okstra 레포에서 편집·전파) |
18
+ | 강제 수준 | 선언 + 보고서 증거 (validator/wrapper sentinel 없음) |
19
+ | 규칙 언어 | English (기존 Forbidden actions·stage-scope 규칙과 일관) |
20
+ | executor 전사 포인터 | 둔다 (CLI executor 가 규칙을 받도록) |
21
+
22
+ YAGNI 로 제외: validator/wrapper 강제, data.json 스키마 필드, 프로젝트별 override.
23
+
24
+ ## 3. 아키텍처 근거
25
+
26
+ - 프로파일은 `{{INCLUDE:<name>}}` 로 공유 계약을 합친다 — resolver 는 임의 파일을 지원한다(`scripts/okstra_ctl/run.py:973` `_expand_profile_includes`, 재귀 depth ≤4, 누락 시 `PrepareError` fail-fast). 추가 코드 변경 없이 새 사이드카를 INCLUDE 할 수 있다.
27
+ - `implementation-planning` 의 행위자는 lead/planner 본인이라 INCLUDE 로 직접 바인딩된다.
28
+ - `implementation` 의 실제 행위자인 **executor(codex/gemini CLI)는 lead 컨텍스트를 공유하지 않는다**. 기존 계약은 lead 가 규칙을 executor 프롬프트에 **verbatim 전사**하게 한다(`prompts/profiles/_implementation-executor.md:27` "CLI executor transcription"). 따라서 implementation 규칙이 CLI executor 에 닿으려면 전사 포인터가 필요하다.
29
+ - executor 사이드카(`_implementation-executor.md`)는 lead 가 Phase 5 에 raw 로 lazy-load 하며 `{{INCLUDE}}` resolver 를 거치지 않는다(`_implementation-executor.md:3`). 그래서 사이드카에는 규칙 **텍스트를 복사하지 않고** 단일 출처를 가리키는 **전사 지시 한 줄**만 둔다(DRY).
30
+
31
+ ## 4. 설계
32
+
33
+ ### 4.1 신규 단일 파일 `prompts/profiles/_stage-discipline.md` (SSOT)
34
+
35
+ 규칙의 유일한 출처. 핵심 원칙 + phase 별 적용 + 보고서 증거 절을 담는다. 전체 본문:
36
+
37
+ ```markdown
38
+ <!--
39
+ Shared stage-discipline rule. INCLUDEd by implementation-planning.md (binds the
40
+ planner) and implementation.md (binds the lead). The implementation executor
41
+ sidecar instructs the lead to transcribe this rule into a CLI executor prompt,
42
+ since codex/gemini executors do not share lead context.
43
+
44
+ Do NOT write the literal include directive token in this file's body — the
45
+ resolver matches it anywhere and would recurse on this file itself.
46
+ -->
47
+
48
+ ## Stage discipline — preceding stages are behavior-frozen
49
+
50
+ A stage that is already `done` (a `depends-on` predecessor whose work is merged
51
+ into this run's base) is **behavior-frozen**: code it added or modified keeps its
52
+ existing behavior. Later stages MAY call, extend, or compose with that code, but
53
+ MUST NOT change what it already does. This preserves the verified-stays-verified
54
+ invariant — a completed, conformance-passed stage must not silently regress when
55
+ a later stage runs.
56
+
57
+ - **implementation-planning** — decompose stages forward-only. No stage's scope
58
+ may require altering an earlier stage's behavior; each stage builds additively
59
+ on its predecessors' Stage Exit Contracts. If delivering an increment would
60
+ force changing an earlier stage's behavior, that earlier stage was mis-scoped —
61
+ fold the change into that stage or re-partition; do not plan a later stage that
62
+ rewrites it.
63
+ - **implementation** — the executor MUST NOT alter the behavior of any file added
64
+ or modified by a preceding done stage. Touching such a file to *extend* it (a
65
+ new branch, a new caller, an additive field) is allowed; changing its existing
66
+ behavior is not. If a plan step appears to require a behavior change to
67
+ prior-stage code, treat it as a planning defect: record the finding and route
68
+ to a new `implementation-planning` run — never silently rewrite it.
69
+ - **Report evidence (declaration-level).** When this stage modifies a file that
70
+ a preceding stage added or modified, the final report MUST record — in the
71
+ deliverable's existing edits / validation evidence section — which file, why
72
+ it was touched, and how the prior stage's behavior was preserved. Absent that
73
+ note, a reviewer treats a prior-stage edit as an unverified regression risk.
74
+ ```
75
+
76
+ ### 4.2 INCLUDE 배선
77
+
78
+ - `prompts/profiles/implementation.md`: 기존 `{{INCLUDE:_common-contract.md}}`(line 17) 바로 다음 줄에 `{{INCLUDE:_stage-discipline.md}}` 추가.
79
+ - `prompts/profiles/implementation-planning.md`: 기존 `{{INCLUDE:_common-contract.md}}`(line 10) 바로 다음 줄에 `{{INCLUDE:_stage-discipline.md}}` 추가.
80
+
81
+ ### 4.3 executor 전사 포인터
82
+
83
+ `prompts/profiles/_implementation-executor.md` 의 기존 전사 규칙(`:27` "CLI executor transcription") 근처에 형제 bullet 한 줄 추가 (규칙 텍스트 복사 없음):
84
+
85
+ ```markdown
86
+ - **Stage discipline transcription (when a preceding stage is `done`):** the lead
87
+ MUST transcribe the `Stage discipline` rule (from this run's rendered profile)
88
+ verbatim into the dispatched CLI executor prompt, so a codex/gemini executor
89
+ honors the prior-stage behavior-freeze. Declaration-level — no wrapper sentinel.
90
+ ```
91
+
92
+ ### 4.4 전파
93
+
94
+ `tools/build.mjs` 가 `prompts/` → `runtime/` 를 동기화하고 install 이 사용자 머신으로 복사한다. INCLUDE resolver 가 임의 파일을 이미 지원하므로 추가 코드 변경은 없다.
95
+
96
+ ## 5. 테스트
97
+
98
+ `tests/test_okstra_profile_includes.py` 확장:
99
+ - `implementation.md` 렌더 결과에 `Stage discipline — preceding stages are behavior-frozen` 헤딩과 `behavior-frozen` 본문이 INCLUDE 로 들어오는지 단언.
100
+ - `implementation-planning.md` 렌더 결과에 같은 규칙 텍스트가 들어오는지 단언.
101
+ - `_stage-discipline.md` 파일 존재 + `implementation` / `implementation-planning` 두 프로파일에 INCLUDE 디렉티브가 있는지 단언 (executor 사이드카에는 전사 포인터 문자열이 있는지 단언).
102
+
103
+ (누락 INCLUDE 는 `_expand_profile_includes` 가 이미 `PrepareError` 로 fail-fast.)
104
+
105
+ ## 6. 영향받지 않는 것
106
+
107
+ - 다른 phase(`requirements-discovery` / `error-analysis` / `final-verification` / `release-handoff` / `improvement-discovery`)는 무변경 — 규칙은 두 프로파일에만 INCLUDE 된다.
108
+ - data.json 스키마, validator, CLI 플래그, wizard 무변경.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.74.0",
3
+ "version": "0.75.0",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.74.0",
3
- "builtAt": "2026-06-11T18:11:19.238Z",
2
+ "package": "0.75.0",
3
+ "builtAt": "2026-06-11T19:32:02.652Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -25,6 +25,7 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
25
25
  - **Language-agnostic principles that ALWAYS bind (the TDD loop below MUST satisfy them):** (1) no self-mocking of the SUT — stub/spy only injected collaborators, never the subject's own methods; (2) behavioral assertions on outcomes (return value, state, persisted rows, events, boundary calls) — never `toHaveBeenCalled*` on an internal helper as the only/primary assertion; (3) truthful names — a `get*` / `find*` that writes/inserts, or a name encoding the caller's use-case (`*ForInit`) or hiding a domain rule (`findValid*`), is a defect; (4) single-purpose functions ≤50 effective lines, plain-English readability.
26
26
  - **Graceful degradation (codex / gemini executor runtimes, or any runtime where the `~/.claude/skills/okstra-coding-preflight/` files are absent or unreadable):** do NOT skip the gate — apply the agnostic principles above plus the project's own `CLAUDE.md` / `CONTRIBUTING` / formatter+lint config, and record `coding-conventions: skill-unavailable → applied <project rules + agnostic principles>` in the final report. Never claim a skill read that did not happen.
27
27
  - **CLI executor transcription (BLOCKING when the executor provider is `codex` or `gemini`):** the executor CLI process does NOT share the lead's context — a gate that stays in lead memory never reaches it. The lead MUST copy this entire "Coding-conventions preflight" bullet tree (file-read instructions, project review rule packs, agnostic principles, graceful degradation) verbatim into the dispatched executor prompt body. Enforcement: the CLI wrapper agents refuse an implementation-Executor dispatch whose persisted prompt lacks the literal heading `Coding-conventions preflight`, returning `<SENTINEL_PREFIX>_PREFLIGHT_MISSING` (see `agents/workers/_cli-wrapper-template.md` → Prompt Composition).
28
+ - **Stage discipline transcription (when a preceding stage is `done`):** the lead MUST transcribe the `Stage discipline` rule (from this run's rendered profile — the INCLUDEd `_stage-discipline.md` body) verbatim into the dispatched CLI executor prompt, so a codex/gemini executor honors the prior-stage behavior-freeze. Declaration-level — no wrapper sentinel.
28
29
  - **Non-interactive auto-execution (BLOCKING when the executor provider is `codex` or `gemini`).** A CLI executor runs head-less (`codex exec` / gemini equivalent) — there is no human at the keyboard. Skills loaded during the run (tdd, coding-preflight, and others) contain "get user approval", "state your plan to the user and wait", or "ask before proceeding" gates written for interactive sessions; in this run those gates are **already satisfied** by the upstream `implementation-planning` approval (the plan this stage executes was human-approved). The executor MUST NOT stop to request approval, MUST NOT end its turn after only producing a plan, and MUST carry the stage through end-to-end — RED → GREEN → refactor → per-step commits → `### Stage Carry Evidence`. The ONLY skill step to skip is the interactive user-approval prompt itself; every other skill rule (TDD discipline, conventions, real-IO isolation) still binds. The lead MUST transcribe this bullet verbatim into the dispatched CLI executor prompt (same reason as the preflight transcription rule above — the CLI process does not share lead context). Stopping early for approval in a head-less run is the observed empty-exit failure (exit 0, no diff): treat it as `contract-violated`.
29
30
  - **Mandatory TDD loop**: BEFORE the first `Edit` or `Write` call, the executor MUST apply a red-green-refactor loop for every code change in this run. This is required; skipping it is a `contract-violated` outcome. This governs HOW each step is executed (failing test first → minimal implementation → refactor); it does not override the approved plan's WHAT/file scope.
30
31
  - Order of operations per plan step: (1) write/extend the test that captures the step's acceptance criterion and confirm it fails for the right reason, (2) commit the failing test (`test(<scope>): ...`), (3) implement the minimum change to make it pass, (4) commit the implementation (`feat|fix(<scope>): ...`), (5) refactor without changing behaviour and commit separately if any cleanup is made (`refactor(<scope>): ...`). The failing-then-passing transition between steps (2) and (4) is the `TDD evidence` required by the final report.
@@ -0,0 +1,36 @@
1
+ <!--
2
+ Shared stage-discipline rule. INCLUDEd by implementation-planning.md (binds the
3
+ planner) and implementation.md (binds the lead). The implementation executor
4
+ sidecar instructs the lead to transcribe this rule into a CLI executor prompt,
5
+ since codex/gemini executors do not share lead context.
6
+
7
+ Do NOT write the literal include directive token in this file's body — the
8
+ resolver matches it anywhere and would recurse on this file itself.
9
+ -->
10
+
11
+ ## Stage discipline — preceding stages are behavior-frozen
12
+
13
+ A stage that is already `done` (a `depends-on` predecessor whose work is merged
14
+ into this run's base) is **behavior-frozen**: code it added or modified keeps its
15
+ existing behavior. Later stages MAY call, extend, or compose with that code, but
16
+ MUST NOT change what it already does. This preserves the verified-stays-verified
17
+ invariant — a completed, conformance-passed stage must not silently regress when
18
+ a later stage runs.
19
+
20
+ - **implementation-planning** — decompose stages forward-only. No stage's scope
21
+ may require altering an earlier stage's behavior; each stage builds additively
22
+ on its predecessors' Stage Exit Contracts. If delivering an increment would
23
+ force changing an earlier stage's behavior, that earlier stage was mis-scoped —
24
+ fold the change into that stage or re-partition; do not plan a later stage that
25
+ rewrites it.
26
+ - **implementation** — the executor MUST NOT alter the behavior of any file added
27
+ or modified by a preceding done stage. Touching such a file to *extend* it (a
28
+ new branch, a new caller, an additive field) is allowed; changing its existing
29
+ behavior is not. If a plan step appears to require a behavior change to
30
+ prior-stage code, treat it as a planning defect: record the finding and route
31
+ to a new `implementation-planning` run — never silently rewrite it.
32
+ - **Report evidence (declaration-level).** When this stage modifies a file that
33
+ a preceding stage added or modified, the final report MUST record — in the
34
+ deliverable's existing edits / validation evidence section — which file, why
35
+ it was touched, and how the prior stage's behavior was preserved. Absent that
36
+ note, a reviewer treats a prior-stage edit as an unverified regression risk.
@@ -8,6 +8,7 @@
8
8
  - Optional workers (opt-in via `--workers`):
9
9
  - gemini — when added to the roster it joins the analyser set; omitted by default
10
10
  {{INCLUDE:_common-contract.md}}
11
+ {{INCLUDE:_stage-discipline.md}}
11
12
  - Brief consumption (phase-specific addendum — shared rules live in `_common-contract.md` under "Brief handoff contract"):
12
13
  - Apply the shared reporter-confirmation precondition exactly as written. In this phase, unresolved `intent-check:` / `conversion-block:` rows use `Blocks=approval`; the operator cannot flip the approval frontmatter to `approved: true` until those rows are resolved.
13
14
  - never plan around an unconfirmed `intent-inference` augmentation as if it were a settled requirement. After the precondition runs, a `[CONFIRMED …]` marker on the matching `intent-check:` row is the signal that the inference can be treated as settled; otherwise it remains a `Blocks=approval` clarification item per the precondition's `skipped` branch.
@@ -15,6 +15,7 @@
15
15
  - Executor model: `{{EXECUTOR_MODEL_DISPLAY}}` (launch value: `{{EXECUTOR_MODEL_EXECUTION_VALUE}}`)
16
16
  - 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.
17
17
  {{INCLUDE:_common-contract.md}}
18
+ {{INCLUDE:_stage-discipline.md}}
18
19
  - Pre-implementation gate (mandatory — refuse to start if any item fails):
19
20
  - the run brief MUST cite `--approved-plan <path>` pointing to a `final-report.md` produced by a prior `implementation-planning` run located under `runs/implementation-planning/.../reports/final-report.md`
20
21
  - that file's YAML frontmatter MUST carry `approved: true`. report-writer emits `approved: false` by default; the user flips it to `true` to authorise this run. Free-form approvals such as "lgtm" / "go ahead" / paraphrased confirmations are NOT accepted; re-edit the plan file's frontmatter to `approved: true` before invoking implementation, or pass `--approve` so the CLI flips it on the user's behalf (`okstra_ctl.run._apply_cli_approval`).
package/src/install.mjs CHANGED
@@ -167,6 +167,34 @@ async function copyTreeIfChanged(srcRoot, dstRoot, opts) {
167
167
  return { copied, skipped, missingSource };
168
168
  }
169
169
 
170
+ // Single-file variant of copyTreeIfChanged, used for the lead `okstra` skill
171
+ // whose source is one file (agents/SKILL.md) rather than a skill directory.
172
+ async function copyFileIfChanged(src, dst, opts) {
173
+ const { refresh = false, dryRun = false, mode } = opts ?? {};
174
+ let srcBuf;
175
+ try {
176
+ srcBuf = await fs.readFile(src);
177
+ } catch {
178
+ return { copied: 0, skipped: 0, missingSource: true };
179
+ }
180
+ let needsCopy = refresh;
181
+ if (!needsCopy) {
182
+ try {
183
+ const [srcHash, dstHash] = await Promise.all([hashFile(src), hashFile(dst)]);
184
+ needsCopy = srcHash !== dstHash;
185
+ } catch {
186
+ needsCopy = true;
187
+ }
188
+ }
189
+ if (!needsCopy) return { copied: 0, skipped: 1, missingSource: false };
190
+ if (dryRun) {
191
+ process.stdout.write(`[dry-run] copy ${src} -> ${dst}\n`);
192
+ return { copied: 1, skipped: 0, missingSource: false };
193
+ }
194
+ await writeFileAtomic(dst, srcBuf, mode);
195
+ return { copied: 1, skipped: 0, missingSource: false };
196
+ }
197
+
170
198
  async function ensureSymlink(target, linkPath, opts) {
171
199
  const { dryRun = false } = opts ?? {};
172
200
  try {
@@ -244,7 +272,8 @@ async function installLinkMode(repoPath, paths, opts) {
244
272
  }
245
273
 
246
274
  const skillResult = await installSkillsLink(repoAbs, { dryRun, quiet });
247
- await writeSkillsManifest(paths.home, skillResult.installed, { dryRun });
275
+ const leadSkillResult = await installLeadSkillLink(repoAbs, { dryRun, quiet });
276
+ await writeSkillsManifest(paths.home, [...skillResult.installed, ...leadSkillResult], { dryRun });
248
277
 
249
278
  const agentResult = await installAgentsLink(repoAbs, { dryRun, quiet });
250
279
  await writeAgentsManifest(paths.home, agentResult.installed, { dryRun });
@@ -487,6 +516,43 @@ async function installSkillsLink(repoAbs, opts) {
487
516
  return { installed: names };
488
517
  }
489
518
 
519
+ // The lead `okstra` skill source is agents/SKILL.md (frontmatter `name: okstra`);
520
+ // it doubles as the lead agent contract, so it lives under agents/ rather than
521
+ // skills/. Claude Code only discovers skills at ~/.claude/skills/<name>/SKILL.md,
522
+ // so it must be installed there as its own skill dir — otherwise install ships
523
+ // every okstra-* helper skill but not the lead skill, and edits to agents/SKILL.md
524
+ // never reach the running lead.
525
+ const LEAD_SKILL_NAME = "okstra";
526
+
527
+ async function installLeadSkillCopy(runtimeRoot, opts) {
528
+ const { refresh, dryRun, quiet } = opts;
529
+ const src = join(runtimeRoot, "agents", "SKILL.md");
530
+ const dst = join(CLAUDE_SKILLS_DIR, LEAD_SKILL_NAME, "SKILL.md");
531
+ const r = await copyFileIfChanged(src, dst, { refresh, dryRun, mode: 0o644 });
532
+ if (r.missingSource) {
533
+ if (!quiet) process.stdout.write(" skills/okstra: runtime/agents/SKILL.md missing — skipped\n");
534
+ return [];
535
+ }
536
+ if (!quiet) {
537
+ process.stdout.write(` skills/okstra: ${r.copied ? "copied" : "unchanged"} -> ${dst}\n`);
538
+ }
539
+ return [LEAD_SKILL_NAME];
540
+ }
541
+
542
+ async function installLeadSkillLink(repoAbs, opts) {
543
+ const { dryRun, quiet } = opts;
544
+ const src = join(repoAbs, "agents", "SKILL.md");
545
+ if (!(await fileExists(src))) {
546
+ if (!quiet) process.stdout.write(" skills/okstra: <repo>/agents/SKILL.md missing — skipped\n");
547
+ return [];
548
+ }
549
+ const dst = join(CLAUDE_SKILLS_DIR, LEAD_SKILL_NAME, "SKILL.md");
550
+ if (!dryRun) await fs.mkdir(join(CLAUDE_SKILLS_DIR, LEAD_SKILL_NAME), { recursive: true });
551
+ const action = await ensureSymlink(src, dst, { dryRun });
552
+ if (!quiet) process.stdout.write(` skills/okstra/SKILL.md: ${action}\n`);
553
+ return [LEAD_SKILL_NAME];
554
+ }
555
+
490
556
  function parseInstallArgs(args) {
491
557
  const result = {
492
558
  dryRun: false,
@@ -623,7 +689,12 @@ export async function runInstall(args) {
623
689
  }
624
690
 
625
691
  const skillResult = await installSkillsCopy(runtimeRoot, opts);
626
- await writeSkillsManifest(paths.home, skillResult.installed, { dryRun: opts.dryRun });
692
+ const leadSkillResult = await installLeadSkillCopy(runtimeRoot, opts);
693
+ await writeSkillsManifest(
694
+ paths.home,
695
+ [...skillResult.installed, ...leadSkillResult],
696
+ { dryRun: opts.dryRun },
697
+ );
627
698
 
628
699
  const agentResult = await installAgentsCopy(runtimeRoot, opts);
629
700
  await writeAgentsManifest(paths.home, agentResult.installed, { dryRun: opts.dryRun });
package/src/uninstall.mjs CHANGED
@@ -18,6 +18,7 @@ const BIN_ENTRYPOINTS = [
18
18
  ];
19
19
 
20
20
  const FALLBACK_SKILL_NAMES = [
21
+ "okstra",
21
22
  "okstra-setup",
22
23
  "okstra-brief",
23
24
  "okstra-run",