convoke-agents 3.0.3 → 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.
@@ -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 (restoreError) {
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
  }