okstra 0.109.0 → 0.111.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.kr.md +2 -1
- package/README.md +2 -1
- package/docs/for-ai/skills/okstra-run.md +7 -5
- package/docs/kr/architecture.md +11 -12
- package/docs/kr/cli.md +4 -4
- package/docs/project-structure-overview.md +12 -12
- package/docs/task-process/README.md +4 -4
- package/docs/task-process/common-flow.md +8 -5
- package/docs/task-process/implementation.md +4 -5
- package/docs/task-process/release-handoff.md +3 -3
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +1 -0
- package/runtime/agents/workers/codex-worker.md +1 -0
- package/runtime/prompts/lead/convergence.md +7 -7
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +43 -0
- package/runtime/prompts/profiles/_implementation-executor.md +7 -9
- package/runtime/prompts/profiles/_implementation-self-check.md +11 -5
- package/runtime/python/okstra_ctl/codex_dispatch.py +3 -0
- package/runtime/python/okstra_ctl/consumers.py +4 -0
- package/runtime/python/okstra_ctl/handoff.py +5 -23
- package/runtime/python/okstra_ctl/implementation_stage.py +11 -11
- package/runtime/python/okstra_ctl/report_views.py +76 -26
- package/runtime/python/okstra_ctl/stage_targets.py +152 -0
- package/runtime/python/okstra_ctl/wizard.py +47 -0
- package/runtime/python/okstra_ctl/worktree.py +14 -13
- package/runtime/python/okstra_project/__init__.py +2 -0
- package/runtime/python/okstra_project/state.py +44 -2
- package/runtime/skills/okstra-run/SKILL.md +21 -17
- package/src/commands/execute/wizard.mjs +3 -1
- package/src/commands/inspect/task-show.mjs +11 -31
|
@@ -105,17 +105,16 @@ DEFAULT_WORKTREE_SYNC_FILES: tuple[str, ...] = (
|
|
|
105
105
|
DEFAULT_WORKTREE_SNAPSHOT_FILES: tuple[str, ...] = ()
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
# Work-category →
|
|
109
|
-
# `--work-category` (bugfix / feature / refactor / ops /
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
# Work-category → branch namespace (slash-prefixed). Mirrors the values
|
|
109
|
+
# accepted by `--work-category` (bugfix / feature / refactor / ops /
|
|
110
|
+
# improvement); `feature` and `improvement` share the `feature/` namespace,
|
|
111
|
+
# and any unset/unrecognised category falls back to `task/`.
|
|
112
|
+
_WORK_CATEGORY_NAMESPACE = {
|
|
113
|
+
"feature": "feature",
|
|
114
|
+
"improvement": "feature",
|
|
113
115
|
"bugfix": "fix",
|
|
114
116
|
"refactor": "refactor",
|
|
115
117
|
"ops": "ops",
|
|
116
|
-
"improvement": "improve",
|
|
117
|
-
"docs": "doc",
|
|
118
|
-
"doc": "doc",
|
|
119
118
|
}
|
|
120
119
|
|
|
121
120
|
|
|
@@ -241,9 +240,9 @@ def _safe_segment(value: str) -> str:
|
|
|
241
240
|
return _safe_fs_segment(value)
|
|
242
241
|
|
|
243
242
|
|
|
244
|
-
def
|
|
243
|
+
def _work_category_namespace(work_category: str) -> str:
|
|
245
244
|
key = (work_category or "").strip().lower()
|
|
246
|
-
return
|
|
245
|
+
return _WORK_CATEGORY_NAMESPACE.get(key, "task")
|
|
247
246
|
|
|
248
247
|
|
|
249
248
|
def _git(cwd: Path, *args: str) -> subprocess.CompletedProcess:
|
|
@@ -663,11 +662,13 @@ def compute_branch_name(
|
|
|
663
662
|
stage_number: Optional[int] = None,
|
|
664
663
|
group_id: Optional[str] = None,
|
|
665
664
|
) -> str:
|
|
666
|
-
"""One branch per task-key
|
|
667
|
-
implementation stage worktree.
|
|
665
|
+
"""One branch per task-key as `<namespace>/<task-id>`, or
|
|
666
|
+
`<namespace>/<task-id>-s<N>` for an implementation stage worktree. The
|
|
667
|
+
namespace is a controlled constant so its slash is preserved; only the
|
|
668
|
+
task-id segment is sanitised."""
|
|
668
669
|
if stage_number is not None and group_id is not None:
|
|
669
670
|
raise ValueError("stage_number and group_id are mutually exclusive")
|
|
670
|
-
name = f"{
|
|
671
|
+
name = f"{_work_category_namespace(work_category)}/{_safe_segment(task_id_segment)}"
|
|
671
672
|
if stage_number is not None:
|
|
672
673
|
name = f"{name}-s{stage_number}"
|
|
673
674
|
if group_id is not None:
|
|
@@ -38,6 +38,7 @@ from .state import (
|
|
|
38
38
|
resolve_task_identity,
|
|
39
39
|
resolve_task_reference,
|
|
40
40
|
slugify,
|
|
41
|
+
task_read_side_snapshot,
|
|
41
42
|
)
|
|
42
43
|
|
|
43
44
|
__all__ = [
|
|
@@ -64,6 +65,7 @@ __all__ = [
|
|
|
64
65
|
"resolve_task_identity",
|
|
65
66
|
"resolve_task_reference",
|
|
66
67
|
"slugify",
|
|
68
|
+
"task_read_side_snapshot",
|
|
67
69
|
"tasks_root",
|
|
68
70
|
"upsert_project_json",
|
|
69
71
|
]
|
|
@@ -37,6 +37,10 @@ from .dirs import (
|
|
|
37
37
|
class StateError(Exception):
|
|
38
38
|
"""state file 읽기/파싱 실패 — 호출자가 surface 해야 할 오류."""
|
|
39
39
|
|
|
40
|
+
def __init__(self, message: str, *, stage: Optional[str] = None):
|
|
41
|
+
super().__init__(message)
|
|
42
|
+
self.stage = stage
|
|
43
|
+
|
|
40
44
|
|
|
41
45
|
def _load_json(path: Path) -> Optional[dict]:
|
|
42
46
|
if not path.is_file():
|
|
@@ -268,10 +272,16 @@ def resolve_task_identity(project_root: Path, task_key: str) -> dict:
|
|
|
268
272
|
"""
|
|
269
273
|
task_root = find_task_root(project_root, task_key)
|
|
270
274
|
if task_root is None:
|
|
271
|
-
raise StateError(
|
|
275
|
+
raise StateError(
|
|
276
|
+
f"task root not found for {task_key!r}",
|
|
277
|
+
stage="task_root_missing",
|
|
278
|
+
)
|
|
272
279
|
manifest = read_task_manifest(task_root)
|
|
273
280
|
if manifest is None:
|
|
274
|
-
raise StateError(
|
|
281
|
+
raise StateError(
|
|
282
|
+
f"task-manifest.json missing under {task_root}",
|
|
283
|
+
stage="manifest_missing",
|
|
284
|
+
)
|
|
275
285
|
workflow = manifest.get("workflow") or {}
|
|
276
286
|
if not isinstance(workflow, dict):
|
|
277
287
|
workflow = {}
|
|
@@ -292,3 +302,35 @@ def resolve_task_identity(project_root: Path, task_key: str) -> dict:
|
|
|
292
302
|
"taskBriefPath": manifest.get("taskBriefPath") or "",
|
|
293
303
|
"manifest": manifest,
|
|
294
304
|
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def task_read_side_snapshot(project_root: Path, task_key: str) -> dict:
|
|
308
|
+
"""task-show / inspect 용 curated task 상태 view 를 반환한다.
|
|
309
|
+
|
|
310
|
+
resolve_task_identity 가 내부 raw manifest 를 들고 있지만, 이 accessor 는
|
|
311
|
+
caller 에게 manifest shape 를 노출하지 않는 read-side 계약이다.
|
|
312
|
+
"""
|
|
313
|
+
identity = resolve_task_identity(project_root, task_key)
|
|
314
|
+
manifest = identity["manifest"]
|
|
315
|
+
workflow = manifest.get("workflow")
|
|
316
|
+
if not isinstance(workflow, dict):
|
|
317
|
+
workflow = {}
|
|
318
|
+
return {
|
|
319
|
+
"projectRoot": identity["projectRoot"],
|
|
320
|
+
"taskKey": manifest.get("taskKey"),
|
|
321
|
+
"taskType": manifest.get("taskType"),
|
|
322
|
+
"taskRoot": identity["taskRoot"],
|
|
323
|
+
"taskBriefPath": manifest.get("taskBriefPath"),
|
|
324
|
+
"workflow": {
|
|
325
|
+
"currentPhase": workflow.get("currentPhase"),
|
|
326
|
+
"currentPhaseState": workflow.get("currentPhaseState"),
|
|
327
|
+
"lastCompletedPhase": workflow.get("lastCompletedPhase"),
|
|
328
|
+
"nextRecommendedPhase": workflow.get("nextRecommendedPhase"),
|
|
329
|
+
"routingStatus": workflow.get("routingStatus"),
|
|
330
|
+
"phaseStates": workflow.get("phaseStates"),
|
|
331
|
+
},
|
|
332
|
+
"resultContract": manifest.get("resultContract"),
|
|
333
|
+
"artifacts": manifest.get("artifacts"),
|
|
334
|
+
"modelAssignments": manifest.get("modelAssignments"),
|
|
335
|
+
"latestRunPath": manifest.get("latestRunPath"),
|
|
336
|
+
}
|
|
@@ -145,13 +145,30 @@ Output: `{ok: true, text: "선택 확인:\n task-type : ...\n ..."}`. Prin
|
|
|
145
145
|
|
|
146
146
|
## Step 5: Render the task bundle
|
|
147
147
|
|
|
148
|
-
When `next.kind == "done"`, fetch the
|
|
148
|
+
When `next.kind == "done"`, fetch the public wizard outcome:
|
|
149
149
|
|
|
150
150
|
```bash
|
|
151
|
-
okstra wizard
|
|
151
|
+
okstra wizard outcome --state-file /var/folders/.../okstra-wizard.AbCd.json
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
-
Output: `{ok: true,
|
|
154
|
+
Output: `{ok: true, outcome: {renderArgs: {...}, persistActions: [...], confirmationText: "..."}}`.
|
|
155
|
+
|
|
156
|
+
Run every `outcome.persistActions[]` entry BEFORE `render-bundle`. The only supported action is:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{"command":"config.set","key":"pr-template-path","scope":"project|global","value":"<path>"}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Execute the matching command for `scope`:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
okstra config set pr-template-path "<value>" --scope project
|
|
166
|
+
okstra config set pr-template-path "<value>" --scope global
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
If an action has an unknown `command`, `key`, or `scope`, stop and report the wizard output instead of inventing a command.
|
|
170
|
+
|
|
171
|
+
Build the `okstra render-bundle` invocation from `outcome.renderArgs`, passing each key as `--<key>` and the value verbatim (including empty strings — they are intentional `use phase default` markers).
|
|
155
172
|
|
|
156
173
|
**Empty-value rule (same as Step 3)**: every flag whose value is the empty string MUST still be passed explicitly as `--<key> ""`. For example: `--workers ""`, `--directive ""`, `--related-tasks ""`. Omitting the flag is forbidden — `render-bundle` distinguishes "flag absent" from "flag present with empty value", and the wizard's intent is always the latter.
|
|
157
174
|
|
|
@@ -295,20 +312,7 @@ stale-SHA 재조정(git-reconcile 분기)을 띄우면, **그 stage 에서 연
|
|
|
295
312
|
|
|
296
313
|
## Persisting the PR template scope (release-handoff)
|
|
297
314
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
```bash
|
|
301
|
-
# project scope
|
|
302
|
-
okstra config set pr-template-path "<path>" --scope project
|
|
303
|
-
# global scope (must be absolute or ~/-prefixed)
|
|
304
|
-
okstra config set pr-template-path "<path>" --scope global
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
The scope is held in the wizard state but is not yet exposed by any `okstra wizard` subcommand. Until the subcommand below ships, read the JSON state file directly with the `Read` tool (literal path captured in Step 2) and inspect the `pr_template_scope` field — it is a plain serialized `WizardState`. Do not shell out (`python3 -c`, `jq`, etc.); the literal-token Bash rule rejects them.
|
|
308
|
-
|
|
309
|
-
## Out-of-scope backlog
|
|
310
|
-
|
|
311
|
-
- **`okstra wizard pr-template-scope --state-file PATH`**: add a thin subcommand to `scripts/okstra_ctl/wizard.py` that prints `{ok: true, scope: "once" | "project" | "global"}` so this skill can drop the `Read`-the-raw-state-file detour. The subcommand should reuse the existing `load_state_file` path; no schema changes required.
|
|
315
|
+
Do not read the wizard state file directly. `okstra wizard outcome` exposes any release-handoff PR template write as `outcome.persistActions[]`; execute those actions before `render-bundle`.
|
|
312
316
|
|
|
313
317
|
## Concurrency
|
|
314
318
|
|
|
@@ -16,6 +16,7 @@ Subcommands:
|
|
|
16
16
|
step submit an answer (or fetch the current prompt) and emit next
|
|
17
17
|
render-args emit the final --flag/value map for 'okstra render-bundle'
|
|
18
18
|
confirmation emit the multi-line confirmation echo block
|
|
19
|
+
outcome emit render args, persistence actions, and confirmation text
|
|
19
20
|
|
|
20
21
|
Usage:
|
|
21
22
|
okstra wizard new-state-file
|
|
@@ -23,6 +24,7 @@ Usage:
|
|
|
23
24
|
okstra wizard step --state-file <path> [--answer <value>]
|
|
24
25
|
okstra wizard render-args --state-file <path>
|
|
25
26
|
okstra wizard confirmation --state-file <path>
|
|
27
|
+
okstra wizard outcome --state-file <path>
|
|
26
28
|
|
|
27
29
|
'init' auto-fills --workspace-root from 'okstra paths --field workspace',
|
|
28
30
|
so callers do not pass it.
|
|
@@ -65,7 +67,7 @@ export async function run(args) {
|
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
const [sub, ...rest] = args;
|
|
68
|
-
if (!["new-state-file", "init", "step", "render-args", "confirmation"].includes(sub)) {
|
|
70
|
+
if (!["new-state-file", "init", "step", "render-args", "confirmation", "outcome"].includes(sub)) {
|
|
69
71
|
process.stderr.write(`error: unknown wizard subcommand '${sub}'\n\n${USAGE}`);
|
|
70
72
|
return 2;
|
|
71
73
|
}
|
|
@@ -40,8 +40,8 @@ const SCRIPT = `
|
|
|
40
40
|
import json, sys
|
|
41
41
|
from pathlib import Path
|
|
42
42
|
from okstra_project import (
|
|
43
|
-
resolve_project_root,
|
|
44
|
-
ResolverError,
|
|
43
|
+
resolve_project_root, task_read_side_snapshot,
|
|
44
|
+
ResolverError, StateError,
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
explicit = sys.argv[1]
|
|
@@ -55,37 +55,17 @@ except ResolverError as e:
|
|
|
55
55
|
sys.exit(2)
|
|
56
56
|
|
|
57
57
|
pr_path = Path(pr)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
try:
|
|
59
|
+
snapshot = task_read_side_snapshot(pr_path, task_key)
|
|
60
|
+
except StateError as e:
|
|
61
|
+
print(json.dumps({
|
|
62
|
+
"ok": False,
|
|
63
|
+
"stage": getattr(e, "stage", None) or "task_snapshot",
|
|
64
|
+
"reason": str(e),
|
|
65
|
+
}))
|
|
66
66
|
sys.exit(1)
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
out = {
|
|
70
|
-
"ok": True,
|
|
71
|
-
"projectRoot": str(pr_path),
|
|
72
|
-
"taskKey": manifest.get("taskKey"),
|
|
73
|
-
"taskType": manifest.get("taskType"),
|
|
74
|
-
"taskRoot": str(task_root),
|
|
75
|
-
"taskBriefPath": manifest.get("taskBriefPath"),
|
|
76
|
-
"workflow": {
|
|
77
|
-
"currentPhase": wf.get("currentPhase"),
|
|
78
|
-
"currentPhaseState": wf.get("currentPhaseState"),
|
|
79
|
-
"lastCompletedPhase": wf.get("lastCompletedPhase"),
|
|
80
|
-
"nextRecommendedPhase": wf.get("nextRecommendedPhase"),
|
|
81
|
-
"routingStatus": wf.get("routingStatus"),
|
|
82
|
-
"phaseStates": wf.get("phaseStates"),
|
|
83
|
-
},
|
|
84
|
-
"resultContract": manifest.get("resultContract"),
|
|
85
|
-
"artifacts": manifest.get("artifacts"),
|
|
86
|
-
"modelAssignments": manifest.get("modelAssignments"),
|
|
87
|
-
"latestRunPath": manifest.get("latestRunPath"),
|
|
88
|
-
}
|
|
68
|
+
out = {"ok": True, **snapshot}
|
|
89
69
|
print(json.dumps(out, ensure_ascii=False, default=str, indent=2))
|
|
90
70
|
`;
|
|
91
71
|
|