@cleocode/core 2026.3.74 → 2026.4.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.
- package/README.md +1 -1
- package/dist/agents/agent-schema.d.ts.map +1 -1
- package/dist/agents/retry.js +26 -21
- package/dist/agents/retry.js.map +1 -1
- package/dist/cant/approval.d.ts +110 -0
- package/dist/cant/approval.d.ts.map +1 -0
- package/dist/cant/approval.js +185 -0
- package/dist/cant/approval.js.map +1 -0
- package/dist/cant/context-builder.d.ts +79 -0
- package/dist/cant/context-builder.d.ts.map +1 -0
- package/dist/cant/context-builder.js +117 -0
- package/dist/cant/context-builder.js.map +1 -0
- package/dist/cant/discretion.d.ts +95 -0
- package/dist/cant/discretion.d.ts.map +1 -0
- package/dist/cant/discretion.js +116 -0
- package/dist/cant/discretion.js.map +1 -0
- package/dist/cant/index.d.ts +25 -0
- package/dist/cant/index.d.ts.map +1 -0
- package/dist/cant/index.js +23 -0
- package/dist/cant/index.js.map +1 -0
- package/dist/cant/parallel-runner.d.ts +38 -0
- package/dist/cant/parallel-runner.d.ts.map +1 -0
- package/dist/cant/parallel-runner.js +173 -0
- package/dist/cant/parallel-runner.js.map +1 -0
- package/dist/cant/types.d.ts +127 -0
- package/dist/cant/types.d.ts.map +1 -0
- package/dist/cant/types.js +11 -0
- package/dist/cant/types.js.map +1 -0
- package/dist/cant/workflow-executor.d.ts +105 -0
- package/dist/cant/workflow-executor.d.ts.map +1 -0
- package/dist/cant/workflow-executor.js +440 -0
- package/dist/cant/workflow-executor.js.map +1 -0
- package/dist/cleo.js +21 -1
- package/dist/cleo.js.map +1 -1
- package/dist/code/index.d.ts +10 -0
- package/dist/code/index.d.ts.map +1 -0
- package/dist/code/outline.d.ts +51 -0
- package/dist/code/outline.d.ts.map +1 -0
- package/dist/code/parser.d.ts +30 -0
- package/dist/code/parser.d.ts.map +1 -0
- package/dist/code/search.d.ts +42 -0
- package/dist/code/search.d.ts.map +1 -0
- package/dist/code/unfold.d.ts +44 -0
- package/dist/code/unfold.d.ts.map +1 -0
- package/dist/conduit/conduit-client.d.ts +35 -0
- package/dist/conduit/conduit-client.d.ts.map +1 -0
- package/dist/conduit/conduit-client.js +94 -0
- package/dist/conduit/conduit-client.js.map +1 -0
- package/dist/conduit/factory.d.ts +15 -0
- package/dist/conduit/factory.d.ts.map +1 -0
- package/dist/conduit/factory.js +35 -0
- package/dist/conduit/factory.js.map +1 -0
- package/dist/conduit/http-transport.d.ts +44 -0
- package/dist/conduit/http-transport.d.ts.map +1 -0
- package/dist/conduit/http-transport.js +165 -0
- package/dist/conduit/http-transport.js.map +1 -0
- package/dist/conduit/index.d.ts +15 -0
- package/dist/conduit/index.d.ts.map +1 -0
- package/dist/conduit/index.js +12 -0
- package/dist/conduit/index.js.map +1 -0
- package/dist/conduit/local-transport.d.ts +91 -0
- package/dist/conduit/local-transport.d.ts.map +1 -0
- package/dist/conduit/sse-transport.d.ts +68 -0
- package/dist/conduit/sse-transport.d.ts.map +1 -0
- package/dist/config.js +4 -3
- package/dist/config.js.map +1 -1
- package/dist/crypto/credentials.d.ts +40 -0
- package/dist/crypto/credentials.d.ts.map +1 -0
- package/dist/crypto/credentials.js +144 -0
- package/dist/crypto/credentials.js.map +1 -0
- package/dist/engine-result.d.ts +1 -1
- package/dist/engine-result.d.ts.map +1 -1
- package/dist/error-catalog.d.ts +1 -1
- package/dist/error-catalog.d.ts.map +1 -1
- package/dist/error-registry.d.ts +1 -1
- package/dist/error-registry.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/hooks/handlers/agent-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/agent-hooks.js +106 -0
- package/dist/hooks/handlers/agent-hooks.js.map +1 -0
- package/dist/hooks/handlers/context-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/context-hooks.js +111 -0
- package/dist/hooks/handlers/context-hooks.js.map +1 -0
- package/dist/hooks/handlers/error-hooks.d.ts +14 -5
- package/dist/hooks/handlers/error-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/error-hooks.js +15 -6
- package/dist/hooks/handlers/error-hooks.js.map +1 -1
- package/dist/hooks/handlers/file-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/file-hooks.js +35 -11
- package/dist/hooks/handlers/file-hooks.js.map +1 -1
- package/dist/hooks/handlers/handler-helpers.d.ts +41 -0
- package/dist/hooks/handlers/handler-helpers.d.ts.map +1 -0
- package/dist/hooks/handlers/handler-helpers.js +61 -0
- package/dist/hooks/handlers/handler-helpers.js.map +1 -0
- package/dist/hooks/handlers/index.js +10 -1
- package/dist/hooks/handlers/index.js.map +1 -1
- package/dist/hooks/handlers/mcp-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/mcp-hooks.js +88 -21
- package/dist/hooks/handlers/mcp-hooks.js.map +1 -1
- package/dist/hooks/handlers/session-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/session-hooks.js +5 -10
- package/dist/hooks/handlers/session-hooks.js.map +1 -1
- package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/task-hooks.js +5 -10
- package/dist/hooks/handlers/task-hooks.js.map +1 -1
- package/dist/hooks/handlers/work-capture-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/work-capture-hooks.js +165 -0
- package/dist/hooks/handlers/work-capture-hooks.js.map +1 -0
- package/dist/hooks/payload-schemas.js +83 -26
- package/dist/hooks/payload-schemas.js.map +1 -1
- package/dist/hooks/provider-hooks.js +37 -5
- package/dist/hooks/provider-hooks.js.map +1 -1
- package/dist/hooks/registry.js +76 -23
- package/dist/hooks/registry.js.map +1 -1
- package/dist/hooks/types.js +17 -13
- package/dist/hooks/types.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6452 -3371
- package/dist/index.js.map +4 -4
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +12 -0
- package/dist/init.js.map +1 -1
- package/dist/internal.d.ts +11 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +10 -0
- package/dist/internal.js.map +1 -1
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/tree-sitter-languages.d.ts +29 -0
- package/dist/lib/tree-sitter-languages.d.ts.map +1 -0
- package/dist/memory/brain-links.d.ts.map +1 -1
- package/dist/memory/brain-maintenance.d.ts +13 -0
- package/dist/memory/brain-maintenance.d.ts.map +1 -1
- package/dist/memory/brain-retrieval.d.ts +3 -0
- package/dist/memory/brain-retrieval.d.ts.map +1 -1
- package/dist/memory/brain-retrieval.js +5 -0
- package/dist/memory/brain-retrieval.js.map +1 -1
- package/dist/memory/decisions.d.ts.map +1 -1
- package/dist/mvi-helpers.d.ts +52 -0
- package/dist/mvi-helpers.d.ts.map +1 -0
- package/dist/mvi-helpers.js +74 -0
- package/dist/mvi-helpers.js.map +1 -0
- package/dist/nexus/index.js +2 -0
- package/dist/nexus/index.js.map +1 -1
- package/dist/nexus/workspace.d.ts.map +1 -1
- package/dist/nexus/workspace.js +355 -0
- package/dist/nexus/workspace.js.map +1 -0
- package/dist/orchestration/hierarchy.d.ts +32 -0
- package/dist/orchestration/hierarchy.d.ts.map +1 -0
- package/dist/orchestration/index.d.ts +1 -0
- package/dist/orchestration/index.d.ts.map +1 -1
- package/dist/output.d.ts +2 -2
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +40 -8
- package/dist/output.js.map +1 -1
- package/dist/pagination.d.ts +1 -1
- package/dist/pagination.d.ts.map +1 -1
- package/dist/sessions/find.d.ts +3 -0
- package/dist/sessions/find.d.ts.map +1 -1
- package/dist/sessions/find.js +3 -1
- package/dist/sessions/find.js.map +1 -1
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +11 -4
- package/dist/sessions/index.js.map +1 -1
- package/dist/sessions/snapshot.js +213 -0
- package/dist/sessions/snapshot.js.map +1 -0
- package/dist/store/agent-registry-accessor.d.ts +31 -0
- package/dist/store/agent-registry-accessor.d.ts.map +1 -0
- package/dist/store/agent-registry-accessor.js +169 -0
- package/dist/store/agent-registry-accessor.js.map +1 -0
- package/dist/store/converters.d.ts.map +1 -1
- package/dist/store/converters.js +2 -0
- package/dist/store/converters.js.map +1 -1
- package/dist/store/cross-db-cleanup.d.ts +34 -0
- package/dist/store/cross-db-cleanup.d.ts.map +1 -1
- package/dist/store/db-helpers.d.ts.map +1 -1
- package/dist/store/db-helpers.js +1 -0
- package/dist/store/db-helpers.js.map +1 -1
- package/dist/store/json.js +2 -2
- package/dist/store/safety-data-accessor.d.ts +7 -0
- package/dist/store/safety-data-accessor.d.ts.map +1 -1
- package/dist/store/safety-data-accessor.js +14 -0
- package/dist/store/safety-data-accessor.js.map +1 -1
- package/dist/store/signaldock-sqlite.d.ts +48 -0
- package/dist/store/signaldock-sqlite.d.ts.map +1 -0
- package/dist/store/signaldock-sqlite.js +178 -0
- package/dist/store/signaldock-sqlite.js.map +1 -0
- package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
- package/dist/store/sqlite-data-accessor.js +50 -0
- package/dist/store/sqlite-data-accessor.js.map +1 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +30 -1
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/task-store.d.ts.map +1 -1
- package/dist/store/task-store.js +2 -0
- package/dist/store/task-store.js.map +1 -1
- package/dist/store/tasks-schema.d.ts +16 -0
- package/dist/store/tasks-schema.d.ts.map +1 -1
- package/dist/store/tasks-schema.js +33 -0
- package/dist/store/tasks-schema.js.map +1 -1
- package/dist/store/validation-schemas.d.ts +32 -0
- package/dist/store/validation-schemas.d.ts.map +1 -1
- package/dist/system/health.d.ts +1 -1
- package/dist/system/health.d.ts.map +1 -1
- package/dist/system/health.js +35 -0
- package/dist/system/health.js.map +1 -1
- package/dist/task-work/index.d.ts.map +1 -1
- package/dist/task-work/index.js +8 -4
- package/dist/task-work/index.js.map +1 -1
- package/dist/tasks/complete.js +5 -2
- package/dist/tasks/complete.js.map +1 -1
- package/dist/tasks/find.d.ts +3 -0
- package/dist/tasks/find.d.ts.map +1 -1
- package/dist/tasks/find.js +7 -1
- package/dist/tasks/find.js.map +1 -1
- package/dist/tasks/list.d.ts +5 -2
- package/dist/tasks/list.d.ts.map +1 -1
- package/dist/tasks/list.js +9 -2
- package/dist/tasks/list.js.map +1 -1
- package/dist/tasks/show.d.ts +3 -0
- package/dist/tasks/show.d.ts.map +1 -1
- package/dist/tasks/show.js +2 -0
- package/dist/tasks/show.js.map +1 -1
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +15 -0
- package/dist/upgrade.js.map +1 -1
- package/migrations/drizzle-tasks/20260324000000_assignee-column/migration.sql +6 -0
- package/migrations/drizzle-tasks/20260324000000_assignee-column/snapshot.json +9 -0
- package/migrations/drizzle-tasks/20260327000000_agent-credentials/migration.sql +23 -0
- package/package.json +17 -7
- package/src/__tests__/cli-parity.test.js +11 -1
- package/src/__tests__/cli-parity.test.js.map +1 -1
- package/src/__tests__/cli-parity.test.ts +17 -1
- package/src/__tests__/human-output.test.js +11 -1
- package/src/__tests__/human-output.test.js.map +1 -1
- package/src/__tests__/human-output.test.ts +18 -1
- package/src/__tests__/injection-chain.test.js +3 -2
- package/src/__tests__/injection-chain.test.js.map +1 -1
- package/src/__tests__/injection-mvi-tiers.test.d.ts +2 -2
- package/src/__tests__/injection-mvi-tiers.test.js +15 -15
- package/src/__tests__/injection-mvi-tiers.test.js.map +1 -1
- package/src/__tests__/lafs-conformance.test.d.ts +1 -1
- package/src/__tests__/lafs-conformance.test.js +2 -2
- package/src/__tests__/sharing.test.js +19 -0
- package/src/__tests__/sharing.test.js.map +1 -1
- package/src/agents/__tests__/agent-registry.test.d.ts +12 -0
- package/src/agents/__tests__/agent-registry.test.d.ts.map +1 -0
- package/src/agents/__tests__/agent-registry.test.js +262 -0
- package/src/agents/__tests__/agent-registry.test.js.map +1 -0
- package/src/agents/__tests__/execution-learning.test.d.ts +14 -0
- package/src/agents/__tests__/execution-learning.test.d.ts.map +1 -0
- package/src/agents/__tests__/execution-learning.test.js +533 -0
- package/src/agents/__tests__/execution-learning.test.js.map +1 -0
- package/src/agents/__tests__/health-monitor.test.d.ts +10 -0
- package/src/agents/__tests__/health-monitor.test.d.ts.map +1 -0
- package/src/agents/__tests__/health-monitor.test.js +259 -0
- package/src/agents/__tests__/health-monitor.test.js.map +1 -0
- package/src/agents/__tests__/registry.test.js +27 -2
- package/src/agents/__tests__/registry.test.js.map +1 -1
- package/src/agents/agent-schema.ts +2 -5
- package/src/cant/__tests__/cant-agent-parse.test.ts +94 -0
- package/src/cant/approval.ts +218 -0
- package/src/cant/context-builder.ts +135 -0
- package/src/cant/discretion.ts +149 -0
- package/src/cant/index.ts +58 -0
- package/src/cant/parallel-runner.ts +205 -0
- package/src/cant/types.ts +158 -0
- package/src/cant/workflow-executor.ts +618 -0
- package/src/code/index.ts +10 -0
- package/src/code/outline.ts +214 -0
- package/src/code/parser.ts +299 -0
- package/src/code/search.ts +173 -0
- package/src/code/unfold.ts +204 -0
- package/src/conduit/__tests__/dual-api-e2e.test.ts +212 -0
- package/src/conduit/__tests__/local-credential-flow.test.ts +230 -0
- package/src/conduit/__tests__/local-transport.test.ts +320 -0
- package/src/conduit/__tests__/sse-transport.test.ts +344 -0
- package/src/conduit/conduit-client.ts +123 -0
- package/src/conduit/factory.ts +49 -0
- package/src/conduit/http-transport.ts +201 -0
- package/src/conduit/index.ts +15 -0
- package/src/conduit/local-transport.ts +309 -0
- package/src/conduit/sse-transport.ts +382 -0
- package/src/crypto/credentials.ts +166 -0
- package/src/engine-result.ts +1 -1
- package/src/error-catalog.ts +1 -1
- package/src/error-registry.ts +1 -1
- package/src/errors.ts +1 -1
- package/src/hooks/handlers/__tests__/hook-automation-e2e.test.d.ts +13 -0
- package/src/hooks/handlers/__tests__/hook-automation-e2e.test.d.ts.map +1 -0
- package/src/hooks/handlers/__tests__/hook-automation-e2e.test.js +501 -0
- package/src/hooks/handlers/__tests__/hook-automation-e2e.test.js.map +1 -0
- package/src/hooks/handlers/agent-hooks.ts +1 -30
- package/src/hooks/handlers/context-hooks.ts +1 -30
- package/src/hooks/handlers/error-hooks.ts +14 -5
- package/src/hooks/handlers/file-hooks.ts +1 -6
- package/src/hooks/handlers/handler-helpers.ts +62 -0
- package/src/hooks/handlers/mcp-hooks.ts +2 -14
- package/src/hooks/handlers/session-hooks.ts +1 -6
- package/src/hooks/handlers/task-hooks.ts +1 -6
- package/src/hooks/handlers/work-capture-hooks.ts +1 -10
- package/src/index.ts +12 -1
- package/src/init.ts +12 -0
- package/src/intelligence/__tests__/adaptive-validation.test.d.ts +11 -0
- package/src/intelligence/__tests__/adaptive-validation.test.d.ts.map +1 -0
- package/src/intelligence/__tests__/adaptive-validation.test.js +517 -0
- package/src/intelligence/__tests__/adaptive-validation.test.js.map +1 -0
- package/src/intelligence/__tests__/impact.test.d.ts +1 -0
- package/src/intelligence/__tests__/impact.test.d.ts.map +1 -1
- package/src/intelligence/__tests__/impact.test.js +132 -1
- package/src/intelligence/__tests__/impact.test.js.map +1 -1
- package/src/internal.ts +22 -0
- package/src/lib/__tests__/retry.test.d.ts +7 -0
- package/src/lib/__tests__/retry.test.d.ts.map +1 -0
- package/src/lib/__tests__/retry.test.js +225 -0
- package/src/lib/__tests__/retry.test.js.map +1 -0
- package/src/lib/index.ts +8 -0
- package/src/lib/tree-sitter-languages.ts +88 -0
- package/src/lifecycle/__tests__/chain-store.test.js +6 -0
- package/src/lifecycle/__tests__/chain-store.test.js.map +1 -1
- package/src/lifecycle/__tests__/tessera-engine.test.js +52 -0
- package/src/lifecycle/__tests__/tessera-engine.test.js.map +1 -1
- package/src/memory/__tests__/brain-automation.test.d.ts +11 -0
- package/src/memory/__tests__/brain-automation.test.d.ts.map +1 -0
- package/src/memory/__tests__/brain-automation.test.js +730 -0
- package/src/memory/__tests__/brain-automation.test.js.map +1 -0
- package/src/memory/__tests__/brain-links.test.ts +14 -0
- package/src/memory/__tests__/brain-retrieval.test.ts +10 -0
- package/src/memory/__tests__/session-memory.test.ts +17 -0
- package/src/memory/brain-links.ts +17 -0
- package/src/memory/brain-maintenance.ts +33 -1
- package/src/memory/brain-retrieval.ts +27 -2
- package/src/memory/decisions.ts +18 -2
- package/src/mvi-helpers.ts +81 -0
- package/src/nexus/workspace.ts +19 -7
- package/src/orchestration/hierarchy.ts +202 -0
- package/src/orchestration/index.ts +1 -0
- package/src/output.ts +43 -10
- package/src/pagination.ts +1 -1
- package/src/sessions/__tests__/session-edge-cases.test.js +20 -1
- package/src/sessions/__tests__/session-edge-cases.test.js.map +1 -1
- package/src/sessions/__tests__/session-find.test.js +1 -1
- package/src/sessions/__tests__/session-find.test.js.map +1 -1
- package/src/sessions/__tests__/session-find.test.ts +1 -1
- package/src/sessions/find.ts +6 -1
- package/src/sessions/index.ts +9 -0
- package/src/store/__tests__/migration-safety.test.js +3 -0
- package/src/store/__tests__/migration-safety.test.js.map +1 -1
- package/src/store/__tests__/session-store.test.js +128 -1
- package/src/store/__tests__/session-store.test.js.map +1 -1
- package/src/store/__tests__/task-store.test.js +18 -1
- package/src/store/__tests__/task-store.test.js.map +1 -1
- package/src/store/__tests__/test-db-helper.d.ts.map +1 -1
- package/src/store/__tests__/test-db-helper.js +12 -0
- package/src/store/__tests__/test-db-helper.js.map +1 -1
- package/src/store/agent-registry-accessor.ts +375 -0
- package/src/store/converters.ts +2 -0
- package/src/store/cross-db-cleanup.ts +175 -1
- package/src/store/db-helpers.ts +1 -0
- package/src/store/safety-data-accessor.ts +23 -0
- package/src/store/signaldock-sqlite.ts +429 -0
- package/src/store/sqlite-data-accessor.ts +72 -0
- package/src/store/sqlite.ts +4 -1
- package/src/store/task-store.ts +9 -1
- package/src/store/tasks-schema.ts +7 -0
- package/src/system/__tests__/health.test.ts +2 -2
- package/src/system/health.ts +54 -2
- package/src/task-work/index.ts +5 -0
- package/src/tasks/__tests__/add.test.js +19 -1
- package/src/tasks/__tests__/add.test.js.map +1 -1
- package/src/tasks/__tests__/assignee.test.d.ts +14 -0
- package/src/tasks/__tests__/assignee.test.d.ts.map +1 -0
- package/src/tasks/__tests__/assignee.test.js +125 -0
- package/src/tasks/__tests__/assignee.test.js.map +1 -0
- package/src/tasks/__tests__/assignee.test.ts +162 -0
- package/src/tasks/__tests__/complete-unblocks.test.js +13 -1
- package/src/tasks/__tests__/complete-unblocks.test.js.map +1 -1
- package/src/tasks/__tests__/complete.test.js +28 -7
- package/src/tasks/__tests__/complete.test.js.map +1 -1
- package/src/tasks/__tests__/epic-enforcement.test.d.ts +15 -0
- package/src/tasks/__tests__/epic-enforcement.test.d.ts.map +1 -0
- package/src/tasks/__tests__/epic-enforcement.test.js +669 -0
- package/src/tasks/__tests__/epic-enforcement.test.js.map +1 -0
- package/src/tasks/__tests__/hierarchy-policy.test.js +5 -0
- package/src/tasks/__tests__/hierarchy-policy.test.js.map +1 -1
- package/src/tasks/__tests__/minimal-test.test.d.ts +2 -0
- package/src/tasks/__tests__/minimal-test.test.d.ts.map +1 -0
- package/src/tasks/__tests__/minimal-test.test.js +25 -0
- package/src/tasks/__tests__/minimal-test.test.js.map +1 -0
- package/src/tasks/__tests__/pipeline-stage.test.d.ts +14 -0
- package/src/tasks/__tests__/pipeline-stage.test.d.ts.map +1 -0
- package/src/tasks/__tests__/pipeline-stage.test.js +277 -0
- package/src/tasks/__tests__/pipeline-stage.test.js.map +1 -0
- package/src/tasks/__tests__/update.test.js +43 -6
- package/src/tasks/__tests__/update.test.js.map +1 -1
- package/src/tasks/find.ts +11 -1
- package/src/tasks/list.ts +14 -3
- package/src/tasks/show.ts +6 -0
- package/src/upgrade.ts +16 -0
- package/dist/tasks/reparent.d.ts +0 -38
- package/dist/tasks/reparent.d.ts.map +0 -1
- package/dist/ui/injection-legacy.d.ts +0 -26
- package/dist/ui/injection-legacy.d.ts.map +0 -1
- package/dist/ui/injection-legacy.js +0 -42
- package/dist/ui/injection-legacy.js.map +0 -1
- package/src/signaldock/__tests__/claude-code-transport.test.d.ts +0 -7
- package/src/signaldock/__tests__/claude-code-transport.test.d.ts.map +0 -1
- package/src/signaldock/__tests__/claude-code-transport.test.js +0 -147
- package/src/signaldock/__tests__/claude-code-transport.test.js.map +0 -1
- package/src/signaldock/__tests__/claude-code-transport.test.ts +0 -180
- package/src/signaldock/__tests__/factory.test.d.ts +0 -7
- package/src/signaldock/__tests__/factory.test.d.ts.map +0 -1
- package/src/signaldock/__tests__/factory.test.js +0 -55
- package/src/signaldock/__tests__/factory.test.js.map +0 -1
- package/src/signaldock/__tests__/factory.test.ts +0 -61
- package/src/signaldock/__tests__/signaldock-transport.test.d.ts +0 -9
- package/src/signaldock/__tests__/signaldock-transport.test.d.ts.map +0 -1
- package/src/signaldock/__tests__/signaldock-transport.test.js +0 -321
- package/src/signaldock/__tests__/signaldock-transport.test.js.map +0 -1
- package/src/signaldock/__tests__/signaldock-transport.test.ts +0 -421
- package/src/signaldock/claude-code-transport.ts +0 -137
- package/src/signaldock/factory.ts +0 -39
- package/src/signaldock/index.ts +0 -28
- package/src/signaldock/signaldock-transport.ts +0 -194
- package/src/signaldock/transport.ts +0 -78
- package/src/signaldock/types.ts +0 -100
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for epic lifecycle pipeline enforcement (T062).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - validateEpicCreation: min-5 AC, description required, mode gating
|
|
6
|
+
* - validateChildStageCeiling: child stage must not exceed epic's stage
|
|
7
|
+
* - validateEpicStageAdvancement: epic blocked by in-flight children
|
|
8
|
+
* - findEpicAncestor: correct ancestor traversal
|
|
9
|
+
* - Integration via addTask / updateTask
|
|
10
|
+
*
|
|
11
|
+
* @task T062
|
|
12
|
+
* @epic T056
|
|
13
|
+
*/
|
|
14
|
+
import { unlink, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
17
|
+
import { createTestDb } from '../../store/__tests__/test-db-helper.js';
|
|
18
|
+
// Epic enforcement tests NEED enforcement active — temporarily clear VITEST
|
|
19
|
+
const savedVitest = process.env.VITEST;
|
|
20
|
+
beforeAll(() => {
|
|
21
|
+
delete process.env.VITEST;
|
|
22
|
+
});
|
|
23
|
+
afterAll(() => {
|
|
24
|
+
if (savedVitest)
|
|
25
|
+
process.env.VITEST = savedVitest;
|
|
26
|
+
});
|
|
27
|
+
import { addTask } from '../add.js';
|
|
28
|
+
import { EPIC_MIN_AC, findEpicAncestor, getLifecycleMode, validateChildStageCeiling, validateEpicCreation, validateEpicStageAdvancement, } from '../epic-enforcement.js';
|
|
29
|
+
import { updateTask } from '../update.js';
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Config helpers
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
/** Config that disables session and acceptance enforcement for test isolation. */
|
|
34
|
+
function makeConfig(lifecycleMode = 'strict') {
|
|
35
|
+
return JSON.stringify({
|
|
36
|
+
lifecycle: { mode: lifecycleMode },
|
|
37
|
+
enforcement: {
|
|
38
|
+
session: { requiredForMutate: false },
|
|
39
|
+
acceptance: { mode: 'off' },
|
|
40
|
+
},
|
|
41
|
+
verification: { enabled: false },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Unit: EPIC_MIN_AC
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
describe('EPIC_MIN_AC constant', () => {
|
|
48
|
+
it('is 5', () => {
|
|
49
|
+
expect(EPIC_MIN_AC).toBe(5);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Unit: getLifecycleMode
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
describe('getLifecycleMode', () => {
|
|
56
|
+
let env;
|
|
57
|
+
beforeEach(async () => {
|
|
58
|
+
env = await createTestDb();
|
|
59
|
+
});
|
|
60
|
+
afterEach(async () => {
|
|
61
|
+
await env.cleanup();
|
|
62
|
+
});
|
|
63
|
+
it('returns strict by default when no config', async () => {
|
|
64
|
+
// Remove the config.json written by createTestDb() so getLifecycleMode
|
|
65
|
+
// sees no project config and falls back to the 'strict' default.
|
|
66
|
+
await unlink(join(env.cleoDir, 'config.json'));
|
|
67
|
+
const mode = await getLifecycleMode(env.tempDir);
|
|
68
|
+
expect(mode).toBe('strict');
|
|
69
|
+
});
|
|
70
|
+
it('returns advisory from config', async () => {
|
|
71
|
+
await writeFile(join(env.cleoDir, 'config.json'), makeConfig('advisory'));
|
|
72
|
+
const mode = await getLifecycleMode(env.tempDir);
|
|
73
|
+
expect(mode).toBe('advisory');
|
|
74
|
+
});
|
|
75
|
+
it('returns off from config', async () => {
|
|
76
|
+
await writeFile(join(env.cleoDir, 'config.json'), makeConfig('off'));
|
|
77
|
+
const mode = await getLifecycleMode(env.tempDir);
|
|
78
|
+
expect(mode).toBe('off');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Unit: validateEpicCreation
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
describe('validateEpicCreation (strict mode)', () => {
|
|
85
|
+
let env;
|
|
86
|
+
beforeEach(async () => {
|
|
87
|
+
env = await createTestDb();
|
|
88
|
+
await writeFile(join(env.cleoDir, 'config.json'), makeConfig('strict'));
|
|
89
|
+
});
|
|
90
|
+
afterEach(async () => {
|
|
91
|
+
await env.cleanup();
|
|
92
|
+
});
|
|
93
|
+
it('accepts when 5 AC items and description are present', async () => {
|
|
94
|
+
const result = await validateEpicCreation({
|
|
95
|
+
acceptance: ['ac1', 'ac2', 'ac3', 'ac4', 'ac5'],
|
|
96
|
+
description: 'Some completion criteria',
|
|
97
|
+
}, env.tempDir);
|
|
98
|
+
expect(result.valid).toBe(true);
|
|
99
|
+
expect(result.warning).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
it('throws when fewer than 5 AC items', async () => {
|
|
102
|
+
await expect(validateEpicCreation({
|
|
103
|
+
acceptance: ['ac1', 'ac2', 'ac3', 'ac4'],
|
|
104
|
+
description: 'Some completion criteria',
|
|
105
|
+
}, env.tempDir)).rejects.toThrow(/5 acceptance criteria/);
|
|
106
|
+
});
|
|
107
|
+
it('throws when description is empty', async () => {
|
|
108
|
+
await expect(validateEpicCreation({
|
|
109
|
+
acceptance: ['ac1', 'ac2', 'ac3', 'ac4', 'ac5'],
|
|
110
|
+
description: '',
|
|
111
|
+
}, env.tempDir)).rejects.toThrow(/non-empty description/);
|
|
112
|
+
});
|
|
113
|
+
it('throws when both AC count and description are insufficient', async () => {
|
|
114
|
+
await expect(validateEpicCreation({ acceptance: [], description: ' ' }, env.tempDir)).rejects.toThrow();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe('validateEpicCreation (advisory mode)', () => {
|
|
118
|
+
let env;
|
|
119
|
+
beforeEach(async () => {
|
|
120
|
+
env = await createTestDb();
|
|
121
|
+
await writeFile(join(env.cleoDir, 'config.json'), makeConfig('advisory'));
|
|
122
|
+
});
|
|
123
|
+
afterEach(async () => {
|
|
124
|
+
await env.cleanup();
|
|
125
|
+
});
|
|
126
|
+
it('does not throw on violation, returns warning', async () => {
|
|
127
|
+
const result = await validateEpicCreation({ acceptance: ['ac1'], description: 'OK' }, env.tempDir);
|
|
128
|
+
expect(result.valid).toBe(true);
|
|
129
|
+
expect(result.warning).toMatch(/5 acceptance criteria/);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('validateEpicCreation (off mode)', () => {
|
|
133
|
+
let env;
|
|
134
|
+
beforeEach(async () => {
|
|
135
|
+
env = await createTestDb();
|
|
136
|
+
await writeFile(join(env.cleoDir, 'config.json'), makeConfig('off'));
|
|
137
|
+
});
|
|
138
|
+
afterEach(async () => {
|
|
139
|
+
await env.cleanup();
|
|
140
|
+
});
|
|
141
|
+
it('skips all checks and returns valid', async () => {
|
|
142
|
+
const result = await validateEpicCreation({ acceptance: [], description: '' }, env.tempDir);
|
|
143
|
+
expect(result.valid).toBe(true);
|
|
144
|
+
expect(result.warning).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Unit: validateChildStageCeiling
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
describe('validateChildStageCeiling (strict)', () => {
|
|
151
|
+
let env;
|
|
152
|
+
let accessor;
|
|
153
|
+
beforeEach(async () => {
|
|
154
|
+
env = await createTestDb();
|
|
155
|
+
accessor = env.accessor;
|
|
156
|
+
await writeFile(join(env.cleoDir, 'config.json'), makeConfig('strict'));
|
|
157
|
+
});
|
|
158
|
+
afterEach(async () => {
|
|
159
|
+
await env.cleanup();
|
|
160
|
+
});
|
|
161
|
+
async function seedEpicAtStage(stage) {
|
|
162
|
+
const now = new Date().toISOString();
|
|
163
|
+
await accessor.upsertSingleTask({
|
|
164
|
+
id: 'T001',
|
|
165
|
+
title: 'Epic',
|
|
166
|
+
description: 'Epic description',
|
|
167
|
+
status: 'pending',
|
|
168
|
+
priority: 'medium',
|
|
169
|
+
type: 'epic',
|
|
170
|
+
pipelineStage: stage,
|
|
171
|
+
createdAt: now,
|
|
172
|
+
updatedAt: now,
|
|
173
|
+
});
|
|
174
|
+
return 'T001';
|
|
175
|
+
}
|
|
176
|
+
it('allows child stage equal to epic stage', async () => {
|
|
177
|
+
const epicId = await seedEpicAtStage('specification');
|
|
178
|
+
const result = await validateChildStageCeiling({ childStage: 'specification', epicId }, accessor, env.tempDir);
|
|
179
|
+
expect(result.valid).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
it('allows child stage below epic stage', async () => {
|
|
182
|
+
const epicId = await seedEpicAtStage('implementation');
|
|
183
|
+
const result = await validateChildStageCeiling({ childStage: 'research', epicId }, accessor, env.tempDir);
|
|
184
|
+
expect(result.valid).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
it('throws when child stage exceeds epic stage', async () => {
|
|
187
|
+
const epicId = await seedEpicAtStage('research');
|
|
188
|
+
await expect(validateChildStageCeiling({ childStage: 'implementation', epicId }, accessor, env.tempDir)).rejects.toThrow(/cannot be at pipeline stage/);
|
|
189
|
+
});
|
|
190
|
+
it('skips check when epic ID does not exist', async () => {
|
|
191
|
+
const result = await validateChildStageCeiling({ childStage: 'testing', epicId: 'T999' }, accessor, env.tempDir);
|
|
192
|
+
expect(result.valid).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
describe('validateChildStageCeiling (advisory)', () => {
|
|
196
|
+
let env;
|
|
197
|
+
let accessor;
|
|
198
|
+
beforeEach(async () => {
|
|
199
|
+
env = await createTestDb();
|
|
200
|
+
accessor = env.accessor;
|
|
201
|
+
await writeFile(join(env.cleoDir, 'config.json'), makeConfig('advisory'));
|
|
202
|
+
});
|
|
203
|
+
afterEach(async () => {
|
|
204
|
+
await env.cleanup();
|
|
205
|
+
});
|
|
206
|
+
it('returns warning instead of throwing', async () => {
|
|
207
|
+
const now = new Date().toISOString();
|
|
208
|
+
await accessor.upsertSingleTask({
|
|
209
|
+
id: 'T001',
|
|
210
|
+
title: 'Epic',
|
|
211
|
+
description: 'desc',
|
|
212
|
+
status: 'pending',
|
|
213
|
+
priority: 'medium',
|
|
214
|
+
type: 'epic',
|
|
215
|
+
pipelineStage: 'research',
|
|
216
|
+
createdAt: now,
|
|
217
|
+
updatedAt: now,
|
|
218
|
+
});
|
|
219
|
+
const result = await validateChildStageCeiling({ childStage: 'testing', epicId: 'T001' }, accessor, env.tempDir);
|
|
220
|
+
expect(result.valid).toBe(true);
|
|
221
|
+
expect(result.warning).toMatch(/cannot be at pipeline stage/);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Unit: validateEpicStageAdvancement
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
describe('validateEpicStageAdvancement (strict)', () => {
|
|
228
|
+
let env;
|
|
229
|
+
let accessor;
|
|
230
|
+
beforeEach(async () => {
|
|
231
|
+
env = await createTestDb();
|
|
232
|
+
accessor = env.accessor;
|
|
233
|
+
await writeFile(join(env.cleoDir, 'config.json'), makeConfig('strict'));
|
|
234
|
+
});
|
|
235
|
+
afterEach(async () => {
|
|
236
|
+
await env.cleanup();
|
|
237
|
+
});
|
|
238
|
+
it('allows advancement when no children exist', async () => {
|
|
239
|
+
const result = await validateEpicStageAdvancement({ epicId: 'T001', currentStage: 'research', newStage: 'implementation' }, accessor, env.tempDir);
|
|
240
|
+
expect(result.valid).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
it('allows advancement when all children are done', async () => {
|
|
243
|
+
const now = new Date().toISOString();
|
|
244
|
+
// Seed epic
|
|
245
|
+
await accessor.upsertSingleTask({
|
|
246
|
+
id: 'T001',
|
|
247
|
+
title: 'Epic',
|
|
248
|
+
description: 'Epic',
|
|
249
|
+
status: 'pending',
|
|
250
|
+
priority: 'medium',
|
|
251
|
+
type: 'epic',
|
|
252
|
+
pipelineStage: 'research',
|
|
253
|
+
createdAt: now,
|
|
254
|
+
updatedAt: now,
|
|
255
|
+
});
|
|
256
|
+
// Seed done child at research
|
|
257
|
+
await accessor.upsertSingleTask({
|
|
258
|
+
id: 'T002',
|
|
259
|
+
title: 'Child',
|
|
260
|
+
description: 'Child',
|
|
261
|
+
status: 'done',
|
|
262
|
+
priority: 'medium',
|
|
263
|
+
type: 'task',
|
|
264
|
+
parentId: 'T001',
|
|
265
|
+
pipelineStage: 'research',
|
|
266
|
+
createdAt: now,
|
|
267
|
+
updatedAt: now,
|
|
268
|
+
});
|
|
269
|
+
const result = await validateEpicStageAdvancement({ epicId: 'T001', currentStage: 'research', newStage: 'implementation' }, accessor, env.tempDir);
|
|
270
|
+
expect(result.valid).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
it('throws when child is in-flight at current stage', async () => {
|
|
273
|
+
const now = new Date().toISOString();
|
|
274
|
+
await accessor.upsertSingleTask({
|
|
275
|
+
id: 'T001',
|
|
276
|
+
title: 'Epic',
|
|
277
|
+
description: 'Epic',
|
|
278
|
+
status: 'pending',
|
|
279
|
+
priority: 'medium',
|
|
280
|
+
type: 'epic',
|
|
281
|
+
pipelineStage: 'research',
|
|
282
|
+
createdAt: now,
|
|
283
|
+
updatedAt: now,
|
|
284
|
+
});
|
|
285
|
+
await accessor.upsertSingleTask({
|
|
286
|
+
id: 'T002',
|
|
287
|
+
title: 'Child',
|
|
288
|
+
description: 'Child',
|
|
289
|
+
status: 'active',
|
|
290
|
+
priority: 'medium',
|
|
291
|
+
type: 'task',
|
|
292
|
+
parentId: 'T001',
|
|
293
|
+
pipelineStage: 'research',
|
|
294
|
+
createdAt: now,
|
|
295
|
+
updatedAt: now,
|
|
296
|
+
});
|
|
297
|
+
await expect(validateEpicStageAdvancement({ epicId: 'T001', currentStage: 'research', newStage: 'implementation' }, accessor, env.tempDir)).rejects.toThrow(/cannot advance/);
|
|
298
|
+
});
|
|
299
|
+
it('ignores children at a different (later) stage', async () => {
|
|
300
|
+
const now = new Date().toISOString();
|
|
301
|
+
await accessor.upsertSingleTask({
|
|
302
|
+
id: 'T001',
|
|
303
|
+
title: 'Epic',
|
|
304
|
+
description: 'Epic',
|
|
305
|
+
status: 'pending',
|
|
306
|
+
priority: 'medium',
|
|
307
|
+
type: 'epic',
|
|
308
|
+
pipelineStage: 'research',
|
|
309
|
+
createdAt: now,
|
|
310
|
+
updatedAt: now,
|
|
311
|
+
});
|
|
312
|
+
// Child is at implementation (not at current stage 'research')
|
|
313
|
+
await accessor.upsertSingleTask({
|
|
314
|
+
id: 'T002',
|
|
315
|
+
title: 'Child',
|
|
316
|
+
description: 'Child',
|
|
317
|
+
status: 'active',
|
|
318
|
+
priority: 'medium',
|
|
319
|
+
type: 'task',
|
|
320
|
+
parentId: 'T001',
|
|
321
|
+
pipelineStage: 'implementation',
|
|
322
|
+
createdAt: now,
|
|
323
|
+
updatedAt: now,
|
|
324
|
+
});
|
|
325
|
+
const result = await validateEpicStageAdvancement({ epicId: 'T001', currentStage: 'research', newStage: 'implementation' }, accessor, env.tempDir);
|
|
326
|
+
expect(result.valid).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
it('ignores cancelled children at current stage', async () => {
|
|
329
|
+
const now = new Date().toISOString();
|
|
330
|
+
await accessor.upsertSingleTask({
|
|
331
|
+
id: 'T001',
|
|
332
|
+
title: 'Epic',
|
|
333
|
+
description: 'Epic',
|
|
334
|
+
status: 'pending',
|
|
335
|
+
priority: 'medium',
|
|
336
|
+
type: 'epic',
|
|
337
|
+
pipelineStage: 'research',
|
|
338
|
+
createdAt: now,
|
|
339
|
+
updatedAt: now,
|
|
340
|
+
});
|
|
341
|
+
await accessor.upsertSingleTask({
|
|
342
|
+
id: 'T002',
|
|
343
|
+
title: 'Child',
|
|
344
|
+
description: 'Child',
|
|
345
|
+
status: 'cancelled',
|
|
346
|
+
priority: 'medium',
|
|
347
|
+
type: 'task',
|
|
348
|
+
parentId: 'T001',
|
|
349
|
+
pipelineStage: 'research',
|
|
350
|
+
createdAt: now,
|
|
351
|
+
updatedAt: now,
|
|
352
|
+
});
|
|
353
|
+
const result = await validateEpicStageAdvancement({ epicId: 'T001', currentStage: 'research', newStage: 'implementation' }, accessor, env.tempDir);
|
|
354
|
+
expect(result.valid).toBe(true);
|
|
355
|
+
});
|
|
356
|
+
it('is no-op for same-stage (no advancement)', async () => {
|
|
357
|
+
const result = await validateEpicStageAdvancement({ epicId: 'T001', currentStage: 'research', newStage: 'research' }, accessor, env.tempDir);
|
|
358
|
+
expect(result.valid).toBe(true);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
describe('validateEpicStageAdvancement (advisory)', () => {
|
|
362
|
+
let env;
|
|
363
|
+
let accessor;
|
|
364
|
+
beforeEach(async () => {
|
|
365
|
+
env = await createTestDb();
|
|
366
|
+
accessor = env.accessor;
|
|
367
|
+
await writeFile(join(env.cleoDir, 'config.json'), makeConfig('advisory'));
|
|
368
|
+
});
|
|
369
|
+
afterEach(async () => {
|
|
370
|
+
await env.cleanup();
|
|
371
|
+
});
|
|
372
|
+
it('returns warning instead of throwing when blocked', async () => {
|
|
373
|
+
const now = new Date().toISOString();
|
|
374
|
+
await accessor.upsertSingleTask({
|
|
375
|
+
id: 'T001',
|
|
376
|
+
title: 'Epic',
|
|
377
|
+
description: 'Epic',
|
|
378
|
+
status: 'pending',
|
|
379
|
+
priority: 'medium',
|
|
380
|
+
type: 'epic',
|
|
381
|
+
pipelineStage: 'research',
|
|
382
|
+
createdAt: now,
|
|
383
|
+
updatedAt: now,
|
|
384
|
+
});
|
|
385
|
+
await accessor.upsertSingleTask({
|
|
386
|
+
id: 'T002',
|
|
387
|
+
title: 'Child',
|
|
388
|
+
description: 'Child',
|
|
389
|
+
status: 'pending',
|
|
390
|
+
priority: 'medium',
|
|
391
|
+
type: 'task',
|
|
392
|
+
parentId: 'T001',
|
|
393
|
+
pipelineStage: 'research',
|
|
394
|
+
createdAt: now,
|
|
395
|
+
updatedAt: now,
|
|
396
|
+
});
|
|
397
|
+
const result = await validateEpicStageAdvancement({ epicId: 'T001', currentStage: 'research', newStage: 'implementation' }, accessor, env.tempDir);
|
|
398
|
+
expect(result.valid).toBe(true);
|
|
399
|
+
expect(result.warning).toMatch(/cannot advance/);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
// Unit: findEpicAncestor
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
describe('findEpicAncestor', () => {
|
|
406
|
+
let env;
|
|
407
|
+
let accessor;
|
|
408
|
+
beforeEach(async () => {
|
|
409
|
+
env = await createTestDb();
|
|
410
|
+
accessor = env.accessor;
|
|
411
|
+
});
|
|
412
|
+
afterEach(async () => {
|
|
413
|
+
await env.cleanup();
|
|
414
|
+
});
|
|
415
|
+
it('returns null when task has no ancestors', async () => {
|
|
416
|
+
const result = await findEpicAncestor('T999', accessor);
|
|
417
|
+
expect(result).toBeNull();
|
|
418
|
+
});
|
|
419
|
+
it('finds a direct epic parent', async () => {
|
|
420
|
+
const now = new Date().toISOString();
|
|
421
|
+
await accessor.upsertSingleTask({
|
|
422
|
+
id: 'T001',
|
|
423
|
+
title: 'Epic',
|
|
424
|
+
description: 'Epic',
|
|
425
|
+
status: 'pending',
|
|
426
|
+
priority: 'medium',
|
|
427
|
+
type: 'epic',
|
|
428
|
+
pipelineStage: 'research',
|
|
429
|
+
createdAt: now,
|
|
430
|
+
updatedAt: now,
|
|
431
|
+
});
|
|
432
|
+
await accessor.upsertSingleTask({
|
|
433
|
+
id: 'T002',
|
|
434
|
+
title: 'Child',
|
|
435
|
+
description: 'Child',
|
|
436
|
+
status: 'pending',
|
|
437
|
+
priority: 'medium',
|
|
438
|
+
type: 'task',
|
|
439
|
+
parentId: 'T001',
|
|
440
|
+
pipelineStage: 'research',
|
|
441
|
+
createdAt: now,
|
|
442
|
+
updatedAt: now,
|
|
443
|
+
});
|
|
444
|
+
// findEpicAncestor takes the task whose ancestors to walk.
|
|
445
|
+
// For T002's parent T001, we pass T001 to check its ancestors, OR
|
|
446
|
+
// we pass T002 to check ancestors of T002 (which includes T001).
|
|
447
|
+
const epic = await findEpicAncestor('T001', accessor);
|
|
448
|
+
// T001 has no ancestors (root level), so no epic ancestor
|
|
449
|
+
expect(epic).toBeNull();
|
|
450
|
+
// For T002's parent: we already know T001 is the epic, so we pass parentId=T001
|
|
451
|
+
// and check if T001 itself is an epic — that's handled in add.ts separately.
|
|
452
|
+
// findEpicAncestor walks ancestors of the supplied ID.
|
|
453
|
+
const fromChild = await findEpicAncestor('T002', accessor);
|
|
454
|
+
// T002's ancestor is T001 (epic) — should be found
|
|
455
|
+
expect(fromChild?.id).toBe('T001');
|
|
456
|
+
});
|
|
457
|
+
it('returns null when no epic in ancestor chain', async () => {
|
|
458
|
+
const now = new Date().toISOString();
|
|
459
|
+
await accessor.upsertSingleTask({
|
|
460
|
+
id: 'T001',
|
|
461
|
+
title: 'Parent task',
|
|
462
|
+
description: 'desc',
|
|
463
|
+
status: 'pending',
|
|
464
|
+
priority: 'medium',
|
|
465
|
+
type: 'task',
|
|
466
|
+
pipelineStage: 'research',
|
|
467
|
+
createdAt: now,
|
|
468
|
+
updatedAt: now,
|
|
469
|
+
});
|
|
470
|
+
await accessor.upsertSingleTask({
|
|
471
|
+
id: 'T002',
|
|
472
|
+
title: 'Child subtask',
|
|
473
|
+
description: 'desc',
|
|
474
|
+
status: 'pending',
|
|
475
|
+
priority: 'medium',
|
|
476
|
+
type: 'subtask',
|
|
477
|
+
parentId: 'T001',
|
|
478
|
+
pipelineStage: 'research',
|
|
479
|
+
createdAt: now,
|
|
480
|
+
updatedAt: now,
|
|
481
|
+
});
|
|
482
|
+
const epic = await findEpicAncestor('T002', accessor);
|
|
483
|
+
expect(epic).toBeNull();
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
// Integration: addTask epic creation enforcement
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
describe('addTask epic creation enforcement (strict)', () => {
|
|
490
|
+
let env;
|
|
491
|
+
let accessor;
|
|
492
|
+
beforeEach(async () => {
|
|
493
|
+
env = await createTestDb();
|
|
494
|
+
accessor = env.accessor;
|
|
495
|
+
await writeFile(join(env.cleoDir, 'config.json'), makeConfig('strict'));
|
|
496
|
+
});
|
|
497
|
+
afterEach(async () => {
|
|
498
|
+
await env.cleanup();
|
|
499
|
+
});
|
|
500
|
+
it('creates an epic when 5 AC and description are provided', async () => {
|
|
501
|
+
const result = await addTask({
|
|
502
|
+
title: 'My epic',
|
|
503
|
+
description: 'Completion criteria: all features shipped',
|
|
504
|
+
type: 'epic',
|
|
505
|
+
acceptance: ['ac1', 'ac2', 'ac3', 'ac4', 'ac5'],
|
|
506
|
+
}, env.tempDir, accessor);
|
|
507
|
+
expect(result.task.type).toBe('epic');
|
|
508
|
+
expect(result.task.acceptance?.length).toBe(5);
|
|
509
|
+
});
|
|
510
|
+
it('blocks epic creation with fewer than 5 AC', async () => {
|
|
511
|
+
await expect(addTask({
|
|
512
|
+
title: 'Bad epic',
|
|
513
|
+
description: 'Completion criteria here',
|
|
514
|
+
type: 'epic',
|
|
515
|
+
acceptance: ['ac1', 'ac2', 'ac3'],
|
|
516
|
+
}, env.tempDir, accessor)).rejects.toThrow(/5 acceptance criteria/);
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
describe('addTask child stage ceiling (strict)', () => {
|
|
520
|
+
let env;
|
|
521
|
+
let accessor;
|
|
522
|
+
beforeEach(async () => {
|
|
523
|
+
env = await createTestDb();
|
|
524
|
+
accessor = env.accessor;
|
|
525
|
+
await writeFile(join(env.cleoDir, 'config.json'), makeConfig('strict'));
|
|
526
|
+
});
|
|
527
|
+
afterEach(async () => {
|
|
528
|
+
await env.cleanup();
|
|
529
|
+
});
|
|
530
|
+
it('allows child at same stage as epic', async () => {
|
|
531
|
+
// Create epic at research
|
|
532
|
+
const epicResult = await addTask({
|
|
533
|
+
title: 'Epic',
|
|
534
|
+
description: 'Epic desc',
|
|
535
|
+
type: 'epic',
|
|
536
|
+
acceptance: ['ac1', 'ac2', 'ac3', 'ac4', 'ac5'],
|
|
537
|
+
pipelineStage: 'research',
|
|
538
|
+
}, env.tempDir, accessor);
|
|
539
|
+
const epicId = epicResult.task.id;
|
|
540
|
+
const childResult = await addTask({
|
|
541
|
+
title: 'Child',
|
|
542
|
+
description: 'Child task',
|
|
543
|
+
parentId: epicId,
|
|
544
|
+
pipelineStage: 'research',
|
|
545
|
+
}, env.tempDir, accessor);
|
|
546
|
+
expect(childResult.task.pipelineStage).toBe('research');
|
|
547
|
+
});
|
|
548
|
+
it('blocks child at stage beyond epic stage', async () => {
|
|
549
|
+
const epicResult = await addTask({
|
|
550
|
+
title: 'Epic',
|
|
551
|
+
description: 'Epic desc',
|
|
552
|
+
type: 'epic',
|
|
553
|
+
acceptance: ['ac1', 'ac2', 'ac3', 'ac4', 'ac5'],
|
|
554
|
+
pipelineStage: 'research',
|
|
555
|
+
}, env.tempDir, accessor);
|
|
556
|
+
const epicId = epicResult.task.id;
|
|
557
|
+
await expect(addTask({
|
|
558
|
+
title: 'Child',
|
|
559
|
+
description: 'Child task',
|
|
560
|
+
parentId: epicId,
|
|
561
|
+
pipelineStage: 'implementation',
|
|
562
|
+
}, env.tempDir, accessor)).rejects.toThrow(/cannot be at pipeline stage/);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
// Integration: updateTask epic stage advancement gate
|
|
567
|
+
// ---------------------------------------------------------------------------
|
|
568
|
+
describe('updateTask epic stage advancement gate (strict)', () => {
|
|
569
|
+
let env;
|
|
570
|
+
let accessor;
|
|
571
|
+
beforeEach(async () => {
|
|
572
|
+
env = await createTestDb();
|
|
573
|
+
accessor = env.accessor;
|
|
574
|
+
await writeFile(join(env.cleoDir, 'config.json'), makeConfig('strict'));
|
|
575
|
+
});
|
|
576
|
+
afterEach(async () => {
|
|
577
|
+
await env.cleanup();
|
|
578
|
+
});
|
|
579
|
+
it('allows advancing epic stage when no in-flight children at current stage', async () => {
|
|
580
|
+
// Create epic at research with 5 AC
|
|
581
|
+
const epicResult = await addTask({
|
|
582
|
+
title: 'Epic',
|
|
583
|
+
description: 'Epic desc',
|
|
584
|
+
type: 'epic',
|
|
585
|
+
acceptance: ['ac1', 'ac2', 'ac3', 'ac4', 'ac5'],
|
|
586
|
+
pipelineStage: 'research',
|
|
587
|
+
}, env.tempDir, accessor);
|
|
588
|
+
const epicId = epicResult.task.id;
|
|
589
|
+
// Advance epic to implementation — no children
|
|
590
|
+
const updateResult = await updateTask({ taskId: epicId, pipelineStage: 'implementation' }, env.tempDir, accessor);
|
|
591
|
+
expect(updateResult.task.pipelineStage).toBe('implementation');
|
|
592
|
+
});
|
|
593
|
+
it('blocks advancing epic when child is in-flight at current stage', async () => {
|
|
594
|
+
// Create epic at research
|
|
595
|
+
const epicResult = await addTask({
|
|
596
|
+
title: 'Epic',
|
|
597
|
+
description: 'Epic desc',
|
|
598
|
+
type: 'epic',
|
|
599
|
+
acceptance: ['ac1', 'ac2', 'ac3', 'ac4', 'ac5'],
|
|
600
|
+
pipelineStage: 'research',
|
|
601
|
+
}, env.tempDir, accessor);
|
|
602
|
+
const epicId = epicResult.task.id;
|
|
603
|
+
// Create in-flight child at research stage
|
|
604
|
+
await addTask({
|
|
605
|
+
title: 'Child',
|
|
606
|
+
description: 'Child task',
|
|
607
|
+
parentId: epicId,
|
|
608
|
+
pipelineStage: 'research',
|
|
609
|
+
}, env.tempDir, accessor);
|
|
610
|
+
// Attempt to advance epic — should be blocked
|
|
611
|
+
await expect(updateTask({ taskId: epicId, pipelineStage: 'implementation' }, env.tempDir, accessor)).rejects.toThrow(/cannot advance/);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
describe('updateTask child stage ceiling on update (strict)', () => {
|
|
615
|
+
let env;
|
|
616
|
+
let accessor;
|
|
617
|
+
beforeEach(async () => {
|
|
618
|
+
env = await createTestDb();
|
|
619
|
+
accessor = env.accessor;
|
|
620
|
+
await writeFile(join(env.cleoDir, 'config.json'), makeConfig('strict'));
|
|
621
|
+
});
|
|
622
|
+
afterEach(async () => {
|
|
623
|
+
await env.cleanup();
|
|
624
|
+
});
|
|
625
|
+
it('blocks updating child to a stage beyond epic', async () => {
|
|
626
|
+
// Create epic at research
|
|
627
|
+
const epicResult = await addTask({
|
|
628
|
+
title: 'Epic',
|
|
629
|
+
description: 'Epic desc',
|
|
630
|
+
type: 'epic',
|
|
631
|
+
acceptance: ['ac1', 'ac2', 'ac3', 'ac4', 'ac5'],
|
|
632
|
+
pipelineStage: 'research',
|
|
633
|
+
}, env.tempDir, accessor);
|
|
634
|
+
const epicId = epicResult.task.id;
|
|
635
|
+
// Create child at research
|
|
636
|
+
const childResult = await addTask({
|
|
637
|
+
title: 'Child',
|
|
638
|
+
description: 'Child task',
|
|
639
|
+
parentId: epicId,
|
|
640
|
+
pipelineStage: 'research',
|
|
641
|
+
}, env.tempDir, accessor);
|
|
642
|
+
const childId = childResult.task.id;
|
|
643
|
+
// Attempt to update child to implementation (beyond epic's research)
|
|
644
|
+
await expect(updateTask({ taskId: childId, pipelineStage: 'implementation' }, env.tempDir, accessor)).rejects.toThrow(/cannot be at pipeline stage/);
|
|
645
|
+
});
|
|
646
|
+
it('allows updating child to a stage equal to epic', async () => {
|
|
647
|
+
// Create epic at implementation
|
|
648
|
+
const epicResult = await addTask({
|
|
649
|
+
title: 'Epic',
|
|
650
|
+
description: 'Epic desc',
|
|
651
|
+
type: 'epic',
|
|
652
|
+
acceptance: ['ac1', 'ac2', 'ac3', 'ac4', 'ac5'],
|
|
653
|
+
pipelineStage: 'implementation',
|
|
654
|
+
}, env.tempDir, accessor);
|
|
655
|
+
const epicId = epicResult.task.id;
|
|
656
|
+
// Create child at research
|
|
657
|
+
const childResult = await addTask({
|
|
658
|
+
title: 'Child',
|
|
659
|
+
description: 'Child task',
|
|
660
|
+
parentId: epicId,
|
|
661
|
+
pipelineStage: 'research',
|
|
662
|
+
}, env.tempDir, accessor);
|
|
663
|
+
const childId = childResult.task.id;
|
|
664
|
+
// Update child to implementation (same as epic)
|
|
665
|
+
const updateResult = await updateTask({ taskId: childId, pipelineStage: 'implementation' }, env.tempDir, accessor);
|
|
666
|
+
expect(updateResult.task.pipelineStage).toBe('implementation');
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
//# sourceMappingURL=epic-enforcement.test.js.map
|