@grifhinz/logics-manager 2.0.5 → 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.
@@ -38,6 +38,8 @@ REF_PREFIXES = ("req", "item", "task", "prod", "adr", "spec")
38
38
  _CONTEXT_PACK_CACHE: dict[str, dict[str, object]] = {}
39
39
  MERMAID_BLOCK_PATTERN = re.compile(r"```mermaid\s*\n(.*?)\n```", re.DOTALL)
40
40
  MERMAID_SIGNATURE_PATTERN = re.compile(r"^\s*%%\s*logics-signature:\s*(.+?)\s*$", re.MULTILINE)
41
+ APPROVED_WORKFLOW_INDICATORS = ("Status", "Progress", "Understanding", "Confidence", "Theme", "Complexity")
42
+ MAX_MUTATION_TEXT_CHARS = 2000
41
43
 
42
44
 
43
45
  def _read_text(path: Path) -> str:
@@ -303,6 +305,9 @@ def _resolve_target_docs(repo_root: Path, sources: list[str]) -> list[tuple[str,
303
305
 
304
306
  resolved: list[tuple[str, Path]] = []
305
307
  for source in sources:
308
+ raw_source = Path(source)
309
+ if raw_source.is_absolute() or any(part == ".." for part in raw_source.parts):
310
+ raise SystemExit(f"Unsupported workflow doc target `{source}`.")
306
311
  candidate = (repo_root / source).resolve()
307
312
  if candidate.is_file():
308
313
  for kind_name, kind in DOC_KINDS.items():
@@ -342,6 +347,249 @@ def _schema_status(repo_root: Path, targets: list[str]) -> dict[str, object]:
342
347
  }
343
348
 
344
349
 
350
+ def build_context_pack_payload(repo_root: Path, ref: str, *, mode: str = "summary-only", profile: str = "normal", config: dict[str, object] | None = None) -> dict[str, object]:
351
+ return _build_context_pack(repo_root, ref, mode=mode, profile=profile, config=config)
352
+
353
+
354
+ def _default_section_names(kind: str) -> list[str]:
355
+ return {
356
+ "request": ["Needs", "Context", "Acceptance criteria", "Backlog", "Tasks", "AI Context"],
357
+ "backlog": ["Problem", "Scope", "Acceptance criteria", "AC Traceability", "Tasks", "AI Context"],
358
+ "task": ["Definition of Done (DoD)", "Backlog", "Acceptance criteria", "Validation", "Report", "AI Context"],
359
+ }.get(kind, ["AI Context"])
360
+
361
+
362
+ def read_logics_doc_payload(repo_root: Path, source: str, *, max_chars: int = 4000, sections: list[str] | None = None) -> dict[str, object]:
363
+ targets = _resolve_target_docs(repo_root, [source])
364
+ if len(targets) != 1:
365
+ raise SystemExit(f"Expected one workflow doc target for `{source}`.")
366
+ kind, path = targets[0]
367
+ doc = parse_workflow_doc(path, repo_root=repo_root)
368
+ requested_sections = sections or _default_section_names(kind)
369
+ selected_sections = {
370
+ heading: [line for line in doc.sections.get(heading, []) if line.strip()]
371
+ for heading in requested_sections
372
+ if heading in doc.sections
373
+ }
374
+ text = _read_text(path)
375
+ return {
376
+ "ref": doc.ref,
377
+ "kind": doc.kind,
378
+ "path": doc.path,
379
+ "title": doc.title,
380
+ "status": doc.indicators.get("Status", ""),
381
+ "indicators": doc.indicators,
382
+ "linked_refs": {prefix: refs for prefix, refs in doc.refs.items() if refs},
383
+ "sections": selected_sections,
384
+ "content": text[:max_chars],
385
+ "truncated": len(text) > max_chars,
386
+ "max_chars": max_chars,
387
+ }
388
+
389
+
390
+ def list_logics_docs_payload(
391
+ repo_root: Path,
392
+ *,
393
+ kind: str = "all",
394
+ status: str | None = None,
395
+ ref_prefix: str | None = None,
396
+ limit: int = 50,
397
+ ) -> dict[str, object]:
398
+ docs = sorted(_load_workflow_docs(repo_root).values(), key=lambda doc: doc.path)
399
+ if kind != "all":
400
+ docs = [doc for doc in docs if doc.kind == kind]
401
+ if status:
402
+ expected_status = " ".join(status.split()).lower()
403
+ docs = [doc for doc in docs if " ".join(doc.indicators.get("Status", "").split()).lower() == expected_status]
404
+ if ref_prefix:
405
+ docs = [doc for doc in docs if doc.ref.startswith(ref_prefix)]
406
+ limited = docs[:limit]
407
+ return {
408
+ "items": [
409
+ {
410
+ "ref": doc.ref,
411
+ "kind": doc.kind,
412
+ "path": doc.path,
413
+ "title": doc.title,
414
+ "status": doc.indicators.get("Status", ""),
415
+ "linked_refs": {prefix: refs for prefix, refs in doc.refs.items() if refs},
416
+ }
417
+ for doc in limited
418
+ ],
419
+ "total_count": len(docs),
420
+ "returned_count": len(limited),
421
+ "truncated": len(docs) > len(limited),
422
+ "limit": limit,
423
+ }
424
+
425
+
426
+ def _snippet_for_line(lines: list[str], index: int, *, max_chars: int) -> str:
427
+ start = max(0, index - 1)
428
+ end = min(len(lines), index + 2)
429
+ snippet = "\n".join(line for line in lines[start:end] if line.strip())
430
+ return snippet[:max_chars]
431
+
432
+
433
+ def search_logics_docs_payload(
434
+ repo_root: Path,
435
+ query: str,
436
+ *,
437
+ kind: str = "all",
438
+ status: str | None = None,
439
+ limit: int = 20,
440
+ max_snippet_chars: int = 240,
441
+ ) -> dict[str, object]:
442
+ normalized_query = query.strip().lower()
443
+ if not normalized_query:
444
+ raise SystemExit("Search query is required.")
445
+ docs_payload = list_logics_docs_payload(repo_root, kind=kind, status=status, limit=10000)
446
+ docs_by_ref = _load_workflow_docs(repo_root)
447
+ matches: list[dict[str, object]] = []
448
+ for item in docs_payload["items"]:
449
+ ref = str(item["ref"])
450
+ doc = docs_by_ref.get(ref)
451
+ if doc is None:
452
+ continue
453
+ text = _strip_mermaid_blocks(_read_text(repo_root / doc.path))
454
+ lines = text.splitlines()
455
+ for idx, line in enumerate(lines):
456
+ if normalized_query in line.lower():
457
+ matches.append(
458
+ {
459
+ "ref": doc.ref,
460
+ "kind": doc.kind,
461
+ "path": doc.path,
462
+ "title": doc.title,
463
+ "status": doc.indicators.get("Status", ""),
464
+ "line": idx + 1,
465
+ "snippet": _snippet_for_line(lines, idx, max_chars=max_snippet_chars),
466
+ }
467
+ )
468
+ break
469
+ if len(matches) >= limit:
470
+ break
471
+ return {
472
+ "query": query,
473
+ "matches": matches,
474
+ "returned_count": len(matches),
475
+ "truncated": len(matches) >= limit,
476
+ "limit": limit,
477
+ }
478
+
479
+
480
+ def _clean_mutation_text(text: str, *, field: str) -> str:
481
+ cleaned = " ".join(text.split())
482
+ if not cleaned:
483
+ raise SystemExit(f"{field} is required.")
484
+ if len(cleaned) > MAX_MUTATION_TEXT_CHARS:
485
+ raise SystemExit(f"{field} exceeds {MAX_MUTATION_TEXT_CHARS} characters.")
486
+ if cleaned.startswith("#") or "```" in cleaned:
487
+ raise SystemExit(f"{field} contains unsupported Markdown structure.")
488
+ return cleaned
489
+
490
+
491
+ def _replace_indicator(lines: list[str], key: str, value: str) -> tuple[list[str], bool]:
492
+ rendered = f"> {key}: {value}"
493
+ for idx, line in enumerate(lines):
494
+ if line.startswith(f"> {key}:"):
495
+ if line == rendered:
496
+ return lines, False
497
+ updated = list(lines)
498
+ updated[idx] = rendered
499
+ return updated, True
500
+ insert_at = 1
501
+ while insert_at < len(lines) and lines[insert_at].startswith("> "):
502
+ insert_at += 1
503
+ updated = list(lines)
504
+ updated.insert(insert_at, rendered)
505
+ return updated, True
506
+
507
+
508
+ def update_workflow_indicators_payload(repo_root: Path, source: str, indicators: dict[str, str], *, dry_run: bool = False) -> dict[str, object]:
509
+ unknown = sorted(set(indicators) - set(APPROVED_WORKFLOW_INDICATORS))
510
+ if unknown:
511
+ raise SystemExit(f"Unsupported workflow indicator(s): {', '.join(unknown)}.")
512
+ cleaned = {key: _clean_mutation_text(value, field=key) for key, value in indicators.items() if value is not None}
513
+ if not cleaned:
514
+ raise SystemExit("At least one workflow indicator is required.")
515
+
516
+ targets = _resolve_target_docs(repo_root, [source])
517
+ if len(targets) != 1:
518
+ raise SystemExit(f"Expected one workflow doc target for `{source}`.")
519
+ kind, path = targets[0]
520
+ lines = _read_lines(path)
521
+ changed = False
522
+ for key in APPROVED_WORKFLOW_INDICATORS:
523
+ if key not in cleaned:
524
+ continue
525
+ if key == "Progress" and kind not in {"backlog", "task"}:
526
+ raise SystemExit("Progress is only supported for backlog and task documents.")
527
+ lines, key_changed = _replace_indicator(lines, key, cleaned[key])
528
+ changed = changed or key_changed
529
+ if changed and not dry_run:
530
+ path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
531
+ refresh_workflow_mermaid_signature_file(path, kind, dry_run=False, repo_root=repo_root)
532
+ return {
533
+ "path": path.relative_to(repo_root).as_posix(),
534
+ "ref": path.stem,
535
+ "kind": kind,
536
+ "updated_indicators": cleaned,
537
+ "changed": changed,
538
+ "dry_run": dry_run,
539
+ }
540
+
541
+
542
+ def _section_for_note(kind: str, note_kind: str) -> str:
543
+ if note_kind == "report":
544
+ if kind != "task":
545
+ raise SystemExit("Report entries are only supported for task documents.")
546
+ return "Report"
547
+ if note_kind == "validation":
548
+ return "Validation"
549
+ if note_kind == "decision":
550
+ return "Decision framing" if kind == "backlog" else "Notes"
551
+ raise SystemExit(f"Unsupported note kind `{note_kind}`.")
552
+
553
+
554
+ def append_workflow_note_payload(repo_root: Path, source: str, *, note_kind: str, text: str, dry_run: bool = False) -> dict[str, object]:
555
+ targets = _resolve_target_docs(repo_root, [source])
556
+ if len(targets) != 1:
557
+ raise SystemExit(f"Expected one workflow doc target for `{source}`.")
558
+ kind, path = targets[0]
559
+ section = _section_for_note(kind, note_kind)
560
+ cleaned = _clean_mutation_text(text, field="text")
561
+ bullet = f"- {cleaned}"
562
+ lines = _read_lines(path)
563
+ insert_at = None
564
+ for idx, line in enumerate(lines):
565
+ if line.startswith("# ") and line[2:].strip().lower() == section.lower():
566
+ insert_at = idx + 1
567
+ while insert_at < len(lines) and lines[insert_at].strip().startswith("- "):
568
+ insert_at += 1
569
+ break
570
+ changed = True
571
+ if insert_at is None:
572
+ lines.extend(["", f"# {section}", bullet])
573
+ else:
574
+ existing = {line.strip() for line in lines if line.strip().startswith("- ")}
575
+ if bullet in existing:
576
+ changed = False
577
+ else:
578
+ lines.insert(insert_at, bullet)
579
+ if changed and not dry_run:
580
+ path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
581
+ refresh_workflow_mermaid_signature_file(path, kind, dry_run=False, repo_root=repo_root)
582
+ return {
583
+ "path": path.relative_to(repo_root).as_posix(),
584
+ "ref": path.stem,
585
+ "kind": kind,
586
+ "section": section,
587
+ "text": cleaned,
588
+ "changed": changed,
589
+ "dry_run": dry_run,
590
+ }
591
+
592
+
345
593
  def _graph_payload(repo_root: Path, *, config: dict[str, object] | None = None) -> dict[str, object]:
346
594
  docs = _load_workflow_docs(repo_root)
347
595
  nodes = []
@@ -482,6 +730,50 @@ def build_parser() -> argparse.ArgumentParser:
482
730
  schema_status.add_argument("--format", choices=("text", "json"), default="text")
483
731
  schema_status.set_defaults(func=cmd_schema_status)
484
732
 
733
+ read_doc = sub.add_parser("read-doc", help="Read a bounded workflow document payload by ref or path.")
734
+ read_doc.add_argument("source", help="Workflow ref or repo-relative path.")
735
+ read_doc.add_argument("--max-chars", type=int, default=4000)
736
+ read_doc.add_argument("--section", action="append", default=[], help="Section heading to include; repeatable.")
737
+ read_doc.add_argument("--format", choices=("text", "json"), default="text")
738
+ read_doc.set_defaults(func=cmd_read_doc)
739
+
740
+ list_docs = sub.add_parser("list-docs", help="List workflow docs by bounded criteria.")
741
+ list_docs.add_argument("--kind", choices=("all", "request", "backlog", "task"), default="all")
742
+ list_docs.add_argument("--status", default=None)
743
+ list_docs.add_argument("--ref-prefix", default=None)
744
+ list_docs.add_argument("--limit", type=int, default=50)
745
+ list_docs.add_argument("--format", choices=("text", "json"), default="text")
746
+ list_docs.set_defaults(func=cmd_list_docs)
747
+
748
+ search_docs = sub.add_parser("search-docs", help="Search approved workflow docs with bounded snippets.")
749
+ search_docs.add_argument("query")
750
+ search_docs.add_argument("--kind", choices=("all", "request", "backlog", "task"), default="all")
751
+ search_docs.add_argument("--status", default=None)
752
+ search_docs.add_argument("--limit", type=int, default=20)
753
+ search_docs.add_argument("--max-snippet-chars", type=int, default=240)
754
+ search_docs.add_argument("--format", choices=("text", "json"), default="text")
755
+ search_docs.set_defaults(func=cmd_search_docs)
756
+
757
+ update_indicators = sub.add_parser("update-indicators", help="Update approved indicators on one workflow doc.")
758
+ update_indicators.add_argument("source", help="Workflow ref or repo-relative path.")
759
+ update_indicators.add_argument("--status")
760
+ update_indicators.add_argument("--progress")
761
+ update_indicators.add_argument("--understanding")
762
+ update_indicators.add_argument("--confidence")
763
+ update_indicators.add_argument("--theme")
764
+ update_indicators.add_argument("--complexity")
765
+ update_indicators.add_argument("--format", choices=("text", "json"), default="text")
766
+ update_indicators.add_argument("--dry-run", action="store_true")
767
+ update_indicators.set_defaults(func=cmd_update_indicators)
768
+
769
+ append_note = sub.add_parser("append-note", help="Append a bounded note to an approved workflow section.")
770
+ append_note.add_argument("source", help="Workflow ref or repo-relative path.")
771
+ append_note.add_argument("--section", choices=("report", "validation", "decision"), required=True)
772
+ append_note.add_argument("--text", required=True)
773
+ append_note.add_argument("--format", choices=("text", "json"), default="text")
774
+ append_note.add_argument("--dry-run", action="store_true")
775
+ append_note.set_defaults(func=cmd_append_note)
776
+
485
777
  context_pack = sub.add_parser("context-pack", help="Build a compact context pack from workflow docs.")
486
778
  context_pack.add_argument("ref", help="Seed workflow ref for the context pack.")
487
779
  context_pack.add_argument("--mode", choices=("summary-only", "diff-first", "full"), default="summary-only")
@@ -526,6 +818,26 @@ def _build_help() -> str:
526
818
  " Build a compact JSON context pack from workflow docs.",
527
819
  " Flags: --mode {summary-only,diff-first,full}, --profile {tiny,normal,deep}, --out, --format {text,json}, --dry-run",
528
820
  "",
821
+ " read-doc <source>",
822
+ " Read a bounded workflow document payload by ref or path.",
823
+ " Flags: --max-chars, --section, --format {text,json}",
824
+ "",
825
+ " list-docs",
826
+ " List workflow docs by bounded criteria.",
827
+ " Flags: --kind {all,request,backlog,task}, --status, --ref-prefix, --limit, --format {text,json}",
828
+ "",
829
+ " search-docs <query>",
830
+ " Search approved workflow docs with bounded snippets.",
831
+ " Flags: --kind {all,request,backlog,task}, --status, --limit, --max-snippet-chars, --format {text,json}",
832
+ "",
833
+ " update-indicators <source>",
834
+ " Update approved indicators on one workflow doc.",
835
+ " Flags: --status, --progress, --understanding, --confidence, --theme, --complexity, --format {text,json}, --dry-run",
836
+ "",
837
+ " append-note <source>",
838
+ " Append a bounded note to an approved workflow section.",
839
+ " Flags: --section {report,validation,decision}, --text, --format {text,json}, --dry-run",
840
+ "",
529
841
  " export-graph",
530
842
  " Export workflow relationships as a machine-readable graph.",
531
843
  " Flags: --out, --format {text,json}, --dry-run",
@@ -606,6 +918,91 @@ def _build_subcommand_help(command: str) -> str:
606
918
  " logics-manager sync context-pack req_001_my_request --out logics/context-pack.json",
607
919
  ]
608
920
  )
921
+ if command == "read-doc":
922
+ return "\n".join(
923
+ [
924
+ "Logics Sync Read Doc",
925
+ "Read a bounded workflow document payload by ref or path.",
926
+ "",
927
+ "Usage:",
928
+ " logics-manager sync read-doc <source> [args...]",
929
+ "",
930
+ "Flags:",
931
+ " --max-chars",
932
+ " --section",
933
+ " --format {text,json}",
934
+ ]
935
+ )
936
+ if command == "list-docs":
937
+ return "\n".join(
938
+ [
939
+ "Logics Sync List Docs",
940
+ "List workflow docs by bounded criteria.",
941
+ "",
942
+ "Usage:",
943
+ " logics-manager sync list-docs [args...]",
944
+ "",
945
+ "Flags:",
946
+ " --kind {all,request,backlog,task}",
947
+ " --status",
948
+ " --ref-prefix",
949
+ " --limit",
950
+ " --format {text,json}",
951
+ ]
952
+ )
953
+ if command == "search-docs":
954
+ return "\n".join(
955
+ [
956
+ "Logics Sync Search Docs",
957
+ "Search approved workflow docs with bounded snippets.",
958
+ "",
959
+ "Usage:",
960
+ " logics-manager sync search-docs <query> [args...]",
961
+ "",
962
+ "Flags:",
963
+ " --kind {all,request,backlog,task}",
964
+ " --status",
965
+ " --limit",
966
+ " --max-snippet-chars",
967
+ " --format {text,json}",
968
+ ]
969
+ )
970
+ if command == "update-indicators":
971
+ return "\n".join(
972
+ [
973
+ "Logics Sync Update Indicators",
974
+ "Update approved indicators on one workflow doc.",
975
+ "",
976
+ "Usage:",
977
+ " logics-manager sync update-indicators <source> [args...]",
978
+ "",
979
+ "Flags:",
980
+ " --status",
981
+ " --progress",
982
+ " --understanding",
983
+ " --confidence",
984
+ " --theme",
985
+ " --complexity",
986
+ " --format {text,json}",
987
+ " --dry-run",
988
+ ]
989
+ )
990
+ if command == "append-note":
991
+ return "\n".join(
992
+ [
993
+ "Logics Sync Append Note",
994
+ "Append a bounded note to an approved workflow section.",
995
+ "",
996
+ "Usage:",
997
+ " logics-manager sync append-note <source> --section <section> --text <text> [args...]",
998
+ "",
999
+ "Flags:",
1000
+ " --section {report,validation,decision}",
1001
+ " --text",
1002
+ " --format {text,json}",
1003
+ " --dry-run",
1004
+ ]
1005
+ )
609
1006
  if command == "export-graph":
610
1007
  return "\n".join(
611
1008
  [
@@ -689,6 +1086,84 @@ def cmd_schema_status(args: argparse.Namespace) -> dict[str, object]:
689
1086
  return {"command": "sync", "kind": "schema-status", "repo_root": repo_root.as_posix(), **payload}
690
1087
 
691
1088
 
1089
+ def _bounded_positive(value: int, *, default: int, maximum: int) -> int:
1090
+ if value <= 0:
1091
+ return default
1092
+ return min(value, maximum)
1093
+
1094
+
1095
+ def cmd_read_doc(args: argparse.Namespace) -> dict[str, object]:
1096
+ repo_root = _find_repo_root(Path.cwd())
1097
+ payload = read_logics_doc_payload(repo_root, args.source, max_chars=_bounded_positive(args.max_chars, default=4000, maximum=12000), sections=args.section or None)
1098
+ if args.format == "json":
1099
+ print(json.dumps(payload, indent=2, sort_keys=True))
1100
+ else:
1101
+ print(f"{payload['ref']} ({payload['kind']}): {payload['title']}")
1102
+ print(f"- path: {payload['path']}")
1103
+ print(f"- status: {payload['status']}")
1104
+ print(f"- truncated: {payload['truncated']}")
1105
+ return {"command": "sync", "kind": "read-doc", "repo_root": repo_root.as_posix(), **payload}
1106
+
1107
+
1108
+ def cmd_list_docs(args: argparse.Namespace) -> dict[str, object]:
1109
+ repo_root = _find_repo_root(Path.cwd())
1110
+ payload = list_logics_docs_payload(repo_root, kind=args.kind, status=args.status, ref_prefix=args.ref_prefix, limit=_bounded_positive(args.limit, default=50, maximum=200))
1111
+ if args.format == "json":
1112
+ print(json.dumps(payload, indent=2, sort_keys=True))
1113
+ else:
1114
+ print(f"Workflow docs: {payload['returned_count']} returned of {payload['total_count']}")
1115
+ for item in payload["items"]:
1116
+ print(f"- {item['ref']} [{item['status']}]: {item['title']}")
1117
+ return {"command": "sync", "kind": "list-docs", "repo_root": repo_root.as_posix(), **payload}
1118
+
1119
+
1120
+ def cmd_search_docs(args: argparse.Namespace) -> dict[str, object]:
1121
+ repo_root = _find_repo_root(Path.cwd())
1122
+ payload = search_logics_docs_payload(
1123
+ repo_root,
1124
+ args.query,
1125
+ kind=args.kind,
1126
+ status=args.status,
1127
+ limit=_bounded_positive(args.limit, default=20, maximum=100),
1128
+ max_snippet_chars=_bounded_positive(args.max_snippet_chars, default=240, maximum=1000),
1129
+ )
1130
+ if args.format == "json":
1131
+ print(json.dumps(payload, indent=2, sort_keys=True))
1132
+ else:
1133
+ print(f"Search `{payload['query']}`: {payload['returned_count']} match(es)")
1134
+ for match in payload["matches"]:
1135
+ print(f"- {match['ref']}:{match['line']} {match['title']}")
1136
+ return {"command": "sync", "kind": "search-docs", "repo_root": repo_root.as_posix(), **payload}
1137
+
1138
+
1139
+ def cmd_update_indicators(args: argparse.Namespace) -> dict[str, object]:
1140
+ repo_root = _find_repo_root(Path.cwd())
1141
+ indicators = {
1142
+ "Status": args.status,
1143
+ "Progress": args.progress,
1144
+ "Understanding": args.understanding,
1145
+ "Confidence": args.confidence,
1146
+ "Theme": args.theme,
1147
+ "Complexity": args.complexity,
1148
+ }
1149
+ payload = update_workflow_indicators_payload(repo_root, args.source, {key: value for key, value in indicators.items() if value is not None}, dry_run=args.dry_run)
1150
+ if args.format == "json":
1151
+ print(json.dumps(payload, indent=2, sort_keys=True))
1152
+ else:
1153
+ print(f"Updated indicators for {payload['path']} (changed: {payload['changed']}).")
1154
+ return {"command": "sync", "kind": "update-indicators", "repo_root": repo_root.as_posix(), **payload}
1155
+
1156
+
1157
+ def cmd_append_note(args: argparse.Namespace) -> dict[str, object]:
1158
+ repo_root = _find_repo_root(Path.cwd())
1159
+ payload = append_workflow_note_payload(repo_root, args.source, note_kind=args.section, text=args.text, dry_run=args.dry_run)
1160
+ if args.format == "json":
1161
+ print(json.dumps(payload, indent=2, sort_keys=True))
1162
+ else:
1163
+ print(f"Appended {args.section} note to {payload['path']} (changed: {payload['changed']}).")
1164
+ return {"command": "sync", "kind": "append-note", "repo_root": repo_root.as_posix(), **payload}
1165
+
1166
+
692
1167
  def cmd_context_pack(args: argparse.Namespace) -> dict[str, object]:
693
1168
  repo_root = _find_repo_root(Path.cwd())
694
1169
  payload = _build_context_pack(repo_root, args.ref, mode=args.mode, profile=args.profile, config=None)
@@ -733,7 +1208,7 @@ def main(argv: list[str]) -> int:
733
1208
  if not argv or argv[0] in ("-h", "--help"):
734
1209
  _print_help(_build_help())
735
1210
  return 0
736
- if argv[0] in {"close-eligible-requests", "refresh-mermaid-signatures", "schema-status", "context-pack", "export-graph"} and len(argv) > 1 and argv[1] in ("-h", "--help"):
1211
+ if argv[0] in {"close-eligible-requests", "refresh-mermaid-signatures", "schema-status", "read-doc", "list-docs", "search-docs", "update-indicators", "append-note", "context-pack", "export-graph"} and len(argv) > 1 and argv[1] in ("-h", "--help"):
737
1212
  _print_help(_build_subcommand_help(argv[0]))
738
1213
  return 0
739
1214
  parser = build_parser()
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@grifhinz/logics-manager",
3
3
  "displayName": "Logics Orchestrator",
4
4
  "description": "Visual orchestration for Logics workflows inside VS Code.",
5
- "version": "2.0.5",
5
+ "version": "2.1.0",
6
6
  "publisher": "cdx-logics",
7
7
  "icon": "media/icon.png",
8
8
  "repository": {
@@ -159,5 +159,13 @@
159
159
  "vitest": "^4.1.2",
160
160
  "yaml": "^2.8.3",
161
161
  "yauzl": "^3.2.0"
162
+ },
163
+ "overrides": {
164
+ "fast-uri": "3.1.2",
165
+ "minimatch@10.2.5": {
166
+ "brace-expansion": "5.0.6"
167
+ },
168
+ "qs": "6.15.2",
169
+ "ws": "8.21.0"
162
170
  }
163
171
  }
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "logics-manager"
7
- version = "2.0.5"
7
+ version = "2.1.0"
8
8
  description = "Canonical Logics CLI"
9
9
  requires-python = ">=3.10"
10
10