@thxgg/steward 0.1.23 → 0.1.25
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.
- package/.output/nitro.json +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/9ce7f1bc-d5e2-47bf-8026-f4910c257b2e.json +1 -0
- package/.output/server/chunks/_/prd-service.mjs.map +1 -1
- package/.output/server/chunks/build/styles.mjs +2 -2
- package/.output/server/chunks/nitro/nitro.mjs +873 -571
- package/.output/server/chunks/nitro/nitro.mjs.map +1 -1
- package/.output/server/package.json +1 -1
- package/README.md +55 -9
- package/bin/prd +1 -1
- package/dist/host/src/api/repo-context.js +19 -2
- package/dist/host/src/index.js +10 -0
- package/dist/host/src/prompts.js +60 -36
- package/dist/host/src/sync.js +201 -0
- package/dist/server/utils/db.js +64 -0
- package/dist/server/utils/prd-state.js +24 -2
- package/dist/server/utils/repos.js +12 -2
- package/dist/server/utils/state-migration.js +4 -3
- package/dist/server/utils/sync-apply.js +380 -0
- package/dist/server/utils/sync-export.js +183 -0
- package/dist/server/utils/sync-identity.js +231 -0
- package/dist/server/utils/sync-inspect.js +103 -0
- package/dist/server/utils/sync-merge.js +579 -0
- package/dist/server/utils/sync-schema.js +100 -0
- package/docs/MCP.md +15 -4
- package/package.json +1 -1
- package/.output/public/_nuxt/builds/meta/bd99c09c-d991-4bcb-8c66-ab2088e1da03.json +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/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,
|
|
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/sync-identity.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","execGit","isGitRepo","nowIso","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,44,45,47,48,49,50,51]}
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Add to your MCP client config:
|
|
|
33
33
|
```json
|
|
34
34
|
{
|
|
35
35
|
"mcpServers": {
|
|
36
|
-
"
|
|
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
|
|
|
@@ -56,14 +58,55 @@ Note: `execute` runs in a VM sandbox by design, so globals like `process` are in
|
|
|
56
58
|
prd ui
|
|
57
59
|
prd ui --port 3100 --host 127.0.0.1
|
|
58
60
|
prd mcp
|
|
61
|
+
prd sync export ./steward-sync.json
|
|
62
|
+
prd sync inspect ./steward-sync.json
|
|
63
|
+
prd sync merge ./steward-sync.json
|
|
64
|
+
prd sync merge ./steward-sync.json --apply
|
|
65
|
+
prd sync merge ./steward-sync.json --apply --map rsk_source=/Users/you/Projects/repo
|
|
59
66
|
```
|
|
60
67
|
|
|
68
|
+
### Sync Bundles (Cross-Device)
|
|
69
|
+
|
|
70
|
+
Steward supports local-first state sharing across devices using portable JSON bundles.
|
|
71
|
+
|
|
72
|
+
- `prd sync export <bundle-path>` writes a versioned bundle of repos/state/archives.
|
|
73
|
+
- `prd sync inspect <bundle-path>` validates and summarizes bundle contents.
|
|
74
|
+
- `prd sync merge <bundle-path>` plans a merge in dry-run mode by default.
|
|
75
|
+
- `prd sync merge <bundle-path> --apply` applies the planned merge transactionally.
|
|
76
|
+
|
|
77
|
+
Path hint privacy defaults:
|
|
78
|
+
|
|
79
|
+
- Export defaults to `--path-hints basename` to avoid leaking absolute filesystem paths.
|
|
80
|
+
- Use `--path-hints none` to omit path hints entirely.
|
|
81
|
+
- Use `--path-hints absolute` only when you explicitly want full paths in the bundle.
|
|
82
|
+
|
|
83
|
+
Representative command output summaries:
|
|
84
|
+
|
|
85
|
+
- `sync export` prints bundle path, bundle id, and row totals (`repos/states/archives`).
|
|
86
|
+
- `sync inspect` prints bundle metadata (`bundleId`, `sourceDeviceId`, format version), totals, and unknown references.
|
|
87
|
+
- `sync merge` prints mapping totals, state/archive action counts, and conflict counts.
|
|
88
|
+
- `sync merge --apply` also prints backup path and retention cleanup counts.
|
|
89
|
+
|
|
90
|
+
Safety defaults and retention:
|
|
91
|
+
|
|
92
|
+
- Merge defaults to dry-run; no writes happen unless `--apply` is provided.
|
|
93
|
+
- Apply creates a SQLite backup before any write and runs an integrity check before commit.
|
|
94
|
+
- Re-applying the same bundle id is idempotent and returns a no-op result.
|
|
95
|
+
- Default retention keeps backups for 30 days (max 20 files) and sync apply logs for 180 days (max 10,000 rows).
|
|
96
|
+
|
|
97
|
+
Troubleshooting sync:
|
|
98
|
+
|
|
99
|
+
- Unresolved mapping on apply: run `prd sync inspect <bundle-path>` and pass one or more `--map <incomingRepoSyncKey>=<localPathOrRepoRef>` values.
|
|
100
|
+
- Expected no-op reapply: if bundle id was already applied, merge returns "already applied" and leaves state unchanged.
|
|
101
|
+
- Restore from backup: stop Steward processes, then replace your DB file with a backup in the same directory (files match `state.db.sync-backup.*.db`).
|
|
102
|
+
|
|
61
103
|
## Architecture
|
|
62
104
|
|
|
63
105
|
```
|
|
64
106
|
┌─────────────────────────────────────────┐
|
|
65
107
|
│ Steward CLI (Node) │
|
|
66
108
|
│ - `prd ui` runs prebuilt UI server │
|
|
109
|
+
│ - `prd sync` manages state bundles │
|
|
67
110
|
│ - `prd mcp` starts MCP over stdio │
|
|
68
111
|
└─────────────────────────────────────────┘
|
|
69
112
|
│
|
|
@@ -89,10 +132,14 @@ Steward exposes one MCP tool: `execute`.
|
|
|
89
132
|
It also exposes workflow prompts:
|
|
90
133
|
|
|
91
134
|
- `create_prd(feature_request)`
|
|
92
|
-
- `break_into_tasks(prd_slug)`
|
|
93
|
-
- `complete_next_task(prd_slug)`
|
|
135
|
+
- `break_into_tasks(prd_slug?)`
|
|
136
|
+
- `complete_next_task(prd_slug?)`
|
|
137
|
+
|
|
138
|
+
In OpenCode these are shown as MCP slash entries like `/steward:create_prd:mcp` and inserted as `/steward:create_prd`.
|
|
94
139
|
|
|
95
|
-
|
|
140
|
+
- `prd_slug` is optional for break/complete prompts.
|
|
141
|
+
- When omitted, Steward workflows auto-resolve the slug from repo state.
|
|
142
|
+
- `complete_next_task` includes required commit hygiene (one-line commit message, no `Co-authored-by`, no task-related dirty changes left behind).
|
|
96
143
|
|
|
97
144
|
```js
|
|
98
145
|
const repo = await repos.current()
|
|
@@ -175,13 +222,12 @@ npm run build
|
|
|
175
222
|
| `PRD_STATE_HOME` | Base directory for DB (`state.db` inside) |
|
|
176
223
|
| `XDG_DATA_HOME` | Fallback base path for default DB location |
|
|
177
224
|
|
|
178
|
-
## OpenCode
|
|
225
|
+
## OpenCode Integration
|
|
179
226
|
|
|
180
|
-
|
|
227
|
+
Steward now uses MCP-registered prompts as the single workflow surface.
|
|
181
228
|
|
|
182
|
-
-
|
|
183
|
-
-
|
|
184
|
-
- Script: `prd-db.mjs`
|
|
229
|
+
- Use MCP prompts directly (for example `/steward:create_prd`, `/steward:break_into_tasks`, `/steward:complete_next_task`).
|
|
230
|
+
- This repository no longer ships separate OpenCode command/skill bundles for PRD workflows.
|
|
185
231
|
|
|
186
232
|
## License
|
|
187
233
|
|
package/bin/prd
CHANGED
|
@@ -12,7 +12,7 @@ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
|
12
12
|
const entryPath = resolve(packageRoot, 'dist', 'host', 'src', 'index.js')
|
|
13
13
|
|
|
14
14
|
function requiresSqliteRuntime(command) {
|
|
15
|
-
return command === 'mcp' || command === 'ui'
|
|
15
|
+
return command === 'mcp' || command === 'ui' || command === 'sync'
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function isSqliteRuntimeError(error) {
|
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/host/src/index.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { runMcpServer } from './mcp.js';
|
|
2
|
+
import { runSync } from './sync.js';
|
|
2
3
|
import { runUi } from './ui.js';
|
|
3
4
|
function printUsage() {
|
|
4
5
|
console.log(`prd - Steward CLI
|
|
5
6
|
|
|
6
7
|
Usage:
|
|
7
8
|
prd ui [--preview] [--port <port>] [--host <host>]
|
|
9
|
+
prd sync <subcommand> [options]
|
|
8
10
|
prd mcp
|
|
9
11
|
|
|
10
12
|
Commands:
|
|
11
13
|
ui Launch the prebuilt PRD web UI server
|
|
14
|
+
sync Export/inspect/merge sync bundles
|
|
12
15
|
mcp Start MCP server over stdio (codemode)
|
|
13
16
|
|
|
14
17
|
Options:
|
|
@@ -84,6 +87,13 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
84
87
|
}
|
|
85
88
|
return;
|
|
86
89
|
}
|
|
90
|
+
if (command === 'sync') {
|
|
91
|
+
const exitCode = await runSync(rest);
|
|
92
|
+
if (exitCode !== 0) {
|
|
93
|
+
process.exitCode = exitCode;
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
87
97
|
throw new Error(`Unknown command: ${command}`);
|
|
88
98
|
}
|
|
89
99
|
if (import.meta.main) {
|
package/dist/host/src/prompts.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
44
|
-
prdSlug,
|
|
44
|
+
`PRD slug input: ${resolvedInput || '<auto-resolve>'}`,
|
|
45
45
|
'',
|
|
46
46
|
'Workflow:',
|
|
47
|
-
'1.
|
|
48
|
-
'
|
|
49
|
-
'
|
|
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
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
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
|
-
'
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
'
|
|
61
|
-
|
|
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
|
-
|
|
69
|
-
prdSlug,
|
|
77
|
+
`PRD slug input: ${resolvedInput || '<auto-resolve>'}`,
|
|
70
78
|
'',
|
|
71
79
|
'Workflow:',
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
'
|
|
87
|
-
'
|
|
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
|
-
'
|
|
93
|
-
'
|
|
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 (
|
|
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
|
|
135
|
+
prd_slug: z.string().optional().describe('Optional PRD slug (filename in docs/prd without .md). Auto-resolved when omitted.')
|
|
112
136
|
}
|
|
113
|
-
}, async (
|
|
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 (
|
|
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
|
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { buildSyncBundle, serializeSyncBundle } from '../../server/utils/sync-export.js';
|
|
4
|
+
import { executeSyncMergeJson } from '../../server/utils/sync-apply.js';
|
|
5
|
+
import { inspectSyncBundleJson } from '../../server/utils/sync-inspect.js';
|
|
6
|
+
function printSyncUsage() {
|
|
7
|
+
console.log(`Sync Usage:
|
|
8
|
+
prd sync export <bundle-path> [--path-hints basename|none|absolute]
|
|
9
|
+
prd sync inspect <bundle-path>
|
|
10
|
+
prd sync merge <bundle-path> [--map <incomingRepoSyncKey>=<localPathOrRepoRef>] [--dry-run] [--apply]
|
|
11
|
+
|
|
12
|
+
Notes:
|
|
13
|
+
- merge defaults to --dry-run when --apply is omitted
|
|
14
|
+
- --map can be provided multiple times
|
|
15
|
+
`);
|
|
16
|
+
}
|
|
17
|
+
function resolveBundlePath(pathInput) {
|
|
18
|
+
return resolve(process.cwd(), pathInput);
|
|
19
|
+
}
|
|
20
|
+
function parsePathHintsMode(rawValue) {
|
|
21
|
+
if (rawValue === 'basename' || rawValue === 'none' || rawValue === 'absolute') {
|
|
22
|
+
return rawValue;
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`Invalid --path-hints value: ${rawValue}`);
|
|
25
|
+
}
|
|
26
|
+
function parseMapAssignment(rawValue) {
|
|
27
|
+
const separatorIndex = rawValue.indexOf('=');
|
|
28
|
+
if (separatorIndex <= 0 || separatorIndex >= rawValue.length - 1) {
|
|
29
|
+
throw new Error(`Invalid --map value: ${rawValue}. Expected <incomingRepoSyncKey>=<localPathOrRepoRef>`);
|
|
30
|
+
}
|
|
31
|
+
const source = rawValue.slice(0, separatorIndex).trim();
|
|
32
|
+
const target = rawValue.slice(separatorIndex + 1).trim();
|
|
33
|
+
if (!source || !target) {
|
|
34
|
+
throw new Error(`Invalid --map value: ${rawValue}. Expected <incomingRepoSyncKey>=<localPathOrRepoRef>`);
|
|
35
|
+
}
|
|
36
|
+
return { source, target };
|
|
37
|
+
}
|
|
38
|
+
export function parseSyncExportArgs(args) {
|
|
39
|
+
let bundlePath = null;
|
|
40
|
+
let pathHints = 'basename';
|
|
41
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
42
|
+
const arg = args[index];
|
|
43
|
+
if (!arg) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (arg === '--path-hints') {
|
|
47
|
+
const next = args[index + 1];
|
|
48
|
+
if (!next) {
|
|
49
|
+
throw new Error('--path-hints requires a value');
|
|
50
|
+
}
|
|
51
|
+
pathHints = parsePathHintsMode(next);
|
|
52
|
+
index += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (arg.startsWith('--')) {
|
|
56
|
+
throw new Error(`Unknown option for sync export: ${arg}`);
|
|
57
|
+
}
|
|
58
|
+
if (bundlePath !== null) {
|
|
59
|
+
throw new Error(`Unexpected argument for sync export: ${arg}`);
|
|
60
|
+
}
|
|
61
|
+
bundlePath = arg;
|
|
62
|
+
}
|
|
63
|
+
if (!bundlePath) {
|
|
64
|
+
throw new Error('sync export requires <bundle-path>');
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
bundlePath: resolveBundlePath(bundlePath),
|
|
68
|
+
pathHints
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function parseSyncInspectArgs(args) {
|
|
72
|
+
const [bundlePath] = args;
|
|
73
|
+
if (args.length !== 1 || !bundlePath || bundlePath.startsWith('--')) {
|
|
74
|
+
throw new Error('sync inspect requires exactly one <bundle-path>');
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
bundlePath: resolveBundlePath(bundlePath)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function parseSyncMergeArgs(args) {
|
|
81
|
+
let bundlePath = null;
|
|
82
|
+
let apply = false;
|
|
83
|
+
let dryRun = false;
|
|
84
|
+
const repoMap = {};
|
|
85
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
86
|
+
const arg = args[index];
|
|
87
|
+
if (!arg) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (arg === '--apply') {
|
|
91
|
+
apply = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (arg === '--dry-run') {
|
|
95
|
+
dryRun = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (arg === '--map') {
|
|
99
|
+
const next = args[index + 1];
|
|
100
|
+
if (!next) {
|
|
101
|
+
throw new Error('--map requires a value');
|
|
102
|
+
}
|
|
103
|
+
const { source, target } = parseMapAssignment(next);
|
|
104
|
+
repoMap[source] = target;
|
|
105
|
+
index += 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (arg.startsWith('--')) {
|
|
109
|
+
throw new Error(`Unknown option for sync merge: ${arg}`);
|
|
110
|
+
}
|
|
111
|
+
if (bundlePath !== null) {
|
|
112
|
+
throw new Error(`Unexpected argument for sync merge: ${arg}`);
|
|
113
|
+
}
|
|
114
|
+
bundlePath = arg;
|
|
115
|
+
}
|
|
116
|
+
if (!bundlePath) {
|
|
117
|
+
throw new Error('sync merge requires <bundle-path>');
|
|
118
|
+
}
|
|
119
|
+
if (apply && dryRun) {
|
|
120
|
+
throw new Error('Cannot use --apply and --dry-run together');
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
bundlePath: resolveBundlePath(bundlePath),
|
|
124
|
+
apply,
|
|
125
|
+
repoMap
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function runSyncExport(args) {
|
|
129
|
+
const parsed = parseSyncExportArgs(args);
|
|
130
|
+
const bundle = await buildSyncBundle({
|
|
131
|
+
pathHints: parsed.pathHints
|
|
132
|
+
});
|
|
133
|
+
await fs.mkdir(dirname(parsed.bundlePath), { recursive: true });
|
|
134
|
+
await fs.writeFile(parsed.bundlePath, `${serializeSyncBundle(bundle)}\n`, 'utf-8');
|
|
135
|
+
console.log(`[steward] Exported bundle: ${parsed.bundlePath}`);
|
|
136
|
+
console.log(`[steward] Bundle ID: ${bundle.bundleId}`);
|
|
137
|
+
console.log(`[steward] Rows: repos=${bundle.repos.length} states=${bundle.states.length} archives=${bundle.archives.length}`);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
async function runSyncInspect(args) {
|
|
141
|
+
const parsed = parseSyncInspectArgs(args);
|
|
142
|
+
const jsonPayload = await fs.readFile(parsed.bundlePath, 'utf-8');
|
|
143
|
+
const inspection = inspectSyncBundleJson(jsonPayload);
|
|
144
|
+
console.log(`[steward] Bundle: ${parsed.bundlePath}`);
|
|
145
|
+
console.log(`[steward] ID=${inspection.bundleId} sourceDevice=${inspection.sourceDeviceId} format=v${inspection.formatVersion}`);
|
|
146
|
+
console.log(`[steward] Totals: repos=${inspection.totals.repos} states=${inspection.totals.states} archives=${inspection.totals.archives}`);
|
|
147
|
+
console.log(`[steward] Unknown references: states=${inspection.totals.unknownRepoStates} archives=${inspection.totals.unknownRepoArchives}`);
|
|
148
|
+
for (const repo of inspection.repos) {
|
|
149
|
+
const hint = repo.pathHint ? ` pathHint=${repo.pathHint}` : '';
|
|
150
|
+
console.log(`[steward] Repo ${repo.repoSyncKey} (${repo.name})${hint} states=${repo.stateCount} archives=${repo.archiveCount}`);
|
|
151
|
+
}
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
async function runSyncMerge(args) {
|
|
155
|
+
const parsed = parseSyncMergeArgs(args);
|
|
156
|
+
const jsonPayload = await fs.readFile(parsed.bundlePath, 'utf-8');
|
|
157
|
+
const result = await executeSyncMergeJson(jsonPayload, {
|
|
158
|
+
apply: parsed.apply,
|
|
159
|
+
repoMap: parsed.repoMap
|
|
160
|
+
});
|
|
161
|
+
const mode = result.mode === 'dry_run' ? 'dry-run' : 'apply';
|
|
162
|
+
console.log(`[steward] Merge mode: ${mode}`);
|
|
163
|
+
console.log(`[steward] Bundle ID: ${result.bundleId}`);
|
|
164
|
+
console.log(`[steward] Repo mapping: mapped=${result.plan.summary.repos.mapped} unresolved=${result.plan.summary.repos.unresolved}`);
|
|
165
|
+
console.log(`[steward] State actions: insert=${result.plan.summary.states.insert} update=${result.plan.summary.states.update} skip=${result.plan.summary.states.skip} unresolved=${result.plan.summary.states.unresolved} conflicts=${result.plan.summary.states.conflicts}`);
|
|
166
|
+
console.log(`[steward] Archive actions: insert=${result.plan.summary.archives.insert} update=${result.plan.summary.archives.update} skip=${result.plan.summary.archives.skip} unresolved=${result.plan.summary.archives.unresolved}`);
|
|
167
|
+
if (result.mode === 'apply') {
|
|
168
|
+
if (result.alreadyApplied) {
|
|
169
|
+
console.log('[steward] Bundle already applied; no changes were written.');
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
console.log(`[steward] Applied: ${result.applied ? 'yes' : 'no'}`);
|
|
173
|
+
if (result.backupPath) {
|
|
174
|
+
console.log(`[steward] Backup: ${result.backupPath}`);
|
|
175
|
+
}
|
|
176
|
+
console.log(`[steward] Retention: backupsDeleted=${result.retention.backupsDeleted} logsDeleted=${result.retention.logsDeleted}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
console.log('[steward] Dry-run only; re-run with --apply to write changes.');
|
|
181
|
+
}
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
export async function runSync(args) {
|
|
185
|
+
const [subcommandRaw, ...rest] = args;
|
|
186
|
+
const subcommand = subcommandRaw || '';
|
|
187
|
+
if (subcommand === '' || subcommand === '-h' || subcommand === '--help') {
|
|
188
|
+
printSyncUsage();
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
if (subcommand === 'export') {
|
|
192
|
+
return await runSyncExport(rest);
|
|
193
|
+
}
|
|
194
|
+
if (subcommand === 'inspect') {
|
|
195
|
+
return await runSyncInspect(rest);
|
|
196
|
+
}
|
|
197
|
+
if (subcommand === 'merge') {
|
|
198
|
+
return await runSyncMerge(rest);
|
|
199
|
+
}
|
|
200
|
+
throw new Error(`Unknown sync subcommand: ${subcommandRaw}`);
|
|
201
|
+
}
|