@multileaf/ai-workflow 0.1.3 → 0.1.4

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 CHANGED
@@ -4,6 +4,22 @@ All notable changes to this project are documented in this file. The format foll
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.4] - 2026-09-12
8
+
9
+ ### Added
10
+
11
+ - Guided `/ai-init` onboarding with model-cost guidance, parallel project analysis, monorepo-aware profiles, selectable resource blocks, and project-specific resource personalization.
12
+ - NestJS, Prisma, and Vite capability recommendations with matching skills, agents, and data-access guidance.
13
+
14
+ ### Changed
15
+
16
+ - Allow recommendation selection by individual resource, including an explicit select-all option.
17
+ - Expand project profiles with workspace-level technologies, quality tools, test runners, architecture, patterns, and evidence.
18
+
19
+ ### Documentation
20
+
21
+ - Specify onboarding flow, host capabilities, MCP recommendations, safety expectations, and acceptance criteria.
22
+
7
23
  ## [0.1.3] - 2026-09-12
8
24
 
9
25
  ### Fixed
@@ -50,7 +66,8 @@ All notable changes to this project are documented in this file. The format foll
50
66
 
51
67
  - Path confinement, symlink protection, explicit permission gates, secret redaction, strict parsers, atomic migrations, and immutable execution checkpoints.
52
68
 
53
- [Unreleased]: https://github.com/MultiLeaf/aiw/compare/v0.1.3...HEAD
69
+ [Unreleased]: https://github.com/MultiLeaf/aiw/compare/v0.1.4...HEAD
70
+ [0.1.4]: https://github.com/MultiLeaf/aiw/compare/v0.1.3...v0.1.4
54
71
  [0.1.3]: https://github.com/MultiLeaf/aiw/compare/v0.1.2...v0.1.3
55
72
  [0.1.2]: https://github.com/MultiLeaf/aiw/compare/v0.1.1...v0.1.2
56
73
  [0.1.1]: https://github.com/MultiLeaf/aiw/releases/tag/v0.1.1
package/dist/adapter.js CHANGED
@@ -40,12 +40,14 @@ This skill is the bootstrap for AI Workflow. The installer intentionally install
40
40
 
41
41
  ## First-run setup
42
42
 
43
- 1. Inspect the repository's existing AI instructions and project structure. Preserve user-authored files and do not expose secrets.
44
- 2. Run \`npx --yes --package=@multileaf/ai-workflow -- aiw scan\`. Explain the detected facts and their evidence; ask the user to confirm uncertain inferences before they influence generated content.
45
- 3. Run \`npx --yes --package=@multileaf/ai-workflow -- aiw recommend\`, then read \`.aiw/recommendations.yml\` and \`.aiw/profile.yml\`. Explain why each suggested skill, rule, agent, hook, or template fits and identify external permissions. Ask which capabilities to install, and ask separately before granting an external permission such as \`network:external\`.
46
- 4. Record the user's choices with \`npx --yes --package=@multileaf/ai-workflow -- aiw recommend --select=id,id\`; use \`npx --yes --package=@multileaf/ai-workflow -- aiw recommend --select=\` when all recommendations are declined. If at least one capability is selected, run \`npx --yes --package=@multileaf/ai-workflow -- aiw sync\`; when the user separately approved external network access, add \`--allow=network:external\`. Sync installs only selected resources and fills supported project rules with detected stack and quality-tool details. Do not add unselected resources or overwrite existing files.
47
- 5. Review the installed resource list and the generated project-specific files with the user. Ask before changing project policy or tailoring any other skill/rule.
48
- 6. Introduce the selected workflow gates: brainstorming → specification → architecture decision when needed → implementation plan → implementation with tests → verification → traceability → review. Use only gates supported by the resources the user chose.
43
+ 1. Before scanning, recommend that the user switch to a cheaper model for this onboarding. Ask whether they switched or prefer to continue with the current model; never change model settings yourself. Explain that analysis is read-only and optional resources will not be installed without their selection.
44
+ 2. Use the host's native subagent mechanism to launch these read-only analyses in parallel: (a) technologies, root and nested manifests, workspaces, and monorepo boundaries; (b) architecture, modules, entry points, and recurring code patterns; (c) tests, quality commands, CI, and developer tools. Keep the main agent as orchestrator. Wait for all results, verify important claims against cited project files, reconcile disagreements, and consolidate one evidence-backed project profile. Do not inspect secrets, ignored files, dependency directories such as \`node_modules\`, or generated output; inspect declared dependencies in project manifests. If subagents are unavailable, perform the same analyses directly and disclose the fallback.
45
+ 3. Run \`npx --yes --package=@multileaf/ai-workflow -- aiw scan\` and read \`.aiw/profile.yml\`. Treat this deterministic scan as another evidence source, not as a replacement for workspace-level analysis. Summarize each workspace separately; do not assume root dependencies or commands apply to every module. Ask the user to confirm uncertain or conflicting facts before using them as project policy. Add confirmed architecture and recurring patterns to the matching entries in the \`project_modules\` JSON array in \`.aiw/profile.yml\`, including file-path evidence; create a root-level entry when only the root project exists. Preserve existing detected fields and never store secrets or source-code excerpts there.
46
+ 4. Run \`npx --yes --package=@multileaf/ai-workflow -- aiw recommend\` and read \`.aiw/recommendations.yml\`. Combine catalog results with the consolidated project profile. Recommend matching skills, rules, agents, hooks, templates, and MCP servers. For every MCP, disclose its publisher/source, purpose, capabilities, permissions, transport, and credential needs; verify them against current official host and server documentation. Recommend only integrations supported by project evidence and never invent credentials.
47
+ 5. Use the host's native selection widget. First offer three choices: install all recommendations, customize, or install none. If the user chooses customization, show the install plan in separate selectable blocks for skills, rules, agents, hooks, templates, and MCPs, with each item, its rationale, project-specific changes, and dependencies visible. If the host lacks a multi-select widget, use a numbered all/custom/none prompt and grouped checklists. MCP selection does not grant permission to execute the server or use external access; ask separately for those permissions.
48
+ 6. Record all recommendations with \`npx --yes --package=@multileaf/ai-workflow -- aiw recommend --select=all\`; for customized item-level choices, pass the selected resource references such as \`skills/verification,rules/tdd-policy,agents/test-engineer\`. Use \`npx --yes --package=@multileaf/ai-workflow -- aiw recommend --select=\` when none are chosen. If any AI Workflow resources are selected, run \`npx --yes --package=@multileaf/ai-workflow -- aiw sync\`, adding \`--allow=network:external\` only after separate approval. Install only selected resources and declared dependencies. Tailor instructions only where project evidence changes their usefulness (such as test commands, workspace boundaries, framework patterns, or module architecture). Preserve existing files, configuration, and secrets; report conflicts rather than overwriting them.
49
+ 7. For selected MCPs, add only the selected servers to the host's supported project configuration. Check current official host and server documentation for the configuration format, merge existing settings without replacing unrelated entries, and use environment-variable references instead of literal credentials. Do not run server install/start/authentication commands without separate approval. If the target requires user-level changes or safe configuration cannot be validated, give the user exact manual steps and report the MCP as pending rather than installed.
50
+ 8. Report installed resources by block, project-specific changes, skipped recommendations, MCP trust/authentication steps, and conflicts. Recommend restarting or reloading the coding-agent session so it discovers newly installed resources; follow known host behavior if it reloads changes automatically. Introduce only the gates supported by installed resources: brainstorming → specification → architecture decision when needed → implementation plan → implementation with tests → verification → traceability → review.
49
51
 
50
52
  If the user declines all recommendations, leave the project with only the bootstrap skill and scan state. Do not treat scanning or recommendation as consent to install.
51
53
 
package/dist/generator.js CHANGED
@@ -3,11 +3,13 @@ export function generateProjectResources(profile, selected, write) {
3
3
  const linterCommand = safeInlineValue(profile.quality.linter?.command);
4
4
  const formatterCommand = safeInlineValue(profile.quality.formatter?.command);
5
5
  const testCommand = safeInlineValue(profile.testing?.command);
6
- if (selected.includes("typescript-quality"))
6
+ if (selected.includes("typescript-quality") || selected.includes("rules/project-quality"))
7
7
  write(resolveGeneratedPath("rules/project-quality.md"), `# Project Quality Rules\n\n- Use ${profile.runtime.languages.map((language) => (language === "typescript" ? "TypeScript" : language)).join(", ")} with strict typing.\n${linterCommand ? `- Run \`${linterCommand}\` before completion.\n` : ""}${formatterCommand ? `- Run \`${formatterCommand}\` before review.\n` : ""}- Keep all generated AI Workflow artifacts in English.\n`);
8
- if (selected.includes("tdd-development"))
8
+ if (selected.includes("tdd-development") || selected.includes("rules/tdd-project-policy"))
9
9
  write(resolveGeneratedPath("rules/tdd-project-policy.md"), `# TDD Project Policy\n\n${testCommand ? `- Run \`${testCommand}\` for verification.\n` : ""}- Write a failing behavioral test before implementation.\n- Refactor only after the test is green.\n`);
10
- if (selected.includes("nextjs-development") || selected.includes("react-best-practices"))
10
+ if (selected.includes("nextjs-development") ||
11
+ selected.includes("react-best-practices") ||
12
+ selected.includes("rules/architecture-policy"))
11
13
  write(resolveGeneratedPath("agents/project-context.md"), `# Project Context\n\nThis project uses ${profile.frameworks.map((framework) => (framework === "nextjs" ? "Next.js" : framework)).join(", ")}. Follow the detected project structure and existing framework conventions.\n`);
12
14
  if (selected.length > 0) {
13
15
  write(resolveGeneratedPath("context/project-profile.md"), `# Confirmed Project Profile\n\n- Languages: ${profile.runtime.languages.join(", ") || "none detected"}\n- Frameworks: ${profile.frameworks.join(", ") || "none detected"}\n- Package manager: ${profile.packageManager}\n- All generated content is in English.\n`);
package/dist/profile.d.ts CHANGED
@@ -3,6 +3,24 @@ export type Tool = {
3
3
  name: string;
4
4
  command?: string;
5
5
  };
6
+ export type ProjectModule = {
7
+ path: string;
8
+ name?: string;
9
+ runtime: {
10
+ languages: string[];
11
+ };
12
+ frameworks: string[];
13
+ packageManager: string;
14
+ quality: {
15
+ linter?: Tool;
16
+ formatter?: Tool;
17
+ typecheck?: Tool;
18
+ };
19
+ testing?: Tool;
20
+ architecture?: string[];
21
+ patterns?: string[];
22
+ evidence?: string[];
23
+ };
6
24
  export type ProjectProfile = {
7
25
  runtime: {
8
26
  languages: string[];
@@ -17,6 +35,7 @@ export type ProjectProfile = {
17
35
  testing?: Tool;
18
36
  ci: string[];
19
37
  workspaces: string[];
38
+ modules?: ProjectModule[];
20
39
  facts: EvidenceFact[];
21
40
  };
22
41
  export declare function parseProjectProfile(content: string): ProjectProfile;
package/dist/profile.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
2
+ import { dirname, join } from "node:path";
3
3
  import { scanProject } from "./scanner.js";
4
4
  import { buildFacts } from "./evidence.js";
5
5
  function scalar(content, key) {
@@ -36,6 +36,15 @@ export function parseProjectProfile(content) {
36
36
  const toolName = scalar(content, name);
37
37
  return toolName ? { name: toolName, command: scalar(content, command) } : undefined;
38
38
  };
39
+ let modules = [];
40
+ try {
41
+ const parsed = JSON.parse(scalar(content, "project_modules") ?? "[]");
42
+ if (Array.isArray(parsed))
43
+ modules = parseModules(parsed);
44
+ }
45
+ catch {
46
+ modules = [];
47
+ }
39
48
  return {
40
49
  runtime: { languages },
41
50
  frameworks,
@@ -48,12 +57,68 @@ export function parseProjectProfile(content) {
48
57
  testing: tool("testing", "testing_command"),
49
58
  ci: list(content, "ci"),
50
59
  workspaces: list(content, "workspaces"),
60
+ modules,
51
61
  facts: [],
52
62
  };
53
63
  }
64
+ function parseModules(values) {
65
+ return values.slice(0, 256).flatMap((value) => {
66
+ if (!value || typeof value !== "object" || Array.isArray(value))
67
+ return [];
68
+ const item = value;
69
+ if (typeof item.path !== "string" || !item.path.trim())
70
+ return [];
71
+ const runtime = item.runtime && typeof item.runtime === "object"
72
+ ? item.runtime
73
+ : {};
74
+ const quality = item.quality && typeof item.quality === "object"
75
+ ? item.quality
76
+ : {};
77
+ const tool = (candidate) => {
78
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
79
+ return undefined;
80
+ const details = candidate;
81
+ if (typeof details.name !== "string" || !details.name.trim())
82
+ return undefined;
83
+ return {
84
+ name: details.name.slice(0, 180),
85
+ command: typeof details.command === "string" ? details.command.slice(0, 180) : undefined,
86
+ };
87
+ };
88
+ return [
89
+ {
90
+ path: item.path.slice(0, 240),
91
+ name: typeof item.name === "string" ? item.name.slice(0, 180) : undefined,
92
+ runtime: { languages: stringArray(runtime.languages) },
93
+ frameworks: stringArray(item.frameworks),
94
+ packageManager: typeof item.packageManager === "string" ? item.packageManager.slice(0, 80) : "unknown",
95
+ quality: {
96
+ linter: tool(quality.linter),
97
+ formatter: tool(quality.formatter),
98
+ typecheck: tool(quality.typecheck),
99
+ },
100
+ testing: tool(item.testing),
101
+ architecture: stringArray(item.architecture),
102
+ patterns: stringArray(item.patterns),
103
+ evidence: stringArray(item.evidence),
104
+ },
105
+ ];
106
+ });
107
+ }
108
+ function stringArray(value) {
109
+ return Array.isArray(value)
110
+ ? value
111
+ .filter((item) => typeof item === "string")
112
+ .slice(0, 32)
113
+ .map((item) => item.slice(0, 240))
114
+ : [];
115
+ }
54
116
  async function packageManifest(root) {
117
+ return packageManifestFile(join(root, "package.json"));
118
+ }
119
+ async function packageManifestFile(path) {
55
120
  try {
56
- return JSON.parse(await readFile(join(root, "package.json"), "utf8"));
121
+ return JSON.parse(await readFile(path, "utf8"));
57
122
  }
58
123
  catch {
59
124
  return {};
@@ -69,17 +134,21 @@ export async function profileProject(root) {
69
134
  const scan = await scanProject(root);
70
135
  const manifest = await packageManifest(root);
71
136
  const files = scan.files;
137
+ const moduleManifests = files.filter((file) => /(^|\/)package\.json$/.test(file));
138
+ const packageDirectories = moduleManifests.map((file) => dirname(file));
139
+ const modules = await Promise.all(moduleManifests
140
+ .filter((file) => dirname(file) !== ".")
141
+ .map((file) => profileModule(root, file, files, scan.evidence, undefined, packageDirectories)));
142
+ const rootModule = await profileModule(root, "package.json", files, scan.evidence, manifest, packageDirectories);
72
143
  const languages = [
73
- ...new Set(scan.evidence
74
- .filter((item) => item.fact.startsWith("language."))
75
- .map((item) => item.fact.replace("language.", ""))),
144
+ ...new Set([
145
+ ...rootModule.runtime.languages,
146
+ ...modules.flatMap((item) => item.runtime.languages),
147
+ ]),
76
148
  ];
77
149
  const frameworks = [
78
- hasDependency(manifest, "next") ? "nextjs" : "",
79
- hasDependency(manifest, "react") ? "react" : "",
80
- hasDependency(manifest, "vue") ? "vue" : "",
81
- hasDependency(manifest, "express") ? "express" : "",
82
- ].filter(Boolean);
150
+ ...new Set([...rootModule.frameworks, ...modules.flatMap((item) => item.frameworks)]),
151
+ ];
83
152
  const packageManager = files.includes("pnpm-lock.yaml")
84
153
  ? "pnpm"
85
154
  : files.includes("yarn.lock")
@@ -116,8 +185,74 @@ export async function profileProject(root) {
116
185
  testing: testingName ? { name: testingName, command: commandFor(manifest, "test") } : undefined,
117
186
  ci: files.some((file) => file.startsWith(".github/workflows/")) ? ["github-actions"] : [],
118
187
  workspaces,
188
+ modules,
119
189
  facts: [],
120
190
  };
121
191
  profile.facts = buildFacts(profile, scan.evidence);
122
192
  return profile;
123
193
  }
194
+ async function profileModule(root, manifestPath, files, evidence, existingManifest, packageDirectories = []) {
195
+ const manifest = existingManifest ?? (await packageManifestFile(join(root, manifestPath)));
196
+ const modulePath = dirname(manifestPath) === "." ? "." : dirname(manifestPath).split("\\").join("/");
197
+ const prefix = modulePath === "." ? "" : `${modulePath}/`;
198
+ const childPrefixes = packageDirectories
199
+ .filter((directory) => directory !== modulePath && directory.startsWith(prefix))
200
+ .map((directory) => `${directory}/`);
201
+ const moduleFiles = files.filter((file) => file.startsWith(prefix) && !childPrefixes.some((childPrefix) => file.startsWith(childPrefix)));
202
+ const languages = [
203
+ ...new Set(evidence
204
+ .filter((item) => item.fact.startsWith("language.") && item.source.startsWith(prefix))
205
+ .map((item) => item.fact.slice("language.".length))),
206
+ ];
207
+ const frameworkSet = new Set();
208
+ const addDependency = (dependency, label) => {
209
+ if (hasDependency(manifest, dependency))
210
+ frameworkSet.add(label);
211
+ };
212
+ addDependency("next", "nextjs");
213
+ addDependency("react", "react");
214
+ addDependency("vue", "vue");
215
+ addDependency("express", "express");
216
+ addDependency("@nestjs/core", "nestjs");
217
+ addDependency("@prisma/client", "prisma");
218
+ addDependency("prisma", "prisma");
219
+ addDependency("vite", "vite");
220
+ addDependency("fastify", "fastify");
221
+ if (moduleFiles.some((file) => file.startsWith(`${prefix}prisma/`) && file.endsWith(".prisma")))
222
+ frameworkSet.add("prisma");
223
+ const testingName = hasDependency(manifest, "vitest")
224
+ ? "vitest"
225
+ : hasDependency(manifest, "jest")
226
+ ? "jest"
227
+ : hasDependency(manifest, "@playwright/test")
228
+ ? "playwright"
229
+ : hasDependency(manifest, "cypress")
230
+ ? "cypress"
231
+ : undefined;
232
+ const qualityTool = (dependency, config, script, name) => hasDependency(manifest, dependency) ||
233
+ moduleFiles.some((file) => file.startsWith(`${prefix}${config}`))
234
+ ? { name, command: commandFor(manifest, script) }
235
+ : undefined;
236
+ const packageManager = moduleFiles.some((file) => file === `${prefix}pnpm-lock.yaml`)
237
+ ? "pnpm"
238
+ : moduleFiles.some((file) => file === `${prefix}yarn.lock`)
239
+ ? "yarn"
240
+ : moduleFiles.some((file) => file === `${prefix}package-lock.json`)
241
+ ? "npm"
242
+ : "unknown";
243
+ return {
244
+ path: modulePath,
245
+ name: typeof manifest.name === "string" ? manifest.name : undefined,
246
+ runtime: { languages },
247
+ frameworks: [...frameworkSet],
248
+ packageManager,
249
+ quality: {
250
+ linter: qualityTool("eslint", "eslint.config", "lint", "eslint"),
251
+ formatter: qualityTool("prettier", "prettier", "format", "prettier"),
252
+ typecheck: commandFor(manifest, "typecheck")
253
+ ? { name: "typescript", command: commandFor(manifest, "typecheck") }
254
+ : undefined,
255
+ },
256
+ testing: testingName ? { name: testingName, command: commandFor(manifest, "test") } : undefined,
257
+ };
258
+ }
@@ -85,6 +85,36 @@ const CATALOG = [
85
85
  ? { evidence: ["language:typescript"], confidence: 1 }
86
86
  : undefined,
87
87
  },
88
+ {
89
+ id: "nestjs-backend",
90
+ provider: "multileaf",
91
+ rationale: "NestJS is detected in this project; its module, dependency-injection, and test conventions can guide backend work.",
92
+ permissions: [],
93
+ resources: local([
94
+ { type: "skills", id: "nestjs-development" },
95
+ { type: "agents", id: "nestjs-engineer" },
96
+ { type: "rules", id: "architecture-policy" },
97
+ { type: "templates", id: "technical-design" },
98
+ ]),
99
+ matches: (profile) => profile.frameworks.includes("nestjs")
100
+ ? { evidence: projectEvidence(profile, "nestjs"), confidence: 1 }
101
+ : undefined,
102
+ },
103
+ {
104
+ id: "prisma-data-access",
105
+ provider: "multileaf",
106
+ rationale: "Prisma is detected; schema and data-access changes need migration-aware guidance aligned to the owning workspace.",
107
+ permissions: [],
108
+ resources: local([
109
+ { type: "skills", id: "prisma-data-access" },
110
+ { type: "agents", id: "prisma-engineer" },
111
+ { type: "rules", id: "data-access-policy" },
112
+ { type: "rules", id: "security-policy" },
113
+ ]),
114
+ matches: (profile) => profile.frameworks.includes("prisma")
115
+ ? { evidence: projectEvidence(profile, "prisma"), confidence: 1 }
116
+ : undefined,
117
+ },
88
118
  {
89
119
  id: "vitest-testing",
90
120
  provider: "multileaf",
@@ -130,6 +160,21 @@ const CATALOG = [
130
160
  ? { evidence: ["framework:nextjs"], confidence: 1 }
131
161
  : undefined,
132
162
  },
163
+ {
164
+ id: "vite-frontend",
165
+ provider: "multileaf",
166
+ rationale: "Vite is detected; frontend build, test, and module guidance should follow the owning app's framework and scripts.",
167
+ permissions: [],
168
+ resources: local([
169
+ { type: "skills", id: "vite-frontend" },
170
+ { type: "agents", id: "typescript-engineer" },
171
+ { type: "rules", id: "quality-gates" },
172
+ { type: "templates", id: "technical-design" },
173
+ ]),
174
+ matches: (profile) => profile.frameworks.includes("vite")
175
+ ? { evidence: projectEvidence(profile, "vite"), confidence: 1 }
176
+ : undefined,
177
+ },
133
178
  {
134
179
  id: "security-review",
135
180
  provider: "multileaf",
@@ -163,15 +208,21 @@ export function recommendCapabilities(profile) {
163
208
  });
164
209
  }
165
210
  export function resourcesForCapabilities(ids) {
166
- const selected = new Set(ids);
167
- const resources = CATALOG.filter(({ id }) => selected.has(id)).flatMap(({ resources }) => resources);
211
+ const selectedCapabilities = new Set(CATALOG.filter(({ id }) => ids.includes(id)).map(({ id }) => id));
212
+ const selectedResources = new Set(ids.filter((id) => id.includes("/")));
213
+ const resources = CATALOG.flatMap(({ id, resources }) => selectedCapabilities.has(id)
214
+ ? resources
215
+ : resources.filter(({ type, id: resourceId }) => selectedResources.has(`${type}/${resourceId}`)));
168
216
  return [
169
217
  ...new Map(resources.map((resource) => [`${resource.type}/${resource.id}`, resource])).values(),
170
218
  ];
171
219
  }
172
220
  export function serializeRecommendations(items, selected = []) {
173
221
  return `schema: 1\nrecommendations:\n${items
174
- .map((item) => ` - id: ${item.id}\n provider: ${item.provider}\n confidence: ${item.confidence}\n selected: ${selected.includes(item.id)}\n resources: [${item.resources.map(({ type, id }) => `${type}/${id}`).join(", ")}]\n evidence: [${item.evidence.map((evidence) => `"${evidence}"`).join(", ")}]\n rationale: ${item.rationale}`)
222
+ .map((item) => ` - id: ${item.id}\n provider: ${item.provider}\n confidence: ${item.confidence}\n selected: ${selected.includes(item.id)}\n resources: [${item.resources.map(({ type, id }) => `${type}/${id}`).join(", ")}]\n selected_resources: [${item.resources
223
+ .filter(({ type, id }) => selected.includes(`${type}/${id}`))
224
+ .map(({ type, id }) => `${type}/${id}`)
225
+ .join(", ")}]\n evidence: [${item.evidence.map((evidence) => `"${evidence}"`).join(", ")}]\n rationale: ${item.rationale}`)
175
226
  .join("\n")}\n`;
176
227
  }
177
228
  function hasProjectSignals(profile) {
@@ -182,7 +233,15 @@ function hasProjectSignals(profile) {
182
233
  profile.quality.linter ||
183
234
  profile.ci.length);
184
235
  }
185
- function projectEvidence(profile) {
236
+ function projectEvidence(profile, framework) {
237
+ const moduleEvidence = (profile.modules ?? [])
238
+ .filter((module) => !framework || module.frameworks.includes(framework))
239
+ .flatMap((module) => [
240
+ `workspace:${module.path}`,
241
+ ...module.frameworks.map((name) => `framework:${name}`),
242
+ ...(module.testing ? [`testing:${module.testing.name}`] : []),
243
+ ...(module.testing?.command ? [`test-command:${module.testing.command}`] : []),
244
+ ]);
186
245
  return [
187
246
  ...profile.runtime.languages.map((language) => `language:${language}`),
188
247
  ...profile.frameworks.map((framework) => `framework:${framework}`),
@@ -190,5 +249,6 @@ function projectEvidence(profile) {
190
249
  ...(profile.testing ? [`testing:${profile.testing.name}`] : []),
191
250
  ...(profile.quality.linter ? [`linter:${profile.quality.linter.name}`] : []),
192
251
  ...(profile.ci.length ? profile.ci.map((system) => `ci:${system}`) : []),
252
+ ...moduleEvidence,
193
253
  ];
194
254
  }
package/dist/selection.js CHANGED
@@ -1,6 +1,12 @@
1
1
  export async function selectRecommendations(items, answer, selectedIds) {
2
+ const available = new Set(items.flatMap((item) => [
3
+ item.id,
4
+ ...(item.resources ?? []).map(({ type, id }) => `${type}/${id}`),
5
+ ]));
6
+ if (selectedIds?.includes("all"))
7
+ return items.map(({ id }) => id);
2
8
  if (selectedIds)
3
- return selectedIds.filter((id) => items.some((item) => item.id === id));
9
+ return [...new Set(selectedIds.filter((id) => available.has(id)))];
4
10
  const selected = [];
5
11
  for (const item of items) {
6
12
  const response = (await answer(`Install ${item.id}? [Y/n] `)).trim().toLowerCase();
package/dist/workflow.js CHANGED
@@ -290,7 +290,7 @@ export async function runCommand(args, root, fs, services = {}) {
290
290
  const evidence = profile.facts
291
291
  .map((fact) => ` - key: ${fact.key}\n value: ${fact.value}\n state: ${fact.state}\n method: ${fact.method}\n confidence: ${fact.confidence}${fact.requiresConfirmation === undefined ? "" : `\n requires_confirmation: ${fact.requiresConfirmation}`}\n evidence: [${fact.evidence.map((item) => `"${item.source}"`).join(", ")}]`)
292
292
  .join("\n");
293
- fs.write(join(aiw, "profile.yml"), `schema: 1\nstatus: scanned\nfacts:\n${evidence}\nruntime:\n languages: [${profile.runtime.languages.join(", ")}]\nframeworks: [${profile.frameworks.join(", ")}]\npackage_manager: ${profile.packageManager}\nquality:\n linter: ${profile.quality.linter?.name ?? "unknown"}\n linter_command: ${profile.quality.linter?.command ?? "unknown"}\n formatter: ${profile.quality.formatter?.name ?? "unknown"}\n formatter_command: ${profile.quality.formatter?.command ?? "unknown"}\n typecheck: ${profile.quality.typecheck?.name ?? "unknown"}\n typecheck_command: ${profile.quality.typecheck?.command ?? "unknown"}\ntesting: ${profile.testing?.name ?? "unknown"}\ntesting_command: ${profile.testing?.command ?? "unknown"}\nci: [${profile.ci.join(", ")}]\nworkspaces: [${profile.workspaces.join(", ")}]\npolicies:\n artifact_language: en\n`);
293
+ fs.write(join(aiw, "profile.yml"), `schema: 1\nstatus: scanned\nfacts:\n${evidence}\nruntime:\n languages: [${profile.runtime.languages.join(", ")}]\nframeworks: [${profile.frameworks.join(", ")}]\npackage_manager: ${profile.packageManager}\nquality:\n linter: ${profile.quality.linter?.name ?? "unknown"}\n linter_command: ${profile.quality.linter?.command ?? "unknown"}\n formatter: ${profile.quality.formatter?.name ?? "unknown"}\n formatter_command: ${profile.quality.formatter?.command ?? "unknown"}\n typecheck: ${profile.quality.typecheck?.name ?? "unknown"}\n typecheck_command: ${profile.quality.typecheck?.command ?? "unknown"}\ntesting: ${profile.testing?.name ?? "unknown"}\ntesting_command: ${profile.testing?.command ?? "unknown"}\nci: [${profile.ci.join(", ")}]\nworkspaces: [${profile.workspaces.join(", ")}]\nproject_modules: ${JSON.stringify(profile.modules ?? [])}\npolicies:\n artifact_language: en\n`);
294
294
  return { exitCode: 0, output: "Project profile generated." };
295
295
  }
296
296
  if (command === "target") {
@@ -1630,7 +1630,19 @@ function syncSelectedResources(fs, root, aiw, profile, selected) {
1630
1630
  };
1631
1631
  }
1632
1632
  function personalizeBundledResource(type, id, content, profile) {
1633
- if (type !== "rules" || !["quality-gates", "tdd-policy", "dependency-policy"].includes(id))
1633
+ const contextual = (type === "rules" &&
1634
+ [
1635
+ "quality-gates",
1636
+ "tdd-policy",
1637
+ "dependency-policy",
1638
+ "architecture-policy",
1639
+ "data-access-policy",
1640
+ ].includes(id)) ||
1641
+ (type === "skills" &&
1642
+ ["nestjs-development", "prisma-data-access", "vite-frontend"].includes(id)) ||
1643
+ (type === "agents" &&
1644
+ ["technical-architect", "typescript-engineer", "nestjs-engineer", "prisma-engineer"].includes(id));
1645
+ if (!contextual)
1634
1646
  return content;
1635
1647
  const signals = [
1636
1648
  profile.runtime.languages.length ? `- Languages: ${profile.runtime.languages.join(", ")}` : "",
@@ -1647,6 +1659,24 @@ function personalizeBundledResource(type, id, content, profile) {
1647
1659
  profile.quality.formatter?.command
1648
1660
  ? `- Format command: \`${safeProjectSignal(profile.quality.formatter.command)}\``
1649
1661
  : "",
1662
+ ...(profile.modules ?? []).map((module) => {
1663
+ const parts = [
1664
+ module.name
1665
+ ? `${safeProjectSignal(module.name)} (${safeProjectSignal(module.path)})`
1666
+ : safeProjectSignal(module.path),
1667
+ module.frameworks.length ? `frameworks: ${module.frameworks.join(", ")}` : "",
1668
+ ...(module.architecture ?? []).map((item) => `architecture: ${safeProjectSignal(item)}`),
1669
+ ...(module.patterns ?? []).map((item) => `patterns: ${safeProjectSignal(item)}`),
1670
+ module.testing
1671
+ ? `tests: ${module.testing.name}${module.testing.command ? ` (${safeProjectSignal(module.testing.command)})` : ""}`
1672
+ : "",
1673
+ module.quality.linter
1674
+ ? `linter: ${module.quality.linter.name}${module.quality.linter.command ? ` (${safeProjectSignal(module.quality.linter.command)})` : ""}`
1675
+ : "",
1676
+ ].filter(Boolean);
1677
+ const evidence = (module.evidence ?? []).slice(0, 8).map(safeProjectSignal);
1678
+ return `- Module ${parts.join("; ")}${evidence.length ? `; evidence: ${evidence.join(", ")}` : ""}`;
1679
+ }),
1650
1680
  ].filter(Boolean);
1651
1681
  return signals.length
1652
1682
  ? `${content.trimEnd()}\n\n## Detected project context\n\n${signals.join("\n")}\n`
@@ -1666,11 +1696,20 @@ function readSelectedRecommendations(fs, aiw) {
1666
1696
  if (!fs.exists(join(aiw, "recommendations.yml")))
1667
1697
  return [];
1668
1698
  const content = fs.read(join(aiw, "recommendations.yml"));
1669
- return content
1699
+ const selections = content
1670
1700
  .split(/^\s{2}- id: /m)
1671
1701
  .slice(1)
1672
- .filter((block) => block.includes("selected: true"))
1673
- .map((block) => block.split("\n", 1)[0].trim());
1702
+ .flatMap((block) => {
1703
+ const id = block.split("\n", 1)[0].trim();
1704
+ const selectedCapability = /^\s{4}selected: true$/m.test(block) ? [id] : [];
1705
+ const selectedResources = block
1706
+ .match(/^\s{4}selected_resources: \[([^\]]*)\]$/m)?.[1]
1707
+ ?.split(",")
1708
+ .map((resource) => resource.trim())
1709
+ .filter(Boolean) ?? [];
1710
+ return [...selectedCapability, ...selectedResources];
1711
+ });
1712
+ return [...new Set(selections)];
1674
1713
  }
1675
1714
  function parseApprovedPermissions(args) {
1676
1715
  return args
@@ -0,0 +1,45 @@
1
+ # AI Init Project Onboarding Specification
2
+
3
+ ## Goal
4
+
5
+ Make `/ai-init` a guided, evidence-backed onboarding flow. The initial `npx` install remains a small bootstrap; `/ai-init` analyzes the repository, recommends project-matched capabilities, gets an explicit selection, installs only that selection, and reports any restart needed to load it.
6
+
7
+ ## User flow
8
+
9
+ 1. **Model and consent.** Before repository analysis, recommend switching to a lower-cost model. Do not change model settings. Ask whether the user switched or wants to continue with the current model. Explain that repository analysis is read-only until resource selection.
10
+ 2. **Parallel project analysis.** Use the host's native subagent mechanism to start independent read-only scans in parallel. Assign separate work to (a) technologies and package/workspace topology, (b) architecture, modules, and code patterns, and (c) tests, quality tools, and CI. The initiating agent is the orchestrator: it consolidates findings, resolves conflicts using repository evidence, and records paths for every material conclusion. If the host cannot start subagents, do the same scans directly and say so.
11
+ 3. **Monorepo coverage.** Discover root and nested project manifests, workspace definitions, package boundaries, framework configs, tests, and module entry points. Analyze each workspace separately, then summarize shared tooling and cross-workspace relationships. Never infer that root dependencies apply to every workspace.
12
+ 4. **Scan record.** Run the deterministic AI Workflow scan and combine it with the parallel analysis. Present technologies, architecture, patterns, modules, tests, quality commands, workspace topology, confidence, and source evidence. Do not include secret values. Ask for confirmation where conflicting or uncertain findings would affect installed guidance.
13
+ 5. **Recommendations.** Match catalog capabilities to the consolidated project profile. Show each recommendation with its purpose, evidence, resource types (skills, rules, agents, hooks, templates), required permissions, MCP server/source, and project-specific configuration steps. Recommend an MCP only when repository evidence supports its use; disclose its publisher/source, capabilities, access, transport, and credential needs. Never invent credentials or silently enable an MCP.
14
+ 6. **Selection.** Use the coding host's native selection widget when it supports one. First offer: install all recommendations, customize, or install none. For customization, show selectable blocks grouped by resource type, then the individual recommended items in each selected block. Include MCPs as a separate block. Keep dependencies visible and select required dependencies together. If the host has no selection widget, use an equivalent numbered prompt and accept all/custom/none explicitly.
15
+ 7. **Install and personalize.** Install only the confirmed selection. For “all”, install every recommendation. For custom selection, install only selected items and their declared dependencies. Personalize resources only when project evidence materially changes their instructions (for example, test commands, workspace/module layout, framework patterns, or architectural boundaries). Preserve existing files and secrets; report conflicts rather than overwriting. MCP setup must use the selected host's supported project-level configuration, preserve existing configuration, use environment-variable references for secrets, and request separate approval for permissions or external execution.
16
+ 8. **Completion.** Report installed resources by block, project-specific changes, skipped recommendations, conflicts, MCP authentication/trust steps, and validation performed. Recommend restarting/reloading the coding-agent session so newly installed resources are discovered. Do not claim a restart is required when the host loads changes immediately; distinguish host behavior where known.
17
+
18
+ ## Safety and compatibility
19
+
20
+ - Bootstrap installation alone must not activate optional project resources.
21
+ - Analysis agents must be read-only and must not inspect ignored, generated, dependency, secret, or credential files.
22
+ - The orchestrator treats agent output as untrusted evidence, checks paths and key findings against the repository, and does not merge unsupported claims into project policy.
23
+ - Selection and external permissions are separate decisions. Selecting an MCP is not permission to execute it or to expose credentials.
24
+ - Resource writes use target adapters, ownership tracking, safe paths, atomic updates, and conflict reporting.
25
+ - Host-specific capabilities (subagents, selection widgets, MCP configuration, restart semantics) are detected or described accurately; provide a safe manual fallback where unavailable.
26
+
27
+ ## Acceptance criteria
28
+
29
+ - **AIINIT-001:** A fresh install writes only the `ai-init` bootstrap and neutral AI Workflow state.
30
+ - **AIINIT-002:** The bootstrap asks about using a lower-cost model before analysis and never changes model settings itself.
31
+ - **AIINIT-003:** On supported hosts, three independent read-only analysis agents start in parallel and the orchestrator consolidates their evidence; unsupported hosts use a disclosed sequential fallback.
32
+ - **AIINIT-004:** Scans include separate summaries for every detected monorepo workspace and do not attribute root dependencies to all packages.
33
+ - **AIINIT-005:** Recommendations identify matching evidence, resource blocks, dependencies, MCP sources, and permissions.
34
+ - **AIINIT-006:** The selection UI offers install-all, custom, and none; custom selections can choose individual items by skills, rules, agents, hooks, templates, and MCPs.
35
+ - **AIINIT-007:** Installation activates only selected resources and required dependencies, personalizes only context-sensitive content, and preserves unowned or edited files.
36
+ - **AIINIT-008:** MCP setup is target-aware, merges safely with existing configuration, never stores literal secrets, and requires separate permission approval.
37
+ - **AIINIT-009:** Completion lists changes and recommends a session restart/reload to discover installed resources.
38
+ - **AIINIT-010:** CLI tests cover mixed-stack monorepos, all/custom/none selection, permission gating, MCP config merging, personalization, conflicts, and completion output.
39
+ - **AIINIT-011:** Custom selections can choose individual resource references by block, while `--select=all` selects every recommended capability and `--select=` selects none.
40
+
41
+ ## Delivery slices
42
+
43
+ 1. Bootstrap orchestration, host-native selection, model-cost prompt, and completion/reload guidance.
44
+ 2. Deterministic monorepo profiles, stack-specific capability recommendations, resource-level selection, and context-aware sync.
45
+ 3. MCP catalog provenance, per-target project configuration/merge support, credential placeholders, ownership, and config validation.
@@ -24,6 +24,7 @@
24
24
  - Selected resources must be customized using confirmed project facts.
25
25
  - `/ai-init` must orchestrate scan → recommendations → user selection → selected-resource sync and summarize what it added.
26
26
  - The bootstrap must invoke CLI operations through the package runner; it cannot assume `aiw` is installed globally or in the consuming project's dependencies.
27
+ - The complete onboarding behavior and acceptance criteria are defined in [`ai-init-workflow-spec.md`](./ai-init-workflow-spec.md).
27
28
 
28
29
  ### Development workflow
29
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multileaf/ai-workflow",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Portable spec-driven development workflow for AI coding agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,3 @@
1
+ # NestJS Engineer
2
+
3
+ Analyze the target NestJS module and its tests before proposing changes. Report relevant controllers, providers, dependency-injection relationships, DTO validation, and module boundaries with file evidence. Implement only within the assigned scope, preserve existing patterns, and return changed paths plus the exact workspace checks run. Do not access secrets or run migrations.
@@ -0,0 +1,3 @@
1
+ # Prisma Engineer
2
+
3
+ Inspect the active Prisma schema, generated-client usage, migrations, and data-access tests. Explain the impact of proposed schema/query changes and preserve the project's transaction and migration conventions. Never reset, migrate, or access a database without explicit approval. Return evidence paths, changed paths, and workspace-scoped validation results; do not include connection strings or credentials.
@@ -28,6 +28,9 @@ resources:
28
28
  - { id: documentation-maintenance, version: 0.1.0, path: skills/documentation-maintenance.md }
29
29
  - { id: adapter-development, version: 0.1.0, path: skills/adapter-development.md }
30
30
  - { id: skill-authoring, version: 0.1.0, path: skills/skill-authoring.md }
31
+ - { id: nestjs-development, version: 0.1.0, path: skills/nestjs-development.md }
32
+ - { id: prisma-data-access, version: 0.1.0, path: skills/prisma-data-access.md }
33
+ - { id: vite-frontend, version: 0.1.0, path: skills/vite-frontend.md }
31
34
  rules:
32
35
  - { id: language-policy, version: 0.1.0, path: rules/language-policy.md }
33
36
  - { id: tdd-policy, version: 0.1.0, path: rules/tdd-policy.md }
@@ -39,6 +42,7 @@ resources:
39
42
  - { id: documentation-policy, version: 0.1.0, path: rules/documentation-policy.md }
40
43
  - { id: self-validation, version: 0.1.0, path: rules/self-validation.md }
41
44
  - { id: completion-validation, version: 0.1.0, path: rules/completion-validation.md }
45
+ - { id: data-access-policy, version: 0.1.0, path: rules/data-access-policy.md }
42
46
  agents:
43
47
  - { id: requirements-analyst, version: 0.1.0, path: agents/requirements-analyst.md }
44
48
  - { id: technical-architect, version: 0.1.0, path: agents/technical-architect.md }
@@ -48,6 +52,8 @@ resources:
48
52
  - { id: security-reviewer, version: 0.1.0, path: agents/security-reviewer.md }
49
53
  - { id: adapter-engineer, version: 0.1.0, path: agents/adapter-engineer.md }
50
54
  - { id: documentation-writer, version: 0.1.0, path: agents/documentation-writer.md }
55
+ - { id: nestjs-engineer, version: 0.1.0, path: agents/nestjs-engineer.md }
56
+ - { id: prisma-engineer, version: 0.1.0, path: agents/prisma-engineer.md }
51
57
  hooks:
52
58
  - { id: pre-implementation, version: 0.1.0, path: hooks/pre-implementation.md }
53
59
  - { id: post-implementation, version: 0.1.0, path: hooks/post-implementation.md }
@@ -66,8 +72,62 @@ resources:
66
72
  - { id: security-review, version: 0.1.0, path: templates/security-review.md }
67
73
  - { id: migration-report, version: 0.1.0, path: templates/migration-report.md }
68
74
  provides:
69
- skills: [project-init, brainstorming, requirements-specification, technical-design, implementation-planning, tdd-development, verification, code-review, security-review, documentation-maintenance, adapter-development, skill-authoring]
70
- rules: [language-policy, tdd-policy, architecture-policy, quality-gates, security-policy, dependency-policy, generated-files, documentation-policy, self-validation, completion-validation]
71
- agents: [requirements-analyst, technical-architect, typescript-engineer, test-engineer, quality-reviewer, security-reviewer, adapter-engineer, documentation-writer]
75
+ skills:
76
+ [
77
+ project-init,
78
+ brainstorming,
79
+ requirements-specification,
80
+ technical-design,
81
+ implementation-planning,
82
+ tdd-development,
83
+ verification,
84
+ code-review,
85
+ security-review,
86
+ documentation-maintenance,
87
+ adapter-development,
88
+ skill-authoring,
89
+ nestjs-development,
90
+ prisma-data-access,
91
+ vite-frontend,
92
+ ]
93
+ rules:
94
+ [
95
+ language-policy,
96
+ tdd-policy,
97
+ architecture-policy,
98
+ quality-gates,
99
+ security-policy,
100
+ dependency-policy,
101
+ generated-files,
102
+ documentation-policy,
103
+ self-validation,
104
+ completion-validation,
105
+ data-access-policy,
106
+ ]
107
+ agents:
108
+ [
109
+ requirements-analyst,
110
+ technical-architect,
111
+ typescript-engineer,
112
+ test-engineer,
113
+ quality-reviewer,
114
+ security-reviewer,
115
+ adapter-engineer,
116
+ documentation-writer,
117
+ nestjs-engineer,
118
+ prisma-engineer,
119
+ ]
72
120
  hooks: [pre-implementation, post-implementation, pre-commit, post-change, pre-release]
73
- templates: [brainstorm, specification, technical-design, decision-record, implementation-plan, task, test-plan, verification-report, security-review, migration-report]
121
+ templates:
122
+ [
123
+ brainstorm,
124
+ specification,
125
+ technical-design,
126
+ decision-record,
127
+ implementation-plan,
128
+ task,
129
+ test-plan,
130
+ verification-report,
131
+ security-review,
132
+ migration-report,
133
+ ]
@@ -0,0 +1,7 @@
1
+ # Data Access Policy
2
+
3
+ - Follow the repository's existing data-access and transaction boundaries.
4
+ - Scope commands and tests to the workspace that owns the database schema or query.
5
+ - Review migration contents before applying them; never run destructive database commands without explicit approval.
6
+ - Keep credentials in the project's approved secret store or environment, never in source, logs, or generated AI instructions.
7
+ - Validate data-access changes with relevant tests and report any migration or compatibility impact.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: nestjs-development
3
+ description: Implement and review NestJS services using the repository's module boundaries and conventions.
4
+ ---
5
+
6
+ # NestJS Development
7
+
8
+ Inspect the relevant NestJS module before editing. Follow the project's established controller, provider, dependency-injection, guard, pipe, interceptor, and DTO patterns; do not introduce a new architectural layer without evidence or approval. Keep transport concerns in controllers and business behavior in the existing service/domain boundaries. Validate external input with the project's configured validation approach. Add focused unit or integration tests using the test runner configured for this workspace, and run that workspace's declared quality commands.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: prisma-data-access
3
+ description: Change Prisma schemas and data access safely within the repository's database conventions.
4
+ ---
5
+
6
+ # Prisma Data Access
7
+
8
+ Read the active Prisma schema, client setup, existing migrations, and callers before changing data access. Preserve naming, relation, transaction, and repository patterns already used by the project. Do not apply migrations, reset databases, or run destructive commands without explicit approval. When schema changes are selected, explain migration impact, add or update tests around affected queries, and use the exact Prisma and test commands declared by the relevant workspace. Never copy connection strings or credentials into generated instructions.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: vite-frontend
3
+ description: Build and verify Vite frontends using the repository's framework, module, and test conventions.
4
+ ---
5
+
6
+ # Vite Frontend
7
+
8
+ Inspect the Vite entry point, framework plugins, route/component structure, and workspace-local scripts before making changes. Follow the existing component, state, styling, accessibility, and API-client patterns. Keep frontend configuration scoped to the correct workspace. Add focused tests with the configured runner and verify the workspace's declared lint, typecheck, and build commands. Do not assume that a root-level command applies to every app in a monorepo.