@c-d-cc/reap 0.3.5 → 0.5.0
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/README.ja.md +97 -11
- package/README.ko.md +97 -11
- package/README.md +97 -11
- package/README.zh-CN.md +97 -11
- package/dist/cli.js +627 -217
- package/dist/templates/artifacts/merge/01-detect.md +18 -0
- package/dist/templates/artifacts/merge/02-mate.md +12 -0
- package/dist/templates/artifacts/merge/03-merge.md +15 -0
- package/dist/templates/artifacts/merge/04-sync.md +18 -0
- package/dist/templates/artifacts/merge/05-validation.md +11 -0
- package/dist/templates/artifacts/merge/06-completion.md +13 -0
- package/dist/templates/commands/reap.completion.md +1 -1
- package/dist/templates/commands/reap.evolve.md +7 -0
- package/dist/templates/commands/reap.merge.completion.md +20 -0
- package/dist/templates/commands/reap.merge.detect.md +20 -0
- package/dist/templates/commands/reap.merge.evolve.md +28 -0
- package/dist/templates/commands/reap.merge.mate.md +27 -0
- package/dist/templates/commands/reap.merge.md +47 -0
- package/dist/templates/commands/reap.merge.merge.md +22 -0
- package/dist/templates/commands/reap.merge.start.md +21 -0
- package/dist/templates/commands/reap.merge.sync.md +32 -0
- package/dist/templates/commands/reap.merge.validation.md +25 -0
- package/dist/templates/commands/reap.next.md +14 -1
- package/dist/templates/commands/reap.pull.md +51 -0
- package/dist/templates/commands/reap.push.md +18 -0
- package/dist/templates/commands/reap.start.md +4 -4
- package/dist/templates/hooks/genome-loader.cjs +34 -12
- package/dist/templates/hooks/opencode-session-start.js +2 -2
- package/dist/templates/hooks/reap-guide.md +1 -1
- package/dist/templates/hooks/session-start.cjs +2 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8999,6 +8999,18 @@ class ConfigManager {
|
|
|
8999
8999
|
const content = import_yaml.default.stringify(config);
|
|
9000
9000
|
await writeTextFile(paths.config, content);
|
|
9001
9001
|
}
|
|
9002
|
+
static resolveStrict(strict) {
|
|
9003
|
+
if (strict === undefined || strict === false) {
|
|
9004
|
+
return { edit: false, merge: false };
|
|
9005
|
+
}
|
|
9006
|
+
if (strict === true) {
|
|
9007
|
+
return { edit: true, merge: true };
|
|
9008
|
+
}
|
|
9009
|
+
return {
|
|
9010
|
+
edit: strict.edit ?? false,
|
|
9011
|
+
merge: strict.merge ?? false
|
|
9012
|
+
};
|
|
9013
|
+
}
|
|
9002
9014
|
}
|
|
9003
9015
|
|
|
9004
9016
|
// src/core/agents/claude-code.ts
|
|
@@ -9419,7 +9431,18 @@ var COMMAND_NAMES = [
|
|
|
9419
9431
|
"reap.status",
|
|
9420
9432
|
"reap.sync",
|
|
9421
9433
|
"reap.help",
|
|
9422
|
-
"reap.update"
|
|
9434
|
+
"reap.update",
|
|
9435
|
+
"reap.merge.start",
|
|
9436
|
+
"reap.merge.detect",
|
|
9437
|
+
"reap.merge.mate",
|
|
9438
|
+
"reap.merge.merge",
|
|
9439
|
+
"reap.merge.sync",
|
|
9440
|
+
"reap.merge.validation",
|
|
9441
|
+
"reap.merge.completion",
|
|
9442
|
+
"reap.merge.evolve",
|
|
9443
|
+
"reap.merge",
|
|
9444
|
+
"reap.pull",
|
|
9445
|
+
"reap.push"
|
|
9423
9446
|
];
|
|
9424
9447
|
async function initProject(projectRoot, projectName, entryMode, preset, onProgress) {
|
|
9425
9448
|
const log = onProgress ?? (() => {});
|
|
@@ -9468,6 +9491,15 @@ async function initProject(projectRoot, projectName, entryMode, preset, onProgre
|
|
|
9468
9491
|
const domainGuideSrc = join4(ReapPaths.packageGenomeDir, "domain/README.md");
|
|
9469
9492
|
const domainGuideDest = join4(ReapPaths.userReapTemplates, "domain-guide.md");
|
|
9470
9493
|
await writeTextFile(domainGuideDest, await readTextFileOrThrow(domainGuideSrc));
|
|
9494
|
+
const mergeTemplatesDir = join4(ReapPaths.userReapTemplates, "merge");
|
|
9495
|
+
await mkdir3(mergeTemplatesDir, { recursive: true });
|
|
9496
|
+
const mergeArtifactFiles = ["01-detect.md", "02-mate.md", "03-merge.md", "04-sync.md", "05-validation.md", "06-completion.md"];
|
|
9497
|
+
const mergeSourceDir = join4(ReapPaths.packageArtifactsDir, "merge");
|
|
9498
|
+
for (const file of mergeArtifactFiles) {
|
|
9499
|
+
const src = join4(mergeSourceDir, file);
|
|
9500
|
+
const dest = join4(mergeTemplatesDir, file);
|
|
9501
|
+
await writeTextFile(dest, await readTextFileOrThrow(src));
|
|
9502
|
+
}
|
|
9471
9503
|
log("Installing hook conditions...");
|
|
9472
9504
|
const conditionsSourceDir = join4(ReapPaths.packageTemplatesDir, "conditions");
|
|
9473
9505
|
const conditionsDestDir = join4(paths.hooks, "conditions");
|
|
@@ -9497,8 +9529,8 @@ async function initProject(projectRoot, projectName, entryMode, preset, onProgre
|
|
|
9497
9529
|
}
|
|
9498
9530
|
|
|
9499
9531
|
// src/cli/commands/update.ts
|
|
9500
|
-
import { readdir as
|
|
9501
|
-
import { join as
|
|
9532
|
+
import { readdir as readdir8, unlink as unlink3, rm as rm2, mkdir as mkdir6 } from "fs/promises";
|
|
9533
|
+
import { join as join9 } from "path";
|
|
9502
9534
|
|
|
9503
9535
|
// src/core/hooks.ts
|
|
9504
9536
|
async function migrateHooks(dryRun = false) {
|
|
@@ -9513,147 +9545,16 @@ async function migrateHooks(dryRun = false) {
|
|
|
9513
9545
|
return { results };
|
|
9514
9546
|
}
|
|
9515
9547
|
|
|
9516
|
-
// src/
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
throw new Error("Not a REAP project. Run 'reap init' first.");
|
|
9521
|
-
}
|
|
9522
|
-
const result = { updated: [], skipped: [], removed: [] };
|
|
9523
|
-
const config = await ConfigManager.read(paths);
|
|
9524
|
-
const adapters = await AgentRegistry.getActiveAdapters(config ?? undefined);
|
|
9525
|
-
const commandsDir = ReapPaths.packageCommandsDir;
|
|
9526
|
-
const commandFiles = await readdir4(commandsDir);
|
|
9527
|
-
for (const adapter of adapters) {
|
|
9528
|
-
const agentCmdDir = adapter.getCommandsDir();
|
|
9529
|
-
const label = `${adapter.displayName}`;
|
|
9530
|
-
for (const file of commandFiles) {
|
|
9531
|
-
if (!file.endsWith(".md"))
|
|
9532
|
-
continue;
|
|
9533
|
-
const src = await readTextFileOrThrow(join5(commandsDir, file));
|
|
9534
|
-
const dest = join5(agentCmdDir, file);
|
|
9535
|
-
const existingContent = await readTextFile(dest);
|
|
9536
|
-
if (existingContent !== null && existingContent === src) {
|
|
9537
|
-
result.skipped.push(`[${label}] commands/${file}`);
|
|
9538
|
-
} else {
|
|
9539
|
-
if (!dryRun) {
|
|
9540
|
-
await mkdir4(agentCmdDir, { recursive: true });
|
|
9541
|
-
await writeTextFile(dest, src);
|
|
9542
|
-
}
|
|
9543
|
-
result.updated.push(`[${label}] commands/${file}`);
|
|
9544
|
-
}
|
|
9545
|
-
}
|
|
9546
|
-
const validCommandFiles = new Set(commandFiles);
|
|
9547
|
-
if (!dryRun) {
|
|
9548
|
-
await adapter.removeStaleCommands(validCommandFiles);
|
|
9549
|
-
}
|
|
9550
|
-
}
|
|
9551
|
-
await mkdir4(ReapPaths.userReapTemplates, { recursive: true });
|
|
9552
|
-
const artifactFiles = ["01-objective.md", "02-planning.md", "03-implementation.md", "04-validation.md", "05-completion.md"];
|
|
9553
|
-
for (const file of artifactFiles) {
|
|
9554
|
-
const src = await readTextFileOrThrow(join5(ReapPaths.packageArtifactsDir, file));
|
|
9555
|
-
const dest = join5(ReapPaths.userReapTemplates, file);
|
|
9556
|
-
const existingContent = await readTextFile(dest);
|
|
9557
|
-
if (existingContent !== null && existingContent === src) {
|
|
9558
|
-
result.skipped.push(`~/.reap/templates/${file}`);
|
|
9559
|
-
} else {
|
|
9560
|
-
if (!dryRun)
|
|
9561
|
-
await writeTextFile(dest, src);
|
|
9562
|
-
result.updated.push(`~/.reap/templates/${file}`);
|
|
9563
|
-
}
|
|
9564
|
-
}
|
|
9565
|
-
const domainGuideSrc = await readTextFileOrThrow(join5(ReapPaths.packageGenomeDir, "domain/README.md"));
|
|
9566
|
-
const domainGuideDest = join5(ReapPaths.userReapTemplates, "domain-guide.md");
|
|
9567
|
-
const domainExistingContent = await readTextFile(domainGuideDest);
|
|
9568
|
-
if (domainExistingContent !== null && domainExistingContent === domainGuideSrc) {
|
|
9569
|
-
result.skipped.push(`~/.reap/templates/domain-guide.md`);
|
|
9570
|
-
} else {
|
|
9571
|
-
if (!dryRun)
|
|
9572
|
-
await writeTextFile(domainGuideDest, domainGuideSrc);
|
|
9573
|
-
result.updated.push(`~/.reap/templates/domain-guide.md`);
|
|
9574
|
-
}
|
|
9575
|
-
const migrations = await migrateHooks(dryRun);
|
|
9576
|
-
for (const m of migrations.results) {
|
|
9577
|
-
if (m.action === "migrated") {
|
|
9578
|
-
result.updated.push(`[${m.agent}] hooks (migrated)`);
|
|
9579
|
-
}
|
|
9580
|
-
}
|
|
9581
|
-
for (const adapter of adapters) {
|
|
9582
|
-
const hookResult = await adapter.syncSessionHook(dryRun);
|
|
9583
|
-
if (hookResult.action === "updated") {
|
|
9584
|
-
result.updated.push(`[${adapter.displayName}] session hook`);
|
|
9585
|
-
} else {
|
|
9586
|
-
result.skipped.push(`[${adapter.displayName}] session hook`);
|
|
9587
|
-
}
|
|
9588
|
-
}
|
|
9589
|
-
await migrateLegacyFiles(paths, dryRun, result);
|
|
9590
|
-
return result;
|
|
9591
|
-
}
|
|
9592
|
-
async function migrateLegacyFiles(paths, dryRun, result) {
|
|
9593
|
-
await removeDirIfExists(paths.legacyCommands, ".reap/commands/", dryRun, result);
|
|
9594
|
-
await removeDirIfExists(paths.legacyTemplates, ".reap/templates/", dryRun, result);
|
|
9595
|
-
try {
|
|
9596
|
-
const claudeCmdDir = paths.legacyClaudeCommands;
|
|
9597
|
-
const files = await readdir4(claudeCmdDir);
|
|
9598
|
-
for (const file of files) {
|
|
9599
|
-
if (file.startsWith("reap.") && file.endsWith(".md")) {
|
|
9600
|
-
if (!dryRun)
|
|
9601
|
-
await unlink3(join5(claudeCmdDir, file));
|
|
9602
|
-
result.removed.push(`.claude/commands/${file}`);
|
|
9603
|
-
}
|
|
9604
|
-
}
|
|
9605
|
-
} catch {}
|
|
9606
|
-
try {
|
|
9607
|
-
const legacyHooksJson = paths.legacyClaudeHooksJson;
|
|
9608
|
-
const fileContent = await readTextFile(legacyHooksJson);
|
|
9609
|
-
if (fileContent !== null) {
|
|
9610
|
-
const content = JSON.parse(fileContent);
|
|
9611
|
-
const sessionStart = content["SessionStart"];
|
|
9612
|
-
if (Array.isArray(sessionStart)) {
|
|
9613
|
-
const filtered = sessionStart.filter((entry) => {
|
|
9614
|
-
if (typeof entry !== "object" || entry === null)
|
|
9615
|
-
return true;
|
|
9616
|
-
const hooks = entry["hooks"];
|
|
9617
|
-
if (!Array.isArray(hooks))
|
|
9618
|
-
return true;
|
|
9619
|
-
return !hooks.some((h) => {
|
|
9620
|
-
if (typeof h !== "object" || h === null)
|
|
9621
|
-
return false;
|
|
9622
|
-
const cmd = h["command"];
|
|
9623
|
-
return typeof cmd === "string" && cmd.includes(".reap/hooks/");
|
|
9624
|
-
});
|
|
9625
|
-
});
|
|
9626
|
-
if (filtered.length !== sessionStart.length) {
|
|
9627
|
-
if (!dryRun) {
|
|
9628
|
-
if (filtered.length === 0 && Object.keys(content).length === 1) {
|
|
9629
|
-
await unlink3(legacyHooksJson);
|
|
9630
|
-
result.removed.push(`.claude/hooks.json (legacy)`);
|
|
9631
|
-
} else {
|
|
9632
|
-
content["SessionStart"] = filtered;
|
|
9633
|
-
await writeTextFile(legacyHooksJson, JSON.stringify(content, null, 2) + `
|
|
9634
|
-
`);
|
|
9635
|
-
result.removed.push(`.claude/hooks.json (legacy REAP hook entry)`);
|
|
9636
|
-
}
|
|
9637
|
-
}
|
|
9638
|
-
}
|
|
9639
|
-
}
|
|
9640
|
-
}
|
|
9641
|
-
} catch {}
|
|
9642
|
-
}
|
|
9643
|
-
async function removeDirIfExists(dirPath, label, dryRun, result) {
|
|
9644
|
-
try {
|
|
9645
|
-
const entries = await readdir4(dirPath);
|
|
9646
|
-
if (entries.length > 0 || true) {
|
|
9647
|
-
if (!dryRun)
|
|
9648
|
-
await rm(dirPath, { recursive: true });
|
|
9649
|
-
result.removed.push(label);
|
|
9650
|
-
}
|
|
9651
|
-
} catch {}
|
|
9652
|
-
}
|
|
9548
|
+
// src/core/migration.ts
|
|
9549
|
+
var import_yaml5 = __toESM(require_dist(), 1);
|
|
9550
|
+
import { readdir as readdir7, rename as rename2 } from "fs/promises";
|
|
9551
|
+
import { join as join8 } from "path";
|
|
9653
9552
|
|
|
9654
9553
|
// src/core/generation.ts
|
|
9655
|
-
var
|
|
9656
|
-
import {
|
|
9554
|
+
var import_yaml4 = __toESM(require_dist(), 1);
|
|
9555
|
+
import { createHash } from "crypto";
|
|
9556
|
+
import { hostname } from "os";
|
|
9557
|
+
import { readdir as readdir6, mkdir as mkdir4, rename } from "fs/promises";
|
|
9657
9558
|
import { join as join7 } from "path";
|
|
9658
9559
|
|
|
9659
9560
|
// src/types/index.ts
|
|
@@ -9708,14 +9609,35 @@ class LifeCycle {
|
|
|
9708
9609
|
}
|
|
9709
9610
|
|
|
9710
9611
|
// src/core/compression.ts
|
|
9711
|
-
|
|
9712
|
-
import {
|
|
9612
|
+
var import_yaml2 = __toESM(require_dist(), 1);
|
|
9613
|
+
import { readdir as readdir4, rm } from "fs/promises";
|
|
9614
|
+
import { join as join5 } from "path";
|
|
9713
9615
|
var LINEAGE_MAX_LINES = 5000;
|
|
9714
9616
|
var MIN_GENERATIONS_FOR_COMPRESSION = 5;
|
|
9715
9617
|
var LEVEL1_MAX_LINES = 40;
|
|
9716
9618
|
var LEVEL2_MAX_LINES = 60;
|
|
9717
9619
|
var LEVEL2_BATCH_SIZE = 5;
|
|
9718
9620
|
var RECENT_PROTECTED_COUNT = 3;
|
|
9621
|
+
function extractGenNum(name) {
|
|
9622
|
+
const match = name.match(/^gen-(\d{3})/);
|
|
9623
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
9624
|
+
}
|
|
9625
|
+
function parseFrontmatter(content) {
|
|
9626
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
9627
|
+
if (!match)
|
|
9628
|
+
return null;
|
|
9629
|
+
try {
|
|
9630
|
+
return import_yaml2.default.parse(match[1]);
|
|
9631
|
+
} catch {
|
|
9632
|
+
return null;
|
|
9633
|
+
}
|
|
9634
|
+
}
|
|
9635
|
+
function buildFrontmatter(meta) {
|
|
9636
|
+
return `---
|
|
9637
|
+
${import_yaml2.default.stringify(meta).trim()}
|
|
9638
|
+
---
|
|
9639
|
+
`;
|
|
9640
|
+
}
|
|
9719
9641
|
async function countLines(filePath) {
|
|
9720
9642
|
const content = await readTextFile(filePath);
|
|
9721
9643
|
if (content === null)
|
|
@@ -9726,9 +9648,9 @@ async function countLines(filePath) {
|
|
|
9726
9648
|
async function countDirLines(dirPath) {
|
|
9727
9649
|
let total = 0;
|
|
9728
9650
|
try {
|
|
9729
|
-
const entries = await
|
|
9651
|
+
const entries = await readdir4(dirPath, { withFileTypes: true });
|
|
9730
9652
|
for (const entry of entries) {
|
|
9731
|
-
const fullPath =
|
|
9653
|
+
const fullPath = join5(dirPath, entry.name);
|
|
9732
9654
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
9733
9655
|
total += await countLines(fullPath);
|
|
9734
9656
|
} else if (entry.isDirectory()) {
|
|
@@ -9738,33 +9660,113 @@ async function countDirLines(dirPath) {
|
|
|
9738
9660
|
} catch {}
|
|
9739
9661
|
return total;
|
|
9740
9662
|
}
|
|
9663
|
+
async function readDirMeta(dirPath) {
|
|
9664
|
+
const content = await readTextFile(join5(dirPath, "meta.yml"));
|
|
9665
|
+
if (content === null)
|
|
9666
|
+
return null;
|
|
9667
|
+
try {
|
|
9668
|
+
return import_yaml2.default.parse(content);
|
|
9669
|
+
} catch {
|
|
9670
|
+
return null;
|
|
9671
|
+
}
|
|
9672
|
+
}
|
|
9673
|
+
async function readFileMeta(filePath) {
|
|
9674
|
+
const content = await readTextFile(filePath);
|
|
9675
|
+
if (content === null)
|
|
9676
|
+
return null;
|
|
9677
|
+
return parseFrontmatter(content);
|
|
9678
|
+
}
|
|
9741
9679
|
async function scanLineage(paths) {
|
|
9742
9680
|
const entries = [];
|
|
9743
9681
|
try {
|
|
9744
|
-
const items = await
|
|
9682
|
+
const items = await readdir4(paths.lineage, { withFileTypes: true });
|
|
9745
9683
|
for (const item of items) {
|
|
9746
|
-
const fullPath =
|
|
9684
|
+
const fullPath = join5(paths.lineage, item.name);
|
|
9747
9685
|
if (item.isDirectory() && item.name.startsWith("gen-")) {
|
|
9748
|
-
const genNum =
|
|
9686
|
+
const genNum = extractGenNum(item.name);
|
|
9749
9687
|
const lines = await countDirLines(fullPath);
|
|
9750
|
-
|
|
9688
|
+
const meta = await readDirMeta(fullPath);
|
|
9689
|
+
const genId = meta?.id ?? item.name.match(/^gen-\d{3}(?:-[a-f0-9]{6})?/)?.[0] ?? item.name;
|
|
9690
|
+
entries.push({
|
|
9691
|
+
name: item.name,
|
|
9692
|
+
type: "dir",
|
|
9693
|
+
lines,
|
|
9694
|
+
genNum,
|
|
9695
|
+
completedAt: meta?.completedAt ?? "",
|
|
9696
|
+
genId
|
|
9697
|
+
});
|
|
9751
9698
|
} else if (item.isFile() && item.name.startsWith("gen-") && item.name.endsWith(".md")) {
|
|
9752
|
-
const genNum =
|
|
9699
|
+
const genNum = extractGenNum(item.name);
|
|
9753
9700
|
const lines = await countLines(fullPath);
|
|
9754
|
-
|
|
9701
|
+
const meta = await readFileMeta(fullPath);
|
|
9702
|
+
const genId = meta?.id ?? item.name.replace(".md", "").match(/^gen-\d{3}(?:-[a-f0-9]{6})?/)?.[0] ?? item.name;
|
|
9703
|
+
entries.push({
|
|
9704
|
+
name: item.name,
|
|
9705
|
+
type: "level1",
|
|
9706
|
+
lines,
|
|
9707
|
+
genNum,
|
|
9708
|
+
completedAt: meta?.completedAt ?? "",
|
|
9709
|
+
genId
|
|
9710
|
+
});
|
|
9755
9711
|
} else if (item.isFile() && item.name.startsWith("epoch-") && item.name.endsWith(".md")) {
|
|
9756
9712
|
const lines = await countLines(fullPath);
|
|
9757
|
-
entries.push({
|
|
9713
|
+
entries.push({
|
|
9714
|
+
name: item.name,
|
|
9715
|
+
type: "level2",
|
|
9716
|
+
lines,
|
|
9717
|
+
genNum: 0,
|
|
9718
|
+
completedAt: "",
|
|
9719
|
+
genId: ""
|
|
9720
|
+
});
|
|
9758
9721
|
}
|
|
9759
9722
|
}
|
|
9760
9723
|
} catch {}
|
|
9761
|
-
return entries.sort((a, b) =>
|
|
9724
|
+
return entries.sort((a, b) => {
|
|
9725
|
+
if (a.completedAt && b.completedAt) {
|
|
9726
|
+
return a.completedAt.localeCompare(b.completedAt);
|
|
9727
|
+
}
|
|
9728
|
+
return a.genNum - b.genNum;
|
|
9729
|
+
});
|
|
9730
|
+
}
|
|
9731
|
+
async function findLeafNodes(paths, entries) {
|
|
9732
|
+
const allIds = new Set;
|
|
9733
|
+
const referencedAsParent = new Set;
|
|
9734
|
+
for (const entry of entries) {
|
|
9735
|
+
if (entry.type === "level2")
|
|
9736
|
+
continue;
|
|
9737
|
+
let meta = null;
|
|
9738
|
+
const fullPath = join5(paths.lineage, entry.name);
|
|
9739
|
+
if (entry.type === "dir") {
|
|
9740
|
+
meta = await readDirMeta(fullPath);
|
|
9741
|
+
} else {
|
|
9742
|
+
meta = await readFileMeta(fullPath);
|
|
9743
|
+
}
|
|
9744
|
+
if (meta) {
|
|
9745
|
+
allIds.add(meta.id);
|
|
9746
|
+
for (const parent of meta.parents) {
|
|
9747
|
+
referencedAsParent.add(parent);
|
|
9748
|
+
}
|
|
9749
|
+
} else {
|
|
9750
|
+
allIds.add(entry.genId);
|
|
9751
|
+
}
|
|
9752
|
+
}
|
|
9753
|
+
const leaves = new Set;
|
|
9754
|
+
for (const id of allIds) {
|
|
9755
|
+
if (!referencedAsParent.has(id)) {
|
|
9756
|
+
leaves.add(id);
|
|
9757
|
+
}
|
|
9758
|
+
}
|
|
9759
|
+
return leaves;
|
|
9762
9760
|
}
|
|
9763
9761
|
async function compressLevel1(genDir, genName) {
|
|
9764
9762
|
const lines = [];
|
|
9763
|
+
const meta = await readDirMeta(genDir);
|
|
9764
|
+
if (meta) {
|
|
9765
|
+
lines.push(buildFrontmatter(meta));
|
|
9766
|
+
}
|
|
9765
9767
|
let goal = "", completionConditions = "";
|
|
9766
9768
|
{
|
|
9767
|
-
const objective = await readTextFile(
|
|
9769
|
+
const objective = await readTextFile(join5(genDir, "01-objective.md"));
|
|
9768
9770
|
if (objective) {
|
|
9769
9771
|
const goalMatch = objective.match(/## Goal\n([\s\S]*?)(?=\n##)/);
|
|
9770
9772
|
if (goalMatch)
|
|
@@ -9776,7 +9778,7 @@ async function compressLevel1(genDir, genName) {
|
|
|
9776
9778
|
}
|
|
9777
9779
|
let lessons = "", genomeChanges = "", nextBacklog = "";
|
|
9778
9780
|
{
|
|
9779
|
-
const completion = await readTextFile(
|
|
9781
|
+
const completion = await readTextFile(join5(genDir, "05-completion.md"));
|
|
9780
9782
|
if (completion) {
|
|
9781
9783
|
const lessonsMatch = completion.match(/### Lessons Learned\n([\s\S]*?)(?=\n###)/);
|
|
9782
9784
|
if (lessonsMatch)
|
|
@@ -9789,18 +9791,18 @@ async function compressLevel1(genDir, genName) {
|
|
|
9789
9791
|
nextBacklog = backlogMatch[1].trim();
|
|
9790
9792
|
}
|
|
9791
9793
|
}
|
|
9792
|
-
let
|
|
9794
|
+
let summaryText = "";
|
|
9793
9795
|
{
|
|
9794
|
-
const completion = await readTextFile(
|
|
9796
|
+
const completion = await readTextFile(join5(genDir, "05-completion.md"));
|
|
9795
9797
|
if (completion) {
|
|
9796
9798
|
const summaryMatch = completion.match(/## Summary\n([\s\S]*?)(?=\n##)/);
|
|
9797
9799
|
if (summaryMatch)
|
|
9798
|
-
|
|
9800
|
+
summaryText = summaryMatch[1].trim();
|
|
9799
9801
|
}
|
|
9800
9802
|
}
|
|
9801
9803
|
let validationResult = "";
|
|
9802
9804
|
{
|
|
9803
|
-
const validation = await readTextFile(
|
|
9805
|
+
const validation = await readTextFile(join5(genDir, "04-validation.md"));
|
|
9804
9806
|
if (validation) {
|
|
9805
9807
|
const resultMatch = validation.match(/## Result: (.+)/);
|
|
9806
9808
|
if (resultMatch)
|
|
@@ -9809,7 +9811,7 @@ async function compressLevel1(genDir, genName) {
|
|
|
9809
9811
|
}
|
|
9810
9812
|
let deferred = "";
|
|
9811
9813
|
{
|
|
9812
|
-
const impl = await readTextFile(
|
|
9814
|
+
const impl = await readTextFile(join5(genDir, "03-implementation.md"));
|
|
9813
9815
|
if (impl) {
|
|
9814
9816
|
const deferredMatch = impl.match(/## Deferred Tasks\n([\s\S]*?)(?=\n##)/);
|
|
9815
9817
|
if (deferredMatch) {
|
|
@@ -9820,10 +9822,10 @@ async function compressLevel1(genDir, genName) {
|
|
|
9820
9822
|
}
|
|
9821
9823
|
}
|
|
9822
9824
|
}
|
|
9823
|
-
const genId = genName.match(/^gen-\d
|
|
9825
|
+
const genId = genName.match(/^gen-\d{3}(?:-[a-f0-9]{6})?/)?.[0] ?? genName;
|
|
9824
9826
|
lines.push(`# ${genId}`);
|
|
9825
|
-
if (
|
|
9826
|
-
lines.push(
|
|
9827
|
+
if (summaryText) {
|
|
9828
|
+
lines.push(summaryText.replace(/^# .+\n/, "").trim());
|
|
9827
9829
|
}
|
|
9828
9830
|
lines.push("");
|
|
9829
9831
|
if (goal) {
|
|
@@ -9873,18 +9875,19 @@ async function compressLevel1(genDir, genName) {
|
|
|
9873
9875
|
}
|
|
9874
9876
|
async function compressLevel2(level1Files, epochNum) {
|
|
9875
9877
|
const lines = [];
|
|
9876
|
-
const genIds = level1Files.map((f) => f.name.replace(".md", "").match(/^gen-\d
|
|
9878
|
+
const genIds = level1Files.map((f) => f.name.replace(".md", "").match(/^gen-\d{3}(?:-[a-f0-9]{6})?/)?.[0] ?? f.name);
|
|
9877
9879
|
const first = genIds[0];
|
|
9878
9880
|
const last = genIds[genIds.length - 1];
|
|
9879
9881
|
lines.push(`# Epoch ${String(epochNum).padStart(3, "0")} (${first} ~ ${last})`);
|
|
9880
9882
|
lines.push("");
|
|
9881
9883
|
for (const file of level1Files) {
|
|
9882
9884
|
const content = await readTextFileOrThrow(file.path);
|
|
9883
|
-
const
|
|
9884
|
-
const
|
|
9885
|
-
const
|
|
9886
|
-
const
|
|
9887
|
-
const
|
|
9885
|
+
const bodyContent = content.replace(/^---\n[\s\S]*?\n---\n?/, "");
|
|
9886
|
+
const headerMatch = bodyContent.match(/^# (gen-\d{3}(?:-[a-f0-9]{6})?)/m);
|
|
9887
|
+
const goalMatch = bodyContent.match(/- Goal: (.+)/);
|
|
9888
|
+
const periodMatch = bodyContent.match(/- (?:Started|Period): (.+)/);
|
|
9889
|
+
const genomeMatch = bodyContent.match(/- Genome.*: (.+)/);
|
|
9890
|
+
const resultMatch = bodyContent.match(/## Result: (.+)/);
|
|
9888
9891
|
const genId = headerMatch?.[1] ?? "unknown";
|
|
9889
9892
|
const goal = goalMatch?.[1] ?? "";
|
|
9890
9893
|
const result2 = resultMatch?.[1] ?? "";
|
|
@@ -9895,7 +9898,7 @@ async function compressLevel2(level1Files, epochNum) {
|
|
|
9895
9898
|
lines.push(`- ${genomeMatch[0].trim()}`);
|
|
9896
9899
|
if (result2)
|
|
9897
9900
|
lines.push(`- Result: ${result2}`);
|
|
9898
|
-
const changeSection =
|
|
9901
|
+
const changeSection = bodyContent.match(/## Genome Changes\n([\s\S]*?)(?=\n##|$)/);
|
|
9899
9902
|
if (changeSection && !changeSection[1].match(/^\|\s*\|\s*\|\s*\|\s*\|$/)) {
|
|
9900
9903
|
lines.push(`- Genome Changes: ${changeSection[1].trim().split(`
|
|
9901
9904
|
`)[0]}`);
|
|
@@ -9924,21 +9927,23 @@ async function compressLineageIfNeeded(paths) {
|
|
|
9924
9927
|
if (totalLines <= LINEAGE_MAX_LINES) {
|
|
9925
9928
|
return result;
|
|
9926
9929
|
}
|
|
9927
|
-
const
|
|
9928
|
-
const
|
|
9929
|
-
|
|
9930
|
+
const leafNodes = await findLeafNodes(paths, entries);
|
|
9931
|
+
const allDirs = entries.filter((e) => e.type === "dir");
|
|
9932
|
+
const recentIds = new Set(allDirs.slice(Math.max(0, allDirs.length - RECENT_PROTECTED_COUNT)).map((e) => e.genId));
|
|
9933
|
+
const compressibleDirs = allDirs.filter((dir) => !recentIds.has(dir.genId) && !leafNodes.has(dir.genId));
|
|
9934
|
+
for (const dir of compressibleDirs) {
|
|
9930
9935
|
const currentTotal = await countDirLines(paths.lineage);
|
|
9931
9936
|
if (currentTotal <= LINEAGE_MAX_LINES)
|
|
9932
9937
|
break;
|
|
9933
|
-
const dirPath =
|
|
9938
|
+
const dirPath = join5(paths.lineage, dir.name);
|
|
9934
9939
|
const compressed = await compressLevel1(dirPath, dir.name);
|
|
9935
|
-
const genId = dir.name.match(/^gen-\d
|
|
9936
|
-
const outPath =
|
|
9940
|
+
const genId = dir.name.match(/^gen-\d{3}(?:-[a-f0-9]{6})?/)?.[0] ?? dir.name;
|
|
9941
|
+
const outPath = join5(paths.lineage, `${genId}.md`);
|
|
9937
9942
|
await writeTextFile(outPath, compressed);
|
|
9938
|
-
await
|
|
9943
|
+
await rm(dirPath, { recursive: true });
|
|
9939
9944
|
result.level1.push(genId);
|
|
9940
9945
|
}
|
|
9941
|
-
const level1s = (await scanLineage(paths)).filter((e) => e.type === "level1")
|
|
9946
|
+
const level1s = (await scanLineage(paths)).filter((e) => e.type === "level1");
|
|
9942
9947
|
if (level1s.length >= LEVEL2_BATCH_SIZE) {
|
|
9943
9948
|
const existingEpochs = (await scanLineage(paths)).filter((e) => e.type === "level2");
|
|
9944
9949
|
let epochNum = existingEpochs.length + 1;
|
|
@@ -9947,13 +9952,13 @@ async function compressLineageIfNeeded(paths) {
|
|
|
9947
9952
|
const batch = level1s.slice(i * LEVEL2_BATCH_SIZE, (i + 1) * LEVEL2_BATCH_SIZE);
|
|
9948
9953
|
const files = batch.map((e) => ({
|
|
9949
9954
|
name: e.name,
|
|
9950
|
-
path:
|
|
9955
|
+
path: join5(paths.lineage, e.name)
|
|
9951
9956
|
}));
|
|
9952
9957
|
const compressed = await compressLevel2(files, epochNum);
|
|
9953
|
-
const outPath =
|
|
9958
|
+
const outPath = join5(paths.lineage, `epoch-${String(epochNum).padStart(3, "0")}.md`);
|
|
9954
9959
|
await writeTextFile(outPath, compressed);
|
|
9955
9960
|
for (const file of files) {
|
|
9956
|
-
await
|
|
9961
|
+
await rm(file.path);
|
|
9957
9962
|
}
|
|
9958
9963
|
result.level2.push(`epoch-${String(epochNum).padStart(3, "0")}`);
|
|
9959
9964
|
epochNum++;
|
|
@@ -9962,7 +9967,119 @@ async function compressLineageIfNeeded(paths) {
|
|
|
9962
9967
|
return result;
|
|
9963
9968
|
}
|
|
9964
9969
|
|
|
9970
|
+
// src/core/lineage.ts
|
|
9971
|
+
var import_yaml3 = __toESM(require_dist(), 1);
|
|
9972
|
+
import { readdir as readdir5 } from "fs/promises";
|
|
9973
|
+
import { join as join6 } from "path";
|
|
9974
|
+
async function listCompleted(paths) {
|
|
9975
|
+
try {
|
|
9976
|
+
const entries = await readdir5(paths.lineage);
|
|
9977
|
+
return entries.filter((e) => e.startsWith("gen-")).sort();
|
|
9978
|
+
} catch {
|
|
9979
|
+
return [];
|
|
9980
|
+
}
|
|
9981
|
+
}
|
|
9982
|
+
async function readMeta(paths, lineageDirName) {
|
|
9983
|
+
const metaPath = join6(paths.lineage, lineageDirName, "meta.yml");
|
|
9984
|
+
const content = await readTextFile(metaPath);
|
|
9985
|
+
if (content === null)
|
|
9986
|
+
return null;
|
|
9987
|
+
return import_yaml3.default.parse(content);
|
|
9988
|
+
}
|
|
9989
|
+
async function listMeta(paths) {
|
|
9990
|
+
const metas = [];
|
|
9991
|
+
try {
|
|
9992
|
+
const entries = await readdir5(paths.lineage, { withFileTypes: true });
|
|
9993
|
+
for (const entry of entries) {
|
|
9994
|
+
if (entry.isDirectory() && entry.name.startsWith("gen-")) {
|
|
9995
|
+
const meta = await readMeta(paths, entry.name);
|
|
9996
|
+
if (meta)
|
|
9997
|
+
metas.push(meta);
|
|
9998
|
+
} else if (entry.isFile() && entry.name.startsWith("gen-") && entry.name.endsWith(".md")) {
|
|
9999
|
+
const content = await readTextFile(join6(paths.lineage, entry.name));
|
|
10000
|
+
if (content) {
|
|
10001
|
+
const meta = parseFrontmatter(content);
|
|
10002
|
+
if (meta)
|
|
10003
|
+
metas.push(meta);
|
|
10004
|
+
}
|
|
10005
|
+
}
|
|
10006
|
+
}
|
|
10007
|
+
} catch {}
|
|
10008
|
+
return metas;
|
|
10009
|
+
}
|
|
10010
|
+
async function nextSeq(paths, currentId) {
|
|
10011
|
+
const genDirs = await listCompleted(paths);
|
|
10012
|
+
if (genDirs.length === 0) {
|
|
10013
|
+
if (currentId) {
|
|
10014
|
+
return parseGenSeq(currentId) + 1;
|
|
10015
|
+
}
|
|
10016
|
+
return 1;
|
|
10017
|
+
}
|
|
10018
|
+
let maxSeq = 0;
|
|
10019
|
+
for (const dir of genDirs) {
|
|
10020
|
+
const seq = parseGenSeq(dir);
|
|
10021
|
+
if (seq > maxSeq)
|
|
10022
|
+
maxSeq = seq;
|
|
10023
|
+
}
|
|
10024
|
+
return maxSeq + 1;
|
|
10025
|
+
}
|
|
10026
|
+
async function resolveParents(paths) {
|
|
10027
|
+
const metas = await listMeta(paths);
|
|
10028
|
+
if (metas.length > 0) {
|
|
10029
|
+
const sorted = metas.sort((a, b) => new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime());
|
|
10030
|
+
return [sorted[0].id];
|
|
10031
|
+
}
|
|
10032
|
+
const dirs = await listCompleted(paths);
|
|
10033
|
+
if (dirs.length > 0) {
|
|
10034
|
+
const lastDir = dirs[dirs.length - 1];
|
|
10035
|
+
const legacyId = lastDir.match(/^(gen-\d{3}(?:-[a-f0-9]{6})?)/)?.[1];
|
|
10036
|
+
if (legacyId)
|
|
10037
|
+
return [legacyId];
|
|
10038
|
+
}
|
|
10039
|
+
return [];
|
|
10040
|
+
}
|
|
10041
|
+
|
|
9965
10042
|
// src/core/generation.ts
|
|
10043
|
+
function generateGenHash(parents, goal, genomeHash, machineId, startedAt) {
|
|
10044
|
+
const input = JSON.stringify({ parents, goal, genomeHash, machineId, startedAt });
|
|
10045
|
+
return createHash("sha256").update(input).digest("hex").slice(0, 6);
|
|
10046
|
+
}
|
|
10047
|
+
function getMachineId() {
|
|
10048
|
+
return hostname();
|
|
10049
|
+
}
|
|
10050
|
+
async function computeGenomeHash(genomePath) {
|
|
10051
|
+
const hash = createHash("sha256");
|
|
10052
|
+
try {
|
|
10053
|
+
const entries = (await readdir6(genomePath, { recursive: true, withFileTypes: true })).filter((e) => e.isFile()).sort((a, b) => {
|
|
10054
|
+
const pathA = join7(e2path(a), a.name);
|
|
10055
|
+
const pathB = join7(e2path(b), b.name);
|
|
10056
|
+
return pathA.localeCompare(pathB);
|
|
10057
|
+
});
|
|
10058
|
+
for (const entry of entries) {
|
|
10059
|
+
const filePath = join7(e2path(entry), entry.name);
|
|
10060
|
+
const content = await readTextFile(filePath);
|
|
10061
|
+
if (content !== null) {
|
|
10062
|
+
hash.update(filePath.replace(genomePath, ""));
|
|
10063
|
+
hash.update(content);
|
|
10064
|
+
}
|
|
10065
|
+
}
|
|
10066
|
+
} catch {}
|
|
10067
|
+
return hash.digest("hex").slice(0, 8);
|
|
10068
|
+
}
|
|
10069
|
+
function e2path(entry) {
|
|
10070
|
+
return entry.parentPath ?? entry.path ?? "";
|
|
10071
|
+
}
|
|
10072
|
+
function formatGenId(seq, hash) {
|
|
10073
|
+
return `gen-${String(seq).padStart(3, "0")}-${hash}`;
|
|
10074
|
+
}
|
|
10075
|
+
function parseGenSeq(id) {
|
|
10076
|
+
const match = id.match(/^gen-(\d{3})/);
|
|
10077
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
10078
|
+
}
|
|
10079
|
+
function isLegacyId(id) {
|
|
10080
|
+
return /^gen-\d{3}$/.test(id);
|
|
10081
|
+
}
|
|
10082
|
+
|
|
9966
10083
|
class GenerationManager {
|
|
9967
10084
|
paths;
|
|
9968
10085
|
constructor(paths) {
|
|
@@ -9972,20 +10089,33 @@ class GenerationManager {
|
|
|
9972
10089
|
const content = await readTextFile(this.paths.currentYml);
|
|
9973
10090
|
if (content === null || !content.trim())
|
|
9974
10091
|
return null;
|
|
9975
|
-
|
|
10092
|
+
const state = import_yaml4.default.parse(content);
|
|
10093
|
+
if (!state.type)
|
|
10094
|
+
state.type = "normal";
|
|
10095
|
+
if (!state.parents)
|
|
10096
|
+
state.parents = [];
|
|
10097
|
+
return state;
|
|
9976
10098
|
}
|
|
9977
10099
|
async create(goal, genomeVersion) {
|
|
9978
|
-
const
|
|
10100
|
+
const seq = await this.nextSeq();
|
|
9979
10101
|
const now = new Date().toISOString();
|
|
10102
|
+
const genomeHash = await computeGenomeHash(this.paths.genome);
|
|
10103
|
+
const machineId = getMachineId();
|
|
10104
|
+
const parents = await this.resolveParents();
|
|
10105
|
+
const hash = generateGenHash(parents, goal, genomeHash, machineId, now);
|
|
10106
|
+
const id = formatGenId(seq, hash);
|
|
9980
10107
|
const state = {
|
|
9981
10108
|
id,
|
|
9982
10109
|
goal,
|
|
9983
10110
|
stage: "objective",
|
|
9984
10111
|
genomeVersion,
|
|
9985
10112
|
startedAt: now,
|
|
9986
|
-
timeline: [{ stage: "objective", at: now }]
|
|
10113
|
+
timeline: [{ stage: "objective", at: now }],
|
|
10114
|
+
type: "normal",
|
|
10115
|
+
parents,
|
|
10116
|
+
genomeHash
|
|
9987
10117
|
};
|
|
9988
|
-
await writeTextFile(this.paths.currentYml,
|
|
10118
|
+
await writeTextFile(this.paths.currentYml, import_yaml4.default.stringify(state));
|
|
9989
10119
|
return state;
|
|
9990
10120
|
}
|
|
9991
10121
|
async advance() {
|
|
@@ -9999,7 +10129,7 @@ class GenerationManager {
|
|
|
9999
10129
|
if (!state.timeline)
|
|
10000
10130
|
state.timeline = [];
|
|
10001
10131
|
state.timeline.push({ stage: next, at: new Date().toISOString() });
|
|
10002
|
-
await writeTextFile(this.paths.currentYml,
|
|
10132
|
+
await writeTextFile(this.paths.currentYml, import_yaml4.default.stringify(state));
|
|
10003
10133
|
return state;
|
|
10004
10134
|
}
|
|
10005
10135
|
async complete() {
|
|
@@ -10008,10 +10138,22 @@ class GenerationManager {
|
|
|
10008
10138
|
throw new Error("No active generation");
|
|
10009
10139
|
if (state.stage !== "completion")
|
|
10010
10140
|
throw new Error("Generation must be in completion stage to complete");
|
|
10141
|
+
const now = new Date().toISOString();
|
|
10142
|
+
state.completedAt = now;
|
|
10011
10143
|
const goalSlug = state.goal.toLowerCase().replace(/[^a-z0-9가-힣]+/g, "-").replace(/^-|-$/g, "").slice(0, 30);
|
|
10012
10144
|
const genDirName = `${state.id}-${goalSlug}`;
|
|
10013
10145
|
const genDir = this.paths.generationDir(genDirName);
|
|
10014
|
-
await
|
|
10146
|
+
await mkdir4(genDir, { recursive: true });
|
|
10147
|
+
const meta = {
|
|
10148
|
+
id: state.id,
|
|
10149
|
+
type: state.type ?? "normal",
|
|
10150
|
+
parents: state.parents ?? [],
|
|
10151
|
+
goal: state.goal,
|
|
10152
|
+
genomeHash: state.genomeHash ?? "unknown",
|
|
10153
|
+
startedAt: state.startedAt,
|
|
10154
|
+
completedAt: now
|
|
10155
|
+
};
|
|
10156
|
+
await writeTextFile(join7(genDir, "meta.yml"), import_yaml4.default.stringify(meta));
|
|
10015
10157
|
const lifeEntries = await readdir6(this.paths.life);
|
|
10016
10158
|
for (const entry of lifeEntries) {
|
|
10017
10159
|
if (/^\d{2}-[a-z]+(?:-[a-z]+)*\.md$/.test(entry)) {
|
|
@@ -10019,7 +10161,7 @@ class GenerationManager {
|
|
|
10019
10161
|
}
|
|
10020
10162
|
}
|
|
10021
10163
|
const backlogDir = join7(genDir, "backlog");
|
|
10022
|
-
await
|
|
10164
|
+
await mkdir4(backlogDir, { recursive: true });
|
|
10023
10165
|
try {
|
|
10024
10166
|
const backlogEntries = await readdir6(this.paths.backlog);
|
|
10025
10167
|
for (const entry of backlogEntries) {
|
|
@@ -10030,7 +10172,7 @@ class GenerationManager {
|
|
|
10030
10172
|
const mutEntries = await readdir6(this.paths.mutations);
|
|
10031
10173
|
if (mutEntries.length > 0) {
|
|
10032
10174
|
const mutDir = join7(genDir, "mutations");
|
|
10033
|
-
await
|
|
10175
|
+
await mkdir4(mutDir, { recursive: true });
|
|
10034
10176
|
for (const entry of mutEntries) {
|
|
10035
10177
|
await rename(join7(this.paths.mutations, entry), join7(mutDir, entry));
|
|
10036
10178
|
}
|
|
@@ -10041,30 +10183,295 @@ class GenerationManager {
|
|
|
10041
10183
|
return compression;
|
|
10042
10184
|
}
|
|
10043
10185
|
async save(state) {
|
|
10044
|
-
await writeTextFile(this.paths.currentYml,
|
|
10186
|
+
await writeTextFile(this.paths.currentYml, import_yaml4.default.stringify(state));
|
|
10045
10187
|
}
|
|
10046
10188
|
async listCompleted() {
|
|
10189
|
+
return listCompleted(this.paths);
|
|
10190
|
+
}
|
|
10191
|
+
async readMeta(lineageDirName) {
|
|
10192
|
+
return readMeta(this.paths, lineageDirName);
|
|
10193
|
+
}
|
|
10194
|
+
async listMeta() {
|
|
10195
|
+
return listMeta(this.paths);
|
|
10196
|
+
}
|
|
10197
|
+
async resolveParents() {
|
|
10198
|
+
return resolveParents(this.paths);
|
|
10199
|
+
}
|
|
10200
|
+
async nextSeq() {
|
|
10201
|
+
const current = await this.current();
|
|
10202
|
+
return nextSeq(this.paths, current?.id);
|
|
10203
|
+
}
|
|
10204
|
+
async nextGenId() {
|
|
10205
|
+
const seq = await this.nextSeq();
|
|
10206
|
+
return `gen-${String(seq).padStart(3, "0")}`;
|
|
10207
|
+
}
|
|
10208
|
+
}
|
|
10209
|
+
|
|
10210
|
+
// src/core/migration.ts
|
|
10211
|
+
async function needsMigration(paths) {
|
|
10212
|
+
try {
|
|
10213
|
+
const entries = await readdir7(paths.lineage, { withFileTypes: true });
|
|
10214
|
+
for (const entry of entries) {
|
|
10215
|
+
if (!entry.isDirectory() || !entry.name.startsWith("gen-"))
|
|
10216
|
+
continue;
|
|
10217
|
+
const metaPath = join8(paths.lineage, entry.name, "meta.yml");
|
|
10218
|
+
const content = await readTextFile(metaPath);
|
|
10219
|
+
if (content === null)
|
|
10220
|
+
return true;
|
|
10221
|
+
}
|
|
10222
|
+
} catch {
|
|
10223
|
+
return false;
|
|
10224
|
+
}
|
|
10225
|
+
return false;
|
|
10226
|
+
}
|
|
10227
|
+
async function migrateLineage(paths) {
|
|
10228
|
+
const result = { migrated: [], skipped: [], errors: [] };
|
|
10229
|
+
let entries;
|
|
10230
|
+
try {
|
|
10231
|
+
const dirEntries = await readdir7(paths.lineage, { withFileTypes: true });
|
|
10232
|
+
entries = dirEntries.filter((e) => e.isDirectory() && e.name.startsWith("gen-")).map((e) => e.name).sort();
|
|
10233
|
+
} catch {
|
|
10234
|
+
return result;
|
|
10235
|
+
}
|
|
10236
|
+
const plan = [];
|
|
10237
|
+
for (const dirName of entries) {
|
|
10238
|
+
const metaPath = join8(paths.lineage, dirName, "meta.yml");
|
|
10239
|
+
const metaContent = await readTextFile(metaPath);
|
|
10240
|
+
const seq = parseGenSeq(dirName);
|
|
10241
|
+
let goal = "";
|
|
10242
|
+
const objContent = await readTextFile(join8(paths.lineage, dirName, "01-objective.md"));
|
|
10243
|
+
if (objContent) {
|
|
10244
|
+
const goalMatch = objContent.match(/## Goal\n+([\s\S]*?)(?=\n##)/);
|
|
10245
|
+
if (goalMatch)
|
|
10246
|
+
goal = goalMatch[1].trim();
|
|
10247
|
+
}
|
|
10248
|
+
if (!goal) {
|
|
10249
|
+
const slugMatch = dirName.match(/^gen-\d{3}(?:-[a-f0-9]{6})?-(.+)$/);
|
|
10250
|
+
goal = slugMatch ? slugMatch[1].replace(/-/g, " ") : `Generation ${seq}`;
|
|
10251
|
+
}
|
|
10252
|
+
plan.push({ dirName, seq, goal, hasMeta: metaContent !== null });
|
|
10253
|
+
}
|
|
10254
|
+
let prevId = null;
|
|
10255
|
+
for (const entry of plan) {
|
|
10256
|
+
if (entry.hasMeta) {
|
|
10257
|
+
const metaContent = await readTextFile(join8(paths.lineage, entry.dirName, "meta.yml"));
|
|
10258
|
+
if (metaContent) {
|
|
10259
|
+
const meta = import_yaml5.default.parse(metaContent);
|
|
10260
|
+
prevId = meta.id;
|
|
10261
|
+
}
|
|
10262
|
+
result.skipped.push(entry.dirName);
|
|
10263
|
+
continue;
|
|
10264
|
+
}
|
|
10047
10265
|
try {
|
|
10048
|
-
const
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10266
|
+
const parents = prevId ? [prevId] : [];
|
|
10267
|
+
const hash = generateGenHash(parents, entry.goal, "legacy", "migration", `legacy-${entry.seq}`);
|
|
10268
|
+
const newId = formatGenId(entry.seq, hash);
|
|
10269
|
+
const meta = {
|
|
10270
|
+
id: newId,
|
|
10271
|
+
type: "normal",
|
|
10272
|
+
parents,
|
|
10273
|
+
goal: entry.goal,
|
|
10274
|
+
genomeHash: "legacy",
|
|
10275
|
+
startedAt: `legacy-${entry.seq}`,
|
|
10276
|
+
completedAt: `legacy-${entry.seq}`
|
|
10277
|
+
};
|
|
10278
|
+
await writeTextFile(join8(paths.lineage, entry.dirName, "meta.yml"), import_yaml5.default.stringify(meta));
|
|
10279
|
+
const oldSlug = entry.dirName.replace(/^gen-\d{3}/, "");
|
|
10280
|
+
const newDirName = `${newId}${oldSlug}`;
|
|
10281
|
+
if (newDirName !== entry.dirName) {
|
|
10282
|
+
await rename2(join8(paths.lineage, entry.dirName), join8(paths.lineage, newDirName));
|
|
10283
|
+
}
|
|
10284
|
+
prevId = newId;
|
|
10285
|
+
result.migrated.push(`${entry.dirName} → ${newDirName}`);
|
|
10286
|
+
} catch (err) {
|
|
10287
|
+
result.errors.push(`${entry.dirName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
10052
10288
|
}
|
|
10053
10289
|
}
|
|
10054
|
-
|
|
10055
|
-
const
|
|
10056
|
-
if (
|
|
10057
|
-
const
|
|
10058
|
-
if (
|
|
10059
|
-
const
|
|
10060
|
-
|
|
10290
|
+
try {
|
|
10291
|
+
const currentContent = await readTextFile(paths.currentYml);
|
|
10292
|
+
if (currentContent && currentContent.trim()) {
|
|
10293
|
+
const state = import_yaml5.default.parse(currentContent);
|
|
10294
|
+
if (isLegacyId(state.id)) {
|
|
10295
|
+
const parents = prevId ? [prevId] : [];
|
|
10296
|
+
const genomeHash = "legacy";
|
|
10297
|
+
const hash = generateGenHash(parents, state.goal, genomeHash, "migration", state.startedAt);
|
|
10298
|
+
state.id = formatGenId(parseGenSeq(state.id), hash);
|
|
10299
|
+
state.type = state.type ?? "normal";
|
|
10300
|
+
state.parents = parents;
|
|
10301
|
+
state.genomeHash = genomeHash;
|
|
10302
|
+
await writeTextFile(paths.currentYml, import_yaml5.default.stringify(state));
|
|
10303
|
+
result.migrated.push(`current.yml: ${state.id}`);
|
|
10061
10304
|
}
|
|
10062
|
-
return "gen-001";
|
|
10063
10305
|
}
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10306
|
+
} catch {}
|
|
10307
|
+
return result;
|
|
10308
|
+
}
|
|
10309
|
+
|
|
10310
|
+
// src/cli/commands/update.ts
|
|
10311
|
+
async function updateProject(projectRoot, dryRun = false) {
|
|
10312
|
+
const paths = new ReapPaths(projectRoot);
|
|
10313
|
+
if (!await paths.isReapProject()) {
|
|
10314
|
+
throw new Error("Not a REAP project. Run 'reap init' first.");
|
|
10067
10315
|
}
|
|
10316
|
+
const result = { updated: [], skipped: [], removed: [] };
|
|
10317
|
+
const config = await ConfigManager.read(paths);
|
|
10318
|
+
const adapters = await AgentRegistry.getActiveAdapters(config ?? undefined);
|
|
10319
|
+
const commandsDir = ReapPaths.packageCommandsDir;
|
|
10320
|
+
const commandFiles = await readdir8(commandsDir);
|
|
10321
|
+
for (const adapter of adapters) {
|
|
10322
|
+
const agentCmdDir = adapter.getCommandsDir();
|
|
10323
|
+
const label = `${adapter.displayName}`;
|
|
10324
|
+
for (const file of commandFiles) {
|
|
10325
|
+
if (!file.endsWith(".md"))
|
|
10326
|
+
continue;
|
|
10327
|
+
const src = await readTextFileOrThrow(join9(commandsDir, file));
|
|
10328
|
+
const dest = join9(agentCmdDir, file);
|
|
10329
|
+
const existingContent = await readTextFile(dest);
|
|
10330
|
+
if (existingContent !== null && existingContent === src) {
|
|
10331
|
+
result.skipped.push(`[${label}] commands/${file}`);
|
|
10332
|
+
} else {
|
|
10333
|
+
if (!dryRun) {
|
|
10334
|
+
await mkdir6(agentCmdDir, { recursive: true });
|
|
10335
|
+
await writeTextFile(dest, src);
|
|
10336
|
+
}
|
|
10337
|
+
result.updated.push(`[${label}] commands/${file}`);
|
|
10338
|
+
}
|
|
10339
|
+
}
|
|
10340
|
+
const validCommandFiles = new Set(commandFiles);
|
|
10341
|
+
if (!dryRun) {
|
|
10342
|
+
await adapter.removeStaleCommands(validCommandFiles);
|
|
10343
|
+
}
|
|
10344
|
+
}
|
|
10345
|
+
await mkdir6(ReapPaths.userReapTemplates, { recursive: true });
|
|
10346
|
+
const artifactFiles = ["01-objective.md", "02-planning.md", "03-implementation.md", "04-validation.md", "05-completion.md"];
|
|
10347
|
+
for (const file of artifactFiles) {
|
|
10348
|
+
const src = await readTextFileOrThrow(join9(ReapPaths.packageArtifactsDir, file));
|
|
10349
|
+
const dest = join9(ReapPaths.userReapTemplates, file);
|
|
10350
|
+
const existingContent = await readTextFile(dest);
|
|
10351
|
+
if (existingContent !== null && existingContent === src) {
|
|
10352
|
+
result.skipped.push(`~/.reap/templates/${file}`);
|
|
10353
|
+
} else {
|
|
10354
|
+
if (!dryRun)
|
|
10355
|
+
await writeTextFile(dest, src);
|
|
10356
|
+
result.updated.push(`~/.reap/templates/${file}`);
|
|
10357
|
+
}
|
|
10358
|
+
}
|
|
10359
|
+
const domainGuideSrc = await readTextFileOrThrow(join9(ReapPaths.packageGenomeDir, "domain/README.md"));
|
|
10360
|
+
const domainGuideDest = join9(ReapPaths.userReapTemplates, "domain-guide.md");
|
|
10361
|
+
const domainExistingContent = await readTextFile(domainGuideDest);
|
|
10362
|
+
if (domainExistingContent !== null && domainExistingContent === domainGuideSrc) {
|
|
10363
|
+
result.skipped.push(`~/.reap/templates/domain-guide.md`);
|
|
10364
|
+
} else {
|
|
10365
|
+
if (!dryRun)
|
|
10366
|
+
await writeTextFile(domainGuideDest, domainGuideSrc);
|
|
10367
|
+
result.updated.push(`~/.reap/templates/domain-guide.md`);
|
|
10368
|
+
}
|
|
10369
|
+
const mergeTemplatesDir = join9(ReapPaths.userReapTemplates, "merge");
|
|
10370
|
+
await mkdir6(mergeTemplatesDir, { recursive: true });
|
|
10371
|
+
const mergeArtifactFiles = ["01-detect.md", "02-mate.md", "03-merge.md", "04-sync.md", "05-validation.md", "06-completion.md"];
|
|
10372
|
+
const mergeSourceDir = join9(ReapPaths.packageArtifactsDir, "merge");
|
|
10373
|
+
for (const file of mergeArtifactFiles) {
|
|
10374
|
+
const src = await readTextFileOrThrow(join9(mergeSourceDir, file));
|
|
10375
|
+
const dest = join9(mergeTemplatesDir, file);
|
|
10376
|
+
const existing = await readTextFile(dest);
|
|
10377
|
+
if (existing !== null && existing === src) {
|
|
10378
|
+
result.skipped.push(`~/.reap/templates/merge/${file}`);
|
|
10379
|
+
} else {
|
|
10380
|
+
if (!dryRun)
|
|
10381
|
+
await writeTextFile(dest, src);
|
|
10382
|
+
result.updated.push(`~/.reap/templates/merge/${file}`);
|
|
10383
|
+
}
|
|
10384
|
+
}
|
|
10385
|
+
const migrations = await migrateHooks(dryRun);
|
|
10386
|
+
for (const m of migrations.results) {
|
|
10387
|
+
if (m.action === "migrated") {
|
|
10388
|
+
result.updated.push(`[${m.agent}] hooks (migrated)`);
|
|
10389
|
+
}
|
|
10390
|
+
}
|
|
10391
|
+
for (const adapter of adapters) {
|
|
10392
|
+
const hookResult = await adapter.syncSessionHook(dryRun);
|
|
10393
|
+
if (hookResult.action === "updated") {
|
|
10394
|
+
result.updated.push(`[${adapter.displayName}] session hook`);
|
|
10395
|
+
} else {
|
|
10396
|
+
result.skipped.push(`[${adapter.displayName}] session hook`);
|
|
10397
|
+
}
|
|
10398
|
+
}
|
|
10399
|
+
await migrateLegacyFiles(paths, dryRun, result);
|
|
10400
|
+
if (await needsMigration(paths)) {
|
|
10401
|
+
if (!dryRun) {
|
|
10402
|
+
const migrationResult = await migrateLineage(paths);
|
|
10403
|
+
for (const m of migrationResult.migrated) {
|
|
10404
|
+
result.updated.push(`[lineage] ${m}`);
|
|
10405
|
+
}
|
|
10406
|
+
for (const e of migrationResult.errors) {
|
|
10407
|
+
result.removed.push(`[lineage error] ${e}`);
|
|
10408
|
+
}
|
|
10409
|
+
} else {
|
|
10410
|
+
result.updated.push("[lineage] DAG migration pending (dry-run)");
|
|
10411
|
+
}
|
|
10412
|
+
}
|
|
10413
|
+
return result;
|
|
10414
|
+
}
|
|
10415
|
+
async function migrateLegacyFiles(paths, dryRun, result) {
|
|
10416
|
+
await removeDirIfExists(paths.legacyCommands, ".reap/commands/", dryRun, result);
|
|
10417
|
+
await removeDirIfExists(paths.legacyTemplates, ".reap/templates/", dryRun, result);
|
|
10418
|
+
try {
|
|
10419
|
+
const claudeCmdDir = paths.legacyClaudeCommands;
|
|
10420
|
+
const files = await readdir8(claudeCmdDir);
|
|
10421
|
+
for (const file of files) {
|
|
10422
|
+
if (file.startsWith("reap.") && file.endsWith(".md")) {
|
|
10423
|
+
if (!dryRun)
|
|
10424
|
+
await unlink3(join9(claudeCmdDir, file));
|
|
10425
|
+
result.removed.push(`.claude/commands/${file}`);
|
|
10426
|
+
}
|
|
10427
|
+
}
|
|
10428
|
+
} catch {}
|
|
10429
|
+
try {
|
|
10430
|
+
const legacyHooksJson = paths.legacyClaudeHooksJson;
|
|
10431
|
+
const fileContent = await readTextFile(legacyHooksJson);
|
|
10432
|
+
if (fileContent !== null) {
|
|
10433
|
+
const content = JSON.parse(fileContent);
|
|
10434
|
+
const sessionStart = content["SessionStart"];
|
|
10435
|
+
if (Array.isArray(sessionStart)) {
|
|
10436
|
+
const filtered = sessionStart.filter((entry) => {
|
|
10437
|
+
if (typeof entry !== "object" || entry === null)
|
|
10438
|
+
return true;
|
|
10439
|
+
const hooks = entry["hooks"];
|
|
10440
|
+
if (!Array.isArray(hooks))
|
|
10441
|
+
return true;
|
|
10442
|
+
return !hooks.some((h) => {
|
|
10443
|
+
if (typeof h !== "object" || h === null)
|
|
10444
|
+
return false;
|
|
10445
|
+
const cmd = h["command"];
|
|
10446
|
+
return typeof cmd === "string" && cmd.includes(".reap/hooks/");
|
|
10447
|
+
});
|
|
10448
|
+
});
|
|
10449
|
+
if (filtered.length !== sessionStart.length) {
|
|
10450
|
+
if (!dryRun) {
|
|
10451
|
+
if (filtered.length === 0 && Object.keys(content).length === 1) {
|
|
10452
|
+
await unlink3(legacyHooksJson);
|
|
10453
|
+
result.removed.push(`.claude/hooks.json (legacy)`);
|
|
10454
|
+
} else {
|
|
10455
|
+
content["SessionStart"] = filtered;
|
|
10456
|
+
await writeTextFile(legacyHooksJson, JSON.stringify(content, null, 2) + `
|
|
10457
|
+
`);
|
|
10458
|
+
result.removed.push(`.claude/hooks.json (legacy REAP hook entry)`);
|
|
10459
|
+
}
|
|
10460
|
+
}
|
|
10461
|
+
}
|
|
10462
|
+
}
|
|
10463
|
+
}
|
|
10464
|
+
} catch {}
|
|
10465
|
+
}
|
|
10466
|
+
async function removeDirIfExists(dirPath, label, dryRun, result) {
|
|
10467
|
+
try {
|
|
10468
|
+
const entries = await readdir8(dirPath);
|
|
10469
|
+
if (entries.length > 0 || true) {
|
|
10470
|
+
if (!dryRun)
|
|
10471
|
+
await rm2(dirPath, { recursive: true });
|
|
10472
|
+
result.removed.push(label);
|
|
10473
|
+
}
|
|
10474
|
+
} catch {}
|
|
10068
10475
|
}
|
|
10069
10476
|
|
|
10070
10477
|
// src/cli/commands/status.ts
|
|
@@ -10082,15 +10489,18 @@ async function getStatus(projectRoot) {
|
|
|
10082
10489
|
goal: current.goal,
|
|
10083
10490
|
stage: current.stage,
|
|
10084
10491
|
genomeVersion: current.genomeVersion,
|
|
10085
|
-
startedAt: current.startedAt
|
|
10492
|
+
startedAt: current.startedAt,
|
|
10493
|
+
type: current.type,
|
|
10494
|
+
parents: current.parents,
|
|
10495
|
+
genomeHash: current.genomeHash
|
|
10086
10496
|
} : null,
|
|
10087
10497
|
totalGenerations: completedGens.length
|
|
10088
10498
|
};
|
|
10089
10499
|
}
|
|
10090
10500
|
|
|
10091
10501
|
// src/cli/commands/fix.ts
|
|
10092
|
-
var
|
|
10093
|
-
import { mkdir as
|
|
10502
|
+
var import_yaml6 = __toESM(require_dist(), 1);
|
|
10503
|
+
import { mkdir as mkdir7, stat as stat2 } from "fs/promises";
|
|
10094
10504
|
async function dirExists(path) {
|
|
10095
10505
|
try {
|
|
10096
10506
|
const s = await stat2(path);
|
|
@@ -10113,7 +10523,7 @@ async function fixProject(projectRoot) {
|
|
|
10113
10523
|
];
|
|
10114
10524
|
for (const dir of requiredDirs) {
|
|
10115
10525
|
if (!await dirExists(dir.path)) {
|
|
10116
|
-
await
|
|
10526
|
+
await mkdir7(dir.path, { recursive: true });
|
|
10117
10527
|
fixed.push(`Recreated missing directory: ${dir.name}/`);
|
|
10118
10528
|
}
|
|
10119
10529
|
}
|
|
@@ -10124,7 +10534,7 @@ async function fixProject(projectRoot) {
|
|
|
10124
10534
|
if (currentContent !== null) {
|
|
10125
10535
|
if (currentContent.trim()) {
|
|
10126
10536
|
try {
|
|
10127
|
-
const state =
|
|
10537
|
+
const state = import_yaml6.default.parse(currentContent);
|
|
10128
10538
|
if (!state.stage || !LifeCycle.isValid(state.stage)) {
|
|
10129
10539
|
issues.push(`Invalid stage "${state.stage}" in current.yml. Valid stages: ${LifeCycle.stages().join(", ")}. Manual correction required.`);
|
|
10130
10540
|
}
|
|
@@ -10133,7 +10543,7 @@ async function fixProject(projectRoot) {
|
|
|
10133
10543
|
if (!state.goal)
|
|
10134
10544
|
issues.push("current.yml is missing 'goal' field. Manual correction required.");
|
|
10135
10545
|
if (!await dirExists(paths.backlog)) {
|
|
10136
|
-
await
|
|
10546
|
+
await mkdir7(paths.backlog, { recursive: true });
|
|
10137
10547
|
fixed.push("Recreated missing backlog/ directory for active generation");
|
|
10138
10548
|
}
|
|
10139
10549
|
} catch {
|
|
@@ -10146,8 +10556,8 @@ async function fixProject(projectRoot) {
|
|
|
10146
10556
|
}
|
|
10147
10557
|
|
|
10148
10558
|
// src/cli/index.ts
|
|
10149
|
-
import { join as
|
|
10150
|
-
program.name("reap").description("REAP — Recursive Evolutionary Autonomous Pipeline").version("0.
|
|
10559
|
+
import { join as join10 } from "path";
|
|
10560
|
+
program.name("reap").description("REAP — Recursive Evolutionary Autonomous Pipeline").version("0.5.0");
|
|
10151
10561
|
program.command("init").description("Initialize a new REAP project (Genesis)").argument("[project-name]", "Project name (defaults to current directory name)").option("-m, --mode <mode>", "Entry mode: greenfield, migration, adoption", "greenfield").option("-p, --preset <preset>", "Bootstrap with a genome preset (e.g., bun-hono-react)").action(async (projectName, options) => {
|
|
10152
10562
|
try {
|
|
10153
10563
|
const cwd = process.cwd();
|
|
@@ -10269,10 +10679,10 @@ program.command("help").description("Show REAP commands, slash commands, and wor
|
|
|
10269
10679
|
if (l === "korean" || l === "ko")
|
|
10270
10680
|
lang = "ko";
|
|
10271
10681
|
}
|
|
10272
|
-
const helpDir =
|
|
10273
|
-
let helpText = await readTextFile(
|
|
10682
|
+
const helpDir = join10(ReapPaths.packageTemplatesDir, "help");
|
|
10683
|
+
let helpText = await readTextFile(join10(helpDir, `${lang}.txt`));
|
|
10274
10684
|
if (!helpText)
|
|
10275
|
-
helpText = await readTextFile(
|
|
10685
|
+
helpText = await readTextFile(join10(helpDir, "en.txt"));
|
|
10276
10686
|
if (!helpText) {
|
|
10277
10687
|
console.log("Help file not found. Run 'reap update' to install templates.");
|
|
10278
10688
|
return;
|