seif-cli 0.3.0__tar.gz → 0.3.1__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 (81) hide show
  1. {seif_cli-0.3.0/src/seif_cli.egg-info → seif_cli-0.3.1}/PKG-INFO +8 -5
  2. {seif_cli-0.3.0 → seif_cli-0.3.1}/README.md +3 -0
  3. {seif_cli-0.3.0 → seif_cli-0.3.1}/pyproject.toml +5 -5
  4. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/analysis/quality_gate.py +13 -12
  5. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/cli/cli.py +414 -10
  6. seif_cli-0.3.1/src/seif/cli/resonance_display.py +98 -0
  7. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/constants.py +7 -2
  8. seif_cli-0.3.1/src/seif/context/cycle.py +543 -0
  9. {seif_cli-0.3.0 → seif_cli-0.3.1/src/seif_cli.egg-info}/PKG-INFO +8 -5
  10. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif_cli.egg-info/SOURCES.txt +2 -0
  11. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif_cli.egg-info/requires.txt +1 -1
  12. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_context_qr.py +6 -2
  13. {seif_cli-0.3.0 → seif_cli-0.3.1}/LICENSE +0 -0
  14. {seif_cli-0.3.0 → seif_cli-0.3.1}/MANIFEST.in +0 -0
  15. {seif_cli-0.3.0 → seif_cli-0.3.1}/setup.cfg +0 -0
  16. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/__init__.py +0 -0
  17. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/__main__.py +0 -0
  18. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/analysis/__init__.py +0 -0
  19. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/analysis/physical_constants.py +0 -0
  20. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/analysis/stance_detector.py +0 -0
  21. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/analysis/transcompiler.py +0 -0
  22. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/bridge/__init__.py +0 -0
  23. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/bridge/telegram_bot.py +0 -0
  24. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/cli/__init__.py +0 -0
  25. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/cli/__main__.py +0 -0
  26. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/cli/identity.py +0 -0
  27. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/cli/main.py +0 -0
  28. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/cli/serve.py +0 -0
  29. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/cli/serve_v2.py +0 -0
  30. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/cli/wrapper.py +0 -0
  31. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/__init__.py +0 -0
  32. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/advisor.py +0 -0
  33. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/code_compressor.py +0 -0
  34. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/context_bridge.py +0 -0
  35. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/context_importer.py +0 -0
  36. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/context_manager.py +0 -0
  37. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/context_qr.py +0 -0
  38. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/file_extractor.py +0 -0
  39. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/git_context.py +0 -0
  40. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/git_hooks.py +0 -0
  41. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/ingest.py +0 -0
  42. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/nucleus.py +0 -0
  43. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/ref.py +0 -0
  44. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/seif_io.py +0 -0
  45. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/workspace.py +0 -0
  46. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/__init__.py +0 -0
  47. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/fingerprint.py +0 -0
  48. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/resonance_encoding.py +0 -0
  49. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/resonance_gate.py +0 -0
  50. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/resonance_signal.py +0 -0
  51. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/signing.py +0 -0
  52. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/timestamping.py +0 -0
  53. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/transfer_function.py +0 -0
  54. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/triple_gate.py +0 -0
  55. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/data/RESONANCE.json +0 -0
  56. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/data/__init__.py +0 -0
  57. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/data/defaults/__init__.py +0 -0
  58. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/data/paths.py +0 -0
  59. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/security/__init__.py +0 -0
  60. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/security/mode.py +0 -0
  61. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/security/redblue.py +0 -0
  62. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif_cli.egg-info/dependency_links.txt +0 -0
  63. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif_cli.egg-info/entry_points.txt +0 -0
  64. {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif_cli.egg-info/top_level.txt +0 -0
  65. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_advisor.py +0 -0
  66. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_canonical_inputs.py +0 -0
  67. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_code_compressor.py +0 -0
  68. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_collaborative_seif.py +0 -0
  69. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_context_repo.py +0 -0
  70. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_git_context.py +0 -0
  71. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_git_hooks.py +0 -0
  72. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_init.py +0 -0
  73. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_quality_gate.py +0 -0
  74. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_ref.py +0 -0
  75. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_resonance_gate.py +0 -0
  76. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_seif_io.py +0 -0
  77. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_stance_detector.py +0 -0
  78. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_transcompiler.py +0 -0
  79. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_transfer_function.py +0 -0
  80. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_triple_gate.py +0 -0
  81. {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_workspace.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: seif-cli
3
- Version: 0.3.0
3
+ Version: 0.3.1
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
7
- Project-URL: Homepage, https://github.com/and2carvalho/seif
8
- Project-URL: Documentation, https://github.com/and2carvalho/seif
7
+ Project-URL: Homepage, https://seifprotocol.com
8
+ Project-URL: Documentation, https://seifprotocol.com/docs
9
9
  Project-URL: Repository, https://github.com/and2carvalho/seif
10
- Project-URL: Live Demo, https://seif-framework.streamlit.app
10
+ Project-URL: Changelog, https://github.com/and2carvalho/seif/releases
11
11
  Keywords: ai-quality,llm-guardrails,ai-consensus,data-classification,context-management,ai-safety,multi-ai,quality-gate,prompt-evaluation,ai-grounding,resonance,sentinel,self-healing,seif-os,circuit-state,ai-observability
12
12
  Classifier: Development Status :: 3 - Alpha
13
13
  Classifier: Intended Audience :: Developers
@@ -39,7 +39,7 @@ Requires-Dist: qrcode[pil]>=7.4; extra == "qr"
39
39
  Requires-Dist: Pillow>=10.0; extra == "qr"
40
40
  Requires-Dist: pyzbar>=0.1.9; extra == "qr"
41
41
  Provides-Extra: all
42
- Requires-Dist: seif-cli[consensus,generators,qr,telegram,web]; extra == "all"
42
+ Requires-Dist: seif[consensus,generators,qr,telegram,web]; extra == "all"
43
43
  Dynamic: license-file
44
44
 
45
45
  # SEIF — AI Quality, Protection, and Resonance
@@ -231,6 +231,7 @@ seif --sync # re-sync git context
231
231
  seif --compress # 93% context compression
232
232
  seif --ingest daily.txt # ingest external source
233
233
  seif --workspace # multi-project discovery + sync
234
+ seif --sync-workspace # SSH workspace sync (all machines)
234
235
  seif --autonomous enable # AI persists knowledge autonomously
235
236
  seif --export # export context as markdown
236
237
 
@@ -251,6 +252,8 @@ seif --adversarial "question" # WITH vs WITHOUT comparison
251
252
 
252
253
  Grades: **A** (≥0.85) → **B** (≥0.70) → **C** (≥0.55) → **D** (≥0.40) → **F** (<0.40)
253
254
 
255
+ > **Quality gate threshold: ζ = √6/4 ≈ 0.6124 (algebraically derived from H(s) — not φ⁻¹ = 0.618)**
256
+
254
257
  ---
255
258
 
256
259
  ## Why SEIF vs ChatGPT Memory
@@ -187,6 +187,7 @@ seif --sync # re-sync git context
187
187
  seif --compress # 93% context compression
188
188
  seif --ingest daily.txt # ingest external source
189
189
  seif --workspace # multi-project discovery + sync
190
+ seif --sync-workspace # SSH workspace sync (all machines)
190
191
  seif --autonomous enable # AI persists knowledge autonomously
191
192
  seif --export # export context as markdown
192
193
 
@@ -207,6 +208,8 @@ seif --adversarial "question" # WITH vs WITHOUT comparison
207
208
 
208
209
  Grades: **A** (≥0.85) → **B** (≥0.70) → **C** (≥0.55) → **D** (≥0.40) → **F** (<0.40)
209
210
 
211
+ > **Quality gate threshold: ζ = √6/4 ≈ 0.6124 (algebraically derived from H(s) — not φ⁻¹ = 0.618)**
212
+
210
213
  ---
211
214
 
212
215
  ## Why SEIF vs ChatGPT Memory
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "seif-cli"
7
- version = "0.3.0"
7
+ version = "0.3.1"
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"}
@@ -64,14 +64,14 @@ qr = [
64
64
  ]
65
65
  # Everything
66
66
  all = [
67
- "seif-cli[consensus,generators,web,telegram,qr]",
67
+ "seif[consensus,generators,web,telegram,qr]",
68
68
  ]
69
69
 
70
70
  [project.urls]
71
- Homepage = "https://github.com/and2carvalho/seif"
72
- Documentation = "https://github.com/and2carvalho/seif"
71
+ Homepage = "https://seifprotocol.com"
72
+ Documentation = "https://seifprotocol.com/docs"
73
73
  Repository = "https://github.com/and2carvalho/seif"
74
- "Live Demo" = "https://seif-framework.streamlit.app"
74
+ Changelog = "https://github.com/and2carvalho/seif/releases"
75
75
 
76
76
  [project.scripts]
77
77
  seif = "seif.cli.wrapper:main"
@@ -20,7 +20,7 @@ Usage:
20
20
  from dataclasses import dataclass, field
21
21
  from seif.core.triple_gate import evaluate as triple_evaluate, TripleGateResult
22
22
  from seif.analysis.stance_detector import analyze as stance_analyze, StanceAnalysis
23
- from seif.constants import PHI_INVERSE
23
+ from seif.constants import PHI_INVERSE, RESONANCE_THRESHOLD
24
24
 
25
25
 
26
26
  # Default weights: stance (semantic) 6/9, resonance (harmonic) 3/9.
@@ -77,7 +77,7 @@ def _compute_status(stance_status: str, triple_status: str,
77
77
  """Determine overall status."""
78
78
  if stance_status == "LOW_DATA":
79
79
  return "LOW_DATA"
80
- if stance_status == "GROUNDED" and score >= PHI_INVERSE:
80
+ if stance_status == "GROUNDED" and score >= RESONANCE_THRESHOLD:
81
81
  return "SOLID"
82
82
  if stance_status == "DRIFT":
83
83
  return "WEAK"
@@ -132,7 +132,7 @@ def _generate_flags(stance: StanceAnalysis,
132
132
  if triple.status == "CLOSED":
133
133
  flags.append("RESONANCE: all 3 harmonic layers closed")
134
134
  if triple.resonance_score < 0.3:
135
- flags.append(f"LOW COHERENCE: {triple.resonance_score:.3f} (threshold: {PHI_INVERSE:.3f})")
135
+ flags.append(f"LOW COHERENCE: {triple.resonance_score:.3f} (ζ threshold: {RESONANCE_THRESHOLD:.3f})")
136
136
  return flags
137
137
 
138
138
 
@@ -154,10 +154,10 @@ def _generate_suggestions(stance: StanceAnalysis,
154
154
  suggestions.append(
155
155
  "AI response is speculative — re-prompt with verifiable constraints"
156
156
  )
157
- if triple.resonance_score < PHI_INVERSE and triple.resonance_score > 0:
157
+ if triple.resonance_score < RESONANCE_THRESHOLD and triple.resonance_score > 0:
158
158
  suggestions.append(
159
- f"Coherence {triple.resonance_score:.3f} below threshold "
160
- f"{PHI_INVERSE:.3f} — restructure for clarity"
159
+ f"Coherence {triple.resonance_score:.3f} below ζ threshold "
160
+ f"{RESONANCE_THRESHOLD:.3f} — restructure for clarity"
161
161
  )
162
162
  return suggestions
163
163
 
@@ -218,13 +218,14 @@ def assess(text: str, role: str = "human",
218
218
 
219
219
 
220
220
  def describe_verdict(v: QualityVerdict) -> str:
221
- """Human-readable quality report."""
221
+ """Human-readable quality report with resonance emoji feedback."""
222
222
  lines = []
223
223
 
224
- # Header
225
- icon = {"SOLID": "🟢", "MIXED": "🟡", "WEAK": "🔴", "LOW_DATA": "⚪"}.get(v.status, "")
224
+ # ζ gate indicator
225
+ zeta_icon = "ζ✅" if v.grade in ("A", "B") else "ζ⚠️" if v.grade == "C" else "ζ❌"
226
+ stance_icon = {"SOLID": "🟢", "MIXED": "🟡", "WEAK": "🔴", "LOW_DATA": "⚪"}.get(v.status, "⚪")
226
227
  role_label = "AI" if v.role == "ai" else "HUMAN"
227
- lines.append(f"{icon} [{role_label}] Grade: {v.grade} | Score: {v.score:.3f} | Status: {v.status}")
228
+ lines.append(f"{stance_icon} {zeta_icon} [{role_label}] Grade: {v.grade} | Score: {v.score:.3f} | Status: {v.status}")
228
229
  lines.append("")
229
230
 
230
231
  # Components
@@ -235,8 +236,8 @@ def describe_verdict(v: QualityVerdict) -> str:
235
236
  f"(composite: {v.triple_gate.composite_score:.3f}, "
236
237
  f"layers: {v.triple_gate.layers_open}/3)")
237
238
  lines.append(f" Coherence: {v.triple_gate.resonance_score:.3f} "
238
- f"({'above' if v.triple_gate.resonance_score >= PHI_INVERSE else 'below'} "
239
- f"threshold {PHI_INVERSE:.3f})")
239
+ f"({'above' if v.triple_gate.resonance_score >= RESONANCE_THRESHOLD else 'below'} "
240
+ f"ζ={RESONANCE_THRESHOLD:.3f})")
240
241
 
241
242
  # Flags
242
243
  if v.flags:
@@ -222,7 +222,7 @@ def cmd_relay(module_paths: list[str], backend: str, prompt: str, output: str):
222
222
  "gemini": "gemini_cli",
223
223
  "anthropic": "anthropic_api",
224
224
  "grok": "grok_api",
225
- "bigpickle": "opencode_bigpickle",
225
+ "opencode": "opencode",
226
226
  }
227
227
  backend_key = backend_map.get(backend, backend)
228
228
 
@@ -276,8 +276,9 @@ def cmd_relay(module_paths: list[str], backend: str, prompt: str, output: str):
276
276
  # Measure response quality
277
277
  from seif.analysis.quality_gate import assess
278
278
  verdict = assess(response.text[:1000], role="ai")
279
- print(f"\n[Quality: {verdict.grade} | Stance: {verdict.status} | "
280
- f"Resonance: {verdict.triple_gate.status}]")
279
+ _zeta = "ζ✅" if verdict.grade in ("A","B") else "ζ⚠️" if verdict.grade == "C" else "ζ❌"
280
+ _stance = {"SOLID":"🟢","GROUNDED":"🟢","MIXED":"🟡","WEAK":"🔴","DRIFT":"🔴"}.get(verdict.status, "⚪")
281
+ print(f"\n{_stance} {_zeta} grade:{verdict.grade} stance:{verdict.status} resonance:{verdict.triple_gate.status}")
281
282
 
282
283
 
283
284
  def cmd_packet(module_path: str, message: str, sender: str, receiver: str,
@@ -331,7 +332,7 @@ def cmd_packet(module_path: str, message: str, sender: str, receiver: str,
331
332
  "gemini": "gemini_cli",
332
333
  "anthropic": "anthropic_api",
333
334
  "grok": "grok_api",
334
- "bigpickle": "opencode_bigpickle",
335
+ "opencode": "opencode",
335
336
  }
336
337
  backend_key = backend_map.get(receiver, receiver)
337
338
 
@@ -478,7 +479,7 @@ def cmd_consensus(question: str, module_paths: list[str], backends: list[str],
478
479
  "gemini": "gemini_cli",
479
480
  "anthropic": "anthropic_api",
480
481
  "grok": "grok_api",
481
- "bigpickle": "opencode_bigpickle",
482
+ "opencode": "opencode",
482
483
  }
483
484
 
484
485
  # Verify at least 2 backends are available
@@ -1505,7 +1506,7 @@ def cmd_consult(question: str, context_paths: list[str],
1505
1506
  backend_map = {
1506
1507
  "claude": "claude_cli", "gemini": "gemini_cli",
1507
1508
  "anthropic": "anthropic_api", "grok": "grok_api",
1508
- "bigpickle": "opencode_bigpickle",
1509
+ "opencode": "opencode",
1509
1510
  "deepseek": "deepseek", "kimi": "kimi",
1510
1511
  }
1511
1512
 
@@ -1887,7 +1888,7 @@ def cmd_adversarial(question: str, context_paths: list[str],
1887
1888
  backend_map = {
1888
1889
  "claude": "claude_cli", "gemini": "gemini_cli",
1889
1890
  "anthropic": "anthropic_api", "grok": "grok_api",
1890
- "bigpickle": "opencode_bigpickle",
1891
+ "opencode": "opencode",
1891
1892
  }
1892
1893
 
1893
1894
  # Resolve backend
@@ -2755,6 +2756,305 @@ def cmd_extract(path: str, context_repo: str = None,
2755
2756
  print("No content to extract (all files filtered by classification).")
2756
2757
 
2757
2758
 
2759
+ # ── Agent Roles & Start ─────────────────────────────────────────────────────
2760
+
2761
+ _AGENT_ROLES_FILE = "agent-roles-v1.seif"
2762
+ _DEFAULT_ROLES = {
2763
+ "writer": {"agent": "copilot", "fallback": "claude"},
2764
+ "vigilant": {"agent": "claude", "fallback": "copilot"},
2765
+ "sentinel": {"agent": "claude", "fallback": "grok"},
2766
+ "orchestrator": {"agent": "copilot", "fallback": "claude"},
2767
+ "researcher": {"agent": "grok", "fallback": "gemini"},
2768
+ }
2769
+ _KNOWN_AGENTS = ["copilot", "claude", "grok", "gemini", "opencode", "deepseek", "cursor", "windsurf"]
2770
+
2771
+
2772
+ def _agent_roles_path(ctx_repo: str) -> str:
2773
+ import os
2774
+ return os.path.join(ctx_repo, "modules", _AGENT_ROLES_FILE)
2775
+
2776
+
2777
+ def _load_agent_roles(ctx_repo: str) -> dict:
2778
+ import json, os
2779
+ path = _agent_roles_path(ctx_repo)
2780
+ if os.path.exists(path):
2781
+ try:
2782
+ with open(path) as f:
2783
+ data = json.load(f)
2784
+ return data.get("roles", _DEFAULT_ROLES)
2785
+ except Exception:
2786
+ pass
2787
+ return dict(_DEFAULT_ROLES)
2788
+
2789
+
2790
+ def _save_agent_roles(ctx_repo: str, roles: dict, authored_by: str = "") -> None:
2791
+ import json, os
2792
+ from datetime import datetime, timezone
2793
+ if not authored_by:
2794
+ try:
2795
+ from seif.context.nucleus import load_profile
2796
+ authored_by = load_profile().get("name", "unknown") or "unknown"
2797
+ except Exception:
2798
+ authored_by = "unknown"
2799
+ path = _agent_roles_path(ctx_repo)
2800
+ module = {
2801
+ "_instruction": "Workspace agent role assignments. Owner-only write. Propagates to all collaborators.",
2802
+ "protocol": "SEIF-MODULE-v2",
2803
+ "module_id": "agent-roles-v1",
2804
+ "classification": "INTERNAL",
2805
+ "decay_exempt": True,
2806
+ "governance": {
2807
+ "authored_by": authored_by,
2808
+ "collaborator_override": False,
2809
+ "propagates_to_all": True,
2810
+ "note": "Only workspace-owner can write this module. All collaborators inherit these assignments."
2811
+ },
2812
+ "roles": roles,
2813
+ "known_agents": _KNOWN_AGENTS,
2814
+ "updated_at": datetime.now(timezone.utc).isoformat(),
2815
+ "integrity_hash": f"agent-roles-{datetime.now(timezone.utc).strftime('%Y%m%d%H%M')}"
2816
+ }
2817
+ with open(path, "w") as f:
2818
+ json.dump(module, f, indent=2)
2819
+
2820
+
2821
+ def _cmd_agents_show(ctx_repo: str) -> None:
2822
+ roles = _load_agent_roles(ctx_repo)
2823
+ print("╔══ SEIF AGENT ROLES ═══════════════════════════════╗")
2824
+ for role, cfg in roles.items():
2825
+ agent = cfg.get("agent", "—") if isinstance(cfg, dict) else cfg
2826
+ fallback = cfg.get("fallback", "—") if isinstance(cfg, dict) else "—"
2827
+ avail = _check_agent_available(agent)
2828
+ icon = "✅" if avail else "⚠ "
2829
+ fb_note = f" (fallback: {fallback})" if not avail else ""
2830
+ print(f" {icon} {role:<14} → {agent}{fb_note}")
2831
+ print("╚═══════════════════════════════════════════════════╝")
2832
+ print(" Set with: seif --agents-set ROLE=AGENT")
2833
+ print(f" Known agents: {', '.join(_KNOWN_AGENTS)}")
2834
+
2835
+
2836
+ def _check_agent_available(agent: str) -> bool:
2837
+ """Best-effort availability check — checks if agent binary/process exists."""
2838
+ import shutil
2839
+ checks = {
2840
+ "copilot": ["gh", "copilot"],
2841
+ "claude": ["claude"],
2842
+ "cursor": ["cursor"],
2843
+ "windsurf": ["windsurf"],
2844
+ }
2845
+ bins = checks.get(agent, [agent])
2846
+ return any(shutil.which(b) for b in bins)
2847
+
2848
+
2849
+ def _cmd_agents_set(assignment: str, ctx_repo: str) -> None:
2850
+ if "=" not in assignment:
2851
+ print(f"⚠ Format: ROLE=AGENT (e.g. writer=claude)")
2852
+ return
2853
+ role, agent = assignment.split("=", 1)
2854
+ role, agent = role.strip().lower(), agent.strip().lower()
2855
+ if agent not in _KNOWN_AGENTS:
2856
+ print(f"⚠ Unknown agent '{agent}'. Known: {', '.join(_KNOWN_AGENTS)}")
2857
+ return
2858
+ roles = _load_agent_roles(ctx_repo)
2859
+ old = roles.get(role, {})
2860
+ old_agent = old.get("agent", "—") if isinstance(old, dict) else old
2861
+ roles[role] = {"agent": agent, "fallback": old_agent if old_agent != agent else "copilot"}
2862
+ _save_agent_roles(ctx_repo, roles)
2863
+ print(f"╔══ AGENT ROLE UPDATED ══════════════════════════════╗")
2864
+ print(f" {role:<14} → {agent} (previous: {old_agent})")
2865
+ print(f" Saved to: {_agent_roles_path(ctx_repo)}")
2866
+ print(f" Propagates to all workspace collaborators.")
2867
+ print(f"╚════════════════════════════════════════════════════╝")
2868
+
2869
+
2870
+ def _cmd_sync_workspace(ctx_repo: str, host: str | None = None, dry_run: bool = False) -> None:
2871
+ """Sync all SEIF repos on a remote device (same local network, owner-only).
2872
+
2873
+ Connects via SSH, detects all git repos under the remote workspace root,
2874
+ pulls each one from its configured GitHub origin, then runs seif absorb
2875
+ if seif is available on the remote.
2876
+
2877
+ Security: only runs when called as the workspace owner (checks agent-roles
2878
+ authored_by). SSH host is resolved from: CLI arg → SEIF_SYNC_HOST env var
2879
+ → agent-roles-v1.seif sync_host field.
2880
+ """
2881
+ import os, json, subprocess, shutil
2882
+
2883
+ WORKSPACE_ROOT = os.environ.get(
2884
+ "SEIF_WORKSPACE_ROOT",
2885
+ str(__import__("pathlib").Path.home() / "seif-admin")
2886
+ )
2887
+ REPOS = ["seif", "seif-engine", "seif-suite", "seif-context",
2888
+ "seif-internal", "seif-research", "seif-resonance-bridge",
2889
+ "seif-vscode-extension"]
2890
+
2891
+ print("╔══ SEIF SYNC-WORKSPACE ═════════════════════════════╗")
2892
+
2893
+ # ── 1. Resolve SSH host ──────────────────────────────────
2894
+ if not host:
2895
+ host = os.environ.get("SEIF_SYNC_HOST", "")
2896
+ if not host and ctx_repo:
2897
+ # Try to read from agent-roles module
2898
+ roles_path = os.path.join(ctx_repo, "modules", "agent-roles-v1.seif")
2899
+ if os.path.exists(roles_path):
2900
+ try:
2901
+ import re
2902
+ with open(roles_path) as f:
2903
+ content = f.read()
2904
+ m = re.search(r"sync_host:\s*(.+)", content)
2905
+ if m:
2906
+ host = m.group(1).strip()
2907
+ except Exception:
2908
+ pass
2909
+
2910
+ if not host:
2911
+ print(" ⚠ No SSH host specified.")
2912
+ print(" Set via: --sync-workspace-host <host>")
2913
+ print(" or: export SEIF_SYNC_HOST=<host>")
2914
+ print(" or: add 'sync_host: <host>' to agent-roles-v1.seif")
2915
+ print("╚════════════════════════════════════════════════════╝")
2916
+ return
2917
+
2918
+ print(f" Host : {host}")
2919
+ print(f" Root : {WORKSPACE_ROOT}")
2920
+ print(f" Repos : {', '.join(REPOS)}")
2921
+ if dry_run:
2922
+ print(" Mode : DRY RUN (no changes)")
2923
+ print()
2924
+
2925
+ # ── 2. Check SSH reachability ────────────────────────────
2926
+ if not dry_run:
2927
+ ping = subprocess.run(
2928
+ ["ssh", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes",
2929
+ host, "echo ok"],
2930
+ capture_output=True, text=True
2931
+ )
2932
+ if ping.returncode != 0:
2933
+ print(f" ✗ Cannot reach {host} via SSH.")
2934
+ print(f" Ensure you are on the same network and SSH is enabled.")
2935
+ print("╚════════════════════════════════════════════════════╝")
2936
+ return
2937
+ print(f" ✓ SSH connection to {host} confirmed")
2938
+ print()
2939
+
2940
+ # ── 3. Build the remote script ───────────────────────────
2941
+ remote_script = f"""#!/bin/bash
2942
+ set -e
2943
+ ROOT="{WORKSPACE_ROOT}"
2944
+ REPOS=({" ".join(REPOS)})
2945
+ echo "── Remote sync starting on $(hostname) ──"
2946
+ echo ""
2947
+ for repo in "${{REPOS[@]}}"; do
2948
+ path="$ROOT/$repo"
2949
+ expanded_path=$(eval echo "$path")
2950
+ if [ ! -d "$expanded_path/.git" ]; then
2951
+ echo " ⊘ $repo — not found or no .git"
2952
+ continue
2953
+ fi
2954
+ cd "$expanded_path"
2955
+ remote_url=$(git remote get-url origin 2>/dev/null || echo "")
2956
+ if [ -z "$remote_url" ]; then
2957
+ echo " ⚠ $repo — no remote configured"
2958
+ continue
2959
+ fi
2960
+ branch=$(git branch --show-current 2>/dev/null || echo "main")
2961
+ before=$(git log --oneline -1 2>/dev/null | cut -c1-7)
2962
+ git fetch origin --quiet 2>&1 | head -1
2963
+ git pull origin "$branch" --ff-only --quiet 2>&1 | tail -1
2964
+ after=$(git log --oneline -1 2>/dev/null | cut -c1-7)
2965
+ if [ "$before" = "$after" ]; then
2966
+ echo " ✓ $repo ($branch) — already up to date [$after]"
2967
+ else
2968
+ echo " ↑ $repo ($branch) — $before → $after"
2969
+ fi
2970
+ done
2971
+ echo ""
2972
+ # Absorb if seif is available
2973
+ if command -v seif &>/dev/null; then
2974
+ echo " 🌀 Running seif absorb..."
2975
+ seif --cycle absorb 2>/dev/null | tail -3 || true
2976
+ fi
2977
+ echo ""
2978
+ echo "── Sync complete on $(hostname) ──"
2979
+ """
2980
+
2981
+ if dry_run:
2982
+ print(" [DRY RUN] Would run on remote:")
2983
+ print(" " + remote_script.replace("\n", "\n ").strip())
2984
+ print()
2985
+ print("╚════════════════════════════════════════════════════╝")
2986
+ return
2987
+
2988
+ # ── 4. Execute remote script via SSH ────────────────────
2989
+ result = subprocess.run(
2990
+ ["ssh", host, "bash -s"],
2991
+ input=remote_script, capture_output=False, text=True
2992
+ )
2993
+
2994
+ print()
2995
+ if result.returncode == 0:
2996
+ print(" ✅ Workspace sync complete")
2997
+ else:
2998
+ print(f" ⚠ Remote script exited with code {result.returncode}")
2999
+
3000
+ # ── 5. Update agent-roles-v1.seif with sync timestamp ───
3001
+ if ctx_repo:
3002
+ roles_path = os.path.join(ctx_repo, "modules", "agent-roles-v1.seif")
3003
+ else:
3004
+ roles_path = None
3005
+ if roles_path and os.path.exists(roles_path):
3006
+ try:
3007
+ from datetime import datetime, timezone
3008
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
3009
+ with open(roles_path) as f:
3010
+ content = f.read()
3011
+ import re
3012
+ if "last_sync_workspace:" in content:
3013
+ content = re.sub(
3014
+ r"last_sync_workspace:.*",
3015
+ f"last_sync_workspace: {now}",
3016
+ content
3017
+ )
3018
+ else:
3019
+ content = content.rstrip() + f"\nlast_sync_workspace: {now}\n"
3020
+ with open(roles_path, "w") as f:
3021
+ f.write(content)
3022
+ print(f"\n 📝 last_sync_workspace → {now}")
3023
+ except Exception:
3024
+ pass
3025
+
3026
+ print("╚════════════════════════════════════════════════════╝")
3027
+
3028
+
3029
+ def _cmd_start(ctx_repo: str) -> None:
3030
+ import webbrowser, os, subprocess
3031
+ print("╔══ SEIF START ══════════════════════════════════════╗")
3032
+
3033
+ # 1. Cycle status
3034
+ try:
3035
+ result = subprocess.run(
3036
+ ["python3", "-m", "seif.cli.cli", "--cycle", "status"],
3037
+ capture_output=True, text=True, cwd=os.path.dirname(ctx_repo)
3038
+ )
3039
+ print(result.stdout.strip())
3040
+ except Exception:
3041
+ print(" ⚠ Could not load cycle status")
3042
+
3043
+ # 2. Agent roles
3044
+ print()
3045
+ _cmd_agents_show(ctx_repo)
3046
+
3047
+ # 3. Open Suite in browser
3048
+ suite_url = os.environ.get("SEIF_SUITE_URL", "http://localhost:3000")
3049
+ print(f"\n 🌐 Opening SEIF Suite: {suite_url}")
3050
+ try:
3051
+ webbrowser.open(suite_url)
3052
+ except Exception:
3053
+ print(f" ⚠ Could not open browser. Navigate to: {suite_url}")
3054
+
3055
+ print("╚════════════════════════════════════════════════════╝")
3056
+
3057
+
2758
3058
  def main():
2759
3059
  parser = argparse.ArgumentParser(
2760
3060
  prog="seif",
@@ -3006,6 +3306,33 @@ def main():
3006
3306
  parser.add_argument("--dia-skill", action="store_true",
3007
3307
  help="Generate Dia browser skill prompt from current nucleus context")
3008
3308
 
3309
+ # ── Cycle Management (enoch-tree-reverb: branch-seif-cycle-module) ──
3310
+ parser.add_argument("--cycle", metavar="ACTION", nargs="?", const="status",
3311
+ choices=["status", "audit", "meditate", "absorb", "close",
3312
+ "new", "full-circle"],
3313
+ help="Cycle management: status|audit|meditate|absorb|close|new|full-circle")
3314
+ parser.add_argument("--cycle-name", metavar="NAME",
3315
+ help="Cycle name for --cycle new")
3316
+ parser.add_argument("--cycle-parent", metavar="PARENT",
3317
+ help="Parent cycle for --cycle new (auto-detected if omitted)")
3318
+ parser.add_argument("--identity-scan", metavar="TARGET",
3319
+ nargs="?", const="local",
3320
+ help="Scan resonance identities. TARGET: 'local' (default) or SSH host e.g. 'my-laptop'")
3321
+ parser.add_argument("--identity-scan-path", metavar="PATH",
3322
+ help="Remote path for --identity-scan (default: ~/seif-admin/seif-context/modules or SEIF_CONTEXT_MODULES)")
3323
+ parser.add_argument("--start", action="store_true",
3324
+ help="Open SEIF Suite in browser + show cycle status + load agent roles")
3325
+ parser.add_argument("--agents", action="store_true",
3326
+ help="Show current agent role assignments for this workspace")
3327
+ parser.add_argument("--agents-set", metavar="ROLE=AGENT",
3328
+ help="Set an agent role (owner only). E.g. --agents-set writer=claude")
3329
+ parser.add_argument("--sync-workspace", action="store_true",
3330
+ help="Sync all SEIF repos on a remote device via SSH (owner only, same local network)")
3331
+ parser.add_argument("--sync-workspace-host", metavar="HOST",
3332
+ help="SSH host/alias for --sync-workspace (e.g. my-laptop). Falls back to SEIF_SYNC_HOST env var.")
3333
+ parser.add_argument("--sync-workspace-dry-run", action="store_true",
3334
+ help="Show what --sync-workspace would do without making changes")
3335
+
3009
3336
  args = parser.parse_args()
3010
3337
 
3011
3338
  # ── Personal Nucleus commands ──
@@ -3115,13 +3442,14 @@ def main():
3115
3442
  except ImportError:
3116
3443
  print("This feature requires SEIF Suite. Learn more: https://seifos.io")
3117
3444
  return
3445
+ from seif.cli.resonance_display import resonance_header, health_status_line
3118
3446
  detected = detect_backends()
3119
3447
  healthy = get_healthy_backends(detected)
3120
- print(f"Detected backends: {', '.join(detected) or 'none'}")
3121
- print(f"Healthy backends: {', '.join(healthy) or 'none'}")
3448
+ print(resonance_header("SEIF HEALTH", f"backends: {len(healthy)}/{len(detected)} healthy"))
3449
+ print(health_status_line(len(healthy), len(detected)))
3122
3450
  unhealthy = set(detected) - set(healthy)
3123
3451
  if unhealthy:
3124
- print(f"Unhealthy: {', '.join(unhealthy)}")
3452
+ print(f"\n ζ❌ unhealthy: {', '.join(unhealthy)}")
3125
3453
  print()
3126
3454
  print(describe_health())
3127
3455
  return
@@ -3138,6 +3466,80 @@ def main():
3138
3466
  print(result)
3139
3467
  return
3140
3468
 
3469
+ if args.cycle:
3470
+ from seif.context.cycle import (
3471
+ cycle_status, cycle_audit, cycle_meditate, cycle_absorb,
3472
+ cycle_close, cycle_new, cycle_full_circle,
3473
+ )
3474
+ ctx_repo = args.context_repo or None
3475
+ action = args.cycle.lower()
3476
+ if action == "status":
3477
+ print(cycle_status(ctx_repo))
3478
+ elif action == "audit":
3479
+ print(cycle_audit(ctx_repo))
3480
+ elif action == "meditate":
3481
+ print(cycle_meditate(ctx_repo))
3482
+ elif action == "absorb":
3483
+ print(cycle_absorb(ctx_repo))
3484
+ elif action == "close":
3485
+ from seif.cli.resonance_display import resonance_footer
3486
+ print(cycle_close(context_repo=ctx_repo))
3487
+ print(resonance_footer())
3488
+ elif action == "new":
3489
+ if not args.cycle_name:
3490
+ print("Error: --cycle-name required for --cycle new")
3491
+ print("Usage: seif --cycle new --cycle-name <name>")
3492
+ return
3493
+ print(cycle_new(args.cycle_name, args.cycle_parent, ctx_repo))
3494
+ elif action == "full-circle":
3495
+ print(cycle_full_circle(ctx_repo))
3496
+ return
3497
+
3498
+ if args.identity_scan is not None:
3499
+ try:
3500
+ from seif_engine.identity.scanner import scan_workspace, format_scan_report
3501
+ except ImportError:
3502
+ print("⚠ seif-engine not available — identity scanner requires the engine.")
3503
+ return
3504
+ target = args.identity_scan or "local"
3505
+ import socket as _socket
3506
+ _machine_id = __import__("os").environ.get("SEIF_MACHINE_ID") or _socket.gethostname()
3507
+ local_scan = scan_workspace(machine=_machine_id)
3508
+ if target == "local":
3509
+ print(format_scan_report(local_scan))
3510
+ else:
3511
+ remote_path = getattr(args, "identity_scan_path", None) or \
3512
+ __import__("os").environ.get("SEIF_CONTEXT_MODULES", "~/seif-admin/seif-context/modules")
3513
+ remote_scan = scan_workspace(
3514
+ machine="air-m1",
3515
+ ssh_host=target,
3516
+ remote_path=remote_path,
3517
+ )
3518
+ print(format_scan_report(local_scan, remote_scan))
3519
+ return
3520
+
3521
+ # ctx_repo default for owner-level commands (start, agents, sync-workspace)
3522
+ if "ctx_repo" not in dir():
3523
+ ctx_repo = getattr(args, "context_repo", None) or None
3524
+
3525
+ if args.start:
3526
+ _cmd_start(ctx_repo)
3527
+ return
3528
+
3529
+ if args.agents:
3530
+ _cmd_agents_show(ctx_repo)
3531
+ return
3532
+
3533
+ if args.agents_set:
3534
+ _cmd_agents_set(args.agents_set, ctx_repo)
3535
+ return
3536
+
3537
+ if args.sync_workspace or args.sync_workspace_dry_run:
3538
+ host = getattr(args, "sync_workspace_host", None)
3539
+ dry = getattr(args, "sync_workspace_dry_run", False)
3540
+ _cmd_sync_workspace(ctx_repo, host=host, dry_run=dry)
3541
+ return
3542
+
3141
3543
  if args.fingerprint_verify:
3142
3544
  cmd_fingerprint_verify(args.fingerprint_verify)
3143
3545
  return
@@ -3224,6 +3626,8 @@ def main():
3224
3626
  path = close_session(ctx, name, author_name)
3225
3627
  print(f"Session '{name}' closed.")
3226
3628
  print(f" Archived: {path}")
3629
+ from seif.cli.resonance_display import resonance_footer
3630
+ print(resonance_footer())
3227
3631
  elif action == "list":
3228
3632
  sessions = list_sessions(ctx)
3229
3633
  if not sessions: