@planu/cli 5.1.1 → 5.2.0
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 +27 -0
- package/dist/cli/commands/doctor.d.ts +22 -0
- package/dist/cli/commands/doctor.js +176 -2
- package/dist/engine/autopilot/bootstrap.js +27 -0
- package/dist/engine/core-bridge.d.ts +28 -0
- package/dist/engine/core-bridge.js +67 -0
- package/dist/engine/drift-monitor.js +16 -18
- package/dist/engine/living-spec/hash-tracker.js +26 -28
- package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-arm64.node.sbom.json +4 -4
- package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-x64.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +4 -4
- package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +4 -4
- package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +4 -4
- package/dist/engine/spec-language/english-only.d.ts +14 -0
- package/dist/engine/spec-language/english-only.js +58 -0
- package/dist/engine/spec-migrator/criteria-scanner.js +11 -13
- package/dist/engine/spec-migrator/drift-detector.js +10 -12
- package/dist/engine/vector-store/tfidf.d.ts +13 -9
- package/dist/engine/vector-store/tfidf.js +26 -0
- package/dist/engine/worker-config-loader.d.ts +1 -1
- package/dist/engine/worker-config-loader.js +1 -11
- package/dist/engine/workers/schema.d.ts +0 -8
- package/dist/engine/workers/schema.js +0 -1
- package/dist/i18n/index.d.ts +18 -0
- package/dist/i18n/index.js +40 -1
- package/dist/storage/global-store.d.ts +9 -0
- package/dist/storage/global-store.js +23 -0
- package/dist/storage/semantic-index-store.d.ts +23 -0
- package/dist/storage/semantic-index-store.js +105 -0
- package/dist/storage/status-store/self-healing.js +15 -17
- package/dist/tools/challenge-spec/scenarios-utils.js +5 -1
- package/dist/tools/create-spec.js +60 -15
- package/dist/tools/init-project/handler.js +78 -25
- package/dist/tools/learn.js +10 -8
- package/dist/tools/registry/auth.js +1 -11
- package/dist/tools/semantic-search-handler.js +5 -6
- package/dist/tools/status-handler.js +19 -6
- package/dist/tools/validation-loop-handler.js +16 -15
- package/dist/types/spec-language-translation.d.ts +18 -0
- package/dist/types/spec-language-translation.js +5 -0
- package/dist/types/spec-registry.d.ts +0 -2
- package/dist/types/status.d.ts +2 -0
- package/dist/types/vector-store.d.ts +18 -0
- package/dist/types/workers.d.ts +0 -3
- package/package.json +9 -9
- package/planu-native.json +8 -29
- package/planu-plugin.json +1 -1
- package/dist/engine/security/cve-refresher.d.ts +0 -12
- package/dist/engine/security/cve-refresher.js +0 -128
|
@@ -11,6 +11,7 @@ import { analyzeProject, checkProjectCompleteness } from '../../engine/analyzer.
|
|
|
11
11
|
import { parseProjectQualityRules } from '../../engine/auditor-claude-md.js';
|
|
12
12
|
import { detectMissingAreas, buildRequestPrompt } from '../../engine/context-merger.js';
|
|
13
13
|
import { detectProjectPrivacyConfig, maybeGenerateEnvExample, buildBeginnerEnvGuide, } from './helpers.js';
|
|
14
|
+
import { buildDefaultPrivacyConfig } from '../../engine/pii-detector.js';
|
|
14
15
|
import { checkBundledVersionGap } from '../../engine/version-detector/bundled-version-checker.js';
|
|
15
16
|
import { checkAndFixBundledVersion } from '../../engine/mcp-config/mcp-config-writer.js';
|
|
16
17
|
import { buildInitProjectResult } from './result-builder.js';
|
|
@@ -86,6 +87,25 @@ function detectMultipleFrameworks(stack) {
|
|
|
86
87
|
}
|
|
87
88
|
return null;
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* SPEC-1343: Runs one independent, best-effort pipeline stage in isolation so it can be
|
|
92
|
+
* batched with `Promise.all` alongside other independent stages. A rejection never
|
|
93
|
+
* propagates to the caller — it resolves to `fallback` and is flagged via `failed`/`error`
|
|
94
|
+
* so init_project can still succeed and surface the failure in its result summary (AC2).
|
|
95
|
+
*/
|
|
96
|
+
async function runOptionalStage(name, fallback, task) {
|
|
97
|
+
try {
|
|
98
|
+
return { name, value: await task(), failed: false };
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
return {
|
|
102
|
+
name,
|
|
103
|
+
value: fallback,
|
|
104
|
+
failed: true,
|
|
105
|
+
error: err instanceof Error ? err.message : String(err),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
89
109
|
// eslint-disable-next-line max-lines-per-function
|
|
90
110
|
export async function handleInitProject(params, server) {
|
|
91
111
|
// eslint-disable-next-line max-lines-per-function, complexity
|
|
@@ -256,8 +276,14 @@ export async function handleInitProject(params, server) {
|
|
|
256
276
|
const globalConfig = await globalStore.getGlobalConfig();
|
|
257
277
|
const effectiveLocale = locale ?? globalConfig.defaultLocale;
|
|
258
278
|
const effectiveExperience = experienceLevel ?? globalConfig.defaultExperienceLevel;
|
|
259
|
-
//
|
|
260
|
-
|
|
279
|
+
// SPEC-1343: analyzeProject (full tree scan) and detectStackPatterns are both
|
|
280
|
+
// independent read-only scans of the same project tree — run them concurrently
|
|
281
|
+
// instead of back-to-back so wall time is ~1x the slower stage, not the sum (AC1).
|
|
282
|
+
const [knowledge, stackResult] = await Promise.all([
|
|
283
|
+
analyzeProject(projectPath, projectId, effectiveLocale, effectiveExperience),
|
|
284
|
+
detectStackPatterns(projectPath),
|
|
285
|
+
]);
|
|
286
|
+
const { modernPatterns, dddConfig, agileDetection, orchestratorInfo, composeMultiplatform } = stackResult;
|
|
261
287
|
if (approvedTechnologyContract !== null) {
|
|
262
288
|
knowledge.language = approvedTechnologyContract.language ?? knowledge.language;
|
|
263
289
|
knowledge.framework =
|
|
@@ -330,9 +356,6 @@ export async function handleInitProject(params, server) {
|
|
|
330
356
|
if (hourlyRate !== undefined) {
|
|
331
357
|
await globalStore.updateGlobalConfig({ hourlyRate });
|
|
332
358
|
}
|
|
333
|
-
// SPEC-033: Detect modern architecture patterns and stack info
|
|
334
|
-
const stackResult = await detectStackPatterns(projectPath);
|
|
335
|
-
const { modernPatterns, dddConfig, agileDetection, orchestratorInfo, composeMultiplatform } = stackResult;
|
|
336
359
|
// SPEC-568: Smart tool group activation based on detected stack.
|
|
337
360
|
// Runs before saveKnowledge so the snapshot can be persisted in one write.
|
|
338
361
|
const groupProfileSnapshot = applyStackBasedGroupActivations(knowledge.stack);
|
|
@@ -352,16 +375,54 @@ export async function handleInitProject(params, server) {
|
|
|
352
375
|
addProject(projectPath).catch(() => {
|
|
353
376
|
/* best-effort — never block init_project */
|
|
354
377
|
});
|
|
378
|
+
// SPEC-1343: buildProjectConfig -> runSpecMigrations must stay ordered (config must be
|
|
379
|
+
// written before migrations run against it, and runSpecMigrations is invoked exactly
|
|
380
|
+
// once per run, AC3). That ordered chain is independent of the read-only discovery
|
|
381
|
+
// batch below, so both run concurrently (AC1/AC4).
|
|
355
382
|
// SPEC-131: Build legal profile and generate planu.json / What's New
|
|
356
383
|
// SPEC-313: Pass workMode to persist in planu.json
|
|
357
|
-
const
|
|
384
|
+
const configAndMigrations = (async () => {
|
|
385
|
+
const configResult = await buildProjectConfig(projectPath, projectId, knowledge, workMode);
|
|
386
|
+
// Run spec migrations (v0.30 -> v0.40, legacy paths, prefix normalization)
|
|
387
|
+
const migrations = await runSpecMigrations(projectPath, projectId, knowledge, {
|
|
388
|
+
repositoryMigration: 'initialization',
|
|
389
|
+
});
|
|
390
|
+
return { configResult, migrations };
|
|
391
|
+
})();
|
|
392
|
+
// SPEC-1343: Independent, best-effort discovery stages (skills fetch, privacy
|
|
393
|
+
// detection, health check, convention scan) — none of these feed each other, so they
|
|
394
|
+
// batch into a single Promise.all with per-stage error isolation. A failing optional
|
|
395
|
+
// stage falls back to a safe default instead of failing init_project (AC2).
|
|
396
|
+
const [recommendedSkillsOutcome, privacyConfigOutcome, healthReportOutcome, conventionScanOutcome,] = await Promise.all([
|
|
397
|
+
runOptionalStage('fetchRecommendedSkills', [], () => fetchRecommendedSkills(knowledge)),
|
|
398
|
+
runOptionalStage('detectProjectPrivacyConfig', buildDefaultPrivacyConfig('GDPR', []), () => detectProjectPrivacyConfig(projectPath)),
|
|
399
|
+
runOptionalStage('runHealthCheckWithBaseline', null, async () => {
|
|
400
|
+
const allSpecs = await listProjectSpecs(projectId);
|
|
401
|
+
const specSummaries = allSpecs.map((s) => ({
|
|
402
|
+
specId: s.id,
|
|
403
|
+
status: s.status,
|
|
404
|
+
gitBranch: s.gitBranch,
|
|
405
|
+
updatedAt: s.updatedAt,
|
|
406
|
+
}));
|
|
407
|
+
return runHealthCheckWithBaseline(projectPath, projectId, knowledge, specSummaries);
|
|
408
|
+
}),
|
|
409
|
+
runOptionalStage('runConventionScanSafe', 'failed', () => runConventionScanSafe(projectPath)),
|
|
410
|
+
]);
|
|
411
|
+
const recommendedSkills = recommendedSkillsOutcome.value;
|
|
412
|
+
const privacyConfig = privacyConfigOutcome.value;
|
|
413
|
+
const healthReport = healthReportOutcome.value;
|
|
414
|
+
const conventionScanStatus = conventionScanOutcome.value;
|
|
415
|
+
const optionalStageFailures = [
|
|
416
|
+
recommendedSkillsOutcome,
|
|
417
|
+
privacyConfigOutcome,
|
|
418
|
+
healthReportOutcome,
|
|
419
|
+
conventionScanOutcome,
|
|
420
|
+
]
|
|
421
|
+
.filter((outcome) => outcome.failed)
|
|
422
|
+
.map((outcome) => `${outcome.name}: ${outcome.error ?? 'unknown error'}`);
|
|
423
|
+
const { configResult, migrations } = await configAndMigrations;
|
|
358
424
|
const { planuConfigPath, planuConfigGenerated, whatsNew } = configResult;
|
|
359
|
-
|
|
360
|
-
const { discoveryResult, migrationResult, folderMigrationResult, criticalMigrationFailures, nonCriticalWarnings: nonCriticalMigrationWarnings, migrationReportPath, changedPaths: migrationChangedPaths = [], } = await runSpecMigrations(projectPath, projectId, knowledge, {
|
|
361
|
-
repositoryMigration: 'initialization',
|
|
362
|
-
});
|
|
363
|
-
// Auto-fetch recommended skills based on detected stack
|
|
364
|
-
const recommendedSkills = await fetchRecommendedSkills(knowledge);
|
|
425
|
+
const { discoveryResult, migrationResult, folderMigrationResult, criticalMigrationFailures, nonCriticalWarnings: nonCriticalMigrationWarnings, migrationReportPath, changedPaths: migrationChangedPaths = [], } = migrations;
|
|
365
426
|
// Auto-install or queue skills (opt-out via planu.json or input param, SPEC-185)
|
|
366
427
|
const autoInstallFromConfig = autoInstallSkills ?? (await readAutoInstallFlag(planuConfigPath));
|
|
367
428
|
const { skillsAutoInstalled, skillsPendingInstall, skillsSkipped } = await orchestrateSkillInstalls(recommendedSkills, projectPath, autoInstallFromConfig);
|
|
@@ -393,17 +454,6 @@ export async function handleInitProject(params, server) {
|
|
|
393
454
|
const beginnerEnvGuide = effectiveExperience === 'beginner' && envVars.length > 0
|
|
394
455
|
? buildBeginnerEnvGuide(envVars)
|
|
395
456
|
: null;
|
|
396
|
-
// SPEC-030: Detect legal framework and third-party data processors
|
|
397
|
-
const privacyConfig = await detectProjectPrivacyConfig(projectPath);
|
|
398
|
-
// SPEC-180: Project health check (best-effort)
|
|
399
|
-
const allSpecs = await listProjectSpecs(projectId);
|
|
400
|
-
const specSummaries = allSpecs.map((s) => ({
|
|
401
|
-
specId: s.id,
|
|
402
|
-
status: s.status,
|
|
403
|
-
gitBranch: s.gitBranch,
|
|
404
|
-
updatedAt: s.updatedAt,
|
|
405
|
-
}));
|
|
406
|
-
const healthReport = await runHealthCheckWithBaseline(projectPath, projectId, knowledge, specSummaries);
|
|
407
457
|
// SPEC-491: Auto-cleanup planu/ — remove legacy HTMLs, migrate to lean spec format (fire-and-forget)
|
|
408
458
|
import('../../engine/spec-migrator/planu-root-cleaner.js')
|
|
409
459
|
.then(({ cleanPlanuRoot }) => cleanPlanuRoot(join(projectPath, 'planu')))
|
|
@@ -414,8 +464,6 @@ export async function handleInitProject(params, server) {
|
|
|
414
464
|
void withAudit(projectPath, 'init_project', 'regeneratePages', () => regeneratePages(projectPath, ['knowledge', 'risks', 'decisions']), (pages) => ({ pages: pages.length })).catch(() => {
|
|
415
465
|
/* best-effort — never fail init */
|
|
416
466
|
});
|
|
417
|
-
// SPEC-228: Deep scan — detect conventions and write planu/conventions.json (best-effort)
|
|
418
|
-
const conventionScanStatus = await runConventionScanSafe(projectPath);
|
|
419
467
|
// SPEC-645: Detect LLM client and cache in conventions.json (fire-and-forget)
|
|
420
468
|
void import('../../engine/client-detection.js')
|
|
421
469
|
.then(({ detectAndCacheClient }) => detectAndCacheClient(projectPath))
|
|
@@ -480,6 +528,11 @@ export async function handleInitProject(params, server) {
|
|
|
480
528
|
}
|
|
481
529
|
// SPEC-469: Build autopilot summary from setup steps performed
|
|
482
530
|
const collector = new AutopilotSummaryCollector();
|
|
531
|
+
// SPEC-1343 AC2: surface optional discovery-stage failures in the result summary
|
|
532
|
+
// instead of failing init_project.
|
|
533
|
+
for (const failure of optionalStageFailures) {
|
|
534
|
+
collector.pushFail('discovery-stage-failed', failure);
|
|
535
|
+
}
|
|
483
536
|
if (knowledge.stack.length > 0) {
|
|
484
537
|
collector.pushOk('stack-detection', `Stack detected: ${knowledge.stack.join(', ')}`);
|
|
485
538
|
}
|
package/dist/tools/learn.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// to pattern-store for future use by estimation, auditing, and spec generation.
|
|
4
4
|
// SPEC-011: adds dispatchFeedbackEvent for feedback loop dispatch.
|
|
5
5
|
import { patternStore, knowledgeStore } from '../storage/index.js';
|
|
6
|
+
import { SemanticIndexStore } from '../storage/semantic-index-store.js';
|
|
6
7
|
import { t, ti } from '../i18n/index.js';
|
|
7
|
-
import { TFIDFEngine } from '../engine/vector-store/tfidf.js';
|
|
8
8
|
import { cosineSimilarity } from '../engine/vector-store/similarity.js';
|
|
9
9
|
import { compactJson } from './output-formatter.js';
|
|
10
10
|
/** Default cosine similarity threshold for semantic deduplication. */
|
|
@@ -45,7 +45,7 @@ export async function handleLearn(args) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
// 3. Semantic deduplication check (SPEC-075 AC-08)
|
|
48
|
-
const dedupWarning = checkSemanticDuplicate(existingPatterns, pattern, type);
|
|
48
|
+
const dedupWarning = await checkSemanticDuplicate(projectId, existingPatterns, pattern, type);
|
|
49
49
|
// 4. Create new pattern (store even if semantically similar — just warn)
|
|
50
50
|
const patternId = `PAT-${type.substring(0, 3).toUpperCase()}-${Date.now().toString(36).toUpperCase()}`;
|
|
51
51
|
const learnedPattern = {
|
|
@@ -93,18 +93,20 @@ export async function handleLearn(args) {
|
|
|
93
93
|
/**
|
|
94
94
|
* SPEC-075 AC-08: Check existing patterns for semantic similarity using TF-IDF.
|
|
95
95
|
* Returns a DedupWarning if a pattern with cosine similarity > threshold exists.
|
|
96
|
+
* SPEC-1345: the same-type corpus is loaded from a persisted, incrementally
|
|
97
|
+
* updated TF-IDF index instead of being rebuilt from scratch on every call.
|
|
96
98
|
*/
|
|
97
|
-
function checkSemanticDuplicate(existingPatterns, newPattern, patternType, threshold = DEFAULT_DEDUP_THRESHOLD) {
|
|
99
|
+
async function checkSemanticDuplicate(projectId, existingPatterns, newPattern, patternType, threshold = DEFAULT_DEDUP_THRESHOLD) {
|
|
98
100
|
// Filter to same-type patterns only
|
|
99
101
|
const sameType = existingPatterns.filter((p) => p.type === patternType);
|
|
100
102
|
if (sameType.length === 0) {
|
|
101
103
|
return undefined;
|
|
102
104
|
}
|
|
103
|
-
|
|
104
|
-
const tfidf =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
const indexStore = new SemanticIndexStore(projectId, `pattern-${patternType}`);
|
|
106
|
+
const tfidf = await indexStore.loadOrBuild(sameType.map((p) => ({ id: p.id, content: p.pattern })));
|
|
107
|
+
// Embed the candidate in-memory only (it isn't stored yet, so it never
|
|
108
|
+
// becomes part of the persisted corpus) so the IDF reflects it, matching
|
|
109
|
+
// the previous non-persisted behavior.
|
|
108
110
|
tfidf.addDocument(newPattern);
|
|
109
111
|
// Embed the new pattern
|
|
110
112
|
const newEmbedding = tfidf.embed(newPattern);
|
|
@@ -22,8 +22,6 @@ export async function handleRegistryLogin(args) {
|
|
|
22
22
|
const token = args.token;
|
|
23
23
|
const user = args.user;
|
|
24
24
|
const email = args.email;
|
|
25
|
-
const rawPlan = args.plan;
|
|
26
|
-
const plan = rawPlan ?? 'free';
|
|
27
25
|
const expiresAt = args.expiresAt;
|
|
28
26
|
if (!token || !user || !email) {
|
|
29
27
|
return {
|
|
@@ -37,7 +35,6 @@ export async function handleRegistryLogin(args) {
|
|
|
37
35
|
token: await storeSecretField('global/registry/pat', 'registry-pat', token),
|
|
38
36
|
user,
|
|
39
37
|
email,
|
|
40
|
-
plan,
|
|
41
38
|
expiresAt: expiresAt ?? new Date(Date.now() + TOKEN_EXPIRY_MS).toISOString(),
|
|
42
39
|
};
|
|
43
40
|
const credPath = credentialsPath();
|
|
@@ -45,12 +42,7 @@ export async function handleRegistryLogin(args) {
|
|
|
45
42
|
await chmod(resolveStorageLayout().secrets, 0o700);
|
|
46
43
|
await writeFile(credPath, JSON.stringify(credentials, null, 2), 'utf-8');
|
|
47
44
|
await chmod(credPath, 0o600);
|
|
48
|
-
return compactResult([
|
|
49
|
-
`Done: Logged in successfully`,
|
|
50
|
-
`**user**: ${user}`,
|
|
51
|
-
`**email**: ${email}`,
|
|
52
|
-
`**plan**: ${plan}`,
|
|
53
|
-
].join('\n'));
|
|
45
|
+
return compactResult([`Done: Logged in successfully`, `**user**: ${user}`, `**email**: ${email}`].join('\n'));
|
|
54
46
|
}
|
|
55
47
|
catch (error) {
|
|
56
48
|
/* v8 ignore start -- unexpected runtime errors */
|
|
@@ -100,7 +92,6 @@ export async function handleRegistryWhoami(_args) {
|
|
|
100
92
|
const info = {
|
|
101
93
|
user: cred.user,
|
|
102
94
|
email: cred.email,
|
|
103
|
-
plan: cred.plan,
|
|
104
95
|
expiresAt: cred.expiresAt,
|
|
105
96
|
expired: isExpired,
|
|
106
97
|
authenticated: !isExpired,
|
|
@@ -108,7 +99,6 @@ export async function handleRegistryWhoami(_args) {
|
|
|
108
99
|
return compactResult([
|
|
109
100
|
`**user**: ${info.user}`,
|
|
110
101
|
`**email**: ${info.email}`,
|
|
111
|
-
`**plan**: ${info.plan}`,
|
|
112
102
|
`**expiresAt**: ${info.expiresAt}`,
|
|
113
103
|
`**expired**: ${String(info.expired)}`,
|
|
114
104
|
`**authenticated**: ${String(info.authenticated)}`,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// tools/semantic-search-handler.ts — Handler for semantic_search tool.
|
|
2
2
|
// SPEC-075 AC-07: Generates TF-IDF embedding of query, searches backend.
|
|
3
3
|
import { getOrCreateBackend } from '../storage/vector-store/backend-factory.js';
|
|
4
|
-
import {
|
|
4
|
+
import { SemanticIndexStore } from '../storage/semantic-index-store.js';
|
|
5
5
|
/**
|
|
6
6
|
* Handle a semantic_search request.
|
|
7
7
|
* Embeds the query with TF-IDF, then searches the vector backend.
|
|
@@ -33,11 +33,10 @@ export async function handleSemanticSearch(args) {
|
|
|
33
33
|
],
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
36
|
+
// SPEC-1345: load the persisted TF-IDF index for this scope, incrementally
|
|
37
|
+
// reconciled against the current corpus (no full rebuild for unchanged docs).
|
|
38
|
+
const indexStore = new SemanticIndexStore(projectId, scope);
|
|
39
|
+
const tfidf = await indexStore.loadOrBuild(allDocs.map((doc) => ({ id: doc.id, content: doc.content })));
|
|
41
40
|
// Embed the query
|
|
42
41
|
const queryEmbedding = tfidf.embed(query);
|
|
43
42
|
// Search
|
|
@@ -79,18 +79,28 @@ function parseSpecNumber(id) {
|
|
|
79
79
|
const match = /SPEC-(\d+)/i.exec(id);
|
|
80
80
|
return match?.[1] !== undefined ? parseInt(match[1], 10) : 0;
|
|
81
81
|
}
|
|
82
|
+
function selectActiveSpec(specs) {
|
|
83
|
+
// Lifecycle-first ordering: implementing always outranks review, regardless of SPEC number.
|
|
84
|
+
// Within the same lifecycle status, the tie-break is descending SPEC number (most recent first).
|
|
85
|
+
for (const status of ['implementing', 'review']) {
|
|
86
|
+
const candidates = specs
|
|
87
|
+
.filter((s) => s.status === status)
|
|
88
|
+
.sort((a, b) => parseSpecNumber(b.id) - parseSpecNumber(a.id));
|
|
89
|
+
const candidate = candidates[0];
|
|
90
|
+
if (candidate !== undefined) {
|
|
91
|
+
return { id: candidate.id, title: candidate.title, status };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
82
96
|
async function getStatusSpecSnapshot(projectId) {
|
|
83
97
|
try {
|
|
84
98
|
const specs = await listSpecs(projectId);
|
|
85
|
-
const
|
|
86
|
-
.filter((s) => s.status === 'implementing' || s.status === 'review')
|
|
87
|
-
.sort((a, b) => parseSpecNumber(b.id) - parseSpecNumber(a.id));
|
|
99
|
+
const active = selectActiveSpec(specs.filter((s) => s.status === 'implementing' || s.status === 'review'));
|
|
88
100
|
const approved = specs
|
|
89
101
|
.filter((s) => s.status === 'approved')
|
|
90
102
|
.sort((a, b) => parseSpecNumber(a.id) - parseSpecNumber(b.id));
|
|
91
|
-
const activeItem = inProgress[0];
|
|
92
103
|
const nextItem = approved[0];
|
|
93
|
-
const active = activeItem !== undefined ? { id: activeItem.id, title: activeItem.title } : null;
|
|
94
104
|
const next = nextItem !== undefined ? { id: nextItem.id, title: nextItem.title } : null;
|
|
95
105
|
return { active, next, queueCount: approved.length };
|
|
96
106
|
}
|
|
@@ -130,6 +140,9 @@ async function getSLABreaches(projectId) {
|
|
|
130
140
|
// ---------------------------------------------------------------------------
|
|
131
141
|
function buildSuggestion(snapshot) {
|
|
132
142
|
if (snapshot.active !== null) {
|
|
143
|
+
if (snapshot.active.status === 'review') {
|
|
144
|
+
return `Resolve review for ${snapshot.active.id} — it is not yet implementing`;
|
|
145
|
+
}
|
|
133
146
|
return `Continue ${snapshot.active.id} — it may be blocking others`;
|
|
134
147
|
}
|
|
135
148
|
if (snapshot.next !== null) {
|
|
@@ -145,7 +158,7 @@ function buildOutput(params) {
|
|
|
145
158
|
const lines = ['Planu Status', '━━━━━━━━━━━━━━━'];
|
|
146
159
|
if (snapshot.active !== null) {
|
|
147
160
|
const title = snapshot.active.title.slice(0, 40);
|
|
148
|
-
lines.push(`ACTIVE ${snapshot.active.id} ${title} (
|
|
161
|
+
lines.push(`ACTIVE ${snapshot.active.id} ${title} (${snapshot.active.status})`);
|
|
149
162
|
}
|
|
150
163
|
else {
|
|
151
164
|
lines.push(`ACTIVE (none)`);
|
|
@@ -34,22 +34,20 @@ export async function handleSpecHealthCheck(input) {
|
|
|
34
34
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
import {
|
|
37
|
+
import { fastScanSpecsAsync } from '../engine/core-bridge.js';
|
|
38
38
|
export async function handleHealthCheckAll(input) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
};
|
|
52
|
-
}
|
|
39
|
+
const { value: briefs, degradationNotice } = await fastScanSpecsAsync(input.projectPath);
|
|
40
|
+
if (briefs) {
|
|
41
|
+
// Sort worst to best
|
|
42
|
+
briefs.sort((a, b) => a.healthScore - b.healthScore);
|
|
43
|
+
const lines = [
|
|
44
|
+
`Health check for all ${briefs.length} specs (sorted worst to best) [NATIVE]:`,
|
|
45
|
+
``,
|
|
46
|
+
...briefs.map((b) => ` ${b.id}: ${b.healthScore}/100`),
|
|
47
|
+
];
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
50
|
+
};
|
|
53
51
|
}
|
|
54
52
|
const projectId = hashProjectPath(input.projectPath);
|
|
55
53
|
const specs = await specStore.listSpecs(projectId);
|
|
@@ -66,6 +64,9 @@ export async function handleHealthCheckAll(input) {
|
|
|
66
64
|
``,
|
|
67
65
|
...scores.map((s) => ` ${s.specId}: ${s.total}/100`),
|
|
68
66
|
];
|
|
67
|
+
if (degradationNotice !== undefined) {
|
|
68
|
+
lines.push('', degradationNotice);
|
|
69
|
+
}
|
|
69
70
|
return {
|
|
70
71
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
71
72
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { InteractiveQuestion } from './interactive-question.js';
|
|
2
|
+
/** Outcome of resolving the English-only gate for create_spec's title/description. */
|
|
3
|
+
export type EnglishOnlyGateAction = 'ok' | 'ask' | 'translated' | 'reject';
|
|
4
|
+
/** Result of `resolveEnglishOnlySpecGate`. Shape depends on `action`. */
|
|
5
|
+
export interface EnglishOnlyGateResolution {
|
|
6
|
+
action: EnglishOnlyGateAction;
|
|
7
|
+
/** Set for 'ask' | 'translated' | 'reject' — the language detected in the original input. */
|
|
8
|
+
originalLanguage?: 'es' | 'pt';
|
|
9
|
+
/** Set for 'ask' — the translation clarification questions to relay via AskUserQuestion. */
|
|
10
|
+
questions?: InteractiveQuestion[];
|
|
11
|
+
/** Set for 'reject' — human-readable reason from the underlying language detector. */
|
|
12
|
+
reason?: string;
|
|
13
|
+
/** Set for 'translated' — the English title to use instead of the original. */
|
|
14
|
+
englishTitle?: string;
|
|
15
|
+
/** Set for 'translated' — the English description to use instead of the original. */
|
|
16
|
+
englishDescription?: string;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=spec-language-translation.d.ts.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// types/spec-language-translation.ts — SPEC-1342: translation-request shapes for the
|
|
2
|
+
// English-only spec gate. Lets create_spec offer an in-band translation clarification
|
|
3
|
+
// round instead of hard-rejecting non-English title/description input.
|
|
4
|
+
export {};
|
|
5
|
+
//# sourceMappingURL=spec-language-translation.js.map
|
package/dist/types/status.d.ts
CHANGED
|
@@ -10,10 +10,12 @@ export interface GitState {
|
|
|
10
10
|
export interface SessionState {
|
|
11
11
|
lastCiStatus?: string;
|
|
12
12
|
}
|
|
13
|
+
export type StatusActiveLifecycleStatus = 'implementing' | 'review';
|
|
13
14
|
export interface StatusSpecSnapshot {
|
|
14
15
|
active: {
|
|
15
16
|
id: string;
|
|
16
17
|
title: string;
|
|
18
|
+
status: StatusActiveLifecycleStatus;
|
|
17
19
|
} | null;
|
|
18
20
|
next: {
|
|
19
21
|
id: string;
|
|
@@ -21,6 +21,24 @@ export interface TFIDFConfig {
|
|
|
21
21
|
minDocFrequency: number;
|
|
22
22
|
stopWords: Set<string>;
|
|
23
23
|
}
|
|
24
|
+
/** Serializable TF-IDF engine state (SPEC-1345: persisted index). */
|
|
25
|
+
export interface TFIDFExportedState {
|
|
26
|
+
schemaVersion: number;
|
|
27
|
+
docFreq: [string, number][];
|
|
28
|
+
docCount: number;
|
|
29
|
+
}
|
|
30
|
+
/** A single corpus document as seen by the persisted semantic index (SPEC-1345). */
|
|
31
|
+
export interface SemanticIndexDoc {
|
|
32
|
+
id: string;
|
|
33
|
+
content: string;
|
|
34
|
+
}
|
|
35
|
+
/** On-disk shape of a persisted TF-IDF index + hash manifest (SPEC-1345). */
|
|
36
|
+
export interface PersistedSemanticIndex {
|
|
37
|
+
schemaVersion: number;
|
|
38
|
+
docHashes: Record<string, string>;
|
|
39
|
+
docTokens: Record<string, string[]>;
|
|
40
|
+
tfidfState: TFIDFExportedState;
|
|
41
|
+
}
|
|
24
42
|
/** Configuration for the HNSW index. */
|
|
25
43
|
export interface HNSWConfig {
|
|
26
44
|
M: number;
|
package/dist/types/workers.d.ts
CHANGED
|
@@ -8,7 +8,6 @@ export interface WorkerTrigger {
|
|
|
8
8
|
/** Interval in ms for schedule triggers */
|
|
9
9
|
intervalMs?: number;
|
|
10
10
|
}
|
|
11
|
-
export type WorkerTier = 'free' | 'pro';
|
|
12
11
|
export interface WorkerDefinition {
|
|
13
12
|
name: string;
|
|
14
13
|
description: string;
|
|
@@ -19,7 +18,6 @@ export interface WorkerDefinition {
|
|
|
19
18
|
/** Minimum ms between executions */
|
|
20
19
|
cooldownMs: number;
|
|
21
20
|
enabled: boolean;
|
|
22
|
-
tier: WorkerTier;
|
|
23
21
|
}
|
|
24
22
|
export type WorkerFindingSeverity = 'critical' | 'high' | 'medium' | 'low' | 'warning' | 'info';
|
|
25
23
|
export interface WorkerFinding {
|
|
@@ -66,7 +64,6 @@ export interface WorkerOverride {
|
|
|
66
64
|
enabled?: boolean;
|
|
67
65
|
cooldownMs?: number;
|
|
68
66
|
priority?: number;
|
|
69
|
-
tier?: WorkerTier;
|
|
70
67
|
}
|
|
71
68
|
export interface WorkerProjectConfig {
|
|
72
69
|
overrides: Record<string, WorkerConfigOverride>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
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",
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
"packageName": "@planu/core"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|
|
38
|
-
"@planu/core-darwin-arm64": "5.
|
|
39
|
-
"@planu/core-darwin-x64": "5.
|
|
40
|
-
"@planu/core-linux-arm64-gnu": "5.
|
|
41
|
-
"@planu/core-linux-arm64-musl": "5.
|
|
42
|
-
"@planu/core-linux-x64-gnu": "5.
|
|
43
|
-
"@planu/core-linux-x64-musl": "5.
|
|
44
|
-
"@planu/core-win32-arm64-msvc": "5.
|
|
45
|
-
"@planu/core-win32-x64-msvc": "5.
|
|
38
|
+
"@planu/core-darwin-arm64": "5.2.0",
|
|
39
|
+
"@planu/core-darwin-x64": "5.2.0",
|
|
40
|
+
"@planu/core-linux-arm64-gnu": "5.2.0",
|
|
41
|
+
"@planu/core-linux-arm64-musl": "5.2.0",
|
|
42
|
+
"@planu/core-linux-x64-gnu": "5.2.0",
|
|
43
|
+
"@planu/core-linux-x64-musl": "5.2.0",
|
|
44
|
+
"@planu/core-win32-arm64-msvc": "5.2.0",
|
|
45
|
+
"@planu/core-win32-x64-msvc": "5.2.0"
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|
|
48
48
|
"node": ">=24.0.0"
|
package/planu-native.json
CHANGED
|
@@ -1,26 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dev.planu.native",
|
|
3
3
|
"displayName": "Planu Native Lightweight Surface",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.2.0",
|
|
5
5
|
"packageName": "@planu/cli",
|
|
6
6
|
"modes": {
|
|
7
7
|
"lightweight": {
|
|
8
8
|
"requiresMcp": false,
|
|
9
9
|
"requiresDaemon": false,
|
|
10
|
-
"hosts": [
|
|
11
|
-
"codex",
|
|
12
|
-
"claude-code"
|
|
13
|
-
],
|
|
10
|
+
"hosts": ["codex", "claude-code"],
|
|
14
11
|
"commands": [
|
|
15
12
|
{
|
|
16
13
|
"id": "planu.status",
|
|
17
14
|
"title": "Project status",
|
|
18
15
|
"description": "Show the compact Planu project snapshot without loading the MCP tool graph.",
|
|
19
16
|
"invocation": "planu status",
|
|
20
|
-
"hosts": [
|
|
21
|
-
"codex",
|
|
22
|
-
"claude-code"
|
|
23
|
-
],
|
|
17
|
+
"hosts": ["codex", "claude-code"],
|
|
24
18
|
"requiresMcp": false,
|
|
25
19
|
"requiresDaemon": false,
|
|
26
20
|
"mapsTo": "handlePlanStatus"
|
|
@@ -30,10 +24,7 @@
|
|
|
30
24
|
"title": "Create spec",
|
|
31
25
|
"description": "Create a new spec through the CLI-backed SDD contract.",
|
|
32
26
|
"invocation": "planu spec create \"<title>\"",
|
|
33
|
-
"hosts": [
|
|
34
|
-
"codex",
|
|
35
|
-
"claude-code"
|
|
36
|
-
],
|
|
27
|
+
"hosts": ["codex", "claude-code"],
|
|
37
28
|
"requiresMcp": false,
|
|
38
29
|
"requiresDaemon": false,
|
|
39
30
|
"mapsTo": "handleCreateSpec"
|
|
@@ -43,10 +34,7 @@
|
|
|
43
34
|
"title": "List specs",
|
|
44
35
|
"description": "List specs in the current project with optional status/type filters.",
|
|
45
36
|
"invocation": "planu spec list",
|
|
46
|
-
"hosts": [
|
|
47
|
-
"codex",
|
|
48
|
-
"claude-code"
|
|
49
|
-
],
|
|
37
|
+
"hosts": ["codex", "claude-code"],
|
|
50
38
|
"requiresMcp": false,
|
|
51
39
|
"requiresDaemon": false,
|
|
52
40
|
"mapsTo": "handleListSpecs"
|
|
@@ -56,10 +44,7 @@
|
|
|
56
44
|
"title": "Validate spec",
|
|
57
45
|
"description": "Validate a spec against the current codebase from the native CLI surface.",
|
|
58
46
|
"invocation": "planu spec validate SPEC-001",
|
|
59
|
-
"hosts": [
|
|
60
|
-
"codex",
|
|
61
|
-
"claude-code"
|
|
62
|
-
],
|
|
47
|
+
"hosts": ["codex", "claude-code"],
|
|
63
48
|
"requiresMcp": false,
|
|
64
49
|
"requiresDaemon": false,
|
|
65
50
|
"mapsTo": "handleValidate"
|
|
@@ -69,10 +54,7 @@
|
|
|
69
54
|
"title": "Audit technical debt",
|
|
70
55
|
"description": "Run the read-only project audit path for lightweight debt checks.",
|
|
71
56
|
"invocation": "planu audit debt",
|
|
72
|
-
"hosts": [
|
|
73
|
-
"codex",
|
|
74
|
-
"claude-code"
|
|
75
|
-
],
|
|
57
|
+
"hosts": ["codex", "claude-code"],
|
|
76
58
|
"requiresMcp": false,
|
|
77
59
|
"requiresDaemon": false,
|
|
78
60
|
"mapsTo": "handleAudit"
|
|
@@ -82,10 +64,7 @@
|
|
|
82
64
|
"title": "Check release readiness",
|
|
83
65
|
"description": "Check local-first release readiness, branch cleanliness, and optional gitflow drift.",
|
|
84
66
|
"invocation": "planu release check",
|
|
85
|
-
"hosts": [
|
|
86
|
-
"codex",
|
|
87
|
-
"claude-code"
|
|
88
|
-
],
|
|
67
|
+
"hosts": ["codex", "claude-code"],
|
|
89
68
|
"requiresMcp": false,
|
|
90
69
|
"requiresDaemon": false,
|
|
91
70
|
"mapsTo": "releaseCommand"
|
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.
|
|
5
|
+
"version": "5.2.0",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": ["npx", "@planu/cli@latest"],
|
|
8
8
|
"packageName": "@planu/cli",
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { StalenessInfo } from '../../types/security/index.js';
|
|
2
|
-
export type { StalenessInfo };
|
|
3
|
-
export declare const CVE_DB_PATH: string;
|
|
4
|
-
/** Check if known-cves.json is older than 7 days. */
|
|
5
|
-
export declare function isCveDatabaseStale(): Promise<StalenessInfo>;
|
|
6
|
-
/**
|
|
7
|
-
* Refresh known-cves.json from OSV.dev for the given ecosystems.
|
|
8
|
-
* Queries each package already in the database to get up-to-date CVE data.
|
|
9
|
-
* Writes updated JSON via atomicWriteFile. Records refresh in cve-refresh-log.json.
|
|
10
|
-
*/
|
|
11
|
-
export declare function refreshCveDatabase(ecosystems: string[]): Promise<void>;
|
|
12
|
-
//# sourceMappingURL=cve-refresher.d.ts.map
|