@planu/cli 4.1.3 → 4.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 (32) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/dist/engine/code-scanner/layer-scanner.js +29 -116
  3. package/dist/engine/core-bridge.d.ts +6 -6
  4. package/dist/engine/crash-shield/index.js +26 -63
  5. package/dist/engine/detect-duplication.js +63 -53
  6. package/dist/engine/drift-monitor.js +1 -1
  7. package/dist/engine/onboarding/new-project-resolver.d.ts +7 -0
  8. package/dist/engine/onboarding/new-project-resolver.js +265 -0
  9. package/dist/engine/reviewer-tokens/signer.js +4 -22
  10. package/dist/engine/scan-project/module-discoverer.js +9 -11
  11. package/dist/engine/spec-migrator/strict-planu-cleanup.js +2 -1
  12. package/dist/engine/validator/analyzer.js +14 -11
  13. package/dist/engine/validator/deep-code-checker.js +4 -8
  14. package/dist/storage/base-store.js +2 -6
  15. package/dist/storage/gaps-log.js +38 -32
  16. package/dist/storage/index.d.ts +1 -0
  17. package/dist/storage/index.js +1 -0
  18. package/dist/storage/technology-selection-store.d.ts +5 -0
  19. package/dist/storage/technology-selection-store.js +42 -0
  20. package/dist/tools/create-spec.js +23 -3
  21. package/dist/tools/facilitate.js +39 -29
  22. package/dist/tools/init-project/handler.js +59 -12
  23. package/dist/tools/register-spec-tools/core-spec-tools.js +33 -1
  24. package/dist/tools/tool-registry/core-tools.js +35 -1
  25. package/dist/tools/update-status/batch.js +4 -1
  26. package/dist/types/facilitator.d.ts +13 -0
  27. package/dist/types/index.d.ts +1 -0
  28. package/dist/types/index.js +1 -0
  29. package/dist/types/project/inputs.d.ts +12 -0
  30. package/dist/types/technology-selection.d.ts +77 -0
  31. package/dist/types/technology-selection.js +3 -0
  32. package/package.json +22 -8
@@ -2,6 +2,7 @@ import { checkGate } from '../engine/clarification-gate/gate.js';
2
2
  import { upsertToken, hashQuestions } from '../engine/clarification-gate/token-store.js';
3
3
  import { ti } from '../i18n/index.js';
4
4
  import { knowledgeStore, specStore } from '../storage/index.js';
5
+ import { readTechnologySelectionContract } from '../storage/technology-selection-store.js';
5
6
  import { formatSuccess, addNextSteps, toolResult, interactiveResult } from './response-helpers.js';
6
7
  import { writeFile, mkdir, rm, readFile, stat } from 'node:fs/promises';
7
8
  import { createHash } from 'node:crypto';
@@ -515,17 +516,36 @@ export async function handleCreateSpec(inputParams, server) {
515
516
  await measureStep('mkdir-specDir', () => mkdir(specDir, { recursive: true }));
516
517
  // SPEC-586: Filter suggested criteria by spec tags/target/scope before injection
517
518
  const filteredCriteria = await filterCriteriaByTags(autopilot.suggestedCriteria, spec.tags, spec.target, spec.scope).catch(() => autopilot.suggestedCriteria);
519
+ const technologyContract = await readTechnologySelectionContract(params.projectPath ?? '');
520
+ const contractNote = technologyContract
521
+ ? [
522
+ '',
523
+ 'Technology Contract:',
524
+ `- Mode: ${technologyContract.mode}`,
525
+ technologyContract.language ? `- Language: ${technologyContract.language}` : '',
526
+ technologyContract.framework
527
+ ? `- Framework: ${technologyContract.framework}`
528
+ : '- Framework: none or not selected',
529
+ technologyContract.platform ? `- Platform: ${technologyContract.platform}` : '',
530
+ technologyContract.projectType
531
+ ? `- Project type: ${technologyContract.projectType}`
532
+ : '',
533
+ '- Agents must not choose a different stack unless the user approves a new contract.',
534
+ ]
535
+ .filter(Boolean)
536
+ .join('\n')
537
+ : '';
518
538
  const specGenerator = new FallbackGenerator();
519
539
  const generatedSpec = await measureStep('generateSpecBody', () => specGenerator.generate({
520
540
  title: spec.title,
521
- description,
541
+ description: `${description}${contractNote}`,
522
542
  type: spec.type,
523
543
  scope: spec.scope,
524
544
  target: spec.target,
525
545
  acFormat: params.acFormat,
526
546
  projectContext: {
527
- language: knowledge?.language ?? undefined,
528
- framework: knowledge?.framework ?? undefined,
547
+ language: technologyContract?.language ?? knowledge?.language ?? undefined,
548
+ framework: technologyContract?.framework ?? knowledge?.framework ?? undefined,
529
549
  architecture: knowledge?.architecture.primary ?? undefined,
530
550
  },
531
551
  }));
@@ -7,6 +7,8 @@ import { loadContext } from '../engine/facilitate/context-loader.js';
7
7
  import { route } from '../engine/facilitate/router.js';
8
8
  import { parsePlanContent } from '../engine/facilitate/plan-parser.js';
9
9
  import { compactJson, compactResult, compactError } from './output-formatter.js';
10
+ import { interactiveResult } from './response-helpers.js';
11
+ import { resolveNewProjectOnboarding } from '../engine/onboarding/new-project-resolver.js';
10
12
  // ---------------------------------------------------------------------------
11
13
  // Scenario detection (SPEC-037 legacy — kept for backward compatibility)
12
14
  // ---------------------------------------------------------------------------
@@ -31,29 +33,6 @@ function detectScenario(description) {
31
33
  return 'new_project';
32
34
  }
33
35
  // ---------------------------------------------------------------------------
34
- // Step questions for new_project scenario
35
- // ---------------------------------------------------------------------------
36
- const NEW_PROJECT_STEPS = [
37
- 'What is the main purpose of your app? What problem does it solve and for whom?',
38
- 'What platform will it run on? (web app, mobile iOS/Android, REST API, desktop app, CLI tool)',
39
- 'Do you have a preferred tech stack? (language, framework, database — skip if unsure)',
40
- ];
41
- function buildNewProjectResponse(step, collectedInfo) {
42
- if (step < NEW_PROJECT_STEPS.length) {
43
- const question = NEW_PROJECT_STEPS[step] ?? NEW_PROJECT_STEPS[0];
44
- const intro = step === 0
45
- ? "Welcome to Planu! I'll guide you with a few questions to generate a precise spec.\n\n"
46
- : `Got it! ${Object.values(collectedInfo).join(', ')}\n\n`;
47
- return `${intro}Step ${step + 1}/3: ${question}`;
48
- }
49
- const info = Object.values(collectedInfo).join(' | ');
50
- return (`Great — I have enough context to generate your spec.\n\n` +
51
- `Summary: ${info}\n\n` +
52
- `Next step: Call \`create_spec\` with this description:\n` +
53
- `"${Object.values(collectedInfo).join('. ')}."\n\n` +
54
- `Or call \`clarify_requirements\` first for deeper requirement analysis.`);
55
- }
56
- // ---------------------------------------------------------------------------
57
36
  // Existing project scenario
58
37
  // ---------------------------------------------------------------------------
59
38
  function buildExistingProjectResponse(description) {
@@ -206,7 +185,7 @@ async function runSmartOrchestrator(description, projectId, planContent) {
206
185
  // Main handler
207
186
  // ---------------------------------------------------------------------------
208
187
  export async function handleFacilitate(params, server) {
209
- const { description, projectId, sessionId, scenario: explicitScenario, collectedInfo = {}, step = 0, targetTool, checkFocus = false, changeFocus, paradigmReport, planContent, } = params;
188
+ const { description, projectId, sessionId, scenario: explicitScenario, step = 0, targetTool, checkFocus = false, changeFocus, paradigmReport, planContent, } = params;
210
189
  const sessionKey = sessionId ?? projectId ?? 'default';
211
190
  // SPEC-264: When planContent provided OR no explicit scenario/control params given,
212
191
  // use the smart orchestrator path.
@@ -355,11 +334,42 @@ export async function handleFacilitate(params, server) {
355
334
  let guidance;
356
335
  switch (scenario) {
357
336
  case 'new_project': {
358
- const updatedInfo = step === 0
359
- ? { ...collectedInfo, step0: description }
360
- : { ...collectedInfo, [`step${step}`]: description };
361
- guidance = buildNewProjectResponse(step, updatedInfo);
362
- break;
337
+ const resolution = await resolveNewProjectOnboarding({
338
+ description,
339
+ parentWorkspacePath: params.parentWorkspacePath,
340
+ projectPath: params.projectId,
341
+ appName: params.appName,
342
+ appSlug: params.appSlug,
343
+ projectType: params.projectType,
344
+ platform: params.platform,
345
+ language: params.language,
346
+ framework: params.framework,
347
+ database: params.database,
348
+ createDirectory: params.createDirectory,
349
+ clarificationAnswers: params.clarificationAnswers,
350
+ });
351
+ if (!resolution.ready) {
352
+ const response = interactiveResult(resolution.questions, 'New project onboarding needs a target app folder and technology choices before Planu can initialize anything. Do not call init_project on the parent workspace yet.', 'universal');
353
+ response.structuredContent = {
354
+ ...response.structuredContent,
355
+ mode: 'new_project',
356
+ parentWorkspacePath: resolution.parentWorkspacePath,
357
+ resolvedProjectPath: resolution.resolvedProjectPath,
358
+ appSlug: resolution.appSlug,
359
+ errors: resolution.errors,
360
+ nextAction: 'Ask the user these questions, then call facilitate again with clarificationAnswers or explicit fields. Only call init_project with the resolved child projectPath.',
361
+ };
362
+ return response;
363
+ }
364
+ return ok({
365
+ action: 'facilitate',
366
+ scenario,
367
+ mode: 'new_project',
368
+ readyToInitialize: true,
369
+ resolvedProjectPath: resolution.resolvedProjectPath,
370
+ technologySelectionContract: resolution.contract,
371
+ nextAction: 'Call init_project with mode="new_project", projectPath set to resolvedProjectPath, createDirectory=true, and technologySelectionContract from this response.',
372
+ });
363
373
  }
364
374
  case 'existing_project':
365
375
  guidance = buildExistingProjectResponse(description);
@@ -1,7 +1,7 @@
1
1
  import { elicitOrFallback, questionsToFormSchema } from '../../engine/elicitation/elicit-helper.js';
2
2
  import { ti } from '../../i18n/index.js';
3
3
  import { AutopilotSummaryCollector } from '../../engine/autopilot/summary-collector.js';
4
- import { hashProjectPath, knowledgeStore, globalStore, licenseStore } from '../../storage/index.js';
4
+ import { hashProjectPath, knowledgeStore, globalStore, licenseStore, technologySelectionStore, } from '../../storage/index.js';
5
5
  import { addProject } from '../../storage/global-projects-store.js';
6
6
  import { getCurrentTier } from '../license-gate.js';
7
7
  import { checkLimits } from '../../engine/license-validator.js';
@@ -24,7 +24,7 @@ import { readAutoInstallFlag, orchestrateSkillInstalls, runHealthCheckWithBaseli
24
24
  import { injectProactiveRules } from '../../engine/claude-md-injector/index.js';
25
25
  import { discoverSkillsForInit } from '../../engine/skill-bootstrap/registry-fetcher.js';
26
26
  import { join } from 'node:path';
27
- import { stat } from 'node:fs/promises';
27
+ import { mkdir, stat } from 'node:fs/promises';
28
28
  import { generateAgentTeamsRulesIfMissing, generateWorkflowRulesIfMissing, } from './rules-generator.js';
29
29
  import { installUniversalRules } from '../../engine/universal-rules/installer.js';
30
30
  import { detectHost } from '../../engine/host-detection/detect-host.js';
@@ -37,6 +37,8 @@ import { applyStackBasedGroupActivations } from './tool-group-activator.js';
37
37
  import { checkGate } from '../../engine/clarification-gate/gate.js';
38
38
  import { upsertToken, hashQuestions } from '../../engine/clarification-gate/token-store.js';
39
39
  import { reconcileInteractiveQuestionHooks } from '../reconcile-interactive-question-hooks.js';
40
+ import { resolveNewProjectOnboarding } from '../../engine/onboarding/new-project-resolver.js';
41
+ import { interactiveResult } from '../response-helpers.js';
40
42
  /** Frontend framework groups — mutually exclusive. Two or more detected → multi-stack conflict. */
41
43
  const FRONTEND_FRAMEWORK_GROUPS = [
42
44
  ['react', 'next', 'next.js', 'nextjs', 'remix', 'preact'],
@@ -81,8 +83,29 @@ export async function handleInitProject(params, server) {
81
83
  }
82
84
  // eslint-disable-next-line max-lines-per-function, complexity
83
85
  return trackCost(params.projectPath, 'init_project', async () => {
84
- const { projectPath, locale, hourlyRate, experienceLevel, userProfile, autoInstallSkills, workMode, } = params;
86
+ const { projectPath, mode, locale, hourlyRate, experienceLevel, userProfile, autoInstallSkills, workMode, } = params;
85
87
  try {
88
+ let approvedTechnologyContract = params.technologySelectionContract ?? null;
89
+ if (mode === 'new_project' && approvedTechnologyContract === null) {
90
+ const resolution = await resolveNewProjectOnboarding({
91
+ description: params.appName ?? params.projectType ?? 'New project',
92
+ parentWorkspacePath: params.parentWorkspacePath,
93
+ projectPath,
94
+ appName: params.appName,
95
+ appSlug: params.appSlug,
96
+ projectType: params.projectType,
97
+ platform: params.platform,
98
+ language: params.language,
99
+ framework: params.framework,
100
+ database: params.database,
101
+ createDirectory: params.createDirectory,
102
+ clarificationAnswers: params.clarificationAnswers,
103
+ });
104
+ if (!resolution.ready) {
105
+ return interactiveResult(resolution.questions, 'init_project new_project mode needs a confirmed child app folder and technology choices before writing project files.', 'universal');
106
+ }
107
+ approvedTechnologyContract = resolution.contract ?? null;
108
+ }
86
109
  // Validate projectPath exists and is a directory before any I/O
87
110
  try {
88
111
  const pathStat = await stat(projectPath);
@@ -99,15 +122,20 @@ export async function handleInitProject(params, server) {
99
122
  }
100
123
  }
101
124
  catch {
102
- return {
103
- content: [
104
- {
105
- type: 'text',
106
- text: `Project directory does not exist: "${projectPath}". Create the directory first and try again.`,
107
- },
108
- ],
109
- isError: true,
110
- };
125
+ if (mode === 'new_project' && params.createDirectory === true) {
126
+ await mkdir(projectPath, { recursive: true });
127
+ }
128
+ else {
129
+ return {
130
+ content: [
131
+ {
132
+ type: 'text',
133
+ text: `Project directory does not exist: "${projectPath}". Create the directory first and try again.`,
134
+ },
135
+ ],
136
+ isError: true,
137
+ };
138
+ }
111
139
  }
112
140
  const projectId = hashProjectPath(projectPath);
113
141
  // SPEC-1007: migrate legacy <cwd>/data/projects/{id} into ~/.planu/data/
@@ -147,6 +175,19 @@ export async function handleInitProject(params, server) {
147
175
  const effectiveExperience = experienceLevel ?? existing?.experienceLevel ?? globalConfig.defaultExperienceLevel;
148
176
  // Run full project analysis
149
177
  const knowledge = await analyzeProject(projectPath, projectId, effectiveLocale, effectiveExperience);
178
+ if (approvedTechnologyContract !== null) {
179
+ knowledge.language = approvedTechnologyContract.language ?? knowledge.language;
180
+ knowledge.framework =
181
+ approvedTechnologyContract.framework !== undefined
182
+ ? approvedTechnologyContract.framework
183
+ : knowledge.framework;
184
+ const stackAdditions = [
185
+ approvedTechnologyContract.language,
186
+ approvedTechnologyContract.framework ?? undefined,
187
+ approvedTechnologyContract.database ?? undefined,
188
+ ].filter((item) => typeof item === 'string' && item.length > 0);
189
+ knowledge.stack = Array.from(new Set([...knowledge.stack, ...stackAdditions]));
190
+ }
150
191
  // Apply overrides
151
192
  if (userProfile) {
152
193
  knowledge.userProfile = userProfile;
@@ -214,6 +255,12 @@ export async function handleInitProject(params, server) {
214
255
  }
215
256
  // Save initial project knowledge
216
257
  await knowledgeStore.saveKnowledge(projectId, knowledge);
258
+ if (approvedTechnologyContract !== null) {
259
+ await technologySelectionStore.writeTechnologySelectionContract(projectPath, {
260
+ ...approvedTechnologyContract,
261
+ projectPath,
262
+ });
263
+ }
217
264
  // SPEC-789: Register project in global workspace registry so workspace_health
218
265
  // and list_registered_projects can discover it without manual registration.
219
266
  addProject(projectPath).catch(() => {
@@ -18,6 +18,34 @@ import { handleTypeSafetyGate } from '../type-safety-gate.js';
18
18
  /** init_project inputSchema — extracted to keep registerCoreSpecTools within line budget. */
19
19
  const INIT_PROJECT_INPUT_SCHEMA = {
20
20
  projectPath: z.string().max(4096).describe('Absolute path to the project root'),
21
+ mode: z
22
+ .enum(['existing_project', 'new_project'])
23
+ .optional()
24
+ .describe('Onboarding mode. Defaults to existing_project for backwards compatibility.'),
25
+ parentWorkspacePath: z
26
+ .string()
27
+ .max(4096)
28
+ .optional()
29
+ .describe('Parent workspace path for explicit new_project onboarding'),
30
+ appName: z.string().max(500).optional().describe('Human-readable app name for new_project'),
31
+ appSlug: z
32
+ .string()
33
+ .max(200)
34
+ .optional()
35
+ .describe('Safe folder slug for new_project; must not contain path separators'),
36
+ projectType: z.string().max(500).optional().describe('Type of project being initialized'),
37
+ platform: z.string().max(500).optional().describe('Runtime/deployment platform'),
38
+ language: z.string().max(500).optional().describe('User-confirmed language'),
39
+ framework: z.string().max(500).nullable().optional().describe('User-confirmed framework'),
40
+ database: z.string().max(500).nullable().optional().describe('User-confirmed database'),
41
+ createDirectory: z
42
+ .boolean()
43
+ .optional()
44
+ .describe('When true with mode=new_project, create only the resolved projectPath directory'),
45
+ technologySelectionContract: z
46
+ .record(z.string(), z.unknown())
47
+ .optional()
48
+ .describe('Approved TechnologySelectionContract returned by facilitate new_project flow'),
21
49
  locale: SupportedLocaleEnum.optional().describe('Preferred locale for this project'),
22
50
  hourlyRate: z.number().optional().describe('Developer hourly rate in USD for cost estimation'),
23
51
  experienceLevel: ExperienceLevelEnum.optional().describe('Developer experience level: beginner, intermediate, or expert'),
@@ -317,7 +345,11 @@ export function registerCoreSpecTools(server) {
317
345
  .describe('Absolute project root. Preferred when projectId is unknown.'),
318
346
  status: SpecStatusEnum.describe('New status for all specs'),
319
347
  dryRun: z.boolean().optional().describe('Preview the batch without mutating any spec.'),
320
- reviewNotes: z.string().max(10_000).optional().describe('Optional notes for each transition.'),
348
+ reviewNotes: z
349
+ .string()
350
+ .max(10_000)
351
+ .optional()
352
+ .describe('Optional notes for each transition.'),
321
353
  },
322
354
  }, safeTracked('update_status_batch', async (args) => handleUpdateStatusBatch(args)));
323
355
  // 8. estimate
@@ -456,11 +456,45 @@ const coreToolsRegistry = [
456
456
  .record(z.string().max(500), z.string().max(10_000))
457
457
  .optional()
458
458
  .describe('Answers collected in previous steps'),
459
+ parentWorkspacePath: z
460
+ .string()
461
+ .max(4096)
462
+ .optional()
463
+ .describe('Parent workspace path that should contain a new project folder'),
464
+ appName: z.string().max(500).optional().describe('Human-readable new app name'),
465
+ appSlug: z
466
+ .string()
467
+ .max(200)
468
+ .optional()
469
+ .describe('Safe folder name for the new app; must not contain path separators'),
470
+ projectType: z.string().max(500).optional().describe('Kind of project to create'),
471
+ platform: z.string().max(500).optional().describe('Runtime or deployment platform'),
472
+ language: z.string().max(500).optional().describe('User-confirmed programming language'),
473
+ framework: z
474
+ .string()
475
+ .max(500)
476
+ .nullable()
477
+ .optional()
478
+ .describe('User-confirmed framework, null for no framework'),
479
+ database: z
480
+ .string()
481
+ .max(500)
482
+ .nullable()
483
+ .optional()
484
+ .describe('User-confirmed database, null for no database'),
485
+ createDirectory: z
486
+ .boolean()
487
+ .optional()
488
+ .describe('When true in new_project flow, create only the resolved child project folder'),
489
+ clarificationAnswers: z
490
+ .record(z.string().max(500), z.string().max(10_000))
491
+ .optional()
492
+ .describe('Answers to interactiveQuestions emitted by facilitate'),
459
493
  step: z
460
494
  .number()
461
495
  .int()
462
496
  .min(0)
463
- .max(2)
497
+ .max(10)
464
498
  .optional()
465
499
  .describe('Current step index for new_project flow (0–2)'),
466
500
  targetTool: z
@@ -8,7 +8,10 @@ function firstText(result) {
8
8
  export async function handleUpdateStatusBatch(input) {
9
9
  const uniqueSpecIds = [...new Set(input.specIds)].sort();
10
10
  if (uniqueSpecIds.length === 0) {
11
- return { content: [{ type: 'text', text: 'update_status_batch requires at least one specId.' }], isError: true };
11
+ return {
12
+ content: [{ type: 'text', text: 'update_status_batch requires at least one specId.' }],
13
+ isError: true,
14
+ };
12
15
  }
13
16
  const resolved = await resolveProjectIdOrAutoDetect({
14
17
  projectId: input.projectId,
@@ -1,4 +1,5 @@
1
1
  import type { ParadigmReport } from './paradigm.js';
2
+ import type { TechnologySelectionContract } from './technology-selection.js';
2
3
  /** Ambiguity level for a tool request. */
3
4
  export type AmbiguityLevel = 'none' | 'low' | 'medium' | 'critical';
4
5
  /** Dimensions evaluated during ambiguity scoring. */
@@ -68,6 +69,18 @@ export interface FacilitateInput {
68
69
  scenario?: FacilitateScenario;
69
70
  /** Cumulative answers from previous steps. */
70
71
  collectedInfo?: Record<string, string>;
72
+ /** Parent workspace path for new-project onboarding. */
73
+ parentWorkspacePath?: string;
74
+ appName?: string;
75
+ appSlug?: string;
76
+ projectType?: string;
77
+ platform?: string;
78
+ language?: string;
79
+ framework?: string | null;
80
+ database?: string | null;
81
+ createDirectory?: boolean;
82
+ clarificationAnswers?: Record<string, string>;
83
+ technologySelectionContract?: TechnologySelectionContract;
71
84
  /** Step index (0-based). Incremented by the caller. */
72
85
  step?: number;
73
86
  /** Target tool for ambiguity check (optional). */
@@ -212,6 +212,7 @@ export * from './steering.js';
212
212
  export * from './sentry.js';
213
213
  export * from './supabase.js';
214
214
  export * from './skill-bootstrap.js';
215
+ export * from './technology-selection.js';
215
216
  export * from './compliance-gate.js';
216
217
  export * from './schema-parity.js';
217
218
  export * from './auto-update.js';
@@ -209,6 +209,7 @@ export * from './steering.js';
209
209
  export * from './sentry.js';
210
210
  export * from './supabase.js';
211
211
  export * from './skill-bootstrap.js';
212
+ export * from './technology-selection.js';
212
213
  export * from './compliance-gate.js';
213
214
  export * from './schema-parity.js';
214
215
  export * from './auto-update.js';
@@ -2,12 +2,24 @@ import type { SupportedLocale, ExperienceLevel, WorkMode } from '../common/index
2
2
  import type { ConstitutionPrinciple } from './core.js';
3
3
  import type { PermissionsMode } from '../permissions-config.js';
4
4
  import type { PluginInstallMode } from '../plugin-install.js';
5
+ import type { ProjectOnboardingMode, TechnologySelectionContract } from '../technology-selection.js';
5
6
  export interface SetLocaleInput {
6
7
  projectId?: string;
7
8
  locale: SupportedLocale;
8
9
  }
9
10
  export interface InitProjectInput {
10
11
  projectPath: string;
12
+ mode?: ProjectOnboardingMode;
13
+ parentWorkspacePath?: string;
14
+ appName?: string;
15
+ appSlug?: string;
16
+ projectType?: string;
17
+ platform?: string;
18
+ language?: string;
19
+ framework?: string | null;
20
+ database?: string | null;
21
+ createDirectory?: boolean;
22
+ technologySelectionContract?: TechnologySelectionContract;
11
23
  locale?: SupportedLocale;
12
24
  hourlyRate?: number;
13
25
  experienceLevel?: ExperienceLevel;
@@ -0,0 +1,77 @@
1
+ import type { InteractiveQuestion } from './clarification.js';
2
+ export type ProjectOnboardingMode = 'existing_project' | 'new_project';
3
+ export type TechnologyDecisionSource = 'detected' | 'user-confirmed' | 'suggested-confirmed';
4
+ export type TechnologyVerificationStatus = 'verified' | 'unverified';
5
+ export interface TechnologyDocsVerification {
6
+ name: string;
7
+ url: string;
8
+ status: TechnologyVerificationStatus;
9
+ reason?: string;
10
+ }
11
+ export interface TechnologySelectionContract {
12
+ mode: ProjectOnboardingMode;
13
+ projectPath: string;
14
+ parentWorkspacePath?: string;
15
+ appName?: string;
16
+ appSlug?: string;
17
+ projectType?: string;
18
+ platform?: string;
19
+ language?: string;
20
+ framework?: string | null;
21
+ database?: string | null;
22
+ source: TechnologyDecisionSource;
23
+ evidence: string[];
24
+ officialDocs: TechnologyDocsVerification[];
25
+ verifiedAt?: string;
26
+ }
27
+ export interface NewProjectOnboardingInput {
28
+ description: string;
29
+ parentWorkspacePath?: string;
30
+ projectPath?: string;
31
+ appName?: string;
32
+ appSlug?: string;
33
+ projectType?: string;
34
+ platform?: string;
35
+ language?: string;
36
+ framework?: string | null;
37
+ database?: string | null;
38
+ createDirectory?: boolean;
39
+ clarificationAnswers?: Record<string, string>;
40
+ }
41
+ export interface NewProjectResolution {
42
+ ready: boolean;
43
+ parentWorkspacePath?: string;
44
+ resolvedProjectPath?: string;
45
+ appSlug?: string;
46
+ questions: InteractiveQuestion[];
47
+ contract?: TechnologySelectionContract;
48
+ errors: string[];
49
+ }
50
+ export type NewProjectQuestionState = Partial<{
51
+ parentWorkspacePath: string;
52
+ appSlug: string;
53
+ projectType: string;
54
+ platform: string;
55
+ language: string;
56
+ framework: string | null;
57
+ languageAnswered: boolean;
58
+ frameworkAnswered: boolean;
59
+ createDirectoryAnswered: boolean;
60
+ }>;
61
+ export interface NewProjectResolvedFields {
62
+ parentWorkspacePath?: string;
63
+ appName?: string;
64
+ appSlug?: string;
65
+ projectType?: string;
66
+ platform?: string;
67
+ language?: string;
68
+ framework?: string | null;
69
+ database?: string | null;
70
+ languageAnswered: boolean;
71
+ frameworkAnswered: boolean;
72
+ createDirectoryAnswered: boolean;
73
+ createDirectory: boolean;
74
+ resolvedProjectPath?: string;
75
+ errors: string[];
76
+ }
77
+ //# sourceMappingURL=technology-selection.d.ts.map
@@ -0,0 +1,3 @@
1
+ // types/technology-selection.ts — SPEC-1021 runtime onboarding contract
2
+ export {};
3
+ //# sourceMappingURL=technology-selection.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "4.1.3",
3
+ "version": "4.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",
@@ -32,12 +32,12 @@
32
32
  "packageName": "@planu/core"
33
33
  },
34
34
  "optionalDependencies": {
35
- "@planu/core-darwin-arm64": "4.1.3",
36
- "@planu/core-darwin-x64": "4.1.3",
37
- "@planu/core-linux-arm64-gnu": "4.1.3",
38
- "@planu/core-linux-arm64-musl": "4.1.3",
39
- "@planu/core-linux-x64-gnu": "4.1.3",
40
- "@planu/core-linux-x64-musl": "4.1.3"
35
+ "@planu/core-darwin-arm64": "4.2.0",
36
+ "@planu/core-darwin-x64": "4.2.0",
37
+ "@planu/core-linux-arm64-gnu": "4.2.0",
38
+ "@planu/core-linux-arm64-musl": "4.2.0",
39
+ "@planu/core-linux-x64-gnu": "4.2.0",
40
+ "@planu/core-linux-x64-musl": "4.2.0"
41
41
  },
42
42
  "engines": {
43
43
  "node": ">=24.0.0"
@@ -143,6 +143,20 @@
143
143
  "hono": ">=4.12.14",
144
144
  "postcss": ">=8.5.10",
145
145
  "fast-uri": ">=3.1.2"
146
+ },
147
+ "supportedArchitectures": {
148
+ "os": [
149
+ "darwin",
150
+ "linux"
151
+ ],
152
+ "cpu": [
153
+ "arm64",
154
+ "x64"
155
+ ],
156
+ "libc": [
157
+ "glibc",
158
+ "musl"
159
+ ]
146
160
  }
147
161
  },
148
162
  "devDependencies": {
@@ -171,7 +185,7 @@
171
185
  "eslint-plugin-import": "^2.32.0",
172
186
  "happy-dom": "^20.9.0",
173
187
  "husky": "^9.1.7",
174
- "javascript-obfuscator": "^5.4.2",
188
+ "javascript-obfuscator": "^5.4.3",
175
189
  "knip": "^6.14.1",
176
190
  "lint-staged": "^17.0.5",
177
191
  "madge": "^8.0.0",