@thxgg/steward 0.1.16 → 0.1.17

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/.env.example +6 -0
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/_nuxt/{wbjFvimm.js → 4r0X30JV.js} +1 -1
  4. package/.output/public/_nuxt/{ynmyrfyT.js → BMdjSp24.js} +1 -1
  5. package/.output/public/_nuxt/{BWVTacYj.js → BSZqAKg4.js} +1 -1
  6. package/.output/public/_nuxt/{Dum5qplW.js → BdjPva1I.js} +1 -1
  7. package/.output/public/_nuxt/Beeir9iR.js +1 -0
  8. package/.output/public/_nuxt/Bh3vsUvl.js +1 -0
  9. package/.output/public/_nuxt/{Bibm_IDv.js → BlTKcjLJ.js} +2 -2
  10. package/.output/public/_nuxt/{CxHOXVf6.js → By7gAVcL.js} +1 -1
  11. package/.output/public/_nuxt/CbJfCtEa.js +1 -0
  12. package/.output/public/_nuxt/CbkpNvIu.js +141 -0
  13. package/.output/public/_nuxt/CmhLcqDu.js +1 -0
  14. package/.output/public/_nuxt/DC6iPLz1.js +30 -0
  15. package/.output/public/_nuxt/{BSA0RJ-H.js → DD--ojY9.js} +1 -1
  16. package/.output/public/_nuxt/Detail.DSyVQNdr.css +1 -0
  17. package/.output/public/_nuxt/DhKWRjCh.js +60 -0
  18. package/.output/public/_nuxt/_prd_.BkpxMFSV.css +1 -0
  19. package/.output/public/_nuxt/builds/latest.json +1 -1
  20. package/.output/public/_nuxt/builds/meta/f3f42dbd-d501-442b-871c-3d06157e7aa1.json +1 -0
  21. package/.output/public/_nuxt/c1sXju8w.js +1 -0
  22. package/.output/public/_nuxt/eGCjCghR.js +1 -0
  23. package/.output/public/_nuxt/nX8Sf7cz.js +13 -0
  24. package/.output/server/chunks/_/git-api.mjs +100 -7
  25. package/.output/server/chunks/_/git-api.mjs.map +1 -1
  26. package/.output/server/chunks/_/git.mjs +3 -10
  27. package/.output/server/chunks/_/git.mjs.map +1 -1
  28. package/.output/server/chunks/_/prd-service.mjs +234 -0
  29. package/.output/server/chunks/_/prd-service.mjs.map +1 -0
  30. package/.output/server/chunks/_/task-graph.mjs +3 -3
  31. package/.output/server/chunks/_/task-graph.mjs.map +1 -1
  32. package/.output/server/chunks/_/watcher.mjs +26 -46
  33. package/.output/server/chunks/_/watcher.mjs.map +1 -1
  34. package/.output/server/chunks/build/{Detail-CUfU85GY.mjs → Detail-MGwP_u2d.mjs} +63 -34
  35. package/.output/server/chunks/build/Detail-MGwP_u2d.mjs.map +1 -0
  36. package/.output/server/chunks/build/DiffViewer-styles-1.mjs-BFsE2PCW.mjs +4 -0
  37. package/.output/server/chunks/build/DiffViewer-styles-1.mjs-BFsE2PCW.mjs.map +1 -0
  38. package/.output/server/chunks/build/DiffViewer-styles.D2bqX3nK.mjs +8 -0
  39. package/.output/server/chunks/build/DiffViewer-styles.D2bqX3nK.mjs.map +1 -0
  40. package/.output/server/chunks/build/DiffViewer-styles.FoV36wuV.mjs +10 -0
  41. package/.output/server/chunks/build/DiffViewer-styles.FoV36wuV.mjs.map +1 -0
  42. package/.output/server/chunks/build/Viewer-styles.D6wYWFb1.mjs +8 -0
  43. package/.output/server/chunks/build/Viewer-styles.D6wYWFb1.mjs.map +1 -0
  44. package/.output/server/chunks/build/{_prd_-CeVnQzOV.mjs → _prd_-C-Aj4fVa.mjs} +75 -33
  45. package/.output/server/chunks/build/_prd_-C-Aj4fVa.mjs.map +1 -0
  46. package/.output/server/chunks/build/client.precomputed.mjs +1 -1
  47. package/.output/server/chunks/build/{default-DWCOHHTE.mjs → default-Cao5eO80.mjs} +4 -3
  48. package/.output/server/chunks/build/default-Cao5eO80.mjs.map +1 -0
  49. package/.output/server/chunks/build/error-404-Bf6kdO80.mjs +2 -1
  50. package/.output/server/chunks/build/error-500-D_bcARXN.mjs +2 -1
  51. package/.output/server/chunks/build/{index-CckL_NBD.mjs → index-ByZO4Bvq.mjs} +2 -2
  52. package/.output/server/chunks/build/index-ByZO4Bvq.mjs.map +1 -0
  53. package/.output/server/chunks/build/{index-QVeSHT3L.mjs → index-ljj9uTXI.mjs} +8 -5
  54. package/.output/server/chunks/build/index-ljj9uTXI.mjs.map +1 -0
  55. package/.output/server/chunks/build/nuxt-link-SvT1nf8Z.mjs +1 -1
  56. package/.output/server/chunks/build/{repo-graph-CHNl58mY.mjs → repo-graph-EuhMeFt7.mjs} +25 -10
  57. package/.output/server/chunks/build/repo-graph-EuhMeFt7.mjs.map +1 -0
  58. package/.output/server/chunks/build/server.mjs +7 -6
  59. package/.output/server/chunks/build/styles.mjs +4 -4
  60. package/.output/server/chunks/build/{usePrd-SqcxGyFU.mjs → usePrd-f7ylhIqs.mjs} +10 -34
  61. package/.output/server/chunks/build/usePrd-f7ylhIqs.mjs.map +1 -0
  62. package/.output/server/chunks/nitro/nitro.mjs +1106 -677
  63. package/.output/server/chunks/nitro/nitro.mjs.map +1 -1
  64. package/.output/server/chunks/routes/api/browse.get.mjs +12 -6
  65. package/.output/server/chunks/routes/api/browse.get.mjs.map +1 -1
  66. package/.output/server/chunks/routes/api/index.get.mjs +2 -1
  67. package/.output/server/chunks/routes/api/index.get.mjs.map +1 -1
  68. package/.output/server/chunks/routes/api/index.post.mjs +3 -2
  69. package/.output/server/chunks/routes/api/index.post.mjs.map +1 -1
  70. package/.output/server/chunks/routes/api/repos/_repoId/git/commits.get.mjs +11 -13
  71. package/.output/server/chunks/routes/api/repos/_repoId/git/commits.get.mjs.map +1 -1
  72. package/.output/server/chunks/routes/api/repos/_repoId/git/diff.get.mjs +11 -6
  73. package/.output/server/chunks/routes/api/repos/_repoId/git/diff.get.mjs.map +1 -1
  74. package/.output/server/chunks/routes/api/repos/_repoId/git/file-content.get.mjs +31 -12
  75. package/.output/server/chunks/routes/api/repos/_repoId/git/file-content.get.mjs.map +1 -1
  76. package/.output/server/chunks/routes/api/repos/_repoId/git/file-diff.get.mjs +13 -13
  77. package/.output/server/chunks/routes/api/repos/_repoId/git/file-diff.get.mjs.map +1 -1
  78. package/.output/server/chunks/routes/api/repos/_repoId/graph.get.mjs +5 -1
  79. package/.output/server/chunks/routes/api/repos/_repoId/graph.get.mjs.map +1 -1
  80. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/graph.get.mjs +14 -1
  81. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/graph.get.mjs.map +1 -1
  82. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/progress.get.mjs +20 -9
  83. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/progress.get.mjs.map +1 -1
  84. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks/_taskId/commits.get.mjs +20 -85
  85. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks/_taskId/commits.get.mjs.map +1 -1
  86. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks.get.mjs +20 -9
  87. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks.get.mjs.map +1 -1
  88. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug_.get.mjs +30 -50
  89. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug_.get.mjs.map +1 -1
  90. package/.output/server/chunks/routes/api/repos/_repoId/prds.get.mjs +19 -49
  91. package/.output/server/chunks/routes/api/repos/_repoId/prds.get.mjs.map +1 -1
  92. package/.output/server/chunks/routes/api/repos/_repoId/refresh-git-repos.post.mjs +6 -13
  93. package/.output/server/chunks/routes/api/repos/_repoId/refresh-git-repos.post.mjs.map +1 -1
  94. package/.output/server/chunks/routes/api/repos/_repoId_.delete.mjs +2 -1
  95. package/.output/server/chunks/routes/api/repos/_repoId_.delete.mjs.map +1 -1
  96. package/.output/server/chunks/routes/api/runtime.get.mjs +3 -2
  97. package/.output/server/chunks/routes/api/runtime.get.mjs.map +1 -1
  98. package/.output/server/chunks/routes/api/watch.get.mjs +5 -4
  99. package/.output/server/chunks/routes/api/watch.get.mjs.map +1 -1
  100. package/.output/server/chunks/routes/renderer.mjs +1 -1
  101. package/.output/server/index.mjs +3 -2
  102. package/.output/server/index.mjs.map +1 -1
  103. package/.output/server/node_modules/zod/index.js +4 -0
  104. package/.output/server/node_modules/zod/package.json +118 -0
  105. package/.output/server/node_modules/zod/v3/ZodError.js +133 -0
  106. package/.output/server/node_modules/zod/v3/errors.js +9 -0
  107. package/.output/server/node_modules/zod/v3/external.js +6 -0
  108. package/.output/server/node_modules/zod/v3/helpers/errorUtil.js +6 -0
  109. package/.output/server/node_modules/zod/v3/helpers/parseUtil.js +109 -0
  110. package/.output/server/node_modules/zod/v3/helpers/typeAliases.js +1 -0
  111. package/.output/server/node_modules/zod/v3/helpers/util.js +133 -0
  112. package/.output/server/node_modules/zod/v3/locales/en.js +109 -0
  113. package/.output/server/node_modules/zod/v3/types.js +3693 -0
  114. package/.output/server/package.json +2 -1
  115. package/README.md +7 -2
  116. package/dist/host/src/api/prds.js +6 -172
  117. package/dist/host/src/api/repos.js +3 -16
  118. package/dist/host/src/api/state.js +7 -2
  119. package/dist/host/src/executor-runner.js +368 -0
  120. package/dist/host/src/executor.js +138 -260
  121. package/dist/host/src/index.js +7 -2
  122. package/dist/host/src/mcp.js +27 -1
  123. package/dist/host/src/ui.js +18 -3
  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 +235 -0
  127. package/dist/server/utils/prd-state.js +57 -45
  128. package/dist/server/utils/repos.js +58 -13
  129. package/dist/server/utils/state-schema.js +61 -0
  130. package/dist/server/utils/task-graph.js +2 -2
  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.17",
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
@@ -55,6 +55,7 @@ Note: `execute` runs in a VM sandbox by design, so globals like `process` are in
55
55
  ```bash
56
56
  prd ui
57
57
  prd ui --port 3100 --host 127.0.0.1
58
+ prd ui --host 0.0.0.0 --allow-remote
58
59
  prd mcp
59
60
  ```
60
61
 
@@ -143,8 +144,10 @@ Detailed API docs and examples: `docs/MCP.md`
143
144
 
144
145
  Steward reads local filesystem and git metadata by design.
145
146
 
146
- - Run only on trusted local machines
147
- - Do not expose directly to the public internet
147
+ - By default, UI/API only accept loopback requests
148
+ - Non-loopback access requires explicit opt-in (`--allow-remote` or `STEWARD_ALLOW_REMOTE=1`)
149
+ - Optional token auth for remote API access: set `STEWARD_API_TOKEN`
150
+ - For browser sessions, you can initialize the auth cookie with `?token=<STEWARD_API_TOKEN>`
148
151
  - Treat as a workstation tool, not a hosted multi-user service
149
152
 
150
153
  ## Storage
@@ -171,6 +174,8 @@ npm run build
171
174
  | `PRD_STATE_DB_PATH` | Absolute path to SQLite DB file |
172
175
  | `PRD_STATE_HOME` | Base directory for DB (`state.db` inside) |
173
176
  | `XDG_DATA_HOME` | Fallback base path for default DB location |
177
+ | `STEWARD_ALLOW_REMOTE` | Set to `1` to allow non-loopback requests |
178
+ | `STEWARD_API_TOKEN` | Optional token required for remote `/api/*` access |
174
179
 
175
180
  ## OpenCode Bundle
176
181
 
@@ -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,4 +1,4 @@
1
- import { addRepo, discoverGitRepos, getRepoById, getRepos, removeRepo, saveRepos, validateRepoPath } from '../../../server/utils/repos.js';
1
+ import { addRepo, discoverGitRepos, getRepoById, getRepos, removeRepo, updateRepoGitRepos, validateRepoPath } from '../../../server/utils/repos.js';
2
2
  import { migrateLegacyStateForRepo } from '../../../server/utils/prd-state.js';
3
3
  import { requireCurrentRepo, requireRepo } from './repo-context.js';
4
4
  export const repos = {
@@ -29,22 +29,9 @@ export const repos = {
29
29
  return { removed: true };
30
30
  },
31
31
  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];
32
+ const repo = await requireRepo(repoId);
39
33
  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);
34
+ await updateRepoGitRepos(repo.id, gitRepos.length > 0 ? gitRepos : undefined);
48
35
  return {
49
36
  discovered: gitRepos.length,
50
37
  gitRepos
@@ -1,9 +1,14 @@
1
1
  import { getPrdState, getPrdStateSummaries, migrateLegacyStateForRepo, 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
  }
@@ -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
+ });