okstra 0.186.7 → 0.188.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 +1 -1
- package/dist/cli-registry.d.mts +88 -1
- package/dist/cli-registry.mjs +68 -111
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.mjs +0 -1
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/run.mjs +8 -3
- package/dist/commands/execute/run.mjs.map +1 -1
- package/dist/commands/lifecycle/check-project.mjs +1 -14
- package/dist/commands/lifecycle/check-project.mjs.map +1 -1
- package/dist/commands/lifecycle/config.mjs +38 -40
- package/dist/commands/lifecycle/config.mjs.map +1 -1
- package/dist/commands/lifecycle/doctor.d.mts +22 -7
- package/dist/commands/lifecycle/doctor.mjs +77 -49
- package/dist/commands/lifecycle/doctor.mjs.map +1 -1
- package/dist/commands/lifecycle/install.d.mts +12 -10
- package/dist/commands/lifecycle/install.mjs +107 -40
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/dist/commands/lifecycle/paths.mjs +8 -3
- package/dist/commands/lifecycle/paths.mjs.map +1 -1
- package/dist/commands/lifecycle/preflight.mjs +2 -1
- package/dist/commands/lifecycle/preflight.mjs.map +1 -1
- package/dist/commands/lifecycle/setup.mjs +22 -36
- package/dist/commands/lifecycle/setup.mjs.map +1 -1
- package/dist/commands/lifecycle/uninstall.d.mts +4 -2
- package/dist/commands/lifecycle/uninstall.mjs +59 -15
- package/dist/commands/lifecycle/uninstall.mjs.map +1 -1
- package/dist/commands/memory/memory.mjs +7 -1
- package/dist/commands/memory/memory.mjs.map +1 -1
- package/dist/lib/helper-scripts.d.mts +1 -1
- package/dist/lib/helper-scripts.mjs +10 -18
- package/dist/lib/helper-scripts.mjs.map +1 -1
- package/dist/lib/host-config.d.mts +72 -0
- package/dist/lib/host-config.mjs +404 -0
- package/dist/lib/host-config.mjs.map +1 -0
- package/dist/lib/host-registry-client.d.mts +2 -2
- package/dist/lib/host-registry-client.mjs +0 -3
- package/dist/lib/host-registry-client.mjs.map +1 -1
- package/dist/lib/install-assets.d.mts +1 -0
- package/dist/lib/install-assets.mjs +4 -0
- package/dist/lib/install-assets.mjs.map +1 -1
- package/dist/lib/proc.d.mts +2 -0
- package/dist/lib/proc.mjs +12 -0
- package/dist/lib/proc.mjs.map +1 -1
- package/dist/lib/python-command.d.mts +2 -0
- package/dist/lib/python-command.mjs +52 -0
- package/dist/lib/python-command.mjs.map +1 -0
- package/dist/lib/python-helper.d.mts +2 -5
- package/dist/lib/python-helper.mjs +3 -52
- package/dist/lib/python-helper.mjs.map +1 -1
- package/dist/lib/runtime-payload.d.mts +23 -0
- package/dist/lib/runtime-payload.mjs +57 -0
- package/dist/lib/runtime-payload.mjs.map +1 -0
- package/dist/lib/types.d.mts +24 -13
- package/docs/architecture/storage-model.md +21 -5
- package/docs/architecture.md +39 -38
- package/docs/cli.md +55 -76
- package/docs/coding-rules.md +295 -0
- package/docs/container.md +10 -36
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/for-ai/skills/okstra-code-review.md +0 -1
- package/docs/for-ai/skills/okstra-container-build.md +8 -41
- package/docs/for-ai/skills/okstra-inspect.md +4 -4
- package/docs/for-ai/skills/okstra-manager.md +2 -2
- package/docs/for-ai/skills/okstra-rollup.md +0 -1
- package/docs/for-ai/skills/okstra-run.md +3 -3
- package/docs/for-ai/skills/okstra-user-response.md +0 -1
- package/docs/performance-improvement-plan-v2.md +1 -1
- package/docs/project-structure-overview.md +62 -62
- package/docs/task-process/README.md +3 -3
- package/docs/task-process/common-flow.md +1 -1
- package/docs/task-process/error-analysis.md +3 -3
- package/docs/task-process/implementation-planning.md +3 -1
- package/docs/task-process/release-handoff.md +1 -1
- package/package.json +3 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +6 -11
- package/runtime/agents/workers/report-writer-worker.md +1 -1
- package/runtime/bin/lib/okstra/cli.sh +4 -0
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/interactive.sh +26 -173
- package/runtime/bin/lib/okstra/project-resolver.sh +23 -61
- package/runtime/bin/lib/okstra/usage.sh +3 -3
- package/runtime/bin/okstra-compact-reminder.sh +1 -5
- package/runtime/bin/okstra-error-log.py +19 -2
- package/runtime/bin/okstra-import-check.py +26 -0
- package/runtime/bin/okstra-inject-report-index.py +5 -4
- package/runtime/bin/okstra-provider-exec.py +22 -17
- package/runtime/bin/okstra-render-final-report.py +16 -6
- package/runtime/bin/okstra-render-report-views.py +47 -22
- package/runtime/bin/okstra-report-translate.py +30 -18
- package/runtime/bin/okstra-spawn-followups.py +4 -0
- package/runtime/bin/okstra-token-usage.py +3 -0
- package/runtime/bin/okstra.sh +4 -2
- package/runtime/bin/okstra_bootstrap.py +58 -0
- package/runtime/prompts/coding-preflight/overview.md +1 -1
- package/runtime/prompts/duties/planning-worker.md +1 -1
- package/runtime/prompts/launch.template.md +35 -9
- package/runtime/prompts/lead/adapters/cmux.md +3 -3
- package/runtime/prompts/lead/convergence.md +99 -88
- package/runtime/prompts/lead/okstra-lead-contract.md +63 -12
- package/runtime/prompts/lead/plan-body-verification.md +65 -46
- package/runtime/prompts/lead/report-writer.md +35 -5
- package/runtime/prompts/lead/team-contract.md +4 -1
- package/runtime/prompts/profiles/_clarification-recommendation.md +2 -1
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +3 -3
- package/runtime/prompts/profiles/_coverage-critic.md +1 -1
- package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +3 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +16 -2
- package/runtime/prompts/profiles/error-analysis.md +4 -3
- package/runtime/prompts/profiles/final-verification.md +2 -2
- package/runtime/prompts/profiles/implementation-planning.md +22 -17
- package/runtime/prompts/profiles/implementation.md +3 -2
- package/runtime/prompts/profiles/requirements-discovery.md +2 -1
- package/runtime/prompts/wizard/prompts.ko.json +29 -8
- package/runtime/python/okstra_ctl/__init__.py +6 -27
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -5
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +21 -2
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +3 -12
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +63 -22
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +20 -21
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +37 -20
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +86 -14
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +12 -12
- package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +4 -7
- package/runtime/python/okstra_ctl/agent/__init__.py +1 -0
- package/runtime/python/okstra_ctl/{agent_activity.py → agent/activity.py} +16 -2
- package/runtime/python/okstra_ctl/{agent_invocation.py → agent/invocation.py} +34 -5
- package/runtime/python/okstra_ctl/agent/prompt_cli/__init__.py +19 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/__main__.py +11 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +228 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +155 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/emit.py +66 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/inputs.py +138 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +605 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/results.py +197 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +107 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +47 -4
- package/runtime/python/okstra_ctl/approval_decisions.py +287 -10
- package/runtime/python/okstra_ctl/assignment_resolver.py +0 -12
- package/runtime/python/okstra_ctl/attempt_evidence.py +12 -24
- package/runtime/python/okstra_ctl/blocking_checks.py +251 -0
- package/runtime/python/okstra_ctl/brief_frontmatter.py +0 -7
- package/runtime/python/okstra_ctl/clarification_items/__init__.py +102 -0
- package/runtime/python/okstra_ctl/clarification_items/carry.py +224 -0
- package/runtime/python/okstra_ctl/clarification_items/dispositions.py +135 -0
- package/runtime/python/okstra_ctl/clarification_items/parsing.py +253 -0
- package/runtime/python/okstra_ctl/clarification_items/rows.py +115 -0
- package/runtime/python/okstra_ctl/clarification_items/scan.py +215 -0
- package/runtime/python/okstra_ctl/clarification_items/sidecars.py +211 -0
- package/runtime/python/okstra_ctl/cmux.py +38 -31
- package/runtime/python/okstra_ctl/code_review_target.py +163 -1
- package/runtime/python/okstra_ctl/conformance.py +23 -0
- package/runtime/python/okstra_ctl/container.py +38 -421
- package/runtime/python/okstra_ctl/context_cost.py +32 -4
- package/runtime/python/okstra_ctl/contract_graph_cli.py +7 -0
- package/runtime/python/okstra_ctl/convergence.py +107 -16
- package/runtime/python/okstra_ctl/convergence_critic_prompt.py +345 -0
- package/runtime/python/okstra_ctl/convergence_engine.py +382 -70
- package/runtime/python/okstra_ctl/convergence_store.py +5 -29
- package/runtime/python/okstra_ctl/design_prep.py +108 -12
- package/runtime/python/okstra_ctl/design_snapshot.py +11 -1
- package/runtime/python/okstra_ctl/design_surfaces.py +29 -2
- package/runtime/python/okstra_ctl/dispatch_core.py +236 -49
- package/runtime/python/okstra_ctl/dispatch_state.py +417 -41
- package/runtime/python/okstra_ctl/doctor_cli.py +48 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +10 -5
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +28 -1
- package/runtime/python/okstra_ctl/domain/worker_stream.py +52 -21
- package/runtime/python/okstra_ctl/domain/write_policy.py +219 -0
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +9 -2
- package/runtime/python/okstra_ctl/error_log_core.py +1 -1
- package/runtime/python/okstra_ctl/error_report.py +14 -0
- package/runtime/python/okstra_ctl/error_zip.py +18 -2
- package/runtime/python/okstra_ctl/exact_coverage.py +5 -0
- package/runtime/python/okstra_ctl/execution_identity.py +100 -7
- package/runtime/python/okstra_ctl/execution_manifest.py +67 -20
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +90 -8
- package/runtime/python/okstra_ctl/final_report_paths.py +17 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +319 -17
- package/runtime/python/okstra_ctl/git_reconcile.py +22 -2
- package/runtime/python/okstra_ctl/handoff.py +24 -1
- package/runtime/python/okstra_ctl/ids.py +6 -16
- package/runtime/python/okstra_ctl/implementation_direction.py +142 -33
- package/runtime/python/okstra_ctl/implementation_options.py +32 -13
- package/runtime/python/okstra_ctl/implementation_outcome.py +5 -2
- package/runtime/python/okstra_ctl/improvement_lenses.py +0 -14
- package/runtime/python/okstra_ctl/incremental_carry.py +69 -8
- package/runtime/python/okstra_ctl/incremental_scope.py +173 -12
- package/runtime/python/okstra_ctl/index.py +2 -2
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +48 -8
- package/runtime/python/okstra_ctl/interactive_cli.py +223 -0
- package/runtime/python/okstra_ctl/json_boundary.py +10 -0
- package/runtime/python/okstra_ctl/listing.py +0 -91
- package/runtime/python/okstra_ctl/locks.py +5 -19
- package/runtime/python/okstra_ctl/log_report.py +14 -0
- package/runtime/python/okstra_ctl/manager_cli.py +22 -3
- package/runtime/python/okstra_ctl/manager_launch.py +4 -8
- package/runtime/python/okstra_ctl/material.py +2 -2
- package/runtime/python/okstra_ctl/migrate.py +23 -1
- package/runtime/python/okstra_ctl/model_cli.py +19 -5
- package/runtime/python/okstra_ctl/model_discovery.py +46 -47
- package/runtime/python/okstra_ctl/model_io/__init__.py +1 -0
- package/runtime/python/okstra_ctl/model_io/lines.py +163 -0
- package/runtime/python/okstra_ctl/model_io/references.py +370 -0
- package/runtime/python/okstra_ctl/model_io/renderers.py +498 -0
- package/runtime/python/okstra_ctl/model_io_cli.py +30 -945
- package/runtime/python/okstra_ctl/model_pool.py +10 -6
- package/runtime/python/okstra_ctl/models.py +8 -8
- package/runtime/python/okstra_ctl/mutation_recovery.py +5 -63
- package/runtime/python/okstra_ctl/next_phase.py +121 -69
- package/runtime/python/okstra_ctl/pane_reclaim.py +23 -0
- package/runtime/python/okstra_ctl/pane_title.py +11 -8
- package/runtime/python/okstra_ctl/path_hints.py +36 -11
- package/runtime/python/okstra_ctl/paths.py +76 -8
- package/runtime/python/okstra_ctl/plan_items.py +132 -16
- package/runtime/python/okstra_ctl/plan_items_cli.py +486 -36
- package/runtime/python/okstra_ctl/plan_run_root.py +1 -1
- package/runtime/python/okstra_ctl/plan_validate_cli.py +51 -0
- package/runtime/python/okstra_ctl/plan_verify_cli.py +80 -0
- package/runtime/python/okstra_ctl/prepare_error.py +20 -0
- package/runtime/python/okstra_ctl/prior_planning.py +157 -0
- package/runtime/python/okstra_ctl/profile_show.py +18 -1
- package/runtime/python/okstra_ctl/project_setup_cli.py +140 -0
- package/runtime/python/okstra_ctl/recap.py +22 -1
- package/runtime/python/okstra_ctl/reconcile.py +84 -0
- package/runtime/python/okstra_ctl/registry/host_registry.py +18 -1
- package/runtime/python/okstra_ctl/render.py +98 -32
- package/runtime/python/okstra_ctl/render_final_report.py +4 -8
- package/runtime/python/okstra_ctl/report_assembly.py +297 -7
- package/runtime/python/okstra_ctl/report_contract.py +30 -7
- package/runtime/python/okstra_ctl/report_finalize.py +213 -16
- package/runtime/python/okstra_ctl/report_html/common.py +2 -2
- package/runtime/python/okstra_ctl/report_html/render.py +8 -12
- package/runtime/python/okstra_ctl/report_language.py +8 -4
- package/runtime/python/okstra_ctl/report_narrative.py +220 -34
- package/runtime/python/okstra_ctl/report_projections.py +8 -2
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +393 -1
- package/runtime/python/okstra_ctl/report_translation.py +2 -35
- package/runtime/python/okstra_ctl/report_view_artifacts.py +42 -2
- package/runtime/python/okstra_ctl/report_views.py +4 -22
- package/runtime/python/okstra_ctl/resolve_task_key.py +13 -0
- package/runtime/python/okstra_ctl/rollup.py +13 -0
- package/runtime/python/okstra_ctl/run.py +639 -138
- package/runtime/python/okstra_ctl/run_audit.py +13 -0
- package/runtime/python/okstra_ctl/run_index_row.py +0 -12
- package/runtime/python/okstra_ctl/seeding.py +21 -11
- package/runtime/python/okstra_ctl/sequence.py +1 -1
- package/runtime/python/okstra_ctl/session.py +64 -14
- package/runtime/python/okstra_ctl/set_work_status.py +18 -0
- package/runtime/python/okstra_ctl/stage_integrate.py +15 -1
- package/runtime/python/okstra_ctl/stage_ledger.py +1 -1
- package/runtime/python/okstra_ctl/stage_map.py +57 -11
- package/runtime/python/okstra_ctl/stage_map_cli.py +83 -0
- package/runtime/python/okstra_ctl/stage_map_view.py +65 -0
- package/runtime/python/okstra_ctl/stage_targets.py +3 -12
- package/runtime/python/okstra_ctl/task_list_cli.py +138 -0
- package/runtime/python/okstra_ctl/task_show_cli.py +66 -0
- package/runtime/python/okstra_ctl/task_target.py +4 -16
- package/runtime/python/okstra_ctl/team.py +49 -2
- package/runtime/python/okstra_ctl/time_report.py +17 -2
- package/runtime/python/okstra_ctl/usage_report.py +11 -0
- package/runtime/python/okstra_ctl/user_response.py +55 -218
- package/runtime/python/okstra_ctl/user_response_values.py +242 -0
- package/runtime/python/okstra_ctl/validation_contract.py +3 -0
- package/runtime/python/okstra_ctl/verdict_blocks.py +28 -8
- package/runtime/python/okstra_ctl/verification_target.py +68 -0
- package/runtime/python/okstra_ctl/wizard.py +457 -92
- package/runtime/python/okstra_ctl/worker_artifacts.py +75 -16
- package/runtime/python/okstra_ctl/worker_audit_check.py +22 -0
- package/runtime/python/okstra_ctl/worker_dispatch.py +21 -0
- package/runtime/python/okstra_ctl/worker_liveness.py +33 -0
- package/runtime/python/okstra_ctl/worker_prompt_body.py +15 -2
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +98 -7
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +24 -2
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +18 -3
- package/runtime/python/okstra_ctl/worker_request.py +2 -1
- package/runtime/python/okstra_ctl/worker_runner.py +58 -17
- package/runtime/python/okstra_ctl/worker_state.py +16 -1
- package/runtime/python/okstra_ctl/workflow.py +18 -1
- package/runtime/python/okstra_ctl/worktree/__init__.py +92 -0
- package/runtime/python/okstra_ctl/worktree/cleanliness.py +89 -0
- package/runtime/python/okstra_ctl/worktree/decisions.py +127 -0
- package/runtime/python/okstra_ctl/worktree/git_ops.py +165 -0
- package/runtime/python/okstra_ctl/worktree/linking.py +249 -0
- package/runtime/python/okstra_ctl/worktree/naming.py +91 -0
- package/runtime/python/okstra_ctl/worktree/provision.py +385 -0
- package/runtime/python/okstra_ctl/worktree/sync_config.py +181 -0
- package/runtime/python/okstra_ctl/worktree_cli.py +75 -0
- package/runtime/python/okstra_ctl/worktree_lookup_cli.py +39 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +7 -0
- package/runtime/python/okstra_ctl/worktree_status_cli.py +53 -0
- package/runtime/python/okstra_ctl/write_policy.py +90 -213
- package/runtime/python/okstra_project/__init__.py +0 -4
- package/runtime/python/okstra_project/dirs.py +2 -2
- package/runtime/python/okstra_project/phase_pointer.py +84 -0
- package/runtime/python/okstra_project/slug.py +24 -0
- package/runtime/python/okstra_project/state.py +22 -230
- package/runtime/python/okstra_token_usage/antigravity.py +50 -12
- package/runtime/python/okstra_token_usage/claude.py +9 -1
- package/runtime/python/okstra_token_usage/cli.py +3 -1
- package/runtime/python/okstra_token_usage/collect.py +171 -36
- package/runtime/python/okstra_token_usage/cursor.py +2 -0
- package/runtime/python/okstra_token_usage/grok.py +1 -5
- package/runtime/python/okstra_token_usage/paths.py +26 -0
- package/runtime/python/okstra_token_usage/pricing.py +23 -8
- package/runtime/schemas/convergence-critic-results-v1.0.schema.json +5 -0
- package/runtime/schemas/execution-manifest-v2.schema.json +10 -10
- package/runtime/schemas/final-report-v2.0.schema.json +9 -26
- package/runtime/schemas/final-report-v3.0.schema.json +173 -41
- package/runtime/schemas/report-narrative-v3.0.schema.json +3 -2
- package/runtime/skills/okstra-brief-gen/SKILL.md +35 -2
- package/runtime/skills/okstra-container-build/SKILL.md +16 -47
- package/runtime/skills/okstra-inspect/facets/history.md +1 -1
- package/runtime/skills/okstra-inspect/facets/status.md +7 -6
- package/runtime/skills/okstra-pr-gen/SKILL.md +1 -1
- package/runtime/skills/okstra-run/SKILL.md +7 -5
- package/runtime/templates/report-writer-prompt-preamble.md +3 -1
- package/runtime/templates/reports/brief.template.md +6 -2
- package/runtime/templates/reports/error-analysis-input.template.md +2 -0
- package/runtime/templates/reports/final-verification-input.template.md +2 -0
- package/runtime/templates/reports/implementation-input.template.md +7 -1
- package/runtime/templates/reports/implementation-planning-input.template.md +4 -0
- package/runtime/templates/reports/quick-input.template.md +2 -0
- package/runtime/templates/reports/release-handoff-input.template.md +6 -1
- package/runtime/templates/reports/report.js +20 -3
- package/runtime/templates/reports/schedule.template.md +2 -0
- package/runtime/templates/reports/settings.template.json +0 -10
- package/runtime/templates/reports/task-brief.template.md +3 -0
- package/runtime/templates/reports/user-response.template.md +7 -3
- package/runtime/validators/checks/fixtures-01.py +57 -0
- package/runtime/validators/checks/fixtures-02.py +565 -0
- package/runtime/validators/checks/runners-01.py +108 -0
- package/runtime/validators/checks/validate-assets-01.py +60 -0
- package/runtime/validators/checks/validate-prompt-metadata-01.py +261 -0
- package/runtime/validators/checks/validate-tasks-01.py +46 -0
- package/runtime/validators/checks/validate-tasks-02.py +85 -0
- package/runtime/validators/checks/validate-tasks-03.py +61 -0
- package/runtime/validators/checks/validate-tasks-04.py +118 -0
- package/runtime/validators/forbidden_actions.py +73 -3
- package/runtime/validators/lib/common.sh +5 -0
- package/runtime/validators/lib/fixtures.sh +7 -591
- package/runtime/validators/lib/paths.sh +13 -0
- package/runtime/validators/lib/runners.sh +6 -104
- package/runtime/validators/lib/summary.sh +1 -1
- package/runtime/validators/lib/validate-assets.sh +6 -56
- package/runtime/validators/lib/validate-prompt-metadata.sh +6 -257
- package/runtime/validators/lib/validate-tasks.sh +9 -294
- package/runtime/validators/validate-implementation-plan-stages.py +4 -4
- package/runtime/validators/validate-run.py +1363 -2643
- package/runtime/validators/validate-workflow.sh +56 -16
- package/runtime/validators/validate_improvement_report.py +2 -1
- package/runtime/validators/validate_session_conformance.py +295 -49
- package/dist/commands/execute/agent-prompt.d.mts +0 -1
- package/dist/commands/execute/agent-prompt.mjs +0 -24
- package/dist/commands/execute/agent-prompt.mjs.map +0 -1
- package/dist/commands/execute/codex-dispatch.d.mts +0 -3
- package/dist/commands/execute/codex-dispatch.mjs +0 -6
- package/dist/commands/execute/codex-dispatch.mjs.map +0 -1
- package/dist/commands/execute/codex-run.d.mts +0 -3
- package/dist/commands/execute/codex-run.mjs +0 -62
- package/dist/commands/execute/codex-run.mjs.map +0 -1
- package/dist/commands/execute/convergence.d.mts +0 -1
- package/dist/commands/execute/convergence.mjs +0 -38
- package/dist/commands/execute/convergence.mjs.map +0 -1
- package/dist/commands/execute/error-log.d.mts +0 -1
- package/dist/commands/execute/error-log.mjs +0 -18
- package/dist/commands/execute/error-log.mjs.map +0 -1
- package/dist/commands/execute/git-reconcile.d.mts +0 -1
- package/dist/commands/execute/git-reconcile.mjs +0 -30
- package/dist/commands/execute/git-reconcile.mjs.map +0 -1
- package/dist/commands/execute/handoff.d.mts +0 -1
- package/dist/commands/execute/handoff.mjs +0 -31
- package/dist/commands/execute/handoff.mjs.map +0 -1
- package/dist/commands/execute/incremental-carry.d.mts +0 -1
- package/dist/commands/execute/incremental-carry.mjs +0 -20
- package/dist/commands/execute/incremental-carry.mjs.map +0 -1
- package/dist/commands/execute/incremental-scope.d.mts +0 -1
- package/dist/commands/execute/incremental-scope.mjs +0 -29
- package/dist/commands/execute/incremental-scope.mjs.map +0 -1
- package/dist/commands/execute/integrate-stages.d.mts +0 -1
- package/dist/commands/execute/integrate-stages.mjs +0 -24
- package/dist/commands/execute/integrate-stages.mjs.map +0 -1
- package/dist/commands/execute/pane-title.d.mts +0 -1
- package/dist/commands/execute/pane-title.mjs +0 -20
- package/dist/commands/execute/pane-title.mjs.map +0 -1
- package/dist/commands/execute/plan-items.d.mts +0 -1
- package/dist/commands/execute/plan-items.mjs +0 -9
- package/dist/commands/execute/plan-items.mjs.map +0 -1
- package/dist/commands/execute/plan-validate.d.mts +0 -1
- package/dist/commands/execute/plan-validate.mjs +0 -68
- package/dist/commands/execute/plan-validate.mjs.map +0 -1
- package/dist/commands/execute/plan-verify.d.mts +0 -1
- package/dist/commands/execute/plan-verify.mjs +0 -43
- package/dist/commands/execute/plan-verify.mjs.map +0 -1
- package/dist/commands/execute/spawn-followups.d.mts +0 -1
- package/dist/commands/execute/spawn-followups.mjs +0 -22
- package/dist/commands/execute/spawn-followups.mjs.map +0 -1
- package/dist/commands/execute/team.d.mts +0 -3
- package/dist/commands/execute/team.mjs +0 -66
- package/dist/commands/execute/team.mjs.map +0 -1
- package/dist/commands/execute/token-usage.d.mts +0 -1
- package/dist/commands/execute/token-usage.mjs +0 -19
- package/dist/commands/execute/token-usage.mjs.map +0 -1
- package/dist/commands/execute/worker-audit-check.d.mts +0 -1
- package/dist/commands/execute/worker-audit-check.mjs +0 -34
- package/dist/commands/execute/worker-audit-check.mjs.map +0 -1
- package/dist/commands/execute/worker-dispatch.d.mts +0 -7
- package/dist/commands/execute/worker-dispatch.mjs +0 -64
- package/dist/commands/execute/worker-dispatch.mjs.map +0 -1
- package/dist/commands/execute/worker-state.d.mts +0 -1
- package/dist/commands/execute/worker-state.mjs +0 -28
- package/dist/commands/execute/worker-state.mjs.map +0 -1
- package/dist/commands/execute/worktree-lookup.d.mts +0 -1
- package/dist/commands/execute/worktree-lookup.mjs +0 -92
- package/dist/commands/execute/worktree-lookup.mjs.map +0 -1
- package/dist/commands/execute/worktree-status.d.mts +0 -1
- package/dist/commands/execute/worktree-status.mjs +0 -121
- package/dist/commands/execute/worktree-status.mjs.map +0 -1
- package/dist/commands/inspect/code-review.d.mts +0 -1
- package/dist/commands/inspect/code-review.mjs +0 -32
- package/dist/commands/inspect/code-review.mjs.map +0 -1
- package/dist/commands/inspect/container.d.mts +0 -1
- package/dist/commands/inspect/container.mjs +0 -25
- package/dist/commands/inspect/container.mjs.map +0 -1
- package/dist/commands/inspect/context-cost.d.mts +0 -1
- package/dist/commands/inspect/context-cost.mjs +0 -25
- package/dist/commands/inspect/context-cost.mjs.map +0 -1
- package/dist/commands/inspect/design-prep.d.mts +0 -1
- package/dist/commands/inspect/design-prep.mjs +0 -22
- package/dist/commands/inspect/design-prep.mjs.map +0 -1
- package/dist/commands/inspect/error-report.d.mts +0 -1
- package/dist/commands/inspect/error-report.mjs +0 -25
- package/dist/commands/inspect/error-report.mjs.map +0 -1
- package/dist/commands/inspect/error-zip.d.mts +0 -1
- package/dist/commands/inspect/error-zip.mjs +0 -24
- package/dist/commands/inspect/error-zip.mjs.map +0 -1
- package/dist/commands/inspect/log-report.d.mts +0 -1
- package/dist/commands/inspect/log-report.mjs +0 -26
- package/dist/commands/inspect/log-report.mjs.map +0 -1
- package/dist/commands/inspect/model-io.d.mts +0 -1
- package/dist/commands/inspect/model-io.mjs +0 -25
- package/dist/commands/inspect/model-io.mjs.map +0 -1
- package/dist/commands/inspect/profile-show.d.mts +0 -1
- package/dist/commands/inspect/profile-show.mjs +0 -28
- package/dist/commands/inspect/profile-show.mjs.map +0 -1
- package/dist/commands/inspect/recap.d.mts +0 -1
- package/dist/commands/inspect/recap.mjs +0 -30
- package/dist/commands/inspect/recap.mjs.map +0 -1
- package/dist/commands/inspect/resolve-task-key.d.mts +0 -1
- package/dist/commands/inspect/resolve-task-key.mjs +0 -24
- package/dist/commands/inspect/resolve-task-key.mjs.map +0 -1
- package/dist/commands/inspect/rollup.d.mts +0 -1
- package/dist/commands/inspect/rollup.mjs +0 -25
- package/dist/commands/inspect/rollup.mjs.map +0 -1
- package/dist/commands/inspect/run-audit.d.mts +0 -1
- package/dist/commands/inspect/run-audit.mjs +0 -25
- package/dist/commands/inspect/run-audit.mjs.map +0 -1
- package/dist/commands/inspect/set-work-status.d.mts +0 -1
- package/dist/commands/inspect/set-work-status.mjs +0 -29
- package/dist/commands/inspect/set-work-status.mjs.map +0 -1
- package/dist/commands/inspect/stage-map.d.mts +0 -1
- package/dist/commands/inspect/stage-map.mjs +0 -131
- package/dist/commands/inspect/stage-map.mjs.map +0 -1
- package/dist/commands/inspect/task-list.d.mts +0 -1
- package/dist/commands/inspect/task-list.mjs +0 -149
- package/dist/commands/inspect/task-list.mjs.map +0 -1
- package/dist/commands/inspect/task-show.d.mts +0 -1
- package/dist/commands/inspect/task-show.mjs +0 -108
- package/dist/commands/inspect/task-show.mjs.map +0 -1
- package/dist/commands/inspect/time-report.d.mts +0 -1
- package/dist/commands/inspect/time-report.mjs +0 -24
- package/dist/commands/inspect/time-report.mjs.map +0 -1
- package/dist/commands/inspect/usage-report.d.mts +0 -1
- package/dist/commands/inspect/usage-report.mjs +0 -23
- package/dist/commands/inspect/usage-report.mjs.map +0 -1
- package/dist/commands/inspect/user-response.d.mts +0 -1
- package/dist/commands/inspect/user-response.mjs +0 -35
- package/dist/commands/inspect/user-response.mjs.map +0 -1
- package/dist/commands/inspect/worker-liveness.d.mts +0 -1
- package/dist/commands/inspect/worker-liveness.mjs +0 -45
- package/dist/commands/inspect/worker-liveness.mjs.map +0 -1
- package/dist/commands/lifecycle/contract-check.d.mts +0 -1
- package/dist/commands/lifecycle/contract-check.mjs +0 -18
- package/dist/commands/lifecycle/contract-check.mjs.map +0 -1
- package/dist/commands/lifecycle/migrate.d.mts +0 -1
- package/dist/commands/lifecycle/migrate.mjs +0 -30
- package/dist/commands/lifecycle/migrate.mjs.map +0 -1
- package/dist/commands/lifecycle/model.d.mts +0 -1
- package/dist/commands/lifecycle/model.mjs +0 -22
- package/dist/commands/lifecycle/model.mjs.map +0 -1
- package/dist/commands/manager.d.mts +0 -3
- package/dist/commands/manager.mjs +0 -50
- package/dist/commands/manager.mjs.map +0 -1
- package/dist/commands/report/agent-activity.d.mts +0 -1
- package/dist/commands/report/agent-activity.mjs +0 -20
- package/dist/commands/report/agent-activity.mjs.map +0 -1
- package/dist/commands/report/approval-decision.d.mts +0 -1
- package/dist/commands/report/approval-decision.mjs +0 -21
- package/dist/commands/report/approval-decision.mjs.map +0 -1
- package/dist/commands/report/design-snapshot.d.mts +0 -1
- package/dist/commands/report/design-snapshot.mjs +0 -19
- package/dist/commands/report/design-snapshot.mjs.map +0 -1
- package/dist/commands/report/finalize.d.mts +0 -4
- package/dist/commands/report/finalize.mjs +0 -64
- package/dist/commands/report/finalize.mjs.map +0 -1
- package/dist/commands/report/inject-report-index.d.mts +0 -1
- package/dist/commands/report/inject-report-index.mjs +0 -21
- package/dist/commands/report/inject-report-index.mjs.map +0 -1
- package/dist/commands/report/render-final-report.d.mts +0 -1
- package/dist/commands/report/render-final-report.mjs +0 -23
- package/dist/commands/report/render-final-report.mjs.map +0 -1
- package/dist/commands/report/render-views.d.mts +0 -1
- package/dist/commands/report/render-views.mjs +0 -27
- package/dist/commands/report/render-views.mjs.map +0 -1
- package/dist/commands/report/translate.d.mts +0 -1
- package/dist/commands/report/translate.mjs +0 -33
- package/dist/commands/report/translate.mjs.map +0 -1
- package/runtime/bin/lib/okstra/tmux-pane.sh +0 -40
- package/runtime/bin/lib/okstra-ctl/cmd-batch.sh +0 -59
- package/runtime/bin/lib/okstra-ctl/cmd-list.sh +0 -35
- package/runtime/bin/lib/okstra-ctl/cmd-open.sh +0 -36
- package/runtime/bin/lib/okstra-ctl/cmd-projects.sh +0 -26
- package/runtime/bin/lib/okstra-ctl/cmd-reconcile.sh +0 -29
- package/runtime/bin/lib/okstra-ctl/cmd-reindex.sh +0 -38
- package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +0 -345
- package/runtime/bin/lib/okstra-ctl/cmd-show.sh +0 -27
- package/runtime/bin/lib/okstra-ctl/cmd-tail.sh +0 -92
- package/runtime/bin/lib/okstra-ctl/main.sh +0 -41
- package/runtime/bin/lib/okstra-ctl/prepare.sh +0 -31
- package/runtime/bin/lib/okstra-ctl/usage.sh +0 -23
- package/runtime/bin/okstra-central.sh +0 -152
- package/runtime/bin/okstra-incremental-carry.py +0 -10
- package/runtime/bin/okstra-incremental-scope.py +0 -10
- package/runtime/bin/okstra-team-reconcile.sh +0 -36
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +0 -1156
- package/runtime/python/okstra_ctl/batch.py +0 -60
- package/runtime/python/okstra_ctl/clarification_items.py +0 -1050
- package/runtime/python/okstra_ctl/container_registry.py +0 -72
- package/runtime/python/okstra_ctl/improvement_assignment.py +0 -61
- package/runtime/python/okstra_ctl/resolver.py +0 -54
- package/runtime/python/okstra_ctl/team_reconcile.py +0 -275
- package/runtime/python/okstra_ctl/tmux.py +0 -134
- package/runtime/python/okstra_ctl/worktree.py +0 -1099
|
@@ -6,7 +6,7 @@ import json
|
|
|
6
6
|
from collections.abc import Mapping
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Any
|
|
9
|
+
from typing import Any, Literal
|
|
10
10
|
|
|
11
11
|
from .domain.provider import HostModelBinding, ServedModelAttestation
|
|
12
12
|
from .domain.worker_exec import WorkerWriteCapability
|
|
@@ -116,9 +116,6 @@ class Invocation:
|
|
|
116
116
|
dispatch_kind: str
|
|
117
117
|
round: int
|
|
118
118
|
input_digest: str
|
|
119
|
-
write_policy: Mapping[str, Any]
|
|
120
|
-
write_policy_digest: str
|
|
121
|
-
write_enforcement: Mapping[str, Any]
|
|
122
119
|
|
|
123
120
|
def to_payload(self) -> dict[str, Any]:
|
|
124
121
|
return {
|
|
@@ -131,14 +128,19 @@ class Invocation:
|
|
|
131
128
|
"dispatchKind": self.dispatch_kind,
|
|
132
129
|
"round": self.round,
|
|
133
130
|
"inputDigest": self.input_digest,
|
|
134
|
-
"writePolicy": dict(self.write_policy),
|
|
135
|
-
"writePolicyDigest": self.write_policy_digest,
|
|
136
|
-
"writeEnforcement": dict(self.write_enforcement),
|
|
137
131
|
}
|
|
138
132
|
|
|
139
133
|
|
|
140
134
|
@dataclass(frozen=True)
|
|
141
135
|
class Attempt:
|
|
136
|
+
"""One dispatch of a logical invocation, with the write contract it ran under.
|
|
137
|
+
|
|
138
|
+
쓰기 계약이 invocation 이 아니라 여기 있는 이유: 계약의 artifactPolicy 는 그
|
|
139
|
+
시도가 실제로 쓰는 프롬프트 파생 파일(`...-attempt-2.log` 등)을 이름으로
|
|
140
|
+
담는다. attempt 마다 다른 값이므로 attempt-불변인 invocation 행에 두면 같은
|
|
141
|
+
invocationRef 의 2회차가 `invocationRef drift` 로 거절된다.
|
|
142
|
+
"""
|
|
143
|
+
|
|
142
144
|
invocation_ref: str
|
|
143
145
|
attempt: int
|
|
144
146
|
started_at: str
|
|
@@ -148,6 +150,9 @@ class Attempt:
|
|
|
148
150
|
error_path: str | None
|
|
149
151
|
change_summary: Mapping[str, Any]
|
|
150
152
|
evidence_seal_ref: str | None
|
|
153
|
+
write_policy: Mapping[str, Any]
|
|
154
|
+
write_policy_digest: str
|
|
155
|
+
write_enforcement: Mapping[str, Any]
|
|
151
156
|
|
|
152
157
|
def to_payload(self) -> dict[str, Any]:
|
|
153
158
|
return {
|
|
@@ -160,6 +165,9 @@ class Attempt:
|
|
|
160
165
|
"errorPath": self.error_path,
|
|
161
166
|
"changeSummary": dict(self.change_summary),
|
|
162
167
|
"evidenceSealRef": self.evidence_seal_ref,
|
|
168
|
+
"writePolicy": dict(self.write_policy),
|
|
169
|
+
"writePolicyDigest": self.write_policy_digest,
|
|
170
|
+
"writeEnforcement": dict(self.write_enforcement),
|
|
163
171
|
}
|
|
164
172
|
|
|
165
173
|
|
|
@@ -528,3 +536,88 @@ def _required_index_text(
|
|
|
528
536
|
def _optional_text(value: object) -> str | None:
|
|
529
537
|
text = str(value or "").strip()
|
|
530
538
|
return text or None
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
# ---- 무거운 모듈에서 옮겨 온 값 타입 ------------------------------------
|
|
542
|
+
# 아래 둘은 dispatch_state(2,092줄) 와 mutation_recovery 안에 있었다. 값 하나가
|
|
543
|
+
# 필요한 모듈이 그 모듈 전체를 끌어오게 되어, 반대 방향은 함수 본문 지연
|
|
544
|
+
# import 로 밀려나 순환이 됐다. 원래 소유 모듈이 이름을 재노출한다.
|
|
545
|
+
|
|
546
|
+
def build_dispatch_id(invocation_id: str, attempt: int) -> str:
|
|
547
|
+
"""The only place Python assembles a dispatch id.
|
|
548
|
+
|
|
549
|
+
Two generators drifted: this module pinned the attempt to 1 while
|
|
550
|
+
`dispatch_core` used the real one, so a re-dispatch took the first attempt's
|
|
551
|
+
id, team-state kept the same row twice, and `validate-run` failed that run
|
|
552
|
+
with `agent dispatch ID is duplicated`.
|
|
553
|
+
|
|
554
|
+
Prose mirrors of this shape live outside Python, where no guard reaches
|
|
555
|
+
them: `prompts/lead/convergence.md`, `prompts/lead/report-writer.md`,
|
|
556
|
+
`prompts/lead/plan-body-verification.md` and the five
|
|
557
|
+
`adapters/hosts/*/relay.md` each spell an `--dispatch-id` argument out for
|
|
558
|
+
the lead. They agree with this function only because
|
|
559
|
+
`record_verified_agent_dispatch` records the first attempt and no other;
|
|
560
|
+
changing that convention has to change them too.
|
|
561
|
+
"""
|
|
562
|
+
return f"{invocation_id}:attempt-{attempt}"
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
@dataclass(frozen=True)
|
|
566
|
+
class MutationRecoveryDecision:
|
|
567
|
+
recovery_ref: str
|
|
568
|
+
source_invocation_ref: str
|
|
569
|
+
source_attempt_ref: str
|
|
570
|
+
source_evidence_seal_ref: str | None
|
|
571
|
+
original_baseline_digest: str
|
|
572
|
+
terminal_source_diff_digest: str
|
|
573
|
+
terminal_git_chain_digest: str
|
|
574
|
+
verifier_role_execution_ref: str | None
|
|
575
|
+
verifier_invocation_ref: str | None
|
|
576
|
+
recovered_evidence_refs: tuple[Mapping[str, Any], ...]
|
|
577
|
+
recovered_out_of_plan_edits: tuple[Mapping[str, Any], ...]
|
|
578
|
+
unmet_obligations: tuple[str, ...]
|
|
579
|
+
decision: Literal["carry-forward", "reject", "cleanup-confirmed"]
|
|
580
|
+
reason: str = ""
|
|
581
|
+
|
|
582
|
+
def to_payload(self) -> dict[str, Any]:
|
|
583
|
+
return {
|
|
584
|
+
"recoveryRef": self.recovery_ref,
|
|
585
|
+
"sourceInvocationRef": self.source_invocation_ref,
|
|
586
|
+
"sourceAttemptRef": self.source_attempt_ref,
|
|
587
|
+
"sourceEvidenceSealRef": self.source_evidence_seal_ref,
|
|
588
|
+
"originalBaselineDigest": self.original_baseline_digest,
|
|
589
|
+
"terminalSourceDiffDigest": self.terminal_source_diff_digest,
|
|
590
|
+
"terminalGitChainDigest": self.terminal_git_chain_digest,
|
|
591
|
+
"verifierRoleExecutionRef": self.verifier_role_execution_ref,
|
|
592
|
+
"verifierInvocationRef": self.verifier_invocation_ref,
|
|
593
|
+
"recoveredEvidenceRefs": [dict(row) for row in self.recovered_evidence_refs],
|
|
594
|
+
"recoveredOutOfPlanEdits": [
|
|
595
|
+
dict(row) for row in self.recovered_out_of_plan_edits
|
|
596
|
+
],
|
|
597
|
+
"unmetObligations": list(self.unmet_obligations),
|
|
598
|
+
"decision": self.decision,
|
|
599
|
+
"reason": self.reason,
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
@classmethod
|
|
603
|
+
def from_payload(cls, payload: Mapping[str, Any]) -> "MutationRecoveryDecision":
|
|
604
|
+
return cls(
|
|
605
|
+
recovery_ref=str(payload["recoveryRef"]),
|
|
606
|
+
source_invocation_ref=str(payload["sourceInvocationRef"]),
|
|
607
|
+
source_attempt_ref=str(payload["sourceAttemptRef"]),
|
|
608
|
+
source_evidence_seal_ref=payload.get("sourceEvidenceSealRef"),
|
|
609
|
+
original_baseline_digest=str(payload.get("originalBaselineDigest") or ""),
|
|
610
|
+
terminal_source_diff_digest=str(
|
|
611
|
+
payload.get("terminalSourceDiffDigest") or ""
|
|
612
|
+
),
|
|
613
|
+
terminal_git_chain_digest=str(payload.get("terminalGitChainDigest") or ""),
|
|
614
|
+
verifier_role_execution_ref=payload.get("verifierRoleExecutionRef"),
|
|
615
|
+
verifier_invocation_ref=payload.get("verifierInvocationRef"),
|
|
616
|
+
recovered_evidence_refs=tuple(payload.get("recoveredEvidenceRefs") or ()),
|
|
617
|
+
recovered_out_of_plan_edits=tuple(
|
|
618
|
+
payload.get("recoveredOutOfPlanEdits") or ()
|
|
619
|
+
),
|
|
620
|
+
unmet_obligations=tuple(payload.get("unmetObligations") or ()),
|
|
621
|
+
decision=str(payload["decision"]),
|
|
622
|
+
reason=str(payload.get("reason") or ""),
|
|
623
|
+
)
|
|
@@ -4,7 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
6
|
import tempfile
|
|
7
|
-
from collections.abc import Mapping, Sequence
|
|
7
|
+
from collections.abc import Mapping, MutableMapping, Sequence
|
|
8
|
+
from dataclasses import replace
|
|
8
9
|
from functools import lru_cache
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Any, Callable
|
|
@@ -13,6 +14,7 @@ from .domain.provider import HostModelBinding, ServedModelAttestation
|
|
|
13
14
|
from .domain.worker_exec import WorkerWriteCapability
|
|
14
15
|
from .execution_identity import (
|
|
15
16
|
Attempt,
|
|
17
|
+
build_dispatch_id,
|
|
16
18
|
EXECUTION_IDENTITY_VERSION,
|
|
17
19
|
ExecutionManifest,
|
|
18
20
|
ExecutionManifestError,
|
|
@@ -62,10 +64,50 @@ def read_execution_manifest_view(
|
|
|
62
64
|
surface = read_identity_surface(path, surface="run-manifest")
|
|
63
65
|
if surface.legacy:
|
|
64
66
|
return synthesize_v1_execution_view(surface.payload), surface.payload
|
|
67
|
+
hoist_legacy_attempt_write_contract(surface.payload)
|
|
65
68
|
validate_execution_manifest_payload(surface.payload)
|
|
66
69
|
return _execution_manifest_from_payload(surface.payload), surface.payload
|
|
67
70
|
|
|
68
71
|
|
|
72
|
+
WRITE_CONTRACT_KEYS = ("writePolicy", "writePolicyDigest", "writeEnforcement")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def hoist_legacy_attempt_write_contract(payload: Any) -> None:
|
|
76
|
+
"""쓰기 계약이 invocation 에 실린 매니페스트를 attempt 소유로 옮긴다.
|
|
77
|
+
|
|
78
|
+
계약은 attempt 마다 다르다(`execution_identity.Attempt` 참고). 그 이전에
|
|
79
|
+
쓰인 매니페스트는 계약을 invocation 행에 담고 attempt 행에는 안 담으므로,
|
|
80
|
+
새 검증기가 보면 attempt 가 계약 없는 행이 된다 — 진행 중인 태스크가 전부
|
|
81
|
+
그 상태다. 여기서 옮겨 두면 다음 쓰기에서 파일 자체가 새 모양이 된다.
|
|
82
|
+
|
|
83
|
+
이 함수는 옮기기만 하고 값은 만들지 않는다. attempt 가 없는 invocation 의
|
|
84
|
+
계약은 버린다 — 그 값을 읽는 소비자는 없고, 디스패치가 attempt 를 열 때
|
|
85
|
+
다시 계산한다. 진행 중인 태스크가 모두 끝나면 통째로 지울 수 있다.
|
|
86
|
+
"""
|
|
87
|
+
if not isinstance(payload, MutableMapping):
|
|
88
|
+
return
|
|
89
|
+
invocations = payload.get("invocations")
|
|
90
|
+
attempts = payload.get("attempts")
|
|
91
|
+
if not isinstance(invocations, list):
|
|
92
|
+
return
|
|
93
|
+
carried: dict[str, dict[str, Any]] = {}
|
|
94
|
+
for row in invocations:
|
|
95
|
+
if not isinstance(row, MutableMapping) or "writePolicy" not in row:
|
|
96
|
+
continue
|
|
97
|
+
contract = {key: row.pop(key) for key in WRITE_CONTRACT_KEYS if key in row}
|
|
98
|
+
ref = row.get("invocationRef")
|
|
99
|
+
if isinstance(ref, str):
|
|
100
|
+
carried[ref] = contract
|
|
101
|
+
if not carried or not isinstance(attempts, list):
|
|
102
|
+
return
|
|
103
|
+
for row in attempts:
|
|
104
|
+
if not isinstance(row, MutableMapping) or "writePolicy" in row:
|
|
105
|
+
continue
|
|
106
|
+
contract = carried.get(row.get("invocationRef"))
|
|
107
|
+
if contract:
|
|
108
|
+
row.update(contract)
|
|
109
|
+
|
|
110
|
+
|
|
69
111
|
def write_execution_manifest(path: Path, manifest: ExecutionManifest) -> None:
|
|
70
112
|
if manifest.legacy:
|
|
71
113
|
raise ExecutionManifestError("cannot write a v1 memory view")
|
|
@@ -130,7 +172,7 @@ def _drift_detail(existing: Invocation, incoming: Invocation) -> str:
|
|
|
130
172
|
before = existing.to_payload()
|
|
131
173
|
after = incoming.to_payload()
|
|
132
174
|
changed = sorted(
|
|
133
|
-
key for key in (*before, *after) if before.get(key) != after.get(key)
|
|
175
|
+
{key for key in (*before, *after) if before.get(key) != after.get(key)}
|
|
134
176
|
)
|
|
135
177
|
return ", ".join(changed) if changed else "(no field differs)"
|
|
136
178
|
|
|
@@ -349,10 +391,8 @@ def finish_attempt_mutation(
|
|
|
349
391
|
if index is None:
|
|
350
392
|
raise ExecutionManifestError("attempt to finish is unknown")
|
|
351
393
|
existing = manifest.attempts[index]
|
|
352
|
-
closed =
|
|
353
|
-
|
|
354
|
-
attempt=attempt,
|
|
355
|
-
started_at=existing.started_at,
|
|
394
|
+
closed = replace(
|
|
395
|
+
existing,
|
|
356
396
|
finished_at=finished_at,
|
|
357
397
|
status=status,
|
|
358
398
|
result_path=result_path,
|
|
@@ -422,7 +462,7 @@ def validate_execution_manifest_payload(payload: Mapping[str, Any]) -> None:
|
|
|
422
462
|
invocations = _indexed_rows(payload["invocations"], "invocationRef")
|
|
423
463
|
_validate_roles(payload, participants)
|
|
424
464
|
_validate_invocations(participants, roles, invocations)
|
|
425
|
-
_validate_attempts(payload["attempts"], invocations)
|
|
465
|
+
_validate_attempts(payload["attempts"], invocations, participants, roles)
|
|
426
466
|
_validate_seals_and_recoveries(payload)
|
|
427
467
|
|
|
428
468
|
|
|
@@ -613,10 +653,9 @@ def _validate_invocations(
|
|
|
613
653
|
source = row.get("sourceInvocationRef")
|
|
614
654
|
if source is not None and source not in invocations:
|
|
615
655
|
raise ExecutionManifestError("invocation sourceInvocationRef is unknown")
|
|
616
|
-
_validate_invocation_write_contract(row, participants[participant_ref], roles[role_ref])
|
|
617
656
|
|
|
618
657
|
|
|
619
|
-
def
|
|
658
|
+
def _validate_attempt_write_contract(
|
|
620
659
|
row: Mapping[str, Any],
|
|
621
660
|
participant: Mapping[str, Any],
|
|
622
661
|
role: Mapping[str, Any],
|
|
@@ -624,21 +663,21 @@ def _validate_invocation_write_contract(
|
|
|
624
663
|
policy = row.get("writePolicy")
|
|
625
664
|
enforcement = row.get("writeEnforcement")
|
|
626
665
|
if not isinstance(policy, Mapping) or not isinstance(enforcement, Mapping):
|
|
627
|
-
raise ExecutionManifestError("
|
|
666
|
+
raise ExecutionManifestError("attempt write contract must be structured")
|
|
628
667
|
try:
|
|
629
668
|
validate_write_policy_payload(policy)
|
|
630
669
|
validate_write_enforcement_payload(enforcement)
|
|
631
670
|
expected_digest = write_policy_digest(policy)
|
|
632
671
|
except WritePolicyError as exc:
|
|
633
|
-
raise ExecutionManifestError(f"
|
|
672
|
+
raise ExecutionManifestError(f"attempt write contract: {exc}") from exc
|
|
634
673
|
if row.get("writePolicyDigest") != expected_digest:
|
|
635
|
-
raise ExecutionManifestError("
|
|
674
|
+
raise ExecutionManifestError("attempt writePolicyDigest does not match")
|
|
636
675
|
source = policy.get("sourcePolicy")
|
|
637
676
|
expected_mode = (
|
|
638
677
|
"project-mutation" if role.get("role") == "implementer" else "source-readonly"
|
|
639
678
|
)
|
|
640
679
|
if not isinstance(source, Mapping) or source.get("mode") != expected_mode:
|
|
641
|
-
raise ExecutionManifestError("
|
|
680
|
+
raise ExecutionManifestError("attempt sourcePolicy does not match role")
|
|
642
681
|
git_policy = policy.get("gitPolicy")
|
|
643
682
|
expected_git_mode = (
|
|
644
683
|
"fast-forward-descendant-chain"
|
|
@@ -649,10 +688,10 @@ def _validate_invocation_write_contract(
|
|
|
649
688
|
not isinstance(git_policy, Mapping)
|
|
650
689
|
or git_policy.get("mode") != expected_git_mode
|
|
651
690
|
):
|
|
652
|
-
raise ExecutionManifestError("
|
|
691
|
+
raise ExecutionManifestError("attempt gitPolicy does not match role")
|
|
653
692
|
if expected_mode == "source-readonly" and source.get("plannedPaths"):
|
|
654
693
|
raise ExecutionManifestError(
|
|
655
|
-
"source-readonly
|
|
694
|
+
"source-readonly attempt cannot declare planned paths"
|
|
656
695
|
)
|
|
657
696
|
_validate_enforcement_capability(enforcement, participant, role)
|
|
658
697
|
|
|
@@ -674,7 +713,7 @@ def _validate_enforcement_capability(
|
|
|
674
713
|
)
|
|
675
714
|
return
|
|
676
715
|
if not isinstance(capability, Mapping):
|
|
677
|
-
raise ExecutionManifestError("worker
|
|
716
|
+
raise ExecutionManifestError("worker attempt has no write capability")
|
|
678
717
|
rank = {"none": 0, "directory-boundary": 1, "exact-path": 2}
|
|
679
718
|
maximum = capability.get("maxBoundaryPrecision")
|
|
680
719
|
actual = enforcement.get("boundaryPrecision")
|
|
@@ -685,6 +724,8 @@ def _validate_enforcement_capability(
|
|
|
685
724
|
def _validate_attempts(
|
|
686
725
|
rows: object,
|
|
687
726
|
invocations: Mapping[str, Mapping[str, Any]],
|
|
727
|
+
participants: Mapping[str, Mapping[str, Any]],
|
|
728
|
+
roles: Mapping[str, Mapping[str, Any]],
|
|
688
729
|
) -> None:
|
|
689
730
|
if not isinstance(rows, list):
|
|
690
731
|
raise ExecutionManifestError("execution manifest attempts must be an array")
|
|
@@ -698,6 +739,12 @@ def _validate_attempts(
|
|
|
698
739
|
attempt = row.get("attempt")
|
|
699
740
|
if not isinstance(attempt, int) or attempt < 1:
|
|
700
741
|
raise ExecutionManifestError("attempt number must be positive")
|
|
742
|
+
invocation = invocations[invocation_ref]
|
|
743
|
+
participant = participants.get(str(invocation.get("participantRef")))
|
|
744
|
+
role = roles.get(str(invocation.get("roleExecutionRef")))
|
|
745
|
+
if participant is None or role is None:
|
|
746
|
+
raise ExecutionManifestError("attempt references unknown execution identity")
|
|
747
|
+
_validate_attempt_write_contract(row, participant, role)
|
|
701
748
|
attempts_by_invocation.setdefault(invocation_ref, []).append(attempt)
|
|
702
749
|
for invocation_ref, values in attempts_by_invocation.items():
|
|
703
750
|
if sorted(values) != list(range(1, len(values) + 1)):
|
|
@@ -710,7 +757,6 @@ def _validate_seals_and_recoveries(payload: Mapping[str, Any]) -> None:
|
|
|
710
757
|
attempts = payload.get("attempts")
|
|
711
758
|
if not isinstance(attempts, list):
|
|
712
759
|
return
|
|
713
|
-
from .dispatch_state import build_dispatch_id
|
|
714
760
|
|
|
715
761
|
attempt_refs = {
|
|
716
762
|
build_dispatch_id(str(row.get("invocationRef")), int(row.get("attempt") or 0))
|
|
@@ -925,9 +971,6 @@ def _invocation_from_payload(row: Mapping[str, Any]) -> Invocation:
|
|
|
925
971
|
dispatch_kind=str(row["dispatchKind"]),
|
|
926
972
|
round=int(row["round"]),
|
|
927
973
|
input_digest=str(row["inputDigest"]),
|
|
928
|
-
write_policy=dict(row["writePolicy"]),
|
|
929
|
-
write_policy_digest=str(row["writePolicyDigest"]),
|
|
930
|
-
write_enforcement=dict(row["writeEnforcement"]),
|
|
931
974
|
)
|
|
932
975
|
|
|
933
976
|
|
|
@@ -942,6 +985,9 @@ def _attempt_from_payload(row: Mapping[str, Any]) -> Attempt:
|
|
|
942
985
|
error_path=_optional_text(row.get("errorPath")),
|
|
943
986
|
change_summary=dict(row["changeSummary"]),
|
|
944
987
|
evidence_seal_ref=_optional_text(row.get("evidenceSealRef")),
|
|
988
|
+
write_policy=dict(row["writePolicy"]),
|
|
989
|
+
write_policy_digest=str(row["writePolicyDigest"]),
|
|
990
|
+
write_enforcement=dict(row["writeEnforcement"]),
|
|
945
991
|
)
|
|
946
992
|
|
|
947
993
|
|
|
@@ -1223,6 +1269,7 @@ def _role_execution_index_from_runs(
|
|
|
1223
1269
|
surface = read_identity_surface(resolved_path, surface="run-manifest")
|
|
1224
1270
|
if surface.legacy:
|
|
1225
1271
|
continue
|
|
1272
|
+
hoist_legacy_attempt_write_contract(surface.payload)
|
|
1226
1273
|
validate_execution_manifest_payload(surface.payload)
|
|
1227
1274
|
run_ref = resolved_path.relative_to(resolved_reference_root).as_posix()
|
|
1228
1275
|
rows.extend(
|
|
@@ -83,6 +83,10 @@ class MutationSnapshot:
|
|
|
83
83
|
class MutationAuditResult:
|
|
84
84
|
status: str
|
|
85
85
|
changed_paths: tuple[str, ...]
|
|
86
|
+
# 추적되지 않는 채로 생기거나 바뀐 경로. 소스 변경이 아니라 명령이 남긴
|
|
87
|
+
# 산출물이라 판정을 막지 않는다 — 그래도 기록은 남긴다. 없던 일로 하면
|
|
88
|
+
# 워커가 워크트리에 무엇을 남겼는지 아무도 모른다.
|
|
89
|
+
untracked_paths: tuple[str, ...]
|
|
86
90
|
source_changed: bool
|
|
87
91
|
git_changed: bool
|
|
88
92
|
attribution: str
|
|
@@ -96,6 +100,7 @@ class MutationAuditResult:
|
|
|
96
100
|
return {
|
|
97
101
|
"mutationStatus": self.status,
|
|
98
102
|
"changedPaths": list(self.changed_paths),
|
|
103
|
+
"untrackedPaths": list(self.untracked_paths),
|
|
99
104
|
"sourceChanged": self.source_changed,
|
|
100
105
|
"gitChanged": self.git_changed,
|
|
101
106
|
"attribution": self.attribution,
|
|
@@ -118,11 +123,13 @@ class ExecutionMutationAudit:
|
|
|
118
123
|
rows = tuple(policies)
|
|
119
124
|
root, artifact_root = _validate_batch(rows)
|
|
120
125
|
generated = _generated_paths(rows)
|
|
121
|
-
file_digests = _content_snapshot(root, generated)
|
|
126
|
+
file_digests = _content_snapshot(root, generated | _ignored_paths(root))
|
|
122
127
|
artifact_digests = (
|
|
123
128
|
file_digests
|
|
124
129
|
if artifact_root == root
|
|
125
|
-
else _content_snapshot(
|
|
130
|
+
else _content_snapshot(
|
|
131
|
+
artifact_root, generated | _ignored_paths(artifact_root)
|
|
132
|
+
)
|
|
126
133
|
)
|
|
127
134
|
scratch_digests = _scratch_snapshot(rows)
|
|
128
135
|
git_projection = _git_projection(root)
|
|
@@ -174,7 +181,9 @@ class ExecutionMutationAudit:
|
|
|
174
181
|
scratch_changed = _changed_keys(
|
|
175
182
|
before.scratch_digests, after.scratch_digests
|
|
176
183
|
)
|
|
177
|
-
source_changes =
|
|
184
|
+
source_changes, untracked_changes = _split_tracked(
|
|
185
|
+
before.root, _source_changes(changed, rows, before)
|
|
186
|
+
)
|
|
178
187
|
# Reported and, through `_retry_allowed`, load-bearing: a stat-cache
|
|
179
188
|
# refresh must not read as "this worker touched Git" and must not
|
|
180
189
|
# withhold the retry a worker that failed for another reason is owed.
|
|
@@ -203,6 +212,7 @@ class ExecutionMutationAudit:
|
|
|
203
212
|
return MutationAuditResult(
|
|
204
213
|
status=status,
|
|
205
214
|
changed_paths=tuple(sorted(source_changes)),
|
|
215
|
+
untracked_paths=tuple(sorted(untracked_changes)),
|
|
206
216
|
source_changed=bool(source_changes),
|
|
207
217
|
git_changed=git_changed,
|
|
208
218
|
attribution="batch-unattributed",
|
|
@@ -273,7 +283,9 @@ when a run produces the same evidence for it, not before.
|
|
|
273
283
|
"""
|
|
274
284
|
|
|
275
285
|
|
|
276
|
-
def _content_snapshot(root: Path,
|
|
286
|
+
def _content_snapshot(root: Path, excluded: frozenset[str]) -> dict[str, str]:
|
|
287
|
+
"""감사 대상 트리의 내용 다이제스트. `excluded` 는 정책이 선언한 생성
|
|
288
|
+
경로와 레포가 무시하는 경로를 합친 접두사 집합이다."""
|
|
277
289
|
rows: dict[str, str] = {}
|
|
278
290
|
for current, directories, filenames in os.walk(root, followlinks=False):
|
|
279
291
|
current_path = Path(current)
|
|
@@ -284,7 +296,7 @@ def _content_snapshot(root: Path, generated: frozenset[str]) -> dict[str, str]:
|
|
|
284
296
|
if (
|
|
285
297
|
name == ".git"
|
|
286
298
|
or name in _INSTALLED_DEPENDENCY_DIRS
|
|
287
|
-
or _excluded(relative_path,
|
|
299
|
+
or _excluded(relative_path, excluded)
|
|
288
300
|
):
|
|
289
301
|
continue
|
|
290
302
|
if path.is_symlink():
|
|
@@ -295,7 +307,7 @@ def _content_snapshot(root: Path, generated: frozenset[str]) -> dict[str, str]:
|
|
|
295
307
|
for name in sorted(filenames):
|
|
296
308
|
path = current_path / name
|
|
297
309
|
relative = path.relative_to(root).as_posix()
|
|
298
|
-
if relative == ".git" or _excluded(Path(relative),
|
|
310
|
+
if relative == ".git" or _excluded(Path(relative), excluded):
|
|
299
311
|
continue
|
|
300
312
|
rows[relative] = _path_digest(path)
|
|
301
313
|
return rows
|
|
@@ -327,9 +339,79 @@ def _path_digest(path: Path) -> str:
|
|
|
327
339
|
return "sha256:" + hashlib.sha256(value).hexdigest()
|
|
328
340
|
|
|
329
341
|
|
|
330
|
-
def _excluded(path: Path,
|
|
342
|
+
def _excluded(path: Path, excluded: frozenset[str]) -> bool:
|
|
331
343
|
relative = path.as_posix()
|
|
332
|
-
return any(relative == item or relative.startswith(item + "/") for item in
|
|
344
|
+
return any(relative == item or relative.startswith(item + "/") for item in excluded)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _ignored_paths(root: Path) -> frozenset[str]:
|
|
348
|
+
"""레포가 스스로 소스가 아니라고 선언한 경로.
|
|
349
|
+
|
|
350
|
+
감사는 워커가 *소스* 를 바꿨는지를 묻는다. `.gitignore` 는 그 레포에서
|
|
351
|
+
무엇이 소스가 아닌지에 대한 사람이 쓴 답이므로, 그 답을 여기서 다시 쓴다 —
|
|
352
|
+
okstra 가 파일 이름 목록을 따로 들고 있으면 레포마다 틀린다.
|
|
353
|
+
|
|
354
|
+
이 면제가 필요한 이유는 감사 창이 사람·OS 와 공유되기 때문이다. 실측
|
|
355
|
+
(2026-08-26, `fontsninja-nlpvibe` planning run 019~022): 완주한 워커의 결과가
|
|
356
|
+
버려진 10건 중 2건의 유발자가 Finder 가 쓴 `.DS_Store` 와 IDE 가 쓴
|
|
357
|
+
`.idea/workspace.xml` 이었다. 어느 쪽도 워커가 쓴 것이 아니고, 둘 다 그
|
|
358
|
+
레포의 무시 대상이다.
|
|
359
|
+
|
|
360
|
+
추적 중인 파일은 git 이 무시 대상으로 세지 않으므로 그대로 감사한다 —
|
|
361
|
+
나머지 8건처럼 사람이 추적 소스를 동시에 고친 경우는 이 면제가 가리지
|
|
362
|
+
않는다. git 이 없거나 레포가 아니면 빈 집합을 돌려 종전대로 전부 감사한다.
|
|
363
|
+
"""
|
|
364
|
+
result = subprocess.run(
|
|
365
|
+
["git", "-C", str(root), "ls-files", "-z", "--others", "--ignored",
|
|
366
|
+
"--exclude-standard", "--directory"],
|
|
367
|
+
capture_output=True,
|
|
368
|
+
text=True,
|
|
369
|
+
check=False,
|
|
370
|
+
)
|
|
371
|
+
if result.returncode != 0:
|
|
372
|
+
return frozenset()
|
|
373
|
+
# `--directory` 는 통째로 무시되는 디렉터리를 `name/` 한 줄로 접는다.
|
|
374
|
+
# `_excluded` 는 접두사로 대조하므로 그 꼬리 슬래시를 떼어 준다.
|
|
375
|
+
return frozenset(
|
|
376
|
+
entry.rstrip("/") for entry in result.stdout.split("\0") if entry
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _tracked_paths(root: Path) -> frozenset[str] | None:
|
|
381
|
+
"""이 레포가 추적 중인 파일. git 이 없거나 레포가 아니면 ``None``.
|
|
382
|
+
|
|
383
|
+
`source-readonly` 가 묻는 것은 워커가 **소스** 를 바꿨는가이다. 추적 중인
|
|
384
|
+
파일의 변경이 그 답이다. 추적되지 않는 새 파일은 소스의 변경이 아니라
|
|
385
|
+
명령이 남긴 산출물이고, 그 둘을 한 등급으로 묶으면 규칙을 만족시킬 수 있는
|
|
386
|
+
실물 조건이 없어진다 — 프로필은 검증자에게 승인 계획의 Tier 1 명령을 독립
|
|
387
|
+
재실행하라고 요구하는데, 실제 프로젝트의 그 명령들은 빌드 산출물과 평가
|
|
388
|
+
리포트를 쓴다. 실측(2026-08-26, `fontsninja-nlpvibe` final-verification
|
|
389
|
+
run 005): 계획대로 게이트를 돌린 검증자 셋이 전부
|
|
390
|
+
`readonly source changed [eval/reports/clip-baseline.{html,json}]` 로
|
|
391
|
+
버려졌다. 두 파일은 추적되지 않는 생성 산출물이고, 계획이 승인 시점에
|
|
392
|
+
적어 둔 명령이 만든 것이다.
|
|
393
|
+
|
|
394
|
+
``None`` 은 "판정할 수 없음" 이다 — 그때는 좁히지 않고 종전대로 전부
|
|
395
|
+
소스 변경으로 본다. 추적 여부를 모르면서 면제하면 진짜 소스 변경을 놓친다.
|
|
396
|
+
"""
|
|
397
|
+
result = subprocess.run(
|
|
398
|
+
["git", "-C", str(root), "ls-files", "-z"],
|
|
399
|
+
capture_output=True,
|
|
400
|
+
text=True,
|
|
401
|
+
check=False,
|
|
402
|
+
)
|
|
403
|
+
if result.returncode != 0:
|
|
404
|
+
return None
|
|
405
|
+
return frozenset(entry for entry in result.stdout.split("\0") if entry)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _split_tracked(root: Path, changes: set[str]) -> tuple[set[str], set[str]]:
|
|
409
|
+
"""``(추적 중인 소스 변경, 추적되지 않는 산출물)``."""
|
|
410
|
+
tracked = _tracked_paths(root)
|
|
411
|
+
if tracked is None:
|
|
412
|
+
return set(changes), set()
|
|
413
|
+
untracked = {path for path in changes if path not in tracked}
|
|
414
|
+
return changes - untracked, untracked
|
|
333
415
|
|
|
334
416
|
|
|
335
417
|
def _generated_paths(policies: Sequence[WritePolicy]) -> frozenset[str]:
|
|
@@ -119,3 +119,20 @@ def translation_sidecar_path(data_path: Path, lang: str) -> Path:
|
|
|
119
119
|
overwrite it.
|
|
120
120
|
"""
|
|
121
121
|
return data_path.with_name(f"{_stem(data_path)}.i18n.{lang}.json")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def sidecar_source_rel(path: Path) -> str:
|
|
125
|
+
"""사이드카 프론트매터가 `source-report` / `source-data` 에 적는 경로.
|
|
126
|
+
|
|
127
|
+
기준점은 **태스크 루트**다 (`runs/<task-type>/reports/...`). 사이드카를
|
|
128
|
+
읽는 쪽은 리포트를 태스크 루트에서 되짚으므로
|
|
129
|
+
(`implementation_direction._relative_source`), 다른 기준으로 적힌 값은
|
|
130
|
+
같은 파일을 가리켜도 대조에 실패한다.
|
|
131
|
+
|
|
132
|
+
HTML 열람본을 렌더할 때 넘기는 `--source-report` 와 CLI 가 직접 쓰는
|
|
133
|
+
사이드카가 이 함수 하나를 공유한다. 둘이 각자 계산하던 동안 HTML Export 는
|
|
134
|
+
프로젝트 루트 기준을 적었고, 그 사이드카는 방향 선택 게이트를 통과할 수
|
|
135
|
+
없었다.
|
|
136
|
+
"""
|
|
137
|
+
indices = [index for index, part in enumerate(path.parts) if part == "runs"]
|
|
138
|
+
return Path(*path.parts[indices[-1]:]).as_posix() if indices else path.name
|