@planu/cli 5.3.45 → 5.3.47
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
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## [5.3.47] - 2026-08-24
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix(spec-1599): make handoff ownership and spec-review approval errors self-serviceable
|
|
5
|
+
|
|
6
|
+
### Chores
|
|
7
|
+
- chore(planu): record SPEC-1599 done and file SPEC-1600 dogfood spec
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [5.3.46] - 2026-08-24
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
- fix(spec-1285): meta filter sees backticked tool token; harden AC1 coverage
|
|
14
|
+
- fix(spec-1285): stop local privacy specs activating auth/meta challenge families
|
|
15
|
+
|
|
16
|
+
### Chores
|
|
17
|
+
- chore(planu): record SPEC-1285 done and SPEC-1598 discarded (merged into SPEC-1285)
|
|
18
|
+
|
|
19
|
+
|
|
1
20
|
## [5.3.45] - 2026-08-24
|
|
2
21
|
|
|
3
22
|
### Bug Fixes
|
|
@@ -239,7 +239,7 @@ function isSafeOwnershipResidual(value) {
|
|
|
239
239
|
remaining = remaining.slice(connector.length);
|
|
240
240
|
continue;
|
|
241
241
|
}
|
|
242
|
-
if ('
|
|
242
|
+
if (',;&.'.includes(remaining[0] ?? '')) {
|
|
243
243
|
remaining = remaining.slice(1);
|
|
244
244
|
continue;
|
|
245
245
|
}
|
|
@@ -259,8 +259,30 @@ function isSafeOwnershipResidual(value) {
|
|
|
259
259
|
}
|
|
260
260
|
return true;
|
|
261
261
|
}
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
const OWNERSHIP_LINE_SHAPE_HINT = 'Use exactly one backticked full path: - Create|Modify: `exact/canonical/path` (pending) — plain description. No path in prose, no extra backticked tokens.';
|
|
263
|
+
const OWNERSHIP_DUPLICATE_HINT = 'Path is declared twice under the same section; list it once.';
|
|
264
|
+
const OWNERSHIP_CONFLICT_HINT = 'Path is declared under both ### Create and ### Modify; keep it in only one.';
|
|
265
|
+
function ownershipMissingSectionHint(classification) {
|
|
266
|
+
return `Add a ### ${classification} subsection under ## Files with a - \`path\` bullet, or - None. if there are none.`;
|
|
267
|
+
}
|
|
268
|
+
function ownershipMalformedSectionHint(classification) {
|
|
269
|
+
return `The ### ${classification} subsection has no list items; add a - \`path\` bullet or - None.`;
|
|
270
|
+
}
|
|
271
|
+
function ownershipNoneConflictHint(classification) {
|
|
272
|
+
return `The ### ${classification} subsection mixes - None. with real paths; use one or the other.`;
|
|
273
|
+
}
|
|
274
|
+
function ownershipAgreementHint(canonicalClass, workPlanClass, path) {
|
|
275
|
+
if (canonicalClass === 'Absent') {
|
|
276
|
+
return `Declared as ${workPlanClass} in ### File-Level Work Plan with no matching entry in ## Files; add "- ${workPlanClass}: \`${path}\`" under ### ${workPlanClass} in ## Files, or remove the work-plan line.`;
|
|
277
|
+
}
|
|
278
|
+
if (workPlanClass === 'Absent') {
|
|
279
|
+
return `Declared under ### ${canonicalClass} in ## Files; add the line "- ${canonicalClass}: \`${path}\` (pending) — <description>" to ### File-Level Work Plan.`;
|
|
280
|
+
}
|
|
281
|
+
return `Declared under ### ${canonicalClass} in ## Files but as ${workPlanClass} in ### File-Level Work Plan; align the verb in one of the two sections.`;
|
|
282
|
+
}
|
|
283
|
+
function ownershipBlocker(path, classifications, hint) {
|
|
284
|
+
const base = `handoff_ownership_invalid path=${path} classifications=${classifications.join(',')}`;
|
|
285
|
+
return hint ? `${base} hint=${JSON.stringify(hint)}` : base;
|
|
264
286
|
}
|
|
265
287
|
function stripInertOwnershipMarkup(content) {
|
|
266
288
|
let fence = null;
|
|
@@ -312,7 +334,7 @@ function collectStrictOwnershipEntry(rawValue, classification, state) {
|
|
|
312
334
|
const residualIsSafe = isSafeOwnershipResidual(residual);
|
|
313
335
|
if (backtickedPaths.some((path) => path === null) ||
|
|
314
336
|
(backtickedValues.length > 0 && !residualIsSafe)) {
|
|
315
|
-
state.blockers.push(ownershipBlocker('<redacted>', [classification, 'InvalidPath']));
|
|
337
|
+
state.blockers.push(ownershipBlocker('<redacted>', [classification, 'InvalidPath'], OWNERSHIP_LINE_SHAPE_HINT));
|
|
316
338
|
return;
|
|
317
339
|
}
|
|
318
340
|
const directPath = normalizeStrictOwnedPath(rawPath);
|
|
@@ -323,7 +345,7 @@ function collectStrictOwnershipEntry(rawValue, classification, state) {
|
|
|
323
345
|
: [directPath];
|
|
324
346
|
if (paths.length === 0) {
|
|
325
347
|
const diagnosticPath = /^`[^`]+`$/u.test(rawPath) ? '<redacted>' : rawPath || '<empty>';
|
|
326
|
-
state.blockers.push(ownershipBlocker(diagnosticPath, [classification, 'InvalidPath']));
|
|
348
|
+
state.blockers.push(ownershipBlocker(diagnosticPath, [classification, 'InvalidPath'], OWNERSHIP_LINE_SHAPE_HINT));
|
|
327
349
|
return;
|
|
328
350
|
}
|
|
329
351
|
for (const path of paths) {
|
|
@@ -334,11 +356,11 @@ function collectStrictOwnershipPath(path, classification, state) {
|
|
|
334
356
|
const own = classification === 'Create' ? state.create : state.modify;
|
|
335
357
|
const other = classification === 'Create' ? state.modify : state.create;
|
|
336
358
|
if (own.has(path)) {
|
|
337
|
-
state.blockers.push(ownershipBlocker(path, [classification, classification]));
|
|
359
|
+
state.blockers.push(ownershipBlocker(path, [classification, classification], OWNERSHIP_DUPLICATE_HINT));
|
|
338
360
|
return;
|
|
339
361
|
}
|
|
340
362
|
if (other.has(path)) {
|
|
341
|
-
state.blockers.push(ownershipBlocker(path, ['Create', 'Modify']));
|
|
363
|
+
state.blockers.push(ownershipBlocker(path, ['Create', 'Modify'], OWNERSHIP_CONFLICT_HINT));
|
|
342
364
|
own.delete(path);
|
|
343
365
|
other.delete(path);
|
|
344
366
|
return;
|
|
@@ -354,7 +376,7 @@ function extractStrictCanonicalOwnership(content) {
|
|
|
354
376
|
const parseSection = (classification) => {
|
|
355
377
|
const subsection = sectionContentAtLevel(filesSection, classification, 3);
|
|
356
378
|
if (subsection === null) {
|
|
357
|
-
state.blockers.push(ownershipBlocker('<missing>', [classification, 'MissingSection']));
|
|
379
|
+
state.blockers.push(ownershipBlocker('<missing>', [classification, 'MissingSection'], ownershipMissingSectionHint(classification)));
|
|
358
380
|
return;
|
|
359
381
|
}
|
|
360
382
|
const items = subsection
|
|
@@ -362,14 +384,14 @@ function extractStrictCanonicalOwnership(content) {
|
|
|
362
384
|
.map((line) => /^\s*[-*]\s+(.+)$/u.exec(line)?.[1]?.trim())
|
|
363
385
|
.filter((item) => Boolean(item));
|
|
364
386
|
if (items.length === 0) {
|
|
365
|
-
state.blockers.push(ownershipBlocker('<empty>', [classification, 'Malformed']));
|
|
387
|
+
state.blockers.push(ownershipBlocker('<empty>', [classification, 'Malformed'], ownershipMalformedSectionHint(classification)));
|
|
366
388
|
return;
|
|
367
389
|
}
|
|
368
390
|
let explicitNone = false;
|
|
369
391
|
for (const item of items) {
|
|
370
392
|
if (/^none\.?$/iu.test(item.trim())) {
|
|
371
393
|
if (explicitNone) {
|
|
372
|
-
state.blockers.push(ownershipBlocker('<none>', [classification, classification]));
|
|
394
|
+
state.blockers.push(ownershipBlocker('<none>', [classification, classification], ownershipNoneConflictHint(classification)));
|
|
373
395
|
}
|
|
374
396
|
explicitNone = true;
|
|
375
397
|
continue;
|
|
@@ -378,7 +400,7 @@ function extractStrictCanonicalOwnership(content) {
|
|
|
378
400
|
}
|
|
379
401
|
const own = classification === 'Create' ? state.create : state.modify;
|
|
380
402
|
if (explicitNone && own.size > 0) {
|
|
381
|
-
state.blockers.push(ownershipBlocker('<none>', [classification, classification]));
|
|
403
|
+
state.blockers.push(ownershipBlocker('<none>', [classification, classification], ownershipNoneConflictHint(classification)));
|
|
382
404
|
}
|
|
383
405
|
};
|
|
384
406
|
parseSection('Create');
|
|
@@ -432,7 +454,9 @@ function validateOwnershipAgreement(canonical, workPlan, testDeclaredPaths) {
|
|
|
432
454
|
const workPlanClass = classification(workPlan, path);
|
|
433
455
|
return canonicalClass === workPlanClass
|
|
434
456
|
? []
|
|
435
|
-
: [
|
|
457
|
+
: [
|
|
458
|
+
ownershipBlocker(path, [`Canonical:${canonicalClass}`, `WorkPlan:${workPlanClass}`], ownershipAgreementHint(canonicalClass, workPlanClass, path)),
|
|
459
|
+
];
|
|
436
460
|
});
|
|
437
461
|
}
|
|
438
462
|
function extractBacktickedFiles(content) {
|
|
@@ -25,6 +25,7 @@ const EXPECTED_META_ANALYSIS_RE = /^\s*expected\s*:(?=[^.!?。!?]{0,240}\b(?
|
|
|
25
25
|
const QUALIFIED_EVIDENCE_SUBJECT_RE = /^\s*(?:[-*+>]\s+|\d+[.)]\s+)?(?:(?:given|when|then|and)\s+)?(?=[^.!?。!?]{0,240}\b(?:positive|negative|affirmative|meta-analysis(?:-only)?|regression|table-driven)\b)(?=[^.!?。!?]{0,240}\b(?:fixtures?|controls?|tests?|examples?|assertions?|findings?)\b)[^.!?。!?]{1,240}$/iu;
|
|
26
26
|
const CONTRACT_ANALYSIS_SUBJECT_RE = /^\s*(?:[-*+>]\s+|\d+[.)]\s+)?(?:(?:given|when|then|and)\s+)?(?:(?:the|this|that|a|an|any|each|our|existing|current)\s+)?(?:[\p{L}\p{N}_-]+\s+){0,2}contract\s+(?:evidence|inference|requirements?)\b/iu;
|
|
27
27
|
const CONTRACT_ANALYSIS_PREDICATE_RE = /\b(?:(?:is|are)\s+(?:inferred|expected|selected|classified|matched|detected|discussed|reported|recorded)|expects?|records?|describes?)\b|\b(?:detector|classifier|matcher)\s+evidence\b|\b(?:positive[ -]?control|expected\s+absence|meta-analysis(?:-only)?)\b/iu;
|
|
28
|
+
const META_ANALYSIS_TOOL_OUTPUT_RE = /\b(?:challenge[_\s]spec|challenge\s+tool)\b[^.!?\n]{0,120}?\bcall\b[^.!?\n]{0,120}?\b(?:emitted|returned|produced|reported|surfaced)\b[^.!?\n]{0,120}?\bscenarios?\b/iu;
|
|
28
29
|
function stripExcludedMarkdownSections(text) {
|
|
29
30
|
const lines = text.split('\n');
|
|
30
31
|
const kept = [];
|
|
@@ -71,7 +72,8 @@ export function stripMetaAnalysisText(text) {
|
|
|
71
72
|
!META_ANALYSIS_EVIDENCE_RE.test(clause) &&
|
|
72
73
|
!EXPECTED_META_ANALYSIS_RE.test(clause) &&
|
|
73
74
|
!QUALIFIED_EVIDENCE_SUBJECT_RE.test(clause) &&
|
|
74
|
-
!(CONTRACT_ANALYSIS_SUBJECT_RE.test(clause) && CONTRACT_ANALYSIS_PREDICATE_RE.test(clause))
|
|
75
|
+
!(CONTRACT_ANALYSIS_SUBJECT_RE.test(clause) && CONTRACT_ANALYSIS_PREDICATE_RE.test(clause)) &&
|
|
76
|
+
!META_ANALYSIS_TOOL_OUTPUT_RE.test(clause))
|
|
75
77
|
.join('\n');
|
|
76
78
|
}
|
|
77
79
|
function clauseBefore(text, index) {
|
|
@@ -16,7 +16,7 @@ const CAPABILITY_SIGNALS = {
|
|
|
16
16
|
],
|
|
17
17
|
authentication: [/\b(?:login|sign[ -]?in|password|jwt|oauth2?|session\s+auth)\b/i],
|
|
18
18
|
authorization: [
|
|
19
|
-
/\b(?:authori[sz]ation|permission|rbac|role-based|access\s+control|resource\s+ownership|tenant)\b/i,
|
|
19
|
+
/\b(?:authori[sz]ation|permission\s+(?:check|model|grant|denied|scope|boundary|set|policy)|access\s+permission|role\s+permission|rbac|role-based|access\s+control|resource\s+ownership|tenant)\b/i,
|
|
20
20
|
],
|
|
21
21
|
userInput: [
|
|
22
22
|
/\b(?:user|client)\s+input\b/i,
|
|
@@ -133,7 +133,7 @@ export function contentMentions(content, keywords) {
|
|
|
133
133
|
export function detectChallengeCapabilities(spec, specContent) {
|
|
134
134
|
const contractSource = stripReaderPrefix(spec, specContent);
|
|
135
135
|
const source = contractSource.trim().length > 0 ? contractSource : spec.title;
|
|
136
|
-
const contract = stripMetaAnalysisText(
|
|
136
|
+
const contract = stripNonContractText(stripMetaAnalysisText(source));
|
|
137
137
|
const capabilities = Object.fromEntries(Object.entries(CAPABILITY_SIGNALS).map(([capability, patterns]) => [
|
|
138
138
|
capability,
|
|
139
139
|
hasAnyAffirmedMatch(contract, patterns),
|
|
@@ -581,7 +581,7 @@ export async function checkSpecReviewGate(specId, projectId, _forceApprove) {
|
|
|
581
581
|
error: 'spec_review_stub',
|
|
582
582
|
message: 'Spec review feedback is a lifecycle-generated stub, not evidence from a real reviewer.',
|
|
583
583
|
blockers: [],
|
|
584
|
-
fixHint:
|
|
584
|
+
fixHint: `Write handoffs/${specId}/review_feedback.md as a substantive ReviewFeedbackV1 JSON: schema_version 1.0.0, verdict, reviewer.kind spec-review-agent, reviewer.agent planu-spec-reviewer, and a body covering scope reviewed, findings, and verdict rationale (not the lifecycle stub body). It must be written by an independent reviewer that is not the spec author.`,
|
|
585
585
|
});
|
|
586
586
|
}
|
|
587
587
|
if (review.verdict !== 'approved' || review.reviewer.verdict !== 'approved') {
|
package/package.json
CHANGED
package/planu-plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dev.planu.cli",
|
|
3
3
|
"displayName": "Planu — Spec Driven Development",
|
|
4
4
|
"description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
|
|
5
|
-
"version": "5.3.
|
|
5
|
+
"version": "5.3.47",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": ["npx", "@planu/cli@latest"],
|
|
8
8
|
"packageName": "@planu/cli",
|