okstra 0.207.1 → 0.209.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 +3 -2
- package/dist/cli-registry.mjs +6 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.mjs +1 -1
- package/dist/commands/lifecycle/doctor.mjs +1 -1
- package/dist/lib/skill-catalog.mjs +1 -0
- package/dist/lib/skill-catalog.mjs.map +1 -1
- package/docs/architecture/storage-model.md +14 -0
- package/docs/architecture.md +30 -9
- package/docs/cli.md +26 -22
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/project-structure-overview.md +15 -8
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/operations/explain-flow.json +6 -0
- package/runtime/bin/lib/okstra/cli.sh +1 -5
- package/runtime/bin/lib/okstra/globals.sh +0 -2
- package/runtime/bin/lib/okstra/usage.sh +5 -3
- package/runtime/bin/okstra.sh +0 -2
- package/runtime/prompts/duties/business-flow-investigator.json +14 -0
- package/runtime/prompts/lead/context-loader.md +1 -1
- package/runtime/prompts/lead/convergence.md +22 -7
- package/runtime/prompts/lead/okstra-lead-contract.md +10 -6
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/lead/team-contract.md +12 -17
- package/runtime/prompts/profiles/_common-contract.md +3 -3
- package/runtime/prompts/wizard/prompts.ko.json +0 -91
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +6 -0
- package/runtime/python/okstra_ctl/agent/invocation.py +1 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/batch.py +1 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +1 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +11 -125
- package/runtime/python/okstra_ctl/agent/standalone.py +183 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +39 -8
- package/runtime/python/okstra_ctl/assignment_resolver.py +7 -1
- package/runtime/python/okstra_ctl/brief_frontmatter.py +10 -0
- package/runtime/python/okstra_ctl/business_flow/__init__.py +4 -0
- package/runtime/python/okstra_ctl/business_flow/cli.py +134 -0
- package/runtime/python/okstra_ctl/business_flow/contracts.py +268 -0
- package/runtime/python/okstra_ctl/business_flow/engine.py +518 -0
- package/runtime/python/okstra_ctl/business_flow/hooks.py +221 -0
- package/runtime/python/okstra_ctl/business_flow/invocation.py +170 -0
- package/runtime/python/okstra_ctl/business_flow/report.py +49 -0
- package/runtime/python/okstra_ctl/business_flow/source.py +206 -0
- package/runtime/python/okstra_ctl/business_flow/store.py +388 -0
- package/runtime/python/okstra_ctl/cmux.py +21 -16
- package/runtime/python/okstra_ctl/convergence.py +173 -2
- package/runtime/python/okstra_ctl/convergence_critic_verify_prompt.py +18 -0
- package/runtime/python/okstra_ctl/convergence_provenance.py +8 -0
- package/runtime/python/okstra_ctl/coverage_census.py +603 -0
- package/runtime/python/okstra_ctl/design_surfaces.py +4 -0
- package/runtime/python/okstra_ctl/direct_work.py +1 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +7 -3
- package/runtime/python/okstra_ctl/doctor.py +12 -6
- package/runtime/python/okstra_ctl/domain/role.py +1 -0
- package/runtime/python/okstra_ctl/group_context.py +5 -4
- package/runtime/python/okstra_ctl/legacy_model_selection.py +7 -51
- package/runtime/python/okstra_ctl/manager_split.py +4 -1
- package/runtime/python/okstra_ctl/model_io/lines.py +1 -24
- package/runtime/python/okstra_ctl/model_io/renderers.py +54 -41
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/profile.md +0 -8
- package/runtime/python/okstra_ctl/phases/error_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/error_analysis/profile.md +6 -8
- package/runtime/python/okstra_ctl/phases/feature_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/feature_analysis/profile.md +0 -8
- package/runtime/python/okstra_ctl/phases/final_verification/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/final_verification/profile.md +2 -8
- package/runtime/python/okstra_ctl/phases/implementation/boundary.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/profile.md +0 -6
- package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation-input.template.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +4 -3
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +1 -14
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.md +5 -8
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/spec.md +3 -3
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/validation.py +15 -5
- package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +9 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/boundary.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +4 -2
- package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +25 -9
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +7 -10
- package/runtime/python/okstra_ctl/phases/improvement_discovery/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/improvement_discovery/profile.md +4 -11
- package/runtime/python/okstra_ctl/phases/project_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/project_analysis/profile.md +0 -8
- package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +1 -1
- package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +1 -1
- package/runtime/python/okstra_ctl/phases/requirements_discovery/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/requirements_discovery/profile.md +14 -8
- package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +3 -3
- package/runtime/python/okstra_ctl/phases/technical_verification/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/technical_verification/profile.md +0 -4
- package/runtime/python/okstra_ctl/plan_items.py +1 -1
- package/runtime/python/okstra_ctl/render.py +10 -43
- package/runtime/python/okstra_ctl/render_final_report.py +3 -0
- package/runtime/python/okstra_ctl/report_assembly.py +15 -1
- package/runtime/python/okstra_ctl/report_finalize.py +50 -0
- package/runtime/python/okstra_ctl/report_html/render.py +3 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +1 -2
- package/runtime/python/okstra_ctl/run.py +78 -409
- package/runtime/python/okstra_ctl/wizard/__init__.py +2 -24
- package/runtime/python/okstra_ctl/wizard/cli.py +3 -6
- package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -35
- package/runtime/python/okstra_ctl/wizard/engine.py +2 -4
- package/runtime/python/okstra_ctl/wizard/ids.py +1 -88
- package/runtime/python/okstra_ctl/wizard/registry.py +36 -228
- package/runtime/python/okstra_ctl/wizard/render.py +2 -2
- package/runtime/python/okstra_ctl/wizard/roles.py +1 -3
- package/runtime/python/okstra_ctl/wizard/sources.py +9 -40
- package/runtime/python/okstra_ctl/wizard/state.py +36 -145
- package/runtime/python/okstra_ctl/wizard/statefile.py +27 -128
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +22 -10
- package/runtime/python/okstra_ctl/wizard/steps_options.py +5 -4
- package/runtime/python/okstra_ctl/wizard/steps_roles.py +15 -565
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +9 -2
- package/runtime/schemas/business-flow-v1.schema.json +847 -0
- package/runtime/schemas/convergence-groups-v2.0.schema.json +7 -0
- package/runtime/skills/okstra-explain-flow/SKILL.md +42 -0
- package/runtime/skills/okstra-inspect/facets/history.md +5 -5
- package/runtime/skills/okstra-run/SKILL.md +2 -2
- package/runtime/templates/reports/business-flow.template.md +106 -0
- package/runtime/templates/reports/html/base.template.html +14 -1
- package/runtime/templates/reports/html/business-flow.template.html +31 -0
- package/runtime/templates/reports/html/i18n/en.json +1 -0
- package/runtime/templates/reports/html/i18n/ko.json +1 -0
- package/runtime/templates/worker-prompt-preamble.md +11 -2
- package/runtime/validators/checks/validate-prompt-metadata-01.py +10 -10
- package/runtime/validators/validate-run.py +70 -21
- package/runtime/validators/validate_analysis_report.py +21 -21
- package/runtime/python/okstra_ctl/workers.py +0 -133
|
@@ -39,7 +39,7 @@ from typing import Any, Mapping
|
|
|
39
39
|
from okstra_project import list_project_tasks
|
|
40
40
|
from okstra_project.phase_pointer import STATUS_TERMINAL
|
|
41
41
|
|
|
42
|
-
from .brief_frontmatter import read_brief_frontmatter
|
|
42
|
+
from .brief_frontmatter import brief_task_id, read_brief_frontmatter
|
|
43
43
|
from .ids import slugify_task_segment
|
|
44
44
|
from .paths import find_asset_root
|
|
45
45
|
from .run_context import dir_flock
|
|
@@ -387,7 +387,7 @@ _QUEUE_LINE_RE = re.compile(r"^\d+\. \[(?P<status>[^\]]*)\] (?P<task>\S+)")
|
|
|
387
387
|
|
|
388
388
|
@dataclass(frozen=True)
|
|
389
389
|
class QueueRow:
|
|
390
|
-
task_id: str # slugified brief-id — the task directory name
|
|
390
|
+
task_id: str # slugified task-id (else brief-id) — the task directory name
|
|
391
391
|
brief_id: str
|
|
392
392
|
ticket_id: str
|
|
393
393
|
brief: str # project-relative brief path
|
|
@@ -420,6 +420,7 @@ def group_briefs(project_root: Path, task_group: str) -> list[dict[str, Any]]:
|
|
|
420
420
|
brief_id = frontmatter.get("brief-id") or path.stem
|
|
421
421
|
match = _ORDINAL_RE.match(brief_id)
|
|
422
422
|
briefs.append({
|
|
423
|
+
"task_id": slugify_task_segment(brief_task_id(frontmatter) or brief_id),
|
|
423
424
|
"brief_id": brief_id,
|
|
424
425
|
"ticket_id": frontmatter.get("ticket-id") or ticket_id_from_brief_id(brief_id),
|
|
425
426
|
"ordinal": int(match.group("ordinal")) if match else None,
|
|
@@ -509,7 +510,7 @@ def group_queue(
|
|
|
509
510
|
recorded = catalog_progress(project_root, task_group)
|
|
510
511
|
|
|
511
512
|
def _status(brief: dict[str, Any]) -> tuple[str, str]:
|
|
512
|
-
task_id =
|
|
513
|
+
task_id = brief["task_id"]
|
|
513
514
|
if task_id in recorded:
|
|
514
515
|
return recorded[task_id]
|
|
515
516
|
entry = by_task.get(task_id)
|
|
@@ -527,7 +528,7 @@ def group_queue(
|
|
|
527
528
|
if status_by_ticket[ticket][0] != QUEUE_DONE
|
|
528
529
|
))
|
|
529
530
|
rows.append(QueueRow(
|
|
530
|
-
|
|
531
|
+
brief["task_id"], brief["brief_id"], brief["ticket_id"],
|
|
531
532
|
brief["brief"], status, progress, open_waits,
|
|
532
533
|
duplicates.get(brief["brief_id"], ()),
|
|
533
534
|
))
|
|
@@ -58,7 +58,6 @@ def normalize_model_selection_inputs(
|
|
|
58
58
|
profile: RoleProfile,
|
|
59
59
|
role_counts_raw: Sequence[str],
|
|
60
60
|
role_models_raw: Sequence[str],
|
|
61
|
-
workers: str,
|
|
62
61
|
worker_model: str,
|
|
63
62
|
critic: str,
|
|
64
63
|
executor: str,
|
|
@@ -73,7 +72,6 @@ def normalize_model_selection_inputs(
|
|
|
73
72
|
models = _parse_role_models(role_models_raw)
|
|
74
73
|
legacy = _legacy_selection(
|
|
75
74
|
profile=profile,
|
|
76
|
-
workers=workers,
|
|
77
75
|
worker_model=worker_model,
|
|
78
76
|
critic=critic,
|
|
79
77
|
executor=executor,
|
|
@@ -141,7 +139,6 @@ def _split_role_value(raw: str, option: str) -> tuple[str, str]:
|
|
|
141
139
|
def _legacy_selection(
|
|
142
140
|
*,
|
|
143
141
|
profile: RoleProfile,
|
|
144
|
-
workers: str,
|
|
145
142
|
worker_model: str,
|
|
146
143
|
critic: str,
|
|
147
144
|
executor: str,
|
|
@@ -155,27 +152,12 @@ def _legacy_selection(
|
|
|
155
152
|
constraints: dict[str, tuple[str, ...]] = {}
|
|
156
153
|
values: dict[str, dict[str, str]] = {}
|
|
157
154
|
sources: list[str] = []
|
|
158
|
-
needs_initial = bool(
|
|
159
|
-
workers.strip()
|
|
160
|
-
or worker_model.strip()
|
|
161
|
-
or any(model.strip() for model in provider_models.values())
|
|
162
|
-
)
|
|
163
|
-
initial = (
|
|
164
|
-
_initial_cross_verification_requirement(profile)
|
|
165
|
-
if needs_initial
|
|
166
|
-
else None
|
|
167
|
-
)
|
|
168
|
-
if workers.strip() and initial is not None:
|
|
169
|
-
providers = _legacy_worker_providers(workers)
|
|
170
|
-
_validate_legacy_workers(initial, providers)
|
|
171
|
-
counts[initial.role] = len(providers)
|
|
172
|
-
constraints[initial.role] = providers
|
|
173
|
-
report_writer = _requirement(profile, "report-writer")
|
|
174
|
-
if report_writer is not None:
|
|
175
|
-
counts["report-writer"] = report_writer.recommended_count
|
|
176
|
-
sources.append("--workers")
|
|
177
155
|
worker_values = _parse_worker_models(worker_model, provider_models)
|
|
178
|
-
if worker_values
|
|
156
|
+
if worker_values:
|
|
157
|
+
# A provider model flag fills one slot of the initial cross-verification
|
|
158
|
+
# role per named provider; the remaining slots take the model defaults.
|
|
159
|
+
initial = _initial_cross_verification_requirement(profile)
|
|
160
|
+
constraints[initial.role] = tuple(worker_values)
|
|
179
161
|
values[initial.role] = worker_values
|
|
180
162
|
sources.append("worker-model")
|
|
181
163
|
_add_provider_selection(
|
|
@@ -201,30 +183,8 @@ def _initial_cross_verification_requirement(profile: RoleProfile) -> RoleRequire
|
|
|
201
183
|
}:
|
|
202
184
|
return requirement
|
|
203
185
|
raise ModelSelectionInputError(
|
|
204
|
-
"profile has no initial cross-verification role for
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def _legacy_worker_providers(workers: str) -> tuple[str, ...]:
|
|
209
|
-
tokens = tuple(
|
|
210
|
-
token.strip().lower() for token in workers.split(",") if token.strip()
|
|
186
|
+
"profile has no initial cross-verification role for a provider model flag"
|
|
211
187
|
)
|
|
212
|
-
return tuple(token for token in tokens if token != "report-writer")
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def _validate_legacy_workers(
|
|
216
|
-
requirement: RoleRequirement,
|
|
217
|
-
providers: tuple[str, ...],
|
|
218
|
-
) -> None:
|
|
219
|
-
# minDistinctProviders 제거. 모델 참조 중복 거부는 Task 2.
|
|
220
|
-
if (
|
|
221
|
-
len(providers) < requirement.min_count
|
|
222
|
-
or len(providers) > requirement.max_count
|
|
223
|
-
):
|
|
224
|
-
raise ModelSelectionInputError(
|
|
225
|
-
f"legacy --workers selects {len(providers)} {requirement.role} instances; "
|
|
226
|
-
f"profile requires {requirement.min_count}..{requirement.max_count}"
|
|
227
|
-
)
|
|
228
188
|
|
|
229
189
|
|
|
230
190
|
def _parse_worker_models(
|
|
@@ -346,7 +306,7 @@ def _legacy_matches_canonical(
|
|
|
346
306
|
canonical_models: Mapping[str, tuple[str, ...]],
|
|
347
307
|
legacy: CanonicalModelSelection,
|
|
348
308
|
) -> bool:
|
|
349
|
-
"""레거시(
|
|
309
|
+
"""레거시(공급자 모델 플래그 등)가 변환 후 정본과 같은 값으로 수렴하는지 본다."""
|
|
350
310
|
leg_count = legacy.role_counts.get(role)
|
|
351
311
|
can_count = canonical_counts.get(role)
|
|
352
312
|
if can_count is not None and leg_count is not None and can_count != leg_count:
|
|
@@ -456,10 +416,6 @@ def _validate_role_count(
|
|
|
456
416
|
)
|
|
457
417
|
|
|
458
418
|
|
|
459
|
-
def _requirement(profile: RoleProfile, role: str) -> RoleRequirement | None:
|
|
460
|
-
return next((row for row in profile.roles if row.role == role), None)
|
|
461
|
-
|
|
462
|
-
|
|
463
419
|
def serialize_host_session_context(context: HostSessionContext) -> str:
|
|
464
420
|
"""Serialize host-owned session facts as stable canonical JSON."""
|
|
465
421
|
current = context.current_model
|
|
@@ -277,6 +277,7 @@ def _render_brief(
|
|
|
277
277
|
assignment: dict,
|
|
278
278
|
task_group: str,
|
|
279
279
|
manager_task: str,
|
|
280
|
+
child_task_id: str,
|
|
280
281
|
stem: str,
|
|
281
282
|
created: str,
|
|
282
283
|
key: str,
|
|
@@ -293,6 +294,7 @@ def _render_brief(
|
|
|
293
294
|
"---",
|
|
294
295
|
"type: brief",
|
|
295
296
|
f"brief-id: {stem}",
|
|
297
|
+
f"task-id: {child_task_id}",
|
|
296
298
|
"parent-id: self",
|
|
297
299
|
f"ticket-id: {ticket}",
|
|
298
300
|
f"source-type: {SOURCE_TYPE}",
|
|
@@ -429,7 +431,8 @@ def split_task(
|
|
|
429
431
|
siblings = [(other["taskKey"], other["assignment"], other["briefPath"]) for other in items
|
|
430
432
|
if other["issue"] is item["issue"] and other is not item]
|
|
431
433
|
item["text"] = _render_brief(source=source, issue=item["issue"], assignment=item["assignment"],
|
|
432
|
-
task_group=task_group, manager_task=manager_task,
|
|
434
|
+
task_group=task_group, manager_task=manager_task,
|
|
435
|
+
child_task_id=item["childTaskId"], stem=item["stem"],
|
|
433
436
|
created=when[:10], key=item["taskKey"], project_root=item["projectRoot"],
|
|
434
437
|
siblings=siblings, graph=graph)
|
|
435
438
|
errors = _validate_rendered(items, validator, group_segment)
|
|
@@ -2,18 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
이 층은 파일을 열지 않는다 — 이미 읽힌 매핑을 받아 문자열만 만든다. 그래서
|
|
4
4
|
참조 해석과 독립이고, 서식을 고칠 때 경로 규칙을 다시 읽을 필요가 없다.
|
|
5
|
-
`_provider_models` 만 예외적으로 거절하지만, 그것도 디스크가 아니라 받은
|
|
6
|
-
매핑 안의 모순을 보는 것이다.
|
|
7
5
|
"""
|
|
8
6
|
from __future__ import annotations
|
|
9
7
|
|
|
10
|
-
from pathlib import Path
|
|
11
8
|
from typing import Any, Mapping
|
|
12
9
|
|
|
13
10
|
from okstra_project.phase_pointer import promote
|
|
14
11
|
|
|
15
12
|
from ..fixed_text import line as _line, scalar as _value
|
|
16
|
-
from ..json_boundary import JsonBoundaryError
|
|
17
13
|
|
|
18
14
|
|
|
19
15
|
def _mapping(payload: Mapping[str, Any], key: str) -> Mapping[str, Any]:
|
|
@@ -87,7 +83,7 @@ def _critic_assignment_lines(run: Mapping[str, Any]) -> list[str]:
|
|
|
87
83
|
"""Opt-in critics live only in `invocationAssignments`, never in `workerAssignments`.
|
|
88
84
|
|
|
89
85
|
The id is the assignment ref's last segment, the value team dispatch and
|
|
90
|
-
`
|
|
86
|
+
`criticRoles[].workerId` use (`render._critic_role_rows`).
|
|
91
87
|
"""
|
|
92
88
|
assignments = run.get("invocationAssignments")
|
|
93
89
|
if not isinstance(assignments, Mapping):
|
|
@@ -173,22 +169,3 @@ def _history_overview_text(rows: list[dict[str, object]], limit: int) -> str:
|
|
|
173
169
|
|
|
174
170
|
def _csv(value: object) -> str:
|
|
175
171
|
return ",".join(str(item) for item in value) if isinstance(value, list) else ""
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def _provider_models(run: Mapping[str, Any]) -> dict[str, str]:
|
|
179
|
-
values: dict[str, set[str]] = {}
|
|
180
|
-
assignments = run.get("workerAssignments")
|
|
181
|
-
if isinstance(assignments, list):
|
|
182
|
-
for assignment in assignments:
|
|
183
|
-
if not isinstance(assignment, Mapping):
|
|
184
|
-
continue
|
|
185
|
-
provider = assignment.get("provider")
|
|
186
|
-
model = assignment.get("model")
|
|
187
|
-
if isinstance(provider, str) and isinstance(model, str) and model:
|
|
188
|
-
values.setdefault(provider, set()).add(model)
|
|
189
|
-
for provider, models in values.items():
|
|
190
|
-
if len(models) > 1:
|
|
191
|
-
raise JsonBoundaryError(
|
|
192
|
-
Path("workerAssignments"), "run manifest", f"conflicting models for provider {provider}"
|
|
193
|
-
)
|
|
194
|
-
return {provider: next(iter(models)) for provider, models in values.items()}
|
|
@@ -18,10 +18,11 @@ from ..convergence import (
|
|
|
18
18
|
from ..error_zip import last_output_path
|
|
19
19
|
from ..fixed_text import line as _line, scalar as _value
|
|
20
20
|
from ..json_boundary import JsonBoundaryError, load_owned_object
|
|
21
|
-
from ..paths import okstra_home
|
|
21
|
+
from ..paths import find_asset_root, okstra_home
|
|
22
|
+
from ..phases.catalog import PhaseAssetError, profile_markdown
|
|
22
23
|
from ..recap import assemble_group_recap, assemble_recap
|
|
24
|
+
from ..role_requirements import RoleProfileError, load_role_profile
|
|
23
25
|
from ..timeline_runs import current_run_facts
|
|
24
|
-
from ..worker_artifacts import worker_provider_id
|
|
25
26
|
from .lines import (
|
|
26
27
|
_assignment_line,
|
|
27
28
|
_csv,
|
|
@@ -30,7 +31,6 @@ from .lines import (
|
|
|
30
31
|
_next_phase_lines,
|
|
31
32
|
_project_policy_lines,
|
|
32
33
|
next_phase_pointer,
|
|
33
|
-
_provider_models,
|
|
34
34
|
_status_overview_text,
|
|
35
35
|
_worker_assignment_lines,
|
|
36
36
|
_worker_prompt_lines,
|
|
@@ -465,46 +465,59 @@ def render_report_input(project_root: Path, task_ref: str) -> str:
|
|
|
465
465
|
)
|
|
466
466
|
|
|
467
467
|
|
|
468
|
-
def
|
|
469
|
-
"""
|
|
468
|
+
def _role_selection_rows(run: Mapping[str, Any]) -> list[tuple[str, str]]:
|
|
469
|
+
"""실행된 역할 칸을 `--role-model` 값(`<role>=<provider>/<model>`)으로 옮긴다.
|
|
470
470
|
|
|
471
|
-
`
|
|
472
|
-
|
|
473
|
-
슬롯 id 를 그대로 넘기면 재실행이 `unknown workers` 로 죽으므로, 재실행 입력을
|
|
474
|
-
만드는 이 자리에서 어휘를 맞춘다. 슬롯 수는 `--role-count` 가 나른다.
|
|
471
|
+
critic 은 `workerAssignments` 가 아니라 `invocationAssignments` 의 `critic/*`
|
|
472
|
+
에만 있다(`render._critic_role_rows`).
|
|
475
473
|
"""
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if not isinstance(item, str) or not item.strip():
|
|
474
|
+
rows: list[tuple[str, str]] = []
|
|
475
|
+
assignments = run.get("workerAssignments")
|
|
476
|
+
for assignment in assignments if isinstance(assignments, list) else ():
|
|
477
|
+
if not isinstance(assignment, Mapping):
|
|
481
478
|
continue
|
|
482
|
-
provider =
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
479
|
+
role, provider, model = (
|
|
480
|
+
assignment.get(key) for key in ("role", "provider", "model")
|
|
481
|
+
)
|
|
482
|
+
if all(isinstance(value, str) and value for value in (role, provider, model)):
|
|
483
|
+
rows.append((role, f"{provider}/{model}"))
|
|
484
|
+
invocations = run.get("invocationAssignments")
|
|
485
|
+
if isinstance(invocations, Mapping):
|
|
486
|
+
for ref, assignment in sorted(invocations.items()):
|
|
487
|
+
if not (isinstance(ref, str) and ref.startswith("critic/")):
|
|
488
|
+
continue
|
|
489
|
+
if not isinstance(assignment, Mapping):
|
|
490
|
+
continue
|
|
491
|
+
provider, model = assignment.get("provider"), assignment.get("model")
|
|
492
|
+
if isinstance(provider, str) and provider and isinstance(model, str) and model:
|
|
493
|
+
rows.append(("critic", f"{provider}/{model}"))
|
|
494
|
+
return rows
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _adjustable_roles(run_manifest: Path, task_type: object) -> set[str]:
|
|
498
|
+
"""`--role-count` 를 받는 역할 — 하한과 상한이 다른 정적 역할."""
|
|
499
|
+
root = find_asset_root(("prompts", "profiles"), is_present=Path.is_dir)
|
|
500
|
+
if root is None or not isinstance(task_type, str):
|
|
501
|
+
raise JsonBoundaryError(run_manifest, "run manifest", "phase profile is unavailable")
|
|
502
|
+
try:
|
|
503
|
+
profile = load_role_profile(profile_markdown(root, task_type))
|
|
504
|
+
except (PhaseAssetError, RoleProfileError, ValueError) as exc:
|
|
505
|
+
raise JsonBoundaryError(run_manifest, "run manifest", str(exc)) from exc
|
|
506
|
+
return {
|
|
507
|
+
requirement.role
|
|
508
|
+
for requirement in profile.roles
|
|
509
|
+
if not requirement.dynamic and requirement.min_count < requirement.max_count
|
|
510
|
+
}
|
|
486
511
|
|
|
487
512
|
|
|
488
513
|
def render_rerun_input(run_manifest: Path) -> str:
|
|
489
514
|
run_manifest = _owned_run_manifest_path(run_manifest)
|
|
490
515
|
run = load_owned_object(run_manifest, artifact="run manifest")
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (
|
|
497
|
-
isinstance(executor_provider, str)
|
|
498
|
-
and isinstance(executor_model, str)
|
|
499
|
-
and executor_model
|
|
500
|
-
and models.get(executor_provider)
|
|
501
|
-
and models[executor_provider] != executor_model
|
|
502
|
-
):
|
|
503
|
-
raise JsonBoundaryError(
|
|
504
|
-
run_manifest,
|
|
505
|
-
"run manifest",
|
|
506
|
-
f"executor model conflicts with provider {executor_provider}",
|
|
507
|
-
)
|
|
516
|
+
rows = _role_selection_rows(run)
|
|
517
|
+
adjustable = _adjustable_roles(run_manifest, run.get("taskType"))
|
|
518
|
+
counts: dict[str, int] = {}
|
|
519
|
+
for role, _model_ref in rows:
|
|
520
|
+
counts[role] = counts.get(role, 0) + 1
|
|
508
521
|
return (
|
|
509
522
|
"# Okstra Rerun Input\n\n"
|
|
510
523
|
+ _line("Project ID", run.get("projectId"))
|
|
@@ -512,13 +525,13 @@ def render_rerun_input(run_manifest: Path) -> str:
|
|
|
512
525
|
+ _line("Task ID", run.get("taskId"))
|
|
513
526
|
+ _line("Task type", run.get("taskType"))
|
|
514
527
|
+ _line("Task brief", run.get("taskBriefPath"))
|
|
515
|
-
+ _line("Workers", _worker_providers_csv(run.get("recommendedWorkers")))
|
|
516
528
|
+ _line("Related tasks", _csv(run.get("relatedTasks")))
|
|
517
|
-
+
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
529
|
+
+ "".join(
|
|
530
|
+
_line("Role count", f"{role}={count}")
|
|
531
|
+
for role, count in counts.items()
|
|
532
|
+
if role in adjustable
|
|
533
|
+
)
|
|
534
|
+
+ "".join(_line("Role model", f"{role}={model_ref}") for role, model_ref in rows)
|
|
522
535
|
)
|
|
523
536
|
|
|
524
537
|
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
# Change Impact Analysis Profile
|
|
2
2
|
|
|
3
3
|
- Purpose: assess the read-only impact of a proposed change, including preserved behavior, affected dependencies, and constraints that a later planning phase must resolve
|
|
4
|
-
- Required workers:
|
|
5
|
-
- claude
|
|
6
|
-
- codex
|
|
7
|
-
- report-writer
|
|
8
|
-
- Optional workers (opt-in via `--workers`):
|
|
9
|
-
- antigravity
|
|
10
|
-
- grok — optional adversarial analyser/critic through the Grok CLI wrapper
|
|
11
|
-
- kimi — optional long-context analyser/critic through the Kimi CLI wrapper
|
|
12
4
|
{{INCLUDE:_common-contract.md}}
|
|
13
5
|
- Phase 1.5 questions:
|
|
14
6
|
- What user intent and preserved behavior define the proposed change boundary?
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
# Error Analysis Profile
|
|
2
2
|
|
|
3
3
|
- Purpose: analyse reported errors or incidents and identify likely causes, missing evidence, and validation paths
|
|
4
|
-
- Required workers:
|
|
5
|
-
- claude
|
|
6
|
-
- codex
|
|
7
|
-
- report-writer
|
|
8
|
-
- Optional workers (opt-in via `--workers`):
|
|
9
|
-
- antigravity — when added to the roster it joins the analyser set; omitted by default
|
|
10
|
-
- grok — optional adversarial analyser/critic through the Grok CLI wrapper
|
|
11
|
-
- kimi — optional long-context analyser/critic through the Kimi CLI wrapper
|
|
12
4
|
{{INCLUDE:_common-contract.md}}
|
|
13
5
|
- Brief consumption (phase-specific addendum — shared rules live in `_common-contract.md` under "Brief handoff contract"):
|
|
14
6
|
- Apply the shared reporter-confirmation precondition exactly as written. In this phase, unresolved `intent-check:` / `conversion-block:` rows use `Blocks=next-phase`; any unconfirmed inference may be used as a labelled hypothesis only.
|
|
@@ -26,6 +18,12 @@
|
|
|
26
18
|
- **Graph-aware scope:** a graph edge can explain ordering or duplication, but it is not proof of cause by itself. Cite code/log evidence before claiming an upstream related task caused the current symptom.
|
|
27
19
|
- **Sharp next diagnostic:** end with the single highest-value diagnostic command, log capture, or file inspection that should happen next, plus the expected signal that would confirm or reject the leading cause.
|
|
28
20
|
- **Fix-design boundary:** do not design the implementation fix beyond what is necessary to validate the cause. Record whether the cause is credible and the next diagnostic. The common Phase Routing settles the next phase.
|
|
21
|
+
- Census aspects:
|
|
22
|
+
- requirement pass-observation: Which observation would show the symptom resolved for this requirement?
|
|
23
|
+
- task symptom-lock: Which single observable failure condition does the reported symptom translate to?
|
|
24
|
+
- task reproduction: Was the symptom reproduced, not reproduced, or blocked before reproduction, and on what evidence?
|
|
25
|
+
- task cause-falsification: For each cause candidate, which evidence that could falsify it was checked?
|
|
26
|
+
- task next-diagnostic: Which diagnostic should run next, and which signal would confirm or reject the leading cause?
|
|
29
27
|
- Structured diagnosis and routing contract:
|
|
30
28
|
- `errorAnalysis` is the source of truth for reproduction status, `EA-NNN` cause candidates, the sharp next diagnostic, and the next route.
|
|
31
29
|
- Record the route selected by the lead through the common Phase Routing rules, its `routing.leadingCauseId`, and the matching direction.
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
# Feature Analysis Profile
|
|
2
2
|
|
|
3
3
|
- Purpose: analyse a confirmed feature target's behavior, rules, state changes, external calls, and test coverage scope without designing or changing an implementation
|
|
4
|
-
- Required workers:
|
|
5
|
-
- claude
|
|
6
|
-
- codex
|
|
7
|
-
- report-writer
|
|
8
|
-
- Optional workers (opt-in via `--workers`):
|
|
9
|
-
- antigravity
|
|
10
|
-
- grok — optional adversarial analyser/critic through the Grok CLI wrapper
|
|
11
|
-
- kimi — optional long-context analyser/critic through the Kimi CLI wrapper
|
|
12
4
|
{{INCLUDE:_common-contract.md}}
|
|
13
5
|
- Phase 1.5 questions:
|
|
14
6
|
- What confirmed target and user intent bound this analysis?
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
# Final Verification Profile
|
|
2
2
|
|
|
3
3
|
- Purpose: judge the delivered implementation on three axes before final acceptance — does it cover every requirement (under-delivery), does it carry work no requirement asked for (over-delivery), and does it actually do what it claims (defects, and tests that verify nothing). Whether the run followed okstra's own procedure is not one of the axes: the runtime and `validators/validate-run.py` own that, and a finding about it is not an acceptance judgement
|
|
4
|
-
- Required workers:
|
|
5
|
-
- claude
|
|
6
|
-
- codex
|
|
7
|
-
- report-writer
|
|
8
|
-
- Optional workers (opt-in via `--workers`):
|
|
9
|
-
- antigravity — when added to the roster it joins the analyser set; omitted by default
|
|
10
|
-
- grok — optional read-only adversarial analyser/critic through the Grok CLI wrapper
|
|
11
|
-
- kimi — optional read-only long-context analyser/critic through the Kimi CLI wrapper
|
|
12
4
|
{{INCLUDE:_common-contract.md}}
|
|
13
5
|
- Primary focus areas (each maps to a deliverable section below):
|
|
14
6
|
- Acceptance-gating — a failure here pushes the verdict toward `blocked` / `conditional-accept`:
|
|
@@ -56,6 +48,8 @@
|
|
|
56
48
|
- **Design-preparation carry-in:** read only source implementation report `missingInformation` rows whose `source` starts with `design-prep:`. Recommend `ifStillOpen: block` as an acceptance blocker and `ifStillOpen: follow-up` as residual risk. This phase does not read planning PREP sidecars directly or mutate planning snapshots.
|
|
57
49
|
- **Could-not-verify honesty:** use `not-configured`, `env-unavailable`, `rejected`, `gap`, or `blocked` as appropriate. Never convert unavailable evidence into an executed/pass claim.
|
|
58
50
|
- **Source-mutation prohibition:** verification may write only assigned okstra run artifacts. Do not edit source, schema, deployment, lockfile, or configuration files; route detected defects to a later phase.
|
|
51
|
+
- Census aspects:
|
|
52
|
+
- file added-surface: Does this changed file add behaviour, configuration, or surface no requirement asked for, and does what it adds work?
|
|
59
53
|
- Required deliverable shape (final report, in addition to the standard sections):
|
|
60
54
|
- **Source Implementation Report(s)** (**Enforced:** `phases/final_verification/validation.py` `validate_verification_target_match` (called by `validators/validate-run.py`) compares `verificationScope`, `worktreePath`, `implementationBaseRef`, `capturedHeadSha`, and the `stageReports` stage set against the digest-verified `instruction-set/verification-target.md`; a snapshot whose digest no longer checks out is ignored rather than trusted. `verificationScope` in particular gates both stage-group eligibility and release-handoff routing, so it is not the report's to restate): the `VERIFICATION_TARGET` snapshot verbatim — verification scope, worktree path, base/head refs, the list of stages under verification, and one row per stage citing its originating implementation final-report (`report_path` from `consumers.jsonl`; render `(report_path unrecorded)` when absent). Every analyser prompt carries the same compact target identity (`**Verification scope:** / **Worktree:** / **Verification base ref:** / **Verification head ref:** / **Verification target path:** / **Verification target digest:**`) and reads the sidecar on demand for the complete diff stat. A worker that cannot confirm its analysis ran against that worktree's delivered diff MUST record a `tool-failure`.
|
|
61
55
|
- **Verdict vocabulary**: Section 7 (`Final Verdict`) MUST include a `Verdict Token` field whose value is exactly one of `accepted`, `conditional-accept`, or `blocked`. `conditional-accept` requires an explicit, exhaustive list of conditions; ambiguous verdicts ("looks good", "mostly ready") are not allowed. Each condition MUST be recorded as a row in the **Conditional Acceptance Conditions** deliverable (`id` `CA-NNN`, `condition`, `evidenceRequired`, `blocksReleaseHandoff`). `blocksReleaseHandoff` is a gate, not a note: `false` means this condition alone would not stop the release, and a `conditional-accept` whose conditions are all `false` may route to `release-handoff` (the conditions travel into the PR body as unresolved items). Declare `true` for anything that must be settled before release. The validator enforces verdict↔deliverable consistency: `accepted` ⇒ zero acceptance blockers, `blocked` ⇒ at least one, `conditional-accept` ⇒ at least one condition, and a `release-handoff` routing recommendation is allowed only when the verdict is release-ready by `okstra_ctl.release_gate.release_handoff_allowed`. **Any Acceptance Blocker therefore forces the verdict off `accepted` (to `conditional-accept` or `blocked`); the gates below cite this rule instead of restating the arithmetic.**
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"production credentials, deploy commands, infra mutation (`terraform apply`, `kubectl apply` against non-local cluster, etc.)",
|
|
8
8
|
"external API write calls (POST/PUT/PATCH/DELETE) to third-party services other than localhost test fixtures",
|
|
9
9
|
"source edits or Bash mutations performed by any verifier role (`Antigravity verifier`, `Codex verifier`, `Claude verifier` are read-only — recommend, do not apply)",
|
|
10
|
-
"dispatching parallel sub-agents beyond the
|
|
10
|
+
"dispatching parallel sub-agents beyond the run's selected roster",
|
|
11
11
|
"silent scope expansion: every file edited outside the approved plan list MUST appear in the `Out-of-plan edits` block with rationale",
|
|
12
12
|
"leaving placeholders such as TBD / TODO / \"implement later\" / \"handle edge cases\" in newly-added lines of this run (check via `git diff <base>..HEAD | grep -E '^\\+[^+].*\\b(TBD|TODO|FIXME|XXX|implement later|handle edge cases|similar to|placeholder)\\b'`; pre-existing strings in untouched regions are out of scope)",
|
|
13
13
|
"lead substituting its own verdict when every verifier present in the resolved roster returned a non-result terminal status (`timeout`/`error`/`not-run`); in that case the run MUST end as `blocked` with routing recommendation back to `error-analysis`, never with a lead-only verdict",
|
|
@@ -3,12 +3,6 @@
|
|
|
3
3
|
- Purpose: realise the approved `implementation-planning` deliverable as actual source changes, with cross-model verification, while keeping the run reversible
|
|
4
4
|
- **Run-level fixed cost:** the verifier set, Phase 5.5 convergence, and the Phase 6 report-writer run exactly once per implementation run, over this run's single stage diff — never once per step.
|
|
5
5
|
- **Fix run (profile carries a "Fix-Run Carry" block):** the executor's scope is the carried blocking findings plus the previous routing recommendation — it MUST NOT re-execute plan steps the previous run completed. Verifiers apply the "Fix-run incremental scope" section of `_implementation-verifier.md`; the report writer applies "Fix-run incremental authoring" in `report-writer.md`. The full validation-command re-run is NOT reduced.
|
|
6
|
-
- Required workers:
|
|
7
|
-
- claude
|
|
8
|
-
- codex
|
|
9
|
-
- report-writer
|
|
10
|
-
- Optional workers (opt-in via `--workers`):
|
|
11
|
-
- antigravity — when added to the roster it joins the verifier set; when omitted only the default Claude+Codex verifiers participate. `--executor antigravity` requires `antigravity` in the roster: the direct CLI demands it explicitly in `--workers`, while the wizard adds it automatically when you pick antigravity as the executor.
|
|
12
6
|
- **Executor binding (resolved at run-prep time, fixed for this run):**
|
|
13
7
|
- Executor display name: `{{EXECUTOR_DISPLAY_NAME}}`
|
|
14
8
|
- Executor worker ID: `{{EXECUTOR_WORKER_ID}}`
|
|
@@ -73,7 +73,7 @@ taskType: "{{FM_TASK_TYPE}}"
|
|
|
73
73
|
|
|
74
74
|
- `git push`, publish, deploy, migration execution, third-party write APIs, production credentials
|
|
75
75
|
- Edits or Bash mutations performed by any verifier role
|
|
76
|
-
- Sub-agent dispatch beyond the
|
|
76
|
+
- Sub-agent dispatch beyond the run's selected roster
|
|
77
77
|
- Silent scope expansion — every file touched outside the plan's list MUST be recorded in `Out-of-plan edits` (**Enforced (declared edits only):** `_validate_out_of_plan_edits_are_real`)
|
|
78
78
|
|
|
79
79
|
## Verifier Worker Inputs
|
|
@@ -9,7 +9,7 @@ from .validation import (
|
|
|
9
9
|
MAX_RANKED_OPTIONS,
|
|
10
10
|
MAX_RAW_CANDIDATES_PER_ANALYSER,
|
|
11
11
|
MIN_CRITERION_VALUE,
|
|
12
|
-
|
|
12
|
+
required_feasible_votes,
|
|
13
13
|
)
|
|
14
14
|
from okstra_ctl.exact_coverage import COVERAGE_VERDICT_PRECEDENCE
|
|
15
15
|
|
|
@@ -51,8 +51,9 @@ def requirement_instructions(original_requirement_ids: Sequence[str], participat
|
|
|
51
51
|
"otherwise).",
|
|
52
52
|
"A ranked option is valid only when every participating analyser "
|
|
53
53
|
"supplied one feasibility vote, at least "
|
|
54
|
-
f"{
|
|
55
|
-
"and `unresolvedFeasibilityFacts`
|
|
54
|
+
f"{required_feasible_votes(participating_analysers)} of them are "
|
|
55
|
+
"`feasible`, and both `safetyBlockers` and `unresolvedFeasibilityFacts` "
|
|
56
|
+
"are empty.",
|
|
56
57
|
("Classify each `unresolvedFeasibilityFacts` entry with `resolutionKind`: "
|
|
57
58
|
"`user-decision` requires nonempty `clarificationRefs` linking that fact "
|
|
58
59
|
"to actual C-NNN records, including answered records; `technical-verification` "
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
"""방향 비교가 필요한
|
|
1
|
+
"""방향 비교가 필요한 요구사항을 확인한다."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Sequence
|
|
6
|
-
|
|
7
5
|
from okstra_ctl.prepare_error import PrepareError
|
|
8
6
|
from okstra_ctl.scope_provenance import brief_end_state_id_sequence
|
|
9
7
|
|
|
@@ -14,14 +12,3 @@ def validate_brief_requirements(brief_path: Path) -> None:
|
|
|
14
12
|
"regenerate the brief with stable end-state IDs before starting "
|
|
15
13
|
"implementation-option-selection"
|
|
16
14
|
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def validate_analyser_roster(
|
|
20
|
-
workers: Sequence[str]
|
|
21
|
-
) -> None:
|
|
22
|
-
"""Require independent analysis from three workers before option selection."""
|
|
23
|
-
analyser_count = sum(worker != "report-writer" for worker in workers)
|
|
24
|
-
if analyser_count < 3:
|
|
25
|
-
raise PrepareError(
|
|
26
|
-
"implementation-option-selection requires at least 3 analyser workers"
|
|
27
|
-
)
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
# Implementation Option Selection Profile
|
|
2
2
|
|
|
3
3
|
- Purpose: compare feasible implementation directions before planning, preserving a read-only record of the evidence and trade-offs that selects the direction to plan
|
|
4
|
-
- Required workers:
|
|
5
|
-
- claude
|
|
6
|
-
- codex
|
|
7
|
-
- antigravity
|
|
8
|
-
- report-writer
|
|
9
|
-
- Optional workers (opt-in via `--workers`):
|
|
10
|
-
- grok
|
|
11
|
-
- kimi
|
|
12
4
|
{{INCLUDE:_common-contract.md}}
|
|
13
5
|
- Brief consumption:
|
|
14
6
|
- Apply the shared reporter-confirmation precondition exactly as written. Unresolved `intent-check:` and `conversion-block:` rows use `Blocks=next-phase`.
|
|
@@ -18,6 +10,11 @@
|
|
|
18
10
|
- In `candidate-comparison` mode only, submit at most three candidates. A candidate must be feasible from inspected evidence, not from an assumed future change.
|
|
19
11
|
- In `preselected-validation` mode, receive one preselected direction from the lead and validate its evidence, counterevidence, criterion scores, and requirement mappings, and state the same feasibility verdict with its rationale and counterevidence. The worker must not generate new candidates.
|
|
20
12
|
- Do not produce detailed file lists, stage maps, execution commands, or a plan approval request.
|
|
13
|
+
- Census aspects:
|
|
14
|
+
- requirement mapping: Which candidate or preselected direction satisfies this requirement, and how?
|
|
15
|
+
- requirement support: Which inspected evidence supports that mapping?
|
|
16
|
+
- requirement counter-evidence: What is the strongest evidence against that mapping?
|
|
17
|
+
- task feasibility: Is each candidate feasible, not feasible, or uncertain on inspected evidence?
|
|
21
18
|
- Pre-selection context exploration:
|
|
22
19
|
- In `candidate-comparison` mode, inspect the code paths, interfaces, tests, and constraints needed to distinguish candidates before assigning scores.
|
|
23
20
|
- In `preselected-validation` mode, inspect the code paths, interfaces, tests, and constraints needed to validate the one preselected direction.
|