okstra 0.202.0 → 0.204.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli-registry.mjs +7 -7
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/lifecycle/install.mjs +50 -124
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/dist/commands/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 +25 -19
- package/docs/cli.md +15 -12
- package/docs/contributor-change-matrix.md +3 -2
- package/docs/performance-improvement-plan-v2.md +2 -3
- package/docs/project-structure-overview.md +35 -9
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/common-flow.md +1 -1
- package/docs/task-process/final-verification.md +3 -1
- package/docs/task-process/implementation.md +1 -1
- package/docs/task-process/release-handoff.md +36 -39
- package/package.json +1 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/common.json +28 -0
- package/runtime/agents/operations/code-review.json +6 -0
- package/runtime/agents/operations/report-translation.json +6 -0
- package/runtime/agents/operations/schedule-verification.json +6 -0
- package/runtime/agents/roles/analyser.json +18 -0
- package/runtime/agents/roles/critic.json +18 -0
- package/runtime/agents/roles/designer.json +18 -0
- package/runtime/agents/roles/implementer.json +20 -0
- package/runtime/agents/roles/leader.json +20 -0
- package/runtime/agents/roles/planner.json +18 -0
- package/runtime/agents/roles/report-writer.json +19 -0
- package/runtime/agents/roles/translator.json +19 -0
- package/runtime/agents/roles/verifier.json +18 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- package/runtime/prompts/duties/acceptance-critic.json +32 -0
- package/runtime/prompts/duties/acceptance-verifier.json +32 -0
- package/runtime/prompts/duties/analysis-worker.json +32 -0
- package/runtime/prompts/duties/code-reviewer.json +32 -0
- package/runtime/prompts/duties/diagnosis-worker.json +32 -0
- package/runtime/prompts/duties/direction-selection-worker.json +32 -0
- package/runtime/prompts/duties/discovery-worker.json +32 -0
- package/runtime/prompts/duties/implementation-executor.json +32 -0
- package/runtime/prompts/duties/implementation-verifier.json +32 -0
- package/runtime/prompts/duties/lead.json +32 -0
- package/runtime/prompts/duties/planning-worker.json +36 -0
- package/runtime/prompts/duties/report-writer.json +32 -0
- package/runtime/prompts/duties/reverification-worker.json +32 -0
- package/runtime/prompts/duties/schedule-verifier.json +32 -0
- package/runtime/prompts/duties/scope-critic.json +32 -0
- package/runtime/prompts/duties/technical-verification-worker.json +32 -0
- package/runtime/prompts/duties/translator.json +32 -0
- package/runtime/prompts/launch.template.md +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 +113 -4
- package/runtime/prompts/lead/plan-body-verification.md +6 -6
- package/runtime/prompts/lead/report-writer.md +3 -3
- package/runtime/prompts/profiles/_common-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +4 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +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 +3 -22
- package/runtime/prompts/profiles/forbidden-actions.json +4 -3
- package/runtime/prompts/profiles/implementation-option-selection.json +31 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +0 -20
- package/runtime/prompts/profiles/implementation-planning.json +40 -0
- package/runtime/prompts/profiles/implementation-planning.md +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 +71 -73
- package/runtime/prompts/profiles/requirements-discovery.json +39 -0
- package/runtime/prompts/profiles/requirements-discovery.md +0 -25
- package/runtime/prompts/profiles/technical-verification.json +39 -0
- package/runtime/prompts/profiles/technical-verification.md +0 -25
- package/runtime/prompts/wizard/prompts.ko.json +12 -17
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +3 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -3
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/worker-session.md +108 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +8 -1
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +8 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +23 -6
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +6 -2
- package/runtime/python/okstra_ctl/agent/invocation.py +168 -113
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +120 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +107 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -49
- package/runtime/python/okstra_ctl/analysis_packet.py +4 -1
- package/runtime/python/okstra_ctl/application/open_worker.py +6 -1
- package/runtime/python/okstra_ctl/assignment_resolver.py +16 -5
- package/runtime/python/okstra_ctl/cmux.py +69 -20
- package/runtime/python/okstra_ctl/code_review_target.py +16 -8
- package/runtime/python/okstra_ctl/conformance.py +43 -0
- package/runtime/python/okstra_ctl/consumers.py +6 -3
- package/runtime/python/okstra_ctl/container.py +31 -8
- package/runtime/python/okstra_ctl/context_cost.py +11 -15
- package/runtime/python/okstra_ctl/contract_refreeze.py +156 -0
- package/runtime/python/okstra_ctl/convergence_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 +167 -277
- package/runtime/python/okstra_ctl/implementation_stage.py +9 -0
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +113 -0
- package/runtime/python/okstra_ctl/lead_progress.py +1 -1
- package/runtime/python/okstra_ctl/legacy_model_selection.py +2 -2
- package/runtime/python/okstra_ctl/manager_cli.py +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 +50 -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 +3 -23
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +5 -1
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +35 -7
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +66 -48
- package/runtime/python/okstra_ctl/workflow.py +1 -1
- package/runtime/python/okstra_ctl/worktree/__init__.py +3 -1
- package/runtime/python/okstra_ctl/worktree/naming.py +9 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +38 -9
- package/runtime/python/okstra_token_usage/pricing.py +6 -4
- package/runtime/schemas/agent-common-v1.schema.json +34 -0
- package/runtime/schemas/agent-duty-v1.schema.json +38 -0
- package/runtime/schemas/agent-operation-v1.schema.json +11 -0
- package/runtime/schemas/agent-profile-v1.schema.json +46 -0
- package/runtime/schemas/agent-role-v1.schema.json +29 -0
- package/runtime/schemas/final-report-v2.0.schema.json +118 -97
- package/runtime/schemas/final-report-v3.0.schema.json +118 -97
- package/runtime/skills/okstra-brief-gen/SKILL.md +84 -4
- package/runtime/skills/okstra-chat/SKILL.md +2 -2
- package/runtime/skills/okstra-code-review/SKILL.md +23 -9
- package/runtime/skills/okstra-container-build/SKILL.md +10 -10
- package/runtime/skills/okstra-inspect/SKILL.md +1 -1
- package/runtime/skills/okstra-inspect/facets/cost.md +1 -1
- package/runtime/skills/okstra-inspect/facets/error-zip.md +9 -9
- package/runtime/skills/okstra-inspect/facets/errors.md +16 -16
- package/runtime/skills/okstra-inspect/facets/logs.md +7 -7
- package/runtime/skills/okstra-inspect/facets/recap.md +2 -2
- package/runtime/skills/okstra-inspect/facets/report.md +1 -1
- package/runtime/skills/okstra-inspect/facets/status.md +4 -3
- package/runtime/skills/okstra-inspect/facets/time.md +11 -10
- package/runtime/skills/okstra-manager/SKILL.md +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 +59 -9
- package/runtime/validators/validate-schedule.py +9 -0
- package/docs/for-ai/README.md +0 -68
- package/docs/for-ai/skills/okstra-brief-gen.md +0 -262
- package/docs/for-ai/skills/okstra-chat.md +0 -34
- package/docs/for-ai/skills/okstra-code-review.md +0 -57
- package/docs/for-ai/skills/okstra-container-build.md +0 -129
- package/docs/for-ai/skills/okstra-inspect.md +0 -262
- package/docs/for-ai/skills/okstra-manager.md +0 -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
|
@@ -131,6 +131,8 @@ _REQUIRED_PROMPT_RESOURCES_HEADING = "## Required prompt resources"
|
|
|
131
131
|
_HTML_COMMENTS = re.compile(r"<!--.*?-->\n?", re.DOTALL)
|
|
132
132
|
_AGENT_CONTRACT_KEYS = {
|
|
133
133
|
"schemaVersion",
|
|
134
|
+
# 이 run 이 동결한 계약 형식. 없는 run 은 Markdown 직무를 동결했다.
|
|
135
|
+
"contractFormatVersion",
|
|
134
136
|
"dutyRootPath",
|
|
135
137
|
"catalogDigest",
|
|
136
138
|
"invocationReservationRootPath",
|
|
@@ -214,6 +216,117 @@ def materialize_initial_prompts(
|
|
|
214
216
|
) from exc
|
|
215
217
|
|
|
216
218
|
|
|
219
|
+
def declared_delivery_mode(manifest: Mapping[str, Any]) -> PromptDeliveryMode:
|
|
220
|
+
"""이 run 의 어댑터가 선언한 초기 프롬프트 배송 방식.
|
|
221
|
+
|
|
222
|
+
선언은 `leadAdapter.initialPromptDeliveryMode` 에 있고, 그 값은 render 가
|
|
223
|
+
호스트 descriptor(어댑터가 워커 디스패치를 가져갔으면 그 어댑터)에서
|
|
224
|
+
옮겨 적은 것이다. 선언이 없는 매니페스트는 이 필드가 생기기 전에 준비된
|
|
225
|
+
run 이므로 그때의 동작인 `eager-include` 로 간다. 아는 값이 아니면 멈춘다 —
|
|
226
|
+
프롬프트를 발행하고 나면 그 프롬프트는 불변이라, 잘못 고른 배송 방식은
|
|
227
|
+
나중에 고칠 수 없다.
|
|
228
|
+
"""
|
|
229
|
+
adapter = manifest.get("leadAdapter")
|
|
230
|
+
declared = (
|
|
231
|
+
_string_value(adapter.get("initialPromptDeliveryMode"))
|
|
232
|
+
if isinstance(adapter, Mapping)
|
|
233
|
+
else ""
|
|
234
|
+
)
|
|
235
|
+
if not declared:
|
|
236
|
+
return PromptDeliveryMode.EAGER_INCLUDE
|
|
237
|
+
try:
|
|
238
|
+
return PromptDeliveryMode(declared)
|
|
239
|
+
except ValueError:
|
|
240
|
+
allowed = ", ".join(mode.value for mode in PromptDeliveryMode)
|
|
241
|
+
raise InitialPromptMaterializationError(
|
|
242
|
+
"required_input_missing",
|
|
243
|
+
"leadAdapter.initialPromptDeliveryMode is not a delivery mode: "
|
|
244
|
+
f"{declared!r} (allowed: {allowed})",
|
|
245
|
+
) from None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _read_side_context(
|
|
249
|
+
project_root: Path, run_manifest_path: Path, runtime_root: Path,
|
|
250
|
+
) -> "_MaterializationContext":
|
|
251
|
+
"""프롬프트를 쓰지 않는 호출자를 위한 컨텍스트.
|
|
252
|
+
|
|
253
|
+
읽는 것은 매니페스트·active run context·런타임 루트뿐이다. 발행 권한 검사와
|
|
254
|
+
team-state 는 프롬프트를 쓰는 쪽(호출한 CLI)의 몫이다.
|
|
255
|
+
"""
|
|
256
|
+
root = project_root.resolve()
|
|
257
|
+
manifest = _load_json_object(
|
|
258
|
+
_resolve_input_path(root, run_manifest_path), "run manifest"
|
|
259
|
+
)
|
|
260
|
+
return _MaterializationContext(
|
|
261
|
+
request=InitialPromptMaterializationRequest(
|
|
262
|
+
project_root=root,
|
|
263
|
+
run_manifest_path=run_manifest_path,
|
|
264
|
+
runtime_root=runtime_root,
|
|
265
|
+
delivery_mode=declared_delivery_mode(manifest),
|
|
266
|
+
workers=(),
|
|
267
|
+
),
|
|
268
|
+
project_root=root,
|
|
269
|
+
manifest=manifest,
|
|
270
|
+
team_state={},
|
|
271
|
+
active_context=hydrate_active_run_context(
|
|
272
|
+
_load_referenced_json(root, manifest, "activeRunContextPath")
|
|
273
|
+
),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def implementation_anchor_lines(
|
|
278
|
+
*,
|
|
279
|
+
project_root: Path,
|
|
280
|
+
run_manifest_path: Path,
|
|
281
|
+
runtime_root: Path,
|
|
282
|
+
worker_id: str,
|
|
283
|
+
) -> list[str]:
|
|
284
|
+
"""이 워커의 프롬프트가 싣는 구현 앵커 줄. 구현 audience 가 아니면 빈 목록.
|
|
285
|
+
|
|
286
|
+
executor 의 mutating cwd, 그리고 executor·verifier 공통의
|
|
287
|
+
`**Approved plan:**` / `**Stage for this implementation run:**` 이다.
|
|
288
|
+
`okstra agent-prompt jobs` 는 이 두 헤더를 요구하므로, 리드가 회수 경로로
|
|
289
|
+
발행한 프롬프트는 그 검사에서 거절됐다(실측 2026-09-23, fontradar-v2-api
|
|
290
|
+
dev-10860 stage-3: 리드가 헤더 두 줄을 손으로 지시문에 붙여 통과시켰다).
|
|
291
|
+
"""
|
|
292
|
+
context = _read_side_context(project_root, run_manifest_path, runtime_root)
|
|
293
|
+
return _implementation_anchor_lines(
|
|
294
|
+
context, _prompt_plan(context.manifest, worker_id)
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def required_resource_block(
|
|
299
|
+
*,
|
|
300
|
+
project_root: Path,
|
|
301
|
+
run_manifest_path: Path,
|
|
302
|
+
runtime_root: Path,
|
|
303
|
+
worker_id: str,
|
|
304
|
+
) -> str:
|
|
305
|
+
"""이 워커의 프롬프트가 싣는 필수 자원 블록. 없으면 빈 문자열.
|
|
306
|
+
|
|
307
|
+
디스패치 시점의 composer 와 같은 블록이다 — executor 는 BLOCKING 게이트 세
|
|
308
|
+
본문과 stage discipline, verifier 는 `_implementation-verifier.md`, 둘 다
|
|
309
|
+
clarification 권한 블록과 fix-run carry. 리드가 `okstra agent-prompt
|
|
310
|
+
materialize` 로 먼저 발행한 프롬프트는 디스패치가 기존 파일로 그대로 쓰므로
|
|
311
|
+
composer 가 돌지 않았고, 이 블록이 통째로 빠졌다(실측 2026-09-22,
|
|
312
|
+
fontradar-v2-api dev-10860 implementation stage-1: executor·verifier 4개
|
|
313
|
+
프롬프트 모두 0건). 그 명령이 이 함수로 같은 블록을 붙인다.
|
|
314
|
+
"""
|
|
315
|
+
context = _read_side_context(project_root, run_manifest_path, runtime_root)
|
|
316
|
+
plan = _prompt_plan(context.manifest, worker_id)
|
|
317
|
+
item = _PromptItem(
|
|
318
|
+
worker=InitialPromptWorkerRequest(worker_id, ""),
|
|
319
|
+
final_path=Path(),
|
|
320
|
+
plan=plan,
|
|
321
|
+
existed=False,
|
|
322
|
+
clarification_input=_resolve_clarification_input(context, plan),
|
|
323
|
+
)
|
|
324
|
+
item = _with_prompt_resources(
|
|
325
|
+
item, _resolve_prompt_resources(context, plan, existed=False)
|
|
326
|
+
)
|
|
327
|
+
return "\n".join(_resource_lines(context, item)).strip()
|
|
328
|
+
|
|
329
|
+
|
|
217
330
|
def _materialize_initial_prompts(
|
|
218
331
|
request: InitialPromptMaterializationRequest,
|
|
219
332
|
) -> dict[str, Path]:
|
|
@@ -243,7 +243,7 @@ def _append(args: argparse.Namespace) -> int:
|
|
|
243
243
|
|
|
244
244
|
_CLI_EPILOG = r"""Usage:
|
|
245
245
|
okstra lead-progress append --project-root <dir> --run-manifest <path> --phase phase-1-intake
|
|
246
|
-
okstra lead-progress append --project-root <dir> --run-manifest <path> --phase phase-4-dispatch --worker codex-worker --field model=gpt-
|
|
246
|
+
okstra lead-progress append --project-root <dir> --run-manifest <path> --phase phase-4-dispatch --worker codex-worker --field model=gpt-6-sol
|
|
247
247
|
okstra lead-progress append --project-root <dir> --run-manifest <path> --phase phase-5-collect --worker codex-worker --field status=completed
|
|
248
248
|
|
|
249
249
|
`progressLine` in the output is the line to emit to the user, verbatim.
|
|
@@ -11,7 +11,7 @@ from .domain.host import (
|
|
|
11
11
|
HostSessionContext,
|
|
12
12
|
)
|
|
13
13
|
from .domain.role import RoleCatalogError, normalize_role
|
|
14
|
-
from .role_requirements import RoleProfile, RoleRequirement
|
|
14
|
+
from .role_requirements import RoleProfile, RoleRequirement, StaticRoleRequirement
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class ModelSelectionInputError(ValueError):
|
|
@@ -404,7 +404,7 @@ def _validate_selection(
|
|
|
404
404
|
explicit_counts: Mapping[str, int],
|
|
405
405
|
) -> None:
|
|
406
406
|
requirements = {row.role: row for row in profile.roles if not row.dynamic}
|
|
407
|
-
requirements["leader"] =
|
|
407
|
+
requirements["leader"] = StaticRoleRequirement("leader", 1, 1, 1, "lead")
|
|
408
408
|
for role in sorted(set(counts) | set(models)):
|
|
409
409
|
requirement = requirements.get(role)
|
|
410
410
|
if requirement is None:
|
|
@@ -22,7 +22,13 @@ from .manager_store import (
|
|
|
22
22
|
link_project,
|
|
23
23
|
list_managers,
|
|
24
24
|
list_projects,
|
|
25
|
+
list_task_groups,
|
|
25
26
|
list_tasks,
|
|
27
|
+
remove_child,
|
|
28
|
+
remove_task,
|
|
29
|
+
remove_task_group,
|
|
30
|
+
unlink_project,
|
|
31
|
+
update_task,
|
|
26
32
|
)
|
|
27
33
|
from .manager_split import split_task
|
|
28
34
|
from .manager_sync import status_task, sync_task
|
|
@@ -37,6 +43,8 @@ _LIST_ROWS = {
|
|
|
37
43
|
("project count", "projectCount"), ("task count", "taskCount"))),
|
|
38
44
|
"list-projects": ("Project", (("ID", "projectId"), ("root", "projectRoot"), ("role", "role"),
|
|
39
45
|
("tags", "tags"), ("linked at", "linkedAt"))),
|
|
46
|
+
"list-task-groups": ("Task group", (("name", "taskGroup"), ("created at", "createdAt"),
|
|
47
|
+
("task count", "taskCount"))),
|
|
40
48
|
"list-tasks": ("Task", (("group", "taskGroup"), ("ID", "taskId"), ("objective", "objective"),
|
|
41
49
|
("progress mode", "progressMode"), ("child count", "childCount"),
|
|
42
50
|
("created at", "createdAt"))),
|
|
@@ -47,7 +55,8 @@ def render_manager_text(command: str, payload: dict | list[dict]) -> str:
|
|
|
47
55
|
"""manager 모델 표면에 승인된 식별자와 상태만 투영한다."""
|
|
48
56
|
allowed = {"init", "discover-projects", "new-project", "new-task-group", "new-task",
|
|
49
57
|
"task-assign", "task-note", "task-sync", "task-status", "task-run", "task-split",
|
|
50
|
-
"list-managers", "list-projects", "list-tasks", "view"
|
|
58
|
+
"list-managers", "list-projects", "list-task-groups", "list-tasks", "view",
|
|
59
|
+
"task-update", "task-remove-child", "remove-project", "remove-task", "remove-task-group"}
|
|
51
60
|
if command not in allowed:
|
|
52
61
|
raise ValueError("unknown manager text purpose")
|
|
53
62
|
rows = [f"Okstra manager {command}\n", line("Status", "ready")]
|
|
@@ -94,11 +103,30 @@ def render_manager_text(command: str, payload: dict | list[dict]) -> str:
|
|
|
94
103
|
for label, key in (("task key", "taskKey"), ("project ID", "projectId"), ("ticket", "ticketId"),
|
|
95
104
|
("path", "briefPath"), ("status", "briefStatus")):
|
|
96
105
|
rows.append(line(f"Brief {index} {label}", brief.get(key)))
|
|
106
|
+
if command == "task-update":
|
|
107
|
+
rows.append(line("Changed fields", ", ".join(payload.get("changedFields") or []) or "none"))
|
|
108
|
+
rows.extend(value_lines("Manifest", payload.get("manifest")))
|
|
109
|
+
if command.startswith("remove-") or command == "task-remove-child":
|
|
110
|
+
rows.extend(_render_removal(payload))
|
|
97
111
|
if command == "task-sync" and "syncedAt" in payload:
|
|
98
112
|
rows.append(line("Synced at", payload.get("syncedAt")))
|
|
99
113
|
return "".join(rows)
|
|
100
114
|
|
|
101
115
|
|
|
116
|
+
# 삭제 명령은 --confirm 이 없으면 대상만 보여 준다. 무엇이 지워지고 무엇이 남는지 한 줄씩 찍는다.
|
|
117
|
+
def _render_removal(payload: dict) -> list[str]:
|
|
118
|
+
rows = [line("Removed", "yes" if payload.get("removed") else "no — dry run; rerun with --confirm")]
|
|
119
|
+
for label, key in (("Removed path", "path"), ("Child count", "childCount")):
|
|
120
|
+
if key in payload:
|
|
121
|
+
rows.append(line(label, payload.get(key)))
|
|
122
|
+
for label, key in (("Kept child", "keptChildren"), ("Removed task", "taskIds")):
|
|
123
|
+
values = payload.get(key)
|
|
124
|
+
if isinstance(values, list):
|
|
125
|
+
rows.append(line(f"{label} count", len(values)))
|
|
126
|
+
rows.extend(line(f"{label} {index}", value) for index, value in enumerate(values, 1))
|
|
127
|
+
return rows
|
|
128
|
+
|
|
129
|
+
|
|
102
130
|
def _render_list(command: str, items: list[dict]) -> list[str]:
|
|
103
131
|
prefix, fields = _LIST_ROWS[command]
|
|
104
132
|
rows: list[str] = []
|
|
@@ -126,10 +154,12 @@ def _render_manager_status(payload: dict) -> list[str]:
|
|
|
126
154
|
continue
|
|
127
155
|
for label, key in (("task key", "taskKey"), ("run status", "latestRunStatus"),
|
|
128
156
|
("current phase", "currentPhase"), ("work status", "workStatus"),
|
|
129
|
-
("summary", "summaryBucket"), ("
|
|
157
|
+
("summary", "summaryBucket"), ("project linked", "projectLinked"),
|
|
158
|
+
("ticket", "ticketId"), ("brief path", "briefPath"),
|
|
130
159
|
("report path", "latestReportRecordPath"), ("error", "error")):
|
|
131
160
|
if key in child:
|
|
132
|
-
|
|
161
|
+
value = child.get(key)
|
|
162
|
+
rows.append(line(f"Child {index} {label}", ("yes" if value else "no") if isinstance(value, bool) else value))
|
|
133
163
|
return rows
|
|
134
164
|
|
|
135
165
|
|
|
@@ -188,11 +218,18 @@ _CLI_EPILOG = r"""Usage:
|
|
|
188
218
|
okstra manager task note --manager-id <id> --task-group <task-group> --task-id <task-id> --scope <shared|project> --body <text>
|
|
189
219
|
okstra manager task run --manager-id <id> --project-id <project-id> --task-group <task-group> --task-id <task-id>
|
|
190
220
|
okstra manager task split --manager-id <id> --task-group <task-group> --task-id <task-id> --plan <split-plan.json> [--overwrite]
|
|
221
|
+
okstra manager task update --manager-id <id> --task-group <task-group> --task-id <task-id> [--objective <text>] [--common-brief <path>] [--progress-mode <manual|auto>]
|
|
222
|
+
okstra manager task remove-child --manager-id <id> --task-group <task-group> --task-id <task-id> --project-id <project-id> [--child-task-id <id>] [--confirm]
|
|
223
|
+
okstra manager remove project --manager-id <id> --project-id <project-id> [--confirm]
|
|
224
|
+
okstra manager remove task --manager-id <id> --task-group <task-group> --task-id <task-id> [--confirm]
|
|
225
|
+
okstra manager remove task-group --manager-id <id> --task-group <task-group> [--confirm]
|
|
191
226
|
okstra manager list managers
|
|
192
227
|
okstra manager list projects --manager-id <id>
|
|
228
|
+
okstra manager list task-groups --manager-id <id>
|
|
193
229
|
okstra manager list tasks --manager-id <id>
|
|
194
230
|
okstra manager view --manager-id <id>
|
|
195
231
|
|
|
232
|
+
remove and task remove-child only show their targets unless --confirm is given.
|
|
196
233
|
--workspace-root is owned by this command.
|
|
197
234
|
"""
|
|
198
235
|
_CLI_DESCRIPTION = "Manage cross-project okstra tasks."
|
|
@@ -277,10 +314,42 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
277
314
|
split.add_argument("--plan", required=True)
|
|
278
315
|
split.add_argument("--overwrite", action="store_true")
|
|
279
316
|
|
|
317
|
+
update = task_sub.add_parser("update")
|
|
318
|
+
update.add_argument("--manager-id", required=True)
|
|
319
|
+
update.add_argument("--task-group", required=True)
|
|
320
|
+
update.add_argument("--task-id", required=True)
|
|
321
|
+
update.add_argument("--objective", default=None)
|
|
322
|
+
update.add_argument("--common-brief", default=None)
|
|
323
|
+
update.add_argument("--progress-mode", choices=["manual", "auto"], default=None)
|
|
324
|
+
|
|
325
|
+
remove_child_cmd = task_sub.add_parser("remove-child")
|
|
326
|
+
remove_child_cmd.add_argument("--manager-id", required=True)
|
|
327
|
+
remove_child_cmd.add_argument("--task-group", required=True)
|
|
328
|
+
remove_child_cmd.add_argument("--task-id", required=True)
|
|
329
|
+
remove_child_cmd.add_argument("--project-id", required=True)
|
|
330
|
+
remove_child_cmd.add_argument("--child-task-id", default=None)
|
|
331
|
+
remove_child_cmd.add_argument("--confirm", action="store_true")
|
|
332
|
+
|
|
333
|
+
remove = sub.add_parser("remove")
|
|
334
|
+
remove_sub = remove.add_subparsers(dest="remove_command", required=True)
|
|
335
|
+
remove_project = remove_sub.add_parser("project")
|
|
336
|
+
remove_project.add_argument("--manager-id", required=True)
|
|
337
|
+
remove_project.add_argument("--project-id", required=True)
|
|
338
|
+
remove_project.add_argument("--confirm", action="store_true")
|
|
339
|
+
remove_task_cmd = remove_sub.add_parser("task")
|
|
340
|
+
remove_task_cmd.add_argument("--manager-id", required=True)
|
|
341
|
+
remove_task_cmd.add_argument("--task-group", required=True)
|
|
342
|
+
remove_task_cmd.add_argument("--task-id", required=True)
|
|
343
|
+
remove_task_cmd.add_argument("--confirm", action="store_true")
|
|
344
|
+
remove_group = remove_sub.add_parser("task-group")
|
|
345
|
+
remove_group.add_argument("--manager-id", required=True)
|
|
346
|
+
remove_group.add_argument("--task-group", required=True)
|
|
347
|
+
remove_group.add_argument("--confirm", action="store_true")
|
|
348
|
+
|
|
280
349
|
listing = sub.add_parser("list")
|
|
281
350
|
list_sub = listing.add_subparsers(dest="list_command", required=True)
|
|
282
351
|
list_sub.add_parser("managers")
|
|
283
|
-
for name in ("projects", "tasks"):
|
|
352
|
+
for name in ("projects", "task-groups", "tasks"):
|
|
284
353
|
list_sub.add_parser(name).add_argument("--manager-id", required=True)
|
|
285
354
|
|
|
286
355
|
view = sub.add_parser("view")
|
|
@@ -391,6 +460,25 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
391
460
|
_emit("list-managers", list_managers(home), args.json)
|
|
392
461
|
elif args.command == "list" and args.list_command == "projects":
|
|
393
462
|
_emit("list-projects", list_projects(home, args.manager_id), args.json)
|
|
463
|
+
elif args.command == "task" and args.task_command == "update":
|
|
464
|
+
_emit("task-update", update_task(home, args.manager_id, args.task_group, args.task_id,
|
|
465
|
+
objective=args.objective, common_brief_path=args.common_brief,
|
|
466
|
+
progress_mode=args.progress_mode), args.json)
|
|
467
|
+
elif args.command == "task" and args.task_command == "remove-child":
|
|
468
|
+
_emit("task-remove-child", remove_child(home, args.manager_id, args.task_group, args.task_id,
|
|
469
|
+
args.project_id, child_task_id=args.child_task_id,
|
|
470
|
+
confirm=args.confirm), args.json)
|
|
471
|
+
elif args.command == "remove" and args.remove_command == "project":
|
|
472
|
+
_emit("remove-project", unlink_project(home, args.manager_id, args.project_id,
|
|
473
|
+
confirm=args.confirm), args.json)
|
|
474
|
+
elif args.command == "remove" and args.remove_command == "task":
|
|
475
|
+
_emit("remove-task", remove_task(home, args.manager_id, args.task_group, args.task_id,
|
|
476
|
+
confirm=args.confirm), args.json)
|
|
477
|
+
elif args.command == "remove" and args.remove_command == "task-group":
|
|
478
|
+
_emit("remove-task-group", remove_task_group(home, args.manager_id, args.task_group,
|
|
479
|
+
confirm=args.confirm), args.json)
|
|
480
|
+
elif args.command == "list" and args.list_command == "task-groups":
|
|
481
|
+
_emit("list-task-groups", list_task_groups(home, args.manager_id), args.json)
|
|
394
482
|
elif args.command == "list" and args.list_command == "tasks":
|
|
395
483
|
_emit("list-tasks", list_tasks(home, args.manager_id), args.json)
|
|
396
484
|
elif args.command == "view":
|
|
@@ -33,7 +33,7 @@ def _find_project(projects: dict, project_id: str) -> dict:
|
|
|
33
33
|
for project in projects.get("projects", []):
|
|
34
34
|
if isinstance(project, dict) and project.get("projectId") == project_id:
|
|
35
35
|
return project
|
|
36
|
-
raise ManagerError(f"
|
|
36
|
+
raise ManagerError(f"{project_id}: project unlinked from this manager; re-register it with `okstra manager new project`")
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def _target_task_key(project_id: str, task_group: str, task_id: str) -> str:
|
|
@@ -54,10 +54,21 @@ def manager_root(home: Path, manager_id: str) -> Path:
|
|
|
54
54
|
return Path(home) / "managers" / canonical_manager_id(manager_id)
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
def task_groups_root(home: Path, manager_id: str) -> Path:
|
|
58
|
+
return manager_root(home, manager_id) / "task-groups"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def task_group_root(home: Path, manager_id: str, task_group: str) -> Path:
|
|
62
|
+
return task_groups_root(home, manager_id) / _slug_segment(task_group, "task-group")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def task_group_meta_path(home: Path, manager_id: str, task_group: str) -> Path:
|
|
66
|
+
"""폴더 이름은 slug 라 원래 그룹 이름을 여기에 남긴다."""
|
|
67
|
+
return task_group_root(home, manager_id, task_group) / "group.json"
|
|
68
|
+
|
|
69
|
+
|
|
57
70
|
def task_root(home: Path, manager_id: str, task_group: str, task_id: str) -> Path:
|
|
58
|
-
|
|
59
|
-
task_segment = _slug_segment(task_id, "task-id")
|
|
60
|
-
return manager_root(home, manager_id) / "task-groups" / group_segment / task_segment
|
|
71
|
+
return task_group_root(home, manager_id, task_group) / _slug_segment(task_id, "task-id")
|
|
61
72
|
|
|
62
73
|
|
|
63
74
|
def manager_json_path(home: Path, manager_id: str) -> Path:
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
+
import shutil
|
|
6
7
|
from datetime import datetime, timezone
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
@@ -18,12 +19,17 @@ from okstra_ctl.json_boundary import (
|
|
|
18
19
|
from .manager_paths import (
|
|
19
20
|
ManagerPathError,
|
|
20
21
|
canonical_manager_id,
|
|
22
|
+
child_context_path,
|
|
21
23
|
children_json_path,
|
|
22
24
|
directives_jsonl_path,
|
|
23
25
|
events_jsonl_path,
|
|
24
26
|
manager_json_path,
|
|
25
27
|
manager_root,
|
|
26
28
|
projects_json_path,
|
|
29
|
+
snapshots_json_path,
|
|
30
|
+
task_group_meta_path,
|
|
31
|
+
task_group_root,
|
|
32
|
+
task_groups_root,
|
|
27
33
|
task_manifest_path,
|
|
28
34
|
)
|
|
29
35
|
|
|
@@ -224,11 +230,20 @@ def link_project(
|
|
|
224
230
|
return payload
|
|
225
231
|
|
|
226
232
|
|
|
233
|
+
def _ensure_group_meta(home: Path, manager_id: str, task_group: str, when: str) -> dict:
|
|
234
|
+
"""그룹 폴더와 `group.json` 을 만든다. 이미 있으면 처음 기록을 그대로 둔다."""
|
|
235
|
+
path = task_group_meta_path(home, manager_id, task_group)
|
|
236
|
+
if path.is_file():
|
|
237
|
+
return _read_json(path)
|
|
238
|
+
meta = {"taskGroup": task_group, "createdAt": when}
|
|
239
|
+
_write_json(path, meta)
|
|
240
|
+
return meta
|
|
241
|
+
|
|
242
|
+
|
|
227
243
|
def create_task_group(home: Path, manager_id: str, task_group: str, *, now: str | None = None) -> dict:
|
|
228
244
|
load_manager(home, manager_id)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return {"managerId": manager_id, "taskGroup": task_group, "createdAt": now or _now_iso()}
|
|
245
|
+
meta = _ensure_group_meta(home, manager_id, task_group, now or _now_iso())
|
|
246
|
+
return {"managerId": manager_id, **meta}
|
|
232
247
|
|
|
233
248
|
|
|
234
249
|
def _merge_manifest(existing: dict, requested: dict) -> None:
|
|
@@ -313,6 +328,7 @@ def create_task(
|
|
|
313
328
|
)
|
|
314
329
|
children["children"].extend(added)
|
|
315
330
|
if not existing:
|
|
331
|
+
_ensure_group_meta(home, manager_id, task_group, when)
|
|
316
332
|
_write_json(manifest_path, manifest)
|
|
317
333
|
_write_json(children_json_path(home, manager_id, task_group, task_id), children)
|
|
318
334
|
append_jsonl(
|
|
@@ -423,3 +439,194 @@ def append_directive(
|
|
|
423
439
|
}
|
|
424
440
|
append_jsonl(directives_jsonl_path(home, manager_id, task_group, task_id), row)
|
|
425
441
|
return row
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def list_task_groups(home: Path, manager_id: str) -> list[dict]:
|
|
445
|
+
"""태스크가 없는 그룹까지 포함해 그룹을 이름 순으로 돌려준다.
|
|
446
|
+
|
|
447
|
+
`group.json` 이 없는 기존 그룹은 소속 태스크 manifest 의 그룹 이름을, 태스크도 없으면
|
|
448
|
+
폴더 이름을 쓴다.
|
|
449
|
+
"""
|
|
450
|
+
load_manager(home, manager_id)
|
|
451
|
+
base = task_groups_root(home, manager_id)
|
|
452
|
+
if not base.is_dir():
|
|
453
|
+
return []
|
|
454
|
+
items: list[dict] = []
|
|
455
|
+
for group_dir in sorted(path for path in base.iterdir() if path.is_dir()):
|
|
456
|
+
manifests = sorted(group_dir.glob("*/manifest.json"))
|
|
457
|
+
meta_path = group_dir / "group.json"
|
|
458
|
+
meta = _read_json(meta_path) if meta_path.is_file() else {}
|
|
459
|
+
name = str(meta.get("taskGroup") or "")
|
|
460
|
+
if not name and manifests:
|
|
461
|
+
name = str(_read_json(manifests[0]).get("taskGroup") or "")
|
|
462
|
+
items.append(
|
|
463
|
+
{
|
|
464
|
+
"taskGroup": name or group_dir.name,
|
|
465
|
+
"createdAt": str(meta.get("createdAt") or ""),
|
|
466
|
+
"taskCount": len(manifests),
|
|
467
|
+
}
|
|
468
|
+
)
|
|
469
|
+
return sorted(items, key=lambda item: item["taskGroup"])
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _children_of_project(home: Path, manager_id: str, project_id: str) -> list[tuple[dict, list[str]]]:
|
|
473
|
+
"""(task manifest, 그 task 에서 project_id 를 가리키는 하위 태스크 키) 목록."""
|
|
474
|
+
rows = []
|
|
475
|
+
for path in _task_manifest_paths(home, manager_id):
|
|
476
|
+
children = _read_json_default(path.parent / "children.json", {"children": []})
|
|
477
|
+
keys = [
|
|
478
|
+
str(child.get("taskKey"))
|
|
479
|
+
for child in children.get("children", [])
|
|
480
|
+
if isinstance(child, dict) and child.get("projectId") == project_id
|
|
481
|
+
]
|
|
482
|
+
if keys:
|
|
483
|
+
rows.append((_read_json(path), keys))
|
|
484
|
+
return rows
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def unlink_project(
|
|
488
|
+
home: Path, manager_id: str, project_id: str, *, confirm: bool, now: str | None = None
|
|
489
|
+
) -> dict:
|
|
490
|
+
"""프로젝트 등록을 해제한다. 하위 태스크는 남기고, 등록 목록에 없다는 사실로 해제 상태를 표시한다.
|
|
491
|
+
|
|
492
|
+
`confirm` 이 거짓이면 대상만 돌려주고 아무것도 바꾸지 않는다.
|
|
493
|
+
"""
|
|
494
|
+
load_manager(home, manager_id)
|
|
495
|
+
payload = _load_projects(home, manager_id)
|
|
496
|
+
entry = _project_entry(payload, project_id)
|
|
497
|
+
if entry is None:
|
|
498
|
+
raise ManagerError(f"unknown project membership: {project_id}")
|
|
499
|
+
affected = _children_of_project(home, manager_id, project_id)
|
|
500
|
+
result = {
|
|
501
|
+
"managerId": manager_id,
|
|
502
|
+
"projectId": project_id,
|
|
503
|
+
"projectRoot": str(entry.get("projectRoot") or ""),
|
|
504
|
+
"keptChildren": [key for _, keys in affected for key in keys],
|
|
505
|
+
"removed": confirm,
|
|
506
|
+
}
|
|
507
|
+
if not confirm:
|
|
508
|
+
return result
|
|
509
|
+
payload["projects"] = [item for item in payload["projects"] if item is not entry]
|
|
510
|
+
_write_json(projects_json_path(home, manager_id), payload)
|
|
511
|
+
when = now or _now_iso()
|
|
512
|
+
for manifest, keys in affected:
|
|
513
|
+
append_jsonl(
|
|
514
|
+
events_jsonl_path(home, manager_id, str(manifest["taskGroup"]), str(manifest["taskId"])),
|
|
515
|
+
{"event": "project-unlinked", "createdAt": when, "projectId": project_id, "taskKeys": keys},
|
|
516
|
+
)
|
|
517
|
+
return result
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def remove_child(
|
|
521
|
+
home: Path,
|
|
522
|
+
manager_id: str,
|
|
523
|
+
task_group: str,
|
|
524
|
+
task_id: str,
|
|
525
|
+
project_id: str,
|
|
526
|
+
*,
|
|
527
|
+
child_task_id: str | None = None,
|
|
528
|
+
confirm: bool,
|
|
529
|
+
now: str | None = None,
|
|
530
|
+
) -> dict:
|
|
531
|
+
"""하위 태스크 하나를 manager 에서 뺀다. 프로젝트의 `.okstra/` 는 건드리지 않는다."""
|
|
532
|
+
_read_json(task_manifest_path(home, manager_id, task_group, task_id))
|
|
533
|
+
payload = _load_children(home, manager_id, task_group, task_id)
|
|
534
|
+
child = _select_child_for_assignment(payload["children"], project_id, task_group, child_task_id)
|
|
535
|
+
task_key = str(child.get("taskKey") or "")
|
|
536
|
+
result = {"managerId": manager_id, "taskGroup": task_group, "taskId": task_id,
|
|
537
|
+
"taskKey": task_key, "removed": confirm}
|
|
538
|
+
if not confirm:
|
|
539
|
+
return result
|
|
540
|
+
payload["children"] = [item for item in payload["children"] if item is not child]
|
|
541
|
+
_write_json(children_json_path(home, manager_id, task_group, task_id), payload)
|
|
542
|
+
# 남은 형제의 context 에 지운 하위 태스크가 형제로 실리지 않게 스냅샷 행도 뺀다.
|
|
543
|
+
snapshot_path = snapshots_json_path(home, manager_id, task_group, task_id)
|
|
544
|
+
if snapshot_path.is_file():
|
|
545
|
+
snapshot = _read_json(snapshot_path)
|
|
546
|
+
snapshot["children"] = [
|
|
547
|
+
row for row in snapshot.get("children", []) if not isinstance(row, dict) or row.get("taskKey") != task_key
|
|
548
|
+
]
|
|
549
|
+
_write_json(snapshot_path, snapshot)
|
|
550
|
+
child_context_path(
|
|
551
|
+
home, manager_id, task_group, task_id, project_id, str(child.get("taskId") or task_id)
|
|
552
|
+
).unlink(missing_ok=True)
|
|
553
|
+
append_jsonl(
|
|
554
|
+
events_jsonl_path(home, manager_id, task_group, task_id),
|
|
555
|
+
{"event": "task-child-removed", "createdAt": now or _now_iso(), "taskKey": task_key},
|
|
556
|
+
)
|
|
557
|
+
return result
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def update_task(
|
|
561
|
+
home: Path,
|
|
562
|
+
manager_id: str,
|
|
563
|
+
task_group: str,
|
|
564
|
+
task_id: str,
|
|
565
|
+
*,
|
|
566
|
+
objective: str | None = None,
|
|
567
|
+
common_brief_path: str | None = None,
|
|
568
|
+
progress_mode: str | None = None,
|
|
569
|
+
now: str | None = None,
|
|
570
|
+
) -> dict:
|
|
571
|
+
"""manager task 의 objective·공통 브리프·진행 방식 중 넘긴 값만 바꾼다."""
|
|
572
|
+
manifest_path = task_manifest_path(home, manager_id, task_group, task_id)
|
|
573
|
+
manifest = _read_json(manifest_path)
|
|
574
|
+
requested = {"objective": objective, "commonBriefPath": common_brief_path, "progressMode": progress_mode}
|
|
575
|
+
requested = {key: value for key, value in requested.items() if value is not None}
|
|
576
|
+
if not requested:
|
|
577
|
+
raise ManagerError("task update needs at least one of --objective, --common-brief, --progress-mode")
|
|
578
|
+
if requested.get("progressMode") not in (None, "manual", "auto"):
|
|
579
|
+
raise ManagerError("progress-mode must be manual or auto")
|
|
580
|
+
changes = {
|
|
581
|
+
key: {"from": manifest.get(key), "to": value}
|
|
582
|
+
for key, value in requested.items()
|
|
583
|
+
if manifest.get(key) != value
|
|
584
|
+
}
|
|
585
|
+
for key, change in changes.items():
|
|
586
|
+
manifest[key] = change["to"]
|
|
587
|
+
if changes:
|
|
588
|
+
_write_json(manifest_path, manifest)
|
|
589
|
+
append_jsonl(
|
|
590
|
+
events_jsonl_path(home, manager_id, task_group, task_id),
|
|
591
|
+
{"event": "task-updated", "createdAt": now or _now_iso(), "changes": changes},
|
|
592
|
+
)
|
|
593
|
+
return {"manifest": manifest, "changedFields": sorted(changes)}
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def remove_task(
|
|
597
|
+
home: Path, manager_id: str, task_group: str, task_id: str, *, confirm: bool
|
|
598
|
+
) -> dict:
|
|
599
|
+
"""manager task 폴더를 지운다. 하위 프로젝트의 태스크와 브리프는 남는다."""
|
|
600
|
+
manifest = _read_json(task_manifest_path(home, manager_id, task_group, task_id))
|
|
601
|
+
root = task_manifest_path(home, manager_id, task_group, task_id).parent
|
|
602
|
+
children = _load_children(home, manager_id, task_group, task_id)["children"]
|
|
603
|
+
result = {
|
|
604
|
+
"managerId": manager_id,
|
|
605
|
+
"taskGroup": str(manifest.get("taskGroup") or task_group),
|
|
606
|
+
"taskId": str(manifest.get("taskId") or task_id),
|
|
607
|
+
"path": str(root),
|
|
608
|
+
"childCount": len(children),
|
|
609
|
+
"removed": confirm,
|
|
610
|
+
}
|
|
611
|
+
if confirm:
|
|
612
|
+
shutil.rmtree(root)
|
|
613
|
+
return result
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def remove_task_group(home: Path, manager_id: str, task_group: str, *, confirm: bool) -> dict:
|
|
617
|
+
"""그룹 폴더와 그 안의 manager task 전부를 지운다."""
|
|
618
|
+
load_manager(home, manager_id)
|
|
619
|
+
root = task_group_root(home, manager_id, task_group)
|
|
620
|
+
if not root.is_dir():
|
|
621
|
+
raise ManagerError(f"unknown task group: {task_group}")
|
|
622
|
+
task_ids = [str(_read_json(path).get("taskId") or path.parent.name) for path in sorted(root.glob("*/manifest.json"))]
|
|
623
|
+
result = {
|
|
624
|
+
"managerId": manager_id,
|
|
625
|
+
"taskGroup": task_group,
|
|
626
|
+
"path": str(root),
|
|
627
|
+
"taskIds": task_ids,
|
|
628
|
+
"removed": confirm,
|
|
629
|
+
}
|
|
630
|
+
if confirm:
|
|
631
|
+
shutil.rmtree(root)
|
|
632
|
+
return result
|
|
@@ -109,7 +109,7 @@ def sync_task(home: Path, manager_id: str, task_group: str, task_id: str, *, now
|
|
|
109
109
|
continue
|
|
110
110
|
project = projects.get(str(child.get("projectId") or ""))
|
|
111
111
|
if project is None:
|
|
112
|
-
rows.append({**child, "exists": False, "error": "
|
|
112
|
+
rows.append({**child, "exists": False, "error": "project unlinked from this manager; re-register it with `okstra manager new project`"})
|
|
113
113
|
continue
|
|
114
114
|
root = Path(str(project.get("projectRoot") or ""))
|
|
115
115
|
if not root.is_dir():
|
|
@@ -160,6 +160,7 @@ def status_task(home: Path, manager_id: str, task_group: str, task_id: str) -> d
|
|
|
160
160
|
children = _read_json_default(children_json_path(home, manager_id, task_group, task_id), {"children": []})
|
|
161
161
|
snapshot = _read_json_default(snapshots_json_path(home, manager_id, task_group, task_id), {"children": []})
|
|
162
162
|
by_key = {row.get("taskKey"): row for row in snapshot.get("children", []) if isinstance(row, dict)}
|
|
163
|
+
linked = _project_map(home, manager_id)
|
|
163
164
|
rows = []
|
|
164
165
|
summary = {"planned": 0, "missing": 0, "running": 0, "done": 0, "blocked": 0}
|
|
165
166
|
for child in children.get("children", []):
|
|
@@ -168,6 +169,8 @@ def status_task(home: Path, manager_id: str, task_group: str, task_id: str) -> d
|
|
|
168
169
|
snapshot_row = by_key.get(child.get("taskKey"))
|
|
169
170
|
row = {**child, **(snapshot_row or {})}
|
|
170
171
|
row["summaryBucket"] = "planned" if snapshot_row is None else summary_bucket(row)
|
|
172
|
+
# `remove project` 는 하위 태스크를 남긴다. 등록 목록에 없으면 해제된 프로젝트의 것이다.
|
|
173
|
+
row["projectLinked"] = str(child.get("projectId") or "") in linked
|
|
171
174
|
summary[row["summaryBucket"]] += 1
|
|
172
175
|
rows.append(row)
|
|
173
176
|
return {"manifest": manifest, "summary": summary, "children": rows, "lastSnapshot": snapshot}
|
|
@@ -95,7 +95,8 @@ def _child_rows(children: list[dict], roots: dict[str, str]) -> list[list[str]]:
|
|
|
95
95
|
assignment = assignment or "; ".join(child.get("scope") or [])
|
|
96
96
|
rows.append(
|
|
97
97
|
[
|
|
98
|
-
f"<code>{_e(child.get('taskKey'))}</code>"
|
|
98
|
+
f"<code>{_e(child.get('taskKey'))}</code>"
|
|
99
|
+
+ ("" if child.get("projectLinked", True) else " " + _chip("unlinked")),
|
|
99
100
|
_e(child.get("ticketId") or "-"),
|
|
100
101
|
_chip(str(child.get("summaryBucket") or "planned")),
|
|
101
102
|
_e(child.get("workStatus") or "-"),
|