@projitive/mcp 2.0.2 → 2.0.4

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 (41) hide show
  1. package/README.md +14 -1
  2. package/output/package.json +8 -2
  3. package/output/source/common/artifacts.js +1 -1
  4. package/output/source/common/artifacts.test.js +11 -11
  5. package/output/source/common/errors.js +19 -19
  6. package/output/source/common/files.js +11 -11
  7. package/output/source/common/files.test.js +14 -14
  8. package/output/source/common/index.js +10 -10
  9. package/output/source/common/linter.js +27 -27
  10. package/output/source/common/linter.test.js +9 -9
  11. package/output/source/common/markdown.js +3 -3
  12. package/output/source/common/markdown.test.js +15 -15
  13. package/output/source/common/response.js +74 -74
  14. package/output/source/common/response.test.js +30 -30
  15. package/output/source/common/store.js +40 -40
  16. package/output/source/common/store.test.js +72 -72
  17. package/output/source/common/types.js +3 -3
  18. package/output/source/common/utils.js +8 -8
  19. package/output/source/index.js +16 -16
  20. package/output/source/index.test.js +64 -64
  21. package/output/source/prompts/index.js +3 -3
  22. package/output/source/prompts/quickStart.js +96 -96
  23. package/output/source/prompts/taskDiscovery.js +184 -180
  24. package/output/source/prompts/taskExecution.js +148 -147
  25. package/output/source/resources/designs.js +26 -26
  26. package/output/source/resources/designs.test.js +88 -88
  27. package/output/source/resources/governance.js +19 -19
  28. package/output/source/resources/index.js +2 -2
  29. package/output/source/resources/readme.js +7 -7
  30. package/output/source/resources/readme.test.js +113 -113
  31. package/output/source/resources/reports.js +10 -10
  32. package/output/source/resources/reports.test.js +83 -83
  33. package/output/source/tools/index.js +3 -3
  34. package/output/source/tools/project.js +196 -191
  35. package/output/source/tools/project.test.js +187 -164
  36. package/output/source/tools/roadmap.js +173 -76
  37. package/output/source/tools/roadmap.test.js +58 -42
  38. package/output/source/tools/task.js +380 -255
  39. package/output/source/tools/task.test.js +117 -110
  40. package/output/source/types.js +22 -22
  41. package/package.json +8 -2
@@ -1,76 +1,76 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- const MESSAGE_TEMPLATE_ENV = "PROJITIVE_MESSAGE_TEMPLATE_PATH";
4
- const CONTENT_TEMPLATE_TOKEN = "{{content}}";
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const MESSAGE_TEMPLATE_ENV = 'PROJITIVE_MESSAGE_TEMPLATE_PATH';
4
+ const CONTENT_TEMPLATE_TOKEN = '{{content}}';
5
5
  function baseToolTemplateMarkdown() {
6
6
  return [
7
- "# {{tool_name}}",
8
- "",
9
- "## Summary",
10
- "{{summary}}",
11
- "",
12
- "## Evidence",
13
- "{{evidence}}",
14
- "",
15
- "## Agent Guidance",
16
- "{{guidance}}",
17
- "",
18
- "## Next Call",
19
- "{{next_call}}",
20
- "",
21
- "## Raw Response",
22
- "{{content}}",
23
- ].join("\n");
7
+ '# {{tool_name}}',
8
+ '',
9
+ '## Summary',
10
+ '{{summary}}',
11
+ '',
12
+ '## Evidence',
13
+ '{{evidence}}',
14
+ '',
15
+ '## Agent Guidance',
16
+ '{{guidance}}',
17
+ '',
18
+ '## Next Call',
19
+ '{{next_call}}',
20
+ '',
21
+ '## Raw Response',
22
+ '{{content}}',
23
+ ].join('\n');
24
24
  }
25
25
  function contextGuideTemplateExtra() {
26
26
  return [
27
- "",
28
- "## Common Tool Guides To Read First",
29
- "- ./CLAUDE.md",
30
- "- ./AGENTS.md",
31
- "- ./.github/copilot-instructions.md",
32
- "- ./.cursorrules",
33
- "- ./.github/instructions/*",
34
- "- ./.cursor/rules/*",
27
+ '',
28
+ '## Common Tool Guides To Read First',
29
+ '- ./CLAUDE.md',
30
+ '- ./AGENTS.md',
31
+ '- ./.github/copilot-instructions.md',
32
+ '- ./.cursorrules',
33
+ '- ./.github/instructions/*',
34
+ '- ./.cursor/rules/*',
35
35
  ];
36
36
  }
37
37
  function idleDiscoveryTemplateExtra() {
38
38
  return [
39
- "",
40
- "## Idle Discovery Checklist (When No Actionable Task)",
41
- "- Scan backlog comments: TODO / FIXME / HACK / XXX.",
42
- "- Check lint gaps and create executable fix tasks.",
43
- "- Check test quality gaps (missing tests, flaky tests, low-value coverage).",
44
- "- Learn current project architecture and consolidate/update design docs in designs/.",
45
- "- Re-run {{tool_name}} after creating 1-3 focused TODO tasks.",
39
+ '',
40
+ '## Idle Discovery Checklist (When No Actionable Task)',
41
+ '- Scan backlog comments: TODO / FIXME / HACK / XXX.',
42
+ '- Check lint gaps and create executable fix tasks.',
43
+ '- Check test quality gaps (missing tests, flaky tests, low-value coverage).',
44
+ '- Learn current project architecture and consolidate/update design docs in designs/.',
45
+ '- Re-run {{tool_name}} after creating 1-3 focused TODO tasks.',
46
46
  ];
47
47
  }
48
48
  function commitReminderTemplateExtra() {
49
49
  return [
50
- "",
51
- "## Commit Reminder",
52
- "- After this update, create a commit to keep progress auditable.",
53
- "- Recommended format: type(scope): summary",
54
- "- Example: feat(task): complete TASK-0007 validation flow",
55
- "- Footer suggestion: Refs: TASK-0007, ROADMAP-0002",
50
+ '',
51
+ '## Commit Reminder',
52
+ '- After this update, create a commit to keep progress auditable.',
53
+ '- Recommended format: type(scope): summary',
54
+ '- Example: feat(task): complete TASK-0007 validation flow',
55
+ '- Footer suggestion: Refs: TASK-0007, ROADMAP-0002',
56
56
  ];
57
57
  }
58
58
  export function getDefaultToolTemplateMarkdown(toolName) {
59
- const base = baseToolTemplateMarkdown().split("\n");
60
- if (toolName === "taskNext") {
61
- return [...base, ...idleDiscoveryTemplateExtra()].join("\n");
59
+ const base = baseToolTemplateMarkdown().split('\n');
60
+ if (toolName === 'taskNext') {
61
+ return [...base, ...idleDiscoveryTemplateExtra()].join('\n');
62
62
  }
63
- if (toolName === "projectContext" || toolName === "taskContext" || toolName === "roadmapContext") {
64
- return [...base, ...contextGuideTemplateExtra()].join("\n");
63
+ if (toolName === 'projectContext' || toolName === 'taskContext' || toolName === 'roadmapContext') {
64
+ return [...base, ...contextGuideTemplateExtra()].join('\n');
65
65
  }
66
- if (toolName === "taskUpdate" || toolName === "roadmapUpdate") {
67
- return [...base, ...commitReminderTemplateExtra()].join("\n");
66
+ if (toolName === 'taskUpdate' || toolName === 'roadmapUpdate') {
67
+ return [...base, ...commitReminderTemplateExtra()].join('\n');
68
68
  }
69
- return base.join("\n");
69
+ return base.join('\n');
70
70
  }
71
71
  function loadTemplateFile(templatePath) {
72
72
  try {
73
- const content = fs.readFileSync(templatePath, "utf-8").trim();
73
+ const content = fs.readFileSync(templatePath, 'utf-8').trim();
74
74
  return content.length > 0 ? content : undefined;
75
75
  }
76
76
  catch {
@@ -84,13 +84,13 @@ function ensureTemplateFile(templatePath, toolName) {
84
84
  }
85
85
  fs.mkdirSync(path.dirname(templatePath), { recursive: true });
86
86
  const generated = getDefaultToolTemplateMarkdown(toolName);
87
- fs.writeFileSync(templatePath, `${generated}\n`, "utf-8");
87
+ fs.writeFileSync(templatePath, `${generated}\n`, 'utf-8');
88
88
  return generated;
89
89
  }
90
90
  function resolveTemplateTarget(toolName) {
91
91
  const configuredPath = process.env[MESSAGE_TEMPLATE_ENV]?.trim();
92
92
  if (!configuredPath) {
93
- return path.resolve(process.cwd(), ".projitive", "templates", "tools", `${toolName}.md`);
93
+ return path.resolve(process.cwd(), '.projitive', 'templates', 'tools', `${toolName}.md`);
94
94
  }
95
95
  const absolutePath = path.resolve(configuredPath);
96
96
  try {
@@ -102,7 +102,7 @@ function resolveTemplateTarget(toolName) {
102
102
  }
103
103
  catch {
104
104
  const ext = path.extname(absolutePath).toLowerCase();
105
- if (ext === ".md") {
105
+ if (ext === '.md') {
106
106
  return absolutePath;
107
107
  }
108
108
  return path.join(absolutePath, `${toolName}.md`);
@@ -114,17 +114,17 @@ function loadMessageTemplate(toolName) {
114
114
  }
115
115
  export function asText(markdown) {
116
116
  return {
117
- content: [{ type: "text", text: markdown }],
117
+ content: [{ type: 'text', text: markdown }],
118
118
  };
119
119
  }
120
120
  function withFallback(lines) {
121
- return lines.length > 0 ? lines : ["- (none)"];
121
+ return lines.length > 0 ? lines : ['- (none)'];
122
122
  }
123
123
  function shouldKeepRawLine(trimmed) {
124
124
  if (trimmed.length === 0) {
125
125
  return true;
126
126
  }
127
- if (trimmed.startsWith("#") || trimmed.startsWith(">") || trimmed.startsWith("```")) {
127
+ if (trimmed.startsWith('#') || trimmed.startsWith('>') || trimmed.startsWith('```')) {
128
128
  return true;
129
129
  }
130
130
  if (/^[-*+]\s/.test(trimmed)) {
@@ -149,25 +149,25 @@ export function section(title, lines) {
149
149
  return { title, lines: normalizeLines(lines) };
150
150
  }
151
151
  export function summarySection(lines) {
152
- return section("Summary", lines);
152
+ return section('Summary', lines);
153
153
  }
154
154
  export function evidenceSection(lines) {
155
- return section("Evidence", lines);
155
+ return section('Evidence', lines);
156
156
  }
157
157
  export function guidanceSection(lines) {
158
- return section("Agent Guidance", lines);
158
+ return section('Agent Guidance', lines);
159
159
  }
160
160
  export function lintSection(lines) {
161
- return section("Lint Suggestions", lines);
161
+ return section('Lint Suggestions', lines);
162
162
  }
163
163
  export function nextCallSection(nextCall) {
164
- return section("Next Call", nextCall ? [nextCall] : []);
164
+ return section('Next Call', nextCall ? [nextCall] : []);
165
165
  }
166
166
  function toSectionText(section) {
167
167
  if (!section) {
168
- return "- (none)";
168
+ return '- (none)';
169
169
  }
170
- return withFallback(section.lines).join("\n");
170
+ return withFallback(section.lines).join('\n');
171
171
  }
172
172
  function resolveSection(payload, title) {
173
173
  return payload.sections.find((item) => item.title === title);
@@ -176,10 +176,10 @@ function buildToolTemplateVariables(payload, classicMarkdown) {
176
176
  return {
177
177
  tool_name: payload.toolName,
178
178
  content: classicMarkdown,
179
- summary: toSectionText(resolveSection(payload, "Summary")),
180
- evidence: toSectionText(resolveSection(payload, "Evidence")),
181
- guidance: toSectionText(resolveSection(payload, "Agent Guidance")),
182
- next_call: toSectionText(resolveSection(payload, "Next Call")),
179
+ summary: toSectionText(resolveSection(payload, 'Summary')),
180
+ evidence: toSectionText(resolveSection(payload, 'Evidence')),
181
+ guidance: toSectionText(resolveSection(payload, 'Agent Guidance')),
182
+ next_call: toSectionText(resolveSection(payload, 'Next Call')),
183
183
  };
184
184
  }
185
185
  function applyTemplateVariables(template, variables) {
@@ -196,13 +196,13 @@ export function renderToolResponseMarkdown(payload) {
196
196
  const body = payload.sections.flatMap((section) => [
197
197
  `## ${section.title}`,
198
198
  ...withFallback(section.lines),
199
- "",
199
+ '',
200
200
  ]);
201
201
  const classicMarkdown = [
202
202
  `# ${payload.toolName}`,
203
- "",
203
+ '',
204
204
  ...body,
205
- ].join("\n").trimEnd();
205
+ ].join('\n').trimEnd();
206
206
  const template = loadMessageTemplate(payload.toolName);
207
207
  const variables = buildToolTemplateVariables(payload, classicMarkdown);
208
208
  return applyTemplateVariables(template, variables);
@@ -211,9 +211,9 @@ export function renderErrorMarkdown(toolName, cause, nextSteps, retryExample) {
211
211
  return renderToolResponseMarkdown({
212
212
  toolName,
213
213
  sections: [
214
- section("Error", [`cause: ${cause}`]),
215
- section("Next Step", nextSteps),
216
- section("Retry Example", [retryExample ?? "(none)"]),
214
+ section('Error', [`cause: ${cause}`]),
215
+ section('Next Step', nextSteps),
216
+ section('Retry Example', [retryExample ?? '(none)']),
217
217
  ],
218
218
  });
219
219
  }
@@ -1,50 +1,50 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { describe, expect, it } from 'vitest';
2
2
  import { asText, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from './response.js';
3
- describe("response helpers", () => {
4
- it("wraps markdown text as MCP text content", () => {
5
- const result = asText("# hello");
6
- expect(result.content).toEqual([{ type: "text", text: "# hello" }]);
3
+ describe('response helpers', () => {
4
+ it('wraps markdown text as MCP text content', () => {
5
+ const result = asText('# hello');
6
+ expect(result.content).toEqual([{ type: 'text', text: '# hello' }]);
7
7
  });
8
- it("renders error markdown sections", () => {
9
- const markdown = renderErrorMarkdown("taskContext", "bad id", ["retry"], "taskContext(...)");
10
- expect(markdown).toContain("# taskContext");
11
- expect(markdown).toContain("## Error");
12
- expect(markdown).toContain("- cause: bad id");
13
- expect(markdown).toContain("- retry");
14
- expect(markdown).toContain("## Retry Example");
8
+ it('renders error markdown sections', () => {
9
+ const markdown = renderErrorMarkdown('taskContext', 'bad id', ['retry'], 'taskContext(...)');
10
+ expect(markdown).toContain('# taskContext');
11
+ expect(markdown).toContain('## Error');
12
+ expect(markdown).toContain('- cause: bad id');
13
+ expect(markdown).toContain('- retry');
14
+ expect(markdown).toContain('## Retry Example');
15
15
  });
16
- it("renders standard tool response sections with fallback", () => {
16
+ it('renders standard tool response sections with fallback', () => {
17
17
  const markdown = renderToolResponseMarkdown({
18
- toolName: "taskList",
18
+ toolName: 'taskList',
19
19
  sections: [
20
- { title: "Summary", lines: ["- governanceDir: /tmp/.projitive"] },
21
- { title: "Evidence", lines: [] },
20
+ { title: 'Summary', lines: ['- governanceDir: /tmp/.projitive'] },
21
+ { title: 'Evidence', lines: [] },
22
22
  ],
23
23
  });
24
- expect(markdown).toContain("# taskList");
25
- expect(markdown).toContain("## Summary");
26
- expect(markdown).toContain("## Evidence");
27
- expect(markdown).toContain("- (none)");
24
+ expect(markdown).toContain('# taskList');
25
+ expect(markdown).toContain('## Summary');
26
+ expect(markdown).toContain('## Evidence');
27
+ expect(markdown).toContain('- (none)');
28
28
  });
29
- it("auto-prefixes plain lines in section helpers", () => {
29
+ it('auto-prefixes plain lines in section helpers', () => {
30
30
  const markdown = renderToolResponseMarkdown({
31
- toolName: "taskList",
31
+ toolName: 'taskList',
32
32
  sections: [
33
- summarySection(["governanceDir: /tmp/.projitive"]),
33
+ summarySection(['governanceDir: /tmp/.projitive']),
34
34
  ],
35
35
  });
36
- expect(markdown).toContain("- governanceDir: /tmp/.projitive");
36
+ expect(markdown).toContain('- governanceDir: /tmp/.projitive');
37
37
  });
38
- it("nextCallSection accepts optional call and falls back when missing", () => {
38
+ it('nextCallSection accepts optional call and falls back when missing', () => {
39
39
  const withCall = renderToolResponseMarkdown({
40
- toolName: "taskList",
41
- sections: [nextCallSection("taskContext(projectPath=\"/tmp\", taskId=\"TASK-0001\")")],
40
+ toolName: 'taskList',
41
+ sections: [nextCallSection('taskContext(projectPath="/tmp", taskId="TASK-0001")')],
42
42
  });
43
- expect(withCall).toContain("- taskContext(projectPath=\"/tmp\", taskId=\"TASK-0001\")");
43
+ expect(withCall).toContain('- taskContext(projectPath="/tmp", taskId="TASK-0001")');
44
44
  const withoutCall = renderToolResponseMarkdown({
45
- toolName: "taskList",
45
+ toolName: 'taskList',
46
46
  sections: [nextCallSection(undefined)],
47
47
  });
48
- expect(withoutCall).toContain("- (none)");
48
+ expect(withoutCall).toContain('- (none)');
49
49
  });
50
50
  });
@@ -1,5 +1,5 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
3
  const STORE_SCHEMA_VERSION = 3;
4
4
  const storeCache = new Map();
5
5
  const storeLocks = new Map();
@@ -8,13 +8,13 @@ function defaultViewState(name) {
8
8
  name,
9
9
  dirty: true,
10
10
  lastSourceVersion: 0,
11
- lastBuiltAt: "",
11
+ lastBuiltAt: '',
12
12
  recordVersion: 1,
13
13
  };
14
14
  }
15
15
  function defaultStore() {
16
16
  return {
17
- schema: "projitive-json-store",
17
+ schema: 'projitive-json-store',
18
18
  tasks: [],
19
19
  roadmaps: [],
20
20
  meta: {
@@ -23,8 +23,8 @@ function defaultStore() {
23
23
  store_schema_version: STORE_SCHEMA_VERSION,
24
24
  },
25
25
  view_state: {
26
- tasks_markdown: defaultViewState("tasks_markdown"),
27
- roadmaps_markdown: defaultViewState("roadmaps_markdown"),
26
+ tasks_markdown: defaultViewState('tasks_markdown'),
27
+ roadmaps_markdown: defaultViewState('roadmaps_markdown'),
28
28
  },
29
29
  migration_history: [],
30
30
  };
@@ -33,24 +33,24 @@ function nowIso() {
33
33
  return new Date().toISOString();
34
34
  }
35
35
  function normalizeTaskStatus(status) {
36
- if (status === "IN_PROGRESS" || status === "BLOCKED" || status === "DONE") {
36
+ if (status === 'IN_PROGRESS' || status === 'BLOCKED' || status === 'DONE') {
37
37
  return status;
38
38
  }
39
- return "TODO";
39
+ return 'TODO';
40
40
  }
41
41
  function normalizeRoadmapStatus(status) {
42
- return status === "done" ? "done" : "active";
42
+ return status === 'done' ? 'done' : 'active';
43
43
  }
44
44
  function normalizeStore(input) {
45
45
  const base = defaultStore();
46
46
  const meta = input.meta ?? {};
47
47
  const tasks = Array.isArray(input.tasks)
48
48
  ? input.tasks.map((task) => ({
49
- id: String(task.id ?? ""),
50
- title: String(task.title ?? ""),
51
- status: normalizeTaskStatus(String(task.status ?? "TODO")),
52
- owner: String(task.owner ?? ""),
53
- summary: String(task.summary ?? ""),
49
+ id: String(task.id ?? ''),
50
+ title: String(task.title ?? ''),
51
+ status: normalizeTaskStatus(String(task.status ?? 'TODO')),
52
+ owner: String(task.owner ?? ''),
53
+ summary: String(task.summary ?? ''),
54
54
  updatedAt: String(task.updatedAt ?? nowIso()),
55
55
  links: Array.isArray(task.links) ? task.links.map((item) => String(item)) : [],
56
56
  roadmapRefs: Array.isArray(task.roadmapRefs) ? task.roadmapRefs.map((item) => String(item)) : [],
@@ -61,10 +61,10 @@ function normalizeStore(input) {
61
61
  : [];
62
62
  const roadmaps = Array.isArray(input.roadmaps)
63
63
  ? input.roadmaps.map((milestone) => ({
64
- id: String(milestone.id ?? ""),
65
- title: String(milestone.title ?? ""),
66
- status: normalizeRoadmapStatus(String(milestone.status ?? "active")),
67
- time: typeof milestone.time === "string" && milestone.time.length > 0 ? milestone.time : undefined,
64
+ id: String(milestone.id ?? ''),
65
+ title: String(milestone.title ?? ''),
66
+ status: normalizeRoadmapStatus(String(milestone.status ?? 'active')),
67
+ time: typeof milestone.time === 'string' && milestone.time.length > 0 ? milestone.time : undefined,
68
68
  updatedAt: String(milestone.updatedAt ?? nowIso()),
69
69
  recordVersion: Number.isFinite(Number(milestone.recordVersion)) ? Number(milestone.recordVersion) : 1,
70
70
  }))
@@ -72,7 +72,7 @@ function normalizeStore(input) {
72
72
  const tasksView = input.view_state?.tasks_markdown;
73
73
  const roadmapsView = input.view_state?.roadmaps_markdown;
74
74
  return {
75
- schema: "projitive-json-store",
75
+ schema: 'projitive-json-store',
76
76
  tasks,
77
77
  roadmaps,
78
78
  meta: {
@@ -83,22 +83,22 @@ function normalizeStore(input) {
83
83
  view_state: {
84
84
  tasks_markdown: {
85
85
  ...base.view_state.tasks_markdown,
86
- dirty: typeof tasksView?.dirty === "boolean" ? tasksView.dirty : base.view_state.tasks_markdown.dirty,
86
+ dirty: typeof tasksView?.dirty === 'boolean' ? tasksView.dirty : base.view_state.tasks_markdown.dirty,
87
87
  lastSourceVersion: Number.isFinite(Number(tasksView?.lastSourceVersion))
88
88
  ? Number(tasksView?.lastSourceVersion)
89
89
  : base.view_state.tasks_markdown.lastSourceVersion,
90
- lastBuiltAt: typeof tasksView?.lastBuiltAt === "string" ? tasksView.lastBuiltAt : base.view_state.tasks_markdown.lastBuiltAt,
90
+ lastBuiltAt: typeof tasksView?.lastBuiltAt === 'string' ? tasksView.lastBuiltAt : base.view_state.tasks_markdown.lastBuiltAt,
91
91
  recordVersion: Number.isFinite(Number(tasksView?.recordVersion))
92
92
  ? Number(tasksView?.recordVersion)
93
93
  : base.view_state.tasks_markdown.recordVersion,
94
94
  },
95
95
  roadmaps_markdown: {
96
96
  ...base.view_state.roadmaps_markdown,
97
- dirty: typeof roadmapsView?.dirty === "boolean" ? roadmapsView.dirty : base.view_state.roadmaps_markdown.dirty,
97
+ dirty: typeof roadmapsView?.dirty === 'boolean' ? roadmapsView.dirty : base.view_state.roadmaps_markdown.dirty,
98
98
  lastSourceVersion: Number.isFinite(Number(roadmapsView?.lastSourceVersion))
99
99
  ? Number(roadmapsView?.lastSourceVersion)
100
100
  : base.view_state.roadmaps_markdown.lastSourceVersion,
101
- lastBuiltAt: typeof roadmapsView?.lastBuiltAt === "string" ? roadmapsView.lastBuiltAt : base.view_state.roadmaps_markdown.lastBuiltAt,
101
+ lastBuiltAt: typeof roadmapsView?.lastBuiltAt === 'string' ? roadmapsView.lastBuiltAt : base.view_state.roadmaps_markdown.lastBuiltAt,
102
102
  recordVersion: Number.isFinite(Number(roadmapsView?.recordVersion))
103
103
  ? Number(roadmapsView?.recordVersion)
104
104
  : base.view_state.roadmaps_markdown.recordVersion,
@@ -111,7 +111,7 @@ async function persistStore(dbPath, store) {
111
111
  await fs.mkdir(path.dirname(dbPath), { recursive: true });
112
112
  const tempPath = `${dbPath}.tmp-${process.pid}-${Date.now()}`;
113
113
  const body = `${JSON.stringify(store, null, 2)}\n`;
114
- await fs.writeFile(tempPath, body, "utf8");
114
+ await fs.writeFile(tempPath, body, 'utf8');
115
115
  await fs.rename(tempPath, dbPath);
116
116
  }
117
117
  async function loadStoreFromDisk(dbPath) {
@@ -119,7 +119,7 @@ async function loadStoreFromDisk(dbPath) {
119
119
  if (!file || file.length === 0) {
120
120
  return { store: defaultStore(), shouldPersist: true };
121
121
  }
122
- const text = file.toString("utf8").trim();
122
+ const text = file.toString('utf8').trim();
123
123
  if (text.length === 0) {
124
124
  return { store: defaultStore(), shouldPersist: true };
125
125
  }
@@ -164,7 +164,7 @@ async function withStoreLock(dbPath, action) {
164
164
  }
165
165
  }
166
166
  function bumpVersionAndDirtyView(store, kind) {
167
- if (kind === "tasks") {
167
+ if (kind === 'tasks') {
168
168
  store.meta.tasks_version += 1;
169
169
  const view = store.view_state.tasks_markdown;
170
170
  view.dirty = true;
@@ -205,9 +205,9 @@ function safeTime(value) {
205
205
  return Number.isFinite(t) ? t : 0;
206
206
  }
207
207
  function normalizeStatusForSort(status) {
208
- if (status === "IN_PROGRESS")
208
+ if (status === 'IN_PROGRESS')
209
209
  return 2;
210
- if (status === "TODO")
210
+ if (status === 'TODO')
211
211
  return 1;
212
212
  return 0;
213
213
  }
@@ -216,7 +216,7 @@ export async function ensureStore(dbPath) {
216
216
  }
217
217
  export async function getStoreVersion(dbPath, kind) {
218
218
  const store = await openStore(dbPath);
219
- return kind === "tasks" ? store.meta.tasks_version : store.meta.roadmaps_version;
219
+ return kind === 'tasks' ? store.meta.tasks_version : store.meta.roadmaps_version;
220
220
  }
221
221
  export async function getMarkdownViewState(dbPath, viewName) {
222
222
  const store = await openStore(dbPath);
@@ -253,20 +253,20 @@ export async function loadTasksFromStore(dbPath) {
253
253
  }
254
254
  export async function loadTaskStatusStatsFromStore(dbPath) {
255
255
  const tasks = await loadTasksFromStore(dbPath);
256
- const todo = tasks.filter((task) => task.status === "TODO").length;
257
- const inProgress = tasks.filter((task) => task.status === "IN_PROGRESS").length;
258
- const blocked = tasks.filter((task) => task.status === "BLOCKED").length;
259
- const done = tasks.filter((task) => task.status === "DONE").length;
256
+ const todo = tasks.filter((task) => task.status === 'TODO').length;
257
+ const inProgress = tasks.filter((task) => task.status === 'IN_PROGRESS').length;
258
+ const blocked = tasks.filter((task) => task.status === 'BLOCKED').length;
259
+ const done = tasks.filter((task) => task.status === 'DONE').length;
260
260
  const total = tasks.length;
261
261
  const latestUpdatedAt = tasks
262
262
  .map((task) => task.updatedAt)
263
- .sort((a, b) => safeTime(b) - safeTime(a))[0] ?? "";
263
+ .sort((a, b) => safeTime(b) - safeTime(a))[0] ?? '';
264
264
  return { todo, inProgress, blocked, done, total, latestUpdatedAt };
265
265
  }
266
266
  export async function loadActionableTasksFromStore(dbPath, limit) {
267
267
  const tasks = await loadTasksFromStore(dbPath);
268
268
  const sorted = tasks
269
- .filter((task) => task.status === "IN_PROGRESS" || task.status === "TODO")
269
+ .filter((task) => task.status === 'IN_PROGRESS' || task.status === 'TODO')
270
270
  .sort((a, b) => {
271
271
  const ap = normalizeStatusForSort(a.status);
272
272
  const bp = normalizeStatusForSort(b.status);
@@ -279,7 +279,7 @@ export async function loadActionableTasksFromStore(dbPath, limit) {
279
279
  }
280
280
  return b.id.localeCompare(a.id);
281
281
  });
282
- if (typeof limit === "number" && Number.isFinite(limit) && limit > 0) {
282
+ if (typeof limit === 'number' && Number.isFinite(limit) && limit > 0) {
283
283
  return sorted.slice(0, Math.floor(limit));
284
284
  }
285
285
  return sorted;
@@ -307,7 +307,7 @@ export async function upsertTaskInStore(dbPath, task) {
307
307
  recordVersion: 1,
308
308
  });
309
309
  }
310
- bumpVersionAndDirtyView(store, "tasks");
310
+ bumpVersionAndDirtyView(store, 'tasks');
311
311
  await persistStore(dbPath, store);
312
312
  });
313
313
  }
@@ -321,7 +321,7 @@ export async function replaceTasksInStore(dbPath, tasks) {
321
321
  roadmapRefs: [...(task.roadmapRefs ?? [])],
322
322
  recordVersion: 1,
323
323
  }));
324
- bumpVersionAndDirtyView(store, "tasks");
324
+ bumpVersionAndDirtyView(store, 'tasks');
325
325
  await persistStore(dbPath, store);
326
326
  });
327
327
  }
@@ -349,7 +349,7 @@ export async function replaceRoadmapsInStore(dbPath, milestones) {
349
349
  status: normalizeRoadmapStatus(milestone.status),
350
350
  recordVersion: 1,
351
351
  }));
352
- bumpVersionAndDirtyView(store, "roadmaps");
352
+ bumpVersionAndDirtyView(store, 'roadmaps');
353
353
  await persistStore(dbPath, store);
354
354
  });
355
355
  }
@@ -372,7 +372,7 @@ export async function upsertRoadmapInStore(dbPath, milestone) {
372
372
  recordVersion: 1,
373
373
  });
374
374
  }
375
- bumpVersionAndDirtyView(store, "roadmaps");
375
+ bumpVersionAndDirtyView(store, 'roadmaps');
376
376
  await persistStore(dbPath, store);
377
377
  });
378
378
  }