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
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"""트래커 분할 계획을 받아 프로젝트별 범위 지정 브리프를 쓰고 manager 하위 태스크로 등록한다.
|
|
2
|
+
|
|
3
|
+
Linear 조회와 본문 저작은 에이전트(okstra-manager 스킬)가 한다. 이 모듈은 계획 검증,
|
|
4
|
+
브리프 렌더링, 브리프 검증기 통과 확인, 파일 쓰기, 하위 태스크 등록을 맡는다. 검증이
|
|
5
|
+
하나라도 실패하면 브리프도 manager 상태도 쓰지 않는다.
|
|
6
|
+
|
|
7
|
+
브리프 계약(EB/PB/EO 형식, Open Questions 접두어, 관계 표 값)은 여기서 다시 구현하지
|
|
8
|
+
않는다. 렌더링 결과를 `validators/validate-brief.py` 로 검사해서 집행한다.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib.util
|
|
13
|
+
import re
|
|
14
|
+
import tempfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from types import ModuleType
|
|
17
|
+
|
|
18
|
+
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
|
|
19
|
+
from okstra_ctl.jsonl import append_jsonl
|
|
20
|
+
|
|
21
|
+
from .ids import slugify_task_segment
|
|
22
|
+
from .manager_paths import children_json_path, events_jsonl_path, split_plan_path, task_manifest_path
|
|
23
|
+
from .manager_store import (
|
|
24
|
+
ManagerError,
|
|
25
|
+
_now_iso,
|
|
26
|
+
_read_json,
|
|
27
|
+
_write_json,
|
|
28
|
+
create_task,
|
|
29
|
+
list_projects,
|
|
30
|
+
load_manager,
|
|
31
|
+
)
|
|
32
|
+
from .paths import BRIEF_VALIDATOR_RELATIVE, find_asset_root
|
|
33
|
+
|
|
34
|
+
PLAN_SCHEMA_VERSION = 1
|
|
35
|
+
SOURCE_KINDS = ("project", "issue")
|
|
36
|
+
RECOMMENDED_PHASES = ("requirements-discovery", "error-analysis", "improvement-discovery")
|
|
37
|
+
DEFAULT_PHASE = "requirements-discovery"
|
|
38
|
+
GENERATOR = "okstra-manager"
|
|
39
|
+
SOURCE_TYPE = "linear"
|
|
40
|
+
# 브리프 계약의 필수 서술 필드. 비어 있으면 검증기가 거절하므로 계획 단계에서 먼저 막는다.
|
|
41
|
+
_REQUIRED_TEXT_FIELDS = ("context", "problem", "desiredOutcome")
|
|
42
|
+
_LIST_FIELDS = (
|
|
43
|
+
"expectedBehavior",
|
|
44
|
+
"preservedBehavior",
|
|
45
|
+
"expectedOutcome",
|
|
46
|
+
"externalGates",
|
|
47
|
+
"constraints",
|
|
48
|
+
"openQuestions",
|
|
49
|
+
"outOfScope",
|
|
50
|
+
)
|
|
51
|
+
# 파일명·하위 task id 로 쓰이므로 경로 구분자나 공백을 받지 않는다.
|
|
52
|
+
_TICKET_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
|
53
|
+
# okstra-brief-gen Step 2b: 영숫자·공백·한글만 남긴다.
|
|
54
|
+
_FILE_TITLE_DROP = re.compile(r"[^0-9A-Za-z가-힣ㄱ-ㅎㅏ-ㅣ ]+")
|
|
55
|
+
_FILE_TITLE_MAX = 60
|
|
56
|
+
_NONE = "_(none)_"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def brief_file_title(title: str) -> str:
|
|
60
|
+
"""okstra-brief-gen Step 2b 의 `<file-title>` 규칙."""
|
|
61
|
+
slug = re.sub(r" +", "-", _FILE_TITLE_DROP.sub("", title).strip()).lower()
|
|
62
|
+
if len(slug) > _FILE_TITLE_MAX:
|
|
63
|
+
cut = slug[:_FILE_TITLE_MAX]
|
|
64
|
+
boundary = cut.rfind("-")
|
|
65
|
+
slug = cut[:boundary] if boundary > 0 else cut
|
|
66
|
+
return slug.strip("-")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _load_brief_validator() -> ModuleType:
|
|
70
|
+
root = find_asset_root(BRIEF_VALIDATOR_RELATIVE)
|
|
71
|
+
if root is None:
|
|
72
|
+
raise ManagerError("brief validator not found: " + "/".join(BRIEF_VALIDATOR_RELATIVE))
|
|
73
|
+
spec = importlib.util.spec_from_file_location("okstra_validate_brief", root.joinpath(*BRIEF_VALIDATOR_RELATIVE))
|
|
74
|
+
if spec is None or spec.loader is None:
|
|
75
|
+
raise ManagerError("brief validator could not be loaded")
|
|
76
|
+
module = importlib.util.module_from_spec(spec)
|
|
77
|
+
spec.loader.exec_module(module)
|
|
78
|
+
return module
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _load_plan(path: Path) -> dict:
|
|
82
|
+
try:
|
|
83
|
+
return load_owned_object(Path(path), artifact="manager split plan")
|
|
84
|
+
except JsonBoundaryError as exc:
|
|
85
|
+
raise ManagerError(str(exc)) from exc
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _text(value: object) -> str:
|
|
89
|
+
return value.strip() if isinstance(value, str) else ""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _string_list(value: object, where: str, errors: list[str]) -> list[str]:
|
|
93
|
+
if value is None:
|
|
94
|
+
return []
|
|
95
|
+
if not isinstance(value, list):
|
|
96
|
+
errors.append(f"{where} must be a list of strings")
|
|
97
|
+
return []
|
|
98
|
+
items = []
|
|
99
|
+
for index, item in enumerate(value, 1):
|
|
100
|
+
text = _text(item)
|
|
101
|
+
if not text:
|
|
102
|
+
errors.append(f"{where}[{index}] must be a non-empty string")
|
|
103
|
+
continue
|
|
104
|
+
items.append(text)
|
|
105
|
+
return items
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _require_text(record: dict, key: str, where: str, errors: list[str]) -> str:
|
|
109
|
+
value = _text(record.get(key))
|
|
110
|
+
if not value:
|
|
111
|
+
errors.append(f"{where}.{key} is required")
|
|
112
|
+
return value
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _no_pipe(value: str, where: str, errors: list[str]) -> None:
|
|
116
|
+
# 관계 표의 셀 구분자라 값에 들어오면 표가 깨진다.
|
|
117
|
+
if "|" in value:
|
|
118
|
+
errors.append(f"{where} must not contain '|'")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _normalize_source(raw: object, errors: list[str]) -> dict:
|
|
122
|
+
if not isinstance(raw, dict):
|
|
123
|
+
errors.append("source must be an object")
|
|
124
|
+
return {}
|
|
125
|
+
source = {key: _require_text(raw, key, "source", errors) for key in ("kind", "ref", "title", "body")}
|
|
126
|
+
if source["kind"] and source["kind"] not in SOURCE_KINDS:
|
|
127
|
+
errors.append(f"source.kind must be one of {list(SOURCE_KINDS)}, got {source['kind']!r}")
|
|
128
|
+
source["fetchedVia"] = _text(raw.get("fetchedVia")) or "user-paste"
|
|
129
|
+
source["fetchedAt"] = _text(raw.get("fetchedAt"))
|
|
130
|
+
_no_pipe(source["ref"], "source.ref", errors)
|
|
131
|
+
_no_pipe(source["title"], "source.title", errors)
|
|
132
|
+
return source
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _normalize_assignment(raw: object, where: str, members: set[str], errors: list[str]) -> dict:
|
|
136
|
+
if not isinstance(raw, dict):
|
|
137
|
+
errors.append(f"{where} must be an object")
|
|
138
|
+
return {}
|
|
139
|
+
assignment = {"projectId": _require_text(raw, "projectId", where, errors)}
|
|
140
|
+
if assignment["projectId"] and assignment["projectId"] not in members:
|
|
141
|
+
errors.append(f"{where}.projectId {assignment['projectId']!r} is not a project of this manager")
|
|
142
|
+
assignment["scope"] = _string_list(raw.get("scope"), f"{where}.scope", errors)
|
|
143
|
+
if not assignment["scope"]:
|
|
144
|
+
errors.append(f"{where}.scope must list at least one item of work for this project")
|
|
145
|
+
phase = _text(raw.get("recommendedPhase")) or DEFAULT_PHASE
|
|
146
|
+
if phase not in RECOMMENDED_PHASES:
|
|
147
|
+
errors.append(f"{where}.recommendedPhase must be one of {list(RECOMMENDED_PHASES)}, got {phase!r}")
|
|
148
|
+
assignment["recommendedPhase"] = phase
|
|
149
|
+
for key in _REQUIRED_TEXT_FIELDS:
|
|
150
|
+
assignment[key] = _require_text(raw, key, where, errors)
|
|
151
|
+
for key in _LIST_FIELDS:
|
|
152
|
+
assignment[key] = _string_list(raw.get(key), f"{where}.{key}", errors)
|
|
153
|
+
return assignment
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _normalize_relations(raw: object, where: str, relations: set[str], errors: list[str]) -> list[dict]:
|
|
157
|
+
if raw is None:
|
|
158
|
+
return []
|
|
159
|
+
if not isinstance(raw, list):
|
|
160
|
+
errors.append(f"{where}.relations must be a list")
|
|
161
|
+
return []
|
|
162
|
+
rows = []
|
|
163
|
+
for index, item in enumerate(raw, 1):
|
|
164
|
+
at = f"{where}.relations[{index}]"
|
|
165
|
+
if not isinstance(item, dict):
|
|
166
|
+
errors.append(f"{at} must be an object")
|
|
167
|
+
continue
|
|
168
|
+
row = {key: _require_text(item, key, at, errors) for key in ("relation", "to", "source", "impact")}
|
|
169
|
+
if row["relation"] and row["relation"] not in relations:
|
|
170
|
+
errors.append(f"{at}.relation must be one of {sorted(relations)}, got {row['relation']!r}")
|
|
171
|
+
for key, value in row.items():
|
|
172
|
+
_no_pipe(value, f"{at}.{key}", errors)
|
|
173
|
+
rows.append(row)
|
|
174
|
+
return rows
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _normalize_issue(raw: object, where: str, members: set[str], relations: set[str], errors: list[str]) -> dict:
|
|
178
|
+
if not isinstance(raw, dict):
|
|
179
|
+
errors.append(f"{where} must be an object")
|
|
180
|
+
return {}
|
|
181
|
+
issue = {key: _require_text(raw, key, where, errors) for key in ("ticketId", "title", "body")}
|
|
182
|
+
if issue["ticketId"] and not _TICKET_ID.match(issue["ticketId"]):
|
|
183
|
+
errors.append(f"{where}.ticketId {issue['ticketId']!r} must match {_TICKET_ID.pattern}")
|
|
184
|
+
issue["url"] = _text(raw.get("url"))
|
|
185
|
+
_no_pipe(issue["url"], f"{where}.url", errors)
|
|
186
|
+
issue["fetchedVia"] = _text(raw.get("fetchedVia")) or "user-paste"
|
|
187
|
+
issue["fetchedAt"] = _text(raw.get("fetchedAt"))
|
|
188
|
+
issue["relations"] = _normalize_relations(raw.get("relations"), where, relations, errors)
|
|
189
|
+
raw_assignments = raw.get("assignments")
|
|
190
|
+
if not isinstance(raw_assignments, list) or not raw_assignments:
|
|
191
|
+
errors.append(f"{where}.assignments must list at least one project")
|
|
192
|
+
raw_assignments = []
|
|
193
|
+
issue["assignments"] = [
|
|
194
|
+
_normalize_assignment(item, f"{where}.assignments[{index}]", members, errors)
|
|
195
|
+
for index, item in enumerate(raw_assignments, 1)
|
|
196
|
+
]
|
|
197
|
+
seen = [a.get("projectId") for a in issue["assignments"] if a.get("projectId")]
|
|
198
|
+
for project_id in sorted({pid for pid in seen if seen.count(pid) > 1}):
|
|
199
|
+
errors.append(f"{where}.assignments names project {project_id!r} more than once")
|
|
200
|
+
return issue
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def normalize_plan(plan: dict, members: set[str], relations: set[str]) -> tuple[dict, list[dict], list[str]]:
|
|
204
|
+
"""계획을 (source, issues, errors) 로 정리한다. errors 가 비어야 쓸 수 있다."""
|
|
205
|
+
errors: list[str] = []
|
|
206
|
+
if plan.get("schemaVersion") != PLAN_SCHEMA_VERSION:
|
|
207
|
+
errors.append(f"schemaVersion must be {PLAN_SCHEMA_VERSION}, got {plan.get('schemaVersion')!r}")
|
|
208
|
+
source = _normalize_source(plan.get("source"), errors)
|
|
209
|
+
raw_issues = plan.get("issues")
|
|
210
|
+
if not isinstance(raw_issues, list) or not raw_issues:
|
|
211
|
+
errors.append("issues must list at least one issue")
|
|
212
|
+
raw_issues = []
|
|
213
|
+
issues = [
|
|
214
|
+
_normalize_issue(item, f"issues[{index}]", members, relations, errors)
|
|
215
|
+
for index, item in enumerate(raw_issues, 1)
|
|
216
|
+
]
|
|
217
|
+
ticket_ids = [issue.get("ticketId") for issue in issues if issue.get("ticketId")]
|
|
218
|
+
for ticket_id in sorted({tid for tid in ticket_ids if ticket_ids.count(tid) > 1}):
|
|
219
|
+
errors.append(f"issues name ticketId {ticket_id!r} more than once")
|
|
220
|
+
return source, issues, errors
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _bullets(items: list[str]) -> list[str]:
|
|
224
|
+
return [f"- {item}" for item in items] if items else [_NONE]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _fence(body: str) -> str:
|
|
228
|
+
longest = max((len(run) for run in re.findall(r"`+", body)), default=0)
|
|
229
|
+
return "`" * max(3, longest + 1)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _source_block(index: int, ref: str, fetched_via: str, fetched_at: str, body: str) -> list[str]:
|
|
233
|
+
fence = _fence(body)
|
|
234
|
+
return [
|
|
235
|
+
f"### Source {index} — {SOURCE_TYPE}",
|
|
236
|
+
"",
|
|
237
|
+
f"- ref: {ref}",
|
|
238
|
+
f"- fetched-via: {fetched_via}",
|
|
239
|
+
f"- fetched-at: {fetched_at or '-'}",
|
|
240
|
+
"- format: as-is",
|
|
241
|
+
"",
|
|
242
|
+
fence,
|
|
243
|
+
body,
|
|
244
|
+
fence,
|
|
245
|
+
"",
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _source_is_issue(source: dict, issue: dict) -> bool:
|
|
250
|
+
return source["ref"] in {issue["ticketId"], issue["url"]}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _graph_rows(source: dict, issues: list[dict], keys: dict, directed: set[str]) -> list[tuple[str, ...]]:
|
|
254
|
+
"""분할 전체 위상. 모든 브리프에 같은 표가 들어간다(okstra-brief-gen 분할 규칙)."""
|
|
255
|
+
membership = "Linear project membership" if source["kind"] == "project" else "Linear sub-issue"
|
|
256
|
+
rows: list[tuple[str, ...]] = []
|
|
257
|
+
for issue in issues:
|
|
258
|
+
ticket = issue["ticketId"]
|
|
259
|
+
if not _source_is_issue(source, issue):
|
|
260
|
+
rows.append((source["ref"], "parent-of", ticket, "directed", membership,
|
|
261
|
+
f"{ticket} is part of {source['title']}"))
|
|
262
|
+
for assignment in issue["assignments"]:
|
|
263
|
+
rows.append((keys[(ticket, assignment["projectId"])], "split-from", ticket, "directed",
|
|
264
|
+
"okstra manager task split",
|
|
265
|
+
f"covers only the {assignment['projectId']} part of {ticket}"))
|
|
266
|
+
for relation in issue["relations"]:
|
|
267
|
+
direction = "directed" if relation["relation"] in directed else "undirected"
|
|
268
|
+
rows.append((ticket, relation["relation"], relation["to"], direction,
|
|
269
|
+
relation["source"], relation["impact"]))
|
|
270
|
+
return rows
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _render_brief(
|
|
274
|
+
*,
|
|
275
|
+
source: dict,
|
|
276
|
+
issue: dict,
|
|
277
|
+
assignment: dict,
|
|
278
|
+
task_group: str,
|
|
279
|
+
manager_task: str,
|
|
280
|
+
stem: str,
|
|
281
|
+
created: str,
|
|
282
|
+
key: str,
|
|
283
|
+
project_root: str,
|
|
284
|
+
siblings: list[tuple[str, dict, Path]],
|
|
285
|
+
graph: list[tuple[str, ...]],
|
|
286
|
+
) -> str:
|
|
287
|
+
ticket = issue["ticketId"]
|
|
288
|
+
project_id = assignment["projectId"]
|
|
289
|
+
out_of_scope = list(assignment["outOfScope"])
|
|
290
|
+
out_of_scope.extend(f"`{sibling_key}` (project `{other['projectId']}`): {'; '.join(other['scope'])}"
|
|
291
|
+
for sibling_key, other, _ in siblings)
|
|
292
|
+
lines = [
|
|
293
|
+
"---",
|
|
294
|
+
"type: brief",
|
|
295
|
+
f"brief-id: {stem}",
|
|
296
|
+
"parent-id: self",
|
|
297
|
+
f"ticket-id: {ticket}",
|
|
298
|
+
f"source-type: {SOURCE_TYPE}",
|
|
299
|
+
f"task-group: {task_group}",
|
|
300
|
+
"depth: 0",
|
|
301
|
+
f"created: {created}",
|
|
302
|
+
f"generator: {GENERATOR}",
|
|
303
|
+
"reporter-confirmations: skipped",
|
|
304
|
+
f"project-id: {project_id}",
|
|
305
|
+
f"manager-task: {manager_task}",
|
|
306
|
+
"---",
|
|
307
|
+
"",
|
|
308
|
+
f"# Task Brief: {task_group}/{stem}",
|
|
309
|
+
"",
|
|
310
|
+
f"> - Generated: {GENERATOR} · {created}",
|
|
311
|
+
f"> - Source type: {SOURCE_TYPE}",
|
|
312
|
+
f"> - Tracker key (if any): {ticket}",
|
|
313
|
+
f"> - Project: `{project_id}` (child task `{key}`)",
|
|
314
|
+
f"> - Recommended next phase: {assignment['recommendedPhase']}",
|
|
315
|
+
"> - Handoff contract: see `prompts/profiles/_common-contract.md` § \"Brief handoff contract\"",
|
|
316
|
+
"",
|
|
317
|
+
"## Source Material",
|
|
318
|
+
"",
|
|
319
|
+
*_source_block(1, issue["url"] or ticket, issue["fetchedVia"], issue["fetchedAt"], issue["body"]),
|
|
320
|
+
]
|
|
321
|
+
if not _source_is_issue(source, issue):
|
|
322
|
+
lines.extend(_source_block(2, source["ref"], source["fetchedVia"], source["fetchedAt"], source["body"]))
|
|
323
|
+
lines.extend(
|
|
324
|
+
[
|
|
325
|
+
"## Context",
|
|
326
|
+
"",
|
|
327
|
+
assignment["context"],
|
|
328
|
+
"",
|
|
329
|
+
"## Project Scope",
|
|
330
|
+
"",
|
|
331
|
+
f"This brief covers only the part of {ticket} that lands in project `{project_id}` "
|
|
332
|
+
f"(`{project_root}`). Work outside the in-scope list belongs to the tasks named under out of scope.",
|
|
333
|
+
"",
|
|
334
|
+
"### In scope",
|
|
335
|
+
"",
|
|
336
|
+
*_bullets(assignment["scope"]),
|
|
337
|
+
"",
|
|
338
|
+
"### Out of scope",
|
|
339
|
+
"",
|
|
340
|
+
*_bullets(out_of_scope),
|
|
341
|
+
"",
|
|
342
|
+
"## Problem / Symptom",
|
|
343
|
+
"",
|
|
344
|
+
assignment["problem"],
|
|
345
|
+
"",
|
|
346
|
+
"## Desired Outcome",
|
|
347
|
+
"",
|
|
348
|
+
assignment["desiredOutcome"],
|
|
349
|
+
"",
|
|
350
|
+
]
|
|
351
|
+
)
|
|
352
|
+
for heading, field in (("Expected Behavior", "expectedBehavior"), ("Preserved Behavior", "preservedBehavior"),
|
|
353
|
+
("Expected Outcome", "expectedOutcome"), ("External Gates", "externalGates"),
|
|
354
|
+
("Constraints", "constraints")):
|
|
355
|
+
lines.extend([f"## {heading}", "", *_bullets(assignment[field]), ""])
|
|
356
|
+
artifacts = [ref for ref in (issue["url"], source["ref"]) if ref]
|
|
357
|
+
artifacts.extend(f"`{sibling_key}` brief: {path}" for sibling_key, _, path in siblings)
|
|
358
|
+
lines.extend(["## Related Artifacts", "", *_bullets(artifacts), "", "## Related Task Graph", ""])
|
|
359
|
+
lines.append("| From | Relation | To | Direction | Source | Impact |")
|
|
360
|
+
lines.append("|---|---|---|---|---|---|")
|
|
361
|
+
lines.extend("| " + " | ".join(row) + " |" for row in graph)
|
|
362
|
+
lines.extend(
|
|
363
|
+
[
|
|
364
|
+
"",
|
|
365
|
+
"## Open Questions",
|
|
366
|
+
"",
|
|
367
|
+
*_bullets(assignment["openQuestions"]),
|
|
368
|
+
"",
|
|
369
|
+
"## Reporter Confirmations",
|
|
370
|
+
"",
|
|
371
|
+
"_(none — pending or skipped)_",
|
|
372
|
+
"",
|
|
373
|
+
]
|
|
374
|
+
)
|
|
375
|
+
return "\n".join(lines)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _validate_rendered(items: list[dict], validator: ModuleType, group_segment: str) -> list[str]:
|
|
379
|
+
errors: list[str] = []
|
|
380
|
+
with tempfile.TemporaryDirectory(prefix="okstra-split-") as scratch:
|
|
381
|
+
for index, item in enumerate(items):
|
|
382
|
+
briefs_root = Path(scratch) / str(index) / "briefs"
|
|
383
|
+
path = briefs_root / group_segment / item["briefPath"].name
|
|
384
|
+
path.parent.mkdir(parents=True)
|
|
385
|
+
path.write_text(item["text"], encoding="utf-8")
|
|
386
|
+
errors.extend(f"{item['taskKey']}: {error}" for error in validator.validate_brief(path, briefs_root))
|
|
387
|
+
return errors
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def split_task(
|
|
391
|
+
home: Path,
|
|
392
|
+
manager_id: str,
|
|
393
|
+
task_group: str,
|
|
394
|
+
task_id: str,
|
|
395
|
+
plan_path: Path,
|
|
396
|
+
*,
|
|
397
|
+
overwrite: bool = False,
|
|
398
|
+
now: str | None = None,
|
|
399
|
+
) -> dict:
|
|
400
|
+
"""분할 계획으로 브리프를 쓰고 하위 태스크를 등록한다. 결과 브리프 목록을 돌려준다."""
|
|
401
|
+
load_manager(home, manager_id)
|
|
402
|
+
projects = {str(project.get("projectId")): project for project in list_projects(home, manager_id)}
|
|
403
|
+
group_segment = slugify_task_segment(task_group)
|
|
404
|
+
if not group_segment:
|
|
405
|
+
raise ManagerError(f"task-group {task_group!r} has no usable slug")
|
|
406
|
+
plan = _load_plan(plan_path)
|
|
407
|
+
validator = _load_brief_validator()
|
|
408
|
+
source, issues, errors = normalize_plan(plan, set(projects), set(validator.TASK_RELATIONS))
|
|
409
|
+
if errors:
|
|
410
|
+
raise ManagerError("split plan rejected:\n- " + "\n- ".join(errors))
|
|
411
|
+
when = now or _now_iso()
|
|
412
|
+
manager_task = f"{manager_id}/{task_group}/{task_id}"
|
|
413
|
+
keys: dict[tuple[str, str], str] = {}
|
|
414
|
+
items: list[dict] = []
|
|
415
|
+
for issue in issues:
|
|
416
|
+
ticket = issue["ticketId"]
|
|
417
|
+
stem = "-".join(part for part in (ticket, brief_file_title(issue["title"])) if part)
|
|
418
|
+
for assignment in issue["assignments"]:
|
|
419
|
+
project_id = assignment["projectId"]
|
|
420
|
+
child_task_id = slugify_task_segment(ticket)
|
|
421
|
+
key = f"{project_id}:{task_group}:{child_task_id}"
|
|
422
|
+
keys[(ticket, project_id)] = key
|
|
423
|
+
root = Path(str(projects[project_id].get("projectRoot") or ""))
|
|
424
|
+
items.append({"issue": issue, "assignment": assignment, "stem": stem, "taskKey": key,
|
|
425
|
+
"projectId": project_id, "childTaskId": child_task_id, "projectRoot": str(root),
|
|
426
|
+
"briefPath": root / ".okstra" / "briefs" / group_segment / f"{stem}.md"})
|
|
427
|
+
graph = _graph_rows(source, issues, keys, set(validator.DIRECTED_TASK_RELATIONS))
|
|
428
|
+
for item in items:
|
|
429
|
+
siblings = [(other["taskKey"], other["assignment"], other["briefPath"]) for other in items
|
|
430
|
+
if other["issue"] is item["issue"] and other is not item]
|
|
431
|
+
item["text"] = _render_brief(source=source, issue=item["issue"], assignment=item["assignment"],
|
|
432
|
+
task_group=task_group, manager_task=manager_task, stem=item["stem"],
|
|
433
|
+
created=when[:10], key=item["taskKey"], project_root=item["projectRoot"],
|
|
434
|
+
siblings=siblings, graph=graph)
|
|
435
|
+
errors = _validate_rendered(items, validator, group_segment)
|
|
436
|
+
if errors:
|
|
437
|
+
raise ManagerError("rendered briefs failed validate-brief:\n- " + "\n- ".join(errors))
|
|
438
|
+
for item in items:
|
|
439
|
+
path = item["briefPath"]
|
|
440
|
+
if not path.is_file():
|
|
441
|
+
item["briefStatus"] = "created"
|
|
442
|
+
elif path.read_text(encoding="utf-8") == item["text"]:
|
|
443
|
+
item["briefStatus"] = "unchanged"
|
|
444
|
+
else:
|
|
445
|
+
item["briefStatus"] = "overwritten"
|
|
446
|
+
conflicts = [str(item["briefPath"]) for item in items if item["briefStatus"] == "overwritten"]
|
|
447
|
+
if conflicts and not overwrite:
|
|
448
|
+
raise ManagerError(
|
|
449
|
+
"briefs already exist with different content; pass --overwrite to replace them:\n- "
|
|
450
|
+
+ "\n- ".join(conflicts)
|
|
451
|
+
)
|
|
452
|
+
for item in items:
|
|
453
|
+
if item["briefStatus"] != "unchanged":
|
|
454
|
+
item["briefPath"].parent.mkdir(parents=True, exist_ok=True)
|
|
455
|
+
item["briefPath"].write_text(item["text"], encoding="utf-8")
|
|
456
|
+
existing = task_manifest_path(home, manager_id, task_group, task_id).is_file()
|
|
457
|
+
create_task(home, manager_id, task_group, task_id,
|
|
458
|
+
[{"projectId": item["projectId"], "taskId": item["childTaskId"]} for item in items],
|
|
459
|
+
objective="" if existing else source["title"], now=when)
|
|
460
|
+
children_path = children_json_path(home, manager_id, task_group, task_id)
|
|
461
|
+
children = _read_json(children_path)
|
|
462
|
+
by_key = {child.get("taskKey"): child for child in children.get("children", []) if isinstance(child, dict)}
|
|
463
|
+
for item in items:
|
|
464
|
+
by_key[item["taskKey"]].update({"ticketId": item["issue"]["ticketId"], "briefPath": str(item["briefPath"]),
|
|
465
|
+
"scope": list(item["assignment"]["scope"]),
|
|
466
|
+
"recommendedPhase": item["assignment"]["recommendedPhase"]})
|
|
467
|
+
_write_json(children_path, children)
|
|
468
|
+
_write_json(split_plan_path(home, manager_id, task_group, task_id), plan)
|
|
469
|
+
briefs = [{"taskKey": item["taskKey"], "projectId": item["projectId"], "ticketId": item["issue"]["ticketId"],
|
|
470
|
+
"briefPath": str(item["briefPath"]), "briefStatus": item["briefStatus"]} for item in items]
|
|
471
|
+
append_jsonl(events_jsonl_path(home, manager_id, task_group, task_id),
|
|
472
|
+
{"event": "task-split", "createdAt": when, "source": source["ref"],
|
|
473
|
+
"taskKeys": [brief["taskKey"] for brief in briefs]})
|
|
474
|
+
return {"managerId": manager_id, "taskGroup": task_group, "taskId": task_id, "briefs": briefs}
|