@planu/cli 4.1.4 → 4.2.1
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 +20 -1
- package/dist/engine/onboarding/new-project-resolver.d.ts +7 -0
- package/dist/engine/onboarding/new-project-resolver.js +265 -0
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +2 -1
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/technology-selection-store.d.ts +5 -0
- package/dist/storage/technology-selection-store.js +42 -0
- package/dist/tools/create-spec.js +23 -3
- package/dist/tools/facilitate.js +39 -29
- package/dist/tools/init-project/handler.js +59 -12
- package/dist/tools/register-spec-tools/core-spec-tools.js +33 -1
- package/dist/tools/tool-registry/core-tools.js +35 -1
- package/dist/tools/update-status/batch.js +4 -1
- package/dist/types/facilitator.d.ts +13 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/project/inputs.d.ts +12 -0
- package/dist/types/technology-selection.d.ts +77 -0
- package/dist/types/technology-selection.js +3 -0
- package/package.json +12 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## [4.2.1] - 2026-05-21
|
|
2
|
+
|
|
3
|
+
**Tarball SHA-256:** `9ff8a396e25eba3e4941bbef80ee19cc41bae46eaafa703cd32092c7ffbfdf2e`
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
- fix(release): add windows native core packages
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## [4.2.0] - 2026-05-21
|
|
10
|
+
|
|
11
|
+
**Tarball SHA-256:** `d036f3d5f73279fc0a12935995f899950fdf06afe454bba49da748183e07f477`
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
- feat(onboarding): add new project technology contract
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
- fix(release): sync native package versions
|
|
18
|
+
|
|
19
|
+
|
|
1
20
|
## [4.1.4] - 2026-05-21
|
|
2
21
|
|
|
3
22
|
### Bug Fixes
|
|
@@ -3845,4 +3864,4 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
|
|
|
3845
3864
|
- Mermaid diagram generation (architecture, sequence, state machine, ER, data flow)
|
|
3846
3865
|
- Multi-language i18n (EN/ES/PT) for generated specs
|
|
3847
3866
|
- Clean Architecture (hexagonal) — engine, tools, storage, types layers
|
|
3848
|
-
- 10,857 tests with ≥95% coverage
|
|
3867
|
+
- 10,857 tests with ≥95% coverage
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { InteractiveQuestion, NewProjectOnboardingInput, NewProjectQuestionState, NewProjectResolution } from '../../types/index.js';
|
|
2
|
+
export declare function slugifyAppName(input: string): string;
|
|
3
|
+
export declare function validateAppSlug(slug: string): string | null;
|
|
4
|
+
export declare function resolveNewProjectPath(parentWorkspacePath: string, appSlug: string): string;
|
|
5
|
+
export declare function buildNewProjectQuestions(input: NewProjectOnboardingInput, resolved: NewProjectQuestionState): InteractiveQuestion[];
|
|
6
|
+
export declare function resolveNewProjectOnboarding(input: NewProjectOnboardingInput): Promise<NewProjectResolution>;
|
|
7
|
+
//# sourceMappingURL=new-project-resolver.d.ts.map
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// engine/onboarding/new-project-resolver.ts — SPEC-1021 safe new-project onboarding
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import path, { resolve } from 'node:path';
|
|
4
|
+
import { findDocsEntry, loadDocsRegistry } from '../web-fetcher/registry-loader.js';
|
|
5
|
+
const NONE_LABELS = new Set(['none', 'no framework', 'ninguno', 'sin framework', 'n/a']);
|
|
6
|
+
const UNSURE_LABELS = new Set(['not sure', 'unsure', 'no estoy seguro', 'not-sure']);
|
|
7
|
+
function answer(input, key) {
|
|
8
|
+
const value = input.clarificationAnswers?.[key] ?? input.clarificationAnswers?.[key.toLowerCase()];
|
|
9
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
10
|
+
}
|
|
11
|
+
function valueOrAnswer(explicit, input, key) {
|
|
12
|
+
if (typeof explicit === 'string' && explicit.trim().length > 0) {
|
|
13
|
+
return explicit.trim();
|
|
14
|
+
}
|
|
15
|
+
return answer(input, key);
|
|
16
|
+
}
|
|
17
|
+
function normalizeNullableTech(value) {
|
|
18
|
+
if (value === null) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const trimmed = value?.trim();
|
|
22
|
+
if (!trimmed) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const lower = trimmed.toLowerCase();
|
|
26
|
+
if (NONE_LABELS.has(lower)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
if (UNSURE_LABELS.has(lower)) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return trimmed;
|
|
33
|
+
}
|
|
34
|
+
export function slugifyAppName(input) {
|
|
35
|
+
return input
|
|
36
|
+
.trim()
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
39
|
+
.replace(/[-.]{2,}/g, '-')
|
|
40
|
+
.replace(/^[-.]+|[-.]+$/g, '');
|
|
41
|
+
}
|
|
42
|
+
export function validateAppSlug(slug) {
|
|
43
|
+
if (!slug) {
|
|
44
|
+
return 'App folder is required.';
|
|
45
|
+
}
|
|
46
|
+
if (slug === '.' || slug === '..' || slug.includes('/') || slug.includes('\\')) {
|
|
47
|
+
return 'App folder must be a simple folder name, not a path.';
|
|
48
|
+
}
|
|
49
|
+
if (slug.includes('..')) {
|
|
50
|
+
return 'App folder cannot contain path traversal.';
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
function isWinPath(value) {
|
|
55
|
+
return /^[a-zA-Z]:[\\/]/.test(value) || value.startsWith('\\\\');
|
|
56
|
+
}
|
|
57
|
+
export function resolveNewProjectPath(parentWorkspacePath, appSlug) {
|
|
58
|
+
const slugError = validateAppSlug(appSlug);
|
|
59
|
+
if (slugError) {
|
|
60
|
+
throw new Error(slugError);
|
|
61
|
+
}
|
|
62
|
+
if (isWinPath(parentWorkspacePath)) {
|
|
63
|
+
return path.win32.normalize(path.win32.join(parentWorkspacePath, appSlug));
|
|
64
|
+
}
|
|
65
|
+
return resolve(parentWorkspacePath, appSlug);
|
|
66
|
+
}
|
|
67
|
+
function buildQuestion(header, question, recommended, alternate, fallback) {
|
|
68
|
+
return {
|
|
69
|
+
header,
|
|
70
|
+
question,
|
|
71
|
+
multiSelect: false,
|
|
72
|
+
options: [
|
|
73
|
+
{ label: `${recommended} (Recommended)`, description: `Use "${recommended}".` },
|
|
74
|
+
{ label: alternate, description: `Use "${alternate}".` },
|
|
75
|
+
{ label: fallback, description: `Use "${fallback}" or provide another value.` },
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function buildRequiredQuestions(input, resolved) {
|
|
80
|
+
const suggestedSlug = slugifyAppName(input.appName ?? input.description).slice(0, 48) || 'new-app';
|
|
81
|
+
const specs = [
|
|
82
|
+
[
|
|
83
|
+
!resolved.parentWorkspacePath,
|
|
84
|
+
buildQuestion('Parent', 'Which parent workspace should contain the new app folder?', process.cwd(), input.projectPath ?? './', 'Custom path'),
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
!resolved.appSlug,
|
|
88
|
+
buildQuestion('App Folder', 'What should the new app folder be called?', suggestedSlug, 'api', 'Custom folder'),
|
|
89
|
+
],
|
|
90
|
+
[
|
|
91
|
+
!resolved.projectType,
|
|
92
|
+
buildQuestion('Project Type', 'What type of project are you creating?', 'api', 'web app', 'Custom type'),
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
!resolved.platform,
|
|
96
|
+
buildQuestion('Platform', 'Where will this project run?', 'server', 'web', 'Custom platform'),
|
|
97
|
+
],
|
|
98
|
+
[
|
|
99
|
+
!resolved.languageAnswered,
|
|
100
|
+
buildQuestion('Language', 'Which programming language should this project use?', 'Not sure', 'Use project/team default', 'Custom language'),
|
|
101
|
+
],
|
|
102
|
+
[
|
|
103
|
+
!resolved.frameworkAnswered,
|
|
104
|
+
buildQuestion('Framework', 'Which framework should this project use?', 'Not sure', 'No framework', 'Custom framework'),
|
|
105
|
+
],
|
|
106
|
+
];
|
|
107
|
+
return specs.filter(([missing]) => missing).map(([, question]) => question);
|
|
108
|
+
}
|
|
109
|
+
function buildCreateDirectoryQuestion() {
|
|
110
|
+
return {
|
|
111
|
+
header: 'Create Dir',
|
|
112
|
+
question: 'May Planu create the project folder if it does not exist?',
|
|
113
|
+
multiSelect: false,
|
|
114
|
+
options: [
|
|
115
|
+
{ label: 'yes (Recommended)', description: 'Create only the resolved app folder.' },
|
|
116
|
+
{ label: 'no', description: 'Do not create folders; I will create it myself.' },
|
|
117
|
+
{ label: 'already exists', description: 'The project folder already exists.' },
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export function buildNewProjectQuestions(input, resolved) {
|
|
122
|
+
const questions = buildRequiredQuestions(input, resolved);
|
|
123
|
+
if (!resolved.createDirectoryAnswered) {
|
|
124
|
+
questions.push(buildCreateDirectoryQuestion());
|
|
125
|
+
}
|
|
126
|
+
return questions;
|
|
127
|
+
}
|
|
128
|
+
async function verifyDocs(names) {
|
|
129
|
+
const registry = await loadDocsRegistry();
|
|
130
|
+
return names
|
|
131
|
+
.filter((name) => name.trim().length > 0)
|
|
132
|
+
.map((name) => {
|
|
133
|
+
const entry = findDocsEntry(name, registry);
|
|
134
|
+
if (entry) {
|
|
135
|
+
return { name, url: entry.base, status: 'verified' };
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
name,
|
|
139
|
+
url: '',
|
|
140
|
+
status: 'unverified',
|
|
141
|
+
reason: 'No official docs registry entry found.',
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function collectResolvedFields(input) {
|
|
146
|
+
const parentWorkspacePath = valueOrAnswer(input.parentWorkspacePath, input, 'Parent') ??
|
|
147
|
+
valueOrAnswer(input.projectPath, input, 'Parent');
|
|
148
|
+
const appName = valueOrAnswer(input.appName, input, 'App Folder');
|
|
149
|
+
const appSlug = valueOrAnswer(input.appSlug, input, 'App Folder') ??
|
|
150
|
+
(appName ? slugifyAppName(appName) : undefined);
|
|
151
|
+
const projectType = valueOrAnswer(input.projectType, input, 'Project Type');
|
|
152
|
+
const platform = valueOrAnswer(input.platform, input, 'Platform');
|
|
153
|
+
const rawLanguage = valueOrAnswer(input.language, input, 'Language');
|
|
154
|
+
const rawFramework = input.framework === null ? null : valueOrAnswer(input.framework, input, 'Framework');
|
|
155
|
+
const rawDatabase = input.database === null ? null : valueOrAnswer(input.database, input, 'Database');
|
|
156
|
+
const language = normalizeNullableTech(rawLanguage);
|
|
157
|
+
const framework = normalizeNullableTech(rawFramework);
|
|
158
|
+
const database = normalizeNullableTech(rawDatabase);
|
|
159
|
+
const createDirAnswer = answer(input, 'Create Dir')?.toLowerCase();
|
|
160
|
+
const languageAnswered = rawLanguage !== undefined;
|
|
161
|
+
const frameworkAnswered = rawFramework !== undefined;
|
|
162
|
+
const createDirectoryAnswered = input.createDirectory !== undefined || createDirAnswer !== undefined;
|
|
163
|
+
const createDirectory = input.createDirectory === true ||
|
|
164
|
+
createDirAnswer === 'yes' ||
|
|
165
|
+
createDirAnswer === 'yes (recommended)' ||
|
|
166
|
+
createDirAnswer === 'already exists';
|
|
167
|
+
const errors = appSlug ? [validateAppSlug(appSlug)].filter((e) => Boolean(e)) : [];
|
|
168
|
+
const resolvedProjectPath = parentWorkspacePath && appSlug && errors.length === 0
|
|
169
|
+
? resolveNewProjectPath(parentWorkspacePath, appSlug)
|
|
170
|
+
: undefined;
|
|
171
|
+
return {
|
|
172
|
+
parentWorkspacePath,
|
|
173
|
+
appName,
|
|
174
|
+
appSlug,
|
|
175
|
+
projectType,
|
|
176
|
+
platform,
|
|
177
|
+
language: language ?? undefined,
|
|
178
|
+
framework,
|
|
179
|
+
database,
|
|
180
|
+
languageAnswered,
|
|
181
|
+
frameworkAnswered,
|
|
182
|
+
createDirectoryAnswered,
|
|
183
|
+
createDirectory,
|
|
184
|
+
resolvedProjectPath,
|
|
185
|
+
errors,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function isReady(fields) {
|
|
189
|
+
return Boolean(fields.parentWorkspacePath &&
|
|
190
|
+
fields.appSlug &&
|
|
191
|
+
fields.projectType &&
|
|
192
|
+
fields.platform &&
|
|
193
|
+
fields.languageAnswered &&
|
|
194
|
+
fields.frameworkAnswered &&
|
|
195
|
+
fields.createDirectoryAnswered &&
|
|
196
|
+
fields.createDirectory &&
|
|
197
|
+
fields.resolvedProjectPath &&
|
|
198
|
+
fields.errors.length === 0);
|
|
199
|
+
}
|
|
200
|
+
async function buildTechnologyContract(input, fields) {
|
|
201
|
+
const resolvedProjectPath = fields.resolvedProjectPath;
|
|
202
|
+
if (!resolvedProjectPath) {
|
|
203
|
+
throw new Error('Resolved project path is required to build a technology contract.');
|
|
204
|
+
}
|
|
205
|
+
const docNames = [fields.language, fields.framework, fields.database].filter((name) => typeof name === 'string' && name.length > 0);
|
|
206
|
+
const officialDocs = await verifyDocs(docNames);
|
|
207
|
+
return {
|
|
208
|
+
mode: 'new_project',
|
|
209
|
+
projectPath: resolvedProjectPath,
|
|
210
|
+
parentWorkspacePath: fields.parentWorkspacePath,
|
|
211
|
+
appName: fields.appName,
|
|
212
|
+
appSlug: fields.appSlug,
|
|
213
|
+
projectType: fields.projectType,
|
|
214
|
+
platform: fields.platform,
|
|
215
|
+
language: fields.language,
|
|
216
|
+
framework: fields.framework ?? null,
|
|
217
|
+
database: fields.database ?? null,
|
|
218
|
+
source: 'user-confirmed',
|
|
219
|
+
evidence: [
|
|
220
|
+
`User requested a new project: ${input.description}`,
|
|
221
|
+
`Resolved project folder: ${resolvedProjectPath}`,
|
|
222
|
+
],
|
|
223
|
+
officialDocs,
|
|
224
|
+
verifiedAt: new Date().toISOString(),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
export async function resolveNewProjectOnboarding(input) {
|
|
228
|
+
const fields = collectResolvedFields(input);
|
|
229
|
+
if (!isReady(fields)) {
|
|
230
|
+
return {
|
|
231
|
+
ready: false,
|
|
232
|
+
parentWorkspacePath: fields.parentWorkspacePath,
|
|
233
|
+
resolvedProjectPath: fields.resolvedProjectPath,
|
|
234
|
+
appSlug: fields.appSlug,
|
|
235
|
+
questions: buildNewProjectQuestions(input, {
|
|
236
|
+
parentWorkspacePath: fields.parentWorkspacePath,
|
|
237
|
+
appSlug: fields.appSlug,
|
|
238
|
+
projectType: fields.projectType,
|
|
239
|
+
platform: fields.platform,
|
|
240
|
+
language: fields.language,
|
|
241
|
+
framework: fields.framework,
|
|
242
|
+
languageAnswered: fields.languageAnswered,
|
|
243
|
+
frameworkAnswered: fields.frameworkAnswered,
|
|
244
|
+
createDirectoryAnswered: fields.createDirectoryAnswered,
|
|
245
|
+
}),
|
|
246
|
+
errors: fields.errors,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const resolvedProjectPath = fields.resolvedProjectPath;
|
|
250
|
+
if (!resolvedProjectPath) {
|
|
251
|
+
throw new Error('Resolved project path is required before directory creation.');
|
|
252
|
+
}
|
|
253
|
+
await mkdir(resolvedProjectPath, { recursive: true });
|
|
254
|
+
const contract = await buildTechnologyContract(input, fields);
|
|
255
|
+
return {
|
|
256
|
+
ready: true,
|
|
257
|
+
parentWorkspacePath: fields.parentWorkspacePath,
|
|
258
|
+
resolvedProjectPath: fields.resolvedProjectPath,
|
|
259
|
+
appSlug: fields.appSlug,
|
|
260
|
+
questions: [],
|
|
261
|
+
contract,
|
|
262
|
+
errors: fields.errors,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=new-project-resolver.js.map
|
|
@@ -171,7 +171,8 @@ export async function validateStrictPlanuLayout(projectPath) {
|
|
|
171
171
|
for (const entry of entries) {
|
|
172
172
|
const full = join(planuDir, entry);
|
|
173
173
|
const isDir = await pathIsDirectory(full);
|
|
174
|
-
if ((isDir && !isCanonicalPlanuRootDir(entry)) ||
|
|
174
|
+
if ((isDir && !isCanonicalPlanuRootDir(entry)) ||
|
|
175
|
+
(!isDir && !isCanonicalPlanuRootFile(entry))) {
|
|
175
176
|
offenders.push(relative(projectPath, full));
|
|
176
177
|
}
|
|
177
178
|
}
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -19,5 +19,6 @@ export * as conventionBaselineStore from './convention-baseline.js';
|
|
|
19
19
|
export * as lessonsStore from './lessons-store.js';
|
|
20
20
|
export * as actualsStore from './actuals-store.js';
|
|
21
21
|
export * as approvalStore from './approval-store.js';
|
|
22
|
+
export * as technologySelectionStore from './technology-selection-store.js';
|
|
22
23
|
export * from './skill-registry-storage.js';
|
|
23
24
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/storage/index.js
CHANGED
|
@@ -20,5 +20,6 @@ export * as conventionBaselineStore from './convention-baseline.js';
|
|
|
20
20
|
export * as lessonsStore from './lessons-store.js';
|
|
21
21
|
export * as actualsStore from './actuals-store.js';
|
|
22
22
|
export * as approvalStore from './approval-store.js';
|
|
23
|
+
export * as technologySelectionStore from './technology-selection-store.js';
|
|
23
24
|
export * from './skill-registry-storage.js';
|
|
24
25
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { TechnologySelectionContract } from '../types/index.js';
|
|
2
|
+
export declare const TECHNOLOGY_SELECTION_RELATIVE_PATH = ".planu/technology-selection.json";
|
|
3
|
+
export declare function readTechnologySelectionContract(projectPath: string): Promise<TechnologySelectionContract | null>;
|
|
4
|
+
export declare function writeTechnologySelectionContract(projectPath: string, contract: TechnologySelectionContract): Promise<void>;
|
|
5
|
+
//# sourceMappingURL=technology-selection-store.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// storage/technology-selection-store.ts — SPEC-1021 project-local technology contract
|
|
2
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { writeJsonSafe } from '../engine/safety/atomic-writer.js';
|
|
5
|
+
import { safeJsonParse } from './base-store.js';
|
|
6
|
+
import { withFileLock } from './file-mutex.js';
|
|
7
|
+
export const TECHNOLOGY_SELECTION_RELATIVE_PATH = '.planu/technology-selection.json';
|
|
8
|
+
function contractPath(projectPath) {
|
|
9
|
+
return join(projectPath, TECHNOLOGY_SELECTION_RELATIVE_PATH);
|
|
10
|
+
}
|
|
11
|
+
function isStringArray(value) {
|
|
12
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
13
|
+
}
|
|
14
|
+
function isTechnologySelectionContract(value) {
|
|
15
|
+
if (value === null || typeof value !== 'object') {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const rec = value;
|
|
19
|
+
return ((rec.mode === 'existing_project' || rec.mode === 'new_project') &&
|
|
20
|
+
typeof rec.projectPath === 'string' &&
|
|
21
|
+
(rec.source === 'detected' ||
|
|
22
|
+
rec.source === 'user-confirmed' ||
|
|
23
|
+
rec.source === 'suggested-confirmed') &&
|
|
24
|
+
isStringArray(rec.evidence) &&
|
|
25
|
+
Array.isArray(rec.officialDocs));
|
|
26
|
+
}
|
|
27
|
+
export async function readTechnologySelectionContract(projectPath) {
|
|
28
|
+
try {
|
|
29
|
+
const raw = await readFile(contractPath(projectPath), 'utf-8');
|
|
30
|
+
const parsed = safeJsonParse(raw, null);
|
|
31
|
+
return isTechnologySelectionContract(parsed) ? parsed : null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function writeTechnologySelectionContract(projectPath, contract) {
|
|
38
|
+
const filePath = contractPath(projectPath);
|
|
39
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
40
|
+
await withFileLock(filePath, () => writeJsonSafe(filePath, contract));
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=technology-selection-store.js.map
|
|
@@ -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
|
}));
|
package/dist/tools/facilitate.js
CHANGED
|
@@ -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,
|
|
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
|
|
359
|
-
|
|
360
|
-
:
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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(
|
|
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 {
|
|
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). */
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/types/index.js
CHANGED
|
@@ -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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.1
|
|
3
|
+
"version": "4.2.1",
|
|
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,14 @@
|
|
|
32
32
|
"packageName": "@planu/core"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|
|
35
|
-
"@planu/core-darwin-arm64": "4.1
|
|
36
|
-
"@planu/core-darwin-x64": "4.1
|
|
37
|
-
"@planu/core-linux-arm64-gnu": "4.1
|
|
38
|
-
"@planu/core-linux-arm64-musl": "4.1
|
|
39
|
-
"@planu/core-linux-x64-gnu": "4.1
|
|
40
|
-
"@planu/core-linux-x64-musl": "4.1
|
|
35
|
+
"@planu/core-darwin-arm64": "4.2.1",
|
|
36
|
+
"@planu/core-darwin-x64": "4.2.1",
|
|
37
|
+
"@planu/core-linux-arm64-gnu": "4.2.1",
|
|
38
|
+
"@planu/core-linux-arm64-musl": "4.2.1",
|
|
39
|
+
"@planu/core-linux-x64-gnu": "4.2.1",
|
|
40
|
+
"@planu/core-linux-x64-musl": "4.2.1",
|
|
41
|
+
"@planu/core-win32-arm64-msvc": "4.2.1",
|
|
42
|
+
"@planu/core-win32-x64-msvc": "4.2.1"
|
|
41
43
|
},
|
|
42
44
|
"engines": {
|
|
43
45
|
"node": ">=24.0.0"
|
|
@@ -147,7 +149,8 @@
|
|
|
147
149
|
"supportedArchitectures": {
|
|
148
150
|
"os": [
|
|
149
151
|
"darwin",
|
|
150
|
-
"linux"
|
|
152
|
+
"linux",
|
|
153
|
+
"win32"
|
|
151
154
|
],
|
|
152
155
|
"cpu": [
|
|
153
156
|
"arm64",
|
|
@@ -185,7 +188,7 @@
|
|
|
185
188
|
"eslint-plugin-import": "^2.32.0",
|
|
186
189
|
"happy-dom": "^20.9.0",
|
|
187
190
|
"husky": "^9.1.7",
|
|
188
|
-
"javascript-obfuscator": "^5.4.
|
|
191
|
+
"javascript-obfuscator": "^5.4.3",
|
|
189
192
|
"knip": "^6.14.1",
|
|
190
193
|
"lint-staged": "^17.0.5",
|
|
191
194
|
"madge": "^8.0.0",
|