@grifhinz/logics-manager 2.1.2 → 2.2.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.
@@ -11,9 +11,13 @@ from .bootstrap import bootstrap_payload, render_bootstrap
11
11
  from .assist import main as assist_main
12
12
  from .audit import audit_payload, build_parser as build_audit_parser
13
13
  from .audit import render_audit
14
+ from .cli_output import render_payload
14
15
  from .config import ConfigError, find_repo_root, render_config_show
15
16
  from .index import index_payload, render_index
17
+ from .insights import followups_payload, health_payload, render_followups, render_health, render_status, status_payload
18
+ from .insights import product_consistency_payload, render_product_consistency
16
19
  from .lint import lint_payload, render_lint
20
+ from .sync import search_logics_docs_payload
17
21
  from .doctor import render_doctor
18
22
  from .termstyle import colorize_help
19
23
 
@@ -28,14 +32,29 @@ ROOT_COMMANDS = (
28
32
  "assist",
29
33
  "audit",
30
34
  "index",
35
+ "health",
36
+ "followups",
37
+ "product-consistency",
38
+ "status",
31
39
  "lint",
32
40
  "config",
33
41
  "doctor",
34
42
  "mcp",
35
43
  "self-update",
44
+ "search",
36
45
  )
37
46
 
38
47
 
48
+ def _expand_json_alias(argv: list[str]) -> list[str]:
49
+ expanded: list[str] = []
50
+ for arg in argv:
51
+ if arg == "--json":
52
+ expanded.extend(["--format", "json"])
53
+ else:
54
+ expanded.append(arg)
55
+ return expanded
56
+
57
+
39
58
  def _build_root_help() -> str:
40
59
  sections = [
41
60
  "Logics Manager CLI",
@@ -52,17 +71,23 @@ def _build_root_help() -> str:
52
71
  "Common workflows:",
53
72
  ' logics-manager flow new request --title "My request"',
54
73
  " logics-manager audit --group-by-doc",
74
+ " logics-manager status",
55
75
  " logics-manager sync refresh-mermaid-signatures",
56
76
  " logics-manager mcp tunnel --repo-root . --port 8765",
57
77
  "",
58
78
  "Workflow authoring:",
59
79
  " flow Create, promote, split, close, and finish workflow docs.",
60
- " Subcommands: new, list, companion, promote, split, close, finish",
80
+ " Subcommands: new, list, companion, deliver, validate-closeout, repair, closeout, promote, split, close, finish",
61
81
  " sync Maintain generated workflow state and doc metadata.",
62
82
  " Subcommands: close-eligible-requests, refresh-mermaid-signatures,",
63
83
  " schema-status, read-doc, list-docs, search-docs,",
64
84
  " update-indicators, append-note, context-pack, export-graph",
65
85
  " index Generate logics/INDEX.md from the workflow corpus.",
86
+ " health Show workflow health counts and issue signals.",
87
+ " followups List follow-up areas with request creation commands.",
88
+ " product-consistency Check product brief lineage links.",
89
+ " status Summarize open workflow docs and next actions.",
90
+ " search Search workflow docs directly.",
66
91
  "",
67
92
  "Validation:",
68
93
  " lint Check filenames, headings, indicators, and changed-doc hygiene.",
@@ -118,6 +143,7 @@ def main(argv: list[str] | None = None) -> int:
118
143
  print(f"logics-manager {get_cli_version()}")
119
144
  return 0
120
145
 
146
+ argv = _expand_json_alias(argv)
121
147
  command = argv[0]
122
148
  if command not in ROOT_COMMANDS:
123
149
  raise SystemExit(f"Unsupported command: {command}")
@@ -129,7 +155,7 @@ def main(argv: list[str] | None = None) -> int:
129
155
  config_args = rest[1:]
130
156
  parser = argparse.ArgumentParser(prog="logics-manager config show", add_help=False)
131
157
  parser.add_argument("--format", choices=("text", "json"), default="text")
132
- parsed, _unknown = parser.parse_known_args(config_args)
158
+ parsed = parser.parse_args(config_args)
133
159
  repo_root = find_repo_root(Path.cwd())
134
160
  try:
135
161
  output = render_config_show(repo_root, output_format=parsed.format)
@@ -141,7 +167,7 @@ def main(argv: list[str] | None = None) -> int:
141
167
  doctor_args = rest
142
168
  parser = argparse.ArgumentParser(prog="logics-manager doctor", add_help=False)
143
169
  parser.add_argument("--format", choices=("text", "json"), default="text")
144
- parsed, _unknown = parser.parse_known_args(doctor_args)
170
+ parsed = parser.parse_args(doctor_args)
145
171
  repo_root = find_repo_root(Path.cwd())
146
172
  try:
147
173
  output = render_doctor(repo_root, output_format=parsed.format)
@@ -153,7 +179,7 @@ def main(argv: list[str] | None = None) -> int:
153
179
  parser = argparse.ArgumentParser(prog="logics-manager bootstrap", add_help=False)
154
180
  parser.add_argument("--check", action="store_true")
155
181
  parser.add_argument("--format", choices=("text", "json"), default="text")
156
- parsed, _unknown = parser.parse_known_args(rest)
182
+ parsed = parser.parse_args(rest)
157
183
  try:
158
184
  repo_root = find_repo_root(Path.cwd())
159
185
  except ConfigError:
@@ -167,7 +193,7 @@ def main(argv: list[str] | None = None) -> int:
167
193
  parser.add_argument("--package", default=DEFAULT_SELF_UPDATE_PACKAGE)
168
194
  parser.add_argument("--python-package", default=DEFAULT_SELF_UPDATE_PY_PACKAGE)
169
195
  parser.add_argument("--dry-run", action="store_true")
170
- parsed, _unknown = parser.parse_known_args(rest)
196
+ parsed = parser.parse_args(rest)
171
197
 
172
198
  manager = parsed.manager
173
199
  if manager == "auto":
@@ -196,7 +222,7 @@ def main(argv: list[str] | None = None) -> int:
196
222
  target = parsed.python_package if manager == "pip" else parsed.package
197
223
  print(f"Updated {target} via {manager}.")
198
224
  return result.returncode
199
- if command == "flow" and (rest[:1] in (["new"], ["list"], ["companion"], ["promote"], ["split"], ["close"], ["finish"]) or rest[:1] in HELP_ARGV):
225
+ if command == "flow" and (rest[:1] in (["new"], ["list"], ["companion"], ["deliver"], ["validate-closeout"], ["repair"], ["closeout"], ["promote"], ["split"], ["close"], ["finish"]) or rest[:1] in HELP_ARGV):
200
226
  from .flow import main as flow_main
201
227
 
202
228
  return flow_main(rest)
@@ -207,7 +233,7 @@ def main(argv: list[str] | None = None) -> int:
207
233
 
208
234
  return sync_main(rest)
209
235
  if command == "assist":
210
- if rest[:1] not in (["runtime-status"], ["diff-risk"], ["commit-plan"], ["changed-surface-summary"], ["doc-consistency"], ["review-checklist"], ["validation-checklist"], ["validation-summary"], ["test-impact-summary"], ["roi-report"], ["next-step"], ["claude-bridges"], ["claude-instructions"], ["request-draft"], ["spec-first-pass"], ["backlog-groom"], ["closure-summary"], ["context"]) and rest[:1] not in HELP_ARGV:
236
+ if rest[:1] not in (["runtime-status"], ["diff-risk"], ["commit-plan"], ["changed-surface-summary"], ["doc-consistency"], ["review-checklist"], ["validation-checklist"], ["validation-summary"], ["test-impact-summary"], ["roi-report"], ["next-step"], ["claude-bridges"], ["claude-instructions"], ["request-draft"], ["spec-first-pass"], ["backlog-groom"], ["closure-summary"], ["handoff"], ["context"]) and rest[:1] not in HELP_ARGV:
211
237
  raise SystemExit("Unsupported assist subcommand for the native CLI slice.")
212
238
  return assist_main(rest)
213
239
  if command == "mcp":
@@ -216,7 +242,7 @@ def main(argv: list[str] | None = None) -> int:
216
242
  return mcp_main(rest)
217
243
  if command == "audit":
218
244
  audit_parser = build_audit_parser()
219
- parsed, _unknown = audit_parser.parse_known_args(rest)
245
+ parsed = audit_parser.parse_args(rest)
220
246
  repo_root = find_repo_root(Path.cwd())
221
247
  try:
222
248
  payload = audit_payload(
@@ -253,25 +279,119 @@ def main(argv: list[str] | None = None) -> int:
253
279
  except ConfigError as exc:
254
280
  raise SystemExit(str(exc)) from exc
255
281
  print(output)
256
- return 0 if payload["ok"] else 1
282
+ return 0
257
283
  if command == "index":
258
284
  parser = argparse.ArgumentParser(prog="logics-manager index", add_help=False)
259
285
  parser.add_argument("--out", default="logics/INDEX.md")
260
286
  parser.add_argument("--format", choices=("text", "json"), default="text")
261
- parsed, _unknown = parser.parse_known_args(rest)
287
+ parsed = parser.parse_args(rest)
262
288
  repo_root = find_repo_root(Path.cwd())
263
289
  try:
264
290
  payload = index_payload(repo_root, out=parsed.out)
265
291
  except ConfigError as exc:
266
292
  raise SystemExit(str(exc)) from exc
267
- output = render_index(repo_root, out=parsed.out, output_format=parsed.format) if parsed.format == "json" else f"Wrote {payload['output_path']}"
293
+ output = render_payload(payload, parsed.format, f"Wrote {payload['output_path']}")
294
+ print(output)
295
+ return 0 if payload["ok"] else 1
296
+ if command == "status":
297
+ parser = argparse.ArgumentParser(prog="logics-manager status", add_help=False)
298
+ parser.add_argument("--limit", type=int, default=10)
299
+ parser.add_argument("--format", choices=("text", "json"), default="text")
300
+ parsed = parser.parse_args(rest)
301
+ repo_root = find_repo_root(Path.cwd())
302
+ try:
303
+ payload = status_payload(repo_root, limit=parsed.limit)
304
+ output = render_status(repo_root, output_format=parsed.format, limit=parsed.limit)
305
+ except ConfigError as exc:
306
+ raise SystemExit(str(exc)) from exc
307
+ print(output)
308
+ return 0
309
+ if command == "health":
310
+ parser = argparse.ArgumentParser(prog="logics-manager health", add_help=False)
311
+ parser.add_argument("--limit", type=int, default=10)
312
+ parser.add_argument("--format", choices=("text", "json"), default="text")
313
+ parsed = parser.parse_args(rest)
314
+ repo_root = find_repo_root(Path.cwd())
315
+ try:
316
+ payload = health_payload(repo_root, limit=parsed.limit)
317
+ output = render_health(repo_root, output_format=parsed.format, limit=parsed.limit)
318
+ except ConfigError as exc:
319
+ raise SystemExit(str(exc)) from exc
320
+ print(output)
321
+ return 0
322
+ if command == "followups":
323
+ parser = argparse.ArgumentParser(prog="logics-manager followups", add_help=False)
324
+ parser.add_argument("--limit", type=int, default=50)
325
+ parser.add_argument("--source-kind", choices=("all", "request", "backlog", "task", "product", "architecture"), default="all")
326
+ parser.add_argument("--include-closed", action="store_true")
327
+ parser.add_argument("--closed-only", action="store_true")
328
+ parser.add_argument("--format", choices=("text", "json"), default="text")
329
+ parsed = parser.parse_args(rest)
330
+ if parsed.include_closed and parsed.closed_only:
331
+ raise SystemExit("--include-closed and --closed-only are mutually exclusive.")
332
+ repo_root = find_repo_root(Path.cwd())
333
+ try:
334
+ payload = followups_payload(
335
+ repo_root,
336
+ limit=parsed.limit,
337
+ source_kind=parsed.source_kind,
338
+ include_closed=parsed.include_closed,
339
+ closed_only=parsed.closed_only,
340
+ )
341
+ output = render_followups(
342
+ repo_root,
343
+ output_format=parsed.format,
344
+ limit=parsed.limit,
345
+ source_kind=parsed.source_kind,
346
+ include_closed=parsed.include_closed,
347
+ closed_only=parsed.closed_only,
348
+ )
349
+ except ConfigError as exc:
350
+ raise SystemExit(str(exc)) from exc
268
351
  print(output)
269
352
  return 0 if payload["ok"] else 1
353
+ if command == "search":
354
+ parser = argparse.ArgumentParser(prog="logics-manager search", add_help=False)
355
+ parser.add_argument("query")
356
+ parser.add_argument("--kind", choices=("all", "request", "backlog", "task"), default="all")
357
+ parser.add_argument("--status")
358
+ parser.add_argument("--limit", type=int, default=20)
359
+ parser.add_argument("--max-snippet-chars", type=int, default=240)
360
+ parser.add_argument("--format", choices=("text", "json"), default="text")
361
+ parsed = parser.parse_args(rest)
362
+ repo_root = find_repo_root(Path.cwd())
363
+ payload = search_logics_docs_payload(
364
+ repo_root,
365
+ parsed.query,
366
+ kind=parsed.kind,
367
+ status=parsed.status,
368
+ limit=parsed.limit,
369
+ max_snippet_chars=parsed.max_snippet_chars,
370
+ )
371
+ if parsed.format == "json":
372
+ output = render_payload(payload, "json")
373
+ else:
374
+ lines = [f"Search `{payload['query']}`: {payload['returned_count']} match(es)"]
375
+ for match in payload["matches"]:
376
+ lines.append(f"- {match['ref']}:{match['line']} {match['title']}")
377
+ output = "\n".join(lines)
378
+ print(output)
379
+ return 0
380
+ if command == "product-consistency":
381
+ parser = argparse.ArgumentParser(prog="logics-manager product-consistency", add_help=False)
382
+ parser.add_argument("--limit", type=int, default=50)
383
+ parser.add_argument("--strict", action="store_true")
384
+ parser.add_argument("--format", choices=("text", "json"), default="text")
385
+ parsed = parser.parse_args(rest)
386
+ repo_root = find_repo_root(Path.cwd())
387
+ payload = product_consistency_payload(repo_root, limit=parsed.limit)
388
+ print(render_product_consistency(repo_root, output_format=parsed.format, limit=parsed.limit))
389
+ return 1 if parsed.strict and not payload["ok"] else 0
270
390
  if command == "lint":
271
391
  parser = argparse.ArgumentParser(prog="logics-manager lint", add_help=False)
272
392
  parser.add_argument("--require-status", action="store_true")
273
393
  parser.add_argument("--format", choices=("text", "json"), default="text")
274
- parsed, _unknown = parser.parse_known_args(rest)
394
+ parsed = parser.parse_args(rest)
275
395
  repo_root = find_repo_root(Path.cwd())
276
396
  try:
277
397
  payload = lint_payload(repo_root, require_status=parsed.require_status)
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Callable
5
+
6
+
7
+ def render_payload(payload: dict[str, object], output_format: str, text: str | Callable[[], str] | None = None) -> str:
8
+ if output_format == "json":
9
+ return json.dumps(payload, indent=2, sort_keys=True)
10
+ if callable(text):
11
+ return text()
12
+ return text or ""
13
+
14
+
15
+ def print_payload(payload: dict[str, object], output_format: str, text: str | Callable[[], str] | None = None) -> None:
16
+ output = render_payload(payload, output_format, text)
17
+ if output:
18
+ print(output)