@ngockhoale/ukit 1.4.0 → 1.4.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 (40) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +1 -1
  3. package/src/bug/triageBug.js +1 -33
  4. package/src/cli/commands/install.js +5 -10
  5. package/src/context/detectProjectContext.js +3 -24
  6. package/src/core/compact/index.js +19 -27
  7. package/src/core/ensureGitignore.js +1 -1
  8. package/src/core/fileOps.js +41 -2
  9. package/src/core/memory/hygiene.js +17 -1
  10. package/src/core/memory/store.js +14 -36
  11. package/src/core/metadata.js +5 -5
  12. package/src/core/output/index.js +20 -20
  13. package/src/core/packageManager.js +51 -0
  14. package/src/core/router/router.js +22 -6
  15. package/src/core/runInstallPipeline.js +1 -36
  16. package/src/core/runtimeConfig.js +71 -3
  17. package/src/core/token/index.js +21 -1
  18. package/src/core/uninstall.js +15 -38
  19. package/src/index/buildIndex.js +217 -49
  20. package/src/index/gitHooks.js +32 -7
  21. package/src/index/impactContext.js +16 -6
  22. package/src/index/importResolution.js +105 -28
  23. package/src/index/paths.js +29 -0
  24. package/src/index/queryIndex.js +20 -35
  25. package/src/index/relatedTests.js +15 -2
  26. package/src/index/routeCatalog.js +1 -1
  27. package/src/index/taskRouting.js +438 -18
  28. package/src/index/verificationPlan.js +2 -36
  29. package/templates/.claude/hooks/reinject-context.sh +2 -0
  30. package/templates/.claude/hooks/session-start.md +2 -0
  31. package/templates/.claude/hooks/skill-router.sh +657 -15
  32. package/templates/.claude/ukit/index/route-catalog.mjs +1 -1
  33. package/templates/.claude/ukit/index/route-task.mjs +475 -5
  34. package/templates/.claude/ukit/runtime/reinject-context.mjs +120 -3
  35. package/templates/.codex/README.md +8 -1
  36. package/templates/.codex/settings.json +53 -0
  37. package/templates/AGENTS.md +3 -0
  38. package/templates/CLAUDE.md +5 -1
  39. package/templates/ukit/README.md +1 -1
  40. package/templates/ukit/storage/config.json +61 -2
@@ -52,18 +52,44 @@ export async function deriveTaskRoute({
52
52
  selectedIds,
53
53
  targetFile: normalizedTarget,
54
54
  });
55
+ const executionScores = deriveExecutionScores({
56
+ promptText: normalizedPrompt,
57
+ commandText: normalizedCommand,
58
+ targetFile: normalizedTarget,
59
+ intentMode,
60
+ taskType: inferredTaskType,
61
+ });
62
+ const executionMode = deriveExecutionMode({
63
+ promptText: normalizedPrompt,
64
+ commandText: normalizedCommand,
65
+ targetFile: normalizedTarget,
66
+ intentMode,
67
+ taskType: inferredTaskType,
68
+ executionScores,
69
+ });
55
70
  const preservedPrompt = normalizedPrompt || String(lastExplicitUserPromptText || '').trim();
56
- const contextResult = useIndexedContext && (contextIntent || normalizedTarget)
57
- ? await resolveContext({
58
- rootDir: absoluteRoot,
59
- intent: contextIntent,
60
- targetFile: normalizedTarget,
61
- taskType: inferredTaskType,
62
- })
63
- : null;
71
+ const degradedWarnings = [];
72
+ let contextResult = null;
73
+ if (useIndexedContext && (contextIntent || normalizedTarget)) {
74
+ const indexReadError = await findUnreadableIndexArtifact(absoluteRoot);
75
+ if (indexReadError) {
76
+ degradedWarnings.push(`resolve-context failed: ${indexReadError.message}`);
77
+ } else {
78
+ try {
79
+ contextResult = await resolveContext({
80
+ rootDir: absoluteRoot,
81
+ intent: contextIntent,
82
+ targetFile: normalizedTarget,
83
+ taskType: inferredTaskType,
84
+ });
85
+ } catch (error) {
86
+ degradedWarnings.push(`resolve-context failed: ${error?.message ?? String(error)}`);
87
+ }
88
+ }
89
+ }
64
90
  const enrichedContextResult = expandRouteContext(contextResult);
65
91
 
66
- const contextRecommendation = useIndexedContext
92
+ const contextRecommendation = useIndexedContext && degradedWarnings.length === 0
67
93
  ? buildContextRecommendation({
68
94
  commandNamespace,
69
95
  contextIntent,
@@ -72,8 +98,10 @@ export async function deriveTaskRoute({
72
98
  contextResult: enrichedContextResult,
73
99
  })
74
100
  : null;
75
- const verificationPlan = useIndexedContext
76
- ? await deriveVerificationPlan({
101
+ let verificationPlan = null;
102
+ if (useIndexedContext && contextResult) {
103
+ try {
104
+ verificationPlan = await deriveVerificationPlan({
77
105
  rootDir: absoluteRoot,
78
106
  intent: contextIntent,
79
107
  targetFile: normalizedTarget,
@@ -81,8 +109,11 @@ export async function deriveTaskRoute({
81
109
  contextResult: enrichedContextResult,
82
110
  skillIds: selectedIds,
83
111
  autonomyLevel,
84
- })
85
- : null;
112
+ });
113
+ } catch (error) {
114
+ degradedWarnings.push(`verify-context failed: ${error?.message ?? String(error)}`);
115
+ }
116
+ }
86
117
 
87
118
  const verificationRecommendation = verificationPlan
88
119
  ? {
@@ -112,11 +143,14 @@ export async function deriveTaskRoute({
112
143
  taskType: inferredTaskType,
113
144
  intentMode,
114
145
  autonomyLevel,
146
+ executionScores,
147
+ executionMode,
115
148
  },
116
149
  contextRecommendation,
117
150
  verificationRecommendation,
118
151
  nextAction,
119
152
  });
153
+ const approachSelector = routeSummary?.approachSelector ?? null;
120
154
 
121
155
  return {
122
156
  activeSkills,
@@ -129,11 +163,15 @@ export async function deriveTaskRoute({
129
163
  taskType: inferredTaskType,
130
164
  intentMode,
131
165
  autonomyLevel,
166
+ executionScores,
167
+ executionMode,
132
168
  },
169
+ approachSelector,
133
170
  contextRecommendation,
134
171
  verificationRecommendation,
135
172
  nextAction,
136
173
  routeSummary,
174
+ ...(degradedWarnings.length > 0 ? { degradedWarnings } : {}),
137
175
  };
138
176
  }
139
177
 
@@ -169,6 +207,21 @@ export function buildRouteSummary({
169
207
  const compactHelperLane = nextAction?.type === 'pull-indexed-context'
170
208
  && typeof contextRecommendation?.command === 'string'
171
209
  && contextRecommendation.command.trim();
210
+ const executionMode = routingContext.executionMode ?? null;
211
+ const executionScores = routingContext.executionScores ?? null;
212
+ const approachSelector = buildApproachSelectorResult({
213
+ executionMode,
214
+ executionScores,
215
+ });
216
+ const executionContract = buildExecutionContract(executionMode);
217
+ const completionState = buildCompletionState({
218
+ executionMode,
219
+ verificationRecommendation,
220
+ });
221
+ const continuationState = buildContinuationState({
222
+ nextActionType: nextAction?.type ?? null,
223
+ completionState,
224
+ });
172
225
  const helperHint = compactHelperHint(
173
226
  compactHelperLane
174
227
  ? contextRecommendation?.command
@@ -187,7 +240,6 @@ export function buildRouteSummary({
187
240
  editGuardHint ? `editGuard=${editGuardHint}` : null,
188
241
  delegationRecommendation?.hint ? `delegate=${delegationRecommendation.hint}` : null,
189
242
  policyMode ? `policy=${policyMode}` : null,
190
- contextMode ? `mode=${contextMode}` : null,
191
243
  ].filter(Boolean).join(' | ');
192
244
 
193
245
  return {
@@ -196,6 +248,12 @@ export function buildRouteSummary({
196
248
  preferredOrder,
197
249
  policyMode,
198
250
  editGuardHint,
251
+ executionMode,
252
+ executionScores,
253
+ approachSelector,
254
+ executionContract,
255
+ completionState,
256
+ continuationState,
199
257
  intentMode: routingContext.intentMode ?? null,
200
258
  delegateHint: delegationRecommendation?.hint ?? null,
201
259
  nextActionType: nextAction?.type ?? null,
@@ -212,6 +270,327 @@ function deriveContextMode(taskType) {
212
270
  return null;
213
271
  }
214
272
 
273
+ function deriveExecutionScores({
274
+ promptText = '',
275
+ commandText = '',
276
+ targetFile = null,
277
+ intentMode = null,
278
+ taskType = null,
279
+ } = {}) {
280
+ const raw = `${promptText ?? ''}\n${commandText ?? ''}`.toLowerCase();
281
+ const sharedRisk = isSharedImpactFile(targetFile);
282
+ const directTransformSignal = /\b(change|replace|set|rename|convert|swap)\b/.test(raw) || /\bchange\s+.+\s+to\s+.+\b/.test(raw);
283
+ const smallFixSignal = /\b(adjust|tweak|patch|fix|modify|update|apply)\b/.test(raw);
284
+ const buildSignal = /\b(build|create|add|implement|feature)\b/.test(raw);
285
+ const debugSignal = intentMode === 'debug-specific' || /\b(root cause|debug|triage|flaky|investigate|why)\b/.test(raw);
286
+ const reviewSignal = intentMode === 'review-specific' || /\b(review|audit|verify|release readiness)\b/.test(raw);
287
+
288
+ let editCertainty = 0;
289
+ if (directTransformSignal) {
290
+ editCertainty = 3;
291
+ } else if (smallFixSignal) {
292
+ editCertainty = 2;
293
+ } else if (buildSignal) {
294
+ editCertainty = 1;
295
+ }
296
+
297
+ let investigationNeed = 0;
298
+ if (debugSignal) {
299
+ investigationNeed = 3;
300
+ } else if (/\b(failing|broken|error|crash|timeout)\b/.test(raw)) {
301
+ investigationNeed = 2;
302
+ }
303
+
304
+ let blastRadius = 0;
305
+ if (sharedRisk) {
306
+ blastRadius = 3;
307
+ } else if (taskType === 'non-trivial' || taskType === 'shared-simple') {
308
+ blastRadius = 2;
309
+ } else if (!targetFile) {
310
+ blastRadius = 1;
311
+ }
312
+
313
+ let verificationBurden = 1;
314
+ if (reviewSignal || sharedRisk) {
315
+ verificationBurden = 3;
316
+ } else if (debugSignal || taskType === 'non-trivial') {
317
+ verificationBurden = 2;
318
+ }
319
+
320
+ let ambiguity = 0;
321
+ if (!targetFile) {
322
+ ambiguity = 2;
323
+ } else if (buildSignal && !directTransformSignal && !smallFixSignal) {
324
+ ambiguity = 1;
325
+ }
326
+
327
+ return {
328
+ editCertainty,
329
+ investigationNeed,
330
+ blastRadius,
331
+ verificationBurden,
332
+ ambiguity,
333
+ };
334
+ }
335
+
336
+ function deriveExecutionMode({
337
+ promptText = '',
338
+ commandText = '',
339
+ targetFile = null,
340
+ intentMode = null,
341
+ executionScores = {},
342
+ } = {}) {
343
+ const raw = `${promptText ?? ''}\n${commandText ?? ''}`.toLowerCase();
344
+ const scores = executionScores;
345
+ const explicitReviewLead = /\b(review this|audit this|review\b.*\brelease readiness|verify\b.*\brelease readiness)\b/.test(raw);
346
+
347
+ if (intentMode === 'review-specific' || explicitReviewLead) {
348
+ return 'review-release';
349
+ }
350
+
351
+ if (scores.blastRadius >= 3 && (scores.ambiguity >= 2 || scores.investigationNeed >= 2)) {
352
+ return 'map-impact';
353
+ }
354
+
355
+ if (scores.blastRadius >= 3 || isSharedImpactFile(targetFile)) {
356
+ return 'shared-edit';
357
+ }
358
+
359
+ if (scores.investigationNeed >= 3) {
360
+ return 'find-cause';
361
+ }
362
+
363
+ if (
364
+ scores.editCertainty >= 3
365
+ && scores.ambiguity === 0
366
+ && scores.blastRadius === 0
367
+ && scores.verificationBurden <= 1
368
+ ) {
369
+ return 'tiny-fix';
370
+ }
371
+
372
+ if (
373
+ /\b(build|create|add|implement|feature|summary card)\b/.test(raw)
374
+ && scores.investigationNeed === 0
375
+ && scores.blastRadius < 3
376
+ ) {
377
+ return 'local-build';
378
+ }
379
+
380
+ if (scores.editCertainty >= 2 && scores.investigationNeed === 0 && scores.blastRadius === 0) {
381
+ return 'local-fix';
382
+ }
383
+
384
+ return 'local-build';
385
+ }
386
+
387
+ function buildExecutionContract(executionMode = null) {
388
+ if (!executionMode) {
389
+ return null;
390
+ }
391
+
392
+ const contracts = {
393
+ 'tiny-fix': {
394
+ maxReadPasses: 0,
395
+ maxContextPulls: 0,
396
+ verificationPolicy: 'minimal-or-targeted',
397
+ completionRule: 'never-claim-done-without-write',
398
+ delegationPolicy: 'disallow',
399
+ completionEvidence: ['write-evidence'],
400
+ },
401
+ 'local-fix': {
402
+ maxReadPasses: 1,
403
+ maxContextPulls: 1,
404
+ verificationPolicy: 'targeted-if-covered',
405
+ completionRule: 'require-write',
406
+ delegationPolicy: 'disallow',
407
+ completionEvidence: ['write-evidence'],
408
+ },
409
+ 'local-build': {
410
+ maxReadPasses: 2,
411
+ maxContextPulls: 1,
412
+ verificationPolicy: 'targeted-if-covered',
413
+ completionRule: 'require-write',
414
+ delegationPolicy: 'disallow-by-default',
415
+ completionEvidence: ['write-evidence'],
416
+ },
417
+ 'find-cause': {
418
+ maxReadPassesBeforeReassess: 3,
419
+ verificationPolicy: 'root-cause-then-targeted',
420
+ completionRule: 'never-claim-fixed-without-write-and-verification',
421
+ delegationPolicy: 'allow-specialized-debug-lane',
422
+ completionEvidence: ['write-evidence', 'verification-evidence'],
423
+ },
424
+ 'shared-edit': {
425
+ maxReadPasses: 2,
426
+ maxContextPulls: 2,
427
+ verificationPolicy: 'targeted-then-widen-on-risk',
428
+ completionRule: 'require-write-and-verification',
429
+ delegationPolicy: 'allow-qualified-sidecar',
430
+ completionEvidence: ['write-evidence', 'verification-evidence'],
431
+ mirrorConsistencyRequired: true,
432
+ },
433
+ 'map-impact': {
434
+ maxReadPasses: 3,
435
+ maxContextPulls: 3,
436
+ verificationPolicy: 'impact-first-then-targeted-then-widen-on-risk',
437
+ completionRule: 'require-impact-evidence-before-edit-claim',
438
+ delegationPolicy: 'allow-impact-sidecar',
439
+ completionEvidence: ['impact-evidence', 'write-evidence', 'verification-evidence'],
440
+ mirrorConsistencyRequired: true,
441
+ },
442
+ 'review-release': {
443
+ verificationPolicy: 'evidence-first',
444
+ completionRule: 'report-findings-not-implementation',
445
+ delegationPolicy: 'allow-review-sidecar',
446
+ completionEvidence: ['verification-evidence'],
447
+ },
448
+ };
449
+
450
+ return contracts[executionMode] ? { ...contracts[executionMode] } : null;
451
+ }
452
+
453
+ function buildApproachSelectorResult({
454
+ executionMode = null,
455
+ executionScores = null,
456
+ } = {}) {
457
+ if (!executionMode) {
458
+ return null;
459
+ }
460
+
461
+ const executionContract = buildExecutionContract(executionMode);
462
+ const policyByMode = {
463
+ 'tiny-fix': {
464
+ riskLevel: 'minimal',
465
+ contextPolicy: 'confirm-target-only',
466
+ },
467
+ 'local-fix': {
468
+ riskLevel: 'local',
469
+ contextPolicy: 'bounded-local',
470
+ },
471
+ 'local-build': {
472
+ riskLevel: 'local',
473
+ contextPolicy: 'bounded-with-related-files',
474
+ },
475
+ 'find-cause': {
476
+ riskLevel: 'investigation',
477
+ contextPolicy: 'trace-then-bounded-context',
478
+ },
479
+ 'shared-edit': {
480
+ riskLevel: 'shared',
481
+ contextPolicy: 'bounded-with-related-tests',
482
+ },
483
+ 'map-impact': {
484
+ riskLevel: 'wide',
485
+ contextPolicy: 'impact-map-first',
486
+ },
487
+ 'review-release': {
488
+ riskLevel: 'release',
489
+ contextPolicy: 'evidence-review-only',
490
+ },
491
+ };
492
+ const selectedPolicy = policyByMode[executionMode] ?? {
493
+ riskLevel: 'local',
494
+ contextPolicy: 'bounded-local',
495
+ };
496
+
497
+ return {
498
+ executionMode,
499
+ executionScores: executionScores ? { ...executionScores } : null,
500
+ riskLevel: selectedPolicy.riskLevel,
501
+ contextPolicy: selectedPolicy.contextPolicy,
502
+ verificationPolicy: executionContract?.verificationPolicy ?? null,
503
+ completionRule: executionContract?.completionRule ?? null,
504
+ };
505
+ }
506
+
507
+ function buildCompletionState({ executionMode = null, verificationRecommendation = null } = {}) {
508
+ if (!executionMode) {
509
+ return null;
510
+ }
511
+
512
+ const contract = buildExecutionContract(executionMode);
513
+ const missingEvidence = [...(contract?.completionEvidence ?? [])];
514
+ const requiresVerification = missingEvidence.includes('verification-evidence');
515
+ if (
516
+ requiresVerification
517
+ && verificationRecommendation
518
+ && !(verificationRecommendation.commands?.length || verificationRecommendation.fallbackCommands?.length)
519
+ ) {
520
+ missingEvidence.push('verification-plan');
521
+ }
522
+
523
+ let reason = 'completion evidence is still required';
524
+ if (['tiny-fix', 'local-fix', 'local-build', 'shared-edit'].includes(executionMode)) {
525
+ reason = 'implement request has not produced an edit yet';
526
+ } else if (executionMode === 'review-release') {
527
+ reason = 'review/release evidence is still required before final claim';
528
+ } else if (executionMode === 'map-impact') {
529
+ reason = 'impact evidence is still required before safe completion claim';
530
+ }
531
+
532
+ return {
533
+ claimAllowed: false,
534
+ missingEvidence,
535
+ reason,
536
+ };
537
+ }
538
+
539
+ function buildContinuationState({
540
+ nextActionType = null,
541
+ completionState = null,
542
+ } = {}) {
543
+ const reasons = [];
544
+ const missingEvidence = unique(completionState?.missingEvidence ?? []);
545
+
546
+ if (nextActionType === 'pull-indexed-context') {
547
+ reasons.push('pending-bounded-context');
548
+ }
549
+
550
+ if (nextActionType === 'read-skill-instructions') {
551
+ reasons.push('pending-skill-read');
552
+ }
553
+
554
+ if (missingEvidence.includes('write-evidence')) {
555
+ reasons.push('missing-write-evidence');
556
+ }
557
+
558
+ if (missingEvidence.includes('verification-evidence') || missingEvidence.includes('verification-plan')) {
559
+ reasons.push('missing-verification-evidence');
560
+ }
561
+
562
+ if (missingEvidence.includes('impact-evidence')) {
563
+ reasons.push('missing-impact-evidence');
564
+ }
565
+
566
+ const nextMilestone = reasons.includes('pending-bounded-context')
567
+ ? 'complete-bounded-context-then-continue'
568
+ : (
569
+ reasons.includes('missing-write-evidence')
570
+ ? 'produce-write-evidence'
571
+ : (
572
+ reasons.includes('missing-verification-evidence')
573
+ ? 'produce-verification-evidence'
574
+ : (
575
+ reasons.includes('missing-impact-evidence')
576
+ ? 'produce-impact-evidence'
577
+ : null
578
+ )
579
+ )
580
+ );
581
+
582
+ return {
583
+ required: reasons.length > 0,
584
+ reasons,
585
+ doneLanguageBlocked: reasons.length > 0,
586
+ stopAllowedOnlyFor: reasons.length > 0 ? ['real-blocker'] : [],
587
+ nextMilestone,
588
+ repeatCount: reasons.length > 0 ? 1 : 0,
589
+ stuckRisk: null,
590
+ rescueMode: null,
591
+ };
592
+ }
593
+
215
594
  async function selectActiveSkills({ rootDir, promptText, commandText, targetFile, intentMode = null }) {
216
595
  const routeSignals = {
217
596
  promptRawText: String(promptText || '').toLowerCase(),
@@ -252,6 +631,10 @@ function shouldKeepRouteEntryForIntent(entry, intentMode) {
252
631
  return false;
253
632
  }
254
633
 
634
+ if (entry.id === 'code-review' && intentMode === 'implement-specific') {
635
+ return false;
636
+ }
637
+
255
638
  return true;
256
639
  }
257
640
 
@@ -296,6 +679,10 @@ function deriveSkillPriorityBoost(skillId, routeSignals = {}) {
296
679
  routeSignals.commandRawText ?? '',
297
680
  routeSignals.commandNormalizedText ?? '',
298
681
  ].join('\n');
682
+ const rawPromptSignals = [
683
+ routeSignals.promptRawText ?? '',
684
+ routeSignals.commandRawText ?? '',
685
+ ].join('\n');
299
686
 
300
687
  if (
301
688
  skillId === 'testing-quality'
@@ -304,6 +691,14 @@ function deriveSkillPriorityBoost(skillId, routeSignals = {}) {
304
691
  return 1;
305
692
  }
306
693
 
694
+ if (
695
+ skillId === 'code-review'
696
+ && /\b(implement|build|create|add|ship|deliver|refactor|integrate|integration|scaffold|feature|update|change|modify|inject|apply)\b/.test(rawPromptSignals)
697
+ && !/\b(review|audit|diff|pr feedback|code review|kiem tra|soat)\b/.test(rawPromptSignals)
698
+ ) {
699
+ return -4;
700
+ }
701
+
307
702
  return 0;
308
703
  }
309
704
 
@@ -357,10 +752,10 @@ function deriveIntentMode({ promptText = '', commandText = '', targetFile = null
357
752
  if (/\b(bug|debug|error|crash|broken|failing|stack trace|triage|fix|login|timeout)\b/.test(lower)) {
358
753
  return 'debug-specific';
359
754
  }
360
- if (/\b(review|audit|diff|pr feedback|code review|kiem tra|soat)\b/.test(lower)) {
755
+ if (/\b(review|audit|diff|pr feedback|code review|kiem tra|soat)\b/.test(raw)) {
361
756
  return 'review-specific';
362
757
  }
363
- if (/\b(implement|build|create|add|ship|deliver|refactor|integrate|integration|scaffold|feature)\b/.test(lower)) {
758
+ if (/\b(implement|build|create|add|ship|deliver|refactor|integrate|integration|scaffold|feature|update|change|modify|inject|apply)\b/.test(raw)) {
364
759
  return 'implement-specific';
365
760
  }
366
761
  }
@@ -392,7 +787,7 @@ function hasConcreteTaskSignal(lower, raw, targetFile, { taskQueueNext = false }
392
787
  return true;
393
788
  }
394
789
 
395
- return /\b(bug|debug|error|crash|broken|failing|stack trace|triage|fix|implement|build|create|add|ship|deliver|refactor|integrate|integration|scaffold|feature|review|audit|diff|pr feedback|code review|auth|login|api|endpoint|test|spec)\b/.test(lower)
790
+ return /\b(bug|debug|error|crash|broken|failing|stack trace|triage|fix|implement|build|create|add|ship|deliver|refactor|integrate|integration|scaffold|feature|update|change|modify|inject|apply|review|audit|diff|pr feedback|code review|auth|login|api|endpoint|test|spec)\b/.test(lower)
396
791
  || /\b(sửa|fix|lỗi|bug|debug|implement|cài|thêm|review|kiểm tra|soát|đăng nhập)\b/.test(raw);
397
792
  }
398
793
 
@@ -857,12 +1252,36 @@ function normalizeRelativeFile(rootDir, rawFilePath) {
857
1252
  if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
858
1253
  return relative.replaceAll('\\', '/');
859
1254
  }
860
- return trimmed.replaceAll('\\', '/');
1255
+ return null;
861
1256
  }
862
1257
 
863
1258
  return trimmed.replace(/^\.\/+/, '').replaceAll('\\', '/');
864
1259
  }
865
1260
 
1261
+ async function findUnreadableIndexArtifact(rootDir) {
1262
+ const indexDir = path.join(rootDir, '.cache', 'index');
1263
+ let entries;
1264
+ try {
1265
+ entries = await fs.readdir(indexDir);
1266
+ } catch {
1267
+ return null;
1268
+ }
1269
+
1270
+ for (const entry of entries) {
1271
+ if (!entry.endsWith('.json')) {
1272
+ continue;
1273
+ }
1274
+
1275
+ try {
1276
+ JSON.parse(await fs.readFile(path.join(indexDir, entry), 'utf8'));
1277
+ } catch (error) {
1278
+ return new Error(`${entry}: ${error?.message ?? String(error)}`);
1279
+ }
1280
+ }
1281
+
1282
+ return null;
1283
+ }
1284
+
866
1285
  async function pathExists(filePath) {
867
1286
  try {
868
1287
  await fs.access(filePath);
@@ -879,6 +1298,7 @@ function unique(values) {
879
1298
  const DELEGATABLE_IMPLEMENTATION_SKILL_IDS = new Set([
880
1299
  'delivery',
881
1300
  'frontend',
1301
+ 'frontend-design',
882
1302
  'frontend-vue',
883
1303
  'backend-api',
884
1304
  'postgres',
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
+ import { detectPackageManagerFromFingerprint, getDeclaredPackageManager } from '../core/packageManager.js';
4
5
  import { resolveContext } from './resolveContext.js';
5
6
 
6
7
  const RISKY_SKILL_IDS = new Set(['discover-security', 'repo-maintenance']);
@@ -277,7 +278,7 @@ async function loadProjectVerificationEnvironment({
277
278
  .then((resolvedPkg) => ({
278
279
  pkg: resolvedPkg,
279
280
  scripts: extractScripts(resolvedPkg),
280
- packageManager: detectPackageManager({ fingerprintEntries, pkg: resolvedPkg }),
281
+ packageManager: detectPackageManagerFromFingerprint({ fingerprintEntries, pkg: resolvedPkg }),
281
282
  }))
282
283
  .catch((error) => {
283
284
  PROJECT_VERIFICATION_ENV_CACHE.delete(cacheKey);
@@ -297,41 +298,6 @@ function extractScripts(pkg = null) {
297
298
  return pkg?.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
298
299
  }
299
300
 
300
- function detectPackageManager({ fingerprintEntries = [], pkg = null } = {}) {
301
- const declaredPackageManager = getDeclaredPackageManager(pkg);
302
- if (declaredPackageManager) return declaredPackageManager;
303
-
304
- const existingFiles = new Set(
305
- fingerprintEntries
306
- .filter((entry) => entry && entry.mtimeMs !== null)
307
- .map((entry) => entry.filePath),
308
- );
309
- const checks = [
310
- ['pnpm-lock.yaml', 'pnpm'],
311
- ['yarn.lock', 'yarn'],
312
- ['bun.lockb', 'bun'],
313
- ['bun.lock', 'bun'],
314
- ['package-lock.json', 'npm'],
315
- ];
316
-
317
- for (const [lockFile, pm] of checks) {
318
- if (existingFiles.has(lockFile)) {
319
- return pm;
320
- }
321
- }
322
-
323
- return 'npm';
324
- }
325
-
326
- function getDeclaredPackageManager(pkg = null) {
327
- const declared = String(pkg?.packageManager ?? '').toLowerCase();
328
- if (declared.startsWith('pnpm')) return 'pnpm';
329
- if (declared.startsWith('yarn')) return 'yarn';
330
- if (declared.startsWith('bun')) return 'bun';
331
- if (declared.startsWith('npm')) return 'npm';
332
- return null;
333
- }
334
-
335
301
  function buildScriptCommand(packageManager, scriptName, args = []) {
336
302
  const filteredArgs = args.filter(Boolean);
337
303
 
@@ -23,6 +23,8 @@ cat <<'FALLBACK'
23
23
  - Auto-activate matching project-local skills from prompt + tool/file signals.
24
24
  - Reuse existing code. Use yarn. Run tests after code changes.
25
25
  - Scope-based verification: match verify effort to change scope.
26
+ - If a routed lane still needs `pull-indexed-context` or still has completion debt, treat that as internal continuation work, not a stopping point.
27
+ - Do not end with “done/applied/fixed” language while write/verification evidence is still missing unless a real blocker is found.
26
28
  - If broad verification gets blocked by a Claude hook, follow the routed targeted order or ask for explicit full-suite confirmation.
27
29
  === END ===
28
30
  FALLBACK
@@ -8,6 +8,8 @@
8
8
  - Auto-activate matching project-local skills from prompt + tool/file signals; end users should not need to know skill names.
9
9
  - After routing skill + context, prefer verification derived from related tests/context bundle before broad test runs.
10
10
  - Follow routed verification policy: auto-run targeted checks when localized, widen in order for shared/risky scope, and ask before blanket broad runs when no related test evidence exists.
11
+ - Nếu route còn `pull-indexed-context`, coi đó là bước nội bộ để tiếp tục chứ không phải điểm dừng; đọc xong context thì tiếp tục edit/verify ngay khi an toàn.
12
+ - Không được kết thúc bằng “done/applied/fixed” khi còn thiếu write-evidence hoặc verification-evidence, trừ khi có blocker thật.
11
13
  - If Claude blocks a broad verification command, do not fight the hook blindly — use the targeted indexed lane first or get explicit confirmation for a full-suite run.
12
14
  - Nếu mơ hồ hoặc risk cao (security/migration/auth/data-loss/uninstall-core), escalate đọc sâu hơn hoặc hỏi 1 câu clarify ngắn.
13
15
  - Docs là navigation aid, source code là ground truth. Nếu mâu thuẫn → source thắng, update docs trước khi tiếp tục.