@thxgg/steward 0.1.12 → 0.1.14

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 (89) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/-k8zG74W.js +61 -0
  3. package/.output/public/_nuxt/B-5VWizU.js +1 -0
  4. package/.output/public/_nuxt/{Bq6edYSd.js → BDqHART1.js} +1 -1
  5. package/.output/public/_nuxt/BMAq0QVD.js +42 -0
  6. package/.output/public/_nuxt/BPeTf9dd.js +1 -0
  7. package/.output/public/_nuxt/{dOaEkD-3.js → BubpH_wW.js} +1 -1
  8. package/.output/public/_nuxt/C2HGkiSP.js +1 -0
  9. package/.output/public/_nuxt/CMu9GKTH.js +4 -0
  10. package/.output/public/_nuxt/{BZ1iIOYp.js → CVvrkZkq.js} +1 -1
  11. package/.output/public/_nuxt/C_NevjZD.js +3 -0
  12. package/.output/public/_nuxt/Detail.CzXXlavD.css +1 -0
  13. package/.output/public/_nuxt/_prd_.KTotLoF_.css +1 -0
  14. package/.output/public/_nuxt/builds/latest.json +1 -1
  15. package/.output/public/_nuxt/builds/meta/b57a8fc3-6a38-4f58-b2ae-54768412ea40.json +1 -0
  16. package/.output/public/_nuxt/entry.LcDOtJnR.css +1 -0
  17. package/.output/public/_nuxt/{BFv4l3hn.js → nYTZJhvT.js} +1 -1
  18. package/.output/public/_nuxt/{kTT8NKtq.js → qKRNa41x.js} +1 -1
  19. package/.output/public/_nuxt/qt5OEWHC.js +1 -0
  20. package/.output/public/_nuxt/{C897Egk9.js → uTyw4SRK.js} +1 -1
  21. package/.output/public/_nuxt/{DoNqd8jQ.js → wbj-mIhK.js} +1 -1
  22. package/.output/server/chunks/_/git.mjs.map +1 -1
  23. package/.output/server/chunks/_/task-graph.mjs +196 -0
  24. package/.output/server/chunks/_/task-graph.mjs.map +1 -0
  25. package/.output/server/chunks/build/{_prd_-CkKfJB6U.mjs → Detail-DC-KJQ1f.mjs} +911 -1688
  26. package/.output/server/chunks/build/Detail-DC-KJQ1f.mjs.map +1 -0
  27. package/.output/server/chunks/build/DiffViewer-styles-1.mjs-D0sb4vsK.mjs +4 -0
  28. package/.output/server/chunks/build/DiffViewer-styles-1.mjs-D0sb4vsK.mjs.map +1 -0
  29. package/.output/server/chunks/build/DiffViewer-styles.CkSjCQ0r.mjs +10 -0
  30. package/.output/server/chunks/build/DiffViewer-styles.CkSjCQ0r.mjs.map +1 -0
  31. package/.output/server/chunks/build/DiffViewer-styles.FJJuYjYB.mjs +8 -0
  32. package/.output/server/chunks/build/DiffViewer-styles.FJJuYjYB.mjs.map +1 -0
  33. package/.output/server/chunks/build/_prd_-C1C4GAhW.mjs +1596 -0
  34. package/.output/server/chunks/build/_prd_-C1C4GAhW.mjs.map +1 -0
  35. package/.output/server/chunks/build/client.precomputed.mjs +1 -1
  36. package/.output/server/chunks/build/{default-B5nw9_Xg.mjs → default-DWCOHHTE.mjs} +32 -20
  37. package/.output/server/chunks/build/default-DWCOHHTE.mjs.map +1 -0
  38. package/.output/server/chunks/build/{index-CTpuP9Mj.mjs → index-CckL_NBD.mjs} +2 -2
  39. package/.output/server/chunks/build/index-CckL_NBD.mjs.map +1 -0
  40. package/.output/server/chunks/build/{index-D21S97KB.mjs → index-QVeSHT3L.mjs} +3 -3
  41. package/.output/server/chunks/build/index-QVeSHT3L.mjs.map +1 -0
  42. package/.output/server/chunks/build/repo-graph-CTEkxiYd.mjs +205 -0
  43. package/.output/server/chunks/build/repo-graph-CTEkxiYd.mjs.map +1 -0
  44. package/.output/server/chunks/build/server.mjs +12 -3
  45. package/.output/server/chunks/build/styles.mjs +4 -4
  46. package/.output/server/chunks/build/{usePrd-YhvN6Ary.mjs → usePrd-SqcxGyFU.mjs} +20 -2
  47. package/.output/server/chunks/build/usePrd-SqcxGyFU.mjs.map +1 -0
  48. package/.output/server/chunks/nitro/nitro.mjs +669 -637
  49. package/.output/server/chunks/routes/api/repos/_repoId/graph.get.mjs +41 -0
  50. package/.output/server/chunks/routes/api/repos/_repoId/graph.get.mjs.map +1 -0
  51. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/graph.get.mjs +42 -0
  52. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/graph.get.mjs.map +1 -0
  53. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/progress.get.mjs +1 -1
  54. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks/_taskId/commits.get.mjs +1 -1
  55. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks.get.mjs +1 -1
  56. package/.output/server/chunks/routes/api/repos/_repoId/prds.get.mjs +1 -1
  57. package/.output/server/node_modules/@vue-flow/background/dist/vue-flow-background.mjs +126 -0
  58. package/.output/server/node_modules/@vue-flow/background/package.json +73 -0
  59. package/.output/server/node_modules/@vue-flow/controls/dist/vue-flow-controls.mjs +207 -0
  60. package/.output/server/node_modules/@vue-flow/controls/package.json +77 -0
  61. package/.output/server/node_modules/@vue-flow/core/dist/vue-flow-core.mjs +10186 -0
  62. package/.output/server/node_modules/@vue-flow/core/package.json +95 -0
  63. package/.output/server/package.json +4 -1
  64. package/dist/app/types/graph.js +1 -0
  65. package/dist/host/src/api/git.js +71 -1
  66. package/dist/host/src/help.js +12 -0
  67. package/dist/server/utils/git.js +104 -1
  68. package/dist/server/utils/task-graph.js +190 -0
  69. package/docs/MCP.md +21 -0
  70. package/package.json +5 -1
  71. package/.output/public/_nuxt/BuQdImno.js +0 -1
  72. package/.output/public/_nuxt/CMUOpExW.js +0 -3
  73. package/.output/public/_nuxt/DE885CbX.js +0 -1
  74. package/.output/public/_nuxt/DomrzX-T.js +0 -76
  75. package/.output/public/_nuxt/R2cvz8mH.js +0 -4
  76. package/.output/public/_nuxt/_prd_.DYvuV73Q.css +0 -1
  77. package/.output/public/_nuxt/builds/meta/6f66fabf-cc26-482b-8adf-f8731dd68f83.json +0 -1
  78. package/.output/public/_nuxt/entry.Dk19PK4d.css +0 -1
  79. package/.output/server/chunks/build/DiffViewer-styles-1.mjs-ZdBUa15f.mjs +0 -4
  80. package/.output/server/chunks/build/DiffViewer-styles-1.mjs-ZdBUa15f.mjs.map +0 -1
  81. package/.output/server/chunks/build/DiffViewer-styles.CoMVrk_N.mjs +0 -8
  82. package/.output/server/chunks/build/DiffViewer-styles.CoMVrk_N.mjs.map +0 -1
  83. package/.output/server/chunks/build/DiffViewer-styles.cLfMOdMh.mjs +0 -10
  84. package/.output/server/chunks/build/DiffViewer-styles.cLfMOdMh.mjs.map +0 -1
  85. package/.output/server/chunks/build/_prd_-CkKfJB6U.mjs.map +0 -1
  86. package/.output/server/chunks/build/default-B5nw9_Xg.mjs.map +0 -1
  87. package/.output/server/chunks/build/index-CTpuP9Mj.mjs.map +0 -1
  88. package/.output/server/chunks/build/index-D21S97KB.mjs.map +0 -1
  89. package/.output/server/chunks/build/usePrd-YhvN6Ary.mjs.map +0 -1
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "@vue-flow/core",
3
+ "version": "1.48.2",
4
+ "private": false,
5
+ "license": "MIT",
6
+ "author": "Burak Cakmakoglu<78412429+bcakmakoglu@users.noreply.github.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/bcakmakoglu/vue-flow",
10
+ "directory": "packages/core"
11
+ },
12
+ "homepage": "https://vueflow.dev",
13
+ "bugs": {
14
+ "url": "https://github.com/bcakmakoglu/vue-flow/issues"
15
+ },
16
+ "keywords": [
17
+ "vue",
18
+ "flow",
19
+ "diagram",
20
+ "editor",
21
+ "graph",
22
+ "node",
23
+ "link",
24
+ "port",
25
+ "slot",
26
+ "vue3",
27
+ "composition-api",
28
+ "vue-flow",
29
+ "vueflow",
30
+ "typescript"
31
+ ],
32
+ "main": "./dist/vue-flow-core.js",
33
+ "module": "./dist/vue-flow-core.mjs",
34
+ "types": "./dist/index.d.ts",
35
+ "unpkg": "./dist/vue-flow-core.iife.js",
36
+ "jsdelivr": "./dist/vue-flow-core.iife.js",
37
+ "exports": {
38
+ ".": {
39
+ "types": "./dist/index.d.ts",
40
+ "import": "./dist/vue-flow-core.mjs",
41
+ "require": "./dist/vue-flow-core.js"
42
+ },
43
+ "./dist/style.css": "./dist/style.css",
44
+ "./dist/theme-default.css": "./dist/theme-default.css"
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "*.d.ts"
49
+ ],
50
+ "sideEffects": [
51
+ "*.css"
52
+ ],
53
+ "publishConfig": {
54
+ "access": "public",
55
+ "registry": "https://registry.npmjs.org/"
56
+ },
57
+ "peerDependencies": {
58
+ "vue": "^3.3.0"
59
+ },
60
+ "dependencies": {
61
+ "@vueuse/core": "^10.5.0",
62
+ "d3-drag": "^3.0.0",
63
+ "d3-interpolate": "^3.0.1",
64
+ "d3-selection": "^3.0.0",
65
+ "d3-zoom": "^3.0.0"
66
+ },
67
+ "devDependencies": {
68
+ "@rollup/plugin-replace": "^5.0.3",
69
+ "@types/d3-drag": "^3.0.7",
70
+ "@types/d3-interpolate": "^3.0.4",
71
+ "@types/d3-selection": "^3.0.11",
72
+ "@types/d3-transition": "^3.0.9",
73
+ "@types/d3-zoom": "^3.0.8",
74
+ "@vitejs/plugin-vue": "^4.4.0",
75
+ "autoprefixer": "^10.4.16",
76
+ "postcss": "^8.4.31",
77
+ "postcss-cli": "^10.1.0",
78
+ "postcss-nested": "^6.0.1",
79
+ "vite": "^4.4.11",
80
+ "vue-tsc": "^1.8.16",
81
+ "@tooling/eslint-config": "0.0.0",
82
+ "@tooling/tsconfig": "0.0.0"
83
+ },
84
+ "scripts": {
85
+ "dev": "pnpm types:watch & pnpm build:watch",
86
+ "build": "vite build && vite build -c vite.config.iife.ts",
87
+ "build:watch": "vite build --watch & vite build -c vite.config.iife.ts --watch",
88
+ "types": "vue-tsc --declaration --emitDeclarationOnly && tsc -p ./tsconfig.build.json && shx rm -rf tmp && pnpm lint:dist",
89
+ "types:watch": "vue-tsc --declaration --emitDeclarationOnly --watch & tsc -p ./tsconfig.build.json --watch",
90
+ "theme": "postcss src/style.css -o dist/style.css && postcss src/theme-default.css -o dist/theme-default.css",
91
+ "lint": "eslint --ext .js,.ts,.vue ./",
92
+ "lint:dist": "eslint --ext \".ts,.tsx\" -c .eslintrc.js --fix --ignore-pattern !**/* ./dist",
93
+ "test": "exit 0"
94
+ }
95
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thxgg/steward-prod",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "dependencies": {
@@ -21,6 +21,9 @@
21
21
  "@swc/helpers": "0.5.18",
22
22
  "@tanstack/virtual-core": "3.13.18",
23
23
  "@tanstack/vue-virtual": "3.13.18",
24
+ "@vue-flow/background": "1.3.2",
25
+ "@vue-flow/controls": "1.1.3",
26
+ "@vue-flow/core": "1.48.2",
24
27
  "@vue/compiler-core": "3.5.28",
25
28
  "@vue/compiler-dom": "3.5.28",
26
29
  "@vue/compiler-ssr": "3.5.28",
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,14 @@
1
- import { getCommitDiff, getCommitInfo, getFileContent, getFileDiff, isGitRepo, validatePathInRepo } from '../../../server/utils/git.js';
1
+ import { commitStagedChanges, getCommitDiff, getCommitInfo, getFileContent, getFileDiff, getWorkingTreeStatus, isGitRepo, stagePaths, validatePathInRepo } from '../../../server/utils/git.js';
2
2
  import { requireRepo } from './repo-context.js';
3
+ function toGitStatus(status) {
4
+ const hasStagedChanges = status.staged.length > 0;
5
+ const hasChanges = hasStagedChanges || status.unstaged.length > 0 || status.untracked.length > 0;
6
+ return {
7
+ ...status,
8
+ hasChanges,
9
+ hasStagedChanges
10
+ };
11
+ }
3
12
  function resolveGitRepoPath(repo, repoPath) {
4
13
  if (!repoPath) {
5
14
  return repo.path;
@@ -15,6 +24,15 @@ function resolveGitRepoPath(repo, repoPath) {
15
24
  return matchedRepo.absolutePath;
16
25
  }
17
26
  export const git = {
27
+ async getStatus(repoId, repoPath) {
28
+ const repo = await requireRepo(repoId);
29
+ const gitRepoPath = resolveGitRepoPath(repo, repoPath);
30
+ if (!await isGitRepo(gitRepoPath)) {
31
+ throw new Error('Resolved path is not a git repository');
32
+ }
33
+ const status = await getWorkingTreeStatus(gitRepoPath);
34
+ return toGitStatus(status);
35
+ },
18
36
  async getCommits(repoId, shas, repoPath) {
19
37
  if (!Array.isArray(shas) || shas.length === 0) {
20
38
  throw new Error('At least one SHA is required');
@@ -85,5 +103,57 @@ export const git = {
85
103
  throw new Error('Resolved path is not a git repository');
86
104
  }
87
105
  return await getFileContent(gitRepoPath, commit, file);
106
+ },
107
+ async commitIfChanged(repoId, message, options) {
108
+ if (!message || !message.trim()) {
109
+ throw new Error('message is required');
110
+ }
111
+ const repo = await requireRepo(repoId);
112
+ const relativeRepoPath = options?.repoPath || '';
113
+ const gitRepoPath = resolveGitRepoPath(repo, relativeRepoPath);
114
+ if (!await isGitRepo(gitRepoPath)) {
115
+ throw new Error('Resolved path is not a git repository');
116
+ }
117
+ const paths = Array.isArray(options?.paths)
118
+ ? options.paths.filter((path) => typeof path === 'string' && path.trim().length > 0)
119
+ : [];
120
+ if (paths.length > 0) {
121
+ await stagePaths(gitRepoPath, paths);
122
+ }
123
+ const statusBefore = await getWorkingTreeStatus(gitRepoPath);
124
+ if (statusBefore.staged.length === 0) {
125
+ const noChanges = statusBefore.unstaged.length === 0 && statusBefore.untracked.length === 0;
126
+ return {
127
+ committed: false,
128
+ repoPath: relativeRepoPath,
129
+ staged: statusBefore.staged,
130
+ unstaged: statusBefore.unstaged,
131
+ untracked: statusBefore.untracked,
132
+ reason: noChanges ? 'no_changes' : 'no_staged_changes'
133
+ };
134
+ }
135
+ const commit = await commitStagedChanges(gitRepoPath, message);
136
+ if (!commit) {
137
+ return {
138
+ committed: false,
139
+ repoPath: relativeRepoPath,
140
+ staged: statusBefore.staged,
141
+ unstaged: statusBefore.unstaged,
142
+ untracked: statusBefore.untracked,
143
+ reason: 'no_staged_changes'
144
+ };
145
+ }
146
+ const statusAfter = await getWorkingTreeStatus(gitRepoPath);
147
+ return {
148
+ committed: true,
149
+ repoPath: relativeRepoPath,
150
+ staged: statusAfter.staged,
151
+ unstaged: statusAfter.unstaged,
152
+ untracked: statusAfter.untracked,
153
+ sha: commit.sha,
154
+ shortSha: commit.shortSha,
155
+ message: commit.message,
156
+ committedFiles: commit.files
157
+ };
88
158
  }
89
159
  };
@@ -39,6 +39,10 @@ const HELP = {
39
39
  }
40
40
  ],
41
41
  git: [
42
+ {
43
+ signature: 'git.getStatus(repoId, repoPath?)',
44
+ description: 'Load working tree status (staged/unstaged/untracked)'
45
+ },
42
46
  { signature: 'git.getCommits(repoId, shas, repoPath?)', description: 'Load commit metadata' },
43
47
  { signature: 'git.getDiff(repoId, commit, repoPath?)', description: 'Load full commit diff' },
44
48
  {
@@ -48,6 +52,10 @@ const HELP = {
48
52
  {
49
53
  signature: 'git.getFileContent(repoId, commit, file, repoPath?)',
50
54
  description: 'Load file content at commit'
55
+ },
56
+ {
57
+ signature: 'git.commitIfChanged(repoId, message, options?)',
58
+ description: 'Stage optional paths and commit when staged changes exist'
51
59
  }
52
60
  ],
53
61
  state: [
@@ -86,6 +94,10 @@ const HELP = {
86
94
  {
87
95
  title: 'Upsert without repoId',
88
96
  code: `await state.upsertCurrent('prd-viewer', {\n notes: '# Updated from MCP'\n})\n\nreturn { saved: true }`
97
+ },
98
+ {
99
+ title: 'Commit task-related changes when present',
100
+ code: `const repo = await repos.current()\n\nconst result = await git.commitIfChanged(repo.id, 'docs: update task notes', {\n paths: ['docs/prd/prd-viewer.md']\n})\n\nreturn result`
89
101
  }
90
102
  ]
91
103
  };
@@ -55,6 +55,109 @@ export function validatePathInRepo(repoPath, filePath) {
55
55
  const relativePath = relative(resolvedRepo, resolvedFile);
56
56
  return !relativePath.startsWith('..') && !isAbsolute(relativePath);
57
57
  }
58
+ function dedupeAndSort(values) {
59
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
60
+ }
61
+ function normalizeStatusPath(rawPath) {
62
+ const trimmed = rawPath.trim();
63
+ if (!trimmed.includes(' -> ')) {
64
+ return trimmed;
65
+ }
66
+ const segments = trimmed.split(' -> ');
67
+ return segments[segments.length - 1]?.trim() || trimmed;
68
+ }
69
+ function normalizePathForGit(repoPath, path) {
70
+ if (!validatePathInRepo(repoPath, path)) {
71
+ throw new Error(`Invalid file path: ${path}`);
72
+ }
73
+ const absolutePath = isAbsolute(path)
74
+ ? resolve(path)
75
+ : resolve(repoPath, path);
76
+ const relativePath = relative(resolve(repoPath), absolutePath);
77
+ if (!relativePath || relativePath === '.') {
78
+ throw new Error('Path must point to a file or subdirectory inside the repository');
79
+ }
80
+ return relativePath;
81
+ }
82
+ /**
83
+ * Get working tree changes split by staged/unstaged/untracked buckets.
84
+ */
85
+ export async function getWorkingTreeStatus(repoPath) {
86
+ const output = await execGit(repoPath, ['status', '--porcelain']);
87
+ const staged = new Set();
88
+ const unstaged = new Set();
89
+ const untracked = new Set();
90
+ const lines = output
91
+ .split('\n')
92
+ .map(line => line.trimEnd())
93
+ .filter(line => line.length >= 3);
94
+ for (const line of lines) {
95
+ const indexStatus = line.charAt(0);
96
+ const worktreeStatus = line.charAt(1);
97
+ const path = normalizeStatusPath(line.slice(3));
98
+ if (!path) {
99
+ continue;
100
+ }
101
+ if (indexStatus === '?' && worktreeStatus === '?') {
102
+ untracked.add(path);
103
+ continue;
104
+ }
105
+ if (indexStatus !== ' ' && indexStatus !== '?') {
106
+ staged.add(path);
107
+ }
108
+ if (worktreeStatus !== ' ') {
109
+ unstaged.add(path);
110
+ }
111
+ }
112
+ return {
113
+ staged: dedupeAndSort(staged),
114
+ unstaged: dedupeAndSort(unstaged),
115
+ untracked: dedupeAndSort(untracked)
116
+ };
117
+ }
118
+ /**
119
+ * Stage explicit paths in a repository.
120
+ */
121
+ export async function stagePaths(repoPath, paths) {
122
+ if (!Array.isArray(paths) || paths.length === 0) {
123
+ return [];
124
+ }
125
+ const normalizedPaths = dedupeAndSort(paths
126
+ .map(path => path.trim())
127
+ .filter(path => path.length > 0)
128
+ .map(path => normalizePathForGit(repoPath, path)));
129
+ if (normalizedPaths.length === 0) {
130
+ return [];
131
+ }
132
+ await execGit(repoPath, ['add', '--', ...normalizedPaths]);
133
+ return normalizedPaths;
134
+ }
135
+ /**
136
+ * Commit currently staged changes. Returns null when nothing is staged.
137
+ */
138
+ export async function commitStagedChanges(repoPath, message) {
139
+ const trimmedMessage = message.trim();
140
+ if (!trimmedMessage) {
141
+ throw new Error('Commit message is required');
142
+ }
143
+ const stagedOutput = await execGit(repoPath, ['diff', '--cached', '--name-only']);
144
+ const stagedFiles = stagedOutput
145
+ .split('\n')
146
+ .map(line => line.trim())
147
+ .filter(line => line.length > 0);
148
+ if (stagedFiles.length === 0) {
149
+ return null;
150
+ }
151
+ await execGit(repoPath, ['commit', '-m', trimmedMessage]);
152
+ const sha = (await execGit(repoPath, ['rev-parse', 'HEAD'])).trim();
153
+ const shortSha = (await execGit(repoPath, ['rev-parse', '--short', 'HEAD'])).trim();
154
+ return {
155
+ sha,
156
+ shortSha,
157
+ message: trimmedMessage,
158
+ files: stagedFiles
159
+ };
160
+ }
58
161
  /**
59
162
  * Get commit information by SHA
60
163
  */
@@ -333,7 +436,7 @@ async function commitExistsInRepo(repoPath, sha) {
333
436
  *
334
437
  * @param repoConfig - The repository configuration with optional gitRepos
335
438
  * @param sha - The commit SHA to find
336
- * @returns The GitRepoInfo where the commit was found, or throws if not found
439
+ * @returns Resolved commit data for the matching repo, or throws if not found
337
440
  */
338
441
  export async function findRepoForCommit(repoConfig, sha) {
339
442
  // Validate SHA format
@@ -0,0 +1,190 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { getPrdState, getPrdStateSummaries, migrateLegacyStateForRepo } from './prd-state.js';
4
+ const NODE_SEPARATOR = '::';
5
+ const MISSING_PREFIX = 'missing';
6
+ export function createTaskNodeId(prdSlug, taskId) {
7
+ return `${prdSlug}${NODE_SEPARATOR}${taskId}`;
8
+ }
9
+ function createMissingNodeId(prdSlug, taskId) {
10
+ return `${MISSING_PREFIX}${NODE_SEPARATOR}${prdSlug}${NODE_SEPARATOR}${taskId}`;
11
+ }
12
+ function createEdgeId(source, target) {
13
+ return `${source}->${target}`;
14
+ }
15
+ function parseDependency(rawDependency, currentPrdSlug) {
16
+ const trimmed = rawDependency.trim();
17
+ if (!trimmed) {
18
+ return {
19
+ prdSlug: currentPrdSlug,
20
+ taskId: rawDependency,
21
+ reference: rawDependency
22
+ };
23
+ }
24
+ const parts = trimmed.split(NODE_SEPARATOR);
25
+ if (parts.length === 2) {
26
+ const [prdSlug, taskId] = parts;
27
+ if (prdSlug && taskId) {
28
+ return {
29
+ prdSlug,
30
+ taskId,
31
+ reference: `${prdSlug}${NODE_SEPARATOR}${taskId}`
32
+ };
33
+ }
34
+ }
35
+ return {
36
+ prdSlug: currentPrdSlug,
37
+ taskId: trimmed,
38
+ reference: trimmed
39
+ };
40
+ }
41
+ async function resolvePrdName(repo, prdSlug) {
42
+ const prdPath = join(repo.path, 'docs', 'prd', `${prdSlug}.md`);
43
+ try {
44
+ const content = await fs.readFile(prdPath, 'utf-8');
45
+ const h1Match = content.match(/^#\s+(.+)$/m);
46
+ return h1Match?.[1]?.trim() || prdSlug;
47
+ }
48
+ catch {
49
+ return prdSlug;
50
+ }
51
+ }
52
+ function buildStats(taskNodes, unresolvedCount) {
53
+ const pending = taskNodes.filter((node) => node.status === 'pending').length;
54
+ const inProgress = taskNodes.filter((node) => node.status === 'in_progress').length;
55
+ const completed = taskNodes.filter((node) => node.status === 'completed').length;
56
+ return {
57
+ total: taskNodes.length,
58
+ pending,
59
+ inProgress,
60
+ completed,
61
+ unresolved: unresolvedCount
62
+ };
63
+ }
64
+ function sortNodes(nodes) {
65
+ return [...nodes].sort((a, b) => {
66
+ if (a.kind !== b.kind) {
67
+ return a.kind === 'task' ? -1 : 1;
68
+ }
69
+ return a.id.localeCompare(b.id);
70
+ });
71
+ }
72
+ function sortEdges(edges) {
73
+ return [...edges].sort((a, b) => a.id.localeCompare(b.id));
74
+ }
75
+ function buildGraphFromInputs(inputs) {
76
+ const taskNodes = new Map();
77
+ const missingNodes = new Map();
78
+ const edges = new Map();
79
+ for (const input of inputs) {
80
+ for (const task of input.tasks) {
81
+ const nodeId = createTaskNodeId(input.prdSlug, task.id);
82
+ taskNodes.set(nodeId, {
83
+ id: nodeId,
84
+ kind: 'task',
85
+ taskId: task.id,
86
+ prdSlug: input.prdSlug,
87
+ prdName: input.prdName,
88
+ title: task.title,
89
+ status: task.status,
90
+ category: task.category,
91
+ priority: task.priority
92
+ });
93
+ }
94
+ }
95
+ for (const input of inputs) {
96
+ for (const task of input.tasks) {
97
+ const targetId = createTaskNodeId(input.prdSlug, task.id);
98
+ for (const rawDependency of task.dependencies) {
99
+ const dependency = parseDependency(rawDependency, input.prdSlug);
100
+ const sourceTaskId = createTaskNodeId(dependency.prdSlug, dependency.taskId);
101
+ if (taskNodes.has(sourceTaskId)) {
102
+ const edgeId = createEdgeId(sourceTaskId, targetId);
103
+ edges.set(edgeId, {
104
+ id: edgeId,
105
+ source: sourceTaskId,
106
+ target: targetId,
107
+ type: 'dependency'
108
+ });
109
+ continue;
110
+ }
111
+ const missingNodeId = createMissingNodeId(dependency.prdSlug, dependency.taskId);
112
+ if (!missingNodes.has(missingNodeId)) {
113
+ missingNodes.set(missingNodeId, {
114
+ id: missingNodeId,
115
+ kind: 'external',
116
+ title: `Missing: ${dependency.reference}`,
117
+ unresolved: true,
118
+ dependencyRef: dependency.reference
119
+ });
120
+ }
121
+ const unresolvedEdgeId = createEdgeId(missingNodeId, targetId);
122
+ edges.set(unresolvedEdgeId, {
123
+ id: unresolvedEdgeId,
124
+ source: missingNodeId,
125
+ target: targetId,
126
+ type: 'dependency',
127
+ unresolved: true
128
+ });
129
+ }
130
+ }
131
+ }
132
+ const taskNodeList = [...taskNodes.values()];
133
+ const nodeList = sortNodes([...taskNodeList, ...missingNodes.values()]);
134
+ const edgeList = sortEdges([...edges.values()]);
135
+ return {
136
+ nodes: nodeList,
137
+ edges: edgeList,
138
+ stats: buildStats(taskNodeList, missingNodes.size)
139
+ };
140
+ }
141
+ export async function buildPrdGraph(repo, prdSlug) {
142
+ await migrateLegacyStateForRepo(repo);
143
+ const [state, prdName] = await Promise.all([
144
+ getPrdState(repo.id, prdSlug),
145
+ resolvePrdName(repo, prdSlug)
146
+ ]);
147
+ const tasks = Array.isArray(state?.tasks?.tasks) ? state.tasks.tasks : [];
148
+ const graph = buildGraphFromInputs([
149
+ {
150
+ prdSlug,
151
+ prdName,
152
+ tasks
153
+ }
154
+ ]);
155
+ return {
156
+ scope: 'prd',
157
+ repoId: repo.id,
158
+ prdSlug,
159
+ nodes: graph.nodes,
160
+ edges: graph.edges,
161
+ stats: graph.stats
162
+ };
163
+ }
164
+ export async function buildRepoGraph(repo) {
165
+ await migrateLegacyStateForRepo(repo);
166
+ const summaries = await getPrdStateSummaries(repo.id);
167
+ const slugs = [...summaries.keys()].sort((a, b) => a.localeCompare(b));
168
+ const inputs = [];
169
+ for (const slug of slugs) {
170
+ const state = await getPrdState(repo.id, slug);
171
+ const tasks = state?.tasks?.tasks;
172
+ if (!Array.isArray(tasks)) {
173
+ continue;
174
+ }
175
+ inputs.push({
176
+ prdSlug: slug,
177
+ prdName: await resolvePrdName(repo, slug),
178
+ tasks
179
+ });
180
+ }
181
+ const graph = buildGraphFromInputs(inputs);
182
+ return {
183
+ scope: 'repo',
184
+ repoId: repo.id,
185
+ prds: inputs.map((input) => input.prdSlug).sort((a, b) => a.localeCompare(b)),
186
+ nodes: graph.nodes,
187
+ edges: graph.edges,
188
+ stats: graph.stats
189
+ };
190
+ }
package/docs/MCP.md CHANGED
@@ -119,10 +119,12 @@ In-sandbox discovery helper:
119
119
 
120
120
  ### `git`
121
121
 
122
+ - `git.getStatus(repoId, repoPath?)`
122
123
  - `git.getCommits(repoId, shas, repoPath?)`
123
124
  - `git.getDiff(repoId, commit, repoPath?)`
124
125
  - `git.getFileDiff(repoId, commit, file, repoPath?)`
125
126
  - `git.getFileContent(repoId, commit, file, repoPath?)`
127
+ - `git.commitIfChanged(repoId, message, options?)`
126
128
 
127
129
  ### `state`
128
130
 
@@ -181,6 +183,24 @@ return await Promise.all(commits.map(async (entry) => ({
181
183
  })))
182
184
  ```
183
185
 
186
+ Commit task-related changes when present:
187
+
188
+ ```js
189
+ const repo = await repos.current()
190
+
191
+ const commit = await git.commitIfChanged(repo.id, 'test: add task graph coverage', {
192
+ paths: ['app/components/graph/Explorer.spec.ts']
193
+ })
194
+
195
+ return commit
196
+ ```
197
+
198
+ `git.commitIfChanged` behavior:
199
+
200
+ - stages only the explicit `options.paths` list when provided
201
+ - commits only when staged changes exist
202
+ - returns `committed: false` with `reason: "no_changes" | "no_staged_changes"` instead of creating empty commits
203
+
184
204
  Inspect signatures at runtime:
185
205
 
186
206
  ```js
@@ -238,4 +258,5 @@ return { saved: true }
238
258
 
239
259
  - This server is for trusted local development.
240
260
  - APIs can read local filesystem and git history for registered repositories.
261
+ - `git.commitIfChanged` can create local commits when staged changes exist.
241
262
  - Do not expose this server to untrusted environments.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thxgg/steward",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Local-first PRD workflow steward with codemode MCP and web UI.",
5
5
  "type": "module",
6
6
  "author": "thxgg",
@@ -54,7 +54,11 @@
54
54
  "prepack": "npm run build"
55
55
  },
56
56
  "dependencies": {
57
+ "@dagrejs/dagre": "^2.0.4",
57
58
  "@modelcontextprotocol/sdk": "^1.26.0",
59
+ "@vue-flow/background": "^1.3.2",
60
+ "@vue-flow/controls": "^1.1.3",
61
+ "@vue-flow/core": "^1.48.2",
58
62
  "@vueuse/core": "^14.1.0",
59
63
  "chokidar": "^5.0.0",
60
64
  "class-variance-authority": "^0.7.1",