prjct-cli 0.9.2 → 0.10.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/core/__tests__/agentic/memory-system.test.js +263 -0
  3. package/core/__tests__/agentic/plan-mode.test.js +336 -0
  4. package/core/agentic/agent-router.js +253 -186
  5. package/core/agentic/chain-of-thought.js +578 -0
  6. package/core/agentic/command-executor.js +299 -17
  7. package/core/agentic/context-builder.js +208 -8
  8. package/core/agentic/context-filter.js +83 -83
  9. package/core/agentic/ground-truth.js +591 -0
  10. package/core/agentic/loop-detector.js +406 -0
  11. package/core/agentic/memory-system.js +850 -0
  12. package/core/agentic/parallel-tools.js +366 -0
  13. package/core/agentic/plan-mode.js +572 -0
  14. package/core/agentic/prompt-builder.js +127 -2
  15. package/core/agentic/response-templates.js +290 -0
  16. package/core/agentic/semantic-compression.js +517 -0
  17. package/core/agentic/think-blocks.js +657 -0
  18. package/core/agentic/tool-registry.js +32 -0
  19. package/core/agentic/validation-rules.js +380 -0
  20. package/core/command-registry.js +48 -0
  21. package/core/commands.js +128 -60
  22. package/core/context-sync.js +183 -0
  23. package/core/domain/agent-generator.js +77 -46
  24. package/core/domain/agent-loader.js +183 -0
  25. package/core/domain/agent-matcher.js +217 -0
  26. package/core/domain/agent-validator.js +217 -0
  27. package/core/domain/context-estimator.js +175 -0
  28. package/core/domain/product-standards.js +92 -0
  29. package/core/domain/smart-cache.js +157 -0
  30. package/core/domain/task-analyzer.js +353 -0
  31. package/core/domain/tech-detector.js +365 -0
  32. package/package.json +8 -16
  33. package/templates/commands/done.md +7 -0
  34. package/templates/commands/feature.md +8 -0
  35. package/templates/commands/ship.md +8 -0
  36. package/templates/commands/spec.md +128 -0
  37. package/templates/global/CLAUDE.md +17 -0
  38. package/core/__tests__/agentic/agent-router.test.js +0 -398
  39. package/core/__tests__/agentic/command-executor.test.js +0 -223
  40. package/core/__tests__/agentic/context-builder.test.js +0 -160
  41. package/core/__tests__/agentic/context-filter.test.js +0 -494
  42. package/core/__tests__/agentic/prompt-builder.test.js +0 -212
  43. package/core/__tests__/agentic/template-loader.test.js +0 -164
  44. package/core/__tests__/agentic/tool-registry.test.js +0 -243
  45. package/core/__tests__/domain/agent-generator.test.js +0 -296
  46. package/core/__tests__/domain/analyzer.test.js +0 -324
  47. package/core/__tests__/infrastructure/author-detector.test.js +0 -103
  48. package/core/__tests__/infrastructure/config-manager.test.js +0 -454
  49. package/core/__tests__/infrastructure/path-manager.test.js +0 -412
  50. package/core/__tests__/setup.test.js +0 -15
  51. package/core/__tests__/utils/date-helper.test.js +0 -169
  52. package/core/__tests__/utils/file-helper.test.js +0 -258
  53. package/core/__tests__/utils/jsonl-helper.test.js +0 -387
@@ -0,0 +1,657 @@
1
+ /**
2
+ * Think Blocks - Devin Pattern Implementation
3
+ *
4
+ * P3.1: Advanced reasoning layer that triggers <think> blocks
5
+ * in specific situations to prevent hallucination and improve decisions.
6
+ *
7
+ * WHEN TO THINK (Devin's rules):
8
+ * 1. Before critical git/GitHub decisions (branch, PR, merge)
9
+ * 2. When transitioning from exploration to editing
10
+ * 3. Before reporting task completion to user
11
+ * 4. When there's no clear next step
12
+ * 5. When encountering unexpected difficulties
13
+ * 6. When tests/CI fail unexpectedly
14
+ *
15
+ * Source: Devin AI system prompt
16
+ */
17
+
18
+ const fs = require('fs').promises
19
+
20
+ /**
21
+ * Think trigger types and their conditions
22
+ */
23
+ const THINK_TRIGGERS = {
24
+ // Git/GitHub critical decisions
25
+ GIT_DECISION: {
26
+ id: 'git_decision',
27
+ description: 'Before critical git operations',
28
+ commands: ['ship', 'git'],
29
+ conditions: ['has_uncommitted', 'branch_decision', 'merge_conflict'],
30
+ priority: 1
31
+ },
32
+
33
+ // Transitioning from exploration to action
34
+ EXPLORE_TO_EDIT: {
35
+ id: 'explore_to_edit',
36
+ description: 'Transitioning from reading to editing',
37
+ commands: ['feature', 'spec', 'design'],
38
+ conditions: ['first_edit', 'complex_change'],
39
+ priority: 2
40
+ },
41
+
42
+ // Before reporting completion
43
+ REPORT_COMPLETE: {
44
+ id: 'report_complete',
45
+ description: 'Before marking task as done',
46
+ commands: ['done', 'ship'],
47
+ conditions: ['task_complete', 'feature_ship'],
48
+ priority: 1
49
+ },
50
+
51
+ // Unclear next step
52
+ UNCLEAR_NEXT: {
53
+ id: 'unclear_next',
54
+ description: 'No clear next action',
55
+ commands: ['next', 'help', 'suggest'],
56
+ conditions: ['empty_queue', 'ambiguous_state'],
57
+ priority: 3
58
+ },
59
+
60
+ // Unexpected difficulties
61
+ UNEXPECTED_ERROR: {
62
+ id: 'unexpected_error',
63
+ description: 'Something went wrong unexpectedly',
64
+ commands: ['*'],
65
+ conditions: ['test_fail', 'ci_fail', 'repeated_error'],
66
+ priority: 1
67
+ },
68
+
69
+ // Complex feature analysis
70
+ COMPLEX_ANALYSIS: {
71
+ id: 'complex_analysis',
72
+ description: 'Analyzing complex feature or system',
73
+ commands: ['analyze', 'spec', 'feature'],
74
+ conditions: ['multi_file', 'architecture_change'],
75
+ priority: 2
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Think block generator
81
+ */
82
+ class ThinkBlocks {
83
+ constructor() {
84
+ this.triggers = THINK_TRIGGERS
85
+ this.thinkHistory = []
86
+ this.maxHistory = 50
87
+ }
88
+
89
+ /**
90
+ * Detect if current context requires a think block
91
+ * @param {string} commandName - Command being executed
92
+ * @param {Object} context - Execution context
93
+ * @param {Object} state - Current state
94
+ * @returns {Object|null} Think trigger if applicable
95
+ */
96
+ detectTrigger(commandName, context, state) {
97
+ const applicableTriggers = []
98
+
99
+ for (const [, trigger] of Object.entries(this.triggers)) {
100
+ // Check if command matches
101
+ const commandMatches =
102
+ trigger.commands.includes('*') ||
103
+ trigger.commands.includes(commandName)
104
+
105
+ if (!commandMatches) continue
106
+
107
+ // Check conditions
108
+ const conditionsMet = this._checkConditions(trigger.conditions, context, state)
109
+
110
+ if (conditionsMet.length > 0) {
111
+ applicableTriggers.push({
112
+ ...trigger,
113
+ metConditions: conditionsMet
114
+ })
115
+ }
116
+ }
117
+
118
+ // Return highest priority trigger
119
+ if (applicableTriggers.length === 0) return null
120
+
121
+ applicableTriggers.sort((a, b) => a.priority - b.priority)
122
+ return applicableTriggers[0]
123
+ }
124
+
125
+ /**
126
+ * Check which conditions are met
127
+ * @private
128
+ */
129
+ _checkConditions(conditions, context, state) {
130
+ const met = []
131
+
132
+ for (const condition of conditions) {
133
+ const checker = this._getConditionChecker(condition)
134
+ if (checker && checker(context, state)) {
135
+ met.push(condition)
136
+ }
137
+ }
138
+
139
+ return met
140
+ }
141
+
142
+ /**
143
+ * Get condition checker function
144
+ * @private
145
+ */
146
+ _getConditionChecker(condition) {
147
+ const checkers = {
148
+ // Git conditions
149
+ has_uncommitted: (ctx) => ctx.groundTruth?.actual?.hasUncommittedChanges,
150
+ branch_decision: (ctx) => ctx.params?.branch || ctx.needsBranch,
151
+ merge_conflict: (ctx) => ctx.groundTruth?.actual?.hasMergeConflict,
152
+
153
+ // State conditions
154
+ task_complete: (ctx, state) => state.now && state.now.trim() !== '',
155
+ feature_ship: (ctx) => ctx.params?.feature || ctx.params?.description,
156
+ empty_queue: (ctx, state) => !state.next || state.next.trim() === '',
157
+ ambiguous_state: (ctx, state) => !state.now && !state.next,
158
+
159
+ // Complexity conditions
160
+ first_edit: (ctx) => ctx.isFirstEdit,
161
+ complex_change: (ctx) => ctx.params?.complex || ctx.filesAffected > 5,
162
+ multi_file: (ctx) => ctx.filesAffected > 3,
163
+ architecture_change: (ctx, state) => {
164
+ const desc = ctx.params?.description?.toLowerCase() || ''
165
+ return desc.includes('refactor') ||
166
+ desc.includes('architecture') ||
167
+ desc.includes('migrate')
168
+ },
169
+
170
+ // Error conditions
171
+ test_fail: (ctx) => ctx.groundTruth?.actual?.testsFailed,
172
+ ci_fail: (ctx) => ctx.groundTruth?.actual?.ciFailed,
173
+ repeated_error: (ctx) => ctx.errorCount > 2
174
+ }
175
+
176
+ return checkers[condition]
177
+ }
178
+
179
+ /**
180
+ * Generate a think block for the given trigger
181
+ * @param {Object} trigger - The triggered think condition
182
+ * @param {string} commandName - Command being executed
183
+ * @param {Object} context - Execution context
184
+ * @param {Object} state - Current state
185
+ * @returns {Object} Think block content
186
+ */
187
+ async generate(trigger, commandName, context, state) {
188
+ const thinkBlock = {
189
+ id: `think_${Date.now()}`,
190
+ trigger: trigger.id,
191
+ command: commandName,
192
+ timestamp: new Date().toISOString(),
193
+ questions: [],
194
+ observations: [],
195
+ conclusions: [],
196
+ plan: [],
197
+ confidence: 0
198
+ }
199
+
200
+ // Generate questions based on trigger type
201
+ const generator = this._getThinkGenerator(trigger.id)
202
+ if (generator) {
203
+ const content = await generator.call(this, context, state, trigger)
204
+ Object.assign(thinkBlock, content)
205
+ }
206
+
207
+ // Calculate confidence
208
+ thinkBlock.confidence = this._calculateConfidence(thinkBlock)
209
+
210
+ // Add to history
211
+ this._addToHistory(thinkBlock)
212
+
213
+ return thinkBlock
214
+ }
215
+
216
+ /**
217
+ * Get think generator for trigger type
218
+ * @private
219
+ */
220
+ _getThinkGenerator(triggerId) {
221
+ const generators = {
222
+ git_decision: this._thinkGitDecision,
223
+ explore_to_edit: this._thinkExploreToEdit,
224
+ report_complete: this._thinkReportComplete,
225
+ unclear_next: this._thinkUnclearNext,
226
+ unexpected_error: this._thinkUnexpectedError,
227
+ complex_analysis: this._thinkComplexAnalysis
228
+ }
229
+ return generators[triggerId]
230
+ }
231
+
232
+ /**
233
+ * Think: Git Decision
234
+ * @private
235
+ */
236
+ async _thinkGitDecision(context, state, trigger) {
237
+ const questions = [
238
+ 'What git operation am I about to perform?',
239
+ 'Are there uncommitted changes that should be included?',
240
+ 'What branch should this go on?',
241
+ 'Should I create a PR or commit directly?'
242
+ ]
243
+
244
+ const observations = []
245
+ const conclusions = []
246
+ const plan = []
247
+
248
+ // Check uncommitted changes
249
+ if (context.groundTruth?.actual?.hasUncommittedChanges) {
250
+ observations.push(`Found ${context.groundTruth.actual.uncommittedFiles} uncommitted files`)
251
+ conclusions.push('Should commit changes before proceeding')
252
+ plan.push('Stage relevant changes')
253
+ plan.push('Create commit with prjct footer')
254
+ } else {
255
+ observations.push('No uncommitted changes')
256
+ }
257
+
258
+ // Check branch
259
+ if (context.groundTruth?.actual?.currentBranch) {
260
+ observations.push(`Current branch: ${context.groundTruth.actual.currentBranch}`)
261
+ if (context.groundTruth.actual.currentBranch === 'main') {
262
+ conclusions.push('On main branch - consider feature branch for safety')
263
+ }
264
+ }
265
+
266
+ // Version check
267
+ if (context.groundTruth?.actual?.currentVersion) {
268
+ observations.push(`Current version: ${context.groundTruth.actual.currentVersion}`)
269
+ if (trigger.metConditions.includes('feature_ship')) {
270
+ conclusions.push('May need version bump')
271
+ plan.push('Check if version bump needed')
272
+ }
273
+ }
274
+
275
+ plan.push('Execute git operation')
276
+ plan.push('Verify success')
277
+
278
+ return { questions, observations, conclusions, plan }
279
+ }
280
+
281
+ /**
282
+ * Think: Explore to Edit transition
283
+ * @private
284
+ */
285
+ async _thinkExploreToEdit(context, state, trigger) {
286
+ const questions = [
287
+ 'What have I learned from exploring the codebase?',
288
+ 'Am I confident about the changes needed?',
289
+ 'What files will be affected?',
290
+ 'Are there any risks or edge cases?'
291
+ ]
292
+
293
+ const observations = []
294
+ const conclusions = []
295
+ const plan = []
296
+
297
+ // Feature analysis
298
+ const featureDesc = context.params?.description || context.params?.feature
299
+ if (featureDesc) {
300
+ observations.push(`Feature: "${featureDesc}"`)
301
+
302
+ // Complexity assessment
303
+ const complexKeywords = ['auth', 'payment', 'database', 'refactor', 'migrate', 'security']
304
+ const isComplex = complexKeywords.some(kw => featureDesc.toLowerCase().includes(kw))
305
+
306
+ if (isComplex) {
307
+ conclusions.push('This is a complex feature - proceed carefully')
308
+ plan.push('Create spec document first')
309
+ plan.push('Break into small tasks (20-30 min each)')
310
+ }
311
+ }
312
+
313
+ // Files affected
314
+ if (context.filesAffected) {
315
+ observations.push(`Estimated files affected: ${context.filesAffected}`)
316
+ if (context.filesAffected > 5) {
317
+ conclusions.push('Multiple files affected - consider incremental approach')
318
+ }
319
+ }
320
+
321
+ plan.push('Document design decision')
322
+ plan.push('Start with smallest change first')
323
+ plan.push('Test after each change')
324
+
325
+ return { questions, observations, conclusions, plan }
326
+ }
327
+
328
+ /**
329
+ * Think: Report Complete
330
+ * @private
331
+ */
332
+ async _thinkReportComplete(context, state, trigger) {
333
+ const questions = [
334
+ 'Is the task actually complete?',
335
+ 'Have all acceptance criteria been met?',
336
+ 'Are there any tests I should run?',
337
+ 'Is the code ready for review?'
338
+ ]
339
+
340
+ const observations = []
341
+ const conclusions = []
342
+ const plan = []
343
+
344
+ // Check current task
345
+ if (state.now) {
346
+ const taskName = this._extractTaskName(state.now)
347
+ observations.push(`Current task: "${taskName}"`)
348
+
349
+ // Check for started time
350
+ const startMatch = state.now.match(/Started:\s*(.+)/i)
351
+ if (startMatch) {
352
+ const duration = this._calculateDuration(startMatch[1])
353
+ observations.push(`Duration: ${duration}`)
354
+ }
355
+ }
356
+
357
+ // Ground truth verification
358
+ if (context.groundTruth?.verified === false) {
359
+ conclusions.push('Ground truth verification found issues')
360
+ context.groundTruth.warnings?.forEach(w => {
361
+ conclusions.push(`Warning: ${w}`)
362
+ })
363
+ } else {
364
+ conclusions.push('Ground truth verification passed')
365
+ }
366
+
367
+ // Check for tests
368
+ if (context.groundTruth?.actual?.hasTestScript) {
369
+ observations.push('Project has tests')
370
+ plan.push('Run tests to verify')
371
+ }
372
+
373
+ plan.push('Mark task complete')
374
+ plan.push('Update metrics')
375
+ plan.push('Suggest next task')
376
+
377
+ return { questions, observations, conclusions, plan }
378
+ }
379
+
380
+ /**
381
+ * Think: Unclear Next Step
382
+ * @private
383
+ */
384
+ async _thinkUnclearNext(context, state, trigger) {
385
+ const questions = [
386
+ 'What is the current project state?',
387
+ 'What was the last completed task?',
388
+ 'Are there any pending items in the queue?',
389
+ 'What should the user focus on?'
390
+ ]
391
+
392
+ const observations = []
393
+ const conclusions = []
394
+ const plan = []
395
+
396
+ // Check queue
397
+ if (!state.next || state.next.trim() === '') {
398
+ observations.push('Queue is empty')
399
+ conclusions.push('User needs to add tasks or features')
400
+ plan.push('Suggest /p:feature or /p:idea')
401
+ } else {
402
+ const taskCount = (state.next.match(/- \[ \]/g) || []).length
403
+ observations.push(`Queue has ${taskCount} pending tasks`)
404
+ plan.push('Show top priority task')
405
+ plan.push('Suggest starting with /p:now')
406
+ }
407
+
408
+ // Check roadmap
409
+ if (state.roadmap && state.roadmap.trim() !== '') {
410
+ observations.push('Roadmap exists')
411
+ plan.push('Check roadmap for planned features')
412
+ }
413
+
414
+ // Check ideas
415
+ if (state.ideas && state.ideas.trim() !== '') {
416
+ observations.push('Ideas backlog exists')
417
+ plan.push('Review ideas for inspiration')
418
+ }
419
+
420
+ return { questions, observations, conclusions, plan }
421
+ }
422
+
423
+ /**
424
+ * Think: Unexpected Error
425
+ * @private
426
+ */
427
+ async _thinkUnexpectedError(context, state, trigger) {
428
+ const questions = [
429
+ 'What error occurred?',
430
+ 'Is this a known issue?',
431
+ 'Have I seen this pattern before?',
432
+ 'What are the possible solutions?'
433
+ ]
434
+
435
+ const observations = []
436
+ const conclusions = []
437
+ const plan = []
438
+
439
+ // Error analysis
440
+ if (context.lastError) {
441
+ observations.push(`Error: ${context.lastError.message || context.lastError}`)
442
+
443
+ // Pattern matching
444
+ const errorPatterns = {
445
+ permission: /permission|EACCES|denied/i,
446
+ not_found: /not found|ENOENT|missing/i,
447
+ syntax: /syntax|parse|unexpected/i,
448
+ network: /network|ECONNREFUSED|timeout/i,
449
+ git: /git|merge|conflict/i
450
+ }
451
+
452
+ for (const [type, pattern] of Object.entries(errorPatterns)) {
453
+ if (pattern.test(context.lastError.message || context.lastError)) {
454
+ conclusions.push(`Error type: ${type}`)
455
+ break
456
+ }
457
+ }
458
+ }
459
+
460
+ // Error count
461
+ if (context.errorCount > 2) {
462
+ conclusions.push(`Repeated error (${context.errorCount} times) - may need different approach`)
463
+ plan.push('Consider alternative approach')
464
+ plan.push('Ask user for help if stuck')
465
+ }
466
+
467
+ plan.push('Analyze error details')
468
+ plan.push('Try recovery action')
469
+ plan.push('Report to user if unresolvable')
470
+
471
+ return { questions, observations, conclusions, plan }
472
+ }
473
+
474
+ /**
475
+ * Think: Complex Analysis
476
+ * @private
477
+ */
478
+ async _thinkComplexAnalysis(context, state, trigger) {
479
+ const questions = [
480
+ 'What is the scope of this analysis?',
481
+ 'What components are involved?',
482
+ 'Are there dependencies between components?',
483
+ 'What is the best order to proceed?'
484
+ ]
485
+
486
+ const observations = []
487
+ const conclusions = []
488
+ const plan = []
489
+
490
+ const desc = context.params?.description || context.params?.feature || ''
491
+
492
+ // Scope assessment
493
+ if (desc.includes('refactor')) {
494
+ observations.push('This is a refactoring task')
495
+ conclusions.push('Need to understand existing code first')
496
+ plan.push('Map current architecture')
497
+ plan.push('Identify affected components')
498
+ }
499
+
500
+ if (desc.includes('migrate')) {
501
+ observations.push('This is a migration task')
502
+ conclusions.push('Need migration strategy with rollback plan')
503
+ plan.push('Document current state')
504
+ plan.push('Plan incremental migration')
505
+ plan.push('Create rollback strategy')
506
+ }
507
+
508
+ if (desc.includes('architecture')) {
509
+ observations.push('This involves architecture changes')
510
+ conclusions.push('High-impact change - needs careful planning')
511
+ plan.push('Create architecture diagram')
512
+ plan.push('Get user approval before proceeding')
513
+ }
514
+
515
+ // Default plan additions
516
+ plan.push('Break into small incremental changes')
517
+ plan.push('Test after each change')
518
+ plan.push('Document decisions')
519
+
520
+ return { questions, observations, conclusions, plan }
521
+ }
522
+
523
+ /**
524
+ * Format think block for output
525
+ * @param {Object} thinkBlock - Generated think block
526
+ * @param {boolean} verbose - Whether to show full output
527
+ * @returns {string}
528
+ */
529
+ format(thinkBlock, verbose = false) {
530
+ if (!thinkBlock) return ''
531
+
532
+ const lines = ['<think>']
533
+
534
+ // Questions (only in verbose)
535
+ if (verbose && thinkBlock.questions.length > 0) {
536
+ lines.push('QUESTIONS:')
537
+ thinkBlock.questions.forEach(q => lines.push(` ? ${q}`))
538
+ lines.push('')
539
+ }
540
+
541
+ // Observations
542
+ if (thinkBlock.observations.length > 0) {
543
+ lines.push('OBSERVATIONS:')
544
+ thinkBlock.observations.forEach(o => lines.push(` • ${o}`))
545
+ lines.push('')
546
+ }
547
+
548
+ // Conclusions
549
+ if (thinkBlock.conclusions.length > 0) {
550
+ lines.push('CONCLUSIONS:')
551
+ thinkBlock.conclusions.forEach(c => lines.push(` → ${c}`))
552
+ lines.push('')
553
+ }
554
+
555
+ // Plan
556
+ if (thinkBlock.plan.length > 0) {
557
+ lines.push('PLAN:')
558
+ thinkBlock.plan.forEach((p, i) => lines.push(` ${i + 1}. ${p}`))
559
+ }
560
+
561
+ lines.push('</think>')
562
+ lines.push(`Confidence: ${Math.round(thinkBlock.confidence * 100)}%`)
563
+
564
+ return lines.join('\n')
565
+ }
566
+
567
+ /**
568
+ * Format compact version for prompts
569
+ * @param {Object} thinkBlock
570
+ * @returns {string}
571
+ */
572
+ formatCompact(thinkBlock) {
573
+ if (!thinkBlock) return ''
574
+
575
+ const parts = []
576
+
577
+ if (thinkBlock.conclusions.length > 0) {
578
+ parts.push(`Conclusions: ${thinkBlock.conclusions.join('; ')}`)
579
+ }
580
+
581
+ if (thinkBlock.plan.length > 0) {
582
+ parts.push(`Plan: ${thinkBlock.plan.slice(0, 3).join(' → ')}`)
583
+ }
584
+
585
+ return `<think>${parts.join(' | ')}</think>`
586
+ }
587
+
588
+ // Helper methods
589
+
590
+ _extractTaskName(nowContent) {
591
+ if (!nowContent) return ''
592
+ const lines = nowContent.split('\n')
593
+ for (const line of lines) {
594
+ if (line.startsWith('**') || line.startsWith('# ')) {
595
+ return line.replace(/[*#]/g, '').trim()
596
+ }
597
+ }
598
+ return nowContent.substring(0, 50).trim()
599
+ }
600
+
601
+ _calculateDuration(startTime) {
602
+ try {
603
+ const start = new Date(startTime)
604
+ const now = new Date()
605
+ const ms = now - start
606
+ const hours = Math.floor(ms / (1000 * 60 * 60))
607
+ const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60))
608
+ return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`
609
+ } catch {
610
+ return 'unknown'
611
+ }
612
+ }
613
+
614
+ _calculateConfidence(thinkBlock) {
615
+ let score = 0.5 // Base confidence
616
+
617
+ // More observations = more confidence
618
+ score += Math.min(thinkBlock.observations.length * 0.1, 0.3)
619
+
620
+ // Clear conclusions = more confidence
621
+ score += Math.min(thinkBlock.conclusions.length * 0.1, 0.2)
622
+
623
+ // Having a plan = more confidence
624
+ if (thinkBlock.plan.length > 0) score += 0.1
625
+
626
+ return Math.min(score, 1.0)
627
+ }
628
+
629
+ _addToHistory(thinkBlock) {
630
+ this.thinkHistory.unshift(thinkBlock)
631
+ if (this.thinkHistory.length > this.maxHistory) {
632
+ this.thinkHistory.pop()
633
+ }
634
+ }
635
+
636
+ /**
637
+ * Get recent think history
638
+ * @param {number} limit
639
+ * @returns {Array}
640
+ */
641
+ getHistory(limit = 10) {
642
+ return this.thinkHistory.slice(0, limit)
643
+ }
644
+
645
+ /**
646
+ * Check if we should think based on command and context
647
+ * @param {string} commandName
648
+ * @param {Object} context
649
+ * @param {Object} state
650
+ * @returns {boolean}
651
+ */
652
+ shouldThink(commandName, context, state) {
653
+ return this.detectTrigger(commandName, context, state) !== null
654
+ }
655
+ }
656
+
657
+ module.exports = new ThinkBlocks()
@@ -2,6 +2,8 @@
2
2
  * Tool Registry
3
3
  * Maps allowed-tools from templates to actual functions
4
4
  * Simple I/O operations - NO business logic, NO if/else
5
+ *
6
+ * P3.2: Added parallelization hints for multi-tool execution
5
7
  */
6
8
 
7
9
  const fs = require('fs').promises
@@ -22,6 +24,36 @@ class ToolRegistry {
22
24
  GetDate: this.getDate.bind(this),
23
25
  GetDateTime: this.getDateTime.bind(this),
24
26
  }
27
+
28
+ // P3.2: Parallelization hints
29
+ // Used by parallel-tools.js to optimize execution
30
+ this.parallelHints = {
31
+ Read: { parallel: true, category: 'read' },
32
+ Write: { parallel: false, category: 'write' },
33
+ Bash: { parallel: false, category: 'execute' },
34
+ Exec: { parallel: false, category: 'execute' },
35
+ GetTimestamp: { parallel: true, category: 'read' },
36
+ GetDate: { parallel: true, category: 'read' },
37
+ GetDateTime: { parallel: true, category: 'read' },
38
+ }
39
+ }
40
+
41
+ /**
42
+ * P3.2: Check if tool can be parallelized
43
+ * @param {string} toolName
44
+ * @returns {boolean}
45
+ */
46
+ canParallelize(toolName) {
47
+ return this.parallelHints[toolName]?.parallel ?? false
48
+ }
49
+
50
+ /**
51
+ * P3.2: Get tool category (read/write/execute)
52
+ * @param {string} toolName
53
+ * @returns {string}
54
+ */
55
+ getCategory(toolName) {
56
+ return this.parallelHints[toolName]?.category ?? 'unknown'
25
57
  }
26
58
 
27
59
  /**