@mrpatronz/nexusflow 0.2.13 → 0.2.18

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 (228) hide show
  1. package/.agents/skills/ccc/SKILL.md +71 -0
  2. package/.agents/skills/ccc/references/management.md +110 -0
  3. package/.agents/skills/ccc/references/settings.md +126 -0
  4. package/.github/workflows/ci.yml +44 -0
  5. package/.github/workflows/dependabot-merge.yml +53 -0
  6. package/.github/workflows/release-desktop.yml +67 -0
  7. package/.github/workflows/release-npm.yml +65 -0
  8. package/.github/workflows/release-vscode.yml +61 -0
  9. package/GETTING_STARTED.md +5 -0
  10. package/README.md +53 -0
  11. package/desktop/build-installer.js +228 -0
  12. package/desktop/build.js +118 -0
  13. package/desktop/installer/nexusflow.iss +35 -0
  14. package/desktop/neutralino.config.json +39 -0
  15. package/desktop/package-lock.json +1060 -0
  16. package/desktop/package.json +14 -0
  17. package/dist/commands/adapter.d.ts +27 -0
  18. package/dist/commands/adapter.d.ts.map +1 -0
  19. package/dist/commands/adapter.js +318 -0
  20. package/dist/commands/adapter.js.map +1 -0
  21. package/dist/commands/add-repo.d.ts.map +1 -1
  22. package/dist/commands/add-repo.js +0 -1
  23. package/dist/commands/add-repo.js.map +1 -1
  24. package/dist/commands/commands.test.js +2 -2
  25. package/dist/commands/commands.test.js.map +1 -1
  26. package/dist/commands/config.d.ts +22 -0
  27. package/dist/commands/config.d.ts.map +1 -0
  28. package/dist/commands/config.js +50 -0
  29. package/dist/commands/config.js.map +1 -0
  30. package/dist/commands/create.d.ts.map +1 -1
  31. package/dist/commands/create.js +14 -17
  32. package/dist/commands/create.js.map +1 -1
  33. package/dist/commands/desktop.d.ts +12 -0
  34. package/dist/commands/desktop.d.ts.map +1 -0
  35. package/dist/commands/desktop.js +119 -0
  36. package/dist/commands/desktop.js.map +1 -0
  37. package/dist/commands/doctor.d.ts.map +1 -1
  38. package/dist/commands/doctor.js +11 -0
  39. package/dist/commands/doctor.js.map +1 -1
  40. package/dist/commands/refresh.d.ts +1 -0
  41. package/dist/commands/refresh.d.ts.map +1 -1
  42. package/dist/commands/refresh.js +2 -14
  43. package/dist/commands/refresh.js.map +1 -1
  44. package/dist/commands/sync.d.ts.map +1 -1
  45. package/dist/commands/sync.js +1 -8
  46. package/dist/commands/sync.js.map +1 -1
  47. package/dist/commands/tui.d.ts +8 -0
  48. package/dist/commands/tui.d.ts.map +1 -0
  49. package/dist/commands/tui.js +439 -0
  50. package/dist/commands/tui.js.map +1 -0
  51. package/dist/commands/ui.d.ts +3 -1
  52. package/dist/commands/ui.d.ts.map +1 -1
  53. package/dist/commands/ui.js +76 -19
  54. package/dist/commands/ui.js.map +1 -1
  55. package/dist/core/adapters/local-storage.d.ts +14 -0
  56. package/dist/core/adapters/local-storage.d.ts.map +1 -0
  57. package/dist/core/adapters/local-storage.js +58 -0
  58. package/dist/core/adapters/local-storage.js.map +1 -0
  59. package/dist/core/adapters/obsidian-storage.d.ts +28 -0
  60. package/dist/core/adapters/obsidian-storage.d.ts.map +1 -0
  61. package/dist/core/adapters/obsidian-storage.js +135 -0
  62. package/dist/core/adapters/obsidian-storage.js.map +1 -0
  63. package/dist/core/adapters/registry.d.ts +8 -0
  64. package/dist/core/adapters/registry.d.ts.map +1 -0
  65. package/dist/core/adapters/registry.js +37 -0
  66. package/dist/core/adapters/registry.js.map +1 -0
  67. package/dist/core/adapters/storage.test.d.ts +2 -0
  68. package/dist/core/adapters/storage.test.d.ts.map +1 -0
  69. package/dist/core/adapters/storage.test.js +106 -0
  70. package/dist/core/adapters/storage.test.js.map +1 -0
  71. package/dist/core/adapters/vault-storage.d.ts +16 -0
  72. package/dist/core/adapters/vault-storage.d.ts.map +1 -0
  73. package/dist/core/adapters/vault-storage.js +77 -0
  74. package/dist/core/adapters/vault-storage.js.map +1 -0
  75. package/dist/core/config.d.ts.map +1 -1
  76. package/dist/core/config.js +28 -4
  77. package/dist/core/config.js.map +1 -1
  78. package/dist/core/plugins/index.d.ts +14 -0
  79. package/dist/core/plugins/index.d.ts.map +1 -0
  80. package/dist/core/plugins/index.js +2 -0
  81. package/dist/core/plugins/index.js.map +1 -0
  82. package/dist/core/plugins/loader.d.ts +2 -0
  83. package/dist/core/plugins/loader.d.ts.map +1 -0
  84. package/dist/core/plugins/loader.js +34 -0
  85. package/dist/core/plugins/loader.js.map +1 -0
  86. package/dist/core/ports/storage.d.ts +59 -0
  87. package/dist/core/ports/storage.d.ts.map +1 -0
  88. package/dist/core/ports/storage.js +10 -0
  89. package/dist/core/ports/storage.js.map +1 -0
  90. package/dist/core/storage.d.ts +10 -0
  91. package/dist/core/storage.d.ts.map +1 -0
  92. package/dist/core/storage.js +29 -0
  93. package/dist/core/storage.js.map +1 -0
  94. package/dist/core/workspace.d.ts.map +1 -1
  95. package/dist/core/workspace.js +110 -16
  96. package/dist/core/workspace.js.map +1 -1
  97. package/dist/generators/base.d.ts.map +1 -1
  98. package/dist/generators/base.js +19 -6
  99. package/dist/generators/base.js.map +1 -1
  100. package/dist/generators/diff-context.d.ts +6 -0
  101. package/dist/generators/diff-context.d.ts.map +1 -0
  102. package/dist/generators/diff-context.js +68 -0
  103. package/dist/generators/diff-context.js.map +1 -0
  104. package/dist/generators/index.d.ts +1 -1
  105. package/dist/generators/index.d.ts.map +1 -1
  106. package/dist/generators/index.js +75 -18
  107. package/dist/generators/index.js.map +1 -1
  108. package/dist/generators/map-generator.d.ts.map +1 -1
  109. package/dist/generators/map-generator.js +7 -11
  110. package/dist/generators/map-generator.js.map +1 -1
  111. package/dist/generators/plan-generator.d.ts.map +1 -1
  112. package/dist/generators/plan-generator.js +3 -5
  113. package/dist/generators/plan-generator.js.map +1 -1
  114. package/dist/gui/app_logo.png +0 -0
  115. package/dist/gui/assets/index-CB-jWded.css +1 -0
  116. package/dist/gui/assets/index-DDXpZZu3.js +25 -0
  117. package/dist/gui/index.html +2 -2
  118. package/dist/index.js +174 -20
  119. package/dist/index.js.map +1 -1
  120. package/dist/server.d.ts.map +1 -1
  121. package/dist/server.js +212 -28
  122. package/dist/server.js.map +1 -1
  123. package/dist/server.test.js +173 -38
  124. package/dist/server.test.js.map +1 -1
  125. package/dist/types.d.ts +11 -2
  126. package/dist/types.d.ts.map +1 -1
  127. package/dist/utils/git.d.ts +16 -0
  128. package/dist/utils/git.d.ts.map +1 -1
  129. package/dist/utils/git.js +52 -0
  130. package/dist/utils/git.js.map +1 -1
  131. package/dist/utils/local-ai.d.ts.map +1 -1
  132. package/dist/utils/local-ai.js +7 -3
  133. package/dist/utils/local-ai.js.map +1 -1
  134. package/dist/utils/multi-git.d.ts +2 -0
  135. package/dist/utils/multi-git.d.ts.map +1 -1
  136. package/dist/utils/multi-git.js +5 -2
  137. package/dist/utils/multi-git.js.map +1 -1
  138. package/dist/utils/update-check.d.ts +3 -1
  139. package/dist/utils/update-check.d.ts.map +1 -1
  140. package/dist/utils/update-check.js +23 -34
  141. package/dist/utils/update-check.js.map +1 -1
  142. package/dist/utils/workflow-advisor.d.ts +16 -0
  143. package/dist/utils/workflow-advisor.d.ts.map +1 -0
  144. package/dist/utils/workflow-advisor.js +100 -0
  145. package/dist/utils/workflow-advisor.js.map +1 -0
  146. package/dist/utils/workflows.d.ts +11 -0
  147. package/dist/utils/workflows.d.ts.map +1 -0
  148. package/dist/utils/workflows.js +156 -0
  149. package/dist/utils/workflows.js.map +1 -0
  150. package/dist/utils/workflows.test.d.ts +2 -0
  151. package/dist/utils/workflows.test.d.ts.map +1 -0
  152. package/dist/utils/workflows.test.js +86 -0
  153. package/dist/utils/workflows.test.js.map +1 -0
  154. package/extension/media/icon.svg +1 -0
  155. package/extension/package-lock.json +4893 -1660
  156. package/extension/package.json +58 -11
  157. package/extension/src/extension.ts +476 -27
  158. package/extension/tsconfig.json +4 -2
  159. package/gui/e2e/wizard.spec.ts +19 -0
  160. package/gui/package-lock.json +342 -381
  161. package/gui/package.json +12 -12
  162. package/gui/public/app_logo.png +0 -0
  163. package/gui/src/App.tsx +991 -81
  164. package/gui/src/features/workspace/WorkspaceList.tsx +30 -30
  165. package/gui/src/index.css +64 -22
  166. package/gui/src/main.tsx +89 -5
  167. package/package.json +11 -12
  168. package/resources/workflows/plan-implement-review.md +8 -0
  169. package/resources/workflows/research-verify.md +6 -0
  170. package/resources/workflows/solo-developer.md +3 -0
  171. package/skills-lock.json +11 -0
  172. package/src/commands/adapter.ts +348 -0
  173. package/src/commands/add-repo.ts +0 -1
  174. package/src/commands/commands.test.ts +3 -2
  175. package/src/commands/config.ts +52 -0
  176. package/src/commands/create.ts +14 -17
  177. package/src/commands/desktop.ts +128 -0
  178. package/src/commands/doctor.ts +13 -0
  179. package/src/commands/mcp.ts +2 -0
  180. package/src/commands/refresh.ts +4 -14
  181. package/src/commands/sync.ts +2 -8
  182. package/src/commands/tui.ts +484 -0
  183. package/src/commands/ui.ts +83 -19
  184. package/src/core/adapters/local-storage.ts +66 -0
  185. package/src/core/adapters/obsidian-storage.ts +151 -0
  186. package/src/core/adapters/registry.ts +44 -0
  187. package/src/core/adapters/storage.test.ts +174 -0
  188. package/src/core/adapters/vault-storage.ts +86 -0
  189. package/src/core/config.ts +29 -4
  190. package/src/core/plugins/index.ts +17 -0
  191. package/src/core/plugins/loader.ts +36 -0
  192. package/src/core/ports/storage.ts +71 -0
  193. package/src/core/storage.ts +37 -0
  194. package/src/core/workspace.ts +122 -15
  195. package/src/generators/base.ts +21 -5
  196. package/src/generators/diff-context.ts +75 -0
  197. package/src/generators/index.ts +93 -21
  198. package/src/generators/map-generator.ts +7 -10
  199. package/src/generators/plan-generator.ts +6 -4
  200. package/src/index.ts +178 -21
  201. package/src/server.test.ts +193 -39
  202. package/src/server.ts +238 -28
  203. package/src/types.ts +14 -2
  204. package/src/utils/git.ts +56 -0
  205. package/src/utils/local-ai.ts +8 -3
  206. package/src/utils/multi-git.ts +16 -9
  207. package/src/utils/update-check.ts +42 -31
  208. package/src/utils/workflow-advisor.ts +118 -0
  209. package/src/utils/workflows.test.ts +114 -0
  210. package/src/utils/workflows.ts +178 -0
  211. package/.github/workflows/release.yml +0 -100
  212. package/dist/commands/pack.d.ts +0 -7
  213. package/dist/commands/pack.d.ts.map +0 -1
  214. package/dist/commands/pack.js +0 -64
  215. package/dist/commands/pack.js.map +0 -1
  216. package/dist/core/packer.d.ts +0 -14
  217. package/dist/core/packer.d.ts.map +0 -1
  218. package/dist/core/packer.js +0 -87
  219. package/dist/core/packer.js.map +0 -1
  220. package/dist/core/packer.test.d.ts +0 -2
  221. package/dist/core/packer.test.d.ts.map +0 -1
  222. package/dist/core/packer.test.js +0 -82
  223. package/dist/core/packer.test.js.map +0 -1
  224. package/dist/gui/assets/index-CFWwqFux.js +0 -23
  225. package/dist/gui/assets/index-Dvc9QMvl.css +0 -2
  226. package/src/commands/pack.ts +0 -73
  227. package/src/core/packer.test.ts +0 -99
  228. package/src/core/packer.ts +0 -107
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @module core/ports/storage
3
+ * Storage Port defining the interface for workspace document persistence.
4
+ *
5
+ * Adapters implement this interface to provide different storage backends.
6
+ * Each adapter is self-describing via the `meta` property, which enables
7
+ * automatic discovery in CLI, GUI, and plugin systems.
8
+ */
9
+
10
+ /** Describes a single configurable field for an adapter. */
11
+ export interface AdapterConfigField {
12
+ /** Machine-readable key, stored in config.json. */
13
+ key: string;
14
+ /** Human-readable label for CLI prompts and GUI forms. */
15
+ label: string;
16
+ /** Value type — 'path' renders a file picker in the GUI. */
17
+ type: 'string' | 'boolean' | 'number' | 'path';
18
+ /** Whether the field must be set before the adapter can be used. */
19
+ required?: boolean;
20
+ /** Default value when not explicitly configured. */
21
+ default?: unknown;
22
+ /** Help text shown in CLI prompts and GUI tooltips. */
23
+ description?: string;
24
+ }
25
+
26
+ /** Metadata that makes an adapter self-describing and discoverable. */
27
+ export interface StorageAdapterMeta {
28
+ /** Unique machine-readable identifier (e.g. 'obsidian'). */
29
+ name: string;
30
+ /** Human-readable name for CLI tables and GUI dropdowns (e.g. 'Obsidian Vault'). */
31
+ displayName: string;
32
+ /** One-line description of what this adapter does. */
33
+ description: string;
34
+ /** Configuration fields this adapter accepts. Empty array = no config needed. */
35
+ configFields: AdapterConfigField[];
36
+ }
37
+
38
+ export interface StoragePort {
39
+ /** Adapter metadata — who am I, what do I need? */
40
+ readonly meta: StorageAdapterMeta;
41
+
42
+ /** Called once after registration with the user's per-adapter settings. */
43
+ configure?(settings: Record<string, unknown>): void;
44
+
45
+ /** Writes a workspace context/metadata document. */
46
+ writeWorkspaceFile(workspacePath: string, featureId: string, filename: string, content: string): Promise<void>;
47
+
48
+ /** Reads a workspace context/metadata document. */
49
+ readWorkspaceFile(workspacePath: string, featureId: string, filename: string): Promise<string>;
50
+
51
+ /** Checks if a workspace context/metadata document exists. */
52
+ workspaceFileExists(workspacePath: string, featureId: string, filename: string): Promise<boolean>;
53
+
54
+ /** Resolves the human-readable path or link to the workspace document for the AI assistant. */
55
+ resolveWorkspaceFileUrl(workspacePath: string, featureId: string, filename: string): string;
56
+
57
+ /** Writes a base-layer context document (stable maps/conventions across workspaces). */
58
+ writeBaseFile(workspacePath: string, repoName: string, filename: string, content: string): Promise<void>;
59
+
60
+ /** Reads a base-layer context document. */
61
+ readBaseFile(workspacePath: string, repoName: string, filename: string): Promise<string>;
62
+
63
+ /** Checks if a base-layer context document exists. */
64
+ baseFileExists(workspacePath: string, repoName: string, filename: string): Promise<boolean>;
65
+
66
+ /** Resolves the human-readable path or link to the base document. */
67
+ resolveBaseFileUrl(workspacePath: string, repoName: string, filename: string): string;
68
+
69
+ /** Cleanly deletes all workspace-specific documents from the storage backend. */
70
+ deleteWorkspace(workspacePath: string, featureId: string): Promise<void>;
71
+ }
@@ -0,0 +1,37 @@
1
+ import { getActiveStorageProvider } from './adapters/registry.js';
2
+
3
+ export async function writeWorkspaceFile(workspacePath: string, featureId: string, filename: string, content: string): Promise<void> {
4
+ await getActiveStorageProvider().writeWorkspaceFile(workspacePath, featureId, filename, content);
5
+ }
6
+
7
+ export async function readWorkspaceFile(workspacePath: string, featureId: string, filename: string): Promise<string> {
8
+ return await getActiveStorageProvider().readWorkspaceFile(workspacePath, featureId, filename);
9
+ }
10
+
11
+ export async function workspaceFileExists(workspacePath: string, featureId: string, filename: string): Promise<boolean> {
12
+ return await getActiveStorageProvider().workspaceFileExists(workspacePath, featureId, filename);
13
+ }
14
+
15
+ export function resolveWorkspaceFileUrl(workspacePath: string, featureId: string, filename: string): string {
16
+ return getActiveStorageProvider().resolveWorkspaceFileUrl(workspacePath, featureId, filename);
17
+ }
18
+
19
+ export async function writeBaseFile(workspacePath: string, repoName: string, filename: string, content: string): Promise<void> {
20
+ await getActiveStorageProvider().writeBaseFile(workspacePath, repoName, filename, content);
21
+ }
22
+
23
+ export async function readBaseFile(workspacePath: string, repoName: string, filename: string): Promise<string> {
24
+ return await getActiveStorageProvider().readBaseFile(workspacePath, repoName, filename);
25
+ }
26
+
27
+ export async function baseFileExists(workspacePath: string, repoName: string, filename: string): Promise<boolean> {
28
+ return await getActiveStorageProvider().baseFileExists(workspacePath, repoName, filename);
29
+ }
30
+
31
+ export function resolveBaseFileUrl(workspacePath: string, repoName: string, filename: string): string {
32
+ return getActiveStorageProvider().resolveBaseFileUrl(workspacePath, repoName, filename);
33
+ }
34
+
35
+ export async function deleteWorkspaceFiles(workspacePath: string, featureId: string): Promise<void> {
36
+ await getActiveStorageProvider().deleteWorkspace(workspacePath, featureId);
37
+ }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import * as fs from 'node:fs/promises';
9
9
  import * as path from 'node:path';
10
+ import * as os from 'node:os';
10
11
  import { execa } from 'execa';
11
12
 
12
13
  import type { Feature, RepoInfo, WorkspaceContext } from '../types.js';
@@ -14,8 +15,9 @@ import { createWorktree, removeWorktree } from './worktree.js';
14
15
  import { detectDefaultBranch } from '../utils/git.js';
15
16
  import { analyzeAllRepos } from '../analyzers/index.js';
16
17
  import { generateContextFiles } from '../generators/index.js';
17
- import { packWorkspace } from './packer.js';
18
18
  import { loadConfig } from './config.js';
19
+ import { getActiveStorageProvider } from './adapters/registry.js';
20
+ import { deleteWorkspaceFiles } from './storage.js';
19
21
 
20
22
  /** Name of the per-workspace manifest file. */
21
23
  const MANIFEST_FILE = 'nexusflow.json';
@@ -66,17 +68,40 @@ export async function createWorkspace(
66
68
  console.warn('Warning: Failed to initialize git repository at workspace root:', error);
67
69
  }
68
70
 
69
- // Create .vscode/settings.json to allow VS Code search to query inside ignored sub-repos
71
+ // Generate a .code-workspace file so VS Code opens each repo as a top-level workspace
72
+ // folder with its own SCM provider. Without this, VS Code's git scanner respects the
73
+ // root .gitignore (which lists every repo dir) and never discovers the worktrees.
74
+ try {
75
+ const workspaceName = path.basename(workspacePath);
76
+ const codeWorkspace = {
77
+ folders: [
78
+ { path: '.', name: `${workspaceName} (workspace)` },
79
+ ...repos.map((repo) => ({ path: repo.name, name: repo.name })),
80
+ ],
81
+ settings: {
82
+ 'search.useIgnoreFiles': false,
83
+ },
84
+ };
85
+ await fs.writeFile(
86
+ path.join(workspacePath, `${workspaceName}.code-workspace`),
87
+ JSON.stringify(codeWorkspace, null, 2) + '\n',
88
+ 'utf-8',
89
+ );
90
+ } catch (error) {
91
+ console.warn('Warning: Failed to create .code-workspace file:', error);
92
+ }
93
+
94
+ // Create .vscode/settings.json for editors that don't use the .code-workspace file
70
95
  try {
71
96
  const vscodeDir = path.join(workspacePath, '.vscode');
72
97
  await fs.mkdir(vscodeDir, { recursive: true });
73
98
  const settings = {
74
- "search.useIgnoreFiles": false
99
+ 'search.useIgnoreFiles': false,
75
100
  };
76
101
  await fs.writeFile(
77
102
  path.join(vscodeDir, 'settings.json'),
78
103
  JSON.stringify(settings, null, 2) + '\n',
79
- 'utf-8'
104
+ 'utf-8',
80
105
  );
81
106
  } catch (error) {
82
107
  console.warn('Warning: Failed to create .vscode/settings.json:', error);
@@ -90,7 +115,7 @@ export async function createWorkspace(
90
115
  "mcpServers": {
91
116
  "nexusflow": {
92
117
  "command": "npx",
93
- "args": ["-y", "@mrpatronz/nexusflow", "mcp", "run", workspacePath]
118
+ "args": ["-y", "@mrpatronz/nexusflow", "mcp", "start"]
94
119
  }
95
120
  }
96
121
  };
@@ -117,6 +142,9 @@ export async function createWorkspace(
117
142
  // Persist the feature manifest.
118
143
  await saveFeatureConfig(workspacePath, feature);
119
144
 
145
+ // Exclude NexusFlow files from git
146
+ await excludeNexusFlowFiles(workspacePath, feature);
147
+
120
148
  return workspacePath;
121
149
  }
122
150
 
@@ -178,9 +206,9 @@ export async function saveFeatureConfig(
178
206
  workspacePath: string,
179
207
  feature: Feature,
180
208
  ): Promise<void> {
181
- const manifestPath = path.join(workspacePath, MANIFEST_FILE);
182
209
  const data = JSON.stringify(feature, null, 2) + '\n';
183
- await fs.writeFile(manifestPath, data, 'utf-8');
210
+ const adapter = getActiveStorageProvider();
211
+ await adapter.writeWorkspaceFile(workspacePath, feature.id, MANIFEST_FILE, data);
184
212
  }
185
213
 
186
214
  /**
@@ -194,16 +222,36 @@ export async function saveFeatureConfig(
194
222
  export async function loadFeatureConfig(
195
223
  workspacePath: string,
196
224
  ): Promise<Feature | null> {
197
- const rootDir = await findWorkspaceRoot(workspacePath);
198
- if (!rootDir) return null;
225
+ const featureId = path.basename(workspacePath);
226
+ const adapter = getActiveStorageProvider();
199
227
 
200
- const manifestPath = path.join(rootDir, MANIFEST_FILE);
228
+ // 1. Try reading via the storage adapter first
229
+ try {
230
+ const raw = await adapter.readWorkspaceFile(workspacePath, featureId, MANIFEST_FILE);
231
+ return JSON.parse(raw) as Feature;
232
+ } catch {}
233
+
234
+ // 2. Try direct local check
235
+ const manifestPath = path.join(workspacePath, MANIFEST_FILE);
201
236
  try {
202
237
  const raw = await fs.readFile(manifestPath, 'utf-8');
203
238
  return JSON.parse(raw) as Feature;
204
- } catch {
205
- return null;
239
+ } catch {}
240
+
241
+ // 3. Try direct vault check
242
+ const vaultManifest = path.join(os.homedir(), '.nexusflow', 'vault', featureId, MANIFEST_FILE);
243
+ try {
244
+ const raw = await fs.readFile(vaultManifest, 'utf-8');
245
+ return JSON.parse(raw) as Feature;
246
+ } catch {}
247
+
248
+ // 4. Traverse parent directories
249
+ const rootDir = await findWorkspaceRoot(workspacePath);
250
+ if (rootDir && rootDir !== workspacePath) {
251
+ return loadFeatureConfig(rootDir);
206
252
  }
253
+
254
+ return null;
207
255
  }
208
256
 
209
257
  /**
@@ -255,6 +303,12 @@ export async function deleteWorkspace(
255
303
  ): Promise<void> {
256
304
  const feature = await loadFeatureConfig(workspacePath);
257
305
  if (feature) {
306
+ try {
307
+ await deleteWorkspaceFiles(workspacePath, feature.id);
308
+ } catch (error) {
309
+ console.warn(`Warning: failed to delete workspace context files from storage:`, error);
310
+ }
311
+
258
312
  const origRepos = feature.originalRepos || [];
259
313
  for (let i = 0; i < feature.repos.length; i++) {
260
314
  const worktreePath = feature.repos[i]!;
@@ -373,8 +427,61 @@ export async function addRepoToWorkspace(
373
427
  };
374
428
 
375
429
  await generateContextFiles(ctx, feature.assistants, workspacePath);
376
- const config = await loadConfig();
377
- if (config.packContextXml) {
378
- await packWorkspace(workspacePath);
430
+ await excludeNexusFlowFiles(workspacePath, feature);
431
+ }
432
+
433
+ /**
434
+ * Automatically adds NexusFlow workspace context and config files to git's local
435
+ * exclude list for each repository so they never show up in `git status` or get committed.
436
+ */
437
+ async function excludeNexusFlowFiles(workspacePath: string, feature: Feature): Promise<void> {
438
+ const excludeEntries = [
439
+ 'CLAUDE.md',
440
+ 'AGENTS.md',
441
+ 'WORKSPACE.md',
442
+ 'nexusflow-knowledge.md',
443
+ 'nexusflow-plan.md',
444
+ 'nexusflow-conventions-*.md',
445
+ 'nexusflow-map-*.md',
446
+ 'nexusflow-diff-context.md',
447
+ '.cursor/rules/',
448
+ '.vscode/settings.json'
449
+ ];
450
+
451
+ for (const repoPath of feature.repos) {
452
+ try {
453
+ const gitDir = path.join(repoPath, '.git');
454
+ let worktreeGitDir = gitDir;
455
+ const stat = await fs.stat(gitDir);
456
+ if (stat.isFile()) {
457
+ const fileContent = await fs.readFile(gitDir, 'utf8');
458
+ const match = fileContent.match(/gitdir:\s*(.+)/);
459
+ if (match) {
460
+ worktreeGitDir = match[1].trim();
461
+ }
462
+ }
463
+
464
+ const excludeFilePath = path.join(worktreeGitDir, 'info', 'exclude');
465
+ await fs.mkdir(path.dirname(excludeFilePath), { recursive: true });
466
+
467
+ let excludeContent = '';
468
+ try {
469
+ excludeContent = await fs.readFile(excludeFilePath, 'utf8');
470
+ } catch {}
471
+
472
+ let updated = false;
473
+ for (const entry of excludeEntries) {
474
+ if (!excludeContent.includes(entry)) {
475
+ excludeContent = excludeContent.trim() + '\n' + entry + '\n';
476
+ updated = true;
477
+ }
478
+ }
479
+
480
+ if (updated) {
481
+ await fs.writeFile(excludeFilePath, excludeContent.trim() + '\n', 'utf8');
482
+ }
483
+ } catch {
484
+ // Ignore if not a git repo or directory missing
485
+ }
379
486
  }
380
487
  }
@@ -8,6 +8,7 @@ import * as path from 'node:path';
8
8
  import * as fs from 'node:fs';
9
9
  import * as os from 'node:os';
10
10
  import type { WorkspaceContext, ProjectAnalysis } from '../types.js';
11
+ import { resolveBaseFileUrl, resolveWorkspaceFileUrl } from '../core/storage.js';
11
12
 
12
13
  /**
13
14
  * Formats a ProjectAnalysis into a readable markdown section.
@@ -17,9 +18,12 @@ function formatProjectSection(analysis: ProjectAnalysis, workspacePath: string):
17
18
 
18
19
  lines.push(`### ${analysis.name}`);
19
20
 
20
- const mapPath = path.join(workspacePath, `nexusflow-map-${analysis.name}.md`).replace(/\\/g, '/');
21
+ const mapPath = resolveBaseFileUrl(workspacePath, analysis.name, `nexusflow-map-${analysis.name}.md`);
21
22
  lines.push(`- **Architecture Map**: [nexusflow-map-${analysis.name}.md](file:///${mapPath}) — **Instruction**: Before modifying this repository, read its architecture map. For exploration, consult the map's section index on demand.`);
22
23
 
24
+ const baseKnowledgePath = resolveBaseFileUrl(workspacePath, analysis.name, 'nexusflow-knowledge.md');
25
+ lines.push(`- **Base Knowledge & Decisions**: [nexusflow-knowledge.md](file:///${baseKnowledgePath}) — **Instruction**: Read this for persistent codebase learnings, decisions, and conventions.`);
26
+
23
27
  // Tech stack
24
28
  const { techStack } = analysis;
25
29
  if (techStack.languages.length > 0 && techStack.languages[0] !== 'other') {
@@ -151,19 +155,30 @@ ${parts.join('\n')}
151
155
  }
152
156
  }
153
157
 
154
- const knowledgePath = path.join(workspacePath, 'nexusflow-knowledge.md').replace(/\\/g, '/');
158
+ const knowledgePath = resolveWorkspaceFileUrl(workspacePath, feature.id, 'nexusflow-knowledge.md').replace(/\\/g, '/');
155
159
 
156
160
  let setupDone = false;
157
161
  try {
158
- const realKnowledgePath = path.join(workspacePath, 'nexusflow-knowledge.md');
159
- if (fs.existsSync(realKnowledgePath)) {
160
- const content = fs.readFileSync(realKnowledgePath, 'utf-8');
162
+ if (fs.existsSync(knowledgePath)) {
163
+ const content = fs.readFileSync(knowledgePath, 'utf-8');
161
164
  if (!content.includes('No assumptions recorded yet') && !content.includes('AI assistant to populate')) {
162
165
  setupDone = true;
163
166
  }
164
167
  }
165
168
  } catch {}
166
169
 
170
+ // Teamwork strategy section
171
+ let teamworkSection = '';
172
+ if (feature.teamworkInstructions) {
173
+ teamworkSection = `
174
+ ---
175
+
176
+ ## Team Collaboration Strategy
177
+
178
+ ${feature.teamworkInstructions}
179
+ `;
180
+ }
181
+
167
182
  const taskSection = setupDone
168
183
  ? `## Setup Status\n\n✅ **Setup Completed**: Project assumptions and initial questions have been addressed. Refer to [nexusflow-knowledge.md](file:///${knowledgePath}) for persistent session details.`
169
184
  : `## First Steps\n\nYour very first task upon entering this workspace is to explore the codebase and align with the user:\n\n1. **Verify Assumptions**: Open [nexusflow-knowledge.md](file:///${knowledgePath}) and fill in the **Project Assumptions** section with a brief description of what each project does, its tech stack, and responsibilities.\n2. **Raise Questions**: Document any outstanding uncertainties or architectural questions in the **Clarifying Questions for the User** section.\n3. **Obtain Approval**: Ask the user to confirm your assumptions and answer your questions before writing any code.`;
@@ -189,6 +204,7 @@ ${resumptionSection}
189
204
  ---
190
205
 
191
206
  ${taskSection}
207
+ ${teamworkSection}
192
208
  ---
193
209
 
194
210
  ## Guidelines
@@ -0,0 +1,75 @@
1
+ import { execa } from 'execa';
2
+ import * as path from 'node:path';
3
+ import * as fs from 'node:fs/promises';
4
+ import type { WorkspaceContext } from '../types.js';
5
+ import { getActiveStorageProvider } from '../core/adapters/registry.js';
6
+
7
+ /**
8
+ * Generates an incremental task context file listing only changed files on the branch.
9
+ */
10
+ export async function generateDiffContext(
11
+ ctx: WorkspaceContext,
12
+ workspacePath: string
13
+ ): Promise<void> {
14
+ let content = `# Incremental Task Context (Git Diff)\n\n`;
15
+ content += `This file lists the files modified or added on this feature branch compared to the base branch. Use this to focus the AI's attention.\n\n`;
16
+
17
+ let hasDiff = false;
18
+
19
+ for (const repo of ctx.repos) {
20
+ const repoPath = path.join(workspacePath, repo.name);
21
+ try {
22
+ await fs.access(repoPath);
23
+ } catch {
24
+ continue;
25
+ }
26
+
27
+ const defaultBranch = repo.defaultBranch || 'main';
28
+
29
+ let diffFiles: string[] = [];
30
+ try {
31
+ const { stdout } = await execa('git', ['diff', `origin/${defaultBranch}...HEAD`, '--name-only'], {
32
+ cwd: repoPath,
33
+ shell: process.platform === 'win32',
34
+ });
35
+ diffFiles = stdout.split('\n').map(f => f.trim()).filter(Boolean);
36
+ } catch {
37
+ try {
38
+ const { stdout } = await execa('git', ['diff', `${defaultBranch}...HEAD`, '--name-only'], {
39
+ cwd: repoPath,
40
+ shell: process.platform === 'win32',
41
+ });
42
+ diffFiles = stdout.split('\n').map(f => f.trim()).filter(Boolean);
43
+ } catch {
44
+ try {
45
+ const { stdout } = await execa('git', ['status', '--porcelain'], {
46
+ cwd: repoPath,
47
+ shell: process.platform === 'win32',
48
+ });
49
+ diffFiles = stdout.split('\n')
50
+ .map(line => line.slice(3).trim())
51
+ .filter(Boolean);
52
+ } catch {
53
+ // git command failed or not a repo
54
+ }
55
+ }
56
+ }
57
+
58
+ if (diffFiles.length > 0) {
59
+ hasDiff = true;
60
+ content += `## Repository: ${repo.name}\n\n`;
61
+ for (const file of diffFiles) {
62
+ const absoluteFilePath = path.join(repoPath, file).replace(/\\/g, '/');
63
+ content += `* [${file}](file:///${absoluteFilePath})\n`;
64
+ }
65
+ content += `\n`;
66
+ }
67
+ }
68
+
69
+ if (!hasDiff) {
70
+ content += `*No changed files detected on this branch yet.*\n`;
71
+ }
72
+
73
+ const storage = getActiveStorageProvider();
74
+ await storage.writeWorkspaceFile(workspacePath, ctx.feature.id, 'nexusflow-diff-context.md', content);
75
+ }
@@ -10,6 +10,8 @@ import { generateCursorConfig } from './cursor.js';
10
10
  import { buildContextContent } from './base.js';
11
11
  import { generateImplementationPlan } from './plan-generator.js';
12
12
  import { generateSkills } from './skills-generator.js';
13
+ import { writeWorkspaceFile, workspaceFileExists, writeBaseFile, readBaseFile, baseFileExists } from '../core/storage.js';
14
+ import { generateDiffContext } from './diff-context.js';
13
15
 
14
16
  /** Maps each assistant to its generator function and the file it produces. */
15
17
  const GENERATORS: Record<
@@ -102,6 +104,34 @@ _(No gotchas recorded yet.)_
102
104
  `;
103
105
  }
104
106
 
107
+ /**
108
+ * Builds the base codebase knowledge starter content.
109
+ */
110
+ function buildBaseKnowledgeContent(repoName: string): string {
111
+ return `# Base Codebase Knowledge — ${repoName}
112
+
113
+ > **This is a persistent cross-session memory file for ${repoName}.**
114
+ > AI assistants should read this at the start of each session, and append new architectural learnings, conventions, decisions, and gotchas at the end.
115
+ > This file persists across all features and tasks for this repository.
116
+
117
+ ## Architecture & Domain Concepts
118
+ <!-- Document major design patterns, domain layers, and system concepts here. -->
119
+ - None recorded yet.
120
+
121
+ ## Coding Conventions & Invariants
122
+ <!-- Document coding styles, library rules, and invariants that must not be broken. -->
123
+ - None recorded yet.
124
+
125
+ ## Discovered Gotchas & Watch-outs
126
+ <!-- Document weird behaviors, workarounds, or bugs to avoid repeating them. -->
127
+ - None recorded yet.
128
+
129
+ ## Architecture Decisions
130
+ <!-- Document key architectural decisions made for this repository over time. -->
131
+ - None recorded yet.
132
+ `;
133
+ }
134
+
105
135
  /**
106
136
  * Generates AI context files for each of the selected assistants.
107
137
  *
@@ -119,32 +149,70 @@ export async function generateContextFiles(
119
149
  assistants: AIAssistant[],
120
150
  workspacePath: string,
121
151
  onlyRepo?: string,
152
+ onlyBase?: boolean,
122
153
  ): Promise<void> {
123
154
  // Always generate a universal WORKSPACE.md at the workspace root
124
- try {
125
- const content = buildContextContent(ctx);
126
- const filePath = path.join(workspacePath, 'WORKSPACE.md');
127
- await fse.writeFile(filePath, content, 'utf-8');
128
- console.log(
129
- chalk.green(' ✔'),
130
- `Generated universal ${chalk.bold('WORKSPACE.md')}`,
131
- );
155
+ if (!onlyBase) {
156
+ try {
157
+ const content = buildContextContent(ctx);
158
+ await writeWorkspaceFile(workspacePath, ctx.feature.id, 'WORKSPACE.md', content);
159
+ console.log(
160
+ chalk.green(' ✔'),
161
+ `Generated universal ${chalk.bold('WORKSPACE.md')}`,
162
+ );
163
+ } catch (error) {
164
+ const message = error instanceof Error ? error.message : String(error);
165
+ console.error(
166
+ chalk.red(' ✖'),
167
+ `Failed to generate universal WORKSPACE.md: ${message}`,
168
+ );
169
+ }
170
+ }
132
171
 
133
- // Generate nexusflow-knowledge.md if it does not exist
134
- const knowledgePath = path.join(workspacePath, 'nexusflow-knowledge.md');
135
- if (!(await fse.pathExists(knowledgePath))) {
136
- const knowledgeContent = buildKnowledgeContent(ctx);
137
- await fse.writeFile(knowledgePath, knowledgeContent, 'utf-8');
138
- console.log(chalk.green(' ✔'), `Generated ${chalk.bold('nexusflow-knowledge.md')} (persistent AI memory)`);
172
+ // Generate base knowledge for each repo if it does not exist (or force recreate if onlyBase is true)
173
+ for (const repo of ctx.repos) {
174
+ if (onlyRepo && repo.name !== onlyRepo) {
175
+ continue;
176
+ }
177
+ const baseExists = await baseFileExists(workspacePath, repo.name, 'nexusflow-knowledge.md');
178
+ if (!baseExists || onlyBase) {
179
+ const baseContent = buildBaseKnowledgeContent(repo.name);
180
+ await writeBaseFile(workspacePath, repo.name, 'nexusflow-knowledge.md', baseContent);
181
+ console.log(
182
+ chalk.green(' ✔'),
183
+ `Generated base knowledge for ${chalk.bold(repo.name)}`,
184
+ );
139
185
  } else {
140
- console.log(chalk.gray(' ○'), `nexusflow-knowledge.md already exists — preserving existing content`);
186
+ console.log(
187
+ chalk.gray(' ○'),
188
+ `Base knowledge for ${repo.name} already exists — preserving`,
189
+ );
190
+ }
191
+ }
192
+
193
+ if (!onlyBase) {
194
+ try {
195
+ // Generate nexusflow-knowledge.md if it does not exist
196
+ const knowledgeExists = await workspaceFileExists(workspacePath, ctx.feature.id, 'nexusflow-knowledge.md');
197
+ if (!knowledgeExists) {
198
+ const knowledgeContent = buildKnowledgeContent(ctx);
199
+ await writeWorkspaceFile(workspacePath, ctx.feature.id, 'nexusflow-knowledge.md', knowledgeContent);
200
+ console.log(chalk.green(' ✔'), `Generated ${chalk.bold('nexusflow-knowledge.md')} (persistent AI memory)`);
201
+ } else {
202
+ console.log(chalk.gray(' ○'), `nexusflow-knowledge.md already exists — preserving existing content`);
203
+ }
204
+
205
+ // Generate Git branch diff context file
206
+ await generateDiffContext(ctx, workspacePath);
207
+ console.log(chalk.green(' ✔'), `Generated ${chalk.bold('nexusflow-diff-context.md')} (incremental git diff)`);
208
+
209
+ } catch (error) {
210
+ const message = error instanceof Error ? error.message : String(error);
211
+ console.error(
212
+ chalk.red(' ✖'),
213
+ `Failed to generate workspace context files: ${message}`,
214
+ );
141
215
  }
142
- } catch (error) {
143
- const message = error instanceof Error ? error.message : String(error);
144
- console.error(
145
- chalk.red(' ✖'),
146
- `Failed to generate universal context files: ${message}`,
147
- );
148
216
  }
149
217
 
150
218
  // Generate per-repo architecture maps
@@ -178,6 +246,10 @@ export async function generateContextFiles(
178
246
  }
179
247
  }
180
248
 
249
+ if (onlyBase) {
250
+ return;
251
+ }
252
+
181
253
  // Generate configuration for each selected assistant
182
254
  for (const assistant of assistants) {
183
255
  const entry = GENERATORS[assistant];
@@ -7,6 +7,7 @@
7
7
  import * as fs from 'node:fs/promises';
8
8
  import * as path from 'node:path';
9
9
  import { globby } from 'globby';
10
+ import { writeWorkspaceFile, readWorkspaceFile, workspaceFileExists, writeBaseFile, readBaseFile, baseFileExists } from '../core/storage.js';
10
11
  import type { ProjectAnalysis, RepoInfo } from '../types.js';
11
12
 
12
13
  interface PatternRule {
@@ -493,14 +494,11 @@ export async function generateRepoMap(
493
494
  md.push('## 📝 Discovered Conventions');
494
495
  md.push('');
495
496
 
496
- const conventionsFile = path.join(workspacePath, `nexusflow-conventions-${repoName}.md`);
497
+ const featureId = path.basename(workspacePath);
498
+ const conventionsFilename = `nexusflow-conventions-${repoName}.md`;
497
499
  let customConventions = '';
498
500
 
499
- let conventionsExist = false;
500
- try {
501
- await fs.access(conventionsFile);
502
- conventionsExist = true;
503
- } catch {}
501
+ const conventionsExist = await workspaceFileExists(workspacePath, featureId, conventionsFilename);
504
502
 
505
503
  if (!conventionsExist) {
506
504
  const starterContent = `# Repository Conventions — ${repoName}
@@ -519,12 +517,12 @@ Future AI assistant sessions will read these conventions from the repository map
519
517
  - None recorded yet.
520
518
  `;
521
519
  try {
522
- await fs.writeFile(conventionsFile, starterContent, 'utf-8');
520
+ await writeBaseFile(workspacePath, repoName, conventionsFilename, starterContent);
523
521
  customConventions = starterContent.replace(/^#\s+.+\n?/, '').trim();
524
522
  } catch {}
525
523
  } else {
526
524
  try {
527
- customConventions = await fs.readFile(conventionsFile, 'utf-8');
525
+ customConventions = await readBaseFile(workspacePath, repoName, conventionsFilename);
528
526
  customConventions = customConventions.replace(/^#\s+.+\n?/, '').trim();
529
527
  } catch {}
530
528
  }
@@ -537,6 +535,5 @@ Future AI assistant sessions will read these conventions from the repository map
537
535
  md.push('');
538
536
  }
539
537
 
540
- const outPath = path.join(workspacePath, `nexusflow-map-${repoName}.md`);
541
- await fs.writeFile(outPath, md.join('\n'), 'utf-8');
538
+ await writeBaseFile(workspacePath, repoName, `nexusflow-map-${repoName}.md`, md.join('\n'));
542
539
  }