prjct-cli 0.12.2 → 0.13.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/CHANGELOG.md +43 -0
- package/CLAUDE.md +18 -6
- package/core/data/index.ts +19 -5
- package/core/data/md-base-manager.ts +203 -0
- package/core/data/md-queue-manager.ts +179 -0
- package/core/data/md-state-manager.ts +133 -0
- package/core/serializers/index.ts +20 -0
- package/core/serializers/queue-serializer.ts +210 -0
- package/core/serializers/state-serializer.ts +136 -0
- package/core/utils/file-helper.ts +12 -0
- package/package.json +1 -1
- package/packages/web/app/api/projects/[id]/stats/route.ts +6 -29
- package/packages/web/app/page.tsx +1 -6
- package/packages/web/app/project/[id]/page.tsx +34 -1
- package/packages/web/app/project/[id]/stats/page.tsx +11 -5
- package/packages/web/app/settings/page.tsx +2 -221
- package/packages/web/components/BlockersCard/BlockersCard.tsx +67 -0
- package/packages/web/components/BlockersCard/BlockersCard.types.ts +11 -0
- package/packages/web/components/BlockersCard/index.ts +2 -0
- package/packages/web/components/CommandButton/CommandButton.tsx +10 -3
- package/packages/web/lib/projects.ts +28 -27
- package/packages/web/lib/services/projects.server.ts +25 -21
- package/packages/web/lib/services/stats.server.ts +355 -57
- package/packages/web/next-env.d.ts +1 -1
- package/packages/web/package.json +0 -4
- package/templates/commands/decision.md +226 -0
- package/templates/commands/done.md +100 -68
- package/templates/commands/feature.md +102 -103
- package/templates/commands/idea.md +41 -38
- package/templates/commands/now.md +94 -33
- package/templates/commands/pause.md +90 -30
- package/templates/commands/ship.md +179 -74
- package/templates/commands/sync.md +324 -200
- package/packages/web/app/api/migrate/route.ts +0 -46
- package/packages/web/app/api/settings/route.ts +0 -97
- package/packages/web/app/api/v2/projects/[id]/unified/route.ts +0 -57
- package/packages/web/components/MigrationGate/MigrationGate.tsx +0 -304
- package/packages/web/components/MigrationGate/index.ts +0 -1
- package/packages/web/lib/json-loader.ts +0 -630
- package/packages/web/lib/services/migration.server.ts +0 -580
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.13.0] - 2025-12-10
|
|
4
|
+
|
|
5
|
+
### Deep Sync + Cleanup migrations
|
|
6
|
+
|
|
7
|
+
Major update: Deep git analysis, removed external API dependencies, MD-first architecture.
|
|
8
|
+
|
|
9
|
+
- **Deep Sync (`/p:sync`)**
|
|
10
|
+
- Full git analysis: `git status`, `git log`, `git diff`, `git branch`
|
|
11
|
+
- Auto-detect completed tasks from commit messages
|
|
12
|
+
- Sync ALL MD files: now.md, next.md, shipped.md, ideas.md, roadmap.md
|
|
13
|
+
- Update project.json with real stats (fileCount, commitCount, version)
|
|
14
|
+
- Update CLAUDE.md Quick Reference table with current data
|
|
15
|
+
- Infer current task from uncommitted changes
|
|
16
|
+
|
|
17
|
+
- **Sync Button in Project Detail**
|
|
18
|
+
- Prominent primary-colored button in sidebar
|
|
19
|
+
- Always visible for easy access
|
|
20
|
+
- Mobile support in command grid
|
|
21
|
+
|
|
22
|
+
- **Removed External Dependencies**
|
|
23
|
+
- Removed OpenAI/OpenRouter API key management
|
|
24
|
+
- Removed migration service (no longer needed)
|
|
25
|
+
- Removed settings API route
|
|
26
|
+
- Cleaned up web package dependencies
|
|
27
|
+
|
|
28
|
+
- **Ship Workflow Improvements**
|
|
29
|
+
- Language agnostic versioning (package.json, Cargo.toml, pyproject.toml, VERSION)
|
|
30
|
+
- CHANGELOG.md generation is now MANDATORY
|
|
31
|
+
- Auto-runs deep sync after shipping
|
|
32
|
+
- prjct signature in commits
|
|
33
|
+
|
|
34
|
+
- **Files Removed**:
|
|
35
|
+
- `packages/web/app/api/settings/route.ts`
|
|
36
|
+
- `packages/web/app/api/migrate/route.ts`
|
|
37
|
+
- `packages/web/lib/services/migration.server.ts`
|
|
38
|
+
- `packages/web/components/MigrationGate/`
|
|
39
|
+
|
|
40
|
+
- **Files Modified**:
|
|
41
|
+
- `templates/commands/sync.md` - Complete rewrite for deep analysis
|
|
42
|
+
- `templates/commands/ship.md` - Language agnostic, mandatory changelog
|
|
43
|
+
- `packages/web/app/settings/page.tsx` - Removed API keys section
|
|
44
|
+
- `packages/web/app/project/[id]/page.tsx` - Added Sync button
|
|
45
|
+
|
|
3
46
|
## [0.12.0] - 2025-12-10
|
|
4
47
|
|
|
5
48
|
### Added - JSON-First Architecture (Zero Data Loss)
|
package/CLAUDE.md
CHANGED
|
@@ -28,19 +28,31 @@ p. done → Mark complete, next task
|
|
|
28
28
|
p. ship → Lint/test/commit/push
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
## Architecture
|
|
31
|
+
## Architecture: MD-First
|
|
32
|
+
|
|
33
|
+
**MD files are the source of truth.** No JSON data files - all state is stored directly in Markdown.
|
|
32
34
|
|
|
33
35
|
### Global Storage
|
|
34
36
|
```
|
|
35
37
|
~/.prjct-cli/projects/{id}/
|
|
36
|
-
├── core/ # now.md, next.md
|
|
37
|
-
├── progress/ # shipped.md,
|
|
38
|
-
├── planning/ # ideas.md, roadmap.md
|
|
38
|
+
├── core/ # now.md (current task), next.md (queue)
|
|
39
|
+
├── progress/ # shipped.md, sessions/
|
|
40
|
+
├── planning/ # ideas.md, roadmap.md
|
|
39
41
|
├── analysis/ # repo-summary.md
|
|
40
|
-
├── memory/ # context.jsonl
|
|
41
|
-
|
|
42
|
+
├── memory/ # context.jsonl (append-only logs)
|
|
43
|
+
├── agents/ # Generated specialists
|
|
44
|
+
└── project.json # Metadata only (name, repoPath, techStack)
|
|
42
45
|
```
|
|
43
46
|
|
|
47
|
+
### Key Files (Source of Truth)
|
|
48
|
+
| File | Purpose |
|
|
49
|
+
|------|---------|
|
|
50
|
+
| `core/now.md` | Current task state |
|
|
51
|
+
| `core/next.md` | Task queue |
|
|
52
|
+
| `planning/ideas.md` | Ideas backlog |
|
|
53
|
+
| `planning/roadmap.md` | Feature roadmap |
|
|
54
|
+
| `progress/shipped.md` | Shipped features |
|
|
55
|
+
|
|
44
56
|
### Local Config
|
|
45
57
|
```
|
|
46
58
|
.prjct/prjct.config.json # Links to global data via projectId
|
package/core/data/index.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Data Module
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* MD-First Architecture: MD files are the source of truth.
|
|
5
|
+
* JSON managers are deprecated in favor of MD managers.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
// Base
|
|
8
|
+
// Base (legacy JSON)
|
|
8
9
|
export { BaseManager, ArrayManager } from './base-manager'
|
|
9
10
|
|
|
10
|
-
//
|
|
11
|
+
// MD-First Base
|
|
12
|
+
export { MdBaseManager, MdArrayManager } from './md-base-manager'
|
|
13
|
+
|
|
14
|
+
// MD-First Managers (NEW - use these!)
|
|
15
|
+
export { mdStateManager } from './md-state-manager'
|
|
16
|
+
export { mdQueueManager } from './md-queue-manager'
|
|
17
|
+
|
|
18
|
+
// Legacy JSON Managers (deprecated - for backwards compatibility only)
|
|
11
19
|
export { stateManager, default as stateManagerDefault } from './state-manager'
|
|
12
20
|
export { projectManager, default as projectManagerDefault } from './project-manager'
|
|
13
21
|
export { agentsManager, default as agentsManagerDefault } from './agents-manager'
|
|
@@ -17,7 +25,13 @@ export { shippedManager, default as shippedManagerDefault } from './shipped-mana
|
|
|
17
25
|
export { analysisManager, default as analysisManagerDefault } from './analysis-manager'
|
|
18
26
|
export { outcomesManager, default as outcomesManagerDefault } from './outcomes-manager'
|
|
19
27
|
|
|
20
|
-
//
|
|
28
|
+
// MD-First managers (preferred)
|
|
29
|
+
export const mdManagers = {
|
|
30
|
+
state: require('./md-state-manager').mdStateManager,
|
|
31
|
+
queue: require('./md-queue-manager').mdQueueManager
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Legacy JSON managers (deprecated)
|
|
21
35
|
export const dataManagers = {
|
|
22
36
|
state: require('./state-manager').stateManager,
|
|
23
37
|
project: require('./project-manager').projectManager,
|
|
@@ -29,4 +43,4 @@ export const dataManagers = {
|
|
|
29
43
|
outcomes: require('./outcomes-manager').outcomesManager
|
|
30
44
|
}
|
|
31
45
|
|
|
32
|
-
export default
|
|
46
|
+
export default mdManagers
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MD Base Manager
|
|
3
|
+
*
|
|
4
|
+
* Abstract base class for MD file managers.
|
|
5
|
+
* MD-First Architecture: MD is the source of truth.
|
|
6
|
+
*
|
|
7
|
+
* Each concrete manager must implement:
|
|
8
|
+
* - parse(content: string): T - Convert MD to schema
|
|
9
|
+
* - serialize(data: T): string - Convert schema to MD
|
|
10
|
+
* - getDefault(projectId: string): T - Default value when file doesn't exist
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import path from 'path'
|
|
14
|
+
import fs from 'fs/promises'
|
|
15
|
+
import * as fileHelper from '../utils/file-helper'
|
|
16
|
+
import pathManager from '../infrastructure/path-manager'
|
|
17
|
+
|
|
18
|
+
export abstract class MdBaseManager<T> {
|
|
19
|
+
protected filename: string
|
|
20
|
+
protected cache: Map<string, T> = new Map()
|
|
21
|
+
protected cacheTimeout = 5000 // 5 seconds
|
|
22
|
+
protected lastRead: Map<string, number> = new Map()
|
|
23
|
+
|
|
24
|
+
constructor(filename: string) {
|
|
25
|
+
this.filename = filename
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get file path for a project.
|
|
30
|
+
*/
|
|
31
|
+
protected getFilePath(projectId: string): string {
|
|
32
|
+
const globalPath = pathManager.getGlobalProjectPath(projectId)
|
|
33
|
+
return path.join(globalPath, this.filename)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get default value for this data type.
|
|
38
|
+
*/
|
|
39
|
+
protected abstract getDefault(projectId: string): T
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse MD content to schema.
|
|
43
|
+
*/
|
|
44
|
+
protected abstract parse(content: string): T
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Serialize schema to MD content.
|
|
48
|
+
*/
|
|
49
|
+
protected abstract serialize(data: T): string
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Read data from MD file.
|
|
53
|
+
*/
|
|
54
|
+
async read(projectId: string): Promise<T> {
|
|
55
|
+
const now = Date.now()
|
|
56
|
+
const lastReadTime = this.lastRead.get(projectId) || 0
|
|
57
|
+
|
|
58
|
+
// Return cached if fresh
|
|
59
|
+
if (now - lastReadTime < this.cacheTimeout && this.cache.has(projectId)) {
|
|
60
|
+
return this.cache.get(projectId)!
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const filePath = this.getFilePath(projectId)
|
|
64
|
+
const content = await fileHelper.readFile(filePath, '')
|
|
65
|
+
|
|
66
|
+
const data = content.trim() ? this.parse(content) : this.getDefault(projectId)
|
|
67
|
+
|
|
68
|
+
// Update cache
|
|
69
|
+
this.cache.set(projectId, data)
|
|
70
|
+
this.lastRead.set(projectId, now)
|
|
71
|
+
|
|
72
|
+
return data
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Write data to MD file using atomic write (prevents partial writes).
|
|
77
|
+
*/
|
|
78
|
+
async write(projectId: string, data: T): Promise<void> {
|
|
79
|
+
const filePath = this.getFilePath(projectId)
|
|
80
|
+
const content = this.serialize(data)
|
|
81
|
+
|
|
82
|
+
// Ensure directory exists
|
|
83
|
+
await fileHelper.ensureDir(path.dirname(filePath))
|
|
84
|
+
|
|
85
|
+
// Atomic write: write to temp file, then rename
|
|
86
|
+
const tempPath = `${filePath}.${Date.now()}.tmp`
|
|
87
|
+
await fs.writeFile(tempPath, content, 'utf-8')
|
|
88
|
+
await fs.rename(tempPath, filePath)
|
|
89
|
+
|
|
90
|
+
// Update cache
|
|
91
|
+
this.cache.set(projectId, data)
|
|
92
|
+
this.lastRead.set(projectId, Date.now())
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if file exists.
|
|
97
|
+
*/
|
|
98
|
+
async exists(projectId: string): Promise<boolean> {
|
|
99
|
+
const filePath = this.getFilePath(projectId)
|
|
100
|
+
return fileHelper.fileExists(filePath)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Initialize with default data.
|
|
105
|
+
*/
|
|
106
|
+
async initialize(projectId: string): Promise<T> {
|
|
107
|
+
const data = this.getDefault(projectId)
|
|
108
|
+
await this.write(projectId, data)
|
|
109
|
+
return data
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Clear cache.
|
|
114
|
+
*/
|
|
115
|
+
clearCache(projectId?: string): void {
|
|
116
|
+
if (projectId) {
|
|
117
|
+
this.cache.delete(projectId)
|
|
118
|
+
this.lastRead.delete(projectId)
|
|
119
|
+
} else {
|
|
120
|
+
this.cache.clear()
|
|
121
|
+
this.lastRead.clear()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Update data with an updater function (read-modify-write).
|
|
127
|
+
*/
|
|
128
|
+
async update(projectId: string, updater: (data: T) => T): Promise<T> {
|
|
129
|
+
const current = await this.read(projectId)
|
|
130
|
+
const updated = updater(current)
|
|
131
|
+
await this.write(projectId, updated)
|
|
132
|
+
return updated
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get raw MD content without parsing.
|
|
137
|
+
*/
|
|
138
|
+
async readRaw(projectId: string): Promise<string> {
|
|
139
|
+
const filePath = this.getFilePath(projectId)
|
|
140
|
+
return fileHelper.readFile(filePath, '')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Write raw MD content without serialization.
|
|
145
|
+
*/
|
|
146
|
+
async writeRaw(projectId: string, content: string): Promise<void> {
|
|
147
|
+
const filePath = this.getFilePath(projectId)
|
|
148
|
+
await fileHelper.ensureDir(path.dirname(filePath))
|
|
149
|
+
await fileHelper.atomicWrite(filePath, content)
|
|
150
|
+
this.clearCache(projectId)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Base manager for array-based MD files (like shipped, ideas).
|
|
156
|
+
*/
|
|
157
|
+
export abstract class MdArrayManager<T> extends MdBaseManager<T[]> {
|
|
158
|
+
protected getDefault(): T[] {
|
|
159
|
+
return []
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Add item to array.
|
|
164
|
+
*/
|
|
165
|
+
async add(projectId: string, item: T): Promise<T[]> {
|
|
166
|
+
return this.update(projectId, (data) => [...data, item])
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Prepend item to array (add at beginning).
|
|
171
|
+
*/
|
|
172
|
+
async prepend(projectId: string, item: T): Promise<T[]> {
|
|
173
|
+
return this.update(projectId, (data) => [item, ...data])
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Remove item by predicate.
|
|
178
|
+
*/
|
|
179
|
+
async remove(projectId: string, predicate: (item: T) => boolean): Promise<T[]> {
|
|
180
|
+
return this.update(projectId, (data) => data.filter((item) => !predicate(item)))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Find item by predicate.
|
|
185
|
+
*/
|
|
186
|
+
async find(projectId: string, predicate: (item: T) => boolean): Promise<T | undefined> {
|
|
187
|
+
const data = await this.read(projectId)
|
|
188
|
+
return data.find(predicate)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Update item by predicate.
|
|
193
|
+
*/
|
|
194
|
+
async updateItem(
|
|
195
|
+
projectId: string,
|
|
196
|
+
predicate: (item: T) => boolean,
|
|
197
|
+
updater: (item: T) => T
|
|
198
|
+
): Promise<T[]> {
|
|
199
|
+
return this.update(projectId, (data) =>
|
|
200
|
+
data.map((item) => (predicate(item) ? updater(item) : item))
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MD Queue Manager
|
|
3
|
+
*
|
|
4
|
+
* MD-First Architecture: Manages queue via next.md.
|
|
5
|
+
* Source of truth is the markdown file, not JSON.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { MdBaseManager } from './md-base-manager'
|
|
9
|
+
import { parseQueue, serializeQueue } from '../serializers'
|
|
10
|
+
import type { QueueJson, QueueTask, Priority, TaskType, TaskSection } from '../schemas/state'
|
|
11
|
+
|
|
12
|
+
class MdQueueManager extends MdBaseManager<QueueJson> {
|
|
13
|
+
constructor() {
|
|
14
|
+
super('core/next.md')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protected getDefault(): QueueJson {
|
|
18
|
+
return {
|
|
19
|
+
tasks: [],
|
|
20
|
+
lastUpdated: new Date().toISOString().split('T')[0]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
protected parse(content: string): QueueJson {
|
|
25
|
+
return parseQueue(content)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected serialize(data: QueueJson): string {
|
|
29
|
+
return serializeQueue(data)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// =========== Queue Operations ===========
|
|
33
|
+
|
|
34
|
+
async getTasks(projectId: string): Promise<QueueTask[]> {
|
|
35
|
+
const queue = await this.read(projectId)
|
|
36
|
+
return queue.tasks
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async getActiveTasks(projectId: string): Promise<QueueTask[]> {
|
|
40
|
+
const queue = await this.read(projectId)
|
|
41
|
+
return queue.tasks.filter(t => t.section === 'active' && !t.completed)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async getBacklog(projectId: string): Promise<QueueTask[]> {
|
|
45
|
+
const queue = await this.read(projectId)
|
|
46
|
+
return queue.tasks.filter(t => t.section === 'backlog' && !t.completed)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async addTask(
|
|
50
|
+
projectId: string,
|
|
51
|
+
task: Omit<QueueTask, 'id' | 'createdAt' | 'completed'>
|
|
52
|
+
): Promise<QueueJson> {
|
|
53
|
+
const newTask: QueueTask = {
|
|
54
|
+
...task,
|
|
55
|
+
id: `task_${Date.now()}`,
|
|
56
|
+
createdAt: new Date().toISOString(),
|
|
57
|
+
completed: false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return this.update(projectId, (queue) => ({
|
|
61
|
+
tasks: this.sortTasks([...queue.tasks, newTask]),
|
|
62
|
+
lastUpdated: new Date().toISOString().split('T')[0]
|
|
63
|
+
}))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async removeTask(projectId: string, taskId: string): Promise<QueueJson> {
|
|
67
|
+
return this.update(projectId, (queue) => ({
|
|
68
|
+
tasks: queue.tasks.filter(t => t.id !== taskId),
|
|
69
|
+
lastUpdated: new Date().toISOString().split('T')[0]
|
|
70
|
+
}))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async completeTask(projectId: string, taskId: string): Promise<QueueJson> {
|
|
74
|
+
return this.update(projectId, (queue) => ({
|
|
75
|
+
tasks: queue.tasks.map(t =>
|
|
76
|
+
t.id === taskId
|
|
77
|
+
? { ...t, completed: true, completedAt: new Date().toISOString() }
|
|
78
|
+
: t
|
|
79
|
+
),
|
|
80
|
+
lastUpdated: new Date().toISOString().split('T')[0]
|
|
81
|
+
}))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async getNextTask(projectId: string): Promise<QueueTask | null> {
|
|
85
|
+
const queue = await this.read(projectId)
|
|
86
|
+
return queue.tasks.find(t => t.section === 'active' && !t.completed) || null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async moveToSection(
|
|
90
|
+
projectId: string,
|
|
91
|
+
taskId: string,
|
|
92
|
+
section: TaskSection
|
|
93
|
+
): Promise<QueueJson> {
|
|
94
|
+
return this.update(projectId, (queue) => ({
|
|
95
|
+
tasks: queue.tasks.map(t =>
|
|
96
|
+
t.id === taskId ? { ...t, section } : t
|
|
97
|
+
),
|
|
98
|
+
lastUpdated: new Date().toISOString().split('T')[0]
|
|
99
|
+
}))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async setPriority(
|
|
103
|
+
projectId: string,
|
|
104
|
+
taskId: string,
|
|
105
|
+
priority: Priority
|
|
106
|
+
): Promise<QueueJson> {
|
|
107
|
+
return this.update(projectId, (queue) => ({
|
|
108
|
+
tasks: this.sortTasks(
|
|
109
|
+
queue.tasks.map(t => t.id === taskId ? { ...t, priority } : t)
|
|
110
|
+
),
|
|
111
|
+
lastUpdated: new Date().toISOString().split('T')[0]
|
|
112
|
+
}))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Add multiple tasks at once (e.g., from a feature breakdown)
|
|
117
|
+
*/
|
|
118
|
+
async addTasks(
|
|
119
|
+
projectId: string,
|
|
120
|
+
tasks: Array<Omit<QueueTask, 'id' | 'createdAt' | 'completed'>>
|
|
121
|
+
): Promise<QueueJson> {
|
|
122
|
+
const newTasks: QueueTask[] = tasks.map((task, i) => ({
|
|
123
|
+
...task,
|
|
124
|
+
id: `task_${Date.now()}_${i}`,
|
|
125
|
+
createdAt: new Date().toISOString(),
|
|
126
|
+
completed: false
|
|
127
|
+
}))
|
|
128
|
+
|
|
129
|
+
return this.update(projectId, (queue) => ({
|
|
130
|
+
tasks: this.sortTasks([...queue.tasks, ...newTasks]),
|
|
131
|
+
lastUpdated: new Date().toISOString().split('T')[0]
|
|
132
|
+
}))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Clear completed tasks
|
|
137
|
+
*/
|
|
138
|
+
async clearCompleted(projectId: string): Promise<QueueJson> {
|
|
139
|
+
return this.update(projectId, (queue) => ({
|
|
140
|
+
tasks: queue.tasks.filter(t => !t.completed),
|
|
141
|
+
lastUpdated: new Date().toISOString().split('T')[0]
|
|
142
|
+
}))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Sort tasks by priority then by creation date
|
|
147
|
+
*/
|
|
148
|
+
private sortTasks(tasks: QueueTask[]): QueueTask[] {
|
|
149
|
+
const priorityOrder: Record<Priority, number> = {
|
|
150
|
+
critical: 0,
|
|
151
|
+
high: 1,
|
|
152
|
+
medium: 2,
|
|
153
|
+
low: 3
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return tasks.sort((a, b) => {
|
|
157
|
+
// First by section (active first)
|
|
158
|
+
const sectionOrder: Record<TaskSection, number> = {
|
|
159
|
+
active: 0,
|
|
160
|
+
previously_active: 1,
|
|
161
|
+
backlog: 2
|
|
162
|
+
}
|
|
163
|
+
if (sectionOrder[a.section] !== sectionOrder[b.section]) {
|
|
164
|
+
return sectionOrder[a.section] - sectionOrder[b.section]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Then by priority
|
|
168
|
+
if (priorityOrder[a.priority] !== priorityOrder[b.priority]) {
|
|
169
|
+
return priorityOrder[a.priority] - priorityOrder[b.priority]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Finally by creation date
|
|
173
|
+
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export const mdQueueManager = new MdQueueManager()
|
|
179
|
+
export default mdQueueManager
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MD State Manager
|
|
3
|
+
*
|
|
4
|
+
* MD-First Architecture: Manages state via now.md.
|
|
5
|
+
* Source of truth is the markdown file, not JSON.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { MdBaseManager } from './md-base-manager'
|
|
9
|
+
import { parseState, serializeState } from '../serializers'
|
|
10
|
+
import type { StateJson, CurrentTask, PreviousTask } from '../schemas/state'
|
|
11
|
+
|
|
12
|
+
class MdStateManager extends MdBaseManager<StateJson> {
|
|
13
|
+
constructor() {
|
|
14
|
+
super('core/now.md')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protected getDefault(): StateJson {
|
|
18
|
+
return {
|
|
19
|
+
currentTask: null,
|
|
20
|
+
lastUpdated: ''
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
protected parse(content: string): StateJson {
|
|
25
|
+
return parseState(content)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected serialize(data: StateJson): string {
|
|
29
|
+
return serializeState(data)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// =========== Current Task ===========
|
|
33
|
+
|
|
34
|
+
async getCurrentTask(projectId: string): Promise<CurrentTask | null> {
|
|
35
|
+
const state = await this.read(projectId)
|
|
36
|
+
return state.currentTask
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async setCurrentTask(projectId: string, task: CurrentTask | null): Promise<StateJson> {
|
|
40
|
+
return this.update(projectId, (state) => ({
|
|
41
|
+
...state,
|
|
42
|
+
currentTask: task,
|
|
43
|
+
lastUpdated: new Date().toISOString()
|
|
44
|
+
}))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async startTask(
|
|
48
|
+
projectId: string,
|
|
49
|
+
task: Omit<CurrentTask, 'startedAt'>
|
|
50
|
+
): Promise<StateJson> {
|
|
51
|
+
const currentTask: CurrentTask = {
|
|
52
|
+
...task,
|
|
53
|
+
startedAt: new Date().toISOString()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return this.update(projectId, () => ({
|
|
57
|
+
currentTask,
|
|
58
|
+
lastUpdated: new Date().toISOString()
|
|
59
|
+
}))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async completeTask(projectId: string): Promise<StateJson> {
|
|
63
|
+
const state = await this.read(projectId)
|
|
64
|
+
if (!state.currentTask) {
|
|
65
|
+
throw new Error('No active task to complete')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return this.update(projectId, () => ({
|
|
69
|
+
currentTask: null,
|
|
70
|
+
lastUpdated: new Date().toISOString()
|
|
71
|
+
}))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async pauseTask(projectId: string): Promise<StateJson> {
|
|
75
|
+
const state = await this.read(projectId)
|
|
76
|
+
if (!state.currentTask) {
|
|
77
|
+
throw new Error('No active task to pause')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const previousTask: PreviousTask = {
|
|
81
|
+
id: state.currentTask.id,
|
|
82
|
+
description: state.currentTask.description,
|
|
83
|
+
status: 'paused',
|
|
84
|
+
startedAt: state.currentTask.startedAt,
|
|
85
|
+
pausedAt: new Date().toISOString()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return this.update(projectId, () => ({
|
|
89
|
+
currentTask: null,
|
|
90
|
+
previousTask,
|
|
91
|
+
lastUpdated: new Date().toISOString()
|
|
92
|
+
}))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async resumeTask(projectId: string): Promise<StateJson> {
|
|
96
|
+
const state = await this.read(projectId)
|
|
97
|
+
if (!state.previousTask) {
|
|
98
|
+
throw new Error('No paused task to resume')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const currentTask: CurrentTask = {
|
|
102
|
+
id: state.previousTask.id,
|
|
103
|
+
description: state.previousTask.description,
|
|
104
|
+
startedAt: new Date().toISOString(), // Reset start time
|
|
105
|
+
sessionId: `sess_${Date.now()}`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return this.update(projectId, () => ({
|
|
109
|
+
currentTask,
|
|
110
|
+
previousTask: null,
|
|
111
|
+
lastUpdated: new Date().toISOString()
|
|
112
|
+
}))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async clearTask(projectId: string): Promise<StateJson> {
|
|
116
|
+
return this.update(projectId, () => ({
|
|
117
|
+
currentTask: null,
|
|
118
|
+
previousTask: null,
|
|
119
|
+
lastUpdated: new Date().toISOString()
|
|
120
|
+
}))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if there's an active or paused task
|
|
125
|
+
*/
|
|
126
|
+
async hasTask(projectId: string): Promise<boolean> {
|
|
127
|
+
const state = await this.read(projectId)
|
|
128
|
+
return state.currentTask !== null || state.previousTask !== null
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const mdStateManager = new MdStateManager()
|
|
133
|
+
export default mdStateManager
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializers Index
|
|
3
|
+
*
|
|
4
|
+
* MD-First Architecture: These serializers convert between TypeScript schemas and MD format.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// State (now.md)
|
|
8
|
+
export {
|
|
9
|
+
parseState,
|
|
10
|
+
serializeState,
|
|
11
|
+
createCurrentTaskMd,
|
|
12
|
+
createEmptyStateMd
|
|
13
|
+
} from './state-serializer'
|
|
14
|
+
|
|
15
|
+
// Queue (next.md)
|
|
16
|
+
export {
|
|
17
|
+
parseQueue,
|
|
18
|
+
serializeQueue,
|
|
19
|
+
createEmptyQueueMd
|
|
20
|
+
} from './queue-serializer'
|