@appsforgood/next-supabase-kit 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/BEST_PRACTICE_EVIDENCE.md +45 -0
- package/CHANGELOG.md +44 -0
- package/CODE_OF_CONDUCT.md +26 -0
- package/CONTRIBUTING.md +48 -0
- package/DOGFOOD.md +121 -0
- package/GOVERNANCE.md +45 -0
- package/LICENSE +21 -0
- package/README.md +251 -0
- package/REPOSITORY_SETTINGS.md +70 -0
- package/RESEARCH_CITATION_POLICY.md +26 -0
- package/SECURITY.md +29 -0
- package/SUPPLY_CHAIN.md +55 -0
- package/SUPPORT.md +28 -0
- package/UPGRADE.md +77 -0
- package/agents/deployment-observability-engineer.md +13 -0
- package/agents/docs-maintainer.md +17 -0
- package/agents/frontend-design-lead.md +22 -0
- package/agents/lead-architect.md +25 -0
- package/agents/marketing-copy-lead.md +20 -0
- package/agents/nextjs-engineer.md +20 -0
- package/agents/planner.md +20 -0
- package/agents/qa-engineer.md +19 -0
- package/agents/research-analyst.md +13 -0
- package/agents/security-reviewer.md +16 -0
- package/agents/supabase-postgres-engineer.md +19 -0
- package/assistant-adapters/README.md +28 -0
- package/assistant-adapters/claude-code-subagents.md +37 -0
- package/assistant-adapters/codex-agents.md +35 -0
- package/assistant-adapters/cursor-agent-kit.mdc +30 -0
- package/assistant-adapters/github-copilot-instructions.md +35 -0
- package/assistant-adapters/github-next-supabase.instructions.md +28 -0
- package/assistant-adapters/model-selection/claude-code-subagents-with-models.md +32 -0
- package/assistant-adapters/model-selection/codex-config.example.toml +29 -0
- package/assistant-adapters/model-selection/cursor-model-selection.mdc +24 -0
- package/assistant-adapters/model-selection/github-copilot-model-selection.md +20 -0
- package/checklists/accessibility.md +12 -0
- package/checklists/agent-council.md +13 -0
- package/checklists/brand-content.md +15 -0
- package/checklists/deployment.md +10 -0
- package/checklists/design-critique.md +13 -0
- package/checklists/frontend-distinctiveness.md +12 -0
- package/checklists/frontend-product-quality.md +13 -0
- package/checklists/frontend-quality.md +20 -0
- package/checklists/marketing-copy.md +11 -0
- package/checklists/owasp.md +12 -0
- package/checklists/rls.md +10 -0
- package/checklists/testing.md +12 -0
- package/checklists/upgrade.md +13 -0
- package/checklists/visual-regression.md +11 -0
- package/design-adapters/claude-design.prompt.md +27 -0
- package/design-adapters/figma.prompt.md +18 -0
- package/design-adapters/google-stitch.prompt.md +36 -0
- package/design-adapters/human-designer-brief.prompt.md +36 -0
- package/design-briefs/admin-dashboard.md +21 -0
- package/design-briefs/ai-workflow-product.md +25 -0
- package/design-briefs/community-social.md +26 -0
- package/design-briefs/content-app.md +21 -0
- package/design-briefs/ecommerce.md +25 -0
- package/design-briefs/education-course.md +25 -0
- package/design-briefs/marketplace.md +21 -0
- package/design-briefs/portfolio-venue.md +25 -0
- package/design-briefs/saas.md +21 -0
- package/design-briefs/tool.md +21 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3521 -0
- package/dist/index.js.map +1 -0
- package/examples/next-supabase-installed/.agent-kit/agent-roster.json +228 -0
- package/examples/next-supabase-installed/.agent-kit/manifest.json +58 -0
- package/examples/next-supabase-installed/.agent-kit/model-routing.json +164 -0
- package/examples/next-supabase-installed/.agent-kit/overrides.json +9 -0
- package/examples/next-supabase-installed/README.md +15 -0
- package/examples/next-supabase-installed/audit-output.json +336 -0
- package/examples/next-supabase-installed/tree.txt +38 -0
- package/model-routing/default-model-routing.json +164 -0
- package/package.json +98 -0
- package/profiles/admin-app.md +17 -0
- package/profiles/content-app.md +17 -0
- package/profiles/marketplace.md +17 -0
- package/profiles/saas.md +17 -0
- package/profiles/stack-next-firebase.md +25 -0
- package/profiles/stack-next-postgres.md +24 -0
- package/profiles/stack-remix-supabase.md +24 -0
- package/prompts/audit-project-setup.md +28 -0
- package/prompts/brand-content-intake.md +17 -0
- package/prompts/copy-review.md +15 -0
- package/prompts/council-session-review.md +17 -0
- package/prompts/creative-direction-matrix.md +22 -0
- package/prompts/design-critique-gate.md +28 -0
- package/prompts/docs-update.md +16 -0
- package/prompts/frontend-design-review.md +29 -0
- package/prompts/frontend-distinctiveness-benchmark.md +32 -0
- package/prompts/frontend-product-quality-scorecard.md +35 -0
- package/prompts/implement-feature.md +14 -0
- package/prompts/migration-review.md +14 -0
- package/prompts/screenshot-review.md +27 -0
- package/prompts/security-review.md +17 -0
- package/prompts/upgrade-review.md +18 -0
- package/prompts/visual-qa-plan.md +16 -0
- package/research/proposed-updates.md +70 -0
- package/research/scan-config.json +261 -0
- package/research/scan-plan.md +24 -0
- package/research/summaries/.gitkeep +1 -0
- package/research/summaries/agent-workflow-patterns.md +37 -0
- package/research/summaries/creative-design-patterns.md +38 -0
- package/research/summaries/design-critique-patterns.md +34 -0
- package/research/summaries/docs-and-agent-patterns.md +64 -0
- package/research/summaries/dogfood-adoption-patterns.md +33 -0
- package/research/summaries/frontend-design-patterns.md +64 -0
- package/research/summaries/frontend-distinctiveness-benchmark-patterns.md +38 -0
- package/research/summaries/frontend-product-quality-rubric-patterns.md +37 -0
- package/research/summaries/maturity-model-patterns.md +29 -0
- package/research/summaries/nextjs-patterns.md +65 -0
- package/research/summaries/repo-health-patterns.md +41 -0
- package/research/summaries/scan-overview.md +46 -0
- package/research/summaries/security-patterns.md +64 -0
- package/research/summaries/supabase-rls-patterns.md +54 -0
- package/research/summaries/supply-chain-patterns.md +38 -0
- package/research/summaries/testing-patterns.md +63 -0
- package/research/summaries/upgrade-lifecycle-patterns.md +26 -0
- package/research/summaries/visual-qa-patterns.md +39 -0
- package/rosters/next-supabase-default-council.json +228 -0
- package/schemas/agent-roster.schema.json +54 -0
- package/schemas/audit-report.schema.json +50 -0
- package/schemas/correction-rules.schema.json +32 -0
- package/schemas/council-session.schema.json +65 -0
- package/schemas/model-routing.schema.json +72 -0
- package/schemas/project-context.schema.json +94 -0
- package/schemas/session-event.schema.json +46 -0
- package/schemas/studio-session.schema.json +48 -0
- package/skills/accessibility-wcag.md +15 -0
- package/skills/agent-handoff-tracing.md +44 -0
- package/skills/best-practice-maturity-review.md +26 -0
- package/skills/content-first-design.md +50 -0
- package/skills/conversion-copywriting.md +38 -0
- package/skills/deployment-observability.md +14 -0
- package/skills/docs-maintainer.md +19 -0
- package/skills/frontend-design-system.md +68 -0
- package/skills/frontend-distinctiveness-benchmark.md +40 -0
- package/skills/frontend-product-quality-rubric.md +59 -0
- package/skills/landing-page-copy.md +29 -0
- package/skills/nextjs-app-router.md +18 -0
- package/skills/onboarding-empty-state-copy.md +37 -0
- package/skills/owasp-security-review.md +19 -0
- package/skills/planning-council.md +21 -0
- package/skills/positioning-messaging.md +42 -0
- package/skills/postgres-migrations.md +14 -0
- package/skills/product-voice-tone.md +35 -0
- package/skills/reference-led-design-critique.md +48 -0
- package/skills/supabase-auth-rls.md +20 -0
- package/skills/testing-qa.md +15 -0
- package/skills/upgrade-maintenance.md +32 -0
- package/skills/visual-regression-qa.md +42 -0
- package/templates/next-supabase/AGENTS.md +138 -0
- package/templates/next-supabase/AGENT_ROSTER.md +98 -0
- package/templates/next-supabase/ASSISTANT_ADAPTERS.md +82 -0
- package/templates/next-supabase/COUNCIL.md +54 -0
- package/templates/next-supabase/DECISIONS.md +45 -0
- package/templates/next-supabase/DEPLOYMENT.md +45 -0
- package/templates/next-supabase/DESIGN.md +171 -0
- package/templates/next-supabase/DOCS.md +62 -0
- package/templates/next-supabase/MESSAGING.md +81 -0
- package/templates/next-supabase/MODEL_ROUTING.md +109 -0
- package/templates/next-supabase/QUALITY_GATES.md +87 -0
- package/templates/next-supabase/SECURITY.md +54 -0
- package/templates/next-supabase/SKILLS.md +221 -0
- package/templates/next-supabase/SPEC.md +114 -0
- package/templates/next-supabase/STYLE_GUIDE.md +104 -0
- package/templates/next-supabase/TESTING.md +68 -0
- package/templates/next-supabase/UPGRADE.md +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3521 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/install/add-skill.ts
|
|
7
|
+
import { existsSync as existsSync3 } from "fs";
|
|
8
|
+
import { join as join3 } from "path";
|
|
9
|
+
|
|
10
|
+
// src/utils/fs.ts
|
|
11
|
+
import { createHash } from "crypto";
|
|
12
|
+
import {
|
|
13
|
+
cpSync,
|
|
14
|
+
existsSync,
|
|
15
|
+
lstatSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
readdirSync,
|
|
19
|
+
writeFileSync
|
|
20
|
+
} from "fs";
|
|
21
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
|
|
22
|
+
function ensureDir(path) {
|
|
23
|
+
mkdirSync(path, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
function readTextIfExists(path) {
|
|
26
|
+
if (!existsSync(path)) return null;
|
|
27
|
+
return readFileSync(path, "utf8");
|
|
28
|
+
}
|
|
29
|
+
function writeText(path, content) {
|
|
30
|
+
ensureDir(dirname(path));
|
|
31
|
+
writeFileSync(path, content);
|
|
32
|
+
}
|
|
33
|
+
function sha256(content) {
|
|
34
|
+
return createHash("sha256").update(content).digest("hex");
|
|
35
|
+
}
|
|
36
|
+
function resolveInside(root, requestedPath) {
|
|
37
|
+
const resolvedRoot = resolve(root);
|
|
38
|
+
const resolvedPath = resolve(resolvedRoot, requestedPath);
|
|
39
|
+
const rel = relative(resolvedRoot, resolvedPath);
|
|
40
|
+
if (rel === ".." || rel.startsWith("..") || isAbsolute(rel)) {
|
|
41
|
+
throw new Error(`Unsafe path outside root: ${requestedPath}`);
|
|
42
|
+
}
|
|
43
|
+
return resolvedPath;
|
|
44
|
+
}
|
|
45
|
+
function listFilesRecursive(root) {
|
|
46
|
+
if (!existsSync(root)) return [];
|
|
47
|
+
const out = [];
|
|
48
|
+
const visit = (dir) => {
|
|
49
|
+
for (const entry of readdirSync(dir)) {
|
|
50
|
+
const fullPath = join(dir, entry);
|
|
51
|
+
let stats;
|
|
52
|
+
try {
|
|
53
|
+
stats = lstatSync(fullPath);
|
|
54
|
+
} catch {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (stats.isSymbolicLink()) continue;
|
|
58
|
+
if (stats.isDirectory()) {
|
|
59
|
+
if (entry === "node_modules" || entry === ".git") continue;
|
|
60
|
+
visit(fullPath);
|
|
61
|
+
} else {
|
|
62
|
+
out.push(relative(root, fullPath));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
visit(root);
|
|
67
|
+
return out.sort();
|
|
68
|
+
}
|
|
69
|
+
function copyTextWithConflict(sourcePath, targetRoot, targetRelativePath, options = {}) {
|
|
70
|
+
const targetPath = resolveInside(targetRoot, targetRelativePath);
|
|
71
|
+
const sourceContent = readFileSync(sourcePath, "utf8");
|
|
72
|
+
const existingContent = readTextIfExists(targetPath);
|
|
73
|
+
if (existingContent === null) {
|
|
74
|
+
writeText(targetPath, sourceContent);
|
|
75
|
+
return { action: "created", target: targetRelativePath };
|
|
76
|
+
}
|
|
77
|
+
if (sha256(existingContent) === sha256(sourceContent)) {
|
|
78
|
+
return { action: "unchanged", target: targetRelativePath };
|
|
79
|
+
}
|
|
80
|
+
if (options.force) {
|
|
81
|
+
writeText(targetPath, sourceContent);
|
|
82
|
+
return { action: "overwritten", target: targetRelativePath };
|
|
83
|
+
}
|
|
84
|
+
const conflictRoot = options.conflictRoot ?? join(targetRoot, ".agent-kit", "conflicts");
|
|
85
|
+
const safeName = `${Date.now()}-${targetRelativePath.replace(/[^a-zA-Z0-9_.-]/g, "_")}`;
|
|
86
|
+
const conflictPath = join(conflictRoot, safeName);
|
|
87
|
+
writeText(conflictPath, sourceContent);
|
|
88
|
+
return {
|
|
89
|
+
action: "conflict",
|
|
90
|
+
target: targetRelativePath,
|
|
91
|
+
conflictPath: relative(targetRoot, conflictPath)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function copyDirectory(sourceRoot, targetRoot) {
|
|
95
|
+
ensureDir(dirname(targetRoot));
|
|
96
|
+
cpSync(sourceRoot, targetRoot, {
|
|
97
|
+
recursive: true,
|
|
98
|
+
force: true,
|
|
99
|
+
filter: (source) => !basename(source).startsWith(".DS_Store")
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/utils/package-root.ts
|
|
104
|
+
import { existsSync as existsSync2 } from "fs";
|
|
105
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
106
|
+
import { fileURLToPath } from "url";
|
|
107
|
+
function findPackageRoot(start = dirname2(fileURLToPath(import.meta.url))) {
|
|
108
|
+
let current = resolve2(start);
|
|
109
|
+
for (let i = 0; i < 8; i += 1) {
|
|
110
|
+
if (existsSync2(join2(current, "package.json"))) {
|
|
111
|
+
return current;
|
|
112
|
+
}
|
|
113
|
+
const parent = dirname2(current);
|
|
114
|
+
if (parent === current) break;
|
|
115
|
+
current = parent;
|
|
116
|
+
}
|
|
117
|
+
throw new Error("Unable to locate package root from runtime path.");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/install/add-skill.ts
|
|
121
|
+
function listSkills() {
|
|
122
|
+
const packageRoot = findPackageRoot();
|
|
123
|
+
return listFilesRecursive(join3(packageRoot, "skills")).filter((file) => file.endsWith(".md"));
|
|
124
|
+
}
|
|
125
|
+
function addSkill(cwd, skillName, options = {}) {
|
|
126
|
+
const packageRoot = findPackageRoot();
|
|
127
|
+
const normalized = skillName.endsWith(".md") ? skillName : `${skillName}.md`;
|
|
128
|
+
if (!/^[a-z0-9-]+\.md$/.test(normalized)) {
|
|
129
|
+
throw new Error("Skill names may contain only lowercase letters, numbers, and hyphens.");
|
|
130
|
+
}
|
|
131
|
+
const sourcePath = join3(packageRoot, "skills", normalized);
|
|
132
|
+
if (!existsSync3(sourcePath)) {
|
|
133
|
+
const available = listSkills().join(", ");
|
|
134
|
+
throw new Error(`Unknown skill "${skillName}". Available skills: ${available}`);
|
|
135
|
+
}
|
|
136
|
+
const result = copyTextWithConflict(sourcePath, cwd, join3(".agent-kit", "skills", normalized), {
|
|
137
|
+
force: Boolean(options.force),
|
|
138
|
+
conflictRoot: join3(cwd, ".agent-kit", "conflicts")
|
|
139
|
+
});
|
|
140
|
+
return `${result.action}: ${result.target}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/install/audit.ts
|
|
144
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, statSync } from "fs";
|
|
145
|
+
import { join as join6 } from "path";
|
|
146
|
+
|
|
147
|
+
// src/config/contracts.ts
|
|
148
|
+
import { z } from "zod";
|
|
149
|
+
var stringList = z.array(z.string());
|
|
150
|
+
var AgentRosterContract = z.object({
|
|
151
|
+
schemaVersion: z.literal(1),
|
|
152
|
+
id: z.string().min(1),
|
|
153
|
+
stack: z.string().min(1),
|
|
154
|
+
required: z.boolean(),
|
|
155
|
+
defaultWorkflow: z.string().min(1),
|
|
156
|
+
principle: z.string().optional(),
|
|
157
|
+
agents: z.array(
|
|
158
|
+
z.object({
|
|
159
|
+
id: z.string().min(1),
|
|
160
|
+
name: z.string().optional(),
|
|
161
|
+
file: z.string().optional(),
|
|
162
|
+
defaultFor: stringList.optional(),
|
|
163
|
+
skills: z.array(z.string()).min(1),
|
|
164
|
+
handsOffTo: stringList.optional()
|
|
165
|
+
}).strict()
|
|
166
|
+
).min(1),
|
|
167
|
+
workflows: z.array(
|
|
168
|
+
z.object({
|
|
169
|
+
id: z.string().min(1),
|
|
170
|
+
triggers: stringList.optional(),
|
|
171
|
+
sequence: z.array(z.string()).min(1),
|
|
172
|
+
council: stringList,
|
|
173
|
+
requiredOutputs: stringList
|
|
174
|
+
}).strict()
|
|
175
|
+
).min(1),
|
|
176
|
+
handoffRules: z.array(z.string()).min(1)
|
|
177
|
+
}).strict();
|
|
178
|
+
var CouncilSessionContract = z.object({
|
|
179
|
+
schemaVersion: z.literal(1),
|
|
180
|
+
sessionId: z.string().min(1),
|
|
181
|
+
createdAt: z.string().datetime(),
|
|
182
|
+
workflowId: z.string().min(1),
|
|
183
|
+
status: z.enum(["planned", "in-progress", "blocked", "complete"]),
|
|
184
|
+
request: z.string().min(1),
|
|
185
|
+
affectedLayers: stringList.optional(),
|
|
186
|
+
handoffs: z.array(
|
|
187
|
+
z.object({
|
|
188
|
+
agentId: z.string().min(1),
|
|
189
|
+
decision: z.string().min(1),
|
|
190
|
+
risk: z.string().min(1),
|
|
191
|
+
nextHandoff: z.string().min(1),
|
|
192
|
+
evidence: stringList.optional()
|
|
193
|
+
}).strict()
|
|
194
|
+
).min(1),
|
|
195
|
+
requiredOutputs: z.array(
|
|
196
|
+
z.object({
|
|
197
|
+
name: z.string().min(1),
|
|
198
|
+
status: z.enum(["missing", "partial", "complete", "not-applicable"]),
|
|
199
|
+
evidence: z.string().optional()
|
|
200
|
+
}).strict()
|
|
201
|
+
),
|
|
202
|
+
verification: z.array(
|
|
203
|
+
z.object({
|
|
204
|
+
command: z.string().min(1),
|
|
205
|
+
result: z.enum(["pass", "fail", "skipped"]),
|
|
206
|
+
notes: z.string().optional()
|
|
207
|
+
}).strict()
|
|
208
|
+
)
|
|
209
|
+
}).strict();
|
|
210
|
+
var AuditReportContract = z.object({
|
|
211
|
+
summary: z.object({
|
|
212
|
+
pass: z.number().int().min(0),
|
|
213
|
+
warn: z.number().int().min(0),
|
|
214
|
+
fail: z.number().int().min(0)
|
|
215
|
+
}).strict(),
|
|
216
|
+
readiness: z.object({
|
|
217
|
+
level: z.enum(["needs-setup", "baseline-setup", "needs-improvement", "best-practice-candidate"]),
|
|
218
|
+
summary: z.string().min(1),
|
|
219
|
+
nextActions: z.array(z.string())
|
|
220
|
+
}).strict(),
|
|
221
|
+
findings: z.array(
|
|
222
|
+
z.object({
|
|
223
|
+
level: z.enum(["pass", "warn", "fail"]),
|
|
224
|
+
area: z.string().min(1),
|
|
225
|
+
message: z.string().min(1),
|
|
226
|
+
remediation: z.string().min(1).optional()
|
|
227
|
+
}).strict()
|
|
228
|
+
)
|
|
229
|
+
}).strict();
|
|
230
|
+
var ModelRoutingContract = z.object({
|
|
231
|
+
schemaVersion: z.literal(1),
|
|
232
|
+
id: z.string().min(1),
|
|
233
|
+
stack: z.string().min(1),
|
|
234
|
+
reviewedAt: z.string().min(1),
|
|
235
|
+
reviewCadence: z.string().min(1),
|
|
236
|
+
principle: z.string().min(1),
|
|
237
|
+
profiles: z.array(
|
|
238
|
+
z.object({
|
|
239
|
+
id: z.string().min(1),
|
|
240
|
+
label: z.string().min(1),
|
|
241
|
+
intent: z.string().min(1),
|
|
242
|
+
reasoningEffort: z.enum(["low", "medium", "high", "varies"]),
|
|
243
|
+
contextWindow: z.enum(["small", "medium", "large", "very-large", "varies"]),
|
|
244
|
+
latency: z.enum(["low", "medium", "high", "varies"]),
|
|
245
|
+
cost: z.enum(["low", "medium", "high", "varies"]),
|
|
246
|
+
preferredFor: z.array(z.string()).min(1)
|
|
247
|
+
}).strict()
|
|
248
|
+
).min(1),
|
|
249
|
+
agentRoutes: z.array(
|
|
250
|
+
z.object({
|
|
251
|
+
agentId: z.string().min(1),
|
|
252
|
+
profileId: z.string().min(1),
|
|
253
|
+
defaultEffort: z.enum(["low", "medium", "high"]),
|
|
254
|
+
escalationProfileId: z.string().optional(),
|
|
255
|
+
notes: z.string().optional()
|
|
256
|
+
}).strict()
|
|
257
|
+
).min(1),
|
|
258
|
+
toolSurfaces: z.array(
|
|
259
|
+
z.object({
|
|
260
|
+
tool: z.string().min(1),
|
|
261
|
+
instructionSurface: z.string().min(1),
|
|
262
|
+
modelSelection: z.string().min(1),
|
|
263
|
+
enforcement: z.enum(["enforced", "partial", "advisory", "manual"]),
|
|
264
|
+
adapter: z.string().min(1)
|
|
265
|
+
}).strict()
|
|
266
|
+
).min(1),
|
|
267
|
+
updatePolicy: z.array(z.string()).min(1)
|
|
268
|
+
}).strict();
|
|
269
|
+
var projectEvidenceItem = z.object({
|
|
270
|
+
source: z.string().min(1),
|
|
271
|
+
note: z.string().min(1)
|
|
272
|
+
}).strict();
|
|
273
|
+
var ProjectContextContract = z.object({
|
|
274
|
+
schemaVersion: z.literal(1),
|
|
275
|
+
projectName: z.string(),
|
|
276
|
+
productSummary: z.string(),
|
|
277
|
+
productCategory: z.string(),
|
|
278
|
+
primaryAudience: z.string(),
|
|
279
|
+
primaryWorkflows: z.array(z.string()),
|
|
280
|
+
businessCriticalBehavior: z.array(z.string()),
|
|
281
|
+
architecture: z.object({
|
|
282
|
+
packageManager: z.string().optional(),
|
|
283
|
+
scripts: z.array(z.string()),
|
|
284
|
+
frameworks: z.array(z.string()),
|
|
285
|
+
uiLibraries: z.array(z.string()),
|
|
286
|
+
hasSupabase: z.boolean(),
|
|
287
|
+
supabaseSignals: z.array(z.string()),
|
|
288
|
+
testTools: z.array(z.string()),
|
|
289
|
+
envExampleKeys: z.array(z.string()),
|
|
290
|
+
deployment: z.array(z.string())
|
|
291
|
+
}).strict(),
|
|
292
|
+
dataSensitivity: z.array(z.string()),
|
|
293
|
+
authModel: z.string(),
|
|
294
|
+
tenantModel: z.string(),
|
|
295
|
+
integrations: z.array(z.string()),
|
|
296
|
+
uiDirection: z.object({
|
|
297
|
+
preferred: z.string(),
|
|
298
|
+
avoid: z.string()
|
|
299
|
+
}).strict(),
|
|
300
|
+
messaging: z.object({
|
|
301
|
+
valueProposition: z.string(),
|
|
302
|
+
proof: z.array(z.string()),
|
|
303
|
+
objections: z.array(z.string())
|
|
304
|
+
}).strict(),
|
|
305
|
+
qualityTarget: z.enum(["baseline-setup", "needs-improvement", "best-practice-candidate"]),
|
|
306
|
+
knownConstraints: z.array(z.string()),
|
|
307
|
+
openQuestions: z.array(z.string()),
|
|
308
|
+
evidence: z.array(projectEvidenceItem),
|
|
309
|
+
lastReviewedAt: z.string().datetime(),
|
|
310
|
+
owners: z.array(z.string())
|
|
311
|
+
}).strict();
|
|
312
|
+
var CorrectionRuleContract = z.object({
|
|
313
|
+
id: z.string().min(1),
|
|
314
|
+
scope: z.enum(["session", "project", "agent", "upstream-proposal"]),
|
|
315
|
+
status: z.enum(["active", "retired", "proposed"]),
|
|
316
|
+
text: z.string().min(1),
|
|
317
|
+
appliesToAgents: z.array(z.string()).optional(),
|
|
318
|
+
agentId: z.string().min(1).optional(),
|
|
319
|
+
sourceSessionId: z.string().min(1).optional(),
|
|
320
|
+
createdAt: z.string().datetime(),
|
|
321
|
+
reviewedAt: z.string().datetime().nullable().optional(),
|
|
322
|
+
retiredAt: z.string().datetime().optional(),
|
|
323
|
+
reason: z.string().optional()
|
|
324
|
+
}).strict().superRefine((rule, context2) => {
|
|
325
|
+
if (rule.scope === "agent" && !rule.agentId && (!rule.appliesToAgents || rule.appliesToAgents.length === 0)) {
|
|
326
|
+
context2.addIssue({
|
|
327
|
+
code: z.ZodIssueCode.custom,
|
|
328
|
+
message: "agent-scoped corrections require agentId or appliesToAgents",
|
|
329
|
+
path: ["agentId"]
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
var CorrectionRulesContract = z.object({
|
|
334
|
+
schemaVersion: z.literal(1),
|
|
335
|
+
rules: z.array(CorrectionRuleContract)
|
|
336
|
+
}).strict();
|
|
337
|
+
var StudioSessionContract = z.object({
|
|
338
|
+
schemaVersion: z.literal(1),
|
|
339
|
+
sessionId: z.string().min(1),
|
|
340
|
+
title: z.string().min(1),
|
|
341
|
+
createdAt: z.string().datetime(),
|
|
342
|
+
updatedAt: z.string().datetime(),
|
|
343
|
+
status: z.enum(["planned", "in-progress", "blocked", "complete"]),
|
|
344
|
+
workflowId: z.string().min(1),
|
|
345
|
+
request: z.string().min(1),
|
|
346
|
+
affectedLayers: z.array(z.string()),
|
|
347
|
+
activeAgentId: z.string().optional(),
|
|
348
|
+
nextAgentId: z.string().optional(),
|
|
349
|
+
qualityTarget: z.enum(["baseline-setup", "needs-improvement", "best-practice-candidate"]),
|
|
350
|
+
requiredOutputs: z.array(
|
|
351
|
+
z.object({
|
|
352
|
+
name: z.string().min(1),
|
|
353
|
+
status: z.enum(["missing", "partial", "complete", "not-applicable"]),
|
|
354
|
+
evidence: z.string().optional()
|
|
355
|
+
}).strict()
|
|
356
|
+
),
|
|
357
|
+
renderedAt: z.string().datetime().optional()
|
|
358
|
+
}).strict();
|
|
359
|
+
var SessionEventContract = z.object({
|
|
360
|
+
type: z.enum([
|
|
361
|
+
"session_started",
|
|
362
|
+
"project_context_loaded",
|
|
363
|
+
"agent_message",
|
|
364
|
+
"agent_decision",
|
|
365
|
+
"handoff",
|
|
366
|
+
"human_correction",
|
|
367
|
+
"correction_promoted",
|
|
368
|
+
"artifact_recorded",
|
|
369
|
+
"command_recorded",
|
|
370
|
+
"verification_recorded",
|
|
371
|
+
"open_question",
|
|
372
|
+
"required_output_updated",
|
|
373
|
+
"session_status_changed",
|
|
374
|
+
"session_rendered"
|
|
375
|
+
]),
|
|
376
|
+
createdAt: z.string().datetime(),
|
|
377
|
+
agentId: z.string().min(1).optional(),
|
|
378
|
+
fromAgentId: z.string().min(1).optional(),
|
|
379
|
+
toAgentId: z.string().min(1).optional(),
|
|
380
|
+
text: z.string().optional(),
|
|
381
|
+
decision: z.string().optional(),
|
|
382
|
+
risk: z.string().optional(),
|
|
383
|
+
evidence: z.array(z.string()).optional(),
|
|
384
|
+
scope: z.enum(["session", "project", "agent", "upstream-proposal"]).optional(),
|
|
385
|
+
correctionId: z.string().min(1).optional(),
|
|
386
|
+
artifactPath: z.string().min(1).optional(),
|
|
387
|
+
command: z.string().min(1).optional(),
|
|
388
|
+
result: z.enum(["pass", "fail", "skipped"]).optional(),
|
|
389
|
+
outputName: z.string().min(1).optional(),
|
|
390
|
+
outputStatus: z.enum(["missing", "partial", "complete", "not-applicable"]).optional(),
|
|
391
|
+
status: z.enum(["planned", "in-progress", "blocked", "complete"]).optional(),
|
|
392
|
+
notes: z.string().optional()
|
|
393
|
+
}).strict().superRefine((event, context2) => {
|
|
394
|
+
if (event.type === "handoff") {
|
|
395
|
+
for (const field of ["fromAgentId", "toAgentId", "decision", "risk"]) {
|
|
396
|
+
if (!event[field]) {
|
|
397
|
+
context2.addIssue({ code: z.ZodIssueCode.custom, message: `${field} is required for handoff events`, path: [field] });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (event.type === "human_correction" && (!event.text || !event.scope)) {
|
|
402
|
+
context2.addIssue({ code: z.ZodIssueCode.custom, message: "human corrections require text and scope", path: ["text"] });
|
|
403
|
+
}
|
|
404
|
+
if (event.type === "verification_recorded" && (!event.command || !event.result)) {
|
|
405
|
+
context2.addIssue({ code: z.ZodIssueCode.custom, message: "verification events require command and result", path: ["command"] });
|
|
406
|
+
}
|
|
407
|
+
if (event.type === "artifact_recorded" && !event.artifactPath) {
|
|
408
|
+
context2.addIssue({ code: z.ZodIssueCode.custom, message: "artifact events require artifactPath", path: ["artifactPath"] });
|
|
409
|
+
}
|
|
410
|
+
if (event.type === "required_output_updated" && (!event.outputName || !event.outputStatus)) {
|
|
411
|
+
context2.addIssue({ code: z.ZodIssueCode.custom, message: "required output events require outputName and outputStatus", path: ["outputName"] });
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
function formatContractIssues(error) {
|
|
415
|
+
return error.issues.map((issue) => {
|
|
416
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "<root>";
|
|
417
|
+
return `${path}: ${issue.message}`;
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/config/defaults.ts
|
|
422
|
+
var PACKAGE_NAME = "@agent-skills/next-supabase-kit";
|
|
423
|
+
var PACKAGE_VERSION = "0.1.0";
|
|
424
|
+
var DEFAULT_CONFIG = {
|
|
425
|
+
stack: "next-supabase",
|
|
426
|
+
projectType: "saas",
|
|
427
|
+
docsMode: "advisory",
|
|
428
|
+
agentCouncil: {
|
|
429
|
+
required: true,
|
|
430
|
+
rosterPath: ".agent-kit/agent-roster.json",
|
|
431
|
+
defaultWorkflow: "planning",
|
|
432
|
+
coreChangeWorkflow: "core-change"
|
|
433
|
+
},
|
|
434
|
+
modelRouting: {
|
|
435
|
+
required: true,
|
|
436
|
+
routingPath: ".agent-kit/model-routing.json",
|
|
437
|
+
reviewCadence: "quarterly-or-when-model-docs-change"
|
|
438
|
+
},
|
|
439
|
+
designProviders: ["stitch", "claude", "figma", "human"],
|
|
440
|
+
research: {
|
|
441
|
+
maxRepos: 100,
|
|
442
|
+
githubTokenEnv: "GITHUB_TOKEN",
|
|
443
|
+
workdir: "research/workdir"
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
var ROOT_DOCS = [
|
|
447
|
+
"AGENTS.md",
|
|
448
|
+
"AGENT_ROSTER.md",
|
|
449
|
+
"ASSISTANT_ADAPTERS.md",
|
|
450
|
+
"COUNCIL.md",
|
|
451
|
+
"SKILLS.md",
|
|
452
|
+
"SPEC.md",
|
|
453
|
+
"DECISIONS.md",
|
|
454
|
+
"DOCS.md",
|
|
455
|
+
"DESIGN.md",
|
|
456
|
+
"MESSAGING.md",
|
|
457
|
+
"MODEL_ROUTING.md",
|
|
458
|
+
"QUALITY_GATES.md",
|
|
459
|
+
"STYLE_GUIDE.md",
|
|
460
|
+
"SECURITY.md",
|
|
461
|
+
"TESTING.md",
|
|
462
|
+
"DEPLOYMENT.md",
|
|
463
|
+
"UPGRADE.md"
|
|
464
|
+
];
|
|
465
|
+
var LIBRARY_FOLDERS = [
|
|
466
|
+
"agents",
|
|
467
|
+
"skills",
|
|
468
|
+
"prompts",
|
|
469
|
+
"checklists",
|
|
470
|
+
"design-adapters",
|
|
471
|
+
"assistant-adapters",
|
|
472
|
+
"design-briefs",
|
|
473
|
+
"profiles",
|
|
474
|
+
"rosters",
|
|
475
|
+
"schemas"
|
|
476
|
+
];
|
|
477
|
+
var DEFAULT_AGENT_ROSTER_SOURCE = "rosters/next-supabase-default-council.json";
|
|
478
|
+
var DEFAULT_AGENT_ROSTER_TARGET = ".agent-kit/agent-roster.json";
|
|
479
|
+
var DEFAULT_MODEL_ROUTING_SOURCE = "model-routing/default-model-routing.json";
|
|
480
|
+
var DEFAULT_MODEL_ROUTING_TARGET = ".agent-kit/model-routing.json";
|
|
481
|
+
var CURSOR_ADAPTER_FILES = [
|
|
482
|
+
{
|
|
483
|
+
source: "assistant-adapters/cursor-agent-kit.mdc",
|
|
484
|
+
target: ".cursor/rules/cursor-agent-kit.mdc"
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
source: "assistant-adapters/model-selection/cursor-model-selection.mdc",
|
|
488
|
+
target: ".cursor/rules/cursor-model-selection.mdc"
|
|
489
|
+
}
|
|
490
|
+
];
|
|
491
|
+
|
|
492
|
+
// src/studio/shared.ts
|
|
493
|
+
import { appendFileSync, existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
494
|
+
import { basename as basename2, dirname as dirname3, join as join4 } from "path";
|
|
495
|
+
var AGENT_KIT_DIR = ".agent-kit";
|
|
496
|
+
var CONTEXT_JSON = ".agent-kit/project-context.json";
|
|
497
|
+
var CONTEXT_MD = ".agent-kit/project-context.md";
|
|
498
|
+
var CORRECTIONS_DIR = ".agent-kit/corrections";
|
|
499
|
+
var PROJECT_RULES_JSON = ".agent-kit/corrections/project-rules.json";
|
|
500
|
+
var AGENT_RULES_JSON = ".agent-kit/corrections/agent-rules.json";
|
|
501
|
+
var UPSTREAM_PROPOSALS_JSON = ".agent-kit/corrections/upstream-proposals.json";
|
|
502
|
+
var COUNCIL_SESSIONS_DIR = ".agent-kit/council-sessions";
|
|
503
|
+
var ACTIVE_SESSION_FILE = ".agent-kit/council-sessions/active";
|
|
504
|
+
var STUDIO_EXPORT_HTML = ".agent-kit/studio/index.html";
|
|
505
|
+
var SECRET_PATTERNS = [
|
|
506
|
+
/gh[pousr]_[A-Za-z0-9_]{12,}/g,
|
|
507
|
+
/github_pat_[A-Za-z0-9_]{20,}/g,
|
|
508
|
+
/sk_(?:live|test)_[A-Za-z0-9_]{8,}/g,
|
|
509
|
+
/sbp_[A-Za-z0-9_]{12,}/g,
|
|
510
|
+
/(?:SUPABASE_SERVICE_ROLE_KEY|DATABASE_URL|OPENAI_API_KEY|ANTHROPIC_API_KEY|GITHUB_TOKEN)=["']?[^"'\s]+/gi,
|
|
511
|
+
/postgres(?:ql)?:\/\/[^\s)]+/gi
|
|
512
|
+
];
|
|
513
|
+
function nowIso() {
|
|
514
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
515
|
+
}
|
|
516
|
+
function redactSensitive(text) {
|
|
517
|
+
return SECRET_PATTERNS.reduce((current, pattern) => current.replace(pattern, "[REDACTED]"), text);
|
|
518
|
+
}
|
|
519
|
+
function containsLikelySecret(text) {
|
|
520
|
+
return SECRET_PATTERNS.some((pattern) => {
|
|
521
|
+
pattern.lastIndex = 0;
|
|
522
|
+
return pattern.test(text);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
function safeSlug(input) {
|
|
526
|
+
const lowered = input.trim().toLowerCase();
|
|
527
|
+
if (!lowered) throw new Error("A non-empty title or id is required.");
|
|
528
|
+
if (lowered.includes("/") || lowered.includes("\\") || lowered.includes("..")) {
|
|
529
|
+
throw new Error(`Unsafe session or correction id: ${input}`);
|
|
530
|
+
}
|
|
531
|
+
const slug = lowered.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
532
|
+
if (!slug) throw new Error(`Could not derive a safe id from: ${input}`);
|
|
533
|
+
return slug;
|
|
534
|
+
}
|
|
535
|
+
function ensureStudioDirs(cwd) {
|
|
536
|
+
ensureDir(join4(cwd, AGENT_KIT_DIR));
|
|
537
|
+
ensureDir(join4(cwd, CORRECTIONS_DIR));
|
|
538
|
+
ensureDir(join4(cwd, COUNCIL_SESSIONS_DIR));
|
|
539
|
+
}
|
|
540
|
+
function readJsonFile(cwd, relativePath) {
|
|
541
|
+
const path = resolveInside(cwd, relativePath);
|
|
542
|
+
if (!existsSync4(path)) return null;
|
|
543
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
544
|
+
}
|
|
545
|
+
function writeJsonFile(cwd, relativePath, value) {
|
|
546
|
+
writeText(resolveInside(cwd, relativePath), `${JSON.stringify(value, null, 2)}
|
|
547
|
+
`);
|
|
548
|
+
}
|
|
549
|
+
function appendJsonLine(cwd, relativePath, value) {
|
|
550
|
+
const path = resolveInside(cwd, relativePath);
|
|
551
|
+
ensureDir(dirname3(path));
|
|
552
|
+
appendFileSync(path, `${JSON.stringify(value)}
|
|
553
|
+
`);
|
|
554
|
+
}
|
|
555
|
+
function readJsonLines(cwd, relativePath) {
|
|
556
|
+
const text = readTextIfExists(resolveInside(cwd, relativePath));
|
|
557
|
+
if (!text) return [];
|
|
558
|
+
return text.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
|
|
559
|
+
}
|
|
560
|
+
function writeTextFile(cwd, relativePath, content) {
|
|
561
|
+
writeText(resolveInside(cwd, relativePath), content);
|
|
562
|
+
}
|
|
563
|
+
function readTextFile(cwd, relativePath) {
|
|
564
|
+
return readTextIfExists(resolveInside(cwd, relativePath));
|
|
565
|
+
}
|
|
566
|
+
function validateRelativeArtifactPath(cwd, requestedPath) {
|
|
567
|
+
const resolved = resolveInside(cwd, requestedPath);
|
|
568
|
+
if (basename2(resolved).startsWith(".")) {
|
|
569
|
+
throw new Error(`Hidden files cannot be recorded as artifacts: ${requestedPath}`);
|
|
570
|
+
}
|
|
571
|
+
return requestedPath.replace(/\\/g, "/");
|
|
572
|
+
}
|
|
573
|
+
function escapeMarkdownText(value) {
|
|
574
|
+
return redactSensitive(value ?? "").replace(/\\/g, "\\\\").replace(/</g, "<").replace(/>/g, ">").replace(/[`![\]()|]/g, "\\$&").replace(/\r?\n/g, "<br>");
|
|
575
|
+
}
|
|
576
|
+
function escapeMarkdownTableCell(value) {
|
|
577
|
+
return escapeMarkdownText(value).replace(/\|/g, "\\|");
|
|
578
|
+
}
|
|
579
|
+
function listMarkdown(items) {
|
|
580
|
+
if (items.length === 0) return "- None recorded.\n";
|
|
581
|
+
return items.map((item) => `- ${escapeMarkdownText(item)}`).join("\n") + "\n";
|
|
582
|
+
}
|
|
583
|
+
function unique(values) {
|
|
584
|
+
return [...new Set(values.filter(Boolean))].sort();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/install/install.ts
|
|
588
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
589
|
+
import { join as join5 } from "path";
|
|
590
|
+
function initProject(options) {
|
|
591
|
+
const cwd = options.cwd;
|
|
592
|
+
const stack = options.stack ?? DEFAULT_CONFIG.stack;
|
|
593
|
+
const packageRoot = findPackageRoot();
|
|
594
|
+
const templateRoot = join5(packageRoot, "templates", stack);
|
|
595
|
+
if (!existsSync5(templateRoot)) {
|
|
596
|
+
throw new Error(`Unsupported stack profile: ${stack}`);
|
|
597
|
+
}
|
|
598
|
+
ensureDir(join5(cwd, ".agent-kit"));
|
|
599
|
+
ensureDir(join5(cwd, ".agent-kit", "conflicts"));
|
|
600
|
+
const result = {
|
|
601
|
+
copied: [],
|
|
602
|
+
unchanged: [],
|
|
603
|
+
conflicts: [],
|
|
604
|
+
overwritten: [],
|
|
605
|
+
manifestPath: ".agent-kit/manifest.json"
|
|
606
|
+
};
|
|
607
|
+
const templateHashes = {};
|
|
608
|
+
for (const doc of ROOT_DOCS) {
|
|
609
|
+
const templatePath = join5(templateRoot, doc);
|
|
610
|
+
templateHashes[doc] = sha256(readFileSync3(templatePath, "utf8"));
|
|
611
|
+
const copyResult = copyTextWithConflict(templatePath, cwd, doc, {
|
|
612
|
+
force: Boolean(options.force),
|
|
613
|
+
conflictRoot: join5(cwd, ".agent-kit", "conflicts")
|
|
614
|
+
});
|
|
615
|
+
if (copyResult.action === "created") result.copied.push(copyResult.target);
|
|
616
|
+
if (copyResult.action === "unchanged") result.unchanged.push(copyResult.target);
|
|
617
|
+
if (copyResult.action === "overwritten") result.overwritten.push(copyResult.target);
|
|
618
|
+
if (copyResult.action === "conflict") {
|
|
619
|
+
result.conflicts.push(`${copyResult.target} -> ${copyResult.conflictPath}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
for (const folder of LIBRARY_FOLDERS) {
|
|
623
|
+
copyDirectory(join5(packageRoot, folder), join5(cwd, ".agent-kit", folder));
|
|
624
|
+
}
|
|
625
|
+
for (const adapter of CURSOR_ADAPTER_FILES) {
|
|
626
|
+
const adapterCopy = copyTextWithConflict(join5(packageRoot, adapter.source), cwd, adapter.target, {
|
|
627
|
+
force: Boolean(options.force),
|
|
628
|
+
conflictRoot: join5(cwd, ".agent-kit", "conflicts")
|
|
629
|
+
});
|
|
630
|
+
if (adapterCopy.action === "created") result.copied.push(adapterCopy.target);
|
|
631
|
+
if (adapterCopy.action === "unchanged") result.unchanged.push(adapterCopy.target);
|
|
632
|
+
if (adapterCopy.action === "overwritten") result.overwritten.push(adapterCopy.target);
|
|
633
|
+
if (adapterCopy.action === "conflict") {
|
|
634
|
+
result.conflicts.push(`${adapterCopy.target} -> ${adapterCopy.conflictPath}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const rosterCopy = copyTextWithConflict(join5(packageRoot, DEFAULT_AGENT_ROSTER_SOURCE), cwd, DEFAULT_AGENT_ROSTER_TARGET, {
|
|
638
|
+
force: Boolean(options.force),
|
|
639
|
+
conflictRoot: join5(cwd, ".agent-kit", "conflicts")
|
|
640
|
+
});
|
|
641
|
+
if (rosterCopy.action === "created") result.copied.push(rosterCopy.target);
|
|
642
|
+
if (rosterCopy.action === "unchanged") result.unchanged.push(rosterCopy.target);
|
|
643
|
+
if (rosterCopy.action === "overwritten") result.overwritten.push(rosterCopy.target);
|
|
644
|
+
if (rosterCopy.action === "conflict") result.conflicts.push(`${rosterCopy.target} -> ${rosterCopy.conflictPath}`);
|
|
645
|
+
const modelRoutingCopy = copyTextWithConflict(join5(packageRoot, DEFAULT_MODEL_ROUTING_SOURCE), cwd, DEFAULT_MODEL_ROUTING_TARGET, {
|
|
646
|
+
force: Boolean(options.force),
|
|
647
|
+
conflictRoot: join5(cwd, ".agent-kit", "conflicts")
|
|
648
|
+
});
|
|
649
|
+
if (modelRoutingCopy.action === "created") result.copied.push(modelRoutingCopy.target);
|
|
650
|
+
if (modelRoutingCopy.action === "unchanged") result.unchanged.push(modelRoutingCopy.target);
|
|
651
|
+
if (modelRoutingCopy.action === "overwritten") result.overwritten.push(modelRoutingCopy.target);
|
|
652
|
+
if (modelRoutingCopy.action === "conflict") result.conflicts.push(`${modelRoutingCopy.target} -> ${modelRoutingCopy.conflictPath}`);
|
|
653
|
+
const manifest = {
|
|
654
|
+
packageName: PACKAGE_NAME,
|
|
655
|
+
packageVersion: PACKAGE_VERSION,
|
|
656
|
+
stack,
|
|
657
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
658
|
+
docs: [...ROOT_DOCS],
|
|
659
|
+
libraryFolders: [...LIBRARY_FOLDERS],
|
|
660
|
+
agentRoster: DEFAULT_AGENT_ROSTER_TARGET,
|
|
661
|
+
modelRouting: DEFAULT_MODEL_ROUTING_TARGET,
|
|
662
|
+
templateHashes
|
|
663
|
+
};
|
|
664
|
+
writeText(join5(cwd, ".agent-kit", "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
665
|
+
`);
|
|
666
|
+
writeText(join5(cwd, ".agent-kit", "config.json"), `${JSON.stringify(DEFAULT_CONFIG, null, 2)}
|
|
667
|
+
`);
|
|
668
|
+
const overridesPath = join5(cwd, ".agent-kit", "overrides.json");
|
|
669
|
+
if (!existsSync5(overridesPath)) writeText(overridesPath, `${JSON.stringify({ templates: {} }, null, 2)}
|
|
670
|
+
`);
|
|
671
|
+
return result;
|
|
672
|
+
}
|
|
673
|
+
function readManifest(cwd) {
|
|
674
|
+
const manifestPath = join5(cwd, ".agent-kit", "manifest.json");
|
|
675
|
+
if (!existsSync5(manifestPath)) return null;
|
|
676
|
+
return JSON.parse(readFileSync3(manifestPath, "utf8"));
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/install/audit.ts
|
|
680
|
+
var REQUIRED_AGENT_IDS = [
|
|
681
|
+
"planner",
|
|
682
|
+
"lead-architect",
|
|
683
|
+
"nextjs-engineer",
|
|
684
|
+
"supabase-postgres-engineer",
|
|
685
|
+
"security-reviewer",
|
|
686
|
+
"frontend-design-lead",
|
|
687
|
+
"marketing-copy-lead",
|
|
688
|
+
"qa-engineer",
|
|
689
|
+
"docs-maintainer",
|
|
690
|
+
"deployment-observability-engineer"
|
|
691
|
+
];
|
|
692
|
+
var REQUIRED_SKILL_IDS = [
|
|
693
|
+
"planning-council",
|
|
694
|
+
"best-practice-maturity-review",
|
|
695
|
+
"upgrade-maintenance",
|
|
696
|
+
"nextjs-app-router",
|
|
697
|
+
"supabase-auth-rls",
|
|
698
|
+
"postgres-migrations",
|
|
699
|
+
"owasp-security-review",
|
|
700
|
+
"agent-handoff-tracing",
|
|
701
|
+
"content-first-design",
|
|
702
|
+
"reference-led-design-critique",
|
|
703
|
+
"frontend-distinctiveness-benchmark",
|
|
704
|
+
"frontend-product-quality-rubric",
|
|
705
|
+
"frontend-design-system",
|
|
706
|
+
"visual-regression-qa",
|
|
707
|
+
"positioning-messaging",
|
|
708
|
+
"conversion-copywriting",
|
|
709
|
+
"landing-page-copy",
|
|
710
|
+
"product-voice-tone",
|
|
711
|
+
"onboarding-empty-state-copy",
|
|
712
|
+
"accessibility-wcag",
|
|
713
|
+
"testing-qa",
|
|
714
|
+
"docs-maintainer",
|
|
715
|
+
"deployment-observability"
|
|
716
|
+
];
|
|
717
|
+
var REQUIRED_SCHEMA_FILES = [
|
|
718
|
+
"agent-roster.schema.json",
|
|
719
|
+
"council-session.schema.json",
|
|
720
|
+
"audit-report.schema.json",
|
|
721
|
+
"model-routing.schema.json",
|
|
722
|
+
"project-context.schema.json",
|
|
723
|
+
"correction-rules.schema.json",
|
|
724
|
+
"session-event.schema.json",
|
|
725
|
+
"studio-session.schema.json"
|
|
726
|
+
];
|
|
727
|
+
var COUNCIL_SESSION_DIR = ".agent-kit/council-sessions";
|
|
728
|
+
var READINESS_ORDER = ["needs-setup", "baseline-setup", "needs-improvement", "best-practice-candidate"];
|
|
729
|
+
function isAuditReadinessLevel(value) {
|
|
730
|
+
return READINESS_ORDER.includes(value);
|
|
731
|
+
}
|
|
732
|
+
function meetsMinimumReadiness(actual, minimum) {
|
|
733
|
+
return READINESS_ORDER.indexOf(actual) >= READINESS_ORDER.indexOf(minimum);
|
|
734
|
+
}
|
|
735
|
+
function includesAny(text, values) {
|
|
736
|
+
const lower = text.toLowerCase();
|
|
737
|
+
return values.some((value) => lower.includes(value.toLowerCase()));
|
|
738
|
+
}
|
|
739
|
+
function includesAll(text, values) {
|
|
740
|
+
const lower = text.toLowerCase();
|
|
741
|
+
return values.every((value) => lower.includes(value.toLowerCase()));
|
|
742
|
+
}
|
|
743
|
+
function readDoc(cwd, file) {
|
|
744
|
+
const path = join6(cwd, file);
|
|
745
|
+
return existsSync6(path) ? readFileSync4(path, "utf8") : "";
|
|
746
|
+
}
|
|
747
|
+
function readOverrides(cwd) {
|
|
748
|
+
const path = join6(cwd, ".agent-kit", "overrides.json");
|
|
749
|
+
if (!existsSync6(path)) return {};
|
|
750
|
+
try {
|
|
751
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
752
|
+
const templates = parsed.templates ?? {};
|
|
753
|
+
return Object.fromEntries(
|
|
754
|
+
Object.entries(templates).map(([file, override]) => [
|
|
755
|
+
file,
|
|
756
|
+
typeof override === "string" ? { reason: override } : override && typeof override === "object" ? override : { reason: String(override) }
|
|
757
|
+
])
|
|
758
|
+
);
|
|
759
|
+
} catch {
|
|
760
|
+
return {};
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function readTemplate(stack, file) {
|
|
764
|
+
const path = join6(findPackageRoot(), "templates", stack, file);
|
|
765
|
+
return existsSync6(path) ? readFileSync4(path, "utf8") : null;
|
|
766
|
+
}
|
|
767
|
+
function asStringArray(value) {
|
|
768
|
+
if (!Array.isArray(value)) return [];
|
|
769
|
+
return value.filter((item) => typeof item === "string");
|
|
770
|
+
}
|
|
771
|
+
function isRecord(value) {
|
|
772
|
+
return typeof value === "object" && value !== null;
|
|
773
|
+
}
|
|
774
|
+
function addAgentRosterFindings(cwd, findings) {
|
|
775
|
+
const rosterPath = join6(cwd, DEFAULT_AGENT_ROSTER_TARGET);
|
|
776
|
+
if (!existsSync6(rosterPath)) {
|
|
777
|
+
findings.push({
|
|
778
|
+
level: "fail",
|
|
779
|
+
area: "agents",
|
|
780
|
+
message: `${DEFAULT_AGENT_ROSTER_TARGET} is missing.`,
|
|
781
|
+
remediation: "Run agent-kit update to install the default council roster and agent-to-skill routing."
|
|
782
|
+
});
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
let roster;
|
|
786
|
+
try {
|
|
787
|
+
const parsed = JSON.parse(readFileSync4(rosterPath, "utf8"));
|
|
788
|
+
if (!isRecord(parsed)) throw new Error("Roster must be a JSON object.");
|
|
789
|
+
const contractResult = AgentRosterContract.safeParse(parsed);
|
|
790
|
+
if (!contractResult.success) {
|
|
791
|
+
findings.push({
|
|
792
|
+
level: "fail",
|
|
793
|
+
area: "agents",
|
|
794
|
+
message: `${DEFAULT_AGENT_ROSTER_TARGET} does not match the schema-backed roster contract.`,
|
|
795
|
+
remediation: `Fix roster shape before relying on council routing. First issue: ${formatContractIssues(contractResult.error)[0]}`
|
|
796
|
+
});
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
roster = contractResult.data;
|
|
800
|
+
findings.push({
|
|
801
|
+
level: "pass",
|
|
802
|
+
area: "agents",
|
|
803
|
+
message: "Agent roster matches the schema-backed runtime contract."
|
|
804
|
+
});
|
|
805
|
+
} catch {
|
|
806
|
+
findings.push({
|
|
807
|
+
level: "fail",
|
|
808
|
+
area: "agents",
|
|
809
|
+
message: `${DEFAULT_AGENT_ROSTER_TARGET} is not valid roster JSON.`,
|
|
810
|
+
remediation: "Replace it with .agent-kit/rosters/next-supabase-default-council.json or rerun agent-kit update."
|
|
811
|
+
});
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (roster.schemaVersion !== 1 || roster.required !== true || roster.defaultWorkflow !== "planning") {
|
|
815
|
+
findings.push({
|
|
816
|
+
level: "warn",
|
|
817
|
+
area: "agents",
|
|
818
|
+
message: "Agent roster metadata does not match the expected default council contract.",
|
|
819
|
+
remediation: "Keep schemaVersion 1, required true, and defaultWorkflow planning unless a documented override exists."
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
const agents = Array.isArray(roster.agents) ? roster.agents.filter(isRecord) : [];
|
|
823
|
+
const agentIds = new Set(agents.map((agent) => typeof agent.id === "string" ? agent.id : "").filter(Boolean));
|
|
824
|
+
const missingAgents = REQUIRED_AGENT_IDS.filter((agentId) => !agentIds.has(agentId));
|
|
825
|
+
if (missingAgents.length > 0) {
|
|
826
|
+
findings.push({
|
|
827
|
+
level: "fail",
|
|
828
|
+
area: "agents",
|
|
829
|
+
message: `Agent roster is missing required default agents: ${missingAgents.join(", ")}.`,
|
|
830
|
+
remediation: "Restore the default council roster so Planner, Architect, implementation, security, QA, docs, and deployment handoffs are always available."
|
|
831
|
+
});
|
|
832
|
+
} else {
|
|
833
|
+
findings.push({
|
|
834
|
+
level: "pass",
|
|
835
|
+
area: "agents",
|
|
836
|
+
message: "Agent roster contains the required default council agents."
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
const skillIds = new Set(agents.flatMap((agent) => asStringArray(agent.skills)));
|
|
840
|
+
const missingSkills = REQUIRED_SKILL_IDS.filter((skillId) => !skillIds.has(skillId));
|
|
841
|
+
if (missingSkills.length > 0) {
|
|
842
|
+
findings.push({
|
|
843
|
+
level: "fail",
|
|
844
|
+
area: "agents",
|
|
845
|
+
message: `Agent roster is missing required skill routing: ${missingSkills.join(", ")}.`,
|
|
846
|
+
remediation: "Map each default agent to its associated skills so handoffs can invoke the right review path automatically."
|
|
847
|
+
});
|
|
848
|
+
} else {
|
|
849
|
+
findings.push({
|
|
850
|
+
level: "pass",
|
|
851
|
+
area: "agents",
|
|
852
|
+
message: "Agent roster maps default agents to required skills."
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
const planner = agents.find((agent) => agent.id === "planner");
|
|
856
|
+
if (!planner || !asStringArray(planner.defaultFor).includes("planning")) {
|
|
857
|
+
findings.push({
|
|
858
|
+
level: "fail",
|
|
859
|
+
area: "agents",
|
|
860
|
+
message: "Planner is not marked as the default agent for planning.",
|
|
861
|
+
remediation: "Set planner.defaultFor to include planning so planning requests do not bypass the planning role."
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
const workflows = Array.isArray(roster.workflows) ? roster.workflows.filter(isRecord) : [];
|
|
865
|
+
const planningWorkflow = workflows.find((workflow) => workflow.id === "planning");
|
|
866
|
+
const coreChangeWorkflow = workflows.find((workflow) => workflow.id === "core-change");
|
|
867
|
+
const frontendWorkflow = workflows.find((workflow) => workflow.id === "frontend-change");
|
|
868
|
+
const marketingCopyWorkflow = workflows.find((workflow) => workflow.id === "marketing-copy");
|
|
869
|
+
if (!planningWorkflow || asStringArray(planningWorkflow.sequence)[0] !== "planner") {
|
|
870
|
+
findings.push({
|
|
871
|
+
level: "fail",
|
|
872
|
+
area: "agents",
|
|
873
|
+
message: "Planning workflow does not start with Planner.",
|
|
874
|
+
remediation: "Define a planning workflow whose first sequence item is planner."
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
const coreSequence = asStringArray(coreChangeWorkflow?.sequence);
|
|
878
|
+
const coreCouncil = asStringArray(coreChangeWorkflow?.council);
|
|
879
|
+
if (!coreChangeWorkflow || !coreSequence.includes("lead-architect") || !coreCouncil.includes("lead-architect")) {
|
|
880
|
+
findings.push({
|
|
881
|
+
level: "fail",
|
|
882
|
+
area: "agents",
|
|
883
|
+
message: "Core-change workflow does not require Lead Architect council review.",
|
|
884
|
+
remediation: "Define core-change with Lead Architect in both sequence and council."
|
|
885
|
+
});
|
|
886
|
+
} else if (!coreCouncil.includes("security-reviewer") || !coreCouncil.includes("qa-engineer")) {
|
|
887
|
+
findings.push({
|
|
888
|
+
level: "warn",
|
|
889
|
+
area: "agents",
|
|
890
|
+
message: "Core-change council is missing security or QA participation.",
|
|
891
|
+
remediation: "Include Security Reviewer and QA Engineer in core-change council membership."
|
|
892
|
+
});
|
|
893
|
+
} else {
|
|
894
|
+
findings.push({
|
|
895
|
+
level: "pass",
|
|
896
|
+
area: "agents",
|
|
897
|
+
message: "Core-change workflow requires architect-led council handoff."
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
const frontendSequence = asStringArray(frontendWorkflow?.sequence);
|
|
901
|
+
const frontendOutputs = asStringArray(frontendWorkflow?.requiredOutputs).join(" ").toLowerCase();
|
|
902
|
+
if (!frontendWorkflow || !frontendSequence.includes("frontend-design-lead")) {
|
|
903
|
+
findings.push({
|
|
904
|
+
level: "fail",
|
|
905
|
+
area: "agents",
|
|
906
|
+
message: "Frontend-change workflow does not require Frontend Design Lead review.",
|
|
907
|
+
remediation: "Define frontend-change with Frontend Design Lead in the sequence before implementation is accepted."
|
|
908
|
+
});
|
|
909
|
+
} else if (!frontendOutputs.includes("brand") || !frontendOutputs.includes("creative") || !frontendOutputs.includes("reference") || !frontendOutputs.includes("distinctiveness") || !frontendOutputs.includes("critique") || !frontendOutputs.includes("scorecard") || !frontendOutputs.includes("visual")) {
|
|
910
|
+
findings.push({
|
|
911
|
+
level: "warn",
|
|
912
|
+
area: "agents",
|
|
913
|
+
message: "Frontend-change workflow is missing brand/content intake, creative-direction, reference-led critique, or visual QA outputs.",
|
|
914
|
+
remediation: "Require brand/content intake, creative-direction rationale, reference-set evidence, distinctiveness benchmark, design critique verdict, frontend product-quality scorecard, visual QA evidence, state coverage, accessibility checks, and screenshot evidence."
|
|
915
|
+
});
|
|
916
|
+
} else {
|
|
917
|
+
findings.push({
|
|
918
|
+
level: "pass",
|
|
919
|
+
area: "agents",
|
|
920
|
+
message: "Frontend-change workflow requires content-first design, reference-led critique, distinctiveness benchmarking, product-quality scoring, and creative-direction evidence."
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
const marketingCopyLead = agents.find((agent) => agent.id === "marketing-copy-lead");
|
|
924
|
+
const marketingDefaultFor = asStringArray(marketingCopyLead?.defaultFor);
|
|
925
|
+
const marketingSkills = asStringArray(marketingCopyLead?.skills);
|
|
926
|
+
if (!marketingCopyLead || !marketingDefaultFor.includes("copywriting") || !marketingDefaultFor.includes("value-proposition") || !marketingSkills.includes("positioning-messaging") || !marketingSkills.includes("conversion-copywriting")) {
|
|
927
|
+
findings.push({
|
|
928
|
+
level: "fail",
|
|
929
|
+
area: "agents",
|
|
930
|
+
message: "Marketing Copy Lead is missing question-led positioning or conversion-copy skill routing.",
|
|
931
|
+
remediation: "Restore Marketing Copy Lead with copywriting, positioning, value-proposition, conversion, voice/tone, landing-page, onboarding, and empty-state routing."
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
const marketingSequence = asStringArray(marketingCopyWorkflow?.sequence);
|
|
935
|
+
const marketingOutputs = asStringArray(marketingCopyWorkflow?.requiredOutputs).join(" ").toLowerCase();
|
|
936
|
+
if (!marketingCopyWorkflow || !marketingSequence.includes("marketing-copy-lead")) {
|
|
937
|
+
findings.push({
|
|
938
|
+
level: "fail",
|
|
939
|
+
area: "agents",
|
|
940
|
+
message: "Marketing-copy workflow does not require Marketing Copy Lead review.",
|
|
941
|
+
remediation: "Define marketing-copy with Marketing Copy Lead in the sequence before public-facing copy is accepted."
|
|
942
|
+
});
|
|
943
|
+
} else if (!marketingOutputs.includes("questions") || !marketingOutputs.includes("audience") || !marketingOutputs.includes("value proposition") || !marketingOutputs.includes("proof") || !marketingOutputs.includes("objections") || !marketingOutputs.includes("voice") || !marketingOutputs.includes("conversion")) {
|
|
944
|
+
findings.push({
|
|
945
|
+
level: "warn",
|
|
946
|
+
area: "agents",
|
|
947
|
+
message: "Marketing-copy workflow is missing discovery questions, audience, value proposition, proof, objections, voice, or conversion outputs.",
|
|
948
|
+
remediation: "Require discovery questions, audience and segment assumptions, problem/pain/outcome, value proposition, differentiators, proof, objections, voice/tone, conversion goal, and design handoff notes."
|
|
949
|
+
});
|
|
950
|
+
} else {
|
|
951
|
+
findings.push({
|
|
952
|
+
level: "pass",
|
|
953
|
+
area: "agents",
|
|
954
|
+
message: "Marketing-copy workflow requires question-led positioning, value proposition, proof, objections, voice, and conversion evidence."
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
const handoffRules = asStringArray(roster.handoffRules).join(" ").toLowerCase();
|
|
958
|
+
if (!includesAll(handoffRules, ["decision", "risk", "handoff", "evidence"])) {
|
|
959
|
+
findings.push({
|
|
960
|
+
level: "warn",
|
|
961
|
+
area: "agents",
|
|
962
|
+
message: "Agent roster handoff rules do not require decision, risk, handoff, and evidence capture.",
|
|
963
|
+
remediation: "Add handoff rules that require each meaningful council step to record decision, risk, next handoff, and evidence."
|
|
964
|
+
});
|
|
965
|
+
} else {
|
|
966
|
+
findings.push({
|
|
967
|
+
level: "pass",
|
|
968
|
+
area: "agents",
|
|
969
|
+
message: "Agent roster requires auditable handoff evidence."
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function addCouncilSessionRecordFindings(cwd, findings) {
|
|
974
|
+
const sessionsRoot = join6(cwd, COUNCIL_SESSION_DIR);
|
|
975
|
+
if (!existsSync6(sessionsRoot)) return;
|
|
976
|
+
const sessionFiles = listFilesRecursive(sessionsRoot).filter((file) => file.endsWith(".json") && !/[\\/]/.test(file));
|
|
977
|
+
if (sessionFiles.length === 0) return;
|
|
978
|
+
let invalidCount = 0;
|
|
979
|
+
for (const sessionFile of sessionFiles) {
|
|
980
|
+
const displayPath = `${COUNCIL_SESSION_DIR}/${sessionFile}`;
|
|
981
|
+
try {
|
|
982
|
+
const parsed = JSON.parse(readFileSync4(join6(sessionsRoot, sessionFile), "utf8"));
|
|
983
|
+
const contractResult = CouncilSessionContract.safeParse(parsed);
|
|
984
|
+
if (!contractResult.success) {
|
|
985
|
+
invalidCount += 1;
|
|
986
|
+
findings.push({
|
|
987
|
+
level: "fail",
|
|
988
|
+
area: "agents",
|
|
989
|
+
message: `${displayPath} does not match the council-session contract.`,
|
|
990
|
+
remediation: `Fix the structured council record. First issue: ${formatContractIssues(contractResult.error)[0]}`
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
} catch {
|
|
994
|
+
invalidCount += 1;
|
|
995
|
+
findings.push({
|
|
996
|
+
level: "fail",
|
|
997
|
+
area: "agents",
|
|
998
|
+
message: `${displayPath} is not valid council-session JSON.`,
|
|
999
|
+
remediation: "Replace it with a valid JSON record matching .agent-kit/schemas/council-session.schema.json."
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (invalidCount === 0) {
|
|
1004
|
+
findings.push({
|
|
1005
|
+
level: "pass",
|
|
1006
|
+
area: "agents",
|
|
1007
|
+
message: `Structured council-session records match the runtime contract (${sessionFiles.length} checked).`
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
function addSchemaFindings(cwd, findings) {
|
|
1012
|
+
for (const schemaFile of REQUIRED_SCHEMA_FILES) {
|
|
1013
|
+
const schemaPath = join6(cwd, ".agent-kit", "schemas", schemaFile);
|
|
1014
|
+
if (!existsSync6(schemaPath)) {
|
|
1015
|
+
findings.push({
|
|
1016
|
+
level: "warn",
|
|
1017
|
+
area: "agents",
|
|
1018
|
+
message: `.agent-kit/schemas/${schemaFile} is missing.`,
|
|
1019
|
+
remediation: "Run agent-kit update to install the schema-backed council and roster contracts."
|
|
1020
|
+
});
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
try {
|
|
1024
|
+
const parsed = JSON.parse(readFileSync4(schemaPath, "utf8"));
|
|
1025
|
+
if (!isRecord(parsed) || typeof parsed.$schema !== "string" || !isRecord(parsed.properties)) {
|
|
1026
|
+
throw new Error("Schema file is missing JSON Schema metadata.");
|
|
1027
|
+
}
|
|
1028
|
+
findings.push({
|
|
1029
|
+
level: "pass",
|
|
1030
|
+
area: "agents",
|
|
1031
|
+
message: `.agent-kit/schemas/${schemaFile} is present and parseable.`
|
|
1032
|
+
});
|
|
1033
|
+
} catch {
|
|
1034
|
+
findings.push({
|
|
1035
|
+
level: "fail",
|
|
1036
|
+
area: "agents",
|
|
1037
|
+
message: `.agent-kit/schemas/${schemaFile} is not valid JSON Schema.`,
|
|
1038
|
+
remediation: "Restore the schema from the package or rerun agent-kit update."
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function addAgentStudioFindings(cwd, findings) {
|
|
1044
|
+
const contextPath = join6(cwd, CONTEXT_JSON);
|
|
1045
|
+
if (!existsSync6(contextPath)) {
|
|
1046
|
+
findings.push({
|
|
1047
|
+
level: "warn",
|
|
1048
|
+
area: "studio",
|
|
1049
|
+
message: `${CONTEXT_JSON} is missing.`,
|
|
1050
|
+
remediation: "Run agent-kit onboard or agent-kit init --guided so agents can start with project-specific context."
|
|
1051
|
+
});
|
|
1052
|
+
} else {
|
|
1053
|
+
try {
|
|
1054
|
+
const parsed = JSON.parse(readFileSync4(contextPath, "utf8"));
|
|
1055
|
+
const result = ProjectContextContract.safeParse(parsed);
|
|
1056
|
+
if (!result.success) {
|
|
1057
|
+
findings.push({
|
|
1058
|
+
level: "fail",
|
|
1059
|
+
area: "studio",
|
|
1060
|
+
message: `${CONTEXT_JSON} does not match the project-context contract.`,
|
|
1061
|
+
remediation: `Fix project context before relying on guided agent behavior. First issue: ${formatContractIssues(result.error)[0]}`
|
|
1062
|
+
});
|
|
1063
|
+
} else {
|
|
1064
|
+
const missingHighValue = [
|
|
1065
|
+
["product summary", result.data.productSummary],
|
|
1066
|
+
["primary audience", result.data.primaryAudience],
|
|
1067
|
+
["auth model", result.data.authModel],
|
|
1068
|
+
["tenant model", result.data.tenantModel]
|
|
1069
|
+
].filter(([, value]) => typeof value === "string" && !value.trim());
|
|
1070
|
+
if (missingHighValue.length > 0 || result.data.primaryWorkflows.length === 0) {
|
|
1071
|
+
findings.push({
|
|
1072
|
+
level: "warn",
|
|
1073
|
+
area: "studio",
|
|
1074
|
+
message: `${CONTEXT_JSON} is valid but still missing high-value project context.`,
|
|
1075
|
+
remediation: "Answer product summary, audience, workflows, auth/tenant model, UI direction, value proposition, and quality target before claiming context-aware setup."
|
|
1076
|
+
});
|
|
1077
|
+
} else {
|
|
1078
|
+
findings.push({
|
|
1079
|
+
level: "pass",
|
|
1080
|
+
area: "studio",
|
|
1081
|
+
message: "Project context is valid and contains high-value onboarding context."
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
} catch {
|
|
1086
|
+
findings.push({
|
|
1087
|
+
level: "fail",
|
|
1088
|
+
area: "studio",
|
|
1089
|
+
message: `${CONTEXT_JSON} is not valid JSON.`,
|
|
1090
|
+
remediation: "Regenerate project context with agent-kit context init or fix the JSON syntax."
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
const contextMdPath = join6(cwd, CONTEXT_MD);
|
|
1095
|
+
if (existsSync6(contextPath) && !existsSync6(contextMdPath)) {
|
|
1096
|
+
findings.push({
|
|
1097
|
+
level: "warn",
|
|
1098
|
+
area: "studio",
|
|
1099
|
+
message: `${CONTEXT_MD} is missing while project context JSON exists.`,
|
|
1100
|
+
remediation: "Run agent-kit context render so humans can review the context agents will load."
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
for (const relativePath of [PROJECT_RULES_JSON, AGENT_RULES_JSON]) {
|
|
1104
|
+
const path = join6(cwd, relativePath);
|
|
1105
|
+
if (!existsSync6(path)) continue;
|
|
1106
|
+
try {
|
|
1107
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
1108
|
+
const result = CorrectionRulesContract.safeParse(parsed);
|
|
1109
|
+
if (!result.success) {
|
|
1110
|
+
findings.push({
|
|
1111
|
+
level: "fail",
|
|
1112
|
+
area: "studio",
|
|
1113
|
+
message: `${relativePath} does not match the correction-rules contract.`,
|
|
1114
|
+
remediation: `Fix correction rules before relying on persistent human feedback. First issue: ${formatContractIssues(result.error)[0]}`
|
|
1115
|
+
});
|
|
1116
|
+
} else if (result.data.rules.some((rule) => rule.status === "active")) {
|
|
1117
|
+
findings.push({
|
|
1118
|
+
level: "pass",
|
|
1119
|
+
area: "studio",
|
|
1120
|
+
message: `${relativePath} contains valid active correction rules.`
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
} catch {
|
|
1124
|
+
findings.push({
|
|
1125
|
+
level: "fail",
|
|
1126
|
+
area: "studio",
|
|
1127
|
+
message: `${relativePath} is not valid JSON.`,
|
|
1128
|
+
remediation: "Fix the JSON syntax or regenerate correction files with agent-kit correction commands."
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
const studioExportPath = join6(cwd, STUDIO_EXPORT_HTML);
|
|
1133
|
+
if (existsSync6(studioExportPath)) {
|
|
1134
|
+
const exportHtml = readFileSync4(studioExportPath, "utf8");
|
|
1135
|
+
if (containsLikelySecret(exportHtml)) {
|
|
1136
|
+
findings.push({
|
|
1137
|
+
level: "fail",
|
|
1138
|
+
area: "studio",
|
|
1139
|
+
message: `${STUDIO_EXPORT_HTML} appears to contain a secret-like value.`,
|
|
1140
|
+
remediation: "Regenerate the static studio export after redacting sensitive context, session, correction, and artifact data."
|
|
1141
|
+
});
|
|
1142
|
+
} else if (!exportHtml.includes("agent-studio-data") || !exportHtml.includes("Agent Studio")) {
|
|
1143
|
+
findings.push({
|
|
1144
|
+
level: "warn",
|
|
1145
|
+
area: "studio",
|
|
1146
|
+
message: `${STUDIO_EXPORT_HTML} does not look like a complete Agent Studio export.`,
|
|
1147
|
+
remediation: "Regenerate it with agent-kit studio export."
|
|
1148
|
+
});
|
|
1149
|
+
} else {
|
|
1150
|
+
findings.push({
|
|
1151
|
+
level: "pass",
|
|
1152
|
+
area: "studio",
|
|
1153
|
+
message: `${STUDIO_EXPORT_HTML} is present and does not contain known secret patterns.`
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
const sessionsRoot = join6(cwd, COUNCIL_SESSION_DIR);
|
|
1158
|
+
if (!existsSync6(sessionsRoot)) return;
|
|
1159
|
+
const files = listFilesRecursive(sessionsRoot);
|
|
1160
|
+
const studioSessionFiles = files.filter((file) => /[\\/]session\.json$/.test(file));
|
|
1161
|
+
for (const sessionFile of studioSessionFiles) {
|
|
1162
|
+
const normalizedSessionFile = sessionFile.replace(/\\/g, "/");
|
|
1163
|
+
const sessionRelative = `${COUNCIL_SESSION_DIR}/${normalizedSessionFile}`;
|
|
1164
|
+
const sessionDir2 = sessionFile.replace(/[\\/]session\.json$/, "");
|
|
1165
|
+
const normalizedSessionDir = sessionDir2.replace(/\\/g, "/");
|
|
1166
|
+
const eventsRelative = `${COUNCIL_SESSION_DIR}/${normalizedSessionDir}/events.jsonl`;
|
|
1167
|
+
const indexRelative = `${COUNCIL_SESSION_DIR}/${normalizedSessionDir}/index.md`;
|
|
1168
|
+
const transcriptRelative = `${COUNCIL_SESSION_DIR}/${normalizedSessionDir}/transcript.md`;
|
|
1169
|
+
const sessionDirPath = join6(sessionsRoot, sessionDir2);
|
|
1170
|
+
let sessionResult = null;
|
|
1171
|
+
try {
|
|
1172
|
+
sessionResult = StudioSessionContract.safeParse(JSON.parse(readFileSync4(join6(sessionDirPath, "session.json"), "utf8")));
|
|
1173
|
+
if (!sessionResult.success) {
|
|
1174
|
+
findings.push({
|
|
1175
|
+
level: "fail",
|
|
1176
|
+
area: "studio",
|
|
1177
|
+
message: `${sessionRelative} does not match the studio-session contract.`,
|
|
1178
|
+
remediation: `Fix session metadata. First issue: ${formatContractIssues(sessionResult.error)[0]}`
|
|
1179
|
+
});
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
} catch {
|
|
1183
|
+
findings.push({
|
|
1184
|
+
level: "fail",
|
|
1185
|
+
area: "studio",
|
|
1186
|
+
message: `${sessionRelative} is not valid JSON.`,
|
|
1187
|
+
remediation: "Fix session metadata JSON before relying on rendered session evidence."
|
|
1188
|
+
});
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
const eventsPath2 = join6(sessionDirPath, "events.jsonl");
|
|
1192
|
+
if (!existsSync6(eventsPath2)) {
|
|
1193
|
+
findings.push({
|
|
1194
|
+
level: "fail",
|
|
1195
|
+
area: "studio",
|
|
1196
|
+
message: `${eventsRelative} is missing.`,
|
|
1197
|
+
remediation: "Record session events or remove the incomplete studio session folder."
|
|
1198
|
+
});
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
const eventText = readFileSync4(eventsPath2, "utf8");
|
|
1202
|
+
if (containsLikelySecret(eventText)) {
|
|
1203
|
+
findings.push({
|
|
1204
|
+
level: "fail",
|
|
1205
|
+
area: "studio",
|
|
1206
|
+
message: `${eventsRelative} appears to contain a secret-like value.`,
|
|
1207
|
+
remediation: "Redact tokens, database URLs, env values, and private customer data from session logs."
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
const eventLines = eventText.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
1211
|
+
let validEvents = 0;
|
|
1212
|
+
let verificationCount = 0;
|
|
1213
|
+
for (const [index, line] of eventLines.entries()) {
|
|
1214
|
+
try {
|
|
1215
|
+
const result = SessionEventContract.safeParse(JSON.parse(line));
|
|
1216
|
+
if (!result.success) {
|
|
1217
|
+
findings.push({
|
|
1218
|
+
level: "fail",
|
|
1219
|
+
area: "studio",
|
|
1220
|
+
message: `${eventsRelative} line ${index + 1} does not match the session-event contract.`,
|
|
1221
|
+
remediation: `Fix the event row. First issue: ${formatContractIssues(result.error)[0]}`
|
|
1222
|
+
});
|
|
1223
|
+
} else {
|
|
1224
|
+
validEvents += 1;
|
|
1225
|
+
if (result.data.type === "verification_recorded") verificationCount += 1;
|
|
1226
|
+
}
|
|
1227
|
+
} catch {
|
|
1228
|
+
findings.push({
|
|
1229
|
+
level: "fail",
|
|
1230
|
+
area: "studio",
|
|
1231
|
+
message: `${eventsRelative} line ${index + 1} is not valid JSON.`,
|
|
1232
|
+
remediation: "Fix malformed JSONL before rendering or auditing session history."
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
if (!existsSync6(join6(sessionDirPath, "index.md")) || !existsSync6(join6(sessionDirPath, "transcript.md"))) {
|
|
1237
|
+
findings.push({
|
|
1238
|
+
level: "warn",
|
|
1239
|
+
area: "studio",
|
|
1240
|
+
message: `${sessionDir2} has unrendered session Markdown.`,
|
|
1241
|
+
remediation: "Run agent-kit session render so humans can inspect the current agent transcript and handoffs."
|
|
1242
|
+
});
|
|
1243
|
+
} else {
|
|
1244
|
+
const indexText = readFileSync4(join6(sessionDirPath, "index.md"), "utf8");
|
|
1245
|
+
const transcriptText = readFileSync4(join6(sessionDirPath, "transcript.md"), "utf8");
|
|
1246
|
+
if (containsLikelySecret(indexText) || containsLikelySecret(transcriptText)) {
|
|
1247
|
+
findings.push({
|
|
1248
|
+
level: "fail",
|
|
1249
|
+
area: "studio",
|
|
1250
|
+
message: `${sessionDir2} rendered Markdown appears to contain a secret-like value.`,
|
|
1251
|
+
remediation: "Regenerate Markdown after redacting sensitive values from the event log."
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
if (statSync(eventsPath2).mtimeMs > statSync(join6(sessionDirPath, "index.md")).mtimeMs) {
|
|
1255
|
+
findings.push({
|
|
1256
|
+
level: "warn",
|
|
1257
|
+
area: "studio",
|
|
1258
|
+
message: `${sessionDir2} has events newer than rendered Markdown.`,
|
|
1259
|
+
remediation: "Run agent-kit session render after recording new events."
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
const session2 = sessionResult.data;
|
|
1264
|
+
if (session2.status === "complete") {
|
|
1265
|
+
const missingOutputs = session2.requiredOutputs.filter((output) => output.status === "missing" || output.status === "partial");
|
|
1266
|
+
if (missingOutputs.length > 0 || verificationCount === 0) {
|
|
1267
|
+
findings.push({
|
|
1268
|
+
level: "fail",
|
|
1269
|
+
area: "studio",
|
|
1270
|
+
message: `${sessionRelative} is complete but lacks required outputs or verification evidence.`,
|
|
1271
|
+
remediation: "Do not mark sessions complete until required outputs are complete or not-applicable and verification evidence is recorded."
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (validEvents > 0) {
|
|
1276
|
+
findings.push({
|
|
1277
|
+
level: "pass",
|
|
1278
|
+
area: "studio",
|
|
1279
|
+
message: `${sessionDir2} has ${validEvents} valid Agent Studio events.`
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
function addCouncilDocFindings(cwd, findings) {
|
|
1285
|
+
const councilDoc = readDoc(cwd, "COUNCIL.md");
|
|
1286
|
+
if (!councilDoc) return;
|
|
1287
|
+
if (!includesAll(councilDoc, ["council session", "handoff", "decision", "risk", "evidence"])) {
|
|
1288
|
+
findings.push({
|
|
1289
|
+
level: "warn",
|
|
1290
|
+
area: "agents",
|
|
1291
|
+
message: "COUNCIL.md does not define a complete council-session handoff record.",
|
|
1292
|
+
remediation: "Require workflow, decision, risk, next handoff, evidence, required outputs, and verification status in council sessions."
|
|
1293
|
+
});
|
|
1294
|
+
} else {
|
|
1295
|
+
findings.push({
|
|
1296
|
+
level: "pass",
|
|
1297
|
+
area: "agents",
|
|
1298
|
+
message: "COUNCIL.md defines council-session handoff evidence."
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
function addAssistantAdapterFindings(cwd, findings) {
|
|
1303
|
+
const adaptersDoc = readDoc(cwd, "ASSISTANT_ADAPTERS.md");
|
|
1304
|
+
const adapterRoot = join6(cwd, ".agent-kit", "assistant-adapters");
|
|
1305
|
+
if (!existsSync6(adapterRoot)) {
|
|
1306
|
+
findings.push({
|
|
1307
|
+
level: "warn",
|
|
1308
|
+
area: "agents",
|
|
1309
|
+
message: ".agent-kit/assistant-adapters is missing.",
|
|
1310
|
+
remediation: "Run agent-kit update so tool-specific adapter templates are available for Codex, Copilot, Cursor, and Claude Code."
|
|
1311
|
+
});
|
|
1312
|
+
} else {
|
|
1313
|
+
findings.push({
|
|
1314
|
+
level: "pass",
|
|
1315
|
+
area: "agents",
|
|
1316
|
+
message: ".agent-kit/assistant-adapters is installed."
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
if (!adaptersDoc) return;
|
|
1320
|
+
if (!includesAll(adaptersDoc, ["AGENTS.md", "agent-roster.json", "copilot-instructions", ".cursor/rules", ".claude/agents", "source of truth"])) {
|
|
1321
|
+
findings.push({
|
|
1322
|
+
level: "warn",
|
|
1323
|
+
area: "agents",
|
|
1324
|
+
message: "ASSISTANT_ADAPTERS.md does not map the council roster to supported tool instruction surfaces.",
|
|
1325
|
+
remediation: "Document Codex/AGENTS.md, GitHub Copilot, Cursor rules, Claude Code subagents, and the source-of-truth rule for avoiding divergent agent instructions."
|
|
1326
|
+
});
|
|
1327
|
+
} else {
|
|
1328
|
+
findings.push({
|
|
1329
|
+
level: "pass",
|
|
1330
|
+
area: "agents",
|
|
1331
|
+
message: "ASSISTANT_ADAPTERS.md maps the council roster to tool-specific instruction surfaces."
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
if (!includesAll(adaptersDoc, ["model selection", "enforcement", "MODEL_ROUTING.md", "model-routing.json"])) {
|
|
1335
|
+
findings.push({
|
|
1336
|
+
level: "warn",
|
|
1337
|
+
area: "models",
|
|
1338
|
+
message: "ASSISTANT_ADAPTERS.md does not record model-selection and enforcement status for active tools.",
|
|
1339
|
+
remediation: "Document which IDEs can enforce, partially apply, advise, or require manual model selection from MODEL_ROUTING.md."
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
if (/\bTBD\b/i.test(adaptersDoc)) {
|
|
1343
|
+
findings.push({
|
|
1344
|
+
level: "warn",
|
|
1345
|
+
area: "models",
|
|
1346
|
+
message: "ASSISTANT_ADAPTERS.md still has unverified tool-surface rows.",
|
|
1347
|
+
remediation: "Replace TBD adapter rows with date, owner, evidence, model-selection status, and known limitations for each active IDE."
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
function addModelRoutingFindings(cwd, findings) {
|
|
1352
|
+
const modelRoutingDoc = readDoc(cwd, "MODEL_ROUTING.md");
|
|
1353
|
+
if (!modelRoutingDoc) {
|
|
1354
|
+
findings.push({
|
|
1355
|
+
level: "warn",
|
|
1356
|
+
area: "models",
|
|
1357
|
+
message: "MODEL_ROUTING.md is missing.",
|
|
1358
|
+
remediation: "Run agent-kit update to install the model-routing guidance and document the active IDE model-selection setup."
|
|
1359
|
+
});
|
|
1360
|
+
} else if (!includesAll(modelRoutingDoc, ["model routing", "agent", "profile", "codex", "claude code", "cursor", "github copilot", "enforcement"])) {
|
|
1361
|
+
findings.push({
|
|
1362
|
+
level: "warn",
|
|
1363
|
+
area: "models",
|
|
1364
|
+
message: "MODEL_ROUTING.md does not explain agent profiles and IDE enforcement limits.",
|
|
1365
|
+
remediation: "Restore model-routing guidance that maps agents to profiles and records Codex, Claude Code, Cursor, and GitHub Copilot setup status."
|
|
1366
|
+
});
|
|
1367
|
+
} else {
|
|
1368
|
+
findings.push({
|
|
1369
|
+
level: "pass",
|
|
1370
|
+
area: "models",
|
|
1371
|
+
message: "MODEL_ROUTING.md documents agent model profiles and IDE enforcement limits."
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
const routingPath = join6(cwd, DEFAULT_MODEL_ROUTING_TARGET);
|
|
1375
|
+
if (!existsSync6(routingPath)) {
|
|
1376
|
+
findings.push({
|
|
1377
|
+
level: "warn",
|
|
1378
|
+
area: "models",
|
|
1379
|
+
message: `${DEFAULT_MODEL_ROUTING_TARGET} is missing.`,
|
|
1380
|
+
remediation: "Run agent-kit update to install the provider-neutral model-routing contract."
|
|
1381
|
+
});
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
let routing;
|
|
1385
|
+
try {
|
|
1386
|
+
routing = JSON.parse(readFileSync4(routingPath, "utf8"));
|
|
1387
|
+
} catch {
|
|
1388
|
+
findings.push({
|
|
1389
|
+
level: "warn",
|
|
1390
|
+
area: "models",
|
|
1391
|
+
message: `${DEFAULT_MODEL_ROUTING_TARGET} is not valid JSON.`,
|
|
1392
|
+
remediation: "Replace it with .agent-kit/model-routing/default-model-routing.json or rerun agent-kit update."
|
|
1393
|
+
});
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
const contractResult = ModelRoutingContract.safeParse(routing);
|
|
1397
|
+
if (!contractResult.success) {
|
|
1398
|
+
findings.push({
|
|
1399
|
+
level: "warn",
|
|
1400
|
+
area: "models",
|
|
1401
|
+
message: `${DEFAULT_MODEL_ROUTING_TARGET} does not match the model-routing contract.`,
|
|
1402
|
+
remediation: `Fix model-routing shape before relying on model recommendations. First issue: ${formatContractIssues(contractResult.error)[0]}`
|
|
1403
|
+
});
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
findings.push({
|
|
1407
|
+
level: "pass",
|
|
1408
|
+
area: "models",
|
|
1409
|
+
message: "Model routing matches the schema-backed runtime contract."
|
|
1410
|
+
});
|
|
1411
|
+
const profileIds = new Set(contractResult.data.profiles.map((profile) => profile.id));
|
|
1412
|
+
const missingAgents = REQUIRED_AGENT_IDS.filter((agentId) => !contractResult.data.agentRoutes.some((route) => route.agentId === agentId));
|
|
1413
|
+
const danglingRoutes = contractResult.data.agentRoutes.filter(
|
|
1414
|
+
(route) => !profileIds.has(route.profileId) || (route.escalationProfileId ? !profileIds.has(route.escalationProfileId) : false)
|
|
1415
|
+
);
|
|
1416
|
+
if (missingAgents.length > 0) {
|
|
1417
|
+
findings.push({
|
|
1418
|
+
level: "warn",
|
|
1419
|
+
area: "models",
|
|
1420
|
+
message: `Model routing is missing default agent routes: ${missingAgents.join(", ")}.`,
|
|
1421
|
+
remediation: "Map every default council agent to a model profile so model selection does not collapse into one generic default."
|
|
1422
|
+
});
|
|
1423
|
+
} else {
|
|
1424
|
+
findings.push({
|
|
1425
|
+
level: "pass",
|
|
1426
|
+
area: "models",
|
|
1427
|
+
message: "Model routing maps every default council agent to a profile."
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
if (danglingRoutes.length > 0) {
|
|
1431
|
+
findings.push({
|
|
1432
|
+
level: "warn",
|
|
1433
|
+
area: "models",
|
|
1434
|
+
message: "Model routing references profile ids that are not defined.",
|
|
1435
|
+
remediation: "Ensure every profileId and escalationProfileId exists in model-routing profiles."
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
function addTemplateHashFindings(cwd, findings) {
|
|
1440
|
+
const manifest = readManifest(cwd);
|
|
1441
|
+
if (!manifest) return;
|
|
1442
|
+
const overrides = readOverrides(cwd);
|
|
1443
|
+
for (const doc of ROOT_DOCS) {
|
|
1444
|
+
const targetPath = join6(cwd, doc);
|
|
1445
|
+
if (!existsSync6(targetPath)) continue;
|
|
1446
|
+
const currentTemplate = readTemplate(manifest.stack, doc);
|
|
1447
|
+
if (!currentTemplate) continue;
|
|
1448
|
+
const targetHash = sha256(readFileSync4(targetPath, "utf8"));
|
|
1449
|
+
const currentTemplateHash = sha256(currentTemplate);
|
|
1450
|
+
const installedTemplateHash = manifest.templateHashes?.[doc];
|
|
1451
|
+
const override = overrides[doc];
|
|
1452
|
+
if (!installedTemplateHash) {
|
|
1453
|
+
findings.push({
|
|
1454
|
+
level: "warn",
|
|
1455
|
+
area: "templates",
|
|
1456
|
+
message: `${doc} has no stored template hash in .agent-kit/manifest.json.`,
|
|
1457
|
+
remediation: "Run agent-kit update to refresh manifest metadata and review any conflicts."
|
|
1458
|
+
});
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
if (installedTemplateHash === currentTemplateHash && targetHash === currentTemplateHash) {
|
|
1462
|
+
findings.push({
|
|
1463
|
+
level: "pass",
|
|
1464
|
+
area: "templates",
|
|
1465
|
+
message: `${doc} matches the current bundled template.`
|
|
1466
|
+
});
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
if (installedTemplateHash !== currentTemplateHash && targetHash === installedTemplateHash) {
|
|
1470
|
+
findings.push({
|
|
1471
|
+
level: "warn",
|
|
1472
|
+
area: "templates",
|
|
1473
|
+
message: `${doc} still matches an older installed template hash.`,
|
|
1474
|
+
remediation: "Run agent-kit update and review the generated conflict file before adopting the new template."
|
|
1475
|
+
});
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
if (targetHash === currentTemplateHash) {
|
|
1479
|
+
findings.push({
|
|
1480
|
+
level: "pass",
|
|
1481
|
+
area: "templates",
|
|
1482
|
+
message: `${doc} matches the current template even though manifest metadata is older.`
|
|
1483
|
+
});
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
if (installedTemplateHash === currentTemplateHash) {
|
|
1487
|
+
if (override) {
|
|
1488
|
+
findings.push({
|
|
1489
|
+
level: "pass",
|
|
1490
|
+
area: "templates",
|
|
1491
|
+
message: `${doc} has a documented local override.`,
|
|
1492
|
+
remediation: override.reviewedAt ? `Last reviewed at ${override.reviewedAt}.` : "Add reviewedAt to the override after the next template review."
|
|
1493
|
+
});
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
findings.push({
|
|
1497
|
+
level: "warn",
|
|
1498
|
+
area: "templates",
|
|
1499
|
+
message: `${doc} is locally customized or was preserved from before agent-kit install.`,
|
|
1500
|
+
remediation: "Compare the local file with .agent-kit/conflicts or agent-kit diff before adopting template changes."
|
|
1501
|
+
});
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
if (override) {
|
|
1505
|
+
findings.push({
|
|
1506
|
+
level: "warn",
|
|
1507
|
+
area: "templates",
|
|
1508
|
+
message: `${doc} has a documented local override, but the bundled template changed since install.`,
|
|
1509
|
+
remediation: "Review the override against the current conflict template and update reviewedAt when accepted."
|
|
1510
|
+
});
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
findings.push({
|
|
1514
|
+
level: "warn",
|
|
1515
|
+
area: "templates",
|
|
1516
|
+
message: `${doc} differs from both the installed template hash and current bundled template.`,
|
|
1517
|
+
remediation: "Review local customizations with agent-kit diff before updating."
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
function readLikelyLandingFiles(cwd) {
|
|
1522
|
+
const candidates = listFilesRecursive(cwd).filter((file) => {
|
|
1523
|
+
const normalized = file.replace(/\\/g, "/");
|
|
1524
|
+
return /(^|\/)(app|pages|src\/app|src\/pages)\/(page|index)\.(tsx|jsx)$/.test(normalized) || /(^|\/)components\/.*(hero|landing|marketing).*\.(tsx|jsx)$/i.test(normalized);
|
|
1525
|
+
});
|
|
1526
|
+
return candidates.slice(0, 20).map((file) => readDoc(cwd, file)).join("\n");
|
|
1527
|
+
}
|
|
1528
|
+
function addFrontendFindings(cwd, findings) {
|
|
1529
|
+
const styleGuide = readDoc(cwd, "STYLE_GUIDE.md");
|
|
1530
|
+
const designDoc = readDoc(cwd, "DESIGN.md");
|
|
1531
|
+
if (!includesAny(styleGuide, ["generic AI", "gradient", "design token"])) {
|
|
1532
|
+
findings.push({
|
|
1533
|
+
level: "warn",
|
|
1534
|
+
area: "frontend",
|
|
1535
|
+
message: "STYLE_GUIDE.md does not contain anti-generic-AI-site design guidance.",
|
|
1536
|
+
remediation: "Add rules for task-first screens, design tokens, real states, and non-generic visual direction."
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
if (!includesAll(styleGuide, ["design token", "color", "typography", "spacing", "radius"])) {
|
|
1540
|
+
findings.push({
|
|
1541
|
+
level: "warn",
|
|
1542
|
+
area: "frontend",
|
|
1543
|
+
message: "STYLE_GUIDE.md is missing a complete design-token inventory.",
|
|
1544
|
+
remediation: "Document semantic color, typography, spacing, radius, motion, and depth decisions."
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
if (!includesAll(styleGuide, ["loading", "empty", "error", "disabled", "success", "mobile"])) {
|
|
1548
|
+
findings.push({
|
|
1549
|
+
level: "warn",
|
|
1550
|
+
area: "frontend",
|
|
1551
|
+
message: "STYLE_GUIDE.md is missing required component state coverage.",
|
|
1552
|
+
remediation: "Require loading, empty, error, disabled, success, focus, and mobile states for interactive UI."
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
if (!includesAll(styleGuide, ["landing page", "working app", "task-first"])) {
|
|
1556
|
+
findings.push({
|
|
1557
|
+
level: "warn",
|
|
1558
|
+
area: "frontend",
|
|
1559
|
+
message: "STYLE_GUIDE.md does not explicitly prevent generic landing-page defaults.",
|
|
1560
|
+
remediation: "State when landing pages are inappropriate and require the first screen to show the real product task."
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
if (!includesAll(designDoc, ["brand", "content", "user needs", "creative direction", "design tokens"])) {
|
|
1564
|
+
findings.push({
|
|
1565
|
+
level: "warn",
|
|
1566
|
+
area: "frontend",
|
|
1567
|
+
message: "DESIGN.md is missing brand, content, user-need, creative-direction, or token guidance.",
|
|
1568
|
+
remediation: "Use the DESIGN.md template to capture product category, audience, content inventory, brand traits, creative directions, and token decisions."
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
if (!includesAll(designDoc, ["reference set", "anti-reference", "distinctiveness", "critique"])) {
|
|
1572
|
+
findings.push({
|
|
1573
|
+
level: "warn",
|
|
1574
|
+
area: "frontend",
|
|
1575
|
+
message: "DESIGN.md is missing reference-set, anti-reference, distinctiveness, or critique-gate guidance.",
|
|
1576
|
+
remediation: "Add category references, anti-references, source-safety notes, distinctiveness criteria, and a design critique verdict before accepting frontend work as best-practice ready."
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
if (!includesAll(designDoc, ["distinctiveness benchmark", "first-screen proof", "content fingerprint", "asset provenance", "state proof", "visual QA proof"])) {
|
|
1580
|
+
findings.push({
|
|
1581
|
+
level: "warn",
|
|
1582
|
+
area: "frontend",
|
|
1583
|
+
message: "DESIGN.md is missing frontend distinctiveness benchmark evidence.",
|
|
1584
|
+
remediation: "Add first-screen proof, content fingerprint, reference benchmark, creative divergence, asset provenance, state proof, visual QA proof, generic-risk, and source-safety fields before accepting significant frontend work."
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
if (!includesAll(designDoc, ["product quality scorecard", "user/task fit", "content specificity", "source safety", "total score"])) {
|
|
1588
|
+
findings.push({
|
|
1589
|
+
level: "warn",
|
|
1590
|
+
area: "frontend",
|
|
1591
|
+
message: "DESIGN.md is missing a frontend product-quality scorecard.",
|
|
1592
|
+
remediation: "Add scorecard fields for user/task fit, content specificity, visual identity, information architecture, component states, accessibility and interaction, source safety, total score, and acceptance verdict."
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
if (!includesAll(styleGuide, ["DESIGN.md", "content-first", "creative direction"])) {
|
|
1596
|
+
findings.push({
|
|
1597
|
+
level: "warn",
|
|
1598
|
+
area: "frontend",
|
|
1599
|
+
message: "STYLE_GUIDE.md does not require content-first creative direction before frontend implementation.",
|
|
1600
|
+
remediation: "Reference DESIGN.md, brand/content intake, and creative-direction selection before visual implementation starts."
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
const landingText = readLikelyLandingFiles(cwd);
|
|
1604
|
+
if (landingText && includesAny(landingText, ["bg-gradient", "from-purple", "to-blue", "ai-powered", "supercharge", "revolutionize", "10x"])) {
|
|
1605
|
+
findings.push({
|
|
1606
|
+
level: "warn",
|
|
1607
|
+
area: "frontend",
|
|
1608
|
+
message: "Likely landing or hero files contain generic AI-site visual or copy patterns.",
|
|
1609
|
+
remediation: "Review the first screen for domain-specific hierarchy, restrained tokens, real workflows, and useful states."
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
function addQualityGateFindings(cwd, findings) {
|
|
1614
|
+
const qualityGates = readDoc(cwd, "QUALITY_GATES.md");
|
|
1615
|
+
if (!qualityGates) return;
|
|
1616
|
+
if (!includesAll(qualityGates, ["baseline", "strong", "best-practice", "evidence"])) {
|
|
1617
|
+
findings.push({
|
|
1618
|
+
level: "warn",
|
|
1619
|
+
area: "quality",
|
|
1620
|
+
message: "QUALITY_GATES.md does not define baseline, strong, best-practice, and evidence expectations.",
|
|
1621
|
+
remediation: "Restore the maturity model so project quality is evaluated by evidence, not only by file presence."
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
if (!includesAll(qualityGates, [
|
|
1625
|
+
"council",
|
|
1626
|
+
"architecture",
|
|
1627
|
+
"security",
|
|
1628
|
+
"supabase",
|
|
1629
|
+
"messaging",
|
|
1630
|
+
"frontend",
|
|
1631
|
+
"accessibility",
|
|
1632
|
+
"testing",
|
|
1633
|
+
"release",
|
|
1634
|
+
"repo health"
|
|
1635
|
+
])) {
|
|
1636
|
+
findings.push({
|
|
1637
|
+
level: "warn",
|
|
1638
|
+
area: "quality",
|
|
1639
|
+
message: "QUALITY_GATES.md is missing one or more best-practice coverage areas.",
|
|
1640
|
+
remediation: "Cover council routing, architecture, security, Supabase/RLS, frontend, accessibility, testing, release, and repo-health evidence."
|
|
1641
|
+
});
|
|
1642
|
+
} else {
|
|
1643
|
+
findings.push({
|
|
1644
|
+
level: "pass",
|
|
1645
|
+
area: "quality",
|
|
1646
|
+
message: "QUALITY_GATES.md defines a multi-area best-practice maturity model."
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
function addUpgradeFindings(cwd, findings) {
|
|
1651
|
+
const upgradeDoc = readDoc(cwd, "UPGRADE.md");
|
|
1652
|
+
if (!upgradeDoc) return;
|
|
1653
|
+
if (!includesAll(upgradeDoc, ["agent-kit diff", "agent-kit update", "audit --min-readiness", "rollback", "release notes"])) {
|
|
1654
|
+
findings.push({
|
|
1655
|
+
level: "warn",
|
|
1656
|
+
area: "upgrade",
|
|
1657
|
+
message: "UPGRADE.md does not define the full agent-kit diff, update, audit, release-notes, and rollback flow.",
|
|
1658
|
+
remediation: "Document branch creation, agent-kit diff, agent-kit update, conflict review, audit readiness threshold, release notes, and rollback evidence."
|
|
1659
|
+
});
|
|
1660
|
+
} else if (!includesAll(upgradeDoc, ["next.js", "codemod", "supabase", "migration", "generated"])) {
|
|
1661
|
+
findings.push({
|
|
1662
|
+
level: "warn",
|
|
1663
|
+
area: "upgrade",
|
|
1664
|
+
message: "UPGRADE.md does not cover framework codemods, Supabase migrations, or generated type review.",
|
|
1665
|
+
remediation: "Include Next.js upgrade/codemod review and Supabase migration history, RLS impact, rollback, and generated type checks."
|
|
1666
|
+
});
|
|
1667
|
+
} else {
|
|
1668
|
+
findings.push({
|
|
1669
|
+
level: "pass",
|
|
1670
|
+
area: "upgrade",
|
|
1671
|
+
message: "UPGRADE.md defines a reviewable upgrade, migration, audit, and rollback lifecycle."
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
var STARTER_EVIDENCE_PATTERNS = [
|
|
1676
|
+
/\bTBD\b/i,
|
|
1677
|
+
/replace with real/i,
|
|
1678
|
+
/example_table/i,
|
|
1679
|
+
/describe the product/i,
|
|
1680
|
+
/document required/i,
|
|
1681
|
+
/pass\/fail\/skipped/i
|
|
1682
|
+
];
|
|
1683
|
+
var EVIDENCE_DOCS = [
|
|
1684
|
+
"COUNCIL.md",
|
|
1685
|
+
"SPEC.md",
|
|
1686
|
+
"DESIGN.md",
|
|
1687
|
+
"MESSAGING.md",
|
|
1688
|
+
"SECURITY.md",
|
|
1689
|
+
"TESTING.md",
|
|
1690
|
+
"DEPLOYMENT.md",
|
|
1691
|
+
"ASSISTANT_ADAPTERS.md",
|
|
1692
|
+
"MODEL_ROUTING.md",
|
|
1693
|
+
"UPGRADE.md"
|
|
1694
|
+
];
|
|
1695
|
+
function addProjectEvidenceFindings(cwd, findings) {
|
|
1696
|
+
const placeholderDocs = EVIDENCE_DOCS.filter((doc) => {
|
|
1697
|
+
const text = readDoc(cwd, doc);
|
|
1698
|
+
return text && STARTER_EVIDENCE_PATTERNS.some((pattern) => pattern.test(text));
|
|
1699
|
+
});
|
|
1700
|
+
if (placeholderDocs.length > 0) {
|
|
1701
|
+
findings.push({
|
|
1702
|
+
level: "warn",
|
|
1703
|
+
area: "evidence",
|
|
1704
|
+
message: `Project evidence docs still contain starter placeholders: ${placeholderDocs.join(", ")}.`,
|
|
1705
|
+
remediation: "Replace TBD/example rows with real project evidence, or document why an item is not applicable before claiming strong or best-practice maturity."
|
|
1706
|
+
});
|
|
1707
|
+
} else {
|
|
1708
|
+
findings.push({
|
|
1709
|
+
level: "pass",
|
|
1710
|
+
area: "evidence",
|
|
1711
|
+
message: "Project evidence docs do not contain starter placeholders."
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
function addMessagingFindings(cwd, findings) {
|
|
1716
|
+
const messagingDoc = readDoc(cwd, "MESSAGING.md");
|
|
1717
|
+
if (!messagingDoc) return;
|
|
1718
|
+
if (!includesAll(messagingDoc, ["discovery questions", "audience", "pain", "outcome", "differentiator", "proof", "objections", "voice", "conversion"])) {
|
|
1719
|
+
findings.push({
|
|
1720
|
+
level: "warn",
|
|
1721
|
+
area: "messaging",
|
|
1722
|
+
message: "MESSAGING.md does not capture the required discovery questions and value-proposition inputs.",
|
|
1723
|
+
remediation: "Document audience, pain, outcome, differentiator, proof, objections, voice, conversion goal, and unanswered discovery questions before accepting final copy."
|
|
1724
|
+
});
|
|
1725
|
+
} else {
|
|
1726
|
+
findings.push({
|
|
1727
|
+
level: "pass",
|
|
1728
|
+
area: "messaging",
|
|
1729
|
+
message: "MESSAGING.md captures question-led positioning and value-proposition inputs."
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
if (!includesAll(messagingDoc, ["claim", "proof required", "current proof", "objection", "cta"])) {
|
|
1733
|
+
findings.push({
|
|
1734
|
+
level: "warn",
|
|
1735
|
+
area: "messaging",
|
|
1736
|
+
message: "MESSAGING.md does not connect claims, proof, objections, and CTA hierarchy.",
|
|
1737
|
+
remediation: "Track claims against proof, objection handling, primary CTA, secondary CTA, and risky claims before release."
|
|
1738
|
+
});
|
|
1739
|
+
} else {
|
|
1740
|
+
findings.push({
|
|
1741
|
+
level: "pass",
|
|
1742
|
+
area: "messaging",
|
|
1743
|
+
message: "MESSAGING.md connects claims, proof, objections, and CTA hierarchy."
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
function auditProject(cwd) {
|
|
1748
|
+
const findings = [];
|
|
1749
|
+
const manifest = readManifest(cwd);
|
|
1750
|
+
if (!manifest) {
|
|
1751
|
+
findings.push({
|
|
1752
|
+
level: "fail",
|
|
1753
|
+
area: "install",
|
|
1754
|
+
message: "Project has no .agent-kit/manifest.json.",
|
|
1755
|
+
remediation: "Run agent-kit init --stack next-supabase."
|
|
1756
|
+
});
|
|
1757
|
+
} else {
|
|
1758
|
+
findings.push({
|
|
1759
|
+
level: "pass",
|
|
1760
|
+
area: "install",
|
|
1761
|
+
message: `Agent kit installed at version ${manifest.packageVersion}.`
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
addTemplateHashFindings(cwd, findings);
|
|
1765
|
+
addAgentRosterFindings(cwd, findings);
|
|
1766
|
+
addSchemaFindings(cwd, findings);
|
|
1767
|
+
addCouncilSessionRecordFindings(cwd, findings);
|
|
1768
|
+
addAgentStudioFindings(cwd, findings);
|
|
1769
|
+
for (const doc of ROOT_DOCS) {
|
|
1770
|
+
if (existsSync6(join6(cwd, doc))) {
|
|
1771
|
+
findings.push({ level: "pass", area: "docs", message: `${doc} exists.` });
|
|
1772
|
+
} else {
|
|
1773
|
+
findings.push({
|
|
1774
|
+
level: doc === "MODEL_ROUTING.md" ? "warn" : "fail",
|
|
1775
|
+
area: "docs",
|
|
1776
|
+
message: `${doc} is missing.`,
|
|
1777
|
+
remediation: `Run agent-kit init or restore ${doc} from the next-supabase template.`
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
addCouncilDocFindings(cwd, findings);
|
|
1782
|
+
addAssistantAdapterFindings(cwd, findings);
|
|
1783
|
+
addModelRoutingFindings(cwd, findings);
|
|
1784
|
+
addMessagingFindings(cwd, findings);
|
|
1785
|
+
addQualityGateFindings(cwd, findings);
|
|
1786
|
+
addUpgradeFindings(cwd, findings);
|
|
1787
|
+
addProjectEvidenceFindings(cwd, findings);
|
|
1788
|
+
const security = readDoc(cwd, "SECURITY.md");
|
|
1789
|
+
if (!includesAny(security, ["OWASP", "Top 10"])) {
|
|
1790
|
+
findings.push({
|
|
1791
|
+
level: "fail",
|
|
1792
|
+
area: "security",
|
|
1793
|
+
message: "SECURITY.md does not explicitly reference OWASP Top 10 review.",
|
|
1794
|
+
remediation: "Add OWASP Top 10 coverage to the security checklist."
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
if (!includesAny(security, ["RLS", "row level security"])) {
|
|
1798
|
+
findings.push({
|
|
1799
|
+
level: "fail",
|
|
1800
|
+
area: "security",
|
|
1801
|
+
message: "SECURITY.md does not explicitly cover Supabase RLS.",
|
|
1802
|
+
remediation: "Require authorization to be enforced in Postgres RLS, not only in the UI."
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
if (!includesAny(security, ["service-role", "service role"])) {
|
|
1806
|
+
findings.push({
|
|
1807
|
+
level: "warn",
|
|
1808
|
+
area: "security",
|
|
1809
|
+
message: "SECURITY.md does not mention service-role key isolation.",
|
|
1810
|
+
remediation: "Document that service-role keys are server-only and never exposed to client bundles."
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
addFrontendFindings(cwd, findings);
|
|
1814
|
+
const testing = readDoc(cwd, "TESTING.md");
|
|
1815
|
+
if (!includesAny(testing, ["Playwright", "smoke"])) {
|
|
1816
|
+
findings.push({
|
|
1817
|
+
level: "warn",
|
|
1818
|
+
area: "testing",
|
|
1819
|
+
message: "TESTING.md does not require Playwright or smoke coverage.",
|
|
1820
|
+
remediation: "Define critical-path Playwright smoke tests for auth and primary workflows."
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
if (!includesAny(testing, ["visual regression", "visual QA", "screenshot evidence", "toHaveScreenshot", "Storybook", "Chromatic", "Argos"])) {
|
|
1824
|
+
findings.push({
|
|
1825
|
+
level: "warn",
|
|
1826
|
+
area: "testing",
|
|
1827
|
+
message: "TESTING.md does not define visual QA or visual-regression evidence.",
|
|
1828
|
+
remediation: "Document the visual QA tier: screenshot review, Playwright screenshots, Storybook visual tests, or a visual-regression service for important UI changes."
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
return findings;
|
|
1832
|
+
}
|
|
1833
|
+
function createReadiness(findings, summary) {
|
|
1834
|
+
const nextActions = findings.filter((finding) => finding.level === "fail" || finding.level === "warn").map((finding) => finding.remediation ?? finding.message).filter((value, index, values) => values.indexOf(value) === index).slice(0, 5);
|
|
1835
|
+
if (summary.fail > 0) {
|
|
1836
|
+
return {
|
|
1837
|
+
level: "needs-setup",
|
|
1838
|
+
summary: "Required setup or contract checks are failing.",
|
|
1839
|
+
nextActions
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
if (findings.some((finding) => finding.level === "warn" && finding.area === "evidence")) {
|
|
1843
|
+
return {
|
|
1844
|
+
level: "baseline-setup",
|
|
1845
|
+
summary: "Agent kit setup is valid, but project-specific evidence still needs to replace starter placeholders.",
|
|
1846
|
+
nextActions
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
if (summary.warn > 0) {
|
|
1850
|
+
return {
|
|
1851
|
+
level: "needs-improvement",
|
|
1852
|
+
summary: "No blocking failures, but warnings remain before this can be treated as best-practice ready.",
|
|
1853
|
+
nextActions
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
return {
|
|
1857
|
+
level: "best-practice-candidate",
|
|
1858
|
+
summary: "Static audit found no setup, evidence, security, frontend, testing, or council warnings.",
|
|
1859
|
+
nextActions
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
function createAuditReport(cwd) {
|
|
1863
|
+
const findings = auditProject(cwd);
|
|
1864
|
+
const summary = { pass: 0, warn: 0, fail: 0 };
|
|
1865
|
+
for (const finding of findings) summary[finding.level] += 1;
|
|
1866
|
+
return { summary, readiness: createReadiness(findings, summary), findings };
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// src/install/diff.ts
|
|
1870
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
1871
|
+
import { join as join7 } from "path";
|
|
1872
|
+
function statusForTextFile(target, template) {
|
|
1873
|
+
if (!existsSync7(target)) return "missing";
|
|
1874
|
+
const targetHash = sha256(readFileSync5(target, "utf8"));
|
|
1875
|
+
const templateHash = sha256(readFileSync5(template, "utf8"));
|
|
1876
|
+
return targetHash === templateHash ? "unchanged" : "changed";
|
|
1877
|
+
}
|
|
1878
|
+
function diffProject(cwd, stack = "next-supabase") {
|
|
1879
|
+
const packageRoot = findPackageRoot();
|
|
1880
|
+
const templateRoot = join7(packageRoot, "templates", stack);
|
|
1881
|
+
const libraryFolders = {
|
|
1882
|
+
missing: [],
|
|
1883
|
+
present: [],
|
|
1884
|
+
willRefresh: [...LIBRARY_FOLDERS]
|
|
1885
|
+
};
|
|
1886
|
+
const result = {
|
|
1887
|
+
missing: [],
|
|
1888
|
+
unchanged: [],
|
|
1889
|
+
changed: [],
|
|
1890
|
+
agentRoster: "missing",
|
|
1891
|
+
modelRouting: "missing",
|
|
1892
|
+
libraryFolders,
|
|
1893
|
+
preview: {
|
|
1894
|
+
wouldCreate: [],
|
|
1895
|
+
wouldWriteConflicts: [],
|
|
1896
|
+
wouldRefreshLibraryFolders: [...LIBRARY_FOLDERS],
|
|
1897
|
+
wouldCreateAgentRoster: false,
|
|
1898
|
+
wouldWriteAgentRosterConflict: false,
|
|
1899
|
+
wouldCreateModelRouting: false,
|
|
1900
|
+
wouldWriteModelRoutingConflict: false
|
|
1901
|
+
}
|
|
1902
|
+
};
|
|
1903
|
+
for (const doc of ROOT_DOCS) {
|
|
1904
|
+
const target = join7(cwd, doc);
|
|
1905
|
+
const template = join7(templateRoot, doc);
|
|
1906
|
+
const status = statusForTextFile(target, template);
|
|
1907
|
+
if (status === "missing") {
|
|
1908
|
+
result.missing.push(doc);
|
|
1909
|
+
result.preview.wouldCreate.push(doc);
|
|
1910
|
+
continue;
|
|
1911
|
+
}
|
|
1912
|
+
if (status === "unchanged") result.unchanged.push(doc);
|
|
1913
|
+
else {
|
|
1914
|
+
result.changed.push(doc);
|
|
1915
|
+
result.preview.wouldWriteConflicts.push(doc);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
result.agentRoster = statusForTextFile(join7(cwd, DEFAULT_AGENT_ROSTER_TARGET), join7(packageRoot, DEFAULT_AGENT_ROSTER_SOURCE));
|
|
1919
|
+
if (result.agentRoster === "missing") {
|
|
1920
|
+
result.preview.wouldCreate.push(DEFAULT_AGENT_ROSTER_TARGET);
|
|
1921
|
+
result.preview.wouldCreateAgentRoster = true;
|
|
1922
|
+
}
|
|
1923
|
+
if (result.agentRoster === "changed") {
|
|
1924
|
+
result.preview.wouldWriteConflicts.push(DEFAULT_AGENT_ROSTER_TARGET);
|
|
1925
|
+
result.preview.wouldWriteAgentRosterConflict = true;
|
|
1926
|
+
}
|
|
1927
|
+
result.modelRouting = statusForTextFile(join7(cwd, DEFAULT_MODEL_ROUTING_TARGET), join7(packageRoot, DEFAULT_MODEL_ROUTING_SOURCE));
|
|
1928
|
+
if (result.modelRouting === "missing") {
|
|
1929
|
+
result.preview.wouldCreate.push(DEFAULT_MODEL_ROUTING_TARGET);
|
|
1930
|
+
result.preview.wouldCreateModelRouting = true;
|
|
1931
|
+
}
|
|
1932
|
+
if (result.modelRouting === "changed") {
|
|
1933
|
+
result.preview.wouldWriteConflicts.push(DEFAULT_MODEL_ROUTING_TARGET);
|
|
1934
|
+
result.preview.wouldWriteModelRoutingConflict = true;
|
|
1935
|
+
}
|
|
1936
|
+
for (const folder of LIBRARY_FOLDERS) {
|
|
1937
|
+
const target = join7(cwd, ".agent-kit", folder);
|
|
1938
|
+
if (existsSync7(target)) libraryFolders.present.push(folder);
|
|
1939
|
+
else libraryFolders.missing.push(folder);
|
|
1940
|
+
}
|
|
1941
|
+
return result;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// src/research/discover.ts
|
|
1945
|
+
import { Octokit } from "@octokit/rest";
|
|
1946
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1947
|
+
import { join as join8 } from "path";
|
|
1948
|
+
|
|
1949
|
+
// src/research/config.ts
|
|
1950
|
+
import { z as z2 } from "zod";
|
|
1951
|
+
var researchCategorySchema = z2.object({
|
|
1952
|
+
name: z2.string().min(1),
|
|
1953
|
+
queries: z2.array(z2.string().min(1)).min(1),
|
|
1954
|
+
targetCount: z2.number().int().positive()
|
|
1955
|
+
});
|
|
1956
|
+
var researchSeedRepoSchema = z2.object({
|
|
1957
|
+
fullName: z2.string().regex(/^[^/\s]+\/[^/\s]+$/),
|
|
1958
|
+
category: z2.string().min(1)
|
|
1959
|
+
});
|
|
1960
|
+
var researchConfigSchema = z2.object({
|
|
1961
|
+
maxRepos: z2.number().int().positive().default(100),
|
|
1962
|
+
minStars: z2.number().int().nonnegative().default(100),
|
|
1963
|
+
activeSince: z2.string().default("2024-12-01"),
|
|
1964
|
+
excludeRepos: z2.array(z2.string().regex(/^[^/\s]+\/[^/\s]+$/)).default([]),
|
|
1965
|
+
categories: z2.array(researchCategorySchema).min(1),
|
|
1966
|
+
seedRepos: z2.array(researchSeedRepoSchema).default([])
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
// src/research/discover.ts
|
|
1970
|
+
async function discoverRepos(options) {
|
|
1971
|
+
const packageRoot = findPackageRoot();
|
|
1972
|
+
const configPath = join8(packageRoot, "research", "scan-config.json");
|
|
1973
|
+
const config = researchConfigSchema.parse(JSON.parse(readFileSync6(configPath, "utf8")));
|
|
1974
|
+
const token = options.token ?? process.env.GITHUB_TOKEN;
|
|
1975
|
+
if (!token) {
|
|
1976
|
+
throw new Error("GITHUB_TOKEN is required for GitHub API research discovery.");
|
|
1977
|
+
}
|
|
1978
|
+
const octokit = new Octokit({ auth: token });
|
|
1979
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
1980
|
+
const maxRepos = options.limit ?? config.maxRepos;
|
|
1981
|
+
const excludedRepos = new Set(config.excludeRepos.map((repo) => repo.toLowerCase()));
|
|
1982
|
+
for (const category of config.categories) {
|
|
1983
|
+
let categoryCount = 0;
|
|
1984
|
+
for (const query of category.queries) {
|
|
1985
|
+
if (deduped.size >= maxRepos || categoryCount >= category.targetCount) break;
|
|
1986
|
+
const response = await octokit.search.repos({
|
|
1987
|
+
q: `${query} archived:false pushed:>=${config.activeSince} stars:>=${config.minStars}`,
|
|
1988
|
+
sort: "stars",
|
|
1989
|
+
order: "desc",
|
|
1990
|
+
per_page: 100
|
|
1991
|
+
});
|
|
1992
|
+
for (const repo of response.data.items) {
|
|
1993
|
+
if (deduped.size >= maxRepos || categoryCount >= category.targetCount) break;
|
|
1994
|
+
if (deduped.has(repo.full_name)) continue;
|
|
1995
|
+
if (excludedRepos.has(repo.full_name.toLowerCase())) continue;
|
|
1996
|
+
deduped.set(repo.full_name, {
|
|
1997
|
+
fullName: repo.full_name,
|
|
1998
|
+
htmlUrl: repo.html_url,
|
|
1999
|
+
description: repo.description ?? "",
|
|
2000
|
+
stars: repo.stargazers_count,
|
|
2001
|
+
pushedAt: repo.pushed_at ?? "",
|
|
2002
|
+
language: repo.language,
|
|
2003
|
+
topics: repo.topics ?? [],
|
|
2004
|
+
category: category.name
|
|
2005
|
+
});
|
|
2006
|
+
categoryCount += 1;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
for (const seed of config.seedRepos) {
|
|
2011
|
+
if (deduped.size >= maxRepos) break;
|
|
2012
|
+
if (deduped.has(seed.fullName)) continue;
|
|
2013
|
+
if (excludedRepos.has(seed.fullName.toLowerCase())) continue;
|
|
2014
|
+
const [owner, repo] = seed.fullName.split("/");
|
|
2015
|
+
if (!owner || !repo) continue;
|
|
2016
|
+
try {
|
|
2017
|
+
const response = await octokit.repos.get({ owner, repo });
|
|
2018
|
+
deduped.set(response.data.full_name, {
|
|
2019
|
+
fullName: response.data.full_name,
|
|
2020
|
+
htmlUrl: response.data.html_url,
|
|
2021
|
+
description: response.data.description ?? "",
|
|
2022
|
+
stars: response.data.stargazers_count,
|
|
2023
|
+
pushedAt: response.data.pushed_at ?? "",
|
|
2024
|
+
language: response.data.language,
|
|
2025
|
+
topics: response.data.topics ?? [],
|
|
2026
|
+
category: seed.category
|
|
2027
|
+
});
|
|
2028
|
+
} catch (error) {
|
|
2029
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2030
|
+
console.warn(`Skipping seed repo ${seed.fullName}: ${message}`);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
const candidates = [...deduped.values()].slice(0, maxRepos);
|
|
2034
|
+
const output = options.output ?? join8(options.cwd, "research", "repo-candidates.json");
|
|
2035
|
+
writeText(output, `${JSON.stringify(candidates, null, 2)}
|
|
2036
|
+
`);
|
|
2037
|
+
return candidates;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// src/research/scan.ts
|
|
2041
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync2, readFileSync as readFileSync8, rmSync } from "fs";
|
|
2042
|
+
import { join as join10 } from "path";
|
|
2043
|
+
import { simpleGit } from "simple-git";
|
|
2044
|
+
|
|
2045
|
+
// src/research/analyze.ts
|
|
2046
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
2047
|
+
import { join as join9 } from "path";
|
|
2048
|
+
function normalizeRelativePath(file) {
|
|
2049
|
+
return file.replace(/\\/g, "/");
|
|
2050
|
+
}
|
|
2051
|
+
function hasFile(files, matcher) {
|
|
2052
|
+
return files.some((file) => matcher.test(normalizeRelativePath(file)));
|
|
2053
|
+
}
|
|
2054
|
+
function fileText(root, file) {
|
|
2055
|
+
const path = join9(root, file);
|
|
2056
|
+
return existsSync9(path) ? readFileSync7(path, "utf8") : "";
|
|
2057
|
+
}
|
|
2058
|
+
function textIncludes(root, files, matcher, terms) {
|
|
2059
|
+
const lowerTerms = terms.map((term) => term.toLowerCase());
|
|
2060
|
+
return files.filter((file) => matcher.test(normalizeRelativePath(file))).some((file) => {
|
|
2061
|
+
const text = fileText(root, file).toLowerCase();
|
|
2062
|
+
return lowerTerms.some((term) => text.includes(term));
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
function bounded(value) {
|
|
2066
|
+
return Math.max(0, Math.min(5, value));
|
|
2067
|
+
}
|
|
2068
|
+
function analyzeRepository(candidate, repoRoot) {
|
|
2069
|
+
const files = listFilesRecursive(repoRoot);
|
|
2070
|
+
const score = {
|
|
2071
|
+
architecture: bounded(
|
|
2072
|
+
Number(hasFile(files, /^app\//)) + Number(hasFile(files, /^components\//)) + Number(hasFile(files, /^lib\//)) + Number(hasFile(files, /^server\//)) + Number(hasFile(files, /^packages\//))
|
|
2073
|
+
),
|
|
2074
|
+
supabaseAuthRls: bounded(
|
|
2075
|
+
Number(hasFile(files, /^supabase\/migrations\//)) * 2 + Number(textIncludes(repoRoot, files, /\.(sql|md|ts|tsx)$/, ["row level security", "policy", "rls"])) + Number(textIncludes(repoRoot, files, /\.(ts|tsx)$/, ["createServerClient", "supabase-js"])) + Number(textIncludes(repoRoot, files, /\.(ts|tsx)$/, ["service_role", "service-role"]))
|
|
2076
|
+
),
|
|
2077
|
+
security: bounded(
|
|
2078
|
+
Number(hasFile(files, /(^|\/)SECURITY\.md$/)) + Number(textIncludes(repoRoot, files, /\.(md|yml|yaml|ts|tsx)$/, ["OWASP", "CodeQL", "rate limit"])) + Number(hasFile(files, /^\.github\/workflows\//)) + Number(textIncludes(repoRoot, files, /\.(ts|tsx)$/, ["zod", "safeParse"])) + Number(textIncludes(repoRoot, files, /\.(md|ts|tsx)$/, ["csrf", "ssrf", "idor"]))
|
|
2079
|
+
),
|
|
2080
|
+
frontendDesign: bounded(
|
|
2081
|
+
Number(hasFile(files, /^components\//)) + Number(textIncludes(repoRoot, files, /\.(css|ts|tsx|json)$/, ["tokens", "theme", "tailwind", "radix"])) + Number(textIncludes(repoRoot, files, /\.(tsx|md|mdx)$/, ["empty state", "loading", "error state"])) + Number(hasFile(files, /(^|\/)(DESIGN|STYLE_GUIDE)\.md$/)) + Number(textIncludes(repoRoot, files, /\.(md|mdx)$/, ["creative direction", "content inventory", "user needs"])) + Number(textIncludes(repoRoot, files, /\.(md|mdx)$/, ["reference set", "anti-reference", "design critique", "distinctiveness"])) + Number(textIncludes(repoRoot, files, /\.(md|mdx)$/, ["product quality scorecard", "user/task fit", "content specificity", "source safety"])) + Number(textIncludes(repoRoot, files, /\.(tsx|css)$/, ["aria-", "focus-visible"]))
|
|
2082
|
+
),
|
|
2083
|
+
accessibility: bounded(
|
|
2084
|
+
Number(textIncludes(repoRoot, files, /\.(tsx|md)$/, ["aria-", "keyboard", "focus-visible"])) + Number(textIncludes(repoRoot, files, /\.(tsx|md)$/, ["WCAG", "accessibility", "a11y"])) + Number(textIncludes(repoRoot, files, /\.(json|js|ts)$/, ["axe", "eslint-plugin-jsx-a11y"]))
|
|
2085
|
+
),
|
|
2086
|
+
testing: bounded(
|
|
2087
|
+
Number(hasFile(files, /(vitest|jest)\.config\.(js|ts|mjs)$/)) + Number(hasFile(files, /playwright\.config\.(js|ts|mjs)$/)) * 2 + Number(hasFile(files, /(^|\/)(tests|__tests__|e2e)\//)) + Number(hasFile(files, /^\.storybook\//)) + Number(textIncludes(repoRoot, files, /\.(ts|tsx|js|jsx|md|mdx|json|yml|yaml)$/, ["toHaveScreenshot", "visual regression", "chromatic", "argos", "loki", "test-storybook"])) + Number(textIncludes(repoRoot, files, /package\.json$/, ["test"]))
|
|
2088
|
+
),
|
|
2089
|
+
documentation: bounded(
|
|
2090
|
+
Number(hasFile(files, /(^|\/)README\.md$/)) + Number(hasFile(files, /(^|\/)CONTRIBUTING\.md$/)) + Number(hasFile(files, /(^|\/)CHANGELOG\.md$/)) + Number(hasFile(files, /(^|\/)(CODE_OF_CONDUCT|SUPPORT|GOVERNANCE)\.md$/)) + Number(hasFile(files, /^docs\//)) + Number(textIncludes(repoRoot, files, /\.(md|mdx)$/, ["architecture", "decision", "deployment"]))
|
|
2091
|
+
),
|
|
2092
|
+
ciDeployment: bounded(
|
|
2093
|
+
Number(hasFile(files, /^\.github\/workflows\//)) * 2 + Number(hasFile(files, /vercel\.json$/)) + Number(textIncludes(repoRoot, files, /\.(yml|yaml|json|md)$/, ["deployment", "preview", "production"]))
|
|
2094
|
+
),
|
|
2095
|
+
repoHealth: bounded(
|
|
2096
|
+
Number(hasFile(files, /^\.github\/ISSUE_TEMPLATE\//)) + Number(hasFile(files, /(^|\/)pull_request_template\.md$/i)) + Number(hasFile(files, /^\.github\/CODEOWNERS$/)) + Number(hasFile(files, /^\.github\/(dependabot|labels|labeler)\.ya?ml$/)) + Number(hasFile(files, /^\.github\/workflows\/.*(codeql|labeler).*\.ya?ml$/)) + Number(hasFile(files, /(^|\/)(CODE_OF_CONDUCT|SUPPORT|GOVERNANCE|REPOSITORY_SETTINGS)\.md$/)) + Number(textIncludes(repoRoot, files, /\.(md|yml|yaml)$/, ["branch protection", "private vulnerability reporting", "required status checks"]))
|
|
2097
|
+
),
|
|
2098
|
+
supplyChain: bounded(
|
|
2099
|
+
Number(hasFile(files, /^\.github\/workflows\/.*dependency.*review.*\.ya?ml$/)) + Number(hasFile(files, /^\.github\/workflows\/.*scorecard.*\.ya?ml$/)) + Number(textIncludes(repoRoot, files, /^\.github\/workflows\/.*\.ya?ml$/, ["id-token: write", "npm publish", "trusted publishing", "provenance"])) + Number(textIncludes(repoRoot, files, /^\.github\/workflows\/.*\.ya?ml$/, ["dependency-review-action", "scorecard-action", "npm audit"])) + Number(hasFile(files, /(^|\/)SUPPLY_CHAIN\.md$/)) + Number(textIncludes(repoRoot, files, /\.(md|yml|yaml|json)$/, ["provenance", "OIDC", "trusted publishing", "OpenSSF"]))
|
|
2100
|
+
),
|
|
2101
|
+
agentReadiness: bounded(
|
|
2102
|
+
Number(hasFile(files, /(^|\/)AGENTS\.md$/)) * 2 + Number(hasFile(files, /(^|\/)\.cursor\//)) + Number(hasFile(files, /(^|\/)CLAUDE\.md$/)) + Number(hasFile(files, /(^|\/)schemas\//)) + Number(textIncludes(repoRoot, files, /\.(md|mdx|json|ts|tsx)$/, ["agent", "prompt", "skill", "handoff", "trace", "guardrail", "agent-roster"]))
|
|
2103
|
+
)
|
|
2104
|
+
};
|
|
2105
|
+
const selectedFiles = files.filter(
|
|
2106
|
+
(file) => /(^|\/)(README|SECURITY|CONTRIBUTING|CHANGELOG|CODE_OF_CONDUCT|SUPPORT|GOVERNANCE|REPOSITORY_SETTINGS|SUPPLY_CHAIN|AGENTS|CLAUDE|DESIGN|STYLE_GUIDE|TESTING|DEPLOYMENT)\.md$/.test(file) || /(^|\/)COUNCIL\.md$/.test(file) || /^\.github\/workflows\//.test(file) || /^\.github\/ISSUE_TEMPLATE\//.test(file) || /^\.github\/CODEOWNERS$/.test(file) || /^\.github\/(dependabot|labels|labeler)\.ya?ml$/.test(file) || /(^|\/)pull_request_template\.md$/i.test(file) || /^\.storybook\//.test(file) || /^schemas\//.test(file) || /^supabase\/migrations\//.test(file) || /package\.json$/.test(file) || /playwright\.config\.(js|ts|mjs)$/.test(file) || /(chromatic|argos|loki)\.(config\.)?(json|js|ts|mjs|yml|yaml)$/.test(file)
|
|
2107
|
+
).slice(0, 30);
|
|
2108
|
+
const strongPractices = [];
|
|
2109
|
+
if (score.security >= 4) strongPractices.push("Security posture is explicit through docs, validation, CI, or review tooling.");
|
|
2110
|
+
if (score.supabaseAuthRls >= 4) strongPractices.push("Supabase authorization appears to be handled close to the data boundary.");
|
|
2111
|
+
if (score.testing >= 4) strongPractices.push("Test setup includes meaningful automated, browser-level, component-state, or visual-regression coverage.");
|
|
2112
|
+
if (score.frontendDesign >= 4)
|
|
2113
|
+
strongPractices.push("Frontend implementation shows reusable components, states, design-system, content-first direction, or reference-led critique signals.");
|
|
2114
|
+
if (score.documentation >= 4) strongPractices.push("Documentation is strong enough for external contributors or agents to onboard.");
|
|
2115
|
+
if (score.repoHealth >= 4) strongPractices.push("Repository health is supported by issue/PR templates, labels, dependency automation, code scanning, ownership, branch protection guidance, or support docs.");
|
|
2116
|
+
if (score.supplyChain >= 4) strongPractices.push("Supply-chain posture includes provenance, dependency review, Scorecard, OIDC publishing, or release integrity signals.");
|
|
2117
|
+
const weakPractices = [];
|
|
2118
|
+
if (score.security < 3) weakPractices.push("Security expectations are implicit or incomplete.");
|
|
2119
|
+
if (score.supabaseAuthRls < 3) weakPractices.push("Supabase RLS/Auth practices are not clearly discoverable.");
|
|
2120
|
+
if (score.accessibility < 3) weakPractices.push("Accessibility signals are weak or absent.");
|
|
2121
|
+
if (score.repoHealth < 3) weakPractices.push("Public repository health files, labels, branch protection guidance, contribution workflow, dependency automation, or code scanning are weak.");
|
|
2122
|
+
if (score.supplyChain < 3) weakPractices.push("Supply-chain provenance, dependency review, Scorecard, or release-integrity signals are weak.");
|
|
2123
|
+
if (score.agentReadiness < 3) weakPractices.push("Agent handoff, tracing, guardrail, schema, or AI-workflow instructions are not mature.");
|
|
2124
|
+
const patternsToAdopt = [
|
|
2125
|
+
"Prefer explicit docs and checklists over tribal conventions.",
|
|
2126
|
+
"Promote authorization and validation rules into reusable review gates.",
|
|
2127
|
+
"Separate frontend design quality from generic implementation review.",
|
|
2128
|
+
"Treat brand, content, and creative-direction evidence as frontend quality inputs, not optional polish.",
|
|
2129
|
+
"Use references and anti-references as critique inputs, not as source designs to copy.",
|
|
2130
|
+
"Use repeatable frontend product-quality scoring for user task, content specificity, visual identity, IA, states, accessibility, and source safety.",
|
|
2131
|
+
"Treat component-state screenshots and visual-regression evidence as acceptance artifacts for high-risk UI changes.",
|
|
2132
|
+
"Promote agent handoff practices into schema-backed records and auditable evidence instead of prose-only instructions.",
|
|
2133
|
+
"Treat public repository health files, issue/PR templates, labels, branch protection guidance, dependency automation, and code scanning as release-readiness assets.",
|
|
2134
|
+
"Treat package provenance, dependency review, Scorecard, and release workflow controls as package trust requirements."
|
|
2135
|
+
];
|
|
2136
|
+
const impactOnKit = [
|
|
2137
|
+
"Use score deltas to decide which templates and skills need stronger language.",
|
|
2138
|
+
"Add repeated high-confidence patterns to checklists, not one-off project quirks."
|
|
2139
|
+
];
|
|
2140
|
+
return {
|
|
2141
|
+
candidate,
|
|
2142
|
+
score,
|
|
2143
|
+
selectedFiles,
|
|
2144
|
+
strongPractices,
|
|
2145
|
+
weakPractices,
|
|
2146
|
+
patternsToAdopt,
|
|
2147
|
+
impactOnKit
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
// src/research/scan.ts
|
|
2152
|
+
function findingToMarkdown(finding) {
|
|
2153
|
+
const total = Object.values(finding.score).reduce((sum, value) => sum + value, 0);
|
|
2154
|
+
const maxScore = Object.keys(finding.score).length * 5;
|
|
2155
|
+
return `# Repo Finding: ${finding.candidate.fullName}
|
|
2156
|
+
|
|
2157
|
+
## Why It Was Selected
|
|
2158
|
+
- Category: ${finding.candidate.category}
|
|
2159
|
+
- Stars: ${finding.candidate.stars}
|
|
2160
|
+
- Last pushed: ${finding.candidate.pushedAt}
|
|
2161
|
+
- Language: ${finding.candidate.language ?? "unknown"}
|
|
2162
|
+
- URL: ${finding.candidate.htmlUrl}
|
|
2163
|
+
- Score: ${total}/${maxScore}
|
|
2164
|
+
|
|
2165
|
+
## Score
|
|
2166
|
+
\`\`\`json
|
|
2167
|
+
${JSON.stringify(finding.score, null, 2)}
|
|
2168
|
+
\`\`\`
|
|
2169
|
+
|
|
2170
|
+
## Strong Practices
|
|
2171
|
+
${finding.strongPractices.map((item) => `- ${item}`).join("\n") || "- None detected by static scan."}
|
|
2172
|
+
|
|
2173
|
+
## Weaknesses / Not Worth Copying Blindly
|
|
2174
|
+
${finding.weakPractices.map((item) => `- ${item}`).join("\n") || "- None detected by static scan."}
|
|
2175
|
+
|
|
2176
|
+
## Files Worth Studying
|
|
2177
|
+
${finding.selectedFiles.map((item) => `- \`${item}\``).join("\n") || "- No high-signal files detected."}
|
|
2178
|
+
|
|
2179
|
+
## Patterns To Adopt
|
|
2180
|
+
${finding.patternsToAdopt.map((item) => `- ${item}`).join("\n")}
|
|
2181
|
+
|
|
2182
|
+
## Impact On Agent Kit
|
|
2183
|
+
${finding.impactOnKit.map((item) => `- ${item}`).join("\n")}
|
|
2184
|
+
`;
|
|
2185
|
+
}
|
|
2186
|
+
async function scanRepos(options) {
|
|
2187
|
+
const candidatesPath = options.candidatesPath ?? join10(options.cwd, "research", "repo-candidates.json");
|
|
2188
|
+
if (!existsSync10(candidatesPath)) {
|
|
2189
|
+
throw new Error(`Candidates file not found: ${candidatesPath}`);
|
|
2190
|
+
}
|
|
2191
|
+
const candidates = JSON.parse(readFileSync8(candidatesPath, "utf8"));
|
|
2192
|
+
const workdir = options.workdir ?? join10(options.cwd, "research", "workdir");
|
|
2193
|
+
mkdirSync2(workdir, { recursive: true });
|
|
2194
|
+
mkdirSync2(join10(options.cwd, "research", "findings"), { recursive: true });
|
|
2195
|
+
const findings = [];
|
|
2196
|
+
const git = simpleGit();
|
|
2197
|
+
for (const candidate of candidates) {
|
|
2198
|
+
const repoSlug = candidate.fullName.replace("/", "__");
|
|
2199
|
+
const repoPath = join10(workdir, repoSlug);
|
|
2200
|
+
if (existsSync10(repoPath)) rmSync(repoPath, { recursive: true, force: true });
|
|
2201
|
+
await git.raw(["clone", "--depth", "1", candidate.htmlUrl, repoPath]);
|
|
2202
|
+
const finding = analyzeRepository(candidate, repoPath);
|
|
2203
|
+
findings.push(finding);
|
|
2204
|
+
writeText(join10(options.cwd, "research", "findings", `${repoSlug}.md`), findingToMarkdown(finding));
|
|
2205
|
+
if (!options.keepClones) {
|
|
2206
|
+
rmSync(repoPath, { recursive: true, force: true });
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
return findings;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
// src/research/summarize.ts
|
|
2213
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9, readdirSync as readdirSync2 } from "fs";
|
|
2214
|
+
import { join as join11 } from "path";
|
|
2215
|
+
var SUMMARY_TARGETS = {
|
|
2216
|
+
"nextjs-patterns": {
|
|
2217
|
+
title: "Next.js Patterns",
|
|
2218
|
+
scoreKeys: ["architecture", "ciDeployment", "documentation"],
|
|
2219
|
+
categories: ["official-nextjs", "production-saas"]
|
|
2220
|
+
},
|
|
2221
|
+
"supabase-rls-patterns": {
|
|
2222
|
+
title: "Supabase RLS Patterns",
|
|
2223
|
+
scoreKeys: ["supabaseAuthRls"],
|
|
2224
|
+
categories: ["supabase-nextjs"]
|
|
2225
|
+
},
|
|
2226
|
+
"security-patterns": {
|
|
2227
|
+
title: "Security Patterns",
|
|
2228
|
+
scoreKeys: ["security"],
|
|
2229
|
+
categories: ["security-quality", "supabase-nextjs", "production-saas"]
|
|
2230
|
+
},
|
|
2231
|
+
"frontend-design-patterns": {
|
|
2232
|
+
title: "Frontend Design Patterns",
|
|
2233
|
+
scoreKeys: ["frontendDesign", "accessibility"],
|
|
2234
|
+
categories: ["design-systems", "production-saas"]
|
|
2235
|
+
},
|
|
2236
|
+
"testing-patterns": {
|
|
2237
|
+
title: "Testing Patterns",
|
|
2238
|
+
scoreKeys: ["testing"],
|
|
2239
|
+
categories: ["testing-docs-agents", "official-nextjs", "production-saas"]
|
|
2240
|
+
},
|
|
2241
|
+
"docs-and-agent-patterns": {
|
|
2242
|
+
title: "Docs And Agent Patterns",
|
|
2243
|
+
scoreKeys: ["documentation", "agentReadiness"],
|
|
2244
|
+
categories: ["testing-docs-agents", "official-nextjs"]
|
|
2245
|
+
},
|
|
2246
|
+
"repo-health-patterns": {
|
|
2247
|
+
title: "Repo Health Patterns",
|
|
2248
|
+
scoreKeys: ["repoHealth", "documentation", "ciDeployment", "security"],
|
|
2249
|
+
categories: ["repo-health-maintainers", "testing-docs-agents", "security-quality", "production-saas"]
|
|
2250
|
+
},
|
|
2251
|
+
"supply-chain-patterns": {
|
|
2252
|
+
title: "Supply Chain Patterns",
|
|
2253
|
+
scoreKeys: ["supplyChain", "security", "ciDeployment", "repoHealth"],
|
|
2254
|
+
categories: ["supply-chain-security", "repo-health-maintainers", "security-quality"]
|
|
2255
|
+
}
|
|
2256
|
+
};
|
|
2257
|
+
var SCORE_KEYS = [
|
|
2258
|
+
"architecture",
|
|
2259
|
+
"supabaseAuthRls",
|
|
2260
|
+
"security",
|
|
2261
|
+
"frontendDesign",
|
|
2262
|
+
"accessibility",
|
|
2263
|
+
"testing",
|
|
2264
|
+
"documentation",
|
|
2265
|
+
"ciDeployment",
|
|
2266
|
+
"repoHealth",
|
|
2267
|
+
"supplyChain",
|
|
2268
|
+
"agentReadiness"
|
|
2269
|
+
];
|
|
2270
|
+
function sectionBullets(text, start, end) {
|
|
2271
|
+
const startIndex = text.indexOf(start);
|
|
2272
|
+
if (startIndex === -1) return [];
|
|
2273
|
+
const afterStart = text.slice(startIndex + start.length);
|
|
2274
|
+
const endIndex = afterStart.indexOf(end);
|
|
2275
|
+
const section = endIndex === -1 ? afterStart : afterStart.slice(0, endIndex);
|
|
2276
|
+
return section.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("- ") && !line.includes("None detected")).map((line) => line.slice(2));
|
|
2277
|
+
}
|
|
2278
|
+
function parseFinding(file, text) {
|
|
2279
|
+
const fullName = text.match(/^# Repo Finding: (.+)$/m)?.[1];
|
|
2280
|
+
const category = text.match(/^- Category: (.+)$/m)?.[1];
|
|
2281
|
+
const stars = Number.parseInt(text.match(/^- Stars: (\d+)$/m)?.[1] ?? "0", 10);
|
|
2282
|
+
const scoreJson = text.match(/## Score\n```json\n([\s\S]*?)\n```/)?.[1];
|
|
2283
|
+
if (!fullName || !category || !scoreJson) return null;
|
|
2284
|
+
const parsedScore = JSON.parse(scoreJson);
|
|
2285
|
+
const score = Object.fromEntries(SCORE_KEYS.map((key) => [key, parsedScore[key] ?? 0]));
|
|
2286
|
+
const totalScore = Object.values(score).reduce((sum, value) => sum + value, 0);
|
|
2287
|
+
return {
|
|
2288
|
+
file,
|
|
2289
|
+
fullName,
|
|
2290
|
+
category,
|
|
2291
|
+
stars,
|
|
2292
|
+
score,
|
|
2293
|
+
totalScore,
|
|
2294
|
+
strongPractices: sectionBullets(text, "## Strong Practices", "## Weaknesses / Not Worth Copying Blindly"),
|
|
2295
|
+
weakPractices: sectionBullets(text, "## Weaknesses / Not Worth Copying Blindly", "## Files Worth Studying")
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
function countBy(values) {
|
|
2299
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2300
|
+
for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
2301
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
2302
|
+
}
|
|
2303
|
+
function scoreFor(finding, scoreKeys) {
|
|
2304
|
+
return scoreKeys.reduce((sum, key) => sum + finding.score[key], 0);
|
|
2305
|
+
}
|
|
2306
|
+
function averageScore(findings, scoreKeys) {
|
|
2307
|
+
if (findings.length === 0) return "0.00";
|
|
2308
|
+
const max = scoreKeys.length * 5;
|
|
2309
|
+
const avg = findings.reduce((sum, finding) => sum + scoreFor(finding, scoreKeys) / max, 0) / findings.length;
|
|
2310
|
+
return avg.toFixed(2);
|
|
2311
|
+
}
|
|
2312
|
+
function renderRepoList(findings, scoreKeys) {
|
|
2313
|
+
const maxTotalScore = findings[0] ? Object.keys(findings[0].score).length * 5 : 0;
|
|
2314
|
+
return findings.slice().sort((a, b) => scoreFor(b, scoreKeys) - scoreFor(a, scoreKeys) || b.totalScore - a.totalScore || b.stars - a.stars).slice(0, 12).map((finding) => `- ${finding.fullName} (${finding.category}) - focus score ${scoreFor(finding, scoreKeys)}, total ${finding.totalScore}/${maxTotalScore}`).join("\n");
|
|
2315
|
+
}
|
|
2316
|
+
function summarizeFindings(cwd) {
|
|
2317
|
+
const findingsDir = join11(cwd, "research", "findings");
|
|
2318
|
+
if (!existsSync11(findingsDir)) {
|
|
2319
|
+
throw new Error("No research/findings directory exists. Run agent-kit research scan first.");
|
|
2320
|
+
}
|
|
2321
|
+
const findingFiles = readdirSync2(findingsDir).filter((file) => file.endsWith(".md"));
|
|
2322
|
+
const findings = findingFiles.map((file) => parseFinding(file, readFileSync9(join11(findingsDir, file), "utf8"))).filter((finding) => finding !== null);
|
|
2323
|
+
const categoryCounts = countBy(findings.map((finding) => finding.category));
|
|
2324
|
+
const outputs = [];
|
|
2325
|
+
const overview = `# Research Scan Overview
|
|
2326
|
+
|
|
2327
|
+
Generated from ${findings.length} parsed repository findings.
|
|
2328
|
+
|
|
2329
|
+
## Category Coverage
|
|
2330
|
+
${categoryCounts.map(([category, count]) => `- ${category}: ${count}`).join("\n")}
|
|
2331
|
+
|
|
2332
|
+
## Highest Total Scores
|
|
2333
|
+
${findings.slice().sort((a, b) => b.totalScore - a.totalScore || b.stars - a.stars).slice(0, 20).map((finding) => `- ${finding.fullName} (${finding.category}) - ${finding.totalScore}/${Object.keys(finding.score).length * 5}`).join("\n")}
|
|
2334
|
+
|
|
2335
|
+
## Most Repeated Strengths
|
|
2336
|
+
${countBy(findings.flatMap((finding) => finding.strongPractices)).slice(0, 12).map(([practice, count]) => `- ${practice} (${count})`).join("\n")}
|
|
2337
|
+
|
|
2338
|
+
## Most Repeated Gaps
|
|
2339
|
+
${countBy(findings.flatMap((finding) => finding.weakPractices)).slice(0, 12).map(([practice, count]) => `- ${practice} (${count})`).join("\n")}
|
|
2340
|
+
`;
|
|
2341
|
+
const overviewPath = join11(cwd, "research", "summaries", "scan-overview.md");
|
|
2342
|
+
writeText(overviewPath, overview);
|
|
2343
|
+
outputs.push(overviewPath);
|
|
2344
|
+
for (const [target, config] of Object.entries(SUMMARY_TARGETS)) {
|
|
2345
|
+
const categories = config.categories;
|
|
2346
|
+
const scopedFindings = findings.filter((finding) => categories.includes(finding.category));
|
|
2347
|
+
const path = join11(cwd, "research", "summaries", `${target}.md`);
|
|
2348
|
+
const summary = `# ${config.title}
|
|
2349
|
+
|
|
2350
|
+
Generated from ${scopedFindings.length} relevant repository findings.
|
|
2351
|
+
|
|
2352
|
+
## Focus Areas
|
|
2353
|
+
${config.scoreKeys.map((key) => `- ${key}`).join("\n")}
|
|
2354
|
+
|
|
2355
|
+
## Aggregate Evidence
|
|
2356
|
+
- Average normalized focus score: ${averageScore(scopedFindings, config.scoreKeys)}
|
|
2357
|
+
- Repositories considered: ${scopedFindings.length}
|
|
2358
|
+
|
|
2359
|
+
## Strongest Repositories For This Topic
|
|
2360
|
+
${renderRepoList(scopedFindings, config.scoreKeys) || "- No matching findings."}
|
|
2361
|
+
|
|
2362
|
+
## Repeated Strengths
|
|
2363
|
+
${countBy(scopedFindings.flatMap((finding) => finding.strongPractices)).slice(0, 8).map(([practice, count]) => `- ${practice} (${count})`).join("\n") || "- No repeated strengths detected."}
|
|
2364
|
+
|
|
2365
|
+
## Repeated Gaps
|
|
2366
|
+
${countBy(scopedFindings.flatMap((finding) => finding.weakPractices)).slice(0, 8).map(([practice, count]) => `- ${practice} (${count})`).join("\n") || "- No repeated gaps detected."}
|
|
2367
|
+
|
|
2368
|
+
## Source Findings
|
|
2369
|
+
${scopedFindings.slice().sort((a, b) => scoreFor(b, config.scoreKeys) - scoreFor(a, config.scoreKeys)).slice(0, 25).map((finding) => `- research/findings/${finding.file}`).join("\n")}
|
|
2370
|
+
`;
|
|
2371
|
+
writeText(path, summary);
|
|
2372
|
+
outputs.push(path);
|
|
2373
|
+
}
|
|
2374
|
+
return outputs;
|
|
2375
|
+
}
|
|
2376
|
+
function proposeUpdates(cwd) {
|
|
2377
|
+
const output = `# Proposed Agent Kit Updates
|
|
2378
|
+
|
|
2379
|
+
Review the generated research summaries, then convert repeated best practices into:
|
|
2380
|
+
|
|
2381
|
+
- Root markdown templates in \`templates/next-supabase\`
|
|
2382
|
+
- Reusable skills in \`skills/\`
|
|
2383
|
+
- Agent role docs in \`agents/\`
|
|
2384
|
+
- Security and frontend checklists in \`checklists/\`
|
|
2385
|
+
|
|
2386
|
+
Do not copy source code from scanned repositories. Adopt only generalized practices with clear rationale.
|
|
2387
|
+
`;
|
|
2388
|
+
const path = join11(cwd, "research", "proposed-updates.md");
|
|
2389
|
+
writeText(path, output);
|
|
2390
|
+
return path;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// src/studio/corrections.ts
|
|
2394
|
+
function fileForScope(scope) {
|
|
2395
|
+
if (scope === "agent") return AGENT_RULES_JSON;
|
|
2396
|
+
if (scope === "upstream-proposal") return UPSTREAM_PROPOSALS_JSON;
|
|
2397
|
+
return PROJECT_RULES_JSON;
|
|
2398
|
+
}
|
|
2399
|
+
function emptyCorrectionRules() {
|
|
2400
|
+
return { schemaVersion: 1, rules: [] };
|
|
2401
|
+
}
|
|
2402
|
+
function readCorrectionRules(cwd, relativePath) {
|
|
2403
|
+
const parsed = readJsonFile(cwd, relativePath) ?? emptyCorrectionRules();
|
|
2404
|
+
const result = CorrectionRulesContract.safeParse(parsed);
|
|
2405
|
+
if (!result.success) {
|
|
2406
|
+
throw new Error(`Invalid ${relativePath}: ${formatContractIssues(result.error).join("; ")}`);
|
|
2407
|
+
}
|
|
2408
|
+
return result.data;
|
|
2409
|
+
}
|
|
2410
|
+
function ensureCorrectionFiles(cwd) {
|
|
2411
|
+
ensureStudioDirs(cwd);
|
|
2412
|
+
for (const path of [PROJECT_RULES_JSON, AGENT_RULES_JSON, UPSTREAM_PROPOSALS_JSON]) {
|
|
2413
|
+
if (!readJsonFile(cwd, path)) writeJsonFile(cwd, path, emptyCorrectionRules());
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
function addCorrection(cwd, options) {
|
|
2417
|
+
ensureCorrectionFiles(cwd);
|
|
2418
|
+
const targetPath = fileForScope(options.scope);
|
|
2419
|
+
const rules = readCorrectionRules(cwd, targetPath);
|
|
2420
|
+
const id = options.id ?? safeSlug(options.text).slice(0, 48);
|
|
2421
|
+
const rule = {
|
|
2422
|
+
id,
|
|
2423
|
+
scope: options.scope,
|
|
2424
|
+
status: options.scope === "upstream-proposal" ? "proposed" : "active",
|
|
2425
|
+
text: redactSensitive(options.text),
|
|
2426
|
+
...options.agentId ? { agentId: options.agentId, appliesToAgents: [options.agentId] } : {},
|
|
2427
|
+
...options.sourceSessionId ? { sourceSessionId: options.sourceSessionId } : {},
|
|
2428
|
+
createdAt: nowIso(),
|
|
2429
|
+
reviewedAt: null
|
|
2430
|
+
};
|
|
2431
|
+
const parsed = CorrectionRulesContract.parse({ ...rules, rules: [...rules.rules.filter((item) => item.id !== id), rule] });
|
|
2432
|
+
writeJsonFile(cwd, targetPath, parsed);
|
|
2433
|
+
return rule;
|
|
2434
|
+
}
|
|
2435
|
+
function listCorrections(cwd) {
|
|
2436
|
+
ensureCorrectionFiles(cwd);
|
|
2437
|
+
return {
|
|
2438
|
+
project: readCorrectionRules(cwd, PROJECT_RULES_JSON).rules,
|
|
2439
|
+
agent: readCorrectionRules(cwd, AGENT_RULES_JSON).rules,
|
|
2440
|
+
upstream: readCorrectionRules(cwd, UPSTREAM_PROPOSALS_JSON).rules
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
function applyCorrection(cwd, id) {
|
|
2444
|
+
ensureCorrectionFiles(cwd);
|
|
2445
|
+
for (const path of [PROJECT_RULES_JSON, AGENT_RULES_JSON, UPSTREAM_PROPOSALS_JSON]) {
|
|
2446
|
+
const rules = readCorrectionRules(cwd, path);
|
|
2447
|
+
const index = rules.rules.findIndex((rule) => rule.id === id);
|
|
2448
|
+
if (index === -1) continue;
|
|
2449
|
+
const current = rules.rules[index];
|
|
2450
|
+
if (!current) continue;
|
|
2451
|
+
const { retiredAt: _retiredAt, reason: _reason, ...base } = current;
|
|
2452
|
+
const updated = {
|
|
2453
|
+
...base,
|
|
2454
|
+
status: "active",
|
|
2455
|
+
reviewedAt: nowIso()
|
|
2456
|
+
};
|
|
2457
|
+
rules.rules[index] = updated;
|
|
2458
|
+
writeJsonFile(cwd, path, CorrectionRulesContract.parse(rules));
|
|
2459
|
+
return updated;
|
|
2460
|
+
}
|
|
2461
|
+
throw new Error(`Correction not found: ${id}`);
|
|
2462
|
+
}
|
|
2463
|
+
function retireCorrection(cwd, id, reason) {
|
|
2464
|
+
ensureCorrectionFiles(cwd);
|
|
2465
|
+
for (const path of [PROJECT_RULES_JSON, AGENT_RULES_JSON, UPSTREAM_PROPOSALS_JSON]) {
|
|
2466
|
+
const rules = readCorrectionRules(cwd, path);
|
|
2467
|
+
const index = rules.rules.findIndex((rule) => rule.id === id);
|
|
2468
|
+
if (index === -1) continue;
|
|
2469
|
+
const current = rules.rules[index];
|
|
2470
|
+
if (!current) continue;
|
|
2471
|
+
const updated = {
|
|
2472
|
+
...current,
|
|
2473
|
+
status: "retired",
|
|
2474
|
+
retiredAt: nowIso(),
|
|
2475
|
+
reason
|
|
2476
|
+
};
|
|
2477
|
+
rules.rules[index] = updated;
|
|
2478
|
+
writeJsonFile(cwd, path, CorrectionRulesContract.parse(rules));
|
|
2479
|
+
return updated;
|
|
2480
|
+
}
|
|
2481
|
+
throw new Error(`Correction not found: ${id}`);
|
|
2482
|
+
}
|
|
2483
|
+
function proposeCorrectionUpstream(cwd, id) {
|
|
2484
|
+
const all = listCorrections(cwd);
|
|
2485
|
+
const found = [...all.project, ...all.agent].find((rule) => rule.id === id);
|
|
2486
|
+
if (!found) throw new Error(`Correction not found: ${id}`);
|
|
2487
|
+
return addCorrection(cwd, {
|
|
2488
|
+
scope: "upstream-proposal",
|
|
2489
|
+
text: found.text,
|
|
2490
|
+
...found.agentId ? { agentId: found.agentId } : {},
|
|
2491
|
+
...found.sourceSessionId ? { sourceSessionId: found.sourceSessionId } : {},
|
|
2492
|
+
id: `${found.id}-upstream`
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
// src/studio/context.ts
|
|
2497
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
|
|
2498
|
+
import { join as join12 } from "path";
|
|
2499
|
+
function readPackageJson(cwd) {
|
|
2500
|
+
const path = join12(cwd, "package.json");
|
|
2501
|
+
if (!existsSync12(path)) return null;
|
|
2502
|
+
return JSON.parse(readFileSync10(path, "utf8"));
|
|
2503
|
+
}
|
|
2504
|
+
function detectPackageManager(cwd) {
|
|
2505
|
+
if (existsSync12(join12(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2506
|
+
if (existsSync12(join12(cwd, "yarn.lock"))) return "yarn";
|
|
2507
|
+
if (existsSync12(join12(cwd, "package-lock.json"))) return "npm";
|
|
2508
|
+
if (existsSync12(join12(cwd, "bun.lockb")) || existsSync12(join12(cwd, "bun.lock"))) return "bun";
|
|
2509
|
+
return void 0;
|
|
2510
|
+
}
|
|
2511
|
+
function detectFromDependencies(packageJson, names) {
|
|
2512
|
+
const deps = {
|
|
2513
|
+
...packageJson?.dependencies ?? {},
|
|
2514
|
+
...packageJson?.devDependencies ?? {}
|
|
2515
|
+
};
|
|
2516
|
+
return names.filter((name) => deps[name] !== void 0);
|
|
2517
|
+
}
|
|
2518
|
+
function readEnvExampleKeys(cwd) {
|
|
2519
|
+
const envText = readTextIfExists(join12(cwd, ".env.example")) ?? "";
|
|
2520
|
+
return unique(
|
|
2521
|
+
envText.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && line.includes("=")).map((line) => line.split("=")[0]?.trim() ?? "").filter(Boolean)
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
function inferOpenQuestions(context2) {
|
|
2525
|
+
const missing = [];
|
|
2526
|
+
const questions = [
|
|
2527
|
+
["productSummary", "What does this product do in one concrete paragraph?"],
|
|
2528
|
+
["primaryAudience", "Who is the primary user or buyer?"],
|
|
2529
|
+
["authModel", "What authentication model should agents preserve?"],
|
|
2530
|
+
["tenantModel", "Is this single-user, team, tenant, marketplace, admin, or public content?"]
|
|
2531
|
+
];
|
|
2532
|
+
missing.push(...questions.flatMap(([field, question]) => context2[field].trim() ? [] : [question]));
|
|
2533
|
+
if (context2.primaryWorkflows.length === 0) missing.push("What are the top three user workflows?");
|
|
2534
|
+
if (!context2.uiDirection.preferred.trim()) missing.push("What should the UI feel like, and what should it avoid?");
|
|
2535
|
+
if (!context2.messaging.valueProposition.trim()) missing.push("What value proposition and proof should public copy use?");
|
|
2536
|
+
return unique([...context2.openQuestions, ...missing]);
|
|
2537
|
+
}
|
|
2538
|
+
function scanProjectContext(cwd) {
|
|
2539
|
+
const packageJson = readPackageJson(cwd);
|
|
2540
|
+
const files = listFilesRecursive(cwd);
|
|
2541
|
+
const dependencies = detectFromDependencies(packageJson, [
|
|
2542
|
+
"next",
|
|
2543
|
+
"react",
|
|
2544
|
+
"@supabase/supabase-js",
|
|
2545
|
+
"@supabase/ssr",
|
|
2546
|
+
"tailwindcss",
|
|
2547
|
+
"vitest",
|
|
2548
|
+
"playwright",
|
|
2549
|
+
"@playwright/test",
|
|
2550
|
+
"jest",
|
|
2551
|
+
"storybook"
|
|
2552
|
+
]);
|
|
2553
|
+
const frameworks = dependencies.filter((name) => ["next", "react", "@supabase/supabase-js", "@supabase/ssr"].includes(name));
|
|
2554
|
+
const uiLibraries = dependencies.filter((name) => ["tailwindcss", "storybook"].includes(name));
|
|
2555
|
+
const testTools = dependencies.filter((name) => ["vitest", "playwright", "@playwright/test", "jest", "storybook"].includes(name));
|
|
2556
|
+
const supabaseSignals = files.filter((file) => /(^|\/)(supabase|migrations|seed)\b/.test(file) || file.includes("supabase"));
|
|
2557
|
+
const deployment = files.filter((file) => /(^|\/)(vercel\.json|netlify\.toml|Dockerfile|docker-compose\.yml|\.github\/workflows\/.*\.ya?ml)$/.test(file));
|
|
2558
|
+
const existing = readJsonFile(cwd, CONTEXT_JSON);
|
|
2559
|
+
const now = nowIso();
|
|
2560
|
+
const context2 = {
|
|
2561
|
+
schemaVersion: 1,
|
|
2562
|
+
projectName: existing?.projectName || readPackageName(cwd) || "TBD",
|
|
2563
|
+
productSummary: existing?.productSummary ?? "",
|
|
2564
|
+
productCategory: existing?.productCategory ?? "TBD",
|
|
2565
|
+
primaryAudience: existing?.primaryAudience ?? "",
|
|
2566
|
+
primaryWorkflows: existing?.primaryWorkflows ?? [],
|
|
2567
|
+
businessCriticalBehavior: existing?.businessCriticalBehavior ?? [],
|
|
2568
|
+
architecture: {
|
|
2569
|
+
packageManager: detectPackageManager(cwd) ?? existing?.architecture.packageManager,
|
|
2570
|
+
scripts: unique(Object.keys(packageJson?.scripts ?? {})),
|
|
2571
|
+
frameworks: unique([...existing?.architecture.frameworks ?? [], ...frameworks]),
|
|
2572
|
+
uiLibraries: unique([...existing?.architecture.uiLibraries ?? [], ...uiLibraries]),
|
|
2573
|
+
hasSupabase: Boolean(existing?.architecture.hasSupabase || supabaseSignals.length > 0 || frameworks.some((name) => name.includes("supabase"))),
|
|
2574
|
+
supabaseSignals: unique([...existing?.architecture.supabaseSignals ?? [], ...supabaseSignals.slice(0, 20)]),
|
|
2575
|
+
testTools: unique([...existing?.architecture.testTools ?? [], ...testTools]),
|
|
2576
|
+
envExampleKeys: readEnvExampleKeys(cwd),
|
|
2577
|
+
deployment: unique([...existing?.architecture.deployment ?? [], ...deployment.slice(0, 20)])
|
|
2578
|
+
},
|
|
2579
|
+
dataSensitivity: existing?.dataSensitivity ?? [],
|
|
2580
|
+
authModel: existing?.authModel ?? "",
|
|
2581
|
+
tenantModel: existing?.tenantModel ?? "",
|
|
2582
|
+
integrations: existing?.integrations ?? [],
|
|
2583
|
+
uiDirection: existing?.uiDirection ?? { preferred: "", avoid: "" },
|
|
2584
|
+
messaging: existing?.messaging ?? { valueProposition: "", proof: [], objections: [] },
|
|
2585
|
+
qualityTarget: existing?.qualityTarget ?? "baseline-setup",
|
|
2586
|
+
knownConstraints: existing?.knownConstraints ?? [],
|
|
2587
|
+
openQuestions: existing?.openQuestions ?? [],
|
|
2588
|
+
evidence: uniqueEvidence([
|
|
2589
|
+
...existing?.evidence ?? [],
|
|
2590
|
+
{ source: "agent-kit context scan", note: `Scanned ${files.length} files for package, Supabase, test, env example, and deployment signals.` }
|
|
2591
|
+
]),
|
|
2592
|
+
lastReviewedAt: now,
|
|
2593
|
+
owners: existing?.owners ?? []
|
|
2594
|
+
};
|
|
2595
|
+
context2.openQuestions = inferOpenQuestions(context2);
|
|
2596
|
+
return ProjectContextContract.parse(context2);
|
|
2597
|
+
}
|
|
2598
|
+
function readPackageName(cwd) {
|
|
2599
|
+
const packageJson = readPackageJson(cwd);
|
|
2600
|
+
return typeof packageJson?.name === "string" ? packageJson.name : null;
|
|
2601
|
+
}
|
|
2602
|
+
function uniqueEvidence(items) {
|
|
2603
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2604
|
+
return items.filter((item) => {
|
|
2605
|
+
const key = `${item.source}:${item.note}`;
|
|
2606
|
+
if (seen.has(key)) return false;
|
|
2607
|
+
seen.add(key);
|
|
2608
|
+
return true;
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
function writeProjectContext(cwd, context2) {
|
|
2612
|
+
ensureStudioDirs(cwd);
|
|
2613
|
+
const parsed = ProjectContextContract.parse(context2);
|
|
2614
|
+
writeJsonFile(cwd, CONTEXT_JSON, parsed);
|
|
2615
|
+
const markdown = renderProjectContextMarkdown(parsed);
|
|
2616
|
+
writeTextFile(cwd, CONTEXT_MD, markdown);
|
|
2617
|
+
return { contextPath: CONTEXT_JSON, markdownPath: CONTEXT_MD, openQuestions: parsed.openQuestions };
|
|
2618
|
+
}
|
|
2619
|
+
function initProjectContext(cwd) {
|
|
2620
|
+
return writeProjectContext(cwd, scanProjectContext(cwd));
|
|
2621
|
+
}
|
|
2622
|
+
function validateProjectContext(cwd) {
|
|
2623
|
+
const context2 = readJsonFile(cwd, CONTEXT_JSON);
|
|
2624
|
+
const result = ProjectContextContract.safeParse(context2);
|
|
2625
|
+
if (!result.success) {
|
|
2626
|
+
throw new Error(`Invalid ${CONTEXT_JSON}: ${formatContractIssues(result.error).join("; ")}`);
|
|
2627
|
+
}
|
|
2628
|
+
return { contextPath: CONTEXT_JSON, markdownPath: CONTEXT_MD, openQuestions: result.data.openQuestions };
|
|
2629
|
+
}
|
|
2630
|
+
function renderProjectContext(cwd) {
|
|
2631
|
+
const context2 = readJsonFile(cwd, CONTEXT_JSON);
|
|
2632
|
+
const result = ProjectContextContract.safeParse(context2);
|
|
2633
|
+
if (!result.success) {
|
|
2634
|
+
throw new Error(`Invalid ${CONTEXT_JSON}: ${formatContractIssues(result.error).join("; ")}`);
|
|
2635
|
+
}
|
|
2636
|
+
writeTextFile(cwd, CONTEXT_MD, renderProjectContextMarkdown(result.data));
|
|
2637
|
+
return { contextPath: CONTEXT_JSON, markdownPath: CONTEXT_MD, openQuestions: result.data.openQuestions };
|
|
2638
|
+
}
|
|
2639
|
+
function renderProjectContextMarkdown(context2) {
|
|
2640
|
+
return `# Project Context
|
|
2641
|
+
|
|
2642
|
+
Generated from \`${CONTEXT_JSON}\`.
|
|
2643
|
+
|
|
2644
|
+
## Summary
|
|
2645
|
+
|
|
2646
|
+
- Project: ${redactSensitive(context2.projectName || "TBD")}
|
|
2647
|
+
- Category: ${redactSensitive(context2.productCategory || "TBD")}
|
|
2648
|
+
- Audience: ${redactSensitive(context2.primaryAudience || "TBD")}
|
|
2649
|
+
- Quality target: ${context2.qualityTarget}
|
|
2650
|
+
- Last reviewed: ${context2.lastReviewedAt}
|
|
2651
|
+
|
|
2652
|
+
${redactSensitive(context2.productSummary || "No product summary recorded.")}
|
|
2653
|
+
|
|
2654
|
+
## Primary Workflows
|
|
2655
|
+
|
|
2656
|
+
${listMarkdown(context2.primaryWorkflows)}
|
|
2657
|
+
|
|
2658
|
+
## Architecture Signals
|
|
2659
|
+
|
|
2660
|
+
- Package manager: ${context2.architecture.packageManager ?? "unknown"}
|
|
2661
|
+
- Frameworks: ${context2.architecture.frameworks.join(", ") || "none detected"}
|
|
2662
|
+
- UI libraries: ${context2.architecture.uiLibraries.join(", ") || "none detected"}
|
|
2663
|
+
- Test tools: ${context2.architecture.testTools.join(", ") || "none detected"}
|
|
2664
|
+
- Supabase detected: ${context2.architecture.hasSupabase ? "yes" : "no"}
|
|
2665
|
+
- Env example keys: ${context2.architecture.envExampleKeys.join(", ") || "none detected"}
|
|
2666
|
+
- Deployment files: ${context2.architecture.deployment.join(", ") || "none detected"}
|
|
2667
|
+
|
|
2668
|
+
## Security And Data
|
|
2669
|
+
|
|
2670
|
+
- Auth model: ${redactSensitive(context2.authModel || "TBD")}
|
|
2671
|
+
- Tenant model: ${redactSensitive(context2.tenantModel || "TBD")}
|
|
2672
|
+
|
|
2673
|
+
Data sensitivity:
|
|
2674
|
+
|
|
2675
|
+
${listMarkdown(context2.dataSensitivity)}
|
|
2676
|
+
|
|
2677
|
+
## UI Direction
|
|
2678
|
+
|
|
2679
|
+
- Preferred: ${redactSensitive(context2.uiDirection.preferred || "TBD")}
|
|
2680
|
+
- Avoid: ${redactSensitive(context2.uiDirection.avoid || "TBD")}
|
|
2681
|
+
|
|
2682
|
+
## Messaging
|
|
2683
|
+
|
|
2684
|
+
- Value proposition: ${redactSensitive(context2.messaging.valueProposition || "TBD")}
|
|
2685
|
+
|
|
2686
|
+
Proof:
|
|
2687
|
+
|
|
2688
|
+
${listMarkdown(context2.messaging.proof)}
|
|
2689
|
+
|
|
2690
|
+
Objections:
|
|
2691
|
+
|
|
2692
|
+
${listMarkdown(context2.messaging.objections)}
|
|
2693
|
+
|
|
2694
|
+
## Open Questions
|
|
2695
|
+
|
|
2696
|
+
${listMarkdown(context2.openQuestions)}
|
|
2697
|
+
|
|
2698
|
+
## Evidence
|
|
2699
|
+
|
|
2700
|
+
| Source | Note |
|
|
2701
|
+
| --- | --- |
|
|
2702
|
+
${context2.evidence.map((item) => `| ${redactSensitive(item.source)} | ${redactSensitive(item.note).replace(/\|/g, "\\|")} |`).join("\n")}
|
|
2703
|
+
`;
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
// src/studio/session.ts
|
|
2707
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
2708
|
+
import { join as join13 } from "path";
|
|
2709
|
+
function sessionDir(sessionId) {
|
|
2710
|
+
return `${COUNCIL_SESSIONS_DIR}/${safeSlug(sessionId)}`;
|
|
2711
|
+
}
|
|
2712
|
+
function sessionJsonPath(sessionId) {
|
|
2713
|
+
return `${sessionDir(sessionId)}/session.json`;
|
|
2714
|
+
}
|
|
2715
|
+
function eventsPath(sessionId) {
|
|
2716
|
+
return `${sessionDir(sessionId)}/events.jsonl`;
|
|
2717
|
+
}
|
|
2718
|
+
function indexPath(sessionId) {
|
|
2719
|
+
return `${sessionDir(sessionId)}/index.md`;
|
|
2720
|
+
}
|
|
2721
|
+
function transcriptPath(sessionId) {
|
|
2722
|
+
return `${sessionDir(sessionId)}/transcript.md`;
|
|
2723
|
+
}
|
|
2724
|
+
function readDefaultWorkflowOutputs(cwd, workflowId) {
|
|
2725
|
+
const roster = readJsonFile(cwd, DEFAULT_AGENT_ROSTER_TARGET);
|
|
2726
|
+
const workflow = roster?.workflows?.find((item) => item.id === workflowId);
|
|
2727
|
+
return (workflow?.requiredOutputs ?? ["decision", "risk", "verification evidence"]).map((name) => ({
|
|
2728
|
+
name,
|
|
2729
|
+
status: "missing"
|
|
2730
|
+
}));
|
|
2731
|
+
}
|
|
2732
|
+
function startSession(cwd, options) {
|
|
2733
|
+
ensureStudioDirs(cwd);
|
|
2734
|
+
const datePrefix = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2735
|
+
const sessionId = safeSlug(`${datePrefix}-${options.title}`);
|
|
2736
|
+
const now = nowIso();
|
|
2737
|
+
const workflowId = options.workflowId ?? "planning";
|
|
2738
|
+
const session2 = {
|
|
2739
|
+
schemaVersion: 1,
|
|
2740
|
+
sessionId,
|
|
2741
|
+
title: options.title,
|
|
2742
|
+
createdAt: now,
|
|
2743
|
+
updatedAt: now,
|
|
2744
|
+
status: "in-progress",
|
|
2745
|
+
workflowId,
|
|
2746
|
+
request: options.request ?? options.title,
|
|
2747
|
+
affectedLayers: options.affectedLayers ?? [],
|
|
2748
|
+
activeAgentId: "planner",
|
|
2749
|
+
nextAgentId: void 0,
|
|
2750
|
+
qualityTarget: options.qualityTarget ?? "baseline-setup",
|
|
2751
|
+
requiredOutputs: readDefaultWorkflowOutputs(cwd, workflowId)
|
|
2752
|
+
};
|
|
2753
|
+
writeJsonFile(cwd, sessionJsonPath(sessionId), StudioSessionContract.parse(session2));
|
|
2754
|
+
writeTextFile(cwd, ACTIVE_SESSION_FILE, `${sessionId}
|
|
2755
|
+
`);
|
|
2756
|
+
appendSessionEvent(cwd, sessionId, {
|
|
2757
|
+
type: "session_started",
|
|
2758
|
+
createdAt: now,
|
|
2759
|
+
agentId: "planner",
|
|
2760
|
+
text: `Started ${workflowId} session: ${options.title}`
|
|
2761
|
+
});
|
|
2762
|
+
return { sessionId, sessionPath: sessionDir(sessionId) };
|
|
2763
|
+
}
|
|
2764
|
+
function listSessions(cwd) {
|
|
2765
|
+
const root = join13(cwd, COUNCIL_SESSIONS_DIR);
|
|
2766
|
+
if (!existsSync13(root)) return [];
|
|
2767
|
+
return readdirSync3(root).filter((entry) => entry !== "active").map((entry) => join13(root, entry, "session.json")).filter((path) => existsSync13(path) && statSync2(path).isFile()).map((path) => StudioSessionContract.parse(JSON.parse(readFileSync11(path, "utf8")))).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
2768
|
+
}
|
|
2769
|
+
function getActiveSessionId(cwd) {
|
|
2770
|
+
const active = readTextFile(cwd, ACTIVE_SESSION_FILE)?.trim();
|
|
2771
|
+
if (!active) throw new Error("No active Agent Studio session. Run agent-kit session start first.");
|
|
2772
|
+
return safeSlug(active);
|
|
2773
|
+
}
|
|
2774
|
+
function readSession(cwd, sessionId = getActiveSessionId(cwd)) {
|
|
2775
|
+
const parsed = readJsonFile(cwd, sessionJsonPath(sessionId));
|
|
2776
|
+
const result = StudioSessionContract.safeParse(parsed);
|
|
2777
|
+
if (!result.success) throw new Error(`Invalid ${sessionJsonPath(sessionId)}: ${formatContractIssues(result.error).join("; ")}`);
|
|
2778
|
+
return result.data;
|
|
2779
|
+
}
|
|
2780
|
+
function readSessionEvents(cwd, sessionId = getActiveSessionId(cwd)) {
|
|
2781
|
+
return readJsonLines(cwd, eventsPath(sessionId)).map((event, index) => {
|
|
2782
|
+
const result = SessionEventContract.safeParse(event);
|
|
2783
|
+
if (!result.success) {
|
|
2784
|
+
throw new Error(`Invalid ${eventsPath(sessionId)} line ${index + 1}: ${formatContractIssues(result.error).join("; ")}`);
|
|
2785
|
+
}
|
|
2786
|
+
return result.data;
|
|
2787
|
+
});
|
|
2788
|
+
}
|
|
2789
|
+
function writeSession(cwd, session2) {
|
|
2790
|
+
writeJsonFile(cwd, sessionJsonPath(session2.sessionId), StudioSessionContract.parse(session2));
|
|
2791
|
+
}
|
|
2792
|
+
function appendSessionEvent(cwd, sessionId, event) {
|
|
2793
|
+
const parsed = SessionEventContract.parse(redactEvent(event));
|
|
2794
|
+
appendJsonLine(cwd, eventsPath(sessionId), parsed);
|
|
2795
|
+
const session2 = readSession(cwd, sessionId);
|
|
2796
|
+
const updated = {
|
|
2797
|
+
...session2,
|
|
2798
|
+
updatedAt: parsed.createdAt
|
|
2799
|
+
};
|
|
2800
|
+
if (parsed.agentId) updated.activeAgentId = parsed.agentId;
|
|
2801
|
+
if (parsed.type === "handoff" && parsed.toAgentId) {
|
|
2802
|
+
updated.activeAgentId = parsed.toAgentId;
|
|
2803
|
+
updated.nextAgentId = parsed.toAgentId;
|
|
2804
|
+
}
|
|
2805
|
+
if (parsed.type === "session_status_changed" && parsed.status) updated.status = parsed.status;
|
|
2806
|
+
writeSession(cwd, updated);
|
|
2807
|
+
return parsed;
|
|
2808
|
+
}
|
|
2809
|
+
function redactEvent(event) {
|
|
2810
|
+
return {
|
|
2811
|
+
...event,
|
|
2812
|
+
...event.text ? { text: redactSensitive(event.text) } : {},
|
|
2813
|
+
...event.decision ? { decision: redactSensitive(event.decision) } : {},
|
|
2814
|
+
...event.risk ? { risk: redactSensitive(event.risk) } : {},
|
|
2815
|
+
...event.notes ? { notes: redactSensitive(event.notes) } : {},
|
|
2816
|
+
...event.command ? { command: redactSensitive(event.command) } : {},
|
|
2817
|
+
...event.outputName ? { outputName: redactSensitive(event.outputName) } : {},
|
|
2818
|
+
...event.evidence ? { evidence: event.evidence.map(redactSensitive) } : {}
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
function recordNote(cwd, agentId, text) {
|
|
2822
|
+
return appendSessionEvent(cwd, getActiveSessionId(cwd), { type: "agent_message", createdAt: nowIso(), agentId, text });
|
|
2823
|
+
}
|
|
2824
|
+
function recordDecision(cwd, agentId, decision, risk) {
|
|
2825
|
+
return appendSessionEvent(cwd, getActiveSessionId(cwd), {
|
|
2826
|
+
type: "agent_decision",
|
|
2827
|
+
createdAt: nowIso(),
|
|
2828
|
+
agentId,
|
|
2829
|
+
decision,
|
|
2830
|
+
...risk ? { risk } : {}
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
function recordHandoff(cwd, options) {
|
|
2834
|
+
return appendSessionEvent(cwd, getActiveSessionId(cwd), {
|
|
2835
|
+
type: "handoff",
|
|
2836
|
+
createdAt: nowIso(),
|
|
2837
|
+
fromAgentId: options.fromAgentId,
|
|
2838
|
+
toAgentId: options.toAgentId,
|
|
2839
|
+
decision: options.decision,
|
|
2840
|
+
risk: options.risk,
|
|
2841
|
+
evidence: options.evidence ?? []
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
function recordCorrection(cwd, options) {
|
|
2845
|
+
const sessionId = getActiveSessionId(cwd);
|
|
2846
|
+
const correction2 = options.scope === "session" ? void 0 : addCorrection(cwd, {
|
|
2847
|
+
scope: options.scope,
|
|
2848
|
+
text: options.text,
|
|
2849
|
+
...options.agentId ? { agentId: options.agentId } : {},
|
|
2850
|
+
sourceSessionId: sessionId
|
|
2851
|
+
});
|
|
2852
|
+
return appendSessionEvent(cwd, sessionId, {
|
|
2853
|
+
type: "human_correction",
|
|
2854
|
+
createdAt: nowIso(),
|
|
2855
|
+
...options.agentId ? { agentId: options.agentId } : {},
|
|
2856
|
+
scope: options.scope,
|
|
2857
|
+
text: options.text,
|
|
2858
|
+
...correction2 ? { correctionId: correction2.id } : {}
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
function recordArtifact(cwd, file, note) {
|
|
2862
|
+
const artifactPath = validateRelativeArtifactPath(cwd, file);
|
|
2863
|
+
return appendSessionEvent(cwd, getActiveSessionId(cwd), {
|
|
2864
|
+
type: "artifact_recorded",
|
|
2865
|
+
createdAt: nowIso(),
|
|
2866
|
+
artifactPath,
|
|
2867
|
+
...note ? { notes: note } : {}
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
function recordVerification(cwd, command, result, notes) {
|
|
2871
|
+
return appendSessionEvent(cwd, getActiveSessionId(cwd), {
|
|
2872
|
+
type: "verification_recorded",
|
|
2873
|
+
createdAt: nowIso(),
|
|
2874
|
+
command,
|
|
2875
|
+
result,
|
|
2876
|
+
...notes ? { notes } : {}
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
function recordRequiredOutput(cwd, name, status, evidence) {
|
|
2880
|
+
const trimmedName = name.trim();
|
|
2881
|
+
if (!trimmedName) throw new Error("Required output name is required.");
|
|
2882
|
+
const sessionId = getActiveSessionId(cwd);
|
|
2883
|
+
const session2 = readSession(cwd, sessionId);
|
|
2884
|
+
const now = nowIso();
|
|
2885
|
+
const output = {
|
|
2886
|
+
name: trimmedName,
|
|
2887
|
+
status,
|
|
2888
|
+
...evidence ? { evidence: redactSensitive(evidence) } : {}
|
|
2889
|
+
};
|
|
2890
|
+
const outputIndex = session2.requiredOutputs.findIndex((item) => item.name === trimmedName);
|
|
2891
|
+
const requiredOutputs = outputIndex === -1 ? [...session2.requiredOutputs, output] : session2.requiredOutputs.map((item, index) => index === outputIndex ? { ...item, ...output } : item);
|
|
2892
|
+
writeSession(cwd, { ...session2, requiredOutputs, updatedAt: now });
|
|
2893
|
+
return appendSessionEvent(cwd, sessionId, {
|
|
2894
|
+
type: "required_output_updated",
|
|
2895
|
+
createdAt: now,
|
|
2896
|
+
outputName: trimmedName,
|
|
2897
|
+
outputStatus: status,
|
|
2898
|
+
...evidence ? { evidence: [evidence] } : {}
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
function closeSession(cwd, status) {
|
|
2902
|
+
const sessionId = getActiveSessionId(cwd);
|
|
2903
|
+
appendSessionEvent(cwd, sessionId, {
|
|
2904
|
+
type: "session_status_changed",
|
|
2905
|
+
createdAt: nowIso(),
|
|
2906
|
+
status,
|
|
2907
|
+
text: `Session marked ${status}.`
|
|
2908
|
+
});
|
|
2909
|
+
return readSession(cwd, sessionId);
|
|
2910
|
+
}
|
|
2911
|
+
function renderActiveSession(cwd) {
|
|
2912
|
+
return renderSession(cwd, getActiveSessionId(cwd));
|
|
2913
|
+
}
|
|
2914
|
+
function renderSession(cwd, sessionId) {
|
|
2915
|
+
const session2 = readSession(cwd, sessionId);
|
|
2916
|
+
const events = readSessionEvents(cwd, sessionId);
|
|
2917
|
+
const renderedAt = nowIso();
|
|
2918
|
+
const updated = { ...session2, renderedAt, updatedAt: renderedAt };
|
|
2919
|
+
writeTextFile(cwd, indexPath(sessionId), renderSessionIndex(updated, events));
|
|
2920
|
+
writeTextFile(cwd, transcriptPath(sessionId), renderSessionTranscript(updated, events));
|
|
2921
|
+
writeSession(cwd, updated);
|
|
2922
|
+
return { sessionId, sessionPath: sessionDir(sessionId) };
|
|
2923
|
+
}
|
|
2924
|
+
function renderSessionIndex(session2, events) {
|
|
2925
|
+
const handoffs = events.filter((event) => event.type === "handoff");
|
|
2926
|
+
const decisions = events.filter((event) => event.type === "agent_decision" || event.type === "handoff");
|
|
2927
|
+
const corrections = events.filter((event) => event.type === "human_correction");
|
|
2928
|
+
const artifacts = events.filter((event) => event.type === "artifact_recorded");
|
|
2929
|
+
const verification = events.filter((event) => event.type === "verification_recorded");
|
|
2930
|
+
return `# Council Session: ${escapeMarkdownText(session2.title)}
|
|
2931
|
+
|
|
2932
|
+
Generated from \`${sessionDir(session2.sessionId)}/events.jsonl\` at ${session2.renderedAt ?? session2.updatedAt}.
|
|
2933
|
+
|
|
2934
|
+
## Current State
|
|
2935
|
+
|
|
2936
|
+
- Session: ${session2.sessionId}
|
|
2937
|
+
- Workflow: ${session2.workflowId}
|
|
2938
|
+
- Status: ${session2.status}
|
|
2939
|
+
- Active agent: ${session2.activeAgentId ?? "none"}
|
|
2940
|
+
- Next agent: ${session2.nextAgentId ?? "none"}
|
|
2941
|
+
- Quality target: ${session2.qualityTarget}
|
|
2942
|
+
- Request: ${escapeMarkdownText(session2.request)}
|
|
2943
|
+
|
|
2944
|
+
## Handoff Graph
|
|
2945
|
+
|
|
2946
|
+
\`\`\`mermaid
|
|
2947
|
+
${renderMermaidGraph(handoffs)}
|
|
2948
|
+
\`\`\`
|
|
2949
|
+
|
|
2950
|
+
## Decisions
|
|
2951
|
+
|
|
2952
|
+
| Agent | Decision | Risk | Evidence |
|
|
2953
|
+
| --- | --- | --- | --- |
|
|
2954
|
+
${decisions.map(renderDecisionRow).join("\n") || "| None | None recorded | None recorded | None |"}
|
|
2955
|
+
|
|
2956
|
+
## Human Corrections
|
|
2957
|
+
|
|
2958
|
+
| Scope | Agent | Correction | Durable Rule |
|
|
2959
|
+
| --- | --- | --- | --- |
|
|
2960
|
+
${corrections.map((event) => `| ${event.scope ?? "session"} | ${escapeMarkdownTableCell(event.agentId ?? "all")} | ${escapeMarkdownTableCell(event.text)} | ${event.correctionId ?? "session-only"} |`).join("\n") || "| None | None | None recorded | None |"}
|
|
2961
|
+
|
|
2962
|
+
## Required Outputs
|
|
2963
|
+
|
|
2964
|
+
| Output | Status | Evidence |
|
|
2965
|
+
| --- | --- | --- |
|
|
2966
|
+
${session2.requiredOutputs.map((output) => `| ${escapeMarkdownTableCell(output.name)} | ${output.status} | ${escapeMarkdownTableCell(output.evidence)} |`).join("\n")}
|
|
2967
|
+
|
|
2968
|
+
## Artifacts
|
|
2969
|
+
|
|
2970
|
+
${listMarkdown(artifacts.map((event) => `${event.artifactPath}${event.notes ? ` - ${event.notes}` : ""}`))}
|
|
2971
|
+
|
|
2972
|
+
## Verification
|
|
2973
|
+
|
|
2974
|
+
| Command | Result | Notes |
|
|
2975
|
+
| --- | --- | --- |
|
|
2976
|
+
${verification.map((event) => `| ${escapeMarkdownTableCell(event.command)} | ${event.result ?? "skipped"} | ${escapeMarkdownTableCell(event.notes)} |`).join("\n") || "| None recorded | skipped | Add verification before completion |"}
|
|
2977
|
+
|
|
2978
|
+
## Next Actions
|
|
2979
|
+
|
|
2980
|
+
${renderNextActions(session2, verification)}
|
|
2981
|
+
`;
|
|
2982
|
+
}
|
|
2983
|
+
function renderDecisionRow(event) {
|
|
2984
|
+
if (event.type === "handoff") {
|
|
2985
|
+
return `| ${escapeMarkdownTableCell(`${event.fromAgentId ?? "unknown"} -> ${event.toAgentId ?? "unknown"}`)} | ${escapeMarkdownTableCell(event.decision)} | ${escapeMarkdownTableCell(event.risk)} | ${escapeMarkdownTableCell(event.evidence?.join(", "))} |`;
|
|
2986
|
+
}
|
|
2987
|
+
return `| ${escapeMarkdownTableCell(event.agentId ?? "unknown")} | ${escapeMarkdownTableCell(event.decision)} | ${escapeMarkdownTableCell(event.risk)} | ${escapeMarkdownTableCell(event.evidence?.join(", "))} |`;
|
|
2988
|
+
}
|
|
2989
|
+
function renderMermaidGraph(handoffs) {
|
|
2990
|
+
if (handoffs.length === 0) return 'flowchart LR\n session["Session"]';
|
|
2991
|
+
const lines = ["flowchart LR"];
|
|
2992
|
+
for (const handoff of handoffs) {
|
|
2993
|
+
const from = safeNodeId(handoff.fromAgentId ?? "unknown");
|
|
2994
|
+
const to = safeNodeId(handoff.toAgentId ?? "unknown");
|
|
2995
|
+
lines.push(` ${from}["${safeMermaidLabel(handoff.fromAgentId ?? "unknown")}"] --> ${to}["${safeMermaidLabel(handoff.toAgentId ?? "unknown")}"]`);
|
|
2996
|
+
}
|
|
2997
|
+
return lines.join("\n");
|
|
2998
|
+
}
|
|
2999
|
+
function safeNodeId(value) {
|
|
3000
|
+
const id = value.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
3001
|
+
return id || "unknown";
|
|
3002
|
+
}
|
|
3003
|
+
function safeMermaidLabel(value) {
|
|
3004
|
+
const label = redactSensitive(value).replace(/[^a-zA-Z0-9 _./:-]/g, " ").replace(/\s+/g, " ").trim();
|
|
3005
|
+
return label || "unknown";
|
|
3006
|
+
}
|
|
3007
|
+
function renderNextActions(session2, verification) {
|
|
3008
|
+
const missingOutputs = session2.requiredOutputs.filter((output) => output.status === "missing" || output.status === "partial");
|
|
3009
|
+
const actions = [
|
|
3010
|
+
...missingOutputs.map((output) => `Complete required output: ${output.name}.`),
|
|
3011
|
+
...verification.length === 0 ? ["Record verification evidence before closing the session."] : [],
|
|
3012
|
+
...session2.nextAgentId ? [`Continue with ${session2.nextAgentId}.`] : []
|
|
3013
|
+
];
|
|
3014
|
+
return listMarkdown(actions);
|
|
3015
|
+
}
|
|
3016
|
+
function renderSessionTranscript(session2, events) {
|
|
3017
|
+
const byAgent = /* @__PURE__ */ new Map();
|
|
3018
|
+
for (const event of events) {
|
|
3019
|
+
const key = event.agentId ?? event.fromAgentId ?? "session";
|
|
3020
|
+
byAgent.set(key, [...byAgent.get(key) ?? [], event]);
|
|
3021
|
+
}
|
|
3022
|
+
const sections = [...byAgent.entries()].map(([agentId, agentEvents]) => {
|
|
3023
|
+
const rows = agentEvents.map((event) => {
|
|
3024
|
+
const detail = event.text ?? event.decision ?? event.command ?? event.artifactPath ?? (event.outputName ? `${event.outputName}: ${event.outputStatus ?? ""}` : void 0) ?? event.status ?? "";
|
|
3025
|
+
return `- ${event.createdAt} \`${event.type}\`: ${escapeMarkdownText(detail)}`;
|
|
3026
|
+
});
|
|
3027
|
+
return `## ${escapeMarkdownText(agentId)}
|
|
3028
|
+
|
|
3029
|
+
${rows.join("\n")}`;
|
|
3030
|
+
}).join("\n\n");
|
|
3031
|
+
return `# Transcript: ${escapeMarkdownText(session2.title)}
|
|
3032
|
+
|
|
3033
|
+
Generated from \`${sessionDir(session2.sessionId)}/events.jsonl\`.
|
|
3034
|
+
|
|
3035
|
+
${sections || "No events recorded."}
|
|
3036
|
+
`;
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
// src/studio/export.ts
|
|
3040
|
+
function exportStaticStudio(cwd) {
|
|
3041
|
+
ensureStudioDirs(cwd);
|
|
3042
|
+
const exportedAt = nowIso();
|
|
3043
|
+
const context2 = readProjectContext(cwd);
|
|
3044
|
+
const contextMarkdown = readTextFile(cwd, CONTEXT_MD);
|
|
3045
|
+
const corrections = listCorrections(cwd);
|
|
3046
|
+
const sessions = listSessions(cwd).map((session2) => {
|
|
3047
|
+
const events = readSessionEvents(cwd, session2.sessionId);
|
|
3048
|
+
return {
|
|
3049
|
+
session: session2,
|
|
3050
|
+
events,
|
|
3051
|
+
indexPath: `${COUNCIL_SESSIONS_DIR}/${safeSlug(session2.sessionId)}/index.md`,
|
|
3052
|
+
transcriptPath: `${COUNCIL_SESSIONS_DIR}/${safeSlug(session2.sessionId)}/transcript.md`,
|
|
3053
|
+
agents: unique(events.flatMap((event) => [event.agentId, event.fromAgentId, event.toAgentId].filter(Boolean)))
|
|
3054
|
+
};
|
|
3055
|
+
});
|
|
3056
|
+
const data = {
|
|
3057
|
+
generatedAt: exportedAt,
|
|
3058
|
+
context: context2,
|
|
3059
|
+
contextMarkdown,
|
|
3060
|
+
corrections,
|
|
3061
|
+
activeSessionId: readTextFile(cwd, ACTIVE_SESSION_FILE)?.trim() ?? null,
|
|
3062
|
+
sessions
|
|
3063
|
+
};
|
|
3064
|
+
const redactedData = redactDeep(data);
|
|
3065
|
+
const html = renderStaticStudioHtml(redactedData);
|
|
3066
|
+
if (containsLikelySecret(html)) {
|
|
3067
|
+
throw new Error("Refusing to write static Agent Studio export because the rendered HTML contains a secret-like value.");
|
|
3068
|
+
}
|
|
3069
|
+
writeTextFile(cwd, STUDIO_EXPORT_HTML, html);
|
|
3070
|
+
return { studioPath: STUDIO_EXPORT_HTML, sessionCount: sessions.length, exportedAt };
|
|
3071
|
+
}
|
|
3072
|
+
function readProjectContext(cwd) {
|
|
3073
|
+
const raw = readJsonFile(cwd, CONTEXT_JSON);
|
|
3074
|
+
if (!raw) return null;
|
|
3075
|
+
return ProjectContextContract.parse(raw);
|
|
3076
|
+
}
|
|
3077
|
+
function redactDeep(value) {
|
|
3078
|
+
if (typeof value === "string") return redactSensitive(value);
|
|
3079
|
+
if (Array.isArray(value)) return value.map(redactDeep);
|
|
3080
|
+
if (value && typeof value === "object") {
|
|
3081
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactDeep(item)]));
|
|
3082
|
+
}
|
|
3083
|
+
return value;
|
|
3084
|
+
}
|
|
3085
|
+
function renderStaticStudioHtml(data) {
|
|
3086
|
+
const sessionCount = data.sessions.length;
|
|
3087
|
+
const correctionCount = data.corrections.project.length + data.corrections.agent.length;
|
|
3088
|
+
return `<!doctype html>
|
|
3089
|
+
<html lang="en">
|
|
3090
|
+
<head>
|
|
3091
|
+
<meta charset="utf-8">
|
|
3092
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3093
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'">
|
|
3094
|
+
<title>Agent Studio</title>
|
|
3095
|
+
<style>
|
|
3096
|
+
:root { color-scheme: light; --ink: #1f2933; --muted: #52606d; --line: #d9e2ec; --panel: #ffffff; --bg: #f5f7fa; --accent: #0f766e; --accent-2: #7c3aed; --warn: #b45309; }
|
|
3097
|
+
* { box-sizing: border-box; }
|
|
3098
|
+
body { margin: 0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--ink); background: var(--bg); line-height: 1.5; }
|
|
3099
|
+
header { padding: 28px clamp(16px, 4vw, 48px) 20px; background: #0b1f24; color: #f8fafc; }
|
|
3100
|
+
header h1 { margin: 0 0 8px; font-size: clamp(28px, 4vw, 44px); letter-spacing: 0; }
|
|
3101
|
+
header p { margin: 0; color: #cbd5e1; max-width: 980px; }
|
|
3102
|
+
main { display: grid; grid-template-columns: minmax(0, 1fr); gap: 18px; padding: 20px clamp(16px, 4vw, 48px) 48px; }
|
|
3103
|
+
section, details { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; }
|
|
3104
|
+
section { padding: 18px; }
|
|
3105
|
+
h2 { margin: 0 0 12px; font-size: 22px; letter-spacing: 0; }
|
|
3106
|
+
h3 { margin: 18px 0 10px; font-size: 17px; letter-spacing: 0; }
|
|
3107
|
+
.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-top: 18px; }
|
|
3108
|
+
.metric { border: 1px solid rgba(255,255,255,.22); border-radius: 8px; padding: 12px; background: rgba(255,255,255,.08); }
|
|
3109
|
+
.metric strong { display: block; font-size: 24px; }
|
|
3110
|
+
.metric span { color: #cbd5e1; font-size: 13px; }
|
|
3111
|
+
.grid { display: grid; grid-template-columns: minmax(0, 340px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3112
|
+
.stack { display: grid; gap: 12px; }
|
|
3113
|
+
.muted { color: var(--muted); }
|
|
3114
|
+
.path { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; color: var(--muted); overflow-wrap: anywhere; }
|
|
3115
|
+
.pill { display: inline-flex; align-items: center; border-radius: 999px; padding: 2px 9px; border: 1px solid var(--line); background: #f8fafc; font-size: 12px; margin: 2px 4px 2px 0; }
|
|
3116
|
+
.session-card { padding: 0; overflow: hidden; }
|
|
3117
|
+
.session-head { padding: 16px 18px; border-bottom: 1px solid var(--line); display: flex; gap: 10px; flex-wrap: wrap; align-items: baseline; justify-content: space-between; }
|
|
3118
|
+
.session-head h2 { margin: 0; }
|
|
3119
|
+
.session-body { padding: 18px; display: grid; gap: 18px; }
|
|
3120
|
+
.graph-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: 8px; background: #fbfdff; padding: 8px; }
|
|
3121
|
+
svg.agent-graph { width: 100%; min-width: 520px; max-height: 240px; }
|
|
3122
|
+
.event-list { margin: 0; padding-left: 20px; }
|
|
3123
|
+
.event-list li { margin: 7px 0; }
|
|
3124
|
+
details.agent-stream { padding: 0; }
|
|
3125
|
+
details.agent-stream > summary { cursor: pointer; padding: 12px 14px; font-weight: 700; }
|
|
3126
|
+
details.agent-stream[open] > summary { border-bottom: 1px solid var(--line); }
|
|
3127
|
+
details.agent-stream .stream-body { padding: 12px 14px; }
|
|
3128
|
+
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
3129
|
+
th, td { text-align: left; border-bottom: 1px solid var(--line); padding: 8px; vertical-align: top; }
|
|
3130
|
+
th { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
|
|
3131
|
+
code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; background: #edf2f7; padding: 1px 4px; border-radius: 4px; }
|
|
3132
|
+
@media (max-width: 860px) { .grid { grid-template-columns: 1fr; } header { padding-top: 22px; } }
|
|
3133
|
+
</style>
|
|
3134
|
+
</head>
|
|
3135
|
+
<body>
|
|
3136
|
+
<header>
|
|
3137
|
+
<h1>Agent Studio</h1>
|
|
3138
|
+
<p>Static local export generated from Agent Kit JSON and JSONL files. It embeds a redacted snapshot at export time and does not connect to a server, database, or model API.</p>
|
|
3139
|
+
<div class="metrics">
|
|
3140
|
+
<div class="metric"><strong>${sessionCount}</strong><span>sessions</span></div>
|
|
3141
|
+
<div class="metric"><strong>${correctionCount}</strong><span>active correction files</span></div>
|
|
3142
|
+
<div class="metric"><strong>${escapeHtml(data.generatedAt)}</strong><span>generated at</span></div>
|
|
3143
|
+
</div>
|
|
3144
|
+
</header>
|
|
3145
|
+
<main>
|
|
3146
|
+
<section>
|
|
3147
|
+
<h2>Project Context</h2>
|
|
3148
|
+
${renderContextSummary(data)}
|
|
3149
|
+
</section>
|
|
3150
|
+
<div class="grid">
|
|
3151
|
+
<aside class="stack">
|
|
3152
|
+
<section>
|
|
3153
|
+
<h2>Corrections</h2>
|
|
3154
|
+
${renderCorrections(data)}
|
|
3155
|
+
</section>
|
|
3156
|
+
<section>
|
|
3157
|
+
<h2>Session Index</h2>
|
|
3158
|
+
${renderSessionIndexLinks(data.sessions)}
|
|
3159
|
+
</section>
|
|
3160
|
+
</aside>
|
|
3161
|
+
<div class="stack">
|
|
3162
|
+
${data.sessions.map(renderSessionSection).join("\n")}
|
|
3163
|
+
</div>
|
|
3164
|
+
</div>
|
|
3165
|
+
</main>
|
|
3166
|
+
<script type="application/json" id="agent-studio-data">${safeJsonForHtml(data)}</script>
|
|
3167
|
+
</body>
|
|
3168
|
+
</html>
|
|
3169
|
+
`;
|
|
3170
|
+
}
|
|
3171
|
+
function renderContextSummary(data) {
|
|
3172
|
+
const context2 = data.context;
|
|
3173
|
+
if (!context2) return `<p class="muted">No project context found. Run <code>agent-kit onboard</code> or <code>agent-kit init --guided</code>.</p>`;
|
|
3174
|
+
return `<div class="stack">
|
|
3175
|
+
<p><strong>${escapeHtml(context2.projectName || "TBD")}</strong> ${escapeHtml(context2.productSummary || "No product summary recorded.")}</p>
|
|
3176
|
+
<p class="muted">Audience: ${escapeHtml(context2.primaryAudience || "TBD")} | Quality target: ${escapeHtml(context2.qualityTarget || "baseline-setup")}</p>
|
|
3177
|
+
<p>${renderPills([...context2.architecture?.frameworks ?? [], ...context2.architecture?.testTools ?? [], context2.architecture?.hasSupabase ? "supabase" : ""].filter(Boolean))}</p>
|
|
3178
|
+
${context2.openQuestions?.length ? `<h3>Open Questions</h3>${renderList(context2.openQuestions)}` : ""}
|
|
3179
|
+
<p class="path">Source: ${CONTEXT_JSON}${data.contextMarkdown ? ` and ${CONTEXT_MD}` : ""}</p>
|
|
3180
|
+
</div>`;
|
|
3181
|
+
}
|
|
3182
|
+
function renderCorrections(data) {
|
|
3183
|
+
const rules = [...data.corrections.project, ...data.corrections.agent].filter((rule) => rule.status === "active");
|
|
3184
|
+
if (rules.length === 0) return `<p class="muted">No active durable corrections recorded.</p>`;
|
|
3185
|
+
return `<table>
|
|
3186
|
+
<thead><tr><th>Scope</th><th>Agent</th><th>Correction</th></tr></thead>
|
|
3187
|
+
<tbody>
|
|
3188
|
+
${rules.map((rule) => `<tr><td>${escapeHtml(rule.scope)}</td><td>${escapeHtml(rule.agentId ?? rule.appliesToAgents?.join(", ") ?? "all")}</td><td>${escapeHtml(rule.text)}</td></tr>`).join("\n")}
|
|
3189
|
+
</tbody>
|
|
3190
|
+
</table>`;
|
|
3191
|
+
}
|
|
3192
|
+
function renderSessionIndexLinks(sessions) {
|
|
3193
|
+
if (sessions.length === 0) return `<p class="muted">No sessions recorded yet.</p>`;
|
|
3194
|
+
return `<ol class="event-list">${sessions.map((item) => `<li><a href="#${htmlId(item.session.sessionId)}">${escapeHtml(item.session.title)}</a><br><span class="muted">${escapeHtml(item.session.status)} | ${escapeHtml(item.session.workflowId)}</span></li>`).join("")}</ol>`;
|
|
3195
|
+
}
|
|
3196
|
+
function renderSessionSection(item) {
|
|
3197
|
+
const verification = item.events.filter((event) => event.type === "verification_recorded");
|
|
3198
|
+
const corrections = item.events.filter((event) => event.type === "human_correction");
|
|
3199
|
+
const artifacts = item.events.filter((event) => event.type === "artifact_recorded");
|
|
3200
|
+
return `<section class="session-card" id="${htmlId(item.session.sessionId)}">
|
|
3201
|
+
<div class="session-head">
|
|
3202
|
+
<h2>${escapeHtml(item.session.title)}</h2>
|
|
3203
|
+
<div>
|
|
3204
|
+
<span class="pill">${escapeHtml(item.session.status)}</span>
|
|
3205
|
+
<span class="pill">${escapeHtml(item.session.workflowId)}</span>
|
|
3206
|
+
<span class="pill">${item.events.length} events</span>
|
|
3207
|
+
</div>
|
|
3208
|
+
</div>
|
|
3209
|
+
<div class="session-body">
|
|
3210
|
+
<p class="muted">Request: ${escapeHtml(item.session.request)}</p>
|
|
3211
|
+
<p class="path">Markdown: ${escapeHtml(item.indexPath)} | ${escapeHtml(item.transcriptPath)}</p>
|
|
3212
|
+
<div class="graph-wrap">${renderSvgGraph(item.events, item.agents)}</div>
|
|
3213
|
+
<div>
|
|
3214
|
+
<h3>Agent Streams</h3>
|
|
3215
|
+
${renderAgentStreams(item)}
|
|
3216
|
+
</div>
|
|
3217
|
+
<div class="grid">
|
|
3218
|
+
<div>
|
|
3219
|
+
<h3>Verification</h3>
|
|
3220
|
+
${verification.length ? renderEventList(verification) : `<p class="muted">No verification recorded.</p>`}
|
|
3221
|
+
</div>
|
|
3222
|
+
<div>
|
|
3223
|
+
<h3>Corrections And Artifacts</h3>
|
|
3224
|
+
${corrections.length || artifacts.length ? renderEventList([...corrections, ...artifacts]) : `<p class="muted">No corrections or artifacts recorded.</p>`}
|
|
3225
|
+
</div>
|
|
3226
|
+
</div>
|
|
3227
|
+
</div>
|
|
3228
|
+
</section>`;
|
|
3229
|
+
}
|
|
3230
|
+
function renderAgentStreams(item) {
|
|
3231
|
+
if (item.events.length === 0) return `<p class="muted">No events recorded.</p>`;
|
|
3232
|
+
const byAgent = /* @__PURE__ */ new Map();
|
|
3233
|
+
for (const event of item.events) {
|
|
3234
|
+
const agent = event.agentId ?? event.fromAgentId ?? "session";
|
|
3235
|
+
byAgent.set(agent, [...byAgent.get(agent) ?? [], event]);
|
|
3236
|
+
}
|
|
3237
|
+
return [...byAgent.entries()].map(([agent, events]) => `<details class="agent-stream"><summary>${escapeHtml(agent)} (${events.length})</summary><div class="stream-body">${renderEventList(events)}</div></details>`).join("\n");
|
|
3238
|
+
}
|
|
3239
|
+
function renderEventList(events) {
|
|
3240
|
+
return `<ol class="event-list">${events.map((event) => `<li><time>${escapeHtml(event.createdAt)}</time> <code>${escapeHtml(event.type)}</code>: ${escapeHtml(eventDetail(event))}</li>`).join("\n")}</ol>`;
|
|
3241
|
+
}
|
|
3242
|
+
function eventDetail(event) {
|
|
3243
|
+
if (event.type === "handoff") return `${event.fromAgentId ?? "unknown"} -> ${event.toAgentId ?? "unknown"}: ${event.decision ?? ""} Risk: ${event.risk ?? ""}`;
|
|
3244
|
+
if (event.type === "required_output_updated") return `${event.outputName ?? "output"}: ${event.outputStatus ?? "unknown"}`;
|
|
3245
|
+
return event.text ?? event.decision ?? event.command ?? event.artifactPath ?? event.status ?? "";
|
|
3246
|
+
}
|
|
3247
|
+
function renderSvgGraph(events, knownAgents) {
|
|
3248
|
+
const handoffs = events.filter((event) => event.type === "handoff");
|
|
3249
|
+
const agents = unique([...knownAgents, ...handoffs.flatMap((event) => [event.fromAgentId, event.toAgentId].filter(Boolean))]);
|
|
3250
|
+
if (agents.length === 0) {
|
|
3251
|
+
return `<svg class="agent-graph" viewBox="0 0 520 120" role="img" aria-label="No agent handoffs recorded"><text x="24" y="62" fill="#52606d">No handoffs recorded yet.</text></svg>`;
|
|
3252
|
+
}
|
|
3253
|
+
const width = Math.max(520, agents.length * 180);
|
|
3254
|
+
const y = 78;
|
|
3255
|
+
const positions = new Map(agents.map((agent, index) => [agent, { x: 74 + index * 170, y }]));
|
|
3256
|
+
const edges = handoffs.map((event) => {
|
|
3257
|
+
const from = positions.get(event.fromAgentId ?? "");
|
|
3258
|
+
const to = positions.get(event.toAgentId ?? "");
|
|
3259
|
+
if (!from || !to) return "";
|
|
3260
|
+
return `<line x1="${from.x + 34}" y1="${from.y}" x2="${to.x - 34}" y2="${to.y}" stroke="#7c3aed" stroke-width="2" marker-end="url(#arrow)" />`;
|
|
3261
|
+
}).join("\n");
|
|
3262
|
+
const nodes = agents.map((agent) => {
|
|
3263
|
+
const position = positions.get(agent);
|
|
3264
|
+
if (!position) return "";
|
|
3265
|
+
return `<g><circle cx="${position.x}" cy="${position.y}" r="34" fill="#ccfbf1" stroke="#0f766e" stroke-width="2" /><text x="${position.x}" y="${position.y + 56}" text-anchor="middle" fill="#1f2933" font-size="12">${escapeSvgText(agent)}</text></g>`;
|
|
3266
|
+
}).join("\n");
|
|
3267
|
+
return `<svg class="agent-graph" viewBox="0 0 ${width} 150" role="img" aria-label="Agent handoff graph">
|
|
3268
|
+
<defs><marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z" fill="#7c3aed" /></marker></defs>
|
|
3269
|
+
${edges}
|
|
3270
|
+
${nodes}
|
|
3271
|
+
</svg>`;
|
|
3272
|
+
}
|
|
3273
|
+
function renderPills(values) {
|
|
3274
|
+
const cleaned = values.filter(Boolean);
|
|
3275
|
+
return cleaned.length ? cleaned.map((value) => `<span class="pill">${escapeHtml(value)}</span>`).join("") : `<span class="muted">No stack signals recorded.</span>`;
|
|
3276
|
+
}
|
|
3277
|
+
function renderList(values) {
|
|
3278
|
+
return `<ul>${values.map((value) => `<li>${escapeHtml(value)}</li>`).join("")}</ul>`;
|
|
3279
|
+
}
|
|
3280
|
+
function htmlId(value) {
|
|
3281
|
+
return `session-${safeSlug(value)}`;
|
|
3282
|
+
}
|
|
3283
|
+
function escapeHtml(value) {
|
|
3284
|
+
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3285
|
+
}
|
|
3286
|
+
function escapeSvgText(value) {
|
|
3287
|
+
return escapeHtml(value).slice(0, 42);
|
|
3288
|
+
}
|
|
3289
|
+
function safeJsonForHtml(value) {
|
|
3290
|
+
return JSON.stringify(value).replace(/&/g, "\\u0026").replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
// src/cli/index.ts
|
|
3294
|
+
var program = new Command();
|
|
3295
|
+
var requiredOutputStatuses = ["missing", "partial", "complete", "not-applicable"];
|
|
3296
|
+
function isRequiredOutputStatus(value) {
|
|
3297
|
+
return requiredOutputStatuses.includes(value);
|
|
3298
|
+
}
|
|
3299
|
+
program.name("agent-kit").description("Next.js + Supabase agent, skill, docs, design, and research kit.").version("0.1.0");
|
|
3300
|
+
program.command("init").description("Install agent-kit docs and library files into a project.").option("--stack <stack>", "Stack profile to install.", "next-supabase").option("--force", "Overwrite existing docs instead of writing conflicts.").option("--guided", "Also create local project context files from a non-interactive scan.").action((options) => {
|
|
3301
|
+
const result = initProject({
|
|
3302
|
+
cwd: process.cwd(),
|
|
3303
|
+
stack: options.stack,
|
|
3304
|
+
force: Boolean(options.force)
|
|
3305
|
+
});
|
|
3306
|
+
if (options.guided) {
|
|
3307
|
+
console.log(JSON.stringify({ install: result, context: initProjectContext(process.cwd()) }, null, 2));
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3311
|
+
});
|
|
3312
|
+
program.command("audit").description("Audit an existing project for agent-kit coverage gaps.").option("--json", "Print machine-readable JSON output.").option("--min-readiness <level>", `Exit non-zero unless readiness is at least this level: ${READINESS_ORDER.join(", ")}.`).action((options) => {
|
|
3313
|
+
const report = createAuditReport(process.cwd());
|
|
3314
|
+
let minimumReadiness;
|
|
3315
|
+
if (options.minReadiness) {
|
|
3316
|
+
if (!isAuditReadinessLevel(options.minReadiness)) {
|
|
3317
|
+
console.error(`Invalid --min-readiness value "${options.minReadiness}". Expected one of: ${READINESS_ORDER.join(", ")}.`);
|
|
3318
|
+
process.exitCode = 1;
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
minimumReadiness = options.minReadiness;
|
|
3322
|
+
}
|
|
3323
|
+
if (options.json) {
|
|
3324
|
+
console.log(JSON.stringify(report, null, 2));
|
|
3325
|
+
} else {
|
|
3326
|
+
console.log(`READINESS ${report.readiness.level}: ${report.readiness.summary}`);
|
|
3327
|
+
console.log(`SUMMARY pass=${report.summary.pass} warn=${report.summary.warn} fail=${report.summary.fail}`);
|
|
3328
|
+
if (report.readiness.nextActions.length > 0) {
|
|
3329
|
+
console.log("NEXT ACTIONS");
|
|
3330
|
+
for (const action of report.readiness.nextActions) console.log(`- ${action}`);
|
|
3331
|
+
}
|
|
3332
|
+
console.log("");
|
|
3333
|
+
for (const finding of report.findings) {
|
|
3334
|
+
const prefix = finding.level.toUpperCase().padEnd(4);
|
|
3335
|
+
console.log(`${prefix} ${finding.area}: ${finding.message}`);
|
|
3336
|
+
if (finding.remediation) console.log(` remediation: ${finding.remediation}`);
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
if (report.summary.fail > 0) {
|
|
3340
|
+
process.exitCode = 1;
|
|
3341
|
+
}
|
|
3342
|
+
if (minimumReadiness && !meetsMinimumReadiness(report.readiness.level, minimumReadiness)) {
|
|
3343
|
+
console.error(`Audit readiness ${report.readiness.level} is below required minimum ${minimumReadiness}.`);
|
|
3344
|
+
process.exitCode = 1;
|
|
3345
|
+
}
|
|
3346
|
+
});
|
|
3347
|
+
program.command("diff").description("Compare project docs against bundled templates.").action(() => {
|
|
3348
|
+
console.log(JSON.stringify(diffProject(process.cwd()), null, 2));
|
|
3349
|
+
});
|
|
3350
|
+
program.command("update").description("Update installed templates, preserving local conflicts by default.").option("--force", "Overwrite local docs.").action((options) => {
|
|
3351
|
+
const result = initProject({ cwd: process.cwd(), force: Boolean(options.force) });
|
|
3352
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3353
|
+
});
|
|
3354
|
+
var addCommand = program.command("add").description("Add one agent-kit asset.");
|
|
3355
|
+
addCommand.command("skill <name>").description("Add a single skill into .agent-kit/skills.").option("--force", "Overwrite existing skill.").action((name, options) => {
|
|
3356
|
+
console.log(addSkill(process.cwd(), name, { force: Boolean(options.force) }));
|
|
3357
|
+
});
|
|
3358
|
+
program.command("doctor").description("Validate local CLI runtime prerequisites.").action(() => {
|
|
3359
|
+
console.log("agent-kit doctor");
|
|
3360
|
+
console.log(`node: ${process.version}`);
|
|
3361
|
+
console.log(`available skills: ${listSkills().length}`);
|
|
3362
|
+
console.log("status: ok");
|
|
3363
|
+
});
|
|
3364
|
+
program.command("onboard").description("Create or refresh local project context files for installed agents.").option("--refresh", "Refresh inferred context from the current project state.").action(() => {
|
|
3365
|
+
console.log(JSON.stringify(initProjectContext(process.cwd()), null, 2));
|
|
3366
|
+
});
|
|
3367
|
+
var context = program.command("context").description("Manage local project context for Agent Studio.");
|
|
3368
|
+
context.command("init").description("Create project context from a local scan.").action(() => {
|
|
3369
|
+
console.log(JSON.stringify(initProjectContext(process.cwd()), null, 2));
|
|
3370
|
+
});
|
|
3371
|
+
context.command("scan").description("Print inferred project context without writing it.").action(() => {
|
|
3372
|
+
console.log(JSON.stringify(scanProjectContext(process.cwd()), null, 2));
|
|
3373
|
+
});
|
|
3374
|
+
context.command("ask").description("Print unanswered high-value project context questions.").action(() => {
|
|
3375
|
+
const result = initProjectContext(process.cwd());
|
|
3376
|
+
for (const question of result.openQuestions) console.log(`- ${question}`);
|
|
3377
|
+
});
|
|
3378
|
+
context.command("render").description("Render .agent-kit/project-context.md from project-context.json.").action(() => {
|
|
3379
|
+
console.log(JSON.stringify(renderProjectContext(process.cwd()), null, 2));
|
|
3380
|
+
});
|
|
3381
|
+
context.command("validate").description("Validate .agent-kit/project-context.json.").action(() => {
|
|
3382
|
+
console.log(JSON.stringify(validateProjectContext(process.cwd()), null, 2));
|
|
3383
|
+
});
|
|
3384
|
+
context.command("show").description("Print rendered project context markdown.").action(() => {
|
|
3385
|
+
console.log(readTextFile(process.cwd(), ".agent-kit/project-context.md") ?? "");
|
|
3386
|
+
});
|
|
3387
|
+
var session = program.command("session").description("Record and render local Agent Studio council sessions.");
|
|
3388
|
+
session.command("start <title...>").description("Start a local council session.").option("--workflow <workflow>", "Workflow id.", "planning").option("--request <request>", "Original user request.").action((titleParts, options) => {
|
|
3389
|
+
const title = titleParts.join(" ");
|
|
3390
|
+
console.log(
|
|
3391
|
+
JSON.stringify(
|
|
3392
|
+
startSession(process.cwd(), {
|
|
3393
|
+
title,
|
|
3394
|
+
workflowId: options.workflow,
|
|
3395
|
+
...options.request ? { request: options.request } : {}
|
|
3396
|
+
}),
|
|
3397
|
+
null,
|
|
3398
|
+
2
|
|
3399
|
+
)
|
|
3400
|
+
);
|
|
3401
|
+
});
|
|
3402
|
+
session.command("list").description("List local council sessions.").action(() => {
|
|
3403
|
+
console.log(JSON.stringify(listSessions(process.cwd()), null, 2));
|
|
3404
|
+
});
|
|
3405
|
+
session.command("active").description("Print the active council session id.").action(() => {
|
|
3406
|
+
console.log(getActiveSessionId(process.cwd()));
|
|
3407
|
+
});
|
|
3408
|
+
session.command("note <text...>").description("Record a visible agent message.").requiredOption("--agent <agent>", "Agent id.").action((textParts, options) => {
|
|
3409
|
+
console.log(JSON.stringify(recordNote(process.cwd(), options.agent, textParts.join(" ")), null, 2));
|
|
3410
|
+
});
|
|
3411
|
+
session.command("decision <text...>").description("Record an agent decision.").requiredOption("--agent <agent>", "Agent id.").option("--risk <risk>", "Risk associated with the decision.").action((textParts, options) => {
|
|
3412
|
+
console.log(JSON.stringify(recordDecision(process.cwd(), options.agent, textParts.join(" "), options.risk), null, 2));
|
|
3413
|
+
});
|
|
3414
|
+
session.command("handoff").description("Record an agent handoff.").requiredOption("--from <agent>", "Source agent id.").requiredOption("--to <agent>", "Target agent id.").requiredOption("--decision <decision>", "Decision being handed off.").requiredOption("--risk <risk>", "Risk that remains.").option("--evidence <evidence...>", "Evidence paths or notes.").action((options) => {
|
|
3415
|
+
console.log(
|
|
3416
|
+
JSON.stringify(
|
|
3417
|
+
recordHandoff(process.cwd(), {
|
|
3418
|
+
fromAgentId: options.from,
|
|
3419
|
+
toAgentId: options.to,
|
|
3420
|
+
decision: options.decision,
|
|
3421
|
+
risk: options.risk,
|
|
3422
|
+
...options.evidence ? { evidence: options.evidence } : {}
|
|
3423
|
+
}),
|
|
3424
|
+
null,
|
|
3425
|
+
2
|
|
3426
|
+
)
|
|
3427
|
+
);
|
|
3428
|
+
});
|
|
3429
|
+
session.command("correct <text...>").description("Record a human correction and optionally promote it to durable rules.").option("--agent <agent>", "Agent id.").option("--scope <scope>", "Correction scope: session, project, agent, upstream-proposal.", "session").action((textParts, options) => {
|
|
3430
|
+
console.log(
|
|
3431
|
+
JSON.stringify(
|
|
3432
|
+
recordCorrection(process.cwd(), {
|
|
3433
|
+
...options.agent ? { agentId: options.agent } : {},
|
|
3434
|
+
scope: options.scope,
|
|
3435
|
+
text: textParts.join(" ")
|
|
3436
|
+
}),
|
|
3437
|
+
null,
|
|
3438
|
+
2
|
|
3439
|
+
)
|
|
3440
|
+
);
|
|
3441
|
+
});
|
|
3442
|
+
session.command("artifact").description("Record a changed or relevant artifact path.").requiredOption("--file <file>", "Artifact file path.").option("--note <note>", "Artifact note.").action((options) => {
|
|
3443
|
+
console.log(JSON.stringify(recordArtifact(process.cwd(), options.file, options.note), null, 2));
|
|
3444
|
+
});
|
|
3445
|
+
session.command("verify").description("Record verification evidence.").requiredOption("--command <command>", "Command or review performed.").requiredOption("--result <result>", "pass, fail, or skipped.").option("--notes <notes>", "Verification notes.").action((options) => {
|
|
3446
|
+
console.log(JSON.stringify(recordVerification(process.cwd(), options.command, options.result, options.notes), null, 2));
|
|
3447
|
+
});
|
|
3448
|
+
session.command("output <name...>").description("Mark a required session output status.").requiredOption("--status <status>", "missing, partial, complete, or not-applicable.").option("--evidence <evidence>", "Evidence path, command, or note.").action((nameParts, options) => {
|
|
3449
|
+
if (!isRequiredOutputStatus(options.status)) {
|
|
3450
|
+
console.error(`Invalid --status value "${options.status}". Expected one of: ${requiredOutputStatuses.join(", ")}.`);
|
|
3451
|
+
process.exitCode = 1;
|
|
3452
|
+
return;
|
|
3453
|
+
}
|
|
3454
|
+
console.log(JSON.stringify(recordRequiredOutput(process.cwd(), nameParts.join(" "), options.status, options.evidence), null, 2));
|
|
3455
|
+
});
|
|
3456
|
+
session.command("render").description("Render active session Markdown files.").action(() => {
|
|
3457
|
+
console.log(JSON.stringify(renderActiveSession(process.cwd()), null, 2));
|
|
3458
|
+
});
|
|
3459
|
+
session.command("close").description("Close the active session.").option("--status <status>", "planned, in-progress, blocked, or complete.", "complete").action((options) => {
|
|
3460
|
+
console.log(JSON.stringify(closeSession(process.cwd(), options.status), null, 2));
|
|
3461
|
+
});
|
|
3462
|
+
var correction = program.command("correction").description("Manage durable Agent Studio correction rules.");
|
|
3463
|
+
correction.command("list").description("List correction rules.").action(() => {
|
|
3464
|
+
console.log(JSON.stringify(listCorrections(process.cwd()), null, 2));
|
|
3465
|
+
});
|
|
3466
|
+
correction.command("add <text...>").description("Add a durable correction rule.").option("--scope <scope>", "Correction scope: project, agent, or upstream-proposal.", "project").option("--agent <agent>", "Agent id for agent-scoped corrections.").action((textParts, options) => {
|
|
3467
|
+
console.log(
|
|
3468
|
+
JSON.stringify(
|
|
3469
|
+
addCorrection(process.cwd(), {
|
|
3470
|
+
scope: options.scope,
|
|
3471
|
+
...options.agent ? { agentId: options.agent } : {},
|
|
3472
|
+
text: textParts.join(" ")
|
|
3473
|
+
}),
|
|
3474
|
+
null,
|
|
3475
|
+
2
|
|
3476
|
+
)
|
|
3477
|
+
);
|
|
3478
|
+
});
|
|
3479
|
+
correction.command("apply [id]").description("Mark a correction rule active and reviewed.").option("--id <id>", "Correction id.").action((idArgument, options) => {
|
|
3480
|
+
const id = idArgument ?? options.id;
|
|
3481
|
+
if (!id) {
|
|
3482
|
+
console.error("Missing correction id. Use agent-kit correction apply <id> or --id <id>.");
|
|
3483
|
+
process.exitCode = 1;
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
3486
|
+
console.log(JSON.stringify(applyCorrection(process.cwd(), id), null, 2));
|
|
3487
|
+
});
|
|
3488
|
+
correction.command("retire <id>").description("Retire a correction rule.").requiredOption("--reason <reason>", "Reason for retirement.").action((id, options) => {
|
|
3489
|
+
console.log(JSON.stringify(retireCorrection(process.cwd(), id, options.reason), null, 2));
|
|
3490
|
+
});
|
|
3491
|
+
correction.command("propose-upstream <id>").description("Create an upstream proposal from a project or agent correction.").action((id) => {
|
|
3492
|
+
console.log(JSON.stringify(proposeCorrectionUpstream(process.cwd(), id), null, 2));
|
|
3493
|
+
});
|
|
3494
|
+
var studio = program.command("studio").description("Export local Agent Studio views.");
|
|
3495
|
+
studio.command("export").description("Generate a self-contained static Agent Studio HTML file.").action(() => {
|
|
3496
|
+
console.log(JSON.stringify(exportStaticStudio(process.cwd()), null, 2));
|
|
3497
|
+
});
|
|
3498
|
+
var research = program.command("research").description("Research high-quality open-source repositories.");
|
|
3499
|
+
research.command("discover").description("Discover GitHub repo candidates using configured queries.").option("--limit <number>", "Maximum repositories to write.", (value) => Number.parseInt(value, 10)).action(async (options) => {
|
|
3500
|
+
const repos = await discoverRepos({
|
|
3501
|
+
cwd: process.cwd(),
|
|
3502
|
+
...options.limit === void 0 ? {} : { limit: options.limit }
|
|
3503
|
+
});
|
|
3504
|
+
console.log(`Wrote ${repos.length} candidates to research/repo-candidates.json`);
|
|
3505
|
+
});
|
|
3506
|
+
research.command("scan").description("Shallow clone candidate repos and write repo findings.").option("--keep-clones", "Keep cloned repositories in research/workdir.").action(async (options) => {
|
|
3507
|
+
const findings = await scanRepos({ cwd: process.cwd(), keepClones: Boolean(options.keepClones) });
|
|
3508
|
+
console.log(`Wrote ${findings.length} findings to research/findings`);
|
|
3509
|
+
});
|
|
3510
|
+
research.command("summarize").description("Generate category summaries from repo findings.").action(() => {
|
|
3511
|
+
const outputs = summarizeFindings(process.cwd());
|
|
3512
|
+
for (const output of outputs) console.log(output);
|
|
3513
|
+
});
|
|
3514
|
+
research.command("propose-updates").description("Create a research-to-template update brief.").action(() => {
|
|
3515
|
+
console.log(proposeUpdates(process.cwd()));
|
|
3516
|
+
});
|
|
3517
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
3518
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
3519
|
+
process.exitCode = 1;
|
|
3520
|
+
});
|
|
3521
|
+
//# sourceMappingURL=index.js.map
|