forge-workflow 0.0.2 → 0.0.4

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 (175) hide show
  1. package/.claude/commands/dev.md +26 -0
  2. package/.claude/commands/plan.md +141 -9
  3. package/.claude/commands/premerge.md +0 -3
  4. package/.claude/commands/rollback.md +4 -4
  5. package/.claude/commands/ship.md +71 -41
  6. package/.claude/commands/status.md +9 -38
  7. package/.claude/commands/validate.md +47 -2
  8. package/.cline/workflows/dev.md +337 -0
  9. package/.cline/workflows/plan.md +518 -0
  10. package/.cline/workflows/premerge.md +173 -0
  11. package/.cline/workflows/research.md +39 -0
  12. package/.cline/workflows/review.md +439 -0
  13. package/.cline/workflows/rollback.md +718 -0
  14. package/.cline/workflows/ship.md +161 -0
  15. package/.cline/workflows/sonarcloud.md +146 -0
  16. package/.cline/workflows/status.md +45 -0
  17. package/.cline/workflows/validate.md +279 -0
  18. package/.cline/workflows/verify.md +218 -0
  19. package/.codex/config.toml +11 -0
  20. package/.codex/skills/dev/SKILL.md +340 -0
  21. package/.codex/skills/plan/SKILL.md +521 -0
  22. package/.codex/skills/premerge/SKILL.md +176 -0
  23. package/.codex/skills/research/SKILL.md +42 -0
  24. package/.codex/skills/review/SKILL.md +442 -0
  25. package/.codex/skills/rollback/SKILL.md +721 -0
  26. package/.codex/skills/ship/SKILL.md +164 -0
  27. package/.codex/skills/sonarcloud/SKILL.md +149 -0
  28. package/.codex/skills/status/SKILL.md +48 -0
  29. package/.codex/skills/validate/SKILL.md +282 -0
  30. package/.codex/skills/verify/SKILL.md +221 -0
  31. package/.cursor/commands/dev.md +337 -0
  32. package/.cursor/commands/plan.md +518 -0
  33. package/.cursor/commands/premerge.md +173 -0
  34. package/.cursor/commands/research.md +39 -0
  35. package/.cursor/commands/review.md +439 -0
  36. package/.cursor/commands/rollback.md +718 -0
  37. package/.cursor/commands/ship.md +161 -0
  38. package/.cursor/commands/sonarcloud.md +146 -0
  39. package/.cursor/commands/status.md +45 -0
  40. package/.cursor/commands/validate.md +279 -0
  41. package/.cursor/commands/verify.md +218 -0
  42. package/.cursor/hooks/state/continual-learning-index.json +19 -0
  43. package/.cursor/hooks/state/continual-learning.json +8 -0
  44. package/.cursor/rules/permissions-guidance.mdc +37 -0
  45. package/.github/prompts/dev.prompt.md +342 -0
  46. package/.github/prompts/plan.prompt.md +523 -0
  47. package/.github/prompts/premerge.prompt.md +178 -0
  48. package/.github/prompts/research.prompt.md +44 -0
  49. package/.github/prompts/review.prompt.md +444 -0
  50. package/.github/prompts/rollback.prompt.md +723 -0
  51. package/.github/prompts/ship.prompt.md +166 -0
  52. package/.github/prompts/sonarcloud.prompt.md +151 -0
  53. package/.github/prompts/status.prompt.md +50 -0
  54. package/.github/prompts/validate.prompt.md +284 -0
  55. package/.github/prompts/verify.prompt.md +223 -0
  56. package/.kilocode/workflows/dev.md +341 -0
  57. package/.kilocode/workflows/plan.md +522 -0
  58. package/.kilocode/workflows/premerge.md +177 -0
  59. package/.kilocode/workflows/research.md +43 -0
  60. package/.kilocode/workflows/review.md +443 -0
  61. package/.kilocode/workflows/rollback.md +722 -0
  62. package/.kilocode/workflows/ship.md +165 -0
  63. package/.kilocode/workflows/sonarcloud.md +150 -0
  64. package/.kilocode/workflows/status.md +49 -0
  65. package/.kilocode/workflows/validate.md +283 -0
  66. package/.kilocode/workflows/verify.md +222 -0
  67. package/.opencode/commands/dev.md +340 -0
  68. package/.opencode/commands/plan.md +521 -0
  69. package/.opencode/commands/premerge.md +176 -0
  70. package/.opencode/commands/research.md +42 -0
  71. package/.opencode/commands/review.md +442 -0
  72. package/.opencode/commands/rollback.md +721 -0
  73. package/.opencode/commands/ship.md +164 -0
  74. package/.opencode/commands/sonarcloud.md +149 -0
  75. package/.opencode/commands/status.md +48 -0
  76. package/.opencode/commands/validate.md +282 -0
  77. package/.opencode/commands/verify.md +221 -0
  78. package/.roo/commands/dev.md +341 -0
  79. package/.roo/commands/plan.md +522 -0
  80. package/.roo/commands/premerge.md +177 -0
  81. package/.roo/commands/research.md +43 -0
  82. package/.roo/commands/review.md +443 -0
  83. package/.roo/commands/rollback.md +722 -0
  84. package/.roo/commands/ship.md +165 -0
  85. package/.roo/commands/sonarcloud.md +150 -0
  86. package/.roo/commands/status.md +49 -0
  87. package/.roo/commands/validate.md +283 -0
  88. package/.roo/commands/verify.md +222 -0
  89. package/AGENTS.md +7 -1
  90. package/CLAUDE.md +5 -4
  91. package/README.md +21 -19
  92. package/bin/{forge-validate.js → forge-preflight.js} +21 -15
  93. package/bin/forge.js +209 -138
  94. package/docs/AGENT_INSTALL_PROMPT.md +1 -1
  95. package/docs/BEADS_GITHUB_SYNC.md +251 -0
  96. package/docs/ENHANCED_ONBOARDING.md +8 -8
  97. package/docs/EXAMPLES.md +4 -4
  98. package/docs/GREPTILE_SETUP.md +1 -1
  99. package/docs/MANUAL_REVIEW_GUIDE.md +1 -1
  100. package/docs/ROADMAP.md +6 -6
  101. package/docs/SETUP.md +1 -2
  102. package/docs/TOOLCHAIN.md +15 -234
  103. package/docs/VALIDATION.md +11 -11
  104. package/install.sh +33 -39
  105. package/lib/agents-config.js +3 -3
  106. package/lib/commands/plan.js +11 -15
  107. package/lib/commands/recommend.js +2 -2
  108. package/lib/dep-guard/analyzer.js +294 -0
  109. package/lib/dep-guard/behavior-detector.js +98 -0
  110. package/lib/dep-guard/contract-detector.js +162 -0
  111. package/lib/dep-guard/import-detector.js +498 -0
  112. package/lib/dep-guard/path-utils.js +13 -0
  113. package/lib/dep-guard/rubric.js +120 -0
  114. package/lib/dep-guard/task-parser.js +318 -0
  115. package/lib/detect-agent.js +191 -0
  116. package/lib/detect-worktree.js +47 -0
  117. package/lib/file-hash.js +26 -0
  118. package/lib/plugin-catalog.js +18 -28
  119. package/lib/setup-action-log.js +139 -0
  120. package/lib/setup-summary-renderer.js +106 -0
  121. package/lib/setup.js +75 -1
  122. package/lib/workflow-profiles.js +5 -11
  123. package/package.json +17 -7
  124. package/skills/parallel-deep-research/SKILL.md +108 -0
  125. package/skills/parallel-deep-research/evals/README.md +27 -0
  126. package/skills/parallel-deep-research/evals/evals.json +62 -0
  127. package/skills/sonarcloud-analysis/SKILL.md +171 -0
  128. package/skills/sonarcloud-analysis/evals/README.md +27 -0
  129. package/skills/sonarcloud-analysis/evals/evals.json +50 -0
  130. package/skills/sonarcloud-analysis/references/api-reference.md +466 -0
  131. package/docs/WORKFLOW.md +0 -400
  132. package/docs/planning/PROGRESS.md +0 -396
  133. package/docs/plans/.gitkeep +0 -0
  134. package/docs/plans/2026-02-27-forge-test-suite-v2-decisions.md +0 -21
  135. package/docs/plans/2026-02-27-forge-test-suite-v2-design.md +0 -362
  136. package/docs/plans/2026-02-27-forge-test-suite-v2-tasks.md +0 -343
  137. package/docs/plans/2026-03-02-superpowers-gaps-decisions.md +0 -26
  138. package/docs/plans/2026-03-02-superpowers-gaps-design.md +0 -239
  139. package/docs/plans/2026-03-02-superpowers-gaps-tasks.md +0 -260
  140. package/docs/plans/2026-03-04-agent-command-parity-design.md +0 -163
  141. package/docs/plans/2026-03-04-verify-worktree-cleanup-decisions.md +0 -7
  142. package/docs/plans/2026-03-04-verify-worktree-cleanup-design.md +0 -165
  143. package/docs/plans/2026-03-05-forge-uto-decisions.md +0 -6
  144. package/docs/plans/2026-03-05-forge-uto-design.md +0 -116
  145. package/docs/plans/2026-03-05-forge-uto-tasks.md +0 -244
  146. package/docs/plans/2026-03-10-command-creator-and-eval-decisions.md +0 -52
  147. package/docs/plans/2026-03-10-command-creator-and-eval-design.md +0 -350
  148. package/docs/plans/2026-03-10-command-creator-and-eval-tasks.md +0 -426
  149. package/docs/plans/2026-03-10-stale-workflow-refs-decisions.md +0 -8
  150. package/docs/plans/2026-03-10-stale-workflow-refs-design.md +0 -80
  151. package/docs/plans/2026-03-10-stale-workflow-refs-tasks.md +0 -90
  152. package/docs/plans/2026-03-14-beads-plan-context-decisions.md +0 -9
  153. package/docs/plans/2026-03-14-beads-plan-context-design.md +0 -171
  154. package/docs/plans/2026-03-14-beads-plan-context-tasks.md +0 -160
  155. package/docs/plans/2026-03-14-skill-eval-loop-decisions.md +0 -33
  156. package/docs/plans/2026-03-14-skill-eval-loop-design.md +0 -118
  157. package/docs/plans/2026-03-14-skill-eval-loop-results.md +0 -78
  158. package/docs/plans/2026-03-14-skill-eval-loop-tasks.md +0 -160
  159. package/docs/plans/2026-03-15-agent-command-parity-v2-decisions.md +0 -11
  160. package/docs/plans/2026-03-15-agent-command-parity-v2-design.md +0 -145
  161. package/docs/plans/2026-03-15-agent-command-parity-v2-tasks.md +0 -211
  162. package/docs/research/TEMPLATE.md +0 -292
  163. package/docs/research/advanced-testing.md +0 -297
  164. package/docs/research/agent-permissions.md +0 -167
  165. package/docs/research/dependency-chain.md +0 -328
  166. package/docs/research/forge-workflow-v2.md +0 -550
  167. package/docs/research/plugin-architecture.md +0 -772
  168. package/docs/research/pr4-cli-automation.md +0 -326
  169. package/docs/research/premerge-verify-restructure.md +0 -205
  170. package/docs/research/skills-restructure.md +0 -508
  171. package/docs/research/sonarcloud-perfection-plan.md +0 -166
  172. package/docs/research/sonarcloud-quality-gate.md +0 -184
  173. package/docs/research/superpowers-integration.md +0 -403
  174. package/docs/research/superpowers.md +0 -319
  175. package/docs/research/test-environment.md +0 -519
@@ -0,0 +1,318 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const { normalizeRepoPath } = require('./path-utils.js');
4
+
5
+ function ensureTaskFile(taskFile) {
6
+ if (!taskFile || typeof taskFile !== 'string') {
7
+ throw new Error('Task file path is required');
8
+ }
9
+
10
+ const absolutePath = path.resolve(taskFile);
11
+ if (!fs.existsSync(absolutePath)) {
12
+ throw new Error(`Task file does not exist: ${absolutePath}`);
13
+ }
14
+
15
+ return absolutePath;
16
+ }
17
+
18
+ function parseFilesLine(value) {
19
+ if (!value) {
20
+ return [];
21
+ }
22
+
23
+ return value
24
+ .split(',')
25
+ .map((file) => file.replaceAll('`', '').trim())
26
+ .filter(Boolean);
27
+ }
28
+
29
+ function flushTask(tasks, currentTask) {
30
+ if (!currentTask) {
31
+ return;
32
+ }
33
+
34
+ tasks.push({
35
+ number: currentTask.number,
36
+ title: currentTask.title,
37
+ files: currentTask.files,
38
+ whatToImplement: currentTask.whatToImplement.trim(),
39
+ expectedOutput: currentTask.expectedOutput.trim(),
40
+ });
41
+ }
42
+
43
+ function isIdentifierStart(character) {
44
+ return /[A-Za-z_]/.test(character);
45
+ }
46
+
47
+ function isIdentifierPart(character) {
48
+ return /\w/.test(character);
49
+ }
50
+
51
+ function extractSymbols(text) {
52
+ const symbols = new Set();
53
+ let searchIndex = 0;
54
+
55
+ while (searchIndex < text.length) {
56
+ const callIndex = text.indexOf('()', searchIndex);
57
+ if (callIndex === -1) {
58
+ break;
59
+ }
60
+
61
+ let startIndex = callIndex - 1;
62
+ while (startIndex >= 0 && isIdentifierPart(text[startIndex])) {
63
+ startIndex -= 1;
64
+ }
65
+
66
+ const symbol = text.slice(startIndex + 1, callIndex);
67
+ if (symbol && isIdentifierStart(symbol[0])) {
68
+ symbols.add(symbol);
69
+ }
70
+
71
+ searchIndex = callIndex + 2;
72
+ }
73
+
74
+ for (const match of text.match(/`([^`\r\n]+)`/g) ?? []) {
75
+ const token = match.slice(1, -1).trim();
76
+ if (!token || /[/.\\\s]/.test(token)) {
77
+ continue;
78
+ }
79
+
80
+ if (/^[A-Za-z_][A-Za-z0-9_-]*$/.test(token)) {
81
+ symbols.add(token);
82
+ }
83
+ }
84
+
85
+ return Array.from(symbols);
86
+ }
87
+
88
+ function detectContractType(symbol, annotation = '') {
89
+ if (annotation === 'data-format' || annotation === 'command-contract' || annotation === 'return-shape') {
90
+ return annotation;
91
+ }
92
+
93
+ if (/Schema$|Format$/i.test(symbol)) {
94
+ return 'data-format';
95
+ }
96
+
97
+ if (/Contract$/i.test(symbol)) {
98
+ return 'command-contract';
99
+ }
100
+
101
+ if (symbol.includes('-')) {
102
+ return 'command-contract';
103
+ }
104
+
105
+ if (/Shape$/i.test(symbol)) {
106
+ return 'return-shape';
107
+ }
108
+
109
+ return 'modified';
110
+ }
111
+
112
+ function extractContractMentions(text) {
113
+ const mentions = new Map();
114
+ for (const symbol of extractSymbols(text)) {
115
+ mentions.set(symbol, {
116
+ symbol,
117
+ annotation: text.includes(`${symbol}()`) ? 'modified' : detectContractType(symbol),
118
+ });
119
+ }
120
+
121
+ for (const match of text.match(/\b[A-Za-z_]\w*(?:Schema|Format|Contract|Shape)\b/g) ?? []) {
122
+ if (mentions.has(match)) {
123
+ continue;
124
+ }
125
+
126
+ mentions.set(match, {
127
+ symbol: match,
128
+ annotation: detectContractType(match),
129
+ });
130
+ }
131
+
132
+ return Array.from(mentions.values());
133
+ }
134
+
135
+ function escapeRegExp(value) {
136
+ return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
137
+ }
138
+
139
+ function resolveRepositoryFile(repositoryRoot, filePath) {
140
+ if (!repositoryRoot || !filePath || path.isAbsolute(filePath)) {
141
+ return null;
142
+ }
143
+
144
+ const absoluteRoot = path.resolve(repositoryRoot);
145
+ const absolutePath = path.resolve(absoluteRoot, filePath);
146
+ const relativePath = path.relative(absoluteRoot, absolutePath);
147
+ if (
148
+ relativePath === ''
149
+ || relativePath.startsWith(`..${path.sep}`)
150
+ || relativePath === '..'
151
+ || path.isAbsolute(relativePath)
152
+ ) {
153
+ return null;
154
+ }
155
+
156
+ return absolutePath;
157
+ }
158
+
159
+ function fileContainsSymbol(repositoryRoot, filePath, symbol) {
160
+ const absolutePath = resolveRepositoryFile(repositoryRoot, filePath);
161
+ if (!absolutePath) {
162
+ return false;
163
+ }
164
+
165
+ if (!fs.existsSync(absolutePath)) {
166
+ return false;
167
+ }
168
+
169
+ const content = fs.readFileSync(absolutePath, 'utf8');
170
+ if (/^[A-Za-z_]\w*$/.test(symbol)) {
171
+ return new RegExp(String.raw`\b${escapeRegExp(symbol)}\b`).test(content);
172
+ }
173
+
174
+ return content.includes(symbol);
175
+ }
176
+
177
+ function resolveMentionFiles(task, mention, repositoryRoot) {
178
+ const files = task.files ?? [];
179
+ if (files.length === 0) {
180
+ return [];
181
+ }
182
+
183
+ if (!repositoryRoot) {
184
+ return files.map((file) => normalizeRepoPath(file));
185
+ }
186
+
187
+ const matchingFiles = files
188
+ .filter((file) => fileContainsSymbol(repositoryRoot, file, mention.symbol))
189
+ .map((file) => normalizeRepoPath(file));
190
+ return matchingFiles;
191
+ }
192
+
193
+ function extractTaskContracts(taskContext, options = {}) {
194
+ if (!taskContext || !Array.isArray(taskContext.tasks)) {
195
+ return [];
196
+ }
197
+
198
+ const contracts = new Set();
199
+ const repositoryRoot = options.repositoryRoot;
200
+
201
+ for (const task of taskContext.tasks) {
202
+ const mentions = extractContractMentions(task.whatToImplement ?? '');
203
+ for (const mention of mentions) {
204
+ for (const file of resolveMentionFiles(task, mention, repositoryRoot)) {
205
+ contracts.add(`${file}:${mention.symbol}(${mention.annotation})`);
206
+ }
207
+ }
208
+ }
209
+
210
+ return Array.from(contracts).sort((left, right) => left.localeCompare(right));
211
+ }
212
+
213
+ function isTaskHeader(line) {
214
+ return line.startsWith('## Task ');
215
+ }
216
+
217
+ function parseTaskHeader(line) {
218
+ if (!isTaskHeader(line)) {
219
+ return null;
220
+ }
221
+
222
+ const prefixLength = '## Task '.length;
223
+ const separatorIndex = line.indexOf(':', prefixLength);
224
+ if (separatorIndex === -1) {
225
+ return null;
226
+ }
227
+
228
+ const numberText = line.slice(prefixLength, separatorIndex).trim();
229
+ if (!numberText || Array.from(numberText).some((character) => character < '0' || character > '9')) {
230
+ return null;
231
+ }
232
+
233
+ const title = line.slice(separatorIndex + 1).trim();
234
+ if (!title) {
235
+ return null;
236
+ }
237
+
238
+ return {
239
+ number: Number(numberText),
240
+ title,
241
+ };
242
+ }
243
+
244
+ function parseTaskField(currentTask, line, currentField) {
245
+ if (line.startsWith('File(s):')) {
246
+ currentTask.files = parseFilesLine(line.slice('File(s):'.length).trim());
247
+ return null;
248
+ }
249
+
250
+ if (line.startsWith('What to implement:')) {
251
+ currentTask.whatToImplement = line.slice('What to implement:'.length).trim();
252
+ return 'whatToImplement';
253
+ }
254
+
255
+ if (line.startsWith('Expected output:')) {
256
+ currentTask.expectedOutput = line.slice('Expected output:'.length).trim();
257
+ return 'expectedOutput';
258
+ }
259
+
260
+ if (line === 'TDD steps:' || line === '---') {
261
+ return null;
262
+ }
263
+
264
+ if (currentField && line.trim()) {
265
+ currentTask[currentField] = `${currentTask[currentField]} ${line.trim()}`.trim();
266
+ }
267
+
268
+ return currentField;
269
+ }
270
+
271
+ function parseTaskFile(taskFile) {
272
+ const absolutePath = ensureTaskFile(taskFile);
273
+ const content = fs.readFileSync(absolutePath, 'utf8');
274
+ const lines = content.split(/\r?\n/);
275
+ const tasks = [];
276
+ let currentTask = null;
277
+ let currentField = null;
278
+
279
+ for (const rawLine of lines) {
280
+ const line = rawLine.trimEnd();
281
+ const taskHeader = parseTaskHeader(line);
282
+
283
+ if (taskHeader) {
284
+ flushTask(tasks, currentTask);
285
+ currentTask = {
286
+ number: taskHeader.number,
287
+ title: taskHeader.title,
288
+ files: [],
289
+ whatToImplement: '',
290
+ expectedOutput: '',
291
+ };
292
+ currentField = null;
293
+ continue;
294
+ }
295
+
296
+ if (!currentTask) {
297
+ continue;
298
+ }
299
+
300
+ currentField = parseTaskField(currentTask, line, currentField);
301
+ }
302
+
303
+ flushTask(tasks, currentTask);
304
+
305
+ return {
306
+ path: absolutePath,
307
+ taskCount: tasks.length,
308
+ tasks,
309
+ };
310
+ }
311
+
312
+ module.exports = {
313
+ detectContractType,
314
+ extractContractMentions,
315
+ extractSymbols,
316
+ extractTaskContracts,
317
+ parseTaskFile,
318
+ };
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Agent Auto-Detection Module
3
+ *
4
+ * 4-layer detection strategy:
5
+ * Layer 1: AI_AGENT env var (universal standard)
6
+ * Layer 2: Agent-specific env vars (high confidence)
7
+ * Layer 3: VSCode path parsing (medium confidence)
8
+ * Layer 4: Config file signatures (medium-low confidence)
9
+ *
10
+ * @module detect-agent
11
+ */
12
+
13
+ const fs = require('node:fs');
14
+ const path = require('node:path');
15
+
16
+ /**
17
+ * Layer 2 mapping: env var → agent name.
18
+ * Order matters — first match wins within this layer.
19
+ * Each entry: [envVarName, agentName]
20
+ * @type {Array<[string, string]>}
21
+ */
22
+ const AGENT_ENV_VARS = [
23
+ // Forge-supported agents only (7 agents)
24
+ // Claude Code (with cowork sub-check handled separately)
25
+ ['CLAUDECODE', 'claude'],
26
+ ['CLAUDE_CODE', 'claude'],
27
+ // Cursor
28
+ ['CURSOR_TRACE_ID', 'cursor'],
29
+ ['CURSOR_AGENT', 'cursor'],
30
+ // Codex (OpenAI)
31
+ ['CODEX_SANDBOX', 'codex'],
32
+ ['CODEX_CI', 'codex'],
33
+ ['CODEX_THREAD_ID', 'codex'],
34
+ // OpenCode
35
+ ['OPENCODE_CLIENT', 'opencode'],
36
+ // GitHub Copilot
37
+ ['COPILOT_MODEL', 'github-copilot'],
38
+ ['COPILOT_ALLOW_ALL', 'github-copilot'],
39
+ ['COPILOT_GITHUB_TOKEN', 'github-copilot'],
40
+ // Cline, Roo Code, Kilocode — no env vars (VSCode extensions, config-file-only)
41
+ ];
42
+
43
+ /**
44
+ * Layer 4 mapping: config file/dir paths → agent name.
45
+ * Paths are relative to project root.
46
+ * @type {Array<[string, string]>}
47
+ */
48
+ const CONFIG_SIGNATURES = [
49
+ // Forge-supported agents only (7 agents)
50
+ [path.join('.claude', 'settings.json'), 'claude'],
51
+ ['.cursorrules', 'cursor'],
52
+ [path.join('.cursor', 'rules'), 'cursor'],
53
+ ['.clinerules', 'cline'],
54
+ ['.cline', 'cline'],
55
+ [path.join('.roo', 'rules'), 'roo-code'],
56
+ ['.roo', 'roo-code'],
57
+ ['.kilocode', 'kilocode'],
58
+ ['codex.md', 'codex'],
59
+ ['.codex', 'codex'],
60
+ [path.join('.opencode', 'commands'), 'opencode'],
61
+ ['.opencode', 'opencode'],
62
+ [path.join('.github', 'copilot-instructions.md'), 'github-copilot'],
63
+ ];
64
+
65
+ /**
66
+ * Detect the actively running AI agent from environment signals.
67
+ *
68
+ * Covers layers 1-3 of the 4-layer detection strategy:
69
+ * 1. AI_AGENT env var (universal, highest priority)
70
+ * 2. Agent-specific env vars (high confidence)
71
+ * 3. VSCode path parsing (medium confidence)
72
+ * Layer 4 (config file signatures) is in detectConfiguredAgents().
73
+ *
74
+ * @param {Record<string, string>} [env=process.env] - Environment variables
75
+ * @returns {{ name: string, source: 'env'|'path', confidence: 'high'|'medium' } | null}
76
+ */
77
+ function detectActiveAgent(env = process.env) {
78
+ // Layer 1: AI_AGENT universal env var
79
+ if (env.AI_AGENT) {
80
+ return { name: env.AI_AGENT, source: 'env', confidence: 'high' };
81
+ }
82
+
83
+ // Layer 2: Agent-specific env vars
84
+ // Special sub-check: Claude Code cowork mode
85
+ if ((env.CLAUDECODE || env.CLAUDE_CODE) && env.CLAUDE_CODE_IS_COWORK) {
86
+ return { name: 'cowork', source: 'env', confidence: 'high' };
87
+ }
88
+
89
+ for (const [varName, agentName] of AGENT_ENV_VARS) {
90
+ if (env[varName]) {
91
+ return { name: agentName, source: 'env', confidence: 'high' };
92
+ }
93
+ }
94
+
95
+ // Layer 3: VSCode path parsing
96
+ const pathResult = _detectFromVSCodePaths(env);
97
+ if (pathResult && pathResult.agent) {
98
+ return { name: pathResult.agent, source: 'path', confidence: 'medium' };
99
+ }
100
+
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * Parse VSCode-related env vars for editor/agent identification.
106
+ *
107
+ * @param {Record<string, string>} env - Environment variables
108
+ * @returns {{ agent: string|null, editor: string|null } | null}
109
+ * @private
110
+ */
111
+ function _detectFromVSCodePaths(env) {
112
+ const pathsToCheck = [
113
+ env.VSCODE_CODE_CACHE_PATH,
114
+ env.VSCODE_NLS_CONFIG,
115
+ ].filter(Boolean);
116
+
117
+ if (pathsToCheck.length === 0) return null;
118
+
119
+ const combined = pathsToCheck.join(' ').toLowerCase();
120
+
121
+ // Only detect Forge-supported agents via VSCode paths
122
+ if (combined.includes('cursor')) {
123
+ return { agent: 'cursor', editor: 'cursor' };
124
+ }
125
+
126
+ // Generic VSCode (or unsupported VSCode forks) — not a specific agent
127
+ if (combined.includes('code')) {
128
+ return { agent: null, editor: 'vscode' };
129
+ }
130
+
131
+ return null;
132
+ }
133
+
134
+ /**
135
+ * Detect all agents with config files present in the project root (Layer 4).
136
+ *
137
+ * @param {string} projectRoot - Absolute path to project root
138
+ * @returns {string[]} Array of unique agent names detected from config files
139
+ */
140
+ function detectConfiguredAgents(projectRoot) {
141
+ const detected = new Set();
142
+
143
+ for (const [relativePath, agentName] of CONFIG_SIGNATURES) {
144
+ const fullPath = path.join(projectRoot, relativePath);
145
+ try {
146
+ if (fs.existsSync(fullPath)) {
147
+ detected.add(agentName);
148
+ }
149
+ } catch (_err) {
150
+ // Permission error or other fs issue — skip silently
151
+ }
152
+ }
153
+
154
+ return [...detected];
155
+ }
156
+
157
+ /**
158
+ * Full environment detection combining all 4 layers.
159
+ *
160
+ * @param {string} projectRoot - Absolute path to project root
161
+ * @param {Record<string, string>} [env=process.env] - Environment variables
162
+ * @returns {{
163
+ * activeAgent: string|null,
164
+ * activeAgentSource: 'env'|'path'|null,
165
+ * confidence: 'high'|'medium'|null,
166
+ * configuredAgents: string[],
167
+ * editor: string|null
168
+ * }}
169
+ */
170
+ function detectEnvironment(projectRoot, env = process.env) {
171
+ const active = detectActiveAgent(env);
172
+ const configuredAgents = detectConfiguredAgents(projectRoot);
173
+
174
+ // Get editor from VSCode paths (_detectFromVSCodePaths is cheap and deterministic)
175
+ const vscodePaths = _detectFromVSCodePaths(env);
176
+ const editor = vscodePaths ? vscodePaths.editor : null;
177
+
178
+ return {
179
+ activeAgent: active ? active.name : null,
180
+ activeAgentSource: active ? active.source : null,
181
+ confidence: active ? active.confidence : null,
182
+ configuredAgents,
183
+ editor,
184
+ };
185
+ }
186
+
187
+ module.exports = {
188
+ detectActiveAgent,
189
+ detectConfiguredAgents,
190
+ detectEnvironment,
191
+ };
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('child_process');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Detect if the current directory is inside a git worktree.
8
+ * Uses git rev-parse --git-dir vs --git-common-dir — they differ in worktrees.
9
+ *
10
+ * @param {string} [cwd=process.cwd()] - Directory to check
11
+ * @returns {{ inWorktree: boolean, branch?: string, mainWorktree?: string }}
12
+ */
13
+ function detectWorktree(cwd = process.cwd()) {
14
+ try {
15
+ const gitDir = execFileSync('git', ['rev-parse', '--git-dir'], {
16
+ encoding: 'utf8', cwd, stdio: ['pipe', 'pipe', 'pipe']
17
+ }).trim();
18
+
19
+ const gitCommonDir = execFileSync('git', ['rev-parse', '--git-common-dir'], {
20
+ encoding: 'utf8', cwd, stdio: ['pipe', 'pipe', 'pipe']
21
+ }).trim();
22
+
23
+ // Resolve to absolute paths for reliable comparison
24
+ const absGitDir = path.resolve(cwd, gitDir);
25
+ const absCommonDir = path.resolve(cwd, gitCommonDir);
26
+
27
+ // In a worktree, git-dir is like .git/worktrees/<name>
28
+ // while git-common-dir is the main .git directory
29
+ if (absGitDir !== absCommonDir) {
30
+ const branch = execFileSync('git', ['branch', '--show-current'], {
31
+ encoding: 'utf8', cwd, stdio: ['pipe', 'pipe', 'pipe']
32
+ }).trim();
33
+
34
+ // Main worktree is one level above the common .git dir
35
+ const mainWorktree = path.resolve(absCommonDir, '..');
36
+
37
+ return { inWorktree: true, branch, mainWorktree };
38
+ }
39
+
40
+ return { inWorktree: false };
41
+ } catch (_err) {
42
+ // Not in a git repo or git not available
43
+ return { inWorktree: false };
44
+ }
45
+ }
46
+
47
+ module.exports = { detectWorktree };
@@ -0,0 +1,26 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+
4
+ /**
5
+ * Compute SHA-256 hex digest of a string.
6
+ * @param {string} content
7
+ * @returns {string} 64-char lowercase hex hash
8
+ */
9
+ function contentHash(content) {
10
+ return crypto.createHash('sha256').update(content).digest('hex');
11
+ }
12
+
13
+ /**
14
+ * Check whether an existing file's content matches the given string.
15
+ * Returns false if the file does not exist.
16
+ * @param {string} filePath
17
+ * @param {string} newContent
18
+ * @returns {boolean}
19
+ */
20
+ function fileMatchesContent(filePath, newContent) {
21
+ if (!fs.existsSync(filePath)) return false;
22
+ const existing = fs.readFileSync(filePath, 'utf8');
23
+ return contentHash(existing) === contentHash(newContent);
24
+ }
25
+
26
+ module.exports = { contentHash, fileMatchesContent };