@c-d-cc/reap 0.14.0 → 0.15.1

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/dist/cli.js CHANGED
@@ -9027,7 +9027,8 @@ class ConfigManager {
9027
9027
  strict: false,
9028
9028
  autoUpdate: true,
9029
9029
  autoSubagent: true,
9030
- autoIssueReport: false
9030
+ autoIssueReport: false,
9031
+ lastSyncedGeneration: ""
9031
9032
  };
9032
9033
  for (const [key, defaultValue] of Object.entries(defaults)) {
9033
9034
  if (config[key] === undefined) {
@@ -9035,6 +9036,14 @@ class ConfigManager {
9035
9036
  added.push(key);
9036
9037
  }
9037
9038
  }
9039
+ if (config.lastSyncedCommit !== undefined) {
9040
+ if (!config.lastSyncedGeneration && config.lastSyncedCommit) {
9041
+ config.lastSyncedGeneration = "legacy";
9042
+ added.push("lastSyncedGeneration(migrated)");
9043
+ }
9044
+ delete config.lastSyncedCommit;
9045
+ added.push("lastSyncedCommit(removed)");
9046
+ }
9038
9047
  if (added.length > 0) {
9039
9048
  await ConfigManager.write(paths, config);
9040
9049
  }
@@ -9064,8 +9073,8 @@ var exports_genome_sync = {};
9064
9073
  __export(exports_genome_sync, {
9065
9074
  syncGenomeFromProject: () => syncGenomeFromProject
9066
9075
  });
9067
- import { join as join4 } from "path";
9068
- import { readdir as readdir3, stat as stat2 } from "fs/promises";
9076
+ import { join as join5 } from "path";
9077
+ import { readdir as readdir4, stat as stat2 } from "fs/promises";
9069
9078
  async function scanProject(projectRoot) {
9070
9079
  const scan = {
9071
9080
  language: "Unknown",
@@ -9084,7 +9093,7 @@ async function scanProject(projectRoot) {
9084
9093
  directories: [],
9085
9094
  existingDocs: []
9086
9095
  };
9087
- const pkgContent = await readTextFile(join4(projectRoot, "package.json"));
9096
+ const pkgContent = await readTextFile(join5(projectRoot, "package.json"));
9088
9097
  if (pkgContent) {
9089
9098
  try {
9090
9099
  const pkg = JSON.parse(pkgContent);
@@ -9143,16 +9152,16 @@ async function scanProject(projectRoot) {
9143
9152
  scan.buildTool = "Turbopack";
9144
9153
  } catch {}
9145
9154
  }
9146
- if (await fileExists(join4(projectRoot, "go.mod"))) {
9155
+ if (await fileExists(join5(projectRoot, "go.mod"))) {
9147
9156
  scan.language = "Go";
9148
9157
  scan.runtime = "Go";
9149
9158
  scan.packageManager = "Go Modules";
9150
9159
  }
9151
- if (await fileExists(join4(projectRoot, "pyproject.toml")) || await fileExists(join4(projectRoot, "requirements.txt"))) {
9160
+ if (await fileExists(join5(projectRoot, "pyproject.toml")) || await fileExists(join5(projectRoot, "requirements.txt"))) {
9152
9161
  scan.language = "Python";
9153
9162
  scan.runtime = "Python";
9154
- if (await fileExists(join4(projectRoot, "pyproject.toml"))) {
9155
- const pyproject = await readTextFile(join4(projectRoot, "pyproject.toml"));
9163
+ if (await fileExists(join5(projectRoot, "pyproject.toml"))) {
9164
+ const pyproject = await readTextFile(join5(projectRoot, "pyproject.toml"));
9156
9165
  if (pyproject?.includes("[tool.poetry]"))
9157
9166
  scan.packageManager = "Poetry";
9158
9167
  else if (pyproject?.includes("[tool.uv]") || pyproject?.includes("[project]"))
@@ -9167,23 +9176,23 @@ async function scanProject(projectRoot) {
9167
9176
  scan.framework = "Flask";
9168
9177
  }
9169
9178
  }
9170
- if (await fileExists(join4(projectRoot, "Cargo.toml"))) {
9179
+ if (await fileExists(join5(projectRoot, "Cargo.toml"))) {
9171
9180
  scan.language = "Rust";
9172
9181
  scan.runtime = "Rust";
9173
9182
  scan.packageManager = "Cargo";
9174
9183
  }
9175
- if (await fileExists(join4(projectRoot, "tsconfig.json"))) {
9184
+ if (await fileExists(join5(projectRoot, "tsconfig.json"))) {
9176
9185
  scan.hasTypeScript = true;
9177
9186
  scan.language = "TypeScript";
9178
9187
  }
9179
- scan.hasDocker = await fileExists(join4(projectRoot, "Dockerfile")) || await fileExists(join4(projectRoot, "docker-compose.yml"));
9188
+ scan.hasDocker = await fileExists(join5(projectRoot, "Dockerfile")) || await fileExists(join5(projectRoot, "docker-compose.yml"));
9180
9189
  try {
9181
- const entries = await readdir3(projectRoot);
9190
+ const entries = await readdir4(projectRoot);
9182
9191
  for (const entry of entries) {
9183
9192
  if (entry.startsWith(".") || entry === "node_modules")
9184
9193
  continue;
9185
9194
  try {
9186
- const s = await stat2(join4(projectRoot, entry));
9195
+ const s = await stat2(join5(projectRoot, entry));
9187
9196
  if (s.isDirectory())
9188
9197
  scan.directories.push(entry);
9189
9198
  } catch {}
@@ -9191,7 +9200,7 @@ async function scanProject(projectRoot) {
9191
9200
  } catch {}
9192
9201
  const docFiles = ["README.md", "CLAUDE.md", "AGENTS.md", "CONTRIBUTING.md", "ARCHITECTURE.md"];
9193
9202
  for (const file of docFiles) {
9194
- const content = await readTextFile(join4(projectRoot, file));
9203
+ const content = await readTextFile(join5(projectRoot, file));
9195
9204
  if (content) {
9196
9205
  scan.existingDocs.push({ file, content: content.substring(0, 2000) });
9197
9206
  }
@@ -9320,17 +9329,17 @@ async function syncGenomeFromProject(projectRoot, genomePath, onProgress) {
9320
9329
  log(`Detected: ${scan.language}, ${scan.framework !== "None" ? scan.framework : "no framework"}, ${scan.packageManager}`);
9321
9330
  const { writeTextFile: writeTextFile2 } = await Promise.resolve().then(() => (init_fs(), exports_fs));
9322
9331
  log("Generating constraints.md...");
9323
- await writeTextFile2(join4(genomePath, "constraints.md"), generateConstraints(scan));
9332
+ await writeTextFile2(join5(genomePath, "constraints.md"), generateConstraints(scan));
9324
9333
  log("Generating conventions.md...");
9325
- await writeTextFile2(join4(genomePath, "conventions.md"), generateConventions(scan));
9334
+ await writeTextFile2(join5(genomePath, "conventions.md"), generateConventions(scan));
9326
9335
  log("Generating principles.md...");
9327
- await writeTextFile2(join4(genomePath, "principles.md"), generatePrinciples(scan));
9336
+ await writeTextFile2(join5(genomePath, "principles.md"), generatePrinciples(scan));
9328
9337
  log("Generating source-map.md...");
9329
- await writeTextFile2(join4(genomePath, "source-map.md"), generateSourceMap(scan));
9330
- const { mkdir: mkdir3 } = await import("fs/promises");
9331
- const domainDir = join4(genomePath, "domain");
9332
- await mkdir3(domainDir, { recursive: true });
9333
- const domainReadme = join4(domainDir, "README.md");
9338
+ await writeTextFile2(join5(genomePath, "source-map.md"), generateSourceMap(scan));
9339
+ const { mkdir: mkdir4 } = await import("fs/promises");
9340
+ const domainDir = join5(genomePath, "domain");
9341
+ await mkdir4(domainDir, { recursive: true });
9342
+ const domainReadme = join5(domainDir, "README.md");
9334
9343
  if (!await fileExists(domainReadme)) {
9335
9344
  await writeTextFile2(domainReadme, [
9336
9345
  "# Domain Rules",
@@ -9473,8 +9482,12 @@ function gitCurrentBranch(cwd) {
9473
9482
  var init_git = () => {};
9474
9483
 
9475
9484
  // src/core/compression.ts
9476
- import { readdir as readdir5, rm } from "fs/promises";
9477
- import { join as join6 } from "path";
9485
+ import { readdir as readdir6, rm } from "fs/promises";
9486
+ import { join as join7 } from "path";
9487
+ function safeCompletedAtTime(dateStr) {
9488
+ const t = new Date(dateStr).getTime();
9489
+ return Number.isNaN(t) ? 0 : t;
9490
+ }
9478
9491
  function extractGenNum(name) {
9479
9492
  const match = name.match(/^gen-(\d{3})/);
9480
9493
  return match ? parseInt(match[1], 10) : 0;
@@ -9505,9 +9518,9 @@ async function countLines(filePath) {
9505
9518
  async function countDirLines(dirPath) {
9506
9519
  let total = 0;
9507
9520
  try {
9508
- const entries = await readdir5(dirPath, { withFileTypes: true });
9521
+ const entries = await readdir6(dirPath, { withFileTypes: true });
9509
9522
  for (const entry of entries) {
9510
- const fullPath = join6(dirPath, entry.name);
9523
+ const fullPath = join7(dirPath, entry.name);
9511
9524
  if (entry.isFile() && entry.name.endsWith(".md")) {
9512
9525
  total += await countLines(fullPath);
9513
9526
  } else if (entry.isDirectory()) {
@@ -9518,7 +9531,7 @@ async function countDirLines(dirPath) {
9518
9531
  return total;
9519
9532
  }
9520
9533
  async function readDirMeta(dirPath) {
9521
- const content = await readTextFile(join6(dirPath, "meta.yml"));
9534
+ const content = await readTextFile(join7(dirPath, "meta.yml"));
9522
9535
  if (content === null)
9523
9536
  return null;
9524
9537
  try {
@@ -9536,9 +9549,9 @@ async function readFileMeta(filePath) {
9536
9549
  async function scanLineage(paths) {
9537
9550
  const entries = [];
9538
9551
  try {
9539
- const items = await readdir5(paths.lineage, { withFileTypes: true });
9552
+ const items = await readdir6(paths.lineage, { withFileTypes: true });
9540
9553
  for (const item of items) {
9541
- const fullPath = join6(paths.lineage, item.name);
9554
+ const fullPath = join7(paths.lineage, item.name);
9542
9555
  if (item.isDirectory() && item.name.startsWith("gen-")) {
9543
9556
  const genNum = extractGenNum(item.name);
9544
9557
  const lines = await countDirLines(fullPath);
@@ -9579,11 +9592,10 @@ async function scanLineage(paths) {
9579
9592
  }
9580
9593
  } catch {}
9581
9594
  return entries.sort((a, b) => {
9582
- if (a.completedAt && b.completedAt) {
9583
- const cmp = a.completedAt.localeCompare(b.completedAt);
9584
- if (cmp !== 0)
9585
- return cmp;
9586
- }
9595
+ const aTime = safeCompletedAtTime(a.completedAt);
9596
+ const bTime = safeCompletedAtTime(b.completedAt);
9597
+ if (aTime !== bTime)
9598
+ return aTime - bTime;
9587
9599
  return a.genNum - b.genNum;
9588
9600
  });
9589
9601
  }
@@ -9594,7 +9606,7 @@ async function findLeafNodes(paths, entries) {
9594
9606
  if (entry.type === "level2")
9595
9607
  continue;
9596
9608
  let meta = null;
9597
- const fullPath = join6(paths.lineage, entry.name);
9609
+ const fullPath = join7(paths.lineage, entry.name);
9598
9610
  if (entry.type === "dir") {
9599
9611
  meta = await readDirMeta(fullPath);
9600
9612
  } else {
@@ -9625,7 +9637,7 @@ async function compressLevel1(genDir, genName) {
9625
9637
  }
9626
9638
  let goal = "", completionConditions = "";
9627
9639
  {
9628
- const objective = await readTextFile(join6(genDir, "01-objective.md"));
9640
+ const objective = await readTextFile(join7(genDir, "01-objective.md"));
9629
9641
  if (objective) {
9630
9642
  const goalMatch = objective.match(/## Goal\n([\s\S]*?)(?=\n##)/);
9631
9643
  if (goalMatch)
@@ -9637,7 +9649,7 @@ async function compressLevel1(genDir, genName) {
9637
9649
  }
9638
9650
  let lessons = "", genomeChanges = "", nextBacklog = "";
9639
9651
  {
9640
- const completion = await readTextFile(join6(genDir, "05-completion.md"));
9652
+ const completion = await readTextFile(join7(genDir, "05-completion.md"));
9641
9653
  if (completion) {
9642
9654
  const lessonsMatch = completion.match(/### Lessons Learned\n([\s\S]*?)(?=\n###)/);
9643
9655
  if (lessonsMatch)
@@ -9652,7 +9664,7 @@ async function compressLevel1(genDir, genName) {
9652
9664
  }
9653
9665
  let summaryText = "";
9654
9666
  {
9655
- const completion = await readTextFile(join6(genDir, "05-completion.md"));
9667
+ const completion = await readTextFile(join7(genDir, "05-completion.md"));
9656
9668
  if (completion) {
9657
9669
  const summaryMatch = completion.match(/## Summary\n([\s\S]*?)(?=\n##)/);
9658
9670
  if (summaryMatch)
@@ -9661,7 +9673,7 @@ async function compressLevel1(genDir, genName) {
9661
9673
  }
9662
9674
  let validationResult = "";
9663
9675
  {
9664
- const validation = await readTextFile(join6(genDir, "04-validation.md"));
9676
+ const validation = await readTextFile(join7(genDir, "04-validation.md"));
9665
9677
  if (validation) {
9666
9678
  const resultMatch = validation.match(/## Result: (.+)/);
9667
9679
  if (resultMatch)
@@ -9670,7 +9682,7 @@ async function compressLevel1(genDir, genName) {
9670
9682
  }
9671
9683
  let deferred = "";
9672
9684
  {
9673
- const impl = await readTextFile(join6(genDir, "03-implementation.md"));
9685
+ const impl = await readTextFile(join7(genDir, "03-implementation.md"));
9674
9686
  if (impl) {
9675
9687
  const deferredMatch = impl.match(/## Deferred Tasks\n([\s\S]*?)(?=\n##)/);
9676
9688
  if (deferredMatch) {
@@ -9756,7 +9768,7 @@ async function findForkedByOtherBranches(paths, cwd) {
9756
9768
  }
9757
9769
  async function compressLevel2Single(level1Files, paths) {
9758
9770
  const compressed = [];
9759
- const epochPath = join6(paths.lineage, "epoch.md");
9771
+ const epochPath = join7(paths.lineage, "epoch.md");
9760
9772
  let existingMeta = { generations: [] };
9761
9773
  let existingBody = "";
9762
9774
  const existingContent = await readTextFile(epochPath);
@@ -9828,10 +9840,10 @@ async function compressLineageIfNeeded(paths, projectRoot) {
9828
9840
  const currentTotal = await countDirLines(paths.lineage);
9829
9841
  if (currentTotal <= LINEAGE_MAX_LINES)
9830
9842
  break;
9831
- const dirPath = join6(paths.lineage, dir.name);
9843
+ const dirPath = join7(paths.lineage, dir.name);
9832
9844
  const compressed = await compressLevel1(dirPath, dir.name);
9833
9845
  const genId = dir.name.match(/^gen-\d{3}(?:-[a-f0-9]{6})?/)?.[0] ?? dir.name;
9834
- const outPath = join6(paths.lineage, `${genId}.md`);
9846
+ const outPath = join7(paths.lineage, `${genId}.md`);
9835
9847
  await writeTextFile(outPath, compressed);
9836
9848
  await rm(dirPath, { recursive: true });
9837
9849
  result.level1.push(genId);
@@ -9852,8 +9864,8 @@ async function compressLineageIfNeeded(paths, projectRoot) {
9852
9864
  if (compressible.length > 0) {
9853
9865
  const filesWithMeta = await Promise.all(compressible.map(async (e) => ({
9854
9866
  name: e.name,
9855
- path: join6(paths.lineage, e.name),
9856
- meta: await readFileMeta(join6(paths.lineage, e.name))
9867
+ path: join7(paths.lineage, e.name),
9868
+ meta: await readFileMeta(join7(paths.lineage, e.name))
9857
9869
  })));
9858
9870
  const compressed = await compressLevel2Single(filesWithMeta, paths);
9859
9871
  result.level2.push(...compressed);
@@ -9869,18 +9881,18 @@ var init_compression = __esm(() => {
9869
9881
  });
9870
9882
 
9871
9883
  // src/core/lineage.ts
9872
- import { readdir as readdir6 } from "fs/promises";
9873
- import { join as join7 } from "path";
9884
+ import { readdir as readdir7 } from "fs/promises";
9885
+ import { join as join8 } from "path";
9874
9886
  async function listCompleted(paths) {
9875
9887
  try {
9876
- const entries = await readdir6(paths.lineage);
9888
+ const entries = await readdir7(paths.lineage);
9877
9889
  return entries.filter((e) => e.startsWith("gen-")).sort();
9878
9890
  } catch {
9879
9891
  return [];
9880
9892
  }
9881
9893
  }
9882
9894
  async function readMeta(paths, lineageDirName) {
9883
- const metaPath = join7(paths.lineage, lineageDirName, "meta.yml");
9895
+ const metaPath = join8(paths.lineage, lineageDirName, "meta.yml");
9884
9896
  const content = await readTextFile(metaPath);
9885
9897
  if (content === null)
9886
9898
  return null;
@@ -9889,14 +9901,14 @@ async function readMeta(paths, lineageDirName) {
9889
9901
  async function listMeta(paths) {
9890
9902
  const metas = [];
9891
9903
  try {
9892
- const entries = await readdir6(paths.lineage, { withFileTypes: true });
9904
+ const entries = await readdir7(paths.lineage, { withFileTypes: true });
9893
9905
  for (const entry of entries) {
9894
9906
  if (entry.isDirectory() && entry.name.startsWith("gen-")) {
9895
9907
  const meta = await readMeta(paths, entry.name);
9896
9908
  if (meta)
9897
9909
  metas.push(meta);
9898
9910
  } else if (entry.isFile() && entry.name.startsWith("gen-") && entry.name.endsWith(".md")) {
9899
- const content = await readTextFile(join7(paths.lineage, entry.name));
9911
+ const content = await readTextFile(join8(paths.lineage, entry.name));
9900
9912
  if (content) {
9901
9913
  const meta = parseFrontmatter(content);
9902
9914
  if (meta)
@@ -9923,10 +9935,14 @@ async function nextSeq(paths, currentId) {
9923
9935
  }
9924
9936
  return maxSeq + 1;
9925
9937
  }
9938
+ function safeCompletedAtTime2(dateStr) {
9939
+ const t = new Date(dateStr).getTime();
9940
+ return Number.isNaN(t) ? 0 : t;
9941
+ }
9926
9942
  async function resolveParents(paths) {
9927
9943
  const metas = await listMeta(paths);
9928
9944
  if (metas.length > 0) {
9929
- const sorted = metas.sort((a, b) => new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime());
9945
+ const sorted = metas.sort((a, b) => safeCompletedAtTime2(b.completedAt) - safeCompletedAtTime2(a.completedAt));
9930
9946
  return [sorted[0].id];
9931
9947
  }
9932
9948
  const dirs = await listCompleted(paths);
@@ -9949,16 +9965,17 @@ var init_lineage = __esm(() => {
9949
9965
  // src/core/generation.ts
9950
9966
  import { createHash, randomBytes } from "crypto";
9951
9967
  import { hostname } from "os";
9952
- import { readdir as readdir7, mkdir as mkdir4, rename, unlink as unlink3 } from "fs/promises";
9953
- import { join as join8 } from "path";
9954
- function generateStageToken(genId, stage) {
9968
+ import { readdir as readdir8, mkdir as mkdir5, rename, unlink as unlink3 } from "fs/promises";
9969
+ import { join as join9 } from "path";
9970
+ function generateToken(genId, stage, phase) {
9955
9971
  const nonce = randomBytes(16).toString("hex");
9956
- const hash = createHash("sha256").update(nonce + genId + stage).digest("hex");
9972
+ const input = phase ? `${nonce}${genId}${stage}:${phase}` : `${nonce}${genId}${stage}`;
9973
+ const hash = createHash("sha256").update(input).digest("hex");
9957
9974
  return { nonce, hash };
9958
9975
  }
9959
- function verifyStageToken(token, genId, stage, expectedHash) {
9960
- const computed = createHash("sha256").update(token + genId + stage).digest("hex");
9961
- return computed === expectedHash;
9976
+ function verifyToken(token, genId, stage, expectedHash, phase) {
9977
+ const input = phase ? `${token}${genId}${stage}:${phase}` : `${token}${genId}${stage}`;
9978
+ return createHash("sha256").update(input).digest("hex") === expectedHash;
9962
9979
  }
9963
9980
  function generateGenHash(parents, goal, genomeHash, machineId, startedAt) {
9964
9981
  const input = JSON.stringify({ parents, goal, genomeHash, machineId, startedAt });
@@ -9970,13 +9987,13 @@ function getMachineId() {
9970
9987
  async function computeGenomeHash(genomePath) {
9971
9988
  const hash = createHash("sha256");
9972
9989
  try {
9973
- const entries = (await readdir7(genomePath, { recursive: true, withFileTypes: true })).filter((e) => e.isFile()).sort((a, b) => {
9974
- const pathA = join8(e2path(a), a.name);
9975
- const pathB = join8(e2path(b), b.name);
9990
+ const entries = (await readdir8(genomePath, { recursive: true, withFileTypes: true })).filter((e) => e.isFile()).sort((a, b) => {
9991
+ const pathA = join9(e2path(a), a.name);
9992
+ const pathB = join9(e2path(b), b.name);
9976
9993
  return pathA.localeCompare(pathB);
9977
9994
  });
9978
9995
  for (const entry of entries) {
9979
- const filePath = join8(e2path(entry), entry.name);
9996
+ const filePath = join9(e2path(entry), entry.name);
9980
9997
  const content = await readTextFile(filePath);
9981
9998
  if (content !== null) {
9982
9999
  hash.update(filePath.replace(genomePath, ""));
@@ -10038,6 +10055,32 @@ class GenerationManager {
10038
10055
  await writeTextFile(this.paths.currentYml, CURRENT_YML_HEADER + import_yaml4.default.stringify(state));
10039
10056
  return state;
10040
10057
  }
10058
+ async createRecoveryGeneration(goal, genomeVersion, recovers) {
10059
+ if (recovers.length === 0) {
10060
+ throw new Error("Recovery generation requires at least one target generation ID");
10061
+ }
10062
+ const seq = await this.nextSeq();
10063
+ const now = new Date().toISOString();
10064
+ const genomeHash = await computeGenomeHash(this.paths.genome);
10065
+ const machineId = getMachineId();
10066
+ const parents = await this.resolveParents();
10067
+ const hash = generateGenHash(parents, goal, genomeHash, machineId, now);
10068
+ const id = formatGenId(seq, hash);
10069
+ const state = {
10070
+ id,
10071
+ goal,
10072
+ stage: "objective",
10073
+ genomeVersion,
10074
+ startedAt: now,
10075
+ timeline: [{ stage: "objective", at: now }],
10076
+ type: "recovery",
10077
+ parents,
10078
+ genomeHash,
10079
+ recovers
10080
+ };
10081
+ await writeTextFile(this.paths.currentYml, CURRENT_YML_HEADER + import_yaml4.default.stringify(state));
10082
+ return state;
10083
+ }
10041
10084
  async advance() {
10042
10085
  const state = await this.current();
10043
10086
  if (!state)
@@ -10063,7 +10106,7 @@ class GenerationManager {
10063
10106
  const goalSlug = state.goal.toLowerCase().replace(/[^a-z0-9가-힣]+/g, "-").replace(/^-|-$/g, "").slice(0, 30);
10064
10107
  const genDirName = `${state.id}-${goalSlug}`;
10065
10108
  const genDir = this.paths.generationDir(genDirName);
10066
- await mkdir4(genDir, { recursive: true });
10109
+ await mkdir5(genDir, { recursive: true });
10067
10110
  const meta = {
10068
10111
  id: state.id,
10069
10112
  type: state.type ?? "normal",
@@ -10071,14 +10114,15 @@ class GenerationManager {
10071
10114
  goal: state.goal,
10072
10115
  genomeHash: state.genomeHash ?? "unknown",
10073
10116
  startedAt: state.startedAt,
10074
- completedAt: now
10117
+ completedAt: now,
10118
+ ...state.type === "recovery" && state.recovers ? { recovers: state.recovers } : {}
10075
10119
  };
10076
- await writeTextFile(join8(genDir, "meta.yml"), import_yaml4.default.stringify(meta));
10077
- const lifeEntries = await readdir7(this.paths.life);
10120
+ await writeTextFile(join9(genDir, "meta.yml"), import_yaml4.default.stringify(meta));
10121
+ const lifeEntries = await readdir8(this.paths.life);
10078
10122
  for (const entry of lifeEntries) {
10079
10123
  if (/^\d{2}-[a-z]+(?:-[a-z]+)*\.md$/.test(entry)) {
10080
- const srcPath = join8(this.paths.life, entry);
10081
- const destPath = join8(genDir, entry);
10124
+ const srcPath = join9(this.paths.life, entry);
10125
+ const destPath = join9(genDir, entry);
10082
10126
  let content = await readTextFile(srcPath);
10083
10127
  if (content && content.startsWith("# REAP MANAGED")) {
10084
10128
  content = content.replace(/^# REAP MANAGED[^\n]*\n/, "");
@@ -10087,28 +10131,28 @@ class GenerationManager {
10087
10131
  await unlink3(srcPath);
10088
10132
  }
10089
10133
  }
10090
- const backlogDir = join8(genDir, "backlog");
10091
- await mkdir4(backlogDir, { recursive: true });
10134
+ const backlogDir = join9(genDir, "backlog");
10135
+ await mkdir5(backlogDir, { recursive: true });
10092
10136
  try {
10093
- const backlogEntries = await readdir7(this.paths.backlog);
10137
+ const backlogEntries = await readdir8(this.paths.backlog);
10094
10138
  for (const entry of backlogEntries) {
10095
- const content = await readTextFile(join8(this.paths.backlog, entry));
10139
+ const content = await readTextFile(join9(this.paths.backlog, entry));
10096
10140
  if (!content)
10097
10141
  continue;
10098
10142
  const isConsumed = /status:\s*consumed/i.test(content) || /consumed:\s*true/i.test(content);
10099
- await writeTextFile(join8(backlogDir, entry), content);
10143
+ await writeTextFile(join9(backlogDir, entry), content);
10100
10144
  if (isConsumed) {
10101
- await unlink3(join8(this.paths.backlog, entry));
10145
+ await unlink3(join9(this.paths.backlog, entry));
10102
10146
  }
10103
10147
  }
10104
10148
  } catch {}
10105
10149
  try {
10106
- const mutEntries = await readdir7(this.paths.mutations);
10150
+ const mutEntries = await readdir8(this.paths.mutations);
10107
10151
  if (mutEntries.length > 0) {
10108
- const mutDir = join8(genDir, "mutations");
10109
- await mkdir4(mutDir, { recursive: true });
10152
+ const mutDir = join9(genDir, "mutations");
10153
+ await mkdir5(mutDir, { recursive: true });
10110
10154
  for (const entry of mutEntries) {
10111
- await rename(join8(this.paths.mutations, entry), join8(mutDir, entry));
10155
+ await rename(join9(this.paths.mutations, entry), join9(mutDir, entry));
10112
10156
  }
10113
10157
  }
10114
10158
  } catch {}
@@ -10197,6 +10241,40 @@ function emitError(command, message, details) {
10197
10241
  process.exit(1);
10198
10242
  }
10199
10243
 
10244
+ // src/cli/commands/run/next.ts
10245
+ var exports_next = {};
10246
+ __export(exports_next, {
10247
+ execute: () => execute
10248
+ });
10249
+ async function execute(paths, _phase) {
10250
+ const gm = new GenerationManager(paths);
10251
+ const state = await gm.current();
10252
+ if (!state) {
10253
+ emitError("next", "No active Generation. Run /reap.start first.");
10254
+ }
10255
+ const isMerge = state.type === "merge";
10256
+ if (state.lastNonce) {
10257
+ const nextCommand = isMerge ? `reap run merge-${state.stage}` : `reap run ${state.stage}`;
10258
+ emitOutput({
10259
+ status: "ok",
10260
+ command: "next",
10261
+ phase: "done",
10262
+ completed: ["gate", "auto-transition-detected"],
10263
+ context: {
10264
+ generationId: state.id,
10265
+ stage: state.stage,
10266
+ type: state.type
10267
+ },
10268
+ message: `Stage already advanced to ${state.stage} by auto-transition. Run: ${nextCommand}`,
10269
+ nextCommand
10270
+ });
10271
+ }
10272
+ emitError("next", `Stage transition not available. The current stage '${state.stage}' has not been completed yet. Run the stage command with --phase complete first, which will auto-advance to the next stage.`);
10273
+ }
10274
+ var init_next = __esm(() => {
10275
+ init_generation();
10276
+ });
10277
+
10200
10278
  // src/core/merge-lifecycle.ts
10201
10279
  class MergeLifeCycle {
10202
10280
  static next(stage) {
@@ -10244,14 +10322,14 @@ var init_merge_lifecycle = __esm(() => {
10244
10322
  });
10245
10323
 
10246
10324
  // src/core/hook-engine.ts
10247
- import { readdir as readdir10 } from "fs/promises";
10248
- import { join as join12 } from "path";
10325
+ import { readdir as readdir14 } from "fs/promises";
10326
+ import { join as join16 } from "path";
10249
10327
  import { execSync as execSync4 } from "child_process";
10250
10328
  async function executeHooks(hooksDir, event, projectRoot) {
10251
10329
  const hooks = await scanHooks(hooksDir, event);
10252
10330
  if (hooks.length === 0)
10253
10331
  return [];
10254
- const conditionsDir = join12(hooksDir, "conditions");
10332
+ const conditionsDir = join16(hooksDir, "conditions");
10255
10333
  const results = [];
10256
10334
  for (const hook of hooks) {
10257
10335
  const conditionMet = await evaluateCondition(conditionsDir, hook.condition, projectRoot);
@@ -10276,7 +10354,7 @@ async function executeHooks(hooksDir, event, projectRoot) {
10276
10354
  async function scanHooks(hooksDir, event) {
10277
10355
  let entries;
10278
10356
  try {
10279
- entries = await readdir10(hooksDir);
10357
+ entries = await readdir14(hooksDir);
10280
10358
  } catch {
10281
10359
  return [];
10282
10360
  }
@@ -10286,7 +10364,7 @@ async function scanHooks(hooksDir, event) {
10286
10364
  const match = filename.match(pattern);
10287
10365
  if (!match)
10288
10366
  continue;
10289
- const meta = await parseHookMeta(join12(hooksDir, filename), match[2]);
10367
+ const meta = await parseHookMeta(join16(hooksDir, filename), match[2]);
10290
10368
  hooks.push({
10291
10369
  filename,
10292
10370
  name: match[1],
@@ -10307,7 +10385,7 @@ async function parseHookMeta(filePath, ext) {
10307
10385
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
10308
10386
  if (fmMatch) {
10309
10387
  try {
10310
- const fm = import_yaml8.default.parse(fmMatch[1]) ?? {};
10388
+ const fm = import_yaml9.default.parse(fmMatch[1]) ?? {};
10311
10389
  return {
10312
10390
  condition: String(fm.condition ?? "always"),
10313
10391
  order: Number(fm.order ?? 50)
@@ -10333,7 +10411,7 @@ async function parseHookMeta(filePath, ext) {
10333
10411
  async function evaluateCondition(conditionsDir, conditionName, projectRoot) {
10334
10412
  if (conditionName === "always")
10335
10413
  return { met: true };
10336
- const scriptPath = join12(conditionsDir, `${conditionName}.sh`);
10414
+ const scriptPath = join16(conditionsDir, `${conditionName}.sh`);
10337
10415
  if (!await fileExists(scriptPath)) {
10338
10416
  return { met: false, reason: `condition script not found: ${conditionName}.sh` };
10339
10417
  }
@@ -10346,7 +10424,7 @@ async function evaluateCondition(conditionsDir, conditionName, projectRoot) {
10346
10424
  }
10347
10425
  async function executeShHook(hook, event, projectRoot, hooksDir) {
10348
10426
  try {
10349
- const stdout = execSync4(`bash "${join12(hooksDir, hook.filename)}"`, {
10427
+ const stdout = execSync4(`bash "${join16(hooksDir, hook.filename)}"`, {
10350
10428
  cwd: projectRoot,
10351
10429
  timeout: 60000,
10352
10430
  stdio: "pipe"
@@ -10372,7 +10450,7 @@ async function executeShHook(hook, event, projectRoot, hooksDir) {
10372
10450
  }
10373
10451
  }
10374
10452
  async function executeMdHook(hook, event, hooksDir) {
10375
- const content = await readTextFile(join12(hooksDir, hook.filename));
10453
+ const content = await readTextFile(join16(hooksDir, hook.filename));
10376
10454
  const body = content?.replace(/^---\n[\s\S]*?\n---\n?/, "").trim() ?? "";
10377
10455
  return {
10378
10456
  name: hook.name,
@@ -10382,115 +10460,10 @@ async function executeMdHook(hook, event, hooksDir) {
10382
10460
  content: body
10383
10461
  };
10384
10462
  }
10385
- var import_yaml8;
10463
+ var import_yaml9;
10386
10464
  var init_hook_engine = __esm(() => {
10387
10465
  init_fs();
10388
- import_yaml8 = __toESM(require_dist(), 1);
10389
- });
10390
-
10391
- // src/cli/commands/run/next.ts
10392
- var exports_next = {};
10393
- __export(exports_next, {
10394
- execute: () => execute
10395
- });
10396
- import { join as join13 } from "path";
10397
- async function execute(paths, _phase) {
10398
- const gm = new GenerationManager(paths);
10399
- const state = await gm.current();
10400
- if (!state) {
10401
- emitError("next", "No active Generation. Run /reap.start first.");
10402
- }
10403
- if (state.expectedTokenHash) {
10404
- const args = process.argv.slice(2);
10405
- const nonce = args.find((a) => !a.startsWith("-") && a !== "run" && a !== "next");
10406
- if (!nonce) {
10407
- emitError("next", `Stage transition blocked: no token provided. The stage command outputs a nonce that must be passed to /reap.next. Example: /reap.next <nonce>. This ensures the stage command was actually executed — you cannot skip stages.`);
10408
- }
10409
- if (!verifyStageToken(nonce, state.id, state.stage, state.expectedTokenHash)) {
10410
- emitError("next", `Token verification failed. The provided nonce does not match. Re-run the current stage command (reap run ${state.stage}) to get a valid token. You cannot forge or guess the token.`);
10411
- }
10412
- }
10413
- const isMerge = state.type === "merge";
10414
- let nextStage;
10415
- if (isMerge) {
10416
- nextStage = MergeLifeCycle.next(state.stage);
10417
- } else {
10418
- nextStage = LifeCycle.next(state.stage);
10419
- }
10420
- if (!nextStage) {
10421
- emitError("next", `Cannot advance from '${state.stage}' — already at the last stage.`);
10422
- }
10423
- state.stage = nextStage;
10424
- if (!state.timeline)
10425
- state.timeline = [];
10426
- state.timeline.push({ stage: nextStage, at: new Date().toISOString() });
10427
- state.expectedTokenHash = undefined;
10428
- await gm.save(state);
10429
- const artifactFile = isMerge ? MERGE_ARTIFACT[nextStage] : NORMAL_ARTIFACT[nextStage];
10430
- if (artifactFile) {
10431
- const templateDir = join13(__require("os").homedir(), ".reap", "templates");
10432
- const templatePath = join13(templateDir, artifactFile);
10433
- const destPath = paths.artifact(artifactFile);
10434
- if (await fileExists(templatePath) && !await fileExists(destPath)) {
10435
- const templateContent = await readTextFile(templatePath);
10436
- if (templateContent) {
10437
- await writeTextFile(destPath, templateContent);
10438
- }
10439
- }
10440
- }
10441
- const previousStage = state.timeline[state.timeline.length - 2]?.stage;
10442
- const STAGE_HOOK = {
10443
- planning: "onLifeObjected",
10444
- implementation: "onLifePlanned",
10445
- validation: "onLifeImplemented",
10446
- completion: "onLifeValidated",
10447
- mate: "onMergeDetected",
10448
- merge: "onMergeMated",
10449
- sync: "onMergeMerged",
10450
- "validation:merge": "onMergeSynced",
10451
- "completion:merge": "onMergeValidated"
10452
- };
10453
- const stageKey = isMerge && (nextStage === "validation" || nextStage === "completion") ? `${nextStage}:merge` : nextStage;
10454
- const stageHookEvent = STAGE_HOOK[stageKey];
10455
- const stageHookResults = stageHookEvent ? await executeHooks(paths.hooks, stageHookEvent, paths.projectRoot) : [];
10456
- const transitionEvent = isMerge ? "onMergeTransited" : "onLifeTransited";
10457
- const transitionHookResults = await executeHooks(paths.hooks, transitionEvent, paths.projectRoot);
10458
- emitOutput({
10459
- status: "ok",
10460
- command: "next",
10461
- phase: "done",
10462
- completed: ["gate", "nonce-verify", "advance-stage", "create-artifact", "hooks"],
10463
- context: {
10464
- generationId: state.id,
10465
- previousStage,
10466
- stage: nextStage,
10467
- type: state.type,
10468
- artifactFile,
10469
- hookResults: [...stageHookResults, ...transitionHookResults]
10470
- },
10471
- message: `Advanced to ${nextStage}. Proceed with /reap.${nextStage}.`
10472
- });
10473
- }
10474
- var NORMAL_ARTIFACT, MERGE_ARTIFACT;
10475
- var init_next = __esm(() => {
10476
- init_generation();
10477
- init_lifecycle();
10478
- init_merge_lifecycle();
10479
- init_fs();
10480
- init_hook_engine();
10481
- NORMAL_ARTIFACT = {
10482
- planning: "02-planning.md",
10483
- implementation: "03-implementation.md",
10484
- validation: "04-validation.md",
10485
- completion: "05-completion.md"
10486
- };
10487
- MERGE_ARTIFACT = {
10488
- mate: "02-mate.md",
10489
- merge: "03-merge.md",
10490
- sync: "04-sync.md",
10491
- validation: "05-validation.md",
10492
- completion: "06-completion.md"
10493
- };
10466
+ import_yaml9 = __toESM(require_dist(), 1);
10494
10467
  });
10495
10468
 
10496
10469
  // src/cli/commands/run/back.ts
@@ -10596,12 +10569,12 @@ var init_back = __esm(() => {
10596
10569
  });
10597
10570
 
10598
10571
  // src/core/backlog.ts
10599
- import { readdir as readdir11 } from "fs/promises";
10600
- import { join as join14 } from "path";
10572
+ import { readdir as readdir15 } from "fs/promises";
10573
+ import { join as join17 } from "path";
10601
10574
  async function scanBacklog(backlogDir) {
10602
10575
  let entries;
10603
10576
  try {
10604
- entries = await readdir11(backlogDir);
10577
+ entries = await readdir15(backlogDir);
10605
10578
  } catch {
10606
10579
  return [];
10607
10580
  }
@@ -10609,7 +10582,7 @@ async function scanBacklog(backlogDir) {
10609
10582
  for (const filename of entries) {
10610
10583
  if (!filename.endsWith(".md"))
10611
10584
  continue;
10612
- const content = await readTextFile(join14(backlogDir, filename));
10585
+ const content = await readTextFile(join17(backlogDir, filename));
10613
10586
  if (!content)
10614
10587
  continue;
10615
10588
  const { frontmatter, body } = parseFrontmatter2(content);
@@ -10627,7 +10600,7 @@ async function scanBacklog(backlogDir) {
10627
10600
  return items;
10628
10601
  }
10629
10602
  async function markBacklogConsumed(backlogDir, filename, genId) {
10630
- const filePath = join14(backlogDir, filename);
10603
+ const filePath = join17(backlogDir, filename);
10631
10604
  const content = await readTextFile(filePath);
10632
10605
  if (!content)
10633
10606
  return;
@@ -10636,7 +10609,7 @@ async function markBacklogConsumed(backlogDir, filename, genId) {
10636
10609
  frontmatter.consumedBy = genId;
10637
10610
  delete frontmatter.consumed;
10638
10611
  const newContent = `---
10639
- ${import_yaml9.default.stringify(frontmatter).trim()}
10612
+ ${import_yaml10.default.stringify(frontmatter).trim()}
10640
10613
  ---
10641
10614
  ${body}`;
10642
10615
  await writeTextFile(filePath, newContent);
@@ -10644,14 +10617,14 @@ ${body}`;
10644
10617
  async function revertBacklogConsumed(backlogDir, genId) {
10645
10618
  let entries;
10646
10619
  try {
10647
- entries = await readdir11(backlogDir);
10620
+ entries = await readdir15(backlogDir);
10648
10621
  } catch {
10649
10622
  return;
10650
10623
  }
10651
10624
  for (const filename of entries) {
10652
10625
  if (!filename.endsWith(".md"))
10653
10626
  continue;
10654
- const filePath = join14(backlogDir, filename);
10627
+ const filePath = join17(backlogDir, filename);
10655
10628
  const content = await readTextFile(filePath);
10656
10629
  if (!content)
10657
10630
  continue;
@@ -10660,7 +10633,7 @@ async function revertBacklogConsumed(backlogDir, genId) {
10660
10633
  frontmatter.status = "pending";
10661
10634
  delete frontmatter.consumedBy;
10662
10635
  const newContent = `---
10663
- ${import_yaml9.default.stringify(frontmatter).trim()}
10636
+ ${import_yaml10.default.stringify(frontmatter).trim()}
10664
10637
  ---
10665
10638
  ${body}`;
10666
10639
  await writeTextFile(filePath, newContent);
@@ -10672,15 +10645,15 @@ function parseFrontmatter2(content) {
10672
10645
  if (!match)
10673
10646
  return { frontmatter: {}, body: content };
10674
10647
  try {
10675
- return { frontmatter: import_yaml9.default.parse(match[1]) ?? {}, body: match[2] };
10648
+ return { frontmatter: import_yaml10.default.parse(match[1]) ?? {}, body: match[2] };
10676
10649
  } catch {
10677
10650
  return { frontmatter: {}, body: content };
10678
10651
  }
10679
10652
  }
10680
- var import_yaml9;
10653
+ var import_yaml10;
10681
10654
  var init_backlog = __esm(() => {
10682
10655
  init_fs();
10683
- import_yaml9 = __toESM(require_dist(), 1);
10656
+ import_yaml10 = __toESM(require_dist(), 1);
10684
10657
  });
10685
10658
 
10686
10659
  // src/cli/commands/run/start.ts
@@ -10688,8 +10661,8 @@ var exports_start = {};
10688
10661
  __export(exports_start, {
10689
10662
  execute: () => execute3
10690
10663
  });
10691
- import { join as join15 } from "path";
10692
- import { readdir as readdir12 } from "fs/promises";
10664
+ import { join as join18 } from "path";
10665
+ import { readdir as readdir16 } from "fs/promises";
10693
10666
  function getFlag2(args, name) {
10694
10667
  const idx = args.indexOf(`--${name}`);
10695
10668
  return idx !== -1 && args[idx + 1] ? args[idx + 1] : undefined;
@@ -10738,18 +10711,18 @@ async function execute3(paths, phase, argv = []) {
10738
10711
  }
10739
10712
  let genomeVersion = 1;
10740
10713
  try {
10741
- const lineageEntries = await readdir12(paths.lineage);
10714
+ const lineageEntries = await readdir16(paths.lineage);
10742
10715
  genomeVersion = lineageEntries.filter((e) => e.startsWith("gen-")).length + 1;
10743
10716
  } catch {}
10744
10717
  const state = await gm.create(goal, genomeVersion);
10745
- const { nonce, hash } = generateStageToken(state.id, state.stage);
10746
- state.expectedTokenHash = hash;
10718
+ const { nonce, hash } = generateToken(state.id, state.stage);
10719
+ state.expectedHash = hash;
10747
10720
  await gm.save(state);
10748
10721
  if (backlogFile) {
10749
10722
  await markBacklogConsumed(paths.backlog, backlogFile, state.id);
10750
10723
  }
10751
- const templateDir = join15(__require("os").homedir(), ".reap", "templates");
10752
- const templatePath = join15(templateDir, "01-objective.md");
10724
+ const templateDir = join18(__require("os").homedir(), ".reap", "templates");
10725
+ const templatePath = join18(templateDir, "01-objective.md");
10753
10726
  const destPath = paths.artifact("01-objective.md");
10754
10727
  if (await fileExists(templatePath)) {
10755
10728
  let template = await readTextFile(templatePath);
@@ -10808,12 +10781,133 @@ function checkSubmodules(projectRoot) {
10808
10781
  }
10809
10782
  var init_commit = () => {};
10810
10783
 
10784
+ // src/core/stage-transition.ts
10785
+ import { join as join19 } from "path";
10786
+ function verifyStageEntry(command, state) {
10787
+ if (!state.lastNonce) {
10788
+ return;
10789
+ }
10790
+ if (state.phase) {
10791
+ return;
10792
+ }
10793
+ if (!state.expectedHash) {
10794
+ emitError(command, "Stage transition error: lastNonce exists but expectedHash is missing. State may be corrupted.");
10795
+ }
10796
+ const isMerge = state.type === "merge";
10797
+ let prevStage;
10798
+ if (isMerge) {
10799
+ prevStage = MergeLifeCycle.prev(state.stage);
10800
+ } else {
10801
+ prevStage = LifeCycle.prev(state.stage);
10802
+ }
10803
+ if (!prevStage) {
10804
+ emitError(command, "Stage transition error: cannot determine previous stage for token verification.");
10805
+ }
10806
+ if (!verifyToken(state.lastNonce, state.id, prevStage, state.expectedHash)) {
10807
+ emitError(command, `Token verification failed. The stage chain token does not match. Re-run the previous stage command to get a valid token.`);
10808
+ }
10809
+ state.lastNonce = undefined;
10810
+ state.expectedHash = undefined;
10811
+ }
10812
+ function setPhaseNonce(state, stage, phase) {
10813
+ const { nonce, hash } = generateToken(state.id, stage, phase);
10814
+ state.lastNonce = nonce;
10815
+ state.expectedHash = hash;
10816
+ state.phase = phase;
10817
+ }
10818
+ function verifyPhaseEntry(command, state, stage, phase) {
10819
+ if (!state.lastNonce) {
10820
+ emitError(command, `Phase nonce missing. Complete the previous phase before running --phase ${phase}.`);
10821
+ }
10822
+ if (!state.expectedHash) {
10823
+ emitError(command, "Phase transition error: lastNonce exists but expectedHash is missing. State may be corrupted.");
10824
+ }
10825
+ if (!verifyToken(state.lastNonce, state.id, stage, state.expectedHash, phase)) {
10826
+ emitError(command, `Phase token verification failed. Re-run the previous phase to get a valid token.`);
10827
+ }
10828
+ state.lastNonce = undefined;
10829
+ state.expectedHash = undefined;
10830
+ state.phase = undefined;
10831
+ }
10832
+ async function performTransition(paths, state, saveFn) {
10833
+ const isMerge = state.type === "merge";
10834
+ let nextStage;
10835
+ if (isMerge) {
10836
+ nextStage = MergeLifeCycle.next(state.stage);
10837
+ } else {
10838
+ nextStage = LifeCycle.next(state.stage);
10839
+ }
10840
+ if (!nextStage) {
10841
+ throw new Error(`Cannot advance from '${state.stage}' — already at the last stage.`);
10842
+ }
10843
+ state.stage = nextStage;
10844
+ if (!state.timeline)
10845
+ state.timeline = [];
10846
+ state.timeline.push({ stage: nextStage, at: new Date().toISOString() });
10847
+ await saveFn(state);
10848
+ const artifactFile = isMerge ? MERGE_ARTIFACT[nextStage] : NORMAL_ARTIFACT[nextStage];
10849
+ if (artifactFile) {
10850
+ const templateDir = join19(__require("os").homedir(), ".reap", "templates");
10851
+ const templatePath = join19(templateDir, artifactFile);
10852
+ const destPath = paths.artifact(artifactFile);
10853
+ if (await fileExists(templatePath) && !await fileExists(destPath)) {
10854
+ const templateContent = await readTextFile(templatePath);
10855
+ if (templateContent) {
10856
+ await writeTextFile(destPath, templateContent);
10857
+ }
10858
+ }
10859
+ }
10860
+ const stageKey = isMerge && (nextStage === "validation" || nextStage === "completion") ? `${nextStage}:merge` : nextStage;
10861
+ const stageHookEvent = STAGE_HOOK[stageKey];
10862
+ const stageHookResults = stageHookEvent ? await executeHooks(paths.hooks, stageHookEvent, paths.projectRoot) : [];
10863
+ const transitionEvent = isMerge ? "onMergeTransited" : "onLifeTransited";
10864
+ const transitionHookResults = await executeHooks(paths.hooks, transitionEvent, paths.projectRoot);
10865
+ return {
10866
+ nextStage,
10867
+ artifactFile,
10868
+ stageHookResults,
10869
+ transitionHookResults
10870
+ };
10871
+ }
10872
+ var NORMAL_ARTIFACT, MERGE_ARTIFACT, STAGE_HOOK;
10873
+ var init_stage_transition = __esm(() => {
10874
+ init_lifecycle();
10875
+ init_merge_lifecycle();
10876
+ init_generation();
10877
+ init_fs();
10878
+ init_hook_engine();
10879
+ NORMAL_ARTIFACT = {
10880
+ planning: "02-planning.md",
10881
+ implementation: "03-implementation.md",
10882
+ validation: "04-validation.md",
10883
+ completion: "05-completion.md"
10884
+ };
10885
+ MERGE_ARTIFACT = {
10886
+ mate: "02-mate.md",
10887
+ merge: "03-merge.md",
10888
+ sync: "04-sync.md",
10889
+ validation: "05-validation.md",
10890
+ completion: "06-completion.md"
10891
+ };
10892
+ STAGE_HOOK = {
10893
+ planning: "onLifeObjected",
10894
+ implementation: "onLifePlanned",
10895
+ validation: "onLifeImplemented",
10896
+ completion: "onLifeValidated",
10897
+ mate: "onMergeDetected",
10898
+ merge: "onMergeMated",
10899
+ sync: "onMergeMerged",
10900
+ "validation:merge": "onMergeSynced",
10901
+ "completion:merge": "onMergeValidated"
10902
+ };
10903
+ });
10904
+
10811
10905
  // src/cli/commands/run/completion.ts
10812
10906
  var exports_completion = {};
10813
10907
  __export(exports_completion, {
10814
10908
  execute: () => execute4
10815
10909
  });
10816
- import { join as join16 } from "path";
10910
+ import { join as join20 } from "path";
10817
10911
  import { execSync as execSync6 } from "child_process";
10818
10912
  function detectGenomeImpact(projectRoot) {
10819
10913
  const impact = {
@@ -10894,6 +10988,8 @@ async function execute4(paths, phase) {
10894
10988
  if (state.stage !== "completion") {
10895
10989
  emitError("completion", `Stage is '${state.stage}', expected 'completion'.`);
10896
10990
  }
10991
+ verifyStageEntry("completion", state);
10992
+ await gm.save(state);
10897
10993
  const validationArtifact = paths.artifact("04-validation.md");
10898
10994
  if (!await fileExists(validationArtifact)) {
10899
10995
  emitError("completion", "04-validation.md does not exist. Complete validation first.");
@@ -10904,8 +11000,8 @@ async function execute4(paths, phase) {
10904
11000
  const implContent = await readTextFile(paths.artifact("03-implementation.md"));
10905
11001
  const destPath = paths.artifact("05-completion.md");
10906
11002
  if (!await fileExists(destPath)) {
10907
- const templateDir = join16(__require("os").homedir(), ".reap", "templates");
10908
- const templatePath = join16(templateDir, "05-completion.md");
11003
+ const templateDir = join20(__require("os").homedir(), ".reap", "templates");
11004
+ const templatePath = join20(templateDir, "05-completion.md");
10909
11005
  if (await fileExists(templatePath)) {
10910
11006
  const { writeTextFile: writeTextFile2 } = await Promise.resolve().then(() => (init_fs(), exports_fs));
10911
11007
  const template = await readTextFile(templatePath);
@@ -10913,6 +11009,8 @@ async function execute4(paths, phase) {
10913
11009
  await writeTextFile2(destPath, template);
10914
11010
  }
10915
11011
  }
11012
+ setPhaseNonce(state, "completion", "retrospective");
11013
+ await gm.save(state);
10916
11014
  emitOutput({
10917
11015
  status: "prompt",
10918
11016
  command: "completion",
@@ -10932,6 +11030,8 @@ async function execute4(paths, phase) {
10932
11030
  });
10933
11031
  }
10934
11032
  if (phase === "feedKnowledge") {
11033
+ verifyPhaseEntry("completion", state, "completion", "retrospective");
11034
+ await gm.save(state);
10935
11035
  const backlogItems = await scanBacklog(paths.backlog);
10936
11036
  const genomeChanges = backlogItems.filter((b) => b.type === "genome-change" && b.status !== "consumed");
10937
11037
  const envChanges = backlogItems.filter((b) => b.type === "environment-change" && b.status !== "consumed");
@@ -11021,6 +11121,7 @@ var init_completion = __esm(() => {
11021
11121
  init_backlog();
11022
11122
  init_hook_engine();
11023
11123
  init_commit();
11124
+ init_stage_transition();
11024
11125
  });
11025
11126
 
11026
11127
  // src/cli/commands/run/abort.ts
@@ -11028,8 +11129,8 @@ var exports_abort = {};
11028
11129
  __export(exports_abort, {
11029
11130
  execute: () => execute5
11030
11131
  });
11031
- import { join as join17 } from "path";
11032
- import { readdir as readdir13, unlink as unlink5 } from "fs/promises";
11132
+ import { join as join21 } from "path";
11133
+ import { readdir as readdir17, unlink as unlink6 } from "fs/promises";
11033
11134
  function getFlag3(args, name) {
11034
11135
  const idx = args.indexOf(`--${name}`);
11035
11136
  return idx !== -1 && args[idx + 1] ? args[idx + 1] : undefined;
@@ -11104,15 +11205,15 @@ async function execute5(paths, phase, argv = []) {
11104
11205
  sourceAction === "stash" ? "git stash pop으로 코드 복구" : sourceAction === "hold" ? "코드 변경이 working tree에 유지됨" : sourceAction === "rollback" ? "코드 변경이 revert됨. objective부터 재시작 필요" : "소스 코드 변경 없음"
11105
11206
  ].filter((line) => line !== null).join(`
11106
11207
  `);
11107
- await writeTextFile(join17(paths.backlog, `aborted-${state.id}.md`), backlogContent);
11208
+ await writeTextFile(join21(paths.backlog, `aborted-${state.id}.md`), backlogContent);
11108
11209
  backlogSaved = true;
11109
11210
  }
11110
11211
  await revertBacklogConsumed(paths.backlog, state.id);
11111
11212
  try {
11112
- const lifeEntries = await readdir13(paths.life);
11213
+ const lifeEntries = await readdir17(paths.life);
11113
11214
  for (const entry of lifeEntries) {
11114
11215
  if (/^\d{2}-[a-z]+(?:-[a-z]+)*\.md$/.test(entry)) {
11115
- await unlink5(join17(paths.life, entry));
11216
+ await unlink6(join21(paths.life, entry));
11116
11217
  }
11117
11218
  }
11118
11219
  } catch {}
@@ -11171,8 +11272,8 @@ var exports_objective = {};
11171
11272
  __export(exports_objective, {
11172
11273
  execute: () => execute7
11173
11274
  });
11174
- import { join as join18 } from "path";
11175
- import { readdir as readdir14 } from "fs/promises";
11275
+ import { join as join22 } from "path";
11276
+ import { readdir as readdir18 } from "fs/promises";
11176
11277
  async function execute7(paths, phase) {
11177
11278
  const gm = new GenerationManager(paths);
11178
11279
  const state = await gm.current();
@@ -11189,13 +11290,13 @@ async function execute7(paths, phase) {
11189
11290
  const envSummary = await readTextFile(paths.environmentSummary);
11190
11291
  let prevCompletion = null;
11191
11292
  try {
11192
- const lineageEntries = await readdir14(paths.lineage);
11293
+ const lineageEntries = await readdir18(paths.lineage);
11193
11294
  const genDirs = lineageEntries.filter((e) => e.startsWith("gen-")).sort();
11194
11295
  if (genDirs.length > 0) {
11195
11296
  const lastGen = genDirs[genDirs.length - 1];
11196
- prevCompletion = await readTextFile(join18(paths.lineage, lastGen, "05-completion.md"));
11297
+ prevCompletion = await readTextFile(join22(paths.lineage, lastGen, "05-completion.md"));
11197
11298
  if (!prevCompletion) {
11198
- const compressed = await readTextFile(join18(paths.lineage, `${lastGen}.md`));
11299
+ const compressed = await readTextFile(join22(paths.lineage, `${lastGen}.md`));
11199
11300
  if (compressed)
11200
11301
  prevCompletion = compressed.slice(0, 2000);
11201
11302
  }
@@ -11208,12 +11309,12 @@ async function execute7(paths, phase) {
11208
11309
  const configContent = await readTextFile(paths.config);
11209
11310
  let lineageCount = 0;
11210
11311
  try {
11211
- const entries = await readdir14(paths.lineage);
11312
+ const entries = await readdir18(paths.lineage);
11212
11313
  lineageCount = entries.filter((e) => e.startsWith("gen-")).length;
11213
11314
  } catch {}
11214
11315
  if (!isReentry) {
11215
- const templateDir = join18(__require("os").homedir(), ".reap", "templates");
11216
- const templatePath = join18(templateDir, "01-objective.md");
11316
+ const templateDir = join22(__require("os").homedir(), ".reap", "templates");
11317
+ const templatePath = join22(templateDir, "01-objective.md");
11217
11318
  if (await fileExists(templatePath)) {
11218
11319
  let template = await readTextFile(templatePath);
11219
11320
  if (template) {
@@ -11222,6 +11323,8 @@ async function execute7(paths, phase) {
11222
11323
  }
11223
11324
  }
11224
11325
  }
11326
+ setPhaseNonce(state, "objective", "work");
11327
+ await gm.save(state);
11225
11328
  emitOutput({
11226
11329
  status: "prompt",
11227
11330
  command: "objective",
@@ -11283,6 +11386,8 @@ async function execute7(paths, phase) {
11283
11386
  });
11284
11387
  }
11285
11388
  if (phase === "complete") {
11389
+ verifyPhaseEntry("objective", state, "objective", "work");
11390
+ await gm.save(state);
11286
11391
  const artifactPath = paths.artifact("01-objective.md");
11287
11392
  if (!await fileExists(artifactPath)) {
11288
11393
  emitError("objective", "01-objective.md does not exist. Complete the objective work first.");
@@ -11291,20 +11396,26 @@ async function execute7(paths, phase) {
11291
11396
  if (!content || content.length < 50) {
11292
11397
  emitError("objective", "01-objective.md appears incomplete (too short). Fill in the objective before completing.");
11293
11398
  }
11294
- const { nonce, hash } = generateStageToken(state.id, state.stage);
11295
- state.expectedTokenHash = hash;
11296
- await gm.save(state);
11399
+ const { nonce, hash } = generateToken(state.id, state.stage);
11400
+ state.expectedHash = hash;
11401
+ state.lastNonce = nonce;
11297
11402
  const hookResults = await executeHooks(paths.hooks, "onLifeObjected", paths.projectRoot);
11403
+ const transition = await performTransition(paths, state, (s) => gm.save(s));
11404
+ const nextCommand = `reap run ${transition.nextStage}`;
11298
11405
  emitOutput({
11299
11406
  status: "ok",
11300
11407
  command: "objective",
11301
11408
  phase: "complete",
11302
- completed: ["gate", "context-collect", "artifact-ensure", "creative-work", "artifact-verify", "hooks"],
11409
+ completed: ["gate", "context-collect", "artifact-ensure", "creative-work", "artifact-verify", "hooks", "auto-transition"],
11303
11410
  context: {
11304
11411
  id: state.id,
11305
- hookResults
11412
+ hookResults,
11413
+ nextStage: transition.nextStage,
11414
+ artifactFile: transition.artifactFile,
11415
+ transitionHookResults: [...transition.stageHookResults, ...transition.transitionHookResults]
11306
11416
  },
11307
- message: `Objective stage complete. Advance with: /reap.next ${nonce}`
11417
+ message: `Objective stage complete. Auto-advanced to ${transition.nextStage}. Run: ${nextCommand}`,
11418
+ nextCommand
11308
11419
  });
11309
11420
  }
11310
11421
  }
@@ -11313,6 +11424,7 @@ var init_objective = __esm(() => {
11313
11424
  init_fs();
11314
11425
  init_backlog();
11315
11426
  init_hook_engine();
11427
+ init_stage_transition();
11316
11428
  });
11317
11429
 
11318
11430
  // src/cli/commands/run/planning.ts
@@ -11320,7 +11432,7 @@ var exports_planning = {};
11320
11432
  __export(exports_planning, {
11321
11433
  execute: () => execute8
11322
11434
  });
11323
- import { join as join19 } from "path";
11435
+ import { join as join23 } from "path";
11324
11436
  async function execute8(paths, phase) {
11325
11437
  const gm = new GenerationManager(paths);
11326
11438
  const state = await gm.current();
@@ -11330,6 +11442,8 @@ async function execute8(paths, phase) {
11330
11442
  if (state.stage !== "planning") {
11331
11443
  emitError("planning", `Current stage is '${state.stage}', not 'planning'.`);
11332
11444
  }
11445
+ verifyStageEntry("planning", state);
11446
+ await gm.save(state);
11333
11447
  const objectiveArtifact = paths.artifact("01-objective.md");
11334
11448
  if (!await fileExists(objectiveArtifact)) {
11335
11449
  emitError("planning", "01-objective.md does not exist. Complete the objective stage first.");
@@ -11344,14 +11458,16 @@ async function execute8(paths, phase) {
11344
11458
  const principlesContent = await readTextFile(paths.principles);
11345
11459
  const implContent = await readTextFile(paths.artifact("03-implementation.md"));
11346
11460
  if (!isReentry) {
11347
- const templateDir = join19(__require("os").homedir(), ".reap", "templates");
11348
- const templatePath = join19(templateDir, "02-planning.md");
11461
+ const templateDir = join23(__require("os").homedir(), ".reap", "templates");
11462
+ const templatePath = join23(templateDir, "02-planning.md");
11349
11463
  if (await fileExists(templatePath)) {
11350
11464
  const template = await readTextFile(templatePath);
11351
11465
  if (template)
11352
11466
  await writeTextFile(artifactPath, template);
11353
11467
  }
11354
11468
  }
11469
+ setPhaseNonce(state, "planning", "work");
11470
+ await gm.save(state);
11355
11471
  emitOutput({
11356
11472
  status: "prompt",
11357
11473
  command: "planning",
@@ -11408,6 +11524,8 @@ async function execute8(paths, phase) {
11408
11524
  });
11409
11525
  }
11410
11526
  if (phase === "complete") {
11527
+ verifyPhaseEntry("planning", state, "planning", "work");
11528
+ await gm.save(state);
11411
11529
  const artifactPath = paths.artifact("02-planning.md");
11412
11530
  if (!await fileExists(artifactPath)) {
11413
11531
  emitError("planning", "02-planning.md does not exist. Complete the planning work first.");
@@ -11416,20 +11534,26 @@ async function execute8(paths, phase) {
11416
11534
  if (!content || content.length < 50) {
11417
11535
  emitError("planning", "02-planning.md appears incomplete. Fill in the plan before completing.");
11418
11536
  }
11419
- const { nonce, hash } = generateStageToken(state.id, state.stage);
11420
- state.expectedTokenHash = hash;
11421
- await gm.save(state);
11537
+ const { nonce, hash } = generateToken(state.id, state.stage);
11538
+ state.expectedHash = hash;
11539
+ state.lastNonce = nonce;
11422
11540
  const hookResults = await executeHooks(paths.hooks, "onLifePlanned", paths.projectRoot);
11541
+ const transition = await performTransition(paths, state, (s) => gm.save(s));
11542
+ const nextCommand = `reap run ${transition.nextStage}`;
11423
11543
  emitOutput({
11424
11544
  status: "ok",
11425
11545
  command: "planning",
11426
11546
  phase: "complete",
11427
- completed: ["gate", "context-collect", "artifact-ensure", "creative-work", "artifact-verify", "hooks"],
11547
+ completed: ["gate", "context-collect", "artifact-ensure", "creative-work", "artifact-verify", "hooks", "auto-transition"],
11428
11548
  context: {
11429
11549
  id: state.id,
11430
- hookResults
11550
+ hookResults,
11551
+ nextStage: transition.nextStage,
11552
+ artifactFile: transition.artifactFile,
11553
+ transitionHookResults: [...transition.stageHookResults, ...transition.transitionHookResults]
11431
11554
  },
11432
- message: `Planning stage complete. Advance with: /reap.next ${nonce}`
11555
+ message: `Planning stage complete. Auto-advanced to ${transition.nextStage}. Run: ${nextCommand}`,
11556
+ nextCommand
11433
11557
  });
11434
11558
  }
11435
11559
  }
@@ -11437,6 +11561,7 @@ var init_planning = __esm(() => {
11437
11561
  init_generation();
11438
11562
  init_fs();
11439
11563
  init_hook_engine();
11564
+ init_stage_transition();
11440
11565
  });
11441
11566
 
11442
11567
  // src/cli/commands/run/implementation.ts
@@ -11444,7 +11569,7 @@ var exports_implementation = {};
11444
11569
  __export(exports_implementation, {
11445
11570
  execute: () => execute9
11446
11571
  });
11447
- import { join as join20 } from "path";
11572
+ import { join as join24 } from "path";
11448
11573
  async function execute9(paths, phase) {
11449
11574
  const gm = new GenerationManager(paths);
11450
11575
  const state = await gm.current();
@@ -11454,6 +11579,8 @@ async function execute9(paths, phase) {
11454
11579
  if (state.stage !== "implementation") {
11455
11580
  emitError("implementation", `Current stage is '${state.stage}', not 'implementation'.`);
11456
11581
  }
11582
+ verifyStageEntry("implementation", state);
11583
+ await gm.save(state);
11457
11584
  const planningArtifact = paths.artifact("02-planning.md");
11458
11585
  if (!await fileExists(planningArtifact)) {
11459
11586
  emitError("implementation", "02-planning.md does not exist. Complete the planning stage first.");
@@ -11466,14 +11593,16 @@ async function execute9(paths, phase) {
11466
11593
  const conventionsContent = await readTextFile(paths.conventions);
11467
11594
  const constraintsContent = await readTextFile(paths.constraints);
11468
11595
  if (!isReentry) {
11469
- const templateDir = join20(__require("os").homedir(), ".reap", "templates");
11470
- const templatePath = join20(templateDir, "03-implementation.md");
11596
+ const templateDir = join24(__require("os").homedir(), ".reap", "templates");
11597
+ const templatePath = join24(templateDir, "03-implementation.md");
11471
11598
  if (await fileExists(templatePath)) {
11472
11599
  const template = await readTextFile(templatePath);
11473
11600
  if (template)
11474
11601
  await writeTextFile(artifactPath, template);
11475
11602
  }
11476
11603
  }
11604
+ setPhaseNonce(state, "implementation", "work");
11605
+ await gm.save(state);
11477
11606
  emitOutput({
11478
11607
  status: "prompt",
11479
11608
  command: "implementation",
@@ -11533,24 +11662,32 @@ async function execute9(paths, phase) {
11533
11662
  });
11534
11663
  }
11535
11664
  if (phase === "complete") {
11665
+ verifyPhaseEntry("implementation", state, "implementation", "work");
11666
+ await gm.save(state);
11536
11667
  const artifactPath = paths.artifact("03-implementation.md");
11537
11668
  if (!await fileExists(artifactPath)) {
11538
11669
  emitError("implementation", "03-implementation.md does not exist. Complete the implementation work first.");
11539
11670
  }
11540
- const { nonce, hash } = generateStageToken(state.id, state.stage);
11541
- state.expectedTokenHash = hash;
11542
- await gm.save(state);
11671
+ const { nonce, hash } = generateToken(state.id, state.stage);
11672
+ state.expectedHash = hash;
11673
+ state.lastNonce = nonce;
11543
11674
  const hookResults = await executeHooks(paths.hooks, "onLifeImplemented", paths.projectRoot);
11675
+ const transition = await performTransition(paths, state, (s) => gm.save(s));
11676
+ const nextCommand = `reap run ${transition.nextStage}`;
11544
11677
  emitOutput({
11545
11678
  status: "ok",
11546
11679
  command: "implementation",
11547
11680
  phase: "complete",
11548
- completed: ["gate", "context-collect", "artifact-ensure", "creative-work", "hooks"],
11681
+ completed: ["gate", "context-collect", "artifact-ensure", "creative-work", "hooks", "auto-transition"],
11549
11682
  context: {
11550
11683
  id: state.id,
11551
- hookResults
11684
+ hookResults,
11685
+ nextStage: transition.nextStage,
11686
+ artifactFile: transition.artifactFile,
11687
+ transitionHookResults: [...transition.stageHookResults, ...transition.transitionHookResults]
11552
11688
  },
11553
- message: `Implementation stage complete. Advance with: /reap.next ${nonce}`
11689
+ message: `Implementation stage complete. Auto-advanced to ${transition.nextStage}. Run: ${nextCommand}`,
11690
+ nextCommand
11554
11691
  });
11555
11692
  }
11556
11693
  }
@@ -11558,6 +11695,7 @@ var init_implementation = __esm(() => {
11558
11695
  init_generation();
11559
11696
  init_fs();
11560
11697
  init_hook_engine();
11698
+ init_stage_transition();
11561
11699
  });
11562
11700
 
11563
11701
  // src/cli/commands/run/validation.ts
@@ -11565,7 +11703,7 @@ var exports_validation = {};
11565
11703
  __export(exports_validation, {
11566
11704
  execute: () => execute10
11567
11705
  });
11568
- import { join as join21 } from "path";
11706
+ import { join as join25 } from "path";
11569
11707
  async function execute10(paths, phase) {
11570
11708
  const gm = new GenerationManager(paths);
11571
11709
  const state = await gm.current();
@@ -11575,6 +11713,8 @@ async function execute10(paths, phase) {
11575
11713
  if (state.stage !== "validation") {
11576
11714
  emitError("validation", `Current stage is '${state.stage}', not 'validation'.`);
11577
11715
  }
11716
+ verifyStageEntry("validation", state);
11717
+ await gm.save(state);
11578
11718
  const implArtifact = paths.artifact("03-implementation.md");
11579
11719
  if (!await fileExists(implArtifact)) {
11580
11720
  emitError("validation", "03-implementation.md does not exist. Complete the implementation stage first.");
@@ -11588,14 +11728,16 @@ async function execute10(paths, phase) {
11588
11728
  const implContent = await readTextFile(implArtifact);
11589
11729
  const objectiveContent = await readTextFile(paths.artifact("01-objective.md"));
11590
11730
  if (!isReentry) {
11591
- const templateDir = join21(__require("os").homedir(), ".reap", "templates");
11592
- const templatePath = join21(templateDir, "04-validation.md");
11731
+ const templateDir = join25(__require("os").homedir(), ".reap", "templates");
11732
+ const templatePath = join25(templateDir, "04-validation.md");
11593
11733
  if (await fileExists(templatePath)) {
11594
11734
  const template = await readTextFile(templatePath);
11595
11735
  if (template)
11596
11736
  await writeTextFile(artifactPath, template);
11597
11737
  }
11598
11738
  }
11739
+ setPhaseNonce(state, "validation", "work");
11740
+ await gm.save(state);
11599
11741
  emitOutput({
11600
11742
  status: "prompt",
11601
11743
  command: "validation",
@@ -11665,24 +11807,32 @@ async function execute10(paths, phase) {
11665
11807
  });
11666
11808
  }
11667
11809
  if (phase === "complete") {
11810
+ verifyPhaseEntry("validation", state, "validation", "work");
11811
+ await gm.save(state);
11668
11812
  const artifactPath = paths.artifact("04-validation.md");
11669
11813
  if (!await fileExists(artifactPath)) {
11670
11814
  emitError("validation", "04-validation.md does not exist. Complete the validation work first.");
11671
11815
  }
11672
- const { nonce, hash } = generateStageToken(state.id, state.stage);
11673
- state.expectedTokenHash = hash;
11674
- await gm.save(state);
11816
+ const { nonce, hash } = generateToken(state.id, state.stage);
11817
+ state.expectedHash = hash;
11818
+ state.lastNonce = nonce;
11675
11819
  const hookResults = await executeHooks(paths.hooks, "onLifeValidated", paths.projectRoot);
11820
+ const transition = await performTransition(paths, state, (s) => gm.save(s));
11821
+ const nextCommand = transition.nextStage !== "completion" ? `reap run ${transition.nextStage}` : "reap run completion";
11676
11822
  emitOutput({
11677
11823
  status: "ok",
11678
11824
  command: "validation",
11679
11825
  phase: "complete",
11680
- completed: ["gate", "context-collect", "artifact-ensure", "creative-work", "artifact-verify", "hooks"],
11826
+ completed: ["gate", "context-collect", "artifact-ensure", "creative-work", "artifact-verify", "hooks", "auto-transition"],
11681
11827
  context: {
11682
11828
  id: state.id,
11683
- hookResults
11829
+ hookResults,
11830
+ nextStage: transition.nextStage,
11831
+ artifactFile: transition.artifactFile,
11832
+ transitionHookResults: [...transition.stageHookResults, ...transition.transitionHookResults]
11684
11833
  },
11685
- message: `Validation stage complete. Advance with: /reap.next ${nonce}`
11834
+ message: `Validation stage complete. Auto-advanced to ${transition.nextStage}. Run: ${nextCommand}`,
11835
+ nextCommand
11686
11836
  });
11687
11837
  }
11688
11838
  }
@@ -11690,6 +11840,7 @@ var init_validation = __esm(() => {
11690
11840
  init_generation();
11691
11841
  init_fs();
11692
11842
  init_hook_engine();
11843
+ init_stage_transition();
11693
11844
  });
11694
11845
 
11695
11846
  // src/cli/commands/run/evolve.ts
@@ -11707,8 +11858,9 @@ function buildSubagentPrompt(paths, state, genomeSummaries, backlogSummary) {
11707
11858
  lines.push("");
11708
11859
  lines.push("## Rules");
11709
11860
  lines.push("- ALWAYS use `reap run <cmd>` commands to drive lifecycle. NEVER modify `current.yml` directly.");
11710
- lines.push("- Use `/reap.next` to advance stages and `/reap.back` to regress.");
11711
- lines.push("- Each stage command runs its own hook automatically at completion.");
11861
+ lines.push("- Each `--phase complete` auto-transitions to the next stage. No explicit `/reap.next` needed.");
11862
+ lines.push("- Use `/reap.back` to regress to a previous stage.");
11863
+ lines.push("- Each stage command verifies the stage chain token at entry (auto-verified from lastNonce).");
11712
11864
  lines.push("- `/reap.completion` handles archiving and the final commit.");
11713
11865
  lines.push("");
11714
11866
  lines.push("## Current State");
@@ -11743,7 +11895,7 @@ function buildSubagentPrompt(paths, state, genomeSummaries, backlogSummary) {
11743
11895
  lines.push(`Resume from stage: **${state.stage}**`);
11744
11896
  }
11745
11897
  lines.push("");
11746
- lines.push("### Stage Loop");
11898
+ lines.push("### Stage Loop (Auto-Transition)");
11747
11899
  lines.push("1. Read `current.yml` to confirm the current stage.");
11748
11900
  lines.push("2. Execute the stage command:");
11749
11901
  lines.push(" - `objective` -> `/reap.objective`");
@@ -11752,31 +11904,48 @@ function buildSubagentPrompt(paths, state, genomeSummaries, backlogSummary) {
11752
11904
  lines.push(" - `validation` -> `/reap.validation`");
11753
11905
  lines.push(" - `completion` -> `/reap.completion`");
11754
11906
  lines.push("3. Write the required artifact BEFORE completing the stage.");
11755
- lines.push("4. Run the stage complete phase if applicable (e.g., `reap run <stage> --phase complete`).");
11756
- lines.push("5. If current stage is NOT `completion`: run `/reap.next` to advance, then go to step 1.");
11757
- lines.push("6. If current stage IS `completion`: `/reap.completion` auto-archives after feedKnowledge phase. Done.");
11907
+ lines.push("4. Run `--phase complete` this auto-transitions to the next stage.");
11908
+ lines.push("5. If current stage IS `completion`: `/reap.completion` auto-archives after feedKnowledge phase. Done.");
11909
+ lines.push("6. Otherwise: the output tells you the next command go to step 1.");
11758
11910
  lines.push("");
11759
- lines.push("Note: `/reap.next` is a transition command, NOT a lifecycle stage.");
11911
+ lines.push("Note: `--phase complete` auto-transitions. `/reap.next` is a fallback, not required.");
11760
11912
  lines.push("");
11761
11913
  lines.push("## Project");
11762
11914
  lines.push(`- Path: ${paths.projectRoot}`);
11763
11915
  lines.push("");
11764
- lines.push("## Stage Chain Token");
11765
- lines.push("- Each stage command returns a `stageToken` in its output context.");
11766
- lines.push("- You MUST pass this token to `/reap.next --token <TOKEN>` (or set `REAP_STAGE_TOKEN` env var).");
11767
- lines.push("- Without a valid token, stage transition will be REJECTED.");
11768
- lines.push("- If token is missing or mismatched, re-run the current stage command to obtain a new token.");
11916
+ lines.push("## Stage Chain Token (Auto-Transition)");
11917
+ lines.push("- Each `--phase complete` generates a stage chain token and auto-transitions to the next stage.");
11918
+ lines.push("- The next stage command verifies the token at entry this ensures stages were not skipped.");
11919
+ lines.push("- `/reap.next` is maintained as a fallback but is no longer required in the normal flow.");
11769
11920
  lines.push("");
11770
11921
  lines.push("## Commit Rules");
11771
11922
  lines.push("- Create a git commit after implementation and after completion.");
11772
11923
  lines.push("- Use conventional commit format: `feat|fix|chore(scope): description`");
11773
11924
  lines.push("- Include the generation ID in the commit message.");
11774
11925
  lines.push("");
11926
+ lines.push("## Submodule Commit Rules");
11927
+ lines.push("- 커밋 전 반드시 `git -C tests status -s` 로 tests submodule의 dirty 상태를 확인하라.");
11928
+ lines.push("- dirty 파일이 있으면:");
11929
+ lines.push(' 1. `git -C tests add -A && git -C tests commit -m "..." && git -C tests push`');
11930
+ lines.push(" 2. parent repo에서 `git add tests` 로 submodule ref 업데이트");
11931
+ lines.push(" 3. 그 다음 parent repo의 나머지 파일과 함께 커밋");
11932
+ lines.push('- completion의 prompt에 "Dirty submodules detected"가 있으면 반드시 위 절차를 따르라.');
11933
+ lines.push("");
11775
11934
  lines.push("## Hook Prompt Execution");
11776
11935
  lines.push("- completion output의 `prompt` 필드에 '## Hook Prompts' 섹션이 포함되어 있으면, 해당 hook prompt를 반드시 순서대로 실행하라.");
11777
11936
  lines.push("- Hook prompt는 `.md` hook 파일의 내용으로, AI가 따라야 할 지시사항이다.");
11778
11937
  lines.push("- Hook prompt에 유저 확인이 필요한 경우(예: 프리뷰+컨펌), autonomous mode에서도 이를 존중하라.");
11779
11938
  lines.push("");
11939
+ lines.push("## Artifact Consistency & Design Pivot Detection");
11940
+ lines.push("- Before proceeding to a new stage, verify that previous stage artifacts align with the current design direction.");
11941
+ lines.push("- If the prompt context contains design corrections that contradict existing artifacts:");
11942
+ lines.push(" 1. Prioritize prompt instructions over the goal text.");
11943
+ lines.push(" 2. Use `/reap.back` to regress to the stage with the inconsistent artifact and rewrite it.");
11944
+ lines.push("- Regression triggers (use `/reap.back` in these situations):");
11945
+ lines.push(" - Artifacts contradict the design direction given in the prompt");
11946
+ lines.push(" - Implementation approach differs from what objective/planning described");
11947
+ lines.push(" - Prompt provides explicit design changes but earlier artifacts still reflect the old design");
11948
+ lines.push("");
11780
11949
  lines.push("## Interrupt Protection");
11781
11950
  lines.push('- 사용자의 새 메시지가 중간에 들어와도, 명시적 kill/중단 요청("중단", "stop", "abort")이 아닌 한 현재 작업을 끝까지 완료하라.');
11782
11951
  lines.push("- 작업을 shortcut으로 건너뛰거나 결과를 추정하지 마라. 모든 validation은 실제 실행 결과를 확인하라.");
@@ -11876,7 +12045,7 @@ async function execute11(paths, phase) {
11876
12045
  "",
11877
12046
  "### HARD-GATE",
11878
12047
  "NEVER modify `current.yml` directly to change the stage.",
11879
- "ALWAYS use `/reap.next` to advance and `/reap.back` to regress.",
12048
+ "Stage transitions happen automatically via `--phase complete`. Use `/reap.back` to regress.",
11880
12049
  "",
11881
12050
  "### Autonomous Override",
11882
12051
  "- Skip routine human confirmations. Proceed autonomously.",
@@ -11892,10 +12061,10 @@ async function execute11(paths, phase) {
11892
12061
  "- `/reap.validation` -> `onLifeValidated`",
11893
12062
  "- `/reap.completion` -> `onLifeCompleted` (before archiving and commit)",
11894
12063
  "",
11895
- "`/reap.next` only handles stage transitions -- it does NOT execute hooks or archiving.",
12064
+ "`--phase complete` auto-transitions to the next stage. `/reap.next` is a fallback.",
11896
12065
  "`/reap.completion` handles archiving and the final commit.",
11897
12066
  "",
11898
- "### Lifecycle Loop",
12067
+ "### Lifecycle Loop (Auto-Transition)",
11899
12068
  "Execute the following loop until the generation is complete:",
11900
12069
  "1. Read `current.yml` to determine the current stage",
11901
12070
  "2. Execute the corresponding stage command:",
@@ -11904,14 +12073,16 @@ async function execute11(paths, phase) {
11904
12073
  " - `implementation` -> `/reap.implementation`",
11905
12074
  " - `validation` -> `/reap.validation`",
11906
12075
  " - `completion` -> `/reap.completion`",
11907
- "3. When the stage command completes (hooks already executed by the stage command):",
11908
- " - If the current stage is `completion`: the loop ends.",
11909
- " - Otherwise: run `/reap.next` to advance, then return to step 1.",
12076
+ "3. `--phase complete` auto-transitions to the next stage.",
12077
+ " - If the stage is `completion`: the loop ends.",
12078
+ " - Otherwise: follow the `nextCommand` in the output to run the next stage.",
11910
12079
  "",
11911
12080
  "### Handling Issues",
11912
12081
  "- If validation fails: `/reap.back` to return to implementation (or earlier), then resume the loop",
11913
- "- If the human wants to pause: stop the loop",
11914
- "- If the human wants to skip a stage: advance with `/reap.next` without running the stage command"
12082
+ "- If artifacts contradict the design direction in the prompt: `/reap.back` to fix the inconsistent artifact",
12083
+ "- If implementation approach differs from what objective/planning described: `/reap.back` to objective or planning",
12084
+ "- If the prompt provides design corrections that differ from existing artifacts: prioritize prompt instructions, `/reap.back` to rewrite artifacts with the corrected design",
12085
+ "- If the human wants to pause: stop the loop"
11915
12086
  ].join(`
11916
12087
  `)
11917
12088
  });
@@ -11957,8 +12128,8 @@ var exports_sync_genome = {};
11957
12128
  __export(exports_sync_genome, {
11958
12129
  execute: () => execute13
11959
12130
  });
11960
- import { readdir as readdir15 } from "fs/promises";
11961
- import { join as join22 } from "path";
12131
+ import { readdir as readdir19 } from "fs/promises";
12132
+ import { join as join26 } from "path";
11962
12133
  async function execute13(paths, phase) {
11963
12134
  const gm = new GenerationManager(paths);
11964
12135
  const state = await gm.current();
@@ -11970,10 +12141,10 @@ async function execute13(paths, phase) {
11970
12141
  const sourceMapContent = await readTextFile(paths.sourceMap);
11971
12142
  const domainFiles = {};
11972
12143
  try {
11973
- const entries = await readdir15(paths.domain);
12144
+ const entries = await readdir19(paths.domain);
11974
12145
  for (const entry of entries) {
11975
12146
  if (entry.endsWith(".md")) {
11976
- const content = await readTextFile(join22(paths.domain, entry));
12147
+ const content = await readTextFile(join26(paths.domain, entry));
11977
12148
  if (content)
11978
12149
  domainFiles[entry] = content.slice(0, 1000);
11979
12150
  }
@@ -11981,7 +12152,7 @@ async function execute13(paths, phase) {
11981
12152
  } catch {}
11982
12153
  let genomeVersion = 0;
11983
12154
  try {
11984
- const lineageEntries = await readdir15(paths.lineage);
12155
+ const lineageEntries = await readdir19(paths.lineage);
11985
12156
  genomeVersion = lineageEntries.filter((e) => e.startsWith("gen-")).length;
11986
12157
  } catch {}
11987
12158
  emitOutput({
@@ -12032,20 +12203,26 @@ async function execute13(paths, phase) {
12032
12203
  });
12033
12204
  }
12034
12205
  if (phase === "complete") {
12206
+ const genId = state?.id || "manual";
12207
+ const config = await ConfigManager.read(paths);
12208
+ config.lastSyncedGeneration = genId;
12209
+ await ConfigManager.write(paths, config);
12035
12210
  emitOutput({
12036
12211
  status: "ok",
12037
12212
  command: "sync-genome",
12038
12213
  phase: "complete",
12039
- completed: ["gate", "context-collect", "analyze", "apply"],
12214
+ completed: ["gate", "context-collect", "analyze", "apply", "update-sync-state"],
12040
12215
  context: {
12041
- hasActiveGeneration: hasActiveGen
12216
+ hasActiveGeneration: hasActiveGen,
12217
+ lastSyncedGeneration: genId
12042
12218
  },
12043
- message: hasActiveGen ? "Genome differences recorded as backlog items. Apply during Completion." : "Genome synchronized."
12219
+ message: hasActiveGen ? "Genome differences recorded as backlog items. Apply during Completion." : `Genome synchronized. lastSyncedGeneration: ${genId}`
12044
12220
  });
12045
12221
  }
12046
12222
  }
12047
12223
  var init_sync_genome = __esm(() => {
12048
12224
  init_generation();
12225
+ init_config();
12049
12226
  init_fs();
12050
12227
  });
12051
12228
 
@@ -12054,8 +12231,8 @@ var exports_sync_environment = {};
12054
12231
  __export(exports_sync_environment, {
12055
12232
  execute: () => execute14
12056
12233
  });
12057
- import { readdir as readdir16 } from "fs/promises";
12058
- import { join as join23 } from "path";
12234
+ import { readdir as readdir20 } from "fs/promises";
12235
+ import { join as join27 } from "path";
12059
12236
  async function execute14(paths, phase) {
12060
12237
  const gm = new GenerationManager(paths);
12061
12238
  const state = await gm.current();
@@ -12064,10 +12241,10 @@ async function execute14(paths, phase) {
12064
12241
  const envSummary = await readTextFile(paths.environmentSummary);
12065
12242
  const envDocs = {};
12066
12243
  try {
12067
- const docsEntries = await readdir16(paths.environmentDocs);
12244
+ const docsEntries = await readdir20(paths.environmentDocs);
12068
12245
  for (const entry of docsEntries) {
12069
12246
  if (entry.endsWith(".md")) {
12070
- const content = await readTextFile(join23(paths.environmentDocs, entry));
12247
+ const content = await readTextFile(join27(paths.environmentDocs, entry));
12071
12248
  if (content)
12072
12249
  envDocs[entry] = content.slice(0, 1000);
12073
12250
  }
@@ -12075,7 +12252,7 @@ async function execute14(paths, phase) {
12075
12252
  } catch {}
12076
12253
  let linksContent = null;
12077
12254
  try {
12078
- linksContent = await readTextFile(join23(paths.environmentResources, "links.md"));
12255
+ linksContent = await readTextFile(join27(paths.environmentResources, "links.md"));
12079
12256
  } catch {}
12080
12257
  emitOutput({
12081
12258
  status: "prompt",
@@ -12147,7 +12324,7 @@ var exports_help = {};
12147
12324
  __export(exports_help, {
12148
12325
  execute: () => execute15
12149
12326
  });
12150
- import { join as join24 } from "path";
12327
+ import { join as join28 } from "path";
12151
12328
  function detectLanguage(configContent) {
12152
12329
  const raw = configContent?.match(/language:\s*(\S+)/)?.[1] ?? null;
12153
12330
  if (raw && raw in LANGUAGE_ALIASES)
@@ -12182,7 +12359,7 @@ async function execute15(paths) {
12182
12359
  const gm = new GenerationManager(paths);
12183
12360
  const state = await gm.current();
12184
12361
  const configContent = await readTextFile(paths.config);
12185
- const installedVersion = "0.14.0";
12362
+ const installedVersion = "0.15.1";
12186
12363
  const autoUpdate = configContent?.match(/autoUpdate:\s*(true|false)/)?.[1] === "true";
12187
12364
  const versionDisplay = formatVersionLine(installedVersion, !autoUpdate);
12188
12365
  const rawLang = detectLanguage(configContent);
@@ -12193,7 +12370,7 @@ async function execute15(paths) {
12193
12370
  const stateDisplay = state?.id ? `Active: **${state.id}** — ${state.goal} (Stage: ${state.stage})` : "No active Generation → `/reap.start` or `/reap.evolve`";
12194
12371
  const lines = buildLines(versionDisplay, lang, stateDisplay);
12195
12372
  if (topic) {
12196
- const guidePath = join24(ReapPaths.packageHooksDir, "reap-guide.md");
12373
+ const guidePath = join28(ReapPaths.packageHooksDir, "reap-guide.md");
12197
12374
  const reapGuide = await readTextFile(guidePath) ?? "";
12198
12375
  emitOutput({
12199
12376
  status: "prompt",
@@ -12498,8 +12675,8 @@ var init_merge = __esm(() => {
12498
12675
  });
12499
12676
 
12500
12677
  // src/core/merge-generation.ts
12501
- import { readdir as readdir17, mkdir as mkdir8, rename as rename3 } from "fs/promises";
12502
- import { join as join25 } from "path";
12678
+ import { readdir as readdir21, mkdir as mkdir10, rename as rename3 } from "fs/promises";
12679
+ import { join as join29 } from "path";
12503
12680
 
12504
12681
  class MergeGenerationManager {
12505
12682
  paths;
@@ -12510,7 +12687,7 @@ class MergeGenerationManager {
12510
12687
  const content = await readTextFile(this.paths.currentYml);
12511
12688
  if (content === null || !content.trim())
12512
12689
  return null;
12513
- const state = import_yaml10.default.parse(content);
12690
+ const state = import_yaml11.default.parse(content);
12514
12691
  if (!state.type)
12515
12692
  state.type = "normal";
12516
12693
  if (!state.parents)
@@ -12543,7 +12720,7 @@ class MergeGenerationManager {
12543
12720
  genomeHash,
12544
12721
  commonAncestor: commonAncestor ?? undefined
12545
12722
  };
12546
- await writeTextFile(this.paths.currentYml, import_yaml10.default.stringify(state));
12723
+ await writeTextFile(this.paths.currentYml, import_yaml11.default.stringify(state));
12547
12724
  return state;
12548
12725
  }
12549
12726
  async createFromBranch(targetBranch, projectRoot) {
@@ -12587,7 +12764,7 @@ class MergeGenerationManager {
12587
12764
  genomeHash,
12588
12765
  commonAncestor: commonAncestor ?? undefined
12589
12766
  };
12590
- await writeTextFile(this.paths.currentYml, import_yaml10.default.stringify(state));
12767
+ await writeTextFile(this.paths.currentYml, import_yaml11.default.stringify(state));
12591
12768
  return { state, report };
12592
12769
  }
12593
12770
  async resolveLatestGenId(branch, cwd) {
@@ -12612,7 +12789,7 @@ class MergeGenerationManager {
12612
12789
  const content = gitShow(ref, metaFile, cwd);
12613
12790
  if (content) {
12614
12791
  try {
12615
- const meta = import_yaml10.default.parse(content);
12792
+ const meta = import_yaml11.default.parse(content);
12616
12793
  if (meta?.id)
12617
12794
  metas.push(meta);
12618
12795
  } catch {}
@@ -12659,7 +12836,7 @@ class MergeGenerationManager {
12659
12836
  if (!state.timeline)
12660
12837
  state.timeline = [];
12661
12838
  state.timeline.push({ stage: next, at: new Date().toISOString() });
12662
- await writeTextFile(this.paths.currentYml, import_yaml10.default.stringify(state));
12839
+ await writeTextFile(this.paths.currentYml, import_yaml11.default.stringify(state));
12663
12840
  return state;
12664
12841
  }
12665
12842
  async complete() {
@@ -12675,7 +12852,7 @@ class MergeGenerationManager {
12675
12852
  const goalSlug = state.goal.toLowerCase().replace(/[^a-z0-9가-힣]+/g, "-").replace(/^-|-$/g, "").slice(0, 30);
12676
12853
  const genDirName = `${state.id}-${goalSlug}`;
12677
12854
  const genDir = this.paths.generationDir(genDirName);
12678
- await mkdir8(genDir, { recursive: true });
12855
+ await mkdir10(genDir, { recursive: true });
12679
12856
  const meta = {
12680
12857
  id: state.id,
12681
12858
  type: "merge",
@@ -12685,19 +12862,19 @@ class MergeGenerationManager {
12685
12862
  startedAt: state.startedAt,
12686
12863
  completedAt: now
12687
12864
  };
12688
- await writeTextFile(join25(genDir, "meta.yml"), import_yaml10.default.stringify(meta));
12689
- const lifeEntries = await readdir17(this.paths.life);
12865
+ await writeTextFile(join29(genDir, "meta.yml"), import_yaml11.default.stringify(meta));
12866
+ const lifeEntries = await readdir21(this.paths.life);
12690
12867
  for (const entry of lifeEntries) {
12691
12868
  if (/^\d{2}-[a-z]+(?:-[a-z]+)*\.md$/.test(entry)) {
12692
- await rename3(join25(this.paths.life, entry), join25(genDir, entry));
12869
+ await rename3(join29(this.paths.life, entry), join29(genDir, entry));
12693
12870
  }
12694
12871
  }
12695
- const backlogDir = join25(genDir, "backlog");
12696
- await mkdir8(backlogDir, { recursive: true });
12872
+ const backlogDir = join29(genDir, "backlog");
12873
+ await mkdir10(backlogDir, { recursive: true });
12697
12874
  try {
12698
- const backlogEntries = await readdir17(this.paths.backlog);
12875
+ const backlogEntries = await readdir21(this.paths.backlog);
12699
12876
  for (const entry of backlogEntries) {
12700
- await rename3(join25(this.paths.backlog, entry), join25(backlogDir, entry));
12877
+ await rename3(join29(this.paths.backlog, entry), join29(backlogDir, entry));
12701
12878
  }
12702
12879
  } catch {}
12703
12880
  await writeTextFile(this.paths.currentYml, "");
@@ -12705,7 +12882,7 @@ class MergeGenerationManager {
12705
12882
  return compression;
12706
12883
  }
12707
12884
  async save(state) {
12708
- await writeTextFile(this.paths.currentYml, import_yaml10.default.stringify(state));
12885
+ await writeTextFile(this.paths.currentYml, import_yaml11.default.stringify(state));
12709
12886
  }
12710
12887
  }
12711
12888
  function canFastForward(localLatestId, remoteLatestId, allMetas) {
@@ -12767,7 +12944,7 @@ function findCommonAncestor(idA, idB, metas) {
12767
12944
  }
12768
12945
  return null;
12769
12946
  }
12770
- var import_yaml10;
12947
+ var import_yaml11;
12771
12948
  var init_merge_generation = __esm(() => {
12772
12949
  init_merge_lifecycle();
12773
12950
  init_compression();
@@ -12777,7 +12954,7 @@ var init_merge_generation = __esm(() => {
12777
12954
  init_merge();
12778
12955
  init_lineage();
12779
12956
  init_compression();
12780
- import_yaml10 = __toESM(require_dist(), 1);
12957
+ import_yaml11 = __toESM(require_dist(), 1);
12781
12958
  });
12782
12959
 
12783
12960
  // src/cli/commands/run/merge-start.ts
@@ -12894,6 +13071,8 @@ async function execute18(paths, phase) {
12894
13071
  emitError("merge-detect", "01-detect.md does not exist. Run /reap.merge.start first.");
12895
13072
  }
12896
13073
  const detectContent = await readTextFile(detectArtifact);
13074
+ setPhaseNonce(state, "detect", "review");
13075
+ await mgm.save(state);
12897
13076
  emitOutput({
12898
13077
  status: "prompt",
12899
13078
  command: "merge-detect",
@@ -12922,24 +13101,37 @@ async function execute18(paths, phase) {
12922
13101
  });
12923
13102
  }
12924
13103
  if (phase === "complete") {
13104
+ verifyPhaseEntry("merge-detect", state, "detect", "review");
13105
+ await mgm.save(state);
13106
+ const { nonce, hash } = generateToken(state.id, state.stage);
13107
+ state.expectedHash = hash;
13108
+ state.lastNonce = nonce;
12925
13109
  const hookResults = await executeHooks(paths.hooks, "onMergeDetected", paths.projectRoot);
13110
+ const transition = await performTransition(paths, state, (s) => mgm.save(s));
13111
+ const nextCommand = `reap run merge-${transition.nextStage}`;
12926
13112
  emitOutput({
12927
13113
  status: "ok",
12928
13114
  command: "merge-detect",
12929
13115
  phase: "complete",
12930
- completed: ["gate", "artifact-read", "review", "hooks"],
13116
+ completed: ["gate", "artifact-read", "review", "hooks", "auto-transition"],
12931
13117
  context: {
12932
13118
  id: state.id,
12933
- hookResults
13119
+ hookResults,
13120
+ nextStage: transition.nextStage,
13121
+ artifactFile: transition.artifactFile,
13122
+ transitionHookResults: [...transition.stageHookResults, ...transition.transitionHookResults]
12934
13123
  },
12935
- message: "Detect stage complete. Run /reap.next to advance to mate stage."
13124
+ message: `Detect stage complete. Auto-advanced to ${transition.nextStage}. Run: ${nextCommand}`,
13125
+ nextCommand
12936
13126
  });
12937
13127
  }
12938
13128
  }
12939
13129
  var init_merge_detect = __esm(() => {
12940
13130
  init_merge_generation();
13131
+ init_generation();
12941
13132
  init_fs();
12942
13133
  init_hook_engine();
13134
+ init_stage_transition();
12943
13135
  });
12944
13136
 
12945
13137
  // src/cli/commands/run/merge-mate.ts
@@ -12959,12 +13151,16 @@ async function execute19(paths, phase) {
12959
13151
  if (state.stage !== "mate") {
12960
13152
  emitError("merge-mate", `Stage is '${state.stage}', expected 'mate'.`);
12961
13153
  }
13154
+ verifyStageEntry("merge-mate", state);
13155
+ await mgm.save(state);
12962
13156
  const detectArtifact = paths.artifact("01-detect.md");
12963
13157
  if (!await fileExists(detectArtifact)) {
12964
13158
  emitError("merge-mate", "01-detect.md does not exist. Complete detect stage first.");
12965
13159
  }
12966
13160
  if (!phase || phase === "resolve") {
12967
13161
  const detectContent = await readTextFile(detectArtifact);
13162
+ setPhaseNonce(state, "mate", "resolve");
13163
+ await mgm.save(state);
12968
13164
  emitOutput({
12969
13165
  status: "prompt",
12970
13166
  command: "merge-mate",
@@ -13004,24 +13200,37 @@ async function execute19(paths, phase) {
13004
13200
  });
13005
13201
  }
13006
13202
  if (phase === "complete") {
13203
+ verifyPhaseEntry("merge-mate", state, "mate", "resolve");
13204
+ await mgm.save(state);
13205
+ const { nonce, hash } = generateToken(state.id, state.stage);
13206
+ state.expectedHash = hash;
13207
+ state.lastNonce = nonce;
13007
13208
  const hookResults = await executeHooks(paths.hooks, "onMergeMated", paths.projectRoot);
13209
+ const transition = await performTransition(paths, state, (s) => mgm.save(s));
13210
+ const nextCommand = `reap run merge-${transition.nextStage}`;
13008
13211
  emitOutput({
13009
13212
  status: "ok",
13010
13213
  command: "merge-mate",
13011
13214
  phase: "complete",
13012
- completed: ["gate", "artifact-read", "conflict-resolution", "hooks"],
13215
+ completed: ["gate", "artifact-read", "conflict-resolution", "hooks", "auto-transition"],
13013
13216
  context: {
13014
13217
  id: state.id,
13015
- hookResults
13218
+ hookResults,
13219
+ nextStage: transition.nextStage,
13220
+ artifactFile: transition.artifactFile,
13221
+ transitionHookResults: [...transition.stageHookResults, ...transition.transitionHookResults]
13016
13222
  },
13017
- message: "Mate stage complete. Run /reap.next to advance to merge stage."
13223
+ message: `Mate stage complete. Auto-advanced to ${transition.nextStage}. Run: ${nextCommand}`,
13224
+ nextCommand
13018
13225
  });
13019
13226
  }
13020
13227
  }
13021
13228
  var init_merge_mate = __esm(() => {
13022
13229
  init_merge_generation();
13230
+ init_generation();
13023
13231
  init_fs();
13024
13232
  init_hook_engine();
13233
+ init_stage_transition();
13025
13234
  });
13026
13235
 
13027
13236
  // src/cli/commands/run/merge-merge.ts
@@ -13041,6 +13250,8 @@ async function execute20(paths, phase) {
13041
13250
  if (state.stage !== "merge") {
13042
13251
  emitError("merge-merge", `Stage is '${state.stage}', expected 'merge'.`);
13043
13252
  }
13253
+ verifyStageEntry("merge-merge", state);
13254
+ await mgm.save(state);
13044
13255
  const mateArtifact = paths.artifact("02-mate.md");
13045
13256
  if (!await fileExists(mateArtifact)) {
13046
13257
  emitError("merge-merge", "02-mate.md does not exist. Complete mate stage first.");
@@ -13049,6 +13260,8 @@ async function execute20(paths, phase) {
13049
13260
  const mateContent = await readTextFile(mateArtifact);
13050
13261
  const detectContent = await readTextFile(paths.artifact("01-detect.md"));
13051
13262
  const targetBranch = state.goal.split(" + ").pop() ?? "";
13263
+ setPhaseNonce(state, "merge", "work");
13264
+ await mgm.save(state);
13052
13265
  emitOutput({
13053
13266
  status: "prompt",
13054
13267
  command: "merge-merge",
@@ -13083,24 +13296,37 @@ async function execute20(paths, phase) {
13083
13296
  });
13084
13297
  }
13085
13298
  if (phase === "complete") {
13299
+ verifyPhaseEntry("merge-merge", state, "merge", "work");
13300
+ await mgm.save(state);
13301
+ const { nonce, hash } = generateToken(state.id, state.stage);
13302
+ state.expectedHash = hash;
13303
+ state.lastNonce = nonce;
13086
13304
  const hookResults = await executeHooks(paths.hooks, "onMergeMerged", paths.projectRoot);
13305
+ const transition = await performTransition(paths, state, (s) => mgm.save(s));
13306
+ const nextCommand = `reap run merge-${transition.nextStage}`;
13087
13307
  emitOutput({
13088
13308
  status: "ok",
13089
13309
  command: "merge-merge",
13090
13310
  phase: "complete",
13091
- completed: ["gate", "artifact-read", "source-merge", "hooks"],
13311
+ completed: ["gate", "artifact-read", "source-merge", "hooks", "auto-transition"],
13092
13312
  context: {
13093
13313
  id: state.id,
13094
- hookResults
13314
+ hookResults,
13315
+ nextStage: transition.nextStage,
13316
+ artifactFile: transition.artifactFile,
13317
+ transitionHookResults: [...transition.stageHookResults, ...transition.transitionHookResults]
13095
13318
  },
13096
- message: "Merge stage complete. Run /reap.next to advance to sync stage."
13319
+ message: `Merge stage complete. Auto-advanced to ${transition.nextStage}. Run: ${nextCommand}`,
13320
+ nextCommand
13097
13321
  });
13098
13322
  }
13099
13323
  }
13100
13324
  var init_merge_merge = __esm(() => {
13101
13325
  init_merge_generation();
13326
+ init_generation();
13102
13327
  init_fs();
13103
13328
  init_hook_engine();
13329
+ init_stage_transition();
13104
13330
  });
13105
13331
 
13106
13332
  // src/cli/commands/run/merge-sync.ts
@@ -13120,6 +13346,8 @@ async function execute21(paths, phase) {
13120
13346
  if (state.stage !== "sync") {
13121
13347
  emitError("merge-sync", `Stage is '${state.stage}', expected 'sync'.`);
13122
13348
  }
13349
+ verifyStageEntry("merge-sync", state);
13350
+ await mgm.save(state);
13123
13351
  const mergeArtifact = paths.artifact("03-merge.md");
13124
13352
  if (!await fileExists(mergeArtifact)) {
13125
13353
  emitError("merge-sync", "03-merge.md does not exist. Complete merge stage first.");
@@ -13129,6 +13357,8 @@ async function execute21(paths, phase) {
13129
13357
  const genomeConstraints = await readTextFile(paths.constraints);
13130
13358
  const genomePrinciples = await readTextFile(paths.principles);
13131
13359
  const mergeContent = await readTextFile(mergeArtifact);
13360
+ setPhaseNonce(state, "sync", "verify");
13361
+ await mgm.save(state);
13132
13362
  emitOutput({
13133
13363
  status: "prompt",
13134
13364
  command: "merge-sync",
@@ -13173,24 +13403,37 @@ async function execute21(paths, phase) {
13173
13403
  });
13174
13404
  }
13175
13405
  if (phase === "complete") {
13406
+ verifyPhaseEntry("merge-sync", state, "sync", "verify");
13407
+ await mgm.save(state);
13408
+ const { nonce, hash } = generateToken(state.id, state.stage);
13409
+ state.expectedHash = hash;
13410
+ state.lastNonce = nonce;
13176
13411
  const hookResults = await executeHooks(paths.hooks, "onMergeSynced", paths.projectRoot);
13412
+ const transition = await performTransition(paths, state, (s) => mgm.save(s));
13413
+ const nextCommand = `reap run merge-${transition.nextStage}`;
13177
13414
  emitOutput({
13178
13415
  status: "ok",
13179
13416
  command: "merge-sync",
13180
13417
  phase: "complete",
13181
- completed: ["gate", "context-collect", "sync-verify", "hooks"],
13418
+ completed: ["gate", "context-collect", "sync-verify", "hooks", "auto-transition"],
13182
13419
  context: {
13183
13420
  id: state.id,
13184
- hookResults
13421
+ hookResults,
13422
+ nextStage: transition.nextStage,
13423
+ artifactFile: transition.artifactFile,
13424
+ transitionHookResults: [...transition.stageHookResults, ...transition.transitionHookResults]
13185
13425
  },
13186
- message: "Sync stage complete. Run /reap.next to advance to validation stage."
13426
+ message: `Sync stage complete. Auto-advanced to ${transition.nextStage}. Run: ${nextCommand}`,
13427
+ nextCommand
13187
13428
  });
13188
13429
  }
13189
13430
  }
13190
13431
  var init_merge_sync = __esm(() => {
13191
13432
  init_merge_generation();
13433
+ init_generation();
13192
13434
  init_fs();
13193
13435
  init_hook_engine();
13436
+ init_stage_transition();
13194
13437
  });
13195
13438
 
13196
13439
  // src/cli/commands/run/merge-validation.ts
@@ -13210,12 +13453,16 @@ async function execute22(paths, phase) {
13210
13453
  if (state.stage !== "validation") {
13211
13454
  emitError("merge-validation", `Stage is '${state.stage}', expected 'validation'.`);
13212
13455
  }
13456
+ verifyStageEntry("merge-validation", state);
13457
+ await mgm.save(state);
13213
13458
  const syncArtifact = paths.artifact("04-sync.md");
13214
13459
  if (!await fileExists(syncArtifact)) {
13215
13460
  emitError("merge-validation", "04-sync.md does not exist. Complete sync stage first.");
13216
13461
  }
13217
13462
  if (!phase || phase === "work") {
13218
13463
  const constraintsContent = await readTextFile(paths.constraints);
13464
+ setPhaseNonce(state, "validation", "work");
13465
+ await mgm.save(state);
13219
13466
  emitOutput({
13220
13467
  status: "prompt",
13221
13468
  command: "merge-validation",
@@ -13252,28 +13499,41 @@ async function execute22(paths, phase) {
13252
13499
  });
13253
13500
  }
13254
13501
  if (phase === "complete") {
13502
+ verifyPhaseEntry("merge-validation", state, "validation", "work");
13503
+ await mgm.save(state);
13255
13504
  const validationArtifact = paths.artifact("05-validation.md");
13256
13505
  if (!await fileExists(validationArtifact)) {
13257
13506
  emitError("merge-validation", "05-validation.md does not exist. Complete validation work first.");
13258
13507
  }
13508
+ const { nonce, hash } = generateToken(state.id, state.stage);
13509
+ state.expectedHash = hash;
13510
+ state.lastNonce = nonce;
13259
13511
  const hookResults = await executeHooks(paths.hooks, "onMergeValidated", paths.projectRoot);
13512
+ const transition = await performTransition(paths, state, (s) => mgm.save(s));
13513
+ const nextCommand = `reap run merge-${transition.nextStage}`;
13260
13514
  emitOutput({
13261
13515
  status: "ok",
13262
13516
  command: "merge-validation",
13263
13517
  phase: "complete",
13264
- completed: ["gate", "context-collect", "validation-work", "hooks"],
13518
+ completed: ["gate", "context-collect", "validation-work", "hooks", "auto-transition"],
13265
13519
  context: {
13266
13520
  id: state.id,
13267
- hookResults
13521
+ hookResults,
13522
+ nextStage: transition.nextStage,
13523
+ artifactFile: transition.artifactFile,
13524
+ transitionHookResults: [...transition.stageHookResults, ...transition.transitionHookResults]
13268
13525
  },
13269
- message: "Validation stage complete. Run /reap.next to advance to completion stage."
13526
+ message: `Validation stage complete. Auto-advanced to ${transition.nextStage}. Run: ${nextCommand}`,
13527
+ nextCommand
13270
13528
  });
13271
13529
  }
13272
13530
  }
13273
13531
  var init_merge_validation = __esm(() => {
13274
13532
  init_merge_generation();
13533
+ init_generation();
13275
13534
  init_fs();
13276
13535
  init_hook_engine();
13536
+ init_stage_transition();
13277
13537
  });
13278
13538
 
13279
13539
  // src/cli/commands/run/merge-completion.ts
@@ -13293,6 +13553,8 @@ async function execute23(paths, phase) {
13293
13553
  if (state.stage !== "completion") {
13294
13554
  emitError("merge-completion", `Stage is '${state.stage}', expected 'completion'.`);
13295
13555
  }
13556
+ verifyStageEntry("merge-completion", state);
13557
+ await mgm.save(state);
13296
13558
  const validationArtifact = paths.artifact("05-validation.md");
13297
13559
  if (!await fileExists(validationArtifact)) {
13298
13560
  emitError("merge-completion", "05-validation.md does not exist. Complete validation first.");
@@ -13302,6 +13564,8 @@ async function execute23(paths, phase) {
13302
13564
  const mateContent = await readTextFile(paths.artifact("02-mate.md"));
13303
13565
  const mergeContent = await readTextFile(paths.artifact("03-merge.md"));
13304
13566
  const validationContent = await readTextFile(validationArtifact);
13567
+ setPhaseNonce(state, "completion", "retrospective");
13568
+ await mgm.save(state);
13305
13569
  emitOutput({
13306
13570
  status: "prompt",
13307
13571
  command: "merge-completion",
@@ -13332,6 +13596,8 @@ async function execute23(paths, phase) {
13332
13596
  });
13333
13597
  }
13334
13598
  if (phase === "archive") {
13599
+ verifyPhaseEntry("merge-completion", state, "completion", "retrospective");
13600
+ await mgm.save(state);
13335
13601
  const hookResults = await executeHooks(paths.hooks, "onMergeCompleted", paths.projectRoot);
13336
13602
  const submodules = checkSubmodules(paths.projectRoot);
13337
13603
  const dirtySubmodules = submodules.filter((s) => s.dirty);
@@ -13358,6 +13624,7 @@ var init_merge_completion = __esm(() => {
13358
13624
  init_fs();
13359
13625
  init_hook_engine();
13360
13626
  init_commit();
13627
+ init_stage_transition();
13361
13628
  });
13362
13629
 
13363
13630
  // src/cli/commands/run/merge-evolve.ts
@@ -13410,10 +13677,10 @@ async function execute24(paths, phase) {
13410
13677
  "- `/reap.merge.validation` -> `onMergeValidated`",
13411
13678
  "- `/reap.merge.completion` -> `onMergeCompleted` (before archiving and commit)",
13412
13679
  "",
13413
- "`/reap.next` only handles stage transitions -- it does NOT execute hooks or archiving.",
13680
+ "`--phase complete` auto-transitions to the next stage. `/reap.next` is a fallback.",
13414
13681
  "`/reap.merge.completion` handles archiving and the final commit.",
13415
13682
  "",
13416
- "### Merge Lifecycle Loop",
13683
+ "### Merge Lifecycle Loop (Auto-Transition)",
13417
13684
  "Execute the following loop until the generation is complete:",
13418
13685
  "1. Read `current.yml` to determine the current stage",
13419
13686
  "2. Execute the corresponding merge stage command:",
@@ -13423,9 +13690,17 @@ async function execute24(paths, phase) {
13423
13690
  " - `sync` -> `/reap.merge.sync`",
13424
13691
  " - `validation` -> `/reap.merge.validation`",
13425
13692
  " - `completion` -> `/reap.merge.completion`",
13426
- "3. When a stage command completes (hooks already executed by the stage command):",
13427
- " - If the current stage is `completion`: the loop ends.",
13428
- " - Otherwise: run `/reap.next` to advance, then return to step 1.",
13693
+ "3. `--phase complete` auto-transitions to the next stage.",
13694
+ " - If the stage is `completion`: the loop ends.",
13695
+ " - Otherwise: follow the `nextCommand` in the output to run the next stage.",
13696
+ "",
13697
+ "### Submodule Commit Rules",
13698
+ "- 커밋 전 반드시 `git -C tests status -s` 로 tests submodule의 dirty 상태를 확인하라.",
13699
+ "- dirty 파일이 있으면:",
13700
+ ' 1. `git -C tests add -A && git -C tests commit -m "..." && git -C tests push`',
13701
+ " 2. parent repo에서 `git add tests` 로 submodule ref 업데이트",
13702
+ " 3. 그 다음 parent repo의 나머지 파일과 함께 커밋",
13703
+ '- completion의 prompt에 "Dirty submodules detected"가 있으면 반드시 위 절차를 따르라.',
13429
13704
  "",
13430
13705
  "### Handling Issues",
13431
13706
  "- If validation fails: `/reap.back merge` or `/reap.back mate`, then resume the loop",
@@ -13527,12 +13802,174 @@ var init_merge2 = __esm(() => {
13527
13802
  init_lineage();
13528
13803
  });
13529
13804
 
13805
+ // src/cli/commands/run/evolve-recovery.ts
13806
+ var exports_evolve_recovery = {};
13807
+ __export(exports_evolve_recovery, {
13808
+ execute: () => execute26
13809
+ });
13810
+ import { join as join30 } from "path";
13811
+ import { readdir as readdir22 } from "fs/promises";
13812
+ function getFlag4(args, name) {
13813
+ const idx = args.indexOf(`--${name}`);
13814
+ return idx !== -1 && args[idx + 1] ? args[idx + 1] : undefined;
13815
+ }
13816
+ function getPositionals3(args, valueFlags) {
13817
+ const result = [];
13818
+ for (let i = 0;i < args.length; i++) {
13819
+ if (args[i].startsWith("--")) {
13820
+ const flagName = args[i].slice(2);
13821
+ if (valueFlags.includes(flagName) && i + 1 < args.length)
13822
+ i++;
13823
+ continue;
13824
+ }
13825
+ result.push(args[i]);
13826
+ }
13827
+ return result;
13828
+ }
13829
+ async function loadLineageArtifacts(paths, genId) {
13830
+ const completed = await listCompleted(paths);
13831
+ const genDir = completed.find((d) => d.startsWith(genId));
13832
+ if (genDir) {
13833
+ const dirPath = join30(paths.lineage, genDir);
13834
+ const [objective, planning, implementation, completion] = await Promise.all([
13835
+ readTextFile(join30(dirPath, "01-objective.md")),
13836
+ readTextFile(join30(dirPath, "02-planning.md")),
13837
+ readTextFile(join30(dirPath, "03-implementation.md")),
13838
+ readTextFile(join30(dirPath, "05-completion.md"))
13839
+ ]);
13840
+ return {
13841
+ objective: objective ?? "(not found)",
13842
+ planning: planning ?? "(not found)",
13843
+ implementation: implementation ?? "(not found)",
13844
+ completion: completion ?? "(not found)"
13845
+ };
13846
+ }
13847
+ try {
13848
+ const entries = await readdir22(paths.lineage);
13849
+ const compressedFile = entries.find((e) => e.startsWith(genId) && e.endsWith(".md"));
13850
+ if (compressedFile) {
13851
+ const content = await readTextFile(join30(paths.lineage, compressedFile));
13852
+ if (content) {
13853
+ return {
13854
+ objective: content,
13855
+ planning: "(compressed - see above)",
13856
+ implementation: "(compressed - see above)",
13857
+ completion: "(compressed - see above)"
13858
+ };
13859
+ }
13860
+ }
13861
+ } catch {}
13862
+ return null;
13863
+ }
13864
+ async function execute26(paths, phase, argv = []) {
13865
+ const positionals = getPositionals3(argv, ["reason"]);
13866
+ const targetGenIds = positionals;
13867
+ const reason = getFlag4(argv, "reason");
13868
+ const gm = new GenerationManager(paths);
13869
+ if (!phase || phase === "review") {
13870
+ if (targetGenIds.length === 0) {
13871
+ emitError("evolve-recovery", 'Target generation ID(s) required. Usage: reap run evolve-recovery <gen-id> [<gen-id>...] [--reason "..."]');
13872
+ }
13873
+ const state = await gm.current();
13874
+ if (state && state.id) {
13875
+ emitError("evolve-recovery", `Generation ${state.id} is in progress (stage: ${state.stage}). Complete it before starting a recovery.`);
13876
+ }
13877
+ const artifactsByGen = {};
13878
+ const notFound = [];
13879
+ for (const genId of targetGenIds) {
13880
+ const artifacts = await loadLineageArtifacts(paths, genId);
13881
+ if (artifacts) {
13882
+ artifactsByGen[genId] = artifacts;
13883
+ } else {
13884
+ notFound.push(genId);
13885
+ }
13886
+ }
13887
+ if (notFound.length > 0) {
13888
+ emitError("evolve-recovery", `Generation(s) not found in lineage: ${notFound.join(", ")}`);
13889
+ }
13890
+ const artifactSections = [];
13891
+ for (const [genId, artifacts] of Object.entries(artifactsByGen)) {
13892
+ artifactSections.push(`### Generation: ${genId}`, "", "#### 01-objective.md", artifacts.objective.slice(0, 2000), "", "#### 02-planning.md", artifacts.planning.slice(0, 2000), "", "#### 03-implementation.md", artifacts.implementation.slice(0, 2000), "", "#### 05-completion.md", artifacts.completion.slice(0, 2000), "");
13893
+ }
13894
+ emitOutput({
13895
+ status: "prompt",
13896
+ command: "evolve-recovery",
13897
+ phase: "review",
13898
+ completed: ["gate", "artifact-load"],
13899
+ context: {
13900
+ targetGenIds,
13901
+ reason: reason ?? null,
13902
+ artifactsByGen
13903
+ },
13904
+ prompt: [
13905
+ "## Recovery Review",
13906
+ "",
13907
+ "Review the following generation artifacts and determine if a recovery generation is needed.",
13908
+ "",
13909
+ "### Review Criteria",
13910
+ "(a) **Artifact Inconsistency**: Are there contradictions or misalignments between objective, planning, implementation, and completion artifacts?",
13911
+ "(b) **Structural Defects**: Are there architectural issues, missing edge cases, or technical debt introduced?",
13912
+ "(c) **Human-Specified Corrections**: " + (reason ? `The human specified: "${reason}"` : "No specific corrections requested."),
13913
+ "",
13914
+ "### Target Generation Artifacts",
13915
+ "",
13916
+ ...artifactSections,
13917
+ "",
13918
+ "### Decision",
13919
+ "If recovery is needed, run: `reap run evolve-recovery --phase create " + targetGenIds.join(" ") + (reason ? ` --reason "${reason}"` : "") + "`",
13920
+ 'If no recovery is needed, report: "No recovery needed" with your reasoning.'
13921
+ ].join(`
13922
+ `)
13923
+ });
13924
+ }
13925
+ if (phase === "create") {
13926
+ if (targetGenIds.length === 0) {
13927
+ emitError("evolve-recovery", "Target generation ID(s) required for create phase.");
13928
+ }
13929
+ const existing = await gm.current();
13930
+ if (existing && existing.id) {
13931
+ emitError("evolve-recovery", `Generation ${existing.id} is already in progress.`);
13932
+ }
13933
+ let genomeVersion = 1;
13934
+ try {
13935
+ const lineageEntries = await readdir22(paths.lineage);
13936
+ genomeVersion = lineageEntries.filter((e) => e.startsWith("gen-")).length + 1;
13937
+ } catch {}
13938
+ const goal = reason ? `Recovery: ${reason} (corrects ${targetGenIds.join(", ")})` : `Recovery for ${targetGenIds.join(", ")}`;
13939
+ const state = await gm.createRecoveryGeneration(goal, genomeVersion, targetGenIds);
13940
+ const { nonce, hash } = generateToken(state.id, state.stage);
13941
+ state.expectedHash = hash;
13942
+ await gm.save(state);
13943
+ emitOutput({
13944
+ status: "prompt",
13945
+ command: "evolve-recovery",
13946
+ phase: "created",
13947
+ completed: ["gate", "create-generation"],
13948
+ context: {
13949
+ generationId: state.id,
13950
+ goal: state.goal,
13951
+ type: state.type,
13952
+ recovers: state.recovers,
13953
+ parents: state.parents,
13954
+ genomeHash: state.genomeHash
13955
+ },
13956
+ prompt: `Recovery generation ${state.id} created (corrects: ${targetGenIds.join(", ")}). Proceed with /reap.objective or /reap.evolve.`,
13957
+ message: `Recovery generation ${state.id} started.`
13958
+ });
13959
+ }
13960
+ }
13961
+ var init_evolve_recovery = __esm(() => {
13962
+ init_generation();
13963
+ init_fs();
13964
+ init_lineage();
13965
+ });
13966
+
13530
13967
  // src/cli/commands/run/pull.ts
13531
13968
  var exports_pull = {};
13532
13969
  __export(exports_pull, {
13533
- execute: () => execute26
13970
+ execute: () => execute27
13534
13971
  });
13535
- async function execute26(paths, phase, argv = []) {
13972
+ async function execute27(paths, phase, argv = []) {
13536
13973
  const positionals = argv.filter((a) => !a.startsWith("--"));
13537
13974
  const targetBranchArg = positionals[0];
13538
13975
  const gm = new GenerationManager(paths);
@@ -13591,9 +14028,9 @@ async function execute26(paths, phase, argv = []) {
13591
14028
  const localLatest = localMetas.sort((a, b) => new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime())[0];
13592
14029
  const remoteLatest = remoteMetasRaw.sort((a, b) => new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime())[0];
13593
14030
  const allMetas = [...localMetas];
13594
- for (const rm3 of remoteMetasRaw) {
13595
- if (!allMetas.find((m) => m.id === rm3.id))
13596
- allMetas.push(rm3);
14031
+ for (const rm5 of remoteMetasRaw) {
14032
+ if (!allMetas.find((m) => m.id === rm5.id))
14033
+ allMetas.push(rm5);
13597
14034
  }
13598
14035
  const ffResult = canFastForward(localLatest.id, remoteLatest.id, allMetas);
13599
14036
  if (ffResult.fastForward) {
@@ -13653,9 +14090,9 @@ var init_pull = __esm(() => {
13653
14090
  // src/cli/commands/run/config.ts
13654
14091
  var exports_config = {};
13655
14092
  __export(exports_config, {
13656
- execute: () => execute27
14093
+ execute: () => execute28
13657
14094
  });
13658
- async function execute27(paths) {
14095
+ async function execute28(paths) {
13659
14096
  const config = await ConfigManager.read(paths);
13660
14097
  const lines = [
13661
14098
  `REAP Configuration (${paths.config})`,
@@ -13684,25 +14121,119 @@ var init_config2 = __esm(() => {
13684
14121
  init_config();
13685
14122
  });
13686
14123
 
14124
+ // src/cli/commands/run/update-genome.ts
14125
+ var exports_update_genome = {};
14126
+ __export(exports_update_genome, {
14127
+ execute: () => execute29
14128
+ });
14129
+ async function execute29(paths, phase) {
14130
+ const gm = new GenerationManager(paths);
14131
+ const current = await gm.current();
14132
+ if (current !== null) {
14133
+ emitError(COMMAND, `Active generation exists (${current.id}). Cannot run update-genome during a generation.`);
14134
+ }
14135
+ const backlogItems = await scanBacklog(paths.backlog);
14136
+ const pending = backlogItems.filter((b) => b.type === "genome-change" && b.status === "pending");
14137
+ if (phase === "apply") {
14138
+ await applyPhase(paths, pending);
14139
+ } else {
14140
+ scanPhase(pending);
14141
+ }
14142
+ }
14143
+ function scanPhase(pending) {
14144
+ if (pending.length === 0) {
14145
+ emitOutput({
14146
+ status: "ok",
14147
+ command: COMMAND,
14148
+ phase: "scan",
14149
+ completed: ["gate", "scan"],
14150
+ message: "No pending genome changes."
14151
+ });
14152
+ }
14153
+ emitOutput({
14154
+ status: "prompt",
14155
+ command: COMMAND,
14156
+ phase: "scan",
14157
+ completed: ["gate", "scan"],
14158
+ context: {
14159
+ pendingCount: pending.length,
14160
+ items: pending.map((b) => ({
14161
+ filename: b.filename,
14162
+ title: b.title,
14163
+ body: b.body
14164
+ }))
14165
+ },
14166
+ prompt: [
14167
+ "Pending genome-change backlog items are listed in context.items.",
14168
+ "For each item, apply the described changes to the corresponding .reap/genome/ files.",
14169
+ "Only modify files under .reap/genome/. Do NOT modify source code.",
14170
+ "After all changes are applied, run: reap run update-genome --phase apply"
14171
+ ].join(`
14172
+ `),
14173
+ nextCommand: "reap run update-genome --phase apply"
14174
+ });
14175
+ }
14176
+ async function applyPhase(paths, pending) {
14177
+ if (pending.length === 0) {
14178
+ emitOutput({
14179
+ status: "ok",
14180
+ command: COMMAND,
14181
+ phase: "apply",
14182
+ completed: ["gate", "scan", "apply"],
14183
+ message: "No pending genome changes to apply."
14184
+ });
14185
+ }
14186
+ for (const item of pending) {
14187
+ await markBacklogConsumed(paths.backlog, item.filename, "update-genome");
14188
+ }
14189
+ const config = await ConfigManager.read(paths);
14190
+ config.genomeVersion = (config.genomeVersion ?? 0) + 1;
14191
+ await ConfigManager.write(paths, config);
14192
+ emitOutput({
14193
+ status: "ok",
14194
+ command: COMMAND,
14195
+ phase: "apply",
14196
+ completed: ["gate", "scan", "apply", "version-bump"],
14197
+ context: {
14198
+ consumedCount: pending.length,
14199
+ consumedFiles: pending.map((b) => b.filename),
14200
+ genomeVersion: config.genomeVersion
14201
+ },
14202
+ message: `Applied ${pending.length} genome-change(s). genomeVersion → v${config.genomeVersion}. Please commit the changes.`,
14203
+ prompt: [
14204
+ `${pending.length} genome-change backlog item(s) consumed. genomeVersion bumped to v${config.genomeVersion}.`,
14205
+ "Commit the genome changes with message:",
14206
+ ` chore: update-genome v${config.genomeVersion} — ${pending.map((b) => b.title).join(", ")}`
14207
+ ].join(`
14208
+ `)
14209
+ });
14210
+ }
14211
+ var COMMAND = "update-genome";
14212
+ var init_update_genome = __esm(() => {
14213
+ init_config();
14214
+ init_generation();
14215
+ init_backlog();
14216
+ });
14217
+
13687
14218
  // src/cli/commands/run/refresh-knowledge.ts
13688
14219
  var exports_refresh_knowledge = {};
13689
14220
  __export(exports_refresh_knowledge, {
13690
- execute: () => execute28
14221
+ execute: () => execute30
13691
14222
  });
13692
- import { join as join26 } from "path";
13693
- import { readdir as readdir18 } from "fs/promises";
14223
+ import { join as join31 } from "path";
14224
+ import { readdir as readdir23 } from "fs/promises";
13694
14225
  async function loadGenome(genomeDir) {
13695
14226
  let content = "";
13696
14227
  let l1Lines = 0;
13697
14228
  let smLimit = null;
13698
- const smContent = await readTextFile(join26(genomeDir, "source-map.md"));
14229
+ const smContent = await readTextFile(join31(genomeDir, "source-map.md"));
13699
14230
  if (smContent) {
13700
14231
  const limitMatch = smContent.match(/줄 수 한도:\s*~?(\d+)줄/);
13701
14232
  if (limitMatch)
13702
14233
  smLimit = parseInt(limitMatch[1], 10);
13703
14234
  }
13704
14235
  for (const file of L1_FILES) {
13705
- const fileContent = await readTextFile(join26(genomeDir, file));
14236
+ const fileContent = await readTextFile(join31(genomeDir, file));
13706
14237
  if (!fileContent)
13707
14238
  continue;
13708
14239
  const lines = fileContent.split(`
@@ -13724,14 +14255,14 @@ ${fileContent.split(`
13724
14255
  `;
13725
14256
  }
13726
14257
  }
13727
- const domainDir = join26(genomeDir, "domain");
14258
+ const domainDir = join31(genomeDir, "domain");
13728
14259
  if (await fileExists(domainDir)) {
13729
14260
  let l2Lines = 0;
13730
14261
  let l2Overflow = false;
13731
14262
  try {
13732
- const domainFiles = (await readdir18(domainDir)).filter((f) => f.endsWith(".md")).sort();
14263
+ const domainFiles = (await readdir23(domainDir)).filter((f) => f.endsWith(".md")).sort();
13733
14264
  for (const file of domainFiles) {
13734
- const fileContent = await readTextFile(join26(domainDir, file));
14265
+ const fileContent = await readTextFile(join31(domainDir, file));
13735
14266
  if (!fileContent)
13736
14267
  continue;
13737
14268
  const lines = fileContent.split(`
@@ -13781,8 +14312,8 @@ function buildStrictSection(strict, genStage) {
13781
14312
  }
13782
14313
  return sections;
13783
14314
  }
13784
- async function execute28(paths) {
13785
- const guidePath = join26(ReapPaths.packageHooksDir, "reap-guide.md");
14315
+ async function execute30(paths) {
14316
+ const guidePath = join31(ReapPaths.packageHooksDir, "reap-guide.md");
13786
14317
  const reapGuide = await readTextFile(guidePath) || "";
13787
14318
  const { content: genomeContent } = await loadGenome(paths.genome);
13788
14319
  const envSummary = await readTextFile(paths.environmentSummary) || "";
@@ -13872,7 +14403,7 @@ async function runCommand(command, phase, argv = []) {
13872
14403
  try {
13873
14404
  const config = await ConfigManager.read(paths);
13874
14405
  if (config.autoIssueReport) {
13875
- const version = "0.14.0";
14406
+ const version = "0.15.1";
13876
14407
  const errMsg = err instanceof Error ? err.message : String(err);
13877
14408
  const title = `[auto] reap run ${command}: ${errMsg.slice(0, 80)}`;
13878
14409
  const body = [
@@ -13918,8 +14449,10 @@ var init_run = __esm(() => {
13918
14449
  "merge-completion": () => Promise.resolve().then(() => (init_merge_completion(), exports_merge_completion)),
13919
14450
  "merge-evolve": () => Promise.resolve().then(() => (init_merge_evolve(), exports_merge_evolve)),
13920
14451
  merge: () => Promise.resolve().then(() => (init_merge2(), exports_merge)),
14452
+ "evolve-recovery": () => Promise.resolve().then(() => (init_evolve_recovery(), exports_evolve_recovery)),
13921
14453
  pull: () => Promise.resolve().then(() => (init_pull(), exports_pull)),
13922
14454
  config: () => Promise.resolve().then(() => (init_config2(), exports_config)),
14455
+ "update-genome": () => Promise.resolve().then(() => (init_update_genome(), exports_update_genome)),
13923
14456
  refreshKnowledge: () => Promise.resolve().then(() => (init_refresh_knowledge(), exports_refresh_knowledge))
13924
14457
  };
13925
14458
  });
@@ -13940,11 +14473,14 @@ var {
13940
14473
  Help
13941
14474
  } = import__.default;
13942
14475
 
14476
+ // src/cli/index.ts
14477
+ import { createInterface } from "readline";
14478
+
13943
14479
  // src/cli/commands/init.ts
13944
14480
  init_paths();
13945
14481
  init_config();
13946
- import { mkdir as mkdir3, readdir as readdir4, chmod } from "fs/promises";
13947
- import { join as join5 } from "path";
14482
+ import { mkdir as mkdir4, readdir as readdir5, chmod } from "fs/promises";
14483
+ import { join as join6 } from "path";
13948
14484
 
13949
14485
  // src/core/agents/claude-code.ts
13950
14486
  init_fs();
@@ -14394,6 +14930,42 @@ class AgentRegistry {
14394
14930
 
14395
14931
  // src/cli/commands/init.ts
14396
14932
  init_fs();
14933
+
14934
+ // src/core/skills.ts
14935
+ init_paths();
14936
+ init_fs();
14937
+ import { readdir as readdir3, mkdir as mkdir3 } from "fs/promises";
14938
+ import { join as join4 } from "path";
14939
+ async function syncSkillsToProject(projectRoot, dryRun = false) {
14940
+ const projectClaudeSkills = join4(projectRoot, ".claude", "skills");
14941
+ const reapCmdFiles = (await readdir3(ReapPaths.userReapCommands)).filter((f) => f.startsWith("reap.") && f.endsWith(".md"));
14942
+ let installed = 0;
14943
+ for (const file of reapCmdFiles) {
14944
+ const src = await readTextFileOrThrow(join4(ReapPaths.userReapCommands, file));
14945
+ const name = file.replace(/\.md$/, "");
14946
+ const skillDir = join4(projectClaudeSkills, name);
14947
+ const skillFile = join4(skillDir, "SKILL.md");
14948
+ const fmMatch = src.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
14949
+ const description = fmMatch ? fmMatch[1].match(/^description:\s*"?([^"\n]*)"?/m)?.[1]?.trim() ?? "" : "";
14950
+ const body = fmMatch ? fmMatch[2] : src;
14951
+ const skillContent = `---
14952
+ name: ${name}
14953
+ description: "${description}"
14954
+ ---
14955
+ ${body}`;
14956
+ const existing = await readTextFile(skillFile);
14957
+ if (existing !== null && existing === skillContent)
14958
+ continue;
14959
+ if (!dryRun) {
14960
+ await mkdir3(skillDir, { recursive: true });
14961
+ await writeTextFile(skillFile, skillContent);
14962
+ }
14963
+ installed++;
14964
+ }
14965
+ return { installed, total: reapCmdFiles.length };
14966
+ }
14967
+
14968
+ // src/cli/commands/init.ts
14397
14969
  var COMMAND_NAMES = [
14398
14970
  "reap.objective",
14399
14971
  "reap.planning",
@@ -14420,10 +14992,12 @@ var COMMAND_NAMES = [
14420
14992
  "reap.merge.validation",
14421
14993
  "reap.merge.completion",
14422
14994
  "reap.merge.evolve",
14995
+ "reap.evolve.recovery",
14423
14996
  "reap.merge",
14424
14997
  "reap.pull",
14425
14998
  "reap.push",
14426
14999
  "reap.config",
15000
+ "reap.update-genome",
14427
15001
  "reap.refreshKnowledge"
14428
15002
  ];
14429
15003
  async function initProject(projectRoot, projectName, entryMode, preset, onProgress) {
@@ -14433,21 +15007,21 @@ async function initProject(projectRoot, projectName, entryMode, preset, onProgre
14433
15007
  throw new Error(".reap/ already exists. This is already a REAP project.");
14434
15008
  }
14435
15009
  if (preset) {
14436
- const presetDir = join5(ReapPaths.packageTemplatesDir, "presets", preset);
14437
- const presetExists = await fileExists(join5(presetDir, "principles.md"));
15010
+ const presetDir = join6(ReapPaths.packageTemplatesDir, "presets", preset);
15011
+ const presetExists = await fileExists(join6(presetDir, "principles.md"));
14438
15012
  if (!presetExists) {
14439
15013
  throw new Error(`Unknown preset: "${preset}". Available presets: bun-hono-react`);
14440
15014
  }
14441
15015
  }
14442
15016
  log("Creating .reap/ directory structure...");
14443
- await mkdir3(paths.genome, { recursive: true });
14444
- await mkdir3(paths.domain, { recursive: true });
14445
- await mkdir3(paths.environment, { recursive: true });
14446
- await mkdir3(join5(paths.environment, "docs"), { recursive: true });
14447
- await mkdir3(join5(paths.environment, "resources"), { recursive: true });
14448
- await mkdir3(paths.life, { recursive: true });
14449
- await mkdir3(paths.backlog, { recursive: true });
14450
- await mkdir3(paths.lineage, { recursive: true });
15017
+ await mkdir4(paths.genome, { recursive: true });
15018
+ await mkdir4(paths.domain, { recursive: true });
15019
+ await mkdir4(paths.environment, { recursive: true });
15020
+ await mkdir4(join6(paths.environment, "docs"), { recursive: true });
15021
+ await mkdir4(join6(paths.environment, "resources"), { recursive: true });
15022
+ await mkdir4(paths.life, { recursive: true });
15023
+ await mkdir4(paths.backlog, { recursive: true });
15024
+ await mkdir4(paths.lineage, { recursive: true });
14451
15025
  log("Writing config.yml...");
14452
15026
  let hasGhCli = false;
14453
15027
  try {
@@ -14460,7 +15034,7 @@ async function initProject(projectRoot, projectName, entryMode, preset, onProgre
14460
15034
  }
14461
15035
  const detectedLanguage = await AgentRegistry.readLanguage();
14462
15036
  const config = {
14463
- version: "0.14.0",
15037
+ version: "0.15.1",
14464
15038
  project: projectName,
14465
15039
  entryMode,
14466
15040
  strict: false,
@@ -14473,10 +15047,10 @@ async function initProject(projectRoot, projectName, entryMode, preset, onProgre
14473
15047
  await ConfigManager.write(paths, config);
14474
15048
  log("Setting up Genome templates...");
14475
15049
  const genomeTemplates = ["principles.md", "conventions.md", "constraints.md", "source-map.md"];
14476
- const genomeSourceDir = preset ? join5(ReapPaths.packageTemplatesDir, "presets", preset) : ReapPaths.packageGenomeDir;
15050
+ const genomeSourceDir = preset ? join6(ReapPaths.packageTemplatesDir, "presets", preset) : ReapPaths.packageGenomeDir;
14477
15051
  for (const file of genomeTemplates) {
14478
- const src = join5(genomeSourceDir, file);
14479
- const dest = join5(paths.genome, file);
15052
+ const src = join6(genomeSourceDir, file);
15053
+ const dest = join6(paths.genome, file);
14480
15054
  await writeTextFile(dest, await readTextFileOrThrow(src));
14481
15055
  }
14482
15056
  if ((entryMode === "adoption" || entryMode === "migration") && !preset) {
@@ -14485,35 +15059,47 @@ async function initProject(projectRoot, projectName, entryMode, preset, onProgre
14485
15059
  await syncGenomeFromProject2(projectRoot, paths.genome, log);
14486
15060
  }
14487
15061
  log("Installing artifact templates...");
14488
- await mkdir3(ReapPaths.userReapTemplates, { recursive: true });
15062
+ await mkdir4(ReapPaths.userReapTemplates, { recursive: true });
14489
15063
  const artifactFiles = ["01-objective.md", "02-planning.md", "03-implementation.md", "04-validation.md", "05-completion.md"];
14490
15064
  for (const file of artifactFiles) {
14491
- const src = join5(ReapPaths.packageArtifactsDir, file);
14492
- const dest = join5(ReapPaths.userReapTemplates, file);
15065
+ const src = join6(ReapPaths.packageArtifactsDir, file);
15066
+ const dest = join6(ReapPaths.userReapTemplates, file);
14493
15067
  await writeTextFile(dest, await readTextFileOrThrow(src));
14494
15068
  }
14495
- const domainGuideSrc = join5(ReapPaths.packageGenomeDir, "domain/README.md");
14496
- const domainGuideDest = join5(ReapPaths.userReapTemplates, "domain-guide.md");
15069
+ const domainGuideSrc = join6(ReapPaths.packageGenomeDir, "domain/README.md");
15070
+ const domainGuideDest = join6(ReapPaths.userReapTemplates, "domain-guide.md");
14497
15071
  await writeTextFile(domainGuideDest, await readTextFileOrThrow(domainGuideSrc));
14498
- const mergeTemplatesDir = join5(ReapPaths.userReapTemplates, "merge");
14499
- await mkdir3(mergeTemplatesDir, { recursive: true });
15072
+ const mergeTemplatesDir = join6(ReapPaths.userReapTemplates, "merge");
15073
+ await mkdir4(mergeTemplatesDir, { recursive: true });
14500
15074
  const mergeArtifactFiles = ["01-detect.md", "02-mate.md", "03-merge.md", "04-sync.md", "05-validation.md", "06-completion.md"];
14501
- const mergeSourceDir = join5(ReapPaths.packageArtifactsDir, "merge");
15075
+ const mergeSourceDir = join6(ReapPaths.packageArtifactsDir, "merge");
14502
15076
  for (const file of mergeArtifactFiles) {
14503
- const src = join5(mergeSourceDir, file);
14504
- const dest = join5(mergeTemplatesDir, file);
15077
+ const src = join6(mergeSourceDir, file);
15078
+ const dest = join6(mergeTemplatesDir, file);
14505
15079
  await writeTextFile(dest, await readTextFileOrThrow(src));
14506
15080
  }
14507
15081
  log("Installing hook conditions...");
14508
- const conditionsSourceDir = join5(ReapPaths.packageTemplatesDir, "conditions");
14509
- const conditionsDestDir = join5(paths.hooks, "conditions");
14510
- await mkdir3(conditionsDestDir, { recursive: true });
14511
- const conditionFiles = await readdir4(conditionsSourceDir);
15082
+ const conditionsSourceDir = join6(ReapPaths.packageTemplatesDir, "conditions");
15083
+ const conditionsDestDir = join6(paths.hooks, "conditions");
15084
+ await mkdir4(conditionsDestDir, { recursive: true });
15085
+ const conditionFiles = await readdir5(conditionsSourceDir);
14512
15086
  for (const file of conditionFiles) {
14513
15087
  if (!file.endsWith(".sh"))
14514
15088
  continue;
14515
- const src = join5(conditionsSourceDir, file);
14516
- const dest = join5(conditionsDestDir, file);
15089
+ const src = join6(conditionsSourceDir, file);
15090
+ const dest = join6(conditionsDestDir, file);
15091
+ await writeTextFile(dest, await readTextFileOrThrow(src));
15092
+ await chmod(dest, 493);
15093
+ }
15094
+ log("Installing hook scripts...");
15095
+ const hooksSourceDir = join6(ReapPaths.packageTemplatesDir, "hooks");
15096
+ const { readdir: readdirAsync } = await import("fs/promises");
15097
+ const hookFiles = await readdirAsync(hooksSourceDir);
15098
+ for (const file of hookFiles) {
15099
+ if (!file.endsWith(".sh") || !file.startsWith("on"))
15100
+ continue;
15101
+ const src = join6(hooksSourceDir, file);
15102
+ const dest = join6(paths.hooks, file);
14517
15103
  await writeTextFile(dest, await readTextFileOrThrow(src));
14518
15104
  await chmod(dest, 493);
14519
15105
  }
@@ -14533,6 +15119,11 @@ async function initProject(projectRoot, projectName, entryMode, preset, onProgre
14533
15119
  if (detectedAgents.length === 0) {
14534
15120
  log(" No AI agents detected.");
14535
15121
  }
15122
+ log("Syncing skills to project...");
15123
+ const skillsResult = await syncSkillsToProject(projectRoot);
15124
+ if (skillsResult.installed > 0) {
15125
+ log(` .claude/skills/ (${skillsResult.installed} synced)`);
15126
+ }
14536
15127
  const autoSynced = (entryMode === "adoption" || entryMode === "migration") && !preset;
14537
15128
  if (!autoSynced) {
14538
15129
  log(`
@@ -14543,8 +15134,8 @@ async function initProject(projectRoot, projectName, entryMode, preset, onProgre
14543
15134
 
14544
15135
  // src/cli/commands/update.ts
14545
15136
  init_paths();
14546
- import { readdir as readdir9, unlink as unlink4, rm as rm2, mkdir as mkdir6 } from "fs/promises";
14547
- import { join as join10 } from "path";
15137
+ import { readdir as readdir10, unlink as unlink4, rm as rm2, mkdir as mkdir7 } from "fs/promises";
15138
+ import { join as join11 } from "path";
14548
15139
  import { execSync as execSync2 } from "child_process";
14549
15140
 
14550
15141
  // src/core/hooks.ts
@@ -14571,15 +15162,15 @@ init_config();
14571
15162
  init_fs();
14572
15163
  init_generation();
14573
15164
  var import_yaml5 = __toESM(require_dist(), 1);
14574
- import { readdir as readdir8, rename as rename2 } from "fs/promises";
14575
- import { join as join9 } from "path";
15165
+ import { readdir as readdir9, rename as rename2 } from "fs/promises";
15166
+ import { join as join10 } from "path";
14576
15167
  async function needsMigration(paths) {
14577
15168
  try {
14578
- const entries = await readdir8(paths.lineage, { withFileTypes: true });
15169
+ const entries = await readdir9(paths.lineage, { withFileTypes: true });
14579
15170
  for (const entry of entries) {
14580
15171
  if (!entry.isDirectory() || !entry.name.startsWith("gen-"))
14581
15172
  continue;
14582
- const metaPath = join9(paths.lineage, entry.name, "meta.yml");
15173
+ const metaPath = join10(paths.lineage, entry.name, "meta.yml");
14583
15174
  const content = await readTextFile(metaPath);
14584
15175
  if (content === null)
14585
15176
  return true;
@@ -14593,18 +15184,18 @@ async function migrateLineage(paths) {
14593
15184
  const result = { migrated: [], skipped: [], errors: [] };
14594
15185
  let entries;
14595
15186
  try {
14596
- const dirEntries = await readdir8(paths.lineage, { withFileTypes: true });
15187
+ const dirEntries = await readdir9(paths.lineage, { withFileTypes: true });
14597
15188
  entries = dirEntries.filter((e) => e.isDirectory() && e.name.startsWith("gen-")).map((e) => e.name).sort();
14598
15189
  } catch {
14599
15190
  return result;
14600
15191
  }
14601
15192
  const plan = [];
14602
15193
  for (const dirName of entries) {
14603
- const metaPath = join9(paths.lineage, dirName, "meta.yml");
15194
+ const metaPath = join10(paths.lineage, dirName, "meta.yml");
14604
15195
  const metaContent = await readTextFile(metaPath);
14605
15196
  const seq = parseGenSeq(dirName);
14606
15197
  let goal = "";
14607
- const objContent = await readTextFile(join9(paths.lineage, dirName, "01-objective.md"));
15198
+ const objContent = await readTextFile(join10(paths.lineage, dirName, "01-objective.md"));
14608
15199
  if (objContent) {
14609
15200
  const goalMatch = objContent.match(/## Goal\n+([\s\S]*?)(?=\n##)/);
14610
15201
  if (goalMatch)
@@ -14619,7 +15210,7 @@ async function migrateLineage(paths) {
14619
15210
  let prevId = null;
14620
15211
  for (const entry of plan) {
14621
15212
  if (entry.hasMeta) {
14622
- const metaContent = await readTextFile(join9(paths.lineage, entry.dirName, "meta.yml"));
15213
+ const metaContent = await readTextFile(join10(paths.lineage, entry.dirName, "meta.yml"));
14623
15214
  if (metaContent) {
14624
15215
  const meta = import_yaml5.default.parse(metaContent);
14625
15216
  prevId = meta.id;
@@ -14640,11 +15231,11 @@ async function migrateLineage(paths) {
14640
15231
  startedAt: `legacy-${entry.seq}`,
14641
15232
  completedAt: `legacy-${entry.seq}`
14642
15233
  };
14643
- await writeTextFile(join9(paths.lineage, entry.dirName, "meta.yml"), import_yaml5.default.stringify(meta));
15234
+ await writeTextFile(join10(paths.lineage, entry.dirName, "meta.yml"), import_yaml5.default.stringify(meta));
14644
15235
  const oldSlug = entry.dirName.replace(/^gen-\d{3}/, "");
14645
15236
  const newDirName = `${newId}${oldSlug}`;
14646
15237
  if (newDirName !== entry.dirName) {
14647
- await rename2(join9(paths.lineage, entry.dirName), join9(paths.lineage, newDirName));
15238
+ await rename2(join10(paths.lineage, entry.dirName), join10(paths.lineage, newDirName));
14648
15239
  }
14649
15240
  prevId = newId;
14650
15241
  result.migrated.push(`${entry.dirName} → ${newDirName}`);
@@ -14921,13 +15512,13 @@ async function updateProject(projectRoot, dryRun = false) {
14921
15512
  const config = await paths.isReapProject() ? await ConfigManager.read(paths) : null;
14922
15513
  const adapters = await AgentRegistry.getActiveAdapters(config ?? undefined);
14923
15514
  const commandsDir = ReapPaths.packageCommandsDir;
14924
- const commandFiles = await readdir9(commandsDir);
14925
- await mkdir6(ReapPaths.userReapCommands, { recursive: true });
15515
+ const commandFiles = await readdir10(commandsDir);
15516
+ await mkdir7(ReapPaths.userReapCommands, { recursive: true });
14926
15517
  for (const file of commandFiles) {
14927
15518
  if (!file.endsWith(".md"))
14928
15519
  continue;
14929
- const src = await readTextFileOrThrow(join10(commandsDir, file));
14930
- const dest = join10(ReapPaths.userReapCommands, file);
15520
+ const src = await readTextFileOrThrow(join11(commandsDir, file));
15521
+ const dest = join11(ReapPaths.userReapCommands, file);
14931
15522
  const existing = await readTextFile(dest);
14932
15523
  if (existing !== null && existing === src) {
14933
15524
  result.skipped.push(`~/.reap/commands/${file}`);
@@ -14941,11 +15532,11 @@ async function updateProject(projectRoot, dryRun = false) {
14941
15532
  const agentCmdDir = adapter.getCommandsDir();
14942
15533
  const label = adapter.displayName;
14943
15534
  try {
14944
- const existing = await readdir9(agentCmdDir);
15535
+ const existing = await readdir10(agentCmdDir);
14945
15536
  for (const file of existing) {
14946
15537
  if (!file.startsWith("reap.") || !file.endsWith(".md"))
14947
15538
  continue;
14948
- const filePath = join10(agentCmdDir, file);
15539
+ const filePath = join11(agentCmdDir, file);
14949
15540
  const content = await readTextFile(filePath);
14950
15541
  if (content !== null && content.includes("redirected to ~/.reap/commands/")) {
14951
15542
  if (!dryRun)
@@ -14963,11 +15554,11 @@ async function updateProject(projectRoot, dryRun = false) {
14963
15554
  }
14964
15555
  }
14965
15556
  }
14966
- await mkdir6(ReapPaths.userReapTemplates, { recursive: true });
15557
+ await mkdir7(ReapPaths.userReapTemplates, { recursive: true });
14967
15558
  const artifactFiles = ["01-objective.md", "02-planning.md", "03-implementation.md", "04-validation.md", "05-completion.md"];
14968
15559
  for (const file of artifactFiles) {
14969
- const src = await readTextFileOrThrow(join10(ReapPaths.packageArtifactsDir, file));
14970
- const dest = join10(ReapPaths.userReapTemplates, file);
15560
+ const src = await readTextFileOrThrow(join11(ReapPaths.packageArtifactsDir, file));
15561
+ const dest = join11(ReapPaths.userReapTemplates, file);
14971
15562
  const existingContent = await readTextFile(dest);
14972
15563
  if (existingContent !== null && existingContent === src) {
14973
15564
  result.skipped.push(`~/.reap/templates/${file}`);
@@ -14977,8 +15568,8 @@ async function updateProject(projectRoot, dryRun = false) {
14977
15568
  result.updated.push(`~/.reap/templates/${file}`);
14978
15569
  }
14979
15570
  }
14980
- const domainGuideSrc = await readTextFileOrThrow(join10(ReapPaths.packageGenomeDir, "domain/README.md"));
14981
- const domainGuideDest = join10(ReapPaths.userReapTemplates, "domain-guide.md");
15571
+ const domainGuideSrc = await readTextFileOrThrow(join11(ReapPaths.packageGenomeDir, "domain/README.md"));
15572
+ const domainGuideDest = join11(ReapPaths.userReapTemplates, "domain-guide.md");
14982
15573
  const domainExistingContent = await readTextFile(domainGuideDest);
14983
15574
  if (domainExistingContent !== null && domainExistingContent === domainGuideSrc) {
14984
15575
  result.skipped.push(`~/.reap/templates/domain-guide.md`);
@@ -14987,13 +15578,13 @@ async function updateProject(projectRoot, dryRun = false) {
14987
15578
  await writeTextFile(domainGuideDest, domainGuideSrc);
14988
15579
  result.updated.push(`~/.reap/templates/domain-guide.md`);
14989
15580
  }
14990
- const mergeTemplatesDir = join10(ReapPaths.userReapTemplates, "merge");
14991
- await mkdir6(mergeTemplatesDir, { recursive: true });
15581
+ const mergeTemplatesDir = join11(ReapPaths.userReapTemplates, "merge");
15582
+ await mkdir7(mergeTemplatesDir, { recursive: true });
14992
15583
  const mergeArtifactFiles = ["01-detect.md", "02-mate.md", "03-merge.md", "04-sync.md", "05-validation.md", "06-completion.md"];
14993
- const mergeSourceDir = join10(ReapPaths.packageArtifactsDir, "merge");
15584
+ const mergeSourceDir = join11(ReapPaths.packageArtifactsDir, "merge");
14994
15585
  for (const file of mergeArtifactFiles) {
14995
- const src = await readTextFileOrThrow(join10(mergeSourceDir, file));
14996
- const dest = join10(mergeTemplatesDir, file);
15586
+ const src = await readTextFileOrThrow(join11(mergeSourceDir, file));
15587
+ const dest = join11(mergeTemplatesDir, file);
14997
15588
  const existing = await readTextFile(dest);
14998
15589
  if (existing !== null && existing === src) {
14999
15590
  result.skipped.push(`~/.reap/templates/merge/${file}`);
@@ -15034,47 +15625,23 @@ async function updateProject(projectRoot, dryRun = false) {
15034
15625
  result.updated.push(`Config: added ${backfillResult.added.join(", ")}`);
15035
15626
  }
15036
15627
  }
15037
- const projectClaudeSkills = join10(paths.projectRoot, ".claude", "skills");
15038
- const reapCmdFiles = (await readdir9(ReapPaths.userReapCommands)).filter((f) => f.startsWith("reap.") && f.endsWith(".md"));
15039
- let cmdInstalled = 0;
15040
- for (const file of reapCmdFiles) {
15041
- const src = await readTextFileOrThrow(join10(ReapPaths.userReapCommands, file));
15042
- const name = file.replace(/\.md$/, "");
15043
- const skillDir = join10(projectClaudeSkills, name);
15044
- const skillFile = join10(skillDir, "SKILL.md");
15045
- const fmMatch = src.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
15046
- const description = fmMatch ? fmMatch[1].match(/^description:\s*"?([^"\n]*)"?/m)?.[1]?.trim() ?? "" : "";
15047
- const body = fmMatch ? fmMatch[2] : src;
15048
- const skillContent = `---
15049
- name: ${name}
15050
- description: "${description}"
15051
- ---
15052
- ${body}`;
15053
- const existing = await readTextFile(skillFile);
15054
- if (existing !== null && existing === skillContent)
15055
- continue;
15056
- if (!dryRun) {
15057
- await mkdir6(skillDir, { recursive: true });
15058
- await writeTextFile(skillFile, skillContent);
15059
- }
15060
- cmdInstalled++;
15061
- }
15062
- if (cmdInstalled > 0) {
15063
- result.updated.push(`.claude/skills/ (${cmdInstalled} synced)`);
15628
+ const skillsResult = await syncSkillsToProject(paths.projectRoot, dryRun);
15629
+ if (skillsResult.installed > 0) {
15630
+ result.updated.push(`.claude/skills/ (${skillsResult.installed} synced)`);
15064
15631
  } else {
15065
- result.skipped.push(`.claude/skills/ (${reapCmdFiles.length} unchanged)`);
15632
+ result.skipped.push(`.claude/skills/ (${skillsResult.total} unchanged)`);
15066
15633
  }
15067
- const projectClaudeCommands = join10(paths.projectRoot, ".claude", "commands");
15634
+ const projectClaudeCommands = join11(paths.projectRoot, ".claude", "commands");
15068
15635
  try {
15069
- const legacyFiles = (await readdir9(projectClaudeCommands)).filter((f) => f.startsWith("reap.") && f.endsWith(".md"));
15636
+ const legacyFiles = (await readdir10(projectClaudeCommands)).filter((f) => f.startsWith("reap.") && f.endsWith(".md"));
15070
15637
  for (const file of legacyFiles) {
15071
15638
  if (!dryRun)
15072
- await unlink4(join10(projectClaudeCommands, file));
15639
+ await unlink4(join11(projectClaudeCommands, file));
15073
15640
  result.removed.push(`.claude/commands/${file} (legacy)`);
15074
15641
  }
15075
15642
  } catch {}
15076
15643
  await migrateLegacyFiles(paths, dryRun, result);
15077
- const currentVersion = "0.14.0";
15644
+ const currentVersion = "0.15.1";
15078
15645
  const migrationResult = await MigrationRunner.run(paths, currentVersion, dryRun);
15079
15646
  for (const m of migrationResult.migrated) {
15080
15647
  result.updated.push(`[migration] ${m}`);
@@ -15150,7 +15717,7 @@ async function migrateLegacyFiles(paths, dryRun, result) {
15150
15717
  }
15151
15718
  async function removeDirIfExists(dirPath, label, dryRun, result) {
15152
15719
  try {
15153
- const entries = await readdir9(dirPath);
15720
+ const entries = await readdir10(dirPath);
15154
15721
  if (entries.length > 0 || true) {
15155
15722
  if (!dryRun)
15156
15723
  await rm2(dirPath, { recursive: true });
@@ -15173,6 +15740,7 @@ async function getStatus(projectRoot) {
15173
15740
  version: config.version,
15174
15741
  project: config.project,
15175
15742
  entryMode: config.entryMode,
15743
+ lastSyncedGeneration: config.lastSyncedGeneration,
15176
15744
  generation: current ? {
15177
15745
  id: current.id,
15178
15746
  goal: current.goal,
@@ -15191,21 +15759,361 @@ async function getStatus(projectRoot) {
15191
15759
  init_paths();
15192
15760
  init_lifecycle();
15193
15761
  init_fs();
15762
+ var import_yaml8 = __toESM(require_dist(), 1);
15763
+ import { mkdir as mkdir8, stat as stat4, copyFile } from "fs/promises";
15764
+ import { join as join13 } from "path";
15765
+
15766
+ // src/core/integrity.ts
15767
+ init_fs();
15768
+ init_lifecycle();
15769
+ init_types();
15194
15770
  var import_yaml7 = __toESM(require_dist(), 1);
15195
- import { mkdir as mkdir7, stat as stat3, copyFile } from "fs/promises";
15196
- import { join as join11 } from "path";
15197
- async function dirExists(path) {
15771
+ import { readdir as readdir11 } from "fs/promises";
15772
+ import { join as join12 } from "path";
15773
+ var VALID_BACKLOG_TYPES = ["genome-change", "environment-change", "task"];
15774
+ var VALID_BACKLOG_STATUSES = ["pending", "consumed"];
15775
+ var VALID_GENERATION_TYPES = ["normal", "merge", "recovery"];
15776
+ var GENOME_LINE_WARNING_THRESHOLD = 100;
15777
+ var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$/;
15778
+ function isISODate(s) {
15779
+ return ISO_DATE_RE.test(s) && !Number.isNaN(new Date(s).getTime());
15780
+ }
15781
+ async function checkIntegrity(paths) {
15782
+ const errors = [];
15783
+ const warnings = [];
15784
+ await checkConfig(paths, errors, warnings);
15785
+ const state = await checkCurrentYml(paths, errors, warnings);
15786
+ await checkLineage(paths, errors, warnings);
15787
+ await checkGenome(paths, errors, warnings);
15788
+ await checkBacklog(paths, errors, warnings);
15789
+ if (state) {
15790
+ await checkArtifacts(paths, state, errors, warnings);
15791
+ }
15792
+ return { errors, warnings };
15793
+ }
15794
+ async function checkConfig(paths, errors, warnings) {
15795
+ const content = await readTextFile(paths.config);
15796
+ if (content === null) {
15797
+ errors.push("config.yml does not exist");
15798
+ return;
15799
+ }
15800
+ let config;
15198
15801
  try {
15199
- const s = await stat3(path);
15200
- return s.isDirectory();
15802
+ config = import_yaml7.default.parse(content) ?? {};
15201
15803
  } catch {
15202
- return false;
15804
+ errors.push("config.yml is not valid YAML");
15805
+ return;
15806
+ }
15807
+ if (typeof config !== "object" || Array.isArray(config)) {
15808
+ errors.push("config.yml root must be a YAML mapping");
15809
+ return;
15810
+ }
15811
+ if (!config.project || typeof config.project !== "string") {
15812
+ errors.push("config.yml: missing or invalid 'project' field (string required)");
15813
+ }
15814
+ if (!config.entryMode || typeof config.entryMode !== "string") {
15815
+ errors.push("config.yml: missing or invalid 'entryMode' field (string required)");
15816
+ } else {
15817
+ const validModes = ["greenfield", "migration", "adoption"];
15818
+ if (!validModes.includes(config.entryMode)) {
15819
+ errors.push(`config.yml: invalid entryMode "${config.entryMode}" (valid: ${validModes.join(", ")})`);
15820
+ }
15821
+ }
15822
+ if (config.version !== undefined && typeof config.version !== "string") {
15823
+ warnings.push("config.yml: 'version' should be a string");
15824
+ }
15825
+ if (config.strict !== undefined && typeof config.strict !== "boolean" && typeof config.strict !== "object") {
15826
+ warnings.push("config.yml: 'strict' should be boolean or object");
15827
+ }
15828
+ if (config.autoUpdate !== undefined && typeof config.autoUpdate !== "boolean") {
15829
+ warnings.push("config.yml: 'autoUpdate' should be boolean");
15830
+ }
15831
+ if (config.autoSubagent !== undefined && typeof config.autoSubagent !== "boolean") {
15832
+ warnings.push("config.yml: 'autoSubagent' should be boolean");
15203
15833
  }
15204
15834
  }
15205
- async function fixProject(projectRoot) {
15206
- const paths = new ReapPaths(projectRoot);
15207
- const issues = [];
15208
- const fixed = [];
15835
+ async function checkCurrentYml(paths, errors, warnings) {
15836
+ const content = await readTextFile(paths.currentYml);
15837
+ if (content === null || !content.trim()) {
15838
+ return null;
15839
+ }
15840
+ let state;
15841
+ try {
15842
+ state = import_yaml7.default.parse(content) ?? {};
15843
+ } catch {
15844
+ errors.push("current.yml is not valid YAML");
15845
+ return null;
15846
+ }
15847
+ if (typeof state !== "object" || Array.isArray(state)) {
15848
+ errors.push("current.yml root must be a YAML mapping");
15849
+ return null;
15850
+ }
15851
+ const requiredStrings = ["id", "goal", "stage", "startedAt"];
15852
+ for (const field of requiredStrings) {
15853
+ if (!state[field] || typeof state[field] !== "string") {
15854
+ errors.push(`current.yml: missing or invalid '${field}' field (string required)`);
15855
+ }
15856
+ }
15857
+ if (state.genomeVersion === undefined || typeof state.genomeVersion !== "number") {
15858
+ errors.push("current.yml: missing or invalid 'genomeVersion' field (number required)");
15859
+ }
15860
+ if (!Array.isArray(state.timeline)) {
15861
+ errors.push("current.yml: missing or invalid 'timeline' field (array required)");
15862
+ }
15863
+ if (state.type !== undefined) {
15864
+ if (!VALID_GENERATION_TYPES.includes(state.type)) {
15865
+ errors.push(`current.yml: invalid type "${state.type}" (valid: ${VALID_GENERATION_TYPES.join(", ")})`);
15866
+ }
15867
+ }
15868
+ if (state.parents !== undefined && !Array.isArray(state.parents)) {
15869
+ errors.push("current.yml: 'parents' must be an array");
15870
+ }
15871
+ if (typeof state.stage === "string" && !LifeCycle.isValid(state.stage)) {
15872
+ const mergeStages = ["detect", "mate", "merge", "sync", "validation", "completion"];
15873
+ if (!mergeStages.includes(state.stage)) {
15874
+ errors.push(`current.yml: invalid stage "${state.stage}"`);
15875
+ }
15876
+ }
15877
+ if (state.type === "recovery") {
15878
+ if (!state.recovers || !Array.isArray(state.recovers) || state.recovers.length === 0) {
15879
+ errors.push("current.yml: recovery generation must have non-empty 'recovers' array");
15880
+ }
15881
+ }
15882
+ return state;
15883
+ }
15884
+ async function checkLineage(paths, errors, warnings) {
15885
+ let entries;
15886
+ try {
15887
+ const items2 = await readdir11(paths.lineage, { withFileTypes: true });
15888
+ entries = items2.map((i) => ({ name: i.name, isDir: i.isDirectory(), isFile: i.isFile() }));
15889
+ } catch {
15890
+ return;
15891
+ }
15892
+ const items = await readdir11(paths.lineage, { withFileTypes: true });
15893
+ const allMetaIds = new Set;
15894
+ for (const item of items) {
15895
+ if (item.isDirectory() && item.name.startsWith("gen-")) {
15896
+ const metaPath = join12(paths.lineage, item.name, "meta.yml");
15897
+ const metaContent = await readTextFile(metaPath);
15898
+ if (metaContent === null) {
15899
+ errors.push(`lineage/${item.name}: missing meta.yml`);
15900
+ continue;
15901
+ }
15902
+ let meta;
15903
+ try {
15904
+ meta = import_yaml7.default.parse(metaContent) ?? {};
15905
+ } catch {
15906
+ errors.push(`lineage/${item.name}/meta.yml: invalid YAML`);
15907
+ continue;
15908
+ }
15909
+ validateLineageMeta(meta, `lineage/${item.name}/meta.yml`, errors, warnings);
15910
+ if (meta.id)
15911
+ allMetaIds.add(meta.id);
15912
+ } else if (item.isFile() && item.name.startsWith("gen-") && item.name.endsWith(".md")) {
15913
+ const content = await readTextFile(join12(paths.lineage, item.name));
15914
+ if (!content)
15915
+ continue;
15916
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
15917
+ if (!fmMatch) {
15918
+ warnings.push(`lineage/${item.name}: compressed file missing frontmatter`);
15919
+ continue;
15920
+ }
15921
+ let meta;
15922
+ try {
15923
+ meta = import_yaml7.default.parse(fmMatch[1]) ?? {};
15924
+ } catch {
15925
+ errors.push(`lineage/${item.name}: invalid frontmatter YAML`);
15926
+ continue;
15927
+ }
15928
+ validateLineageMeta(meta, `lineage/${item.name}`, errors, warnings);
15929
+ if (meta.id)
15930
+ allMetaIds.add(meta.id);
15931
+ }
15932
+ }
15933
+ for (const item of items) {
15934
+ let parents = [];
15935
+ if (item.isDirectory() && item.name.startsWith("gen-")) {
15936
+ const metaContent = await readTextFile(join12(paths.lineage, item.name, "meta.yml"));
15937
+ if (metaContent) {
15938
+ try {
15939
+ const meta = import_yaml7.default.parse(metaContent);
15940
+ parents = meta?.parents ?? [];
15941
+ } catch {}
15942
+ }
15943
+ } else if (item.isFile() && item.name.startsWith("gen-") && item.name.endsWith(".md")) {
15944
+ const content = await readTextFile(join12(paths.lineage, item.name));
15945
+ if (content) {
15946
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
15947
+ if (fmMatch) {
15948
+ try {
15949
+ const meta = import_yaml7.default.parse(fmMatch[1]);
15950
+ parents = meta?.parents ?? [];
15951
+ } catch {}
15952
+ }
15953
+ }
15954
+ }
15955
+ for (const parent of parents) {
15956
+ if (parent && !allMetaIds.has(parent)) {
15957
+ const epochContent = await readTextFile(join12(paths.lineage, "epoch.md"));
15958
+ let inEpoch = false;
15959
+ if (epochContent) {
15960
+ inEpoch = epochContent.includes(parent);
15961
+ }
15962
+ if (!inEpoch) {
15963
+ warnings.push(`lineage/${item.name}: parent "${parent}" not found in lineage (may be in a compressed epoch)`);
15964
+ }
15965
+ }
15966
+ }
15967
+ }
15968
+ }
15969
+ function validateLineageMeta(meta, location, errors, warnings) {
15970
+ const requiredStrings = ["id", "goal", "startedAt", "completedAt"];
15971
+ for (const field of requiredStrings) {
15972
+ if (!meta[field] || typeof meta[field] !== "string") {
15973
+ errors.push(`${location}: missing or invalid '${field}' field`);
15974
+ }
15975
+ }
15976
+ if (typeof meta.completedAt === "string") {
15977
+ if (!isISODate(meta.completedAt)) {
15978
+ errors.push(`${location}: completedAt "${meta.completedAt}" is not a valid ISO date`);
15979
+ }
15980
+ }
15981
+ if (typeof meta.startedAt === "string") {
15982
+ if (!isISODate(meta.startedAt)) {
15983
+ warnings.push(`${location}: startedAt "${meta.startedAt}" is not a valid ISO date`);
15984
+ }
15985
+ }
15986
+ if (!Array.isArray(meta.parents)) {
15987
+ errors.push(`${location}: missing or invalid 'parents' field (array required)`);
15988
+ }
15989
+ if (!meta.type || typeof meta.type !== "string") {
15990
+ errors.push(`${location}: missing or invalid 'type' field`);
15991
+ } else if (!VALID_GENERATION_TYPES.includes(meta.type)) {
15992
+ errors.push(`${location}: invalid type "${meta.type}"`);
15993
+ }
15994
+ if (meta.genomeHash !== undefined && typeof meta.genomeHash !== "string") {
15995
+ warnings.push(`${location}: genomeHash should be a string`);
15996
+ }
15997
+ }
15998
+ async function checkGenome(paths, errors, warnings) {
15999
+ const l1Files = [
16000
+ { path: paths.principles, name: "principles.md" },
16001
+ { path: paths.conventions, name: "conventions.md" },
16002
+ { path: paths.constraints, name: "constraints.md" },
16003
+ { path: paths.sourceMap, name: "source-map.md" }
16004
+ ];
16005
+ for (const gf of l1Files) {
16006
+ if (!await fileExists(gf.path)) {
16007
+ errors.push(`genome/${gf.name} does not exist`);
16008
+ continue;
16009
+ }
16010
+ const content = await readTextFile(gf.path);
16011
+ if (content === null)
16012
+ continue;
16013
+ const lines = content.split(`
16014
+ `).length;
16015
+ if (lines > GENOME_LINE_WARNING_THRESHOLD) {
16016
+ warnings.push(`genome/${gf.name}: ${lines} lines (exceeds ~${GENOME_LINE_WARNING_THRESHOLD} line guideline)`);
16017
+ }
16018
+ const stripped = content.split(`
16019
+ `).filter((l) => !l.startsWith("#") && !l.startsWith(">") && !l.startsWith("-") && l.trim() !== "").join("").trim();
16020
+ if (stripped.length === 0) {
16021
+ warnings.push(`genome/${gf.name}: appears to be placeholder-only (no substantive content)`);
16022
+ }
16023
+ }
16024
+ }
16025
+ async function checkBacklog(paths, errors, warnings) {
16026
+ let entries;
16027
+ try {
16028
+ entries = await readdir11(paths.backlog);
16029
+ } catch {
16030
+ return;
16031
+ }
16032
+ for (const filename of entries) {
16033
+ if (!filename.endsWith(".md"))
16034
+ continue;
16035
+ const content = await readTextFile(join12(paths.backlog, filename));
16036
+ if (!content)
16037
+ continue;
16038
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
16039
+ if (!fmMatch) {
16040
+ errors.push(`backlog/${filename}: missing frontmatter`);
16041
+ continue;
16042
+ }
16043
+ let fm;
16044
+ try {
16045
+ fm = import_yaml7.default.parse(fmMatch[1]) ?? {};
16046
+ } catch {
16047
+ errors.push(`backlog/${filename}: invalid frontmatter YAML`);
16048
+ continue;
16049
+ }
16050
+ if (!fm.type || typeof fm.type !== "string") {
16051
+ errors.push(`backlog/${filename}: missing or invalid 'type' field in frontmatter`);
16052
+ } else if (!VALID_BACKLOG_TYPES.includes(fm.type)) {
16053
+ errors.push(`backlog/${filename}: invalid type "${fm.type}" (valid: ${VALID_BACKLOG_TYPES.join(", ")})`);
16054
+ }
16055
+ if (!fm.status || typeof fm.status !== "string") {
16056
+ errors.push(`backlog/${filename}: missing or invalid 'status' field in frontmatter`);
16057
+ } else if (!VALID_BACKLOG_STATUSES.includes(fm.status)) {
16058
+ errors.push(`backlog/${filename}: invalid status "${fm.status}" (valid: ${VALID_BACKLOG_STATUSES.join(", ")})`);
16059
+ }
16060
+ if (fm.status === "consumed" && !fm.consumedBy) {
16061
+ errors.push(`backlog/${filename}: status is 'consumed' but missing 'consumedBy' field`);
16062
+ }
16063
+ }
16064
+ }
16065
+ var STAGE_ARTIFACT_MAP = {
16066
+ objective: "01-objective.md",
16067
+ planning: "02-planning.md",
16068
+ implementation: "03-implementation.md",
16069
+ validation: "04-validation.md",
16070
+ completion: "05-completion.md"
16071
+ };
16072
+ async function checkArtifacts(paths, state, errors, warnings) {
16073
+ const currentStageIdx = LIFECYCLE_ORDER.indexOf(state.stage);
16074
+ if (currentStageIdx < 0)
16075
+ return;
16076
+ for (let i = 0;i < currentStageIdx; i++) {
16077
+ const stage = LIFECYCLE_ORDER[i];
16078
+ const artifactName = STAGE_ARTIFACT_MAP[stage];
16079
+ if (!artifactName)
16080
+ continue;
16081
+ const artifactPath = paths.artifact(artifactName);
16082
+ if (!await fileExists(artifactPath)) {
16083
+ errors.push(`artifact ${artifactName} missing (expected for completed stage '${stage}')`);
16084
+ continue;
16085
+ }
16086
+ const content = await readTextFile(artifactPath);
16087
+ if (content && !content.startsWith("# REAP MANAGED")) {
16088
+ warnings.push(`artifact ${artifactName}: missing 'REAP MANAGED' header`);
16089
+ }
16090
+ }
16091
+ const currentArtifact = STAGE_ARTIFACT_MAP[state.stage];
16092
+ if (currentArtifact) {
16093
+ const artifactPath = paths.artifact(currentArtifact);
16094
+ if (!await fileExists(artifactPath)) {
16095
+ warnings.push(`artifact ${currentArtifact} not yet created for current stage '${state.stage}'`);
16096
+ }
16097
+ }
16098
+ }
16099
+
16100
+ // src/cli/commands/fix.ts
16101
+ async function checkProject(projectRoot) {
16102
+ const paths = new ReapPaths(projectRoot);
16103
+ return checkIntegrity(paths);
16104
+ }
16105
+ async function dirExists(path) {
16106
+ try {
16107
+ const s = await stat4(path);
16108
+ return s.isDirectory();
16109
+ } catch {
16110
+ return false;
16111
+ }
16112
+ }
16113
+ async function fixProject(projectRoot) {
16114
+ const paths = new ReapPaths(projectRoot);
16115
+ const issues = [];
16116
+ const fixed = [];
15209
16117
  const requiredDirs = [
15210
16118
  { path: paths.genome, name: "genome" },
15211
16119
  { path: paths.domain, name: "genome/domain" },
@@ -15216,7 +16124,7 @@ async function fixProject(projectRoot) {
15216
16124
  ];
15217
16125
  for (const dir of requiredDirs) {
15218
16126
  if (!await dirExists(dir.path)) {
15219
- await mkdir7(dir.path, { recursive: true });
16127
+ await mkdir8(dir.path, { recursive: true });
15220
16128
  fixed.push(`Recreated missing directory: ${dir.name}/`);
15221
16129
  }
15222
16130
  }
@@ -15228,7 +16136,7 @@ async function fixProject(projectRoot) {
15228
16136
  ];
15229
16137
  for (const gf of genomeFiles) {
15230
16138
  if (!await fileExists(gf.path)) {
15231
- const templateSrc = join11(ReapPaths.packageGenomeDir, gf.name);
16139
+ const templateSrc = join13(ReapPaths.packageGenomeDir, gf.name);
15232
16140
  if (await fileExists(templateSrc)) {
15233
16141
  await copyFile(templateSrc, gf.path);
15234
16142
  fixed.push(`Restored missing genome/${gf.name} from template`);
@@ -15244,7 +16152,7 @@ async function fixProject(projectRoot) {
15244
16152
  if (currentContent !== null) {
15245
16153
  if (currentContent.trim()) {
15246
16154
  try {
15247
- const state = import_yaml7.default.parse(currentContent);
16155
+ const state = import_yaml8.default.parse(currentContent);
15248
16156
  if (!state.stage || !LifeCycle.isValid(state.stage)) {
15249
16157
  issues.push(`Invalid stage "${state.stage}" in current.yml. Valid stages: ${LifeCycle.stages().join(", ")}. Manual correction required.`);
15250
16158
  }
@@ -15253,7 +16161,7 @@ async function fixProject(projectRoot) {
15253
16161
  if (!state.goal)
15254
16162
  issues.push("current.yml is missing 'goal' field. Manual correction required.");
15255
16163
  if (!await dirExists(paths.backlog)) {
15256
- await mkdir7(paths.backlog, { recursive: true });
16164
+ await mkdir8(paths.backlog, { recursive: true });
15257
16165
  fixed.push("Recreated missing backlog/ directory for active generation");
15258
16166
  }
15259
16167
  } catch {
@@ -15265,14 +16173,273 @@ async function fixProject(projectRoot) {
15265
16173
  return { issues, fixed };
15266
16174
  }
15267
16175
 
16176
+ // src/cli/commands/destroy.ts
16177
+ init_fs();
16178
+ init_paths();
16179
+ init_config();
16180
+ import { rm as rm3, readdir as readdir12, unlink as unlink5 } from "fs/promises";
16181
+ import { join as join14 } from "path";
16182
+ async function getProjectName(projectRoot) {
16183
+ try {
16184
+ const paths = new ReapPaths(projectRoot);
16185
+ const config = await ConfigManager.read(paths);
16186
+ return config.project;
16187
+ } catch {
16188
+ return null;
16189
+ }
16190
+ }
16191
+ async function destroyProject(projectRoot) {
16192
+ const removed = [];
16193
+ const skipped = [];
16194
+ const reapDir = join14(projectRoot, ".reap");
16195
+ if (await fileExists(reapDir)) {
16196
+ await rm3(reapDir, { recursive: true, force: true });
16197
+ removed.push(".reap/");
16198
+ } else {
16199
+ skipped.push(".reap/ (not found)");
16200
+ }
16201
+ const claudeCommandsDir = join14(projectRoot, ".claude", "commands");
16202
+ await removeGlobFiles(claudeCommandsDir, "reap.", removed, skipped, ".claude/commands/");
16203
+ const claudeSkillsDir = join14(projectRoot, ".claude", "skills");
16204
+ await removeGlobDirs(claudeSkillsDir, "reap.", removed, skipped, ".claude/skills/");
16205
+ await cleanClaudeMd(projectRoot, removed, skipped);
16206
+ await cleanGitignore(projectRoot, removed, skipped);
16207
+ return { removed, skipped };
16208
+ }
16209
+ async function removeGlobFiles(dir, prefix, removed, skipped, displayPrefix) {
16210
+ try {
16211
+ const files = await readdir12(dir);
16212
+ const matched = files.filter((f) => f.startsWith(prefix));
16213
+ if (matched.length === 0) {
16214
+ skipped.push(`${displayPrefix}${prefix}* (none found)`);
16215
+ return;
16216
+ }
16217
+ for (const file of matched) {
16218
+ await unlink5(join14(dir, file));
16219
+ removed.push(`${displayPrefix}${file}`);
16220
+ }
16221
+ } catch {
16222
+ skipped.push(`${displayPrefix}${prefix}* (directory not found)`);
16223
+ }
16224
+ }
16225
+ async function removeGlobDirs(dir, prefix, removed, skipped, displayPrefix) {
16226
+ try {
16227
+ const entries = await readdir12(dir);
16228
+ const matched = entries.filter((e) => e.startsWith(prefix));
16229
+ if (matched.length === 0) {
16230
+ skipped.push(`${displayPrefix}${prefix}* (none found)`);
16231
+ return;
16232
+ }
16233
+ for (const entry of matched) {
16234
+ await rm3(join14(dir, entry), { recursive: true, force: true });
16235
+ removed.push(`${displayPrefix}${entry}`);
16236
+ }
16237
+ } catch {
16238
+ skipped.push(`${displayPrefix}${prefix}* (directory not found)`);
16239
+ }
16240
+ }
16241
+ async function cleanClaudeMd(projectRoot, removed, skipped) {
16242
+ const claudeMdPath = join14(projectRoot, ".claude", "CLAUDE.md");
16243
+ const content = await readTextFile(claudeMdPath);
16244
+ if (content === null) {
16245
+ skipped.push(".claude/CLAUDE.md (not found)");
16246
+ return;
16247
+ }
16248
+ const marker = "# REAP Project";
16249
+ if (!content.includes(marker)) {
16250
+ skipped.push(".claude/CLAUDE.md (no REAP section)");
16251
+ return;
16252
+ }
16253
+ const cleaned = content.replace(/# REAP Project[\s\S]*?(?=\n# |\s*$)/, "").trim();
16254
+ if (cleaned.length === 0) {
16255
+ await unlink5(claudeMdPath);
16256
+ removed.push(".claude/CLAUDE.md (deleted, was REAP-only)");
16257
+ } else {
16258
+ await writeTextFile(claudeMdPath, cleaned + `
16259
+ `);
16260
+ removed.push(".claude/CLAUDE.md (REAP section removed)");
16261
+ }
16262
+ }
16263
+ async function cleanGitignore(projectRoot, removed, skipped) {
16264
+ const gitignorePath = join14(projectRoot, ".gitignore");
16265
+ const content = await readTextFile(gitignorePath);
16266
+ if (content === null) {
16267
+ skipped.push(".gitignore (not found)");
16268
+ return;
16269
+ }
16270
+ const reapPatterns = [
16271
+ /^# REAP.*$/m,
16272
+ /^\.claude\/skills\/reap\..*$/m,
16273
+ /^\.claude\/commands\/reap\..*$/m,
16274
+ /^\.reap\/.*$/m
16275
+ ];
16276
+ let cleaned = content;
16277
+ let anyRemoved = false;
16278
+ for (const pattern of reapPatterns) {
16279
+ if (pattern.test(cleaned)) {
16280
+ cleaned = cleaned.replace(new RegExp(pattern.source, "gm"), "");
16281
+ anyRemoved = true;
16282
+ }
16283
+ }
16284
+ if (!anyRemoved) {
16285
+ skipped.push(".gitignore (no REAP entries)");
16286
+ return;
16287
+ }
16288
+ cleaned = cleaned.replace(/\n{3,}/g, `
16289
+
16290
+ `).trim() + `
16291
+ `;
16292
+ await writeTextFile(gitignorePath, cleaned);
16293
+ removed.push(".gitignore (REAP entries removed)");
16294
+ }
16295
+
16296
+ // src/cli/commands/clean.ts
16297
+ init_paths();
16298
+ init_fs();
16299
+ init_generation();
16300
+ import { rm as rm4, readdir as readdir13, mkdir as mkdir9 } from "fs/promises";
16301
+ import { join as join15 } from "path";
16302
+ async function hasActiveGeneration(projectRoot) {
16303
+ const paths = new ReapPaths(projectRoot);
16304
+ try {
16305
+ const mgr = new GenerationManager(paths);
16306
+ const current = await mgr.current();
16307
+ return current !== null;
16308
+ } catch {
16309
+ return false;
16310
+ }
16311
+ }
16312
+ async function cleanProject(projectRoot, options) {
16313
+ const paths = new ReapPaths(projectRoot);
16314
+ const actions = [];
16315
+ const warnings = [];
16316
+ if (options.lineage === "delete") {
16317
+ await rm4(paths.lineage, { recursive: true, force: true });
16318
+ await mkdir9(paths.lineage, { recursive: true });
16319
+ actions.push("Lineage: 전체 삭제됨");
16320
+ } else {
16321
+ await compressLineage(paths, actions);
16322
+ }
16323
+ await cleanLife(paths, actions);
16324
+ if (options.hooks === "reset") {
16325
+ const hooksDir = paths.hooks;
16326
+ if (await fileExists(hooksDir)) {
16327
+ await rm4(hooksDir, { recursive: true, force: true });
16328
+ await mkdir9(hooksDir, { recursive: true });
16329
+ actions.push("Hooks: 초기화됨");
16330
+ } else {
16331
+ actions.push("Hooks: 디렉토리 없음 (skip)");
16332
+ }
16333
+ } else {
16334
+ actions.push("Hooks: 기존 유지");
16335
+ }
16336
+ if (options.genome === "template") {
16337
+ await resetGenomeToTemplate(paths, actions);
16338
+ } else if (options.genome === "keep") {
16339
+ actions.push("Genome/Environment: 기존 유지");
16340
+ } else {
16341
+ actions.push("Genome/Environment: 수동 편집 모드 (변경 없음)");
16342
+ }
16343
+ const backlogDir = paths.backlog;
16344
+ if (options.backlog === "delete") {
16345
+ if (await fileExists(backlogDir)) {
16346
+ await rm4(backlogDir, { recursive: true, force: true });
16347
+ await mkdir9(backlogDir, { recursive: true });
16348
+ actions.push("Backlog: 삭제됨");
16349
+ } else {
16350
+ actions.push("Backlog: 디렉토리 없음 (skip)");
16351
+ }
16352
+ } else {
16353
+ actions.push("Backlog: 보존됨");
16354
+ }
16355
+ return { actions, warnings };
16356
+ }
16357
+ async function compressLineage(paths, actions) {
16358
+ const lineageDir = paths.lineage;
16359
+ if (!await fileExists(lineageDir)) {
16360
+ actions.push("Lineage: 디렉토리 없음 (skip)");
16361
+ return;
16362
+ }
16363
+ let entries;
16364
+ try {
16365
+ entries = await readdir13(lineageDir);
16366
+ } catch {
16367
+ actions.push("Lineage: 읽기 실패 (skip)");
16368
+ return;
16369
+ }
16370
+ const genDirs = entries.filter((e) => e.startsWith("gen-"));
16371
+ if (genDirs.length === 0) {
16372
+ actions.push("Lineage: 세대 기록 없음 (skip)");
16373
+ return;
16374
+ }
16375
+ const epochId = `epoch-${new Date().toISOString().slice(0, 10).replace(/-/g, "")}`;
16376
+ const summary = [
16377
+ `# Epoch: ${epochId}`,
16378
+ `# Compressed ${genDirs.length} generations`,
16379
+ `# Date: ${new Date().toISOString()}`,
16380
+ "",
16381
+ "## Generations",
16382
+ ...genDirs.map((d) => `- ${d}`),
16383
+ ""
16384
+ ].join(`
16385
+ `);
16386
+ for (const dir of genDirs) {
16387
+ await rm4(join15(lineageDir, dir), { recursive: true, force: true });
16388
+ }
16389
+ await writeTextFile(join15(lineageDir, `${epochId}.md`), summary);
16390
+ actions.push(`Lineage: ${genDirs.length}개 세대를 ${epochId}로 압축`);
16391
+ }
16392
+ async function cleanLife(paths, actions) {
16393
+ const lifeDir = paths.life;
16394
+ if (!await fileExists(lifeDir)) {
16395
+ actions.push("Life: 디렉토리 없음 (skip)");
16396
+ return;
16397
+ }
16398
+ let entries;
16399
+ try {
16400
+ entries = await readdir13(lifeDir);
16401
+ } catch {
16402
+ actions.push("Life: 읽기 실패 (skip)");
16403
+ return;
16404
+ }
16405
+ let removedCount = 0;
16406
+ for (const entry of entries) {
16407
+ if (entry === "backlog")
16408
+ continue;
16409
+ const entryPath = join15(lifeDir, entry);
16410
+ await rm4(entryPath, { recursive: true, force: true });
16411
+ removedCount++;
16412
+ }
16413
+ actions.push(`Life: ${removedCount}개 파일/디렉토리 정리됨`);
16414
+ }
16415
+ async function resetGenomeToTemplate(paths, actions) {
16416
+ const genomeFiles = ["principles.md", "conventions.md", "constraints.md", "source-map.md"];
16417
+ for (const file of genomeFiles) {
16418
+ const templatePath = join15(ReapPaths.packageGenomeDir, file);
16419
+ const destPath = join15(paths.genome, file);
16420
+ try {
16421
+ const templateContent = await readTextFileOrThrow(templatePath);
16422
+ await writeTextFile(destPath, templateContent);
16423
+ } catch {}
16424
+ }
16425
+ const envSummaryTemplate = join15(ReapPaths.packageTemplatesDir, "environment", "summary.md");
16426
+ if (await fileExists(envSummaryTemplate)) {
16427
+ try {
16428
+ const content = await readTextFileOrThrow(envSummaryTemplate);
16429
+ await writeTextFile(paths.environmentSummary, content);
16430
+ } catch {}
16431
+ }
16432
+ actions.push("Genome/Environment: 템플릿으로 초기화됨");
16433
+ }
16434
+
15268
16435
  // src/cli/index.ts
15269
16436
  init_lifecycle();
15270
16437
  init_paths();
15271
16438
  init_fs();
15272
16439
  init_version();
15273
16440
  init_config();
15274
- import { join as join27 } from "path";
15275
- program.name("reap").description("REAP — Recursive Evolutionary Autonomous Pipeline").version("0.14.0");
16441
+ import { join as join32 } from "path";
16442
+ program.name("reap").description("REAP — Recursive Evolutionary Autonomous Pipeline").version("0.15.1");
15276
16443
  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) => {
15277
16444
  try {
15278
16445
  const cwd = process.cwd();
@@ -15310,8 +16477,9 @@ Initializing REAP project "${name}" (${mode} mode)...
15310
16477
  console.log(`
15311
16478
  Getting started:`);
15312
16479
  console.log(` 1. Open your AI agent (${initResult.agents[0] || "Claude Code or OpenCode"})`);
15313
- console.log(` 2. Run /reap.start to begin your first Generation`);
15314
- console.log(` 3. Or run /reap.evolve for autonomous execution`);
16480
+ console.log(` 2. Run /reap.sync to synchronize Genome with your project`);
16481
+ console.log(` 3. Run /reap.start to begin your first Generation`);
16482
+ console.log(` 4. Or run /reap.evolve for autonomous execution`);
15315
16483
  if (initResult.agents.length === 0) {
15316
16484
  console.log(`
15317
16485
  ⚠ No AI agents detected. Install Claude Code or OpenCode, then run 'reap update'.`);
@@ -15328,10 +16496,12 @@ program.command("status").description("Show current project and Generation statu
15328
16496
  const paths = new ReapPaths(cwd);
15329
16497
  const config = await ConfigManager.read(paths);
15330
16498
  const skipCheck = config.autoUpdate === false;
15331
- const installedVersion = "0.14.0";
16499
+ const installedVersion = "0.15.1";
15332
16500
  const versionLine = formatVersionLine(installedVersion, skipCheck);
15333
16501
  console.log(`${versionLine} | Project: ${status.project} (${status.entryMode})`);
15334
16502
  console.log(`Completed Generations: ${status.totalGenerations}`);
16503
+ const syncLabel = status.lastSyncedGeneration ? `synced (${status.lastSyncedGeneration})` : "never synced";
16504
+ console.log(`Genome Sync: ${syncLabel}`);
15335
16505
  if (status.generation) {
15336
16506
  console.log(`
15337
16507
  Active Generation: ${status.generation.id}`);
@@ -15347,19 +16517,38 @@ No active Generation. Run '/reap.start' to start one.`);
15347
16517
  process.exit(1);
15348
16518
  }
15349
16519
  });
15350
- program.command("fix").description("Diagnose and repair .reap/ directory structure").action(async () => {
16520
+ program.command("fix").description("Diagnose and repair .reap/ directory structure").option("--check", "Check-only mode: report issues without fixing anything").action(async (options) => {
15351
16521
  try {
15352
- const result = await fixProject(process.cwd());
15353
- if (result.fixed.length === 0 && result.issues.length === 0) {
15354
- console.log("✓ Project is healthy. No issues found.");
15355
- } else {
15356
- if (result.fixed.length > 0) {
15357
- console.log("Fixed:");
15358
- result.fixed.forEach((f) => console.log(` ✓ ${f}`));
16522
+ if (options.check) {
16523
+ const result = await checkProject(process.cwd());
16524
+ if (result.errors.length === 0 && result.warnings.length === 0) {
16525
+ console.log("✓ Integrity check passed. No issues found.");
16526
+ } else {
16527
+ if (result.errors.length > 0) {
16528
+ console.log("Errors:");
16529
+ result.errors.forEach((e) => console.log(` ✗ ${e}`));
16530
+ }
16531
+ if (result.warnings.length > 0) {
16532
+ console.log("Warnings:");
16533
+ result.warnings.forEach((w) => console.log(` ⚠ ${w}`));
16534
+ }
15359
16535
  }
15360
- if (result.issues.length > 0) {
15361
- console.log("Issues (require manual intervention):");
15362
- result.issues.forEach((i) => console.log(` ✗ ${i}`));
16536
+ if (result.errors.length > 0) {
16537
+ process.exit(1);
16538
+ }
16539
+ } else {
16540
+ const result = await fixProject(process.cwd());
16541
+ if (result.fixed.length === 0 && result.issues.length === 0) {
16542
+ console.log("✓ Project is healthy. No issues found.");
16543
+ } else {
16544
+ if (result.fixed.length > 0) {
16545
+ console.log("Fixed:");
16546
+ result.fixed.forEach((f) => console.log(` ✓ ${f}`));
16547
+ }
16548
+ if (result.issues.length > 0) {
16549
+ console.log("Issues (require manual intervention):");
16550
+ result.issues.forEach((i) => console.log(` ✗ ${i}`));
16551
+ }
15363
16552
  }
15364
16553
  }
15365
16554
  } catch (e) {
@@ -15407,16 +16596,113 @@ program.command("help").description("Show REAP commands, slash commands, and wor
15407
16596
  if (l === "korean" || l === "ko")
15408
16597
  lang = "ko";
15409
16598
  }
15410
- const helpDir = join27(ReapPaths.packageTemplatesDir, "help");
15411
- let helpText = await readTextFile(join27(helpDir, `${lang}.txt`));
16599
+ const helpDir = join32(ReapPaths.packageTemplatesDir, "help");
16600
+ let helpText = await readTextFile(join32(helpDir, `${lang}.txt`));
15412
16601
  if (!helpText)
15413
- helpText = await readTextFile(join27(helpDir, "en.txt"));
16602
+ helpText = await readTextFile(join32(helpDir, "en.txt"));
15414
16603
  if (!helpText) {
15415
16604
  console.log("Help file not found. Run 'reap update' to install templates.");
15416
16605
  return;
15417
16606
  }
15418
16607
  console.log(helpText);
15419
16608
  });
16609
+ program.command("destroy").description("Remove all REAP files from this project").action(async () => {
16610
+ try {
16611
+ const cwd = process.cwd();
16612
+ const projectName = await getProjectName(cwd);
16613
+ if (!projectName) {
16614
+ console.error("Error: Not a REAP project (cannot read .reap/config.yml).");
16615
+ process.exit(1);
16616
+ }
16617
+ const expectedInput = "yes destroy";
16618
+ console.log(`
16619
+ This will permanently remove all REAP files from this project.`);
16620
+ console.log(`To confirm, type '${expectedInput}':
16621
+ `);
16622
+ const answer = await prompt("> ");
16623
+ if (answer.trim() !== expectedInput) {
16624
+ console.log(`
16625
+ Confirmation mismatch. Destroy cancelled.`);
16626
+ process.exit(0);
16627
+ }
16628
+ console.log("");
16629
+ const result = await destroyProject(cwd);
16630
+ if (result.removed.length > 0) {
16631
+ console.log("Removed:");
16632
+ result.removed.forEach((f) => console.log(` - ${f}`));
16633
+ }
16634
+ if (result.skipped.length > 0) {
16635
+ console.log("Skipped:");
16636
+ result.skipped.forEach((f) => console.log(` - ${f}`));
16637
+ }
16638
+ console.log(`
16639
+ REAP has been removed from this project.`);
16640
+ } catch (e) {
16641
+ console.error(`Error: ${e.message}`);
16642
+ process.exit(1);
16643
+ }
16644
+ });
16645
+ program.command("clean").description("Reset REAP project with interactive options").action(async () => {
16646
+ try {
16647
+ const cwd = process.cwd();
16648
+ const paths = new ReapPaths(cwd);
16649
+ if (!await paths.isReapProject()) {
16650
+ console.error("Error: Not a REAP project (.reap/ not found).");
16651
+ process.exit(1);
16652
+ }
16653
+ if (await hasActiveGeneration(cwd)) {
16654
+ console.log(`
16655
+ ⚠ Warning: There is an active generation in progress.`);
16656
+ const proceed = await prompt("Continue and discard it? (y/N): ");
16657
+ if (proceed.trim().toLowerCase() !== "y") {
16658
+ console.log("Clean cancelled.");
16659
+ process.exit(0);
16660
+ }
16661
+ }
16662
+ console.log(`
16663
+ --- REAP Clean ---
16664
+ `);
16665
+ console.log("1. Lineage (generation history):");
16666
+ console.log(" [1] Compress into epoch summary");
16667
+ console.log(" [2] Delete entirely");
16668
+ const lineageChoice = await prompt(" Choice (1/2): ");
16669
+ const lineage = lineageChoice.trim() === "2" ? "delete" : "compress";
16670
+ console.log(`
16671
+ 2. Hooks:`);
16672
+ console.log(" [1] Keep existing hooks");
16673
+ console.log(" [2] Reset to defaults");
16674
+ const hooksChoice = await prompt(" Choice (1/2): ");
16675
+ const hooks = hooksChoice.trim() === "2" ? "reset" : "keep";
16676
+ console.log(`
16677
+ 3. Genome / Environment:`);
16678
+ console.log(" [1] Override with templates (then run /reap.sync)");
16679
+ console.log(" [2] Keep current files");
16680
+ console.log(" [3] Manual editing (no changes)");
16681
+ const genomeChoice = await prompt(" Choice (1/2/3): ");
16682
+ const genome = genomeChoice.trim() === "1" ? "template" : genomeChoice.trim() === "3" ? "manual" : "keep";
16683
+ console.log(`
16684
+ 4. Backlog:`);
16685
+ console.log(" [1] Keep existing backlog items");
16686
+ console.log(" [2] Delete all");
16687
+ const backlogChoice = await prompt(" Choice (1/2): ");
16688
+ const backlog = backlogChoice.trim() === "2" ? "delete" : "keep";
16689
+ console.log(`
16690
+ Applying...
16691
+ `);
16692
+ const result = await cleanProject(cwd, { lineage, hooks, genome, backlog });
16693
+ for (const action of result.actions) {
16694
+ console.log(` - ${action}`);
16695
+ }
16696
+ for (const warning of result.warnings) {
16697
+ console.log(` ⚠ ${warning}`);
16698
+ }
16699
+ console.log(`
16700
+ Clean complete. Run /reap.start to begin a new generation.`);
16701
+ } catch (e) {
16702
+ console.error(`Error: ${e.message}`);
16703
+ process.exit(1);
16704
+ }
16705
+ });
15420
16706
  program.command("run <command>").description("Run a REAP command script (internal, used by slash commands)").option("--phase <phase>", "Start from a specific phase").allowUnknownOption().action(async (command, options, cmd) => {
15421
16707
  const rawArgs = cmd.args.slice(1);
15422
16708
  const passArgs = [];
@@ -15430,4 +16716,13 @@ program.command("run <command>").description("Run a REAP command script (interna
15430
16716
  const { runCommand: runCommand2 } = await Promise.resolve().then(() => (init_run(), exports_run));
15431
16717
  await runCommand2(command, options.phase, passArgs);
15432
16718
  });
16719
+ function prompt(question) {
16720
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
16721
+ return new Promise((resolve) => {
16722
+ rl.question(question, (answer) => {
16723
+ rl.close();
16724
+ resolve(answer ?? "");
16725
+ });
16726
+ });
16727
+ }
15433
16728
  program.parse();