@multileaf/ai-workflow 0.1.0 → 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.
@@ -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/scanner.js CHANGED
@@ -1,3 +1,4 @@
1
+ import ignore from "ignore";
1
2
  import { lstat, readFile, readdir } from "node:fs/promises";
2
3
  import { basename, relative, resolve } from "node:path";
3
4
  const DEFAULT_IGNORES = new Set([
@@ -11,48 +12,49 @@ const DEFAULT_IGNORES = new Set([
11
12
  "coverage",
12
13
  ]);
13
14
  const SENSITIVE_NAMES = /^(\.env(?:\..*)?|.*\.(pem|key|p12|pfx|secret))$/i;
14
- function matchesIgnore(path, patterns) {
15
- const name = basename(path);
16
- return patterns.some((pattern) => {
17
- const normalized = pattern.replace(/^\//, "").replace(/\/$/, "");
18
- if (normalized.includes("*")) {
19
- const expression = new RegExp(`^${normalized.split("*").map(escapeRegExp).join(".*")}$`);
20
- return expression.test(path) || expression.test(name);
21
- }
22
- return path === normalized || path.startsWith(`${normalized}/`) || name === normalized;
23
- });
24
- }
25
- function escapeRegExp(value) {
26
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15
+ function isIgnoredByProjectRules(absolute, isDirectory, scopes) {
16
+ let ignored = false;
17
+ for (const scope of scopes) {
18
+ const scopedPath = relative(scope.root, absolute).split("\\").join("/");
19
+ if (!scopedPath || scopedPath === ".." || scopedPath.startsWith("../"))
20
+ continue;
21
+ const result = scope.matcher.test(isDirectory ? `${scopedPath}/` : scopedPath);
22
+ if (result.ignored)
23
+ ignored = true;
24
+ else if (result.unignored)
25
+ ignored = false;
26
+ }
27
+ return ignored;
27
28
  }
28
- async function ignoredPatterns(root) {
29
+ async function addIgnoreFile(directory, scopes) {
29
30
  try {
30
- const content = await readFile(resolve(root, ".gitignore"), "utf8");
31
- return content
32
- .split(/\r?\n/)
33
- .map((line) => line.trim())
34
- .filter((line) => line && !line.startsWith("#"));
31
+ const content = await readFile(resolve(directory, ".gitignore"), "utf8");
32
+ return [...scopes, { root: directory, matcher: ignore().add(content) }];
35
33
  }
36
- catch {
37
- return [];
34
+ catch (error) {
35
+ if (error.code === "ENOENT")
36
+ return scopes;
37
+ throw error;
38
38
  }
39
39
  }
40
- async function collect(root, current, patterns, files) {
40
+ async function collect(root, current, inheritedScopes, files) {
41
+ const scopes = await addIgnoreFile(current, inheritedScopes);
41
42
  const entries = await readdir(current, { withFileTypes: true });
42
43
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
43
44
  const absolute = resolve(current, entry.name);
44
45
  const relativePath = relative(root, absolute).split("\\").join("/");
46
+ const isDirectory = entry.isDirectory();
45
47
  if (DEFAULT_IGNORES.has(entry.name) ||
46
48
  SENSITIVE_NAMES.test(entry.name) ||
47
- matchesIgnore(relativePath, patterns))
49
+ isIgnoredByProjectRules(absolute, isDirectory, scopes))
48
50
  continue;
49
51
  if (entry.isSymbolicLink()) {
50
52
  const link = await lstat(absolute);
51
53
  if (link.isSymbolicLink())
52
54
  continue;
53
55
  }
54
- if (entry.isDirectory())
55
- await collect(root, absolute, patterns, files);
56
+ if (isDirectory)
57
+ await collect(root, absolute, scopes, files);
56
58
  else if (entry.isFile())
57
59
  files.push(relativePath);
58
60
  }
@@ -78,6 +80,6 @@ function detectEvidence(files) {
78
80
  export async function scanProject(root) {
79
81
  const projectRoot = resolve(root);
80
82
  const files = [];
81
- await collect(projectRoot, projectRoot, await ignoredPatterns(projectRoot), files);
83
+ await collect(projectRoot, projectRoot, [], files);
82
84
  return { files, evidence: detectEvidence(files) };
83
85
  }
package/dist/types.d.ts CHANGED
@@ -11,12 +11,14 @@ export type FileSystem = {
11
11
  read(path: string): string;
12
12
  readBytes?(path: string): Uint8Array;
13
13
  write(path: string, content: string): void;
14
+ mkdirExclusive?(path: string): boolean;
14
15
  createExclusive?(path: string, content: string): boolean;
15
16
  writeBytes?(path: string, content: Uint8Array): void;
16
17
  mkdir(path: string): void;
17
18
  list?(path: string): string[];
18
19
  listFiles?(path: string): string[];
19
20
  remove?(path: string): void;
21
+ removeDirectory?(path: string): void;
20
22
  pathType?(path: string): PathType;
21
23
  realpath?(path: string): string;
22
24
  };