convoke-agents 3.0.4 → 3.1.0
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/CHANGELOG.md +29 -0
- package/README.md +14 -13
- package/_bmad/bme/_gyre/guides/GYRE-TEAM-GUIDE.md +506 -0
- package/_bmad/bme/_vortex/config.yaml +1 -1
- package/_bmad/bme/_vortex/guides/VORTEX-TEAM-GUIDE.md +441 -0
- package/package.json +7 -3
- package/scripts/archive.js +26 -45
- package/scripts/convoke-check.js +88 -0
- package/scripts/convoke-doctor.js +132 -4
- package/scripts/lib/artifact-utils.js +1674 -0
- package/scripts/lib/portfolio/formatters/markdown-formatter.js +40 -0
- package/scripts/lib/portfolio/formatters/terminal-formatter.js +56 -0
- package/scripts/lib/portfolio/portfolio-engine.js +305 -0
- package/scripts/lib/portfolio/rules/artifact-chain-rule.js +126 -0
- package/scripts/lib/portfolio/rules/conflict-resolver.js +77 -0
- package/scripts/lib/portfolio/rules/frontmatter-rule.js +42 -0
- package/scripts/lib/portfolio/rules/git-recency-rule.js +69 -0
- package/scripts/lib/types.js +122 -0
- package/scripts/migrate-artifacts.js +380 -0
- package/scripts/update/lib/migration-runner.js +1 -1
- package/scripts/update/lib/taxonomy-merger.js +138 -0
- package/scripts/update/migrations/2.0.x-to-3.1.0.js +50 -0
- package/scripts/update/migrations/3.0.x-to-3.1.0.js +41 -0
- package/scripts/update/migrations/registry.js +14 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule 3: Infer status from git recency (stale detection).
|
|
3
|
+
*
|
|
4
|
+
* Checks the most recent git commit date for the initiative's artifacts.
|
|
5
|
+
* If within stale_days → ongoing. If beyond → stale.
|
|
6
|
+
*
|
|
7
|
+
* Known limitation: checks current branch only.
|
|
8
|
+
*
|
|
9
|
+
* @param {import('../../types').InitiativeState} state - Current initiative state
|
|
10
|
+
* @param {Array<{filename: string, dir: string, fullPath: string}>} artifacts - Artifacts for this initiative
|
|
11
|
+
* @param {Object} options
|
|
12
|
+
* @param {number} [options.staleDays=30] - Days threshold for stale detection
|
|
13
|
+
* @param {string} options.projectRoot - Absolute path to project root
|
|
14
|
+
* @returns {import('../../types').InitiativeState} Enriched state
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { execFileSync } = require('child_process');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
function applyGitRecencyRule(state, artifacts, options = {}) {
|
|
21
|
+
// Don't override explicit frontmatter status
|
|
22
|
+
if (state.status.confidence === 'explicit') return state;
|
|
23
|
+
|
|
24
|
+
const { staleDays = 30, projectRoot } = options;
|
|
25
|
+
if (!projectRoot || artifacts.length === 0) return state;
|
|
26
|
+
|
|
27
|
+
// Find most recent git activity across all artifacts for this initiative
|
|
28
|
+
let latestDate = null;
|
|
29
|
+
let latestFile = null;
|
|
30
|
+
|
|
31
|
+
for (const artifact of artifacts) {
|
|
32
|
+
try {
|
|
33
|
+
const relativePath = path.relative(projectRoot, artifact.fullPath);
|
|
34
|
+
const dateStr = execFileSync(
|
|
35
|
+
'git', ['log', '-1', '--format=%as', '--', relativePath],
|
|
36
|
+
{ cwd: projectRoot, encoding: 'utf8', stdio: 'pipe' }
|
|
37
|
+
).trim();
|
|
38
|
+
|
|
39
|
+
if (dateStr && (!latestDate || dateStr > latestDate)) {
|
|
40
|
+
latestDate = dateStr;
|
|
41
|
+
latestFile = artifact.filename;
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// File not tracked or git unavailable — skip
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!latestDate) return state;
|
|
49
|
+
|
|
50
|
+
// Update lastArtifact if git date is more recent than artifact-chain's date
|
|
51
|
+
if (!state.lastArtifact.file || latestDate > (state.lastArtifact.date || '')) {
|
|
52
|
+
state.lastArtifact = { file: latestFile, date: latestDate };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Calculate days since last activity
|
|
56
|
+
const lastActivity = new Date(latestDate);
|
|
57
|
+
const now = new Date();
|
|
58
|
+
const daysSince = Math.floor((now - lastActivity) / (1000 * 60 * 60 * 24));
|
|
59
|
+
|
|
60
|
+
if (daysSince <= staleDays) {
|
|
61
|
+
state.status = { value: 'ongoing', source: 'git-recency', confidence: 'inferred' };
|
|
62
|
+
} else {
|
|
63
|
+
state.status = { value: 'stale', source: 'git-recency', confidence: 'inferred' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return state;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { applyGitRecencyRule };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared JSDoc type definitions for the Artifact Governance & Portfolio system.
|
|
3
|
+
* Used by: artifact-utils.js, migrate-artifacts.js, portfolio-engine.js, archive.js
|
|
4
|
+
*
|
|
5
|
+
* @module types
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Inference signal with value, source, and confidence level.
|
|
10
|
+
* @typedef {Object} InferenceSignal
|
|
11
|
+
* @property {string} value - The inferred value
|
|
12
|
+
* @property {string} source - Where the inference came from (e.g., 'frontmatter', 'artifact-chain', 'git-recency')
|
|
13
|
+
* @property {'explicit'|'inferred'} confidence - Whether this is from explicit data or heuristic inference
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* State of an initiative as derived by the portfolio inference rule chain.
|
|
18
|
+
* This is the core data structure that flows through the rule chain and into formatters.
|
|
19
|
+
* @typedef {Object} InitiativeState
|
|
20
|
+
* @property {string} initiative - Initiative ID from taxonomy (e.g., 'helm', 'gyre')
|
|
21
|
+
* @property {InferenceSignal} phase - Current phase: discovery, planning, build, blocked, complete, unknown
|
|
22
|
+
* @property {InferenceSignal} status - Current status: ongoing, blocked, paused, complete, stale, unknown
|
|
23
|
+
* @property {{file: string, date: string}} lastArtifact - Most recently modified artifact for this initiative
|
|
24
|
+
* @property {{value: string, source: string}} nextAction - Suggested next action based on chain gap analysis
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @deprecated Use ManifestEntry instead. Planning placeholder with incomplete fields.
|
|
29
|
+
* @typedef {Object} RenameManifestEntry
|
|
30
|
+
* @property {string} oldPath - Current file path (relative to project root)
|
|
31
|
+
* @property {string} newPath - Proposed new file path
|
|
32
|
+
* @property {string} initiative - Inferred initiative ID
|
|
33
|
+
* @property {string} artifactType - Inferred artifact type
|
|
34
|
+
* @property {'high'|'low'} confidence - Inference confidence level
|
|
35
|
+
* @property {'fully-governed'|'half-governed'|'ungoverned'|'invalid-governed'} governanceState - Current governance state
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Manifest entry for dry-run display. Replaces RenameManifestEntry.
|
|
40
|
+
* @typedef {Object} ManifestEntry
|
|
41
|
+
* @property {string} oldPath - Current relative path (e.g., 'planning-artifacts/prd-gyre.md')
|
|
42
|
+
* @property {string|null} newPath - Proposed new path (null for SKIP/CONFLICT/AMBIGUOUS)
|
|
43
|
+
* @property {string|null} initiative - Resolved initiative ID (null if ambiguous)
|
|
44
|
+
* @property {string|null} artifactType - Resolved artifact type (null if ungoverned)
|
|
45
|
+
* @property {'high'|'low'} confidence - Initiative inference confidence
|
|
46
|
+
* @property {string} source - Inference source (exact/alias/empty/unresolved)
|
|
47
|
+
* @property {'RENAME'|'SKIP'|'INJECT_ONLY'|'CONFLICT'|'AMBIGUOUS'} action
|
|
48
|
+
* @property {string} dir - Directory name (e.g., 'planning-artifacts')
|
|
49
|
+
* @property {{firstLines: string[], gitAuthor: string|null, gitDate: string|null}|null} contextClues
|
|
50
|
+
* @property {string[]|null} crossReferences - Files referencing this one (verbose only)
|
|
51
|
+
* @property {string[]} candidates - Possible initiative matches (ambiguous only)
|
|
52
|
+
* @property {string[]|null} collisionWith - Other files colliding on same newPath
|
|
53
|
+
* @property {string|null} frontmatterInitiative - Initiative from frontmatter (for CONFLICT display)
|
|
54
|
+
* @property {string|null} fileInitiative - Initiative from filename (for CONFLICT display)
|
|
55
|
+
* @property {'high'|'low'} typeConfidence - Artifact type inference confidence
|
|
56
|
+
* @property {string} typeSource - Artifact type inference source (prefix/alias/none)
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Result of generateManifest() containing all entries, collisions, and summary.
|
|
61
|
+
* @typedef {Object} ManifestResult
|
|
62
|
+
* @property {ManifestEntry[]} entries - All manifest entries
|
|
63
|
+
* @property {Map<string, string[]>} collisions - Colliding newPath -> list of oldPaths
|
|
64
|
+
* @property {{total: number, skip: number, rename: number, inject: number, conflict: number, ambiguous: number}} summary
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A markdown link that needs updating after file renames.
|
|
69
|
+
* @typedef {Object} LinkUpdate
|
|
70
|
+
* @property {string} filePath - Path of the file containing the link
|
|
71
|
+
* @property {string} oldLink - Original link target
|
|
72
|
+
* @property {string} newLink - Updated link target
|
|
73
|
+
* @property {'bracket-link'|'relative-link'|'parent-link'|'frontmatter-array'} pattern - Which link pattern matched
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parsed taxonomy configuration from _bmad/_config/taxonomy.yaml.
|
|
78
|
+
* @typedef {Object} TaxonomyConfig
|
|
79
|
+
* @property {{platform: string[], user: string[]}} initiatives - Initiative IDs split by ownership
|
|
80
|
+
* @property {string[]} artifact_types - Valid artifact type identifiers
|
|
81
|
+
* @property {Object<string, string>} aliases - Historical name → canonical initiative ID mapping (migration-only)
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Frontmatter metadata fields for governed artifacts.
|
|
86
|
+
* @typedef {Object} FrontmatterSchema
|
|
87
|
+
* @property {string} initiative - Initiative ID from taxonomy
|
|
88
|
+
* @property {string} artifact_type - Artifact type from taxonomy
|
|
89
|
+
* @property {'draft'|'validated'|'superseded'|'active'} [status] - Optional artifact-level status
|
|
90
|
+
* @property {string} created - ISO 8601 date string (YYYY-MM-DD)
|
|
91
|
+
* @property {number} schema_version - Schema version integer (currently 1)
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Result of parsing a filename against naming conventions.
|
|
96
|
+
* @typedef {Object} ParsedFilename
|
|
97
|
+
* @property {string} filename - Original filename
|
|
98
|
+
* @property {boolean} isDated - Whether the file has a date suffix
|
|
99
|
+
* @property {string|null} date - Extracted date (YYYY-MM-DD) or null
|
|
100
|
+
* @property {string} baseName - Filename without date and extension
|
|
101
|
+
* @property {string|null} category - Extracted category prefix or null
|
|
102
|
+
* @property {boolean} hasValidCategory - Whether category is in the valid list
|
|
103
|
+
* @property {boolean} isUppercase - Whether filename contains uppercase characters
|
|
104
|
+
* @property {boolean} matchesConvention - Whether filename fully matches naming convention
|
|
105
|
+
*/
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Result of a frontmatter conflict check.
|
|
109
|
+
* @typedef {Object} FrontmatterConflict
|
|
110
|
+
* @property {string} field - The conflicting field name
|
|
111
|
+
* @property {*} existingValue - Current value in frontmatter
|
|
112
|
+
* @property {*} newValue - Proposed new value
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Result of injectFrontmatter() including conflict detection.
|
|
117
|
+
* @typedef {Object} InjectResult
|
|
118
|
+
* @property {string} content - The modified file content with injected frontmatter
|
|
119
|
+
* @property {FrontmatterConflict[]} conflicts - Any field conflicts detected (empty if none)
|
|
120
|
+
*/
|
|
121
|
+
|
|
122
|
+
module.exports = {};
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convoke Artifact Governance Migration CLI
|
|
5
|
+
*
|
|
6
|
+
* Dry-run by default — shows what the migration would do without changing anything.
|
|
7
|
+
* Use --apply to execute renames. Use --apply --force to skip confirmation.
|
|
8
|
+
*
|
|
9
|
+
* @module migrate-artifacts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs-extra');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const yaml = require('js-yaml');
|
|
15
|
+
const { findProjectRoot } = require('./update/lib/utils');
|
|
16
|
+
const {
|
|
17
|
+
readTaxonomy,
|
|
18
|
+
generateManifest,
|
|
19
|
+
formatManifest,
|
|
20
|
+
ensureCleanTree,
|
|
21
|
+
executeRenames,
|
|
22
|
+
ArtifactMigrationError,
|
|
23
|
+
verifyHistoryChain,
|
|
24
|
+
executeInjections,
|
|
25
|
+
resolveAmbiguous,
|
|
26
|
+
detectMigrationState,
|
|
27
|
+
generateGovernanceADR,
|
|
28
|
+
supersedePreviousADR
|
|
29
|
+
} = require('./lib/artifact-utils');
|
|
30
|
+
|
|
31
|
+
// --- CLI Argument Parsing ---
|
|
32
|
+
|
|
33
|
+
const DEFAULT_INCLUDE_DIRS = Object.freeze(['planning-artifacts', 'vortex-artifacts', 'gyre-artifacts']);
|
|
34
|
+
|
|
35
|
+
/** Pattern for valid directory names: lowercase alphanumeric, dashes, underscores */
|
|
36
|
+
const VALID_DIR_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse CLI arguments from argv array.
|
|
40
|
+
*
|
|
41
|
+
* @param {string[]} argv - Arguments (typically process.argv.slice(2))
|
|
42
|
+
* @returns {{help: boolean, includeDirs: string[], apply: boolean, force: boolean, verbose: boolean}}
|
|
43
|
+
*/
|
|
44
|
+
function parseArgs(argv) {
|
|
45
|
+
const help = argv.includes('--help') || argv.includes('-h');
|
|
46
|
+
const apply = argv.includes('--apply');
|
|
47
|
+
const force = argv.includes('--force');
|
|
48
|
+
const verbose = argv.includes('--verbose');
|
|
49
|
+
|
|
50
|
+
let includeDirs = [...DEFAULT_INCLUDE_DIRS];
|
|
51
|
+
const includeIdx = argv.indexOf('--include');
|
|
52
|
+
if (includeIdx !== -1) {
|
|
53
|
+
const nextArg = argv[includeIdx + 1];
|
|
54
|
+
// Skip if next arg is missing or is another flag
|
|
55
|
+
if (nextArg && !nextArg.startsWith('--')) {
|
|
56
|
+
const parsed = nextArg.split(',').map(d => d.trim()).filter(Boolean);
|
|
57
|
+
// Validate: only simple directory names (no path traversal)
|
|
58
|
+
const valid = parsed.filter(d => VALID_DIR_PATTERN.test(d));
|
|
59
|
+
const invalid = parsed.filter(d => !VALID_DIR_PATTERN.test(d));
|
|
60
|
+
if (invalid.length > 0) {
|
|
61
|
+
console.warn(`Warning: Invalid directory names ignored: ${invalid.join(', ')}`);
|
|
62
|
+
}
|
|
63
|
+
if (valid.length > 0) {
|
|
64
|
+
includeDirs = valid;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { help, includeDirs, apply, force, verbose };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// --- Help ---
|
|
73
|
+
|
|
74
|
+
function printHelp() {
|
|
75
|
+
console.log(`
|
|
76
|
+
Usage: convoke-migrate-artifacts [options]
|
|
77
|
+
|
|
78
|
+
Analyze artifact files and show what the governance migration would do.
|
|
79
|
+
Dry-run by default — no files are modified.
|
|
80
|
+
|
|
81
|
+
Options:
|
|
82
|
+
--include <dirs> Comma-separated directory names to scan (relative to _bmad-output/)
|
|
83
|
+
Default: planning-artifacts,vortex-artifacts,gyre-artifacts
|
|
84
|
+
--verbose Show cross-references for ambiguous files
|
|
85
|
+
--apply Execute the rename migration (commit 1: git mv)
|
|
86
|
+
--force Bypass confirmation prompt (use with --apply for automation)
|
|
87
|
+
--help, -h Show this help
|
|
88
|
+
|
|
89
|
+
Examples:
|
|
90
|
+
convoke-migrate-artifacts Dry-run with default scope
|
|
91
|
+
convoke-migrate-artifacts --verbose Dry-run with cross-references
|
|
92
|
+
convoke-migrate-artifacts --include planning-artifacts Dry-run for one directory
|
|
93
|
+
`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- Taxonomy Bootstrap ---
|
|
97
|
+
|
|
98
|
+
const PLATFORM_INITIATIVES = ['vortex', 'gyre', 'bmm', 'forge', 'helm', 'enhance', 'loom', 'convoke'];
|
|
99
|
+
|
|
100
|
+
const DEFAULT_ARTIFACT_TYPES = [
|
|
101
|
+
'prd', 'epic', 'arch', 'adr', 'persona', 'lean-persona', 'empathy-map',
|
|
102
|
+
'problem-def', 'hypothesis', 'experiment', 'signal', 'decision', 'scope',
|
|
103
|
+
'pre-reg', 'sprint', 'brief', 'vision', 'report', 'research', 'story', 'spec'
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create taxonomy.yaml with platform defaults if it does not exist.
|
|
108
|
+
* Idempotent — never overwrites an existing file.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
111
|
+
* @returns {boolean} true if file was created, false if already existed
|
|
112
|
+
*/
|
|
113
|
+
function bootstrapTaxonomy(projectRoot) {
|
|
114
|
+
const configDir = path.join(projectRoot, '_bmad', '_config');
|
|
115
|
+
const configPath = path.join(configDir, 'taxonomy.yaml');
|
|
116
|
+
|
|
117
|
+
if (fs.existsSync(configPath)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const defaults = {
|
|
122
|
+
initiatives: {
|
|
123
|
+
platform: PLATFORM_INITIATIVES,
|
|
124
|
+
user: []
|
|
125
|
+
},
|
|
126
|
+
artifact_types: DEFAULT_ARTIFACT_TYPES,
|
|
127
|
+
aliases: {}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const header = [
|
|
131
|
+
'# Artifact Governance Taxonomy Configuration',
|
|
132
|
+
'# Schema version: 1',
|
|
133
|
+
`# Created by: convoke-migrate-artifacts bootstrap`,
|
|
134
|
+
'#',
|
|
135
|
+
'# This file is the single source of truth for initiative IDs, artifact types,',
|
|
136
|
+
'# and historical name aliases used by the governance system.',
|
|
137
|
+
''
|
|
138
|
+
].join('\n');
|
|
139
|
+
|
|
140
|
+
fs.ensureDirSync(configDir);
|
|
141
|
+
fs.writeFileSync(configPath, header + yaml.dump(defaults, { lineWidth: -1 }), 'utf8');
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --- Interactive Prompt ---
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Prompt the operator to confirm migration apply.
|
|
149
|
+
* Exported for mocking in tests — tests should NEVER interact with real readline.
|
|
150
|
+
*
|
|
151
|
+
* @returns {Promise<boolean>} true if operator confirms
|
|
152
|
+
*/
|
|
153
|
+
async function confirmApply() {
|
|
154
|
+
const readline = require('readline');
|
|
155
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
156
|
+
return new Promise(resolve => {
|
|
157
|
+
rl.on('close', () => resolve(false)); // piped/closed stdin → reject
|
|
158
|
+
rl.question('Apply migration? [y/n] ', answer => {
|
|
159
|
+
rl.close();
|
|
160
|
+
resolve(typeof answer === 'string' && answer.trim().toLowerCase() === 'y');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- Main ---
|
|
166
|
+
|
|
167
|
+
async function main() {
|
|
168
|
+
const args = parseArgs(process.argv.slice(2));
|
|
169
|
+
|
|
170
|
+
if (args.help) {
|
|
171
|
+
printHelp();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (args.force && !args.apply) {
|
|
176
|
+
console.log('Warning: --force has no effect without --apply. Running dry-run instead.');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const projectRoot = findProjectRoot();
|
|
180
|
+
if (!projectRoot) {
|
|
181
|
+
console.error('Error: Not in a Convoke project. Could not find _bmad/ directory.');
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Archive exclusion (FR50): always strip _archive from includeDirs
|
|
186
|
+
const excludeDirs = ['_archive'];
|
|
187
|
+
const filteredIncludeDirs = args.includeDirs.filter(d => {
|
|
188
|
+
if (d === '_archive') {
|
|
189
|
+
console.warn('Warning: _archive is always excluded from migration scope (FR50). Ignoring.');
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (filteredIncludeDirs.length === 0) {
|
|
196
|
+
console.error('Error: No directories to scan. All specified directories were excluded.');
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Taxonomy bootstrap (FR49): create if absent, never overwrite
|
|
201
|
+
const created = bootstrapTaxonomy(projectRoot);
|
|
202
|
+
if (created) {
|
|
203
|
+
console.log('Created _bmad/_config/taxonomy.yaml with platform defaults.');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Validate taxonomy (NFR22: graceful error, no stack traces)
|
|
207
|
+
try {
|
|
208
|
+
readTaxonomy(projectRoot);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
console.error(`Error: Invalid taxonomy configuration.`);
|
|
211
|
+
console.error(` ${err.message}`);
|
|
212
|
+
console.error(` Fix the file at: _bmad/_config/taxonomy.yaml`);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Generate manifest (shared by dry-run and apply)
|
|
217
|
+
const manifest = await generateManifest(projectRoot, {
|
|
218
|
+
includeDirs: filteredIncludeDirs,
|
|
219
|
+
excludeDirs,
|
|
220
|
+
verbose: args.verbose
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const output = formatManifest(manifest, { verbose: args.verbose });
|
|
224
|
+
console.log(output);
|
|
225
|
+
|
|
226
|
+
// Dry-run mode (default): just print manifest and exit
|
|
227
|
+
if (!args.apply) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// --- Apply mode ---
|
|
232
|
+
|
|
233
|
+
// Idempotent recovery detection
|
|
234
|
+
let migrationState = detectMigrationState(projectRoot);
|
|
235
|
+
if (migrationState === 'complete') {
|
|
236
|
+
// Secondary check: verify manifest confirms all files governed (catches new files added since migration)
|
|
237
|
+
const hasWork = manifest.entries.some(e => e.action === 'RENAME' || e.action === 'AMBIGUOUS');
|
|
238
|
+
if (!hasWork) {
|
|
239
|
+
console.log('\nNothing to migrate -- all files governed.');
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
// New files found — proceed as fresh migration
|
|
243
|
+
console.log('\nPrevious migration detected, but new ungoverned files found. Proceeding with fresh migration.');
|
|
244
|
+
migrationState = 'fresh';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Load taxonomy for ambiguous resolution
|
|
248
|
+
const taxonomy = readTaxonomy(projectRoot);
|
|
249
|
+
|
|
250
|
+
// Resolve ambiguous files interactively (or auto-skip in --force mode)
|
|
251
|
+
const resolution = await resolveAmbiguous(manifest, taxonomy, projectRoot, { force: args.force });
|
|
252
|
+
if (resolution.resolved > 0 || resolution.skipped > 0) {
|
|
253
|
+
console.log(`\nAmbiguous resolution: ${resolution.resolved} resolved, ${resolution.skipped} skipped.`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Re-compute counts and re-check collisions after resolution (new RENAME entries may collide)
|
|
257
|
+
const { detectCollisions } = require('./lib/artifact-utils');
|
|
258
|
+
manifest.collisions = detectCollisions(manifest.entries);
|
|
259
|
+
const renameCount = manifest.summary.rename;
|
|
260
|
+
const skipCount = manifest.entries.filter(e => e.action === 'SKIP').length;
|
|
261
|
+
const ambiguousLeft = manifest.summary.ambiguous;
|
|
262
|
+
console.log(`\n${renameCount} files will be renamed. ${skipCount} skipped. ${ambiguousLeft} still ambiguous.`);
|
|
263
|
+
|
|
264
|
+
// Block on collisions (includes post-resolution collisions)
|
|
265
|
+
if (manifest.collisions.size > 0) {
|
|
266
|
+
console.error(`Error: ${manifest.collisions.size} filename collision(s) detected. Resolve before applying.`);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (renameCount === 0) {
|
|
271
|
+
console.log('Nothing to rename.');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Confirmation prompt (unless --force)
|
|
276
|
+
if (!args.force) {
|
|
277
|
+
const confirmed = await confirmApply();
|
|
278
|
+
if (!confirmed) {
|
|
279
|
+
console.log('Migration aborted.');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Pre-flight: ensure clean tree
|
|
285
|
+
try {
|
|
286
|
+
ensureCleanTree(filteredIncludeDirs, projectRoot);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
console.error(`Error: ${err.message}`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Execute migration phases
|
|
293
|
+
try {
|
|
294
|
+
// Phase routing based on idempotent recovery state
|
|
295
|
+
if (migrationState === 'renames-done') {
|
|
296
|
+
const priorCount = manifest.entries.filter(e => e.action === 'RENAME').length;
|
|
297
|
+
console.log(`\nDetected partial migration (${priorCount} renames done, frontmatter pending). Resuming commit 2.`);
|
|
298
|
+
} else {
|
|
299
|
+
// Commit 1: renames
|
|
300
|
+
const renameResult = executeRenames(manifest, projectRoot);
|
|
301
|
+
console.log(`\nRename phase complete. ${renameResult.renamedCount} files renamed. Commit: ${renameResult.commitSha}`);
|
|
302
|
+
|
|
303
|
+
// Verify history chain (informational)
|
|
304
|
+
const renamedEntries = manifest.entries.filter(e => e.action === 'RENAME');
|
|
305
|
+
const verification = verifyHistoryChain(renamedEntries, projectRoot);
|
|
306
|
+
if (verification.failed.length > 0) {
|
|
307
|
+
console.warn(`Warning: git log --follow failed for ${verification.failed.length} file(s):`);
|
|
308
|
+
for (const f of verification.failed) {
|
|
309
|
+
console.warn(` ${f}`);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
console.log(`History chain verified for ${verification.verified} sample file(s).`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Commit 2: frontmatter injection + link updating + rename map
|
|
317
|
+
const injResult = await executeInjections(manifest, projectRoot, filteredIncludeDirs);
|
|
318
|
+
console.log(`\nInjection phase complete. ${injResult.injectedCount} files injected, ${injResult.linkUpdates.updatedLinks} links updated, ${injResult.conflictCount} conflicts skipped. Commit: ${injResult.commitSha}`);
|
|
319
|
+
|
|
320
|
+
// Commit 3: ADR generation (non-blocking — failure logs warning, doesn't rollback)
|
|
321
|
+
try {
|
|
322
|
+
const date = new Date().toISOString().split('T')[0];
|
|
323
|
+
const newADRFilename = `adr-artifact-governance-convention-${date}.md`;
|
|
324
|
+
const adrContent = generateGovernanceADR(date, {
|
|
325
|
+
renamedCount: renameCount,
|
|
326
|
+
injectedCount: injResult.injectedCount,
|
|
327
|
+
linksUpdated: injResult.linkUpdates.updatedLinks,
|
|
328
|
+
scopeDirs: filteredIncludeDirs
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const adrDir = path.join(projectRoot, '_bmad-output', 'planning-artifacts');
|
|
332
|
+
fs.ensureDirSync(adrDir);
|
|
333
|
+
const adrPath = path.join(adrDir, newADRFilename);
|
|
334
|
+
// Write new ADR FIRST, then supersede old (prevents orphaned supersession if write fails)
|
|
335
|
+
fs.writeFileSync(adrPath, adrContent, 'utf8');
|
|
336
|
+
supersedePreviousADR(projectRoot, newADRFilename);
|
|
337
|
+
|
|
338
|
+
const { execFileSync: execGit } = require('child_process');
|
|
339
|
+
execGit('git', ['add', '_bmad-output/planning-artifacts/'], { cwd: projectRoot, stdio: 'pipe' });
|
|
340
|
+
execGit('git', ['commit', '-m', 'chore: generate governance convention ADR'], { cwd: projectRoot, stdio: 'pipe' });
|
|
341
|
+
console.log(`\nADR generated: ${newADRFilename}`);
|
|
342
|
+
} catch (adrErr) {
|
|
343
|
+
console.warn(`\nWarning: ADR generation failed: ${adrErr.message}`);
|
|
344
|
+
console.warn('Migration data is intact (commits 1-2 preserved). ADR can be generated manually.');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Final summary
|
|
348
|
+
console.log(`\nMigration complete. ${renameCount} files renamed, ${injResult.injectedCount} frontmatter injected, ${injResult.linkUpdates.updatedLinks} links updated, ${skipCount} skipped.`);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
if (err instanceof ArtifactMigrationError && err.phase === 'rename') {
|
|
351
|
+
console.error(`\nRename failed: ${err.message}`);
|
|
352
|
+
if (err.recoverable) {
|
|
353
|
+
console.error('Rollback complete. No changes made.');
|
|
354
|
+
} else {
|
|
355
|
+
console.error('WARNING: Rollback may have failed. Run `git status` to check working tree state.');
|
|
356
|
+
}
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
if (err instanceof ArtifactMigrationError && err.phase === 'inject') {
|
|
360
|
+
console.error(`\nInjection failed: ${err.message}`);
|
|
361
|
+
if (err.recoverable) {
|
|
362
|
+
console.error('Renames preserved (commit 1). Injections discarded.');
|
|
363
|
+
} else {
|
|
364
|
+
console.error('WARNING: Rollback may have failed. Run `git status` to check working tree state.');
|
|
365
|
+
}
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
throw err;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Run if invoked directly
|
|
373
|
+
if (require.main === module) {
|
|
374
|
+
main().catch(err => {
|
|
375
|
+
console.error(`Error: ${err.message}`);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
module.exports = { parseArgs, main, confirmApply, bootstrapTaxonomy, DEFAULT_INCLUDE_DIRS, PLATFORM_INITIATIVES, DEFAULT_ARTIFACT_TYPES, VALID_DIR_PATTERN };
|
|
@@ -466,7 +466,7 @@ async function runRefreshOnly(fromVersion, options = {}) {
|
|
|
466
466
|
try {
|
|
467
467
|
await backupManager.restoreBackup(backupMetadata, projectRoot);
|
|
468
468
|
console.log(chalk.green('✓ Installation restored from backup'));
|
|
469
|
-
} catch (
|
|
469
|
+
} catch (_restoreError) {
|
|
470
470
|
console.error(chalk.red('✗ Restore failed!'));
|
|
471
471
|
console.error(chalk.yellow(`Manual restore may be needed from: ${backupMetadata.backup_dir}`));
|
|
472
472
|
}
|