diff --git a/.changeset/schema-label-from-status.md b/.changeset/schema-label-from-status.md new file mode 100644 index 0000000000..b482256fb5 --- /dev/null +++ b/.changeset/schema-label-from-status.md @@ -0,0 +1,5 @@ +--- +"@fission-ai/openspec": patch +--- + +Stop workflows from displaying schema names that `openspec list --json` does not return. Update and continue no longer fabricate a `spec-driven` picker label, while bulk archive and explore describe only the change fields the list command actually provides. diff --git a/skills/openspec-bulk-archive-change/SKILL.md b/skills/openspec-bulk-archive-change/SKILL.md index d72aa7e499..03309b44e1 100644 --- a/skills/openspec-bulk-archive-change/SKILL.md +++ b/skills/openspec-bulk-archive-change/SKILL.md @@ -41,7 +41,7 @@ In both branches, never create the root as a side effect: do not run `openspec i 2. **Prompt for change selection** Ask the user to choose changes (multi-select): - - Show each change with its schema + - Show each change name and task status from the list output - Include an option for "All changes" - Allow any number of selections (1+ works, 2+ is the typical use case) diff --git a/skills/openspec-continue-change/SKILL.md b/skills/openspec-continue-change/SKILL.md index 1693da7fa3..872b7f502f 100644 --- a/skills/openspec-continue-change/SKILL.md +++ b/skills/openspec-continue-change/SKILL.md @@ -37,7 +37,6 @@ In both branches, never create the root as a side effect: do not run `openspec i When prompting, present the top 3-4 most recently modified changes as options, showing: - Change name - - Schema (from `schema` field if present, otherwise "spec-driven") - Status (e.g., "0/5 tasks", "complete", "no tasks") - How recently it was modified (from `lastModified` field) diff --git a/skills/openspec-explore/SKILL.md b/skills/openspec-explore/SKILL.md index 5b4534e6b9..ebab9a3cbd 100644 --- a/skills/openspec-explore/SKILL.md +++ b/skills/openspec-explore/SKILL.md @@ -128,7 +128,7 @@ openspec list --json This tells you: - If there are active changes -- Their names, schemas, and status +- Their names and task status - What the user might be working on That is the *change* list - work in flight. It does not include the project's durable capabilities, so list those too: diff --git a/skills/openspec-update-change/SKILL.md b/skills/openspec-update-change/SKILL.md index 0504ccd5de..34649eb40c 100644 --- a/skills/openspec-update-change/SKILL.md +++ b/skills/openspec-update-change/SKILL.md @@ -39,7 +39,6 @@ This workflow revises artifacts that already exist; `/openspec-continue-change` When prompting, present the top 3-4 most recently modified changes as options, showing: - Change name - - Schema (from `schema` field if present, otherwise "spec-driven") - Status (e.g., "0/5 tasks", "complete", "no tasks") - How recently it was modified (from `lastModified` field) diff --git a/src/core/templates/workflows/bulk-archive-change.ts b/src/core/templates/workflows/bulk-archive-change.ts index 897ea14fd9..f9c2f8d3d4 100644 --- a/src/core/templates/workflows/bulk-archive-change.ts +++ b/src/core/templates/workflows/bulk-archive-change.ts @@ -55,7 +55,7 @@ ${PROJECT_ROOT_GUARD} 2. **Prompt for change selection** Ask the user to choose changes (multi-select): - - Show each change with its schema + - Show each change name and task status from the list output - Include an option for "All changes" - Allow any number of selections (1+ works, 2+ is the typical use case) @@ -219,7 +219,7 @@ ${PROJECT_ROOT_GUARD} Process changes in the determined order (respecting conflict resolution): a. **Sync included delta specs**: - - Run the \`openspec-sync-specs\` workflow inline (agent-driven intelligent merge) only for changes with entries in \`includedDeltas\`, passing only the included delta paths and explicitly instructing it to ignore that change's \`excludedDeltas\`. Wait for it to finish. + - ${optionalWorkflow('sync', 'Run the `openspec-sync-specs` workflow inline (agent-driven intelligent merge)', 'Perform the delta-to-main-spec merge inline yourself (agent-driven intelligent merge)')} only for changes with entries in \`includedDeltas\`, passing only the included delta paths and explicitly instructing it to ignore that change's \`excludedDeltas\`. Wait for it to finish. - For conflicts, apply in resolved order. - Pass that change's fetched specs-rule snapshot into inline sync; inline sync must reuse it without fetching instructions again @@ -372,7 +372,7 @@ No active changes found. Create a new change to get started. - Archive directory target uses the current date, computed once in step 3d and reused at the move: YYYY-MM-DD-; a name that already starts with a \`YYYY-MM-DD-\` prefix is used as-is (never stack a second date) - If archive target exists, fail that change but continue with others - Check every archive target in step 3, before the first main-spec write; a change whose target exists is never synced or moved -- If sync is requested, run the \`openspec-sync-specs\` workflow inline (agent-driven) for each change with included delta specs +- If sync is requested, ${optionalWorkflow('sync', 'run the `openspec-sync-specs` workflow inline (agent-driven)', 'perform the delta-to-main-spec merge inline (agent-driven)')} for each change with included delta specs - Carry the per-delta \`includedDeltas\` and \`excludedDeltas\` decisions into execution; sync and verify only included deltas - Report every excluded delta as \`sync skipped\` without treating the archive itself as skipped - Never archive a change while a spec sync is still in flight — run the sync inline and verify main specs at \`/openspec/specs//spec.md\` before moving \`changeRoot\` @@ -421,7 +421,7 @@ ${PROJECT_ROOT_GUARD} 2. **Prompt for change selection** Ask the user to choose changes (multi-select): - - Show each change with its schema + - Show each change name and task status from the list output - Include an option for "All changes" - Allow any number of selections (1+ works, 2+ is the typical use case) diff --git a/src/core/templates/workflows/continue-change.ts b/src/core/templates/workflows/continue-change.ts index 56d5b34a63..296801a756 100644 --- a/src/core/templates/workflows/continue-change.ts +++ b/src/core/templates/workflows/continue-change.ts @@ -47,7 +47,6 @@ ${PROJECT_ROOT_GUARD} When prompting, present the top 3-4 most recently modified changes as options, showing: - Change name - - Schema (from \`schema\` field if present, otherwise "spec-driven") - Status (e.g., "0/5 tasks", "complete", "no tasks") - How recently it was modified (from \`lastModified\` field) @@ -167,7 +166,6 @@ ${PROJECT_ROOT_GUARD} When prompting, present the top 3-4 most recently modified changes as options, showing: - Change name - - Schema (from \`schema\` field if present, otherwise "spec-driven") - Status (e.g., "0/5 tasks", "complete", "no tasks") - How recently it was modified (from \`lastModified\` field) diff --git a/src/core/templates/workflows/explore.ts b/src/core/templates/workflows/explore.ts index 96fe854bd7..e5ad09819c 100644 --- a/src/core/templates/workflows/explore.ts +++ b/src/core/templates/workflows/explore.ts @@ -167,7 +167,7 @@ openspec list --json This tells you: - If there are active changes -- Their names, schemas, and status +- Their names and task status - What the user might be working on That is the *change* list - work in flight. It does not include the project's durable capabilities, so list those too: @@ -499,7 +499,7 @@ openspec list --json This tells you: - If there are active changes -- Their names, schemas, and status +- Their names and task status - What the user might be working on That is the *change* list - work in flight. It does not include the project's durable capabilities, so list those too: diff --git a/src/core/templates/workflows/update-change.ts b/src/core/templates/workflows/update-change.ts index b3992a7658..7ea49fbf1c 100644 --- a/src/core/templates/workflows/update-change.ts +++ b/src/core/templates/workflows/update-change.ts @@ -98,7 +98,6 @@ ${CONTINUE_SCOPE_NOTE} When prompting, present the top 3-4 most recently modified changes as options, showing: - Change name - - Schema (from \`schema\` field if present, otherwise "spec-driven") - Status (e.g., "0/5 tasks", "complete", "no tasks") - How recently it was modified (from \`lastModified\` field) @@ -199,7 +198,6 @@ ${CONTINUE_SCOPE_NOTE} When prompting, present the top 3-4 most recently modified changes as options, showing: - Change name - - Schema (from \`schema\` field if present, otherwise "spec-driven") - Status (e.g., "0/5 tasks", "complete", "no tasks") - How recently it was modified (from \`lastModified\` field) diff --git a/test/cli-e2e/basic.test.ts b/test/cli-e2e/basic.test.ts index c05965ea72..a8bc3a42a9 100644 --- a/test/cli-e2e/basic.test.ts +++ b/test/cli-e2e/basic.test.ts @@ -153,6 +153,55 @@ describe('openspec CLI e2e basics', () => { }); }); + it.each([ + { tasks: '', completedTasks: 0, totalTasks: 0, status: 'no-tasks' }, + { tasks: '- [x] Done\n- [ ] Pending\n', completedTasks: 1, totalTasks: 2, status: 'in-progress' }, + { tasks: '- [x] Done\n', completedTasks: 1, totalTasks: 1, status: 'complete' }, + ])('lists custom-schema task status ($status) without inventing a schema field', async (expected) => { + const projectDir = await prepareFixture('tmp-init'); + const schemaDir = path.join(projectDir, 'openspec', 'schemas', 'custom-workflow'); + await fs.mkdir(schemaDir, { recursive: true }); + await fs.writeFile(path.join(schemaDir, 'schema.yaml'), [ + 'name: custom-workflow', + 'version: 1', + 'description: Custom tracked work', + 'artifacts:', + ' - id: work', + ' generates: work.md', + ' description: Implementation work', + ' template: work.md', + ' requires: []', + 'apply:', + ' requires: [work]', + ' tracks: work.md', + '', + ].join('\n')); + const changeDir = path.join(projectDir, 'openspec', 'changes', 'custom-change'); + await fs.mkdir(changeDir, { recursive: true }); + await fs.writeFile(path.join(changeDir, '.openspec.yaml'), 'schema: custom-workflow\n'); + await fs.writeFile(path.join(changeDir, 'work.md'), expected.tasks); + + const listResult = await runCLI(['list', '--json'], { cwd: projectDir }); + expectJsonOnlyOutput(listResult); + const change = JSON.parse(listResult.stdout).changes.find( + (entry: { name: string }) => entry.name === 'custom-change' + ); + expect(change).toMatchObject({ + name: 'custom-change', + completedTasks: expected.completedTasks, + totalTasks: expected.totalTasks, + status: expected.status, + lastModified: expect.any(String), + }); + expect(Number.isFinite(Date.parse(change.lastModified))).toBe(true); + expect(change).not.toHaveProperty('schema'); + expect(change).not.toHaveProperty('schemaName'); + + const statusResult = await runCLI(['status', '--change', 'custom-change', '--json'], { cwd: projectDir }); + expectJsonOnlyOutput(statusResult); + expect(JSON.parse(statusResult.stdout).schemaName).toBe('custom-workflow'); + }); + it('keeps schemas --json free of spinner output', async () => { const projectDir = await prepareFixture('tmp-init'); const result = await runCLI(['schemas', '--json'], { cwd: projectDir }); diff --git a/test/core/templates/archive-task-progress.test.ts b/test/core/templates/archive-task-progress.test.ts index 9bd2df18ae..eefa1672ab 100644 --- a/test/core/templates/archive-task-progress.test.ts +++ b/test/core/templates/archive-task-progress.test.ts @@ -3,20 +3,33 @@ import { promises as fs } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { - getArchiveChangeSkillTemplate, - getBulkArchiveChangeSkillTemplate, - getOpsxArchiveCommandTemplate, - getOpsxBulkArchiveCommandTemplate, -} from '../../../src/core/templates/skill-templates.js'; -import { generateSkillContent } from '../../../src/core/shared/skill-generation.js'; + generateSkillContent, + getCommandTemplates, + getSkillTemplates, +} from '../../../src/core/shared/skill-generation.js'; import { getGlobalDataDir, registerStore } from '../../../src/core/index.js'; import { runCLI } from '../../helpers/run-cli.js'; +// Go through getSkillTemplates/getCommandTemplates rather than the raw +// templates: these workflows carry optional-workflow blocks, and only these +// entry points resolve them against an installed set. Building from the raw +// template leaves `[[opsx:if-workflow ...]]` in the text, which skill +// generation rejects. The set names sync so the installed branch is chosen, +// which is the wording these assertions are about. +const WORKFLOWS = ['archive', 'bulk-archive', 'sync']; +const skill = (workflowId: string): string => + generateSkillContent( + getSkillTemplates(WORKFLOWS).find((entry) => entry.workflowId === workflowId)!.template, + 'test' + ); +const command = (id: string): string => + getCommandTemplates(WORKFLOWS).find((entry) => entry.id === id)!.template.content; + const surfaces = [ - ['archive skill', generateSkillContent(getArchiveChangeSkillTemplate(), 'test')], - ['archive command', getOpsxArchiveCommandTemplate().content], - ['bulk archive skill', generateSkillContent(getBulkArchiveChangeSkillTemplate(), 'test')], - ['bulk archive command', getOpsxBulkArchiveCommandTemplate().content], + ['archive skill', skill('archive')], + ['archive command', command('archive')], + ['bulk archive skill', skill('bulk-archive')], + ['bulk archive command', command('bulk-archive')], ] as const; describe('archive task discovery uses schema-resolved CLI progress', () => { diff --git a/test/core/templates/list-json-contract.test.ts b/test/core/templates/list-json-contract.test.ts new file mode 100644 index 0000000000..6a5f22919c --- /dev/null +++ b/test/core/templates/list-json-contract.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, it } from 'vitest'; + +import { + getBulkArchiveChangeSkillTemplate, + getContinueChangeSkillTemplate, + getExploreSkillTemplate, + getOpsxBulkArchiveCommandTemplate, + getOpsxContinueCommandTemplate, + getOpsxExploreCommandTemplate, + getOpsxUpdateCommandTemplate, + getUpdateChangeSkillTemplate, +} from '../../../src/core/templates/skill-templates.js'; +import { getCommandTemplates, getSkillTemplates } from '../../../src/core/shared/skill-generation.js'; + +describe('workflow list --json field usage', () => { + it('does not invent schema labels in update and continue pickers', () => { + const bodies = [ + getUpdateChangeSkillTemplate().instructions, + getOpsxUpdateCommandTemplate().content, + getContinueChangeSkillTemplate().instructions, + getOpsxContinueCommandTemplate().content, + ]; + + for (const body of bodies) { + const picker = body.slice(body.indexOf('1. **Select the change**'), body.indexOf('2. **')); + expect(picker).toContain('openspec list --json'); + expect(picker).toContain('- Change name'); + expect(picker).toContain('- Status'); + expect(picker).toContain('`lastModified`'); + expect(picker).not.toMatch(/schema/i); + expect(picker).not.toContain('openspec status'); + + const status = body.slice(body.indexOf('2. **'), body.indexOf('3. **')); + expect(status).toContain('openspec status --change "" --json'); + expect(status).toContain('`schemaName`'); + } + }); + + it('limits bulk archive selection to list fields', () => { + const bodies = [ + getBulkArchiveChangeSkillTemplate().instructions, + getOpsxBulkArchiveCommandTemplate().content, + ]; + + for (const body of bodies) { + const picker = body.slice(body.indexOf('2. **'), body.indexOf('3. **')); + expect(picker).toContain('Show each change name and task status from the list output'); + expect(picker).not.toMatch(/schema/i); + expect(picker).not.toContain('openspec status'); + + const status = body.slice(body.indexOf('3. **'), body.indexOf('4. **')); + expect(status).toContain('openspec status --change "" --json'); + expect(status).toContain('`schemaName`'); + } + }); + + it('does not claim explore receives schemas from list output', () => { + const bodies = [ + getExploreSkillTemplate().instructions, + getOpsxExploreCommandTemplate().content, + ]; + + for (const body of bodies) { + expect(body).toContain('Their names and task status'); + expect(body).not.toContain('Their names, schemas, and status'); + } + }); + + it('keeps bulk archive sync available with and without the sync workflow', () => { + const variants = [ + [ + getSkillTemplates(['bulk-archive', 'sync']).find((entry) => entry.workflowId === 'bulk-archive')!.template.instructions, + getSkillTemplates(['bulk-archive'])[0].template.instructions, + 'openspec-sync-specs', + ], + [ + getCommandTemplates(['bulk-archive', 'sync']).find((entry) => entry.id === 'bulk-archive')!.template.content, + getCommandTemplates(['bulk-archive'])[0].template.content, + '/opsx:sync', + ], + ] as const; + + for (const [withSync, withoutSync, workflow] of variants) { + const syncStep = (text: string) => text.slice( + text.indexOf('a. **Sync included delta specs**'), + text.indexOf('b. **Verify included delta specs') + ); + + expect(syncStep(withSync)).toContain(workflow); + expect(syncStep(withoutSync)).not.toContain(workflow); + expect(syncStep(withoutSync)).toContain('Perform the delta-to-main-spec merge inline yourself'); + expect(syncStep(withoutSync)).toContain('`includedDeltas`'); + expect(syncStep(withoutSync)).toContain('`excludedDeltas`'); + expect(withoutSync).toContain('If sync is requested, perform the delta-to-main-spec merge inline'); + } + }); +}); diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index b394b30ca4..4157a5d9d0 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -76,46 +76,46 @@ function specDrivenTitles(): Record { } const EXPECTED_FUNCTION_HASHES: Record = { - getExploreSkillTemplate: 'b17a409b5634b5e48864a87f038f2111a74c2442288e9f4cb7a704f86b6d75e7', + getExploreSkillTemplate: 'c1fddb294758004936add586f5826694cb06175cff935b75fd3a8d92332332e6', getNewChangeSkillTemplate: '0e5035b7b42198afc430206a1dbc9579096650ef0813d85e837d5a6cd0b98a85', - getContinueChangeSkillTemplate: '550dc22bc8e0921b1ca5cef867379c4f370c5f1902c420bf9fa3bbfa75cea933', + getContinueChangeSkillTemplate: 'c2c8a0ba7f8c8fc7b174793832cd50f7c404eb8e1f7d49c47000993d621633b6', getApplyChangeSkillTemplate: '04ae407c97b5f9cb0cc15199fe877ccc7cd1eff78bfe10ad70c16a112b10a661', getFfChangeSkillTemplate: 'd091600476a815ba99f69b446bcd46af5bf73d1c2810215a0c6196937d019cf6', getSyncSpecsSkillTemplate: 'bc80fe9b07eaa289e5eb8a3ce65eb7df722a16d864e37283c678220712e4f230', getOnboardSkillTemplate: '84258a06c0ca88de708a23dd74e9a17efe11eff63a071b3864c781dcd5a0a4b7', - getOpsxExploreCommandTemplate: 'f6cf22825643281d653355745623a6c1a4566db46cc2f262d2282243c6d8169a', + getOpsxExploreCommandTemplate: '5d11f8ecb4c457140a3e874a8bf7aa72674e922e698c208832b1f34d3c617719', getOpsxNewCommandTemplate: '6d504fef1e0d4ced7c423f4cc9d9d2cee11b1a6224edf685e06a3f0757e0ebff', - getOpsxContinueCommandTemplate: 'ace5c9cc239c12b57dc86fd9a1c02a6ca467cb8e1245127340c07ab1b9d37c11', + getOpsxContinueCommandTemplate: '241c50f97d5d681412d456d6b982743c3a5babeb77017fc8099c418bcf0d92df', getOpsxApplyCommandTemplate: 'd70cecce3b7d1dd4dbd5fd1fc2bccb538f5e61f5b43d520e4beca896e3f9e6b3', getOpsxFfCommandTemplate: '743a7304c7efc84aa87f556154c034e1e0e561c276c51870a30ada58f33eb9af', getArchiveChangeSkillTemplate: '71715f9d5899498942af03e182e6d1ac2c95952dde967950c2a9161084a53a8b', - getBulkArchiveChangeSkillTemplate: '5997f8c5c33f8933b7b3e61a0e122222e27e36e0b713167c6254e0508d1b95bf', + getBulkArchiveChangeSkillTemplate: '2a6ec08fea0f942158b4abe9c8d1af9622038e0c4dc684e7c73dad2fb8379a54', getOpsxSyncCommandTemplate: '60550b7bb9829421656d6324a9e4c951bc912f48f88882d1a07ce7f78397a5e7', getVerifyChangeSkillTemplate: '2e069a277dac23818b13bb50b66e806ab405bc3b7f535400e1ebf81b84153699', getOpsxArchiveCommandTemplate: '3d2a330b46043fbb9f220831aa42ebbb62f411e9597b2bb491ad1ac1fa2d0873', getOpsxOnboardCommandTemplate: '0cf66e164c0e14c916c6d1ebb5d80ded07d7fb8e55d4eb34eba43e8ca9c28558', - getOpsxBulkArchiveCommandTemplate: '090913b517d62bf6dc093689ab44d1c1c00df22719b6824977c7cc092dbed38f', + getOpsxBulkArchiveCommandTemplate: '4e2e39c4d634074f4a1ed67f076d5c4d0ead8b998f4d75218c33cdc6173719be', getOpsxVerifyCommandTemplate: '938f52f20fb9a3b811ea47314baac1034cd550e8ab363ae878ccba4b6329348f', getOpsxProposeSkillTemplate: '1aa2f2eb9c8cbc4dcab9d777bf8832b92ca04f9ef91d0494f1224a566aefdfe8', getOpsxProposeCommandTemplate: '3b7090ce5e79e879ab9b5bdaf4ff2b52e3c02211f71188838772d36ac337f96c', getFeedbackSkillTemplate: 'dabeb5e825b9349abc8156c3e7b8608f27987912a6d9bf47ef29addde6138133', - getUpdateChangeSkillTemplate: '90c49d56fbc4feb67983d18064aa6b60909a9f81d5b7f6ff192f877d6b7a6b6c', - getOpsxUpdateCommandTemplate: 'aa86facbb5354ba7316e0a708bae40d5ab87553be37ef2c220a832130cd73e0c', + getUpdateChangeSkillTemplate: '8380139769cf9b247cb64089c07628e73923405fca4704052d52a67f35526fe8', + getOpsxUpdateCommandTemplate: 'ec6c8b7f3f366d65a216c4ba423bc41b8dbd18974a432186ad44e60c1891340b', }; const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { - 'openspec-explore': '8b02eb77ae87a4374a43ad33930e5ea19cbaf1c8a7624d5b613c03d7ee1c5f14', + 'openspec-explore': '7d80caf9cd25a2565ba190b1297f1631c7f2c2db5e614597b4284abc0118ea70', 'openspec-new-change': '27e09d43785953827efc9a98bb9d6cf06db48fe6abe7e1c049409fe5b5061323', - 'openspec-continue-change': '182f015de6a1a114c79a6106c0565fd71f368d629641d0ad088de54bd871b52f', + 'openspec-continue-change': '1f92fad53022270e96f8ea34de75f7c12c08225edd5a9e8f4e864b63b5ef79c5', 'openspec-apply-change': 'f3e92c229fab8d77df9f0a77dcb117cf46279b53a208d53aed89bfe0bab2ac09', 'openspec-ff-change': 'a7ab656d46f04d45dff0c8888df4a126a2e62288b7336f7445bce4d1715055f5', 'openspec-sync-specs': '3909936a236a21a9a6d5bf495f90b396b3b68fc9220d7b2c1894668653beb2e4', 'openspec-archive-change': 'd01d9eeb06223ee89708b7963e82c5ebc11719c5b2dc62d4abb268ee016fcb7b', - 'openspec-bulk-archive-change': '8018999e6444bdfe97b3b95ffdb5451fb06becf2cfb46bd8fd3277157bdde5d7', + 'openspec-bulk-archive-change': '10f050ad5ef77084dc55a202427988b23903ee122f00985238a4eb9354a5dc3c', 'openspec-verify-change': 'ad8a3098bd27d852721687c47a12db7107ed8b8dfc7f071406bb19961652e7ee', 'openspec-onboard': '6993eff867d97d485e080078f9dfb80e968e242f3b17a924eeb077715fd548fa', 'openspec-propose': '66e3395adf9f2d93a09e8ef1d20e4efb010e5e8d4811f2d42a9316e4d1ca5a8b', - 'openspec-update-change': 'c53a8416550d1c2dd7b2131acddd3498b8eef5c10c86bc2bc21bb1cdecb891aa', + 'openspec-update-change': '5f4ea19aa732b33d87a2120ec393ee34578e70678d97e8c3bb10f988c00cb4d3', }; // Intentionally excludes getFeedbackSkillTemplate: this list only models templates