diff --git a/README.md b/README.md index acb9b44..1094748 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ jobs: with: fetch-depth: 0 - - uses: Conalh/TaskBound@v0.2.0 + - uses: Conalh/TaskBound@v0.2.1 with: fail-on: none ``` @@ -127,7 +127,7 @@ The action uploads nothing by default. It reads local git state from the checked You can still override the task explicitly: ```yaml - - uses: Conalh/TaskBound@v0.2.0 + - uses: Conalh/TaskBound@v0.2.1 with: task: Fix header CSS styling fail-on: none @@ -138,7 +138,7 @@ API key to the job. If the key is absent or the model call fails, TaskBound fall back to heuristic scope inference and records `scopeSource: llm_fallback` in JSON. ```yaml - - uses: Conalh/TaskBound@v0.2.0 + - uses: Conalh/TaskBound@v0.2.1 env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} with: diff --git a/package-lock.json b/package-lock.json index a60a3c3..91b1b77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "taskbound", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "taskbound", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "bin": { "taskbound": "dist/index.js" diff --git a/package.json b/package.json index 80ace7a..9a27261 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "taskbound", - "version": "0.2.0", + "version": "0.2.1", "description": "Post-session scope creep review for AI agent edits.", "type": "module", "bin": { diff --git a/src/scope-infer.ts b/src/scope-infer.ts index 56fbd5b..f523d98 100644 --- a/src/scope-infer.ts +++ b/src/scope-infer.ts @@ -154,7 +154,7 @@ function inferExtensions(task: string): string[] { const extensions = new Set(); for (const hint of EXTENSION_HINTS) { - if (hint.markers.some((marker) => normalized.includes(marker))) { + if (hint.markers.some((marker) => matchesMarker(normalized, marker))) { for (const extension of hint.extensions) { extensions.add(extension); } @@ -227,7 +227,7 @@ function inferDirectories(task: string, explicitPaths: string[]): string[] { const directories = new Set(); for (const hint of DIRECTORY_HINTS) { - if (hint.markers.some((marker) => normalized.includes(marker))) { + if (hint.markers.some((marker) => matchesMarker(normalized, marker))) { for (const directory of hint.directories) { directories.add(directory); } @@ -248,6 +248,14 @@ function inferDirectories(task: string, explicitPaths: string[]): string[] { return [...directories]; } +function matchesMarker(text: string, marker: string): boolean { + if (marker.length <= 3) { + return new RegExp(`(^|[^a-z0-9])${escapeRegExp(marker)}([^a-z0-9]|$)`, 'i').test(text); + } + + return text.includes(marker); +} + function buildScopeSummary( explicitPaths: string[], extensions: string[], @@ -278,3 +286,7 @@ function buildScopeSummary( return summary; } + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} diff --git a/test/scope-infer.test.mjs b/test/scope-infer.test.mjs index bc2f4ef..65ade32 100644 --- a/test/scope-infer.test.mjs +++ b/test/scope-infer.test.mjs @@ -25,6 +25,18 @@ test('scope inference accepts explicit directory scope context', () => { assert.equal(isFileInScope('package.json', scope), false); }); +test('scope inference does not treat ci inside ordinary words as workflow scope', () => { + const scope = inferScope( + 'Fix header CSS styling', + '## Summary\nFix header CSS spacing.\n\n## Scope\n- styles/header.css' + ); + + assert.equal(scope.extensions.includes('yml'), false); + assert.equal(scope.extensions.includes('yaml'), false); + assert.equal(scope.directories.includes('.github/workflows'), false); + assert.equal(isFileInScope('.github/workflows/demo-scope-creep.yml', scope), false); +}); + test('file scope detector flags sensitive surfaces for CSS task', () => { const findings = detectFileScope({ task: 'Fix header CSS styling',