salmon-loop 0.4.1 → 0.5.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.
- package/dist/cli/authorization/provider.js +2 -10
- package/dist/cli/commands/config.js +2 -2
- package/dist/cli/commands/mode.js +2 -2
- package/dist/cli/commands/run/handler.js +3 -1
- package/dist/cli/commands/run/loop-params.js +1 -0
- package/dist/cli/commands/run/runtime-options.js +3 -1
- package/dist/cli/config.js +0 -8
- package/dist/cli/locales/en.js +2 -2
- package/dist/cli/reporters/standard.js +10 -0
- package/dist/core/adapters/fs/file-adapter.js +3 -1
- package/dist/core/adapters/git/git-adapter.js +6 -3
- package/dist/core/adapters/git/git-runner.js +5 -2
- package/dist/core/adapters/git/lock-manager.js +7 -4
- package/dist/core/checkpoint-domain/manifest-store.js +21 -13
- package/dist/core/checkpoint-domain/service.js +3 -1
- package/dist/core/config/limits.js +1 -1
- package/dist/core/config/model-pricing.js +61 -0
- package/dist/core/context/ast/skeleton-extractor.js +225 -0
- package/dist/core/context/ast/source-outline.js +24 -1
- package/dist/core/context/budget/dynamic-adjuster.js +20 -5
- package/dist/core/context/builder.js +7 -3
- package/dist/core/context/cache/store-factory.js +3 -1
- package/dist/core/context/dependencies.js +2 -1
- package/dist/core/context/effectiveness/persistence.js +50 -0
- package/dist/core/context/effectiveness/tracker.js +24 -0
- package/dist/core/context/gatherers/architecture-gatherer.js +2 -1
- package/dist/core/context/gatherers/artifact-gatherer.js +7 -4
- package/dist/core/context/gatherers/ast-gatherer.js +30 -28
- package/dist/core/context/gatherers/git-history-gatherer.js +3 -1
- package/dist/core/context/gatherers/knowledge-gatherer.js +18 -2
- package/dist/core/context/gatherers/metadata-gatherer.js +12 -7
- package/dist/core/context/gatherers/ripgrep-gatherer.js +6 -3
- package/dist/core/context/service.js +4 -2
- package/dist/core/context/steps/context-gather.js +14 -3
- package/dist/core/context/steps/context-targets.js +1 -0
- package/dist/core/context/targeting/target-resolver.js +29 -11
- package/dist/core/context/token/cache.js +5 -2
- package/dist/core/context/truncation/strategies/json.js +5 -2
- package/dist/core/context/truncation/type-detector.js +3 -1
- package/dist/core/extensions/paths.js +2 -2
- package/dist/core/facades/cli-authorization-provider.js +1 -0
- package/dist/core/feedback/parsers.js +290 -1
- package/dist/core/grizzco/dsl/llm-strategy.js +1 -1
- package/dist/core/grizzco/engine/observability/loop-telemetry.js +5 -2
- package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +15 -3
- package/dist/core/grizzco/engine/transaction/attempt-failure.js +44 -20
- package/dist/core/grizzco/engine/transaction/transaction-runner.js +40 -34
- package/dist/core/grizzco/execution/RejectionManager.js +7 -5
- package/dist/core/grizzco/runtime/apply-back-runtime.js +3 -1
- package/dist/core/grizzco/services/implementations/default/GitConfigService.js +2 -1
- package/dist/core/grizzco/steps/autopilot.js +21 -32
- package/dist/core/grizzco/steps/explore.js +5 -2
- package/dist/core/grizzco/steps/generateReview.js +3 -1
- package/dist/core/grizzco/steps/research.js +3 -1
- package/dist/core/grizzco/steps/verify.js +7 -1
- package/dist/core/grizzco/validation/AstValidationService.js +3 -1
- package/dist/core/history/input-history.js +3 -1
- package/dist/core/intent/chat-intent.js +3 -1
- package/dist/core/llm/ai-sdk/message-mapper.js +13 -8
- package/dist/core/llm/ai-sdk/request-params.js +1 -3
- package/dist/core/llm/ai-sdk/retry-classifier.js +12 -4
- package/dist/core/llm/ai-sdk/retry-executor.js +1 -1
- package/dist/core/llm/errors.js +5 -4
- package/dist/core/llm/retry-utils.js +8 -2
- package/dist/core/llm/stream-utils.js +5 -3
- package/dist/core/llm/sub-agent-factory.js +3 -0
- package/dist/core/mcp/bridge/resource-context-provider.js +3 -1
- package/dist/core/mcp/catalog/discovery.js +3 -1
- package/dist/core/mcp/client/connection-manager.js +4 -2
- package/dist/core/mcp/client/transport-factory.js +7 -3
- package/dist/core/observability/audit-file.js +2 -1
- package/dist/core/observability/audit-trail.js +3 -1
- package/dist/core/observability/logger.js +2 -1
- package/dist/core/observability/monitor.js +24 -0
- package/dist/core/observability/run-outcome-reporter.js +1 -0
- package/dist/core/permission-gate/default-gate.js +5 -8
- package/dist/core/plan/storage.js +7 -4
- package/dist/core/plugin/loader.js +3 -1
- package/dist/core/prompts/registry.js +1 -1
- package/dist/core/prompts/runtime.js +3 -1
- package/dist/core/prompts/templates/system/autopilot_system.hbs +28 -4
- package/dist/core/protocols/a2a/sdk/executor.js +3 -1
- package/dist/core/protocols/a2a/sdk/server.js +3 -1
- package/dist/core/protocols/acp/acp-command-runner.js +7 -6
- package/dist/core/protocols/acp/acp-session-persistence.js +13 -10
- package/dist/core/protocols/acp/formal-agent.js +3 -2
- package/dist/core/protocols/acp/permission-provider.js +3 -2
- package/dist/core/reflection/engine.js +114 -14
- package/dist/core/runtime/batch-runner.js +81 -0
- package/dist/core/runtime/initialize.js +2 -1
- package/dist/core/runtime/loop-finalize.js +3 -0
- package/dist/core/runtime/loop-session-runner.js +5 -0
- package/dist/core/runtime/loop.js +4 -0
- package/dist/core/runtime/paths.js +9 -6
- package/dist/core/runtime/spawn-interactive.js +5 -4
- package/dist/core/security/redaction.js +3 -2
- package/dist/core/session/compression.js +3 -1
- package/dist/core/session/manager.js +2 -1
- package/dist/core/session/pruning-strategy.js +2 -1
- package/dist/core/session/token-tracker.js +11 -4
- package/dist/core/skills/permissions.js +2 -2
- package/dist/core/strata/checkpoint/manager.js +16 -10
- package/dist/core/strata/checkpoint/snapshot-create.js +5 -4
- package/dist/core/strata/checkpoint/snapshot-write-tree.js +7 -3
- package/dist/core/strata/engine/shadow-merge-engine.js +4 -2
- package/dist/core/strata/interaction/file-system-provider.js +2 -1
- package/dist/core/strata/layers/file-state-resolver.js +9 -7
- package/dist/core/strata/layers/immutable-git-layer.js +3 -1
- package/dist/core/strata/layers/shadow-driver/readonly-lock.js +8 -6
- package/dist/core/strata/layers/shadow-driver/shadow-driver.js +2 -1
- package/dist/core/strata/layers/worktree.js +2 -1
- package/dist/core/strata/runtime/environment.js +2 -1
- package/dist/core/strata/runtime/synchronizer.js +18 -17
- package/dist/core/structured-output/json-extract.js +3 -1
- package/dist/core/sub-agent/artifacts/store.js +2 -1
- package/dist/core/sub-agent/core/manager.js +24 -1
- package/dist/core/sub-agent/registry-defaults.js +2 -2
- package/dist/core/sub-agent/summary.js +96 -0
- package/dist/core/sub-agent/tools/task-spawn.js +7 -4
- package/dist/core/target-runtime/profile.js +3 -1
- package/dist/core/tools/audit.js +3 -2
- package/dist/core/tools/budget.js +3 -1
- package/dist/core/tools/builtin/ast.js +144 -0
- package/dist/core/tools/builtin/code-search/backends/powershell.js +3 -1
- package/dist/core/tools/builtin/code-search/backends/rg.js +3 -1
- package/dist/core/tools/builtin/code-search/parse/plain-grep.js +3 -1
- package/dist/core/tools/builtin/code-search/parse/rg-json.js +3 -1
- package/dist/core/tools/builtin/fs.js +76 -1
- package/dist/core/tools/builtin/git.js +242 -0
- package/dist/core/tools/builtin/glob.js +79 -0
- package/dist/core/tools/builtin/index.js +12 -4
- package/dist/core/tools/builtin/knowledge.js +146 -4
- package/dist/core/tools/builtin/proposal.js +3 -1
- package/dist/core/tools/builtin/verify.js +35 -3
- package/dist/core/tools/permissions/permission-rules.js +3 -1
- package/dist/core/tools/router.js +88 -5
- package/dist/core/tools/session.js +10 -5
- package/dist/core/types/batch.js +2 -0
- package/dist/core/utils/sanitizer.js +5 -2
- package/dist/core/utils/serialize.js +5 -2
- package/dist/core/verification/detect-runner.js +86 -0
- package/dist/core/verification/runner.js +76 -0
- package/dist/core/version.js +3 -1
- package/dist/languages/python/index.js +154 -0
- package/dist/locales/en.js +6 -0
- package/package.json +2 -1
|
@@ -4,12 +4,13 @@ import { agentTeamSpec } from '../../sub-agent/tools/team.js';
|
|
|
4
4
|
import { defineTool } from '../types.js';
|
|
5
5
|
import { artifactReadSpec, executeArtifactRead } from './artifact.js';
|
|
6
6
|
import { astGrepSpec, executeAstGrep } from './ast-grep.js';
|
|
7
|
-
import { astDefsRefsSpec, executeAstDefsRefs } from './ast.js';
|
|
7
|
+
import { astDefsRefsSpec, executeAstDefsRefs, codeFindReferencesSpec, executeCodeFindReferences, } from './ast.js';
|
|
8
8
|
import { benchmarkReportSpec, executeBenchmarkReport, executeGitApplyCheck, executeGitDiffCheck, executeSweBenchGetReport, executeSweBenchLoadInstance, executeSweBenchSubmitPredictions, executeSweBenchWritePrediction, gitApplyCheckSpec, gitDiffCheckSpec, sweBenchGetReportSpec, sweBenchLoadInstanceSpec, sweBenchSubmitPredictionsSpec, sweBenchWritePredictionSpec, } from './benchmark.js';
|
|
9
9
|
import { codeSearchExecutor } from './code-search/executor.js';
|
|
10
10
|
import { CodeSearchSpec } from './code-search/spec.js';
|
|
11
|
-
import { codeReadSpec, executeFsCreateDirectory, executeFsList, executeFsListDirectory, executeFsListFiles, executeFsReadFile, executeFsDeleteFile, executeFsWriteFile, fsCreateDirectorySpec, fsDeleteFileSpec, fsListSpec, fsListDirectorySpec, fsListFilesSpec, fsReadFileSpec, fsWriteFileSpec, } from './fs.js';
|
|
12
|
-
import { gitCatSpec, executeGitCat, gitStatusSpec, executeGitStatus } from './git.js';
|
|
11
|
+
import { codeReadSpec, executeFsCreateDirectory, executeFsEditFile, executeFsList, executeFsListDirectory, executeFsListFiles, executeFsReadFile, executeFsDeleteFile, executeFsWriteFile, fsCreateDirectorySpec, fsDeleteFileSpec, fsEditFileSpec, fsListSpec, fsListDirectorySpec, fsListFilesSpec, fsReadFileSpec, fsWriteFileSpec, } from './fs.js';
|
|
12
|
+
import { gitCatSpec, executeGitCat, gitStatusSpec, executeGitStatus, gitBlameSpec, executeGitBlame, gitLogSpec, executeGitLog, gitShowSpec, executeGitShow, } from './git.js';
|
|
13
|
+
import { globFindSpec, executeGlobFind } from './glob.js';
|
|
13
14
|
import { askUserSpec } from './interaction.js';
|
|
14
15
|
import { updateKnowledgeSpec, executeUpdateKnowledge } from './knowledge.js';
|
|
15
16
|
import { planInitSpec, planReadSpec, planUpdateSpec } from './plan.js';
|
|
@@ -34,12 +35,18 @@ export function registerAllBuiltins(registry) {
|
|
|
34
35
|
// Code search & AST
|
|
35
36
|
registry.register(defineTool(CodeSearchSpec, codeSearchExecutor));
|
|
36
37
|
registry.register(defineTool(astDefsRefsSpec, executeAstDefsRefs));
|
|
38
|
+
registry.register(defineTool(codeFindReferencesSpec, executeCodeFindReferences));
|
|
37
39
|
registry.register(defineTool(astGrepSpec, executeAstGrep));
|
|
38
40
|
// Git
|
|
39
41
|
registry.register(defineTool(gitCatSpec, executeGitCat));
|
|
40
42
|
registry.register(defineTool(gitStatusSpec, executeGitStatus));
|
|
43
|
+
registry.register(defineTool(gitBlameSpec, executeGitBlame));
|
|
44
|
+
registry.register(defineTool(gitLogSpec, executeGitLog));
|
|
45
|
+
registry.register(defineTool(gitShowSpec, executeGitShow));
|
|
41
46
|
registry.register(defineTool(gitDiffCheckSpec, executeGitDiffCheck));
|
|
42
47
|
registry.register(defineTool(gitApplyCheckSpec, executeGitApplyCheck));
|
|
48
|
+
// Glob
|
|
49
|
+
registry.register(defineTool(globFindSpec, executeGlobFind));
|
|
43
50
|
// Benchmark / SWE-bench
|
|
44
51
|
registry.register(defineTool(benchmarkReportSpec, executeBenchmarkReport));
|
|
45
52
|
registry.register(defineTool(sweBenchLoadInstanceSpec, executeSweBenchLoadInstance));
|
|
@@ -53,6 +60,7 @@ export function registerAllBuiltins(registry) {
|
|
|
53
60
|
registry.register(defineTool(fsListDirectorySpec, executeFsListDirectory));
|
|
54
61
|
registry.register(defineTool(fsListFilesSpec, executeFsListFiles));
|
|
55
62
|
registry.register(defineTool(fsWriteFileSpec, executeFsWriteFile));
|
|
63
|
+
registry.register(defineTool(fsEditFileSpec, executeFsEditFile));
|
|
56
64
|
registry.register(defineTool(fsCreateDirectorySpec, executeFsCreateDirectory));
|
|
57
65
|
registry.register(defineTool(fsDeleteFileSpec, executeFsDeleteFile));
|
|
58
66
|
// Execution
|
|
@@ -64,5 +72,5 @@ export function registerAllBuiltins(registry) {
|
|
|
64
72
|
registry.register(planUpdateSpec);
|
|
65
73
|
registry.register(askUserSpec);
|
|
66
74
|
}
|
|
67
|
-
export { CodeSearchSpec, codeSearchExecutor, astDefsRefsSpec as codeAstSpec, executeAstDefsRefs as executeCodeAst, gitCatSpec, executeGitCat, gitStatusSpec, executeGitStatus, codeReadSpec, fsListSpec, executeFsList, fsReadFileSpec as fsReadSpec, executeFsReadFile as executeFsRead, updateKnowledgeSpec, executeUpdateKnowledge, astGrepSpec as codeSearchAstSpec, executeAstGrep as executeCodeSearchAst, verifyRunSpec as testRunSpec, executeVerifyRun as executeTestRun, gitDiffCheckSpec, executeGitDiffCheck, gitApplyCheckSpec, executeGitApplyCheck, benchmarkReportSpec, executeBenchmarkReport, sweBenchLoadInstanceSpec, executeSweBenchLoadInstance, sweBenchWritePredictionSpec, executeSweBenchWritePrediction, sweBenchSubmitPredictionsSpec, executeSweBenchSubmitPredictions, sweBenchGetReportSpec, executeSweBenchGetReport, workspaceInfoSpec, executeWorkspaceInfo, };
|
|
75
|
+
export { CodeSearchSpec, codeSearchExecutor, astDefsRefsSpec as codeAstSpec, executeAstDefsRefs as executeCodeAst, codeFindReferencesSpec, executeCodeFindReferences, gitCatSpec, executeGitCat, gitStatusSpec, executeGitStatus, gitBlameSpec, executeGitBlame, gitLogSpec, executeGitLog, gitShowSpec, executeGitShow, globFindSpec, executeGlobFind, codeReadSpec, fsListSpec, executeFsList, fsReadFileSpec as fsReadSpec, executeFsReadFile as executeFsRead, updateKnowledgeSpec, executeUpdateKnowledge, astGrepSpec as codeSearchAstSpec, executeAstGrep as executeCodeSearchAst, verifyRunSpec as testRunSpec, executeVerifyRun as executeTestRun, gitDiffCheckSpec, executeGitDiffCheck, gitApplyCheckSpec, executeGitApplyCheck, benchmarkReportSpec, executeBenchmarkReport, sweBenchLoadInstanceSpec, executeSweBenchLoadInstance, sweBenchWritePredictionSpec, executeSweBenchWritePrediction, sweBenchSubmitPredictionsSpec, executeSweBenchSubmitPredictions, sweBenchGetReportSpec, executeSweBenchGetReport, workspaceInfoSpec, executeWorkspaceInfo, };
|
|
68
76
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,10 +1,86 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { writeFile, mkdir } from '../../adapters/fs/node-fs.js';
|
|
2
|
+
import { readdir, readFile, writeFile, mkdir } from '../../adapters/fs/node-fs.js';
|
|
3
3
|
import { getDefaultIndexPath } from '../../config/paths.js';
|
|
4
|
+
import { getLogger, tryGetLogger } from '../../observability/logger.js';
|
|
4
5
|
import { Phase } from '../../types/runtime.js';
|
|
5
6
|
import { safeJoin } from '../../utils/path.js';
|
|
6
7
|
let lastEventTimestampMs = 0;
|
|
7
8
|
let eventSequence = 0;
|
|
9
|
+
// ── Knowledge quality gates ──────────────────────────────────────────────────
|
|
10
|
+
const MIN_RULE_LENGTH = 10;
|
|
11
|
+
const MAX_RULE_LENGTH = 500;
|
|
12
|
+
function isValidContent(text) {
|
|
13
|
+
const trimmed = text.trim();
|
|
14
|
+
return (trimmed.length >= MIN_RULE_LENGTH && trimmed.length <= MAX_RULE_LENGTH && /[\w]/.test(trimmed));
|
|
15
|
+
}
|
|
16
|
+
/** Simple Levenshtein distance for short strings. */
|
|
17
|
+
function levenshtein(a, b) {
|
|
18
|
+
if (a === b)
|
|
19
|
+
return 0;
|
|
20
|
+
if (a.length === 0)
|
|
21
|
+
return b.length;
|
|
22
|
+
if (b.length === 0)
|
|
23
|
+
return a.length;
|
|
24
|
+
const matrix = [];
|
|
25
|
+
for (let i = 0; i <= b.length; i++)
|
|
26
|
+
matrix[i] = [i];
|
|
27
|
+
for (let j = 0; j <= a.length; j++)
|
|
28
|
+
matrix[0][j] = j;
|
|
29
|
+
for (let i = 1; i <= b.length; i++) {
|
|
30
|
+
for (let j = 1; j <= a.length; j++) {
|
|
31
|
+
const cost = b[i - 1] === a[j - 1] ? 0 : 1;
|
|
32
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return matrix[b.length][a.length];
|
|
36
|
+
}
|
|
37
|
+
/** Check if a rule is too similar to any existing rule. */
|
|
38
|
+
function isDuplicateRule(newRule, existingRules) {
|
|
39
|
+
const normalized = newRule.trim().toLowerCase();
|
|
40
|
+
for (const existing of existingRules) {
|
|
41
|
+
const existingNorm = existing.trim().toLowerCase();
|
|
42
|
+
if (normalized === existingNorm)
|
|
43
|
+
return true;
|
|
44
|
+
if (levenshtein(normalized, existingNorm) < 5)
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
/** Load existing knowledge to check for duplicates. */
|
|
50
|
+
async function loadExistingKnowledge(knowledgeDir) {
|
|
51
|
+
const rules = [];
|
|
52
|
+
const decisions = [];
|
|
53
|
+
const deprecatedRules = [];
|
|
54
|
+
try {
|
|
55
|
+
const files = await readdir(knowledgeDir);
|
|
56
|
+
const jsonFiles = files.filter((f) => f.endsWith('.json')).sort();
|
|
57
|
+
for (const file of jsonFiles) {
|
|
58
|
+
try {
|
|
59
|
+
const content = await readFile(safeJoin(knowledgeDir, file), 'utf-8');
|
|
60
|
+
const data = JSON.parse(content);
|
|
61
|
+
if (Array.isArray(data.project_rules))
|
|
62
|
+
rules.push(...data.project_rules);
|
|
63
|
+
if (Array.isArray(data.deprecated_rules))
|
|
64
|
+
deprecatedRules.push(...data.deprecated_rules);
|
|
65
|
+
if (Array.isArray(data.architectural_decisions)) {
|
|
66
|
+
for (const d of data.architectural_decisions) {
|
|
67
|
+
if (typeof d.decision === 'string')
|
|
68
|
+
decisions.push(d.decision);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
/* skip corrupted */
|
|
74
|
+
getLogger().debug(`[Knowledge] Failed to read knowledge file ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
/* dir missing */
|
|
80
|
+
getLogger().debug(`[Knowledge] Failed to read knowledge directory: ${error instanceof Error ? error.message : String(error)}`);
|
|
81
|
+
}
|
|
82
|
+
return { rules, decisions, deprecatedRules };
|
|
83
|
+
}
|
|
8
84
|
function nextEventFilePrefix() {
|
|
9
85
|
const nowMs = Date.now();
|
|
10
86
|
if (nowMs === lastEventTimestampMs) {
|
|
@@ -36,6 +112,14 @@ const updateKnowledgeInputSchema = z.discriminatedUnion('category', [
|
|
|
36
112
|
category: z.literal('user_preferences'),
|
|
37
113
|
preferences: z.string().describe('Updated description of user personal preferences'),
|
|
38
114
|
}),
|
|
115
|
+
z.object({
|
|
116
|
+
category: z.literal('lessons_learned'),
|
|
117
|
+
lessons: z.array(z.string()).describe('Lessons learned from execution outcomes'),
|
|
118
|
+
source: z
|
|
119
|
+
.enum(['success', 'failure'])
|
|
120
|
+
.optional()
|
|
121
|
+
.describe('Whether lessons came from success or failure'),
|
|
122
|
+
}),
|
|
39
123
|
]);
|
|
40
124
|
export const updateKnowledgeSpec = {
|
|
41
125
|
name: 'update_knowledge',
|
|
@@ -56,8 +140,60 @@ export async function executeUpdateKnowledge(input, ctx) {
|
|
|
56
140
|
const { repoRoot } = ctx;
|
|
57
141
|
const indexPath = getDefaultIndexPath(repoRoot);
|
|
58
142
|
const knowledgeDir = safeJoin(indexPath, 'knowledge');
|
|
59
|
-
|
|
60
|
-
|
|
143
|
+
await mkdir(knowledgeDir, { recursive: true });
|
|
144
|
+
const existing = await loadExistingKnowledge(knowledgeDir);
|
|
145
|
+
// ── Quality gates ────────────────────────────────────────────────────────
|
|
146
|
+
if (input.category === 'project_rules') {
|
|
147
|
+
// Filter out rules that are invalid or duplicate
|
|
148
|
+
const validRules = [];
|
|
149
|
+
let skipped = 0;
|
|
150
|
+
for (const rule of input.rules) {
|
|
151
|
+
if (!isValidContent(rule)) {
|
|
152
|
+
skipped++;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (isDuplicateRule(rule, existing.rules)) {
|
|
156
|
+
skipped++;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (isDuplicateRule(rule, existing.deprecatedRules)) {
|
|
160
|
+
skipped++;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
validRules.push(rule);
|
|
164
|
+
}
|
|
165
|
+
if (skipped > 0) {
|
|
166
|
+
tryGetLogger()?.debug(`[Knowledge] Filtered ${skipped} invalid/duplicate rules`);
|
|
167
|
+
}
|
|
168
|
+
// If all rules were filtered, skip the write entirely
|
|
169
|
+
if (validRules.length === 0 &&
|
|
170
|
+
(!input.deprecated_rules || input.deprecated_rules.length === 0)) {
|
|
171
|
+
return { success: true, message: 'All rules were duplicates or invalid, nothing to record' };
|
|
172
|
+
}
|
|
173
|
+
// Rewrite input with filtered rules
|
|
174
|
+
input.rules = validRules;
|
|
175
|
+
}
|
|
176
|
+
if (input.category === 'architectural_decisions') {
|
|
177
|
+
if (!isValidContent(input.decision)) {
|
|
178
|
+
return { success: true, message: 'Decision too short or invalid, nothing to record' };
|
|
179
|
+
}
|
|
180
|
+
if (isDuplicateRule(input.decision, existing.decisions)) {
|
|
181
|
+
return { success: true, message: 'Decision already recorded, skipping duplicate' };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (input.category === 'user_preferences') {
|
|
185
|
+
if (!isValidContent(input.preferences)) {
|
|
186
|
+
return { success: true, message: 'Preferences too short or invalid, nothing to record' };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (input.category === 'lessons_learned') {
|
|
190
|
+
const validLessons = input.lessons.filter((l) => isValidContent(l));
|
|
191
|
+
if (validLessons.length === 0) {
|
|
192
|
+
return { success: true, message: 'All lessons were invalid, nothing to record' };
|
|
193
|
+
}
|
|
194
|
+
input.lessons = validLessons;
|
|
195
|
+
}
|
|
196
|
+
// ── Write ────────────────────────────────────────────────────────────────
|
|
61
197
|
const fileName = `${nextEventFilePrefix()}-${input.category}.json`;
|
|
62
198
|
const filePath = safeJoin(knowledgeDir, fileName);
|
|
63
199
|
let dataToSave = {};
|
|
@@ -82,9 +218,15 @@ export async function executeUpdateKnowledge(input, ctx) {
|
|
|
82
218
|
case 'user_preferences':
|
|
83
219
|
dataToSave = { user_preferences: input.preferences };
|
|
84
220
|
break;
|
|
221
|
+
case 'lessons_learned':
|
|
222
|
+
dataToSave = {
|
|
223
|
+
lessons_learned: input.lessons,
|
|
224
|
+
source: input.source ?? 'unknown',
|
|
225
|
+
date: new Date().toISOString().split('T')[0],
|
|
226
|
+
};
|
|
227
|
+
break;
|
|
85
228
|
}
|
|
86
229
|
try {
|
|
87
|
-
await mkdir(knowledgeDir, { recursive: true });
|
|
88
230
|
await writeFile(filePath, JSON.stringify(dataToSave, null, 2));
|
|
89
231
|
return {
|
|
90
232
|
success: true,
|
|
@@ -9,6 +9,7 @@ import { Executor } from '../../grizzco/execution/Executor.js';
|
|
|
9
9
|
import { WorkerFactory } from '../../grizzco/execution/WorkerFactory.js';
|
|
10
10
|
import { MockLockService } from '../../grizzco/services/implementations/mock/MockLockService.js';
|
|
11
11
|
import { registry } from '../../grizzco/services/registry.js';
|
|
12
|
+
import { getLogger } from '../../observability/logger.js';
|
|
12
13
|
import { normalizeDiff, validateDiff, convertDiffToShadowOperations } from '../../patch/diff.js';
|
|
13
14
|
import { getRejectionsDir } from '../../runtime/paths.js';
|
|
14
15
|
import { FileStateResolver } from '../../strata/layers/file-state-resolver.js';
|
|
@@ -67,7 +68,8 @@ export const proposalApplySpec = {
|
|
|
67
68
|
changedFilesTruncated: meta.changedFiles.length > changedFiles.length,
|
|
68
69
|
});
|
|
69
70
|
}
|
|
70
|
-
catch {
|
|
71
|
+
catch (error) {
|
|
72
|
+
getLogger().warn(`[Proposal] Failed to validate diff for authorization preview: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
73
|
return JSON.stringify({ handle, preview: 'invalid_diff' });
|
|
72
74
|
}
|
|
73
75
|
},
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { text } from '../../../locales/index.js';
|
|
3
|
+
import { parseRunnerOutput, parseStructuredSummary } from '../../feedback/parsers.js';
|
|
3
4
|
import { Phase } from '../../types/runtime.js';
|
|
4
|
-
import {
|
|
5
|
+
import { detectRunner, injectJsonFlags, } from '../../verification/detect-runner.js';
|
|
6
|
+
import { runVerify, classifyError, isRetryable as checkRetryable, parseTestSummary, } from '../../verification/runner.js';
|
|
5
7
|
import { processResource, repoResource } from '../parallel/resource-helpers.js';
|
|
6
8
|
export const verifyRunSpec = {
|
|
7
9
|
name: 'test.run',
|
|
@@ -14,6 +16,10 @@ export const verifyRunSpec = {
|
|
|
14
16
|
computeResources: (_input, ctx) => [repoResource(ctx), processResource(ctx)],
|
|
15
17
|
inputSchema: z.object({
|
|
16
18
|
command: z.string().describe('The shell command to run for verification'),
|
|
19
|
+
runner: z
|
|
20
|
+
.enum(['jest', 'vitest', 'pytest', 'tsc', 'eslint', 'bun', 'go'])
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('Test runner type. Auto-detected from command if omitted.'),
|
|
17
23
|
}),
|
|
18
24
|
outputSchema: z.object({
|
|
19
25
|
ok: z.boolean(),
|
|
@@ -21,6 +27,24 @@ export const verifyRunSpec = {
|
|
|
21
27
|
exitCode: z.number().nullable(),
|
|
22
28
|
errorType: z.string().optional(),
|
|
23
29
|
isRetryable: z.boolean().optional(),
|
|
30
|
+
diagnostics: z
|
|
31
|
+
.array(z.object({
|
|
32
|
+
file: z.string(),
|
|
33
|
+
line: z.number().optional(),
|
|
34
|
+
column: z.number().optional(),
|
|
35
|
+
severity: z.enum(['error', 'warning']),
|
|
36
|
+
message: z.string(),
|
|
37
|
+
source: z.string(),
|
|
38
|
+
}))
|
|
39
|
+
.optional(),
|
|
40
|
+
summary: z
|
|
41
|
+
.object({
|
|
42
|
+
total: z.number(),
|
|
43
|
+
passed: z.number(),
|
|
44
|
+
failed: z.number(),
|
|
45
|
+
skipped: z.number(),
|
|
46
|
+
})
|
|
47
|
+
.optional(),
|
|
24
48
|
}),
|
|
25
49
|
allowedPhases: [Phase.VERIFY],
|
|
26
50
|
};
|
|
@@ -29,13 +53,21 @@ export const verifyRunSpec = {
|
|
|
29
53
|
*/
|
|
30
54
|
export async function executeVerifyRun(input, ctx) {
|
|
31
55
|
const { command } = input;
|
|
56
|
+
const runner = input.runner ?? detectRunner(command);
|
|
57
|
+
const effectiveCommand = injectJsonFlags(command, runner);
|
|
32
58
|
const activePath = ctx.worktreeRoot || ctx.repoRoot;
|
|
33
|
-
const result = await runVerify(activePath,
|
|
59
|
+
const result = await runVerify(activePath, effectiveCommand, ctx.env, ctx.signal);
|
|
34
60
|
const errorType = !result.ok ? classifyError(result.output) : undefined;
|
|
61
|
+
// Structured parsing when we know the runner; generic text heuristics otherwise
|
|
62
|
+
const diagnostics = !result.ok ? parseRunnerOutput(result.output, runner) : [];
|
|
63
|
+
// Prefer structured JSON summary; fall back to regex-based text parser
|
|
64
|
+
const summary = parseStructuredSummary(result.output, runner) ?? parseTestSummary(result.output);
|
|
35
65
|
return {
|
|
36
66
|
...result,
|
|
37
67
|
errorType,
|
|
38
|
-
isRetryable:
|
|
68
|
+
isRetryable: errorType ? checkRetryable(errorType) : false,
|
|
69
|
+
diagnostics: diagnostics.length > 0 ? diagnostics : undefined,
|
|
70
|
+
summary,
|
|
39
71
|
};
|
|
40
72
|
}
|
|
41
73
|
//# sourceMappingURL=verify.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { text } from '../../../locales/index.js';
|
|
2
|
+
import { getLogger } from '../../observability/logger.js';
|
|
2
3
|
import { normalizeDiff, validateDiff } from '../../patch/diff.js';
|
|
3
4
|
import { ArtifactStore } from '../../sub-agent/artifacts/store.js';
|
|
4
5
|
import { normalizeRepoRelativePath } from '../../utils/path.js';
|
|
@@ -363,7 +364,8 @@ async function loadProposalChangedFiles(handle) {
|
|
|
363
364
|
const meta = validateDiff(normalized);
|
|
364
365
|
return meta.changedFiles ?? [];
|
|
365
366
|
}
|
|
366
|
-
catch {
|
|
367
|
+
catch (error) {
|
|
368
|
+
getLogger().warn(`[PermissionRules] Failed to load proposal changed files: ${error instanceof Error ? error.message : String(error)}`);
|
|
367
369
|
return null;
|
|
368
370
|
}
|
|
369
371
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import * as crypto from 'crypto';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
import { z } from 'zod';
|
|
4
|
+
import { readFile } from '../adapters/fs/node-fs.js';
|
|
5
|
+
import { AstParser } from '../ast/parser.js';
|
|
3
6
|
import { LIMITS } from '../config/limits.js';
|
|
4
7
|
import { getLogger } from '../observability/logger.js';
|
|
5
8
|
import { isRecord } from '../utils/serialize.js';
|
|
@@ -110,7 +113,7 @@ export class ToolRouter {
|
|
|
110
113
|
ctx: normalizedEnvelope.ctx,
|
|
111
114
|
});
|
|
112
115
|
if (permissionDecision.kind === 'deny') {
|
|
113
|
-
const result = this.createErrorResult(normalizedEnvelope, startedAt, 'denied', 'PERMISSION_RULE_DENY', permissionDecision.reason, {
|
|
116
|
+
const result = this.createErrorResult(normalizedEnvelope, startedAt, 'denied', 'PERMISSION_RULE_DENY', permissionDecision.reason ?? 'Permission denied', {
|
|
114
117
|
authorization: {
|
|
115
118
|
outcome: 'deny',
|
|
116
119
|
reason: permissionDecision.reason,
|
|
@@ -192,6 +195,12 @@ export class ToolRouter {
|
|
|
192
195
|
outputSummary: sanitized.summary,
|
|
193
196
|
durationMs,
|
|
194
197
|
};
|
|
198
|
+
// Per-edit syntax guard: check for syntax errors after file writes
|
|
199
|
+
const syntaxWarnings = await checkPostEditSyntax(spec, normalizedEnvelope.args, rawOutput, normalizedEnvelope.ctx);
|
|
200
|
+
if (syntaxWarnings.length > 0) {
|
|
201
|
+
result.warnings = syntaxWarnings;
|
|
202
|
+
result.meta = { ...result.meta, syntaxWarning: true };
|
|
203
|
+
}
|
|
195
204
|
this.audit.onEnd(result);
|
|
196
205
|
return result;
|
|
197
206
|
}
|
|
@@ -260,7 +269,7 @@ export class ToolRouter {
|
|
|
260
269
|
ctx: normalizedEnvelope.ctx,
|
|
261
270
|
});
|
|
262
271
|
if (permissionDecision.kind === 'deny') {
|
|
263
|
-
const toolResult = this.createErrorResult(normalizedEnvelope, startedAt, 'denied', 'PERMISSION_RULE_DENY', permissionDecision.reason, {
|
|
272
|
+
const toolResult = this.createErrorResult(normalizedEnvelope, startedAt, 'denied', 'PERMISSION_RULE_DENY', permissionDecision.reason ?? 'Permission denied', {
|
|
264
273
|
authorization: {
|
|
265
274
|
outcome: 'deny',
|
|
266
275
|
reason: permissionDecision.reason,
|
|
@@ -379,7 +388,8 @@ export class ToolRouter {
|
|
|
379
388
|
return raw;
|
|
380
389
|
return `${raw.slice(0, maxLength)}...`;
|
|
381
390
|
}
|
|
382
|
-
catch {
|
|
391
|
+
catch (error) {
|
|
392
|
+
getLogger().debug(`[ToolRouter] Failed to summarize args: ${error instanceof Error ? error.message : String(error)}`);
|
|
383
393
|
return '[Unserializable]';
|
|
384
394
|
}
|
|
385
395
|
}
|
|
@@ -392,7 +402,8 @@ export class ToolRouter {
|
|
|
392
402
|
// Truncation to 16 hex was insufficient collision resistance for security use.
|
|
393
403
|
return crypto.createHash('sha256').update(raw).digest('hex');
|
|
394
404
|
}
|
|
395
|
-
catch {
|
|
405
|
+
catch (error) {
|
|
406
|
+
getLogger().debug(`[ToolRouter] Failed to hash args: ${error instanceof Error ? error.message : String(error)}`);
|
|
396
407
|
return undefined;
|
|
397
408
|
}
|
|
398
409
|
}
|
|
@@ -495,9 +506,81 @@ export class ToolRouter {
|
|
|
495
506
|
]);
|
|
496
507
|
return typeof result === 'string' && result.trim() ? result : fallback;
|
|
497
508
|
}
|
|
498
|
-
catch {
|
|
509
|
+
catch (error) {
|
|
510
|
+
getLogger().debug(`[ToolRouter] Failed to get authorization args summary: ${error instanceof Error ? error.message : String(error)}`);
|
|
499
511
|
return fallback;
|
|
500
512
|
}
|
|
501
513
|
}
|
|
502
514
|
}
|
|
515
|
+
/**
|
|
516
|
+
* Per-edit syntax guard: after a file write, parse the file with tree-sitter
|
|
517
|
+
* and return syntax error warnings. Non-blocking — returns empty array on
|
|
518
|
+
* any failure (missing grammar, parse error, etc.).
|
|
519
|
+
*/
|
|
520
|
+
async function checkPostEditSyntax(spec, args, rawOutput, ctx) {
|
|
521
|
+
if (spec.name !== 'fs.write_file' && spec.name !== 'fs.edit_file')
|
|
522
|
+
return [];
|
|
523
|
+
if (!isRecord(rawOutput) || typeof rawOutput.path !== 'string')
|
|
524
|
+
return [];
|
|
525
|
+
const filePath = rawOutput.path;
|
|
526
|
+
let content;
|
|
527
|
+
if (spec.name === 'fs.write_file') {
|
|
528
|
+
if (!isRecord(args) || typeof args.content !== 'string')
|
|
529
|
+
return [];
|
|
530
|
+
content = args.content;
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
// fs.edit_file — read post-edit content from disk
|
|
534
|
+
try {
|
|
535
|
+
const absolutePath = path.resolve(ctx.repoRoot, filePath);
|
|
536
|
+
content = await readFile(absolutePath, 'utf-8');
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
getLogger().debug(`[ToolRouter] Failed to read file for post-edit syntax check: ${error instanceof Error ? error.message : String(error)}`);
|
|
540
|
+
return [];
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Detect language from extension
|
|
544
|
+
const ext = path.extname(filePath).toLowerCase().replace('.', '');
|
|
545
|
+
if (!ext)
|
|
546
|
+
return [];
|
|
547
|
+
// Only check for languages that have tree-sitter support
|
|
548
|
+
const plugin = ctx.languagePlugins?.getByExtension(`.${ext}`);
|
|
549
|
+
if (!plugin)
|
|
550
|
+
return [];
|
|
551
|
+
try {
|
|
552
|
+
const tree = await AstParser.parse(content, plugin.meta.id);
|
|
553
|
+
if (!tree?.rootNode)
|
|
554
|
+
return [];
|
|
555
|
+
const errors = collectSyntaxErrors(tree.rootNode);
|
|
556
|
+
if (errors.length === 0)
|
|
557
|
+
return [];
|
|
558
|
+
return [
|
|
559
|
+
`Syntax warning in ${filePath}: ${errors.length} error(s) detected — ` +
|
|
560
|
+
errors
|
|
561
|
+
.slice(0, 3)
|
|
562
|
+
.map((e) => `line ${e.line}: ${e.text}`)
|
|
563
|
+
.join('; '),
|
|
564
|
+
];
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
// Tree-sitter parse failed (no grammar, etc.) — silently skip
|
|
568
|
+
getLogger().debug(`[ToolRouter] Post-edit syntax check parse failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
569
|
+
return [];
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function collectSyntaxErrors(node, errors = [], depth = 0) {
|
|
573
|
+
if (depth > 50)
|
|
574
|
+
return errors; // prevent stack overflow
|
|
575
|
+
if (node.type === 'ERROR' || node.isMissing) {
|
|
576
|
+
errors.push({
|
|
577
|
+
line: (node.startPosition?.row ?? 0) + 1,
|
|
578
|
+
text: node.text?.slice(0, 80) ?? node.type,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
for (const child of node.children ?? []) {
|
|
582
|
+
collectSyntaxErrors(child, errors, depth + 1);
|
|
583
|
+
}
|
|
584
|
+
return errors;
|
|
585
|
+
}
|
|
503
586
|
//# sourceMappingURL=router.js.map
|
|
@@ -46,8 +46,9 @@ function safeParseJson(argsText) {
|
|
|
46
46
|
try {
|
|
47
47
|
value = JSON.parse(nested);
|
|
48
48
|
}
|
|
49
|
-
catch {
|
|
49
|
+
catch (error) {
|
|
50
50
|
// Ignore: fall back to the first parse result to preserve observability.
|
|
51
|
+
getLogger().debug(`[ToolSession] Double-decoded JSON parse fallback: ${error instanceof Error ? error.message : String(error)}`);
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
}
|
|
@@ -71,7 +72,8 @@ function formatToolResultForModel(result) {
|
|
|
71
72
|
try {
|
|
72
73
|
return JSON.stringify(payload);
|
|
73
74
|
}
|
|
74
|
-
catch {
|
|
75
|
+
catch (error) {
|
|
76
|
+
getLogger().debug(`[ToolSession] Failed to serialize tool result: ${error instanceof Error ? error.message : String(error)}`);
|
|
75
77
|
return JSON.stringify({
|
|
76
78
|
id: result.id,
|
|
77
79
|
toolName: result.toolName,
|
|
@@ -88,7 +90,8 @@ function safeStringifyForAudit(value) {
|
|
|
88
90
|
try {
|
|
89
91
|
return redactJsonString(JSON.stringify(redactValue(value)));
|
|
90
92
|
}
|
|
91
|
-
catch {
|
|
93
|
+
catch (error) {
|
|
94
|
+
getLogger().debug(`[ToolSession] Failed to stringify value for audit: ${error instanceof Error ? error.message : String(error)}`);
|
|
92
95
|
return '[Unserializable]';
|
|
93
96
|
}
|
|
94
97
|
}
|
|
@@ -215,7 +218,8 @@ function serializeToolResultOutputForArtifact(output) {
|
|
|
215
218
|
fileExt: 'json',
|
|
216
219
|
};
|
|
217
220
|
}
|
|
218
|
-
catch {
|
|
221
|
+
catch (error) {
|
|
222
|
+
getLogger().debug(`[ToolSession] Failed to serialize tool output for artifact: ${error instanceof Error ? error.message : String(error)}`);
|
|
219
223
|
return undefined;
|
|
220
224
|
}
|
|
221
225
|
}
|
|
@@ -789,7 +793,8 @@ function coercePlanUpdatePatch(args) {
|
|
|
789
793
|
coercedPatchSource: 'stringified',
|
|
790
794
|
};
|
|
791
795
|
}
|
|
792
|
-
catch {
|
|
796
|
+
catch (error) {
|
|
797
|
+
getLogger().debug(`[ToolSession] Failed to parse plan.update patch JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
793
798
|
return { args, error: formatPlanUpdatePatchTypeError('string') };
|
|
794
799
|
}
|
|
795
800
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getLogger } from '../observability/logger.js';
|
|
1
2
|
/**
|
|
2
3
|
* Sanitizes any error input (object, string, or mixed) to prevent leakage
|
|
3
4
|
* of sensitive technical data like Zod dumps or stack traces.
|
|
@@ -10,7 +11,8 @@ export function sanitizeErrorMessage(err) {
|
|
|
10
11
|
try {
|
|
11
12
|
msg = err instanceof Error ? err.message : typeof err === 'string' ? err : JSON.stringify(err);
|
|
12
13
|
}
|
|
13
|
-
catch {
|
|
14
|
+
catch (error) {
|
|
15
|
+
getLogger().debug(`[Sanitizer] Failed to convert error to string: ${error instanceof Error ? error.message : String(error)}`);
|
|
14
16
|
msg = String(err);
|
|
15
17
|
}
|
|
16
18
|
// 2. Strict Whitelist Detection
|
|
@@ -87,7 +89,8 @@ export function sanitizeObject(obj, maxDepth = MAX_DEPTH, depth = 0) {
|
|
|
87
89
|
try {
|
|
88
90
|
result[key] = sanitizeObject(value, maxDepth, depth + 1);
|
|
89
91
|
}
|
|
90
|
-
catch {
|
|
92
|
+
catch (error) {
|
|
93
|
+
getLogger().debug(`[Sanitizer] Circular reference detected during object sanitization: ${error instanceof Error ? error.message : String(error)}`);
|
|
91
94
|
result[key] = '[CIRCULAR]';
|
|
92
95
|
}
|
|
93
96
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getLogger } from '../observability/logger.js';
|
|
1
2
|
/**
|
|
2
3
|
* Safely serialize a value to JSON string.
|
|
3
4
|
* Returns '[Unserializable]' if JSON.stringify throws and String() also fails.
|
|
@@ -10,11 +11,13 @@ export function safeStringify(value, options) {
|
|
|
10
11
|
}
|
|
11
12
|
return raw;
|
|
12
13
|
}
|
|
13
|
-
catch {
|
|
14
|
+
catch (error) {
|
|
15
|
+
getLogger().debug(`[Serialize] JSON.stringify failed, falling back to String(): ${error instanceof Error ? error.message : String(error)}`);
|
|
14
16
|
try {
|
|
15
17
|
return String(value);
|
|
16
18
|
}
|
|
17
|
-
catch {
|
|
19
|
+
catch (innerError) {
|
|
20
|
+
getLogger().debug(`[Serialize] String() conversion also failed: ${innerError instanceof Error ? innerError.message : String(innerError)}`);
|
|
18
21
|
return '[Unserializable]';
|
|
19
22
|
}
|
|
20
23
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test runner detection and JSON flag injection.
|
|
3
|
+
*
|
|
4
|
+
* Detects which test runner a command invokes and, when the runner
|
|
5
|
+
* supports structured JSON output, rewrites the command to emit JSON
|
|
6
|
+
* so downstream parsers can consume machine-readable data instead of
|
|
7
|
+
* regex-matching human-friendly text.
|
|
8
|
+
*/
|
|
9
|
+
// ── Detection ────────────────────────────────────────────────────────────────
|
|
10
|
+
/** Heuristic detection from the raw command string. */
|
|
11
|
+
export function detectRunner(command) {
|
|
12
|
+
const cmd = command.toLowerCase();
|
|
13
|
+
// Order matters: more specific patterns first
|
|
14
|
+
if (/\bvitest\b/.test(cmd))
|
|
15
|
+
return 'vitest';
|
|
16
|
+
if (/\bjest\b/.test(cmd))
|
|
17
|
+
return 'jest';
|
|
18
|
+
if (/\bpytest\b/.test(cmd) || /\bpy\.test\b/.test(cmd))
|
|
19
|
+
return 'pytest';
|
|
20
|
+
if (/\btsc\b/.test(cmd))
|
|
21
|
+
return 'tsc';
|
|
22
|
+
if (/\beslint\b/.test(cmd))
|
|
23
|
+
return 'eslint';
|
|
24
|
+
if (/\bbun\s+test\b/.test(cmd))
|
|
25
|
+
return 'bun';
|
|
26
|
+
if (/\bgo\s+test\b/.test(cmd))
|
|
27
|
+
return 'go';
|
|
28
|
+
// npm/pnpm/yarn script proxies — try to infer from script name
|
|
29
|
+
if (/\bnpm\s+run\s+/.test(cmd) || /\bpnpm\s+/.test(cmd) || /\byarn\s+/.test(cmd)) {
|
|
30
|
+
if (/test:unit|test:e2e|test:integration|test:full/.test(cmd))
|
|
31
|
+
return 'unknown';
|
|
32
|
+
if (/\btest\b/.test(cmd))
|
|
33
|
+
return 'unknown'; // could be anything
|
|
34
|
+
}
|
|
35
|
+
return 'unknown';
|
|
36
|
+
}
|
|
37
|
+
// ── JSON flag injection ──────────────────────────────────────────────────────
|
|
38
|
+
/** Returns true when the runner supports structured JSON output. */
|
|
39
|
+
export function supportsJsonOutput(runner) {
|
|
40
|
+
return (runner === 'jest' ||
|
|
41
|
+
runner === 'vitest' ||
|
|
42
|
+
runner === 'eslint' ||
|
|
43
|
+
runner === 'bun' ||
|
|
44
|
+
runner === 'go');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Rewrite the command to emit structured output.
|
|
48
|
+
*
|
|
49
|
+
* Only modifies commands for runners that support JSON.
|
|
50
|
+
* Returns the original command unchanged when the runner
|
|
51
|
+
* has no JSON mode (pytest, tsc) or is unknown.
|
|
52
|
+
*/
|
|
53
|
+
export function injectJsonFlags(command, runner) {
|
|
54
|
+
switch (runner) {
|
|
55
|
+
case 'jest':
|
|
56
|
+
// jest --json --outputFile=/dev/null would suppress file write;
|
|
57
|
+
// but --json alone prints to stdout which is what we want.
|
|
58
|
+
// Avoid duplicating --json if already present.
|
|
59
|
+
if (command.includes('--json'))
|
|
60
|
+
return command;
|
|
61
|
+
return `${command} --json`;
|
|
62
|
+
case 'vitest':
|
|
63
|
+
// vitest --reporter=json --run (--run prevents watch mode)
|
|
64
|
+
if (command.includes('--reporter=json') || command.includes('--reporter json'))
|
|
65
|
+
return command;
|
|
66
|
+
return `${command} --reporter=json --run`;
|
|
67
|
+
case 'eslint':
|
|
68
|
+
// eslint --format json
|
|
69
|
+
if (command.includes('--format json') || command.includes('--format=json'))
|
|
70
|
+
return command;
|
|
71
|
+
return `${command} --format json`;
|
|
72
|
+
case 'bun':
|
|
73
|
+
// bun test --json (outputs NDJSON to stdout)
|
|
74
|
+
if (command.includes('--json'))
|
|
75
|
+
return command;
|
|
76
|
+
return `${command} --json`;
|
|
77
|
+
case 'go':
|
|
78
|
+
// go test -json
|
|
79
|
+
if (command.includes('-json'))
|
|
80
|
+
return command;
|
|
81
|
+
return `${command} -json`;
|
|
82
|
+
default:
|
|
83
|
+
return command;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=detect-runner.js.map
|