@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.
Files changed (165) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +32 -0
  4. package/dist/adapter-contract.d.ts +13 -0
  5. package/dist/adapter-contract.js +50 -0
  6. package/dist/adapter.d.ts +15 -0
  7. package/dist/adapter.js +62 -0
  8. package/dist/claude-layout.d.ts +9 -0
  9. package/dist/claude-layout.js +23 -0
  10. package/dist/cli.d.ts +4 -0
  11. package/dist/cli.js +30 -0
  12. package/dist/codex-layout.d.ts +10 -0
  13. package/dist/codex-layout.js +24 -0
  14. package/dist/confirmation.d.ts +9 -0
  15. package/dist/confirmation.js +41 -0
  16. package/dist/constants.d.ts +3 -0
  17. package/dist/constants.js +10 -0
  18. package/dist/context-cache.d.ts +8 -0
  19. package/dist/context-cache.js +17 -0
  20. package/dist/context-quality.d.ts +11 -0
  21. package/dist/context-quality.js +27 -0
  22. package/dist/context-retrieval.d.ts +3 -0
  23. package/dist/context-retrieval.js +13 -0
  24. package/dist/context-store.d.ts +9 -0
  25. package/dist/context-store.js +18 -0
  26. package/dist/copilot-layout.d.ts +9 -0
  27. package/dist/copilot-layout.js +24 -0
  28. package/dist/cursor-layout.d.ts +9 -0
  29. package/dist/cursor-layout.js +25 -0
  30. package/dist/dashboard.d.ts +37 -0
  31. package/dist/dashboard.js +251 -0
  32. package/dist/drift.d.ts +2 -0
  33. package/dist/drift.js +10 -0
  34. package/dist/evidence.d.ts +20 -0
  35. package/dist/evidence.js +32 -0
  36. package/dist/files.d.ts +2 -0
  37. package/dist/files.js +58 -0
  38. package/dist/gemini-layout.d.ts +9 -0
  39. package/dist/gemini-layout.js +23 -0
  40. package/dist/generator.d.ts +3 -0
  41. package/dist/generator.js +16 -0
  42. package/dist/intelligence.d.ts +2 -0
  43. package/dist/intelligence.js +13 -0
  44. package/dist/interpreter.d.ts +23 -0
  45. package/dist/interpreter.js +117 -0
  46. package/dist/lockfile.d.ts +12 -0
  47. package/dist/lockfile.js +59 -0
  48. package/dist/manifest.d.ts +15 -0
  49. package/dist/manifest.js +35 -0
  50. package/dist/migration.d.ts +22 -0
  51. package/dist/migration.js +50 -0
  52. package/dist/orchestration.d.ts +30 -0
  53. package/dist/orchestration.js +172 -0
  54. package/dist/organization-policy.d.ts +14 -0
  55. package/dist/organization-policy.js +47 -0
  56. package/dist/package-audit.d.ts +19 -0
  57. package/dist/package-audit.js +28 -0
  58. package/dist/package-contract.d.ts +30 -0
  59. package/dist/package-contract.js +93 -0
  60. package/dist/package-integrity.d.ts +8 -0
  61. package/dist/package-integrity.js +15 -0
  62. package/dist/package-updates.d.ts +11 -0
  63. package/dist/package-updates.js +32 -0
  64. package/dist/plugin-sdk.d.ts +20 -0
  65. package/dist/plugin-sdk.js +243 -0
  66. package/dist/policy-gate.d.ts +6 -0
  67. package/dist/policy-gate.js +20 -0
  68. package/dist/profile.d.ts +23 -0
  69. package/dist/profile.js +123 -0
  70. package/dist/providers.d.ts +20 -0
  71. package/dist/providers.js +67 -0
  72. package/dist/quality-gates.d.ts +2 -0
  73. package/dist/quality-gates.js +40 -0
  74. package/dist/recommendations.d.ts +12 -0
  75. package/dist/recommendations.js +63 -0
  76. package/dist/registry.d.ts +11 -0
  77. package/dist/registry.js +27 -0
  78. package/dist/scanner.d.ts +9 -0
  79. package/dist/scanner.js +83 -0
  80. package/dist/selection.d.ts +3 -0
  81. package/dist/selection.js +11 -0
  82. package/dist/self-validation.d.ts +9 -0
  83. package/dist/self-validation.js +9 -0
  84. package/dist/services.d.ts +42 -0
  85. package/dist/services.js +1 -0
  86. package/dist/team-configuration.d.ts +23 -0
  87. package/dist/team-configuration.js +104 -0
  88. package/dist/telemetry.d.ts +20 -0
  89. package/dist/telemetry.js +95 -0
  90. package/dist/token-usage.d.ts +9 -0
  91. package/dist/token-usage.js +38 -0
  92. package/dist/traceability.d.ts +23 -0
  93. package/dist/traceability.js +51 -0
  94. package/dist/types.d.ts +22 -0
  95. package/dist/types.js +1 -0
  96. package/dist/universal-layout.d.ts +9 -0
  97. package/dist/universal-layout.js +21 -0
  98. package/dist/vercel-skills.d.ts +17 -0
  99. package/dist/vercel-skills.js +49 -0
  100. package/dist/workflow.d.ts +4 -0
  101. package/dist/workflow.js +1130 -0
  102. package/docs/README.md +20 -0
  103. package/docs/adapter-model.md +16 -0
  104. package/docs/architecture.md +66 -0
  105. package/docs/backlog.md +93 -0
  106. package/docs/orchestration.md +50 -0
  107. package/docs/package-model.md +53 -0
  108. package/docs/plugin-authoring.md +31 -0
  109. package/docs/product-requirements.md +57 -0
  110. package/docs/product-vision.md +26 -0
  111. package/docs/project-intelligence.md +59 -0
  112. package/docs/quality.md +32 -0
  113. package/docs/release-backlog.md +7 -0
  114. package/docs/release-process.md +38 -0
  115. package/docs/security.md +18 -0
  116. package/docs/telemetry.md +28 -0
  117. package/docs/token-efficiency.md +16 -0
  118. package/package.json +76 -0
  119. package/resources/README.md +4 -0
  120. package/resources/agents/adapter-engineer.md +4 -0
  121. package/resources/agents/documentation-writer.md +4 -0
  122. package/resources/agents/quality-reviewer.md +4 -0
  123. package/resources/agents/requirements-analyst.md +4 -0
  124. package/resources/agents/security-reviewer.md +4 -0
  125. package/resources/agents/technical-architect.md +4 -0
  126. package/resources/agents/test-engineer.md +4 -0
  127. package/resources/agents/typescript-engineer.md +4 -0
  128. package/resources/hooks/post-change.md +4 -0
  129. package/resources/hooks/post-implementation.md +4 -0
  130. package/resources/hooks/pre-commit.md +4 -0
  131. package/resources/hooks/pre-implementation.md +4 -0
  132. package/resources/hooks/pre-release.md +4 -0
  133. package/resources/package.yaml +36 -0
  134. package/resources/rules/architecture-policy.md +4 -0
  135. package/resources/rules/completion-validation.md +19 -0
  136. package/resources/rules/dependency-policy.md +4 -0
  137. package/resources/rules/documentation-policy.md +4 -0
  138. package/resources/rules/generated-files.md +4 -0
  139. package/resources/rules/language-policy.md +4 -0
  140. package/resources/rules/quality-gates.md +4 -0
  141. package/resources/rules/security-policy.md +4 -0
  142. package/resources/rules/self-validation.md +12 -0
  143. package/resources/rules/tdd-policy.md +4 -0
  144. package/resources/skills/adapter-development.md +9 -0
  145. package/resources/skills/brainstorming.md +9 -0
  146. package/resources/skills/code-review.md +9 -0
  147. package/resources/skills/documentation-maintenance.md +9 -0
  148. package/resources/skills/implementation-planning.md +9 -0
  149. package/resources/skills/project-init.md +9 -0
  150. package/resources/skills/requirements-specification.md +9 -0
  151. package/resources/skills/security-review.md +9 -0
  152. package/resources/skills/skill-authoring.md +9 -0
  153. package/resources/skills/tdd-development.md +9 -0
  154. package/resources/skills/technical-design.md +9 -0
  155. package/resources/skills/verification.md +9 -0
  156. package/resources/templates/brainstorm.md +16 -0
  157. package/resources/templates/decision-record.md +12 -0
  158. package/resources/templates/implementation-plan.md +12 -0
  159. package/resources/templates/migration-report.md +14 -0
  160. package/resources/templates/security-review.md +14 -0
  161. package/resources/templates/specification.md +23 -0
  162. package/resources/templates/task.md +14 -0
  163. package/resources/templates/technical-design.md +14 -0
  164. package/resources/templates/test-plan.md +12 -0
  165. package/resources/templates/verification-report.md +12 -0
@@ -0,0 +1,251 @@
1
+ import { createServer } from "node:http";
2
+ import { isAbsolute, join, relative, sep } from "node:path";
3
+ import { TARGETS } from "./types.js";
4
+ export function buildDashboardModel(root, fs) {
5
+ const projectRoot = fs.realpath?.(root) ?? root;
6
+ const read = (path) => {
7
+ if (!fs.exists(path))
8
+ return "";
9
+ if (fs.pathType?.(path) === "symlink")
10
+ throw new Error(`Dashboard input cannot be a symbolic link: ${path}`);
11
+ const realPath = fs.realpath?.(path) ?? path;
12
+ if (!isInside(projectRoot, realPath))
13
+ throw new Error(`Dashboard input must stay inside the project: ${path}`);
14
+ return fs.read(path);
15
+ };
16
+ const aiw = join(root, ".aiw");
17
+ const manifest = read(join(aiw, "manifest.yml"));
18
+ const profile = read(join(aiw, "profile.yml"));
19
+ const recommendations = read(join(aiw, "recommendations.yml"));
20
+ const traceability = read(join(aiw, "generated/artifacts/traceability.yml"));
21
+ return {
22
+ project: manifest.match(/^\s{2}name:\s*(.+)$/m)?.[1]?.trim() ?? root.split("/").at(-1) ?? root,
23
+ target: manifest.match(/^\s{2}active:\s*(.+)$/m)?.[1]?.trim() ?? "not configured",
24
+ facts: blocks(profile, /^\s{2}- key:\s*(.+)$/gm).map(({ heading, body }) => ({
25
+ key: heading,
26
+ value: field(body, "value"),
27
+ state: field(body, "state"),
28
+ confidence: field(body, "confidence"),
29
+ })),
30
+ recommendations: blocks(recommendations, /^\s{2}- id:\s*(.+)$/gm).map(({ heading, body }) => ({
31
+ id: heading,
32
+ provider: field(body, "provider"),
33
+ selected: field(body, "selected") === "true",
34
+ })),
35
+ traceability: blocks(traceability, /^\s{2}- requirement:\s*(.+)$/gm).map(({ heading, body }) => ({
36
+ requirement: heading,
37
+ decisions: inlineList(field(body, "decisions")),
38
+ tasks: inlineList(field(body, "tasks")),
39
+ evidence: inlineList(field(body, "evidence")),
40
+ })),
41
+ };
42
+ }
43
+ export function renderDashboard(model, notice = "") {
44
+ const facts = model.facts.length
45
+ ? model.facts
46
+ .map((fact) => `<tr><th>${escapeHtml(fact.key)}</th><td>${escapeHtml(fact.value)}</td><td><span class="state">${escapeHtml(fact.state)}</span></td><td>${escapeHtml(fact.confidence)}</td></tr>`)
47
+ .join("")
48
+ : '<tr><td colspan="4" class="empty">Run <code>aiw scan</code> to build the project profile.</td></tr>';
49
+ const recommendations = model.recommendations.length
50
+ ? model.recommendations
51
+ .map((item) => `<li><span><strong>${escapeHtml(item.id)}</strong><small>${escapeHtml(item.provider)}</small></span><b class="${item.selected ? "selected" : "pending"}">${item.selected ? "Selected" : "Available"}</b></li>`)
52
+ .join("")
53
+ : '<li class="empty">Run <code>aiw recommend</code> to see project-specific capabilities.</li>';
54
+ const traces = model.traceability.length
55
+ ? model.traceability
56
+ .map((link) => `<article class="trace"><strong>${escapeHtml(link.requirement)}</strong><i></i><span>${escapeHtml(link.decisions.join(", ") || "No decision")}</span><i></i><span>${escapeHtml(link.tasks.join(", ") || "No task")}</span><i></i><span>${escapeHtml(link.evidence.join(", ") || "No evidence")}</span></article>`)
57
+ .join("")
58
+ : '<p class="empty">Run <code>aiw trace</code> to connect requirements to evidence.</p>';
59
+ const options = TARGETS.map((target) => `<option value="${target}"${target === model.target ? " selected" : ""}>${target}</option>`).join("");
60
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(model.project)} · AI Workflow</title><style>${styles}</style></head><body><header><p class="eyebrow">AI WORKFLOW / PROJECT CONTROL</p><h1>${escapeHtml(model.project)}</h1><div class="target">Active target <strong>${escapeHtml(model.target)}</strong></div></header><main>${notice ? `<p class="notice" role="status">${escapeHtml(notice)}</p>` : ""}<section><div class="section-title"><span>Profile</span><small>Confirmed project signals</small></div><div class="table-wrap"><table><thead><tr><th>Fact</th><th>Value</th><th>State</th><th>Confidence</th></tr></thead><tbody>${facts}</tbody></table></div></section><section><div class="section-title"><span>Recommendations</span><small>Capabilities matched to this repository</small></div><ul>${recommendations}</ul></section><section class="wide"><div class="section-title"><span>Traceability</span><small>Requirement → decision → task → evidence</small></div><div class="traces">${traces}</div></section><section class="wide migration"><div><div class="section-title"><span>Migration</span><small>Render neutral resources for another assistant</small></div><p>Changing target updates generated adapter files. Type <code>MIGRATE</code> to confirm.</p></div><form method="post" action="/migrate"><label>Target<select name="target">${options}</select></label><label>Confirmation<input name="confirmation" autocomplete="off" placeholder="MIGRATE" required></label><button type="submit">Migrate target</button></form></section></main><footer>Local-only dashboard · No external assets or telemetry</footer></body></html>`;
61
+ }
62
+ export async function startDashboard(root, fs, execute, port = 0) {
63
+ if (!fs.exists(join(root, ".aiw/manifest.yml")))
64
+ throw new Error("Run `aiw install` first.");
65
+ let notice = "";
66
+ const server = createServer(async (request, response) => {
67
+ response.setHeader("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'");
68
+ response.setHeader("X-Content-Type-Options", "nosniff");
69
+ response.setHeader("Cache-Control", "no-store");
70
+ const address = server.address();
71
+ const expectedOrigin = address && typeof address !== "string" ? `http://127.0.0.1:${address.port}` : "";
72
+ if (request.headers.host !== expectedOrigin.slice("http://".length)) {
73
+ response.statusCode = 400;
74
+ response.end("Invalid host");
75
+ return;
76
+ }
77
+ if (request.method === "GET" && request.url === "/api/dashboard") {
78
+ try {
79
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
80
+ response.end(JSON.stringify(buildDashboardModel(root, fs)));
81
+ }
82
+ catch {
83
+ response.statusCode = 500;
84
+ response.end(JSON.stringify({ error: "Dashboard project state is unsafe or unreadable." }));
85
+ }
86
+ return;
87
+ }
88
+ if (request.method === "POST" && request.url === "/migrate") {
89
+ try {
90
+ if (request.headers.origin !== expectedOrigin)
91
+ throw new Error("Migration requests must originate from this dashboard.");
92
+ if (!/^application\/x-www-form-urlencoded(?:;\s*charset=utf-8)?$/i.test(request.headers["content-type"] ?? ""))
93
+ throw new Error("Migration request content type is invalid.");
94
+ const params = parseForm(await requestBody(request));
95
+ const target = params.target;
96
+ if (!TARGETS.includes(target) || params.confirmation !== "MIGRATE")
97
+ throw new Error("Select a supported target and type MIGRATE to confirm.");
98
+ const result = await execute(["target", target]);
99
+ notice = result.output ?? result.error ?? "Migration finished.";
100
+ if (result.exitCode !== 0) {
101
+ renderResponse(response, 409, safeDashboardHtml(root, fs, notice));
102
+ notice = "";
103
+ return;
104
+ }
105
+ response.statusCode = 303;
106
+ }
107
+ catch (error) {
108
+ notice = error instanceof Error ? error.message : String(error);
109
+ renderResponse(response, 400, safeDashboardHtml(root, fs, notice));
110
+ notice = "";
111
+ return;
112
+ }
113
+ response.setHeader("Location", "/");
114
+ response.end();
115
+ return;
116
+ }
117
+ if (request.method !== "GET" || request.url !== "/") {
118
+ response.statusCode = 404;
119
+ response.end("Not found");
120
+ return;
121
+ }
122
+ response.setHeader("Content-Type", "text/html; charset=utf-8");
123
+ try {
124
+ response.end(renderDashboard(buildDashboardModel(root, fs), notice));
125
+ }
126
+ catch {
127
+ response.statusCode = 500;
128
+ response.end("Dashboard project state is unsafe or unreadable.");
129
+ }
130
+ notice = "";
131
+ });
132
+ await new Promise((resolve, reject) => {
133
+ server.once("error", reject);
134
+ server.listen(port, "127.0.0.1", resolve);
135
+ });
136
+ const address = server.address();
137
+ if (!address || typeof address === "string")
138
+ throw new Error("Dashboard address is unavailable.");
139
+ return { url: `http://127.0.0.1:${address.port}`, close: () => closeServer(server) };
140
+ }
141
+ function blocks(content, expression) {
142
+ const matches = [...content.matchAll(expression)];
143
+ return matches.map((match, index) => ({
144
+ heading: match[1].trim(),
145
+ body: content.slice(match.index, matches[index + 1]?.index ?? content.length),
146
+ }));
147
+ }
148
+ function field(block, name) {
149
+ return block.match(new RegExp(`^\\s{4}${name}:\\s*(.*)$`, "m"))?.[1]?.trim() ?? "unknown";
150
+ }
151
+ function inlineList(value) {
152
+ if (!value.startsWith("[") || !value.endsWith("]"))
153
+ return [];
154
+ const items = [];
155
+ let current = "";
156
+ let quoted = false;
157
+ let escaped = false;
158
+ for (const character of value.slice(1, -1)) {
159
+ if (escaped) {
160
+ current += character;
161
+ escaped = false;
162
+ }
163
+ else if (character === "\\" && quoted) {
164
+ current += character;
165
+ escaped = true;
166
+ }
167
+ else if (character === '"') {
168
+ current += character;
169
+ quoted = !quoted;
170
+ }
171
+ else if (character === "," && !quoted) {
172
+ items.push(current);
173
+ current = "";
174
+ }
175
+ else {
176
+ current += character;
177
+ }
178
+ }
179
+ items.push(current);
180
+ return items.map(parseInlineItem).filter(Boolean);
181
+ }
182
+ function parseInlineItem(value) {
183
+ const item = value.trim();
184
+ if (item.startsWith('"') && item.endsWith('"')) {
185
+ try {
186
+ return JSON.parse(item);
187
+ }
188
+ catch {
189
+ return item.slice(1, -1);
190
+ }
191
+ }
192
+ return item;
193
+ }
194
+ function escapeHtml(value) {
195
+ return value
196
+ .replaceAll("&", "&amp;")
197
+ .replaceAll("<", "&lt;")
198
+ .replaceAll(">", "&gt;")
199
+ .replaceAll('"', "&quot;")
200
+ .replaceAll("'", "&#39;");
201
+ }
202
+ async function requestBody(request) {
203
+ const chunks = [];
204
+ let size = 0;
205
+ for await (const chunk of request) {
206
+ const value = Buffer.from(chunk);
207
+ size += value.length;
208
+ if (size > 4096)
209
+ throw new Error("Request body is too large.");
210
+ chunks.push(value);
211
+ }
212
+ return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks));
213
+ }
214
+ function parseForm(body) {
215
+ const values = new Map();
216
+ for (const pair of body.split("&")) {
217
+ const separator = pair.indexOf("=");
218
+ if (!pair || separator <= 0 || separator !== pair.lastIndexOf("="))
219
+ throw new Error("Migration request form is malformed.");
220
+ const decode = (value) => decodeURIComponent(value.replaceAll("+", " "));
221
+ const key = decode(pair.slice(0, separator));
222
+ const value = decode(pair.slice(separator + 1));
223
+ if (!["target", "confirmation"].includes(key) || values.has(key))
224
+ throw new Error("Migration request fields are invalid or duplicated.");
225
+ values.set(key, value);
226
+ }
227
+ if (values.size !== 2 || !values.has("target") || !values.has("confirmation"))
228
+ throw new Error("Migration request fields are incomplete.");
229
+ return { target: values.get("target") ?? "", confirmation: values.get("confirmation") ?? "" };
230
+ }
231
+ function closeServer(server) {
232
+ return new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
233
+ }
234
+ function renderResponse(response, status, html) {
235
+ response.statusCode = status;
236
+ response.setHeader("Content-Type", "text/html; charset=utf-8");
237
+ response.end(html);
238
+ }
239
+ function safeDashboardHtml(root, fs, notice) {
240
+ try {
241
+ return renderDashboard(buildDashboardModel(root, fs), notice);
242
+ }
243
+ catch {
244
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>AI Workflow</title></head><body><main><h1>AI Workflow</h1><p role="alert">${escapeHtml(notice)}</p><p>Dashboard project state is unsafe or unreadable.</p></main></body></html>`;
245
+ }
246
+ }
247
+ function isInside(root, destination) {
248
+ const path = relative(root, destination);
249
+ return path === "" || (path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path));
250
+ }
251
+ const styles = `:root{color-scheme:dark;--ink:#09111f;--panel:#101d30;--line:#29405d;--text:#e8f1fa;--muted:#8fa6bd;--cyan:#55d6d0;--amber:#f0b35a}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 80% 0,#173251 0,transparent 35%),var(--ink);color:var(--text);font:15px/1.55 ui-sans-serif,system-ui,sans-serif;overflow-wrap:anywhere}header,main,footer{max-width:1180px;margin:auto;min-width:0}header{padding:52px 24px 30px;border-bottom:1px solid var(--line);position:relative}h1{font:700 clamp(2.4rem,7vw,5.5rem)/.9 ui-monospace,SFMono-Regular,monospace;letter-spacing:-.08em;margin:.2em 0;overflow-wrap:anywhere}.eyebrow,.section-title,small,footer{color:var(--muted);letter-spacing:.08em;text-transform:uppercase;font:600 11px/1.4 ui-monospace,monospace}.target{position:absolute;right:24px;bottom:32px}.target strong,.state{color:var(--cyan)}main{padding:28px 24px;display:grid;grid-template-columns:1.3fr .7fr;gap:18px}section{background:color-mix(in srgb,var(--panel) 88%,transparent);border:1px solid var(--line);padding:22px;min-width:0}.wide{grid-column:1/-1}.section-title{display:flex;justify-content:space-between;gap:12px;color:var(--text);margin-bottom:18px}.section-title small{color:var(--muted);text-align:right}section:not(.wide) .section-title{align-items:flex-start;flex-direction:column}section:not(.wide) .section-title small{text-align:left}table{width:100%;border-collapse:collapse;text-align:left}th,td{padding:10px;border-bottom:1px solid var(--line)}thead th{color:var(--muted);font-size:11px;text-transform:uppercase}tbody th{font-family:ui-monospace,monospace}ul{list-style:none;padding:0;margin:0}li{display:flex;justify-content:space-between;padding:11px 0;border-bottom:1px solid var(--line)}li small{display:block}.selected{color:var(--cyan)}.pending{color:var(--amber)}.trace{display:grid;grid-template-columns:auto minmax(20px,1fr) auto minmax(20px,1fr) auto minmax(20px,1fr) auto;align-items:center;gap:10px;margin:12px 0;font-family:ui-monospace,monospace}.trace>*{min-width:0;overflow-wrap:anywhere}.trace i{height:1px;background:linear-gradient(90deg,var(--cyan),var(--line));position:relative}.trace i:after{content:'›';position:absolute;right:-2px;top:-12px;color:var(--cyan)}.migration{display:flex;justify-content:space-between;gap:24px;align-items:end}.migration form{display:flex;gap:10px;align-items:end}label{color:var(--muted);font-size:12px}select,input,button{display:block;margin-top:5px;background:var(--ink);color:var(--text);border:1px solid var(--line);padding:10px 12px;font:inherit;max-width:100%}button{background:var(--cyan);color:var(--ink);font-weight:800;cursor:pointer}button:focus-visible,select:focus-visible,input:focus-visible{outline:3px solid var(--amber);outline-offset:2px}.notice{grid-column:1/-1;border-left:3px solid var(--cyan);padding:12px 16px;background:var(--panel)}.empty{color:var(--muted)}code{font-family:ui-monospace,monospace;color:var(--cyan)}footer{padding:18px 24px 40px}@media(max-width:760px){header{padding-top:32px}.target{position:static}main{grid-template-columns:1fr}.wide{grid-column:auto}.migration,.migration form{display:grid}.trace{grid-template-columns:1fr}.trace i{width:28px}.table-wrap{overflow:auto}}@media(prefers-reduced-motion:no-preference){section{animation:enter .35s ease both}@keyframes enter{from{opacity:0;transform:translateY(8px)}}}`;
@@ -0,0 +1,2 @@
1
+ export declare function resolveGeneratedPath(path: string): string;
2
+ export declare function detectDrift(expected: string, actual: string): boolean;
package/dist/drift.js ADDED
@@ -0,0 +1,10 @@
1
+ import { normalize } from "node:path";
2
+ export function resolveGeneratedPath(path) {
3
+ const normalized = normalize(path).replaceAll("\\", "/");
4
+ if (normalized.startsWith("../") || normalized === ".." || normalized.startsWith("/"))
5
+ throw new Error("Generated path is outside generated resources");
6
+ return `generated/${normalized}`;
7
+ }
8
+ export function detectDrift(expected, actual) {
9
+ return expected !== actual;
10
+ }
@@ -0,0 +1,20 @@
1
+ import type { ProjectProfile } from "./profile.js";
2
+ export type EvidenceState = "detected" | "inferred" | "confirmed";
3
+ export type EvidenceMethod = "deterministic" | "ai-inference" | "user-confirmed";
4
+ export type Evidence = {
5
+ source: string;
6
+ reason: string;
7
+ };
8
+ export type EvidenceFact = {
9
+ key: string;
10
+ value: string;
11
+ state: EvidenceState;
12
+ method: EvidenceMethod;
13
+ confidence: number;
14
+ requiresConfirmation?: boolean;
15
+ evidence: Evidence[];
16
+ };
17
+ export declare function buildFacts(profile: ProjectProfile, detectedEvidence?: {
18
+ fact: string;
19
+ source: string;
20
+ }[]): EvidenceFact[];
@@ -0,0 +1,32 @@
1
+ export function buildFacts(profile, detectedEvidence = []) {
2
+ const facts = [];
3
+ const add = (key, value) => {
4
+ facts.push({
5
+ key,
6
+ value,
7
+ state: "detected",
8
+ method: "deterministic",
9
+ confidence: 1,
10
+ evidence: detectedEvidence
11
+ .filter((item) => item.fact === key ||
12
+ item.fact.endsWith(`.${value}`) ||
13
+ (key === "tool.linter" && item.fact === "tool.eslint") ||
14
+ (key === "tool.formatter" && item.fact === "tool.prettier") ||
15
+ (key === "tool.testing" && item.fact === `tool.${value}`))
16
+ .map((item) => ({ source: item.source, reason: "" })),
17
+ });
18
+ };
19
+ profile.runtime.languages.forEach((language) => add("runtime.language", language));
20
+ profile.frameworks.forEach((framework) => add("framework", framework));
21
+ if (profile.packageManager !== "unknown")
22
+ add("package-manager", profile.packageManager);
23
+ if (profile.quality.linter)
24
+ add("tool.linter", profile.quality.linter.name);
25
+ if (profile.quality.formatter)
26
+ add("tool.formatter", profile.quality.formatter.name);
27
+ if (profile.testing)
28
+ add("tool.testing", profile.testing.name);
29
+ profile.ci.forEach((provider) => add("ci.provider", provider));
30
+ profile.workspaces.forEach((workspace) => add("workspace", workspace));
31
+ return facts;
32
+ }
@@ -0,0 +1,2 @@
1
+ import type { FileSystem } from "./types.js";
2
+ export declare const nodeFileSystem: FileSystem;
package/dist/files.js ADDED
@@ -0,0 +1,58 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, openSync, closeSync, lstatSync, realpathSync, } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ export const nodeFileSystem = {
4
+ exists: existsSync,
5
+ read: (path) => readFileSync(path, "utf8"),
6
+ readBytes: (path) => readFileSync(path),
7
+ write: (path, content) => {
8
+ mkdirSync(dirname(path), { recursive: true });
9
+ writeFileSync(path, content);
10
+ },
11
+ createExclusive: (path, content) => {
12
+ mkdirSync(dirname(path), { recursive: true });
13
+ try {
14
+ const descriptor = openSync(path, "wx");
15
+ try {
16
+ writeFileSync(descriptor, content);
17
+ }
18
+ finally {
19
+ closeSync(descriptor);
20
+ }
21
+ return true;
22
+ }
23
+ catch (error) {
24
+ if (error.code === "EEXIST")
25
+ return false;
26
+ throw error;
27
+ }
28
+ },
29
+ writeBytes: (path, content) => {
30
+ mkdirSync(dirname(path), { recursive: true });
31
+ writeFileSync(path, content);
32
+ },
33
+ mkdir: (path) => mkdirSync(path, { recursive: true }),
34
+ list: (path) => readdirSync(path).sort(),
35
+ listFiles: (path) => readdirSync(path, { recursive: true, withFileTypes: true })
36
+ .filter((entry) => entry.isFile())
37
+ .map((entry) => join(entry.parentPath.slice(path.length + 1), entry.name))
38
+ .sort(),
39
+ remove: unlinkSync,
40
+ pathType: (path) => {
41
+ try {
42
+ const entry = lstatSync(path);
43
+ if (entry.isSymbolicLink())
44
+ return "symlink";
45
+ if (entry.isFile())
46
+ return "file";
47
+ if (entry.isDirectory())
48
+ return "directory";
49
+ return "other";
50
+ }
51
+ catch (error) {
52
+ if (error.code === "ENOENT")
53
+ return "missing";
54
+ throw error;
55
+ }
56
+ },
57
+ realpath: realpathSync,
58
+ };
@@ -0,0 +1,9 @@
1
+ import type { ResourceType } from "./package-contract.js";
2
+ import type { FileSystem } from "./types.js";
3
+ export type GeminiResourceType = ResourceType | "context";
4
+ export type GeminiResourceDescriptor = {
5
+ path: string;
6
+ support: "native" | "compatibility";
7
+ };
8
+ export declare function describeGeminiResource(type: GeminiResourceType, id: string): GeminiResourceDescriptor;
9
+ export declare function renderGeminiContext(root: string, content: string, fs: FileSystem): string;
@@ -0,0 +1,23 @@
1
+ import { join } from "node:path";
2
+ export function describeGeminiResource(type, id) {
3
+ if (type === "skills")
4
+ return { path: `.gemini/skills/${id}/SKILL.md`, support: "native" };
5
+ if (type === "agents")
6
+ return { path: `.gemini/agents/${id}.md`, support: "native" };
7
+ if (type === "context")
8
+ return { path: "GEMINI.md", support: "native" };
9
+ const roots = {
10
+ rules: ".gemini/aiw/rules",
11
+ hooks: ".gemini/aiw/hooks",
12
+ templates: ".gemini/aiw/templates",
13
+ };
14
+ const root = roots[type];
15
+ if (!root)
16
+ throw new Error(`Unsupported Gemini resource type: ${type}`);
17
+ return { path: `${root}/${id}.md`, support: "compatibility" };
18
+ }
19
+ export function renderGeminiContext(root, content, fs) {
20
+ const path = describeGeminiResource("context", "project").path;
21
+ fs.write(join(root, path), content);
22
+ return path;
23
+ }
@@ -0,0 +1,3 @@
1
+ import type { ProjectProfile } from "./profile.js";
2
+ export type ResourceWriter = (path: string, content: string) => void;
3
+ export declare function generateProjectResources(profile: ProjectProfile, selected: string[], write: ResourceWriter): void;
@@ -0,0 +1,16 @@
1
+ import { resolveGeneratedPath } from "./drift.js";
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;
6
+ if (selected.includes("typescript-quality"))
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"))
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"))
11
+ 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
+ if (selected.length > 0) {
13
+ 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`);
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
+ }
16
+ }
@@ -0,0 +1,2 @@
1
+ import type { EvidenceFact } from "./evidence.js";
2
+ export declare function mergeFacts(detected: EvidenceFact[], proposed: EvidenceFact[]): EvidenceFact[];
@@ -0,0 +1,13 @@
1
+ export function mergeFacts(detected, proposed) {
2
+ const result = [...detected];
3
+ for (const candidate of proposed) {
4
+ const index = result.findIndex((fact) => fact.key === candidate.key);
5
+ if (index < 0) {
6
+ result.push(candidate);
7
+ continue;
8
+ }
9
+ if (candidate.state === "confirmed" && candidate.method === "user-confirmed")
10
+ result[index] = candidate;
11
+ }
12
+ return result;
13
+ }
@@ -0,0 +1,23 @@
1
+ import type { EvidenceFact } from "./evidence.js";
2
+ import type { ProjectProfile } from "./profile.js";
3
+ export type InterpretationRequest = {
4
+ profile: ProjectProfile;
5
+ files: string[];
6
+ maxTokens: number;
7
+ estimatedTokens: number;
8
+ };
9
+ export type Interpreter = {
10
+ interpret(request: InterpretationRequest): Promise<EvidenceFact[]>;
11
+ };
12
+ export type AiProviderRequest = {
13
+ system: string;
14
+ prompt: string;
15
+ maxTokens: number;
16
+ responseFormat: "json";
17
+ };
18
+ export type AiProvider = {
19
+ generate(request: AiProviderRequest): Promise<string>;
20
+ };
21
+ export type ContextReader = (absolutePath: string) => Promise<string>;
22
+ export declare function createInterpretationRequest(profile: ProjectProfile, files: string[], maxTokens?: number): InterpretationRequest;
23
+ export declare function createProjectInterpreter(root: string, provider: AiProvider, contextReader?: ContextReader): Interpreter;
@@ -0,0 +1,117 @@
1
+ import { lstat, readFile } from "node:fs/promises";
2
+ import { isAbsolute, relative, resolve } from "node:path";
3
+ const EXCLUDED_SEGMENT = /(^|\/)(\.git|node_modules|vendor|\.venv|venv|dist|build|coverage|out|target|\.next|\.nuxt|\.output)(\/|$)/i;
4
+ const EXCLUDED_AIW = /(^|\/)\.aiw\/(generated|checkpoints)(\/|$)/i;
5
+ const SENSITIVE = /(^|\/)\.env(?:\.[^/]*)?$|\.(pem|key|p12|pfx|secret)$/i;
6
+ const TEXT_FILE = /(^|\/)(README|CONTRIBUTING|ARCHITECTURE)(\.[^/]*)?$|\.(c|cc|cpp|cs|css|go|graphql|h|hpp|html|java|js|jsx|json|kt|md|mjs|cjs|php|py|rb|rs|sh|sql|swift|toml|ts|tsx|txt|vue|yaml|yml)$/i;
7
+ function safeFile(file) {
8
+ const normalized = file.replaceAll("\\", "/").replace(/^\.\//, "");
9
+ return (normalized.length > 0 &&
10
+ !isAbsolute(file) &&
11
+ !normalized.split("/").includes("..") &&
12
+ !EXCLUDED_SEGMENT.test(normalized) &&
13
+ !EXCLUDED_AIW.test(normalized) &&
14
+ !SENSITIVE.test(normalized) &&
15
+ TEXT_FILE.test(normalized));
16
+ }
17
+ function redactSecrets(content) {
18
+ return content
19
+ .replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/g, "[REDACTED]")
20
+ .replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[REDACTED]")
21
+ .replace(/\b(api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|password)\b(\s*[=:]\s*)["']?[^\s,"'}]+/gi, "$1$2[REDACTED]");
22
+ }
23
+ function isBinary(content) {
24
+ return [...content].some((character) => {
25
+ const code = character.codePointAt(0) ?? 0;
26
+ return (code === 0 ||
27
+ (code >= 1 && code <= 8) ||
28
+ code === 11 ||
29
+ code === 12 ||
30
+ (code >= 14 && code <= 31));
31
+ });
32
+ }
33
+ export function createInterpretationRequest(profile, files, maxTokens = 4000) {
34
+ if (!Number.isInteger(maxTokens) || maxTokens <= 0)
35
+ throw new Error("maxTokens must be a positive integer");
36
+ const selectedFiles = [
37
+ ...new Set(files.filter(safeFile).map((file) => file.replaceAll("\\", "/"))),
38
+ ].sort();
39
+ return {
40
+ profile,
41
+ files: selectedFiles,
42
+ maxTokens,
43
+ estimatedTokens: Math.min(maxTokens, JSON.stringify(profile).length + selectedFiles.join("\n").length),
44
+ };
45
+ }
46
+ function parseFacts(output, allowedFiles) {
47
+ let value;
48
+ try {
49
+ value = JSON.parse(output);
50
+ }
51
+ catch {
52
+ throw new Error("AI provider returned invalid JSON");
53
+ }
54
+ const facts = value?.facts;
55
+ if (!Array.isArray(facts))
56
+ throw new Error("AI provider response must contain a facts array");
57
+ return facts.map((item) => {
58
+ if (typeof item.key !== "string" ||
59
+ typeof item.value !== "string" ||
60
+ typeof item.confidence !== "number" ||
61
+ item.confidence < 0 ||
62
+ item.confidence > 1 ||
63
+ !Array.isArray(item.evidence))
64
+ throw new Error("AI provider returned an invalid inferred fact");
65
+ const evidence = item.evidence.map((entry) => {
66
+ const candidate = entry;
67
+ if (typeof candidate.source !== "string" || typeof candidate.reason !== "string")
68
+ throw new Error("AI provider returned invalid evidence");
69
+ if (!allowedFiles.has(candidate.source))
70
+ throw new Error(`AI provider cited a file outside the scoped context: ${candidate.source}`);
71
+ return { source: candidate.source, reason: candidate.reason };
72
+ });
73
+ return {
74
+ key: item.key,
75
+ value: item.value,
76
+ state: "inferred",
77
+ method: "ai-inference",
78
+ confidence: item.confidence,
79
+ requiresConfirmation: item.confidence < 0.9,
80
+ evidence,
81
+ };
82
+ });
83
+ }
84
+ export function createProjectInterpreter(root, provider, contextReader = (path) => readFile(path, "utf8")) {
85
+ const projectRoot = resolve(root);
86
+ return {
87
+ async interpret(request) {
88
+ const scoped = createInterpretationRequest(request.profile, request.files, request.maxTokens);
89
+ const sections = [];
90
+ for (const file of scoped.files) {
91
+ const absolute = resolve(projectRoot, file);
92
+ if (relative(projectRoot, absolute).startsWith(".."))
93
+ continue;
94
+ try {
95
+ if ((await lstat(absolute)).isSymbolicLink())
96
+ continue;
97
+ }
98
+ catch {
99
+ // Injected context readers may provide virtual files in tests or adapters.
100
+ }
101
+ const content = await contextReader(absolute);
102
+ if (isBinary(content))
103
+ continue;
104
+ sections.push(`## ${file}\n${redactSecrets(content)}`);
105
+ }
106
+ if (sections.length === 0)
107
+ return [];
108
+ const output = await provider.generate({
109
+ system: "Interpret project conventions from only the supplied context. Return JSON with a facts array. Each fact must have key, string value, confidence from 0 to 1, and evidence entries with source and reason. Do not infer secrets or repeat credentials.",
110
+ prompt: `Detected profile:\n${JSON.stringify(scoped.profile)}\n\nScoped project context:\n${sections.join("\n\n")}`,
111
+ maxTokens: scoped.maxTokens,
112
+ responseFormat: "json",
113
+ });
114
+ return parseFacts(output, new Set(scoped.files));
115
+ },
116
+ };
117
+ }
@@ -0,0 +1,12 @@
1
+ import type { PackageContract } from "./package-contract.js";
2
+ export type LockedPackage = {
3
+ id: string;
4
+ version: string;
5
+ provider: string;
6
+ source: string;
7
+ integrity: string;
8
+ permissions?: string[];
9
+ };
10
+ export declare function resolvePackage(pkg: PackageContract): LockedPackage;
11
+ export declare function serializeLock(packages: LockedPackage[]): string;
12
+ export declare function parseLock(content: string): LockedPackage[];