discoclaw 0.1.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/.context/README.md +42 -0
- package/.context/architecture.md +58 -0
- package/.context/bot-setup.md +24 -0
- package/.context/dev.md +230 -0
- package/.context/discord.md +144 -0
- package/.context/memory.md +257 -0
- package/.context/ops.md +59 -0
- package/.context/pa-safety.md +47 -0
- package/.context/pa.md +118 -0
- package/.context/project.md +43 -0
- package/.context/runtime.md +253 -0
- package/.context/tasks.md +71 -0
- package/.context/tools.md +75 -0
- package/.env.example +88 -0
- package/.env.example.full +378 -0
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/dist/beads/auto-tag.js +2 -0
- package/dist/beads/auto-tag.test.js +62 -0
- package/dist/beads/bd-cli.js +9 -0
- package/dist/beads/bd-cli.test.js +495 -0
- package/dist/beads/bead-hooks-cli.js +149 -0
- package/dist/beads/bead-sync-cli.js +5 -0
- package/dist/beads/bead-sync-cli.test.js +72 -0
- package/dist/beads/bead-sync-coordinator.js +4 -0
- package/dist/beads/bead-sync-coordinator.test.js +239 -0
- package/dist/beads/bead-sync-watcher.js +2 -0
- package/dist/beads/bead-sync-watcher.test.js +96 -0
- package/dist/beads/bead-sync.js +7 -0
- package/dist/beads/bead-sync.test.js +876 -0
- package/dist/beads/bead-thread-cache.js +8 -0
- package/dist/beads/bead-thread-cache.test.js +91 -0
- package/dist/beads/discord-sync.js +18 -0
- package/dist/beads/discord-sync.test.js +782 -0
- package/dist/beads/find-bead-by-thread.test.js +36 -0
- package/dist/beads/forum-guard.js +2 -0
- package/dist/beads/forum-guard.test.js +204 -0
- package/dist/beads/initialize.js +3 -0
- package/dist/beads/initialize.test.js +304 -0
- package/dist/beads/types.js +10 -0
- package/dist/cli/daemon-installer.js +225 -0
- package/dist/cli/daemon-installer.test.js +289 -0
- package/dist/cli/index.js +42 -0
- package/dist/cli/init-wizard.js +374 -0
- package/dist/cli/init-wizard.test.js +191 -0
- package/dist/config.js +385 -0
- package/dist/config.test.js +589 -0
- package/dist/cron/auto-tag.js +100 -0
- package/dist/cron/auto-tag.test.js +91 -0
- package/dist/cron/cadence.js +74 -0
- package/dist/cron/cadence.test.js +53 -0
- package/dist/cron/cron-sync-coordinator.js +66 -0
- package/dist/cron/cron-sync-coordinator.test.js +118 -0
- package/dist/cron/cron-sync.js +165 -0
- package/dist/cron/cron-sync.test.js +228 -0
- package/dist/cron/cron-tag-map-watcher.js +128 -0
- package/dist/cron/cron-tag-map-watcher.test.js +155 -0
- package/dist/cron/default-timezone.js +23 -0
- package/dist/cron/default-timezone.test.js +30 -0
- package/dist/cron/discord-sync.js +205 -0
- package/dist/cron/discord-sync.test.js +353 -0
- package/dist/cron/executor.js +303 -0
- package/dist/cron/executor.test.js +614 -0
- package/dist/cron/forum-sync.js +347 -0
- package/dist/cron/forum-sync.test.js +539 -0
- package/dist/cron/job-lock.js +164 -0
- package/dist/cron/job-lock.test.js +178 -0
- package/dist/cron/parser.js +68 -0
- package/dist/cron/parser.test.js +115 -0
- package/dist/cron/run-control.js +24 -0
- package/dist/cron/run-control.test.js +27 -0
- package/dist/cron/run-stats.js +265 -0
- package/dist/cron/run-stats.test.js +160 -0
- package/dist/cron/scheduler.js +97 -0
- package/dist/cron/scheduler.test.js +112 -0
- package/dist/cron/tag-map.js +47 -0
- package/dist/cron/tag-map.test.js +64 -0
- package/dist/cron/types.js +1 -0
- package/dist/discoclaw-plan-format.test.js +137 -0
- package/dist/discoclaw-recipe-format.test.js +137 -0
- package/dist/discord/abort-registry.js +70 -0
- package/dist/discord/action-categories.js +36 -0
- package/dist/discord/action-types.js +1 -0
- package/dist/discord/action-utils.js +58 -0
- package/dist/discord/action-utils.test.js +58 -0
- package/dist/discord/actions-beads.js +1 -0
- package/dist/discord/actions-beads.test.js +372 -0
- package/dist/discord/actions-bot-profile.js +107 -0
- package/dist/discord/actions-bot-profile.test.js +138 -0
- package/dist/discord/actions-channels.js +427 -0
- package/dist/discord/actions-channels.test.js +697 -0
- package/dist/discord/actions-config.js +173 -0
- package/dist/discord/actions-config.test.js +322 -0
- package/dist/discord/actions-crons.js +586 -0
- package/dist/discord/actions-crons.test.js +499 -0
- package/dist/discord/actions-defer.js +60 -0
- package/dist/discord/actions-defer.test.js +134 -0
- package/dist/discord/actions-forge.js +134 -0
- package/dist/discord/actions-forge.test.js +206 -0
- package/dist/discord/actions-guild.js +301 -0
- package/dist/discord/actions-guild.test.js +386 -0
- package/dist/discord/actions-memory.js +106 -0
- package/dist/discord/actions-memory.test.js +248 -0
- package/dist/discord/actions-messaging.js +401 -0
- package/dist/discord/actions-messaging.test.js +738 -0
- package/dist/discord/actions-moderation.js +65 -0
- package/dist/discord/actions-moderation.test.js +88 -0
- package/dist/discord/actions-plan.js +445 -0
- package/dist/discord/actions-plan.test.js +610 -0
- package/dist/discord/actions-poll.js +38 -0
- package/dist/discord/actions-poll.test.js +93 -0
- package/dist/discord/actions-tasks.js +3 -0
- package/dist/discord/actions-tasks.test.js +418 -0
- package/dist/discord/actions.js +600 -0
- package/dist/discord/actions.test.js +522 -0
- package/dist/discord/allowed-mentions.js +3 -0
- package/dist/discord/allowed-mentions.test.js +17 -0
- package/dist/discord/allowlist.js +29 -0
- package/dist/discord/allowlist.test.js +24 -0
- package/dist/discord/audit-handler.js +191 -0
- package/dist/discord/audit-handler.test.js +361 -0
- package/dist/discord/bot.js +141 -0
- package/dist/discord/channel-context.js +181 -0
- package/dist/discord/defer-scheduler.js +45 -0
- package/dist/discord/destructive-confirmation.js +128 -0
- package/dist/discord/destructive-confirmation.test.js +49 -0
- package/dist/discord/discord-plan-auto-implement.test.js +18 -0
- package/dist/discord/durable-memory.js +145 -0
- package/dist/discord/durable-memory.test.js +281 -0
- package/dist/discord/durable-write-queue.js +4 -0
- package/dist/discord/file-download.js +308 -0
- package/dist/discord/file-download.test.js +303 -0
- package/dist/discord/forge-audit-verdict.js +140 -0
- package/dist/discord/forge-auto-implement.js +80 -0
- package/dist/discord/forge-auto-implement.test.js +110 -0
- package/dist/discord/forge-commands.js +698 -0
- package/dist/discord/forge-commands.test.js +1606 -0
- package/dist/discord/forge-plan-registry.js +68 -0
- package/dist/discord/forge-plan-registry.test.js +127 -0
- package/dist/discord/forum-count-sync.js +130 -0
- package/dist/discord/forum-count-sync.test.js +200 -0
- package/dist/discord/health-command.js +98 -0
- package/dist/discord/health-command.test.js +195 -0
- package/dist/discord/help-command.js +22 -0
- package/dist/discord/help-command.test.js +49 -0
- package/dist/discord/image-download.js +201 -0
- package/dist/discord/image-download.test.js +499 -0
- package/dist/discord/inflight-replies.js +228 -0
- package/dist/discord/inflight-replies.test.js +295 -0
- package/dist/discord/json-extract.js +110 -0
- package/dist/discord/keyed-queue.js +22 -0
- package/dist/discord/memory-commands.js +85 -0
- package/dist/discord/memory-commands.test.js +159 -0
- package/dist/discord/memory-timing.integration.test.js +159 -0
- package/dist/discord/message-coordinator.js +2347 -0
- package/dist/discord/message-coordinator.onboarding.test.js +183 -0
- package/dist/discord/message-coordinator.plan-run.test.js +264 -0
- package/dist/discord/message-history.js +53 -0
- package/dist/discord/message-history.test.js +95 -0
- package/dist/discord/models-command.js +59 -0
- package/dist/discord/models-command.test.js +150 -0
- package/dist/discord/nickname.test.js +76 -0
- package/dist/discord/onboarding-completion.js +55 -0
- package/dist/discord/onboarding-completion.test.js +176 -0
- package/dist/discord/output-common.js +178 -0
- package/dist/discord/output-common.test.js +198 -0
- package/dist/discord/output-utils.js +156 -0
- package/dist/discord/parse-identity-name.test.js +129 -0
- package/dist/discord/plan-commands.js +612 -0
- package/dist/discord/plan-commands.test.js +1622 -0
- package/dist/discord/plan-manager.js +1491 -0
- package/dist/discord/plan-manager.test.js +2380 -0
- package/dist/discord/plan-parser.js +110 -0
- package/dist/discord/plan-parser.test.js +63 -0
- package/dist/discord/plan-run-phase-start.js +20 -0
- package/dist/discord/plan-run-phase-start.test.js +29 -0
- package/dist/discord/platform-message.js +45 -0
- package/dist/discord/platform-message.test.js +110 -0
- package/dist/discord/prompt-common.js +240 -0
- package/dist/discord/prompt-common.test.js +423 -0
- package/dist/discord/reaction-handler.js +691 -0
- package/dist/discord/reaction-handler.test.js +1574 -0
- package/dist/discord/reaction-prompts.js +118 -0
- package/dist/discord/reaction-prompts.test.js +253 -0
- package/dist/discord/reply-reference.js +66 -0
- package/dist/discord/reply-reference.test.js +125 -0
- package/dist/discord/restart-command.js +143 -0
- package/dist/discord/restart-command.test.js +196 -0
- package/dist/discord/runtime-utils.js +43 -0
- package/dist/discord/runtime-utils.test.js +112 -0
- package/dist/discord/session-key.js +7 -0
- package/dist/discord/session-key.test.js +13 -0
- package/dist/discord/shortterm-memory.js +166 -0
- package/dist/discord/shortterm-memory.test.js +345 -0
- package/dist/discord/shutdown-context.js +122 -0
- package/dist/discord/shutdown-context.test.js +279 -0
- package/dist/discord/startup-profile.test.js +214 -0
- package/dist/discord/status-channel.js +190 -0
- package/dist/discord/status-channel.test.js +282 -0
- package/dist/discord/status-command.js +206 -0
- package/dist/discord/status-command.test.js +341 -0
- package/dist/discord/streaming-progress.js +107 -0
- package/dist/discord/streaming-progress.test.js +93 -0
- package/dist/discord/summarizer.js +89 -0
- package/dist/discord/summarizer.test.js +245 -0
- package/dist/discord/system-bootstrap.js +396 -0
- package/dist/discord/system-bootstrap.test.js +724 -0
- package/dist/discord/thread-context.js +169 -0
- package/dist/discord/thread-context.test.js +386 -0
- package/dist/discord/tool-aware-queue.js +116 -0
- package/dist/discord/tool-aware-queue.test.js +180 -0
- package/dist/discord/update-command.js +127 -0
- package/dist/discord/update-command.test.js +275 -0
- package/dist/discord/user-errors.js +40 -0
- package/dist/discord/user-errors.test.js +31 -0
- package/dist/discord/user-turn-to-durable.js +111 -0
- package/dist/discord/user-turn-to-durable.test.js +273 -0
- package/dist/discord-followup.test.js +677 -0
- package/dist/discord.channel-context.test.js +95 -0
- package/dist/discord.fail-closed.test.js +199 -0
- package/dist/discord.health-command.integration.test.js +140 -0
- package/dist/discord.js +190 -0
- package/dist/discord.prompt-context.test.js +1431 -0
- package/dist/discord.render.test.js +621 -0
- package/dist/discord.status-wiring.test.js +187 -0
- package/dist/engine/claudeCli.js +137 -0
- package/dist/engine/types.js +1 -0
- package/dist/group-queue.js +25 -0
- package/dist/health/credential-check.js +175 -0
- package/dist/health/credential-check.test.js +401 -0
- package/dist/health/startup-healing.js +139 -0
- package/dist/health/startup-healing.test.js +298 -0
- package/dist/identity.js +36 -0
- package/dist/index.js +1378 -0
- package/dist/logging/logger-like.js +1 -0
- package/dist/observability/memory-sampler.js +51 -0
- package/dist/observability/memory-sampler.test.js +93 -0
- package/dist/observability/metrics.js +88 -0
- package/dist/observability/metrics.test.js +42 -0
- package/dist/onboarding/onboarding-flow.js +246 -0
- package/dist/onboarding/onboarding-flow.test.js +238 -0
- package/dist/onboarding/onboarding-writer.js +102 -0
- package/dist/onboarding/onboarding-writer.test.js +143 -0
- package/dist/pidlock.js +187 -0
- package/dist/pidlock.test.js +128 -0
- package/dist/pipeline/engine.js +206 -0
- package/dist/pipeline/engine.test.js +771 -0
- package/dist/root-policy.js +21 -0
- package/dist/root-policy.test.js +55 -0
- package/dist/runtime/claude-code-cli.js +35 -0
- package/dist/runtime/claude-code-cli.test.js +1199 -0
- package/dist/runtime/cli-adapter.js +584 -0
- package/dist/runtime/cli-output-parsers.js +108 -0
- package/dist/runtime/cli-shared.js +96 -0
- package/dist/runtime/cli-shared.test.js +104 -0
- package/dist/runtime/cli-strategy.js +6 -0
- package/dist/runtime/codex-cli.js +16 -0
- package/dist/runtime/codex-cli.test.js +862 -0
- package/dist/runtime/concurrency-limit.js +80 -0
- package/dist/runtime/concurrency-limit.test.js +137 -0
- package/dist/runtime/gemini-cli.js +16 -0
- package/dist/runtime/gemini-cli.test.js +413 -0
- package/dist/runtime/long-running-process.js +415 -0
- package/dist/runtime/long-running-process.test.js +318 -0
- package/dist/runtime/model-smoke-helpers.js +160 -0
- package/dist/runtime/model-smoke.test.js +194 -0
- package/dist/runtime/model-tiers.js +33 -0
- package/dist/runtime/model-tiers.test.js +65 -0
- package/dist/runtime/openai-auth.js +151 -0
- package/dist/runtime/openai-auth.test.js +361 -0
- package/dist/runtime/openai-compat.js +178 -0
- package/dist/runtime/openai-compat.test.js +449 -0
- package/dist/runtime/process-pool.js +93 -0
- package/dist/runtime/process-pool.test.js +148 -0
- package/dist/runtime/registry.js +15 -0
- package/dist/runtime/registry.test.js +47 -0
- package/dist/runtime/session-scanner.js +186 -0
- package/dist/runtime/session-scanner.test.js +257 -0
- package/dist/runtime/strategies/claude-strategy.js +193 -0
- package/dist/runtime/strategies/codex-strategy.js +161 -0
- package/dist/runtime/strategies/gemini-strategy.js +64 -0
- package/dist/runtime/strategies/template-strategy.js +85 -0
- package/dist/runtime/tool-capabilities.js +27 -0
- package/dist/runtime/tool-capabilities.test.js +24 -0
- package/dist/runtime/tool-labels.js +48 -0
- package/dist/runtime/types.js +2 -0
- package/dist/sessionManager.js +47 -0
- package/dist/sessions.js +18 -0
- package/dist/tasks/architecture-contract.js +33 -0
- package/dist/tasks/architecture-contract.test.js +90 -0
- package/dist/tasks/auto-tag.js +50 -0
- package/dist/tasks/auto-tag.test.js +64 -0
- package/dist/tasks/bd-cli.js +164 -0
- package/dist/tasks/bd-cli.test.js +359 -0
- package/dist/tasks/bead-sync.js +1 -0
- package/dist/tasks/context-summary.js +27 -0
- package/dist/tasks/discord-sync.js +3 -0
- package/dist/tasks/discord-sync.test.js +685 -0
- package/dist/tasks/discord-types.js +4 -0
- package/dist/tasks/find-task-by-thread.test.js +36 -0
- package/dist/tasks/forum-guard.js +81 -0
- package/dist/tasks/forum-guard.test.js +192 -0
- package/dist/tasks/initialize.js +77 -0
- package/dist/tasks/initialize.test.js +263 -0
- package/dist/tasks/logger-types.js +1 -0
- package/dist/tasks/metrics-types.js +3 -0
- package/dist/tasks/migrate.js +33 -0
- package/dist/tasks/migrate.test.js +156 -0
- package/dist/tasks/path-defaults.js +67 -0
- package/dist/tasks/path-defaults.test.js +73 -0
- package/dist/tasks/runtime-types.js +1 -0
- package/dist/tasks/service.js +33 -0
- package/dist/tasks/service.test.js +51 -0
- package/dist/tasks/store.js +238 -0
- package/dist/tasks/store.test.js +417 -0
- package/dist/tasks/sync-context.js +1 -0
- package/dist/tasks/sync-contract.js +24 -0
- package/dist/tasks/sync-contract.test.js +25 -0
- package/dist/tasks/sync-coordinator-metrics.js +41 -0
- package/dist/tasks/sync-coordinator-retries.js +71 -0
- package/dist/tasks/sync-coordinator.js +96 -0
- package/dist/tasks/sync-coordinator.test.js +501 -0
- package/dist/tasks/sync-types.js +1 -0
- package/dist/tasks/sync-watcher.js +27 -0
- package/dist/tasks/sync-watcher.test.js +92 -0
- package/dist/tasks/tag-map.js +36 -0
- package/dist/tasks/tag-map.test.js +54 -0
- package/dist/tasks/task-action-contract.js +16 -0
- package/dist/tasks/task-action-contract.test.js +16 -0
- package/dist/tasks/task-action-executor.js +18 -0
- package/dist/tasks/task-action-executor.test.js +420 -0
- package/dist/tasks/task-action-mutation-helpers.js +17 -0
- package/dist/tasks/task-action-mutations.js +151 -0
- package/dist/tasks/task-action-prompt.js +62 -0
- package/dist/tasks/task-action-read-ops.js +73 -0
- package/dist/tasks/task-action-runner-types.js +1 -0
- package/dist/tasks/task-action-thread-sync.js +82 -0
- package/dist/tasks/task-actions.js +3 -0
- package/dist/tasks/task-cli.js +227 -0
- package/dist/tasks/task-context.js +1 -0
- package/dist/tasks/task-lifecycle.js +46 -0
- package/dist/tasks/task-lifecycle.test.js +35 -0
- package/dist/tasks/task-sync-apply-plan.js +95 -0
- package/dist/tasks/task-sync-apply-types.js +12 -0
- package/dist/tasks/task-sync-apply.js +319 -0
- package/dist/tasks/task-sync-cli.js +89 -0
- package/dist/tasks/task-sync-cli.test.js +70 -0
- package/dist/tasks/task-sync-engine.js +88 -0
- package/dist/tasks/task-sync-engine.test.js +934 -0
- package/dist/tasks/task-sync-phase-apply.js +171 -0
- package/dist/tasks/task-sync-pipeline.js +2 -0
- package/dist/tasks/task-sync-pipeline.test.js +265 -0
- package/dist/tasks/task-sync-reconcile-plan.js +182 -0
- package/dist/tasks/task-sync-reconcile.js +144 -0
- package/dist/tasks/task-sync.js +56 -0
- package/dist/tasks/task-sync.test.js +86 -0
- package/dist/tasks/thread-cache.js +42 -0
- package/dist/tasks/thread-cache.test.js +89 -0
- package/dist/tasks/thread-contracts.test.js +711 -0
- package/dist/tasks/thread-forum-ops.js +68 -0
- package/dist/tasks/thread-helpers.js +86 -0
- package/dist/tasks/thread-helpers.test.js +33 -0
- package/dist/tasks/thread-lifecycle-ops.js +144 -0
- package/dist/tasks/thread-ops-shared.js +21 -0
- package/dist/tasks/thread-ops.js +2 -0
- package/dist/tasks/types.js +20 -0
- package/dist/tasks/types.test.js +60 -0
- package/dist/test-setup.js +11 -0
- package/dist/test-setup.test.js +42 -0
- package/dist/transport/types.js +1 -0
- package/dist/validate.js +41 -0
- package/dist/validate.test.js +94 -0
- package/dist/version.js +15 -0
- package/dist/version.test.js +31 -0
- package/dist/webhook/server.js +199 -0
- package/dist/webhook/server.test.js +460 -0
- package/dist/workspace-bootstrap.js +135 -0
- package/dist/workspace-bootstrap.test.js +514 -0
- package/dist/workspace-permissions.js +134 -0
- package/dist/workspace-permissions.test.js +181 -0
- package/package.json +74 -0
- package/scripts/cron/cron-tag-map.json +9 -0
- package/scripts/tasks/tag-map.json +10 -0
- package/systemd/discoclaw.service +19 -0
- package/templates/recipes/integration.discoclaw-recipe.md +171 -0
- package/templates/workspace/AGENTS.md +217 -0
- package/templates/workspace/BOOTSTRAP.md +1 -0
- package/templates/workspace/HEARTBEAT.md +10 -0
- package/templates/workspace/IDENTITY.md +16 -0
- package/templates/workspace/MEMORY.md +24 -0
- package/templates/workspace/SOUL.md +52 -0
- package/templates/workspace/TOOLS.md +304 -0
- package/templates/workspace/USER.md +37 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createTaskService } from './service.js';
|
|
2
|
+
import { runTaskSync } from './task-sync.js';
|
|
3
|
+
export function resolveTaskService(taskCtx) {
|
|
4
|
+
if (taskCtx.taskService)
|
|
5
|
+
return taskCtx.taskService;
|
|
6
|
+
const taskService = createTaskService(taskCtx.store);
|
|
7
|
+
taskCtx.taskService = taskService;
|
|
8
|
+
return taskService;
|
|
9
|
+
}
|
|
10
|
+
export function resolveTaskId(action) {
|
|
11
|
+
return (action.taskId ?? '').trim();
|
|
12
|
+
}
|
|
13
|
+
export function scheduleRepairSync(taskCtx, taskId, ctx) {
|
|
14
|
+
runTaskSync(taskCtx, { client: ctx.client, guild: ctx.guild }).catch((err) => {
|
|
15
|
+
taskCtx.log?.warn({ err, taskId }, 'tasks:repair sync failed');
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { TASK_STATUSES, isTaskStatus } from './types.js';
|
|
2
|
+
import { withDirectTaskLifecycle } from './task-lifecycle.js';
|
|
3
|
+
import { autoTagTask } from './auto-tag.js';
|
|
4
|
+
import { taskThreadCache } from './thread-cache.js';
|
|
5
|
+
import { resolveTaskId, resolveTaskService, scheduleRepairSync, } from './task-action-mutation-helpers.js';
|
|
6
|
+
import { ensureCreatedTaskThreadLink, syncClosedTaskThread, syncUpdatedTaskThread, } from './task-action-thread-sync.js';
|
|
7
|
+
/** Pre-computed set for filtering status names from tag candidates. */
|
|
8
|
+
const STATUS_NAME_SET = new Set(TASK_STATUSES);
|
|
9
|
+
/**
|
|
10
|
+
* Discord message content limit. Descriptions are rendered in the thread's
|
|
11
|
+
* starter message, so they must fit within this budget (minus ~100 chars of
|
|
12
|
+
* metadata overhead added by buildTaskStarterContent).
|
|
13
|
+
*/
|
|
14
|
+
const TASK_DESC_WRITE_MAX = 1900;
|
|
15
|
+
export async function handleTaskCreate(action, ctx, taskCtx) {
|
|
16
|
+
if (!action.title) {
|
|
17
|
+
return { ok: false, error: 'taskCreate requires a title' };
|
|
18
|
+
}
|
|
19
|
+
if (action.description && action.description.length > TASK_DESC_WRITE_MAX) {
|
|
20
|
+
return { ok: false, error: `description exceeds ${TASK_DESC_WRITE_MAX} character limit (got ${action.description.length})` };
|
|
21
|
+
}
|
|
22
|
+
const labels = [];
|
|
23
|
+
if (action.tags) {
|
|
24
|
+
labels.push(...action.tags.split(',').map((t) => t.trim()).filter(Boolean));
|
|
25
|
+
}
|
|
26
|
+
const taskService = resolveTaskService(taskCtx);
|
|
27
|
+
const task = taskService.create({
|
|
28
|
+
title: action.title,
|
|
29
|
+
description: action.description,
|
|
30
|
+
priority: action.priority,
|
|
31
|
+
labels,
|
|
32
|
+
});
|
|
33
|
+
let threadId = '';
|
|
34
|
+
let needsRepairSync = false;
|
|
35
|
+
await withDirectTaskLifecycle(task.id, async () => {
|
|
36
|
+
const tagNames = Object.keys(taskCtx.tagMap).filter((k) => !STATUS_NAME_SET.has(k));
|
|
37
|
+
if (taskCtx.autoTag && tagNames.length > 0) {
|
|
38
|
+
try {
|
|
39
|
+
const suggestedTags = await autoTagTask(taskCtx.runtime, task.title, task.description ?? '', tagNames, {
|
|
40
|
+
model: taskCtx.autoTagModel,
|
|
41
|
+
cwd: taskCtx.tasksCwd || process.cwd(),
|
|
42
|
+
modelResolver: taskCtx.resolveModel,
|
|
43
|
+
});
|
|
44
|
+
for (const tag of suggestedTags) {
|
|
45
|
+
if (!labels.includes(tag))
|
|
46
|
+
labels.push(tag);
|
|
47
|
+
}
|
|
48
|
+
for (const tag of suggestedTags) {
|
|
49
|
+
try {
|
|
50
|
+
taskService.addLabel(task.id, `tag:${tag}`);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// best-effort
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
taskCtx.log?.warn({ err, taskId: task.id }, 'tasks:auto-tag failed');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const threadLink = await ensureCreatedTaskThreadLink({
|
|
62
|
+
actionType: action.type,
|
|
63
|
+
taskCtx,
|
|
64
|
+
runCtx: ctx,
|
|
65
|
+
taskService,
|
|
66
|
+
task,
|
|
67
|
+
labels,
|
|
68
|
+
});
|
|
69
|
+
threadId = threadLink.threadId;
|
|
70
|
+
if (threadLink.needsRepairSync)
|
|
71
|
+
needsRepairSync = true;
|
|
72
|
+
});
|
|
73
|
+
if (needsRepairSync) {
|
|
74
|
+
scheduleRepairSync(taskCtx, task.id, ctx);
|
|
75
|
+
}
|
|
76
|
+
taskThreadCache.invalidate();
|
|
77
|
+
taskCtx.forumCountSync?.requestUpdate();
|
|
78
|
+
const threadNote = threadId ? ' (thread linked)' : '';
|
|
79
|
+
return { ok: true, summary: `Task ${task.id} created: "${task.title}"${threadNote}` };
|
|
80
|
+
}
|
|
81
|
+
export async function handleTaskUpdate(action, ctx, taskCtx) {
|
|
82
|
+
const taskId = resolveTaskId(action);
|
|
83
|
+
if (!taskId) {
|
|
84
|
+
return { ok: false, error: 'taskUpdate requires taskId' };
|
|
85
|
+
}
|
|
86
|
+
if (action.description && action.description.length > TASK_DESC_WRITE_MAX) {
|
|
87
|
+
return { ok: false, error: `description exceeds ${TASK_DESC_WRITE_MAX} character limit (got ${action.description.length})` };
|
|
88
|
+
}
|
|
89
|
+
if (action.status && !isTaskStatus(action.status)) {
|
|
90
|
+
return { ok: false, error: `Invalid task status: "${action.status}"` };
|
|
91
|
+
}
|
|
92
|
+
let needsRepairSync = false;
|
|
93
|
+
const taskService = resolveTaskService(taskCtx);
|
|
94
|
+
await withDirectTaskLifecycle(taskId, async () => {
|
|
95
|
+
const updatedTask = taskService.update(taskId, {
|
|
96
|
+
title: action.title,
|
|
97
|
+
description: action.description,
|
|
98
|
+
priority: action.priority,
|
|
99
|
+
status: action.status,
|
|
100
|
+
});
|
|
101
|
+
const threadRepair = await syncUpdatedTaskThread({
|
|
102
|
+
actionType: action.type,
|
|
103
|
+
taskCtx,
|
|
104
|
+
runCtx: ctx,
|
|
105
|
+
taskId,
|
|
106
|
+
updatedTask,
|
|
107
|
+
});
|
|
108
|
+
if (threadRepair)
|
|
109
|
+
needsRepairSync = true;
|
|
110
|
+
});
|
|
111
|
+
if (needsRepairSync) {
|
|
112
|
+
scheduleRepairSync(taskCtx, taskId, ctx);
|
|
113
|
+
}
|
|
114
|
+
taskThreadCache.invalidate();
|
|
115
|
+
if (action.status)
|
|
116
|
+
taskCtx.forumCountSync?.requestUpdate();
|
|
117
|
+
const changes = [];
|
|
118
|
+
if (action.title)
|
|
119
|
+
changes.push(`title → "${action.title}"`);
|
|
120
|
+
if (action.status)
|
|
121
|
+
changes.push(`status → ${action.status}`);
|
|
122
|
+
if (action.priority != null)
|
|
123
|
+
changes.push(`priority → P${action.priority}`);
|
|
124
|
+
return { ok: true, summary: `Task ${taskId} updated: ${changes.join(', ') || 'no changes'}` };
|
|
125
|
+
}
|
|
126
|
+
export async function handleTaskClose(action, ctx, taskCtx) {
|
|
127
|
+
const taskId = resolveTaskId(action);
|
|
128
|
+
if (!taskId) {
|
|
129
|
+
return { ok: false, error: 'taskClose requires taskId' };
|
|
130
|
+
}
|
|
131
|
+
let needsRepairSync = false;
|
|
132
|
+
const taskService = resolveTaskService(taskCtx);
|
|
133
|
+
await withDirectTaskLifecycle(taskId, async () => {
|
|
134
|
+
const closedTask = taskService.close(taskId, action.reason);
|
|
135
|
+
const threadRepair = await syncClosedTaskThread({
|
|
136
|
+
actionType: action.type,
|
|
137
|
+
taskCtx,
|
|
138
|
+
runCtx: ctx,
|
|
139
|
+
taskId,
|
|
140
|
+
closedTask,
|
|
141
|
+
});
|
|
142
|
+
if (threadRepair)
|
|
143
|
+
needsRepairSync = true;
|
|
144
|
+
});
|
|
145
|
+
if (needsRepairSync) {
|
|
146
|
+
scheduleRepairSync(taskCtx, taskId, ctx);
|
|
147
|
+
}
|
|
148
|
+
taskThreadCache.invalidate();
|
|
149
|
+
taskCtx.forumCountSync?.requestUpdate();
|
|
150
|
+
return { ok: true, summary: `Task ${taskId} closed${action.reason ? `: ${action.reason}` : ''}` };
|
|
151
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export function taskActionsPromptSection() {
|
|
2
|
+
return `### Task Tracking
|
|
3
|
+
|
|
4
|
+
**taskCreate** — Create a new task:
|
|
5
|
+
\`\`\`
|
|
6
|
+
<discord-action>{"type":"taskCreate","title":"Task title","description":"Optional details","priority":2,"tags":"feature,work"}</discord-action>
|
|
7
|
+
\`\`\`
|
|
8
|
+
- \`title\` (required): Task title.
|
|
9
|
+
- \`description\` (optional): Detailed description.
|
|
10
|
+
- \`priority\` (optional): 0-4 (0=highest, default 2).
|
|
11
|
+
- \`tags\` (optional): Comma-separated labels/tags.
|
|
12
|
+
|
|
13
|
+
**taskUpdate** — Update a task's fields:
|
|
14
|
+
\`\`\`
|
|
15
|
+
<discord-action>{"type":"taskUpdate","taskId":"ws-001","status":"in_progress","priority":1}</discord-action>
|
|
16
|
+
\`\`\`
|
|
17
|
+
- \`taskId\` (required): Task ID.
|
|
18
|
+
- \`title\`, \`description\`, \`priority\`, \`status\` (optional): Fields to update.
|
|
19
|
+
|
|
20
|
+
**taskClose** — Close a task:
|
|
21
|
+
\`\`\`
|
|
22
|
+
<discord-action>{"type":"taskClose","taskId":"ws-001","reason":"Done"}</discord-action>
|
|
23
|
+
\`\`\`
|
|
24
|
+
|
|
25
|
+
**taskShow** — Show task details:
|
|
26
|
+
\`\`\`
|
|
27
|
+
<discord-action>{"type":"taskShow","taskId":"ws-001"}</discord-action>
|
|
28
|
+
\`\`\`
|
|
29
|
+
|
|
30
|
+
**taskList** — List tasks:
|
|
31
|
+
\`\`\`
|
|
32
|
+
<discord-action>{"type":"taskList","status":"open","limit":10}</discord-action>
|
|
33
|
+
\`\`\`
|
|
34
|
+
- \`status\` (optional): Filter by status (open, in_progress, blocked, closed, all).
|
|
35
|
+
- \`label\` (optional): Filter by label.
|
|
36
|
+
- \`limit\` (optional): Max results.
|
|
37
|
+
|
|
38
|
+
**taskSync** — Run full sync between local task store and Discord threads:
|
|
39
|
+
\`\`\`
|
|
40
|
+
<discord-action>{"type":"taskSync"}</discord-action>
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
43
|
+
**tagMapReload** — Reload tag map from disk (hot-reload without restart):
|
|
44
|
+
\`\`\`
|
|
45
|
+
<discord-action>{"type":"tagMapReload"}</discord-action>
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
#### Task Quality Guidelines
|
|
49
|
+
- **Title**: imperative mood, specific, <60 chars. Good: "Add retry logic to webhook handler", "Plan March Denver trip". Bad: "fix stuff".
|
|
50
|
+
- **Description** should answer what/why/scope. Use markdown for structure. Include what "done" looks like for larger tasks. Max 1900 characters — the system will reject longer descriptions with an error.
|
|
51
|
+
- **Priority**: P0=urgent, P1=important, P2=normal (default), P3=nice-to-have, P4=someday.
|
|
52
|
+
- If the user explicitly asks to create a task, always create it.
|
|
53
|
+
- Apply the same description quality standards when using taskUpdate to backfill details.
|
|
54
|
+
|
|
55
|
+
#### Cross-Task References
|
|
56
|
+
When interacting with another task, always use task actions with its task ID, not channel-name based messaging actions:
|
|
57
|
+
- **Read task content**: \`taskShow <id>\`
|
|
58
|
+
- **Update a task**: \`taskUpdate <id>\`
|
|
59
|
+
- **Close a task**: \`taskClose <id>\`
|
|
60
|
+
- **Find tasks**: \`taskList\` (filter by status or label)
|
|
61
|
+
- **Reconcile Discord threads**: \`taskSync\``;
|
|
62
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { runTaskSync } from './task-sync.js';
|
|
2
|
+
import { reloadTagMapInPlace } from './tag-map.js';
|
|
3
|
+
function resolveTaskId(action) {
|
|
4
|
+
return (action.taskId ?? '').trim();
|
|
5
|
+
}
|
|
6
|
+
export async function handleTaskShow(action, _ctx, taskCtx) {
|
|
7
|
+
const taskId = resolveTaskId(action);
|
|
8
|
+
if (!taskId) {
|
|
9
|
+
return { ok: false, error: 'taskShow requires taskId' };
|
|
10
|
+
}
|
|
11
|
+
const task = taskCtx.store.get(taskId);
|
|
12
|
+
if (!task) {
|
|
13
|
+
return { ok: false, error: `Task "${taskId}" not found` };
|
|
14
|
+
}
|
|
15
|
+
const lines = [
|
|
16
|
+
`**${task.title}** (\`${task.id}\`)`,
|
|
17
|
+
`Status: ${task.status} | Priority: P${task.priority}`,
|
|
18
|
+
];
|
|
19
|
+
if (task.owner)
|
|
20
|
+
lines.push(`Owner: ${task.owner}`);
|
|
21
|
+
if (task.labels?.length)
|
|
22
|
+
lines.push(`Labels: ${task.labels.join(', ')}`);
|
|
23
|
+
if (task.description)
|
|
24
|
+
lines.push(`\n${task.description.slice(0, 500)}`);
|
|
25
|
+
return { ok: true, summary: lines.join('\n') };
|
|
26
|
+
}
|
|
27
|
+
export async function handleTaskList(action, _ctx, taskCtx) {
|
|
28
|
+
const tasks = taskCtx.store.list({
|
|
29
|
+
status: action.status,
|
|
30
|
+
label: action.label,
|
|
31
|
+
limit: action.limit ?? 50,
|
|
32
|
+
});
|
|
33
|
+
if (tasks.length === 0) {
|
|
34
|
+
return { ok: true, summary: 'No tasks found matching the filter.' };
|
|
35
|
+
}
|
|
36
|
+
const lines = tasks.map((t) => `\`${t.id}\` [${t.status}] P${t.priority} — ${t.title}`);
|
|
37
|
+
return { ok: true, summary: lines.join('\n') };
|
|
38
|
+
}
|
|
39
|
+
export async function handleTaskSync(_action, ctx, taskCtx) {
|
|
40
|
+
try {
|
|
41
|
+
const result = await runTaskSync(taskCtx, { client: ctx.client, guild: ctx.guild }, taskCtx.statusPoster);
|
|
42
|
+
if (!result) {
|
|
43
|
+
return { ok: true, summary: 'Sync already running; changes will be picked up.' };
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
summary: `Sync complete: ${result.threadsCreated} created, ${result.emojisUpdated} updated, ${result.starterMessagesUpdated} starters, ${result.tagsUpdated} tags, ${result.threadsArchived} archived, ${result.statusesUpdated} status-fixes${result.threadsReconciled ? `, ${result.threadsReconciled} reconciled` : ''}${result.orphanThreadsFound ? `, ${result.orphanThreadsFound} orphans` : ''}${result.warnings ? `, ${result.warnings} warnings` : ''}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
52
|
+
return { ok: false, error: `Task sync failed: ${msg}` };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function handleTagMapReload(_action, _ctx, taskCtx) {
|
|
56
|
+
if (!taskCtx.tagMapPath) {
|
|
57
|
+
return { ok: false, error: 'Tag map path not configured' };
|
|
58
|
+
}
|
|
59
|
+
const oldCount = Object.keys(taskCtx.tagMap).length;
|
|
60
|
+
try {
|
|
61
|
+
await reloadTagMapInPlace(taskCtx.tagMapPath, taskCtx.tagMap);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
65
|
+
return { ok: false, error: `Tag map reload failed: ${msg}` };
|
|
66
|
+
}
|
|
67
|
+
const newCount = Object.keys(taskCtx.tagMap).length;
|
|
68
|
+
const tagList = Object.keys(taskCtx.tagMap);
|
|
69
|
+
const tagsDisplay = tagList.length <= 10
|
|
70
|
+
? tagList.join(', ')
|
|
71
|
+
: `${tagList.slice(0, 10).join(', ')} (+${tagList.length - 10} more)`;
|
|
72
|
+
return { ok: true, summary: `Tag map reloaded (${oldCount} -> ${newCount}): ${tagsDisplay}` };
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { shouldActionUseDirectThreadLifecycle } from './sync-contract.js';
|
|
2
|
+
import { closeTaskThread, createTaskThread, ensureUnarchived, findExistingThreadForTask, resolveTasksForum, updateTaskStarterMessage, updateTaskThreadName, updateTaskThreadTags, } from './thread-ops.js';
|
|
3
|
+
import { getThreadIdFromTask } from './thread-helpers.js';
|
|
4
|
+
export async function ensureCreatedTaskThreadLink(opts) {
|
|
5
|
+
if (!shouldActionUseDirectThreadLifecycle(opts.actionType)) {
|
|
6
|
+
return { threadId: '', needsRepairSync: false };
|
|
7
|
+
}
|
|
8
|
+
const latest = opts.taskCtx.store.get(opts.task.id) ?? opts.task;
|
|
9
|
+
const currentThreadId = getThreadIdFromTask(latest);
|
|
10
|
+
if (currentThreadId) {
|
|
11
|
+
return { threadId: currentThreadId, needsRepairSync: false };
|
|
12
|
+
}
|
|
13
|
+
if (opts.labels.includes('no-thread') || (latest.labels ?? []).includes('no-thread')) {
|
|
14
|
+
return { threadId: '', needsRepairSync: false };
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const forum = await resolveTasksForum(opts.runCtx.guild, opts.taskCtx.forumId);
|
|
18
|
+
if (!forum)
|
|
19
|
+
return { threadId: '', needsRepairSync: false };
|
|
20
|
+
const existing = await findExistingThreadForTask(forum, opts.task.id);
|
|
21
|
+
const threadId = existing ?? await createTaskThread(forum, { ...latest, labels: opts.labels }, opts.taskCtx.tagMap, opts.taskCtx.mentionUserId);
|
|
22
|
+
try {
|
|
23
|
+
const newest = opts.taskCtx.store.get(opts.task.id) ?? opts.task;
|
|
24
|
+
const newestThreadId = getThreadIdFromTask(newest);
|
|
25
|
+
if (newestThreadId !== threadId) {
|
|
26
|
+
opts.taskService.update(opts.task.id, { externalRef: `discord:${threadId}` });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
opts.taskCtx.log?.warn({ err, taskId: opts.task.id, threadId }, 'tasks:external-ref update failed');
|
|
31
|
+
}
|
|
32
|
+
return { threadId, needsRepairSync: false };
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
opts.taskCtx.log?.warn({ err, taskId: opts.task.id }, 'tasks:thread creation failed');
|
|
36
|
+
return { threadId: '', needsRepairSync: true };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function syncUpdatedTaskThread(opts) {
|
|
40
|
+
const threadId = getThreadIdFromTask(opts.updatedTask);
|
|
41
|
+
if (!threadId || !shouldActionUseDirectThreadLifecycle(opts.actionType)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
let needsRepairSync = false;
|
|
45
|
+
try {
|
|
46
|
+
await ensureUnarchived(opts.runCtx.client, threadId);
|
|
47
|
+
await updateTaskThreadName(opts.runCtx.client, threadId, opts.updatedTask);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
needsRepairSync = true;
|
|
51
|
+
opts.taskCtx.log?.warn({ err, taskId: opts.taskId, threadId }, 'tasks:thread name update failed');
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await updateTaskStarterMessage(opts.runCtx.client, threadId, opts.updatedTask, opts.taskCtx.sidebarMentionUserId);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
needsRepairSync = true;
|
|
58
|
+
opts.taskCtx.log?.warn({ err, taskId: opts.taskId, threadId }, 'tasks:starter message update failed');
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
await updateTaskThreadTags(opts.runCtx.client, threadId, opts.updatedTask, opts.taskCtx.tagMap);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
needsRepairSync = true;
|
|
65
|
+
opts.taskCtx.log?.warn({ err, taskId: opts.taskId, threadId }, 'tasks:thread tag update failed');
|
|
66
|
+
}
|
|
67
|
+
return needsRepairSync;
|
|
68
|
+
}
|
|
69
|
+
export async function syncClosedTaskThread(opts) {
|
|
70
|
+
const threadId = getThreadIdFromTask(opts.closedTask);
|
|
71
|
+
if (!threadId || !shouldActionUseDirectThreadLifecycle(opts.actionType)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
await closeTaskThread(opts.runCtx.client, threadId, opts.closedTask, opts.taskCtx.tagMap, opts.taskCtx.log);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
opts.taskCtx.log?.warn({ err, taskId: opts.taskId, threadId }, 'tasks:thread close failed');
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { TaskStore } from './store.js';
|
|
2
|
+
import { createTaskService } from './service.js';
|
|
3
|
+
import { isTaskStatus } from './types.js';
|
|
4
|
+
import { resolveTaskDataLoadPath, resolveTaskDataPath } from './path-defaults.js';
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
function envOpt(name) {
|
|
9
|
+
const v = (process.env[name] ?? '').trim();
|
|
10
|
+
return v || undefined;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Extract the value for a named flag from an args array.
|
|
14
|
+
* Supports both `--flag value` and `--flag=value` forms.
|
|
15
|
+
*/
|
|
16
|
+
function argValue(args, ...flags) {
|
|
17
|
+
for (const flag of flags) {
|
|
18
|
+
// --flag value
|
|
19
|
+
const idx = args.indexOf(flag);
|
|
20
|
+
if (idx >= 0 && idx + 1 < args.length)
|
|
21
|
+
return args[idx + 1];
|
|
22
|
+
// --flag=value
|
|
23
|
+
const prefix = `${flag}=`;
|
|
24
|
+
const hit = args.find((a) => a.startsWith(prefix));
|
|
25
|
+
if (hit)
|
|
26
|
+
return hit.slice(prefix.length);
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
function hasFlag(args, ...flags) {
|
|
31
|
+
return flags.some((f) => args.includes(f));
|
|
32
|
+
}
|
|
33
|
+
/** Return non-flag positional arguments (args that don't start with `-`). */
|
|
34
|
+
function positional(args) {
|
|
35
|
+
const result = [];
|
|
36
|
+
for (let i = 0; i < args.length; i++) {
|
|
37
|
+
const a = args[i];
|
|
38
|
+
if (a.startsWith('-')) {
|
|
39
|
+
// Skip the next arg if this is a value-bearing flag.
|
|
40
|
+
if (!a.includes('=') && i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
result.push(a);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Store factory
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
async function getStore() {
|
|
54
|
+
const explicitPath = envOpt('DISCOCLAW_TASKS_PATH');
|
|
55
|
+
const dataDir = envOpt('DISCOCLAW_DATA_DIR');
|
|
56
|
+
const tasksPath = explicitPath
|
|
57
|
+
?? await resolveTaskDataLoadPath(dataDir, 'tasks.jsonl')
|
|
58
|
+
?? resolveTaskDataPath(dataDir, 'tasks.jsonl');
|
|
59
|
+
if (!tasksPath) {
|
|
60
|
+
throw new Error('DISCOCLAW_TASKS_PATH or DISCOCLAW_DATA_DIR is required');
|
|
61
|
+
}
|
|
62
|
+
const store = new TaskStore({ persistPath: tasksPath });
|
|
63
|
+
await store.load();
|
|
64
|
+
return store;
|
|
65
|
+
}
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Main
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
async function main() {
|
|
70
|
+
const args = process.argv.slice(2);
|
|
71
|
+
const subcommand = args[0];
|
|
72
|
+
if (!subcommand || subcommand === '--help' || subcommand === '-h') {
|
|
73
|
+
process.stdout.write([
|
|
74
|
+
'Usage: task-cli <subcommand> [args]',
|
|
75
|
+
'',
|
|
76
|
+
'Subcommands:',
|
|
77
|
+
' create <title> [--description <d>] [--priority <n>] [--type <t>]',
|
|
78
|
+
' [--owner <o>] [--assignee <o>] [--labels <l1,l2>]',
|
|
79
|
+
' quick <title> — create and output only the ID',
|
|
80
|
+
' get <id> — show a single task as a JSON array',
|
|
81
|
+
' list [--status <s>] [--label <l>] [--limit <n>] [--all]',
|
|
82
|
+
' update <id> [--title <t>] [--description <d>] [--priority <n>]',
|
|
83
|
+
' [--status <s>] [--owner <o>] [--assignee <o>] [--external-ref <r>]',
|
|
84
|
+
' close <id> [--reason <r>]',
|
|
85
|
+
' label-add <id> <label>',
|
|
86
|
+
'',
|
|
87
|
+
'Env vars: DISCOCLAW_TASKS_PATH, DISCOCLAW_DATA_DIR',
|
|
88
|
+
].join('\n') + '\n');
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
const rest = args.slice(1);
|
|
92
|
+
switch (subcommand) {
|
|
93
|
+
case 'create': {
|
|
94
|
+
const pos = positional(rest);
|
|
95
|
+
const title = pos[0];
|
|
96
|
+
if (!title)
|
|
97
|
+
throw new Error('create requires a title');
|
|
98
|
+
const description = argValue(rest, '--description', '-d');
|
|
99
|
+
const priorityStr = argValue(rest, '--priority', '-p');
|
|
100
|
+
const issueType = argValue(rest, '--type', '-t');
|
|
101
|
+
const owner = argValue(rest, '--owner', '--assignee');
|
|
102
|
+
const labelsStr = argValue(rest, '--labels');
|
|
103
|
+
const labels = labelsStr
|
|
104
|
+
? labelsStr.split(',').map((l) => l.trim()).filter(Boolean)
|
|
105
|
+
: undefined;
|
|
106
|
+
const priority = priorityStr != null ? Number(priorityStr) : undefined;
|
|
107
|
+
const store = await getStore();
|
|
108
|
+
const taskService = createTaskService(store);
|
|
109
|
+
const task = taskService.create({
|
|
110
|
+
title,
|
|
111
|
+
...(description !== undefined && { description }),
|
|
112
|
+
...(priority != null && Number.isFinite(priority) && { priority }),
|
|
113
|
+
...(issueType !== undefined && { issueType }),
|
|
114
|
+
...(owner !== undefined && { owner }),
|
|
115
|
+
...(labels?.length && { labels }),
|
|
116
|
+
});
|
|
117
|
+
await store.flush();
|
|
118
|
+
process.stdout.write(JSON.stringify(task) + '\n');
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case 'quick': {
|
|
122
|
+
const title = rest[0];
|
|
123
|
+
if (!title || title.startsWith('-'))
|
|
124
|
+
throw new Error('quick requires a title');
|
|
125
|
+
const store = await getStore();
|
|
126
|
+
const taskService = createTaskService(store);
|
|
127
|
+
const task = taskService.create({ title });
|
|
128
|
+
await store.flush();
|
|
129
|
+
process.stdout.write(task.id + '\n');
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case 'get':
|
|
133
|
+
case 'show': {
|
|
134
|
+
const id = rest[0];
|
|
135
|
+
if (!id)
|
|
136
|
+
throw new Error(`${subcommand} requires an id`);
|
|
137
|
+
const store = await getStore();
|
|
138
|
+
const task = store.get(id);
|
|
139
|
+
if (!task) {
|
|
140
|
+
process.stderr.write(`not found: ${id}\n`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
// Output as a JSON array to match the shape of bdShow / bd show --json.
|
|
144
|
+
process.stdout.write(JSON.stringify([task]) + '\n');
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case 'list': {
|
|
148
|
+
const status = argValue(rest, '--status', '-s');
|
|
149
|
+
const label = argValue(rest, '--label');
|
|
150
|
+
const limitStr = argValue(rest, '--limit');
|
|
151
|
+
const limit = limitStr != null ? Number(limitStr) : undefined;
|
|
152
|
+
const all = hasFlag(rest, '--all');
|
|
153
|
+
const store = await getStore();
|
|
154
|
+
const tasks = store.list({
|
|
155
|
+
status: all ? 'all' : (status ?? undefined),
|
|
156
|
+
label: label ?? undefined,
|
|
157
|
+
limit: limit != null && Number.isFinite(limit) ? limit : undefined,
|
|
158
|
+
});
|
|
159
|
+
process.stdout.write(JSON.stringify(tasks) + '\n');
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 'update': {
|
|
163
|
+
const pos = positional(rest);
|
|
164
|
+
const id = pos[0];
|
|
165
|
+
if (!id)
|
|
166
|
+
throw new Error('update requires an id');
|
|
167
|
+
const title = argValue(rest, '--title');
|
|
168
|
+
const description = argValue(rest, '--description', '-d');
|
|
169
|
+
const priorityStr = argValue(rest, '--priority', '-p');
|
|
170
|
+
const status = argValue(rest, '--status', '-s');
|
|
171
|
+
const owner = argValue(rest, '--owner', '--assignee');
|
|
172
|
+
const externalRef = argValue(rest, '--external-ref');
|
|
173
|
+
const priority = priorityStr != null ? Number(priorityStr) : undefined;
|
|
174
|
+
if (status && !isTaskStatus(status)) {
|
|
175
|
+
throw new Error(`invalid status: ${status}. Must be one of: open, in_progress, blocked, closed`);
|
|
176
|
+
}
|
|
177
|
+
const validStatus = status && isTaskStatus(status) ? status : undefined;
|
|
178
|
+
const store = await getStore();
|
|
179
|
+
const taskService = createTaskService(store);
|
|
180
|
+
taskService.update(id, {
|
|
181
|
+
...(title !== undefined && { title }),
|
|
182
|
+
...(description !== undefined && { description }),
|
|
183
|
+
...(priority != null && Number.isFinite(priority) && { priority }),
|
|
184
|
+
...(validStatus !== undefined && { status: validStatus }),
|
|
185
|
+
...(owner !== undefined && { owner }),
|
|
186
|
+
...(externalRef !== undefined && { externalRef }),
|
|
187
|
+
});
|
|
188
|
+
await store.flush();
|
|
189
|
+
process.stdout.write(JSON.stringify(store.get(id)) + '\n');
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case 'close': {
|
|
193
|
+
const pos = positional(rest);
|
|
194
|
+
const id = pos[0];
|
|
195
|
+
if (!id)
|
|
196
|
+
throw new Error('close requires an id');
|
|
197
|
+
const reason = argValue(rest, '--reason', '-r');
|
|
198
|
+
const store = await getStore();
|
|
199
|
+
const taskService = createTaskService(store);
|
|
200
|
+
taskService.close(id, reason ?? undefined);
|
|
201
|
+
await store.flush();
|
|
202
|
+
process.stdout.write(JSON.stringify(store.get(id)) + '\n');
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case 'label-add': {
|
|
206
|
+
const id = rest[0];
|
|
207
|
+
const label = rest[1];
|
|
208
|
+
if (!id || !label)
|
|
209
|
+
throw new Error('label-add requires an id and a label');
|
|
210
|
+
const store = await getStore();
|
|
211
|
+
const taskService = createTaskService(store);
|
|
212
|
+
taskService.addLabel(id, label);
|
|
213
|
+
await store.flush();
|
|
214
|
+
process.stdout.write(JSON.stringify(store.get(id)) + '\n');
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
default:
|
|
218
|
+
throw new Error(`unknown subcommand: ${subcommand}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Only execute when invoked directly as a script, not when imported as a module.
|
|
222
|
+
if (import.meta.url === new URL(process.argv[1] ?? '', 'file:').href) {
|
|
223
|
+
await main().catch((err) => {
|
|
224
|
+
process.stderr.write((err instanceof Error ? err.message : String(err)) + '\n');
|
|
225
|
+
process.exit(1);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-task lifecycle lock and ownership markers used to coordinate direct
|
|
3
|
+
* action-owned thread operations with background sync passes.
|
|
4
|
+
*/
|
|
5
|
+
const TASK_LIFECYCLE_TAILS = new Map();
|
|
6
|
+
const DIRECT_TASK_LIFECYCLE_ACTIVE = new Set();
|
|
7
|
+
/**
|
|
8
|
+
* Serialize lifecycle work for a specific task ID.
|
|
9
|
+
*/
|
|
10
|
+
export async function withTaskLifecycleLock(taskId, work) {
|
|
11
|
+
const previousTail = TASK_LIFECYCLE_TAILS.get(taskId) ?? Promise.resolve();
|
|
12
|
+
let releaseCurrent;
|
|
13
|
+
const currentSignal = new Promise((resolve) => {
|
|
14
|
+
releaseCurrent = resolve;
|
|
15
|
+
});
|
|
16
|
+
const currentTail = previousTail.then(() => currentSignal);
|
|
17
|
+
TASK_LIFECYCLE_TAILS.set(taskId, currentTail);
|
|
18
|
+
await previousTail;
|
|
19
|
+
try {
|
|
20
|
+
return await work();
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
releaseCurrent();
|
|
24
|
+
if (TASK_LIFECYCLE_TAILS.get(taskId) === currentTail) {
|
|
25
|
+
TASK_LIFECYCLE_TAILS.delete(taskId);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Mark a task as direct-action-owned while its lifecycle work is running.
|
|
31
|
+
* This is used by store-event wiring to avoid duplicate sync ownership.
|
|
32
|
+
*/
|
|
33
|
+
export async function withDirectTaskLifecycle(taskId, work) {
|
|
34
|
+
return withTaskLifecycleLock(taskId, async () => {
|
|
35
|
+
DIRECT_TASK_LIFECYCLE_ACTIVE.add(taskId);
|
|
36
|
+
try {
|
|
37
|
+
return await work();
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
DIRECT_TASK_LIFECYCLE_ACTIVE.delete(taskId);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export function isDirectTaskLifecycleActive(taskId) {
|
|
45
|
+
return DIRECT_TASK_LIFECYCLE_ACTIVE.has(taskId);
|
|
46
|
+
}
|