From b6b17ff43fab51f0f528c87863c907f82190b4a0 Mon Sep 17 00:00:00 2001 From: "shit-agents[bot]" Date: Sun, 6 Sep 2026 07:23:06 -0700 Subject: [PATCH 1/2] night-shift: knowledge-161-writegate-stderr --- app/Commands/KnowledgeAddCommand.php | 52 +++++++++++-------- app/Mcp/Tools/RememberTool.php | 10 ++-- .../Commands/KnowledgeAddCommandTest.php | 25 ++++++++- 3 files changed, 59 insertions(+), 28 deletions(-) diff --git a/app/Commands/KnowledgeAddCommand.php b/app/Commands/KnowledgeAddCommand.php index 063df04..39a6cc4 100644 --- a/app/Commands/KnowledgeAddCommand.php +++ b/app/Commands/KnowledgeAddCommand.php @@ -13,7 +13,6 @@ use Illuminate\Support\Str; use LaravelZero\Framework\Commands\Command; -use function Laravel\Prompts\error; use function Laravel\Prompts\info; use function Laravel\Prompts\spin; use function Laravel\Prompts\table; @@ -42,6 +41,7 @@ class KnowledgeAddCommand extends Command {--no-git : Skip automatic git context detection} {--force : Skip write gate and duplicate detection} {--skip-enhance : Skip queueing for Ollama enhancement} + {--quiet : Suppress stderr messages} {--project= : Override project namespace} {--global : Search across all projects}'; @@ -91,38 +91,39 @@ public function handle(GitContextService $gitService, QdrantService $qdrant, Wri $force = (bool) $this->option('force'); /** @var bool $skipEnhance */ $skipEnhance = (bool) $this->option('skip-enhance'); + /** @var bool $quiet */ + $quiet = (bool) $this->option('quiet'); // Validate required fields if ($content === null || $content === '') { - error('The content field is required.'); + $this->writeToStderr('The content field is required.', $quiet); return self::FAILURE; } // Validate confidence if (! is_numeric($confidence) || $confidence < 0 || $confidence > 100) { - error('The confidence must be between 0 and 100.'); + $this->writeToStderr('The confidence must be between 0 and 100.', $quiet); return self::FAILURE; } - // Validate category + // Validate category - unknown category is not an error, just warn if ($category !== null && ! in_array($category, self::VALID_CATEGORIES, true)) { - error('Invalid category. Valid: '.implode(', ', self::VALID_CATEGORIES)); - - return self::FAILURE; + $this->writeToStderr('Unknown category "'.$category.'". Ignoring category.', $quiet); + $category = null; } // Validate priority if (! in_array($priority, self::VALID_PRIORITIES, true)) { - error('Invalid priority. Valid: '.implode(', ', self::VALID_PRIORITIES)); + $this->writeToStderr('Invalid priority. Valid: '.implode(', ', self::VALID_PRIORITIES), $quiet); return self::FAILURE; } // Validate status if (! in_array($status, self::VALID_STATUSES, true)) { - error('Invalid status. Valid: '.implode(', ', self::VALID_STATUSES)); + $this->writeToStderr('Invalid status. Valid: '.implode(', ', self::VALID_STATUSES), $quiet); return self::FAILURE; } @@ -163,7 +164,7 @@ public function handle(GitContextService $gitService, QdrantService $qdrant, Wri if (! $force) { $gateResult = $writeGate->evaluate($data); if (! $gateResult['passed']) { - error('Write gate rejected entry: '.$gateResult['reason']); + $this->writeToStderr('Write gate rejected entry: '.$gateResult['reason'], $quiet); return self::FAILURE; } @@ -182,12 +183,12 @@ public function handle(GitContextService $gitService, QdrantService $qdrant, Wri ); if (! $success) { - error('Failed to create knowledge entry'); + $this->writeToStderr('Failed to create knowledge entry', $quiet); return self::FAILURE; } } catch (DuplicateEntryException $e) { - return $this->handleDuplicate($e, $data, $qdrant, (int) $confidence); + return $this->handleDuplicate($e, $data, (int) $confidence, $quiet); } // Queue for Ollama enhancement unless skipped @@ -203,30 +204,30 @@ public function handle(GitContextService $gitService, QdrantService $qdrant, Wri } /** - * Handle a duplicate entry by offering to supersede or aborting. + * Handle a duplicate entry. * * @param array $data */ private function handleDuplicate( DuplicateEntryException $e, array $data, - QdrantService $qdrant, - int $confidence + int $confidence, + bool $quiet ): int { $existingId = $e->existingId; if ($e->duplicateType === DuplicateEntryException::TYPE_HASH) { - error("Duplicate content detected: This exact content already exists as entry '{$existingId}'"); + $this->writeToStderr("Duplicate content detected: This exact content already exists as entry '{$existingId}'", $quiet); - return self::FAILURE; + return self::SUCCESS; } $percentage = $e->similarityScore !== null ? round($e->similarityScore * 100, 1) : 95; - warning("Potential duplicate detected: {$percentage}% similar to existing entry '{$existingId}'"); + $this->writeToStderr("Potential duplicate detected: {$percentage}% similar to existing entry '{$existingId}'", $quiet); // Require confirmation when confidence is low (below 70) if ($confidence < 70) { - warning("Low confidence ({$confidence}%) - please confirm this supersedes the existing entry."); + $this->writeToStderr("Low confidence ({$confidence}%) - please confirm this supersedes the existing entry.", $quiet); } $shouldSupersede = $this->confirm( @@ -235,7 +236,7 @@ private function handleDuplicate( ); if (! $shouldSupersede) { - error('Entry not created. Existing knowledge preserved.'); + $this->writeToStderr('Entry not created. Existing knowledge preserved.', $quiet); return self::FAILURE; } @@ -247,7 +248,7 @@ private function handleDuplicate( ); if (! $success) { - error('Failed to create knowledge entry'); + $this->writeToStderr('Failed to create knowledge entry', $quiet); return self::FAILURE; } @@ -257,7 +258,7 @@ private function handleDuplicate( $marked = $qdrant->markSuperseded($existingId, $data['id'], $reason); if (! $marked) { - warning('New entry created but failed to mark old entry as superseded.'); + $this->writeToStderr('New entry created but failed to mark old entry as superseded.', $quiet); } info('Knowledge entry created! Previous entry marked as superseded.'); @@ -276,6 +277,13 @@ private function handleDuplicate( return self::SUCCESS; } + private function writeToStderr(string $message, bool $quiet): void + { + if (! $quiet) { + fwrite(STDERR, $message.PHP_EOL); + } + } + /** * Display the entry summary table. * diff --git a/app/Mcp/Tools/RememberTool.php b/app/Mcp/Tools/RememberTool.php index f077a12..8b0ed79 100644 --- a/app/Mcp/Tools/RememberTool.php +++ b/app/Mcp/Tools/RememberTool.php @@ -78,12 +78,14 @@ public function handle(Request $request): Response try { $this->qdrant->upsert($entry, $project, true); } catch (DuplicateEntryException $e) { - return Response::text(json_encode([ + $response = json_encode([ 'status' => 'duplicate_detected', 'existing_id' => $e->existingId, 'similarity' => $e->similarityScore !== null ? round($e->similarityScore * 100, 1) : null, 'message' => "Similar entry already exists (ID: {$e->existingId}). Use the `correct` tool to update it, or add more distinct content.", - ], JSON_THROW_ON_ERROR)); + ], JSON_THROW_ON_ERROR); + + return Response::text($response); } // Queue for Ollama auto-tagging @@ -111,8 +113,8 @@ public function schema(JsonSchema $schema): array ->description('Detailed description of the discovery or insight (10-10000 chars).') ->required(), 'category' => $schema->string() - ->enum(['architecture', 'patterns', 'decisions', 'gotchas', 'debugging', 'testing', 'deployment', 'security']) - ->description('Knowledge category. Omit to let auto-tagging classify it.'), + ->description('Knowledge category. Omit to let auto-tagging classify it.') + ->nullable(), 'tags' => $schema->array() ->description('Tags for categorization (max 10). e.g., ["laravel", "pest", "testing"]'), 'priority' => $schema->string() diff --git a/tests/Feature/Commands/KnowledgeAddCommandTest.php b/tests/Feature/Commands/KnowledgeAddCommandTest.php index 3d62fa8..13eacb7 100644 --- a/tests/Feature/Commands/KnowledgeAddCommandTest.php +++ b/tests/Feature/Commands/KnowledgeAddCommandTest.php @@ -93,13 +93,16 @@ }); it('validates category must be valid enum value', function (): void { - $this->mockQdrant->shouldNotReceive('upsert'); + $this->mockQdrant->shouldReceive('upsert') + ->once() + ->with(Mockery::on(fn ($data): bool => $data['category'] === null), Mockery::any(), Mockery::any()) + ->andReturn(true); $this->artisan('add', [ 'title' => 'Test Entry', '--content' => 'Content', '--category' => 'invalid-category', - ])->assertFailed(); + ])->assertSuccessful(); }); it('validates status must be valid enum value', function (): void { @@ -398,3 +401,21 @@ '--content' => 'Enhancement must look in the same collection the entry was stored in', ])->assertSuccessful(); }); + +it('does not queue enhancement when --skip-enhance flag is used', function (): void { + $this->mockQdrant->shouldReceive('upsert') + ->once() + ->andReturn(true); + + $mockQueue = Mockery::mock(\App\Services\EnhancementQueueService::class); + $mockQueue->shouldNotReceive('queue'); + $this->app->instance(\App\Services\EnhancementQueueService::class, $mockQueue); + + config(['search.ollama.enabled' => true]); + + $this->artisan('add', [ + 'title' => 'Skip Enhancement Entry', + '--content' => 'Content', + '--skip-enhance' => true, + ])->assertSuccessful(); +}); From c9cc6ea7c66f1896e4651e95834e6ce74bf2fe70 Mon Sep 17 00:00:00 2001 From: "shit-agents[bot]" Date: Sun, 6 Sep 2026 07:43:35 -0700 Subject: [PATCH 2/2] fix(knowledge): drop colliding --quiet; restore add semantics Custom {--quiet} collided with Symfony's built-in and blew up every KnowledgeAddCommand test. Use output->isQuiet() instead, dual-write errors to Prompts + STDERR, keep hard category/hash failure paths, and restore QdrantService in handleDuplicate. --- app/Commands/KnowledgeAddCommand.php | 74 +++++++++++-------- .../Commands/KnowledgeAddCommandTest.php | 7 +- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/app/Commands/KnowledgeAddCommand.php b/app/Commands/KnowledgeAddCommand.php index 39a6cc4..ed03f01 100644 --- a/app/Commands/KnowledgeAddCommand.php +++ b/app/Commands/KnowledgeAddCommand.php @@ -13,6 +13,7 @@ use Illuminate\Support\Str; use LaravelZero\Framework\Commands\Command; +use function Laravel\Prompts\error; use function Laravel\Prompts\info; use function Laravel\Prompts\spin; use function Laravel\Prompts\table; @@ -41,7 +42,6 @@ class KnowledgeAddCommand extends Command {--no-git : Skip automatic git context detection} {--force : Skip write gate and duplicate detection} {--skip-enhance : Skip queueing for Ollama enhancement} - {--quiet : Suppress stderr messages} {--project= : Override project namespace} {--global : Search across all projects}'; @@ -91,39 +91,40 @@ public function handle(GitContextService $gitService, QdrantService $qdrant, Wri $force = (bool) $this->option('force'); /** @var bool $skipEnhance */ $skipEnhance = (bool) $this->option('skip-enhance'); - /** @var bool $quiet */ - $quiet = (bool) $this->option('quiet'); + // Built-in -q/--quiet; do not declare {--quiet} (Symfony collision). + $quiet = $this->output->isQuiet(); // Validate required fields if ($content === null || $content === '') { - $this->writeToStderr('The content field is required.', $quiet); + $this->emitError('The content field is required.', $quiet); return self::FAILURE; } // Validate confidence if (! is_numeric($confidence) || $confidence < 0 || $confidence > 100) { - $this->writeToStderr('The confidence must be between 0 and 100.', $quiet); + $this->emitError('The confidence must be between 0 and 100.', $quiet); return self::FAILURE; } - // Validate category - unknown category is not an error, just warn + // Validate category if ($category !== null && ! in_array($category, self::VALID_CATEGORIES, true)) { - $this->writeToStderr('Unknown category "'.$category.'". Ignoring category.', $quiet); - $category = null; + $this->emitError('Invalid category. Valid: '.implode(', ', self::VALID_CATEGORIES), $quiet); + + return self::FAILURE; } // Validate priority if (! in_array($priority, self::VALID_PRIORITIES, true)) { - $this->writeToStderr('Invalid priority. Valid: '.implode(', ', self::VALID_PRIORITIES), $quiet); + $this->emitError('Invalid priority. Valid: '.implode(', ', self::VALID_PRIORITIES), $quiet); return self::FAILURE; } // Validate status if (! in_array($status, self::VALID_STATUSES, true)) { - $this->writeToStderr('Invalid status. Valid: '.implode(', ', self::VALID_STATUSES), $quiet); + $this->emitError('Invalid status. Valid: '.implode(', ', self::VALID_STATUSES), $quiet); return self::FAILURE; } @@ -164,7 +165,7 @@ public function handle(GitContextService $gitService, QdrantService $qdrant, Wri if (! $force) { $gateResult = $writeGate->evaluate($data); if (! $gateResult['passed']) { - $this->writeToStderr('Write gate rejected entry: '.$gateResult['reason'], $quiet); + $this->emitError('Write gate rejected entry: '.$gateResult['reason'], $quiet); return self::FAILURE; } @@ -183,12 +184,12 @@ public function handle(GitContextService $gitService, QdrantService $qdrant, Wri ); if (! $success) { - $this->writeToStderr('Failed to create knowledge entry', $quiet); + $this->emitError('Failed to create knowledge entry', $quiet); return self::FAILURE; } } catch (DuplicateEntryException $e) { - return $this->handleDuplicate($e, $data, (int) $confidence, $quiet); + return $this->handleDuplicate($e, $data, $qdrant, (int) $confidence, $quiet); } // Queue for Ollama enhancement unless skipped @@ -204,30 +205,31 @@ public function handle(GitContextService $gitService, QdrantService $qdrant, Wri } /** - * Handle a duplicate entry. + * Handle a duplicate entry by offering to supersede or aborting. * * @param array $data */ private function handleDuplicate( DuplicateEntryException $e, array $data, + QdrantService $qdrant, int $confidence, - bool $quiet + bool $quiet = false ): int { $existingId = $e->existingId; if ($e->duplicateType === DuplicateEntryException::TYPE_HASH) { - $this->writeToStderr("Duplicate content detected: This exact content already exists as entry '{$existingId}'", $quiet); + $this->emitError("Duplicate content detected: This exact content already exists as entry '{$existingId}'", $quiet); - return self::SUCCESS; + return self::FAILURE; } $percentage = $e->similarityScore !== null ? round($e->similarityScore * 100, 1) : 95; - $this->writeToStderr("Potential duplicate detected: {$percentage}% similar to existing entry '{$existingId}'", $quiet); + $this->emitWarning("Potential duplicate detected: {$percentage}% similar to existing entry '{$existingId}'", $quiet); // Require confirmation when confidence is low (below 70) if ($confidence < 70) { - $this->writeToStderr("Low confidence ({$confidence}%) - please confirm this supersedes the existing entry.", $quiet); + $this->emitWarning("Low confidence ({$confidence}%) - please confirm this supersedes the existing entry.", $quiet); } $shouldSupersede = $this->confirm( @@ -236,7 +238,7 @@ private function handleDuplicate( ); if (! $shouldSupersede) { - $this->writeToStderr('Entry not created. Existing knowledge preserved.', $quiet); + $this->emitError('Entry not created. Existing knowledge preserved.', $quiet); return self::FAILURE; } @@ -248,7 +250,7 @@ private function handleDuplicate( ); if (! $success) { - $this->writeToStderr('Failed to create knowledge entry', $quiet); + $this->emitError('Failed to create knowledge entry', $quiet); return self::FAILURE; } @@ -258,7 +260,7 @@ private function handleDuplicate( $marked = $qdrant->markSuperseded($existingId, $data['id'], $reason); if (! $marked) { - $this->writeToStderr('New entry created but failed to mark old entry as superseded.', $quiet); + $this->emitWarning('New entry created but failed to mark old entry as superseded.', $quiet); } info('Knowledge entry created! Previous entry marked as superseded.'); @@ -277,18 +279,32 @@ private function handleDuplicate( return self::SUCCESS; } - private function writeToStderr(string $message, bool $quiet): void - { - if (! $quiet) { - fwrite(STDERR, $message.PHP_EOL); - } - } - /** * Display the entry summary table. * * @param array|null $tags */ + + private function emitError(string $message, bool $quiet = false): void + { + if ($quiet) { + return; + } + + error($message); + fwrite(STDERR, $message.PHP_EOL); + } + + private function emitWarning(string $message, bool $quiet = false): void + { + if ($quiet) { + return; + } + + warning($message); + fwrite(STDERR, $message.PHP_EOL); + } + private function displayEntryTable( string $id, string $title, diff --git a/tests/Feature/Commands/KnowledgeAddCommandTest.php b/tests/Feature/Commands/KnowledgeAddCommandTest.php index 13eacb7..db36b2b 100644 --- a/tests/Feature/Commands/KnowledgeAddCommandTest.php +++ b/tests/Feature/Commands/KnowledgeAddCommandTest.php @@ -93,16 +93,13 @@ }); it('validates category must be valid enum value', function (): void { - $this->mockQdrant->shouldReceive('upsert') - ->once() - ->with(Mockery::on(fn ($data): bool => $data['category'] === null), Mockery::any(), Mockery::any()) - ->andReturn(true); + $this->mockQdrant->shouldNotReceive('upsert'); $this->artisan('add', [ 'title' => 'Test Entry', '--content' => 'Content', '--category' => 'invalid-category', - ])->assertSuccessful(); + ])->assertFailed(); }); it('validates status must be valid enum value', function (): void {