@grifhinz/logics-manager 2.1.1 → 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,78 +32,80 @@ 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",
42
- "Canonical CLI for workflow, validation, and runtime ops.",
61
+ "Canonical CLI for Logics workflow, validation, MCP, and runtime ops.",
43
62
  "",
44
63
  "Usage:",
45
64
  " logics-manager <command> [args...]",
46
- " logics-manager config show [options]",
65
+ " logics-manager <command> --help",
47
66
  "",
48
67
  "Top-level options:",
49
68
  " -h, --help Show this help message and exit.",
50
- " -v, --version Print the installed version.",
51
- " --version Print the installed version.",
52
- "",
53
- "Commands:",
54
- " bootstrap",
55
- " Prepare or check the workflow tree and generated instructions.",
56
- " Options: --check, --format {text,json}",
57
- "",
58
- " flow",
59
- " Create and manage workflow docs.",
60
- " Subcommands: new, list, companion, promote, split, close, finish",
61
- " Key flags: --title, --slug, --from-version, --understanding, --confidence, --status, --complexity, --theme, --progress, --format {text,json}, --dry-run",
62
- "",
63
- " sync",
64
- " Synchronize workflow transitions and exports.",
65
- " Subcommands: close-eligible-requests, refresh-mermaid-signatures, schema-status, read-doc, list-docs, search-docs, update-indicators, append-note, context-pack, export-graph",
69
+ " -v, --version Print the installed version and exit.",
66
70
  "",
67
- " assist",
68
- " Inspect runtime signals and build context bundles.",
69
- " Subcommands: runtime-status, diff-risk, commit-plan, changed-surface-summary, doc-consistency, review-checklist, validation-checklist, validation-summary, test-impact-summary, roi-report, claude-bridges, context, claude-instructions, next-step, request-draft, spec-first-pass, backlog-groom, closure-summary",
70
- "",
71
- " audit",
72
- " Audit request, backlog, and task consistency.",
73
- " Options: --stale-days, --skip-ac-traceability, --skip-gates, --legacy-cutoff-version, --format {text,json}, --group-by-doc, --autofix-ac-traceability, --paths, --refs, --since-version, --token-hygiene, --autofix-structure, --governance-profile",
74
- "",
75
- " index",
76
- " Generate `logics/INDEX.md` from the workflow corpus.",
77
- " Options: --out, --format {text,json}",
78
- "",
79
- " lint",
80
- " Lint workflow documents for filenames, headings, and indicators.",
81
- " Options: --require-status, --format {text,json}",
82
- "",
83
- " config show",
84
- " Render the merged runtime config.",
85
- " Options: --format {text,json}",
71
+ "Common workflows:",
72
+ ' logics-manager flow new request --title "My request"',
73
+ " logics-manager audit --group-by-doc",
74
+ " logics-manager status",
75
+ " logics-manager sync refresh-mermaid-signatures",
76
+ " logics-manager mcp tunnel --repo-root . --port 8765",
86
77
  "",
87
- " doctor",
88
- " Check required workflow directories and schema metadata.",
89
- " Options: --format {text,json}",
78
+ "Workflow authoring:",
79
+ " flow Create, promote, split, close, and finish workflow docs.",
80
+ " Subcommands: new, list, companion, deliver, validate-closeout, repair, closeout, promote, split, close, finish",
81
+ " sync Maintain generated workflow state and doc metadata.",
82
+ " Subcommands: close-eligible-requests, refresh-mermaid-signatures,",
83
+ " schema-status, read-doc, list-docs, search-docs,",
84
+ " update-indicators, append-note, context-pack, export-graph",
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.",
90
91
  "",
91
- " mcp",
92
- " Expose bounded Logics tools for MCP clients.",
93
- " Subcommands: serve, serve-http, connect, tools, call",
92
+ "Validation:",
93
+ " lint Check filenames, headings, indicators, and changed-doc hygiene.",
94
+ " audit Check workflow consistency and traceability.",
95
+ " Use --governance-profile {relaxed,standard,strict}.",
96
+ " JSON output includes issue_count, warning_count, can_continue,",
97
+ " and release_ready for agent workflows.",
98
+ " doctor Check required workflow directories and schema metadata.",
94
99
  "",
95
- " self-update",
96
- " Update the installed Python or npm package.",
97
- " Options: --manager {auto,pip,npm}, --package, --python-package, --dry-run",
100
+ "Agent and integration surfaces:",
101
+ " assist Inspect runtime signals and build bounded context bundles.",
102
+ " mcp Expose bounded Logics tools for MCP clients.",
103
+ " Subcommands: serve, serve-http, connect, tunnel, tools, call",
104
+ " config Render merged runtime config. Example: config show --format json",
98
105
  "",
99
- "Examples:",
100
- ' logics-manager flow new request --title "My request"',
101
- " logics-manager audit",
102
- " logics-manager config show --format json",
106
+ "Maintenance:",
107
+ " bootstrap Prepare or check the workflow tree and generated instructions.",
108
+ " self-update Update the installed Python or npm package.",
103
109
  ]
104
110
  return "\n".join(sections)
105
111
 
@@ -137,6 +143,7 @@ def main(argv: list[str] | None = None) -> int:
137
143
  print(f"logics-manager {get_cli_version()}")
138
144
  return 0
139
145
 
146
+ argv = _expand_json_alias(argv)
140
147
  command = argv[0]
141
148
  if command not in ROOT_COMMANDS:
142
149
  raise SystemExit(f"Unsupported command: {command}")
@@ -148,7 +155,7 @@ def main(argv: list[str] | None = None) -> int:
148
155
  config_args = rest[1:]
149
156
  parser = argparse.ArgumentParser(prog="logics-manager config show", add_help=False)
150
157
  parser.add_argument("--format", choices=("text", "json"), default="text")
151
- parsed, _unknown = parser.parse_known_args(config_args)
158
+ parsed = parser.parse_args(config_args)
152
159
  repo_root = find_repo_root(Path.cwd())
153
160
  try:
154
161
  output = render_config_show(repo_root, output_format=parsed.format)
@@ -160,7 +167,7 @@ def main(argv: list[str] | None = None) -> int:
160
167
  doctor_args = rest
161
168
  parser = argparse.ArgumentParser(prog="logics-manager doctor", add_help=False)
162
169
  parser.add_argument("--format", choices=("text", "json"), default="text")
163
- parsed, _unknown = parser.parse_known_args(doctor_args)
170
+ parsed = parser.parse_args(doctor_args)
164
171
  repo_root = find_repo_root(Path.cwd())
165
172
  try:
166
173
  output = render_doctor(repo_root, output_format=parsed.format)
@@ -172,7 +179,7 @@ def main(argv: list[str] | None = None) -> int:
172
179
  parser = argparse.ArgumentParser(prog="logics-manager bootstrap", add_help=False)
173
180
  parser.add_argument("--check", action="store_true")
174
181
  parser.add_argument("--format", choices=("text", "json"), default="text")
175
- parsed, _unknown = parser.parse_known_args(rest)
182
+ parsed = parser.parse_args(rest)
176
183
  try:
177
184
  repo_root = find_repo_root(Path.cwd())
178
185
  except ConfigError:
@@ -186,7 +193,7 @@ def main(argv: list[str] | None = None) -> int:
186
193
  parser.add_argument("--package", default=DEFAULT_SELF_UPDATE_PACKAGE)
187
194
  parser.add_argument("--python-package", default=DEFAULT_SELF_UPDATE_PY_PACKAGE)
188
195
  parser.add_argument("--dry-run", action="store_true")
189
- parsed, _unknown = parser.parse_known_args(rest)
196
+ parsed = parser.parse_args(rest)
190
197
 
191
198
  manager = parsed.manager
192
199
  if manager == "auto":
@@ -215,7 +222,7 @@ def main(argv: list[str] | None = None) -> int:
215
222
  target = parsed.python_package if manager == "pip" else parsed.package
216
223
  print(f"Updated {target} via {manager}.")
217
224
  return result.returncode
218
- 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):
219
226
  from .flow import main as flow_main
220
227
 
221
228
  return flow_main(rest)
@@ -226,7 +233,7 @@ def main(argv: list[str] | None = None) -> int:
226
233
 
227
234
  return sync_main(rest)
228
235
  if command == "assist":
229
- 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:
230
237
  raise SystemExit("Unsupported assist subcommand for the native CLI slice.")
231
238
  return assist_main(rest)
232
239
  if command == "mcp":
@@ -235,7 +242,7 @@ def main(argv: list[str] | None = None) -> int:
235
242
  return mcp_main(rest)
236
243
  if command == "audit":
237
244
  audit_parser = build_audit_parser()
238
- parsed, _unknown = audit_parser.parse_known_args(rest)
245
+ parsed = audit_parser.parse_args(rest)
239
246
  repo_root = find_repo_root(Path.cwd())
240
247
  try:
241
248
  payload = audit_payload(
@@ -272,25 +279,119 @@ def main(argv: list[str] | None = None) -> int:
272
279
  except ConfigError as exc:
273
280
  raise SystemExit(str(exc)) from exc
274
281
  print(output)
275
- return 0 if payload["ok"] else 1
282
+ return 0
276
283
  if command == "index":
277
284
  parser = argparse.ArgumentParser(prog="logics-manager index", add_help=False)
278
285
  parser.add_argument("--out", default="logics/INDEX.md")
279
286
  parser.add_argument("--format", choices=("text", "json"), default="text")
280
- parsed, _unknown = parser.parse_known_args(rest)
287
+ parsed = parser.parse_args(rest)
281
288
  repo_root = find_repo_root(Path.cwd())
282
289
  try:
283
290
  payload = index_payload(repo_root, out=parsed.out)
284
291
  except ConfigError as exc:
285
292
  raise SystemExit(str(exc)) from exc
286
- 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']}")
287
294
  print(output)
288
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
351
+ print(output)
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
289
390
  if command == "lint":
290
391
  parser = argparse.ArgumentParser(prog="logics-manager lint", add_help=False)
291
392
  parser.add_argument("--require-status", action="store_true")
292
393
  parser.add_argument("--format", choices=("text", "json"), default="text")
293
- parsed, _unknown = parser.parse_known_args(rest)
394
+ parsed = parser.parse_args(rest)
294
395
  repo_root = find_repo_root(Path.cwd())
295
396
  try:
296
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)