okstra 0.202.0 → 0.205.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 +7 -6
- 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/memory/memory.mjs +41 -8
- package/dist/commands/memory/memory.mjs.map +1 -1
- 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 +14 -11
- package/docs/architecture.md +26 -20
- package/docs/cli.md +15 -12
- package/docs/contributor-change-matrix.md +3 -2
- package/docs/performance-improvement-plan-v2.md +3 -9
- package/docs/project-structure-overview.md +39 -11
- 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-option-selection.md +1 -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 +2 -1
- 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-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +4 -1
- package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- 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 +4 -23
- 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 +4 -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 +74 -74
- 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 +14 -18
- 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 +23 -8
- 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_engine.py +38 -0
- package/runtime/python/okstra_ctl/convergence_provenance.py +7 -1
- 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/handoff.py +384 -286
- package/runtime/python/okstra_ctl/handoff_verification.py +25 -6
- 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 +92 -4
- package/runtime/python/okstra_ctl/manager_launch.py +1 -1
- package/runtime/python/okstra_ctl/manager_paths.py +14 -3
- package/runtime/python/okstra_ctl/manager_store.py +210 -3
- package/runtime/python/okstra_ctl/manager_sync.py +4 -1
- package/runtime/python/okstra_ctl/manager_view.py +2 -1
- 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/profile_show.py +42 -1
- 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 +79 -0
- package/runtime/python/okstra_ctl/report_contract.py +1 -1
- package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +21 -3
- 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/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 +12 -21
- 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 +18 -2
- 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 +31 -12
- package/runtime/skills/okstra-schedule-gen/SKILL.md +19 -14
- package/runtime/skills/okstra-setup/SKILL.md +12 -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 +1 -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 +5 -4
- package/runtime/templates/reports/html/i18n/ko.json +5 -4
- 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 +70 -0
- package/runtime/validators/validate-implementation-plan-stages.py +2 -1
- package/runtime/validators/validate-run.py +72 -15
- 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 -86
- 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 -167
- 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
|
|
|
@@ -218,9 +218,15 @@ def _append_row(plan_run_root: Path, record: Dict[str, Any]) -> None:
|
|
|
218
218
|
|
|
219
219
|
def append_verified(plan_run_root: Path, *, impl_task_key: str, stage: int,
|
|
220
220
|
verdict: str, report_path: str, head_commit: str = "",
|
|
221
|
-
final_verdict: dict | None = None
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
final_verdict: dict | None = None,
|
|
222
|
+
verification_scope: str = "",
|
|
223
|
+
verified_head_commit: str = "") -> None:
|
|
224
|
+
"""final-verification 결과를 stage 별로 기록. 같은 report_path 재기록은 멱등,
|
|
225
|
+
다른 report_path 는 재검증으로 append 한다 (읽기는 last-wins).
|
|
226
|
+
|
|
227
|
+
`head_commit` 은 언제나 이 stage 가 완료로 기록한 커밋이다. 전체 task 검증이
|
|
228
|
+
근거인 행은 검증된 head 가 그 커밋이 아니므로, 그 head 를
|
|
229
|
+
`verified_head_commit` 으로 함께 남긴다 — 자격 판정이 둘을 각각 대조한다."""
|
|
224
230
|
with consumers_mutex(plan_run_root):
|
|
225
231
|
rows = read_consumers(plan_run_root)
|
|
226
232
|
done = latest_done_by_stage(rows).get(stage, {})
|
|
@@ -234,25 +240,34 @@ def append_verified(plan_run_root: Path, *, impl_task_key: str, stage: int,
|
|
|
234
240
|
and row.get("status") == "verified"
|
|
235
241
|
and row.get("report_path") == report_path):
|
|
236
242
|
return
|
|
237
|
-
|
|
243
|
+
row: Dict[str, Any] = {
|
|
238
244
|
"impl_task_key": impl_task_key, "stage": stage,
|
|
239
245
|
"status": "verified", "verdict": verdict,
|
|
240
246
|
"report_path": report_path,
|
|
241
247
|
"head_commit": head_commit,
|
|
242
248
|
"final_verdict": final_verdict or {},
|
|
243
|
-
}
|
|
249
|
+
}
|
|
250
|
+
# 단독-stage 행에는 싣지 않는다 — 그 행의 검증된 head 는 `head_commit`
|
|
251
|
+
# 자신이고, 빈 필드를 더하면 옛 행과 새 행의 모양이 갈라진다.
|
|
252
|
+
if verification_scope:
|
|
253
|
+
row["verification_scope"] = verification_scope
|
|
254
|
+
row["verified_head_commit"] = verified_head_commit
|
|
255
|
+
_append_row(plan_run_root, row)
|
|
244
256
|
|
|
245
257
|
|
|
246
258
|
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 행이 이미 있으면 멱등.
|
|
259
|
+
branch: str, url: str, base: str = "") -> None:
|
|
260
|
+
"""handoff 의 PR 생성/재사용 기록. 같은 branch 의 pr 행이 이미 있으면 멱등.
|
|
261
|
+
|
|
262
|
+
`base` 는 그 PR 이 올라앉은 브랜치다. stage PR 은 스택이라 base 가 릴리스
|
|
263
|
+
브랜치가 아닐 수 있고, 어느 순서로 머지해야 하는지는 이 값에만 남는다."""
|
|
249
264
|
with consumers_mutex(plan_run_root):
|
|
250
265
|
for row in read_consumers(plan_run_root):
|
|
251
266
|
if row.get("status") == "pr" and row.get("branch") == branch:
|
|
252
267
|
return
|
|
253
268
|
_append_row(plan_run_root, {
|
|
254
269
|
"impl_task_key": impl_task_key, "stages": sorted(stages),
|
|
255
|
-
"status": "pr", "branch": branch, "url": url,
|
|
270
|
+
"status": "pr", "branch": branch, "base": base, "url": url,
|
|
256
271
|
})
|
|
257
272
|
|
|
258
273
|
|
|
@@ -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
|
|
@@ -849,6 +849,44 @@ def validate_working_state(state: Mapping[str, Any]) -> list[str]:
|
|
|
849
849
|
return errors
|
|
850
850
|
|
|
851
851
|
|
|
852
|
+
def lead_only_final_state(task_key: str) -> dict[str, Any]:
|
|
853
|
+
"""워커를 띄우지 않는 phase 의 수렴 최종 상태.
|
|
854
|
+
|
|
855
|
+
리포트 계약 3.0 은 모든 리포트에 수렴 입력을 요구한다(ADR-0020: 사실마다
|
|
856
|
+
소유자가 하나). release-handoff 처럼 프로필이 "convergence 를 돌지 않는다"
|
|
857
|
+
고 선언한 phase 에는 그 파일을 만드는 주체가 없었고, report-finalize 가
|
|
858
|
+
`required input is missing` 으로 멈췄다. 리드는 매 run 마다 seed →
|
|
859
|
+
plan-round → finalize 를 손으로 돌려 빈 상태를 만들어 왔다(실측: dev-10860
|
|
860
|
+
release-handoff 001·002 둘 다).
|
|
861
|
+
|
|
862
|
+
"수렴이 돌지 않았다" 는 부재가 아니라 사실이므로 파일로 남긴다 — 분석자가
|
|
863
|
+
둘 미만이어서 자동 비활성이라는 뜻이고, 그게 이 어휘의 `auto-disabled` 다.
|
|
864
|
+
"""
|
|
865
|
+
state = {
|
|
866
|
+
"schemaVersion": FINAL_SCHEMA_VERSION,
|
|
867
|
+
"taskKey": task_key,
|
|
868
|
+
"config": {
|
|
869
|
+
"enabled": False,
|
|
870
|
+
"adversarial": False,
|
|
871
|
+
"maxRounds": 1,
|
|
872
|
+
"effectiveMaxRounds": 1,
|
|
873
|
+
"verificationMode": "lightweight",
|
|
874
|
+
"autoDisabled": "fewer-than-two-analysers",
|
|
875
|
+
},
|
|
876
|
+
"findings": [],
|
|
877
|
+
"roundHistory": [],
|
|
878
|
+
"round2SkippedReason": "auto-disabled",
|
|
879
|
+
"finalState": "converged",
|
|
880
|
+
"totalRounds": 0,
|
|
881
|
+
"finalClassificationCounts": classification_counts_from_findings([]),
|
|
882
|
+
}
|
|
883
|
+
errors = validate_final_state(state)
|
|
884
|
+
if errors:
|
|
885
|
+
raise ConvergenceContractError(
|
|
886
|
+
"lead-only convergence state is invalid: " + "; ".join(errors))
|
|
887
|
+
return state
|
|
888
|
+
|
|
889
|
+
|
|
852
890
|
def validate_final_state(state: Mapping[str, Any]) -> list[str]:
|
|
853
891
|
"""Validate historical v1.0-v1.2 and strict current v1.3 artifacts."""
|
|
854
892
|
errors: list[str] = []
|
|
@@ -154,7 +154,13 @@ def worker_result_paths(
|
|
|
154
154
|
if candidate.resolve().parent != results_dir.resolve():
|
|
155
155
|
raise ValueError(f"accepted resultPath is outside worker-results: {candidate}")
|
|
156
156
|
if not candidate.is_file():
|
|
157
|
-
|
|
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
|
|
158
164
|
paths[row["workerId"]] = candidate
|
|
159
165
|
return paths
|
|
160
166
|
|
|
@@ -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)
|