feat(#1023): first phase-6 cutover — ui-review (verify:post) inline → loop.render-hooks dispatch (#1024)
Replace the inlined ui-review invocation in autonomous.md §3d.5 with a loop.render-hooks verify:post dispatch — the first workflow to consume render-hooks and fire a skill from it (closes the #1018 live-execution residual as real wiring). Capability-driven, equivalence-preserving for the current registry (only ui-review at verify:post, default on): fires gsd-ui-review under the same precondition (UI-SPEC exists via consumes-gate + workflow.ui_review). Gate findings (real pattern issues, fixed so every future cutover inherits them): - bug-2643 static "Skill() references a real skill" check vs templated Skill(skill="gsd-${ref.skill}") dispatch → skip ${...}-templated names. - Coverage moved, not lost: gen-capability-registry now validates steps[].ref.skill in skills + ref.agent in agents + rejects gsd- double-prefix. - Tightened §3d.5 tests; markdown clarity (consumes rule, LLM-native JSON read, UI-REVIEW.md score hint). gsd-ui-review skill + §3a.5/ui-phase untouched. §5.6/ui-phase cutover deferred (#1022 step-can-halt-vs-gate model question). Closes #1023 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -418,11 +418,16 @@ function validateFeatureBody(cap) {
|
||||
}
|
||||
}
|
||||
|
||||
// Build the declared skill/agent sets for ref membership checks (used in validateStep).
|
||||
// Only build these if the arrays are valid (already validated above).
|
||||
const declaredSkills = Array.isArray(cap.skills) ? new Set(cap.skills.filter((s) => typeof s === 'string')) : null;
|
||||
const declaredAgents = Array.isArray(cap.agents) ? new Set(cap.agents.filter((a) => typeof a === 'string')) : null;
|
||||
|
||||
if (!Array.isArray(cap.steps)) {
|
||||
errors.push('steps must be an array');
|
||||
} else {
|
||||
for (let i = 0; i < cap.steps.length; i++) {
|
||||
errors.push(...validateStep(cap.steps[i], 'steps[' + i + ']'));
|
||||
errors.push(...validateStep(cap.steps[i], 'steps[' + i + ']', declaredSkills, declaredAgents));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,7 +496,18 @@ function validateRuntimeBody(cap) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateStep(step, prefix) {
|
||||
/**
|
||||
* Validate a single step entry.
|
||||
*
|
||||
* @param {object} step The step to validate.
|
||||
* @param {string} prefix Path prefix for error messages (e.g. "steps[0]").
|
||||
* @param {Set|null} declaredSkills Set of skill stems declared in this capability's skills array,
|
||||
* or null if the skills array was not valid (skip membership check).
|
||||
* @param {Set|null} declaredAgents Set of agent names declared in this capability's agents array,
|
||||
* or null if the agents array was not valid (skip membership check).
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function validateStep(step, prefix, declaredSkills, declaredAgents) {
|
||||
const errors = [];
|
||||
|
||||
if (!VALID_LOOP_POINTS.has(step.point)) {
|
||||
@@ -511,9 +527,31 @@ function validateStep(step, prefix) {
|
||||
}
|
||||
if (hasSkill && typeof step.ref.skill !== 'string') {
|
||||
errors.push(prefix + '.ref.skill must be a string');
|
||||
} else if (hasSkill && typeof step.ref.skill === 'string' && step.ref.skill.startsWith('gsd-')) {
|
||||
// Double-prefix guard: ref.skill is an unprefixed stem (e.g. "ui-review").
|
||||
// Workflow dispatch prepends "gsd-" at runtime → "gsd-ui-review".
|
||||
// A stem that already starts with "gsd-" would produce "gsd-gsd-..." at dispatch.
|
||||
errors.push(
|
||||
prefix + '.ref.skill "' + step.ref.skill + '" must not start with "gsd-" ' +
|
||||
'(it is an unprefixed stem; the workflow prepends "gsd-" at dispatch — ' +
|
||||
'starting with "gsd-" would produce "gsd-' + step.ref.skill + '")',
|
||||
);
|
||||
} else if (hasSkill && typeof step.ref.skill === 'string' && declaredSkills !== null && !declaredSkills.has(step.ref.skill)) {
|
||||
// Membership check: ref.skill must be declared in this capability's skills array.
|
||||
// This catches typos and ensures every dispatched skill is owned by this capability.
|
||||
errors.push(
|
||||
prefix + '.ref.skill "' + step.ref.skill + '" is not declared in this capability\'s skills: [' +
|
||||
[...declaredSkills].join(', ') + ']',
|
||||
);
|
||||
}
|
||||
if (hasAgent && typeof step.ref.agent !== 'string') {
|
||||
errors.push(prefix + '.ref.agent must be a string');
|
||||
} else if (hasAgent && typeof step.ref.agent === 'string' && declaredAgents !== null && !declaredAgents.has(step.ref.agent)) {
|
||||
// Membership check: ref.agent must be declared in this capability's agents array.
|
||||
errors.push(
|
||||
prefix + '.ref.agent "' + step.ref.agent + '" is not declared in this capability\'s agents: [' +
|
||||
[...declaredAgents].join(', ') + ']',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user