@planu/cli 5.7.6 → 5.7.7
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 +13 -0
- package/dist/.planu-build.json +1 -1
- package/dist/config/hook-templates/planu-review-panel-trigger.sh +2 -3
- package/dist/config/skill-templates/planu-multi-teammate-review.md +7 -11
- package/dist/engine/evidence-gates/evidence-skeletons.js +3 -1
- package/dist/engine/readiness-checker.js +17 -6
- package/dist/engine/reconcile/propagate-mirrors.js +6 -6
- package/dist/engine/spec-format/lean-spec-generator.js +2 -2
- package/dist/engine/spec-grounding/contract.d.ts +3 -3
- package/dist/engine/spec-grounding/contract.js +23 -76
- package/dist/engine/spec-grounding/grounding-entry-parser.d.ts +5 -0
- package/dist/engine/spec-grounding/grounding-entry-parser.js +185 -0
- package/dist/storage/spec-store.js +8 -1
- package/dist/tools/bump-spec-version.js +5 -5
- package/dist/tools/init-project/skills-multi-teammate-review-writer.js +1 -1
- package/dist/tools/multi-teammate-review.js +1 -14
- package/dist/tools/register-platform-tools/design-stack-tools.js +3 -3
- package/dist/tools/register-spec-tools/analysis-tools.js +22 -9
- package/dist/tools/register-spec-tools/core-spec-tools.js +97 -43
- package/dist/tools/tool-registry/core-tools.js +71 -24
- package/dist/tools/tool-registry/group-quality-compliance.js +11 -14
- package/dist/tools/tool-registry-helpers.js +6 -2
- package/dist/types/spec-grounding.d.ts +14 -0
- package/package.json +1 -1
- package/planu-plugin.json +1 -1
- package/dist/tools/tool-registry/deprecated-stubs.d.ts +0 -19
- package/dist/tools/tool-registry/deprecated-stubs.js +0 -65
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## [5.7.7] - 2026-09-02
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix(hooks): route src/config template assets through reference classification in selector
|
|
5
|
+
- fix(tools): drop phantom multi_teammate_review from review-panel hook and scan hook templates
|
|
6
|
+
- fix(tools): delete dead deprecated-tool stubs dropped by the purged registry
|
|
7
|
+
- fix(tools): extend schema constraint audit to nested items and combinators
|
|
8
|
+
- fix(tools): declare regex and max constraints in tool input schema descriptions
|
|
9
|
+
- fix(storage): spec store returns absent for deleted canonical spec so validate fails closed with spec_not_found
|
|
10
|
+
- fix(spec-grounding): surface structured diagnostics for dropped grounding records
|
|
11
|
+
- fix(spec-format): accept plus bullet markers in checkbox and plain-list criterion parsers
|
|
12
|
+
|
|
13
|
+
|
|
1
14
|
## [5.7.6] - 2026-09-01
|
|
2
15
|
|
|
3
16
|
### Features
|
package/dist/.planu-build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"schemaVersion":1,"commit":"
|
|
1
|
+
{"schemaVersion":1,"commit":"239ab5e0b6df0ae6aae7e6a1a7ec9d699e1a66b8"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Wired as a PreToolUse hook for the Bash tool.
|
|
6
6
|
# When Claude Code is about to run a git diff or review command and the diff is
|
|
7
|
-
# large (>500 LOC), this hook
|
|
7
|
+
# large (>500 LOC), this hook suggests the panel review skill.
|
|
8
8
|
# It NEVER blocks execution — exit 0 always.
|
|
9
9
|
|
|
10
10
|
set -uo pipefail
|
|
@@ -26,8 +26,7 @@ if [[ "$LOC" -gt "$THRESHOLD" ]]; then
|
|
|
26
26
|
echo ""
|
|
27
27
|
echo "💡 Planu tip: This diff is ${LOC} LOC (threshold: ${THRESHOLD})."
|
|
28
28
|
echo " Consider running the 5-specialist panel review for thorough analysis:"
|
|
29
|
-
echo " →
|
|
30
|
-
echo " Or use the /planu-multi-teammate-review skill."
|
|
29
|
+
echo " → use the /planu-multi-teammate-review skill."
|
|
31
30
|
echo ""
|
|
32
31
|
fi
|
|
33
32
|
|
|
@@ -25,18 +25,14 @@ Use this skill when you want a thorough, multi-perspective code review:
|
|
|
25
25
|
|
|
26
26
|
**Trigger phrases**: "panel review", "thorough review", "5-agent review", "deep review", "run the specialist panel"
|
|
27
27
|
|
|
28
|
-
## How
|
|
28
|
+
## How this skill runs the review
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
commitRange: "main..HEAD", // optional: for context
|
|
37
|
-
spec: "<linked spec content>" // optional: helps the arbitrator detect spec drift
|
|
38
|
-
})
|
|
39
|
-
```
|
|
30
|
+
This skill runs the panel directly — it is not an MCP tool call. Gather the review inputs below, then run the panel yourself following the specialist roles described in this skill:
|
|
31
|
+
|
|
32
|
+
- `projectPath` — absolute path to the project
|
|
33
|
+
- `diff` — unified diff text (preferred: paste the output of `git diff`)
|
|
34
|
+
- `prNumber`, `branch`, or `commitRange` — optional context for the report header
|
|
35
|
+
- `spec` — optional linked spec content, enables spec-drift detection in the Maintainability specialist's report
|
|
40
36
|
|
|
41
37
|
At least one of `diff`, `prNumber`, `branch`, or `commitRange` must be provided.
|
|
42
38
|
|
|
@@ -32,7 +32,9 @@ function outOfScopeFromBody(specBody) {
|
|
|
32
32
|
}
|
|
33
33
|
/** Build a strict-schema discovery.json skeleton grounded in the spec's own grounding metadata. */
|
|
34
34
|
export function generateDiscoverySkeleton(args) {
|
|
35
|
-
const records = args.body
|
|
35
|
+
const records = args.body
|
|
36
|
+
? parseCriterionGroundingRecords(specFrontmatter(args.body)).records
|
|
37
|
+
: [];
|
|
36
38
|
const rules = records.length > 0 ? [...new Set(records.map((record) => record.text))] : [DEFAULT_RULE];
|
|
37
39
|
const examples = records.length > 0
|
|
38
40
|
? records.map((record) => ({
|
|
@@ -12,14 +12,29 @@ import { evaluateImplementationContract } from './implementation-contract/index.
|
|
|
12
12
|
import { extractNormalizedAcceptanceCriteria } from './spec-format/acceptance-criteria.js';
|
|
13
13
|
import { isRetiredScaffoldText } from './spec-format/retired-scaffold.js';
|
|
14
14
|
import { detectCrossSpecPremiseContradictions } from './contradiction-detector.js';
|
|
15
|
+
import { parseCriterionGroundingRecords, parseTechnicalReferenceGroundingRecords, } from './spec-grounding/contract.js';
|
|
15
16
|
// ── SPEC-784: Technical section quality constants ─────────────────────────────
|
|
16
17
|
const TECHNICAL_MIN_CHARS = 500;
|
|
18
|
+
const GROUNDING_FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
|
|
17
19
|
// Detects "See technical.md", "See spec.md", or "See `<file>` technical.md" patterns
|
|
18
20
|
const TECHNICAL_PLACEHOLDER_REGEX = /\bSee\s+(?:`?[^`\n]+`?\s+)?(?:technical|spec)\.md/i;
|
|
19
21
|
const BDD_CRITERION_RE = /\bGIVEN\b[\s\S]*\bWHEN\b[\s\S]*\bTHEN\b/i;
|
|
20
22
|
function isExecutableBddLine(line) {
|
|
21
23
|
return !isRetiredScaffoldText(line) && BDD_CRITERION_RE.test(line);
|
|
22
24
|
}
|
|
25
|
+
function groundingParseDiagnosticFindings(huRaw) {
|
|
26
|
+
const frontmatter = GROUNDING_FRONTMATTER_RE.exec(huRaw)?.[1] ?? '';
|
|
27
|
+
const criteriaDiagnostics = parseCriterionGroundingRecords(frontmatter).diagnostics;
|
|
28
|
+
const technicalDiagnostics = parseTechnicalReferenceGroundingRecords(frontmatter).diagnostics;
|
|
29
|
+
return [...criteriaDiagnostics, ...technicalDiagnostics].map((diagnostic) => `GROUNDING_PARSE_DROPPED: "${diagnostic.identifier}" has an invalid ${diagnostic.field} ` +
|
|
30
|
+
`("${diagnostic.value}") — allowed: ${diagnostic.allowed.join(', ') || 'n/a'}`);
|
|
31
|
+
}
|
|
32
|
+
function checkScenariosWithoutTests(huRaw, bddScenarioCount) {
|
|
33
|
+
if (bddScenarioCount === 0) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
return findScenariosWithoutTests(huRaw).map((title) => `scenarios_missing_tests: scenario "${title}" has no tests array — add tests entries as a block list (tests: with one "- path" line per test) or an inline array (tests: ["path"]) (SPEC-732)`);
|
|
37
|
+
}
|
|
23
38
|
function checkZeroBddCriteria(criteriaLines) {
|
|
24
39
|
if (criteriaLines.length === 0 || criteriaLines.some(isExecutableBddLine)) {
|
|
25
40
|
return [];
|
|
@@ -449,16 +464,12 @@ export async function checkSpecReadiness(spec, mode, projectHash) {
|
|
|
449
464
|
// SPEC-732: Executable AC gate — block approved when any scenario lacks a `tests` array.
|
|
450
465
|
// Only enforce when the spec uses frontmatter scenarios (BDD format).
|
|
451
466
|
const bddScenarioCount = countFrontmatterScenarios(huRaw);
|
|
452
|
-
|
|
453
|
-
const scenariosWithoutTests = findScenariosWithoutTests(huRaw);
|
|
454
|
-
for (const title of scenariosWithoutTests) {
|
|
455
|
-
allBlockers.push(`scenarios_missing_tests: scenario "${title}" has no tests array — add tests entries as a block list (tests: with one "- path" line per test) or an inline array (tests: ["path"]) (SPEC-732)`);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
467
|
+
allBlockers.push(...checkScenariosWithoutTests(huRaw, bddScenarioCount));
|
|
458
468
|
const scavengeWarning = frontmatterScenarioScavengeWarning(bddScenarioCount, normalizedCriteria);
|
|
459
469
|
if (scavengeWarning) {
|
|
460
470
|
allWarnings.push(scavengeWarning);
|
|
461
471
|
}
|
|
472
|
+
allWarnings.push(...groundingParseDiagnosticFindings(huRaw));
|
|
462
473
|
// SPEC-1214: BDD-criteria executable-evidence gate — when a spec has no frontmatter
|
|
463
474
|
// scenarios, its criteria must carry a TEST: marker mapping to an executable test file,
|
|
464
475
|
// or validation will later score 0 with "No executable scenarios declared".
|
|
@@ -33,9 +33,9 @@ function propagateAcceptanceCriteria(content) {
|
|
|
33
33
|
const sectionBody = content.slice(section.contentStart, section.end);
|
|
34
34
|
const criteriaTexts = extractSectionCriteria(sectionBody);
|
|
35
35
|
const scenarios = criteriaTexts.map(criterionToScenario);
|
|
36
|
-
const oldRecords = mapWithFrontmatter(content, (fm) => indexCriterionRecords(parseCriterionGroundingRecords(fm)));
|
|
36
|
+
const oldRecords = mapWithFrontmatter(content, (fm) => indexCriterionRecords(parseCriterionGroundingRecords(fm).records));
|
|
37
37
|
const oldTestPaths = mapWithFrontmatter(content, (fm) => parseTechnicalReferenceGroundingRecords(fm)
|
|
38
|
-
.filter((record) => record.section === 'test')
|
|
38
|
+
.records.filter((record) => record.section === 'test')
|
|
39
39
|
.map((record) => record.path));
|
|
40
40
|
const criteriaRecords = criteriaTexts.map((text) => oldRecords.get(normalizeCriterionText(text)) ?? {
|
|
41
41
|
text,
|
|
@@ -59,9 +59,9 @@ function propagateFiles(content) {
|
|
|
59
59
|
const sectionBody = content.slice(section.contentStart, section.end);
|
|
60
60
|
const files = parseFilesSection(sectionBody);
|
|
61
61
|
const oldTestPaths = mapWithFrontmatter(content, (fm) => parseTechnicalReferenceGroundingRecords(fm)
|
|
62
|
-
.filter((record) => record.section === 'test')
|
|
62
|
+
.records.filter((record) => record.section === 'test')
|
|
63
63
|
.map((record) => record.path));
|
|
64
|
-
const oldTechnicalReferences = mapWithFrontmatter(content, (fm) => indexTechnicalReferenceRecords(parseTechnicalReferenceGroundingRecords(fm)));
|
|
64
|
+
const oldTechnicalReferences = mapWithFrontmatter(content, (fm) => indexTechnicalReferenceRecords(parseTechnicalReferenceGroundingRecords(fm).records));
|
|
65
65
|
const technicalReferences = buildTechnicalReferenceRecords(files, oldTechnicalReferences);
|
|
66
66
|
const newTestPaths = files.test;
|
|
67
67
|
let result = transformFrontmatter(content, (fm) => replaceFrontmatterKeyBlock(fm, 'grounding', groundingBlockLines(criteriaRecordsFromFrontmatter(fm), technicalReferences)));
|
|
@@ -285,10 +285,10 @@ function mapWithFrontmatter(content, read) {
|
|
|
285
285
|
return read(match?.[1] ?? '');
|
|
286
286
|
}
|
|
287
287
|
function technicalReferencesFromFrontmatter(frontmatter) {
|
|
288
|
-
return parseTechnicalReferenceGroundingRecords(frontmatter);
|
|
288
|
+
return parseTechnicalReferenceGroundingRecords(frontmatter).records;
|
|
289
289
|
}
|
|
290
290
|
function criteriaRecordsFromFrontmatter(frontmatter) {
|
|
291
|
-
return parseCriterionGroundingRecords(frontmatter);
|
|
291
|
+
return parseCriterionGroundingRecords(frontmatter).records;
|
|
292
292
|
}
|
|
293
293
|
function groundingBlockLines(criteria, technicalReferences) {
|
|
294
294
|
const criteriaLines = renderGroundingFrontmatter(criteria).slice(2);
|
|
@@ -154,7 +154,7 @@ export function extractCriteria(description) {
|
|
|
154
154
|
/** Extract checkbox-style criteria: `- [ ] text` or `- [x] text`. */
|
|
155
155
|
function extractCheckboxCriteria(description) {
|
|
156
156
|
const criteria = [];
|
|
157
|
-
const checkboxRegex = /^[
|
|
157
|
+
const checkboxRegex = /^[-*+]\s*\[([ xX])\]\s*(.+)$/gm;
|
|
158
158
|
let match = checkboxRegex.exec(description);
|
|
159
159
|
while (match) {
|
|
160
160
|
criteria.push({
|
|
@@ -205,7 +205,7 @@ function extractPlainListCriteria(description) {
|
|
|
205
205
|
}
|
|
206
206
|
const criteria = [];
|
|
207
207
|
// Plain list: `- text` or `* text`, not followed by `[`
|
|
208
|
-
const plainListRegex = /^[
|
|
208
|
+
const plainListRegex = /^[-*+]\s+(?!\[)(.+)$/gm;
|
|
209
209
|
let match = plainListRegex.exec(acceptanceSection);
|
|
210
210
|
while (match) {
|
|
211
211
|
const text = match[1]?.trim() ?? '';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Spec } from '../../types/index.js';
|
|
2
|
-
import type { CriterionGroundingRecord, GroundingGateResult, TechnicalReferenceGroundingRecord } from '../../types/spec-grounding.js';
|
|
2
|
+
import type { CriterionGroundingRecord, GroundingGateResult, GroundingParseResult, TechnicalReferenceGroundingRecord } from '../../types/spec-grounding.js';
|
|
3
3
|
export { normalizeCriterionText } from '../criterion-identity.js';
|
|
4
4
|
export declare function isGenericCriterion(text: string): boolean;
|
|
5
5
|
export declare function filterGroundedCriteria(criteria: string[]): string[];
|
|
@@ -19,8 +19,8 @@ export declare function getAdvisoryCriteria(records: CriterionGroundingRecord[])
|
|
|
19
19
|
export declare function renderGroundingFrontmatter(records: CriterionGroundingRecord[]): string[];
|
|
20
20
|
export declare function renderTechnicalReferenceGroundingFrontmatter(records: TechnicalReferenceGroundingRecord[]): string[];
|
|
21
21
|
export declare function checkGroundedSpecContract(spec: Spec, content: string): GroundingGateResult;
|
|
22
|
-
export declare function parseCriterionGroundingRecords(frontmatter: string): CriterionGroundingRecord
|
|
23
|
-
export declare function parseTechnicalReferenceGroundingRecords(frontmatter: string): TechnicalReferenceGroundingRecord
|
|
22
|
+
export declare function parseCriterionGroundingRecords(frontmatter: string): GroundingParseResult<CriterionGroundingRecord>;
|
|
23
|
+
export declare function parseTechnicalReferenceGroundingRecords(frontmatter: string): GroundingParseResult<TechnicalReferenceGroundingRecord>;
|
|
24
24
|
export declare function parseApprovedFileLevelWorkPlanPaths(content: string): string[];
|
|
25
25
|
export declare function parseGroundedTechnicalPaths(content: string): string[];
|
|
26
26
|
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import YAML from 'yaml';
|
|
2
2
|
import { createCriterionIdentity, normalizeCriterionText } from '../criterion-identity.js';
|
|
3
|
-
|
|
4
|
-
'user_input',
|
|
5
|
-
'project_evidence',
|
|
6
|
-
'documented_assumption',
|
|
7
|
-
'ungrounded_advisory',
|
|
8
|
-
];
|
|
9
|
-
const GROUNDING_CONFIDENCES = ['low', 'medium', 'high'];
|
|
10
|
-
const TECHNICAL_REFERENCE_SECTIONS = [
|
|
11
|
-
'create',
|
|
12
|
-
'modify',
|
|
13
|
-
'test',
|
|
14
|
-
];
|
|
3
|
+
import { isRecord, toCriterionGroundingRecord, toTechnicalReferenceGroundingRecord, } from './grounding-entry-parser.js';
|
|
15
4
|
export { normalizeCriterionText } from '../criterion-identity.js';
|
|
16
5
|
const GENERIC_CRITERION_PATTERNS = [
|
|
17
6
|
/\bimplementation complete\b/i,
|
|
@@ -115,7 +104,7 @@ export function renderTechnicalReferenceGroundingFrontmatter(records) {
|
|
|
115
104
|
export function checkGroundedSpecContract(spec, content) {
|
|
116
105
|
const frontmatter = FRONTMATTER_RE.exec(content)?.[1] ?? '';
|
|
117
106
|
const required = /^grounding_required:\s*true\s*$/m.test(frontmatter);
|
|
118
|
-
const records = parseCriterionGroundingRecords(frontmatter);
|
|
107
|
+
const records = parseCriterionGroundingRecords(frontmatter).records;
|
|
119
108
|
if (!required) {
|
|
120
109
|
return { passed: true, required: false, issues: [], records };
|
|
121
110
|
}
|
|
@@ -131,7 +120,7 @@ export function checkGroundedSpecContract(spec, content) {
|
|
|
131
120
|
issues.push(`Criterion has no grounding evidence: ${record.text}`);
|
|
132
121
|
}
|
|
133
122
|
}
|
|
134
|
-
const technicalRecords = parseTechnicalReferenceGroundingRecords(frontmatter);
|
|
123
|
+
const technicalRecords = parseTechnicalReferenceGroundingRecords(frontmatter).records;
|
|
135
124
|
for (const record of technicalRecords) {
|
|
136
125
|
if (record.source === 'ungrounded_advisory' || record.source === 'documented_assumption') {
|
|
137
126
|
issues.push(`Technical reference is advisory-only and cannot be contract: ${record.path}`);
|
|
@@ -150,23 +139,28 @@ export function checkGroundedSpecContract(spec, content) {
|
|
|
150
139
|
export function parseCriterionGroundingRecords(frontmatter) {
|
|
151
140
|
const outcome = parseFrontmatterYaml(frontmatter);
|
|
152
141
|
if (!outcome.parsed) {
|
|
153
|
-
return parseCriterionGroundingRecordsLegacy(frontmatter);
|
|
142
|
+
return { records: parseCriterionGroundingRecordsLegacy(frontmatter), diagnostics: [] };
|
|
154
143
|
}
|
|
155
144
|
const grounding = readGroundingSection(outcome.document);
|
|
156
145
|
const entries = Array.isArray(grounding?.criteria) ? grounding.criteria : [];
|
|
157
146
|
const records = [];
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
147
|
+
const diagnostics = [];
|
|
148
|
+
for (const [index, entry] of entries.entries()) {
|
|
149
|
+
const entryOutcome = toCriterionGroundingRecord(entry, index);
|
|
150
|
+
if (entryOutcome.record) {
|
|
151
|
+
records.push(entryOutcome.record);
|
|
162
152
|
}
|
|
153
|
+
diagnostics.push(...entryOutcome.diagnostics);
|
|
163
154
|
}
|
|
164
|
-
return records;
|
|
155
|
+
return { records, diagnostics };
|
|
165
156
|
}
|
|
166
157
|
export function parseTechnicalReferenceGroundingRecords(frontmatter) {
|
|
167
158
|
const outcome = parseFrontmatterYaml(frontmatter);
|
|
168
159
|
if (!outcome.parsed) {
|
|
169
|
-
return
|
|
160
|
+
return {
|
|
161
|
+
records: parseTechnicalReferenceGroundingRecordsLegacy(frontmatter),
|
|
162
|
+
diagnostics: [],
|
|
163
|
+
};
|
|
170
164
|
}
|
|
171
165
|
const grounding = readGroundingSection(outcome.document);
|
|
172
166
|
const entries = grounding
|
|
@@ -177,13 +171,15 @@ export function parseTechnicalReferenceGroundingRecords(frontmatter) {
|
|
|
177
171
|
? outcome.document.technicalReferences
|
|
178
172
|
: [];
|
|
179
173
|
const records = [];
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
174
|
+
const diagnostics = [];
|
|
175
|
+
for (const [index, entry] of entries.entries()) {
|
|
176
|
+
const entryOutcome = toTechnicalReferenceGroundingRecord(entry, index);
|
|
177
|
+
if (entryOutcome.record) {
|
|
178
|
+
records.push(entryOutcome.record);
|
|
184
179
|
}
|
|
180
|
+
diagnostics.push(...entryOutcome.diagnostics);
|
|
185
181
|
}
|
|
186
|
-
return records;
|
|
182
|
+
return { records, diagnostics };
|
|
187
183
|
}
|
|
188
184
|
function parseFrontmatterYaml(frontmatter) {
|
|
189
185
|
let value;
|
|
@@ -199,55 +195,6 @@ function readGroundingSection(document) {
|
|
|
199
195
|
const grounding = document?.grounding;
|
|
200
196
|
return isRecord(grounding) ? grounding : undefined;
|
|
201
197
|
}
|
|
202
|
-
function toCriterionGroundingRecord(entry) {
|
|
203
|
-
if (!isRecord(entry)) {
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
const text = typeof entry.text === 'string' && entry.text.length > 0 ? entry.text : null;
|
|
207
|
-
const source = toGroundingSource(entry.source);
|
|
208
|
-
const confidence = toGroundingConfidence(entry.confidence);
|
|
209
|
-
if (!text || !source || !confidence) {
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
return { text, source, evidence: toStringArray(entry.evidence), confidence };
|
|
213
|
-
}
|
|
214
|
-
function toTechnicalReferenceGroundingRecord(entry) {
|
|
215
|
-
if (!isRecord(entry)) {
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
const path = typeof entry.path === 'string' && entry.path.length > 0 ? entry.path : null;
|
|
219
|
-
const section = toTechnicalReferenceSection(entry.section);
|
|
220
|
-
const source = toGroundingSource(entry.source);
|
|
221
|
-
const confidence = toGroundingConfidence(entry.confidence);
|
|
222
|
-
if (!path || !section || !source || !confidence) {
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
return { path, section, source, evidence: toStringArray(entry.evidence), confidence };
|
|
226
|
-
}
|
|
227
|
-
function toGroundingSource(value) {
|
|
228
|
-
return typeof value === 'string' && GROUNDING_SOURCES.includes(value)
|
|
229
|
-
? value
|
|
230
|
-
: null;
|
|
231
|
-
}
|
|
232
|
-
function toGroundingConfidence(value) {
|
|
233
|
-
return typeof value === 'string' && GROUNDING_CONFIDENCES.includes(value)
|
|
234
|
-
? value
|
|
235
|
-
: null;
|
|
236
|
-
}
|
|
237
|
-
function toTechnicalReferenceSection(value) {
|
|
238
|
-
return typeof value === 'string' &&
|
|
239
|
-
TECHNICAL_REFERENCE_SECTIONS.includes(value)
|
|
240
|
-
? value
|
|
241
|
-
: null;
|
|
242
|
-
}
|
|
243
|
-
function toStringArray(value) {
|
|
244
|
-
return Array.isArray(value)
|
|
245
|
-
? value.filter((item) => typeof item === 'string')
|
|
246
|
-
: [];
|
|
247
|
-
}
|
|
248
|
-
function isRecord(value) {
|
|
249
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
250
|
-
}
|
|
251
198
|
function parseCriterionGroundingRecordsLegacy(frontmatter) {
|
|
252
199
|
const records = [];
|
|
253
200
|
const chunks = frontmatter.split(/\n\s{4}- text:\s*/).slice(1);
|
|
@@ -311,7 +258,7 @@ export function parseGroundedTechnicalPaths(content) {
|
|
|
311
258
|
const frontmatter = FRONTMATTER_RE.exec(content)?.[1] ?? '';
|
|
312
259
|
return [
|
|
313
260
|
...new Set([
|
|
314
|
-
...parseTechnicalReferenceGroundingRecords(frontmatter).map((record) => record.path),
|
|
261
|
+
...parseTechnicalReferenceGroundingRecords(frontmatter).records.map((record) => record.path),
|
|
315
262
|
...parseApprovedFileLevelWorkPlanPaths(content),
|
|
316
263
|
]),
|
|
317
264
|
];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CriterionGroundingRecord, GroundingEntryOutcome, TechnicalReferenceGroundingRecord } from '../../types/spec-grounding.js';
|
|
2
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
3
|
+
export declare function toCriterionGroundingRecord(entry: unknown, index: number): GroundingEntryOutcome<CriterionGroundingRecord>;
|
|
4
|
+
export declare function toTechnicalReferenceGroundingRecord(entry: unknown, index: number): GroundingEntryOutcome<TechnicalReferenceGroundingRecord>;
|
|
5
|
+
//# sourceMappingURL=grounding-entry-parser.d.ts.map
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const GROUNDING_SOURCES = [
|
|
2
|
+
'user_input',
|
|
3
|
+
'project_evidence',
|
|
4
|
+
'documented_assumption',
|
|
5
|
+
'ungrounded_advisory',
|
|
6
|
+
];
|
|
7
|
+
const GROUNDING_CONFIDENCES = ['low', 'medium', 'high'];
|
|
8
|
+
const TECHNICAL_REFERENCE_SECTIONS = [
|
|
9
|
+
'create',
|
|
10
|
+
'modify',
|
|
11
|
+
'test',
|
|
12
|
+
];
|
|
13
|
+
export function isRecord(value) {
|
|
14
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function stringifyDiagnosticValue(value) {
|
|
17
|
+
if (typeof value === 'string') {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
if (value === undefined) {
|
|
21
|
+
return 'undefined';
|
|
22
|
+
}
|
|
23
|
+
if (value === null) {
|
|
24
|
+
return 'null';
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
27
|
+
return value.toString();
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return JSON.stringify(value);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return 'unstringifiable value';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function nonObjectEntryDiagnostic(entry, index) {
|
|
37
|
+
return {
|
|
38
|
+
identifier: `entry[${String(index)}]`,
|
|
39
|
+
field: 'entry',
|
|
40
|
+
value: stringifyDiagnosticValue(entry),
|
|
41
|
+
allowed: [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function toGroundingSource(value) {
|
|
45
|
+
return typeof value === 'string' && GROUNDING_SOURCES.includes(value)
|
|
46
|
+
? value
|
|
47
|
+
: null;
|
|
48
|
+
}
|
|
49
|
+
function toGroundingConfidence(value) {
|
|
50
|
+
return typeof value === 'string' && GROUNDING_CONFIDENCES.includes(value)
|
|
51
|
+
? value
|
|
52
|
+
: null;
|
|
53
|
+
}
|
|
54
|
+
function toTechnicalReferenceSection(value) {
|
|
55
|
+
return typeof value === 'string' &&
|
|
56
|
+
TECHNICAL_REFERENCE_SECTIONS.includes(value)
|
|
57
|
+
? value
|
|
58
|
+
: null;
|
|
59
|
+
}
|
|
60
|
+
function toStringArrayWithDiagnostics(value, identifier) {
|
|
61
|
+
if (!Array.isArray(value)) {
|
|
62
|
+
return { evidence: [], diagnostics: [] };
|
|
63
|
+
}
|
|
64
|
+
const evidence = [];
|
|
65
|
+
const diagnostics = [];
|
|
66
|
+
for (const item of value) {
|
|
67
|
+
if (typeof item === 'string') {
|
|
68
|
+
evidence.push(item);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
diagnostics.push({
|
|
72
|
+
identifier,
|
|
73
|
+
field: 'evidence',
|
|
74
|
+
value: stringifyDiagnosticValue(item),
|
|
75
|
+
allowed: ['string'],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { evidence, diagnostics };
|
|
80
|
+
}
|
|
81
|
+
export function toCriterionGroundingRecord(entry, index) {
|
|
82
|
+
if (!isRecord(entry)) {
|
|
83
|
+
return { record: null, diagnostics: [nonObjectEntryDiagnostic(entry, index)] };
|
|
84
|
+
}
|
|
85
|
+
const text = typeof entry.text === 'string' && entry.text.length > 0 ? entry.text : null;
|
|
86
|
+
const identifier = text ?? `entry[${String(index)}]`;
|
|
87
|
+
const source = toGroundingSource(entry.source);
|
|
88
|
+
if (!source) {
|
|
89
|
+
return {
|
|
90
|
+
record: null,
|
|
91
|
+
diagnostics: [
|
|
92
|
+
{
|
|
93
|
+
identifier,
|
|
94
|
+
field: 'source',
|
|
95
|
+
value: stringifyDiagnosticValue(entry.source),
|
|
96
|
+
allowed: GROUNDING_SOURCES,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const confidence = toGroundingConfidence(entry.confidence);
|
|
102
|
+
if (!confidence) {
|
|
103
|
+
return {
|
|
104
|
+
record: null,
|
|
105
|
+
diagnostics: [
|
|
106
|
+
{
|
|
107
|
+
identifier,
|
|
108
|
+
field: 'confidence',
|
|
109
|
+
value: stringifyDiagnosticValue(entry.confidence),
|
|
110
|
+
allowed: GROUNDING_CONFIDENCES,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (!text) {
|
|
116
|
+
return {
|
|
117
|
+
record: null,
|
|
118
|
+
diagnostics: [
|
|
119
|
+
{ identifier, field: 'text', value: stringifyDiagnosticValue(entry.text), allowed: [] },
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const { evidence, diagnostics } = toStringArrayWithDiagnostics(entry.evidence, identifier);
|
|
124
|
+
return { record: { text, source, evidence, confidence }, diagnostics };
|
|
125
|
+
}
|
|
126
|
+
export function toTechnicalReferenceGroundingRecord(entry, index) {
|
|
127
|
+
if (!isRecord(entry)) {
|
|
128
|
+
return { record: null, diagnostics: [nonObjectEntryDiagnostic(entry, index)] };
|
|
129
|
+
}
|
|
130
|
+
const path = typeof entry.path === 'string' && entry.path.length > 0 ? entry.path : null;
|
|
131
|
+
const identifier = path ?? `entry[${String(index)}]`;
|
|
132
|
+
const section = toTechnicalReferenceSection(entry.section);
|
|
133
|
+
if (!section) {
|
|
134
|
+
return {
|
|
135
|
+
record: null,
|
|
136
|
+
diagnostics: [
|
|
137
|
+
{
|
|
138
|
+
identifier,
|
|
139
|
+
field: 'section',
|
|
140
|
+
value: stringifyDiagnosticValue(entry.section),
|
|
141
|
+
allowed: TECHNICAL_REFERENCE_SECTIONS,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const source = toGroundingSource(entry.source);
|
|
147
|
+
if (!source) {
|
|
148
|
+
return {
|
|
149
|
+
record: null,
|
|
150
|
+
diagnostics: [
|
|
151
|
+
{
|
|
152
|
+
identifier,
|
|
153
|
+
field: 'source',
|
|
154
|
+
value: stringifyDiagnosticValue(entry.source),
|
|
155
|
+
allowed: GROUNDING_SOURCES,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const confidence = toGroundingConfidence(entry.confidence);
|
|
161
|
+
if (!confidence) {
|
|
162
|
+
return {
|
|
163
|
+
record: null,
|
|
164
|
+
diagnostics: [
|
|
165
|
+
{
|
|
166
|
+
identifier,
|
|
167
|
+
field: 'confidence',
|
|
168
|
+
value: stringifyDiagnosticValue(entry.confidence),
|
|
169
|
+
allowed: GROUNDING_CONFIDENCES,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (!path) {
|
|
175
|
+
return {
|
|
176
|
+
record: null,
|
|
177
|
+
diagnostics: [
|
|
178
|
+
{ identifier, field: 'path', value: stringifyDiagnosticValue(entry.path), allowed: [] },
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const { evidence, diagnostics } = toStringArrayWithDiagnostics(entry.evidence, identifier);
|
|
183
|
+
return { record: { path, section, source, evidence, confidence }, diagnostics };
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=grounding-entry-parser.js.map
|
|
@@ -256,6 +256,9 @@ async function refreshSpecFromDisk(projectId, spec, canonicalRoot) {
|
|
|
256
256
|
specMdPath = await resolveVerifiedSpecPath(spec.id, spec.specPath, canonicalRoot);
|
|
257
257
|
}
|
|
258
258
|
catch (error) {
|
|
259
|
+
if (error instanceof PortablePathError && error.code === 'NOT_FOUND') {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
259
262
|
reportClassifiedDegradation('SPEC_CONTENT_DIGEST_CHECK_SKIPPED', error);
|
|
260
263
|
return spec;
|
|
261
264
|
}
|
|
@@ -264,6 +267,9 @@ async function refreshSpecFromDisk(projectId, spec, canonicalRoot) {
|
|
|
264
267
|
content = await readFile(specMdPath, 'utf-8');
|
|
265
268
|
}
|
|
266
269
|
catch (error) {
|
|
270
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
267
273
|
reportClassifiedDegradation('SPEC_CONTENT_DIGEST_CHECK_SKIPPED', error);
|
|
268
274
|
return spec;
|
|
269
275
|
}
|
|
@@ -290,7 +296,8 @@ export async function listSpecs(projectId, canonicalRoot) {
|
|
|
290
296
|
if (canonicalRoot === undefined) {
|
|
291
297
|
return specs;
|
|
292
298
|
}
|
|
293
|
-
|
|
299
|
+
const refreshed = await Promise.all(specs.map((spec) => refreshSpecFromDisk(projectId, spec, canonicalRoot)));
|
|
300
|
+
return refreshed.filter((spec) => spec !== null);
|
|
294
301
|
}
|
|
295
302
|
/**
|
|
296
303
|
* Run a read-modify side effect against a fresh spec snapshot while holding the
|
|
@@ -19,14 +19,14 @@ const BumpSpecVersionSchema = {
|
|
|
19
19
|
.min(1)
|
|
20
20
|
.max(4096)
|
|
21
21
|
.optional()
|
|
22
|
-
.describe('Absolute path to the project root. Takes precedence over projectId.'),
|
|
22
|
+
.describe('Absolute path to the project root. Takes precedence over projectId. Max 4096 characters.'),
|
|
23
23
|
projectId: z
|
|
24
24
|
.string()
|
|
25
25
|
.min(1)
|
|
26
26
|
.max(500)
|
|
27
27
|
.optional()
|
|
28
|
-
.describe('Project ID (hash). Required if projectPath is not provided.'),
|
|
29
|
-
specId: SpecIdSchema.describe('Spec ID to bump, e.g. SPEC-042.'),
|
|
28
|
+
.describe('Project ID (hash). Required if projectPath is not provided. Max 500 characters.'),
|
|
29
|
+
specId: SpecIdSchema.describe('Spec ID to bump, e.g. SPEC-042. Must match pattern ^SPEC-\\d+$, max 50 characters.'),
|
|
30
30
|
kind: z
|
|
31
31
|
.enum(['patch', 'minor', 'major'])
|
|
32
32
|
.describe('Bump kind: patch (typo/format), minor (new criterion/scope expansion), major (breaking change).'),
|
|
@@ -34,13 +34,13 @@ const BumpSpecVersionSchema = {
|
|
|
34
34
|
.string()
|
|
35
35
|
.min(5)
|
|
36
36
|
.max(500)
|
|
37
|
-
.describe('Free-text reason for the bump
|
|
37
|
+
.describe('Free-text reason for the bump. Min 5, max 500 characters. Recorded in history.'),
|
|
38
38
|
by: z
|
|
39
39
|
.string()
|
|
40
40
|
.min(1)
|
|
41
41
|
.max(200)
|
|
42
42
|
.optional()
|
|
43
|
-
.describe('Actor identifier (session id, plugin id, username). Defaults to a session token.'),
|
|
43
|
+
.describe('Actor identifier (session id, plugin id, username). Defaults to a session token. Max 200 characters.'),
|
|
44
44
|
};
|
|
45
45
|
// ---------------------------------------------------------------------------
|
|
46
46
|
// Handler
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
// tools/multi-teammate-review.ts — MCP tool registration for multi-teammate review (SPEC-593)
|
|
2
|
-
// NOTE (SPEC-658): registerMultiTeammateReviewTool now registers a deprecation stub.
|
|
3
|
-
// The underlying handleMultiTeammateReview logic is kept for direct import by skills/tests.
|
|
4
1
|
import { z } from 'zod';
|
|
5
|
-
// safeTracked removed — registration replaced by deprecation stub (SPEC-658)
|
|
6
2
|
import { toolResult, formatError } from './response-helpers.js';
|
|
7
|
-
import { makeDeprecationStub } from './tool-registry/deprecated-stubs.js';
|
|
8
|
-
import { registerFromEntries } from './tool-entry.js';
|
|
9
3
|
import { runReviewPanel } from '../engine/multi-teammate-review/panel-orchestrator.js';
|
|
10
4
|
import { buildModelHeader } from '../engine/model-router/subtask-model-assigner.js';
|
|
11
5
|
// ---------------------------------------------------------------------------
|
|
@@ -117,14 +111,7 @@ export async function handleMultiTeammateReview(args) {
|
|
|
117
111
|
];
|
|
118
112
|
return toolResult(outputLines.join('\n'));
|
|
119
113
|
}
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
// Tool registration (SPEC-658: replaced with deprecation stub)
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
114
|
export function registerMultiTeammateReviewTool(s) {
|
|
124
|
-
|
|
125
|
-
// The tool stays registered so callers receive a helpful message, not "unknown tool".
|
|
126
|
-
registerFromEntries(s, [
|
|
127
|
-
makeDeprecationStub('multi_teammate_review', '.claude/skills/review-and-merge.md', 'review-and-merge'),
|
|
128
|
-
]);
|
|
115
|
+
void s;
|
|
129
116
|
}
|
|
130
117
|
//# sourceMappingURL=multi-teammate-review.js.map
|