prjct-cli 0.60.2 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.61.0] - 2026-02-05
4
+
5
+ ### Features
6
+
7
+ - add .prjct-state.md local state file for persistence (PRJ-112) (#102)
8
+
9
+
10
+ ## [0.61.0] - 2026-02-05
11
+
12
+ ### Features
13
+
14
+ - **Local state file (PRJ-112)**: New `.prjct-state.md` file generated in project root for local persistence
15
+
16
+ ### Implementation Details
17
+
18
+ Created `LocalStateGenerator` service that generates a markdown file showing current task state. Integrated via write-through pattern - `StateStorage.write()` now also generates the local state file. Also hooks into `sync-service.ts` for state.json updates during sync.
19
+
20
+ ### Learnings
21
+
22
+ - Write-through pattern: JSON storage triggers MD generation automatically
23
+ - State can be written from multiple entry points (storage class + sync service) - need hooks in both places
24
+
25
+ ### Test Plan
26
+
27
+ #### For QA
28
+ 1. Run `prjct sync` on any project - verify `.prjct-state.md` is generated in project root
29
+ 2. Start a task with `p. task "test"` - verify `.prjct-state.md` updates with task info
30
+ 3. Check that subtasks, progress, and status are displayed correctly
31
+ 4. Verify the file has "DO NOT EDIT" header comment
32
+
33
+ #### For Users
34
+ - New `.prjct-state.md` file in project root shows current task state
35
+ - Automatic - file updates whenever prjct state changes
36
+ - No breaking changes
37
+
38
+
3
39
  ## [0.60.2] - 2026-02-05
4
40
 
5
41
  ### Performance
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Local State Generator
3
+ *
4
+ * Generates .prjct-state.md in the project root for local persistence.
5
+ * This provides a quick reference to current task state without needing
6
+ * to access global storage.
7
+ *
8
+ * @see PRJ-112
9
+ */
10
+
11
+ import fs from 'node:fs/promises'
12
+ import path from 'node:path'
13
+ import type { StateJson } from '../schemas/state'
14
+ import { isNotFoundError } from '../types/fs'
15
+
16
+ const LOCAL_STATE_FILENAME = '.prjct-state.md'
17
+
18
+ // Extended runtime types (state.json has fields not in strict schema)
19
+ interface RuntimeTask {
20
+ description: string
21
+ startedAt: string
22
+ linearId?: string
23
+ branch?: string
24
+ status?: string
25
+ subtasks?: Array<{
26
+ description: string
27
+ status: string
28
+ }>
29
+ currentSubtaskIndex?: number
30
+ }
31
+
32
+ interface RuntimePreviousTask {
33
+ description: string
34
+ status: string
35
+ prUrl?: string
36
+ }
37
+
38
+ class LocalStateGenerator {
39
+ /**
40
+ * Generate .prjct-state.md in the project root
41
+ */
42
+ async generate(projectPath: string, state: StateJson): Promise<void> {
43
+ const filePath = path.join(projectPath, LOCAL_STATE_FILENAME)
44
+ const content = this.toMarkdown(state)
45
+ await fs.writeFile(filePath, content, 'utf-8')
46
+ }
47
+
48
+ /**
49
+ * Remove local state file
50
+ */
51
+ async remove(projectPath: string): Promise<void> {
52
+ const filePath = path.join(projectPath, LOCAL_STATE_FILENAME)
53
+ try {
54
+ await fs.unlink(filePath)
55
+ } catch (error) {
56
+ if (!isNotFoundError(error)) throw error
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Check if local state file exists
62
+ */
63
+ async exists(projectPath: string): Promise<boolean> {
64
+ const filePath = path.join(projectPath, LOCAL_STATE_FILENAME)
65
+ try {
66
+ await fs.access(filePath)
67
+ return true
68
+ } catch {
69
+ return false
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Convert state to markdown format
75
+ * Note: Uses runtime types since state.json has fields not in strict Zod schema
76
+ */
77
+ private toMarkdown(state: StateJson): string {
78
+ const lines: string[] = [
79
+ '<!-- Auto-generated by prjct - DO NOT EDIT -->',
80
+ '<!-- This file provides local state persistence for AI tools -->',
81
+ '',
82
+ '# prjct State',
83
+ '',
84
+ ]
85
+
86
+ if (state.currentTask) {
87
+ // Cast to runtime type (state.json has additional fields)
88
+ const task = state.currentTask as unknown as RuntimeTask
89
+ lines.push('## Current Task')
90
+ lines.push('')
91
+ lines.push(`**${task.description}**`)
92
+ lines.push('')
93
+ lines.push(`- Started: ${task.startedAt}`)
94
+ if (task.linearId) {
95
+ lines.push(`- Linear: ${task.linearId}`)
96
+ }
97
+ if (task.branch) {
98
+ lines.push(`- Branch: ${task.branch}`)
99
+ }
100
+ lines.push(`- Status: ${task.status || 'active'}`)
101
+ lines.push('')
102
+
103
+ // Subtasks
104
+ if (task.subtasks && task.subtasks.length > 0) {
105
+ lines.push('### Subtasks')
106
+ lines.push('')
107
+
108
+ task.subtasks.forEach((subtask, index) => {
109
+ const statusIcon =
110
+ subtask.status === 'completed' ? '✅' : subtask.status === 'in_progress' ? '▶️' : '⏳'
111
+ const isActive = index === task.currentSubtaskIndex ? ' ← **Active**' : ''
112
+ lines.push(`${index + 1}. ${statusIcon} ${subtask.description}${isActive}`)
113
+ })
114
+
115
+ lines.push('')
116
+
117
+ // Progress
118
+ const completed = task.subtasks.filter((s) => s.status === 'completed').length
119
+ const total = task.subtasks.length
120
+ const percentage = Math.round((completed / total) * 100)
121
+ lines.push(`**Progress**: ${completed}/${total} (${percentage}%)`)
122
+ lines.push('')
123
+ }
124
+ } else {
125
+ lines.push('*No active task*')
126
+ lines.push('')
127
+ lines.push('Start a task with `p. task "description"`')
128
+ lines.push('')
129
+ }
130
+
131
+ // Previous task info
132
+ if (state.previousTask) {
133
+ // Cast to runtime type
134
+ const prevTask = state.previousTask as unknown as RuntimePreviousTask
135
+ lines.push('---')
136
+ lines.push('')
137
+ lines.push('## Previous Task')
138
+ lines.push('')
139
+ lines.push(`**${prevTask.description}**`)
140
+ lines.push('')
141
+ lines.push(`- Status: ${prevTask.status}`)
142
+ if (prevTask.prUrl) {
143
+ lines.push(`- PR: ${prevTask.prUrl}`)
144
+ }
145
+ lines.push('')
146
+ }
147
+
148
+ // Footer
149
+ lines.push('---')
150
+ lines.push(`*Last updated: ${state.lastUpdated || new Date().toISOString()}*`)
151
+ lines.push('')
152
+
153
+ return lines.join('\n')
154
+ }
155
+ }
156
+
157
+ export const localStateGenerator = new LocalStateGenerator()
158
+ export default localStateGenerator
@@ -33,6 +33,7 @@ import { metricsStorage } from '../storage/metrics-storage'
33
33
  import dateHelper from '../utils/date-helper'
34
34
  import { ContextFileGenerator } from './context-generator'
35
35
  import type { SyncDiff } from './diff-generator'
36
+ import { localStateGenerator } from './local-state-generator'
36
37
  import { type StackDetection, StackDetector } from './stack-detector'
37
38
 
38
39
  const execAsync = promisify(exec)
@@ -826,6 +827,16 @@ You are the ${name} expert for this project. Apply best practices for the detect
826
827
  }
827
828
 
828
829
  await fs.writeFile(statePath, JSON.stringify(state, null, 2), 'utf-8')
830
+
831
+ // Also generate local .prjct-state.md (PRJ-112)
832
+ try {
833
+ await localStateGenerator.generate(
834
+ this.projectPath,
835
+ state as import('../schemas/state').StateJson
836
+ )
837
+ } catch {
838
+ // Silently fail - local state is optional
839
+ }
829
840
  }
830
841
 
831
842
  // ==========================================================================
@@ -3,6 +3,9 @@
3
3
  *
4
4
  * Manages current task state via storage/state.json
5
5
  * Generates context/now.md for Claude
6
+ *
7
+ * Note: Local .prjct-state.md is generated by sync-service which has access
8
+ * to projectPath. This class only has projectId.
6
9
  */
7
10
 
8
11
  import { generateUUID } from '../schemas'