okstra 0.179.2 → 0.183.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli-registry.mjs +30 -2
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/incremental-carry.mjs +9 -8
- package/dist/commands/execute/incremental-carry.mjs.map +1 -1
- package/dist/commands/execute/plan-verify.mjs +3 -1
- package/dist/commands/execute/plan-verify.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.d.mts +4 -2
- package/dist/commands/execute/render-bundle.mjs +46 -5
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/run.mjs +11 -3
- package/dist/commands/execute/run.mjs.map +1 -1
- package/dist/commands/inspect/model-io.d.mts +1 -0
- package/dist/commands/inspect/model-io.mjs +25 -0
- package/dist/commands/inspect/model-io.mjs.map +1 -0
- package/dist/commands/inspect/stage-map.mjs +29 -8
- package/dist/commands/inspect/stage-map.mjs.map +1 -1
- package/dist/commands/inspect/task-list.mjs +52 -6
- package/dist/commands/inspect/task-list.mjs.map +1 -1
- package/dist/commands/inspect/user-response.mjs +14 -4
- package/dist/commands/inspect/user-response.mjs.map +1 -1
- package/dist/commands/lifecycle/check-project.d.mts +1 -0
- package/dist/commands/lifecycle/check-project.mjs +69 -50
- package/dist/commands/lifecycle/check-project.mjs.map +1 -1
- package/dist/commands/lifecycle/contract-check.d.mts +1 -0
- package/dist/commands/lifecycle/contract-check.mjs +18 -0
- package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
- package/dist/commands/lifecycle/preflight.mjs +154 -51
- package/dist/commands/lifecycle/preflight.mjs.map +1 -1
- package/dist/commands/pr/pr.d.mts +1 -0
- package/dist/commands/pr/pr.mjs +19 -1
- package/dist/commands/pr/pr.mjs.map +1 -1
- package/dist/commands/report/agent-activity.mjs +2 -2
- package/dist/commands/report/approval-decision.d.mts +1 -0
- package/dist/commands/report/approval-decision.mjs +21 -0
- package/dist/commands/report/approval-decision.mjs.map +1 -0
- package/dist/commands/report/design-snapshot.d.mts +1 -0
- package/dist/commands/report/design-snapshot.mjs +19 -0
- package/dist/commands/report/design-snapshot.mjs.map +1 -0
- package/dist/commands/report/translate.mjs +3 -0
- package/dist/commands/report/translate.mjs.map +1 -1
- package/dist/lib/host-registry-client.mjs +13 -9
- package/dist/lib/host-registry-client.mjs.map +1 -1
- package/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +21 -10
- package/docs/cli.md +36 -20
- package/docs/container.md +6 -4
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
- package/docs/for-ai/skills/okstra-code-review.md +4 -4
- package/docs/for-ai/skills/okstra-container-build.md +20 -17
- package/docs/for-ai/skills/okstra-inspect.md +20 -23
- package/docs/for-ai/skills/okstra-manager.md +19 -18
- package/docs/for-ai/skills/okstra-memory.md +2 -2
- package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
- package/docs/for-ai/skills/okstra-rollup.md +14 -13
- package/docs/for-ai/skills/okstra-run.md +7 -3
- package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
- package/docs/for-ai/skills/okstra-setup.md +7 -7
- package/docs/for-ai/skills/okstra-usage.md +5 -4
- package/docs/for-ai/skills/okstra-user-response.md +50 -32
- package/docs/project-structure-overview.md +43 -31
- package/docs/task-process/implementation-planning.md +2 -2
- package/docs/task-process/implementation.md +7 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/report-writer-worker.md +15 -164
- package/runtime/agents/workers/translator-worker.md +5 -13
- package/runtime/bin/okstra-error-log.py +51 -11
- package/runtime/bin/okstra-report-translate.py +210 -23
- package/runtime/prompts/host-orchestration/implementation.md +1 -1
- package/runtime/prompts/launch.template.md +9 -12
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/context-loader.md +41 -141
- package/runtime/prompts/lead/convergence.md +10 -8
- package/runtime/prompts/lead/okstra-lead-contract.md +40 -49
- package/runtime/prompts/lead/plan-body-verification.md +217 -23
- package/runtime/prompts/lead/report-writer.md +81 -421
- package/runtime/prompts/lead/team-contract.md +9 -54
- package/runtime/prompts/profiles/_clarification-recommendation.md +5 -4
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
- package/runtime/prompts/profiles/_common-contract.md +4 -4
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/change-impact-analysis.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/feature-analysis.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +20 -12
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/profiles/project-analysis.md +1 -1
- package/runtime/prompts/profiles/release-handoff.md +0 -1
- package/runtime/prompts/profiles/requirements-discovery.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +0 -1
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +8 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
- package/runtime/python/okstra_ctl/agent_activity.py +141 -38
- package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +70 -23
- package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
- package/runtime/python/okstra_ctl/analysis_packet.py +124 -3
- package/runtime/python/okstra_ctl/approval_decisions.py +328 -0
- package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
- package/runtime/python/okstra_ctl/backfill.py +13 -10
- package/runtime/python/okstra_ctl/batch.py +2 -4
- package/runtime/python/okstra_ctl/build_tools.py +6 -3
- package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
- package/runtime/python/okstra_ctl/clarification_items.py +27 -13
- package/runtime/python/okstra_ctl/cmux.py +130 -52
- package/runtime/python/okstra_ctl/code_review_target.py +34 -8
- package/runtime/python/okstra_ctl/consumers.py +5 -4
- package/runtime/python/okstra_ctl/container.py +103 -8
- package/runtime/python/okstra_ctl/context_cost.py +2 -1
- package/runtime/python/okstra_ctl/contract_graph.py +497 -0
- package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
- package/runtime/python/okstra_ctl/convergence.py +338 -17
- package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
- package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
- package/runtime/python/okstra_ctl/convergence_store.py +55 -34
- package/runtime/python/okstra_ctl/design_prep.py +7 -4
- package/runtime/python/okstra_ctl/design_snapshot.py +134 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +97 -69
- package/runtime/python/okstra_ctl/dispatch_state.py +163 -63
- package/runtime/python/okstra_ctl/doctor.py +6 -3
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
- package/runtime/python/okstra_ctl/error_log_write.py +35 -30
- package/runtime/python/okstra_ctl/error_report.py +26 -1
- package/runtime/python/okstra_ctl/error_zip.py +27 -5
- package/runtime/python/okstra_ctl/execution_identity.py +3 -2
- package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -2
- package/runtime/python/okstra_ctl/final_report_schema.py +26 -17
- package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
- package/runtime/python/okstra_ctl/fixed_text.py +39 -0
- package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
- package/runtime/python/okstra_ctl/handoff.py +5 -4
- package/runtime/python/okstra_ctl/i18n.py +4 -2
- package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
- package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
- package/runtime/python/okstra_ctl/incremental_carry.py +130 -17
- package/runtime/python/okstra_ctl/incremental_scope.py +12 -4
- package/runtime/python/okstra_ctl/index.py +8 -11
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
- package/runtime/python/okstra_ctl/invocation.py +3 -6
- package/runtime/python/okstra_ctl/json_boundary.py +366 -0
- package/runtime/python/okstra_ctl/json_registry.py +10 -12
- package/runtime/python/okstra_ctl/jsonl.py +19 -2
- package/runtime/python/okstra_ctl/lead_events.py +33 -1
- package/runtime/python/okstra_ctl/listing.py +3 -3
- package/runtime/python/okstra_ctl/log_report.py +24 -2
- package/runtime/python/okstra_ctl/manager_cli.py +92 -7
- package/runtime/python/okstra_ctl/manager_store.py +12 -10
- package/runtime/python/okstra_ctl/material.py +5 -1
- package/runtime/python/okstra_ctl/migrate.py +29 -25
- package/runtime/python/okstra_ctl/model_cli.py +3 -15
- package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
- package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
- package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
- package/runtime/python/okstra_ctl/path_hints.py +12 -0
- package/runtime/python/okstra_ctl/paths.py +21 -0
- package/runtime/python/okstra_ctl/plan_items.py +115 -4
- package/runtime/python/okstra_ctl/plan_items_cli.py +626 -34
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +2 -1
- package/runtime/python/okstra_ctl/pr_template.py +3 -2
- package/runtime/python/okstra_ctl/project_meta.py +5 -7
- package/runtime/python/okstra_ctl/recap.py +5 -4
- package/runtime/python/okstra_ctl/reconcile.py +21 -27
- package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
- package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
- package/runtime/python/okstra_ctl/render.py +74 -16
- package/runtime/python/okstra_ctl/render_final_report.py +10 -8
- package/runtime/python/okstra_ctl/report_assembly.py +509 -0
- package/runtime/python/okstra_ctl/report_contract.py +2 -1
- package/runtime/python/okstra_ctl/report_finalize.py +67 -32
- package/runtime/python/okstra_ctl/report_html/render.py +3 -2
- package/runtime/python/okstra_ctl/report_inputs.py +72 -0
- package/runtime/python/okstra_ctl/report_language.py +3 -2
- package/runtime/python/okstra_ctl/report_markdown.py +81 -8
- package/runtime/python/okstra_ctl/report_narrative.py +351 -0
- package/runtime/python/okstra_ctl/report_projections.py +265 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
- package/runtime/python/okstra_ctl/report_views.py +3 -2
- package/runtime/python/okstra_ctl/rollup.py +65 -4
- package/runtime/python/okstra_ctl/run.py +184 -65
- package/runtime/python/okstra_ctl/run_audit.py +3 -2
- package/runtime/python/okstra_ctl/run_context.py +6 -9
- package/runtime/python/okstra_ctl/run_index_row.py +2 -8
- package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
- package/runtime/python/okstra_ctl/schema_excerpt.py +15 -8
- package/runtime/python/okstra_ctl/session_transcript.py +27 -1
- package/runtime/python/okstra_ctl/set_work_status.py +64 -38
- package/runtime/python/okstra_ctl/stage_fix_carry.py +8 -6
- package/runtime/python/okstra_ctl/stage_ledger.py +132 -18
- package/runtime/python/okstra_ctl/stage_map.py +96 -28
- package/runtime/python/okstra_ctl/stage_targets.py +3 -4
- package/runtime/python/okstra_ctl/team.py +3 -2
- package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
- package/runtime/python/okstra_ctl/time_report.py +51 -4
- package/runtime/python/okstra_ctl/usage_identity.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +58 -4
- package/runtime/python/okstra_ctl/user_response.py +1431 -66
- package/runtime/python/okstra_ctl/wizard.py +11 -44
- package/runtime/python/okstra_ctl/work_categories.py +3 -2
- package/runtime/python/okstra_ctl/worker_dispatch.py +5 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +49 -3
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +31 -3
- package/runtime/python/okstra_ctl/worker_runner.py +14 -12
- package/runtime/python/okstra_ctl/workflow.py +2 -1
- package/runtime/python/okstra_ctl/worktree.py +3 -2
- package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
- package/runtime/python/okstra_ctl/write_policy.py +4 -2
- package/runtime/python/okstra_token_usage/antigravity.py +39 -12
- package/runtime/python/okstra_token_usage/collect.py +90 -38
- package/runtime/python/okstra_token_usage/grok.py +127 -0
- package/runtime/schemas/final-report-v2.0.schema.json +9 -0
- package/runtime/schemas/final-report-v3.0.schema.json +10219 -0
- package/runtime/schemas/report-narrative-v3.0.schema.json +30 -0
- package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
- package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
- package/runtime/skills/okstra-code-review/SKILL.md +21 -11
- package/runtime/skills/okstra-container-build/SKILL.md +18 -18
- package/runtime/skills/okstra-inspect/SKILL.md +12 -11
- package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
- package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
- package/runtime/skills/okstra-inspect/facets/history.md +9 -14
- package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
- package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
- package/runtime/skills/okstra-inspect/facets/report.md +6 -10
- package/runtime/skills/okstra-inspect/facets/status.md +9 -8
- package/runtime/skills/okstra-inspect/facets/time.md +3 -3
- package/runtime/skills/okstra-manager/SKILL.md +16 -14
- package/runtime/skills/okstra-memory/SKILL.md +3 -3
- package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
- package/runtime/skills/okstra-rollup/SKILL.md +6 -16
- package/runtime/skills/okstra-run/SKILL.md +7 -7
- package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
- package/runtime/skills/okstra-setup/SKILL.md +21 -13
- package/runtime/skills/okstra-setup/references/project-config.md +2 -2
- package/runtime/skills/okstra-usage/SKILL.md +10 -10
- package/runtime/skills/okstra-user-response/SKILL.md +78 -107
- package/runtime/templates/report-writer-prompt-preamble.md +29 -19
- package/runtime/templates/reports/html/macros/forms.html +6 -4
- package/runtime/templates/reports/schedule.template.md +4 -4
- package/runtime/templates/worker-error-contract.md +17 -29
- package/runtime/validators/validate-run.py +636 -72
- package/runtime/validators/validate_session_conformance.py +43 -7
|
@@ -94,6 +94,7 @@ from okstra_ctl.clarification_items import (
|
|
|
94
94
|
user_response_sidecars,
|
|
95
95
|
)
|
|
96
96
|
from okstra_ctl.convergence_store import write_json_atomic
|
|
97
|
+
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
|
|
97
98
|
from okstra_ctl.incremental_scope import (
|
|
98
99
|
parse_stage_graph,
|
|
99
100
|
preview_link_availability_for_report,
|
|
@@ -869,13 +870,13 @@ def _validate_sidecar_option(plan_path: Path, option_name: str, errors_t: dict)
|
|
|
869
870
|
candidates: list[str] = []
|
|
870
871
|
if data_path.is_file():
|
|
871
872
|
try:
|
|
872
|
-
data =
|
|
873
|
+
data = load_owned_object(data_path, artifact="planning final report")
|
|
873
874
|
planning = data.get("implementationPlanning") or {}
|
|
874
875
|
candidates = [
|
|
875
876
|
c.get("name", "") for c in planning.get("optionCandidates") or []
|
|
876
877
|
if isinstance(c, dict) and c.get("name")
|
|
877
878
|
]
|
|
878
|
-
except (OSError,
|
|
879
|
+
except (OSError, JsonBoundaryError):
|
|
879
880
|
candidates = []
|
|
880
881
|
if option_name not in candidates:
|
|
881
882
|
raise WizardError(
|
|
@@ -2002,8 +2003,8 @@ def _load_wizard_root(workspace_root: str) -> dict:
|
|
|
2002
2003
|
"Re-run `okstra install` or check the workspace_root."
|
|
2003
2004
|
)
|
|
2004
2005
|
try:
|
|
2005
|
-
raw =
|
|
2006
|
-
except
|
|
2006
|
+
raw = load_owned_object(path, artifact="wizard prompts")
|
|
2007
|
+
except JsonBoundaryError as exc:
|
|
2007
2008
|
raise WizardError(f"wizard prompt JSON malformed: {path}: {exc}") from exc
|
|
2008
2009
|
if not isinstance(raw, dict):
|
|
2009
2010
|
raise WizardError(f"wizard prompt JSON root must be an object: {path}")
|
|
@@ -4217,7 +4218,7 @@ def _prior_stage_numbers(state: WizardState) -> set[int]:
|
|
|
4217
4218
|
)
|
|
4218
4219
|
data_path = final_report_data_path(report)
|
|
4219
4220
|
try:
|
|
4220
|
-
data =
|
|
4221
|
+
data = load_owned_object(data_path, artifact="planning final report")
|
|
4221
4222
|
stages = {num for num, _ in parse_stage_graph(data)}
|
|
4222
4223
|
except (OSError, ValueError, KeyError, TypeError) as exc:
|
|
4223
4224
|
raise WizardError(
|
|
@@ -4300,8 +4301,8 @@ def _suggest_project_pr_template(state: WizardState) -> str:
|
|
|
4300
4301
|
if not project_json.is_file():
|
|
4301
4302
|
return ""
|
|
4302
4303
|
try:
|
|
4303
|
-
data =
|
|
4304
|
-
except (OSError,
|
|
4304
|
+
data = load_owned_object(project_json, artifact="project config")
|
|
4305
|
+
except (OSError, JsonBoundaryError):
|
|
4305
4306
|
return ""
|
|
4306
4307
|
val = data.get("prTemplatePath") or ""
|
|
4307
4308
|
return val if isinstance(val, str) else ""
|
|
@@ -5138,39 +5139,6 @@ def _worktree_preview_line_impl(state: WizardState) -> str:
|
|
|
5138
5139
|
stage=stage, path=str(decision.path), branch=decision.branch)
|
|
5139
5140
|
|
|
5140
5141
|
|
|
5141
|
-
def _critic_collision_lines(state: WizardState) -> list[str]:
|
|
5142
|
-
"""critic provider 가 분석 로스터와 겹칠 때 그 대가를 확인 화면에서 말한다.
|
|
5143
|
-
|
|
5144
|
-
gap 검증 투표자는 `convergence_engine._is_critic_worker` 가 critic **provider
|
|
5145
|
-
이름**으로 걸러낸다 — critic 워커 id 가 아니라 provider 다. 그래서 같은
|
|
5146
|
-
provider 의 분석 워커가 투표에서 빠지고, 분석 워커가 둘뿐인 로스터에서는
|
|
5147
|
-
정족수가 1로 준다. 그 사실이 선택 시점에는 어디에도 드러나지 않아, critic 도
|
|
5148
|
-
돌고 gap 검증 라운드도 완료된 뒤 `apply-critic-gaps` 가 거부할 때에야 알려졌다
|
|
5149
|
-
— 이미 워커 실행 비용을 쓴 다음이다.
|
|
5150
|
-
|
|
5151
|
-
막지는 않는다. 겹치는 provider 를 고르는 것이 잘못은 아니고, 그 대가를 알고
|
|
5152
|
-
고르는 것과 모르고 고르는 것의 차이만 없앤다.
|
|
5153
|
-
"""
|
|
5154
|
-
if not state.critic or state.critic == "off":
|
|
5155
|
-
return []
|
|
5156
|
-
analysers = [
|
|
5157
|
-
worker for worker in _resolved_roster(state) if worker != "report-writer"
|
|
5158
|
-
]
|
|
5159
|
-
excluded = [
|
|
5160
|
-
worker for worker in analysers
|
|
5161
|
-
if worker in {state.critic, f"{state.critic}-worker"}
|
|
5162
|
-
]
|
|
5163
|
-
if not excluded:
|
|
5164
|
-
return []
|
|
5165
|
-
return [
|
|
5166
|
-
_msg(
|
|
5167
|
-
state.workspace_root, "confirmation", "critic_shares_analyser_provider",
|
|
5168
|
-
worker=excluded[0],
|
|
5169
|
-
remaining=len(analysers) - len(excluded),
|
|
5170
|
-
)
|
|
5171
|
-
]
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
5142
|
def _build_confirm(state: WizardState) -> Prompt:
|
|
5175
5143
|
t = _p(state.workspace_root, "confirm")
|
|
5176
5144
|
return Prompt(
|
|
@@ -6496,7 +6464,6 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
6496
6464
|
if (not role_selection
|
|
6497
6465
|
and state.task_type in ("requirements-discovery", "error-analysis", "implementation-planning", "final-verification")):
|
|
6498
6466
|
lines.append(f" critic : {state.critic or '(off)'}")
|
|
6499
|
-
lines.extend(_critic_collision_lines(state))
|
|
6500
6467
|
if state.task_type in _STAGE_SCOPED_TASK_TYPES:
|
|
6501
6468
|
lines.append(f" approved-plan : {state.approved_plan_path}")
|
|
6502
6469
|
stage = wizard_stage_confirmation_label(
|
|
@@ -6631,8 +6598,8 @@ def _matching_v2_companion(
|
|
|
6631
6598
|
source_metadata: dict[str, str],
|
|
6632
6599
|
) -> dict[str, Any] | None:
|
|
6633
6600
|
try:
|
|
6634
|
-
payload =
|
|
6635
|
-
except (OSError, UnicodeError,
|
|
6601
|
+
payload = load_owned_object(path, artifact="wizard state")
|
|
6602
|
+
except (OSError, UnicodeError, JsonBoundaryError):
|
|
6636
6603
|
return None
|
|
6637
6604
|
if not isinstance(payload, dict):
|
|
6638
6605
|
return None
|
|
@@ -6661,7 +6628,7 @@ def _load_matching_v2_state(
|
|
|
6661
6628
|
def load_state_file(path: Path) -> WizardState:
|
|
6662
6629
|
source_path = Path(path)
|
|
6663
6630
|
source = source_path.read_bytes()
|
|
6664
|
-
data =
|
|
6631
|
+
data = load_owned_object(source_path, artifact="wizard state")
|
|
6665
6632
|
source_version = data.get("executionIdentityVersion", 1)
|
|
6666
6633
|
source_metadata = _legacy_source_metadata(source_path, source)
|
|
6667
6634
|
source_digest = source_metadata["sha256"]
|
|
@@ -12,6 +12,7 @@ import json
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
14
|
from .paths import task_dir, task_manifest_file
|
|
15
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
15
16
|
|
|
16
17
|
WORK_CATEGORIES: tuple[str, ...] = (
|
|
17
18
|
"bugfix",
|
|
@@ -32,8 +33,8 @@ def recorded_work_category(project_root: Path, task_group: str, task_id: str) ->
|
|
|
32
33
|
"""task-manifest.json 에 이미 기록된 work-category. 없거나 enum 밖이면 ""."""
|
|
33
34
|
manifest = task_manifest_file(task_dir(project_root, task_group, task_id))
|
|
34
35
|
try:
|
|
35
|
-
data =
|
|
36
|
-
except
|
|
36
|
+
data = load_owned_object(manifest, artifact="task manifest")
|
|
37
|
+
except JsonBoundaryError:
|
|
37
38
|
return ""
|
|
38
39
|
value = str(data.get("workCategory") or "").strip()
|
|
39
40
|
return value if is_valid_category(value) else ""
|
|
@@ -76,7 +76,7 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
76
76
|
parser.add_argument("--okstra-bin", default="")
|
|
77
77
|
parser.add_argument("--workers", default="")
|
|
78
78
|
parser.add_argument("--dry-run", action="store_true")
|
|
79
|
-
parser.add_argument("--idle-timeout-seconds", default="
|
|
79
|
+
parser.add_argument("--idle-timeout-seconds", default="")
|
|
80
80
|
parser.add_argument("--dispatch-kind", default="initial")
|
|
81
81
|
parser.add_argument("--jobs-file", default="")
|
|
82
82
|
return parser
|
|
@@ -86,7 +86,10 @@ def _parse_workers(raw: str) -> list[str]:
|
|
|
86
86
|
return [item.strip() for item in raw.split(",") if item.strip()]
|
|
87
87
|
|
|
88
88
|
|
|
89
|
-
def _parse_idle_timeout(raw: str) -> int:
|
|
89
|
+
def _parse_idle_timeout(raw: str) -> int | None:
|
|
90
|
+
"""명시된 예산, 또는 역할이 정하게 두는 ``None``."""
|
|
91
|
+
if not raw:
|
|
92
|
+
return None
|
|
90
93
|
try:
|
|
91
94
|
value = int(raw)
|
|
92
95
|
except ValueError as exc:
|
|
@@ -89,8 +89,18 @@ def report_writer_prompt_body(
|
|
|
89
89
|
team_state: Mapping[str, Any],
|
|
90
90
|
model: str,
|
|
91
91
|
report_language: str,
|
|
92
|
+
synthesis_packet_path: str = "",
|
|
92
93
|
) -> list[str]:
|
|
93
94
|
"""Render the provider-neutral body for the report-writer audience."""
|
|
95
|
+
if str(manifest.get("reportContractVersion") or "") == "3.0":
|
|
96
|
+
return _report_writer_v3_body(
|
|
97
|
+
manifest,
|
|
98
|
+
active_context,
|
|
99
|
+
team_state,
|
|
100
|
+
model,
|
|
101
|
+
report_language,
|
|
102
|
+
synthesis_packet_path,
|
|
103
|
+
)
|
|
94
104
|
return [
|
|
95
105
|
f"**Model:** Report writer worker, {model}",
|
|
96
106
|
f"**Report Language:** {report_language}",
|
|
@@ -102,9 +112,11 @@ def report_writer_prompt_body(
|
|
|
102
112
|
f"- Task type: `{_require_string(manifest, 'taskType')}`",
|
|
103
113
|
"",
|
|
104
114
|
"## Inputs",
|
|
105
|
-
*
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
*(
|
|
116
|
+
[f"- Report synthesis packet: `{synthesis_packet_path}`"]
|
|
117
|
+
if synthesis_packet_path
|
|
118
|
+
else report_writer_input_lines(manifest, active_context, team_state)
|
|
119
|
+
),
|
|
108
120
|
"",
|
|
109
121
|
"## Output Contract",
|
|
110
122
|
"You are the author of TWO files:",
|
|
@@ -124,6 +136,40 @@ def report_writer_prompt_body(
|
|
|
124
136
|
]
|
|
125
137
|
|
|
126
138
|
|
|
139
|
+
def _report_writer_v3_body(
|
|
140
|
+
manifest: Mapping[str, Any],
|
|
141
|
+
active_context: Mapping[str, Any],
|
|
142
|
+
team_state: Mapping[str, Any],
|
|
143
|
+
model: str,
|
|
144
|
+
report_language: str,
|
|
145
|
+
synthesis_packet_path: str,
|
|
146
|
+
) -> list[str]:
|
|
147
|
+
return [
|
|
148
|
+
f"**Model:** Report writer worker, {model}",
|
|
149
|
+
f"**Report Language:** {report_language}",
|
|
150
|
+
"",
|
|
151
|
+
"# Report Writer Worker Dispatch",
|
|
152
|
+
"",
|
|
153
|
+
"## Task",
|
|
154
|
+
f"- Task key: `{_require_string(manifest, 'taskKey')}`",
|
|
155
|
+
f"- Task type: `{_require_string(manifest, 'taskType')}`",
|
|
156
|
+
"",
|
|
157
|
+
"## Inputs",
|
|
158
|
+
*(
|
|
159
|
+
[f"- Report synthesis packet: `{synthesis_packet_path}`"]
|
|
160
|
+
if synthesis_packet_path
|
|
161
|
+
else report_writer_input_lines(manifest, active_context, team_state)
|
|
162
|
+
),
|
|
163
|
+
"",
|
|
164
|
+
"## Output Contract",
|
|
165
|
+
"You are the author of the report narrative Markdown at Result Path.",
|
|
166
|
+
"Write the worker-result pointer at Worker Result Path.",
|
|
167
|
+
"Maintain the separate audit sidecar at Audit sidecar path.",
|
|
168
|
+
"Do not write the final report record or another owner's input ledger.",
|
|
169
|
+
"Do not return the report inline.",
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
|
|
127
173
|
def report_writer_input_lines(
|
|
128
174
|
manifest: Mapping[str, Any],
|
|
129
175
|
active_context: Mapping[str, Any],
|
|
@@ -8,6 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
from typing import Any, Iterable, Mapping, Sequence
|
|
9
9
|
|
|
10
10
|
from .worker_prompt_body import analysis_worker_label
|
|
11
|
+
from .json_boundary import load_owned_object
|
|
11
12
|
from .worker_prompt_policy import (
|
|
12
13
|
ERRORS_PATH_HEADERS,
|
|
13
14
|
IMPLEMENTATION_HEADERS,
|
|
@@ -389,8 +390,8 @@ def _validate_record_metadata(text: str, record: PromptRecord) -> list[str]:
|
|
|
389
390
|
if record.metadata_path is None:
|
|
390
391
|
return [*errors, "agent invocation metadata path is required"]
|
|
391
392
|
try:
|
|
392
|
-
metadata =
|
|
393
|
-
record.metadata_path
|
|
393
|
+
metadata = load_owned_object(
|
|
394
|
+
record.metadata_path, artifact="agent invocation metadata"
|
|
394
395
|
)
|
|
395
396
|
except (OSError, UnicodeError, ValueError):
|
|
396
397
|
return [*errors, "agent invocation metadata is invalid"]
|
|
@@ -120,9 +120,7 @@ def resolve_prompt_plan(
|
|
|
120
120
|
# that are *supposed* to differ and fail every critic-enabled run.
|
|
121
121
|
analysis_equality_group = None if dispatch_kind == "critic" else "analysis-core"
|
|
122
122
|
analysis_duty: AgentAudience = (
|
|
123
|
-
|
|
124
|
-
if dispatch_kind == "critic" and task_type == "final-verification"
|
|
125
|
-
else "scope-critic"
|
|
123
|
+
critic_duty_for_task_type(task_type)
|
|
126
124
|
if dispatch_kind == "critic"
|
|
127
125
|
else "acceptance-verifier"
|
|
128
126
|
if task_type == "final-verification"
|
|
@@ -232,6 +230,36 @@ def _worker_facing_headers(
|
|
|
232
230
|
return (*ERRORS_PATH_HEADERS, *required_headers)
|
|
233
231
|
|
|
234
232
|
|
|
233
|
+
# 하나의 run 이 실제로 디스패치할 수 있는 critic 은 한 종류다. 판정은 task type
|
|
234
|
+
# 만의 함수이므로 여기 한 번만 적고, 로스터 등록과 프롬프트 정체성 검사가 같은
|
|
235
|
+
# 값을 읽는다 — 로스터가 실행될 수 없는 critic 행을 하나 더 들고 있으면 provider
|
|
236
|
+
# 로만 만든 role 라벨이 겹쳐 validate-run 의 중복 역할 검사에 걸린다.
|
|
237
|
+
CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT: dict[str, AgentAudience] = {
|
|
238
|
+
"scope": "scope-critic",
|
|
239
|
+
"acceptance": "acceptance-critic",
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def critic_duty_for_task_type(task_type: str) -> AgentAudience:
|
|
244
|
+
"""이 task type 의 critic 직무."""
|
|
245
|
+
return (
|
|
246
|
+
"acceptance-critic"
|
|
247
|
+
if task_type == "final-verification"
|
|
248
|
+
else "scope-critic"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def critic_assignment_ref(task_type: str) -> str:
|
|
253
|
+
"""이 task type 이 등록할 유일한 critic 배정 참조."""
|
|
254
|
+
duty = critic_duty_for_task_type(task_type)
|
|
255
|
+
segment = next(
|
|
256
|
+
key
|
|
257
|
+
for key, value in CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT.items()
|
|
258
|
+
if value == duty
|
|
259
|
+
)
|
|
260
|
+
return f"critic/{segment}"
|
|
261
|
+
|
|
262
|
+
|
|
235
263
|
def _executor_worker_id(manifest: Mapping[str, Any]) -> str | None:
|
|
236
264
|
team_contract = manifest.get("teamContract")
|
|
237
265
|
if not isinstance(team_contract, Mapping):
|
|
@@ -28,6 +28,7 @@ from .domain.worker_exec import (
|
|
|
28
28
|
)
|
|
29
29
|
from .domain.worker_presentation import JsonEvents, Presentation
|
|
30
30
|
from .session_transcript import SessionTranscript
|
|
31
|
+
from .json_boundary import JsonBoundaryError, write_owned_object_atomic
|
|
31
32
|
|
|
32
33
|
LIVE = "live"
|
|
33
34
|
QUIET = "quiet"
|
|
@@ -225,7 +226,11 @@ def _with_served_model_observation(
|
|
|
225
226
|
observation.record(observed)
|
|
226
227
|
return observed
|
|
227
228
|
|
|
228
|
-
return JsonEvents(
|
|
229
|
+
return JsonEvents(
|
|
230
|
+
normalise=presentation.normalise,
|
|
231
|
+
observe=observe,
|
|
232
|
+
join_adjacent_text=presentation.join_adjacent_text,
|
|
233
|
+
)
|
|
229
234
|
|
|
230
235
|
|
|
231
236
|
class _ServedModelObservation:
|
|
@@ -350,9 +355,7 @@ def _pump(
|
|
|
350
355
|
stdin_text: str | None = None,
|
|
351
356
|
) -> tuple[int, bool, int]:
|
|
352
357
|
selector = selectors.DefaultSelector()
|
|
353
|
-
readers = _register_streams(
|
|
354
|
-
selector, process, strategy, transcript, presentation == LIVE
|
|
355
|
-
)
|
|
358
|
+
readers = _register_streams(selector, process, strategy, transcript)
|
|
356
359
|
outgoing = _register_prompt(selector, process, stdin_text)
|
|
357
360
|
closing_text: str | None = None
|
|
358
361
|
last_output = time.monotonic()
|
|
@@ -387,6 +390,7 @@ def _pump(
|
|
|
387
390
|
|
|
388
391
|
for reader in readers.values():
|
|
389
392
|
closing_text = reader.flush() or closing_text
|
|
393
|
+
strategy.flush(transcript)
|
|
390
394
|
|
|
391
395
|
exit_code = process.wait()
|
|
392
396
|
if closing_text is None and isinstance(strategy, JsonEvents):
|
|
@@ -441,13 +445,12 @@ def _register_streams(
|
|
|
441
445
|
process: subprocess.Popen[bytes],
|
|
442
446
|
presentation: Presentation,
|
|
443
447
|
transcript: SessionTranscript,
|
|
444
|
-
live: bool,
|
|
445
448
|
) -> dict[int, _LineReader]:
|
|
446
449
|
"""이 자식이 말하는 스트림마다 전략이 지정한 싱크를 붙인다."""
|
|
447
450
|
assert process.stdout is not None
|
|
448
451
|
streams = {"stdout": process.stdout, "stderr": process.stderr}
|
|
449
452
|
readers: dict[int, _LineReader] = {}
|
|
450
|
-
for channel, sink in presentation.sinks(transcript
|
|
453
|
+
for channel, sink in presentation.sinks(transcript):
|
|
451
454
|
stream = streams[channel]
|
|
452
455
|
assert stream is not None, channel
|
|
453
456
|
selector.register(stream, selectors.EVENT_READ)
|
|
@@ -599,12 +602,11 @@ def _write_status(path: Path, status: Mapping[str, Any]) -> None:
|
|
|
599
602
|
Replaced whole rather than rewritten in place: readers poll this file while
|
|
600
603
|
the worker is still running and must never see a half-written document.
|
|
601
604
|
"""
|
|
602
|
-
temporary = Path(f"{path}.tmp")
|
|
603
605
|
try:
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
606
|
+
write_owned_object_atomic(
|
|
607
|
+
path,
|
|
608
|
+
status,
|
|
609
|
+
artifact="worker wrapper status",
|
|
607
610
|
)
|
|
608
|
-
|
|
609
|
-
except OSError:
|
|
611
|
+
except (OSError, JsonBoundaryError):
|
|
610
612
|
return
|
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import json
|
|
16
16
|
from pathlib import Path
|
|
17
|
+
from .json_boundary import load_owned_object
|
|
17
18
|
|
|
18
19
|
PHASE_SEQUENCE = [
|
|
19
20
|
"requirements-discovery",
|
|
@@ -160,7 +161,7 @@ def load_phase_forbidden(workspace_root: Path) -> dict[str, str]:
|
|
|
160
161
|
`prompts/profiles/forbidden-actions.json`.
|
|
161
162
|
"""
|
|
162
163
|
path = Path(workspace_root) / "prompts" / "profiles" / "forbidden-actions.json"
|
|
163
|
-
data =
|
|
164
|
+
data = load_owned_object(path, artifact="forbidden actions profile")
|
|
164
165
|
if "unknown" not in data:
|
|
165
166
|
# compute_workflow_state falls back to the "unknown" entry for any
|
|
166
167
|
# unmapped task_type; without it a missing/renamed key would surface as
|
|
@@ -44,6 +44,7 @@ from typing import Optional
|
|
|
44
44
|
from okstra_project.dirs import okstra_home, project_json_path
|
|
45
45
|
|
|
46
46
|
from .ids import _safe_fs_segment
|
|
47
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
47
48
|
from . import worktree_registry
|
|
48
49
|
from .seeding import (
|
|
49
50
|
SettingsLinkError,
|
|
@@ -415,8 +416,8 @@ def _read_project_json_field(project_root: Path, field: str) -> Optional[tuple[s
|
|
|
415
416
|
if not target.is_file():
|
|
416
417
|
return None
|
|
417
418
|
try:
|
|
418
|
-
data =
|
|
419
|
-
except (OSError,
|
|
419
|
+
data = load_owned_object(target, artifact="project config")
|
|
420
|
+
except (OSError, JsonBoundaryError):
|
|
420
421
|
return None
|
|
421
422
|
if not isinstance(data, dict):
|
|
422
423
|
return None
|
|
@@ -6,6 +6,8 @@ from dataclasses import dataclass
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any, Mapping
|
|
8
8
|
|
|
9
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
@dataclass(frozen=True)
|
|
11
13
|
class WrapperStatus:
|
|
@@ -65,8 +67,8 @@ def prompt_derived_paths(prompt_path: Path) -> tuple[Path, ...]:
|
|
|
65
67
|
|
|
66
68
|
def read_wrapper_status(path: Path) -> WrapperStatus | None:
|
|
67
69
|
try:
|
|
68
|
-
raw =
|
|
69
|
-
except (OSError,
|
|
70
|
+
raw = load_owned_object(path, artifact="worker wrapper status")
|
|
71
|
+
except (OSError, JsonBoundaryError):
|
|
70
72
|
return None
|
|
71
73
|
if not isinstance(raw, dict):
|
|
72
74
|
return None
|
|
@@ -9,6 +9,8 @@ from dataclasses import dataclass
|
|
|
9
9
|
from pathlib import Path, PurePosixPath
|
|
10
10
|
from typing import Any, Literal
|
|
11
11
|
|
|
12
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
13
|
+
|
|
12
14
|
BoundaryPrecision = Literal["exact-path", "directory-boundary", "none"]
|
|
13
15
|
|
|
14
16
|
_PRECISION_RANK = {"none": 0, "directory-boundary": 1, "exact-path": 2}
|
|
@@ -538,8 +540,8 @@ def _rooted(project_root: Path, value: str) -> Path:
|
|
|
538
540
|
|
|
539
541
|
def _read_json(path: Path, label: str) -> Mapping[str, Any]:
|
|
540
542
|
try:
|
|
541
|
-
value =
|
|
542
|
-
except (OSError, UnicodeError,
|
|
543
|
+
value = load_owned_object(path, artifact=label)
|
|
544
|
+
except (OSError, UnicodeError, JsonBoundaryError) as exc:
|
|
543
545
|
raise WritePolicyError(f"{label} is invalid: {path}") from exc
|
|
544
546
|
if not isinstance(value, Mapping):
|
|
545
547
|
raise WritePolicyError(f"{label} must be an object: {path}")
|
|
@@ -1,29 +1,56 @@
|
|
|
1
1
|
"""Antigravity (`agy`) CLI usage collector.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
per-run token/cost usage cannot be attributed to the antigravity worker. These
|
|
7
|
-
collectors therefore report "unavailable" rather than fabricating numbers — the
|
|
8
|
-
worker's usage block degrades to `na` and `/okstra-inspect context-cost` shows
|
|
9
|
-
N/A for antigravity. If a future `agy` build exposes a transcript, wire its
|
|
10
|
-
parser here (model ids stay `gemini-*`, so `antigravity_cost_usd` already prices it).
|
|
3
|
+
공식 headless 문서는 ``--output-format json`` / ``stream-json`` 의 최종
|
|
4
|
+
``usage`` 객체에 토큰을 둔다. okstra 는 이미 ``stream-json`` 으로 호출하고
|
|
5
|
+
그 줄을 워커 로그에 남긴다. 정본은 마지막 ``result.usage`` 이다.
|
|
11
6
|
"""
|
|
12
7
|
from __future__ import annotations
|
|
13
8
|
|
|
14
9
|
from pathlib import Path
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
PRINT_MODE_USAGE_NOTE = "N/A — print mode exposes no transcript"
|
|
11
|
+
from .jsonl_io import iter_jsonl
|
|
18
12
|
|
|
19
13
|
|
|
20
14
|
def antigravity_session_total(json_path: Path) -> dict:
|
|
21
|
-
|
|
15
|
+
"""stream-json 로그에서 마지막 result.usage 를 읽는다."""
|
|
16
|
+
result_usage: dict | None = None
|
|
17
|
+
step_usage: dict | None = None
|
|
18
|
+
model: str | None = None
|
|
19
|
+
for record in iter_jsonl(json_path):
|
|
20
|
+
kind = record.get("event")
|
|
21
|
+
if kind == "init":
|
|
22
|
+
init = record.get("init")
|
|
23
|
+
if isinstance(init, dict) and isinstance(init.get("model"), str):
|
|
24
|
+
model = init["model"]
|
|
25
|
+
continue
|
|
26
|
+
if kind == "result":
|
|
27
|
+
result = record.get("result")
|
|
28
|
+
if isinstance(result, dict) and isinstance(result.get("usage"), dict):
|
|
29
|
+
result_usage = result["usage"]
|
|
30
|
+
continue
|
|
31
|
+
if kind == "step_update":
|
|
32
|
+
step = record.get("step_update")
|
|
33
|
+
if isinstance(step, dict) and isinstance(step.get("usage"), dict):
|
|
34
|
+
step_usage = step["usage"]
|
|
35
|
+
usage = result_usage or step_usage
|
|
36
|
+
if usage is None:
|
|
37
|
+
return {"totalTokens": 0, "available": False}
|
|
38
|
+
return {
|
|
39
|
+
"totalTokens": usage.get("total_tokens", 0) or 0,
|
|
40
|
+
"inputTokens": usage.get("input_tokens", 0) or 0,
|
|
41
|
+
"outputTokens": usage.get("output_tokens", 0) or 0,
|
|
42
|
+
"thoughtsTokens": usage.get("thinking_tokens", 0) or 0,
|
|
43
|
+
"cacheReadTokens": usage.get("cache_read_tokens", 0) or 0,
|
|
44
|
+
"model": model,
|
|
45
|
+
"available": True,
|
|
46
|
+
}
|
|
22
47
|
|
|
23
48
|
|
|
24
49
|
def find_antigravity_session(project_root: Path, started_at: str, ended_at: str) -> Path | None:
|
|
25
|
-
|
|
50
|
+
sessions = find_antigravity_sessions(project_root, started_at, ended_at)
|
|
51
|
+
return sessions[-1] if sessions else None
|
|
26
52
|
|
|
27
53
|
|
|
28
54
|
def find_antigravity_sessions(project_root: Path, started_at: str, ended_at: str) -> list[Path]:
|
|
55
|
+
# 세션 파일은 홈 디렉터리가 아니라 워커 로그다. 경로는 호출부가 prompt 에서 붙인다.
|
|
29
56
|
return []
|