seif-cli 0.5.0__tar.gz → 0.5.2__tar.gz

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 (115) hide show
  1. {seif_cli-0.5.0/src/seif_cli.egg-info → seif_cli-0.5.2}/PKG-INFO +1 -1
  2. {seif_cli-0.5.0 → seif_cli-0.5.2}/pyproject.toml +1 -1
  3. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/analysis/stance_detector.py +51 -5
  4. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/cli/cli.py +501 -0
  5. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/cli/wrapper.py +130 -2
  6. seif_cli-0.5.2/src/seif/context/host_init.py +319 -0
  7. seif_cli-0.5.2/src/seif/context/model_probe.py +319 -0
  8. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/registry.py +126 -0
  9. seif_cli-0.5.2/src/seif/plugins/claude-code/scripts/orchestra-probe.py +40 -0
  10. {seif_cli-0.5.0 → seif_cli-0.5.2/src/seif_cli.egg-info}/PKG-INFO +1 -1
  11. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif_cli.egg-info/SOURCES.txt +6 -0
  12. seif_cli-0.5.2/tests/test_audit_host.py +108 -0
  13. seif_cli-0.5.2/tests/test_host_init.py +163 -0
  14. seif_cli-0.5.2/tests/test_model_probe.py +147 -0
  15. seif_cli-0.5.2/tests/test_rebuild_registry.py +154 -0
  16. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_stance_detector.py +49 -0
  17. seif_cli-0.5.0/src/seif/plugins/claude-code/scripts/orchestra-probe.py +0 -291
  18. {seif_cli-0.5.0 → seif_cli-0.5.2}/LICENSE +0 -0
  19. {seif_cli-0.5.0 → seif_cli-0.5.2}/MANIFEST.in +0 -0
  20. {seif_cli-0.5.0 → seif_cli-0.5.2}/README.md +0 -0
  21. {seif_cli-0.5.0 → seif_cli-0.5.2}/setup.cfg +0 -0
  22. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/__init__.py +0 -0
  23. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/__main__.py +0 -0
  24. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/analysis/__init__.py +0 -0
  25. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/analysis/physical_constants.py +0 -0
  26. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/analysis/quality_gate.py +0 -0
  27. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/analysis/transcompiler.py +0 -0
  28. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/bridge/__init__.py +0 -0
  29. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/bridge/native_client.py +0 -0
  30. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/bridge/telegram_bot.py +0 -0
  31. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/cli/__init__.py +0 -0
  32. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/cli/__main__.py +0 -0
  33. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/cli/chat.py +0 -0
  34. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/cli/identity.py +0 -0
  35. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/cli/main.py +0 -0
  36. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/cli/resonance_display.py +0 -0
  37. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/cli/serve.py +0 -0
  38. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/cli/serve_v2.py +0 -0
  39. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/cli/setup.py +0 -0
  40. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/constants.py +0 -0
  41. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/__init__.py +0 -0
  42. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/advisor.py +0 -0
  43. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/code_compressor.py +0 -0
  44. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/context_bridge.py +0 -0
  45. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/context_importer.py +0 -0
  46. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/context_manager.py +0 -0
  47. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/context_qr.py +0 -0
  48. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/cycle.py +0 -0
  49. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/file_extractor.py +0 -0
  50. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/git_context.py +0 -0
  51. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/git_hooks.py +0 -0
  52. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/ingest.py +0 -0
  53. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/nucleus.py +0 -0
  54. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/ref.py +0 -0
  55. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/seif_io.py +0 -0
  56. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/sessions.py +0 -0
  57. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/context/workspace.py +0 -0
  58. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/core/__init__.py +0 -0
  59. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/core/fingerprint.py +0 -0
  60. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/core/resonance_encoding.py +0 -0
  61. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/core/resonance_gate.py +0 -0
  62. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/core/resonance_signal.py +0 -0
  63. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/core/signing.py +0 -0
  64. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/core/timestamping.py +0 -0
  65. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/core/transfer_function.py +0 -0
  66. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/core/triple_gate.py +0 -0
  67. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/data/RESONANCE.json +0 -0
  68. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/data/__init__.py +0 -0
  69. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/data/defaults/__init__.py +0 -0
  70. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/data/defaults/circuit-recovery-v1.seif +0 -0
  71. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/data/defaults/definitions-v1.seif +0 -0
  72. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/data/defaults/ise-dissonance-v1.seif +0 -0
  73. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/data/defaults/multi-agent-sync-v1.seif +0 -0
  74. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/data/defaults/onboarding.seif +0 -0
  75. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/data/defaults/partial-attention-axiom-v1.seif +0 -0
  76. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/data/defaults/seif-cycle-v1.seif +0 -0
  77. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/data/defaults/seif-os-architecture-v1.seif +0 -0
  78. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/data/defaults/triad-convergence-v1.seif +0 -0
  79. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/data/paths.py +0 -0
  80. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/plugins/__init__.py +0 -0
  81. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/plugins/claude-code/hooks/hooks.json +0 -0
  82. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/plugins/claude-code/scripts/circuit-check.sh +0 -0
  83. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/plugins/claude-code/scripts/circuit-monitor.py +0 -0
  84. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/plugins/claude-code/scripts/classification-gate.sh +0 -0
  85. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/plugins/claude-code/scripts/kernel-seed.py +0 -0
  86. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/plugins/claude-code/scripts/quality-gate.sh +0 -0
  87. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/plugins/claude-code/scripts/session-end.sh +0 -0
  88. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/plugins/claude-code/scripts/session-start.sh +0 -0
  89. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/plugins/claude-code/skills/gate/SKILL.md +0 -0
  90. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/plugins/claude-code/skills/status/SKILL.md +0 -0
  91. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/plugins/claude-code/skills/sync/SKILL.md +0 -0
  92. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/security/__init__.py +0 -0
  93. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif/security/mode.py +0 -0
  94. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif_cli.egg-info/dependency_links.txt +0 -0
  95. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif_cli.egg-info/entry_points.txt +0 -0
  96. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif_cli.egg-info/requires.txt +0 -0
  97. {seif_cli-0.5.0 → seif_cli-0.5.2}/src/seif_cli.egg-info/top_level.txt +0 -0
  98. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_advisor.py +0 -0
  99. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_canonical_inputs.py +0 -0
  100. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_code_compressor.py +0 -0
  101. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_collaborative_seif.py +0 -0
  102. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_context_qr.py +0 -0
  103. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_context_repo.py +0 -0
  104. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_git_context.py +0 -0
  105. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_git_hooks.py +0 -0
  106. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_init.py +0 -0
  107. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_quality_gate.py +0 -0
  108. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_ref.py +0 -0
  109. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_registry.py +0 -0
  110. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_resonance_gate.py +0 -0
  111. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_seif_io.py +0 -0
  112. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_transcompiler.py +0 -0
  113. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_transfer_function.py +0 -0
  114. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_triple_gate.py +0 -0
  115. {seif_cli-0.5.0 → seif_cli-0.5.2}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: seif-cli
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Measure AI output quality, protect sensitive data, watch your AI environment resonate. Quality Gate, Classification, Sentinel & Auto-Healing, SEIF OS.
5
5
  Author: André Cunha Antero de Carvalho
6
6
  License: CC-BY-NC-SA-4.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "seif-cli"
7
- version = "0.5.0"
7
+ version = "0.5.2"
8
8
  description = "Measure AI output quality, protect sensitive data, watch your AI environment resonate. Quality Gate, Classification, Sentinel & Auto-Healing, SEIF OS."
9
9
  readme = "README.md"
10
10
  license = {text = "CC-BY-NC-SA-4.0"}
@@ -55,8 +55,15 @@ class StanceAnalysis:
55
55
  return "\n".join(lines)
56
56
 
57
57
 
58
- # Patterns that indicate verifiable content
58
+ # Patterns that indicate verifiable content.
59
+ # Two groups: (1) original physics/EE/math patterns, (2) software-engineering
60
+ # verifiable artifacts added by the s14 calibration debate (semver, SHAs,
61
+ # file:line refs, test counts, ISO timestamps, PR refs, code identifiers, etc.).
62
+ # The split exists because session summaries are dense in (2) and were scoring
63
+ # F as "low data" before. Calibrated against 27 positive + 14 negative samples
64
+ # at 100% recall / 0% false positives.
59
65
  VERIFIABLE_PATTERNS = [
66
+ # --- Group 1: physics / EE / math ---
60
67
  r'\d+\.?\d*\s*(%|Hz|Ω|ohm|mH|μF|uF|nF|pF|dB|°|deg|rad|ms|kHz|MHz|V|A|W|bpm|rpm)',
61
68
  r'[=≈≠<>]\s*\d',
62
69
  r'[ζφωπ√]',
@@ -69,9 +76,41 @@ VERIFIABLE_PATTERNS = [
69
76
  r'\b(?:theorem|proof|exhaustive|brute.?force)\b',
70
77
  r'(?:ISE|IAE|ITAE|RLC|PCB|BOM|DRC)',
71
78
  r'formal.?symbolic',
72
- r'\b\w+[_]\w+\s*=', # variable_name = (e.g. f_peak = f_n/2)
73
- r'\b[A-Z]\([a-z]\)', # H(s), F(x), G(s)
74
- r'property|unique|halving', # mathematical property language
79
+ r'\b\w+[_]\w+\s*=', # variable_name = ...
80
+ r'\b[A-Z]\([a-z]\)', # H(s), F(x)
81
+ r'property|unique|halving', # math property language
82
+
83
+ # --- Group 2: software-engineering verifiable artifacts ---
84
+ r'\bv?\d+\.\d+\.\d+(?:[-+][\w.]+)?\b', # semver
85
+ r'\b(?:commit|tag|branch|sha|hash|merge|revert|cherry-?pick|HEAD)\s+[0-9a-f]{7,12}\b', # short SHA in context
86
+ r'\b[0-9a-f]{40}\b', # long SHA
87
+ r'\b(?:sha\d{2,3}|md5|blake[23]b?):[0-9a-f]{16,128}\b', # prefixed digest
88
+ r'\b[\w./-]+\.(?:py|ts|tsx|js|jsx|go|rs|java|cpp|c|h|hpp|md|json|yaml|yml|toml|sh|rb|php|cs|kt|swift|sql|html|css|scss):\d+(?:-\d+)?\b', # file:line
89
+ r'(?:#|\b)L\d+(?:-L?\d+)?\b', # GitHub line anchors
90
+ r'\b\d+\s+(?:tests?|specs?|suites?|cases?|assertions?|examples?)\b\s*(?:passing|failing|skipped|pending|run|across|in)?', # test count fwd
91
+ r'\b(?:tests?|specs?|suites?|cases?|assertions?|examples?)\s+(?:passing|failing|skipped|pending)?\s*(?:is|are|were|equals?|=)\s+\d+\b', # test count inv
92
+ r'\b(?:added|removed|deleted|modified|fixed|merged|reverted|created|closed|reopened|broke|patched|migrated)\s+\d+\b', # eng verb + count
93
+ r'\b\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?\b', # ISO 8601
94
+ r'(?<!\w)(?:#\d+|(?:PR|GH|MR|RFC|RFD|ISSUE|JIRA|[A-Z]{2,8})-\d+)\b', # PR / issue refs
95
+ r'\b(?:returns?|status|code|HTTP|got|expected|received|responded\s+with)\s+(?:1\d{2}|2\d{2}|3\d{2}|4\d{2}|5\d{2})\b', # HTTP verb-prefixed
96
+ r'\b(?:1\d{2}|2\d{2}|3\d{2}|4\d{2}|5\d{2})\s+(?:OK|Created|Accepted|No\s+Content|Moved|Found|Bad\s+Request|Unauthorized|Forbidden|Not\s+Found|Conflict|Gone|Too\s+Many|Internal\s+Server|Bad\s+Gateway|Service\s+Unavailable|Gateway\s+Timeout)\b', # HTTP code + reason
97
+ r'(?:^|[\s(\[])(?:localhost|127\.0\.0\.1|0\.0\.0\.0|::1)?:(?:[1-9]\d{0,4})\b', # port number
98
+ r'\b[\w.-]+@\^?\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?\b', # package@version
99
+ r'\b[a-z][a-z0-9_]+\([^)]{0,80}\)', # snake_case(...)
100
+ r'\b[A-Z][a-zA-Z0-9]+\([^)]{0,80}\)', # PascalCase(...)
101
+ r'\b\w+\.\w+\([^)]{0,80}\)', # foo.bar(...)
102
+ r'[+\-−]\d+\s*(?:[/,]\s*[+\-−]\d+|lines?|loc)?', # diff stats
103
+ r'\bexit(?:ed|s)?\s+(?:with\s+)?(?:code\s+)?-?\d+\b', # exit code
104
+ r'\breturn\s+code\s+-?\d+\b', # return code
105
+ r'\b\d+(?:\.\d+)?\s*(?:[KMGTP]i?B|bytes?|kbps|Mbps|Gbps)\b', # size with unit
106
+ r'\b\d+(?:\.\d+)?\s*(?:ns|us|μs|ms|min|sec|hr|hrs|hours?|minutes?|seconds?)\b', # duration
107
+ r'\b\d+m\s*\d+s\b', # 4m 32s
108
+ r'\b\d+h\s*\d+m\b', # 1h 18m
109
+ r'\b(?:feature|bugfix|hotfix|release|chore|refactor|fix|feat)/[\w./-]+\b', # branch convention
110
+ # File paths — require leading slash, ./, ../, OR dotted extension
111
+ # (rejects casual prose like "production/staging environments").
112
+ r'(?:^|(?<=[\s(\[]))(?:\.{1,2}/|/)[\w-]+(?:/[\w.-]+)+',
113
+ r'\b[\w-]+(?:/[\w.-]+)+\.[a-z]{1,6}\b',
75
114
  ]
76
115
 
77
116
  # Patterns that indicate metaphorical/interpretive drift
@@ -98,7 +137,14 @@ def analyze(text: str) -> StanceAnalysis:
98
137
  This does NOT judge the truth of claims. It measures the RATIO of
99
138
  verifiable to interpretive content, enabling informed reading.
100
139
  """
101
- sentences = [s.strip() for s in re.split(r'(?<![0-9])[.!?]|\n', text) if len(s.strip()) > 10]
140
+ # Sentence boundary rules:
141
+ # - Split on `.!?` only when NOT inside a decimal (no digit immediately
142
+ # after the punctuation), OR when followed by whitespace + a capital
143
+ # letter (the "0.612. This" case — punctuation preceded by a digit
144
+ # but clearly closing a sentence).
145
+ # - Newlines always split.
146
+ splitter = re.compile(r'[.!?](?!\d)(?=\s+[A-Z]|\s*$|\s+\Z)|(?<![0-9])[.!?]|\n')
147
+ sentences = [s.strip() for s in splitter.split(text) if len(s.strip()) > 10]
102
148
 
103
149
  if len(sentences) < 2:
104
150
  return StanceAnalysis(
@@ -3305,6 +3305,432 @@ def cmd_list():
3305
3305
  print(f" ... and {len(unregistered) - 3} more")
3306
3306
 
3307
3307
 
3308
+ def cmd_audit_host(fix: bool = False, dry_run: bool = False):
3309
+ """Audit ~/.seif/ host-level structure across the 5 architecture layers.
3310
+
3311
+ Layer A — Protocol (RESONANCE.json deployed)
3312
+ Layer B — Init-generated (config.json, registry.json with current workspace)
3313
+ Layer C — Derived (mapper.json, nucleus.seif, model_registry.json freshness)
3314
+ Layer D — Per-host config (machine_id present)
3315
+ Layer E — Heartbeat liveness
3316
+
3317
+ With --fix, calls the canonical regenerator for each missing/stale item.
3318
+ """
3319
+ import json as _json
3320
+ import time as _time
3321
+ from pathlib import Path as _P
3322
+ from datetime import datetime, timezone
3323
+
3324
+ try:
3325
+ from seif.data.paths import get_user_home, get_resonance_path
3326
+ from seif.context.registry import (
3327
+ load_registry, find_context_entry, rebuild_registry,
3328
+ )
3329
+ from seif.context.host_init import load_machine_id, ensure_machine_id
3330
+ from seif.context.model_probe import (
3331
+ probe_all, write_registry as write_model_registry,
3332
+ get_registry_path as get_model_registry_path,
3333
+ )
3334
+ except ImportError as e:
3335
+ print(f"Installation error: {e}")
3336
+ return
3337
+
3338
+ home = get_user_home()
3339
+ cwd = _P.cwd()
3340
+ findings = [] # (layer, status, message, fixer)
3341
+
3342
+ # ── Layer A: Protocol artefacts ─────────────────────────────────────────
3343
+ resonance_canonical = get_resonance_path()
3344
+ resonance_deployed = home / "RESONANCE.json"
3345
+ if not resonance_canonical.exists():
3346
+ findings.append(("A", "FAIL", "RESONANCE.json missing in seif package — reinstall seif-cli", None))
3347
+ elif not resonance_deployed.exists():
3348
+ def _fix_resonance():
3349
+ import shutil as _sh
3350
+ _sh.copy2(resonance_canonical, resonance_deployed)
3351
+ return f"deployed {resonance_canonical} → {resonance_deployed}"
3352
+ findings.append(("A", "MISSING", "~/.seif/RESONANCE.json not deployed", _fix_resonance))
3353
+ else:
3354
+ findings.append(("A", "OK", f"RESONANCE.json deployed ({resonance_deployed.stat().st_size}B)", None))
3355
+
3356
+ # ── Layer B: Init-generated ─────────────────────────────────────────────
3357
+ workspace_seif = cwd / ".seif"
3358
+ in_workspace = workspace_seif.is_dir()
3359
+
3360
+ config_path = home / "config.json"
3361
+ if not config_path.exists():
3362
+ findings.append(("B", "MISSING", "~/.seif/config.json absent — run seif --init in a workspace", None))
3363
+ else:
3364
+ findings.append(("B", "OK", f"config.json present ({config_path.stat().st_size}B)", None))
3365
+
3366
+ registry_path = home / "registry.json"
3367
+ if not registry_path.exists():
3368
+ findings.append(("B", "MISSING", "~/.seif/registry.json absent",
3369
+ lambda: rebuild_registry([str(cwd.parent)]) and "rebuilt"))
3370
+ else:
3371
+ registry = load_registry()
3372
+ if in_workspace:
3373
+ entry = find_context_entry(registry, str(workspace_seif))
3374
+ if not entry:
3375
+ def _fix_registry():
3376
+ rebuild_registry([str(cwd.parent), str(cwd)])
3377
+ return "rebuilt with current workspace"
3378
+ findings.append(("B", "STALE",
3379
+ f"current workspace {workspace_seif} not in registry "
3380
+ f"({len(registry.get('contexts', []))} entries)",
3381
+ _fix_registry))
3382
+ else:
3383
+ findings.append(("B", "OK",
3384
+ f"registry has {len(registry.get('contexts', []))} entries, current workspace registered",
3385
+ None))
3386
+ else:
3387
+ findings.append(("B", "OK",
3388
+ f"registry has {len(registry.get('contexts', []))} entries",
3389
+ None))
3390
+
3391
+ # ── Layer C: Derived (workspace-scoped) ─────────────────────────────────
3392
+ if in_workspace:
3393
+ for fname, label in [("mapper.json", "module index"),
3394
+ ("nucleus.seif", "workspace context")]:
3395
+ f = workspace_seif / fname
3396
+ if not f.exists():
3397
+ findings.append(("C", "MISSING",
3398
+ f".seif/{fname} absent — run seif --sync to regenerate",
3399
+ None))
3400
+ else:
3401
+ age_h = (_time.time() - f.stat().st_mtime) / 3600
3402
+ state = "OK" if age_h < 24 * 7 else "STALE"
3403
+ findings.append(("C", state,
3404
+ f".seif/{fname} ({label}, age {age_h:.1f}h)",
3405
+ None))
3406
+
3407
+ # model_registry.json freshness (host-level)
3408
+ mrp = get_model_registry_path()
3409
+ if not mrp.exists():
3410
+ def _fix_models():
3411
+ r = probe_all()
3412
+ write_model_registry(r)
3413
+ return f"probed {len(r.models)} models"
3414
+ findings.append(("C", "MISSING", "~/.seif/model_registry.json absent — run seif --probe-models", _fix_models))
3415
+ else:
3416
+ age_h = (_time.time() - mrp.stat().st_mtime) / 3600
3417
+ if age_h > 24:
3418
+ def _fix_models_stale():
3419
+ r = probe_all()
3420
+ write_model_registry(r)
3421
+ return f"reprobed {len(r.models)} models"
3422
+ findings.append(("C", "STALE",
3423
+ f"model_registry.json age {age_h:.1f}h > 24h",
3424
+ _fix_models_stale))
3425
+ else:
3426
+ findings.append(("C", "OK", f"model_registry.json fresh (age {age_h:.1f}h)", None))
3427
+
3428
+ # ── Layer D: Per-host ───────────────────────────────────────────────────
3429
+ mid = load_machine_id()
3430
+ if mid is None:
3431
+ def _fix_machine_id():
3432
+ payload, _ = ensure_machine_id()
3433
+ return f"created with label={payload['label']}"
3434
+ findings.append(("D", "MISSING", "~/.seif/machine_id absent — run seif --init-host", _fix_machine_id))
3435
+ else:
3436
+ findings.append(("D", "OK",
3437
+ f"machine_id {mid.get('label','?')}/{mid.get('role','?')} "
3438
+ f"fp={mid.get('fingerprint','?')[:8]}",
3439
+ None))
3440
+
3441
+ # ── Layer E: Heartbeat liveness ─────────────────────────────────────────
3442
+ hb_file = home / "heartbeat"
3443
+ if not hb_file.exists():
3444
+ findings.append(("E", "MISSING",
3445
+ "~/.seif/heartbeat not present — start heartbeatd "
3446
+ "(seif-resonance-bridge/seif-heartbeatd.py --daemon)",
3447
+ None))
3448
+ else:
3449
+ try:
3450
+ data = _json.loads(hb_file.read_text())
3451
+ ts = data.get("timestamp", "")
3452
+ from datetime import datetime as _dt
3453
+ t = _dt.fromisoformat(ts.replace("Z", "+00:00"))
3454
+ age_s = (_dt.now(timezone.utc) - t).total_seconds()
3455
+ if age_s > 300:
3456
+ findings.append(("E", "STALE",
3457
+ f"heartbeat {age_s:.0f}s old (>5min) — heartbeatd may be down",
3458
+ None))
3459
+ else:
3460
+ findings.append(("E", "OK",
3461
+ f"heartbeat {age_s:.0f}s old, machine={data.get('machine','?')}",
3462
+ None))
3463
+ except Exception as e:
3464
+ findings.append(("E", "WARN", f"heartbeat unparseable: {e}", None))
3465
+
3466
+ # ── Report ──────────────────────────────────────────────────────────────
3467
+ print(f"SEIF audit — host={home.parent.name}, cwd={cwd}")
3468
+ print()
3469
+ icons = {"OK": "✓", "MISSING": "✗", "STALE": "~", "FAIL": "!", "WARN": "?"}
3470
+ layer_labels = {
3471
+ "A": "Protocol ",
3472
+ "B": "Init-generated",
3473
+ "C": "Derived ",
3474
+ "D": "Per-host ",
3475
+ "E": "Heartbeat ",
3476
+ }
3477
+ counts = {"OK": 0, "MISSING": 0, "STALE": 0, "FAIL": 0, "WARN": 0}
3478
+ fixable = []
3479
+ for layer, status, msg, fixer in findings:
3480
+ icon = icons.get(status, "?")
3481
+ print(f" [{icon}] {layer_labels[layer]} [{status:<7}] {msg}")
3482
+ counts[status] += 1
3483
+ if fixer is not None:
3484
+ fixable.append((layer, status, msg, fixer))
3485
+
3486
+ print()
3487
+ print(f"Summary: {counts['OK']} OK, {counts['MISSING']} missing, "
3488
+ f"{counts['STALE']} stale, {counts['FAIL']} failed, {counts['WARN']} warned.")
3489
+
3490
+ if fixable and fix and not dry_run:
3491
+ print()
3492
+ print(f"Auto-fixing {len(fixable)} item(s):")
3493
+ for layer, status, msg, fixer in fixable:
3494
+ try:
3495
+ outcome = fixer()
3496
+ print(f" ✓ [{layer}] {outcome}")
3497
+ except Exception as e:
3498
+ print(f" ✗ [{layer}] fixer raised: {e}")
3499
+ elif fixable and not fix:
3500
+ print()
3501
+ print(f"{len(fixable)} item(s) can be auto-fixed. Re-run with --audit --fix to apply.")
3502
+ elif fixable and dry_run:
3503
+ print()
3504
+ print(f"(dry-run) {len(fixable)} item(s) would be fixed.")
3505
+
3506
+
3507
+ def _parse_hub_spec(spec: str):
3508
+ """Parse '--hub name=host[:port][,ssh=h][,key=p][,priority=1]' into HubSpec."""
3509
+ from seif.context.host_init import HubSpec
3510
+ if "=" not in spec:
3511
+ raise ValueError(f"hub spec missing 'name=': {spec!r}")
3512
+ name, _, rest = spec.partition("=")
3513
+ name = name.strip()
3514
+ if not name:
3515
+ raise ValueError(f"hub spec has empty name: {spec!r}")
3516
+ parts = [p.strip() for p in rest.split(",") if p.strip()]
3517
+ if not parts:
3518
+ raise ValueError(f"hub spec missing host: {spec!r}")
3519
+ host_part = parts[0]
3520
+ if ":" in host_part:
3521
+ host, _, port_s = host_part.partition(":")
3522
+ port = int(port_s)
3523
+ else:
3524
+ host, port = host_part, 7332
3525
+ extras = {}
3526
+ for p in parts[1:]:
3527
+ if "=" not in p:
3528
+ continue
3529
+ k, _, v = p.partition("=")
3530
+ extras[k.strip()] = v.strip()
3531
+ return HubSpec(
3532
+ name=name, host=host, port=port,
3533
+ health_port=int(extras.get("health_port", port + 2)),
3534
+ ssh_host=extras.get("ssh"),
3535
+ ssh_key=extras.get("key"),
3536
+ priority=int(extras.get("priority", 1)),
3537
+ transport=extras.get("transport", "direct"),
3538
+ )
3539
+
3540
+
3541
+ def cmd_init_host(label=None, role="dev", overwrite_machine_id=False,
3542
+ hub_specs=None, source_specs=None, dry_run=False):
3543
+ """Initialize host-specific files (~/.seif/machine_id + optional bridge/sources)."""
3544
+ try:
3545
+ from seif.context.host_init import (
3546
+ init_host, get_machine_id_path, get_bridge_config_path, get_sources_path,
3547
+ )
3548
+ except ImportError:
3549
+ print("Installation error: seif.context.host_init not found.")
3550
+ return
3551
+
3552
+ # Parse hub specs
3553
+ hubs = []
3554
+ for spec in (hub_specs or []):
3555
+ try:
3556
+ hubs.append(_parse_hub_spec(spec))
3557
+ except (ValueError, TypeError) as e:
3558
+ print(f"Invalid --hub spec {spec!r}: {e}")
3559
+ return
3560
+
3561
+ # Validate source specs early
3562
+ if source_specs:
3563
+ from seif.context.host_init import parse_source_spec
3564
+ for spec in source_specs:
3565
+ try:
3566
+ parse_source_spec(spec)
3567
+ except ValueError as e:
3568
+ print(f"Invalid --source spec {spec!r}: {e}")
3569
+ return
3570
+
3571
+ print(f"Host directory: {get_machine_id_path().parent}")
3572
+ if dry_run:
3573
+ print("(dry-run — no files will be written)")
3574
+ print()
3575
+
3576
+ result = init_host(
3577
+ label=label, role=role,
3578
+ overwrite_machine_id=overwrite_machine_id,
3579
+ hubs=hubs or None,
3580
+ source_specs=source_specs or None,
3581
+ dry_run=dry_run,
3582
+ )
3583
+
3584
+ mid = result.machine_id
3585
+ print("[machine_id]")
3586
+ print(f" Status: {'created' if result.machine_id_created else 'reused'}")
3587
+ print(f" Hostname: {mid['hostname']}")
3588
+ print(f" Label: {mid['label']}")
3589
+ print(f" Role: {mid['role']}")
3590
+ print(f" Fingerprint: {mid['fingerprint']}")
3591
+ if not dry_run:
3592
+ print(f" File: {get_machine_id_path()}")
3593
+ print()
3594
+
3595
+ if result.bridge_config is not None:
3596
+ print("[bridge-config.json]")
3597
+ for h in result.bridge_config["hubs"]:
3598
+ ssh = f" via ssh={h.get('ssh_host')}" if h.get("ssh_host") else ""
3599
+ print(f" - {h['name']}: {h['host']}:{h['port']}{ssh}")
3600
+ if not dry_run:
3601
+ print(f" File: {get_bridge_config_path()}")
3602
+ print()
3603
+
3604
+ if result.sources:
3605
+ print("[sources.json]")
3606
+ for s in result.sources:
3607
+ print(f" - {s['repo']:<45} {s['type']:<8} {s['classification']}")
3608
+ if result.sources_added:
3609
+ print(f" Added {result.sources_added} new source(s).")
3610
+ if not dry_run:
3611
+ print(f" File: {get_sources_path()}")
3612
+ print()
3613
+
3614
+ if dry_run:
3615
+ print("Re-run without --dry-run to persist.")
3616
+
3617
+
3618
+ def cmd_probe_models(machine_label: str = "local",
3619
+ output_format: str = "summary",
3620
+ dry_run: bool = False):
3621
+ """Probe model orchestra and regenerate ~/.seif/model_registry.json."""
3622
+ try:
3623
+ from seif.context.model_probe import (
3624
+ probe_all, merge_cached_remote, write_registry,
3625
+ format_orchestra, get_registry_path,
3626
+ )
3627
+ except ImportError:
3628
+ print("Installation error: seif.context.model_probe not found.")
3629
+ return
3630
+ import json as _json
3631
+
3632
+ result = probe_all(machine_label=machine_label)
3633
+ merge_cached_remote(result)
3634
+
3635
+ if output_format == "json":
3636
+ print(_json.dumps(result.to_registry(), indent=2, ensure_ascii=False))
3637
+ elif output_format == "orchestra":
3638
+ print(format_orchestra(result))
3639
+ else: # summary
3640
+ path = get_registry_path()
3641
+ print(f"Probed in {result.duration_ms} ms.")
3642
+ print(f"Registry: {path}{' (dry-run)' if dry_run else ''}")
3643
+ print(f" Models discovered: {len(result.models)}")
3644
+ cli_n = sum(1 for m in result.models if m['id'].startswith('cli/'))
3645
+ ide_n = sum(1 for m in result.models if m['id'].startswith('ide/'))
3646
+ ollama_n = sum(1 for m in result.models if m['id'].startswith('ollama/'))
3647
+ api_n = sum(1 for m in result.models
3648
+ if any(m['id'].startswith(p) for p in ('xai/', 'anthropic/', 'google/', 'openai/')))
3649
+ cached_n = sum(1 for m in result.models if m['status'] == 'cached')
3650
+ print(f" CLI tools: {cli_n}")
3651
+ print(f" IDE: {ide_n}")
3652
+ print(f" Ollama local: {ollama_n}")
3653
+ print(f" API keys: {api_n}")
3654
+ print(f" Cached remote:{cached_n}")
3655
+ c = result.circuit
3656
+ if c.get("connected"):
3657
+ print(f" Circuit: connected ({c.get('machine','?')}/{c.get('transport','?')})")
3658
+ else:
3659
+ print(f" Circuit: offline")
3660
+
3661
+ if not dry_run:
3662
+ write_registry(result)
3663
+
3664
+
3665
+ def cmd_rebuild_registry(scan_roots: list[str], dry_run: bool = False):
3666
+ """Rebuild ~/.seif/registry.json from filesystem scan.
3667
+
3668
+ Drops stale entries (missing paths, ephemeral tempdirs) and discovers any
3669
+ .seif/ workspaces under the given scan_roots that are not yet registered.
3670
+ """
3671
+ try:
3672
+ from seif.context.registry import (
3673
+ rebuild_registry,
3674
+ load_registry,
3675
+ get_registry_path,
3676
+ )
3677
+ except ImportError:
3678
+ print("Installation error: seif.context.registry not found.")
3679
+ return
3680
+
3681
+ # If no scan roots given, derive from existing registry: parents of known
3682
+ # contexts make natural search roots.
3683
+ if not scan_roots:
3684
+ existing = load_registry().get("contexts", [])
3685
+ derived = set()
3686
+ for ctx in existing:
3687
+ try:
3688
+ from pathlib import Path as _P
3689
+ p = _P(ctx["path"]).parent.parent
3690
+ if p.exists() and "/var/folders/" not in str(p):
3691
+ derived.add(str(p))
3692
+ except (KeyError, TypeError):
3693
+ continue
3694
+ if not derived:
3695
+ print("No --scan-root given and no usable parents in existing registry.")
3696
+ print("Provide one or more roots, e.g.:")
3697
+ print(" seif --rebuild-registry --scan-root /Volumes/DockerData")
3698
+ return
3699
+ scan_roots = sorted(derived)
3700
+
3701
+ print(f"Registry: {get_registry_path()}")
3702
+ print(f"Scan roots:")
3703
+ for r in scan_roots:
3704
+ print(f" - {r}")
3705
+ if dry_run:
3706
+ print("(dry-run — no changes will be written)")
3707
+ print()
3708
+
3709
+ result = rebuild_registry(scan_roots, dry_run=dry_run)
3710
+
3711
+ print(f"Kept : {result['kept']:>4} (existing entries with valid paths)")
3712
+ print(f"Dropped : {result['dropped']:>4} (stale or ephemeral)")
3713
+ print(f"Added : {result['added']:>4} (newly discovered)")
3714
+ print(f"Total : {result['total']:>4}")
3715
+
3716
+ if result["added_entries"]:
3717
+ print()
3718
+ print("Added contexts:")
3719
+ for e in result["added_entries"]:
3720
+ print(f" + {e['name']:<25} {e['path']}")
3721
+ if result["dropped_entries"]:
3722
+ print()
3723
+ print(f"Dropped {len(result['dropped_entries'])} entries (first 5 shown):")
3724
+ for e in result["dropped_entries"][:5]:
3725
+ name = e.get("name", "?")
3726
+ path = e.get("path", "?")
3727
+ print(f" - {name:<25} {path}")
3728
+
3729
+ if dry_run:
3730
+ print()
3731
+ print("Re-run without --dry-run to write changes.")
3732
+
3733
+
3308
3734
  def cmd_status(context_repo: str = None):
3309
3735
  """Show detailed status of the current .seif context."""
3310
3736
  from pathlib import Path
@@ -4114,6 +4540,58 @@ def main():
4114
4540
  parser.add_argument("--all", action="store_true", help="Pipeline completo (padrão)")
4115
4541
  parser.add_argument("--init", nargs="?", const=".", metavar="PATH",
4116
4542
  help="Initialize S.E.I.F.: scan, detect projects, extract git, generate .seif")
4543
+ parser.add_argument("--rebuild-registry", action="store_true",
4544
+ help="Rebuild ~/.seif/registry.json by scanning the filesystem. "
4545
+ "Drops stale entries (missing paths, tempdirs from old test runs). "
4546
+ "Use --scan-root to specify directories to scan; defaults to "
4547
+ "the parent of any context already in the registry.")
4548
+ parser.add_argument("--scan-root", action="append", default=[], metavar="PATH",
4549
+ help="Directory to scan for .seif/ workspaces (repeatable). "
4550
+ "Used with --rebuild-registry.")
4551
+ parser.add_argument("--dry-run", action="store_true",
4552
+ help="Preview the operation without writing changes "
4553
+ "(applies to --rebuild-registry, --probe-models).")
4554
+ parser.add_argument("--probe-models", action="store_true",
4555
+ help="Probe local Ollama, installed CLI tools, API keys, and circuitd; "
4556
+ "regenerate ~/.seif/model_registry.json for this host. "
4557
+ "Per-host file (each machine sees different reachable models).")
4558
+ parser.add_argument("--audit-host", action="store_true",
4559
+ help="Audit ~/.seif/ host-level structure across the 5 architecture "
4560
+ "layers (A: protocol, B: init-generated, C: derived, D: per-host, "
4561
+ "E: heartbeat). Complementary to --audit (which checks context "
4562
+ "modules). With --fix-host, regenerates missing/stale files.")
4563
+ parser.add_argument("--fix-host", action="store_true",
4564
+ help="With --audit-host, automatically regenerate missing or stale files.")
4565
+ parser.add_argument("--init-host", action="store_true",
4566
+ help="Initialize host-specific files (~/.seif/machine_id, "
4567
+ "optionally bridge-config.json and sources.json). Idempotent: "
4568
+ "only writes missing fields unless --overwrite-machine-id is set. "
4569
+ "These files are NEVER synced between machines.")
4570
+ parser.add_argument("--label", metavar="LABEL",
4571
+ help="Machine label (e.g. 'mini-m4'). Default: derived from hostname. "
4572
+ "Used with --init-host.")
4573
+ parser.add_argument("--host-role", default="dev", choices=("production", "dev", "edge", "ephemeral"),
4574
+ metavar="ROLE", dest="host_role",
4575
+ help="Host role within the SEIF circuit. Used with --init-host. "
4576
+ "(Note: --role is reserved for --quality-gate's text author role.)")
4577
+ parser.add_argument("--overwrite-machine-id", action="store_true",
4578
+ help="Rotate the machine_id fingerprint (otherwise re-running --init-host "
4579
+ "preserves the existing fingerprint).")
4580
+ parser.add_argument("--hub", action="append", default=[], metavar="SPEC",
4581
+ help="Bridge hub spec: 'name=host[:port][,ssh=ssh-host][,key=path]'. "
4582
+ "Repeatable. Triggers writing bridge-config.json. "
4583
+ "Example: --hub 'mini-primary=100.93.186.89:7332,ssh=mini'")
4584
+ parser.add_argument("--source", action="append", default=[], metavar="SPEC",
4585
+ help="Source repo spec: 'repo[:type[:classification]]'. Type is one of "
4586
+ "context|research|project (default: context). Classification "
4587
+ "defaults to PUBLIC/INTERNAL/CONFIDENTIAL based on type. Repeatable.")
4588
+ parser.add_argument("--machine-label", default="local", metavar="LABEL",
4589
+ help="Machine label for CLI-tool entries (e.g. 'air-m1', 'mini-m4'). "
4590
+ "Used with --probe-models.")
4591
+ parser.add_argument("--format", default="summary", choices=("summary", "orchestra", "json"),
4592
+ metavar="FMT",
4593
+ help="Output format for --probe-models: summary (counts), "
4594
+ "orchestra (session-start block), json (full registry).")
4117
4595
  parser.add_argument("-y", "--yes", action="store_true",
4118
4596
  help="Skip confirmation prompt (auto-approve)")
4119
4597
  parser.add_argument("--install-hooks", nargs="?", const=".", metavar="REPO",
@@ -4955,6 +5433,29 @@ def main():
4955
5433
  auto_yes=args.yes)
4956
5434
  return
4957
5435
 
5436
+ if getattr(args, "rebuild_registry", False):
5437
+ cmd_rebuild_registry(scan_roots=args.scan_root, dry_run=args.dry_run)
5438
+ return
5439
+
5440
+ if getattr(args, "probe_models", False):
5441
+ cmd_probe_models(machine_label=args.machine_label,
5442
+ output_format=args.format,
5443
+ dry_run=args.dry_run)
5444
+ return
5445
+
5446
+ if getattr(args, "init_host", False):
5447
+ cmd_init_host(label=args.label,
5448
+ role=args.host_role,
5449
+ overwrite_machine_id=args.overwrite_machine_id,
5450
+ hub_specs=args.hub,
5451
+ source_specs=args.source,
5452
+ dry_run=args.dry_run)
5453
+ return
5454
+
5455
+ if getattr(args, "audit_host", False):
5456
+ cmd_audit_host(fix=args.fix_host, dry_run=args.dry_run)
5457
+ return
5458
+
4958
5459
  if args.sync is not None:
4959
5460
  cmd_sync(args.sync, args.author, args.via, context_repo=args.context_repo)
4960
5461
  return