okstra 0.214.1 → 0.216.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 +2 -1
- package/dist/cli-registry.d.mts +14 -0
- package/dist/cli-registry.mjs +12 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/chat/chat.mjs +2 -56
- package/dist/commands/chat/chat.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.mjs +5 -2
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/wizard.mjs +6 -0
- package/dist/commands/execute/wizard.mjs.map +1 -1
- package/dist/commands/memory/memory.mjs +57 -6
- package/dist/commands/memory/memory.mjs.map +1 -1
- package/dist/lib/file-lock.d.mts +1 -0
- package/dist/lib/file-lock.mjs +60 -0
- package/dist/lib/file-lock.mjs.map +1 -0
- package/dist/lib/python-command.mjs +11 -1
- package/dist/lib/python-command.mjs.map +1 -1
- package/dist/lib/skill-catalog.mjs +1 -0
- package/dist/lib/skill-catalog.mjs.map +1 -1
- package/dist/lib/types.d.mts +1 -1
- package/docs/architecture.md +2 -2
- package/docs/cli.md +36 -14
- package/docs/project-structure-overview.md +6 -3
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/cli.sh +9 -2
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/interactive.sh +8 -2
- package/runtime/bin/lib/okstra/usage.sh +3 -1
- package/runtime/bin/okstra.sh +7 -1
- package/runtime/prompts/duties/discussion-worker.json +32 -0
- package/runtime/prompts/lead/convergence.md +58 -136
- package/runtime/prompts/lead/okstra-lead-contract.md +45 -12
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +14 -1
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +4 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +4 -11
- package/runtime/python/okstra_ctl/agent/invocation.py +1 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +2 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +4 -4
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +2 -2
- package/runtime/python/okstra_ctl/analysis_packet.py +3 -0
- package/runtime/python/okstra_ctl/consumers.py +8 -2
- package/runtime/python/okstra_ctl/convergence.py +143 -29
- package/runtime/python/okstra_ctl/convergence_critic_prompt.py +57 -0
- package/runtime/python/okstra_ctl/convergence_critic_verify_prompt.py +106 -0
- package/runtime/python/okstra_ctl/convergence_engine.py +140 -95
- package/runtime/python/okstra_ctl/convergence_reverify_prompt.py +3 -27
- package/runtime/python/okstra_ctl/convergence_store.py +11 -4
- package/runtime/python/okstra_ctl/dispatch_core.py +4 -1
- package/runtime/python/okstra_ctl/domain/role.py +1 -0
- package/runtime/python/okstra_ctl/manager_cli.py +27 -1
- package/runtime/python/okstra_ctl/manager_launch.py +44 -4
- package/runtime/python/okstra_ctl/manager_sync.py +14 -1
- package/runtime/python/okstra_ctl/manager_view.py +53 -1
- package/runtime/python/okstra_ctl/next_phase.py +1 -0
- package/runtime/python/okstra_ctl/phases/catalog.py +2 -1
- package/runtime/python/okstra_ctl/phases/discussion/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/discussion/boundary.json +11 -0
- package/runtime/python/okstra_ctl/phases/discussion/cli.py +512 -0
- package/runtime/python/okstra_ctl/phases/discussion/entry.py +116 -0
- package/runtime/python/okstra_ctl/phases/discussion/followup.py +876 -0
- package/runtime/python/okstra_ctl/phases/discussion/memory.py +294 -0
- package/runtime/python/okstra_ctl/phases/discussion/overview.py +85 -0
- package/runtime/python/okstra_ctl/phases/discussion/profile.json +31 -0
- package/runtime/python/okstra_ctl/phases/discussion/profile.md +48 -0
- package/runtime/python/okstra_ctl/phases/discussion/report.py +73 -0
- package/runtime/python/okstra_ctl/phases/discussion/report_assets/discussion.template.html +15 -0
- package/runtime/python/okstra_ctl/phases/discussion/report_assets/discussion.template.md +6 -0
- package/runtime/python/okstra_ctl/phases/discussion/spec.md +197 -0
- package/runtime/python/okstra_ctl/phases/discussion/store.py +1143 -0
- package/runtime/python/okstra_ctl/phases/discussion/validation.py +111 -0
- package/runtime/python/okstra_ctl/phases/discussion/wizard.py +90 -0
- package/runtime/python/okstra_ctl/phases/discussion/workflow.py +667 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/host-rules.md +4 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +1 -1
- package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +1 -1
- package/runtime/python/okstra_ctl/plan_run_root.py +8 -0
- package/runtime/python/okstra_ctl/render.py +7 -20
- package/runtime/python/okstra_ctl/render_final_report.py +2 -1
- package/runtime/python/okstra_ctl/report_assembly.py +22 -0
- package/runtime/python/okstra_ctl/report_contract.py +2 -0
- package/runtime/python/okstra_ctl/report_narrative.py +13 -4
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +10 -0
- package/runtime/python/okstra_ctl/run.py +40 -7
- package/runtime/python/okstra_ctl/stage_close.py +46 -11
- package/runtime/python/okstra_ctl/stage_reopen.py +151 -0
- package/runtime/python/okstra_ctl/verdict_blocks.py +11 -35
- package/runtime/python/okstra_ctl/wizard/cli.py +8 -0
- package/runtime/python/okstra_ctl/wizard/confirmation.py +4 -1
- package/runtime/python/okstra_ctl/wizard/engine.py +8 -0
- package/runtime/python/okstra_ctl/wizard/ids.py +12 -0
- package/runtime/python/okstra_ctl/wizard/registry.py +35 -3
- package/runtime/python/okstra_ctl/wizard/render.py +4 -1
- package/runtime/python/okstra_ctl/wizard/sources.py +2 -2
- package/runtime/python/okstra_ctl/wizard/state.py +11 -1
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +4 -4
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +24 -6
- package/runtime/python/okstra_token_usage/pricing.py +2 -0
- package/runtime/schemas/convergence-groups-v1.0.schema.json +1 -1
- package/runtime/schemas/convergence-groups-v2.0.schema.json +1 -1
- package/runtime/schemas/final-report-v3.0.schema.json +90 -1
- package/runtime/skills/_fragments/wizard-bash-invocation-rule.md +1 -0
- package/runtime/skills/_fragments/wizard-confirm-step.md +7 -0
- package/runtime/skills/_fragments/wizard-lead-takeover.md +8 -0
- package/runtime/skills/_fragments/wizard-preflight.md +75 -0
- package/runtime/skills/_fragments/wizard-prompt-loop.md +22 -0
- package/runtime/skills/_fragments/wizard-relay-protocol.md +40 -0
- package/runtime/skills/_fragments/wizard-render-bundle.md +74 -0
- package/runtime/skills/okstra-discussion/SKILL.md +347 -0
- package/runtime/skills/okstra-pr-gen/SKILL.md +7 -0
- package/runtime/skills/okstra-run/SKILL.md +22 -2
- package/runtime/templates/manager/view.template.html +3 -0
- package/runtime/templates/reports/html/i18n/en.json +5 -0
- package/runtime/templates/reports/html/i18n/ko.json +5 -0
- package/runtime/validators/validate-brief.py +3 -2
- package/runtime/validators/validate-run.py +4 -6
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
"""`okstra discussion` — 진행자가 토론 기록을 쓰고 읽는 유일한 명령."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from okstra_project import (
|
|
10
|
+
ResolverError,
|
|
11
|
+
StateError,
|
|
12
|
+
resolve_project_root,
|
|
13
|
+
resolve_task_identity,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from . import followup, memory, workflow
|
|
17
|
+
from .store import (
|
|
18
|
+
COMMON,
|
|
19
|
+
SCOPE_KINDS,
|
|
20
|
+
USER,
|
|
21
|
+
DiscussionStore,
|
|
22
|
+
DiscussionStoreError,
|
|
23
|
+
append_record,
|
|
24
|
+
confirm_completion,
|
|
25
|
+
read_draft,
|
|
26
|
+
read_revision,
|
|
27
|
+
render_draft_text,
|
|
28
|
+
render_full_record,
|
|
29
|
+
resolve_source_run,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_CLI_EPILOG = r"""Usage:
|
|
33
|
+
okstra discussion confirm-subject <task-key> --subject "<what>" --question "<to settle>" --run-manifest <path>
|
|
34
|
+
okstra discussion ask <task-key> --author <leader|worker-id> --text "<question for the user>" --run-manifest <path>
|
|
35
|
+
okstra discussion address <task-key> --to <worker-id> --text "<the user's question>" --run-manifest <path>
|
|
36
|
+
okstra discussion answer <task-key> --question <Q-NNNN> --author <user|worker-id> --text "<answer>" --run-manifest <path>
|
|
37
|
+
okstra discussion decide <task-key> --text "<the user's decision>" [--question <Q-NNNN>] --run-manifest <path>
|
|
38
|
+
okstra discussion add-material <task-key> --kind <evidence|alternative> --author <user|leader|worker-id> --text "<what>" --run-manifest <path>
|
|
39
|
+
okstra discussion draft <task-key>
|
|
40
|
+
okstra discussion status <task-key>
|
|
41
|
+
okstra discussion cycle-open <task-key> --run-manifest <path>
|
|
42
|
+
okstra discussion settle <task-key> --run-manifest <path>
|
|
43
|
+
okstra discussion recover <task-key> --failure <X-NNNN> --choice <retry|replace|reduce> --run-manifest <path>
|
|
44
|
+
okstra discussion cycle-close <task-key> --run-manifest <path>
|
|
45
|
+
okstra discussion question-prompt <task-key> --question <Q-NNNN> --run-manifest <path>
|
|
46
|
+
okstra discussion collect-answer <task-key> --question <Q-NNNN> --run-manifest <path>
|
|
47
|
+
okstra discussion add-requirement <task-key> --text "<requirement>" [--applies-to <common|follow-up label>] --author <user|leader|worker-id> --run-manifest <path>
|
|
48
|
+
okstra discussion add-scope <task-key> --kind <in-scope|excluded> --text "<what>" --author <user|leader|worker-id> --run-manifest <path>
|
|
49
|
+
okstra discussion withdraw <task-key> --item <N-NNNN|B-NNNN> --reason "<why the user drops it>" --run-manifest <path>
|
|
50
|
+
okstra discussion confirm-completion <task-key> --draft-version <sha256:...> --direction "<the user's chosen direction>" --risk "<the risks the user accepts>" --run-manifest <path>
|
|
51
|
+
okstra discussion reopen <task-key> --revision <task-key/revision-NNN> --reason "<why it changes>" --run-manifest <path>
|
|
52
|
+
okstra discussion revisions <task-key>
|
|
53
|
+
okstra discussion revision-show <task-key> --revision <task-key/revision-NNN>
|
|
54
|
+
okstra discussion record <task-key>
|
|
55
|
+
okstra discussion memory-sync <task-key> [--revision <task-key/revision-NNN> [--summary-file <path>]]
|
|
56
|
+
okstra discussion follow-up-brief <task-key> --revision <task-key/revision-NNN> --follow-up <label> --target-task <task-key> [--target-project-root <dir>] [--end-state <N-NNNN>=<EB|PB|EO>[:<how to observe>]]... [--replace]
|
|
57
|
+
okstra discussion links <task-key>
|
|
58
|
+
okstra discussion impact <task-key> --from <revision-id> --to <revision-id> [--follow-up <label>]
|
|
59
|
+
okstra discussion apply <task-key> --revision <latest revision-id> --target-task <task-key> --choice <adopt|keep> [--end-state ...]... [--replace]
|
|
60
|
+
okstra discussion complete-amendment <task-key> --revision <revision-id> --target-task <task-key> [--end-state ...]... [--replace]
|
|
61
|
+
|
|
62
|
+
Records go to <task-root>/discussion/records.jsonl under a directory lock, and
|
|
63
|
+
the draft read model is rebuilt after every record. --run-manifest names the
|
|
64
|
+
discussion run the record comes from; a manifest of another task, or of another
|
|
65
|
+
task type, is refused. Authors must be `leader` or a worker of that run; a
|
|
66
|
+
question asked with `ask` goes to the user, a question asked with `address`
|
|
67
|
+
comes from the user and goes to the named worker, and only its addressee may
|
|
68
|
+
answer it. Sending the same record again returns the stored record
|
|
69
|
+
(`created: false`) instead of adding a second one.
|
|
70
|
+
|
|
71
|
+
`draft` prints the fixed-text read model every participant reads: the confirmed
|
|
72
|
+
subject and question, start inputs, user decisions, questions waiting for the
|
|
73
|
+
user or for a participant, and every answered question with its answers.
|
|
74
|
+
|
|
75
|
+
`status` prints the phase (framing, ready-for-research, researching,
|
|
76
|
+
awaiting-recovery, awaiting-new-run, awaiting-user, awaiting-participant,
|
|
77
|
+
awaiting-choice, completed), the draft version, the published revisions, the
|
|
78
|
+
one question to put to the user next, unrecovered failures and the remaining
|
|
79
|
+
disagreements.
|
|
80
|
+
|
|
81
|
+
One research and refutation cycle runs per discussion run. `cycle-open` either
|
|
82
|
+
writes the research instruction every analyser receives (action `dispatch`) or
|
|
83
|
+
says why no research is dispatched: `await-user`, `await-participant`,
|
|
84
|
+
`await-recovery`, `present-options` (the input references — start inputs,
|
|
85
|
+
subject, answers, materials — equal the last closed cycle's), `prepare-run` (a
|
|
86
|
+
continuation run with `--discussion-start <task-key>` holds the next cycle), or
|
|
87
|
+
`completed` (the user completed the discussion). `settle` records each analyser's completed result and each failure
|
|
88
|
+
the dispatcher left after its retry loop; `recover` records the user's choice
|
|
89
|
+
for one failure. `cycle-close` imports the run's finalized convergence state as
|
|
90
|
+
claims with their authors and verdicts and ends that verification cycle; it
|
|
91
|
+
never completes the discussion. `question-prompt` writes the instruction and the
|
|
92
|
+
`agent-prompt materialize --batch` file for one direct question, and
|
|
93
|
+
`collect-answer` records the addressee's whole result file as the answer.
|
|
94
|
+
|
|
95
|
+
Completion belongs to the user. `add-requirement` and `add-scope` record the
|
|
96
|
+
requirements (common, or for one follow-up) and the in-scope and excluded
|
|
97
|
+
items of the current draft; adding an item the draft already holds returns it.
|
|
98
|
+
After `reopen`, the draft starts with the reopened revision's requirements and
|
|
99
|
+
scope, each under its original record id and marked with the revision it was
|
|
100
|
+
carried from. `withdraw` removes one carried or added item with the user's
|
|
101
|
+
reason; replacing an item is `withdraw` followed by `add-requirement` or
|
|
102
|
+
`add-scope`. `confirm-completion` binds the user's direction
|
|
103
|
+
choice and risk confirmation to the draft version `status` and `draft` print;
|
|
104
|
+
it is refused when that version is stale, or while the subject, a requirement,
|
|
105
|
+
an in-scope or excluded entry, a closed research cycle, a user or participant
|
|
106
|
+
answer or a failure choice is missing. It then publishes one immutable
|
|
107
|
+
revision, <task-root>/discussion/revisions/revision-NNN.json, whose id is the
|
|
108
|
+
task key and the completion sequence; resending the same confirmation returns
|
|
109
|
+
the published revision. Remaining disagreements, unverified scope and the
|
|
110
|
+
reduced roster are carried, not blocking. A completed discussion takes no other
|
|
111
|
+
record until `reopen` names the latest revision and the reason for change; the
|
|
112
|
+
next revision then carries that reference and reason, lists under
|
|
113
|
+
`itemChanges` what it carried, added and withdrawn, and earlier revisions
|
|
114
|
+
stay byte-for-byte readable with `revision-show`. `revisions` lists the
|
|
115
|
+
published revisions, `record` prints the draft read model followed by every
|
|
116
|
+
research result in full. Finalizing a run never confirms completion.
|
|
117
|
+
|
|
118
|
+
Every published revision is registered in the Memory Book after publication,
|
|
119
|
+
without a prompt: `okstra memory add --source-key discussion:<revision-id>`
|
|
120
|
+
stores a summary of its decisions, disagreements, risks and unverified scope
|
|
121
|
+
with the revision id, content digest, project and task — never the discussion
|
|
122
|
+
record itself. The summary and the registration state live in
|
|
123
|
+
<task-root>/discussion/memory/. A failed registration never undoes the
|
|
124
|
+
revision: it stays `pending`, `status` lists it under memoryRegistration, and
|
|
125
|
+
`memory-sync` retries it with the same source key; registering the same
|
|
126
|
+
revision again returns its existing entry. A summary the Memory Book refuses as
|
|
127
|
+
a likely secret is `refused` and is not retried until --summary-file supplies a
|
|
128
|
+
changed summary for that --revision.
|
|
129
|
+
|
|
130
|
+
Follow-ups run outside a discussion run, at the user's request. `follow-up-brief`
|
|
131
|
+
writes the brief of one follow-up task from one revision: the common
|
|
132
|
+
requirements and those that apply to --follow-up, the confirmed subject,
|
|
133
|
+
direction, scope, exclusions, disagreements and unverified scope, each naming
|
|
134
|
+
its record in the revision. --end-state maps a requirement to an Expected
|
|
135
|
+
Behavior, Preserved Behavior or Expected Outcome item; an observation method the
|
|
136
|
+
revision does not carry is new meaning, so the brief stays
|
|
137
|
+
`reporter-confirmations: pending` with an `intent-check:` row until the user
|
|
138
|
+
confirms it (okstra-brief-gen Step 6.5). The brief lands at
|
|
139
|
+
<target-project-root>/.okstra/briefs/<task-group>/<task-id>-<label>.md, passes
|
|
140
|
+
validators/validate-brief.py, recommends `requirements-discovery`, and pins the
|
|
141
|
+
discussion, the revision id, its content digest and the follow-up's requirement
|
|
142
|
+
ids in its frontmatter; prepare copies that pin into the run inputs as
|
|
143
|
+
`discussionRevisionRef`. The brief is the target task's own record of the
|
|
144
|
+
revision it uses; <task-root>/discussion/links.jsonl only remembers where each
|
|
145
|
+
target is. The brief is written first and the link after it, so resending an
|
|
146
|
+
interrupted command records the link once. A brief this command did not write
|
|
147
|
+
last is not replaced without --replace.
|
|
148
|
+
|
|
149
|
+
`links` re-reads every linked target: `current`, `needs-impact-review` (a newer
|
|
150
|
+
revision exists and the user has not chosen), `kept`, `amendment-pending`, or
|
|
151
|
+
`unknown` when the target cannot be read; an `amendment-pending` target names the
|
|
152
|
+
approved amended plan, or what is still missing. `impact` prints what changed between
|
|
153
|
+
two revisions, for one follow-up when --follow-up is given. `apply` records the
|
|
154
|
+
user's choice for one target and the latest revision: `keep` changes nothing,
|
|
155
|
+
`adopt` rewrites the brief of a target without an implementation-planning plan,
|
|
156
|
+
and routes a target with a plan to the plan amendment path without touching its
|
|
157
|
+
plan, brief or source. Resending the same choice returns the stored record; a
|
|
158
|
+
different choice for the same target and revision is refused.
|
|
159
|
+
`complete-amendment` finishes a routed adoption once the target has an
|
|
160
|
+
implementation-planning report newer than the one it had when the adoption was
|
|
161
|
+
routed and the user approved it (`frontmatter.approved: true`): it rewrites the
|
|
162
|
+
brief to pin the revision under the same --replace rule and records the plan
|
|
163
|
+
report it relied on. Without that report it is refused and names what is
|
|
164
|
+
missing; it never edits or approves a plan, and a resend returns the record.
|
|
165
|
+
|
|
166
|
+
Output: JSON { ok, created, record, pendingForUser, pendingForParticipants } for
|
|
167
|
+
the record commands, JSON { ok, ... } for the cycle, status, revisions,
|
|
168
|
+
memory-sync and follow-up commands, a `memory` field with the registration
|
|
169
|
+
state on confirm-completion, plain text for `draft` and `record`, and the revision file's own
|
|
170
|
+
bytes for `revision-show`. Exit 1 on a refusal, 2 when PROJECT_ROOT or the
|
|
171
|
+
task cannot be resolved.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _add_common(parser: argparse.ArgumentParser) -> None:
|
|
176
|
+
parser.add_argument("task_key", metavar="task-key", help="project-id:task-group:task-id")
|
|
177
|
+
parser.add_argument("--project-root", "--project", default="",
|
|
178
|
+
help="use this directory as PROJECT_ROOT")
|
|
179
|
+
parser.add_argument("--cwd", default=".", help="resolve PROJECT_ROOT starting from here")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _add_run(parser: argparse.ArgumentParser) -> None:
|
|
183
|
+
parser.add_argument("--run-manifest", required=True,
|
|
184
|
+
help="the discussion run manifest the record comes from")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _add_target(parser: argparse.ArgumentParser) -> None:
|
|
188
|
+
parser.add_argument("--target-task", required=True, help="the follow-up task key")
|
|
189
|
+
parser.add_argument("--end-state", action="append", default=[],
|
|
190
|
+
help="<N-NNNN>=<EB|PB|EO>[:<how to observe it>]; repeat per requirement")
|
|
191
|
+
parser.add_argument("--replace", action="store_true",
|
|
192
|
+
help="replace a brief this command did not write last")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _follow_up(store: DiscussionStore, args: argparse.Namespace) -> dict:
|
|
196
|
+
if args.command == "links":
|
|
197
|
+
return followup.links(store)
|
|
198
|
+
if args.command == "impact":
|
|
199
|
+
return followup.impact(store, from_revision=args.from_revision, to_revision=args.to_revision,
|
|
200
|
+
label=args.follow_up)
|
|
201
|
+
end_states = [followup.parse_end_state(value) for value in args.end_state]
|
|
202
|
+
if args.command == "complete-amendment":
|
|
203
|
+
return followup.complete_amendment(store, revision_id=args.revision, target_key=args.target_task,
|
|
204
|
+
end_states=end_states, replace=args.replace)
|
|
205
|
+
if args.command == "apply":
|
|
206
|
+
return followup.apply_revision(store, revision_id=args.revision, target_key=args.target_task,
|
|
207
|
+
choice=args.choice, end_states=end_states, replace=args.replace)
|
|
208
|
+
target_root = Path(args.target_project_root) if args.target_project_root else store.project_root
|
|
209
|
+
return followup.create_follow_up(
|
|
210
|
+
store, revision_id=args.revision, label=args.follow_up,
|
|
211
|
+
target=followup.target_of(args.target_task, target_root), end_states=end_states, replace=args.replace,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
_FOLLOW_UP_COMMANDS = frozenset({"follow-up-brief", "links", "impact", "apply", "complete-amendment"})
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
219
|
+
parser = argparse.ArgumentParser(
|
|
220
|
+
prog="okstra discussion",
|
|
221
|
+
description="Record and read a discussion task's questions, answers and decisions, drive its research cycles and publish the user's completed revisions.",
|
|
222
|
+
epilog=_CLI_EPILOG,
|
|
223
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
224
|
+
)
|
|
225
|
+
parser.add_argument("--okstra-cli", default="", help=argparse.SUPPRESS)
|
|
226
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
227
|
+
|
|
228
|
+
subject = sub.add_parser("confirm-subject", help="record the subject and question the user confirmed")
|
|
229
|
+
_add_common(subject)
|
|
230
|
+
subject.add_argument("--subject", required=True)
|
|
231
|
+
subject.add_argument("--question", required=True)
|
|
232
|
+
_add_run(subject)
|
|
233
|
+
|
|
234
|
+
ask = sub.add_parser("ask", help="register a question for the user")
|
|
235
|
+
_add_common(ask)
|
|
236
|
+
ask.add_argument("--author", required=True, help="`leader` or a worker id of the run")
|
|
237
|
+
ask.add_argument("--text", required=True)
|
|
238
|
+
_add_run(ask)
|
|
239
|
+
|
|
240
|
+
address = sub.add_parser("address", help="register the user's question to one participant")
|
|
241
|
+
_add_common(address)
|
|
242
|
+
address.add_argument("--to", required=True, help="the worker id the user addresses")
|
|
243
|
+
address.add_argument("--text", required=True)
|
|
244
|
+
_add_run(address)
|
|
245
|
+
|
|
246
|
+
answer = sub.add_parser("answer", help="record the answer to a question")
|
|
247
|
+
_add_common(answer)
|
|
248
|
+
answer.add_argument("--question", required=True, help="the question id (Q-NNNN)")
|
|
249
|
+
answer.add_argument("--author", required=True, help="`user` or the addressed worker id")
|
|
250
|
+
answer.add_argument("--text", required=True)
|
|
251
|
+
_add_run(answer)
|
|
252
|
+
|
|
253
|
+
decide = sub.add_parser("decide", help="record a user decision")
|
|
254
|
+
_add_common(decide)
|
|
255
|
+
decide.add_argument("--text", required=True)
|
|
256
|
+
decide.add_argument("--question", default="", help="the question the decision settles")
|
|
257
|
+
_add_run(decide)
|
|
258
|
+
|
|
259
|
+
material = sub.add_parser("add-material", help="record new evidence or a new alternative")
|
|
260
|
+
_add_common(material)
|
|
261
|
+
material.add_argument("--kind", required=True, choices=("evidence", "alternative"))
|
|
262
|
+
material.add_argument("--author", required=True, help="`user`, `leader` or a worker id of the run")
|
|
263
|
+
material.add_argument("--text", required=True)
|
|
264
|
+
_add_run(material)
|
|
265
|
+
|
|
266
|
+
draft = sub.add_parser("draft", help="print the draft read model as fixed text")
|
|
267
|
+
_add_common(draft)
|
|
268
|
+
|
|
269
|
+
state = sub.add_parser("status", help="print the phase and the next question for the user")
|
|
270
|
+
_add_common(state)
|
|
271
|
+
|
|
272
|
+
for name, help_text in (
|
|
273
|
+
("cycle-open", "open this run's research cycle or say why none is dispatched"),
|
|
274
|
+
("settle", "record the research results and failures of this run's cycle"),
|
|
275
|
+
("cycle-close", "end this run's verification cycle from its convergence final state"),
|
|
276
|
+
):
|
|
277
|
+
command = sub.add_parser(name, help=help_text)
|
|
278
|
+
_add_common(command)
|
|
279
|
+
_add_run(command)
|
|
280
|
+
|
|
281
|
+
recover = sub.add_parser("recover", help="record the user's choice for one failure")
|
|
282
|
+
_add_common(recover)
|
|
283
|
+
recover.add_argument("--failure", required=True, help="the failure id (X-NNNN)")
|
|
284
|
+
recover.add_argument("--choice", required=True, choices=workflow.CHOICES)
|
|
285
|
+
_add_run(recover)
|
|
286
|
+
|
|
287
|
+
requirement = sub.add_parser("add-requirement", help="record a requirement of the current draft")
|
|
288
|
+
_add_common(requirement)
|
|
289
|
+
requirement.add_argument("--text", required=True)
|
|
290
|
+
requirement.add_argument("--applies-to", default=COMMON,
|
|
291
|
+
help="`common`, or the follow-up the requirement belongs to")
|
|
292
|
+
requirement.add_argument("--author", required=True, help="`user`, `leader` or a worker id of the run")
|
|
293
|
+
_add_run(requirement)
|
|
294
|
+
|
|
295
|
+
scope = sub.add_parser("add-scope", help="record an in-scope or excluded item of the current draft")
|
|
296
|
+
_add_common(scope)
|
|
297
|
+
scope.add_argument("--kind", required=True, choices=SCOPE_KINDS)
|
|
298
|
+
scope.add_argument("--text", required=True)
|
|
299
|
+
scope.add_argument("--author", required=True, help="`user`, `leader` or a worker id of the run")
|
|
300
|
+
_add_run(scope)
|
|
301
|
+
|
|
302
|
+
withdraw = sub.add_parser("withdraw", help="withdraw one requirement or scope item from the current draft")
|
|
303
|
+
_add_common(withdraw)
|
|
304
|
+
withdraw.add_argument("--item", required=True, help="the requirement or scope record id (N-NNNN or B-NNNN)")
|
|
305
|
+
withdraw.add_argument("--reason", required=True, help="why the user withdraws it")
|
|
306
|
+
_add_run(withdraw)
|
|
307
|
+
|
|
308
|
+
confirm = sub.add_parser("confirm-completion",
|
|
309
|
+
help="record the user's completion confirmation and publish a revision")
|
|
310
|
+
_add_common(confirm)
|
|
311
|
+
confirm.add_argument("--draft-version", required=True,
|
|
312
|
+
help="the draft version the user confirmed (from `status` or `draft`)")
|
|
313
|
+
confirm.add_argument("--direction", required=True, help="the direction the user chose")
|
|
314
|
+
confirm.add_argument("--risk", required=True,
|
|
315
|
+
help="the remaining disagreements and risks the user confirmed")
|
|
316
|
+
_add_run(confirm)
|
|
317
|
+
|
|
318
|
+
reopen = sub.add_parser("reopen", help="open a new draft on top of the latest revision")
|
|
319
|
+
_add_common(reopen)
|
|
320
|
+
reopen.add_argument("--revision", required=True, help="the latest revision id")
|
|
321
|
+
reopen.add_argument("--reason", required=True, help="why the outcome changes")
|
|
322
|
+
_add_run(reopen)
|
|
323
|
+
|
|
324
|
+
revisions = sub.add_parser("revisions", help="list the published revisions")
|
|
325
|
+
_add_common(revisions)
|
|
326
|
+
|
|
327
|
+
show = sub.add_parser("revision-show", help="print one published revision byte-for-byte")
|
|
328
|
+
_add_common(show)
|
|
329
|
+
show.add_argument("--revision", required=True, help="the revision id (<task-key>/revision-NNN)")
|
|
330
|
+
|
|
331
|
+
record = sub.add_parser("record", help="print the whole discussion record as fixed text")
|
|
332
|
+
_add_common(record)
|
|
333
|
+
|
|
334
|
+
sync = sub.add_parser("memory-sync", help="register published revisions the Memory Book does not hold yet")
|
|
335
|
+
_add_common(sync)
|
|
336
|
+
sync.add_argument("--revision", default="", help="only this revision")
|
|
337
|
+
sync.add_argument("--summary-file", default="",
|
|
338
|
+
help="a changed summary for --revision, after the Memory Book refused the last one")
|
|
339
|
+
|
|
340
|
+
brief = sub.add_parser("follow-up-brief", help="write one follow-up task's brief from a revision")
|
|
341
|
+
_add_common(brief)
|
|
342
|
+
brief.add_argument("--revision", required=True, help="the revision id the follow-up uses")
|
|
343
|
+
brief.add_argument("--follow-up", required=True, help="the follow-up label the requirements apply to")
|
|
344
|
+
_add_target(brief)
|
|
345
|
+
brief.add_argument("--target-project-root", default="",
|
|
346
|
+
help="the follow-up's project root (default: this discussion's project)")
|
|
347
|
+
|
|
348
|
+
links = sub.add_parser("links", help="list the linked follow-ups and their impact-review state")
|
|
349
|
+
_add_common(links)
|
|
350
|
+
|
|
351
|
+
changes = sub.add_parser("impact", help="print what changed between two revisions")
|
|
352
|
+
_add_common(changes)
|
|
353
|
+
changes.add_argument("--from", dest="from_revision", required=True, help="the earlier revision id")
|
|
354
|
+
changes.add_argument("--to", dest="to_revision", required=True, help="the later revision id")
|
|
355
|
+
changes.add_argument("--follow-up", default="", help="only the requirements of this follow-up")
|
|
356
|
+
|
|
357
|
+
adopt = sub.add_parser("apply", help="record the user's choice to adopt or keep the latest revision")
|
|
358
|
+
_add_common(adopt)
|
|
359
|
+
adopt.add_argument("--revision", required=True, help="the latest revision id")
|
|
360
|
+
_add_target(adopt)
|
|
361
|
+
adopt.add_argument("--choice", required=True, choices=followup.CHOICES)
|
|
362
|
+
|
|
363
|
+
amended = sub.add_parser("complete-amendment",
|
|
364
|
+
help="move a follow-up routed to plan amendment to the revision after its amended plan is approved")
|
|
365
|
+
_add_common(amended)
|
|
366
|
+
amended.add_argument("--revision", required=True, help="the revision the routed adoption named")
|
|
367
|
+
_add_target(amended)
|
|
368
|
+
|
|
369
|
+
for name, help_text in (
|
|
370
|
+
("question-prompt", "write the dispatch input of one direct question"),
|
|
371
|
+
("collect-answer", "record the addressee's answer to one direct question"),
|
|
372
|
+
):
|
|
373
|
+
command = sub.add_parser(name, help=help_text)
|
|
374
|
+
_add_common(command)
|
|
375
|
+
command.add_argument("--question", required=True, help="the question id (Q-NNNN)")
|
|
376
|
+
_add_run(command)
|
|
377
|
+
return parser
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _workflow(store: DiscussionStore, args: argparse.Namespace) -> dict:
|
|
381
|
+
run = workflow.load_run(store, args.run_manifest)
|
|
382
|
+
if args.command == "cycle-open":
|
|
383
|
+
return workflow.open_cycle(store, run)
|
|
384
|
+
if args.command == "settle":
|
|
385
|
+
return workflow.settle_cycle(store, run)
|
|
386
|
+
if args.command == "cycle-close":
|
|
387
|
+
return workflow.close_cycle(store, run)
|
|
388
|
+
if args.command == "recover":
|
|
389
|
+
return workflow.choose_recovery(store, run, args.failure, args.choice)
|
|
390
|
+
if args.command == "question-prompt":
|
|
391
|
+
return workflow.question_dispatch(store, run, args.question)
|
|
392
|
+
return workflow.collect_answer(store, run, args.question)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
_WORKFLOW_COMMANDS = frozenset({
|
|
396
|
+
"cycle-open", "settle", "cycle-close", "recover", "question-prompt", "collect-answer",
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _write(store: DiscussionStore, args: argparse.Namespace) -> tuple[dict, bool]:
|
|
401
|
+
source_run = resolve_source_run(store, args.run_manifest)
|
|
402
|
+
if args.command == "confirm-subject":
|
|
403
|
+
return append_record(store, "subject", {"subject": args.subject, "question": args.question},
|
|
404
|
+
author=USER, source_run=source_run)
|
|
405
|
+
if args.command == "ask":
|
|
406
|
+
return append_record(store, "question", {"to": USER, "text": args.text},
|
|
407
|
+
author=args.author, source_run=source_run)
|
|
408
|
+
if args.command == "address":
|
|
409
|
+
return append_record(store, "question", {"to": args.to, "text": args.text},
|
|
410
|
+
author=USER, source_run=source_run)
|
|
411
|
+
if args.command == "answer":
|
|
412
|
+
return append_record(store, "answer", {"questionId": args.question, "text": args.text},
|
|
413
|
+
author=args.author, source_run=source_run)
|
|
414
|
+
if args.command == "add-material":
|
|
415
|
+
return append_record(store, "material", {"materialKind": args.kind, "text": args.text},
|
|
416
|
+
author=args.author, source_run=source_run)
|
|
417
|
+
if args.command == "add-requirement":
|
|
418
|
+
return append_record(store, "requirement", {"appliesTo": args.applies_to, "text": args.text},
|
|
419
|
+
author=args.author, source_run=source_run)
|
|
420
|
+
if args.command == "add-scope":
|
|
421
|
+
return append_record(store, "scope", {"scopeKind": args.kind, "text": args.text},
|
|
422
|
+
author=args.author, source_run=source_run)
|
|
423
|
+
if args.command == "withdraw":
|
|
424
|
+
return append_record(store, "withdrawal", {"itemId": args.item, "reason": args.reason},
|
|
425
|
+
author=USER, source_run=source_run)
|
|
426
|
+
if args.command == "confirm-completion":
|
|
427
|
+
return confirm_completion(store, draft_version=args.draft_version, direction=args.direction,
|
|
428
|
+
risk_acknowledgement=args.risk, source_run=source_run)
|
|
429
|
+
if args.command == "reopen":
|
|
430
|
+
return append_record(store, "reopen", {"revisionId": args.revision, "reason": args.reason},
|
|
431
|
+
author=USER, source_run=source_run)
|
|
432
|
+
return append_record(store, "decision", {"questionId": args.question, "text": args.text},
|
|
433
|
+
author=USER, source_run=source_run)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def main(argv: list[str] | None = None) -> int:
|
|
437
|
+
args = build_parser().parse_args(argv)
|
|
438
|
+
|
|
439
|
+
def emit(payload: dict) -> None:
|
|
440
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
441
|
+
|
|
442
|
+
try:
|
|
443
|
+
project_root = Path(resolve_project_root(explicit_root=args.project_root, cwd=args.cwd))
|
|
444
|
+
identity = resolve_task_identity(project_root, args.task_key)
|
|
445
|
+
except (ResolverError, StateError) as exc:
|
|
446
|
+
emit({"ok": False, "stage": "resolve", "reason": str(exc)})
|
|
447
|
+
return 2
|
|
448
|
+
store = DiscussionStore(project_root, identity["taskKey"])
|
|
449
|
+
try:
|
|
450
|
+
if args.command == "draft":
|
|
451
|
+
print(render_draft_text(read_draft(store)), end="")
|
|
452
|
+
return 0
|
|
453
|
+
if args.command == "status":
|
|
454
|
+
emit({"ok": True, **workflow.status(read_draft(store)), "memoryRegistration": memory.views(store)})
|
|
455
|
+
return 0
|
|
456
|
+
if args.command == "memory-sync":
|
|
457
|
+
summary = _read_summary(args.summary_file) if args.summary_file else None
|
|
458
|
+
emit({"ok": True, "memory": memory.sync(store, okstra_cli=args.okstra_cli,
|
|
459
|
+
revision_id=args.revision, summary_text=summary)})
|
|
460
|
+
return 0
|
|
461
|
+
if args.command == "record":
|
|
462
|
+
print(render_full_record(store), end="")
|
|
463
|
+
return 0
|
|
464
|
+
if args.command == "revisions":
|
|
465
|
+
emit({"ok": True, "revisions": read_draft(store)["revisions"]})
|
|
466
|
+
return 0
|
|
467
|
+
if args.command == "revision-show":
|
|
468
|
+
_, data = read_revision(store, args.revision)
|
|
469
|
+
sys.stdout.write(data.decode("utf-8"))
|
|
470
|
+
return 0
|
|
471
|
+
if args.command in _FOLLOW_UP_COMMANDS:
|
|
472
|
+
emit({"ok": True, **_follow_up(store, args)})
|
|
473
|
+
return 0
|
|
474
|
+
if args.command in _WORKFLOW_COMMANDS:
|
|
475
|
+
emit({"ok": True, **_workflow(store, args)})
|
|
476
|
+
return 0
|
|
477
|
+
record, created = _write(store, args)
|
|
478
|
+
draft = read_draft(store)
|
|
479
|
+
except DiscussionStoreError as exc:
|
|
480
|
+
emit({"ok": False, "stage": args.command, "reason": str(exc)})
|
|
481
|
+
return 1
|
|
482
|
+
payload = {
|
|
483
|
+
"ok": True,
|
|
484
|
+
"created": created,
|
|
485
|
+
"record": record,
|
|
486
|
+
"pendingForUser": draft["pendingForUser"],
|
|
487
|
+
"pendingForParticipants": draft["pendingForParticipants"],
|
|
488
|
+
}
|
|
489
|
+
if args.command == "confirm-completion":
|
|
490
|
+
payload["memory"] = _register(store, record["revisionId"], args.okstra_cli)
|
|
491
|
+
emit(payload)
|
|
492
|
+
return 0
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _read_summary(path: str) -> str:
|
|
496
|
+
try:
|
|
497
|
+
return Path(path).read_text(encoding="utf-8")
|
|
498
|
+
except OSError as exc:
|
|
499
|
+
raise DiscussionStoreError(f"--summary-file {path} cannot be read: {exc}") from exc
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _register(store: DiscussionStore, revision_id: str, okstra_cli: str) -> dict:
|
|
503
|
+
"""확정본은 이미 발행됐다 — 등록 실패는 발행을 되돌리지 않고 `pending` 으로 남아 `memory-sync` 가 다시 부른다."""
|
|
504
|
+
try:
|
|
505
|
+
return memory.register_revision(store, revision_id, okstra_cli=okstra_cli)
|
|
506
|
+
except (DiscussionStoreError, OSError) as exc:
|
|
507
|
+
return {"revisionId": revision_id, "sourceKey": memory.source_key(revision_id), "status": memory.PENDING,
|
|
508
|
+
"reason": f"registration did not start: {exc}", "next": "okstra discussion memory-sync <task-key>"}
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
if __name__ == "__main__":
|
|
512
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""토론 시작 입력을 분류·검증하고, 그 입력으로 단계 소유 입력 문서를 만든다."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from okstra_ctl.prepare_error import PrepareError
|
|
8
|
+
|
|
9
|
+
from .store import (
|
|
10
|
+
USER,
|
|
11
|
+
DiscussionStore,
|
|
12
|
+
DiscussionStoreError,
|
|
13
|
+
SourceRun,
|
|
14
|
+
append_record,
|
|
15
|
+
completed_revision,
|
|
16
|
+
has_records,
|
|
17
|
+
read_draft,
|
|
18
|
+
render_draft_text,
|
|
19
|
+
same_task,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_URL = re.compile(r"^https?://\S+$")
|
|
23
|
+
_TASK_KEY = re.compile(r"^[^:\s]+:[^:\s]+:[^:\s]+$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _task_key(inp) -> str:
|
|
27
|
+
return f"{inp.project_id}:{inp.task_group}:{inp.task_id}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def classify_start_input(text: str, project_root: Path, task_key: str) -> tuple[str, str]:
|
|
31
|
+
"""호출자에게 종류를 묻지 않고 값의 모양으로 정한다 — URL·프로젝트 안 파일은 link,
|
|
32
|
+
task key 모양은 이 task 의 토론, 나머지는 description."""
|
|
33
|
+
value = text.strip()
|
|
34
|
+
if not value:
|
|
35
|
+
raise PrepareError(
|
|
36
|
+
"--discussion-start is empty — give a ticket or project link, a short "
|
|
37
|
+
"description, or this task's key to continue its discussion"
|
|
38
|
+
)
|
|
39
|
+
if _URL.match(value):
|
|
40
|
+
return "link", value
|
|
41
|
+
if _TASK_KEY.match(value):
|
|
42
|
+
if not same_task(value, task_key):
|
|
43
|
+
raise PrepareError(
|
|
44
|
+
f"--discussion-start names the discussion of {value}, not of {task_key}; "
|
|
45
|
+
"a discussion continues in its own task — start this task from a link "
|
|
46
|
+
"or a description instead"
|
|
47
|
+
)
|
|
48
|
+
if not has_records(DiscussionStore(project_root, task_key)):
|
|
49
|
+
raise PrepareError(
|
|
50
|
+
f"{task_key} has no discussion record to continue yet — start it from "
|
|
51
|
+
"a link or a description"
|
|
52
|
+
)
|
|
53
|
+
return "discussion", task_key
|
|
54
|
+
candidate = project_root / value
|
|
55
|
+
if "\n" not in value and candidate.is_file():
|
|
56
|
+
try:
|
|
57
|
+
return "link", candidate.resolve().relative_to(project_root.resolve()).as_posix()
|
|
58
|
+
except ValueError:
|
|
59
|
+
# expected-miss: 프로젝트 밖 파일은 링크가 아니라 설명으로 기록한다.
|
|
60
|
+
return "description", value
|
|
61
|
+
return "description", value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def validate_discussion_start(project_root: Path, inp) -> None:
|
|
65
|
+
"""prepare 의 어떤 쓰기보다 먼저 돈다 — 입력 오류가 부분 run 을 남기지 않게."""
|
|
66
|
+
if inp.task_type != "discussion":
|
|
67
|
+
if inp.discussion_start:
|
|
68
|
+
raise PrepareError(
|
|
69
|
+
"--discussion-start is only meaningful with --task-type discussion; "
|
|
70
|
+
f"got {inp.task_type}"
|
|
71
|
+
)
|
|
72
|
+
return
|
|
73
|
+
if str(inp.brief_path).strip() not in ("", "."):
|
|
74
|
+
raise PrepareError(
|
|
75
|
+
"--task-brief is not accepted for --task-type discussion — pass "
|
|
76
|
+
"--discussion-start <link | short description | this task's key>; "
|
|
77
|
+
"prepare writes the discussion input document itself"
|
|
78
|
+
)
|
|
79
|
+
task_key = _task_key(inp)
|
|
80
|
+
kind, _ = classify_start_input(inp.discussion_start, project_root, task_key)
|
|
81
|
+
try:
|
|
82
|
+
completed = completed_revision(DiscussionStore(project_root, task_key))
|
|
83
|
+
except DiscussionStoreError as exc:
|
|
84
|
+
raise PrepareError(str(exc)) from exc
|
|
85
|
+
if completed is not None and kind != "discussion":
|
|
86
|
+
raise PrepareError(
|
|
87
|
+
f"{task_key} is completed as {completed}; continue it with --discussion-start "
|
|
88
|
+
f"{task_key} and reopen it (`okstra discussion reopen`) to discuss a change"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def materialize_discussion_input(project_root: Path, inp, ctx: dict) -> None:
|
|
93
|
+
task_key = _task_key(inp)
|
|
94
|
+
kind, text = classify_start_input(inp.discussion_start, project_root, task_key)
|
|
95
|
+
store = DiscussionStore(project_root, task_key)
|
|
96
|
+
try:
|
|
97
|
+
if kind != "discussion":
|
|
98
|
+
append_record(
|
|
99
|
+
store, "start", {"startKind": kind, "text": text},
|
|
100
|
+
author=USER,
|
|
101
|
+
source_run=SourceRun(
|
|
102
|
+
manifest=str(ctx["RUN_MANIFEST_RELATIVE_PATH"]),
|
|
103
|
+
participants=frozenset(),
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
draft = read_draft(store)
|
|
107
|
+
except DiscussionStoreError as exc:
|
|
108
|
+
raise PrepareError(str(exc)) from exc
|
|
109
|
+
store.input_path.write_text(
|
|
110
|
+
"# Discussion input\n\n"
|
|
111
|
+
f"- Start input of this run: [{kind}] {text}\n"
|
|
112
|
+
f"- Run: `{ctx['RUN_MANIFEST_RELATIVE_PATH']}`\n\n"
|
|
113
|
+
+ render_draft_text(draft),
|
|
114
|
+
encoding="utf-8",
|
|
115
|
+
)
|
|
116
|
+
inp.brief_path = store.input_path
|