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.
- {seif_cli-0.3.0/src/seif_cli.egg-info → seif_cli-0.3.1}/PKG-INFO +8 -5
- {seif_cli-0.3.0 → seif_cli-0.3.1}/README.md +3 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/pyproject.toml +5 -5
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/analysis/quality_gate.py +13 -12
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/cli/cli.py +414 -10
- seif_cli-0.3.1/src/seif/cli/resonance_display.py +98 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/constants.py +7 -2
- seif_cli-0.3.1/src/seif/context/cycle.py +543 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1/src/seif_cli.egg-info}/PKG-INFO +8 -5
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif_cli.egg-info/SOURCES.txt +2 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif_cli.egg-info/requires.txt +1 -1
- {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_context_qr.py +6 -2
- {seif_cli-0.3.0 → seif_cli-0.3.1}/LICENSE +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/MANIFEST.in +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/setup.cfg +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/__init__.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/__main__.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/analysis/__init__.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/analysis/physical_constants.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/analysis/stance_detector.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/analysis/transcompiler.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/bridge/__init__.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/bridge/telegram_bot.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/cli/__init__.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/cli/__main__.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/cli/identity.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/cli/main.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/cli/serve.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/cli/serve_v2.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/cli/wrapper.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/__init__.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/advisor.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/code_compressor.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/context_bridge.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/context_importer.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/context_manager.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/context_qr.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/file_extractor.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/git_context.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/git_hooks.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/ingest.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/nucleus.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/ref.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/seif_io.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/context/workspace.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/__init__.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/fingerprint.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/resonance_encoding.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/resonance_gate.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/resonance_signal.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/signing.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/timestamping.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/transfer_function.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/core/triple_gate.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/data/RESONANCE.json +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/data/__init__.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/data/defaults/__init__.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/data/paths.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/security/__init__.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/security/mode.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif/security/redblue.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif_cli.egg-info/dependency_links.txt +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif_cli.egg-info/entry_points.txt +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/src/seif_cli.egg-info/top_level.txt +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_advisor.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_canonical_inputs.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_code_compressor.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_collaborative_seif.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_context_repo.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_git_context.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_git_hooks.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_init.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_quality_gate.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_ref.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_resonance_gate.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_seif_io.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_stance_detector.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_transcompiler.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_transfer_function.py +0 -0
- {seif_cli-0.3.0 → seif_cli-0.3.1}/tests/test_triple_gate.py +0 -0
- {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.
|
|
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://
|
|
8
|
-
Project-URL: Documentation, https://
|
|
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:
|
|
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
|
|
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.
|
|
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
|
|
67
|
+
"seif[consensus,generators,web,telegram,qr]",
|
|
68
68
|
]
|
|
69
69
|
|
|
70
70
|
[project.urls]
|
|
71
|
-
Homepage = "https://
|
|
72
|
-
Documentation = "https://
|
|
71
|
+
Homepage = "https://seifprotocol.com"
|
|
72
|
+
Documentation = "https://seifprotocol.com/docs"
|
|
73
73
|
Repository = "https://github.com/and2carvalho/seif"
|
|
74
|
-
|
|
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 >=
|
|
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: {
|
|
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 <
|
|
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"{
|
|
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
|
-
#
|
|
225
|
-
|
|
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"{
|
|
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 >=
|
|
239
|
-
f"
|
|
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
|
-
"
|
|
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
|
-
|
|
280
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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"
|
|
3121
|
-
print(
|
|
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"
|
|
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:
|