okstra 0.183.2 → 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 +2 -2
- 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 +10 -8
- package/docs/cli.md +9 -5
- 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 +6 -5
- package/docs/task-process/README.md +2 -2
- package/docs/task-process/common-flow.md +2 -3
- package/docs/task-process/error-analysis.md +3 -4
- package/docs/task-process/final-verification.md +2 -3
- package/docs/task-process/implementation-planning.md +3 -4
- package/docs/task-process/implementation.md +2 -3
- package/docs/task-process/release-handoff.md +3 -4
- package/docs/task-process/requirements-discovery.md +3 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +8 -7
- package/runtime/prompts/lead/okstra-lead-contract.md +7 -6
- package/runtime/prompts/lead/plan-body-verification.md +27 -19
- package/runtime/prompts/lead/report-writer.md +4 -4
- package/runtime/prompts/profiles/_clarification-recommendation.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +1 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +11 -12
- package/runtime/prompts/wizard/prompts.ko.json +9 -10
- 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/conformance.py +37 -1
- package/runtime/python/okstra_ctl/incremental_scope.py +84 -39
- package/runtime/python/okstra_ctl/next_phase.py +67 -4
- package/runtime/python/okstra_ctl/plan_items.py +410 -1
- package/runtime/python/okstra_ctl/plan_items_cli.py +346 -31
- package/runtime/python/okstra_ctl/render.py +4 -0
- package/runtime/python/okstra_ctl/user_response.py +147 -37
- package/runtime/python/okstra_ctl/wizard.py +52 -73
- package/runtime/schemas/final-report-v2.0.schema.json +12 -0
- package/runtime/schemas/final-report-v3.0.schema.json +12 -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 +4 -4
- package/runtime/skills/okstra-user-response/SKILL.md +50 -16
- package/runtime/validators/validate-run.py +254 -81
- package/runtime/validators/validate_session_conformance.py +24 -5
|
@@ -954,12 +954,15 @@ def list_awaiting_tasks(home: Path, project_id: str, limit: int) -> list[dict]:
|
|
|
954
954
|
# `§x.y` is a full-reading-copy heading and is not a record coordinate.
|
|
955
955
|
# `path.ext:line` is a source pointer the record does not define.
|
|
956
956
|
_SECTION_REF_RE = re.compile(r"§[\d.]+|[A-Z]{1,4}-\d+|[\w./-]+\.\w+:\d+")
|
|
957
|
+
_PATH_LINE_RE = re.compile(r"[\w./-]+\.\w+:\d+")
|
|
957
958
|
_ID_TOKEN_RE = re.compile(r"^[A-Z]{1,4}-\d+$")
|
|
959
|
+
_PLAN_ITEM_ID_RE = re.compile(r"^P-")
|
|
958
960
|
_ROW_DEFINITION_KEYS = (
|
|
959
961
|
"statement",
|
|
960
962
|
"summary",
|
|
961
963
|
"item",
|
|
962
964
|
"title",
|
|
965
|
+
"subject",
|
|
963
966
|
"check",
|
|
964
967
|
"action",
|
|
965
968
|
"evidence",
|
|
@@ -1992,6 +1995,138 @@ def _option_view(option: Mapping[str, Any], index: int) -> list[str]:
|
|
|
1992
1995
|
]
|
|
1993
1996
|
|
|
1994
1997
|
|
|
1998
|
+
def _option_probe_texts(options: list[Any]) -> list[str]:
|
|
1999
|
+
texts: list[str] = []
|
|
2000
|
+
for option in options:
|
|
2001
|
+
if not isinstance(option, Mapping):
|
|
2002
|
+
continue
|
|
2003
|
+
texts.extend(
|
|
2004
|
+
str(option.get(key) or "")
|
|
2005
|
+
for key in ("answer", "rationale", "addedWork", "directionChange")
|
|
2006
|
+
)
|
|
2007
|
+
return texts
|
|
2008
|
+
|
|
2009
|
+
|
|
2010
|
+
def _row_probe_texts(row: Mapping[str, Any]) -> list[str]:
|
|
2011
|
+
return [
|
|
2012
|
+
str(row.get("statement") or ""),
|
|
2013
|
+
str(row.get("expected_form") or ""),
|
|
2014
|
+
*_option_probe_texts(list(row.get("options") or [])),
|
|
2015
|
+
]
|
|
2016
|
+
|
|
2017
|
+
|
|
2018
|
+
def _path_line_refs(*texts: str) -> list[str]:
|
|
2019
|
+
found: list[str] = []
|
|
2020
|
+
seen: set[str] = set()
|
|
2021
|
+
for text in texts:
|
|
2022
|
+
for match in _PATH_LINE_RE.findall(text or ""):
|
|
2023
|
+
if match not in seen:
|
|
2024
|
+
seen.add(match)
|
|
2025
|
+
found.append(match)
|
|
2026
|
+
return found
|
|
2027
|
+
|
|
2028
|
+
|
|
2029
|
+
def _why_asked(row: Mapping[str, Any]) -> str:
|
|
2030
|
+
approval = row.get("approval_context") or {}
|
|
2031
|
+
if not isinstance(approval, Mapping):
|
|
2032
|
+
return "not stated in the report"
|
|
2033
|
+
unblock = str(approval.get("unblockCondition") or "").strip()
|
|
2034
|
+
if unblock:
|
|
2035
|
+
return unblock
|
|
2036
|
+
classification = str(approval.get("classification") or "").strip()
|
|
2037
|
+
return classification or "not stated in the report"
|
|
2038
|
+
|
|
2039
|
+
|
|
2040
|
+
def _linked_plan_items(
|
|
2041
|
+
record: dict[str, Any] | None, clarification_id: str
|
|
2042
|
+
) -> list[dict[str, str]]:
|
|
2043
|
+
if record is None:
|
|
2044
|
+
return []
|
|
2045
|
+
linked: list[dict[str, str]] = []
|
|
2046
|
+
seen: set[str] = set()
|
|
2047
|
+
|
|
2048
|
+
def walk(node: object) -> None:
|
|
2049
|
+
if isinstance(node, dict):
|
|
2050
|
+
row_id = node.get("id")
|
|
2051
|
+
refs = node.get("clarificationRefs") or []
|
|
2052
|
+
if (
|
|
2053
|
+
isinstance(row_id, str)
|
|
2054
|
+
and _PLAN_ITEM_ID_RE.match(row_id)
|
|
2055
|
+
and isinstance(refs, list)
|
|
2056
|
+
and clarification_id in refs
|
|
2057
|
+
and row_id not in seen
|
|
2058
|
+
):
|
|
2059
|
+
seen.add(row_id)
|
|
2060
|
+
linked.append({
|
|
2061
|
+
"id": row_id,
|
|
2062
|
+
"definition": _row_definition(node) or "not stated in the report",
|
|
2063
|
+
})
|
|
2064
|
+
for value in node.values():
|
|
2065
|
+
walk(value)
|
|
2066
|
+
elif isinstance(node, list):
|
|
2067
|
+
for item in node:
|
|
2068
|
+
walk(item)
|
|
2069
|
+
|
|
2070
|
+
walk(record)
|
|
2071
|
+
return linked
|
|
2072
|
+
|
|
2073
|
+
|
|
2074
|
+
def _format_ref_list(label: str, items: list[str]) -> list[str]:
|
|
2075
|
+
if not items:
|
|
2076
|
+
return [f"{label}: none"]
|
|
2077
|
+
return [f"{label}:", *(f"- {item}" for item in items)]
|
|
2078
|
+
|
|
2079
|
+
|
|
2080
|
+
def _format_open_row_view(
|
|
2081
|
+
row: Mapping[str, Any],
|
|
2082
|
+
record: dict[str, Any] | None,
|
|
2083
|
+
markdown_text: str,
|
|
2084
|
+
response: UserResponseEntry | None,
|
|
2085
|
+
) -> list[str]:
|
|
2086
|
+
item = row["item"]
|
|
2087
|
+
probe = _row_probe_texts(row)
|
|
2088
|
+
refs = sorted(set(_SECTION_REF_RE.findall(" ".join(probe))))
|
|
2089
|
+
resolved = (
|
|
2090
|
+
resolve_refs_from_record(record, refs)
|
|
2091
|
+
if record is not None
|
|
2092
|
+
else resolve_refs(markdown_text, refs)
|
|
2093
|
+
)
|
|
2094
|
+
lines = [
|
|
2095
|
+
"",
|
|
2096
|
+
f"[{item.row_id}]",
|
|
2097
|
+
f"Kind: {item.kind}",
|
|
2098
|
+
f"Blocks: {item.blocks}",
|
|
2099
|
+
f"Report status: {item.status}",
|
|
2100
|
+
f"Question: {row['statement']}",
|
|
2101
|
+
f"Expected form: {row['expected_form']}",
|
|
2102
|
+
f"Current response: {response.value if response else 'none'}",
|
|
2103
|
+
f"Current disposition: {response.disposition if response else 'none'}",
|
|
2104
|
+
f"Why asked: {_why_asked(row)}",
|
|
2105
|
+
"Options:",
|
|
2106
|
+
]
|
|
2107
|
+
approval = row.get("approval_context") or {}
|
|
2108
|
+
if isinstance(approval, Mapping) and approval:
|
|
2109
|
+
lines.extend([
|
|
2110
|
+
f"Approval classification: {approval.get('classification', '')}",
|
|
2111
|
+
f"Approval unblock condition: {approval.get('unblockCondition', '')}",
|
|
2112
|
+
f"Approval recommended disposition: {approval.get('recommendedDisposition', '')}",
|
|
2113
|
+
])
|
|
2114
|
+
for index, option in enumerate(row["options"], start=1):
|
|
2115
|
+
lines.extend(_option_view(option, index))
|
|
2116
|
+
linked = _linked_plan_items(record, item.row_id)
|
|
2117
|
+
lines.extend(_format_ref_list(
|
|
2118
|
+
"Linked plan items",
|
|
2119
|
+
[f"{plan['id']}: {plan['definition']}" for plan in linked],
|
|
2120
|
+
))
|
|
2121
|
+
lines.extend(_format_ref_list("Cited artifacts", _path_line_refs(*probe)))
|
|
2122
|
+
lines.append("Context:")
|
|
2123
|
+
lines.extend(
|
|
2124
|
+
f"- {ref['ref']}: {ref['definition'] or 'not stated in the report'}"
|
|
2125
|
+
for ref in resolved
|
|
2126
|
+
)
|
|
2127
|
+
return lines
|
|
2128
|
+
|
|
2129
|
+
|
|
1995
2130
|
def format_show_view(report_path: Path, project_root: Path) -> str:
|
|
1996
2131
|
context = _validate_owned_report_context(
|
|
1997
2132
|
report_path, expected_project_root=project_root
|
|
@@ -1999,6 +2134,10 @@ def format_show_view(report_path: Path, project_root: Path) -> str:
|
|
|
1999
2134
|
rows, record = _all_report_rows(context.report_path)
|
|
2000
2135
|
state = _existing_sidecar_state(context.sidecar_path)
|
|
2001
2136
|
current = {entry.response_id: entry for entry in state.entries}
|
|
2137
|
+
markdown_text = (
|
|
2138
|
+
"" if record is not None
|
|
2139
|
+
else context.markdown_path.read_text(encoding="utf-8")
|
|
2140
|
+
)
|
|
2002
2141
|
lines = [
|
|
2003
2142
|
"USER RESPONSE REPORT",
|
|
2004
2143
|
f"Report: {context.report_path}",
|
|
@@ -2015,51 +2154,22 @@ def format_show_view(report_path: Path, project_root: Path) -> str:
|
|
|
2015
2154
|
if candidates:
|
|
2016
2155
|
lines.append("Plan option candidates:")
|
|
2017
2156
|
for index, candidate in enumerate(candidates, start=1):
|
|
2157
|
+
current_pick = (
|
|
2158
|
+
state.plan_decision is not None
|
|
2159
|
+
and state.plan_decision.implementation_option == candidate
|
|
2160
|
+
)
|
|
2018
2161
|
lines.extend([
|
|
2019
2162
|
f"Plan option {index}: {candidate}",
|
|
2020
2163
|
f" Recommended: {'yes' if candidate == recommended_name else 'no'}",
|
|
2021
|
-
" Current decision: "
|
|
2022
|
-
f"{'yes' if state.plan_decision and state.plan_decision.implementation_option == candidate else 'no'}",
|
|
2164
|
+
f" Current decision: {'yes' if current_pick else 'no'}",
|
|
2023
2165
|
])
|
|
2024
2166
|
for row in rows:
|
|
2025
2167
|
item = row["item"]
|
|
2026
2168
|
if item.status not in {"open", "answered"} or item.row_id in current:
|
|
2027
2169
|
continue
|
|
2028
|
-
|
|
2029
|
-
row
|
|
2030
|
-
))
|
|
2031
|
-
resolved = (
|
|
2032
|
-
resolve_refs_from_record(record, refs)
|
|
2033
|
-
if record is not None
|
|
2034
|
-
else resolve_refs(context.markdown_path.read_text(encoding="utf-8"), refs)
|
|
2035
|
-
)
|
|
2036
|
-
response = current.get(item.row_id)
|
|
2037
|
-
lines.extend([
|
|
2038
|
-
"",
|
|
2039
|
-
f"[{item.row_id}]",
|
|
2040
|
-
f"Kind: {item.kind}",
|
|
2041
|
-
f"Blocks: {item.blocks}",
|
|
2042
|
-
f"Report status: {item.status}",
|
|
2043
|
-
f"Question: {row['statement']}",
|
|
2044
|
-
f"Expected form: {row['expected_form']}",
|
|
2045
|
-
f"Current response: {response.value if response else 'none'}",
|
|
2046
|
-
f"Current disposition: {response.disposition if response else 'none'}",
|
|
2047
|
-
"Options:",
|
|
2048
|
-
])
|
|
2049
|
-
approval = row.get("approval_context") or {}
|
|
2050
|
-
if approval:
|
|
2051
|
-
lines.extend([
|
|
2052
|
-
f"Approval classification: {approval.get('classification', '')}",
|
|
2053
|
-
f"Approval unblock condition: {approval.get('unblockCondition', '')}",
|
|
2054
|
-
f"Approval recommended disposition: {approval.get('recommendedDisposition', '')}",
|
|
2055
|
-
])
|
|
2056
|
-
for index, option in enumerate(row["options"], start=1):
|
|
2057
|
-
lines.extend(_option_view(option, index))
|
|
2058
|
-
lines.append("Context:")
|
|
2059
|
-
lines.extend(
|
|
2060
|
-
f"- {ref['ref']}: {ref['definition'] or 'not stated in the report'}"
|
|
2061
|
-
for ref in resolved
|
|
2062
|
-
)
|
|
2170
|
+
lines.extend(_format_open_row_view(
|
|
2171
|
+
row, record, markdown_text, current.get(item.row_id),
|
|
2172
|
+
))
|
|
2063
2173
|
return "\n".join(lines) + "\n"
|
|
2064
2174
|
|
|
2065
2175
|
|
|
@@ -1231,17 +1231,6 @@ def _wizard_state_from_json(payload: dict[str, Any]) -> WizardState:
|
|
|
1231
1231
|
if version != 2:
|
|
1232
1232
|
state.execution_identity_version = 2
|
|
1233
1233
|
_convert_v1_provider_selections(state, payload)
|
|
1234
|
-
# v1 은 리더 확인 칸이 없었다. 변환 재개 시 다시 묻지 않는다.
|
|
1235
|
-
if (
|
|
1236
|
-
state.host_entry_mode == "current-session"
|
|
1237
|
-
and _LEADER_SESSION_STEP not in state.answered
|
|
1238
|
-
):
|
|
1239
|
-
state.answered.append(_LEADER_SESSION_STEP)
|
|
1240
|
-
if (
|
|
1241
|
-
state.host_entry_mode == "current-session"
|
|
1242
|
-
and _LEADER_SESSION_STEP not in state.role_selection_order
|
|
1243
|
-
):
|
|
1244
|
-
state.role_selection_order.insert(0, _LEADER_SESSION_STEP)
|
|
1245
1234
|
return state
|
|
1246
1235
|
|
|
1247
1236
|
|
|
@@ -1281,13 +1270,8 @@ def _role_model_prompt_id(role: str, ordinal: int) -> str:
|
|
|
1281
1270
|
return f"role-model:{role}:{ordinal}"
|
|
1282
1271
|
|
|
1283
1272
|
|
|
1284
|
-
_LEADER_SESSION_STEP = "leader-session"
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
1273
|
def _is_role_selection_step(step_id: str) -> bool:
|
|
1288
|
-
return step_id
|
|
1289
|
-
("role-count:", "role-model:", "role-add:")
|
|
1290
|
-
)
|
|
1274
|
+
return step_id.startswith(("role-count:", "role-model:", "role-add:"))
|
|
1291
1275
|
|
|
1292
1276
|
|
|
1293
1277
|
def _selected_role_count(
|
|
@@ -1320,36 +1304,6 @@ def _selectable_static_requirements(
|
|
|
1320
1304
|
return tuple(requirements)
|
|
1321
1305
|
|
|
1322
1306
|
|
|
1323
|
-
def _leader_session_prompt(state: WizardState) -> Prompt:
|
|
1324
|
-
"""current-session 리더 칸: 모델/effort 표시만 하고 role_models 에 쓰지 않는다."""
|
|
1325
|
-
attestation = _host_session_context(state).current_model
|
|
1326
|
-
model_ref = (
|
|
1327
|
-
attestation.normalized_model_ref
|
|
1328
|
-
or attestation.observed_model
|
|
1329
|
-
or "current-session"
|
|
1330
|
-
)
|
|
1331
|
-
steps = _load_wizard_root(state.workspace_root)["steps"]
|
|
1332
|
-
raw = steps.get("leader_session") or {}
|
|
1333
|
-
effort_suffix = ""
|
|
1334
|
-
if attestation.effort:
|
|
1335
|
-
effort_template = raw.get("effort_suffix", " · effort {effort}")
|
|
1336
|
-
effort_suffix = effort_template.format(effort=attestation.effort)
|
|
1337
|
-
prompt = _p(
|
|
1338
|
-
state.workspace_root,
|
|
1339
|
-
"leader_session",
|
|
1340
|
-
model_ref=model_ref,
|
|
1341
|
-
effort_suffix=effort_suffix,
|
|
1342
|
-
)
|
|
1343
|
-
continue_label = prompt["options"].get("continue", "계속")
|
|
1344
|
-
return Prompt(
|
|
1345
|
-
step=_LEADER_SESSION_STEP,
|
|
1346
|
-
kind="pick",
|
|
1347
|
-
label=prompt["label"],
|
|
1348
|
-
options=[_opt("continue", continue_label)],
|
|
1349
|
-
echo_template=prompt["echo_template"],
|
|
1350
|
-
)
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
1307
|
def _count_prompt(
|
|
1354
1308
|
state: WizardState,
|
|
1355
1309
|
requirement: RoleRequirement,
|
|
@@ -1777,13 +1731,6 @@ def next_role_prompt(state: WizardState) -> Prompt | None:
|
|
|
1777
1731
|
if not _role_selection_enabled(state) or not _identity_ready(state):
|
|
1778
1732
|
return None
|
|
1779
1733
|
state.use_defaults = False
|
|
1780
|
-
# current-session: 리더는 읽기 전용 확인 칸만 먼저 보여 준다.
|
|
1781
|
-
if (
|
|
1782
|
-
state.host_entry_mode == "current-session"
|
|
1783
|
-
and _LEADER_SESSION_STEP not in state.answered
|
|
1784
|
-
and _LEADER_SESSION_STEP not in state.role_selection_order
|
|
1785
|
-
):
|
|
1786
|
-
return _leader_session_prompt(state)
|
|
1787
1734
|
profile = _load_role_profile_for_state(state)
|
|
1788
1735
|
for requirement in profile.roles:
|
|
1789
1736
|
# 필수 수량: min < max 이고 min > 0 일 때만.
|
|
@@ -1884,11 +1831,6 @@ def _validate_submitted_role_model(
|
|
|
1884
1831
|
|
|
1885
1832
|
|
|
1886
1833
|
def _submit_role_prompt(state: WizardState, prompt: Prompt, value: str) -> str:
|
|
1887
|
-
if prompt.step == _LEADER_SESSION_STEP:
|
|
1888
|
-
if value != "continue":
|
|
1889
|
-
raise WizardError("leader session step accepts only 'continue'")
|
|
1890
|
-
# role_models["leader"] 에 쓰지 않는다 — 현재 세션 모델을 그대로 쓴다.
|
|
1891
|
-
return "leader-session: continue"
|
|
1892
1834
|
if prompt.step.startswith("role-count:"):
|
|
1893
1835
|
role = prompt.step.split(":", 1)[1]
|
|
1894
1836
|
profile = _load_role_profile_for_state(state)
|
|
@@ -2041,9 +1983,11 @@ def _p(workspace_root: str, step_id: str, **vars: str) -> dict:
|
|
|
2041
1983
|
raise WizardError(f"unknown wizard step_id: {step_id!r}")
|
|
2042
1984
|
label_template = raw.get("label", "")
|
|
2043
1985
|
fv_label_template = raw.get("label_final_verification", "")
|
|
1986
|
+
unlinked_template = raw.get("label_unlinked", "")
|
|
2044
1987
|
try:
|
|
2045
1988
|
label = label_template.format(**vars)
|
|
2046
1989
|
label_final_verification = fv_label_template.format(**vars)
|
|
1990
|
+
label_unlinked = unlinked_template.format(**vars)
|
|
2047
1991
|
except KeyError as exc:
|
|
2048
1992
|
missing = exc.args[0] if exc.args else "<unknown>"
|
|
2049
1993
|
raise WizardError(
|
|
@@ -2052,6 +1996,7 @@ def _p(workspace_root: str, step_id: str, **vars: str) -> dict:
|
|
|
2052
1996
|
return {
|
|
2053
1997
|
"label": label,
|
|
2054
1998
|
"label_final_verification": label_final_verification,
|
|
1999
|
+
"label_unlinked": label_unlinked,
|
|
2055
2000
|
"echo_template": raw.get("echo_template", ""),
|
|
2056
2001
|
"options": raw.get("options", {}),
|
|
2057
2002
|
"options_final_verification": raw.get("options_final_verification", {}),
|
|
@@ -2788,6 +2733,8 @@ def _build_task_type(state: WizardState) -> Prompt:
|
|
|
2788
2733
|
recommended_suffix = t["options"].get("_RECOMMENDED_SUFFIX", "")
|
|
2789
2734
|
rerun_suffix = t["options"].get("_RERUN_SUFFIX", "")
|
|
2790
2735
|
next_suffix = t["options"].get("_NEXT_SUFFIX", "")
|
|
2736
|
+
approve_suffix = t["options"].get("_APPROVE_SUFFIX", recommended_suffix)
|
|
2737
|
+
blocked_rerun_suffix = t["options"].get("_BLOCKED_RERUN_SUFFIX", rerun_suffix)
|
|
2791
2738
|
description_by_type = dict(TASK_TYPES)
|
|
2792
2739
|
options: list[Option] = []
|
|
2793
2740
|
|
|
@@ -2813,10 +2760,21 @@ def _build_task_type(state: WizardState) -> Prompt:
|
|
|
2813
2760
|
# `ready` 가 아니면 추천은 비고, 아래 `currentPhase` 재실행 옵션이 남는다.
|
|
2814
2761
|
# 실패한 run 의 포인터가 `{"phase": "", "status": "blocked"}` 라는 점에서
|
|
2815
2762
|
# 그것이 맞는 제안이다 — 그 옵션은 포인터가 아니라 `currentPhase` 에서 온다.
|
|
2763
|
+
# 계획 승인 대기는 `awaitingApproval` 로 표시한다. 구현이 추천이지만 먼저
|
|
2764
|
+
# 승인을 받아야 하므로 접미사로 구분한다. 열린 C-NNN 때문에 blocked 면
|
|
2765
|
+
# 재실행은 답이 기록된 뒤에만 고르라고 접미사로 말한다.
|
|
2816
2766
|
recommended = (revision_requested or state.task_type
|
|
2817
2767
|
or next_phase.autofill_task_type({"workflow": workflow}))
|
|
2818
2768
|
if not recommended and not workflow:
|
|
2819
2769
|
recommended = TASK_TYPE_VALUES[0]
|
|
2770
|
+
pointer = next_phase.promote(workflow.get("nextRecommendedPhase"))
|
|
2771
|
+
if workflow.get("awaitingApproval") is True and recommended == "implementation":
|
|
2772
|
+
recommended_suffix = approve_suffix
|
|
2773
|
+
if (
|
|
2774
|
+
pointer["status"] == next_phase.STATUS_BLOCKED
|
|
2775
|
+
and (workflow.get("currentPhase") or "") == "implementation-planning"
|
|
2776
|
+
):
|
|
2777
|
+
rerun_suffix = blocked_rerun_suffix
|
|
2820
2778
|
add(recommended, recommended_suffix)
|
|
2821
2779
|
add(workflow.get("currentPhase") or "", rerun_suffix)
|
|
2822
2780
|
add(_phase_after(recommended), next_suffix)
|
|
@@ -4191,14 +4149,15 @@ def _reverify_scope_preview(state: WizardState) -> Optional[dict]:
|
|
|
4191
4149
|
|
|
4192
4150
|
|
|
4193
4151
|
def _reverify_scope_pick_required(state: WizardState) -> bool:
|
|
4194
|
-
"""좁힐 여지가
|
|
4152
|
+
"""좁힐 여지가 있거나, unlinked id 의 stage 번호를 받아야 할 때 묻는다.
|
|
4195
4153
|
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
확인 블록의 `reverify-scope: full 예상` 줄이 이유까지 같이 알려준다.
|
|
4154
|
+
unlinked 는 full 확정이 아니다. SHA 가 바뀌었거나 입력을 읽을 수 없어
|
|
4155
|
+
`wouldForceFull` 인 경우만 질문이 의미 없다.
|
|
4199
4156
|
"""
|
|
4200
4157
|
preview = _reverify_scope_preview(state)
|
|
4201
|
-
return preview is not None and
|
|
4158
|
+
return preview is not None and (
|
|
4159
|
+
not preview["wouldForceFull"] or bool(preview["unlinkedIds"])
|
|
4160
|
+
)
|
|
4202
4161
|
|
|
4203
4162
|
|
|
4204
4163
|
def _reverify_scope_step_pending(state: WizardState) -> bool:
|
|
@@ -4234,13 +4193,20 @@ def _prior_stage_numbers(state: WizardState) -> set[int]:
|
|
|
4234
4193
|
def _build_reverify_scope_pick(state: WizardState) -> Prompt:
|
|
4235
4194
|
t = _p(state.workspace_root, "reverify_scope_pick")
|
|
4236
4195
|
opts = t["options"]
|
|
4196
|
+
preview = _reverify_scope_preview(state) or {}
|
|
4197
|
+
unlinked = bool(preview.get("unlinkedIds"))
|
|
4198
|
+
options = []
|
|
4199
|
+
if not unlinked:
|
|
4200
|
+
options.append(_opt("auto", opts["auto"]))
|
|
4201
|
+
options.append(_opt("full", opts["full"]))
|
|
4202
|
+
options.append(_opt(PICK_TYPE_CUSTOM, opts[PICK_TYPE_CUSTOM]))
|
|
4203
|
+
else:
|
|
4204
|
+
options.append(_opt(PICK_TYPE_CUSTOM, opts[PICK_TYPE_CUSTOM]))
|
|
4205
|
+
options.append(_opt("full", opts["full"]))
|
|
4206
|
+
label = t["label_unlinked"] if unlinked else t["label"]
|
|
4237
4207
|
return Prompt(
|
|
4238
|
-
step=S_REVERIFY_SCOPE_PICK, kind="pick", label=
|
|
4239
|
-
options=
|
|
4240
|
-
_opt("auto", opts["auto"]),
|
|
4241
|
-
_opt("full", opts["full"]),
|
|
4242
|
-
_opt(PICK_TYPE_CUSTOM, opts[PICK_TYPE_CUSTOM]),
|
|
4243
|
-
],
|
|
4208
|
+
step=S_REVERIFY_SCOPE_PICK, kind="pick", label=label,
|
|
4209
|
+
options=options,
|
|
4244
4210
|
echo_template=t["echo_template"])
|
|
4245
4211
|
|
|
4246
4212
|
|
|
@@ -4255,6 +4221,13 @@ def _submit_reverify_scope_pick(state: WizardState, value: str) -> Optional[str]
|
|
|
4255
4221
|
raise WizardError(
|
|
4256
4222
|
f"expected 'auto' / 'full' / {PICK_TYPE_CUSTOM!r}, got: {value!r}"
|
|
4257
4223
|
)
|
|
4224
|
+
if picked == "auto":
|
|
4225
|
+
preview = _reverify_scope_preview(state) or {}
|
|
4226
|
+
unlinked = preview.get("unlinkedIds") or []
|
|
4227
|
+
if unlinked:
|
|
4228
|
+
raise WizardError(
|
|
4229
|
+
t["errors"]["unlinked_auto"].format(ids=", ".join(unlinked))
|
|
4230
|
+
)
|
|
4258
4231
|
state.reverify_scope = picked
|
|
4259
4232
|
state.reverify_scope_pending_text = False
|
|
4260
4233
|
return t["echo_suffixes"][picked]
|
|
@@ -4271,6 +4244,12 @@ def _submit_reverify_scope_stages(state: WizardState, value: str) -> Optional[st
|
|
|
4271
4244
|
t = _p(state.workspace_root, "reverify_scope_stages")
|
|
4272
4245
|
tokens = [token.strip() for token in value.split(",") if token.strip()]
|
|
4273
4246
|
if not tokens:
|
|
4247
|
+
preview = _reverify_scope_preview(state) or {}
|
|
4248
|
+
unlinked = preview.get("unlinkedIds") or []
|
|
4249
|
+
if unlinked:
|
|
4250
|
+
raise WizardError(
|
|
4251
|
+
t["errors"]["unlinked_empty"].format(ids=", ".join(unlinked))
|
|
4252
|
+
)
|
|
4274
4253
|
state.reverify_scope = "auto"
|
|
4275
4254
|
state.reverify_scope_pending_text = False
|
|
4276
4255
|
return t["echo_suffixes"]["auto"]
|
|
@@ -6341,13 +6320,13 @@ def _reverify_scope_line(state: WizardState) -> Optional[str]:
|
|
|
6341
6320
|
if state.reverify_scope and state.reverify_scope != "auto":
|
|
6342
6321
|
return _msg(state.workspace_root, "confirmation",
|
|
6343
6322
|
"reverify_scope_user_stages", stages=state.reverify_scope)
|
|
6344
|
-
if not preview["wouldForceFull"]:
|
|
6345
|
-
return _msg(state.workspace_root, "confirmation",
|
|
6346
|
-
"reverify_scope_incremental")
|
|
6347
6323
|
if preview["unlinkedIds"]:
|
|
6348
6324
|
return _msg(state.workspace_root, "confirmation",
|
|
6349
6325
|
"reverify_scope_unlinked",
|
|
6350
6326
|
ids=", ".join(preview["unlinkedIds"]))
|
|
6327
|
+
if not preview["wouldForceFull"]:
|
|
6328
|
+
return _msg(state.workspace_root, "confirmation",
|
|
6329
|
+
"reverify_scope_incremental")
|
|
6351
6330
|
return _msg(state.workspace_root, "confirmation", "reverify_scope_full",
|
|
6352
6331
|
reason=preview["reason"])
|
|
6353
6332
|
|
|
@@ -9145,6 +9145,12 @@
|
|
|
9145
9145
|
"properties": {
|
|
9146
9146
|
"setAside": {"type": "array", "items": {"type": "object", "required": ["id", "reason"], "additionalProperties": false, "properties": {"id": {"type": "string", "minLength": 1}, "reason": {"type": "string", "enum": ["record", "observed", "deferred"]}}}},
|
|
9147
9147
|
"stageLedger": {"type": "object", "additionalProperties": {"type": "string", "enum": ["done", "active", "ready", "blocked"]}},
|
|
9148
|
+
"dispatchQueue": {
|
|
9149
|
+
"type": "array",
|
|
9150
|
+
"description": "Plan-item ids sent to verifiers this round. In-scope plus plan-wide; deferred and observed stages are omitted.",
|
|
9151
|
+
"items": {"type": "string", "minLength": 1},
|
|
9152
|
+
"uniqueItems": true
|
|
9153
|
+
},
|
|
9148
9154
|
"uniformVerifiers": {
|
|
9149
9155
|
"type": "array",
|
|
9150
9156
|
"description": "Verifiers whose every vote this round was one verdict. Advisory: a unanimous round is legitimate, but the gate reads as a three-way cross-check unless this sits beside it.",
|
|
@@ -9172,6 +9178,10 @@
|
|
|
9172
9178
|
}
|
|
9173
9179
|
}
|
|
9174
9180
|
},
|
|
9181
|
+
"gating": {
|
|
9182
|
+
"type": "boolean",
|
|
9183
|
+
"description": "If false, plan-body verification is advisory: majority-disagree does not block approval, the self-fix loop does not run, and only one automatic round is allowed. Legal only when designPreparation.mode is no-design-inputs and the Stage Map has exactly one row. Prepare emits true; plan-items prepare/seed flip it after those facts exist."
|
|
9184
|
+
},
|
|
9175
9185
|
"roundCount": {
|
|
9176
9186
|
"type": "integer",
|
|
9177
9187
|
"minimum": 0
|
|
@@ -9247,6 +9257,8 @@
|
|
|
9247
9257
|
"properties": {
|
|
9248
9258
|
"block": {"type": "string", "enum": ["execution", "record"]},
|
|
9249
9259
|
"stageScope": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "integer", "minimum": 1}},
|
|
9260
|
+
"contentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "SHA-256 of subject+payload+block+stageScope for the current extract."},
|
|
9261
|
+
"verifiedContentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "contentHash at the last apply-verdicts. Matching hashes keep a pre-self-fix verdict."},
|
|
9250
9262
|
"id": {
|
|
9251
9263
|
"type": "string",
|
|
9252
9264
|
"minLength": 1
|
|
@@ -9140,6 +9140,12 @@
|
|
|
9140
9140
|
"properties": {
|
|
9141
9141
|
"setAside": {"type": "array", "items": {"type": "object", "required": ["id", "reason"], "additionalProperties": false, "properties": {"id": {"type": "string", "minLength": 1}, "reason": {"type": "string", "enum": ["record", "observed", "deferred"]}}}},
|
|
9142
9142
|
"stageLedger": {"type": "object", "additionalProperties": {"type": "string", "enum": ["done", "active", "ready", "blocked"]}},
|
|
9143
|
+
"dispatchQueue": {
|
|
9144
|
+
"type": "array",
|
|
9145
|
+
"description": "Plan-item ids sent to verifiers this round. In-scope plus plan-wide; deferred and observed stages are omitted.",
|
|
9146
|
+
"items": {"type": "string", "minLength": 1},
|
|
9147
|
+
"uniqueItems": true
|
|
9148
|
+
},
|
|
9143
9149
|
"uniformVerifiers": {
|
|
9144
9150
|
"type": "array",
|
|
9145
9151
|
"description": "Verifiers whose every vote this round was one verdict. Advisory: a unanimous round is legitimate, but the gate reads as a three-way cross-check unless this sits beside it.",
|
|
@@ -9167,6 +9173,10 @@
|
|
|
9167
9173
|
}
|
|
9168
9174
|
}
|
|
9169
9175
|
},
|
|
9176
|
+
"gating": {
|
|
9177
|
+
"type": "boolean",
|
|
9178
|
+
"description": "If false, plan-body verification is advisory: majority-disagree does not block approval, the self-fix loop does not run, and only one automatic round is allowed. Legal only when designPreparation.mode is no-design-inputs and the Stage Map has exactly one row. Prepare emits true; plan-items prepare/seed flip it after those facts exist."
|
|
9179
|
+
},
|
|
9170
9180
|
"roundCount": {
|
|
9171
9181
|
"type": "integer",
|
|
9172
9182
|
"minimum": 0
|
|
@@ -9242,6 +9252,8 @@
|
|
|
9242
9252
|
"properties": {
|
|
9243
9253
|
"block": {"type": "string", "enum": ["execution", "record"]},
|
|
9244
9254
|
"stageScope": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "integer", "minimum": 1}},
|
|
9255
|
+
"contentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "SHA-256 of subject+payload+block+stageScope for the current extract."},
|
|
9256
|
+
"verifiedContentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "contentHash at the last apply-verdicts. Matching hashes keep a pre-self-fix verdict."},
|
|
9245
9257
|
"id": {
|
|
9246
9258
|
"type": "string",
|
|
9247
9259
|
"minLength": 1
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: okstra-chat
|
|
3
|
+
description: Use when the user wants to create or join a global okstra chat room, send a message to everyone or to one participant, read unread arrivals, or reopen the inbox. Trigger words include "okstra chat", "okstra-chat", "chat room", "join the room", "send a chat message".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# okstra-chat
|
|
7
|
+
|
|
8
|
+
Cross-session rooms in the global okstra home. Not a project task artifact.
|
|
9
|
+
Do not write JSON. Call `okstra chat` and read its fixed text.
|
|
10
|
+
|
|
11
|
+
Rooms are independent of tasks and runs. A participant is this host session.
|
|
12
|
+
The display name is typed at join. Do not invent a default name.
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
- The user wants a room that a Claude lead and a Grok lead can both join.
|
|
17
|
+
- The user wants to send a message, see unread arrivals, or reopen the inbox.
|
|
18
|
+
|
|
19
|
+
## Step 0: CLI
|
|
20
|
+
|
|
21
|
+
Run as a separate Bash tool call with literal leading token:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
okstra chat --help
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
If `okstra` is not on PATH, tell the user:
|
|
28
|
+
|
|
29
|
+
`okstra not installed — run npx okstra@latest install once, then retry this skill.`
|
|
30
|
+
|
|
31
|
+
Do not use `npx` from this skill.
|
|
32
|
+
|
|
33
|
+
## Step 1: Create or join
|
|
34
|
+
|
|
35
|
+
Ask one question: create a room, or join an existing room.
|
|
36
|
+
|
|
37
|
+
If the host has a native picker, use it. Otherwise print a numbered list.
|
|
38
|
+
|
|
39
|
+
### Create
|
|
40
|
+
|
|
41
|
+
1. Ask for the room name as free input.
|
|
42
|
+
2. Run `okstra chat create --room <room>`.
|
|
43
|
+
3. Ask for the display name as free input. Do not suggest a generated name.
|
|
44
|
+
4. Run `okstra chat join --room <room> --name <display>`.
|
|
45
|
+
|
|
46
|
+
### Join
|
|
47
|
+
|
|
48
|
+
1. Run `okstra chat rooms`.
|
|
49
|
+
2. If the output is `no rooms`, say so and offer create.
|
|
50
|
+
3. Otherwise pick a room from that list (picker, or numbered list if the picker limit is exceeded).
|
|
51
|
+
4. Ask for the display name as free input.
|
|
52
|
+
5. Run `okstra chat join --room <room> --name <display>`.
|
|
53
|
+
|
|
54
|
+
`--name` is required. An empty name, `all`, or a name already in the room fails.
|
|
55
|
+
|
|
56
|
+
## Step 2: Unread
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
okstra chat unread --room <room> --as <display>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Show the rows. Each row is `id:time:recipient:body`.
|
|
63
|
+
|
|
64
|
+
## Step 3: Next action
|
|
65
|
+
|
|
66
|
+
Ask: send, inbox, log, ack, or done.
|
|
67
|
+
|
|
68
|
+
### Send
|
|
69
|
+
|
|
70
|
+
1. Run `okstra chat members --room <room>`.
|
|
71
|
+
2. Pick the recipient from `all` plus those names. Recipient is required.
|
|
72
|
+
3. Ask for the body as free input.
|
|
73
|
+
4. Run `okstra chat send --room <room> --as <display> --to <all|name> --body <text>`.
|
|
74
|
+
|
|
75
|
+
### Inbox
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
okstra chat inbox --room <room> --as <display>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This is every arrival to `@you` or `all`, including messages already acked.
|
|
82
|
+
|
|
83
|
+
### Log
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
okstra chat log --room <room> --as <display>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The whole room, including messages not addressed to you.
|
|
90
|
+
|
|
91
|
+
### Ack
|
|
92
|
+
|
|
93
|
+
After unread, mark the last id the user has read:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
okstra chat ack --room <room> --as <display> --through <id>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Rules
|
|
100
|
+
|
|
101
|
+
- Call only `okstra chat`. Do not open files under the chat store.
|
|
102
|
+
- Do not treat chat rows as evidence for a finding, verdict, or assignment.
|
|
103
|
+
- Workers may run the same commands with `--name` and `--as`. Joining is optional.
|
|
104
|
+
- Do not generate a display name from the provider, model, or execution label.
|
|
@@ -95,12 +95,13 @@ It has already promoted legacy values.
|
|
|
95
95
|
The status response always includes one of:
|
|
96
96
|
|
|
97
97
|
1. **Resume current run** — if `latestResumeCommandPath` exists, display that path.
|
|
98
|
-
2. **
|
|
99
|
-
|
|
98
|
+
2. **Ask the user to approve** — if `workflow.awaitingApproval` is true. Tell the user to approve the plan (`okstra-run` with `--task-type implementation`, which asks `approve_plan_confirm`, or `--approve`). Quote `nextRecommendedPhase.rationale`. A `ready` pointer to `implementation` here means implementation is next after approval, not that it may launch as if already approved. Do not re-run `implementation-planning`.
|
|
99
|
+
3. **Restart current phase** — only when `awaitingApproval` is false and the pointer is not `ready`. The task can be re-run with the same `task-key` and current `taskType`.
|
|
100
|
+
Branches 4–6 are decided by `workflow.nextRecommendedPhase.status` when `awaitingApproval` is false — one status, one branch:
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
4. **Start next phase** — `status` is `ready` and `awaitingApproval` is false. Propose `nextRecommendedPhase.phase` as the next run's `--task-type` and quote its `rationale` as the reason. This is the only status under which a named phase may be launched without a prior approval ask, so it is the only branch that proposes a run. A `ready` pointer to `release-handoff` already implies an `accepted` final-verification verdict (the report validator refuses that routing target otherwise), so do not re-gate it here.
|
|
103
|
+
5. **Need more information** — `status` is `pending` (the last run did not settle where this task goes next) or `blocked` (it did settle, and the answer is that something outside the run has to change first). Neither proposes a run, and a leftover `phase` name does not change that — `prepare` keeps the name when it lowers a pointer to `pending`, so read `status`, not the emptiness of `phase`. Show the `rationale`, and for `blocked` state what it names as the obstacle. After `implementation-planning`, the first action is `okstra-user-response` on the named `C-NNN` ids; do not re-run planning until those answers exist, and do not start implementation.
|
|
104
|
+
6. **Task complete (terminal)** — `status` is `terminal`: the task lifecycle ends here. This is **not** a "next phase" — do not propose a new okstra run. Surface the latest report and ask the user whether any follow-up task should be opened separately.
|
|
104
105
|
|
|
105
106
|
### status.4 — Update workStatus (write)
|
|
106
107
|
|