@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.
Files changed (27) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/.planu-build.json +1 -1
  3. package/dist/config/hook-templates/planu-review-panel-trigger.sh +2 -3
  4. package/dist/config/skill-templates/planu-multi-teammate-review.md +7 -11
  5. package/dist/engine/evidence-gates/evidence-skeletons.js +3 -1
  6. package/dist/engine/readiness-checker.js +17 -6
  7. package/dist/engine/reconcile/propagate-mirrors.js +6 -6
  8. package/dist/engine/spec-format/lean-spec-generator.js +2 -2
  9. package/dist/engine/spec-grounding/contract.d.ts +3 -3
  10. package/dist/engine/spec-grounding/contract.js +23 -76
  11. package/dist/engine/spec-grounding/grounding-entry-parser.d.ts +5 -0
  12. package/dist/engine/spec-grounding/grounding-entry-parser.js +185 -0
  13. package/dist/storage/spec-store.js +8 -1
  14. package/dist/tools/bump-spec-version.js +5 -5
  15. package/dist/tools/init-project/skills-multi-teammate-review-writer.js +1 -1
  16. package/dist/tools/multi-teammate-review.js +1 -14
  17. package/dist/tools/register-platform-tools/design-stack-tools.js +3 -3
  18. package/dist/tools/register-spec-tools/analysis-tools.js +22 -9
  19. package/dist/tools/register-spec-tools/core-spec-tools.js +97 -43
  20. package/dist/tools/tool-registry/core-tools.js +71 -24
  21. package/dist/tools/tool-registry/group-quality-compliance.js +11 -14
  22. package/dist/tools/tool-registry-helpers.js +6 -2
  23. package/dist/types/spec-grounding.d.ts +14 -0
  24. package/package.json +1 -1
  25. package/planu-plugin.json +1 -1
  26. package/dist/tools/tool-registry/deprecated-stubs.d.ts +0 -19
  27. 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
@@ -1 +1 @@
1
- {"schemaVersion":1,"commit":"1eaa28a4fbc0df47b8b62c888bbb34446c05810e"}
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 prints a suggestion to use multi_teammate_review.
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 " → multi_teammate_review({ projectPath: \"$(pwd)\", diff: \"<paste diff here>\" })"
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 to invoke the tool
28
+ ## How this skill runs the review
29
29
 
30
- ```
31
- multi_teammate_review({
32
- projectPath: "/absolute/path/to/project",
33
- diff: "<unified diff text>", // preferred: paste the output of git diff
34
- prNumber: 42, // optional: for context in the report header
35
- branch: "feat/my-feature", // optional: for context
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 ? parseCriterionGroundingRecords(specFrontmatter(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
- if (bddScenarioCount > 0) {
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 = /^[-*]\s*\[([ xX])\]\s*(.+)$/gm;
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 = /^[-*]\s+(?!\[)(.+)$/gm;
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
- const GROUNDING_SOURCES = [
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
- for (const entry of entries) {
159
- const record = toCriterionGroundingRecord(entry);
160
- if (record) {
161
- records.push(record);
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 parseTechnicalReferenceGroundingRecordsLegacy(frontmatter);
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
- for (const entry of entries) {
181
- const record = toTechnicalReferenceGroundingRecord(entry);
182
- if (record) {
183
- records.push(record);
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
- return Promise.all(specs.map((spec) => refreshSpecFromDisk(projectId, spec, canonicalRoot)));
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 (min 5 chars). Recorded in history.'),
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
@@ -31,7 +31,7 @@ triggers:
31
31
 
32
32
  # /planu-multi-teammate-review
33
33
 
34
- Invoke \`multi_teammate_review\` for a 5-specialist panel code review.
34
+ Runs a 5-specialist panel code review directly — not an MCP tool call.
35
35
  `;
36
36
  }
37
37
  }
@@ -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
- // multi_teammate_review has been migrated to .claude/skills/review-and-merge.md
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