okstra 0.206.0 → 0.207.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 -3
- package/dist/cli-registry.mjs +7 -1
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/lifecycle/install.mjs +1 -1
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/docs/architecture/storage-model.md +1 -0
- package/docs/architecture.md +40 -16
- package/docs/cli.md +17 -15
- package/docs/contributor-change-matrix.md +3 -2
- package/docs/performance-improvement-plan-v2.md +1 -1
- package/docs/project-structure-overview.md +43 -20
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/operations/code-review.json +1 -1
- package/runtime/bin/lib/okstra/usage.sh +3 -3
- package/runtime/bin/okstra-compact-reminder.sh +1 -1
- package/runtime/bin/okstra-spawn-followups.py +2 -2
- package/runtime/prompts/duties/direction-selection-worker.json +1 -1
- package/runtime/prompts/launch.template.md +2 -2
- package/runtime/prompts/lead/adapters/cmux.md +4 -3
- package/runtime/prompts/lead/context-loader.md +1 -1
- package/runtime/prompts/lead/convergence.md +44 -12
- package/runtime/prompts/lead/okstra-lead-contract.md +44 -73
- package/runtime/prompts/lead/phase-routing.md +64 -0
- package/runtime/prompts/lead/report-writer.md +10 -8
- package/runtime/prompts/lead/team-contract.md +1 -1
- package/runtime/prompts/profiles/_clarification-recommendation.md +4 -4
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +2 -2
- package/runtime/prompts/profiles/_coverage-critic.md +1 -1
- package/runtime/prompts/profiles/forbidden-actions.json +0 -94
- package/runtime/prompts/wizard/prompts.ko.json +2 -1
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +5 -5
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -2
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +17 -26
- package/runtime/python/okstra_ctl/agent/prompt_cli/batch.py +183 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +60 -10
- package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +1 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/jobs.py +21 -4
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +10 -1
- package/runtime/python/okstra_ctl/analysis_inputs.py +0 -39
- package/runtime/python/okstra_ctl/analysis_scope.py +31 -0
- package/runtime/python/okstra_ctl/approval_decisions.py +32 -2
- package/runtime/python/okstra_ctl/asset_roots.py +19 -0
- package/runtime/python/okstra_ctl/assignment_resolver.py +8 -0
- package/runtime/python/okstra_ctl/blocking_checks.py +7 -0
- package/runtime/python/okstra_ctl/code_review_target.py +92 -6
- package/runtime/python/okstra_ctl/consumers.py +12 -0
- package/runtime/python/okstra_ctl/dispatch_checkpoints.py +121 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +54 -32
- package/runtime/python/okstra_ctl/dispatch_state.py +34 -5
- package/runtime/python/okstra_ctl/doctor.py +2 -1
- package/runtime/python/okstra_ctl/domain/provider.py +5 -0
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +21 -2
- package/runtime/python/okstra_ctl/domain/write_policy.py +2 -1
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +46 -9
- package/runtime/python/okstra_ctl/handoff.py +11 -466
- package/runtime/python/okstra_ctl/handoff_error.py +5 -0
- package/runtime/python/okstra_ctl/implementation_direction.py +0 -477
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +13 -1
- package/runtime/python/okstra_ctl/lead_progress.py +33 -1
- package/runtime/python/okstra_ctl/manager_view.py +26 -19
- package/runtime/python/okstra_ctl/model_io/lines.py +21 -4
- package/runtime/python/okstra_ctl/models.py +4 -1
- package/runtime/python/okstra_ctl/operation_invocation.py +11 -2
- package/runtime/python/okstra_ctl/option_comparison.py +3 -165
- package/runtime/python/okstra_ctl/option_votes.py +3 -191
- package/runtime/python/okstra_ctl/paths.py +8 -6
- package/runtime/python/okstra_ctl/phases/catalog.py +56 -12
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/boundary.json +11 -0
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/entry.py +39 -0
- package/runtime/python/okstra_ctl/{report_html/view_models/change_impact_analysis.py → phases/change_impact_analysis/report.py} +3 -3
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/spec.md +26 -0
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/validation.py +23 -0
- package/runtime/python/okstra_ctl/phases/error_analysis/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/error_analysis/boundary.json +9 -0
- package/runtime/{prompts/profiles/error-analysis.md → python/okstra_ctl/phases/error_analysis/profile.md} +2 -2
- package/runtime/python/okstra_ctl/{report_html/view_models/error_analysis.py → phases/error_analysis/report.py} +9 -8
- package/runtime/{templates/reports → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis-input.template.md +1 -1
- package/runtime/python/okstra_ctl/phases/error_analysis/spec.md +118 -0
- package/runtime/python/okstra_ctl/phases/error_analysis/validation.py +241 -0
- package/runtime/python/okstra_ctl/phases/feature_analysis/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/feature_analysis/boundary.json +8 -0
- package/runtime/python/okstra_ctl/phases/feature_analysis/entry.py +63 -0
- package/runtime/python/okstra_ctl/{report_html/view_models/feature_analysis.py → phases/feature_analysis/report.py} +12 -5
- package/runtime/python/okstra_ctl/phases/feature_analysis/spec.md +22 -0
- package/runtime/python/okstra_ctl/phases/feature_analysis/validation.py +27 -0
- package/runtime/python/okstra_ctl/phases/feature_analysis/wizard.py +95 -0
- package/runtime/python/okstra_ctl/phases/final_verification/boundary.json +8 -0
- package/runtime/python/okstra_ctl/phases/final_verification/profile.md +2 -2
- package/runtime/{templates/reports → python/okstra_ctl/phases/final_verification/report_assets}/final-verification-input.template.md +1 -1
- package/runtime/python/okstra_ctl/phases/final_verification/spec.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/implementation/boundary.json +17 -0
- package/runtime/python/okstra_ctl/{implementation_stage.py → phases/implementation/entry.py} +22 -10
- package/runtime/{prompts/host-orchestration/implementation.md → python/okstra_ctl/phases/implementation/host-rules.md} +1 -1
- package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-deliverable.md +1 -1
- package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-executor.md +4 -3
- package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-verifier.md +18 -7
- package/runtime/{prompts/profiles/implementation.md → python/okstra_ctl/phases/implementation/profile.md} +5 -5
- package/runtime/python/okstra_ctl/{report_html/view_models/implementation.py → phases/implementation/report.py} +3 -3
- package/runtime/{templates/reports → python/okstra_ctl/phases/implementation/report_assets}/implementation-input.template.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/spec.md +238 -0
- package/runtime/python/okstra_ctl/phases/implementation/validation.py +205 -0
- package/runtime/python/okstra_ctl/phases/implementation/wizard.py +39 -0
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +80 -0
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/boundary.json +10 -0
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/comparison.py +168 -0
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +27 -0
- package/runtime/{prompts/profiles/implementation-option-selection.md → python/okstra_ctl/phases/implementation_option_selection/profile.md} +3 -3
- package/runtime/python/okstra_ctl/{report_html/view_models/implementation_option_selection.py → phases/implementation_option_selection/report.py} +2 -2
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/spec.md +83 -0
- package/runtime/python/okstra_ctl/{implementation_options.py → phases/implementation_option_selection/validation.py} +3 -3
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/votes.py +194 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +2345 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/boundary.json +12 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/entry.py +161 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/guidance.py +178 -0
- package/runtime/{prompts/lead → python/okstra_ctl/phases/implementation_planning/instructions}/plan-body-verification.md +61 -51
- package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +3295 -0
- package/runtime/{prompts/profiles/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/profile.md} +74 -25
- package/runtime/python/okstra_ctl/phases/implementation_planning/report.py +237 -0
- package/runtime/{templates/reports → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning-input.template.md +2 -2
- package/runtime/python/okstra_ctl/phases/implementation_planning/spec.md +204 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/validation.py +597 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/wizard.py +166 -0
- package/runtime/python/okstra_ctl/phases/improvement_discovery/boundary.json +12 -0
- package/runtime/python/okstra_ctl/{improvement_lenses.py → phases/improvement_discovery/lenses.py} +1 -6
- package/runtime/{prompts/profiles/improvement-discovery.md → python/okstra_ctl/phases/improvement_discovery/profile.md} +5 -5
- package/runtime/python/okstra_ctl/{report_html/view_models/improvement_discovery.py → phases/improvement_discovery/report.py} +3 -3
- package/runtime/{templates/reports → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery-input.template.md +1 -2
- package/runtime/python/okstra_ctl/phases/improvement_discovery/spec.md +29 -0
- package/runtime/{validators/validate_improvement_report.py → python/okstra_ctl/phases/improvement_discovery/validation.py} +5 -14
- package/runtime/python/okstra_ctl/phases/project_analysis/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/project_analysis/boundary.json +8 -0
- package/runtime/python/okstra_ctl/phases/project_analysis/entry.py +11 -0
- package/runtime/python/okstra_ctl/{report_html/view_models/project_analysis.py → phases/project_analysis/report.py} +3 -3
- package/runtime/python/okstra_ctl/phases/project_analysis/spec.md +33 -0
- package/runtime/python/okstra_ctl/phases/project_analysis/validation.py +55 -0
- package/runtime/python/okstra_ctl/phases/release_handoff/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +17 -0
- package/runtime/python/okstra_ctl/phases/release_handoff/entry.py +147 -0
- package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +446 -0
- package/runtime/{prompts/profiles/release-handoff.md → python/okstra_ctl/phases/release_handoff/profile.md} +3 -3
- package/runtime/python/okstra_ctl/{report_html/view_models/release_handoff.py → phases/release_handoff/report.py} +3 -3
- package/runtime/{templates/reports → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff-input.template.md +1 -1
- package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +233 -0
- package/runtime/python/okstra_ctl/phases/release_handoff/wizard.py +84 -0
- package/runtime/python/okstra_ctl/phases/requirements_discovery/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/requirements_discovery/boundary.json +9 -0
- package/runtime/{prompts/profiles/requirements-discovery.md → python/okstra_ctl/phases/requirements_discovery/profile.md} +2 -3
- package/runtime/python/okstra_ctl/{report_html/view_models/requirements_discovery.py → phases/requirements_discovery/report.py} +3 -3
- package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +132 -0
- package/runtime/{validators/validate_fanout.py → python/okstra_ctl/phases/requirements_discovery/validation.py} +11 -12
- package/runtime/python/okstra_ctl/phases/technical_verification/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/technical_verification/boundary.json +9 -0
- package/runtime/python/okstra_ctl/phases/technical_verification/entry.py +100 -0
- package/runtime/{prompts/profiles/technical-verification.md → python/okstra_ctl/phases/technical_verification/profile.md} +2 -2
- package/runtime/python/okstra_ctl/{report_html/view_models/technical_verification.py → phases/technical_verification/report.py} +2 -2
- package/runtime/python/okstra_ctl/phases/technical_verification/spec.md +37 -0
- package/runtime/python/okstra_ctl/phases/technical_verification/validation.py +90 -0
- package/runtime/python/okstra_ctl/plan_approval.py +70 -0
- package/runtime/python/okstra_ctl/plan_items_cli.py +2 -2130
- package/runtime/python/okstra_ctl/process_group.py +118 -0
- package/runtime/python/okstra_ctl/profile_show.py +3 -3
- package/runtime/python/okstra_ctl/render.py +15 -4
- package/runtime/python/okstra_ctl/report_assembly.py +28 -92
- package/runtime/python/okstra_ctl/report_finalize.py +106 -2
- package/runtime/python/okstra_ctl/report_html/context_links.py +1 -1
- package/runtime/python/okstra_ctl/report_projections.py +1 -36
- package/runtime/python/okstra_ctl/report_routing.py +23 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +4 -73
- package/runtime/python/okstra_ctl/report_validation_identity.py +38 -0
- package/runtime/python/okstra_ctl/report_views.py +1 -1
- package/runtime/python/okstra_ctl/run.py +68 -350
- package/runtime/python/okstra_ctl/run_artifact_prune.py +200 -0
- package/runtime/python/okstra_ctl/stage_map.py +13 -0
- package/runtime/python/okstra_ctl/team.py +108 -9
- package/runtime/python/okstra_ctl/technical_verification_facts.py +52 -0
- package/runtime/python/okstra_ctl/wizard/__init__.py +31 -31
- package/runtime/python/okstra_ctl/wizard/api.py +18 -0
- package/runtime/python/okstra_ctl/wizard/outcome.py +3 -12
- package/runtime/python/okstra_ctl/wizard/registry.py +20 -12
- package/runtime/python/okstra_ctl/wizard/steps_analysis.py +0 -97
- package/runtime/python/okstra_ctl/wizard/steps_options.py +8 -0
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +10 -263
- package/runtime/python/okstra_ctl/wizard/steps_roles.py +2 -1
- package/runtime/python/okstra_ctl/work_categories.py +1 -1
- package/runtime/python/okstra_ctl/worker_dispatch.py +44 -3
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +36 -0
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +19 -0
- package/runtime/python/okstra_ctl/worker_runner.py +21 -3
- package/runtime/python/okstra_ctl/workflow.py +26 -143
- package/runtime/python/okstra_ctl/write_policy.py +57 -7
- package/runtime/python/okstra_project/dirs.py +14 -0
- package/runtime/python/okstra_project/resolver.py +2 -1
- package/runtime/schemas/execution-manifest-v2.schema.json +2 -1
- package/runtime/skills/okstra-brief-gen/SKILL.md +3 -3
- package/runtime/skills/okstra-code-review/SKILL.md +70 -32
- package/runtime/skills/okstra-code-review/references/review-calibration.md +26 -6
- package/runtime/skills/okstra-run/SKILL.md +3 -3
- package/runtime/templates/manager/view.template.html +18 -1
- package/runtime/templates/reports/quick-input.template.md +1 -1
- package/runtime/templates/reports/task-brief.template.md +1 -1
- package/runtime/validators/validate-brief.py +2 -2
- package/runtime/validators/validate-run.py +299 -3940
- package/runtime/validators/validate_analysis_report.py +14 -126
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +0 -147
- package/runtime/python/okstra_ctl/technical_verification.py +0 -195
- /package/runtime/{prompts/profiles/change-impact-analysis.json → python/okstra_ctl/phases/change_impact_analysis/profile.json} +0 -0
- /package/runtime/{prompts/profiles/change-impact-analysis.md → python/okstra_ctl/phases/change_impact_analysis/profile.md} +0 -0
- /package/runtime/{templates/reports → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis-input.template.md +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.md +0 -0
- /package/runtime/{prompts/profiles/error-analysis.json → python/okstra_ctl/phases/error_analysis/profile.json} +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.md +0 -0
- /package/runtime/{prompts/profiles/feature-analysis.json → python/okstra_ctl/phases/feature_analysis/profile.json} +0 -0
- /package/runtime/{prompts/profiles/feature-analysis.md → python/okstra_ctl/phases/feature_analysis/profile.md} +0 -0
- /package/runtime/{templates/reports → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis-input.template.md +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.md +0 -0
- /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-diff-review.md +0 -0
- /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-self-check.md +0 -0
- /package/runtime/{prompts/profiles/implementation.json → python/okstra_ctl/phases/implementation/profile.json} +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.md +0 -0
- /package/runtime/{prompts/profiles/implementation-option-selection.json → python/okstra_ctl/phases/implementation_option_selection/profile.json} +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.md +0 -0
- /package/runtime/{prompts/host-orchestration/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/host-rules.md} +0 -0
- /package/runtime/{prompts/profiles/implementation-planning.json → python/okstra_ctl/phases/implementation_planning/profile.json} +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.md +0 -0
- /package/runtime/{prompts/profiles/improvement-discovery.json → python/okstra_ctl/phases/improvement_discovery/profile.json} +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.md +0 -0
- /package/runtime/{prompts/profiles/project-analysis.json → python/okstra_ctl/phases/project_analysis/profile.json} +0 -0
- /package/runtime/{prompts/profiles/project-analysis.md → python/okstra_ctl/phases/project_analysis/profile.md} +0 -0
- /package/runtime/{templates/reports → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis-input.template.md +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.md +0 -0
- /package/runtime/{prompts/profiles/release-handoff.json → python/okstra_ctl/phases/release_handoff/profile.json} +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.md +0 -0
- /package/runtime/python/okstra_ctl/{fanout.py → phases/requirements_discovery/fanout.py} +0 -0
- /package/runtime/{prompts/profiles/requirements-discovery.json → python/okstra_ctl/phases/requirements_discovery/profile.json} +0 -0
- /package/runtime/{templates/reports → python/okstra_ctl/phases/requirements_discovery/report_assets}/fan-out-unit.template.md +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.md +0 -0
- /package/runtime/{prompts/profiles/technical-verification.json → python/okstra_ctl/phases/technical_verification/profile.json} +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/technical_verification/report_assets}/technical-verification.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/technical_verification/report_assets}/technical-verification.template.md +0 -0
|
@@ -27,7 +27,8 @@ from . import code_review_paths, stage_targets, worktree_registry
|
|
|
27
27
|
from .code_review_paths import branch_review_dir, next_branch_review
|
|
28
28
|
from .paths import RunRef
|
|
29
29
|
from .stage_map import StageMapError, load_latest_plan_stage_map
|
|
30
|
-
from .fixed_text import line
|
|
30
|
+
from .fixed_text import line, scalar
|
|
31
|
+
from .report_language import ReportLanguageError, resolve_report_language
|
|
31
32
|
|
|
32
33
|
DEFAULT_BRANCH_CANDIDATES = ("main", "master")
|
|
33
34
|
|
|
@@ -101,9 +102,12 @@ def _branch_snapshot(repo: Path, branch: str, base: str, review_date: str) -> di
|
|
|
101
102
|
_CLI_EPILOG = r"""Usage:
|
|
102
103
|
okstra code-review target --task-key <k> --stage <N> [--base <ref>] [--project-root <dir>] [--cwd <dir>] --json
|
|
103
104
|
okstra code-review target --branch <name> [--base <ref>] [--date <YYYY-MM-DD>] [--project-root <dir>] --json
|
|
105
|
+
okstra code-review check-lines --base <ref> --head <ref> --cite <path:line> [--cite ...] [--project-root <dir>]
|
|
104
106
|
|
|
105
107
|
Output: JSON { ok, mode, worktreePath, branch, baseCommit, headCommit,
|
|
106
|
-
reviewPath, round }.
|
|
108
|
+
reviewPath, round, reportLanguage }. `check-lines` prints `ok`, `not-changed`
|
|
109
|
+
(with the file's changed lines) or `not-in-diff` per citation and exits 1 when
|
|
110
|
+
any citation is not a line the range changed. `--base` sets the base in either mode. Without it, stage
|
|
107
111
|
mode reads the base commit from the stage registry row and branch mode falls
|
|
108
112
|
back to the merge-base with the default branch. An empty worktreePath means the review reads the branch ref
|
|
109
113
|
instead of a checked-out tree. This is read-only — it never creates
|
|
@@ -116,11 +120,9 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
116
120
|
epilog=_CLI_EPILOG,
|
|
117
121
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
118
122
|
prog="okstra code-review",
|
|
119
|
-
description="Resolve a code review's diff range and result file path."
|
|
123
|
+
description="Resolve a code review's diff range and result file path, or check finding citations."
|
|
120
124
|
)
|
|
121
|
-
|
|
122
|
-
# 그 토큰을 파서가 직접 받는다.
|
|
123
|
-
parser.add_argument("command", choices=("target",))
|
|
125
|
+
parser.add_argument("command", choices=("target", "check-lines"))
|
|
124
126
|
parser.add_argument("--task-key", default="", help="project-id:task-group:task-id")
|
|
125
127
|
parser.add_argument("--stage", type=int, default=0, help="stage number to review")
|
|
126
128
|
parser.add_argument("--branch", default="", help="branch to review")
|
|
@@ -128,6 +130,11 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
128
130
|
"--base", default="",
|
|
129
131
|
help="explicit base ref; overrides the stage registry base or the branch merge-base",
|
|
130
132
|
)
|
|
133
|
+
parser.add_argument("--head", default="", help="check-lines: head ref of the reviewed range")
|
|
134
|
+
parser.add_argument(
|
|
135
|
+
"--cite", action="append", default=[],
|
|
136
|
+
help="check-lines: a finding's `path:line` (repeatable)",
|
|
137
|
+
)
|
|
131
138
|
parser.add_argument("--date", default="", help="YYYY-MM-DD for branch result naming")
|
|
132
139
|
parser.add_argument("--project-root", default="", help="explicit project root")
|
|
133
140
|
parser.add_argument("--cwd", default=".", help="cwd for project root resolution")
|
|
@@ -160,6 +167,7 @@ def render_code_review_target_text(payload: dict) -> str:
|
|
|
160
167
|
("Worktree path", "worktreePath"), ("Branch", "branch"),
|
|
161
168
|
("Base commit", "baseCommit"), ("Head commit", "headCommit"),
|
|
162
169
|
("Review path", "reviewPath"), ("Round", "round"),
|
|
170
|
+
("Report language", "reportLanguage"),
|
|
163
171
|
("Task key", "taskKey"), ("Task root", "taskRoot"), ("Stage", "stage"),
|
|
164
172
|
("Failure stage", "stage"), ("Failure reason", "reason"),
|
|
165
173
|
)
|
|
@@ -170,6 +178,77 @@ def render_code_review_target_text(payload: dict) -> str:
|
|
|
170
178
|
return "".join(rows)
|
|
171
179
|
|
|
172
180
|
|
|
181
|
+
_HUNK_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def changed_lines(repo: Path, base: str, head: str) -> dict[str, set[int]]:
|
|
185
|
+
"""Head-side line numbers each file's diff touched.
|
|
186
|
+
|
|
187
|
+
A pure deletion changes no head line, so it anchors on the line before the
|
|
188
|
+
cut — the line a reviewer quotes when it points at what was removed.
|
|
189
|
+
"""
|
|
190
|
+
diff = _git_out(repo, "diff", "-U0", "--no-color", f"{base}..{head}")
|
|
191
|
+
lines: dict[str, set[int]] = {}
|
|
192
|
+
current: set[int] | None = None
|
|
193
|
+
for row in diff.splitlines():
|
|
194
|
+
if row.startswith("+++ "):
|
|
195
|
+
path = row[4:]
|
|
196
|
+
current = None if path == "/dev/null" else lines.setdefault(path[2:], set())
|
|
197
|
+
continue
|
|
198
|
+
match = _HUNK_RE.match(row)
|
|
199
|
+
if match and current is not None:
|
|
200
|
+
start = int(match[1])
|
|
201
|
+
count = 1 if match[2] is None else int(match[2])
|
|
202
|
+
current.update(range(start, start + count) if count else {max(start, 1)})
|
|
203
|
+
return lines
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def render_check_lines(changed: dict[str, set[int]], cites: list[str]) -> tuple[str, bool]:
|
|
207
|
+
rows = ["Okstra code review citation check\n"]
|
|
208
|
+
all_ok = True
|
|
209
|
+
for cite in cites:
|
|
210
|
+
path, _, number = cite.rpartition(":")
|
|
211
|
+
touched = changed.get(path)
|
|
212
|
+
label = f"- `{scalar(cite)}`: "
|
|
213
|
+
if touched is None:
|
|
214
|
+
all_ok = False
|
|
215
|
+
rows.append(label + "`not-in-diff`\n")
|
|
216
|
+
elif number.isdigit() and int(number) in touched:
|
|
217
|
+
rows.append(label + "`ok`\n")
|
|
218
|
+
else:
|
|
219
|
+
all_ok = False
|
|
220
|
+
rows.append(
|
|
221
|
+
label + "`not-changed` — changed lines "
|
|
222
|
+
f"{_line_ranges(sorted(touched))}\n"
|
|
223
|
+
)
|
|
224
|
+
return "".join(rows), all_ok
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _line_ranges(numbers: list[int]) -> str:
|
|
228
|
+
ranges: list[str] = []
|
|
229
|
+
for number in numbers:
|
|
230
|
+
if ranges and number == int(ranges[-1].split("-")[-1]) + 1:
|
|
231
|
+
ranges[-1] = f"{ranges[-1].split('-')[0]}-{number}"
|
|
232
|
+
else:
|
|
233
|
+
ranges.append(str(number))
|
|
234
|
+
return ", ".join(ranges)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _check_lines(args: argparse.Namespace, project_root: Path) -> int:
|
|
238
|
+
if not (args.base and args.head and args.cite):
|
|
239
|
+
print("okstra code-review check-lines: pass --base, --head and at least one --cite",
|
|
240
|
+
file=sys.stderr)
|
|
241
|
+
return 2
|
|
242
|
+
try:
|
|
243
|
+
changed = changed_lines(project_root, args.base, args.head)
|
|
244
|
+
except StateError as exc:
|
|
245
|
+
print(f"okstra code-review check-lines: {exc}", file=sys.stderr)
|
|
246
|
+
return 2
|
|
247
|
+
text, all_ok = render_check_lines(changed, args.cite)
|
|
248
|
+
print(text, end="")
|
|
249
|
+
return 0 if all_ok else 1
|
|
250
|
+
|
|
251
|
+
|
|
173
252
|
def main(argv: list[str] | None = None) -> int:
|
|
174
253
|
args = _build_parser().parse_args(argv)
|
|
175
254
|
try:
|
|
@@ -181,8 +260,15 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
181
260
|
print(render_code_review_target_text(payload) if args.text else json.dumps(payload))
|
|
182
261
|
return 2
|
|
183
262
|
|
|
263
|
+
if args.command == "check-lines":
|
|
264
|
+
return _check_lines(args, Path(project_root))
|
|
184
265
|
try:
|
|
185
266
|
snapshot = _resolve(args, Path(project_root))
|
|
267
|
+
snapshot["reportLanguage"] = resolve_report_language(Path(project_root), {}, {})
|
|
268
|
+
except ReportLanguageError as exc:
|
|
269
|
+
payload = {"ok": False, "stage": "report-language", "reason": str(exc)}
|
|
270
|
+
print(render_code_review_target_text(payload) if args.text else json.dumps(payload))
|
|
271
|
+
return 1
|
|
186
272
|
except StateError as exc:
|
|
187
273
|
payload = {
|
|
188
274
|
"ok": False,
|
|
@@ -430,3 +430,15 @@ def backfill_done_from_carry(plan_run_root: Path) -> int:
|
|
|
430
430
|
settled_stages.add(stage)
|
|
431
431
|
recovered += 1
|
|
432
432
|
return recovered
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def require_done_task_key(rows: List[Dict[str, Any]], stage: int) -> str:
|
|
436
|
+
from .handoff_error import HandoffError
|
|
437
|
+
|
|
438
|
+
done = latest_done_by_stage(rows)
|
|
439
|
+
row = done.get(stage)
|
|
440
|
+
if not row:
|
|
441
|
+
raise HandoffError(
|
|
442
|
+
f"stage {stage} has no done row in consumers.jsonl — "
|
|
443
|
+
"finish the implementation stage first")
|
|
444
|
+
return row.get("impl_task_key", "")
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""워커를 띄우고 거두는 명령이 대신 기록하는 리드 체크포인트.
|
|
2
|
+
|
|
3
|
+
dispatch 를 정하고 결과를 확정하는 곳은 `team dispatch` / `team await`(pane)와
|
|
4
|
+
`worker-dispatch`(CLI, 블로킹)이다. 그래서 `phase-3-team-create`,
|
|
5
|
+
`phase-4-dispatch`, `phase-5-collect`, `phase-6-synthesis` 가 말하는 사실을 이
|
|
6
|
+
명령들이 갖고 있다. 리드가 같은 사실을 `okstra lead-progress append` 로 다시
|
|
7
|
+
적으면 체크포인트마다 전체 컨텍스트를 한 턴씩 쓰는데, 여기서 기록하면 턴이 들지 않는다.
|
|
8
|
+
|
|
9
|
+
워커별 체크포인트는 `initial` dispatch 만 센다. reverify·critic·plan-verify 는
|
|
10
|
+
리드가 따로 알리는 이후 라운드이고, 여기서 기록하면 activity-contract run 이
|
|
11
|
+
존재하지 않는 `worker-dispatched` / `worker-completed` 행을 요구받게 된다.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Mapping, Sequence
|
|
17
|
+
|
|
18
|
+
from .dispatch_state import (
|
|
19
|
+
TERMINAL_WORKER_STATUSES,
|
|
20
|
+
dispatch_worker_key,
|
|
21
|
+
has_recorded_team_create,
|
|
22
|
+
worker_dispatch_records,
|
|
23
|
+
)
|
|
24
|
+
from .lead_progress import record_checkpoint
|
|
25
|
+
from .worker_prompt_body import REPORT_WRITER_WORKER_ID
|
|
26
|
+
|
|
27
|
+
_INITIAL = "initial"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def record_dispatch_checkpoints(
|
|
31
|
+
project_root: Path,
|
|
32
|
+
run_manifest: str | Path,
|
|
33
|
+
team_state: Mapping[str, Any],
|
|
34
|
+
jobs: Sequence[Any],
|
|
35
|
+
*,
|
|
36
|
+
source: str,
|
|
37
|
+
) -> list[str]:
|
|
38
|
+
"""곧 띄울 배치의 체크포인트. 배치가 시작되기 전의 team-state 로 판정한다.
|
|
39
|
+
|
|
40
|
+
`phase-3-team-create` 는 이 dispatch 가 implicit-team 마커를 쓸 때만 기록한다
|
|
41
|
+
(`dispatch_state.record_dispatch_facts`). 리드가 미리 적은 마커(concurrent-run
|
|
42
|
+
`skipped` 결정)는 리드가 알린다. `phase-6-synthesis` 는 run 의 첫 report-writer
|
|
43
|
+
dispatch 에서만 기록해서, 교정 라운드가 Phase 6 을 다시 열지 않게 한다.
|
|
44
|
+
"""
|
|
45
|
+
if not jobs:
|
|
46
|
+
return []
|
|
47
|
+
lines: list[str | None] = []
|
|
48
|
+
if not has_recorded_team_create(team_state):
|
|
49
|
+
lines.append(record_checkpoint(
|
|
50
|
+
project_root, run_manifest, "phase-3-team-create", source=source,
|
|
51
|
+
))
|
|
52
|
+
for job in jobs:
|
|
53
|
+
if job.dispatch_kind == _INITIAL:
|
|
54
|
+
lines.append(record_checkpoint(
|
|
55
|
+
project_root, run_manifest, "phase-4-dispatch", source=source,
|
|
56
|
+
worker=job.worker_id,
|
|
57
|
+
fields=(("model", job.model_execution_value),),
|
|
58
|
+
))
|
|
59
|
+
dispatches_writer = any(job.worker_id == REPORT_WRITER_WORKER_ID for job in jobs)
|
|
60
|
+
writer_dispatched_before = bool(
|
|
61
|
+
worker_dispatch_records(team_state, REPORT_WRITER_WORKER_ID)
|
|
62
|
+
)
|
|
63
|
+
if dispatches_writer and not writer_dispatched_before:
|
|
64
|
+
lines.append(record_checkpoint(
|
|
65
|
+
project_root, run_manifest, "phase-6-synthesis", source=source,
|
|
66
|
+
))
|
|
67
|
+
return [line for line in lines if line]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def settled_initial_dispatches(
|
|
71
|
+
team_state: Mapping[str, Any],
|
|
72
|
+
) -> dict[str, Mapping[str, Any]]:
|
|
73
|
+
"""최신 attempt 가 종결 상태에 이른 `initial` dispatch 들.
|
|
74
|
+
|
|
75
|
+
재시도해도 바뀌지 않는 prompt 경로를 키로 쓴다. 실패 후 재시도 중인 attempt 는
|
|
76
|
+
재시도까지 끝나야 거둔 결과로 친다.
|
|
77
|
+
"""
|
|
78
|
+
latest: dict[str, Mapping[str, Any]] = {}
|
|
79
|
+
for record in worker_dispatch_records(team_state):
|
|
80
|
+
if record.get("kind") != _INITIAL:
|
|
81
|
+
continue
|
|
82
|
+
key = str(record.get("promptPath") or "")
|
|
83
|
+
current = latest.get(key)
|
|
84
|
+
if current is None or _attempt(record) >= _attempt(current):
|
|
85
|
+
latest[key] = record
|
|
86
|
+
return {
|
|
87
|
+
key: record
|
|
88
|
+
for key, record in latest.items()
|
|
89
|
+
if record.get("status") in TERMINAL_WORKER_STATUSES
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def record_collect_checkpoints(
|
|
94
|
+
project_root: Path,
|
|
95
|
+
run_manifest: str | Path,
|
|
96
|
+
before: Mapping[str, Mapping[str, Any]],
|
|
97
|
+
after: Mapping[str, Mapping[str, Any]],
|
|
98
|
+
*,
|
|
99
|
+
source: str,
|
|
100
|
+
) -> list[str]:
|
|
101
|
+
"""이 명령이 확정한 dispatch 마다 `phase-5-collect` 를 기록한다.
|
|
102
|
+
|
|
103
|
+
확정이 곧 체크포인트가 알리는 결과 검증이다: 필요한 산출물이 있고, 변경 감사를
|
|
104
|
+
통과했고, 결과가 dispatch 에 연결됐거나(`dispatch_core._finish_attempt`),
|
|
105
|
+
attempt 가 `error` / `timeout` 으로 닫혔다.
|
|
106
|
+
"""
|
|
107
|
+
lines = []
|
|
108
|
+
for key, record in after.items():
|
|
109
|
+
previous = before.get(key)
|
|
110
|
+
if previous is not None and _attempt(previous) == _attempt(record):
|
|
111
|
+
continue
|
|
112
|
+
lines.append(record_checkpoint(
|
|
113
|
+
project_root, run_manifest, "phase-5-collect", source=source,
|
|
114
|
+
worker=dispatch_worker_key(record),
|
|
115
|
+
fields=(("status", str(record.get("status"))),),
|
|
116
|
+
))
|
|
117
|
+
return [line for line in lines if line]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _attempt(record: Mapping[str, Any]) -> int:
|
|
121
|
+
return int(record.get("attempt", 1))
|
|
@@ -9,7 +9,7 @@ from dataclasses import dataclass, field, replace
|
|
|
9
9
|
from datetime import datetime, timezone
|
|
10
10
|
import os
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import Any, Mapping, Sequence
|
|
12
|
+
from typing import Any, Callable, Mapping, Sequence
|
|
13
13
|
|
|
14
14
|
from .dispatch_state import abandon_agent_dispatch_attempt
|
|
15
15
|
from .agent.invocation import invocation_input_digest
|
|
@@ -33,7 +33,7 @@ from .dispatch_state import (
|
|
|
33
33
|
link_agent_dispatch_result as _link_agent_dispatch_result,
|
|
34
34
|
missing_completion_paths as _missing_completion_paths,
|
|
35
35
|
unusable_result_defect,
|
|
36
|
-
|
|
36
|
+
dispatch_worker_key,
|
|
37
37
|
_plan_verify_result_aliases,
|
|
38
38
|
mutate_team_state as _mutate_team_state,
|
|
39
39
|
require_string as _require_string,
|
|
@@ -408,7 +408,17 @@ def build_dispatch_plan(
|
|
|
408
408
|
)
|
|
409
409
|
|
|
410
410
|
|
|
411
|
-
|
|
411
|
+
BeforeStart = Callable[["DispatchPlan"], None]
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def dispatch_plan(
|
|
415
|
+
plan: DispatchPlan, *, wait: bool = True, before_start: BeforeStart | None = None,
|
|
416
|
+
) -> int:
|
|
417
|
+
"""`before_start` 는 복구·검증을 통과한 plan 으로, 워커를 띄우기 직전에 한 번 불린다.
|
|
418
|
+
|
|
419
|
+
거부된 배치와 증거 보완으로 빠진 작업은 여기까지 오지 않으므로, 띄울 워커에
|
|
420
|
+
대한 사실(체크포인트)은 이 시점에 기록해야 실제로 뜬 워커만 남는다.
|
|
421
|
+
"""
|
|
412
422
|
plan = _recover_declaration_failures(plan)
|
|
413
423
|
if not plan.jobs:
|
|
414
424
|
return 0
|
|
@@ -416,21 +426,19 @@ def dispatch_plan(plan: DispatchPlan, *, wait: bool = True) -> int:
|
|
|
416
426
|
_validate_report_writer_isolation(plan.jobs)
|
|
417
427
|
_validate_implementation_phase_order(plan.jobs)
|
|
418
428
|
_revalidate_plan_role_executions(plan)
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
429
|
+
pane_backends = sorted(
|
|
430
|
+
{job.backend for job in plan.jobs if job.backend == BACKEND_CMUX_PANE}
|
|
431
|
+
)
|
|
432
|
+
if wait and pane_backends:
|
|
433
|
+
raise DispatchError(
|
|
434
|
+
f"wait=True dispatch does not support {'/'.join(pane_backends)} "
|
|
435
|
+
"workers: the per-job blocking loop would serialize panes "
|
|
436
|
+
"instead of running them concurrently; dispatch panes with "
|
|
437
|
+
"wait=False"
|
|
426
438
|
)
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
"workers: the per-job blocking loop would serialize panes "
|
|
431
|
-
"instead of running them concurrently; dispatch panes with "
|
|
432
|
-
"wait=False"
|
|
433
|
-
)
|
|
439
|
+
if before_start is not None:
|
|
440
|
+
before_start(plan)
|
|
441
|
+
if wait:
|
|
434
442
|
_record_dispatch_facts(plan.team_state_path, _dispatch_mode(plan.jobs))
|
|
435
443
|
for job in plan.jobs:
|
|
436
444
|
result = _dispatch_job_with_retry(plan, job)
|
|
@@ -509,7 +517,9 @@ def _round_artifact_paths(plan: DispatchPlan) -> tuple[Path, ...]:
|
|
|
509
517
|
)
|
|
510
518
|
|
|
511
519
|
|
|
512
|
-
def dispatch_cli_wrapper_plan(
|
|
520
|
+
def dispatch_cli_wrapper_plan(
|
|
521
|
+
plan: DispatchPlan, *, before_start: BeforeStart | None = None,
|
|
522
|
+
) -> int:
|
|
513
523
|
"""Start one dependency-free CLI batch before collecting any worker."""
|
|
514
524
|
if any(job.backend != BACKEND_CLI_WRAPPER for job in plan.jobs):
|
|
515
525
|
raise DispatchError("concurrent CLI dispatch requires cli-wrapper jobs only")
|
|
@@ -520,6 +530,8 @@ def dispatch_cli_wrapper_plan(plan: DispatchPlan) -> int:
|
|
|
520
530
|
_validate_report_writer_isolation(plan.jobs)
|
|
521
531
|
_validate_implementation_phase_order(plan.jobs)
|
|
522
532
|
_revalidate_plan_role_executions(plan)
|
|
533
|
+
if before_start is not None:
|
|
534
|
+
before_start(plan)
|
|
523
535
|
_record_dispatch_facts(plan.team_state_path, _dispatch_mode(plan.jobs))
|
|
524
536
|
final_codes = _dispatch_cli_wrapper_batch(plan, plan.jobs)
|
|
525
537
|
return next((final_codes[job.worker_id] for job in plan.jobs
|
|
@@ -1855,6 +1867,7 @@ def _canonical_write_contract(
|
|
|
1855
1867
|
try:
|
|
1856
1868
|
return build_invocation_write_contract(
|
|
1857
1869
|
role=execution.role,
|
|
1870
|
+
task_type=str(plan.manifest.get("taskType") or ""),
|
|
1858
1871
|
project_root=plan.project_root,
|
|
1859
1872
|
worktree=worktree,
|
|
1860
1873
|
artifact_paths=artifacts,
|
|
@@ -2269,14 +2282,23 @@ def _mutation_failure_reason(mutation, outcome: WorkerOutcome | None = None) ->
|
|
|
2269
2282
|
failure = _wrapper_failure_text(outcome)
|
|
2270
2283
|
if failure:
|
|
2271
2284
|
reason = f"{reason}: {failure}"
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2285
|
+
# 산출물 위반은 소스 변경 목록에 없다 — 그 목록만 보이면 정책 안에 있던
|
|
2286
|
+
# 소스 파일을 범인으로 읽는다 (실측 2026-09-26, dev-11054 `qa/baseline/`).
|
|
2287
|
+
for label, paths in (
|
|
2288
|
+
("outside artifact policy", mutation.violating_artifact_paths),
|
|
2289
|
+
("changed", mutation.changed_paths),
|
|
2290
|
+
):
|
|
2291
|
+
if paths:
|
|
2292
|
+
reason += f" [{label}: {_capped_path_list(paths)}]"
|
|
2293
|
+
return reason
|
|
2294
|
+
|
|
2295
|
+
|
|
2296
|
+
def _capped_path_list(paths: Sequence[str]) -> str:
|
|
2275
2297
|
shown = ", ".join(paths[:_MUTATION_REASON_PATH_LIMIT])
|
|
2276
2298
|
remaining = len(paths) - _MUTATION_REASON_PATH_LIMIT
|
|
2277
2299
|
if remaining > 0:
|
|
2278
2300
|
shown += f", +{remaining} more"
|
|
2279
|
-
return
|
|
2301
|
+
return shown
|
|
2280
2302
|
|
|
2281
2303
|
|
|
2282
2304
|
def _finish_attempt(
|
|
@@ -2751,7 +2773,7 @@ def _record_wrapper_failure(
|
|
|
2751
2773
|
out_path=out_path,
|
|
2752
2774
|
task_key=_string_value(plan.manifest.get("taskKey")),
|
|
2753
2775
|
phase=_workflow_phase(plan.manifest),
|
|
2754
|
-
agent=_error_log_agent(job
|
|
2776
|
+
agent=_error_log_agent(job),
|
|
2755
2777
|
agent_role=(
|
|
2756
2778
|
"report-writer"
|
|
2757
2779
|
if job.worker_id == REPORT_WRITER_WORKER_ID
|
|
@@ -2790,16 +2812,16 @@ def _job_stored_identity(job: WorkerJob, attempt: int) -> dict[str, Any]:
|
|
|
2790
2812
|
}
|
|
2791
2813
|
|
|
2792
2814
|
|
|
2793
|
-
def _error_log_agent(
|
|
2794
|
-
"""The error log's `--agent` enum value for a
|
|
2815
|
+
def _error_log_agent(job: WorkerJob) -> str:
|
|
2816
|
+
"""The error log's `--agent` enum value for a job.
|
|
2795
2817
|
|
|
2796
|
-
The
|
|
2797
|
-
|
|
2798
|
-
|
|
2818
|
+
The enum is keyed by provider (`codex-worker`), not by worker id: a role
|
|
2819
|
+
worker id such as `codex-verifier` produced `codex-verifier-worker`, which
|
|
2820
|
+
the allow-list rejected, so no wrapper failure reached the log.
|
|
2799
2821
|
"""
|
|
2800
|
-
if worker_id == REPORT_WRITER_WORKER_ID:
|
|
2822
|
+
if job.worker_id == REPORT_WRITER_WORKER_ID:
|
|
2801
2823
|
return REPORT_WRITER_WORKER_ID
|
|
2802
|
-
return f"{
|
|
2824
|
+
return f"{job.provider}-worker"
|
|
2803
2825
|
|
|
2804
2826
|
|
|
2805
2827
|
def _workflow_phase(manifest: Mapping[str, Any]) -> str:
|
|
@@ -3349,7 +3371,7 @@ def _record_missing_completion_paths(record: Mapping[str, Any]) -> tuple[Path, .
|
|
|
3349
3371
|
result_path = Path(_string_value(record.get("resultPath")))
|
|
3350
3372
|
worker_result = Path(_string_value(record.get("workerResultPath")))
|
|
3351
3373
|
missing: list[Path] = []
|
|
3352
|
-
worker_id =
|
|
3374
|
+
worker_id = dispatch_worker_key(record)
|
|
3353
3375
|
dispatch_kind = _string_value(record.get("dispatchKind"))
|
|
3354
3376
|
for path in _record_completion_paths(record):
|
|
3355
3377
|
if path.is_file():
|
|
@@ -3366,7 +3388,7 @@ def _record_missing_completion_paths(record: Mapping[str, Any]) -> tuple[Path, .
|
|
|
3366
3388
|
|
|
3367
3389
|
def _record_artifact_defects(record: Mapping[str, Any]) -> tuple[str, ...]:
|
|
3368
3390
|
defect = unusable_result_defect(
|
|
3369
|
-
|
|
3391
|
+
dispatch_worker_key(record),
|
|
3370
3392
|
Path(_string_value(record.get("resultPath"))),
|
|
3371
3393
|
_string_value(record.get("dispatchKind")),
|
|
3372
3394
|
)
|
|
@@ -59,6 +59,7 @@ from .worker_prompt_body import REPORT_WRITER_WORKER_ID
|
|
|
59
59
|
from .worker_prompt_contract import (
|
|
60
60
|
PromptRecord,
|
|
61
61
|
validate_initial_prompt_records,
|
|
62
|
+
validate_plan_verify_dispatch_identity,
|
|
62
63
|
validate_prompt_model_header,
|
|
63
64
|
validate_reverify_prompt,
|
|
64
65
|
)
|
|
@@ -364,6 +365,12 @@ def resolve_required_path(
|
|
|
364
365
|
def worktree_path(
|
|
365
366
|
manifest: Mapping[str, Any], active_context: Mapping[str, Any]
|
|
366
367
|
) -> str:
|
|
368
|
+
# Must match the prompt's `**Worktree:**` anchor
|
|
369
|
+
# (`worker_prompt_headers._final_verification_target_headers`).
|
|
370
|
+
if manifest.get("taskType") == "final-verification":
|
|
371
|
+
target = active_context.get("verificationTarget")
|
|
372
|
+
if isinstance(target, Mapping) and target.get("worktreePath"):
|
|
373
|
+
return str(target["worktreePath"])
|
|
367
374
|
worktree = active_context.get("executorWorktree")
|
|
368
375
|
if not isinstance(worktree, Mapping):
|
|
369
376
|
return ""
|
|
@@ -861,6 +868,7 @@ def _agent_write_contract(
|
|
|
861
868
|
try:
|
|
862
869
|
return build_invocation_write_contract(
|
|
863
870
|
role=execution.role,
|
|
871
|
+
task_type=str(authority.get("taskType") or ""),
|
|
864
872
|
project_root=project_root,
|
|
865
873
|
worktree=worktree,
|
|
866
874
|
artifact_paths=artifacts,
|
|
@@ -1625,12 +1633,12 @@ def record_dispatch_facts(team_state_path: Path, dispatch_mode: str) -> None:
|
|
|
1625
1633
|
with _team_state_lock(team_state_path):
|
|
1626
1634
|
payload = load_json_object(team_state_path, "team-state")
|
|
1627
1635
|
payload["dispatchMode"] = dispatch_mode
|
|
1628
|
-
if not
|
|
1636
|
+
if not has_recorded_team_create(payload):
|
|
1629
1637
|
payload["teamCreate"] = {"attempted": False, "status": "implicit"}
|
|
1630
1638
|
write_json(team_state_path, payload)
|
|
1631
1639
|
|
|
1632
1640
|
|
|
1633
|
-
def
|
|
1641
|
+
def has_recorded_team_create(team_state: Mapping[str, Any]) -> bool:
|
|
1634
1642
|
existing = team_state.get("teamCreate")
|
|
1635
1643
|
return (isinstance(existing, dict)
|
|
1636
1644
|
and bool(str(existing.get("status") or "").strip()))
|
|
@@ -1770,7 +1778,7 @@ def worker_dispatch_records(
|
|
|
1770
1778
|
집계, v2 명부 행 `scope` 는 `promptPath` 가 비어 0회).
|
|
1771
1779
|
|
|
1772
1780
|
v1 행은 `workerId`, v2 행은 `assignmentRef` 마지막 마디로 워커에 묶인다
|
|
1773
|
-
(`
|
|
1781
|
+
(`dispatch_worker_key`). 리스트가 아니면 빈 결과 — `worker_session_ids`
|
|
1774
1782
|
와 같은 이유로 호출자를 깨지 않는다.
|
|
1775
1783
|
"""
|
|
1776
1784
|
dispatches = team_state.get("workerDispatches")
|
|
@@ -1778,13 +1786,13 @@ def worker_dispatch_records(
|
|
|
1778
1786
|
for record in dispatches if isinstance(dispatches, list) else []:
|
|
1779
1787
|
if not isinstance(record, Mapping):
|
|
1780
1788
|
continue
|
|
1781
|
-
if worker_id is not None and
|
|
1789
|
+
if worker_id is not None and dispatch_worker_key(record) != worker_id:
|
|
1782
1790
|
continue
|
|
1783
1791
|
records.append(record)
|
|
1784
1792
|
return records
|
|
1785
1793
|
|
|
1786
1794
|
|
|
1787
|
-
def
|
|
1795
|
+
def dispatch_worker_key(record: Mapping[str, Any]) -> str:
|
|
1788
1796
|
"""The `workers[]` row a dispatch record belongs to.
|
|
1789
1797
|
|
|
1790
1798
|
A v1 record names it as `workerId`. A v2 record carries execution identity
|
|
@@ -1905,6 +1913,26 @@ def validate_initial_prompts(
|
|
|
1905
1913
|
raise DispatchError(f"{task_type} prompt contract: " + "; ".join(errors))
|
|
1906
1914
|
|
|
1907
1915
|
|
|
1916
|
+
def _validate_plan_verify_identities(jobs: Sequence[WorkerJob]) -> None:
|
|
1917
|
+
errors: list[str] = []
|
|
1918
|
+
for job in jobs:
|
|
1919
|
+
try:
|
|
1920
|
+
text = job.prompt_path.read_text(encoding="utf-8")
|
|
1921
|
+
except OSError as exc:
|
|
1922
|
+
errors.append(f"{job.worker_id}: cannot read prompt {job.prompt_path}: {exc}")
|
|
1923
|
+
continue
|
|
1924
|
+
errors.extend(
|
|
1925
|
+
f"{job.worker_id}: {error}"
|
|
1926
|
+
for error in validate_plan_verify_dispatch_identity(
|
|
1927
|
+
text,
|
|
1928
|
+
dispatch_kind=job.dispatch_kind,
|
|
1929
|
+
result_name=job.worker_result_path.name,
|
|
1930
|
+
)
|
|
1931
|
+
)
|
|
1932
|
+
if errors:
|
|
1933
|
+
raise DispatchError("plan-body dispatch contract: " + "; ".join(errors))
|
|
1934
|
+
|
|
1935
|
+
|
|
1908
1936
|
def validate_dispatch_prompts(
|
|
1909
1937
|
manifest: Mapping[str, Any],
|
|
1910
1938
|
active_context: Mapping[str, Any],
|
|
@@ -1912,6 +1940,7 @@ def validate_dispatch_prompts(
|
|
|
1912
1940
|
) -> None:
|
|
1913
1941
|
if isinstance(manifest.get("agentContract"), Mapping):
|
|
1914
1942
|
_validate_agent_invocations(manifest, jobs)
|
|
1943
|
+
_validate_plan_verify_identities(jobs)
|
|
1915
1944
|
initial_jobs = [
|
|
1916
1945
|
job for job in jobs if not is_verification_dispatch_kind(job.dispatch_kind)
|
|
1917
1946
|
]
|
|
@@ -11,7 +11,8 @@ from typing import Iterable
|
|
|
11
11
|
from okstra_project import ResolverError, project_json_path, resolve_project_root
|
|
12
12
|
from okstra_project.resolver import resolve_review_rule_packs
|
|
13
13
|
|
|
14
|
-
from . import
|
|
14
|
+
from . import worktree_registry
|
|
15
|
+
from .phases.improvement_discovery import lenses as improvement_lenses
|
|
15
16
|
from .models import provider_wrappers
|
|
16
17
|
from .phases.catalog import PhaseAssetError, UnknownTaskType, profile_markdown
|
|
17
18
|
from .workers import resolve_profile_workers
|
|
@@ -65,6 +65,9 @@ class ModelSpec:
|
|
|
65
65
|
supported_roles: frozenset[str] | None
|
|
66
66
|
selectable: bool
|
|
67
67
|
pricing: Optional[tuple[float, float, float]]
|
|
68
|
+
# 이 모델을 실행할 때마다 공급자 CLI 에 덧붙는 인자. 사용자 CLI 설정에 맡기면
|
|
69
|
+
# 기계마다 달라지는 값(예: reasoning effort)을 카탈로그가 고정한다.
|
|
70
|
+
cli_args: tuple[str, ...]
|
|
68
71
|
|
|
69
72
|
def __init__(
|
|
70
73
|
self,
|
|
@@ -79,6 +82,7 @@ class ModelSpec:
|
|
|
79
82
|
pricing: Optional[tuple[float, float, float]] = None,
|
|
80
83
|
*,
|
|
81
84
|
in_picker: bool | None = None,
|
|
85
|
+
cli_args: tuple[str, ...] = (),
|
|
82
86
|
) -> None:
|
|
83
87
|
"""Accept the former display/execution form while adapters migrate."""
|
|
84
88
|
is_legacy = execution_value is _MISSING or not isinstance(execution_value, str)
|
|
@@ -102,6 +106,7 @@ class ModelSpec:
|
|
|
102
106
|
object.__setattr__(self, "supported_roles", supported_roles)
|
|
103
107
|
object.__setattr__(self, "selectable", selectable if in_picker is None else in_picker)
|
|
104
108
|
object.__setattr__(self, "pricing", resolved_pricing)
|
|
109
|
+
object.__setattr__(self, "cli_args", cli_args)
|
|
105
110
|
|
|
106
111
|
@property
|
|
107
112
|
def display(self) -> str:
|
|
@@ -93,13 +93,18 @@ class SplitText:
|
|
|
93
93
|
가야 하므로 종결 텍스트로 돌려주고, 진행은 기록과 화면에만 남는다.
|
|
94
94
|
"""
|
|
95
95
|
|
|
96
|
+
_answer: list[str] = field(default_factory=list, repr=False, compare=False)
|
|
97
|
+
|
|
96
98
|
def merges_stderr(self) -> bool:
|
|
97
99
|
return False
|
|
98
100
|
|
|
99
101
|
def sinks(self, writer: TranscriptWriter) -> tuple[SinkSpec, ...]:
|
|
100
102
|
def result(line: str) -> str | None:
|
|
101
103
|
writer.write(WORKER, line)
|
|
102
|
-
|
|
104
|
+
# 러너는 마지막으로 돌려받은 값만 호출자에게 넘기므로, 그 줄이 아니라
|
|
105
|
+
# 지금까지의 답 전체를 돌려준다.
|
|
106
|
+
self._answer.append(strip_terminal_colors(line))
|
|
107
|
+
return "\n".join(self._answer).strip("\n") or None
|
|
103
108
|
|
|
104
109
|
def progress(line: str) -> str | None:
|
|
105
110
|
writer.write(WORKER, line)
|
|
@@ -140,6 +145,9 @@ class JsonEvents:
|
|
|
140
145
|
_saw_conclusion: list[bool] = field(
|
|
141
146
|
default_factory=list, repr=False, compare=False
|
|
142
147
|
)
|
|
148
|
+
# 마지막 도구 호출 뒤에 온 산문. Result 없는 어휘에서는 이것이 최종 답이다.
|
|
149
|
+
_trailing_text: list[str] = field(default_factory=list, repr=False, compare=False)
|
|
150
|
+
_saw_result: list[bool] = field(default_factory=list, repr=False, compare=False)
|
|
143
151
|
|
|
144
152
|
def merges_stderr(self) -> bool:
|
|
145
153
|
return True
|
|
@@ -164,11 +172,17 @@ class JsonEvents:
|
|
|
164
172
|
if not isinstance(event, dict):
|
|
165
173
|
return None
|
|
166
174
|
self.observe(event)
|
|
167
|
-
|
|
175
|
+
concludes = self.is_conclusion is not None and self.is_conclusion(event)
|
|
176
|
+
if concludes:
|
|
168
177
|
self._saw_conclusion.append(True)
|
|
169
178
|
closing: str | None = None
|
|
170
179
|
for entry in self.normalise(event):
|
|
171
180
|
closing = self._write_entry(writer, entry) or closing
|
|
181
|
+
if concludes and closing is None and not self._saw_result:
|
|
182
|
+
# 종결 표지가 본문을 싣지 않는 어휘다. 돌려줄 것이
|
|
183
|
+
# 없으면 quiet 호출자의 stdout 이 비고 답은 로그에만 남는다.
|
|
184
|
+
joiner = "" if self.join_adjacent_text else "\n"
|
|
185
|
+
closing = joiner.join(self._trailing_text).strip() or None
|
|
172
186
|
return closing
|
|
173
187
|
|
|
174
188
|
return (("stdout", emit),)
|
|
@@ -179,6 +193,10 @@ class JsonEvents:
|
|
|
179
193
|
def _write_entry(
|
|
180
194
|
self, writer: TranscriptWriter, entry: StreamEvent
|
|
181
195
|
) -> str | None:
|
|
196
|
+
if isinstance(entry, Text):
|
|
197
|
+
self._trailing_text.append(entry.body)
|
|
198
|
+
else:
|
|
199
|
+
self._trailing_text.clear()
|
|
182
200
|
if isinstance(entry, Text) and self.join_adjacent_text:
|
|
183
201
|
self._pending_text.append(entry.body)
|
|
184
202
|
self._publish_complete_lines(writer)
|
|
@@ -197,6 +215,7 @@ class JsonEvents:
|
|
|
197
215
|
# Result 이벤트는 어휘와 무관한 종결이다. 빈 결과 텍스트도 종결이므로
|
|
198
216
|
# closing_text 의 진릿값과 따로 기록한다.
|
|
199
217
|
self._saw_conclusion.append(True)
|
|
218
|
+
self._saw_result.append(True)
|
|
200
219
|
return closing
|
|
201
220
|
|
|
202
221
|
def _publish_complete_lines(self, writer: TranscriptWriter) -> None:
|