@grifhinz/logics-manager 2.1.2 → 2.3.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.
Files changed (49) hide show
  1. package/README.md +106 -4
  2. package/VERSION +1 -1
  3. package/clients/README.md +9 -0
  4. package/clients/shared-web/media/css/board.css +658 -0
  5. package/clients/shared-web/media/css/details.css +457 -0
  6. package/clients/shared-web/media/css/layout.css +123 -0
  7. package/clients/shared-web/media/css/toolbar.css +576 -0
  8. package/clients/shared-web/media/harnessApi.js +324 -0
  9. package/clients/shared-web/media/hostApi.js +213 -0
  10. package/clients/shared-web/media/hostApiContract.js +55 -0
  11. package/clients/shared-web/media/icon.png +0 -0
  12. package/clients/shared-web/media/layoutController.js +246 -0
  13. package/clients/shared-web/media/logics.svg +7 -0
  14. package/clients/shared-web/media/logicsModel.js +910 -0
  15. package/clients/shared-web/media/main.css +112 -0
  16. package/clients/shared-web/media/main.js +3 -0
  17. package/clients/shared-web/media/mainApp.js +1005 -0
  18. package/clients/shared-web/media/mainCore.js +604 -0
  19. package/clients/shared-web/media/mainInteractionHandlers.js +324 -0
  20. package/clients/shared-web/media/mainInteractions.js +378 -0
  21. package/clients/shared-web/media/renderBoard.js +3 -0
  22. package/clients/shared-web/media/renderBoardApp.js +1339 -0
  23. package/clients/shared-web/media/renderDetails.js +685 -0
  24. package/clients/shared-web/media/renderMarkdown.js +449 -0
  25. package/clients/shared-web/media/toolsPanelLayout.js +172 -0
  26. package/clients/shared-web/media/uiStatus.js +54 -0
  27. package/clients/shared-web/media/webviewChrome.js +405 -0
  28. package/clients/shared-web/media/webviewPersistence.js +116 -0
  29. package/clients/shared-web/media/webviewSelectors.js +491 -0
  30. package/clients/viewer/README.md +5 -0
  31. package/clients/viewer/browser-host.js +847 -0
  32. package/clients/viewer/index.html +237 -0
  33. package/clients/viewer/viewer.css +433 -0
  34. package/logics_manager/assist.py +94 -63
  35. package/logics_manager/assist_handoff.py +132 -0
  36. package/logics_manager/assist_surface.py +38 -0
  37. package/logics_manager/cli.py +152 -12
  38. package/logics_manager/cli_output.py +18 -0
  39. package/logics_manager/flow.py +1360 -84
  40. package/logics_manager/flow_evidence.py +63 -0
  41. package/logics_manager/index.py +3 -7
  42. package/logics_manager/insights.py +418 -0
  43. package/logics_manager/mcp.py +50 -0
  44. package/logics_manager/path_utils.py +31 -0
  45. package/logics_manager/sync.py +24 -12
  46. package/logics_manager/update_check.py +138 -0
  47. package/logics_manager/viewer.py +533 -0
  48. package/package.json +12 -6
  49. package/pyproject.toml +1 -1
@@ -11,11 +11,16 @@ 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
23
+ from .update_check import get_update_notice
19
24
 
20
25
 
21
26
  DEFAULT_SELF_UPDATE_PY_PACKAGE = "logics-manager"
@@ -28,14 +33,30 @@ ROOT_COMMANDS = (
28
33
  "assist",
29
34
  "audit",
30
35
  "index",
36
+ "health",
37
+ "followups",
38
+ "product-consistency",
39
+ "status",
31
40
  "lint",
41
+ "view",
32
42
  "config",
33
43
  "doctor",
34
44
  "mcp",
35
45
  "self-update",
46
+ "search",
36
47
  )
37
48
 
38
49
 
50
+ def _expand_json_alias(argv: list[str]) -> list[str]:
51
+ expanded: list[str] = []
52
+ for arg in argv:
53
+ if arg == "--json":
54
+ expanded.extend(["--format", "json"])
55
+ else:
56
+ expanded.append(arg)
57
+ return expanded
58
+
59
+
39
60
  def _build_root_help() -> str:
40
61
  sections = [
41
62
  "Logics Manager CLI",
@@ -52,17 +73,24 @@ def _build_root_help() -> str:
52
73
  "Common workflows:",
53
74
  ' logics-manager flow new request --title "My request"',
54
75
  " logics-manager audit --group-by-doc",
76
+ " logics-manager status",
55
77
  " logics-manager sync refresh-mermaid-signatures",
56
78
  " logics-manager mcp tunnel --repo-root . --port 8765",
57
79
  "",
58
80
  "Workflow authoring:",
59
81
  " flow Create, promote, split, close, and finish workflow docs.",
60
- " Subcommands: new, list, companion, promote, split, close, finish",
82
+ " Subcommands: new, list, companion, deliver, validate-closeout, repair, closeout, promote, split, close, finish",
61
83
  " sync Maintain generated workflow state and doc metadata.",
62
84
  " Subcommands: close-eligible-requests, refresh-mermaid-signatures,",
63
85
  " schema-status, read-doc, list-docs, search-docs,",
64
86
  " update-indicators, append-note, context-pack, export-graph",
65
87
  " index Generate logics/INDEX.md from the workflow corpus.",
88
+ " health Show workflow health counts and issue signals.",
89
+ " followups List follow-up areas with request creation commands.",
90
+ " product-consistency Check product brief lineage links.",
91
+ " status Summarize open workflow docs and next actions.",
92
+ " search Search workflow docs directly.",
93
+ " view Start a local read-only browser viewer for the Logics corpus.",
66
94
  "",
67
95
  "Validation:",
68
96
  " lint Check filenames, headings, indicators, and changed-doc hygiene.",
@@ -105,6 +133,18 @@ def get_cli_version() -> str:
105
133
  return "0.0.0"
106
134
 
107
135
 
136
+ def _is_json_mode(argv: list[str]) -> bool:
137
+ return "--json" in argv or any(argv[index] == "--format" and index + 1 < len(argv) and argv[index + 1] == "json" for index in range(len(argv)))
138
+
139
+
140
+ def _maybe_print_update_notice(command: str, argv: list[str]) -> None:
141
+ if command in {"self-update", "mcp", "view"} or _is_json_mode(argv) or not sys.stdout.isatty():
142
+ return
143
+ notice = get_update_notice(get_cli_version())
144
+ if notice:
145
+ print(notice, file=sys.stderr)
146
+
147
+
108
148
  def main(argv: list[str] | None = None) -> int:
109
149
  if argv is None:
110
150
  argv = sys.argv[1:]
@@ -118,9 +158,11 @@ def main(argv: list[str] | None = None) -> int:
118
158
  print(f"logics-manager {get_cli_version()}")
119
159
  return 0
120
160
 
161
+ argv = _expand_json_alias(argv)
121
162
  command = argv[0]
122
163
  if command not in ROOT_COMMANDS:
123
164
  raise SystemExit(f"Unsupported command: {command}")
165
+ _maybe_print_update_notice(command, argv)
124
166
 
125
167
  rest = argv[1:]
126
168
  if command == "config":
@@ -129,7 +171,7 @@ def main(argv: list[str] | None = None) -> int:
129
171
  config_args = rest[1:]
130
172
  parser = argparse.ArgumentParser(prog="logics-manager config show", add_help=False)
131
173
  parser.add_argument("--format", choices=("text", "json"), default="text")
132
- parsed, _unknown = parser.parse_known_args(config_args)
174
+ parsed = parser.parse_args(config_args)
133
175
  repo_root = find_repo_root(Path.cwd())
134
176
  try:
135
177
  output = render_config_show(repo_root, output_format=parsed.format)
@@ -141,7 +183,7 @@ def main(argv: list[str] | None = None) -> int:
141
183
  doctor_args = rest
142
184
  parser = argparse.ArgumentParser(prog="logics-manager doctor", add_help=False)
143
185
  parser.add_argument("--format", choices=("text", "json"), default="text")
144
- parsed, _unknown = parser.parse_known_args(doctor_args)
186
+ parsed = parser.parse_args(doctor_args)
145
187
  repo_root = find_repo_root(Path.cwd())
146
188
  try:
147
189
  output = render_doctor(repo_root, output_format=parsed.format)
@@ -153,7 +195,7 @@ def main(argv: list[str] | None = None) -> int:
153
195
  parser = argparse.ArgumentParser(prog="logics-manager bootstrap", add_help=False)
154
196
  parser.add_argument("--check", action="store_true")
155
197
  parser.add_argument("--format", choices=("text", "json"), default="text")
156
- parsed, _unknown = parser.parse_known_args(rest)
198
+ parsed = parser.parse_args(rest)
157
199
  try:
158
200
  repo_root = find_repo_root(Path.cwd())
159
201
  except ConfigError:
@@ -167,7 +209,7 @@ def main(argv: list[str] | None = None) -> int:
167
209
  parser.add_argument("--package", default=DEFAULT_SELF_UPDATE_PACKAGE)
168
210
  parser.add_argument("--python-package", default=DEFAULT_SELF_UPDATE_PY_PACKAGE)
169
211
  parser.add_argument("--dry-run", action="store_true")
170
- parsed, _unknown = parser.parse_known_args(rest)
212
+ parsed = parser.parse_args(rest)
171
213
 
172
214
  manager = parsed.manager
173
215
  if manager == "auto":
@@ -196,7 +238,7 @@ def main(argv: list[str] | None = None) -> int:
196
238
  target = parsed.python_package if manager == "pip" else parsed.package
197
239
  print(f"Updated {target} via {manager}.")
198
240
  return result.returncode
199
- if command == "flow" and (rest[:1] in (["new"], ["list"], ["companion"], ["promote"], ["split"], ["close"], ["finish"]) or rest[:1] in HELP_ARGV):
241
+ 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
242
  from .flow import main as flow_main
201
243
 
202
244
  return flow_main(rest)
@@ -207,16 +249,20 @@ def main(argv: list[str] | None = None) -> int:
207
249
 
208
250
  return sync_main(rest)
209
251
  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:
252
+ 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
253
  raise SystemExit("Unsupported assist subcommand for the native CLI slice.")
212
254
  return assist_main(rest)
213
255
  if command == "mcp":
214
256
  from .mcp import main as mcp_main
215
257
 
216
258
  return mcp_main(rest)
259
+ if command == "view":
260
+ from .viewer import main as viewer_main
261
+
262
+ return viewer_main(rest)
217
263
  if command == "audit":
218
264
  audit_parser = build_audit_parser()
219
- parsed, _unknown = audit_parser.parse_known_args(rest)
265
+ parsed = audit_parser.parse_args(rest)
220
266
  repo_root = find_repo_root(Path.cwd())
221
267
  try:
222
268
  payload = audit_payload(
@@ -253,25 +299,119 @@ def main(argv: list[str] | None = None) -> int:
253
299
  except ConfigError as exc:
254
300
  raise SystemExit(str(exc)) from exc
255
301
  print(output)
256
- return 0 if payload["ok"] else 1
302
+ return 0
257
303
  if command == "index":
258
304
  parser = argparse.ArgumentParser(prog="logics-manager index", add_help=False)
259
305
  parser.add_argument("--out", default="logics/INDEX.md")
260
306
  parser.add_argument("--format", choices=("text", "json"), default="text")
261
- parsed, _unknown = parser.parse_known_args(rest)
307
+ parsed = parser.parse_args(rest)
262
308
  repo_root = find_repo_root(Path.cwd())
263
309
  try:
264
310
  payload = index_payload(repo_root, out=parsed.out)
265
311
  except ConfigError as exc:
266
312
  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']}"
313
+ output = render_payload(payload, parsed.format, f"Wrote {payload['output_path']}")
268
314
  print(output)
269
315
  return 0 if payload["ok"] else 1
316
+ if command == "status":
317
+ parser = argparse.ArgumentParser(prog="logics-manager status", add_help=False)
318
+ parser.add_argument("--limit", type=int, default=10)
319
+ parser.add_argument("--format", choices=("text", "json"), default="text")
320
+ parsed = parser.parse_args(rest)
321
+ repo_root = find_repo_root(Path.cwd())
322
+ try:
323
+ payload = status_payload(repo_root, limit=parsed.limit)
324
+ output = render_status(repo_root, output_format=parsed.format, limit=parsed.limit)
325
+ except ConfigError as exc:
326
+ raise SystemExit(str(exc)) from exc
327
+ print(output)
328
+ return 0
329
+ if command == "health":
330
+ parser = argparse.ArgumentParser(prog="logics-manager health", add_help=False)
331
+ parser.add_argument("--limit", type=int, default=10)
332
+ parser.add_argument("--format", choices=("text", "json"), default="text")
333
+ parsed = parser.parse_args(rest)
334
+ repo_root = find_repo_root(Path.cwd())
335
+ try:
336
+ payload = health_payload(repo_root, limit=parsed.limit)
337
+ output = render_health(repo_root, output_format=parsed.format, limit=parsed.limit)
338
+ except ConfigError as exc:
339
+ raise SystemExit(str(exc)) from exc
340
+ print(output)
341
+ return 0
342
+ if command == "followups":
343
+ parser = argparse.ArgumentParser(prog="logics-manager followups", add_help=False)
344
+ parser.add_argument("--limit", type=int, default=50)
345
+ parser.add_argument("--source-kind", choices=("all", "request", "backlog", "task", "product", "architecture"), default="all")
346
+ parser.add_argument("--include-closed", action="store_true")
347
+ parser.add_argument("--closed-only", action="store_true")
348
+ parser.add_argument("--format", choices=("text", "json"), default="text")
349
+ parsed = parser.parse_args(rest)
350
+ if parsed.include_closed and parsed.closed_only:
351
+ raise SystemExit("--include-closed and --closed-only are mutually exclusive.")
352
+ repo_root = find_repo_root(Path.cwd())
353
+ try:
354
+ payload = followups_payload(
355
+ repo_root,
356
+ limit=parsed.limit,
357
+ source_kind=parsed.source_kind,
358
+ include_closed=parsed.include_closed,
359
+ closed_only=parsed.closed_only,
360
+ )
361
+ output = render_followups(
362
+ repo_root,
363
+ output_format=parsed.format,
364
+ limit=parsed.limit,
365
+ source_kind=parsed.source_kind,
366
+ include_closed=parsed.include_closed,
367
+ closed_only=parsed.closed_only,
368
+ )
369
+ except ConfigError as exc:
370
+ raise SystemExit(str(exc)) from exc
371
+ print(output)
372
+ return 0 if payload["ok"] else 1
373
+ if command == "search":
374
+ parser = argparse.ArgumentParser(prog="logics-manager search", add_help=False)
375
+ parser.add_argument("query")
376
+ parser.add_argument("--kind", choices=("all", "request", "backlog", "task"), default="all")
377
+ parser.add_argument("--status")
378
+ parser.add_argument("--limit", type=int, default=20)
379
+ parser.add_argument("--max-snippet-chars", type=int, default=240)
380
+ parser.add_argument("--format", choices=("text", "json"), default="text")
381
+ parsed = parser.parse_args(rest)
382
+ repo_root = find_repo_root(Path.cwd())
383
+ payload = search_logics_docs_payload(
384
+ repo_root,
385
+ parsed.query,
386
+ kind=parsed.kind,
387
+ status=parsed.status,
388
+ limit=parsed.limit,
389
+ max_snippet_chars=parsed.max_snippet_chars,
390
+ )
391
+ if parsed.format == "json":
392
+ output = render_payload(payload, "json")
393
+ else:
394
+ lines = [f"Search `{payload['query']}`: {payload['returned_count']} match(es)"]
395
+ for match in payload["matches"]:
396
+ lines.append(f"- {match['ref']}:{match['line']} {match['title']}")
397
+ output = "\n".join(lines)
398
+ print(output)
399
+ return 0
400
+ if command == "product-consistency":
401
+ parser = argparse.ArgumentParser(prog="logics-manager product-consistency", add_help=False)
402
+ parser.add_argument("--limit", type=int, default=50)
403
+ parser.add_argument("--strict", action="store_true")
404
+ parser.add_argument("--format", choices=("text", "json"), default="text")
405
+ parsed = parser.parse_args(rest)
406
+ repo_root = find_repo_root(Path.cwd())
407
+ payload = product_consistency_payload(repo_root, limit=parsed.limit)
408
+ print(render_product_consistency(repo_root, output_format=parsed.format, limit=parsed.limit))
409
+ return 1 if parsed.strict and not payload["ok"] else 0
270
410
  if command == "lint":
271
411
  parser = argparse.ArgumentParser(prog="logics-manager lint", add_help=False)
272
412
  parser.add_argument("--require-status", action="store_true")
273
413
  parser.add_argument("--format", choices=("text", "json"), default="text")
274
- parsed, _unknown = parser.parse_known_args(rest)
414
+ parsed = parser.parse_args(rest)
275
415
  repo_root = find_repo_root(Path.cwd())
276
416
  try:
277
417
  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)