okstra 0.201.3 → 0.204.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli-registry.mjs +7 -7
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/lifecycle/install.mjs +50 -124
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/dist/commands/lifecycle/setup.mjs +15 -0
- package/dist/commands/lifecycle/setup.mjs.map +1 -1
- package/dist/commands/memory/memory.mjs +41 -8
- package/dist/commands/memory/memory.mjs.map +1 -1
- package/dist/lib/citation-guidance.d.mts +21 -0
- package/dist/lib/citation-guidance.mjs +79 -0
- package/dist/lib/citation-guidance.mjs.map +1 -0
- package/dist/lib/install-assets.mjs +3 -0
- package/dist/lib/install-assets.mjs.map +1 -1
- package/dist/lib/runtime-manifest.mjs +2 -1
- package/dist/lib/runtime-manifest.mjs.map +1 -1
- package/dist/lib/types.d.mts +2 -1
- package/docs/architecture/storage-model.md +17 -10
- package/docs/architecture.md +26 -20
- package/docs/cli.md +16 -13
- package/docs/contributor-change-matrix.md +3 -2
- package/docs/performance-improvement-plan-v2.md +2 -3
- package/docs/project-structure-overview.md +38 -9
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/common-flow.md +1 -1
- package/docs/task-process/final-verification.md +3 -1
- package/docs/task-process/implementation.md +1 -1
- package/docs/task-process/release-handoff.md +36 -39
- package/package.json +1 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/common.json +28 -0
- package/runtime/agents/operations/code-review.json +6 -0
- package/runtime/agents/operations/report-translation.json +6 -0
- package/runtime/agents/operations/schedule-verification.json +6 -0
- package/runtime/agents/roles/analyser.json +18 -0
- package/runtime/agents/roles/critic.json +18 -0
- package/runtime/agents/roles/designer.json +18 -0
- package/runtime/agents/roles/implementer.json +20 -0
- package/runtime/agents/roles/leader.json +20 -0
- package/runtime/agents/roles/planner.json +18 -0
- package/runtime/agents/roles/report-writer.json +19 -0
- package/runtime/agents/roles/translator.json +19 -0
- package/runtime/agents/roles/verifier.json +18 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- package/runtime/prompts/duties/acceptance-critic.json +32 -0
- package/runtime/prompts/duties/acceptance-verifier.json +32 -0
- package/runtime/prompts/duties/analysis-worker.json +32 -0
- package/runtime/prompts/duties/code-reviewer.json +32 -0
- package/runtime/prompts/duties/diagnosis-worker.json +32 -0
- package/runtime/prompts/duties/direction-selection-worker.json +32 -0
- package/runtime/prompts/duties/discovery-worker.json +32 -0
- package/runtime/prompts/duties/implementation-executor.json +32 -0
- package/runtime/prompts/duties/implementation-verifier.json +32 -0
- package/runtime/prompts/duties/lead.json +32 -0
- package/runtime/prompts/duties/planning-worker.json +36 -0
- package/runtime/prompts/duties/report-writer.json +32 -0
- package/runtime/prompts/duties/reverification-worker.json +32 -0
- package/runtime/prompts/duties/schedule-verifier.json +32 -0
- package/runtime/prompts/duties/scope-critic.json +32 -0
- package/runtime/prompts/duties/technical-verification-worker.json +32 -0
- package/runtime/prompts/duties/translator.json +32 -0
- package/runtime/prompts/launch.template.md +3 -2
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/convergence.md +4 -4
- package/runtime/prompts/lead/okstra-lead-contract.md +115 -6
- package/runtime/prompts/lead/plan-body-verification.md +6 -6
- package/runtime/prompts/lead/report-writer.md +3 -3
- package/runtime/prompts/profiles/_common-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +4 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/change-impact-analysis.json +31 -0
- package/runtime/prompts/profiles/change-impact-analysis.md +0 -20
- package/runtime/prompts/profiles/error-analysis.json +39 -0
- package/runtime/prompts/profiles/error-analysis.md +0 -25
- package/runtime/prompts/profiles/feature-analysis.json +31 -0
- package/runtime/prompts/profiles/feature-analysis.md +0 -20
- package/runtime/prompts/profiles/final-verification.json +30 -0
- package/runtime/prompts/profiles/final-verification.md +3 -22
- package/runtime/prompts/profiles/forbidden-actions.json +4 -3
- package/runtime/prompts/profiles/implementation-option-selection.json +31 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +0 -20
- package/runtime/prompts/profiles/implementation-planning.json +40 -0
- package/runtime/prompts/profiles/implementation-planning.md +6 -29
- package/runtime/prompts/profiles/implementation.json +30 -0
- package/runtime/prompts/profiles/implementation.md +1 -20
- package/runtime/prompts/profiles/improvement-discovery.json +31 -0
- package/runtime/prompts/profiles/improvement-discovery.md +0 -20
- package/runtime/prompts/profiles/project-analysis.json +31 -0
- package/runtime/prompts/profiles/project-analysis.md +0 -20
- package/runtime/prompts/profiles/release-handoff.json +5 -0
- package/runtime/prompts/profiles/release-handoff.md +71 -73
- package/runtime/prompts/profiles/requirements-discovery.json +39 -0
- package/runtime/prompts/profiles/requirements-discovery.md +0 -25
- package/runtime/prompts/profiles/technical-verification.json +39 -0
- package/runtime/prompts/profiles/technical-verification.md +0 -25
- package/runtime/prompts/wizard/prompts.ko.json +12 -17
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +3 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -3
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/worker-session.md +108 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +8 -1
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +8 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +23 -6
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +6 -2
- package/runtime/python/okstra_ctl/agent/invocation.py +168 -113
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +120 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +107 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -49
- package/runtime/python/okstra_ctl/analysis_packet.py +4 -1
- package/runtime/python/okstra_ctl/application/open_worker.py +6 -1
- package/runtime/python/okstra_ctl/assignment_resolver.py +16 -5
- package/runtime/python/okstra_ctl/cmux.py +69 -20
- package/runtime/python/okstra_ctl/code_review_target.py +16 -8
- package/runtime/python/okstra_ctl/conformance.py +43 -0
- package/runtime/python/okstra_ctl/consumers.py +6 -3
- package/runtime/python/okstra_ctl/container.py +31 -8
- package/runtime/python/okstra_ctl/context_cost.py +11 -15
- package/runtime/python/okstra_ctl/contract_refreeze.py +156 -0
- package/runtime/python/okstra_ctl/convergence_critic_prompt.py +4 -6
- package/runtime/python/okstra_ctl/convergence_provenance.py +81 -18
- package/runtime/python/okstra_ctl/design_prep.py +34 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +53 -27
- package/runtime/python/okstra_ctl/domain/host.py +5 -0
- package/runtime/python/okstra_ctl/domain/worker_runtime.py +10 -0
- package/runtime/python/okstra_ctl/error_report.py +4 -3
- package/runtime/python/okstra_ctl/execution_manifest.py +71 -18
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +21 -21
- package/runtime/python/okstra_ctl/handoff.py +167 -277
- package/runtime/python/okstra_ctl/implementation_stage.py +9 -0
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +113 -0
- package/runtime/python/okstra_ctl/lead_progress.py +1 -1
- package/runtime/python/okstra_ctl/legacy_model_selection.py +2 -2
- package/runtime/python/okstra_ctl/manager_cli.py +175 -14
- package/runtime/python/okstra_ctl/manager_launch.py +41 -19
- package/runtime/python/okstra_ctl/manager_paths.py +22 -3
- package/runtime/python/okstra_ctl/manager_split.py +474 -0
- package/runtime/python/okstra_ctl/manager_store.py +331 -21
- package/runtime/python/okstra_ctl/manager_sync.py +37 -16
- package/runtime/python/okstra_ctl/manager_view.py +217 -0
- package/runtime/python/okstra_ctl/model_discovery.py +30 -0
- package/runtime/python/okstra_ctl/model_io/lines.py +14 -1
- package/runtime/python/okstra_ctl/model_io/renderers.py +4 -3
- package/runtime/python/okstra_ctl/models.py +1 -1
- package/runtime/python/okstra_ctl/next_phase.py +16 -6
- package/runtime/python/okstra_ctl/operation_invocation.py +86 -0
- package/runtime/python/okstra_ctl/option_comparison.py +168 -0
- package/runtime/python/okstra_ctl/path_hints.py +9 -0
- package/runtime/python/okstra_ctl/paths.py +3 -0
- package/runtime/python/okstra_ctl/plan_items_cli.py +6 -1
- package/runtime/python/okstra_ctl/profile_show.py +42 -1
- package/runtime/python/okstra_ctl/qa_commands.py +15 -0
- package/runtime/python/okstra_ctl/registry/host_discovery.py +20 -12
- package/runtime/python/okstra_ctl/registry/host_registry.py +11 -0
- package/runtime/python/okstra_ctl/render.py +50 -0
- package/runtime/python/okstra_ctl/report_contract.py +1 -1
- package/runtime/python/okstra_ctl/report_finalize.py +13 -6
- package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +2 -21
- package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +21 -3
- package/runtime/python/okstra_ctl/report_html/visualizations.py +0 -5
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +177 -17
- package/runtime/python/okstra_ctl/report_translation.py +2 -1
- package/runtime/python/okstra_ctl/report_translation_dispatch.py +69 -9
- package/runtime/python/okstra_ctl/role_requirements.py +142 -129
- package/runtime/python/okstra_ctl/rollup.py +3 -1
- package/runtime/python/okstra_ctl/run.py +76 -29
- package/runtime/python/okstra_ctl/schedule_semantics.py +17 -6
- package/runtime/python/okstra_ctl/stage_fix_carry.py +23 -4
- package/runtime/python/okstra_ctl/stage_integrate.py +178 -18
- package/runtime/python/okstra_ctl/stage_map.py +16 -2
- package/runtime/python/okstra_ctl/stage_targets.py +209 -43
- package/runtime/python/okstra_ctl/team.py +22 -13
- package/runtime/python/okstra_ctl/time_report.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +3 -1
- package/runtime/python/okstra_ctl/verification_target.py +13 -2
- package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -9
- package/runtime/python/okstra_ctl/wizard/ids.py +1 -1
- package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
- package/runtime/python/okstra_ctl/wizard/state.py +3 -5
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +3 -23
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +5 -1
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +35 -7
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +66 -48
- package/runtime/python/okstra_ctl/workflow.py +1 -1
- package/runtime/python/okstra_ctl/worktree/__init__.py +3 -1
- package/runtime/python/okstra_ctl/worktree/naming.py +9 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +38 -9
- package/runtime/python/okstra_token_usage/pricing.py +6 -4
- package/runtime/schemas/agent-common-v1.schema.json +34 -0
- package/runtime/schemas/agent-duty-v1.schema.json +38 -0
- package/runtime/schemas/agent-operation-v1.schema.json +11 -0
- package/runtime/schemas/agent-profile-v1.schema.json +46 -0
- package/runtime/schemas/agent-role-v1.schema.json +29 -0
- package/runtime/schemas/final-report-v2.0.schema.json +118 -97
- package/runtime/schemas/final-report-v3.0.schema.json +118 -97
- package/runtime/skills/okstra-brief-gen/SKILL.md +84 -4
- package/runtime/skills/okstra-chat/SKILL.md +2 -2
- package/runtime/skills/okstra-code-review/SKILL.md +23 -9
- package/runtime/skills/okstra-container-build/SKILL.md +10 -10
- package/runtime/skills/okstra-inspect/SKILL.md +1 -1
- package/runtime/skills/okstra-inspect/facets/cost.md +1 -1
- package/runtime/skills/okstra-inspect/facets/error-zip.md +9 -9
- package/runtime/skills/okstra-inspect/facets/errors.md +16 -16
- package/runtime/skills/okstra-inspect/facets/logs.md +7 -7
- package/runtime/skills/okstra-inspect/facets/recap.md +2 -2
- package/runtime/skills/okstra-inspect/facets/report.md +1 -1
- package/runtime/skills/okstra-inspect/facets/status.md +4 -3
- package/runtime/skills/okstra-inspect/facets/time.md +11 -10
- package/runtime/skills/okstra-manager/SKILL.md +70 -5
- package/runtime/skills/okstra-pr-gen/SKILL.md +6 -5
- package/runtime/skills/okstra-rollup/SKILL.md +5 -5
- package/runtime/skills/okstra-run/SKILL.md +32 -13
- package/runtime/skills/okstra-schedule-gen/SKILL.md +19 -14
- package/runtime/skills/okstra-setup/SKILL.md +21 -10
- package/runtime/skills/okstra-setup/references/project-config.md +7 -6
- package/runtime/skills/okstra-usage/SKILL.md +1 -1
- package/runtime/skills/okstra-user-response/SKILL.md +1 -1
- package/runtime/templates/manager/view.template.html +109 -0
- package/runtime/templates/report-writer-prompt-preamble.md +8 -0
- package/runtime/templates/reports/brief.template.md +14 -4
- package/runtime/templates/reports/html/i18n/en.json +7 -4
- package/runtime/templates/reports/html/i18n/ko.json +7 -4
- package/runtime/templates/reports/html/tasks/final-verification.template.html +2 -2
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +8 -5
- package/runtime/templates/reports/i18n/en.json +1 -1
- package/runtime/templates/reports/md/tasks/release-handoff.template.md +1 -1
- package/runtime/templates/reports/release-handoff-input.template.md +6 -4
- package/runtime/templates/translator-prompt-preamble.md +36 -0
- package/runtime/validators/checks/validate-assets-01.py +7 -8
- package/runtime/validators/validate-brief.py +77 -2
- package/runtime/validators/validate-implementation-plan-stages.py +2 -1
- package/runtime/validators/validate-run.py +59 -9
- package/runtime/validators/validate-schedule.py +9 -0
- package/docs/for-ai/README.md +0 -68
- package/docs/for-ai/skills/okstra-brief-gen.md +0 -262
- package/docs/for-ai/skills/okstra-chat.md +0 -34
- package/docs/for-ai/skills/okstra-code-review.md +0 -57
- package/docs/for-ai/skills/okstra-container-build.md +0 -129
- package/docs/for-ai/skills/okstra-inspect.md +0 -262
- package/docs/for-ai/skills/okstra-manager.md +0 -69
- package/docs/for-ai/skills/okstra-memory.md +0 -126
- package/docs/for-ai/skills/okstra-pr-gen.md +0 -49
- package/docs/for-ai/skills/okstra-rollup.md +0 -114
- package/docs/for-ai/skills/okstra-run.md +0 -250
- package/docs/for-ai/skills/okstra-schedule-gen.md +0 -240
- package/docs/for-ai/skills/okstra-setup.md +0 -158
- package/docs/for-ai/skills/okstra-usage.md +0 -29
- package/docs/for-ai/skills/okstra-user-response.md +0 -72
- package/runtime/agents/workers/claude-worker.md +0 -128
- package/runtime/agents/workers/report-writer-worker.md +0 -37
- package/runtime/agents/workers/translator-worker.md +0 -63
- package/runtime/prompts/duties/acceptance-critic.md +0 -44
- package/runtime/prompts/duties/acceptance-verifier.md +0 -44
- package/runtime/prompts/duties/analysis-worker.md +0 -44
- package/runtime/prompts/duties/code-reviewer.md +0 -44
- package/runtime/prompts/duties/common.md +0 -39
- package/runtime/prompts/duties/diagnosis-worker.md +0 -44
- package/runtime/prompts/duties/direction-selection-worker.md +0 -44
- package/runtime/prompts/duties/discovery-worker.md +0 -44
- package/runtime/prompts/duties/implementation-executor.md +0 -44
- package/runtime/prompts/duties/implementation-verifier.md +0 -44
- package/runtime/prompts/duties/lead.md +0 -44
- package/runtime/prompts/duties/planning-worker.md +0 -52
- package/runtime/prompts/duties/report-writer.md +0 -44
- package/runtime/prompts/duties/reverification-worker.md +0 -44
- package/runtime/prompts/duties/schedule-verifier.md +0 -44
- package/runtime/prompts/duties/scope-critic.md +0 -44
- package/runtime/prompts/duties/technical-verification-worker.md +0 -44
- package/runtime/prompts/duties/translator.md +0 -44
- package/runtime/python/okstra_ctl/pane_title.py +0 -154
|
@@ -219,6 +219,49 @@ def exempt_stage_surface_conflicts(
|
|
|
219
219
|
return conflicts
|
|
220
220
|
|
|
221
221
|
|
|
222
|
+
def declared_stage_surface_gaps(
|
|
223
|
+
data: object, patterns: object = None,
|
|
224
|
+
) -> list[dict[str, object]]:
|
|
225
|
+
"""`Conformance tests:` 의 `requires` 가 계획된 경로의 표면을 다 덮지 못한 stage.
|
|
226
|
+
|
|
227
|
+
구현 게이트는 두 가지를 동시에 요구한다 — diff 가 건드린 표면을 `requires`
|
|
228
|
+
가 덮을 것(`_validate_conformance_surfaces`), 그리고 선언된 stage 의
|
|
229
|
+
`requires` 가 계획 선언과 같을 것(`_declared_conformance_errors`). 계획이
|
|
230
|
+
`requires` 를 좁게 적었는데 그 stage 가 계획한 경로가 다른 표면을 건드리면
|
|
231
|
+
구현은 둘 중 하나에 반드시 걸린다. 실측(2026-09-22, fontradar-v2-api dev-10860
|
|
232
|
+
Stage 1~3): `requires=[io]` 로 선언하고 `catalog.controller.ts`(http)를
|
|
233
|
+
계획해, 구현 run 이 "undeclared surface" 와 "requires mismatch" 를 번갈아 냈다.
|
|
234
|
+
면제 stage 와 같은 판별을 선언 stage 에도 승인 전에 한다.
|
|
235
|
+
|
|
236
|
+
반환 행: `{"stage": <int>, "surfaces": [빠진 표면..], "paths": [..],
|
|
237
|
+
"requires": [선언된 것..]}`.
|
|
238
|
+
"""
|
|
239
|
+
planning = data.get("implementationPlanning") if isinstance(data, dict) else None
|
|
240
|
+
stages = planning.get("stages") if isinstance(planning, dict) else None
|
|
241
|
+
gaps: list[dict[str, object]] = []
|
|
242
|
+
for stage in stages if isinstance(stages, list) else []:
|
|
243
|
+
if not isinstance(stage, dict):
|
|
244
|
+
continue
|
|
245
|
+
parsed = parse_conformance_tests(stage.get("conformanceTests"))
|
|
246
|
+
number = stage.get("stage")
|
|
247
|
+
if parsed is None or not isinstance(number, int) or isinstance(number, bool):
|
|
248
|
+
continue
|
|
249
|
+
_script, requires = parsed
|
|
250
|
+
touching = sorted(
|
|
251
|
+
path for path in _stage_planned_paths(stage)
|
|
252
|
+
if detect_surfaces([path], patterns) - requires
|
|
253
|
+
)
|
|
254
|
+
missing = detect_surfaces(touching, patterns) - requires
|
|
255
|
+
if missing:
|
|
256
|
+
gaps.append({
|
|
257
|
+
"stage": number,
|
|
258
|
+
"surfaces": sorted(missing),
|
|
259
|
+
"paths": touching,
|
|
260
|
+
"requires": sorted(requires),
|
|
261
|
+
})
|
|
262
|
+
return gaps
|
|
263
|
+
|
|
264
|
+
|
|
222
265
|
def _stage_planned_paths(stage: dict) -> list[str]:
|
|
223
266
|
"""stage 의 step 들이 계획한 경로 — 경로 모양인 문자열만.
|
|
224
267
|
|
|
@@ -244,15 +244,18 @@ def append_verified(plan_run_root: Path, *, impl_task_key: str, stage: int,
|
|
|
244
244
|
|
|
245
245
|
|
|
246
246
|
def append_pr(plan_run_root: Path, *, impl_task_key: str, stages: List[int],
|
|
247
|
-
branch: str, url: str) -> None:
|
|
248
|
-
"""handoff 의 PR 생성/재사용 기록. 같은 branch 의 pr 행이 이미 있으면 멱등.
|
|
247
|
+
branch: str, url: str, base: str = "") -> None:
|
|
248
|
+
"""handoff 의 PR 생성/재사용 기록. 같은 branch 의 pr 행이 이미 있으면 멱등.
|
|
249
|
+
|
|
250
|
+
`base` 는 그 PR 이 올라앉은 브랜치다. stage PR 은 스택이라 base 가 릴리스
|
|
251
|
+
브랜치가 아닐 수 있고, 어느 순서로 머지해야 하는지는 이 값에만 남는다."""
|
|
249
252
|
with consumers_mutex(plan_run_root):
|
|
250
253
|
for row in read_consumers(plan_run_root):
|
|
251
254
|
if row.get("status") == "pr" and row.get("branch") == branch:
|
|
252
255
|
return
|
|
253
256
|
_append_row(plan_run_root, {
|
|
254
257
|
"impl_task_key": impl_task_key, "stages": sorted(stages),
|
|
255
|
-
"status": "pr", "branch": branch, "url": url,
|
|
258
|
+
"status": "pr", "branch": branch, "base": base, "url": url,
|
|
256
259
|
})
|
|
257
260
|
|
|
258
261
|
|
|
@@ -213,8 +213,18 @@ def _compose_ps(project_name: str, cwd: str, include_stopped: bool = False) -> l
|
|
|
213
213
|
return rows
|
|
214
214
|
|
|
215
215
|
|
|
216
|
-
def _docker_run(argv: list[str], cwd: str | None = None) -> str:
|
|
216
|
+
def _docker_run(argv: list[str], cwd: str | None = None, *, check: bool = False) -> str:
|
|
217
|
+
"""docker 명령을 돌려 stdout 을 돌려준다.
|
|
218
|
+
|
|
219
|
+
`check=True` 면 0 이 아닌 종료를 PrepareError 로 올린다. status 가 빈 목록을,
|
|
220
|
+
down 이 성공을 보고하는데 실제로는 docker 가 실패한 경우를 막는다. config 계열
|
|
221
|
+
조회는 실패해도 진행하도록 설계돼 있어 기본값은 그대로 둔다."""
|
|
217
222
|
result = subprocess.run(argv, capture_output=True, text=True, check=False, cwd=cwd)
|
|
223
|
+
if check and result.returncode != 0:
|
|
224
|
+
detail = (result.stderr or result.stdout or "").strip().splitlines()
|
|
225
|
+
raise PrepareError(
|
|
226
|
+
f"`{' '.join(argv[:4])}` 가 exit {result.returncode} 로 실패했습니다: "
|
|
227
|
+
f"{detail[-1] if detail else '출력 없음'}")
|
|
218
228
|
return result.stdout or ""
|
|
219
229
|
|
|
220
230
|
|
|
@@ -222,7 +232,7 @@ def query_containers_by_label(project_name: str) -> list[dict]:
|
|
|
222
232
|
"""라벨 SSOT(spec §11): `okstra.project-name` 으로 컨테이너를 질의한다.
|
|
223
233
|
|
|
224
234
|
registry 가 아니라 docker 라벨이 컨테이너 존재의 기준값이다."""
|
|
225
|
-
out = _docker_run([
|
|
235
|
+
out = _docker_run(check=True, argv=[
|
|
226
236
|
"docker", "ps", "-a", "--filter",
|
|
227
237
|
f"label=okstra.project-name={project_name}",
|
|
228
238
|
"--format", (
|
|
@@ -431,7 +441,7 @@ def provision_container_group(
|
|
|
431
441
|
raise PrepareError(
|
|
432
442
|
f"container up: `up -d` 후 기동된 컨테이너가 없습니다(`docker compose -p "
|
|
433
443
|
f"{project_name} ps -a` 로 확인). compose 정의·활성 profile 을 점검한 뒤 "
|
|
434
|
-
f"`
|
|
444
|
+
f"`{_down_hint(project_root, task_key)}` 로 정리하고 다시 시도하세요.")
|
|
435
445
|
failed = poll_healthcheck(
|
|
436
446
|
project_name=project_name, cwd=str(worktree), services=started,
|
|
437
447
|
healthcheck_timeout_seconds=healthcheck_timeout_seconds,
|
|
@@ -445,8 +455,8 @@ def provision_container_group(
|
|
|
445
455
|
raise PrepareError(
|
|
446
456
|
f"container up: 다음 서비스가 시간 내 기동하지 못했습니다: {', '.join(failed)} "
|
|
447
457
|
f"(timeout={healthcheck_timeout_seconds}s). `docker compose -p "
|
|
448
|
-
f"{project_name} logs` 로 원인을 확인한 뒤
|
|
449
|
-
f"
|
|
458
|
+
f"{project_name} logs` 로 원인을 확인한 뒤 "
|
|
459
|
+
f"`{_down_hint(project_root, task_key)}` 로 정리하세요.")
|
|
450
460
|
# deploy-state 는 실제 기동된 started 만 기록한다 — full services 를 쓰면
|
|
451
461
|
# profiles 로 안 뜬 서비스가 `down --all` 스윕의 대상으로 남는다.
|
|
452
462
|
_write_deploy_state(cp, project_name=project_name, task_key=task_key,
|
|
@@ -528,12 +538,18 @@ def status_container_group(*, project_root, task_key: str) -> dict:
|
|
|
528
538
|
"containers": query_containers_by_label(project_name)}
|
|
529
539
|
|
|
530
540
|
|
|
541
|
+
def _down_hint(project_root, task_key: str) -> str:
|
|
542
|
+
"""실패 메시지가 안내하는 정리 명령. 파서가 요구하는 인자를 모두 싣는다."""
|
|
543
|
+
return f"okstra container down --project-root {project_root} --task-key {task_key}"
|
|
544
|
+
|
|
545
|
+
|
|
531
546
|
def down_container_group(*, project_root, task_key: str = "", all_groups: bool = False) -> dict:
|
|
532
|
-
"""
|
|
547
|
+
"""compose 프로젝트 이름으로 task 의 컨테이너 그룹을 down 한다."""
|
|
533
548
|
from .ids import compose_project_name
|
|
534
549
|
|
|
535
550
|
project_root = Path(project_root)
|
|
536
551
|
results = []
|
|
552
|
+
failures: list[str] = []
|
|
537
553
|
for project_id, group, task_id in _down_targets(project_root, task_key, all_groups):
|
|
538
554
|
project_name = compose_project_name(project_id, group, task_id)
|
|
539
555
|
# `docker rm -f` 는 컨테이너만 지우고 compose 가 만든 네트워크를 누수시킨다.
|
|
@@ -541,9 +557,16 @@ def down_container_group(*, project_root, task_key: str = "", all_groups: bool =
|
|
|
541
557
|
# 단위 컨테이너+네트워크를 회수한다. `-v` 는 의도적으로 빼서 compose 에
|
|
542
558
|
# 선언된 named volume(예: DB 데이터)을 보존한다 — down 한 번에 사용자
|
|
543
559
|
# 영속 데이터를 파괴하지 않는다.
|
|
544
|
-
|
|
545
|
-
|
|
560
|
+
try:
|
|
561
|
+
_docker_run(["docker", "compose", "-p", project_name, "down",
|
|
562
|
+
"--remove-orphans"], check=True)
|
|
563
|
+
except PrepareError as exc:
|
|
564
|
+
failures.append(f"{project_name}: {exc}")
|
|
565
|
+
continue
|
|
546
566
|
results.append({"projectName": project_name})
|
|
567
|
+
if failures:
|
|
568
|
+
# 나머지 대상은 끝까지 내리고, 실패한 것만 모아 알린다.
|
|
569
|
+
raise PrepareError("container down 실패 — " + "; ".join(failures))
|
|
547
570
|
return {"downed": results}
|
|
548
571
|
|
|
549
572
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
The estimator answers a narrow operational question: how much file/context
|
|
4
4
|
surface does the current task bundle ask the lead, analysis workers, and
|
|
5
|
-
report-writer to absorb? It
|
|
5
|
+
report-writer to absorb? It writes no report or run artifact; resolving the task
|
|
6
|
+
may record a finished implementation outcome (see `find_task_root`).
|
|
6
7
|
"""
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
@@ -38,11 +39,9 @@ HOT_PATH_LEAD_RESOURCES = (
|
|
|
38
39
|
"convergence",
|
|
39
40
|
"report-writer",
|
|
40
41
|
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"translator-worker.md",
|
|
45
|
-
)
|
|
42
|
+
# 모든 디스패치 프롬프트 앞머리에 실리는 계약. 종전에는 `~/.claude/agents` 의
|
|
43
|
+
# 에이전트 정의를 셌는데, 그 정의는 더 이상 설치되지 않는다(ADR-0017).
|
|
44
|
+
HOT_PATH_AGENT_CONTRACTS = ("agents/common.json",)
|
|
46
45
|
TIMESTAMPED_ARTIFACT_RE = re.compile(r"\d{4}-\d{2}-\d{2}[_T]\d{2}-\d{2}-\d{2}")
|
|
47
46
|
|
|
48
47
|
|
|
@@ -136,7 +135,7 @@ def _duty_contract_files(
|
|
|
136
135
|
if not isinstance(duty_root_value, str) or not duty_root_value:
|
|
137
136
|
return []
|
|
138
137
|
duty_root = project_root / duty_root_value
|
|
139
|
-
files = [duty_root / "common.
|
|
138
|
+
files = [duty_root / "common.json", duty_root / f"{audience}.json"]
|
|
140
139
|
return [path for path in files if path.is_file()]
|
|
141
140
|
|
|
142
141
|
|
|
@@ -239,19 +238,15 @@ def _skill_assets_metric() -> dict:
|
|
|
239
238
|
instruction footprint and are the prompt-diet ranking input."""
|
|
240
239
|
entries = []
|
|
241
240
|
okstra_home = _okstra_asset_home()
|
|
242
|
-
claude_home = Path.home() / ".claude"
|
|
243
241
|
for name in HOT_PATH_LEAD_RESOURCES:
|
|
244
242
|
path = _installed_or_dev(
|
|
245
243
|
okstra_home / "prompts" / "lead" / f"{name}.md",
|
|
246
244
|
f"prompts/lead/{name}.md",
|
|
247
245
|
)
|
|
248
246
|
entries.append((f"resource:{name}", path))
|
|
249
|
-
for
|
|
250
|
-
path = _installed_or_dev(
|
|
251
|
-
|
|
252
|
-
f"runtime/agents/workers/{fname}",
|
|
253
|
-
)
|
|
254
|
-
entries.append((f"agent:{fname}", path))
|
|
247
|
+
for relative in HOT_PATH_AGENT_CONTRACTS:
|
|
248
|
+
path = _installed_or_dev(okstra_home / relative, relative)
|
|
249
|
+
entries.append((f"contract:{relative}", path))
|
|
255
250
|
|
|
256
251
|
files = []
|
|
257
252
|
for label, path in entries:
|
|
@@ -512,7 +507,8 @@ _CLI_EPILOG = r"""Usage:
|
|
|
512
507
|
|
|
513
508
|
Output: JSON with task file counts, current-run counts, lead Phase 1 read
|
|
514
509
|
surface, analysis-worker initial read surface, and report-writer synthesis
|
|
515
|
-
surface.
|
|
510
|
+
surface. Resolving a task may record a finished implementation outcome in its task
|
|
511
|
+
manifest, the task catalog, and consumers.jsonl; nothing else is written.
|
|
516
512
|
"""
|
|
517
513
|
|
|
518
514
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""진행 중인 run 의 동결 계약을 현재 형식으로 다시 동결한다.
|
|
2
|
+
|
|
3
|
+
run 은 시작할 때 직무·역할·공통 계약을 자기 폴더로 동결하고, 그 다이제스트를
|
|
4
|
+
매니페스트에 적는다. 계약 형식이 바뀐 릴리스를 설치하면 그 run 의 다이제스트는
|
|
5
|
+
새 규칙으로 계산한 값과 영영 다르고, 프롬프트를 하나도 더 만들 수 없다 — 실측
|
|
6
|
+
2026-09-23(fontradar-v2-api dev-10860 implementation-planning-002): 같은 run 에서
|
|
7
|
+
8회 성공한 뒤 설치가 한 번 일어나자 그다음 materialize 가
|
|
8
|
+
`run duty snapshot catalog digest does not match` 로 멈췄다.
|
|
9
|
+
|
|
10
|
+
이 모듈은 그 run 을 현재 형식으로 다시 동결하고 매니페스트의 다이제스트와 형식
|
|
11
|
+
표식을 갱신한다. 계약 본문은 런타임이 소유하므로 새로 쓰는 것이 원래 값이고,
|
|
12
|
+
run 의 증거(프롬프트·결과·원장)는 건드리지 않는다. 사용자가 명시적으로 부를 때만
|
|
13
|
+
도는 복구 경로다.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import tempfile
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Mapping
|
|
23
|
+
|
|
24
|
+
from .agent.invocation import digest_duty_catalog
|
|
25
|
+
from .json_boundary import (
|
|
26
|
+
JsonBoundaryError,
|
|
27
|
+
load_owned_object,
|
|
28
|
+
write_owned_object_atomic,
|
|
29
|
+
)
|
|
30
|
+
from .run import (
|
|
31
|
+
CONTRACT_FORMAT_VERSION,
|
|
32
|
+
PrepareError,
|
|
33
|
+
_common_contract_source,
|
|
34
|
+
_duty_catalog_source,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ContractRefreezeError(RuntimeError):
|
|
39
|
+
"""다시 동결할 수 없는 run. 메시지가 그 자리를 이름한다."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class RefreezeResult:
|
|
44
|
+
duty_root: str
|
|
45
|
+
previous_digest: str
|
|
46
|
+
digest: str
|
|
47
|
+
previous_format_version: int
|
|
48
|
+
format_version: int
|
|
49
|
+
changed: bool
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def frozen_format_version(contract: Mapping[str, Any]) -> int:
|
|
53
|
+
"""이 run 이 동결한 계약 형식. 표식이 없는 run 은 1(Markdown 직무)이다."""
|
|
54
|
+
value = contract.get("contractFormatVersion")
|
|
55
|
+
return value if isinstance(value, int) and not isinstance(value, bool) else 1
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def refreeze_message(duty_root: str) -> str:
|
|
59
|
+
"""형식이 어긋난 run 이 받아야 할 문장 — 원인과 다음 명령."""
|
|
60
|
+
return (
|
|
61
|
+
"run froze an older duty contract format and this install cannot read "
|
|
62
|
+
f"it: {duty_root}. Re-freeze this run's contracts with "
|
|
63
|
+
"`okstra agent-prompt refreeze-contracts --project-root <dir> "
|
|
64
|
+
"--run-manifest <path>`, then repeat the command that failed."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def refreeze_run_contracts(
|
|
69
|
+
*, project_root: Path, run_manifest_path: Path, workspace_root: Path,
|
|
70
|
+
) -> RefreezeResult:
|
|
71
|
+
"""이 run 의 계약 폴더를 현재 런타임 카탈로그로 갈아 끼운다."""
|
|
72
|
+
root = project_root.resolve()
|
|
73
|
+
manifest_path = _resolve(root, run_manifest_path)
|
|
74
|
+
manifest = _load(manifest_path, "run manifest")
|
|
75
|
+
contract = manifest.get("agentContract")
|
|
76
|
+
if not isinstance(contract, dict):
|
|
77
|
+
raise ContractRefreezeError(f"run manifest has no agentContract: {manifest_path}")
|
|
78
|
+
duty_root_rel = str(contract.get("dutyRootPath") or "")
|
|
79
|
+
if not duty_root_rel:
|
|
80
|
+
raise ContractRefreezeError(f"agentContract has no dutyRootPath: {manifest_path}")
|
|
81
|
+
duty_root = _resolve(root, Path(duty_root_rel))
|
|
82
|
+
previous_digest = str(contract.get("catalogDigest") or "")
|
|
83
|
+
previous_format = frozen_format_version(contract)
|
|
84
|
+
try:
|
|
85
|
+
source = _duty_catalog_source(workspace_root)
|
|
86
|
+
common_source = _common_contract_source(workspace_root)
|
|
87
|
+
except PrepareError as exc:
|
|
88
|
+
raise ContractRefreezeError(str(exc)) from exc
|
|
89
|
+
digest = _replace_snapshot(duty_root, source, common_source)
|
|
90
|
+
changed = (
|
|
91
|
+
digest != previous_digest or previous_format != CONTRACT_FORMAT_VERSION
|
|
92
|
+
)
|
|
93
|
+
if changed:
|
|
94
|
+
contract["catalogDigest"] = digest
|
|
95
|
+
contract["contractFormatVersion"] = CONTRACT_FORMAT_VERSION
|
|
96
|
+
_write_manifest(manifest_path, manifest)
|
|
97
|
+
return RefreezeResult(
|
|
98
|
+
duty_root=str(duty_root),
|
|
99
|
+
previous_digest=previous_digest,
|
|
100
|
+
digest=digest,
|
|
101
|
+
previous_format_version=previous_format,
|
|
102
|
+
format_version=CONTRACT_FORMAT_VERSION,
|
|
103
|
+
changed=changed,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _replace_snapshot(duty_root: Path, source: Path, common_source: Path) -> str:
|
|
108
|
+
"""계약 폴더를 통째로 새로 쓴다. 옛 형식 파일은 남기지 않는다.
|
|
109
|
+
|
|
110
|
+
옛 파일을 남기면 두 형식이 한 폴더에 섞이고, 다이제스트가 세는 대상과 로더가
|
|
111
|
+
읽는 대상이 다시 갈린다 — 이 결함이 처음 생긴 자리가 그 갈림이다.
|
|
112
|
+
"""
|
|
113
|
+
duty_root.parent.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
staged = Path(tempfile.mkdtemp(
|
|
115
|
+
dir=duty_root.parent, prefix=f".{duty_root.name}.", suffix=".refreeze",
|
|
116
|
+
))
|
|
117
|
+
retired = duty_root.with_name(f".{duty_root.name}.retired")
|
|
118
|
+
try:
|
|
119
|
+
shutil.copytree(source, staged, dirs_exist_ok=True)
|
|
120
|
+
shutil.copyfile(common_source, staged / "common.json")
|
|
121
|
+
shutil.copytree(
|
|
122
|
+
common_source.parent / "roles", staged / "roles", dirs_exist_ok=True
|
|
123
|
+
)
|
|
124
|
+
digest = digest_duty_catalog(staged)
|
|
125
|
+
if duty_root.exists():
|
|
126
|
+
shutil.rmtree(retired, ignore_errors=True)
|
|
127
|
+
os.rename(duty_root, retired)
|
|
128
|
+
os.rename(staged, duty_root)
|
|
129
|
+
except OSError as exc:
|
|
130
|
+
raise ContractRefreezeError(f"cannot re-freeze duty contracts: {exc}") from exc
|
|
131
|
+
finally:
|
|
132
|
+
shutil.rmtree(staged, ignore_errors=True)
|
|
133
|
+
shutil.rmtree(retired, ignore_errors=True)
|
|
134
|
+
return digest
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _resolve(project_root: Path, value: Path) -> Path:
|
|
138
|
+
path = Path(value).expanduser()
|
|
139
|
+
return path if path.is_absolute() else project_root / path
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _load(path: Path, label: str) -> dict[str, Any]:
|
|
143
|
+
try:
|
|
144
|
+
payload = load_owned_object(path, artifact=label)
|
|
145
|
+
except (OSError, JsonBoundaryError) as exc:
|
|
146
|
+
raise ContractRefreezeError(f"cannot read {label} {path}: {exc}") from exc
|
|
147
|
+
if not isinstance(payload, dict):
|
|
148
|
+
raise ContractRefreezeError(f"{label} is not an object: {path}")
|
|
149
|
+
return payload
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _write_manifest(path: Path, manifest: Mapping[str, Any]) -> None:
|
|
153
|
+
try:
|
|
154
|
+
write_owned_object_atomic(path, manifest, artifact="run manifest")
|
|
155
|
+
except JsonBoundaryError as exc:
|
|
156
|
+
raise ContractRefreezeError(f"cannot write run manifest {path}: {exc}") from exc
|
|
@@ -28,7 +28,7 @@ from dataclasses import dataclass
|
|
|
28
28
|
from pathlib import Path
|
|
29
29
|
from typing import Any, Mapping, Sequence
|
|
30
30
|
|
|
31
|
-
from .convergence_provenance import worker_result_suffix
|
|
31
|
+
from .convergence_provenance import worker_result_paths, worker_result_suffix
|
|
32
32
|
from .fixed_text import line
|
|
33
33
|
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
34
34
|
from .paths import RunRef
|
|
@@ -132,9 +132,7 @@ def analyser_results(
|
|
|
132
132
|
) -> list[AnalyserResult]:
|
|
133
133
|
"""분석 audience 워커마다 결과 파일 경로와 finding id 를 짝지어 돌려준다.
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
`<worker>-worker-<task-type>-<workerResults seq>.md`. state 시퀀스와 다른
|
|
137
|
-
카운터라 그룹 파일명에서 접미사를 빌려 오면 빗나간다.
|
|
135
|
+
출처 검사와 같은 실행 원장을 사용해야 교정 전 결과를 재검증하지 않는다.
|
|
138
136
|
"""
|
|
139
137
|
suffix = worker_result_suffix(Path(run_dir), groups)
|
|
140
138
|
if suffix is None:
|
|
@@ -142,13 +140,13 @@ def analyser_results(
|
|
|
142
140
|
"cannot resolve the worker-result suffix from the grouping's "
|
|
143
141
|
"runManifestPath"
|
|
144
142
|
)
|
|
145
|
-
|
|
143
|
+
paths = worker_result_paths(Path(run_dir), groups, suffix)
|
|
146
144
|
by_worker = _findings_by_worker(groups)
|
|
147
145
|
return [
|
|
148
146
|
AnalyserResult(
|
|
149
147
|
worker_id=worker,
|
|
150
148
|
result_path=_project_relative(
|
|
151
|
-
project_root,
|
|
149
|
+
project_root, paths[worker]
|
|
152
150
|
),
|
|
153
151
|
finding_ids=tuple(by_worker.get(worker, ())),
|
|
154
152
|
)
|
|
@@ -21,6 +21,8 @@ from pathlib import Path
|
|
|
21
21
|
from typing import Mapping
|
|
22
22
|
|
|
23
23
|
from okstra_ctl.convergence_engine import grouped_input_digest
|
|
24
|
+
from okstra_ctl.execution_identity import Attempt, ExecutionManifest
|
|
25
|
+
from okstra_ctl.execution_manifest import read_execution_manifest_view
|
|
24
26
|
from okstra_ctl.json_boundary import (
|
|
25
27
|
load_owned_object,
|
|
26
28
|
)
|
|
@@ -97,6 +99,72 @@ def groups_digest_ok(state_dir: Path, suffix: str, document: dict) -> bool:
|
|
|
97
99
|
return grouped_input_digest(document) == recorded
|
|
98
100
|
|
|
99
101
|
|
|
102
|
+
def discovery_attempts(manifest: ExecutionManifest) -> dict[str, list[Attempt]]:
|
|
103
|
+
"""교정 실행은 발견 결과를 대체하지만 재검증 투표는 발견 출처가 아니다."""
|
|
104
|
+
latest: dict[str, Attempt] = {}
|
|
105
|
+
for attempt in manifest.attempts:
|
|
106
|
+
current = latest.get(attempt.invocation_ref)
|
|
107
|
+
if current is None or attempt.attempt > current.attempt:
|
|
108
|
+
latest[attempt.invocation_ref] = attempt
|
|
109
|
+
by_role: dict[str, list[Attempt]] = {}
|
|
110
|
+
for invocation in manifest.invocations:
|
|
111
|
+
if invocation.dispatch_kind not in {"initial", "correction"}:
|
|
112
|
+
continue
|
|
113
|
+
attempt = latest.get(invocation.invocation_ref)
|
|
114
|
+
if attempt is not None:
|
|
115
|
+
by_role.setdefault(invocation.role_execution_ref, []).append(attempt)
|
|
116
|
+
return by_role
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def worker_result_paths(
|
|
120
|
+
run_dir: Path, document: Mapping, suffix: str
|
|
121
|
+
) -> dict[str, Path]:
|
|
122
|
+
"""실행 원장의 채택된 발견 결과를 고른다. 구형 원장은 파일명 규칙을 쓴다."""
|
|
123
|
+
results_dir = run_dir / "worker-results"
|
|
124
|
+
workers = document.get("workers") or []
|
|
125
|
+
paths = {
|
|
126
|
+
row["workerId"]: results_dir / f"{row['workerId']}-worker-{suffix}.md"
|
|
127
|
+
for row in workers if isinstance(row, Mapping) and row.get("workerId")
|
|
128
|
+
}
|
|
129
|
+
if document.get("schemaVersion") != "2.0":
|
|
130
|
+
return paths
|
|
131
|
+
recorded = document.get("runManifestPath")
|
|
132
|
+
if not _nonempty_string(recorded):
|
|
133
|
+
return paths
|
|
134
|
+
manifest_path = run_dir / "manifests" / Path(recorded).name
|
|
135
|
+
manifest, authority = read_execution_manifest_view(manifest_path)
|
|
136
|
+
if manifest.legacy:
|
|
137
|
+
return paths
|
|
138
|
+
attempts = discovery_attempts(manifest)
|
|
139
|
+
for row in workers:
|
|
140
|
+
if not isinstance(row, Mapping) or not row.get("workerId"):
|
|
141
|
+
continue
|
|
142
|
+
accepted = [
|
|
143
|
+
attempt for attempt in attempts.get(row.get("sourceRoleExecutionRef"), [])
|
|
144
|
+
if attempt.status == "ok" and attempt.result_path
|
|
145
|
+
]
|
|
146
|
+
if not accepted:
|
|
147
|
+
continue
|
|
148
|
+
candidate = Path(accepted[-1].result_path)
|
|
149
|
+
if not candidate.is_absolute():
|
|
150
|
+
project_root = authority.get("projectRoot")
|
|
151
|
+
if not _nonempty_string(project_root):
|
|
152
|
+
raise ValueError("relative accepted resultPath requires projectRoot")
|
|
153
|
+
candidate = Path(project_root) / candidate
|
|
154
|
+
if candidate.resolve().parent != results_dir.resolve():
|
|
155
|
+
raise ValueError(f"accepted resultPath is outside worker-results: {candidate}")
|
|
156
|
+
if not candidate.is_file():
|
|
157
|
+
# 이 모듈의 계약은 "없는 워커 결과는 조용히 건너뛴다" 이고, 그 판정은
|
|
158
|
+
# 아래 `read_canonical_worker_result` 가 None 을 돌려주는 것으로
|
|
159
|
+
# 이루어진다. 여기서 던지면 판정이 아니라 run 이 죽는다 — 실측
|
|
160
|
+
# 2026-09-23(jobs dev-10860 stage 1): 옛 run 의 원장이 ok 로 적은
|
|
161
|
+
# 결과 파일이 디스크에 없어 `report-finalize --only validate-run` 이
|
|
162
|
+
# 예외로 멈췄다. 파일명 규칙 경로를 그대로 두고 넘어간다.
|
|
163
|
+
continue
|
|
164
|
+
paths[row["workerId"]] = candidate
|
|
165
|
+
return paths
|
|
166
|
+
|
|
167
|
+
|
|
100
168
|
def read_canonical_worker_result(
|
|
101
169
|
worker_results_dir: Path, worker: str, suffix: str
|
|
102
170
|
) -> str | None:
|
|
@@ -140,20 +208,25 @@ def provenance_errors(
|
|
|
140
208
|
claims = group_claims(document)
|
|
141
209
|
if claims is None:
|
|
142
210
|
return []
|
|
211
|
+
paths = worker_result_paths(worker_results_dir.parent, document, suffix)
|
|
143
212
|
contents: dict[str, str | None] = {}
|
|
144
213
|
errors: list[str] = []
|
|
145
214
|
for finding_id, worker, item_id in claims:
|
|
146
215
|
if worker not in contents:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
216
|
+
candidate = paths.get(worker)
|
|
217
|
+
if candidate is not None and candidate.name != f"{worker}-worker-{suffix}.md":
|
|
218
|
+
contents[worker] = candidate.read_text(encoding="utf-8")
|
|
219
|
+
else:
|
|
220
|
+
contents[worker] = read_canonical_worker_result(
|
|
221
|
+
worker_results_dir, worker, suffix
|
|
222
|
+
)
|
|
150
223
|
text = contents[worker]
|
|
151
224
|
if text is None or id_occurs_wordbounded(text, item_id):
|
|
152
225
|
continue
|
|
153
226
|
errors.append(
|
|
154
227
|
f"convergence groups `{groups_label}` group {finding_id} "
|
|
155
228
|
f"cites source item `{worker}:{item_id}`, but that ID does not "
|
|
156
|
-
f"occur in the worker's result file `{worker}-worker-{suffix}.md` — a "
|
|
229
|
+
f"occur in the worker's result file `{paths.get(worker, Path(f'{worker}-worker-{suffix}.md')).name}` — a "
|
|
157
230
|
"grouping may not invent a provenance link to an item the "
|
|
158
231
|
"worker never reported."
|
|
159
232
|
)
|
|
@@ -243,10 +316,10 @@ DISCARDED_ATTEMPT_STATUSES = frozenset(
|
|
|
243
316
|
|
|
244
317
|
|
|
245
318
|
def discarded_workers(document: object, execution_manifest: object) -> dict[str, str]:
|
|
246
|
-
"""``{workerId: "<invocationRef>#<attempt> <status>"}`` —
|
|
319
|
+
"""``{workerId: "<invocationRef>#<attempt> <status>"}`` — 발견 attempt 가 전부
|
|
247
320
|
폐기돼 채택할 결과가 없는 분석 워커.
|
|
248
321
|
|
|
249
|
-
한 워커의
|
|
322
|
+
한 워커의 발견 invocation 이 둘 이상이면(교정 디스패치는 새 invocation 이다)
|
|
250
323
|
그중 하나라도 마지막 attempt 가 ``ok`` 면 채택 가능한 결과가 있는 것이므로
|
|
251
324
|
폐기로 보지 않는다. legacy 매니페스트나 v1 roster 는 판정하지 않는다.
|
|
252
325
|
"""
|
|
@@ -254,17 +327,7 @@ def discarded_workers(document: object, execution_manifest: object) -> dict[str,
|
|
|
254
327
|
return {}
|
|
255
328
|
if execution_manifest is None or getattr(execution_manifest, "legacy", False):
|
|
256
329
|
return {}
|
|
257
|
-
|
|
258
|
-
for invocation in getattr(execution_manifest, "invocations", ()):
|
|
259
|
-
if invocation.dispatch_kind == "initial":
|
|
260
|
-
initial_refs.setdefault(invocation.role_execution_ref, set()).add(
|
|
261
|
-
invocation.invocation_ref
|
|
262
|
-
)
|
|
263
|
-
latest: dict[str, object] = {}
|
|
264
|
-
for attempt in getattr(execution_manifest, "attempts", ()):
|
|
265
|
-
current = latest.get(attempt.invocation_ref)
|
|
266
|
-
if current is None or attempt.attempt > current.attempt:
|
|
267
|
-
latest[attempt.invocation_ref] = attempt
|
|
330
|
+
attempts = discovery_attempts(execution_manifest)
|
|
268
331
|
result: dict[str, str] = {}
|
|
269
332
|
for row in document["workers"]:
|
|
270
333
|
if not isinstance(row, dict):
|
|
@@ -273,7 +336,7 @@ def discarded_workers(document: object, execution_manifest: object) -> dict[str,
|
|
|
273
336
|
role_ref = row.get("sourceRoleExecutionRef")
|
|
274
337
|
if not _nonempty_string(worker) or not _nonempty_string(role_ref):
|
|
275
338
|
continue
|
|
276
|
-
rows =
|
|
339
|
+
rows = attempts.get(role_ref, [])
|
|
277
340
|
if not rows or any(item.status == "ok" for item in rows):
|
|
278
341
|
continue
|
|
279
342
|
discarded = [item for item in rows if item.status in DISCARDED_ATTEMPT_STATUSES]
|
|
@@ -20,6 +20,7 @@ from fnmatch import fnmatchcase
|
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
from typing import Any, Iterator
|
|
22
22
|
|
|
23
|
+
from .consumers import read_stage_consumer_state
|
|
23
24
|
from .final_report_schema import load_schema, validate as validate_schema
|
|
24
25
|
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
25
26
|
from okstra_project import ResolverError, resolve_project_root
|
|
@@ -466,6 +467,7 @@ def materialize_design_prep_requests(plan_data_path: Path) -> list[Path]:
|
|
|
466
467
|
# 충돌은 한 항목씩 알리지 않는다. 요청 12개가 모두 옛 리포트의 파일과 부딪히면
|
|
467
468
|
# 첫 건에서 멈추는 구현은 사용자에게 12회 왕복을 시키고, 그때마다 이미 쓴
|
|
468
469
|
# 앞쪽 파일이 남아 상태가 절반만 진행된다. 전부 렌더해 전부 검사한 뒤 쓴다.
|
|
470
|
+
done = _done_stages(data_path)
|
|
469
471
|
rendered = [
|
|
470
472
|
_render_request(
|
|
471
473
|
data_path=data_path,
|
|
@@ -474,7 +476,7 @@ def materialize_design_prep_requests(plan_data_path: Path) -> list[Path]:
|
|
|
474
476
|
item=item,
|
|
475
477
|
)
|
|
476
478
|
for item in items
|
|
477
|
-
if item.get("status") in _REQUEST_STATUSES
|
|
479
|
+
if item.get("status") in _REQUEST_STATUSES and not _is_implemented(item, done)
|
|
478
480
|
]
|
|
479
481
|
conflicts = [
|
|
480
482
|
reason
|
|
@@ -494,6 +496,37 @@ def materialize_design_prep_requests(plan_data_path: Path) -> list[Path]:
|
|
|
494
496
|
return written
|
|
495
497
|
|
|
496
498
|
|
|
499
|
+
def _done_stages(data_path: Path) -> frozenset[int]:
|
|
500
|
+
"""이 태스크에서 이미 구현이 끝난 stage.
|
|
501
|
+
|
|
502
|
+
원장을 못 읽으면 빈 집합이다 — 그때의 동작은 이 판정이 없던 때와 같고,
|
|
503
|
+
"모르니까 건너뛴다" 는 반대 방향의 실패보다 안전하다.
|
|
504
|
+
"""
|
|
505
|
+
try:
|
|
506
|
+
state = read_stage_consumer_state(data_path.parent.parent)
|
|
507
|
+
except (OSError, UnicodeError, ValueError):
|
|
508
|
+
return frozenset()
|
|
509
|
+
return frozenset(state.done_stages)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _is_implemented(item: Mapping[str, Any], done: frozenset[int]) -> bool:
|
|
513
|
+
"""이 항목이 가리키는 stage 가 전부 구현 완료인가.
|
|
514
|
+
|
|
515
|
+
구현이 끝난 stage 의 계획은 역사이지 계획이 아니다. 그런데 탐지기는 매
|
|
516
|
+
계획 run 마다 이월된 stage 본문을 다시 훑고, 그 사이 선택 방향의
|
|
517
|
+
`fileStructure` 가 바뀌면 같은 PREP 항목이 다른 증거로 렌더된다. 요청
|
|
518
|
+
파일은 태스크 단위 이름이라 불변 파일과 충돌하고, report-finalize 는
|
|
519
|
+
빠져나갈 길 없이 멈춘다(2026-09-24, dev-10860: 9건). 이미 지어진 것에
|
|
520
|
+
설계 준비를 다시 요구하는 것이므로, 답도 쓸모가 없다.
|
|
521
|
+
"""
|
|
522
|
+
stages = [
|
|
523
|
+
stage
|
|
524
|
+
for stage in item.get("stageRefs") or ()
|
|
525
|
+
if isinstance(stage, int) and not isinstance(stage, bool)
|
|
526
|
+
]
|
|
527
|
+
return bool(stages) and all(stage in done for stage in stages)
|
|
528
|
+
|
|
529
|
+
|
|
497
530
|
def load_design_prep_items(report_path: Path) -> tuple[dict[str, Any], ...]:
|
|
498
531
|
"""Return validated snapshot items without exposing mutable report data."""
|
|
499
532
|
data_path = _canonical_data_path(report_path)
|