okstra 0.50.0 → 0.52.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.kr.md +8 -7
- package/README.md +8 -7
- package/bin/okstra +2 -0
- package/docs/kr/architecture.md +15 -16
- package/docs/kr/cli.md +5 -5
- package/docs/project-structure-overview.md +10 -6
- package/docs/superpowers/specs/2026-06-06-vertical-slice-tdd-planning-design.md +179 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +15 -11
- package/runtime/agents/workers/claude-worker.md +3 -3
- package/runtime/agents/workers/codex-worker.md +2 -2
- package/runtime/agents/workers/gemini-worker.md +2 -2
- package/runtime/bin/lib/okstra/cli.sh +8 -1
- package/runtime/bin/lib/okstra/globals.sh +3 -0
- package/runtime/bin/lib/okstra/interactive.sh +14 -12
- package/runtime/bin/lib/okstra/usage.sh +6 -0
- package/runtime/bin/okstra-team-reconcile.sh +28 -0
- package/runtime/bin/okstra.sh +2 -0
- package/runtime/prompts/launch.template.md +3 -1
- package/runtime/prompts/profiles/_common-contract.md +4 -4
- package/runtime/prompts/profiles/_implementation-executor.md +2 -2
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +8 -4
- package/runtime/prompts/profiles/implementation.md +1 -1
- package/runtime/python/okstra_ctl/analysis_packet.py +259 -0
- package/runtime/python/okstra_ctl/context_cost.py +308 -0
- package/runtime/python/okstra_ctl/migrate.py +2 -12
- package/runtime/python/okstra_ctl/paths.py +22 -0
- package/runtime/python/okstra_ctl/render.py +284 -125
- package/runtime/python/okstra_ctl/render_final_report.py +31 -0
- package/runtime/python/okstra_ctl/run.py +507 -245
- package/runtime/python/okstra_ctl/sequence.py +2 -5
- package/runtime/python/okstra_ctl/team_reconcile.py +131 -0
- package/runtime/python/okstra_ctl/wizard.py +129 -133
- package/runtime/python/okstra_ctl/worktree.py +13 -5
- package/runtime/schemas/final-report-v1.0.schema.json +4 -0
- package/runtime/skills/okstra-coding-preflight/SKILL.md +69 -0
- package/runtime/skills/okstra-coding-preflight/architecture/hexagonal.md +116 -0
- package/runtime/skills/okstra-coding-preflight/clean-code.md +254 -0
- package/runtime/skills/okstra-coding-preflight/languages/java.md +64 -0
- package/runtime/skills/okstra-coding-preflight/languages/javascript-typescript.md +87 -0
- package/runtime/skills/okstra-coding-preflight/languages/kotlin.md +69 -0
- package/runtime/skills/okstra-coding-preflight/languages/nodejs.md +66 -0
- package/runtime/skills/okstra-coding-preflight/languages/python.md +179 -0
- package/runtime/skills/okstra-coding-preflight/languages/rust.md +105 -0
- package/runtime/skills/okstra-coding-preflight/languages/sql.md +68 -0
- package/runtime/skills/okstra-context-loader/SKILL.md +12 -6
- package/runtime/skills/okstra-inspect/SKILL.md +100 -1
- package/runtime/skills/okstra-memory/SKILL.md +28 -5
- package/runtime/skills/okstra-report-writer/SKILL.md +5 -1
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/skills/okstra-team-contract/SKILL.md +7 -4
- package/runtime/templates/reports/final-report.template.md +1 -0
- package/runtime/templates/worker-prompt-preamble.md +3 -3
- package/runtime/validators/validate-implementation-plan-stages.py +57 -11
- package/src/_python-helper.mjs +3 -3
- package/src/context-cost.mjs +27 -0
- package/src/install.mjs +1 -0
- package/src/memory.mjs +50 -11
- package/src/uninstall.mjs +1 -0
|
@@ -24,6 +24,11 @@ from pathlib import Path
|
|
|
24
24
|
|
|
25
25
|
from okstra_project.dirs import OKSTRA_DIR_NAME, project_json_path
|
|
26
26
|
|
|
27
|
+
# phase 시퀀스 / 기본 next-phase 매핑의 SSOT 는 workflow 모듈이다. 과거
|
|
28
|
+
# render_task_manifest 가 동일한 리스트/딕셔너리를 로컬에 중복 정의했는데,
|
|
29
|
+
# 이는 silent drift 위험이 있어 SSOT import 로 통합한다.
|
|
30
|
+
from .workflow import DEFAULT_NEXT_PHASE, PHASE_SEQUENCE
|
|
31
|
+
|
|
27
32
|
|
|
28
33
|
class TokenRenderError(Exception):
|
|
29
34
|
"""Raised when a template references a `{{TOKEN}}` not present in ctx.
|
|
@@ -238,11 +243,154 @@ def _worker_catalog(ctx: dict) -> dict:
|
|
|
238
243
|
}
|
|
239
244
|
|
|
240
245
|
|
|
246
|
+
def _active_workers(ctx: dict) -> list[dict]:
|
|
247
|
+
catalog = _worker_catalog(ctx)
|
|
248
|
+
workers = []
|
|
249
|
+
for worker_id in _resolve_workers(ctx):
|
|
250
|
+
item = catalog[worker_id]
|
|
251
|
+
workers.append({
|
|
252
|
+
"workerId": item["workerId"],
|
|
253
|
+
"role": item["role"],
|
|
254
|
+
"agent": item["agent"],
|
|
255
|
+
"agentLabel": item["agentLabel"],
|
|
256
|
+
"model": item["model"],
|
|
257
|
+
"modelExecutionValue": item["modelExecutionValue"],
|
|
258
|
+
"promptPath": item["promptPath"],
|
|
259
|
+
"resultPath": item["resultPath"],
|
|
260
|
+
"attemptRequired": True,
|
|
261
|
+
})
|
|
262
|
+
return workers
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _active_task(ctx: dict) -> dict:
|
|
266
|
+
return {
|
|
267
|
+
"projectId": ctx.get("PROJECT_ID", ""),
|
|
268
|
+
"taskGroup": ctx.get("TASK_GROUP", ""),
|
|
269
|
+
"taskId": ctx.get("TASK_ID", ""),
|
|
270
|
+
"taskKey": ctx.get("TASK_KEY", ""),
|
|
271
|
+
"taskType": ctx.get("TASK_TYPE", ""),
|
|
272
|
+
"workCategory": ctx.get("WORKFLOW_WORK_CATEGORY", "unknown"),
|
|
273
|
+
"projectRoot": ctx.get("PROJECT_ROOT", ""),
|
|
274
|
+
"taskRootPath": ctx.get("TASK_ROOT_RELATIVE_PATH", ""),
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _active_workflow(ctx: dict) -> dict:
|
|
279
|
+
return {
|
|
280
|
+
"currentPhase": ctx.get("WORKFLOW_CURRENT_PHASE", ""),
|
|
281
|
+
"currentPhaseState": ctx.get("WORKFLOW_CURRENT_PHASE_STATE", ""),
|
|
282
|
+
"lastCompletedPhase": ctx.get("WORKFLOW_LAST_COMPLETED_PHASE", ""),
|
|
283
|
+
"nextRecommendedPhase": ctx.get("WORKFLOW_NEXT_RECOMMENDED_PHASE", ""),
|
|
284
|
+
"awaitingApproval": ctx.get("WORKFLOW_AWAITING_APPROVAL", "false") == "true",
|
|
285
|
+
"routingStatus": ctx.get("WORKFLOW_ROUTING_STATUS", ""),
|
|
286
|
+
"allowedOutputs": ctx.get("PHASE_ALLOWED_OUTPUTS", ""),
|
|
287
|
+
"forbiddenActions": ctx.get("PHASE_FORBIDDEN_ACTIONS", ""),
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _active_run(ctx: dict) -> dict:
|
|
292
|
+
return {
|
|
293
|
+
"runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
|
|
294
|
+
"runManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
|
|
295
|
+
"teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
|
|
296
|
+
"promptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
|
|
297
|
+
"finalReportPath": ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
|
|
298
|
+
"finalStatusPath": ctx.get("FINAL_STATUS_RELATIVE_PATH", ""),
|
|
299
|
+
"validatorScriptPath": ctx.get("RUN_VALIDATOR_RELATIVE_PATH", ""),
|
|
300
|
+
"resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
|
|
301
|
+
"workerPromptsDirectoryPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
|
|
302
|
+
"workerResultsDirectoryPath": ctx.get("WORKER_RESULTS_RELATIVE_PATH", ""),
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _active_instruction_set(ctx: dict) -> dict:
|
|
307
|
+
instruction_set = ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")
|
|
308
|
+
clarification = (
|
|
309
|
+
instruction_set + "/clarification-response.md"
|
|
310
|
+
if ctx.get("CLARIFICATION_RESPONSE_RELATIVE_PATH", "") else ""
|
|
311
|
+
)
|
|
312
|
+
return {
|
|
313
|
+
"path": instruction_set,
|
|
314
|
+
"analysisPacketPath": ctx.get("ANALYSIS_PACKET_RELATIVE_PATH", ""),
|
|
315
|
+
"taskBriefPath": instruction_set + "/task-brief.md",
|
|
316
|
+
"analysisProfilePath": instruction_set + "/analysis-profile.md",
|
|
317
|
+
"analysisMaterialPath": instruction_set + "/analysis-material.md",
|
|
318
|
+
"referenceExpectationsPath": ctx.get("REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""),
|
|
319
|
+
"clarificationResponsePath": clarification,
|
|
320
|
+
"finalReportTemplatePath": ctx.get("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", ""),
|
|
321
|
+
"finalReportSchemaPath": ctx.get("FINAL_REPORT_SCHEMA_RELATIVE_PATH", ""),
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _active_error_logs(ctx: dict) -> dict:
|
|
326
|
+
return {
|
|
327
|
+
"runErrorsLogPath": ctx.get("RUN_ERRORS_LOG_RELATIVE_PATH", ""),
|
|
328
|
+
"sidecarsByWorkerId": {
|
|
329
|
+
"claude": ctx.get("CLAUDE_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
|
|
330
|
+
"codex": ctx.get("CODEX_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
|
|
331
|
+
"gemini": ctx.get("GEMINI_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
|
|
332
|
+
"report-writer": ctx.get("REPORT_WRITER_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _active_executor_worktree(ctx: dict) -> dict:
|
|
338
|
+
return {
|
|
339
|
+
"status": ctx.get("EXECUTOR_WORKTREE_STATUS", ""),
|
|
340
|
+
"path": ctx.get("EXECUTOR_WORKTREE_PATH", ""),
|
|
341
|
+
"branch": ctx.get("EXECUTOR_WORKTREE_BRANCH", ""),
|
|
342
|
+
"baseRef": ctx.get("EXECUTOR_WORKTREE_BASE_REF", ""),
|
|
343
|
+
"note": ctx.get("EXECUTOR_WORKTREE_NOTE", ""),
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _active_source_artifacts(ctx: dict) -> dict:
|
|
348
|
+
return {
|
|
349
|
+
"taskManifestPath": ctx.get("TASK_MANIFEST_RELATIVE_PATH", ""),
|
|
350
|
+
"runContextPath": ctx.get("RUN_CONTEXT_RELATIVE_PATH", ""),
|
|
351
|
+
"runInputsPath": ctx.get("RUN_INPUTS_RELATIVE_PATH", ""),
|
|
352
|
+
"historyTimelinePath": ctx.get("TIMELINE_RELATIVE_PATH", ""),
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _active_lazy_read_plan() -> dict:
|
|
357
|
+
return {
|
|
358
|
+
"leadPhase1Primary": True,
|
|
359
|
+
"readTaskIndexOnlyForHumanSummary": True,
|
|
360
|
+
"readHistoryTimelineOnlyForHistoryOrCarryInDisambiguation": True,
|
|
361
|
+
"readFinalReportTemplateOnlyForReportWriter": True,
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
|
|
241
365
|
# --------------------------------------------------------------------------- #
|
|
242
366
|
# team-state
|
|
243
367
|
# --------------------------------------------------------------------------- #
|
|
244
368
|
|
|
245
369
|
|
|
370
|
+
def render_active_run_context(active_context_path: str, ctx: dict) -> None:
|
|
371
|
+
"""Write the compact lead intake surface for the current run.
|
|
372
|
+
|
|
373
|
+
This is an interface file, not a replacement for task/run manifests. The
|
|
374
|
+
source manifests remain the audit/replay authority; this file concentrates
|
|
375
|
+
the fields the lead needs at Phase 1 so it does not have to recompose the
|
|
376
|
+
current run from several shallow artifacts.
|
|
377
|
+
"""
|
|
378
|
+
payload = {
|
|
379
|
+
"schemaVersion": "1.0",
|
|
380
|
+
"kind": "active-run-context",
|
|
381
|
+
"task": _active_task(ctx),
|
|
382
|
+
"workflow": _active_workflow(ctx),
|
|
383
|
+
"run": _active_run(ctx),
|
|
384
|
+
"instructionSet": _active_instruction_set(ctx),
|
|
385
|
+
"workers": _active_workers(ctx),
|
|
386
|
+
"errorLogs": _active_error_logs(ctx),
|
|
387
|
+
"executorWorktree": _active_executor_worktree(ctx),
|
|
388
|
+
"sourceArtifacts": _active_source_artifacts(ctx),
|
|
389
|
+
"lazyReadPlan": _active_lazy_read_plan(),
|
|
390
|
+
}
|
|
391
|
+
_write_json(Path(active_context_path), payload)
|
|
392
|
+
|
|
393
|
+
|
|
246
394
|
def render_team_state(team_state_path: str, ctx: dict) -> None:
|
|
247
395
|
selected = _resolve_workers(ctx)
|
|
248
396
|
catalog = _worker_catalog(ctx)
|
|
@@ -598,6 +746,121 @@ def _required_worker_roles(ctx: dict, reviewers: list[str]) -> list[dict]:
|
|
|
598
746
|
]
|
|
599
747
|
|
|
600
748
|
|
|
749
|
+
def _derive_phase_states(existing_workflow: dict, ctx: dict) -> tuple[dict, str, str]:
|
|
750
|
+
"""phaseStates dict + (current_phase, current_phase_state) 를 도출한다.
|
|
751
|
+
|
|
752
|
+
기존 manifest 의 phaseStates 를 보존하면서 PHASE_SEQUENCE 의 모든 phase 를
|
|
753
|
+
not-started 로 채우고 current_phase 만 현재 상태로 덮어쓴다."""
|
|
754
|
+
phase_states = (
|
|
755
|
+
existing_workflow.get("phaseStates", {})
|
|
756
|
+
if isinstance(existing_workflow.get("phaseStates"), dict)
|
|
757
|
+
else {}
|
|
758
|
+
)
|
|
759
|
+
current_phase = ctx.get("WORKFLOW_CURRENT_PHASE", ctx.get("TASK_TYPE", ""))
|
|
760
|
+
current_phase_state = ctx.get("WORKFLOW_CURRENT_PHASE_STATE", "not-started")
|
|
761
|
+
for phase in PHASE_SEQUENCE:
|
|
762
|
+
phase_states.setdefault(phase, "not-started")
|
|
763
|
+
if current_phase:
|
|
764
|
+
phase_states[current_phase] = current_phase_state
|
|
765
|
+
return phase_states, current_phase, current_phase_state
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def _derive_next_recommended_phase(
|
|
769
|
+
current_phase: str, current_phase_state: str, existing_workflow: dict, ctx: dict
|
|
770
|
+
) -> str:
|
|
771
|
+
"""canonical next phase 를 deterministic 하게 결정한다.
|
|
772
|
+
|
|
773
|
+
`current_phase` 가 terminal-success(`completed`) 가 아니면 그 자리에 머무르도록
|
|
774
|
+
recommend 한다. `prepared` / `in-progress` / `blocked` / `error` /
|
|
775
|
+
`contract-violated` 상태에서 다음 phase 로 미리 advance 하면 wizard(와 사람)
|
|
776
|
+
가 현재 phase 를 끝난 것으로 오인한다. (Historical bug: implementation 이
|
|
777
|
+
`prepared` 상태로 provision 됐을 때 wizard 가 `final-verification` 을 기본
|
|
778
|
+
task_type 으로 추천했다.)"""
|
|
779
|
+
canonical_next = DEFAULT_NEXT_PHASE.get(current_phase) or ctx.get(
|
|
780
|
+
"WORKFLOW_NEXT_RECOMMENDED_PHASE", "unknown"
|
|
781
|
+
)
|
|
782
|
+
existing_next = existing_workflow.get("nextRecommendedPhase") or ""
|
|
783
|
+
if current_phase_state not in {"completed"}:
|
|
784
|
+
# 아직 끝나지 않음 — 같은 phase 를 추천한다. sequence 상 더 앞을 가리키는
|
|
785
|
+
# stale existing_next 는 무시하고, 같은 current_phase 를 가리키는 값만 존중.
|
|
786
|
+
return current_phase
|
|
787
|
+
if existing_next and existing_next != current_phase:
|
|
788
|
+
return existing_next
|
|
789
|
+
return canonical_next
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def _derive_latest_pointers(existing: dict, ctx: dict, current_report_relative: str) -> dict:
|
|
793
|
+
"""latest run/report/team 포인터 + lastSafeCheckpoint 를 도출한다.
|
|
794
|
+
|
|
795
|
+
render-only 경로는 기존 manifest 의 포인터를 보존(checkpoint 가 진행 중인
|
|
796
|
+
실제 run 을 덮어쓰지 않도록)하고, 일반 경로는 이번 run 의 ctx 값으로 갱신한다."""
|
|
797
|
+
render_only = ctx.get("RENDER_ONLY", "") == "true"
|
|
798
|
+
existing_workflow = (
|
|
799
|
+
existing.get("workflow", {}) if isinstance(existing.get("workflow"), dict) else {}
|
|
800
|
+
)
|
|
801
|
+
existing_checkpoint = existing_workflow.get("lastSafeCheckpoint", {})
|
|
802
|
+
if not isinstance(existing_checkpoint, dict):
|
|
803
|
+
existing_checkpoint = {}
|
|
804
|
+
if render_only:
|
|
805
|
+
return {
|
|
806
|
+
"lastSafeCheckpoint": {
|
|
807
|
+
"label": existing_checkpoint.get("label", ""),
|
|
808
|
+
"taskManifestPath": existing_checkpoint.get(
|
|
809
|
+
"taskManifestPath", ctx.get("TASK_MANIFEST_RELATIVE_PATH", "")
|
|
810
|
+
),
|
|
811
|
+
"taskIndexPath": existing_checkpoint.get(
|
|
812
|
+
"taskIndexPath", ctx.get("TASK_INDEX_RELATIVE_PATH", "")
|
|
813
|
+
),
|
|
814
|
+
"latestRunPath": existing_checkpoint.get(
|
|
815
|
+
"latestRunPath", existing.get("latestRunPath", "")
|
|
816
|
+
),
|
|
817
|
+
"latestRunManifestPath": existing_checkpoint.get(
|
|
818
|
+
"latestRunManifestPath", ""
|
|
819
|
+
),
|
|
820
|
+
"latestTeamStatePath": existing_checkpoint.get(
|
|
821
|
+
"latestTeamStatePath", existing.get("teamStatePath", "")
|
|
822
|
+
),
|
|
823
|
+
"latestReportPath": existing_checkpoint.get(
|
|
824
|
+
"latestReportPath", existing.get("latestReportPath", "")
|
|
825
|
+
),
|
|
826
|
+
"latestResumeCommandPath": existing_checkpoint.get(
|
|
827
|
+
"latestResumeCommandPath", existing.get("latestResumeCommandPath", "")
|
|
828
|
+
),
|
|
829
|
+
},
|
|
830
|
+
"latestRunPath": existing.get("latestRunPath", "")
|
|
831
|
+
or ctx.get("LATEST_RUN_RELATIVE_PATH", ""),
|
|
832
|
+
"latestRunStatus": existing.get("latestRunStatus", "")
|
|
833
|
+
or ctx.get("CURRENT_RUN_STATUS", ""),
|
|
834
|
+
"latestRunPromptsPath": existing.get("latestRunPromptsPath", "")
|
|
835
|
+
or ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
|
|
836
|
+
"latestReportPath": existing.get("latestReportPath", "")
|
|
837
|
+
or current_report_relative,
|
|
838
|
+
"latestTeamStatePath": existing.get("teamStatePath", "")
|
|
839
|
+
or ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
|
|
840
|
+
"latestResumeCommandPath": existing.get("latestResumeCommandPath", ""),
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
"lastSafeCheckpoint": {
|
|
844
|
+
"label": ctx.get("WORKFLOW_LAST_SAFE_CHECKPOINT_LABEL", ""),
|
|
845
|
+
"taskManifestPath": ctx.get("TASK_MANIFEST_RELATIVE_PATH", ""),
|
|
846
|
+
"taskIndexPath": ctx.get("TASK_INDEX_RELATIVE_PATH", ""),
|
|
847
|
+
"latestRunPath": ctx.get("LATEST_RUN_RELATIVE_PATH", ""),
|
|
848
|
+
"latestRunManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
|
|
849
|
+
"latestTeamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
|
|
850
|
+
"latestReportPath": current_report_relative,
|
|
851
|
+
"latestResumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
|
|
852
|
+
},
|
|
853
|
+
"latestRunPath": ctx.get("LATEST_RUN_RELATIVE_PATH", ""),
|
|
854
|
+
"latestRunStatus": ctx.get("CURRENT_RUN_STATUS", ""),
|
|
855
|
+
"latestRunPromptsPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
|
|
856
|
+
"latestReportPath": current_report_relative
|
|
857
|
+
or existing.get("latestReportPath", ""),
|
|
858
|
+
"latestTeamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
|
|
859
|
+
"latestResumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", "")
|
|
860
|
+
or existing.get("latestResumeCommandPath", ""),
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
|
|
601
864
|
def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
602
865
|
path = Path(manifest_path)
|
|
603
866
|
existing = {}
|
|
@@ -608,22 +871,6 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
608
871
|
existing = {}
|
|
609
872
|
reviewers = _resolve_workers(ctx)
|
|
610
873
|
catalog = _worker_catalog(ctx)
|
|
611
|
-
phase_sequence = [
|
|
612
|
-
"requirements-discovery",
|
|
613
|
-
"error-analysis",
|
|
614
|
-
"implementation-planning",
|
|
615
|
-
"implementation",
|
|
616
|
-
"final-verification",
|
|
617
|
-
"release-handoff",
|
|
618
|
-
]
|
|
619
|
-
default_next_phase = {
|
|
620
|
-
"requirements-discovery": "pending-routing-decision",
|
|
621
|
-
"error-analysis": "implementation-planning",
|
|
622
|
-
"implementation-planning": "implementation",
|
|
623
|
-
"implementation": "final-verification",
|
|
624
|
-
"final-verification": "pending-release-handoff",
|
|
625
|
-
"release-handoff": "done-or-follow-up",
|
|
626
|
-
}
|
|
627
874
|
required_worker_roles = _required_worker_roles(ctx, reviewers)
|
|
628
875
|
worker_prompt_paths = {item: catalog[item]["promptPath"] for item in reviewers}
|
|
629
876
|
required_agent_status_entries = ["Claude lead"] + [
|
|
@@ -633,129 +880,37 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
633
880
|
current_report_relative = ctx.get("LATEST_REPORT_RELATIVE_PATH") or ctx.get(
|
|
634
881
|
"FINAL_REPORT_RELATIVE_PATH", ""
|
|
635
882
|
)
|
|
636
|
-
|
|
883
|
+
existing_workflow = (
|
|
637
884
|
existing.get("workflow", {})
|
|
638
885
|
if isinstance(existing.get("workflow"), dict)
|
|
639
886
|
else {}
|
|
640
887
|
)
|
|
641
|
-
phase_states = (
|
|
642
|
-
|
|
643
|
-
if isinstance(workflow.get("phaseStates"), dict)
|
|
644
|
-
else {}
|
|
888
|
+
phase_states, current_phase, current_phase_state = _derive_phase_states(
|
|
889
|
+
existing_workflow, ctx
|
|
645
890
|
)
|
|
646
|
-
current_phase = ctx.get("WORKFLOW_CURRENT_PHASE", ctx.get("TASK_TYPE", ""))
|
|
647
|
-
current_phase_state = ctx.get("WORKFLOW_CURRENT_PHASE_STATE", "not-started")
|
|
648
|
-
for phase in phase_sequence:
|
|
649
|
-
phase_states.setdefault(phase, "not-started")
|
|
650
|
-
if current_phase:
|
|
651
|
-
phase_states[current_phase] = current_phase_state
|
|
652
891
|
work_category = existing.get("workCategory") or ctx.get(
|
|
653
892
|
"WORKFLOW_WORK_CATEGORY", "unknown"
|
|
654
893
|
)
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
# (`completed`). For `prepared` / `in-progress` / `blocked` / `error` /
|
|
658
|
-
# `contract-violated` states the recommendation MUST stay on
|
|
659
|
-
# `current_phase` — advancing prematurely makes wizards (and humans)
|
|
660
|
-
# think the current phase is done when it has merely been provisioned.
|
|
661
|
-
# Historical bug: implementation provisioned in `prepared` state caused
|
|
662
|
-
# the wizard to recommend `final-verification` as the default task_type.
|
|
663
|
-
canonical_next = default_next_phase.get(
|
|
664
|
-
current_phase, ctx.get("WORKFLOW_NEXT_RECOMMENDED_PHASE", "unknown")
|
|
894
|
+
next_recommended_phase = _derive_next_recommended_phase(
|
|
895
|
+
current_phase, current_phase_state, existing_workflow, ctx
|
|
665
896
|
)
|
|
666
|
-
|
|
667
|
-
terminal_success_states = {"completed"}
|
|
668
|
-
if current_phase_state not in terminal_success_states:
|
|
669
|
-
# Current phase has not finished — recommend staying on it. Suppress
|
|
670
|
-
# any stale `existing_next` that points further forward in the
|
|
671
|
-
# sequence; only honour a stale value if it points to the SAME
|
|
672
|
-
# current_phase (i.e. encodes "re-enter current phase").
|
|
673
|
-
next_recommended_phase = current_phase
|
|
674
|
-
elif existing_next and existing_next != current_phase:
|
|
675
|
-
next_recommended_phase = existing_next
|
|
676
|
-
else:
|
|
677
|
-
next_recommended_phase = canonical_next
|
|
678
|
-
last_completed_phase = workflow.get("lastCompletedPhase") or ctx.get(
|
|
897
|
+
last_completed_phase = existing_workflow.get("lastCompletedPhase") or ctx.get(
|
|
679
898
|
"WORKFLOW_LAST_COMPLETED_PHASE", ""
|
|
680
899
|
)
|
|
681
|
-
routing_status =
|
|
900
|
+
routing_status = existing_workflow.get("routingStatus") or ctx.get(
|
|
682
901
|
"WORKFLOW_ROUTING_STATUS", "not-applicable"
|
|
683
902
|
)
|
|
684
|
-
awaiting_approval =
|
|
903
|
+
awaiting_approval = existing_workflow.get("awaitingApproval")
|
|
685
904
|
if not isinstance(awaiting_approval, bool):
|
|
686
905
|
awaiting_approval = ctx.get("WORKFLOW_AWAITING_APPROVAL", "false") == "true"
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
if render_only:
|
|
696
|
-
last_safe_checkpoint = {
|
|
697
|
-
"label": existing_checkpoint.get("label", ""),
|
|
698
|
-
"taskManifestPath": existing_checkpoint.get(
|
|
699
|
-
"taskManifestPath", ctx.get("TASK_MANIFEST_RELATIVE_PATH", "")
|
|
700
|
-
),
|
|
701
|
-
"taskIndexPath": existing_checkpoint.get(
|
|
702
|
-
"taskIndexPath", ctx.get("TASK_INDEX_RELATIVE_PATH", "")
|
|
703
|
-
),
|
|
704
|
-
"latestRunPath": existing_checkpoint.get(
|
|
705
|
-
"latestRunPath", existing.get("latestRunPath", "")
|
|
706
|
-
),
|
|
707
|
-
"latestRunManifestPath": existing_checkpoint.get(
|
|
708
|
-
"latestRunManifestPath", ""
|
|
709
|
-
),
|
|
710
|
-
"latestTeamStatePath": existing_checkpoint.get(
|
|
711
|
-
"latestTeamStatePath", existing.get("teamStatePath", "")
|
|
712
|
-
),
|
|
713
|
-
"latestReportPath": existing_checkpoint.get(
|
|
714
|
-
"latestReportPath", existing.get("latestReportPath", "")
|
|
715
|
-
),
|
|
716
|
-
"latestResumeCommandPath": existing_checkpoint.get(
|
|
717
|
-
"latestResumeCommandPath", existing.get("latestResumeCommandPath", "")
|
|
718
|
-
),
|
|
719
|
-
}
|
|
720
|
-
latest_run_relative = existing.get("latestRunPath", "") or ctx.get(
|
|
721
|
-
"LATEST_RUN_RELATIVE_PATH", ""
|
|
722
|
-
)
|
|
723
|
-
latest_run_status = existing.get("latestRunStatus", "") or ctx.get(
|
|
724
|
-
"CURRENT_RUN_STATUS", ""
|
|
725
|
-
)
|
|
726
|
-
latest_run_prompts_relative = existing.get(
|
|
727
|
-
"latestRunPromptsPath", ""
|
|
728
|
-
) or ctx.get("RUN_PROMPTS_RELATIVE_PATH", "")
|
|
729
|
-
latest_report_relative = (
|
|
730
|
-
existing.get("latestReportPath", "") or current_report_relative
|
|
731
|
-
)
|
|
732
|
-
latest_team_state_relative = existing.get("teamStatePath", "") or ctx.get(
|
|
733
|
-
"TEAM_STATE_RELATIVE_PATH", ""
|
|
734
|
-
)
|
|
735
|
-
latest_resume_command_relative = existing.get("latestResumeCommandPath", "")
|
|
736
|
-
else:
|
|
737
|
-
last_safe_checkpoint = {
|
|
738
|
-
"label": ctx.get("WORKFLOW_LAST_SAFE_CHECKPOINT_LABEL", ""),
|
|
739
|
-
"taskManifestPath": ctx.get("TASK_MANIFEST_RELATIVE_PATH", ""),
|
|
740
|
-
"taskIndexPath": ctx.get("TASK_INDEX_RELATIVE_PATH", ""),
|
|
741
|
-
"latestRunPath": ctx.get("LATEST_RUN_RELATIVE_PATH", ""),
|
|
742
|
-
"latestRunManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
|
|
743
|
-
"latestTeamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
|
|
744
|
-
"latestReportPath": current_report_relative,
|
|
745
|
-
"latestResumeCommandPath": ctx.get(
|
|
746
|
-
"CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""
|
|
747
|
-
),
|
|
748
|
-
}
|
|
749
|
-
latest_run_relative = ctx.get("LATEST_RUN_RELATIVE_PATH", "")
|
|
750
|
-
latest_run_status = ctx.get("CURRENT_RUN_STATUS", "")
|
|
751
|
-
latest_run_prompts_relative = ctx.get("RUN_PROMPTS_RELATIVE_PATH", "")
|
|
752
|
-
latest_report_relative = current_report_relative or existing.get(
|
|
753
|
-
"latestReportPath", ""
|
|
754
|
-
)
|
|
755
|
-
latest_team_state_relative = ctx.get("TEAM_STATE_RELATIVE_PATH", "")
|
|
756
|
-
latest_resume_command_relative = ctx.get(
|
|
757
|
-
"CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""
|
|
758
|
-
) or existing.get("latestResumeCommandPath", "")
|
|
906
|
+
pointers = _derive_latest_pointers(existing, ctx, current_report_relative)
|
|
907
|
+
last_safe_checkpoint = pointers["lastSafeCheckpoint"]
|
|
908
|
+
latest_run_relative = pointers["latestRunPath"]
|
|
909
|
+
latest_run_status = pointers["latestRunStatus"]
|
|
910
|
+
latest_run_prompts_relative = pointers["latestRunPromptsPath"]
|
|
911
|
+
latest_report_relative = pointers["latestReportPath"]
|
|
912
|
+
latest_team_state_relative = pointers["latestTeamStatePath"]
|
|
913
|
+
latest_resume_command_relative = pointers["latestResumeCommandPath"]
|
|
759
914
|
convergence_block = _build_convergence_block(ctx)
|
|
760
915
|
payload = {
|
|
761
916
|
"schemaVersion": "1.0",
|
|
@@ -791,7 +946,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
791
946
|
"latestResumeCommandPath": latest_resume_command_relative,
|
|
792
947
|
"teamStatePath": latest_team_state_relative,
|
|
793
948
|
"workflow": {
|
|
794
|
-
"phaseSequence":
|
|
949
|
+
"phaseSequence": PHASE_SEQUENCE,
|
|
795
950
|
"currentPhase": current_phase,
|
|
796
951
|
"currentPhaseState": phase_states.get(current_phase, current_phase_state),
|
|
797
952
|
"phaseStates": phase_states,
|
|
@@ -806,6 +961,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
806
961
|
+ "/analysis-profile.md",
|
|
807
962
|
"analysisMaterialPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")
|
|
808
963
|
+ "/analysis-material.md",
|
|
964
|
+
"analysisPacketPath": ctx.get("ANALYSIS_PACKET_RELATIVE_PATH", ""),
|
|
809
965
|
"taskBriefCopyPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")
|
|
810
966
|
+ "/task-brief.md",
|
|
811
967
|
"referenceExpectationsPath": ctx.get(
|
|
@@ -821,6 +977,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
821
977
|
),
|
|
822
978
|
"resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
|
|
823
979
|
"teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
|
|
980
|
+
"activeRunContextPath": ctx.get("ACTIVE_RUN_CONTEXT_RELATIVE_PATH", ""),
|
|
824
981
|
"workerResultsDirectoryPath": ctx.get("WORKER_RESULTS_RELATIVE_PATH", ""),
|
|
825
982
|
"validatorScriptPath": ctx.get("RUN_VALIDATOR_RELATIVE_PATH", ""),
|
|
826
983
|
},
|
|
@@ -1009,6 +1166,8 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1009
1166
|
"expectedReportPath": ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
|
|
1010
1167
|
"expectedStatusPath": ctx.get("FINAL_STATUS_RELATIVE_PATH", ""),
|
|
1011
1168
|
"teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
|
|
1169
|
+
"activeRunContextPath": ctx.get("ACTIVE_RUN_CONTEXT_RELATIVE_PATH", ""),
|
|
1170
|
+
"analysisPacketPath": ctx.get("ANALYSIS_PACKET_RELATIVE_PATH", ""),
|
|
1012
1171
|
"workerResultsDirectoryPath": ctx.get("WORKER_RESULTS_RELATIVE_PATH", ""),
|
|
1013
1172
|
"reportTemplatePath": ctx.get("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", ""),
|
|
1014
1173
|
"validatorScriptPath": ctx.get("RUN_VALIDATOR_RELATIVE_PATH", ""),
|
|
@@ -17,6 +17,11 @@ Phase 7 mutation flow: ``okstra-token-usage.py --substitute-data`` fills
|
|
|
17
17
|
the ``tokenUsage`` and ``executionStatus[].totalTokens`` etc. cells in
|
|
18
18
|
data.json, then re-invokes this renderer so the markdown stays in sync.
|
|
19
19
|
The markdown is never hand-edited.
|
|
20
|
+
|
|
21
|
+
As of v0.33+, the renderer itself is the schema-enforcement seam: data.json
|
|
22
|
+
is validated against ``schemas/final-report-v1.0.schema.json`` before any
|
|
23
|
+
Jinja2 rendering begins, so schema violations are caught at write-time
|
|
24
|
+
rather than only by the post-hoc ``validators/validate-run.py``.
|
|
20
25
|
"""
|
|
21
26
|
from __future__ import annotations
|
|
22
27
|
|
|
@@ -34,6 +39,7 @@ from typing import Any
|
|
|
34
39
|
import okstra_vendor # noqa: F401 — side effect: sys.modules aliases
|
|
35
40
|
from jinja2 import ChainableUndefined, Environment, FileSystemLoader
|
|
36
41
|
|
|
42
|
+
from okstra_ctl.final_report_schema import SchemaError, load_schema, validate as schema_validate
|
|
37
43
|
from okstra_ctl.i18n import I18nError, SUPPORTED_LANGS, load_dictionary, make_jinja_global
|
|
38
44
|
|
|
39
45
|
|
|
@@ -104,6 +110,29 @@ def _yaml_inline_list(values: list[str]) -> str:
|
|
|
104
110
|
return "[" + ", ".join(_yaml_scalar(v) for v in values) + "]"
|
|
105
111
|
|
|
106
112
|
|
|
113
|
+
def _enforce_schema(data: dict) -> None:
|
|
114
|
+
"""렌더 전에 data.json 을 스키마에 대해 검증하는 seam.
|
|
115
|
+
|
|
116
|
+
스키마 파일을 찾지 못하는 경우(손상된 설치 환경)는 경고만 출력하고 계속
|
|
117
|
+
진행한다 — validate-run 과 install 경고가 이미 해당 상황을 표면화하므로
|
|
118
|
+
Phase 7 재렌더를 hard-fail 시키는 것은 과도하다.
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
schema = load_schema()
|
|
122
|
+
except SchemaError as exc:
|
|
123
|
+
print(
|
|
124
|
+
f"render-final-report: schema not locatable; skipping schema enforcement ({exc})",
|
|
125
|
+
file=sys.stderr,
|
|
126
|
+
)
|
|
127
|
+
return
|
|
128
|
+
errors = schema_validate(data, schema)
|
|
129
|
+
if errors:
|
|
130
|
+
raise FinalReportRenderError(
|
|
131
|
+
f"final-report data.json fails schema validation ({len(errors)} error(s)): "
|
|
132
|
+
+ "; ".join(errors[:5])
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
107
136
|
def _build_environment(template_dir: Path) -> Environment:
|
|
108
137
|
# ChainableUndefined lets optional fields (e.g.
|
|
109
138
|
# ``clarificationCarryIn``, ``ticketCoverage.omit``) silently evaluate
|
|
@@ -161,6 +190,8 @@ def render(
|
|
|
161
190
|
if not template_path.is_file():
|
|
162
191
|
raise FinalReportRenderError(f"template not found: {template_path}")
|
|
163
192
|
|
|
193
|
+
_enforce_schema(data)
|
|
194
|
+
|
|
164
195
|
lang = resolve_report_language(data, override=report_language)
|
|
165
196
|
try:
|
|
166
197
|
dictionary = load_dictionary(lang)
|