okstra 0.122.0 → 0.123.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 +4 -2
- package/docs/architecture/storage-model.md +14 -0
- package/docs/architecture.md +34 -6
- package/docs/cli.md +45 -5
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-rollup.md +1 -1
- package/docs/for-ai/skills/{okstra-schedule.md → okstra-schedule-gen.md} +3 -3
- package/docs/project-structure-overview.md +3 -2
- package/docs/task-process/implementation-planning.md +33 -9
- package/docs/task-process/implementation.md +21 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/usage.sh +3 -3
- package/runtime/prompts/launch.template.md +5 -2
- package/runtime/prompts/lead/convergence.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
- package/runtime/prompts/lead/plan-body-verification.md +29 -0
- package/runtime/prompts/lead/report-writer.md +9 -3
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/_implementation-deliverable.md +3 -1
- package/runtime/prompts/profiles/_implementation-executor.md +5 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +2 -0
- package/runtime/prompts/profiles/implementation-planning.md +11 -1
- package/runtime/prompts/wizard/prompts.ko.json +44 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +23 -1
- package/runtime/python/okstra_ctl/design_prep.py +1462 -0
- package/runtime/python/okstra_ctl/design_surfaces.py +243 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +33 -1
- package/runtime/python/okstra_ctl/implementation_stage.py +35 -0
- package/runtime/python/okstra_ctl/incremental_carry.py +294 -21
- package/runtime/python/okstra_ctl/incremental_scope.py +51 -5
- package/runtime/python/okstra_ctl/material.py +1 -1
- package/runtime/python/okstra_ctl/model_discovery.py +98 -0
- package/runtime/python/okstra_ctl/models.py +8 -3
- package/runtime/python/okstra_ctl/render.py +5 -0
- package/runtime/python/okstra_ctl/run.py +53 -5
- package/runtime/python/okstra_ctl/user_response.py +67 -2
- package/runtime/python/okstra_ctl/wizard.py +283 -3
- package/runtime/python/okstra_token_usage/report.py +11 -0
- package/runtime/schemas/final-report-v1.0.schema.json +336 -0
- package/runtime/skills/okstra-inspect/SKILL.md +2 -2
- package/runtime/skills/okstra-rollup/SKILL.md +1 -1
- package/runtime/skills/{okstra-schedule → okstra-schedule-gen}/SKILL.md +2 -2
- package/runtime/skills/okstra-user-response/SKILL.md +8 -6
- package/runtime/templates/reports/final-report.template.md +67 -0
- package/runtime/templates/reports/i18n/en.json +31 -0
- package/runtime/templates/reports/i18n/ko.json +31 -0
- package/runtime/validators/validate-run.py +426 -5
- package/runtime/validators/validate-schedule.py +4 -4
- package/src/cli-registry.mjs +7 -0
- package/src/commands/inspect/design-prep.mjs +23 -0
- package/src/lib/skill-catalog.mjs +2 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import re
|
|
5
|
+
import unicodedata
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Mapping
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DesignSurfaceError(ValueError):
|
|
11
|
+
"""The planning structure cannot be mapped to deterministic design surfaces."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class SurfaceRule:
|
|
16
|
+
kind: str
|
|
17
|
+
path_tokens: tuple[str, ...]
|
|
18
|
+
path_suffixes: tuple[str, ...]
|
|
19
|
+
path_patterns: tuple[str, ...]
|
|
20
|
+
action_phrases: tuple[str, ...]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class TriggerEvidence:
|
|
25
|
+
step: int | None
|
|
26
|
+
field: str
|
|
27
|
+
match: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class DesignSurfaceTrigger:
|
|
32
|
+
stage: int
|
|
33
|
+
kind: str
|
|
34
|
+
evidence: tuple[TriggerEvidence, ...]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
RULES = (
|
|
38
|
+
SurfaceRule(
|
|
39
|
+
"domain-contract",
|
|
40
|
+
("/domain/",),
|
|
41
|
+
(),
|
|
42
|
+
(),
|
|
43
|
+
("entity", "value object", "aggregate", "domain error"),
|
|
44
|
+
),
|
|
45
|
+
SurfaceRule(
|
|
46
|
+
"persistence-schema",
|
|
47
|
+
("/migrations/", "/orm/", "/repositories/"),
|
|
48
|
+
(".sql",),
|
|
49
|
+
(),
|
|
50
|
+
(
|
|
51
|
+
"create table",
|
|
52
|
+
"alter table",
|
|
53
|
+
"column",
|
|
54
|
+
"index",
|
|
55
|
+
"foreign key",
|
|
56
|
+
"unique constraint",
|
|
57
|
+
),
|
|
58
|
+
),
|
|
59
|
+
SurfaceRule(
|
|
60
|
+
"external-interface",
|
|
61
|
+
("/ports/", "/adapters/", "/clients/", "/gateways/", "/infrastructure/http/"),
|
|
62
|
+
(),
|
|
63
|
+
(),
|
|
64
|
+
("port", "adapter", "client", "gateway", "external api"),
|
|
65
|
+
),
|
|
66
|
+
SurfaceRule(
|
|
67
|
+
"transaction-consistency",
|
|
68
|
+
(),
|
|
69
|
+
(),
|
|
70
|
+
(),
|
|
71
|
+
("transaction", "outbox", "unit of work", "atomic write"),
|
|
72
|
+
),
|
|
73
|
+
SurfaceRule(
|
|
74
|
+
"transformation-mapping",
|
|
75
|
+
("/mappers/", "/parsers/", "/normalizers/"),
|
|
76
|
+
(),
|
|
77
|
+
(),
|
|
78
|
+
("parse", "map", "canonical", "normalize", "transform"),
|
|
79
|
+
),
|
|
80
|
+
SurfaceRule(
|
|
81
|
+
"lifecycle-state-machine",
|
|
82
|
+
(),
|
|
83
|
+
(),
|
|
84
|
+
(),
|
|
85
|
+
(
|
|
86
|
+
"claim",
|
|
87
|
+
"lease",
|
|
88
|
+
"retry",
|
|
89
|
+
"dead",
|
|
90
|
+
"dead-letter",
|
|
91
|
+
"state transition",
|
|
92
|
+
"state machine",
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
SurfaceRule(
|
|
96
|
+
"rollout-observability",
|
|
97
|
+
("/monitoring/", "/deploy/", "/helm/"),
|
|
98
|
+
("/values.yaml",),
|
|
99
|
+
("*/values-*.yaml",),
|
|
100
|
+
("rollout", "liveness", "alert", "feature flag"),
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _normalise(value: object) -> str:
|
|
106
|
+
return unicodedata.normalize("NFC", str(value or "")).replace("\\", "/").lower()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _phrase_matches(text: str, phrase: str) -> bool:
|
|
110
|
+
return re.search(rf"(?<![\w-]){re.escape(phrase)}(?![\w-])", text) is not None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _selected_candidate(planning: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
114
|
+
recommended = _normalise((planning.get("recommendedOption") or {}).get("name"))
|
|
115
|
+
candidates = [
|
|
116
|
+
row
|
|
117
|
+
for row in planning.get("optionCandidates") or []
|
|
118
|
+
if isinstance(row, Mapping)
|
|
119
|
+
]
|
|
120
|
+
exact = [row for row in candidates if _normalise(row.get("name")) == recommended]
|
|
121
|
+
if len(exact) > 1:
|
|
122
|
+
raise DesignSurfaceError(
|
|
123
|
+
f"recommended option {recommended!r} is missing or ambiguous"
|
|
124
|
+
)
|
|
125
|
+
if len(exact) == 1:
|
|
126
|
+
return exact[0]
|
|
127
|
+
prefixed = [
|
|
128
|
+
row
|
|
129
|
+
for row in candidates
|
|
130
|
+
if _normalise(row.get("name")).startswith(
|
|
131
|
+
(recommended + ":", recommended + " —")
|
|
132
|
+
)
|
|
133
|
+
]
|
|
134
|
+
if len(prefixed) != 1:
|
|
135
|
+
raise DesignSurfaceError(
|
|
136
|
+
f"recommended option {recommended!r} is missing or ambiguous"
|
|
137
|
+
)
|
|
138
|
+
return prefixed[0]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def expected_prep_plan_item_id(stage: int, kind: str) -> str:
|
|
142
|
+
return f"P-Prep-S{stage}-{kind}"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _path_matches(rule: SurfaceRule, raw_path: object) -> bool:
|
|
146
|
+
path = "/" + _normalise(raw_path).lstrip("/")
|
|
147
|
+
return (
|
|
148
|
+
any(token in path for token in rule.path_tokens)
|
|
149
|
+
or any(path.endswith(suffix) for suffix in rule.path_suffixes)
|
|
150
|
+
or any(fnmatch.fnmatch(path, pattern) for pattern in rule.path_patterns)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _matching_rule_evidence(
|
|
155
|
+
*,
|
|
156
|
+
step: int | None,
|
|
157
|
+
field: str,
|
|
158
|
+
value: object,
|
|
159
|
+
) -> list[tuple[str, TriggerEvidence]]:
|
|
160
|
+
text = _normalise(value)
|
|
161
|
+
matches = []
|
|
162
|
+
for rule in RULES:
|
|
163
|
+
path_hit = field == "files" and _path_matches(rule, text)
|
|
164
|
+
action_hit = field == "action" and any(
|
|
165
|
+
_phrase_matches(text, phrase) for phrase in rule.action_phrases
|
|
166
|
+
)
|
|
167
|
+
if path_hit or action_hit:
|
|
168
|
+
matches.append((rule.kind, TriggerEvidence(step, field, text)))
|
|
169
|
+
return matches
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _stage_rows(planning: Mapping[str, Any]) -> dict[int, Mapping[str, Any]]:
|
|
173
|
+
rows = {
|
|
174
|
+
int(stage["stage"]): stage
|
|
175
|
+
for stage in planning.get("stages") or []
|
|
176
|
+
if isinstance(stage, Mapping) and isinstance(stage.get("stage"), int)
|
|
177
|
+
}
|
|
178
|
+
if not rows:
|
|
179
|
+
raise DesignSurfaceError("implementationPlanning.stages is empty")
|
|
180
|
+
return rows
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _stage_for_selected_path(
|
|
184
|
+
path: str,
|
|
185
|
+
stages: Mapping[int, Mapping[str, Any]],
|
|
186
|
+
) -> int:
|
|
187
|
+
normalised = _normalise(path)
|
|
188
|
+
matched = []
|
|
189
|
+
for stage_number, stage in stages.items():
|
|
190
|
+
files = [
|
|
191
|
+
_normalise(step.get("files"))
|
|
192
|
+
for step in stage.get("stepwiseExecution") or []
|
|
193
|
+
if isinstance(step, Mapping)
|
|
194
|
+
]
|
|
195
|
+
path_pattern = rf"(?<![\w./@-]){re.escape(normalised)}(?![\w./@-])"
|
|
196
|
+
if any(re.search(path_pattern, cell) is not None for cell in files):
|
|
197
|
+
matched.append(stage_number)
|
|
198
|
+
if len(matched) != 1:
|
|
199
|
+
raise DesignSurfaceError(
|
|
200
|
+
f"selected option path {path!r} is not mapped to a stage exactly once"
|
|
201
|
+
)
|
|
202
|
+
return matched[0]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def detect_design_surfaces(
|
|
206
|
+
planning: Mapping[str, Any],
|
|
207
|
+
) -> list[DesignSurfaceTrigger]:
|
|
208
|
+
stages = _stage_rows(planning)
|
|
209
|
+
grouped: dict[tuple[int, str], list[TriggerEvidence]] = {}
|
|
210
|
+
for stage_number, stage in stages.items():
|
|
211
|
+
for step in stage.get("stepwiseExecution") or []:
|
|
212
|
+
if not isinstance(step, Mapping):
|
|
213
|
+
continue
|
|
214
|
+
step_number = (
|
|
215
|
+
int(step.get("step")) if isinstance(step.get("step"), int) else None
|
|
216
|
+
)
|
|
217
|
+
for field in ("files", "action"):
|
|
218
|
+
for kind, evidence in _matching_rule_evidence(
|
|
219
|
+
step=step_number,
|
|
220
|
+
field=field,
|
|
221
|
+
value=step.get(field),
|
|
222
|
+
):
|
|
223
|
+
grouped.setdefault((stage_number, kind), []).append(evidence)
|
|
224
|
+
selected = _selected_candidate(planning)
|
|
225
|
+
for row in selected.get("fileStructure") or []:
|
|
226
|
+
if not isinstance(row, Mapping):
|
|
227
|
+
continue
|
|
228
|
+
path = str(row.get("path") or "")
|
|
229
|
+
stage_number = _stage_for_selected_path(path, stages)
|
|
230
|
+
for kind, evidence in _matching_rule_evidence(
|
|
231
|
+
step=None,
|
|
232
|
+
field="files",
|
|
233
|
+
value=path,
|
|
234
|
+
):
|
|
235
|
+
grouped.setdefault((stage_number, kind), []).append(evidence)
|
|
236
|
+
rule_order = {rule.kind: index for index, rule in enumerate(RULES)}
|
|
237
|
+
return [
|
|
238
|
+
DesignSurfaceTrigger(stage, kind, tuple(grouped[(stage, kind)]))
|
|
239
|
+
for stage, kind in sorted(
|
|
240
|
+
grouped,
|
|
241
|
+
key=lambda key: (key[0], rule_order[key[1]]),
|
|
242
|
+
)
|
|
243
|
+
]
|
|
@@ -4,7 +4,7 @@ This is a deliberately narrow JSON Schema implementation that supports
|
|
|
4
4
|
exactly the keywords used by ``schemas/final-report-v1.0.schema.json``:
|
|
5
5
|
|
|
6
6
|
type, required, properties, additionalProperties, enum, const, pattern,
|
|
7
|
-
minLength, minItems, maxItems, items, minimum, maximum, $ref ($defs),
|
|
7
|
+
minLength, minItems, maxItems, uniqueItems, items, minimum, maximum, $ref ($defs),
|
|
8
8
|
oneOf, allOf, if/then/else, contains, not
|
|
9
9
|
|
|
10
10
|
We do NOT depend on the ``jsonschema`` PyPI package because its
|
|
@@ -62,6 +62,32 @@ def _check_type(value: Any, expected: str) -> bool:
|
|
|
62
62
|
return isinstance(value, _TYPE_PYTHON.get(expected, ()))
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
def _json_schema_equal(left: Any, right: Any) -> bool:
|
|
66
|
+
if isinstance(left, bool) or isinstance(right, bool):
|
|
67
|
+
return isinstance(left, bool) and isinstance(right, bool) and left == right
|
|
68
|
+
if isinstance(left, (int, float)) or isinstance(right, (int, float)):
|
|
69
|
+
return (
|
|
70
|
+
isinstance(left, (int, float))
|
|
71
|
+
and isinstance(right, (int, float))
|
|
72
|
+
and left == right
|
|
73
|
+
)
|
|
74
|
+
if isinstance(left, list) or isinstance(right, list):
|
|
75
|
+
return (
|
|
76
|
+
isinstance(left, list)
|
|
77
|
+
and isinstance(right, list)
|
|
78
|
+
and len(left) == len(right)
|
|
79
|
+
and all(_json_schema_equal(a, b) for a, b in zip(left, right))
|
|
80
|
+
)
|
|
81
|
+
if isinstance(left, dict) or isinstance(right, dict):
|
|
82
|
+
return (
|
|
83
|
+
isinstance(left, dict)
|
|
84
|
+
and isinstance(right, dict)
|
|
85
|
+
and left.keys() == right.keys()
|
|
86
|
+
and all(_json_schema_equal(left[key], right[key]) for key in left)
|
|
87
|
+
)
|
|
88
|
+
return type(left) is type(right) and left == right
|
|
89
|
+
|
|
90
|
+
|
|
65
91
|
def _resolve_ref(ref: str, root: dict) -> dict:
|
|
66
92
|
"""Resolve a ``#/$defs/Name`` style reference against ``root``."""
|
|
67
93
|
if not ref.startswith("#/"):
|
|
@@ -165,6 +191,12 @@ class _Validator:
|
|
|
165
191
|
max_items = schema.get("maxItems")
|
|
166
192
|
if max_items is not None and len(instance) > max_items:
|
|
167
193
|
self._err(path, f"array length {len(instance)} > maxItems {max_items}")
|
|
194
|
+
if schema.get("uniqueItems") is True and any(
|
|
195
|
+
_json_schema_equal(item, previous)
|
|
196
|
+
for index, item in enumerate(instance)
|
|
197
|
+
for previous in instance[:index]
|
|
198
|
+
):
|
|
199
|
+
self._err(path, "array items are not unique")
|
|
168
200
|
items = schema.get("items")
|
|
169
201
|
if items is not None:
|
|
170
202
|
for i, value in enumerate(instance):
|
|
@@ -14,6 +14,8 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
16
|
from . import stage_targets
|
|
17
|
+
from .design_prep import DesignPrepDecision, resolve_design_prep
|
|
18
|
+
from .final_report_paths import final_report_data_path
|
|
17
19
|
from .stage_reconcile import auto_reconcile_best_effort
|
|
18
20
|
|
|
19
21
|
|
|
@@ -32,6 +34,7 @@ class StageRunClaim:
|
|
|
32
34
|
worktree_status: str
|
|
33
35
|
worktree_note: str
|
|
34
36
|
started_head_commit: str
|
|
37
|
+
design_prep_decision: DesignPrepDecision
|
|
35
38
|
concurrent_stages: list[int] = field(default_factory=list)
|
|
36
39
|
|
|
37
40
|
|
|
@@ -50,6 +53,20 @@ def _as_implementation_stage_error(exc: Exception) -> ImplementationStageError:
|
|
|
50
53
|
return ImplementationStageError(str(exc))
|
|
51
54
|
|
|
52
55
|
|
|
56
|
+
def _resolve_stage_design_prep(plan_path: Path, stage: int) -> DesignPrepDecision:
|
|
57
|
+
plan_data_path = final_report_data_path(plan_path)
|
|
58
|
+
if plan_data_path.is_file():
|
|
59
|
+
return resolve_design_prep(plan_data_path, stage)
|
|
60
|
+
return DesignPrepDecision(
|
|
61
|
+
outcome="proceed",
|
|
62
|
+
effective_items=(),
|
|
63
|
+
assumptions_to_inject=(),
|
|
64
|
+
warnings=("legacy-unassessed",),
|
|
65
|
+
request_paths=(),
|
|
66
|
+
reason="legacy plan has no design preparation assessment",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
53
70
|
def claim_implementation_stage_run(
|
|
54
71
|
inp: Any,
|
|
55
72
|
ctx_stage_map: list[dict[str, Any]],
|
|
@@ -87,6 +104,21 @@ def claim_implementation_stage_run(
|
|
|
87
104
|
except stage_targets.StageTargetError as exc:
|
|
88
105
|
raise _as_implementation_stage_error(exc) from exc
|
|
89
106
|
|
|
107
|
+
design_prep_decision = _resolve_stage_design_prep(
|
|
108
|
+
Path(inp.approved_plan_path), selected
|
|
109
|
+
)
|
|
110
|
+
if design_prep_decision.outcome == "wait_for_input":
|
|
111
|
+
requests = ", ".join(design_prep_decision.request_paths) or "no request path"
|
|
112
|
+
raise ImplementationStageError(
|
|
113
|
+
f"stage {selected} waits for design input: "
|
|
114
|
+
f"{design_prep_decision.reason}; {requests}"
|
|
115
|
+
)
|
|
116
|
+
if design_prep_decision.outcome == "replan":
|
|
117
|
+
raise ImplementationStageError(
|
|
118
|
+
f"stage {selected} requires implementation-planning rerun: "
|
|
119
|
+
f"{design_prep_decision.reason}"
|
|
120
|
+
)
|
|
121
|
+
|
|
90
122
|
concurrent_stages = snapshot.concurrent_stage_numbers(
|
|
91
123
|
selected_stage=selected,
|
|
92
124
|
)
|
|
@@ -101,6 +133,7 @@ def claim_implementation_stage_run(
|
|
|
101
133
|
worktree_status=executor_worktree_status,
|
|
102
134
|
worktree_note="",
|
|
103
135
|
started_head_commit=head,
|
|
136
|
+
design_prep_decision=design_prep_decision,
|
|
104
137
|
concurrent_stages=concurrent_stages,
|
|
105
138
|
)
|
|
106
139
|
_record_stage_run_claim_started(task_key, plan_run_root, claim)
|
|
@@ -174,6 +207,7 @@ def claim_implementation_stage_run(
|
|
|
174
207
|
worktree_status=prov.status,
|
|
175
208
|
worktree_note=prov.note,
|
|
176
209
|
started_head_commit=prov.base_ref,
|
|
210
|
+
design_prep_decision=design_prep_decision,
|
|
177
211
|
concurrent_stages=concurrent_stages,
|
|
178
212
|
)
|
|
179
213
|
_record_stage_run_claim_started(task_key, plan_run_root, claim)
|
|
@@ -216,6 +250,7 @@ def publish_stage_run_claim(
|
|
|
216
250
|
"Do NOT recompute from `consumers.jsonl`; the runtime already selected "
|
|
217
251
|
"and reserved this stage."
|
|
218
252
|
)
|
|
253
|
+
ctx["DESIGN_PREP_CONTEXT"] = claim.design_prep_decision.as_prompt_markdown()
|
|
219
254
|
inp.stage = csv
|
|
220
255
|
print(f"selected stages: {csv}", file=sys.stdout)
|
|
221
256
|
|