okstra 0.201.3 → 0.204.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli-registry.mjs +7 -7
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/lifecycle/install.mjs +50 -124
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/dist/commands/lifecycle/setup.mjs +15 -0
- package/dist/commands/lifecycle/setup.mjs.map +1 -1
- package/dist/commands/memory/memory.mjs +41 -8
- package/dist/commands/memory/memory.mjs.map +1 -1
- package/dist/lib/citation-guidance.d.mts +21 -0
- package/dist/lib/citation-guidance.mjs +79 -0
- package/dist/lib/citation-guidance.mjs.map +1 -0
- package/dist/lib/install-assets.mjs +3 -0
- package/dist/lib/install-assets.mjs.map +1 -1
- package/dist/lib/runtime-manifest.mjs +2 -1
- package/dist/lib/runtime-manifest.mjs.map +1 -1
- package/dist/lib/types.d.mts +2 -1
- package/docs/architecture/storage-model.md +17 -10
- package/docs/architecture.md +26 -20
- package/docs/cli.md +16 -13
- package/docs/contributor-change-matrix.md +3 -2
- package/docs/performance-improvement-plan-v2.md +2 -3
- package/docs/project-structure-overview.md +38 -9
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/common-flow.md +1 -1
- package/docs/task-process/final-verification.md +3 -1
- package/docs/task-process/implementation.md +1 -1
- package/docs/task-process/release-handoff.md +36 -39
- package/package.json +1 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/common.json +28 -0
- package/runtime/agents/operations/code-review.json +6 -0
- package/runtime/agents/operations/report-translation.json +6 -0
- package/runtime/agents/operations/schedule-verification.json +6 -0
- package/runtime/agents/roles/analyser.json +18 -0
- package/runtime/agents/roles/critic.json +18 -0
- package/runtime/agents/roles/designer.json +18 -0
- package/runtime/agents/roles/implementer.json +20 -0
- package/runtime/agents/roles/leader.json +20 -0
- package/runtime/agents/roles/planner.json +18 -0
- package/runtime/agents/roles/report-writer.json +19 -0
- package/runtime/agents/roles/translator.json +19 -0
- package/runtime/agents/roles/verifier.json +18 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- package/runtime/prompts/duties/acceptance-critic.json +32 -0
- package/runtime/prompts/duties/acceptance-verifier.json +32 -0
- package/runtime/prompts/duties/analysis-worker.json +32 -0
- package/runtime/prompts/duties/code-reviewer.json +32 -0
- package/runtime/prompts/duties/diagnosis-worker.json +32 -0
- package/runtime/prompts/duties/direction-selection-worker.json +32 -0
- package/runtime/prompts/duties/discovery-worker.json +32 -0
- package/runtime/prompts/duties/implementation-executor.json +32 -0
- package/runtime/prompts/duties/implementation-verifier.json +32 -0
- package/runtime/prompts/duties/lead.json +32 -0
- package/runtime/prompts/duties/planning-worker.json +36 -0
- package/runtime/prompts/duties/report-writer.json +32 -0
- package/runtime/prompts/duties/reverification-worker.json +32 -0
- package/runtime/prompts/duties/schedule-verifier.json +32 -0
- package/runtime/prompts/duties/scope-critic.json +32 -0
- package/runtime/prompts/duties/technical-verification-worker.json +32 -0
- package/runtime/prompts/duties/translator.json +32 -0
- package/runtime/prompts/launch.template.md +3 -2
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/convergence.md +4 -4
- package/runtime/prompts/lead/okstra-lead-contract.md +115 -6
- package/runtime/prompts/lead/plan-body-verification.md +6 -6
- package/runtime/prompts/lead/report-writer.md +3 -3
- package/runtime/prompts/profiles/_common-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +4 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/change-impact-analysis.json +31 -0
- package/runtime/prompts/profiles/change-impact-analysis.md +0 -20
- package/runtime/prompts/profiles/error-analysis.json +39 -0
- package/runtime/prompts/profiles/error-analysis.md +0 -25
- package/runtime/prompts/profiles/feature-analysis.json +31 -0
- package/runtime/prompts/profiles/feature-analysis.md +0 -20
- package/runtime/prompts/profiles/final-verification.json +30 -0
- package/runtime/prompts/profiles/final-verification.md +3 -22
- package/runtime/prompts/profiles/forbidden-actions.json +4 -3
- package/runtime/prompts/profiles/implementation-option-selection.json +31 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +0 -20
- package/runtime/prompts/profiles/implementation-planning.json +40 -0
- package/runtime/prompts/profiles/implementation-planning.md +6 -29
- package/runtime/prompts/profiles/implementation.json +30 -0
- package/runtime/prompts/profiles/implementation.md +1 -20
- package/runtime/prompts/profiles/improvement-discovery.json +31 -0
- package/runtime/prompts/profiles/improvement-discovery.md +0 -20
- package/runtime/prompts/profiles/project-analysis.json +31 -0
- package/runtime/prompts/profiles/project-analysis.md +0 -20
- package/runtime/prompts/profiles/release-handoff.json +5 -0
- package/runtime/prompts/profiles/release-handoff.md +71 -73
- package/runtime/prompts/profiles/requirements-discovery.json +39 -0
- package/runtime/prompts/profiles/requirements-discovery.md +0 -25
- package/runtime/prompts/profiles/technical-verification.json +39 -0
- package/runtime/prompts/profiles/technical-verification.md +0 -25
- package/runtime/prompts/wizard/prompts.ko.json +12 -17
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +3 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -3
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/worker-session.md +108 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +8 -1
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +8 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +23 -6
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +6 -2
- package/runtime/python/okstra_ctl/agent/invocation.py +168 -113
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +120 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +107 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -49
- package/runtime/python/okstra_ctl/analysis_packet.py +4 -1
- package/runtime/python/okstra_ctl/application/open_worker.py +6 -1
- package/runtime/python/okstra_ctl/assignment_resolver.py +16 -5
- package/runtime/python/okstra_ctl/cmux.py +69 -20
- package/runtime/python/okstra_ctl/code_review_target.py +16 -8
- package/runtime/python/okstra_ctl/conformance.py +43 -0
- package/runtime/python/okstra_ctl/consumers.py +6 -3
- package/runtime/python/okstra_ctl/container.py +31 -8
- package/runtime/python/okstra_ctl/context_cost.py +11 -15
- package/runtime/python/okstra_ctl/contract_refreeze.py +156 -0
- package/runtime/python/okstra_ctl/convergence_critic_prompt.py +4 -6
- package/runtime/python/okstra_ctl/convergence_provenance.py +81 -18
- package/runtime/python/okstra_ctl/design_prep.py +34 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +53 -27
- package/runtime/python/okstra_ctl/domain/host.py +5 -0
- package/runtime/python/okstra_ctl/domain/worker_runtime.py +10 -0
- package/runtime/python/okstra_ctl/error_report.py +4 -3
- package/runtime/python/okstra_ctl/execution_manifest.py +71 -18
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +21 -21
- package/runtime/python/okstra_ctl/handoff.py +167 -277
- package/runtime/python/okstra_ctl/implementation_stage.py +9 -0
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +113 -0
- package/runtime/python/okstra_ctl/lead_progress.py +1 -1
- package/runtime/python/okstra_ctl/legacy_model_selection.py +2 -2
- package/runtime/python/okstra_ctl/manager_cli.py +175 -14
- package/runtime/python/okstra_ctl/manager_launch.py +41 -19
- package/runtime/python/okstra_ctl/manager_paths.py +22 -3
- package/runtime/python/okstra_ctl/manager_split.py +474 -0
- package/runtime/python/okstra_ctl/manager_store.py +331 -21
- package/runtime/python/okstra_ctl/manager_sync.py +37 -16
- package/runtime/python/okstra_ctl/manager_view.py +217 -0
- package/runtime/python/okstra_ctl/model_discovery.py +30 -0
- package/runtime/python/okstra_ctl/model_io/lines.py +14 -1
- package/runtime/python/okstra_ctl/model_io/renderers.py +4 -3
- package/runtime/python/okstra_ctl/models.py +1 -1
- package/runtime/python/okstra_ctl/next_phase.py +16 -6
- package/runtime/python/okstra_ctl/operation_invocation.py +86 -0
- package/runtime/python/okstra_ctl/option_comparison.py +168 -0
- package/runtime/python/okstra_ctl/path_hints.py +9 -0
- package/runtime/python/okstra_ctl/paths.py +3 -0
- package/runtime/python/okstra_ctl/plan_items_cli.py +6 -1
- package/runtime/python/okstra_ctl/profile_show.py +42 -1
- package/runtime/python/okstra_ctl/qa_commands.py +15 -0
- package/runtime/python/okstra_ctl/registry/host_discovery.py +20 -12
- package/runtime/python/okstra_ctl/registry/host_registry.py +11 -0
- package/runtime/python/okstra_ctl/render.py +50 -0
- package/runtime/python/okstra_ctl/report_contract.py +1 -1
- package/runtime/python/okstra_ctl/report_finalize.py +13 -6
- package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +2 -21
- package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +21 -3
- package/runtime/python/okstra_ctl/report_html/visualizations.py +0 -5
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +177 -17
- package/runtime/python/okstra_ctl/report_translation.py +2 -1
- package/runtime/python/okstra_ctl/report_translation_dispatch.py +69 -9
- package/runtime/python/okstra_ctl/role_requirements.py +142 -129
- package/runtime/python/okstra_ctl/rollup.py +3 -1
- package/runtime/python/okstra_ctl/run.py +76 -29
- package/runtime/python/okstra_ctl/schedule_semantics.py +17 -6
- package/runtime/python/okstra_ctl/stage_fix_carry.py +23 -4
- package/runtime/python/okstra_ctl/stage_integrate.py +178 -18
- package/runtime/python/okstra_ctl/stage_map.py +16 -2
- package/runtime/python/okstra_ctl/stage_targets.py +209 -43
- package/runtime/python/okstra_ctl/team.py +22 -13
- package/runtime/python/okstra_ctl/time_report.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +3 -1
- package/runtime/python/okstra_ctl/verification_target.py +13 -2
- package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -9
- package/runtime/python/okstra_ctl/wizard/ids.py +1 -1
- package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
- package/runtime/python/okstra_ctl/wizard/state.py +3 -5
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +3 -23
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +5 -1
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +35 -7
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +66 -48
- package/runtime/python/okstra_ctl/workflow.py +1 -1
- package/runtime/python/okstra_ctl/worktree/__init__.py +3 -1
- package/runtime/python/okstra_ctl/worktree/naming.py +9 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +38 -9
- package/runtime/python/okstra_token_usage/pricing.py +6 -4
- package/runtime/schemas/agent-common-v1.schema.json +34 -0
- package/runtime/schemas/agent-duty-v1.schema.json +38 -0
- package/runtime/schemas/agent-operation-v1.schema.json +11 -0
- package/runtime/schemas/agent-profile-v1.schema.json +46 -0
- package/runtime/schemas/agent-role-v1.schema.json +29 -0
- package/runtime/schemas/final-report-v2.0.schema.json +118 -97
- package/runtime/schemas/final-report-v3.0.schema.json +118 -97
- package/runtime/skills/okstra-brief-gen/SKILL.md +84 -4
- package/runtime/skills/okstra-chat/SKILL.md +2 -2
- package/runtime/skills/okstra-code-review/SKILL.md +23 -9
- package/runtime/skills/okstra-container-build/SKILL.md +10 -10
- package/runtime/skills/okstra-inspect/SKILL.md +1 -1
- package/runtime/skills/okstra-inspect/facets/cost.md +1 -1
- package/runtime/skills/okstra-inspect/facets/error-zip.md +9 -9
- package/runtime/skills/okstra-inspect/facets/errors.md +16 -16
- package/runtime/skills/okstra-inspect/facets/logs.md +7 -7
- package/runtime/skills/okstra-inspect/facets/recap.md +2 -2
- package/runtime/skills/okstra-inspect/facets/report.md +1 -1
- package/runtime/skills/okstra-inspect/facets/status.md +4 -3
- package/runtime/skills/okstra-inspect/facets/time.md +11 -10
- package/runtime/skills/okstra-manager/SKILL.md +70 -5
- package/runtime/skills/okstra-pr-gen/SKILL.md +6 -5
- package/runtime/skills/okstra-rollup/SKILL.md +5 -5
- package/runtime/skills/okstra-run/SKILL.md +32 -13
- package/runtime/skills/okstra-schedule-gen/SKILL.md +19 -14
- package/runtime/skills/okstra-setup/SKILL.md +21 -10
- package/runtime/skills/okstra-setup/references/project-config.md +7 -6
- package/runtime/skills/okstra-usage/SKILL.md +1 -1
- package/runtime/skills/okstra-user-response/SKILL.md +1 -1
- package/runtime/templates/manager/view.template.html +109 -0
- package/runtime/templates/report-writer-prompt-preamble.md +8 -0
- package/runtime/templates/reports/brief.template.md +14 -4
- package/runtime/templates/reports/html/i18n/en.json +7 -4
- package/runtime/templates/reports/html/i18n/ko.json +7 -4
- package/runtime/templates/reports/html/tasks/final-verification.template.html +2 -2
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +8 -5
- package/runtime/templates/reports/i18n/en.json +1 -1
- package/runtime/templates/reports/md/tasks/release-handoff.template.md +1 -1
- package/runtime/templates/reports/release-handoff-input.template.md +6 -4
- package/runtime/templates/translator-prompt-preamble.md +36 -0
- package/runtime/validators/checks/validate-assets-01.py +7 -8
- package/runtime/validators/validate-brief.py +77 -2
- package/runtime/validators/validate-implementation-plan-stages.py +2 -1
- package/runtime/validators/validate-run.py +59 -9
- package/runtime/validators/validate-schedule.py +9 -0
- package/docs/for-ai/README.md +0 -68
- package/docs/for-ai/skills/okstra-brief-gen.md +0 -262
- package/docs/for-ai/skills/okstra-chat.md +0 -34
- package/docs/for-ai/skills/okstra-code-review.md +0 -57
- package/docs/for-ai/skills/okstra-container-build.md +0 -129
- package/docs/for-ai/skills/okstra-inspect.md +0 -262
- package/docs/for-ai/skills/okstra-manager.md +0 -69
- package/docs/for-ai/skills/okstra-memory.md +0 -126
- package/docs/for-ai/skills/okstra-pr-gen.md +0 -49
- package/docs/for-ai/skills/okstra-rollup.md +0 -114
- package/docs/for-ai/skills/okstra-run.md +0 -250
- package/docs/for-ai/skills/okstra-schedule-gen.md +0 -240
- package/docs/for-ai/skills/okstra-setup.md +0 -158
- package/docs/for-ai/skills/okstra-usage.md +0 -29
- package/docs/for-ai/skills/okstra-user-response.md +0 -72
- package/runtime/agents/workers/claude-worker.md +0 -128
- package/runtime/agents/workers/report-writer-worker.md +0 -37
- package/runtime/agents/workers/translator-worker.md +0 -63
- package/runtime/prompts/duties/acceptance-critic.md +0 -44
- package/runtime/prompts/duties/acceptance-verifier.md +0 -44
- package/runtime/prompts/duties/analysis-worker.md +0 -44
- package/runtime/prompts/duties/code-reviewer.md +0 -44
- package/runtime/prompts/duties/common.md +0 -39
- package/runtime/prompts/duties/diagnosis-worker.md +0 -44
- package/runtime/prompts/duties/direction-selection-worker.md +0 -44
- package/runtime/prompts/duties/discovery-worker.md +0 -44
- package/runtime/prompts/duties/implementation-executor.md +0 -44
- package/runtime/prompts/duties/implementation-verifier.md +0 -44
- package/runtime/prompts/duties/lead.md +0 -44
- package/runtime/prompts/duties/planning-worker.md +0 -52
- package/runtime/prompts/duties/report-writer.md +0 -44
- package/runtime/prompts/duties/reverification-worker.md +0 -44
- package/runtime/prompts/duties/schedule-verifier.md +0 -44
- package/runtime/prompts/duties/scope-critic.md +0 -44
- package/runtime/prompts/duties/technical-verification-worker.md +0 -44
- package/runtime/prompts/duties/translator.md +0 -44
- package/runtime/python/okstra_ctl/pane_title.py +0 -154
|
@@ -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
|
|
|
@@ -96,6 +102,63 @@ def load_manager(home: Path, manager_id: str) -> dict:
|
|
|
96
102
|
return _read_json(path)
|
|
97
103
|
|
|
98
104
|
|
|
105
|
+
def _task_manifest_paths(home: Path, manager_id: str) -> list[Path]:
|
|
106
|
+
return sorted(manager_root(home, manager_id).glob("task-groups/*/*/manifest.json"))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def list_managers(home: Path) -> list[dict]:
|
|
110
|
+
"""이 홈의 manager 를 id 순으로, 소속 프로젝트 수와 manager task 수와 함께 돌려준다."""
|
|
111
|
+
base = Path(home) / "managers"
|
|
112
|
+
if not base.is_dir():
|
|
113
|
+
return []
|
|
114
|
+
items: list[dict] = []
|
|
115
|
+
for path in sorted(base.glob("*/manager.json")):
|
|
116
|
+
manager = _read_json(path)
|
|
117
|
+
manager_id = str(manager.get("managerId") or path.parent.name)
|
|
118
|
+
items.append(
|
|
119
|
+
{
|
|
120
|
+
"managerId": manager_id,
|
|
121
|
+
"createdAt": str(manager.get("createdAt") or ""),
|
|
122
|
+
"projectCount": len(_load_projects(home, manager_id).get("projects", [])),
|
|
123
|
+
"taskCount": len(_task_manifest_paths(home, manager_id)),
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
return items
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def list_projects(home: Path, manager_id: str) -> list[dict]:
|
|
130
|
+
"""manager 에 등록된 프로젝트를 등록 파일(projects.json) 순서 그대로 돌려준다."""
|
|
131
|
+
load_manager(home, manager_id)
|
|
132
|
+
return [
|
|
133
|
+
project
|
|
134
|
+
for project in _load_projects(home, manager_id).get("projects", [])
|
|
135
|
+
if isinstance(project, dict)
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def list_tasks(home: Path, manager_id: str) -> list[dict]:
|
|
140
|
+
"""manager task 를 task-group, task-id 순으로 하위 task 수와 함께 돌려준다.
|
|
141
|
+
|
|
142
|
+
디렉터리 이름은 slug 라 원래 입력값과 다를 수 있으므로 식별자는 manifest 에서 읽는다.
|
|
143
|
+
"""
|
|
144
|
+
load_manager(home, manager_id)
|
|
145
|
+
items: list[dict] = []
|
|
146
|
+
for path in _task_manifest_paths(home, manager_id):
|
|
147
|
+
manifest = _read_json(path)
|
|
148
|
+
children = _read_json_default(path.parent / "children.json", {"children": []})
|
|
149
|
+
items.append(
|
|
150
|
+
{
|
|
151
|
+
"taskGroup": str(manifest.get("taskGroup") or ""),
|
|
152
|
+
"taskId": str(manifest.get("taskId") or ""),
|
|
153
|
+
"objective": str(manifest.get("objective") or ""),
|
|
154
|
+
"progressMode": str(manifest.get("progressMode") or ""),
|
|
155
|
+
"createdAt": str(manifest.get("createdAt") or ""),
|
|
156
|
+
"childCount": len(children.get("children", [])),
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
return sorted(items, key=lambda item: (item["taskGroup"], item["taskId"]))
|
|
160
|
+
|
|
161
|
+
|
|
99
162
|
def discover_projects(home: Path) -> list[dict]:
|
|
100
163
|
base = Path(home) / "projects"
|
|
101
164
|
if not base.is_dir():
|
|
@@ -167,11 +230,31 @@ def link_project(
|
|
|
167
230
|
return payload
|
|
168
231
|
|
|
169
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
|
+
|
|
170
243
|
def create_task_group(home: Path, manager_id: str, task_group: str, *, now: str | None = None) -> dict:
|
|
171
244
|
load_manager(home, manager_id)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
245
|
+
meta = _ensure_group_meta(home, manager_id, task_group, now or _now_iso())
|
|
246
|
+
return {"managerId": manager_id, **meta}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _merge_manifest(existing: dict, requested: dict) -> None:
|
|
250
|
+
"""이미 있는 manager task 의 manifest 값을 `new task` 가 바꾸지 못하게 한다."""
|
|
251
|
+
for key, value in requested.items():
|
|
252
|
+
if value is None or value == "" or existing.get(key) == value:
|
|
253
|
+
continue
|
|
254
|
+
raise ManagerError(
|
|
255
|
+
f"manager task already exists with {key}={existing.get(key)!r}; "
|
|
256
|
+
f"refusing to change it to {value!r}"
|
|
257
|
+
)
|
|
175
258
|
|
|
176
259
|
|
|
177
260
|
def create_task(
|
|
@@ -183,32 +266,56 @@ def create_task(
|
|
|
183
266
|
*,
|
|
184
267
|
objective: str = "",
|
|
185
268
|
common_brief_path: str = "",
|
|
186
|
-
progress_mode: str =
|
|
269
|
+
progress_mode: str | None = None,
|
|
187
270
|
now: str | None = None,
|
|
188
271
|
) -> dict:
|
|
272
|
+
"""manager task 를 만들거나, 이미 있으면 새 하위 task 만 덧붙인다.
|
|
273
|
+
|
|
274
|
+
다시 실행해도 기존 하위 task 의 assignment·launch 상태는 지우지 않는다.
|
|
275
|
+
"""
|
|
276
|
+
load_manager(home, manager_id)
|
|
189
277
|
projects = _load_projects(home, manager_id)
|
|
190
278
|
when = now or _now_iso()
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
279
|
+
manifest_path = task_manifest_path(home, manager_id, task_group, task_id)
|
|
280
|
+
existing = manifest_path.is_file()
|
|
281
|
+
if existing:
|
|
282
|
+
manifest = _read_json(manifest_path)
|
|
283
|
+
_merge_manifest(
|
|
284
|
+
manifest,
|
|
285
|
+
{
|
|
286
|
+
"objective": objective,
|
|
287
|
+
"commonBriefPath": common_brief_path,
|
|
288
|
+
"progressMode": progress_mode,
|
|
289
|
+
},
|
|
290
|
+
)
|
|
291
|
+
else:
|
|
292
|
+
manifest = {
|
|
293
|
+
"managerId": manager_id,
|
|
294
|
+
"taskGroup": task_group,
|
|
295
|
+
"taskId": task_id,
|
|
296
|
+
"progressMode": progress_mode or "manual",
|
|
297
|
+
"commonBriefPath": common_brief_path,
|
|
298
|
+
"objective": objective,
|
|
299
|
+
"createdAt": when,
|
|
300
|
+
}
|
|
301
|
+
children = _load_children(home, manager_id, task_group, task_id)
|
|
302
|
+
known_keys = {child.get("taskKey") for child in children["children"] if isinstance(child, dict)}
|
|
303
|
+
added = []
|
|
201
304
|
for spec in child_specs:
|
|
202
305
|
project_id = str(spec.get("projectId") or "")
|
|
203
306
|
child_task_id = str(spec.get("taskId") or task_id)
|
|
204
307
|
if _project_entry(projects, project_id) is None:
|
|
205
308
|
raise ManagerError(f"unknown project membership: {project_id}")
|
|
206
|
-
|
|
309
|
+
task_key = _child_task_key(project_id, task_group, child_task_id)
|
|
310
|
+
if task_key in known_keys:
|
|
311
|
+
continue
|
|
312
|
+
known_keys.add(task_key)
|
|
313
|
+
added.append(
|
|
207
314
|
{
|
|
208
315
|
"projectId": project_id,
|
|
209
316
|
"taskGroup": task_group,
|
|
210
317
|
"taskId": child_task_id,
|
|
211
|
-
"taskKey":
|
|
318
|
+
"taskKey": task_key,
|
|
212
319
|
"role": "",
|
|
213
320
|
"tags": [],
|
|
214
321
|
"assignment": "",
|
|
@@ -219,14 +326,20 @@ def create_task(
|
|
|
219
326
|
},
|
|
220
327
|
}
|
|
221
328
|
)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
329
|
+
children["children"].extend(added)
|
|
330
|
+
if not existing:
|
|
331
|
+
_ensure_group_meta(home, manager_id, task_group, when)
|
|
332
|
+
_write_json(manifest_path, manifest)
|
|
333
|
+
_write_json(children_json_path(home, manager_id, task_group, task_id), children)
|
|
225
334
|
append_jsonl(
|
|
226
335
|
events_jsonl_path(home, manager_id, task_group, task_id),
|
|
227
|
-
{
|
|
336
|
+
{
|
|
337
|
+
"event": "task-children-added" if existing else "task-created",
|
|
338
|
+
"createdAt": when,
|
|
339
|
+
"taskKeys": [child["taskKey"] for child in added],
|
|
340
|
+
},
|
|
228
341
|
)
|
|
229
|
-
return
|
|
342
|
+
return {"manifest": manifest, "children": children}
|
|
230
343
|
|
|
231
344
|
|
|
232
345
|
def _load_children(home: Path, manager_id: str, task_group: str, task_id: str) -> dict:
|
|
@@ -286,6 +399,7 @@ def assign_child(
|
|
|
286
399
|
assignment: str,
|
|
287
400
|
child_task_id: str | None = None,
|
|
288
401
|
) -> dict:
|
|
402
|
+
_read_json(task_manifest_path(home, manager_id, task_group, task_id))
|
|
289
403
|
payload = _load_children(home, manager_id, task_group, task_id)
|
|
290
404
|
child = _select_child_for_assignment(payload["children"], project_id, task_group, child_task_id)
|
|
291
405
|
child["role"] = role
|
|
@@ -311,6 +425,11 @@ def append_directive(
|
|
|
311
425
|
raise ManagerError("scope must be shared or project")
|
|
312
426
|
if scope == "project" and not project_id:
|
|
313
427
|
raise ManagerError("project directive requires project-id")
|
|
428
|
+
_read_json(task_manifest_path(home, manager_id, task_group, task_id))
|
|
429
|
+
if scope == "project":
|
|
430
|
+
children = _load_children(home, manager_id, task_group, task_id)["children"]
|
|
431
|
+
if not any(isinstance(child, dict) and child.get("projectId") == project_id for child in children):
|
|
432
|
+
raise ManagerError(f"project directive target has no child in this task: {project_id}")
|
|
314
433
|
row = {
|
|
315
434
|
"scope": scope,
|
|
316
435
|
"projectId": project_id if scope == "project" else "",
|
|
@@ -320,3 +439,194 @@ def append_directive(
|
|
|
320
439
|
}
|
|
321
440
|
append_jsonl(directives_jsonl_path(home, manager_id, task_group, task_id), row)
|
|
322
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
|
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from okstra_project import TASKS_RELATIVE, StateError, parse_task_key, read_task_catalog, read_task_manifest, slugify
|
|
7
7
|
|
|
8
8
|
from . import next_phase
|
|
9
|
+
from .next_phase import STATUS_BLOCKED, STATUS_TERMINAL
|
|
9
10
|
from .manager_paths import children_json_path, projects_json_path, snapshots_json_path, task_manifest_path
|
|
10
11
|
from .manager_store import _now_iso, _read_json, _read_json_default, _write_json
|
|
11
12
|
|
|
@@ -89,6 +90,7 @@ def _snapshot_child(project_root: Path, child: dict) -> dict:
|
|
|
89
90
|
workflow.get("nextRecommendedPhase")
|
|
90
91
|
),
|
|
91
92
|
"latestRunStatus": str(manifest.get("latestRunStatus") or manifest.get("currentStatus") or ""),
|
|
93
|
+
"workStatus": str(manifest.get("workStatus") or ""),
|
|
92
94
|
"latestReportRecordPath": str(manifest.get("latestReportRecordPath") or ""),
|
|
93
95
|
"finalVerdict": str(outcome.get("finalVerdict") or ""),
|
|
94
96
|
"crossProjectDependencies": _list_field(outcome, "crossProjectDependencies"),
|
|
@@ -107,7 +109,7 @@ def sync_task(home: Path, manager_id: str, task_group: str, task_id: str, *, now
|
|
|
107
109
|
continue
|
|
108
110
|
project = projects.get(str(child.get("projectId") or ""))
|
|
109
111
|
if project is None:
|
|
110
|
-
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`"})
|
|
111
113
|
continue
|
|
112
114
|
root = Path(str(project.get("projectRoot") or ""))
|
|
113
115
|
if not root.is_dir():
|
|
@@ -125,31 +127,50 @@ def sync_task(home: Path, manager_id: str, task_group: str, task_id: str, *, now
|
|
|
125
127
|
return payload
|
|
126
128
|
|
|
127
129
|
|
|
130
|
+
# 사용자가 `okstra set-work-status` 로 적은 값이 파생 포인터보다 우선한다.
|
|
131
|
+
# 그룹 큐(group_context.catalog_progress)와 같은 순서다. `latestRunStatus` 는
|
|
132
|
+
# phase 한 번의 실행 결과(`completed` / `contract-violated` / `in-progress` …)라
|
|
133
|
+
# task 가 끝났는지 말해 주지 않으므로 요약 분류에 쓰지 않는다.
|
|
134
|
+
_WORK_STATUS_BUCKETS = {
|
|
135
|
+
"done": "done",
|
|
136
|
+
"blocked": "blocked",
|
|
137
|
+
"in-progress": "running",
|
|
138
|
+
"todo": "planned",
|
|
139
|
+
}
|
|
140
|
+
_POINTER_STATUS_BUCKETS = {
|
|
141
|
+
STATUS_TERMINAL: "done",
|
|
142
|
+
STATUS_BLOCKED: "blocked",
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def summary_bucket(row: dict) -> str:
|
|
147
|
+
"""스냅샷이 있는 하위 task 한 줄을 요약 칸 하나로 분류한다."""
|
|
148
|
+
if not row.get("exists", False):
|
|
149
|
+
return "missing"
|
|
150
|
+
work_status = str(row.get("workStatus") or "")
|
|
151
|
+
if work_status in _WORK_STATUS_BUCKETS:
|
|
152
|
+
return _WORK_STATUS_BUCKETS[work_status]
|
|
153
|
+
pointer = row.get("nextRecommendedPhase")
|
|
154
|
+
pointer_status = pointer.get("status") if isinstance(pointer, dict) else ""
|
|
155
|
+
return _POINTER_STATUS_BUCKETS.get(pointer_status, "running")
|
|
156
|
+
|
|
157
|
+
|
|
128
158
|
def status_task(home: Path, manager_id: str, task_group: str, task_id: str) -> dict:
|
|
129
159
|
manifest = _read_json(task_manifest_path(home, manager_id, task_group, task_id))
|
|
130
160
|
children = _read_json_default(children_json_path(home, manager_id, task_group, task_id), {"children": []})
|
|
131
161
|
snapshot = _read_json_default(snapshots_json_path(home, manager_id, task_group, task_id), {"children": []})
|
|
132
162
|
by_key = {row.get("taskKey"): row for row in snapshot.get("children", []) if isinstance(row, dict)}
|
|
163
|
+
linked = _project_map(home, manager_id)
|
|
133
164
|
rows = []
|
|
134
165
|
summary = {"planned": 0, "missing": 0, "running": 0, "done": 0, "blocked": 0}
|
|
135
166
|
for child in children.get("children", []):
|
|
136
167
|
if not isinstance(child, dict):
|
|
137
168
|
continue
|
|
138
|
-
|
|
139
|
-
snapshot_row = by_key.get(key)
|
|
169
|
+
snapshot_row = by_key.get(child.get("taskKey"))
|
|
140
170
|
row = {**child, **(snapshot_row or {})}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
summary["missing"] += 1
|
|
146
|
-
elif status == "done":
|
|
147
|
-
summary["done"] += 1
|
|
148
|
-
elif status in {"running", "started", "in-progress"}:
|
|
149
|
-
summary["running"] += 1
|
|
150
|
-
elif status == "blocked":
|
|
151
|
-
summary["blocked"] += 1
|
|
152
|
-
else:
|
|
153
|
-
summary["planned"] += 1
|
|
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
|
|
174
|
+
summary[row["summaryBucket"]] += 1
|
|
154
175
|
rows.append(row)
|
|
155
176
|
return {"manifest": manifest, "summary": summary, "children": rows, "lastSnapshot": snapshot}
|