agentplane 0.1.8 → 0.2.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 +4 -4
- package/assets/AGENTS.md +294 -72
- package/assets/agents/CODER.json +1 -0
- package/assets/agents/INTEGRATOR.json +1 -0
- package/assets/agents/ORCHESTRATOR.json +1 -0
- package/assets/agents/PLANNER.json +1 -0
- package/assets/agents/TESTER.json +1 -0
- package/dist/backends/task-backend/load.d.ts +13 -0
- package/dist/backends/task-backend/load.d.ts.map +1 -0
- package/dist/backends/task-backend/load.js +58 -0
- package/dist/backends/task-backend/local-backend.d.ts +28 -0
- package/dist/backends/task-backend/local-backend.d.ts.map +1 -0
- package/dist/backends/task-backend/local-backend.js +335 -0
- package/dist/backends/task-backend/redmine/client.d.ts +8 -0
- package/dist/backends/task-backend/redmine/client.d.ts.map +1 -0
- package/dist/backends/task-backend/redmine/client.js +60 -0
- package/dist/backends/task-backend/redmine/comments.d.ts +12 -0
- package/dist/backends/task-backend/redmine/comments.d.ts.map +1 -0
- package/dist/backends/task-backend/redmine/comments.js +54 -0
- package/dist/backends/task-backend/redmine/fields.d.ts +9 -0
- package/dist/backends/task-backend/redmine/fields.d.ts.map +1 -0
- package/dist/backends/task-backend/redmine/fields.js +38 -0
- package/dist/backends/task-backend/redmine/mapping.d.ts +20 -0
- package/dist/backends/task-backend/redmine/mapping.d.ts.map +1 -0
- package/dist/backends/task-backend/redmine/mapping.js +114 -0
- package/dist/backends/task-backend/redmine/parse.d.ts +3 -0
- package/dist/backends/task-backend/redmine/parse.d.ts.map +1 -0
- package/dist/backends/task-backend/redmine/parse.js +27 -0
- package/dist/backends/task-backend/redmine/remote.d.ts +19 -0
- package/dist/backends/task-backend/redmine/remote.d.ts.map +1 -0
- package/dist/backends/task-backend/redmine/remote.js +82 -0
- package/dist/backends/task-backend/redmine-backend.d.ts +80 -0
- package/dist/backends/task-backend/redmine-backend.d.ts.map +1 -0
- package/dist/backends/task-backend/redmine-backend.js +505 -0
- package/dist/backends/task-backend/shared/concurrency.d.ts +3 -0
- package/dist/backends/task-backend/shared/concurrency.d.ts.map +1 -0
- package/dist/backends/task-backend/shared/concurrency.js +21 -0
- package/dist/backends/task-backend/shared/constants.d.ts +4 -0
- package/dist/backends/task-backend/shared/constants.d.ts.map +1 -0
- package/dist/backends/task-backend/shared/constants.js +4 -0
- package/dist/backends/task-backend/shared/doc.d.ts +11 -0
- package/dist/backends/task-backend/shared/doc.d.ts.map +1 -0
- package/dist/backends/task-backend/shared/doc.js +78 -0
- package/dist/backends/task-backend/shared/errors.d.ts +10 -0
- package/dist/backends/task-backend/shared/errors.d.ts.map +1 -0
- package/dist/backends/task-backend/shared/errors.js +18 -0
- package/dist/backends/task-backend/shared/events.d.ts +3 -0
- package/dist/backends/task-backend/shared/events.d.ts.map +1 -0
- package/dist/backends/task-backend/shared/events.js +29 -0
- package/dist/backends/task-backend/shared/export.d.ts +15 -0
- package/dist/backends/task-backend/shared/export.d.ts.map +1 -0
- package/dist/backends/task-backend/shared/export.js +60 -0
- package/dist/backends/task-backend/shared/id.d.ts +13 -0
- package/dist/backends/task-backend/shared/id.d.ts.map +1 -0
- package/dist/backends/task-backend/shared/id.js +17 -0
- package/dist/backends/task-backend/shared/normalize.d.ts +8 -0
- package/dist/backends/task-backend/shared/normalize.d.ts.map +1 -0
- package/dist/backends/task-backend/shared/normalize.js +54 -0
- package/dist/backends/task-backend/shared/record.d.ts +4 -0
- package/dist/backends/task-backend/shared/record.d.ts.map +1 -0
- package/dist/backends/task-backend/shared/record.js +53 -0
- package/dist/backends/task-backend/shared/strings.d.ts +4 -0
- package/dist/backends/task-backend/shared/strings.d.ts.map +1 -0
- package/dist/backends/task-backend/shared/strings.js +21 -0
- package/dist/backends/task-backend/shared/types.d.ts +84 -0
- package/dist/backends/task-backend/shared/types.d.ts.map +1 -0
- package/dist/backends/task-backend/shared/types.js +1 -0
- package/dist/backends/task-backend/shared.d.ts +11 -0
- package/dist/backends/task-backend/shared.d.ts.map +1 -0
- package/dist/backends/task-backend/shared.js +9 -0
- package/dist/backends/task-backend.d.ts +4 -192
- package/dist/backends/task-backend.d.ts.map +1 -1
- package/dist/backends/task-backend.js +4 -1329
- package/dist/backends/task-index.js +2 -2
- package/dist/cli/archive.d.ts +0 -2
- package/dist/cli/archive.d.ts.map +1 -1
- package/dist/cli/archive.js +1 -2
- package/dist/cli/command-guide.d.ts.map +1 -1
- package/dist/cli/command-guide.js +28 -12
- package/dist/cli/parse/lifecycle.d.ts +64 -0
- package/dist/cli/parse/lifecycle.d.ts.map +1 -0
- package/dist/cli/parse/lifecycle.js +280 -0
- package/dist/cli/run-cli/command-catalog.d.ts +16 -0
- package/dist/cli/run-cli/command-catalog.d.ts.map +1 -0
- package/dist/cli/run-cli/command-catalog.js +204 -0
- package/dist/cli/run-cli/commands/config.d.ts +20 -0
- package/dist/cli/run-cli/commands/config.d.ts.map +1 -0
- package/dist/cli/run-cli/commands/config.js +130 -0
- package/dist/cli/run-cli/commands/core.d.ts +14 -0
- package/dist/cli/run-cli/commands/core.d.ts.map +1 -0
- package/dist/cli/run-cli/commands/core.js +144 -0
- package/dist/cli/run-cli/commands/ide.d.ts +13 -0
- package/dist/cli/run-cli/commands/ide.d.ts.map +1 -0
- package/dist/cli/run-cli/commands/ide.js +67 -0
- package/dist/cli/run-cli/commands/init/base-branch.d.ts +9 -0
- package/dist/cli/run-cli/commands/init/base-branch.d.ts.map +1 -0
- package/dist/cli/run-cli/commands/init/base-branch.js +11 -0
- package/dist/cli/run-cli/commands/init/conflicts.d.ts +11 -0
- package/dist/cli/run-cli/commands/init/conflicts.d.ts.map +1 -0
- package/dist/cli/run-cli/commands/init/conflicts.js +42 -0
- package/dist/cli/run-cli/commands/init/git.d.ts +8 -0
- package/dist/cli/run-cli/commands/init/git.d.ts.map +1 -0
- package/dist/cli/run-cli/commands/init/git.js +12 -0
- package/dist/cli/run-cli/commands/init/ide-sync.d.ts +9 -0
- package/dist/cli/run-cli/commands/init/ide-sync.d.ts.map +1 -0
- package/dist/cli/run-cli/commands/init/ide-sync.js +18 -0
- package/dist/cli/run-cli/commands/init/recipes.d.ts +2 -0
- package/dist/cli/run-cli/commands/init/recipes.d.ts.map +1 -0
- package/dist/cli/run-cli/commands/init/recipes.js +11 -0
- package/dist/cli/run-cli/commands/init/write-agents.d.ts +11 -0
- package/dist/cli/run-cli/commands/init/write-agents.d.ts.map +1 -0
- package/dist/cli/run-cli/commands/init/write-agents.js +30 -0
- package/dist/cli/run-cli/commands/init/write-config.d.ts +15 -0
- package/dist/cli/run-cli/commands/init/write-config.d.ts.map +1 -0
- package/dist/cli/run-cli/commands/init/write-config.js +45 -0
- package/dist/cli/run-cli/commands/init.d.ts +21 -0
- package/dist/cli/run-cli/commands/init.d.ts.map +1 -0
- package/dist/cli/run-cli/commands/init.js +332 -0
- package/dist/cli/run-cli/registry.run.d.ts +4 -0
- package/dist/cli/run-cli/registry.run.d.ts.map +1 -0
- package/dist/cli/run-cli/registry.run.js +19 -0
- package/dist/cli/run-cli.d.ts.map +1 -1
- package/dist/cli/run-cli.js +182 -2408
- package/dist/cli/spec/docs-render.d.ts +3 -0
- package/dist/cli/spec/docs-render.d.ts.map +1 -0
- package/dist/cli/spec/docs-render.js +118 -0
- package/dist/cli/spec/errors.d.ts +9 -0
- package/dist/cli/spec/errors.d.ts.map +1 -0
- package/dist/cli/spec/errors.js +18 -0
- package/dist/cli/spec/help-render.d.ts +43 -0
- package/dist/cli/spec/help-render.d.ts.map +1 -0
- package/dist/cli/spec/help-render.js +185 -0
- package/dist/cli/spec/help.d.ts +10 -0
- package/dist/cli/spec/help.d.ts.map +1 -0
- package/dist/cli/spec/help.js +64 -0
- package/dist/cli/spec/parse.d.ts +8 -0
- package/dist/cli/spec/parse.d.ts.map +1 -0
- package/dist/cli/spec/parse.js +188 -0
- package/dist/cli/spec/registry.d.ts +12 -0
- package/dist/cli/spec/registry.d.ts.map +1 -0
- package/dist/cli/spec/registry.js +47 -0
- package/dist/cli/spec/spec.d.ts +76 -0
- package/dist/cli/spec/spec.d.ts.map +1 -0
- package/dist/cli/spec/spec.js +1 -0
- package/dist/cli/spec/suggest.d.ts +2 -0
- package/dist/cli/spec/suggest.d.ts.map +1 -0
- package/dist/cli/spec/suggest.js +45 -0
- package/dist/commands/backend/sync.command.d.ts +12 -0
- package/dist/commands/backend/sync.command.d.ts.map +1 -0
- package/dist/commands/backend/sync.command.js +79 -0
- package/dist/commands/backend.d.ts +21 -8
- package/dist/commands/backend.d.ts.map +1 -1
- package/dist/commands/backend.js +28 -165
- package/dist/commands/block.command.d.ts +19 -0
- package/dist/commands/block.command.d.ts.map +1 -0
- package/dist/commands/block.command.js +143 -0
- package/dist/commands/branch/base.command.d.ts +20 -0
- package/dist/commands/branch/base.command.d.ts.map +1 -0
- package/dist/commands/branch/base.command.js +110 -0
- package/dist/commands/branch/base.d.ts +19 -0
- package/dist/commands/branch/base.d.ts.map +1 -0
- package/dist/commands/branch/base.js +114 -0
- package/dist/commands/branch/cleanup-merged.d.ts +11 -0
- package/dist/commands/branch/cleanup-merged.d.ts.map +1 -0
- package/dist/commands/branch/cleanup-merged.js +141 -0
- package/dist/commands/branch/index.d.ts +6 -59
- package/dist/commands/branch/index.d.ts.map +1 -1
- package/dist/commands/branch/index.js +6 -513
- package/dist/commands/branch/internal/archive-pr.d.ts +2 -0
- package/dist/commands/branch/internal/archive-pr.d.ts.map +1 -0
- package/dist/commands/branch/internal/archive-pr.js +17 -0
- package/dist/commands/branch/internal/work-validate.d.ts +3 -0
- package/dist/commands/branch/internal/work-validate.d.ts.map +1 -0
- package/dist/commands/branch/internal/work-validate.js +27 -0
- package/dist/commands/branch/remove.command.d.ts +10 -0
- package/dist/commands/branch/remove.command.d.ts.map +1 -0
- package/dist/commands/branch/remove.command.js +63 -0
- package/dist/commands/branch/remove.d.ts +9 -0
- package/dist/commands/branch/remove.d.ts.map +1 -0
- package/dist/commands/branch/remove.js +65 -0
- package/dist/commands/branch/status.command.d.ts +8 -0
- package/dist/commands/branch/status.command.d.ts.map +1 -0
- package/dist/commands/branch/status.command.js +36 -0
- package/dist/commands/branch/status.d.ts +7 -0
- package/dist/commands/branch/status.d.ts.map +1 -0
- package/dist/commands/branch/status.js +60 -0
- package/dist/commands/branch/work-start.command.d.ts +11 -0
- package/dist/commands/branch/work-start.command.d.ts.map +1 -0
- package/dist/commands/branch/work-start.command.js +80 -0
- package/dist/commands/branch/work-start.d.ts +11 -0
- package/dist/commands/branch/work-start.d.ts.map +1 -0
- package/dist/commands/branch/work-start.js +120 -0
- package/dist/commands/cleanup/merged.command.d.ts +17 -0
- package/dist/commands/cleanup/merged.command.d.ts.map +1 -0
- package/dist/commands/cleanup/merged.command.js +75 -0
- package/dist/commands/commit.command.d.ts +6 -0
- package/dist/commands/commit.command.d.ts.map +1 -0
- package/dist/commands/commit.command.js +24 -0
- package/dist/commands/commit.spec.d.ts +18 -0
- package/dist/commands/commit.spec.d.ts.map +1 -0
- package/dist/commands/commit.spec.js +119 -0
- package/dist/commands/docs/cli.command.d.ts +9 -0
- package/dist/commands/docs/cli.command.d.ts.map +1 -0
- package/dist/commands/docs/cli.command.js +51 -0
- package/dist/commands/finish.command.d.ts +28 -0
- package/dist/commands/finish.command.d.ts.map +1 -0
- package/dist/commands/finish.command.js +237 -0
- package/dist/commands/guard/clean.command.d.ts +7 -0
- package/dist/commands/guard/clean.command.d.ts.map +1 -0
- package/dist/commands/guard/clean.command.js +14 -0
- package/dist/commands/guard/commit.command.d.ts +19 -0
- package/dist/commands/guard/commit.command.d.ts.map +1 -0
- package/dist/commands/guard/commit.command.js +132 -0
- package/dist/commands/guard/guard.command.d.ts +5 -0
- package/dist/commands/guard/guard.command.d.ts.map +1 -0
- package/dist/commands/guard/guard.command.js +21 -0
- package/dist/commands/guard/impl/allow.d.ts +18 -0
- package/dist/commands/guard/impl/allow.d.ts.map +1 -0
- package/dist/commands/guard/impl/allow.js +77 -0
- package/dist/commands/guard/impl/close-message.d.ts +16 -0
- package/dist/commands/guard/impl/close-message.d.ts.map +1 -0
- package/dist/commands/guard/impl/close-message.js +156 -0
- package/dist/commands/guard/impl/commands.d.ts +32 -0
- package/dist/commands/guard/impl/commands.d.ts.map +1 -0
- package/dist/commands/guard/impl/commands.js +191 -0
- package/dist/commands/guard/impl/comment-commit.d.ts +25 -0
- package/dist/commands/guard/impl/comment-commit.d.ts.map +1 -0
- package/dist/commands/guard/impl/comment-commit.js +137 -0
- package/dist/commands/guard/impl/env.d.ts +10 -0
- package/dist/commands/guard/impl/env.d.ts.map +1 -0
- package/dist/commands/guard/impl/env.js +12 -0
- package/dist/commands/guard/impl/policy.d.ts +19 -0
- package/dist/commands/guard/impl/policy.d.ts.map +1 -0
- package/dist/commands/guard/impl/policy.js +46 -0
- package/dist/commands/guard/index.d.ts +4 -66
- package/dist/commands/guard/index.d.ts.map +1 -1
- package/dist/commands/guard/index.js +4 -367
- package/dist/commands/guard/suggest-allow.command.d.ts +7 -0
- package/dist/commands/guard/suggest-allow.command.d.ts.map +1 -0
- package/dist/commands/guard/suggest-allow.command.js +28 -0
- package/dist/commands/hooks/hooks.command.d.ts +5 -0
- package/dist/commands/hooks/hooks.command.d.ts.map +1 -0
- package/dist/commands/hooks/hooks.command.js +18 -0
- package/dist/commands/hooks/index.d.ts.map +1 -1
- package/dist/commands/hooks/index.js +42 -77
- package/dist/commands/hooks/install.command.d.ts +7 -0
- package/dist/commands/hooks/install.command.d.ts.map +1 -0
- package/dist/commands/hooks/install.command.js +14 -0
- package/dist/commands/hooks/run.command.d.ts +9 -0
- package/dist/commands/hooks/run.command.d.ts.map +1 -0
- package/dist/commands/hooks/run.command.js +39 -0
- package/dist/commands/hooks/uninstall.command.d.ts +7 -0
- package/dist/commands/hooks/uninstall.command.d.ts.map +1 -0
- package/dist/commands/hooks/uninstall.command.js +16 -0
- package/dist/commands/integrate.command.d.ts +14 -0
- package/dist/commands/integrate.command.d.ts.map +1 -0
- package/dist/commands/integrate.command.js +61 -0
- package/dist/commands/pr/check.d.ts +8 -0
- package/dist/commands/pr/check.d.ts.map +1 -0
- package/dist/commands/pr/check.js +78 -0
- package/dist/commands/pr/index.d.ts +5 -45
- package/dist/commands/pr/index.d.ts.map +1 -1
- package/dist/commands/pr/index.js +5 -857
- package/dist/commands/pr/integrate/artifacts.d.ts +14 -0
- package/dist/commands/pr/integrate/artifacts.d.ts.map +1 -0
- package/dist/commands/pr/integrate/artifacts.js +45 -0
- package/dist/commands/pr/integrate/cmd.d.ts +14 -0
- package/dist/commands/pr/integrate/cmd.d.ts.map +1 -0
- package/dist/commands/pr/integrate/cmd.js +150 -0
- package/dist/commands/pr/integrate/internal/finalize.d.ts +25 -0
- package/dist/commands/pr/integrate/internal/finalize.d.ts.map +1 -0
- package/dist/commands/pr/integrate/internal/finalize.js +86 -0
- package/dist/commands/pr/integrate/internal/merge.d.ts +40 -0
- package/dist/commands/pr/integrate/internal/merge.d.ts.map +1 -0
- package/dist/commands/pr/integrate/internal/merge.js +138 -0
- package/dist/commands/pr/integrate/internal/prepare.d.ts +33 -0
- package/dist/commands/pr/integrate/internal/prepare.d.ts.map +1 -0
- package/dist/commands/pr/integrate/internal/prepare.js +142 -0
- package/dist/commands/pr/integrate/internal/worktree.d.ts +14 -0
- package/dist/commands/pr/integrate/internal/worktree.d.ts.map +1 -0
- package/dist/commands/pr/integrate/internal/worktree.js +51 -0
- package/dist/commands/pr/integrate/verify.d.ts +22 -0
- package/dist/commands/pr/integrate/verify.d.ts.map +1 -0
- package/dist/commands/pr/integrate/verify.js +60 -0
- package/dist/commands/pr/integrate.d.ts +2 -0
- package/dist/commands/pr/integrate.d.ts.map +1 -0
- package/dist/commands/pr/integrate.js +1 -0
- package/dist/commands/pr/internal/pr-paths.d.ts +29 -0
- package/dist/commands/pr/internal/pr-paths.d.ts.map +1 -0
- package/dist/commands/pr/internal/pr-paths.js +38 -0
- package/dist/commands/pr/internal/review-template.d.ts +9 -0
- package/dist/commands/pr/internal/review-template.d.ts.map +1 -0
- package/dist/commands/pr/internal/review-template.js +62 -0
- package/dist/commands/pr/note.d.ts +10 -0
- package/dist/commands/pr/note.d.ts.map +1 -0
- package/dist/commands/pr/note.js +50 -0
- package/dist/commands/pr/open.d.ts +10 -0
- package/dist/commands/pr/open.d.ts.map +1 -0
- package/dist/commands/pr/open.js +80 -0
- package/dist/commands/pr/pr.command.d.ts +33 -0
- package/dist/commands/pr/pr.command.d.ts.map +1 -0
- package/dist/commands/pr/pr.command.js +172 -0
- package/dist/commands/pr/update.d.ts +8 -0
- package/dist/commands/pr/update.d.ts.map +1 -0
- package/dist/commands/pr/update.js +103 -0
- package/dist/commands/ready.command.d.ts +8 -0
- package/dist/commands/ready.command.d.ts.map +1 -0
- package/dist/commands/ready.command.js +28 -0
- package/dist/commands/recipes/cache-prune.command.d.ts +6 -0
- package/dist/commands/recipes/cache-prune.command.d.ts.map +1 -0
- package/dist/commands/recipes/cache-prune.command.js +30 -0
- package/dist/commands/recipes/cache.command.d.ts +6 -0
- package/dist/commands/recipes/cache.command.d.ts.map +1 -0
- package/dist/commands/recipes/cache.command.js +37 -0
- package/dist/commands/recipes/explain.command.d.ts +7 -0
- package/dist/commands/recipes/explain.command.d.ts.map +1 -0
- package/dist/commands/recipes/explain.command.js +10 -0
- package/dist/commands/recipes/impl/apply.d.ts +16 -0
- package/dist/commands/recipes/impl/apply.d.ts.map +1 -0
- package/dist/commands/recipes/impl/apply.js +97 -0
- package/dist/commands/recipes/impl/archive.d.ts +2 -0
- package/dist/commands/recipes/impl/archive.d.ts.map +1 -0
- package/dist/commands/recipes/impl/archive.js +19 -0
- package/dist/commands/recipes/impl/commands/cache-prune.d.ts +7 -0
- package/dist/commands/recipes/impl/commands/cache-prune.d.ts.map +1 -0
- package/dist/commands/recipes/impl/commands/cache-prune.js +94 -0
- package/dist/commands/recipes/impl/commands/explain.d.ts +6 -0
- package/dist/commands/recipes/impl/commands/explain.d.ts.map +1 -0
- package/dist/commands/recipes/impl/commands/explain.js +88 -0
- package/dist/commands/recipes/impl/commands/info.d.ts +6 -0
- package/dist/commands/recipes/impl/commands/info.d.ts.map +1 -0
- package/dist/commands/recipes/impl/commands/info.js +58 -0
- package/dist/commands/recipes/impl/commands/install.d.ts +11 -0
- package/dist/commands/recipes/impl/commands/install.d.ts.map +1 -0
- package/dist/commands/recipes/impl/commands/install.js +212 -0
- package/dist/commands/recipes/impl/commands/list-remote.d.ts +7 -0
- package/dist/commands/recipes/impl/commands/list-remote.d.ts.map +1 -0
- package/dist/commands/recipes/impl/commands/list-remote.js +53 -0
- package/dist/commands/recipes/impl/commands/list.d.ts +7 -0
- package/dist/commands/recipes/impl/commands/list.d.ts.map +1 -0
- package/dist/commands/recipes/impl/commands/list.js +38 -0
- package/dist/commands/recipes/impl/commands/remove.d.ts +6 -0
- package/dist/commands/recipes/impl/commands/remove.d.ts.map +1 -0
- package/dist/commands/recipes/impl/commands/remove.js +35 -0
- package/dist/commands/recipes/impl/commands.d.ts +8 -0
- package/dist/commands/recipes/impl/commands.d.ts.map +1 -0
- package/dist/commands/recipes/impl/commands.js +7 -0
- package/dist/commands/recipes/impl/constants.d.ts +13 -0
- package/dist/commands/recipes/impl/constants.d.ts.map +1 -0
- package/dist/commands/recipes/impl/constants.js +16 -0
- package/dist/commands/recipes/impl/format.d.ts +2 -0
- package/dist/commands/recipes/impl/format.d.ts.map +1 -0
- package/dist/commands/recipes/impl/format.js +9 -0
- package/dist/commands/recipes/impl/index.d.ts +12 -0
- package/dist/commands/recipes/impl/index.d.ts.map +1 -0
- package/dist/commands/recipes/impl/index.js +150 -0
- package/dist/commands/recipes/impl/installed-recipes.d.ts +4 -0
- package/dist/commands/recipes/impl/installed-recipes.d.ts.map +1 -0
- package/dist/commands/recipes/impl/installed-recipes.js +58 -0
- package/dist/commands/recipes/impl/manifest.d.ts +4 -0
- package/dist/commands/recipes/impl/manifest.d.ts.map +1 -0
- package/dist/commands/recipes/impl/manifest.js +43 -0
- package/dist/commands/recipes/impl/normalize.d.ts +5 -0
- package/dist/commands/recipes/impl/normalize.d.ts.map +1 -0
- package/dist/commands/recipes/impl/normalize.js +50 -0
- package/dist/commands/recipes/impl/paths.d.ts +13 -0
- package/dist/commands/recipes/impl/paths.d.ts.map +1 -0
- package/dist/commands/recipes/impl/paths.js +27 -0
- package/dist/commands/recipes/impl/project.d.ts +8 -0
- package/dist/commands/recipes/impl/project.d.ts.map +1 -0
- package/dist/commands/recipes/impl/project.js +23 -0
- package/dist/commands/recipes/impl/scenario.d.ts +16 -0
- package/dist/commands/recipes/impl/scenario.d.ts.map +1 -0
- package/dist/commands/recipes/impl/scenario.js +128 -0
- package/dist/commands/recipes/impl/types.d.ts +107 -0
- package/dist/commands/recipes/impl/types.d.ts.map +1 -0
- package/dist/commands/recipes/impl/types.js +1 -0
- package/dist/commands/recipes/info.command.d.ts +7 -0
- package/dist/commands/recipes/info.command.d.ts.map +1 -0
- package/dist/commands/recipes/info.command.js +10 -0
- package/dist/commands/recipes/install.command.d.ts +12 -0
- package/dist/commands/recipes/install.command.d.ts.map +1 -0
- package/dist/commands/recipes/install.command.js +161 -0
- package/dist/commands/recipes/list-remote.command.d.ts +6 -0
- package/dist/commands/recipes/list-remote.command.d.ts.map +1 -0
- package/dist/commands/recipes/list-remote.command.js +46 -0
- package/dist/commands/recipes/list.command.d.ts +6 -0
- package/dist/commands/recipes/list.command.d.ts.map +1 -0
- package/dist/commands/recipes/list.command.js +39 -0
- package/dist/commands/recipes/recipes.command.d.ts +6 -0
- package/dist/commands/recipes/recipes.command.d.ts.map +1 -0
- package/dist/commands/recipes/recipes.command.js +45 -0
- package/dist/commands/recipes/remove.command.d.ts +7 -0
- package/dist/commands/recipes/remove.command.d.ts.map +1 -0
- package/dist/commands/recipes/remove.command.js +10 -0
- package/dist/commands/recipes.d.ts +7 -12
- package/dist/commands/recipes.d.ts.map +1 -1
- package/dist/commands/recipes.js +6 -1963
- package/dist/commands/scenario/impl/commands.d.ts +19 -0
- package/dist/commands/scenario/impl/commands.d.ts.map +1 -0
- package/dist/commands/scenario/impl/commands.js +336 -0
- package/dist/commands/scenario/impl/report.d.ts +30 -0
- package/dist/commands/scenario/impl/report.d.ts.map +1 -0
- package/dist/commands/scenario/impl/report.js +99 -0
- package/dist/commands/scenario/info.command.d.ts +8 -0
- package/dist/commands/scenario/info.command.d.ts.map +1 -0
- package/dist/commands/scenario/info.command.js +27 -0
- package/dist/commands/scenario/list.command.d.ts +5 -0
- package/dist/commands/scenario/list.command.d.ts.map +1 -0
- package/dist/commands/scenario/list.command.js +9 -0
- package/dist/commands/scenario/run.command.d.ts +8 -0
- package/dist/commands/scenario/run.command.d.ts.map +1 -0
- package/dist/commands/scenario/run.command.js +27 -0
- package/dist/commands/scenario/scenario.command.d.ts +6 -0
- package/dist/commands/scenario/scenario.command.d.ts.map +1 -0
- package/dist/commands/scenario/scenario.command.js +37 -0
- package/dist/commands/scenario.d.ts +2 -0
- package/dist/commands/scenario.d.ts.map +1 -0
- package/dist/commands/scenario.js +1 -0
- package/dist/commands/shared/git-context.d.ts +23 -0
- package/dist/commands/shared/git-context.d.ts.map +1 -0
- package/dist/commands/shared/git-context.js +140 -0
- package/dist/commands/shared/policy-deny.d.ts +3 -0
- package/dist/commands/shared/policy-deny.d.ts.map +1 -0
- package/dist/commands/shared/policy-deny.js +12 -0
- package/dist/commands/shared/task-backend.d.ts +31 -3
- package/dist/commands/shared/task-backend.d.ts.map +1 -1
- package/dist/commands/shared/task-backend.js +42 -5
- package/dist/commands/shared/task-store.d.ts +16 -0
- package/dist/commands/shared/task-store.d.ts.map +1 -0
- package/dist/commands/shared/task-store.js +142 -0
- package/dist/commands/start.command.d.ts +19 -0
- package/dist/commands/start.command.d.ts.map +1 -0
- package/dist/commands/start.command.js +143 -0
- package/dist/commands/sync.command.d.ts +6 -0
- package/dist/commands/sync.command.d.ts.map +1 -0
- package/dist/commands/sync.command.js +57 -0
- package/dist/commands/task/add.command.d.ts +18 -0
- package/dist/commands/task/add.command.d.ts.map +1 -0
- package/dist/commands/task/add.command.js +157 -0
- package/dist/commands/task/add.d.ts +13 -3
- package/dist/commands/task/add.d.ts.map +1 -1
- package/dist/commands/task/add.js +21 -126
- package/dist/commands/task/block.d.ts +2 -2
- package/dist/commands/task/block.d.ts.map +1 -1
- package/dist/commands/task/block.js +31 -21
- package/dist/commands/task/comment.command.d.ts +10 -0
- package/dist/commands/task/comment.command.d.ts.map +1 -0
- package/dist/commands/task/comment.command.js +57 -0
- package/dist/commands/task/comment.d.ts +2 -0
- package/dist/commands/task/comment.d.ts.map +1 -1
- package/dist/commands/task/comment.js +19 -9
- package/dist/commands/task/derive.command.d.ts +13 -0
- package/dist/commands/task/derive.command.d.ts.map +1 -0
- package/dist/commands/task/derive.command.js +94 -0
- package/dist/commands/task/derive.d.ts +13 -0
- package/dist/commands/task/derive.d.ts.map +1 -0
- package/dist/commands/task/derive.js +71 -0
- package/dist/commands/task/doc-set.command.d.ts +12 -0
- package/dist/commands/task/doc-set.command.d.ts.map +1 -0
- package/dist/commands/task/doc-set.command.js +82 -0
- package/dist/commands/task/doc-show.command.d.ts +10 -0
- package/dist/commands/task/doc-show.command.d.ts.map +1 -0
- package/dist/commands/task/doc-show.command.js +54 -0
- package/dist/commands/task/doc.command.d.ts +7 -0
- package/dist/commands/task/doc.command.d.ts.map +1 -0
- package/dist/commands/task/doc.command.js +22 -0
- package/dist/commands/task/doc.d.ts +9 -6
- package/dist/commands/task/doc.d.ts.map +1 -1
- package/dist/commands/task/doc.js +61 -113
- package/dist/commands/task/export.command.d.ts +6 -0
- package/dist/commands/task/export.command.d.ts.map +1 -0
- package/dist/commands/task/export.command.js +17 -0
- package/dist/commands/task/export.d.ts +2 -0
- package/dist/commands/task/export.d.ts.map +1 -1
- package/dist/commands/task/export.js +7 -9
- package/dist/commands/task/finish.d.ts +5 -4
- package/dist/commands/task/finish.d.ts.map +1 -1
- package/dist/commands/task/finish.js +80 -37
- package/dist/commands/task/index.d.ts +14 -13
- package/dist/commands/task/index.d.ts.map +1 -1
- package/dist/commands/task/index.js +13 -13
- package/dist/commands/task/lint.command.d.ts +5 -0
- package/dist/commands/task/lint.command.d.ts.map +1 -0
- package/dist/commands/task/lint.command.js +11 -0
- package/dist/commands/task/list.command.d.ts +9 -0
- package/dist/commands/task/list.command.d.ts.map +1 -0
- package/dist/commands/task/list.command.js +68 -0
- package/dist/commands/task/list.d.ts +6 -2
- package/dist/commands/task/list.d.ts.map +1 -1
- package/dist/commands/task/list.js +12 -15
- package/dist/commands/task/migrate-doc.command.d.ts +9 -0
- package/dist/commands/task/migrate-doc.command.d.ts.map +1 -0
- package/dist/commands/task/migrate-doc.command.js +65 -0
- package/dist/commands/task/migrate-doc.d.ts +3 -3
- package/dist/commands/task/migrate-doc.d.ts.map +1 -1
- package/dist/commands/task/migrate-doc.js +40 -47
- package/dist/commands/task/migrate.command.d.ts +10 -0
- package/dist/commands/task/migrate.command.d.ts.map +1 -0
- package/dist/commands/task/migrate.command.js +50 -0
- package/dist/commands/task/migrate.d.ts +5 -1
- package/dist/commands/task/migrate.d.ts.map +1 -1
- package/dist/commands/task/migrate.js +10 -44
- package/dist/commands/task/new.command.d.ts +6 -0
- package/dist/commands/task/new.command.d.ts.map +1 -0
- package/dist/commands/task/new.command.js +13 -0
- package/dist/commands/task/new.d.ts +13 -4
- package/dist/commands/task/new.d.ts.map +1 -1
- package/dist/commands/task/new.js +30 -92
- package/dist/commands/task/new.spec.d.ts +4 -0
- package/dist/commands/task/new.spec.d.ts.map +1 -0
- package/dist/commands/task/new.spec.js +79 -0
- package/dist/commands/task/next.command.d.ts +9 -0
- package/dist/commands/task/next.command.d.ts.map +1 -0
- package/dist/commands/task/next.command.js +89 -0
- package/dist/commands/task/next.d.ts +4 -1
- package/dist/commands/task/next.d.ts.map +1 -1
- package/dist/commands/task/next.js +15 -16
- package/dist/commands/task/normalize.command.d.ts +9 -0
- package/dist/commands/task/normalize.command.d.ts.map +1 -0
- package/dist/commands/task/normalize.command.js +39 -0
- package/dist/commands/task/normalize.d.ts +4 -1
- package/dist/commands/task/normalize.d.ts.map +1 -1
- package/dist/commands/task/normalize.js +18 -31
- package/dist/commands/task/plan-approve.command.d.ts +10 -0
- package/dist/commands/task/plan-approve.command.d.ts.map +1 -0
- package/dist/commands/task/plan-approve.command.js +54 -0
- package/dist/commands/task/plan-reject.command.d.ts +10 -0
- package/dist/commands/task/plan-reject.command.d.ts.map +1 -0
- package/dist/commands/task/plan-reject.command.js +59 -0
- package/dist/commands/task/plan-set.command.d.ts +11 -0
- package/dist/commands/task/plan-set.command.d.ts.map +1 -0
- package/dist/commands/task/plan-set.command.js +76 -0
- package/dist/commands/task/plan.d.ts +23 -10
- package/dist/commands/task/plan.d.ts.map +1 -1
- package/dist/commands/task/plan.js +182 -177
- package/dist/commands/task/ready.d.ts +2 -0
- package/dist/commands/task/ready.d.ts.map +1 -1
- package/dist/commands/task/ready.js +4 -6
- package/dist/commands/task/scaffold.command.d.ts +12 -0
- package/dist/commands/task/scaffold.command.d.ts.map +1 -0
- package/dist/commands/task/scaffold.command.js +73 -0
- package/dist/commands/task/scaffold.d.ts +7 -3
- package/dist/commands/task/scaffold.d.ts.map +1 -1
- package/dist/commands/task/scaffold.js +57 -67
- package/dist/commands/task/scrub.command.d.ts +11 -0
- package/dist/commands/task/scrub.command.d.ts.map +1 -0
- package/dist/commands/task/scrub.command.js +72 -0
- package/dist/commands/task/scrub.d.ts +6 -3
- package/dist/commands/task/scrub.d.ts.map +1 -1
- package/dist/commands/task/scrub.js +12 -68
- package/dist/commands/task/search.command.d.ts +11 -0
- package/dist/commands/task/search.command.d.ts.map +1 -0
- package/dist/commands/task/search.command.js +101 -0
- package/dist/commands/task/search.d.ts +5 -1
- package/dist/commands/task/search.d.ts.map +1 -1
- package/dist/commands/task/search.js +14 -23
- package/dist/commands/task/set-status.command.d.ts +21 -0
- package/dist/commands/task/set-status.command.d.ts.map +1 -0
- package/dist/commands/task/set-status.command.js +171 -0
- package/dist/commands/task/set-status.d.ts +2 -0
- package/dist/commands/task/set-status.d.ts.map +1 -1
- package/dist/commands/task/set-status.js +32 -12
- package/dist/commands/task/shared.d.ts +8 -2
- package/dist/commands/task/shared.d.ts.map +1 -1
- package/dist/commands/task/shared.js +68 -28
- package/dist/commands/task/show.command.d.ts +8 -0
- package/dist/commands/task/show.command.d.ts.map +1 -0
- package/dist/commands/task/show.command.js +19 -0
- package/dist/commands/task/show.d.ts +2 -0
- package/dist/commands/task/show.d.ts.map +1 -1
- package/dist/commands/task/show.js +5 -7
- package/dist/commands/task/start.d.ts +2 -2
- package/dist/commands/task/start.d.ts.map +1 -1
- package/dist/commands/task/start.js +66 -23
- package/dist/commands/task/update.command.d.ts +18 -0
- package/dist/commands/task/update.command.d.ts.map +1 -0
- package/dist/commands/task/update.command.js +141 -0
- package/dist/commands/task/update.d.ts +13 -3
- package/dist/commands/task/update.d.ts.map +1 -1
- package/dist/commands/task/update.js +31 -122
- package/dist/commands/task/verify-ok.command.d.ts +13 -0
- package/dist/commands/task/verify-ok.command.d.ts.map +1 -0
- package/dist/commands/task/verify-ok.command.js +83 -0
- package/dist/commands/task/verify-record.d.ts +30 -8
- package/dist/commands/task/verify-record.d.ts.map +1 -1
- package/dist/commands/task/verify-record.js +114 -117
- package/dist/commands/task/verify-rework.command.d.ts +13 -0
- package/dist/commands/task/verify-rework.command.d.ts.map +1 -0
- package/dist/commands/task/verify-rework.command.js +83 -0
- package/dist/commands/task/verify-show.command.d.ts +9 -0
- package/dist/commands/task/verify-show.command.d.ts.map +1 -0
- package/dist/commands/task/verify-show.command.js +38 -0
- package/dist/commands/task/verify.command.d.ts +7 -0
- package/dist/commands/task/verify.command.d.ts.map +1 -0
- package/dist/commands/task/verify.command.js +20 -0
- package/dist/commands/upgrade.command.d.ts +6 -0
- package/dist/commands/upgrade.command.d.ts.map +1 -0
- package/dist/commands/upgrade.command.js +104 -0
- package/dist/commands/upgrade.d.ts +13 -2
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +12 -80
- package/dist/commands/verify.command.d.ts +16 -0
- package/dist/commands/verify.command.d.ts.map +1 -0
- package/dist/commands/verify.command.js +113 -0
- package/dist/commands/workflow.d.ts +4 -6
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +4 -7
- package/dist/meta/release.d.ts +2 -0
- package/dist/meta/release.d.ts.map +1 -0
- package/dist/meta/release.js +50 -0
- package/dist/policy/evaluate.d.ts +3 -0
- package/dist/policy/evaluate.d.ts.map +1 -0
- package/dist/policy/evaluate.js +27 -0
- package/dist/policy/result.d.ts +7 -0
- package/dist/policy/result.d.ts.map +1 -0
- package/dist/policy/result.js +21 -0
- package/dist/policy/rules/allowlist.d.ts +3 -0
- package/dist/policy/rules/allowlist.d.ts.map +1 -0
- package/dist/policy/rules/allowlist.js +30 -0
- package/dist/policy/rules/branch-pr-base.d.ts +3 -0
- package/dist/policy/rules/branch-pr-base.d.ts.map +1 -0
- package/dist/policy/rules/branch-pr-base.js +43 -0
- package/dist/policy/rules/clean-tree.d.ts +3 -0
- package/dist/policy/rules/clean-tree.d.ts.map +1 -0
- package/dist/policy/rules/clean-tree.js +19 -0
- package/dist/policy/rules/commit-subject.d.ts +3 -0
- package/dist/policy/rules/commit-subject.d.ts.map +1 -0
- package/dist/policy/rules/commit-subject.js +33 -0
- package/dist/policy/rules/protected-paths.d.ts +3 -0
- package/dist/policy/rules/protected-paths.d.ts.map +1 -0
- package/dist/policy/rules/protected-paths.js +53 -0
- package/dist/policy/types.d.ts +38 -0
- package/dist/policy/types.d.ts.map +1 -0
- package/dist/policy/types.js +1 -0
- package/dist/shared/git-log.d.ts +5 -0
- package/dist/shared/git-log.d.ts.map +1 -0
- package/dist/shared/git-log.js +14 -0
- package/dist/shared/git-path.d.ts +3 -0
- package/dist/shared/git-path.d.ts.map +1 -0
- package/dist/shared/git-path.js +30 -0
- package/dist/shared/guards.d.ts +2 -0
- package/dist/shared/guards.d.ts.map +1 -0
- package/dist/shared/guards.js +3 -0
- package/dist/shared/protected-paths.d.ts +12 -0
- package/dist/shared/protected-paths.d.ts.map +1 -0
- package/dist/shared/protected-paths.js +51 -0
- package/dist/shared/strings.d.ts +2 -0
- package/dist/shared/strings.d.ts.map +1 -0
- package/dist/shared/strings.js +14 -0
- package/dist/shared/write-if-changed.d.ts +3 -0
- package/dist/shared/write-if-changed.d.ts.map +1 -0
- package/dist/shared/write-if-changed.js +25 -0
- package/package.json +2 -2
- package/dist/cli/help.d.ts +0 -2
- package/dist/cli/help.d.ts.map +0 -1
- package/dist/cli/help.js +0 -127
- package/dist/commands/task/verify.d.ts +0 -2
- package/dist/commands/task/verify.d.ts.map +0 -1
- package/dist/commands/task/verify.js +0 -1
|
@@ -1,1329 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import { loadDotEnv } from "../shared/env.js";
|
|
6
|
-
import { buildTaskIndexEntry, loadTaskIndex, resolveTaskIndexPath, saveTaskIndex, } from "./task-index.js";
|
|
7
|
-
const TASK_ID_RE = new RegExp(String.raw `^\d{12}-[${TASK_ID_ALPHABET}]{4,}$`);
|
|
8
|
-
const DEFAULT_DOC_UPDATED_BY = "agentplane";
|
|
9
|
-
const DOC_VERSION = 2;
|
|
10
|
-
const atomicWriteFile = atomicWriteFileCore;
|
|
11
|
-
const extractTaskDoc = extractTaskDocCore;
|
|
12
|
-
const mergeTaskDoc = mergeTaskDocCore;
|
|
13
|
-
const generateTaskId = generateTaskIdCore;
|
|
14
|
-
function nowIso() {
|
|
15
|
-
return new Date().toISOString();
|
|
16
|
-
}
|
|
17
|
-
function isRecord(value) {
|
|
18
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
19
|
-
}
|
|
20
|
-
function toStringSafe(value) {
|
|
21
|
-
if (typeof value === "string")
|
|
22
|
-
return value;
|
|
23
|
-
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
24
|
-
return String(value);
|
|
25
|
-
}
|
|
26
|
-
return "";
|
|
27
|
-
}
|
|
28
|
-
function firstNonEmptyString(...values) {
|
|
29
|
-
for (const value of values) {
|
|
30
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
31
|
-
return value.trim();
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return "";
|
|
35
|
-
}
|
|
36
|
-
function sleep(ms) {
|
|
37
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
38
|
-
}
|
|
39
|
-
function normalizeUpdatedBy(value) {
|
|
40
|
-
if (typeof value !== "string")
|
|
41
|
-
return "";
|
|
42
|
-
const trimmed = value.trim();
|
|
43
|
-
if (!trimmed)
|
|
44
|
-
return "";
|
|
45
|
-
if (trimmed.toLowerCase() === DEFAULT_DOC_UPDATED_BY.toLowerCase())
|
|
46
|
-
return "";
|
|
47
|
-
return trimmed;
|
|
48
|
-
}
|
|
49
|
-
function ensureDocMetadata(task, updatedBy) {
|
|
50
|
-
task.doc_version = DOC_VERSION;
|
|
51
|
-
task.doc_updated_at = nowIso();
|
|
52
|
-
const explicit = normalizeUpdatedBy(updatedBy);
|
|
53
|
-
if (updatedBy !== undefined) {
|
|
54
|
-
task.doc_updated_by =
|
|
55
|
-
explicit || resolveDocUpdatedByFromTask(task, DEFAULT_DOC_UPDATED_BY);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
task.doc_updated_by = resolveDocUpdatedByFromTask(task, DEFAULT_DOC_UPDATED_BY);
|
|
59
|
-
}
|
|
60
|
-
function lastCommentAuthor(comments) {
|
|
61
|
-
if (!Array.isArray(comments))
|
|
62
|
-
return null;
|
|
63
|
-
const entries = comments;
|
|
64
|
-
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
65
|
-
const entry = entries[i];
|
|
66
|
-
if (!isRecord(entry))
|
|
67
|
-
continue;
|
|
68
|
-
const author = entry.author;
|
|
69
|
-
if (typeof author === "string") {
|
|
70
|
-
const trimmed = author.trim();
|
|
71
|
-
if (trimmed)
|
|
72
|
-
return trimmed;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
function resolveDocUpdatedByFromFrontmatter(frontmatter, updatedBy, fallback) {
|
|
78
|
-
if (updatedBy !== undefined) {
|
|
79
|
-
const explicit = normalizeUpdatedBy(updatedBy);
|
|
80
|
-
if (explicit)
|
|
81
|
-
return explicit;
|
|
82
|
-
}
|
|
83
|
-
const author = lastCommentAuthor(frontmatter.comments);
|
|
84
|
-
if (author)
|
|
85
|
-
return author;
|
|
86
|
-
const existing = normalizeUpdatedBy(frontmatter.doc_updated_by);
|
|
87
|
-
if (existing)
|
|
88
|
-
return existing;
|
|
89
|
-
const owner = normalizeUpdatedBy(frontmatter.owner);
|
|
90
|
-
if (owner)
|
|
91
|
-
return owner;
|
|
92
|
-
const fallbackValue = normalizeUpdatedBy(fallback);
|
|
93
|
-
return fallbackValue || fallback;
|
|
94
|
-
}
|
|
95
|
-
function resolveDocUpdatedByFromTask(task, fallback) {
|
|
96
|
-
const author = lastCommentAuthor(task.comments);
|
|
97
|
-
if (author)
|
|
98
|
-
return author;
|
|
99
|
-
const existing = normalizeUpdatedBy(task.doc_updated_by);
|
|
100
|
-
if (existing)
|
|
101
|
-
return existing;
|
|
102
|
-
const owner = normalizeUpdatedBy(task.owner);
|
|
103
|
-
if (owner)
|
|
104
|
-
return owner;
|
|
105
|
-
const fallbackValue = normalizeUpdatedBy(fallback);
|
|
106
|
-
return fallbackValue || fallback;
|
|
107
|
-
}
|
|
108
|
-
export { extractTaskDoc, mergeTaskDoc };
|
|
109
|
-
function validateTaskId(taskId) {
|
|
110
|
-
if (TASK_ID_RE.test(taskId))
|
|
111
|
-
return;
|
|
112
|
-
throw new Error(`Invalid task id: ${taskId} (expected YYYYMMDDHHMM-XXXX)`);
|
|
113
|
-
}
|
|
114
|
-
function missingTaskIdMessage() {
|
|
115
|
-
return "Missing task id (expected non-empty value)";
|
|
116
|
-
}
|
|
117
|
-
function unknownTaskIdMessage(taskId) {
|
|
118
|
-
return `Unknown task id: ${taskId}`;
|
|
119
|
-
}
|
|
120
|
-
function invalidLengthMessage(value, min) {
|
|
121
|
-
return `Invalid length: ${value} (expected >= ${min})`;
|
|
122
|
-
}
|
|
123
|
-
function redmineConfigMissingMessage(detail) {
|
|
124
|
-
return `Missing required Redmine config: ${detail}`;
|
|
125
|
-
}
|
|
126
|
-
function redmineIssueIdMissingMessage() {
|
|
127
|
-
return "Missing Redmine issue id for task";
|
|
128
|
-
}
|
|
129
|
-
export class BackendError extends Error {
|
|
130
|
-
code;
|
|
131
|
-
constructor(message, code) {
|
|
132
|
-
super(message);
|
|
133
|
-
this.code = code;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
export class RedmineUnavailable extends BackendError {
|
|
137
|
-
constructor(message) {
|
|
138
|
-
super(message, "E_NETWORK");
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
function toStringArray(value) {
|
|
142
|
-
if (!Array.isArray(value))
|
|
143
|
-
return [];
|
|
144
|
-
return value.filter((v) => typeof v === "string");
|
|
145
|
-
}
|
|
146
|
-
function normalizeDependsOn(value) {
|
|
147
|
-
if (Array.isArray(value)) {
|
|
148
|
-
return value.filter((v) => typeof v === "string" && v.trim() !== "[]");
|
|
149
|
-
}
|
|
150
|
-
if (typeof value === "string" && value.trim() === "[]")
|
|
151
|
-
return [];
|
|
152
|
-
return [];
|
|
153
|
-
}
|
|
154
|
-
function normalizePriority(value) {
|
|
155
|
-
const raw = toStringSafe(value).trim().toLowerCase();
|
|
156
|
-
if (!raw)
|
|
157
|
-
return "med";
|
|
158
|
-
if (raw === "low")
|
|
159
|
-
return "low";
|
|
160
|
-
if (raw === "normal")
|
|
161
|
-
return "normal";
|
|
162
|
-
if (raw === "medium" || raw === "med")
|
|
163
|
-
return "med";
|
|
164
|
-
if (raw === "high")
|
|
165
|
-
return "high";
|
|
166
|
-
if (raw === "urgent" || raw === "immediate")
|
|
167
|
-
return "high";
|
|
168
|
-
return "med";
|
|
169
|
-
}
|
|
170
|
-
function defaultPlanApproval() {
|
|
171
|
-
return { state: "pending", updated_at: null, updated_by: null, note: null };
|
|
172
|
-
}
|
|
173
|
-
function normalizePlanApproval(value) {
|
|
174
|
-
if (!isRecord(value))
|
|
175
|
-
return null;
|
|
176
|
-
const state = typeof value.state === "string" ? value.state : "";
|
|
177
|
-
if (state !== "pending" && state !== "approved" && state !== "rejected")
|
|
178
|
-
return null;
|
|
179
|
-
const updatedAt = value.updated_at === null || typeof value.updated_at === "string" ? value.updated_at : null;
|
|
180
|
-
const updatedBy = value.updated_by === null || typeof value.updated_by === "string" ? value.updated_by : null;
|
|
181
|
-
const note = value.note === null || typeof value.note === "string" ? value.note : null;
|
|
182
|
-
return { state, updated_at: updatedAt, updated_by: updatedBy, note };
|
|
183
|
-
}
|
|
184
|
-
function defaultVerificationResult() {
|
|
185
|
-
return { state: "pending", updated_at: null, updated_by: null, note: null };
|
|
186
|
-
}
|
|
187
|
-
function normalizeVerificationResult(value) {
|
|
188
|
-
if (!isRecord(value))
|
|
189
|
-
return null;
|
|
190
|
-
const state = typeof value.state === "string" ? value.state : "";
|
|
191
|
-
if (state !== "pending" && state !== "ok" && state !== "needs_rework")
|
|
192
|
-
return null;
|
|
193
|
-
const updatedAt = value.updated_at === null || typeof value.updated_at === "string" ? value.updated_at : null;
|
|
194
|
-
const updatedBy = value.updated_by === null || typeof value.updated_by === "string" ? value.updated_by : null;
|
|
195
|
-
const note = value.note === null || typeof value.note === "string" ? value.note : null;
|
|
196
|
-
return { state, updated_at: updatedAt, updated_by: updatedBy, note };
|
|
197
|
-
}
|
|
198
|
-
export function taskRecordToData(record) {
|
|
199
|
-
const fm = record.frontmatter;
|
|
200
|
-
const comments = Array.isArray(fm.comments)
|
|
201
|
-
? fm.comments
|
|
202
|
-
.filter((item) => isRecord(item))
|
|
203
|
-
.filter((item) => typeof item.author === "string" && typeof item.body === "string")
|
|
204
|
-
.map((item) => ({ author: item.author, body: item.body }))
|
|
205
|
-
: [];
|
|
206
|
-
const commit = isRecord(fm.commit) &&
|
|
207
|
-
typeof fm.commit.hash === "string" &&
|
|
208
|
-
typeof fm.commit.message === "string"
|
|
209
|
-
? { hash: fm.commit.hash, message: fm.commit.message }
|
|
210
|
-
: null;
|
|
211
|
-
const planApproval = normalizePlanApproval(fm.plan_approval);
|
|
212
|
-
const verification = normalizeVerificationResult(fm.verification);
|
|
213
|
-
const baseId = typeof fm.id === "string" ? fm.id : typeof record.id === "string" ? record.id : "";
|
|
214
|
-
const task = {
|
|
215
|
-
id: baseId.trim(),
|
|
216
|
-
title: typeof fm.title === "string" ? fm.title : "",
|
|
217
|
-
description: typeof fm.description === "string" ? fm.description : "",
|
|
218
|
-
status: typeof fm.status === "string" ? fm.status : "TODO",
|
|
219
|
-
priority: typeof fm.priority === "string" || typeof fm.priority === "number" ? fm.priority : "",
|
|
220
|
-
owner: typeof fm.owner === "string" ? fm.owner : "",
|
|
221
|
-
depends_on: normalizeDependsOn(fm.depends_on),
|
|
222
|
-
tags: toStringArray(fm.tags),
|
|
223
|
-
verify: toStringArray(fm.verify),
|
|
224
|
-
plan_approval: planApproval ?? undefined,
|
|
225
|
-
verification: verification ?? undefined,
|
|
226
|
-
commit,
|
|
227
|
-
comments,
|
|
228
|
-
doc_version: typeof fm.doc_version === "number" ? fm.doc_version : undefined,
|
|
229
|
-
doc_updated_at: typeof fm.doc_updated_at === "string" ? fm.doc_updated_at : undefined,
|
|
230
|
-
doc_updated_by: typeof fm.doc_updated_by === "string" ? fm.doc_updated_by : undefined,
|
|
231
|
-
dirty: typeof fm.dirty === "boolean" ? fm.dirty : undefined,
|
|
232
|
-
id_source: typeof fm.id_source === "string" ? fm.id_source : undefined,
|
|
233
|
-
};
|
|
234
|
-
const doc = extractTaskDoc(record.body);
|
|
235
|
-
if (doc)
|
|
236
|
-
task.doc = doc;
|
|
237
|
-
return task;
|
|
238
|
-
}
|
|
239
|
-
function taskDataToExport(task) {
|
|
240
|
-
return {
|
|
241
|
-
...task,
|
|
242
|
-
id: task.id,
|
|
243
|
-
title: task.title ?? "",
|
|
244
|
-
description: task.description ?? "",
|
|
245
|
-
status: task.status ?? "",
|
|
246
|
-
priority: typeof task.priority === "number" ? String(task.priority) : (task.priority ?? ""),
|
|
247
|
-
owner: task.owner ?? "",
|
|
248
|
-
depends_on: toStringArray(task.depends_on),
|
|
249
|
-
tags: toStringArray(task.tags),
|
|
250
|
-
verify: toStringArray(task.verify),
|
|
251
|
-
commit: task.commit ?? null,
|
|
252
|
-
comments: Array.isArray(task.comments)
|
|
253
|
-
? task.comments.filter((item) => !!item && typeof item.author === "string" && typeof item.body === "string")
|
|
254
|
-
: [],
|
|
255
|
-
doc_version: task.doc_version ?? DOC_VERSION,
|
|
256
|
-
doc_updated_at: task.doc_updated_at ?? "",
|
|
257
|
-
doc_updated_by: resolveDocUpdatedByFromTask(task, DEFAULT_DOC_UPDATED_BY),
|
|
258
|
-
dirty: Boolean(task.dirty),
|
|
259
|
-
id_source: task.id_source ?? "generated",
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
export function buildTasksExportSnapshotFromTasks(tasks) {
|
|
263
|
-
const exportTasks = tasks.map((task) => taskDataToExport(task));
|
|
264
|
-
const sorted = exportTasks.toSorted((a, b) => a.id.localeCompare(b.id));
|
|
265
|
-
const canonical = JSON.stringify(canonicalizeJson({ tasks: sorted }));
|
|
266
|
-
const checksum = createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
267
|
-
return {
|
|
268
|
-
tasks: sorted,
|
|
269
|
-
meta: {
|
|
270
|
-
schema_version: 1,
|
|
271
|
-
managed_by: "agentplane",
|
|
272
|
-
checksum_algo: "sha256",
|
|
273
|
-
checksum,
|
|
274
|
-
},
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
export async function writeTasksExportFromTasks(opts) {
|
|
278
|
-
const snapshot = buildTasksExportSnapshotFromTasks(opts.tasks);
|
|
279
|
-
await mkdir(path.dirname(opts.outputPath), { recursive: true });
|
|
280
|
-
await atomicWriteFile(opts.outputPath, `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
281
|
-
}
|
|
282
|
-
export class LocalBackend {
|
|
283
|
-
id = "local";
|
|
284
|
-
root;
|
|
285
|
-
updatedBy;
|
|
286
|
-
constructor(settings) {
|
|
287
|
-
this.root = path.resolve(settings?.dir ?? ".agentplane/tasks");
|
|
288
|
-
this.updatedBy = settings?.updatedBy ?? DEFAULT_DOC_UPDATED_BY;
|
|
289
|
-
}
|
|
290
|
-
async generateTaskId(opts) {
|
|
291
|
-
const length = opts.length;
|
|
292
|
-
if (length < 4)
|
|
293
|
-
throw new Error(invalidLengthMessage(length, 4));
|
|
294
|
-
const attempts = Math.max(1, opts.attempts);
|
|
295
|
-
return await generateTaskId({
|
|
296
|
-
length,
|
|
297
|
-
attempts,
|
|
298
|
-
isAvailable: async (taskId) => {
|
|
299
|
-
const readmePath = taskReadmePath(this.root, taskId);
|
|
300
|
-
try {
|
|
301
|
-
await readFile(readmePath, "utf8");
|
|
302
|
-
return false;
|
|
303
|
-
}
|
|
304
|
-
catch (err) {
|
|
305
|
-
const code = err?.code;
|
|
306
|
-
if (code === "ENOENT")
|
|
307
|
-
return true;
|
|
308
|
-
throw err;
|
|
309
|
-
}
|
|
310
|
-
},
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
async listTasks() {
|
|
314
|
-
const tasks = [];
|
|
315
|
-
const entries = await readdir(this.root, { withFileTypes: true }).catch(() => []);
|
|
316
|
-
const indexPath = resolveTaskIndexPath(this.root);
|
|
317
|
-
const cachedIndex = await loadTaskIndex(indexPath);
|
|
318
|
-
const cachedByPath = new Map();
|
|
319
|
-
if (cachedIndex) {
|
|
320
|
-
for (const entry of cachedIndex.tasks) {
|
|
321
|
-
cachedByPath.set(entry.readmePath, entry);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
const nextIndex = [];
|
|
325
|
-
const seen = new Set();
|
|
326
|
-
for (const entry of entries) {
|
|
327
|
-
if (!entry.isDirectory())
|
|
328
|
-
continue;
|
|
329
|
-
const readme = path.join(this.root, entry.name, "README.md");
|
|
330
|
-
let stats;
|
|
331
|
-
try {
|
|
332
|
-
stats = await stat(readme);
|
|
333
|
-
}
|
|
334
|
-
catch {
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
if (!stats.isFile())
|
|
338
|
-
continue;
|
|
339
|
-
const cached = cachedByPath.get(readme);
|
|
340
|
-
if (cached?.mtimeMs === stats.mtimeMs) {
|
|
341
|
-
const taskId = cached.task.id.trim();
|
|
342
|
-
if (taskId) {
|
|
343
|
-
validateTaskId(taskId);
|
|
344
|
-
if (seen.has(taskId)) {
|
|
345
|
-
throw new Error(`Duplicate task id in local backend: ${taskId}`);
|
|
346
|
-
}
|
|
347
|
-
seen.add(taskId);
|
|
348
|
-
}
|
|
349
|
-
tasks.push(cached.task);
|
|
350
|
-
nextIndex.push(cached);
|
|
351
|
-
continue;
|
|
352
|
-
}
|
|
353
|
-
let text = "";
|
|
354
|
-
try {
|
|
355
|
-
text = await readFile(readme, "utf8");
|
|
356
|
-
}
|
|
357
|
-
catch {
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
let parsed;
|
|
361
|
-
try {
|
|
362
|
-
parsed = parseTaskReadme(text);
|
|
363
|
-
}
|
|
364
|
-
catch {
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
const fm = parsed.frontmatter;
|
|
368
|
-
if (!isRecord(fm) || Object.keys(fm).length === 0)
|
|
369
|
-
continue;
|
|
370
|
-
const taskId = (typeof fm.id === "string" ? fm.id : entry.name).trim();
|
|
371
|
-
if (taskId) {
|
|
372
|
-
validateTaskId(taskId);
|
|
373
|
-
if (seen.has(taskId)) {
|
|
374
|
-
throw new Error(`Duplicate task id in local backend: ${taskId}`);
|
|
375
|
-
}
|
|
376
|
-
seen.add(taskId);
|
|
377
|
-
}
|
|
378
|
-
const task = taskRecordToData({
|
|
379
|
-
id: taskId,
|
|
380
|
-
frontmatter: fm,
|
|
381
|
-
body: parsed.body,
|
|
382
|
-
readmePath: readme,
|
|
383
|
-
});
|
|
384
|
-
tasks.push(task);
|
|
385
|
-
nextIndex.push(buildTaskIndexEntry(task, readme, stats.mtimeMs));
|
|
386
|
-
}
|
|
387
|
-
try {
|
|
388
|
-
await saveTaskIndex(indexPath, { schema_version: 1, tasks: nextIndex });
|
|
389
|
-
}
|
|
390
|
-
catch {
|
|
391
|
-
// Best-effort cache; ignore failures.
|
|
392
|
-
}
|
|
393
|
-
return tasks;
|
|
394
|
-
}
|
|
395
|
-
async getTask(taskId) {
|
|
396
|
-
const readme = taskReadmePath(this.root, taskId);
|
|
397
|
-
let text = "";
|
|
398
|
-
try {
|
|
399
|
-
text = await readFile(readme, "utf8");
|
|
400
|
-
}
|
|
401
|
-
catch (err) {
|
|
402
|
-
const code = err?.code;
|
|
403
|
-
if (code === "ENOENT")
|
|
404
|
-
return null;
|
|
405
|
-
throw err;
|
|
406
|
-
}
|
|
407
|
-
const parsed = parseTaskReadme(text);
|
|
408
|
-
const task = taskRecordToData({
|
|
409
|
-
id: taskId,
|
|
410
|
-
frontmatter: parsed.frontmatter,
|
|
411
|
-
body: parsed.body,
|
|
412
|
-
readmePath: readme,
|
|
413
|
-
});
|
|
414
|
-
return task;
|
|
415
|
-
}
|
|
416
|
-
async getTaskDoc(taskId) {
|
|
417
|
-
const readme = taskReadmePath(this.root, taskId);
|
|
418
|
-
const text = await readFile(readme, "utf8");
|
|
419
|
-
const parsed = parseTaskReadme(text);
|
|
420
|
-
return extractTaskDoc(parsed.body);
|
|
421
|
-
}
|
|
422
|
-
async writeTask(task) {
|
|
423
|
-
const taskId = task.id.trim();
|
|
424
|
-
if (!taskId)
|
|
425
|
-
throw new Error(missingTaskIdMessage());
|
|
426
|
-
validateTaskId(taskId);
|
|
427
|
-
const readme = taskReadmePath(this.root, taskId);
|
|
428
|
-
let body = "";
|
|
429
|
-
let existingDoc = "";
|
|
430
|
-
let existingFrontmatter = {};
|
|
431
|
-
try {
|
|
432
|
-
const text = await readFile(readme, "utf8");
|
|
433
|
-
const parsed = parseTaskReadme(text);
|
|
434
|
-
body = parsed.body;
|
|
435
|
-
existingDoc = extractTaskDoc(parsed.body);
|
|
436
|
-
existingFrontmatter = parsed.frontmatter;
|
|
437
|
-
}
|
|
438
|
-
catch (err) {
|
|
439
|
-
const code = err?.code;
|
|
440
|
-
if (code !== "ENOENT")
|
|
441
|
-
throw err;
|
|
442
|
-
}
|
|
443
|
-
const payload = { ...task };
|
|
444
|
-
delete payload.doc;
|
|
445
|
-
for (const [key, value] of Object.entries(payload)) {
|
|
446
|
-
if (value === undefined)
|
|
447
|
-
delete payload[key];
|
|
448
|
-
}
|
|
449
|
-
for (const key of ["doc_version", "doc_updated_at", "doc_updated_by"]) {
|
|
450
|
-
if (payload[key] === undefined && existingFrontmatter[key] !== undefined) {
|
|
451
|
-
payload[key] = existingFrontmatter[key];
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
if (payload.plan_approval === undefined && existingFrontmatter.plan_approval !== undefined) {
|
|
455
|
-
payload.plan_approval = existingFrontmatter.plan_approval;
|
|
456
|
-
}
|
|
457
|
-
if (payload.plan_approval === undefined) {
|
|
458
|
-
payload.plan_approval = defaultPlanApproval();
|
|
459
|
-
}
|
|
460
|
-
if (payload.verification === undefined && existingFrontmatter.verification !== undefined) {
|
|
461
|
-
payload.verification = existingFrontmatter.verification;
|
|
462
|
-
}
|
|
463
|
-
if (payload.verification === undefined) {
|
|
464
|
-
payload.verification = defaultVerificationResult();
|
|
465
|
-
}
|
|
466
|
-
if (task.doc !== undefined) {
|
|
467
|
-
const docText = String(task.doc ?? "");
|
|
468
|
-
body = mergeTaskDoc(body, docText);
|
|
469
|
-
if (docChanged(existingDoc, docText)) {
|
|
470
|
-
payload.doc_version = DOC_VERSION;
|
|
471
|
-
payload.doc_updated_at = nowIso();
|
|
472
|
-
payload.doc_updated_by = resolveDocUpdatedByFromTask(task, this.updatedBy);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
if (payload.doc_version !== DOC_VERSION) {
|
|
476
|
-
payload.doc_version = DOC_VERSION;
|
|
477
|
-
}
|
|
478
|
-
if (payload.doc_updated_at === undefined || payload.doc_updated_at === "") {
|
|
479
|
-
payload.doc_updated_at = nowIso();
|
|
480
|
-
}
|
|
481
|
-
if (payload.doc_updated_by === undefined || payload.doc_updated_by === "") {
|
|
482
|
-
payload.doc_updated_by = resolveDocUpdatedByFromTask(task, this.updatedBy);
|
|
483
|
-
}
|
|
484
|
-
await mkdir(path.dirname(readme), { recursive: true });
|
|
485
|
-
const text = renderTaskReadme(payload, body || "");
|
|
486
|
-
await atomicWriteFile(readme, text.endsWith("\n") ? text : `${text}\n`);
|
|
487
|
-
}
|
|
488
|
-
async setTaskDoc(taskId, doc, updatedBy) {
|
|
489
|
-
const readme = taskReadmePath(this.root, taskId);
|
|
490
|
-
const text = await readFile(readme, "utf8");
|
|
491
|
-
const parsed = parseTaskReadme(text);
|
|
492
|
-
const docText = String(doc ?? "");
|
|
493
|
-
const body = mergeTaskDoc(parsed.body, docText);
|
|
494
|
-
const frontmatter = { ...parsed.frontmatter };
|
|
495
|
-
if (docChanged(extractTaskDoc(parsed.body), docText) || !frontmatter.doc_updated_at) {
|
|
496
|
-
frontmatter.doc_version = DOC_VERSION;
|
|
497
|
-
frontmatter.doc_updated_at = nowIso();
|
|
498
|
-
frontmatter.doc_updated_by = resolveDocUpdatedByFromFrontmatter(frontmatter, updatedBy, this.updatedBy);
|
|
499
|
-
}
|
|
500
|
-
if (frontmatter.doc_version !== DOC_VERSION) {
|
|
501
|
-
frontmatter.doc_version = DOC_VERSION;
|
|
502
|
-
}
|
|
503
|
-
const next = renderTaskReadme(frontmatter, body);
|
|
504
|
-
await atomicWriteFile(readme, next.endsWith("\n") ? next : `${next}\n`);
|
|
505
|
-
}
|
|
506
|
-
async touchTaskDocMetadata(taskId, updatedBy) {
|
|
507
|
-
const readme = taskReadmePath(this.root, taskId);
|
|
508
|
-
const text = await readFile(readme, "utf8");
|
|
509
|
-
const parsed = parseTaskReadme(text);
|
|
510
|
-
const frontmatter = { ...parsed.frontmatter };
|
|
511
|
-
frontmatter.doc_version = DOC_VERSION;
|
|
512
|
-
frontmatter.doc_updated_at = nowIso();
|
|
513
|
-
frontmatter.doc_updated_by = resolveDocUpdatedByFromFrontmatter(frontmatter, updatedBy, this.updatedBy);
|
|
514
|
-
const next = renderTaskReadme(frontmatter, parsed.body || "");
|
|
515
|
-
await atomicWriteFile(readme, next.endsWith("\n") ? next : `${next}\n`);
|
|
516
|
-
}
|
|
517
|
-
async writeTasks(tasks) {
|
|
518
|
-
for (const task of tasks) {
|
|
519
|
-
await this.writeTask(task);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
async exportTasksJson(outputPath) {
|
|
523
|
-
const tasks = await this.listTasks();
|
|
524
|
-
await writeTasksExportFromTasks({ outputPath, tasks });
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
export class RedmineBackend {
|
|
528
|
-
id = "redmine";
|
|
529
|
-
baseUrl;
|
|
530
|
-
apiKey;
|
|
531
|
-
projectId;
|
|
532
|
-
assigneeId;
|
|
533
|
-
ownerAgent;
|
|
534
|
-
statusMap;
|
|
535
|
-
customFields;
|
|
536
|
-
batchSize;
|
|
537
|
-
batchPause;
|
|
538
|
-
cache;
|
|
539
|
-
issueCache = new Map();
|
|
540
|
-
reverseStatus = new Map();
|
|
541
|
-
constructor(settings, opts) {
|
|
542
|
-
const envUrl = firstNonEmptyString(process.env.AGENTPLANE_REDMINE_URL);
|
|
543
|
-
const envApiKey = firstNonEmptyString(process.env.AGENTPLANE_REDMINE_API_KEY);
|
|
544
|
-
const envProjectId = firstNonEmptyString(process.env.AGENTPLANE_REDMINE_PROJECT_ID);
|
|
545
|
-
const envAssignee = (process.env.AGENTPLANE_REDMINE_ASSIGNEE_ID ?? "").trim();
|
|
546
|
-
const envOwner = firstNonEmptyString(process.env.AGENTPLANE_REDMINE_OWNER, process.env.AGENTPLANE_REDMINE_OWNER_AGENT);
|
|
547
|
-
this.baseUrl = firstNonEmptyString(envUrl, settings.url).replaceAll(/\/+$/gu, "");
|
|
548
|
-
this.apiKey = firstNonEmptyString(envApiKey, settings.api_key);
|
|
549
|
-
this.projectId = firstNonEmptyString(envProjectId, settings.project_id);
|
|
550
|
-
this.assigneeId = envAssignee && /^\d+$/u.test(envAssignee) ? Number(envAssignee) : null;
|
|
551
|
-
this.statusMap = isRecord(settings.status_map) ? settings.status_map : {};
|
|
552
|
-
this.customFields = isRecord(settings.custom_fields) ? settings.custom_fields : {};
|
|
553
|
-
this.batchSize = typeof settings.batch_size === "number" ? settings.batch_size : 20;
|
|
554
|
-
this.batchPause = typeof settings.batch_pause === "number" ? settings.batch_pause : 0.5;
|
|
555
|
-
this.ownerAgent = firstNonEmptyString(envOwner, settings.owner_agent, "REDMINE");
|
|
556
|
-
this.cache = opts.cache ?? null;
|
|
557
|
-
if (!this.baseUrl || !this.apiKey || !this.projectId) {
|
|
558
|
-
throw new BackendError(redmineConfigMissingMessage("url, api_key, project_id"), "E_BACKEND");
|
|
559
|
-
}
|
|
560
|
-
if (!this.customFields?.task_id) {
|
|
561
|
-
throw new BackendError(redmineConfigMissingMessage("custom_fields.task_id"), "E_BACKEND");
|
|
562
|
-
}
|
|
563
|
-
for (const [key, value] of Object.entries(this.statusMap)) {
|
|
564
|
-
if (typeof value === "number")
|
|
565
|
-
this.reverseStatus.set(value, key);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
async generateTaskId(opts) {
|
|
569
|
-
const length = opts.length;
|
|
570
|
-
const attempts = opts.attempts;
|
|
571
|
-
let existingIds = new Set();
|
|
572
|
-
try {
|
|
573
|
-
const tasks = await this.listTasksRemote();
|
|
574
|
-
existingIds = new Set(tasks.map((task) => toStringSafe(task.id)).filter(Boolean));
|
|
575
|
-
}
|
|
576
|
-
catch (err) {
|
|
577
|
-
if (!(err instanceof RedmineUnavailable))
|
|
578
|
-
throw err;
|
|
579
|
-
if (!this.cache)
|
|
580
|
-
throw err;
|
|
581
|
-
const cached = await this.cache.listTasks();
|
|
582
|
-
existingIds = new Set(cached.map((task) => toStringSafe(task.id)).filter(Boolean));
|
|
583
|
-
}
|
|
584
|
-
return await generateTaskId({
|
|
585
|
-
length,
|
|
586
|
-
attempts,
|
|
587
|
-
isAvailable: (taskId) => !existingIds.has(taskId),
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
async listTasks() {
|
|
591
|
-
try {
|
|
592
|
-
const tasks = await this.listTasksRemote();
|
|
593
|
-
for (const task of tasks) {
|
|
594
|
-
await this.cacheTask(task, false);
|
|
595
|
-
}
|
|
596
|
-
return tasks;
|
|
597
|
-
}
|
|
598
|
-
catch (err) {
|
|
599
|
-
if (err instanceof RedmineUnavailable) {
|
|
600
|
-
if (!this.cache)
|
|
601
|
-
throw err;
|
|
602
|
-
return await this.cache.listTasks();
|
|
603
|
-
}
|
|
604
|
-
throw err;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
async exportTasksJson(outputPath) {
|
|
608
|
-
const tasks = await this.listTasks();
|
|
609
|
-
await writeTasksExportFromTasks({ outputPath, tasks });
|
|
610
|
-
}
|
|
611
|
-
async getTask(taskId) {
|
|
612
|
-
try {
|
|
613
|
-
const issue = await this.findIssueByTaskId(taskId);
|
|
614
|
-
if (!issue)
|
|
615
|
-
return null;
|
|
616
|
-
const task = this.issueToTask(issue, taskId);
|
|
617
|
-
if (task)
|
|
618
|
-
await this.cacheTask(task, false);
|
|
619
|
-
return task;
|
|
620
|
-
}
|
|
621
|
-
catch (err) {
|
|
622
|
-
if (err instanceof RedmineUnavailable) {
|
|
623
|
-
if (!this.cache)
|
|
624
|
-
throw err;
|
|
625
|
-
const cached = await this.cache.getTask(taskId);
|
|
626
|
-
return cached ?? null;
|
|
627
|
-
}
|
|
628
|
-
throw err;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
async getTaskDoc(taskId) {
|
|
632
|
-
const task = await this.getTask(taskId);
|
|
633
|
-
if (!task)
|
|
634
|
-
throw new Error(unknownTaskIdMessage(taskId));
|
|
635
|
-
return toStringSafe(task.doc);
|
|
636
|
-
}
|
|
637
|
-
async setTaskDoc(taskId, doc, updatedBy) {
|
|
638
|
-
if (!this.customFields.doc) {
|
|
639
|
-
throw new BackendError(redmineConfigMissingMessage("custom_fields.doc"), "E_BACKEND");
|
|
640
|
-
}
|
|
641
|
-
try {
|
|
642
|
-
const issue = await this.findIssueByTaskId(taskId);
|
|
643
|
-
if (!issue)
|
|
644
|
-
throw new Error(unknownTaskIdMessage(taskId));
|
|
645
|
-
const issueIdText = toStringSafe(issue.id);
|
|
646
|
-
if (!issueIdText)
|
|
647
|
-
throw new Error(redmineIssueIdMissingMessage());
|
|
648
|
-
const taskDoc = { doc: String(doc ?? "") };
|
|
649
|
-
ensureDocMetadata(taskDoc, updatedBy);
|
|
650
|
-
const customFields = [];
|
|
651
|
-
this.appendCustomField(customFields, "doc", taskDoc.doc);
|
|
652
|
-
this.appendCustomField(customFields, "doc_version", taskDoc.doc_version);
|
|
653
|
-
this.appendCustomField(customFields, "doc_updated_at", taskDoc.doc_updated_at);
|
|
654
|
-
this.appendCustomField(customFields, "doc_updated_by", taskDoc.doc_updated_by);
|
|
655
|
-
await this.requestJson("PUT", `issues/${issueIdText}.json`, {
|
|
656
|
-
issue: { custom_fields: customFields },
|
|
657
|
-
});
|
|
658
|
-
const task = this.issueToTask(issue, taskId);
|
|
659
|
-
if (task) {
|
|
660
|
-
task.doc = taskDoc.doc;
|
|
661
|
-
task.doc_version = taskDoc.doc_version;
|
|
662
|
-
task.doc_updated_at = taskDoc.doc_updated_at;
|
|
663
|
-
task.doc_updated_by = taskDoc.doc_updated_by;
|
|
664
|
-
await this.cacheTask(task, false);
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
catch (err) {
|
|
668
|
-
if (err instanceof RedmineUnavailable) {
|
|
669
|
-
if (!this.cache)
|
|
670
|
-
throw err;
|
|
671
|
-
const cached = await this.cache.getTask(taskId);
|
|
672
|
-
if (!cached)
|
|
673
|
-
throw new Error(unknownTaskIdMessage(taskId));
|
|
674
|
-
cached.doc = String(doc ?? "");
|
|
675
|
-
ensureDocMetadata(cached, updatedBy);
|
|
676
|
-
cached.dirty = true;
|
|
677
|
-
await this.cache.writeTask(cached);
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
throw err;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
async touchTaskDocMetadata(taskId, updatedBy) {
|
|
684
|
-
try {
|
|
685
|
-
const issue = await this.findIssueByTaskId(taskId);
|
|
686
|
-
if (!issue)
|
|
687
|
-
throw new Error(unknownTaskIdMessage(taskId));
|
|
688
|
-
const issueIdText = toStringSafe(issue.id);
|
|
689
|
-
if (!issueIdText)
|
|
690
|
-
throw new Error(redmineIssueIdMissingMessage());
|
|
691
|
-
const docValue = this.customFieldValue(issue, this.customFields.doc);
|
|
692
|
-
const taskDoc = { doc: docValue ?? "" };
|
|
693
|
-
ensureDocMetadata(taskDoc, updatedBy);
|
|
694
|
-
const customFields = [];
|
|
695
|
-
this.appendCustomField(customFields, "doc_version", taskDoc.doc_version);
|
|
696
|
-
this.appendCustomField(customFields, "doc_updated_at", taskDoc.doc_updated_at);
|
|
697
|
-
this.appendCustomField(customFields, "doc_updated_by", taskDoc.doc_updated_by);
|
|
698
|
-
if (customFields.length > 0) {
|
|
699
|
-
await this.requestJson("PUT", `issues/${issueIdText}.json`, {
|
|
700
|
-
issue: { custom_fields: customFields },
|
|
701
|
-
});
|
|
702
|
-
const task = this.issueToTask(issue, taskId);
|
|
703
|
-
if (task) {
|
|
704
|
-
task.doc_version = taskDoc.doc_version;
|
|
705
|
-
task.doc_updated_at = taskDoc.doc_updated_at;
|
|
706
|
-
task.doc_updated_by = taskDoc.doc_updated_by;
|
|
707
|
-
await this.cacheTask(task, false);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
catch (err) {
|
|
712
|
-
if (err instanceof RedmineUnavailable) {
|
|
713
|
-
if (!this.cache)
|
|
714
|
-
throw err;
|
|
715
|
-
const cached = await this.cache.getTask(taskId);
|
|
716
|
-
if (!cached)
|
|
717
|
-
throw new Error(unknownTaskIdMessage(taskId));
|
|
718
|
-
ensureDocMetadata(cached, updatedBy);
|
|
719
|
-
cached.dirty = true;
|
|
720
|
-
await this.cache.writeTask(cached);
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
throw err;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
async writeTask(task) {
|
|
727
|
-
const taskId = toStringSafe(task.id).trim();
|
|
728
|
-
if (!taskId)
|
|
729
|
-
throw new Error(missingTaskIdMessage());
|
|
730
|
-
validateTaskId(taskId);
|
|
731
|
-
try {
|
|
732
|
-
this.ensureDocMetadata(task);
|
|
733
|
-
let issue = await this.findIssueByTaskId(taskId);
|
|
734
|
-
let issueId = issue?.id;
|
|
735
|
-
let issueIdText = issueId ? toStringSafe(issueId) : "";
|
|
736
|
-
let existingIssue = issue ?? null;
|
|
737
|
-
if (issueIdText && !existingIssue) {
|
|
738
|
-
const payload = await this.requestJson("GET", `issues/${issueIdText}.json`);
|
|
739
|
-
existingIssue = this.issueFromPayload(payload);
|
|
740
|
-
}
|
|
741
|
-
const payload = this.taskToIssuePayload(task, existingIssue ?? undefined);
|
|
742
|
-
if (issueIdText) {
|
|
743
|
-
await this.requestJson("PUT", `issues/${issueIdText}.json`, { issue: payload });
|
|
744
|
-
}
|
|
745
|
-
else {
|
|
746
|
-
const createPayload = { ...payload, project_id: this.projectId };
|
|
747
|
-
const created = await this.requestJson("POST", "issues.json", { issue: createPayload });
|
|
748
|
-
const createdIssue = this.issueFromPayload(created);
|
|
749
|
-
issueId = createdIssue?.id;
|
|
750
|
-
issueIdText = issueId ? toStringSafe(issueId) : "";
|
|
751
|
-
if (issueIdText) {
|
|
752
|
-
const updatePayload = { ...payload };
|
|
753
|
-
delete updatePayload.project_id;
|
|
754
|
-
await this.requestJson("PUT", `issues/${issueIdText}.json`, { issue: updatePayload });
|
|
755
|
-
const refreshed = await this.requestJson("GET", `issues/${issueIdText}.json`);
|
|
756
|
-
existingIssue = this.issueFromPayload(refreshed);
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
if (issueIdText) {
|
|
760
|
-
const existingComments = existingIssue && this.customFields.comments
|
|
761
|
-
? this.normalizeComments(this.maybeParseJson(this.customFieldValue(existingIssue, this.customFields.comments)))
|
|
762
|
-
: [];
|
|
763
|
-
const desiredComments = this.normalizeComments(task.comments);
|
|
764
|
-
await this.appendCommentNotes(issueIdText, existingComments, desiredComments);
|
|
765
|
-
}
|
|
766
|
-
task.dirty = false;
|
|
767
|
-
await this.cacheTask(task, false);
|
|
768
|
-
this.issueCache.clear();
|
|
769
|
-
}
|
|
770
|
-
catch (err) {
|
|
771
|
-
if (err instanceof RedmineUnavailable) {
|
|
772
|
-
if (!this.cache)
|
|
773
|
-
throw err;
|
|
774
|
-
task.dirty = true;
|
|
775
|
-
await this.cacheTask(task, true);
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
throw err;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
async writeTasks(tasks) {
|
|
782
|
-
for (const [index, task] of tasks.entries()) {
|
|
783
|
-
await this.writeTask(task);
|
|
784
|
-
if (this.batchPause && this.batchSize > 0 && (index + 1) % this.batchSize === 0) {
|
|
785
|
-
await sleep(this.batchPause * 1000);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
async sync(opts) {
|
|
790
|
-
if (opts.direction === "push") {
|
|
791
|
-
await this.syncPush(opts.quiet, opts.confirm);
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
if (opts.direction === "pull") {
|
|
795
|
-
await this.syncPull(opts.conflict, opts.quiet);
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
throw new BackendError("Invalid sync direction (expected push|pull)", "E_BACKEND");
|
|
799
|
-
}
|
|
800
|
-
ensureDocMetadata(task) {
|
|
801
|
-
if (task.doc === undefined)
|
|
802
|
-
return;
|
|
803
|
-
if (task.doc_version !== DOC_VERSION)
|
|
804
|
-
task.doc_version = DOC_VERSION;
|
|
805
|
-
task.doc_updated_at ??= nowIso();
|
|
806
|
-
task.doc_updated_by ??= DEFAULT_DOC_UPDATED_BY;
|
|
807
|
-
}
|
|
808
|
-
async syncPush(quiet, confirm) {
|
|
809
|
-
if (!this.cache) {
|
|
810
|
-
throw new BackendError("Redmine cache is disabled; sync push is unavailable", "E_BACKEND");
|
|
811
|
-
}
|
|
812
|
-
const tasks = await this.cache.listTasks();
|
|
813
|
-
const dirty = tasks.filter((task) => task.dirty);
|
|
814
|
-
if (dirty.length === 0) {
|
|
815
|
-
if (!quiet)
|
|
816
|
-
process.stdout.write("ℹ️ no local task changes to push\n");
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
if (!confirm) {
|
|
820
|
-
for (const task of dirty) {
|
|
821
|
-
process.stdout.write(`- pending push: ${task.id}\n`);
|
|
822
|
-
}
|
|
823
|
-
throw new BackendError("Refusing to push without --yes (preview above)", "E_BACKEND");
|
|
824
|
-
}
|
|
825
|
-
await this.writeTasks(dirty);
|
|
826
|
-
if (!quiet)
|
|
827
|
-
process.stdout.write(`✅ pushed ${dirty.length} task(s) (dirty)\n`);
|
|
828
|
-
}
|
|
829
|
-
async syncPull(conflict, quiet) {
|
|
830
|
-
if (!this.cache) {
|
|
831
|
-
throw new BackendError("Redmine cache is disabled; sync pull is unavailable", "E_BACKEND");
|
|
832
|
-
}
|
|
833
|
-
const remoteTasks = await this.listTasksRemote();
|
|
834
|
-
const remoteById = new Map();
|
|
835
|
-
for (const task of remoteTasks) {
|
|
836
|
-
const taskId = toStringSafe(task.id);
|
|
837
|
-
if (taskId)
|
|
838
|
-
remoteById.set(taskId, task);
|
|
839
|
-
}
|
|
840
|
-
const localTasks = await this.cache.listTasks();
|
|
841
|
-
const localById = new Map();
|
|
842
|
-
for (const task of localTasks) {
|
|
843
|
-
const taskId = toStringSafe(task.id);
|
|
844
|
-
if (taskId)
|
|
845
|
-
localById.set(taskId, task);
|
|
846
|
-
}
|
|
847
|
-
for (const [taskId, remoteTask] of remoteById.entries()) {
|
|
848
|
-
const localTask = localById.get(taskId);
|
|
849
|
-
if (localTask?.dirty) {
|
|
850
|
-
if (this.tasksDiffer(localTask, remoteTask)) {
|
|
851
|
-
await this.handleConflict(taskId, localTask, remoteTask, conflict);
|
|
852
|
-
continue;
|
|
853
|
-
}
|
|
854
|
-
localTask.dirty = false;
|
|
855
|
-
await this.cacheTask(localTask, false);
|
|
856
|
-
continue;
|
|
857
|
-
}
|
|
858
|
-
await this.cacheTask(remoteTask, false);
|
|
859
|
-
}
|
|
860
|
-
if (!quiet)
|
|
861
|
-
process.stdout.write(`✅ pulled ${remoteById.size} task(s) (remote)\n`);
|
|
862
|
-
}
|
|
863
|
-
async handleConflict(taskId, localTask, remoteTask, conflict) {
|
|
864
|
-
if (conflict === "prefer-local") {
|
|
865
|
-
await this.writeTask(localTask);
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
if (conflict === "prefer-remote") {
|
|
869
|
-
await this.cacheTask(remoteTask, false);
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
if (conflict === "diff") {
|
|
873
|
-
const diff = this.diffTasks(localTask, remoteTask);
|
|
874
|
-
process.stdout.write(`${diff}\n`);
|
|
875
|
-
throw new BackendError(`Conflict detected for ${taskId}`, "E_BACKEND");
|
|
876
|
-
}
|
|
877
|
-
throw new BackendError(`Conflict detected for ${taskId}`, "E_BACKEND");
|
|
878
|
-
}
|
|
879
|
-
diffTasks(localTask, remoteTask) {
|
|
880
|
-
const localText = JSON.stringify(canonicalizeJson(localTask), null, 2).split("\n");
|
|
881
|
-
const remoteText = JSON.stringify(canonicalizeJson(remoteTask), null, 2).split("\n");
|
|
882
|
-
const diff = ["--- remote", "+++ local"];
|
|
883
|
-
const max = Math.max(localText.length, remoteText.length);
|
|
884
|
-
for (let i = 0; i < max; i++) {
|
|
885
|
-
const l = localText[i];
|
|
886
|
-
const r = remoteText[i];
|
|
887
|
-
if (l === r)
|
|
888
|
-
continue;
|
|
889
|
-
if (r !== undefined)
|
|
890
|
-
diff.push(`- ${r}`);
|
|
891
|
-
if (l !== undefined)
|
|
892
|
-
diff.push(`+ ${l}`);
|
|
893
|
-
}
|
|
894
|
-
return diff.join("\n");
|
|
895
|
-
}
|
|
896
|
-
tasksDiffer(localTask, remoteTask) {
|
|
897
|
-
const localText = JSON.stringify(canonicalizeJson(localTask));
|
|
898
|
-
const remoteText = JSON.stringify(canonicalizeJson(remoteTask));
|
|
899
|
-
return localText !== remoteText;
|
|
900
|
-
}
|
|
901
|
-
async cacheTask(task, dirty) {
|
|
902
|
-
if (!this.cache)
|
|
903
|
-
return;
|
|
904
|
-
const next = { ...task, dirty };
|
|
905
|
-
await this.cache.writeTask(next);
|
|
906
|
-
}
|
|
907
|
-
taskIdFieldId() {
|
|
908
|
-
const fieldId = this.customFields?.task_id;
|
|
909
|
-
if (fieldId)
|
|
910
|
-
return fieldId;
|
|
911
|
-
throw new BackendError(redmineConfigMissingMessage("custom_fields.task_id"), "E_BACKEND");
|
|
912
|
-
}
|
|
913
|
-
setIssueCustomFieldValue(issue, fieldId, value) {
|
|
914
|
-
const fields = Array.isArray(issue.custom_fields) ? issue.custom_fields : [];
|
|
915
|
-
let found = false;
|
|
916
|
-
const updated = fields.map((field) => {
|
|
917
|
-
if (isRecord(field) && field.id === fieldId) {
|
|
918
|
-
found = true;
|
|
919
|
-
return { ...field, value };
|
|
920
|
-
}
|
|
921
|
-
return field;
|
|
922
|
-
});
|
|
923
|
-
if (!found)
|
|
924
|
-
updated.push({ id: fieldId, value });
|
|
925
|
-
issue.custom_fields = updated;
|
|
926
|
-
}
|
|
927
|
-
async listTasksRemote() {
|
|
928
|
-
const tasks = [];
|
|
929
|
-
const allIssues = [];
|
|
930
|
-
let offset = 0;
|
|
931
|
-
const limit = 100;
|
|
932
|
-
const taskField = this.taskIdFieldId();
|
|
933
|
-
this.issueCache.clear();
|
|
934
|
-
while (true) {
|
|
935
|
-
const payload = await this.requestJson("GET", "issues.json", undefined, {
|
|
936
|
-
project_id: this.projectId,
|
|
937
|
-
limit,
|
|
938
|
-
offset,
|
|
939
|
-
status_id: "*",
|
|
940
|
-
});
|
|
941
|
-
const issues = Array.isArray(payload.issues) ? payload.issues : [];
|
|
942
|
-
const pageIssues = issues.filter((issue) => isRecord(issue));
|
|
943
|
-
allIssues.push(...pageIssues);
|
|
944
|
-
const total = Number(payload.total_count ?? 0);
|
|
945
|
-
if (total === 0 || offset + limit >= total)
|
|
946
|
-
break;
|
|
947
|
-
offset += limit;
|
|
948
|
-
}
|
|
949
|
-
const existingIds = new Set();
|
|
950
|
-
const duplicates = new Set();
|
|
951
|
-
for (const issue of allIssues) {
|
|
952
|
-
const taskId = this.customFieldValue(issue, taskField);
|
|
953
|
-
if (!taskId)
|
|
954
|
-
continue;
|
|
955
|
-
const taskIdStr = toStringSafe(taskId);
|
|
956
|
-
if (!TASK_ID_RE.test(taskIdStr))
|
|
957
|
-
continue;
|
|
958
|
-
if (existingIds.has(taskIdStr))
|
|
959
|
-
duplicates.add(taskIdStr);
|
|
960
|
-
existingIds.add(taskIdStr);
|
|
961
|
-
}
|
|
962
|
-
if (duplicates.size > 0) {
|
|
963
|
-
const sample = [...duplicates].toSorted().slice(0, 5).join(", ");
|
|
964
|
-
throw new BackendError(`Duplicate task_id values found in Redmine: ${sample}`, "E_BACKEND");
|
|
965
|
-
}
|
|
966
|
-
for (const issue of allIssues) {
|
|
967
|
-
const taskId = this.customFieldValue(issue, taskField);
|
|
968
|
-
const taskIdText = toStringSafe(taskId);
|
|
969
|
-
if (!taskIdText || !TASK_ID_RE.test(taskIdText))
|
|
970
|
-
continue;
|
|
971
|
-
const task = this.issueToTask(issue, taskIdText);
|
|
972
|
-
if (task) {
|
|
973
|
-
const idText = toStringSafe(task.id);
|
|
974
|
-
if (idText)
|
|
975
|
-
this.issueCache.set(idText, issue);
|
|
976
|
-
tasks.push(task);
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
return tasks;
|
|
980
|
-
}
|
|
981
|
-
issueFromPayload(payload) {
|
|
982
|
-
return isRecord(payload.issue) ? payload.issue : null;
|
|
983
|
-
}
|
|
984
|
-
async findIssueByTaskId(taskId) {
|
|
985
|
-
const id = toStringSafe(taskId).trim();
|
|
986
|
-
if (!id)
|
|
987
|
-
return null;
|
|
988
|
-
const cached = this.issueCache.get(id);
|
|
989
|
-
if (cached)
|
|
990
|
-
return cached;
|
|
991
|
-
const taskField = this.taskIdFieldId();
|
|
992
|
-
const payload = await this.requestJson("GET", "issues.json", undefined, {
|
|
993
|
-
project_id: this.projectId,
|
|
994
|
-
status_id: "*",
|
|
995
|
-
[`cf_${String(taskField)}`]: id,
|
|
996
|
-
limit: 100,
|
|
997
|
-
});
|
|
998
|
-
const candidates = Array.isArray(payload.issues) ? payload.issues : [];
|
|
999
|
-
for (const candidate of candidates) {
|
|
1000
|
-
if (!isRecord(candidate))
|
|
1001
|
-
continue;
|
|
1002
|
-
const val = this.customFieldValue(candidate, taskField);
|
|
1003
|
-
if (val && String(val) === id) {
|
|
1004
|
-
this.issueCache.set(id, candidate);
|
|
1005
|
-
return candidate;
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
await this.listTasksRemote();
|
|
1009
|
-
const refreshed = this.issueCache.get(id);
|
|
1010
|
-
return refreshed ?? null;
|
|
1011
|
-
}
|
|
1012
|
-
issueToTask(issue, taskIdOverride) {
|
|
1013
|
-
const taskId = taskIdOverride ?? this.customFieldValue(issue, this.customFields.task_id);
|
|
1014
|
-
if (!taskId)
|
|
1015
|
-
return null;
|
|
1016
|
-
const statusVal = isRecord(issue.status) ? issue.status : null;
|
|
1017
|
-
const statusId = statusVal && typeof statusVal.id === "number" ? statusVal.id : null;
|
|
1018
|
-
const status = statusId !== null && this.reverseStatus.has(statusId)
|
|
1019
|
-
? this.reverseStatus.get(statusId)
|
|
1020
|
-
: "TODO";
|
|
1021
|
-
const verifyVal = this.customFieldValue(issue, this.customFields.verify);
|
|
1022
|
-
const commitVal = this.customFieldValue(issue, this.customFields.commit);
|
|
1023
|
-
const docVal = this.customFieldValue(issue, this.customFields.doc);
|
|
1024
|
-
const commentsVal = this.customFieldValue(issue, this.customFields.comments);
|
|
1025
|
-
const docVersionVal = this.customFieldValue(issue, this.customFields.doc_version);
|
|
1026
|
-
const docUpdatedAtVal = this.customFieldValue(issue, this.customFields.doc_updated_at);
|
|
1027
|
-
const docUpdatedByVal = this.customFieldValue(issue, this.customFields.doc_updated_by);
|
|
1028
|
-
const updatedOn = typeof issue.updated_on === "string"
|
|
1029
|
-
? issue.updated_on
|
|
1030
|
-
: typeof issue.created_on === "string"
|
|
1031
|
-
? issue.created_on
|
|
1032
|
-
: null;
|
|
1033
|
-
const priorityVal = isRecord(issue.priority) ? issue.priority : null;
|
|
1034
|
-
const priorityName = normalizePriority(priorityVal?.name);
|
|
1035
|
-
const tags = [];
|
|
1036
|
-
if (Array.isArray(issue.tags)) {
|
|
1037
|
-
for (const tag of issue.tags) {
|
|
1038
|
-
if (isRecord(tag) && tag.name)
|
|
1039
|
-
tags.push(toStringSafe(tag.name));
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
const task = {
|
|
1043
|
-
id: toStringSafe(taskId),
|
|
1044
|
-
title: toStringSafe(issue.subject),
|
|
1045
|
-
description: toStringSafe(issue.description),
|
|
1046
|
-
status: status ?? "TODO",
|
|
1047
|
-
priority: priorityName,
|
|
1048
|
-
owner: this.ownerAgent,
|
|
1049
|
-
tags,
|
|
1050
|
-
depends_on: [],
|
|
1051
|
-
verify: this.maybeParseJson(verifyVal),
|
|
1052
|
-
commit: this.maybeParseJson(commitVal),
|
|
1053
|
-
comments: this.normalizeComments(this.maybeParseJson(commentsVal)),
|
|
1054
|
-
id_source: "custom",
|
|
1055
|
-
};
|
|
1056
|
-
if (docVal)
|
|
1057
|
-
task.doc = toStringSafe(docVal);
|
|
1058
|
-
const docVersion = this.coerceDocVersion(docVersionVal);
|
|
1059
|
-
task.doc_version = docVersion ?? DOC_VERSION;
|
|
1060
|
-
task.doc_updated_at = docUpdatedAtVal ? toStringSafe(docUpdatedAtVal) : (updatedOn ?? nowIso());
|
|
1061
|
-
task.doc_updated_by = docUpdatedByVal ? toStringSafe(docUpdatedByVal) : this.ownerAgent;
|
|
1062
|
-
return task;
|
|
1063
|
-
}
|
|
1064
|
-
taskToIssuePayload(task, existingIssue) {
|
|
1065
|
-
const status = toStringSafe(task.status).trim().toUpperCase();
|
|
1066
|
-
const payload = {
|
|
1067
|
-
subject: toStringSafe(task.title),
|
|
1068
|
-
description: toStringSafe(task.description),
|
|
1069
|
-
};
|
|
1070
|
-
if (status && this.statusMap && status in this.statusMap) {
|
|
1071
|
-
payload.status_id = this.statusMap[status];
|
|
1072
|
-
}
|
|
1073
|
-
if (typeof task.priority === "number")
|
|
1074
|
-
payload.priority_id = task.priority;
|
|
1075
|
-
let existingAssignee = null;
|
|
1076
|
-
if (existingIssue && isRecord(existingIssue.assigned_to)) {
|
|
1077
|
-
existingAssignee = existingIssue.assigned_to.id;
|
|
1078
|
-
}
|
|
1079
|
-
if (this.assigneeId && !existingAssignee)
|
|
1080
|
-
payload.assigned_to_id = this.assigneeId;
|
|
1081
|
-
const startDate = this.startDateFromTaskId(toStringSafe(task.id));
|
|
1082
|
-
if (startDate)
|
|
1083
|
-
payload.start_date = startDate;
|
|
1084
|
-
const doneRatio = this.doneRatioForStatus(status);
|
|
1085
|
-
if (doneRatio !== null)
|
|
1086
|
-
payload.done_ratio = doneRatio;
|
|
1087
|
-
const customFields = [];
|
|
1088
|
-
this.ensureDocMetadata(task);
|
|
1089
|
-
this.appendCustomField(customFields, "task_id", task.id);
|
|
1090
|
-
this.appendCustomField(customFields, "verify", task.verify);
|
|
1091
|
-
this.appendCustomField(customFields, "commit", task.commit);
|
|
1092
|
-
this.appendCustomField(customFields, "comments", task.comments);
|
|
1093
|
-
this.appendCustomField(customFields, "doc", task.doc);
|
|
1094
|
-
this.appendCustomField(customFields, "doc_version", task.doc_version);
|
|
1095
|
-
this.appendCustomField(customFields, "doc_updated_at", task.doc_updated_at);
|
|
1096
|
-
this.appendCustomField(customFields, "doc_updated_by", task.doc_updated_by);
|
|
1097
|
-
if (customFields.length > 0)
|
|
1098
|
-
payload.custom_fields = customFields;
|
|
1099
|
-
return payload;
|
|
1100
|
-
}
|
|
1101
|
-
appendCustomField(fields, key, value) {
|
|
1102
|
-
const fieldId = this.customFields?.[key];
|
|
1103
|
-
if (!fieldId)
|
|
1104
|
-
return;
|
|
1105
|
-
let payloadValue = value;
|
|
1106
|
-
if (Array.isArray(value) || isRecord(value)) {
|
|
1107
|
-
payloadValue = JSON.stringify(value);
|
|
1108
|
-
}
|
|
1109
|
-
fields.push({ id: fieldId, value: payloadValue });
|
|
1110
|
-
}
|
|
1111
|
-
normalizeComments(value) {
|
|
1112
|
-
if (Array.isArray(value)) {
|
|
1113
|
-
return value.filter((item) => isRecord(item) ? typeof item.author === "string" && typeof item.body === "string" : false);
|
|
1114
|
-
}
|
|
1115
|
-
if (isRecord(value))
|
|
1116
|
-
return [value];
|
|
1117
|
-
if (typeof value === "string" && value.trim()) {
|
|
1118
|
-
return [{ author: "redmine", body: value.trim() }];
|
|
1119
|
-
}
|
|
1120
|
-
return [];
|
|
1121
|
-
}
|
|
1122
|
-
commentsToPairs(comments) {
|
|
1123
|
-
const pairs = [];
|
|
1124
|
-
for (const comment of comments) {
|
|
1125
|
-
const author = toStringSafe(comment.author).trim();
|
|
1126
|
-
const body = toStringSafe(comment.body).trim();
|
|
1127
|
-
if (!author && !body)
|
|
1128
|
-
continue;
|
|
1129
|
-
pairs.push([author, body]);
|
|
1130
|
-
}
|
|
1131
|
-
return pairs;
|
|
1132
|
-
}
|
|
1133
|
-
formatCommentNote(author = "unknown", body = "") {
|
|
1134
|
-
const authorText = author;
|
|
1135
|
-
const bodyText = body;
|
|
1136
|
-
return `[comment] ${authorText}: ${bodyText}`.trim();
|
|
1137
|
-
}
|
|
1138
|
-
async appendCommentNotes(issueId, existingComments, desiredComments) {
|
|
1139
|
-
const issueIdText = toStringSafe(issueId);
|
|
1140
|
-
if (!issueIdText)
|
|
1141
|
-
return;
|
|
1142
|
-
const existingPairs = this.commentsToPairs(existingComments);
|
|
1143
|
-
const desiredPairs = this.commentsToPairs(desiredComments);
|
|
1144
|
-
if (desiredPairs.length === 0)
|
|
1145
|
-
return;
|
|
1146
|
-
if (desiredPairs.length < existingPairs.length)
|
|
1147
|
-
return;
|
|
1148
|
-
if (existingPairs.length > 0) {
|
|
1149
|
-
const prefix = desiredPairs.slice(0, existingPairs.length);
|
|
1150
|
-
const matches = prefix.length === existingPairs.length &&
|
|
1151
|
-
prefix.every((pair, idx) => pair[0] === existingPairs[idx]?.[0] && pair[1] === existingPairs[idx]?.[1]);
|
|
1152
|
-
if (!matches)
|
|
1153
|
-
return;
|
|
1154
|
-
}
|
|
1155
|
-
const newPairs = desiredPairs.slice(existingPairs.length);
|
|
1156
|
-
for (const [author, body] of newPairs) {
|
|
1157
|
-
const note = this.formatCommentNote(author, body);
|
|
1158
|
-
if (note) {
|
|
1159
|
-
await this.requestJson("PUT", `issues/${issueIdText}.json`, { issue: { notes: note } });
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
startDateFromTaskId(taskId) {
|
|
1164
|
-
if (!taskId.includes("-"))
|
|
1165
|
-
return null;
|
|
1166
|
-
const prefix = taskId.split("-", 1)[0] ?? "";
|
|
1167
|
-
if (prefix.length < 8)
|
|
1168
|
-
return null;
|
|
1169
|
-
const year = prefix.slice(0, 4);
|
|
1170
|
-
const month = prefix.slice(4, 6);
|
|
1171
|
-
const day = prefix.slice(6, 8);
|
|
1172
|
-
if (!/^\d{8}$/u.test(`${year}${month}${day}`))
|
|
1173
|
-
return null;
|
|
1174
|
-
return `${year}-${month}-${day}`;
|
|
1175
|
-
}
|
|
1176
|
-
doneRatioForStatus(status) {
|
|
1177
|
-
if (!status)
|
|
1178
|
-
return null;
|
|
1179
|
-
if (status === "DONE")
|
|
1180
|
-
return 100;
|
|
1181
|
-
return 0;
|
|
1182
|
-
}
|
|
1183
|
-
customFieldValue(issue, fieldId) {
|
|
1184
|
-
if (!fieldId)
|
|
1185
|
-
return null;
|
|
1186
|
-
const fields = Array.isArray(issue.custom_fields) ? issue.custom_fields : [];
|
|
1187
|
-
for (const field of fields) {
|
|
1188
|
-
if (isRecord(field) && field.id === fieldId) {
|
|
1189
|
-
const value = field.value;
|
|
1190
|
-
return value !== undefined && value !== null ? toStringSafe(value) : "";
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
return null;
|
|
1194
|
-
}
|
|
1195
|
-
maybeParseJson(value) {
|
|
1196
|
-
if (value === null || value === undefined)
|
|
1197
|
-
return null;
|
|
1198
|
-
const raw = toStringSafe(value).trim();
|
|
1199
|
-
if (!raw)
|
|
1200
|
-
return null;
|
|
1201
|
-
if (raw.startsWith("{") || raw.startsWith("[")) {
|
|
1202
|
-
try {
|
|
1203
|
-
return JSON.parse(raw);
|
|
1204
|
-
}
|
|
1205
|
-
catch {
|
|
1206
|
-
return raw;
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
return raw;
|
|
1210
|
-
}
|
|
1211
|
-
coerceDocVersion(value) {
|
|
1212
|
-
if (value === null || value === undefined)
|
|
1213
|
-
return null;
|
|
1214
|
-
if (typeof value === "number")
|
|
1215
|
-
return value;
|
|
1216
|
-
const raw = toStringSafe(value).trim();
|
|
1217
|
-
if (/^\d+$/u.test(raw))
|
|
1218
|
-
return Number(raw);
|
|
1219
|
-
return null;
|
|
1220
|
-
}
|
|
1221
|
-
async requestJson(method, reqPath, payload, params, opts) {
|
|
1222
|
-
let url = `${this.baseUrl}/${reqPath.replace(/^\//u, "")}`;
|
|
1223
|
-
if (params) {
|
|
1224
|
-
const search = new URLSearchParams();
|
|
1225
|
-
for (const [key, value] of Object.entries(params)) {
|
|
1226
|
-
if (value === undefined || value === null)
|
|
1227
|
-
continue;
|
|
1228
|
-
search.append(key, toStringSafe(value));
|
|
1229
|
-
}
|
|
1230
|
-
const qs = search.toString();
|
|
1231
|
-
if (qs)
|
|
1232
|
-
url += `?${qs}`;
|
|
1233
|
-
}
|
|
1234
|
-
const attempts = Math.max(1, opts?.attempts ?? 3);
|
|
1235
|
-
const backoff = opts?.backoff ?? 0.5;
|
|
1236
|
-
let lastError = null;
|
|
1237
|
-
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
1238
|
-
try {
|
|
1239
|
-
const resp = await fetch(url, {
|
|
1240
|
-
method,
|
|
1241
|
-
headers: {
|
|
1242
|
-
"Content-Type": "application/json",
|
|
1243
|
-
"X-Redmine-API-Key": this.apiKey,
|
|
1244
|
-
},
|
|
1245
|
-
body: payload ? JSON.stringify(payload) : undefined,
|
|
1246
|
-
});
|
|
1247
|
-
const text = await resp.text();
|
|
1248
|
-
if (!resp.ok) {
|
|
1249
|
-
if ((resp.status === 429 || resp.status >= 500) && attempt < attempts) {
|
|
1250
|
-
await sleep(backoff * attempt * 1000);
|
|
1251
|
-
continue;
|
|
1252
|
-
}
|
|
1253
|
-
throw new BackendError(`Redmine API error: ${resp.status} ${text}`, "E_BACKEND");
|
|
1254
|
-
}
|
|
1255
|
-
if (!text)
|
|
1256
|
-
return {};
|
|
1257
|
-
try {
|
|
1258
|
-
const parsed = JSON.parse(text);
|
|
1259
|
-
if (isRecord(parsed))
|
|
1260
|
-
return parsed;
|
|
1261
|
-
return {};
|
|
1262
|
-
}
|
|
1263
|
-
catch {
|
|
1264
|
-
return {};
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
catch (err) {
|
|
1268
|
-
lastError = err;
|
|
1269
|
-
if (err instanceof BackendError)
|
|
1270
|
-
throw err;
|
|
1271
|
-
if (attempt >= attempts) {
|
|
1272
|
-
throw new RedmineUnavailable("Redmine unavailable");
|
|
1273
|
-
}
|
|
1274
|
-
await sleep(backoff * attempt * 1000);
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
throw lastError instanceof Error ? lastError : new RedmineUnavailable("Redmine unavailable");
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
async function loadBackendConfig(configPath) {
|
|
1281
|
-
try {
|
|
1282
|
-
const raw = JSON.parse(await readFile(configPath, "utf8"));
|
|
1283
|
-
return isRecord(raw) ? raw : null;
|
|
1284
|
-
}
|
|
1285
|
-
catch (err) {
|
|
1286
|
-
const code = err?.code;
|
|
1287
|
-
if (code === "ENOENT")
|
|
1288
|
-
return null;
|
|
1289
|
-
throw err;
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
function resolveMaybeRelative(root, input) {
|
|
1293
|
-
if (!input)
|
|
1294
|
-
return null;
|
|
1295
|
-
const raw = toStringSafe(input).trim();
|
|
1296
|
-
if (!raw)
|
|
1297
|
-
return null;
|
|
1298
|
-
return path.isAbsolute(raw) ? raw : path.join(root, raw);
|
|
1299
|
-
}
|
|
1300
|
-
function normalizeBackendConfig(raw) {
|
|
1301
|
-
if (!isRecord(raw)) {
|
|
1302
|
-
return { id: "local", version: 1, settings: {} };
|
|
1303
|
-
}
|
|
1304
|
-
const id = toStringSafe(raw.id).trim() || "local";
|
|
1305
|
-
const version = typeof raw.version === "number" ? raw.version : 1;
|
|
1306
|
-
const settings = isRecord(raw.settings) ? raw.settings : {};
|
|
1307
|
-
return { id, version, settings };
|
|
1308
|
-
}
|
|
1309
|
-
export async function loadTaskBackend(opts) {
|
|
1310
|
-
const resolved = await resolveProject({ cwd: opts.cwd, rootOverride: opts.rootOverride ?? null });
|
|
1311
|
-
const loaded = await loadConfig(resolved.agentplaneDir);
|
|
1312
|
-
const backendConfigPath = path.join(resolved.gitRoot, loaded.config.tasks_backend.config_path);
|
|
1313
|
-
const backendConfig = await loadBackendConfig(backendConfigPath);
|
|
1314
|
-
const normalized = normalizeBackendConfig(backendConfig);
|
|
1315
|
-
const backendId = normalized.id;
|
|
1316
|
-
const settings = normalized.settings;
|
|
1317
|
-
if (backendId === "redmine") {
|
|
1318
|
-
await loadDotEnv(resolved.gitRoot);
|
|
1319
|
-
const cacheDirRaw = resolveMaybeRelative(resolved.gitRoot, settings.cache_dir);
|
|
1320
|
-
const cacheDir = cacheDirRaw ?? path.join(resolved.gitRoot, loaded.config.paths.workflow_dir);
|
|
1321
|
-
const cache = cacheDir ? new LocalBackend({ dir: cacheDir }) : null;
|
|
1322
|
-
const redmine = new RedmineBackend(settings, { cache });
|
|
1323
|
-
return { backend: redmine, backendId, resolved, config: loaded.config, backendConfigPath };
|
|
1324
|
-
}
|
|
1325
|
-
const localDir = resolveMaybeRelative(resolved.gitRoot, settings.dir) ??
|
|
1326
|
-
path.join(resolved.gitRoot, loaded.config.paths.workflow_dir);
|
|
1327
|
-
const local = new LocalBackend({ dir: localDir });
|
|
1328
|
-
return { backend: local, backendId: "local", resolved, config: loaded.config, backendConfigPath };
|
|
1329
|
-
}
|
|
1
|
+
export { BackendError, RedmineUnavailable, buildTasksExportSnapshotFromTasks, extractTaskDoc, mergeTaskDoc, taskRecordToData, writeTasksExportFromTasks, } from "./task-backend/shared.js";
|
|
2
|
+
export { LocalBackend } from "./task-backend/local-backend.js";
|
|
3
|
+
export { RedmineBackend } from "./task-backend/redmine-backend.js";
|
|
4
|
+
export { loadTaskBackend } from "./task-backend/load.js";
|