@sienklogic/plan-build-run 2.56.3 → 2.57.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/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/plugins/copilot-pbr/plugin.json +1 -1
- package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
- package/plugins/pbr/.claude-plugin/plugin.json +1 -1
- package/plugins/pbr/scripts/local-llm/operations/classify-commit.js +70 -1
- package/plugins/pbr/scripts/local-llm/operations/classify-file-intent.js +99 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to Plan-Build-Run will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.57.0](https://github.com/SienkLogic/plan-build-run/compare/plan-build-run-v2.56.3...plan-build-run-v2.57.0) (2026-03-05)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **quick-021:** add heuristic-first classification for file intent and commits ([a70167e](https://github.com/SienkLogic/plan-build-run/commit/a70167e0d9c7449339c056ccd44d7be819ae6ce3))
|
|
14
|
+
|
|
8
15
|
## [2.56.3](https://github.com/SienkLogic/plan-build-run/compare/plan-build-run-v2.56.2...plan-build-run-v2.56.3) (2026-03-03)
|
|
9
16
|
|
|
10
17
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pbr",
|
|
3
3
|
"displayName": "Plan-Build-Run",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.57.0",
|
|
5
5
|
"description": "Plan-Build-Run — Structured development workflow for GitHub Copilot CLI. Solves context rot through disciplined agent delegation, structured planning, atomic execution, and goal-backward verification.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "SienkLogic",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pbr",
|
|
3
3
|
"displayName": "Plan-Build-Run",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.57.0",
|
|
5
5
|
"description": "Plan-Build-Run — Structured development workflow for Cursor. Solves context rot through disciplined subagent delegation, structured planning, atomic execution, and goal-backward verification.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "SienkLogic",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pbr",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.57.0",
|
|
4
4
|
"description": "Plan-Build-Run — Structured development workflow for Claude Code. Solves context rot through disciplined subagent delegation, structured planning, atomic execution, and goal-backward verification.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "SienkLogic",
|
|
@@ -6,6 +6,52 @@ const { route } = require('../router');
|
|
|
6
6
|
|
|
7
7
|
const VALID_CLASSIFICATIONS = ['correct', 'type_mismatch', 'vague'];
|
|
8
8
|
|
|
9
|
+
const CONVENTIONAL_COMMIT_RE = /^(feat|fix|refactor|test|docs|chore|wip|perf|ci|build|revert)(\(.*?\))?!?:/;
|
|
10
|
+
|
|
11
|
+
const TEST_FILE_RE = /(?:^|[/\\])tests[/\\]|\.test\.[jt]sx?$|\.spec\.[jt]sx?$/;
|
|
12
|
+
const MARKDOWN_RE = /\.mdx?$/i;
|
|
13
|
+
const CONFIG_TOOLING_RE = /\.(?:json|ya?ml|toml|ini|env|lock)$|(?:^|[/\\])(?:\.github|\.husky|scripts)[/\\]|(?:Makefile|Dockerfile|\.eslintrc|\.prettierrc|babel\.config|jest\.config|tsconfig|webpack\.config|rollup\.config)/i;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Heuristic-first classifier for git commit messages.
|
|
17
|
+
* Runs before the LLM path and returns early for unambiguous cases.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} commitMessage - the commit message to classify
|
|
20
|
+
* @param {string[]} [stagedFiles] - optional list of staged file paths
|
|
21
|
+
* @returns {{ classification: string, confidence: number }|null} result or null (fall through to LLM)
|
|
22
|
+
*/
|
|
23
|
+
function classifyCommitHeuristic(commitMessage, stagedFiles) {
|
|
24
|
+
const match = CONVENTIONAL_COMMIT_RE.exec(commitMessage);
|
|
25
|
+
if (!match) return null;
|
|
26
|
+
|
|
27
|
+
const type = match[1];
|
|
28
|
+
const files = stagedFiles && stagedFiles.length > 0 ? stagedFiles : [];
|
|
29
|
+
|
|
30
|
+
// Check for "fix" type but description implies addition
|
|
31
|
+
const descriptionPart = commitMessage.slice(match[0].length).trim();
|
|
32
|
+
if (type === 'fix' && /^(?:add|adds|adding|new\b)/i.test(descriptionPart)) {
|
|
33
|
+
return { classification: 'type_mismatch', confidence: 0.9 };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Type-to-file alignment checks (only when staged files are known)
|
|
37
|
+
if (files.length > 0) {
|
|
38
|
+
if (type === 'test' && files.every(f => TEST_FILE_RE.test(f))) {
|
|
39
|
+
return { classification: 'correct', confidence: 1.0 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (type === 'docs' && files.every(f => MARKDOWN_RE.test(f))) {
|
|
43
|
+
return { classification: 'correct', confidence: 1.0 };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if ((type === 'chore' || type === 'ci' || type === 'build') && files.every(f => CONFIG_TOOLING_RE.test(f))) {
|
|
47
|
+
return { classification: 'correct', confidence: 1.0 };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Type parsed cleanly — most conventional commits are typed correctly
|
|
52
|
+
return { classification: 'correct', confidence: 0.8 };
|
|
53
|
+
}
|
|
54
|
+
|
|
9
55
|
/**
|
|
10
56
|
* Classifies a git commit message for semantic correctness using the local LLM.
|
|
11
57
|
* Goes beyond regex validation — checks whether the commit type matches the
|
|
@@ -22,6 +68,29 @@ async function classifyCommit(config, planningDir, commitMessage, stagedFiles, s
|
|
|
22
68
|
if (!config.enabled || !config.features.commit_classification) return null;
|
|
23
69
|
if (isDisabled('commit-classification', config.advanced.disable_after_failures)) return null;
|
|
24
70
|
|
|
71
|
+
// Heuristic-first: skip LLM for unambiguous cases
|
|
72
|
+
const heuristic = classifyCommitHeuristic(commitMessage, stagedFiles);
|
|
73
|
+
if (heuristic !== null) {
|
|
74
|
+
logMetric(planningDir, {
|
|
75
|
+
session_id: sessionId || 'unknown',
|
|
76
|
+
timestamp: new Date().toISOString(),
|
|
77
|
+
operation: 'commit-classification',
|
|
78
|
+
model: 'heuristic',
|
|
79
|
+
latency_ms: 0,
|
|
80
|
+
tokens_used_local: 0,
|
|
81
|
+
tokens_saved_frontier: 150,
|
|
82
|
+
result: heuristic.classification,
|
|
83
|
+
fallback_used: false,
|
|
84
|
+
confidence: heuristic.confidence
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
classification: heuristic.classification,
|
|
88
|
+
confidence: heuristic.confidence,
|
|
89
|
+
latency_ms: 0,
|
|
90
|
+
fallback_used: false
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
25
94
|
const filesContext = stagedFiles && stagedFiles.length > 0
|
|
26
95
|
? '\nStaged files: ' + stagedFiles.slice(0, 20).join(', ')
|
|
27
96
|
: '';
|
|
@@ -65,4 +134,4 @@ async function classifyCommit(config, planningDir, commitMessage, stagedFiles, s
|
|
|
65
134
|
}
|
|
66
135
|
}
|
|
67
136
|
|
|
68
|
-
module.exports = { classifyCommit, VALID_CLASSIFICATIONS };
|
|
137
|
+
module.exports = { classifyCommit, classifyCommitHeuristic, VALID_CLASSIFICATIONS };
|
|
@@ -3,10 +3,83 @@
|
|
|
3
3
|
const { complete, tryParseJSON, isDisabled } = require('../client');
|
|
4
4
|
const { logMetric } = require('../metrics');
|
|
5
5
|
const { route } = require('../router');
|
|
6
|
+
const path = require('path');
|
|
6
7
|
|
|
7
8
|
const VALID_FILE_TYPES = ['plan', 'state', 'code', 'test', 'config', 'docs', 'template', 'other'];
|
|
8
9
|
const VALID_INTENTS = ['create', 'update', 'fix', 'refactor', 'delete'];
|
|
9
10
|
|
|
11
|
+
// Normalize path separators to forward slashes for consistent pattern matching.
|
|
12
|
+
function normalizePath(filePath) {
|
|
13
|
+
return filePath.replace(/\\/g, '/');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Heuristic-based file classification. Runs before the LLM path and returns a
|
|
18
|
+
* result object for clear-cut cases, or null for ambiguous inputs that should
|
|
19
|
+
* fall through to the LLM.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} filePath - the target file path
|
|
22
|
+
* @param {string} contentSnippet - first ~200 chars of the content being written
|
|
23
|
+
* @returns {{ file_type: string, intent: string, confidence: number }|null}
|
|
24
|
+
*/
|
|
25
|
+
function classifyFileIntentHeuristic(filePath, contentSnippet) {
|
|
26
|
+
const normalized = normalizePath(filePath);
|
|
27
|
+
const basename = path.basename(normalized);
|
|
28
|
+
const snippet = contentSnippet || '';
|
|
29
|
+
|
|
30
|
+
// --- Extension map (test extensions checked before generic ones) ---
|
|
31
|
+
if (/\.(test|spec)\.(js|ts|mjs|cjs)$/.test(basename)) {
|
|
32
|
+
return { file_type: 'test', intent: 'update', confidence: 1.0 };
|
|
33
|
+
}
|
|
34
|
+
if (/\.(tmpl|ejs)$/.test(basename)) {
|
|
35
|
+
return { file_type: 'template', intent: 'update', confidence: 1.0 };
|
|
36
|
+
}
|
|
37
|
+
if (/\.(json|yaml|yml)$/.test(basename)) {
|
|
38
|
+
return { file_type: 'config', intent: 'update', confidence: 1.0 };
|
|
39
|
+
}
|
|
40
|
+
if (/\.(js|ts|mjs|cjs)$/.test(basename)) {
|
|
41
|
+
// Even if in tests/ directory the double-extension rule above already caught it,
|
|
42
|
+
// but check path pattern here too as an extra safety net.
|
|
43
|
+
if (/(?:^|\/)(?:tests?|__tests__)\//.test(normalized)) {
|
|
44
|
+
return { file_type: 'test', intent: 'update', confidence: 1.0 };
|
|
45
|
+
}
|
|
46
|
+
return { file_type: 'code', intent: 'update', confidence: 1.0 };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- Path patterns (checked before .md extension so directory wins) ---
|
|
50
|
+
if (/(?:^|\/)(?:tests?|__tests__)\//.test(normalized)) {
|
|
51
|
+
return { file_type: 'test', intent: 'update', confidence: 1.0 };
|
|
52
|
+
}
|
|
53
|
+
if (/(?:^|\/)docs\//.test(normalized)) {
|
|
54
|
+
return { file_type: 'docs', intent: 'update', confidence: 1.0 };
|
|
55
|
+
}
|
|
56
|
+
if (/(?:^|\/)scripts\//.test(normalized)) {
|
|
57
|
+
return { file_type: 'code', intent: 'update', confidence: 1.0 };
|
|
58
|
+
}
|
|
59
|
+
// .planning/ special cases — STATE.md first, then general plan
|
|
60
|
+
if (/(?:^|\/)\.planning\//.test(normalized)) {
|
|
61
|
+
if (basename === 'STATE.md') {
|
|
62
|
+
return { file_type: 'state', intent: 'update', confidence: 1.0 };
|
|
63
|
+
}
|
|
64
|
+
return { file_type: 'plan', intent: 'update', confidence: 1.0 };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- Content signals (apply to any extension) ---
|
|
68
|
+
const trimmed = snippet.trimStart();
|
|
69
|
+
if (/^(?:describe|test|it)\s*\(/.test(trimmed)) {
|
|
70
|
+
return { file_type: 'test', intent: 'update', confidence: 1.0 };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --- .md extension: only classify when in a recognized path, else null ---
|
|
74
|
+
if (/\.md$/.test(basename)) {
|
|
75
|
+
// docs/ and .planning/ were already handled above; all other .md is ambiguous.
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Anything else is ambiguous — let the LLM decide.
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
10
83
|
/**
|
|
11
84
|
* Classifies the type and intent of a Write/Edit operation using the local LLM.
|
|
12
85
|
* Uses the file path and a short content snippet to determine what kind of file
|
|
@@ -25,6 +98,31 @@ async function classifyFileIntent(config, planningDir, filePath, contentSnippet,
|
|
|
25
98
|
|
|
26
99
|
const snippet = contentSnippet.length > 800 ? contentSnippet.slice(0, 800) : contentSnippet;
|
|
27
100
|
|
|
101
|
+
// --- Heuristic fast-path: skip the LLM for deterministic cases ---
|
|
102
|
+
const heuristic = classifyFileIntentHeuristic(filePath, snippet);
|
|
103
|
+
if (heuristic !== null) {
|
|
104
|
+
logMetric(planningDir, {
|
|
105
|
+
session_id: sessionId || 'unknown',
|
|
106
|
+
timestamp: new Date().toISOString(),
|
|
107
|
+
operation: 'file-intent',
|
|
108
|
+
model: 'heuristic',
|
|
109
|
+
latency_ms: 0,
|
|
110
|
+
tokens_used_local: 0,
|
|
111
|
+
tokens_saved_frontier: 150,
|
|
112
|
+
result: heuristic.file_type + '/' + heuristic.intent,
|
|
113
|
+
fallback_used: false,
|
|
114
|
+
confidence: heuristic.confidence
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
file_type: heuristic.file_type,
|
|
118
|
+
intent: heuristic.intent,
|
|
119
|
+
confidence: heuristic.confidence,
|
|
120
|
+
latency_ms: 0,
|
|
121
|
+
fallback_used: false
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// --- End heuristic fast-path ---
|
|
125
|
+
|
|
28
126
|
const prompt =
|
|
29
127
|
'Classify this file write operation. Based on the file path and content snippet, determine: (1) file_type: plan (PLAN.md, ROADMAP.md, planning docs), state (STATE.md, status tracking), code (source code, scripts), test (test files), config (JSON/YAML config, package.json), docs (README, documentation), template (templates, EJS), other. (2) intent: create (new file), update (modify existing), fix (bug fix), refactor (restructure), delete (removing content). Respond with JSON: {"file_type": "<one of 8>", "intent": "<one of 5>", "confidence": 0.0-1.0}\n\nPath: ' +
|
|
30
128
|
filePath + '\nContent snippet:\n' + snippet;
|
|
@@ -70,4 +168,4 @@ async function classifyFileIntent(config, planningDir, filePath, contentSnippet,
|
|
|
70
168
|
}
|
|
71
169
|
}
|
|
72
170
|
|
|
73
|
-
module.exports = { classifyFileIntent, VALID_FILE_TYPES, VALID_INTENTS };
|
|
171
|
+
module.exports = { classifyFileIntent, classifyFileIntentHeuristic, VALID_FILE_TYPES, VALID_INTENTS };
|