@relipa/ai-flow-kit 0.0.5-beta.1 → 0.0.6-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 +81 -69
- package/bin/aiflow.js +46 -2
- package/custom/rules/java/spring-boot-examples.md +329 -0
- package/custom/skills/read-study-requirement/SKILL.md +54 -0
- package/custom/skills/review-plan/SKILL.md +21 -0
- package/custom/templates/shared/gate-workflow.md +88 -88
- package/custom/templates/spring-boot.md +224 -523
- package/docs/common/AIFLOW.md +501 -462
- package/docs/common/CHANGELOG.md +162 -132
- package/docs/common/QUICK_START.md +44 -12
- package/docs/common/cli-reference.md +115 -6
- package/docs/common/getting-started.md +3 -0
- package/docs/project/ARCHITECTURE.md +28 -28
- package/package.json +6 -1
- package/scripts/doctor.js +89 -48
- package/scripts/guide.js +28 -5
- package/scripts/hooks/session-start.js +144 -45
- package/scripts/init.js +109 -50
- package/scripts/prompt.js +431 -431
- package/scripts/task.js +384 -0
- package/scripts/telemetry/cli.js +243 -243
- package/scripts/telemetry/config.js +91 -91
- package/scripts/telemetry/crypto.js +20 -20
- package/scripts/telemetry/flush.js +162 -162
- package/scripts/telemetry/record.js +138 -138
- package/scripts/use.js +94 -2
package/scripts/task.js
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
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
|
+
|
|
26
|
+
console.log(chalk.yellow('Usage:'));
|
|
27
|
+
console.log(chalk.gray(' aiflow task status — show active task and pending list'));
|
|
28
|
+
console.log(chalk.gray(' aiflow task list — list all saved tasks'));
|
|
29
|
+
console.log(chalk.gray(' aiflow task pause — pause current task'));
|
|
30
|
+
console.log(chalk.gray(' aiflow task pause --note "..." — pause with a note'));
|
|
31
|
+
console.log(chalk.gray(' aiflow task switch <ticket-id> — pause current + switch to another task'));
|
|
32
|
+
console.log(chalk.gray(' aiflow task resume <ticket-id> — resume a paused task'));
|
|
33
|
+
console.log(chalk.gray(' aiflow task reset <ticket-id> — reset task to Gate 1 (keeps context)'));
|
|
34
|
+
console.log(chalk.gray(' aiflow task remove <ticket-id> — permanently delete all task data'));
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ──────────────────────────────────────────────────────────────
|
|
41
|
+
// Commands
|
|
42
|
+
// ──────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
async function showStatus() {
|
|
45
|
+
const tasks = await loadAllTaskStates();
|
|
46
|
+
const active = tasks.filter(t => t.status === 'active');
|
|
47
|
+
const pending = tasks.filter(t => t.status === 'pending');
|
|
48
|
+
|
|
49
|
+
console.log(chalk.cyan('\nTask Status\n'));
|
|
50
|
+
|
|
51
|
+
if (active.length === 0) {
|
|
52
|
+
console.log(chalk.gray(' No active task. Run `aiflow use <ticket>` to start one.'));
|
|
53
|
+
} else {
|
|
54
|
+
const t = active[0];
|
|
55
|
+
console.log(` ${chalk.green('● Active')} ${chalk.white(t.taskId)} ${chalk.gray('Gate ' + t.currentGate)} ${chalk.gray(t.title.substring(0, 55))}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (pending.length > 0) {
|
|
59
|
+
console.log(chalk.yellow(`\n Pending (${pending.length}):`));
|
|
60
|
+
for (const t of pending) {
|
|
61
|
+
const paused = t.pausedAt ? chalk.gray(` paused ${formatRelative(t.pausedAt)}`) : '';
|
|
62
|
+
const note = t.notes ? chalk.gray(` — ${t.notes.substring(0, 40)}`) : '';
|
|
63
|
+
console.log(` ${chalk.gray('○ Pending')} ${chalk.white(t.taskId.padEnd(14))} Gate ${t.currentGate} ${chalk.gray(t.title.substring(0, 40))}${paused}${note}`);
|
|
64
|
+
}
|
|
65
|
+
console.log(chalk.gray('\n Resume with: aiflow task resume <ticket-id>'));
|
|
66
|
+
}
|
|
67
|
+
console.log();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function listTasks() {
|
|
71
|
+
const tasks = await loadAllTaskStates();
|
|
72
|
+
if (tasks.length === 0) {
|
|
73
|
+
console.log(chalk.gray('No saved tasks.'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
console.log(chalk.cyan('\nAll Tasks:\n'));
|
|
77
|
+
for (const t of tasks) {
|
|
78
|
+
const icon = t.status === 'active' ? chalk.green('●') : chalk.gray('○');
|
|
79
|
+
const label = t.status === 'active' ? chalk.green('active ') : chalk.yellow('pending');
|
|
80
|
+
console.log(` ${icon} ${label} ${chalk.white(t.taskId.padEnd(16))} Gate ${t.currentGate} ${chalk.gray(t.title.substring(0, 50))}`);
|
|
81
|
+
}
|
|
82
|
+
console.log();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function pauseTask(note = '') {
|
|
86
|
+
if (!(await fs.pathExists(CURRENT_FILE))) {
|
|
87
|
+
console.log(chalk.yellow('No active task to pause.'));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
|
|
92
|
+
if (!ctx || !ctx.taskId) {
|
|
93
|
+
console.log(chalk.yellow('Current context has no taskId — cannot pause.'));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const taskId = ctx.taskId;
|
|
98
|
+
const taskDir = path.join(TASKS_DIR, taskId);
|
|
99
|
+
await fs.ensureDir(taskDir);
|
|
100
|
+
|
|
101
|
+
// Save context snapshot
|
|
102
|
+
await fs.writeJson(path.join(taskDir, 'context.json'), ctx, { spaces: 2 });
|
|
103
|
+
|
|
104
|
+
// Detect gate from plan/ artifacts
|
|
105
|
+
const currentGate = await detectCurrentGate(taskId);
|
|
106
|
+
|
|
107
|
+
// Write / update task-state.json
|
|
108
|
+
const statePath = path.join(taskDir, 'task-state.json');
|
|
109
|
+
const existing = (await fs.pathExists(statePath)) ? await fs.readJson(statePath).catch(() => ({})) : {};
|
|
110
|
+
const now = new Date().toISOString();
|
|
111
|
+
|
|
112
|
+
const taskState = {
|
|
113
|
+
...existing,
|
|
114
|
+
taskId,
|
|
115
|
+
title: ctx.title || '',
|
|
116
|
+
status: 'pending',
|
|
117
|
+
createdAt: existing.createdAt || now,
|
|
118
|
+
updatedAt: now,
|
|
119
|
+
pausedAt: now,
|
|
120
|
+
currentGate,
|
|
121
|
+
gateApprovals: existing.gateApprovals || {},
|
|
122
|
+
notes: note || existing.notes || '',
|
|
123
|
+
};
|
|
124
|
+
await fs.writeJson(statePath, taskState, { spaces: 2 });
|
|
125
|
+
|
|
126
|
+
// Remove current.json so no task is active
|
|
127
|
+
await fs.remove(CURRENT_FILE);
|
|
128
|
+
|
|
129
|
+
console.log(chalk.green(`✓ Task ${taskId} paused at Gate ${currentGate}.`));
|
|
130
|
+
if (note) console.log(chalk.gray(` Note: ${note}`));
|
|
131
|
+
console.log(chalk.gray(` Resume later: aiflow task resume ${taskId}`));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function switchTask(targetId) {
|
|
135
|
+
// Pause current task if one is active
|
|
136
|
+
if (await fs.pathExists(CURRENT_FILE)) {
|
|
137
|
+
const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
|
|
138
|
+
if (ctx && ctx.taskId && ctx.taskId !== targetId) {
|
|
139
|
+
console.log(chalk.gray(` Pausing current task: ${ctx.taskId}`));
|
|
140
|
+
await pauseTask();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Load target task if it exists
|
|
145
|
+
const statePath = path.join(TASKS_DIR, targetId, 'task-state.json');
|
|
146
|
+
if (await fs.pathExists(statePath)) {
|
|
147
|
+
await resumeTask(targetId);
|
|
148
|
+
} else {
|
|
149
|
+
console.log(chalk.yellow(`Task ${targetId} not found in saved tasks.`));
|
|
150
|
+
console.log(chalk.gray(` Run 'aiflow use ${targetId}' to load it from your ticket system.`));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function resumeTask(taskId) {
|
|
155
|
+
const taskDir = path.join(TASKS_DIR, taskId);
|
|
156
|
+
const statePath = path.join(taskDir, 'task-state.json');
|
|
157
|
+
const ctxPath = path.join(taskDir, 'context.json');
|
|
158
|
+
|
|
159
|
+
if (!(await fs.pathExists(statePath))) {
|
|
160
|
+
console.log(chalk.red(`Task not found: ${taskId}`));
|
|
161
|
+
console.log(chalk.gray(' Run `aiflow task list` to see available tasks.'));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const taskState = await fs.readJson(statePath);
|
|
166
|
+
const ctx = await fs.readJson(ctxPath).catch(() => null);
|
|
167
|
+
|
|
168
|
+
if (!ctx) {
|
|
169
|
+
console.log(chalk.red(`Context snapshot missing for task: ${taskId}`));
|
|
170
|
+
console.log(chalk.gray(` Try loading it fresh: aiflow use ${taskId}`));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Restore context as current
|
|
175
|
+
await fs.ensureDir(CONTEXT_DIR);
|
|
176
|
+
await fs.writeJson(CURRENT_FILE, ctx, { spaces: 2 });
|
|
177
|
+
|
|
178
|
+
// Mark as active
|
|
179
|
+
const now = new Date().toISOString();
|
|
180
|
+
taskState.status = 'active';
|
|
181
|
+
taskState.pausedAt = null;
|
|
182
|
+
taskState.updatedAt = now;
|
|
183
|
+
await fs.writeJson(statePath, taskState, { spaces: 2 });
|
|
184
|
+
|
|
185
|
+
console.log(chalk.green(`✓ Resumed task: ${taskId}`));
|
|
186
|
+
console.log(` ${chalk.white('Gate:')} ${taskState.currentGate} (${gateLabel(taskState.currentGate)})`);
|
|
187
|
+
console.log(` ${chalk.white('Title:')} ${ctx.title.substring(0, 70)}`);
|
|
188
|
+
if (taskState.notes) console.log(` ${chalk.white('Note:')} ${taskState.notes}`);
|
|
189
|
+
console.log(chalk.gray('\n Open Claude to resume: claude'));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function resetTask(taskId) {
|
|
193
|
+
const taskDir = path.join(TASKS_DIR, taskId);
|
|
194
|
+
const statePath = path.join(taskDir, 'task-state.json');
|
|
195
|
+
|
|
196
|
+
if (!(await fs.pathExists(statePath))) {
|
|
197
|
+
console.log(chalk.red(`Task not found: ${taskId}`));
|
|
198
|
+
console.log(chalk.gray(' Run `aiflow task list` to see available tasks.'));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const taskState = await fs.readJson(statePath);
|
|
203
|
+
const planDir = path.join(PROJECT_DIR, 'plan', taskId);
|
|
204
|
+
const planFiles = [];
|
|
205
|
+
if (await fs.pathExists(planDir)) {
|
|
206
|
+
const entries = await fs.readdir(planDir);
|
|
207
|
+
for (const f of entries) planFiles.push(path.join('plan', taskId, f));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Show what will happen
|
|
211
|
+
console.log(chalk.cyan(`\nReset task: ${taskId}\n`));
|
|
212
|
+
console.log(chalk.white(' The following will be reset:'));
|
|
213
|
+
console.log(chalk.gray(` • .aiflow/tasks/${taskId}/task-state.json`));
|
|
214
|
+
console.log(chalk.gray(` Gate: ${taskState.currentGate} → 1 | Approvals cleared | Status: active`));
|
|
215
|
+
|
|
216
|
+
const deletePlan = planFiles.length > 0 && await confirm({
|
|
217
|
+
message: `Also delete ${planFiles.length} plan file(s) in plan/${taskId}/?`,
|
|
218
|
+
default: false,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (deletePlan) {
|
|
222
|
+
console.log(chalk.gray('\n Plan files to delete:'));
|
|
223
|
+
for (const f of planFiles) console.log(chalk.gray(` • ${f}`));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log();
|
|
227
|
+
const ok = await confirm({ message: 'Proceed with reset?', default: false });
|
|
228
|
+
if (!ok) { console.log(chalk.gray('Cancelled.')); return; }
|
|
229
|
+
|
|
230
|
+
// Reset task-state.json to Gate 1
|
|
231
|
+
const now = new Date().toISOString();
|
|
232
|
+
await fs.writeJson(statePath, {
|
|
233
|
+
...taskState,
|
|
234
|
+
status: 'active',
|
|
235
|
+
currentGate: 1,
|
|
236
|
+
gateApprovals: {},
|
|
237
|
+
pausedAt: null,
|
|
238
|
+
updatedAt: now,
|
|
239
|
+
notes: '',
|
|
240
|
+
}, { spaces: 2 });
|
|
241
|
+
|
|
242
|
+
// Restore as current context
|
|
243
|
+
const ctxPath = path.join(taskDir, 'context.json');
|
|
244
|
+
if (await fs.pathExists(ctxPath)) {
|
|
245
|
+
await fs.ensureDir(CONTEXT_DIR);
|
|
246
|
+
await fs.copy(ctxPath, CURRENT_FILE, { overwrite: true });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (deletePlan) await fs.remove(planDir);
|
|
250
|
+
|
|
251
|
+
console.log(chalk.green(`✓ Task ${taskId} reset to Gate 1.`));
|
|
252
|
+
if (deletePlan) console.log(chalk.gray(` Deleted plan/${taskId}/`));
|
|
253
|
+
console.log(chalk.gray(' Open Claude to start Gate 1: claude'));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function removeTask(taskId) {
|
|
257
|
+
const taskDir = path.join(TASKS_DIR, taskId);
|
|
258
|
+
const statePath = path.join(taskDir, 'task-state.json');
|
|
259
|
+
|
|
260
|
+
if (!(await fs.pathExists(statePath))) {
|
|
261
|
+
console.log(chalk.red(`Task not found: ${taskId}`));
|
|
262
|
+
console.log(chalk.gray(' Run `aiflow task list` to see available tasks.'));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const isActive = await fs.pathExists(CURRENT_FILE) &&
|
|
267
|
+
((await fs.readJson(CURRENT_FILE).catch(() => ({}))).taskId === taskId);
|
|
268
|
+
const planDir = path.join(PROJECT_DIR, 'plan', taskId);
|
|
269
|
+
const hasPlan = await fs.pathExists(planDir);
|
|
270
|
+
const planFiles = hasPlan ? await fs.readdir(planDir) : [];
|
|
271
|
+
|
|
272
|
+
// Show what will be deleted
|
|
273
|
+
console.log(chalk.cyan(`\nRemove task: ${taskId}\n`));
|
|
274
|
+
console.log(chalk.white(' The following will be permanently deleted:'));
|
|
275
|
+
console.log(chalk.gray(` • .aiflow/tasks/${taskId}/ (context snapshot + gate state)`));
|
|
276
|
+
if (isActive) console.log(chalk.gray(` • .aiflow/context/current.json (this is the active task)`));
|
|
277
|
+
if (hasPlan) {
|
|
278
|
+
console.log(chalk.gray(` • plan/${taskId}/ (${planFiles.length} file(s)):`));
|
|
279
|
+
for (const f of planFiles) console.log(chalk.gray(` - plan/${taskId}/${f}`));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
console.log();
|
|
283
|
+
const ok = await confirm({
|
|
284
|
+
message: chalk.red('This cannot be undone. Proceed?'),
|
|
285
|
+
default: false,
|
|
286
|
+
});
|
|
287
|
+
if (!ok) { console.log(chalk.gray('Cancelled.')); return; }
|
|
288
|
+
|
|
289
|
+
await fs.remove(taskDir);
|
|
290
|
+
if (isActive) await fs.remove(CURRENT_FILE);
|
|
291
|
+
if (hasPlan) await fs.remove(planDir);
|
|
292
|
+
|
|
293
|
+
console.log(chalk.green(`✓ Task ${taskId} removed.`));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ──────────────────────────────────────────────────────────────
|
|
297
|
+
// Internal helpers
|
|
298
|
+
// ──────────────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
async function loadAllTaskStates() {
|
|
301
|
+
if (!(await fs.pathExists(TASKS_DIR))) return [];
|
|
302
|
+
const entries = await fs.readdir(TASKS_DIR, { withFileTypes: true });
|
|
303
|
+
const states = [];
|
|
304
|
+
for (const entry of entries) {
|
|
305
|
+
if (!entry.isDirectory()) continue;
|
|
306
|
+
const statePath = path.join(TASKS_DIR, entry.name, 'task-state.json');
|
|
307
|
+
if (await fs.pathExists(statePath)) {
|
|
308
|
+
const s = await fs.readJson(statePath).catch(() => null);
|
|
309
|
+
if (s) states.push(s);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return states.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function detectCurrentGate(taskId) {
|
|
316
|
+
const planDir = path.join(PROJECT_DIR, 'plan', taskId);
|
|
317
|
+
if (!(await fs.pathExists(planDir))) return 1;
|
|
318
|
+
if (await fs.pathExists(path.join(planDir, 'summary.md'))) return 5;
|
|
319
|
+
if (await fs.pathExists(path.join(planDir, 'plan.md'))) return 3;
|
|
320
|
+
if (await fs.pathExists(path.join(planDir, 'requirement.md'))) return 2;
|
|
321
|
+
return 1;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function gateLabel(n) {
|
|
325
|
+
const labels = {
|
|
326
|
+
1: 'AI Analyze Requirement',
|
|
327
|
+
2: 'Implementation Plan',
|
|
328
|
+
3: 'Code Generation',
|
|
329
|
+
4: 'AI Self-Review',
|
|
330
|
+
5: 'Peer Review & PR',
|
|
331
|
+
};
|
|
332
|
+
return labels[n] || `Gate ${n}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function formatRelative(isoStr) {
|
|
336
|
+
const ms = Date.now() - new Date(isoStr).getTime();
|
|
337
|
+
const mins = Math.floor(ms / 60000);
|
|
338
|
+
if (mins < 60) return `${mins}m ago`;
|
|
339
|
+
const hrs = Math.floor(mins / 60);
|
|
340
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
341
|
+
return `${Math.floor(hrs / 24)}d ago`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ──────────────────────────────────────────────────────────────
|
|
345
|
+
// Export helper used by session-start hook and use.js
|
|
346
|
+
// ──────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
module.exports.getActiveTaskState = async function getActiveTaskState() {
|
|
349
|
+
if (!(await fs.pathExists(CURRENT_FILE))) return null;
|
|
350
|
+
const ctx = await fs.readJson(CURRENT_FILE).catch(() => null);
|
|
351
|
+
if (!ctx || !ctx.taskId) return null;
|
|
352
|
+
const statePath = path.join(TASKS_DIR, ctx.taskId, 'task-state.json');
|
|
353
|
+
return (await fs.pathExists(statePath)) ? fs.readJson(statePath).catch(() => null) : null;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
module.exports.createOrActivateTaskState = async function createOrActivateTaskState(ctx) {
|
|
357
|
+
const taskId = ctx.taskId;
|
|
358
|
+
if (!taskId) return;
|
|
359
|
+
const taskDir = path.join(TASKS_DIR, taskId);
|
|
360
|
+
const statePath = path.join(taskDir, 'task-state.json');
|
|
361
|
+
await fs.ensureDir(taskDir);
|
|
362
|
+
|
|
363
|
+
// Save context snapshot
|
|
364
|
+
await fs.writeJson(path.join(taskDir, 'context.json'), ctx, { spaces: 2 });
|
|
365
|
+
|
|
366
|
+
// Detect current gate
|
|
367
|
+
const currentGate = await detectCurrentGate(taskId);
|
|
368
|
+
|
|
369
|
+
const existing = (await fs.pathExists(statePath)) ? await fs.readJson(statePath).catch(() => ({})) : {};
|
|
370
|
+
const now = new Date().toISOString();
|
|
371
|
+
const taskState = {
|
|
372
|
+
...existing,
|
|
373
|
+
taskId,
|
|
374
|
+
title: ctx.title || '',
|
|
375
|
+
status: 'active',
|
|
376
|
+
createdAt: existing.createdAt || now,
|
|
377
|
+
updatedAt: now,
|
|
378
|
+
pausedAt: null,
|
|
379
|
+
currentGate: existing.currentGate || currentGate,
|
|
380
|
+
gateApprovals: existing.gateApprovals || {},
|
|
381
|
+
notes: existing.notes || '',
|
|
382
|
+
};
|
|
383
|
+
await fs.writeJson(statePath, taskState, { spaces: 2 });
|
|
384
|
+
};
|