@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/cli/commands/doctor.d.ts +22 -0
  3. package/dist/cli/commands/doctor.js +176 -2
  4. package/dist/engine/autopilot/bootstrap.js +27 -0
  5. package/dist/engine/core-bridge.d.ts +28 -0
  6. package/dist/engine/core-bridge.js +67 -0
  7. package/dist/engine/drift-monitor.js +16 -18
  8. package/dist/engine/living-spec/hash-tracker.js +26 -28
  9. package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
  10. package/dist/engine/planu-core.darwin-arm64.node.sbom.json +4 -4
  11. package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
  12. package/dist/engine/planu-core.darwin-x64.node.sbom.json +4 -4
  13. package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
  14. package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +4 -4
  15. package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
  16. package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +4 -4
  17. package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
  18. package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +4 -4
  19. package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
  20. package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +4 -4
  21. package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
  22. package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +4 -4
  23. package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
  24. package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +4 -4
  25. package/dist/engine/spec-language/english-only.d.ts +14 -0
  26. package/dist/engine/spec-language/english-only.js +58 -0
  27. package/dist/engine/spec-migrator/criteria-scanner.js +11 -13
  28. package/dist/engine/spec-migrator/drift-detector.js +10 -12
  29. package/dist/engine/vector-store/tfidf.d.ts +13 -9
  30. package/dist/engine/vector-store/tfidf.js +26 -0
  31. package/dist/engine/worker-config-loader.d.ts +1 -1
  32. package/dist/engine/worker-config-loader.js +1 -11
  33. package/dist/engine/workers/schema.d.ts +0 -8
  34. package/dist/engine/workers/schema.js +0 -1
  35. package/dist/i18n/index.d.ts +18 -0
  36. package/dist/i18n/index.js +40 -1
  37. package/dist/storage/global-store.d.ts +9 -0
  38. package/dist/storage/global-store.js +23 -0
  39. package/dist/storage/semantic-index-store.d.ts +23 -0
  40. package/dist/storage/semantic-index-store.js +105 -0
  41. package/dist/storage/status-store/self-healing.js +15 -17
  42. package/dist/tools/challenge-spec/scenarios-utils.js +5 -1
  43. package/dist/tools/create-spec.js +60 -15
  44. package/dist/tools/init-project/handler.js +78 -25
  45. package/dist/tools/learn.js +10 -8
  46. package/dist/tools/registry/auth.js +1 -11
  47. package/dist/tools/semantic-search-handler.js +5 -6
  48. package/dist/tools/status-handler.js +19 -6
  49. package/dist/tools/validation-loop-handler.js +16 -15
  50. package/dist/types/spec-language-translation.d.ts +18 -0
  51. package/dist/types/spec-language-translation.js +5 -0
  52. package/dist/types/spec-registry.d.ts +0 -2
  53. package/dist/types/status.d.ts +2 -0
  54. package/dist/types/vector-store.d.ts +18 -0
  55. package/dist/types/workers.d.ts +0 -3
  56. package/package.json +9 -9
  57. package/planu-native.json +8 -29
  58. package/planu-plugin.json +1 -1
  59. package/dist/engine/security/cve-refresher.d.ts +0 -12
  60. 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
- // Run full project analysis
260
- const knowledge = await analyzeProject(projectPath, projectId, effectiveLocale, effectiveExperience);
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 configResult = await buildProjectConfig(projectPath, projectId, knowledge, workMode);
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
- // Run spec migrations (v0.30 -> v0.40, legacy paths, prefix normalization)
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
  }
@@ -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
- // Build TF-IDF engine from existing patterns + the new one
104
- const tfidf = new TFIDFEngine();
105
- for (const p of sameType) {
106
- tfidf.addDocument(p.pattern);
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 { TFIDFEngine } from '../engine/vector-store/tfidf.js';
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
- // Build TF-IDF from existing corpus
37
- const tfidf = new TFIDFEngine();
38
- for (const doc of allDocs) {
39
- tfidf.addDocument(doc.content);
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 inProgress = specs
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} (in-progress)`);
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 { isNativeActive, fastScanSpecs } from '../engine/core-bridge.js';
37
+ import { fastScanSpecsAsync } from '../engine/core-bridge.js';
38
38
  export async function handleHealthCheckAll(input) {
39
- if (isNativeActive()) {
40
- const briefs = fastScanSpecs(input.projectPath);
41
- if (briefs) {
42
- // Sort worst to best
43
- briefs.sort((a, b) => a.healthScore - b.healthScore);
44
- const lines = [
45
- `Health check for all ${briefs.length} specs (sorted worst to best) [NATIVE]:`,
46
- ``,
47
- ...briefs.map((b) => ` ${b.id}: ${b.healthScore}/100`),
48
- ];
49
- return {
50
- content: [{ type: 'text', text: lines.join('\n') }],
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
@@ -87,8 +87,6 @@ export interface RegistryCredentials {
87
87
  user: string;
88
88
  /** Email associated with the account. */
89
89
  email: string;
90
- /** License plan level. */
91
- plan: 'free' | 'pro';
92
90
  /** ISO 8601 token expiration timestamp. */
93
91
  expiresAt: string;
94
92
  }
@@ -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;
@@ -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.1.1",
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.1.1",
39
- "@planu/core-darwin-x64": "5.1.1",
40
- "@planu/core-linux-arm64-gnu": "5.1.1",
41
- "@planu/core-linux-arm64-musl": "5.1.1",
42
- "@planu/core-linux-x64-gnu": "5.1.1",
43
- "@planu/core-linux-x64-musl": "5.1.1",
44
- "@planu/core-win32-arm64-msvc": "5.1.1",
45
- "@planu/core-win32-x64-msvc": "5.1.1"
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.1.1",
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.1.1",
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