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.
- package/.claude/commands/dev.md +26 -0
- package/.claude/commands/plan.md +141 -9
- package/.claude/commands/premerge.md +0 -3
- package/.claude/commands/rollback.md +4 -4
- package/.claude/commands/ship.md +71 -41
- package/.claude/commands/status.md +9 -38
- package/.claude/commands/validate.md +47 -2
- package/.cline/workflows/dev.md +337 -0
- package/.cline/workflows/plan.md +518 -0
- package/.cline/workflows/premerge.md +173 -0
- package/.cline/workflows/research.md +39 -0
- package/.cline/workflows/review.md +439 -0
- package/.cline/workflows/rollback.md +718 -0
- package/.cline/workflows/ship.md +161 -0
- package/.cline/workflows/sonarcloud.md +146 -0
- package/.cline/workflows/status.md +45 -0
- package/.cline/workflows/validate.md +279 -0
- package/.cline/workflows/verify.md +218 -0
- package/.codex/config.toml +11 -0
- package/.codex/skills/dev/SKILL.md +340 -0
- package/.codex/skills/plan/SKILL.md +521 -0
- package/.codex/skills/premerge/SKILL.md +176 -0
- package/.codex/skills/research/SKILL.md +42 -0
- package/.codex/skills/review/SKILL.md +442 -0
- package/.codex/skills/rollback/SKILL.md +721 -0
- package/.codex/skills/ship/SKILL.md +164 -0
- package/.codex/skills/sonarcloud/SKILL.md +149 -0
- package/.codex/skills/status/SKILL.md +48 -0
- package/.codex/skills/validate/SKILL.md +282 -0
- package/.codex/skills/verify/SKILL.md +221 -0
- package/.cursor/commands/dev.md +337 -0
- package/.cursor/commands/plan.md +518 -0
- package/.cursor/commands/premerge.md +173 -0
- package/.cursor/commands/research.md +39 -0
- package/.cursor/commands/review.md +439 -0
- package/.cursor/commands/rollback.md +718 -0
- package/.cursor/commands/ship.md +161 -0
- package/.cursor/commands/sonarcloud.md +146 -0
- package/.cursor/commands/status.md +45 -0
- package/.cursor/commands/validate.md +279 -0
- package/.cursor/commands/verify.md +218 -0
- package/.cursor/hooks/state/continual-learning-index.json +19 -0
- package/.cursor/hooks/state/continual-learning.json +8 -0
- package/.cursor/rules/permissions-guidance.mdc +37 -0
- package/.github/prompts/dev.prompt.md +342 -0
- package/.github/prompts/plan.prompt.md +523 -0
- package/.github/prompts/premerge.prompt.md +178 -0
- package/.github/prompts/research.prompt.md +44 -0
- package/.github/prompts/review.prompt.md +444 -0
- package/.github/prompts/rollback.prompt.md +723 -0
- package/.github/prompts/ship.prompt.md +166 -0
- package/.github/prompts/sonarcloud.prompt.md +151 -0
- package/.github/prompts/status.prompt.md +50 -0
- package/.github/prompts/validate.prompt.md +284 -0
- package/.github/prompts/verify.prompt.md +223 -0
- package/.kilocode/workflows/dev.md +341 -0
- package/.kilocode/workflows/plan.md +522 -0
- package/.kilocode/workflows/premerge.md +177 -0
- package/.kilocode/workflows/research.md +43 -0
- package/.kilocode/workflows/review.md +443 -0
- package/.kilocode/workflows/rollback.md +722 -0
- package/.kilocode/workflows/ship.md +165 -0
- package/.kilocode/workflows/sonarcloud.md +150 -0
- package/.kilocode/workflows/status.md +49 -0
- package/.kilocode/workflows/validate.md +283 -0
- package/.kilocode/workflows/verify.md +222 -0
- package/.opencode/commands/dev.md +340 -0
- package/.opencode/commands/plan.md +521 -0
- package/.opencode/commands/premerge.md +176 -0
- package/.opencode/commands/research.md +42 -0
- package/.opencode/commands/review.md +442 -0
- package/.opencode/commands/rollback.md +721 -0
- package/.opencode/commands/ship.md +164 -0
- package/.opencode/commands/sonarcloud.md +149 -0
- package/.opencode/commands/status.md +48 -0
- package/.opencode/commands/validate.md +282 -0
- package/.opencode/commands/verify.md +221 -0
- package/.roo/commands/dev.md +341 -0
- package/.roo/commands/plan.md +522 -0
- package/.roo/commands/premerge.md +177 -0
- package/.roo/commands/research.md +43 -0
- package/.roo/commands/review.md +443 -0
- package/.roo/commands/rollback.md +722 -0
- package/.roo/commands/ship.md +165 -0
- package/.roo/commands/sonarcloud.md +150 -0
- package/.roo/commands/status.md +49 -0
- package/.roo/commands/validate.md +283 -0
- package/.roo/commands/verify.md +222 -0
- package/AGENTS.md +7 -1
- package/CLAUDE.md +5 -4
- package/README.md +21 -19
- package/bin/{forge-validate.js → forge-preflight.js} +21 -15
- package/bin/forge.js +209 -138
- package/docs/AGENT_INSTALL_PROMPT.md +1 -1
- package/docs/BEADS_GITHUB_SYNC.md +251 -0
- package/docs/ENHANCED_ONBOARDING.md +8 -8
- package/docs/EXAMPLES.md +4 -4
- package/docs/GREPTILE_SETUP.md +1 -1
- package/docs/MANUAL_REVIEW_GUIDE.md +1 -1
- package/docs/ROADMAP.md +6 -6
- package/docs/SETUP.md +1 -2
- package/docs/TOOLCHAIN.md +15 -234
- package/docs/VALIDATION.md +11 -11
- package/install.sh +33 -39
- package/lib/agents-config.js +3 -3
- package/lib/commands/plan.js +11 -15
- package/lib/commands/recommend.js +2 -2
- package/lib/dep-guard/analyzer.js +294 -0
- package/lib/dep-guard/behavior-detector.js +98 -0
- package/lib/dep-guard/contract-detector.js +162 -0
- package/lib/dep-guard/import-detector.js +498 -0
- package/lib/dep-guard/path-utils.js +13 -0
- package/lib/dep-guard/rubric.js +120 -0
- package/lib/dep-guard/task-parser.js +318 -0
- package/lib/detect-agent.js +191 -0
- package/lib/detect-worktree.js +47 -0
- package/lib/file-hash.js +26 -0
- package/lib/plugin-catalog.js +18 -28
- package/lib/setup-action-log.js +139 -0
- package/lib/setup-summary-renderer.js +106 -0
- package/lib/setup.js +75 -1
- package/lib/workflow-profiles.js +5 -11
- package/package.json +17 -7
- package/skills/parallel-deep-research/SKILL.md +108 -0
- package/skills/parallel-deep-research/evals/README.md +27 -0
- package/skills/parallel-deep-research/evals/evals.json +62 -0
- package/skills/sonarcloud-analysis/SKILL.md +171 -0
- package/skills/sonarcloud-analysis/evals/README.md +27 -0
- package/skills/sonarcloud-analysis/evals/evals.json +50 -0
- package/skills/sonarcloud-analysis/references/api-reference.md +466 -0
- package/docs/WORKFLOW.md +0 -400
- package/docs/planning/PROGRESS.md +0 -396
- package/docs/plans/.gitkeep +0 -0
- package/docs/plans/2026-02-27-forge-test-suite-v2-decisions.md +0 -21
- package/docs/plans/2026-02-27-forge-test-suite-v2-design.md +0 -362
- package/docs/plans/2026-02-27-forge-test-suite-v2-tasks.md +0 -343
- package/docs/plans/2026-03-02-superpowers-gaps-decisions.md +0 -26
- package/docs/plans/2026-03-02-superpowers-gaps-design.md +0 -239
- package/docs/plans/2026-03-02-superpowers-gaps-tasks.md +0 -260
- package/docs/plans/2026-03-04-agent-command-parity-design.md +0 -163
- package/docs/plans/2026-03-04-verify-worktree-cleanup-decisions.md +0 -7
- package/docs/plans/2026-03-04-verify-worktree-cleanup-design.md +0 -165
- package/docs/plans/2026-03-05-forge-uto-decisions.md +0 -6
- package/docs/plans/2026-03-05-forge-uto-design.md +0 -116
- package/docs/plans/2026-03-05-forge-uto-tasks.md +0 -244
- package/docs/plans/2026-03-10-command-creator-and-eval-decisions.md +0 -52
- package/docs/plans/2026-03-10-command-creator-and-eval-design.md +0 -350
- package/docs/plans/2026-03-10-command-creator-and-eval-tasks.md +0 -426
- package/docs/plans/2026-03-10-stale-workflow-refs-decisions.md +0 -8
- package/docs/plans/2026-03-10-stale-workflow-refs-design.md +0 -80
- package/docs/plans/2026-03-10-stale-workflow-refs-tasks.md +0 -90
- package/docs/plans/2026-03-14-beads-plan-context-decisions.md +0 -9
- package/docs/plans/2026-03-14-beads-plan-context-design.md +0 -171
- package/docs/plans/2026-03-14-beads-plan-context-tasks.md +0 -160
- package/docs/plans/2026-03-14-skill-eval-loop-decisions.md +0 -33
- package/docs/plans/2026-03-14-skill-eval-loop-design.md +0 -118
- package/docs/plans/2026-03-14-skill-eval-loop-results.md +0 -78
- package/docs/plans/2026-03-14-skill-eval-loop-tasks.md +0 -160
- package/docs/plans/2026-03-15-agent-command-parity-v2-decisions.md +0 -11
- package/docs/plans/2026-03-15-agent-command-parity-v2-design.md +0 -145
- package/docs/plans/2026-03-15-agent-command-parity-v2-tasks.md +0 -211
- package/docs/research/TEMPLATE.md +0 -292
- package/docs/research/advanced-testing.md +0 -297
- package/docs/research/agent-permissions.md +0 -167
- package/docs/research/dependency-chain.md +0 -328
- package/docs/research/forge-workflow-v2.md +0 -550
- package/docs/research/plugin-architecture.md +0 -772
- package/docs/research/pr4-cli-automation.md +0 -326
- package/docs/research/premerge-verify-restructure.md +0 -205
- package/docs/research/skills-restructure.md +0 -508
- package/docs/research/sonarcloud-perfection-plan.md +0 -166
- package/docs/research/sonarcloud-quality-gate.md +0 -184
- package/docs/research/superpowers-integration.md +0 -403
- package/docs/research/superpowers.md +0 -319
- 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 };
|
package/lib/file-hash.js
ADDED
|
@@ -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 };
|