cclaw-cli 0.5.3 → 0.5.5

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/install.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import fs from "node:fs/promises";
3
- import os from "node:os";
4
3
  import path from "node:path";
5
4
  import { promisify } from "node:util";
6
5
  import { COMMAND_FILE_ORDER, REQUIRED_DIRS, RUNTIME_ROOT, UTILITY_COMMANDS } from "./constants.js";
@@ -13,7 +12,7 @@ import { startCommandContract, startCommandSkillMarkdown } from "./content/start
13
12
  import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
14
13
  import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
15
14
  import { sessionStartScript, stopCheckpointScript, opencodePluginJs, claudeHooksJson, cursorHooksJson, codexHooksJson } from "./content/hooks.js";
16
- import { contextMonitorScript, observeScript, promptGuardScript, workflowGuardScript, summarizeObservationsRuntimeModule, summarizeObservationsScript } from "./content/observe.js";
15
+ import { contextMonitorScript, promptGuardScript, workflowGuardScript } from "./content/observe.js";
17
16
  import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
18
17
  import { ARTIFACT_TEMPLATES, CURSOR_WORKFLOW_RULE_MDC, RULEBOOK_MARKDOWN, buildRulesJson } from "./content/templates.js";
19
18
  import { stageSkillFolder, stageSkillMarkdown } from "./content/skills.js";
@@ -32,22 +31,6 @@ const execFileAsync = promisify(execFile);
32
31
  function runtimePath(projectRoot, ...segments) {
33
32
  return path.join(projectRoot, RUNTIME_ROOT, ...segments);
34
33
  }
35
- function resolveGlobalLearningsPath(projectRoot, config) {
36
- if (config.globalLearnings !== true) {
37
- return null;
38
- }
39
- const raw = config.globalLearningsPath?.trim() ?? "";
40
- if (raw.length === 0) {
41
- return path.join(os.homedir(), ".cclaw-global-learnings.jsonl");
42
- }
43
- if (raw.startsWith("~/")) {
44
- return path.join(os.homedir(), raw.slice(2));
45
- }
46
- if (path.isAbsolute(raw)) {
47
- return raw;
48
- }
49
- return path.join(projectRoot, raw);
50
- }
51
34
  async function resolveGitHooksDir(projectRoot) {
52
35
  try {
53
36
  const { stdout } = await execFileAsync("git", ["rev-parse", "--git-path", "hooks"], {
@@ -497,23 +480,14 @@ async function writeHooks(projectRoot, config) {
497
480
  const harnesses = config.harnesses;
498
481
  const hooksDir = runtimePath(projectRoot, "hooks");
499
482
  await ensureDir(hooksDir);
500
- await writeFileSafe(path.join(hooksDir, "session-start.sh"), sessionStartScript({
501
- globalLearningsEnabled: config.globalLearnings === true,
502
- globalLearningsPath: config.globalLearningsPath
503
- }));
483
+ await writeFileSafe(path.join(hooksDir, "session-start.sh"), sessionStartScript());
504
484
  await writeFileSafe(path.join(hooksDir, "stop-checkpoint.sh"), stopCheckpointScript());
505
485
  await writeFileSafe(path.join(hooksDir, "prompt-guard.sh"), promptGuardScript({
506
486
  strictMode: config.promptGuardMode === "strict"
507
487
  }));
508
488
  await writeFileSafe(path.join(hooksDir, "workflow-guard.sh"), workflowGuardScript());
509
489
  await writeFileSafe(path.join(hooksDir, "context-monitor.sh"), contextMonitorScript());
510
- await writeFileSafe(path.join(hooksDir, "observe.sh"), observeScript());
511
- await writeFileSafe(path.join(hooksDir, "summarize-observations.sh"), summarizeObservationsScript());
512
- await writeFileSafe(path.join(hooksDir, "summarize-observations.mjs"), summarizeObservationsRuntimeModule());
513
- const opencodePluginSource = opencodePluginJs({
514
- globalLearningsEnabled: config.globalLearnings === true,
515
- globalLearningsPath: config.globalLearningsPath
516
- });
490
+ const opencodePluginSource = opencodePluginJs();
517
491
  await writeFileSafe(path.join(hooksDir, "opencode-plugin.mjs"), opencodePluginSource);
518
492
  try {
519
493
  for (const script of [
@@ -522,9 +496,6 @@ async function writeHooks(projectRoot, config) {
522
496
  "prompt-guard.sh",
523
497
  "workflow-guard.sh",
524
498
  "context-monitor.sh",
525
- "observe.sh",
526
- "summarize-observations.sh",
527
- "summarize-observations.mjs",
528
499
  "opencode-plugin.mjs"
529
500
  ]) {
530
501
  await fs.chmod(path.join(hooksDir, script), 0o755);
@@ -565,20 +536,10 @@ async function writeHooks(projectRoot, config) {
565
536
  // OpenCode registration is auto-managed via opencode.json/opencode.jsonc.
566
537
  }
567
538
  }
568
- async function ensureLearningsStore(projectRoot) {
569
- const storePath = runtimePath(projectRoot, "learnings.jsonl");
539
+ async function ensureKnowledgeStore(projectRoot) {
540
+ const storePath = runtimePath(projectRoot, "knowledge.md");
570
541
  if (!(await exists(storePath))) {
571
- await writeFileSafe(storePath, "");
572
- }
573
- }
574
- async function ensureGlobalLearningsStore(projectRoot, config) {
575
- const globalPath = resolveGlobalLearningsPath(projectRoot, config);
576
- if (!globalPath) {
577
- return;
578
- }
579
- await ensureDir(path.dirname(globalPath));
580
- if (!(await exists(globalPath))) {
581
- await writeFileSafe(globalPath, "");
542
+ await writeFileSafe(storePath, "# Project Knowledge\n\n");
582
543
  }
583
544
  }
584
545
  async function ensureSessionStateFiles(projectRoot) {
@@ -723,6 +684,20 @@ async function cleanLegacyArtifacts(projectRoot) {
723
684
  // best-effort cleanup
724
685
  }
725
686
  }
687
+ for (const legacyRuntimeFile of [
688
+ runtimePath(projectRoot, "learnings.jsonl"),
689
+ runtimePath(projectRoot, "observations.jsonl"),
690
+ runtimePath(projectRoot, "hooks", "observe.sh"),
691
+ runtimePath(projectRoot, "hooks", "summarize-observations.sh"),
692
+ runtimePath(projectRoot, "hooks", "summarize-observations.mjs")
693
+ ]) {
694
+ try {
695
+ await fs.rm(legacyRuntimeFile, { force: true });
696
+ }
697
+ catch {
698
+ // best-effort cleanup
699
+ }
700
+ }
726
701
  }
727
702
  async function cleanStaleFiles(projectRoot) {
728
703
  const expectedShimFiles = new Set([
@@ -766,11 +741,10 @@ async function materializeRuntime(projectRoot, config, forceStateReset) {
766
741
  await writeArtifactTemplates(projectRoot);
767
742
  await writeRulebook(projectRoot);
768
743
  await writeState(projectRoot, forceStateReset);
769
- await ensureRunSystem(projectRoot);
744
+ await ensureRunSystem(projectRoot, { createIfMissing: false });
770
745
  await ensureSessionStateFiles(projectRoot);
771
746
  await writeAdapterManifest(projectRoot, harnesses);
772
- await ensureLearningsStore(projectRoot);
773
- await ensureGlobalLearningsStore(projectRoot, config);
747
+ await ensureKnowledgeStore(projectRoot);
774
748
  await writeHooks(projectRoot, config);
775
749
  await syncDisabledHarnessArtifacts(projectRoot, harnesses);
776
750
  await syncManagedGitHooks(projectRoot, config);
@@ -854,7 +828,7 @@ function stripManagedHookCommands(value) {
854
828
  }
855
829
  function isManagedRuntimeHookCommand(command) {
856
830
  const normalized = command.trim().replace(/\s+/gu, " ");
857
- return /(^|\s)(?:bash\s+)?(?:\.\/)?\.cclaw\/hooks\/(?:session-start|stop-checkpoint|prompt-guard|workflow-guard|context-monitor|observe|summarize-observations)\.sh(?:\s|$)/u.test(normalized);
831
+ return /(^|\s)(?:bash\s+)?(?:\.\/)?\.cclaw\/hooks\/(?:session-start|stop-checkpoint|prompt-guard|workflow-guard|context-monitor)\.sh(?:\s|$)/u.test(normalized);
858
832
  }
859
833
  async function removeManagedHookEntries(hookFilePath) {
860
834
  if (!(await exists(hookFilePath)))
package/dist/policy.js CHANGED
@@ -85,9 +85,8 @@ export async function policyChecks(projectRoot, options = {}) {
85
85
  // --- utility skill checks ---
86
86
  const runtimeFile = (relativePath) => `${RUNTIME_ROOT}/${relativePath}`;
87
87
  const utilitySkillChecks = [
88
- { file: runtimeFile("skills/learnings/SKILL.md"), needle: "## Learning Entry Schema", name: "utility_skill:learnings:schema" },
88
+ { file: runtimeFile("skills/learnings/SKILL.md"), needle: "## Entry format (append-only)", name: "utility_skill:learnings:entry_format" },
89
89
  { file: runtimeFile("skills/learnings/SKILL.md"), needle: "## Subcommands", name: "utility_skill:learnings:subcommands" },
90
- { file: runtimeFile("skills/learnings/SKILL.md"), needle: "## Confidence Decay", name: "utility_skill:learnings:decay" },
91
90
  { file: runtimeFile("skills/learnings/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:learnings:hard_gate" },
92
91
  { file: runtimeFile("commands/learn.md"), needle: "## Subcommands", name: "utility_command:learn:subcommands" },
93
92
  { file: runtimeFile("skills/subagent-dev/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:sdd:hard_gate" },
@@ -152,8 +151,6 @@ export async function policyChecks(projectRoot, options = {}) {
152
151
  { file: runtimeFile("hooks/workflow-guard.sh"), needle: "stage_invocation_without_recent_flow_read", name: "hooks:workflow_guard:flow_read_reason" },
153
152
  { file: runtimeFile("hooks/workflow-guard.sh"), needle: "stage_jump_", name: "hooks:workflow_guard:stage_jump_reason" },
154
153
  { file: runtimeFile("hooks/context-monitor.sh"), needle: "remaining is", name: "hooks:context:threshold_warning" },
155
- { file: runtimeFile("hooks/observe.sh"), needle: "stage-activity.jsonl", name: "hooks:observe:activity_write" },
156
- { file: runtimeFile("hooks/summarize-observations.mjs"), needle: "frequent-errors-", name: "hooks:summarize:runtime_module" },
157
154
  { file: runtimeFile("hooks/opencode-plugin.mjs"), needle: "activeRunId", name: "hooks:opencode:active_run" }
158
155
  ];
159
156
  if (activeHarnesses.has("opencode")) {
package/dist/runs.d.ts CHANGED
@@ -3,16 +3,20 @@ export interface CclawRunMeta {
3
3
  id: string;
4
4
  title: string;
5
5
  createdAt: string;
6
- archivedAt?: string;
7
- stateSnapshot?: Omit<FlowState, "activeRunId">;
6
+ }
7
+ export interface ArchiveRunResult {
8
+ archiveId: string;
9
+ archivePath: string;
10
+ archivedAt: string;
11
+ featureName: string;
12
+ resetState: FlowState;
13
+ }
14
+ interface EnsureRunSystemOptions {
15
+ createIfMissing?: boolean;
8
16
  }
9
17
  export declare function readFlowState(projectRoot: string): Promise<FlowState>;
10
18
  export declare function writeFlowState(projectRoot: string, state: FlowState): Promise<void>;
19
+ export declare function ensureRunSystem(projectRoot: string, _options?: EnsureRunSystemOptions): Promise<FlowState>;
11
20
  export declare function listRuns(projectRoot: string): Promise<CclawRunMeta[]>;
12
- export declare function ensureRunSystem(projectRoot: string): Promise<FlowState>;
13
- export declare function startNewFeatureRun(projectRoot: string, title?: string): Promise<CclawRunMeta>;
14
- export declare function resumeRun(projectRoot: string, runId: string): Promise<CclawRunMeta>;
15
- export declare function archiveRun(projectRoot: string, runId?: string): Promise<{
16
- archived: CclawRunMeta;
17
- active: CclawRunMeta;
18
- }>;
21
+ export declare function archiveRun(projectRoot: string, featureName?: string): Promise<ArchiveRunResult>;
22
+ export {};
package/dist/runs.js CHANGED
@@ -6,8 +6,6 @@ import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "./fs-utils.
6
6
  const FLOW_STATE_REL_PATH = `${RUNTIME_ROOT}/state/flow-state.json`;
7
7
  const RUNS_DIR_REL_PATH = `${RUNTIME_ROOT}/runs`;
8
8
  const ACTIVE_ARTIFACTS_REL_PATH = `${RUNTIME_ROOT}/artifacts`;
9
- const RUN_META_FILE = "run.json";
10
- const RUN_HANDOFF_FILE = "handoff.md";
11
9
  const FLOW_STAGE_SET = new Set(COMMAND_FILE_ORDER);
12
10
  function flowStatePath(projectRoot) {
13
11
  return path.join(projectRoot, FLOW_STATE_REL_PATH);
@@ -21,65 +19,6 @@ function runsRoot(projectRoot) {
21
19
  function activeArtifactsPath(projectRoot) {
22
20
  return path.join(projectRoot, ACTIVE_ARTIFACTS_REL_PATH);
23
21
  }
24
- function runRoot(projectRoot, runId) {
25
- return path.join(runsRoot(projectRoot), requireSafeRunId(runId));
26
- }
27
- function runArtifactsPath(projectRoot, runId) {
28
- return path.join(runRoot(projectRoot, runId), "artifacts");
29
- }
30
- function runMetaPath(projectRoot, runId) {
31
- return path.join(runRoot(projectRoot, runId), RUN_META_FILE);
32
- }
33
- function runHandoffPath(projectRoot, runId) {
34
- return path.join(runRoot(projectRoot, runId), RUN_HANDOFF_FILE);
35
- }
36
- function nowIso() {
37
- return new Date().toISOString();
38
- }
39
- function pad2(value) {
40
- return value.toString().padStart(2, "0");
41
- }
42
- function buildRunId(date = new Date()) {
43
- const yyyy = date.getUTCFullYear();
44
- const mm = pad2(date.getUTCMonth() + 1);
45
- const dd = pad2(date.getUTCDate());
46
- const hh = pad2(date.getUTCHours());
47
- const min = pad2(date.getUTCMinutes());
48
- const ss = pad2(date.getUTCSeconds());
49
- const random = Math.random().toString(36).slice(2, 6);
50
- return `run-${yyyy}${mm}${dd}-${hh}${min}${ss}-${random}`;
51
- }
52
- function normalizeTitle(title) {
53
- const trimmed = (title ?? "").trim();
54
- if (trimmed.length === 0) {
55
- return "New feature run";
56
- }
57
- return trimmed;
58
- }
59
- function isSafeRunId(value) {
60
- return /^[A-Za-z0-9_-]{1,128}$/u.test(value);
61
- }
62
- function sanitizeRunId(value) {
63
- if (typeof value !== "string")
64
- return undefined;
65
- const trimmed = value.trim();
66
- return isSafeRunId(trimmed) ? trimmed : undefined;
67
- }
68
- function requireSafeRunId(runId) {
69
- const safe = sanitizeRunId(runId);
70
- if (!safe) {
71
- throw new Error(`Invalid run id "${runId}"`);
72
- }
73
- return safe;
74
- }
75
- function snapshotState(state) {
76
- return {
77
- currentStage: state.currentStage,
78
- completedStages: [...state.completedStages],
79
- guardEvidence: { ...state.guardEvidence },
80
- stageGateCatalog: JSON.parse(JSON.stringify(state.stageGateCatalog))
81
- };
82
- }
83
22
  function isFlowStage(value) {
84
23
  return typeof value === "string" && FLOW_STAGE_SET.has(value);
85
24
  }
@@ -144,296 +83,148 @@ function sanitizeStageGateCatalog(value, fallback) {
144
83
  }
145
84
  return next;
146
85
  }
147
- function coerceFlowState(parsed, activeRunIdOverride) {
148
- const overrideTrim = sanitizeRunId(activeRunIdOverride);
149
- const parsedActiveRun = sanitizeRunId(parsed.activeRunId);
150
- const seedRunId = overrideTrim ?? parsedActiveRun;
151
- const next = createInitialFlowState(seedRunId);
86
+ function coerceFlowState(parsed) {
87
+ const next = createInitialFlowState();
88
+ const activeRunIdRaw = parsed.activeRunId;
89
+ const activeRunId = typeof activeRunIdRaw === "string" && activeRunIdRaw.trim().length > 0
90
+ ? activeRunIdRaw.trim()
91
+ : next.activeRunId;
152
92
  return {
153
- activeRunId: overrideTrim ?? parsedActiveRun ?? next.activeRunId,
93
+ activeRunId,
154
94
  currentStage: isFlowStage(parsed.currentStage) ? parsed.currentStage : next.currentStage,
155
95
  completedStages: sanitizeCompletedStages(parsed.completedStages),
156
96
  guardEvidence: sanitizeGuardEvidence(parsed.guardEvidence),
157
97
  stageGateCatalog: sanitizeStageGateCatalog(parsed.stageGateCatalog, next.stageGateCatalog)
158
98
  };
159
99
  }
160
- function createdAtFromRunId(runId) {
161
- const match = /^run-(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})-[a-z0-9]+$/iu.exec(runId);
162
- if (!match) {
163
- return null;
100
+ function toArchiveDate(date = new Date()) {
101
+ const yyyy = date.getFullYear().toString();
102
+ const mm = (date.getMonth() + 1).toString().padStart(2, "0");
103
+ const dd = date.getDate().toString().padStart(2, "0");
104
+ return `${yyyy}-${mm}-${dd}`;
105
+ }
106
+ function slugifyFeatureName(value) {
107
+ const slug = value
108
+ .toLowerCase()
109
+ .trim()
110
+ .replace(/[^a-z0-9]+/gu, "-")
111
+ .replace(/^-+/u, "")
112
+ .replace(/-+$/u, "");
113
+ if (slug.length === 0) {
114
+ return "feature";
115
+ }
116
+ return slug.slice(0, 64);
117
+ }
118
+ async function inferFeatureNameFromArtifacts(projectRoot) {
119
+ const ideaPath = path.join(projectRoot, ACTIVE_ARTIFACTS_REL_PATH, "00-idea.md");
120
+ if (!(await exists(ideaPath))) {
121
+ return "feature";
164
122
  }
165
- const [, year, month, day, hour, minute, second] = match;
166
- const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second)));
167
- return Number.isNaN(date.getTime()) ? null : date.toISOString();
168
- }
169
- async function readJsonFile(filePath) {
170
- if (!(await exists(filePath)))
171
- return null;
172
123
  try {
173
- return JSON.parse(await fs.readFile(filePath, "utf8"));
124
+ const raw = await fs.readFile(ideaPath, "utf8");
125
+ const firstMeaningful = raw
126
+ .split(/\r?\n/gu)
127
+ .map((line) => line.trim())
128
+ .find((line) => line.length > 0);
129
+ if (!firstMeaningful) {
130
+ return "feature";
131
+ }
132
+ return firstMeaningful.replace(/^[-#*\s]+/u, "").trim() || "feature";
174
133
  }
175
134
  catch {
176
- return null;
177
- }
178
- }
179
- async function listImmediateFiles(dirPath) {
180
- if (!(await exists(dirPath)))
181
- return [];
182
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
183
- return entries.filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
184
- }
185
- async function clearImmediateFiles(dirPath) {
186
- if (!(await exists(dirPath)))
187
- return;
188
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
189
- for (const entry of entries) {
190
- if (entry.isFile()) {
191
- await fs.rm(path.join(dirPath, entry.name), { force: true });
192
- }
135
+ return "feature";
193
136
  }
194
137
  }
195
- async function copyImmediateFiles(fromDir, toDir) {
196
- await ensureDir(toDir);
197
- const fileNames = await listImmediateFiles(fromDir);
198
- for (const fileName of fileNames) {
199
- const sourcePath = path.join(fromDir, fileName);
200
- const targetPath = path.join(toDir, fileName);
201
- await fs.copyFile(sourcePath, targetPath);
138
+ async function uniqueArchiveId(projectRoot, baseId) {
139
+ let index = 1;
140
+ let candidate = baseId;
141
+ while (await exists(path.join(runsRoot(projectRoot), candidate))) {
142
+ index += 1;
143
+ candidate = `${baseId}-${index}`;
202
144
  }
203
- }
204
- function handoffMarkdown(runMeta, state) {
205
- return `# Run Handoff
206
-
207
- ## Run
208
- - ID: ${runMeta.id}
209
- - Title: ${runMeta.title}
210
- - Created: ${runMeta.createdAt}
211
- - Archived: ${runMeta.archivedAt ?? "active"}
212
-
213
- ## Flow Snapshot
214
- - Active stage: ${state.currentStage}
215
- - Completed stages: ${state.completedStages.join(", ") || "(none)"}
216
- - Active run ID in flow-state: ${state.activeRunId}
217
-
218
- ## Paths
219
- - Active artifacts: \`${RUNTIME_ROOT}/artifacts/\`
220
- - Canonical run artifacts: \`${RUNTIME_ROOT}/runs/${runMeta.id}/artifacts/\`
221
-
222
- ## Resume
223
- 1. Continue with the stage command for \`${state.currentStage}\`
224
- 2. If needed, sync artifacts from \`${RUNTIME_ROOT}/runs/${runMeta.id}/artifacts/\`
225
- `;
145
+ return candidate;
226
146
  }
227
147
  export async function readFlowState(projectRoot) {
228
148
  const statePath = flowStatePath(projectRoot);
229
- const parsed = await readJsonFile(statePath);
230
- if (!parsed || typeof parsed !== "object") {
149
+ if (!(await exists(statePath))) {
150
+ return createInitialFlowState();
151
+ }
152
+ try {
153
+ const parsed = JSON.parse(await fs.readFile(statePath, "utf8"));
154
+ return coerceFlowState(parsed);
155
+ }
156
+ catch {
231
157
  return createInitialFlowState();
232
158
  }
233
- return coerceFlowState(parsed);
234
159
  }
235
160
  export async function writeFlowState(projectRoot, state) {
236
161
  await withDirectoryLock(flowStateLockPath(projectRoot), async () => {
237
- const safe = coerceFlowState({ ...state }, state.activeRunId);
162
+ const safe = coerceFlowState({ ...state });
238
163
  await writeFileSafe(flowStatePath(projectRoot), `${JSON.stringify(safe, null, 2)}\n`);
239
164
  });
240
165
  }
166
+ export async function ensureRunSystem(projectRoot, _options = {}) {
167
+ await ensureDir(runsRoot(projectRoot));
168
+ await ensureDir(activeArtifactsPath(projectRoot));
169
+ const statePath = flowStatePath(projectRoot);
170
+ const state = await readFlowState(projectRoot);
171
+ if (!(await exists(statePath))) {
172
+ await writeFlowState(projectRoot, state);
173
+ }
174
+ return state;
175
+ }
241
176
  export async function listRuns(projectRoot) {
242
177
  const root = runsRoot(projectRoot);
243
- if (!(await exists(root)))
178
+ if (!(await exists(root))) {
244
179
  return [];
245
- const dirs = await fs.readdir(root, { withFileTypes: true });
246
- const metas = [];
247
- for (const dir of dirs) {
248
- if (!dir.isDirectory())
249
- continue;
250
- const runId = dir.name;
251
- if (!isSafeRunId(runId))
252
- continue;
253
- const meta = await readJsonFile(runMetaPath(projectRoot, runId));
254
- if (meta && typeof meta.id === "string" && meta.id === runId) {
255
- metas.push(meta);
180
+ }
181
+ const entries = await fs.readdir(root, { withFileTypes: true });
182
+ const runs = [];
183
+ for (const entry of entries) {
184
+ if (!entry.isDirectory()) {
256
185
  continue;
257
186
  }
258
- let fallbackCreatedAt = createdAtFromRunId(runId);
259
- if (!fallbackCreatedAt) {
260
- try {
261
- const stat = await fs.stat(path.join(root, runId));
262
- fallbackCreatedAt = stat.birthtime?.toISOString?.() ?? stat.mtime.toISOString();
263
- }
264
- catch {
265
- fallbackCreatedAt = null;
266
- }
187
+ const runPath = path.join(root, entry.name);
188
+ let createdAt = new Date().toISOString();
189
+ try {
190
+ const stat = await fs.stat(runPath);
191
+ createdAt = stat.birthtime?.toISOString?.() ?? stat.mtime.toISOString();
192
+ }
193
+ catch {
194
+ // keep fallback timestamp
267
195
  }
268
- metas.push({
269
- id: runId,
270
- title: runId,
271
- createdAt: fallbackCreatedAt ?? nowIso()
196
+ runs.push({
197
+ id: entry.name,
198
+ title: entry.name,
199
+ createdAt
272
200
  });
273
201
  }
274
- return metas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
275
- }
276
- async function ensureRunMetadata(projectRoot, meta) {
277
- await writeFileSafe(runMetaPath(projectRoot, meta.id), `${JSON.stringify(meta, null, 2)}\n`);
278
- }
279
- async function persistRunStateSnapshot(projectRoot, runId, state) {
280
- const meta = await readJsonFile(runMetaPath(projectRoot, runId));
281
- if (!meta)
282
- return;
283
- const safeState = coerceFlowState({ ...state }, state.activeRunId);
284
- await ensureRunMetadata(projectRoot, {
285
- ...meta,
286
- stateSnapshot: snapshotState(safeState)
287
- });
288
- }
289
- async function syncActiveArtifactsToRun(projectRoot, runId) {
290
- const fromDir = activeArtifactsPath(projectRoot);
291
- const toDir = runArtifactsPath(projectRoot, runId);
292
- await ensureDir(toDir);
293
- await clearImmediateFiles(toDir);
294
- await copyImmediateFiles(fromDir, toDir);
295
- }
296
- async function loadRunArtifactsToActive(projectRoot, runId) {
297
- const fromDir = runArtifactsPath(projectRoot, runId);
298
- const toDir = activeArtifactsPath(projectRoot);
299
- await ensureDir(toDir);
300
- await clearImmediateFiles(toDir);
301
- await copyImmediateFiles(fromDir, toDir);
302
- }
303
- async function createRun(projectRoot, options) {
304
- const runId = buildRunId();
305
- const meta = {
306
- id: runId,
307
- title: normalizeTitle(options?.title),
308
- createdAt: nowIso()
309
- };
310
- await ensureDir(runRoot(projectRoot, runId));
311
- await ensureRunMetadata(projectRoot, meta);
312
- const runArtifactsDir = runArtifactsPath(projectRoot, runId);
313
- await ensureDir(runArtifactsDir);
314
- if (options?.seedFromActiveArtifacts && (await exists(activeArtifactsPath(projectRoot)))) {
315
- await copyImmediateFiles(activeArtifactsPath(projectRoot), runArtifactsDir);
316
- }
317
- return meta;
318
- }
319
- async function ensureRunHandoff(projectRoot, runId) {
320
- const state = await readFlowState(projectRoot);
321
- const meta = await readJsonFile(runMetaPath(projectRoot, runId));
322
- if (!meta)
323
- return;
324
- await writeFileSafe(runHandoffPath(projectRoot, runId), handoffMarkdown(meta, state));
202
+ return runs.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
325
203
  }
326
- export async function ensureRunSystem(projectRoot) {
327
- await ensureDir(runsRoot(projectRoot));
328
- await ensureDir(activeArtifactsPath(projectRoot));
329
- let state = await readFlowState(projectRoot);
330
- let activeRunId = state.activeRunId;
331
- const activeRunExists = activeRunId.trim().length > 0 && (await exists(runArtifactsPath(projectRoot, activeRunId)));
332
- if (!activeRunExists) {
333
- const activeHasArtifacts = (await listImmediateFiles(activeArtifactsPath(projectRoot))).length > 0;
334
- const initialRun = await createRun(projectRoot, {
335
- title: activeHasArtifacts ? "Migrated active run" : "Initial feature run",
336
- seedFromActiveArtifacts: activeHasArtifacts
337
- });
338
- activeRunId = initialRun.id;
339
- state = { ...state, activeRunId };
340
- await writeFlowState(projectRoot, state);
341
- }
342
- const runArtifactsDir = runArtifactsPath(projectRoot, activeRunId);
343
- await ensureDir(runArtifactsDir);
344
- if ((await listImmediateFiles(activeArtifactsPath(projectRoot))).length === 0) {
345
- await loadRunArtifactsToActive(projectRoot, activeRunId);
346
- }
347
- else {
348
- await syncActiveArtifactsToRun(projectRoot, activeRunId);
349
- }
350
- await persistRunStateSnapshot(projectRoot, activeRunId, state);
351
- await ensureRunHandoff(projectRoot, activeRunId);
352
- return state;
353
- }
354
- export async function startNewFeatureRun(projectRoot, title) {
355
- await ensureRunSystem(projectRoot);
356
- const state = await readFlowState(projectRoot);
357
- await syncActiveArtifactsToRun(projectRoot, state.activeRunId);
358
- await persistRunStateSnapshot(projectRoot, state.activeRunId, state);
359
- await ensureRunHandoff(projectRoot, state.activeRunId);
360
- const nextRun = await createRun(projectRoot, {
361
- title,
362
- seedFromActiveArtifacts: false
363
- });
364
- const nextState = {
365
- ...createInitialFlowState(nextRun.id),
366
- activeRunId: nextRun.id
367
- };
368
- await writeFlowState(projectRoot, nextState);
369
- await persistRunStateSnapshot(projectRoot, nextRun.id, nextState);
370
- await loadRunArtifactsToActive(projectRoot, nextRun.id);
371
- await ensureRunHandoff(projectRoot, nextRun.id);
372
- return nextRun;
373
- }
374
- export async function resumeRun(projectRoot, runId) {
204
+ export async function archiveRun(projectRoot, featureName) {
375
205
  await ensureRunSystem(projectRoot);
376
- const safeRunId = requireSafeRunId(runId);
377
- const targetMeta = await readJsonFile(runMetaPath(projectRoot, safeRunId));
378
- if (!targetMeta) {
379
- throw new Error(`Run "${safeRunId}" not found under ${RUNTIME_ROOT}/runs/`);
380
- }
381
- const state = await readFlowState(projectRoot);
382
- await syncActiveArtifactsToRun(projectRoot, state.activeRunId);
383
- await persistRunStateSnapshot(projectRoot, state.activeRunId, state);
384
- await ensureRunHandoff(projectRoot, state.activeRunId);
385
- const nextState = targetMeta.stateSnapshot
386
- ? coerceFlowState({
387
- ...createInitialFlowState(safeRunId),
388
- ...targetMeta.stateSnapshot,
389
- activeRunId: safeRunId
390
- }, safeRunId)
391
- : coerceFlowState({
392
- ...createInitialFlowState(safeRunId),
393
- activeRunId: safeRunId
394
- }, safeRunId);
395
- await writeFlowState(projectRoot, nextState);
396
- await persistRunStateSnapshot(projectRoot, safeRunId, nextState);
397
- await loadRunArtifactsToActive(projectRoot, safeRunId);
398
- await ensureRunHandoff(projectRoot, safeRunId);
399
- return targetMeta;
400
- }
401
- export async function archiveRun(projectRoot, runId) {
402
- await ensureRunSystem(projectRoot);
403
- const state = await readFlowState(projectRoot);
404
- const targetRunId = runId ? requireSafeRunId(runId) : state.activeRunId;
405
- const targetMeta = await readJsonFile(runMetaPath(projectRoot, targetRunId));
406
- if (!targetMeta) {
407
- throw new Error(`Run "${targetRunId}" not found under ${RUNTIME_ROOT}/runs/`);
408
- }
409
- if (targetRunId === state.activeRunId) {
410
- await syncActiveArtifactsToRun(projectRoot, targetRunId);
411
- await persistRunStateSnapshot(projectRoot, targetRunId, state);
412
- }
413
- const archivedMeta = {
414
- ...targetMeta,
415
- archivedAt: nowIso()
416
- };
417
- await ensureRunMetadata(projectRoot, archivedMeta);
418
- await ensureRunHandoff(projectRoot, targetRunId);
419
- if (targetRunId !== state.activeRunId) {
420
- const activeMeta = await readJsonFile(runMetaPath(projectRoot, state.activeRunId));
421
- if (!activeMeta) {
422
- throw new Error(`Active run "${state.activeRunId}" is missing metadata`);
423
- }
424
- return { archived: archivedMeta, active: activeMeta };
425
- }
426
- const nextRun = await createRun(projectRoot, {
427
- title: "Post-archive run",
428
- seedFromActiveArtifacts: false
429
- });
430
- const nextState = {
431
- ...createInitialFlowState(nextRun.id),
432
- activeRunId: nextRun.id
206
+ const artifactsDir = activeArtifactsPath(projectRoot);
207
+ const runsDir = runsRoot(projectRoot);
208
+ await ensureDir(runsDir);
209
+ await ensureDir(artifactsDir);
210
+ const feature = (featureName?.trim() && featureName.trim().length > 0)
211
+ ? featureName.trim()
212
+ : await inferFeatureNameFromArtifacts(projectRoot);
213
+ const archiveBaseId = `${toArchiveDate()}-${slugifyFeatureName(feature)}`;
214
+ const archiveId = await uniqueArchiveId(projectRoot, archiveBaseId);
215
+ const archivePath = path.join(runsDir, archiveId);
216
+ const archiveArtifactsPath = path.join(archivePath, "artifacts");
217
+ await ensureDir(archivePath);
218
+ await fs.rename(artifactsDir, archiveArtifactsPath);
219
+ await ensureDir(artifactsDir);
220
+ const resetState = createInitialFlowState();
221
+ await writeFlowState(projectRoot, resetState);
222
+ const archivedAt = new Date().toISOString();
223
+ return {
224
+ archiveId,
225
+ archivePath,
226
+ archivedAt,
227
+ featureName: feature,
228
+ resetState
433
229
  };
434
- await writeFlowState(projectRoot, nextState);
435
- await persistRunStateSnapshot(projectRoot, nextRun.id, nextState);
436
- await loadRunArtifactsToActive(projectRoot, nextRun.id);
437
- await ensureRunHandoff(projectRoot, nextRun.id);
438
- return { archived: archivedMeta, active: nextRun };
439
230
  }