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.
Files changed (118) hide show
  1. package/README.md +2 -1
  2. package/dist/cli-registry.d.mts +14 -0
  3. package/dist/cli-registry.mjs +12 -0
  4. package/dist/cli-registry.mjs.map +1 -1
  5. package/dist/commands/chat/chat.mjs +2 -56
  6. package/dist/commands/chat/chat.mjs.map +1 -1
  7. package/dist/commands/execute/render-bundle.mjs +5 -2
  8. package/dist/commands/execute/render-bundle.mjs.map +1 -1
  9. package/dist/commands/execute/wizard.mjs +6 -0
  10. package/dist/commands/execute/wizard.mjs.map +1 -1
  11. package/dist/commands/memory/memory.mjs +57 -6
  12. package/dist/commands/memory/memory.mjs.map +1 -1
  13. package/dist/lib/file-lock.d.mts +1 -0
  14. package/dist/lib/file-lock.mjs +60 -0
  15. package/dist/lib/file-lock.mjs.map +1 -0
  16. package/dist/lib/python-command.mjs +11 -1
  17. package/dist/lib/python-command.mjs.map +1 -1
  18. package/dist/lib/skill-catalog.mjs +1 -0
  19. package/dist/lib/skill-catalog.mjs.map +1 -1
  20. package/dist/lib/types.d.mts +1 -1
  21. package/docs/architecture.md +2 -2
  22. package/docs/cli.md +36 -14
  23. package/docs/project-structure-overview.md +6 -3
  24. package/package.json +1 -1
  25. package/runtime/BUILD.json +2 -2
  26. package/runtime/bin/lib/okstra/cli.sh +9 -2
  27. package/runtime/bin/lib/okstra/globals.sh +2 -0
  28. package/runtime/bin/lib/okstra/interactive.sh +8 -2
  29. package/runtime/bin/lib/okstra/usage.sh +3 -1
  30. package/runtime/bin/okstra.sh +7 -1
  31. package/runtime/prompts/duties/discussion-worker.json +32 -0
  32. package/runtime/prompts/lead/convergence.md +58 -136
  33. package/runtime/prompts/lead/okstra-lead-contract.md +45 -12
  34. package/runtime/prompts/profiles/_common-contract.md +1 -1
  35. package/runtime/prompts/wizard/prompts.ko.json +14 -1
  36. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +4 -0
  37. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +4 -11
  38. package/runtime/python/okstra_ctl/agent/invocation.py +1 -0
  39. package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +2 -2
  40. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +4 -4
  41. package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +2 -2
  42. package/runtime/python/okstra_ctl/analysis_packet.py +3 -0
  43. package/runtime/python/okstra_ctl/consumers.py +8 -2
  44. package/runtime/python/okstra_ctl/convergence.py +143 -29
  45. package/runtime/python/okstra_ctl/convergence_critic_prompt.py +57 -0
  46. package/runtime/python/okstra_ctl/convergence_critic_verify_prompt.py +106 -0
  47. package/runtime/python/okstra_ctl/convergence_engine.py +140 -95
  48. package/runtime/python/okstra_ctl/convergence_reverify_prompt.py +3 -27
  49. package/runtime/python/okstra_ctl/convergence_store.py +11 -4
  50. package/runtime/python/okstra_ctl/dispatch_core.py +4 -1
  51. package/runtime/python/okstra_ctl/domain/role.py +1 -0
  52. package/runtime/python/okstra_ctl/manager_cli.py +27 -1
  53. package/runtime/python/okstra_ctl/manager_launch.py +44 -4
  54. package/runtime/python/okstra_ctl/manager_sync.py +14 -1
  55. package/runtime/python/okstra_ctl/manager_view.py +53 -1
  56. package/runtime/python/okstra_ctl/next_phase.py +1 -0
  57. package/runtime/python/okstra_ctl/phases/catalog.py +2 -1
  58. package/runtime/python/okstra_ctl/phases/discussion/__init__.py +1 -0
  59. package/runtime/python/okstra_ctl/phases/discussion/boundary.json +11 -0
  60. package/runtime/python/okstra_ctl/phases/discussion/cli.py +512 -0
  61. package/runtime/python/okstra_ctl/phases/discussion/entry.py +116 -0
  62. package/runtime/python/okstra_ctl/phases/discussion/followup.py +876 -0
  63. package/runtime/python/okstra_ctl/phases/discussion/memory.py +294 -0
  64. package/runtime/python/okstra_ctl/phases/discussion/overview.py +85 -0
  65. package/runtime/python/okstra_ctl/phases/discussion/profile.json +31 -0
  66. package/runtime/python/okstra_ctl/phases/discussion/profile.md +48 -0
  67. package/runtime/python/okstra_ctl/phases/discussion/report.py +73 -0
  68. package/runtime/python/okstra_ctl/phases/discussion/report_assets/discussion.template.html +15 -0
  69. package/runtime/python/okstra_ctl/phases/discussion/report_assets/discussion.template.md +6 -0
  70. package/runtime/python/okstra_ctl/phases/discussion/spec.md +197 -0
  71. package/runtime/python/okstra_ctl/phases/discussion/store.py +1143 -0
  72. package/runtime/python/okstra_ctl/phases/discussion/validation.py +111 -0
  73. package/runtime/python/okstra_ctl/phases/discussion/wizard.py +90 -0
  74. package/runtime/python/okstra_ctl/phases/discussion/workflow.py +667 -0
  75. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +1 -1
  76. package/runtime/python/okstra_ctl/phases/implementation_planning/host-rules.md +4 -1
  77. package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +1 -1
  78. package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +1 -1
  79. package/runtime/python/okstra_ctl/plan_run_root.py +8 -0
  80. package/runtime/python/okstra_ctl/render.py +7 -20
  81. package/runtime/python/okstra_ctl/render_final_report.py +2 -1
  82. package/runtime/python/okstra_ctl/report_assembly.py +22 -0
  83. package/runtime/python/okstra_ctl/report_contract.py +2 -0
  84. package/runtime/python/okstra_ctl/report_narrative.py +13 -4
  85. package/runtime/python/okstra_ctl/report_synthesis_packet.py +10 -0
  86. package/runtime/python/okstra_ctl/run.py +40 -7
  87. package/runtime/python/okstra_ctl/stage_close.py +46 -11
  88. package/runtime/python/okstra_ctl/stage_reopen.py +151 -0
  89. package/runtime/python/okstra_ctl/verdict_blocks.py +11 -35
  90. package/runtime/python/okstra_ctl/wizard/cli.py +8 -0
  91. package/runtime/python/okstra_ctl/wizard/confirmation.py +4 -1
  92. package/runtime/python/okstra_ctl/wizard/engine.py +8 -0
  93. package/runtime/python/okstra_ctl/wizard/ids.py +12 -0
  94. package/runtime/python/okstra_ctl/wizard/registry.py +35 -3
  95. package/runtime/python/okstra_ctl/wizard/render.py +4 -1
  96. package/runtime/python/okstra_ctl/wizard/sources.py +2 -2
  97. package/runtime/python/okstra_ctl/wizard/state.py +11 -1
  98. package/runtime/python/okstra_ctl/wizard/steps_identity.py +4 -4
  99. package/runtime/python/okstra_ctl/worker_prompt_policy.py +24 -6
  100. package/runtime/python/okstra_token_usage/pricing.py +2 -0
  101. package/runtime/schemas/convergence-groups-v1.0.schema.json +1 -1
  102. package/runtime/schemas/convergence-groups-v2.0.schema.json +1 -1
  103. package/runtime/schemas/final-report-v3.0.schema.json +90 -1
  104. package/runtime/skills/_fragments/wizard-bash-invocation-rule.md +1 -0
  105. package/runtime/skills/_fragments/wizard-confirm-step.md +7 -0
  106. package/runtime/skills/_fragments/wizard-lead-takeover.md +8 -0
  107. package/runtime/skills/_fragments/wizard-preflight.md +75 -0
  108. package/runtime/skills/_fragments/wizard-prompt-loop.md +22 -0
  109. package/runtime/skills/_fragments/wizard-relay-protocol.md +40 -0
  110. package/runtime/skills/_fragments/wizard-render-bundle.md +74 -0
  111. package/runtime/skills/okstra-discussion/SKILL.md +347 -0
  112. package/runtime/skills/okstra-pr-gen/SKILL.md +7 -0
  113. package/runtime/skills/okstra-run/SKILL.md +22 -2
  114. package/runtime/templates/manager/view.template.html +3 -0
  115. package/runtime/templates/reports/html/i18n/en.json +5 -0
  116. package/runtime/templates/reports/html/i18n/ko.json +5 -0
  117. package/runtime/validators/validate-brief.py +3 -2
  118. 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)