diff --git a/openspec/changes/support-non-asset-files/tasks.md b/openspec/changes/support-non-asset-files/tasks.md index 020149a1..b432734d 100644 --- a/openspec/changes/support-non-asset-files/tasks.md +++ b/openspec/changes/support-non-asset-files/tasks.md @@ -125,10 +125,10 @@ - [x] 13.1 Implement: Add an editable default `README.md` scaffold option and template that writes the file and top-level declaration atomically without regenerating authored content after identity edits - [x] 13.2 Implement: Add the dedicated create README card/editor flow, optional disable behavior, state snapshotting, and explicit confirmation preview, and align headless create with the documented policy -- [ ] 13.3 Implement: Extend edit scanning and reconciliation for undeclared skill companions, common root files, and missing declared supplementary files while routing only exact `README.md` and `README` paths to a dedicated panel +- [x] 13.3 Implement: Extend edit scanning and reconciliation for undeclared skill companions, common root files, and missing declared supplementary files while routing only exact `README.md` and `README` paths to a dedicated panel - [ ] 13.4 Implement: Add independent tagged states and actions for both conventional README paths, preserving bytes on adoption and retaining exact paths for scaffold, edit, removal, and declaration changes -- [ ] 13.5 Implement: Replace string-parsed reconciliation keys with stable structured identities and represent file/declaration operations as tagged variants with no invalid combinations -- [ ] 13.6 Implement: Apply README, companion, and generic supplementary changes transactionally with manifest edits and show every queued exact-path operation before Apply +- [x] 13.5 Implement: Replace string-parsed reconciliation keys with stable structured identities and represent file/declaration operations as tagged variants with no invalid combinations +- [x] 13.6 Implement: Apply README, companion, and generic supplementary changes transactionally with manifest edits and show every queued exact-path operation before Apply - [ ] 13.7 Implement: Add engine, CLI, TUI, integration, and create-build end-to-end tests covering README defaults/disable/edit preservation in interactive and headless creates, both README paths, adoption, missing-file choices, companion discovery, skill deletion preserving undeclared files, confirmation, cancellation, and buildability - [ ] 13.8 Verify: Run focused scaffold, edit, create, TUI, integration, and end-to-end tests diff --git a/packages/cli/src/__tests__/edit-integration.test.ts b/packages/cli/src/__tests__/edit-integration.test.ts index a94fc7a3..5d286540 100644 --- a/packages/cli/src/__tests__/edit-integration.test.ts +++ b/packages/cli/src/__tests__/edit-integration.test.ts @@ -33,9 +33,9 @@ describe('edit integration', () => { const result = await buildEditContext(dir) expect(result.ok).toBe(true) if (!result.ok) expect.unreachable() - const additions = result.context.reconciliationItems.filter((i) => i.kind === 'addition') + const additions = result.context.reconciliationItems.filter((i) => i.kind === 'asset-addition') expect(additions).toHaveLength(1) - expect(additions[0]?.name).toBe('new-one') + expect(additions[0]?.kind === 'asset-addition' && additions[0].name).toBe('new-one') }) test('buildEditContext detects missing files in manifest', async () => { @@ -55,9 +55,9 @@ describe('edit integration', () => { const result = await buildEditContext(dir) expect(result.ok).toBe(true) if (!result.ok) expect.unreachable() - const missing = result.context.reconciliationItems.filter((i) => i.kind === 'missing') + const missing = result.context.reconciliationItems.filter((i) => i.kind === 'asset-missing') expect(missing).toHaveLength(1) - expect(missing[0]?.name).toBe('gone') + expect(missing[0]?.kind === 'asset-missing' && missing[0].name).toBe('gone') }) test('buildEditContext does NOT flag matched files that contain front matter', async () => { @@ -97,9 +97,13 @@ describe('edit integration', () => { version: '1.0.0', skills: { helper: { description: 'A helper skill' } }, } - const operations: EditOperation[] = [{ op: 'write-manifest' }, { op: 'scaffold', type: 'skills', name: 'helper' }] + const operations: EditOperation[] = [ + { op: 'write-manifest', manifest }, + { op: 'scaffold-asset', assetType: 'skills', name: 'helper' }, + ] - await applyOperations(manifest, operations, dir) + const applied = await applyOperations(operations, dir) + expect(applied.ok).toBe(true) const manifestExists = await Bun.file(join(dir, 'facet.json')).exists() expect(manifestExists).toBe(true) @@ -114,9 +118,13 @@ describe('edit integration', () => { await Bun.write(join(dir, 'skills/old/SKILL.md'), '# Old skill') const manifest = { name: 'test', version: '1.0.0', skills: { remaining: { description: 'Remaining' } } } - const operations: EditOperation[] = [{ op: 'write-manifest' }, { op: 'delete-file', type: 'skills', name: 'old' }] + const operations: EditOperation[] = [ + { op: 'write-manifest', manifest }, + { op: 'delete-asset', assetType: 'skills', name: 'old', companionPaths: [] }, + ] - await applyOperations(manifest, operations, dir) + const applied = await applyOperations(operations, dir) + expect(applied.ok).toBe(true) const deleted = await Bun.file(join(dir, 'skills/old/SKILL.md')).exists() expect(deleted).toBe(false) @@ -129,9 +137,13 @@ describe('edit integration', () => { version: '1.0.0', skills: { example: { description: 'An example skill' } }, } - const operations: EditOperation[] = [{ op: 'write-manifest' }, { op: 'scaffold', type: 'skills', name: 'example' }] + const operations: EditOperation[] = [ + { op: 'write-manifest', manifest }, + { op: 'scaffold-asset', assetType: 'skills', name: 'example' }, + ] - await applyOperations(manifest, operations, dir) + const applied = await applyOperations(operations, dir) + expect(applied.ok).toBe(true) const buildResult = await runBuildPipeline(dir) expect(buildResult.ok).toBe(true) diff --git a/packages/cli/src/commands/edit/index.ts b/packages/cli/src/commands/edit/index.ts index 36b7bf64..84eb75d6 100644 --- a/packages/cli/src/commands/edit/index.ts +++ b/packages/cli/src/commands/edit/index.ts @@ -24,11 +24,24 @@ export const editCommand: Command = { if (!loaded.ok) return loaded.exitCode const buildArg = args[0] ? ` ${displayDir}` : '' + let applyError: string | null = null const completed = await runEditWizardInk(loaded.context, { - onApply: (result) => applyEditOperations(result.manifest, result.operations, rootDir), + onApply: async (result) => { + const applied = await applyEditOperations(result.operations, rootDir) + if (!applied.ok) { + applyError = `Failed to apply changes at ${applied.failedPath}: ${applied.reason}${ + applied.rollbackOk ? ' (rolled back)' : ' (rollback incomplete)' + }` + } + }, buildArg, }) + if (applyError) { + console.error(applyError) + return 1 + } + if (!completed) { console.log('\nCancelled — no changes applied.') return 1 diff --git a/packages/cli/src/tui/views/__tests__/confirm-privacy.test.tsx b/packages/cli/src/tui/views/__tests__/confirm-privacy.test.tsx index 744416f8..deda3cab 100644 --- a/packages/cli/src/tui/views/__tests__/confirm-privacy.test.tsx +++ b/packages/cli/src/tui/views/__tests__/confirm-privacy.test.tsx @@ -49,7 +49,7 @@ function renderEdit(isPrivate: boolean) { return render( - {}} onBack={() => {}} /> + {}} onBack={() => {}} /> , ) diff --git a/packages/cli/src/tui/views/edit/edit-confirm-view.tsx b/packages/cli/src/tui/views/edit/edit-confirm-view.tsx index 5d32c399..1e77ba8c 100644 --- a/packages/cli/src/tui/views/edit/edit-confirm-view.tsx +++ b/packages/cli/src/tui/views/edit/edit-confirm-view.tsx @@ -1,4 +1,5 @@ import { ASSET_TYPE_COLORS } from '@agent-facets/brand' +import { type EditOperation, previewEditOperations } from '@agent-facets/engine' import { Box, Text } from 'ink' import { useEffect } from 'react' import { truncateDescription } from '../../components/asset-description.tsx' @@ -15,9 +16,18 @@ const ASSET_LABELS: Record = { agent: 'Agents', } -export function EditConfirmView({ onConfirm, onBack }: { onConfirm: () => void; onBack: () => void }) { +export function EditConfirmView({ + operations, + onConfirm, + onBack, +}: { + operations: EditOperation[] + onConfirm: () => void + onBack: () => void +}) { const { form } = useFormState() const { setFocusIds, focus, focusedId } = useFocusOrder() + const opLines = previewEditOperations(operations) useEffect(() => { setFocusIds(['edit-apply-btn', 'edit-back-btn']) @@ -78,6 +88,25 @@ export function EditConfirmView({ onConfirm, onBack }: { onConfirm: () => void; ) })} + + + File changes: + + {opLines.length === 0 ? ( + + (manifest only) + + ) : ( + opLines.map((line) => ( + + + {line.verb} {line.path} + + + )) + )} + +