@haoyiyin/workflow 0.2.2 → 0.2.3
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/package.json +9 -8
- package/src/agents/contracts.ts +559 -0
- package/src/agents/dispatcher-enhanced.ts +350 -0
- package/src/agents/dispatcher.ts +680 -0
- package/src/agents/index.ts +48 -0
- package/src/agents/resilience.ts +255 -0
- package/src/agents/token-budget.ts +83 -0
- package/src/agents/types.ts +73 -0
- package/src/guard/main-agent.ts +245 -0
- package/src/hooks/builtin/index.ts +8 -0
- package/src/hooks/builtin/on-error.ts +23 -0
- package/src/hooks/builtin/post-execute.ts +40 -0
- package/src/hooks/builtin/post-plan.ts +23 -0
- package/src/hooks/builtin/pre-execute.ts +30 -0
- package/src/hooks/builtin/pre-plan.ts +26 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/loader.ts +98 -0
- package/src/hooks/manager.ts +99 -0
- package/src/hooks/types-enhanced.ts +38 -0
- package/src/hooks/types.ts +35 -0
- package/src/index.ts +127 -0
- package/src/persistence/index.ts +17 -0
- package/src/persistence/plan-md.ts +141 -0
- package/src/persistence/state-md.ts +167 -0
- package/src/persistence/types.ts +89 -0
- package/src/router/classifier.ts +610 -0
- package/src/router/guard.ts +483 -0
- package/src/router/index.ts +22 -0
- package/src/router/router.ts +108 -0
- package/src/router/types.ts +127 -0
- package/src/skills/agents-md/SKILL.md +45 -0
- package/src/skills/agents-md/index.ts +33 -0
- package/src/skills/execute-plan/SKILL.md +60 -0
- package/src/skills/execute-plan/index.ts +970 -0
- package/src/skills/index.ts +13 -0
- package/src/skills/quick-task/SKILL.md +54 -0
- package/src/skills/quick-task/index.ts +346 -0
- package/src/skills/registry.ts +59 -0
- package/src/skills/review-diff/SKILL.md +53 -0
- package/src/skills/review-diff/index.ts +394 -0
- package/src/skills/skill.ts +59 -0
- package/src/skills/systematic-debugging/SKILL.md +56 -0
- package/src/skills/systematic-debugging/index.ts +404 -0
- package/src/skills/tdd/SKILL.md +52 -0
- package/src/skills/tdd/index.ts +409 -0
- package/src/skills/to-plan/SKILL.md +56 -0
- package/src/skills/to-plan/index-enhanced.ts +551 -0
- package/src/skills/to-plan/index.ts +586 -0
- package/src/skills/types.ts +47 -0
- package/src/state/cleanup.ts +118 -0
- package/src/state/index.ts +8 -0
- package/src/state/manager.ts +96 -0
- package/src/state/persistence.ts +77 -0
- package/src/state/types.ts +30 -0
- package/src/state/validator.ts +78 -0
- package/src/types.ts +102 -0
- package/src/utils/compress.ts +347 -0
- package/src/utils/git.ts +82 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/logger.ts +23 -0
- package/src/utils/paths.ts +55 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Compression Utility - Compresses verbose output to terse prose
|
|
3
|
+
*
|
|
4
|
+
* Pure text transformation. No subagent dispatch needed.
|
|
5
|
+
* Use case: Take verbose AI output and compress it to essential facts.
|
|
6
|
+
*
|
|
7
|
+
* Pattern: Input text → Compression rules → Terse output
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Schemas
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export interface CompressOptions {
|
|
15
|
+
style: 'terse' | 'bullets' | 'oneline'
|
|
16
|
+
maxLength?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CompressResult {
|
|
20
|
+
compressed: string
|
|
21
|
+
originalLength: number
|
|
22
|
+
compressedLength: number
|
|
23
|
+
compressionRatio: number
|
|
24
|
+
sectionsRemoved: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Compression rules (pure functions, immutable)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/** Words and phrases that can be removed without losing meaning */
|
|
32
|
+
const FILLER_PATTERNS: ReadonlyArray<{ pattern: RegExp; replacement: string }> = [
|
|
33
|
+
{ pattern: /\b(?:basically|essentially|fundamentally)\b/gi, replacement: '' },
|
|
34
|
+
{ pattern: /\b(?:actually|literally|virtually)\b/gi, replacement: '' },
|
|
35
|
+
{ pattern: /\b(?:obviously|clearly|evidently)\b/gi, replacement: '' },
|
|
36
|
+
{ pattern: /\b(?:interestingly|notably|importantly)\b/gi, replacement: '' },
|
|
37
|
+
{ pattern: /\b(?:I think|I believe|in my opinion|it seems that|it appears that)\b/gi, replacement: '' },
|
|
38
|
+
{ pattern: /\b(?:as mentioned|as noted|as discussed|as previously stated)\b/gi, replacement: '' },
|
|
39
|
+
{ pattern: /\b(?:it is worth noting that|it should be noted that|note that)\b/gi, replacement: '' },
|
|
40
|
+
{ pattern: /\b(?:in order to)\b/gi, replacement: 'to' },
|
|
41
|
+
{ pattern: /\b(?:due to the fact that)\b/gi, replacement: 'because' },
|
|
42
|
+
{ pattern: /\b(?:at this point in time)\b/gi, replacement: 'now' },
|
|
43
|
+
{ pattern: /\b(?:in the event that)\b/gi, replacement: 'if' },
|
|
44
|
+
{ pattern: /\b(?:a number of)\b/gi, replacement: 'several' },
|
|
45
|
+
{ pattern: /\b(?:the majority of)\b/gi, replacement: 'most' },
|
|
46
|
+
{ pattern: /\b(?:a large number of)\b/gi, replacement: 'many' },
|
|
47
|
+
{ pattern: /\b(?:in the near future)\b/gi, replacement: 'soon' },
|
|
48
|
+
{ pattern: /\b(?:has the ability to)\b/gi, replacement: 'can' },
|
|
49
|
+
{ pattern: /\b(?:is able to)\b/gi, replacement: 'can' },
|
|
50
|
+
{ pattern: /\b(?:is capable of)\b/gi, replacement: 'can' },
|
|
51
|
+
{ pattern: /\b(?:make a decision)\b/gi, replacement: 'decide' },
|
|
52
|
+
{ pattern: /\b(?:take into account)\b/gi, replacement: 'consider' },
|
|
53
|
+
{ pattern: /\b(?:with regard to|in regard to|with respect to)\b/gi, replacement: 'about' },
|
|
54
|
+
{ pattern: /\b(?:in the process of)\b/gi, replacement: '' },
|
|
55
|
+
{ pattern: /\b(?:please note that)\b/gi, replacement: '' },
|
|
56
|
+
{ pattern: /\b(?:I would like to|I want to|I will now|let me)\b/gi, replacement: '' },
|
|
57
|
+
{ pattern: /\b(?:here is|here are|below is|below are)\b/gi, replacement: '' },
|
|
58
|
+
{ pattern: /\b(?:the following(?: is| are)?)\b/gi, replacement: '' },
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
/** Section headers that can be collapsed */
|
|
62
|
+
const SECTION_PATTERNS: ReadonlyArray<RegExp> = [
|
|
63
|
+
/^#{1,4}\s+Introduction\b/gim,
|
|
64
|
+
/^#{1,4}\s+Overview\b/gim,
|
|
65
|
+
/^#{1,4}\s+Background\b/gim,
|
|
66
|
+
/^#{1,4}\s+Context\b/gim,
|
|
67
|
+
/^#{1,4}\s+Disclaimer\b/gim,
|
|
68
|
+
/^#{1,4}\s+Note\b/gim,
|
|
69
|
+
/^#{1,4}\s+Notes?\b/gim,
|
|
70
|
+
/^#{1,4}\s+Additional Information\b/gim,
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
/** Lines that are purely transitional and can be removed */
|
|
74
|
+
const TRANSITION_PATTERNS: ReadonlyArray<RegExp> = [
|
|
75
|
+
/^(?:Now|Next|Then|Finally|Firstly|Secondly|Thirdly|Lastly),\s*.+$/gim,
|
|
76
|
+
/^(?:Moving on|Let'?s move on|Let'?s continue).+$/gim,
|
|
77
|
+
/^(?:To summarize|In summary|To conclude|In conclusion|Wrapping up).*$/gim,
|
|
78
|
+
/^(?:Without further ado|As you can see).*$/gim,
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Core compression functions (pure, immutable)
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Apply all filler-word replacements to the text.
|
|
87
|
+
* Each rule is applied sequentially on the accumulated result (immutable pattern).
|
|
88
|
+
*/
|
|
89
|
+
function removeFillerWords(text: string): string {
|
|
90
|
+
let result = text
|
|
91
|
+
for (const { pattern, replacement } of FILLER_PATTERNS) {
|
|
92
|
+
result = result.replace(pattern, replacement)
|
|
93
|
+
}
|
|
94
|
+
return result
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Remove known section headers that add no information.
|
|
99
|
+
*/
|
|
100
|
+
function removeBoilerplateSections(text: string): string {
|
|
101
|
+
let result = text
|
|
102
|
+
for (const pattern of SECTION_PATTERNS) {
|
|
103
|
+
// Remove the header line and the following paragraph until next header or blank line
|
|
104
|
+
result = result.replace(
|
|
105
|
+
new RegExp(`${pattern.source}\\n*(?:[^#\\n]\\n*)*`, 'gim'),
|
|
106
|
+
'',
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
return result
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Remove transitional sentences that don't carry content.
|
|
114
|
+
*/
|
|
115
|
+
function removeTransitions(text: string): string {
|
|
116
|
+
let result = text
|
|
117
|
+
for (const pattern of TRANSITION_PATTERNS) {
|
|
118
|
+
result = result.replace(pattern, '')
|
|
119
|
+
}
|
|
120
|
+
return result
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Collapse multiple blank lines into a single blank line.
|
|
125
|
+
*/
|
|
126
|
+
function collapseWhitespace(text: string): string {
|
|
127
|
+
return text
|
|
128
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
129
|
+
.replace(/[ \t]+/g, ' ')
|
|
130
|
+
.replace(/^[ \t]+/gm, '')
|
|
131
|
+
.replace(/[ \t]+$/gm, '')
|
|
132
|
+
.trim()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Collapse repeated sentences (simple cosine of word overlap).
|
|
137
|
+
*/
|
|
138
|
+
function deduplicateSentences(text: string): string {
|
|
139
|
+
const sentences = text.match(/[^.!?\n]+[.!?]?/g) ?? []
|
|
140
|
+
if (sentences.length <= 1) return text
|
|
141
|
+
|
|
142
|
+
const seen = new Set<string>()
|
|
143
|
+
const kept: string[] = []
|
|
144
|
+
|
|
145
|
+
for (const sentence of sentences) {
|
|
146
|
+
// Normalize for comparison: lowercase, strip punctuation, sort words
|
|
147
|
+
const normalized = sentence
|
|
148
|
+
.toLowerCase()
|
|
149
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
150
|
+
.trim()
|
|
151
|
+
.split(/\s+/)
|
|
152
|
+
.filter((w) => w.length > 2)
|
|
153
|
+
.sort()
|
|
154
|
+
.join(' ')
|
|
155
|
+
|
|
156
|
+
if (normalized.length < 10) {
|
|
157
|
+
kept.push(sentence)
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (seen.has(normalized)) continue
|
|
162
|
+
|
|
163
|
+
// Check for near-duplicates (Jaccard similarity)
|
|
164
|
+
let isDuplicate = false
|
|
165
|
+
const words = new Set(normalized.split(' '))
|
|
166
|
+
for (const existing of seen) {
|
|
167
|
+
const existingWords = new Set(existing.split(' '))
|
|
168
|
+
const intersection = [...words].filter((w) => existingWords.has(w)).length
|
|
169
|
+
const union = new Set([...words, ...existingWords]).size
|
|
170
|
+
if (union > 0 && intersection / union > 0.7) {
|
|
171
|
+
isDuplicate = true
|
|
172
|
+
break
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!isDuplicate) {
|
|
177
|
+
seen.add(normalized)
|
|
178
|
+
kept.push(sentence)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return kept.join(' ')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Extract the key sentences: first sentence, sentences with numbers/data,
|
|
187
|
+
* sentences with key action verbs.
|
|
188
|
+
*/
|
|
189
|
+
function extractKeySentences(text: string): string {
|
|
190
|
+
const sentences = text.match(/[^.!?\n]+[.!?]?/g) ?? []
|
|
191
|
+
if (sentences.length <= 3) return text
|
|
192
|
+
|
|
193
|
+
const scored = sentences.map((s, i) => {
|
|
194
|
+
let score = 0
|
|
195
|
+
// First sentence is important
|
|
196
|
+
if (i === 0) score += 3
|
|
197
|
+
// Sentences with data/numbers
|
|
198
|
+
if (/\d/.test(s)) score += 2
|
|
199
|
+
// Sentences with code indicators
|
|
200
|
+
if (/[`'']|```|function|class|import|export|const|let|var/.test(s)) score += 2
|
|
201
|
+
// Sentences with action verbs
|
|
202
|
+
if (/\b(must|should|need|require|fix|change|update|create|delete|remove|add)\b/i.test(s)) score += 2
|
|
203
|
+
// Short sentences (likely statements of fact)
|
|
204
|
+
if (s.length < 80) score += 1
|
|
205
|
+
// Last sentence (conclusion)
|
|
206
|
+
if (i === sentences.length - 1) score += 2
|
|
207
|
+
return { sentence: s, score }
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
// Keep top-scoring sentences (at least 2, at most 60% of original)
|
|
211
|
+
const keepCount = Math.max(2, Math.ceil(sentences.length * 0.6))
|
|
212
|
+
const sorted = [...scored].sort((a, b) => b.score - a.score)
|
|
213
|
+
const kept = sorted.slice(0, keepCount)
|
|
214
|
+
// Restore original order
|
|
215
|
+
const keptSet = new Set(kept.map((k) => k.sentence))
|
|
216
|
+
return sentences.filter((s) => keptSet.has(s)).join(' ')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Convert prose to bullet points by splitting on sentence boundaries
|
|
221
|
+
* and prefixing each meaningful chunk with "-".
|
|
222
|
+
*/
|
|
223
|
+
function toBullets(text: string): string {
|
|
224
|
+
const sentences = text.match(/[^.!?\n]+[.!?]?/g) ?? []
|
|
225
|
+
if (sentences.length <= 1) return text
|
|
226
|
+
|
|
227
|
+
return sentences
|
|
228
|
+
.map((s) => s.trim())
|
|
229
|
+
.filter((s) => s.length > 5)
|
|
230
|
+
.map((s) => `- ${s}`)
|
|
231
|
+
.join('\n')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Convert text to a single line by replacing all newlines with spaces.
|
|
236
|
+
*/
|
|
237
|
+
function toOneLine(text: string): string {
|
|
238
|
+
return text.replace(/\n+/g, ' ').replace(/\s{2,}/g, ' ').trim()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Truncate text to maxLength characters, preserving word boundaries.
|
|
243
|
+
*/
|
|
244
|
+
function truncateToMaxLength(text: string, maxLength: number): string {
|
|
245
|
+
if (text.length <= maxLength) return text
|
|
246
|
+
const truncated = text.slice(0, maxLength)
|
|
247
|
+
const lastSpace = truncated.lastIndexOf(' ')
|
|
248
|
+
if (lastSpace > maxLength * 0.8) {
|
|
249
|
+
return truncated.slice(0, lastSpace) + '...'
|
|
250
|
+
}
|
|
251
|
+
return truncated + '...'
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Count the number of sections (markdown headers) in the text.
|
|
256
|
+
*/
|
|
257
|
+
function countSections(text: string): number {
|
|
258
|
+
const matches = text.match(/^#{1,4}\s+.+$/gm)
|
|
259
|
+
return matches ? matches.length : 0
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// Main compression pipeline
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Compress verbose text through a pipeline of transformations.
|
|
268
|
+
* All steps are pure functions that return new strings (immutable pattern).
|
|
269
|
+
*/
|
|
270
|
+
export function compressText(text: string, options: CompressOptions): CompressResult {
|
|
271
|
+
const originalLength = text.length
|
|
272
|
+
const sectionsBefore = countSections(text)
|
|
273
|
+
|
|
274
|
+
// Stage 1: Remove filler words and verbose phrases
|
|
275
|
+
let result = removeFillerWords(text)
|
|
276
|
+
|
|
277
|
+
// Stage 2: Remove boilerplate sections
|
|
278
|
+
result = removeBoilerplateSections(result)
|
|
279
|
+
|
|
280
|
+
// Stage 3: Remove transitional fluff
|
|
281
|
+
result = removeTransitions(result)
|
|
282
|
+
|
|
283
|
+
// Stage 4: Deduplicate near-identical sentences
|
|
284
|
+
result = deduplicateSentences(result)
|
|
285
|
+
|
|
286
|
+
// Stage 5: Extract key sentences when text is lengthy
|
|
287
|
+
if (result.length > 500) {
|
|
288
|
+
result = extractKeySentences(result)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Stage 6: Apply style formatting
|
|
292
|
+
switch (options.style) {
|
|
293
|
+
case 'bullets':
|
|
294
|
+
result = toBullets(result)
|
|
295
|
+
break
|
|
296
|
+
case 'oneline':
|
|
297
|
+
result = toOneLine(result)
|
|
298
|
+
break
|
|
299
|
+
case 'terse':
|
|
300
|
+
// Terse is the default prose after all prior stages
|
|
301
|
+
break
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Stage 7: Collapse whitespace
|
|
305
|
+
result = collapseWhitespace(result)
|
|
306
|
+
|
|
307
|
+
// Stage 8: Truncate if needed
|
|
308
|
+
if (options.maxLength && result.length > options.maxLength) {
|
|
309
|
+
result = truncateToMaxLength(result, options.maxLength)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const sectionsAfter = countSections(result)
|
|
313
|
+
const compressedLength = result.length
|
|
314
|
+
const compressionRatio =
|
|
315
|
+
originalLength > 0
|
|
316
|
+
? Math.round(((originalLength - compressedLength) / originalLength) * 100) / 100
|
|
317
|
+
: 0
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
compressed: result,
|
|
321
|
+
originalLength,
|
|
322
|
+
compressedLength,
|
|
323
|
+
compressionRatio,
|
|
324
|
+
sectionsRemoved: Math.max(0, sectionsBefore - sectionsAfter),
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Convenience function for terse compression
|
|
330
|
+
*/
|
|
331
|
+
export function compressTerse(text: string, maxLength?: number): string {
|
|
332
|
+
return compressText(text, { style: 'terse', maxLength }).compressed
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Convenience function for bullet compression
|
|
337
|
+
*/
|
|
338
|
+
export function compressBullets(text: string, maxLength?: number): string {
|
|
339
|
+
return compressText(text, { style: 'bullets', maxLength }).compressed
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Convenience function for one-line compression
|
|
344
|
+
*/
|
|
345
|
+
export function compressOneLine(text: string, maxLength?: number): string {
|
|
346
|
+
return compressText(text, { style: 'oneline', maxLength }).compressed
|
|
347
|
+
}
|
package/src/utils/git.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git utility functions
|
|
3
|
+
*/
|
|
4
|
+
import { execSync } from 'child_process'
|
|
5
|
+
|
|
6
|
+
export function getCurrentBranch(cwd: string): string {
|
|
7
|
+
try {
|
|
8
|
+
return execSync('git rev-parse --abbrev-ref HEAD', {
|
|
9
|
+
cwd,
|
|
10
|
+
encoding: 'utf-8',
|
|
11
|
+
}).trim()
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error('Not a git repository')
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getBaseBranch(cwd: string): string | null {
|
|
18
|
+
try {
|
|
19
|
+
// Try main first
|
|
20
|
+
execSync('git rev-parse --verify main', { cwd })
|
|
21
|
+
return 'main'
|
|
22
|
+
} catch {
|
|
23
|
+
try {
|
|
24
|
+
// Fallback to master
|
|
25
|
+
execSync('git rev-parse --verify master', { cwd })
|
|
26
|
+
return 'master'
|
|
27
|
+
} catch {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isWorktree(cwd: string): boolean {
|
|
34
|
+
try {
|
|
35
|
+
const gitDir = execSync('git rev-parse --git-dir', {
|
|
36
|
+
cwd,
|
|
37
|
+
encoding: 'utf-8',
|
|
38
|
+
}).trim()
|
|
39
|
+
return gitDir.includes('.git/worktrees')
|
|
40
|
+
} catch {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getWorktreePath(cwd: string): string | null {
|
|
46
|
+
if (!isWorktree(cwd)) return null
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
50
|
+
cwd,
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
}).trim()
|
|
53
|
+
} catch {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function hasUncommittedChanges(cwd: string): boolean {
|
|
59
|
+
try {
|
|
60
|
+
const status = execSync('git status --porcelain', {
|
|
61
|
+
cwd,
|
|
62
|
+
encoding: 'utf-8',
|
|
63
|
+
})
|
|
64
|
+
return status.trim().length > 0
|
|
65
|
+
} catch {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function commitChanges(cwd: string, message: string): void {
|
|
71
|
+
execSync('git add -A', { cwd })
|
|
72
|
+
execSync(`git commit -m "${message}"`, { cwd })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function mergeBranch(cwd: string, branch: string, base: string): void {
|
|
76
|
+
execSync(`git checkout ${base}`, { cwd })
|
|
77
|
+
execSync(`git merge ${branch}`, { cwd })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function deleteBranch(cwd: string, branch: string): void {
|
|
81
|
+
execSync(`git branch -D ${branch}`, { cwd })
|
|
82
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger utility for yi-workflow
|
|
3
|
+
*/
|
|
4
|
+
import type { Logger } from '../types.js'
|
|
5
|
+
|
|
6
|
+
export function createLogger(verbose = false): Logger {
|
|
7
|
+
return {
|
|
8
|
+
info: (message: string, ...args: unknown[]) => {
|
|
9
|
+
console.log(`[INFO] ${message}`, ...args)
|
|
10
|
+
},
|
|
11
|
+
warn: (message: string, ...args: unknown[]) => {
|
|
12
|
+
console.warn(`[WARN] ${message}`, ...args)
|
|
13
|
+
},
|
|
14
|
+
error: (message: string, ...args: unknown[]) => {
|
|
15
|
+
console.error(`[ERROR] ${message}`, ...args)
|
|
16
|
+
},
|
|
17
|
+
debug: (message: string, ...args: unknown[]) => {
|
|
18
|
+
if (verbose) {
|
|
19
|
+
console.log(`[DEBUG] ${message}`, ...args)
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path management utilities
|
|
3
|
+
*/
|
|
4
|
+
import { join, dirname, resolve } from 'path'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import { homedir } from 'os'
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
9
|
+
const __dirname = dirname(__filename)
|
|
10
|
+
|
|
11
|
+
export function getPackageRoot(): string {
|
|
12
|
+
return resolve(__dirname, '../..')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getConfigPath(): string {
|
|
16
|
+
return join(homedir(), '.config', 'yi-workflow', 'config.json')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getCachePath(): string {
|
|
20
|
+
return join(homedir(), '.cache', 'yi-workflow')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getPiPath(cwd: string): string {
|
|
24
|
+
return join(cwd, '.pi')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getPlansPath(cwd: string): string {
|
|
28
|
+
return join(cwd, '.pi', 'plans')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getStatePath(cwd: string): string {
|
|
32
|
+
return join(cwd, '.pi', 'yi-workflow', 'state')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getActivePlansPath(cwd: string): string {
|
|
36
|
+
return join(getPlansPath(cwd), 'active')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getArchivePlansPath(cwd: string): string {
|
|
40
|
+
return join(getPlansPath(cwd), 'archive')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getActiveStatePath(cwd: string): string {
|
|
44
|
+
return join(getStatePath(cwd), 'active')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getArchiveStatePath(cwd: string): string {
|
|
48
|
+
return join(getStatePath(cwd), 'archive')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function generatePlanFileName(topic: string): string {
|
|
52
|
+
const date = new Date().toISOString().split('T')[0]
|
|
53
|
+
const sanitized = topic.toLowerCase().replace(/[^a-z0-9]+/g, '-')
|
|
54
|
+
return `${date}-${sanitized}-implementation-plan.md`
|
|
55
|
+
}
|