@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,108 @@
1
+ import type { ParsedArgs } from "../cli.ts";
2
+ import { flagValue } from "../cli.ts";
3
+ import { SPEC_STATUSES, isSpecStatus } from "../domain.ts";
4
+ import { createSpec, loadProjectContext, readProjectConfig, setSpecStatus } from "../repository.ts";
5
+ import { buildSpecImportBundle, renderSpecImportBundle } from "../services/spec-import.ts";
6
+
7
+ function parseCommaList(value: string | undefined): string[] {
8
+ return value
9
+ ? value
10
+ .split(",")
11
+ .map((item) => item.trim())
12
+ .filter(Boolean)
13
+ : [];
14
+ }
15
+
16
+ export async function runSpecCommand(rootDir: string, parsed: ParsedArgs): Promise<string> {
17
+ if (parsed.subcommand === "create") {
18
+ const title = flagValue(parsed, "title");
19
+ const summary = flagValue(parsed, "summary");
20
+ const status = flagValue(parsed, "status");
21
+
22
+ if (!title || !summary) {
23
+ throw new Error("`spec create` requires --title and --summary.");
24
+ }
25
+
26
+ if (status && !isSpecStatus(status)) {
27
+ throw new Error(`Invalid spec status: ${status}. Expected one of ${SPEC_STATUSES.join(", ")}.`);
28
+ }
29
+
30
+ const spec = await createSpec(rootDir, {
31
+ title,
32
+ summary,
33
+ status,
34
+ taskIds: parseCommaList(flagValue(parsed, "task-ids")),
35
+ tags: parseCommaList(flagValue(parsed, "tags")),
36
+ owner: flagValue(parsed, "owner"),
37
+ });
38
+
39
+ if (parsed.flags.has("json")) {
40
+ return JSON.stringify(spec, null, 2);
41
+ }
42
+
43
+ return `Created ${spec.id} [${spec.status}] in ${spec.filePath}`;
44
+ }
45
+
46
+ if (parsed.subcommand === "set-status") {
47
+ const specId = parsed.positionals[2];
48
+ const status = parsed.positionals[3];
49
+
50
+ if (!specId || !status) {
51
+ throw new Error("`spec set-status` requires <SPEC-ID> <status>.");
52
+ }
53
+
54
+ if (!isSpecStatus(status)) {
55
+ throw new Error(`Invalid spec status: ${status}. Expected one of ${SPEC_STATUSES.join(", ")}.`);
56
+ }
57
+
58
+ const spec = await setSpecStatus(rootDir, specId, status);
59
+
60
+ if (parsed.flags.has("json")) {
61
+ return JSON.stringify(spec, null, 2);
62
+ }
63
+
64
+ return `Updated ${spec.id} to [${spec.status}] in ${spec.filePath}`;
65
+ }
66
+
67
+ if (parsed.subcommand === "import-prompt") {
68
+ const source = flagValue(parsed, "source");
69
+ if (!source) {
70
+ throw new Error("`spec import-prompt` requires --source <folder>.");
71
+ }
72
+
73
+ const maxCharsRaw = flagValue(parsed, "max-chars-per-file");
74
+ const maxCharsPerFile = maxCharsRaw ? Number.parseInt(maxCharsRaw, 10) : undefined;
75
+ const config = await readProjectConfig(rootDir);
76
+ const bundle = await buildSpecImportBundle(rootDir, source, {
77
+ instructions: flagValue(parsed, "instructions"),
78
+ maxCharsPerFile: Number.isFinite(maxCharsPerFile) ? maxCharsPerFile : undefined,
79
+ targetMemoryDir: config.memoryDir,
80
+ });
81
+
82
+ if (parsed.flags.has("json")) {
83
+ return JSON.stringify(bundle, null, 2);
84
+ }
85
+
86
+ return renderSpecImportBundle(bundle);
87
+ }
88
+
89
+ if (parsed.subcommand !== "list") {
90
+ throw new Error("Supported spec commands in v1 are `spec list`, `spec create`, `spec set-status`, and `spec import-prompt`.");
91
+ }
92
+
93
+ const context = await loadProjectContext(rootDir);
94
+ const statusFilter = flagValue(parsed, "status");
95
+ const specs = context.specs.filter((spec) => (statusFilter ? spec.status === statusFilter : true));
96
+
97
+ if (parsed.flags.has("json")) {
98
+ return JSON.stringify(specs, null, 2);
99
+ }
100
+
101
+ if (specs.length === 0) {
102
+ return "No specs found.";
103
+ }
104
+
105
+ return specs
106
+ .map((spec) => `${spec.id} [${spec.status}] ${spec.title}\n tasks: ${spec.taskIds.join(", ") || "none"}\n ${spec.summary}`)
107
+ .join("\n\n");
108
+ }
@@ -0,0 +1,98 @@
1
+ import type { ParsedArgs } from "../cli.ts";
2
+ import { deriveStatusSnapshot, deriveNextActions } from "../engine.ts";
3
+ import { loadProjectContext } from "../repository.ts";
4
+ import { buildAgentsHealthReport } from "../services/agents.ts";
5
+
6
+ export async function runStatusCommand(rootDir: string, parsed: ParsedArgs): Promise<string> {
7
+ const [context, agentsReport] = await Promise.all([loadProjectContext(rootDir), buildAgentsHealthReport(rootDir)]);
8
+ const snapshot = deriveStatusSnapshot(context);
9
+ const nextActions = deriveNextActions(context, snapshot);
10
+
11
+ if (parsed.flags.has("json")) {
12
+ return JSON.stringify(
13
+ {
14
+ project: context.config.projectName,
15
+ focus: context.currentState.focus,
16
+ summary: context.currentState.summary,
17
+ counts: {
18
+ specs: snapshot.specCount,
19
+ tasks: snapshot.taskCount,
20
+ in_progress: snapshot.inProgressTasks.length,
21
+ review: snapshot.reviewTasks.length,
22
+ blocked: snapshot.blockedTasks.length,
23
+ ready: snapshot.readyTasks.length,
24
+ },
25
+ active_spec_ids: snapshot.activeSpecIds,
26
+ active_task_ids: snapshot.activeTaskIds,
27
+ next_actions: nextActions,
28
+ missing_dependencies: snapshot.missingDependencies,
29
+ changed_items: snapshot.changedItems,
30
+ git: context.git,
31
+ agents: agentsReport.summary,
32
+ },
33
+ null,
34
+ 2,
35
+ );
36
+ }
37
+
38
+ const lines = [
39
+ `Project: ${context.config.projectName}`,
40
+ `Focus: ${context.currentState.focus}`,
41
+ `Summary: ${context.currentState.summary}`,
42
+ "",
43
+ `Specs: ${snapshot.specCount} total | active ${snapshot.activeSpecIds.length}`,
44
+ `Tasks: ${snapshot.taskCount} total | in progress ${snapshot.inProgressTasks.length} | review ${snapshot.reviewTasks.length} | blocked ${snapshot.blockedTasks.length} | ready ${snapshot.readyTasks.length}`,
45
+ `Git: ${context.git.available ? `${context.git.branch ?? "unknown"}${context.git.dirty ? " (dirty)" : " (clean)"}` : "not available"}`,
46
+ `AGENTS: ${agentsReport.summary.ok ? "healthy" : "needs attention"} | files ${agentsReport.summary.fileCount} | max file ${agentsReport.summary.maxFileTokens} tokens | max chain ${agentsReport.summary.maxChainTokens} tokens`,
47
+ "",
48
+ "Active specs:",
49
+ ...(snapshot.activeSpecIds.length > 0 ? snapshot.activeSpecIds.map((id) => `- ${id}`) : ["- none"]),
50
+ "Active tasks:",
51
+ ...(snapshot.activeTaskIds.length > 0 ? snapshot.activeTaskIds.map((id) => `- ${id}`) : ["- none"]),
52
+ "Next actions:",
53
+ ...nextActions.map((action) => `- ${action}`),
54
+ ];
55
+
56
+ if (snapshot.blockedTasks.length > 0) {
57
+ lines.push("", "Blocked tasks:");
58
+ for (const blocked of snapshot.blockedTasks) {
59
+ const reason = blocked.unmetDependencies.length > 0 ? blocked.unmetDependencies.join(", ") : "manually blocked";
60
+ lines.push(`- ${blocked.task.id}: ${blocked.task.title} (${reason})`);
61
+ }
62
+ }
63
+
64
+ if (snapshot.reviewTasks.length > 0) {
65
+ lines.push("", "Review tasks:");
66
+ for (const review of snapshot.reviewTasks) {
67
+ lines.push(`- ${review.task.id}: ${review.task.title}`);
68
+ }
69
+ }
70
+
71
+ if (snapshot.missingDependencies.length > 0) {
72
+ lines.push("", "Dependency issues:");
73
+ for (const item of snapshot.missingDependencies) {
74
+ lines.push(`- ${item.taskId} references missing dependency ${item.missingTaskId}`);
75
+ }
76
+ }
77
+
78
+ if (snapshot.changedItems.length > 0) {
79
+ lines.push("", "Changed items:");
80
+ for (const item of snapshot.changedItems.slice(0, 10)) {
81
+ const label = item.id ? `${item.id}${item.title ? `: ${item.title}` : ""}` : item.path;
82
+ lines.push(`- [${item.kind}] ${label}`);
83
+ }
84
+ }
85
+
86
+ if (snapshot.recentHandoff) {
87
+ lines.push("", `Recent handoff: ${snapshot.recentHandoff.id} | ${snapshot.recentHandoff.title}`);
88
+ }
89
+
90
+ if (!agentsReport.summary.ok) {
91
+ lines.push("", "AGENTS issues:");
92
+ for (const failure of agentsReport.failures) {
93
+ lines.push(`- ${failure}`);
94
+ }
95
+ }
96
+
97
+ return lines.join("\n");
98
+ }
@@ -0,0 +1,190 @@
1
+ import type { ParsedArgs } from "../cli.ts";
2
+ import { flagValue } from "../cli.ts";
3
+ import { TASK_STATUSES, isTaskStatus } from "../domain.ts";
4
+ import { buildTaskIndex, summarizeTaskNode } from "../engine.ts";
5
+ import { archiveCancelledTasks, archiveDoneTasks, archiveTask, createTask, loadProjectContext, restoreTask, setTaskStatus } from "../repository.ts";
6
+
7
+ function parseCommaList(value: string | undefined): string[] {
8
+ return value
9
+ ? value
10
+ .split(",")
11
+ .map((item) => item.trim())
12
+ .filter(Boolean)
13
+ : [];
14
+ }
15
+
16
+ export async function runTaskCommand(rootDir: string, parsed: ParsedArgs): Promise<string> {
17
+ if (parsed.subcommand === "create") {
18
+ const title = flagValue(parsed, "title");
19
+ const summary = flagValue(parsed, "summary");
20
+ const specIds = parseCommaList(flagValue(parsed, "spec-ids"));
21
+ const status = flagValue(parsed, "status");
22
+ const priorityRaw = flagValue(parsed, "priority");
23
+ const priority = priorityRaw ? Number.parseInt(priorityRaw, 10) : undefined;
24
+
25
+ if (!title || !summary) {
26
+ throw new Error("`task create` requires --title and --summary.");
27
+ }
28
+
29
+ if (specIds.length === 0) {
30
+ throw new Error("`task create` requires --spec-ids <SPEC-001[,SPEC-002]>.");
31
+ }
32
+
33
+ if (status && !isTaskStatus(status)) {
34
+ throw new Error(`Invalid task status: ${status}. Expected one of ${TASK_STATUSES.join(", ")}.`);
35
+ }
36
+
37
+ if (priority !== undefined && (!Number.isFinite(priority) || priority <= 0)) {
38
+ throw new Error("`task create --priority` must be a positive integer.");
39
+ }
40
+
41
+ const task = await createTask(rootDir, {
42
+ title,
43
+ summary,
44
+ specIds,
45
+ status,
46
+ priority,
47
+ dependsOn: parseCommaList(flagValue(parsed, "depends-on")),
48
+ blockedBy: parseCommaList(flagValue(parsed, "blocked-by")),
49
+ ownerRole: flagValue(parsed, "owner-role"),
50
+ tags: parseCommaList(flagValue(parsed, "tags")),
51
+ });
52
+
53
+ if (parsed.flags.has("json")) {
54
+ return JSON.stringify(task, null, 2);
55
+ }
56
+
57
+ return `Created ${task.id} [${task.status}] in ${task.filePath}`;
58
+ }
59
+
60
+ if (parsed.subcommand === "set-status") {
61
+ const taskId = parsed.positionals[2];
62
+ const status = parsed.positionals[3];
63
+
64
+ if (!taskId || !status) {
65
+ throw new Error("`task set-status` requires <TASK-ID> <status>.");
66
+ }
67
+
68
+ if (!isTaskStatus(status)) {
69
+ throw new Error(`Invalid task status: ${status}. Expected one of ${TASK_STATUSES.join(", ")}.`);
70
+ }
71
+
72
+ const task = await setTaskStatus(rootDir, taskId, status);
73
+
74
+ if (parsed.flags.has("json")) {
75
+ return JSON.stringify(task, null, 2);
76
+ }
77
+
78
+ return `Updated ${task.id} to [${task.status}] in ${task.filePath}`;
79
+ }
80
+
81
+ if (parsed.subcommand === "archive") {
82
+ const taskId = parsed.positionals[2];
83
+ if (!taskId) {
84
+ throw new Error("`task archive` requires <TASK-ID>.");
85
+ }
86
+
87
+ const task = await archiveTask(rootDir, taskId, {
88
+ reason: flagValue(parsed, "reason"),
89
+ });
90
+
91
+ if (parsed.flags.has("json")) {
92
+ return JSON.stringify(task, null, 2);
93
+ }
94
+
95
+ return `Archived ${task.id} in ${task.filePath}`;
96
+ }
97
+
98
+ if (parsed.subcommand === "archive-done") {
99
+ const olderThan = flagValue(parsed, "older-than");
100
+ if (!olderThan) {
101
+ throw new Error("`task archive-done` requires --older-than <duration>.");
102
+ }
103
+
104
+ const tasks = await archiveDoneTasks(rootDir, olderThan, {
105
+ reason: flagValue(parsed, "reason"),
106
+ });
107
+
108
+ if (parsed.flags.has("json")) {
109
+ return JSON.stringify(tasks, null, 2);
110
+ }
111
+
112
+ return tasks.length === 0 ? "No done tasks were archived." : `Archived ${tasks.length} done task(s).`;
113
+ }
114
+
115
+ if (parsed.subcommand === "archive-cancelled") {
116
+ const olderThan = flagValue(parsed, "older-than");
117
+ if (!olderThan) {
118
+ throw new Error("`task archive-cancelled` requires --older-than <duration>.");
119
+ }
120
+
121
+ const tasks = await archiveCancelledTasks(rootDir, olderThan, {
122
+ reason: flagValue(parsed, "reason"),
123
+ });
124
+
125
+ if (parsed.flags.has("json")) {
126
+ return JSON.stringify(tasks, null, 2);
127
+ }
128
+
129
+ return tasks.length === 0 ? "No cancelled tasks were archived." : `Archived ${tasks.length} cancelled task(s).`;
130
+ }
131
+
132
+ if (parsed.subcommand === "restore") {
133
+ const taskId = parsed.positionals[2];
134
+ if (!taskId) {
135
+ throw new Error("`task restore` requires <TASK-ID>.");
136
+ }
137
+
138
+ const task = await restoreTask(rootDir, taskId);
139
+
140
+ if (parsed.flags.has("json")) {
141
+ return JSON.stringify(task, null, 2);
142
+ }
143
+
144
+ return `Restored ${task.id} in ${task.filePath}`;
145
+ }
146
+
147
+ if (parsed.subcommand !== "list") {
148
+ throw new Error(
149
+ "Supported task commands are `task list`, `task create`, `task set-status`, `task archive`, `task archive-done`, `task archive-cancelled`, and `task restore`.",
150
+ );
151
+ }
152
+
153
+ const context = await loadProjectContext(rootDir);
154
+ const statusFilter = flagValue(parsed, "status");
155
+ const specFilter = flagValue(parsed, "spec");
156
+ const taskIndex = buildTaskIndex(context.tasks);
157
+ const tasks = context.tasks.filter((task) => {
158
+ if (statusFilter && task.status !== statusFilter) {
159
+ return false;
160
+ }
161
+
162
+ if (specFilter && !task.specIds.includes(specFilter)) {
163
+ return false;
164
+ }
165
+
166
+ return true;
167
+ });
168
+
169
+ if (parsed.flags.has("json")) {
170
+ return JSON.stringify(tasks, null, 2);
171
+ }
172
+
173
+ if (tasks.length === 0) {
174
+ return "No tasks found.";
175
+ }
176
+
177
+ return tasks
178
+ .sort((left, right) => left.priority - right.priority || left.id.localeCompare(right.id))
179
+ .map((task) => {
180
+ const summary = summarizeTaskNode(task, taskIndex);
181
+ const deps = task.dependsOn.join(", ") || "none";
182
+ const unmet = summary.unmetDependencies.join(", ") || "none";
183
+ const statusLabel = task.archivedAt ? `${task.status}, archived` : task.status;
184
+ const archivedLine = task.archivedAt
185
+ ? `\n archived_at: ${task.archivedAt}${task.archiveReason ? ` (${task.archiveReason})` : ""}`
186
+ : "";
187
+ return `${task.id} [${statusLabel}] p${task.priority} ${task.title}\n specs: ${task.specIds.join(", ") || "none"}\n depends_on: ${deps}\n unmet_dependencies: ${unmet}${archivedLine}\n ${task.summary}`;
188
+ })
189
+ .join("\n\n");
190
+ }
@@ -0,0 +1,35 @@
1
+ import { spawn } from "node:child_process";
2
+ import { env, execPath } from "node:process";
3
+ import type { ParsedArgs } from "../cli.ts";
4
+ import { resolveUiLaunchConfig } from "../services/ui.ts";
5
+
6
+ export async function runUiCommand(rootDir: string, parsed: ParsedArgs): Promise<string> {
7
+ const launch = await resolveUiLaunchConfig(rootDir, parsed);
8
+
9
+ await new Promise<void>((resolve, reject) => {
10
+ const child = spawn(
11
+ execPath,
12
+ ["run", "dev", "--", "--host", launch.host, "--port", String(launch.port)],
13
+ {
14
+ cwd: launch.webDir,
15
+ stdio: "inherit",
16
+ env: {
17
+ ...env,
18
+ SPM_PROJECT_ROOT: rootDir,
19
+ },
20
+ },
21
+ );
22
+
23
+ child.on("error", reject);
24
+ child.on("close", (code, signal) => {
25
+ if (signal === "SIGINT" || signal === "SIGTERM" || code === 0) {
26
+ resolve();
27
+ return;
28
+ }
29
+
30
+ reject(new Error(`UI server exited with code ${code ?? "unknown"}.`));
31
+ });
32
+ });
33
+
34
+ return "";
35
+ }