okstra 0.184.0 → 0.185.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.md +1 -1
- package/dist/cli-registry.mjs +9 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/chat/chat.d.mts +1 -0
- package/dist/commands/chat/chat.mjs +385 -0
- package/dist/commands/chat/chat.mjs.map +1 -0
- package/dist/lib/skill-catalog.mjs +1 -0
- package/dist/lib/skill-catalog.mjs.map +1 -1
- package/docs/architecture.md +8 -6
- package/docs/cli.md +2 -1
- package/docs/for-ai/README.md +4 -2
- package/docs/for-ai/skills/okstra-chat.md +28 -0
- package/docs/for-ai/skills/okstra-inspect.md +1 -1
- package/docs/for-ai/skills/okstra-run.md +2 -2
- package/docs/for-ai/skills/okstra-user-response.md +10 -8
- package/docs/project-structure-overview.md +5 -4
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/implementation-planning.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/lead/okstra-lead-contract.md +6 -5
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/profiles/_clarification-recommendation.md +2 -2
- package/runtime/prompts/profiles/implementation-planning.md +2 -1
- package/runtime/prompts/wizard/prompts.ko.json +2 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +1 -1
- package/runtime/python/okstra_ctl/next_phase.py +67 -4
- package/runtime/python/okstra_ctl/user_response.py +147 -37
- package/runtime/python/okstra_ctl/wizard.py +13 -0
- package/runtime/skills/okstra-chat/SKILL.md +104 -0
- package/runtime/skills/okstra-inspect/facets/status.md +6 -5
- package/runtime/skills/okstra-run/SKILL.md +2 -2
- package/runtime/skills/okstra-user-response/SKILL.md +50 -16
- package/runtime/validators/validate-run.py +90 -15
|
@@ -642,6 +642,35 @@ def write_json(path: Path, payload: dict) -> None:
|
|
|
642
642
|
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
|
|
643
643
|
|
|
644
644
|
|
|
645
|
+
def _report_already_approved(report_data: Mapping[str, Any] | None) -> bool:
|
|
646
|
+
if not isinstance(report_data, Mapping):
|
|
647
|
+
return False
|
|
648
|
+
frontmatter = report_data.get("frontmatter")
|
|
649
|
+
return isinstance(frontmatter, Mapping) and frontmatter.get("approved") is True
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _derive_awaiting_approval(
|
|
653
|
+
*,
|
|
654
|
+
existing: bool,
|
|
655
|
+
validation_status: str,
|
|
656
|
+
current_phase: str,
|
|
657
|
+
pointer: Mapping[str, str],
|
|
658
|
+
report_data: Mapping[str, Any] | None,
|
|
659
|
+
) -> bool:
|
|
660
|
+
"""planning 이 승인 가능한 plan-ready 를 남기면 올리고, implementation
|
|
661
|
+
이 그 승인을 소비하면 내린다. 차단 게이트나 열린 Blocks=approval 은
|
|
662
|
+
포인터가 blocked 라 올리지 않는다."""
|
|
663
|
+
if validation_status == "passed" and current_phase == "implementation":
|
|
664
|
+
return False
|
|
665
|
+
if validation_status == "passed" and current_phase == "implementation-planning":
|
|
666
|
+
return (
|
|
667
|
+
pointer.get("phase") == "implementation"
|
|
668
|
+
and pointer.get("status") == next_phase.STATUS_READY
|
|
669
|
+
and not _report_already_approved(report_data)
|
|
670
|
+
)
|
|
671
|
+
return existing
|
|
672
|
+
|
|
673
|
+
|
|
645
674
|
def update_workflow_metadata(
|
|
646
675
|
run_manifest: dict,
|
|
647
676
|
task_manifest: dict,
|
|
@@ -693,7 +722,8 @@ def update_workflow_metadata(
|
|
|
693
722
|
next_recommended_phase = next_phase.make(
|
|
694
723
|
phase=projected["phase"],
|
|
695
724
|
status=projected["status"],
|
|
696
|
-
rationale=
|
|
725
|
+
rationale=projected["rationale"]
|
|
726
|
+
or (
|
|
697
727
|
"리포트 라우팅에서 투영됨. 리드가 쓴 값과 근거는 "
|
|
698
728
|
"nextRecommendedPhaseCorrection.authored 에 있다."
|
|
699
729
|
),
|
|
@@ -713,16 +743,16 @@ def update_workflow_metadata(
|
|
|
713
743
|
status=next_phase.STATUS_BLOCKED, rationale=authored["rationale"]
|
|
714
744
|
)
|
|
715
745
|
|
|
716
|
-
|
|
717
|
-
if not isinstance(
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
746
|
+
awaiting_existing = workflow.get("awaitingApproval")
|
|
747
|
+
if not isinstance(awaiting_existing, bool):
|
|
748
|
+
awaiting_existing = False
|
|
749
|
+
awaiting_approval = _derive_awaiting_approval(
|
|
750
|
+
existing=awaiting_existing,
|
|
751
|
+
validation_status=validation_status,
|
|
752
|
+
current_phase=current_phase,
|
|
753
|
+
pointer=next_recommended_phase,
|
|
754
|
+
report_data=report_data,
|
|
755
|
+
)
|
|
726
756
|
|
|
727
757
|
last_safe_checkpoint = workflow.get("lastSafeCheckpoint", {})
|
|
728
758
|
if not isinstance(last_safe_checkpoint, dict):
|
|
@@ -3562,6 +3592,7 @@ def validate_final_report_data(
|
|
|
3562
3592
|
_validate_clarification_evidence_note(data, failures)
|
|
3563
3593
|
_validate_approval_clarification_backtrace(data, failures)
|
|
3564
3594
|
_validate_rerun_guidance(data, failures)
|
|
3595
|
+
_validate_approval_guidance(data, failures)
|
|
3565
3596
|
_validate_variation_point_analysis(
|
|
3566
3597
|
(data.get("implementationPlanning") or {}).get("variationPointAnalysis"),
|
|
3567
3598
|
resolve_architecture(_project_root_from_report(report_path)),
|
|
@@ -6140,6 +6171,7 @@ def _validate_approval_clarification_backtrace(
|
|
|
6140
6171
|
|
|
6141
6172
|
|
|
6142
6173
|
_RERUN_FLAG = "--answered-clarifications"
|
|
6174
|
+
_APPROVE_HINT = re.compile(r"--approve|\bapprov", re.IGNORECASE)
|
|
6143
6175
|
|
|
6144
6176
|
|
|
6145
6177
|
def _next_step_texts(steps: object) -> list[str]:
|
|
@@ -6156,6 +6188,24 @@ def _next_step_texts(steps: object) -> list[str]:
|
|
|
6156
6188
|
return texts
|
|
6157
6189
|
|
|
6158
6190
|
|
|
6191
|
+
def _has_blocks_approval_row(data: dict) -> bool:
|
|
6192
|
+
return any(
|
|
6193
|
+
isinstance(row, dict) and row.get("blocks") == "approval"
|
|
6194
|
+
for row in data.get("clarificationItems") or []
|
|
6195
|
+
)
|
|
6196
|
+
|
|
6197
|
+
|
|
6198
|
+
def _planning_gate_blocks_approval(data: dict) -> bool:
|
|
6199
|
+
planning = data.get("implementationPlanning")
|
|
6200
|
+
if not isinstance(planning, dict):
|
|
6201
|
+
return False
|
|
6202
|
+
verification = planning.get("planBodyVerification")
|
|
6203
|
+
if not isinstance(verification, dict):
|
|
6204
|
+
return False
|
|
6205
|
+
gate = str(verification.get("gateResult") or "").strip().lower()
|
|
6206
|
+
return gate in {"blocked-by-disagreement", "aborted-non-result"}
|
|
6207
|
+
|
|
6208
|
+
|
|
6159
6209
|
def _validate_rerun_guidance(data: dict, failures: list[str]) -> None:
|
|
6160
6210
|
"""A report that withholds approval must say how to come back from it.
|
|
6161
6211
|
|
|
@@ -6168,10 +6218,7 @@ def _validate_rerun_guidance(data: dict, failures: list[str]) -> None:
|
|
|
6168
6218
|
"""
|
|
6169
6219
|
if (data.get("header") or {}).get("taskType") != "implementation-planning":
|
|
6170
6220
|
return
|
|
6171
|
-
has_blocker =
|
|
6172
|
-
isinstance(row, dict) and row.get("blocks") == "approval"
|
|
6173
|
-
for row in data.get("clarificationItems") or []
|
|
6174
|
-
)
|
|
6221
|
+
has_blocker = _has_blocks_approval_row(data)
|
|
6175
6222
|
if not has_blocker:
|
|
6176
6223
|
return
|
|
6177
6224
|
if any(_RERUN_FLAG in text for text in _next_step_texts(
|
|
@@ -6187,6 +6234,34 @@ def _validate_rerun_guidance(data: dict, failures: list[str]) -> None:
|
|
|
6187
6234
|
)
|
|
6188
6235
|
|
|
6189
6236
|
|
|
6237
|
+
def _validate_approval_guidance(data: dict, failures: list[str]) -> None:
|
|
6238
|
+
"""승인 가능한 plan-ready 는 사용자에게 승인하라고 말해야 한다.
|
|
6239
|
+
|
|
6240
|
+
포인터가 implementation/ready 여도 승인은 사용자만 뒤집는다. 다음 단계
|
|
6241
|
+
안내가 계획 재실행이면 승인 칸을 건너뛰고 같은 단계를 다시 돈다.
|
|
6242
|
+
"""
|
|
6243
|
+
if (data.get("header") or {}).get("taskType") != "implementation-planning":
|
|
6244
|
+
return
|
|
6245
|
+
planning = data.get("implementationPlanning")
|
|
6246
|
+
if not isinstance(planning, dict) or planning.get("outcome") != "plan-ready":
|
|
6247
|
+
return
|
|
6248
|
+
if _has_blocks_approval_row(data) or _planning_gate_blocks_approval(data):
|
|
6249
|
+
return
|
|
6250
|
+
if _report_already_approved(data):
|
|
6251
|
+
return
|
|
6252
|
+
if any(_APPROVE_HINT.search(text) for text in _next_step_texts(
|
|
6253
|
+
data.get("recommendedNextSteps")
|
|
6254
|
+
)):
|
|
6255
|
+
return
|
|
6256
|
+
failures.append(
|
|
6257
|
+
"final-report data.json: this plan is ready for the user to approve, "
|
|
6258
|
+
"but no `recommendedNextSteps` entry tells the reader to approve — "
|
|
6259
|
+
"name `--approve` or the in-session wizard in a step's `text` or "
|
|
6260
|
+
"one of its `commands`. Do not recommend another "
|
|
6261
|
+
"implementation-planning run."
|
|
6262
|
+
)
|
|
6263
|
+
|
|
6264
|
+
|
|
6190
6265
|
def _validate_self_fix_grouping(data: dict, failures: list[str]) -> None:
|
|
6191
6266
|
"""A self-fix round must be instructed by cause, not as a flat item list.
|
|
6192
6267
|
|