@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.
Files changed (79) hide show
  1. package/dist/lib/branch-context.d.ts +61 -8
  2. package/dist/lib/branch-context.js +370 -125
  3. package/dist/lib/branch-context.js.map +1 -1
  4. package/dist/lib/current-work.js +15 -108
  5. package/dist/lib/current-work.js.map +1 -1
  6. package/dist/lib/discoveries.js +4 -15
  7. package/dist/lib/discoveries.js.map +1 -1
  8. package/dist/lib/history.js +1 -1
  9. package/dist/lib/history.js.map +1 -1
  10. package/dist/lib/status.d.ts +2 -0
  11. package/dist/lib/status.js +9 -16
  12. package/dist/lib/status.js.map +1 -1
  13. package/package.json +1 -1
  14. package/template/.agents/skills/adopt/SKILL.md +1 -1
  15. package/template/.agents/skills/audit/SKILL.md +1 -1
  16. package/template/.agents/skills/autopilot/SKILL.md +1 -1
  17. package/template/.agents/skills/brainstorm/SKILL.md +2 -2
  18. package/template/.agents/skills/brief/SKILL.md +1 -1
  19. package/template/.agents/skills/check/SKILL.md +5 -3
  20. package/template/.agents/skills/ci/SKILL.md +1 -1
  21. package/template/.agents/skills/complete/SKILL.md +9 -3
  22. package/template/.agents/skills/convert-any-to-md/SKILL.md +1 -1
  23. package/template/.agents/skills/debug/SKILL.md +1 -1
  24. package/template/.agents/skills/devflow/SKILL.md +1 -1
  25. package/template/.agents/skills/discovery/SKILL.md +1 -1
  26. package/template/.agents/skills/doctor/SKILL.md +1 -1
  27. package/template/.agents/skills/feature/SKILL.md +10 -5
  28. package/template/.agents/skills/fix/SKILL.md +6 -8
  29. package/template/.agents/skills/grill/SKILL.md +3 -3
  30. package/template/.agents/skills/idea/SKILL.md +3 -3
  31. package/template/.agents/skills/implement/SKILL.md +10 -4
  32. package/template/.agents/skills/onboard/SKILL.md +1 -1
  33. package/template/.agents/skills/overview/SKILL.md +1 -1
  34. package/template/.agents/skills/prototype/SKILL.md +1 -1
  35. package/template/.agents/skills/release/SKILL.md +1 -1
  36. package/template/.agents/skills/report-html/SKILL.md +13 -13
  37. package/template/.agents/skills/rollback/SKILL.md +1 -1
  38. package/template/.agents/skills/status/SKILL.md +1 -1
  39. package/template/.agents/skills/test/SKILL.md +5 -5
  40. package/template/.agents/skills/tests/SKILL.md +1 -1
  41. package/template/.agents/skills/try/SKILL.md +1 -1
  42. package/template/.claude/skills/adopt/SKILL.md +1 -1
  43. package/template/.claude/skills/audit/SKILL.md +1 -1
  44. package/template/.claude/skills/autopilot/SKILL.md +1 -1
  45. package/template/.claude/skills/brainstorm/SKILL.md +2 -2
  46. package/template/.claude/skills/brief/SKILL.md +1 -1
  47. package/template/.claude/skills/check/SKILL.md +5 -3
  48. package/template/.claude/skills/ci/SKILL.md +1 -1
  49. package/template/.claude/skills/complete/SKILL.md +9 -3
  50. package/template/.claude/skills/convert-any-to-md/SKILL.md +1 -1
  51. package/template/.claude/skills/debug/SKILL.md +1 -1
  52. package/template/.claude/skills/devflow/SKILL.md +1 -1
  53. package/template/.claude/skills/discovery/SKILL.md +1 -1
  54. package/template/.claude/skills/doctor/SKILL.md +1 -1
  55. package/template/.claude/skills/feature/SKILL.md +10 -5
  56. package/template/.claude/skills/fix/SKILL.md +6 -8
  57. package/template/.claude/skills/grill/SKILL.md +3 -3
  58. package/template/.claude/skills/idea/SKILL.md +3 -3
  59. package/template/.claude/skills/implement/SKILL.md +10 -4
  60. package/template/.claude/skills/onboard/SKILL.md +1 -1
  61. package/template/.claude/skills/overview/SKILL.md +1 -1
  62. package/template/.claude/skills/prototype/SKILL.md +1 -1
  63. package/template/.claude/skills/release/SKILL.md +1 -1
  64. package/template/.claude/skills/report-html/SKILL.md +13 -13
  65. package/template/.claude/skills/rollback/SKILL.md +1 -1
  66. package/template/.claude/skills/status/SKILL.md +1 -1
  67. package/template/.claude/skills/test/SKILL.md +5 -5
  68. package/template/.claude/skills/tests/SKILL.md +1 -1
  69. package/template/.claude/skills/try/SKILL.md +1 -1
  70. package/template/AGENTS.md +1 -1
  71. package/template/devflow/build-plan.md +13 -1
  72. package/template/devflow/context/ai-interaction.md +1 -1
  73. package/template/devflow/context/coding-standards.md +2 -2
  74. package/template/devflow/decisions/README.md +1 -1
  75. package/template/devflow/history/features/README.md +1 -1
  76. package/template/devflow/history/fixes/README.md +1 -1
  77. package/template/devflow/history/rollbacks/README.md +1 -1
  78. package/template/devflow/reference/mockup.html +16 -31
  79. 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,416 @@ 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: 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
- 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)) {
218
+ // If only 1 active run exists, auto-pick it
219
+ if (activeRuns.length === 1) {
220
+ const single = activeRuns[0];
52
221
  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
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
- // 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)) {
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: sanitized,
71
- branchDir: nexusBranchDir,
72
- featureSpecPath: nexusBranchFeature,
73
- stagePath: fsSync.existsSync(nexusBranchStage) ? nexusBranchStage : defaultStagePath,
74
- findingsPath: defaultFindingsPath
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: sanitized,
360
+ branchName: explicitBranch || null,
361
+ sanitizedBranch: null,
81
362
  branchDir: null,
82
- featureSpecPath: defaultFeaturePath,
83
- stagePath: defaultStagePath,
84
- findingsPath: defaultFindingsPath
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 under devflow/context/<sanitized>/ (or .nexus/branches/)
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 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
- }
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: featurePath,
127
- stagePath,
128
- findingsPath: path.join(projectRoot, "devflow", "context", "findings.md")
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 after completion.
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 devflowBranchDir = path.join(projectRoot, "devflow", "context", sanitized);
392
+ const cleanedRun = await cleanupRunContext(projectRoot, sanitized);
137
393
  const nexusBranchDir = path.join(projectRoot, ".nexus", "branches", sanitized);
138
- let cleaned = false;
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
- cleaned = true;
398
+ cleanedNexus = true;
147
399
  }
148
- return cleaned;
149
400
  }
150
401
  catch {
151
- return false;
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 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
- }
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
- 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
- });
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
  }