@projitive/mcp 1.2.0 → 2.0.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.
@@ -2,9 +2,10 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
4
  import { z } from "zod";
5
- import { discoverGovernanceArtifacts, catchIt, PROJECT_LINT_CODES, renderLintSuggestions } from "../common/index.js";
6
- import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderToolResponseMarkdown, summarySection, } from "../common/index.js";
7
- import { collectTaskLintSuggestions, loadTasksDocument } from "./task.js";
5
+ import { discoverGovernanceArtifacts, catchIt, PROJECT_LINT_CODES, renderLintSuggestions, ensureStore, replaceRoadmapsInStore, replaceTasksInStore, markMarkdownViewDirty, loadTaskStatusStatsFromStore, loadRoadmapIdsFromStore, } from "../common/index.js";
6
+ import { asText, evidenceSection, getDefaultToolTemplateMarkdown, guidanceSection, lintSection, nextCallSection, renderToolResponseMarkdown, summarySection, } from "../common/index.js";
7
+ import { collectTaskLintSuggestions, loadTasksDocument, loadTasksDocumentWithOptions, renderTasksMarkdown } from "./task.js";
8
+ import { loadRoadmapDocumentWithOptions, renderRoadmapMarkdown } from "./roadmap.js";
8
9
  export const PROJECT_MARKER = ".projitive";
9
10
  const DEFAULT_GOVERNANCE_DIR = ".projitive";
10
11
  const ignoreNames = new Set(["node_modules", ".git", ".next", "dist", "build"]);
@@ -109,49 +110,45 @@ function renderArtifactsMarkdown(artifacts) {
109
110
  return rows.join("\n");
110
111
  }
111
112
  async function readTasksSnapshot(governanceDir) {
112
- const tasksPath = path.join(governanceDir, "tasks.md");
113
- const markdown = await fs.readFile(tasksPath, "utf-8").catch(() => undefined);
114
- if (typeof markdown !== "string") {
113
+ const tasksPath = path.join(governanceDir, PROJECT_MARKER);
114
+ const exists = await fs.access(tasksPath).then(() => true).catch(() => false);
115
+ if (!exists) {
115
116
  return {
116
- tasksPath,
117
117
  exists: false,
118
- tasks: [],
119
118
  lintSuggestions: renderLintSuggestions([
120
119
  {
121
120
  code: PROJECT_LINT_CODES.TASKS_FILE_MISSING,
122
- message: "tasks.md is missing.",
121
+ message: "governance store is missing.",
123
122
  fixHint: "Initialize governance tasks structure first.",
124
123
  },
125
124
  ]),
125
+ todo: 0,
126
+ inProgress: 0,
127
+ blocked: 0,
128
+ done: 0,
129
+ total: 0,
130
+ latestUpdatedAt: "(unknown)",
131
+ score: 0,
126
132
  };
127
133
  }
128
- const { parseTasksBlock } = await import("./task.js");
129
- const tasks = await parseTasksBlock(markdown);
130
- return { tasksPath, exists: true, tasks, lintSuggestions: collectTaskLintSuggestions(tasks, markdown) };
131
- }
132
- function latestTaskUpdatedAt(tasks) {
133
- const timestamps = tasks
134
- .map((task) => new Date(task.updatedAt).getTime())
135
- .filter((value) => Number.isFinite(value));
136
- if (timestamps.length === 0) {
137
- return "(unknown)";
138
- }
139
- return new Date(Math.max(...timestamps)).toISOString();
140
- }
141
- function actionableScore(tasks) {
142
- return tasks.filter((task) => task.status === "IN_PROGRESS").length * 2
143
- + tasks.filter((task) => task.status === "TODO").length;
134
+ await ensureStore(tasksPath);
135
+ const stats = await loadTaskStatusStatsFromStore(tasksPath);
136
+ return {
137
+ exists: true,
138
+ lintSuggestions: [],
139
+ todo: stats.todo,
140
+ inProgress: stats.inProgress,
141
+ blocked: stats.blocked,
142
+ done: stats.done,
143
+ total: stats.total,
144
+ latestUpdatedAt: stats.latestUpdatedAt || "(unknown)",
145
+ score: stats.inProgress * 2 + stats.todo,
146
+ };
144
147
  }
145
148
  async function readRoadmapIds(governanceDir) {
146
- const roadmapPath = path.join(governanceDir, "roadmap.md");
147
- try {
148
- const markdown = await fs.readFile(roadmapPath, "utf-8");
149
- const matches = markdown.match(/ROADMAP-\d{4}/g) ?? [];
150
- return Array.from(new Set(matches));
151
- }
152
- catch {
153
- return [];
154
- }
149
+ const dbPath = path.join(governanceDir, PROJECT_MARKER);
150
+ await ensureStore(dbPath);
151
+ return loadRoadmapIdsFromStore(dbPath);
155
152
  }
156
153
  export async function hasProjectMarker(dirPath) {
157
154
  const markerPath = path.join(dirPath, PROJECT_MARKER);
@@ -248,6 +245,21 @@ export async function discoverProjectsAcrossRoots(rootPaths, maxDepth) {
248
245
  const perRootResults = await Promise.all(rootPaths.map((rootPath) => discoverProjects(rootPath, maxDepth)));
249
246
  return Array.from(new Set(perRootResults.flat())).sort();
250
247
  }
248
+ const DEFAULT_TOOL_TEMPLATE_NAMES = [
249
+ "projectInit",
250
+ "projectScan",
251
+ "projectNext",
252
+ "projectLocate",
253
+ "projectContext",
254
+ "syncViews",
255
+ "taskList",
256
+ "taskNext",
257
+ "taskContext",
258
+ "taskUpdate",
259
+ "roadmapList",
260
+ "roadmapContext",
261
+ "roadmapUpdate",
262
+ ];
251
263
  async function pathExists(targetPath) {
252
264
  const accessResult = await catchIt(fs.access(targetPath));
253
265
  return !accessResult.isError();
@@ -267,51 +279,62 @@ function defaultReadmeMarkdown(governanceDirName) {
267
279
  `This directory (\`${governanceDirName}/\`) is the governance root for this project.`,
268
280
  "",
269
281
  "## Conventions",
270
- "- Keep roadmap/task/design/report files in markdown.",
282
+ "- Keep roadmap/task source of truth in .projitive sqlite tables.",
283
+ "- Treat roadmap.md/tasks.md as generated views from sqlite.",
271
284
  "- Keep IDs stable (TASK-xxxx / ROADMAP-xxxx).",
272
285
  "- Update report evidence before status transitions.",
273
286
  ].join("\n");
274
287
  }
275
- function defaultRoadmapMarkdown() {
276
- return [
277
- "# Roadmap",
278
- "",
279
- "## Active Milestones",
280
- "- [ ] ROADMAP-0001: Bootstrap governance baseline (time: 2026-Q1)",
281
- ].join("\n");
288
+ function defaultRoadmapMarkdown(milestones = defaultRoadmapMilestones()) {
289
+ return renderRoadmapMarkdown(milestones);
282
290
  }
283
- function defaultTasksMarkdown() {
284
- const updatedAt = new Date().toISOString();
285
- return [
286
- "# Tasks",
287
- "",
288
- "<!-- PROJITIVE:TASKS:START -->",
289
- "## TASK-0001 | TODO | Bootstrap governance workspace",
290
- "- owner: unassigned",
291
- "- summary: Create initial governance artifacts and confirm task execution loop.",
292
- `- updatedAt: ${updatedAt}`,
293
- "- roadmapRefs: ROADMAP-0001",
294
- "- links:",
295
- " - (none)",
296
- "<!-- PROJITIVE:TASKS:END -->",
297
- ].join("\n");
291
+ function defaultTasksMarkdown(updatedAt = new Date().toISOString()) {
292
+ return renderTasksMarkdown([
293
+ {
294
+ id: "TASK-0001",
295
+ title: "Bootstrap governance workspace",
296
+ status: "TODO",
297
+ owner: "unassigned",
298
+ summary: "Create initial governance artifacts and confirm task execution loop.",
299
+ updatedAt,
300
+ links: [],
301
+ roadmapRefs: ["ROADMAP-0001"],
302
+ },
303
+ ]);
304
+ }
305
+ function defaultRoadmapMilestones() {
306
+ return [{
307
+ id: "ROADMAP-0001",
308
+ title: "Bootstrap governance baseline",
309
+ status: "active",
310
+ time: "2026-Q1",
311
+ updatedAt: new Date().toISOString(),
312
+ }];
298
313
  }
299
- function defaultNoTaskDiscoveryHookMarkdown() {
314
+ function defaultTemplateReadmeMarkdown() {
300
315
  return [
301
- "Objective:",
302
- "- When no actionable task exists, proactively discover meaningful work and convert it into TODO tasks.",
316
+ "# Template Guide",
317
+ "",
318
+ "This directory stores response templates (one file per tool).",
303
319
  "",
304
- "Checklist:",
305
- "- Check whether code violates project guides/specs; create tasks for each actionable gap.",
306
- "- Check test coverage improvement opportunities; create tasks for high-value missing tests.",
307
- "- Check development/testing workflow bottlenecks; create tasks for reliability and speed improvements.",
308
- "- Check TODO/FIXME/HACK comments; turn feasible items into governed tasks.",
309
- "- Check dependency/security hygiene and stale tooling; create tasks where upgrades are justified.",
320
+ "How to enable:",
321
+ "- Set env `PROJITIVE_MESSAGE_TEMPLATE_PATH` to a template directory.",
322
+ "- Example: .projitive/templates/tools",
310
323
  "",
311
- "Output Format:",
312
- "- Candidate findings (3-10)",
313
- "- Proposed tasks (TASK-xxxx style)",
314
- "- Priority rationale",
324
+ "Rule:",
325
+ "- Prefer one template per tool: <toolName>.md (e.g. taskNext.md).",
326
+ "- Template directory mode only loads <toolName>.md files.",
327
+ "- If a tool template file is missing, Projitive will auto-generate that file before rendering.",
328
+ "- Include {{content}} to render original tool output.",
329
+ "- If {{content}} is missing, original output is appended after template text.",
330
+ "",
331
+ "Basic Variables:",
332
+ "- {{tool_name}}",
333
+ "- {{summary}}",
334
+ "- {{evidence}}",
335
+ "- {{guidance}}",
336
+ "- {{next_call}}",
337
+ "- {{content}}",
315
338
  ].join("\n");
316
339
  }
317
340
  export async function initializeProjectStructure(inputPath, governanceDir, force = false) {
@@ -326,24 +349,49 @@ export async function initializeProjectStructure(inputPath, governanceDir, force
326
349
  }
327
350
  const governancePath = path.join(projectPath, governanceDirName);
328
351
  const directories = [];
329
- const requiredDirectories = [governancePath, path.join(governancePath, "designs"), path.join(governancePath, "reports"), path.join(governancePath, "hooks")];
352
+ const requiredDirectories = [
353
+ governancePath,
354
+ path.join(governancePath, "designs"),
355
+ path.join(governancePath, "reports"),
356
+ path.join(governancePath, "templates"),
357
+ path.join(governancePath, "templates", "tools"),
358
+ ];
330
359
  for (const dirPath of requiredDirectories) {
331
360
  const exists = await pathExists(dirPath);
332
361
  await fs.mkdir(dirPath, { recursive: true });
333
362
  directories.push({ path: dirPath, action: exists ? "skipped" : "created" });
334
363
  }
335
364
  const markerPath = path.join(governancePath, PROJECT_MARKER);
336
- const files = await Promise.all([
337
- writeTextFile(markerPath, "", force),
365
+ const defaultRoadmapData = defaultRoadmapMilestones();
366
+ const defaultTaskUpdatedAt = new Date().toISOString();
367
+ const markerExists = await pathExists(markerPath);
368
+ await ensureStore(markerPath);
369
+ if (force || !markerExists) {
370
+ await replaceRoadmapsInStore(markerPath, defaultRoadmapData);
371
+ await replaceTasksInStore(markerPath, [
372
+ {
373
+ id: "TASK-0001",
374
+ title: "Bootstrap governance workspace",
375
+ status: "TODO",
376
+ owner: "unassigned",
377
+ summary: "Create initial governance artifacts and confirm task execution loop.",
378
+ updatedAt: defaultTaskUpdatedAt,
379
+ links: [],
380
+ roadmapRefs: ["ROADMAP-0001"],
381
+ },
382
+ ]);
383
+ }
384
+ const baseFiles = await Promise.all([
338
385
  writeTextFile(path.join(governancePath, "README.md"), defaultReadmeMarkdown(governanceDirName), force),
339
- writeTextFile(path.join(governancePath, "roadmap.md"), defaultRoadmapMarkdown(), force),
340
- writeTextFile(path.join(governancePath, "tasks.md"), defaultTasksMarkdown(), force),
341
- writeTextFile(path.join(governancePath, "hooks", "task_no_actionable.md"), defaultNoTaskDiscoveryHookMarkdown(), force),
386
+ writeTextFile(path.join(governancePath, "roadmap.md"), defaultRoadmapMarkdown(defaultRoadmapData), force),
387
+ writeTextFile(path.join(governancePath, "tasks.md"), defaultTasksMarkdown(defaultTaskUpdatedAt), force),
388
+ writeTextFile(path.join(governancePath, "templates", "README.md"), defaultTemplateReadmeMarkdown(), force),
342
389
  ]);
390
+ const toolTemplateFiles = await Promise.all(DEFAULT_TOOL_TEMPLATE_NAMES.map((toolName) => writeTextFile(path.join(governancePath, "templates", "tools", `${toolName}.md`), getDefaultToolTemplateMarkdown(toolName), force)));
391
+ const files = [...baseFiles, ...toolTemplateFiles];
343
392
  return {
344
393
  projectPath,
345
394
  governanceDir: governancePath,
346
- markerPath,
347
395
  directories,
348
396
  files,
349
397
  };
@@ -370,7 +418,6 @@ export function registerProjectTools(server) {
370
418
  summarySection([
371
419
  `- projectPath: ${initialized.projectPath}`,
372
420
  `- governanceDir: ${initialized.governanceDir}`,
373
- `- markerPath: ${initialized.markerPath}`,
374
421
  `- force: ${force === true ? "true" : "false"}`,
375
422
  ]),
376
423
  evidenceSection([
@@ -387,8 +434,8 @@ export function registerProjectTools(server) {
387
434
  "- Continue with projectContext and taskList for execution.",
388
435
  ]),
389
436
  lintSection([
390
- "- After init, fill owner/roadmapRefs/links in tasks.md before marking DONE.",
391
- "- Keep task source-of-truth inside marker block only.",
437
+ "- After init, fill owner/roadmapRefs/links in .projitive task table before marking DONE.",
438
+ "- Keep task source-of-truth inside sqlite tables.",
392
439
  ]),
393
440
  nextCallSection(`projectContext(projectPath=\"${initialized.projectPath}\")`),
394
441
  ],
@@ -442,23 +489,18 @@ export function registerProjectTools(server) {
442
489
  const projects = await discoverProjectsAcrossRoots(roots, depth);
443
490
  const snapshots = await Promise.all(projects.map(async (governanceDir) => {
444
491
  const snapshot = await readTasksSnapshot(governanceDir);
445
- const inProgress = snapshot.tasks.filter((task) => task.status === "IN_PROGRESS").length;
446
- const todo = snapshot.tasks.filter((task) => task.status === "TODO").length;
447
- const blocked = snapshot.tasks.filter((task) => task.status === "BLOCKED").length;
448
- const done = snapshot.tasks.filter((task) => task.status === "DONE").length;
449
- const actionable = inProgress + todo;
492
+ const actionable = snapshot.inProgress + snapshot.todo;
450
493
  return {
451
494
  governanceDir,
452
- tasksPath: snapshot.tasksPath,
453
495
  tasksExists: snapshot.exists,
454
496
  lintSuggestions: snapshot.lintSuggestions,
455
- inProgress,
456
- todo,
457
- blocked,
458
- done,
497
+ inProgress: snapshot.inProgress,
498
+ todo: snapshot.todo,
499
+ blocked: snapshot.blocked,
500
+ done: snapshot.done,
459
501
  actionable,
460
- latestUpdatedAt: latestTaskUpdatedAt(snapshot.tasks),
461
- score: actionableScore(snapshot.tasks),
502
+ latestUpdatedAt: snapshot.latestUpdatedAt,
503
+ score: snapshot.score,
462
504
  };
463
505
  }));
464
506
  const ranked = snapshots
@@ -470,6 +512,10 @@ export function registerProjectTools(server) {
470
512
  return b.latestUpdatedAt.localeCompare(a.latestUpdatedAt);
471
513
  })
472
514
  .slice(0, limit ?? 10);
515
+ if (ranked[0]) {
516
+ const topDoc = await loadTasksDocument(ranked[0].governanceDir);
517
+ ranked[0].lintSuggestions = collectTaskLintSuggestions(topDoc.tasks);
518
+ }
473
519
  const markdown = renderToolResponseMarkdown({
474
520
  toolName: "projectNext",
475
521
  sections: [
@@ -483,12 +529,12 @@ export function registerProjectTools(server) {
483
529
  ]),
484
530
  evidenceSection([
485
531
  "- rankedProjects:",
486
- ...ranked.map((item, index) => `${index + 1}. ${item.governanceDir} | actionable=${item.actionable} | in_progress=${item.inProgress} | todo=${item.todo} | blocked=${item.blocked} | done=${item.done} | latest=${item.latestUpdatedAt} | tasksPath=${item.tasksPath}${item.tasksExists ? "" : " (missing)"}`),
532
+ ...ranked.map((item, index) => `${index + 1}. ${item.governanceDir} | actionable=${item.actionable} | in_progress=${item.inProgress} | todo=${item.todo} | blocked=${item.blocked} | done=${item.done} | latest=${item.latestUpdatedAt}${item.tasksExists ? "" : " | store=missing"}`),
487
533
  ]),
488
534
  guidanceSection([
489
535
  "- Pick top 1 project and call `projectContext` with its projectPath.",
490
536
  "- Then call `taskList` and `taskContext` to continue execution.",
491
- "- If `tasksPath` is missing, create tasks.md using project convention before task-level operations.",
537
+ "- If governance store is missing, initialize governance before task-level operations.",
492
538
  ]),
493
539
  lintSection(ranked[0]?.lintSuggestions ?? []),
494
540
  nextCallSection(ranked[0]
@@ -508,7 +554,6 @@ export function registerProjectTools(server) {
508
554
  const resolvedFrom = normalizePath(inputPath);
509
555
  const governanceDir = await resolveGovernanceDir(resolvedFrom);
510
556
  const projectPath = toProjectPath(governanceDir);
511
- const markerPath = path.join(governanceDir, ".projitive");
512
557
  const markdown = renderToolResponseMarkdown({
513
558
  toolName: "projectLocate",
514
559
  sections: [
@@ -516,7 +561,6 @@ export function registerProjectTools(server) {
516
561
  `- resolvedFrom: ${resolvedFrom}`,
517
562
  `- projectPath: ${projectPath}`,
518
563
  `- governanceDir: ${governanceDir}`,
519
- `- markerPath: ${markerPath}`,
520
564
  ]),
521
565
  guidanceSection(["- Call `projectContext` with this projectPath to get task and roadmap summaries."]),
522
566
  lintSection(["- Run `projectContext` to get governance/module lint suggestions for this project."]),
@@ -525,6 +569,61 @@ export function registerProjectTools(server) {
525
569
  });
526
570
  return asText(markdown);
527
571
  });
572
+ server.registerTool("syncViews", {
573
+ title: "Sync Views",
574
+ description: "Materialize markdown views from .projitive sqlite tables (tasks.md / roadmap.md)",
575
+ inputSchema: {
576
+ projectPath: z.string(),
577
+ views: z.array(z.enum(["tasks", "roadmap"])).optional(),
578
+ force: z.boolean().optional(),
579
+ },
580
+ }, async ({ projectPath, views, force }) => {
581
+ const governanceDir = await resolveGovernanceDir(projectPath);
582
+ const dbPath = path.join(governanceDir, PROJECT_MARKER);
583
+ const selectedViews = views && views.length > 0
584
+ ? Array.from(new Set(views))
585
+ : ["tasks", "roadmap"];
586
+ const forceSync = force === true;
587
+ if (forceSync) {
588
+ if (selectedViews.includes("tasks")) {
589
+ await markMarkdownViewDirty(dbPath, "tasks_markdown");
590
+ }
591
+ if (selectedViews.includes("roadmap")) {
592
+ await markMarkdownViewDirty(dbPath, "roadmaps_markdown");
593
+ }
594
+ }
595
+ let taskCount;
596
+ let roadmapCount;
597
+ if (selectedViews.includes("tasks")) {
598
+ const taskDoc = await loadTasksDocumentWithOptions(governanceDir, forceSync);
599
+ taskCount = taskDoc.tasks.length;
600
+ }
601
+ if (selectedViews.includes("roadmap")) {
602
+ const roadmapDoc = await loadRoadmapDocumentWithOptions(governanceDir, forceSync);
603
+ roadmapCount = roadmapDoc.milestones.length;
604
+ }
605
+ const markdown = renderToolResponseMarkdown({
606
+ toolName: "syncViews",
607
+ sections: [
608
+ summarySection([
609
+ `- governanceDir: ${governanceDir}`,
610
+ `- views: ${selectedViews.join(", ")}`,
611
+ `- force: ${forceSync ? "true" : "false"}`,
612
+ ]),
613
+ evidenceSection([
614
+ ...(typeof taskCount === "number" ? [`- tasks.md synced | taskCount=${taskCount}`] : []),
615
+ ...(typeof roadmapCount === "number" ? [`- roadmap.md synced | roadmapCount=${roadmapCount}`] : []),
616
+ ]),
617
+ guidanceSection([
618
+ "Use this tool after batch updates when you need immediate markdown materialization.",
619
+ "Routine workflows can rely on lazy sync and usually do not require force=true.",
620
+ ]),
621
+ lintSection([]),
622
+ nextCallSection(`projectContext(projectPath="${toProjectPath(governanceDir)}")`),
623
+ ],
624
+ });
625
+ return asText(markdown);
626
+ });
528
627
  server.registerTool("projectContext", {
529
628
  title: "Project Context",
530
629
  description: "Get project-level summary before selecting or executing a task",
@@ -535,32 +634,28 @@ export function registerProjectTools(server) {
535
634
  const governanceDir = await resolveGovernanceDir(projectPath);
536
635
  const normalizedProjectPath = toProjectPath(governanceDir);
537
636
  const artifacts = await discoverGovernanceArtifacts(governanceDir);
538
- const { tasksPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
637
+ const dbPath = path.join(governanceDir, PROJECT_MARKER);
638
+ await ensureStore(dbPath);
639
+ const taskStats = await loadTaskStatusStatsFromStore(dbPath);
640
+ const { markdownPath: tasksMarkdownPath, tasks } = await loadTasksDocument(governanceDir);
539
641
  const roadmapIds = await readRoadmapIds(governanceDir);
540
- const lintSuggestions = collectTaskLintSuggestions(tasks, tasksMarkdown);
541
- const taskSummary = {
542
- total: tasks.length,
543
- TODO: tasks.filter((task) => task.status === "TODO").length,
544
- IN_PROGRESS: tasks.filter((task) => task.status === "IN_PROGRESS").length,
545
- BLOCKED: tasks.filter((task) => task.status === "BLOCKED").length,
546
- DONE: tasks.filter((task) => task.status === "DONE").length,
547
- };
642
+ const lintSuggestions = collectTaskLintSuggestions(tasks);
548
643
  const markdown = renderToolResponseMarkdown({
549
644
  toolName: "projectContext",
550
645
  sections: [
551
646
  summarySection([
552
647
  `- projectPath: ${normalizedProjectPath}`,
553
648
  `- governanceDir: ${governanceDir}`,
554
- `- tasksFile: ${tasksPath}`,
649
+ `- tasksView: ${tasksMarkdownPath}`,
555
650
  `- roadmapIds: ${roadmapIds.length}`,
556
651
  ]),
557
652
  evidenceSection([
558
653
  "### Task Summary",
559
- `- total: ${taskSummary.total}`,
560
- `- TODO: ${taskSummary.TODO}`,
561
- `- IN_PROGRESS: ${taskSummary.IN_PROGRESS}`,
562
- `- BLOCKED: ${taskSummary.BLOCKED}`,
563
- `- DONE: ${taskSummary.DONE}`,
654
+ `- total: ${taskStats.total}`,
655
+ `- TODO: ${taskStats.todo}`,
656
+ `- IN_PROGRESS: ${taskStats.inProgress}`,
657
+ `- BLOCKED: ${taskStats.blocked}`,
658
+ `- DONE: ${taskStats.done}`,
564
659
  "",
565
660
  "### Artifacts",
566
661
  renderArtifactsMarkdown(artifacts),
@@ -206,10 +206,13 @@ describe("projitive module", () => {
206
206
  path.join(root, ".projitive", "README.md"),
207
207
  path.join(root, ".projitive", "roadmap.md"),
208
208
  path.join(root, ".projitive", "tasks.md"),
209
- path.join(root, ".projitive", "hooks", "task_no_actionable.md"),
209
+ path.join(root, ".projitive", "templates", "README.md"),
210
+ path.join(root, ".projitive", "templates", "tools", "taskNext.md"),
211
+ path.join(root, ".projitive", "templates", "tools", "taskUpdate.md"),
210
212
  path.join(root, ".projitive", "designs"),
211
213
  path.join(root, ".projitive", "reports"),
212
- path.join(root, ".projitive", "hooks"),
214
+ path.join(root, ".projitive", "templates"),
215
+ path.join(root, ".projitive", "templates", "tools"),
213
216
  ];
214
217
  await Promise.all(expectedPaths.map(async (targetPath) => {
215
218
  await expect(fs.access(targetPath)).resolves.toBeUndefined();
@@ -266,7 +269,7 @@ describe("projitive module", () => {
266
269
  const initialized = await initializeProjectStructure(root);
267
270
  expect(initialized.directories.some(d => d.path.includes("designs"))).toBe(true);
268
271
  expect(initialized.directories.some(d => d.path.includes("reports"))).toBe(true);
269
- expect(initialized.directories.some(d => d.path.includes("hooks"))).toBe(true);
272
+ expect(initialized.directories.some(d => d.path.includes("templates"))).toBe(true);
270
273
  });
271
274
  });
272
275
  describe("utility functions", () => {