@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.
Files changed (94) hide show
  1. package/dist/cli.js +2 -2
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/update.d.ts +1 -1
  4. package/dist/commands/update.d.ts.map +1 -1
  5. package/dist/commands/update.js +18 -1
  6. package/dist/commands/update.js.map +1 -1
  7. package/dist/fs/core-manifest.d.ts +4 -3
  8. package/dist/fs/core-manifest.d.ts.map +1 -1
  9. package/dist/fs/core-manifest.js +5 -4
  10. package/dist/fs/core-manifest.js.map +1 -1
  11. package/dist/fs/materialize.d.ts +2 -0
  12. package/dist/fs/materialize.d.ts.map +1 -1
  13. package/dist/fs/materialize.js +6 -5
  14. package/dist/fs/materialize.js.map +1 -1
  15. package/dist/fs/registry-merge.d.ts +4 -3
  16. package/dist/fs/registry-merge.d.ts.map +1 -1
  17. package/dist/fs/registry-merge.js +5 -4
  18. package/dist/fs/registry-merge.js.map +1 -1
  19. package/dist/scripts/epic-update.cjs +235 -207
  20. package/dist/scripts/epic-update.md +57 -0
  21. package/dist/scripts/plan-update.cjs +224 -222
  22. package/dist/scripts/plan-update.md +68 -0
  23. package/dist/scripts/session-update.cjs +229 -210
  24. package/install-manifest.yaml +4 -0
  25. package/package.json +1 -1
  26. package/sources/defaults/config.yaml +5 -0
  27. package/sources/scripts/epic-update.js +73 -22
  28. package/sources/scripts/epic-update.md +57 -0
  29. package/sources/scripts/plan-update.js +56 -28
  30. package/sources/scripts/plan-update.md +68 -0
  31. package/sources/scripts/session-update.js +68 -24
  32. package/sources/sections/activation-protocol.md +46 -0
  33. package/sources/sections/footer-next-steps.md +1 -1
  34. package/sources/sections/language-constraint.md +3 -18
  35. package/sources/sections/output-format-constraint.md +6 -9
  36. package/sources/sections/role-header.md +1 -1
  37. package/sources/sections/script-usage-rule.md +32 -0
  38. package/sources/sections/session-update.md +30 -110
  39. package/sources/skills/mvt-analyze/business.md +1 -1
  40. package/sources/skills/mvt-analyze/manifest.yaml +13 -16
  41. package/sources/skills/mvt-analyze-code/business.md +3 -0
  42. package/sources/skills/mvt-analyze-code/manifest.yaml +11 -15
  43. package/sources/skills/mvt-bug-detect/manifest.yaml +3 -4
  44. package/sources/skills/mvt-check-context/business.md +2 -2
  45. package/sources/skills/mvt-check-context/manifest.yaml +3 -6
  46. package/sources/skills/mvt-cleanup/business.md +47 -11
  47. package/sources/skills/mvt-cleanup/manifest.yaml +12 -13
  48. package/sources/skills/mvt-config/business.md +45 -49
  49. package/sources/skills/mvt-config/manifest.yaml +21 -25
  50. package/sources/skills/mvt-create-skill/business.md +15 -11
  51. package/sources/skills/mvt-create-skill/manifest.yaml +6 -9
  52. package/sources/skills/mvt-decompose/business.md +21 -9
  53. package/sources/skills/mvt-decompose/manifest.yaml +15 -17
  54. package/sources/skills/mvt-design/business.md +35 -44
  55. package/sources/skills/mvt-design/manifest.yaml +13 -15
  56. package/sources/skills/mvt-fix/business.md +7 -1
  57. package/sources/skills/mvt-fix/manifest.yaml +9 -13
  58. package/sources/skills/mvt-help/business.md +20 -9
  59. package/sources/skills/mvt-help/manifest.yaml +7 -18
  60. package/sources/skills/mvt-implement/business.md +27 -34
  61. package/sources/skills/mvt-implement/manifest.yaml +12 -21
  62. package/sources/skills/mvt-init/manifest.yaml +10 -13
  63. package/sources/skills/mvt-manage-context/business.md +4 -2
  64. package/sources/skills/mvt-manage-context/manifest.yaml +3 -6
  65. package/sources/skills/mvt-plan-dev/business.md +20 -8
  66. package/sources/skills/mvt-plan-dev/manifest.yaml +13 -17
  67. package/sources/skills/mvt-quick-dev/business.md +1 -1
  68. package/sources/skills/mvt-quick-dev/manifest.yaml +3 -4
  69. package/sources/skills/mvt-refactor/business.md +1 -1
  70. package/sources/skills/mvt-refactor/manifest.yaml +3 -4
  71. package/sources/skills/mvt-resume/business.md +3 -3
  72. package/sources/skills/mvt-resume/manifest.yaml +10 -13
  73. package/sources/skills/mvt-review/business.md +12 -11
  74. package/sources/skills/mvt-review/manifest.yaml +17 -18
  75. package/sources/skills/mvt-status/business.md +12 -21
  76. package/sources/skills/mvt-status/manifest.yaml +7 -10
  77. package/sources/skills/mvt-sync-context/business.md +20 -18
  78. package/sources/skills/mvt-sync-context/manifest.yaml +11 -15
  79. package/sources/skills/mvt-template/business.md +5 -5
  80. package/sources/skills/mvt-template/manifest.yaml +3 -6
  81. package/sources/skills/mvt-test/business.md +11 -11
  82. package/sources/skills/mvt-test/manifest.yaml +15 -18
  83. package/sources/skills/mvt-update-plan/business.md +18 -29
  84. package/sources/skills/mvt-update-plan/manifest.yaml +18 -16
  85. package/sources/templates/analyze-output/body.md +41 -0
  86. package/sources/templates/decompose-output/body.md +36 -0
  87. package/sources/templates/design-output/body.md +48 -0
  88. package/sources/templates/implement-output/body.md +48 -3
  89. package/sources/templates/project-context/body.md +45 -0
  90. package/sources/templates/review-output/body.md +59 -0
  91. package/sources/templates/test-output/body.md +72 -0
  92. package/sources/sections/activation-load-config.md +0 -11
  93. package/sources/sections/activation-load-context.md +0 -59
  94. package/sources/sections/activation-preflight.md +0 -14
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uoyo/mvtt",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "My Virtual Tech Team - AI-guided prompt orchestration framework",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,3 +20,8 @@ preferences:
20
20
  history_limits:
21
21
  history: 10
22
22
  changes: 10
23
+
24
+ # Planning granularity for /mvt-plan-dev
25
+ # Options: coarse (fewer, larger tasks) | medium (balanced) | fine (more, smaller tasks)
26
+ planning:
27
+ granularity: medium
@@ -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
- * node .ai-agents/scripts/epic-update.cjs \
16
- * --epic <path-to-epic.yaml> \
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
- process.stdout.write(JSON.stringify({ ok: true, ...result }) + "\n");
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
- * node .ai-agents/scripts/plan-update.cjs \
24
- * --plan <path-to-plan.yaml> \
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
- if (!args.status || args.status === true) return ERRORS.MISSING_STATUS();
130
- if (!VALID_STATUSES.includes(args.status)) return ERRORS.INVALID_STATUS(args.status);
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
- task.status = args.status;
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 first transition to done, else null.
168
- if (args.status === "done" && !task.completed_at) {
169
- task.completed_at = now;
170
- } else if (args.status !== "done") {
171
- task.completed_at = null;
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: args.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
- plan.current_tasks = { default: plan.current_task };
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
- const { warning, project_switch: switchNotif } = recomputeCurrentTasks(plan, args.task, projectList);
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
- * node .ai-agents/scripts/session-update.cjs \
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 (§9.1.1, ADR-10)
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
- - `category` and `description` of each skill in `registry.yaml`
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 constraint governs the language of **everything** this skill produces. It has two independent scopes interactive output (what you say to the user) and persisted document output (what you write to disk). Both are NON-NEGOTIABLE and override any other language signals.
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
- All interactive output chat replies, questions, prompts, status lines, tables, and summaries shown in the conversation MUST be written in the language specified by `preferences.interaction_language` from config.yaml.
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
- All persisted document output (files written to disk) MUST be written in the language specified by `preferences.document_output_language` from config.yaml.
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.