@forwardimpact/pathway 0.1.0 → 0.3.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 (140) hide show
  1. package/app/commands/agent.js +119 -31
  2. package/app/commands/command-factory.js +3 -3
  3. package/app/commands/interview.js +14 -7
  4. package/app/commands/job.js +52 -33
  5. package/app/commands/progress.js +14 -7
  6. package/app/commands/serve.js +5 -0
  7. package/app/commands/stage.js +0 -10
  8. package/app/commands/track.js +5 -8
  9. package/app/components/builder.js +117 -30
  10. package/app/css/components/surfaces.css +16 -0
  11. package/app/formatters/agent/profile.js +30 -115
  12. package/app/formatters/agent/skill.js +23 -44
  13. package/app/formatters/behaviour/dom.js +3 -0
  14. package/app/formatters/behaviour/microdata.js +106 -0
  15. package/app/formatters/discipline/dom.js +28 -1
  16. package/app/formatters/discipline/microdata.js +117 -0
  17. package/app/formatters/discipline/shared.js +49 -8
  18. package/app/formatters/driver/dom.js +3 -0
  19. package/app/formatters/driver/microdata.js +91 -0
  20. package/app/formatters/grade/dom.js +5 -4
  21. package/app/formatters/grade/microdata.js +151 -0
  22. package/app/formatters/index.js +32 -1
  23. package/app/formatters/interview/shared.js +13 -8
  24. package/app/formatters/job/description.js +70 -81
  25. package/app/formatters/job/dom.js +40 -113
  26. package/app/formatters/job/markdown.js +17 -13
  27. package/app/formatters/json-ld.js +242 -0
  28. package/app/formatters/microdata-shared.js +184 -0
  29. package/app/formatters/progress/shared.js +14 -11
  30. package/app/formatters/shared.js +7 -2
  31. package/app/formatters/skill/dom.js +3 -0
  32. package/app/formatters/skill/microdata.js +151 -0
  33. package/app/formatters/stage/dom.js +3 -18
  34. package/app/formatters/stage/microdata.js +110 -0
  35. package/app/formatters/stage/shared.js +0 -27
  36. package/app/formatters/track/dom.js +5 -30
  37. package/app/formatters/track/markdown.js +2 -25
  38. package/app/formatters/track/microdata.js +111 -0
  39. package/app/formatters/track/shared.js +6 -58
  40. package/app/handout-main.js +26 -12
  41. package/app/handout.html +7 -0
  42. package/app/index.html +11 -0
  43. package/app/lib/card-mappers.js +17 -12
  44. package/app/lib/form-controls.js +64 -1
  45. package/app/lib/job-cache.js +12 -9
  46. package/app/lib/render.js +8 -1
  47. package/app/lib/template-loader.js +75 -0
  48. package/app/lib/yaml-loader.js +25 -8
  49. package/app/main.js +8 -4
  50. package/app/model/agent.js +158 -130
  51. package/app/model/checklist.js +57 -91
  52. package/app/model/derivation.js +135 -68
  53. package/app/model/index-generator.js +1 -7
  54. package/app/model/job.js +19 -13
  55. package/app/model/levels.js +20 -12
  56. package/app/model/loader.js +41 -17
  57. package/app/model/matching.js +33 -3
  58. package/app/model/profile.js +38 -45
  59. package/app/model/schema-validation.js +438 -0
  60. package/app/model/validation.js +747 -68
  61. package/app/pages/agent-builder.js +125 -28
  62. package/app/pages/assessment-results.js +10 -4
  63. package/app/pages/discipline.js +36 -6
  64. package/app/pages/driver.js +9 -47
  65. package/app/pages/interview-builder.js +3 -1
  66. package/app/pages/interview.js +15 -4
  67. package/app/pages/job-builder.js +4 -1
  68. package/app/pages/job.js +43 -8
  69. package/app/pages/landing.js +10 -10
  70. package/app/pages/progress-builder.js +3 -1
  71. package/app/pages/progress.js +78 -26
  72. package/app/pages/self-assessment.js +3 -3
  73. package/app/pages/stage.js +3 -126
  74. package/app/slide-main.js +45 -17
  75. package/app/slides/index.js +3 -1
  76. package/app/slides/overview.js +40 -4
  77. package/app/slides/progress.js +4 -2
  78. package/app/slides.html +7 -0
  79. package/bin/pathway.js +28 -75
  80. package/examples/agents/.claude/skills/architecture-design/SKILL.md +58 -16
  81. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +59 -18
  82. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +58 -17
  83. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +64 -18
  84. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +59 -15
  85. package/examples/agents/.claude/skills/sre-practices/SKILL.md +64 -18
  86. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +58 -17
  87. package/examples/agents/.github/agents/se-platform-code.agent.md +39 -88
  88. package/examples/agents/.github/agents/se-platform-plan.agent.md +41 -88
  89. package/examples/agents/.github/agents/se-platform-review.agent.md +38 -15
  90. package/examples/agents/.vscode/settings.json +1 -1
  91. package/examples/behaviours/outcome_ownership.yaml +1 -2
  92. package/examples/behaviours/polymathic_knowledge.yaml +1 -2
  93. package/examples/behaviours/precise_communication.yaml +1 -2
  94. package/examples/behaviours/relentless_curiosity.yaml +1 -2
  95. package/examples/behaviours/systems_thinking.yaml +1 -2
  96. package/examples/capabilities/business.yaml +80 -142
  97. package/examples/capabilities/delivery.yaml +155 -219
  98. package/examples/capabilities/people.yaml +2 -34
  99. package/examples/capabilities/reliability.yaml +161 -80
  100. package/examples/capabilities/scale.yaml +234 -252
  101. package/examples/copilot-setup-steps.yaml +25 -0
  102. package/examples/devcontainer.yaml +21 -0
  103. package/examples/disciplines/_index.yaml +1 -0
  104. package/examples/disciplines/data_engineering.yaml +14 -12
  105. package/examples/disciplines/engineering_management.yaml +63 -0
  106. package/examples/disciplines/software_engineering.yaml +14 -12
  107. package/examples/drivers.yaml +1 -4
  108. package/examples/framework.yaml +1 -2
  109. package/examples/grades.yaml +14 -15
  110. package/examples/questions/behaviours/outcome_ownership.yaml +1 -2
  111. package/examples/questions/behaviours/polymathic_knowledge.yaml +1 -2
  112. package/examples/questions/behaviours/precise_communication.yaml +1 -2
  113. package/examples/questions/behaviours/relentless_curiosity.yaml +1 -2
  114. package/examples/questions/behaviours/systems_thinking.yaml +1 -2
  115. package/examples/questions/skills/architecture_design.yaml +1 -2
  116. package/examples/questions/skills/cloud_platforms.yaml +1 -2
  117. package/examples/questions/skills/code_quality.yaml +1 -2
  118. package/examples/questions/skills/data_modeling.yaml +1 -2
  119. package/examples/questions/skills/devops.yaml +1 -2
  120. package/examples/questions/skills/full_stack_development.yaml +1 -2
  121. package/examples/questions/skills/sre_practices.yaml +1 -2
  122. package/examples/questions/skills/stakeholder_management.yaml +1 -2
  123. package/examples/questions/skills/team_collaboration.yaml +1 -2
  124. package/examples/questions/skills/technical_writing.yaml +1 -2
  125. package/examples/self-assessments.yaml +1 -3
  126. package/examples/stages.yaml +101 -46
  127. package/examples/tracks/_index.yaml +0 -1
  128. package/examples/tracks/platform.yaml +8 -13
  129. package/examples/tracks/sre.yaml +8 -18
  130. package/examples/vscode-settings.yaml +2 -7
  131. package/package.json +9 -3
  132. package/templates/agent.template.md +65 -0
  133. package/templates/job.template.md +47 -0
  134. package/templates/skill.template.md +28 -0
  135. package/examples/agents/.claude/skills/data-modeling/SKILL.md +0 -99
  136. package/examples/agents/.claude/skills/developer-experience/SKILL.md +0 -99
  137. package/examples/agents/.claude/skills/knowledge-management/SKILL.md +0 -100
  138. package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +0 -102
  139. package/examples/agents/.claude/skills/technical-writing/SKILL.md +0 -129
  140. package/examples/tracks/manager.yaml +0 -53
@@ -56,9 +56,10 @@ function createWarning(type, message, path) {
56
56
  * Validate that a skill has required properties
57
57
  * @param {import('./levels.js').Skill} skill - Skill to validate
58
58
  * @param {number} index - Index in the skills array
59
+ * @param {string[]} [requiredStageIds] - Stage IDs that must be present in agent skills
59
60
  * @returns {{errors: Array, warnings: Array}}
60
61
  */
61
- function validateSkill(skill, index) {
62
+ function validateSkill(skill, index, requiredStageIds = []) {
62
63
  const errors = [];
63
64
  const warnings = [];
64
65
  const path = `skills[${index}]`;
@@ -108,6 +109,192 @@ function validateSkill(skill, index) {
108
109
  );
109
110
  }
110
111
 
112
+ // Validate agent section if present
113
+ if (skill.agent) {
114
+ const agentPath = `${path}.agent`;
115
+ if (!skill.agent.name) {
116
+ errors.push(
117
+ createError(
118
+ "MISSING_REQUIRED",
119
+ "Skill agent section missing name",
120
+ `${agentPath}.name`,
121
+ ),
122
+ );
123
+ }
124
+ if (!skill.agent.description) {
125
+ errors.push(
126
+ createError(
127
+ "MISSING_REQUIRED",
128
+ "Skill agent section missing description",
129
+ `${agentPath}.description`,
130
+ ),
131
+ );
132
+ }
133
+
134
+ // Validate stages (required for agent skills)
135
+ if (!skill.agent.stages) {
136
+ errors.push(
137
+ createError(
138
+ "MISSING_REQUIRED",
139
+ "Skill agent section missing stages",
140
+ `${agentPath}.stages`,
141
+ ),
142
+ );
143
+ } else if (typeof skill.agent.stages !== "object") {
144
+ errors.push(
145
+ createError(
146
+ "INVALID_VALUE",
147
+ "Skill agent stages must be an object",
148
+ `${agentPath}.stages`,
149
+ skill.agent.stages,
150
+ ),
151
+ );
152
+ } else {
153
+ // Validate each stage
154
+ const validStageIds = Object.values(Stage);
155
+ for (const [stageId, stageData] of Object.entries(skill.agent.stages)) {
156
+ if (!validStageIds.includes(stageId)) {
157
+ errors.push(
158
+ createError(
159
+ "INVALID_VALUE",
160
+ `Invalid stage ID: ${stageId}. Must be one of: ${validStageIds.join(", ")}`,
161
+ `${agentPath}.stages.${stageId}`,
162
+ stageId,
163
+ ),
164
+ );
165
+ continue;
166
+ }
167
+ const stagePath = `${agentPath}.stages.${stageId}`;
168
+ // focus is required
169
+ if (!stageData.focus) {
170
+ errors.push(
171
+ createError(
172
+ "MISSING_REQUIRED",
173
+ `Stage ${stageId} missing focus`,
174
+ `${stagePath}.focus`,
175
+ ),
176
+ );
177
+ } else if (typeof stageData.focus !== "string") {
178
+ errors.push(
179
+ createError(
180
+ "INVALID_VALUE",
181
+ `Stage ${stageId} focus must be a string`,
182
+ `${stagePath}.focus`,
183
+ stageData.focus,
184
+ ),
185
+ );
186
+ }
187
+ // activities is required and must be an array
188
+ if (!stageData.activities) {
189
+ errors.push(
190
+ createError(
191
+ "MISSING_REQUIRED",
192
+ `Stage ${stageId} missing activities`,
193
+ `${stagePath}.activities`,
194
+ ),
195
+ );
196
+ } else if (!Array.isArray(stageData.activities)) {
197
+ errors.push(
198
+ createError(
199
+ "INVALID_VALUE",
200
+ `Stage ${stageId} activities must be an array`,
201
+ `${stagePath}.activities`,
202
+ stageData.activities,
203
+ ),
204
+ );
205
+ }
206
+ // ready is required and must be an array (these become checklist items)
207
+ if (!stageData.ready) {
208
+ errors.push(
209
+ createError(
210
+ "MISSING_REQUIRED",
211
+ `Stage ${stageId} missing ready criteria`,
212
+ `${stagePath}.ready`,
213
+ ),
214
+ );
215
+ } else if (!Array.isArray(stageData.ready)) {
216
+ errors.push(
217
+ createError(
218
+ "INVALID_VALUE",
219
+ `Stage ${stageId} ready must be an array`,
220
+ `${stagePath}.ready`,
221
+ stageData.ready,
222
+ ),
223
+ );
224
+ }
225
+ }
226
+
227
+ // Check that all required stages are present
228
+ if (requiredStageIds.length > 0) {
229
+ const presentStageIds = Object.keys(skill.agent.stages);
230
+ for (const requiredStageId of requiredStageIds) {
231
+ if (!presentStageIds.includes(requiredStageId)) {
232
+ errors.push(
233
+ createError(
234
+ "MISSING_REQUIRED",
235
+ `Skill agent missing required stage: ${requiredStageId}`,
236
+ `${agentPath}.stages.${requiredStageId}`,
237
+ ),
238
+ );
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ // reference is optional but should be a string if present
245
+ if (
246
+ skill.agent.reference !== undefined &&
247
+ typeof skill.agent.reference !== "string"
248
+ ) {
249
+ errors.push(
250
+ createError(
251
+ "INVALID_VALUE",
252
+ "Skill agent reference must be a string",
253
+ `${agentPath}.reference`,
254
+ skill.agent.reference,
255
+ ),
256
+ );
257
+ }
258
+
259
+ // Error if old fields are still present
260
+ if (skill.agent.body !== undefined) {
261
+ errors.push(
262
+ createError(
263
+ "INVALID_FIELD",
264
+ "Skill agent 'body' field is not supported. Use stages instead.",
265
+ `${agentPath}.body`,
266
+ ),
267
+ );
268
+ }
269
+ if (skill.agent.applicability !== undefined) {
270
+ errors.push(
271
+ createError(
272
+ "INVALID_FIELD",
273
+ "Skill agent 'applicability' field is not supported. Use stages instead.",
274
+ `${agentPath}.applicability`,
275
+ ),
276
+ );
277
+ }
278
+ if (skill.agent.guidance !== undefined) {
279
+ errors.push(
280
+ createError(
281
+ "INVALID_FIELD",
282
+ "Skill agent 'guidance' field is not supported. Use stages instead.",
283
+ `${agentPath}.guidance`,
284
+ ),
285
+ );
286
+ }
287
+ if (skill.agent.verificationCriteria !== undefined) {
288
+ errors.push(
289
+ createError(
290
+ "INVALID_FIELD",
291
+ "Skill agent 'verificationCriteria' field is not supported. Use stages.{stage}.ready instead.",
292
+ `${agentPath}.verificationCriteria`,
293
+ ),
294
+ );
295
+ }
296
+ }
297
+
111
298
  return { errors, warnings };
112
299
  }
113
300
 
@@ -147,6 +334,51 @@ function validateBehaviour(behaviour, index) {
147
334
  );
148
335
  }
149
336
 
337
+ // Validate agent section if present
338
+ if (behaviour.agent) {
339
+ const agentPath = `${path}.agent`;
340
+
341
+ // title is required for agent behaviours
342
+ if (!behaviour.agent.title) {
343
+ errors.push(
344
+ createError(
345
+ "MISSING_REQUIRED",
346
+ "Behaviour agent section missing title",
347
+ `${agentPath}.title`,
348
+ ),
349
+ );
350
+ } else if (typeof behaviour.agent.title !== "string") {
351
+ errors.push(
352
+ createError(
353
+ "INVALID_VALUE",
354
+ "Behaviour agent title must be a string",
355
+ `${agentPath}.title`,
356
+ behaviour.agent.title,
357
+ ),
358
+ );
359
+ }
360
+
361
+ // workingStyle is required for agent behaviours
362
+ if (!behaviour.agent.workingStyle) {
363
+ errors.push(
364
+ createError(
365
+ "MISSING_REQUIRED",
366
+ "Behaviour agent section missing workingStyle",
367
+ `${agentPath}.workingStyle`,
368
+ ),
369
+ );
370
+ } else if (typeof behaviour.agent.workingStyle !== "string") {
371
+ errors.push(
372
+ createError(
373
+ "INVALID_VALUE",
374
+ "Behaviour agent workingStyle must be a string",
375
+ `${agentPath}.workingStyle`,
376
+ behaviour.agent.workingStyle,
377
+ ),
378
+ );
379
+ }
380
+ }
381
+
150
382
  return { errors, warnings };
151
383
  }
152
384
 
@@ -222,9 +454,18 @@ function validateDriver(driver, index, skillIds, behaviourIds) {
222
454
  * @param {number} index - Index in the disciplines array
223
455
  * @param {Set<string>} skillIds - Set of valid skill IDs
224
456
  * @param {Set<string>} behaviourIds - Set of valid behaviour IDs
457
+ * @param {Set<string>} trackIds - Set of valid track IDs
458
+ * @param {Set<string>} gradeIds - Set of valid grade IDs
225
459
  * @returns {{errors: Array, warnings: Array}}
226
460
  */
227
- function validateDiscipline(discipline, index, skillIds, behaviourIds) {
461
+ function validateDiscipline(
462
+ discipline,
463
+ index,
464
+ skillIds,
465
+ behaviourIds,
466
+ trackIds,
467
+ gradeIds,
468
+ ) {
228
469
  const errors = [];
229
470
  const warnings = [];
230
471
  const path = `disciplines[${index}]`;
@@ -249,6 +490,77 @@ function validateDiscipline(discipline, index, skillIds, behaviourIds) {
249
490
  );
250
491
  }
251
492
 
493
+ // Validate validTracks (REQUIRED - must be an array)
494
+ // - null in array = allow trackless (generalist)
495
+ // - string values = specific track IDs
496
+ // - empty array = no valid combinations (discipline cannot be used)
497
+ if (!Array.isArray(discipline.validTracks)) {
498
+ errors.push(
499
+ createError(
500
+ "MISSING_REQUIRED",
501
+ `Discipline "${discipline.id}" missing required validTracks array`,
502
+ `${path}.validTracks`,
503
+ ),
504
+ );
505
+ } else {
506
+ discipline.validTracks.forEach((trackId, i) => {
507
+ // null means "allow trackless" - skip validation
508
+ if (trackId === null) return;
509
+ if (!trackIds.has(trackId)) {
510
+ errors.push(
511
+ createError(
512
+ "INVALID_REFERENCE",
513
+ `Discipline "${discipline.id}" references non-existent track: ${trackId}`,
514
+ `${path}.validTracks[${i}]`,
515
+ trackId,
516
+ ),
517
+ );
518
+ }
519
+ });
520
+ }
521
+
522
+ // Validate minGrade if specified
523
+ if (discipline.minGrade) {
524
+ if (!gradeIds.has(discipline.minGrade)) {
525
+ errors.push(
526
+ createError(
527
+ "INVALID_REFERENCE",
528
+ `Discipline "${discipline.id}" references non-existent grade: ${discipline.minGrade}`,
529
+ `${path}.minGrade`,
530
+ discipline.minGrade,
531
+ ),
532
+ );
533
+ }
534
+ }
535
+
536
+ // Validate isManagement/isProfessional booleans (optional)
537
+ if (
538
+ discipline.isManagement !== undefined &&
539
+ typeof discipline.isManagement !== "boolean"
540
+ ) {
541
+ errors.push(
542
+ createError(
543
+ "INVALID_VALUE",
544
+ `Discipline "${discipline.id}" has invalid isManagement value: ${discipline.isManagement} (must be boolean)`,
545
+ `${path}.isManagement`,
546
+ discipline.isManagement,
547
+ ),
548
+ );
549
+ }
550
+ if (
551
+ discipline.isProfessional !== undefined &&
552
+ typeof discipline.isProfessional !== "boolean"
553
+ ) {
554
+ errors.push(
555
+ createError(
556
+ "INVALID_VALUE",
557
+ `Discipline "${discipline.id}" has invalid isProfessional value: ${discipline.isProfessional} (must be boolean)`,
558
+ `${path}.isProfessional`,
559
+ discipline.isProfessional,
560
+ ),
561
+ );
562
+ }
563
+
252
564
  // Validate core skills
253
565
  if (!discipline.coreSkills || discipline.coreSkills.length === 0) {
254
566
  errors.push(
@@ -333,6 +645,126 @@ function validateDiscipline(discipline, index, skillIds, behaviourIds) {
333
645
  );
334
646
  }
335
647
 
648
+ // Validate agent section if present
649
+ if (discipline.agent) {
650
+ const agentPath = `${path}.agent`;
651
+
652
+ // Required: identity
653
+ if (!discipline.agent.identity) {
654
+ errors.push(
655
+ createError(
656
+ "MISSING_REQUIRED",
657
+ "Discipline agent section missing identity",
658
+ `${agentPath}.identity`,
659
+ ),
660
+ );
661
+ } else if (typeof discipline.agent.identity !== "string") {
662
+ errors.push(
663
+ createError(
664
+ "INVALID_VALUE",
665
+ "Discipline agent identity must be a string",
666
+ `${agentPath}.identity`,
667
+ discipline.agent.identity,
668
+ ),
669
+ );
670
+ }
671
+
672
+ // Optional: priority (string)
673
+ if (
674
+ discipline.agent.priority !== undefined &&
675
+ typeof discipline.agent.priority !== "string"
676
+ ) {
677
+ errors.push(
678
+ createError(
679
+ "INVALID_VALUE",
680
+ "Discipline agent priority must be a string",
681
+ `${agentPath}.priority`,
682
+ discipline.agent.priority,
683
+ ),
684
+ );
685
+ }
686
+
687
+ // Optional: beforeMakingChanges (array of strings)
688
+ if (discipline.agent.beforeMakingChanges !== undefined) {
689
+ if (!Array.isArray(discipline.agent.beforeMakingChanges)) {
690
+ errors.push(
691
+ createError(
692
+ "INVALID_VALUE",
693
+ "Discipline agent beforeMakingChanges must be an array",
694
+ `${agentPath}.beforeMakingChanges`,
695
+ discipline.agent.beforeMakingChanges,
696
+ ),
697
+ );
698
+ } else {
699
+ discipline.agent.beforeMakingChanges.forEach((item, i) => {
700
+ if (typeof item !== "string") {
701
+ errors.push(
702
+ createError(
703
+ "INVALID_VALUE",
704
+ "Discipline agent beforeMakingChanges items must be strings",
705
+ `${agentPath}.beforeMakingChanges[${i}]`,
706
+ item,
707
+ ),
708
+ );
709
+ }
710
+ });
711
+ }
712
+ }
713
+
714
+ // Optional: delegation (string)
715
+ if (
716
+ discipline.agent.delegation !== undefined &&
717
+ typeof discipline.agent.delegation !== "string"
718
+ ) {
719
+ errors.push(
720
+ createError(
721
+ "INVALID_VALUE",
722
+ "Discipline agent delegation must be a string",
723
+ `${agentPath}.delegation`,
724
+ discipline.agent.delegation,
725
+ ),
726
+ );
727
+ }
728
+
729
+ // Optional: constraints (array of strings)
730
+ if (discipline.agent.constraints !== undefined) {
731
+ if (!Array.isArray(discipline.agent.constraints)) {
732
+ errors.push(
733
+ createError(
734
+ "INVALID_VALUE",
735
+ "Discipline agent constraints must be an array",
736
+ `${agentPath}.constraints`,
737
+ discipline.agent.constraints,
738
+ ),
739
+ );
740
+ } else {
741
+ discipline.agent.constraints.forEach((item, i) => {
742
+ if (typeof item !== "string") {
743
+ errors.push(
744
+ createError(
745
+ "INVALID_VALUE",
746
+ "Discipline agent constraints items must be strings",
747
+ `${agentPath}.constraints[${i}]`,
748
+ item,
749
+ ),
750
+ );
751
+ }
752
+ });
753
+ }
754
+ }
755
+
756
+ // Error if old 'coreInstructions' field is still present
757
+ if (discipline.agent.coreInstructions !== undefined) {
758
+ errors.push(
759
+ createError(
760
+ "INVALID_FIELD",
761
+ "Discipline agent 'coreInstructions' field is not supported. Use identity, priority, beforeMakingChanges, and delegation instead.",
762
+ `${agentPath}.coreInstructions`,
763
+ ),
764
+ );
765
+ }
766
+ }
767
+
336
768
  return { errors, warnings };
337
769
  }
338
770
 
@@ -357,7 +789,7 @@ function getAllDisciplineSkillIds(disciplines) {
357
789
  * @param {number} index - Index in the tracks array
358
790
  * @param {Set<string>} disciplineSkillIds - Set of skill IDs used in any discipline
359
791
  * @param {Set<string>} behaviourIds - Set of valid behaviour IDs
360
- * @param {Set<string>} disciplineIds - Set of valid discipline IDs
792
+ * @param {Set<string>} gradeIds - Set of valid grade IDs
361
793
  * @returns {{errors: Array, warnings: Array}}
362
794
  */
363
795
  function validateTrack(
@@ -365,7 +797,6 @@ function validateTrack(
365
797
  index,
366
798
  disciplineSkillIds,
367
799
  behaviourIds,
368
- disciplineIds,
369
800
  gradeIds,
370
801
  ) {
371
802
  const errors = [];
@@ -379,34 +810,6 @@ function validateTrack(
379
810
  );
380
811
  }
381
812
 
382
- // Validate isProfessional/isManagement booleans (optional, default to isProfessional: true)
383
- if (
384
- track.isProfessional !== undefined &&
385
- typeof track.isProfessional !== "boolean"
386
- ) {
387
- errors.push(
388
- createError(
389
- "INVALID_VALUE",
390
- `Track "${track.id}" has invalid isProfessional value: ${track.isProfessional} (must be boolean)`,
391
- `${path}.isProfessional`,
392
- track.isProfessional,
393
- ),
394
- );
395
- }
396
- if (
397
- track.isManagement !== undefined &&
398
- typeof track.isManagement !== "boolean"
399
- ) {
400
- errors.push(
401
- createError(
402
- "INVALID_VALUE",
403
- `Track "${track.id}" has invalid isManagement value: ${track.isManagement} (must be boolean)`,
404
- `${path}.isManagement`,
405
- track.isManagement,
406
- ),
407
- );
408
- }
409
-
410
813
  // Validate skill modifiers - must be capabilities only (not individual skill IDs)
411
814
  if (track.skillModifiers) {
412
815
  Object.entries(track.skillModifiers).forEach(([key, modifier]) => {
@@ -462,22 +865,6 @@ function validateTrack(
462
865
  );
463
866
  }
464
867
 
465
- // Validate validDisciplines if specified
466
- if (track.validDisciplines) {
467
- track.validDisciplines.forEach((disciplineId, i) => {
468
- if (!disciplineIds.has(disciplineId)) {
469
- errors.push(
470
- createError(
471
- "INVALID_REFERENCE",
472
- `Track "${track.id}" references non-existent discipline: ${disciplineId}`,
473
- `${path}.validDisciplines[${i}]`,
474
- disciplineId,
475
- ),
476
- );
477
- }
478
- });
479
- }
480
-
481
868
  // Validate minGrade if specified
482
869
  if (track.minGrade) {
483
870
  if (!gradeIds.has(track.minGrade)) {
@@ -537,6 +924,106 @@ function validateTrack(
537
924
  }
538
925
  }
539
926
 
927
+ // Validate agent section if present
928
+ if (track.agent) {
929
+ const agentPath = `${path}.agent`;
930
+
931
+ // Optional: identity (string) - if provided, overrides discipline identity
932
+ if (
933
+ track.agent.identity !== undefined &&
934
+ typeof track.agent.identity !== "string"
935
+ ) {
936
+ errors.push(
937
+ createError(
938
+ "INVALID_VALUE",
939
+ "Track agent identity must be a string",
940
+ `${agentPath}.identity`,
941
+ track.agent.identity,
942
+ ),
943
+ );
944
+ }
945
+
946
+ // Optional: priority (string)
947
+ if (
948
+ track.agent.priority !== undefined &&
949
+ typeof track.agent.priority !== "string"
950
+ ) {
951
+ errors.push(
952
+ createError(
953
+ "INVALID_VALUE",
954
+ "Track agent priority must be a string",
955
+ `${agentPath}.priority`,
956
+ track.agent.priority,
957
+ ),
958
+ );
959
+ }
960
+
961
+ // Optional: beforeMakingChanges (array of strings)
962
+ if (track.agent.beforeMakingChanges !== undefined) {
963
+ if (!Array.isArray(track.agent.beforeMakingChanges)) {
964
+ errors.push(
965
+ createError(
966
+ "INVALID_VALUE",
967
+ "Track agent beforeMakingChanges must be an array",
968
+ `${agentPath}.beforeMakingChanges`,
969
+ track.agent.beforeMakingChanges,
970
+ ),
971
+ );
972
+ } else {
973
+ track.agent.beforeMakingChanges.forEach((item, i) => {
974
+ if (typeof item !== "string") {
975
+ errors.push(
976
+ createError(
977
+ "INVALID_VALUE",
978
+ "Track agent beforeMakingChanges items must be strings",
979
+ `${agentPath}.beforeMakingChanges[${i}]`,
980
+ item,
981
+ ),
982
+ );
983
+ }
984
+ });
985
+ }
986
+ }
987
+
988
+ // Optional: constraints (array of strings)
989
+ if (track.agent.constraints !== undefined) {
990
+ if (!Array.isArray(track.agent.constraints)) {
991
+ errors.push(
992
+ createError(
993
+ "INVALID_VALUE",
994
+ "Track agent constraints must be an array",
995
+ `${agentPath}.constraints`,
996
+ track.agent.constraints,
997
+ ),
998
+ );
999
+ } else {
1000
+ track.agent.constraints.forEach((item, i) => {
1001
+ if (typeof item !== "string") {
1002
+ errors.push(
1003
+ createError(
1004
+ "INVALID_VALUE",
1005
+ "Track agent constraints items must be strings",
1006
+ `${agentPath}.constraints[${i}]`,
1007
+ item,
1008
+ ),
1009
+ );
1010
+ }
1011
+ });
1012
+ }
1013
+ }
1014
+
1015
+ // Error if old 'coreInstructions' field is still present
1016
+ if (track.agent.coreInstructions !== undefined) {
1017
+ errors.push(
1018
+ createError(
1019
+ "INVALID_FIELD",
1020
+ "Track agent 'coreInstructions' field is not supported. Use identity, priority, beforeMakingChanges, and constraints instead.",
1021
+ `${agentPath}.coreInstructions`,
1022
+ ),
1023
+ );
1024
+ }
1025
+ }
1026
+
540
1027
  return { errors, warnings };
541
1028
  }
542
1029
 
@@ -792,18 +1279,6 @@ function validateStage(stage, index) {
792
1279
  );
793
1280
  }
794
1281
 
795
- // Mode is now inferred from availableTools - no longer required
796
- // Validate availableTools array
797
- if (!stage.availableTools || !Array.isArray(stage.availableTools)) {
798
- errors.push(
799
- createError(
800
- "MISSING_REQUIRED",
801
- "Stage missing availableTools array",
802
- `${path}.availableTools`,
803
- ),
804
- );
805
- }
806
-
807
1282
  if (!stage.handoffs || !Array.isArray(stage.handoffs)) {
808
1283
  warnings.push(
809
1284
  createWarning(
@@ -974,6 +1449,9 @@ export function validateAllData({
974
1449
  const behaviourIds = new Set((behaviours || []).map((b) => b.id));
975
1450
  const capabilityIds = new Set((capabilities || []).map((c) => c.id));
976
1451
 
1452
+ // Extract stage IDs for agent skill validation
1453
+ const requiredStageIds = (stages || []).map((s) => s.id);
1454
+
977
1455
  // Validate skills
978
1456
  if (!skills || skills.length === 0) {
979
1457
  allErrors.push(
@@ -981,7 +1459,11 @@ export function validateAllData({
981
1459
  );
982
1460
  } else {
983
1461
  skills.forEach((skill, index) => {
984
- const { errors, warnings } = validateSkill(skill, index);
1462
+ const { errors, warnings } = validateSkill(
1463
+ skill,
1464
+ index,
1465
+ requiredStageIds,
1466
+ );
985
1467
  allErrors.push(...errors);
986
1468
  allWarnings.push(...warnings);
987
1469
  });
@@ -1036,6 +1518,12 @@ export function validateAllData({
1036
1518
  });
1037
1519
  }
1038
1520
 
1521
+ // Get track IDs for discipline validation
1522
+ const trackIdSet = new Set((tracks || []).map((t) => t.id));
1523
+
1524
+ // Get grade IDs for discipline and track validation
1525
+ const gradeIdSet = new Set((grades || []).map((g) => g.id));
1526
+
1039
1527
  // Validate disciplines
1040
1528
  if (!disciplines || disciplines.length === 0) {
1041
1529
  allErrors.push(
@@ -1048,6 +1536,8 @@ export function validateAllData({
1048
1536
  index,
1049
1537
  skillIds,
1050
1538
  behaviourIds,
1539
+ trackIdSet,
1540
+ gradeIdSet,
1051
1541
  );
1052
1542
  allErrors.push(...errors);
1053
1543
  allWarnings.push(...warnings);
@@ -1075,12 +1565,6 @@ export function validateAllData({
1075
1565
  // Get all skill IDs from disciplines for track validation
1076
1566
  const disciplineSkillIds = getAllDisciplineSkillIds(disciplines || []);
1077
1567
 
1078
- // Get discipline IDs for track validation
1079
- const disciplineIdSet = new Set((disciplines || []).map((d) => d.id));
1080
-
1081
- // Get grade IDs for track validation
1082
- const gradeIdSet = new Set((grades || []).map((g) => g.id));
1083
-
1084
1568
  // Validate tracks
1085
1569
  if (!tracks || tracks.length === 0) {
1086
1570
  allErrors.push(
@@ -1093,7 +1577,6 @@ export function validateAllData({
1093
1577
  index,
1094
1578
  disciplineSkillIds,
1095
1579
  behaviourIds,
1096
- disciplineIdSet,
1097
1580
  gradeIdSet,
1098
1581
  );
1099
1582
  allErrors.push(...errors);
@@ -1383,3 +1866,199 @@ export function validateQuestionBank(questionBank, skills, behaviours) {
1383
1866
 
1384
1867
  return createValidationResult(errors.length === 0, errors, warnings);
1385
1868
  }
1869
+
1870
+ /**
1871
+ * Validate agent-specific data comprehensively
1872
+ * This validates cross-references between human and agent definitions
1873
+ * @param {Object} params - Validation parameters
1874
+ * @param {Object} params.humanData - Human data (disciplines, tracks, skills, behaviours, stages)
1875
+ * @param {Object} params.agentData - Agent-specific data (disciplines, tracks, behaviours with agent sections)
1876
+ * @returns {import('./levels.js').ValidationResult}
1877
+ */
1878
+ export function validateAgentData({ humanData, agentData }) {
1879
+ const errors = [];
1880
+ const warnings = [];
1881
+
1882
+ const humanDisciplineIds = new Set(
1883
+ (humanData.disciplines || []).map((d) => d.id),
1884
+ );
1885
+ const humanTrackIds = new Set((humanData.tracks || []).map((t) => t.id));
1886
+ const humanBehaviourIds = new Set(
1887
+ (humanData.behaviours || []).map((b) => b.id),
1888
+ );
1889
+ const stageIds = new Set((humanData.stages || []).map((s) => s.id));
1890
+
1891
+ // Validate agent disciplines reference human disciplines
1892
+ for (const agentDiscipline of agentData.disciplines || []) {
1893
+ if (!humanDisciplineIds.has(agentDiscipline.id)) {
1894
+ errors.push(
1895
+ createError(
1896
+ "ORPHANED_AGENT",
1897
+ `Agent discipline '${agentDiscipline.id}' has no human definition`,
1898
+ `agentData.disciplines`,
1899
+ agentDiscipline.id,
1900
+ ),
1901
+ );
1902
+ }
1903
+
1904
+ // Validate required identity exists (spread from agent section by loader)
1905
+ if (!agentDiscipline.identity) {
1906
+ errors.push(
1907
+ createError(
1908
+ "MISSING_REQUIRED",
1909
+ `Agent discipline '${agentDiscipline.id}' missing identity`,
1910
+ `agentData.disciplines.${agentDiscipline.id}.identity`,
1911
+ ),
1912
+ );
1913
+ }
1914
+ }
1915
+
1916
+ // Validate agent tracks reference human tracks
1917
+ for (const agentTrack of agentData.tracks || []) {
1918
+ if (!humanTrackIds.has(agentTrack.id)) {
1919
+ errors.push(
1920
+ createError(
1921
+ "ORPHANED_AGENT",
1922
+ `Agent track '${agentTrack.id}' has no human definition`,
1923
+ `agentData.tracks`,
1924
+ agentTrack.id,
1925
+ ),
1926
+ );
1927
+ }
1928
+ }
1929
+
1930
+ // Validate agent behaviours reference human behaviours
1931
+ for (const agentBehaviour of agentData.behaviours || []) {
1932
+ if (!humanBehaviourIds.has(agentBehaviour.id)) {
1933
+ errors.push(
1934
+ createError(
1935
+ "ORPHANED_AGENT",
1936
+ `Agent behaviour '${agentBehaviour.id}' has no human definition`,
1937
+ `agentData.behaviours`,
1938
+ agentBehaviour.id,
1939
+ ),
1940
+ );
1941
+ }
1942
+
1943
+ // Validate required agent fields (spread from agent section by loader)
1944
+ if (!agentBehaviour.title) {
1945
+ errors.push(
1946
+ createError(
1947
+ "MISSING_REQUIRED",
1948
+ `Agent behaviour '${agentBehaviour.id}' missing title`,
1949
+ `agentData.behaviours.${agentBehaviour.id}.title`,
1950
+ ),
1951
+ );
1952
+ }
1953
+ if (!agentBehaviour.workingStyle) {
1954
+ errors.push(
1955
+ createError(
1956
+ "MISSING_REQUIRED",
1957
+ `Agent behaviour '${agentBehaviour.id}' missing workingStyle`,
1958
+ `agentData.behaviours.${agentBehaviour.id}.workingStyle`,
1959
+ ),
1960
+ );
1961
+ }
1962
+ }
1963
+
1964
+ // Validate skills with agent sections have complete stage coverage
1965
+ const skillsWithAgent = (humanData.skills || []).filter((s) => s.agent);
1966
+ const requiredStages = ["plan", "code", "review"];
1967
+
1968
+ for (const skill of skillsWithAgent) {
1969
+ const stages = skill.agent.stages || {};
1970
+ const missingStages = requiredStages.filter((stage) => !stages[stage]);
1971
+
1972
+ if (missingStages.length > 0) {
1973
+ warnings.push(
1974
+ createWarning(
1975
+ "INCOMPLETE_STAGES",
1976
+ `Skill '${skill.id}' agent section missing stages: ${missingStages.join(", ")}`,
1977
+ `skills.${skill.id}.agent.stages`,
1978
+ ),
1979
+ );
1980
+ }
1981
+
1982
+ // Validate each stage has required fields
1983
+ for (const [stageId, stageData] of Object.entries(stages)) {
1984
+ if (!stageData.focus) {
1985
+ errors.push(
1986
+ createError(
1987
+ "MISSING_REQUIRED",
1988
+ `Skill '${skill.id}' agent stage '${stageId}' missing focus`,
1989
+ `skills.${skill.id}.agent.stages.${stageId}.focus`,
1990
+ ),
1991
+ );
1992
+ }
1993
+ if (
1994
+ !stageData.activities ||
1995
+ !Array.isArray(stageData.activities) ||
1996
+ stageData.activities.length === 0
1997
+ ) {
1998
+ errors.push(
1999
+ createError(
2000
+ "MISSING_REQUIRED",
2001
+ `Skill '${skill.id}' agent stage '${stageId}' missing or empty activities`,
2002
+ `skills.${skill.id}.agent.stages.${stageId}.activities`,
2003
+ ),
2004
+ );
2005
+ }
2006
+ if (
2007
+ !stageData.ready ||
2008
+ !Array.isArray(stageData.ready) ||
2009
+ stageData.ready.length === 0
2010
+ ) {
2011
+ errors.push(
2012
+ createError(
2013
+ "MISSING_REQUIRED",
2014
+ `Skill '${skill.id}' agent stage '${stageId}' missing or empty ready criteria`,
2015
+ `skills.${skill.id}.agent.stages.${stageId}.ready`,
2016
+ ),
2017
+ );
2018
+ }
2019
+ }
2020
+ }
2021
+
2022
+ // Validate stage handoff targets exist
2023
+ for (const stage of humanData.stages || []) {
2024
+ if (stage.handoffs) {
2025
+ for (const handoff of stage.handoffs) {
2026
+ const targetId = handoff.targetStage || handoff.target;
2027
+ if (targetId && !stageIds.has(targetId)) {
2028
+ errors.push(
2029
+ createError(
2030
+ "INVALID_REFERENCE",
2031
+ `Stage '${stage.id}' handoff references unknown stage '${targetId}'`,
2032
+ `stages.${stage.id}.handoffs`,
2033
+ targetId,
2034
+ ),
2035
+ );
2036
+ }
2037
+ }
2038
+ }
2039
+ }
2040
+
2041
+ // Summary statistics as warnings (informational)
2042
+ const stats = {
2043
+ agentDisciplines: (agentData.disciplines || []).length,
2044
+ agentTracks: (agentData.tracks || []).length,
2045
+ agentBehaviours: (agentData.behaviours || []).length,
2046
+ skillsWithAgent: skillsWithAgent.length,
2047
+ skillsWithCompleteStages: skillsWithAgent.filter((s) => {
2048
+ const stages = s.agent.stages || {};
2049
+ return requiredStages.every((stage) => stages[stage]);
2050
+ }).length,
2051
+ };
2052
+
2053
+ if (stats.skillsWithCompleteStages < stats.skillsWithAgent) {
2054
+ warnings.push(
2055
+ createWarning(
2056
+ "INCOMPLETE_COVERAGE",
2057
+ `${stats.skillsWithCompleteStages}/${stats.skillsWithAgent} skills have complete stage coverage (plan, code, review)`,
2058
+ "agentData",
2059
+ ),
2060
+ );
2061
+ }
2062
+
2063
+ return createValidationResult(errors.length === 0, errors, warnings);
2064
+ }