jeo-code 0.6.16 → 0.6.18

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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
8
8
 
9
+ ## [0.6.18] - 2026-06-17
10
+ _Memory data-flow diagram and a README "Memory flow" section documenting the actual runtime behavior._
11
+
12
+ ### Added
13
+ - **Editable memory data-flow diagram (`docs/diagrams/memory-flow.drawio`).** A draw.io swimlane diagram traces the OKF memory system's actual runtime behavior end to end: the **WRITE** lane (session-exit `spawnDetachedDistill` → `distillSessionMemory` → one JSON-mode LLM call → per-concept atomic upsert into `facts/`/`commands/`/`gotchas/`/`preferences/`, with a plain-text legacy fallback, then `rebuildIndex`/`updateLog`), the **STORE** lane (the typed concept bundle, `index.md`/`log.md`, the cross-link graph, and the legacy `MEMORY.md`/`.bak`), the **READ** lane (`memoryPromptSection` → `JEO_NO_MEMORY`/`JEO_MEMORY_LEGACY` gates → `loadConcepts` → `selectWithinBudget` priority order with 1-hop graph expansion → `frameMemory` injection-hardening → `<project_memory>` injection), and the one-shot idempotent **MIGRATION** lane (`jeo memory-migrate`).
14
+ - **README "Memory flow" section (all four languages).** A new section in `README.md` / `README.ko.md` / `README.ja.md` / `README.zh.md` explains the local-first distilled memory model and embeds a GitHub-renderable Mermaid version of the write/store/read/migration flow, links the editable `.drawio`, and documents the `JEO_NO_MEMORY` / `JEO_MEMORY_LEGACY` toggles and the migration's rollback path.
15
+
16
+ ## [0.6.17] - 2026-06-17
17
+ _Legacy MEMORY.md migrates losslessly into the OKF concept bundle, with a one-shot command and a rollback toggle._
18
+
19
+ ### Added
20
+ - **`jeo memory-migrate` — legacy memory → OKF bundle migration (OKF Sprint 05).** A one-shot, idempotent migration converts the legacy single-doc `.jeo/memory/MEMORY.md` into the type-partitioned OKF concept bundle: `parseLegacyMemory` maps each `## heading` to a concept type (commands/gotchas/preferences/repo-facts, unknown → RepoFact) and splits top-level bullets into concepts (`**title**: description` form recognized, indented continuation lines become the body — lossless), then `migrateLegacyMemory` writes each concept atomically under `facts/`/`commands/`/`gotchas/`/`preferences/`, (re)builds `index.md`/`log.md`, and renames the legacy doc to `MEMORY.md.bak` for rollback. Re-running is a no-op once the bundle has concepts. The bundle is the default read path; `JEO_MEMORY_LEGACY=1` is a new rollback toggle that ignores the bundle and reads the legacy doc (or its `.bak` backup) through the same injection-hardening, while `JEO_NO_MEMORY=1` still wins over everything.
21
+
9
22
  ## [0.6.16] - 2026-06-17
10
23
  _OKF memory grows a concept cross-link graph: 1-hop search expansion, bundle lint, graphify-optional._
11
24
 
package/README.ja.md CHANGED
@@ -119,6 +119,48 @@ jeo ultragoal
119
119
 
120
120
  非ゼロ終了したフックの出力はモデルが読むツール結果に付加され(バッチ内で重複排除)、フックが赤のまま `done` を呼ぶとフック名付きでプッシュバックされます。
121
121
 
122
+ ## メモリフロー
123
+
124
+ `jeo` は `.jeo/memory/` 配下に **ローカルファースト・蒸留されたプロジェクトメモリ** を保持します(リモートバックエンドなし、ネイティブ依存ゼロ)。過去のセッションは [OKF](docs/okf_mem/) コンセプトバンドルへ蒸留され、次のセッションは関連性の高い予算内のスライスだけをシステムプロンプトへ再注入します — 指示ではなく DATA として堅牢化されます。`JEO_NO_MEMORY=1` ですべて無効化。
125
+
126
+ 📐 **編集可能な図:** [`docs/diagrams/memory-flow.drawio`](docs/diagrams/memory-flow.drawio)([draw.io](https://app.diagrams.net) / デスクトップアプリで開く) — 書き込み/保存/読み込み/移行の全スイムレーン。概要:
127
+
128
+ ```mermaid
129
+ flowchart LR
130
+ subgraph WRITE["WRITE — session-end distill (detached, best-effort)"]
131
+ direction TB
132
+ W1["session exit / ^C^C"] --> W2["spawnDetachedDistill()<br/>payload + detached child, returns instantly"]
133
+ W2 --> W3["distillSessionMemory()<br/>load bundle · transcriptTail · ONE LLM call (JSON)"]
134
+ W3 --> WD{"concepts JSON<br/>parsed?"}
135
+ WD -->|yes| WY["per concept: upsert by title,<br/>atomic write into facts/ commands/<br/>gotchas/ preferences/"]
136
+ WD -->|no| WN["plain text →<br/>legacy MEMORY.md"]
137
+ WY --> WR["rebuildIndex() index.md<br/>updateLog() log.md"]
138
+ end
139
+
140
+ subgraph STORE[".jeo/memory/ — OKF concept bundle"]
141
+ direction TB
142
+ S1["facts/ · commands/ · gotchas/ · preferences/<br/>(YAML frontmatter + body)"]
143
+ S2["index.md · log.md · cross-link graph (Sprint 04)"]
144
+ S3["MEMORY.md (legacy fallback)<br/>MEMORY.md.bak (rollback)"]
145
+ end
146
+
147
+ subgraph READ["READ — memoryPromptSection(cwd, query)"]
148
+ direction TB
149
+ R1["session start (query = task text)"] --> R2{"bundle has<br/>concepts?"}
150
+ R2 -->|yes| R3["selectWithinBudget()<br/>core → query relevance → 1-hop graph<br/>≤ MEMORY_INJECT_MAX_CHARS (3000)"]
151
+ R2 -->|no| R3B["legacy loadMemory()"]
152
+ R3 --> R4["frameMemory()<br/>hard cap · fence-neutralize · DATA framing"]
153
+ R3B --> R4
154
+ R4 --> R5["&lt;project_memory&gt; … injected into system prompt"]
155
+ end
156
+
157
+ WR -->|atomic| STORE
158
+ WN -->|fallback| S3
159
+ STORE -.->|loadConcepts / loadMemory| READ
160
+ ```
161
+
162
+ **移行(`jeo memory-migrate`、ワンショット・冪等).** レガシーの単一ドキュメント `MEMORY.md` をロスレスでバンドルへ変換します: `## 見出し → タイプ`、各箇条書き → タイプ別コンセプト、インデント行 → 本文; `index.md`/`log.md` を再構築し、元ファイルを `MEMORY.md.bak` にリネームします。バンドルにコンセプトができた後の再実行は no-op です。**ロールバック:** `JEO_MEMORY_LEGACY=1` はバンドルを無視し、同じ注入堅牢化を通して `MEMORY.md`/`.bak` を読みます(`JEO_NO_MEMORY=1` がすべてに優先)。
163
+
122
164
  ## ローカルモデル
123
165
 
124
166
  ```bash
@@ -158,11 +200,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
158
200
  ## 変更履歴 (Changelog)
159
201
 
160
202
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
203
+ - **[0.6.18]** (2026-06-17) — Memory data-flow diagram and a README "Memory flow" section documenting the actual runtime behavior.
204
+ - **[0.6.17]** (2026-06-17) — Legacy MEMORY.md migrates losslessly into the OKF concept bundle, with a one-shot command and a rollback toggle.
161
205
  - **[0.6.16]** (2026-06-17) — OKF memory grows a concept cross-link graph: 1-hop search expansion, bundle lint, graphify-optional.
162
206
  - **[0.6.15]** (2026-06-17) — Query-aware OKF memory injection with budget-priority selection, and a truthful end-of-turn Todos receipt.
163
207
  - **[0.6.14]** (2026-06-16) — Memory distillation survives malformed model output, and stream-idle stalls retry instead of failing the turn.
164
- - **[0.6.13]** (2026-06-16) — `team` engine: concrete uncommitted-work reporting and stricter empty-run handling.
165
- - **[0.6.12]** (2026-06-16) — OKF-backed memory distillation — session learnings become structured concept files.
166
208
 
167
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
168
210
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -119,6 +119,48 @@ jeo ultragoal
119
119
 
120
120
  비정상 종료한 훅의 출력은 모델이 읽는 도구 결과에 첨부되고(배치 내 중복 제거), 훅이 빨간 채로 `done`을 부르면 훅 이름과 함께 푸시백됩니다.
121
121
 
122
+ ## 메모리 흐름
123
+
124
+ `jeo`는 `.jeo/memory/` 아래에 **로컬 우선·증류된 프로젝트 메모리**를 둡니다(원격 백엔드 없음, 네이티브 의존성 0). 지난 세션은 [OKF](docs/okf_mem/) 개념 번들로 증류되고, 다음 세션은 관련성 높은 예산 한도 내 일부만 시스템 프롬프트로 다시 주입합니다 — 지시가 아닌 DATA로 강화 처리됩니다. `JEO_NO_MEMORY=1`로 전체 비활성화.
125
+
126
+ 📐 **편집 가능한 다이어그램:** [`docs/diagrams/memory-flow.drawio`](docs/diagrams/memory-flow.drawio) ([draw.io](https://app.diagrams.net) / 데스크톱 앱에서 열기) — 쓰기/저장/읽기/마이그레이션 전체 스윔레인. 요약 보기:
127
+
128
+ ```mermaid
129
+ flowchart LR
130
+ subgraph WRITE["WRITE — session-end distill (detached, best-effort)"]
131
+ direction TB
132
+ W1["session exit / ^C^C"] --> W2["spawnDetachedDistill()<br/>payload + detached child, returns instantly"]
133
+ W2 --> W3["distillSessionMemory()<br/>load bundle · transcriptTail · ONE LLM call (JSON)"]
134
+ W3 --> WD{"concepts JSON<br/>parsed?"}
135
+ WD -->|yes| WY["per concept: upsert by title,<br/>atomic write into facts/ commands/<br/>gotchas/ preferences/"]
136
+ WD -->|no| WN["plain text →<br/>legacy MEMORY.md"]
137
+ WY --> WR["rebuildIndex() index.md<br/>updateLog() log.md"]
138
+ end
139
+
140
+ subgraph STORE[".jeo/memory/ — OKF concept bundle"]
141
+ direction TB
142
+ S1["facts/ · commands/ · gotchas/ · preferences/<br/>(YAML frontmatter + body)"]
143
+ S2["index.md · log.md · cross-link graph (Sprint 04)"]
144
+ S3["MEMORY.md (legacy fallback)<br/>MEMORY.md.bak (rollback)"]
145
+ end
146
+
147
+ subgraph READ["READ — memoryPromptSection(cwd, query)"]
148
+ direction TB
149
+ R1["session start (query = task text)"] --> R2{"bundle has<br/>concepts?"}
150
+ R2 -->|yes| R3["selectWithinBudget()<br/>core → query relevance → 1-hop graph<br/>≤ MEMORY_INJECT_MAX_CHARS (3000)"]
151
+ R2 -->|no| R3B["legacy loadMemory()"]
152
+ R3 --> R4["frameMemory()<br/>hard cap · fence-neutralize · DATA framing"]
153
+ R3B --> R4
154
+ R4 --> R5["&lt;project_memory&gt; … injected into system prompt"]
155
+ end
156
+
157
+ WR -->|atomic| STORE
158
+ WN -->|fallback| S3
159
+ STORE -.->|loadConcepts / loadMemory| READ
160
+ ```
161
+
162
+ **마이그레이션 (`jeo memory-migrate`, 1회성 · 멱등).** 레거시 단일 문서 `MEMORY.md`를 무손실로 번들로 변환합니다: `## 헤딩 → 타입`, 각 불릿 → 타입별 개념, 들여쓴 줄 → 본문; `index.md`/`log.md`를 재생성하고 원본은 `MEMORY.md.bak`으로 이름을 바꿉니다. 번들에 개념이 생긴 뒤 재실행은 no-op입니다. **롤백:** `JEO_MEMORY_LEGACY=1`은 번들을 무시하고 동일한 주입 강화 처리로 `MEMORY.md`/`.bak`를 읽습니다(`JEO_NO_MEMORY=1`이 모든 것에 우선).
163
+
122
164
  ## 로컬 모델
123
165
 
124
166
  ```bash
@@ -158,11 +200,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
158
200
  ## 변경 이력 (Changelog)
159
201
 
160
202
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
203
+ - **[0.6.18]** (2026-06-17) — Memory data-flow diagram and a README "Memory flow" section documenting the actual runtime behavior.
204
+ - **[0.6.17]** (2026-06-17) — Legacy MEMORY.md migrates losslessly into the OKF concept bundle, with a one-shot command and a rollback toggle.
161
205
  - **[0.6.16]** (2026-06-17) — OKF memory grows a concept cross-link graph: 1-hop search expansion, bundle lint, graphify-optional.
162
206
  - **[0.6.15]** (2026-06-17) — Query-aware OKF memory injection with budget-priority selection, and a truthful end-of-turn Todos receipt.
163
207
  - **[0.6.14]** (2026-06-16) — Memory distillation survives malformed model output, and stream-idle stalls retry instead of failing the turn.
164
- - **[0.6.13]** (2026-06-16) — `team` engine: concrete uncommitted-work reporting and stricter empty-run handling.
165
- - **[0.6.12]** (2026-06-16) — OKF-backed memory distillation — session learnings become structured concept files.
166
208
 
167
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
168
210
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -119,6 +119,48 @@ Enable hooks once globally (`"hooks": { "enabled": true }` in `~/.jeo/config.jso
119
119
 
120
120
  Non-zero hook output is appended to the tool result the model reads (deduped per batch); a still-red hook triggers a `done` pushback naming the hook.
121
121
 
122
+ ## Memory flow
123
+
124
+ `jeo` keeps a **local-first, distilled project memory** under `.jeo/memory/` (no remote backend, zero native deps). Past sessions are distilled into an [OKF](docs/okf_mem/) concept bundle, and the next session injects only the relevant, budget-bounded slice back into the system prompt — hardened as DATA, never as instructions. Disable everything with `JEO_NO_MEMORY=1`.
125
+
126
+ 📐 **Editable diagram:** [`docs/diagrams/memory-flow.drawio`](docs/diagrams/memory-flow.drawio) (open in [draw.io](https://app.diagrams.net) / the desktop app) — full write/store/read/migration swimlanes. Quick view:
127
+
128
+ ```mermaid
129
+ flowchart LR
130
+ subgraph WRITE["WRITE — session-end distill (detached, best-effort)"]
131
+ direction TB
132
+ W1["session exit / ^C^C"] --> W2["spawnDetachedDistill()<br/>payload + detached child, returns instantly"]
133
+ W2 --> W3["distillSessionMemory()<br/>load bundle · transcriptTail · ONE LLM call (JSON)"]
134
+ W3 --> WD{"concepts JSON<br/>parsed?"}
135
+ WD -->|yes| WY["per concept: upsert by title,<br/>atomic write into facts/ commands/<br/>gotchas/ preferences/"]
136
+ WD -->|no| WN["plain text →<br/>legacy MEMORY.md"]
137
+ WY --> WR["rebuildIndex() index.md<br/>updateLog() log.md"]
138
+ end
139
+
140
+ subgraph STORE[".jeo/memory/ — OKF concept bundle"]
141
+ direction TB
142
+ S1["facts/ · commands/ · gotchas/ · preferences/<br/>(YAML frontmatter + body)"]
143
+ S2["index.md · log.md · cross-link graph (Sprint 04)"]
144
+ S3["MEMORY.md (legacy fallback)<br/>MEMORY.md.bak (rollback)"]
145
+ end
146
+
147
+ subgraph READ["READ — memoryPromptSection(cwd, query)"]
148
+ direction TB
149
+ R1["session start (query = task text)"] --> R2{"bundle has<br/>concepts?"}
150
+ R2 -->|yes| R3["selectWithinBudget()<br/>core → query relevance → 1-hop graph<br/>≤ MEMORY_INJECT_MAX_CHARS (3000)"]
151
+ R2 -->|no| R3B["legacy loadMemory()"]
152
+ R3 --> R4["frameMemory()<br/>hard cap · fence-neutralize · DATA framing"]
153
+ R3B --> R4
154
+ R4 --> R5["&lt;project_memory&gt; … injected into system prompt"]
155
+ end
156
+
157
+ WR -->|atomic| STORE
158
+ WN -->|fallback| S3
159
+ STORE -.->|loadConcepts / loadMemory| READ
160
+ ```
161
+
162
+ **Migration (`jeo memory-migrate`, one-shot · idempotent).** A legacy single-doc `MEMORY.md` is converted losslessly into the bundle: `## heading → type`, each bullet → a typed concept, indented lines → body; `index.md`/`log.md` are rebuilt and the original is renamed to `MEMORY.md.bak`. Re-running is a no-op once the bundle has concepts. **Rollback:** `JEO_MEMORY_LEGACY=1` ignores the bundle and reads `MEMORY.md`/`.bak` through the same injection-hardening (`JEO_NO_MEMORY=1` still wins over everything).
163
+
122
164
  ## Local models
123
165
 
124
166
  ```bash
@@ -158,11 +200,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
158
200
  ## Changelog
159
201
 
160
202
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
203
+ - **[0.6.18]** (2026-06-17) — Memory data-flow diagram and a README "Memory flow" section documenting the actual runtime behavior.
204
+ - **[0.6.17]** (2026-06-17) — Legacy MEMORY.md migrates losslessly into the OKF concept bundle, with a one-shot command and a rollback toggle.
161
205
  - **[0.6.16]** (2026-06-17) — OKF memory grows a concept cross-link graph: 1-hop search expansion, bundle lint, graphify-optional.
162
206
  - **[0.6.15]** (2026-06-17) — Query-aware OKF memory injection with budget-priority selection, and a truthful end-of-turn Todos receipt.
163
207
  - **[0.6.14]** (2026-06-16) — Memory distillation survives malformed model output, and stream-idle stalls retry instead of failing the turn.
164
- - **[0.6.13]** (2026-06-16) — `team` engine: concrete uncommitted-work reporting and stricter empty-run handling.
165
- - **[0.6.12]** (2026-06-16) — OKF-backed memory distillation — session learnings become structured concept files.
166
208
 
167
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
168
210
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -119,6 +119,48 @@ jeo ultragoal
119
119
 
120
120
  非零退出钩子的输出会附加到模型读取的工具结果中(批内去重);钩子未通过就调用 `done` 会收到带钩子名称的回推。
121
121
 
122
+ ## 内存流程
123
+
124
+ `jeo` 在 `.jeo/memory/` 下保存 **本地优先、蒸馏后的项目内存**(无远程后端,零原生依赖)。过往会话被蒸馏为 [OKF](docs/okf_mem/) 概念包,下一次会话仅把相关的、受预算约束的切片重新注入系统提示 —— 作为 DATA 而非指令加固。用 `JEO_NO_MEMORY=1` 完全禁用。
125
+
126
+ 📐 **可编辑图示:** [`docs/diagrams/memory-flow.drawio`](docs/diagrams/memory-flow.drawio)(在 [draw.io](https://app.diagrams.net) / 桌面应用中打开)—— 写入/存储/读取/迁移完整泳道。概览:
127
+
128
+ ```mermaid
129
+ flowchart LR
130
+ subgraph WRITE["WRITE — session-end distill (detached, best-effort)"]
131
+ direction TB
132
+ W1["session exit / ^C^C"] --> W2["spawnDetachedDistill()<br/>payload + detached child, returns instantly"]
133
+ W2 --> W3["distillSessionMemory()<br/>load bundle · transcriptTail · ONE LLM call (JSON)"]
134
+ W3 --> WD{"concepts JSON<br/>parsed?"}
135
+ WD -->|yes| WY["per concept: upsert by title,<br/>atomic write into facts/ commands/<br/>gotchas/ preferences/"]
136
+ WD -->|no| WN["plain text →<br/>legacy MEMORY.md"]
137
+ WY --> WR["rebuildIndex() index.md<br/>updateLog() log.md"]
138
+ end
139
+
140
+ subgraph STORE[".jeo/memory/ — OKF concept bundle"]
141
+ direction TB
142
+ S1["facts/ · commands/ · gotchas/ · preferences/<br/>(YAML frontmatter + body)"]
143
+ S2["index.md · log.md · cross-link graph (Sprint 04)"]
144
+ S3["MEMORY.md (legacy fallback)<br/>MEMORY.md.bak (rollback)"]
145
+ end
146
+
147
+ subgraph READ["READ — memoryPromptSection(cwd, query)"]
148
+ direction TB
149
+ R1["session start (query = task text)"] --> R2{"bundle has<br/>concepts?"}
150
+ R2 -->|yes| R3["selectWithinBudget()<br/>core → query relevance → 1-hop graph<br/>≤ MEMORY_INJECT_MAX_CHARS (3000)"]
151
+ R2 -->|no| R3B["legacy loadMemory()"]
152
+ R3 --> R4["frameMemory()<br/>hard cap · fence-neutralize · DATA framing"]
153
+ R3B --> R4
154
+ R4 --> R5["&lt;project_memory&gt; … injected into system prompt"]
155
+ end
156
+
157
+ WR -->|atomic| STORE
158
+ WN -->|fallback| S3
159
+ STORE -.->|loadConcepts / loadMemory| READ
160
+ ```
161
+
162
+ **迁移(`jeo memory-migrate`,一次性 · 幂等).** 把旧版单文档 `MEMORY.md` 无损转换为概念包: `## 标题 → 类型`,每个项目符号 → 一个类型化概念,缩进行 → 正文; 重建 `index.md`/`log.md`,并把原文件重命名为 `MEMORY.md.bak`。一旦概念包中已有概念,再次运行即为 no-op。**回滚:** `JEO_MEMORY_LEGACY=1` 忽略概念包,通过相同的注入加固读取 `MEMORY.md`/`.bak`(`JEO_NO_MEMORY=1` 仍优先于一切)。
163
+
122
164
  ## 本地模型
123
165
 
124
166
  ```bash
@@ -158,11 +200,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
158
200
  ## 更新日志 (Changelog)
159
201
 
160
202
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
203
+ - **[0.6.18]** (2026-06-17) — Memory data-flow diagram and a README "Memory flow" section documenting the actual runtime behavior.
204
+ - **[0.6.17]** (2026-06-17) — Legacy MEMORY.md migrates losslessly into the OKF concept bundle, with a one-shot command and a rollback toggle.
161
205
  - **[0.6.16]** (2026-06-17) — OKF memory grows a concept cross-link graph: 1-hop search expansion, bundle lint, graphify-optional.
162
206
  - **[0.6.15]** (2026-06-17) — Query-aware OKF memory injection with budget-priority selection, and a truthful end-of-turn Todos receipt.
163
207
  - **[0.6.14]** (2026-06-16) — Memory distillation survives malformed model output, and stream-idle stalls retry instead of failing the turn.
164
- - **[0.6.13]** (2026-06-16) — `team` engine: concrete uncommitted-work reporting and stricter empty-run handling.
165
- - **[0.6.12]** (2026-06-16) — OKF-backed memory distillation — session learnings become structured concept files.
166
208
 
167
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
168
210
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.6.16",
3
+ "version": "0.6.18",
4
4
 
5
5
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
6
6
  "type": "module",
@@ -17,7 +17,9 @@ The core runtime loop, tool registry, session management, and state persistence
17
17
  | `hooks.ts` | Brief description of purpose |
18
18
  | `json.ts` | Brief description of purpose |
19
19
  | `loop.ts` | The primary execution loop orchestrating model calls and tool execution |
20
- | `memory.ts` | Brief description of purpose |
20
+ | `memory.ts` | OKF concept-bundle memory: session distill, query-aware budget injection, legacy MEMORY.md migration (`migrateLegacyMemory`) + `JEO_MEMORY_LEGACY` rollback toggle |
21
+ | `memory-okf.ts` | OKF v0.1 format layer: frontmatter parse/serialize, concept IDs, conformance validation |
22
+ | `memory-graph.ts` | Concept cross-link graph: build/expand (1-hop search), broken-link-tolerant lint, optional graphify detection |
21
23
  | `model-recency.ts` | Brief description of purpose |
22
24
  | `output-minimizer.ts` | Brief description of purpose |
23
25
  | `output-util.ts` | Brief description of purpose |
@@ -50,6 +50,129 @@ export async function loadMemory(cwd: string): Promise<string> {
50
50
  }
51
51
  }
52
52
 
53
+ /** A concept extracted from a legacy single-doc MEMORY.md during migration. */
54
+ export interface MigratedConcept {
55
+ type: string;
56
+ title: string;
57
+ description: string;
58
+ body: string;
59
+ }
60
+
61
+ /** Map a legacy `## heading` to a jeo concept type. Lenient keyword match —
62
+ * unknown headings default to RepoFact so nothing is dropped. */
63
+ function headingToType(heading: string): string {
64
+ const h = heading.toLowerCase();
65
+ if (/\bcommand/.test(h)) return "Command";
66
+ if (/gotcha|pitfall|caveat/.test(h)) return "Gotcha";
67
+ if (/pref/.test(h)) return "UserPreference";
68
+ if (/repo|fact/.test(h)) return "RepoFact";
69
+ return "RepoFact";
70
+ }
71
+
72
+ /** Parse a legacy 4-heading MEMORY.md into concepts: each `## heading` sets the
73
+ * type, each top-level bullet becomes a concept (`**title**: description` form
74
+ * recognized, indented continuation lines become the body). Lossless: a plain
75
+ * bullet keeps its whole text as the title. */
76
+ export function parseLegacyMemory(doc: string): MigratedConcept[] {
77
+ const concepts: MigratedConcept[] = [];
78
+ let currentType = "RepoFact";
79
+ let cur: MigratedConcept | null = null;
80
+ const flush = () => {
81
+ if (cur) {
82
+ cur.body = cur.body.replace(/\n+$/, "");
83
+ concepts.push(cur);
84
+ cur = null;
85
+ }
86
+ };
87
+ for (const line of doc.split("\n")) {
88
+ const heading = line.match(/^#{1,6}\s+(.*)$/);
89
+ if (heading) {
90
+ flush();
91
+ currentType = headingToType(heading[1]!.trim());
92
+ continue;
93
+ }
94
+ const bullet = line.match(/^\s*[-*]\s+(.*)$/);
95
+ if (bullet) {
96
+ flush();
97
+ const text = bullet[1]!.trim();
98
+ const bold = text.match(/^\*\*(.+?)\*\*\s*:?\s*(.*)$/);
99
+ cur = bold
100
+ ? { type: currentType, title: bold[1]!.trim(), description: bold[2]!.trim(), body: "" }
101
+ : { type: currentType, title: text, description: "", body: "" };
102
+ continue;
103
+ }
104
+ // Continuation line (typically 2-space indented) belongs to the open concept.
105
+ if (cur && line.trim() !== "") {
106
+ cur.body += (cur.body ? "\n" : "") + line.replace(/^ {2}/, "");
107
+ }
108
+ }
109
+ flush();
110
+ return concepts.filter(c => c.title);
111
+ }
112
+
113
+ /** Outcome of a one-shot legacy → OKF bundle migration. */
114
+ export interface MigrationResult {
115
+ migrated: boolean;
116
+ conceptCount: number;
117
+ /** Why nothing was migrated (already a bundle / no legacy doc / nothing parsed). */
118
+ skipped?: string;
119
+ /** Where the legacy MEMORY.md was preserved for rollback, if migrated. */
120
+ backupPath?: string;
121
+ }
122
+
123
+ /**
124
+ * Migrate a legacy single-doc `.jeo/memory/MEMORY.md` into the OKF concept bundle.
125
+ * One-shot and IDEMPOTENT: a bundle that already holds concept docs is left
126
+ * untouched. On success each legacy bullet becomes a type-partitioned concept,
127
+ * index.md/log.md are (re)built, and the legacy doc is renamed to `MEMORY.md.bak`
128
+ * so the active path is the bundle while a rollback copy survives.
129
+ */
130
+ export async function migrateLegacyMemory(cwd: string): Promise<MigrationResult> {
131
+ const bundleDir = path.join(cwd, ".jeo", "memory");
132
+ // Idempotent: an existing concept bundle wins — never double-migrate.
133
+ if ((await loadConcepts(cwd)).length > 0) {
134
+ return { migrated: false, conceptCount: 0, skipped: "bundle already has concepts" };
135
+ }
136
+ const doc = await loadMemory(cwd);
137
+ if (!doc) return { migrated: false, conceptCount: 0, skipped: "no legacy MEMORY.md to migrate" };
138
+ const parsed = parseLegacyMemory(doc);
139
+ if (parsed.length === 0) return { migrated: false, conceptCount: 0, skipped: "no concepts parsed from MEMORY.md" };
140
+
141
+ await fs.mkdir(bundleDir, { recursive: true });
142
+ const written: { title: string; type: string }[] = [];
143
+ const usedSlugs = new Set<string>();
144
+ for (const c of parsed) {
145
+ const dir = DIR_BY_TYPE[c.type] ?? "facts";
146
+ await fs.mkdir(path.join(bundleDir, dir), { recursive: true });
147
+ let slug = slugify(c.title);
148
+ let suffix = 1;
149
+ while (usedSlugs.has(`${dir}/${slug}`)) slug = `${slugify(c.title)}-${suffix++}`;
150
+ usedSlugs.add(`${dir}/${slug}`);
151
+ const frontmatter = {
152
+ type: c.type,
153
+ title: c.title,
154
+ description: c.description,
155
+ tags: [] as string[],
156
+ timestamp: new Date().toISOString(),
157
+ confidence: "high",
158
+ last_verified: new Date().toISOString().split("T")[0]!,
159
+ links: [] as string[],
160
+ };
161
+ const serialized = serializeConcept(frontmatter, c.body);
162
+ const fullPath = path.join(bundleDir, dir, `${slug}.md`);
163
+ const tmpPath = `${fullPath}.tmp-${process.pid}`;
164
+ await fs.writeFile(tmpPath, serialized, "utf-8");
165
+ await fs.rename(tmpPath, fullPath);
166
+ written.push({ title: c.title, type: c.type });
167
+ }
168
+ await rebuildIndex(bundleDir);
169
+ await updateLog(bundleDir, written);
170
+ // Preserve the legacy doc as a rollback backup, off the active read path.
171
+ const backupPath = `${memoryFilePath(cwd)}.bak`;
172
+ await fs.rename(memoryFilePath(cwd), backupPath).catch(() => {});
173
+ return { migrated: true, conceptCount: written.length, backupPath };
174
+ }
175
+
53
176
  /** Render a single index.md-style section: a `## header` followed by one bullet
54
177
  * per concept (`**title**: description`), with the concept body indented beneath. */
55
178
  function renderConceptSection(header: string, list: { title: string; description: string; body: string }[]): string {
@@ -240,12 +363,26 @@ function selectWithinBudget(concepts: Concept[], query: string | undefined, budg
240
363
  * reports: tag-breakout sequences are neutralized and the block is framed as DATA. */
241
364
  export async function memoryPromptSection(cwd: string, query?: string): Promise<string> {
242
365
  if (jeoEnv("NO_MEMORY") === "1") return "";
366
+ // Rollback toggle (Sprint 05): JEO_MEMORY_LEGACY=1 forces the legacy single-doc
367
+ // path, ignoring any concept bundle — reads MEMORY.md, or its migration backup.
368
+ if (jeoEnv("MEMORY_LEGACY") === "1") {
369
+ let memory = await loadMemory(cwd);
370
+ if (!memory) memory = (await fs.readFile(`${memoryFilePath(cwd)}.bak`, "utf-8").catch(() => "")).trim();
371
+ return memory ? frameMemory(memory) : "";
372
+ }
243
373
  // Prefer the OKF concept bundle (budget-selected); fall back to legacy MEMORY.md.
244
374
  const concepts = await loadConcepts(cwd);
245
375
  let memory = concepts.length > 0
246
376
  ? renderConcepts(selectWithinBudget(concepts, query, MEMORY_INJECT_MAX_CHARS))
247
377
  : await loadMemory(cwd);
248
378
  if (!memory) return "";
379
+ return frameMemory(memory);
380
+ }
381
+
382
+ /** Wrap distilled memory text in the hardened `<project_memory>` block: hard char
383
+ * cap, fence-tag neutralization, and DATA framing. Shared by the bundle path and
384
+ * the legacy/rollback path so neither can bypass the injection-hardening. */
385
+ function frameMemory(memory: string): string {
249
386
  // Backstop: legacy MEMORY.md is a single blob (not concept-selectable), and a
250
387
  // pathological single concept can exceed the budget — hard-cap either way.
251
388
  if (memory.length > MEMORY_INJECT_MAX_CHARS) {
package/src/cli/runner.ts CHANGED
@@ -146,6 +146,15 @@ export const COMMANDS: readonly CommandSpec[] = [
146
146
  return args => m.runMemoryDistillCommand(args);
147
147
  },
148
148
  },
149
+ {
150
+ name: "memory-migrate",
151
+ summary: "Migrate a legacy MEMORY.md into the OKF concept bundle (one-shot, idempotent).",
152
+ usage: "memory-migrate",
153
+ loader: async () => {
154
+ const m = await import("../commands/memory-migrate");
155
+ return args => m.runMemoryMigrateCommand(args);
156
+ },
157
+ },
149
158
  {
150
159
  name: "state",
151
160
  summary: "Read or update workflow state receipts under .jeo/state (gjc-state parity).",
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `jeo memory-migrate` — one-shot, idempotent migration of a legacy single-doc
3
+ * `.jeo/memory/MEMORY.md` into the OKF concept bundle (Sprint 05).
4
+ *
5
+ * Safe to re-run: if the bundle already holds concepts it is left untouched.
6
+ * The legacy doc is preserved as `MEMORY.md.bak` for rollback; set
7
+ * `JEO_MEMORY_LEGACY=1` to read that backup again if a rollback is needed.
8
+ */
9
+ import { migrateLegacyMemory } from "../agent/memory";
10
+
11
+ export async function runMemoryMigrateCommand(_args: string[]): Promise<void> {
12
+ const result = await migrateLegacyMemory(process.cwd());
13
+ if (result.migrated) {
14
+ console.log(`migrated ${result.conceptCount} concept(s) from MEMORY.md → OKF bundle (.jeo/memory/).`);
15
+ if (result.backupPath) console.log(`legacy doc preserved at ${result.backupPath} (rollback: JEO_MEMORY_LEGACY=1).`);
16
+ } else {
17
+ console.log(`nothing to migrate — ${result.skipped}.`);
18
+ }
19
+ }