@planu/cli 4.11.3 → 4.11.4

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,10 @@
1
+ ## [4.11.4] - 2026-07-18
2
+
3
+ ### Bug Fixes
4
+ - fix(planu): unblock challenge gate and record debt specs
5
+ - fix(release): harden local recovery environment
6
+
7
+
1
8
  ## [4.11.3] - 2026-07-10
2
9
 
3
10
  ### Bug Fixes
@@ -6,6 +6,8 @@ const FILLER_PATTERNS = [
6
6
  /\bhandle errors\b/i,
7
7
  /\bto be determined\b/i,
8
8
  ];
9
+ const MANUAL_VERIFICATION_STEP_RE = /\b(given|when|then|step|observe|click|run)\b/i;
10
+ const CONCRETE_MANUAL_EVIDENCE_RE = /`[^`]+`|\b(?:src|tests|docs|scripts|website|planu)\/[\w./-]+/i;
9
11
  export function evaluateImplementationContract(specBody, acceptanceCriteria) {
10
12
  const contract = extractTopLevelSection(specBody, IMPLEMENTATION_CONTRACT_SECTION);
11
13
  if (contract.trim().length === 0) {
@@ -62,7 +64,8 @@ export function evaluateImplementationContract(specBody, acceptanceCriteria) {
62
64
  }
63
65
  }
64
66
  if (/\bmanual verification\b/i.test(map) &&
65
- !/\b(given|when|then|step|observe|click|run)\b/i.test(map)) {
67
+ !MANUAL_VERIFICATION_STEP_RE.test(map) &&
68
+ !CONCRETE_MANUAL_EVIDENCE_RE.test(map)) {
66
69
  issues.push({
67
70
  code: 'contract_manual_verification_vague',
68
71
  message: 'Manual verification in the Implementation Contract must include exact observable steps.',
@@ -1,5 +1,7 @@
1
1
  import type { ChallengeReport, ChallengeResolutionEvidence, FailureScenario } from '../../types/index.js';
2
2
  export declare const MINIMUM_RESOLVED_CHALLENGES = 3;
3
+ export declare function requiredResolvedChallenges(totalScenarios: number): number;
4
+ export declare function parseChallengeResolutionEvidence(specContent: string, scenarios: readonly FailureScenario[], runAt: string): ChallengeResolutionEvidence[];
3
5
  /** Count only evidence tied to findings persisted by the same challenge report. */
4
6
  export declare function getResolvedChallengeEvidence(report: ChallengeReport | undefined): ChallengeResolutionEvidence[];
5
7
  /** Build a report whose resolution state is derived exclusively from explicit evidence. */
@@ -8,6 +10,7 @@ export declare function buildChallengeReport(input: {
8
10
  focusAreas: string[];
9
11
  overallRisk: ChallengeReport['overallRisk'];
10
12
  previousReport?: ChallengeReport;
13
+ explicitResolutionEvidence?: ChallengeResolutionEvidence[];
11
14
  runAt?: string;
12
15
  }): ChallengeReport;
13
16
  //# sourceMappingURL=challenge-report.d.ts.map
@@ -1,4 +1,7 @@
1
1
  export const MINIMUM_RESOLVED_CHALLENGES = 3;
2
+ export function requiredResolvedChallenges(totalScenarios) {
3
+ return Math.min(MINIMUM_RESOLVED_CHALLENGES, Math.max(0, totalScenarios));
4
+ }
2
5
  function isExplicitResolutionEvidence(value) {
3
6
  if (typeof value !== 'object' || value === null) {
4
7
  return false;
@@ -12,16 +15,72 @@ function isExplicitResolutionEvidence(value) {
12
15
  typeof candidate.resolvedAt === 'string' &&
13
16
  candidate.resolvedAt.trim().length > 0);
14
17
  }
15
- function uniqueMatchingEvidence(report, scenarioNames) {
16
- const evidence = Array.isArray(report?.resolutionEvidence) ? report.resolutionEvidence : [];
18
+ function uniqueMatchingEvidence(evidence, scenarioNames) {
17
19
  const matched = new Map();
18
- for (const item of evidence) {
20
+ for (const item of evidence ?? []) {
19
21
  if (isExplicitResolutionEvidence(item) && scenarioNames.has(item.scenario)) {
20
22
  matched.set(item.scenario, item);
21
23
  }
22
24
  }
23
25
  return [...matched.values()];
24
26
  }
27
+ function inferResolution(text) {
28
+ return /\b(accepted|accept risk|accepted risk|known risk)\b/i.test(text)
29
+ ? 'accepted'
30
+ : 'mitigated';
31
+ }
32
+ function extractChallengeResolutionSection(specContent) {
33
+ const normalized = specContent.replace(/\r\n/g, '\n');
34
+ const headingRe = /^###\s+Challenge Resolution[ \t]*$/m;
35
+ const match = headingRe.exec(normalized);
36
+ if (!match) {
37
+ return '';
38
+ }
39
+ const start = match.index + match[0].length;
40
+ const nextRe = /^###\s+\S/gm;
41
+ nextRe.lastIndex = start;
42
+ const next = nextRe.exec(normalized);
43
+ return normalized.slice(start, next ? next.index : normalized.length).trim();
44
+ }
45
+ export function parseChallengeResolutionEvidence(specContent, scenarios, runAt) {
46
+ const section = extractChallengeResolutionSection(specContent);
47
+ if (section.length === 0 || scenarios.length === 0) {
48
+ return [];
49
+ }
50
+ if (scenarios.length === 1) {
51
+ const [singleScenario] = scenarios;
52
+ if (!singleScenario) {
53
+ return [];
54
+ }
55
+ return [
56
+ {
57
+ scenario: singleScenario.scenario,
58
+ resolution: inferResolution(section),
59
+ evidence: section.replace(/\s+/g, ' ').trim(),
60
+ resolvedAt: runAt,
61
+ },
62
+ ];
63
+ }
64
+ const bullets = section
65
+ .split('\n')
66
+ .map((line) => line.trim())
67
+ .filter((line) => /^-\s+/.test(line))
68
+ .map((line) => line.replace(/^-\s+/, '').trim());
69
+ const evidence = [];
70
+ for (const bullet of bullets) {
71
+ const scenario = scenarios.find((item) => bullet.includes(item.scenario));
72
+ if (!scenario) {
73
+ continue;
74
+ }
75
+ evidence.push({
76
+ scenario: scenario.scenario,
77
+ resolution: inferResolution(bullet),
78
+ evidence: bullet,
79
+ resolvedAt: runAt,
80
+ });
81
+ }
82
+ return evidence;
83
+ }
25
84
  /** Count only evidence tied to findings persisted by the same challenge report. */
26
85
  export function getResolvedChallengeEvidence(report) {
27
86
  if (!Array.isArray(report?.findings)) {
@@ -30,21 +89,27 @@ export function getResolvedChallengeEvidence(report) {
30
89
  const scenarioNames = new Set(report.findings
31
90
  .map((finding) => finding.scenario)
32
91
  .filter((scenario) => typeof scenario === 'string' && scenario.trim().length > 0));
33
- return uniqueMatchingEvidence(report, scenarioNames);
92
+ return uniqueMatchingEvidence(report.resolutionEvidence, scenarioNames);
34
93
  }
35
94
  /** Build a report whose resolution state is derived exclusively from explicit evidence. */
36
95
  export function buildChallengeReport(input) {
37
96
  const scenarioNames = new Set(input.scenarios.map((scenario) => scenario.scenario));
38
- const resolutionEvidence = uniqueMatchingEvidence(input.previousReport, scenarioNames);
97
+ const resolutionEvidence = uniqueMatchingEvidence([
98
+ ...(Array.isArray(input.previousReport?.resolutionEvidence)
99
+ ? input.previousReport.resolutionEvidence
100
+ : []),
101
+ ...(Array.isArray(input.explicitResolutionEvidence) ? input.explicitResolutionEvidence : []),
102
+ ], scenarioNames);
39
103
  const resolvedScenarios = new Set(resolutionEvidence.map((item) => item.scenario));
40
104
  const addressedCount = resolvedScenarios.size;
105
+ const requiredCount = requiredResolvedChallenges(input.scenarios.length);
41
106
  return {
42
107
  runAt: input.runAt ?? new Date().toISOString(),
43
108
  totalScenarios: input.scenarios.length,
44
109
  addressedCount,
45
110
  focusAreas: input.focusAreas,
46
111
  overallRisk: input.overallRisk,
47
- passed: addressedCount >= MINIMUM_RESOLVED_CHALLENGES,
112
+ passed: addressedCount >= requiredCount,
48
113
  findings: input.scenarios.map((scenario) => ({
49
114
  scenario: scenario.scenario,
50
115
  resolved: resolvedScenarios.has(scenario.scenario),
@@ -18,7 +18,7 @@ import { calculateTokenBudget, injectBudgetIntoPrompt } from '../engine/token-bu
18
18
  import { analyzeMinimalImplementation, loadMinimalImplementationPolicy, } from '../engine/minimality/index.js';
19
19
  import { detectChallengeCapabilities, } from './challenge-spec/scenarios-utils.js';
20
20
  import { collectCapabilityScenarios } from './challenge-spec/scenario-collector.js';
21
- import { buildChallengeReport } from './challenge-spec/challenge-report.js';
21
+ import { buildChallengeReport, parseChallengeResolutionEvidence, } from './challenge-spec/challenge-report.js';
22
22
  const ALL_FOCUS_AREAS = [
23
23
  'failures',
24
24
  'concurrency',
@@ -194,11 +194,15 @@ export async function handleChallengeSpec(args, server) {
194
194
  scalabilityAssessment,
195
195
  overallRisk,
196
196
  };
197
+ const challengeRunAt = new Date().toISOString();
198
+ const explicitResolutionEvidence = parseChallengeResolutionEvidence(specContent, failureScenariosScored, challengeRunAt);
197
199
  const challengeReport = buildChallengeReport({
198
200
  scenarios: failureScenariosScored,
199
201
  focusAreas,
200
202
  overallRisk,
201
203
  previousReport: spec.challengeReport,
204
+ explicitResolutionEvidence,
205
+ runAt: challengeRunAt,
202
206
  });
203
207
  const rawHumanSummary = buildChallengeSpecSummary(prioritized, overallRisk);
204
208
  // SPEC-620: Inject token budget tag into the LLM-facing summary prompt
@@ -76,7 +76,7 @@ export declare function checkReadinessGate(spec: Spec, newStatus: SpecStatus, fo
76
76
  * or does not contain enough explicit resolution evidence.
77
77
  *
78
78
  * - Requires challengeReport to exist on the spec
79
- * - Requires at least 3 scenarios with explicit mitigation/risk-acceptance evidence
79
+ * - Requires explicit mitigation/risk-acceptance evidence for up to the first 3 findings
80
80
  * - High-risk specs (high/critical) require all 5 focus areas
81
81
  */
82
82
  export declare function checkChallengeGate(spec: Spec, newStatus: SpecStatus): ToolResult | null;
@@ -9,7 +9,7 @@ import { validateEnglishOnlySpecText } from '../../engine/spec-language/english-
9
9
  import { checkGroundedSpecContract } from '../../engine/spec-grounding/contract.js';
10
10
  import { checkGenericSpecOutput } from '../../engine/spec-quality/generic-output-gate.js';
11
11
  import { formatKeyValue } from '../output-formatter.js';
12
- import { getResolvedChallengeEvidence, MINIMUM_RESOLVED_CHALLENGES, } from '../challenge-spec/challenge-report.js';
12
+ import { getResolvedChallengeEvidence, requiredResolvedChallenges, } from '../challenge-spec/challenge-report.js';
13
13
  /**
14
14
  * Valid state transitions for spec lifecycle.
15
15
  * draft -> review -> approved -> implementing -> done
@@ -436,7 +436,7 @@ export async function checkReadinessGate(spec, newStatus, forceApprove) {
436
436
  * or does not contain enough explicit resolution evidence.
437
437
  *
438
438
  * - Requires challengeReport to exist on the spec
439
- * - Requires at least 3 scenarios with explicit mitigation/risk-acceptance evidence
439
+ * - Requires explicit mitigation/risk-acceptance evidence for up to the first 3 findings
440
440
  * - High-risk specs (high/critical) require all 5 focus areas
441
441
  */
442
442
  export function checkChallengeGate(spec, newStatus) {
@@ -462,12 +462,16 @@ export function checkChallengeGate(spec, newStatus) {
462
462
  }
463
463
  const resolutionEvidence = getResolvedChallengeEvidence(report);
464
464
  const addressedCount = resolutionEvidence.length;
465
- if (addressedCount < MINIMUM_RESOLVED_CHALLENGES) {
465
+ const findingsCount = Array.isArray(report.findings)
466
+ ? report.findings.length
467
+ : report.totalScenarios;
468
+ const requiredCount = requiredResolvedChallenges(findingsCount);
469
+ if (addressedCount < requiredCount) {
466
470
  return {
467
471
  content: [
468
472
  {
469
473
  type: 'text',
470
- text: `Challenge gate blocked: only ${String(addressedCount)} scenario(s) have explicit resolution evidence (minimum ${String(MINIMUM_RESOLVED_CHALLENGES)} required). Record mitigation or accepted-risk evidence for discovered findings before retrying.`,
474
+ text: `Challenge gate blocked: only ${String(addressedCount)} scenario(s) have explicit resolution evidence (minimum ${String(requiredCount)} required). Record mitigation or accepted-risk evidence for discovered findings before retrying.`,
471
475
  },
472
476
  ],
473
477
  isError: true,
@@ -475,7 +479,7 @@ export function checkChallengeGate(spec, newStatus) {
475
479
  error: 'CHALLENGE_GATE_BLOCKED',
476
480
  code: 'INSUFFICIENT_CHALLENGES',
477
481
  addressedCount,
478
- requiredCount: MINIMUM_RESOLVED_CHALLENGES,
482
+ requiredCount,
479
483
  fixHint: `Resolve challenge_spec findings with concrete evidence, then retry update_status(specId="${spec.id}", status="review").`,
480
484
  },
481
485
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "4.11.3",
3
+ "version": "4.11.4",
4
4
  "description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -34,14 +34,14 @@
34
34
  "packageName": "@planu/core"
35
35
  },
36
36
  "optionalDependencies": {
37
- "@planu/core-darwin-arm64": "4.11.3",
38
- "@planu/core-darwin-x64": "4.11.3",
39
- "@planu/core-linux-arm64-gnu": "4.11.3",
40
- "@planu/core-linux-arm64-musl": "4.11.3",
41
- "@planu/core-linux-x64-gnu": "4.11.3",
42
- "@planu/core-linux-x64-musl": "4.11.3",
43
- "@planu/core-win32-arm64-msvc": "4.11.3",
44
- "@planu/core-win32-x64-msvc": "4.11.3"
37
+ "@planu/core-darwin-arm64": "4.11.4",
38
+ "@planu/core-darwin-x64": "4.11.4",
39
+ "@planu/core-linux-arm64-gnu": "4.11.4",
40
+ "@planu/core-linux-arm64-musl": "4.11.4",
41
+ "@planu/core-linux-x64-gnu": "4.11.4",
42
+ "@planu/core-linux-x64-musl": "4.11.4",
43
+ "@planu/core-win32-arm64-msvc": "4.11.4",
44
+ "@planu/core-win32-x64-msvc": "4.11.4"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=24.0.0"
package/planu-native.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dev.planu.native",
3
3
  "displayName": "Planu Native Lightweight Surface",
4
- "version": "4.11.3",
4
+ "version": "4.11.4",
5
5
  "packageName": "@planu/cli",
6
6
  "modes": {
7
7
  "lightweight": {
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": "4.11.3",
5
+ "version": "4.11.4",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": [
8
8
  "npx",