@thxgg/steward 0.1.7 → 0.1.10

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 (34) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/{Bo1Fdv48.js → BPaqwWyl.js} +2 -2
  3. package/.output/public/_nuxt/{DhQtydpF.js → C8LtDyY4.js} +1 -1
  4. package/.output/public/_nuxt/{D0zW6lUK.js → CQgu_W_k.js} +1 -1
  5. package/.output/public/_nuxt/{BRDbaJqY.js → CZKCADv6.js} +2 -2
  6. package/.output/public/_nuxt/{CEJOILWG.js → CeO4HNxC.js} +1 -1
  7. package/.output/public/_nuxt/{BNzFoVmP.js → Cs5ptsBk.js} +1 -1
  8. package/.output/public/_nuxt/{CFsNy2aC.js → CshyynD6.js} +1 -1
  9. package/.output/public/_nuxt/{DYDTtHLR.js → CzKPXRws.js} +1 -1
  10. package/.output/public/_nuxt/{BqmZq_gb.js → DOvbLsAq.js} +1 -1
  11. package/.output/public/_nuxt/{Bri1ZtcQ.js → DbloiS5Y.js} +1 -1
  12. package/.output/public/_nuxt/{B3hkJjmY.js → DcRwFvvS.js} +1 -1
  13. package/.output/public/_nuxt/builds/latest.json +1 -1
  14. package/.output/public/_nuxt/builds/meta/7fda7510-94bc-443a-a338-a8d2af142ed9.json +1 -0
  15. package/.output/public/_nuxt/{X6fIXIFO.js → vr7VLA9A.js} +1 -1
  16. package/.output/server/chunks/build/{_prd_-CnwhMRyf.mjs → _prd_-CkKfJB6U.mjs} +2 -2
  17. package/.output/server/chunks/build/_prd_-CkKfJB6U.mjs.map +1 -0
  18. package/.output/server/chunks/build/client.precomputed.mjs +1 -1
  19. package/.output/server/chunks/build/server.mjs +1 -1
  20. package/.output/server/chunks/nitro/nitro.mjs +617 -617
  21. package/.output/server/package.json +1 -1
  22. package/README.md +22 -4
  23. package/dist/host/src/api/git.js +1 -8
  24. package/dist/host/src/api/prds.js +2 -8
  25. package/dist/host/src/api/repo-context.js +60 -0
  26. package/dist/host/src/api/repos.js +6 -0
  27. package/dist/host/src/api/state.js +20 -21
  28. package/dist/host/src/executor.js +215 -29
  29. package/dist/host/src/help.js +124 -0
  30. package/dist/host/src/mcp.js +49 -25
  31. package/docs/MCP.md +50 -3
  32. package/package.json +1 -1
  33. package/.output/public/_nuxt/builds/meta/6683a0d9-9c02-4098-b750-bbbc0305261e.json +0 -1
  34. package/.output/server/chunks/build/_prd_-CnwhMRyf.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thxgg/steward-prod",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "dependencies": {
package/README.md CHANGED
@@ -77,9 +77,7 @@ prd mcp
77
77
  Steward exposes one MCP tool: `execute`.
78
78
 
79
79
  ```js
80
- const reposList = await repos.list()
81
- const repo = reposList[0]
82
- if (!repo) return { error: 'No repos configured' }
80
+ const repo = await repos.current()
83
81
 
84
82
  const prdList = await prds.list(repo.id)
85
83
  if (prdList.length === 0) return { repo: repo.name, prds: 0 }
@@ -92,6 +90,26 @@ return {
92
90
  }
93
91
  ```
94
92
 
93
+ Every call returns a structured envelope:
94
+
95
+ ```json
96
+ {
97
+ "ok": true,
98
+ "result": {},
99
+ "logs": [],
100
+ "error": null,
101
+ "meta": {
102
+ "timeoutMs": 30000,
103
+ "durationMs": 10,
104
+ "truncatedResult": false,
105
+ "truncatedLogs": false,
106
+ "resultWasUndefined": false
107
+ }
108
+ }
109
+ ```
110
+
111
+ Use `steward.help()` inside `execute` for runtime API signatures and examples.
112
+
95
113
  ## APIs
96
114
 
97
115
  Inside `execute`, these APIs are available:
@@ -99,7 +117,7 @@ Inside `execute`, these APIs are available:
99
117
  - `repos` - register/list/remove repos and refresh discovered git repos
100
118
  - `prds` - list/read PRD docs, tasks, progress, and task commit refs
101
119
  - `git` - commit metadata, diffs, file diffs, and file contents
102
- - `state` - direct PRD state get/upsert by repo id or path
120
+ - `state` - direct PRD state get/upsert by repo id, path, or current repo
103
121
 
104
122
  Detailed API docs and examples: `docs/MCP.md`
105
123
 
@@ -1,12 +1,5 @@
1
1
  import { getCommitDiff, getCommitInfo, getFileContent, getFileDiff, isGitRepo, validatePathInRepo } from '../../../server/utils/git.js';
2
- import { getRepoById } from '../../../server/utils/repos.js';
3
- async function requireRepo(repoId) {
4
- const repo = await getRepoById(repoId);
5
- if (!repo) {
6
- throw new Error('Repository not found');
7
- }
8
- return repo;
9
- }
2
+ import { requireRepo } from './repo-context.js';
10
3
  function resolveGitRepoPath(repo, repoPath) {
11
4
  if (!repoPath) {
12
5
  return repo.path;
@@ -1,8 +1,9 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import { basename, join } from 'node:path';
3
3
  import { resolveCommitRepo } from '../../../server/utils/git.js';
4
- import { discoverGitRepos, getRepoById, getRepos, saveRepos } from '../../../server/utils/repos.js';
4
+ import { discoverGitRepos, getRepos, saveRepos } from '../../../server/utils/repos.js';
5
5
  import { getPrdState, getPrdStateSummaries, migrateLegacyStateForRepo } from '../../../server/utils/prd-state.js';
6
+ import { requireRepo } from './repo-context.js';
6
7
  function parseMetadata(content) {
7
8
  const metadata = {};
8
9
  const authorMatch = content.match(/\*{0,2}Author\*{0,2}:\*{0,2}\s*(.+?)(?:\n|$)/i);
@@ -30,13 +31,6 @@ function parseMetadata(content) {
30
31
  }
31
32
  return metadata;
32
33
  }
33
- async function requireRepo(repoId) {
34
- const repo = await getRepoById(repoId);
35
- if (!repo) {
36
- throw new Error('Repository not found');
37
- }
38
- return repo;
39
- }
40
34
  async function readPrdFile(repo, prdSlug) {
41
35
  const prdPath = join(repo.path, 'docs', 'prd', `${prdSlug}.md`);
42
36
  try {
@@ -0,0 +1,60 @@
1
+ import { resolve } from 'node:path';
2
+ import { getRepoById, getRepos } from '../../../server/utils/repos.js';
3
+ export class RepoLookupError extends Error {
4
+ code;
5
+ details;
6
+ constructor(message, code, details) {
7
+ super(message);
8
+ this.code = code;
9
+ this.details = details;
10
+ this.name = 'RepoLookupError';
11
+ }
12
+ }
13
+ function summarizeRepos(repos) {
14
+ return repos.map((repo) => ({
15
+ id: repo.id,
16
+ name: repo.name,
17
+ path: repo.path
18
+ }));
19
+ }
20
+ function formatKnownRepos(knownRepos) {
21
+ return knownRepos
22
+ .map((repo) => `${repo.id} (${repo.name}) ${repo.path}`)
23
+ .join('; ');
24
+ }
25
+ export async function requireRepo(repoId) {
26
+ const repo = await getRepoById(repoId);
27
+ if (repo) {
28
+ return repo;
29
+ }
30
+ const allRepos = await getRepos();
31
+ const knownRepos = summarizeRepos(allRepos);
32
+ if (knownRepos.length === 0) {
33
+ throw new RepoLookupError(`Unknown repoId "${repoId}". No repositories are registered. Use repos.add(path) first.`, 'NO_REPOS', { repoId, knownRepos });
34
+ }
35
+ throw new RepoLookupError(`Unknown repoId "${repoId}". Known repositories: ${formatKnownRepos(knownRepos)}`, 'REPO_NOT_FOUND', { repoId, knownRepos });
36
+ }
37
+ export async function requireRepoByPath(repoPath) {
38
+ const absolutePath = resolve(repoPath);
39
+ const allRepos = await getRepos();
40
+ const repo = allRepos.find((candidate) => resolve(candidate.path) === absolutePath);
41
+ if (repo) {
42
+ return repo;
43
+ }
44
+ const knownRepos = summarizeRepos(allRepos);
45
+ if (knownRepos.length === 0) {
46
+ throw new RepoLookupError(`No registered repository found for path: ${absolutePath}. No repositories are registered. Use repos.add(path) first.`, 'NO_REPOS', { repoPath: absolutePath, knownRepos });
47
+ }
48
+ throw new RepoLookupError(`No registered repository found for path: ${absolutePath}. Known repositories: ${formatKnownRepos(knownRepos)}`, 'REPO_PATH_NOT_FOUND', { repoPath: absolutePath, knownRepos });
49
+ }
50
+ export async function requireCurrentRepo() {
51
+ const allRepos = await getRepos();
52
+ if (allRepos.length === 1) {
53
+ return allRepos[0];
54
+ }
55
+ const knownRepos = summarizeRepos(allRepos);
56
+ if (knownRepos.length === 0) {
57
+ throw new RepoLookupError('No repositories are registered. Use repos.add(path) first.', 'NO_REPOS', { knownRepos });
58
+ }
59
+ throw new RepoLookupError(`Cannot resolve a current repository because ${knownRepos.length} repositories are registered. Use an explicit repoId or by-path API. Known repositories: ${formatKnownRepos(knownRepos)}`, 'AMBIGUOUS_REPO', { knownRepos });
60
+ }
@@ -1,5 +1,6 @@
1
1
  import { addRepo, discoverGitRepos, getRepoById, getRepos, removeRepo, saveRepos, validateRepoPath } from '../../../server/utils/repos.js';
2
2
  import { migrateLegacyStateForRepo } from '../../../server/utils/prd-state.js';
3
+ import { requireCurrentRepo, requireRepo } from './repo-context.js';
3
4
  export const repos = {
4
5
  async list() {
5
6
  return await getRepos();
@@ -7,6 +8,9 @@ export const repos = {
7
8
  async get(repoId) {
8
9
  return await getRepoById(repoId) ?? null;
9
10
  },
11
+ async current() {
12
+ return await requireCurrentRepo();
13
+ },
10
14
  async add(path, name) {
11
15
  const validation = await validateRepoPath(path);
12
16
  if (!validation.valid) {
@@ -17,6 +21,7 @@ export const repos = {
17
21
  return repo;
18
22
  },
19
23
  async remove(repoId) {
24
+ await requireRepo(repoId);
20
25
  const removed = await removeRepo(repoId);
21
26
  if (!removed) {
22
27
  throw new Error('Repository not found');
@@ -24,6 +29,7 @@ export const repos = {
24
29
  return { removed: true };
25
30
  },
26
31
  async refreshGitRepos(repoId) {
32
+ await requireRepo(repoId);
27
33
  const allRepos = await getRepos();
28
34
  const repoIndex = allRepos.findIndex((repo) => repo.id === repoId);
29
35
  if (repoIndex === -1) {
@@ -1,22 +1,5 @@
1
- import { resolve } from 'node:path';
2
1
  import { getPrdState, getPrdStateSummaries, migrateLegacyStateForRepo, upsertPrdState } from '../../../server/utils/prd-state.js';
3
- import { getRepoById, getRepos } from '../../../server/utils/repos.js';
4
- async function requireRepo(repoId) {
5
- const repo = await getRepoById(repoId);
6
- if (!repo) {
7
- throw new Error('Repository not found');
8
- }
9
- return repo;
10
- }
11
- async function findRepoByPath(repoPath) {
12
- const absolutePath = resolve(repoPath);
13
- const repos = await getRepos();
14
- const repo = repos.find((candidate) => resolve(candidate.path) === absolutePath);
15
- if (!repo) {
16
- throw new Error(`No registered repository found for path: ${absolutePath}`);
17
- }
18
- return repo;
19
- }
2
+ import { requireCurrentRepo, requireRepo, requireRepoByPath } from './repo-context.js';
20
3
  function mapStateUpdate(payload) {
21
4
  return {
22
5
  ...(payload.tasks !== undefined && { tasks: payload.tasks }),
@@ -34,7 +17,12 @@ export const state = {
34
17
  return await getPrdState(repo.id, slug);
35
18
  },
36
19
  async getByPath(repoPath, slug) {
37
- const repo = await findRepoByPath(repoPath);
20
+ const repo = await requireRepoByPath(repoPath);
21
+ await migrateLegacyStateForRepo(repo);
22
+ return await getPrdState(repo.id, slug);
23
+ },
24
+ async getCurrent(slug) {
25
+ const repo = await requireCurrentRepo();
38
26
  await migrateLegacyStateForRepo(repo);
39
27
  return await getPrdState(repo.id, slug);
40
28
  },
@@ -45,7 +33,13 @@ export const state = {
45
33
  return mapSummaryMap(summaries);
46
34
  },
47
35
  async summariesByPath(repoPath) {
48
- const repo = await findRepoByPath(repoPath);
36
+ const repo = await requireRepoByPath(repoPath);
37
+ await migrateLegacyStateForRepo(repo);
38
+ const summaries = await getPrdStateSummaries(repo.id);
39
+ return mapSummaryMap(summaries);
40
+ },
41
+ async summariesCurrent() {
42
+ const repo = await requireCurrentRepo();
49
43
  await migrateLegacyStateForRepo(repo);
50
44
  const summaries = await getPrdStateSummaries(repo.id);
51
45
  return mapSummaryMap(summaries);
@@ -56,7 +50,12 @@ export const state = {
56
50
  return { saved: true };
57
51
  },
58
52
  async upsertByPath(repoPath, slug, payload) {
59
- const repo = await findRepoByPath(repoPath);
53
+ const repo = await requireRepoByPath(repoPath);
54
+ await upsertPrdState(repo.id, slug, mapStateUpdate(payload));
55
+ return { saved: true };
56
+ },
57
+ async upsertCurrent(slug, payload) {
58
+ const repo = await requireCurrentRepo();
60
59
  await upsertPrdState(repo.id, slug, mapStateUpdate(payload));
61
60
  return { saved: true };
62
61
  }
@@ -1,61 +1,215 @@
1
1
  import vm from 'node:vm';
2
2
  import { git, prds, repos, state } from './api/index.js';
3
+ import { getStewardHelp } from './help.js';
3
4
  const MAX_OUTPUT_SIZE = 50_000;
4
5
  const EXECUTION_TIMEOUT_MS = 30_000;
5
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;
6
10
  export class ExecutionError extends Error {
7
- stackTrace;
8
- constructor(message, stackTrace) {
11
+ options;
12
+ constructor(message, options) {
9
13
  super(message);
10
- this.stackTrace = stackTrace;
14
+ this.options = options;
11
15
  this.name = 'ExecutionError';
12
16
  }
13
17
  }
14
- function truncateOutput(result) {
15
- if (result === undefined) {
16
- return undefined;
17
- }
18
- let json;
18
+ function safeJsonStringify(value) {
19
+ const seen = new WeakSet();
19
20
  try {
20
- json = JSON.stringify(result, null, 2);
21
+ return JSON.stringify(value, (_key, currentValue) => {
22
+ if (typeof currentValue === 'bigint') {
23
+ return `${currentValue}n`;
24
+ }
25
+ if (typeof currentValue === 'function') {
26
+ const functionName = currentValue.name ? ` ${currentValue.name}` : '';
27
+ return `[Function${functionName}]`;
28
+ }
29
+ if (typeof currentValue === 'symbol') {
30
+ return currentValue.toString();
31
+ }
32
+ if (typeof currentValue === 'object' && currentValue !== null) {
33
+ if (seen.has(currentValue)) {
34
+ return '[Circular]';
35
+ }
36
+ seen.add(currentValue);
37
+ }
38
+ return currentValue;
39
+ });
21
40
  }
22
41
  catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ function formatLogValue(value) {
46
+ if (typeof value === 'string') {
47
+ return value;
48
+ }
49
+ const json = safeJsonStringify(value);
50
+ if (json !== undefined) {
51
+ return json;
52
+ }
53
+ return String(value);
54
+ }
55
+ function truncateResult(result) {
56
+ if (result === undefined) {
23
57
  return {
24
- _unserializable: true,
25
- preview: String(result)
58
+ result: null,
59
+ truncatedResult: false,
60
+ resultWasUndefined: true
61
+ };
62
+ }
63
+ const json = safeJsonStringify(result);
64
+ if (json === undefined) {
65
+ return {
66
+ result: {
67
+ _unserializable: true,
68
+ preview: String(result)
69
+ },
70
+ truncatedResult: false,
71
+ resultWasUndefined: false
26
72
  };
27
73
  }
28
74
  if (json.length <= MAX_OUTPUT_SIZE) {
29
- return result;
75
+ return {
76
+ result,
77
+ truncatedResult: false,
78
+ resultWasUndefined: false
79
+ };
30
80
  }
31
81
  return {
32
- _truncated: true,
33
- size: json.length,
34
- preview: json.slice(0, MAX_OUTPUT_SIZE),
35
- message: `Output truncated (${json.length} chars, showing first ${MAX_OUTPUT_SIZE})`
82
+ result: {
83
+ _truncated: true,
84
+ size: json.length,
85
+ preview: json.slice(0, MAX_OUTPUT_SIZE),
86
+ message: `Output truncated (${json.length} chars, showing first ${MAX_OUTPUT_SIZE})`
87
+ },
88
+ truncatedResult: true,
89
+ resultWasUndefined: false
90
+ };
91
+ }
92
+ function normalizeFailure(error) {
93
+ if (error instanceof ExecutionError) {
94
+ return {
95
+ code: error.options?.code || 'EXECUTION_ERROR',
96
+ message: error.message,
97
+ ...(error.options?.stackTrace && { stack: error.options.stackTrace }),
98
+ ...(error.options?.details !== undefined && { details: error.options.details })
99
+ };
100
+ }
101
+ if (error instanceof Error) {
102
+ const { code, details } = error;
103
+ return {
104
+ code: typeof code === 'string' ? code : 'EXECUTION_ERROR',
105
+ message: error.message,
106
+ ...(error.stack && { stack: error.stack }),
107
+ ...(details !== undefined && { details })
108
+ };
109
+ }
110
+ return {
111
+ code: 'EXECUTION_ERROR',
112
+ message: String(error)
36
113
  };
37
114
  }
38
115
  export async function execute(code) {
116
+ const startedAt = Date.now();
117
+ const logs = [];
118
+ let totalLogChars = 0;
119
+ let logsTruncated = false;
120
+ const appendLog = (level, args) => {
121
+ if (logs.length >= MAX_LOG_ENTRIES) {
122
+ logsTruncated = true;
123
+ return;
124
+ }
125
+ let message = args.map(formatLogValue).join(' ');
126
+ if (message.length > MAX_LOG_ENTRY_SIZE) {
127
+ message = `${message.slice(0, MAX_LOG_ENTRY_SIZE)}...`;
128
+ logsTruncated = true;
129
+ }
130
+ if (totalLogChars + message.length > MAX_LOG_OUTPUT_SIZE) {
131
+ logsTruncated = true;
132
+ return;
133
+ }
134
+ totalLogChars += message.length;
135
+ logs.push({
136
+ level,
137
+ message,
138
+ timestamp: new Date().toISOString()
139
+ });
140
+ };
141
+ const buildEnvelope = (params) => ({
142
+ ok: params.ok,
143
+ result: params.result,
144
+ logs,
145
+ error: params.error,
146
+ meta: {
147
+ timeoutMs: EXECUTION_TIMEOUT_MS,
148
+ durationMs: Date.now() - startedAt,
149
+ truncatedResult: params.truncatedResult,
150
+ truncatedLogs: logsTruncated,
151
+ resultWasUndefined: params.resultWasUndefined
152
+ }
153
+ });
39
154
  if (!code || !code.trim()) {
40
- throw new ExecutionError('Code cannot be empty');
155
+ const error = normalizeFailure(new ExecutionError('Code cannot be empty', { code: 'EMPTY_CODE' }));
156
+ return buildEnvelope({
157
+ ok: false,
158
+ result: null,
159
+ error,
160
+ truncatedResult: false,
161
+ resultWasUndefined: false
162
+ });
41
163
  }
42
164
  const timers = new Set();
165
+ let executionTimeout = null;
166
+ let asyncCallbackError = null;
167
+ const wrapTimerHandler = (handler) => {
168
+ return () => {
169
+ try {
170
+ handler();
171
+ }
172
+ catch (error) {
173
+ const normalizedError = error instanceof Error
174
+ ? error
175
+ : new Error(String(error));
176
+ asyncCallbackError = normalizedError;
177
+ appendLog('error', ['Timer callback error:', normalizedError.message]);
178
+ }
179
+ };
180
+ };
181
+ const ensureTimerHandler = (handler) => {
182
+ if (typeof handler !== 'function') {
183
+ throw new ExecutionError('Timer handler must be a function', {
184
+ code: 'INVALID_TIMER_HANDLER'
185
+ });
186
+ }
187
+ return wrapTimerHandler(handler);
188
+ };
43
189
  const sandbox = {
44
190
  repos,
45
191
  prds,
46
192
  git,
47
193
  state,
194
+ steward: {
195
+ help: () => getStewardHelp()
196
+ },
48
197
  console: {
49
- log: (...args) => console.log('[codemode]', ...args),
50
- error: (...args) => console.error('[codemode]', ...args)
198
+ log: (...args) => appendLog('log', args),
199
+ info: (...args) => appendLog('info', args),
200
+ warn: (...args) => appendLog('warn', args),
201
+ error: (...args) => appendLog('error', args)
51
202
  },
52
203
  setTimeout: (handler, timeout) => {
53
204
  if (timers.size >= MAX_TIMERS) {
54
- throw new Error(`Timer limit exceeded (max ${MAX_TIMERS})`);
205
+ throw new ExecutionError(`Timer limit exceeded (max ${MAX_TIMERS})`, {
206
+ code: 'TIMER_LIMIT'
207
+ });
55
208
  }
209
+ const wrappedHandler = ensureTimerHandler(handler);
56
210
  const timer = setTimeout(() => {
57
211
  timers.delete(timer);
58
- handler();
212
+ wrappedHandler();
59
213
  }, timeout);
60
214
  timers.add(timer);
61
215
  return timer;
@@ -66,9 +220,12 @@ export async function execute(code) {
66
220
  },
67
221
  setInterval: (handler, timeout) => {
68
222
  if (timers.size >= MAX_TIMERS) {
69
- throw new Error(`Timer limit exceeded (max ${MAX_TIMERS})`);
223
+ throw new ExecutionError(`Timer limit exceeded (max ${MAX_TIMERS})`, {
224
+ code: 'TIMER_LIMIT'
225
+ });
70
226
  }
71
- const timer = setInterval(handler, timeout);
227
+ const wrappedHandler = ensureTimerHandler(handler);
228
+ const timer = setInterval(wrappedHandler, timeout);
72
229
  timers.add(timer);
73
230
  return timer;
74
231
  },
@@ -88,18 +245,47 @@ export async function execute(code) {
88
245
  filename: 'codemode.js'
89
246
  });
90
247
  const context = vm.createContext(sandbox);
91
- const result = await script.runInContext(context, {
248
+ const executionPromise = Promise.resolve(script.runInContext(context, {
92
249
  timeout: EXECUTION_TIMEOUT_MS
250
+ }));
251
+ const timeoutPromise = new Promise((_resolve, reject) => {
252
+ executionTimeout = setTimeout(() => {
253
+ reject(new ExecutionError(`Execution timed out after ${EXECUTION_TIMEOUT_MS}ms`, {
254
+ code: 'TIMEOUT'
255
+ }));
256
+ }, EXECUTION_TIMEOUT_MS);
257
+ });
258
+ const rawResult = await Promise.race([executionPromise, timeoutPromise]);
259
+ if (asyncCallbackError instanceof Error) {
260
+ throw new ExecutionError(asyncCallbackError.message, {
261
+ code: 'ASYNC_CALLBACK_ERROR',
262
+ stackTrace: asyncCallbackError.stack
263
+ });
264
+ }
265
+ const truncated = truncateResult(rawResult);
266
+ return buildEnvelope({
267
+ ok: true,
268
+ result: truncated.result,
269
+ error: null,
270
+ truncatedResult: truncated.truncatedResult,
271
+ resultWasUndefined: truncated.resultWasUndefined
93
272
  });
94
- return truncateOutput(result);
95
273
  }
96
274
  catch (error) {
97
- if (error instanceof Error) {
98
- throw new ExecutionError(error.message, error.stack);
99
- }
100
- throw new ExecutionError(String(error));
275
+ const failure = normalizeFailure(error);
276
+ appendLog('error', [`${failure.code}: ${failure.message}`]);
277
+ return buildEnvelope({
278
+ ok: false,
279
+ result: null,
280
+ error: failure,
281
+ truncatedResult: false,
282
+ resultWasUndefined: false
283
+ });
101
284
  }
102
285
  finally {
286
+ if (executionTimeout) {
287
+ clearTimeout(executionTimeout);
288
+ }
103
289
  timers.forEach((timer) => {
104
290
  clearTimeout(timer);
105
291
  clearInterval(timer);
@@ -0,0 +1,124 @@
1
+ const HELP = {
2
+ version: 1,
3
+ envelope: {
4
+ ok: 'true on success, false on failure',
5
+ result: 'returned value from your code, or null when no return value exists',
6
+ logs: 'captured console output entries from this execution',
7
+ error: 'null on success, otherwise { code, message, stack?, details? }',
8
+ meta: {
9
+ timeoutMs: 'execution timeout limit in milliseconds',
10
+ durationMs: 'elapsed runtime in milliseconds',
11
+ truncatedResult: 'true when result is truncated to output limit',
12
+ truncatedLogs: 'true when logs are truncated to output limit',
13
+ resultWasUndefined: 'true when code finished without an explicit return value'
14
+ }
15
+ },
16
+ apis: {
17
+ repos: [
18
+ { signature: 'repos.list()', description: 'List registered repositories' },
19
+ { signature: 'repos.get(repoId)', description: 'Get one repository by id' },
20
+ {
21
+ signature: 'repos.current()',
22
+ description: 'Resolve current repository when exactly one is registered'
23
+ },
24
+ { signature: 'repos.add(path, name?)', description: 'Register repository path' },
25
+ { signature: 'repos.remove(repoId)', description: 'Remove repository by id' },
26
+ {
27
+ signature: 'repos.refreshGitRepos(repoId)',
28
+ description: 'Refresh discovered nested git repositories'
29
+ }
30
+ ],
31
+ prds: [
32
+ { signature: 'prds.list(repoId)', description: 'List PRDs for repository' },
33
+ { signature: 'prds.getDocument(repoId, prdSlug)', description: 'Load PRD markdown document' },
34
+ { signature: 'prds.getTasks(repoId, prdSlug)', description: 'Load tasks state for PRD' },
35
+ { signature: 'prds.getProgress(repoId, prdSlug)', description: 'Load progress state for PRD' },
36
+ {
37
+ signature: 'prds.getTaskCommits(repoId, prdSlug, taskId)',
38
+ description: 'Resolve task commit references'
39
+ }
40
+ ],
41
+ git: [
42
+ { signature: 'git.getCommits(repoId, shas, repoPath?)', description: 'Load commit metadata' },
43
+ { signature: 'git.getDiff(repoId, commit, repoPath?)', description: 'Load full commit diff' },
44
+ {
45
+ signature: 'git.getFileDiff(repoId, commit, file, repoPath?)',
46
+ description: 'Load diff hunks for one file'
47
+ },
48
+ {
49
+ signature: 'git.getFileContent(repoId, commit, file, repoPath?)',
50
+ description: 'Load file content at commit'
51
+ }
52
+ ],
53
+ state: [
54
+ { signature: 'state.get(repoId, slug)', description: 'Load stored state by repo id' },
55
+ { signature: 'state.getByPath(repoPath, slug)', description: 'Load stored state by repo path' },
56
+ {
57
+ signature: 'state.getCurrent(slug)',
58
+ description: 'Load state for current repository when unambiguous'
59
+ },
60
+ { signature: 'state.summaries(repoId)', description: 'Load PRD state summaries by repo id' },
61
+ { signature: 'state.summariesByPath(repoPath)', description: 'Load PRD state summaries by path' },
62
+ {
63
+ signature: 'state.summariesCurrent()',
64
+ description: 'Load state summaries for current repository when unambiguous'
65
+ },
66
+ { signature: 'state.upsert(repoId, slug, payload)', description: 'Save tasks/progress/notes by repo id' },
67
+ {
68
+ signature: 'state.upsertByPath(repoPath, slug, payload)',
69
+ description: 'Save tasks/progress/notes by repo path'
70
+ },
71
+ {
72
+ signature: 'state.upsertCurrent(slug, payload)',
73
+ description: 'Save state in current repository when unambiguous'
74
+ }
75
+ ]
76
+ },
77
+ examples: [
78
+ {
79
+ title: 'List repos and PRDs',
80
+ code: `const allRepos = await repos.list()\n\nreturn await Promise.all(allRepos.map(async (repo) => ({\n id: repo.id,\n name: repo.name,\n prds: await prds.list(repo.id)\n})))`
81
+ },
82
+ {
83
+ title: 'Use current repo helper',
84
+ code: `const repo = await repos.current()\nconst slug = 'prd-viewer'\n\nreturn {\n repo,\n tasks: await prds.getTasks(repo.id, slug),\n progress: await prds.getProgress(repo.id, slug)\n}`
85
+ },
86
+ {
87
+ title: 'Upsert without repoId',
88
+ code: `await state.upsertCurrent('prd-viewer', {\n notes: '# Updated from MCP'\n})\n\nreturn { saved: true }`
89
+ }
90
+ ]
91
+ };
92
+ function formatMethodList(methods) {
93
+ return methods
94
+ .map((method) => `- \`${method.signature}\` - ${method.description}`)
95
+ .join('\n');
96
+ }
97
+ export function getStewardHelp() {
98
+ return JSON.parse(JSON.stringify(HELP));
99
+ }
100
+ export function getExecuteToolDescription() {
101
+ return [
102
+ 'Run codemode JavaScript with repos, prds, git, and state APIs.',
103
+ '',
104
+ 'Execution always returns a structured JSON envelope:',
105
+ '`{ ok, result, logs, error, meta }`',
106
+ '',
107
+ 'In-sandbox discovery helper:',
108
+ '- `steward.help()`',
109
+ '',
110
+ 'Repository APIs:',
111
+ formatMethodList(HELP.apis.repos),
112
+ '',
113
+ 'PRD APIs:',
114
+ formatMethodList(HELP.apis.prds),
115
+ '',
116
+ 'Git APIs:',
117
+ formatMethodList(HELP.apis.git),
118
+ '',
119
+ 'State APIs:',
120
+ formatMethodList(HELP.apis.state),
121
+ '',
122
+ 'Use `return` in your code to set the envelope `result` field.'
123
+ ].join('\n');
124
+ }