@winspan/claude-forge 8.26.3 → 8.28.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.
Files changed (55) hide show
  1. package/dist/capability/index.d.ts +9 -0
  2. package/dist/capability/index.d.ts.map +1 -0
  3. package/dist/capability/index.js +9 -0
  4. package/dist/capability/index.js.map +1 -0
  5. package/dist/capability/methodologies/bmad.yaml +69 -0
  6. package/dist/capability/methodologies/harness-engineering.yaml +69 -0
  7. package/dist/capability/methodology-planner.d.ts +33 -0
  8. package/dist/capability/methodology-planner.d.ts.map +1 -0
  9. package/dist/capability/methodology-planner.js +178 -0
  10. package/dist/capability/methodology-planner.js.map +1 -0
  11. package/dist/capability/methodology-registry.d.ts +32 -0
  12. package/dist/capability/methodology-registry.d.ts.map +1 -0
  13. package/dist/capability/methodology-registry.js +97 -0
  14. package/dist/capability/methodology-registry.js.map +1 -0
  15. package/dist/capability/types.d.ts +68 -0
  16. package/dist/capability/types.d.ts.map +1 -0
  17. package/dist/capability/types.js +7 -0
  18. package/dist/capability/types.js.map +1 -0
  19. package/dist/core/storage/schema.sql +40 -0
  20. package/dist/core/storage/sqlite.d.ts +25 -0
  21. package/dist/core/storage/sqlite.d.ts.map +1 -1
  22. package/dist/core/storage/sqlite.js +87 -0
  23. package/dist/core/storage/sqlite.js.map +1 -1
  24. package/dist/daemon/handlers/methodology-formatter.d.ts +9 -0
  25. package/dist/daemon/handlers/methodology-formatter.d.ts.map +1 -0
  26. package/dist/daemon/handlers/methodology-formatter.js +73 -0
  27. package/dist/daemon/handlers/methodology-formatter.js.map +1 -0
  28. package/dist/daemon/handlers/post-tool-use.d.ts +9 -1
  29. package/dist/daemon/handlers/post-tool-use.d.ts.map +1 -1
  30. package/dist/daemon/handlers/post-tool-use.js +94 -2
  31. package/dist/daemon/handlers/post-tool-use.js.map +1 -1
  32. package/dist/daemon/handlers/pre-tool-use.d.ts +5 -0
  33. package/dist/daemon/handlers/pre-tool-use.d.ts.map +1 -1
  34. package/dist/daemon/handlers/pre-tool-use.js +88 -0
  35. package/dist/daemon/handlers/pre-tool-use.js.map +1 -1
  36. package/dist/daemon/handlers/user-prompt.d.ts +5 -1
  37. package/dist/daemon/handlers/user-prompt.d.ts.map +1 -1
  38. package/dist/daemon/handlers/user-prompt.js +106 -16
  39. package/dist/daemon/handlers/user-prompt.js.map +1 -1
  40. package/dist/daemon/index.d.ts.map +1 -1
  41. package/dist/daemon/index.js +8 -2
  42. package/dist/daemon/index.js.map +1 -1
  43. package/dist/daemon/routing-state.d.ts +49 -0
  44. package/dist/daemon/routing-state.d.ts.map +1 -0
  45. package/dist/daemon/routing-state.js +189 -0
  46. package/dist/daemon/routing-state.js.map +1 -0
  47. package/dist/engine/conventions/routing.yaml +5 -0
  48. package/dist/web/server.d.ts.map +1 -1
  49. package/dist/web/server.js +122 -1
  50. package/dist/web/server.js.map +1 -1
  51. package/dist/web/static/assets/index-CtylfoaN.css +1 -0
  52. package/dist/web/static/assets/index-DnaQt27h.js +388 -0
  53. package/dist/web/static/assets/index-DnaQt27h.js.map +1 -0
  54. package/dist/web/static/index.html +12 -2971
  55. package/package.json +5 -2
@@ -0,0 +1,189 @@
1
+ /**
2
+ * RoutingState — in-memory session routing state for PreToolUse enforcement
3
+ *
4
+ * Tracks which sessions have active routing decisions, so PreToolUse can
5
+ * enforce "must use Agent tool" when Claude tries to bypass the routing.
6
+ */
7
+ import { logger } from '../core/utils/logger.js';
8
+ // Auto-cleanup stale routing state after 30 minutes
9
+ const ROUTING_STATE_TTL_MS = 30 * 60 * 1000;
10
+ /**
11
+ * Global routing state manager (singleton pattern)
12
+ */
13
+ class RoutingStateManager {
14
+ state = new Map();
15
+ /**
16
+ * Record that a session has been routed to a specific agent
17
+ */
18
+ setRouting(sessionId, decision) {
19
+ try {
20
+ // Parameter validation
21
+ if (!sessionId || typeof sessionId !== 'string') {
22
+ logger.error(`[RoutingState] setRouting: invalid sessionId: ${JSON.stringify(sessionId)}`);
23
+ return;
24
+ }
25
+ if (!decision || typeof decision !== 'object') {
26
+ logger.error(`[RoutingState] setRouting: invalid decision object for session ${sessionId}`);
27
+ return;
28
+ }
29
+ if (!decision.agentName || typeof decision.agentName !== 'string') {
30
+ logger.error(`[RoutingState] setRouting: invalid agentName for session ${sessionId}: ${JSON.stringify(decision)}`);
31
+ return;
32
+ }
33
+ if (!decision.routeRequestId || typeof decision.routeRequestId !== 'string') {
34
+ logger.error(`[RoutingState] setRouting: invalid routeRequestId for session ${sessionId}: ${JSON.stringify(decision)}`);
35
+ return;
36
+ }
37
+ if (typeof decision.timestamp !== 'number' || decision.timestamp <= 0) {
38
+ logger.error(`[RoutingState] setRouting: invalid timestamp for session ${sessionId}: ${JSON.stringify(decision)}`);
39
+ return;
40
+ }
41
+ this.state.set(sessionId, decision);
42
+ logger.debug(`[RoutingState] Set routing for session ${sessionId} → ${decision.agentName}`);
43
+ }
44
+ catch (error) {
45
+ logger.error(`[RoutingState] setRouting: unexpected error for session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
46
+ }
47
+ }
48
+ /**
49
+ * Get the active routing decision for a session (if any)
50
+ * Returns null if no routing exists or if routing has expired
51
+ */
52
+ getRouting(sessionId) {
53
+ try {
54
+ // Parameter validation
55
+ if (!sessionId || typeof sessionId !== 'string') {
56
+ logger.error(`[RoutingState] getRouting: invalid sessionId: ${JSON.stringify(sessionId)}`);
57
+ return null;
58
+ }
59
+ const decision = this.state.get(sessionId);
60
+ if (!decision)
61
+ return null;
62
+ // Check if routing has expired
63
+ const age = Date.now() - decision.timestamp;
64
+ if (age > ROUTING_STATE_TTL_MS) {
65
+ logger.debug(`[RoutingState] Routing expired for session ${sessionId} (age: ${Math.round(age / 1000)}s)`);
66
+ this.state.delete(sessionId);
67
+ return null;
68
+ }
69
+ return decision;
70
+ }
71
+ catch (error) {
72
+ logger.error(`[RoutingState] getRouting: unexpected error for session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
73
+ return null;
74
+ }
75
+ }
76
+ /**
77
+ * Clear routing state for a session (called after agent is invoked or session ends)
78
+ */
79
+ clearRouting(sessionId) {
80
+ try {
81
+ // Parameter validation
82
+ if (!sessionId || typeof sessionId !== 'string') {
83
+ logger.error(`[RoutingState] clearRouting: invalid sessionId: ${JSON.stringify(sessionId)}`);
84
+ return;
85
+ }
86
+ const decision = this.state.get(sessionId);
87
+ if (decision) {
88
+ logger.debug(`[RoutingState] Cleared routing for session ${sessionId} (was: ${decision.agentName})`);
89
+ }
90
+ this.state.delete(sessionId);
91
+ }
92
+ catch (error) {
93
+ logger.error(`[RoutingState] clearRouting: unexpected error for session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
94
+ }
95
+ }
96
+ /**
97
+ * Check if a session has an active routing decision
98
+ */
99
+ hasRouting(sessionId) {
100
+ try {
101
+ // Parameter validation
102
+ if (!sessionId || typeof sessionId !== 'string') {
103
+ logger.error(`[RoutingState] hasRouting: invalid sessionId: ${JSON.stringify(sessionId)}`);
104
+ return false;
105
+ }
106
+ return this.getRouting(sessionId) !== null;
107
+ }
108
+ catch (error) {
109
+ logger.error(`[RoutingState] hasRouting: unexpected error for session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
110
+ return false;
111
+ }
112
+ }
113
+ /**
114
+ * Get statistics about current routing state (for debugging/monitoring)
115
+ */
116
+ getStats() {
117
+ try {
118
+ if (this.state.size === 0) {
119
+ return { totalSessions: 0, oldestAge: null };
120
+ }
121
+ const now = Date.now();
122
+ let oldestAge = 0;
123
+ for (const decision of this.state.values()) {
124
+ // Validate decision object
125
+ if (!decision || typeof decision.timestamp !== 'number') {
126
+ logger.warn(`[RoutingState] getStats: invalid decision object in state: ${JSON.stringify(decision)}`);
127
+ continue;
128
+ }
129
+ const age = now - decision.timestamp;
130
+ if (age > oldestAge)
131
+ oldestAge = age;
132
+ }
133
+ return {
134
+ totalSessions: this.state.size,
135
+ oldestAge: Math.round(oldestAge / 1000), // seconds
136
+ };
137
+ }
138
+ catch (error) {
139
+ logger.error(`[RoutingState] getStats: unexpected error: ${error instanceof Error ? error.message : String(error)}`);
140
+ return { totalSessions: 0, oldestAge: null };
141
+ }
142
+ }
143
+ /**
144
+ * Cleanup expired routing states (called periodically by daemon)
145
+ */
146
+ cleanup() {
147
+ try {
148
+ const now = Date.now();
149
+ let removed = 0;
150
+ for (const [sessionId, decision] of this.state.entries()) {
151
+ try {
152
+ // Validate decision object
153
+ if (!decision || typeof decision.timestamp !== 'number') {
154
+ logger.warn(`[RoutingState] cleanup: invalid decision object for session ${sessionId}, removing: ${JSON.stringify(decision)}`);
155
+ this.state.delete(sessionId);
156
+ removed++;
157
+ continue;
158
+ }
159
+ const age = now - decision.timestamp;
160
+ if (age > ROUTING_STATE_TTL_MS) {
161
+ this.state.delete(sessionId);
162
+ removed++;
163
+ logger.debug(`[RoutingState] Cleaned up expired routing for session ${sessionId}`);
164
+ }
165
+ }
166
+ catch (error) {
167
+ logger.error(`[RoutingState] cleanup: error processing session ${sessionId}, removing: ${error instanceof Error ? error.message : String(error)}`);
168
+ this.state.delete(sessionId);
169
+ removed++;
170
+ }
171
+ }
172
+ if (removed > 0) {
173
+ logger.info(`[RoutingState] Cleanup: removed ${removed} expired routing state(s)`);
174
+ }
175
+ return removed;
176
+ }
177
+ catch (error) {
178
+ logger.error(`[RoutingState] cleanup: unexpected error: ${error instanceof Error ? error.message : String(error)}`);
179
+ return 0;
180
+ }
181
+ }
182
+ }
183
+ // Singleton instance
184
+ export const routingState = new RoutingStateManager();
185
+ // Auto-cleanup every 10 minutes
186
+ setInterval(() => {
187
+ routingState.cleanup();
188
+ }, 10 * 60 * 1000).unref();
189
+ //# sourceMappingURL=routing-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing-state.js","sourceRoot":"","sources":["../../src/daemon/routing-state.ts"],"names":[],"mappings":"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"}
@@ -137,8 +137,13 @@ rules:
137
137
  - when:
138
138
  taskType: compare_solutions
139
139
  action: { type: route_to_agent, name: researcher }
140
+ # Only route "explain" when the topic is complex enough to warrant a
141
+ # researcher dive. Plain clarification questions ("what does X mean",
142
+ # "does this work?") should stay with the main Claude — routing them out
143
+ # just produces disobey events since the main session will answer inline.
140
144
  - when:
141
145
  taskType: explain
146
+ complexity: complex
142
147
  action: { type: route_to_agent, name: researcher }
143
148
 
144
149
  # -- Documentation ---------------------------------------------------------
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAS3D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,aAAa,CAAC;IACvB,UAAU,EAAE,UAAU,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,qBAAa,SAAS;IAMR,OAAO,CAAC,OAAO;IAL3B,OAAO,CAAC,GAAG,CAAsB;IACjC,OAAO,CAAC,MAAM,CAA0D;IACxE,OAAO,CAAC,MAAM,CAAC,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAC,CAAc;gBAET,OAAO,EAAE,gBAAgB;IAQ7C,OAAO,CAAC,WAAW;IAkyEb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAO5B"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAS3D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,aAAa,CAAC;IACvB,UAAU,EAAE,UAAU,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,qBAAa,SAAS;IAMR,OAAO,CAAC,OAAO;IAL3B,OAAO,CAAC,GAAG,CAAsB;IACjC,OAAO,CAAC,MAAM,CAA0D;IACxE,OAAO,CAAC,MAAM,CAAC,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAC,CAAc;gBAET,OAAO,EAAE,gBAAgB;IAQ7C,OAAO,CAAC,WAAW;IA+6Eb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAO5B"}
@@ -59,8 +59,23 @@ export class WebServer {
59
59
  ];
60
60
  const staticDir = candidates.find(dir => fs.existsSync(dir));
61
61
  if (staticDir) {
62
- this.app.use(express.static(staticDir));
62
+ // Static files with content-hash in filename: long cache
63
+ // index.html: no cache (always fetch latest)
64
+ this.app.use(express.static(staticDir, {
65
+ setHeaders: (res, filePath) => {
66
+ if (filePath.endsWith('index.html')) {
67
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
68
+ res.setHeader('Pragma', 'no-cache');
69
+ res.setHeader('Expires', '0');
70
+ }
71
+ else if (filePath.includes('/assets/')) {
72
+ // Vite assets are content-hashed, safe to cache forever
73
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
74
+ }
75
+ },
76
+ }));
63
77
  this.app.get('/', (_req, res) => {
78
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
64
79
  res.sendFile(path.join(staticDir, 'index.html'));
65
80
  });
66
81
  logger.info(`[Web] Serving static files from ${staticDir}`);
@@ -638,6 +653,99 @@ export class WebServer {
638
653
  const rows = storage.queryRoutingEvents(filter);
639
654
  res.json(rows);
640
655
  });
656
+ // Methodology Executions: list all executions
657
+ this.app.get('/api/methodology-executions', (req, res) => {
658
+ const limit = parseInt(req.query.limit) || 50;
659
+ const sessionId = req.query.session;
660
+ const db = storage.getDatabase();
661
+ const query = sessionId
662
+ ? 'SELECT * FROM methodology_executions WHERE session_id = ? ORDER BY started_at DESC LIMIT ?'
663
+ : 'SELECT * FROM methodology_executions ORDER BY started_at DESC LIMIT ?';
664
+ const params = sessionId ? [sessionId, limit] : [limit];
665
+ const executions = db.prepare(query).all(...params);
666
+ res.json(executions);
667
+ });
668
+ // Methodology Execution Detail: get single execution with phases
669
+ this.app.get('/api/methodology-executions/:id', (req, res) => {
670
+ const executionId = parseInt(req.params.id);
671
+ const db = storage.getDatabase();
672
+ const execution = db.prepare('SELECT * FROM methodology_executions WHERE id = ?').get(executionId);
673
+ if (!execution) {
674
+ res.status(404).json({ error: 'Execution not found' });
675
+ return;
676
+ }
677
+ const phases = db.prepare(`
678
+ SELECT * FROM phase_executions
679
+ WHERE methodology_execution_id = ?
680
+ ORDER BY phase_index ASC
681
+ `).all(executionId);
682
+ res.json({
683
+ ...execution,
684
+ plan: JSON.parse(execution.plan_json),
685
+ phases,
686
+ });
687
+ });
688
+ // Methodology Execution: cancel a running execution
689
+ this.app.post('/api/methodology-executions/:id/cancel', (req, res) => {
690
+ const executionId = parseInt(req.params.id);
691
+ const db = storage.getDatabase();
692
+ const execution = db.prepare('SELECT * FROM methodology_executions WHERE id = ?').get(executionId);
693
+ if (!execution) {
694
+ res.status(404).json({ error: 'Execution not found' });
695
+ return;
696
+ }
697
+ if (execution.status !== 'running') {
698
+ res.status(400).json({ error: `Cannot cancel execution in ${execution.status} state` });
699
+ return;
700
+ }
701
+ db.prepare(`
702
+ UPDATE methodology_executions
703
+ SET status = 'cancelled', completed_at = ?
704
+ WHERE id = ?
705
+ `).run(Date.now(), executionId);
706
+ // Also mark any running phase as cancelled
707
+ db.prepare(`
708
+ UPDATE phase_executions
709
+ SET status = 'cancelled', completed_at = ?
710
+ WHERE methodology_execution_id = ? AND status = 'running'
711
+ `).run(Date.now(), executionId);
712
+ res.json({ success: true, message: 'Execution cancelled' });
713
+ });
714
+ // Methodology Execution: delete an execution
715
+ this.app.delete('/api/methodology-executions/:id', (req, res) => {
716
+ const executionId = parseInt(req.params.id);
717
+ const db = storage.getDatabase();
718
+ db.prepare('DELETE FROM phase_executions WHERE methodology_execution_id = ?').run(executionId);
719
+ const result = db.prepare('DELETE FROM methodology_executions WHERE id = ?').run(executionId);
720
+ if (result.changes === 0) {
721
+ res.status(404).json({ error: 'Execution not found' });
722
+ return;
723
+ }
724
+ res.json({ success: true });
725
+ });
726
+ // Methodology: list all available methodologies
727
+ this.app.get('/api/methodologies', (_req, res) => {
728
+ try {
729
+ const candidates = [
730
+ path.join(__dirname, '../capability/methodologies'),
731
+ path.join(__dirname, '../../src/capability/methodologies'),
732
+ ];
733
+ const dir = candidates.find(d => fs.existsSync(d));
734
+ if (!dir) {
735
+ res.json([]);
736
+ return;
737
+ }
738
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.yaml'));
739
+ const methodologies = files.map(f => {
740
+ const content = fs.readFileSync(path.join(dir, f), 'utf-8');
741
+ return yaml.load(content);
742
+ });
743
+ res.json(methodologies);
744
+ }
745
+ catch (err) {
746
+ res.status(500).json({ error: err.message });
747
+ }
748
+ });
641
749
  // Refusal clustering (Plan A: SQL GROUP BY by taskType × agent)
642
750
  this.app.get('/api/routing/refusals', (req, res) => {
643
751
  const windowHours = parseInt(req.query.window || '168');
@@ -2104,6 +2212,19 @@ Return ONLY a JSON object with this exact structure (no markdown, no explanation
2104
2212
  storage.removeListener('decision', onDecision);
2105
2213
  });
2106
2214
  });
2215
+ // SPA fallback: serve index.html for any non-API route
2216
+ // (must be the LAST route to not interfere with API routes)
2217
+ const spaCandidates = [
2218
+ path.join(__dirname, 'static'),
2219
+ path.join(__dirname, '../../src/web/static'),
2220
+ ];
2221
+ const spaStaticDir = spaCandidates.find(dir => fs.existsSync(dir));
2222
+ if (spaStaticDir) {
2223
+ this.app.get(/^(?!\/api).*$/, (_req, res) => {
2224
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
2225
+ res.sendFile(path.join(spaStaticDir, 'index.html'));
2226
+ });
2227
+ }
2107
2228
  }
2108
2229
  async start() {
2109
2230
  return new Promise((resolve, reject) => {