forge-workflow 0.0.6 → 0.0.8
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/.cursorrules +149 -0
- package/bin/forge.js +43 -3
- package/lib/agents/README.md +46 -1
- package/lib/agents/cline.plugin.json +11 -4
- package/lib/agents/codex.plugin.json +2 -2
- package/lib/agents/copilot.plugin.json +5 -5
- package/lib/agents/cursor.plugin.json +1 -1
- package/lib/agents/kilocode.plugin.json +1 -1
- package/lib/agents/opencode.plugin.json +7 -4
- package/lib/agents/roo.plugin.json +10 -3
- package/lib/agents-config.js +127 -79
- package/lib/codex-skills.js +50 -0
- package/lib/commands/_issue.js +172 -0
- package/lib/commands/_registry.js +40 -1
- package/lib/commands/claim.js +5 -0
- package/lib/commands/close.js +5 -0
- package/lib/commands/commands-reset.js +147 -0
- package/lib/commands/create.js +5 -0
- package/lib/commands/dev.js +26 -0
- package/lib/commands/issue.js +5 -0
- package/lib/commands/list.js +5 -0
- package/lib/commands/plan.js +18 -0
- package/lib/commands/ready.js +5 -0
- package/lib/commands/setup.js +4295 -0
- package/lib/commands/ship.js +20 -0
- package/lib/commands/show.js +5 -0
- package/lib/commands/status.js +210 -44
- package/lib/commands/sync.js +19 -1
- package/lib/commands/update.js +5 -0
- package/lib/commands/validate.js +13 -0
- package/lib/detect-agent.js +38 -8
- package/lib/detection-utils.js +405 -0
- package/lib/file-utils.js +260 -0
- package/lib/forge-context.js +42 -0
- package/lib/frontmatter.js +79 -0
- package/lib/husky-migration.js +113 -12
- package/lib/lefthook-check.js +27 -6
- package/lib/plugin-manager.js +225 -72
- package/lib/project-discovery.js +39 -5
- package/lib/runtime-health.js +305 -0
- package/lib/shell-utils.js +50 -0
- package/lib/ui-utils.js +43 -0
- package/lib/validation-utils.js +163 -0
- package/lib/workflow/enforce-stage.js +179 -0
- package/lib/workflow/stages.js +201 -0
- package/lib/workflow/state.js +332 -0
- package/opencode.json +67 -0
- package/package.json +15 -5
- package/scripts/beads-context.sh +12 -4
- package/scripts/check-agents.js +103 -0
- package/scripts/lib/eval-runner.js +50 -0
- package/scripts/pr-coordinator.sh +71 -21
- package/scripts/smart-status.sh +21 -11
- package/scripts/sync-commands.js +49 -20
- package/scripts/test.js +16 -1
package/lib/commands/ship.js
CHANGED
|
@@ -367,6 +367,26 @@ async function executeShip(options) {
|
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
module.exports = {
|
|
370
|
+
name: 'ship',
|
|
371
|
+
description: 'Create a pull request from validated feature work',
|
|
372
|
+
handler: async (args, flags = {}) => {
|
|
373
|
+
const result = await executeShip({
|
|
374
|
+
featureSlug: args[0],
|
|
375
|
+
title: args[1],
|
|
376
|
+
dryRun: Boolean(flags.dryRun || flags['--dry-run']),
|
|
377
|
+
});
|
|
378
|
+
if (!result.success) {
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const lines = [result.message];
|
|
383
|
+
if (result.prUrl) lines.push(`PR: ${result.prUrl}`);
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
...result,
|
|
387
|
+
output: lines.join('\n'),
|
|
388
|
+
};
|
|
389
|
+
},
|
|
370
390
|
extractKeyDecisions,
|
|
371
391
|
extractTestScenarios,
|
|
372
392
|
getTestCoverage,
|
package/lib/commands/status.js
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
* Detects workflow stage (1-9) with confidence scoring
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const { secureExecFileSync } = require('../shell-utils');
|
|
7
|
+
const {
|
|
8
|
+
readWorkflowState,
|
|
9
|
+
getAllowedTransitionsForWorkflowState,
|
|
10
|
+
} = require('../workflow/state');
|
|
11
|
+
const { STAGE_IDS } = require('../workflow/stages');
|
|
12
|
+
|
|
6
13
|
const WORKFLOW_STAGES = {
|
|
7
14
|
1: { name: 'Fresh Start', nextCommand: 'research' },
|
|
8
15
|
2: { name: 'Research', nextCommand: 'research' },
|
|
@@ -15,6 +22,16 @@ const WORKFLOW_STAGES = {
|
|
|
15
22
|
9: { name: 'Verification', nextCommand: 'verify' },
|
|
16
23
|
};
|
|
17
24
|
|
|
25
|
+
const AUTHORITATIVE_STAGE_NAMES = {
|
|
26
|
+
plan: 'Planning',
|
|
27
|
+
dev: 'Development',
|
|
28
|
+
validate: 'Validation',
|
|
29
|
+
ship: 'Shipping',
|
|
30
|
+
review: 'Review',
|
|
31
|
+
premerge: 'Premerge',
|
|
32
|
+
verify: 'Verification',
|
|
33
|
+
};
|
|
34
|
+
|
|
18
35
|
/**
|
|
19
36
|
* Analyze branch state
|
|
20
37
|
* @param {string} branch - Current branch name
|
|
@@ -302,63 +319,175 @@ function detectStage(context) {
|
|
|
302
319
|
};
|
|
303
320
|
}
|
|
304
321
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
322
|
+
function parseStatusInputs(args = [], flags = {}) {
|
|
323
|
+
const statusArgs = Array.isArray(args) ? args : [];
|
|
324
|
+
const getNextValue = (flagName) => {
|
|
325
|
+
const index = statusArgs.indexOf(flagName);
|
|
326
|
+
if (index !== -1 && index + 1 < statusArgs.length) {
|
|
327
|
+
return statusArgs[index + 1];
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
};
|
|
331
|
+
const getInlineValue = (flagName) => {
|
|
332
|
+
const prefix = `${flagName}=`;
|
|
333
|
+
const match = statusArgs.find(arg => typeof arg === 'string' && arg.startsWith(prefix));
|
|
334
|
+
return match ? match.slice(prefix.length) : null;
|
|
335
|
+
};
|
|
312
336
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
337
|
+
return {
|
|
338
|
+
issueId: flags.issueId || flags['--issue-id'] || getInlineValue('--issue-id') || getNextValue('--issue-id'),
|
|
339
|
+
workflowState: flags.workflowState || flags['--workflow-state'] || getInlineValue('--workflow-state') || getNextValue('--workflow-state'),
|
|
340
|
+
bdComments: flags.bdComments || flags['--bd-comments'] || getInlineValue('--bd-comments') || getNextValue('--bd-comments'),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
318
343
|
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
if (
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
if (result.factors.files.hasPlan) {
|
|
325
|
-
completed.push('✓ Plan created');
|
|
326
|
-
}
|
|
327
|
-
if (result.factors.branch.onFeatureBranch) {
|
|
328
|
-
completed.push('✓ Feature branch created');
|
|
344
|
+
function extractWorkflowStateFromComments(comments = '') {
|
|
345
|
+
const matches = String(comments).match(/^WorkflowState:\s*(\{.*\})$/gm);
|
|
346
|
+
if (!matches || matches.length === 0) {
|
|
347
|
+
return null;
|
|
329
348
|
}
|
|
330
|
-
|
|
331
|
-
|
|
349
|
+
|
|
350
|
+
const latest = matches.at(-1).replace(/^WorkflowState:\s*/, '');
|
|
351
|
+
return readWorkflowState(latest);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function readWorkflowStateFromBeads(issueId, options = {}) {
|
|
355
|
+
if (!issueId) {
|
|
356
|
+
return null;
|
|
332
357
|
}
|
|
333
|
-
|
|
334
|
-
|
|
358
|
+
|
|
359
|
+
const comments = options.comments || secureExecFileSync('bd', ['comments', 'list', issueId], {
|
|
360
|
+
encoding: 'utf8',
|
|
361
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
362
|
+
}).trim();
|
|
363
|
+
|
|
364
|
+
if (!comments) {
|
|
365
|
+
return null;
|
|
335
366
|
}
|
|
336
|
-
|
|
337
|
-
|
|
367
|
+
|
|
368
|
+
return extractWorkflowStateFromComments(comments);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function resolveWorkflowState(inputs) {
|
|
372
|
+
try {
|
|
373
|
+
const workflowState = inputs.workflowState
|
|
374
|
+
? readWorkflowState(inputs.workflowState)
|
|
375
|
+
: readWorkflowStateFromBeads(inputs.issueId, { comments: inputs.bdComments });
|
|
376
|
+
return { workflowState, fallbackReason: null };
|
|
377
|
+
} catch (error) {
|
|
378
|
+
return {
|
|
379
|
+
workflowState: null,
|
|
380
|
+
fallbackReason: error instanceof Error ? error.message : String(error),
|
|
381
|
+
};
|
|
338
382
|
}
|
|
339
|
-
|
|
340
|
-
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function buildAuthoritativeStatus(workflowState) {
|
|
386
|
+
const currentStage = workflowState.currentStage;
|
|
387
|
+
const stageIndex = STAGE_IDS.indexOf(currentStage);
|
|
388
|
+
const nextStages = getAllowedTransitionsForWorkflowState(workflowState);
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
stage: stageIndex === -1 ? null : stageIndex + 1,
|
|
392
|
+
stageId: currentStage,
|
|
393
|
+
stageName: AUTHORITATIVE_STAGE_NAMES[currentStage] || currentStage,
|
|
394
|
+
confidence: 'high',
|
|
395
|
+
confidenceScore: 100,
|
|
396
|
+
runCommand: currentStage,
|
|
397
|
+
nextCommand: nextStages[0] || null,
|
|
398
|
+
nextStages,
|
|
399
|
+
authoritative: true,
|
|
400
|
+
workflowState,
|
|
401
|
+
factors: {
|
|
402
|
+
files: {
|
|
403
|
+
hasResearch: workflowState.completedStages.includes('plan'),
|
|
404
|
+
hasPlan: workflowState.completedStages.includes('plan') || currentStage !== 'plan',
|
|
405
|
+
testsPass: workflowState.completedStages.includes('validate'),
|
|
406
|
+
},
|
|
407
|
+
branch: {},
|
|
408
|
+
pr: {},
|
|
409
|
+
checks: { allChecksPass: workflowState.completedStages.includes('validate') },
|
|
410
|
+
beads: { hasActiveIssue: true },
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function buildMissingWorkflowStateStatus() {
|
|
416
|
+
return {
|
|
417
|
+
stage: null,
|
|
418
|
+
stageId: null,
|
|
419
|
+
stageName: 'Unknown',
|
|
420
|
+
confidence: 'low',
|
|
421
|
+
confidenceScore: 0,
|
|
422
|
+
authoritative: false,
|
|
423
|
+
missingWorkflowState: true,
|
|
424
|
+
output: '\nNo authoritative workflow state available.\nProvide --workflow-state or --issue-id so /status can read recorded stage state.\n',
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function buildHeaderLines(title, detailLines = []) {
|
|
429
|
+
return ['', title, ...detailLines, ''];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function buildSectionLines(title, items = []) {
|
|
433
|
+
if (items.length === 0) {
|
|
434
|
+
return [];
|
|
341
435
|
}
|
|
342
|
-
|
|
343
|
-
|
|
436
|
+
|
|
437
|
+
return [title, ...items.map(item => ` ${item}`), ''];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function collectCompletedChecks(result) {
|
|
441
|
+
const completed = [];
|
|
442
|
+
|
|
443
|
+
if (result.factors.files.hasResearch) completed.push('Research doc exists');
|
|
444
|
+
if (result.factors.files.hasPlan) completed.push('Plan created');
|
|
445
|
+
if (result.factors.branch.onFeatureBranch) completed.push('Feature branch created');
|
|
446
|
+
if (result.factors.files.hasTests) completed.push('Tests written');
|
|
447
|
+
if (result.factors.files.testsPass) completed.push('Tests passing');
|
|
448
|
+
if (result.factors.checks.allChecksPass) completed.push('All checks passing');
|
|
449
|
+
if (result.factors.pr.hasPR) completed.push(`PR created (#${result.factors.pr.prNumber})`);
|
|
450
|
+
if (result.factors.pr.prApproved) completed.push('PR approved');
|
|
451
|
+
if (result.factors.pr.prMerged) completed.push('PR merged');
|
|
452
|
+
|
|
453
|
+
return completed;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function formatAuthoritativeStatus(result) {
|
|
457
|
+
const completedStages = result.workflowState.completedStages.map(stageId => stageId);
|
|
458
|
+
const allowedCommands = result.nextStages.map(stageId => '/' + stageId).join(', ');
|
|
459
|
+
const nextLines = [`Run now: /${result.runCommand}`];
|
|
460
|
+
|
|
461
|
+
if (result.nextCommand) {
|
|
462
|
+
nextLines.push(`Next after this: /${result.nextCommand}`);
|
|
344
463
|
}
|
|
345
|
-
if (
|
|
346
|
-
|
|
464
|
+
if (allowedCommands) {
|
|
465
|
+
nextLines.push(`Allowed after this: ${allowedCommands}`);
|
|
347
466
|
}
|
|
348
467
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
468
|
+
return [
|
|
469
|
+
...buildHeaderLines(`Current Stage: ${result.stageId} - ${result.stageName}`, [
|
|
470
|
+
' Source: authoritative workflow state',
|
|
471
|
+
` Classification: ${result.workflowState.workflowDecisions.classification}`,
|
|
472
|
+
]),
|
|
473
|
+
...buildSectionLines('Completed stages:', completedStages),
|
|
474
|
+
...nextLines,
|
|
475
|
+
'',
|
|
476
|
+
].join('\n');
|
|
477
|
+
}
|
|
354
478
|
|
|
355
|
-
|
|
356
|
-
lines
|
|
357
|
-
|
|
479
|
+
function formatHeuristicStatus(result) {
|
|
480
|
+
const lines = [
|
|
481
|
+
...buildHeaderLines(`Current Stage: ${result.stage} - ${result.stageName}`, [
|
|
482
|
+
` Confidence: ${result.confidence.toUpperCase()} (${result.confidenceScore}%)`,
|
|
483
|
+
]),
|
|
484
|
+
...buildSectionLines('Completed:', collectCompletedChecks(result)),
|
|
485
|
+
`Next: /${result.nextCommand}`,
|
|
486
|
+
'',
|
|
487
|
+
];
|
|
358
488
|
|
|
359
|
-
// Low confidence warning
|
|
360
489
|
if (result.confidence === 'low') {
|
|
361
|
-
lines.push('
|
|
490
|
+
lines.push('Low confidence - Manual verification suggested');
|
|
362
491
|
lines.push(' Conflicting signals detected. Please verify current stage.');
|
|
363
492
|
lines.push('');
|
|
364
493
|
}
|
|
@@ -366,7 +495,44 @@ function formatStatus(result) {
|
|
|
366
495
|
return lines.join('\n');
|
|
367
496
|
}
|
|
368
497
|
|
|
498
|
+
/**
|
|
499
|
+
* Format status output
|
|
500
|
+
* @param {object} result - Detection result
|
|
501
|
+
* @returns {string} Formatted output
|
|
502
|
+
*/
|
|
503
|
+
function formatStatus(result) {
|
|
504
|
+
if (result.authoritative && result.workflowState) {
|
|
505
|
+
return formatAuthoritativeStatus(result);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return formatHeuristicStatus(result);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
|
|
369
512
|
module.exports = {
|
|
513
|
+
name: 'status',
|
|
514
|
+
description: 'Intelligent stage detection with confidence scoring',
|
|
515
|
+
handler: async (args, flags, _projectRoot) => {
|
|
516
|
+
const inputs = parseStatusInputs(args, flags);
|
|
517
|
+
const { workflowState, fallbackReason } = resolveWorkflowState(inputs);
|
|
518
|
+
|
|
519
|
+
if (workflowState) {
|
|
520
|
+
const result = buildAuthoritativeStatus(workflowState);
|
|
521
|
+
return { success: true, output: formatStatus(result), ...result };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
success: true,
|
|
526
|
+
...buildMissingWorkflowStateStatus(),
|
|
527
|
+
...(fallbackReason ? { fallbackReason } : {}),
|
|
528
|
+
};
|
|
529
|
+
},
|
|
530
|
+
buildAuthoritativeStatus,
|
|
531
|
+
buildMissingWorkflowStateStatus,
|
|
532
|
+
extractWorkflowStateFromComments,
|
|
533
|
+
resolveWorkflowState,
|
|
534
|
+
parseStatusInputs,
|
|
535
|
+
readWorkflowStateFromBeads,
|
|
370
536
|
detectStage,
|
|
371
537
|
analyzeBranch,
|
|
372
538
|
analyzeFiles,
|
package/lib/commands/sync.js
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
const { execFileSync } = require('node:child_process');
|
|
4
4
|
|
|
5
|
+
function isRecoverableBeadsSyncError(error) {
|
|
6
|
+
const message = error?.message ?? String(error ?? '');
|
|
7
|
+
return (
|
|
8
|
+
message.includes('failed to open database') ||
|
|
9
|
+
message.includes('database not found') ||
|
|
10
|
+
message.includes('no beads configuration found') ||
|
|
11
|
+
message.includes('remote \'origin\' not found') ||
|
|
12
|
+
message.includes('remote "origin" not found')
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
/**
|
|
6
17
|
* Forge Sync Command
|
|
7
18
|
* Syncs Beads issue data by running dolt pull + push.
|
|
@@ -30,7 +41,7 @@ module.exports = {
|
|
|
30
41
|
// Step 1: Check if bd binary exists
|
|
31
42
|
try {
|
|
32
43
|
exec('bd', ['--version'], { stdio: 'pipe' });
|
|
33
|
-
} catch
|
|
44
|
+
} catch { /* intentional: bd not installed, skip sync gracefully */ // NOSONAR S2486
|
|
34
45
|
return {
|
|
35
46
|
success: true,
|
|
36
47
|
synced: false,
|
|
@@ -43,6 +54,13 @@ module.exports = {
|
|
|
43
54
|
exec('bd', ['dolt', 'pull'], { stdio: 'pipe' });
|
|
44
55
|
exec('bd', ['dolt', 'push'], { stdio: 'pipe' });
|
|
45
56
|
} catch (syncErr) {
|
|
57
|
+
if (isRecoverableBeadsSyncError(syncErr)) {
|
|
58
|
+
return {
|
|
59
|
+
success: true,
|
|
60
|
+
synced: false,
|
|
61
|
+
message: 'Beads is installed but not initialized for sync in this worktree — skipping sync',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
46
64
|
return {
|
|
47
65
|
success: false,
|
|
48
66
|
synced: false,
|
package/lib/commands/validate.js
CHANGED
|
@@ -593,6 +593,19 @@ function executeDebugMode({ fixAttempts = 0, claim } = {}) {
|
|
|
593
593
|
}
|
|
594
594
|
|
|
595
595
|
module.exports = {
|
|
596
|
+
name: 'validate',
|
|
597
|
+
description: 'Run validation checks across type, lint, security, and tests',
|
|
598
|
+
handler: async () => {
|
|
599
|
+
const result = await executeValidate();
|
|
600
|
+
if (!result.success) {
|
|
601
|
+
return result;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
...result,
|
|
606
|
+
output: result.summary,
|
|
607
|
+
};
|
|
608
|
+
},
|
|
596
609
|
runTypeCheck,
|
|
597
610
|
runLint,
|
|
598
611
|
runSecurityScan,
|
package/lib/detect-agent.js
CHANGED
|
@@ -13,6 +13,25 @@
|
|
|
13
13
|
const fs = require('node:fs');
|
|
14
14
|
const path = require('node:path');
|
|
15
15
|
|
|
16
|
+
const CANONICAL_AGENT_IDS = new Set([
|
|
17
|
+
'claude',
|
|
18
|
+
'cline',
|
|
19
|
+
'codex',
|
|
20
|
+
'copilot',
|
|
21
|
+
'cursor',
|
|
22
|
+
'kilocode',
|
|
23
|
+
'opencode',
|
|
24
|
+
'roo',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const LEGACY_AGENT_ALIASES = Object.freeze({
|
|
28
|
+
'claude-code': 'claude',
|
|
29
|
+
'github-copilot': 'copilot',
|
|
30
|
+
kilo: 'kilocode',
|
|
31
|
+
'kilo-code': 'kilocode',
|
|
32
|
+
'roo-code': 'roo',
|
|
33
|
+
});
|
|
34
|
+
|
|
16
35
|
/**
|
|
17
36
|
* Layer 2 mapping: env var → agent name.
|
|
18
37
|
* Order matters — first match wins within this layer.
|
|
@@ -34,9 +53,9 @@ const AGENT_ENV_VARS = [
|
|
|
34
53
|
// OpenCode
|
|
35
54
|
['OPENCODE_CLIENT', 'opencode'],
|
|
36
55
|
// GitHub Copilot
|
|
37
|
-
['COPILOT_MODEL', '
|
|
38
|
-
['COPILOT_ALLOW_ALL', '
|
|
39
|
-
['COPILOT_GITHUB_TOKEN', '
|
|
56
|
+
['COPILOT_MODEL', 'copilot'],
|
|
57
|
+
['COPILOT_ALLOW_ALL', 'copilot'],
|
|
58
|
+
['COPILOT_GITHUB_TOKEN', 'copilot'],
|
|
40
59
|
// Cline, Roo Code, Kilocode — no env vars (VSCode extensions, config-file-only)
|
|
41
60
|
];
|
|
42
61
|
|
|
@@ -52,16 +71,26 @@ const CONFIG_SIGNATURES = [
|
|
|
52
71
|
[path.join('.cursor', 'rules'), 'cursor'],
|
|
53
72
|
['.clinerules', 'cline'],
|
|
54
73
|
['.cline', 'cline'],
|
|
55
|
-
[path.join('.roo', 'rules'), 'roo
|
|
56
|
-
['.roo', 'roo
|
|
74
|
+
[path.join('.roo', 'rules'), 'roo'],
|
|
75
|
+
['.roo', 'roo'],
|
|
76
|
+
['.roorules', 'roo'],
|
|
77
|
+
['.kilo.md', 'kilocode'],
|
|
57
78
|
['.kilocode', 'kilocode'],
|
|
58
79
|
['codex.md', 'codex'],
|
|
59
80
|
['.codex', 'codex'],
|
|
60
81
|
[path.join('.opencode', 'commands'), 'opencode'],
|
|
61
82
|
['.opencode', 'opencode'],
|
|
62
|
-
[path.join('.github', 'copilot-instructions.md'), '
|
|
83
|
+
[path.join('.github', 'copilot-instructions.md'), 'copilot'],
|
|
63
84
|
];
|
|
64
85
|
|
|
86
|
+
function normalizeAgentId(agentId) {
|
|
87
|
+
if (!agentId) return agentId;
|
|
88
|
+
const trimmed = String(agentId).trim();
|
|
89
|
+
if (!trimmed) return trimmed;
|
|
90
|
+
const normalized = trimmed.toLowerCase();
|
|
91
|
+
return LEGACY_AGENT_ALIASES[normalized] || (CANONICAL_AGENT_IDS.has(normalized) ? normalized : trimmed);
|
|
92
|
+
}
|
|
93
|
+
|
|
65
94
|
/**
|
|
66
95
|
* Detect the actively running AI agent from environment signals.
|
|
67
96
|
*
|
|
@@ -77,7 +106,7 @@ const CONFIG_SIGNATURES = [
|
|
|
77
106
|
function detectActiveAgent(env = process.env) {
|
|
78
107
|
// Layer 1: AI_AGENT universal env var
|
|
79
108
|
if (env.AI_AGENT) {
|
|
80
|
-
return { name: env.AI_AGENT, source: 'env', confidence: 'high' };
|
|
109
|
+
return { name: normalizeAgentId(env.AI_AGENT), source: 'env', confidence: 'high' };
|
|
81
110
|
}
|
|
82
111
|
|
|
83
112
|
// Layer 2: Agent-specific env vars
|
|
@@ -144,7 +173,7 @@ function detectConfiguredAgents(projectRoot) {
|
|
|
144
173
|
const fullPath = path.join(projectRoot, relativePath);
|
|
145
174
|
try {
|
|
146
175
|
if (fs.existsSync(fullPath)) {
|
|
147
|
-
detected.add(agentName);
|
|
176
|
+
detected.add(normalizeAgentId(agentName));
|
|
148
177
|
}
|
|
149
178
|
} catch (_err) {
|
|
150
179
|
// Permission error or other fs issue — skip silently
|
|
@@ -188,4 +217,5 @@ module.exports = {
|
|
|
188
217
|
detectActiveAgent,
|
|
189
218
|
detectConfiguredAgents,
|
|
190
219
|
detectEnvironment,
|
|
220
|
+
normalizeAgentId,
|
|
191
221
|
};
|