@jaguilar87/gaia 5.1.0 → 5.1.1

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 (92) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +11 -0
  4. package/agents/gaia-orchestrator.md +4 -10
  5. package/bin/README.md +1 -1
  6. package/bin/cli/approvals.py +0 -144
  7. package/bin/cli/cleanup.py +1 -30
  8. package/bin/cli/context.py +379 -6
  9. package/bin/cli/doctor.py +1 -1
  10. package/bin/cli/history.py +18 -6
  11. package/bin/cli/install.py +4 -2
  12. package/bin/cli/memory.py +86 -10
  13. package/bin/cli/metrics.py +1216 -275
  14. package/bin/cli/query.py +89 -0
  15. package/bin/cli/scan.py +68 -2
  16. package/bin/cli/uninstall.py +9 -5
  17. package/bin/pre-publish-validate.js +7 -4
  18. package/gaia/project.py +49 -20
  19. package/gaia/store/reader.py +175 -11
  20. package/gaia/store/schema.sql +180 -1
  21. package/gaia/store/writer.py +1146 -165
  22. package/hooks/README.md +2 -2
  23. package/hooks/adapters/claude_code.py +6 -12
  24. package/hooks/elicitation_result.py +4 -9
  25. package/hooks/modules/agents/contract_validator.py +2 -1
  26. package/hooks/modules/agents/handoff_persister.py +5 -220
  27. package/hooks/modules/agents/task_info_builder.py +9 -2
  28. package/hooks/modules/agents/transcript_reader.py +41 -21
  29. package/hooks/modules/audit/logger.py +8 -3
  30. package/hooks/modules/context/context_injector.py +10 -3
  31. package/hooks/modules/core/__init__.py +5 -2
  32. package/hooks/modules/core/logging_setup.py +60 -0
  33. package/hooks/modules/core/paths.py +0 -12
  34. package/hooks/modules/security/approval_grants.py +24 -92
  35. package/hooks/modules/security/gaia_db_write_guard.py +7 -2
  36. package/hooks/modules/security/mutative_verbs.py +194 -11
  37. package/hooks/modules/session/pending_scanner.py +24 -222
  38. package/hooks/modules/session/session_manifest.py +13 -288
  39. package/hooks/post_compact.py +4 -9
  40. package/hooks/post_tool_use.py +4 -9
  41. package/hooks/pre_compact.py +5 -10
  42. package/hooks/pre_tool_use.py +4 -11
  43. package/hooks/session_end_hook.py +4 -9
  44. package/hooks/session_start.py +18 -21
  45. package/hooks/stop_hook.py +5 -10
  46. package/hooks/subagent_start.py +5 -10
  47. package/hooks/subagent_stop.py +9 -23
  48. package/hooks/task_completed.py +5 -10
  49. package/hooks/user_prompt_submit.py +4 -9
  50. package/package.json +1 -1
  51. package/pyproject.toml +1 -1
  52. package/scripts/migrations/schema.checksum +2 -2
  53. package/scripts/migrations/v21_to_v22.sql +80 -0
  54. package/scripts/migrations/v22_to_v23.sql +20 -0
  55. package/scripts/migrations/v23_to_v24.sql +30 -0
  56. package/scripts/migrations/v24_to_v25.sql +77 -0
  57. package/scripts/migrations/v25_to_v26.sql +116 -0
  58. package/skills/README.md +8 -0
  59. package/skills/agent-approval-protocol/SKILL.md +27 -27
  60. package/skills/agent-approval-protocol/reference.md +20 -12
  61. package/skills/agent-protocol/SKILL.md +1 -1
  62. package/skills/agent-protocol/examples.md +17 -9
  63. package/skills/agent-response/SKILL.md +1 -1
  64. package/skills/diagram-builder/GLOSSARY.md +77 -0
  65. package/skills/diagram-builder/SKILL.md +156 -0
  66. package/skills/diagram-builder/assets/README.md +45 -0
  67. package/skills/diagram-builder/assets/data/data.generated.js +77 -0
  68. package/skills/diagram-builder/assets/data/document.yaml +12 -0
  69. package/skills/diagram-builder/assets/data/pages/overview.yaml +47 -0
  70. package/skills/diagram-builder/assets/engine/build-data.mjs +51 -0
  71. package/skills/diagram-builder/assets/engine/engine.js +863 -0
  72. package/skills/diagram-builder/assets/index.html +638 -0
  73. package/skills/diagram-builder/assets/package.json +15 -0
  74. package/skills/diagram-builder/assets/tools/verify.mjs +128 -0
  75. package/skills/diagram-builder/reference.md +444 -0
  76. package/skills/execution/SKILL.md +3 -3
  77. package/skills/gaia-patterns/reference.md +2 -2
  78. package/skills/memory/SKILL.md +49 -0
  79. package/skills/orchestrator-present-approval/SKILL.md +90 -79
  80. package/skills/orchestrator-present-approval/reference.md +59 -42
  81. package/skills/orchestrator-present-approval/template.md +16 -14
  82. package/skills/pending-approvals/SKILL.md +80 -29
  83. package/skills/pending-approvals/reference.md +48 -60
  84. package/skills/security-tiers/SKILL.md +1 -1
  85. package/skills/subagent-request-approval/SKILL.md +65 -68
  86. package/skills/subagent-request-approval/reference.md +63 -63
  87. package/skills/visual-verify/SKILL.md +107 -0
  88. package/skills/visual-verify/scripts/screenshot.js +210 -0
  89. package/tools/scan/classify.py +584 -15
  90. package/tools/scan/migrate_workspace.py +9 -4
  91. package/tools/scan/store_populator.py +231 -2
  92. package/tools/scan/tests/conftest.py +31 -0
@@ -348,20 +348,29 @@ def _cmd_query(args) -> int:
348
348
 
349
349
 
350
350
  def _cmd_wipe(args) -> int:
351
- """Handle `gaia context wipe --workspace W [--yes]`.
351
+ """Handle `gaia context wipe --workspace W [--yes] [--purge-memory]`.
352
352
 
353
- Deletes all rows for the workspace (CASCADE removes children).
354
- Requires interactive confirmation unless --yes is passed.
353
+ Deletes the scannable rows for the workspace (CASCADE removes children).
354
+ scan-v2 SV3: curated memory is PRESERVED across the wipe by default; pass
355
+ --purge-memory to also destroy it (explicit human curation). Requires
356
+ interactive confirmation unless --yes is passed.
355
357
  """
356
358
  workspace = getattr(args, "workspace", None)
357
359
  if not workspace:
358
360
  print("gaia context wipe: --workspace is required", file=sys.stderr)
359
361
  return 2
360
362
 
363
+ purge_memory = getattr(args, "purge_memory", False)
361
364
  if not getattr(args, "yes", False):
365
+ mem_note = (
366
+ "curated memory WILL ALSO BE DESTROYED"
367
+ if purge_memory
368
+ else "curated memory will be PRESERVED"
369
+ )
362
370
  try:
363
371
  ans = input(
364
- f"gaia context wipe: about to delete ALL rows for workspace {workspace!r}.\n"
372
+ f"gaia context wipe: about to delete workspace {workspace!r} "
373
+ f"rows ({mem_note}).\n"
365
374
  f"Type 'yes' to confirm: "
366
375
  )
367
376
  except EOFError:
@@ -376,13 +385,291 @@ def _cmd_wipe(args) -> int:
376
385
  print(f"gaia context wipe: failed to import store: {exc}", file=sys.stderr)
377
386
  return 1
378
387
 
388
+ purge_memory = getattr(args, "purge_memory", False)
379
389
  try:
380
- wipe_workspace(workspace)
390
+ # scan-v2 SV3 (Vector 4): preserve curated memory across the wipe by
391
+ # default. --purge-memory is the explicit-human-curation escape hatch
392
+ # that restores the original full CASCADE (memory destroyed).
393
+ wipe_workspace(workspace, preserve_memory=not purge_memory)
381
394
  except Exception as exc:
382
395
  print(f"gaia context wipe: error: {exc}", file=sys.stderr)
383
396
  return 1
384
397
 
385
- print(f"Wiped workspace: {workspace}")
398
+ if purge_memory:
399
+ print(f"Wiped workspace (memory PURGED): {workspace}")
400
+ else:
401
+ print(f"Wiped workspace (memory preserved): {workspace}")
402
+ return 0
403
+
404
+
405
+ def _cmd_move_project(args) -> int:
406
+ """Handle ``gaia context move-project --decision D --from-workspace W1
407
+ --from-name N1 --to-workspace W2 --to-name N2 [--dry-run] [--yes] [--json]``.
408
+
409
+ EXECUTES a human-adjudicated scan-v2 move_candidate. `gaia scan` only
410
+ DETECTS and REPORTS a move (as `move_candidates`); this is the write path
411
+ that resolves one:
412
+
413
+ --decision movido -> re-key / supersede the OLD projects row to link it
414
+ to the successor (writes `superseded_by`, never
415
+ hard-deletes). Curated memory / contracts are only
416
+ PROPOSED for relocation, not auto-moved -- use
417
+ `move-memory` / `move-contracts` for those.
418
+ --decision duplicado -> structural no-op: both rows are legitimately
419
+ --decision worktree independent and are left exactly as they are.
420
+
421
+ Named with a leading `move` token so it hyphen-splits to a MUTATIVE_VERB and
422
+ gates as T3 without any security-layer change (same convention as
423
+ `move-contracts` / `move-memory`); `--dry-run` downgrades to a preview.
424
+ """
425
+ decision = getattr(args, "decision", None)
426
+ from_ws = getattr(args, "from_workspace", None)
427
+ from_name = getattr(args, "from_name", None)
428
+ to_ws = getattr(args, "to_workspace", None)
429
+ to_name = getattr(args, "to_name", None)
430
+ dry_run = getattr(args, "dry_run", False)
431
+ as_json = getattr(args, "json", False)
432
+
433
+ if decision not in ("movido", "duplicado", "worktree"):
434
+ print(
435
+ "gaia context move-project: --decision must be one of "
436
+ "movido/duplicado/worktree",
437
+ file=sys.stderr,
438
+ )
439
+ return 2
440
+ if not all((from_ws, from_name, to_ws, to_name)):
441
+ print(
442
+ "gaia context move-project: --from-workspace/--from-name/"
443
+ "--to-workspace/--to-name are all required",
444
+ file=sys.stderr,
445
+ )
446
+ return 2
447
+
448
+ # duplicado / worktree: legitimate independent rows -> structural no-op.
449
+ if decision in ("duplicado", "worktree"):
450
+ result = {
451
+ "status": "noop",
452
+ "decision": decision,
453
+ "from": {"workspace": from_ws, "name": from_name},
454
+ "to": {"workspace": to_ws, "name": to_name},
455
+ "reason": (
456
+ f"decision {decision!r}: both rows are legitimately independent "
457
+ "-- left untouched (no re-key, no supersede)."
458
+ ),
459
+ }
460
+ if as_json:
461
+ print(json.dumps(result, indent=2, default=str))
462
+ else:
463
+ print(f"[{decision}] no-op: left both project rows untouched "
464
+ f"({from_ws}/{from_name} and {to_ws}/{to_name}).")
465
+ return 0
466
+
467
+ # movido:
468
+ try:
469
+ from gaia.store.writer import resolve_move_candidate
470
+ except Exception as exc:
471
+ print(f"gaia context move-project: failed to import store: {exc}", file=sys.stderr)
472
+ return 1
473
+
474
+ try:
475
+ preview = resolve_move_candidate(
476
+ from_ws, from_name, to_ws, to_name, dry_run=True,
477
+ )
478
+ except ValueError as exc:
479
+ print(f"gaia context move-project: {exc}", file=sys.stderr)
480
+ return 1
481
+
482
+ if dry_run:
483
+ if as_json:
484
+ print(json.dumps(preview, indent=2, default=str))
485
+ else:
486
+ print(f"[dry-run] move-project (movido) {from_ws}/{from_name} -> "
487
+ f"{to_ws}/{to_name}:")
488
+ print(f" action : {preview['action']}")
489
+ print(f" superseded_by : {preview['superseded_by']}")
490
+ rel = preview["proposed_relocations"]
491
+ print(f" proposed relocations (NOT auto-moved): "
492
+ f"memory={rel['memory']} contracts={rel['contracts']}")
493
+ print(" -> relocate collateral with `gaia context move-memory` / "
494
+ "`move-contracts` if desired.")
495
+ return 0
496
+
497
+ if not getattr(args, "yes", False):
498
+ print(f"About to resolve move (movido): re-key/supersede "
499
+ f"{from_ws}/{from_name} -> {to_ws}/{to_name} "
500
+ f"(action={preview['action']}, no hard-delete).")
501
+ try:
502
+ ans = input("Type 'yes' to confirm: ")
503
+ except EOFError:
504
+ ans = ""
505
+ if ans.strip().lower() != "yes":
506
+ print("Aborted (no confirmation).")
507
+ return 1
508
+
509
+ try:
510
+ result = resolve_move_candidate(from_ws, from_name, to_ws, to_name)
511
+ except ValueError as exc:
512
+ print(f"gaia context move-project: {exc}", file=sys.stderr)
513
+ return 1
514
+
515
+ if as_json:
516
+ print(json.dumps(result, indent=2, default=str))
517
+ else:
518
+ rel = result["proposed_relocations"]
519
+ print(f"Resolved move (movido): {from_ws}/{from_name} -> {to_ws}/{to_name} "
520
+ f"(action={result['action']}, superseded_by={result['superseded_by']}).")
521
+ print(f"Proposed (NOT moved): memory={rel['memory']} contracts={rel['contracts']} "
522
+ f"still keyed to {from_ws!r} -- relocate with move-memory/move-contracts.")
523
+ return 0
524
+
525
+
526
+ def _cmd_move_contracts(args) -> int:
527
+ """Handle ``gaia context move-contracts --from W1 --to W2 --contract C ...``.
528
+
529
+ Re-keys project_context_contracts rows between workspaces (the only
530
+ correction path for a mis-keyed contract, since `gaia scan` never touches
531
+ that table). At least one --contract is required.
532
+ """
533
+ from_ws = getattr(args, "from_workspace", None)
534
+ to_ws = getattr(args, "to_workspace", None)
535
+ contracts = getattr(args, "contract", None) or []
536
+ on_conflict = getattr(args, "on_conflict", "error")
537
+ dry_run = getattr(args, "dry_run", False)
538
+ as_json = getattr(args, "json", False)
539
+
540
+ if not from_ws or not to_ws:
541
+ print("gaia context move-contracts: --from and --to are required", file=sys.stderr)
542
+ return 2
543
+ if not contracts:
544
+ print("gaia context move-contracts: at least one --contract is required", file=sys.stderr)
545
+ return 2
546
+
547
+ try:
548
+ from gaia.store.writer import relocate_contracts
549
+ except Exception as exc:
550
+ print(f"gaia context move-contracts: failed to import store: {exc}", file=sys.stderr)
551
+ return 1
552
+
553
+ try:
554
+ preview = relocate_contracts(
555
+ from_ws, to_ws, contracts, on_conflict=on_conflict, dry_run=True
556
+ )
557
+ except ValueError as exc:
558
+ print(f"gaia context move-contracts: {exc}", file=sys.stderr)
559
+ return 1
560
+
561
+ if dry_run:
562
+ if as_json:
563
+ print(json.dumps(preview, indent=2, default=str))
564
+ else:
565
+ print(f"[dry-run] move-contracts {from_ws!r} -> {to_ws!r}:")
566
+ print(f" would move : {preview['moved']}")
567
+ print(f" skipped : {preview['skipped']}")
568
+ print(f" missing : {preview['missing']}")
569
+ print(f" overwritten: {preview['overwritten']}")
570
+ return 0
571
+
572
+ if not getattr(args, "yes", False):
573
+ print(f"About to move {len(preview['moved'])} contract(s) from {from_ws!r} to {to_ws!r}: {preview['moved']}")
574
+ try:
575
+ ans = input("Type 'yes' to confirm: ")
576
+ except EOFError:
577
+ ans = ""
578
+ if ans.strip().lower() != "yes":
579
+ print("Aborted (no confirmation).")
580
+ return 1
581
+
582
+ try:
583
+ result = relocate_contracts(from_ws, to_ws, contracts, on_conflict=on_conflict)
584
+ except ValueError as exc:
585
+ print(f"gaia context move-contracts: {exc}", file=sys.stderr)
586
+ return 1
587
+
588
+ if as_json:
589
+ print(json.dumps(result, indent=2, default=str))
590
+ else:
591
+ print(f"Moved contracts {from_ws!r} -> {to_ws!r}: moved={result['moved']} "
592
+ f"skipped={result['skipped']} missing={result['missing']} "
593
+ f"overwritten={result['overwritten']}")
594
+ return 0
595
+
596
+
597
+ def _cmd_move_memory(args) -> int:
598
+ """Handle ``gaia context move-memory --from W1 --to W2 --name N ...``.
599
+
600
+ Re-keys curated `memory` rows (and their intra-set memory_links) between
601
+ workspaces -- the only correction path for a mis-keyed memory note, since
602
+ `gaia scan` never touches that table. Subject to the curated-memory write
603
+ guard: run from a human shell or the orchestrator/operator context (a
604
+ non-curator subagent dispatch is refused). At least one --name is required.
605
+ """
606
+ from_ws = getattr(args, "from_workspace", None)
607
+ to_ws = getattr(args, "to_workspace", None)
608
+ names = getattr(args, "name", None) or []
609
+ on_conflict = getattr(args, "on_conflict", "error")
610
+ dry_run = getattr(args, "dry_run", False)
611
+ as_json = getattr(args, "json", False)
612
+
613
+ if not from_ws or not to_ws:
614
+ print("gaia context move-memory: --from and --to are required", file=sys.stderr)
615
+ return 2
616
+ if not names:
617
+ print("gaia context move-memory: at least one --name is required", file=sys.stderr)
618
+ return 2
619
+
620
+ try:
621
+ from gaia.store.writer import relocate_memory, MemoryWriteForbidden
622
+ except Exception as exc:
623
+ print(f"gaia context move-memory: failed to import store: {exc}", file=sys.stderr)
624
+ return 1
625
+
626
+ try:
627
+ preview = relocate_memory(
628
+ from_ws, to_ws, names, on_conflict=on_conflict, dry_run=True
629
+ )
630
+ except MemoryWriteForbidden as exc:
631
+ print(f"gaia context move-memory: {exc}", file=sys.stderr)
632
+ return 1
633
+ except ValueError as exc:
634
+ print(f"gaia context move-memory: {exc}", file=sys.stderr)
635
+ return 1
636
+
637
+ if dry_run:
638
+ if as_json:
639
+ print(json.dumps(preview, indent=2, default=str))
640
+ else:
641
+ print(f"[dry-run] move-memory {from_ws!r} -> {to_ws!r}:")
642
+ print(f" would move : {preview['moved']}")
643
+ print(f" skipped : {preview['skipped']}")
644
+ print(f" missing : {preview['missing']}")
645
+ print(f" overwritten : {preview['overwritten']}")
646
+ print(f" links_moved : {preview['links_moved']}")
647
+ print(f" partial_links: {preview['partial_links']}")
648
+ return 0
649
+
650
+ if not getattr(args, "yes", False):
651
+ print(f"About to move {len(preview['moved'])} memory row(s) from {from_ws!r} to {to_ws!r}: {preview['moved']}")
652
+ try:
653
+ ans = input("Type 'yes' to confirm: ")
654
+ except EOFError:
655
+ ans = ""
656
+ if ans.strip().lower() != "yes":
657
+ print("Aborted (no confirmation).")
658
+ return 1
659
+
660
+ try:
661
+ result = relocate_memory(from_ws, to_ws, names, on_conflict=on_conflict)
662
+ except (MemoryWriteForbidden, ValueError) as exc:
663
+ print(f"gaia context move-memory: {exc}", file=sys.stderr)
664
+ return 1
665
+
666
+ if as_json:
667
+ print(json.dumps(result, indent=2, default=str))
668
+ else:
669
+ print(f"Moved memory {from_ws!r} -> {to_ws!r}: moved={result['moved']} "
670
+ f"skipped={result['skipped']} missing={result['missing']} "
671
+ f"overwritten={result['overwritten']} links_moved={result['links_moved']} "
672
+ f"partial_links={result['partial_links']}")
386
673
  return 0
387
674
 
388
675
 
@@ -500,6 +787,86 @@ def register(subparsers) -> None:
500
787
  default=False,
501
788
  help="Skip interactive confirmation",
502
789
  )
790
+ wipe_parser.add_argument(
791
+ "--purge-memory",
792
+ action="store_true",
793
+ default=False,
794
+ help="Also destroy curated memory (default: memory is preserved across "
795
+ "the wipe). Explicit human curation only.",
796
+ )
797
+
798
+ # gaia context move-contracts --from W1 --to W2 --contract C ...
799
+ # (T3: the verb token 'move-contracts' hyphen-splits to 'move', which is in
800
+ # MUTATIVE_VERBS -- gated as T3 without touching the security layer.)
801
+ mv_parser = ctx_subparsers.add_parser(
802
+ "move-contracts",
803
+ help="Re-key project_context_contracts rows between workspaces",
804
+ )
805
+ mv_parser.add_argument("--from", dest="from_workspace", metavar="W1", required=True,
806
+ help="Source workspace (current, wrong key)")
807
+ mv_parser.add_argument("--to", dest="to_workspace", metavar="W2", required=True,
808
+ help="Destination workspace (correct key)")
809
+ mv_parser.add_argument("--contract", metavar="C", action="append", default=None,
810
+ help="Contract name to move (repeatable)")
811
+ mv_parser.add_argument("--on-conflict", dest="on_conflict", default="error",
812
+ choices=("error", "skip", "overwrite"),
813
+ help="Behavior when target already has the contract")
814
+ mv_parser.add_argument("--dry-run", action="store_true", default=False,
815
+ help="Preview the move without mutating")
816
+ mv_parser.add_argument("--json", action="store_true", default=False,
817
+ help="Emit JSON")
818
+ mv_parser.add_argument("--yes", action="store_true", default=False,
819
+ help="Skip interactive confirmation")
820
+
821
+ # gaia context move-memory --from W1 --to W2 --name N ...
822
+ # (T3: the verb token 'move-memory' hyphen-splits to 'move', which is in
823
+ # MUTATIVE_VERBS -- gated as T3 without touching the security layer.)
824
+ mm_parser = ctx_subparsers.add_parser(
825
+ "move-memory",
826
+ help="Re-key curated `memory` rows (and intra-set links) between workspaces",
827
+ )
828
+ mm_parser.add_argument("--from", dest="from_workspace", metavar="W1", required=True,
829
+ help="Source workspace (current, wrong key)")
830
+ mm_parser.add_argument("--to", dest="to_workspace", metavar="W2", required=True,
831
+ help="Destination workspace (correct key)")
832
+ mm_parser.add_argument("--name", metavar="N", action="append", default=None,
833
+ help="Memory row name to move (repeatable)")
834
+ mm_parser.add_argument("--on-conflict", dest="on_conflict", default="error",
835
+ choices=("error", "skip", "overwrite"),
836
+ help="Behavior when target already has the memory row")
837
+ mm_parser.add_argument("--dry-run", action="store_true", default=False,
838
+ help="Preview the move without mutating")
839
+ mm_parser.add_argument("--json", action="store_true", default=False,
840
+ help="Emit JSON")
841
+ mm_parser.add_argument("--yes", action="store_true", default=False,
842
+ help="Skip interactive confirmation")
843
+
844
+ # gaia context move-project --decision D --from-workspace/--from-name
845
+ # --to-workspace/--to-name
846
+ # (T3: the verb token 'move-project' hyphen-splits to 'move', which is in
847
+ # MUTATIVE_VERBS -- gated as T3 without touching the security layer. This is
848
+ # the write path that EXECUTES a scan-v2 move_candidate a human adjudicated.)
849
+ mp_parser = ctx_subparsers.add_parser(
850
+ "move-project",
851
+ help="Resolve an adjudicated scan-v2 move_candidate (re-key/supersede a projects row)",
852
+ )
853
+ mp_parser.add_argument("--decision", required=True,
854
+ choices=("movido", "duplicado", "worktree"),
855
+ help="Human adjudication of the move_candidate")
856
+ mp_parser.add_argument("--from-workspace", dest="from_workspace", metavar="W1", required=True,
857
+ help="Old row workspace (move_candidate from.workspace)")
858
+ mp_parser.add_argument("--from-name", dest="from_name", metavar="N1", required=True,
859
+ help="Old row name (move_candidate from.project)")
860
+ mp_parser.add_argument("--to-workspace", dest="to_workspace", metavar="W2", required=True,
861
+ help="Successor workspace (move_candidate to.workspace)")
862
+ mp_parser.add_argument("--to-name", dest="to_name", metavar="N2", required=True,
863
+ help="Successor name (move_candidate to.project)")
864
+ mp_parser.add_argument("--dry-run", action="store_true", default=False,
865
+ help="Preview the resolution without mutating")
866
+ mp_parser.add_argument("--json", action="store_true", default=False,
867
+ help="Emit JSON")
868
+ mp_parser.add_argument("--yes", action="store_true", default=False,
869
+ help="Skip interactive confirmation")
503
870
 
504
871
 
505
872
  def cmd_context(args) -> int:
@@ -517,6 +884,12 @@ def cmd_context(args) -> int:
517
884
  return _cmd_query(args)
518
885
  if context_cmd == "wipe":
519
886
  return _cmd_wipe(args)
887
+ if context_cmd == "move-contracts":
888
+ return _cmd_move_contracts(args)
889
+ if context_cmd == "move-memory":
890
+ return _cmd_move_memory(args)
891
+ if context_cmd == "move-project":
892
+ return _cmd_move_project(args)
520
893
 
521
894
  # No sub-action: print help for the context subcommand
522
895
  import argparse
package/bin/cli/doctor.py CHANGED
@@ -206,7 +206,7 @@ def _package_root() -> Path:
206
206
  # in lock-step with the INSERT it adds to bootstrap_database.sh. If a user
207
207
  # upgrades the CLI past a schema bump but does not re-run `gaia install`,
208
208
  # `check_schema_version` raises a warning telling them how to repair.
209
- EXPECTED_SCHEMA_VERSION = 21
209
+ EXPECTED_SCHEMA_VERSION = 26
210
210
 
211
211
  # Locations the doctor reads outside the workspace.
212
212
  _INSTALL_ERROR_MARKER = Path("~/.gaia/last-install-error.json").expanduser()
@@ -13,6 +13,7 @@ Flags:
13
13
  --blocked / -b Show only BLOCKED or NEEDS_INPUT sessions
14
14
  --agent / -a NAME Filter by agent name
15
15
  --limit / -n N Max sessions to show (default 20)
16
+ --workspace NAME Workspace identity override (default: gaia.project.current())
16
17
  --json Machine-readable output
17
18
  """
18
19
 
@@ -48,7 +49,7 @@ def _find_project_root() -> Path:
48
49
  # Data readers
49
50
  # ---------------------------------------------------------------------------
50
51
 
51
- def _read_workflow_metrics(root: Path) -> list:
52
+ def _read_workflow_metrics(root: Path, workspace_override: str = None) -> list:
52
53
  """
53
54
  Read agent session history from gaia.db episodes table.
54
55
 
@@ -56,6 +57,9 @@ def _read_workflow_metrics(root: Path) -> list:
56
57
  The legacy episodic-memory/index.json and workflow-episodic-memory/metrics.jsonl
57
58
  files are no longer read here.
58
59
 
60
+ ``workspace_override`` -- explicit ``--workspace`` value from the CLI.
61
+ When set, it wins over ``gaia.project.current()`` resolution.
62
+
59
63
  Returns a list of episode dicts (rows with an 'agent' field).
60
64
  """
61
65
  import sys as _sys
@@ -71,10 +75,13 @@ def _read_workflow_metrics(root: Path) -> list:
71
75
  except ImportError:
72
76
  return []
73
77
 
74
- try:
75
- ws = _project_current(cwd=root)
76
- except Exception:
77
- ws = None
78
+ if workspace_override:
79
+ ws = workspace_override
80
+ else:
81
+ try:
82
+ ws = _project_current(cwd=root)
83
+ except Exception:
84
+ ws = None
78
85
 
79
86
  try:
80
87
  con = _store_connect()
@@ -199,6 +206,10 @@ def register(subparsers):
199
206
  metavar="N",
200
207
  help="Maximum number of sessions to show (default: 20)",
201
208
  )
209
+ p.add_argument(
210
+ "--workspace", default=None,
211
+ help="Workspace identity override. Default: gaia.project.current().",
212
+ )
202
213
  p.add_argument(
203
214
  "--json",
204
215
  action="store_true",
@@ -213,6 +224,7 @@ def cmd_history(args) -> int:
213
224
  root = _find_project_root()
214
225
  claude_dir = root / ".claude"
215
226
  as_json = getattr(args, "json", False)
227
+ workspace_override = getattr(args, "workspace", None)
216
228
 
217
229
  if not claude_dir.exists():
218
230
  if as_json:
@@ -222,7 +234,7 @@ def cmd_history(args) -> int:
222
234
  print(" Run: gaia scan\n")
223
235
  return 1
224
236
 
225
- entries = _read_workflow_metrics(root)
237
+ entries = _read_workflow_metrics(root, workspace_override)
226
238
 
227
239
  if not entries:
228
240
  if as_json:
@@ -295,7 +295,7 @@ def _run_bootstrap(db_path: str | None, verbose: bool, quiet: bool) -> dict:
295
295
 
296
296
  env = os.environ.copy()
297
297
  if db_path:
298
- env["GAIA_DB"] = str(Path(db_path).expanduser())
298
+ env["GAIA_DB"] = str(Path(db_path).expanduser().resolve())
299
299
 
300
300
  cmd = ["bash", str(_BOOTSTRAP_SCRIPT)]
301
301
 
@@ -355,7 +355,9 @@ def _seed_contract_permissions(db_path: str | None, quiet: bool) -> dict:
355
355
 
356
356
  env = os.environ.copy()
357
357
  resolved_db = (
358
- str(Path(db_path).expanduser()) if db_path else str(Path("~/.gaia/gaia.db").expanduser())
358
+ str(Path(db_path).expanduser().resolve())
359
+ if db_path
360
+ else str(Path("~/.gaia/gaia.db").expanduser().resolve())
359
361
  )
360
362
  cmd = [sys.executable, str(_SEED_CONTRACT_PERMS), "--db-path", resolved_db]
361
363