@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.
- package/README.md +1 -11
- package/dist/build/section-loader.d.ts.map +1 -1
- package/dist/build/section-loader.js +18 -8
- package/dist/build/section-loader.js.map +1 -1
- package/dist/fs/materialize.d.ts.map +1 -1
- package/dist/fs/materialize.js +12 -4
- package/dist/fs/materialize.js.map +1 -1
- package/dist/fs/registry-merge.d.ts +19 -0
- package/dist/fs/registry-merge.d.ts.map +1 -0
- package/dist/fs/registry-merge.js +177 -0
- package/dist/fs/registry-merge.js.map +1 -0
- package/dist/scripts/plan-update.cjs +7563 -0
- package/dist/scripts/session-update.cjs +7568 -0
- package/install-manifest.yaml +8 -2
- package/package.json +3 -2
- package/sources/defaults/config.yaml +7 -7
- package/sources/defaults/session.yaml +9 -16
- package/sources/scripts/plan-update.js +353 -0
- package/sources/scripts/session-update.js +351 -0
- package/sources/sections/activation-load-context.md +4 -0
- package/sources/sections/footer-next-steps.md +1 -1
- package/sources/sections/output-format-constraint.md +14 -0
- package/sources/sections/project-context-profile.md +29 -0
- package/sources/sections/session-update.md +100 -32
- package/sources/skills/mvt-analyze/manifest.yaml +4 -0
- package/sources/skills/mvt-analyze-code/manifest.yaml +24 -4
- package/sources/skills/mvt-bug-detect/business.md +99 -101
- package/sources/skills/mvt-bug-detect/manifest.yaml +84 -84
- package/sources/skills/mvt-check-context/business.md +3 -5
- package/sources/skills/mvt-check-context/manifest.yaml +15 -8
- package/sources/skills/mvt-cleanup/business.md +49 -23
- package/sources/skills/mvt-cleanup/manifest.yaml +18 -10
- package/sources/skills/mvt-config/business.md +1 -2
- package/sources/skills/mvt-config/manifest.yaml +11 -4
- package/sources/skills/mvt-create-skill/business.md +6 -5
- package/sources/skills/mvt-create-skill/manifest.yaml +30 -11
- package/sources/skills/mvt-design/business.md +3 -6
- package/sources/skills/mvt-design/manifest.yaml +17 -1
- package/sources/skills/mvt-fix/business.md +2 -1
- package/sources/skills/mvt-fix/manifest.yaml +9 -3
- package/sources/skills/mvt-help/business.md +2 -4
- package/sources/skills/mvt-help/manifest.yaml +13 -5
- package/sources/skills/mvt-implement/business.md +10 -7
- package/sources/skills/mvt-implement/manifest.yaml +16 -0
- package/sources/skills/mvt-init/business.md +2 -2
- package/sources/skills/mvt-init/manifest.yaml +4 -0
- package/sources/skills/mvt-manage-context/business.md +11 -0
- package/sources/skills/mvt-manage-context/manifest.yaml +20 -3
- package/sources/skills/mvt-plan-dev/business.md +101 -20
- package/sources/skills/mvt-plan-dev/manifest.yaml +21 -19
- package/sources/skills/mvt-quick-dev/business.md +2 -1
- package/sources/skills/mvt-quick-dev/manifest.yaml +24 -6
- package/sources/skills/mvt-refactor/business.md +2 -1
- package/sources/skills/mvt-refactor/manifest.yaml +24 -3
- package/sources/skills/mvt-resume/business.md +28 -68
- package/sources/skills/mvt-resume/manifest.yaml +17 -7
- package/sources/skills/mvt-review/business.md +3 -3
- package/sources/skills/mvt-review/manifest.yaml +25 -1
- package/sources/skills/mvt-status/business.md +14 -18
- package/sources/skills/mvt-status/manifest.yaml +11 -3
- package/sources/skills/mvt-sync-context/business.md +69 -35
- package/sources/skills/mvt-sync-context/manifest.yaml +9 -0
- package/sources/skills/mvt-template/business.md +0 -2
- package/sources/skills/mvt-template/manifest.yaml +13 -8
- package/sources/skills/mvt-test/business.md +3 -3
- package/sources/skills/mvt-test/manifest.yaml +17 -1
- package/sources/skills/mvt-update-plan/business.md +41 -28
- package/sources/skills/mvt-update-plan/manifest.yaml +14 -19
- package/dist/build/plan-validator.d.ts +0 -26
- package/dist/build/plan-validator.d.ts.map +0 -1
- package/dist/build/plan-validator.js +0 -225
- package/dist/build/plan-validator.js.map +0 -1
- package/dist/commands/build.d.ts +0 -5
- package/dist/commands/build.d.ts.map +0 -1
- package/dist/commands/build.js +0 -46
- package/dist/commands/build.js.map +0 -1
- package/dist/commands/migrate.d.ts +0 -16
- package/dist/commands/migrate.d.ts.map +0 -1
- package/dist/commands/migrate.js +0 -118
- package/dist/commands/migrate.js.map +0 -1
package/install-manifest.yaml
CHANGED
|
@@ -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/
|
|
13
|
-
source: "
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
#
|
|
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();
|