okstra 0.186.7 → 0.187.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 +104 -39
- 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 +37 -36
- 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 +25 -18
- 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/convergence.md +96 -87
- 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 +34 -4
- 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 +40 -22
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +14 -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} +12 -1
- 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 +215 -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 +408 -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 +260 -8
- 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 +16 -2
- package/runtime/python/okstra_ctl/contract_graph_cli.py +7 -0
- package/runtime/python/okstra_ctl/convergence.py +101 -15
- 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 +102 -25
- package/runtime/python/okstra_ctl/dispatch_state.py +268 -39
- 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 +21 -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/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 +90 -16
- 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 +25 -11
- 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 +3 -7
- 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 +206 -15
- 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 +90 -3
- package/runtime/python/okstra_ctl/report_projections.py +8 -2
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +258 -1
- package/runtime/python/okstra_ctl/report_translation.py +2 -35
- 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/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 +16 -9
- 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/claude.py +9 -1
- package/runtime/python/okstra_token_usage/cli.py +3 -1
- package/runtime/python/okstra_token_usage/collect.py +71 -10
- 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 +19 -7
- 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 +1 -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 +1369 -2654
- 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
|
@@ -74,6 +74,11 @@ class WorktreeEntry:
|
|
|
74
74
|
stage: Optional[int] = None
|
|
75
75
|
implementation_base_commit: str = ""
|
|
76
76
|
stages: Optional[list] = None
|
|
77
|
+
# 프로비저닝 시점에 공유 sync 파일이 갖고 있던 **키 이름** 집합
|
|
78
|
+
# ({relative path: [name, ...]}). 값은 담지 않는다. 다음 런에서 대조해
|
|
79
|
+
# `.env` 키 개명을 알린다 — 그 파일은 worktree 마다 심링크로 공유돼
|
|
80
|
+
# 브랜치 격리가 통하지 않는다.
|
|
81
|
+
sync_file_keys: Optional[dict] = None
|
|
77
82
|
|
|
78
83
|
|
|
79
84
|
def _registry_lock():
|
|
@@ -122,6 +127,7 @@ def reserve(
|
|
|
122
127
|
stage_number: Optional[int] = None,
|
|
123
128
|
group_id: Optional[str] = None,
|
|
124
129
|
stages: Optional[list] = None,
|
|
130
|
+
sync_file_keys: Optional[dict] = None,
|
|
125
131
|
) -> WorktreeEntry:
|
|
126
132
|
"""Atomically insert a new entry. Raises RuntimeError if the
|
|
127
133
|
task-key already exists or the branch is already owned by a
|
|
@@ -158,6 +164,7 @@ def reserve(
|
|
|
158
164
|
"status": "active",
|
|
159
165
|
"stage": stage_number,
|
|
160
166
|
"stages": stages,
|
|
167
|
+
"sync_file_keys": sync_file_keys,
|
|
161
168
|
}
|
|
162
169
|
# Re-reserving a released task-key must not drop the anchor: the
|
|
163
170
|
# implementation base commit is task-lifetime state, not per-reservation,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""`okstra worktree-status` 의 진입점. 핸들러는 [worktree_cli.status] 에 있다."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
|
|
6
|
+
from .worktree_cli import status
|
|
7
|
+
|
|
8
|
+
_CLI_EPILOG = r"""Usage:
|
|
9
|
+
okstra worktree-status [--path <dir>] [--check-clean]
|
|
10
|
+
|
|
11
|
+
okstra provisions `.okstra` plus the configured sync entries (`.project-docs`,
|
|
12
|
+
`.claude`, …) into every task worktree, and an `implementation` stage worktree
|
|
13
|
+
sits inside its task worktree. A bare `git status --porcelain` therefore never
|
|
14
|
+
comes back empty there, so a plan step asserting a clean tree with one fails on
|
|
15
|
+
okstra's scaffolding rather than on the stage's own work. This command asks the
|
|
16
|
+
same question against source paths only — the exact gate okstra's own handoff
|
|
17
|
+
and stage-integration steps use.
|
|
18
|
+
|
|
19
|
+
Use it as the assertion in a plan step:
|
|
20
|
+
okstra worktree-status --check-clean
|
|
21
|
+
|
|
22
|
+
Do not chain a `git tag stage-<N>-exit` onto it. okstra writes that tag when it
|
|
23
|
+
settles the stage, at the commit the carry evidence records; a step that tags
|
|
24
|
+
while the stage is still running puts it on an earlier commit.
|
|
25
|
+
|
|
26
|
+
Output: JSON { ok, path, clean, entries, excluded }. `entries` holds the
|
|
27
|
+
`git status --short` rows that made it dirty; `excluded` lists the paths left
|
|
28
|
+
out of the question.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(argv: list[str] | None = None) -> int:
|
|
33
|
+
parser = argparse.ArgumentParser(
|
|
34
|
+
prog="okstra worktree-status",
|
|
35
|
+
description="Clean-worktree check that ignores okstra's own scaffolding.",
|
|
36
|
+
epilog=_CLI_EPILOG,
|
|
37
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--path", default=".",
|
|
41
|
+
help="directory to inspect (default: the current directory). There is "
|
|
42
|
+
"no --project-root: point --path at the stage or task worktree "
|
|
43
|
+
"you mean, or run it from inside one.",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--check-clean", action="store_true",
|
|
47
|
+
help="exit 1 when the tree is dirty (default: always exit 0)",
|
|
48
|
+
)
|
|
49
|
+
return status(parser.parse_args(argv))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
raise SystemExit(main())
|
|
@@ -9,75 +9,26 @@ from dataclasses import dataclass
|
|
|
9
9
|
from pathlib import Path, PurePosixPath
|
|
10
10
|
from typing import Any, Literal
|
|
11
11
|
|
|
12
|
+
from .domain.write_policy import ( # noqa: F401 — 재노출(값 코어는 도메인 소유)
|
|
13
|
+
BoundaryPrecision,
|
|
14
|
+
WriteEnforcement,
|
|
15
|
+
WritePolicy,
|
|
16
|
+
WritePolicyError,
|
|
17
|
+
_absolute_text,
|
|
18
|
+
_mapping,
|
|
19
|
+
_PRECISION_RANK,
|
|
20
|
+
_relative_paths,
|
|
21
|
+
_text_sequence,
|
|
22
|
+
validate_write_enforcement_payload,
|
|
23
|
+
validate_write_policy_payload,
|
|
24
|
+
write_enforcement_from_payload,
|
|
25
|
+
write_policy_digest,
|
|
26
|
+
write_policy_from_payload,
|
|
27
|
+
)
|
|
28
|
+
from .final_report_paths import final_report_data_path
|
|
12
29
|
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
_PRECISION_RANK = {"none": 0, "directory-boundary": 1, "exact-path": 2}
|
|
17
|
-
_POLICY_KEYS = {
|
|
18
|
-
"artifactPolicy",
|
|
19
|
-
"sourcePolicy",
|
|
20
|
-
"gitPolicy",
|
|
21
|
-
"auxiliaryPolicy",
|
|
22
|
-
}
|
|
23
|
-
_ENFORCEMENT_KEYS = {
|
|
24
|
-
"boundaryPrecision",
|
|
25
|
-
"mutationAudit",
|
|
26
|
-
"attribution",
|
|
27
|
-
"auditedRoots",
|
|
28
|
-
"observationCoverage",
|
|
29
|
-
"unobservedWriteSurfaces",
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class WritePolicyError(ValueError):
|
|
34
|
-
"""Raised when a write policy cannot be made canonical or trustworthy."""
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@dataclass(frozen=True)
|
|
38
|
-
class WritePolicy:
|
|
39
|
-
artifact_policy: Mapping[str, Any]
|
|
40
|
-
source_policy: Mapping[str, Any]
|
|
41
|
-
git_policy: Mapping[str, Any]
|
|
42
|
-
auxiliary_policy: Mapping[str, Any]
|
|
43
|
-
maximum_boundary_precision: BoundaryPrecision = "none"
|
|
44
|
-
|
|
45
|
-
def to_payload(self) -> dict[str, Any]:
|
|
46
|
-
return {
|
|
47
|
-
"artifactPolicy": dict(self.artifact_policy),
|
|
48
|
-
"sourcePolicy": dict(self.source_policy),
|
|
49
|
-
"gitPolicy": dict(self.git_policy),
|
|
50
|
-
"auxiliaryPolicy": dict(self.auxiliary_policy),
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
@property
|
|
54
|
-
def digest(self) -> str:
|
|
55
|
-
return write_policy_digest(self.to_payload())
|
|
56
|
-
|
|
57
|
-
@property
|
|
58
|
-
def source_mode(self) -> str:
|
|
59
|
-
return str(self.source_policy["mode"])
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
@dataclass(frozen=True)
|
|
63
|
-
class WriteEnforcement:
|
|
64
|
-
boundary_precision: BoundaryPrecision
|
|
65
|
-
mutation_audit: Literal["none", "batch"]
|
|
66
|
-
attribution: Literal["call", "batch-unattributed"]
|
|
67
|
-
audited_roots: tuple[Path, ...]
|
|
68
|
-
observation_coverage: Literal["complete", "scoped", "partial"]
|
|
69
|
-
unobserved_write_surfaces: tuple[str, ...]
|
|
70
|
-
|
|
71
|
-
def to_payload(self) -> dict[str, Any]:
|
|
72
|
-
return {
|
|
73
|
-
"boundaryPrecision": self.boundary_precision,
|
|
74
|
-
"mutationAudit": self.mutation_audit,
|
|
75
|
-
"attribution": self.attribution,
|
|
76
|
-
"auditedRoots": [str(path) for path in self.audited_roots],
|
|
77
|
-
"observationCoverage": self.observation_coverage,
|
|
78
|
-
"unobservedWriteSurfaces": list(self.unobserved_write_surfaces),
|
|
79
|
-
}
|
|
80
|
-
|
|
30
|
+
from .path_hints import hydrate_active_run_context
|
|
31
|
+
from .paths import task_conformance_manifest_file, task_qa_dir
|
|
81
32
|
|
|
82
33
|
def build_write_policy(
|
|
83
34
|
invocation: Mapping[str, Any],
|
|
@@ -125,12 +76,55 @@ def build_write_policy(
|
|
|
125
76
|
return policy
|
|
126
77
|
|
|
127
78
|
|
|
79
|
+
def _role_qa_artifact_paths(role: str, task_root: Path | None) -> tuple[Path, ...]:
|
|
80
|
+
"""The task `qa/` writes a role's own contract makes mandatory.
|
|
81
|
+
|
|
82
|
+
`qa/` is a sibling of `runs/` under the task root, so the orchestrator
|
|
83
|
+
allowance — which carries the run directory tree and the task manifest FILE
|
|
84
|
+
— never reached it, and no worker artifact path names it. In a worktree run
|
|
85
|
+
the artifact root is the project root while the source root is the
|
|
86
|
+
worktree, so `_artifact_policy_failures` is live and every such write read
|
|
87
|
+
as `artifact-root change exceeds batch policy union`: a role following its
|
|
88
|
+
own profile could not pass the audit.
|
|
89
|
+
|
|
90
|
+
Split by role because ownership is split. The executor writes the Tier 3
|
|
91
|
+
conformance script, its `tsconfig.json` and any real-IO qa spec under
|
|
92
|
+
`qa/scripts/`, plus the manifest entry naming them
|
|
93
|
+
(`_implementation-executor.md` §"Stage conformance script", §"Real-IO test
|
|
94
|
+
isolation"). Everything else under `qa/` — the self-mock sidecar and its
|
|
95
|
+
diff, the conformance run's `result-*.json` — is written while the verifier
|
|
96
|
+
runs its own gates. Keeping the executor's grant to those two entries is
|
|
97
|
+
what leaves the audit able to catch an executor that runs the verifier's
|
|
98
|
+
self-mock gate (`_implementation-executor.md` §"Verifier gates are not
|
|
99
|
+
yours to run"); widening it to `qa/` would silence that.
|
|
100
|
+
|
|
101
|
+
The verifier's grant is the tree because the filenames are not knowable
|
|
102
|
+
here — the sidecar carries the run's stage name and a conformance script
|
|
103
|
+
names its own `result-*.json`. `qa/self-mock-waivers.json` is inside that
|
|
104
|
+
grant and stays forbidden to it, enforced where the acknowledgement is read
|
|
105
|
+
rather than where the byte is written: `validate-run.py::_validate_selfmock`
|
|
106
|
+
fails a report whose waived entries came from anywhere but the user's file,
|
|
107
|
+
or lack a `reason` / `acknowledgedBy`.
|
|
108
|
+
"""
|
|
109
|
+
if task_root is None:
|
|
110
|
+
return ()
|
|
111
|
+
if role == "implementer":
|
|
112
|
+
return (
|
|
113
|
+
task_qa_dir(task_root) / "scripts",
|
|
114
|
+
task_conformance_manifest_file(task_root),
|
|
115
|
+
)
|
|
116
|
+
if role == "verifier":
|
|
117
|
+
return (task_qa_dir(task_root),)
|
|
118
|
+
return ()
|
|
119
|
+
|
|
120
|
+
|
|
128
121
|
def build_invocation_write_contract(
|
|
129
122
|
*,
|
|
130
123
|
role: str,
|
|
131
124
|
project_root: Path,
|
|
132
125
|
worktree: Path | None,
|
|
133
126
|
artifact_paths: Sequence[Path],
|
|
127
|
+
task_root: Path | None,
|
|
134
128
|
maximum_precision: BoundaryPrecision,
|
|
135
129
|
planned_paths: Sequence[str] = (),
|
|
136
130
|
planned_paths_declared: bool = True,
|
|
@@ -142,7 +136,13 @@ def build_invocation_write_contract(
|
|
|
142
136
|
"""Build the single canonical contract used by every v2 dispatcher."""
|
|
143
137
|
root = project_root.resolve()
|
|
144
138
|
relative_artifacts = tuple(
|
|
145
|
-
|
|
139
|
+
dict.fromkeys(
|
|
140
|
+
_relative_to_root(path, root, "artifact")
|
|
141
|
+
for path in (
|
|
142
|
+
*artifact_paths,
|
|
143
|
+
*_role_qa_artifact_paths(role, task_root),
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
146
|
)
|
|
147
147
|
invocation: dict[str, Any] = {
|
|
148
148
|
"role": role,
|
|
@@ -173,6 +173,22 @@ def build_invocation_write_contract(
|
|
|
173
173
|
return policy, derive_write_enforcement(policy, maximum_precision, role=role)
|
|
174
174
|
|
|
175
175
|
|
|
176
|
+
def task_root_from_run_manifest(
|
|
177
|
+
project_root: Path, manifest: Mapping[str, Any]
|
|
178
|
+
) -> Path | None:
|
|
179
|
+
"""The task root this run belongs to, read from the manifest's own pointer.
|
|
180
|
+
|
|
181
|
+
Derived from `taskManifestPath` rather than by counting directories up from
|
|
182
|
+
the run directory: an `implementation` run carries an extra `stage-<N>`
|
|
183
|
+
level, so one fixed index cannot reach the task root for every task type.
|
|
184
|
+
"""
|
|
185
|
+
value = manifest.get("taskManifestPath")
|
|
186
|
+
if not isinstance(value, str) or not value:
|
|
187
|
+
return None
|
|
188
|
+
candidate = Path(value)
|
|
189
|
+
return (candidate if candidate.is_absolute() else project_root / candidate).parent
|
|
190
|
+
|
|
191
|
+
|
|
176
192
|
def planned_paths_from_run_manifest(
|
|
177
193
|
project_root: Path, manifest: Mapping[str, Any]
|
|
178
194
|
) -> tuple[str, ...]:
|
|
@@ -183,8 +199,6 @@ def planned_paths_from_run_manifest(
|
|
|
183
199
|
# hints instead; the hydrator is what puts the block back. Reading the file
|
|
184
200
|
# raw always saw `None` here and reported the run as having no approved
|
|
185
201
|
# stage authority, which blocked every implementer dispatch.
|
|
186
|
-
from .path_hints import hydrate_active_run_context
|
|
187
|
-
|
|
188
202
|
active = hydrate_active_run_context(
|
|
189
203
|
_read_json(_rooted(project_root, active_value), "active run context")
|
|
190
204
|
)
|
|
@@ -274,111 +288,6 @@ def derive_write_enforcement(
|
|
|
274
288
|
return enforcement
|
|
275
289
|
|
|
276
290
|
|
|
277
|
-
def write_policy_from_payload(payload: Mapping[str, Any]) -> WritePolicy:
|
|
278
|
-
validate_write_policy_payload(payload)
|
|
279
|
-
return WritePolicy(
|
|
280
|
-
artifact_policy=dict(payload["artifactPolicy"]),
|
|
281
|
-
source_policy=dict(payload["sourcePolicy"]),
|
|
282
|
-
git_policy=dict(payload["gitPolicy"]),
|
|
283
|
-
auxiliary_policy=dict(payload["auxiliaryPolicy"]),
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def write_enforcement_from_payload(
|
|
288
|
-
payload: Mapping[str, Any],
|
|
289
|
-
) -> WriteEnforcement:
|
|
290
|
-
validate_write_enforcement_payload(payload)
|
|
291
|
-
return WriteEnforcement(
|
|
292
|
-
boundary_precision=payload["boundaryPrecision"],
|
|
293
|
-
mutation_audit=payload["mutationAudit"],
|
|
294
|
-
attribution=payload["attribution"],
|
|
295
|
-
audited_roots=tuple(Path(value) for value in payload["auditedRoots"]),
|
|
296
|
-
observation_coverage=payload["observationCoverage"],
|
|
297
|
-
unobserved_write_surfaces=tuple(payload["unobservedWriteSurfaces"]),
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def write_policy_digest(payload: Mapping[str, Any]) -> str:
|
|
302
|
-
validate_write_policy_payload(payload)
|
|
303
|
-
encoded = json.dumps(
|
|
304
|
-
payload,
|
|
305
|
-
sort_keys=True,
|
|
306
|
-
separators=(",", ":"),
|
|
307
|
-
ensure_ascii=False,
|
|
308
|
-
).encode("utf-8")
|
|
309
|
-
return "sha256:" + hashlib.sha256(encoded).hexdigest()
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
def validate_write_policy_payload(payload: Mapping[str, Any]) -> None:
|
|
313
|
-
if set(payload) != _POLICY_KEYS:
|
|
314
|
-
raise WritePolicyError("writePolicy must contain four canonical policy sections")
|
|
315
|
-
artifact = _mapping(payload.get("artifactPolicy"), "artifactPolicy")
|
|
316
|
-
source = _mapping(payload.get("sourcePolicy"), "sourcePolicy")
|
|
317
|
-
git = _mapping(payload.get("gitPolicy"), "gitPolicy")
|
|
318
|
-
auxiliary = _mapping(payload.get("auxiliaryPolicy"), "auxiliaryPolicy")
|
|
319
|
-
if set(artifact) != {"allowedRoot", "allowedPaths"}:
|
|
320
|
-
raise WritePolicyError("artifactPolicy is incomplete")
|
|
321
|
-
_absolute_text(artifact.get("allowedRoot"), "artifactPolicy.allowedRoot")
|
|
322
|
-
_relative_paths(artifact.get("allowedPaths", ()))
|
|
323
|
-
if source.get("mode") not in {"source-readonly", "project-mutation"}:
|
|
324
|
-
raise WritePolicyError("sourcePolicy mode is invalid")
|
|
325
|
-
if set(source) != {"mode", "allowedRoot", "plannedPaths", "protectedPaths"}:
|
|
326
|
-
raise WritePolicyError("sourcePolicy is incomplete")
|
|
327
|
-
_absolute_text(source.get("allowedRoot"), "sourcePolicy.allowedRoot")
|
|
328
|
-
_relative_paths(source.get("plannedPaths", ()))
|
|
329
|
-
_relative_paths(source.get("protectedPaths", ()))
|
|
330
|
-
if git.get("mode") not in {"disabled", "fast-forward-descendant-chain"}:
|
|
331
|
-
raise WritePolicyError("gitPolicy mode is invalid")
|
|
332
|
-
if git.get("mode") == "disabled" and set(git) != {"mode"}:
|
|
333
|
-
raise WritePolicyError("disabled gitPolicy must not grant Git paths")
|
|
334
|
-
required_git = {
|
|
335
|
-
"mode", "worktreeGitDir", "gitCommonDir", "expectedBaseCommit",
|
|
336
|
-
"allowedBranchRef", "commitMode",
|
|
337
|
-
}
|
|
338
|
-
if git.get("mode") != "disabled" and set(git) != required_git:
|
|
339
|
-
raise WritePolicyError("project mutation gitPolicy is incomplete")
|
|
340
|
-
if set(auxiliary) != {"scratchRoots", "generatedPaths", "externalRoots"}:
|
|
341
|
-
raise WritePolicyError("auxiliaryPolicy is incomplete")
|
|
342
|
-
_relative_paths(auxiliary.get("generatedPaths", ()))
|
|
343
|
-
for key in ("scratchRoots", "externalRoots"):
|
|
344
|
-
for value in _text_sequence(auxiliary.get(key), f"auxiliaryPolicy.{key}"):
|
|
345
|
-
_absolute_text(value, f"auxiliaryPolicy.{key}")
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
def validate_write_enforcement_payload(payload: Mapping[str, Any]) -> None:
|
|
349
|
-
if set(payload) != _ENFORCEMENT_KEYS:
|
|
350
|
-
raise WritePolicyError("writeEnforcement is incomplete")
|
|
351
|
-
boundary = payload.get("boundaryPrecision")
|
|
352
|
-
mutation_audit = payload.get("mutationAudit")
|
|
353
|
-
attribution = payload.get("attribution")
|
|
354
|
-
coverage = payload.get("observationCoverage")
|
|
355
|
-
if boundary not in _PRECISION_RANK:
|
|
356
|
-
raise WritePolicyError("writeEnforcement boundaryPrecision is invalid")
|
|
357
|
-
if mutation_audit not in {"none", "batch"}:
|
|
358
|
-
raise WritePolicyError("writeEnforcement mutationAudit is invalid")
|
|
359
|
-
if attribution not in {"call", "batch-unattributed"}:
|
|
360
|
-
raise WritePolicyError("writeEnforcement attribution is invalid")
|
|
361
|
-
if coverage not in {"complete", "scoped", "partial"}:
|
|
362
|
-
raise WritePolicyError("writeEnforcement observationCoverage is invalid")
|
|
363
|
-
if (boundary, mutation_audit, attribution) == ("exact-path", "none", "call"):
|
|
364
|
-
if coverage != "complete":
|
|
365
|
-
raise WritePolicyError("exact enforcement must have complete coverage")
|
|
366
|
-
elif (boundary, mutation_audit, attribution) == (
|
|
367
|
-
"none",
|
|
368
|
-
"none",
|
|
369
|
-
"batch-unattributed",
|
|
370
|
-
):
|
|
371
|
-
if coverage != "partial":
|
|
372
|
-
raise WritePolicyError("host-session enforcement must be partial")
|
|
373
|
-
elif mutation_audit != "batch" or attribution != "batch-unattributed":
|
|
374
|
-
raise WritePolicyError("non-exact enforcement must use batch attribution")
|
|
375
|
-
elif boundary == "exact-path" or coverage == "complete":
|
|
376
|
-
raise WritePolicyError("batch enforcement cannot claim exact coverage")
|
|
377
|
-
for root in _text_sequence(payload.get("auditedRoots"), "auditedRoots"):
|
|
378
|
-
_absolute_text(root, "auditedRoots")
|
|
379
|
-
_text_sequence(payload.get("unobservedWriteSurfaces"), "unobservedWriteSurfaces")
|
|
380
|
-
|
|
381
|
-
|
|
382
291
|
def _git_policy(
|
|
383
292
|
invocation: Mapping[str, Any], root: Path, source_mode: str
|
|
384
293
|
) -> dict[str, Any]:
|
|
@@ -483,7 +392,11 @@ def _planned_paths_from_report(
|
|
|
483
392
|
) -> tuple[tuple[str, ...], bool]:
|
|
484
393
|
"""(paths, declared). `declared` is False for a plan written before the
|
|
485
394
|
structured `plannedPaths` column existed."""
|
|
486
|
-
|
|
395
|
+
# `--approved-plan` 은 레코드(`.data.json`)만 받는다(`require_approved_plan_record`).
|
|
396
|
+
# 손으로 접미사를 붙이면 `.data.json` 에 한 번 더 붙어 `.data.data.json` 이 되고,
|
|
397
|
+
# 그 파일은 없으므로 implementer 디스패치가 전부 막혔다. 정본 헬퍼는 이미
|
|
398
|
+
# 레코드인 경로를 그대로 돌려주고 `.md` 만 짝으로 바꾼다.
|
|
399
|
+
payload = _read_json(final_report_data_path(report_path), "approved plan data")
|
|
487
400
|
planning = payload.get("implementationPlanning")
|
|
488
401
|
stages = planning.get("stages") if isinstance(planning, Mapping) else None
|
|
489
402
|
selected = next(
|
|
@@ -548,36 +461,6 @@ def _read_json(path: Path, label: str) -> Mapping[str, Any]:
|
|
|
548
461
|
return value
|
|
549
462
|
|
|
550
463
|
|
|
551
|
-
def _relative_paths(value: object) -> tuple[str, ...]:
|
|
552
|
-
paths: list[str] = []
|
|
553
|
-
for raw in _text_sequence(value, "relative paths"):
|
|
554
|
-
candidate = PurePosixPath(raw)
|
|
555
|
-
if candidate.is_absolute() or not candidate.parts or ".." in candidate.parts:
|
|
556
|
-
raise WritePolicyError("policy paths must be normalized relative paths")
|
|
557
|
-
normalized = candidate.as_posix()
|
|
558
|
-
if normalized in {"", "."} or normalized != raw:
|
|
559
|
-
raise WritePolicyError("policy paths must be normalized relative paths")
|
|
560
|
-
paths.append(normalized)
|
|
561
|
-
if len(set(paths)) != len(paths):
|
|
562
|
-
raise WritePolicyError("policy paths must be unique")
|
|
563
|
-
return tuple(paths)
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
def _mapping(value: object, label: str) -> Mapping[str, Any]:
|
|
567
|
-
if not isinstance(value, Mapping):
|
|
568
|
-
raise WritePolicyError(f"{label} must be an object")
|
|
569
|
-
return value
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
def _text_sequence(value: object, label: str) -> tuple[str, ...]:
|
|
573
|
-
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
|
|
574
|
-
raise WritePolicyError(f"{label} must be an array")
|
|
575
|
-
rows = tuple(value)
|
|
576
|
-
if any(not isinstance(row, str) or not row for row in rows):
|
|
577
|
-
raise WritePolicyError(f"{label} must contain non-empty strings")
|
|
578
|
-
return rows
|
|
579
|
-
|
|
580
|
-
|
|
581
464
|
def _required_text(payload: Mapping[str, Any], key: str) -> str:
|
|
582
465
|
value = payload.get(key)
|
|
583
466
|
if not isinstance(value, str) or not value.strip():
|
|
@@ -585,12 +468,6 @@ def _required_text(payload: Mapping[str, Any], key: str) -> str:
|
|
|
585
468
|
return value
|
|
586
469
|
|
|
587
470
|
|
|
588
|
-
def _absolute_text(value: object, label: str) -> str:
|
|
589
|
-
if not isinstance(value, str) or not value or not Path(value).is_absolute():
|
|
590
|
-
raise WritePolicyError(f"{label} must be an absolute path")
|
|
591
|
-
return value
|
|
592
|
-
|
|
593
|
-
|
|
594
471
|
def _git_path(root: Path, flag: str) -> Path:
|
|
595
472
|
raw = _git_output(root, "rev-parse", "--path-format=absolute", flag)
|
|
596
473
|
path = Path(raw).resolve()
|
|
@@ -28,7 +28,6 @@ from .resolver import (
|
|
|
28
28
|
)
|
|
29
29
|
from .state import (
|
|
30
30
|
StateError,
|
|
31
|
-
code_review_target_snapshot,
|
|
32
31
|
find_task_root,
|
|
33
32
|
list_project_tasks,
|
|
34
33
|
parse_task_key,
|
|
@@ -40,7 +39,6 @@ from .state import (
|
|
|
40
39
|
resolve_task_identity,
|
|
41
40
|
resolve_task_reference,
|
|
42
41
|
slugify,
|
|
43
|
-
stage_map_read_side_snapshot,
|
|
44
42
|
task_read_side_snapshot,
|
|
45
43
|
)
|
|
46
44
|
|
|
@@ -54,7 +52,6 @@ __all__ = [
|
|
|
54
52
|
"StateError",
|
|
55
53
|
"TASKS_RELATIVE",
|
|
56
54
|
"TASK_CATALOG_RELATIVE",
|
|
57
|
-
"code_review_target_snapshot",
|
|
58
55
|
"discovery_dir",
|
|
59
56
|
"find_task_root",
|
|
60
57
|
"list_project_tasks",
|
|
@@ -70,7 +67,6 @@ __all__ = [
|
|
|
70
67
|
"resolve_task_identity",
|
|
71
68
|
"resolve_task_reference",
|
|
72
69
|
"slugify",
|
|
73
|
-
"stage_map_read_side_snapshot",
|
|
74
70
|
"task_read_side_snapshot",
|
|
75
71
|
"tasks_root",
|
|
76
72
|
"upsert_project_json",
|
|
@@ -42,8 +42,8 @@ TASK_MANIFEST_FILENAME = "task-manifest.json"
|
|
|
42
42
|
"""task root 안의 lifecycle manifest 파일명.
|
|
43
43
|
|
|
44
44
|
`okstra_ctl.paths.task_manifest_path` 와 `okstra_project.state.read_task_manifest`
|
|
45
|
-
양쪽이 이 상수를
|
|
46
|
-
|
|
45
|
+
양쪽이 이 상수를 쓴다. 어느 한쪽에 두면 반대쪽이 그 모듈 전체를 끌어오게 되므로
|
|
46
|
+
의존 0인 이 자리가 두 계층의 공유 지점이다."""
|
|
47
47
|
DISCOVERY_RELATIVE = OKSTRA_RELATIVE / "discovery"
|
|
48
48
|
TASK_CATALOG_RELATIVE = DISCOVERY_RELATIVE / "task-catalog.json"
|
|
49
49
|
LATEST_TASK_RELATIVE = DISCOVERY_RELATIVE / "latest-task.json"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""다음 Phase 포인터 값의 shape · 승격 규칙.
|
|
2
|
+
|
|
3
|
+
`{phase, status, rationale}` 구조체를 만들고, 판별하고, 구형 문자열에서
|
|
4
|
+
승격시키는 것까지만 소유한다. 다음 phase 를 **계산**하는 일은 이 모듈이
|
|
5
|
+
하지 않는다 — 그것은 `okstra_ctl.next_phase` 의 몫이고, 그 모듈이 여기의
|
|
6
|
+
심볼을 재노출한다.
|
|
7
|
+
|
|
8
|
+
왜 okstra_ctl 이 아니라 여기 있는가:
|
|
9
|
+
|
|
10
|
+
읽기측 상태 접근자 `okstra_project.state` 가 매니페스트를 돌려줄 때마다
|
|
11
|
+
포인터를 승격시켜야 한다. 그런데 `okstra_ctl` 의 어느 모듈을 import 하든
|
|
12
|
+
`okstra_ctl/__init__.py` 가 먼저 실행되고, 그 `__init__` 은 `.ids` 를 거쳐
|
|
13
|
+
`okstra_project.state` 의 `slugify` 를 도로 끌어온다. 즉 이 값 타입을
|
|
14
|
+
`okstra_ctl` 안에 두면 leaf 로 만들어도 패키지 `__init__` 을 통해 순환이
|
|
15
|
+
되살아난다. 그래서 하위 계층이 소유하고 상위 계층이 재노출한다.
|
|
16
|
+
|
|
17
|
+
이 모듈은 okstra 안의 어떤 것도 import 하지 않는다. 그 성질이 위 설명의
|
|
18
|
+
전제이므로 import 를 추가하기 전에 순환이 되살아나지 않는지 확인할 것.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Any, Mapping
|
|
23
|
+
|
|
24
|
+
STATUS_READY = "ready"
|
|
25
|
+
STATUS_PENDING = "pending"
|
|
26
|
+
STATUS_BLOCKED = "blocked"
|
|
27
|
+
STATUS_TERMINAL = "terminal"
|
|
28
|
+
|
|
29
|
+
POINTER_STATUSES = frozenset(
|
|
30
|
+
{STATUS_READY, STATUS_PENDING, STATUS_BLOCKED, STATUS_TERMINAL}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# 구형 문자열 포인터가 쓰던 센티널 → status. 이 표는 승격에만 쓰이며
|
|
34
|
+
# 새 포인터를 만드는 데는 절대 쓰지 않는다.
|
|
35
|
+
LEGACY_SENTINELS = {
|
|
36
|
+
"": STATUS_PENDING,
|
|
37
|
+
"unknown": STATUS_PENDING,
|
|
38
|
+
"pending-routing-decision": STATUS_PENDING,
|
|
39
|
+
"pending-release-handoff": STATUS_PENDING,
|
|
40
|
+
"done-or-follow-up": STATUS_TERMINAL,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def make(
|
|
45
|
+
phase: str = "", status: str = STATUS_PENDING, rationale: str = ""
|
|
46
|
+
) -> dict[str, str]:
|
|
47
|
+
"""포인터 구조체 하나를 만든다. status 가 허용 집합 밖이면 거부한다."""
|
|
48
|
+
if status not in POINTER_STATUSES:
|
|
49
|
+
raise ValueError(f"unknown next-phase status: {status!r}")
|
|
50
|
+
return {"phase": phase, "status": status, "rationale": rationale}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_pointer(value: Any) -> bool:
|
|
54
|
+
"""value 가 유효한 포인터 구조체인지."""
|
|
55
|
+
return (
|
|
56
|
+
isinstance(value, Mapping)
|
|
57
|
+
and isinstance(value.get("phase"), str)
|
|
58
|
+
and value.get("status") in POINTER_STATUSES
|
|
59
|
+
and isinstance(value.get("rationale"), str)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def promote(value: Any) -> dict[str, str]:
|
|
64
|
+
"""구형 문자열·None 을 포인터로 올린다.
|
|
65
|
+
|
|
66
|
+
이미 포인터인 입력은 같은 값을 담은 새 dict 로 돌려준다 — 입력을 그대로
|
|
67
|
+
돌려주지 않는다. 호출부가 반환값을 그 자리에서 변형해도 manifest 원본이
|
|
68
|
+
오염되지 않아야 하기 때문이다.
|
|
69
|
+
|
|
70
|
+
pre-v1 무호환 원칙의 의도적 예외다. 근거는 업그레이드를 가로지르는
|
|
71
|
+
진행 중 태스크가 영구히 막히는 기존 사례 — ADR-0004 Consequences.
|
|
72
|
+
"""
|
|
73
|
+
if is_pointer(value):
|
|
74
|
+
return {
|
|
75
|
+
"phase": value["phase"],
|
|
76
|
+
"status": value["status"],
|
|
77
|
+
"rationale": value["rationale"],
|
|
78
|
+
}
|
|
79
|
+
if not isinstance(value, str):
|
|
80
|
+
return make()
|
|
81
|
+
text = value.strip()
|
|
82
|
+
if text in LEGACY_SENTINELS:
|
|
83
|
+
return make(status=LEGACY_SENTINELS[text])
|
|
84
|
+
return make(phase=text, status=STATUS_READY)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""디스크 경로 세그먼트 slug 규칙.
|
|
2
|
+
|
|
3
|
+
`okstra_ctl.ids` 와 `okstra_ctl.paths` 가 경로를 조립할 때마다 쓰는 순수 규칙이다.
|
|
4
|
+
이 규칙이 `okstra_project.state`(622줄짜리 상태 접근자) 안에 있던 동안에는
|
|
5
|
+
`okstra_ctl/__init__.py` → `.ids` → `okstra_project.state` 경로가 생겨,
|
|
6
|
+
`state` 가 okstra_ctl 을 되짚는 모든 호출을 함수 본문 지연 import 로 밀어냈다.
|
|
7
|
+
그 쐐기를 뽑기 위해 규칙만 leaf 로 분리한다.
|
|
8
|
+
|
|
9
|
+
이 모듈은 표준 라이브러리 외에 아무것도 import 하지 않는다. 그 성질이 위
|
|
10
|
+
설명의 전제이므로 import 를 추가하기 전에 순환이 되살아나지 않는지 확인할 것.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
def slugify(value: str) -> str:
|
|
17
|
+
"""task-group / task-id segment 를 디스크 경로용 slug 로 정규화한다.
|
|
18
|
+
|
|
19
|
+
소문자화 + 알파넘 외 문자 → '-' 로 collapse.
|
|
20
|
+
okstra.sh 의 path-resolve.sh slug 규칙과 일치해야 한다.
|
|
21
|
+
"""
|
|
22
|
+
value = (value or "").lower()
|
|
23
|
+
value = re.sub(r"[^a-z0-9]+", "-", value).strip("-")
|
|
24
|
+
return value
|