@planu/cli 4.11.3 → 4.11.5
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/engine/ai-integration/gemini/gemini-md-generator.js +7 -0
- package/dist/engine/implementation-contract/evaluator.js +4 -1
- package/dist/engine/outbound-webhook-dispatcher.d.ts +2 -2
- package/dist/engine/outbound-webhook-dispatcher.js +8 -6
- package/dist/engine/web-fetcher/registry-auto-discovery.d.ts +2 -2
- package/dist/engine/web-fetcher/registry-auto-discovery.js +9 -5
- package/dist/tools/challenge-spec/challenge-report.d.ts +3 -0
- package/dist/tools/challenge-spec/challenge-report.js +71 -6
- package/dist/tools/challenge-spec.js +5 -1
- package/dist/tools/update-status/transition-guard.d.ts +1 -1
- package/dist/tools/update-status/transition-guard.js +9 -5
- package/dist/types/docs.d.ts +4 -0
- package/dist/types/outbound-webhook.d.ts +4 -0
- package/package.json +9 -9
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## [4.11.5] - 2026-07-18
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix: close weekly technical debt backlog
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## [4.11.4] - 2026-07-18
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
- fix(planu): unblock challenge gate and record debt specs
|
|
11
|
+
- fix(release): harden local recovery environment
|
|
12
|
+
|
|
13
|
+
|
|
1
14
|
## [4.11.3] - 2026-07-10
|
|
2
15
|
|
|
3
16
|
### Bug Fixes
|
|
@@ -58,12 +58,19 @@ function buildSddSection(options) {
|
|
|
58
58
|
'',
|
|
59
59
|
'### Architecture Rules',
|
|
60
60
|
'',
|
|
61
|
+
'- Primary implementation stays in `src/`; Rust is an optional acceleration layer, not the default destination for every feature',
|
|
61
62
|
'- `types/` → no imports from other layers',
|
|
62
63
|
'- `engine/` → imports only from `types/`',
|
|
63
64
|
'- `storage/` → imports only from `types/`',
|
|
64
65
|
'- `tools/` → imports from `engine/` + `storage/` + `types/`',
|
|
65
66
|
'- Cross-layer violations → ESLint error',
|
|
66
67
|
'',
|
|
68
|
+
'### Release Rules',
|
|
69
|
+
'',
|
|
70
|
+
'- `main` is the authoritative release branch',
|
|
71
|
+
'- `develop` and `release` are optional mirrors that must not drift from `main`',
|
|
72
|
+
'- Run `pnpm check` and `pnpm test` before local release publishing',
|
|
73
|
+
'',
|
|
67
74
|
PLANU_SECTION_END,
|
|
68
75
|
].join('\n');
|
|
69
76
|
}
|
|
@@ -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
|
-
|
|
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,4 +1,4 @@
|
|
|
1
|
-
import type { OutboundWebhookPayload, OutboundWebhookEventType, OutboundWebhookConfig, OutboundWebhookDelivery } from '../types/index.js';
|
|
2
|
-
export declare function dispatchToWebhooks(webhooks: readonly OutboundWebhookConfig[], payload: OutboundWebhookPayload, onDelivery: (delivery: OutboundWebhookDelivery) => Promise<void
|
|
1
|
+
import type { OutboundWebhookPayload, OutboundWebhookEventType, OutboundWebhookConfig, OutboundWebhookDelivery, WebhookDispatchOptions } from '../types/index.js';
|
|
2
|
+
export declare function dispatchToWebhooks(webhooks: readonly OutboundWebhookConfig[], payload: OutboundWebhookPayload, onDelivery: (delivery: OutboundWebhookDelivery) => Promise<void>, options?: WebhookDispatchOptions): Promise<void>;
|
|
3
3
|
export declare function buildSpecLifecyclePayload(event: OutboundWebhookEventType, specId: string, specTitle: string, status: string, projectPath: string): OutboundWebhookPayload;
|
|
4
4
|
//# sourceMappingURL=outbound-webhook-dispatcher.d.ts.map
|
|
@@ -40,13 +40,15 @@ async function attemptDelivery(url, body, signature) {
|
|
|
40
40
|
// Retry logic: 3 attempts with exponential backoff (1s, 5s, 30s)
|
|
41
41
|
// ---------------------------------------------------------------------------
|
|
42
42
|
const RETRY_DELAYS_MS = [1_000, 5_000, 30_000];
|
|
43
|
-
async function deliverWithRetry(webhook, body, payload, onDelivery) {
|
|
43
|
+
async function deliverWithRetry(webhook, body, payload, onDelivery, options) {
|
|
44
44
|
let lastResult = null;
|
|
45
45
|
const signature = signPayload(webhook.secret, body);
|
|
46
|
-
|
|
46
|
+
const retryDelays = options.retryDelaysMs ?? RETRY_DELAYS_MS;
|
|
47
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
48
|
+
for (let attempt = 0; attempt < retryDelays.length + 1; attempt++) {
|
|
47
49
|
if (attempt > 0) {
|
|
48
|
-
const delay =
|
|
49
|
-
await
|
|
50
|
+
const delay = retryDelays[attempt - 1] ?? 0;
|
|
51
|
+
await sleep(delay);
|
|
50
52
|
}
|
|
51
53
|
lastResult = await attemptDelivery(webhook.url, body, signature);
|
|
52
54
|
if (lastResult.success) {
|
|
@@ -71,12 +73,12 @@ async function deliverWithRetry(webhook, body, payload, onDelivery) {
|
|
|
71
73
|
// ---------------------------------------------------------------------------
|
|
72
74
|
// Core dispatch (pure — no storage deps)
|
|
73
75
|
// ---------------------------------------------------------------------------
|
|
74
|
-
export async function dispatchToWebhooks(webhooks, payload, onDelivery) {
|
|
76
|
+
export async function dispatchToWebhooks(webhooks, payload, onDelivery, options = {}) {
|
|
75
77
|
if (webhooks.length === 0) {
|
|
76
78
|
return;
|
|
77
79
|
}
|
|
78
80
|
const body = JSON.stringify(payload);
|
|
79
|
-
await Promise.allSettled(webhooks.map((webhook) => deliverWithRetry(webhook, body, payload, onDelivery)));
|
|
81
|
+
await Promise.allSettled(webhooks.map((webhook) => deliverWithRetry(webhook, body, payload, onDelivery, options)));
|
|
80
82
|
}
|
|
81
83
|
// ---------------------------------------------------------------------------
|
|
82
84
|
// Helper to build spec lifecycle payloads
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DiscoveredDocsEntry, DocsRegistryHealth } from '../../types/index.js';
|
|
1
|
+
import type { DiscoveredDocsEntry, DocsRegistryHealth, DocsDiscoveryOptions } from '../../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Verify that a URL is alive with a HEAD request (timeout 3s).
|
|
4
4
|
* Returns true if status is 200, 301, or 302.
|
|
@@ -9,7 +9,7 @@ export declare function validateRegistryUrl(url: string): Promise<boolean>;
|
|
|
9
9
|
* Respects a 1 req/s rate limit per registry via simple delay.
|
|
10
10
|
* Returns null if not found anywhere.
|
|
11
11
|
*/
|
|
12
|
-
export declare function discoverDocsUrl(tech: string): Promise<DiscoveredDocsEntry | null>;
|
|
12
|
+
export declare function discoverDocsUrl(tech: string, options?: DocsDiscoveryOptions): Promise<DiscoveredDocsEntry | null>;
|
|
13
13
|
/**
|
|
14
14
|
* Batch validate all entries in the docs registry.
|
|
15
15
|
* Categorizes each entry as healthy (<=2s), slow (2-5s), or broken (timeout/error).
|
|
@@ -113,10 +113,14 @@ export async function validateRegistryUrl(url) {
|
|
|
113
113
|
* Respects a 1 req/s rate limit per registry via simple delay.
|
|
114
114
|
* Returns null if not found anywhere.
|
|
115
115
|
*/
|
|
116
|
-
export async function discoverDocsUrl(tech) {
|
|
117
|
-
const
|
|
116
|
+
export async function discoverDocsUrl(tech, options = {}) {
|
|
117
|
+
const rateLimitDelayMs = options.rateLimitDelayMs ?? RATE_LIMIT_DELAY_MS;
|
|
118
|
+
const sleepFn = options.sleep ?? sleep;
|
|
119
|
+
const tryWithDelay = async (fn, shouldDelay) => {
|
|
118
120
|
const result = await fn();
|
|
119
|
-
|
|
121
|
+
if (shouldDelay && rateLimitDelayMs > 0) {
|
|
122
|
+
await sleepFn(rateLimitDelayMs);
|
|
123
|
+
}
|
|
120
124
|
return result;
|
|
121
125
|
};
|
|
122
126
|
const registries = [
|
|
@@ -124,8 +128,8 @@ export async function discoverDocsUrl(tech) {
|
|
|
124
128
|
{ source: 'pypi', fn: () => tryPypi(tech) },
|
|
125
129
|
{ source: 'crates', fn: () => tryCrates(tech) },
|
|
126
130
|
];
|
|
127
|
-
for (const { source, fn } of registries) {
|
|
128
|
-
const url = await tryWithDelay(fn);
|
|
131
|
+
for (const [index, { source, fn }] of registries.entries()) {
|
|
132
|
+
const url = await tryWithDelay(fn, index < registries.length - 1);
|
|
129
133
|
if (url) {
|
|
130
134
|
const verified = await validateRegistryUrl(url);
|
|
131
135
|
return {
|
|
@@ -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(
|
|
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(
|
|
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 >=
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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/dist/types/docs.d.ts
CHANGED
|
@@ -285,6 +285,10 @@ export interface DocsRegistryHealth {
|
|
|
285
285
|
broken: string[];
|
|
286
286
|
slow: string[];
|
|
287
287
|
}
|
|
288
|
+
export interface DocsDiscoveryOptions {
|
|
289
|
+
rateLimitDelayMs?: number;
|
|
290
|
+
sleep?: (ms: number) => Promise<void>;
|
|
291
|
+
}
|
|
288
292
|
/** Input for validate_docs_registry tool. */
|
|
289
293
|
export interface ValidateDocsRegistryInput {
|
|
290
294
|
projectPath: string;
|
|
@@ -48,4 +48,8 @@ export interface WebhookAttemptResult {
|
|
|
48
48
|
readonly success: boolean;
|
|
49
49
|
readonly error?: string;
|
|
50
50
|
}
|
|
51
|
+
export interface WebhookDispatchOptions {
|
|
52
|
+
readonly retryDelaysMs?: readonly number[];
|
|
53
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
54
|
+
}
|
|
51
55
|
//# sourceMappingURL=outbound-webhook.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.11.
|
|
3
|
+
"version": "4.11.5",
|
|
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.
|
|
38
|
-
"@planu/core-darwin-x64": "4.11.
|
|
39
|
-
"@planu/core-linux-arm64-gnu": "4.11.
|
|
40
|
-
"@planu/core-linux-arm64-musl": "4.11.
|
|
41
|
-
"@planu/core-linux-x64-gnu": "4.11.
|
|
42
|
-
"@planu/core-linux-x64-musl": "4.11.
|
|
43
|
-
"@planu/core-win32-arm64-msvc": "4.11.
|
|
44
|
-
"@planu/core-win32-x64-msvc": "4.11.
|
|
37
|
+
"@planu/core-darwin-arm64": "4.11.5",
|
|
38
|
+
"@planu/core-darwin-x64": "4.11.5",
|
|
39
|
+
"@planu/core-linux-arm64-gnu": "4.11.5",
|
|
40
|
+
"@planu/core-linux-arm64-musl": "4.11.5",
|
|
41
|
+
"@planu/core-linux-x64-gnu": "4.11.5",
|
|
42
|
+
"@planu/core-linux-x64-musl": "4.11.5",
|
|
43
|
+
"@planu/core-win32-arm64-msvc": "4.11.5",
|
|
44
|
+
"@planu/core-win32-x64-msvc": "4.11.5"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=24.0.0"
|
package/planu-native.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": "4.11.
|
|
5
|
+
"version": "4.11.5",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": [
|
|
8
8
|
"npx",
|