From 5265cc005900498b81566cf45079f7a12ce66d48 Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:03:38 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Builtin=E7=AE=A1=E7=90=86=E3=82=92?= =?UTF-8?q?=E3=83=90=E3=83=B3=E3=83=89=E3=83=AB=E5=9F=8B=E3=82=81=E8=BE=BC?= =?UTF-8?q?=E3=81=BF=E6=96=B9=E5=BC=8F=E3=81=AB=E7=A7=BB=E8=A1=8C=E3=81=97?= =?UTF-8?q?=E3=80=81/eject=E3=82=B3=E3=83=9E=E3=83=B3=E3=83=89=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=20(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ローダーがユーザーファイル優先、なければdist/resources/からbuiltinを読む方式に変更 - /ejectコマンドを追加(builtinを~/.takt/にコピーしてカスタマイズ可能に) - /refresh-builtinを簡素化(ejectへの移行案内) - config.yamlにdisabled_builtinsフィールドを追加 - ワークフローYAMLをrules形式に統一 --- resources/global/en/workflows/default.yaml | 20 +-- .../global/en/workflows/expert-cqrs.yaml | 28 ++-- resources/global/en/workflows/expert.yaml | 28 ++-- resources/global/en/workflows/magi.yaml | 6 +- resources/global/en/workflows/research.yaml | 6 +- resources/global/en/workflows/simple.yaml | 10 +- resources/global/ja/workflows/default.yaml | 20 +-- .../global/ja/workflows/expert-cqrs.yaml | 28 ++-- resources/global/ja/workflows/expert.yaml | 28 ++-- resources/global/ja/workflows/magi.yaml | 6 +- resources/global/ja/workflows/research.yaml | 6 +- resources/global/ja/workflows/simple.yaml | 10 +- src/__tests__/config.test.ts | 73 ++++++++++- src/__tests__/initialization.test.ts | 43 +----- src/cli.ts | 7 +- src/commands/eject.ts | 124 ++++++++++++++++++ src/commands/help.ts | 6 +- src/commands/index.ts | 1 + src/commands/refreshBuiltin.ts | 47 ++----- src/config/agentLoader.ts | 46 +++---- src/config/globalConfig.ts | 14 ++ src/config/initialization.ts | 52 ++++---- src/config/paths.ts | 12 ++ src/config/workflowLoader.ts | 87 +++++++++--- src/models/schemas.ts | 2 + src/models/types.ts | 2 + src/resources/index.ts | 86 +----------- 27 files changed, 466 insertions(+), 332 deletions(-) create mode 100644 src/commands/eject.ts diff --git a/resources/global/en/workflows/default.yaml b/resources/global/en/workflows/default.yaml index 5beb1a3..e4ba857 100644 --- a/resources/global/en/workflows/default.yaml +++ b/resources/global/en/workflows/default.yaml @@ -29,7 +29,7 @@ initial_step: plan steps: - name: plan edit: false - agent: ~/.takt/agents/default/planner.md + agent: ../agents/default/planner.md report: name: 00-plan.md format: | @@ -89,7 +89,7 @@ steps: - name: implement edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md report: - Scope: 01-coder-scope.md - Decisions: 02-coder-decisions.md @@ -144,7 +144,7 @@ steps: - name: ai_review edit: false - agent: ~/.takt/agents/default/ai-antipattern-reviewer.md + agent: ../agents/default/ai-antipattern-reviewer.md report: name: 03-ai-review.md format: | @@ -194,7 +194,7 @@ steps: - name: ai_fix edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -224,7 +224,7 @@ steps: - name: review edit: false - agent: ~/.takt/agents/default/architecture-reviewer.md + agent: ../agents/default/architecture-reviewer.md report: name: 04-architect-review.md format: | @@ -275,7 +275,7 @@ steps: - name: improve edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -307,7 +307,7 @@ steps: - name: fix edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -334,7 +334,7 @@ steps: - name: security_review edit: false - agent: ~/.takt/agents/default/security-reviewer.md + agent: ../agents/default/security-reviewer.md report: name: 05-security-review.md format: | @@ -387,7 +387,7 @@ steps: - name: security_fix edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -413,7 +413,7 @@ steps: - name: supervise edit: false - agent: ~/.takt/agents/default/supervisor.md + agent: ../agents/default/supervisor.md report: - Validation: 06-supervisor-validation.md - Summary: summary.md diff --git a/resources/global/en/workflows/expert-cqrs.yaml b/resources/global/en/workflows/expert-cqrs.yaml index b8c8dd9..9d4a907 100644 --- a/resources/global/en/workflows/expert-cqrs.yaml +++ b/resources/global/en/workflows/expert-cqrs.yaml @@ -32,7 +32,7 @@ steps: # =========================================== - name: plan edit: false - agent: ~/.takt/agents/default/planner.md + agent: ../agents/default/planner.md report: name: 00-plan.md format: | @@ -89,7 +89,7 @@ steps: # =========================================== - name: implement edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md report: - Scope: 01-coder-scope.md - Decisions: 02-coder-decisions.md @@ -146,7 +146,7 @@ steps: # =========================================== - name: ai_review edit: false - agent: ~/.takt/agents/default/ai-antipattern-reviewer.md + agent: ../agents/default/ai-antipattern-reviewer.md report: name: 03-ai-review.md format: | @@ -196,7 +196,7 @@ steps: - name: ai_fix edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -228,7 +228,7 @@ steps: # =========================================== - name: cqrs_es_review edit: false - agent: ~/.takt/agents/expert-cqrs/cqrs-es-reviewer.md + agent: ../agents/expert-cqrs/cqrs-es-reviewer.md report: name: 04-cqrs-es-review.md format: | @@ -282,7 +282,7 @@ steps: - name: fix_cqrs_es edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -316,7 +316,7 @@ steps: # =========================================== - name: frontend_review edit: false - agent: ~/.takt/agents/expert/frontend-reviewer.md + agent: ../agents/expert/frontend-reviewer.md report: name: 05-frontend-review.md format: | @@ -370,7 +370,7 @@ steps: - name: fix_frontend edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -404,7 +404,7 @@ steps: # =========================================== - name: security_review edit: false - agent: ~/.takt/agents/expert/security-reviewer.md + agent: ../agents/expert/security-reviewer.md report: name: 06-security-review.md format: | @@ -455,7 +455,7 @@ steps: - name: fix_security edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -499,7 +499,7 @@ steps: # =========================================== - name: qa_review edit: false - agent: ~/.takt/agents/expert/qa-reviewer.md + agent: ../agents/expert/qa-reviewer.md report: name: 07-qa-review.md format: | @@ -550,7 +550,7 @@ steps: - name: fix_qa edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -598,7 +598,7 @@ steps: # =========================================== - name: supervise edit: false - agent: ~/.takt/agents/expert/supervisor.md + agent: ../agents/expert/supervisor.md report: - Validation: 08-supervisor-validation.md - Summary: summary.md @@ -691,7 +691,7 @@ steps: - name: fix_supervisor edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob diff --git a/resources/global/en/workflows/expert.yaml b/resources/global/en/workflows/expert.yaml index a7dd490..364db83 100644 --- a/resources/global/en/workflows/expert.yaml +++ b/resources/global/en/workflows/expert.yaml @@ -44,7 +44,7 @@ steps: # =========================================== - name: plan edit: false - agent: ~/.takt/agents/default/planner.md + agent: ../agents/default/planner.md report: name: 00-plan.md format: | @@ -101,7 +101,7 @@ steps: # =========================================== - name: implement edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md report: - Scope: 01-coder-scope.md - Decisions: 02-coder-decisions.md @@ -158,7 +158,7 @@ steps: # =========================================== - name: ai_review edit: false - agent: ~/.takt/agents/default/ai-antipattern-reviewer.md + agent: ../agents/default/ai-antipattern-reviewer.md report: name: 03-ai-review.md format: | @@ -208,7 +208,7 @@ steps: - name: ai_fix edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -240,7 +240,7 @@ steps: # =========================================== - name: architect_review edit: false - agent: ~/.takt/agents/default/architecture-reviewer.md + agent: ../agents/default/architecture-reviewer.md report: name: 04-architect-review.md format: | @@ -300,7 +300,7 @@ steps: - name: fix_architect edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -330,7 +330,7 @@ steps: # =========================================== - name: frontend_review edit: false - agent: ~/.takt/agents/expert/frontend-reviewer.md + agent: ../agents/expert/frontend-reviewer.md report: name: 05-frontend-review.md format: | @@ -384,7 +384,7 @@ steps: - name: fix_frontend edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -418,7 +418,7 @@ steps: # =========================================== - name: security_review edit: false - agent: ~/.takt/agents/expert/security-reviewer.md + agent: ../agents/expert/security-reviewer.md report: name: 06-security-review.md format: | @@ -469,7 +469,7 @@ steps: - name: fix_security edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -513,7 +513,7 @@ steps: # =========================================== - name: qa_review edit: false - agent: ~/.takt/agents/expert/qa-reviewer.md + agent: ../agents/expert/qa-reviewer.md report: name: 07-qa-review.md format: | @@ -564,7 +564,7 @@ steps: - name: fix_qa edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -612,7 +612,7 @@ steps: # =========================================== - name: supervise edit: false - agent: ~/.takt/agents/expert/supervisor.md + agent: ../agents/expert/supervisor.md report: - Validation: 08-supervisor-validation.md - Summary: summary.md @@ -705,7 +705,7 @@ steps: - name: fix_supervisor edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob diff --git a/resources/global/en/workflows/magi.yaml b/resources/global/en/workflows/magi.yaml index 03bf470..38f1b01 100644 --- a/resources/global/en/workflows/magi.yaml +++ b/resources/global/en/workflows/magi.yaml @@ -18,7 +18,7 @@ max_iterations: 5 steps: - name: melchior - agent: ~/.takt/agents/magi/melchior.md + agent: ../agents/magi/melchior.md allowed_tools: - Read - Glob @@ -55,7 +55,7 @@ steps: next: balthasar - name: balthasar - agent: ~/.takt/agents/magi/balthasar.md + agent: ../agents/magi/balthasar.md allowed_tools: - Read - Glob @@ -97,7 +97,7 @@ steps: next: casper - name: casper - agent: ~/.takt/agents/magi/casper.md + agent: ../agents/magi/casper.md allowed_tools: - Read - Glob diff --git a/resources/global/en/workflows/research.yaml b/resources/global/en/workflows/research.yaml index 762e3b5..4f3aa1e 100644 --- a/resources/global/en/workflows/research.yaml +++ b/resources/global/en/workflows/research.yaml @@ -22,7 +22,7 @@ max_iterations: 10 steps: - name: plan - agent: ~/.takt/agents/research/planner.md + agent: ../agents/research/planner.md allowed_tools: - Read - Glob @@ -59,7 +59,7 @@ steps: next: ABORT - name: dig - agent: ~/.takt/agents/research/digger.md + agent: ../agents/research/digger.md allowed_tools: - Read - Glob @@ -101,7 +101,7 @@ steps: next: ABORT - name: supervise - agent: ~/.takt/agents/research/supervisor.md + agent: ../agents/research/supervisor.md allowed_tools: - Read - Glob diff --git a/resources/global/en/workflows/simple.yaml b/resources/global/en/workflows/simple.yaml index 1524ea3..0ef12b5 100644 --- a/resources/global/en/workflows/simple.yaml +++ b/resources/global/en/workflows/simple.yaml @@ -26,7 +26,7 @@ initial_step: plan steps: - name: plan edit: false - agent: ~/.takt/agents/default/planner.md + agent: ../agents/default/planner.md report: name: 00-plan.md format: | @@ -82,7 +82,7 @@ steps: - name: implement edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md report: - Scope: 01-coder-scope.md - Decisions: 02-coder-decisions.md @@ -137,7 +137,7 @@ steps: - name: ai_review edit: false - agent: ~/.takt/agents/default/ai-antipattern-reviewer.md + agent: ../agents/default/ai-antipattern-reviewer.md report: name: 03-ai-review.md format: | @@ -187,7 +187,7 @@ steps: - name: review edit: false - agent: ~/.takt/agents/default/architecture-reviewer.md + agent: ../agents/default/architecture-reviewer.md report: name: 04-architect-review.md format: | @@ -239,7 +239,7 @@ steps: - name: supervise edit: false - agent: ~/.takt/agents/default/supervisor.md + agent: ../agents/default/supervisor.md report: - Validation: 05-supervisor-validation.md - Summary: summary.md diff --git a/resources/global/ja/workflows/default.yaml b/resources/global/ja/workflows/default.yaml index 9026f30..4471f49 100644 --- a/resources/global/ja/workflows/default.yaml +++ b/resources/global/ja/workflows/default.yaml @@ -20,7 +20,7 @@ initial_step: plan steps: - name: plan edit: false - agent: ~/.takt/agents/default/planner.md + agent: ../agents/default/planner.md report: name: 00-plan.md format: | @@ -80,7 +80,7 @@ steps: - name: implement edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md report: - Scope: 01-coder-scope.md - Decisions: 02-coder-decisions.md @@ -140,7 +140,7 @@ steps: - name: ai_review edit: false - agent: ~/.takt/agents/default/ai-antipattern-reviewer.md + agent: ../agents/default/ai-antipattern-reviewer.md report: name: 03-ai-review.md format: | @@ -190,7 +190,7 @@ steps: - name: ai_fix edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -220,7 +220,7 @@ steps: - name: review edit: false - agent: ~/.takt/agents/default/architecture-reviewer.md + agent: ../agents/default/architecture-reviewer.md report: name: 04-architect-review.md format: | @@ -282,7 +282,7 @@ steps: - name: improve edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -314,7 +314,7 @@ steps: - name: fix edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -340,7 +340,7 @@ steps: - name: security_review edit: false - agent: ~/.takt/agents/default/security-reviewer.md + agent: ../agents/default/security-reviewer.md report: name: 05-security-review.md format: | @@ -393,7 +393,7 @@ steps: - name: security_fix edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -419,7 +419,7 @@ steps: - name: supervise edit: false - agent: ~/.takt/agents/default/supervisor.md + agent: ../agents/default/supervisor.md report: - Validation: 06-supervisor-validation.md - Summary: summary.md diff --git a/resources/global/ja/workflows/expert-cqrs.yaml b/resources/global/ja/workflows/expert-cqrs.yaml index ba94f6e..559347e 100644 --- a/resources/global/ja/workflows/expert-cqrs.yaml +++ b/resources/global/ja/workflows/expert-cqrs.yaml @@ -41,7 +41,7 @@ steps: # =========================================== - name: plan edit: false - agent: ~/.takt/agents/default/planner.md + agent: ../agents/default/planner.md report: name: 00-plan.md format: | @@ -98,7 +98,7 @@ steps: # =========================================== - name: implement edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md report: - Scope: 01-coder-scope.md - Decisions: 02-coder-decisions.md @@ -155,7 +155,7 @@ steps: # =========================================== - name: ai_review edit: false - agent: ~/.takt/agents/default/ai-antipattern-reviewer.md + agent: ../agents/default/ai-antipattern-reviewer.md report: name: 03-ai-review.md format: | @@ -205,7 +205,7 @@ steps: - name: ai_fix edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -237,7 +237,7 @@ steps: # =========================================== - name: cqrs_es_review edit: false - agent: ~/.takt/agents/expert-cqrs/cqrs-es-reviewer.md + agent: ../agents/expert-cqrs/cqrs-es-reviewer.md report: name: 04-cqrs-es-review.md format: | @@ -291,7 +291,7 @@ steps: - name: fix_cqrs_es edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -325,7 +325,7 @@ steps: # =========================================== - name: frontend_review edit: false - agent: ~/.takt/agents/expert/frontend-reviewer.md + agent: ../agents/expert/frontend-reviewer.md report: name: 05-frontend-review.md format: | @@ -379,7 +379,7 @@ steps: - name: fix_frontend edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -413,7 +413,7 @@ steps: # =========================================== - name: security_review edit: false - agent: ~/.takt/agents/expert/security-reviewer.md + agent: ../agents/expert/security-reviewer.md report: name: 06-security-review.md format: | @@ -464,7 +464,7 @@ steps: - name: fix_security edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -508,7 +508,7 @@ steps: # =========================================== - name: qa_review edit: false - agent: ~/.takt/agents/expert/qa-reviewer.md + agent: ../agents/expert/qa-reviewer.md report: name: 07-qa-review.md format: | @@ -559,7 +559,7 @@ steps: - name: fix_qa edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -607,7 +607,7 @@ steps: # =========================================== - name: supervise edit: false - agent: ~/.takt/agents/expert/supervisor.md + agent: ../agents/expert/supervisor.md report: - Validation: 08-supervisor-validation.md - Summary: summary.md @@ -700,7 +700,7 @@ steps: - name: fix_supervisor edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob diff --git a/resources/global/ja/workflows/expert.yaml b/resources/global/ja/workflows/expert.yaml index 2084468..5941e7b 100644 --- a/resources/global/ja/workflows/expert.yaml +++ b/resources/global/ja/workflows/expert.yaml @@ -32,7 +32,7 @@ steps: # =========================================== - name: plan edit: false - agent: ~/.takt/agents/default/planner.md + agent: ../agents/default/planner.md report: name: 00-plan.md format: | @@ -89,7 +89,7 @@ steps: # =========================================== - name: implement edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md report: - Scope: 01-coder-scope.md - Decisions: 02-coder-decisions.md @@ -146,7 +146,7 @@ steps: # =========================================== - name: ai_review edit: false - agent: ~/.takt/agents/default/ai-antipattern-reviewer.md + agent: ../agents/default/ai-antipattern-reviewer.md report: name: 03-ai-review.md format: | @@ -196,7 +196,7 @@ steps: - name: ai_fix edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -228,7 +228,7 @@ steps: # =========================================== - name: architect_review edit: false - agent: ~/.takt/agents/default/architecture-reviewer.md + agent: ../agents/default/architecture-reviewer.md report: name: 04-architect-review.md format: | @@ -288,7 +288,7 @@ steps: - name: fix_architect edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -318,7 +318,7 @@ steps: # =========================================== - name: frontend_review edit: false - agent: ~/.takt/agents/expert/frontend-reviewer.md + agent: ../agents/expert/frontend-reviewer.md report: name: 05-frontend-review.md format: | @@ -372,7 +372,7 @@ steps: - name: fix_frontend edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -406,7 +406,7 @@ steps: # =========================================== - name: security_review edit: false - agent: ~/.takt/agents/expert/security-reviewer.md + agent: ../agents/expert/security-reviewer.md report: name: 06-security-review.md format: | @@ -457,7 +457,7 @@ steps: - name: fix_security edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -501,7 +501,7 @@ steps: # =========================================== - name: qa_review edit: false - agent: ~/.takt/agents/expert/qa-reviewer.md + agent: ../agents/expert/qa-reviewer.md report: name: 07-qa-review.md format: | @@ -552,7 +552,7 @@ steps: - name: fix_qa edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob @@ -600,7 +600,7 @@ steps: # =========================================== - name: supervise edit: false - agent: ~/.takt/agents/expert/supervisor.md + agent: ../agents/expert/supervisor.md report: - Validation: 08-supervisor-validation.md - Summary: summary.md @@ -693,7 +693,7 @@ steps: - name: fix_supervisor edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md allowed_tools: - Read - Glob diff --git a/resources/global/ja/workflows/magi.yaml b/resources/global/ja/workflows/magi.yaml index 882758a..8f76983 100644 --- a/resources/global/ja/workflows/magi.yaml +++ b/resources/global/ja/workflows/magi.yaml @@ -18,7 +18,7 @@ max_iterations: 5 steps: - name: melchior - agent: ~/.takt/agents/magi/melchior.md + agent: ../agents/magi/melchior.md allowed_tools: - Read - Glob @@ -55,7 +55,7 @@ steps: next: balthasar - name: balthasar - agent: ~/.takt/agents/magi/balthasar.md + agent: ../agents/magi/balthasar.md allowed_tools: - Read - Glob @@ -97,7 +97,7 @@ steps: next: casper - name: casper - agent: ~/.takt/agents/magi/casper.md + agent: ../agents/magi/casper.md allowed_tools: - Read - Glob diff --git a/resources/global/ja/workflows/research.yaml b/resources/global/ja/workflows/research.yaml index a82d640..81c711d 100644 --- a/resources/global/ja/workflows/research.yaml +++ b/resources/global/ja/workflows/research.yaml @@ -22,7 +22,7 @@ max_iterations: 10 steps: - name: plan - agent: ~/.takt/agents/research/planner.md + agent: ../agents/research/planner.md allowed_tools: - Read - Glob @@ -59,7 +59,7 @@ steps: next: ABORT - name: dig - agent: ~/.takt/agents/research/digger.md + agent: ../agents/research/digger.md allowed_tools: - Read - Glob @@ -101,7 +101,7 @@ steps: next: ABORT - name: supervise - agent: ~/.takt/agents/research/supervisor.md + agent: ../agents/research/supervisor.md allowed_tools: - Read - Glob diff --git a/resources/global/ja/workflows/simple.yaml b/resources/global/ja/workflows/simple.yaml index fd86d39..2092252 100644 --- a/resources/global/ja/workflows/simple.yaml +++ b/resources/global/ja/workflows/simple.yaml @@ -21,7 +21,7 @@ initial_step: plan steps: - name: plan edit: false - agent: ~/.takt/agents/default/planner.md + agent: ../agents/default/planner.md report: name: 00-plan.md format: | @@ -81,7 +81,7 @@ steps: - name: implement edit: true - agent: ~/.takt/agents/default/coder.md + agent: ../agents/default/coder.md report: - Scope: 01-coder-scope.md - Decisions: 02-coder-decisions.md @@ -136,7 +136,7 @@ steps: - name: ai_review edit: false - agent: ~/.takt/agents/default/ai-antipattern-reviewer.md + agent: ../agents/default/ai-antipattern-reviewer.md report: name: 03-ai-review.md format: | @@ -186,7 +186,7 @@ steps: - name: review edit: false - agent: ~/.takt/agents/default/architecture-reviewer.md + agent: ../agents/default/architecture-reviewer.md report: name: 04-architect-review.md format: | @@ -238,7 +238,7 @@ steps: - name: supervise edit: false - agent: ~/.takt/agents/default/supervisor.md + agent: ../agents/default/supervisor.md report: - Validation: 05-supervisor-validation.md - Summary: summary.md diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index a6f306e..8c69908 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -10,11 +10,15 @@ import { randomUUID } from 'node:crypto'; import { getBuiltinWorkflow, loadAllWorkflows, + loadWorkflow, + listWorkflows, + loadAgentPromptFromPath, } from '../config/loader.js'; import { getCurrentWorkflow, setCurrentWorkflow, getProjectConfigDir, + getBuiltinAgentsDir, loadInputHistory, saveInputHistory, addToInputHistory, @@ -27,11 +31,17 @@ import { loadWorktreeSessions, updateWorktreeSession, } from '../config/paths.js'; +import { getLanguage } from '../config/globalConfig.js'; import { loadProjectConfig } from '../config/projectConfig.js'; describe('getBuiltinWorkflow', () => { - it('should return null for all workflow names (no built-in workflows)', () => { - expect(getBuiltinWorkflow('default')).toBeNull(); + it('should return builtin workflow when it exists in resources', () => { + const workflow = getBuiltinWorkflow('default'); + expect(workflow).not.toBeNull(); + expect(workflow!.name).toBe('default'); + }); + + it('should return null for non-existent workflow names', () => { expect(getBuiltinWorkflow('passthrough')).toBeNull(); expect(getBuiltinWorkflow('unknown')).toBeNull(); expect(getBuiltinWorkflow('')).toBeNull(); @@ -78,6 +88,65 @@ steps: }); }); +describe('loadWorkflow (builtin fallback)', () => { + it('should load builtin workflow when user workflow does not exist', () => { + const workflow = loadWorkflow('default'); + expect(workflow).not.toBeNull(); + expect(workflow!.name).toBe('default'); + }); + + it('should return null for non-existent workflow', () => { + const workflow = loadWorkflow('does-not-exist'); + expect(workflow).toBeNull(); + }); + + it('should load builtin workflows like simple, research', () => { + const simple = loadWorkflow('simple'); + expect(simple).not.toBeNull(); + expect(simple!.name).toBe('simple'); + + const research = loadWorkflow('research'); + expect(research).not.toBeNull(); + expect(research!.name).toBe('research'); + }); +}); + +describe('listWorkflows (builtin fallback)', () => { + it('should include builtin workflows', () => { + const workflows = listWorkflows(); + expect(workflows).toContain('default'); + expect(workflows).toContain('simple'); + }); + + it('should return sorted list', () => { + const workflows = listWorkflows(); + const sorted = [...workflows].sort(); + expect(workflows).toEqual(sorted); + }); +}); + +describe('loadAllWorkflows (builtin fallback)', () => { + it('should include builtin workflows in the map', () => { + const workflows = loadAllWorkflows(); + expect(workflows.has('default')).toBe(true); + expect(workflows.has('simple')).toBe(true); + }); +}); + +describe('loadAgentPromptFromPath (builtin paths)', () => { + it('should load agent prompt from builtin resources path', () => { + const lang = getLanguage(); + const builtinAgentsDir = getBuiltinAgentsDir(lang); + const agentPath = join(builtinAgentsDir, 'default', 'coder.md'); + + if (existsSync(agentPath)) { + const prompt = loadAgentPromptFromPath(agentPath); + expect(prompt).toBeTruthy(); + expect(typeof prompt).toBe('string'); + } + }); +}); + describe('getCurrentWorkflow', () => { let testDir: string; diff --git a/src/__tests__/initialization.test.ts b/src/__tests__/initialization.test.ts index c8fd87e..499f6ca 100644 --- a/src/__tests__/initialization.test.ts +++ b/src/__tests__/initialization.test.ts @@ -26,8 +26,8 @@ vi.mock('../prompt/index.js', () => ({ // Import after mocks are set up const { needsLanguageSetup } = await import('../config/initialization.js'); -const { getGlobalAgentsDir, getGlobalWorkflowsDir } = await import('../config/paths.js'); -const { copyLanguageResourcesToDir, copyProjectResourcesToDir, getLanguageResourcesDir, getProjectResourcesDir } = await import('../resources/index.js'); +const { getGlobalConfigPath } = await import('../config/paths.js'); +const { copyProjectResourcesToDir, getLanguageResourcesDir, getProjectResourcesDir } = await import('../resources/index.js'); describe('initialization', () => { beforeEach(() => { @@ -43,48 +43,17 @@ describe('initialization', () => { }); describe('needsLanguageSetup', () => { - it('should return true when neither agents nor workflows exist', () => { + it('should return true when config.yaml does not exist', () => { expect(needsLanguageSetup()).toBe(true); }); - it('should return true when only agents exists', () => { - mkdirSync(getGlobalAgentsDir(), { recursive: true }); - expect(needsLanguageSetup()).toBe(true); - }); - - it('should return true when only workflows exists', () => { - mkdirSync(getGlobalWorkflowsDir(), { recursive: true }); - expect(needsLanguageSetup()).toBe(true); - }); - - it('should return false when both agents and workflows exist', () => { - mkdirSync(getGlobalAgentsDir(), { recursive: true }); - mkdirSync(getGlobalWorkflowsDir(), { recursive: true }); + it('should return false when config.yaml exists', () => { + mkdirSync(testTaktDir, { recursive: true }); + writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8'); expect(needsLanguageSetup()).toBe(false); }); }); - describe('copyLanguageResourcesToDir', () => { - it('should throw error when language directory does not exist', () => { - const nonExistentLang = 'xx' as 'en' | 'ja'; - expect(() => copyLanguageResourcesToDir(testTaktDir, nonExistentLang)).toThrow( - /Language resources not found/ - ); - }); - - it('should copy language resources to target directory', () => { - // This test requires actual language resources to exist - const langDir = getLanguageResourcesDir('ja'); - if (existsSync(langDir)) { - mkdirSync(testTaktDir, { recursive: true }); - copyLanguageResourcesToDir(testTaktDir, 'ja'); - - // Verify that agents and workflows directories were created - expect(existsSync(join(testTaktDir, 'agents'))).toBe(true); - expect(existsSync(join(testTaktDir, 'workflows'))).toBe(true); - } - }); - }); }); describe('copyProjectResourcesToDir', () => { diff --git a/src/cli.ts b/src/cli.ts index a73974f..39266c8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -32,6 +32,7 @@ import { switchConfig, addTask, refreshBuiltin, + ejectBuiltin, watchTasks, listTasks, } from './commands/index.js'; @@ -171,6 +172,10 @@ program await refreshBuiltin(); return; + case 'eject': + await ejectBuiltin(args[0]); + return; + case 'watch': await watchTasks(cwd); return; @@ -182,7 +187,7 @@ program default: error(`Unknown command: /${command}`); - info('Available: /run-tasks (/run), /watch, /add-task (/add), /list-tasks (/list), /switch (/sw), /clear, /refresh-builtin, /help, /config'); + info('Available: /run-tasks (/run), /watch, /add-task (/add), /list-tasks (/list), /switch (/sw), /clear, /eject, /help, /config'); process.exit(1); } } diff --git a/src/commands/eject.ts b/src/commands/eject.ts new file mode 100644 index 0000000..c28135a --- /dev/null +++ b/src/commands/eject.ts @@ -0,0 +1,124 @@ +/** + * /eject command implementation + * + * Copies a builtin workflow (and its agents) to ~/.takt/ for user customization. + * Once ejected, the user copy takes priority over the builtin version. + */ + +import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; +import { join, basename, dirname } from 'node:path'; +import { getGlobalWorkflowsDir, getGlobalAgentsDir, getBuiltinWorkflowsDir, getBuiltinAgentsDir } from '../config/paths.js'; +import { getLanguage } from '../config/globalConfig.js'; +import { header, success, info, warn, error } from '../utils/ui.js'; + +/** + * Eject a builtin workflow to user space for customization. + * Copies the workflow YAML and related agent .md files to ~/.takt/. + * Agent paths in the ejected workflow are rewritten from ../agents/ to ~/.takt/agents/. + */ +export async function ejectBuiltin(name?: string): Promise { + header('Eject Builtin'); + + const lang = getLanguage(); + const builtinWorkflowsDir = getBuiltinWorkflowsDir(lang); + + if (!name) { + // List available builtins + listAvailableBuiltins(builtinWorkflowsDir); + return; + } + + const builtinPath = join(builtinWorkflowsDir, `${name}.yaml`); + if (!existsSync(builtinPath)) { + error(`Builtin workflow not found: ${name}`); + info('Run "takt /eject" to see available builtins.'); + return; + } + + const userWorkflowsDir = getGlobalWorkflowsDir(); + const userAgentsDir = getGlobalAgentsDir(); + const builtinAgentsDir = getBuiltinAgentsDir(lang); + + // Copy workflow YAML (rewrite agent paths) + const workflowDest = join(userWorkflowsDir, `${name}.yaml`); + if (existsSync(workflowDest)) { + warn(`User workflow already exists: ${workflowDest}`); + warn('Skipping workflow copy (user version takes priority).'); + } else { + mkdirSync(dirname(workflowDest), { recursive: true }); + const content = readFileSync(builtinPath, 'utf-8'); + // Rewrite relative agent paths to ~/.takt/agents/ + const rewritten = content.replace( + /agent:\s*\.\.\/agents\//g, + 'agent: ~/.takt/agents/', + ); + writeFileSync(workflowDest, rewritten, 'utf-8'); + success(`Ejected workflow: ${workflowDest}`); + } + + // Copy related agent files + const agentPaths = extractAgentRelativePaths(builtinPath); + let copiedAgents = 0; + + for (const relPath of agentPaths) { + const srcPath = join(builtinAgentsDir, relPath); + const destPath = join(userAgentsDir, relPath); + + if (!existsSync(srcPath)) continue; + + if (existsSync(destPath)) { + info(` Agent already exists: ${destPath}`); + continue; + } + + mkdirSync(dirname(destPath), { recursive: true }); + writeFileSync(destPath, readFileSync(srcPath)); + info(` ✓ ${destPath}`); + copiedAgents++; + } + + if (copiedAgents > 0) { + success(`${copiedAgents} agent file(s) ejected.`); + } +} + +/** List available builtin workflows for ejection */ +function listAvailableBuiltins(builtinWorkflowsDir: string): void { + if (!existsSync(builtinWorkflowsDir)) { + warn('No builtin workflows found.'); + return; + } + + info('Available builtin workflows:'); + console.log(); + + for (const entry of readdirSync(builtinWorkflowsDir).sort()) { + if (!entry.endsWith('.yaml') && !entry.endsWith('.yml')) continue; + if (!statSync(join(builtinWorkflowsDir, entry)).isFile()) continue; + + const name = entry.replace(/\.ya?ml$/, ''); + info(` ${name}`); + } + + console.log(); + info('Usage: takt /eject {name}'); +} + +/** + * Extract agent relative paths from a builtin workflow YAML. + * Matches `agent: ../agents/{path}` and returns the {path} portions. + */ +function extractAgentRelativePaths(workflowPath: string): string[] { + const content = readFileSync(workflowPath, 'utf-8'); + const paths = new Set(); + const regex = /agent:\s*\.\.\/agents\/(.+)/g; + + let match: RegExpExecArray | null; + while ((match = regex.exec(content)) !== null) { + if (match[1]) { + paths.add(match[1].trim()); + } + } + + return Array.from(paths); +} diff --git a/src/commands/help.ts b/src/commands/help.ts index a1ed9a8..dba01a6 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -21,7 +21,8 @@ Usage: takt /list-tasks (/list) List task branches (merge/delete) takt /switch Switch workflow interactively takt /clear Clear agent conversation sessions (reset to initial state) - takt /refresh-builtin Overwrite builtin agents/workflows with latest version + takt /eject Copy builtin workflow/agents to ~/.takt/ for customization + takt /eject {name} Eject a specific builtin workflow takt /help Show this help Examples: @@ -34,7 +35,8 @@ Examples: takt /add-task # Interactive task creation takt /clear # Clear sessions, start fresh takt /watch # Watch & auto-execute tasks - takt /refresh-builtin # Update builtin resources + takt /eject # List available builtins + takt /eject default # Eject default workflow for customization takt /list-tasks # List & merge task branches takt /switch takt /run-tasks diff --git a/src/commands/index.ts b/src/commands/index.ts index af84605..b55819c 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -6,6 +6,7 @@ export { executeWorkflow, type WorkflowExecutionResult, type WorkflowExecutionOp export { executeTask, runAllTasks } from './taskExecution.js'; export { addTask } from './addTask.js'; export { refreshBuiltin } from './refreshBuiltin.js'; +export { ejectBuiltin } from './eject.js'; export { watchTasks } from './watchTasks.js'; export { showHelp } from './help.js'; export { withAgentSession } from './session.js'; diff --git a/src/commands/refreshBuiltin.ts b/src/commands/refreshBuiltin.ts index 16be721..6770981 100644 --- a/src/commands/refreshBuiltin.ts +++ b/src/commands/refreshBuiltin.ts @@ -1,43 +1,22 @@ /** - * /refresh-builtin command implementation + * /refresh-builtin command — DEPRECATED * - * Overwrites builtin workflow and agent files in ~/.takt/ with the latest - * embedded resources. Does NOT touch config.yaml or user-added files. + * Builtin resources are now loaded directly from the package bundle. + * Use /eject to copy individual builtins to ~/.takt/ for customization. */ -import { getGlobalConfigDir } from '../config/paths.js'; -import { getLanguage } from '../config/globalConfig.js'; -import { forceRefreshLanguageResources } from '../resources/index.js'; -import { header, success, info, error } from '../utils/ui.js'; -import { createLogger } from '../utils/debug.js'; - -const log = createLogger('refresh-builtin'); +import { warn, info } from '../utils/ui.js'; /** - * Refresh builtin agents and workflows to latest version. + * Show deprecation notice and guide user to /eject. */ export async function refreshBuiltin(): Promise { - const globalDir = getGlobalConfigDir(); - const lang = getLanguage(); - - header('Refresh Builtin Resources'); - info(`Language: ${lang}`); - info(`Target: ${globalDir}`); - - try { - const overwritten = forceRefreshLanguageResources(globalDir, lang); - - log.info('Builtin resources refreshed', { count: overwritten.length, lang }); - - console.log(); - success(`${overwritten.length} files refreshed.`); - - for (const filePath of overwritten) { - info(` ✓ ${filePath}`); - } - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - log.error('Failed to refresh builtin resources', { error: message }); - error(`Failed to refresh: ${message}`); - } + warn('/refresh-builtin is deprecated.'); + console.log(); + info('Builtin workflows and agents are now loaded directly from the package.'); + info('They no longer need to be copied to ~/.takt/.'); + console.log(); + info('To customize a builtin, use:'); + info(' takt /eject List available builtins'); + info(' takt /eject {name} Copy a builtin to ~/.takt/ for editing'); } diff --git a/src/config/agentLoader.ts b/src/config/agentLoader.ts index 70697c9..679f278 100644 --- a/src/config/agentLoader.ts +++ b/src/config/agentLoader.ts @@ -1,7 +1,9 @@ /** * Agent configuration loader * - * Loads agents from ~/.takt/agents/ directory only. + * Loads agents with user → builtin fallback: + * 1. User agents: ~/.takt/agents/*.md + * 2. Builtin agents: resources/global/{lang}/agents/*.md */ import { readFileSync, existsSync, readdirSync } from 'node:fs'; @@ -10,8 +12,22 @@ import type { CustomAgentConfig } from '../models/types.js'; import { getGlobalAgentsDir, getGlobalWorkflowsDir, + getBuiltinAgentsDir, + getBuiltinWorkflowsDir, isPathSafe, } from './paths.js'; +import { getLanguage } from './globalConfig.js'; + +/** Get all allowed base directories for agent prompt files */ +function getAllowedAgentBases(): string[] { + const lang = getLanguage(); + return [ + getGlobalAgentsDir(), + getGlobalWorkflowsDir(), + getBuiltinAgentsDir(lang), + getBuiltinWorkflowsDir(lang), + ]; +} /** Load agents from markdown files in a directory */ export function loadAgentsFromDir(dirPath: string): CustomAgentConfig[] { @@ -61,19 +77,7 @@ export function loadAgentPrompt(agent: CustomAgentConfig): string { } if (agent.promptFile) { - const allowedBases = [ - getGlobalAgentsDir(), - getGlobalWorkflowsDir(), - ]; - - let isValid = false; - for (const base of allowedBases) { - if (isPathSafe(base, agent.promptFile)) { - isValid = true; - break; - } - } - + const isValid = getAllowedAgentBases().some((base) => isPathSafe(base, agent.promptFile!)); if (!isValid) { throw new Error(`Agent prompt file path is not allowed: ${agent.promptFile}`); } @@ -93,19 +97,7 @@ export function loadAgentPrompt(agent: CustomAgentConfig): string { * Used by workflow engine when agentPath is already resolved. */ export function loadAgentPromptFromPath(agentPath: string): string { - const allowedBases = [ - getGlobalAgentsDir(), - getGlobalWorkflowsDir(), - ]; - - let isValid = false; - for (const base of allowedBases) { - if (isPathSafe(base, agentPath)) { - isValid = true; - break; - } - } - + const isValid = getAllowedAgentBases().some((base) => isPathSafe(base, agentPath)); if (!isValid) { throw new Error(`Agent prompt file path is not allowed: ${agentPath}`); } diff --git a/src/config/globalConfig.ts b/src/config/globalConfig.ts index 141f5d6..9782a12 100644 --- a/src/config/globalConfig.ts +++ b/src/config/globalConfig.ts @@ -36,6 +36,7 @@ export function loadGlobalConfig(): GlobalConfig { logFile: parsed.debug.log_file, } : undefined, worktreeDir: parsed.worktree_dir, + disabledBuiltins: parsed.disabled_builtins, }; } @@ -61,9 +62,22 @@ export function saveGlobalConfig(config: GlobalConfig): void { if (config.worktreeDir) { raw.worktree_dir = config.worktreeDir; } + if (config.disabledBuiltins && config.disabledBuiltins.length > 0) { + raw.disabled_builtins = config.disabledBuiltins; + } writeFileSync(configPath, stringifyYaml(raw), 'utf-8'); } +/** Get list of disabled builtin names */ +export function getDisabledBuiltins(): string[] { + try { + const config = loadGlobalConfig(); + return config.disabledBuiltins ?? []; + } catch { + return []; + } +} + /** Get current language setting */ export function getLanguage(): Language { try { diff --git a/src/config/initialization.ts b/src/config/initialization.ts index 132fca6..a4f1f6a 100644 --- a/src/config/initialization.ts +++ b/src/config/initialization.ts @@ -1,37 +1,32 @@ /** * Initialization module for first-time setup * - * Handles language selection and initial resource setup. - * Separated from paths.ts to avoid UI dependencies in utility modules. + * Handles language selection and initial config.yaml creation. + * Builtin agents/workflows are loaded via fallback from resources/ + * and no longer copied to ~/.takt/ on setup. */ -import { existsSync } from 'node:fs'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; import type { Language } from '../models/types.js'; import { DEFAULT_LANGUAGE } from '../constants.js'; import { selectOptionWithDefault } from '../prompt/index.js'; import { getGlobalConfigDir, - getGlobalAgentsDir, - getGlobalWorkflowsDir, + getGlobalConfigPath, getGlobalLogsDir, getProjectConfigDir, ensureDir, } from './paths.js'; -import { - copyGlobalResourcesToDir, - copyLanguageResourcesToDir, - copyProjectResourcesToDir, -} from '../resources/index.js'; +import { copyProjectResourcesToDir, getLanguageResourcesDir } from '../resources/index.js'; import { setLanguage, setProvider } from './globalConfig.js'; /** - * Check if language-specific resources need to be initialized. - * Returns true if agents or workflows directories don't exist. + * Check if initial setup is needed. + * Returns true if config.yaml doesn't exist yet. */ export function needsLanguageSetup(): boolean { - const agentsDir = getGlobalAgentsDir(); - const workflowsDir = getGlobalWorkflowsDir(); - return !existsSync(agentsDir) || !existsSync(workflowsDir); + return !existsSync(getGlobalConfigPath()); } /** @@ -83,29 +78,32 @@ export async function promptProviderSelection(): Promise<'claude' | 'codex'> { /** * Initialize global takt directory structure with language selection. - * If agents/workflows don't exist, prompts user for language preference. + * On first run, creates config.yaml from language template. + * Agents/workflows are NOT copied — they are loaded via builtin fallback. */ export async function initGlobalDirs(): Promise { ensureDir(getGlobalConfigDir()); ensureDir(getGlobalLogsDir()); - // Check if we need to set up language-specific resources - const needsSetup = needsLanguageSetup(); - - if (needsSetup) { - // Ask user for language preference + if (needsLanguageSetup()) { const lang = await promptLanguageSelection(); const provider = await promptProviderSelection(); - // Copy language-specific resources (agents, workflows, config.yaml) - copyLanguageResourcesToDir(getGlobalConfigDir(), lang); + // Copy only config.yaml from language resources + copyLanguageConfigYaml(lang); - // Explicitly save the selected language (handles case where config.yaml existed) setLanguage(lang); setProvider(provider); - } else { - // Just copy base global resources (won't overwrite existing) - copyGlobalResourcesToDir(getGlobalConfigDir()); + } +} + +/** Copy config.yaml from language resources to ~/.takt/ (if not already present) */ +function copyLanguageConfigYaml(lang: Language): void { + const langDir = getLanguageResourcesDir(lang); + const srcPath = join(langDir, 'config.yaml'); + const destPath = getGlobalConfigPath(); + if (existsSync(srcPath) && !existsSync(destPath)) { + writeFileSync(destPath, readFileSync(srcPath)); } } diff --git a/src/config/paths.ts b/src/config/paths.ts index f2830dc..558595c 100644 --- a/src/config/paths.ts +++ b/src/config/paths.ts @@ -8,6 +8,8 @@ import { homedir } from 'node:os'; import { join, resolve } from 'node:path'; import { existsSync, mkdirSync } from 'node:fs'; +import type { Language } from '../models/types.js'; +import { getLanguageResourcesDir } from '../resources/index.js'; /** Get takt global config directory (~/.takt) */ export function getGlobalConfigDir(): string { @@ -34,6 +36,16 @@ export function getGlobalConfigPath(): string { return join(getGlobalConfigDir(), 'config.yaml'); } +/** Get builtin workflows directory (resources/global/{lang}/workflows) */ +export function getBuiltinWorkflowsDir(lang: Language): string { + return join(getLanguageResourcesDir(lang), 'workflows'); +} + +/** Get builtin agents directory (resources/global/{lang}/agents) */ +export function getBuiltinAgentsDir(lang: Language): string { + return join(getLanguageResourcesDir(lang), 'agents'); +} + /** Get project takt config directory (.takt in project) */ export function getProjectConfigDir(projectDir: string): string { return join(resolve(projectDir), '.takt'); diff --git a/src/config/workflowLoader.ts b/src/config/workflowLoader.ts index a7bc5c1..c18faf6 100644 --- a/src/config/workflowLoader.ts +++ b/src/config/workflowLoader.ts @@ -1,7 +1,9 @@ /** * Workflow configuration loader * - * Loads workflows from ~/.takt/workflows/ directory only. + * Loads workflows with user → builtin fallback: + * 1. User workflows: ~/.takt/workflows/{name}.yaml + * 2. Builtin workflows: resources/global/{lang}/workflows/{name}.yaml */ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'; @@ -9,12 +11,20 @@ import { join, dirname, basename } from 'node:path'; import { parse as parseYaml } from 'yaml'; import { WorkflowConfigRawSchema } from '../models/schemas.js'; import type { WorkflowConfig, WorkflowStep, WorkflowRule, ReportConfig, ReportObjectConfig } from '../models/types.js'; -import { getGlobalWorkflowsDir } from './paths.js'; +import { getGlobalWorkflowsDir, getBuiltinWorkflowsDir } from './paths.js'; +import { getLanguage, getDisabledBuiltins } from './globalConfig.js'; /** Get builtin workflow by name */ export function getBuiltinWorkflow(name: string): WorkflowConfig | null { - // No built-in workflows - all workflows must be defined in ~/.takt/workflows/ - void name; + const lang = getLanguage(); + const disabled = getDisabledBuiltins(); + if (disabled.includes(name)) return null; + + const builtinDir = getBuiltinWorkflowsDir(lang); + const yamlPath = join(builtinDir, `${name}.yaml`); + if (existsSync(yamlPath)) { + return loadWorkflowFromFile(yamlPath); + } return null; } @@ -231,24 +241,47 @@ export function loadWorkflowFromFile(filePath: string): WorkflowConfig { } /** - * Load workflow by name from global directory. - * Looks for ~/.takt/workflows/{name}.yaml + * Load workflow by name. + * Priority: user (~/.takt/workflows/) → builtin (resources/global/{lang}/workflows/) */ export function loadWorkflow(name: string): WorkflowConfig | null { + // 1. User workflow const globalWorkflowsDir = getGlobalWorkflowsDir(); const workflowYamlPath = join(globalWorkflowsDir, `${name}.yaml`); - if (existsSync(workflowYamlPath)) { return loadWorkflowFromFile(workflowYamlPath); } - return null; + + // 2. Builtin fallback + return getBuiltinWorkflow(name); } /** Load all workflows with descriptions (for switch command) */ export function loadAllWorkflows(): Map { const workflows = new Map(); + const disabled = getDisabledBuiltins(); - // Global workflows (~/.takt/workflows/{name}.yaml) + // 1. Builtin workflows (lower priority — will be overridden by user) + const lang = getLanguage(); + const builtinDir = getBuiltinWorkflowsDir(lang); + if (existsSync(builtinDir)) { + for (const entry of readdirSync(builtinDir)) { + if (!entry.endsWith('.yaml') && !entry.endsWith('.yml')) continue; + + const entryPath = join(builtinDir, entry); + if (statSync(entryPath).isFile()) { + const workflowName = entry.replace(/\.ya?ml$/, ''); + if (disabled.includes(workflowName)) continue; + try { + workflows.set(workflowName, loadWorkflowFromFile(entryPath)); + } catch { + // Skip invalid workflows + } + } + } + } + + // 2. User workflows (higher priority — overrides builtins) const globalWorkflowsDir = getGlobalWorkflowsDir(); if (existsSync(globalWorkflowsDir)) { for (const entry of readdirSync(globalWorkflowsDir)) { @@ -270,22 +303,34 @@ export function loadAllWorkflows(): Map { return workflows; } -/** List available workflows from global directory (~/.takt/workflows/) */ +/** List available workflows (user + builtin, excluding disabled) */ export function listWorkflows(): string[] { const workflows = new Set(); + const disabled = getDisabledBuiltins(); + // 1. Builtin workflows + const lang = getLanguage(); + const builtinDir = getBuiltinWorkflowsDir(lang); + scanWorkflowDir(builtinDir, workflows, disabled); + + // 2. User workflows const globalWorkflowsDir = getGlobalWorkflowsDir(); - if (existsSync(globalWorkflowsDir)) { - for (const entry of readdirSync(globalWorkflowsDir)) { - if (!entry.endsWith('.yaml') && !entry.endsWith('.yml')) continue; - - const entryPath = join(globalWorkflowsDir, entry); - if (statSync(entryPath).isFile()) { - const workflowName = entry.replace(/\.ya?ml$/, ''); - workflows.add(workflowName); - } - } - } + scanWorkflowDir(globalWorkflowsDir, workflows); return Array.from(workflows).sort(); } + +/** Scan a directory for .yaml/.yml files and add names to the set */ +function scanWorkflowDir(dir: string, target: Set, disabled?: string[]): void { + if (!existsSync(dir)) return; + for (const entry of readdirSync(dir)) { + if (!entry.endsWith('.yaml') && !entry.endsWith('.yml')) continue; + + const entryPath = join(dir, entry); + if (statSync(entryPath).isFile()) { + const workflowName = entry.replace(/\.ya?ml$/, ''); + if (disabled?.includes(workflowName)) continue; + target.add(workflowName); + } + } +} diff --git a/src/models/schemas.ts b/src/models/schemas.ts index 502adc5..b117d2f 100644 --- a/src/models/schemas.ts +++ b/src/models/schemas.ts @@ -170,6 +170,8 @@ export const GlobalConfigSchema = z.object({ debug: DebugConfigSchema.optional(), /** Directory for shared clones (worktree_dir in config). If empty, uses ../{clone-name} relative to project */ worktree_dir: z.string().optional(), + /** List of builtin workflow/agent names to exclude from fallback loading */ + disabled_builtins: z.array(z.string()).optional().default([]), }); /** Project config schema */ diff --git a/src/models/types.ts b/src/models/types.ts index fc0bd83..acfb23b 100644 --- a/src/models/types.ts +++ b/src/models/types.ts @@ -189,6 +189,8 @@ export interface GlobalConfig { debug?: DebugConfig; /** Directory for shared clones (worktree_dir in config). If empty, uses ../{clone-name} relative to project */ worktreeDir?: string; + /** List of builtin workflow/agent names to exclude from fallback loading */ + disabledBuiltins?: string[]; } /** Project-level configuration */ diff --git a/src/resources/index.ts b/src/resources/index.ts index 2ab325e..6ef6281 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -3,9 +3,9 @@ * * Contains default workflow definitions and resource paths. * Resources are organized into: - * - resources/global/ - Files to copy to ~/.takt - * - resources/global/en/ - English resources - * - resources/global/ja/ - Japanese resources + * - resources/global/{lang}/workflows/ - Builtin workflows (loaded via fallback) + * - resources/global/{lang}/agents/ - Builtin agents (loaded via fallback) + * - resources/project/ - Project-level template files (.gitignore) */ import { readFileSync, readdirSync, existsSync, statSync, mkdirSync, writeFileSync } from 'fs'; @@ -44,20 +44,6 @@ export function getLanguageResourcesDir(lang: Language): string { return join(getGlobalResourcesDir(), lang); } -/** - * Copy global resources directory to ~/.takt. - * Only copies files that don't exist in target. - * Skips language-specific directories (en/, ja/) which are handled by copyLanguageResourcesToDir. - */ -export function copyGlobalResourcesToDir(targetDir: string): void { - const resourcesDir = getGlobalResourcesDir(); - if (!existsSync(resourcesDir)) { - return; - } - // Skip language directories (they are handled by copyLanguageResourcesToDir) - copyDirRecursive(resourcesDir, targetDir, { skipDirs: ['en', 'ja'] }); -} - /** * Copy project resources directory to .takt in project. * Only copies files that don't exist in target (e.g., .gitignore). @@ -72,72 +58,6 @@ export function copyProjectResourcesToDir(targetDir: string): void { }); } -/** - * Copy language-specific resources (agents and workflows) to ~/.takt. - * Copies from resources/global/{lang}/agents to ~/.takt/agents - * and resources/global/{lang}/workflows to ~/.takt/workflows. - * Also copies config.yaml from language directory. - * @throws Error if language directory doesn't exist - */ -export function copyLanguageResourcesToDir(targetDir: string, lang: Language): void { - const langDir = getLanguageResourcesDir(lang); - if (!existsSync(langDir)) { - throw new Error(`Language resources not found: ${langDir}`); - } - - // Copy agents directory - const langAgentsDir = join(langDir, 'agents'); - const targetAgentsDir = join(targetDir, 'agents'); - if (existsSync(langAgentsDir)) { - copyDirRecursive(langAgentsDir, targetAgentsDir); - } - - // Copy workflows directory - const langWorkflowsDir = join(langDir, 'workflows'); - const targetWorkflowsDir = join(targetDir, 'workflows'); - if (existsSync(langWorkflowsDir)) { - copyDirRecursive(langWorkflowsDir, targetWorkflowsDir); - } - - // Copy config.yaml if exists - const langConfigPath = join(langDir, 'config.yaml'); - const targetConfigPath = join(targetDir, 'config.yaml'); - if (existsSync(langConfigPath) && !existsSync(targetConfigPath)) { - const content = readFileSync(langConfigPath); - writeFileSync(targetConfigPath, content); - } -} - -/** - * Force-refresh language-specific resources (agents and workflows) to ~/.takt. - * Overwrites existing builtin files. Does NOT touch config.yaml. - */ -export function forceRefreshLanguageResources(targetDir: string, lang: Language): string[] { - const langDir = getLanguageResourcesDir(lang); - if (!existsSync(langDir)) { - throw new Error(`Language resources not found: ${langDir}`); - } - - const copiedFiles: string[] = []; - const forceOptions = { overwrite: true, copiedFiles }; - - // Overwrite agents directory - const langAgentsDir = join(langDir, 'agents'); - const targetAgentsDir = join(targetDir, 'agents'); - if (existsSync(langAgentsDir)) { - copyDirRecursive(langAgentsDir, targetAgentsDir, forceOptions); - } - - // Overwrite workflows directory - const langWorkflowsDir = join(langDir, 'workflows'); - const targetWorkflowsDir = join(targetDir, 'workflows'); - if (existsSync(langWorkflowsDir)) { - copyDirRecursive(langWorkflowsDir, targetWorkflowsDir, forceOptions); - } - - return copiedFiles; -} - /** Files to skip during resource copy (OS-generated files) */ const SKIP_FILES = ['.DS_Store', 'Thumbs.db'];