jeo-code 0.6.16 → 0.6.17

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,12 @@ 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.17] - 2026-06-17
10
+ _Legacy MEMORY.md migrates losslessly into the OKF concept bundle, with a one-shot command and a rollback toggle._
11
+
12
+ ### Added
13
+ - **`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.
14
+
9
15
  ## [0.6.16] - 2026-06-17
10
16
  _OKF memory grows a concept cross-link graph: 1-hop search expansion, bundle lint, graphify-optional._
11
17
 
package/README.ja.md CHANGED
@@ -158,11 +158,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
158
158
  ## 変更履歴 (Changelog)
159
159
 
160
160
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
161
+ - **[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
162
  - **[0.6.16]** (2026-06-17) — OKF memory grows a concept cross-link graph: 1-hop search expansion, bundle lint, graphify-optional.
162
163
  - **[0.6.15]** (2026-06-17) — Query-aware OKF memory injection with budget-priority selection, and a truthful end-of-turn Todos receipt.
163
164
  - **[0.6.14]** (2026-06-16) — Memory distillation survives malformed model output, and stream-idle stalls retry instead of failing the turn.
164
165
  - **[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
166
 
167
167
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
168
168
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -158,11 +158,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
158
158
  ## 변경 이력 (Changelog)
159
159
 
160
160
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
161
+ - **[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
162
  - **[0.6.16]** (2026-06-17) — OKF memory grows a concept cross-link graph: 1-hop search expansion, bundle lint, graphify-optional.
162
163
  - **[0.6.15]** (2026-06-17) — Query-aware OKF memory injection with budget-priority selection, and a truthful end-of-turn Todos receipt.
163
164
  - **[0.6.14]** (2026-06-16) — Memory distillation survives malformed model output, and stream-idle stalls retry instead of failing the turn.
164
165
  - **[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
166
 
167
167
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
168
168
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -158,11 +158,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
158
158
  ## Changelog
159
159
 
160
160
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
161
+ - **[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
162
  - **[0.6.16]** (2026-06-17) — OKF memory grows a concept cross-link graph: 1-hop search expansion, bundle lint, graphify-optional.
162
163
  - **[0.6.15]** (2026-06-17) — Query-aware OKF memory injection with budget-priority selection, and a truthful end-of-turn Todos receipt.
163
164
  - **[0.6.14]** (2026-06-16) — Memory distillation survives malformed model output, and stream-idle stalls retry instead of failing the turn.
164
165
  - **[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
166
 
167
167
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
168
168
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -158,11 +158,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
158
158
  ## 更新日志 (Changelog)
159
159
 
160
160
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
161
+ - **[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
162
  - **[0.6.16]** (2026-06-17) — OKF memory grows a concept cross-link graph: 1-hop search expansion, bundle lint, graphify-optional.
162
163
  - **[0.6.15]** (2026-06-17) — Query-aware OKF memory injection with budget-priority selection, and a truthful end-of-turn Todos receipt.
163
164
  - **[0.6.14]** (2026-06-16) — Memory distillation survives malformed model output, and stream-idle stalls retry instead of failing the turn.
164
165
  - **[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
166
 
167
167
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
168
168
  <!-- 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.17",
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
+ }