ai-growth 1.1.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/.env.example +19 -0
- package/AGENTS.md +232 -0
- package/IMPLEMENTATION_PLAN.md +207 -0
- package/LICENSE +21 -0
- package/PRODUCT_SPEC.md +1776 -0
- package/README.md +384 -0
- package/SKILL.md +121 -0
- package/START-HERE.md +156 -0
- package/dist/api/app.js +206 -0
- package/dist/api/server.js +42 -0
- package/dist/cli/main.js +799 -0
- package/dist/core/actions.js +264 -0
- package/dist/core/context.js +7 -0
- package/dist/core/directions.js +26 -0
- package/dist/core/evidence.js +155 -0
- package/dist/core/goals.js +188 -0
- package/dist/core/lifeContext.js +41 -0
- package/dist/core/memory.js +35 -0
- package/dist/core/metrics.js +74 -0
- package/dist/core/profile.js +95 -0
- package/dist/core/progress.js +244 -0
- package/dist/core/prompts/index.js +40 -0
- package/dist/core/replan.js +100 -0
- package/dist/core/rollover.js +68 -0
- package/dist/core/rolloverTypes.js +1 -0
- package/dist/core/stateMachine.js +61 -0
- package/dist/core/status.js +57 -0
- package/dist/core/types.js +9 -0
- package/dist/daemon/main.js +64 -0
- package/dist/db/cli.js +19 -0
- package/dist/db/client.js +36 -0
- package/dist/db/migrate.js +34 -0
- package/dist/db/migrations/V1__init.sql +249 -0
- package/dist/device/card.js +257 -0
- package/dist/device/deck.js +157 -0
- package/dist/device/fontCoverage.js +162 -0
- package/dist/device/http.js +170 -0
- package/dist/device/png.js +111 -0
- package/dist/device/provision.js +185 -0
- package/dist/device/render.js +194 -0
- package/dist/device/renderCard.js +48 -0
- package/dist/device/report.js +146 -0
- package/dist/device/serial.js +201 -0
- package/dist/device/token.js +78 -0
- package/dist/device/types.js +2 -0
- package/dist/mcp/ctx.js +48 -0
- package/dist/mcp/server.js +15 -0
- package/dist/mcp/tools.js +586 -0
- package/dist/passport/builder.js +83 -0
- package/dist/passport/state.js +81 -0
- package/dist/passport/sync.js +61 -0
- package/dist/reminder/notify.js +30 -0
- package/dist/reminder/scheduler.js +87 -0
- package/dist/reminder/service.js +102 -0
- package/dist/shared/config.js +50 -0
- package/dist/shared/result.js +27 -0
- package/dist/shared/time.js +67 -0
- package/dist/shared/util.js +21 -0
- package/dist/ui/app.js +500 -0
- package/dist/ui/index.html +38 -0
- package/dist/ui/style.css +367 -0
- package/docs/images/cards.png +0 -0
- package/docs/images/cover.png +0 -0
- package/launchd/com.lairey.ai-growth.daemon.plist +45 -0
- package/llms.txt +52 -0
- package/package.json +79 -0
- package/scripts/check-device-cards.mjs +177 -0
- package/scripts/doctor.sh +194 -0
- package/scripts/gen-device-assets.mjs +188 -0
- package/scripts/install.sh +291 -0
- package/scripts/preview-cards.mjs +120 -0
- package/scripts/reset-dev-data.sh +92 -0
- package/scripts/smoke-device.mjs +401 -0
- package/scripts/uninstall.sh +63 -0
- package/server.json +56 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { newId, ts } from '../shared/util.js';
|
|
2
|
+
import { todayIn } from './profile.js';
|
|
3
|
+
export function getActiveLifeContext(ctx) {
|
|
4
|
+
const today = todayIn(ctx);
|
|
5
|
+
// active 且窗口覆盖今天(无 end 或 end >= today)
|
|
6
|
+
const r = ctx.db
|
|
7
|
+
.prepare(`SELECT * FROM life_contexts WHERE active=1 AND start_date <= ?
|
|
8
|
+
AND (expected_end_date IS NULL OR expected_end_date >= ?)
|
|
9
|
+
ORDER BY created_at DESC LIMIT 1`)
|
|
10
|
+
.get(today, today);
|
|
11
|
+
return r ? rowToLifeContext(r) : null;
|
|
12
|
+
}
|
|
13
|
+
function rowToLifeContext(r) {
|
|
14
|
+
return {
|
|
15
|
+
id: r.id, mode: r.mode, availableMinutesPerDay: r.available_minutes_per_day ?? undefined,
|
|
16
|
+
startDate: r.start_date, expectedEndDate: r.expected_end_date ?? undefined, note: r.note ?? undefined,
|
|
17
|
+
confirmed: !!r.confirmed, active: !!r.active,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** 设置 LifeContext(§19)。同一时间仅一个 active;新的设置使旧的失效 */
|
|
21
|
+
export function setLifeContext(ctx, input) {
|
|
22
|
+
const now = ts();
|
|
23
|
+
const id = newId();
|
|
24
|
+
const tx = ctx.db.transaction(() => {
|
|
25
|
+
ctx.db.prepare('UPDATE life_contexts SET active=0, updated_at=? WHERE active=1').run(now);
|
|
26
|
+
ctx.db
|
|
27
|
+
.prepare(`INSERT INTO life_contexts (id, mode, available_minutes_per_day, start_date, expected_end_date, note, confirmed, active, created_at, updated_at)
|
|
28
|
+
VALUES (?,?,?,?,?,?,?,?,?,?)`)
|
|
29
|
+
.run(id, input.mode, input.availableMinutesPerDay ?? null, input.startDate ?? todayIn(ctx), input.expectedEndDate ?? null, input.note ?? null, input.confirmed === false ? 0 : 1, 1, now, now);
|
|
30
|
+
});
|
|
31
|
+
tx();
|
|
32
|
+
return getActiveLifeContext(ctx);
|
|
33
|
+
}
|
|
34
|
+
/** LifeContext 到期检查:过期后返回 null(Agent 应主动询问是否恢复) */
|
|
35
|
+
export function deactivateExpiredLifeContexts(ctx) {
|
|
36
|
+
const today = todayIn(ctx);
|
|
37
|
+
const res = ctx.db
|
|
38
|
+
.prepare('UPDATE life_contexts SET active=0, updated_at=? WHERE active=1 AND expected_end_date IS NOT NULL AND expected_end_date < ?')
|
|
39
|
+
.run(ts(), today);
|
|
40
|
+
return res.changes;
|
|
41
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { newId, ts, pj } from '../shared/util.js';
|
|
2
|
+
function rowToMemory(r) {
|
|
3
|
+
return {
|
|
4
|
+
id: r.id, type: r.type, content: r.content, confidence: r.confidence,
|
|
5
|
+
evidenceRefs: pj(r.evidence_refs, []), confirmedByUser: !!r.confirmed_by_user, createdAt: r.created_at,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
/** 重要推断需用户确认后 confirmedByUser=1;不得把 AI 推断直接永久写成事实 */
|
|
9
|
+
export function addMemory(ctx, input) {
|
|
10
|
+
const id = newId();
|
|
11
|
+
const now = ts();
|
|
12
|
+
const confidence = input.confidence ?? (input.confirmedByUser ? 1.0 : 0.5);
|
|
13
|
+
ctx.db
|
|
14
|
+
.prepare(`INSERT INTO memories (id, type, content, confidence, evidence_refs, confirmed_by_user, created_at, updated_at)
|
|
15
|
+
VALUES (?,?,?,?,?,?,?,?)`)
|
|
16
|
+
.run(id, input.type, input.content, confidence, JSON.stringify(input.evidenceRefs ?? []), input.confirmedByUser ? 1 : 0, now, now);
|
|
17
|
+
return getMemory(ctx, id);
|
|
18
|
+
}
|
|
19
|
+
export function getMemory(ctx, id) {
|
|
20
|
+
const r = ctx.db.prepare('SELECT * FROM memories WHERE id=? AND deleted_at IS NULL').get(id);
|
|
21
|
+
return r ? rowToMemory(r) : null;
|
|
22
|
+
}
|
|
23
|
+
export function confirmMemory(ctx, id) {
|
|
24
|
+
ctx.db.prepare('UPDATE memories SET confirmed_by_user=1, confidence=1.0, updated_at=? WHERE id=?').run(ts(), id);
|
|
25
|
+
return getMemory(ctx, id);
|
|
26
|
+
}
|
|
27
|
+
export function listMemories(ctx, opts = {}) {
|
|
28
|
+
const rows = opts.type
|
|
29
|
+
? ctx.db.prepare('SELECT * FROM memories WHERE deleted_at IS NULL AND type=? ORDER BY created_at DESC LIMIT ?').all(opts.type, opts.limit ?? 100)
|
|
30
|
+
: ctx.db.prepare('SELECT * FROM memories WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT ?').all(opts.limit ?? 100);
|
|
31
|
+
return rows.map(rowToMemory);
|
|
32
|
+
}
|
|
33
|
+
export function deleteMemory(ctx, id) {
|
|
34
|
+
ctx.db.prepare('UPDATE memories SET deleted_at=?, updated_at=? WHERE id=?').run(ts(), ts(), id);
|
|
35
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { todayIn, resolveTimezone } from './profile.js';
|
|
2
|
+
import { localDateKey } from '../shared/time.js';
|
|
3
|
+
/** 本地指标(§30)。用于 Planner 改善,不是给用户打分。 */
|
|
4
|
+
export function computeMetrics(ctx, windowDays = 14) {
|
|
5
|
+
const tz = resolveTimezone(ctx);
|
|
6
|
+
const since = localDateKey(new Date(Date.now() - windowDays * 86400_000), tz);
|
|
7
|
+
const count = (sql, ...params) => ctx.db.prepare(sql).get(...params).n;
|
|
8
|
+
// 分母口径:窗口内所有"已实际排入计划"的 action(排除 DRAFT 草稿),
|
|
9
|
+
// 分子:COMPLETED。MAIN 与 DAILY 使用同一口径,保证可比。
|
|
10
|
+
const plannedFilter = `date_key >= ? AND deleted_at IS NULL AND status != 'DRAFT'`;
|
|
11
|
+
const mainTotal = count(`SELECT COUNT(*) AS n FROM actions WHERE kind='MAIN_QUEST' AND ${plannedFilter}`, since);
|
|
12
|
+
const mainDone = count(`SELECT COUNT(*) AS n FROM actions WHERE kind='MAIN_QUEST' AND ${plannedFilter} AND status='COMPLETED'`, since);
|
|
13
|
+
const mainReplan = count(`SELECT COUNT(*) AS n FROM actions WHERE kind='MAIN_QUEST' AND ${plannedFilter}
|
|
14
|
+
AND status IN ('PENDING_REPLAN','DELAYED')`, since);
|
|
15
|
+
const dailyTotal = count(`SELECT COUNT(*) AS n FROM actions WHERE kind='DAILY' AND ${plannedFilter}`, since);
|
|
16
|
+
const dailyDone = count(`SELECT COUNT(*) AS n FROM actions WHERE kind='DAILY' AND ${plannedFilter} AND status='COMPLETED'`, since);
|
|
17
|
+
const feedbackRows = ctx.db
|
|
18
|
+
.prepare(`SELECT tf.reason AS reason, COUNT(*) AS n FROM task_feedback tf
|
|
19
|
+
JOIN actions a ON a.id = tf.action_id WHERE a.date_key >= ? GROUP BY tf.reason`)
|
|
20
|
+
.all(since);
|
|
21
|
+
const skipFeedbackDistribution = {};
|
|
22
|
+
for (const r of feedbackRows)
|
|
23
|
+
skipFeedbackDistribution[r.reason] = r.n;
|
|
24
|
+
// Accepted Suggestion Rate / Modification Rate 的分母必须是「已确认的计划」——
|
|
25
|
+
// 尚未确认的计划不构成一次"用户是否接受 AI 建议"的样本,计入会稀释比例。
|
|
26
|
+
const plansConfirmed = count(`SELECT COUNT(*) AS n FROM daily_plans WHERE date_key >= ? AND confirmed_at IS NOT NULL`, since);
|
|
27
|
+
const plansConfirmedUnmodified = count(`SELECT COUNT(*) AS n FROM daily_plans p WHERE p.date_key >= ? AND p.confirmed_at IS NOT NULL
|
|
28
|
+
AND NOT EXISTS (
|
|
29
|
+
SELECT 1 FROM audit_log al WHERE al.entity_type='action'
|
|
30
|
+
AND al.created_at < p.confirmed_at
|
|
31
|
+
AND al.created_at > p.created_at
|
|
32
|
+
AND al.reason LIKE 'agent update%'
|
|
33
|
+
)`, since);
|
|
34
|
+
const acceptedSuggestionRate = plansConfirmed > 0 ? plansConfirmedUnmodified / plansConfirmed : null;
|
|
35
|
+
const modificationRate = acceptedSuggestionRate === null ? null : 1 - acceptedSuggestionRate;
|
|
36
|
+
const sent = count(`SELECT COUNT(*) AS n FROM reminders WHERE status='SENT' AND fired_at >= ?`, new Date(Date.now() - windowDays * 86400_000).toISOString());
|
|
37
|
+
const followed = count(`SELECT COUNT(*) AS n FROM reminders r JOIN actions a ON a.id = r.action_id
|
|
38
|
+
WHERE r.status='SENT' AND r.fired_at >= ? AND a.status='COMPLETED'`, new Date(Date.now() - windowDays * 86400_000).toISOString());
|
|
39
|
+
return {
|
|
40
|
+
windowDays,
|
|
41
|
+
completionRate: {
|
|
42
|
+
main: mainTotal > 0 ? mainDone / mainTotal : null,
|
|
43
|
+
daily: dailyTotal > 0 ? dailyDone / dailyTotal : null,
|
|
44
|
+
},
|
|
45
|
+
replanRate: mainTotal > 0 ? mainReplan / mainTotal : null,
|
|
46
|
+
modificationRate,
|
|
47
|
+
skipFeedbackDistribution,
|
|
48
|
+
reminderResponse: { sent, followedByCompletion: followed },
|
|
49
|
+
acceptedSuggestionRate,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Planner 上下文打包:给 Agent 生成 Daily Plan 用的全部结构化输入(§10) */
|
|
53
|
+
export function plannerContext(ctx) {
|
|
54
|
+
const today = todayIn(ctx);
|
|
55
|
+
const yesterdayKeyRow = ctx.db
|
|
56
|
+
.prepare(`SELECT MAX(date_key) AS k FROM actions WHERE date_key < ?`)
|
|
57
|
+
.get(today);
|
|
58
|
+
const yesterday = yesterdayKeyRow.k;
|
|
59
|
+
const tz = resolveTimezone(ctx);
|
|
60
|
+
return {
|
|
61
|
+
today,
|
|
62
|
+
timezone: tz,
|
|
63
|
+
yesterdayActions: yesterday
|
|
64
|
+
? ctx.db.prepare(`SELECT id, kind, title, status, date_key FROM actions WHERE date_key=? ORDER BY kind DESC`).all(yesterday)
|
|
65
|
+
: [],
|
|
66
|
+
recentEvidence: ctx.db
|
|
67
|
+
.prepare(`SELECT id, type, strength, content, created_at FROM evidence ORDER BY created_at DESC LIMIT 10`)
|
|
68
|
+
.all(),
|
|
69
|
+
recentFeedback: ctx.db
|
|
70
|
+
.prepare(`SELECT tf.reason, tf.comment, a.title FROM task_feedback tf JOIN actions a ON a.id=tf.action_id ORDER BY tf.created_at DESC LIMIT 10`)
|
|
71
|
+
.all(),
|
|
72
|
+
metrics: computeMetrics(ctx, 14),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { newId, ts, pj } from '../shared/util.js';
|
|
2
|
+
import { todayKey } from '../shared/time.js';
|
|
3
|
+
function rowToProfile(r) {
|
|
4
|
+
return {
|
|
5
|
+
id: r.id,
|
|
6
|
+
displayName: r.display_name ?? undefined,
|
|
7
|
+
timezone: r.timezone,
|
|
8
|
+
onboardingStatus: r.onboarding_status,
|
|
9
|
+
timePreferences: pj(r.time_preferences, {}),
|
|
10
|
+
taskCapacity: pj(r.task_capacity, {}),
|
|
11
|
+
learningPreferences: pj(r.learning_preferences, {}),
|
|
12
|
+
reminderPreferences: pj(r.reminder_preferences, {}),
|
|
13
|
+
constraints: pj(r.constraints, {}),
|
|
14
|
+
createdAt: r.created_at,
|
|
15
|
+
updatedAt: r.updated_at,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
// ---------- profile service ----------
|
|
19
|
+
export function getProfile(ctx) {
|
|
20
|
+
const row = ctx.db
|
|
21
|
+
.prepare('SELECT * FROM profiles WHERE deleted_at IS NULL ORDER BY created_at LIMIT 1')
|
|
22
|
+
.get();
|
|
23
|
+
return row ? rowToProfile(row) : null;
|
|
24
|
+
}
|
|
25
|
+
export function ensureProfile(ctx, timezone) {
|
|
26
|
+
const existing = getProfile(ctx);
|
|
27
|
+
if (existing)
|
|
28
|
+
return existing;
|
|
29
|
+
const id = newId();
|
|
30
|
+
const now = ts();
|
|
31
|
+
ctx.db
|
|
32
|
+
.prepare(`INSERT INTO profiles (id, timezone, onboarding_status, created_at, updated_at)
|
|
33
|
+
VALUES (?, ?, 'NOT_STARTED', ?, ?)`)
|
|
34
|
+
.run(id, timezone ?? ctx.tz, now, now);
|
|
35
|
+
return getProfile(ctx);
|
|
36
|
+
}
|
|
37
|
+
export function updateProfile(ctx, input) {
|
|
38
|
+
const current = getProfile(ctx);
|
|
39
|
+
if (!current)
|
|
40
|
+
throw new Error('Profile does not exist; call ensureProfile first');
|
|
41
|
+
const next = {
|
|
42
|
+
displayName: input.displayName ?? current.displayName,
|
|
43
|
+
timezone: input.timezone ?? current.timezone,
|
|
44
|
+
onboardingStatus: input.onboardingStatus ?? current.onboardingStatus,
|
|
45
|
+
timePreferences: input.timePreferences ?? current.timePreferences,
|
|
46
|
+
taskCapacity: input.taskCapacity ?? current.taskCapacity,
|
|
47
|
+
learningPreferences: input.learningPreferences ?? current.learningPreferences,
|
|
48
|
+
reminderPreferences: input.reminderPreferences ?? current.reminderPreferences,
|
|
49
|
+
constraints: input.constraints ?? current.constraints,
|
|
50
|
+
};
|
|
51
|
+
ctx.db
|
|
52
|
+
.prepare(`UPDATE profiles SET display_name=?, timezone=?, onboarding_status=?, time_preferences=?,
|
|
53
|
+
task_capacity=?, learning_preferences=?, reminder_preferences=?, constraints=?, updated_at=?
|
|
54
|
+
WHERE id=?`)
|
|
55
|
+
.run(next.displayName ?? null, next.timezone, next.onboardingStatus, JSON.stringify(next.timePreferences), JSON.stringify(next.taskCapacity), JSON.stringify(next.learningPreferences), JSON.stringify(next.reminderPreferences), JSON.stringify(next.constraints), ts(), current.id);
|
|
56
|
+
return getProfile(ctx);
|
|
57
|
+
}
|
|
58
|
+
/** 解决"当前时区":settings.timezone > profile.timezone > ctx.tz */
|
|
59
|
+
export function resolveTimezone(ctx) {
|
|
60
|
+
const s = getSetting(ctx, 'timezone');
|
|
61
|
+
if (s)
|
|
62
|
+
return s;
|
|
63
|
+
const p = getProfile(ctx);
|
|
64
|
+
if (p?.timezone)
|
|
65
|
+
return p.timezone;
|
|
66
|
+
return ctx.tz;
|
|
67
|
+
}
|
|
68
|
+
// ---------- settings ----------
|
|
69
|
+
export function getSetting(ctx, key) {
|
|
70
|
+
const row = ctx.db.prepare('SELECT value FROM settings WHERE key=?').get(key);
|
|
71
|
+
return row?.value ?? null;
|
|
72
|
+
}
|
|
73
|
+
export function setSetting(ctx, key, value) {
|
|
74
|
+
ctx.db
|
|
75
|
+
.prepare(`INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ?)
|
|
76
|
+
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`)
|
|
77
|
+
.run(key, value, ts());
|
|
78
|
+
}
|
|
79
|
+
// ---------- system state ----------
|
|
80
|
+
export function getSystemState(ctx, key) {
|
|
81
|
+
const row = ctx.db.prepare('SELECT value FROM system_state WHERE key=?').get(key);
|
|
82
|
+
return row?.value ?? null;
|
|
83
|
+
}
|
|
84
|
+
export function setSystemState(ctx, key, value) {
|
|
85
|
+
ctx.db
|
|
86
|
+
.prepare(`INSERT INTO system_state (key, value, updated_at) VALUES (?, ?, ?)
|
|
87
|
+
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`)
|
|
88
|
+
.run(key, value, ts());
|
|
89
|
+
}
|
|
90
|
+
export function lastRolloverDate(ctx) {
|
|
91
|
+
return getSystemState(ctx, 'last_rollover_date');
|
|
92
|
+
}
|
|
93
|
+
export function todayIn(ctx) {
|
|
94
|
+
return todayKey(resolveTimezone(ctx));
|
|
95
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { todayIn } from './profile.js';
|
|
2
|
+
import { listGoals, listStages, getCurrentStage } from './goals.js';
|
|
3
|
+
import { getTodayPlan } from './actions.js';
|
|
4
|
+
import { listGrowthEvents } from './evidence.js';
|
|
5
|
+
import { getActiveLifeContext } from './lifeContext.js';
|
|
6
|
+
const BLOCK_FULL = '█';
|
|
7
|
+
const BLOCK_EMPTY = '░';
|
|
8
|
+
function bar(done, total, width = 20) {
|
|
9
|
+
if (!total)
|
|
10
|
+
return BLOCK_EMPTY.repeat(width);
|
|
11
|
+
const filled = Math.round((done / total) * width);
|
|
12
|
+
return BLOCK_FULL.repeat(Math.max(0, Math.min(width, filled))) + BLOCK_EMPTY.repeat(Math.max(0, width - filled));
|
|
13
|
+
}
|
|
14
|
+
export function renderProgress(ctx) {
|
|
15
|
+
const today = todayIn(ctx);
|
|
16
|
+
const goals = listGoals(ctx);
|
|
17
|
+
const primary = goals.find((g) => g.status === 'ACTIVE') ?? goals[0] ?? null;
|
|
18
|
+
const stages = primary ? listStages(ctx, primary.id) : [];
|
|
19
|
+
const current = primary ? getCurrentStage(ctx, primary.id) : null;
|
|
20
|
+
const { plan, actions } = getTodayPlan(ctx);
|
|
21
|
+
const events = listGrowthEvents(ctx, undefined, 12);
|
|
22
|
+
const life = getActiveLifeContext(ctx);
|
|
23
|
+
const mainActions = actions.filter((a) => a.kind === 'MAIN_QUEST');
|
|
24
|
+
const dailyActions = actions.filter((a) => a.kind === 'DAILY');
|
|
25
|
+
const done = (l) => l.filter((a) => a.status === 'COMPLETED').length;
|
|
26
|
+
const md = [];
|
|
27
|
+
md.push(`## 成长进度 · ${today}`);
|
|
28
|
+
md.push('');
|
|
29
|
+
if (!primary) {
|
|
30
|
+
md.push('> 还没有主线目标。先和 Agent 聊一次你想做成什么,之后这里会自动长出进度。');
|
|
31
|
+
md.push('');
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
md.push(`### 主线:${primary.title}`);
|
|
35
|
+
md.push('');
|
|
36
|
+
if (primary.why)
|
|
37
|
+
md.push(`**为什么** ${primary.why}`);
|
|
38
|
+
if (primary.targetDate)
|
|
39
|
+
md.push(`**目标日期** ${primary.targetDate}`);
|
|
40
|
+
md.push('');
|
|
41
|
+
if (stages.length) {
|
|
42
|
+
const order = current?.order ?? stages.filter((s) => s.status === 'COMPLETED').length + 1;
|
|
43
|
+
md.push('#### 阶段进度');
|
|
44
|
+
md.push('');
|
|
45
|
+
md.push(`\`${bar(order, stages.length)}\` **Stage ${clamp(order, 1, stages.length)} / ${stages.length}**${current ? ` · ${current.title}` : ''}`);
|
|
46
|
+
md.push('');
|
|
47
|
+
// 节点轨道:■ 已完成 / ◧ 进行中 / □ 未开始
|
|
48
|
+
md.push(stages.map((s) => nodeGlyph(s.status)).join(' '));
|
|
49
|
+
md.push('');
|
|
50
|
+
md.push('| # | 阶段 | 状态 | 窗口 |');
|
|
51
|
+
md.push('|---:|---|---|---|');
|
|
52
|
+
for (const s of stages) {
|
|
53
|
+
md.push(`| ${s.order} | ${s.title} | ${stageLabel(s.status)} | ${s.plannedStartDate ?? '—'} → ${s.plannedEndDate ?? '—'} |`);
|
|
54
|
+
}
|
|
55
|
+
md.push('');
|
|
56
|
+
md.push('```mermaid');
|
|
57
|
+
md.push('gantt');
|
|
58
|
+
md.push(' dateFormat YYYY-MM-DD');
|
|
59
|
+
md.push(' title 阶段计划');
|
|
60
|
+
for (const s of stages) {
|
|
61
|
+
const start = s.plannedStartDate ?? today;
|
|
62
|
+
const end = s.plannedEndDate ?? start;
|
|
63
|
+
md.push(` ${s.title} :${mermaidClass(s.status)}, ${start}, ${end}`);
|
|
64
|
+
}
|
|
65
|
+
md.push('```');
|
|
66
|
+
md.push('');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
md.push('#### 今天');
|
|
70
|
+
md.push('');
|
|
71
|
+
if (!actions.length) {
|
|
72
|
+
md.push('今天还没有安排。说一句「开启今日任务」,让 Agent 帮你规划。');
|
|
73
|
+
md.push('');
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
md.push(`\`${bar(done(mainActions) + done(dailyActions), actions.length, 12)}\` ${done(mainActions) + done(dailyActions)} / ${actions.length}`);
|
|
77
|
+
md.push('');
|
|
78
|
+
md.push('| 类型 | 任务 | 状态 | 预计 |');
|
|
79
|
+
md.push('|---|---|---|---|');
|
|
80
|
+
for (const a of actions) {
|
|
81
|
+
const mark = a.status === 'COMPLETED' ? '✅' : a.status === 'EXPIRED' ? '⌛' : a.status === 'SKIPPED' ? '⏭️' : '◻️';
|
|
82
|
+
md.push(`| ${a.kind === 'MAIN_QUEST' ? '🔥 Main' : 'Daily'} | ${a.title} | ${mark} ${actionLabel(a.status)} | ${a.estimatedMinutes ? a.estimatedMinutes + 'min' : '—'} |`);
|
|
83
|
+
}
|
|
84
|
+
md.push('');
|
|
85
|
+
if (plan?.rationale) {
|
|
86
|
+
md.push(`> 为什么这么安排:${plan.rationale}`);
|
|
87
|
+
md.push('');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
md.push('#### 累计事实');
|
|
91
|
+
md.push('');
|
|
92
|
+
md.push('| 指标 | 数值 |');
|
|
93
|
+
md.push('|---|---:|');
|
|
94
|
+
md.push(`| 今日主任务 | ${done(mainActions)} / ${mainActions.length || 0} |`);
|
|
95
|
+
md.push(`| 今日每日任务 | ${done(dailyActions)} / ${dailyActions.length || 0} |`);
|
|
96
|
+
md.push(`| 证据总数 | ${countEvidence(ctx)} |`);
|
|
97
|
+
md.push(`| 成长记录 | ${events.length ? events.length + (events.length >= 12 ? '+' : '') : 0} |`);
|
|
98
|
+
md.push('');
|
|
99
|
+
if (life && life.mode !== 'NORMAL') {
|
|
100
|
+
md.push(`> 当前生活情境:**${life.mode}**${life.availableMinutesPerDay ? `,每天约 ${life.availableMinutesPerDay} 分钟` : ''} —— 负荷已相应下调。`);
|
|
101
|
+
md.push('');
|
|
102
|
+
}
|
|
103
|
+
if (events.length) {
|
|
104
|
+
md.push('#### 最近进展');
|
|
105
|
+
md.push('');
|
|
106
|
+
md.push('| 日期 | 记录 | 领域 | 证据 |');
|
|
107
|
+
md.push('|---|---|---|---:|');
|
|
108
|
+
for (const e of events.slice(0, 10)) {
|
|
109
|
+
md.push(`| ${e.dateKey} | ${e.title} | ${domainLabel(e.domain)} | ${e.evidenceIds.length} |`);
|
|
110
|
+
}
|
|
111
|
+
md.push('');
|
|
112
|
+
}
|
|
113
|
+
md.push('---');
|
|
114
|
+
md.push('');
|
|
115
|
+
md.push('<sub>本报告的进度条与数字均来自本地可核对事实(阶段序号、证据计数、任务状态),不含估算的掌握度或自律分。</sub>');
|
|
116
|
+
return {
|
|
117
|
+
title: `成长进度 · ${today}`,
|
|
118
|
+
markdown: md.join('\n'),
|
|
119
|
+
html: renderHtml({
|
|
120
|
+
today, primary: primary ? { title: primary.title, why: primary.why, targetDate: primary.targetDate } : null,
|
|
121
|
+
stages: stages.map((s) => ({ order: s.order, title: s.title, status: s.status, start: s.plannedStartDate ?? null, end: s.plannedEndDate ?? null })),
|
|
122
|
+
currentOrder: current?.order ?? null,
|
|
123
|
+
actions: actions.map((a) => ({ kind: a.kind, title: a.title, status: a.status, estimatedMinutes: a.estimatedMinutes ?? null })),
|
|
124
|
+
events: events.slice(0, 10).map((e) => ({ date: e.dateKey, title: e.title, domain: e.domain, evidence: e.evidenceIds.length })),
|
|
125
|
+
life: life ? { mode: life.mode, minutes: life.availableMinutesPerDay ?? null } : null,
|
|
126
|
+
plan: plan?.rationale ?? null,
|
|
127
|
+
}),
|
|
128
|
+
facts: {
|
|
129
|
+
today,
|
|
130
|
+
stageOrder: current?.order ?? null,
|
|
131
|
+
stageTotal: stages.length || null,
|
|
132
|
+
mainDoneToday: done(mainActions),
|
|
133
|
+
mainTotalToday: mainActions.length,
|
|
134
|
+
dailyDoneToday: done(dailyActions),
|
|
135
|
+
dailyTotalToday: dailyActions.length,
|
|
136
|
+
evidenceTotal: countEvidence(ctx),
|
|
137
|
+
growthEventTotal: listGrowthEvents(ctx, undefined, 500).length,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function domainLabel(d) {
|
|
142
|
+
return { LEARN: '学习', BUILD: '构建', TRAIN: '训练', HABIT: '习惯', CREATE: '创作',
|
|
143
|
+
RELATIONSHIP: '关系', EXPLORE: '探索', LIFE: '生活', OTHER: '其他' }[d] ?? d;
|
|
144
|
+
}
|
|
145
|
+
function countEvidence(ctx) {
|
|
146
|
+
return ctx.db.prepare('SELECT COUNT(*) AS n FROM evidence').get().n;
|
|
147
|
+
}
|
|
148
|
+
function clamp(v, lo, hi) {
|
|
149
|
+
return Math.max(lo, Math.min(hi, v));
|
|
150
|
+
}
|
|
151
|
+
function nodeGlyph(s) {
|
|
152
|
+
if (s === 'COMPLETED')
|
|
153
|
+
return '■';
|
|
154
|
+
if (s === 'ACTIVE' || s === 'DELAYED')
|
|
155
|
+
return '◧';
|
|
156
|
+
if (s === 'SKIPPED')
|
|
157
|
+
return '▫';
|
|
158
|
+
return '□';
|
|
159
|
+
}
|
|
160
|
+
function stageLabel(s) {
|
|
161
|
+
return { PLANNED: '未开始', ACTIVE: '进行中', COMPLETED: '已完成', DELAYED: '已顺延', SKIPPED: '已跳过' }[s] ?? s;
|
|
162
|
+
}
|
|
163
|
+
function actionLabel(s) {
|
|
164
|
+
return { DRAFT: '待确认', PLANNED: '待进行', IN_PROGRESS: '进行中', COMPLETED: '已完成',
|
|
165
|
+
SKIPPED: '已跳过', DELAYED: '已延期', PENDING_REPLAN: '待重排', EXPIRED: '已过期', CANCELLED: '已取消' }[s] ?? s;
|
|
166
|
+
}
|
|
167
|
+
function mermaidClass(s) {
|
|
168
|
+
if (s === 'COMPLETED')
|
|
169
|
+
return 'done';
|
|
170
|
+
if (s === 'ACTIVE')
|
|
171
|
+
return 'active';
|
|
172
|
+
return 'todo';
|
|
173
|
+
}
|
|
174
|
+
function renderHtml(m) {
|
|
175
|
+
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
176
|
+
const order = m.currentOrder ?? (m.stages.filter((s) => s.status === 'COMPLETED').length + 1);
|
|
177
|
+
const pct = m.stages.length ? Math.round((clamp(order, 1, m.stages.length) / m.stages.length) * 100) : 0;
|
|
178
|
+
return `<!DOCTYPE html>
|
|
179
|
+
<html lang="zh-CN"><head><meta charset="UTF-8">
|
|
180
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
181
|
+
<title>${esc(m.primary?.title ?? 'AI Growth')} · 进度</title>
|
|
182
|
+
<style>
|
|
183
|
+
:root{--bg:#070b16;--panel:#0e1526;--ink:#e8eefc;--dim:#8b9bc4;--faint:#5b6a90;--edge:#1e2a45;--edge2:#2c3d61;--gold:#ffc247;--cyan:#38d6d6;--violet:#9d7bff;--green:#45d483;--amber:#ffa94d}
|
|
184
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
185
|
+
body{background:var(--bg);color:var(--ink);font-family:-apple-system,BlinkMacSystemFont,"PingFang SC",Arial,sans-serif;
|
|
186
|
+
background-image:radial-gradient(900px 480px at 10% -10%,rgba(56,214,214,.12),transparent 60%),radial-gradient(800px 460px at 100% 0,rgba(157,123,255,.14),transparent 60%);
|
|
187
|
+
background-attachment:fixed;padding:26px 18px;font-variant-numeric:tabular-nums}
|
|
188
|
+
.wrap{max-width:860px;margin:0 auto}
|
|
189
|
+
.card{background:linear-gradient(180deg,rgba(255,255,255,.045),rgba(255,255,255,0) 42%),var(--panel);border:1px solid var(--edge);border-radius:14px;padding:16px 17px;margin-bottom:14px;box-shadow:0 12px 28px -22px #000}
|
|
190
|
+
.hd{font-size:11px;font-weight:700;letter-spacing:.13em;color:var(--faint);text-transform:uppercase;margin-bottom:10px}
|
|
191
|
+
h1{font-size:21px;font-weight:750;margin-bottom:6px}
|
|
192
|
+
h2{font-size:17px;font-weight:700;margin-bottom:8px}
|
|
193
|
+
.meta{font-size:13px;color:var(--dim);line-height:1.7}
|
|
194
|
+
.bar{height:12px;border-radius:999px;background:#141d33;border:1px solid var(--edge);overflow:hidden;margin:10px 0 8px}
|
|
195
|
+
.bar span{display:block;height:100%;background:linear-gradient(90deg,var(--cyan),var(--violet));box-shadow:0 0 18px -2px rgba(56,214,214,.7)}
|
|
196
|
+
.track{display:flex;align-items:center;gap:2px;margin:6px 0 12px}
|
|
197
|
+
.node{width:24px;height:24px;border-radius:7px;display:flex;align-items:center;justify-content:center;font-size:11px;background:#131c31;border:1px solid var(--edge2);color:var(--faint)}
|
|
198
|
+
.node.done{background:rgba(69,212,131,.2);border-color:rgba(69,212,131,.6);color:var(--green)}
|
|
199
|
+
.node.now{background:linear-gradient(180deg,rgba(56,214,214,.3),rgba(56,214,214,.1));border-color:var(--cyan);color:var(--cyan);box-shadow:0 0 16px -3px rgba(56,214,214,.85)}
|
|
200
|
+
.link{flex:1;height:2px;background:var(--edge);min-width:6px}
|
|
201
|
+
table{width:100%;border-collapse:collapse;font-size:13px}
|
|
202
|
+
th,td{text-align:left;padding:7px 8px;border-bottom:1px dashed var(--edge)}
|
|
203
|
+
th{color:var(--faint);font-weight:600;font-size:11.5px;letter-spacing:.06em}
|
|
204
|
+
td.n{text-align:right;color:var(--dim)}
|
|
205
|
+
.chip{display:inline-block;font-size:11px;padding:2px 8px;border-radius:999px;background:rgba(255,255,255,.05);border:1px solid var(--edge2);color:var(--dim)}
|
|
206
|
+
.chip.g{background:rgba(69,212,131,.12);border-color:rgba(69,212,131,.32);color:var(--green)}
|
|
207
|
+
.chip.c{background:rgba(56,214,214,.12);border-color:rgba(56,214,214,.32);color:var(--cyan)}
|
|
208
|
+
.chip.a{background:rgba(255,169,77,.12);border-color:rgba(255,169,77,.32);color:var(--amber)}
|
|
209
|
+
.foot{font-size:11.5px;color:var(--faint);text-align:center;padding-top:6px}
|
|
210
|
+
</style></head><body><div class="wrap">
|
|
211
|
+
|
|
212
|
+
<div class="card"><div class="hd">当前主线</div>
|
|
213
|
+
${m.primary
|
|
214
|
+
? `<h2>${esc(m.primary.title)}</h2>
|
|
215
|
+
<div class="meta">${m.primary.why ? '为什么:' + esc(m.primary.why) + '<br>' : ''}${m.primary.targetDate ? '目标日期:' + esc(m.primary.targetDate) : ''}</div>`
|
|
216
|
+
: '<div class="meta">还没有主线目标 —— 先和 Agent 聊一次你想做成什么。</div>'}
|
|
217
|
+
${m.life && m.life.mode !== 'NORMAL' ? `<div style="margin-top:10px"><span class="chip a">${esc(m.life.mode)}${m.life.minutes ? ' · 每天 ' + m.life.minutes + ' 分钟' : ''}</span></div>` : ''}
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
${m.stages.length ? `<div class="card"><div class="hd">阶段进度</div>
|
|
221
|
+
<div class="bar"><span style="width:${pct}%"></span></div>
|
|
222
|
+
<div class="meta"><strong>Stage ${clamp(order, 1, m.stages.length)} / ${m.stages.length}</strong>${m.stages.find((s) => s.order === order)?.title ? ' · ' + esc(m.stages.find((s) => s.order === order).title) : ''}</div>
|
|
223
|
+
<div class="track">${m.stages.map((s, i) => `<div class="node ${s.status === 'COMPLETED' ? 'done' : s.status === 'ACTIVE' || s.status === 'DELAYED' ? 'now' : ''}">${s.status === 'COMPLETED' ? '✓' : s.order}</div>${i < m.stages.length - 1 ? '<div class="link"></div>' : ''}`).join('')}</div>
|
|
224
|
+
<table><tr><th>#</th><th>阶段</th><th>状态</th><th>窗口</th></tr>
|
|
225
|
+
${m.stages.map((s) => `<tr><td>${s.order}</td><td>${esc(s.title)}</td><td><span class="chip ${s.status === 'COMPLETED' ? 'g' : s.status === 'ACTIVE' ? 'c' : ''}">${stageLabel(s.status)}</span></td><td class="n">${esc(s.start ?? '—')} → ${esc(s.end ?? '—')}</td></tr>`).join('')}
|
|
226
|
+
</table></div>` : ''}
|
|
227
|
+
|
|
228
|
+
<div class="card"><div class="hd">今天 · ${esc(m.today)}</div>
|
|
229
|
+
${m.actions.length
|
|
230
|
+
? `<table><tr><th>类型</th><th>任务</th><th>状态</th><th class="n">预计</th></tr>
|
|
231
|
+
${m.actions.map((a) => `<tr><td>${a.kind === 'MAIN_QUEST' ? '🔥 Main' : 'Daily'}</td><td>${esc(a.title)}</td><td><span class="chip ${a.status === 'COMPLETED' ? 'g' : ''}">${actionLabel(a.status)}</span></td><td class="n">${a.estimatedMinutes ? a.estimatedMinutes + 'min' : '—'}</td></tr>`).join('')}
|
|
232
|
+
</table>`
|
|
233
|
+
: '<div class="meta">今天还没有安排。说一句「开启今日任务」让 Agent 帮你规划。</div>'}
|
|
234
|
+
${m.plan ? `<div class="meta" style="margin-top:10px">为什么这么安排:${esc(m.plan)}</div>` : ''}
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
${m.events.length ? `<div class="card"><div class="hd">最近进展</div>
|
|
238
|
+
<table><tr><th>日期</th><th>记录</th><th>领域</th><th class="n">证据</th></tr>
|
|
239
|
+
${m.events.map((e) => `<tr><td>${esc(e.date)}</td><td>${esc(e.title)}</td><td><span class="chip c">${domainLabel(e.domain)}</span></td><td class="n">${e.evidence}</td></tr>`).join('')}
|
|
240
|
+
</table></div>` : ''}
|
|
241
|
+
|
|
242
|
+
<div class="foot">进度条与数字均来自本地可核对事实(阶段序号、证据计数、任务状态),不含估算的掌握度或自律分 · AI Growth</div>
|
|
243
|
+
</div></body></html>`;
|
|
244
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 版本化 Prompts(§20)。
|
|
3
|
+
* 优先让外部 Agent 通过 Skill + MCP 承担对话智能;这些 prompt 供 daemon 内部
|
|
4
|
+
* AI Provider 做后台可自动执行的结构化工作(未配置 AIProvider 时 Core 全部降级为
|
|
5
|
+
* 纯结构化 primitive,不阻塞 V1)。
|
|
6
|
+
*/
|
|
7
|
+
export const PROMPTS = {
|
|
8
|
+
onboarding: {
|
|
9
|
+
version: 'v1',
|
|
10
|
+
prompt: `You are interviewing the user to build a growth profile. Ask conversationally, one topic at a time. Cover: main goal now, expected real outcome (not "finish a course"), target period, daily time available, current level, life constraints, life areas to improve, task load preference. Output strict JSON: {goalTitle, why, desiredOutcome, successCriteria[], targetDate, dailyTimeBudgetMinutes, constraints, loadPreference}.`,
|
|
11
|
+
},
|
|
12
|
+
goalPlanner: {
|
|
13
|
+
version: 'v1',
|
|
14
|
+
prompt: `Given the interview result, produce a goal draft with why / desiredOutcome / successCriteria. Success criteria must be observable outcomes (runnable project, passing assessment, real metric), not "finished content". Output strict JSON matching the create_goal_draft tool schema.`,
|
|
15
|
+
},
|
|
16
|
+
stagePlanner: {
|
|
17
|
+
version: 'v1',
|
|
18
|
+
prompt: `Given a confirmed goal and its period, produce 3-7 stages with objectives and exit criteria. Do NOT generate any daily actions. Stages must be verifiable milestones. Output strict JSON: {stages:[{title, objective, exitCriteria[]}]}.`,
|
|
19
|
+
},
|
|
20
|
+
dailyPlanner: {
|
|
21
|
+
version: 'v1',
|
|
22
|
+
prompt: `Given planner context (active goals, current stage, yesterday's actions, recent evidence, feedback, life context, available minutes), propose today's plan: exactly 1 Main Quest (whyToday, completionCriteria, estimatedMinutes — a step that fits available time) + 0-2 Daily Actions (only from user-confirmed directions). Reduce load when life context is BUSY/RECOVERY/TRAVEL. Output strict JSON matching the plan_today tool schema.`,
|
|
23
|
+
},
|
|
24
|
+
replan: {
|
|
25
|
+
version: 'v1',
|
|
26
|
+
prompt: `A Main Quest was not completed. Given the action, the reason, feedback and stage plan, choose: CONTINUE (retry today), ADJUST (change scope/difficulty), SPLIT (smaller steps), DELAY_STAGE (push stage window), or CANCEL. Prefer the smallest change that preserves the goal. Anything that clearly moves the final deadline requires asking the user first.`,
|
|
27
|
+
},
|
|
28
|
+
assessment: {
|
|
29
|
+
version: 'v1',
|
|
30
|
+
prompt: `Generate a short assessment for the given learning topic at the given level (EXPOSURE/RECALL/EXPLAIN/APPLY). For engineering topics prefer a practical exercise over multiple choice. Your own quiz score alone never proves mastery.`,
|
|
31
|
+
},
|
|
32
|
+
dailyReview: {
|
|
33
|
+
version: 'v1',
|
|
34
|
+
prompt: `Summarize today: main quest done? key evidence? was the task load reasonable? any change worth remembering? any replan needed? Keep it under 120 words. No personality judgments without evidence.`,
|
|
35
|
+
},
|
|
36
|
+
passport: {
|
|
37
|
+
version: 'v1',
|
|
38
|
+
prompt: `Compress the user's current human state into a passport: current chapter, active goals, capabilities with evidence, recent growth, planning hints. Only what a new agent needs to take over. Never invent facts.`,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { writeAudit } from './context.js';
|
|
2
|
+
import { DomainError, ErrorCodes } from '../shared/result.js';
|
|
3
|
+
import { todayIn } from './profile.js';
|
|
4
|
+
import { getAction, updateAction, cancelAction, rearmAction, createAction, } from './actions.js';
|
|
5
|
+
import { shiftStagesFrom, getStage } from './goals.js';
|
|
6
|
+
import { listPendingReplanActions } from './rollover.js';
|
|
7
|
+
/**
|
|
8
|
+
* Replan Engine(§4)。执行层调整 Agent 可自主执行;
|
|
9
|
+
* 若 resolution 会明显影响 Goal Deadline(DELAY_STAGE 大幅顺延),调用方必须先获得用户确认。
|
|
10
|
+
* 状态规则全部由 Core 保证,不依赖模型"记得遵守"。
|
|
11
|
+
*/
|
|
12
|
+
export function resolveReplan(ctx, input) {
|
|
13
|
+
let targetId = input.actionId;
|
|
14
|
+
if (!targetId) {
|
|
15
|
+
const pending = listPendingReplanActions(ctx);
|
|
16
|
+
if (pending.length === 0)
|
|
17
|
+
throw new DomainError(ErrorCodes.NOT_FOUND, 'No pending replan actions');
|
|
18
|
+
targetId = pending[0].id;
|
|
19
|
+
}
|
|
20
|
+
const action = getAction(ctx, targetId);
|
|
21
|
+
if (!action)
|
|
22
|
+
throw new DomainError(ErrorCodes.NOT_FOUND, `Action not found: ${targetId}`);
|
|
23
|
+
if (action.status !== 'PENDING_REPLAN') {
|
|
24
|
+
throw new DomainError(ErrorCodes.INVALID_STATE, `Action ${targetId} is not PENDING_REPLAN (got ${action.status})`);
|
|
25
|
+
}
|
|
26
|
+
const today = todayIn(ctx);
|
|
27
|
+
// 决策级审计(§29.3):单个 action/stage 的改动各自有审计,
|
|
28
|
+
// 这里额外记录"Replan 决策本身"(选了哪种解法、为什么),便于回答"为什么这个任务被改了"。
|
|
29
|
+
writeAudit(ctx, {
|
|
30
|
+
entityType: 'replan',
|
|
31
|
+
entityId: action.id,
|
|
32
|
+
before: { status: action.status, dateKey: action.dateKey, title: action.title },
|
|
33
|
+
after: { resolution: input.resolution, reason: input.reason, ...(input.adjust ?? {}), delayDays: input.delayDays },
|
|
34
|
+
reason: input.reason,
|
|
35
|
+
});
|
|
36
|
+
switch (input.resolution) {
|
|
37
|
+
case 'CONTINUE': {
|
|
38
|
+
const updated = rearmToToday(ctx, action.id, today);
|
|
39
|
+
return { resolution: 'CONTINUE', action: updated };
|
|
40
|
+
}
|
|
41
|
+
case 'ADJUST': {
|
|
42
|
+
if (input.adjust) {
|
|
43
|
+
updateAction(ctx, action.id, { ...input.adjust }, `replan adjust: ${input.reason}`);
|
|
44
|
+
}
|
|
45
|
+
const updated = rearmToToday(ctx, action.id, today);
|
|
46
|
+
return { resolution: 'ADJUST', action: updated };
|
|
47
|
+
}
|
|
48
|
+
case 'SPLIT': {
|
|
49
|
+
if (!input.splitInto?.length) {
|
|
50
|
+
throw new DomainError(ErrorCodes.VALIDATION_ERROR, 'SPLIT requires splitInto tasks');
|
|
51
|
+
}
|
|
52
|
+
const children = input.splitInto.map((s) => createAction(ctx, {
|
|
53
|
+
kind: 'MAIN_QUEST',
|
|
54
|
+
title: s.title,
|
|
55
|
+
estimatedMinutes: s.estimatedMinutes,
|
|
56
|
+
completionCriteria: s.completionCriteria,
|
|
57
|
+
whyToday: action.whyToday,
|
|
58
|
+
basis: action.basis,
|
|
59
|
+
basisGoalId: action.basisGoalId,
|
|
60
|
+
basisStageId: action.basisStageId,
|
|
61
|
+
basisDirectionId: action.basisDirectionId,
|
|
62
|
+
verificationStrategy: action.verificationStrategy,
|
|
63
|
+
}));
|
|
64
|
+
const cancelled = cancelAction(ctx, action.id, `replan split into ${children.length} tasks: ${input.reason}`);
|
|
65
|
+
return { resolution: 'SPLIT', action: cancelled, createdActions: children };
|
|
66
|
+
}
|
|
67
|
+
case 'DELAY_STAGE': {
|
|
68
|
+
const days = input.delayDays ?? 1;
|
|
69
|
+
let stages = null;
|
|
70
|
+
if (action.basisStageId) {
|
|
71
|
+
const stage = getStage(ctx, action.basisStageId);
|
|
72
|
+
if (stage)
|
|
73
|
+
stages = shiftStagesFrom(ctx, stage.goalId, stage.id, days, `replan delay: ${input.reason}`);
|
|
74
|
+
}
|
|
75
|
+
const updated = rearmToToday(ctx, action.id, today);
|
|
76
|
+
return { resolution: 'DELAY_STAGE', action: updated, stages };
|
|
77
|
+
}
|
|
78
|
+
case 'CANCEL': {
|
|
79
|
+
const cancelled = cancelAction(ctx, action.id, `replan cancel: ${input.reason}`);
|
|
80
|
+
return { resolution: 'CANCEL', action: cancelled };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function rearmToToday(ctx, actionId, today) {
|
|
85
|
+
const a = getAction(ctx, actionId);
|
|
86
|
+
if (a.dateKey !== today)
|
|
87
|
+
updateAction(ctx, actionId, { dateKey: today }, 'replan move to today');
|
|
88
|
+
return rearmAction(ctx, actionId);
|
|
89
|
+
}
|
|
90
|
+
/** Planner hint 数据:给 Agent 决策下一步用(Core 不做 LLM 判断) */
|
|
91
|
+
export function replanHints(ctx) {
|
|
92
|
+
return {
|
|
93
|
+
pendingReplans: listPendingReplanActions(ctx).map((a) => ({ id: a.id, title: a.title, dateKey: a.date_key })),
|
|
94
|
+
rules: [
|
|
95
|
+
'先调整 Action,再调整 Stage 内部节奏,再使用缓冲时间',
|
|
96
|
+
'影响 Goal Deadline / Scope 的调整必须生成 Proposal 并让用户确认',
|
|
97
|
+
'大幅改变最终 Deadline 前必须确认(V1.1 §23.4)',
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|