okstra 0.214.1 → 0.216.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 +2 -1
- package/dist/cli-registry.d.mts +14 -0
- package/dist/cli-registry.mjs +12 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/chat/chat.mjs +2 -56
- package/dist/commands/chat/chat.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.mjs +5 -2
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/wizard.mjs +6 -0
- package/dist/commands/execute/wizard.mjs.map +1 -1
- package/dist/commands/memory/memory.mjs +57 -6
- package/dist/commands/memory/memory.mjs.map +1 -1
- package/dist/lib/file-lock.d.mts +1 -0
- package/dist/lib/file-lock.mjs +60 -0
- package/dist/lib/file-lock.mjs.map +1 -0
- package/dist/lib/python-command.mjs +11 -1
- package/dist/lib/python-command.mjs.map +1 -1
- package/dist/lib/skill-catalog.mjs +1 -0
- package/dist/lib/skill-catalog.mjs.map +1 -1
- package/dist/lib/types.d.mts +1 -1
- package/docs/architecture.md +2 -2
- package/docs/cli.md +36 -14
- package/docs/project-structure-overview.md +6 -3
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/cli.sh +9 -2
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/interactive.sh +8 -2
- package/runtime/bin/lib/okstra/usage.sh +3 -1
- package/runtime/bin/okstra.sh +7 -1
- package/runtime/prompts/duties/discussion-worker.json +32 -0
- package/runtime/prompts/lead/convergence.md +58 -136
- package/runtime/prompts/lead/okstra-lead-contract.md +45 -12
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +14 -1
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +4 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +4 -11
- package/runtime/python/okstra_ctl/agent/invocation.py +1 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +2 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +4 -4
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +2 -2
- package/runtime/python/okstra_ctl/analysis_packet.py +3 -0
- package/runtime/python/okstra_ctl/consumers.py +8 -2
- package/runtime/python/okstra_ctl/convergence.py +143 -29
- package/runtime/python/okstra_ctl/convergence_critic_prompt.py +57 -0
- package/runtime/python/okstra_ctl/convergence_critic_verify_prompt.py +106 -0
- package/runtime/python/okstra_ctl/convergence_engine.py +140 -95
- package/runtime/python/okstra_ctl/convergence_reverify_prompt.py +3 -27
- package/runtime/python/okstra_ctl/convergence_store.py +11 -4
- package/runtime/python/okstra_ctl/dispatch_core.py +4 -1
- package/runtime/python/okstra_ctl/domain/role.py +1 -0
- package/runtime/python/okstra_ctl/manager_cli.py +27 -1
- package/runtime/python/okstra_ctl/manager_launch.py +44 -4
- package/runtime/python/okstra_ctl/manager_sync.py +14 -1
- package/runtime/python/okstra_ctl/manager_view.py +53 -1
- package/runtime/python/okstra_ctl/next_phase.py +1 -0
- package/runtime/python/okstra_ctl/phases/catalog.py +2 -1
- package/runtime/python/okstra_ctl/phases/discussion/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/discussion/boundary.json +11 -0
- package/runtime/python/okstra_ctl/phases/discussion/cli.py +512 -0
- package/runtime/python/okstra_ctl/phases/discussion/entry.py +116 -0
- package/runtime/python/okstra_ctl/phases/discussion/followup.py +876 -0
- package/runtime/python/okstra_ctl/phases/discussion/memory.py +294 -0
- package/runtime/python/okstra_ctl/phases/discussion/overview.py +85 -0
- package/runtime/python/okstra_ctl/phases/discussion/profile.json +31 -0
- package/runtime/python/okstra_ctl/phases/discussion/profile.md +48 -0
- package/runtime/python/okstra_ctl/phases/discussion/report.py +73 -0
- package/runtime/python/okstra_ctl/phases/discussion/report_assets/discussion.template.html +15 -0
- package/runtime/python/okstra_ctl/phases/discussion/report_assets/discussion.template.md +6 -0
- package/runtime/python/okstra_ctl/phases/discussion/spec.md +197 -0
- package/runtime/python/okstra_ctl/phases/discussion/store.py +1143 -0
- package/runtime/python/okstra_ctl/phases/discussion/validation.py +111 -0
- package/runtime/python/okstra_ctl/phases/discussion/wizard.py +90 -0
- package/runtime/python/okstra_ctl/phases/discussion/workflow.py +667 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/host-rules.md +4 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +1 -1
- package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +1 -1
- package/runtime/python/okstra_ctl/plan_run_root.py +8 -0
- package/runtime/python/okstra_ctl/render.py +7 -20
- package/runtime/python/okstra_ctl/render_final_report.py +2 -1
- package/runtime/python/okstra_ctl/report_assembly.py +22 -0
- package/runtime/python/okstra_ctl/report_contract.py +2 -0
- package/runtime/python/okstra_ctl/report_narrative.py +13 -4
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +10 -0
- package/runtime/python/okstra_ctl/run.py +40 -7
- package/runtime/python/okstra_ctl/stage_close.py +46 -11
- package/runtime/python/okstra_ctl/stage_reopen.py +151 -0
- package/runtime/python/okstra_ctl/verdict_blocks.py +11 -35
- package/runtime/python/okstra_ctl/wizard/cli.py +8 -0
- package/runtime/python/okstra_ctl/wizard/confirmation.py +4 -1
- package/runtime/python/okstra_ctl/wizard/engine.py +8 -0
- package/runtime/python/okstra_ctl/wizard/ids.py +12 -0
- package/runtime/python/okstra_ctl/wizard/registry.py +35 -3
- package/runtime/python/okstra_ctl/wizard/render.py +4 -1
- package/runtime/python/okstra_ctl/wizard/sources.py +2 -2
- package/runtime/python/okstra_ctl/wizard/state.py +11 -1
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +4 -4
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +24 -6
- package/runtime/python/okstra_token_usage/pricing.py +2 -0
- package/runtime/schemas/convergence-groups-v1.0.schema.json +1 -1
- package/runtime/schemas/convergence-groups-v2.0.schema.json +1 -1
- package/runtime/schemas/final-report-v3.0.schema.json +90 -1
- package/runtime/skills/_fragments/wizard-bash-invocation-rule.md +1 -0
- package/runtime/skills/_fragments/wizard-confirm-step.md +7 -0
- package/runtime/skills/_fragments/wizard-lead-takeover.md +8 -0
- package/runtime/skills/_fragments/wizard-preflight.md +75 -0
- package/runtime/skills/_fragments/wizard-prompt-loop.md +22 -0
- package/runtime/skills/_fragments/wizard-relay-protocol.md +40 -0
- package/runtime/skills/_fragments/wizard-render-bundle.md +74 -0
- package/runtime/skills/okstra-discussion/SKILL.md +347 -0
- package/runtime/skills/okstra-pr-gen/SKILL.md +7 -0
- package/runtime/skills/okstra-run/SKILL.md +22 -2
- package/runtime/templates/manager/view.template.html +3 -0
- package/runtime/templates/reports/html/i18n/en.json +5 -0
- package/runtime/templates/reports/html/i18n/ko.json +5 -0
- package/runtime/validators/validate-brief.py +3 -2
- package/runtime/validators/validate-run.py +4 -6
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""토론 완료 검사 — 완료 확인 전 초안의 빠진 항목, 그리고 발행된 확정본의 필수 내용."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
|
|
6
|
+
_REDUCED_CHOICES = ("reduce",)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def completion_errors(draft: Mapping[str, Any], direction: str, risk_acknowledgement: str) -> list[str]:
|
|
10
|
+
errors: list[str] = []
|
|
11
|
+
if draft["subject"] is None:
|
|
12
|
+
errors.append("the subject and the question are not confirmed (`okstra discussion confirm-subject`)")
|
|
13
|
+
if not direction.strip():
|
|
14
|
+
errors.append("the user's direction choice is empty (--direction)")
|
|
15
|
+
if not risk_acknowledgement.strip():
|
|
16
|
+
errors.append("the user's confirmation of the remaining disagreements and risks is empty (--risk)")
|
|
17
|
+
if not draft["requirements"]:
|
|
18
|
+
errors.append("no requirement is recorded for this draft (`okstra discussion add-requirement`)")
|
|
19
|
+
kinds = {row["scopeKind"] for row in draft["scope"]}
|
|
20
|
+
for kind in ("in-scope", "excluded"):
|
|
21
|
+
if kind not in kinds:
|
|
22
|
+
errors.append(
|
|
23
|
+
f"no `{kind}` entry is recorded for this draft (`okstra discussion add-scope --kind {kind}`); "
|
|
24
|
+
"record an explicit entry even when it is `none`"
|
|
25
|
+
)
|
|
26
|
+
cycles = draft["cycles"]
|
|
27
|
+
if not any(cycle["status"] == "closed" for cycle in cycles):
|
|
28
|
+
errors.append("no research cycle has closed; the revision would carry no evidence")
|
|
29
|
+
if cycles and cycles[-1]["status"] == "open":
|
|
30
|
+
errors.append(f"research cycle {cycles[-1]['recordId']} is still open; settle and close it first")
|
|
31
|
+
if cycles and cycles[-1]["status"] == "superseded":
|
|
32
|
+
errors.append(
|
|
33
|
+
f"the user chose to replace a participant of {cycles[-1]['recordId']}; "
|
|
34
|
+
"the replacement run has not closed its cycle"
|
|
35
|
+
)
|
|
36
|
+
if draft["unrecoveredFailures"]:
|
|
37
|
+
errors.append(
|
|
38
|
+
f"{', '.join(draft['unrecoveredFailures'])} wait for the user's choice (`okstra discussion recover`)"
|
|
39
|
+
)
|
|
40
|
+
if draft["pendingForUser"]:
|
|
41
|
+
errors.append(f"{', '.join(draft['pendingForUser'])} still wait for the user's answer")
|
|
42
|
+
if draft["pendingForParticipants"]:
|
|
43
|
+
errors.append(f"{', '.join(draft['pendingForParticipants'])} still wait for a participant's answer")
|
|
44
|
+
return errors
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _missing(revision: Mapping[str, Any], path: str) -> bool:
|
|
48
|
+
node: Any = revision
|
|
49
|
+
for part in path.split("."):
|
|
50
|
+
if not isinstance(node, Mapping) or part not in node:
|
|
51
|
+
return True
|
|
52
|
+
node = node[part]
|
|
53
|
+
return node is None or node == "" or node == []
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_REQUIRED = (
|
|
57
|
+
"revisionId",
|
|
58
|
+
"taskKey",
|
|
59
|
+
"subject.subject",
|
|
60
|
+
"subject.question",
|
|
61
|
+
"confirmation.recordId",
|
|
62
|
+
"confirmation.draftVersion",
|
|
63
|
+
"confirmation.riskAcknowledgement",
|
|
64
|
+
"direction",
|
|
65
|
+
"requirements",
|
|
66
|
+
"scope.inScope",
|
|
67
|
+
"scope.excluded",
|
|
68
|
+
"participants.cycles",
|
|
69
|
+
)
|
|
70
|
+
_LISTS = ("startInputs", "decisions", "evidence", "disagreements", "unverified", "participants.failures",
|
|
71
|
+
"itemChanges.carried", "itemChanges.added", "itemChanges.withdrawn")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def revision_errors(revision: Mapping[str, Any]) -> list[str]:
|
|
75
|
+
"""확정본이 설계가 정한 내용을 빠짐없이, 서로 어긋나지 않게 싣는가."""
|
|
76
|
+
errors = [f"`{path}` is missing or empty" for path in _REQUIRED if _missing(revision, path)]
|
|
77
|
+
for path in _LISTS:
|
|
78
|
+
node: Any = revision
|
|
79
|
+
for part in path.split("."):
|
|
80
|
+
node = node.get(part) if isinstance(node, Mapping) else None
|
|
81
|
+
if not isinstance(node, list):
|
|
82
|
+
errors.append(f"`{path}` must be a list")
|
|
83
|
+
if errors:
|
|
84
|
+
return errors
|
|
85
|
+
evidence = {row["recordId"] for row in revision["evidence"]}
|
|
86
|
+
errors += [
|
|
87
|
+
f"disagreement {row['recordId']} is not among the evidence claims"
|
|
88
|
+
for row in revision["disagreements"] if row["recordId"] not in evidence
|
|
89
|
+
]
|
|
90
|
+
reduced = [
|
|
91
|
+
row for row in revision["participants"]["failures"]
|
|
92
|
+
if (row.get("recovery") or {}).get("choice") in _REDUCED_CHOICES
|
|
93
|
+
]
|
|
94
|
+
if bool(reduced) != revision["participants"].get("reducedRoster"):
|
|
95
|
+
errors.append("`participants.reducedRoster` disagrees with the recorded failure choices")
|
|
96
|
+
unverified = {row.get("recordId") for row in revision["unverified"]}
|
|
97
|
+
errors += [
|
|
98
|
+
f"failure {row['recordId']} left its scope unverified but is not listed under `unverified`"
|
|
99
|
+
for row in reduced if row["recordId"] not in unverified
|
|
100
|
+
]
|
|
101
|
+
items = [*revision["requirements"], *revision["scope"]["inScope"], *revision["scope"]["excluded"]]
|
|
102
|
+
changes = revision["itemChanges"]
|
|
103
|
+
if sorted(row["recordId"] for row in items) != sorted(
|
|
104
|
+
row["recordId"] for row in (*changes["carried"], *changes["added"])):
|
|
105
|
+
errors.append("`itemChanges` carried and added items disagree with the requirements and scope")
|
|
106
|
+
if changes["carried"] and changes.get("carriedFrom") is None:
|
|
107
|
+
errors.append("`itemChanges.carried` lists items but names no revision they were carried from")
|
|
108
|
+
previous = revision.get("previousRevision")
|
|
109
|
+
if previous is not None and any(_missing(previous, key) for key in ("revisionId", "contentDigest", "changeReason")):
|
|
110
|
+
errors.append("`previousRevision` must name the revision id, its content digest and the reason for change")
|
|
111
|
+
return errors
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""토론 시작 입력 질문 — brief 경로 대신 링크·짧은 설명·이 task 의 토론 이어가기를 받는다."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from okstra_ctl.prepare_error import PrepareError
|
|
8
|
+
from okstra_ctl.wizard.ids import (
|
|
9
|
+
PICK_TYPE_CUSTOM,
|
|
10
|
+
S_DISCUSSION_START,
|
|
11
|
+
S_DISCUSSION_START_PICK,
|
|
12
|
+
)
|
|
13
|
+
from okstra_ctl.wizard.prompts import _opt, _p
|
|
14
|
+
from okstra_ctl.wizard.state import Prompt, WizardError, WizardState
|
|
15
|
+
|
|
16
|
+
from .entry import classify_start_input
|
|
17
|
+
from .store import DiscussionStore, has_records
|
|
18
|
+
|
|
19
|
+
_CONTINUE = "__continue__"
|
|
20
|
+
_BRIEF = "__brief__"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _task_key(state: WizardState) -> str:
|
|
24
|
+
return f"{state.project_id}:{state.task_group}:{state.task_id}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _has_discussion(state: WizardState) -> bool:
|
|
28
|
+
return not state.is_new_task and has_records(
|
|
29
|
+
DiscussionStore(Path(state.project_root), _task_key(state))
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _brief(state: WizardState) -> str:
|
|
34
|
+
"""새 task 는 방금 고른 brief, 기존 task 는 매니페스트에 등록된 brief.
|
|
35
|
+
|
|
36
|
+
토론 run 이 등록한 brief 는 prepare 가 만든 입력 문서라 후보가 아니다."""
|
|
37
|
+
if state.brief_path:
|
|
38
|
+
return state.brief_path
|
|
39
|
+
existing = state.existing_brief_path
|
|
40
|
+
if not existing:
|
|
41
|
+
return ""
|
|
42
|
+
generated = DiscussionStore(Path(state.project_root), _task_key(state)).input_path
|
|
43
|
+
resolved = Path(existing) if Path(existing).is_absolute() else Path(state.project_root) / existing
|
|
44
|
+
return "" if resolved.resolve() == generated.resolve() else existing
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _accept(state: WizardState, value: str) -> str:
|
|
48
|
+
try:
|
|
49
|
+
kind, text = classify_start_input(value, Path(state.project_root), _task_key(state))
|
|
50
|
+
except PrepareError as exc:
|
|
51
|
+
raise WizardError(str(exc)) from exc
|
|
52
|
+
state.discussion_start = text
|
|
53
|
+
state.discussion_start_pending_text = False
|
|
54
|
+
return f"discussion-start: [{kind}] {text}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def build_discussion_start_pick(state: WizardState) -> Prompt:
|
|
58
|
+
t = _p(state.workspace_root, S_DISCUSSION_START_PICK)
|
|
59
|
+
options = []
|
|
60
|
+
if _has_discussion(state):
|
|
61
|
+
options.append(_opt(_CONTINUE, t["options"][_CONTINUE].format(task_key=_task_key(state)),
|
|
62
|
+
recommended=True))
|
|
63
|
+
brief = _brief(state)
|
|
64
|
+
if brief:
|
|
65
|
+
options.append(_opt(_BRIEF, t["options"][_BRIEF].format(brief=brief),
|
|
66
|
+
recommended=not options))
|
|
67
|
+
options.append(_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]))
|
|
68
|
+
return Prompt(step=S_DISCUSSION_START_PICK, kind="pick", label=t["label"],
|
|
69
|
+
options=options, echo_template=t["echo_template"])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def submit_discussion_start_pick(state: WizardState, value: str) -> Optional[str]:
|
|
73
|
+
if value == _CONTINUE and _has_discussion(state):
|
|
74
|
+
return _accept(state, _task_key(state))
|
|
75
|
+
if value == _BRIEF and _brief(state):
|
|
76
|
+
return _accept(state, _brief(state))
|
|
77
|
+
if value == PICK_TYPE_CUSTOM:
|
|
78
|
+
state.discussion_start_pending_text = True
|
|
79
|
+
return None
|
|
80
|
+
raise WizardError(f"expected an offered option or {PICK_TYPE_CUSTOM!r}, got: {value!r}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def build_discussion_start(state: WizardState) -> Prompt:
|
|
84
|
+
t = _p(state.workspace_root, S_DISCUSSION_START)
|
|
85
|
+
return Prompt(step=S_DISCUSSION_START, kind="text", label=t["label"],
|
|
86
|
+
echo_template=t["echo_template"])
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def submit_discussion_start(state: WizardState, value: str) -> Optional[str]:
|
|
90
|
+
return _accept(state, value)
|