@grifhinz/logics-manager 2.0.4 → 2.1.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 +164 -157
- package/VERSION +1 -1
- package/logics_manager/assist.py +366 -0
- package/logics_manager/cli.py +116 -38
- package/logics_manager/flow.py +580 -8
- package/logics_manager/mcp.py +1188 -0
- package/logics_manager/sync.py +613 -0
- package/logics_manager/termstyle.py +75 -0
- package/package.json +10 -1
- package/pyproject.toml +1 -1
package/logics_manager/flow.py
CHANGED
|
@@ -7,6 +7,8 @@ from dataclasses import dataclass
|
|
|
7
7
|
from datetime import date
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
from .termstyle import colorize_help
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
@dataclass(frozen=True)
|
|
12
14
|
class DocKind:
|
|
@@ -33,6 +35,519 @@ STATUS_BY_KIND_DEFAULT = {
|
|
|
33
35
|
"backlog": "Ready",
|
|
34
36
|
"task": "Ready",
|
|
35
37
|
}
|
|
38
|
+
HELP_FLAGS = ("-h", "--help")
|
|
39
|
+
LIST_KIND_CHOICES = ("all", "request", "backlog", "task")
|
|
40
|
+
ACTIVE_FLOW_STATUSES = {"draft", "ready", "in progress", "blocked"}
|
|
41
|
+
FLOW_KIND_ORDER = {"request": 0, "backlog": 1, "task": 2}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _help_requested(argv: list[str], index: int) -> bool:
|
|
45
|
+
return len(argv) <= index or argv[index] in HELP_FLAGS
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _format_flag_list(flags: list[str]) -> str:
|
|
49
|
+
return ", ".join(flags)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _normalize_status(value: str | None) -> str:
|
|
53
|
+
return " ".join(value.split()).lower() if value else ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _is_active_flow_doc(status: str | None) -> bool:
|
|
57
|
+
return _normalize_status(status) in ACTIVE_FLOW_STATUSES
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class FlowListEntry:
|
|
62
|
+
kind: str
|
|
63
|
+
path: Path
|
|
64
|
+
ref: str
|
|
65
|
+
title: str
|
|
66
|
+
status: str | None
|
|
67
|
+
progress: str | None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _parse_flow_doc(path: Path, kind: str) -> FlowListEntry:
|
|
71
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
72
|
+
ref = path.stem
|
|
73
|
+
title = _extract_doc_title(path)
|
|
74
|
+
status: str | None = None
|
|
75
|
+
progress: str | None = None
|
|
76
|
+
|
|
77
|
+
for line in lines:
|
|
78
|
+
if line.startswith("> Status:"):
|
|
79
|
+
status = line.split(":", 1)[1].strip()
|
|
80
|
+
continue
|
|
81
|
+
if line.startswith("> Progress:"):
|
|
82
|
+
progress = line.split(":", 1)[1].strip()
|
|
83
|
+
|
|
84
|
+
return FlowListEntry(
|
|
85
|
+
kind=kind,
|
|
86
|
+
path=path,
|
|
87
|
+
ref=ref,
|
|
88
|
+
title=title,
|
|
89
|
+
status=status,
|
|
90
|
+
progress=progress,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _collect_flow_list_entries(repo_root: Path, kind_filter: str = "all") -> list[FlowListEntry]:
|
|
95
|
+
entries: list[FlowListEntry] = []
|
|
96
|
+
for kind, doc_kind in DOC_KINDS.items():
|
|
97
|
+
if kind_filter != "all" and kind_filter != kind:
|
|
98
|
+
continue
|
|
99
|
+
directory = repo_root / doc_kind.directory
|
|
100
|
+
if not directory.is_dir():
|
|
101
|
+
continue
|
|
102
|
+
for path in sorted(directory.glob("*.md")):
|
|
103
|
+
entry = _parse_flow_doc(path, kind)
|
|
104
|
+
if _is_active_flow_doc(entry.status):
|
|
105
|
+
entries.append(entry)
|
|
106
|
+
entries.sort(key=lambda entry: (FLOW_KIND_ORDER.get(entry.kind, 99), _normalize_status(entry.status), entry.ref))
|
|
107
|
+
return entries
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _render_flow_list_section(title: str, entries: list[FlowListEntry], out_dir: Path) -> str:
|
|
111
|
+
lines: list[str] = [f"## {title}", ""]
|
|
112
|
+
if not entries:
|
|
113
|
+
lines.append("_None_")
|
|
114
|
+
lines.append("")
|
|
115
|
+
return "\n".join(lines)
|
|
116
|
+
|
|
117
|
+
lines.extend(["| Doc | Title | Status | Progress | Path |", "|---|---|---|---|---|"])
|
|
118
|
+
for entry in entries:
|
|
119
|
+
rel = entry.path.relative_to(out_dir).as_posix()
|
|
120
|
+
doc_link = f"[{entry.ref}]({rel})"
|
|
121
|
+
lines.append(f"| {doc_link} | {entry.title} | {entry.status or ''} | {entry.progress or ''} | {rel} |")
|
|
122
|
+
lines.append("")
|
|
123
|
+
return "\n".join(lines)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def flow_list_payload(repo_root: Path, *, kind: str = "all") -> dict[str, object]:
|
|
127
|
+
repo_root = repo_root.resolve()
|
|
128
|
+
entries = _collect_flow_list_entries(repo_root, kind_filter=kind)
|
|
129
|
+
by_kind: dict[str, list[FlowListEntry]] = {key: [] for key in ("request", "backlog", "task")}
|
|
130
|
+
for entry in entries:
|
|
131
|
+
by_kind[entry.kind].append(entry)
|
|
132
|
+
counts = {key: len(values) for key, values in by_kind.items()}
|
|
133
|
+
return {
|
|
134
|
+
"ok": True,
|
|
135
|
+
"kind": kind,
|
|
136
|
+
"count": len(entries),
|
|
137
|
+
"counts_by_kind": counts,
|
|
138
|
+
"entries": [
|
|
139
|
+
{
|
|
140
|
+
"kind": entry.kind,
|
|
141
|
+
"ref": entry.ref,
|
|
142
|
+
"title": entry.title,
|
|
143
|
+
"status": entry.status,
|
|
144
|
+
"progress": entry.progress,
|
|
145
|
+
"path": entry.path.relative_to(repo_root).as_posix(),
|
|
146
|
+
}
|
|
147
|
+
for entry in entries
|
|
148
|
+
],
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def render_flow_list(repo_root: Path, *, kind: str = "all", output_format: str = "text") -> str:
|
|
153
|
+
payload = flow_list_payload(repo_root, kind=kind)
|
|
154
|
+
if output_format == "json":
|
|
155
|
+
return json.dumps(payload, indent=2, sort_keys=True)
|
|
156
|
+
|
|
157
|
+
entries = [
|
|
158
|
+
FlowListEntry(
|
|
159
|
+
kind=str(item["kind"]),
|
|
160
|
+
path=repo_root / str(item["path"]),
|
|
161
|
+
ref=str(item["ref"]),
|
|
162
|
+
title=str(item["title"]),
|
|
163
|
+
status=item["status"],
|
|
164
|
+
progress=item["progress"],
|
|
165
|
+
)
|
|
166
|
+
for item in payload["entries"]
|
|
167
|
+
]
|
|
168
|
+
if not entries:
|
|
169
|
+
return "Flow docs in progress: 0\n\n_None_"
|
|
170
|
+
|
|
171
|
+
sections: list[str] = [f"Flow docs in progress: {payload['count']}", ""]
|
|
172
|
+
kind_titles = {"request": "Requests", "backlog": "Backlog", "task": "Tasks"}
|
|
173
|
+
for key in ("request", "backlog", "task"):
|
|
174
|
+
if kind != "all" and kind != key:
|
|
175
|
+
continue
|
|
176
|
+
section_entries = [entry for entry in entries if entry.kind == key]
|
|
177
|
+
sections.append(_render_flow_list_section(f"{kind_titles[key]} ({len(section_entries)})", section_entries, repo_root))
|
|
178
|
+
return "\n".join(sections).rstrip()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _build_help() -> str:
|
|
182
|
+
return "\n".join(
|
|
183
|
+
[
|
|
184
|
+
"Logics Flow CLI",
|
|
185
|
+
"Create workflow docs with stable IDs, templates, and transitions.",
|
|
186
|
+
"",
|
|
187
|
+
"Usage:",
|
|
188
|
+
" logics-manager flow <command> [args...]",
|
|
189
|
+
"",
|
|
190
|
+
"Commands:",
|
|
191
|
+
" new <request|backlog|task>",
|
|
192
|
+
" Create a new doc from a template.",
|
|
193
|
+
" Common flags: --title, --slug, --from-version, --understanding, --confidence, --status, --complexity, --theme, --progress, --format {text,json}, --dry-run",
|
|
194
|
+
" Request-only flags: --fixture, --smoke-test",
|
|
195
|
+
" Backlog/task-only flags: --auto-create-product-brief, --auto-create-adr",
|
|
196
|
+
"",
|
|
197
|
+
" list",
|
|
198
|
+
" List workflow docs that are still active.",
|
|
199
|
+
" Flags: --kind {all,request,backlog,task}, --format {text,json}",
|
|
200
|
+
"",
|
|
201
|
+
" companion <product|architecture>",
|
|
202
|
+
" Create a companion doc from the integrated runtime.",
|
|
203
|
+
" Flags: --title, --source-ref, --request-ref, --backlog-ref, --task-ref, --format {text,json}, --dry-run",
|
|
204
|
+
"",
|
|
205
|
+
" promote request-to-backlog <source>",
|
|
206
|
+
" Create a backlog slice from a request.",
|
|
207
|
+
"",
|
|
208
|
+
" promote backlog-to-task <source>",
|
|
209
|
+
" Create a task from a backlog item.",
|
|
210
|
+
"",
|
|
211
|
+
" split request <source>",
|
|
212
|
+
" Split a request into multiple backlog items.",
|
|
213
|
+
" Flags: --title (repeatable), plus the common backlog flags above.",
|
|
214
|
+
"",
|
|
215
|
+
" split backlog <source>",
|
|
216
|
+
" Split a backlog item into multiple tasks.",
|
|
217
|
+
" Flags: --title (repeatable), plus the common task flags above.",
|
|
218
|
+
"",
|
|
219
|
+
" close <request|backlog|task> <source>",
|
|
220
|
+
" Close a doc and propagate transitions.",
|
|
221
|
+
" Flags: --format {text,json}, --dry-run",
|
|
222
|
+
"",
|
|
223
|
+
" finish task <source>",
|
|
224
|
+
" Finish a task and verify the closure chain.",
|
|
225
|
+
" Flags: --format {text,json}, --dry-run",
|
|
226
|
+
"",
|
|
227
|
+
"Examples:",
|
|
228
|
+
' logics-manager flow new request --title "My request"',
|
|
229
|
+
" logics-manager flow promote request-to-backlog req_001_my_request",
|
|
230
|
+
" logics-manager flow close task task_003_fix_docs --dry-run",
|
|
231
|
+
]
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _build_new_help() -> str:
|
|
236
|
+
return "\n".join(
|
|
237
|
+
[
|
|
238
|
+
"Logics Flow New",
|
|
239
|
+
"Create a new workflow doc from a template.",
|
|
240
|
+
"",
|
|
241
|
+
"Usage:",
|
|
242
|
+
" logics-manager flow new <request|backlog|task> [args...]",
|
|
243
|
+
"",
|
|
244
|
+
"Kinds:",
|
|
245
|
+
" request",
|
|
246
|
+
" Generates a request doc.",
|
|
247
|
+
" Flags: --title, --slug, --fixture, --smoke-test, --from-version, --understanding, --confidence, --status, --complexity, --theme, --format {text,json}, --dry-run",
|
|
248
|
+
" backlog",
|
|
249
|
+
" Generates a backlog doc.",
|
|
250
|
+
" Flags: --title, --slug, --from-version, --understanding, --confidence, --status, --complexity, --theme, --progress, --auto-create-product-brief, --auto-create-adr, --format {text,json}, --dry-run",
|
|
251
|
+
" task",
|
|
252
|
+
" Generates a task doc.",
|
|
253
|
+
" Flags: --title, --slug, --from-version, --understanding, --confidence, --status, --complexity, --theme, --progress, --auto-create-product-brief, --auto-create-adr, --format {text,json}, --dry-run",
|
|
254
|
+
"",
|
|
255
|
+
"Examples:",
|
|
256
|
+
' logics-manager flow new request --title "Capture migration risks"',
|
|
257
|
+
' logics-manager flow new backlog --title "Break work into slices"',
|
|
258
|
+
' logics-manager flow new task --title "Implement the parser"',
|
|
259
|
+
]
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _build_new_kind_help(kind: str) -> str:
|
|
264
|
+
if kind == "request":
|
|
265
|
+
kind_title = "Request"
|
|
266
|
+
flags = ["--title", "--slug", "--fixture", "--smoke-test", "--from-version", "--understanding", "--confidence", "--status", "--complexity", "--theme", "--format {text,json}", "--dry-run"]
|
|
267
|
+
examples = [' logics-manager flow new request --title "Capture migration risks"']
|
|
268
|
+
elif kind == "backlog":
|
|
269
|
+
kind_title = "Backlog"
|
|
270
|
+
flags = ["--title", "--slug", "--from-version", "--understanding", "--confidence", "--status", "--complexity", "--theme", "--progress", "--auto-create-product-brief", "--auto-create-adr", "--format {text,json}", "--dry-run"]
|
|
271
|
+
examples = [' logics-manager flow new backlog --title "Break work into slices"']
|
|
272
|
+
else:
|
|
273
|
+
kind_title = "Task"
|
|
274
|
+
flags = ["--title", "--slug", "--from-version", "--understanding", "--confidence", "--status", "--complexity", "--theme", "--progress", "--auto-create-product-brief", "--auto-create-adr", "--format {text,json}", "--dry-run"]
|
|
275
|
+
examples = [' logics-manager flow new task --title "Implement the parser"']
|
|
276
|
+
return "\n".join(
|
|
277
|
+
[
|
|
278
|
+
f"Logics Flow New {kind_title}",
|
|
279
|
+
f"Create a new {kind.lower()} doc.",
|
|
280
|
+
"",
|
|
281
|
+
"Usage:",
|
|
282
|
+
f" logics-manager flow new {kind} [args...]",
|
|
283
|
+
"",
|
|
284
|
+
"Flags:",
|
|
285
|
+
f" {_format_flag_list(flags)}",
|
|
286
|
+
"",
|
|
287
|
+
"Examples:",
|
|
288
|
+
*examples,
|
|
289
|
+
]
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _build_list_help() -> str:
|
|
294
|
+
return "\n".join(
|
|
295
|
+
[
|
|
296
|
+
"Logics Flow List",
|
|
297
|
+
"List workflow docs that are still active.",
|
|
298
|
+
"",
|
|
299
|
+
"Usage:",
|
|
300
|
+
" logics-manager flow list [args...]",
|
|
301
|
+
"",
|
|
302
|
+
"Flags:",
|
|
303
|
+
" --kind {all,request,backlog,task}",
|
|
304
|
+
" --format {text,json}",
|
|
305
|
+
"",
|
|
306
|
+
"Examples:",
|
|
307
|
+
" logics-manager flow list",
|
|
308
|
+
" logics-manager flow list --kind backlog",
|
|
309
|
+
]
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _build_companion_help() -> str:
|
|
314
|
+
return "\n".join(
|
|
315
|
+
[
|
|
316
|
+
"Logics Flow Companion",
|
|
317
|
+
"Create a companion doc from the integrated runtime.",
|
|
318
|
+
"",
|
|
319
|
+
"Usage:",
|
|
320
|
+
" logics-manager flow companion <product|architecture> [args...]",
|
|
321
|
+
"",
|
|
322
|
+
"Kinds:",
|
|
323
|
+
" product",
|
|
324
|
+
" Create a product companion doc.",
|
|
325
|
+
" Flags: --title, --source-ref, --request-ref, --backlog-ref, --task-ref, --format {text,json}, --dry-run",
|
|
326
|
+
" architecture",
|
|
327
|
+
" Create an architecture companion doc.",
|
|
328
|
+
" Flags: --title, --source-ref, --request-ref, --backlog-ref, --task-ref, --format {text,json}, --dry-run",
|
|
329
|
+
"",
|
|
330
|
+
"Examples:",
|
|
331
|
+
' logics-manager flow companion product --title "Product note"',
|
|
332
|
+
' logics-manager flow companion architecture --title "Architecture note"',
|
|
333
|
+
]
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _build_companion_kind_help(kind: str) -> str:
|
|
338
|
+
return "\n".join(
|
|
339
|
+
[
|
|
340
|
+
f"Logics Flow Companion {kind.title()}",
|
|
341
|
+
f"Create an {kind} companion doc from the integrated runtime.",
|
|
342
|
+
"",
|
|
343
|
+
"Usage:",
|
|
344
|
+
f" logics-manager flow companion {kind} [args...]",
|
|
345
|
+
"",
|
|
346
|
+
"Flags:",
|
|
347
|
+
" --title, --source-ref, --request-ref, --backlog-ref, --task-ref, --format {text,json}, --dry-run",
|
|
348
|
+
"",
|
|
349
|
+
"Examples:",
|
|
350
|
+
f' logics-manager flow companion {kind} --title "{kind.title()} note"',
|
|
351
|
+
]
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _build_promote_help() -> str:
|
|
356
|
+
return "\n".join(
|
|
357
|
+
[
|
|
358
|
+
"Logics Flow Promote",
|
|
359
|
+
"Promote between workflow stages.",
|
|
360
|
+
"",
|
|
361
|
+
"Usage:",
|
|
362
|
+
" logics-manager flow promote <request-to-backlog|backlog-to-task> <source> [args...]",
|
|
363
|
+
"",
|
|
364
|
+
"Commands:",
|
|
365
|
+
" request-to-backlog <source>",
|
|
366
|
+
" Create a backlog slice from a request.",
|
|
367
|
+
" Flags: --from-version, --understanding, --confidence, --status, --complexity, --theme, --progress, --auto-create-product-brief, --auto-create-adr, --format {text,json}, --dry-run",
|
|
368
|
+
" backlog-to-task <source>",
|
|
369
|
+
" Create a task from a backlog item.",
|
|
370
|
+
" Flags: --from-version, --understanding, --confidence, --status, --complexity, --theme, --progress, --auto-create-product-brief, --auto-create-adr, --format {text,json}, --dry-run",
|
|
371
|
+
"",
|
|
372
|
+
"Examples:",
|
|
373
|
+
" logics-manager flow promote request-to-backlog req_001_capture_migration_risks",
|
|
374
|
+
" logics-manager flow promote backlog-to-task item_002_break_work_into_slices",
|
|
375
|
+
]
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _build_promote_variant_help(promotion: str) -> str:
|
|
380
|
+
if promotion == "request-to-backlog":
|
|
381
|
+
title = "Request to Backlog"
|
|
382
|
+
summary = "Create a backlog slice from a request."
|
|
383
|
+
usage = " logics-manager flow promote request-to-backlog <source> [args...]"
|
|
384
|
+
example = " logics-manager flow promote request-to-backlog req_001_capture_migration_risks"
|
|
385
|
+
else:
|
|
386
|
+
title = "Backlog to Task"
|
|
387
|
+
summary = "Create a task from a backlog item."
|
|
388
|
+
usage = " logics-manager flow promote backlog-to-task <source> [args...]"
|
|
389
|
+
example = " logics-manager flow promote backlog-to-task item_002_break_work_into_slices"
|
|
390
|
+
return "\n".join(
|
|
391
|
+
[
|
|
392
|
+
f"Logics Flow Promote {title}",
|
|
393
|
+
summary,
|
|
394
|
+
"",
|
|
395
|
+
"Usage:",
|
|
396
|
+
usage,
|
|
397
|
+
"",
|
|
398
|
+
"Flags:",
|
|
399
|
+
" --from-version, --understanding, --confidence, --status, --complexity, --theme, --progress, --auto-create-product-brief, --auto-create-adr, --format {text,json}, --dry-run",
|
|
400
|
+
"",
|
|
401
|
+
"Example:",
|
|
402
|
+
example,
|
|
403
|
+
]
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _build_split_help() -> str:
|
|
408
|
+
return "\n".join(
|
|
409
|
+
[
|
|
410
|
+
"Logics Flow Split",
|
|
411
|
+
"Split a request or backlog into bounded children.",
|
|
412
|
+
"",
|
|
413
|
+
"Usage:",
|
|
414
|
+
" logics-manager flow split <request|backlog> <source> [args...]",
|
|
415
|
+
"",
|
|
416
|
+
"Commands:",
|
|
417
|
+
" request <source>",
|
|
418
|
+
" Split a request into multiple backlog items.",
|
|
419
|
+
" Flags: --title (repeatable), --from-version, --understanding, --confidence, --status, --complexity, --theme, --progress, --auto-create-product-brief, --auto-create-adr, --format {text,json}, --dry-run",
|
|
420
|
+
" backlog <source>",
|
|
421
|
+
" Split a backlog item into multiple tasks.",
|
|
422
|
+
" Flags: --title (repeatable), --from-version, --understanding, --confidence, --status, --complexity, --theme, --progress, --auto-create-product-brief, --auto-create-adr, --format {text,json}, --dry-run",
|
|
423
|
+
"",
|
|
424
|
+
"Examples:",
|
|
425
|
+
" logics-manager flow split request req_001_capture_migration_risks --title \"Slice 1\" --title \"Slice 2\"",
|
|
426
|
+
" logics-manager flow split backlog item_002_break_work_into_slices --title \"Task 1\" --title \"Task 2\"",
|
|
427
|
+
]
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _build_split_variant_help(split_kind: str) -> str:
|
|
432
|
+
if split_kind == "request":
|
|
433
|
+
title = "Request"
|
|
434
|
+
summary = "Split a request into multiple backlog items."
|
|
435
|
+
usage = " logics-manager flow split request <source> [args...]"
|
|
436
|
+
example = ' logics-manager flow split request req_001_capture_migration_risks --title "Slice 1" --title "Slice 2"'
|
|
437
|
+
else:
|
|
438
|
+
title = "Backlog"
|
|
439
|
+
summary = "Split a backlog item into multiple tasks."
|
|
440
|
+
usage = " logics-manager flow split backlog <source> [args...]"
|
|
441
|
+
example = ' logics-manager flow split backlog item_002_break_work_into_slices --title "Task 1" --title "Task 2"'
|
|
442
|
+
return "\n".join(
|
|
443
|
+
[
|
|
444
|
+
f"Logics Flow Split {title}",
|
|
445
|
+
summary,
|
|
446
|
+
"",
|
|
447
|
+
"Usage:",
|
|
448
|
+
usage,
|
|
449
|
+
"",
|
|
450
|
+
"Flags:",
|
|
451
|
+
" --title (repeatable), --from-version, --understanding, --confidence, --status, --complexity, --theme, --progress, --auto-create-product-brief, --auto-create-adr, --format {text,json}, --dry-run",
|
|
452
|
+
"",
|
|
453
|
+
"Example:",
|
|
454
|
+
example,
|
|
455
|
+
]
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _build_close_help() -> str:
|
|
460
|
+
return "\n".join(
|
|
461
|
+
[
|
|
462
|
+
"Logics Flow Close",
|
|
463
|
+
"Close a request, backlog item, or task and propagate transitions.",
|
|
464
|
+
"",
|
|
465
|
+
"Usage:",
|
|
466
|
+
" logics-manager flow close <request|backlog|task> <source> [args...]",
|
|
467
|
+
"",
|
|
468
|
+
"Kinds:",
|
|
469
|
+
" request",
|
|
470
|
+
" Close a request doc.",
|
|
471
|
+
" Flags: --format {text,json}, --dry-run",
|
|
472
|
+
" backlog",
|
|
473
|
+
" Close a backlog doc.",
|
|
474
|
+
" Flags: --format {text,json}, --dry-run",
|
|
475
|
+
" task",
|
|
476
|
+
" Close a task doc.",
|
|
477
|
+
" Flags: --format {text,json}, --dry-run",
|
|
478
|
+
"",
|
|
479
|
+
"Examples:",
|
|
480
|
+
" logics-manager flow close request req_001_capture_migration_risks",
|
|
481
|
+
" logics-manager flow close task task_003_fix_docs --dry-run",
|
|
482
|
+
]
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _build_close_kind_help(kind: str) -> str:
|
|
487
|
+
example = {
|
|
488
|
+
"request": " logics-manager flow close request req_001_capture_migration_risks",
|
|
489
|
+
"backlog": " logics-manager flow close backlog item_002_break_work_into_slices",
|
|
490
|
+
"task": " logics-manager flow close task task_003_fix_docs",
|
|
491
|
+
}[kind]
|
|
492
|
+
return "\n".join(
|
|
493
|
+
[
|
|
494
|
+
f"Logics Flow Close {kind.title()}",
|
|
495
|
+
f"Close a {kind} doc and propagate transitions.",
|
|
496
|
+
"",
|
|
497
|
+
"Usage:",
|
|
498
|
+
f" logics-manager flow close {kind} <source> [args...]",
|
|
499
|
+
"",
|
|
500
|
+
"Flags:",
|
|
501
|
+
" --format {text,json}",
|
|
502
|
+
" --dry-run",
|
|
503
|
+
"",
|
|
504
|
+
"Example:",
|
|
505
|
+
example,
|
|
506
|
+
]
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _build_finish_help() -> str:
|
|
511
|
+
return "\n".join(
|
|
512
|
+
[
|
|
513
|
+
"Logics Flow Finish",
|
|
514
|
+
"Finish a task and verify the closure chain.",
|
|
515
|
+
"",
|
|
516
|
+
"Usage:",
|
|
517
|
+
" logics-manager flow finish task <source> [args...]",
|
|
518
|
+
"",
|
|
519
|
+
"Commands:",
|
|
520
|
+
" task <source>",
|
|
521
|
+
" Finish a task.",
|
|
522
|
+
" Flags: --format {text,json}, --dry-run",
|
|
523
|
+
"",
|
|
524
|
+
"Examples:",
|
|
525
|
+
" logics-manager flow finish task task_003_fix_docs",
|
|
526
|
+
]
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _build_finish_kind_help(kind: str) -> str:
|
|
531
|
+
return "\n".join(
|
|
532
|
+
[
|
|
533
|
+
f"Logics Flow Finish {kind.title()}",
|
|
534
|
+
f"Finish a {kind} and verify the closure chain.",
|
|
535
|
+
"",
|
|
536
|
+
"Usage:",
|
|
537
|
+
f" logics-manager flow finish {kind} <source> [args...]",
|
|
538
|
+
"",
|
|
539
|
+
"Flags:",
|
|
540
|
+
" --format {text,json}",
|
|
541
|
+
" --dry-run",
|
|
542
|
+
"",
|
|
543
|
+
"Example:",
|
|
544
|
+
" logics-manager flow finish task task_003_fix_docs",
|
|
545
|
+
]
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _print_help(text: str) -> None:
|
|
550
|
+
print(colorize_help(text))
|
|
36
551
|
|
|
37
552
|
|
|
38
553
|
def _split_titles(raw_titles: list[str]) -> list[str]:
|
|
@@ -887,6 +1402,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
887
1402
|
_add_common_doc_args(kind_parser, kind)
|
|
888
1403
|
kind_parser.set_defaults(func=cmd_new)
|
|
889
1404
|
|
|
1405
|
+
list_parser = sub.add_parser("list", help="List workflow docs that are still active.")
|
|
1406
|
+
list_parser.add_argument("--kind", choices=LIST_KIND_CHOICES, default="all")
|
|
1407
|
+
list_parser.add_argument("--format", choices=("text", "json"), default="text")
|
|
1408
|
+
list_parser.set_defaults(func=cmd_list)
|
|
1409
|
+
|
|
890
1410
|
companion_parser = sub.add_parser("companion", help="Create a companion doc from the integrated runtime.")
|
|
891
1411
|
companion_sub = companion_parser.add_subparsers(dest="kind", required=True)
|
|
892
1412
|
for kind in ("product", "architecture"):
|
|
@@ -1024,6 +1544,13 @@ def cmd_new(args: argparse.Namespace) -> dict[str, object]:
|
|
|
1024
1544
|
return payload
|
|
1025
1545
|
|
|
1026
1546
|
|
|
1547
|
+
def cmd_list(args: argparse.Namespace) -> dict[str, object]:
|
|
1548
|
+
repo_root = _find_repo_root(Path.cwd())
|
|
1549
|
+
payload = flow_list_payload(repo_root, kind=args.kind)
|
|
1550
|
+
print(render_flow_list(repo_root, kind=args.kind, output_format=args.format))
|
|
1551
|
+
return payload
|
|
1552
|
+
|
|
1553
|
+
|
|
1027
1554
|
def cmd_companion(args: argparse.Namespace) -> dict[str, object]:
|
|
1028
1555
|
repo_root = _find_repo_root(Path.cwd())
|
|
1029
1556
|
request_ref, backlog_ref, task_ref = _resolve_workflow_refs_for_companion(
|
|
@@ -1341,18 +1868,18 @@ def _record_finished_task_follow_up(repo_root: Path, task_path: Path, dry_run: b
|
|
|
1341
1868
|
_append_section_bullets(
|
|
1342
1869
|
item_path,
|
|
1343
1870
|
"Notes",
|
|
1344
|
-
[f"
|
|
1871
|
+
[f"Task `{task_ref}` was finished via `logics-manager flow finish task` on {date.today().isoformat()}."],
|
|
1345
1872
|
dry_run,
|
|
1346
1873
|
)
|
|
1347
1874
|
|
|
1348
1875
|
validation_bullets = [
|
|
1349
|
-
f"
|
|
1350
|
-
"
|
|
1876
|
+
f"Finish workflow executed on {date.today().isoformat()}.",
|
|
1877
|
+
"Linked backlog/request close verification passed.",
|
|
1351
1878
|
]
|
|
1352
1879
|
report_bullets = [
|
|
1353
|
-
f"
|
|
1354
|
-
f"
|
|
1355
|
-
f"
|
|
1880
|
+
f"Finished on {date.today().isoformat()}.",
|
|
1881
|
+
f"Linked backlog item(s): {', '.join(f'`{ref}`' for ref in item_refs) if item_refs else '(none)'}",
|
|
1882
|
+
f"Related request(s): {', '.join(f'`{ref}`' for ref in sorted(request_refs)) if request_refs else '(none)'}",
|
|
1356
1883
|
]
|
|
1357
1884
|
_append_section_bullets(task_path, "Validation", validation_bullets, dry_run)
|
|
1358
1885
|
_append_section_bullets(task_path, "Report", report_bullets, dry_run)
|
|
@@ -1424,7 +1951,10 @@ def cmd_finish_task(args: argparse.Namespace) -> dict[str, object]:
|
|
|
1424
1951
|
|
|
1425
1952
|
if args.dry_run:
|
|
1426
1953
|
payload = {"command": "finish", "kind": "task", "source": source_path.relative_to(repo_root).as_posix(), "dry_run": True}
|
|
1427
|
-
|
|
1954
|
+
if args.format == "json":
|
|
1955
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
1956
|
+
else:
|
|
1957
|
+
print("Dry run: skipped post-close verification.")
|
|
1428
1958
|
return payload
|
|
1429
1959
|
|
|
1430
1960
|
issues = _verify_finished_task_chain(repo_root, source_path)
|
|
@@ -1441,9 +1971,51 @@ def cmd_finish_task(args: argparse.Namespace) -> dict[str, object]:
|
|
|
1441
1971
|
|
|
1442
1972
|
|
|
1443
1973
|
def main(argv: list[str]) -> int:
|
|
1974
|
+
if not argv or argv[0] in HELP_FLAGS:
|
|
1975
|
+
_print_help(_build_help())
|
|
1976
|
+
return 0
|
|
1977
|
+
if argv[0] == "new" and _help_requested(argv, 1):
|
|
1978
|
+
_print_help(_build_new_help())
|
|
1979
|
+
return 0
|
|
1980
|
+
if argv[0] == "new" and len(argv) > 1 and argv[1] in DOC_KINDS and _help_requested(argv, 2):
|
|
1981
|
+
_print_help(_build_new_kind_help(argv[1]))
|
|
1982
|
+
return 0
|
|
1983
|
+
if argv[0] == "list" and _help_requested(argv, 1):
|
|
1984
|
+
_print_help(_build_list_help())
|
|
1985
|
+
return 0
|
|
1986
|
+
if argv[0] == "companion" and _help_requested(argv, 1):
|
|
1987
|
+
_print_help(_build_companion_help())
|
|
1988
|
+
return 0
|
|
1989
|
+
if argv[0] == "companion" and len(argv) > 1 and argv[1] in {"product", "architecture"} and _help_requested(argv, 2):
|
|
1990
|
+
_print_help(_build_companion_kind_help(argv[1]))
|
|
1991
|
+
return 0
|
|
1992
|
+
if argv[0] == "promote" and _help_requested(argv, 1):
|
|
1993
|
+
_print_help(_build_promote_help())
|
|
1994
|
+
return 0
|
|
1995
|
+
if argv[0] == "promote" and len(argv) > 1 and argv[1] in {"request-to-backlog", "backlog-to-task"} and _help_requested(argv, 2):
|
|
1996
|
+
_print_help(_build_promote_variant_help(argv[1]))
|
|
1997
|
+
return 0
|
|
1998
|
+
if argv[0] == "split" and _help_requested(argv, 1):
|
|
1999
|
+
_print_help(_build_split_help())
|
|
2000
|
+
return 0
|
|
2001
|
+
if argv[0] == "split" and len(argv) > 1 and argv[1] in {"request", "backlog"} and _help_requested(argv, 2):
|
|
2002
|
+
_print_help(_build_split_variant_help(argv[1]))
|
|
2003
|
+
return 0
|
|
2004
|
+
if argv[0] == "close" and _help_requested(argv, 1):
|
|
2005
|
+
_print_help(_build_close_help())
|
|
2006
|
+
return 0
|
|
2007
|
+
if argv[0] == "close" and len(argv) > 1 and argv[1] in {"request", "backlog", "task"} and _help_requested(argv, 2):
|
|
2008
|
+
_print_help(_build_close_kind_help(argv[1]))
|
|
2009
|
+
return 0
|
|
2010
|
+
if argv[0] == "finish" and _help_requested(argv, 1):
|
|
2011
|
+
_print_help(_build_finish_help())
|
|
2012
|
+
return 0
|
|
2013
|
+
if argv[0] == "finish" and len(argv) > 1 and argv[1] == "task" and _help_requested(argv, 2):
|
|
2014
|
+
_print_help(_build_finish_kind_help(argv[1]))
|
|
2015
|
+
return 0
|
|
1444
2016
|
parser = build_parser()
|
|
1445
2017
|
args = parser.parse_args(argv)
|
|
1446
|
-
if args.command not in {"new", "companion", "promote", "split", "close", "finish"}:
|
|
2018
|
+
if args.command not in {"new", "list", "companion", "promote", "split", "close", "finish"}:
|
|
1447
2019
|
raise SystemExit("Unsupported flow subcommand for the native CLI slice.")
|
|
1448
2020
|
payload = args.func(args)
|
|
1449
2021
|
return 0 if isinstance(payload, dict) else 1
|