depa-codument 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +262 -0
  3. package/package.json +63 -0
  4. package/src/cli/commands/archive.ts +519 -0
  5. package/src/cli/commands/decisions.ts +123 -0
  6. package/src/cli/commands/engineering.ts +105 -0
  7. package/src/cli/commands/init.ts +54 -0
  8. package/src/cli/commands/list.ts +73 -0
  9. package/src/cli/commands/modeling.ts +105 -0
  10. package/src/cli/commands/show.ts +238 -0
  11. package/src/cli/commands/status.ts +140 -0
  12. package/src/cli/commands/upgrade-track.ts +385 -0
  13. package/src/cli/commands/upgrade-workspace.ts +138 -0
  14. package/src/cli/commands/validate.ts +330 -0
  15. package/src/cli/engineering/config.ts +68 -0
  16. package/src/cli/engineering/lint.ts +58 -0
  17. package/src/cli/engineering/merge.ts +172 -0
  18. package/src/cli/engineering/registry.ts +230 -0
  19. package/src/cli/engineering/schema.ts +126 -0
  20. package/src/cli/engineering/validate.ts +286 -0
  21. package/src/cli/index.ts +136 -0
  22. package/src/cli/modeling/config.ts +68 -0
  23. package/src/cli/modeling/lint.ts +58 -0
  24. package/src/cli/modeling/merge.ts +172 -0
  25. package/src/cli/modeling/registry.ts +229 -0
  26. package/src/cli/modeling/schema.ts +160 -0
  27. package/src/cli/modeling/validate.ts +282 -0
  28. package/src/cli/utils/index.ts +941 -0
  29. package/src/cli/utils/install.ts +291 -0
  30. package/src/cli/utils/spec-xml.ts +673 -0
  31. package/src/cli/utils/track-time.ts +75 -0
  32. package/src/cli/utils/vfs.ts +102 -0
  33. package/src/templates/codument/README.md +59 -0
  34. package/src/templates/codument/attractors/product.md +17 -0
  35. package/src/templates/codument/attractors/project.md +10 -0
  36. package/src/templates/codument/backlog/README.md +33 -0
  37. package/src/templates/codument/config/attractor-profiles.xml +31 -0
  38. package/src/templates/codument/config/engineering.xml +22 -0
  39. package/src/templates/codument/config/modeling.xml +22 -0
  40. package/src/templates/codument/config/operation-hooks.xml +55 -0
  41. package/src/templates/codument/memory/README.md +13 -0
  42. package/src/templates/codument/missions/README.md +125 -0
  43. package/src/templates/codument/sop/README.md +14 -0
  44. package/src/templates/codument/std/AGENTS.md +82 -0
  45. package/src/templates/codument/std/attractors/depa-attractor.md +572 -0
  46. package/src/templates/codument/std/attractors/knowledge-tiers.md +128 -0
  47. package/src/templates/codument/std/attractors/model-driven-docs.md +293 -0
  48. package/src/templates/codument/std/attractors/project-memory.md +48 -0
  49. package/src/templates/codument/std/docs-impl-fractal/index.md +110 -0
  50. package/src/templates/codument/std/docs-modeling-fractal/index.md +156 -0
  51. package/src/templates/codument/std/kernel-pointer.md +19 -0
  52. package/src/templates/codument/std/operations/README.md +30 -0
  53. package/src/templates/codument/std/operations/_operation-spec.md +41 -0
  54. package/src/templates/codument/std/operations/archive-mission.md +66 -0
  55. package/src/templates/codument/std/operations/archive-track.md +238 -0
  56. package/src/templates/codument/std/operations/artifact-sync.md +172 -0
  57. package/src/templates/codument/std/operations/discuss-phase.md +214 -0
  58. package/src/templates/codument/std/operations/discuss.md +87 -0
  59. package/src/templates/codument/std/operations/docs-bootstrap.md +148 -0
  60. package/src/templates/codument/std/operations/gap-loop.md +301 -0
  61. package/src/templates/codument/std/operations/impl-mission.md +167 -0
  62. package/src/templates/codument/std/operations/impl-quick.md +79 -0
  63. package/src/templates/codument/std/operations/impl-track.md +537 -0
  64. package/src/templates/codument/std/operations/migrate.md +337 -0
  65. package/src/templates/codument/std/operations/plan-mission.md +230 -0
  66. package/src/templates/codument/std/operations/plan-track-wave.md +231 -0
  67. package/src/templates/codument/std/operations/plan-track.md +579 -0
  68. package/src/templates/codument/std/operations/revise-track.md +136 -0
  69. package/src/templates/codument/std/operations/validate.md +339 -0
  70. package/src/templates/codument/std/operations/verify.md +184 -0
  71. package/src/templates/codument/std/root-agents.md +39 -0
  72. package/src/templates/codument/std/sop/questioning.md +98 -0
  73. package/src/templates/codument/std/sop/tdd.md +26 -0
  74. package/src/templates/codument/std/sop/validation.md +25 -0
  75. package/src/templates/codument/std/sop/wave-exec.md +42 -0
  76. package/src/templates/codument/std/sop/workflow.md +35 -0
  77. package/src/templates/codument/std/spec/behavior-delta.md +36 -0
  78. package/src/templates/codument/std/spec/behavior-registry.md +42 -0
  79. package/src/templates/codument/std/spec/engineering-delta.md +68 -0
  80. package/src/templates/codument/std/spec/engineering-node-schema.md +86 -0
  81. package/src/templates/codument/std/spec/engineering-registry.md +82 -0
  82. package/src/templates/codument/std/spec/flow-notation.md +93 -0
  83. package/src/templates/codument/std/spec/folder-manifest.md +99 -0
  84. package/src/templates/codument/std/spec/mission-xml-spec.md +249 -0
  85. package/src/templates/codument/std/spec/modeling-delta.md +85 -0
  86. package/src/templates/codument/std/spec/modeling-node-schema.md +183 -0
  87. package/src/templates/codument/std/spec/modeling-registry.md +49 -0
  88. package/src/templates/codument/std/spec/track-xml-spec.md +272 -0
  89. package/src/templates/codument/std/spec/xnl-format.md +301 -0
  90. package/src/templates/codument/workflows/README.md +15 -0
  91. package/src/templates/manifest.ts +177 -0
  92. package/src/templates/skills/README.md +38 -0
  93. package/src/templates/skills/codument-archive/SKILL.md +17 -0
  94. package/src/templates/skills/codument-archive-mission/SKILL.md +17 -0
  95. package/src/templates/skills/codument-archive-track/SKILL.md +17 -0
  96. package/src/templates/skills/codument-artifact-sync/SKILL.md +17 -0
  97. package/src/templates/skills/codument-code-quality-score/SKILL.md +67 -0
  98. package/src/templates/skills/codument-decision-tree/SKILL.md +40 -0
  99. package/src/templates/skills/codument-discuss/SKILL.md +17 -0
  100. package/src/templates/skills/codument-discuss-phase/SKILL.md +17 -0
  101. package/src/templates/skills/codument-docs-bootstrap/SKILL.md +17 -0
  102. package/src/templates/skills/codument-gap-loop/SKILL.md +17 -0
  103. package/src/templates/skills/codument-impl-mission/SKILL.md +17 -0
  104. package/src/templates/skills/codument-impl-quick/SKILL.md +17 -0
  105. package/src/templates/skills/codument-impl-track/SKILL.md +17 -0
  106. package/src/templates/skills/codument-implement/SKILL.md +14 -0
  107. package/src/templates/skills/codument-migrate/SKILL.md +17 -0
  108. package/src/templates/skills/codument-modeling-engineering-e2e/SKILL.md +74 -0
  109. package/src/templates/skills/codument-plan-mission/SKILL.md +17 -0
  110. package/src/templates/skills/codument-plan-track/SKILL.md +17 -0
  111. package/src/templates/skills/codument-plan-track-wave/SKILL.md +17 -0
  112. package/src/templates/skills/codument-revise-track/SKILL.md +17 -0
  113. package/src/templates/skills/codument-track/SKILL.md +14 -0
  114. package/src/templates/skills/codument-validate/SKILL.md +17 -0
  115. package/src/templates/skills/codument-verify/SKILL.md +17 -0
  116. package/src/types/text-assets.d.ts +9 -0
  117. package/src/version.ts +1 -0
@@ -0,0 +1,140 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { getTracks, getSpecs, codumentExists, formatStatus, CODUMENT_DIR, walkTrackTasks } from '../utils';
4
+
5
+ export async function statusCommand(args: string[]) {
6
+ if (!codumentExists()) {
7
+ console.error('Codument is not initialized. Run codument init first.');
8
+ process.exit(1);
9
+ }
10
+
11
+ const tracks = getTracks();
12
+ const specs = getSpecs();
13
+
14
+ // Calculate statistics
15
+ const totalTracks = tracks.length;
16
+ const completedTracks = tracks.filter(t => t.metadata.status === 'completed').length;
17
+ const inProgressTracks = tracks.filter(t => t.metadata.status === 'in_progress').length;
18
+ const newTracks = tracks.filter(t => t.metadata.status === 'new').length;
19
+
20
+ let totalTasks = 0;
21
+ let completedTasks = 0;
22
+ let inProgressTasks = 0;
23
+ let todoTasks = 0;
24
+ let blockedTasks = 0;
25
+
26
+ for (const track of tracks) {
27
+ if (track.taskSummary) {
28
+ totalTasks += track.taskSummary.total_tasks;
29
+ completedTasks += track.taskSummary.completed;
30
+ inProgressTasks += track.taskSummary.in_progress;
31
+ todoTasks += track.taskSummary.todo;
32
+ blockedTasks += track.taskSummary.blocked;
33
+ }
34
+ }
35
+
36
+ const overallProgress = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
37
+
38
+ // Find current track and task
39
+ const currentTrack = tracks.find(t => t.metadata.status === 'in_progress');
40
+ let currentPhase = '';
41
+ let currentTask = '';
42
+ let nextTask = '';
43
+
44
+ if (currentTrack) {
45
+ const file = path.join(CODUMENT_DIR, 'tracks', currentTrack.id, 'track.xml');
46
+ if (fs.existsSync(file)) {
47
+ for (const task of walkTrackTasks(file)) {
48
+ if (!currentTask && task.status === 'ACTIVE') {
49
+ currentPhase = task.phaseName;
50
+ currentTask = task.name;
51
+ }
52
+ if (!nextTask && task.status === 'NOT_STARTED') {
53
+ nextTask = task.name;
54
+ }
55
+ if (currentTask && nextTask) {
56
+ break;
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ // Determine project status
63
+ let projectStatus = 'On Track';
64
+ if (blockedTasks > 0) {
65
+ projectStatus = 'Blocked';
66
+ } else if (totalTracks === 0) {
67
+ projectStatus = 'No Tracks';
68
+ } else if (completedTracks === totalTracks) {
69
+ projectStatus = 'Complete';
70
+ }
71
+
72
+ // Print status
73
+ const now = new Date().toISOString().replace('T', ' ').split('.')[0];
74
+
75
+ console.log('\n' + '═'.repeat(60));
76
+ console.log(' Codument Project Status');
77
+ console.log('═'.repeat(60));
78
+
79
+ console.log(`\n📅 Time: ${now}`);
80
+ console.log(`📊 Status: ${projectStatus}`);
81
+
82
+ console.log('\n' + '─'.repeat(60));
83
+ console.log(' Tracks Overview');
84
+ console.log('─'.repeat(60));
85
+
86
+ if (tracks.length === 0) {
87
+ console.log('\n No tracks found. Use "codument track" to create one.\n');
88
+ } else {
89
+ console.log('\n Status Track Progress');
90
+ console.log(' ' + '-'.repeat(56));
91
+
92
+ for (const track of tracks) {
93
+ const status = formatStatus(track.metadata.status);
94
+ const id = track.id.length > 32 ? track.id.slice(0, 29) + '...' : track.id.padEnd(32);
95
+
96
+ let progress = '-';
97
+ if (track.taskSummary) {
98
+ const { completed, total_tasks } = track.taskSummary;
99
+ const pct = total_tasks > 0 ? Math.round((completed / total_tasks) * 100) : 0;
100
+ progress = `${completed}/${total_tasks} (${pct}%)`;
101
+ }
102
+
103
+ console.log(` ${status} ${id} ${progress}`);
104
+ }
105
+ }
106
+
107
+ console.log('\n' + '─'.repeat(60));
108
+ console.log(' Current Progress');
109
+ console.log('─'.repeat(60));
110
+
111
+ if (currentTrack) {
112
+ console.log(`\n🎯 Current Track: ${currentTrack.id}`);
113
+ if (currentPhase) console.log(` Current Phase: ${currentPhase}`);
114
+ if (currentTask) console.log(` Current Task: ${currentTask}`);
115
+ if (nextTask) console.log(`\n📋 Next: ${nextTask}`);
116
+ } else {
117
+ console.log('\n No track in progress.');
118
+ }
119
+
120
+ if (blockedTasks > 0) {
121
+ console.log(`\n⚠️ Blocked: ${blockedTasks} task(s)`);
122
+ }
123
+
124
+ console.log('\n' + '─'.repeat(60));
125
+ console.log(' Statistics');
126
+ console.log('─'.repeat(60));
127
+
128
+ console.log(`\nTracks: ${totalTracks} total | ${inProgressTracks} in progress | ${newTracks} new | ${completedTracks} completed`);
129
+ console.log(`Tasks: ${totalTasks} total | ${completedTasks} done | ${inProgressTasks} in progress | ${todoTasks} todo`);
130
+
131
+ // Progress bar
132
+ const barWidth = 20;
133
+ const filled = Math.round((overallProgress / 100) * barWidth);
134
+ const empty = barWidth - filled;
135
+ const bar = '█'.repeat(filled) + '░'.repeat(empty);
136
+
137
+ console.log(`Progress: ${bar} ${overallProgress}%`);
138
+
139
+ console.log('\n' + '═'.repeat(60) + '\n');
140
+ }
@@ -0,0 +1,385 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ import {
5
+ ARCHIVE_DIR,
6
+ CODUMENT_DIR,
7
+ TRACKS_DIR,
8
+ codumentExists,
9
+ parseOptions,
10
+ } from '../utils';
11
+
12
+ type ExecutionMode = 'wave' | 'sequential';
13
+
14
+ function safeTimestamp(): string {
15
+ return new Date().toISOString().replace(/[:.]/g, '-');
16
+ }
17
+
18
+ function ensureDir(dirPath: string): void {
19
+ if (!fs.existsSync(dirPath)) {
20
+ fs.mkdirSync(dirPath, { recursive: true });
21
+ }
22
+ }
23
+
24
+ function ensureParentDir(filePath: string): void {
25
+ ensureDir(path.dirname(filePath));
26
+ }
27
+
28
+ function copyRecursive(src: string, dest: string): void {
29
+ const stat = fs.statSync(src);
30
+ if (stat.isDirectory()) {
31
+ ensureDir(dest);
32
+ const entries = fs.readdirSync(src, { withFileTypes: true });
33
+ for (const entry of entries) {
34
+ const s = path.join(src, entry.name);
35
+ const d = path.join(dest, entry.name);
36
+ if (entry.isDirectory()) {
37
+ copyRecursive(s, d);
38
+ } else if (entry.isFile()) {
39
+ ensureParentDir(d);
40
+ fs.copyFileSync(s, d);
41
+ }
42
+ }
43
+ return;
44
+ }
45
+ ensureParentDir(dest);
46
+ fs.copyFileSync(src, dest);
47
+ }
48
+
49
+ function backupPath(backupRoot: string, relativePath: string): string {
50
+ const normalized = relativePath.replace(/^\/+/, '');
51
+ return path.join(backupRoot, normalized);
52
+ }
53
+
54
+ function backupIfExists(src: string, backupRoot: string): void {
55
+ if (!fs.existsSync(src)) {
56
+ return;
57
+ }
58
+ copyRecursive(src, backupPath(backupRoot, src));
59
+ }
60
+
61
+ function findTrackDir(identifier: string): { kind: 'track' | 'archive'; dir: string; id: string } | null {
62
+ const trackDir = path.join(TRACKS_DIR, identifier);
63
+ if (fs.existsSync(trackDir) && fs.statSync(trackDir).isDirectory()) {
64
+ return { kind: 'track', dir: trackDir, id: identifier };
65
+ }
66
+
67
+ // Identifier may be an archive id (YYYY-MM-DD-<track-id>) or a track id inside archive.
68
+ if (!fs.existsSync(ARCHIVE_DIR)) {
69
+ return null;
70
+ }
71
+
72
+ const archiveCandidates: Array<{ id: string; dir: string }> = [];
73
+ const visitArchiveDir = (dir: string, depth: number): void => {
74
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
75
+ for (const entry of entries) {
76
+ if (!entry.isDirectory()) {
77
+ continue;
78
+ }
79
+
80
+ const entryDir = path.join(dir, entry.name);
81
+ const name = entry.name;
82
+ if (fs.existsSync(path.join(entryDir, 'plan.xml')) && isArchiveIdMatch(identifier, name)) {
83
+ archiveCandidates.push({ id: name, dir: entryDir });
84
+ continue;
85
+ }
86
+
87
+ // New archive layout nests archive IDs one level under YYYY-MM buckets.
88
+ if (depth < 1 && /^\d{4}-\d{2}$/.test(name)) {
89
+ visitArchiveDir(entryDir, depth + 1);
90
+ }
91
+ }
92
+ };
93
+
94
+ visitArchiveDir(ARCHIVE_DIR, 0);
95
+
96
+ if (archiveCandidates.length === 1) {
97
+ const archive = archiveCandidates[0];
98
+ return { kind: 'archive', dir: archive.dir, id: archive.id };
99
+ }
100
+
101
+ return null;
102
+ }
103
+
104
+ function isArchiveIdMatch(identifier: string, name: string): boolean {
105
+ return name === identifier || name.endsWith(`-${identifier}`);
106
+ }
107
+
108
+ function escapeXmlText(text: string): string {
109
+ return text
110
+ .replace(/&/g, '&amp;')
111
+ .replace(/</g, '&lt;')
112
+ .replace(/>/g, '&gt;');
113
+ }
114
+
115
+ function ensureExecutionMode(content: string, mode: ExecutionMode): string {
116
+ if (content.match(/<execution_mode>[^<]*<\/execution_mode>/)) {
117
+ return content.replace(/<execution_mode>[^<]*<\/execution_mode>/, `<execution_mode>${mode}</execution_mode>`);
118
+ }
119
+
120
+ // Insert under <metadata> (preferred: after <commit_mode> if present)
121
+ const commitModeIdx = content.indexOf('</commit_mode>');
122
+ if (commitModeIdx !== -1) {
123
+ const insertAt = commitModeIdx + '</commit_mode>'.length;
124
+ return content.slice(0, insertAt) + `\n <execution_mode>${mode}</execution_mode>` + content.slice(insertAt);
125
+ }
126
+
127
+ const metadataEndIdx = content.indexOf('</metadata>');
128
+ if (metadataEndIdx !== -1) {
129
+ return content.slice(0, metadataEndIdx) + ` <execution_mode>${mode}</execution_mode>\n` + content.slice(metadataEndIdx);
130
+ }
131
+
132
+ return content;
133
+ }
134
+
135
+ function stripDependenciesTag(taskInner: string): { inner: string; dependencies: string[] } {
136
+ const deps: string[] = [];
137
+ const depRegex = /<dependencies>([\s\S]*?)<\/dependencies>/g;
138
+ let match;
139
+ while ((match = depRegex.exec(taskInner)) !== null) {
140
+ const raw = match[1].trim();
141
+ if (raw) {
142
+ deps.push(...raw.split(',').map((s) => s.trim()).filter(Boolean));
143
+ }
144
+ }
145
+ const without = taskInner.replace(depRegex, '');
146
+ return { inner: without, dependencies: deps };
147
+ }
148
+
149
+ function wrapTaskDescriptionIfNeeded(taskInner: string): string {
150
+ if (taskInner.includes('<description>')) {
151
+ // If a <description> already exists, strip any leading mixed text before the first tag.
152
+ const firstTagIdx = taskInner.indexOf('<');
153
+ if (firstTagIdx <= 0) {
154
+ return taskInner;
155
+ }
156
+ const prefix = taskInner.slice(0, firstTagIdx);
157
+ if (prefix.trim().length === 0) {
158
+ return taskInner.slice(firstTagIdx);
159
+ }
160
+ return taskInner.slice(firstTagIdx);
161
+ }
162
+
163
+ const firstTagIdx = taskInner.indexOf('<');
164
+ const prefix = firstTagIdx === -1 ? taskInner : taskInner.slice(0, firstTagIdx);
165
+ const rest = firstTagIdx === -1 ? '' : taskInner.slice(firstTagIdx);
166
+ const descText = prefix.trim();
167
+ if (!descText) {
168
+ return taskInner;
169
+ }
170
+
171
+ // Use indentation of the next tag if available
172
+ const indentMatch = rest.match(/\n(\s*)</);
173
+ const indent = indentMatch ? indentMatch[1] : ' ';
174
+
175
+ const descLine = `\n${indent}<description>${escapeXmlText(descText)}</description>\n`;
176
+ return descLine + rest;
177
+ }
178
+
179
+ function upgradePhaseToWaves(phaseId: string, phaseInner: string): { phaseInner: string } {
180
+ if (phaseInner.includes('<waves>')) {
181
+ return { phaseInner };
182
+ }
183
+
184
+ // Parse tasks within this phase
185
+ const taskRegex = /<task\s+([^>]+)>([\s\S]*?)<\/task>/g;
186
+ type TaskInfo = {
187
+ attrs: string;
188
+ inner: string;
189
+ taskId: string;
190
+ dependencies: string[];
191
+ };
192
+ const tasks: TaskInfo[] = [];
193
+
194
+ let match;
195
+ while ((match = taskRegex.exec(phaseInner)) !== null) {
196
+ const attrs = match[1];
197
+ const inner = match[2];
198
+ const taskId = attrs.match(/id="([^"]+)"/)?.[1] ?? '';
199
+ const { inner: withoutDeps, dependencies } = stripDependenciesTag(inner);
200
+ tasks.push({ attrs, inner: withoutDeps, taskId, dependencies });
201
+ }
202
+
203
+ // If no tasks, nothing to do
204
+ if (tasks.length === 0) {
205
+ return { phaseInner };
206
+ }
207
+
208
+ // One wave per task, deterministic order
209
+ const taskToWave = new Map<string, string>();
210
+ tasks.forEach((t, idx) => {
211
+ const seq = String(idx + 1).padStart(2, '0');
212
+ taskToWave.set(t.taskId, `WAVE-${phaseId}-${seq}`);
213
+ });
214
+
215
+ const waveLines: string[] = [];
216
+ for (const t of tasks) {
217
+ const waveId = taskToWave.get(t.taskId) || `WAVE-${phaseId}-01`;
218
+ const depWaves = t.dependencies
219
+ .map((depTaskId) => taskToWave.get(depTaskId))
220
+ .filter((w): w is string => Boolean(w));
221
+ const dependsAttr = depWaves.length > 0 ? ` depends_on="${depWaves.join(',')}"` : ' depends_on=""';
222
+ waveLines.push(` <wave id="${waveId}"${dependsAttr} />`);
223
+ }
224
+
225
+ const wavesBlock = `\n <waves>\n${waveLines.join('\n')}\n </waves>\n`;
226
+
227
+ // Insert waves after </context_files> if present, else after </goal>
228
+ const ctxEndIdx = phaseInner.indexOf('</context_files>');
229
+ if (ctxEndIdx !== -1) {
230
+ const insertAt = ctxEndIdx + '</context_files>'.length;
231
+ phaseInner = phaseInner.slice(0, insertAt) + wavesBlock + phaseInner.slice(insertAt);
232
+ } else {
233
+ const goalEndIdx = phaseInner.indexOf('</goal>');
234
+ if (goalEndIdx !== -1) {
235
+ const insertAt = goalEndIdx + '</goal>'.length;
236
+ phaseInner = phaseInner.slice(0, insertAt) + wavesBlock + phaseInner.slice(insertAt);
237
+ } else {
238
+ phaseInner = wavesBlock + phaseInner;
239
+ }
240
+ }
241
+
242
+ // Rewrite tasks: add wave attr + wrap description + ensure dependencies removed
243
+ phaseInner = phaseInner.replace(taskRegex, (_full, attrs: string, inner: string) => {
244
+ const taskId = attrs.match(/id="([^"]+)"/)?.[1] ?? '';
245
+ const waveId = taskToWave.get(taskId);
246
+ let newAttrs = attrs;
247
+ if (waveId && !newAttrs.includes('wave=')) {
248
+ newAttrs = `${newAttrs} wave="${waveId}"`;
249
+ }
250
+
251
+ const { inner: withoutDeps } = stripDependenciesTag(inner);
252
+ const wrapped = wrapTaskDescriptionIfNeeded(withoutDeps);
253
+ return `<task ${newAttrs}>${wrapped}</task>`;
254
+ });
255
+
256
+ return { phaseInner };
257
+ }
258
+
259
+ function upgradePlanXml(original: string, mode: ExecutionMode): string {
260
+ let content = original;
261
+
262
+ // Convert references -> context_files (best-effort)
263
+ content = content
264
+ .replace(/<references>/g, '<context_files>')
265
+ .replace(/<\/references>/g, '</context_files>')
266
+ .replace(/<reference>/g, '<file>')
267
+ .replace(/<\/reference>/g, '</file>');
268
+
269
+ // Ensure execution_mode
270
+ content = ensureExecutionMode(content, mode);
271
+
272
+ // Phase-level upgrade
273
+ const phaseRegex = /<phase\s+id="([^"]+)"\s+name="([^"]+)"(?:\s+milestone="([^"]+)")?[^>]*>([\s\S]*?)<\/phase>/g;
274
+ content = content.replace(phaseRegex, (full, phaseId: string, name: string, milestone: string | undefined, inner: string) => {
275
+ void name;
276
+ void milestone;
277
+ if (mode !== 'wave') {
278
+ // Still remove <dependencies> and wrap <description> in sequential mode
279
+ const taskRegex = /<task\s+([^>]+)>([\s\S]*?)<\/task>/g;
280
+ const upgradedInner = inner.replace(taskRegex, (_tFull, attrs: string, taskInner: string) => {
281
+ const { inner: withoutDeps } = stripDependenciesTag(taskInner);
282
+ const wrapped = wrapTaskDescriptionIfNeeded(withoutDeps);
283
+ return `<task ${attrs}>${wrapped}</task>`;
284
+ });
285
+ return full.replace(inner, upgradedInner);
286
+ }
287
+
288
+ const { phaseInner } = upgradePhaseToWaves(phaseId, inner);
289
+ return full.replace(inner, phaseInner);
290
+ });
291
+
292
+ // As a final pass, strip any remaining <dependencies>
293
+ content = content.replace(/\s*<dependencies>[\s\S]*?<\/dependencies>\s*/g, '\n');
294
+
295
+ return content;
296
+ }
297
+
298
+ function ensureWaveSupportFiles(trackDir: string, waveMode: boolean): void {
299
+ if (!waveMode) {
300
+ return;
301
+ }
302
+ const contextPath = path.join(trackDir, 'context.md');
303
+ const statePath = path.join(trackDir, 'state.md');
304
+ if (!fs.existsSync(contextPath)) {
305
+ fs.writeFileSync(contextPath, '# Context\n', 'utf-8');
306
+ }
307
+ if (!fs.existsSync(statePath)) {
308
+ fs.writeFileSync(statePath, '# State\n', 'utf-8');
309
+ }
310
+ ensureDir(path.join(trackDir, 'phases'));
311
+ ensureDir(path.join(trackDir, 'waves'));
312
+ }
313
+
314
+ export async function upgradeTrackCommand(args: string[]): Promise<void> {
315
+ if (!codumentExists()) {
316
+ console.error('Codument is not initialized. Run codument init first.');
317
+ process.exit(1);
318
+ }
319
+
320
+ const { positional, options } = parseOptions(args);
321
+ const identifier = positional[0];
322
+ if (!identifier) {
323
+ console.error('Please specify a track-id or archive-id.');
324
+ console.log('Usage: codument upgrade-track <track-id|archive-id> [--mode wave|sequential] [--backup-dir <path>] [--no-backup]');
325
+ process.exit(1);
326
+ }
327
+
328
+ const mode = (typeof options['mode'] === 'string' ? String(options['mode']) : 'wave') as ExecutionMode;
329
+ if (mode !== 'wave' && mode !== 'sequential') {
330
+ console.error(`Invalid --mode: ${mode}`);
331
+ process.exit(1);
332
+ }
333
+
334
+ const noBackup = options['no-backup'] === true;
335
+ const backupRoot = typeof options['backup-dir'] === 'string'
336
+ ? path.resolve(String(options['backup-dir']))
337
+ : path.join('.tmp', 'codument', `upgrade-track-${safeTimestamp()}`);
338
+
339
+ const found = findTrackDir(identifier);
340
+ if (!found) {
341
+ console.error(`Track not found: ${identifier}`);
342
+ console.log('Searched:');
343
+ console.log(`- ${path.join(TRACKS_DIR, identifier)}`);
344
+ console.log(`- ${path.join(ARCHIVE_DIR, `*-${identifier}`)}`);
345
+ process.exit(1);
346
+ }
347
+
348
+ const trackDir = found.dir;
349
+ const planPath = path.join(trackDir, 'plan.xml');
350
+ if (!fs.existsSync(planPath)) {
351
+ console.error(`plan.xml not found in: ${trackDir}`);
352
+ process.exit(1);
353
+ }
354
+
355
+ console.log('🔧 Codument Upgrade Track');
356
+ console.log(`Workspace: ${process.cwd()}`);
357
+ console.log(`Target: ${trackDir}`);
358
+ console.log(`Mode: ${mode}`);
359
+ if (!noBackup) {
360
+ console.log(`Backup: ${backupRoot}`);
361
+ }
362
+ console.log('');
363
+
364
+ if (!noBackup) {
365
+ // Backup the entire track directory for rollback
366
+ backupIfExists(trackDir, backupRoot);
367
+ }
368
+
369
+ const original = fs.readFileSync(planPath, 'utf-8');
370
+ const upgraded = upgradePlanXml(original, mode);
371
+ fs.writeFileSync(planPath, upgraded, 'utf-8');
372
+ console.log('✓ Updated plan.xml');
373
+
374
+ ensureWaveSupportFiles(trackDir, mode === 'wave');
375
+ if (mode === 'wave') {
376
+ console.log('✓ Ensured wave support files (context.md, state.md, phases/, waves/)');
377
+ }
378
+
379
+ console.log('');
380
+ if (!noBackup) {
381
+ console.log(`Done. Backup saved at: ${backupRoot}`);
382
+ } else {
383
+ console.log('Done. (no backup)');
384
+ }
385
+ }
@@ -0,0 +1,138 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ import { parseOptions, codumentExists } from '../utils';
5
+ import {
6
+ installSkillTemplates,
7
+ installTemplates,
8
+ ensureCodumentGitignoreRules,
9
+ injectAgentsBlock,
10
+ parseAgents,
11
+ readCliToolsConfig,
12
+ resolveSkillsTargets,
13
+ writeCliToolsConfig,
14
+ type CLITool,
15
+ } from '../utils/install';
16
+
17
+ /**
18
+ * `codument upgrade-workspace` — refresh the embedded templates in place.
19
+ *
20
+ * Pure text copy: overwrites the managed codument/std/** subtree and the agent
21
+ * skill shells with the latest embedded templates; leaves user-owned files
22
+ * (attractors content, config values, tracks, behaviors, backlog/memory) intact.
23
+ * Creates a timestamped backup under .tmp/codument/ before touching workspace
24
+ * files. No per-agent generators, no interactive prompts.
25
+ *
26
+ * Options: same --agent / --skills-dir as `init`.
27
+ */
28
+ export async function upgradeWorkspaceCommand(args: string[]): Promise<void> {
29
+ if (!codumentExists()) {
30
+ console.error('Codument is not initialized. Run codument init first.');
31
+ process.exit(1);
32
+ }
33
+
34
+ const { options } = parseOptions(args);
35
+ const hasExplicitAgent = options['agent'] !== undefined;
36
+ const hasExplicitSkillsDir = options['skills-dir'] !== undefined;
37
+ const backupRoot = createWorkspaceBackup();
38
+ const stateTools = readCliToolsConfig();
39
+ const fallbackTools: CLITool[] = stateTools.length > 0 ? stateTools : ['claude'];
40
+ const selectedTools = hasExplicitAgent
41
+ ? parseAgents(typeof options['agent'] === 'string' ? String(options['agent']) : undefined, fallbackTools)
42
+ : fallbackTools;
43
+ const shouldWriteCliToolsConfig = stateTools.length > 0 || hasExplicitAgent;
44
+ if (shouldWriteCliToolsConfig) {
45
+ writeCliToolsConfig(selectedTools);
46
+ }
47
+ const removedLegacyPaths = removeLegacyWorkspacePaths(backupRoot);
48
+ const targets = resolveSkillsTargets(options, selectedTools);
49
+ const [firstTarget, ...additionalTargets] = targets;
50
+
51
+ const result = installTemplates({ skillsDir: firstTarget.skillsDir, overwriteStd: true });
52
+ const skillResults = [{ ...firstTarget, skillsWritten: result.skillsWritten, skillsRemoved: result.skillsRemoved }];
53
+ for (const target of additionalTargets) {
54
+ skillResults.push({ ...target, ...installSkillTemplates(target.skillsDir) });
55
+ }
56
+ injectAgentsBlock();
57
+ const gitignoreRulesAdded = ensureCodumentGitignoreRules();
58
+
59
+ console.log('Codument workspace upgraded.');
60
+ console.log(` backup : ${backupRoot}`);
61
+ console.log(` codument/ : ${result.workspaceWritten} written (std refreshed), ${result.workspaceSkipped} kept`);
62
+ for (const skillResult of skillResults) {
63
+ const removed = skillResult.skillsRemoved ? `, ${skillResult.skillsRemoved} deprecated removed` : '';
64
+ console.log(` skills : ${skillResult.skillsWritten} → ${skillResult.skillsDir} (agent: ${skillResult.agent}${removed})`);
65
+ }
66
+ if (shouldWriteCliToolsConfig) {
67
+ console.log(' config/cli-tools.json: tools updated');
68
+ }
69
+ if (removedLegacyPaths > 0) {
70
+ console.log(` cleanup : ${removedLegacyPaths} legacy path(s) removed`);
71
+ }
72
+ if (gitignoreRulesAdded > 0) {
73
+ console.log(` .gitignore: ${gitignoreRulesAdded} codument rule(s) added`);
74
+ }
75
+ console.log(' AGENTS.md : managed block refreshed');
76
+ }
77
+
78
+ function safeTimestamp(): string {
79
+ return new Date().toISOString().replace(/[:.]/g, '-');
80
+ }
81
+
82
+ function copyRecursive(src: string, dest: string): void {
83
+ if (!fs.existsSync(src)) {
84
+ return;
85
+ }
86
+ const stat = fs.statSync(src);
87
+ if (stat.isDirectory()) {
88
+ fs.mkdirSync(dest, { recursive: true });
89
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
90
+ copyRecursive(path.join(src, entry.name), path.join(dest, entry.name));
91
+ }
92
+ return;
93
+ }
94
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
95
+ fs.copyFileSync(src, dest);
96
+ }
97
+
98
+ function createWorkspaceBackup(): string {
99
+ const backupRoot = path.join('.tmp', 'codument', `upgrade-workspace-${safeTimestamp()}`);
100
+ const paths = ['codument', 'AGENTS.md'];
101
+ for (const source of paths) {
102
+ if (fs.existsSync(source)) {
103
+ copyRecursive(source, path.join(backupRoot, source));
104
+ }
105
+ }
106
+ return backupRoot;
107
+ }
108
+
109
+ function removeLegacyWorkspacePaths(backupRoot: string): number {
110
+ const legacyPaths = [
111
+ 'codument/state.json',
112
+ 'codument/config/feature.json',
113
+ 'codument/workflows/workflow.md',
114
+ 'codument/workflows/bun-dev-cmds.md',
115
+ 'codument/legacy',
116
+ 'codument/specs',
117
+ 'codument/std/workflow.md',
118
+ 'codument/std/protocols.md',
119
+ 'codument/std/operations/init.md',
120
+ 'codument/std/operations/status.md',
121
+ 'codument/std/plan-xml-spec.md',
122
+ 'codument/std/track-impl-gap-report-1.md',
123
+ 'codument/attractors/knowledge-tiers.md',
124
+ 'codument/attractors/model-driven-docs.md',
125
+ 'codument/attractors/project-memory.md',
126
+ ];
127
+
128
+ let removed = 0;
129
+ for (const legacyPath of legacyPaths) {
130
+ if (!fs.existsSync(legacyPath)) {
131
+ continue;
132
+ }
133
+ copyRecursive(legacyPath, path.join(backupRoot, legacyPath));
134
+ fs.rmSync(legacyPath, { recursive: true, force: true });
135
+ removed++;
136
+ }
137
+ return removed;
138
+ }