tribunal-kit 4.4.3 → 4.4.4

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.
@@ -39,7 +39,7 @@ const ARCHIVE_DIR = path.join(MARATHON_DIR, 'archive');
39
39
 
40
40
  const VALID_COMMANDS = new Set([
41
41
  'init', 'status', 'next', 'mark', 'log',
42
- 'session-start', 'session-end', 'reset', 'add-feature'
42
+ 'session-start', 'session-end', 'reset', 'add-feature', 'distill'
43
43
  ]);
44
44
 
45
45
  // ── Schema Defaults ──────────────────────────────────────────────────────────
@@ -153,25 +153,50 @@ function getGitBranch() {
153
153
  // ── Progress Helpers ─────────────────────────────────────────────────────────
154
154
 
155
155
  /**
156
- * Count passing features.
156
+ * Count passing features and blocked features.
157
157
  * @param {object} featureList
158
- * @returns {{ total: number, passing: number, failing: number }}
158
+ * @returns {{ total: number, passing: number, failing: number, blocked: number }}
159
159
  */
160
160
  function countFeatures(featureList) {
161
161
  const features = featureList.features || [];
162
162
  const total = features.length;
163
163
  const passing = features.filter(f => f.passes === true).length;
164
- return { total, passing, failing: total - passing };
164
+ let blocked = 0;
165
+
166
+ features.forEach(f => {
167
+ if (!f.passes && f.dependencies && f.dependencies.length > 0) {
168
+ const allPassed = f.dependencies.every(depId => {
169
+ const dep = features.find(d => d.id === depId);
170
+ return dep && dep.passes === true;
171
+ });
172
+ if (!allPassed) blocked++;
173
+ }
174
+ });
175
+
176
+ return { total, passing, failing: total - passing, blocked };
165
177
  }
166
178
 
167
179
  /**
168
- * Get the next unfinished feature.
180
+ * Get the next unfinished, unblocked feature.
169
181
  * @param {object} featureList
170
182
  * @returns {object|null}
171
183
  */
172
184
  function getNextFeature(featureList) {
173
185
  const features = featureList.features || [];
174
- return features.find(f => f.passes !== true) || null;
186
+ return features.find(f => {
187
+ if (f.passes === true) return false;
188
+
189
+ // Check dependencies (DAG)
190
+ if (f.dependencies && f.dependencies.length > 0) {
191
+ const allPassed = f.dependencies.every(depId => {
192
+ const dep = features.find(d => d.id === depId);
193
+ return dep && dep.passes === true;
194
+ });
195
+ if (!allPassed) return false; // Feature is blocked
196
+ }
197
+
198
+ return true;
199
+ }) || null;
175
200
  }
176
201
 
177
202
  /**
@@ -240,8 +265,9 @@ function cmdInit(spec) {
240
265
  * @param {string} category
241
266
  * @param {string} description
242
267
  * @param {string[]} steps
268
+ * @param {number[]} deps
243
269
  */
244
- function cmdAddFeature(category, description, steps) {
270
+ function cmdAddFeature(category, description, steps, deps = []) {
245
271
  if (!isActive()) {
246
272
  console.error(`${RED}❌ No active marathon. Run ${CYAN}init${RED} first.${RESET}`);
247
273
  process.exit(1);
@@ -264,6 +290,9 @@ function cmdAddFeature(category, description, steps) {
264
290
  category: category.toLowerCase(),
265
291
  description,
266
292
  steps: steps.length > 0 ? steps : ['Implement and verify'],
293
+ dependencies: deps,
294
+ attempts: 0,
295
+ failureReasons: [],
267
296
  passes: false,
268
297
  sessionCompleted: null
269
298
  };
@@ -289,7 +318,7 @@ function cmdStatus() {
289
318
  const progress = readJSON(PROGRESS_FILE);
290
319
  if (!featureList || !progress) return;
291
320
 
292
- const { total, passing, failing } = countFeatures(featureList);
321
+ const { total, passing, failing, blocked } = countFeatures(featureList);
293
322
  const nextFeature = getNextFeature(featureList);
294
323
  const sessions = progress.sessions || [];
295
324
  const lastSession = sessions[sessions.length - 1] || null;
@@ -303,7 +332,8 @@ function cmdStatus() {
303
332
  console.log();
304
333
 
305
334
  // ── Progress Bar ──
306
- console.log(` ${BOLD}Progress:${RESET} ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total} features`);
335
+ const blockedInfo = blocked > 0 ? ` (${YELLOW}${blocked} blocked${RESET})` : '';
336
+ console.log(` ${BOLD}Progress:${RESET} ${progressBar(passing, total)} ${GREEN}${passing}${RESET}/${total} features${blockedInfo}`);
307
337
  console.log();
308
338
 
309
339
  // ── Category Breakdown ──
@@ -382,7 +412,12 @@ function cmdNext() {
382
412
  const nextFeature = getNextFeature(featureList);
383
413
 
384
414
  if (!nextFeature) {
385
- console.log(`${GREEN}${BOLD}🎉 All ${total} features are passing! Marathon complete.${RESET}`);
415
+ if (passing === total) {
416
+ console.log(`${GREEN}${BOLD}🎉 All ${total} features are passing! Marathon complete.${RESET}`);
417
+ } else {
418
+ console.log(`${RED}${BOLD}⚠️ Deadlock detected: ${total - passing} features remain, but all are blocked by failing dependencies.${RESET}`);
419
+ console.log(` ${DIM}Check 'status' and use 'mark <id> pass' to resolve dependencies.${RESET}`);
420
+ }
386
421
  return;
387
422
  }
388
423
 
@@ -400,6 +435,14 @@ function cmdNext() {
400
435
  console.log();
401
436
  }
402
437
 
438
+ if (nextFeature.failureReasons && nextFeature.failureReasons.length > 0) {
439
+ console.log(` ${RED}${BOLD}Previous Failures (${nextFeature.attempts} attempts):${RESET}`);
440
+ for (const reason of nextFeature.failureReasons) {
441
+ console.log(` ${DIM}* ${reason}${RESET}`);
442
+ }
443
+ console.log();
444
+ }
445
+
403
446
  console.log(` ${DIM}When done: marathon_harness.js mark ${nextFeature.id} pass${RESET}`);
404
447
  console.log();
405
448
  }
@@ -408,8 +451,9 @@ function cmdNext() {
408
451
  * Mark a feature as passing or failing.
409
452
  * @param {number} id
410
453
  * @param {string} verdict - 'pass' or 'fail'
454
+ * @param {string} [reason] - Reason for failure
411
455
  */
412
- function cmdMark(id, verdict) {
456
+ function cmdMark(id, verdict, reason) {
413
457
  if (!isActive()) {
414
458
  console.error(`${RED}❌ No active marathon.${RESET}`);
415
459
  process.exit(1);
@@ -437,6 +481,14 @@ function cmdMark(id, verdict) {
437
481
  feature.passes = newPasses;
438
482
  feature.sessionCompleted = newPasses ? new Date().toISOString() : null;
439
483
 
484
+ if (!newPasses) {
485
+ feature.attempts = (feature.attempts || 0) + 1;
486
+ if (reason) {
487
+ if (!feature.failureReasons) feature.failureReasons = [];
488
+ feature.failureReasons.push(`Attempt ${feature.attempts}: ${reason}`);
489
+ }
490
+ }
491
+
440
492
  writeJSON(FEATURE_LIST_FILE, featureList);
441
493
 
442
494
  const { total, passing } = countFeatures(featureList);
@@ -482,6 +534,30 @@ function cmdLog(message) {
482
534
  ok(`Logged: ${message}`);
483
535
  }
484
536
 
537
+ /**
538
+ * Distill a lesson learned into memory context.
539
+ * @param {string} lesson
540
+ */
541
+ function cmdDistill(lesson) {
542
+ if (!isActive()) {
543
+ console.error(`${RED}❌ No active marathon.${RESET}`);
544
+ process.exit(1);
545
+ }
546
+
547
+ if (!lesson) {
548
+ console.error(`${RED}❌ Lesson required. Usage: distill "Your architectural lesson"${RESET}`);
549
+ process.exit(1);
550
+ }
551
+
552
+ ensureDir();
553
+ const DISTILL_FILE = path.join(MARATHON_DIR, 'distilled_context.md');
554
+ const timestamp = new Date().toISOString().slice(0, 16);
555
+ const entry = `- [${timestamp}] ${lesson}\n`;
556
+
557
+ fs.appendFileSync(DISTILL_FILE, entry, 'utf8');
558
+ ok(`Distilled memory saved: ${lesson}`);
559
+ }
560
+
485
561
  /**
486
562
  * Start a new session — reads state, shows bearings.
487
563
  */
@@ -567,7 +643,11 @@ function cmdSessionStart() {
567
643
  }
568
644
  }
569
645
  } else {
570
- console.log(` ${GREEN}${BOLD}🎉 All features passing! Nothing to implement.${RESET}`);
646
+ if (passing === total) {
647
+ console.log(` ${GREEN}${BOLD}🎉 All features passing! Nothing to implement.${RESET}`);
648
+ } else {
649
+ console.log(` ${RED}${BOLD}⚠️ Deadlock: ${total - passing} features are blocked by failing dependencies.${RESET}`);
650
+ }
571
651
  }
572
652
  console.log();
573
653
 
@@ -717,11 +797,12 @@ function showHelp() {
717
797
  cmd('status', 'Show progress dashboard');
718
798
  cmd('next', 'Show the next unfinished feature');
719
799
  cmd('mark <id> pass', 'Mark a feature as passing');
720
- cmd('mark <id> fail', 'Mark a feature as failing');
800
+ cmd('mark <id> fail', 'Mark a feature as failing (optional: "reason")');
721
801
  cmd('log "note"', 'Add a timestamped progress note');
802
+ cmd('distill "rule"', 'Save an architectural rule or lesson to memory');
722
803
  cmd('session-start', 'Begin a new work session (reads state, shows bearings)');
723
804
  cmd('session-end', 'End session with optional summary');
724
- cmd('add-feature', 'Add a feature: add-feature "category" "description" "step1" ...');
805
+ cmd('add-feature', 'Add a feature (supports --deps=1,2,3 for DAG dependencies)');
725
806
  cmd('reset', 'Archive current marathon and start fresh');
726
807
  console.log();
727
808
  }
@@ -759,11 +840,12 @@ function main() {
759
840
  case 'mark': {
760
841
  const id = parseInt(args[1], 10);
761
842
  const verdict = (args[2] || '').toLowerCase();
843
+ const reason = args.slice(3).join(' ').trim();
762
844
  if (isNaN(id)) {
763
- console.error(`${RED}❌ Feature ID required. Usage: mark <id> pass|fail${RESET}`);
845
+ console.error(`${RED}❌ Feature ID required. Usage: mark <id> pass|fail "reason"${RESET}`);
764
846
  process.exit(1);
765
847
  }
766
- cmdMark(id, verdict);
848
+ cmdMark(id, verdict, reason);
767
849
  break;
768
850
  }
769
851
  case 'log': {
@@ -782,8 +864,23 @@ function main() {
782
864
  case 'add-feature': {
783
865
  const category = args[1] || '';
784
866
  const description = args[2] || '';
785
- const steps = args.slice(3);
786
- cmdAddFeature(category, description, steps);
867
+ let steps = args.slice(3);
868
+ let deps = [];
869
+
870
+ steps = steps.filter(step => {
871
+ if (step.startsWith('--deps=')) {
872
+ deps = step.replace('--deps=', '').split(',').map(Number).filter(n => !isNaN(n));
873
+ return false;
874
+ }
875
+ return true;
876
+ });
877
+
878
+ cmdAddFeature(category, description, steps, deps);
879
+ break;
880
+ }
881
+ case 'distill': {
882
+ const lesson = args.slice(1).join(' ').trim();
883
+ cmdDistill(lesson);
787
884
  break;
788
885
  }
789
886
  case 'reset':
@@ -8,30 +8,13 @@ if (!rawInput) {
8
8
  process.exit(1);
9
9
  }
10
10
 
11
- // 1. Strip conversational fluff (lowers token count)
12
- let cleanInput = rawInput
13
- .replace(/hey,? /gi, '')
14
- .replace(/can you /gi, '')
15
- .replace(/could you /gi, '')
16
- .replace(/please /gi, '')
17
- .replace(/i want to /gi, '')
18
- .replace(/i need you to /gi, '')
19
- .replace(/for me/gi, '')
20
- .replace(/would it be possible to /gi, '')
21
- .trim();
11
+ // 1. Keep original input, only optionally strip leading "please" for action matching
12
+ const cleanInput = rawInput.trim();
22
13
 
23
14
  // 2. Extract Action (Intent mapping)
24
- const actionMatch = cleanInput.match(/^(build|create|fix|debug|refactor|update|write|design|audit)\b/i);
15
+ const actionMatch = cleanInput.match(/^(?:(?:hey,?\s*|please\s+|can you\s+|could you\s+|would you\s+|i need you to\s+|i want to\s+)*)(build|create|fix|debug|refactor|update|write|design|audit)\b/i);
25
16
  const action = actionMatch ? actionMatch[1].toLowerCase() : 'execute';
26
17
 
27
- // Remove the action from the target string
28
- if (actionMatch) {
29
- cleanInput = cleanInput.substring(actionMatch[0].length).trim();
30
- }
31
-
32
- // Strip leading articles
33
- cleanInput = cleanInput.replace(/^(a|an|some)\s+/i, '');
34
-
35
18
  // 3. Extract Technology Stack
36
19
  const techKeywords = [
37
20
  'react', 'tailwind', 'next.js', 'sql', 'postgres', 'express',
@@ -41,16 +24,64 @@ const techKeywords = [
41
24
 
42
25
  const stack = [];
43
26
  techKeywords.forEach(tech => {
44
- // Use word boundaries to prevent partial matches
45
27
  const regex = new RegExp(`\\b${tech.replace('.', '\\.')}\\b`, 'i');
46
28
  if (regex.test(cleanInput)) {
47
29
  stack.push(tech.toLowerCase());
48
30
  }
49
31
  });
50
32
 
51
- // 4. Output highly compressed YAML
33
+ // 4. Intelligent Pre-Routing
34
+ const routerMap = {
35
+ 'react': ['react-specialist', 'frontend-design'],
36
+ 'tailwind': ['tailwind-patterns'],
37
+ 'next.js': ['nextjs-react-expert', 'react-specialist'],
38
+ 'sql': ['sql-pro', 'database-design'],
39
+ 'postgres': ['database-design'],
40
+ 'express': ['nodejs-best-practices'],
41
+ 'python': ['python-pro'],
42
+ 'node': ['nodejs-best-practices'],
43
+ 'vue': ['vue-expert'],
44
+ 'svelte': ['frontend-design'],
45
+ 'typescript': ['typescript-advanced'],
46
+ 'js': ['clean-code'],
47
+ 'css': ['tailwind-patterns'],
48
+ 'html': ['frontend-design'],
49
+ 'prisma': ['database-design'],
50
+ 'drizzle': ['database-design']
51
+ };
52
+
53
+ const actionRouter = {
54
+ 'build': ['architecture'],
55
+ 'create': ['architecture'],
56
+ 'fix': ['systematic-debugging'],
57
+ 'debug': ['systematic-debugging'],
58
+ 'refactor': ['clean-code'],
59
+ 'update': ['clean-code'],
60
+ 'write': ['clean-code'],
61
+ 'design': ['frontend-design'],
62
+ 'audit': ['vulnerability-scanner', 'lint-and-validate']
63
+ };
64
+
65
+ const recommendedSkills = new Set();
66
+
67
+ if (actionRouter[action]) {
68
+ actionRouter[action].forEach(s => recommendedSkills.add(s));
69
+ }
70
+
71
+ stack.forEach(tech => {
72
+ if (routerMap[tech]) {
73
+ routerMap[tech].forEach(s => recommendedSkills.add(s));
74
+ }
75
+ });
76
+
77
+ const finalSkills = Array.from(recommendedSkills).slice(0, 3);
78
+
79
+ // 5. Output highly compressed YAML
52
80
  console.log('---');
53
81
  console.log(`action: ${action}`);
54
- console.log(`target: ${cleanInput}`);
82
+ console.log(`target: |`);
83
+ const indentedTarget = cleanInput.split('\n').map(line => ' ' + line).join('\n');
84
+ console.log(indentedTarget);
55
85
  console.log(`stack: [${stack.join(', ')}]`);
86
+ console.log(`recommended_skills: [${finalSkills.join(', ')}]`);
56
87
  console.log('---');