@relipa/ai-flow-kit 0.1.1-beta.0 → 0.1.2-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -0
- package/bin/aiflow.js +55 -2
- package/bin/ak.js +0 -0
- package/custom/mcp-presets/README.md +204 -204
- package/custom/mcp-presets/backlog.json +0 -0
- package/custom/mcp-presets/figma-desktop.json +0 -0
- package/custom/mcp-presets/figma.json +0 -0
- package/custom/mcp-presets/gitnexus.json +0 -0
- package/custom/mcp-presets/google-sheets.json +0 -0
- package/custom/mcp-presets/jira.json +0 -0
- package/custom/prompts/bug-fix.md +0 -0
- package/custom/prompts/feature.md +0 -0
- package/custom/prompts/investigation.md +0 -0
- package/custom/rules/code-style.md +0 -0
- package/custom/rules/java/code-style.md +0 -0
- package/custom/rules/java/naming.md +0 -0
- package/custom/rules/java/review-checklist.md +0 -0
- package/custom/rules/java/spring-boot-examples.md +0 -0
- package/custom/rules/javascript/code-style.md +0 -0
- package/custom/rules/javascript/naming.md +0 -0
- package/custom/rules/ml-conventions.md +88 -0
- package/custom/rules/ml-review-checklist.md +14 -0
- package/custom/rules/naming.md +0 -0
- package/custom/rules/php/code-style.md +0 -0
- package/custom/rules/php/naming.md +0 -0
- package/custom/rules/project-conventions.md +0 -0
- package/custom/rules/review-checklist.md +0 -0
- package/custom/skills/deploy-model/SKILL.md +90 -0
- package/custom/skills/design-experiment/SKILL.md +88 -0
- package/custom/skills/evaluate-model/SKILL.md +93 -0
- package/custom/skills/explore-data/SKILL.md +87 -0
- package/custom/skills/figma-to-component/SKILL.md +164 -59
- package/custom/skills/frame-ml-problem/SKILL.md +88 -0
- package/custom/skills/generate-spec/SKILL.md +0 -0
- package/custom/skills/impact-analysis/SKILL.md +0 -0
- package/custom/skills/improve-algorithm/SKILL.md +74 -0
- package/custom/skills/investigate-bug/SKILL.md +0 -0
- package/custom/skills/read-study-requirement/SKILL.md +0 -0
- package/custom/skills/report-customer/SKILL.md +0 -0
- package/custom/skills/review-plan/SKILL.md +0 -0
- package/custom/skills/train-model/SKILL.md +78 -0
- package/custom/templates/laravel.md +0 -0
- package/custom/templates/nestjs.md +0 -0
- package/custom/templates/nextjs.md +0 -0
- package/custom/templates/nodejs-express.md +0 -0
- package/custom/templates/php-plain.md +0 -0
- package/custom/templates/php.md +0 -0
- package/custom/templates/python-django.md +0 -0
- package/custom/templates/python-fastapi.md +0 -0
- package/custom/templates/python-ml.md +273 -0
- package/custom/templates/python.md +0 -0
- package/custom/templates/reactjs.md +0 -0
- package/custom/templates/shared/gate-workflow.md +0 -0
- package/custom/templates/shared/ml-gate-workflow.md +103 -0
- package/custom/templates/spring-boot.md +0 -0
- package/custom/templates/tools/claude.md +0 -0
- package/custom/templates/tools/copilot.md +0 -0
- package/custom/templates/tools/cursor.md +0 -0
- package/custom/templates/tools/gemini.md +0 -0
- package/custom/templates/tools/generic.md +0 -0
- package/custom/templates/vue-nuxt.md +0 -0
- package/docs/common/AIFLOW.md +512 -512
- package/docs/common/CHANGELOG.md +16 -0
- package/docs/common/QUICK_START.md +0 -0
- package/docs/common/ai-integration.md +0 -0
- package/docs/common/cli-reference.md +0 -0
- package/docs/common/configuration.md +0 -0
- package/docs/common/getting-started.md +0 -0
- package/docs/common/troubleshooting.md +0 -0
- package/docs/common/workflows/bug-fix.md +0 -0
- package/docs/common/workflows/feature.md +0 -0
- package/docs/common/workflows/figma.md +0 -0
- package/docs/common/workflows/impact-analysis.md +0 -0
- package/docs/common/workflows/investigation.md +0 -0
- package/docs/common/workflows/refactor.md +0 -0
- package/docs/project/ARCHITECTURE.md +0 -0
- package/index.js +0 -0
- package/package.json +2 -2
- package/scripts/checkpoint.js +0 -0
- package/scripts/config.js +0 -0
- package/scripts/context.js +0 -0
- package/scripts/create-score-excel.js +0 -0
- package/scripts/detect.js +239 -239
- package/scripts/doctor.js +192 -218
- package/scripts/gitnexus-worker.js +0 -0
- package/scripts/guide.js +0 -0
- package/scripts/hooks/block-git-write.js +0 -0
- package/scripts/hooks/session-start.js +0 -0
- package/scripts/hooks/session-stop.js +0 -0
- package/scripts/init.js +1112 -1009
- package/scripts/link-resolver.js +0 -0
- package/scripts/memory.js +0 -0
- package/scripts/prompt.js +538 -538
- package/scripts/remove.js +0 -0
- package/scripts/task.js +546 -546
- package/scripts/telemetry/cli.js +0 -0
- package/scripts/telemetry/config.js +0 -0
- package/scripts/telemetry/crypto.js +0 -0
- package/scripts/telemetry/flush.js +0 -0
- package/scripts/telemetry/record.js +0 -0
- package/scripts/update.js +10 -7
- package/scripts/use.js +33 -12
- package/scripts/validate.js +0 -0
- package/upstream/.claude-plugin/marketplace.json +0 -0
- package/upstream/.claude-plugin/plugin.json +0 -0
- package/upstream/.codex/INSTALL.md +0 -0
- package/upstream/.cursor-plugin/plugin.json +0 -0
- package/upstream/.gitattributes +0 -0
- package/upstream/.github/FUNDING.yml +0 -0
- package/upstream/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- package/upstream/.github/ISSUE_TEMPLATE/config.yml +0 -0
- package/upstream/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- package/upstream/.github/ISSUE_TEMPLATE/platform_support.md +0 -0
- package/upstream/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- package/upstream/.opencode/INSTALL.md +0 -0
- package/upstream/.opencode/plugins/superpowers.js +0 -0
- package/upstream/.version-bump.json +0 -0
- package/upstream/AGENTS.md +0 -0
- package/upstream/CHANGELOG.md +0 -0
- package/upstream/CLAUDE.md +0 -0
- package/upstream/CODE_OF_CONDUCT.md +0 -0
- package/upstream/GEMINI.md +0 -0
- package/upstream/LICENSE +0 -0
- package/upstream/README.md +0 -0
- package/upstream/RELEASE-NOTES.md +0 -0
- package/upstream/agents/code-reviewer.md +0 -0
- package/upstream/commands/brainstorm.md +0 -0
- package/upstream/commands/execute-plan.md +0 -0
- package/upstream/commands/write-plan.md +0 -0
- package/upstream/docs/README.codex.md +0 -0
- package/upstream/docs/README.opencode.md +0 -0
- package/upstream/docs/plans/2025-11-22-opencode-support-design.md +0 -0
- package/upstream/docs/plans/2025-11-22-opencode-support-implementation.md +0 -0
- package/upstream/docs/plans/2025-11-28-skills-improvements-from-user-feedback.md +0 -0
- package/upstream/docs/plans/2026-01-17-visual-brainstorming.md +0 -0
- package/upstream/docs/superpowers/plans/2026-01-22-document-review-system.md +0 -0
- package/upstream/docs/superpowers/plans/2026-02-19-visual-brainstorming-refactor.md +0 -0
- package/upstream/docs/superpowers/plans/2026-03-11-zero-dep-brainstorm-server.md +0 -0
- package/upstream/docs/superpowers/plans/2026-03-23-codex-app-compatibility.md +0 -0
- package/upstream/docs/superpowers/specs/2026-01-22-document-review-system-design.md +0 -0
- package/upstream/docs/superpowers/specs/2026-02-19-visual-brainstorming-refactor-design.md +0 -0
- package/upstream/docs/superpowers/specs/2026-03-11-zero-dep-brainstorm-server-design.md +0 -0
- package/upstream/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md +0 -0
- package/upstream/docs/testing.md +0 -0
- package/upstream/docs/windows/polyglot-hooks.md +0 -0
- package/upstream/gemini-extension.json +0 -0
- package/upstream/hooks/hooks-cursor.json +0 -0
- package/upstream/hooks/hooks.json +0 -0
- package/upstream/hooks/run-hook.cmd +0 -0
- package/upstream/hooks/session-start +0 -0
- package/upstream/package.json +0 -0
- package/upstream/scripts/bump-version.sh +0 -0
- package/upstream/skills/brainstorming/SKILL.md +0 -0
- package/upstream/skills/brainstorming/scripts/frame-template.html +0 -0
- package/upstream/skills/brainstorming/scripts/helper.js +0 -0
- package/upstream/skills/brainstorming/scripts/server.cjs +0 -0
- package/upstream/skills/brainstorming/scripts/start-server.sh +0 -0
- package/upstream/skills/brainstorming/scripts/stop-server.sh +0 -0
- package/upstream/skills/brainstorming/spec-document-reviewer-prompt.md +0 -0
- package/upstream/skills/brainstorming/visual-companion.md +0 -0
- package/upstream/skills/dispatching-parallel-agents/SKILL.md +0 -0
- package/upstream/skills/executing-plans/SKILL.md +0 -0
- package/upstream/skills/finishing-a-development-branch/SKILL.md +0 -0
- package/upstream/skills/receiving-code-review/SKILL.md +0 -0
- package/upstream/skills/requesting-code-review/SKILL.md +0 -0
- package/upstream/skills/requesting-code-review/code-reviewer.md +0 -0
- package/upstream/skills/subagent-driven-development/SKILL.md +0 -0
- package/upstream/skills/subagent-driven-development/code-quality-reviewer-prompt.md +0 -0
- package/upstream/skills/subagent-driven-development/implementer-prompt.md +0 -0
- package/upstream/skills/subagent-driven-development/spec-reviewer-prompt.md +0 -0
- package/upstream/skills/systematic-debugging/CREATION-LOG.md +0 -0
- package/upstream/skills/systematic-debugging/SKILL.md +0 -0
- package/upstream/skills/systematic-debugging/condition-based-waiting-example.ts +0 -0
- package/upstream/skills/systematic-debugging/condition-based-waiting.md +0 -0
- package/upstream/skills/systematic-debugging/defense-in-depth.md +0 -0
- package/upstream/skills/systematic-debugging/find-polluter.sh +0 -0
- package/upstream/skills/systematic-debugging/root-cause-tracing.md +0 -0
- package/upstream/skills/systematic-debugging/test-academic.md +0 -0
- package/upstream/skills/systematic-debugging/test-pressure-1.md +0 -0
- package/upstream/skills/systematic-debugging/test-pressure-2.md +0 -0
- package/upstream/skills/systematic-debugging/test-pressure-3.md +0 -0
- package/upstream/skills/tdd-lean/SKILL.md +0 -0
- package/upstream/skills/test-driven-development/SKILL.md +0 -0
- package/upstream/skills/test-driven-development/testing-anti-patterns.md +0 -0
- package/upstream/skills/using-git-worktrees/SKILL.md +0 -0
- package/upstream/skills/using-superpowers/SKILL.md +0 -0
- package/upstream/skills/using-superpowers/references/codex-tools.md +0 -0
- package/upstream/skills/using-superpowers/references/copilot-tools.md +0 -0
- package/upstream/skills/using-superpowers/references/gemini-tools.md +0 -0
- package/upstream/skills/verification-before-completion/SKILL.md +0 -0
- package/upstream/skills/writing-plans/SKILL.md +0 -0
- package/upstream/skills/writing-plans/plan-document-reviewer-prompt.md +0 -0
- package/upstream/skills/writing-skills/SKILL.md +0 -0
- package/upstream/skills/writing-skills/anthropic-best-practices.md +0 -0
- package/upstream/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +0 -0
- package/upstream/skills/writing-skills/graphviz-conventions.dot +0 -0
- package/upstream/skills/writing-skills/persuasion-principles.md +0 -0
- package/upstream/skills/writing-skills/render-graphs.js +0 -0
- package/upstream/skills/writing-skills/testing-skills-with-subagents.md +0 -0
- package/upstream/tests/brainstorm-server/package-lock.json +0 -0
- package/upstream/tests/brainstorm-server/package.json +0 -0
- package/upstream/tests/brainstorm-server/server.test.js +0 -0
- package/upstream/tests/brainstorm-server/windows-lifecycle.test.sh +0 -0
- package/upstream/tests/brainstorm-server/ws-protocol.test.js +0 -0
- package/upstream/tests/claude-code/README.md +0 -0
- package/upstream/tests/claude-code/analyze-token-usage.py +0 -0
- package/upstream/tests/claude-code/run-skill-tests.sh +0 -0
- package/upstream/tests/claude-code/test-document-review-system.sh +0 -0
- package/upstream/tests/claude-code/test-helpers.sh +0 -0
- package/upstream/tests/claude-code/test-subagent-driven-development-integration.sh +0 -0
- package/upstream/tests/claude-code/test-subagent-driven-development.sh +0 -0
- package/upstream/tests/explicit-skill-requests/prompts/action-oriented.txt +0 -0
- package/upstream/tests/explicit-skill-requests/prompts/after-planning-flow.txt +0 -0
- package/upstream/tests/explicit-skill-requests/prompts/claude-suggested-it.txt +0 -0
- package/upstream/tests/explicit-skill-requests/prompts/i-know-what-sdd-means.txt +0 -0
- package/upstream/tests/explicit-skill-requests/prompts/mid-conversation-execute-plan.txt +0 -0
- package/upstream/tests/explicit-skill-requests/prompts/please-use-brainstorming.txt +0 -0
- package/upstream/tests/explicit-skill-requests/prompts/skip-formalities.txt +0 -0
- package/upstream/tests/explicit-skill-requests/prompts/subagent-driven-development-please.txt +0 -0
- package/upstream/tests/explicit-skill-requests/prompts/use-systematic-debugging.txt +0 -0
- package/upstream/tests/explicit-skill-requests/run-all.sh +0 -0
- package/upstream/tests/explicit-skill-requests/run-claude-describes-sdd.sh +0 -0
- package/upstream/tests/explicit-skill-requests/run-extended-multiturn-test.sh +0 -0
- package/upstream/tests/explicit-skill-requests/run-haiku-test.sh +0 -0
- package/upstream/tests/explicit-skill-requests/run-multiturn-test.sh +0 -0
- package/upstream/tests/explicit-skill-requests/run-test.sh +0 -0
- package/upstream/tests/opencode/run-tests.sh +0 -0
- package/upstream/tests/opencode/setup.sh +0 -0
- package/upstream/tests/opencode/test-plugin-loading.sh +0 -0
- package/upstream/tests/opencode/test-priority.sh +0 -0
- package/upstream/tests/opencode/test-tools.sh +0 -0
- package/upstream/tests/skill-triggering/prompts/dispatching-parallel-agents.txt +0 -0
- package/upstream/tests/skill-triggering/prompts/executing-plans.txt +0 -0
- package/upstream/tests/skill-triggering/prompts/requesting-code-review.txt +0 -0
- package/upstream/tests/skill-triggering/prompts/systematic-debugging.txt +0 -0
- package/upstream/tests/skill-triggering/prompts/test-driven-development.txt +0 -0
- package/upstream/tests/skill-triggering/prompts/writing-plans.txt +0 -0
- package/upstream/tests/skill-triggering/run-all.sh +0 -0
- package/upstream/tests/skill-triggering/run-test.sh +0 -0
- package/upstream/tests/subagent-driven-dev/go-fractals/design.md +0 -0
- package/upstream/tests/subagent-driven-dev/go-fractals/plan.md +0 -0
- package/upstream/tests/subagent-driven-dev/go-fractals/scaffold.sh +0 -0
- package/upstream/tests/subagent-driven-dev/run-test.sh +0 -0
- package/upstream/tests/subagent-driven-dev/svelte-todo/design.md +0 -0
- package/upstream/tests/subagent-driven-dev/svelte-todo/plan.md +0 -0
- package/upstream/tests/subagent-driven-dev/svelte-todo/scaffold.sh +0 -0
- 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
|
+
};
|