@multileaf/ai-workflow 0.1.1 → 0.1.2

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,14 @@ All notable changes to this project are documented in this file. The format foll
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.2] - 2026-09-12
8
+
9
+ ### Fixed
10
+
11
+ - Keep first install limited to the `ai-init` bootstrap and neutral AI Workflow state; install optional capabilities only after the project scan and explicit user selection.
12
+ - Materialize only selected skills, rules, agents, hooks, and templates, and personalize supported rules with detected project commands.
13
+ - Allow non-interactive recommendation previews without silently selecting or installing capabilities.
14
+
7
15
  ## [0.1.1] - 2026-09-12
8
16
 
9
17
  ### Fixed
@@ -36,6 +44,7 @@ All notable changes to this project are documented in this file. The format foll
36
44
 
37
45
  - Path confinement, symlink protection, explicit permission gates, secret redaction, strict parsers, atomic migrations, and immutable execution checkpoints.
38
46
 
39
- [Unreleased]: https://github.com/MultiLeaf/aiw/compare/v0.1.1...HEAD
47
+ [Unreleased]: https://github.com/MultiLeaf/aiw/compare/v0.1.2...HEAD
48
+ [0.1.2]: https://github.com/MultiLeaf/aiw/compare/v0.1.1...v0.1.2
40
49
  [0.1.1]: https://github.com/MultiLeaf/aiw/releases/tag/v0.1.1
41
50
  [0.1.0]: https://github.com/MultiLeaf/aiw/releases/tag/v0.1.0
package/README.md CHANGED
@@ -25,13 +25,15 @@ The published package supports Node.js 18 or newer. Full development checks requ
25
25
  npx @multileaf/ai-workflow install --target codex
26
26
  ```
27
27
 
28
+ Then run `/ai-init` in Codex. The bootstrap scans the repository, explains project-specific recommendations, and waits for the user's selections before installing optional resources.
29
+
28
30
  Supported targets are `codex`, `claude`, `cursor`, `gemini`, `copilot`, and `universal`. Every command uses the same entry point:
29
31
 
30
32
  ```bash
31
33
  npx @multileaf/ai-workflow <command> <options>
32
34
  ```
33
35
 
34
- The initial installation creates workflow state and a target-specific `ai-init` skill. Run it in a repository that does not already use the reserved paths shown below: first installation currently writes the base ADR index and target `ai-init` file at those paths. Reinstallation detects existing AI Workflow state and preserves it.
36
+ The initial installation creates workflow state and a target-specific `ai-init` skill. Run it in a repository that does not already use the reserved bootstrap paths shown below: first installation currently writes the base ADR index and target `ai-init` file at those paths. Reinstallation detects existing AI Workflow state and preserves it.
35
37
 
36
38
  ## What gets created
37
39
 
@@ -40,15 +42,7 @@ For a Codex installation, the base layout is:
40
42
  ```text
41
43
  .agents/
42
44
  └── skills/
43
- ├── ai-init/SKILL.md
44
- ├── brainstorming/SKILL.md
45
- ├── requirements-specification/SKILL.md
46
- ├── tdd-development/SKILL.md
47
- └── ...
48
- .agents/agents/ # specialist agent definitions
49
- .agents/hooks/ # lifecycle guidance and compatibility resources
50
- .agents/rules/ # quality, security, and project rules
51
- .agents/templates/ # SDD artifact templates
45
+ └── ai-init/SKILL.md
52
46
  .context/
53
47
  └── adrs/
54
48
  └── INDEX.md
@@ -65,9 +59,9 @@ For a Codex installation, the base layout is:
65
59
  └── profile.yml
66
60
  ```
67
61
 
68
- `.aiw/` stores the provider-neutral manifest, profile, lockfile, checkpoints, and generated working artifacts. ADRs are stored under `.context/adrs/`. The initial `ai-init` resource is rendered directly to the selected target; installed neutral package resources can be rendered through adapters to paths such as `.claude/skills`, `.cursor/rules`, or `.github/copilot-instructions.md`.
62
+ `.aiw/` stores the provider-neutral manifest, profile, lockfile, checkpoints, bootstrap skill, selected resources, and generated working artifacts. ADRs are stored under `.context/adrs/`. The installer intentionally adds only `ai-init` and the base project state.
69
63
 
70
- The installer activates the bundled workflow package for the selected target, including its skills, rules, agents, hooks, and templates. `ai-init` guides the agent through scanning, recommendations, user selection, synchronization, and the SDD gates. Existing conflicting target resources stop first installation with an actionable error; review or move those files before retrying. Hook resources are rendered according to each adapter's native or compatibility support.
64
+ Run `/ai-init` in the selected coding agent to scan the project and review evidence-based recommendations for skills, rules, agents, hooks, and templates. AI Workflow installs only the resources the user selects, then generates project-specific quality guidance from the detected stack and tools. Existing user files are preserved; conflicts are reported instead of overwritten. Hook resources are rendered according to each adapter's native or compatibility support.
71
65
 
72
66
  The installer does not modify `.gitignore`. Add `.aiw/generated/` yourself if generated specifications, plans, and artifacts should remain local. Versioned files under `.aiw/checkpoints/` are designed to remain reviewable project history.
73
67
 
package/dist/adapter.js CHANGED
@@ -36,20 +36,20 @@ description: Initialize AI Workflow and run its specification-driven development
36
36
 
37
37
  # AI Workflow Initialization
38
38
 
39
- Set up the repository's AI-assisted development workflow. Do not stop after scanning or writing configuration: activate the installed skills, rules, agents, hooks, and templates, then guide the user through the project gates.
40
-
41
- Analyze the repository safely, confirm uncertain facts, and recommend project-specific capabilities.
39
+ This skill is the bootstrap for AI Workflow. The installer intentionally installs only this skill and the provider-neutral project state. Do not copy or activate the full resource package automatically.
42
40
 
43
41
  ## First-run setup
44
42
 
45
- 1. Inspect the repository and existing AI instructions. Preserve user-authored files and ask before replacing conflicts.
46
- 2. Run \`aiw scan\` and review detected facts and evidence. Confirm uncertain inferences with the user before treating them as project policy.
47
- 3. Run \`aiw recommend\`. Explain each recommendation and its permissions, then install only the capabilities the user selects. Do not approve network or shell permissions implicitly.
48
- 4. Run \`aiw sync\` to generate the selected project-specific resources. Resolve drift or conflicts with the user instead of overwriting them.
49
- 5. Show the available workflow resources and explain how to use them: brainstorming → specification → architecture decision when needed → implementation plan → implementation with tests → verification → traceability → review.
50
- 6. Before advancing each stage, run its matching \`aiw gate <stage>\`. Record validation evidence with \`aiw verify\` and link requirements, decisions, tasks, code, tests, and evidence with \`aiw trace\`.
43
+ 1. Inspect the repository's existing AI instructions and project structure. Preserve user-authored files and do not expose secrets.
44
+ 2. Run \`aiw scan\`. Explain the detected facts and their evidence; ask the user to confirm uncertain inferences before they influence generated content.
45
+ 3. Run \`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 \`aiw recommend --select=id,id\`; use \`aiw recommend --select=\` when all recommendations are declined. If at least one capability is selected, run \`aiw sync\`; when the user separately approved external network access, pass \`--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.
49
+
50
+ 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
51
 
52
- ## For each feature request
52
+ ## For each feature request after setup
53
53
 
54
54
  - Start with the brainstorming skill when scope, users, constraints, or assumptions are unclear.
55
55
  - Create and complete a specification with stable requirement IDs and Given/When/Then acceptance criteria; pass \`aiw gate specification\` before planning.
@@ -6,4 +6,4 @@ export type BootstrapResource = {
6
6
  path: string;
7
7
  content: string;
8
8
  };
9
- export declare function loadBootstrapResources(target: Target): BootstrapResource[];
9
+ export declare function loadBundledResources(target: Target): BootstrapResource[];
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import { renderResourcePath } from "./adapter.js";
5
5
  import { RESOURCE_TYPES, validatePackageContract } from "./package-contract.js";
6
6
  const resourceRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../resources");
7
- export function loadBootstrapResources(target) {
7
+ export function loadBundledResources(target) {
8
8
  const manifestPath = join(resourceRoot, "package.yaml");
9
9
  const manifest = readFileSync(manifestPath, "utf8");
10
10
  const contract = validatePackageContract(manifest, (path) => {
package/dist/generator.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { resolveGeneratedPath } from "./drift.js";
2
2
  export function generateProjectResources(profile, selected, write) {
3
- const linterCommand = profile.quality.linter?.command;
4
- const formatterCommand = profile.quality.formatter?.command;
5
- const testCommand = profile.testing?.command;
3
+ const linterCommand = safeInlineValue(profile.quality.linter?.command);
4
+ const formatterCommand = safeInlineValue(profile.quality.formatter?.command);
5
+ const testCommand = safeInlineValue(profile.testing?.command);
6
6
  if (selected.includes("typescript-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
8
  if (selected.includes("tdd-development"))
@@ -14,3 +14,13 @@ export function generateProjectResources(profile, selected, write) {
14
14
  write(resolveGeneratedPath("hooks/quality-check.md"), `# Quality Check Hook\n\nRun the available project quality commands before completion.${linterCommand ? `\n\n- Linter: \`${linterCommand}\`` : ""}${formatterCommand ? `\n- Formatter: \`${formatterCommand}\`` : ""}${testCommand ? `\n- Tests: \`${testCommand}\`` : ""}\n`);
15
15
  }
16
16
  }
17
+ function safeInlineValue(value) {
18
+ return value
19
+ ?.split("")
20
+ .map((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127 || character === "`"
21
+ ? " "
22
+ : character)
23
+ .join("")
24
+ .replace(/\s+/g, " ")
25
+ .slice(0, 180);
26
+ }
@@ -1,4 +1,9 @@
1
1
  import type { ProjectProfile } from "./profile.js";
2
+ import type { ResourceType } from "./package-contract.js";
3
+ export type RecommendedResource = {
4
+ type: ResourceType;
5
+ id: string;
6
+ };
2
7
  export type CapabilityRecommendation = {
3
8
  id: string;
4
9
  provider: string;
@@ -7,6 +12,8 @@ export type CapabilityRecommendation = {
7
12
  evidence: string[];
8
13
  permissions: string[];
9
14
  conflicts: string[];
15
+ resources: RecommendedResource[];
10
16
  };
11
17
  export declare function recommendCapabilities(profile: ProjectProfile): CapabilityRecommendation[];
18
+ export declare function resourcesForCapabilities(ids: string[]): RecommendedResource[];
12
19
  export declare function serializeRecommendations(items: CapabilityRecommendation[], selected?: string[]): string;
@@ -1,8 +1,73 @@
1
+ const local = (resources) => resources;
1
2
  const CATALOG = [
3
+ {
4
+ id: "brainstorming",
5
+ provider: "multileaf",
6
+ rationale: "A project-specific brainstorming flow helps clarify goals and constraints before implementation.",
7
+ permissions: [],
8
+ resources: local([
9
+ { type: "skills", id: "brainstorming" },
10
+ { type: "rules", id: "language-policy" },
11
+ { type: "templates", id: "brainstorm" },
12
+ ]),
13
+ matches: (profile) => hasProjectSignals(profile)
14
+ ? { evidence: projectEvidence(profile), confidence: 0.92 }
15
+ : undefined,
16
+ },
17
+ {
18
+ id: "requirements-specification",
19
+ provider: "multileaf",
20
+ rationale: "The project can use explicit requirements and acceptance criteria before changes are planned.",
21
+ permissions: [],
22
+ resources: local([
23
+ { type: "skills", id: "requirements-specification" },
24
+ { type: "agents", id: "requirements-analyst" },
25
+ { type: "templates", id: "specification" },
26
+ ]),
27
+ matches: (profile) => hasProjectSignals(profile)
28
+ ? { evidence: projectEvidence(profile), confidence: 0.9 }
29
+ : undefined,
30
+ },
31
+ {
32
+ id: "implementation-planning",
33
+ provider: "multileaf",
34
+ rationale: "A traceable implementation plan can connect project requirements, tests, risks, and validation.",
35
+ permissions: [],
36
+ resources: local([
37
+ { type: "skills", id: "implementation-planning" },
38
+ { type: "agents", id: "technical-architect" },
39
+ { type: "templates", id: "implementation-plan" },
40
+ ]),
41
+ matches: (profile) => hasProjectSignals(profile)
42
+ ? { evidence: projectEvidence(profile), confidence: 0.88 }
43
+ : undefined,
44
+ },
45
+ {
46
+ id: "code-review",
47
+ provider: "multileaf",
48
+ rationale: "A consistent review checklist helps validate changes against this repository's conventions.",
49
+ permissions: [],
50
+ resources: local([
51
+ { type: "skills", id: "code-review" },
52
+ { type: "agents", id: "quality-reviewer" },
53
+ ]),
54
+ matches: (profile) => profile.runtime.languages.length
55
+ ? { evidence: projectEvidence(profile), confidence: 0.86 }
56
+ : undefined,
57
+ },
2
58
  {
3
59
  id: "tdd-development",
4
60
  provider: "multileaf",
5
- rationale: "The project uses automated tests and should preserve test-first development.",
61
+ rationale: "The project uses automated tests and can apply test-first development with its existing runner.",
62
+ permissions: [],
63
+ resources: local([
64
+ { type: "skills", id: "tdd-development" },
65
+ { type: "rules", id: "tdd-policy" },
66
+ { type: "agents", id: "test-engineer" },
67
+ { type: "hooks", id: "pre-implementation" },
68
+ { type: "hooks", id: "post-implementation" },
69
+ { type: "templates", id: "test-plan" },
70
+ ]),
6
71
  matches: (profile) => profile.testing
7
72
  ? { evidence: [`testing:${profile.testing.name}`], confidence: 1 }
8
73
  : undefined,
@@ -10,7 +75,12 @@ const CATALOG = [
10
75
  {
11
76
  id: "typescript-quality",
12
77
  provider: "multileaf",
13
- rationale: "TypeScript is present and benefits from strict, maintainable code-quality rules.",
78
+ rationale: "TypeScript is detected; add its specialist and tailor quality guidance to the repository's tools.",
79
+ permissions: [],
80
+ resources: local([
81
+ { type: "agents", id: "typescript-engineer" },
82
+ { type: "rules", id: "quality-gates" },
83
+ ]),
14
84
  matches: (profile) => profile.runtime.languages.includes("typescript")
15
85
  ? { evidence: ["language:typescript"], confidence: 1 }
16
86
  : undefined,
@@ -18,7 +88,14 @@ const CATALOG = [
18
88
  {
19
89
  id: "vitest-testing",
20
90
  provider: "multileaf",
21
- rationale: "Vitest is configured and can be integrated into the verification workflow.",
91
+ rationale: "Vitest is configured and can be used by the verification skill and test specialist.",
92
+ permissions: [],
93
+ resources: local([
94
+ { type: "skills", id: "verification" },
95
+ { type: "agents", id: "test-engineer" },
96
+ { type: "templates", id: "test-plan" },
97
+ { type: "templates", id: "verification-report" },
98
+ ]),
22
99
  matches: (profile) => profile.testing?.name === "vitest"
23
100
  ? { evidence: ["testing:vitest"], confidence: 1 }
24
101
  : undefined,
@@ -26,7 +103,14 @@ const CATALOG = [
26
103
  {
27
104
  id: "react-best-practices",
28
105
  provider: "vercel-skills",
29
- rationale: "React is detected; framework-specific guidance can improve component design and performance.",
106
+ rationale: "React is detected; framework guidance can improve component design and performance.",
107
+ permissions: ["network:external"],
108
+ resources: local([
109
+ { type: "skills", id: "technical-design" },
110
+ { type: "agents", id: "technical-architect" },
111
+ { type: "rules", id: "architecture-policy" },
112
+ { type: "templates", id: "technical-design" },
113
+ ]),
30
114
  matches: (profile) => profile.frameworks.includes("react")
31
115
  ? { evidence: ["framework:react"], confidence: 1 }
32
116
  : undefined,
@@ -34,11 +118,34 @@ const CATALOG = [
34
118
  {
35
119
  id: "nextjs-development",
36
120
  provider: "multileaf",
37
- rationale: "Next.js is detected; project-specific conventions can guide routing and rendering decisions.",
121
+ rationale: "Next.js is detected; architecture guidance can follow the project's framework conventions.",
122
+ permissions: [],
123
+ resources: local([
124
+ { type: "skills", id: "technical-design" },
125
+ { type: "agents", id: "technical-architect" },
126
+ { type: "rules", id: "architecture-policy" },
127
+ { type: "templates", id: "technical-design" },
128
+ ]),
38
129
  matches: (profile) => profile.frameworks.includes("nextjs")
39
130
  ? { evidence: ["framework:nextjs"], confidence: 1 }
40
131
  : undefined,
41
132
  },
133
+ {
134
+ id: "security-review",
135
+ provider: "multileaf",
136
+ rationale: "The detected stack and package tooling benefit from project-aware security and dependency review.",
137
+ permissions: [],
138
+ resources: local([
139
+ { type: "skills", id: "security-review" },
140
+ { type: "rules", id: "security-policy" },
141
+ { type: "rules", id: "dependency-policy" },
142
+ { type: "agents", id: "security-reviewer" },
143
+ { type: "templates", id: "security-review" },
144
+ ]),
145
+ matches: (profile) => hasProjectSignals(profile)
146
+ ? { evidence: projectEvidence(profile), confidence: 0.83 }
147
+ : undefined,
148
+ },
42
149
  ];
43
150
  export function recommendCapabilities(profile) {
44
151
  return CATALOG.flatMap((capability) => {
@@ -46,18 +153,42 @@ export function recommendCapabilities(profile) {
46
153
  return match
47
154
  ? [
48
155
  {
49
- id: capability.id,
50
- provider: capability.provider,
156
+ ...capability,
51
157
  confidence: match.confidence,
52
- rationale: capability.rationale,
53
158
  evidence: match.evidence,
54
- permissions: capability.permissions ?? [],
55
159
  conflicts: [],
56
160
  },
57
161
  ]
58
162
  : [];
59
163
  });
60
164
  }
165
+ export function resourcesForCapabilities(ids) {
166
+ const selected = new Set(ids);
167
+ const resources = CATALOG.filter(({ id }) => selected.has(id)).flatMap(({ resources }) => resources);
168
+ return [
169
+ ...new Map(resources.map((resource) => [`${resource.type}/${resource.id}`, resource])).values(),
170
+ ];
171
+ }
61
172
  export function serializeRecommendations(items, selected = []) {
62
- return `schema: 1\nrecommendations:\n${items.map((item) => ` - id: ${item.id}\n provider: ${item.provider}\n confidence: ${item.confidence}\n selected: ${selected.includes(item.id)}\n evidence: [${item.evidence.map((evidence) => `"${evidence}"`).join(", ")}]\n rationale: ${item.rationale}`).join("\n")}\n`;
173
+ 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}`)
175
+ .join("\n")}\n`;
176
+ }
177
+ function hasProjectSignals(profile) {
178
+ return Boolean(profile.runtime.languages.length ||
179
+ profile.frameworks.length ||
180
+ profile.packageManager !== "unknown" ||
181
+ profile.testing ||
182
+ profile.quality.linter ||
183
+ profile.ci.length);
184
+ }
185
+ function projectEvidence(profile) {
186
+ return [
187
+ ...profile.runtime.languages.map((language) => `language:${language}`),
188
+ ...profile.frameworks.map((framework) => `framework:${framework}`),
189
+ ...(profile.packageManager === "unknown" ? [] : [`package-manager:${profile.packageManager}`]),
190
+ ...(profile.testing ? [`testing:${profile.testing.name}`] : []),
191
+ ...(profile.quality.linter ? [`linter:${profile.quality.linter.name}`] : []),
192
+ ...(profile.ci.length ? profile.ci.map((system) => `ci:${system}`) : []),
193
+ ];
63
194
  }
package/dist/workflow.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
2
2
  import { BASE_DIRECTORIES, WORKFLOW_DIRECTORY } from "./constants.js";
3
3
  import { detectTarget, isTarget, renderAiInit, renderAiInitPath, renderResourcePath, } from "./adapter.js";
4
- import { loadBootstrapResources } from "./bootstrap-resources.js";
4
+ import { loadBundledResources } from "./bundled-resources.js";
5
5
  import { assertSafeProjectPath, withSafeProjectWrites, writeProjectFilesAtomically, } from "./files.js";
6
6
  import { parseProjectProfile, profileProject } from "./profile.js";
7
7
  import { serializeOverrides } from "./confirmation.js";
8
8
  import { applyOverrides, parseOverrides } from "./confirmation.js";
9
9
  import { mergeFacts } from "./intelligence.js";
10
10
  import { createInterpretationRequest } from "./interpreter.js";
11
- import { recommendCapabilities, serializeRecommendations } from "./recommendations.js";
11
+ import { recommendCapabilities, resourcesForCapabilities, serializeRecommendations, } from "./recommendations.js";
12
12
  import { selectRecommendations } from "./selection.js";
13
13
  import { createInterface } from "node:readline/promises";
14
14
  import { stdin, stdout } from "node:process";
@@ -124,28 +124,26 @@ export async function runCommand(args, root, fs, services = {}) {
124
124
  output: `AI Workflow is already installed for target: ${active}. Existing state preserved.`,
125
125
  };
126
126
  }
127
- const bootstrapResources = loadBootstrapResources(target);
128
- const desiredResources = new Map();
127
+ const aiInitPath = join(root, renderAiInitPath(target));
128
+ const neutralAiInitPath = join(aiw, "resources/skills/ai-init.md");
129
+ const aiInitContent = renderAiInit(target);
130
+ const desiredResources = new Map([[aiInitPath, aiInitContent]]);
129
131
  const ownershipMetadata = new Map();
130
- bootstrapResources.forEach(({ type, id, path, content }) => {
131
- const targetPath = join(root, path);
132
- const neutralPath = join(root, renderResourcePath("universal", type, id));
133
- desiredResources.set(targetPath, content);
134
- desiredResources.set(neutralPath, content);
135
- ownershipMetadata.set(neutralPath, {
132
+ ownershipMetadata.set(aiInitPath, {
133
+ kind: "ai-init",
134
+ target,
135
+ resourceType: "skills",
136
+ resourceId: "ai-init",
137
+ });
138
+ if (neutralAiInitPath !== aiInitPath) {
139
+ desiredResources.set(neutralAiInitPath, aiInitContent);
140
+ ownershipMetadata.set(neutralAiInitPath, {
136
141
  kind: "neutral-resource",
137
142
  target: "universal",
138
- resourceType: type,
139
- resourceId: id,
143
+ resourceType: "skills",
144
+ resourceId: "ai-init",
140
145
  });
141
- if (targetPath !== neutralPath)
142
- ownershipMetadata.set(targetPath, {
143
- kind: "target-resource",
144
- target,
145
- resourceType: type,
146
- resourceId: id,
147
- });
148
- });
146
+ }
149
147
  const conflicts = [];
150
148
  const pendingWrites = new Map();
151
149
  const inspectDesiredFile = (path, content, allowExisting = false) => {
@@ -161,19 +159,6 @@ export async function runCommand(args, root, fs, services = {}) {
161
159
  };
162
160
  for (const [path, content] of desiredResources)
163
161
  inspectDesiredFile(path, content);
164
- if (conflicts.length)
165
- return {
166
- exitCode: 1,
167
- error: `Installation found existing target resources: ${conflicts.join(", ")}. Review and move them before retrying.`,
168
- };
169
- const aiInitPath = join(root, renderAiInitPath(target));
170
- ownershipMetadata.set(aiInitPath, {
171
- kind: "ai-init",
172
- target,
173
- resourceType: "skills",
174
- resourceId: "ai-init",
175
- });
176
- inspectDesiredFile(aiInitPath, renderAiInit(target));
177
162
  if (conflicts.length)
178
163
  return {
179
164
  exitCode: 1,
@@ -195,7 +180,7 @@ export async function runCommand(args, root, fs, services = {}) {
195
180
  exitCode: 1,
196
181
  error: `Installation found conflicting project state: ${conflicts.join(", ")}. Review and move it before retrying.`,
197
182
  };
198
- const ownedPaths = new Set([...desiredResources.keys(), aiInitPath]);
183
+ const ownedPaths = new Set(desiredResources.keys());
199
184
  if (pendingWrites.has(adrIndex))
200
185
  ownedPaths.add(adrIndex);
201
186
  const ownedFiles = [...ownedPaths]
@@ -207,7 +192,7 @@ export async function runCommand(args, root, fs, services = {}) {
207
192
  writeProjectFilesAtomically(fs, root, [...pendingWrites].map(([path, content]) => ({ path, content })), [...BASE_DIRECTORIES.map((directory) => join(aiw, directory)), join(root, ".context/adrs")]);
208
193
  return {
209
194
  exitCode: 0,
210
- output: `AI Workflow installed for target: ${target}; activated ${bootstrapResources.length} bundled workflow resources. Run the ai-init skill to scan the project, review recommendations, and begin the SDD gates.`,
195
+ output: `AI Workflow initialized for target: ${target} with only the ai-init skill. Run /ai-init to scan the project and choose which resources to install.`,
211
196
  };
212
197
  }
213
198
  if (command === "telemetry") {
@@ -409,7 +394,10 @@ export async function runCommand(args, root, fs, services = {}) {
409
394
  diagnostics.push({ path: ".aiw/resources/skills/ai-init.md", reason: aiInitDiagnostic });
410
395
  const targetAiPath = targetPath;
411
396
  resourceDestinations.add(targetAiPath);
412
- inspectMigrationDestination(targetAiPath, aiInitBytes);
397
+ if (targetAiPath === neutralAiInit)
398
+ fileChanges.set(targetAiPath, aiInitBytes);
399
+ else
400
+ inspectMigrationDestination(targetAiPath, aiInitBytes);
413
401
  const previousTargetEntries = previousTarget === "universal"
414
402
  ? []
415
403
  : inventory.filter((entry) => (entry.kind === "target-resource" || entry.kind === "ai-init") &&
@@ -667,18 +655,25 @@ export async function runCommand(args, root, fs, services = {}) {
667
655
  return { exitCode: 1, error: "Run `aiw install` first." };
668
656
  const profile = await (services.profiler?.profile(root) ?? profileProject(root));
669
657
  const recommendations = services.recommender?.recommend(profile) ?? recommendCapabilities(profile);
670
- const selectedArg = args.find((arg) => arg.startsWith("--select="))?.split("=", 2)[1];
671
- if (!selectedArg && !stdin.isTTY) {
672
- return {
673
- exitCode: 1,
674
- error: "Interactive selection requires a TTY. Use --select=id,id in non-interactive environments.",
675
- };
658
+ const selectedOption = args.find((arg) => arg.startsWith("--select="));
659
+ const selectedArg = selectedOption?.slice("--select=".length) ?? "";
660
+ let selection;
661
+ if (selectedOption !== undefined)
662
+ selection = await selectRecommendations(recommendations, async () => "n", selectedArg ? selectedArg.split(",") : []);
663
+ else if (!stdin.isTTY)
664
+ selection = [];
665
+ else {
666
+ const terminal = createInterface({ input: stdin, output: stdout });
667
+ selection = await selectRecommendations(recommendations, (question) => terminal.question(question));
668
+ terminal.close();
676
669
  }
677
- const terminal = createInterface({ input: stdin, output: stdout });
678
- const selection = await selectRecommendations(recommendations, (question) => terminal.question(question), selectedArg?.split(","));
679
- terminal.close();
680
670
  fs.write(join(aiw, "recommendations.yml"), serializeRecommendations(recommendations, selection));
681
- return { exitCode: 0, output: `${recommendations.length} recommendations generated.` };
671
+ return {
672
+ exitCode: 0,
673
+ output: selectedOption === undefined && !stdin.isTTY
674
+ ? `${recommendations.length} recommendations generated without selection; review .aiw/recommendations.yml, then record user choices with --select=id,id.`
675
+ : `${recommendations.length} recommendations generated.`,
676
+ };
682
677
  }
683
678
  if (command === "skills") {
684
679
  if (!fs.exists(join(aiw, "manifest.yml")))
@@ -770,6 +765,12 @@ export async function runCommand(args, root, fs, services = {}) {
770
765
  await installVercelSkill(services.externalSkills ?? nodeCommandExecutor, "vercel-labs/agent-skills", "vercel-react-best-practices", target);
771
766
  writeVercelSkillLock(fs, root, aiw, "vercel-labs/agent-skills", "vercel-react-best-practices");
772
767
  }
768
+ if (command === "sync") {
769
+ const result = syncSelectedResources(fs, root, aiw, profile, selected);
770
+ return result.error
771
+ ? { exitCode: 1, error: result.error }
772
+ : { exitCode: 0, output: result.output };
773
+ }
773
774
  const conflicts = [];
774
775
  generateProjectResources(profile, selected, (path, content) => {
775
776
  const destination = join(aiw, path);
@@ -1532,6 +1533,135 @@ function containsCredential(value, credential, visited = new Set()) {
1532
1533
  visited.add(value);
1533
1534
  return Object.values(value).some((entry) => containsCredential(entry, credential, visited));
1534
1535
  }
1536
+ function syncSelectedResources(fs, root, aiw, profile, selected) {
1537
+ const target = parseManifest(fs.read(join(aiw, "manifest.yml"))).target.active;
1538
+ const resourceSelections = resourcesForCapabilities(selected);
1539
+ const bundled = loadBundledResources(target);
1540
+ const bundleByKey = new Map(bundled.map((resource) => [`${resource.type}/${resource.id}`, resource]));
1541
+ const desired = new Map();
1542
+ const generatedFiles = new Map();
1543
+ const addOwned = (path, content, metadata) => {
1544
+ const previous = desired.get(path);
1545
+ if (previous && previous.content !== content)
1546
+ throw new Error(`Selected resources render conflicting content at ${path}`);
1547
+ desired.set(path, { content, metadata });
1548
+ };
1549
+ const addResource = (type, id, content) => {
1550
+ const neutralPath = join(root, renderResourcePath("universal", type, id));
1551
+ const targetPath = join(root, renderResourcePath(target, type, id));
1552
+ addOwned(neutralPath, content, {
1553
+ kind: "neutral-resource",
1554
+ target: "universal",
1555
+ resourceType: type,
1556
+ resourceId: id,
1557
+ });
1558
+ if (targetPath !== neutralPath)
1559
+ addOwned(targetPath, content, {
1560
+ kind: "target-resource",
1561
+ target,
1562
+ resourceType: type,
1563
+ resourceId: id,
1564
+ });
1565
+ };
1566
+ for (const reference of resourceSelections) {
1567
+ const key = `${reference.type}/${reference.id}`;
1568
+ const resource = bundleByKey.get(key);
1569
+ if (!resource)
1570
+ return { error: `Bundled resource is unavailable: ${key}` };
1571
+ addResource(reference.type, reference.id, personalizeBundledResource(reference.type, reference.id, resource.content, profile));
1572
+ }
1573
+ generateProjectResources(profile, selected, (path, content) => {
1574
+ generatedFiles.set(join(aiw, path), content);
1575
+ const rule = path.match(/^generated\/rules\/(project-quality|tdd-project-policy)\.md$/);
1576
+ if (rule)
1577
+ addResource("rules", rule[1], content);
1578
+ });
1579
+ const ownershipPath = join(aiw, "ownership.yml");
1580
+ if (fs.pathType?.(ownershipPath) !== "file")
1581
+ return {
1582
+ error: "AI Workflow ownership inventory is missing or unsafe; reinstall before syncing.",
1583
+ };
1584
+ const inventory = parseOwnership(fs.read(ownershipPath));
1585
+ const nextInventory = new Map(inventory.map((entry) => [entry.path, entry]));
1586
+ const changes = [];
1587
+ const conflicts = [];
1588
+ for (const [path, { content, metadata }] of desired) {
1589
+ assertSafeProjectPath(fs, root, path);
1590
+ const relativePath = relative(root, path).split(sep).join("/");
1591
+ const type = fs.pathType?.(path);
1592
+ const owned = nextInventory.get(relativePath);
1593
+ if (type === "missing") {
1594
+ changes.push({ path, content: Buffer.from(content, "utf8") });
1595
+ nextInventory.set(relativePath, createOwnedFile(relativePath, content, metadata));
1596
+ continue;
1597
+ }
1598
+ if (type !== "file" || !owned) {
1599
+ conflicts.push(relativePath);
1600
+ continue;
1601
+ }
1602
+ const actual = fs.read(path);
1603
+ if (createOwnedFile(relativePath, actual).checksum !== owned.checksum) {
1604
+ conflicts.push(relativePath);
1605
+ continue;
1606
+ }
1607
+ if (actual !== content)
1608
+ changes.push({ path, content: Buffer.from(content, "utf8") });
1609
+ nextInventory.set(relativePath, createOwnedFile(relativePath, content, metadata));
1610
+ }
1611
+ for (const [path, content] of generatedFiles) {
1612
+ assertSafeProjectPath(fs, root, path);
1613
+ const type = fs.pathType?.(path);
1614
+ if (type === "missing")
1615
+ changes.push({ path, content: Buffer.from(content, "utf8") });
1616
+ else if (type !== "file" || fs.read(path) !== content)
1617
+ conflicts.push(relative(root, path).split(sep).join("/"));
1618
+ }
1619
+ if (conflicts.length)
1620
+ return {
1621
+ error: `Sync preserved existing or modified project resources: ${[...new Set(conflicts)].sort().join(", ")}`,
1622
+ };
1623
+ const nextOwnership = serializeOwnership([...nextInventory.values()]);
1624
+ if (fs.read(ownershipPath) !== nextOwnership)
1625
+ changes.push({ path: ownershipPath, content: Buffer.from(nextOwnership, "utf8") });
1626
+ if (changes.length)
1627
+ applyProjectFileChanges(fs, root, changes);
1628
+ return {
1629
+ output: `Synchronized ${resourceSelections.length} selected bundled resources for ${target} and generated ${generatedFiles.size} project-specific files.`,
1630
+ };
1631
+ }
1632
+ function personalizeBundledResource(type, id, content, profile) {
1633
+ if (type !== "rules" || !["quality-gates", "tdd-policy", "dependency-policy"].includes(id))
1634
+ return content;
1635
+ const signals = [
1636
+ profile.runtime.languages.length ? `- Languages: ${profile.runtime.languages.join(", ")}` : "",
1637
+ profile.frameworks.length ? `- Frameworks: ${profile.frameworks.join(", ")}` : "",
1638
+ profile.testing ? `- Test runner: ${profile.testing.name}` : "",
1639
+ profile.testing?.command
1640
+ ? `- Test command: \`${safeProjectSignal(profile.testing.command)}\``
1641
+ : "",
1642
+ profile.quality.linter ? `- Linter: ${profile.quality.linter.name}` : "",
1643
+ profile.quality.linter?.command
1644
+ ? `- Lint command: \`${safeProjectSignal(profile.quality.linter.command)}\``
1645
+ : "",
1646
+ profile.quality.formatter ? `- Formatter: ${profile.quality.formatter.name}` : "",
1647
+ profile.quality.formatter?.command
1648
+ ? `- Format command: \`${safeProjectSignal(profile.quality.formatter.command)}\``
1649
+ : "",
1650
+ ].filter(Boolean);
1651
+ return signals.length
1652
+ ? `${content.trimEnd()}\n\n## Detected project context\n\n${signals.join("\n")}\n`
1653
+ : content;
1654
+ }
1655
+ function safeProjectSignal(value) {
1656
+ return value
1657
+ .split("")
1658
+ .map((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127 || character === "`"
1659
+ ? " "
1660
+ : character)
1661
+ .join("")
1662
+ .replace(/\s+/g, " ")
1663
+ .slice(0, 180);
1664
+ }
1535
1665
  function readSelectedRecommendations(fs, aiw) {
1536
1666
  if (!fs.exists(join(aiw, "recommendations.yml")))
1537
1667
  return [];
@@ -26,6 +26,8 @@ package/
26
26
 
27
27
  Packages may come from local paths, Git repositories, npm-compatible providers, Vercel Skills, or a future Multileaf registry.
28
28
 
29
+ The package distributed by AI Workflow is a catalog, not an automatic project install. `aiw install` writes only the target-specific `ai-init` bootstrap and neutral state. `/ai-init` scans the project, recommends matching capabilities, asks which to activate, and `aiw sync` renders only the selected resources. Supported rules and project-quality guidance are personalized from detected project tools; existing or user-edited files are preserved and reported as conflicts.
30
+
29
31
  Local directories and Git repositories are resolved with `aiw resolve --source=<source>`. A local source may be a package directory or its `package.yaml`; relative paths are resolved from the project. Git sources support `git+https`, HTTPS, SSH, and `file://` URLs. Git packages are cloned into an isolated temporary checkout, validated before the lockfile is changed, and removed after resolution. The lockfile records the selected provider and normalized source rather than trusting those fields from the fetched manifest. The legacy `--package=<path>` form remains available for direct manifest resolution.
30
32
 
31
33
  Vercel Skills is exposed through `aiw skills search`, `inspect`, `install`, `check`, and `update`. AIW delegates to the official `npx skills` CLI with argument arrays, requires explicit `network:external` permission for network-mutating/check operations, and records installed skills and upstream integrity in `.aiw/lock.yml`. Update output is inspected for upstream failure text because some CLI releases may report failures with a successful process exit code.
@@ -5,6 +5,7 @@
5
5
  ### Installation
6
6
 
7
7
  - The CLI must install with `npx @multileaf/ai-workflow install`.
8
+ - Initial installation must add only the target-specific `ai-init` bootstrap and neutral state; optional skills, rules, agents, hooks, and templates must not be activated before project analysis and user selection.
8
9
  - Installation must support Codex, Claude Code, Cursor, Gemini CLI, GitHub Copilot, and a universal target over time.
9
10
  - Installation must be idempotent and preserve user overrides.
10
11
  - Installation must create a neutral `.aiw/` source of truth.
@@ -21,6 +22,7 @@
21
22
  - Recommendations must explain their evidence, capabilities, permissions, and conflicts.
22
23
  - The user must select which capabilities to install.
23
24
  - Selected resources must be customized using confirmed project facts.
25
+ - `/ai-init` must orchestrate scan → recommendations → user selection → selected-resource sync and summarize what it added.
24
26
 
25
27
  ### Development workflow
26
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multileaf/ai-workflow",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Portable spec-driven development workflow for AI coding agents",
5
5
  "license": "MIT",
6
6
  "repository": {