@thxgg/steward 0.1.22 → 0.1.24

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 (50) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/{BhBgWPCX.js → 6tINjQEd.js} +1 -1
  3. package/.output/public/_nuxt/{DUrbBKgG.js → B2mIQf5X.js} +1 -1
  4. package/.output/public/_nuxt/{CL_YIWu4.js → BRQ9Cxaw.js} +1 -1
  5. package/.output/public/_nuxt/{okqELTtf.js → BTmXUZ_s.js} +1 -1
  6. package/.output/public/_nuxt/{B4iDhohi.js → Bc2V3wPK.js} +2 -2
  7. package/.output/public/_nuxt/{BuvhTQbX.js → BknRrWsw.js} +1 -1
  8. package/.output/public/_nuxt/{HZy5mmMm.js → C0BBSDJ7.js} +1 -1
  9. package/.output/public/_nuxt/{Du8Y9zEm.js → C53_p0K1.js} +1 -1
  10. package/.output/public/_nuxt/{C_Sxjx0M.js → C73kduX-.js} +1 -1
  11. package/.output/public/_nuxt/{C_5PUO5r.js → CN46Bgts.js} +1 -1
  12. package/.output/public/_nuxt/{DQivOuJJ.js → CTJgb0zb.js} +1 -1
  13. package/.output/public/_nuxt/{rTDTR03M.js → Cce168lk.js} +5 -5
  14. package/.output/public/_nuxt/{CjDuB5VD.js → CyVSeLw5.js} +1 -1
  15. package/.output/public/_nuxt/{CZWhz2fI.js → T11EuTtn.js} +1 -1
  16. package/.output/public/_nuxt/{rG99Mq9f.js → U78rMDmo.js} +1 -1
  17. package/.output/public/_nuxt/{BQBm6AsP.js → ZNypZshD.js} +1 -1
  18. package/.output/public/_nuxt/builds/latest.json +1 -1
  19. package/.output/public/_nuxt/builds/meta/8c342d49-fe70-4f67-a987-821c16f86125.json +1 -0
  20. package/.output/public/_nuxt/entry.Bw0CE6Iz.css +1 -0
  21. package/.output/public/_nuxt/{CuOw9LiK.js → pYJYAY-W.js} +3 -3
  22. package/.output/server/chunks/_/watcher.mjs +6 -1
  23. package/.output/server/chunks/_/watcher.mjs.map +1 -1
  24. package/.output/server/chunks/build/{Detail-B7yBNjgp.mjs → Detail-DMMUwTWr.mjs} +11 -11
  25. package/.output/server/chunks/build/Detail-DMMUwTWr.mjs.map +1 -0
  26. package/.output/server/chunks/build/{_prd_-DY25apyl.mjs → _prd_-ByugK4Yi.mjs} +3 -2
  27. package/.output/server/chunks/build/_prd_-ByugK4Yi.mjs.map +1 -0
  28. package/.output/server/chunks/build/client.precomputed.mjs +1 -1
  29. package/.output/server/chunks/build/nuxt-link-SvT1nf8Z.mjs +1 -1
  30. package/.output/server/chunks/build/{repo-graph-CUcJKW1F.mjs → repo-graph-DzT45gSB.mjs} +2 -2
  31. package/.output/server/chunks/build/repo-graph-DzT45gSB.mjs.map +1 -0
  32. package/.output/server/chunks/build/server.mjs +3 -3
  33. package/.output/server/chunks/build/styles.mjs +2 -2
  34. package/.output/server/chunks/build/usePrd-hXZOmvAv.mjs +1 -1
  35. package/.output/server/chunks/nitro/nitro.mjs +784 -675
  36. package/.output/server/chunks/nitro/nitro.mjs.map +1 -1
  37. package/.output/server/chunks/routes/renderer.mjs +1 -1
  38. package/.output/server/index.mjs +1 -1
  39. package/.output/server/package.json +1 -1
  40. package/README.md +14 -9
  41. package/dist/host/src/api/repo-context.js +19 -2
  42. package/dist/host/src/prompts.js +60 -36
  43. package/dist/server/utils/change-events.js +117 -0
  44. package/docs/MCP.md +15 -4
  45. package/package.json +1 -1
  46. package/.output/public/_nuxt/builds/meta/3e08a0b0-262b-4fb5-b213-4aa35df11afb.json +0 -1
  47. package/.output/public/_nuxt/entry.xHdymH38.css +0 -1
  48. package/.output/server/chunks/build/Detail-B7yBNjgp.mjs.map +0 -1
  49. package/.output/server/chunks/build/_prd_-DY25apyl.mjs.map +0 -1
  50. package/.output/server/chunks/build/repo-graph-CUcJKW1F.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"nitro.mjs","sources":["../../../../node_modules/destr/dist/index.mjs","../../../../node_modules/ufo/dist/index.mjs","../../../../node_modules/radix3/dist/index.mjs","../../../../node_modules/defu/dist/defu.mjs","../../../../node_modules/node-mock-http/dist/index.mjs","../../../../node_modules/h3/dist/index.mjs","../../../../node_modules/hookable/dist/index.mjs","../../../../node_modules/node-fetch-native/dist/native.mjs","../../../../node_modules/ofetch/dist/shared/ofetch.CWycOUEr.mjs","../../../../node_modules/ofetch/dist/node.mjs","../../../../node_modules/unstorage/dist/shared/unstorage.zVDD2mZo.mjs","../../../../node_modules/unstorage/dist/index.mjs","../../../../node_modules/unstorage/drivers/utils/index.mjs","../../../../node_modules/unstorage/drivers/utils/node-fs.mjs","../../../../node_modules/unstorage/drivers/fs-lite.mjs","../../../../node_modules/nitropack/dist/runtime/internal/storage.mjs","../../../../node_modules/ohash/dist/shared/ohash.D__AXeF1.mjs","../../../../node_modules/ohash/dist/crypto/node/index.mjs","../../../../node_modules/ohash/dist/index.mjs","../../../../node_modules/nitropack/dist/runtime/internal/hash.mjs","../../../../node_modules/nitropack/dist/runtime/internal/cache.mjs","../../../../node_modules/klona/dist/index.mjs","../../../../node_modules/scule/dist/index.mjs","../../../../node_modules/nitropack/dist/runtime/internal/utils.env.mjs","../../../../node_modules/nitropack/dist/runtime/internal/config.mjs","../../../../node_modules/unctx/dist/index.mjs","../../../../node_modules/nitropack/dist/runtime/internal/context.mjs","../../../../node_modules/nitropack/dist/runtime/internal/route-rules.mjs","../../../../node_modules/nitropack/dist/runtime/internal/utils.mjs","../../../../node_modules/@nuxt/nitro-server/dist/runtime/utils/error.mjs","../../../../node_modules/@nuxt/nitro-server/dist/runtime/handlers/error.mjs","../../../../node_modules/nitropack/dist/runtime/internal/error/utils.mjs","../../../../node_modules/nitropack/dist/runtime/internal/error/prod.mjs","../../../../node_modules/@nuxtjs/color-mode/dist/runtime/nitro-plugin.js","../../../../node_modules/nitropack/dist/runtime/internal/plugin.mjs","../../../../server/utils/change-events.ts","../../../../server/utils/db.ts","../../../../server/utils/git-repo-path.ts","../../../../server/utils/git.ts","../../../../server/utils/repos.ts","../../../../server/utils/state-schema.ts","../../../../server/utils/state-migration.ts","../../../../server/plugins/00-state-migration.ts","../../../../node_modules/pathe/dist/shared/pathe.M-eThtNZ.mjs","../../../../node_modules/nitropack/dist/runtime/internal/static.mjs","../../../../server/middleware/00-security.ts","../../../../node_modules/nitropack/dist/runtime/internal/app.mjs","../../../../node_modules/nitropack/dist/runtime/internal/renderer.mjs","../../../../node_modules/nitropack/dist/runtime/internal/lib/http-graceful-shutdown.mjs","../../../../node_modules/nitropack/dist/runtime/internal/shutdown.mjs","../../../../node_modules/nitropack/dist/presets/node/runtime/node-server.mjs"],"names":["getQuery","createRouter","f","h","c","i","l","createError","mergeHeaders","s","nodeFetch","Headers","Headers$1","AbortController$1","normalizeKey","defineDriver","DRIVER_NAME","dirname","fsPromises","resolve","fsp","serialize","hash","_inlineAppConfig","createRadixRouter","fs","normalizePathSlashes","isAbsolute","isGitRepo","nitroApp","callNodeRequestHandler","fetchNodeRequestHandler","gracefulShutdown","HttpsServer","HttpServer"],"mappings":"","x_google_ignoreList":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,43,44,46,47,48,49,50]}
1
+ {"version":3,"file":"nitro.mjs","sources":["../../../../node_modules/destr/dist/index.mjs","../../../../node_modules/ufo/dist/index.mjs","../../../../node_modules/radix3/dist/index.mjs","../../../../node_modules/defu/dist/defu.mjs","../../../../node_modules/node-mock-http/dist/index.mjs","../../../../node_modules/h3/dist/index.mjs","../../../../node_modules/hookable/dist/index.mjs","../../../../node_modules/node-fetch-native/dist/native.mjs","../../../../node_modules/ofetch/dist/shared/ofetch.CWycOUEr.mjs","../../../../node_modules/ofetch/dist/node.mjs","../../../../node_modules/unstorage/dist/shared/unstorage.zVDD2mZo.mjs","../../../../node_modules/unstorage/dist/index.mjs","../../../../node_modules/unstorage/drivers/utils/index.mjs","../../../../node_modules/unstorage/drivers/utils/node-fs.mjs","../../../../node_modules/unstorage/drivers/fs-lite.mjs","../../../../node_modules/nitropack/dist/runtime/internal/storage.mjs","../../../../node_modules/ohash/dist/shared/ohash.D__AXeF1.mjs","../../../../node_modules/ohash/dist/crypto/node/index.mjs","../../../../node_modules/ohash/dist/index.mjs","../../../../node_modules/nitropack/dist/runtime/internal/hash.mjs","../../../../node_modules/nitropack/dist/runtime/internal/cache.mjs","../../../../node_modules/klona/dist/index.mjs","../../../../node_modules/scule/dist/index.mjs","../../../../node_modules/nitropack/dist/runtime/internal/utils.env.mjs","../../../../node_modules/nitropack/dist/runtime/internal/config.mjs","../../../../node_modules/unctx/dist/index.mjs","../../../../node_modules/nitropack/dist/runtime/internal/context.mjs","../../../../node_modules/nitropack/dist/runtime/internal/route-rules.mjs","../../../../node_modules/nitropack/dist/runtime/internal/utils.mjs","../../../../node_modules/@nuxt/nitro-server/dist/runtime/utils/error.mjs","../../../../node_modules/@nuxt/nitro-server/dist/runtime/handlers/error.mjs","../../../../node_modules/nitropack/dist/runtime/internal/error/utils.mjs","../../../../node_modules/nitropack/dist/runtime/internal/error/prod.mjs","../../../../node_modules/@nuxtjs/color-mode/dist/runtime/nitro-plugin.js","../../../../node_modules/nitropack/dist/runtime/internal/plugin.mjs","../../../../server/utils/db.ts","../../../../server/utils/change-events.ts","../../../../server/utils/git-repo-path.ts","../../../../server/utils/git.ts","../../../../server/utils/repos.ts","../../../../server/utils/state-schema.ts","../../../../server/utils/state-migration.ts","../../../../server/plugins/00-state-migration.ts","../../../../node_modules/pathe/dist/shared/pathe.M-eThtNZ.mjs","../../../../node_modules/nitropack/dist/runtime/internal/static.mjs","../../../../server/middleware/00-security.ts","../../../../node_modules/nitropack/dist/runtime/internal/app.mjs","../../../../node_modules/nitropack/dist/runtime/internal/renderer.mjs","../../../../node_modules/nitropack/dist/runtime/internal/lib/http-graceful-shutdown.mjs","../../../../node_modules/nitropack/dist/runtime/internal/shutdown.mjs","../../../../node_modules/nitropack/dist/presets/node/runtime/node-server.mjs"],"names":["getQuery","createRouter","f","h","c","i","l","createError","mergeHeaders","s","nodeFetch","Headers","Headers$1","AbortController$1","normalizeKey","defineDriver","DRIVER_NAME","dirname","fsPromises","resolve","fsp","serialize","hash","_inlineAppConfig","createRadixRouter","fs","normalizePathSlashes","isAbsolute","isGitRepo","nitroApp","callNodeRequestHandler","fetchNodeRequestHandler","gracefulShutdown","HttpsServer","HttpServer"],"mappings":"","x_google_ignoreList":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,43,44,46,47,48,49,50]}
@@ -1,5 +1,5 @@
1
1
  import { createRenderer, getRequestDependencies, getPreloadLinks, getPrefetchLinks } from 'vue-bundle-renderer/runtime';
2
- import { K as joinRelativeURL, F as useRuntimeConfig, L as getResponseStatusText, M as getResponseStatus, N as defineRenderHandler, g as getQuery, c as createError, O as destr, P as getRouteRules, Q as joinURL, R as useNitroApp } from '../nitro/nitro.mjs';
2
+ import { M as joinRelativeURL, F as useRuntimeConfig, N as getResponseStatusText, O as getResponseStatus, P as defineRenderHandler, g as getQuery, c as createError, Q as destr, R as getRouteRules, S as joinURL, T as useNitroApp } from '../nitro/nitro.mjs';
3
3
  import { renderToString } from 'vue/server-renderer';
4
4
  import { createHead as createHead$1, propsToString, renderSSRHead } from 'unhead/server';
5
5
  import { stringify, uneval } from 'devalue';
@@ -1,6 +1,6 @@
1
1
  import process from 'node:process';globalThis._importMeta_={url:import.meta.url,env:process.env};import 'node:http';
2
2
  import 'node:https';
3
- export { a6 as default } from './chunks/nitro/nitro.mjs';
3
+ export { a8 as default } from './chunks/nitro/nitro.mjs';
4
4
  import 'node:events';
5
5
  import 'node:buffer';
6
6
  import 'node:fs';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thxgg/steward-prod",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "dependencies": {
package/README.md CHANGED
@@ -33,7 +33,7 @@ Add to your MCP client config:
33
33
  ```json
34
34
  {
35
35
  "mcpServers": {
36
- "prd": {
36
+ "steward": {
37
37
  "command": "npx",
38
38
  "args": ["-y", "@thxgg/steward", "mcp"]
39
39
  }
@@ -41,6 +41,8 @@ Add to your MCP client config:
41
41
  }
42
42
  ```
43
43
 
44
+ The MCP server key (`steward` above) controls the prompt prefix in slash commands.
45
+
44
46
  Steward MCP requires a Node runtime with built-in sqlite support (`node:sqlite`) for `repos`, `prds`, and `state` APIs.
45
47
  If you see `ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite`, run with sqlite enabled:
46
48
 
@@ -89,10 +91,14 @@ Steward exposes one MCP tool: `execute`.
89
91
  It also exposes workflow prompts:
90
92
 
91
93
  - `create_prd(feature_request)`
92
- - `break_into_tasks(prd_slug)`
93
- - `complete_next_task(prd_slug)`
94
+ - `break_into_tasks(prd_slug?)`
95
+ - `complete_next_task(prd_slug?)`
96
+
97
+ In OpenCode these are shown as MCP slash entries like `/steward:create_prd:mcp` and inserted as `/steward:create_prd`.
94
98
 
95
- In OpenCode these are shown as MCP slash entries like `/prd:create_prd:mcp`.
99
+ - `prd_slug` is optional for break/complete prompts.
100
+ - When omitted, Steward workflows auto-resolve the slug from repo state.
101
+ - `complete_next_task` includes required commit hygiene (one-line commit message, no `Co-authored-by`, no task-related dirty changes left behind).
96
102
 
97
103
  ```js
98
104
  const repo = await repos.current()
@@ -175,13 +181,12 @@ npm run build
175
181
  | `PRD_STATE_HOME` | Base directory for DB (`state.db` inside) |
176
182
  | `XDG_DATA_HOME` | Fallback base path for default DB location |
177
183
 
178
- ## OpenCode Bundle
184
+ ## OpenCode Integration
179
185
 
180
- This repo includes curated OpenCode assets under `opencode/`:
186
+ Steward now uses MCP-registered prompts as the single workflow surface.
181
187
 
182
- - Commands: `prd`, `prd-task`, `complete-next-task`, `commit`
183
- - Skills: `prd`, `prd-task`, `complete-next-task`, `commit`
184
- - Script: `prd-db.mjs`
188
+ - Use MCP prompts directly (for example `/steward:create_prd`, `/steward:break_into_tasks`, `/steward:complete_next_task`).
189
+ - This repository no longer ships separate OpenCode command/skill bundles for PRD workflows.
185
190
 
186
191
  ## License
187
192
 
@@ -1,4 +1,4 @@
1
- import { resolve } from 'node:path';
1
+ import { isAbsolute, relative, resolve } from 'node:path';
2
2
  import { getRepoById, getRepos } from '../../../server/utils/repos.js';
3
3
  export class RepoLookupError extends Error {
4
4
  code;
@@ -22,6 +22,18 @@ function formatKnownRepos(knownRepos) {
22
22
  .map((repo) => `${repo.id} (${repo.name}) ${repo.path}`)
23
23
  .join('; ');
24
24
  }
25
+ function isPathWithin(basePath, candidatePath) {
26
+ const relativePath = relative(resolve(basePath), resolve(candidatePath));
27
+ return relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath));
28
+ }
29
+ function resolveRepoFromCwd(repos, cwd) {
30
+ const matches = repos.filter((repo) => isPathWithin(repo.path, cwd));
31
+ if (matches.length === 0) {
32
+ return null;
33
+ }
34
+ matches.sort((left, right) => resolve(right.path).length - resolve(left.path).length);
35
+ return matches[0] ?? null;
36
+ }
25
37
  export async function requireRepo(repoId) {
26
38
  const repo = await getRepoById(repoId);
27
39
  if (repo) {
@@ -56,5 +68,10 @@ export async function requireCurrentRepo() {
56
68
  if (knownRepos.length === 0) {
57
69
  throw new RepoLookupError('No repositories are registered. Use repos.add(path) first.', 'NO_REPOS', { knownRepos });
58
70
  }
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 });
71
+ const cwd = resolve(process.cwd());
72
+ const repoFromCwd = resolveRepoFromCwd(allRepos, cwd);
73
+ if (repoFromCwd) {
74
+ return repoFromCwd;
75
+ }
76
+ throw new RepoLookupError(`Cannot resolve a current repository because ${knownRepos.length} repositories are registered and the working directory does not map to a registered repo. Use an explicit repoId or by-path API. CWD: ${cwd}. Known repositories: ${formatKnownRepos(knownRepos)}`, 'AMBIGUOUS_REPO', { cwd, knownRepos });
60
77
  }
@@ -33,64 +33,88 @@ function createPrdPrompt(featureRequest) {
33
33
  ' - created PRD path',
34
34
  ' - chosen slug',
35
35
  ' - key product decisions and open questions',
36
- ' - recommended next command: /prd:break_into_tasks <slug> (shown as :mcp in autocomplete).'
36
+ ' - recommended next MCP prompt: /<your-mcp-server-prefix>:break_into_tasks <slug> (autocomplete may show an extra :mcp suffix).'
37
37
  ].join('\n');
38
38
  }
39
39
  function breakIntoTasksPrompt(prdSlug) {
40
+ const resolvedInput = typeof prdSlug === 'string' ? prdSlug.trim() : '';
40
41
  return [
41
42
  'You are converting a PRD into structured Steward task state.',
42
43
  '',
43
- 'PRD slug:',
44
- prdSlug,
44
+ `PRD slug input: ${resolvedInput || '<auto-resolve>'}`,
45
45
  '',
46
46
  'Workflow:',
47
- '1. Load docs/prd/<slug>.md. If not found, list available PRDs and stop with a clear correction.',
48
- '2. Update PRD status from Draft to Approved in the markdown file.',
49
- '3. Build tasks JSON with this shape:',
47
+ '1. Resolve repository and PRD slug before writing anything:',
48
+ ' - Resolve repo with execute tool: const repo = await repos.current().',
49
+ ' - If slug input is present, use it as resolvedSlug.',
50
+ ' - If slug input is missing, list PRDs: const prdList = await prds.list(repo.id).',
51
+ ' - Auto-pick resolvedSlug deterministically:',
52
+ ' a) prefer PRDs with hasState=false, newest modifiedAt first, then slug ascending',
53
+ ' b) otherwise pick newest modifiedAt, then slug ascending',
54
+ ' - If no PRDs exist, stop and ask user to run create_prd first.',
55
+ '2. Load docs/prd/<resolvedSlug>.md. If missing, list available PRDs and stop with a clear correction.',
56
+ '3. Update PRD status from Draft to Approved in the markdown file.',
57
+ '4. Build tasks JSON with this shape:',
50
58
  ' { prd: { name, source, createdAt }, tasks: [{ id, category, title, description, steps, passes, dependencies, priority, status }] }',
51
- '4. Use these category values only: setup, feature, integration, testing, documentation.',
52
- '5. Use these priority values only: critical, high, medium, low.',
53
- '6. Set every generated task status to pending and make passes an array of testable criteria.',
54
- '7. Build progress JSON with this shape:',
59
+ '5. Use these category values only: setup, feature, integration, testing, documentation.',
60
+ '6. Use these priority values only: critical, high, medium, low.',
61
+ '7. Set every generated task status to pending and make passes an array of testable criteria.',
62
+ '8. Build progress JSON with this shape:',
55
63
  ' { prdName, totalTasks, completed, inProgress, blocked, startedAt, lastUpdated, patterns, taskLogs }',
56
64
  ' Initialize with completed=0, inProgress=0, blocked=0, startedAt=null, patterns=[], taskLogs=[].',
57
- '8. Persist state through execute tool:',
58
- ` await state.upsertCurrent("${prdSlug}", { tasks: <tasksJson>, progress: <progressJson> })`,
59
- ` return await state.getCurrent("${prdSlug}")`,
60
- '9. Report task count, category breakdown, critical path, and recommended next command:',
61
- ` /prd:complete_next_task ${prdSlug} (shown as :mcp in autocomplete).`
65
+ '9. Persist state through execute tool:',
66
+ ' await state.upsertCurrent(resolvedSlug, { tasks: tasksJson, progress: progressJson })',
67
+ ' return await state.getCurrent(resolvedSlug)',
68
+ '10. Report task count, category breakdown, critical path, resolvedSlug, and recommended next MCP prompt:',
69
+ ' /<your-mcp-server-prefix>:complete_next_task <resolvedSlug> (autocomplete may show an extra :mcp suffix).'
62
70
  ].join('\n');
63
71
  }
64
72
  function completeNextTaskPrompt(prdSlug) {
73
+ const resolvedInput = typeof prdSlug === 'string' ? prdSlug.trim() : '';
65
74
  return [
66
75
  'You are completing the next PRD task with Steward state tracking and commits.',
67
76
  '',
68
- 'PRD slug:',
69
- prdSlug,
77
+ `PRD slug input: ${resolvedInput || '<auto-resolve>'}`,
70
78
  '',
71
79
  'Workflow:',
72
- `1. Load PRD state using execute tool: state.getCurrent("${prdSlug}").`,
73
- ` If tasks are missing, stop and instruct to run /prd:break_into_tasks ${prdSlug}.`,
74
- '2. Select the next task:',
80
+ '1. Resolve repository and PRD slug before any task updates:',
81
+ ' - Resolve repo with execute tool: const repo = await repos.current().',
82
+ ' - If slug input is present, use it as resolvedSlug.',
83
+ ' - If slug input is missing, list PRDs: const prdList = await prds.list(repo.id).',
84
+ ' - Build actionable candidates where hasState=true and completedCount < taskCount.',
85
+ ' - Auto-pick resolvedSlug as the latest actionable candidate (modifiedAt desc, then slug asc).',
86
+ ' - If no actionable candidates exist, fall back to latest PRD with hasState=true, then latest PRD overall.',
87
+ ' - If no PRD exists, stop and ask user to run create_prd first.',
88
+ '2. Load PRD state with execute tool: state.getCurrent(resolvedSlug).',
89
+ ' If tasks are missing, stop and instruct to run /<your-mcp-server-prefix>:break_into_tasks <resolvedSlug>.',
90
+ '3. Select the next task:',
75
91
  ' - first task with status=in_progress, otherwise',
76
92
  ' - first pending task whose dependencies are all completed.',
77
- '3. Immediately mark the task in progress and save with state.upsertCurrent:',
93
+ '4. Immediately mark the task in progress and save with state.upsertCurrent:',
78
94
  ' - task.status = in_progress',
79
95
  ' - task.startedAt = ISO timestamp (if absent)',
80
96
  ' - progress.inProgress updated',
81
97
  ' - progress.lastUpdated updated',
82
98
  ' - taskLogs entry exists with { taskId, status: in_progress, startedAt }',
83
- '4. Implement only this task in repository files.',
84
- '5. Run validation loops relevant to the project before commit (typecheck, tests, lint, format/build where applicable).',
85
- '6. Commit task-related changes with a concise conventional commit message.',
86
- '7. Capture commit SHAs and update taskLogs[].commits (use { sha, repo } objects for nested repos when needed).',
87
- '8. Mark task completed and save with state.upsertCurrent:',
99
+ '5. Implement only this task in repository files.',
100
+ '6. Run validation loops relevant to the project before commit (typecheck, tests, lint, format/build where applicable).',
101
+ '7. Commit requirements (mandatory when task-related files changed):',
102
+ ' - Stage only task-related files (do not stage unrelated work).',
103
+ ' - Create at least one commit before finishing when task-related changes exist.',
104
+ ' - Use a one-line conventional commit subject (single -m line, no body).',
105
+ ' - Do not include trailers or footers, especially no Co-authored-by.',
106
+ ' - Prefer execute tool git.commitIfChanged(repo.id, message, { paths: taskRelatedFiles }).',
107
+ ' - If task-related changes exist, commit result must be committed=true before finishing.',
108
+ ' - If commit fails due to checks/hooks, fix issues and create a new commit.',
109
+ ' - Verify task-related files are not left uncommitted before final output.',
110
+ '8. Capture commit SHAs and update taskLogs[].commits (use { sha, repo } objects for nested repos when needed).',
111
+ '9. Mark task completed and save with state.upsertCurrent:',
88
112
  ' - task.status = completed',
89
113
  ' - task.completedAt = ISO timestamp',
90
114
  ' - progress.completed / progress.inProgress / progress.lastUpdated updated',
91
115
  ' - taskLogs entry updated with completedAt, implemented, filesChanged, learnings, commits',
92
- '9. If all tasks are completed, output exactly: <tasks>COMPLETE</tasks>.',
93
- '10. Report what changed, which commit(s) were created, and the next pending task if any.'
116
+ '10. If all tasks are completed, output exactly: <tasks>COMPLETE</tasks>.',
117
+ '11. Report what changed, resolvedSlug, which commit(s) were created, and the next pending task if any.'
94
118
  ].join('\n');
95
119
  }
96
120
  export function registerStewardPrompts(server) {
@@ -100,28 +124,28 @@ export function registerStewardPrompts(server) {
100
124
  argsSchema: {
101
125
  feature_request: z.string().describe('Feature request, idea, or problem statement for the PRD')
102
126
  }
103
- }, async ({ feature_request }) => ({
127
+ }, async (args) => ({
104
128
  description: 'Guided workflow for creating a PRD file.',
105
- messages: [textMessage(createPrdPrompt(feature_request))]
129
+ messages: [textMessage(createPrdPrompt(args.feature_request))]
106
130
  }));
107
131
  server.registerPrompt('break_into_tasks', {
108
132
  title: 'Break Into Tasks',
109
133
  description: 'Convert a PRD into tasks.json/progress.json style state and save it.',
110
134
  argsSchema: {
111
- prd_slug: z.string().describe('PRD slug, usually the filename in docs/prd without .md')
135
+ prd_slug: z.string().optional().describe('Optional PRD slug (filename in docs/prd without .md). Auto-resolved when omitted.')
112
136
  }
113
- }, async ({ prd_slug }) => ({
137
+ }, async (args) => ({
114
138
  description: 'Workflow for converting an approved PRD into task state.',
115
- messages: [textMessage(breakIntoTasksPrompt(prd_slug))]
139
+ messages: [textMessage(breakIntoTasksPrompt(args.prd_slug))]
116
140
  }));
117
141
  server.registerPrompt('complete_next_task', {
118
142
  title: 'Complete Next Task',
119
143
  description: 'Complete the next in-progress/pending task and persist state updates.',
120
144
  argsSchema: {
121
- prd_slug: z.string().describe('PRD slug whose next task should be completed')
145
+ prd_slug: z.string().optional().describe('Optional PRD slug whose next task should be completed. Auto-resolved when omitted.')
122
146
  }
123
- }, async ({ prd_slug }) => ({
147
+ }, async (args) => ({
124
148
  description: 'Workflow for task execution, verification, commit capture, and state persistence.',
125
- messages: [textMessage(completeNextTaskPrompt(prd_slug))]
149
+ messages: [textMessage(completeNextTaskPrompt(args.prd_slug))]
126
150
  }));
127
151
  }
@@ -1,7 +1,118 @@
1
+ import { dbAll } from './db.js';
1
2
  const listeners = new Set();
2
3
  const pendingEvents = [];
3
4
  let debounceTimer = null;
5
+ let stateChangePollTimer = null;
6
+ let stateChangePollInFlight = false;
7
+ let hasSeededStateSnapshot = false;
8
+ const lastStateSnapshot = new Map();
4
9
  const DEBOUNCE_MS = 300;
10
+ const STATE_CHANGE_POLL_INTERVAL_MS = 1000;
11
+ function toStateSnapshotKey(repoId, slug) {
12
+ return `${repoId}:${slug}`;
13
+ }
14
+ function fromStateSnapshotKey(key) {
15
+ const separatorIndex = key.indexOf(':');
16
+ if (separatorIndex <= 0 || separatorIndex >= key.length - 1) {
17
+ return null;
18
+ }
19
+ return {
20
+ repoId: key.slice(0, separatorIndex),
21
+ slug: key.slice(separatorIndex + 1)
22
+ };
23
+ }
24
+ function emitStateRowChange(repoId, slug) {
25
+ emitChange({
26
+ type: 'change',
27
+ path: `state://${repoId}/${slug}/tasks.json`,
28
+ repoId,
29
+ category: 'tasks'
30
+ });
31
+ emitChange({
32
+ type: 'change',
33
+ path: `state://${repoId}/${slug}/progress.json`,
34
+ repoId,
35
+ category: 'progress'
36
+ });
37
+ }
38
+ async function pollPrdStateChanges() {
39
+ if (stateChangePollInFlight) {
40
+ return;
41
+ }
42
+ stateChangePollInFlight = true;
43
+ try {
44
+ const rows = await dbAll(`
45
+ SELECT repo_id, slug, updated_at
46
+ FROM prd_states
47
+ ORDER BY repo_id ASC, slug ASC
48
+ `);
49
+ const nextSnapshot = new Map();
50
+ const changedRows = [];
51
+ for (const row of rows) {
52
+ const key = toStateSnapshotKey(row.repo_id, row.slug);
53
+ nextSnapshot.set(key, row.updated_at);
54
+ if (!hasSeededStateSnapshot) {
55
+ continue;
56
+ }
57
+ const previousUpdatedAt = lastStateSnapshot.get(key);
58
+ if (previousUpdatedAt === undefined || previousUpdatedAt !== row.updated_at) {
59
+ changedRows.push({ repoId: row.repo_id, slug: row.slug });
60
+ }
61
+ }
62
+ if (hasSeededStateSnapshot) {
63
+ for (const key of lastStateSnapshot.keys()) {
64
+ if (nextSnapshot.has(key)) {
65
+ continue;
66
+ }
67
+ const parsed = fromStateSnapshotKey(key);
68
+ if (parsed) {
69
+ changedRows.push(parsed);
70
+ }
71
+ }
72
+ }
73
+ lastStateSnapshot.clear();
74
+ for (const [key, updatedAt] of nextSnapshot.entries()) {
75
+ lastStateSnapshot.set(key, updatedAt);
76
+ }
77
+ if (!hasSeededStateSnapshot) {
78
+ hasSeededStateSnapshot = true;
79
+ return;
80
+ }
81
+ for (const row of changedRows) {
82
+ emitStateRowChange(row.repoId, row.slug);
83
+ }
84
+ }
85
+ catch {
86
+ // Ignore transient polling failures.
87
+ }
88
+ finally {
89
+ stateChangePollInFlight = false;
90
+ }
91
+ }
92
+ export function startStateChangePolling() {
93
+ if (listeners.size === 0 || stateChangePollTimer) {
94
+ return;
95
+ }
96
+ void pollPrdStateChanges();
97
+ stateChangePollTimer = setInterval(() => {
98
+ void pollPrdStateChanges();
99
+ }, STATE_CHANGE_POLL_INTERVAL_MS);
100
+ }
101
+ export function stopStateChangePolling() {
102
+ if (stateChangePollTimer) {
103
+ clearInterval(stateChangePollTimer);
104
+ stateChangePollTimer = null;
105
+ }
106
+ stateChangePollInFlight = false;
107
+ hasSeededStateSnapshot = false;
108
+ lastStateSnapshot.clear();
109
+ }
110
+ export function getStateChangePollingStatus() {
111
+ return {
112
+ active: stateChangePollTimer !== null,
113
+ intervalMs: STATE_CHANGE_POLL_INTERVAL_MS
114
+ };
115
+ }
5
116
  function flushPendingEvents() {
6
117
  const dedupedEvents = new Map();
7
118
  for (const event of pendingEvents) {
@@ -24,8 +135,14 @@ export function emitChange(event) {
24
135
  }
25
136
  export function addChangeListener(listener) {
26
137
  listeners.add(listener);
138
+ if (listeners.size === 1) {
139
+ startStateChangePolling();
140
+ }
27
141
  return () => {
28
142
  listeners.delete(listener);
143
+ if (listeners.size === 0) {
144
+ stopStateChangePolling();
145
+ }
29
146
  };
30
147
  }
31
148
  export function getChangeListenerCount() {
package/docs/MCP.md CHANGED
@@ -40,7 +40,7 @@ Example MCP client config:
40
40
  ```json
41
41
  {
42
42
  "mcpServers": {
43
- "prd": {
43
+ "steward": {
44
44
  "command": "prd",
45
45
  "args": ["mcp"]
46
46
  }
@@ -48,6 +48,8 @@ Example MCP client config:
48
48
  }
49
49
  ```
50
50
 
51
+ The MCP server key (`steward` above) determines slash prefix names in clients.
52
+
51
53
  ## Runtime Requirements
52
54
 
53
55
  - `repos`, `prds`, and `state` APIs require sqlite runtime support.
@@ -105,10 +107,19 @@ In-sandbox discovery helper:
105
107
  Steward also exposes MCP prompts so MCP clients can surface command-like workflows.
106
108
 
107
109
  - `create_prd(feature_request)`
108
- - `break_into_tasks(prd_slug)`
109
- - `complete_next_task(prd_slug)`
110
+ - `break_into_tasks(prd_slug?)`
111
+ - `complete_next_task(prd_slug?)`
112
+
113
+ In OpenCode, these appear in slash-command autocomplete as MCP commands (for example `/steward:create_prd:mcp`) and insert as `/steward:create_prd` when selected.
114
+
115
+ Notes:
110
116
 
111
- In OpenCode, these appear in slash-command autocomplete as MCP commands (for example `/prd:create_prd:mcp`) and insert as `/prd:create_prd` when selected.
117
+ - `prd_slug` is optional for break/complete prompts and auto-resolves when omitted.
118
+ - `complete_next_task` requires commit hygiene when task-related files changed:
119
+ - at least one commit is created
120
+ - one-line commit subject only
121
+ - no `Co-authored-by` footer/trailers
122
+ - no task-related dirty files left behind
112
123
 
113
124
  ## Available APIs
114
125
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thxgg/steward",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "Local-first PRD workflow steward with codemode MCP and web UI.",
5
5
  "type": "module",
6
6
  "author": "thxgg",
@@ -1 +0,0 @@
1
- {"id":"3e08a0b0-262b-4fb5-b213-4aa35df11afb","timestamp":1772208361233,"prerendered":[]}