@uoyo/mvtt 2.0.0-beta.2 → 2.0.0-beta.4

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 (80) hide show
  1. package/README.md +1 -11
  2. package/dist/build/section-loader.d.ts.map +1 -1
  3. package/dist/build/section-loader.js +18 -8
  4. package/dist/build/section-loader.js.map +1 -1
  5. package/dist/fs/materialize.d.ts.map +1 -1
  6. package/dist/fs/materialize.js +12 -4
  7. package/dist/fs/materialize.js.map +1 -1
  8. package/dist/fs/registry-merge.d.ts +19 -0
  9. package/dist/fs/registry-merge.d.ts.map +1 -0
  10. package/dist/fs/registry-merge.js +177 -0
  11. package/dist/fs/registry-merge.js.map +1 -0
  12. package/dist/scripts/plan-update.cjs +7563 -0
  13. package/dist/scripts/session-update.cjs +7568 -0
  14. package/install-manifest.yaml +8 -2
  15. package/package.json +3 -2
  16. package/sources/defaults/config.yaml +7 -7
  17. package/sources/defaults/session.yaml +9 -16
  18. package/sources/scripts/plan-update.js +353 -0
  19. package/sources/scripts/session-update.js +351 -0
  20. package/sources/sections/activation-load-context.md +4 -0
  21. package/sources/sections/footer-next-steps.md +1 -1
  22. package/sources/sections/output-format-constraint.md +14 -0
  23. package/sources/sections/project-context-profile.md +29 -0
  24. package/sources/sections/session-update.md +100 -32
  25. package/sources/skills/mvt-analyze/manifest.yaml +4 -0
  26. package/sources/skills/mvt-analyze-code/manifest.yaml +24 -4
  27. package/sources/skills/mvt-bug-detect/business.md +99 -101
  28. package/sources/skills/mvt-bug-detect/manifest.yaml +84 -84
  29. package/sources/skills/mvt-check-context/business.md +3 -5
  30. package/sources/skills/mvt-check-context/manifest.yaml +15 -8
  31. package/sources/skills/mvt-cleanup/business.md +49 -23
  32. package/sources/skills/mvt-cleanup/manifest.yaml +18 -10
  33. package/sources/skills/mvt-config/business.md +1 -2
  34. package/sources/skills/mvt-config/manifest.yaml +11 -4
  35. package/sources/skills/mvt-create-skill/business.md +6 -5
  36. package/sources/skills/mvt-create-skill/manifest.yaml +30 -11
  37. package/sources/skills/mvt-design/business.md +3 -6
  38. package/sources/skills/mvt-design/manifest.yaml +17 -1
  39. package/sources/skills/mvt-fix/business.md +2 -1
  40. package/sources/skills/mvt-fix/manifest.yaml +9 -3
  41. package/sources/skills/mvt-help/business.md +2 -4
  42. package/sources/skills/mvt-help/manifest.yaml +13 -5
  43. package/sources/skills/mvt-implement/business.md +10 -7
  44. package/sources/skills/mvt-implement/manifest.yaml +16 -0
  45. package/sources/skills/mvt-init/business.md +2 -2
  46. package/sources/skills/mvt-init/manifest.yaml +4 -0
  47. package/sources/skills/mvt-manage-context/business.md +11 -0
  48. package/sources/skills/mvt-manage-context/manifest.yaml +20 -3
  49. package/sources/skills/mvt-plan-dev/business.md +101 -20
  50. package/sources/skills/mvt-plan-dev/manifest.yaml +21 -19
  51. package/sources/skills/mvt-quick-dev/business.md +2 -1
  52. package/sources/skills/mvt-quick-dev/manifest.yaml +24 -6
  53. package/sources/skills/mvt-refactor/business.md +2 -1
  54. package/sources/skills/mvt-refactor/manifest.yaml +24 -3
  55. package/sources/skills/mvt-resume/business.md +28 -68
  56. package/sources/skills/mvt-resume/manifest.yaml +17 -7
  57. package/sources/skills/mvt-review/business.md +3 -3
  58. package/sources/skills/mvt-review/manifest.yaml +25 -1
  59. package/sources/skills/mvt-status/business.md +14 -18
  60. package/sources/skills/mvt-status/manifest.yaml +11 -3
  61. package/sources/skills/mvt-sync-context/business.md +69 -35
  62. package/sources/skills/mvt-sync-context/manifest.yaml +9 -0
  63. package/sources/skills/mvt-template/business.md +0 -2
  64. package/sources/skills/mvt-template/manifest.yaml +13 -8
  65. package/sources/skills/mvt-test/business.md +3 -3
  66. package/sources/skills/mvt-test/manifest.yaml +17 -1
  67. package/sources/skills/mvt-update-plan/business.md +41 -28
  68. package/sources/skills/mvt-update-plan/manifest.yaml +14 -19
  69. package/dist/build/plan-validator.d.ts +0 -26
  70. package/dist/build/plan-validator.d.ts.map +0 -1
  71. package/dist/build/plan-validator.js +0 -225
  72. package/dist/build/plan-validator.js.map +0 -1
  73. package/dist/commands/build.d.ts +0 -5
  74. package/dist/commands/build.d.ts.map +0 -1
  75. package/dist/commands/build.js +0 -46
  76. package/dist/commands/build.js.map +0 -1
  77. package/dist/commands/migrate.d.ts +0 -16
  78. package/dist/commands/migrate.d.ts.map +0 -1
  79. package/dist/commands/migrate.js +0 -118
  80. package/dist/commands/migrate.js.map +0 -1
@@ -9,8 +9,10 @@ generated:
9
9
  source: "build:templates"
10
10
  - pattern: ".ai-agents/knowledge/core/_framework/**"
11
11
  source: "copy:sources/knowledge/core/_framework/"
12
- - pattern: ".ai-agents/registry.yaml"
13
- source: "copy:registry.yaml"
12
+ - pattern: ".ai-agents/scripts/session-update.cjs"
13
+ source: "bundle:sources/scripts/session-update.js"
14
+ - pattern: ".ai-agents/scripts/plan-update.cjs"
15
+ source: "bundle:sources/scripts/plan-update.js"
14
16
 
15
17
  create_once:
16
18
  - path: ".ai-agents/config.yaml"
@@ -21,6 +23,10 @@ create_once:
21
23
  source: "sources/defaults/project-context.yaml"
22
24
  - path: ".ai-agents/knowledge/core/manifest.yaml"
23
25
  source: "sources/knowledge/core/manifest.yaml"
26
+ # Reconcile-merged on update (see src/fs/registry-merge.ts): framework skills
27
+ # are refreshed, user `custom: true` skills and added knowledge bindings kept.
28
+ - path: ".ai-agents/registry.yaml"
29
+ source: "registry.yaml"
24
30
 
25
31
  user_data_dirs:
26
32
  - ".ai-agents/workspace/artifacts/"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uoyo/mvtt",
3
- "version": "2.0.0-beta.2",
3
+ "version": "2.0.0-beta.4",
4
4
  "description": "My Virtual Tech Team - AI-guided prompt orchestration framework",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,7 +35,7 @@
35
35
  "access": "public"
36
36
  },
37
37
  "scripts": {
38
- "build": "tsc -p tsconfig.build.json",
38
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && node build-scripts.js",
39
39
  "test": "vitest run",
40
40
  "test:watch": "vitest",
41
41
  "test:coverage": "vitest run --coverage",
@@ -45,6 +45,7 @@
45
45
  "@types/node": "^25.6.0",
46
46
  "@types/prompts": "^2.4.9",
47
47
  "@vitest/coverage-v8": "^2.1.9",
48
+ "esbuild": "^0.28.0",
48
49
  "typescript": "^5.4.0",
49
50
  "vitest": "^2.0.0"
50
51
  },
@@ -1,18 +1,13 @@
1
1
  # MVTT Framework - User Configuration
2
- # Unified config center: all skills read and enforce these settings via Activation Protocol
3
- # Modify via: /mvt-config or edit directly
4
2
 
5
3
  version: "2.0"
6
4
 
7
- # ============================================================
8
- # User Preferences (enforced by Activation Protocol Step 2)
9
- # ============================================================
10
5
  preferences:
11
- # Language used for interactive responses (chat replies, prompts, tables shown to the user)
6
+ # Language used for interactive responses
12
7
  # Options: en-US, zh-CN
13
8
  interaction_language: en-US
14
9
 
15
- # Language used for persistent document output (artifacts, project-context.md, generated reports)
10
+ # Language used for persistent document output
16
11
  # If absent, falls back to interaction_language.
17
12
  # Options: en-US, zh-CN
18
13
  document_output_language: en-US
@@ -25,3 +20,8 @@ preferences:
25
20
  # AI routing for /mvt-manage-context add
26
21
  context_routing:
27
22
  relevance_threshold: 70 # Skills scored >= this are pre-checked; below are folded
23
+
24
+ # Session history limits
25
+ history_limits:
26
+ history: 20 # Max history entries (1-100)
27
+ changes: 20 # Max changes entries (1-100)
@@ -1,31 +1,24 @@
1
1
  # Workspace Session State
2
- # Supports flexible workflows -- no longer bound to a fixed pipeline
3
2
 
4
3
  session:
5
4
  initialized_at: ""
6
- last_command: ""
5
+ last_synced_at: ""
7
6
 
8
- # Current active change (id/title/created_at set by /mvt-analyze;
9
- # plan_path/has_plan set by /mvt-plan-dev once a plan is generated)
10
7
  active_change:
11
8
  id: ""
12
9
  title: ""
13
10
  created_at: ""
14
11
  plan_path: ""
15
- has_plan: false
16
12
 
17
- # Recent changes with active plans (max 5, owned by /mvt-plan-dev and /mvt-update-plan)
18
- # Each entry: { id, title, plan_path, last_updated }
19
- # Status is the responsibility of plan.yaml (read on demand by /mvt-resume).
20
- recent_changes: []
13
+ changes: []
14
+ # - id: "chg-001"
15
+ # title: "User authentication"
16
+ # plan_path: ".ai-agents/workspace/artifacts/chg-001/plan.yaml"
17
+ # status: "active" # active | done | abandoned
18
+ # updated_at: "2026-05-23T14:30:00"
21
19
 
22
- # Skill execution history (append-only, max 10)
23
- # Records which skills have been executed, replacing the old progress field
24
- skill_history: []
25
- # - command: "/mvt-analyze"
20
+ history: []
21
+ # - skill: "/mvt-analyze"
26
22
  # completed_at: "2026-05-23T14:30:00"
27
23
  # summary: "Analyzed user authentication requirements"
28
24
  # change_id: "" # set when work belongs to an active change
29
-
30
- # Recent actions (append-only, max 5)
31
- recent_actions: []
@@ -0,0 +1,353 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * plan-update.js — Mechanical plan.yaml mutation script
5
+ *
6
+ * Replaces AI-driven plan.yaml mutation with a deterministic Node.js script.
7
+ * /mvt-update-plan calls this instead of hand-editing the plan: it applies a
8
+ * single task status change, recomputes current_task via the DAG rules, runs
9
+ * the full plan validator, and writes back atomically.
10
+ *
11
+ * The LLM stays responsible for the semantic parts (resolving a default task,
12
+ * mapping natural-language "done"/"blocked: <reason>" to arguments, rendering
13
+ * the JSON result into the user-facing summary). This script does only the
14
+ * mechanical, rule-driven work.
15
+ *
16
+ * NOTE: This source file uses `import from "yaml"`. During the build pipeline,
17
+ * esbuild bundles it into a zero-dependency single file deployed to
18
+ * .ai-agents/scripts/plan-update.cjs.
19
+ *
20
+ * Usage:
21
+ * node .ai-agents/scripts/plan-update.cjs \
22
+ * --plan <path-to-plan.yaml> \
23
+ * --task <task_id> \
24
+ * --status <pending|in_progress|done|blocked|skipped> \
25
+ * [--artifacts "<comma,separated,paths>"] \
26
+ * [--notes "<free-form text>"]
27
+ *
28
+ * Output:
29
+ * Success (exit 0): one-line JSON on stdout, e.g.
30
+ * {"ok":true,"task":{...},"current_task":"t2","plan_status":"in_progress",...}
31
+ * Failure (exit 1): plain-text error message(s) on stderr
32
+ */
33
+
34
+ import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync } from "node:fs";
35
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
36
+
37
+ // ── Constants ─────────────────────────────────────────────────────────────
38
+ const VALID_STATUSES = ["pending", "in_progress", "done", "blocked", "skipped"];
39
+ const TERMINAL_STATUSES = ["done", "blocked", "skipped"];
40
+
41
+ const ERRORS = {
42
+ MISSING_PLAN: () => "Missing required argument: --plan",
43
+ MISSING_TASK: () => "Missing required argument: --task",
44
+ MISSING_STATUS: () => "Missing required argument: --status",
45
+ INVALID_STATUS: (val) =>
46
+ `Invalid --status "${val}". Must be one of: ${VALID_STATUSES.join(", ")}.`,
47
+ PLAN_NOT_FOUND: (p) => `Plan not found at ${p}. Run /mvt-plan-dev to create one.`,
48
+ PLAN_PARSE_FAILED: (detail) =>
49
+ `Failed to parse plan.yaml: ${detail}. Fix the file manually; not repairing silently.`,
50
+ TASK_NOT_FOUND: (id, valid) =>
51
+ `Task "${id}" not found. Valid task ids: ${valid.length ? valid.join(", ") : "(none)"}.`,
52
+ VALIDATION_FAILED: (errs) =>
53
+ `Plan validation failed; file not written:\n - ${errs.join("\n - ")}`,
54
+ PLAN_WRITE_FAILED: (detail) => `Failed to write plan.yaml: ${detail}`,
55
+ };
56
+
57
+ // ── CLI Parsing ─────────────────────────────────────────────────────────────
58
+ function parseArgs(argv) {
59
+ const args = {};
60
+ for (let i = 2; i < argv.length; i++) {
61
+ if (argv[i].startsWith("--")) {
62
+ const key = argv[i].slice(2);
63
+ const next = argv[i + 1];
64
+ if (next && !next.startsWith("--")) {
65
+ args[key] = next;
66
+ i++;
67
+ } else {
68
+ args[key] = true;
69
+ }
70
+ }
71
+ }
72
+ return args;
73
+ }
74
+
75
+ function validateArgs(args) {
76
+ if (!args.plan || args.plan === true) return ERRORS.MISSING_PLAN();
77
+ if (!args.task || args.task === true) return ERRORS.MISSING_TASK();
78
+ if (!args.status || args.status === true) return ERRORS.MISSING_STATUS();
79
+ if (!VALID_STATUSES.includes(args.status)) return ERRORS.INVALID_STATUS(args.status);
80
+ return null;
81
+ }
82
+
83
+ // ── Mutation ─────────────────────────────────────────────────────────────────
84
+ function applyUpdate(plan, args, now) {
85
+ const task = plan.tasks.find((t) => t.id === args.task);
86
+
87
+ const oldStatus = task.status;
88
+ task.status = args.status;
89
+
90
+ if (args.artifacts && args.artifacts !== true) {
91
+ const incoming = args.artifacts
92
+ .split(",")
93
+ .map((s) => s.trim())
94
+ .filter(Boolean);
95
+ if (incoming.length) {
96
+ // artifacts may be null or missing a `files` key on initial plans.
97
+ if (!task.artifacts || typeof task.artifacts !== "object") {
98
+ task.artifacts = { files: [] };
99
+ }
100
+ if (!Array.isArray(task.artifacts.files)) {
101
+ task.artifacts.files = [];
102
+ }
103
+ const seen = new Set(task.artifacts.files);
104
+ for (const f of incoming) {
105
+ if (!seen.has(f)) {
106
+ task.artifacts.files.push(f);
107
+ seen.add(f);
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ if (args.notes && args.notes !== true) {
114
+ task.notes = args.notes;
115
+ }
116
+
117
+ // completed_at consistency: set only when transitioning to done, else null.
118
+ task.completed_at = args.status === "done" ? now : null;
119
+
120
+ plan.updated_at = now;
121
+
122
+ return { id: task.id, title: task.title || "", old_status: oldStatus, new_status: args.status };
123
+ }
124
+
125
+ // ── current_task recomputation (mirrors mvt-update-plan Step 4) ────────────────
126
+ function recomputeCurrentTask(plan, changedTaskId) {
127
+ let warning = null;
128
+
129
+ const changedTask = plan.tasks.find((t) => t.id === changedTaskId);
130
+ const changedToTerminal =
131
+ changedTask && TERMINAL_STATUSES.includes(changedTask.status);
132
+
133
+ // 1. An in_progress task that is NOT the one we just moved to terminal wins.
134
+ const activeInProgress = plan.tasks.find(
135
+ (t) => t.status === "in_progress" && !(t.id === changedTaskId && changedToTerminal)
136
+ );
137
+ if (activeInProgress) {
138
+ plan.current_task = activeInProgress.id;
139
+ plan.status = "in_progress";
140
+ return { warning };
141
+ }
142
+
143
+ // 2. First pending task whose deps are all done.
144
+ const doneIds = new Set(
145
+ plan.tasks.filter((t) => t.status === "done").map((t) => t.id)
146
+ );
147
+ const nextPending = plan.tasks.find(
148
+ (t) =>
149
+ t.status === "pending" &&
150
+ (t.depends_on || []).every((d) => doneIds.has(d))
151
+ );
152
+ if (nextPending) {
153
+ nextPending.status = "in_progress";
154
+ plan.current_task = nextPending.id;
155
+ plan.status = "in_progress";
156
+ return { warning };
157
+ }
158
+
159
+ // 3. Everything done -> plan complete.
160
+ if (plan.tasks.every((t) => t.status === "done")) {
161
+ plan.status = "done";
162
+ plan.current_task = null;
163
+ return { warning };
164
+ }
165
+
166
+ // 4. Pending tasks remain but none are executable (blocked by deps).
167
+ plan.current_task = null;
168
+ plan.status = "in_progress";
169
+ warning =
170
+ "All remaining tasks are blocked by dependencies; resolve a blocker before continuing.";
171
+ return { warning };
172
+ }
173
+
174
+ // ── Validation (mirrors mvt-plan-dev Step 5 + mvt-update-plan Step 5) ──────────
175
+ function validatePlan(plan) {
176
+ const errors = [];
177
+ const tasks = Array.isArray(plan.tasks) ? plan.tasks : [];
178
+
179
+ // Unique ids
180
+ const ids = tasks.map((t) => t.id);
181
+ const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
182
+ if (dupes.length) {
183
+ errors.push(`Duplicate task ids: ${[...new Set(dupes)].join(", ")}`);
184
+ }
185
+
186
+ const idSet = new Set(ids);
187
+
188
+ // Valid depends_on references
189
+ for (const t of tasks) {
190
+ for (const d of t.depends_on || []) {
191
+ if (!idSet.has(d)) {
192
+ errors.push(`Task "${t.id}" depends_on unknown task "${d}"`);
193
+ }
194
+ }
195
+ }
196
+
197
+ // DAG (no cycles) — only meaningful if all references resolve.
198
+ const cycle = findCycle(tasks);
199
+ if (cycle) {
200
+ errors.push(`Dependency cycle detected: ${cycle.join(" -> ")}`);
201
+ }
202
+
203
+ // At most one in_progress
204
+ const inProgress = tasks.filter((t) => t.status === "in_progress");
205
+ if (inProgress.length > 1) {
206
+ errors.push(
207
+ `More than one task is in_progress: ${inProgress.map((t) => t.id).join(", ")}`
208
+ );
209
+ }
210
+
211
+ // Acceptance required
212
+ for (const t of tasks) {
213
+ if (!Array.isArray(t.acceptance) || t.acceptance.length === 0) {
214
+ errors.push(`Task "${t.id}" has no acceptance criteria`);
215
+ }
216
+ }
217
+
218
+ // completed_at consistency
219
+ for (const t of tasks) {
220
+ if (t.status !== "done" && t.completed_at != null) {
221
+ errors.push(`Task "${t.id}" is not done but has completed_at set`);
222
+ }
223
+ }
224
+
225
+ // current_task validity
226
+ if (plan.status === "done") {
227
+ if (plan.current_task != null) {
228
+ errors.push("plan.status is done but current_task is not null");
229
+ }
230
+ } else if (plan.current_task != null) {
231
+ const ct = tasks.find((t) => t.id === plan.current_task);
232
+ if (!ct) {
233
+ errors.push(`current_task "${plan.current_task}" does not reference a task`);
234
+ } else if (ct.status !== "pending" && ct.status !== "in_progress") {
235
+ errors.push(
236
+ `current_task "${plan.current_task}" has status "${ct.status}" (must be pending or in_progress)`
237
+ );
238
+ }
239
+ }
240
+
241
+ return errors;
242
+ }
243
+
244
+ // Returns an array describing a cycle path, or null if the graph is a DAG.
245
+ function findCycle(tasks) {
246
+ const adj = new Map();
247
+ for (const t of tasks) adj.set(t.id, t.depends_on || []);
248
+
249
+ const WHITE = 0, GRAY = 1, BLACK = 2;
250
+ const color = new Map(tasks.map((t) => [t.id, WHITE]));
251
+ const stack = [];
252
+
253
+ function dfs(node) {
254
+ color.set(node, GRAY);
255
+ stack.push(node);
256
+ for (const dep of adj.get(node) || []) {
257
+ if (!color.has(dep)) continue; // unresolved ref handled elsewhere
258
+ if (color.get(dep) === GRAY) {
259
+ const start = stack.indexOf(dep);
260
+ return [...stack.slice(start), dep];
261
+ }
262
+ if (color.get(dep) === WHITE) {
263
+ const found = dfs(dep);
264
+ if (found) return found;
265
+ }
266
+ }
267
+ stack.pop();
268
+ color.set(node, BLACK);
269
+ return null;
270
+ }
271
+
272
+ for (const t of tasks) {
273
+ if (color.get(t.id) === WHITE) {
274
+ const found = dfs(t.id);
275
+ if (found) return found;
276
+ }
277
+ }
278
+ return null;
279
+ }
280
+
281
+ // ── Main ──────────────────────────────────────────────────────────────────────
282
+ function main() {
283
+ const args = parseArgs(process.argv);
284
+
285
+ const argErr = validateArgs(args);
286
+ if (argErr) {
287
+ process.stderr.write(argErr + "\n");
288
+ process.exit(1);
289
+ }
290
+
291
+ if (!existsSync(args.plan)) {
292
+ process.stderr.write(ERRORS.PLAN_NOT_FOUND(args.plan) + "\n");
293
+ process.exit(1);
294
+ }
295
+
296
+ let plan;
297
+ try {
298
+ plan = parseYaml(readFileSync(args.plan, "utf-8"));
299
+ } catch (e) {
300
+ process.stderr.write(ERRORS.PLAN_PARSE_FAILED(e.message) + "\n");
301
+ process.exit(1);
302
+ }
303
+
304
+ if (!plan || !Array.isArray(plan.tasks)) {
305
+ process.stderr.write(ERRORS.PLAN_PARSE_FAILED("missing tasks[]") + "\n");
306
+ process.exit(1);
307
+ }
308
+
309
+ if (!plan.tasks.some((t) => t.id === args.task)) {
310
+ process.stderr.write(
311
+ ERRORS.TASK_NOT_FOUND(args.task, plan.tasks.map((t) => t.id)) + "\n"
312
+ );
313
+ process.exit(1);
314
+ }
315
+
316
+ const now = new Date().toISOString();
317
+
318
+ const taskChange = applyUpdate(plan, args, now);
319
+ const { warning } = recomputeCurrentTask(plan, args.task);
320
+
321
+ const validationErrors = validatePlan(plan);
322
+ if (validationErrors.length) {
323
+ process.stderr.write(ERRORS.VALIDATION_FAILED(validationErrors) + "\n");
324
+ process.exit(1);
325
+ }
326
+
327
+ const tmpPath = args.plan + ".tmp";
328
+ try {
329
+ writeFileSync(tmpPath, stringifyYaml(plan), "utf-8");
330
+ renameSync(tmpPath, args.plan);
331
+ } catch (e) {
332
+ try {
333
+ if (existsSync(tmpPath)) unlinkSync(tmpPath);
334
+ } catch {
335
+ // best effort
336
+ }
337
+ process.stderr.write(ERRORS.PLAN_WRITE_FAILED(e.message) + "\n");
338
+ process.exit(1);
339
+ }
340
+
341
+ const doneCount = plan.tasks.filter((t) => t.status === "done").length;
342
+ const result = {
343
+ ok: true,
344
+ task: taskChange,
345
+ current_task: plan.current_task ?? null,
346
+ plan_status: plan.status,
347
+ progress: { done: doneCount, total: plan.tasks.length },
348
+ warning,
349
+ };
350
+ process.stdout.write(JSON.stringify(result) + "\n");
351
+ }
352
+
353
+ main();