Skip to content

Commit 26628df

Browse files
author
codejunkie99
committed
fix: include full memory in transfer intent
1 parent bf24b1c commit 26628df

7 files changed

Lines changed: 90 additions & 23 deletions

File tree

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,13 @@ or a terminal-only project with the onboarding-style TUI:
203203
```
204204

205205
The wizard turns a plain-language intent into a transfer plan, lets you
206-
review target harnesses and memory scopes, redacts risky content by default,
207-
and emits a one-line curl command the next environment can run. The importer
206+
review target harnesses and memory scopes, blocks secret-like content before
207+
export, and emits a one-line curl command the next environment can run.
208+
For `move my memory`, it includes preferences, accepted lessons, skills,
209+
working memory, episodic/history logs, and candidate lessons. The importer
208210
unpacks the bundle, verifies its SHA-256 digest, merges preferences and
209-
accepted lessons, copies selected skills, and installs the matching adapter
210-
files.
211+
accepted lessons, copies selected skills, restores selected memory files,
212+
and installs the matching adapter files.
211213

212214
For scripted handoff:
213215

docs/superpowers/specs/2026-05-02-transfer-tui-wizard-design.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Primary sources checked:
3232
2. Let users describe the transfer in natural language, then confirm or edit the parsed target and scope.
3333
3. Export a portable, signed memory bundle that can be imported through a generated curl command.
3434
4. Apply the transfer locally when requested, using the existing harness manager adapter installation path.
35-
5. Preserve privacy by default: accepted lessons and preferences transfer; episodic logs, working memory, rejected candidates, and raw traces stay out unless explicitly selected.
35+
5. Treat `move my memory` as the full portable memory set: preferences, accepted lessons, skills, working memory, episodic/history logs, and candidate lessons. Preserve privacy with preview, explicit confirmation for sensitive scopes, and secret blocking.
3636
6. Keep merge behavior transparent and reversible through preview, checksums, and existing git workflows.
3737

3838
## Non-Goals
@@ -74,7 +74,7 @@ The parser detects:
7474

7575
- targets: `codex`, `cursor`, `windsurf`, `terminal`, `all`
7676
- operation: `apply-here`, `generate-curl`, `both`
77-
- scope hints: `preferences`, `lessons`, `skills`, `working`, `episodic`
77+
- scope hints: `preferences`, `lessons`, `skills`, `working`, `episodic`, `history`, `candidates`
7878

7979
If parsing is uncertain, the wizard defaults to `all` targets only after showing a warning and requiring confirmation.
8080

@@ -91,18 +91,18 @@ The user edits detected targets through a multi-select list:
9191

9292
### Step 3: Scope
9393

94-
Default selected:
94+
Default selected for `move my memory`:
9595

9696
- `.agent/memory/personal/PREFERENCES.md`
9797
- accepted rows from `.agent/memory/semantic/lessons.jsonl`
9898
- rendered semantic fallback from `.agent/memory/semantic/LESSONS.md` only when `lessons.jsonl` is missing
9999
- `.agent/skills/` metadata and skill folders, excluding runtime stores
100-
101-
Default unselected:
102-
103100
- `.agent/memory/working/WORKSPACE.md`
104101
- `.agent/memory/episodic/AGENT_LEARNINGS.jsonl`
105102
- staged/rejected candidates
103+
104+
Default unselected:
105+
106106
- `.agent/data-layer/`
107107
- `.agent/flywheel/`
108108
- `.agent/memory/.index/`

harness_manager/transfer_bundle.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,16 @@ def export_bundle(
7878

7979
if "skills" in scopes:
8080
skills_root = agent_root / "skills"
81-
if skills_root.is_dir():
82-
for path in sorted(p for p in skills_root.rglob("*") if p.is_file()):
83-
if _is_runtime_path(path.relative_to(agent_root)):
84-
continue
85-
_add_file(bundle, agent_root, path)
81+
_add_tree(bundle, agent_root, skills_root)
82+
83+
if "working" in scopes:
84+
_add_tree(bundle, agent_root, agent_root / "memory" / "working")
85+
86+
if "episodic" in scopes:
87+
_add_tree(bundle, agent_root, agent_root / "memory" / "episodic")
88+
89+
if "candidates" in scopes:
90+
_add_tree(bundle, agent_root, agent_root / "memory" / "candidates")
8691

8792
return bundle
8893

@@ -168,6 +173,15 @@ def _add_file(bundle: dict[str, Any], agent_root: Path, path: Path) -> None:
168173
)
169174

170175

176+
def _add_tree(bundle: dict[str, Any], agent_root: Path, root: Path) -> None:
177+
if not root.is_dir():
178+
return
179+
for path in sorted(p for p in root.rglob("*") if p.is_file()):
180+
if _is_runtime_path(path.relative_to(agent_root)):
181+
continue
182+
_add_file(bundle, agent_root, path)
183+
184+
171185
def _ensure_allowed(path: Path) -> None:
172186
parts = path.parts
173187
if not parts or parts[0] != ".agent":

harness_manager/transfer_plan.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88

99

1010
VALID_TARGETS = ("codex", "cursor", "windsurf", "terminal")
11-
DEFAULT_SCOPES = ("preferences", "accepted_lessons", "skills")
11+
CORE_SCOPES = ("preferences", "accepted_lessons", "skills")
12+
DEFAULT_SCOPES = CORE_SCOPES + ("working", "episodic", "candidates")
1213
SENSITIVE_SCOPES = ("working", "episodic", "candidates", "data_layer", "flywheel")
13-
VALID_SCOPES = DEFAULT_SCOPES + SENSITIVE_SCOPES
14+
VALID_SCOPES = CORE_SCOPES + SENSITIVE_SCOPES
1415

1516
TARGET_ALIASES = {
1617
"codex": "codex",
@@ -35,8 +36,8 @@
3536
"lesson": "accepted_lessons",
3637
"lessons": "accepted_lessons",
3738
"semantic": "accepted_lessons",
38-
"memory": "accepted_lessons",
39-
"memories": "accepted_lessons",
39+
"memory": "full_memory",
40+
"memories": "full_memory",
4041
"skill": "skills",
4142
"skills": "skills",
4243
"working": "working",
@@ -124,6 +125,9 @@ def normalize_scopes(values: Iterable[str] | None) -> tuple[str, ...]:
124125
for value in values:
125126
key = value.casefold().strip().replace("-", "_")
126127
scope = SCOPE_ALIASES.get(key, key)
128+
if scope == "full_memory":
129+
selected.update(DEFAULT_SCOPES)
130+
continue
127131
if scope not in VALID_SCOPES:
128132
continue
129133
selected.add(scope)
@@ -133,8 +137,7 @@ def normalize_scopes(values: Iterable[str] | None) -> tuple[str, ...]:
133137

134138

135139
def detect_scopes(intent: str) -> tuple[str, ...]:
136-
selected = normalize_scopes(_tokens(intent))
137-
return selected if selected != ("accepted_lessons",) else DEFAULT_SCOPES
140+
return normalize_scopes(_tokens(intent))
138141

139142

140143
def detect_operation(intent: str) -> str:

harness_manager/transfer_tui.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ def run_wizard(target_root: Path, stack_root: Path) -> int:
193193
"skills",
194194
"working",
195195
"episodic",
196+
"candidates",
196197
]
197198
scope_defaults = [scope_choices.index(s) for s in DEFAULT_SCOPES]
198199
chosen_scopes = widgets.ask_multiselect(

test_transfer_bundle.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ def make_agent(self, root: Path):
1818
agent = root / ".agent"
1919
(agent / "memory" / "personal").mkdir(parents=True)
2020
(agent / "memory" / "semantic").mkdir(parents=True)
21+
(agent / "memory" / "working").mkdir(parents=True)
22+
(agent / "memory" / "episodic").mkdir(parents=True)
23+
(agent / "memory" / "candidates" / "pending").mkdir(parents=True)
2124
(agent / "skills" / "deploy-checklist").mkdir(parents=True)
2225
(agent / "memory" / "personal" / "PREFERENCES.md").write_text(
2326
"# Preferences\n\n- Use concise explanations.\n",
@@ -38,6 +41,18 @@ def make_agent(self, root: Path):
3841
"---\nname: deploy-checklist\n---\nCheck deploys.\n",
3942
encoding="utf-8",
4043
)
44+
(agent / "memory" / "working" / "WORKSPACE.md").write_text(
45+
"# Workspace\n\nCurrent task context.\n",
46+
encoding="utf-8",
47+
)
48+
(agent / "memory" / "episodic" / "AGENT_LEARNINGS.jsonl").write_text(
49+
json.dumps({"event": "debugged importer", "ts": "2026-05-02T00:00:00Z"}) + "\n",
50+
encoding="utf-8",
51+
)
52+
(agent / "memory" / "candidates" / "pending" / "candidate.json").write_text(
53+
json.dumps({"id": "candidate_keep", "claim": "Review before accepting."}) + "\n",
54+
encoding="utf-8",
55+
)
4156
return agent
4257

4358
def test_export_encode_decode_round_trip(self):
@@ -108,6 +123,28 @@ def test_export_rejects_secret_preferences(self):
108123
with self.assertRaises(BundleSecurityError):
109124
export_bundle(agent, targets=["codex"], scopes=["preferences"])
110125

126+
def test_export_and_import_full_memory_scopes(self):
127+
with tempfile.TemporaryDirectory() as src_tmp, tempfile.TemporaryDirectory() as dst_tmp:
128+
src_agent = self.make_agent(Path(src_tmp))
129+
dst = Path(dst_tmp)
130+
131+
bundle = export_bundle(
132+
src_agent,
133+
targets=["codex"],
134+
scopes=["working", "episodic", "candidates"],
135+
)
136+
paths = {entry["path"] for entry in bundle["files"]}
137+
138+
self.assertIn(".agent/memory/working/WORKSPACE.md", paths)
139+
self.assertIn(".agent/memory/episodic/AGENT_LEARNINGS.jsonl", paths)
140+
self.assertIn(".agent/memory/candidates/pending/candidate.json", paths)
141+
142+
import_bundle(bundle, dst)
143+
144+
self.assertTrue((dst / ".agent" / "memory" / "working" / "WORKSPACE.md").exists())
145+
self.assertTrue((dst / ".agent" / "memory" / "episodic" / "AGENT_LEARNINGS.jsonl").exists())
146+
self.assertTrue((dst / ".agent" / "memory" / "candidates" / "pending" / "candidate.json").exists())
147+
111148

112149
if __name__ == "__main__":
113150
unittest.main()

test_transfer_plan.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ def test_detects_codex_curl_transfer_defaults(self):
1919

2020
self.assertEqual(plan.targets, ("codex",))
2121
self.assertEqual(plan.operation, "generate-curl")
22-
self.assertEqual(plan.scopes, tuple(DEFAULT_SCOPES))
23-
self.assertFalse(plan.sensitive_scopes)
22+
self.assertEqual(
23+
plan.scopes,
24+
("preferences", "accepted_lessons", "skills", "working", "episodic", "candidates"),
25+
)
26+
self.assertEqual(plan.sensitive_scopes, ("working", "episodic", "candidates"))
2427
self.assertIn("AGENTS.md", [a.dst for a in plan.adapter_actions])
2528
self.assertIn(".agents/skills", [a.dst for a in plan.adapter_actions])
2629

@@ -57,6 +60,13 @@ def test_terminal_preview_is_agents_md_only(self):
5760
self.assertEqual([a.dst for a in plan.adapter_actions], ["AGENTS.md"])
5861
self.assertEqual(plan.adapter_actions[0].merge_policy, "merge_or_alert")
5962

63+
def test_history_alias_selects_episodic_scope(self):
64+
plan = build_plan("move history into codex", ROOT)
65+
66+
self.assertEqual(plan.targets, ("codex",))
67+
self.assertIn("episodic", plan.scopes)
68+
self.assertEqual(plan.sensitive_scopes, ("episodic",))
69+
6070

6171
if __name__ == "__main__":
6272
unittest.main()

0 commit comments

Comments
 (0)