@thxgg/steward 0.1.16 → 0.1.18

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 (157) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/{CxHOXVf6.js → -z_Gr0GN.js} +1 -1
  3. package/.output/public/_nuxt/5LlyHjkF.js +60 -0
  4. package/.output/public/_nuxt/BA0u_CRT.js +1 -0
  5. package/.output/public/_nuxt/{Bibm_IDv.js → BA4e9-N5.js} +2 -2
  6. package/.output/public/_nuxt/{BSA0RJ-H.js → BO8EM227.js} +1 -1
  7. package/.output/public/_nuxt/C0XT5P3Q.js +1 -0
  8. package/.output/public/_nuxt/{BWVTacYj.js → CGzrvVc6.js} +1 -1
  9. package/.output/public/_nuxt/{Dum5qplW.js → CJlXUkTg.js} +1 -1
  10. package/.output/public/_nuxt/CZsXZugv.js +1 -0
  11. package/.output/public/_nuxt/{ynmyrfyT.js → C_HVaH3B.js} +1 -1
  12. package/.output/public/_nuxt/{wbjFvimm.js → DAnnHVQP.js} +1 -1
  13. package/.output/public/_nuxt/DEr8q68O.js +141 -0
  14. package/.output/public/_nuxt/Detail.DSyVQNdr.css +1 -0
  15. package/.output/public/_nuxt/DrXxYwWw.js +30 -0
  16. package/.output/public/_nuxt/QAzsKGuP.js +1 -0
  17. package/.output/public/_nuxt/TSsR_oCL.js +1 -0
  18. package/.output/public/_nuxt/WUF6Thhn.js +13 -0
  19. package/.output/public/_nuxt/_prd_.BkpxMFSV.css +1 -0
  20. package/.output/public/_nuxt/builds/latest.json +1 -1
  21. package/.output/public/_nuxt/builds/meta/19e0e040-a531-4c25-b46d-a6ca54a1ae3e.json +1 -0
  22. package/.output/public/_nuxt/i9wn3hS7.js +1 -0
  23. package/.output/server/chunks/_/git-api.mjs +101 -8
  24. package/.output/server/chunks/_/git-api.mjs.map +1 -1
  25. package/.output/server/chunks/_/git.mjs +3 -10
  26. package/.output/server/chunks/_/git.mjs.map +1 -1
  27. package/.output/server/chunks/_/prd-service.mjs +366 -0
  28. package/.output/server/chunks/_/prd-service.mjs.map +1 -0
  29. package/.output/server/chunks/_/repos.mjs +448 -0
  30. package/.output/server/chunks/_/repos.mjs.map +1 -0
  31. package/.output/server/chunks/_/task-graph.mjs +13 -14
  32. package/.output/server/chunks/_/task-graph.mjs.map +1 -1
  33. package/.output/server/chunks/_/watcher.mjs +54 -68
  34. package/.output/server/chunks/_/watcher.mjs.map +1 -1
  35. package/.output/server/chunks/build/{Detail-CUfU85GY.mjs → Detail-BQSkP9Zm.mjs} +170 -74
  36. package/.output/server/chunks/build/Detail-BQSkP9Zm.mjs.map +1 -0
  37. package/.output/server/chunks/build/DiffViewer-styles-1.mjs-BFsE2PCW.mjs +4 -0
  38. package/.output/server/chunks/build/DiffViewer-styles-1.mjs-BFsE2PCW.mjs.map +1 -0
  39. package/.output/server/chunks/build/DiffViewer-styles.D2bqX3nK.mjs +8 -0
  40. package/.output/server/chunks/build/DiffViewer-styles.D2bqX3nK.mjs.map +1 -0
  41. package/.output/server/chunks/build/DiffViewer-styles.FoV36wuV.mjs +10 -0
  42. package/.output/server/chunks/build/DiffViewer-styles.FoV36wuV.mjs.map +1 -0
  43. package/.output/server/chunks/build/Viewer-styles.D6wYWFb1.mjs +8 -0
  44. package/.output/server/chunks/build/Viewer-styles.D6wYWFb1.mjs.map +1 -0
  45. package/.output/server/chunks/build/{_prd_-CeVnQzOV.mjs → _prd_-CBR_wm9i.mjs} +73 -33
  46. package/.output/server/chunks/build/_prd_-CBR_wm9i.mjs.map +1 -0
  47. package/.output/server/chunks/build/client.precomputed.mjs +1 -1
  48. package/.output/server/chunks/build/{default-DWCOHHTE.mjs → default-Cao5eO80.mjs} +2 -3
  49. package/.output/server/chunks/build/default-Cao5eO80.mjs.map +1 -0
  50. package/.output/server/chunks/build/error-404-Bf6kdO80.mjs +0 -1
  51. package/.output/server/chunks/build/error-500-D_bcARXN.mjs +0 -1
  52. package/.output/server/chunks/build/{index-CckL_NBD.mjs → index-ByZO4Bvq.mjs} +2 -2
  53. package/.output/server/chunks/build/index-ByZO4Bvq.mjs.map +1 -0
  54. package/.output/server/chunks/build/{index-QVeSHT3L.mjs → index-ljj9uTXI.mjs} +6 -5
  55. package/.output/server/chunks/build/index-ljj9uTXI.mjs.map +1 -0
  56. package/.output/server/chunks/build/nuxt-link-SvT1nf8Z.mjs +1 -1
  57. package/.output/server/chunks/build/{repo-graph-CHNl58mY.mjs → repo-graph-CVnkmn8i.mjs} +23 -10
  58. package/.output/server/chunks/build/repo-graph-CVnkmn8i.mjs.map +1 -0
  59. package/.output/server/chunks/build/server.mjs +5 -6
  60. package/.output/server/chunks/build/styles.mjs +4 -4
  61. package/.output/server/chunks/build/{usePrd-SqcxGyFU.mjs → usePrd-f7ylhIqs.mjs} +10 -34
  62. package/.output/server/chunks/build/usePrd-f7ylhIqs.mjs.map +1 -0
  63. package/.output/server/chunks/nitro/nitro.mjs +614 -1211
  64. package/.output/server/chunks/nitro/nitro.mjs.map +1 -1
  65. package/.output/server/chunks/routes/api/browse.get.mjs +34 -10
  66. package/.output/server/chunks/routes/api/browse.get.mjs.map +1 -1
  67. package/.output/server/chunks/routes/api/index.get.mjs +3 -2
  68. package/.output/server/chunks/routes/api/index.get.mjs.map +1 -1
  69. package/.output/server/chunks/routes/api/index.post.mjs +22 -7
  70. package/.output/server/chunks/routes/api/index.post.mjs.map +1 -1
  71. package/.output/server/chunks/routes/api/repos/_repoId/git/commits.get.mjs +29 -23
  72. package/.output/server/chunks/routes/api/repos/_repoId/git/commits.get.mjs.map +1 -1
  73. package/.output/server/chunks/routes/api/repos/_repoId/git/diff.get.mjs +12 -7
  74. package/.output/server/chunks/routes/api/repos/_repoId/git/diff.get.mjs.map +1 -1
  75. package/.output/server/chunks/routes/api/repos/_repoId/git/file-content.get.mjs +32 -13
  76. package/.output/server/chunks/routes/api/repos/_repoId/git/file-content.get.mjs.map +1 -1
  77. package/.output/server/chunks/routes/api/repos/_repoId/git/file-diff.get.mjs +14 -14
  78. package/.output/server/chunks/routes/api/repos/_repoId/git/file-diff.get.mjs.map +1 -1
  79. package/.output/server/chunks/routes/api/repos/_repoId/graph.get.mjs +7 -2
  80. package/.output/server/chunks/routes/api/repos/_repoId/graph.get.mjs.map +1 -1
  81. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/graph.get.mjs +16 -2
  82. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/graph.get.mjs.map +1 -1
  83. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/progress.get.mjs +21 -9
  84. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/progress.get.mjs.map +1 -1
  85. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks/_taskId/commits.get.mjs +21 -85
  86. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks/_taskId/commits.get.mjs.map +1 -1
  87. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks.get.mjs +21 -9
  88. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks.get.mjs.map +1 -1
  89. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug_.get.mjs +31 -50
  90. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug_.get.mjs.map +1 -1
  91. package/.output/server/chunks/routes/api/repos/_repoId/prds.get.mjs +20 -49
  92. package/.output/server/chunks/routes/api/repos/_repoId/prds.get.mjs.map +1 -1
  93. package/.output/server/chunks/routes/api/repos/_repoId/refresh-git-repos.post.mjs +6 -13
  94. package/.output/server/chunks/routes/api/repos/_repoId/refresh-git-repos.post.mjs.map +1 -1
  95. package/.output/server/chunks/routes/api/repos/_repoId_.delete.mjs +3 -2
  96. package/.output/server/chunks/routes/api/repos/_repoId_.delete.mjs.map +1 -1
  97. package/.output/server/chunks/routes/api/runtime.get.mjs +1 -2
  98. package/.output/server/chunks/routes/api/runtime.get.mjs.map +1 -1
  99. package/.output/server/chunks/routes/api/watch.get.mjs +5 -4
  100. package/.output/server/chunks/routes/api/watch.get.mjs.map +1 -1
  101. package/.output/server/chunks/routes/renderer.mjs +1 -1
  102. package/.output/server/index.mjs +1 -2
  103. package/.output/server/index.mjs.map +1 -1
  104. package/.output/server/node_modules/zod/index.js +4 -0
  105. package/.output/server/node_modules/zod/package.json +118 -0
  106. package/.output/server/node_modules/zod/v3/ZodError.js +133 -0
  107. package/.output/server/node_modules/zod/v3/errors.js +9 -0
  108. package/.output/server/node_modules/zod/v3/external.js +6 -0
  109. package/.output/server/node_modules/zod/v3/helpers/errorUtil.js +6 -0
  110. package/.output/server/node_modules/zod/v3/helpers/parseUtil.js +109 -0
  111. package/.output/server/node_modules/zod/v3/helpers/typeAliases.js +1 -0
  112. package/.output/server/node_modules/zod/v3/helpers/util.js +133 -0
  113. package/.output/server/node_modules/zod/v3/locales/en.js +109 -0
  114. package/.output/server/node_modules/zod/v3/types.js +3693 -0
  115. package/.output/server/package.json +2 -1
  116. package/README.md +2 -2
  117. package/dist/host/src/api/prds.js +6 -172
  118. package/dist/host/src/api/repos.js +3 -18
  119. package/dist/host/src/api/state.js +8 -9
  120. package/dist/host/src/executor-runner.js +368 -0
  121. package/dist/host/src/executor.js +138 -260
  122. package/dist/host/src/mcp.js +27 -1
  123. package/dist/host/src/ui.js +14 -4
  124. package/dist/server/utils/change-events.js +33 -0
  125. package/dist/server/utils/git.js +11 -16
  126. package/dist/server/utils/prd-service.js +231 -0
  127. package/dist/server/utils/prd-state.js +54 -162
  128. package/dist/server/utils/repos.js +72 -17
  129. package/dist/server/utils/state-schema.js +61 -0
  130. package/dist/server/utils/task-graph.js +13 -13
  131. package/package.json +2 -1
  132. package/.output/public/_nuxt/CVJh28bx.js +0 -1
  133. package/.output/public/_nuxt/CyZuidLG.js +0 -60
  134. package/.output/public/_nuxt/D0op9E2g.js +0 -1
  135. package/.output/public/_nuxt/DX8awZaa.js +0 -1
  136. package/.output/public/_nuxt/Detail.z33AHKev.css +0 -1
  137. package/.output/public/_nuxt/DiTJUZOC.js +0 -1
  138. package/.output/public/_nuxt/T_3JE9C-.js +0 -1
  139. package/.output/public/_nuxt/WOI2tLsR.js +0 -42
  140. package/.output/public/_nuxt/_prd_.KTotLoF_.css +0 -1
  141. package/.output/public/_nuxt/builds/meta/029070b0-b8e2-4988-84f4-d0c9ff55c998.json +0 -1
  142. package/.output/public/_nuxt/odRGDGwj.js +0 -1
  143. package/.output/server/chunks/build/Detail-CUfU85GY.mjs.map +0 -1
  144. package/.output/server/chunks/build/DiffViewer-styles-1.mjs-CS8FTppg.mjs +0 -4
  145. package/.output/server/chunks/build/DiffViewer-styles-1.mjs-CS8FTppg.mjs.map +0 -1
  146. package/.output/server/chunks/build/DiffViewer-styles.AUfwwelI.mjs +0 -10
  147. package/.output/server/chunks/build/DiffViewer-styles.AUfwwelI.mjs.map +0 -1
  148. package/.output/server/chunks/build/DiffViewer-styles.D_it8zfk.mjs +0 -8
  149. package/.output/server/chunks/build/DiffViewer-styles.D_it8zfk.mjs.map +0 -1
  150. package/.output/server/chunks/build/Viewer-styles.CshnetGw.mjs +0 -8
  151. package/.output/server/chunks/build/Viewer-styles.CshnetGw.mjs.map +0 -1
  152. package/.output/server/chunks/build/_prd_-CeVnQzOV.mjs.map +0 -1
  153. package/.output/server/chunks/build/default-DWCOHHTE.mjs.map +0 -1
  154. package/.output/server/chunks/build/index-CckL_NBD.mjs.map +0 -1
  155. package/.output/server/chunks/build/index-QVeSHT3L.mjs.map +0 -1
  156. package/.output/server/chunks/build/repo-graph-CHNl58mY.mjs.map +0 -1
  157. package/.output/server/chunks/build/usePrd-SqcxGyFU.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thxgg/steward-prod",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "dependencies": {
@@ -76,6 +76,7 @@
76
76
  "vue-demi": "0.14.10",
77
77
  "vue-router": "4.6.4",
78
78
  "vue-sonner": "1.3.2",
79
+ "zod": "3.25.76",
79
80
  "zwitch": "2.0.4"
80
81
  }
81
82
  }
package/README.md CHANGED
@@ -143,8 +143,8 @@ Detailed API docs and examples: `docs/MCP.md`
143
143
 
144
144
  Steward reads local filesystem and git metadata by design.
145
145
 
146
- - Run only on trusted local machines
147
- - Do not expose directly to the public internet
146
+ - UI/API accept loopback requests only
147
+ - Non-loopback requests are rejected
148
148
  - Treat as a workstation tool, not a hosted multi-user service
149
149
 
150
150
  ## Storage
@@ -1,190 +1,24 @@
1
- import { promises as fs } from 'node:fs';
2
- import { basename, join } from 'node:path';
3
- import { resolveCommitRepo } from '../../../server/utils/git.js';
4
- import { discoverGitRepos, getRepos, saveRepos } from '../../../server/utils/repos.js';
5
- import { getPrdState, getPrdStateSummaries, migrateLegacyStateForRepo } from '../../../server/utils/prd-state.js';
1
+ import { listPrdDocuments, readPrdDocument, readPrdProgress, readPrdTasks, resolveTaskCommits } from '../../../server/utils/prd-service.js';
6
2
  import { requireRepo } from './repo-context.js';
7
- function parseMetadata(content) {
8
- const metadata = {};
9
- const authorMatch = content.match(/\*{0,2}Author\*{0,2}:\*{0,2}\s*(.+?)(?:\n|$)/i);
10
- if (authorMatch && authorMatch[1]) {
11
- metadata.author = authorMatch[1].trim();
12
- }
13
- const dateMatch = content.match(/\*{0,2}Date\*{0,2}:\*{0,2}\s*(.+?)(?:\n|$)/i);
14
- if (dateMatch && dateMatch[1]) {
15
- metadata.date = dateMatch[1].trim();
16
- }
17
- const statusMatch = content.match(/\*{0,2}Status\*{0,2}:\*{0,2}\s*(.+?)(?:\n|$)/i);
18
- if (statusMatch && statusMatch[1]) {
19
- metadata.status = statusMatch[1].trim();
20
- }
21
- const shortcutLinkMatch = content.match(/\[([Ss][Cc]-\d+)\]\(([^)]+)\)/);
22
- if (shortcutLinkMatch && shortcutLinkMatch[1] && shortcutLinkMatch[2]) {
23
- metadata.shortcutStory = shortcutLinkMatch[1];
24
- metadata.shortcutUrl = shortcutLinkMatch[2];
25
- }
26
- else {
27
- const shortcutIdMatch = content.match(/\*{0,2}Shortcut(?:\s+Story)?\*{0,2}:\*{0,2}\s*([Ss][Cc]-\d+)/i);
28
- if (shortcutIdMatch && shortcutIdMatch[1]) {
29
- metadata.shortcutStory = shortcutIdMatch[1];
30
- }
31
- }
32
- return metadata;
33
- }
34
- async function readPrdFile(repo, prdSlug) {
35
- const prdPath = join(repo.path, 'docs', 'prd', `${prdSlug}.md`);
36
- try {
37
- return await fs.readFile(prdPath, 'utf-8');
38
- }
39
- catch {
40
- throw new Error('PRD not found');
41
- }
42
- }
43
3
  export const prds = {
44
4
  async list(repoId) {
45
5
  const repo = await requireRepo(repoId);
46
- await migrateLegacyStateForRepo(repo);
47
- const prdDir = join(repo.path, 'docs', 'prd');
48
- let prdFiles = [];
49
- try {
50
- const files = await fs.readdir(prdDir);
51
- prdFiles = files.filter((file) => file.endsWith('.md'));
52
- }
53
- catch {
54
- return [];
55
- }
56
- const stateSummaries = await getPrdStateSummaries(repo.id);
57
- const items = await Promise.all(prdFiles.map(async (filename) => {
58
- const slug = basename(filename, '.md');
59
- const filePath = join(prdDir, filename);
60
- let name = slug;
61
- let modifiedAt = 0;
62
- try {
63
- const [content, stat] = await Promise.all([
64
- fs.readFile(filePath, 'utf-8'),
65
- fs.stat(filePath)
66
- ]);
67
- modifiedAt = stat.mtime.getTime();
68
- const h1Match = content.match(/^#\s+(.+)$/m);
69
- if (h1Match && h1Match[1]) {
70
- name = h1Match[1].trim();
71
- }
72
- }
73
- catch {
74
- // Keep default values when a file cannot be read.
75
- }
76
- const stateSummary = stateSummaries.get(slug);
77
- return {
78
- slug,
79
- name,
80
- source: `docs/prd/${filename}`,
81
- hasState: !!stateSummary?.hasState,
82
- modifiedAt,
83
- ...(stateSummary?.taskCount !== undefined && { taskCount: stateSummary.taskCount }),
84
- ...(stateSummary?.completedCount !== undefined && { completedCount: stateSummary.completedCount })
85
- };
86
- }));
87
- items.sort((a, b) => b.modifiedAt - a.modifiedAt);
88
- return items;
6
+ return await listPrdDocuments(repo);
89
7
  },
90
8
  async getDocument(repoId, prdSlug) {
91
9
  const repo = await requireRepo(repoId);
92
- const content = await readPrdFile(repo, prdSlug);
93
- let name = prdSlug;
94
- const h1Match = content.match(/^#\s+(.+)$/m);
95
- if (h1Match && h1Match[1]) {
96
- name = h1Match[1].trim();
97
- }
98
- return {
99
- slug: prdSlug,
100
- name,
101
- content,
102
- metadata: parseMetadata(content)
103
- };
10
+ return await readPrdDocument(repo, prdSlug);
104
11
  },
105
12
  async getTasks(repoId, prdSlug) {
106
13
  const repo = await requireRepo(repoId);
107
- await migrateLegacyStateForRepo(repo);
108
- const state = await getPrdState(repo.id, prdSlug);
109
- return state?.tasks ?? null;
14
+ return await readPrdTasks(repo, prdSlug);
110
15
  },
111
16
  async getProgress(repoId, prdSlug) {
112
17
  const repo = await requireRepo(repoId);
113
- await migrateLegacyStateForRepo(repo);
114
- const state = await getPrdState(repo.id, prdSlug);
115
- return state?.progress ?? null;
18
+ return await readPrdProgress(repo, prdSlug);
116
19
  },
117
20
  async getTaskCommits(repoId, prdSlug, taskId) {
118
21
  const repo = await requireRepo(repoId);
119
- await migrateLegacyStateForRepo(repo);
120
- const state = await getPrdState(repo.id, prdSlug);
121
- const progress = state?.progress ?? null;
122
- if (!progress) {
123
- return [];
124
- }
125
- const taskLogs = Array.isArray(progress.taskLogs) ? progress.taskLogs : [];
126
- const taskLog = taskLogs.find((log) => log.taskId === taskId);
127
- if (!taskLog) {
128
- return [];
129
- }
130
- if (!taskLog.commits || taskLog.commits.length === 0) {
131
- return [];
132
- }
133
- const resolvedCommits = [];
134
- const failedEntries = [];
135
- for (const commitEntry of taskLog.commits) {
136
- try {
137
- const resolved = await resolveCommitRepo(repo, commitEntry);
138
- resolvedCommits.push({
139
- sha: resolved.sha,
140
- repo: resolved.repoPath
141
- });
142
- }
143
- catch {
144
- failedEntries.push(commitEntry);
145
- }
146
- }
147
- if (failedEntries.length > 0) {
148
- const newGitRepos = await discoverGitRepos(repo.path);
149
- const existingPaths = new Set((repo.gitRepos || []).map((gitRepo) => gitRepo.relativePath));
150
- const hasNewRepos = newGitRepos.some((gitRepo) => !existingPaths.has(gitRepo.relativePath));
151
- let resolvedWithUpdatedRepo = false;
152
- if (hasNewRepos) {
153
- const allRepos = await getRepos();
154
- const repoIndex = allRepos.findIndex((candidate) => candidate.id === repoId);
155
- if (repoIndex !== -1) {
156
- const updatedRepo = {
157
- ...allRepos[repoIndex],
158
- gitRepos: newGitRepos.length > 0 ? newGitRepos : undefined
159
- };
160
- allRepos[repoIndex] = updatedRepo;
161
- await saveRepos(allRepos);
162
- resolvedWithUpdatedRepo = true;
163
- for (const commitEntry of failedEntries) {
164
- try {
165
- const resolved = await resolveCommitRepo(updatedRepo, commitEntry);
166
- resolvedCommits.push({
167
- sha: resolved.sha,
168
- repo: resolved.repoPath
169
- });
170
- }
171
- catch {
172
- const sha = typeof commitEntry === 'string' ? commitEntry : commitEntry.sha;
173
- resolvedCommits.push({ sha, repo: '' });
174
- }
175
- }
176
- }
177
- }
178
- if (!resolvedWithUpdatedRepo) {
179
- for (const commitEntry of failedEntries) {
180
- const sha = typeof commitEntry === 'string' ? commitEntry : commitEntry.sha;
181
- resolvedCommits.push({
182
- sha,
183
- repo: ''
184
- });
185
- }
186
- }
187
- }
188
- return resolvedCommits;
22
+ return await resolveTaskCommits(repo, prdSlug, taskId);
189
23
  }
190
24
  };
@@ -1,5 +1,4 @@
1
- import { addRepo, discoverGitRepos, getRepoById, getRepos, removeRepo, saveRepos, validateRepoPath } from '../../../server/utils/repos.js';
2
- import { migrateLegacyStateForRepo } from '../../../server/utils/prd-state.js';
1
+ import { addRepo, discoverGitRepos, getRepoById, getRepos, removeRepo, updateRepoGitRepos, validateRepoPath } from '../../../server/utils/repos.js';
3
2
  import { requireCurrentRepo, requireRepo } from './repo-context.js';
4
3
  export const repos = {
5
4
  async list() {
@@ -17,7 +16,6 @@ export const repos = {
17
16
  throw new Error(validation.error || 'Invalid repository path');
18
17
  }
19
18
  const repo = await addRepo(path, name);
20
- await migrateLegacyStateForRepo(repo);
21
19
  return repo;
22
20
  },
23
21
  async remove(repoId) {
@@ -29,22 +27,9 @@ export const repos = {
29
27
  return { removed: true };
30
28
  },
31
29
  async refreshGitRepos(repoId) {
32
- await requireRepo(repoId);
33
- const allRepos = await getRepos();
34
- const repoIndex = allRepos.findIndex((repo) => repo.id === repoId);
35
- if (repoIndex === -1) {
36
- throw new Error('Repository not found');
37
- }
38
- const repo = allRepos[repoIndex];
30
+ const repo = await requireRepo(repoId);
39
31
  const gitRepos = await discoverGitRepos(repo.path);
40
- if (gitRepos.length > 0) {
41
- repo.gitRepos = gitRepos;
42
- }
43
- else {
44
- delete repo.gitRepos;
45
- }
46
- allRepos[repoIndex] = repo;
47
- await saveRepos(allRepos);
32
+ await updateRepoGitRepos(repo.id, gitRepos.length > 0 ? gitRepos : undefined);
48
33
  return {
49
34
  discovered: gitRepos.length,
50
35
  gitRepos
@@ -1,9 +1,14 @@
1
- import { getPrdState, getPrdStateSummaries, migrateLegacyStateForRepo, upsertPrdState } from '../../../server/utils/prd-state.js';
1
+ import { getPrdState, getPrdStateSummaries, upsertPrdState } from '../../../server/utils/prd-state.js';
2
+ import { parseProgressFile, parseTasksFile } from '../../../server/utils/state-schema.js';
2
3
  import { requireCurrentRepo, requireRepo, requireRepoByPath } from './repo-context.js';
3
4
  function mapStateUpdate(payload) {
4
5
  return {
5
- ...(payload.tasks !== undefined && { tasks: payload.tasks }),
6
- ...(payload.progress !== undefined && { progress: payload.progress }),
6
+ ...(payload.tasks !== undefined && {
7
+ tasks: payload.tasks === null ? null : parseTasksFile(payload.tasks)
8
+ }),
9
+ ...(payload.progress !== undefined && {
10
+ progress: payload.progress === null ? null : parseProgressFile(payload.progress)
11
+ }),
7
12
  ...(payload.notes !== undefined && { notes: payload.notes })
8
13
  };
9
14
  }
@@ -13,34 +18,28 @@ function mapSummaryMap(summaries) {
13
18
  export const state = {
14
19
  async get(repoId, slug) {
15
20
  const repo = await requireRepo(repoId);
16
- await migrateLegacyStateForRepo(repo);
17
21
  return await getPrdState(repo.id, slug);
18
22
  },
19
23
  async getByPath(repoPath, slug) {
20
24
  const repo = await requireRepoByPath(repoPath);
21
- await migrateLegacyStateForRepo(repo);
22
25
  return await getPrdState(repo.id, slug);
23
26
  },
24
27
  async getCurrent(slug) {
25
28
  const repo = await requireCurrentRepo();
26
- await migrateLegacyStateForRepo(repo);
27
29
  return await getPrdState(repo.id, slug);
28
30
  },
29
31
  async summaries(repoId) {
30
32
  const repo = await requireRepo(repoId);
31
- await migrateLegacyStateForRepo(repo);
32
33
  const summaries = await getPrdStateSummaries(repo.id);
33
34
  return mapSummaryMap(summaries);
34
35
  },
35
36
  async summariesByPath(repoPath) {
36
37
  const repo = await requireRepoByPath(repoPath);
37
- await migrateLegacyStateForRepo(repo);
38
38
  const summaries = await getPrdStateSummaries(repo.id);
39
39
  return mapSummaryMap(summaries);
40
40
  },
41
41
  async summariesCurrent() {
42
42
  const repo = await requireCurrentRepo();
43
- await migrateLegacyStateForRepo(repo);
44
43
  const summaries = await getPrdStateSummaries(repo.id);
45
44
  return mapSummaryMap(summaries);
46
45
  },
@@ -0,0 +1,368 @@
1
+ import vm from 'node:vm';
2
+ import { git, prds, repos, state } from './api/index.js';
3
+ import { getStewardHelp } from './help.js';
4
+ const MAX_OUTPUT_SIZE = 50_000;
5
+ const EXECUTION_TIMEOUT_MS = Number.parseInt(process.env.STEWARD_EXECUTION_TIMEOUT_MS || '30000', 10);
6
+ const MAX_TIMERS = 100;
7
+ const MAX_LOG_ENTRIES = 200;
8
+ const MAX_LOG_OUTPUT_SIZE = 20_000;
9
+ const MAX_LOG_ENTRY_SIZE = 2_000;
10
+ class ExecutionError extends Error {
11
+ options;
12
+ constructor(message, options) {
13
+ super(message);
14
+ this.options = options;
15
+ this.name = 'ExecutionError';
16
+ }
17
+ }
18
+ function deepFreeze(value) {
19
+ if (!value || (typeof value !== 'object' && typeof value !== 'function') || Object.isFrozen(value)) {
20
+ return value;
21
+ }
22
+ for (const key of Reflect.ownKeys(value)) {
23
+ const property = value[key];
24
+ if (property && (typeof property === 'object' || typeof property === 'function')) {
25
+ deepFreeze(property);
26
+ }
27
+ }
28
+ return Object.freeze(value);
29
+ }
30
+ function safeJsonStringify(value) {
31
+ const seen = new WeakSet();
32
+ try {
33
+ return JSON.stringify(value, (_key, currentValue) => {
34
+ if (typeof currentValue === 'bigint') {
35
+ return `${currentValue}n`;
36
+ }
37
+ if (typeof currentValue === 'function') {
38
+ const functionName = currentValue.name ? ` ${currentValue.name}` : '';
39
+ return `[Function${functionName}]`;
40
+ }
41
+ if (typeof currentValue === 'symbol') {
42
+ return currentValue.toString();
43
+ }
44
+ if (typeof currentValue === 'object' && currentValue !== null) {
45
+ if (seen.has(currentValue)) {
46
+ return '[Circular]';
47
+ }
48
+ seen.add(currentValue);
49
+ }
50
+ return currentValue;
51
+ });
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ }
57
+ function formatLogValue(value) {
58
+ if (typeof value === 'string') {
59
+ return value;
60
+ }
61
+ const json = safeJsonStringify(value);
62
+ if (json !== undefined) {
63
+ return json;
64
+ }
65
+ return String(value);
66
+ }
67
+ function truncateResult(result) {
68
+ if (result === undefined) {
69
+ return {
70
+ result: null,
71
+ truncatedResult: false,
72
+ resultWasUndefined: true
73
+ };
74
+ }
75
+ const json = safeJsonStringify(result);
76
+ if (json === undefined) {
77
+ return {
78
+ result: {
79
+ _unserializable: true,
80
+ preview: String(result)
81
+ },
82
+ truncatedResult: false,
83
+ resultWasUndefined: false
84
+ };
85
+ }
86
+ if (json.length <= MAX_OUTPUT_SIZE) {
87
+ return {
88
+ result,
89
+ truncatedResult: false,
90
+ resultWasUndefined: false
91
+ };
92
+ }
93
+ return {
94
+ result: {
95
+ _truncated: true,
96
+ size: json.length,
97
+ preview: json.slice(0, MAX_OUTPUT_SIZE),
98
+ message: `Output truncated (${json.length} chars, showing first ${MAX_OUTPUT_SIZE})`
99
+ },
100
+ truncatedResult: true,
101
+ resultWasUndefined: false
102
+ };
103
+ }
104
+ function normalizeFailure(error) {
105
+ if (error instanceof ExecutionError) {
106
+ return {
107
+ code: error.options?.code || 'EXECUTION_ERROR',
108
+ message: error.message,
109
+ ...(error.options?.stackTrace && { stack: error.options.stackTrace }),
110
+ ...(error.options?.details !== undefined && { details: error.options.details })
111
+ };
112
+ }
113
+ if (error instanceof Error) {
114
+ const { code, details } = error;
115
+ return {
116
+ code: typeof code === 'string' ? code : 'EXECUTION_ERROR',
117
+ message: error.message,
118
+ ...(error.stack && { stack: error.stack }),
119
+ ...(details !== undefined && { details })
120
+ };
121
+ }
122
+ return {
123
+ code: 'EXECUTION_ERROR',
124
+ message: String(error)
125
+ };
126
+ }
127
+ async function execute(code) {
128
+ const startedAt = Date.now();
129
+ const logs = [];
130
+ let totalLogChars = 0;
131
+ let logsTruncated = false;
132
+ const appendLog = (level, args) => {
133
+ if (logs.length >= MAX_LOG_ENTRIES) {
134
+ logsTruncated = true;
135
+ return;
136
+ }
137
+ let message = args.map(formatLogValue).join(' ');
138
+ if (message.length > MAX_LOG_ENTRY_SIZE) {
139
+ message = `${message.slice(0, MAX_LOG_ENTRY_SIZE)}...`;
140
+ logsTruncated = true;
141
+ }
142
+ if (totalLogChars + message.length > MAX_LOG_OUTPUT_SIZE) {
143
+ logsTruncated = true;
144
+ return;
145
+ }
146
+ totalLogChars += message.length;
147
+ logs.push({
148
+ level,
149
+ message,
150
+ timestamp: new Date().toISOString()
151
+ });
152
+ };
153
+ const buildEnvelope = (params) => ({
154
+ ok: params.ok,
155
+ result: params.result,
156
+ logs,
157
+ error: params.error,
158
+ meta: {
159
+ timeoutMs: EXECUTION_TIMEOUT_MS,
160
+ durationMs: Date.now() - startedAt,
161
+ truncatedResult: params.truncatedResult,
162
+ truncatedLogs: logsTruncated,
163
+ resultWasUndefined: params.resultWasUndefined
164
+ }
165
+ });
166
+ if (!code || !code.trim()) {
167
+ const error = normalizeFailure(new ExecutionError('Code cannot be empty', { code: 'EMPTY_CODE' }));
168
+ return buildEnvelope({
169
+ ok: false,
170
+ result: null,
171
+ error,
172
+ truncatedResult: false,
173
+ resultWasUndefined: false
174
+ });
175
+ }
176
+ const timers = new Set();
177
+ let executionTimeout = null;
178
+ let asyncCallbackError = null;
179
+ const wrapTimerHandler = (handler) => {
180
+ return () => {
181
+ try {
182
+ handler();
183
+ }
184
+ catch (error) {
185
+ const normalizedError = error instanceof Error
186
+ ? error
187
+ : new Error(String(error));
188
+ asyncCallbackError = normalizedError;
189
+ appendLog('error', ['Timer callback error:', normalizedError.message]);
190
+ }
191
+ };
192
+ };
193
+ const ensureTimerHandler = (handler) => {
194
+ if (typeof handler !== 'function') {
195
+ throw new ExecutionError('Timer handler must be a function', {
196
+ code: 'INVALID_TIMER_HANDLER'
197
+ });
198
+ }
199
+ return wrapTimerHandler(handler);
200
+ };
201
+ const apiSurface = deepFreeze({
202
+ repos,
203
+ prds,
204
+ git,
205
+ state,
206
+ steward: {
207
+ help: () => getStewardHelp()
208
+ }
209
+ });
210
+ const sandbox = {
211
+ ...apiSurface,
212
+ console: deepFreeze({
213
+ log: (...args) => appendLog('log', args),
214
+ info: (...args) => appendLog('info', args),
215
+ warn: (...args) => appendLog('warn', args),
216
+ error: (...args) => appendLog('error', args)
217
+ }),
218
+ setTimeout: (handler, timeout) => {
219
+ if (timers.size >= MAX_TIMERS) {
220
+ throw new ExecutionError(`Timer limit exceeded (max ${MAX_TIMERS})`, {
221
+ code: 'TIMER_LIMIT'
222
+ });
223
+ }
224
+ const wrappedHandler = ensureTimerHandler(handler);
225
+ const timer = setTimeout(() => {
226
+ timers.delete(timer);
227
+ wrappedHandler();
228
+ }, timeout);
229
+ timers.add(timer);
230
+ return timer;
231
+ },
232
+ clearTimeout: (timer) => {
233
+ timers.delete(timer);
234
+ clearTimeout(timer);
235
+ },
236
+ setInterval: (handler, timeout) => {
237
+ if (timers.size >= MAX_TIMERS) {
238
+ throw new ExecutionError(`Timer limit exceeded (max ${MAX_TIMERS})`, {
239
+ code: 'TIMER_LIMIT'
240
+ });
241
+ }
242
+ const wrappedHandler = ensureTimerHandler(handler);
243
+ const timer = setInterval(wrappedHandler, timeout);
244
+ timers.add(timer);
245
+ return timer;
246
+ },
247
+ clearInterval: (timer) => {
248
+ timers.delete(timer);
249
+ clearInterval(timer);
250
+ },
251
+ Promise
252
+ };
253
+ const wrappedCode = `
254
+ (async () => {
255
+ ${code}
256
+ })()
257
+ `;
258
+ try {
259
+ const script = new vm.Script(wrappedCode, {
260
+ filename: 'codemode.js'
261
+ });
262
+ const context = vm.createContext(sandbox);
263
+ const executionPromise = Promise.resolve(script.runInContext(context, {
264
+ timeout: EXECUTION_TIMEOUT_MS
265
+ }));
266
+ const timeoutPromise = new Promise((_resolve, reject) => {
267
+ executionTimeout = setTimeout(() => {
268
+ reject(new ExecutionError(`Execution timed out after ${EXECUTION_TIMEOUT_MS}ms`, {
269
+ code: 'TIMEOUT'
270
+ }));
271
+ }, EXECUTION_TIMEOUT_MS);
272
+ });
273
+ const rawResult = await Promise.race([executionPromise, timeoutPromise]);
274
+ if (asyncCallbackError instanceof Error) {
275
+ throw new ExecutionError(asyncCallbackError.message, {
276
+ code: 'ASYNC_CALLBACK_ERROR',
277
+ stackTrace: asyncCallbackError.stack
278
+ });
279
+ }
280
+ const truncated = truncateResult(rawResult);
281
+ return buildEnvelope({
282
+ ok: true,
283
+ result: truncated.result,
284
+ error: null,
285
+ truncatedResult: truncated.truncatedResult,
286
+ resultWasUndefined: truncated.resultWasUndefined
287
+ });
288
+ }
289
+ catch (error) {
290
+ const failure = normalizeFailure(error);
291
+ appendLog('error', [`${failure.code}: ${failure.message}`]);
292
+ return buildEnvelope({
293
+ ok: false,
294
+ result: null,
295
+ error: failure,
296
+ truncatedResult: false,
297
+ resultWasUndefined: false
298
+ });
299
+ }
300
+ finally {
301
+ if (executionTimeout) {
302
+ clearTimeout(executionTimeout);
303
+ }
304
+ timers.forEach((timer) => {
305
+ clearTimeout(timer);
306
+ clearInterval(timer);
307
+ });
308
+ timers.clear();
309
+ }
310
+ }
311
+ async function readStdin() {
312
+ return await new Promise((resolveInput) => {
313
+ let input = '';
314
+ process.stdin.setEncoding('utf-8');
315
+ process.stdin.on('data', (chunk) => {
316
+ input += chunk;
317
+ });
318
+ process.stdin.on('end', () => {
319
+ resolveInput(input);
320
+ });
321
+ });
322
+ }
323
+ function printEnvelope(envelope) {
324
+ process.stdout.write(JSON.stringify(envelope));
325
+ }
326
+ function buildBootstrapFailure(code, message) {
327
+ return {
328
+ ok: false,
329
+ result: null,
330
+ logs: [],
331
+ error: {
332
+ code,
333
+ message
334
+ },
335
+ meta: {
336
+ timeoutMs: EXECUTION_TIMEOUT_MS,
337
+ durationMs: 0,
338
+ truncatedResult: false,
339
+ truncatedLogs: false,
340
+ resultWasUndefined: false
341
+ }
342
+ };
343
+ }
344
+ async function main() {
345
+ const rawInput = await readStdin();
346
+ if (!rawInput.trim()) {
347
+ printEnvelope(buildBootstrapFailure('EMPTY_PAYLOAD', 'Execution payload is empty'));
348
+ return;
349
+ }
350
+ let payload;
351
+ try {
352
+ payload = JSON.parse(rawInput);
353
+ }
354
+ catch (error) {
355
+ printEnvelope(buildBootstrapFailure('INVALID_PAYLOAD', `Failed to parse execution payload: ${String(error)}`));
356
+ return;
357
+ }
358
+ if (typeof payload.code !== 'string') {
359
+ printEnvelope(buildBootstrapFailure('INVALID_PAYLOAD', 'Execution payload must include a string "code" field'));
360
+ return;
361
+ }
362
+ const envelope = await execute(payload.code);
363
+ printEnvelope(envelope);
364
+ }
365
+ main().catch((error) => {
366
+ const message = error instanceof Error ? error.message : String(error);
367
+ printEnvelope(buildBootstrapFailure('RUNNER_FAILURE', message));
368
+ });