@jakkrichm/create-nexus-devflow 2.6.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/lib/branch-context.d.ts +61 -8
  2. package/dist/lib/branch-context.js +362 -125
  3. package/dist/lib/branch-context.js.map +1 -1
  4. package/dist/lib/status.d.ts +2 -0
  5. package/dist/lib/status.js +7 -2
  6. package/dist/lib/status.js.map +1 -1
  7. package/package.json +1 -1
  8. package/template/.agents/skills/adopt/SKILL.md +1 -1
  9. package/template/.agents/skills/audit/SKILL.md +1 -1
  10. package/template/.agents/skills/autopilot/SKILL.md +1 -1
  11. package/template/.agents/skills/brainstorm/SKILL.md +1 -1
  12. package/template/.agents/skills/brief/SKILL.md +1 -1
  13. package/template/.agents/skills/check/SKILL.md +5 -3
  14. package/template/.agents/skills/ci/SKILL.md +1 -1
  15. package/template/.agents/skills/complete/SKILL.md +9 -3
  16. package/template/.agents/skills/convert-any-to-md/SKILL.md +1 -1
  17. package/template/.agents/skills/debug/SKILL.md +1 -1
  18. package/template/.agents/skills/devflow/SKILL.md +1 -1
  19. package/template/.agents/skills/discovery/SKILL.md +1 -1
  20. package/template/.agents/skills/doctor/SKILL.md +1 -1
  21. package/template/.agents/skills/feature/SKILL.md +10 -5
  22. package/template/.agents/skills/fix/SKILL.md +6 -8
  23. package/template/.agents/skills/grill/SKILL.md +1 -1
  24. package/template/.agents/skills/idea/SKILL.md +1 -1
  25. package/template/.agents/skills/implement/SKILL.md +10 -4
  26. package/template/.agents/skills/onboard/SKILL.md +1 -1
  27. package/template/.agents/skills/overview/SKILL.md +1 -1
  28. package/template/.agents/skills/prototype/SKILL.md +1 -1
  29. package/template/.agents/skills/release/SKILL.md +1 -1
  30. package/template/.agents/skills/report-html/SKILL.md +1 -1
  31. package/template/.agents/skills/rollback/SKILL.md +1 -1
  32. package/template/.agents/skills/status/SKILL.md +1 -1
  33. package/template/.agents/skills/test/SKILL.md +1 -1
  34. package/template/.agents/skills/tests/SKILL.md +1 -1
  35. package/template/.agents/skills/try/SKILL.md +1 -1
  36. package/template/.claude/skills/adopt/SKILL.md +1 -1
  37. package/template/.claude/skills/audit/SKILL.md +1 -1
  38. package/template/.claude/skills/autopilot/SKILL.md +1 -1
  39. package/template/.claude/skills/brainstorm/SKILL.md +1 -1
  40. package/template/.claude/skills/brief/SKILL.md +1 -1
  41. package/template/.claude/skills/check/SKILL.md +5 -3
  42. package/template/.claude/skills/ci/SKILL.md +1 -1
  43. package/template/.claude/skills/complete/SKILL.md +9 -3
  44. package/template/.claude/skills/convert-any-to-md/SKILL.md +1 -1
  45. package/template/.claude/skills/debug/SKILL.md +1 -1
  46. package/template/.claude/skills/devflow/SKILL.md +1 -1
  47. package/template/.claude/skills/discovery/SKILL.md +1 -1
  48. package/template/.claude/skills/doctor/SKILL.md +1 -1
  49. package/template/.claude/skills/feature/SKILL.md +10 -5
  50. package/template/.claude/skills/fix/SKILL.md +6 -8
  51. package/template/.claude/skills/grill/SKILL.md +1 -1
  52. package/template/.claude/skills/idea/SKILL.md +1 -1
  53. package/template/.claude/skills/implement/SKILL.md +10 -4
  54. package/template/.claude/skills/onboard/SKILL.md +1 -1
  55. package/template/.claude/skills/overview/SKILL.md +1 -1
  56. package/template/.claude/skills/prototype/SKILL.md +1 -1
  57. package/template/.claude/skills/release/SKILL.md +1 -1
  58. package/template/.claude/skills/report-html/SKILL.md +1 -1
  59. package/template/.claude/skills/rollback/SKILL.md +1 -1
  60. package/template/.claude/skills/status/SKILL.md +1 -1
  61. package/template/.claude/skills/test/SKILL.md +1 -1
  62. package/template/.claude/skills/tests/SKILL.md +1 -1
  63. package/template/.claude/skills/try/SKILL.md +1 -1
  64. package/template/AGENTS.md +1 -1
  65. package/template/devflow/build-plan.md +10 -0
  66. 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
- * Resolves active context paths for the current repository and git branch.
17
- * Checks for branch-isolated context in:
18
- * 1. devflow/context/<sanitized>/ (Primary user-visible multi-dev location)
19
- * 2. .nexus/branches/<sanitized>/ (Internal branch cache)
20
- * 3. devflow/context/ (Default fallback)
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 under devflow/context/<sanitized>/ (or .nexus/branches/)
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 after completion.
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,408 @@ export function sanitizeBranchName(branch) {
15
25
  .toLowerCase();
16
26
  }
17
27
  /**
18
- * Resolves active context paths for the current repository and git branch.
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
- export async function resolveActiveContextPaths(projectRoot, explicitBranch) {
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
- let branchName = explicitBranch || null;
29
- if (!branchName) {
30
- const git = await readGitStatus(projectRoot);
31
- if (git.available && git.branch) {
32
- branchName = git.branch;
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
- if (!branchName || branchName === "main" || branchName === "master" || branchName === "HEAD") {
36
- return {
37
- isBranchScoped: false,
38
- branchName,
39
- sanitizedBranch: null,
40
- branchDir: null,
41
- featureSpecPath: defaultFeaturePath,
42
- stagePath: defaultStagePath,
43
- findingsPath: defaultFindingsPath
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: path.join(gitMatched.runDir, "stage.md"),
208
+ findingsPath: path.join(gitMatched.runDir, "findings.md"),
209
+ globalOverviewPath,
210
+ globalStandardsPath
211
+ };
212
+ }
45
213
  }
46
- const sanitized = sanitizeBranchName(branchName);
47
- // Candidate 1: devflow/context/<sanitized>/
48
- const devflowBranchDir = path.join(projectRoot, "devflow", "context", sanitized);
49
- const devflowBranchFeature = path.join(devflowBranchDir, "current-feature.md");
50
- const devflowBranchStage = path.join(devflowBranchDir, "current-stage.md");
51
- if (fsSync.existsSync(devflowBranchFeature)) {
214
+ // If only 1 active run exists, auto-pick it
215
+ if (activeRuns.length === 1) {
216
+ const single = activeRuns[0];
52
217
  return {
53
- isBranchScoped: true,
54
- branchName,
55
- sanitizedBranch: sanitized,
56
- branchDir: devflowBranchDir,
57
- featureSpecPath: devflowBranchFeature,
58
- stagePath: fsSync.existsSync(devflowBranchStage) ? devflowBranchStage : defaultStagePath,
59
- findingsPath: defaultFindingsPath
218
+ isMultiRun: true,
219
+ runId: single.runId,
220
+ runDir: single.runDir,
221
+ specPath: single.specPath,
222
+ stagePath: path.join(single.runDir, "stage.md"),
223
+ findingsPath: path.join(single.runDir, "findings.md"),
224
+ globalOverviewPath,
225
+ globalStandardsPath
60
226
  };
61
227
  }
62
- // Candidate 2: .nexus/branches/<sanitized>/
63
- const nexusBranchDir = path.join(projectRoot, ".nexus", "branches", sanitized);
64
- const nexusBranchFeature = path.join(nexusBranchDir, "current-feature.md");
65
- const nexusBranchStage = path.join(nexusBranchDir, "current-stage.md");
66
- if (fsSync.existsSync(nexusBranchFeature)) {
228
+ // Fallback to root context
229
+ return {
230
+ isMultiRun: false,
231
+ runId: null,
232
+ runDir: null,
233
+ specPath: defaultFeaturePath,
234
+ stagePath: defaultStagePath,
235
+ findingsPath: defaultFindingsPath,
236
+ globalOverviewPath,
237
+ globalStandardsPath
238
+ };
239
+ }
240
+ /**
241
+ * Initializes a new run context under devflow/context/{runId}/
242
+ */
243
+ export async function initRunContext(projectRoot, runId, title, options = {}) {
244
+ const sanitized = sanitizeBranchName(runId);
245
+ const runDir = path.join(projectRoot, "devflow", "context", sanitized);
246
+ await fs.mkdir(runDir, { recursive: true });
247
+ const specPath = path.join(runDir, "spec.md");
248
+ const stagePath = path.join(runDir, "stage.md");
249
+ const findingsPath = path.join(runDir, "findings.md");
250
+ const branch = options.branch || `feature/${sanitized}`;
251
+ const track = options.track || "fast";
252
+ const status = options.status || "spec_ready";
253
+ if (options.initialSpec) {
254
+ await fs.writeFile(specPath, options.initialSpec, "utf8");
255
+ }
256
+ else if (!fsSync.existsSync(specPath)) {
257
+ const defaultSpec = `# 📐 [${sanitized}] ${title}\n\n- **Status**: \`${status}\`\n- **Branch**: \`${branch}\`\n\n## 3. Implementation Checklist\n- [ ] Task 1: Initialize implementation\n`;
258
+ await fs.writeFile(specPath, defaultSpec, "utf8");
259
+ }
260
+ if (!fsSync.existsSync(stagePath)) {
261
+ 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`;
262
+ await fs.writeFile(stagePath, stageContent, "utf8");
263
+ }
264
+ if (!fsSync.existsSync(findingsPath)) {
265
+ const findingsContent = `# Findings\n\n> Findings ledger for \`${sanitized}\`.\n\n_No findings recorded._\n`;
266
+ await fs.writeFile(findingsPath, findingsContent, "utf8");
267
+ }
268
+ return {
269
+ isMultiRun: true,
270
+ runId: sanitized,
271
+ runDir,
272
+ specPath,
273
+ stagePath,
274
+ findingsPath,
275
+ globalOverviewPath: path.join(projectRoot, "devflow", "context", "project-overview.md"),
276
+ globalStandardsPath: path.join(projectRoot, "devflow", "context", "coding-standards.md")
277
+ };
278
+ }
279
+ /**
280
+ * Cleans up a run context directory after completion.
281
+ */
282
+ export async function cleanupRunContext(projectRoot, runId) {
283
+ const sanitized = sanitizeBranchName(runId);
284
+ const runDir = path.join(projectRoot, "devflow", "context", sanitized);
285
+ try {
286
+ if (fsSync.existsSync(runDir)) {
287
+ await fs.rm(runDir, { recursive: true, force: true });
288
+ return true;
289
+ }
290
+ return false;
291
+ }
292
+ catch {
293
+ return false;
294
+ }
295
+ }
296
+ /**
297
+ * Calculates the next sequential 3-digit running ID by scanning history archives and active run contexts.
298
+ */
299
+ export async function calculateNextRunningId(projectRoot) {
300
+ const ids = [];
301
+ // 1. Scan history archives
302
+ const historyRoots = [
303
+ path.join(projectRoot, "devflow", "history", "features"),
304
+ path.join(projectRoot, "devflow", "history", "fixes"),
305
+ path.join(projectRoot, "devflow", "history", "rollbacks")
306
+ ];
307
+ for (const root of historyRoots) {
308
+ if (fsSync.existsSync(root)) {
309
+ try {
310
+ const files = await fs.readdir(root);
311
+ for (const file of files) {
312
+ const match = file.match(/^(\d{3})/);
313
+ if (match) {
314
+ ids.push(parseInt(match[1], 10));
315
+ }
316
+ }
317
+ }
318
+ catch {
319
+ // ignore
320
+ }
321
+ }
322
+ }
323
+ // 2. Scan active run contexts
324
+ const activeRuns = await listActiveRunContexts(projectRoot);
325
+ for (const run of activeRuns) {
326
+ const match = run.runId.match(/^(\d{3})/);
327
+ if (match) {
328
+ ids.push(parseInt(match[1], 10));
329
+ }
330
+ }
331
+ const maxId = ids.length > 0 ? Math.max(...ids) : 0;
332
+ return (maxId + 1).toString().padStart(3, "0");
333
+ }
334
+ /**
335
+ * Resolves active context paths for the current repository and git branch (Legacy/Interoperable helper).
336
+ */
337
+ export async function resolveActiveContextPaths(projectRoot, explicitBranch) {
338
+ const runContext = await resolveActiveRunContext(projectRoot, explicitBranch);
339
+ if (runContext.isMultiRun && runContext.runDir) {
67
340
  return {
68
341
  isBranchScoped: true,
69
- branchName,
70
- sanitizedBranch: sanitized,
71
- branchDir: nexusBranchDir,
72
- featureSpecPath: nexusBranchFeature,
73
- stagePath: fsSync.existsSync(nexusBranchStage) ? nexusBranchStage : defaultStagePath,
74
- findingsPath: defaultFindingsPath
342
+ branchName: explicitBranch || runContext.runId,
343
+ sanitizedBranch: runContext.runId,
344
+ branchDir: runContext.runDir,
345
+ featureSpecPath: runContext.specPath,
346
+ stagePath: runContext.stagePath,
347
+ findingsPath: runContext.findingsPath
75
348
  };
76
349
  }
77
350
  return {
78
351
  isBranchScoped: false,
79
- branchName,
80
- sanitizedBranch: sanitized,
352
+ branchName: explicitBranch || null,
353
+ sanitizedBranch: null,
81
354
  branchDir: null,
82
- featureSpecPath: defaultFeaturePath,
83
- stagePath: defaultStagePath,
84
- findingsPath: defaultFindingsPath
355
+ featureSpecPath: runContext.specPath,
356
+ stagePath: runContext.stagePath,
357
+ findingsPath: runContext.findingsPath
85
358
  };
86
359
  }
87
360
  /**
88
- * Initializes a branch-scoped context directory under devflow/context/<sanitized>/ (or .nexus/branches/)
361
+ * Initializes a branch-scoped context directory (Backward compatibility wrapper).
89
362
  */
90
363
  export async function initBranchContext(projectRoot, branchName, initialSpec, useDevflowContextDir = true) {
91
364
  const sanitized = sanitizeBranchName(branchName);
92
- const branchDir = useDevflowContextDir
93
- ? path.join(projectRoot, "devflow", "context", sanitized)
94
- : path.join(projectRoot, ".nexus", "branches", sanitized);
95
- await fs.mkdir(branchDir, { recursive: true });
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
- }
365
+ const runPaths = await initRunContext(projectRoot, sanitized, branchName, {
366
+ branch: branchName,
367
+ initialSpec
368
+ });
121
369
  return {
122
370
  isBranchScoped: true,
123
371
  branchName,
124
372
  sanitizedBranch: sanitized,
125
- branchDir,
126
- featureSpecPath: featurePath,
127
- stagePath,
128
- findingsPath: path.join(projectRoot, "devflow", "context", "findings.md")
373
+ branchDir: runPaths.runDir,
374
+ featureSpecPath: runPaths.specPath,
375
+ stagePath: runPaths.stagePath,
376
+ findingsPath: runPaths.findingsPath
129
377
  };
130
378
  }
131
379
  /**
132
- * Cleans up branch-scoped context directories after completion.
380
+ * Cleans up branch-scoped context directories (Backward compatibility wrapper).
133
381
  */
134
382
  export async function cleanupBranchContext(projectRoot, branchName) {
135
383
  const sanitized = sanitizeBranchName(branchName);
136
- const devflowBranchDir = path.join(projectRoot, "devflow", "context", sanitized);
384
+ const cleanedRun = await cleanupRunContext(projectRoot, sanitized);
137
385
  const nexusBranchDir = path.join(projectRoot, ".nexus", "branches", sanitized);
138
- let cleaned = false;
386
+ let cleanedNexus = false;
139
387
  try {
140
- if (fsSync.existsSync(devflowBranchDir)) {
141
- await fs.rm(devflowBranchDir, { recursive: true, force: true });
142
- cleaned = true;
143
- }
144
388
  if (fsSync.existsSync(nexusBranchDir)) {
145
389
  await fs.rm(nexusBranchDir, { recursive: true, force: true });
146
- cleaned = true;
390
+ cleanedNexus = true;
147
391
  }
148
- return cleaned;
149
392
  }
150
393
  catch {
151
- return false;
394
+ // ignore
152
395
  }
396
+ return cleanedRun || cleanedNexus;
153
397
  }
154
398
  /**
155
- * Lists all active branch isolated contexts.
399
+ * Lists all active branch isolated contexts (Backward compatibility wrapper).
156
400
  */
157
401
  export async function listBranchContexts(projectRoot) {
158
- const results = [];
159
- const reservedDevflowDirs = new Set(["current-run", "findings.md", "current-feature.md", "current-stage.md", "coding-standards.md", "project-overview.md", "ai-interaction.md"]);
160
- // Scan devflow/context/
161
- const contextRoot = path.join(projectRoot, "devflow", "context");
162
- if (fsSync.existsSync(contextRoot)) {
163
- const entries = await fs.readdir(contextRoot, { withFileTypes: true });
164
- for (const entry of entries) {
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
- }
402
+ const activeRuns = await listActiveRunContexts(projectRoot);
403
+ const results = activeRuns.map((r) => ({
404
+ branch: r.runId,
405
+ path: r.runDir,
406
+ hasSpec: true,
407
+ type: "devflow"
408
+ }));
177
409
  // Scan .nexus/branches/
178
410
  const nexusBranchesRoot = path.join(projectRoot, ".nexus", "branches");
179
411
  if (fsSync.existsSync(nexusBranchesRoot)) {
180
- const entries = await fs.readdir(nexusBranchesRoot, { withFileTypes: true });
181
- for (const entry of entries) {
182
- if (entry.isDirectory()) {
183
- const dirPath = path.join(nexusBranchesRoot, entry.name);
184
- const specPath = path.join(dirPath, "current-feature.md");
185
- results.push({
186
- branch: entry.name,
187
- path: dirPath,
188
- hasSpec: fsSync.existsSync(specPath),
189
- type: "nexus"
190
- });
412
+ try {
413
+ const entries = await fs.readdir(nexusBranchesRoot, { withFileTypes: true });
414
+ for (const entry of entries) {
415
+ if (entry.isDirectory()) {
416
+ const dirPath = path.join(nexusBranchesRoot, entry.name);
417
+ const specPath = path.join(dirPath, "current-feature.md");
418
+ results.push({
419
+ branch: entry.name,
420
+ path: dirPath,
421
+ hasSpec: fsSync.existsSync(specPath),
422
+ type: "nexus"
423
+ });
424
+ }
191
425
  }
192
426
  }
427
+ catch {
428
+ // ignore
429
+ }
193
430
  }
194
431
  return results;
195
432
  }