@multileaf/ai-workflow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/adapter-contract.d.ts +13 -0
- package/dist/adapter-contract.js +50 -0
- package/dist/adapter.d.ts +15 -0
- package/dist/adapter.js +62 -0
- package/dist/claude-layout.d.ts +9 -0
- package/dist/claude-layout.js +23 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +30 -0
- package/dist/codex-layout.d.ts +10 -0
- package/dist/codex-layout.js +24 -0
- package/dist/confirmation.d.ts +9 -0
- package/dist/confirmation.js +41 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +10 -0
- package/dist/context-cache.d.ts +8 -0
- package/dist/context-cache.js +17 -0
- package/dist/context-quality.d.ts +11 -0
- package/dist/context-quality.js +27 -0
- package/dist/context-retrieval.d.ts +3 -0
- package/dist/context-retrieval.js +13 -0
- package/dist/context-store.d.ts +9 -0
- package/dist/context-store.js +18 -0
- package/dist/copilot-layout.d.ts +9 -0
- package/dist/copilot-layout.js +24 -0
- package/dist/cursor-layout.d.ts +9 -0
- package/dist/cursor-layout.js +25 -0
- package/dist/dashboard.d.ts +37 -0
- package/dist/dashboard.js +251 -0
- package/dist/drift.d.ts +2 -0
- package/dist/drift.js +10 -0
- package/dist/evidence.d.ts +20 -0
- package/dist/evidence.js +32 -0
- package/dist/files.d.ts +2 -0
- package/dist/files.js +58 -0
- package/dist/gemini-layout.d.ts +9 -0
- package/dist/gemini-layout.js +23 -0
- package/dist/generator.d.ts +3 -0
- package/dist/generator.js +16 -0
- package/dist/intelligence.d.ts +2 -0
- package/dist/intelligence.js +13 -0
- package/dist/interpreter.d.ts +23 -0
- package/dist/interpreter.js +117 -0
- package/dist/lockfile.d.ts +12 -0
- package/dist/lockfile.js +59 -0
- package/dist/manifest.d.ts +15 -0
- package/dist/manifest.js +35 -0
- package/dist/migration.d.ts +22 -0
- package/dist/migration.js +50 -0
- package/dist/orchestration.d.ts +30 -0
- package/dist/orchestration.js +172 -0
- package/dist/organization-policy.d.ts +14 -0
- package/dist/organization-policy.js +47 -0
- package/dist/package-audit.d.ts +19 -0
- package/dist/package-audit.js +28 -0
- package/dist/package-contract.d.ts +30 -0
- package/dist/package-contract.js +93 -0
- package/dist/package-integrity.d.ts +8 -0
- package/dist/package-integrity.js +15 -0
- package/dist/package-updates.d.ts +11 -0
- package/dist/package-updates.js +32 -0
- package/dist/plugin-sdk.d.ts +20 -0
- package/dist/plugin-sdk.js +243 -0
- package/dist/policy-gate.d.ts +6 -0
- package/dist/policy-gate.js +20 -0
- package/dist/profile.d.ts +23 -0
- package/dist/profile.js +123 -0
- package/dist/providers.d.ts +20 -0
- package/dist/providers.js +67 -0
- package/dist/quality-gates.d.ts +2 -0
- package/dist/quality-gates.js +40 -0
- package/dist/recommendations.d.ts +12 -0
- package/dist/recommendations.js +63 -0
- package/dist/registry.d.ts +11 -0
- package/dist/registry.js +27 -0
- package/dist/scanner.d.ts +9 -0
- package/dist/scanner.js +83 -0
- package/dist/selection.d.ts +3 -0
- package/dist/selection.js +11 -0
- package/dist/self-validation.d.ts +9 -0
- package/dist/self-validation.js +9 -0
- package/dist/services.d.ts +42 -0
- package/dist/services.js +1 -0
- package/dist/team-configuration.d.ts +23 -0
- package/dist/team-configuration.js +104 -0
- package/dist/telemetry.d.ts +20 -0
- package/dist/telemetry.js +95 -0
- package/dist/token-usage.d.ts +9 -0
- package/dist/token-usage.js +38 -0
- package/dist/traceability.d.ts +23 -0
- package/dist/traceability.js +51 -0
- package/dist/types.d.ts +22 -0
- package/dist/types.js +1 -0
- package/dist/universal-layout.d.ts +9 -0
- package/dist/universal-layout.js +21 -0
- package/dist/vercel-skills.d.ts +17 -0
- package/dist/vercel-skills.js +49 -0
- package/dist/workflow.d.ts +4 -0
- package/dist/workflow.js +1130 -0
- package/docs/README.md +20 -0
- package/docs/adapter-model.md +16 -0
- package/docs/architecture.md +66 -0
- package/docs/backlog.md +93 -0
- package/docs/orchestration.md +50 -0
- package/docs/package-model.md +53 -0
- package/docs/plugin-authoring.md +31 -0
- package/docs/product-requirements.md +57 -0
- package/docs/product-vision.md +26 -0
- package/docs/project-intelligence.md +59 -0
- package/docs/quality.md +32 -0
- package/docs/release-backlog.md +7 -0
- package/docs/release-process.md +38 -0
- package/docs/security.md +18 -0
- package/docs/telemetry.md +28 -0
- package/docs/token-efficiency.md +16 -0
- package/package.json +76 -0
- package/resources/README.md +4 -0
- package/resources/agents/adapter-engineer.md +4 -0
- package/resources/agents/documentation-writer.md +4 -0
- package/resources/agents/quality-reviewer.md +4 -0
- package/resources/agents/requirements-analyst.md +4 -0
- package/resources/agents/security-reviewer.md +4 -0
- package/resources/agents/technical-architect.md +4 -0
- package/resources/agents/test-engineer.md +4 -0
- package/resources/agents/typescript-engineer.md +4 -0
- package/resources/hooks/post-change.md +4 -0
- package/resources/hooks/post-implementation.md +4 -0
- package/resources/hooks/pre-commit.md +4 -0
- package/resources/hooks/pre-implementation.md +4 -0
- package/resources/hooks/pre-release.md +4 -0
- package/resources/package.yaml +36 -0
- package/resources/rules/architecture-policy.md +4 -0
- package/resources/rules/completion-validation.md +19 -0
- package/resources/rules/dependency-policy.md +4 -0
- package/resources/rules/documentation-policy.md +4 -0
- package/resources/rules/generated-files.md +4 -0
- package/resources/rules/language-policy.md +4 -0
- package/resources/rules/quality-gates.md +4 -0
- package/resources/rules/security-policy.md +4 -0
- package/resources/rules/self-validation.md +12 -0
- package/resources/rules/tdd-policy.md +4 -0
- package/resources/skills/adapter-development.md +9 -0
- package/resources/skills/brainstorming.md +9 -0
- package/resources/skills/code-review.md +9 -0
- package/resources/skills/documentation-maintenance.md +9 -0
- package/resources/skills/implementation-planning.md +9 -0
- package/resources/skills/project-init.md +9 -0
- package/resources/skills/requirements-specification.md +9 -0
- package/resources/skills/security-review.md +9 -0
- package/resources/skills/skill-authoring.md +9 -0
- package/resources/skills/tdd-development.md +9 -0
- package/resources/skills/technical-design.md +9 -0
- package/resources/skills/verification.md +9 -0
- package/resources/templates/brainstorm.md +16 -0
- package/resources/templates/decision-record.md +12 -0
- package/resources/templates/implementation-plan.md +12 -0
- package/resources/templates/migration-report.md +14 -0
- package/resources/templates/security-review.md +14 -0
- package/resources/templates/specification.md +23 -0
- package/resources/templates/task.md +14 -0
- package/resources/templates/technical-design.md +14 -0
- package/resources/templates/test-plan.md +12 -0
- package/resources/templates/verification-report.md +12 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function compareVersions(left, right) {
|
|
2
|
+
const a = left.split(".").map(Number);
|
|
3
|
+
const b = right.split(".").map(Number);
|
|
4
|
+
for (let index = 0; index < 3; index++) {
|
|
5
|
+
const difference = (a[index] ?? 0) - (b[index] ?? 0);
|
|
6
|
+
if (difference)
|
|
7
|
+
return difference;
|
|
8
|
+
}
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
export function planPackageUpdate(current, candidate, target, lockedVersions = new Map()) {
|
|
12
|
+
const supportedTargets = candidate.targets;
|
|
13
|
+
if (supportedTargets &&
|
|
14
|
+
!supportedTargets.includes(target) &&
|
|
15
|
+
!supportedTargets.includes("universal"))
|
|
16
|
+
return {
|
|
17
|
+
status: "incompatible",
|
|
18
|
+
reason: `${candidate.id} does not support target '${target}'.`,
|
|
19
|
+
};
|
|
20
|
+
const dependencyConflict = candidate.dependencies.find((dependency) => {
|
|
21
|
+
const locked = lockedVersions.get(dependency);
|
|
22
|
+
return locked !== undefined && compareVersions(locked, candidate.version) !== 0;
|
|
23
|
+
});
|
|
24
|
+
if (dependencyConflict)
|
|
25
|
+
return {
|
|
26
|
+
status: "conflict",
|
|
27
|
+
reason: `Dependency '${dependencyConflict}' is locked at an incompatible version.`,
|
|
28
|
+
};
|
|
29
|
+
if (current && compareVersions(candidate.version, current.version) <= 0)
|
|
30
|
+
return { status: "current", reason: `${candidate.id} is already at ${current.version}.` };
|
|
31
|
+
return { status: "update" };
|
|
32
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FileSystem } from "./types.js";
|
|
2
|
+
import { type PackageContract } from "./package-contract.js";
|
|
3
|
+
export type PluginDefinition = {
|
|
4
|
+
id: string;
|
|
5
|
+
version: string;
|
|
6
|
+
description: string;
|
|
7
|
+
};
|
|
8
|
+
export type PluginScaffold = {
|
|
9
|
+
projectRoot: string;
|
|
10
|
+
root: string;
|
|
11
|
+
files: Readonly<Record<string, string>>;
|
|
12
|
+
};
|
|
13
|
+
export type DeepReadonly<T> = T extends (...args: never[]) => unknown ? T : T extends object ? {
|
|
14
|
+
readonly [Key in keyof T]: DeepReadonly<T[Key]>;
|
|
15
|
+
} : T;
|
|
16
|
+
export declare function definePlugin(contract: PackageContract): DeepReadonly<PackageContract>;
|
|
17
|
+
export declare function createPluginScaffold(projectRoot: string, directory: string, definition: PluginDefinition): PluginScaffold;
|
|
18
|
+
export declare function writePluginScaffold(fs: FileSystem, scaffold: PluginScaffold): void;
|
|
19
|
+
export declare function validatePluginDirectory(fs: FileSystem, projectRoot: string, directory: string): PackageContract;
|
|
20
|
+
export declare function resolvePluginDirectory(projectRoot: string, directory: string): string;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { validatePackageContract } from "./package-contract.js";
|
|
3
|
+
export function definePlugin(contract) {
|
|
4
|
+
return deepFreeze(contract);
|
|
5
|
+
}
|
|
6
|
+
export function createPluginScaffold(projectRoot, directory, definition) {
|
|
7
|
+
assertPluginDefinition(definition);
|
|
8
|
+
const root = resolvePluginDirectory(projectRoot, directory);
|
|
9
|
+
const skillId = definition.id.split("/").at(-1) ?? definition.id;
|
|
10
|
+
const skillPath = `skills/${skillId}/SKILL.md`;
|
|
11
|
+
return {
|
|
12
|
+
projectRoot: resolve(projectRoot),
|
|
13
|
+
root,
|
|
14
|
+
files: {
|
|
15
|
+
"package.yaml": pluginManifest(definition, skillId, skillPath),
|
|
16
|
+
[skillPath]: `---\nname: ${skillId}\ndescription: ${JSON.stringify(definition.description)}\n---\n\n# ${title(skillId)}\n\nDescribe the focused workflow this skill provides.\n`,
|
|
17
|
+
"README.md": `# ${definition.id}\n\n${definition.description}\n\nValidate this plugin from the project root with \`aiw plugin validate --directory=${shellQuote(directory)}\`.\n`,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function writePluginScaffold(fs, scaffold) {
|
|
22
|
+
const entries = Object.entries(scaffold.files).map(([path, content]) => {
|
|
23
|
+
if (!path || path === "." || hasUnsafePathSegment(path) || isAbsolute(path))
|
|
24
|
+
throw new Error(`Plugin scaffold path must be a safe relative file path: ${path}`);
|
|
25
|
+
if (typeof content !== "string")
|
|
26
|
+
throw new Error(`Plugin scaffold content must be text: ${path}`);
|
|
27
|
+
const destination = resolve(scaffold.root, path);
|
|
28
|
+
if (!isInside(scaffold.root, destination))
|
|
29
|
+
throw new Error(`Plugin scaffold path must stay inside its root: ${path}`);
|
|
30
|
+
return { content, destination, path };
|
|
31
|
+
});
|
|
32
|
+
const destinations = entries.map(({ destination }) => destination);
|
|
33
|
+
if (new Set(destinations).size !== destinations.length)
|
|
34
|
+
throw new Error("Plugin scaffold paths must resolve to unique destinations.");
|
|
35
|
+
const destinationSet = new Set(destinations);
|
|
36
|
+
if (destinations.some((destination) => ancestorsUntil(destination, scaffold.root).some((ancestor) => ancestor !== scaffold.root && destinationSet.has(ancestor))))
|
|
37
|
+
throw new Error("Plugin scaffold cannot use a planned file as a directory.");
|
|
38
|
+
assertNoSymlinkTraversal(fs, scaffold.projectRoot, scaffold.root);
|
|
39
|
+
const ancestors = new Set(destinations.flatMap((path) => ancestorsUntil(path, scaffold.root)));
|
|
40
|
+
const obstructed = [...ancestors].find((path) => {
|
|
41
|
+
const type = pathType(fs, path);
|
|
42
|
+
return type !== "missing" && type !== "directory";
|
|
43
|
+
});
|
|
44
|
+
if (obstructed)
|
|
45
|
+
throw new Error(`Plugin scaffold has an unsafe or obstructed ancestor: ${obstructed}`);
|
|
46
|
+
const conflict = destinations.find((path) => pathType(fs, path) !== "missing");
|
|
47
|
+
if (conflict)
|
|
48
|
+
throw new Error(`Plugin scaffold would overwrite an existing file: ${conflict}`);
|
|
49
|
+
fs.mkdir(scaffold.root);
|
|
50
|
+
entries.forEach(({ destination, content }) => fs.write(destination, content));
|
|
51
|
+
}
|
|
52
|
+
export function validatePluginDirectory(fs, projectRoot, directory) {
|
|
53
|
+
const pluginRoot = resolvePluginDirectory(projectRoot, directory);
|
|
54
|
+
assertNoSymlinkTraversal(fs, resolve(projectRoot), pluginRoot);
|
|
55
|
+
if (pathType(fs, pluginRoot) !== "directory")
|
|
56
|
+
throw new Error(`Plugin directory does not exist or is not a directory: ${pluginRoot}`);
|
|
57
|
+
const realProjectRoot = fs.realpath?.(resolve(projectRoot)) ?? resolve(projectRoot);
|
|
58
|
+
const realPluginRoot = fs.realpath?.(pluginRoot) ?? pluginRoot;
|
|
59
|
+
if (!isInside(realProjectRoot, realPluginRoot))
|
|
60
|
+
throw new Error("Plugin directory resolves outside the project.");
|
|
61
|
+
const manifestPath = resolve(pluginRoot, "package.yaml");
|
|
62
|
+
if (pathType(fs, manifestPath) !== "file")
|
|
63
|
+
throw new Error(`Plugin manifest does not exist: ${manifestPath}`);
|
|
64
|
+
const content = fs.read(manifestPath);
|
|
65
|
+
assertPluginManifestSyntax(content);
|
|
66
|
+
return validatePackageContract(content, (path) => {
|
|
67
|
+
if (isAbsolute(path))
|
|
68
|
+
return false;
|
|
69
|
+
const resource = resolve(pluginRoot, path);
|
|
70
|
+
if (!isInside(pluginRoot, resource) || pathType(fs, resource) !== "file")
|
|
71
|
+
return false;
|
|
72
|
+
const realResource = fs.realpath?.(resource) ?? resource;
|
|
73
|
+
return isInside(realPluginRoot, realResource);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function resolvePluginDirectory(projectRoot, directory) {
|
|
77
|
+
if (!directory ||
|
|
78
|
+
isAbsolute(directory) ||
|
|
79
|
+
hasUnsafePathSegment(directory) ||
|
|
80
|
+
resolve(projectRoot, directory) === resolve(projectRoot))
|
|
81
|
+
throw new Error("Plugin directory must be a non-empty project-relative path.");
|
|
82
|
+
const root = resolve(projectRoot);
|
|
83
|
+
const destination = resolve(root, directory);
|
|
84
|
+
if (!isInside(root, destination))
|
|
85
|
+
throw new Error("Plugin directory must stay inside the project.");
|
|
86
|
+
return destination;
|
|
87
|
+
}
|
|
88
|
+
function assertPluginDefinition(definition) {
|
|
89
|
+
if (!/^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/.test(definition.id))
|
|
90
|
+
throw new Error("Plugin id must use the owner/name format with lowercase letters and hyphens.");
|
|
91
|
+
if (!/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(definition.version))
|
|
92
|
+
throw new Error("Plugin version must be an exact semantic version.");
|
|
93
|
+
if (!definition.description.trim() || /[\r\n\u0085\u2028\u2029]/.test(definition.description))
|
|
94
|
+
throw new Error("Plugin description must be a non-empty single line.");
|
|
95
|
+
}
|
|
96
|
+
function assertPluginManifestSyntax(content) {
|
|
97
|
+
if ([...content].some((character) => character !== "\n" && isUnsafeCharacter(character)))
|
|
98
|
+
throw new Error("Plugin manifest contains unsupported control characters.");
|
|
99
|
+
const lines = content.split("\n");
|
|
100
|
+
if (lines.at(-1) === "")
|
|
101
|
+
lines.pop();
|
|
102
|
+
if (!lines.length || lines.some((line) => !isValidManifestLine(line)))
|
|
103
|
+
throw new Error("Plugin manifest syntax is invalid.");
|
|
104
|
+
const topLevel = [...content.matchAll(/^([a-z_]+):/gm)].map((match) => match[1]);
|
|
105
|
+
const allowed = new Set([
|
|
106
|
+
"schema",
|
|
107
|
+
"id",
|
|
108
|
+
"version",
|
|
109
|
+
"provider",
|
|
110
|
+
"source",
|
|
111
|
+
"dependencies",
|
|
112
|
+
"permissions",
|
|
113
|
+
"provenance",
|
|
114
|
+
"resources",
|
|
115
|
+
"policies",
|
|
116
|
+
"engines",
|
|
117
|
+
"provides",
|
|
118
|
+
"targets",
|
|
119
|
+
]);
|
|
120
|
+
if (topLevel.some((key) => !allowed.has(key)) || new Set(topLevel).size !== topLevel.length)
|
|
121
|
+
throw new Error("Plugin manifest has unknown or duplicate top-level fields.");
|
|
122
|
+
const id = content.match(/^id:\s*(.*)$/m)?.[1]?.trim() ?? "";
|
|
123
|
+
if (!/^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/.test(id))
|
|
124
|
+
throw new Error("Plugin manifest id must use the lowercase owner/name format.");
|
|
125
|
+
const version = content.match(/^version:\s*(.*)$/m)?.[1]?.trim() ?? "";
|
|
126
|
+
if (!/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(version))
|
|
127
|
+
throw new Error("Plugin manifest version must be an exact semantic version.");
|
|
128
|
+
for (const key of ["dependencies", "permissions", "targets"]) {
|
|
129
|
+
const line = content.match(new RegExp(`^${key}:\\s*(.*)$`, "m"))?.[1];
|
|
130
|
+
if (line !== undefined) {
|
|
131
|
+
const list = line.trim();
|
|
132
|
+
const body = list.slice(1, -1);
|
|
133
|
+
if (!list.startsWith("[") ||
|
|
134
|
+
!list.endsWith("]") ||
|
|
135
|
+
body.includes("[") ||
|
|
136
|
+
body.includes("]") ||
|
|
137
|
+
(body && body.split(",").some((entry) => !isPlainManifestValue(entry.trim()))))
|
|
138
|
+
throw new Error(`Plugin manifest field '${key}' must be an inline list.`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const type of ["skills", "rules", "agents", "hooks", "templates"]) {
|
|
142
|
+
if ((content.match(new RegExp(`^ ${type}:`, "gm")) ?? []).length !== 1)
|
|
143
|
+
throw new Error(`Plugin manifest resource section '${type}' must occur exactly once.`);
|
|
144
|
+
}
|
|
145
|
+
const resourceEntries = [
|
|
146
|
+
...content.matchAll(/^\s{4}- \{ id: ([^,]+), version: ([^,]+), path: ([^ }]+) \}$/gm),
|
|
147
|
+
];
|
|
148
|
+
const identities = new Set();
|
|
149
|
+
for (const entry of resourceEntries) {
|
|
150
|
+
const [, resourceId, resourceVersion, resourcePath] = entry;
|
|
151
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(resourceId))
|
|
152
|
+
throw new Error("Plugin resource id must use lowercase letters and hyphens.");
|
|
153
|
+
if (!/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(resourceVersion))
|
|
154
|
+
throw new Error("Plugin resource version must be an exact semantic version.");
|
|
155
|
+
if (!isPlainManifestValue(resourcePath))
|
|
156
|
+
throw new Error("Plugin resource path is invalid.");
|
|
157
|
+
const identity = `${resourceId}@${resourceVersion}`;
|
|
158
|
+
if (identities.has(identity))
|
|
159
|
+
throw new Error(`Duplicate plugin resource: ${identity}`);
|
|
160
|
+
identities.add(identity);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function isValidManifestLine(line) {
|
|
164
|
+
if (!line.trim() || /^#[^\r\n]*$/.test(line))
|
|
165
|
+
return true;
|
|
166
|
+
if (/^[a-z_]+:$/.test(line))
|
|
167
|
+
return true;
|
|
168
|
+
if (/^(schema|id|version|provider|source): [^\s].*$/.test(line))
|
|
169
|
+
return isPlainManifestValue(line.slice(line.indexOf(":") + 1).trim());
|
|
170
|
+
if (/^(dependencies|permissions|targets): \[.*\]$/.test(line))
|
|
171
|
+
return true;
|
|
172
|
+
if (/^\s{2}(source|checksum|ai_workflow): [^\s].*$/.test(line))
|
|
173
|
+
return isPlainManifestValue(line.slice(line.indexOf(":") + 1).trim());
|
|
174
|
+
if (/^\s{2}(skills|rules|agents|hooks|templates):(?: \[\])?$/.test(line))
|
|
175
|
+
return true;
|
|
176
|
+
if (/^\s{4}- \{ id: [^,]+, version: [^,]+, path: [^ }]+ \}$/.test(line))
|
|
177
|
+
return true;
|
|
178
|
+
if (/^\s{2}- \{ id: [^,]+, value: [^ }]+ \}$/.test(line))
|
|
179
|
+
return true;
|
|
180
|
+
return /^\s{4}(skills|rules|agents|hooks|templates): \[.*\]$/.test(line);
|
|
181
|
+
}
|
|
182
|
+
function isPlainManifestValue(value) {
|
|
183
|
+
if (!value || value.includes("[") || value.includes("]") || /[{}#,]/.test(value))
|
|
184
|
+
return false;
|
|
185
|
+
if (value.startsWith('"') || value.endsWith('"') || value.startsWith("'") || value.endsWith("'"))
|
|
186
|
+
return false;
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
function pluginManifest(definition, skillId, skillPath) {
|
|
190
|
+
return `schema: 1\nid: ${definition.id}\nversion: ${definition.version}\nprovider: local\nsource: .\ndependencies: []\npermissions: []\nprovenance:\n source: .\nresources:\n skills:\n - { id: ${skillId}, version: ${definition.version}, path: ${skillPath} }\n rules: []\n agents: []\n hooks: []\n templates: []\n`;
|
|
191
|
+
}
|
|
192
|
+
function assertNoSymlinkTraversal(fs, projectRoot, destination) {
|
|
193
|
+
let current = destination;
|
|
194
|
+
while (isInside(projectRoot, current)) {
|
|
195
|
+
if (pathType(fs, current) === "symlink")
|
|
196
|
+
throw new Error(`Plugin path cannot traverse a symbolic link: ${current}`);
|
|
197
|
+
if (current === projectRoot)
|
|
198
|
+
return;
|
|
199
|
+
current = dirname(current);
|
|
200
|
+
}
|
|
201
|
+
throw new Error("Plugin path must stay inside the project.");
|
|
202
|
+
}
|
|
203
|
+
function ancestorsUntil(path, root) {
|
|
204
|
+
const ancestors = [];
|
|
205
|
+
let current = dirname(path);
|
|
206
|
+
while (isInside(root, current)) {
|
|
207
|
+
ancestors.push(current);
|
|
208
|
+
if (current === root)
|
|
209
|
+
break;
|
|
210
|
+
current = dirname(current);
|
|
211
|
+
}
|
|
212
|
+
return ancestors;
|
|
213
|
+
}
|
|
214
|
+
function pathType(fs, path) {
|
|
215
|
+
return fs.pathType ? fs.pathType(path) : fs.exists(path) ? "file" : "missing";
|
|
216
|
+
}
|
|
217
|
+
function isInside(root, destination) {
|
|
218
|
+
const path = relative(root, destination);
|
|
219
|
+
return path === "" || (path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path));
|
|
220
|
+
}
|
|
221
|
+
function deepFreeze(value, visited = new WeakSet()) {
|
|
222
|
+
if (!value || typeof value !== "object" || visited.has(value))
|
|
223
|
+
return value;
|
|
224
|
+
visited.add(value);
|
|
225
|
+
Object.values(value).forEach((entry) => deepFreeze(entry, visited));
|
|
226
|
+
return Object.freeze(value);
|
|
227
|
+
}
|
|
228
|
+
function title(value) {
|
|
229
|
+
return value
|
|
230
|
+
.split("-")
|
|
231
|
+
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
|
|
232
|
+
.join(" ");
|
|
233
|
+
}
|
|
234
|
+
function hasUnsafePathSegment(value) {
|
|
235
|
+
return [...value].some(isUnsafeCharacter) || value.split(/[\\/]/).includes("..");
|
|
236
|
+
}
|
|
237
|
+
function isUnsafeCharacter(value) {
|
|
238
|
+
const code = value.codePointAt(0) ?? 0;
|
|
239
|
+
return code <= 31 || code === 127 || code === 133 || code === 8232 || code === 8233;
|
|
240
|
+
}
|
|
241
|
+
function shellQuote(value) {
|
|
242
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
243
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type OrganizationPolicy, type PackagePolicySubject } from "./organization-policy.js";
|
|
2
|
+
export interface PolicyTracking {
|
|
3
|
+
isTracked(root: string, path: string): boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare const gitPolicyTracking: PolicyTracking;
|
|
6
|
+
export declare function enforcePolicyGate(policy: OrganizationPolicy, subjects: readonly PackagePolicySubject[]): void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { enforceOrganizationPolicy, } from "./organization-policy.js";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { relative } from "node:path";
|
|
4
|
+
export const gitPolicyTracking = {
|
|
5
|
+
isTracked(root, path) {
|
|
6
|
+
try {
|
|
7
|
+
execFileSync("git", ["ls-files", "--error-unmatch", "--", relative(root, path)], {
|
|
8
|
+
cwd: root,
|
|
9
|
+
stdio: "ignore",
|
|
10
|
+
});
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
export function enforcePolicyGate(policy, subjects) {
|
|
19
|
+
subjects.forEach((subject) => enforceOrganizationPolicy(policy, subject));
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type EvidenceFact } from "./evidence.js";
|
|
2
|
+
export type Tool = {
|
|
3
|
+
name: string;
|
|
4
|
+
command?: string;
|
|
5
|
+
};
|
|
6
|
+
export type ProjectProfile = {
|
|
7
|
+
runtime: {
|
|
8
|
+
languages: string[];
|
|
9
|
+
};
|
|
10
|
+
frameworks: string[];
|
|
11
|
+
packageManager: string;
|
|
12
|
+
quality: {
|
|
13
|
+
linter?: Tool;
|
|
14
|
+
formatter?: Tool;
|
|
15
|
+
typecheck?: Tool;
|
|
16
|
+
};
|
|
17
|
+
testing?: Tool;
|
|
18
|
+
ci: string[];
|
|
19
|
+
workspaces: string[];
|
|
20
|
+
facts: EvidenceFact[];
|
|
21
|
+
};
|
|
22
|
+
export declare function parseProjectProfile(content: string): ProjectProfile;
|
|
23
|
+
export declare function profileProject(root: string): Promise<ProjectProfile>;
|
package/dist/profile.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { scanProject } from "./scanner.js";
|
|
4
|
+
import { buildFacts } from "./evidence.js";
|
|
5
|
+
function scalar(content, key) {
|
|
6
|
+
const value = content.match(new RegExp(`^\\s*${key}:\\s*(.+)$`, "m"))?.[1]?.trim();
|
|
7
|
+
return value && value !== "unknown" ? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
function list(content, key) {
|
|
10
|
+
const value = content.match(new RegExp(`^${key}:\\s*\\[([^\\]]*)\\]$`, "m"))?.[1] ?? "";
|
|
11
|
+
return value
|
|
12
|
+
.split(",")
|
|
13
|
+
.map((item) => item.trim())
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
export function parseProjectProfile(content) {
|
|
17
|
+
if (!/^status:\s*scanned$/m.test(content))
|
|
18
|
+
throw new Error("Project profile has not been scanned.");
|
|
19
|
+
const languages = list(content, " languages");
|
|
20
|
+
const frameworks = list(content, "frameworks");
|
|
21
|
+
let packageManager = scalar(content, "package_manager") ?? "unknown";
|
|
22
|
+
const confirmed = [
|
|
23
|
+
...content.matchAll(/\s{2}- key: ([^\n]+)\n\s{4}value: ([^\n]+)\n\s{4}state: confirmed/g),
|
|
24
|
+
];
|
|
25
|
+
for (const match of confirmed) {
|
|
26
|
+
const key = match[1].trim();
|
|
27
|
+
const value = match[2].trim();
|
|
28
|
+
if (key === "package-manager")
|
|
29
|
+
packageManager = value;
|
|
30
|
+
if (key === "runtime.language" && !languages.includes(value))
|
|
31
|
+
languages.push(value);
|
|
32
|
+
if (key === "framework" && !frameworks.includes(value))
|
|
33
|
+
frameworks.push(value);
|
|
34
|
+
}
|
|
35
|
+
const tool = (name, command) => {
|
|
36
|
+
const toolName = scalar(content, name);
|
|
37
|
+
return toolName ? { name: toolName, command: scalar(content, command) } : undefined;
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
runtime: { languages },
|
|
41
|
+
frameworks,
|
|
42
|
+
packageManager,
|
|
43
|
+
quality: {
|
|
44
|
+
linter: tool("linter", "linter_command"),
|
|
45
|
+
formatter: tool("formatter", "formatter_command"),
|
|
46
|
+
typecheck: tool("typecheck", "typecheck_command"),
|
|
47
|
+
},
|
|
48
|
+
testing: tool("testing", "testing_command"),
|
|
49
|
+
ci: list(content, "ci"),
|
|
50
|
+
workspaces: list(content, "workspaces"),
|
|
51
|
+
facts: [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function packageManifest(root) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function hasDependency(manifest, name) {
|
|
63
|
+
return Boolean(manifest.dependencies?.[name] || manifest.devDependencies?.[name]);
|
|
64
|
+
}
|
|
65
|
+
function commandFor(manifest, key) {
|
|
66
|
+
return manifest.scripts?.[key];
|
|
67
|
+
}
|
|
68
|
+
export async function profileProject(root) {
|
|
69
|
+
const scan = await scanProject(root);
|
|
70
|
+
const manifest = await packageManifest(root);
|
|
71
|
+
const files = scan.files;
|
|
72
|
+
const languages = [
|
|
73
|
+
...new Set(scan.evidence
|
|
74
|
+
.filter((item) => item.fact.startsWith("language."))
|
|
75
|
+
.map((item) => item.fact.replace("language.", ""))),
|
|
76
|
+
];
|
|
77
|
+
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);
|
|
83
|
+
const packageManager = files.includes("pnpm-lock.yaml")
|
|
84
|
+
? "pnpm"
|
|
85
|
+
: files.includes("yarn.lock")
|
|
86
|
+
? "yarn"
|
|
87
|
+
: files.includes("package.json")
|
|
88
|
+
? "npm"
|
|
89
|
+
: "unknown";
|
|
90
|
+
const linter = hasDependency(manifest, "eslint") ||
|
|
91
|
+
files.some((file) => file.startsWith("eslint.config") || file === ".eslintrc.json")
|
|
92
|
+
? { name: "eslint", command: commandFor(manifest, "lint") }
|
|
93
|
+
: undefined;
|
|
94
|
+
const formatter = hasDependency(manifest, "prettier") || files.some((file) => file.startsWith("prettier"))
|
|
95
|
+
? { name: "prettier", command: commandFor(manifest, "format") }
|
|
96
|
+
: undefined;
|
|
97
|
+
const testingName = hasDependency(manifest, "vitest")
|
|
98
|
+
? "vitest"
|
|
99
|
+
: hasDependency(manifest, "jest")
|
|
100
|
+
? "jest"
|
|
101
|
+
: undefined;
|
|
102
|
+
const workspaces = Array.isArray(manifest.workspaces)
|
|
103
|
+
? manifest.workspaces
|
|
104
|
+
: (manifest.workspaces?.packages ?? []);
|
|
105
|
+
const profile = {
|
|
106
|
+
runtime: { languages },
|
|
107
|
+
frameworks,
|
|
108
|
+
packageManager,
|
|
109
|
+
quality: {
|
|
110
|
+
linter,
|
|
111
|
+
formatter,
|
|
112
|
+
typecheck: commandFor(manifest, "typecheck")
|
|
113
|
+
? { name: "typescript", command: commandFor(manifest, "typecheck") }
|
|
114
|
+
: undefined,
|
|
115
|
+
},
|
|
116
|
+
testing: testingName ? { name: testingName, command: commandFor(manifest, "test") } : undefined,
|
|
117
|
+
ci: files.some((file) => file.startsWith(".github/workflows/")) ? ["github-actions"] : [],
|
|
118
|
+
workspaces,
|
|
119
|
+
facts: [],
|
|
120
|
+
};
|
|
121
|
+
profile.facts = buildFacts(profile, scan.evidence);
|
|
122
|
+
return profile;
|
|
123
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type PackageSource = {
|
|
2
|
+
provider: "local" | "git";
|
|
3
|
+
source: string;
|
|
4
|
+
};
|
|
5
|
+
export type LoadedPackageSource = PackageSource & {
|
|
6
|
+
root: string;
|
|
7
|
+
manifest: string;
|
|
8
|
+
release(): void;
|
|
9
|
+
};
|
|
10
|
+
export interface PackageProvider {
|
|
11
|
+
canResolve(source: string): boolean;
|
|
12
|
+
normalize(source: string, root: string): PackageSource;
|
|
13
|
+
}
|
|
14
|
+
export interface PackageSourceLoader {
|
|
15
|
+
load(source: string, root: string): LoadedPackageSource;
|
|
16
|
+
}
|
|
17
|
+
export declare const localProvider: PackageProvider;
|
|
18
|
+
export declare const gitProvider: PackageProvider;
|
|
19
|
+
export declare function resolveProvider(source: string, root: string): PackageSource;
|
|
20
|
+
export declare const nodePackageSourceLoader: PackageSourceLoader;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
export const localProvider = {
|
|
6
|
+
canResolve: (source) => !source.includes(":") && !source.startsWith("git+") && !/^https?:\/\//.test(source),
|
|
7
|
+
normalize: (source, root) => ({
|
|
8
|
+
provider: "local",
|
|
9
|
+
source: isAbsolute(source) ? source : resolve(root, source),
|
|
10
|
+
}),
|
|
11
|
+
};
|
|
12
|
+
export const gitProvider = {
|
|
13
|
+
canResolve: (source) => source.startsWith("git+") ||
|
|
14
|
+
source.startsWith("git@") ||
|
|
15
|
+
source.startsWith("file://") ||
|
|
16
|
+
/^https?:\/\//.test(source) ||
|
|
17
|
+
source.endsWith(".git"),
|
|
18
|
+
normalize: (source) => ({ provider: "git", source }),
|
|
19
|
+
};
|
|
20
|
+
export function resolveProvider(source, root) {
|
|
21
|
+
const provider = [gitProvider, localProvider].find((candidate) => candidate.canResolve(source));
|
|
22
|
+
if (!provider)
|
|
23
|
+
throw new Error(`Unsupported package source: ${source}`);
|
|
24
|
+
return provider.normalize(source, root);
|
|
25
|
+
}
|
|
26
|
+
export const nodePackageSourceLoader = {
|
|
27
|
+
load(source, projectRoot) {
|
|
28
|
+
const normalized = resolveProvider(source, projectRoot);
|
|
29
|
+
if (normalized.provider === "local") {
|
|
30
|
+
if (!existsSync(normalized.source))
|
|
31
|
+
throw new Error(`Local package source does not exist: ${normalized.source}`);
|
|
32
|
+
const isDirectory = statSync(normalized.source).isDirectory();
|
|
33
|
+
const packageRoot = isDirectory ? normalized.source : resolve(normalized.source, "..");
|
|
34
|
+
const manifestPath = isDirectory ? join(packageRoot, "package.yaml") : normalized.source;
|
|
35
|
+
if (!existsSync(manifestPath))
|
|
36
|
+
throw new Error(`Package manifest does not exist: ${manifestPath}`);
|
|
37
|
+
return {
|
|
38
|
+
...normalized,
|
|
39
|
+
root: packageRoot,
|
|
40
|
+
manifest: readFileSync(manifestPath, "utf8"),
|
|
41
|
+
release() { },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const checkout = mkdtempSync(join(tmpdir(), "aiw-package-"));
|
|
45
|
+
try {
|
|
46
|
+
const cloneSource = normalized.source.startsWith("git+")
|
|
47
|
+
? normalized.source.slice(4)
|
|
48
|
+
: normalized.source;
|
|
49
|
+
execFileSync("git", ["clone", "--depth", "1", "--", cloneSource, checkout], {
|
|
50
|
+
stdio: "ignore",
|
|
51
|
+
});
|
|
52
|
+
const manifestPath = join(checkout, "package.yaml");
|
|
53
|
+
if (!existsSync(manifestPath))
|
|
54
|
+
throw new Error("Git package does not contain package.yaml.");
|
|
55
|
+
return {
|
|
56
|
+
...normalized,
|
|
57
|
+
root: checkout,
|
|
58
|
+
manifest: readFileSync(manifestPath, "utf8"),
|
|
59
|
+
release: () => rmSync(checkout, { recursive: true, force: true }),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
rmSync(checkout, { recursive: true, force: true });
|
|
64
|
+
throw new Error(`Unable to load Git package source: ${normalized.source}`, { cause: error });
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
function section(content, heading) {
|
|
2
|
+
const match = content.match(new RegExp(`^## ${heading}\\s*$([\\s\\S]*?)(?=^## |(?![\\s\\S]))`, "im"));
|
|
3
|
+
return match?.[1].trim();
|
|
4
|
+
}
|
|
5
|
+
function requireSections(content, artifact, headings) {
|
|
6
|
+
return headings.flatMap((heading) => {
|
|
7
|
+
const value = section(content, heading);
|
|
8
|
+
if (value === undefined)
|
|
9
|
+
return [`${artifact} section ${heading} is missing.`];
|
|
10
|
+
return value ? [] : [`${artifact} section ${heading} must contain content.`];
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export function evaluateQualityGate(stage, content) {
|
|
14
|
+
if (stage === "specification")
|
|
15
|
+
return requireSections(content, "Brainstorm", [
|
|
16
|
+
"Goal",
|
|
17
|
+
"Users",
|
|
18
|
+
"Constraints",
|
|
19
|
+
"Risks",
|
|
20
|
+
"Non-goals",
|
|
21
|
+
]);
|
|
22
|
+
if (stage === "plan") {
|
|
23
|
+
const issues = [];
|
|
24
|
+
if (!/REQ-\d+/.test(content))
|
|
25
|
+
issues.push("Specification must define a stable REQ-### ID.");
|
|
26
|
+
if (!/Acceptance criteria/i.test(content))
|
|
27
|
+
issues.push("Specification requirements must define acceptance criteria.");
|
|
28
|
+
if (!["Given", "When", "Then"].every((word) => new RegExp(`\\b${word}[ \\t]+\\S+`, "i").test(content)))
|
|
29
|
+
issues.push("Requirement acceptance criteria must use Given, When, and Then.");
|
|
30
|
+
return issues;
|
|
31
|
+
}
|
|
32
|
+
const fields = ["Requirement", "Code", "Tests", "Validation", "Evidence"];
|
|
33
|
+
const issues = !/TASK-\d+/.test(content)
|
|
34
|
+
? ["Implementation plan must define a TASK-### ID."]
|
|
35
|
+
: [];
|
|
36
|
+
for (const field of fields)
|
|
37
|
+
if (!new RegExp(`^[ \\t]*-?[ \\t]*${field}:[ \\t]*\\S+`, "im").test(content))
|
|
38
|
+
issues.push(`Implementation plan task must define ${field}.`);
|
|
39
|
+
return issues;
|
|
40
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ProjectProfile } from "./profile.js";
|
|
2
|
+
export type CapabilityRecommendation = {
|
|
3
|
+
id: string;
|
|
4
|
+
provider: string;
|
|
5
|
+
confidence: number;
|
|
6
|
+
rationale: string;
|
|
7
|
+
evidence: string[];
|
|
8
|
+
permissions: string[];
|
|
9
|
+
conflicts: string[];
|
|
10
|
+
};
|
|
11
|
+
export declare function recommendCapabilities(profile: ProjectProfile): CapabilityRecommendation[];
|
|
12
|
+
export declare function serializeRecommendations(items: CapabilityRecommendation[], selected?: string[]): string;
|