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/README.md +8 -11
- package/dist/artifact-linter.js +41 -13
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +12 -1
- package/dist/config.js +0 -19
- package/dist/content/contracts.js +7 -12
- package/dist/content/examples.js +42 -26
- package/dist/content/hooks.d.ts +4 -6
- package/dist/content/hooks.js +105 -435
- package/dist/content/learnings.js +55 -203
- package/dist/content/meta-skill.js +8 -11
- package/dist/content/next-command.js +3 -3
- package/dist/content/observe.d.ts +4 -7
- package/dist/content/observe.js +81 -55
- package/dist/content/session-hooks.js +8 -8
- package/dist/content/skills.js +9 -16
- package/dist/content/stage-schema.js +80 -97
- package/dist/content/templates.d.ts +1 -1
- package/dist/content/templates.js +27 -48
- package/dist/delegation.js +7 -7
- package/dist/doctor.js +17 -34
- package/dist/flow-state.js +1 -1
- package/dist/harness-adapters.js +1 -1
- package/dist/install.js +23 -49
- package/dist/policy.js +1 -4
- package/dist/runs.d.ts +13 -9
- package/dist/runs.js +108 -317
- package/dist/trace-matrix.js +8 -18
- package/dist/types.d.ts +0 -4
- package/package.json +1 -1
- package/dist/learnings-summarizer.d.ts +0 -25
- package/dist/learnings-summarizer.js +0 -201
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,
|
|
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
|
-
|
|
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
|
|
569
|
-
const storePath = runtimePath(projectRoot, "
|
|
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
|
|
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
|
|
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: "##
|
|
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
|
-
|
|
7
|
-
|
|
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
|
|
13
|
-
export
|
|
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
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
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
|
|
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
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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 }
|
|
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
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
269
|
-
id:
|
|
270
|
-
title:
|
|
271
|
-
createdAt
|
|
196
|
+
runs.push({
|
|
197
|
+
id: entry.name,
|
|
198
|
+
title: entry.name,
|
|
199
|
+
createdAt
|
|
272
200
|
});
|
|
273
201
|
}
|
|
274
|
-
return
|
|
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
|
|
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
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
await
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
}
|