@jakkrichm/create-nexus-devflow 2.6.0 → 2.6.2
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/lib/branch-context.d.ts +61 -8
- package/dist/lib/branch-context.js +370 -125
- package/dist/lib/branch-context.js.map +1 -1
- package/dist/lib/current-work.js +15 -108
- package/dist/lib/current-work.js.map +1 -1
- package/dist/lib/discoveries.js +4 -15
- package/dist/lib/discoveries.js.map +1 -1
- package/dist/lib/history.js +1 -1
- package/dist/lib/history.js.map +1 -1
- package/dist/lib/status.d.ts +2 -0
- package/dist/lib/status.js +9 -16
- package/dist/lib/status.js.map +1 -1
- package/package.json +1 -1
- package/template/.agents/skills/adopt/SKILL.md +1 -1
- package/template/.agents/skills/audit/SKILL.md +1 -1
- package/template/.agents/skills/autopilot/SKILL.md +1 -1
- package/template/.agents/skills/brainstorm/SKILL.md +2 -2
- package/template/.agents/skills/brief/SKILL.md +1 -1
- package/template/.agents/skills/check/SKILL.md +5 -3
- package/template/.agents/skills/ci/SKILL.md +1 -1
- package/template/.agents/skills/complete/SKILL.md +9 -3
- package/template/.agents/skills/convert-any-to-md/SKILL.md +1 -1
- package/template/.agents/skills/debug/SKILL.md +1 -1
- package/template/.agents/skills/devflow/SKILL.md +1 -1
- package/template/.agents/skills/discovery/SKILL.md +1 -1
- package/template/.agents/skills/doctor/SKILL.md +1 -1
- package/template/.agents/skills/feature/SKILL.md +10 -5
- package/template/.agents/skills/fix/SKILL.md +6 -8
- package/template/.agents/skills/grill/SKILL.md +3 -3
- package/template/.agents/skills/idea/SKILL.md +3 -3
- package/template/.agents/skills/implement/SKILL.md +10 -4
- package/template/.agents/skills/onboard/SKILL.md +1 -1
- package/template/.agents/skills/overview/SKILL.md +1 -1
- package/template/.agents/skills/prototype/SKILL.md +1 -1
- package/template/.agents/skills/release/SKILL.md +1 -1
- package/template/.agents/skills/report-html/SKILL.md +13 -13
- package/template/.agents/skills/rollback/SKILL.md +1 -1
- package/template/.agents/skills/status/SKILL.md +1 -1
- package/template/.agents/skills/test/SKILL.md +5 -5
- package/template/.agents/skills/tests/SKILL.md +1 -1
- package/template/.agents/skills/try/SKILL.md +1 -1
- package/template/.claude/skills/adopt/SKILL.md +1 -1
- package/template/.claude/skills/audit/SKILL.md +1 -1
- package/template/.claude/skills/autopilot/SKILL.md +1 -1
- package/template/.claude/skills/brainstorm/SKILL.md +2 -2
- package/template/.claude/skills/brief/SKILL.md +1 -1
- package/template/.claude/skills/check/SKILL.md +5 -3
- package/template/.claude/skills/ci/SKILL.md +1 -1
- package/template/.claude/skills/complete/SKILL.md +9 -3
- package/template/.claude/skills/convert-any-to-md/SKILL.md +1 -1
- package/template/.claude/skills/debug/SKILL.md +1 -1
- package/template/.claude/skills/devflow/SKILL.md +1 -1
- package/template/.claude/skills/discovery/SKILL.md +1 -1
- package/template/.claude/skills/doctor/SKILL.md +1 -1
- package/template/.claude/skills/feature/SKILL.md +10 -5
- package/template/.claude/skills/fix/SKILL.md +6 -8
- package/template/.claude/skills/grill/SKILL.md +3 -3
- package/template/.claude/skills/idea/SKILL.md +3 -3
- package/template/.claude/skills/implement/SKILL.md +10 -4
- package/template/.claude/skills/onboard/SKILL.md +1 -1
- package/template/.claude/skills/overview/SKILL.md +1 -1
- package/template/.claude/skills/prototype/SKILL.md +1 -1
- package/template/.claude/skills/release/SKILL.md +1 -1
- package/template/.claude/skills/report-html/SKILL.md +13 -13
- package/template/.claude/skills/rollback/SKILL.md +1 -1
- package/template/.claude/skills/status/SKILL.md +1 -1
- package/template/.claude/skills/test/SKILL.md +5 -5
- package/template/.claude/skills/tests/SKILL.md +1 -1
- package/template/.claude/skills/try/SKILL.md +1 -1
- package/template/AGENTS.md +1 -1
- package/template/devflow/build-plan.md +13 -1
- package/template/devflow/context/ai-interaction.md +1 -1
- package/template/devflow/context/coding-standards.md +2 -2
- package/template/devflow/decisions/README.md +1 -1
- package/template/devflow/history/features/README.md +1 -1
- package/template/devflow/history/fixes/README.md +1 -1
- package/template/devflow/history/rollbacks/README.md +1 -1
- package/template/devflow/reference/mockup.html +16 -31
- package/template/devflow/reference/running-id-contract.md +24 -20
|
@@ -7,29 +7,82 @@ export interface ActiveContextPaths {
|
|
|
7
7
|
stagePath: string;
|
|
8
8
|
findingsPath: string;
|
|
9
9
|
}
|
|
10
|
+
export interface RunContextPaths {
|
|
11
|
+
isMultiRun: boolean;
|
|
12
|
+
runId: string | null;
|
|
13
|
+
runDir: string | null;
|
|
14
|
+
specPath: string;
|
|
15
|
+
stagePath: string;
|
|
16
|
+
findingsPath: string;
|
|
17
|
+
globalOverviewPath: string;
|
|
18
|
+
globalStandardsPath: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ActiveRunSummary {
|
|
21
|
+
runId: string;
|
|
22
|
+
title: string;
|
|
23
|
+
status: string;
|
|
24
|
+
track: string;
|
|
25
|
+
branch: string;
|
|
26
|
+
totalTasks: number;
|
|
27
|
+
completedTasks: number;
|
|
28
|
+
remainingTasks: number;
|
|
29
|
+
hasOpenFindings: boolean;
|
|
30
|
+
runDir: string;
|
|
31
|
+
specPath: string;
|
|
32
|
+
}
|
|
33
|
+
export interface InitRunOptions {
|
|
34
|
+
branch?: string;
|
|
35
|
+
track?: string;
|
|
36
|
+
status?: string;
|
|
37
|
+
initialSpec?: string;
|
|
38
|
+
}
|
|
10
39
|
/**
|
|
11
40
|
* Sanitizes a Git branch name into a cross-platform safe directory name.
|
|
12
41
|
* e.g. "feature/044-auth#login" -> "feature-044-auth-login"
|
|
13
42
|
*/
|
|
14
43
|
export declare function sanitizeBranchName(branch: string): string;
|
|
15
44
|
/**
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*
|
|
45
|
+
* Lists all active run contexts under devflow/context/{xxx-slug}/
|
|
46
|
+
*/
|
|
47
|
+
export declare function listActiveRunContexts(projectRoot: string): Promise<ActiveRunSummary[]>;
|
|
48
|
+
/**
|
|
49
|
+
* Fuzzy matches a user input string to an ActiveRunSummary.
|
|
50
|
+
* Matches:
|
|
51
|
+
* 1. Exact runId
|
|
52
|
+
* 2. Leading number (e.g. "1" -> "001-*", "12" -> "012-*")
|
|
53
|
+
* 3. Case-insensitive substring match in runId or title
|
|
54
|
+
*/
|
|
55
|
+
export declare function fuzzyMatchRunId(targetInput: string, activeRuns: ActiveRunSummary[]): ActiveRunSummary | null;
|
|
56
|
+
/**
|
|
57
|
+
* Resolves active run context with Multi-Run and fallback support.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveActiveRunContext(projectRoot: string, targetIdOrBranch?: string): Promise<RunContextPaths>;
|
|
60
|
+
/**
|
|
61
|
+
* Initializes a new run context under devflow/context/{runId}/
|
|
62
|
+
*/
|
|
63
|
+
export declare function initRunContext(projectRoot: string, runId: string, title: string, options?: InitRunOptions): Promise<RunContextPaths>;
|
|
64
|
+
/**
|
|
65
|
+
* Cleans up a run context directory after completion.
|
|
66
|
+
*/
|
|
67
|
+
export declare function cleanupRunContext(projectRoot: string, runId: string): Promise<boolean>;
|
|
68
|
+
/**
|
|
69
|
+
* Calculates the next sequential 3-digit running ID by scanning history archives and active run contexts.
|
|
70
|
+
*/
|
|
71
|
+
export declare function calculateNextRunningId(projectRoot: string): Promise<string>;
|
|
72
|
+
/**
|
|
73
|
+
* Resolves active context paths for the current repository and git branch (Legacy/Interoperable helper).
|
|
21
74
|
*/
|
|
22
75
|
export declare function resolveActiveContextPaths(projectRoot: string, explicitBranch?: string): Promise<ActiveContextPaths>;
|
|
23
76
|
/**
|
|
24
|
-
* Initializes a branch-scoped context directory
|
|
77
|
+
* Initializes a branch-scoped context directory (Backward compatibility wrapper).
|
|
25
78
|
*/
|
|
26
79
|
export declare function initBranchContext(projectRoot: string, branchName: string, initialSpec?: string, useDevflowContextDir?: boolean): Promise<ActiveContextPaths>;
|
|
27
80
|
/**
|
|
28
|
-
* Cleans up branch-scoped context directories
|
|
81
|
+
* Cleans up branch-scoped context directories (Backward compatibility wrapper).
|
|
29
82
|
*/
|
|
30
83
|
export declare function cleanupBranchContext(projectRoot: string, branchName: string): Promise<boolean>;
|
|
31
84
|
/**
|
|
32
|
-
* Lists all active branch isolated contexts.
|
|
85
|
+
* Lists all active branch isolated contexts (Backward compatibility wrapper).
|
|
33
86
|
*/
|
|
34
87
|
export declare function listBranchContexts(projectRoot: string): Promise<Array<{
|
|
35
88
|
branch: string;
|
|
@@ -2,6 +2,16 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { readGitStatus } from "./git-status.js";
|
|
5
|
+
const RESERVED_CONTEXT_DIRS = new Set([
|
|
6
|
+
"current-run",
|
|
7
|
+
"findings.md",
|
|
8
|
+
"current-feature.md",
|
|
9
|
+
"current-stage.md",
|
|
10
|
+
"coding-standards.md",
|
|
11
|
+
"project-overview.md",
|
|
12
|
+
"ai-interaction.md",
|
|
13
|
+
"glossary.md"
|
|
14
|
+
]);
|
|
5
15
|
/**
|
|
6
16
|
* Sanitizes a Git branch name into a cross-platform safe directory name.
|
|
7
17
|
* e.g. "feature/044-auth#login" -> "feature-044-auth-login"
|
|
@@ -15,181 +25,416 @@ export function sanitizeBranchName(branch) {
|
|
|
15
25
|
.toLowerCase();
|
|
16
26
|
}
|
|
17
27
|
/**
|
|
18
|
-
*
|
|
19
|
-
* Checks for branch-isolated context in:
|
|
20
|
-
* 1. devflow/context/<sanitized>/ (Primary user-visible multi-dev location)
|
|
21
|
-
* 2. .nexus/branches/<sanitized>/ (Internal branch cache)
|
|
22
|
-
* 3. devflow/context/ (Default fallback)
|
|
28
|
+
* Parses checklist tasks and status from a living spec content.
|
|
23
29
|
*/
|
|
24
|
-
|
|
30
|
+
function parseSpecTasks(content) {
|
|
31
|
+
let title = "Living Spec";
|
|
32
|
+
const titleMatch = content.match(/^#\s+(?:📐\s+)?(?:\[([^\]]+)\]\s+)?(.*)/m);
|
|
33
|
+
if (titleMatch) {
|
|
34
|
+
title = titleMatch[2]?.trim() || titleMatch[1]?.trim() || title;
|
|
35
|
+
}
|
|
36
|
+
let total = 0;
|
|
37
|
+
let completed = 0;
|
|
38
|
+
const lines = content.split("\n");
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const match = line.match(/^\s*[-*]\s+\[([ xX])\]\s+(.+)/);
|
|
41
|
+
if (match) {
|
|
42
|
+
total++;
|
|
43
|
+
if (match[1].toLowerCase() === "x") {
|
|
44
|
+
completed++;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { total, completed, title };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Lists all active run contexts under devflow/context/{xxx-slug}/
|
|
52
|
+
*/
|
|
53
|
+
export async function listActiveRunContexts(projectRoot) {
|
|
54
|
+
const results = [];
|
|
55
|
+
const contextRoot = path.join(projectRoot, "devflow", "context");
|
|
56
|
+
if (!fsSync.existsSync(contextRoot)) {
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const entries = await fs.readdir(contextRoot, { withFileTypes: true });
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (entry.isDirectory() && !RESERVED_CONTEXT_DIRS.has(entry.name)) {
|
|
63
|
+
const runDir = path.join(contextRoot, entry.name);
|
|
64
|
+
const specPath = fsSync.existsSync(path.join(runDir, "spec.md"))
|
|
65
|
+
? path.join(runDir, "spec.md")
|
|
66
|
+
: path.join(runDir, "current-feature.md");
|
|
67
|
+
if (fsSync.existsSync(specPath)) {
|
|
68
|
+
const specContent = await fs.readFile(specPath, "utf8");
|
|
69
|
+
const { total, completed, title } = parseSpecTasks(specContent);
|
|
70
|
+
let track = "fast";
|
|
71
|
+
let status = "spec_ready";
|
|
72
|
+
let branch = `feature/${entry.name}`;
|
|
73
|
+
const stagePath = path.join(runDir, "stage.md");
|
|
74
|
+
if (fsSync.existsSync(stagePath)) {
|
|
75
|
+
const stageContent = await fs.readFile(stagePath, "utf8");
|
|
76
|
+
const trackMatch = stageContent.match(/Track:\s*`?([a-zA-Z0-9_-]+)`?/i);
|
|
77
|
+
const branchMatch = stageContent.match(/Branch:\s*`?([a-zA-Z0-9_/.-]+)`?/i);
|
|
78
|
+
const statusMatch = stageContent.match(/Status:\s*`?([a-zA-Z0-9_-]+)`?/i);
|
|
79
|
+
if (trackMatch)
|
|
80
|
+
track = trackMatch[1];
|
|
81
|
+
if (branchMatch)
|
|
82
|
+
branch = branchMatch[1];
|
|
83
|
+
if (statusMatch)
|
|
84
|
+
status = statusMatch[1];
|
|
85
|
+
}
|
|
86
|
+
let hasOpenFindings = false;
|
|
87
|
+
const findingsPath = path.join(runDir, "findings.md");
|
|
88
|
+
if (fsSync.existsSync(findingsPath)) {
|
|
89
|
+
const findingsContent = await fs.readFile(findingsPath, "utf8");
|
|
90
|
+
hasOpenFindings = /###\s+\[(FIND-\d+|P[0-3])\]/i.test(findingsContent) &&
|
|
91
|
+
!/Status:\s*`?(closed|fixed)`?/i.test(findingsContent);
|
|
92
|
+
}
|
|
93
|
+
results.push({
|
|
94
|
+
runId: entry.name,
|
|
95
|
+
title,
|
|
96
|
+
status,
|
|
97
|
+
track,
|
|
98
|
+
branch,
|
|
99
|
+
totalTasks: total,
|
|
100
|
+
completedTasks: completed,
|
|
101
|
+
remainingTasks: Math.max(0, total - completed),
|
|
102
|
+
hasOpenFindings,
|
|
103
|
+
runDir,
|
|
104
|
+
specPath
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Ignore directory scan errors
|
|
112
|
+
}
|
|
113
|
+
return results.sort((a, b) => a.runId.localeCompare(b.runId));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Fuzzy matches a user input string to an ActiveRunSummary.
|
|
117
|
+
* Matches:
|
|
118
|
+
* 1. Exact runId
|
|
119
|
+
* 2. Leading number (e.g. "1" -> "001-*", "12" -> "012-*")
|
|
120
|
+
* 3. Case-insensitive substring match in runId or title
|
|
121
|
+
*/
|
|
122
|
+
export function fuzzyMatchRunId(targetInput, activeRuns) {
|
|
123
|
+
if (!targetInput || activeRuns.length === 0)
|
|
124
|
+
return null;
|
|
125
|
+
const normalized = targetInput.trim().toLowerCase();
|
|
126
|
+
// 1. Exact match on runId
|
|
127
|
+
const exact = activeRuns.find((r) => r.runId.toLowerCase() === normalized);
|
|
128
|
+
if (exact)
|
|
129
|
+
return exact;
|
|
130
|
+
// 2. Numeric match (e.g. "12", "012", "1")
|
|
131
|
+
if (/^\d+$/.test(normalized)) {
|
|
132
|
+
const padded = normalized.padStart(3, "0");
|
|
133
|
+
const numMatch = activeRuns.find((r) => r.runId.startsWith(`${padded}-`) || r.runId.startsWith(`${normalized}-`) || r.runId === padded || r.runId === normalized);
|
|
134
|
+
if (numMatch)
|
|
135
|
+
return numMatch;
|
|
136
|
+
}
|
|
137
|
+
// 3. Substring keyword match
|
|
138
|
+
const subMatch = activeRuns.find((r) => r.runId.toLowerCase().includes(normalized) || r.title.toLowerCase().includes(normalized));
|
|
139
|
+
if (subMatch)
|
|
140
|
+
return subMatch;
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Resolves active run context with Multi-Run and fallback support.
|
|
145
|
+
*/
|
|
146
|
+
export async function resolveActiveRunContext(projectRoot, targetIdOrBranch) {
|
|
147
|
+
const globalOverviewPath = path.join(projectRoot, "devflow", "context", "project-overview.md");
|
|
148
|
+
const globalStandardsPath = path.join(projectRoot, "devflow", "context", "coding-standards.md");
|
|
25
149
|
const defaultFeaturePath = path.join(projectRoot, "devflow", "context", "current-feature.md");
|
|
26
150
|
const defaultStagePath = path.join(projectRoot, "devflow", "context", "current-stage.md");
|
|
27
151
|
const defaultFindingsPath = path.join(projectRoot, "devflow", "context", "findings.md");
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
32
|
-
|
|
152
|
+
const activeRuns = await listActiveRunContexts(projectRoot);
|
|
153
|
+
if (targetIdOrBranch) {
|
|
154
|
+
const matched = fuzzyMatchRunId(targetIdOrBranch, activeRuns);
|
|
155
|
+
if (matched) {
|
|
156
|
+
const stageFile = fsSync.existsSync(path.join(matched.runDir, "stage.md"))
|
|
157
|
+
? path.join(matched.runDir, "stage.md")
|
|
158
|
+
: defaultStagePath;
|
|
159
|
+
const findingsFile = fsSync.existsSync(path.join(matched.runDir, "findings.md"))
|
|
160
|
+
? path.join(matched.runDir, "findings.md")
|
|
161
|
+
: defaultFindingsPath;
|
|
162
|
+
return {
|
|
163
|
+
isMultiRun: true,
|
|
164
|
+
runId: matched.runId,
|
|
165
|
+
runDir: matched.runDir,
|
|
166
|
+
specPath: matched.specPath,
|
|
167
|
+
stagePath: stageFile,
|
|
168
|
+
findingsPath: findingsFile,
|
|
169
|
+
globalOverviewPath,
|
|
170
|
+
globalStandardsPath
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// Direct sanitized branch directory check
|
|
174
|
+
const sanitized = sanitizeBranchName(targetIdOrBranch);
|
|
175
|
+
const directDir = path.join(projectRoot, "devflow", "context", sanitized);
|
|
176
|
+
const directSpec = fsSync.existsSync(path.join(directDir, "spec.md"))
|
|
177
|
+
? path.join(directDir, "spec.md")
|
|
178
|
+
: path.join(directDir, "current-feature.md");
|
|
179
|
+
if (fsSync.existsSync(directSpec)) {
|
|
180
|
+
return {
|
|
181
|
+
isMultiRun: true,
|
|
182
|
+
runId: sanitized,
|
|
183
|
+
runDir: directDir,
|
|
184
|
+
specPath: directSpec,
|
|
185
|
+
stagePath: fsSync.existsSync(path.join(directDir, "stage.md"))
|
|
186
|
+
? path.join(directDir, "stage.md")
|
|
187
|
+
: defaultStagePath,
|
|
188
|
+
findingsPath: fsSync.existsSync(path.join(directDir, "findings.md"))
|
|
189
|
+
? path.join(directDir, "findings.md")
|
|
190
|
+
: defaultFindingsPath,
|
|
191
|
+
globalOverviewPath,
|
|
192
|
+
globalStandardsPath
|
|
193
|
+
};
|
|
33
194
|
}
|
|
34
195
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
196
|
+
// If no target given, check git status
|
|
197
|
+
const git = await readGitStatus(projectRoot);
|
|
198
|
+
if (git.available && git.branch) {
|
|
199
|
+
const branchName = git.branch;
|
|
200
|
+
const gitMatched = activeRuns.find((r) => r.branch === branchName || r.runId === sanitizeBranchName(branchName));
|
|
201
|
+
if (gitMatched) {
|
|
202
|
+
return {
|
|
203
|
+
isMultiRun: true,
|
|
204
|
+
runId: gitMatched.runId,
|
|
205
|
+
runDir: gitMatched.runDir,
|
|
206
|
+
specPath: gitMatched.specPath,
|
|
207
|
+
stagePath: fsSync.existsSync(path.join(gitMatched.runDir, "stage.md"))
|
|
208
|
+
? path.join(gitMatched.runDir, "stage.md")
|
|
209
|
+
: defaultStagePath,
|
|
210
|
+
findingsPath: fsSync.existsSync(path.join(gitMatched.runDir, "findings.md"))
|
|
211
|
+
? path.join(gitMatched.runDir, "findings.md")
|
|
212
|
+
: defaultFindingsPath,
|
|
213
|
+
globalOverviewPath,
|
|
214
|
+
globalStandardsPath
|
|
215
|
+
};
|
|
216
|
+
}
|
|
45
217
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const devflowBranchFeature = path.join(devflowBranchDir, "current-feature.md");
|
|
50
|
-
const devflowBranchStage = path.join(devflowBranchDir, "current-stage.md");
|
|
51
|
-
if (fsSync.existsSync(devflowBranchFeature)) {
|
|
218
|
+
// If only 1 active run exists, auto-pick it
|
|
219
|
+
if (activeRuns.length === 1) {
|
|
220
|
+
const single = activeRuns[0];
|
|
52
221
|
return {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
222
|
+
isMultiRun: true,
|
|
223
|
+
runId: single.runId,
|
|
224
|
+
runDir: single.runDir,
|
|
225
|
+
specPath: single.specPath,
|
|
226
|
+
stagePath: fsSync.existsSync(path.join(single.runDir, "stage.md"))
|
|
227
|
+
? path.join(single.runDir, "stage.md")
|
|
228
|
+
: defaultStagePath,
|
|
229
|
+
findingsPath: fsSync.existsSync(path.join(single.runDir, "findings.md"))
|
|
230
|
+
? path.join(single.runDir, "findings.md")
|
|
231
|
+
: defaultFindingsPath,
|
|
232
|
+
globalOverviewPath,
|
|
233
|
+
globalStandardsPath
|
|
60
234
|
};
|
|
61
235
|
}
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
236
|
+
// Fallback to root context
|
|
237
|
+
return {
|
|
238
|
+
isMultiRun: false,
|
|
239
|
+
runId: null,
|
|
240
|
+
runDir: null,
|
|
241
|
+
specPath: defaultFeaturePath,
|
|
242
|
+
stagePath: defaultStagePath,
|
|
243
|
+
findingsPath: defaultFindingsPath,
|
|
244
|
+
globalOverviewPath,
|
|
245
|
+
globalStandardsPath
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Initializes a new run context under devflow/context/{runId}/
|
|
250
|
+
*/
|
|
251
|
+
export async function initRunContext(projectRoot, runId, title, options = {}) {
|
|
252
|
+
const sanitized = sanitizeBranchName(runId);
|
|
253
|
+
const runDir = path.join(projectRoot, "devflow", "context", sanitized);
|
|
254
|
+
await fs.mkdir(runDir, { recursive: true });
|
|
255
|
+
const specPath = path.join(runDir, "spec.md");
|
|
256
|
+
const stagePath = path.join(runDir, "stage.md");
|
|
257
|
+
const findingsPath = path.join(runDir, "findings.md");
|
|
258
|
+
const branch = options.branch || `feature/${sanitized}`;
|
|
259
|
+
const track = options.track || "fast";
|
|
260
|
+
const status = options.status || "spec_ready";
|
|
261
|
+
if (options.initialSpec) {
|
|
262
|
+
await fs.writeFile(specPath, options.initialSpec, "utf8");
|
|
263
|
+
}
|
|
264
|
+
else if (!fsSync.existsSync(specPath)) {
|
|
265
|
+
const defaultSpec = `# 📐 [${sanitized}] ${title}\n\n- **Status**: \`${status}\`\n- **Branch**: \`${branch}\`\n\n## 3. Implementation Checklist\n- [ ] Task 1: Initialize implementation\n`;
|
|
266
|
+
await fs.writeFile(specPath, defaultSpec, "utf8");
|
|
267
|
+
}
|
|
268
|
+
if (!fsSync.existsSync(stagePath)) {
|
|
269
|
+
const stageContent = `# Current Stage\n\n- Track: \`${track}\`\n- Active Running ID: \`${sanitized}\`\n- Status: \`${status}\`\n- Branch: \`${branch}\`\n- Next Action: \`Run /implement to begin execution.\`\n`;
|
|
270
|
+
await fs.writeFile(stagePath, stageContent, "utf8");
|
|
271
|
+
}
|
|
272
|
+
if (!fsSync.existsSync(findingsPath)) {
|
|
273
|
+
const findingsContent = `# Findings\n\n> Findings ledger for \`${sanitized}\`.\n\n_No findings recorded._\n`;
|
|
274
|
+
await fs.writeFile(findingsPath, findingsContent, "utf8");
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
isMultiRun: true,
|
|
278
|
+
runId: sanitized,
|
|
279
|
+
runDir,
|
|
280
|
+
specPath,
|
|
281
|
+
stagePath,
|
|
282
|
+
findingsPath,
|
|
283
|
+
globalOverviewPath: path.join(projectRoot, "devflow", "context", "project-overview.md"),
|
|
284
|
+
globalStandardsPath: path.join(projectRoot, "devflow", "context", "coding-standards.md")
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Cleans up a run context directory after completion.
|
|
289
|
+
*/
|
|
290
|
+
export async function cleanupRunContext(projectRoot, runId) {
|
|
291
|
+
const sanitized = sanitizeBranchName(runId);
|
|
292
|
+
const runDir = path.join(projectRoot, "devflow", "context", sanitized);
|
|
293
|
+
try {
|
|
294
|
+
if (fsSync.existsSync(runDir)) {
|
|
295
|
+
await fs.rm(runDir, { recursive: true, force: true });
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Calculates the next sequential 3-digit running ID by scanning history archives and active run contexts.
|
|
306
|
+
*/
|
|
307
|
+
export async function calculateNextRunningId(projectRoot) {
|
|
308
|
+
const ids = [];
|
|
309
|
+
// 1. Scan history archives
|
|
310
|
+
const historyRoots = [
|
|
311
|
+
path.join(projectRoot, "devflow", "history", "features"),
|
|
312
|
+
path.join(projectRoot, "devflow", "history", "fixes"),
|
|
313
|
+
path.join(projectRoot, "devflow", "history", "rollbacks")
|
|
314
|
+
];
|
|
315
|
+
for (const root of historyRoots) {
|
|
316
|
+
if (fsSync.existsSync(root)) {
|
|
317
|
+
try {
|
|
318
|
+
const files = await fs.readdir(root);
|
|
319
|
+
for (const file of files) {
|
|
320
|
+
const match = file.match(/^(\d{3})/);
|
|
321
|
+
if (match) {
|
|
322
|
+
ids.push(parseInt(match[1], 10));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// ignore
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// 2. Scan active run contexts
|
|
332
|
+
const activeRuns = await listActiveRunContexts(projectRoot);
|
|
333
|
+
for (const run of activeRuns) {
|
|
334
|
+
const match = run.runId.match(/^(\d{3})/);
|
|
335
|
+
if (match) {
|
|
336
|
+
ids.push(parseInt(match[1], 10));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const maxId = ids.length > 0 ? Math.max(...ids) : 0;
|
|
340
|
+
return (maxId + 1).toString().padStart(3, "0");
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Resolves active context paths for the current repository and git branch (Legacy/Interoperable helper).
|
|
344
|
+
*/
|
|
345
|
+
export async function resolveActiveContextPaths(projectRoot, explicitBranch) {
|
|
346
|
+
const runContext = await resolveActiveRunContext(projectRoot, explicitBranch);
|
|
347
|
+
if (runContext.isMultiRun && runContext.runDir) {
|
|
67
348
|
return {
|
|
68
349
|
isBranchScoped: true,
|
|
69
|
-
branchName,
|
|
70
|
-
sanitizedBranch:
|
|
71
|
-
branchDir:
|
|
72
|
-
featureSpecPath:
|
|
73
|
-
stagePath:
|
|
74
|
-
findingsPath:
|
|
350
|
+
branchName: explicitBranch || runContext.runId,
|
|
351
|
+
sanitizedBranch: runContext.runId,
|
|
352
|
+
branchDir: runContext.runDir,
|
|
353
|
+
featureSpecPath: runContext.specPath,
|
|
354
|
+
stagePath: runContext.stagePath,
|
|
355
|
+
findingsPath: runContext.findingsPath
|
|
75
356
|
};
|
|
76
357
|
}
|
|
77
358
|
return {
|
|
78
359
|
isBranchScoped: false,
|
|
79
|
-
branchName,
|
|
80
|
-
sanitizedBranch:
|
|
360
|
+
branchName: explicitBranch || null,
|
|
361
|
+
sanitizedBranch: null,
|
|
81
362
|
branchDir: null,
|
|
82
|
-
featureSpecPath:
|
|
83
|
-
stagePath:
|
|
84
|
-
findingsPath:
|
|
363
|
+
featureSpecPath: runContext.specPath,
|
|
364
|
+
stagePath: runContext.stagePath,
|
|
365
|
+
findingsPath: runContext.findingsPath
|
|
85
366
|
};
|
|
86
367
|
}
|
|
87
368
|
/**
|
|
88
|
-
* Initializes a branch-scoped context directory
|
|
369
|
+
* Initializes a branch-scoped context directory (Backward compatibility wrapper).
|
|
89
370
|
*/
|
|
90
371
|
export async function initBranchContext(projectRoot, branchName, initialSpec, useDevflowContextDir = true) {
|
|
91
372
|
const sanitized = sanitizeBranchName(branchName);
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const featurePath = path.join(branchDir, "current-feature.md");
|
|
97
|
-
const stagePath = path.join(branchDir, "current-stage.md");
|
|
98
|
-
if (initialSpec) {
|
|
99
|
-
await fs.writeFile(featurePath, initialSpec, "utf8");
|
|
100
|
-
}
|
|
101
|
-
else if (!fsSync.existsSync(featurePath)) {
|
|
102
|
-
const defaultFeature = path.join(projectRoot, "devflow", "context", "current-feature.md");
|
|
103
|
-
try {
|
|
104
|
-
const baseline = await fs.readFile(defaultFeature, "utf8");
|
|
105
|
-
await fs.writeFile(featurePath, baseline, "utf8");
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
await fs.writeFile(featurePath, "# Current Feature\n\n_Nothing in progress. Run /feature, /fix, or /rollback to start._\n", "utf8");
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (!fsSync.existsSync(stagePath)) {
|
|
112
|
-
const defaultStage = path.join(projectRoot, "devflow", "context", "current-stage.md");
|
|
113
|
-
try {
|
|
114
|
-
const baseline = await fs.readFile(defaultStage, "utf8");
|
|
115
|
-
await fs.writeFile(stagePath, baseline, "utf8");
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
await fs.writeFile(stagePath, `# Current Stage\n\n- Track: \`fast\`\n- Branch: \`${branchName}\`\n`, "utf8");
|
|
119
|
-
}
|
|
120
|
-
}
|
|
373
|
+
const runPaths = await initRunContext(projectRoot, sanitized, branchName, {
|
|
374
|
+
branch: branchName,
|
|
375
|
+
initialSpec
|
|
376
|
+
});
|
|
121
377
|
return {
|
|
122
378
|
isBranchScoped: true,
|
|
123
379
|
branchName,
|
|
124
380
|
sanitizedBranch: sanitized,
|
|
125
|
-
branchDir,
|
|
126
|
-
featureSpecPath:
|
|
127
|
-
stagePath,
|
|
128
|
-
findingsPath:
|
|
381
|
+
branchDir: runPaths.runDir,
|
|
382
|
+
featureSpecPath: runPaths.specPath,
|
|
383
|
+
stagePath: runPaths.stagePath,
|
|
384
|
+
findingsPath: runPaths.findingsPath
|
|
129
385
|
};
|
|
130
386
|
}
|
|
131
387
|
/**
|
|
132
|
-
* Cleans up branch-scoped context directories
|
|
388
|
+
* Cleans up branch-scoped context directories (Backward compatibility wrapper).
|
|
133
389
|
*/
|
|
134
390
|
export async function cleanupBranchContext(projectRoot, branchName) {
|
|
135
391
|
const sanitized = sanitizeBranchName(branchName);
|
|
136
|
-
const
|
|
392
|
+
const cleanedRun = await cleanupRunContext(projectRoot, sanitized);
|
|
137
393
|
const nexusBranchDir = path.join(projectRoot, ".nexus", "branches", sanitized);
|
|
138
|
-
let
|
|
394
|
+
let cleanedNexus = false;
|
|
139
395
|
try {
|
|
140
|
-
if (fsSync.existsSync(devflowBranchDir)) {
|
|
141
|
-
await fs.rm(devflowBranchDir, { recursive: true, force: true });
|
|
142
|
-
cleaned = true;
|
|
143
|
-
}
|
|
144
396
|
if (fsSync.existsSync(nexusBranchDir)) {
|
|
145
397
|
await fs.rm(nexusBranchDir, { recursive: true, force: true });
|
|
146
|
-
|
|
398
|
+
cleanedNexus = true;
|
|
147
399
|
}
|
|
148
|
-
return cleaned;
|
|
149
400
|
}
|
|
150
401
|
catch {
|
|
151
|
-
|
|
402
|
+
// ignore
|
|
152
403
|
}
|
|
404
|
+
return cleanedRun || cleanedNexus;
|
|
153
405
|
}
|
|
154
406
|
/**
|
|
155
|
-
* Lists all active branch isolated contexts.
|
|
407
|
+
* Lists all active branch isolated contexts (Backward compatibility wrapper).
|
|
156
408
|
*/
|
|
157
409
|
export async function listBranchContexts(projectRoot) {
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (entry.isDirectory() && !reservedDevflowDirs.has(entry.name)) {
|
|
166
|
-
const dirPath = path.join(contextRoot, entry.name);
|
|
167
|
-
const specPath = path.join(dirPath, "current-feature.md");
|
|
168
|
-
results.push({
|
|
169
|
-
branch: entry.name,
|
|
170
|
-
path: dirPath,
|
|
171
|
-
hasSpec: fsSync.existsSync(specPath),
|
|
172
|
-
type: "devflow"
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
410
|
+
const activeRuns = await listActiveRunContexts(projectRoot);
|
|
411
|
+
const results = activeRuns.map((r) => ({
|
|
412
|
+
branch: r.runId,
|
|
413
|
+
path: r.runDir,
|
|
414
|
+
hasSpec: true,
|
|
415
|
+
type: "devflow"
|
|
416
|
+
}));
|
|
177
417
|
// Scan .nexus/branches/
|
|
178
418
|
const nexusBranchesRoot = path.join(projectRoot, ".nexus", "branches");
|
|
179
419
|
if (fsSync.existsSync(nexusBranchesRoot)) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
420
|
+
try {
|
|
421
|
+
const entries = await fs.readdir(nexusBranchesRoot, { withFileTypes: true });
|
|
422
|
+
for (const entry of entries) {
|
|
423
|
+
if (entry.isDirectory()) {
|
|
424
|
+
const dirPath = path.join(nexusBranchesRoot, entry.name);
|
|
425
|
+
const specPath = path.join(dirPath, "current-feature.md");
|
|
426
|
+
results.push({
|
|
427
|
+
branch: entry.name,
|
|
428
|
+
path: dirPath,
|
|
429
|
+
hasSpec: fsSync.existsSync(specPath),
|
|
430
|
+
type: "nexus"
|
|
431
|
+
});
|
|
432
|
+
}
|
|
191
433
|
}
|
|
192
434
|
}
|
|
435
|
+
catch {
|
|
436
|
+
// ignore
|
|
437
|
+
}
|
|
193
438
|
}
|
|
194
439
|
return results;
|
|
195
440
|
}
|