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.
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/package.json +63 -0
- package/src/cli/commands/archive.ts +519 -0
- package/src/cli/commands/decisions.ts +123 -0
- package/src/cli/commands/engineering.ts +105 -0
- package/src/cli/commands/init.ts +54 -0
- package/src/cli/commands/list.ts +73 -0
- package/src/cli/commands/modeling.ts +105 -0
- package/src/cli/commands/show.ts +238 -0
- package/src/cli/commands/status.ts +140 -0
- package/src/cli/commands/upgrade-track.ts +385 -0
- package/src/cli/commands/upgrade-workspace.ts +138 -0
- package/src/cli/commands/validate.ts +330 -0
- package/src/cli/engineering/config.ts +68 -0
- package/src/cli/engineering/lint.ts +58 -0
- package/src/cli/engineering/merge.ts +172 -0
- package/src/cli/engineering/registry.ts +230 -0
- package/src/cli/engineering/schema.ts +126 -0
- package/src/cli/engineering/validate.ts +286 -0
- package/src/cli/index.ts +136 -0
- package/src/cli/modeling/config.ts +68 -0
- package/src/cli/modeling/lint.ts +58 -0
- package/src/cli/modeling/merge.ts +172 -0
- package/src/cli/modeling/registry.ts +229 -0
- package/src/cli/modeling/schema.ts +160 -0
- package/src/cli/modeling/validate.ts +282 -0
- package/src/cli/utils/index.ts +941 -0
- package/src/cli/utils/install.ts +291 -0
- package/src/cli/utils/spec-xml.ts +673 -0
- package/src/cli/utils/track-time.ts +75 -0
- package/src/cli/utils/vfs.ts +102 -0
- package/src/templates/codument/README.md +59 -0
- package/src/templates/codument/attractors/product.md +17 -0
- package/src/templates/codument/attractors/project.md +10 -0
- package/src/templates/codument/backlog/README.md +33 -0
- package/src/templates/codument/config/attractor-profiles.xml +31 -0
- package/src/templates/codument/config/engineering.xml +22 -0
- package/src/templates/codument/config/modeling.xml +22 -0
- package/src/templates/codument/config/operation-hooks.xml +55 -0
- package/src/templates/codument/memory/README.md +13 -0
- package/src/templates/codument/missions/README.md +125 -0
- package/src/templates/codument/sop/README.md +14 -0
- package/src/templates/codument/std/AGENTS.md +82 -0
- package/src/templates/codument/std/attractors/depa-attractor.md +572 -0
- package/src/templates/codument/std/attractors/knowledge-tiers.md +128 -0
- package/src/templates/codument/std/attractors/model-driven-docs.md +293 -0
- package/src/templates/codument/std/attractors/project-memory.md +48 -0
- package/src/templates/codument/std/docs-impl-fractal/index.md +110 -0
- package/src/templates/codument/std/docs-modeling-fractal/index.md +156 -0
- package/src/templates/codument/std/kernel-pointer.md +19 -0
- package/src/templates/codument/std/operations/README.md +30 -0
- package/src/templates/codument/std/operations/_operation-spec.md +41 -0
- package/src/templates/codument/std/operations/archive-mission.md +66 -0
- package/src/templates/codument/std/operations/archive-track.md +238 -0
- package/src/templates/codument/std/operations/artifact-sync.md +172 -0
- package/src/templates/codument/std/operations/discuss-phase.md +214 -0
- package/src/templates/codument/std/operations/discuss.md +87 -0
- package/src/templates/codument/std/operations/docs-bootstrap.md +148 -0
- package/src/templates/codument/std/operations/gap-loop.md +301 -0
- package/src/templates/codument/std/operations/impl-mission.md +167 -0
- package/src/templates/codument/std/operations/impl-quick.md +79 -0
- package/src/templates/codument/std/operations/impl-track.md +537 -0
- package/src/templates/codument/std/operations/migrate.md +337 -0
- package/src/templates/codument/std/operations/plan-mission.md +230 -0
- package/src/templates/codument/std/operations/plan-track-wave.md +231 -0
- package/src/templates/codument/std/operations/plan-track.md +579 -0
- package/src/templates/codument/std/operations/revise-track.md +136 -0
- package/src/templates/codument/std/operations/validate.md +339 -0
- package/src/templates/codument/std/operations/verify.md +184 -0
- package/src/templates/codument/std/root-agents.md +39 -0
- package/src/templates/codument/std/sop/questioning.md +98 -0
- package/src/templates/codument/std/sop/tdd.md +26 -0
- package/src/templates/codument/std/sop/validation.md +25 -0
- package/src/templates/codument/std/sop/wave-exec.md +42 -0
- package/src/templates/codument/std/sop/workflow.md +35 -0
- package/src/templates/codument/std/spec/behavior-delta.md +36 -0
- package/src/templates/codument/std/spec/behavior-registry.md +42 -0
- package/src/templates/codument/std/spec/engineering-delta.md +68 -0
- package/src/templates/codument/std/spec/engineering-node-schema.md +86 -0
- package/src/templates/codument/std/spec/engineering-registry.md +82 -0
- package/src/templates/codument/std/spec/flow-notation.md +93 -0
- package/src/templates/codument/std/spec/folder-manifest.md +99 -0
- package/src/templates/codument/std/spec/mission-xml-spec.md +249 -0
- package/src/templates/codument/std/spec/modeling-delta.md +85 -0
- package/src/templates/codument/std/spec/modeling-node-schema.md +183 -0
- package/src/templates/codument/std/spec/modeling-registry.md +49 -0
- package/src/templates/codument/std/spec/track-xml-spec.md +272 -0
- package/src/templates/codument/std/spec/xnl-format.md +301 -0
- package/src/templates/codument/workflows/README.md +15 -0
- package/src/templates/manifest.ts +177 -0
- package/src/templates/skills/README.md +38 -0
- package/src/templates/skills/codument-archive/SKILL.md +17 -0
- package/src/templates/skills/codument-archive-mission/SKILL.md +17 -0
- package/src/templates/skills/codument-archive-track/SKILL.md +17 -0
- package/src/templates/skills/codument-artifact-sync/SKILL.md +17 -0
- package/src/templates/skills/codument-code-quality-score/SKILL.md +67 -0
- package/src/templates/skills/codument-decision-tree/SKILL.md +40 -0
- package/src/templates/skills/codument-discuss/SKILL.md +17 -0
- package/src/templates/skills/codument-discuss-phase/SKILL.md +17 -0
- package/src/templates/skills/codument-docs-bootstrap/SKILL.md +17 -0
- package/src/templates/skills/codument-gap-loop/SKILL.md +17 -0
- package/src/templates/skills/codument-impl-mission/SKILL.md +17 -0
- package/src/templates/skills/codument-impl-quick/SKILL.md +17 -0
- package/src/templates/skills/codument-impl-track/SKILL.md +17 -0
- package/src/templates/skills/codument-implement/SKILL.md +14 -0
- package/src/templates/skills/codument-migrate/SKILL.md +17 -0
- package/src/templates/skills/codument-modeling-engineering-e2e/SKILL.md +74 -0
- package/src/templates/skills/codument-plan-mission/SKILL.md +17 -0
- package/src/templates/skills/codument-plan-track/SKILL.md +17 -0
- package/src/templates/skills/codument-plan-track-wave/SKILL.md +17 -0
- package/src/templates/skills/codument-revise-track/SKILL.md +17 -0
- package/src/templates/skills/codument-track/SKILL.md +14 -0
- package/src/templates/skills/codument-validate/SKILL.md +17 -0
- package/src/templates/skills/codument-verify/SKILL.md +17 -0
- package/src/types/text-assets.d.ts +9 -0
- 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, '&')
|
|
111
|
+
.replace(/</g, '<')
|
|
112
|
+
.replace(/>/g, '>');
|
|
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
|
+
}
|