@trespies-source/dojo-genesis-plugin 1.0.0 → 1.0.2

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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PluginAPI } from "./tests/__mocks__/openclaw-types.js";
1
+ import type { PluginAPI } from "openclaw";
2
2
  declare const _default: {
3
3
  id: string;
4
4
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;;;;;;;;;;;;;;kBAuBrD,SAAS;;AAfzB,wBA8BE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;;;;;;;;;;;;;;kBAuB1B,SAAS;;AAfzB,wBA8BE"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { registerPluginHooksFromDir } from "./tests/__mocks__/openclaw-types.js";
1
+ import { registerPluginHooksFromDir } from "openclaw";
2
2
  import { registerDojoCommands } from "./src/commands/router.js";
3
3
  import { registerOrchestrationTools } from "./src/orchestration/tool-registry.js";
4
4
  import { initStateManager } from "./src/state/manager.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AAClF,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,eAAe;IACb,EAAE,EAAE,qBAAqB;IACzB,IAAI,EAAE,cAAc;IAEpB,YAAY,EAAE;QACZ,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE;YACV,WAAW,EAAE;gBACX,IAAI,EAAE,QAAiB;gBACvB,OAAO,EAAE,qBAAqB;gBAC9B,WAAW,EAAE,4CAA4C;aAC1D;SACF;KACF;IAED,QAAQ,CAAC,GAAc;QACrB,IAAI,QAAgB,CAAC;QACrB,IAAI,CAAC;YACH,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,qBAAqB,CAAC,CAAC;QACtE,CAAC;QAAC,MAAM,CAAC;YACP,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC;QAC1C,CAAC;QAED,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QAC3B,oBAAoB,CAAC,GAAG,CAAC,CAAC;QAC1B,0BAA0B,CAAC,GAAG,CAAC,CAAC;QAChC,0BAA0B,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAE3C,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;IACrD,CAAC;CACF,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AAClF,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,eAAe;IACb,EAAE,EAAE,qBAAqB;IACzB,IAAI,EAAE,cAAc;IAEpB,YAAY,EAAE;QACZ,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE;YACV,WAAW,EAAE;gBACX,IAAI,EAAE,QAAiB;gBACvB,OAAO,EAAE,qBAAqB;gBAC9B,WAAW,EAAE,4CAA4C;aAC1D;SACF;KACF;IAED,QAAQ,CAAC,GAAc;QACrB,IAAI,QAAgB,CAAC;QACrB,IAAI,CAAC;YACH,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,qBAAqB,CAAC,CAAC;QACtE,CAAC;QAAC,MAAM,CAAC;YACP,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC;QAC1C,CAAC;QAED,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QAC3B,oBAAoB,CAAC,GAAG,CAAC,CAAC;QAC1B,0BAA0B,CAAC,GAAG,CAAC,CAAC;QAChC,0BAA0B,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAE3C,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;IACrD,CAAC;CACF,CAAC"}
package/index.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type { PluginAPI } from "openclaw";
2
+ import { registerPluginHooksFromDir } from "openclaw";
3
+ import { registerDojoCommands } from "./src/commands/router.js";
4
+ import { registerOrchestrationTools } from "./src/orchestration/tool-registry.js";
5
+ import { initStateManager } from "./src/state/manager.js";
6
+ import { homedir } from "os";
7
+ import { join } from "path";
8
+
9
+ export default {
10
+ id: "dojo-genesis-plugin",
11
+ name: "Dojo Genesis",
12
+
13
+ configSchema: {
14
+ type: "object" as const,
15
+ properties: {
16
+ projectsDir: {
17
+ type: "string" as const,
18
+ default: "dojo-genesis-plugin",
19
+ description: "State directory name under OpenClaw config",
20
+ },
21
+ },
22
+ },
23
+
24
+ register(api: PluginAPI) {
25
+ let stateDir: string;
26
+ try {
27
+ stateDir = api.runtime.state.resolveStateDir("dojo-genesis-plugin");
28
+ } catch {
29
+ stateDir = join(homedir(), ".openclaw");
30
+ }
31
+
32
+ initStateManager(stateDir);
33
+ registerDojoCommands(api);
34
+ registerOrchestrationTools(api);
35
+ registerPluginHooksFromDir(api, "./hooks");
36
+
37
+ api.logger.info("Dojo Genesis plugin initialized");
38
+ },
39
+ };
package/openclaw.d.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Type declarations for the openclaw runtime package.
3
+ * The openclaw package does not ship its own .d.ts files,
4
+ * so we declare the subset of the API surface that this plugin uses.
5
+ */
6
+ declare module "openclaw" {
7
+ export interface PluginLogger {
8
+ info(message: string, ...args: unknown[]): void;
9
+ warn(message: string, ...args: unknown[]): void;
10
+ error(message: string, ...args: unknown[]): void;
11
+ debug(message: string, ...args: unknown[]): void;
12
+ }
13
+
14
+ export interface PluginRuntime {
15
+ state: {
16
+ resolveStateDir(pluginId: string): string;
17
+ };
18
+ }
19
+
20
+ export interface CommandContext {
21
+ args?: string;
22
+ }
23
+
24
+ export interface CommandRegistration {
25
+ name: string;
26
+ description: string;
27
+ handler: (ctx: CommandContext) => { text: string } | Promise<{ text: string }>;
28
+ }
29
+
30
+ export interface ToolResult {
31
+ content: Array<{ type: string; text: string }>;
32
+ }
33
+
34
+ export interface ToolRegistration {
35
+ name: string;
36
+ description: string;
37
+ parameters: unknown;
38
+ execute: (id: string, params: Record<string, unknown>) => Promise<ToolResult>;
39
+ }
40
+
41
+ export interface PluginAPI {
42
+ registerCommand(cmd: CommandRegistration): void;
43
+ registerTool(tool: ToolRegistration): void;
44
+ runtime: PluginRuntime;
45
+ logger: PluginLogger;
46
+ }
47
+
48
+ export interface HookEvent {
49
+ type: string;
50
+ action?: string;
51
+ messages: string[];
52
+ context?: Record<string, unknown>;
53
+ }
54
+
55
+ export type HookHandler = (event: HookEvent) => Promise<void> | void;
56
+
57
+ export function registerPluginHooksFromDir(api: PluginAPI, dir: string): void;
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trespies-source/dojo-genesis-plugin",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Specification-driven development orchestration for OpenClaw",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -9,6 +9,9 @@
9
9
  ".": "./dist/index.js"
10
10
  },
11
11
  "files": [
12
+ "index.ts",
13
+ "openclaw.d.ts",
14
+ "src/",
12
15
  "dist/",
13
16
  "skills/",
14
17
  "hooks/",
@@ -18,7 +21,9 @@
18
21
  "CHANGELOG.md"
19
22
  ],
20
23
  "openclaw": {
21
- "extensions": "index.ts"
24
+ "extensions": [
25
+ "./index.ts"
26
+ ]
22
27
  },
23
28
  "scripts": {
24
29
  "build": "tsc",
@@ -0,0 +1,25 @@
1
+ import { stateManager } from "../state/manager.js";
2
+
3
+ export async function handleArchive(args: string[]): Promise<{ text: string }> {
4
+ const name = args[0];
5
+ if (!name) {
6
+ return { text: "Project name is required. Usage: `/dojo archive <name>`" };
7
+ }
8
+
9
+ const global = await stateManager.getGlobalState();
10
+ const meta = global.projects.find((p) => p.id === name);
11
+
12
+ if (!meta) {
13
+ return { text: `Project \`${name}\` not found.` };
14
+ }
15
+
16
+ if (meta.archived) {
17
+ return { text: `Project \`${name}\` is already archived.` };
18
+ }
19
+
20
+ await stateManager.archiveProject(name);
21
+
22
+ return {
23
+ text: `**Project archived:** \`${name}\`\n\nThe project files remain on disk. It will no longer appear in \`/dojo list\` (use \`--all\` to see archived projects).`,
24
+ };
25
+ }
@@ -0,0 +1,68 @@
1
+ import { stateManager } from "../state/manager.js";
2
+ import { validateProjectName } from "../utils/validation.js";
3
+ import { writeTextFile, writeJsonFile, ensureDir } from "../utils/file-ops.js";
4
+ import { generateProjectMd } from "../utils/markdown.js";
5
+ import type { ProjectMetadata, ProjectState } from "../state/types.js";
6
+
7
+ export async function handleInit(args: string[]): Promise<{ text: string }> {
8
+ const name = args[0];
9
+ const validation = validateProjectName(name);
10
+ if (!validation.valid) {
11
+ return { text: `Invalid project name: ${validation.error}` };
12
+ }
13
+
14
+ const global = await stateManager.getGlobalState();
15
+ if (global.projects.some((p) => p.id === name && !p.archived)) {
16
+ return { text: `Project \`${name}\` already exists. Use \`/dojo switch ${name}\` to activate it.` };
17
+ }
18
+
19
+ const descIdx = args.indexOf("--desc");
20
+ const description =
21
+ descIdx >= 0 ? args.slice(descIdx + 1).join(" ").replace(/^"|"$/g, "") : "";
22
+
23
+ const now = new Date().toISOString();
24
+ const date = now.split("T")[0];
25
+
26
+ const meta: ProjectMetadata = {
27
+ id: name,
28
+ name,
29
+ description,
30
+ phase: "initialized",
31
+ createdAt: now,
32
+ lastAccessedAt: now,
33
+ archived: false,
34
+ };
35
+
36
+ const state: ProjectState = {
37
+ projectId: name,
38
+ phase: "initialized",
39
+ tracks: [],
40
+ decisions: [],
41
+ specs: [],
42
+ artifacts: [],
43
+ currentTrack: null,
44
+ lastSkill: "",
45
+ pendingAction: null,
46
+ activityLog: [{ timestamp: now, action: "command:init", summary: "Project created" }],
47
+ lastUpdated: now,
48
+ };
49
+
50
+ const projectDir = `${stateManager.getBasePath()}/projects/${name}`;
51
+ await ensureDir(projectDir);
52
+ await ensureDir(`${projectDir}/scouts`);
53
+ await ensureDir(`${projectDir}/specs`);
54
+ await ensureDir(`${projectDir}/prompts`);
55
+ await ensureDir(`${projectDir}/retros`);
56
+ await ensureDir(`${projectDir}/tracks`);
57
+ await ensureDir(`${projectDir}/artifacts`);
58
+
59
+ await writeTextFile(`${projectDir}/PROJECT.md`, generateProjectMd(name, description, date));
60
+ await writeTextFile(`${projectDir}/decisions.md`, `# Decision Log: ${name}\n\n---\n\n`);
61
+ await writeJsonFile(`${projectDir}/state.json`, state);
62
+
63
+ await stateManager.addProject(meta);
64
+
65
+ return {
66
+ text: `**Project created:** \`${name}\`\n\n**Phase:** initialized\n**Active:** yes\n\nNext: Run \`/dojo scout <tension>\` to start scouting.`,
67
+ };
68
+ }
@@ -0,0 +1,11 @@
1
+ import { stateManager } from "../state/manager.js";
2
+ import { formatProjectList } from "../ui/chat-formatter.js";
3
+
4
+ export async function handleList(args: string[]): Promise<{ text: string }> {
5
+ const showArchived = args.includes("--all");
6
+ const global = await stateManager.getGlobalState();
7
+
8
+ const table = formatProjectList(global.projects, showArchived, global.activeProjectId);
9
+
10
+ return { text: table };
11
+ }
@@ -0,0 +1,91 @@
1
+ import type { PluginAPI } from "../../tests/__mocks__/openclaw-types.js";
2
+ import { handleInit } from "./init.js";
3
+ import { handleSwitch } from "./switch.js";
4
+ import { handleStatus } from "./status.js";
5
+ import { handleList } from "./list.js";
6
+ import { handleArchive } from "./archive.js";
7
+ import { handleSkillInvoke } from "./skill-invoke.js";
8
+ import { SKILL_CATALOG, listSkills } from "../skills/catalog.js";
9
+
10
+ // Keep shorthand aliases for the core pipeline
11
+ const PIPELINE_SHORTCUTS: Record<string, string> = {
12
+ scout: "strategic-scout",
13
+ spec: "release-specification",
14
+ tracks: "parallel-tracks",
15
+ commission: "implementation-prompt",
16
+ retro: "retrospective",
17
+ };
18
+
19
+ const HELP_TEXT = `**Dojo Genesis** — Specification-driven development orchestration
20
+
21
+ **Project Management:**
22
+ \`/dojo init <name>\` — Create a new project
23
+ \`/dojo switch <name>\` — Switch active project
24
+ \`/dojo status\` — Show current project status
25
+ \`/dojo list\` — List all projects
26
+ \`/dojo archive <name>\` — Archive a project
27
+
28
+ **Core Pipeline:**
29
+ \`/dojo scout <tension>\` — Strategic scout
30
+ \`/dojo spec <feature>\` — Release specification
31
+ \`/dojo tracks\` — Parallel track decomposition
32
+ \`/dojo commission\` — Implementation prompts
33
+ \`/dojo retro\` — Retrospective
34
+
35
+ **Skill Catalog (40 skills):**
36
+ \`/dojo run <skill-name> [args]\` — Run any skill
37
+ \`/dojo skills [category]\` — Browse available skills
38
+
39
+ Use \`@project-name\` to target a specific project.`;
40
+
41
+ export function registerDojoCommands(api: PluginAPI): void {
42
+ api.registerCommand({
43
+ name: "dojo",
44
+ description: "Dojo Genesis: specification-driven development orchestration",
45
+ handler: (ctx) => {
46
+ const args = (ctx.args || "").trim().split(/\s+/).filter(Boolean);
47
+ const subcommand = args[0]?.toLowerCase();
48
+
49
+ switch (subcommand) {
50
+ case "init":
51
+ return handleInit(args.slice(1));
52
+ case "switch":
53
+ return handleSwitch(args.slice(1));
54
+ case "status":
55
+ return handleStatus(args.slice(1));
56
+ case "list":
57
+ return handleList(args.slice(1));
58
+ case "archive":
59
+ return handleArchive(args.slice(1));
60
+
61
+ case "scout":
62
+ case "spec":
63
+ case "tracks":
64
+ case "commission":
65
+ case "retro":
66
+ return handleSkillInvoke(PIPELINE_SHORTCUTS[subcommand], args.slice(1));
67
+
68
+ case "run": {
69
+ const skillName = args[1];
70
+ if (!skillName || !SKILL_CATALOG[skillName]) {
71
+ return {
72
+ text: `Unknown skill: \`${skillName || "(none)"}\`. Run \`/dojo skills\` for available skills.`,
73
+ };
74
+ }
75
+ return handleSkillInvoke(skillName, args.slice(2));
76
+ }
77
+
78
+ case "skills": {
79
+ const category = args[1];
80
+ return { text: listSkills(category) };
81
+ }
82
+
83
+ case "help":
84
+ case undefined:
85
+ return { text: HELP_TEXT };
86
+ default:
87
+ return { text: `Unknown command: \`${subcommand}\`. Run \`/dojo help\` for available commands.` };
88
+ }
89
+ },
90
+ });
91
+ }
@@ -0,0 +1,34 @@
1
+ import { stateManager } from "../state/manager.js";
2
+ import type { PendingAction } from "../state/types.js";
3
+
4
+ export async function handleSkillInvoke(
5
+ skillName: string,
6
+ args: string[],
7
+ ): Promise<{ text: string }> {
8
+ const atProject = args.find((a) => a.startsWith("@"));
9
+ const remainingArgs = args.filter((a) => !a.startsWith("@"));
10
+ const projectId = atProject
11
+ ? atProject.slice(1)
12
+ : (await stateManager.getGlobalState()).activeProjectId;
13
+
14
+ if (!projectId) {
15
+ return { text: "No active project. Run `/dojo init <name>` first." };
16
+ }
17
+
18
+ const state = await stateManager.getProjectState(projectId);
19
+ if (!state) {
20
+ return { text: `Project \`${projectId}\` not found.` };
21
+ }
22
+
23
+ const pendingAction: PendingAction = {
24
+ skill: skillName,
25
+ args: remainingArgs.join(" "),
26
+ requestedAt: new Date().toISOString(),
27
+ };
28
+ await stateManager.updateProjectState(projectId, { pendingAction });
29
+ await stateManager.addActivity(projectId, `command:${skillName}`, `Requested ${skillName}`);
30
+
31
+ return {
32
+ text: `**Starting ${skillName}** for project \`${projectId}\` (phase: ${state.phase})\n\nThe agent will pick up this request and run the skill with your project context.`,
33
+ };
34
+ }
@@ -0,0 +1,51 @@
1
+ import { stateManager } from "../state/manager.js";
2
+ import { formatPhase, formatDate, formatTrackTable } from "../ui/chat-formatter.js";
3
+ import type { DojoPhase } from "../state/types.js";
4
+
5
+ export async function handleStatus(args: string[]): Promise<{ text: string }> {
6
+ const atProject = args.find((a) => a.startsWith("@"));
7
+ const projectId = atProject?.slice(1) || undefined;
8
+ const state = await stateManager.getProjectState(projectId);
9
+
10
+ if (!state) {
11
+ return { text: "No active project. Run `/dojo init <name>` to create one." };
12
+ }
13
+
14
+ const recentActivity = state.activityLog.slice(0, 5);
15
+ const nextAction = suggestNextAction(state.phase);
16
+
17
+ let output = `**Project:** \`${state.projectId}\`\n`;
18
+ output += `**Phase:** ${formatPhase(state.phase)}\n`;
19
+ output += `**Last updated:** ${formatDate(state.lastUpdated)}\n\n`;
20
+
21
+ if (state.tracks.length > 0) {
22
+ output += `**Tracks:**\n${formatTrackTable(state.tracks)}\n\n`;
23
+ }
24
+
25
+ if (recentActivity.length > 0) {
26
+ output += `**Recent activity:**\n`;
27
+ for (const entry of recentActivity) {
28
+ output += `- ${formatDate(entry.timestamp)} — ${entry.summary}\n`;
29
+ }
30
+ output += "\n";
31
+ }
32
+
33
+ if (nextAction) {
34
+ output += `**Suggested next:** ${nextAction}`;
35
+ }
36
+
37
+ return { text: output };
38
+ }
39
+
40
+ function suggestNextAction(phase: DojoPhase): string {
41
+ const suggestions: Record<string, string> = {
42
+ initialized: "`/dojo scout <tension>` — Start with a strategic scout",
43
+ scouting: "`/dojo spec <feature>` — Write a release specification",
44
+ specifying: "`/dojo tracks` — Decompose into parallel tracks",
45
+ decomposing: "`/dojo commission` — Generate implementation prompts",
46
+ commissioning: "Hand off prompts to implementation agents",
47
+ implementing: "`/dojo retro` — Run a retrospective when done",
48
+ retrospective: "`/dojo init <name>` — Start a new project, or continue iterating",
49
+ };
50
+ return suggestions[phase] || "";
51
+ }
@@ -0,0 +1,25 @@
1
+ import { stateManager } from "../state/manager.js";
2
+
3
+ export async function handleSwitch(args: string[]): Promise<{ text: string }> {
4
+ const name = args[0];
5
+ if (!name) {
6
+ return { text: "Project name is required. Usage: `/dojo switch <name>`" };
7
+ }
8
+
9
+ const global = await stateManager.getGlobalState();
10
+ const meta = global.projects.find((p) => p.id === name);
11
+
12
+ if (!meta) {
13
+ return { text: `Project \`${name}\` not found.` };
14
+ }
15
+
16
+ if (meta.archived) {
17
+ return { text: `Project \`${name}\` is archived. Unarchive it first before switching.` };
18
+ }
19
+
20
+ await stateManager.setActiveProject(name);
21
+
22
+ return {
23
+ text: `**Switched to:** \`${name}\`\n\n**Phase:** ${meta.phase}`,
24
+ };
25
+ }
@@ -0,0 +1,210 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { stateManager } from "../state/manager.js";
3
+ import { validateOutputDir, sanitizeFilename } from "../utils/validation.js";
4
+ import { writeTextFile, ensureDir } from "../utils/file-ops.js";
5
+ import type { DojoPhase } from "../state/types.js";
6
+
7
+ interface ToolResult {
8
+ content: Array<{ type: string; text: string }>;
9
+ }
10
+
11
+ interface ToolRegistration {
12
+ name: string;
13
+ description: string;
14
+ parameters: unknown;
15
+ execute: (id: string, params: Record<string, unknown>) => Promise<ToolResult>;
16
+ }
17
+
18
+ interface ToolRegistrar {
19
+ registerTool(tool: ToolRegistration): void;
20
+ }
21
+
22
+ function jsonResult(data: unknown): ToolResult {
23
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
24
+ }
25
+
26
+ export function registerOrchestrationTools(api: ToolRegistrar): void {
27
+ api.registerTool({
28
+ name: "dojo_get_context",
29
+ description:
30
+ "Get the active Dojo Genesis project context including phase, tracks, decisions, and recent activity. Call this at the start of any skill execution to check if a project is active.",
31
+ parameters: Type.Object({
32
+ projectId: Type.Optional(
33
+ Type.String({ description: "Target a specific project instead of the active one" }),
34
+ ),
35
+ }),
36
+ async execute(_id, params) {
37
+ const state = await stateManager.getProjectState(params.projectId as string | undefined);
38
+ if (!state) {
39
+ return jsonResult({ active: false, message: "No active project" });
40
+ }
41
+
42
+ return jsonResult({
43
+ active: true,
44
+ projectId: state.projectId,
45
+ phase: state.phase,
46
+ tracks: state.tracks,
47
+ decisions: state.decisions.map((d) => ({ date: d.date, topic: d.topic })),
48
+ specs: state.specs,
49
+ currentTrack: state.currentTrack,
50
+ lastSkill: state.lastSkill,
51
+ recentActivity: state.activityLog.slice(0, 10),
52
+ });
53
+ },
54
+ });
55
+
56
+ api.registerTool({
57
+ name: "dojo_save_artifact",
58
+ description:
59
+ "Save a skill output as a markdown file in the active project directory. Use this after completing a skill to persist the results.",
60
+ parameters: Type.Object({
61
+ filename: Type.String({
62
+ description: "Output filename (e.g., '2026-02-12_scout_build-native.md')",
63
+ }),
64
+ content: Type.String({ description: "Full markdown content to save" }),
65
+ outputDir: Type.String({
66
+ description: "Subdirectory: scouts, specs, prompts, retros, tracks, or artifacts",
67
+ }),
68
+ projectId: Type.Optional(Type.String({ description: "Target specific project" })),
69
+ }),
70
+ async execute(_id, params) {
71
+ const outputDir = params.outputDir as string;
72
+ if (!validateOutputDir(outputDir)) {
73
+ return jsonResult({ error: `Invalid output directory: ${outputDir}` });
74
+ }
75
+
76
+ const global = await stateManager.getGlobalState();
77
+ const id = (params.projectId as string | undefined) || global.activeProjectId;
78
+ if (!id) {
79
+ return jsonResult({ error: "No active project" });
80
+ }
81
+
82
+ const safeName = sanitizeFilename(params.filename as string);
83
+ const basePath = stateManager.getBasePath();
84
+ const dir = `${basePath}/projects/${id}/${outputDir}`;
85
+ const filePath = `${dir}/${safeName}`;
86
+
87
+ await ensureDir(dir);
88
+ await writeTextFile(filePath, params.content as string);
89
+
90
+ const state = await stateManager.getProjectState(id);
91
+ if (state) {
92
+ state.artifacts.push({
93
+ category: outputDir,
94
+ filename: safeName,
95
+ createdAt: new Date().toISOString(),
96
+ skill: state.lastSkill || "unknown",
97
+ });
98
+ await stateManager.updateProjectState(id, { artifacts: state.artifacts });
99
+ await stateManager.addActivity(
100
+ id,
101
+ `artifact:${outputDir}`,
102
+ `Saved ${safeName} to ${outputDir}/`,
103
+ );
104
+ }
105
+
106
+ return jsonResult({ saved: true, path: `${outputDir}/${safeName}` });
107
+ },
108
+ });
109
+
110
+ api.registerTool({
111
+ name: "dojo_update_state",
112
+ description:
113
+ "Update the active project's phase, track status, or other state. Use this after completing a skill to advance the project workflow.",
114
+ parameters: Type.Object({
115
+ phase: Type.Optional(
116
+ Type.String({
117
+ description:
118
+ "New project phase (initialized, scouting, specifying, decomposing, commissioning, implementing, retrospective)",
119
+ }),
120
+ ),
121
+ lastSkill: Type.Optional(
122
+ Type.String({ description: "Name of the skill that just ran" }),
123
+ ),
124
+ currentTrack: Type.Optional(
125
+ Type.String({ description: "Set the current active track" }),
126
+ ),
127
+ addTrack: Type.Optional(
128
+ Type.Object({
129
+ id: Type.String(),
130
+ name: Type.String(),
131
+ dependencies: Type.Optional(Type.Array(Type.String())),
132
+ }),
133
+ ),
134
+ addDecision: Type.Optional(
135
+ Type.Object({
136
+ topic: Type.String(),
137
+ file: Type.String(),
138
+ }),
139
+ ),
140
+ addSpec: Type.Optional(
141
+ Type.Object({
142
+ version: Type.String(),
143
+ file: Type.String(),
144
+ }),
145
+ ),
146
+ projectId: Type.Optional(Type.String({ description: "Target specific project" })),
147
+ }),
148
+ async execute(_id, params) {
149
+ const global = await stateManager.getGlobalState();
150
+ const id = (params.projectId as string | undefined) || global.activeProjectId;
151
+ if (!id) {
152
+ return jsonResult({ error: "No active project" });
153
+ }
154
+
155
+ const state = await stateManager.getProjectState(id);
156
+ if (!state) {
157
+ return jsonResult({ error: `Project not found: ${id}` });
158
+ }
159
+
160
+ const updates: Record<string, unknown> = {};
161
+
162
+ if (params.phase) updates.phase = params.phase as DojoPhase;
163
+ if (params.lastSkill) updates.lastSkill = params.lastSkill;
164
+ if (params.currentTrack) updates.currentTrack = params.currentTrack;
165
+
166
+ if (params.addTrack) {
167
+ const track = params.addTrack as { id: string; name: string; dependencies?: string[] };
168
+ state.tracks.push({
169
+ id: track.id,
170
+ name: track.name,
171
+ status: "pending",
172
+ dependencies: track.dependencies || [],
173
+ promptFile: null,
174
+ });
175
+ updates.tracks = state.tracks;
176
+ }
177
+
178
+ if (params.addDecision) {
179
+ const decision = params.addDecision as { topic: string; file: string };
180
+ state.decisions.push({
181
+ date: new Date().toISOString().split("T")[0],
182
+ topic: decision.topic,
183
+ file: decision.file,
184
+ });
185
+ updates.decisions = state.decisions;
186
+ }
187
+
188
+ if (params.addSpec) {
189
+ const spec = params.addSpec as { version: string; file: string };
190
+ state.specs.push({
191
+ version: spec.version,
192
+ file: spec.file,
193
+ });
194
+ updates.specs = state.specs;
195
+ }
196
+
197
+ await stateManager.updateProjectState(id, updates);
198
+
199
+ if (params.lastSkill) {
200
+ await stateManager.addActivity(
201
+ id,
202
+ `skill:${params.lastSkill}`,
203
+ `${params.lastSkill} completed`,
204
+ );
205
+ }
206
+
207
+ return jsonResult({ updated: true, phase: (params.phase as string) || state.phase });
208
+ },
209
+ });
210
+ }
@@ -0,0 +1,269 @@
1
+ export interface SkillEntry {
2
+ name: string;
3
+ category: string;
4
+ description: string;
5
+ }
6
+
7
+ export const SKILL_CATALOG: Record<string, SkillEntry> = {
8
+ // Pipeline (also available as /dojo scout, spec, tracks, commission, retro)
9
+ "strategic-scout": {
10
+ name: "strategic-scout",
11
+ category: "pipeline",
12
+ description: "Explore strategic tensions and scout multiple routes",
13
+ },
14
+ "release-specification": {
15
+ name: "release-specification",
16
+ category: "pipeline",
17
+ description: "Write a production-ready release specification",
18
+ },
19
+ "parallel-tracks": {
20
+ name: "parallel-tracks",
21
+ category: "pipeline",
22
+ description: "Decompose specs into independent parallel tracks",
23
+ },
24
+ "implementation-prompt": {
25
+ name: "implementation-prompt",
26
+ category: "pipeline",
27
+ description: "Generate structured implementation prompts",
28
+ },
29
+ "retrospective": {
30
+ name: "retrospective",
31
+ category: "pipeline",
32
+ description: "Reflect on what went well, what was hard, what to improve",
33
+ },
34
+
35
+ // Workflow
36
+ "iterative-scouting": {
37
+ name: "iterative-scouting",
38
+ category: "workflow",
39
+ description: "Iterate scout cycles with reframes",
40
+ },
41
+ "strategic-to-tactical-workflow": {
42
+ name: "strategic-to-tactical-workflow",
43
+ category: "workflow",
44
+ description: "Full scout → spec → commission pipeline",
45
+ },
46
+ "pre-implementation-checklist": {
47
+ name: "pre-implementation-checklist",
48
+ category: "workflow",
49
+ description: "Verify specs are ready before commissioning",
50
+ },
51
+ "context-ingestion": {
52
+ name: "context-ingestion",
53
+ category: "workflow",
54
+ description: "Create plans grounded in uploaded files",
55
+ },
56
+ "frontend-from-backend": {
57
+ name: "frontend-from-backend",
58
+ category: "workflow",
59
+ description: "Write frontend specs from backend architecture",
60
+ },
61
+ "spec-constellation-to-prompt-suite": {
62
+ name: "spec-constellation-to-prompt-suite",
63
+ category: "workflow",
64
+ description: "Convert multiple specs into coordinated prompts",
65
+ },
66
+ "planning-with-files": {
67
+ name: "planning-with-files",
68
+ category: "workflow",
69
+ description: "Route file-based planning to specialized modes",
70
+ },
71
+
72
+ // Research
73
+ "research-modes": {
74
+ name: "research-modes",
75
+ category: "research",
76
+ description: "Deep and wide research with structured approaches",
77
+ },
78
+ "research-synthesis": {
79
+ name: "research-synthesis",
80
+ category: "research",
81
+ description: "Synthesize multiple sources into actionable insights",
82
+ },
83
+ "project-exploration": {
84
+ name: "project-exploration",
85
+ category: "research",
86
+ description: "Explore new projects to assess collaboration potential",
87
+ },
88
+ "era-architecture": {
89
+ name: "era-architecture",
90
+ category: "research",
91
+ description: "Architect multi-release eras with shared vocabulary",
92
+ },
93
+ "repo-context-sync": {
94
+ name: "repo-context-sync",
95
+ category: "research",
96
+ description: "Sync and extract context from repositories",
97
+ },
98
+ "documentation-audit": {
99
+ name: "documentation-audit",
100
+ category: "research",
101
+ description: "Audit documentation for drift and accuracy",
102
+ },
103
+ "health-audit": {
104
+ name: "health-audit",
105
+ category: "research",
106
+ description: "Comprehensive repository health check",
107
+ },
108
+
109
+ // Forge
110
+ "skill-creation": {
111
+ name: "skill-creation",
112
+ category: "forge",
113
+ description: "Create new reusable skills",
114
+ },
115
+ "skill-maintenance": {
116
+ name: "skill-maintenance",
117
+ category: "forge",
118
+ description: "Maintain skill health through systematic review",
119
+ },
120
+ "skill-audit-upgrade": {
121
+ name: "skill-audit-upgrade",
122
+ category: "forge",
123
+ description: "Audit and upgrade skills to quality standards",
124
+ },
125
+ "process-extraction": {
126
+ name: "process-extraction",
127
+ category: "forge",
128
+ description: "Transform workflows into reusable skills",
129
+ },
130
+
131
+ // Garden
132
+ "memory-garden": {
133
+ name: "memory-garden",
134
+ category: "garden",
135
+ description: "Write structured memory entries for context management",
136
+ },
137
+ "seed-extraction": {
138
+ name: "seed-extraction",
139
+ category: "garden",
140
+ description: "Extract reusable patterns from experiences",
141
+ },
142
+ "seed-library": {
143
+ name: "seed-library",
144
+ category: "garden",
145
+ description: "Access and apply Dojo Seed Patches",
146
+ },
147
+ "compression-ritual": {
148
+ name: "compression-ritual",
149
+ category: "garden",
150
+ description: "Distill conversation history into memory artifacts",
151
+ },
152
+ "seed-to-skill-converter": {
153
+ name: "seed-to-skill-converter",
154
+ category: "garden",
155
+ description: "Elevate proven seeds into full skills",
156
+ },
157
+
158
+ // Orchestration
159
+ "handoff-protocol": {
160
+ name: "handoff-protocol",
161
+ category: "orchestration",
162
+ description: "Hand off work between agents cleanly",
163
+ },
164
+ "decision-propagation": {
165
+ name: "decision-propagation",
166
+ category: "orchestration",
167
+ description: "Propagate decisions across document ecosystem",
168
+ },
169
+ "workspace-navigation": {
170
+ name: "workspace-navigation",
171
+ category: "orchestration",
172
+ description: "Navigate shared agent workspaces",
173
+ },
174
+ "agent-teaching": {
175
+ name: "agent-teaching",
176
+ category: "orchestration",
177
+ description: "Teach peers through shared practice",
178
+ },
179
+
180
+ // System
181
+ "semantic-clusters": {
182
+ name: "semantic-clusters",
183
+ category: "system",
184
+ description: "Map system capabilities with action-verb clusters",
185
+ },
186
+ "repo-status": {
187
+ name: "repo-status",
188
+ category: "system",
189
+ description: "Generate comprehensive repo status documents",
190
+ },
191
+ "status-template": {
192
+ name: "status-template",
193
+ category: "system",
194
+ description: "Write status docs using 10-section template",
195
+ },
196
+ "status-writing": {
197
+ name: "status-writing",
198
+ category: "system",
199
+ description: "Write and update STATUS.md files",
200
+ },
201
+
202
+ // Tools
203
+ "patient-learning-protocol": {
204
+ name: "patient-learning-protocol",
205
+ category: "tools",
206
+ description: "Learn at the pace of understanding",
207
+ },
208
+ "file-management": {
209
+ name: "file-management",
210
+ category: "tools",
211
+ description: "Organize files and directories flexibly",
212
+ },
213
+ "product-positioning": {
214
+ name: "product-positioning",
215
+ category: "tools",
216
+ description: "Reframe binary product decisions",
217
+ },
218
+ "multi-surface-strategy": {
219
+ name: "multi-surface-strategy",
220
+ category: "tools",
221
+ description: "Design coherent multi-surface strategies",
222
+ },
223
+ };
224
+
225
+ export const CATEGORIES = [
226
+ "pipeline",
227
+ "workflow",
228
+ "research",
229
+ "forge",
230
+ "garden",
231
+ "orchestration",
232
+ "system",
233
+ "tools",
234
+ ] as const;
235
+
236
+ export function listSkills(category?: string): string {
237
+ const entries = Object.values(SKILL_CATALOG);
238
+ const filtered = category
239
+ ? entries.filter((e) => e.category === category)
240
+ : entries;
241
+
242
+ if (filtered.length === 0) {
243
+ return category
244
+ ? `No skills in category "${category}". Available: ${CATEGORIES.join(", ")}`
245
+ : "No skills found.";
246
+ }
247
+
248
+ const grouped: Record<string, SkillEntry[]> = {};
249
+ for (const entry of filtered) {
250
+ (grouped[entry.category] ??= []).push(entry);
251
+ }
252
+
253
+ const lines: string[] = ["**Dojo Genesis Skill Catalog**\n"];
254
+ for (const cat of CATEGORIES) {
255
+ if (!grouped[cat]) continue;
256
+ const label =
257
+ cat === "pipeline"
258
+ ? `${cat} (shorthand: /dojo scout|spec|tracks|commission|retro)`
259
+ : cat;
260
+ lines.push(`**${label}:**`);
261
+ for (const s of grouped[cat]) {
262
+ lines.push(` \`${s.name}\` — ${s.description}`);
263
+ }
264
+ lines.push("");
265
+ }
266
+
267
+ lines.push("Run any skill with: `/dojo run <skill-name> [args]`");
268
+ return lines.join("\n");
269
+ }
@@ -0,0 +1,134 @@
1
+ import { readJsonFile, writeJsonFile, ensureDir } from "../utils/file-ops.js";
2
+ import type { GlobalState, ProjectState, ProjectMetadata } from "./types.js";
3
+ import { checkSchemaVersion } from "./migrations.js";
4
+
5
+ const SCHEMA_VERSION = "1.0.0";
6
+ const MAX_ACTIVITY_LOG = 50;
7
+
8
+ export class DojoStateManager {
9
+ private basePath: string;
10
+ private globalCache: GlobalState | null = null;
11
+ private projectCache: Map<string, ProjectState> = new Map();
12
+
13
+ constructor(configDir: string) {
14
+ this.basePath = `${configDir}/dojo-genesis-plugin`;
15
+ }
16
+
17
+ getBasePath(): string {
18
+ return this.basePath;
19
+ }
20
+
21
+ async getGlobalState(): Promise<GlobalState> {
22
+ if (!this.globalCache) {
23
+ await ensureDir(this.basePath);
24
+ const loaded = await readJsonFile<GlobalState>(
25
+ `${this.basePath}/global-state.json`,
26
+ {
27
+ version: SCHEMA_VERSION,
28
+ activeProjectId: null,
29
+ projects: [],
30
+ lastUpdated: new Date().toISOString(),
31
+ },
32
+ );
33
+ this.globalCache = checkSchemaVersion(loaded);
34
+ }
35
+ return this.globalCache;
36
+ }
37
+
38
+ async setActiveProject(projectId: string | null): Promise<void> {
39
+ const state = await this.getGlobalState();
40
+ state.activeProjectId = projectId;
41
+ state.lastUpdated = new Date().toISOString();
42
+ if (projectId) {
43
+ const meta = state.projects.find((p) => p.id === projectId);
44
+ if (meta) meta.lastAccessedAt = state.lastUpdated;
45
+ }
46
+ await this.saveGlobalState(state);
47
+ }
48
+
49
+ async addProject(meta: ProjectMetadata): Promise<void> {
50
+ const state = await this.getGlobalState();
51
+ state.projects.push(meta);
52
+ state.activeProjectId = meta.id;
53
+ state.lastUpdated = new Date().toISOString();
54
+ await this.saveGlobalState(state);
55
+ }
56
+
57
+ async getProjectState(projectId?: string): Promise<ProjectState | null> {
58
+ const global = await this.getGlobalState();
59
+ const id = projectId || global.activeProjectId;
60
+ if (!id) return null;
61
+
62
+ if (!this.projectCache.has(id)) {
63
+ const state = await readJsonFile<ProjectState | null>(
64
+ `${this.basePath}/projects/${id}/state.json`,
65
+ null,
66
+ );
67
+ if (state) this.projectCache.set(id, state);
68
+ return state;
69
+ }
70
+ return this.projectCache.get(id) || null;
71
+ }
72
+
73
+ async updateProjectState(projectId: string, update: Partial<ProjectState>): Promise<void> {
74
+ const current = await this.getProjectState(projectId);
75
+ if (!current) throw new Error(`Project not found: ${projectId}`);
76
+
77
+ const updated: ProjectState = {
78
+ ...current,
79
+ ...update,
80
+ lastUpdated: new Date().toISOString(),
81
+ };
82
+ await writeJsonFile(`${this.basePath}/projects/${projectId}/state.json`, updated);
83
+ this.projectCache.set(projectId, updated);
84
+
85
+ const global = await this.getGlobalState();
86
+ const meta = global.projects.find((p) => p.id === projectId);
87
+ if (meta) {
88
+ meta.phase = updated.phase;
89
+ meta.lastAccessedAt = updated.lastUpdated;
90
+ await this.saveGlobalState(global);
91
+ }
92
+ }
93
+
94
+ async archiveProject(projectId: string): Promise<boolean> {
95
+ const state = await this.getGlobalState();
96
+ const meta = state.projects.find((p) => p.id === projectId);
97
+ if (!meta) return false;
98
+ if (meta.archived) return false;
99
+
100
+ meta.archived = true;
101
+ meta.lastAccessedAt = new Date().toISOString();
102
+
103
+ if (state.activeProjectId === projectId) {
104
+ state.activeProjectId = null;
105
+ }
106
+
107
+ state.lastUpdated = new Date().toISOString();
108
+ await this.saveGlobalState(state);
109
+ return true;
110
+ }
111
+
112
+ async addActivity(projectId: string, action: string, summary: string): Promise<void> {
113
+ const state = await this.getProjectState(projectId);
114
+ if (!state) return;
115
+
116
+ state.activityLog = [
117
+ { timestamp: new Date().toISOString(), action, summary },
118
+ ...state.activityLog.slice(0, MAX_ACTIVITY_LOG - 1),
119
+ ];
120
+ await this.updateProjectState(projectId, { activityLog: state.activityLog });
121
+ }
122
+
123
+ private async saveGlobalState(state: GlobalState): Promise<void> {
124
+ await writeJsonFile(`${this.basePath}/global-state.json`, state);
125
+ this.globalCache = state;
126
+ }
127
+ }
128
+
129
+ export let stateManager: DojoStateManager;
130
+
131
+ export function initStateManager(configDir: string): DojoStateManager {
132
+ stateManager = new DojoStateManager(configDir);
133
+ return stateManager;
134
+ }
@@ -0,0 +1,12 @@
1
+ import type { GlobalState } from "./types.js";
2
+
3
+ const CURRENT_VERSION = "1.0.0";
4
+
5
+ export function checkSchemaVersion(state: GlobalState): GlobalState {
6
+ if (state.version !== CURRENT_VERSION) {
7
+ throw new Error(
8
+ `Unsupported schema version: ${state.version}. Expected ${CURRENT_VERSION}.`,
9
+ );
10
+ }
11
+ return state;
12
+ }
@@ -0,0 +1,77 @@
1
+ export type DojoPhase =
2
+ | "initialized"
3
+ | "scouting"
4
+ | "specifying"
5
+ | "decomposing"
6
+ | "commissioning"
7
+ | "implementing"
8
+ | "retrospective";
9
+
10
+ export interface GlobalState {
11
+ version: string;
12
+ activeProjectId: string | null;
13
+ projects: ProjectMetadata[];
14
+ lastUpdated: string;
15
+ }
16
+
17
+ export interface ProjectMetadata {
18
+ id: string;
19
+ name: string;
20
+ description: string;
21
+ phase: DojoPhase;
22
+ createdAt: string;
23
+ lastAccessedAt: string;
24
+ archived: boolean;
25
+ }
26
+
27
+ export interface PendingAction {
28
+ skill: string;
29
+ args: string;
30
+ requestedAt: string;
31
+ }
32
+
33
+ export interface ProjectState {
34
+ projectId: string;
35
+ phase: DojoPhase;
36
+ tracks: Track[];
37
+ decisions: DecisionRef[];
38
+ specs: SpecRef[];
39
+ artifacts: ArtifactRef[];
40
+ currentTrack: string | null;
41
+ lastSkill: string;
42
+ pendingAction: PendingAction | null;
43
+ activityLog: ActivityEntry[];
44
+ lastUpdated: string;
45
+ }
46
+
47
+ export interface Track {
48
+ id: string;
49
+ name: string;
50
+ status: "pending" | "in-progress" | "completed" | "blocked";
51
+ dependencies: string[];
52
+ promptFile: string | null;
53
+ }
54
+
55
+ export interface DecisionRef {
56
+ date: string;
57
+ topic: string;
58
+ file: string;
59
+ }
60
+
61
+ export interface SpecRef {
62
+ version: string;
63
+ file: string;
64
+ }
65
+
66
+ export interface ArtifactRef {
67
+ category: string;
68
+ filename: string;
69
+ createdAt: string;
70
+ skill: string;
71
+ }
72
+
73
+ export interface ActivityEntry {
74
+ timestamp: string;
75
+ action: string;
76
+ summary: string;
77
+ }
@@ -0,0 +1,48 @@
1
+ import type { Track, DojoPhase } from "../state/types.js";
2
+
3
+ const PHASE_INDICATOR: Record<DojoPhase, string> = {
4
+ initialized: "[ ]",
5
+ scouting: "[~]",
6
+ specifying: "[~]",
7
+ decomposing: "[~]",
8
+ commissioning: "[~]",
9
+ implementing: "[>]",
10
+ retrospective: "[*]",
11
+ };
12
+
13
+ export function formatPhase(phase: DojoPhase): string {
14
+ return `${PHASE_INDICATOR[phase] || "[ ]"} ${phase}`;
15
+ }
16
+
17
+ export function formatDate(iso: string): string {
18
+ return iso.split("T")[0];
19
+ }
20
+
21
+ export function formatTrackTable(tracks: Track[]): string {
22
+ if (tracks.length === 0) return "_No tracks defined._";
23
+
24
+ let table = "| Track | Name | Status | Dependencies |\n";
25
+ table += "|-------|------|--------|-------------|\n";
26
+ for (const t of tracks) {
27
+ const deps = t.dependencies.length > 0 ? t.dependencies.join(", ") : "none";
28
+ table += `| ${t.id} | ${t.name} | ${t.status} | ${deps} |\n`;
29
+ }
30
+ return table;
31
+ }
32
+
33
+ export function formatProjectList(
34
+ projects: Array<{ id: string; phase: string; lastAccessedAt: string; archived: boolean }>,
35
+ showArchived: boolean,
36
+ activeId: string | null,
37
+ ): string {
38
+ const visible = projects.filter(p => showArchived || !p.archived);
39
+ if (visible.length === 0) return "_No projects. Run `/dojo init <name>` to create one._";
40
+
41
+ let table = "| Project | Phase | Last Active | Active |\n";
42
+ table += "|---------|-------|-------------|--------|\n";
43
+ for (const p of visible) {
44
+ const active = p.id === activeId ? ">>>" : "";
45
+ table += `| ${p.id} | ${p.phase} | ${formatDate(p.lastAccessedAt)} | ${active} |\n`;
46
+ }
47
+ return table;
48
+ }
@@ -0,0 +1,39 @@
1
+ import { promises as fs } from "fs";
2
+ import { dirname } from "path";
3
+
4
+ export async function readJsonFile<T>(path: string, defaultValue: T): Promise<T> {
5
+ try {
6
+ const raw = await fs.readFile(path, "utf-8");
7
+ return JSON.parse(raw) as T;
8
+ } catch (err: unknown) {
9
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return defaultValue;
10
+ throw err;
11
+ }
12
+ }
13
+
14
+ export async function writeJsonFile(path: string, data: unknown): Promise<void> {
15
+ await ensureDir(dirname(path));
16
+ const tmp = `${path}.tmp`;
17
+ await fs.writeFile(tmp, JSON.stringify(data, null, 2), "utf-8");
18
+ await fs.rename(tmp, path);
19
+ }
20
+
21
+ export async function writeTextFile(path: string, content: string): Promise<void> {
22
+ await ensureDir(dirname(path));
23
+ const tmp = `${path}.tmp`;
24
+ await fs.writeFile(tmp, content, "utf-8");
25
+ await fs.rename(tmp, path);
26
+ }
27
+
28
+ export async function ensureDir(dir: string): Promise<void> {
29
+ await fs.mkdir(dir, { recursive: true });
30
+ }
31
+
32
+ export async function fileExists(path: string): Promise<boolean> {
33
+ try {
34
+ await fs.access(path);
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
@@ -0,0 +1,12 @@
1
+ export function generateProjectMd(name: string, description: string, date: string): string {
2
+ const descBlock = description ? `${description}\n\n` : "";
3
+ return (
4
+ `# ${name}\n\n` +
5
+ descBlock +
6
+ `**Phase:** initialized\n` +
7
+ `**Created:** ${date}\n\n` +
8
+ `---\n\n` +
9
+ `## Activity Log\n\n` +
10
+ `- ${date} — Project created\n`
11
+ );
12
+ }
@@ -0,0 +1,30 @@
1
+ const PROJECT_NAME_REGEX = /^[a-z0-9][a-z0-9-]{1,63}$/;
2
+ const DOUBLE_HYPHEN = /--/;
3
+
4
+ export function validateProjectName(name: string): { valid: boolean; error?: string } {
5
+ if (!name) return { valid: false, error: "Project name is required" };
6
+ if (!PROJECT_NAME_REGEX.test(name)) {
7
+ return {
8
+ valid: false,
9
+ error: "Project name must be 2-64 chars, lowercase alphanumeric + hyphens, start with letter/number",
10
+ };
11
+ }
12
+ if (DOUBLE_HYPHEN.test(name)) {
13
+ return { valid: false, error: "Project name cannot contain consecutive hyphens" };
14
+ }
15
+ return { valid: true };
16
+ }
17
+
18
+ export function sanitizeFilename(input: string): string {
19
+ return input
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9-_]/g, "-")
22
+ .replace(/-+/g, "-")
23
+ .replace(/^-|-$/g, "")
24
+ .slice(0, 128);
25
+ }
26
+
27
+ export function validateOutputDir(dir: string): boolean {
28
+ const allowed = ["scouts", "specs", "prompts", "retros", "tracks", "artifacts"];
29
+ return allowed.includes(dir);
30
+ }