specweave 0.23.0 → 0.23.2
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/.claude-plugin/marketplace.json +0 -88
- package/CLAUDE.md +100 -0
- package/bin/fix-marketplace-errors.sh +8 -8
- package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/index.js +5 -17
- package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
- package/dist/src/core/repo-structure/repo-id-generator.d.ts +20 -0
- package/dist/src/core/repo-structure/repo-id-generator.d.ts.map +1 -1
- package/dist/src/core/repo-structure/repo-id-generator.js +44 -0
- package/dist/src/core/repo-structure/repo-id-generator.js.map +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.js +5 -2
- package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/.claude-plugin/plugin.json +10 -0
- package/plugins/specweave/commands/specweave-archive.md +51 -15
- package/plugins/specweave/hooks/post-edit-spec.sh +62 -9
- package/plugins/specweave/hooks/post-metadata-change.sh +160 -0
- package/plugins/specweave/hooks/post-write-spec.sh +62 -8
- package/plugins/specweave/lib/hooks/auto-transition.js.bak +50 -0
- package/plugins/specweave/lib/hooks/auto-transition.ts.bak +84 -0
- package/plugins/specweave/lib/hooks/git-diff-analyzer.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/git-diff-analyzer.d.ts.bak +89 -0
- package/plugins/specweave/lib/hooks/git-diff-analyzer.js.bak +142 -0
- package/plugins/specweave/lib/hooks/git-diff-analyzer.ts.bak +269 -0
- package/plugins/specweave/lib/hooks/invoke-translator-skill.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/invoke-translator-skill.d.ts.bak +60 -0
- package/plugins/specweave/lib/hooks/invoke-translator-skill.js.bak +155 -0
- package/plugins/specweave/lib/hooks/invoke-translator-skill.ts.bak +264 -0
- package/plugins/specweave/lib/hooks/prepare-reflection-context.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/prepare-reflection-context.d.ts.bak +42 -0
- package/plugins/specweave/lib/hooks/prepare-reflection-context.js.bak +110 -0
- package/plugins/specweave/lib/hooks/prepare-reflection-context.ts.bak +178 -0
- package/plugins/specweave/lib/hooks/reflection-config-loader.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/reflection-config-loader.d.ts.bak +45 -0
- package/plugins/specweave/lib/hooks/reflection-config-loader.js.bak +92 -0
- package/plugins/specweave/lib/hooks/reflection-config-loader.ts.bak +156 -0
- package/plugins/specweave/lib/hooks/reflection-parser.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/reflection-parser.d.ts.bak +33 -0
- package/plugins/specweave/lib/hooks/reflection-parser.js.bak +301 -0
- package/plugins/specweave/lib/hooks/reflection-parser.ts.bak +484 -0
- package/plugins/specweave/lib/hooks/reflection-prompt-builder.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/reflection-prompt-builder.d.ts.bak +56 -0
- package/plugins/specweave/lib/hooks/reflection-prompt-builder.js.bak +182 -0
- package/plugins/specweave/lib/hooks/reflection-prompt-builder.ts.bak +306 -0
- package/plugins/specweave/lib/hooks/reflection-storage.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/reflection-storage.d.ts.bak +64 -0
- package/plugins/specweave/lib/hooks/reflection-storage.js.bak +231 -0
- package/plugins/specweave/lib/hooks/reflection-storage.ts.bak +369 -0
- package/plugins/specweave/lib/hooks/run-self-reflection.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/run-self-reflection.d.ts.bak +43 -0
- package/plugins/specweave/lib/hooks/run-self-reflection.js.bak +132 -0
- package/plugins/specweave/lib/hooks/run-self-reflection.ts.bak +258 -0
- package/plugins/specweave/lib/hooks/sync-cache.js.bak +294 -0
- package/plugins/specweave/lib/hooks/sync-living-docs.d.js.bak +1 -0
- package/plugins/specweave/lib/hooks/sync-living-docs.d.ts.bak +27 -0
- package/plugins/specweave/lib/hooks/sync-living-docs.js.bak +339 -0
- package/plugins/specweave/lib/hooks/sync-us-tasks.js.bak +476 -0
- package/plugins/specweave/lib/hooks/translate-file.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/translate-file.d.ts.bak +59 -0
- package/plugins/specweave/lib/hooks/translate-file.js.bak +289 -0
- package/plugins/specweave/lib/hooks/translate-file.ts.bak +428 -0
- package/plugins/specweave/lib/hooks/translate-living-docs.d.js.bak +0 -0
- package/plugins/specweave/lib/hooks/translate-living-docs.d.ts.bak +13 -0
- package/plugins/specweave/lib/hooks/translate-living-docs.js.bak +119 -0
- package/plugins/specweave/lib/hooks/translate-living-docs.ts.bak +224 -0
- package/plugins/specweave/lib/hooks/update-ac-status.js.bak +51 -0
- package/plugins/specweave/lib/hooks/update-ac-status.ts.bak +103 -0
- package/plugins/specweave/lib/hooks/update-tasks-md.d.js.bak +1 -0
- package/plugins/specweave/lib/hooks/update-tasks-md.d.ts.bak +29 -0
- package/plugins/specweave/lib/hooks/update-tasks-md.js.bak +296 -0
- package/plugins/specweave/lib/hooks/update-tasks-md.ts.bak +489 -0
- package/plugins/specweave-ado/lib/ado-multi-project-sync.js +1 -0
- package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +3 -3
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +6225 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { loadAndValidateReflectionConfig } from "./reflection-config-loader";
|
|
2
|
+
import { getModifiedFiles, getModifiedFilesSummary } from "./git-diff-analyzer";
|
|
3
|
+
import { buildReflectionPrompt, estimatePromptTokens } from "./reflection-prompt-builder";
|
|
4
|
+
import { parseReflectionMarkdown, validateReflectionResult } from "./reflection-parser";
|
|
5
|
+
import { saveReflection } from "./reflection-storage";
|
|
6
|
+
import {
|
|
7
|
+
ReflectionMode,
|
|
8
|
+
ReflectionModel
|
|
9
|
+
} from "./types/reflection-types";
|
|
10
|
+
function shouldRunReflection(context) {
|
|
11
|
+
const { config, modifiedFiles } = context;
|
|
12
|
+
if (!config.enabled) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
if (config.mode === ReflectionMode.DISABLED) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (config.mode === ReflectionMode.MANUAL) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (modifiedFiles.length === 0) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
function estimateReflectionCost(model, promptTokens, responseTokens = 2e3) {
|
|
27
|
+
const pricing = {
|
|
28
|
+
[ReflectionModel.HAIKU]: { input: 0.8, output: 4 },
|
|
29
|
+
[ReflectionModel.SONNET]: { input: 3, output: 15 },
|
|
30
|
+
[ReflectionModel.OPUS]: { input: 15, output: 75 }
|
|
31
|
+
};
|
|
32
|
+
const modelPricing = pricing[model];
|
|
33
|
+
const inputCost = promptTokens / 1e6 * modelPricing.input;
|
|
34
|
+
const outputCost = responseTokens / 1e6 * modelPricing.output;
|
|
35
|
+
return inputCost + outputCost;
|
|
36
|
+
}
|
|
37
|
+
async function invokeReflectiveReviewerAgent(prompt, model) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
"Agent invocation not implemented yet. This function will be implemented in hook integration (T-008). Call this from the post-task-completion hook using the Task tool."
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
async function runSelfReflection(context) {
|
|
43
|
+
const { incrementId, taskId, modifiedFiles, config } = context;
|
|
44
|
+
const startTime = Date.now();
|
|
45
|
+
try {
|
|
46
|
+
if (!shouldRunReflection(context)) {
|
|
47
|
+
return {
|
|
48
|
+
success: true,
|
|
49
|
+
result: void 0,
|
|
50
|
+
error: void 0
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const prompt = buildReflectionPrompt({
|
|
54
|
+
taskId,
|
|
55
|
+
taskName: `Task ${taskId}`,
|
|
56
|
+
// Will be enhanced with actual task name in hook
|
|
57
|
+
modifiedFiles,
|
|
58
|
+
config,
|
|
59
|
+
incrementId,
|
|
60
|
+
includeFullDiff: false
|
|
61
|
+
// Use simplified prompt for token efficiency
|
|
62
|
+
});
|
|
63
|
+
const promptTokens = estimatePromptTokens(prompt);
|
|
64
|
+
const estimatedCost = estimateReflectionCost(config.model, promptTokens);
|
|
65
|
+
const agentResponse = await invokeReflectiveReviewerAgent(prompt, config.model);
|
|
66
|
+
const endTime = Date.now();
|
|
67
|
+
const reflectionTime = Math.round((endTime - startTime) / 1e3);
|
|
68
|
+
const reflectionResult = parseReflectionMarkdown(
|
|
69
|
+
agentResponse,
|
|
70
|
+
`Task ${taskId}`,
|
|
71
|
+
config.model,
|
|
72
|
+
reflectionTime,
|
|
73
|
+
estimatedCost
|
|
74
|
+
);
|
|
75
|
+
const fileStats = getModifiedFilesSummary(modifiedFiles);
|
|
76
|
+
reflectionResult.filesModified = {
|
|
77
|
+
count: fileStats.count,
|
|
78
|
+
linesAdded: fileStats.linesAdded,
|
|
79
|
+
linesRemoved: fileStats.linesRemoved
|
|
80
|
+
};
|
|
81
|
+
const validation = validateReflectionResult(reflectionResult);
|
|
82
|
+
if (!validation.valid) {
|
|
83
|
+
return {
|
|
84
|
+
success: false,
|
|
85
|
+
error: {
|
|
86
|
+
message: "Reflection validation failed",
|
|
87
|
+
code: "VALIDATION_ERROR",
|
|
88
|
+
details: validation.errors
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (config.storeReflections) {
|
|
93
|
+
try {
|
|
94
|
+
saveReflection(reflectionResult, incrementId, taskId);
|
|
95
|
+
} catch (storageError) {
|
|
96
|
+
console.warn(`Failed to store reflection: ${storageError.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
success: true,
|
|
101
|
+
result: reflectionResult
|
|
102
|
+
};
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
error: {
|
|
107
|
+
message: error.message || "Unknown error during reflection",
|
|
108
|
+
code: "REFLECTION_ERROR",
|
|
109
|
+
details: error.stack
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function createReflectionContext(incrementId, taskId, projectRoot) {
|
|
115
|
+
const config = loadAndValidateReflectionConfig(projectRoot);
|
|
116
|
+
const modifiedFiles = getModifiedFiles(projectRoot);
|
|
117
|
+
return {
|
|
118
|
+
incrementId,
|
|
119
|
+
taskId,
|
|
120
|
+
modifiedFiles,
|
|
121
|
+
config
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async function runReflectionAuto(incrementId, taskId, projectRoot) {
|
|
125
|
+
const context = createReflectionContext(incrementId, taskId, projectRoot);
|
|
126
|
+
return runSelfReflection(context);
|
|
127
|
+
}
|
|
128
|
+
export {
|
|
129
|
+
createReflectionContext,
|
|
130
|
+
runReflectionAuto,
|
|
131
|
+
runSelfReflection
|
|
132
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Reflection Engine
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the self-reflection process:
|
|
5
|
+
* 1. Load configuration
|
|
6
|
+
* 2. Get modified files
|
|
7
|
+
* 3. Build prompt
|
|
8
|
+
* 4. Invoke reflective-reviewer agent
|
|
9
|
+
* 5. Parse response
|
|
10
|
+
* 6. Store reflection
|
|
11
|
+
*
|
|
12
|
+
* @module run-self-reflection
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { loadAndValidateReflectionConfig } from './reflection-config-loader';
|
|
16
|
+
import { getModifiedFiles, getModifiedFilesSummary } from './git-diff-analyzer';
|
|
17
|
+
import { buildReflectionPrompt, estimatePromptTokens } from './reflection-prompt-builder';
|
|
18
|
+
import { parseReflectionMarkdown, validateReflectionResult } from './reflection-parser';
|
|
19
|
+
import { saveReflection } from './reflection-storage';
|
|
20
|
+
import {
|
|
21
|
+
ReflectionContext,
|
|
22
|
+
ReflectionExecutionResult,
|
|
23
|
+
ReflectionMode,
|
|
24
|
+
ReflectionModel
|
|
25
|
+
} from './types/reflection-types';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if reflection should run for the current context
|
|
29
|
+
*
|
|
30
|
+
* @param context Reflection context with config and modified files
|
|
31
|
+
* @returns True if reflection should run
|
|
32
|
+
*/
|
|
33
|
+
function shouldRunReflection(context: ReflectionContext): boolean {
|
|
34
|
+
const { config, modifiedFiles } = context;
|
|
35
|
+
|
|
36
|
+
// Check if reflection is enabled
|
|
37
|
+
if (!config.enabled) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check mode
|
|
42
|
+
if (config.mode === ReflectionMode.DISABLED) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (config.mode === ReflectionMode.MANUAL) {
|
|
47
|
+
// Manual mode requires explicit invocation
|
|
48
|
+
// This function is only called in auto mode, so skip
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check if there are modified files to analyze
|
|
53
|
+
if (modifiedFiles.length === 0) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Estimate reflection cost based on model and prompt size
|
|
62
|
+
*
|
|
63
|
+
* @param model Reflection model (haiku, sonnet, opus)
|
|
64
|
+
* @param promptTokens Estimated prompt token count
|
|
65
|
+
* @param responseTokens Estimated response token count (default: 2000)
|
|
66
|
+
* @returns Estimated cost in USD
|
|
67
|
+
*/
|
|
68
|
+
function estimateReflectionCost(
|
|
69
|
+
model: ReflectionModel,
|
|
70
|
+
promptTokens: number,
|
|
71
|
+
responseTokens: number = 2000
|
|
72
|
+
): number {
|
|
73
|
+
// Pricing per 1M tokens (as of Nov 2025)
|
|
74
|
+
const pricing = {
|
|
75
|
+
[ReflectionModel.HAIKU]: { input: 0.80, output: 4.00 },
|
|
76
|
+
[ReflectionModel.SONNET]: { input: 3.00, output: 15.00 },
|
|
77
|
+
[ReflectionModel.OPUS]: { input: 15.00, output: 75.00 }
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const modelPricing = pricing[model];
|
|
81
|
+
const inputCost = (promptTokens / 1_000_000) * modelPricing.input;
|
|
82
|
+
const outputCost = (responseTokens / 1_000_000) * modelPricing.output;
|
|
83
|
+
|
|
84
|
+
return inputCost + outputCost;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Invoke reflective-reviewer agent to analyze code
|
|
89
|
+
*
|
|
90
|
+
* @param prompt Reflection prompt
|
|
91
|
+
* @param model Model to use (haiku, sonnet, opus)
|
|
92
|
+
* @returns Agent response (markdown reflection)
|
|
93
|
+
*/
|
|
94
|
+
async function invokeReflectiveReviewerAgent(
|
|
95
|
+
prompt: string,
|
|
96
|
+
model: ReflectionModel
|
|
97
|
+
): Promise<string> {
|
|
98
|
+
// NOTE: This is a placeholder for agent invocation
|
|
99
|
+
// In the real implementation, this would use Claude Code's Task tool
|
|
100
|
+
// to invoke the specweave:reflective-reviewer:reflective-reviewer agent
|
|
101
|
+
//
|
|
102
|
+
// For now, we'll return a mock response to enable testing
|
|
103
|
+
// The actual implementation will be in the hook integration (T-008)
|
|
104
|
+
|
|
105
|
+
throw new Error(
|
|
106
|
+
'Agent invocation not implemented yet. ' +
|
|
107
|
+
'This function will be implemented in hook integration (T-008). ' +
|
|
108
|
+
'Call this from the post-task-completion hook using the Task tool.'
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Run self-reflection analysis
|
|
114
|
+
* Main entry point for the reflection system
|
|
115
|
+
*
|
|
116
|
+
* @param context Reflection context with increment, task, and files
|
|
117
|
+
* @returns Reflection execution result (success or error)
|
|
118
|
+
*/
|
|
119
|
+
export async function runSelfReflection(
|
|
120
|
+
context: ReflectionContext
|
|
121
|
+
): Promise<ReflectionExecutionResult> {
|
|
122
|
+
const { incrementId, taskId, modifiedFiles, config } = context;
|
|
123
|
+
const startTime = Date.now();
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
// Step 1: Check if reflection should run
|
|
127
|
+
if (!shouldRunReflection(context)) {
|
|
128
|
+
return {
|
|
129
|
+
success: true,
|
|
130
|
+
result: undefined,
|
|
131
|
+
error: undefined
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Step 2: Build reflection prompt
|
|
136
|
+
const prompt = buildReflectionPrompt({
|
|
137
|
+
taskId,
|
|
138
|
+
taskName: `Task ${taskId}`, // Will be enhanced with actual task name in hook
|
|
139
|
+
modifiedFiles,
|
|
140
|
+
config,
|
|
141
|
+
incrementId,
|
|
142
|
+
includeFullDiff: false // Use simplified prompt for token efficiency
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Step 3: Estimate cost
|
|
146
|
+
const promptTokens = estimatePromptTokens(prompt);
|
|
147
|
+
const estimatedCost = estimateReflectionCost(config.model, promptTokens);
|
|
148
|
+
|
|
149
|
+
// Step 4: Invoke reflective-reviewer agent
|
|
150
|
+
const agentResponse = await invokeReflectiveReviewerAgent(prompt, config.model);
|
|
151
|
+
|
|
152
|
+
// Step 5: Parse response
|
|
153
|
+
const endTime = Date.now();
|
|
154
|
+
const reflectionTime = Math.round((endTime - startTime) / 1000); // seconds
|
|
155
|
+
|
|
156
|
+
const reflectionResult = parseReflectionMarkdown(
|
|
157
|
+
agentResponse,
|
|
158
|
+
`Task ${taskId}`,
|
|
159
|
+
config.model,
|
|
160
|
+
reflectionTime,
|
|
161
|
+
estimatedCost
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Step 6: Add file modification stats
|
|
165
|
+
const fileStats = getModifiedFilesSummary(modifiedFiles);
|
|
166
|
+
reflectionResult.filesModified = {
|
|
167
|
+
count: fileStats.count,
|
|
168
|
+
linesAdded: fileStats.linesAdded,
|
|
169
|
+
linesRemoved: fileStats.linesRemoved
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Step 7: Validate reflection result
|
|
173
|
+
const validation = validateReflectionResult(reflectionResult);
|
|
174
|
+
if (!validation.valid) {
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
error: {
|
|
178
|
+
message: 'Reflection validation failed',
|
|
179
|
+
code: 'VALIDATION_ERROR',
|
|
180
|
+
details: validation.errors
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Step 8: Store reflection (if enabled)
|
|
186
|
+
if (config.storeReflections) {
|
|
187
|
+
try {
|
|
188
|
+
saveReflection(reflectionResult, incrementId, taskId);
|
|
189
|
+
} catch (storageError: any) {
|
|
190
|
+
// Storage failure shouldn't fail the entire reflection
|
|
191
|
+
console.warn(`Failed to store reflection: ${storageError.message}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Step 9: Return success
|
|
196
|
+
return {
|
|
197
|
+
success: true,
|
|
198
|
+
result: reflectionResult
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
} catch (error: any) {
|
|
202
|
+
// Graceful degradation: Return error but don't throw
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
error: {
|
|
206
|
+
message: error.message || 'Unknown error during reflection',
|
|
207
|
+
code: 'REFLECTION_ERROR',
|
|
208
|
+
details: error.stack
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Create reflection context from hook environment
|
|
216
|
+
* Helper function for hook integration
|
|
217
|
+
*
|
|
218
|
+
* @param incrementId Increment identifier
|
|
219
|
+
* @param taskId Task identifier
|
|
220
|
+
* @param projectRoot Project root directory (optional, auto-detected)
|
|
221
|
+
* @returns Reflection context ready for runSelfReflection
|
|
222
|
+
*/
|
|
223
|
+
export function createReflectionContext(
|
|
224
|
+
incrementId: string,
|
|
225
|
+
taskId: string,
|
|
226
|
+
projectRoot?: string
|
|
227
|
+
): ReflectionContext {
|
|
228
|
+
// Load and validate configuration
|
|
229
|
+
const config = loadAndValidateReflectionConfig(projectRoot);
|
|
230
|
+
|
|
231
|
+
// Get modified files from git
|
|
232
|
+
const modifiedFiles = getModifiedFiles(projectRoot);
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
incrementId,
|
|
236
|
+
taskId,
|
|
237
|
+
modifiedFiles,
|
|
238
|
+
config
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Run reflection with automatic context creation
|
|
244
|
+
* Convenience function for hook integration
|
|
245
|
+
*
|
|
246
|
+
* @param incrementId Increment identifier
|
|
247
|
+
* @param taskId Task identifier
|
|
248
|
+
* @param projectRoot Project root directory (optional)
|
|
249
|
+
* @returns Reflection execution result
|
|
250
|
+
*/
|
|
251
|
+
export async function runReflectionAuto(
|
|
252
|
+
incrementId: string,
|
|
253
|
+
taskId: string,
|
|
254
|
+
projectRoot?: string
|
|
255
|
+
): Promise<ReflectionExecutionResult> {
|
|
256
|
+
const context = createReflectionContext(incrementId, taskId, projectRoot);
|
|
257
|
+
return runSelfReflection(context);
|
|
258
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Sync Performance Cache Module
|
|
4
|
+
*
|
|
5
|
+
* Provides caching layer for living docs sync to meet <500ms target.
|
|
6
|
+
* Caches:
|
|
7
|
+
* - Parsed tasks.md content
|
|
8
|
+
* - File modification timestamps
|
|
9
|
+
* - US-Task mappings
|
|
10
|
+
*
|
|
11
|
+
* Part of increment 0047-us-task-linkage (T-012).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs-extra';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import crypto from 'crypto';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* In-memory cache with TTL
|
|
20
|
+
*/
|
|
21
|
+
class SyncCache {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.cache = new Map();
|
|
24
|
+
this.ttl = 60000; // 60 seconds default TTL
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get cached value
|
|
29
|
+
*
|
|
30
|
+
* @param {string} key - Cache key
|
|
31
|
+
* @returns {any|null} Cached value or null if expired/missing
|
|
32
|
+
*/
|
|
33
|
+
get(key) {
|
|
34
|
+
const entry = this.cache.get(key);
|
|
35
|
+
|
|
36
|
+
if (!entry) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if expired
|
|
41
|
+
if (Date.now() > entry.expiry) {
|
|
42
|
+
this.cache.delete(key);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return entry.value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Set cached value
|
|
51
|
+
*
|
|
52
|
+
* @param {string} key - Cache key
|
|
53
|
+
* @param {any} value - Value to cache
|
|
54
|
+
* @param {number} ttl - Time to live in milliseconds (optional)
|
|
55
|
+
*/
|
|
56
|
+
set(key, value, ttl = this.ttl) {
|
|
57
|
+
this.cache.set(key, {
|
|
58
|
+
value,
|
|
59
|
+
expiry: Date.now() + ttl
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Invalidate cache entry
|
|
65
|
+
*
|
|
66
|
+
* @param {string} key - Cache key
|
|
67
|
+
*/
|
|
68
|
+
invalidate(key) {
|
|
69
|
+
this.cache.delete(key);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Clear all cache
|
|
74
|
+
*/
|
|
75
|
+
clear() {
|
|
76
|
+
this.cache.clear();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get cache size
|
|
81
|
+
*
|
|
82
|
+
* @returns {number} Number of cached entries
|
|
83
|
+
*/
|
|
84
|
+
size() {
|
|
85
|
+
return this.cache.size;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Global cache instance
|
|
90
|
+
const globalCache = new SyncCache();
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get file hash for change detection
|
|
94
|
+
*
|
|
95
|
+
* @param {string} filePath - Path to file
|
|
96
|
+
* @returns {string} SHA256 hash of file content
|
|
97
|
+
*/
|
|
98
|
+
export function getFileHash(filePath) {
|
|
99
|
+
try {
|
|
100
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
101
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get cached parsed tasks.md
|
|
109
|
+
*
|
|
110
|
+
* @param {string} tasksPath - Path to tasks.md
|
|
111
|
+
* @param {Function} parser - Parser function to call if cache miss
|
|
112
|
+
* @returns {any} Parsed tasks (from cache or fresh)
|
|
113
|
+
*/
|
|
114
|
+
export function getCachedTasks(tasksPath, parser) {
|
|
115
|
+
// Generate cache key from file path + hash
|
|
116
|
+
const fileHash = getFileHash(tasksPath);
|
|
117
|
+
|
|
118
|
+
if (!fileHash) {
|
|
119
|
+
// File doesn't exist, call parser directly
|
|
120
|
+
return parser(tasksPath);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const cacheKey = `tasks:${tasksPath}:${fileHash}`;
|
|
124
|
+
|
|
125
|
+
// Try to get from cache
|
|
126
|
+
const cached = globalCache.get(cacheKey);
|
|
127
|
+
|
|
128
|
+
if (cached) {
|
|
129
|
+
// Cache hit
|
|
130
|
+
return cached;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Cache miss - parse and cache
|
|
134
|
+
const parsed = parser(tasksPath);
|
|
135
|
+
globalCache.set(cacheKey, parsed);
|
|
136
|
+
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get cached US file metadata
|
|
142
|
+
*
|
|
143
|
+
* @param {string} usFilePath - Path to US file
|
|
144
|
+
* @returns {object|null} Metadata object or null if file doesn't exist
|
|
145
|
+
*/
|
|
146
|
+
export function getCachedUSMetadata(usFilePath) {
|
|
147
|
+
try {
|
|
148
|
+
const stats = fs.statSync(usFilePath);
|
|
149
|
+
const cacheKey = `us-metadata:${usFilePath}`;
|
|
150
|
+
|
|
151
|
+
const cached = globalCache.get(cacheKey);
|
|
152
|
+
|
|
153
|
+
if (cached && cached.mtime === stats.mtimeMs) {
|
|
154
|
+
// File hasn't changed, return cached metadata
|
|
155
|
+
return cached.metadata;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// File changed or not in cache - read and cache
|
|
159
|
+
const content = fs.readFileSync(usFilePath, 'utf-8');
|
|
160
|
+
const metadata = {
|
|
161
|
+
mtime: stats.mtimeMs,
|
|
162
|
+
metadata: {
|
|
163
|
+
path: usFilePath,
|
|
164
|
+
size: content.length,
|
|
165
|
+
tasksSectionExists: content.includes('## Tasks'),
|
|
166
|
+
acCount: (content.match(/- \[[x ]\] \*\*AC-US\d+-\d{2}\*\*/g) || []).length
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
globalCache.set(cacheKey, metadata);
|
|
171
|
+
|
|
172
|
+
return metadata.metadata;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Batch file operations to reduce I/O
|
|
180
|
+
*
|
|
181
|
+
* @param {Array<{path: string, content: string}>} updates - Files to update
|
|
182
|
+
* @returns {Promise<void>}
|
|
183
|
+
*/
|
|
184
|
+
export async function batchFileUpdates(updates) {
|
|
185
|
+
// Group updates by directory to optimize disk I/O
|
|
186
|
+
const updatesByDir = new Map();
|
|
187
|
+
|
|
188
|
+
updates.forEach(({ path: filePath, content }) => {
|
|
189
|
+
const dir = path.dirname(filePath);
|
|
190
|
+
|
|
191
|
+
if (!updatesByDir.has(dir)) {
|
|
192
|
+
updatesByDir.set(dir, []);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
updatesByDir.get(dir).push({ path: filePath, content });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Write files sequentially within same directory (better disk I/O)
|
|
199
|
+
for (const [dir, fileUpdates] of updatesByDir.entries()) {
|
|
200
|
+
// Ensure directory exists once per directory
|
|
201
|
+
await fs.ensureDir(dir);
|
|
202
|
+
|
|
203
|
+
// Write all files in this directory
|
|
204
|
+
await Promise.all(
|
|
205
|
+
fileUpdates.map(({ path: filePath, content }) =>
|
|
206
|
+
fs.writeFile(filePath, content, 'utf-8')
|
|
207
|
+
)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Check if sync is needed for a US file
|
|
214
|
+
*
|
|
215
|
+
* @param {string} usFilePath - Path to US file
|
|
216
|
+
* @param {Array} tasks - Tasks for this US
|
|
217
|
+
* @param {string} tasksPath - Path to tasks.md
|
|
218
|
+
* @returns {boolean} True if sync is needed
|
|
219
|
+
*/
|
|
220
|
+
export function needsSync(usFilePath, tasks, tasksPath) {
|
|
221
|
+
try {
|
|
222
|
+
// Check if US file exists
|
|
223
|
+
if (!fs.existsSync(usFilePath)) {
|
|
224
|
+
return false; // File doesn't exist, can't sync
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Get file modification times
|
|
228
|
+
const usStats = fs.statSync(usFilePath);
|
|
229
|
+
const tasksStats = fs.statSync(tasksPath);
|
|
230
|
+
|
|
231
|
+
// If tasks.md is newer than US file, sync is needed
|
|
232
|
+
if (tasksStats.mtimeMs > usStats.mtimeMs) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check cache for last sync result
|
|
237
|
+
const cacheKey = `sync-result:${usFilePath}`;
|
|
238
|
+
const cached = globalCache.get(cacheKey);
|
|
239
|
+
|
|
240
|
+
if (cached) {
|
|
241
|
+
// Compare task list with cached
|
|
242
|
+
const currentTaskIds = tasks.map(t => t.id).sort().join(',');
|
|
243
|
+
const cachedTaskIds = cached.taskIds;
|
|
244
|
+
|
|
245
|
+
if (currentTaskIds === cachedTaskIds) {
|
|
246
|
+
// Task list unchanged, no sync needed
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Default: sync is needed
|
|
252
|
+
return true;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
// If error checking, assume sync is needed
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Record sync result for incremental sync
|
|
261
|
+
*
|
|
262
|
+
* @param {string} usFilePath - Path to US file
|
|
263
|
+
* @param {Array} tasks - Tasks that were synced
|
|
264
|
+
*/
|
|
265
|
+
export function recordSync(usFilePath, tasks) {
|
|
266
|
+
const cacheKey = `sync-result:${usFilePath}`;
|
|
267
|
+
const taskIds = tasks.map(t => t.id).sort().join(',');
|
|
268
|
+
|
|
269
|
+
globalCache.set(cacheKey, {
|
|
270
|
+
taskIds,
|
|
271
|
+
timestamp: Date.now()
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get cache statistics
|
|
277
|
+
*
|
|
278
|
+
* @returns {object} Cache stats
|
|
279
|
+
*/
|
|
280
|
+
export function getCacheStats() {
|
|
281
|
+
return {
|
|
282
|
+
size: globalCache.size(),
|
|
283
|
+
entries: Array.from(globalCache.cache.keys())
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Clear cache (for testing)
|
|
289
|
+
*/
|
|
290
|
+
export function clearCache() {
|
|
291
|
+
globalCache.clear();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export default globalCache;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SpecWeave Living Docs Auto-Sync
|
|
4
|
+
*
|
|
5
|
+
* Automatically syncs living documentation after task completion.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node dist/hooks/lib/sync-living-docs.js <incrementId>
|
|
9
|
+
*
|
|
10
|
+
* Example:
|
|
11
|
+
* node dist/hooks/lib/sync-living-docs.js 0006-llm-native-i18n
|
|
12
|
+
*
|
|
13
|
+
* What it does:
|
|
14
|
+
* 1. Checks if sync_living_docs enabled in config
|
|
15
|
+
* 2. Detects changed docs via git diff
|
|
16
|
+
* 3. Invokes /sync-docs update command (future implementation)
|
|
17
|
+
* 4. Logs sync actions
|
|
18
|
+
*
|
|
19
|
+
* @author SpecWeave Team
|
|
20
|
+
* @version 1.0.0
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Main function - sync living docs for given increment
|
|
24
|
+
*/
|
|
25
|
+
declare function syncLivingDocs(incrementId: string): Promise<void>;
|
|
26
|
+
export { syncLivingDocs };
|
|
27
|
+
//# sourceMappingURL=sync-living-docs.d.ts.map
|