@leonarto/spec-embryo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +156 -0
  2. package/package.json +48 -0
  3. package/src/backends/base.ts +18 -0
  4. package/src/backends/deterministic.ts +105 -0
  5. package/src/backends/index.ts +26 -0
  6. package/src/backends/prompt.ts +169 -0
  7. package/src/backends/subprocess.ts +198 -0
  8. package/src/cli.ts +111 -0
  9. package/src/commands/agents.ts +16 -0
  10. package/src/commands/current.ts +95 -0
  11. package/src/commands/doctor.ts +12 -0
  12. package/src/commands/handoff.ts +64 -0
  13. package/src/commands/init.ts +101 -0
  14. package/src/commands/reshape.ts +20 -0
  15. package/src/commands/resume.ts +19 -0
  16. package/src/commands/spec.ts +108 -0
  17. package/src/commands/status.ts +98 -0
  18. package/src/commands/task.ts +190 -0
  19. package/src/commands/ui.ts +35 -0
  20. package/src/domain.ts +357 -0
  21. package/src/engine.ts +290 -0
  22. package/src/frontmatter.ts +83 -0
  23. package/src/index.ts +75 -0
  24. package/src/paths.ts +32 -0
  25. package/src/repository.ts +807 -0
  26. package/src/services/adoption.ts +169 -0
  27. package/src/services/agents.ts +191 -0
  28. package/src/services/dashboard.ts +776 -0
  29. package/src/services/details.ts +453 -0
  30. package/src/services/doctor.ts +452 -0
  31. package/src/services/layout.ts +420 -0
  32. package/src/services/spec-answer-evaluation.ts +103 -0
  33. package/src/services/spec-import.ts +217 -0
  34. package/src/services/spec-questions.ts +343 -0
  35. package/src/services/ui.ts +34 -0
  36. package/src/storage.ts +57 -0
  37. package/src/templates.ts +270 -0
  38. package/tsconfig.json +17 -0
  39. package/web/package.json +24 -0
  40. package/web/src/app.css +83 -0
  41. package/web/src/app.d.ts +6 -0
  42. package/web/src/app.html +11 -0
  43. package/web/src/lib/components/AnalysisFilters.svelte +293 -0
  44. package/web/src/lib/components/DocumentBody.svelte +100 -0
  45. package/web/src/lib/components/MultiSelectDropdown.svelte +280 -0
  46. package/web/src/lib/components/SelectDropdown.svelte +265 -0
  47. package/web/src/lib/server/project-root.ts +34 -0
  48. package/web/src/lib/task-board.ts +20 -0
  49. package/web/src/routes/+layout.server.ts +57 -0
  50. package/web/src/routes/+layout.svelte +421 -0
  51. package/web/src/routes/+layout.ts +1 -0
  52. package/web/src/routes/+page.svelte +530 -0
  53. package/web/src/routes/specs/+page.svelte +416 -0
  54. package/web/src/routes/specs/[specId]/+page.server.ts +81 -0
  55. package/web/src/routes/specs/[specId]/+page.svelte +675 -0
  56. package/web/src/routes/tasks/+page.svelte +341 -0
  57. package/web/src/routes/tasks/[taskId]/+page.server.ts +12 -0
  58. package/web/src/routes/tasks/[taskId]/+page.svelte +431 -0
  59. package/web/src/routes/timeline/+page.svelte +1093 -0
  60. package/web/svelte.config.js +10 -0
  61. package/web/tsconfig.json +9 -0
  62. package/web/vite.config.ts +11 -0
@@ -0,0 +1,169 @@
1
+ import type { DoctorFinding } from "./doctor.ts";
2
+ import type { AdoptionInspection } from "./layout.ts";
3
+
4
+ export interface AdoptionCommandSuggestion {
5
+ command: string;
6
+ reason: string;
7
+ }
8
+
9
+ export interface AdoptionGuidance {
10
+ overview: string;
11
+ whatFound: string[];
12
+ whatChanged: string[];
13
+ remainingWork: string[];
14
+ recommendedCommands: AdoptionCommandSuggestion[];
15
+ firstWeekSequence: string[];
16
+ }
17
+
18
+ function uniqueCommands(suggestions: AdoptionCommandSuggestion[]): AdoptionCommandSuggestion[] {
19
+ const seen = new Set<string>();
20
+ const deduped: AdoptionCommandSuggestion[] = [];
21
+
22
+ for (const suggestion of suggestions) {
23
+ if (seen.has(suggestion.command)) {
24
+ continue;
25
+ }
26
+
27
+ seen.add(suggestion.command);
28
+ deduped.push(suggestion);
29
+ }
30
+
31
+ return deduped;
32
+ }
33
+
34
+ export function buildAdoptionGuidance(input: {
35
+ adoption: AdoptionInspection;
36
+ memoryDir: string;
37
+ created?: string[];
38
+ skipped?: string[];
39
+ doctorFindings?: DoctorFinding[];
40
+ commandContext?: "init" | "doctor";
41
+ }): AdoptionGuidance {
42
+ const created = input.created ?? [];
43
+ const skipped = input.skipped ?? [];
44
+ const doctorFindings = input.doctorFindings ?? [];
45
+ const warningOrBlockingFindings = doctorFindings.filter(
46
+ (finding) => finding.severity === "warning" || finding.severity === "blocking",
47
+ );
48
+
49
+ const whatFound = [...input.adoption.findings];
50
+ const whatChanged: string[] = [];
51
+ const remainingWork: string[] = [];
52
+ const recommendedCommands: AdoptionCommandSuggestion[] = [];
53
+ const firstWeekSequence: string[] = [];
54
+
55
+ if (created.length > 0) {
56
+ whatChanged.push(`Created ${created.length} managed file${created.length === 1 ? "" : "s"} under \`${input.memoryDir}\` and \`.specpm/\`.`);
57
+ } else {
58
+ whatChanged.push("No new managed files were created in this run.");
59
+ }
60
+
61
+ if (skipped.length > 0) {
62
+ whatChanged.push(`Preserved ${skipped.length} existing file${skipped.length === 1 ? "" : "s"} that were already present.`);
63
+ }
64
+
65
+ switch (input.adoption.state) {
66
+ case "fresh":
67
+ remainingWork.push("Create the first active spec and linked tasks so the repo stops being only scaffolded memory.");
68
+ recommendedCommands.push(
69
+ {
70
+ command: "spm spec create --title \"...\" --summary \"...\"",
71
+ reason: "Define the first real planning anchor for the adopted repo.",
72
+ },
73
+ {
74
+ command: "spm task create --title \"...\" --summary \"...\" --spec-ids SPEC-001",
75
+ reason: "Create the first executable work items linked to that spec.",
76
+ },
77
+ );
78
+ break;
79
+ case "legacy-docs": {
80
+ const source = input.adoption.layoutChoice.adoptedHome ?? input.adoption.layoutChoice.candidates[0]?.relativePath ?? "docs";
81
+ remainingWork.push("Legacy docs still need migration or import into Spec Embryo-managed specs and tasks.");
82
+ recommendedCommands.push({
83
+ command: `spm spec import-prompt --source ${source}`,
84
+ reason: "Generate an agent-ready migration bundle from the most likely legacy documentation home.",
85
+ });
86
+ break;
87
+ }
88
+ case "partially-adopted":
89
+ remainingWork.push("The repo still needs migration or cleanup before normal Spec Embryo flows are fully trustworthy.");
90
+ recommendedCommands.push({
91
+ command: "spm doctor",
92
+ reason: "Inspect missing structure, parse errors, and broken links before continuing migration.",
93
+ });
94
+ break;
95
+ case "adopted":
96
+ remainingWork.push("The managed layout is in place; the next work is about initializing meaningful project memory, not structure.");
97
+ break;
98
+ }
99
+
100
+ if (warningOrBlockingFindings.length > 0) {
101
+ remainingWork.push(`Doctor reported ${warningOrBlockingFindings.length} issue${warningOrBlockingFindings.length === 1 ? "" : "s"} that still need migration or cleanup decisions.`);
102
+ }
103
+
104
+ if (warningOrBlockingFindings.length > 0 && input.commandContext !== "doctor") {
105
+ recommendedCommands.push({
106
+ command: "spm doctor --json",
107
+ reason: "Get a structured report that an active agent can turn into explicit next actions without guessing.",
108
+ });
109
+ } else if (!input.commandContext || input.commandContext === "init") {
110
+ recommendedCommands.push({
111
+ command: "spm doctor",
112
+ reason: "Verify the adopted layout before trusting normal resume and UI flows.",
113
+ });
114
+ }
115
+
116
+ recommendedCommands.push({
117
+ command: "spm current set --focus \"...\" --summary \"...\"",
118
+ reason: "Create the first explicit checkpoint so another human or agent can answer where work should continue.",
119
+ });
120
+ recommendedCommands.push({
121
+ command: "spm handoff --title \"Initial adoption checkpoint\"",
122
+ reason: "Capture the migration state once the first meaningful setup pass is complete.",
123
+ });
124
+
125
+ firstWeekSequence.push(
126
+ "Run doctor and review its findings before assuming the repo is ready for normal work.",
127
+ "Import or create the first real specs so the managed memory reflects product intent instead of only scaffolding.",
128
+ "Create linked tasks and mark the first active slice in current state.",
129
+ "Write an initial handoff once the adoption choices and first actions are explicit.",
130
+ );
131
+
132
+ return {
133
+ overview:
134
+ input.adoption.state === "adopted"
135
+ ? "This repo already looks structurally adopted; the next step is making the managed memory meaningful."
136
+ : input.adoption.state === "legacy-docs"
137
+ ? "This repo looks importable but still needs migration work to turn legacy docs into canonical Spec Embryo memory."
138
+ : input.adoption.state === "partially-adopted"
139
+ ? "This repo shows partial Spec Embryo signals and needs cleanup before the workflow becomes fully trustworthy."
140
+ : "This repo is fresh to Spec Embryo and now needs its first real spec/task/current-state content.",
141
+ whatFound,
142
+ whatChanged,
143
+ remainingWork,
144
+ recommendedCommands: uniqueCommands(recommendedCommands),
145
+ firstWeekSequence,
146
+ };
147
+ }
148
+
149
+ export function renderAdoptionGuidance(guidance: AdoptionGuidance): string[] {
150
+ return [
151
+ "Adoption summary:",
152
+ `- ${guidance.overview}`,
153
+ "",
154
+ "What was found:",
155
+ ...guidance.whatFound.map((item) => `- ${item}`),
156
+ "",
157
+ "What changed:",
158
+ ...guidance.whatChanged.map((item) => `- ${item}`),
159
+ "",
160
+ "What still needs migration or follow-up:",
161
+ ...guidance.remainingWork.map((item) => `- ${item}`),
162
+ "",
163
+ "Recommended next commands:",
164
+ ...guidance.recommendedCommands.map((item) => `- ${item.command} — ${item.reason}`),
165
+ "",
166
+ "First-week sequence:",
167
+ ...guidance.firstWeekSequence.map((item, index) => `${index + 1}. ${item}`),
168
+ ];
169
+ }
@@ -0,0 +1,191 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { dirname, join, relative } from "node:path";
3
+
4
+ export interface AgentsBudgetRule {
5
+ target: number;
6
+ hardCap: number;
7
+ }
8
+
9
+ export interface AgentsFileReport {
10
+ absolutePath: string;
11
+ relativePath: string;
12
+ folder: string;
13
+ estimatedTokens: number;
14
+ rule: AgentsBudgetRule;
15
+ ok: boolean;
16
+ }
17
+
18
+ export interface AgentsChainReport {
19
+ relativePath: string;
20
+ chainFolders: string[];
21
+ estimatedTokens: number;
22
+ target: number;
23
+ hardCap: number;
24
+ ok: boolean;
25
+ }
26
+
27
+ export interface AgentsHealthReport {
28
+ files: AgentsFileReport[];
29
+ chains: AgentsChainReport[];
30
+ failures: string[];
31
+ summary: {
32
+ fileCount: number;
33
+ failureCount: number;
34
+ maxFileTokens: number;
35
+ maxChainTokens: number;
36
+ ok: boolean;
37
+ };
38
+ }
39
+
40
+ const EXCLUDED_DIRS = new Set([".git", "node_modules", "dist", "coverage"]);
41
+ const CHAIN_TARGET = 1800;
42
+ const CHAIN_HARD_CAP = 2200;
43
+
44
+ const RULES: Record<string, AgentsBudgetRule> = {
45
+ ".": { target: 900, hardCap: 1200 },
46
+ ".specpm": { target: 300, hardCap: 400 },
47
+ "docs": { target: 300, hardCap: 500 },
48
+ "docs/spm": { target: 280, hardCap: 450 },
49
+ "docs/spm/specs": { target: 220, hardCap: 450 },
50
+ "docs/spm/tasks": { target: 220, hardCap: 450 },
51
+ "docs/spm/skills": { target: 220, hardCap: 450 },
52
+ "scripts": { target: 220, hardCap: 350 },
53
+ "skills": { target: 300, hardCap: 500 },
54
+ "src": { target: 350, hardCap: 600 },
55
+ "src/backends": { target: 240, hardCap: 450 },
56
+ "src/commands": { target: 220, hardCap: 450 },
57
+ "src/services": { target: 220, hardCap: 450 },
58
+ "tests": { target: 180, hardCap: 350 },
59
+ };
60
+
61
+ export function estimateTokens(content: string): number {
62
+ return Math.ceil(content.length / 4);
63
+ }
64
+
65
+ export function agentsRuleForFolder(folder: string): AgentsBudgetRule {
66
+ const normalized = folder === "" ? "." : folder;
67
+ const candidates = Object.keys(RULES).filter((prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`));
68
+ const match = candidates.sort((left, right) => right.length - left.length)[0] ?? ".";
69
+ return RULES[match]!;
70
+ }
71
+
72
+ export async function findAgentsFiles(rootDir: string, currentDir = rootDir): Promise<string[]> {
73
+ const entries = await readdir(currentDir, { withFileTypes: true });
74
+ const results: string[] = [];
75
+
76
+ for (const entry of entries) {
77
+ if (entry.isDirectory()) {
78
+ if (EXCLUDED_DIRS.has(entry.name)) {
79
+ continue;
80
+ }
81
+
82
+ results.push(...(await findAgentsFiles(rootDir, join(currentDir, entry.name))));
83
+ continue;
84
+ }
85
+
86
+ if (entry.isFile() && entry.name === "AGENTS.md") {
87
+ results.push(join(currentDir, entry.name));
88
+ }
89
+ }
90
+
91
+ return results.sort();
92
+ }
93
+
94
+ export async function buildAgentsHealthReport(rootDir: string): Promise<AgentsHealthReport> {
95
+ const files = await findAgentsFiles(rootDir);
96
+ const tokenByFolder = new Map<string, number>();
97
+ const fileReports: AgentsFileReport[] = [];
98
+ const failures: string[] = [];
99
+
100
+ for (const file of files) {
101
+ const relFile = relative(rootDir, file);
102
+ const folder = dirname(relFile) === "." ? "." : dirname(relFile);
103
+ const content = await readFile(file, "utf8");
104
+ const estimatedTokens = estimateTokens(content);
105
+ const rule = agentsRuleForFolder(folder);
106
+ const ok = estimatedTokens <= rule.hardCap;
107
+
108
+ tokenByFolder.set(folder, estimatedTokens);
109
+ fileReports.push({
110
+ absolutePath: file,
111
+ relativePath: relFile,
112
+ folder,
113
+ estimatedTokens,
114
+ rule,
115
+ ok,
116
+ });
117
+
118
+ if (!ok) {
119
+ failures.push(`${relFile} exceeds hard cap (${estimatedTokens} > ${rule.hardCap})`);
120
+ }
121
+ }
122
+
123
+ const chainReports: AgentsChainReport[] = fileReports.map((file) => {
124
+ const segments = file.folder === "." ? [] : file.folder.split("/");
125
+ const chainFolders = ["."];
126
+
127
+ for (let index = 0; index < segments.length; index += 1) {
128
+ chainFolders.push(segments.slice(0, index + 1).join("/"));
129
+ }
130
+
131
+ const estimatedTokens = chainFolders.reduce((sum, entry) => sum + (tokenByFolder.get(entry) ?? 0), 0);
132
+ const ok = estimatedTokens <= CHAIN_HARD_CAP;
133
+
134
+ if (!ok) {
135
+ failures.push(`${file.relativePath} chain exceeds hard cap (${estimatedTokens} > ${CHAIN_HARD_CAP})`);
136
+ }
137
+
138
+ return {
139
+ relativePath: file.relativePath,
140
+ chainFolders,
141
+ estimatedTokens,
142
+ target: CHAIN_TARGET,
143
+ hardCap: CHAIN_HARD_CAP,
144
+ ok,
145
+ };
146
+ });
147
+
148
+ return {
149
+ files: fileReports,
150
+ chains: chainReports,
151
+ failures,
152
+ summary: {
153
+ fileCount: fileReports.length,
154
+ failureCount: failures.length,
155
+ maxFileTokens: fileReports.reduce((max, file) => Math.max(max, file.estimatedTokens), 0),
156
+ maxChainTokens: chainReports.reduce((max, chain) => Math.max(max, chain.estimatedTokens), 0),
157
+ ok: failures.length === 0,
158
+ },
159
+ };
160
+ }
161
+
162
+ export function renderAgentsHealthReport(report: AgentsHealthReport): string {
163
+ if (report.files.length === 0) {
164
+ return "No AGENTS.md files found.";
165
+ }
166
+
167
+ const lines: string[] = ["AGENTS.md budget report", ""];
168
+
169
+ for (const file of report.files) {
170
+ lines.push(
171
+ `${file.ok ? "OK" : "FAIL"} ${file.relativePath} | est_tokens=${file.estimatedTokens} | target<=${file.rule.target} | hard_cap<=${file.rule.hardCap}`,
172
+ );
173
+ }
174
+
175
+ lines.push("", "Chain report", "");
176
+
177
+ for (const chain of report.chains) {
178
+ lines.push(
179
+ `${chain.ok ? "OK" : "FAIL"} ${chain.relativePath} | active_chain_tokens=${chain.estimatedTokens} | target<=${chain.target} | hard_cap<=${chain.hardCap}`,
180
+ );
181
+ }
182
+
183
+ if (report.failures.length > 0) {
184
+ lines.push("", "Failures:");
185
+ for (const failure of report.failures) {
186
+ lines.push(`- ${failure}`);
187
+ }
188
+ }
189
+
190
+ return lines.join("\n");
191
+ }