From dde0b857a241398096b0a92129cfc236d15f82e0 Mon Sep 17 00:00:00 2001 From: Alex <45095641+runsonmypc@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:07:14 -0400 Subject: [PATCH 1/6] fix(archive): use schema-aware task progress in workflows --- openspec/specs/opsx-archive-skill/spec.md | 24 +++-- skills/openspec-archive-change/SKILL.md | 12 ++- skills/openspec-bulk-archive-change/SKILL.md | 12 ++- .../templates/workflows/archive-change.ts | 24 +++-- .../workflows/bulk-archive-change.ts | 24 +++-- .../templates/archive-task-progress.test.ts | 89 +++++++++++++++++++ .../templates/skill-templates-parity.test.ts | 12 +-- 7 files changed, 166 insertions(+), 31 deletions(-) create mode 100644 test/core/templates/archive-task-progress.test.ts diff --git a/openspec/specs/opsx-archive-skill/spec.md b/openspec/specs/opsx-archive-skill/spec.md index 2c76461e54..e052cdef8b 100644 --- a/openspec/specs/opsx-archive-skill/spec.md +++ b/openspec/specs/opsx-archive-skill/spec.md @@ -45,27 +45,37 @@ The skill SHALL check artifact completion status using the artifact graph before ### Requirement: Task Completion Check -The skill SHALL check task completion status from tasks.md before archiving. +The skill SHALL check the selected change's task completion using `totalTasks` and `completedTasks` from `openspec list --json`, with the same selected-root flags used for the rest of the workflow. It SHALL match the change by name and use the CLI's schema-aware task resolution in both single and bulk archive workflows. #### Scenario: Incomplete tasks found -- **WHEN** agent reads tasks.md -- **AND** incomplete tasks are found (marked with `- [ ]`) +- **WHEN** the selected change has `totalTasks` greater than `completedTasks` - **THEN** display warning showing count of incomplete tasks - **AND** prompt user for confirmation to continue - **AND** proceed if user confirms #### Scenario: All tasks complete -- **WHEN** agent reads tasks.md -- **AND** all tasks are complete (marked with `- [x]`) +- **WHEN** the selected change has equal `totalTasks` and `completedTasks` - **THEN** proceed without task-related warning -#### Scenario: No tasks file +#### Scenario: No tracked tasks -- **WHEN** tasks.md does not exist +- **WHEN** the CLI reports `totalTasks` as zero for the selected change - **THEN** proceed without task-related warning +#### Scenario: Custom task artifact or output path + +- **WHEN** the schema tracks tasks under a custom artifact name, output path, or glob +- **THEN** use the CLI totals across the schema-resolved files +- **AND** do not infer completion from artifact existence, an artifact id of `tasks`, or the absence of a top-level `tasks.md` + +#### Scenario: Task progress lookup unavailable + +- **WHEN** the list command fails, returns invalid JSON, or omits a selected change +- **THEN** report the lookup problem and stop before syncing or archiving +- **AND** do not treat the missing progress as zero tasks + ### Requirement: Spec Sync Prompt The skill SHALL prompt to sync delta specs before archiving if specs exist. diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index 5f34ed53a7..6459cb5056 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -74,16 +74,22 @@ Archive a completed change in the experimental workflow. 3. **Check task completion status** - Read the tasks file (typically `tasks.md`) to check for incomplete tasks. + Run `openspec list --json` with the same selected-root flags and find the + entry in `changes` whose `name` exactly matches the selected change. + Use its `totalTasks` and `completedTasks`: the CLI resolves the schema's + tracked task files, including custom artifact names, output paths, and globs. + Incomplete tasks = `totalTasks - completedTasks`. - Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete). + Do not infer task completion from artifact status or the absence of a + top-level `tasks.md`. If the lookup fails, returns invalid JSON, or omits + the selected change, report the problem and stop before syncing or archiving. **If incomplete tasks found:** - Display warning showing count of incomplete tasks - Ask the user to confirm they want to proceed - Proceed if user confirms - **If no tasks file exists:** Proceed without task-related warning. + **If `totalTasks` is zero:** Proceed without a task-related warning. 4. **Assess delta spec sync state** diff --git a/skills/openspec-bulk-archive-change/SKILL.md b/skills/openspec-bulk-archive-change/SKILL.md index 252e1dd155..e2cf254dee 100644 --- a/skills/openspec-bulk-archive-change/SKILL.md +++ b/skills/openspec-bulk-archive-change/SKILL.md @@ -63,15 +63,21 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig 3. **Batch validation - gather status for all selected changes** + Run `openspec list --json` once with the same selected-root flags for task + progress. If the lookup fails, returns invalid JSON, or omits any selected + change, report the problem and stop before syncing or archiving the batch. + For each selected change, collect: a. **Artifact status** - Run `openspec status --change "" --json` - Parse `schemaName`, `artifacts`, `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext` - Note which artifacts are `done` vs other states - b. **Task completion** - Read `artifactPaths.tasks.existingOutputPaths` from status JSON - - Count `- [ ]` (incomplete) vs `- [x]` (complete) - - If no tasks file exists, note as "No tasks" + b. **Task completion** - Find the `changes` entry from the list response whose `name` exactly matches this change + - Use `totalTasks` and `completedTasks`; incomplete tasks = `totalTasks - completedTasks` + - The CLI resolves the schema's tracked task files, including custom artifact names, output paths, and globs + - Do not infer task completion from artifact status, an artifact id of `tasks`, or the absence of a top-level `tasks.md` + - If `totalTasks` is zero, note as "No tasks" c. **Delta specs** - Check `artifactPaths.specs.existingOutputPaths` from status JSON - List which capability specs exist diff --git a/src/core/templates/workflows/archive-change.ts b/src/core/templates/workflows/archive-change.ts index 2dae74d436..81be036cad 100644 --- a/src/core/templates/workflows/archive-change.ts +++ b/src/core/templates/workflows/archive-change.ts @@ -76,16 +76,22 @@ ${STORE_SELECTION_GUIDANCE} 3. **Check task completion status** - Read the tasks file (typically \`tasks.md\`) to check for incomplete tasks. + Run \`openspec list --json\` with the same selected-root flags and find the + entry in \`changes\` whose \`name\` exactly matches the selected change. + Use its \`totalTasks\` and \`completedTasks\`: the CLI resolves the schema's + tracked task files, including custom artifact names, output paths, and globs. + Incomplete tasks = \`totalTasks - completedTasks\`. - Count tasks marked with \`- [ ]\` (incomplete) vs \`- [x]\` (complete). + Do not infer task completion from artifact status or the absence of a + top-level \`tasks.md\`. If the lookup fails, returns invalid JSON, or omits + the selected change, report the problem and stop before syncing or archiving. **If incomplete tasks found:** - Display warning showing count of incomplete tasks - Ask the user to confirm they want to proceed - Proceed if user confirms - **If no tasks file exists:** Proceed without task-related warning. + **If \`totalTasks\` is zero:** Proceed without a task-related warning. 4. **Assess delta spec sync state** @@ -258,16 +264,22 @@ ${STORE_SELECTION_GUIDANCE} 3. **Check task completion status** - Read the tasks file (typically \`tasks.md\`) to check for incomplete tasks. + Run \`openspec list --json\` with the same selected-root flags and find the + entry in \`changes\` whose \`name\` exactly matches the selected change. + Use its \`totalTasks\` and \`completedTasks\`: the CLI resolves the schema's + tracked task files, including custom artifact names, output paths, and globs. + Incomplete tasks = \`totalTasks - completedTasks\`. - Count tasks marked with \`- [ ]\` (incomplete) vs \`- [x]\` (complete). + Do not infer task completion from artifact status or the absence of a + top-level \`tasks.md\`. If the lookup fails, returns invalid JSON, or omits + the selected change, report the problem and stop before syncing or archiving. **If incomplete tasks found:** - Display warning showing count of incomplete tasks - Prompt user for confirmation to continue - Proceed if user confirms - **If no tasks file exists:** Proceed without task-related warning. + **If \`totalTasks\` is zero:** Proceed without a task-related warning. 4. **Assess delta spec sync state** diff --git a/src/core/templates/workflows/bulk-archive-change.ts b/src/core/templates/workflows/bulk-archive-change.ts index cacede2543..31363809c7 100644 --- a/src/core/templates/workflows/bulk-archive-change.ts +++ b/src/core/templates/workflows/bulk-archive-change.ts @@ -65,15 +65,21 @@ ${STORE_SELECTION_GUIDANCE} 3. **Batch validation - gather status for all selected changes** + Run \`openspec list --json\` once with the same selected-root flags for task + progress. If the lookup fails, returns invalid JSON, or omits any selected + change, report the problem and stop before syncing or archiving the batch. + For each selected change, collect: a. **Artifact status** - Run \`openspec status --change "" --json\` - Parse \`schemaName\`, \`artifacts\`, \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\` - Note which artifacts are \`done\` vs other states - b. **Task completion** - Read \`artifactPaths.tasks.existingOutputPaths\` from status JSON - - Count \`- [ ]\` (incomplete) vs \`- [x]\` (complete) - - If no tasks file exists, note as "No tasks" + b. **Task completion** - Find the \`changes\` entry from the list response whose \`name\` exactly matches this change + - Use \`totalTasks\` and \`completedTasks\`; incomplete tasks = \`totalTasks - completedTasks\` + - The CLI resolves the schema's tracked task files, including custom artifact names, output paths, and globs + - Do not infer task completion from artifact status, an artifact id of \`tasks\`, or the absence of a top-level \`tasks.md\` + - If \`totalTasks\` is zero, note as "No tasks" c. **Delta specs** - Check \`artifactPaths.specs.existingOutputPaths\` from status JSON - List which capability specs exist @@ -404,15 +410,21 @@ ${STORE_SELECTION_GUIDANCE} 3. **Batch validation - gather status for all selected changes** + Run \`openspec list --json\` once with the same selected-root flags for task + progress. If the lookup fails, returns invalid JSON, or omits any selected + change, report the problem and stop before syncing or archiving the batch. + For each selected change, collect: a. **Artifact status** - Run \`openspec status --change "" --json\` - Parse \`schemaName\`, \`artifacts\`, \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\` - Note which artifacts are \`done\` vs other states - b. **Task completion** - Read \`artifactPaths.tasks.existingOutputPaths\` from status JSON - - Count \`- [ ]\` (incomplete) vs \`- [x]\` (complete) - - If no tasks file exists, note as "No tasks" + b. **Task completion** - Find the \`changes\` entry from the list response whose \`name\` exactly matches this change + - Use \`totalTasks\` and \`completedTasks\`; incomplete tasks = \`totalTasks - completedTasks\` + - The CLI resolves the schema's tracked task files, including custom artifact names, output paths, and globs + - Do not infer task completion from artifact status, an artifact id of \`tasks\`, or the absence of a top-level \`tasks.md\` + - If \`totalTasks\` is zero, note as "No tasks" c. **Delta specs** - Check \`artifactPaths.specs.existingOutputPaths\` from status JSON - List which capability specs exist diff --git a/test/core/templates/archive-task-progress.test.ts b/test/core/templates/archive-task-progress.test.ts new file mode 100644 index 0000000000..e9c1bf398c --- /dev/null +++ b/test/core/templates/archive-task-progress.test.ts @@ -0,0 +1,89 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +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'; +import { runCLI } from '../../helpers/run-cli.js'; + +const surfaces = [ + ['archive skill', generateSkillContent(getArchiveChangeSkillTemplate(), 'test')], + ['archive command', getOpsxArchiveCommandTemplate().content], + ['bulk archive skill', generateSkillContent(getBulkArchiveChangeSkillTemplate(), 'test')], + ['bulk archive command', getOpsxBulkArchiveCommandTemplate().content], +] as const; + +describe('archive task discovery uses schema-resolved CLI progress', () => { + let root: string; + + async function write(relative: string, content: string) { + const file = path.join(root, relative); + await fs.mkdir(path.dirname(file), { recursive: true }); + await fs.writeFile(file, content); + } + + beforeEach(async () => { + root = await fs.mkdtemp(path.join(os.tmpdir(), 'openspec-archive-task-guidance-')); + await write('openspec/config.yaml', 'schema: custom\n'); + // Include another change so callers must match the selected name, not + // take the first list entry or aggregate progress across the whole root. + await write('openspec/changes/other/tasks.md', '- [x] Unrelated completed work\n'); + }); + + afterEach(async () => { + await fs.rm(root, { recursive: true, force: true }); + }); + + for (const [surface, content] of surfaces) { + it.each([ + ['custom output', 'planning/work-items.md', ['planning/work-items.md']], + ['multiple outputs', 'work/*.md', ['work/backend.md', 'work/frontend.md']], + ] as const)(`${surface}: counts unfinished tasks in %s`, async (_label, generates, files) => { + await write('openspec/schemas/custom/schema.yaml', [ + 'name: custom', + 'version: 1', + 'artifacts:', + ' - id: implementation', + ` generates: "${generates}"`, + ' description: Implementation checklist', + ' template: checklist.md', + ' requires: []', + 'apply:', + ' requires: [implementation]', + ` tracks: "${generates}"`, + ].join('\n')); + await write('openspec/changes/selected/.openspec.yaml', 'schema: custom\n'); + for (const file of files) { + await write(`openspec/changes/selected/${file}`, '- [x] Finished\n- [ ] Pending\n'); + } + + // Execute the lookup actually taught in the task-checking step. The old + // single workflow read tasks.md, and bulk assumed an artifact id "tasks"; + // neither can find this schema's implementation checklist. + const step = content.split('3. **')[1].split('4. **')[0]; + const command = step.match(/openspec (list --json)/); + expect(command, surface).not.toBeNull(); + expect(step).toContain('same selected-root flags'); + expect(step).toMatch(/name` exactly matches/); + expect(step).toContain('totalTasks - completedTasks'); + expect(step).not.toContain('artifactPaths.tasks'); + expect(step).not.toContain('If no tasks file exists'); + + const result = await runCLI(command![1].split(' '), { cwd: root }); + expect(result.exitCode, result.stderr).toBe(0); + const changes = JSON.parse(result.stdout).changes; + expect(changes).toHaveLength(2); + expect(changes.find((change: { name: string }) => change.name === 'selected')).toMatchObject({ + totalTasks: files.length * 2, + completedTasks: files.length, + status: 'in-progress', + }); + await expect(fs.access(path.join(root, 'openspec/changes/selected/tasks.md'))).rejects.toThrow(); + }); + } +}); diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 4d2e717cae..10851ed4c0 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -50,13 +50,13 @@ const EXPECTED_FUNCTION_HASHES: Record = { getOpsxContinueCommandTemplate: 'e50e50266efa1b8e64ff9b6274ee8254f0a240d6adc1b862d126e2f1c9d3a559', getOpsxApplyCommandTemplate: 'e3579ac78f2e2c75fa3d3a7ac7dc3e49c395e96f7323398f0f041d94f8de9bb0', getOpsxFfCommandTemplate: '21132fc9c6d3b3ab2d2295d6bbd72d1e0052eb35ea1be0258c8b1ab3e200c4db', - getArchiveChangeSkillTemplate: '56bfada1a5f35a127791b70de9d428a75b5aedd1584d6c9803a1ecb1fd1b4a23', - getBulkArchiveChangeSkillTemplate: '93875998cade5322d95b43299fba794bc1da754e917dd63a770406386a6d295d', + getArchiveChangeSkillTemplate: '78766871a1ccf72c217a2fa452b40e2de0dd02473fed1f9844339ecf6e408b3d', + getBulkArchiveChangeSkillTemplate: 'b4e1913197cecf6d216678d0d153fe5d18caf7cf6a970de10f51d0dbf42475e1', getOpsxSyncCommandTemplate: '0d2427efb79986e8fff3f96bd075a739c80d45eb29159fae717e950030da8202', getVerifyChangeSkillTemplate: '223b7ffd99299a7d430e13092b9a0a3421b39f0d3217232f46c39d79b5f619ff', - getOpsxArchiveCommandTemplate: '9f973c819b11620985b03322945f0e0a92a02a2ef455b94e74482f5e6292ac5d', + getOpsxArchiveCommandTemplate: '12aea1ca76e050b8c484a4ed2c8c18366d96557bc9382b00c9a92ca15aae31eb', getOpsxOnboardCommandTemplate: 'ee99aa99252c602720fbb8c63fb3ac438a5bd4e952fd961ddf1ae956cbfc2c8f', - getOpsxBulkArchiveCommandTemplate: '9fa8cdebe2f5667ebfc37bdc023396762c59d5b038c771dac2d8fd2c19e2627b', + getOpsxBulkArchiveCommandTemplate: '8b8823fc9cba8d849bf763089f8290e25f338d3ac6deaf2ccd0d5e80666cc939', getOpsxVerifyCommandTemplate: '1efcf7eff0671f48e9d9420f50865c563dd3079ee60f8c380bb7a90dd0102696', getOpsxProposeSkillTemplate: 'b7215583fefddae0127076465de9b3de9c230f2f1ea9ae6e4fb2a46fe510e8d6', getOpsxProposeCommandTemplate: 'f016c66c2b6115b459751154c76a6270e444d6aee31973bb7cb8c0e6d505fb98', @@ -72,8 +72,8 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-apply-change': '81ea96d9fa6ec8536cd23c1fe561ed28e1cc1cad0a8ceb700588e08974cc0e49', 'openspec-ff-change': '31355250514bce51b16ff37ee2b833bc9d475cd0dbd4b1f68fe2041694575623', 'openspec-sync-specs': 'd933d8856584d6c1253de91e652e7aee9e85c77ad4d3531f6476f79d84e6e5e8', - 'openspec-archive-change': '7c65053d674ba4e1e20e2bf73ba7e5a7f94baef2eaa9b33cee48d4cadea51b7a', - 'openspec-bulk-archive-change': '2039b9ecf6e64339dffe0e16272507a386d9fe326f419ff758315aa736fdd96c', + 'openspec-archive-change': 'dfce48b05515c5894e8dd52b6ef42f373a858cd56c068c8aab3d5d8559f114f2', + 'openspec-bulk-archive-change': '9da52fde90403b29c085ceaf873b66dbf037a4ef5acfb777cd86c13dc210f02f', 'openspec-verify-change': 'af9be013dcbe8c6d8f6d9ab10c893fbd03f4c62933c384d82f63894dd0ceb84f', 'openspec-onboard': 'f6f59476acaf5e4d65dbb180da4cef62432612f3cecf207d471a951295e2003a', 'openspec-propose': '679d0f868bed23cfb34a8ecc6b4ba4ff7b88dd7dbaef91563423e98f194f988f', From 8ab3d6e1f95bac28050694a73e017f601930132a Mon Sep 17 00:00:00 2001 From: Alex <45095641+runsonmypc@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:23:40 -0400 Subject: [PATCH 2/6] test(archive): verify task lookup follows the selected store --- .../templates/archive-task-progress.test.ts | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/test/core/templates/archive-task-progress.test.ts b/test/core/templates/archive-task-progress.test.ts index e9c1bf398c..d32d0e29b1 100644 --- a/test/core/templates/archive-task-progress.test.ts +++ b/test/core/templates/archive-task-progress.test.ts @@ -9,6 +9,7 @@ import { getOpsxBulkArchiveCommandTemplate, } from '../../../src/core/templates/skill-templates.js'; import { generateSkillContent } from '../../../src/core/shared/skill-generation.js'; +import { getGlobalDataDir, registerStore } from '../../../src/core/index.js'; import { runCLI } from '../../helpers/run-cli.js'; const surfaces = [ @@ -20,6 +21,9 @@ const surfaces = [ describe('archive task discovery uses schema-resolved CLI progress', () => { let root: string; + let callerRoot: string; + let env: NodeJS.ProcessEnv; + const storeId = 'task-store'; async function write(relative: string, content: string) { const file = path.join(root, relative); @@ -29,10 +33,22 @@ describe('archive task discovery uses schema-resolved CLI progress', () => { beforeEach(async () => { root = await fs.mkdtemp(path.join(os.tmpdir(), 'openspec-archive-task-guidance-')); + env = { + XDG_DATA_HOME: path.join(root, 'data'), + XDG_CONFIG_HOME: path.join(root, 'config'), + }; await write('openspec/config.yaml', 'schema: custom\n'); + await fs.mkdir(path.join(root, 'openspec/specs'), { recursive: true }); // Include another change so callers must match the selected name, not // take the first list entry or aggregate progress across the whole root. await write('openspec/changes/other/tasks.md', '- [x] Unrelated completed work\n'); + await registerStore({ id: storeId, localPath: root, globalDataDir: getGlobalDataDir({ env }) }); + + // The caller's nearest root contains the same change name, but its tasks + // are all done. Omitting --store must therefore produce different progress. + callerRoot = path.join(root, 'caller'); + await write('caller/openspec/config.yaml', 'schema: spec-driven\n'); + await write('caller/openspec/changes/selected/tasks.md', '- [x] Local work is done\n'); }); afterEach(async () => { @@ -66,7 +82,7 @@ describe('archive task discovery uses schema-resolved CLI progress', () => { // single workflow read tasks.md, and bulk assumed an artifact id "tasks"; // neither can find this schema's implementation checklist. const step = content.split('3. **')[1].split('4. **')[0]; - const command = step.match(/openspec (list --json)/); + const command = step.match(/`openspec (list[^`]*)`/); expect(command, surface).not.toBeNull(); expect(step).toContain('same selected-root flags'); expect(step).toMatch(/name` exactly matches/); @@ -74,9 +90,25 @@ describe('archive task discovery uses schema-resolved CLI progress', () => { expect(step).not.toContain('artifactPaths.tasks'); expect(step).not.toContain('If no tasks file exists'); - const result = await runCLI(command![1].split(' '), { cwd: root }); + const args = command![1].trim().split(/\s+/); + expect(args).toEqual(['list', '--json']); + const storeFlag = content.match(/then pass `(--store) `/); + expect(storeFlag).not.toBeNull(); + expect(content).toContain('Every unscoped example of those commands below is shorthand: before running it, append the flag'); + const scopedArgs = [...args, storeFlag![1], storeId]; + expect(scopedArgs).toEqual(['list', '--json', '--store', storeId]); + + const unscoped = await runCLI(args, { cwd: callerRoot, env }); + expect(unscoped.exitCode, unscoped.stderr).toBe(0); + expect(JSON.parse(unscoped.stdout).changes).toEqual([ + expect.objectContaining({ name: 'selected', totalTasks: 1, completedTasks: 1 }), + ]); + + const result = await runCLI(scopedArgs, { cwd: callerRoot, env }); expect(result.exitCode, result.stderr).toBe(0); - const changes = JSON.parse(result.stdout).changes; + const report = JSON.parse(result.stdout); + expect(report.root).toMatchObject({ source: 'store', store_id: storeId }); + const changes = report.changes; expect(changes).toHaveLength(2); expect(changes.find((change: { name: string }) => change.name === 'selected')).toMatchObject({ totalTasks: files.length * 2, From 1609c4d1043865c0d9896610a6bf5c7a086dda3b Mon Sep 17 00:00:00 2001 From: Clay Good Date: Mon, 21 Sep 2026 15:48:06 -0500 Subject: [PATCH 3/6] fix(archive): reject invalid task progress in workflows --- openspec/specs/opsx-archive-skill/spec.md | 2 +- skills/openspec-archive-change/SKILL.md | 11 ++++++---- skills/openspec-bulk-archive-change/SKILL.md | 6 +++-- .../templates/workflows/archive-change.ts | 22 ++++++++++++------- .../workflows/bulk-archive-change.ts | 12 ++++++---- .../templates/archive-task-progress.test.ts | 2 ++ .../templates/skill-templates-parity.test.ts | 12 +++++----- 7 files changed, 42 insertions(+), 25 deletions(-) diff --git a/openspec/specs/opsx-archive-skill/spec.md b/openspec/specs/opsx-archive-skill/spec.md index 826930ff70..0a6311f84e 100644 --- a/openspec/specs/opsx-archive-skill/spec.md +++ b/openspec/specs/opsx-archive-skill/spec.md @@ -72,7 +72,7 @@ The skill SHALL check the selected change's task completion using `totalTasks` a #### Scenario: Task progress lookup unavailable -- **WHEN** the list command fails, returns invalid JSON, or omits a selected change +- **WHEN** the list command fails, returns invalid JSON, omits or duplicates a selected change, or reports invalid task counts - **THEN** report the lookup problem and stop before syncing or archiving - **AND** do not treat the missing progress as zero tasks diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index 063998f7c0..726e19c61c 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -87,13 +87,16 @@ In both branches, never create the root as a side effect: do not run `openspec i Run `openspec list --json` with the same selected-root flags and find the entry in `changes` whose `name` exactly matches the selected change. - Use its `totalTasks` and `completedTasks`: the CLI resolves the schema's - tracked task files, including custom artifact names, output paths, and globs. + Require exactly one match and nonnegative integer `totalTasks` and + `completedTasks`, with `completedTasks <= totalTasks`. The CLI resolves + the schema's tracked task files, including custom artifact names, output + paths, and globs. Incomplete tasks = `totalTasks - completedTasks`. Do not infer task completion from artifact status or the absence of a - top-level `tasks.md`. If the lookup fails, returns invalid JSON, or omits - the selected change, report the problem and stop before syncing or archiving. + top-level `tasks.md`. If the lookup fails, returns invalid JSON, omits or + duplicates the selected change, or returns invalid counts, report the problem + and stop before syncing or archiving. The CLI counts only `x`/`X` checkbox markers as complete; other markers, including unfamiliar ones, remain incomplete. diff --git a/skills/openspec-bulk-archive-change/SKILL.md b/skills/openspec-bulk-archive-change/SKILL.md index f6d4d047e1..d72aa7e499 100644 --- a/skills/openspec-bulk-archive-change/SKILL.md +++ b/skills/openspec-bulk-archive-change/SKILL.md @@ -76,7 +76,8 @@ In both branches, never create the root as a side effect: do not run `openspec i Run `openspec list --json` once with the same selected-root flags for task progress. If the lookup fails, returns invalid JSON, or omits any selected - change, report the problem and stop before syncing or archiving the batch. + change, contains a duplicate selected change, or returns invalid counts, + report the problem and stop before syncing or archiving the batch. For each selected change, collect: @@ -85,7 +86,8 @@ In both branches, never create the root as a side effect: do not run `openspec i - Note which artifacts are `done` vs other states b. **Task completion** - Find the `changes` entry from the list response whose `name` exactly matches this change - - Use `totalTasks` and `completedTasks`; incomplete tasks = `totalTasks - completedTasks` + - Require nonnegative integer `totalTasks` and `completedTasks`, with `completedTasks <= totalTasks` + - Incomplete tasks = `totalTasks - completedTasks` - The CLI resolves the schema's tracked task files, including custom artifact names, output paths, and globs - Do not infer task completion from artifact status, an artifact id of `tasks`, or the absence of a top-level `tasks.md` - The CLI counts only `x`/`X` checkbox markers as complete; other markers remain incomplete diff --git a/src/core/templates/workflows/archive-change.ts b/src/core/templates/workflows/archive-change.ts index 2412df15a7..ee20ff1264 100644 --- a/src/core/templates/workflows/archive-change.ts +++ b/src/core/templates/workflows/archive-change.ts @@ -101,13 +101,16 @@ ${PROJECT_ROOT_GUARD} Run \`openspec list --json\` with the same selected-root flags and find the entry in \`changes\` whose \`name\` exactly matches the selected change. - Use its \`totalTasks\` and \`completedTasks\`: the CLI resolves the schema's - tracked task files, including custom artifact names, output paths, and globs. + Require exactly one match and nonnegative integer \`totalTasks\` and + \`completedTasks\`, with \`completedTasks <= totalTasks\`. The CLI resolves + the schema's tracked task files, including custom artifact names, output + paths, and globs. Incomplete tasks = \`totalTasks - completedTasks\`. Do not infer task completion from artifact status or the absence of a - top-level \`tasks.md\`. If the lookup fails, returns invalid JSON, or omits - the selected change, report the problem and stop before syncing or archiving. + top-level \`tasks.md\`. If the lookup fails, returns invalid JSON, omits or + duplicates the selected change, or returns invalid counts, report the problem + and stop before syncing or archiving. The CLI counts only \`x\`/\`X\` checkbox markers as complete; other markers, including unfamiliar ones, remain incomplete. @@ -299,13 +302,16 @@ ${PROJECT_ROOT_GUARD} Run \`openspec list --json\` with the same selected-root flags and find the entry in \`changes\` whose \`name\` exactly matches the selected change. - Use its \`totalTasks\` and \`completedTasks\`: the CLI resolves the schema's - tracked task files, including custom artifact names, output paths, and globs. + Require exactly one match and nonnegative integer \`totalTasks\` and + \`completedTasks\`, with \`completedTasks <= totalTasks\`. The CLI resolves + the schema's tracked task files, including custom artifact names, output + paths, and globs. Incomplete tasks = \`totalTasks - completedTasks\`. Do not infer task completion from artifact status or the absence of a - top-level \`tasks.md\`. If the lookup fails, returns invalid JSON, or omits - the selected change, report the problem and stop before syncing or archiving. + top-level \`tasks.md\`. If the lookup fails, returns invalid JSON, omits or + duplicates the selected change, or returns invalid counts, report the problem + and stop before syncing or archiving. The CLI counts only \`x\`/\`X\` checkbox markers as complete; other markers, including unfamiliar ones, remain incomplete. diff --git a/src/core/templates/workflows/bulk-archive-change.ts b/src/core/templates/workflows/bulk-archive-change.ts index a214ef924d..897ea14fd9 100644 --- a/src/core/templates/workflows/bulk-archive-change.ts +++ b/src/core/templates/workflows/bulk-archive-change.ts @@ -90,7 +90,8 @@ ${PROJECT_ROOT_GUARD} Run \`openspec list --json\` once with the same selected-root flags for task progress. If the lookup fails, returns invalid JSON, or omits any selected - change, report the problem and stop before syncing or archiving the batch. + change, contains a duplicate selected change, or returns invalid counts, + report the problem and stop before syncing or archiving the batch. For each selected change, collect: @@ -99,7 +100,8 @@ ${PROJECT_ROOT_GUARD} - Note which artifacts are \`done\` vs other states b. **Task completion** - Find the \`changes\` entry from the list response whose \`name\` exactly matches this change - - Use \`totalTasks\` and \`completedTasks\`; incomplete tasks = \`totalTasks - completedTasks\` + - Require nonnegative integer \`totalTasks\` and \`completedTasks\`, with \`completedTasks <= totalTasks\` + - Incomplete tasks = \`totalTasks - completedTasks\` - The CLI resolves the schema's tracked task files, including custom artifact names, output paths, and globs - Do not infer task completion from artifact status, an artifact id of \`tasks\`, or the absence of a top-level \`tasks.md\` - The CLI counts only \`x\`/\`X\` checkbox markers as complete; other markers remain incomplete @@ -454,7 +456,8 @@ ${PROJECT_ROOT_GUARD} Run \`openspec list --json\` once with the same selected-root flags for task progress. If the lookup fails, returns invalid JSON, or omits any selected - change, report the problem and stop before syncing or archiving the batch. + change, contains a duplicate selected change, or returns invalid counts, + report the problem and stop before syncing or archiving the batch. For each selected change, collect: @@ -463,7 +466,8 @@ ${PROJECT_ROOT_GUARD} - Note which artifacts are \`done\` vs other states b. **Task completion** - Find the \`changes\` entry from the list response whose \`name\` exactly matches this change - - Use \`totalTasks\` and \`completedTasks\`; incomplete tasks = \`totalTasks - completedTasks\` + - Require nonnegative integer \`totalTasks\` and \`completedTasks\`, with \`completedTasks <= totalTasks\` + - Incomplete tasks = \`totalTasks - completedTasks\` - The CLI resolves the schema's tracked task files, including custom artifact names, output paths, and globs - Do not infer task completion from artifact status, an artifact id of \`tasks\`, or the absence of a top-level \`tasks.md\` - The CLI counts only \`x\`/\`X\` checkbox markers as complete; other markers remain incomplete diff --git a/test/core/templates/archive-task-progress.test.ts b/test/core/templates/archive-task-progress.test.ts index f4fb78b673..9bd2df18ae 100644 --- a/test/core/templates/archive-task-progress.test.ts +++ b/test/core/templates/archive-task-progress.test.ts @@ -87,6 +87,8 @@ describe('archive task discovery uses schema-resolved CLI progress', () => { expect(step).toContain('same selected-root flags'); expect(step).toMatch(/name` exactly matches/); expect(step).toContain('totalTasks - completedTasks'); + expect(step).toContain('nonnegative integer'); + expect(step).toContain('completedTasks <= totalTasks'); expect(step).toMatch(/other markers.*remain incomplete/s); expect(step).not.toContain('artifactPaths.tasks'); expect(step).not.toContain('If no tasks file exists'); diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 608354816d..590c83f991 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -88,13 +88,13 @@ const EXPECTED_FUNCTION_HASHES: Record = { getOpsxContinueCommandTemplate: 'ace5c9cc239c12b57dc86fd9a1c02a6ca467cb8e1245127340c07ab1b9d37c11', getOpsxApplyCommandTemplate: 'd70cecce3b7d1dd4dbd5fd1fc2bccb538f5e61f5b43d520e4beca896e3f9e6b3', getOpsxFfCommandTemplate: '04cb49b0bf3ebe364b45268a283564ee4fd50b78b01ec1d3f975bcae68179d2d', - getArchiveChangeSkillTemplate: 'ca2251ab0ac61acb18eccd15b4b943d031c9458c84bbd4cc55c634eac489934c', - getBulkArchiveChangeSkillTemplate: 'afaafa40d003e9ae2f0ee80e602f9f919b14fd0c518b39999cc363dd6ab0c596', + getArchiveChangeSkillTemplate: '71715f9d5899498942af03e182e6d1ac2c95952dde967950c2a9161084a53a8b', + getBulkArchiveChangeSkillTemplate: '5997f8c5c33f8933b7b3e61a0e122222e27e36e0b713167c6254e0508d1b95bf', getOpsxSyncCommandTemplate: '60550b7bb9829421656d6324a9e4c951bc912f48f88882d1a07ce7f78397a5e7', getVerifyChangeSkillTemplate: '2e069a277dac23818b13bb50b66e806ab405bc3b7f535400e1ebf81b84153699', - getOpsxArchiveCommandTemplate: 'f82ae76f379c5affb7dad660eaeea5608bfb6645b20a3e2937a9076dd31e0f78', + getOpsxArchiveCommandTemplate: '3d2a330b46043fbb9f220831aa42ebbb62f411e9597b2bb491ad1ac1fa2d0873', getOpsxOnboardCommandTemplate: '9cad751f7b938eea039b0ba207247776269c81bec5923eb335bee468f515f244', - getOpsxBulkArchiveCommandTemplate: 'fe404f25dd3a0aa54dca3bbf6cc15e93e611a1009e15c0487a0a0a7d8860d9ad', + getOpsxBulkArchiveCommandTemplate: '090913b517d62bf6dc093689ab44d1c1c00df22719b6824977c7cc092dbed38f', getOpsxVerifyCommandTemplate: '938f52f20fb9a3b811ea47314baac1034cd550e8ab363ae878ccba4b6329348f', getOpsxProposeSkillTemplate: '1aa2f2eb9c8cbc4dcab9d777bf8832b92ca04f9ef91d0494f1224a566aefdfe8', getOpsxProposeCommandTemplate: '3b7090ce5e79e879ab9b5bdaf4ff2b52e3c02211f71188838772d36ac337f96c', @@ -110,8 +110,8 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-apply-change': 'f3e92c229fab8d77df9f0a77dcb117cf46279b53a208d53aed89bfe0bab2ac09', 'openspec-ff-change': '8ffad1b1a2deea5f097eb7294fb8b9474d5dfb1c31ee2fd3311d9a9d78259323', 'openspec-sync-specs': '3909936a236a21a9a6d5bf495f90b396b3b68fc9220d7b2c1894668653beb2e4', - 'openspec-archive-change': '97c65fc68c2e56060b58daf2ffcedec89e4da0b7b931ae653e3176cceffa4436', - 'openspec-bulk-archive-change': '5e275be82bb71adc3c4b90f7911cfd256ecbcafe53b13165741a0c90107fc95c', + 'openspec-archive-change': 'd01d9eeb06223ee89708b7963e82c5ebc11719c5b2dc62d4abb268ee016fcb7b', + 'openspec-bulk-archive-change': '8018999e6444bdfe97b3b95ffdb5451fb06becf2cfb46bd8fd3277157bdde5d7', 'openspec-verify-change': 'ad8a3098bd27d852721687c47a12db7107ed8b8dfc7f071406bb19961652e7ee', 'openspec-onboard': 'd4c5f3e24c19c8e389950544ea0d1844027753def14748c9684210ae4c6cd5e5', 'openspec-propose': '66e3395adf9f2d93a09e8ef1d20e4efb010e5e8d4811f2d42a9316e4d1ca5a8b', From 82911e1e4e3d00c781fba1bd0a781ed03fd7394e Mon Sep 17 00:00:00 2001 From: Clay Good Date: Tue, 22 Sep 2026 13:34:26 -0500 Subject: [PATCH 4/6] test(parity): regenerate hashes after merging #1940 Co-Authored-By: Claude Opus 5 --- test/core/templates/skill-templates-parity.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 590c83f991..b68c058bfa 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -80,20 +80,20 @@ const EXPECTED_FUNCTION_HASHES: Record = { getNewChangeSkillTemplate: '0e5035b7b42198afc430206a1dbc9579096650ef0813d85e837d5a6cd0b98a85', getContinueChangeSkillTemplate: '550dc22bc8e0921b1ca5cef867379c4f370c5f1902c420bf9fa3bbfa75cea933', getApplyChangeSkillTemplate: '04ae407c97b5f9cb0cc15199fe877ccc7cd1eff78bfe10ad70c16a112b10a661', - getFfChangeSkillTemplate: '6fb5492e78b9ceec068949080ec9f2e0d2a8baff75a2fe33d07ad33ffe542b65', + getFfChangeSkillTemplate: 'd091600476a815ba99f69b446bcd46af5bf73d1c2810215a0c6196937d019cf6', getSyncSpecsSkillTemplate: 'bc80fe9b07eaa289e5eb8a3ce65eb7df722a16d864e37283c678220712e4f230', - getOnboardSkillTemplate: '7d92756ffc0b30053838716005610daf3f65c3fa011f3f4d29b6488f303f9cfb', + getOnboardSkillTemplate: '8f4bb13c097c7c2dfca714c3f51765039008d87f844e62186e066bebd7637376', getOpsxExploreCommandTemplate: 'f6cf22825643281d653355745623a6c1a4566db46cc2f262d2282243c6d8169a', getOpsxNewCommandTemplate: '6d504fef1e0d4ced7c423f4cc9d9d2cee11b1a6224edf685e06a3f0757e0ebff', getOpsxContinueCommandTemplate: 'ace5c9cc239c12b57dc86fd9a1c02a6ca467cb8e1245127340c07ab1b9d37c11', getOpsxApplyCommandTemplate: 'd70cecce3b7d1dd4dbd5fd1fc2bccb538f5e61f5b43d520e4beca896e3f9e6b3', - getOpsxFfCommandTemplate: '04cb49b0bf3ebe364b45268a283564ee4fd50b78b01ec1d3f975bcae68179d2d', + getOpsxFfCommandTemplate: '743a7304c7efc84aa87f556154c034e1e0e561c276c51870a30ada58f33eb9af', getArchiveChangeSkillTemplate: '71715f9d5899498942af03e182e6d1ac2c95952dde967950c2a9161084a53a8b', getBulkArchiveChangeSkillTemplate: '5997f8c5c33f8933b7b3e61a0e122222e27e36e0b713167c6254e0508d1b95bf', getOpsxSyncCommandTemplate: '60550b7bb9829421656d6324a9e4c951bc912f48f88882d1a07ce7f78397a5e7', getVerifyChangeSkillTemplate: '2e069a277dac23818b13bb50b66e806ab405bc3b7f535400e1ebf81b84153699', getOpsxArchiveCommandTemplate: '3d2a330b46043fbb9f220831aa42ebbb62f411e9597b2bb491ad1ac1fa2d0873', - getOpsxOnboardCommandTemplate: '9cad751f7b938eea039b0ba207247776269c81bec5923eb335bee468f515f244', + getOpsxOnboardCommandTemplate: '35332b79e943daefd4118513f03dc48926267af60348f781b8d92bbe5086a986', getOpsxBulkArchiveCommandTemplate: '090913b517d62bf6dc093689ab44d1c1c00df22719b6824977c7cc092dbed38f', getOpsxVerifyCommandTemplate: '938f52f20fb9a3b811ea47314baac1034cd550e8ab363ae878ccba4b6329348f', getOpsxProposeSkillTemplate: '1aa2f2eb9c8cbc4dcab9d777bf8832b92ca04f9ef91d0494f1224a566aefdfe8', @@ -108,12 +108,12 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-new-change': '27e09d43785953827efc9a98bb9d6cf06db48fe6abe7e1c049409fe5b5061323', 'openspec-continue-change': '182f015de6a1a114c79a6106c0565fd71f368d629641d0ad088de54bd871b52f', 'openspec-apply-change': 'f3e92c229fab8d77df9f0a77dcb117cf46279b53a208d53aed89bfe0bab2ac09', - 'openspec-ff-change': '8ffad1b1a2deea5f097eb7294fb8b9474d5dfb1c31ee2fd3311d9a9d78259323', + 'openspec-ff-change': 'a7ab656d46f04d45dff0c8888df4a126a2e62288b7336f7445bce4d1715055f5', 'openspec-sync-specs': '3909936a236a21a9a6d5bf495f90b396b3b68fc9220d7b2c1894668653beb2e4', 'openspec-archive-change': 'd01d9eeb06223ee89708b7963e82c5ebc11719c5b2dc62d4abb268ee016fcb7b', 'openspec-bulk-archive-change': '8018999e6444bdfe97b3b95ffdb5451fb06becf2cfb46bd8fd3277157bdde5d7', 'openspec-verify-change': 'ad8a3098bd27d852721687c47a12db7107ed8b8dfc7f071406bb19961652e7ee', - 'openspec-onboard': 'd4c5f3e24c19c8e389950544ea0d1844027753def14748c9684210ae4c6cd5e5', + 'openspec-onboard': '526bb7f9b8ceb8670b600ce33b0a62fe268393b7e778e120c796316da3a3cc3d', 'openspec-propose': '66e3395adf9f2d93a09e8ef1d20e4efb010e5e8d4811f2d42a9316e4d1ca5a8b', 'openspec-update-change': '19163b8c1b40ccdc0840019aa8005877a90a3a1cd9f7aadb87f76ccce1342f19', }; From b6f9cf7269737f6b7f3340397bf2d4ba8970ea43 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Tue, 22 Sep 2026 14:52:34 -0500 Subject: [PATCH 5/6] test(parity): restore the #1837 regression tests dropped in the merge The earlier conflict resolution took our whole side of the parity file, which discarded the two threshold tests main gained in #1940. Take main's file verbatim and regenerate the hashes instead. Co-Authored-By: Claude Opus 5 --- .../templates/skill-templates-parity.test.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index b68c058bfa..ca088462b8 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -156,6 +156,51 @@ function hash(value: string): string { } describe('skill templates split parity', () => { + it('uses one clarification threshold in fast-forward guidance (#1837)', () => { + const variants: Array<[string, string]> = [ + ['ff skill', getFfChangeSkillTemplate().instructions], + ['ff command', getOpsxFfCommandTemplate().content], + ]; + + for (const [variant, content] of variants) { + expect(content, variant).toContain( + '**If an artifact requires user input** (critically unclear context)' + ); + expect(content, variant).not.toContain( + '**If an artifact requires user input** (unclear context)' + ); + } + }); + + it('approves onboarding tasks before saving or offering implementation (#1837)', () => { + const variants: Array<[string, string]> = [ + ['onboard skill', getOnboardSkillTemplate().instructions], + ['onboard command', getOpsxOnboardCommandTemplate().content], + ]; + + for (const [variant, content] of variants) { + expect(content, variant).toContain('Does this task breakdown look right?'); + expect(content, variant).not.toContain( + 'Each checkbox becomes a unit of work in the apply phase. Ready to implement?' + ); + expect(content, variant).toContain( + '**PAUSE** - Wait for user approval/feedback.\n\n' + + 'After approval, save to the `resolvedOutputPath` from `openspec instructions tasks --change "" --json`.' + ); + expect(content, variant).toContain('> "Tasks are saved. Ready to implement?"'); + expect(content, variant).toContain( + '**PAUSE** - Wait for user to confirm before implementation.' + ); + + const saveAt = content.indexOf('After approval, save to the `resolvedOutputPath`'); + const implementationChoiceAt = content.indexOf('> "Tasks are saved. Ready to implement?"'); + const implementationAt = content.indexOf('## Phase 9: Apply (Implementation)'); + expect(saveAt, variant).toBeGreaterThanOrEqual(0); + expect(implementationChoiceAt, variant).toBeGreaterThan(saveAt); + expect(implementationAt, variant).toBeGreaterThan(implementationChoiceAt); + } + }); + it('preserves all template function payloads exactly', () => { const functionFactories: Record unknown> = { getExploreSkillTemplate, From 1f55befb7a1e09b4e37da70c398b1f48b2f0c748 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Wed, 23 Sep 2026 10:37:01 -0500 Subject: [PATCH 6/6] test(parity): regenerate hashes after merging #1955 Co-Authored-By: Claude Opus 5 --- test/core/templates/skill-templates-parity.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 5bbcb847c3..8ee85789cf 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -88,13 +88,13 @@ const EXPECTED_FUNCTION_HASHES: Record = { getOpsxContinueCommandTemplate: 'ace5c9cc239c12b57dc86fd9a1c02a6ca467cb8e1245127340c07ab1b9d37c11', getOpsxApplyCommandTemplate: 'd70cecce3b7d1dd4dbd5fd1fc2bccb538f5e61f5b43d520e4beca896e3f9e6b3', getOpsxFfCommandTemplate: '743a7304c7efc84aa87f556154c034e1e0e561c276c51870a30ada58f33eb9af', - getArchiveChangeSkillTemplate: '8447a2489240bf0c27f863065d61453dd0264842d1dabafe27b577d6bff96eb3', - getBulkArchiveChangeSkillTemplate: 'f17399959921ff98c7798e4591c8888825b7c9a83b0a90f09d98c7e0984ab793', + getArchiveChangeSkillTemplate: '71715f9d5899498942af03e182e6d1ac2c95952dde967950c2a9161084a53a8b', + getBulkArchiveChangeSkillTemplate: '5997f8c5c33f8933b7b3e61a0e122222e27e36e0b713167c6254e0508d1b95bf', getOpsxSyncCommandTemplate: '60550b7bb9829421656d6324a9e4c951bc912f48f88882d1a07ce7f78397a5e7', getVerifyChangeSkillTemplate: '2e069a277dac23818b13bb50b66e806ab405bc3b7f535400e1ebf81b84153699', - getOpsxArchiveCommandTemplate: '980109e5f8362610872c70fe0a0f1d48d3d2692275b2b17e2f4c91c3de89c2fd', + getOpsxArchiveCommandTemplate: '3d2a330b46043fbb9f220831aa42ebbb62f411e9597b2bb491ad1ac1fa2d0873', getOpsxOnboardCommandTemplate: '0cf66e164c0e14c916c6d1ebb5d80ded07d7fb8e55d4eb34eba43e8ca9c28558', - getOpsxBulkArchiveCommandTemplate: '3db03eadb764abd74c8c180656c3f64a8b9a4971056c91624d38df3209d7b446', + getOpsxBulkArchiveCommandTemplate: '090913b517d62bf6dc093689ab44d1c1c00df22719b6824977c7cc092dbed38f', getOpsxVerifyCommandTemplate: '938f52f20fb9a3b811ea47314baac1034cd550e8ab363ae878ccba4b6329348f', getOpsxProposeSkillTemplate: '1aa2f2eb9c8cbc4dcab9d777bf8832b92ca04f9ef91d0494f1224a566aefdfe8', getOpsxProposeCommandTemplate: '3b7090ce5e79e879ab9b5bdaf4ff2b52e3c02211f71188838772d36ac337f96c', @@ -110,8 +110,8 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-apply-change': 'f3e92c229fab8d77df9f0a77dcb117cf46279b53a208d53aed89bfe0bab2ac09', 'openspec-ff-change': 'a7ab656d46f04d45dff0c8888df4a126a2e62288b7336f7445bce4d1715055f5', 'openspec-sync-specs': '3909936a236a21a9a6d5bf495f90b396b3b68fc9220d7b2c1894668653beb2e4', - 'openspec-archive-change': '305a21a9c76a925055f3bdbaac504f208660ef6948d78f73928de166250609bf', - 'openspec-bulk-archive-change': '4bd638a50111d2ee3a667752a2355ed513f770695b137b93fc28848ca7bf60d2', + 'openspec-archive-change': 'd01d9eeb06223ee89708b7963e82c5ebc11719c5b2dc62d4abb268ee016fcb7b', + 'openspec-bulk-archive-change': '8018999e6444bdfe97b3b95ffdb5451fb06becf2cfb46bd8fd3277157bdde5d7', 'openspec-verify-change': 'ad8a3098bd27d852721687c47a12db7107ed8b8dfc7f071406bb19961652e7ee', 'openspec-onboard': '6993eff867d97d485e080078f9dfb80e968e242f3b17a924eeb077715fd548fa', 'openspec-propose': '66e3395adf9f2d93a09e8ef1d20e4efb010e5e8d4811f2d42a9316e4d1ca5a8b',