@relipa/ai-flow-kit 0.1.1 → 0.1.2

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.
Files changed (253) hide show
  1. package/README.md +0 -0
  2. package/bin/aiflow.js +55 -2
  3. package/bin/ak.js +0 -0
  4. package/custom/mcp-presets/README.md +204 -237
  5. package/custom/mcp-presets/backlog.json +0 -0
  6. package/custom/mcp-presets/figma-desktop.json +0 -0
  7. package/custom/mcp-presets/figma.json +0 -0
  8. package/custom/mcp-presets/gitnexus.json +0 -0
  9. package/custom/mcp-presets/google-sheets.json +0 -0
  10. package/custom/mcp-presets/jira.json +0 -0
  11. package/custom/prompts/bug-fix.md +0 -0
  12. package/custom/prompts/feature.md +0 -0
  13. package/custom/prompts/investigation.md +0 -0
  14. package/custom/rules/code-style.md +0 -0
  15. package/custom/rules/java/code-style.md +0 -0
  16. package/custom/rules/java/naming.md +0 -0
  17. package/custom/rules/java/review-checklist.md +0 -0
  18. package/custom/rules/java/spring-boot-examples.md +0 -0
  19. package/custom/rules/javascript/code-style.md +0 -0
  20. package/custom/rules/javascript/naming.md +0 -0
  21. package/custom/rules/ml-conventions.md +88 -0
  22. package/custom/rules/ml-review-checklist.md +14 -0
  23. package/custom/rules/naming.md +0 -0
  24. package/custom/rules/php/code-style.md +0 -0
  25. package/custom/rules/php/naming.md +0 -0
  26. package/custom/rules/project-conventions.md +0 -0
  27. package/custom/rules/review-checklist.md +0 -0
  28. package/custom/skills/deploy-model/SKILL.md +90 -0
  29. package/custom/skills/design-experiment/SKILL.md +88 -0
  30. package/custom/skills/evaluate-model/SKILL.md +93 -0
  31. package/custom/skills/explore-data/SKILL.md +87 -0
  32. package/custom/skills/figma-to-component/SKILL.md +0 -0
  33. package/custom/skills/frame-ml-problem/SKILL.md +88 -0
  34. package/custom/skills/generate-spec/SKILL.md +0 -0
  35. package/custom/skills/impact-analysis/SKILL.md +0 -0
  36. package/custom/skills/improve-algorithm/SKILL.md +74 -0
  37. package/custom/skills/investigate-bug/SKILL.md +0 -0
  38. package/custom/skills/read-study-requirement/SKILL.md +0 -0
  39. package/custom/skills/report-customer/SKILL.md +0 -0
  40. package/custom/skills/review-plan/SKILL.md +0 -0
  41. package/custom/skills/train-model/SKILL.md +78 -0
  42. package/custom/templates/laravel.md +0 -0
  43. package/custom/templates/nestjs.md +0 -0
  44. package/custom/templates/nextjs.md +0 -0
  45. package/custom/templates/nodejs-express.md +0 -0
  46. package/custom/templates/php-plain.md +0 -0
  47. package/custom/templates/php.md +0 -0
  48. package/custom/templates/python-django.md +0 -0
  49. package/custom/templates/python-fastapi.md +0 -0
  50. package/custom/templates/python-ml.md +273 -0
  51. package/custom/templates/python.md +0 -0
  52. package/custom/templates/reactjs.md +0 -0
  53. package/custom/templates/shared/gate-workflow.md +0 -0
  54. package/custom/templates/shared/ml-gate-workflow.md +103 -0
  55. package/custom/templates/spring-boot.md +0 -0
  56. package/custom/templates/tools/claude.md +0 -0
  57. package/custom/templates/tools/copilot.md +0 -0
  58. package/custom/templates/tools/cursor.md +0 -0
  59. package/custom/templates/tools/gemini.md +0 -0
  60. package/custom/templates/tools/generic.md +0 -0
  61. package/custom/templates/vue-nuxt.md +0 -0
  62. package/docs/common/AIFLOW.md +512 -512
  63. package/docs/common/CHANGELOG.md +32 -0
  64. package/docs/common/QUICK_START.md +0 -0
  65. package/docs/common/ai-integration.md +0 -0
  66. package/docs/common/cli-reference.md +0 -0
  67. package/docs/common/configuration.md +0 -0
  68. package/docs/common/getting-started.md +0 -0
  69. package/docs/common/troubleshooting.md +0 -0
  70. package/docs/common/workflows/bug-fix.md +0 -0
  71. package/docs/common/workflows/feature.md +0 -0
  72. package/docs/common/workflows/figma.md +0 -0
  73. package/docs/common/workflows/impact-analysis.md +0 -0
  74. package/docs/common/workflows/investigation.md +0 -0
  75. package/docs/common/workflows/refactor.md +0 -0
  76. package/docs/project/ARCHITECTURE.md +0 -0
  77. package/index.js +0 -0
  78. package/package.json +1 -1
  79. package/scripts/checkpoint.js +0 -0
  80. package/scripts/config.js +0 -0
  81. package/scripts/context.js +0 -0
  82. package/scripts/create-score-excel.js +0 -0
  83. package/scripts/detect.js +239 -239
  84. package/scripts/doctor.js +192 -218
  85. package/scripts/gitnexus-worker.js +0 -0
  86. package/scripts/guide.js +0 -0
  87. package/scripts/hooks/block-git-write.js +0 -0
  88. package/scripts/hooks/session-start.js +0 -0
  89. package/scripts/hooks/session-stop.js +0 -0
  90. package/scripts/init.js +1112 -1009
  91. package/scripts/link-resolver.js +0 -0
  92. package/scripts/memory.js +0 -0
  93. package/scripts/prompt.js +538 -538
  94. package/scripts/remove.js +0 -0
  95. package/scripts/task.js +546 -546
  96. package/scripts/telemetry/cli.js +0 -0
  97. package/scripts/telemetry/config.js +0 -0
  98. package/scripts/telemetry/crypto.js +0 -0
  99. package/scripts/telemetry/flush.js +0 -0
  100. package/scripts/telemetry/record.js +0 -0
  101. package/scripts/update.js +10 -7
  102. package/scripts/use.js +33 -12
  103. package/scripts/validate.js +0 -0
  104. package/upstream/.claude-plugin/marketplace.json +0 -0
  105. package/upstream/.claude-plugin/plugin.json +0 -0
  106. package/upstream/.codex/INSTALL.md +0 -0
  107. package/upstream/.cursor-plugin/plugin.json +0 -0
  108. package/upstream/.gitattributes +0 -0
  109. package/upstream/.github/FUNDING.yml +0 -0
  110. package/upstream/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  111. package/upstream/.github/ISSUE_TEMPLATE/config.yml +0 -0
  112. package/upstream/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  113. package/upstream/.github/ISSUE_TEMPLATE/platform_support.md +0 -0
  114. package/upstream/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  115. package/upstream/.opencode/INSTALL.md +0 -0
  116. package/upstream/.opencode/plugins/superpowers.js +0 -0
  117. package/upstream/.version-bump.json +0 -0
  118. package/upstream/AGENTS.md +0 -0
  119. package/upstream/CHANGELOG.md +0 -0
  120. package/upstream/CLAUDE.md +0 -0
  121. package/upstream/CODE_OF_CONDUCT.md +0 -0
  122. package/upstream/GEMINI.md +0 -0
  123. package/upstream/LICENSE +0 -0
  124. package/upstream/README.md +0 -0
  125. package/upstream/RELEASE-NOTES.md +0 -0
  126. package/upstream/agents/code-reviewer.md +0 -0
  127. package/upstream/commands/brainstorm.md +0 -0
  128. package/upstream/commands/execute-plan.md +0 -0
  129. package/upstream/commands/write-plan.md +0 -0
  130. package/upstream/docs/README.codex.md +0 -0
  131. package/upstream/docs/README.opencode.md +0 -0
  132. package/upstream/docs/plans/2025-11-22-opencode-support-design.md +0 -0
  133. package/upstream/docs/plans/2025-11-22-opencode-support-implementation.md +0 -0
  134. package/upstream/docs/plans/2025-11-28-skills-improvements-from-user-feedback.md +0 -0
  135. package/upstream/docs/plans/2026-01-17-visual-brainstorming.md +0 -0
  136. package/upstream/docs/superpowers/plans/2026-01-22-document-review-system.md +0 -0
  137. package/upstream/docs/superpowers/plans/2026-02-19-visual-brainstorming-refactor.md +0 -0
  138. package/upstream/docs/superpowers/plans/2026-03-11-zero-dep-brainstorm-server.md +0 -0
  139. package/upstream/docs/superpowers/plans/2026-03-23-codex-app-compatibility.md +0 -0
  140. package/upstream/docs/superpowers/specs/2026-01-22-document-review-system-design.md +0 -0
  141. package/upstream/docs/superpowers/specs/2026-02-19-visual-brainstorming-refactor-design.md +0 -0
  142. package/upstream/docs/superpowers/specs/2026-03-11-zero-dep-brainstorm-server-design.md +0 -0
  143. package/upstream/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md +0 -0
  144. package/upstream/docs/testing.md +0 -0
  145. package/upstream/docs/windows/polyglot-hooks.md +0 -0
  146. package/upstream/gemini-extension.json +0 -0
  147. package/upstream/hooks/hooks-cursor.json +0 -0
  148. package/upstream/hooks/hooks.json +0 -0
  149. package/upstream/hooks/run-hook.cmd +0 -0
  150. package/upstream/hooks/session-start +0 -0
  151. package/upstream/package.json +0 -0
  152. package/upstream/scripts/bump-version.sh +0 -0
  153. package/upstream/skills/brainstorming/SKILL.md +0 -0
  154. package/upstream/skills/brainstorming/scripts/frame-template.html +0 -0
  155. package/upstream/skills/brainstorming/scripts/helper.js +0 -0
  156. package/upstream/skills/brainstorming/scripts/server.cjs +0 -0
  157. package/upstream/skills/brainstorming/scripts/start-server.sh +0 -0
  158. package/upstream/skills/brainstorming/scripts/stop-server.sh +0 -0
  159. package/upstream/skills/brainstorming/spec-document-reviewer-prompt.md +0 -0
  160. package/upstream/skills/brainstorming/visual-companion.md +0 -0
  161. package/upstream/skills/dispatching-parallel-agents/SKILL.md +0 -0
  162. package/upstream/skills/executing-plans/SKILL.md +0 -0
  163. package/upstream/skills/finishing-a-development-branch/SKILL.md +0 -0
  164. package/upstream/skills/receiving-code-review/SKILL.md +0 -0
  165. package/upstream/skills/requesting-code-review/SKILL.md +0 -0
  166. package/upstream/skills/requesting-code-review/code-reviewer.md +0 -0
  167. package/upstream/skills/subagent-driven-development/SKILL.md +0 -0
  168. package/upstream/skills/subagent-driven-development/code-quality-reviewer-prompt.md +0 -0
  169. package/upstream/skills/subagent-driven-development/implementer-prompt.md +0 -0
  170. package/upstream/skills/subagent-driven-development/spec-reviewer-prompt.md +0 -0
  171. package/upstream/skills/systematic-debugging/CREATION-LOG.md +0 -0
  172. package/upstream/skills/systematic-debugging/SKILL.md +0 -0
  173. package/upstream/skills/systematic-debugging/condition-based-waiting-example.ts +0 -0
  174. package/upstream/skills/systematic-debugging/condition-based-waiting.md +0 -0
  175. package/upstream/skills/systematic-debugging/defense-in-depth.md +0 -0
  176. package/upstream/skills/systematic-debugging/find-polluter.sh +0 -0
  177. package/upstream/skills/systematic-debugging/root-cause-tracing.md +0 -0
  178. package/upstream/skills/systematic-debugging/test-academic.md +0 -0
  179. package/upstream/skills/systematic-debugging/test-pressure-1.md +0 -0
  180. package/upstream/skills/systematic-debugging/test-pressure-2.md +0 -0
  181. package/upstream/skills/systematic-debugging/test-pressure-3.md +0 -0
  182. package/upstream/skills/tdd-lean/SKILL.md +0 -0
  183. package/upstream/skills/test-driven-development/SKILL.md +0 -0
  184. package/upstream/skills/test-driven-development/testing-anti-patterns.md +0 -0
  185. package/upstream/skills/using-git-worktrees/SKILL.md +0 -0
  186. package/upstream/skills/using-superpowers/SKILL.md +0 -0
  187. package/upstream/skills/using-superpowers/references/codex-tools.md +0 -0
  188. package/upstream/skills/using-superpowers/references/copilot-tools.md +0 -0
  189. package/upstream/skills/using-superpowers/references/gemini-tools.md +0 -0
  190. package/upstream/skills/verification-before-completion/SKILL.md +0 -0
  191. package/upstream/skills/writing-plans/SKILL.md +0 -0
  192. package/upstream/skills/writing-plans/plan-document-reviewer-prompt.md +0 -0
  193. package/upstream/skills/writing-skills/SKILL.md +0 -0
  194. package/upstream/skills/writing-skills/anthropic-best-practices.md +0 -0
  195. package/upstream/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +0 -0
  196. package/upstream/skills/writing-skills/graphviz-conventions.dot +0 -0
  197. package/upstream/skills/writing-skills/persuasion-principles.md +0 -0
  198. package/upstream/skills/writing-skills/render-graphs.js +0 -0
  199. package/upstream/skills/writing-skills/testing-skills-with-subagents.md +0 -0
  200. package/upstream/tests/brainstorm-server/package-lock.json +0 -0
  201. package/upstream/tests/brainstorm-server/package.json +0 -0
  202. package/upstream/tests/brainstorm-server/server.test.js +0 -0
  203. package/upstream/tests/brainstorm-server/windows-lifecycle.test.sh +0 -0
  204. package/upstream/tests/brainstorm-server/ws-protocol.test.js +0 -0
  205. package/upstream/tests/claude-code/README.md +0 -0
  206. package/upstream/tests/claude-code/analyze-token-usage.py +0 -0
  207. package/upstream/tests/claude-code/run-skill-tests.sh +0 -0
  208. package/upstream/tests/claude-code/test-document-review-system.sh +0 -0
  209. package/upstream/tests/claude-code/test-helpers.sh +0 -0
  210. package/upstream/tests/claude-code/test-subagent-driven-development-integration.sh +0 -0
  211. package/upstream/tests/claude-code/test-subagent-driven-development.sh +0 -0
  212. package/upstream/tests/explicit-skill-requests/prompts/action-oriented.txt +0 -0
  213. package/upstream/tests/explicit-skill-requests/prompts/after-planning-flow.txt +0 -0
  214. package/upstream/tests/explicit-skill-requests/prompts/claude-suggested-it.txt +0 -0
  215. package/upstream/tests/explicit-skill-requests/prompts/i-know-what-sdd-means.txt +0 -0
  216. package/upstream/tests/explicit-skill-requests/prompts/mid-conversation-execute-plan.txt +0 -0
  217. package/upstream/tests/explicit-skill-requests/prompts/please-use-brainstorming.txt +0 -0
  218. package/upstream/tests/explicit-skill-requests/prompts/skip-formalities.txt +0 -0
  219. package/upstream/tests/explicit-skill-requests/prompts/subagent-driven-development-please.txt +0 -0
  220. package/upstream/tests/explicit-skill-requests/prompts/use-systematic-debugging.txt +0 -0
  221. package/upstream/tests/explicit-skill-requests/run-all.sh +0 -0
  222. package/upstream/tests/explicit-skill-requests/run-claude-describes-sdd.sh +0 -0
  223. package/upstream/tests/explicit-skill-requests/run-extended-multiturn-test.sh +0 -0
  224. package/upstream/tests/explicit-skill-requests/run-haiku-test.sh +0 -0
  225. package/upstream/tests/explicit-skill-requests/run-multiturn-test.sh +0 -0
  226. package/upstream/tests/explicit-skill-requests/run-test.sh +0 -0
  227. package/upstream/tests/opencode/run-tests.sh +0 -0
  228. package/upstream/tests/opencode/setup.sh +0 -0
  229. package/upstream/tests/opencode/test-plugin-loading.sh +0 -0
  230. package/upstream/tests/opencode/test-priority.sh +0 -0
  231. package/upstream/tests/opencode/test-tools.sh +0 -0
  232. package/upstream/tests/skill-triggering/prompts/dispatching-parallel-agents.txt +0 -0
  233. package/upstream/tests/skill-triggering/prompts/executing-plans.txt +0 -0
  234. package/upstream/tests/skill-triggering/prompts/requesting-code-review.txt +0 -0
  235. package/upstream/tests/skill-triggering/prompts/systematic-debugging.txt +0 -0
  236. package/upstream/tests/skill-triggering/prompts/test-driven-development.txt +0 -0
  237. package/upstream/tests/skill-triggering/prompts/writing-plans.txt +0 -0
  238. package/upstream/tests/skill-triggering/run-all.sh +0 -0
  239. package/upstream/tests/skill-triggering/run-test.sh +0 -0
  240. package/upstream/tests/subagent-driven-dev/go-fractals/design.md +0 -0
  241. package/upstream/tests/subagent-driven-dev/go-fractals/plan.md +0 -0
  242. package/upstream/tests/subagent-driven-dev/go-fractals/scaffold.sh +0 -0
  243. package/upstream/tests/subagent-driven-dev/run-test.sh +0 -0
  244. package/upstream/tests/subagent-driven-dev/svelte-todo/design.md +0 -0
  245. package/upstream/tests/subagent-driven-dev/svelte-todo/plan.md +0 -0
  246. package/upstream/tests/subagent-driven-dev/svelte-todo/scaffold.sh +0 -0
  247. package/custom/mcp-presets/playwright.json +0 -11
  248. package/custom/rules/test-patterns.md +0 -105
  249. package/custom/skills/automation-testing/SKILL.md +0 -168
  250. package/custom/skills/automation-testing/templates/BasePage.ts +0 -30
  251. package/custom/skills/automation-testing/templates/playwright.config.ts +0 -21
  252. package/custom/skills/automation-testing/templates/repos.json +0 -8
  253. package/scripts/score_members.js +0 -320
package/scripts/task.js CHANGED
@@ -1,546 +1,546 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
- const chalk = require('chalk');
4
- const { confirm } = require('@inquirer/prompts');
5
-
6
- const PROJECT_DIR = process.cwd();
7
- const AIFLOW_DIR = path.join(PROJECT_DIR, '.aiflow');
8
- const CONTEXT_DIR = path.join(AIFLOW_DIR, 'context');
9
- const CURRENT_FILE = path.join(CONTEXT_DIR, 'current.json');
10
- const TASKS_DIR = path.join(AIFLOW_DIR, 'tasks');
11
-
12
- // ──────────────────────────────────────────────────────────────
13
- // Entry point
14
- // ──────────────────────────────────────────────────────────────
15
-
16
- module.exports = async function task(action, options = {}) {
17
- try {
18
- if (action === 'list') return await listTasks();
19
- if (action === 'status') return await showStatus();
20
- if (action === 'pause') return await pauseTask(options.note);
21
- if (action === 'switch' && options.taskId) return await switchTask(options.taskId);
22
- if (action === 'resume' && options.taskId) return await resumeTask(options.taskId);
23
- if (action === 'reset' && options.taskId) return await resetTask(options.taskId);
24
- if (action === 'remove' && options.taskId) return await removeTask(options.taskId);
25
- if (action === 'next') return await nextGate(options.taskId);
26
-
27
- console.log(chalk.yellow('Usage:'));
28
- console.log(chalk.gray(' aiflow task status — show active task and pending list'));
29
- console.log(chalk.gray(' aiflow task list — list all saved tasks'));
30
- console.log(chalk.gray(' aiflow task pause — pause current task'));
31
- console.log(chalk.gray(' aiflow task pause --note "..." — pause with a note'));
32
- console.log(chalk.gray(' aiflow task switch <ticket-id> — pause current + switch to another task'));
33
- console.log(chalk.gray(' aiflow task resume <ticket-id> — resume a paused task'));
34
- console.log(chalk.gray(' aiflow task reset <ticket-id> — reset task to Gate 1 (keeps context)'));
35
- console.log(chalk.gray(' aiflow task remove <ticket-id> — permanently delete all task data'));
36
- console.log(chalk.gray(' aiflow task next — approve current gate + prep next session'));
37
- console.log(chalk.gray(' aiflow task next --ticket <id> — same, specify ticket explicitly'));
38
- } catch (err) {
39
- console.error(chalk.red(`Error: ${err.message}`));
40
- }
41
- };
42
-
43
- // ──────────────────────────────────────────────────────────────
44
- // Commands
45
- // ──────────────────────────────────────────────────────────────
46
-
47
- async function showStatus() {
48
- const tasks = await loadAllTaskStates();
49
- const active = tasks.filter(t => t.status === 'active');
50
- const pending = tasks.filter(t => t.status === 'pending');
51
-
52
- console.log(chalk.cyan('\nTask Status\n'));
53
-
54
- if (active.length === 0) {
55
- console.log(chalk.gray(' No active task. Run `aiflow use <ticket>` to start one.'));
56
- } else {
57
- const t = active[0];
58
- console.log(` ${chalk.green('● Active')} ${chalk.white(t.taskId)} ${chalk.gray('Gate ' + t.currentGate)} ${chalk.gray(t.title.substring(0, 55))}`);
59
- }
60
-
61
- if (pending.length > 0) {
62
- console.log(chalk.yellow(`\n Pending (${pending.length}):`));
63
- for (const t of pending) {
64
- const paused = t.pausedAt ? chalk.gray(` paused ${formatRelative(t.pausedAt)}`) : '';
65
- const note = t.notes ? chalk.gray(` — ${t.notes.substring(0, 40)}`) : '';
66
- console.log(` ${chalk.gray('○ Pending')} ${chalk.white(t.taskId.padEnd(14))} Gate ${t.currentGate} ${chalk.gray(t.title.substring(0, 40))}${paused}${note}`);
67
- }
68
- console.log(chalk.gray('\n Resume with: aiflow task resume <ticket-id>'));
69
- }
70
- console.log();
71
- }
72
-
73
- async function listTasks() {
74
- const tasks = await loadAllTaskStates();
75
- if (tasks.length === 0) {
76
- console.log(chalk.gray('No saved tasks.'));
77
- return;
78
- }
79
- console.log(chalk.cyan('\nAll Tasks:\n'));
80
- for (const t of tasks) {
81
- const icon = t.status === 'active' ? chalk.green('●') : chalk.gray('○');
82
- const label = t.status === 'active' ? chalk.green('active ') : chalk.yellow('pending');
83
- console.log(` ${icon} ${label} ${chalk.white(t.taskId.padEnd(16))} Gate ${t.currentGate} ${chalk.gray(t.title.substring(0, 50))}`);
84
- }
85
- console.log();
86
- }
87
-
88
- async function pauseTask(note = '') {
89
- if (!(await fs.pathExists(CURRENT_FILE))) {
90
- console.log(chalk.yellow('No active task to pause.'));
91
- return;
92
- }
93
-
94
- const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
95
- if (!ctx || !ctx.taskId) {
96
- console.log(chalk.yellow('Current context has no taskId — cannot pause.'));
97
- return;
98
- }
99
-
100
- const taskId = ctx.taskId;
101
- const taskDir = path.join(TASKS_DIR, taskId);
102
- await fs.ensureDir(taskDir);
103
-
104
- // Save context snapshot
105
- await fs.writeJson(path.join(taskDir, 'context.json'), ctx, { spaces: 2 });
106
-
107
- // Detect gate from plan/ artifacts
108
- const currentGate = await detectCurrentGate(taskId);
109
-
110
- // Write / update task-state.json
111
- const statePath = path.join(taskDir, 'task-state.json');
112
- const existing = (await fs.pathExists(statePath)) ? await fs.readJson(statePath).catch(() => ({})) : {};
113
- const now = new Date().toISOString();
114
-
115
- const taskState = {
116
- ...existing,
117
- taskId,
118
- title: ctx.title || '',
119
- status: 'pending',
120
- createdAt: existing.createdAt || now,
121
- updatedAt: now,
122
- pausedAt: now,
123
- currentGate,
124
- gateApprovals: existing.gateApprovals || {},
125
- notes: note || existing.notes || '',
126
- };
127
- await fs.writeJson(statePath, taskState, { spaces: 2 });
128
-
129
- // Remove current.json so no task is active
130
- await fs.remove(CURRENT_FILE);
131
-
132
- console.log(chalk.green(`✓ Task ${taskId} paused at Gate ${currentGate}.`));
133
- if (note) console.log(chalk.gray(` Note: ${note}`));
134
- console.log(chalk.gray(` Resume later: aiflow task resume ${taskId}`));
135
- }
136
-
137
- async function switchTask(targetId) {
138
- // Pause current task if one is active
139
- if (await fs.pathExists(CURRENT_FILE)) {
140
- const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
141
- if (ctx && ctx.taskId && ctx.taskId !== targetId) {
142
- console.log(chalk.gray(` Pausing current task: ${ctx.taskId}`));
143
- await pauseTask();
144
- }
145
- }
146
-
147
- // Load target task if it exists
148
- const statePath = path.join(TASKS_DIR, targetId, 'task-state.json');
149
- if (await fs.pathExists(statePath)) {
150
- await resumeTask(targetId);
151
- } else {
152
- console.log(chalk.yellow(`Task ${targetId} not found in saved tasks.`));
153
- console.log(chalk.gray(` Run 'aiflow use ${targetId}' to load it from your ticket system.`));
154
- }
155
- }
156
-
157
- async function resumeTask(taskId) {
158
- const taskDir = path.join(TASKS_DIR, taskId);
159
- const statePath = path.join(taskDir, 'task-state.json');
160
- const ctxPath = path.join(taskDir, 'context.json');
161
-
162
- if (!(await fs.pathExists(statePath))) {
163
- console.log(chalk.red(`Task not found: ${taskId}`));
164
- console.log(chalk.gray(' Run `aiflow task list` to see available tasks.'));
165
- return;
166
- }
167
-
168
- const taskState = await fs.readJson(statePath);
169
- const ctx = await fs.readJson(ctxPath).catch(() => null);
170
-
171
- if (!ctx) {
172
- console.log(chalk.red(`Context snapshot missing for task: ${taskId}`));
173
- console.log(chalk.gray(` Try loading it fresh: aiflow use ${taskId}`));
174
- return;
175
- }
176
-
177
- // Restore context as current
178
- await fs.ensureDir(CONTEXT_DIR);
179
- await fs.writeJson(CURRENT_FILE, ctx, { spaces: 2 });
180
-
181
- // Deactivate any other tasks before marking this one active
182
- await deactivateOtherTasks(taskId);
183
-
184
- // Mark as active
185
- const now = new Date().toISOString();
186
- taskState.status = 'active';
187
- taskState.pausedAt = null;
188
- taskState.updatedAt = now;
189
- await fs.writeJson(statePath, taskState, { spaces: 2 });
190
-
191
- console.log(chalk.green(`✓ Resumed task: ${taskId}`));
192
- console.log(` ${chalk.white('Gate:')} ${taskState.currentGate} (${gateLabel(taskState.currentGate)})`);
193
- console.log(` ${chalk.white('Title:')} ${ctx.title.substring(0, 70)}`);
194
- if (taskState.notes) console.log(` ${chalk.white('Note:')} ${taskState.notes}`);
195
- console.log(chalk.gray('\n Open Claude to resume: claude'));
196
- }
197
-
198
- async function resetTask(taskId) {
199
- const taskDir = path.join(TASKS_DIR, taskId);
200
- const statePath = path.join(taskDir, 'task-state.json');
201
-
202
- if (!(await fs.pathExists(statePath))) {
203
- console.log(chalk.red(`Task not found: ${taskId}`));
204
- console.log(chalk.gray(' Run `aiflow task list` to see available tasks.'));
205
- return;
206
- }
207
-
208
- const taskState = await fs.readJson(statePath);
209
- const planDir = path.join(PROJECT_DIR, 'plan', taskId);
210
- const planFiles = [];
211
- if (await fs.pathExists(planDir)) {
212
- const entries = await fs.readdir(planDir);
213
- for (const f of entries) planFiles.push(path.join('plan', taskId, f));
214
- }
215
-
216
- // Show what will happen
217
- console.log(chalk.cyan(`\nReset task: ${taskId}\n`));
218
- console.log(chalk.white(' The following will be reset:'));
219
- console.log(chalk.gray(` • .aiflow/tasks/${taskId}/task-state.json`));
220
- console.log(chalk.gray(` Gate: ${taskState.currentGate} → 1 | Approvals cleared | Status: active`));
221
-
222
- const deletePlan = planFiles.length > 0 && await confirm({
223
- message: `Also delete ${planFiles.length} plan file(s) in plan/${taskId}/?`,
224
- default: false,
225
- });
226
-
227
- if (deletePlan) {
228
- console.log(chalk.gray('\n Plan files to delete:'));
229
- for (const f of planFiles) console.log(chalk.gray(` • ${f}`));
230
- }
231
-
232
- console.log();
233
- const ok = await confirm({ message: 'Proceed with reset?', default: false });
234
- if (!ok) { console.log(chalk.gray('Cancelled.')); return; }
235
-
236
- // Reset task-state.json to Gate 1
237
- const now = new Date().toISOString();
238
- await fs.writeJson(statePath, {
239
- ...taskState,
240
- status: 'active',
241
- currentGate: 1,
242
- gateApprovals: {},
243
- pausedAt: null,
244
- updatedAt: now,
245
- notes: '',
246
- }, { spaces: 2 });
247
-
248
- // Restore as current context
249
- const ctxPath = path.join(taskDir, 'context.json');
250
- if (await fs.pathExists(ctxPath)) {
251
- await fs.ensureDir(CONTEXT_DIR);
252
- await fs.copy(ctxPath, CURRENT_FILE, { overwrite: true });
253
- }
254
-
255
- if (deletePlan) await fs.remove(planDir);
256
-
257
- console.log(chalk.green(`✓ Task ${taskId} reset to Gate 1.`));
258
- if (deletePlan) console.log(chalk.gray(` Deleted plan/${taskId}/`));
259
- console.log(chalk.gray(' Open Claude to start Gate 1: claude'));
260
- }
261
-
262
- async function removeTask(taskId) {
263
- const taskDir = path.join(TASKS_DIR, taskId);
264
- const statePath = path.join(taskDir, 'task-state.json');
265
-
266
- if (!(await fs.pathExists(statePath))) {
267
- console.log(chalk.red(`Task not found: ${taskId}`));
268
- console.log(chalk.gray(' Run `aiflow task list` to see available tasks.'));
269
- return;
270
- }
271
-
272
- const isActive = await fs.pathExists(CURRENT_FILE) &&
273
- ((await fs.readJson(CURRENT_FILE).catch(() => ({}))).taskId === taskId);
274
- const planDir = path.join(PROJECT_DIR, 'plan', taskId);
275
- const hasPlan = await fs.pathExists(planDir);
276
- const planFiles = hasPlan ? await fs.readdir(planDir) : [];
277
-
278
- // Show what will be deleted
279
- console.log(chalk.cyan(`\nRemove task: ${taskId}\n`));
280
- console.log(chalk.white(' The following will be permanently deleted:'));
281
- console.log(chalk.gray(` • .aiflow/tasks/${taskId}/ (context snapshot + gate state)`));
282
- if (isActive) console.log(chalk.gray(` • .aiflow/context/current.json (this is the active task)`));
283
- if (hasPlan) {
284
- console.log(chalk.gray(` • plan/${taskId}/ (${planFiles.length} file(s)):`));
285
- for (const f of planFiles) console.log(chalk.gray(` - plan/${taskId}/${f}`));
286
- }
287
-
288
- console.log();
289
- const ok = await confirm({
290
- message: chalk.red('This cannot be undone. Proceed?'),
291
- default: false,
292
- });
293
- if (!ok) { console.log(chalk.gray('Cancelled.')); return; }
294
-
295
- await fs.remove(taskDir);
296
- if (isActive) await fs.remove(CURRENT_FILE);
297
- if (hasPlan) await fs.remove(planDir);
298
-
299
- console.log(chalk.green(`✓ Task ${taskId} removed.`));
300
- }
301
-
302
- async function nextGate(taskId) {
303
- // Resolve ticket ID
304
- let resolvedId = taskId;
305
- if (!resolvedId) {
306
- const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
307
- resolvedId = ctx && ctx.taskId ? ctx.taskId : null;
308
- }
309
- if (!resolvedId) {
310
- console.log(chalk.yellow('No ticket ID provided and no active task found.'));
311
- console.log(chalk.gray(' Usage: aiflow task next --ticket PROJ-123'));
312
- return;
313
- }
314
-
315
- const taskDir = path.join(TASKS_DIR, resolvedId);
316
- const statePath = path.join(taskDir, 'task-state.json');
317
- await fs.ensureDir(taskDir);
318
-
319
- // Load or create task state
320
- const existing = (await fs.pathExists(statePath)) ? await fs.readJson(statePath).catch(() => ({})) : {};
321
- const currentGate = existing.currentGate || await detectCurrentGate(resolvedId);
322
- const now = new Date().toISOString();
323
-
324
- // Record gate approval
325
- const gateApprovals = existing.gateApprovals || {};
326
- gateApprovals[String(currentGate)] = now;
327
- const nextGateNum = currentGate + 1;
328
-
329
- // Save context snapshot if current task is active
330
- if (await fs.pathExists(CURRENT_FILE)) {
331
- const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
332
- if (ctx && ctx.taskId === resolvedId) {
333
- await fs.writeJson(path.join(taskDir, 'context.json'), ctx, { spaces: 2 });
334
- }
335
- }
336
-
337
- const taskState = {
338
- ...existing,
339
- taskId: resolvedId,
340
- status: 'pending',
341
- updatedAt: now,
342
- pausedAt: now,
343
- currentGate: nextGateNum,
344
- gateApprovals,
345
- };
346
- await fs.writeJson(statePath, taskState, { spaces: 2 });
347
-
348
- // Clear current context so no task is "active"
349
- await fs.remove(CURRENT_FILE);
350
-
351
- // Generate cumulative task-summary.md
352
- const planDir = path.join(PROJECT_DIR, 'plan', resolvedId);
353
- await fs.ensureDir(planDir);
354
- const summaryPath = path.join(planDir, 'task-summary.md');
355
- const summaryContent = await generateMarkdownSummary(taskState);
356
- await fs.writeFile(summaryPath, summaryContent, 'utf-8');
357
-
358
- const gateLabels = { 1: 'Analyze', 2: 'Plan', 3: 'Code', 4: 'Review', 5: 'PR' };
359
- console.log(chalk.green(`✓ Gate ${currentGate} approved for ${resolvedId}.`));
360
- console.log(chalk.gray(` Summary saved to: plan/${resolvedId}/task-summary.md`));
361
- console.log(chalk.white(`\n Next: Gate ${nextGateNum} — ${gateLabels[nextGateNum] || 'Done'}`));
362
- console.log(chalk.cyan(`\n To continue in a fresh session (Recommended to avoid context pollution):`));
363
- console.log(chalk.gray(` 1. Open a NEW chatbox or terminal session.`));
364
- console.log(chalk.gray(` 2. Run: aiflow task resume ${resolvedId} (to load context).`));
365
- console.log(chalk.gray(` 3. Type "start" or "continue from the current plan".`));
366
- console.log(chalk.yellow(` (Note: Gate 3 progress is saved via [x] checkboxes in plan.md.`));
367
- console.log(chalk.yellow(` The AI will automatically resume the exact task you left off.)`));
368
- console.log();
369
- }
370
-
371
- // ──────────────────────────────────────────────────────────────
372
- // Internal helpers
373
- // ──────────────────────────────────────────────────────────────
374
-
375
- async function deactivateOtherTasks(exceptTaskId) {
376
- if (!(await fs.pathExists(TASKS_DIR))) return;
377
- const entries = await fs.readdir(TASKS_DIR, { withFileTypes: true });
378
- for (const entry of entries) {
379
- if (!entry.isDirectory() || entry.name === exceptTaskId) continue;
380
- const statePath = path.join(TASKS_DIR, entry.name, 'task-state.json');
381
- if (!(await fs.pathExists(statePath))) continue;
382
- const state = await fs.readJson(statePath).catch(() => null);
383
- if (state && state.status === 'active') {
384
- const now = new Date().toISOString();
385
- await fs.writeJson(statePath, { ...state, status: 'pending', updatedAt: now, pausedAt: now }, { spaces: 2 });
386
- }
387
- }
388
- }
389
-
390
- async function loadAllTaskStates() {
391
- if (!(await fs.pathExists(TASKS_DIR))) return [];
392
- const entries = await fs.readdir(TASKS_DIR, { withFileTypes: true });
393
- const states = [];
394
- for (const entry of entries) {
395
- if (!entry.isDirectory()) continue;
396
- const statePath = path.join(TASKS_DIR, entry.name, 'task-state.json');
397
- if (await fs.pathExists(statePath)) {
398
- const s = await fs.readJson(statePath).catch(() => null);
399
- if (s) states.push(s);
400
- }
401
- }
402
- return states.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
403
- }
404
-
405
- async function detectCurrentGate(taskId) {
406
- const statePath = path.join(TASKS_DIR, taskId, 'task-state.json');
407
- if (await fs.pathExists(statePath)) {
408
- const state = await fs.readJson(statePath).catch(() => ({}));
409
- if (state.currentGate) return state.currentGate;
410
- }
411
-
412
- const planDir = path.join(PROJECT_DIR, 'plan', taskId);
413
- if (!(await fs.pathExists(planDir))) return 1;
414
- if (await fs.pathExists(path.join(planDir, 'summary.md'))) return 5;
415
- if (await fs.pathExists(path.join(planDir, 'plan.md'))) return 3;
416
- if (await fs.pathExists(path.join(planDir, 'requirement.md'))) return 2;
417
- return 1;
418
- }
419
-
420
- async function generateMarkdownSummary(taskState) {
421
- const lines = [];
422
- lines.push(`# Task Summary: ${taskState.taskId}`);
423
- lines.push(`**Title:** ${taskState.title}`);
424
- lines.push(`**Status:** ${taskState.status}`);
425
- lines.push(`**Current Gate:** ${taskState.currentGate} (${gateLabel(taskState.currentGate)})`);
426
- lines.push(`**Updated At:** ${new Date().toLocaleString()}`);
427
- lines.push(``);
428
- lines.push(`## Gate History`);
429
- lines.push(`| Gate | Status | Approved At |`);
430
- lines.push(`|------|--------|-------------|`);
431
- for (let i = 1; i <= 5; i++) {
432
- const approvedAt = taskState.gateApprovals && taskState.gateApprovals[String(i)];
433
- const status = approvedAt ? '✅ Approved' : (i === taskState.currentGate ? '⏳ In Progress' : '⚪ Pending');
434
- const timeStr = approvedAt ? new Date(approvedAt).toLocaleString() : '-';
435
- lines.push(`| Gate ${i} (${gateLabel(i)}) | ${status} | ${timeStr} |`);
436
- }
437
- lines.push(``);
438
- lines.push(`---`);
439
- lines.push(`*Auto-generated by aiflow task next*`);
440
- return lines.join('\n');
441
- }
442
-
443
- function gateLabel(n) {
444
- const labels = {
445
- 1: 'AI Analyze Requirement',
446
- 2: 'Implementation Plan',
447
- 3: 'Code Generation',
448
- 4: 'AI Self-Review',
449
- 5: 'Peer Review & PR',
450
- };
451
- return labels[n] || `Gate ${n}`;
452
- }
453
-
454
- function formatRelative(isoStr) {
455
- const ms = Date.now() - new Date(isoStr).getTime();
456
- const mins = Math.floor(ms / 60000);
457
- if (mins < 60) return `${mins}m ago`;
458
- const hrs = Math.floor(mins / 60);
459
- if (hrs < 24) return `${hrs}h ago`;
460
- return `${Math.floor(hrs / 24)}d ago`;
461
- }
462
-
463
- // ──────────────────────────────────────────────────────────────
464
- // Export helper used by session-start hook and use.js
465
- // ──────────────────────────────────────────────────────────────
466
-
467
- module.exports.getActiveTaskState = async function getActiveTaskState() {
468
- if (!(await fs.pathExists(CURRENT_FILE))) return null;
469
- const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
470
- if (!ctx || !ctx.taskId) return null;
471
- const statePath = path.join(TASKS_DIR, ctx.taskId, 'task-state.json');
472
- return (await fs.pathExists(statePath)) ? fs.readJson(statePath).catch(() => null) : null;
473
- };
474
-
475
- module.exports.createOrActivateTaskState = async function createOrActivateTaskState(ctx) {
476
- const taskId = ctx.taskId;
477
- if (!taskId) return;
478
- const taskDir = path.join(TASKS_DIR, taskId);
479
- const statePath = path.join(taskDir, 'task-state.json');
480
- await fs.ensureDir(taskDir);
481
-
482
- // Save context snapshot
483
- await fs.writeJson(path.join(taskDir, 'context.json'), ctx, { spaces: 2 });
484
-
485
- // Detect current gate
486
- const currentGate = await detectCurrentGate(taskId);
487
-
488
- // Deactivate any other tasks before marking this one active
489
- await deactivateOtherTasks(taskId);
490
-
491
- const existing = (await fs.pathExists(statePath)) ? await fs.readJson(statePath).catch(() => ({})) : {};
492
- const now = new Date().toISOString();
493
- const taskState = {
494
- ...existing,
495
- taskId,
496
- title: ctx.title || '',
497
- status: 'active',
498
- createdAt: existing.createdAt || now,
499
- updatedAt: now,
500
- pausedAt: null,
501
- currentGate: Math.max(existing.currentGate || 1, currentGate),
502
- gateApprovals: existing.gateApprovals || {},
503
- notes: existing.notes || '',
504
- };
505
- await fs.writeJson(statePath, taskState, { spaces: 2 });
506
- };
507
-
508
- /**
509
- * Called by `aiflow gate <N> <start|approved>` to keep task-state.json in sync
510
- * with the actual gate progress — without requiring the user to run `task next`.
511
- *
512
- * - 'start' : advances currentGate to N (if N is higher than stored value)
513
- * - 'approved': records approval timestamp + advances currentGate to N+1
514
- *
515
- * Silently no-ops when task-state.json does not exist yet (task not saved).
516
- */
517
- module.exports.updateTaskGateState = async function updateTaskGateState(ticketId, gateNum, action) {
518
- if (!ticketId) return;
519
- const taskDir = path.join(TASKS_DIR, ticketId);
520
- const statePath = path.join(taskDir, 'task-state.json');
521
- if (!(await fs.pathExists(statePath))) return; // task not yet saved — nothing to update
522
-
523
- const existing = await fs.readJson(statePath).catch(() => null);
524
- if (!existing) return;
525
-
526
- const now = new Date().toISOString();
527
- const updated = { ...existing, updatedAt: now };
528
-
529
- if (action === 'start') {
530
- // Advance currentGate only if this gate is higher (never go backwards)
531
- if (gateNum > (existing.currentGate || 1)) {
532
- updated.currentGate = gateNum;
533
- }
534
- } else if (action === 'approved') {
535
- // Record the approval timestamp
536
- const gateApprovals = { ...(existing.gateApprovals || {}) };
537
- gateApprovals[String(gateNum)] = now;
538
- updated.gateApprovals = gateApprovals;
539
- // Advance currentGate to next gate (only if not already ahead)
540
- if (gateNum >= (existing.currentGate || 1)) {
541
- updated.currentGate = gateNum + 1;
542
- }
543
- }
544
-
545
- await fs.writeJson(statePath, updated, { spaces: 2 });
546
- };
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const { confirm } = require('@inquirer/prompts');
5
+
6
+ const PROJECT_DIR = process.cwd();
7
+ const AIFLOW_DIR = path.join(PROJECT_DIR, '.aiflow');
8
+ const CONTEXT_DIR = path.join(AIFLOW_DIR, 'context');
9
+ const CURRENT_FILE = path.join(CONTEXT_DIR, 'current.json');
10
+ const TASKS_DIR = path.join(AIFLOW_DIR, 'tasks');
11
+
12
+ // ──────────────────────────────────────────────────────────────
13
+ // Entry point
14
+ // ──────────────────────────────────────────────────────────────
15
+
16
+ module.exports = async function task(action, options = {}) {
17
+ try {
18
+ if (action === 'list') return await listTasks();
19
+ if (action === 'status') return await showStatus();
20
+ if (action === 'pause') return await pauseTask(options.note);
21
+ if (action === 'switch' && options.taskId) return await switchTask(options.taskId);
22
+ if (action === 'resume' && options.taskId) return await resumeTask(options.taskId);
23
+ if (action === 'reset' && options.taskId) return await resetTask(options.taskId);
24
+ if (action === 'remove' && options.taskId) return await removeTask(options.taskId);
25
+ if (action === 'next') return await nextGate(options.taskId);
26
+
27
+ console.log(chalk.yellow('Usage:'));
28
+ console.log(chalk.gray(' aiflow task status — show active task and pending list'));
29
+ console.log(chalk.gray(' aiflow task list — list all saved tasks'));
30
+ console.log(chalk.gray(' aiflow task pause — pause current task'));
31
+ console.log(chalk.gray(' aiflow task pause --note "..." — pause with a note'));
32
+ console.log(chalk.gray(' aiflow task switch <ticket-id> — pause current + switch to another task'));
33
+ console.log(chalk.gray(' aiflow task resume <ticket-id> — resume a paused task'));
34
+ console.log(chalk.gray(' aiflow task reset <ticket-id> — reset task to Gate 1 (keeps context)'));
35
+ console.log(chalk.gray(' aiflow task remove <ticket-id> — permanently delete all task data'));
36
+ console.log(chalk.gray(' aiflow task next — approve current gate + prep next session'));
37
+ console.log(chalk.gray(' aiflow task next --ticket <id> — same, specify ticket explicitly'));
38
+ } catch (err) {
39
+ console.error(chalk.red(`Error: ${err.message}`));
40
+ }
41
+ };
42
+
43
+ // ──────────────────────────────────────────────────────────────
44
+ // Commands
45
+ // ──────────────────────────────────────────────────────────────
46
+
47
+ async function showStatus() {
48
+ const tasks = await loadAllTaskStates();
49
+ const active = tasks.filter(t => t.status === 'active');
50
+ const pending = tasks.filter(t => t.status === 'pending');
51
+
52
+ console.log(chalk.cyan('\nTask Status\n'));
53
+
54
+ if (active.length === 0) {
55
+ console.log(chalk.gray(' No active task. Run `aiflow use <ticket>` to start one.'));
56
+ } else {
57
+ const t = active[0];
58
+ console.log(` ${chalk.green('● Active')} ${chalk.white(t.taskId)} ${chalk.gray('Gate ' + t.currentGate)} ${chalk.gray(t.title.substring(0, 55))}`);
59
+ }
60
+
61
+ if (pending.length > 0) {
62
+ console.log(chalk.yellow(`\n Pending (${pending.length}):`));
63
+ for (const t of pending) {
64
+ const paused = t.pausedAt ? chalk.gray(` paused ${formatRelative(t.pausedAt)}`) : '';
65
+ const note = t.notes ? chalk.gray(` — ${t.notes.substring(0, 40)}`) : '';
66
+ console.log(` ${chalk.gray('○ Pending')} ${chalk.white(t.taskId.padEnd(14))} Gate ${t.currentGate} ${chalk.gray(t.title.substring(0, 40))}${paused}${note}`);
67
+ }
68
+ console.log(chalk.gray('\n Resume with: aiflow task resume <ticket-id>'));
69
+ }
70
+ console.log();
71
+ }
72
+
73
+ async function listTasks() {
74
+ const tasks = await loadAllTaskStates();
75
+ if (tasks.length === 0) {
76
+ console.log(chalk.gray('No saved tasks.'));
77
+ return;
78
+ }
79
+ console.log(chalk.cyan('\nAll Tasks:\n'));
80
+ for (const t of tasks) {
81
+ const icon = t.status === 'active' ? chalk.green('●') : chalk.gray('○');
82
+ const label = t.status === 'active' ? chalk.green('active ') : chalk.yellow('pending');
83
+ console.log(` ${icon} ${label} ${chalk.white(t.taskId.padEnd(16))} Gate ${t.currentGate} ${chalk.gray(t.title.substring(0, 50))}`);
84
+ }
85
+ console.log();
86
+ }
87
+
88
+ async function pauseTask(note = '') {
89
+ if (!(await fs.pathExists(CURRENT_FILE))) {
90
+ console.log(chalk.yellow('No active task to pause.'));
91
+ return;
92
+ }
93
+
94
+ const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
95
+ if (!ctx || !ctx.taskId) {
96
+ console.log(chalk.yellow('Current context has no taskId — cannot pause.'));
97
+ return;
98
+ }
99
+
100
+ const taskId = ctx.taskId;
101
+ const taskDir = path.join(TASKS_DIR, taskId);
102
+ await fs.ensureDir(taskDir);
103
+
104
+ // Save context snapshot
105
+ await fs.writeJson(path.join(taskDir, 'context.json'), ctx, { spaces: 2 });
106
+
107
+ // Detect gate from plan/ artifacts
108
+ const currentGate = await detectCurrentGate(taskId);
109
+
110
+ // Write / update task-state.json
111
+ const statePath = path.join(taskDir, 'task-state.json');
112
+ const existing = (await fs.pathExists(statePath)) ? await fs.readJson(statePath).catch(() => ({})) : {};
113
+ const now = new Date().toISOString();
114
+
115
+ const taskState = {
116
+ ...existing,
117
+ taskId,
118
+ title: ctx.title || '',
119
+ status: 'pending',
120
+ createdAt: existing.createdAt || now,
121
+ updatedAt: now,
122
+ pausedAt: now,
123
+ currentGate,
124
+ gateApprovals: existing.gateApprovals || {},
125
+ notes: note || existing.notes || '',
126
+ };
127
+ await fs.writeJson(statePath, taskState, { spaces: 2 });
128
+
129
+ // Remove current.json so no task is active
130
+ await fs.remove(CURRENT_FILE);
131
+
132
+ console.log(chalk.green(`✓ Task ${taskId} paused at Gate ${currentGate}.`));
133
+ if (note) console.log(chalk.gray(` Note: ${note}`));
134
+ console.log(chalk.gray(` Resume later: aiflow task resume ${taskId}`));
135
+ }
136
+
137
+ async function switchTask(targetId) {
138
+ // Pause current task if one is active
139
+ if (await fs.pathExists(CURRENT_FILE)) {
140
+ const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
141
+ if (ctx && ctx.taskId && ctx.taskId !== targetId) {
142
+ console.log(chalk.gray(` Pausing current task: ${ctx.taskId}`));
143
+ await pauseTask();
144
+ }
145
+ }
146
+
147
+ // Load target task if it exists
148
+ const statePath = path.join(TASKS_DIR, targetId, 'task-state.json');
149
+ if (await fs.pathExists(statePath)) {
150
+ await resumeTask(targetId);
151
+ } else {
152
+ console.log(chalk.yellow(`Task ${targetId} not found in saved tasks.`));
153
+ console.log(chalk.gray(` Run 'aiflow use ${targetId}' to load it from your ticket system.`));
154
+ }
155
+ }
156
+
157
+ async function resumeTask(taskId) {
158
+ const taskDir = path.join(TASKS_DIR, taskId);
159
+ const statePath = path.join(taskDir, 'task-state.json');
160
+ const ctxPath = path.join(taskDir, 'context.json');
161
+
162
+ if (!(await fs.pathExists(statePath))) {
163
+ console.log(chalk.red(`Task not found: ${taskId}`));
164
+ console.log(chalk.gray(' Run `aiflow task list` to see available tasks.'));
165
+ return;
166
+ }
167
+
168
+ const taskState = await fs.readJson(statePath);
169
+ const ctx = await fs.readJson(ctxPath).catch(() => null);
170
+
171
+ if (!ctx) {
172
+ console.log(chalk.red(`Context snapshot missing for task: ${taskId}`));
173
+ console.log(chalk.gray(` Try loading it fresh: aiflow use ${taskId}`));
174
+ return;
175
+ }
176
+
177
+ // Restore context as current
178
+ await fs.ensureDir(CONTEXT_DIR);
179
+ await fs.writeJson(CURRENT_FILE, ctx, { spaces: 2 });
180
+
181
+ // Deactivate any other tasks before marking this one active
182
+ await deactivateOtherTasks(taskId);
183
+
184
+ // Mark as active
185
+ const now = new Date().toISOString();
186
+ taskState.status = 'active';
187
+ taskState.pausedAt = null;
188
+ taskState.updatedAt = now;
189
+ await fs.writeJson(statePath, taskState, { spaces: 2 });
190
+
191
+ console.log(chalk.green(`✓ Resumed task: ${taskId}`));
192
+ console.log(` ${chalk.white('Gate:')} ${taskState.currentGate} (${gateLabel(taskState.currentGate)})`);
193
+ console.log(` ${chalk.white('Title:')} ${ctx.title.substring(0, 70)}`);
194
+ if (taskState.notes) console.log(` ${chalk.white('Note:')} ${taskState.notes}`);
195
+ console.log(chalk.gray('\n Open Claude to resume: claude'));
196
+ }
197
+
198
+ async function resetTask(taskId) {
199
+ const taskDir = path.join(TASKS_DIR, taskId);
200
+ const statePath = path.join(taskDir, 'task-state.json');
201
+
202
+ if (!(await fs.pathExists(statePath))) {
203
+ console.log(chalk.red(`Task not found: ${taskId}`));
204
+ console.log(chalk.gray(' Run `aiflow task list` to see available tasks.'));
205
+ return;
206
+ }
207
+
208
+ const taskState = await fs.readJson(statePath);
209
+ const planDir = path.join(PROJECT_DIR, 'plan', taskId);
210
+ const planFiles = [];
211
+ if (await fs.pathExists(planDir)) {
212
+ const entries = await fs.readdir(planDir);
213
+ for (const f of entries) planFiles.push(path.join('plan', taskId, f));
214
+ }
215
+
216
+ // Show what will happen
217
+ console.log(chalk.cyan(`\nReset task: ${taskId}\n`));
218
+ console.log(chalk.white(' The following will be reset:'));
219
+ console.log(chalk.gray(` • .aiflow/tasks/${taskId}/task-state.json`));
220
+ console.log(chalk.gray(` Gate: ${taskState.currentGate} → 1 | Approvals cleared | Status: active`));
221
+
222
+ const deletePlan = planFiles.length > 0 && await confirm({
223
+ message: `Also delete ${planFiles.length} plan file(s) in plan/${taskId}/?`,
224
+ default: false,
225
+ });
226
+
227
+ if (deletePlan) {
228
+ console.log(chalk.gray('\n Plan files to delete:'));
229
+ for (const f of planFiles) console.log(chalk.gray(` • ${f}`));
230
+ }
231
+
232
+ console.log();
233
+ const ok = await confirm({ message: 'Proceed with reset?', default: false });
234
+ if (!ok) { console.log(chalk.gray('Cancelled.')); return; }
235
+
236
+ // Reset task-state.json to Gate 1
237
+ const now = new Date().toISOString();
238
+ await fs.writeJson(statePath, {
239
+ ...taskState,
240
+ status: 'active',
241
+ currentGate: 1,
242
+ gateApprovals: {},
243
+ pausedAt: null,
244
+ updatedAt: now,
245
+ notes: '',
246
+ }, { spaces: 2 });
247
+
248
+ // Restore as current context
249
+ const ctxPath = path.join(taskDir, 'context.json');
250
+ if (await fs.pathExists(ctxPath)) {
251
+ await fs.ensureDir(CONTEXT_DIR);
252
+ await fs.copy(ctxPath, CURRENT_FILE, { overwrite: true });
253
+ }
254
+
255
+ if (deletePlan) await fs.remove(planDir);
256
+
257
+ console.log(chalk.green(`✓ Task ${taskId} reset to Gate 1.`));
258
+ if (deletePlan) console.log(chalk.gray(` Deleted plan/${taskId}/`));
259
+ console.log(chalk.gray(' Open Claude to start Gate 1: claude'));
260
+ }
261
+
262
+ async function removeTask(taskId) {
263
+ const taskDir = path.join(TASKS_DIR, taskId);
264
+ const statePath = path.join(taskDir, 'task-state.json');
265
+
266
+ if (!(await fs.pathExists(statePath))) {
267
+ console.log(chalk.red(`Task not found: ${taskId}`));
268
+ console.log(chalk.gray(' Run `aiflow task list` to see available tasks.'));
269
+ return;
270
+ }
271
+
272
+ const isActive = await fs.pathExists(CURRENT_FILE) &&
273
+ ((await fs.readJson(CURRENT_FILE).catch(() => ({}))).taskId === taskId);
274
+ const planDir = path.join(PROJECT_DIR, 'plan', taskId);
275
+ const hasPlan = await fs.pathExists(planDir);
276
+ const planFiles = hasPlan ? await fs.readdir(planDir) : [];
277
+
278
+ // Show what will be deleted
279
+ console.log(chalk.cyan(`\nRemove task: ${taskId}\n`));
280
+ console.log(chalk.white(' The following will be permanently deleted:'));
281
+ console.log(chalk.gray(` • .aiflow/tasks/${taskId}/ (context snapshot + gate state)`));
282
+ if (isActive) console.log(chalk.gray(` • .aiflow/context/current.json (this is the active task)`));
283
+ if (hasPlan) {
284
+ console.log(chalk.gray(` • plan/${taskId}/ (${planFiles.length} file(s)):`));
285
+ for (const f of planFiles) console.log(chalk.gray(` - plan/${taskId}/${f}`));
286
+ }
287
+
288
+ console.log();
289
+ const ok = await confirm({
290
+ message: chalk.red('This cannot be undone. Proceed?'),
291
+ default: false,
292
+ });
293
+ if (!ok) { console.log(chalk.gray('Cancelled.')); return; }
294
+
295
+ await fs.remove(taskDir);
296
+ if (isActive) await fs.remove(CURRENT_FILE);
297
+ if (hasPlan) await fs.remove(planDir);
298
+
299
+ console.log(chalk.green(`✓ Task ${taskId} removed.`));
300
+ }
301
+
302
+ async function nextGate(taskId) {
303
+ // Resolve ticket ID
304
+ let resolvedId = taskId;
305
+ if (!resolvedId) {
306
+ const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
307
+ resolvedId = ctx && ctx.taskId ? ctx.taskId : null;
308
+ }
309
+ if (!resolvedId) {
310
+ console.log(chalk.yellow('No ticket ID provided and no active task found.'));
311
+ console.log(chalk.gray(' Usage: aiflow task next --ticket PROJ-123'));
312
+ return;
313
+ }
314
+
315
+ const taskDir = path.join(TASKS_DIR, resolvedId);
316
+ const statePath = path.join(taskDir, 'task-state.json');
317
+ await fs.ensureDir(taskDir);
318
+
319
+ // Load or create task state
320
+ const existing = (await fs.pathExists(statePath)) ? await fs.readJson(statePath).catch(() => ({})) : {};
321
+ const currentGate = existing.currentGate || await detectCurrentGate(resolvedId);
322
+ const now = new Date().toISOString();
323
+
324
+ // Record gate approval
325
+ const gateApprovals = existing.gateApprovals || {};
326
+ gateApprovals[String(currentGate)] = now;
327
+ const nextGateNum = currentGate + 1;
328
+
329
+ // Save context snapshot if current task is active
330
+ if (await fs.pathExists(CURRENT_FILE)) {
331
+ const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
332
+ if (ctx && ctx.taskId === resolvedId) {
333
+ await fs.writeJson(path.join(taskDir, 'context.json'), ctx, { spaces: 2 });
334
+ }
335
+ }
336
+
337
+ const taskState = {
338
+ ...existing,
339
+ taskId: resolvedId,
340
+ status: 'pending',
341
+ updatedAt: now,
342
+ pausedAt: now,
343
+ currentGate: nextGateNum,
344
+ gateApprovals,
345
+ };
346
+ await fs.writeJson(statePath, taskState, { spaces: 2 });
347
+
348
+ // Clear current context so no task is "active"
349
+ await fs.remove(CURRENT_FILE);
350
+
351
+ // Generate cumulative task-summary.md
352
+ const planDir = path.join(PROJECT_DIR, 'plan', resolvedId);
353
+ await fs.ensureDir(planDir);
354
+ const summaryPath = path.join(planDir, 'task-summary.md');
355
+ const summaryContent = await generateMarkdownSummary(taskState);
356
+ await fs.writeFile(summaryPath, summaryContent, 'utf-8');
357
+
358
+ const gateLabels = { 1: 'Analyze', 2: 'Plan', 3: 'Code', 4: 'Review', 5: 'PR' };
359
+ console.log(chalk.green(`✓ Gate ${currentGate} approved for ${resolvedId}.`));
360
+ console.log(chalk.gray(` Summary saved to: plan/${resolvedId}/task-summary.md`));
361
+ console.log(chalk.white(`\n Next: Gate ${nextGateNum} — ${gateLabels[nextGateNum] || 'Done'}`));
362
+ console.log(chalk.cyan(`\n To continue in a fresh session (Recommended to avoid context pollution):`));
363
+ console.log(chalk.gray(` 1. Open a NEW chatbox or terminal session.`));
364
+ console.log(chalk.gray(` 2. Run: aiflow task resume ${resolvedId} (to load context).`));
365
+ console.log(chalk.gray(` 3. Type "start" or "continue from the current plan".`));
366
+ console.log(chalk.yellow(` (Note: Gate 3 progress is saved via [x] checkboxes in plan.md.`));
367
+ console.log(chalk.yellow(` The AI will automatically resume the exact task you left off.)`));
368
+ console.log();
369
+ }
370
+
371
+ // ──────────────────────────────────────────────────────────────
372
+ // Internal helpers
373
+ // ──────────────────────────────────────────────────────────────
374
+
375
+ async function deactivateOtherTasks(exceptTaskId) {
376
+ if (!(await fs.pathExists(TASKS_DIR))) return;
377
+ const entries = await fs.readdir(TASKS_DIR, { withFileTypes: true });
378
+ for (const entry of entries) {
379
+ if (!entry.isDirectory() || entry.name === exceptTaskId) continue;
380
+ const statePath = path.join(TASKS_DIR, entry.name, 'task-state.json');
381
+ if (!(await fs.pathExists(statePath))) continue;
382
+ const state = await fs.readJson(statePath).catch(() => null);
383
+ if (state && state.status === 'active') {
384
+ const now = new Date().toISOString();
385
+ await fs.writeJson(statePath, { ...state, status: 'pending', updatedAt: now, pausedAt: now }, { spaces: 2 });
386
+ }
387
+ }
388
+ }
389
+
390
+ async function loadAllTaskStates() {
391
+ if (!(await fs.pathExists(TASKS_DIR))) return [];
392
+ const entries = await fs.readdir(TASKS_DIR, { withFileTypes: true });
393
+ const states = [];
394
+ for (const entry of entries) {
395
+ if (!entry.isDirectory()) continue;
396
+ const statePath = path.join(TASKS_DIR, entry.name, 'task-state.json');
397
+ if (await fs.pathExists(statePath)) {
398
+ const s = await fs.readJson(statePath).catch(() => null);
399
+ if (s) states.push(s);
400
+ }
401
+ }
402
+ return states.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
403
+ }
404
+
405
+ async function detectCurrentGate(taskId) {
406
+ const statePath = path.join(TASKS_DIR, taskId, 'task-state.json');
407
+ if (await fs.pathExists(statePath)) {
408
+ const state = await fs.readJson(statePath).catch(() => ({}));
409
+ if (state.currentGate) return state.currentGate;
410
+ }
411
+
412
+ const planDir = path.join(PROJECT_DIR, 'plan', taskId);
413
+ if (!(await fs.pathExists(planDir))) return 1;
414
+ if (await fs.pathExists(path.join(planDir, 'summary.md'))) return 5;
415
+ if (await fs.pathExists(path.join(planDir, 'plan.md'))) return 3;
416
+ if (await fs.pathExists(path.join(planDir, 'requirement.md'))) return 2;
417
+ return 1;
418
+ }
419
+
420
+ async function generateMarkdownSummary(taskState) {
421
+ const lines = [];
422
+ lines.push(`# Task Summary: ${taskState.taskId}`);
423
+ lines.push(`**Title:** ${taskState.title}`);
424
+ lines.push(`**Status:** ${taskState.status}`);
425
+ lines.push(`**Current Gate:** ${taskState.currentGate} (${gateLabel(taskState.currentGate)})`);
426
+ lines.push(`**Updated At:** ${new Date().toLocaleString()}`);
427
+ lines.push(``);
428
+ lines.push(`## Gate History`);
429
+ lines.push(`| Gate | Status | Approved At |`);
430
+ lines.push(`|------|--------|-------------|`);
431
+ for (let i = 1; i <= 5; i++) {
432
+ const approvedAt = taskState.gateApprovals && taskState.gateApprovals[String(i)];
433
+ const status = approvedAt ? '✅ Approved' : (i === taskState.currentGate ? '⏳ In Progress' : '⚪ Pending');
434
+ const timeStr = approvedAt ? new Date(approvedAt).toLocaleString() : '-';
435
+ lines.push(`| Gate ${i} (${gateLabel(i)}) | ${status} | ${timeStr} |`);
436
+ }
437
+ lines.push(``);
438
+ lines.push(`---`);
439
+ lines.push(`*Auto-generated by aiflow task next*`);
440
+ return lines.join('\n');
441
+ }
442
+
443
+ function gateLabel(n) {
444
+ const labels = {
445
+ 1: 'AI Analyze Requirement',
446
+ 2: 'Implementation Plan',
447
+ 3: 'Code Generation',
448
+ 4: 'AI Self-Review',
449
+ 5: 'Peer Review & PR',
450
+ };
451
+ return labels[n] || `Gate ${n}`;
452
+ }
453
+
454
+ function formatRelative(isoStr) {
455
+ const ms = Date.now() - new Date(isoStr).getTime();
456
+ const mins = Math.floor(ms / 60000);
457
+ if (mins < 60) return `${mins}m ago`;
458
+ const hrs = Math.floor(mins / 60);
459
+ if (hrs < 24) return `${hrs}h ago`;
460
+ return `${Math.floor(hrs / 24)}d ago`;
461
+ }
462
+
463
+ // ──────────────────────────────────────────────────────────────
464
+ // Export helper used by session-start hook and use.js
465
+ // ──────────────────────────────────────────────────────────────
466
+
467
+ module.exports.getActiveTaskState = async function getActiveTaskState() {
468
+ if (!(await fs.pathExists(CURRENT_FILE))) return null;
469
+ const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
470
+ if (!ctx || !ctx.taskId) return null;
471
+ const statePath = path.join(TASKS_DIR, ctx.taskId, 'task-state.json');
472
+ return (await fs.pathExists(statePath)) ? fs.readJson(statePath).catch(() => null) : null;
473
+ };
474
+
475
+ module.exports.createOrActivateTaskState = async function createOrActivateTaskState(ctx) {
476
+ const taskId = ctx.taskId;
477
+ if (!taskId) return;
478
+ const taskDir = path.join(TASKS_DIR, taskId);
479
+ const statePath = path.join(taskDir, 'task-state.json');
480
+ await fs.ensureDir(taskDir);
481
+
482
+ // Save context snapshot
483
+ await fs.writeJson(path.join(taskDir, 'context.json'), ctx, { spaces: 2 });
484
+
485
+ // Detect current gate
486
+ const currentGate = await detectCurrentGate(taskId);
487
+
488
+ // Deactivate any other tasks before marking this one active
489
+ await deactivateOtherTasks(taskId);
490
+
491
+ const existing = (await fs.pathExists(statePath)) ? await fs.readJson(statePath).catch(() => ({})) : {};
492
+ const now = new Date().toISOString();
493
+ const taskState = {
494
+ ...existing,
495
+ taskId,
496
+ title: ctx.title || '',
497
+ status: 'active',
498
+ createdAt: existing.createdAt || now,
499
+ updatedAt: now,
500
+ pausedAt: null,
501
+ currentGate: Math.max(existing.currentGate || 1, currentGate),
502
+ gateApprovals: existing.gateApprovals || {},
503
+ notes: existing.notes || '',
504
+ };
505
+ await fs.writeJson(statePath, taskState, { spaces: 2 });
506
+ };
507
+
508
+ /**
509
+ * Called by `aiflow gate <N> <start|approved>` to keep task-state.json in sync
510
+ * with the actual gate progress — without requiring the user to run `task next`.
511
+ *
512
+ * - 'start' : advances currentGate to N (if N is higher than stored value)
513
+ * - 'approved': records approval timestamp + advances currentGate to N+1
514
+ *
515
+ * Silently no-ops when task-state.json does not exist yet (task not saved).
516
+ */
517
+ module.exports.updateTaskGateState = async function updateTaskGateState(ticketId, gateNum, action) {
518
+ if (!ticketId) return;
519
+ const taskDir = path.join(TASKS_DIR, ticketId);
520
+ const statePath = path.join(taskDir, 'task-state.json');
521
+ if (!(await fs.pathExists(statePath))) return; // task not yet saved — nothing to update
522
+
523
+ const existing = await fs.readJson(statePath).catch(() => null);
524
+ if (!existing) return;
525
+
526
+ const now = new Date().toISOString();
527
+ const updated = { ...existing, updatedAt: now };
528
+
529
+ if (action === 'start') {
530
+ // Advance currentGate only if this gate is higher (never go backwards)
531
+ if (gateNum > (existing.currentGate || 1)) {
532
+ updated.currentGate = gateNum;
533
+ }
534
+ } else if (action === 'approved') {
535
+ // Record the approval timestamp
536
+ const gateApprovals = { ...(existing.gateApprovals || {}) };
537
+ gateApprovals[String(gateNum)] = now;
538
+ updated.gateApprovals = gateApprovals;
539
+ // Advance currentGate to next gate (only if not already ahead)
540
+ if (gateNum >= (existing.currentGate || 1)) {
541
+ updated.currentGate = gateNum + 1;
542
+ }
543
+ }
544
+
545
+ await fs.writeJson(statePath, updated, { spaces: 2 });
546
+ };