wogiflow 2.9.0 → 2.10.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/.claude/commands/wogi-bug.md +49 -4
- package/.claude/commands/wogi-decide.md +25 -0
- package/.claude/commands/wogi-learn.md +12 -5
- package/.claude/commands/wogi-start-continuation.md +84 -0
- package/.claude/commands/wogi-start.md +94 -3
- package/.claude/commands/wogi-statusline-setup.md +18 -5
- package/.claude/docs/claude-code-compatibility.md +77 -1
- package/.claude/docs/explore-agents.md +21 -5
- package/lib/workspace-gates.js +149 -0
- package/package.json +1 -1
- package/scripts/flow-config-defaults.js +7 -1
- package/scripts/flow-context-estimator.js +26 -4
- package/scripts/flow-decision-authority.js +359 -0
- package/scripts/flow-health.js +180 -0
- package/scripts/flow-hypothesis-generator.js +63 -0
- package/scripts/flow-log-manager.js +38 -0
- package/scripts/flow-memory-db.js +53 -2
- package/scripts/flow-section-resolver.js +47 -0
- package/scripts/flow-session-state.js +37 -6
- package/scripts/flow-standards-gate.js +138 -5
- package/scripts/flow-statusline-setup.js +137 -20
- package/scripts/hooks/core/task-completed.js +77 -0
- package/scripts/hooks/entry/claude-code/session-start.js +8 -1
|
@@ -15,7 +15,8 @@ const {
|
|
|
15
15
|
readFile,
|
|
16
16
|
safeJsonParse,
|
|
17
17
|
getConfig,
|
|
18
|
-
color
|
|
18
|
+
color,
|
|
19
|
+
isPathWithinProject
|
|
19
20
|
} = require('./flow-utils');
|
|
20
21
|
|
|
21
22
|
const {
|
|
@@ -56,9 +57,9 @@ function loadTaskContext(taskId) {
|
|
|
56
57
|
return null;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
// Load spec if available
|
|
60
|
+
// Load spec if available (validate path is within project for defense-in-depth)
|
|
60
61
|
let spec = null;
|
|
61
|
-
if (task.specPath && fileExists(task.specPath)) {
|
|
62
|
+
if (task.specPath && isPathWithinProject(path.resolve(task.specPath)) && fileExists(task.specPath)) {
|
|
62
63
|
spec = readFile(task.specPath, '');
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -245,9 +246,33 @@ function runTaskStandardsCheck(taskContext, files, options = {}) {
|
|
|
245
246
|
preventionPrompts = standardsLearner.getPreventionPrompts(taskType, files.map(f => f.path));
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
// Constructor-to-test-mock drift detection
|
|
250
|
+
// Runs on any task that changes service/controller files
|
|
251
|
+
let constructorMockDrift = null;
|
|
252
|
+
const changedFilePaths = files.map(f => f.path);
|
|
253
|
+
const hasServiceFiles = changedFilePaths.some(f =>
|
|
254
|
+
/\.(service|controller|guard|interceptor|resolver|middleware|provider)\./i.test(f)
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (hasServiceFiles) {
|
|
258
|
+
constructorMockDrift = checkConstructorMockDrift(changedFilePaths);
|
|
259
|
+
if (constructorMockDrift.driftDetected) {
|
|
260
|
+
// Add drift violations to the main violations list
|
|
261
|
+
for (const drift of constructorMockDrift.drifts) {
|
|
262
|
+
results.violations.push({
|
|
263
|
+
type: 'constructor-mock-drift',
|
|
264
|
+
file: drift.sourceFile,
|
|
265
|
+
message: `Constructor changed but test mock not updated: ${drift.testFile} missing mocks for [${drift.missingInTest.join(', ')}]`,
|
|
266
|
+
severity: drift.severity,
|
|
267
|
+
suggestion: `Update the test file's mock providers to include: ${drift.missingInTest.join(', ')}`
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
248
273
|
return {
|
|
249
274
|
...results,
|
|
250
|
-
blocked: shouldBlock,
|
|
275
|
+
blocked: mode === 'block' && (shouldBlock || (constructorMockDrift?.driftDetected ?? false)),
|
|
251
276
|
mode,
|
|
252
277
|
taskType,
|
|
253
278
|
taskId: taskContext?.id,
|
|
@@ -257,7 +282,8 @@ function runTaskStandardsCheck(taskContext, files, options = {}) {
|
|
|
257
282
|
reuseCandidateContext,
|
|
258
283
|
aiAsJudge,
|
|
259
284
|
learningResults,
|
|
260
|
-
preventionPrompts
|
|
285
|
+
preventionPrompts,
|
|
286
|
+
constructorMockDrift
|
|
261
287
|
};
|
|
262
288
|
}
|
|
263
289
|
|
|
@@ -504,6 +530,112 @@ Examples:
|
|
|
504
530
|
process.exit(results.blocked ? 1 : 0);
|
|
505
531
|
}
|
|
506
532
|
|
|
533
|
+
// ============================================================================
|
|
534
|
+
// Constructor-to-Test-Mock Drift Detection
|
|
535
|
+
// ============================================================================
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Detect constructor signature changes in diff and verify corresponding test
|
|
539
|
+
* mock/provider setups match the new signature.
|
|
540
|
+
*
|
|
541
|
+
* When a constructor adds/removes dependency injection parameters, the test
|
|
542
|
+
* mock providers must be updated too. This check identifies the drift.
|
|
543
|
+
*
|
|
544
|
+
* Source: Backend mistake #4 — constructor changes forgotten in tests 6 times.
|
|
545
|
+
*
|
|
546
|
+
* @param {string[]} changedFiles - List of changed file paths
|
|
547
|
+
* @returns {{ driftDetected: boolean, drifts: Object[], message: string }}
|
|
548
|
+
*/
|
|
549
|
+
function checkConstructorMockDrift(changedFiles) {
|
|
550
|
+
const drifts = [];
|
|
551
|
+
|
|
552
|
+
for (const filePath of changedFiles) {
|
|
553
|
+
// Only check TypeScript service/controller files (not test files themselves)
|
|
554
|
+
if (!/\.(ts|tsx)$/.test(filePath)) continue;
|
|
555
|
+
if (/\.spec\.|\.test\.|__tests__/.test(filePath)) continue;
|
|
556
|
+
if (!/\.(service|controller|guard|interceptor|resolver|middleware|provider)\./i.test(filePath)) continue;
|
|
557
|
+
|
|
558
|
+
// Read the file to find constructor parameters
|
|
559
|
+
let content;
|
|
560
|
+
try {
|
|
561
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
562
|
+
} catch (_err) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Extract constructor parameters
|
|
567
|
+
const constructorMatch = content.match(/constructor\s*\(([\s\S]*?)\)\s*\{/);
|
|
568
|
+
if (!constructorMatch) continue;
|
|
569
|
+
|
|
570
|
+
const params = constructorMatch[1]
|
|
571
|
+
.split(',')
|
|
572
|
+
.map(p => p.trim())
|
|
573
|
+
.filter(p => p.length > 0)
|
|
574
|
+
.map(p => {
|
|
575
|
+
// Extract parameter name and type: "private readonly userService: UserService"
|
|
576
|
+
const parts = p.replace(/^(private|protected|public|readonly)\s+/g, '').trim();
|
|
577
|
+
const nameMatch = parts.match(/(\w+)\s*:/);
|
|
578
|
+
return nameMatch ? nameMatch[1] : parts.split(/\s/)[0];
|
|
579
|
+
})
|
|
580
|
+
.filter(Boolean);
|
|
581
|
+
|
|
582
|
+
if (params.length === 0) continue;
|
|
583
|
+
|
|
584
|
+
// Find corresponding test file(s)
|
|
585
|
+
const dir = path.dirname(filePath);
|
|
586
|
+
const baseName = path.basename(filePath, path.extname(filePath));
|
|
587
|
+
const possibleTestFiles = [
|
|
588
|
+
path.join(dir, `${baseName}.spec.ts`),
|
|
589
|
+
path.join(dir, `${baseName}.test.ts`),
|
|
590
|
+
path.join(dir, '__tests__', `${baseName}.spec.ts`),
|
|
591
|
+
path.join(dir, '__tests__', `${baseName}.test.ts`)
|
|
592
|
+
];
|
|
593
|
+
|
|
594
|
+
for (const testPath of possibleTestFiles) {
|
|
595
|
+
let testContent;
|
|
596
|
+
try {
|
|
597
|
+
testContent = fs.readFileSync(testPath, 'utf-8');
|
|
598
|
+
} catch (_err) {
|
|
599
|
+
continue; // Test file doesn't exist — separate concern
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Check if test has providers/mocks for each constructor param
|
|
603
|
+
const missingMocks = [];
|
|
604
|
+
for (const param of params) {
|
|
605
|
+
// Look for the parameter name in providers array or mock setup
|
|
606
|
+
// Common patterns: { provide: ParamType, useValue: ... }
|
|
607
|
+
// ParamType as jest.Mocked<ParamType>
|
|
608
|
+
// mock(ParamType)
|
|
609
|
+
const paramPattern = new RegExp(param, 'i');
|
|
610
|
+
if (!paramPattern.test(testContent)) {
|
|
611
|
+
missingMocks.push(param);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (missingMocks.length > 0) {
|
|
616
|
+
drifts.push({
|
|
617
|
+
sourceFile: filePath,
|
|
618
|
+
testFile: testPath,
|
|
619
|
+
constructorParams: params,
|
|
620
|
+
missingInTest: missingMocks,
|
|
621
|
+
severity: 'must-fix'
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
driftDetected: drifts.length > 0,
|
|
629
|
+
drifts,
|
|
630
|
+
message: drifts.length > 0
|
|
631
|
+
? `Constructor-to-test-mock drift detected in ${drifts.length} file(s). ` +
|
|
632
|
+
drifts.map(d =>
|
|
633
|
+
`${d.sourceFile} → ${d.testFile}: missing mocks for [${d.missingInTest.join(', ')}]`
|
|
634
|
+
).join('; ')
|
|
635
|
+
: 'No constructor-mock drift detected'
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
507
639
|
// ============================================================================
|
|
508
640
|
// Exports
|
|
509
641
|
// ============================================================================
|
|
@@ -513,6 +645,7 @@ module.exports = {
|
|
|
513
645
|
extractFilesToChange,
|
|
514
646
|
inferTaskType,
|
|
515
647
|
runTaskStandardsCheck,
|
|
648
|
+
checkConstructorMockDrift,
|
|
516
649
|
formatViolationsForRetry,
|
|
517
650
|
formatReuseCandidatesForAI,
|
|
518
651
|
hasPassedStandards,
|
|
@@ -40,11 +40,17 @@ const FORMATS = {
|
|
|
40
40
|
},
|
|
41
41
|
detailed: {
|
|
42
42
|
name: 'Detailed',
|
|
43
|
-
description: 'Full info including skill',
|
|
44
|
-
format: '{{#if task}}[{{task.id}}] {{task.title}} | {{/if}}{{model}} | {{context_window.used_percentage}}% used{{#if skill}} | {{skill}}{{/if}}'
|
|
43
|
+
description: 'Full info including skill and worktree',
|
|
44
|
+
format: '{{#if workspace.git_worktree}}[WT] {{/if}}{{#if task}}[{{task.id}}] {{task.title}} | {{/if}}{{model}} | {{context_window.used_percentage}}% used{{#if skill}} | {{skill}}{{/if}}'
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
// Default refresh interval (seconds) — re-runs status line every N seconds
|
|
49
|
+
// so live values like task.id, context_window, and skill stay current.
|
|
50
|
+
// Available in Claude Code 2.1.97+. 0 disables auto-refresh.
|
|
51
|
+
const DEFAULT_REFRESH_INTERVAL = 5;
|
|
52
|
+
const MAX_REFRESH_INTERVAL = 3600;
|
|
53
|
+
|
|
48
54
|
// Claude settings file location
|
|
49
55
|
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
|
|
50
56
|
|
|
@@ -71,6 +77,41 @@ function saveClaudeSettings(settings) {
|
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Parse and validate a refresh interval string.
|
|
82
|
+
* Returns the integer value, or null if invalid.
|
|
83
|
+
* Accepts 0 (disable) through MAX_REFRESH_INTERVAL.
|
|
84
|
+
*/
|
|
85
|
+
function parseRefreshInterval(arg) {
|
|
86
|
+
if (arg === undefined || arg === null || arg === '') return null;
|
|
87
|
+
const n = Number(arg);
|
|
88
|
+
if (!Number.isInteger(n) || n < 0 || n > MAX_REFRESH_INTERVAL) return null;
|
|
89
|
+
return n;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build a statusLine config that preserves any existing fields the caller
|
|
94
|
+
* isn't explicitly overriding. This prevents `--format X` from wiping out
|
|
95
|
+
* a user's previously-configured refreshInterval (and vice versa).
|
|
96
|
+
*
|
|
97
|
+
* `refreshInterval` semantics:
|
|
98
|
+
* - undefined → don't touch existing value
|
|
99
|
+
* - 0 → delete the field (disable auto-refresh)
|
|
100
|
+
* - N > 0 → set to N seconds
|
|
101
|
+
*/
|
|
102
|
+
function buildStatusLine(existing, { format, refreshInterval } = {}) {
|
|
103
|
+
const next = { ...(existing || {}), enabled: true };
|
|
104
|
+
if (format !== undefined) next.format = format;
|
|
105
|
+
if (refreshInterval !== undefined) {
|
|
106
|
+
if (refreshInterval === 0) {
|
|
107
|
+
delete next.refreshInterval;
|
|
108
|
+
} else {
|
|
109
|
+
next.refreshInterval = refreshInterval;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return next;
|
|
113
|
+
}
|
|
114
|
+
|
|
74
115
|
function showCurrentConfig() {
|
|
75
116
|
const settings = loadClaudeSettings();
|
|
76
117
|
const statusLine = settings.statusLine || {};
|
|
@@ -82,6 +123,11 @@ function showCurrentConfig() {
|
|
|
82
123
|
} else if (statusLine.format) {
|
|
83
124
|
console.log(`${colors.dim}Status: ${colors.green}Enabled${colors.reset}`);
|
|
84
125
|
console.log(`${colors.dim}Format:${colors.reset} ${statusLine.format}`);
|
|
126
|
+
if (statusLine.refreshInterval) {
|
|
127
|
+
console.log(`${colors.dim}Refresh:${colors.reset} every ${statusLine.refreshInterval}s ${colors.dim}(Claude Code 2.1.97+)${colors.reset}`);
|
|
128
|
+
} else {
|
|
129
|
+
console.log(`${colors.dim}Refresh:${colors.reset} ${colors.yellow}off${colors.reset} ${colors.dim}(updates on prompt only)${colors.reset}`);
|
|
130
|
+
}
|
|
85
131
|
} else {
|
|
86
132
|
console.log(`${colors.dim}Status: ${colors.yellow}Not configured${colors.reset}`);
|
|
87
133
|
}
|
|
@@ -120,13 +166,29 @@ async function interactiveSetup() {
|
|
|
120
166
|
process.exit(1);
|
|
121
167
|
}
|
|
122
168
|
|
|
169
|
+
const refreshAnswer = await question(
|
|
170
|
+
`\nAuto-refresh interval in seconds (0 = off, blank = ${DEFAULT_REFRESH_INTERVAL}, requires Claude Code 2.1.97+): `
|
|
171
|
+
);
|
|
172
|
+
let refreshInterval;
|
|
173
|
+
if (refreshAnswer.trim() === '') {
|
|
174
|
+
refreshInterval = DEFAULT_REFRESH_INTERVAL;
|
|
175
|
+
} else {
|
|
176
|
+
refreshInterval = parseRefreshInterval(refreshAnswer.trim());
|
|
177
|
+
if (refreshInterval === null) {
|
|
178
|
+
errorMsg(`Refresh interval must be an integer between 0 and ${MAX_REFRESH_INTERVAL}`);
|
|
179
|
+
rl.close();
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
123
184
|
const settings = loadClaudeSettings();
|
|
124
|
-
settings.statusLine = {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
};
|
|
185
|
+
settings.statusLine = buildStatusLine(settings.statusLine, {
|
|
186
|
+
format: FORMATS[selectedFormat].format,
|
|
187
|
+
refreshInterval
|
|
188
|
+
});
|
|
128
189
|
|
|
129
|
-
const
|
|
190
|
+
const refreshLabel = refreshInterval === 0 ? 'no auto-refresh' : `refresh every ${refreshInterval}s`;
|
|
191
|
+
const confirm = await question(`\nApply "${FORMATS[selectedFormat].name}" format with ${refreshLabel}? (y/N): `);
|
|
130
192
|
|
|
131
193
|
if (confirm.toLowerCase() === 'y') {
|
|
132
194
|
if (saveClaudeSettings(settings)) {
|
|
@@ -151,21 +213,31 @@ Wogi Flow - Status Line Setup
|
|
|
151
213
|
Configure Claude Code's status line to show task and context info.
|
|
152
214
|
|
|
153
215
|
Usage:
|
|
154
|
-
flow statusline-setup
|
|
155
|
-
flow statusline-setup --format X
|
|
156
|
-
flow statusline-setup --
|
|
157
|
-
|
|
158
|
-
flow statusline-setup --
|
|
216
|
+
flow statusline-setup Interactive setup
|
|
217
|
+
flow statusline-setup --format X Set format directly
|
|
218
|
+
flow statusline-setup --refresh-interval N
|
|
219
|
+
Set auto-refresh interval (seconds, 0 to disable)
|
|
220
|
+
flow statusline-setup --show Show current config
|
|
221
|
+
flow statusline-setup --disable Disable status line
|
|
222
|
+
flow statusline-setup --formats List available formats
|
|
159
223
|
|
|
160
224
|
Formats:
|
|
161
225
|
minimal - Model + context %
|
|
162
226
|
compact - Task ID + model + context %
|
|
163
227
|
standard - Task ID + model + labeled context (recommended)
|
|
164
|
-
detailed -
|
|
228
|
+
detailed - Worktree + task + model + context % + skill
|
|
229
|
+
|
|
230
|
+
Refresh interval (Claude Code 2.1.97+):
|
|
231
|
+
Re-runs the status line every N seconds so live values like task ID,
|
|
232
|
+
context %, and active skill stay current between prompts.
|
|
233
|
+
Default when configured via this tool: ${DEFAULT_REFRESH_INTERVAL}s. Range: 0–${MAX_REFRESH_INTERVAL}.
|
|
234
|
+
0 disables auto-refresh (status line only updates on prompt).
|
|
165
235
|
|
|
166
236
|
Examples:
|
|
167
237
|
flow statusline-setup --format standard
|
|
168
|
-
flow statusline-setup --format detailed
|
|
238
|
+
flow statusline-setup --format detailed --refresh-interval 5
|
|
239
|
+
flow statusline-setup --refresh-interval 10
|
|
240
|
+
flow statusline-setup --refresh-interval 0
|
|
169
241
|
`);
|
|
170
242
|
process.exit(0);
|
|
171
243
|
}
|
|
@@ -182,13 +254,29 @@ Examples:
|
|
|
182
254
|
|
|
183
255
|
if (args.includes('--disable')) {
|
|
184
256
|
const settings = loadClaudeSettings();
|
|
185
|
-
settings.statusLine = { enabled: false };
|
|
257
|
+
settings.statusLine = { ...(settings.statusLine || {}), enabled: false };
|
|
186
258
|
if (saveClaudeSettings(settings)) {
|
|
187
259
|
success('Status line disabled.');
|
|
188
260
|
}
|
|
189
261
|
process.exit(0);
|
|
190
262
|
}
|
|
191
263
|
|
|
264
|
+
// Parse --refresh-interval (may be combined with --format, or used alone)
|
|
265
|
+
const refreshIndex = args.indexOf('--refresh-interval');
|
|
266
|
+
let cliRefreshInterval;
|
|
267
|
+
if (refreshIndex >= 0) {
|
|
268
|
+
const refreshArg = args[refreshIndex + 1];
|
|
269
|
+
if (refreshArg === undefined || refreshArg.startsWith('--')) {
|
|
270
|
+
errorMsg('--refresh-interval requires a value (integer between 0 and ' + MAX_REFRESH_INTERVAL + ')');
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
cliRefreshInterval = parseRefreshInterval(refreshArg);
|
|
274
|
+
if (cliRefreshInterval === null) {
|
|
275
|
+
errorMsg(`--refresh-interval must be an integer between 0 and ${MAX_REFRESH_INTERVAL}`);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
192
280
|
const formatIndex = args.indexOf('--format');
|
|
193
281
|
if (formatIndex >= 0) {
|
|
194
282
|
const format = args[formatIndex + 1];
|
|
@@ -198,18 +286,47 @@ Examples:
|
|
|
198
286
|
}
|
|
199
287
|
|
|
200
288
|
const settings = loadClaudeSettings();
|
|
201
|
-
settings.statusLine = {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
};
|
|
289
|
+
settings.statusLine = buildStatusLine(settings.statusLine, {
|
|
290
|
+
format: FORMATS[format].format,
|
|
291
|
+
refreshInterval: cliRefreshInterval
|
|
292
|
+
});
|
|
205
293
|
|
|
206
294
|
if (saveClaudeSettings(settings)) {
|
|
207
|
-
|
|
295
|
+
let refreshNote;
|
|
296
|
+
if (cliRefreshInterval !== undefined) {
|
|
297
|
+
refreshNote = cliRefreshInterval === 0 ? ' (auto-refresh off)' : ` (refresh every ${cliRefreshInterval}s)`;
|
|
298
|
+
} else if (settings.statusLine.refreshInterval) {
|
|
299
|
+
refreshNote = ` (refresh every ${settings.statusLine.refreshInterval}s preserved)`;
|
|
300
|
+
} else {
|
|
301
|
+
refreshNote = '';
|
|
302
|
+
}
|
|
303
|
+
success(`Status line configured with "${format}" format${refreshNote}.`);
|
|
208
304
|
console.log(`${colors.dim}Restart Claude Code to see changes.${colors.reset}`);
|
|
209
305
|
}
|
|
210
306
|
process.exit(0);
|
|
211
307
|
}
|
|
212
308
|
|
|
309
|
+
// Standalone --refresh-interval (no --format) — update only the interval
|
|
310
|
+
if (cliRefreshInterval !== undefined) {
|
|
311
|
+
const settings = loadClaudeSettings();
|
|
312
|
+
if (!settings.statusLine || !settings.statusLine.format) {
|
|
313
|
+
errorMsg('No status line is configured yet. Run with --format <name> first, or use interactive setup.');
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
settings.statusLine = buildStatusLine(settings.statusLine, {
|
|
317
|
+
refreshInterval: cliRefreshInterval
|
|
318
|
+
});
|
|
319
|
+
if (saveClaudeSettings(settings)) {
|
|
320
|
+
success(
|
|
321
|
+
cliRefreshInterval === 0
|
|
322
|
+
? 'Status line auto-refresh disabled.'
|
|
323
|
+
: `Status line refresh interval set to ${cliRefreshInterval}s.`
|
|
324
|
+
);
|
|
325
|
+
console.log(`${colors.dim}Requires Claude Code 2.1.97+. Restart Claude Code to see changes.${colors.reset}`);
|
|
326
|
+
}
|
|
327
|
+
process.exit(0);
|
|
328
|
+
}
|
|
329
|
+
|
|
213
330
|
// Default: interactive mode
|
|
214
331
|
await interactiveSetup();
|
|
215
332
|
}
|
|
@@ -311,6 +311,83 @@ async function handleTaskCompleted(input) {
|
|
|
311
311
|
// Workspace notifications are handled by the Stop hook (via HTTP to manager port).
|
|
312
312
|
// Removed duplicate file-based notification here to prevent double messages (finding-004).
|
|
313
313
|
|
|
314
|
+
// Compound from success — capture positive patterns (fire-and-forget)
|
|
315
|
+
if (result.completed) {
|
|
316
|
+
try {
|
|
317
|
+
const config = getConfig();
|
|
318
|
+
if (config.skillLearning?.enabled) {
|
|
319
|
+
const { writeToFeedbackPatterns } = require('../../flow-learning-orchestrator');
|
|
320
|
+
const taskType = completedTask.type || 'unknown';
|
|
321
|
+
const changedFiles = input.changedFiles || [];
|
|
322
|
+
const criteriaCount = input.scenarioCount || completedTask.criteria || 0;
|
|
323
|
+
const firstPass = input.firstAttemptPass !== false;
|
|
324
|
+
|
|
325
|
+
// Only record success patterns for non-trivial tasks that passed on first attempt
|
|
326
|
+
if (firstPass && changedFiles.length >= 2 && criteriaCount >= 2) {
|
|
327
|
+
const today = new Date().toISOString().split('T')[0];
|
|
328
|
+
const filesSummary = changedFiles.slice(0, 5).map(f => path.basename(f)).join(', ');
|
|
329
|
+
const entryText = `success-pattern: ${taskType} task (${criteriaCount} criteria, ${changedFiles.length} files) completed first-pass. Files: ${filesSummary}`;
|
|
330
|
+
const tableRow = `| ${today} | ${entryText} | 1 | - | #success |`;
|
|
331
|
+
|
|
332
|
+
writeToFeedbackPatterns({
|
|
333
|
+
content: tableRow,
|
|
334
|
+
entryText,
|
|
335
|
+
caller: 'task-completed-success',
|
|
336
|
+
}).catch(() => {
|
|
337
|
+
// Non-critical
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
} catch (_err) {
|
|
342
|
+
// Non-critical — success pattern capture may not be available
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Skill learning extraction (fire-and-forget)
|
|
347
|
+
if (result.completed) {
|
|
348
|
+
try {
|
|
349
|
+
const { isLearningEnabled, extractLearningContext, matchFilesToSkills, appendLearning, discoverSkills, ensureKnowledgeDir, formatSemanticChanges } = require('../../flow-skill-learn');
|
|
350
|
+
const config = getConfig();
|
|
351
|
+
if (isLearningEnabled(config, 'task')) {
|
|
352
|
+
const changedFiles = input.changedFiles || [];
|
|
353
|
+
if (changedFiles.length > 0) {
|
|
354
|
+
const skills = discoverSkills();
|
|
355
|
+
const { matches: skillMap } = matchFilesToSkills(changedFiles, skills);
|
|
356
|
+
const context = extractLearningContext(changedFiles, 'task');
|
|
357
|
+
|
|
358
|
+
// Enrich context with task info
|
|
359
|
+
context.summary = `Task ${completedTask.id}: ${completedTask.title || ''}`;
|
|
360
|
+
context.taskType = completedTask.type || 'unknown';
|
|
361
|
+
|
|
362
|
+
for (const [skillName, matchedFiles] of skillMap) {
|
|
363
|
+
if (matchedFiles.length > 0) {
|
|
364
|
+
const skill = skills.find(s => s.name === skillName);
|
|
365
|
+
const skillDir = skill?.path;
|
|
366
|
+
if (skillDir) {
|
|
367
|
+
ensureKnowledgeDir(skillDir);
|
|
368
|
+
const entry = [
|
|
369
|
+
`### ${context.summary}`,
|
|
370
|
+
`**Type**: ${context.type} | **Trigger**: task-complete`,
|
|
371
|
+
`**Files**: ${matchedFiles.join(', ')}`,
|
|
372
|
+
];
|
|
373
|
+
if (context.semanticChanges.length > 0) {
|
|
374
|
+
entry.push(`**Changes**: ${formatSemanticChanges(context.semanticChanges).slice(0, 200)}`);
|
|
375
|
+
}
|
|
376
|
+
entry.push('');
|
|
377
|
+
appendLearning(skillDir, entry.join('\n'));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} catch (_err) {
|
|
384
|
+
// Non-critical — skill learning may not be available
|
|
385
|
+
if (process.env.DEBUG) {
|
|
386
|
+
console.error(`[Task Completed] Skill learning failed: ${_err.message}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
314
391
|
// Check pending queue — notify user if items are waiting
|
|
315
392
|
try {
|
|
316
393
|
const { getPendingCount } = require('../../flow-pending');
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const { gatherSessionContext } = require('../../core/session-context');
|
|
11
|
-
const { setCliSessionId, clearStaleCurrentTaskAsync } = require('../../../flow-session-state');
|
|
11
|
+
const { setCliSessionId, clearStaleCurrentTaskAsync, resetSessionTaskCounter } = require('../../../flow-session-state');
|
|
12
12
|
const { checkAndResetStalePhase } = require('../../core/phase-gate');
|
|
13
13
|
const { setRoutingPending } = require('../../core/routing-gate');
|
|
14
14
|
const { getConfig } = require('../../../flow-utils');
|
|
@@ -97,6 +97,13 @@ runHook('SessionStart', async ({ parsedInput }) => {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// Reset session task counter so first task uses full prompt
|
|
101
|
+
try {
|
|
102
|
+
resetSessionTaskCounter();
|
|
103
|
+
} catch (_err) {
|
|
104
|
+
// Non-blocking
|
|
105
|
+
}
|
|
106
|
+
|
|
100
107
|
try {
|
|
101
108
|
const routingResult = setRoutingPending();
|
|
102
109
|
if (process.env.DEBUG) {
|