@uoyo/mvtt 2.0.0 → 2.2.0
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/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/update.d.ts +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +18 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/fs/core-manifest.d.ts +4 -3
- package/dist/fs/core-manifest.d.ts.map +1 -1
- package/dist/fs/core-manifest.js +5 -4
- package/dist/fs/core-manifest.js.map +1 -1
- package/dist/fs/materialize.d.ts +2 -0
- package/dist/fs/materialize.d.ts.map +1 -1
- package/dist/fs/materialize.js +6 -5
- package/dist/fs/materialize.js.map +1 -1
- package/dist/fs/registry-merge.d.ts +4 -3
- package/dist/fs/registry-merge.d.ts.map +1 -1
- package/dist/fs/registry-merge.js +5 -4
- package/dist/fs/registry-merge.js.map +1 -1
- package/dist/scripts/epic-update.cjs +235 -207
- package/dist/scripts/epic-update.md +57 -0
- package/dist/scripts/plan-update.cjs +224 -222
- package/dist/scripts/plan-update.md +68 -0
- package/dist/scripts/session-update.cjs +229 -210
- package/install-manifest.yaml +4 -0
- package/package.json +1 -1
- package/sources/defaults/config.yaml +5 -0
- package/sources/scripts/epic-update.js +73 -22
- package/sources/scripts/epic-update.md +57 -0
- package/sources/scripts/plan-update.js +56 -28
- package/sources/scripts/plan-update.md +68 -0
- package/sources/scripts/session-update.js +68 -24
- package/sources/sections/activation-protocol.md +46 -0
- package/sources/sections/footer-next-steps.md +1 -1
- package/sources/sections/language-constraint.md +3 -18
- package/sources/sections/output-format-constraint.md +6 -9
- package/sources/sections/role-header.md +1 -1
- package/sources/sections/script-usage-rule.md +32 -0
- package/sources/sections/session-update.md +30 -110
- package/sources/skills/mvt-analyze/business.md +1 -1
- package/sources/skills/mvt-analyze/manifest.yaml +13 -16
- package/sources/skills/mvt-analyze-code/business.md +3 -0
- package/sources/skills/mvt-analyze-code/manifest.yaml +11 -15
- package/sources/skills/mvt-bug-detect/manifest.yaml +3 -4
- package/sources/skills/mvt-check-context/business.md +2 -2
- package/sources/skills/mvt-check-context/manifest.yaml +3 -6
- package/sources/skills/mvt-cleanup/business.md +47 -11
- package/sources/skills/mvt-cleanup/manifest.yaml +12 -13
- package/sources/skills/mvt-config/business.md +45 -49
- package/sources/skills/mvt-config/manifest.yaml +21 -25
- package/sources/skills/mvt-create-skill/business.md +15 -11
- package/sources/skills/mvt-create-skill/manifest.yaml +6 -9
- package/sources/skills/mvt-decompose/business.md +21 -9
- package/sources/skills/mvt-decompose/manifest.yaml +15 -17
- package/sources/skills/mvt-design/business.md +35 -44
- package/sources/skills/mvt-design/manifest.yaml +13 -15
- package/sources/skills/mvt-fix/business.md +7 -1
- package/sources/skills/mvt-fix/manifest.yaml +9 -13
- package/sources/skills/mvt-help/business.md +20 -9
- package/sources/skills/mvt-help/manifest.yaml +7 -18
- package/sources/skills/mvt-implement/business.md +27 -34
- package/sources/skills/mvt-implement/manifest.yaml +12 -21
- package/sources/skills/mvt-init/manifest.yaml +10 -13
- package/sources/skills/mvt-manage-context/business.md +4 -2
- package/sources/skills/mvt-manage-context/manifest.yaml +3 -6
- package/sources/skills/mvt-plan-dev/business.md +20 -8
- package/sources/skills/mvt-plan-dev/manifest.yaml +13 -17
- package/sources/skills/mvt-quick-dev/business.md +1 -1
- package/sources/skills/mvt-quick-dev/manifest.yaml +3 -4
- package/sources/skills/mvt-refactor/business.md +1 -1
- package/sources/skills/mvt-refactor/manifest.yaml +3 -4
- package/sources/skills/mvt-resume/business.md +3 -3
- package/sources/skills/mvt-resume/manifest.yaml +10 -13
- package/sources/skills/mvt-review/business.md +12 -11
- package/sources/skills/mvt-review/manifest.yaml +17 -18
- package/sources/skills/mvt-status/business.md +12 -21
- package/sources/skills/mvt-status/manifest.yaml +7 -10
- package/sources/skills/mvt-sync-context/business.md +20 -18
- package/sources/skills/mvt-sync-context/manifest.yaml +11 -15
- package/sources/skills/mvt-template/business.md +5 -5
- package/sources/skills/mvt-template/manifest.yaml +3 -6
- package/sources/skills/mvt-test/business.md +11 -11
- package/sources/skills/mvt-test/manifest.yaml +15 -18
- package/sources/skills/mvt-update-plan/business.md +18 -29
- package/sources/skills/mvt-update-plan/manifest.yaml +18 -16
- package/sources/templates/analyze-output/body.md +41 -0
- package/sources/templates/decompose-output/body.md +36 -0
- package/sources/templates/design-output/body.md +48 -0
- package/sources/templates/implement-output/body.md +48 -3
- package/sources/templates/project-context/body.md +45 -0
- package/sources/templates/review-output/body.md +59 -0
- package/sources/templates/test-output/body.md +72 -0
- package/sources/sections/activation-load-config.md +0 -11
- package/sources/sections/activation-load-context.md +0 -59
- package/sources/sections/activation-preflight.md +0 -14
package/install-manifest.yaml
CHANGED
|
@@ -17,6 +17,10 @@ generated:
|
|
|
17
17
|
source: "bundle:sources/scripts/plan-update.js"
|
|
18
18
|
- pattern: ".ai-agents/scripts/epic-update.cjs"
|
|
19
19
|
source: "bundle:sources/scripts/epic-update.js"
|
|
20
|
+
- pattern: ".ai-agents/scripts/plan-update.md"
|
|
21
|
+
source: "copy:sources/scripts/plan-update.md"
|
|
22
|
+
- pattern: ".ai-agents/scripts/epic-update.md"
|
|
23
|
+
source: "copy:sources/scripts/epic-update.md"
|
|
20
24
|
|
|
21
25
|
create_once:
|
|
22
26
|
- path: ".ai-agents/config.yaml"
|
package/package.json
CHANGED
|
@@ -11,27 +11,9 @@
|
|
|
11
11
|
* esbuild bundles it into a zero-dependency single file deployed to
|
|
12
12
|
* .ai-agents/scripts/epic-update.cjs.
|
|
13
13
|
*
|
|
14
|
-
* Usage:
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* --complete-child <change_id>
|
|
18
|
-
*
|
|
19
|
-
* node .ai-agents/scripts/epic-update.cjs \
|
|
20
|
-
* --epic <path> \
|
|
21
|
-
* --set-child-status <change_id> --child-status <status>
|
|
22
|
-
*
|
|
23
|
-
* node .ai-agents/scripts/epic-update.cjs \
|
|
24
|
-
* --epic <path> \
|
|
25
|
-
* --switch-active <change_id>
|
|
26
|
-
*
|
|
27
|
-
* node .ai-agents/scripts/epic-update.cjs \
|
|
28
|
-
* --epic <path> \
|
|
29
|
-
* --add-child <id> --child-title "<t>" --child-scope "<s>" \
|
|
30
|
-
* [--child-depends-on "dep1,dep2"] \
|
|
31
|
-
* [--add-child <id2> --child-title "<t2>" ...]
|
|
32
|
-
*
|
|
33
|
-
* node .ai-agents/scripts/epic-update.cjs \
|
|
34
|
-
* --validate <path-to-epic.yaml>
|
|
14
|
+
* Usage: see the "Script Usage Rule" section in any skill that references
|
|
15
|
+
* sections/script-usage-rule.md, or read the full reference at
|
|
16
|
+
* .ai-agents/scripts/epic-update.md.
|
|
35
17
|
*
|
|
36
18
|
* Output:
|
|
37
19
|
* Success (exit 0): one-line JSON on stdout
|
|
@@ -506,7 +488,76 @@ function main() {
|
|
|
506
488
|
process.exit(1);
|
|
507
489
|
}
|
|
508
490
|
|
|
509
|
-
|
|
491
|
+
// Best-effort session sync when epic closes (does not affect epic.yaml write).
|
|
492
|
+
let sessionSync = null;
|
|
493
|
+
if (epic.status === "done") {
|
|
494
|
+
sessionSync = syncSessionOnEpicClose(epic, epicPath, now);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
process.stdout.write(
|
|
498
|
+
JSON.stringify({ ok: true, ...result, session_sync: sessionSync }) + "\n"
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ── Session Sync ────────────────────────────────────────────────────────────
|
|
503
|
+
// Best-effort: clears session.active_epic and updates epics[] snapshot when
|
|
504
|
+
// the active epic transitions to "done". Failures are reported in the result
|
|
505
|
+
// but never roll back the epic.yaml write.
|
|
506
|
+
function syncSessionOnEpicClose(epic, epicPath, now) {
|
|
507
|
+
const projectRoot = findProjectRootFromPath(epicPath);
|
|
508
|
+
if (!projectRoot) {
|
|
509
|
+
return { ok: false, reason: "no-project-root" };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const sessionPath = join(projectRoot, ".ai-agents", "workspace", "session.yaml");
|
|
513
|
+
if (!existsSync(sessionPath)) {
|
|
514
|
+
return { ok: false, reason: "session-missing" };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
let session;
|
|
518
|
+
try {
|
|
519
|
+
session = parseYaml(readFileSync(sessionPath, "utf-8"));
|
|
520
|
+
} catch (e) {
|
|
521
|
+
return { ok: false, reason: "parse-failed", detail: e.message };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (!session || typeof session !== "object") {
|
|
525
|
+
return { ok: false, reason: "session-not-object" };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const epicId = epic.epic_id;
|
|
529
|
+
if (session.active_epic?.id !== epicId) {
|
|
530
|
+
return { ok: true, applied: false, reason: "active_epic-not-matching" };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
session.epics = session.epics || [];
|
|
534
|
+
const epicIdx = session.epics.findIndex((e) => e.id === epicId);
|
|
535
|
+
if (epicIdx >= 0) {
|
|
536
|
+
session.epics[epicIdx].status = "done";
|
|
537
|
+
session.epics[epicIdx].updated_at = now;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
session.active_epic = {
|
|
541
|
+
id: "",
|
|
542
|
+
title: "",
|
|
543
|
+
created_at: "",
|
|
544
|
+
epic_path: "",
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const sessionTmp = sessionPath + ".tmp";
|
|
548
|
+
try {
|
|
549
|
+
writeFileSync(sessionTmp, stringifyYaml(session, { lineWidth: 200 }), "utf-8");
|
|
550
|
+
renameSync(sessionTmp, sessionPath);
|
|
551
|
+
} catch (e) {
|
|
552
|
+
try {
|
|
553
|
+
if (existsSync(sessionTmp)) unlinkSync(sessionTmp);
|
|
554
|
+
} catch {
|
|
555
|
+
// best-effort cleanup — temp file overwritten next run
|
|
556
|
+
}
|
|
557
|
+
return { ok: false, reason: "write-failed", detail: e.message };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return { ok: true, applied: true, epic_id: epicId };
|
|
510
561
|
}
|
|
511
562
|
|
|
512
563
|
main();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Epic Update Script — Full Reference
|
|
2
|
+
|
|
3
|
+
> **This file is the authoritative usage reference for `epic-update.cjs`.**
|
|
4
|
+
> Read this file before calling the script. Do NOT read the `.cjs` or `.js` source to learn flag names or semantics.
|
|
5
|
+
|
|
6
|
+
The script has five modes — use exactly one mode per invocation:
|
|
7
|
+
|
|
8
|
+
## Commands
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
# Mode 1 — Complete the current child and advance to the next sub-change
|
|
12
|
+
node .ai-agents/scripts/epic-update.cjs --epic <epic_path> --complete-child <change_id>
|
|
13
|
+
|
|
14
|
+
# Mode 2 — Set a child's status without advancing current_change
|
|
15
|
+
node .ai-agents/scripts/epic-update.cjs --epic <epic_path> --set-child-status <change_id> --child-status <active|pending|done|abandoned>
|
|
16
|
+
|
|
17
|
+
# Mode 3 — Switch the active child to a different one (reorder)
|
|
18
|
+
node .ai-agents/scripts/epic-update.cjs --epic <epic_path> --switch-active <change_id>
|
|
19
|
+
|
|
20
|
+
# Mode 4 — Add one or more children to an existing epic
|
|
21
|
+
node .ai-agents/scripts/epic-update.cjs --epic <epic_path> \
|
|
22
|
+
--add-child <id> --child-title "<title>" --child-scope "<scope>" [--child-depends-on "dep1,dep2"] \
|
|
23
|
+
[--add-child <id2> --child-title "<title2>" --child-scope "<scope2>" ...]
|
|
24
|
+
|
|
25
|
+
# Mode 5 — Validate an epic.yaml (read-only, no write)
|
|
26
|
+
node .ai-agents/scripts/epic-update.cjs --validate <epic_path>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Argument values
|
|
30
|
+
|
|
31
|
+
| Argument | Value source | Example |
|
|
32
|
+
|----------|-------------|---------|
|
|
33
|
+
| `--epic` | `active_epic.epic_path` resolved from session.yaml | `".ai-agents/workspace/artifacts/epic-20260608-ecommerce-platform/epic.yaml"` |
|
|
34
|
+
| `--complete-child` | `active_change.id` of the child whose plan is fully done | `20260608-sub` |
|
|
35
|
+
| `--set-child-status` | target child `change_id` to re-status | `20260608-sub` |
|
|
36
|
+
| `--child-status` | new status (with `--set-child-status`): `active` / `pending` / `done` / `abandoned` | `done` |
|
|
37
|
+
| `--switch-active` | target child `change_id` to make the active one | `20260609-sub2` |
|
|
38
|
+
| `--add-child` | new child id (repeatable for multiple children in one invocation) | `20260620-sub3` |
|
|
39
|
+
| `--child-title` | title for the child added by the preceding `--add-child` | `"Cart"` |
|
|
40
|
+
| `--child-scope` | scope description for the child added by the preceding `--add-child` | `"Shopping cart CRUD and checkout flow"` |
|
|
41
|
+
| `--child-depends-on` | optional; comma-separated prerequisite child `change_id` values | `"20260608-sub"` |
|
|
42
|
+
| `--validate` | path to an `epic.yaml` to validate (read-only) | same as `--epic` |
|
|
43
|
+
|
|
44
|
+
## Parameter semantics
|
|
45
|
+
|
|
46
|
+
| Argument | When to use | Effect on `epic.yaml` |
|
|
47
|
+
|----------|-------------|------------------------|
|
|
48
|
+
| `--complete-child` | A child change's plan is fully done and the epic should advance | Sets the child `status: done`, advances `current_change` to the next `pending` child whose `depends_on` are all `done` (DAG-based). |
|
|
49
|
+
| `--set-child-status` + `--child-status` | Mark a child `done` or `abandoned` without advancing `current_change` (e.g. defer mode) | Sets the child's status only; `current_change` unchanged. |
|
|
50
|
+
| `--switch-active` | Reorder to a different child (dependencies permitting) | Sets the target child `active`, others `pending`, updates `current_change`. Rejects if the target's `depends_on` have unfinished prerequisites. |
|
|
51
|
+
| `--add-child` (+ `--child-title` / `--child-scope` / `--child-depends-on`) | Append one or more children to an existing epic | Adds entries to `children[]`; validates id uniqueness + DAG; defaults `project` to the sole project name when single-project. |
|
|
52
|
+
| `--validate` | Verify `epic.yaml` integrity (e.g. after `/mvt-decompose` writes it) | Read-only check; no write. Reports DAG/structure errors on stderr. |
|
|
53
|
+
|
|
54
|
+
## Output interpretation
|
|
55
|
+
|
|
56
|
+
- **Exit 0**: success. stdout is a single-line JSON object (mirrors `plan-update.cjs` protocol). Use the fields directly to render output. The file is already written — do NOT read it back to verify.
|
|
57
|
+
- **Exit 1**: failure. stderr carries a plain-text error (unknown mode, child not found, dependency unsatisfied, validation failure, parse/write error). The file was **not** modified. Report the error to the user and do not fabricate a success summary.
|
|
@@ -19,16 +19,9 @@
|
|
|
19
19
|
* esbuild bundles it into a zero-dependency single file deployed to
|
|
20
20
|
* .ai-agents/scripts/plan-update.cjs.
|
|
21
21
|
*
|
|
22
|
-
* Usage:
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* --task <task_id> \
|
|
26
|
-
* --status <pending|in_progress|done|blocked|skipped> \
|
|
27
|
-
* [--projects "web,api"] \
|
|
28
|
-
* [--artifacts "<comma,separated,paths>"] \
|
|
29
|
-
* [--notes "<free-form text>"] \
|
|
30
|
-
* [--deliverables-pointer current] \
|
|
31
|
-
* [--mark-deliverable-stale <task_id>[,task_id2,...]]
|
|
22
|
+
* Usage: see the "Script Usage Rule" section in any skill that references
|
|
23
|
+
* sections/script-usage-rule.md, or read the full reference at
|
|
24
|
+
* .ai-agents/scripts/plan-update.md.
|
|
32
25
|
*
|
|
33
26
|
* Output:
|
|
34
27
|
* Success (exit 0): one-line JSON on stdout, e.g.
|
|
@@ -124,10 +117,18 @@ function parseArgs(argv) {
|
|
|
124
117
|
}
|
|
125
118
|
|
|
126
119
|
function validateArgs(args) {
|
|
120
|
+
if (args.validate) return null;
|
|
127
121
|
if (!args.plan || args.plan === true) return ERRORS.MISSING_PLAN();
|
|
128
122
|
if (!args.task || args.task === true) return ERRORS.MISSING_TASK();
|
|
129
|
-
|
|
130
|
-
|
|
123
|
+
const hasStatus = args.status && args.status !== true;
|
|
124
|
+
const hasMutation = hasStatus ||
|
|
125
|
+
(args.artifacts && args.artifacts !== true) ||
|
|
126
|
+
(args.notes && args.notes !== true) ||
|
|
127
|
+
(args["deliverables-pointer"] && args["deliverables-pointer"] !== true) ||
|
|
128
|
+
(args["mark-deliverable-stale"] && args["mark-deliverable-stale"] !== true);
|
|
129
|
+
if (!hasMutation) return ERRORS.MISSING_STATUS();
|
|
130
|
+
if (args.status === true) return ERRORS.MISSING_STATUS();
|
|
131
|
+
if (hasStatus && !VALID_STATUSES.includes(args.status)) return ERRORS.INVALID_STATUS(args.status);
|
|
131
132
|
return null;
|
|
132
133
|
}
|
|
133
134
|
|
|
@@ -136,7 +137,9 @@ function applyUpdate(plan, args, now) {
|
|
|
136
137
|
const task = plan.tasks.find((t) => t.id === args.task);
|
|
137
138
|
|
|
138
139
|
const oldStatus = task.status;
|
|
139
|
-
|
|
140
|
+
if (args.status && args.status !== true) {
|
|
141
|
+
task.status = args.status;
|
|
142
|
+
}
|
|
140
143
|
|
|
141
144
|
if (args.artifacts && args.artifacts !== true) {
|
|
142
145
|
const incoming = args.artifacts
|
|
@@ -164,11 +167,13 @@ function applyUpdate(plan, args, now) {
|
|
|
164
167
|
task.notes = args.notes;
|
|
165
168
|
}
|
|
166
169
|
|
|
167
|
-
// completed_at consistency: set only on
|
|
168
|
-
if (args.status
|
|
169
|
-
task.completed_at
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
// completed_at consistency: set only on status updates.
|
|
171
|
+
if (args.status && args.status !== true) {
|
|
172
|
+
if (args.status === "done" && !task.completed_at) {
|
|
173
|
+
task.completed_at = now;
|
|
174
|
+
} else if (args.status !== "done") {
|
|
175
|
+
task.completed_at = null;
|
|
176
|
+
}
|
|
172
177
|
}
|
|
173
178
|
|
|
174
179
|
// --deliverables-pointer current
|
|
@@ -201,7 +206,7 @@ function applyUpdate(plan, args, now) {
|
|
|
201
206
|
|
|
202
207
|
plan.updated_at = now;
|
|
203
208
|
|
|
204
|
-
return { id: task.id, title: task.title || "", old_status: oldStatus, new_status:
|
|
209
|
+
return { id: task.id, title: task.title || "", old_status: oldStatus, new_status: task.status };
|
|
205
210
|
}
|
|
206
211
|
|
|
207
212
|
// -- current_tasks recomputation (per-project independent advancement) --
|
|
@@ -516,6 +521,10 @@ function findCycleInSubgraph(tasks, taskIds) {
|
|
|
516
521
|
function main() {
|
|
517
522
|
const args = parseArgs(process.argv);
|
|
518
523
|
|
|
524
|
+
if (args.validate && args.validate !== true && !args.plan) {
|
|
525
|
+
args.plan = args.validate;
|
|
526
|
+
}
|
|
527
|
+
|
|
519
528
|
const argErr = validateArgs(args);
|
|
520
529
|
if (argErr) {
|
|
521
530
|
process.stderr.write(argErr + "\n");
|
|
@@ -540,13 +549,6 @@ function main() {
|
|
|
540
549
|
process.exit(1);
|
|
541
550
|
}
|
|
542
551
|
|
|
543
|
-
if (!plan.tasks.some((t) => t.id === args.task)) {
|
|
544
|
-
process.stderr.write(
|
|
545
|
-
ERRORS.TASK_NOT_FOUND(args.task, plan.tasks.map((t) => t.id)) + "\n"
|
|
546
|
-
);
|
|
547
|
-
process.exit(1);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
552
|
// Parse --projects; if not provided, derive from tasks
|
|
551
553
|
let projectList = null;
|
|
552
554
|
if (args.projects && args.projects !== true) {
|
|
@@ -557,10 +559,30 @@ function main() {
|
|
|
557
559
|
projectList = deriveProjectList(plan.tasks);
|
|
558
560
|
}
|
|
559
561
|
|
|
562
|
+
if (args.validate) {
|
|
563
|
+
const validationErrors = validatePlan(plan, projectList);
|
|
564
|
+
if (validationErrors.length) {
|
|
565
|
+
process.stderr.write(ERRORS.VALIDATION_FAILED(validationErrors) + "\n");
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
process.stdout.write(JSON.stringify({ ok: true, plan_status: plan.status, tasks: plan.tasks.length }) + "\n");
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (!plan.tasks.some((t) => t.id === args.task)) {
|
|
573
|
+
process.stderr.write(
|
|
574
|
+
ERRORS.TASK_NOT_FOUND(args.task, plan.tasks.map((t) => t.id)) + "\n"
|
|
575
|
+
);
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
|
|
560
579
|
// Migrate old current_task (string) to current_tasks (Record) if needed.
|
|
561
580
|
// Also remove legacy current_task field even when null (YAML `current_task: null`).
|
|
562
581
|
if (plan.current_task != null && (!plan.current_tasks || typeof plan.current_tasks !== "object")) {
|
|
563
|
-
|
|
582
|
+
const legacyProject = projectList.length === 1
|
|
583
|
+
? projectList[0]
|
|
584
|
+
: (loadSoleProject(findProjectRootFromPath(args.plan))?.[0] || "default");
|
|
585
|
+
plan.current_tasks = { [legacyProject]: plan.current_task };
|
|
564
586
|
}
|
|
565
587
|
if ("current_task" in plan) {
|
|
566
588
|
delete plan.current_task;
|
|
@@ -576,7 +598,13 @@ function main() {
|
|
|
576
598
|
process.stderr.write(taskChange.error + "\n");
|
|
577
599
|
process.exit(1);
|
|
578
600
|
}
|
|
579
|
-
|
|
601
|
+
let warning = null;
|
|
602
|
+
let switchNotif = null;
|
|
603
|
+
if (args.status && args.status !== true) {
|
|
604
|
+
const recomputed = recomputeCurrentTasks(plan, args.task, projectList);
|
|
605
|
+
warning = recomputed.warning;
|
|
606
|
+
switchNotif = recomputed.project_switch;
|
|
607
|
+
}
|
|
580
608
|
|
|
581
609
|
const validationErrors = validatePlan(plan, projectList);
|
|
582
610
|
if (validationErrors.length) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Plan Update Script — Full Reference
|
|
2
|
+
|
|
3
|
+
> **This file is the authoritative usage reference for `plan-update.cjs`.**
|
|
4
|
+
> Read this file before calling the script. Do NOT read the `.cjs` or `.js` source to learn flag names or semantics.
|
|
5
|
+
|
|
6
|
+
## Command
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
node .ai-agents/scripts/plan-update.cjs \
|
|
10
|
+
--plan "<active_change.plan_path>" \
|
|
11
|
+
--task <task_id> \
|
|
12
|
+
--projects "<comma,separated,project,names>" \
|
|
13
|
+
[--status <pending|in_progress|done|blocked|skipped>] \
|
|
14
|
+
[--artifacts "<comma,separated,paths>"] \
|
|
15
|
+
[--notes "<free-form text>"] \
|
|
16
|
+
[--deliverables-pointer current] \
|
|
17
|
+
[--mark-deliverable-stale <task_id>[,task_id2,...]]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Read-only validation mode:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
node .ai-agents/scripts/plan-update.cjs --validate "<draft-plan-path>" [--projects "<comma,separated,project,names>"]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Include `--artifacts`, `--notes`, `--deliverables-pointer`, and `--mark-deliverable-stale` only when the skill's logic determines they apply; omit each flag otherwise.
|
|
27
|
+
|
|
28
|
+
## Argument values
|
|
29
|
+
|
|
30
|
+
| Argument | Value source | Example |
|
|
31
|
+
|----------|-------------|---------|
|
|
32
|
+
| `--plan` | `active_change.plan_path` resolved from session.yaml | `".ai-agents/workspace/artifacts/chg-001/plan.yaml"` |
|
|
33
|
+
| `--task` | the `task_id` being updated (resolved by the skill's Step 1) | `t1` |
|
|
34
|
+
| `--status` | optional; the new status: `pending` / `in_progress` / `done` / `blocked` / `skipped` | `done` |
|
|
35
|
+
| `--projects` | comma-separated project names from `project-context.yaml > projects[].name` (single-project: the sole project name) | `"web,api"` |
|
|
36
|
+
| `--artifacts` | optional; comma-separated paths to append (the script de-duplicates) | `"src/auth.ts,src/auth.test.ts"` |
|
|
37
|
+
| `--notes` | optional; overwrites the task's `notes` | `"blocked on API spec"` |
|
|
38
|
+
| `--deliverables-pointer` | optional; set to `current` to record that this task's deliverables section is written | `current` |
|
|
39
|
+
| `--mark-deliverable-stale` | optional; comma-separated downstream task ids whose deliverables are now stale | `"t3,t4"` |
|
|
40
|
+
| `--validate` | optional read-only validation target; accepts the plan path as its value | `".ai-agents/workspace/artifacts/chg-001/plan.yaml"` |
|
|
41
|
+
|
|
42
|
+
## Parameter semantics
|
|
43
|
+
|
|
44
|
+
| Argument | When to use | Effect on `plan.yaml` |
|
|
45
|
+
|----------|-------------|------------------------|
|
|
46
|
+
| `--task` + optional `--status` | Use `--status` for status transitions; omit it for metadata-only updates such as deliverables freshness | When present, sets the task status; sets `completed_at` to now when `done`, else `null`; refreshes `plan.updated_at`. When omitted, status, `completed_at`, DAG advancement, and `current_tasks` are left unchanged. |
|
|
47
|
+
| `--projects` | Always (per-project validation) | Drives per-project DAG advancement of `current_tasks` and per-project validation. Required for correct multi-project plans. |
|
|
48
|
+
| `--artifacts` | Task produced or touched files | Appends + de-duplicates paths into the task's `artifacts.files`; handles `artifacts: null`. |
|
|
49
|
+
| `--notes` | Task needs a free-form note | Overwrites the task's existing `notes`. |
|
|
50
|
+
| `--deliverables-pointer` + `--mark-deliverable-stale` | Task wrote its deliverables section (e.g. `/mvt-implement` Step 8) | Records the deliverables pointer on the task; marks the listed downstream tasks' deliverables as stale so `/mvt-resume` and `/mvt-status` surface a warning. Use both flags together in a single invocation. |
|
|
51
|
+
| `--validate` | `/mvt-plan-dev` has assembled a draft plan before final write | Parses and validates the plan, prints JSON on success, and never writes the file. |
|
|
52
|
+
|
|
53
|
+
## What the script does (deterministically)
|
|
54
|
+
|
|
55
|
+
1. **Apply**: when `--status` is present, sets the task status and `completed_at`; appends + de-duplicates `--artifacts`; overwrites `--notes`; refreshes `plan.updated_at`.
|
|
56
|
+
2. **Recompute `current_tasks`** only when `--status` is present (per-project independent advancement): for each project, finds the `in_progress` task or advances the first `pending` task whose `depends_on` are all in `resolvedIds` (done + skipped; blocked does NOT satisfy). Detects `project_switch` when advancement crosses a project boundary. Plan done -> `current_tasks = {}`.
|
|
57
|
+
3. **Validate** the mutated plan (unique ids, valid `depends_on` references, DAG/no-cycle per project, one `in_progress` per project, every task has acceptance, `completed_at` consistency, `current_tasks` validity, project naming constraint, task project membership).
|
|
58
|
+
4. **Write atomically** (temp + rename) only if validation passes.
|
|
59
|
+
|
|
60
|
+
## Output interpretation
|
|
61
|
+
|
|
62
|
+
- **Exit 0**: success. stdout is a single-line JSON object, e.g.:
|
|
63
|
+
```json
|
|
64
|
+
{"ok":true,"task":{"id":"t1","title":"...","old_status":"in_progress","new_status":"done"},"current_tasks":{"default":"t2"},"plan_status":"in_progress","progress":{"done":1,"total":4},"warning":null,"project_switch":null}
|
|
65
|
+
```
|
|
66
|
+
Use these fields directly to render output. The file is already written — do NOT read it back to verify. If `warning` is non-null, surface it. If `project_switch` is non-null, note the project boundary crossing.
|
|
67
|
+
- **Exit 1**: failure. stderr carries the error (invalid status, task not found, validation failure, parse/write error). The file was **not** modified. Report the error to the user and do not fabricate a success summary.
|
|
68
|
+
- **Read-only validation**: exit 0 stdout is `{"ok":true,"plan_status":"...","tasks":N}`. Exit 1 stderr carries parse or validation errors; no file is modified.
|
|
@@ -10,25 +10,8 @@
|
|
|
10
10
|
* pipeline, esbuild bundles it into a zero-dependency single file that
|
|
11
11
|
* gets deployed to .ai-agents/scripts/session-update.cjs.
|
|
12
12
|
*
|
|
13
|
-
* Usage:
|
|
14
|
-
*
|
|
15
|
-
* --skill <name> \
|
|
16
|
-
* --summary <text> \
|
|
17
|
-
* [--change-id <id>] \
|
|
18
|
-
* [--new-change <title>] \
|
|
19
|
-
* [--set-initialized] \
|
|
20
|
-
* [--update-change] \
|
|
21
|
-
* [--set-plan-path <path>] \
|
|
22
|
-
* [--close-change] \
|
|
23
|
-
* [--set-change-status <status>] \
|
|
24
|
-
* [--no-change] \
|
|
25
|
-
* [--set-synced] \
|
|
26
|
-
* [--truncate-history <n>] \
|
|
27
|
-
* [--new-epic <title> --epic-id <id>] \
|
|
28
|
-
* [--set-epic-path <path>] \
|
|
29
|
-
* [--set-epic-status <status>] \
|
|
30
|
-
* [--close-epic] \
|
|
31
|
-
* [--epic-id <id>] (with --new-change, links sub-change to epic)
|
|
13
|
+
* Usage: see the "State Update" section in any skill that references
|
|
14
|
+
* sections/session-update.md (rendered with per-skill flag parameters).
|
|
32
15
|
*
|
|
33
16
|
* Output:
|
|
34
17
|
* Success (exit 0): {"ok":true}
|
|
@@ -57,6 +40,7 @@ const ERRORS = {
|
|
|
57
40
|
CLOSE_NEW_EPIC_CONFLICT: () => "--close-epic and --new-epic are mutually exclusive",
|
|
58
41
|
NO_ACTIVE_EPIC: (flag) => `${flag} requires an active epic (active_epic.id is empty)`,
|
|
59
42
|
EPIC_ID_ORPHAN: () => "--epic-id (for sub-change) requires --new-change",
|
|
43
|
+
MISSING_REMOVE_VALUE: () => "--remove-change / --remove-epic requires a non-empty value",
|
|
60
44
|
};
|
|
61
45
|
|
|
62
46
|
// ── Defaults ────────────────────────────────────────────────────────────────
|
|
@@ -99,6 +83,17 @@ function parseArgs(argv) {
|
|
|
99
83
|
return args;
|
|
100
84
|
}
|
|
101
85
|
|
|
86
|
+
// ── Multi-id Helper ─────────────────────────────────────────────────────────
|
|
87
|
+
// Comma-separated list of ids, used by --remove-change / --remove-epic.
|
|
88
|
+
// Mirrors the convention in sources/scripts/epic-update.js.
|
|
89
|
+
function parseIdList(value) {
|
|
90
|
+
if (value == null) return [];
|
|
91
|
+
return String(value)
|
|
92
|
+
.split(",")
|
|
93
|
+
.map((s) => s.trim())
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
}
|
|
96
|
+
|
|
102
97
|
// ── Config Loading ──────────────────────────────────────────────────────────
|
|
103
98
|
function loadHistoryLimits(configPath) {
|
|
104
99
|
const limits = { ...DEFAULT_LIMITS };
|
|
@@ -134,11 +129,25 @@ function validate(args) {
|
|
|
134
129
|
if (!args.summary) return ERRORS.MISSING_SUMMARY();
|
|
135
130
|
if (args["new-change"] && !args["change-id"]) return ERRORS.CHANGE_ID_REQUIRED();
|
|
136
131
|
|
|
137
|
-
// Epic combo validation
|
|
132
|
+
// Epic combo validation
|
|
138
133
|
if (args["new-epic"] && !args["epic-id"]) return ERRORS.EPIC_ID_REQUIRED();
|
|
139
134
|
if (args["close-epic"] && args["new-epic"]) return ERRORS.CLOSE_NEW_EPIC_CONFLICT();
|
|
140
135
|
if (args["epic-id"] && !args["new-change"] && !args["new-epic"]) return ERRORS.EPIC_ID_ORPHAN();
|
|
141
136
|
|
|
137
|
+
// Remove flags require non-empty values
|
|
138
|
+
if (
|
|
139
|
+
args["remove-change"] !== undefined
|
|
140
|
+
&& (args["remove-change"] === true || !String(args["remove-change"]).trim())
|
|
141
|
+
) {
|
|
142
|
+
return ERRORS.MISSING_REMOVE_VALUE();
|
|
143
|
+
}
|
|
144
|
+
if (
|
|
145
|
+
args["remove-epic"] !== undefined
|
|
146
|
+
&& (args["remove-epic"] === true || !String(args["remove-epic"]).trim())
|
|
147
|
+
) {
|
|
148
|
+
return ERRORS.MISSING_REMOVE_VALUE();
|
|
149
|
+
}
|
|
150
|
+
|
|
142
151
|
return null;
|
|
143
152
|
}
|
|
144
153
|
|
|
@@ -226,12 +235,13 @@ function main() {
|
|
|
226
235
|
}
|
|
227
236
|
}
|
|
228
237
|
|
|
229
|
-
// Now set new active_change
|
|
238
|
+
// Now set new active_change (preserve fields only when re-invoking on same change)
|
|
239
|
+
const isSameChange = session.active_change.id === args["change-id"];
|
|
230
240
|
session.active_change.id = args["change-id"];
|
|
231
241
|
session.active_change.title = args["new-change"];
|
|
232
|
-
session.active_change.created_at = now;
|
|
233
|
-
session.active_change.plan_path = "";
|
|
234
|
-
session.active_change.epic_id = args["epic-id"] || "";
|
|
242
|
+
session.active_change.created_at = isSameChange ? (session.active_change.created_at || now) : now;
|
|
243
|
+
session.active_change.plan_path = isSameChange ? (session.active_change.plan_path || "") : "";
|
|
244
|
+
session.active_change.epic_id = args["epic-id"] || session.active_change.epic_id || "";
|
|
235
245
|
}
|
|
236
246
|
|
|
237
247
|
// --set-initialized
|
|
@@ -429,6 +439,40 @@ function main() {
|
|
|
429
439
|
};
|
|
430
440
|
}
|
|
431
441
|
|
|
442
|
+
// --remove-change <ids>: filter session.changes[]
|
|
443
|
+
if (args["remove-change"] !== undefined) {
|
|
444
|
+
session.changes = session.changes || [];
|
|
445
|
+
const rawIds = args["remove-change"];
|
|
446
|
+
let removed = 0;
|
|
447
|
+
for (const id of parseIdList(rawIds)) {
|
|
448
|
+
const before = session.changes.length;
|
|
449
|
+
session.changes = session.changes.filter((e) => e.id !== id);
|
|
450
|
+
if (session.changes.length < before) removed++;
|
|
451
|
+
}
|
|
452
|
+
if (removed === 0) {
|
|
453
|
+
process.stderr.write(
|
|
454
|
+
`Warning: --remove-change requested ids [${rawIds}] not found; no entries removed.\n`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// --remove-epic <ids>: filter session.epics[]
|
|
460
|
+
if (args["remove-epic"] !== undefined) {
|
|
461
|
+
session.epics = session.epics || [];
|
|
462
|
+
const rawIds = args["remove-epic"];
|
|
463
|
+
let removed = 0;
|
|
464
|
+
for (const id of parseIdList(rawIds)) {
|
|
465
|
+
const before = session.epics.length;
|
|
466
|
+
session.epics = session.epics.filter((e) => e.id !== id);
|
|
467
|
+
if (session.epics.length < before) removed++;
|
|
468
|
+
}
|
|
469
|
+
if (removed === 0) {
|
|
470
|
+
process.stderr.write(
|
|
471
|
+
`Warning: --remove-epic requested ids [${rawIds}] not found; no entries removed.\n`,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
432
476
|
// ── Write back atomically ─────────────────────────────────────────────
|
|
433
477
|
const tmpPath = sessionPath + ".tmp";
|
|
434
478
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
## Activation Protocol
|
|
2
|
+
|
|
3
|
+
Two blocks: **Load** (what to read, and when) then **Resolve** (what to decide). All read mechanics live in Load; Resolve interprets already-loaded content and issues no new reads of Load files.
|
|
4
|
+
|
|
5
|
+
### Load (do this first)
|
|
6
|
+
|
|
7
|
+
**Wave 1 — read in ONE parallel batch, then never re-read these:**
|
|
8
|
+
- `.ai-agents/workspace/project-context.yaml`
|
|
9
|
+
- `.ai-agents/registry.yaml`
|
|
10
|
+
- `.ai-agents/config.yaml`
|
|
11
|
+
{{#activation_reads}}
|
|
12
|
+
- `.ai-agents/workspace/{{.}}`
|
|
13
|
+
{{/activation_reads}}
|
|
14
|
+
|
|
15
|
+
**Deferred (load after Wave 1; do not re-read Wave 1 files):**
|
|
16
|
+
- *Knowledge* — depends on the loaded `registry.yaml`; resolve and load per the rule in Resolve. May be serial (manifest-driven).
|
|
17
|
+
{{?extended_context}}
|
|
18
|
+
- *Extended Context* (listed below) — once `session.yaml` values such as `{active_change.id}` / `{plan_path}` are known, read the concrete files (e.g. `analysis.md`, `design.md`, `plan.yaml`, template paths) in ONE parallel sub-batch. Discovery directives (e.g. "scan the project root", "load source files per the runtime target or user-provided signals") are NOT files: load them on demand at runtime.
|
|
19
|
+
|
|
20
|
+
Extended Context entries:
|
|
21
|
+
{{/extended_context}}
|
|
22
|
+
{{#extended_context}}
|
|
23
|
+
- {{.}}
|
|
24
|
+
{{/extended_context}}
|
|
25
|
+
|
|
26
|
+
### Resolve (interpret loaded content — no new reads of Load files)
|
|
27
|
+
|
|
28
|
+
**Project Scope (PS)** — from `project-context.yaml > projects[]`:
|
|
29
|
+
- **Single project** → PS = [the sole project]. Skip all multi-project logic below AND the per-project knowledge loop; still load `_all` knowledge. This is the common case.
|
|
30
|
+
- **Multiple projects** →
|
|
31
|
+
- *Mode A (active plan):* PS = the `current_tasks` project values that exist in `projects[]`; otherwise match current paths against `projects[].path` / `source_paths`; if still unresolved, list candidates and ask. Never silently load all.
|
|
32
|
+
- *Mode B (no plan / ad-hoc):* defer PS to execution — identify the change target, match it against `projects[].path` / `source_paths`.
|
|
33
|
+
|
|
34
|
+
**Knowledge** — always load `knowledge._all` + `skills.<current-skill>.knowledge._all`. In multi-project Mode A/B, additionally load `knowledge[P]` + `skills.<current-skill>.knowledge[P]` for each resolved P. For every entry: base dir = `.ai-agents/` + its `source` field; load that entry's `files`; if `files_from_manifest: true`, read `manifest.yaml` in that dir and load entries with `auto_load: true`. Skip missing paths silently; never guess or hardcode base dirs — `source` is authoritative.
|
|
35
|
+
|
|
36
|
+
**Config** — apply `config.yaml` preferences for the whole session: `preferences.interaction_language` (chat/prompts/tables), `preferences.document_output_language` (files on disk), `preferences.output.no_emojis`, `preferences.output.data_format`, `preferences.context_routing.relevance_threshold`.
|
|
37
|
+
{{?has_preflight}}
|
|
38
|
+
|
|
39
|
+
**Pre-flight** — evaluate each check below against the loaded `session.yaml` / `project-context.yaml`. Levels: **WARN** = emit message, ask "Continue? (y/n)", default **y**; **BLOCK** / **REQUIRED** = emit and stop until satisfied; **INFO** = emit and proceed.
|
|
40
|
+
|
|
41
|
+
| # | Condition | Level | Message |
|
|
42
|
+
|---|-----------|-------|---------|
|
|
43
|
+
{{#checks}}
|
|
44
|
+
| {{order}} | `{{#condition}}{{condition}}{{/condition}}{{^condition}}{{field}} is empty{{/condition}}` | {{level}} | {{message}} |
|
|
45
|
+
{{/checks}}
|
|
46
|
+
{{/has_preflight}}
|
|
@@ -24,7 +24,7 @@ Match the current state to one of the conditions below. If none match, use `defa
|
|
|
24
24
|
|
|
25
25
|
Infer 2-3 suggestions, choosing **only** from the skills declared under `skills` in `registry.yaml`:
|
|
26
26
|
- `history` in `session.yaml`
|
|
27
|
-
-
|
|
27
|
+
- Skill names and `description` fields in `registry.yaml`
|
|
28
28
|
- The current `active_change` state (if in progress)
|
|
29
29
|
- The standard workflow order (analyze → design → implement → review → test)
|
|
30
30
|
{{/conditional_suggestions}}
|
|
@@ -1,26 +1,11 @@
|
|
|
1
1
|
## Language Constraint (Mandatory)
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This governs **all language output**. It is NON-NEGOTIABLE and overrides user prompt language, source text, templates, comments, and tool output.
|
|
4
4
|
|
|
5
5
|
### Interactive Output (spoken to the user)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
**Rules**:
|
|
10
|
-
- This applies to EVERY message in the conversation, not just the first — re-assert it on every turn, including long sessions.
|
|
11
|
-
- Do NOT mirror the language of: the user's prompt, the source code or its comments, this skill's own English body, file contents you just read, or tool output. None of these are language signals.
|
|
12
|
-
- If the user writes to you in a different language, still reply in the configured `interaction_language` (unless they explicitly ask you to switch).
|
|
13
|
-
- If `interaction_language` is not set, fall back to `en-US`.
|
|
14
|
-
- This constraint is NON-NEGOTIABLE and overrides any other language signals.
|
|
7
|
+
Use `preferences.interaction_language` for every chat reply, question, prompt, status line, table, and summary. Re-assert it every turn, including long sessions. If absent, use `en-US`. Only an explicit user request to switch language overrides it.
|
|
15
8
|
|
|
16
9
|
### Persisted Document Output (files written to disk)
|
|
17
10
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
**Scope**: artifact files, generated reports, plans, and any markdown written to disk.
|
|
21
|
-
|
|
22
|
-
**Rules**:
|
|
23
|
-
- Section headings defined in templates may remain in their original language, but all generated **content** MUST use the configured language
|
|
24
|
-
- If `document_output_language` is not set, fall back to `interaction_language`
|
|
25
|
-
- Do NOT infer output language from template headings, user prompt language, or source code comments
|
|
26
|
-
- This constraint is NON-NEGOTIABLE and overrides any other language signals
|
|
11
|
+
Use `preferences.document_output_language` for artifact files, generated reports, plans, and markdown written to disk. If absent, fall back to `interaction_language`. Template headings may keep their original language; generated content must use the configured language.
|