@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.
@@ -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,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/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]}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thxgg/steward-prod",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
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
 
@@ -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
- In OpenCode these are shown as MCP slash entries like `/prd:create_prd:mcp`.
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 Bundle
225
+ ## OpenCode Integration
179
226
 
180
- This repo includes curated OpenCode assets under `opencode/`:
227
+ Steward now uses MCP-registered prompts as the single workflow surface.
181
228
 
182
- - Commands: `prd`, `prd-task`, `complete-next-task`, `commit`
183
- - Skills: `prd`, `prd-task`, `complete-next-task`, `commit`
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
- 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
  }
@@ -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) {
@@ -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
  }
@@ -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
+ }