@kynetic-ai/spec 0.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/README.md +263 -0
- package/dist/acp/client.d.ts +159 -0
- package/dist/acp/client.d.ts.map +1 -0
- package/dist/acp/client.js +255 -0
- package/dist/acp/client.js.map +1 -0
- package/dist/acp/framing.d.ts +119 -0
- package/dist/acp/framing.d.ts.map +1 -0
- package/dist/acp/framing.js +302 -0
- package/dist/acp/framing.js.map +1 -0
- package/dist/acp/index.d.ts +14 -0
- package/dist/acp/index.d.ts.map +1 -0
- package/dist/acp/index.js +13 -0
- package/dist/acp/index.js.map +1 -0
- package/dist/acp/types.d.ts +89 -0
- package/dist/acp/types.d.ts.map +1 -0
- package/dist/acp/types.js +99 -0
- package/dist/acp/types.js.map +1 -0
- package/dist/agents/adapters.d.ts +55 -0
- package/dist/agents/adapters.d.ts.map +1 -0
- package/dist/agents/adapters.js +84 -0
- package/dist/agents/adapters.js.map +1 -0
- package/dist/agents/index.d.ts +8 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +10 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/spawner.d.ts +53 -0
- package/dist/agents/spawner.d.ts.map +1 -0
- package/dist/agents/spawner.js +83 -0
- package/dist/agents/spawner.js.map +1 -0
- package/dist/cli/batch.d.ts +82 -0
- package/dist/cli/batch.d.ts.map +1 -0
- package/dist/cli/batch.js +162 -0
- package/dist/cli/batch.js.map +1 -0
- package/dist/cli/commands/clone-for-testing.d.ts +6 -0
- package/dist/cli/commands/clone-for-testing.d.ts.map +1 -0
- package/dist/cli/commands/clone-for-testing.js +176 -0
- package/dist/cli/commands/clone-for-testing.js.map +1 -0
- package/dist/cli/commands/derive.d.ts +6 -0
- package/dist/cli/commands/derive.d.ts.map +1 -0
- package/dist/cli/commands/derive.js +450 -0
- package/dist/cli/commands/derive.js.map +1 -0
- package/dist/cli/commands/help.d.ts +6 -0
- package/dist/cli/commands/help.d.ts.map +1 -0
- package/dist/cli/commands/help.js +196 -0
- package/dist/cli/commands/help.js.map +1 -0
- package/dist/cli/commands/inbox.d.ts +6 -0
- package/dist/cli/commands/inbox.d.ts.map +1 -0
- package/dist/cli/commands/inbox.js +235 -0
- package/dist/cli/commands/inbox.js.map +1 -0
- package/dist/cli/commands/index.d.ts +20 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/index.js +21 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +245 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/item.d.ts +6 -0
- package/dist/cli/commands/item.d.ts.map +1 -0
- package/dist/cli/commands/item.js +1311 -0
- package/dist/cli/commands/item.js.map +1 -0
- package/dist/cli/commands/link.d.ts +6 -0
- package/dist/cli/commands/link.d.ts.map +1 -0
- package/dist/cli/commands/link.js +288 -0
- package/dist/cli/commands/link.js.map +1 -0
- package/dist/cli/commands/log.d.ts +16 -0
- package/dist/cli/commands/log.d.ts.map +1 -0
- package/dist/cli/commands/log.js +291 -0
- package/dist/cli/commands/log.js.map +1 -0
- package/dist/cli/commands/meta.d.ts +15 -0
- package/dist/cli/commands/meta.d.ts.map +1 -0
- package/dist/cli/commands/meta.js +1378 -0
- package/dist/cli/commands/meta.js.map +1 -0
- package/dist/cli/commands/module.d.ts +6 -0
- package/dist/cli/commands/module.d.ts.map +1 -0
- package/dist/cli/commands/module.js +102 -0
- package/dist/cli/commands/module.js.map +1 -0
- package/dist/cli/commands/ralph.d.ts +9 -0
- package/dist/cli/commands/ralph.d.ts.map +1 -0
- package/dist/cli/commands/ralph.js +465 -0
- package/dist/cli/commands/ralph.js.map +1 -0
- package/dist/cli/commands/search.d.ts +6 -0
- package/dist/cli/commands/search.d.ts.map +1 -0
- package/dist/cli/commands/search.js +134 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/commands/session.d.ts +164 -0
- package/dist/cli/commands/session.d.ts.map +1 -0
- package/dist/cli/commands/session.js +745 -0
- package/dist/cli/commands/session.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +26 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +586 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/shadow.d.ts +6 -0
- package/dist/cli/commands/shadow.d.ts.map +1 -0
- package/dist/cli/commands/shadow.js +299 -0
- package/dist/cli/commands/shadow.js.map +1 -0
- package/dist/cli/commands/task.d.ts +6 -0
- package/dist/cli/commands/task.d.ts.map +1 -0
- package/dist/cli/commands/task.js +1514 -0
- package/dist/cli/commands/task.js.map +1 -0
- package/dist/cli/commands/tasks.d.ts +6 -0
- package/dist/cli/commands/tasks.d.ts.map +1 -0
- package/dist/cli/commands/tasks.js +347 -0
- package/dist/cli/commands/tasks.js.map +1 -0
- package/dist/cli/commands/trait.d.ts +10 -0
- package/dist/cli/commands/trait.d.ts.map +1 -0
- package/dist/cli/commands/trait.js +295 -0
- package/dist/cli/commands/trait.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +6 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +626 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/exit-codes.d.ts +62 -0
- package/dist/cli/exit-codes.d.ts.map +1 -0
- package/dist/cli/exit-codes.js +65 -0
- package/dist/cli/exit-codes.js.map +1 -0
- package/dist/cli/help/content.d.ts +35 -0
- package/dist/cli/help/content.d.ts.map +1 -0
- package/dist/cli/help/content.js +312 -0
- package/dist/cli/help/content.js.map +1 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +85 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/introspection.d.ts +87 -0
- package/dist/cli/introspection.d.ts.map +1 -0
- package/dist/cli/introspection.js +127 -0
- package/dist/cli/introspection.js.map +1 -0
- package/dist/cli/output.d.ts +56 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +467 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/suggest.d.ts +16 -0
- package/dist/cli/suggest.d.ts.map +1 -0
- package/dist/cli/suggest.js +72 -0
- package/dist/cli/suggest.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/alignment.d.ts +113 -0
- package/dist/parser/alignment.d.ts.map +1 -0
- package/dist/parser/alignment.js +261 -0
- package/dist/parser/alignment.js.map +1 -0
- package/dist/parser/assess.d.ts +81 -0
- package/dist/parser/assess.d.ts.map +1 -0
- package/dist/parser/assess.js +197 -0
- package/dist/parser/assess.js.map +1 -0
- package/dist/parser/convention-validation.d.ts +48 -0
- package/dist/parser/convention-validation.d.ts.map +1 -0
- package/dist/parser/convention-validation.js +167 -0
- package/dist/parser/convention-validation.js.map +1 -0
- package/dist/parser/fix.d.ts +38 -0
- package/dist/parser/fix.d.ts.map +1 -0
- package/dist/parser/fix.js +185 -0
- package/dist/parser/fix.js.map +1 -0
- package/dist/parser/index.d.ts +12 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +13 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/items.d.ts +138 -0
- package/dist/parser/items.d.ts.map +1 -0
- package/dist/parser/items.js +321 -0
- package/dist/parser/items.js.map +1 -0
- package/dist/parser/meta.d.ts +120 -0
- package/dist/parser/meta.d.ts.map +1 -0
- package/dist/parser/meta.js +441 -0
- package/dist/parser/meta.js.map +1 -0
- package/dist/parser/refs.d.ts +185 -0
- package/dist/parser/refs.d.ts.map +1 -0
- package/dist/parser/refs.js +404 -0
- package/dist/parser/refs.js.map +1 -0
- package/dist/parser/shadow.d.ts +253 -0
- package/dist/parser/shadow.d.ts.map +1 -0
- package/dist/parser/shadow.js +1053 -0
- package/dist/parser/shadow.js.map +1 -0
- package/dist/parser/traits.d.ts +72 -0
- package/dist/parser/traits.d.ts.map +1 -0
- package/dist/parser/traits.js +120 -0
- package/dist/parser/traits.js.map +1 -0
- package/dist/parser/validate.d.ts +89 -0
- package/dist/parser/validate.d.ts.map +1 -0
- package/dist/parser/validate.js +817 -0
- package/dist/parser/validate.js.map +1 -0
- package/dist/parser/yaml.d.ts +326 -0
- package/dist/parser/yaml.d.ts.map +1 -0
- package/dist/parser/yaml.js +1383 -0
- package/dist/parser/yaml.js.map +1 -0
- package/dist/ralph/cli-renderer.d.ts +20 -0
- package/dist/ralph/cli-renderer.d.ts.map +1 -0
- package/dist/ralph/cli-renderer.js +179 -0
- package/dist/ralph/cli-renderer.js.map +1 -0
- package/dist/ralph/events.d.ts +65 -0
- package/dist/ralph/events.d.ts.map +1 -0
- package/dist/ralph/events.js +397 -0
- package/dist/ralph/events.js.map +1 -0
- package/dist/ralph/index.d.ts +8 -0
- package/dist/ralph/index.d.ts.map +1 -0
- package/dist/ralph/index.js +10 -0
- package/dist/ralph/index.js.map +1 -0
- package/dist/schema/common.d.ts +46 -0
- package/dist/schema/common.d.ts.map +1 -0
- package/dist/schema/common.js +71 -0
- package/dist/schema/common.js.map +1 -0
- package/dist/schema/inbox.d.ts +90 -0
- package/dist/schema/inbox.d.ts.map +1 -0
- package/dist/schema/inbox.js +30 -0
- package/dist/schema/inbox.js.map +1 -0
- package/dist/schema/index.d.ts +6 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +7 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/meta.d.ts +762 -0
- package/dist/schema/meta.d.ts.map +1 -0
- package/dist/schema/meta.js +144 -0
- package/dist/schema/meta.js.map +1 -0
- package/dist/schema/spec.d.ts +912 -0
- package/dist/schema/spec.d.ts.map +1 -0
- package/dist/schema/spec.js +104 -0
- package/dist/schema/spec.js.map +1 -0
- package/dist/schema/task.d.ts +664 -0
- package/dist/schema/task.d.ts.map +1 -0
- package/dist/schema/task.js +130 -0
- package/dist/schema/task.js.map +1 -0
- package/dist/sessions/index.d.ts +11 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +13 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/sessions/store.d.ts +144 -0
- package/dist/sessions/store.d.ts.map +1 -0
- package/dist/sessions/store.js +325 -0
- package/dist/sessions/store.js.map +1 -0
- package/dist/sessions/types.d.ts +157 -0
- package/dist/sessions/types.d.ts.map +1 -0
- package/dist/sessions/types.js +90 -0
- package/dist/sessions/types.js.map +1 -0
- package/dist/strings/errors.d.ts +420 -0
- package/dist/strings/errors.d.ts.map +1 -0
- package/dist/strings/errors.js +282 -0
- package/dist/strings/errors.js.map +1 -0
- package/dist/strings/guidance.d.ts +65 -0
- package/dist/strings/guidance.d.ts.map +1 -0
- package/dist/strings/guidance.js +66 -0
- package/dist/strings/guidance.js.map +1 -0
- package/dist/strings/index.d.ts +12 -0
- package/dist/strings/index.d.ts.map +1 -0
- package/dist/strings/index.js +12 -0
- package/dist/strings/index.js.map +1 -0
- package/dist/strings/labels.d.ts +74 -0
- package/dist/strings/labels.d.ts.map +1 -0
- package/dist/strings/labels.js +75 -0
- package/dist/strings/labels.js.map +1 -0
- package/dist/strings/validation.d.ts +126 -0
- package/dist/strings/validation.d.ts.map +1 -0
- package/dist/strings/validation.js +135 -0
- package/dist/strings/validation.js.map +1 -0
- package/dist/utils/commit.d.ts +23 -0
- package/dist/utils/commit.d.ts.map +1 -0
- package/dist/utils/commit.js +67 -0
- package/dist/utils/commit.js.map +1 -0
- package/dist/utils/git.d.ts +57 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +192 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/grep.d.ts +28 -0
- package/dist/utils/grep.d.ts.map +1 -0
- package/dist/utils/grep.js +86 -0
- package/dist/utils/grep.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/time.d.ts +18 -0
- package/dist/utils/time.d.ts.map +1 -0
- package/dist/utils/time.js +61 -0
- package/dist/utils/time.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session management commands
|
|
3
|
+
*
|
|
4
|
+
* Provides context for starting/resuming work sessions.
|
|
5
|
+
*/
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { initContext, loadAllTasks, loadAllItems, loadInboxItems, loadSessionContext, getReadyTasks, ReferenceIndex, } from '../../parser/index.js';
|
|
8
|
+
import { output, error, isJsonMode } from '../output.js';
|
|
9
|
+
import { sessionHeaders, hints, sessionPrompt, errors } from '../../strings/index.js';
|
|
10
|
+
import { parseTimeSpec, formatRelativeTime, isGitRepo, getRecentCommits, getCurrentBranch, getWorkingTreeStatus, formatCommitGuidance, } from '../../utils/index.js';
|
|
11
|
+
import { shadowPull } from '../../parser/shadow.js';
|
|
12
|
+
import { EXIT_CODES } from '../exit-codes.js';
|
|
13
|
+
// ─── Data Gathering ──────────────────────────────────────────────────────────
|
|
14
|
+
function toActiveTaskSummary(task, index) {
|
|
15
|
+
const lastNote = task.notes.length > 0 ? task.notes[task.notes.length - 1] : null;
|
|
16
|
+
const incompleteTodos = task.todos.filter(t => !t.done).length;
|
|
17
|
+
return {
|
|
18
|
+
ref: index.shortUlid(task._ulid),
|
|
19
|
+
title: task.title,
|
|
20
|
+
started_at: task.started_at || null,
|
|
21
|
+
priority: task.priority,
|
|
22
|
+
spec_ref: task.spec_ref || null,
|
|
23
|
+
note_count: task.notes.length,
|
|
24
|
+
last_note_at: lastNote ? lastNote.created_at : null,
|
|
25
|
+
todo_count: task.todos.length,
|
|
26
|
+
incomplete_todos: incompleteTodos,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function toReadyTaskSummary(task, index) {
|
|
30
|
+
return {
|
|
31
|
+
ref: index.shortUlid(task._ulid),
|
|
32
|
+
title: task.title,
|
|
33
|
+
priority: task.priority,
|
|
34
|
+
spec_ref: task.spec_ref || null,
|
|
35
|
+
tags: task.tags,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function toBlockedTaskSummary(task, allTasks, index) {
|
|
39
|
+
// Find unmet dependencies
|
|
40
|
+
const unmetDeps = [];
|
|
41
|
+
for (const depRef of task.depends_on) {
|
|
42
|
+
const result = index.resolve(depRef);
|
|
43
|
+
if (result.ok) {
|
|
44
|
+
const depItem = result.item;
|
|
45
|
+
if ('status' in depItem && depItem.status !== 'completed') {
|
|
46
|
+
unmetDeps.push(depRef);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
ref: index.shortUlid(task._ulid),
|
|
52
|
+
title: task.title,
|
|
53
|
+
blocked_by: task.blocked_by,
|
|
54
|
+
unmet_deps: unmetDeps,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function toCompletedTaskSummary(task, index) {
|
|
58
|
+
return {
|
|
59
|
+
ref: index.shortUlid(task._ulid),
|
|
60
|
+
title: task.title,
|
|
61
|
+
completed_at: task.completed_at || '',
|
|
62
|
+
closed_reason: task.closed_reason || null,
|
|
63
|
+
origin: task.origin,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function collectRecentNotes(tasks, index, options) {
|
|
67
|
+
const allNotes = [];
|
|
68
|
+
for (const task of tasks) {
|
|
69
|
+
for (const note of task.notes) {
|
|
70
|
+
const noteDate = new Date(note.created_at);
|
|
71
|
+
// Filter by since date if provided
|
|
72
|
+
if (options.since && noteDate < options.since) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
allNotes.push({
|
|
76
|
+
task_ref: index.shortUlid(task._ulid),
|
|
77
|
+
task_title: task.title,
|
|
78
|
+
note_ulid: note._ulid.slice(0, 8),
|
|
79
|
+
created_at: note.created_at,
|
|
80
|
+
author: note.author || null,
|
|
81
|
+
content: note.content,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Sort by date descending, take limit
|
|
86
|
+
return allNotes
|
|
87
|
+
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
|
88
|
+
.slice(0, options.limit);
|
|
89
|
+
}
|
|
90
|
+
function collectIncompleteTodos(tasks, index, options) {
|
|
91
|
+
const allTodos = [];
|
|
92
|
+
for (const task of tasks) {
|
|
93
|
+
for (const todo of task.todos) {
|
|
94
|
+
// Only include incomplete todos
|
|
95
|
+
if (todo.done)
|
|
96
|
+
continue;
|
|
97
|
+
allTodos.push({
|
|
98
|
+
task_ref: index.shortUlid(task._ulid),
|
|
99
|
+
task_title: task.title,
|
|
100
|
+
id: todo.id,
|
|
101
|
+
text: todo.text,
|
|
102
|
+
added_at: todo.added_at,
|
|
103
|
+
added_by: todo.added_by || null,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Sort by added_at descending (most recent first), take limit
|
|
108
|
+
return allTodos
|
|
109
|
+
.sort((a, b) => new Date(b.added_at).getTime() - new Date(a.added_at).getTime())
|
|
110
|
+
.slice(0, options.limit);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Gather session context data
|
|
114
|
+
*/
|
|
115
|
+
export async function gatherSessionContext(ctx, options) {
|
|
116
|
+
const limit = parseInt(options.limit || '10', 10);
|
|
117
|
+
const sinceDate = options.since ? parseTimeSpec(options.since) : null;
|
|
118
|
+
const showGit = options.git !== false; // default true
|
|
119
|
+
// Load all data
|
|
120
|
+
const allTasks = await loadAllTasks(ctx);
|
|
121
|
+
const items = await loadAllItems(ctx);
|
|
122
|
+
const inboxItems = await loadInboxItems(ctx);
|
|
123
|
+
const index = new ReferenceIndex(allTasks, items);
|
|
124
|
+
// Compute stats
|
|
125
|
+
const stats = {
|
|
126
|
+
total_tasks: allTasks.length,
|
|
127
|
+
in_progress: allTasks.filter((t) => t.status === 'in_progress').length,
|
|
128
|
+
pending_review: allTasks.filter((t) => t.status === 'pending_review').length,
|
|
129
|
+
ready: getReadyTasks(allTasks).length,
|
|
130
|
+
blocked: allTasks.filter((t) => t.status === 'blocked').length,
|
|
131
|
+
completed: allTasks.filter((t) => t.status === 'completed').length,
|
|
132
|
+
inbox_items: inboxItems.length,
|
|
133
|
+
};
|
|
134
|
+
// Get active tasks
|
|
135
|
+
const activeTasks = allTasks
|
|
136
|
+
.filter((t) => t.status === 'in_progress')
|
|
137
|
+
.sort((a, b) => a.priority - b.priority)
|
|
138
|
+
.slice(0, options.full ? undefined : limit)
|
|
139
|
+
.map((t) => toActiveTaskSummary(t, index));
|
|
140
|
+
// Get pending review tasks
|
|
141
|
+
const pendingReviewTasks = allTasks
|
|
142
|
+
.filter((t) => t.status === 'pending_review')
|
|
143
|
+
.sort((a, b) => a.priority - b.priority)
|
|
144
|
+
.slice(0, options.full ? undefined : limit)
|
|
145
|
+
.map((t) => toActiveTaskSummary(t, index));
|
|
146
|
+
// Get recent notes from active tasks
|
|
147
|
+
const recentNotes = collectRecentNotes(allTasks.filter((t) => t.status === 'in_progress'), index, { limit: options.full ? limit * 2 : limit, since: sinceDate });
|
|
148
|
+
// Get incomplete todos from active tasks
|
|
149
|
+
const activeTodos = collectIncompleteTodos(allTasks.filter((t) => t.status === 'in_progress'), index, { limit: options.full ? limit * 2 : limit });
|
|
150
|
+
// Get ready tasks (optionally filtered to automation-eligible only)
|
|
151
|
+
const readyTasks = getReadyTasks(allTasks)
|
|
152
|
+
.filter((t) => !options.eligible || t.automation === 'eligible')
|
|
153
|
+
.slice(0, options.full ? undefined : limit)
|
|
154
|
+
.map((t) => toReadyTaskSummary(t, index));
|
|
155
|
+
// Get blocked tasks
|
|
156
|
+
const blockedTasks = allTasks
|
|
157
|
+
.filter((t) => t.status === 'blocked')
|
|
158
|
+
.slice(0, options.full ? undefined : limit)
|
|
159
|
+
.map((t) => toBlockedTaskSummary(t, allTasks, index));
|
|
160
|
+
// Get recently completed tasks
|
|
161
|
+
const recentlyCompleted = allTasks
|
|
162
|
+
.filter((t) => {
|
|
163
|
+
if (t.status !== 'completed' || !t.completed_at)
|
|
164
|
+
return false;
|
|
165
|
+
const completedDate = new Date(t.completed_at);
|
|
166
|
+
if (sinceDate && completedDate < sinceDate)
|
|
167
|
+
return false;
|
|
168
|
+
return true;
|
|
169
|
+
})
|
|
170
|
+
.sort((a, b) => {
|
|
171
|
+
// Sort by completed_at descending (most recent first)
|
|
172
|
+
const aDate = new Date(a.completed_at || 0);
|
|
173
|
+
const bDate = new Date(b.completed_at || 0);
|
|
174
|
+
return bDate.getTime() - aDate.getTime();
|
|
175
|
+
})
|
|
176
|
+
.slice(0, options.full ? undefined : limit)
|
|
177
|
+
.map((t) => toCompletedTaskSummary(t, index));
|
|
178
|
+
// Get git info
|
|
179
|
+
let branch = null;
|
|
180
|
+
let recentCommits = [];
|
|
181
|
+
let workingTree = null;
|
|
182
|
+
if (showGit && isGitRepo(ctx.rootDir)) {
|
|
183
|
+
branch = getCurrentBranch(ctx.rootDir);
|
|
184
|
+
const commits = getRecentCommits({
|
|
185
|
+
limit: options.full ? limit * 2 : limit,
|
|
186
|
+
since: sinceDate || undefined,
|
|
187
|
+
cwd: ctx.rootDir,
|
|
188
|
+
});
|
|
189
|
+
recentCommits = commits.map((c) => ({
|
|
190
|
+
hash: c.hash,
|
|
191
|
+
full_hash: c.fullHash,
|
|
192
|
+
date: c.date.toISOString(),
|
|
193
|
+
message: c.message,
|
|
194
|
+
author: c.author,
|
|
195
|
+
}));
|
|
196
|
+
workingTree = getWorkingTreeStatus(ctx.rootDir);
|
|
197
|
+
}
|
|
198
|
+
// Get inbox items (oldest first to encourage triage)
|
|
199
|
+
const inboxSummaries = inboxItems
|
|
200
|
+
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())
|
|
201
|
+
.slice(0, options.full ? undefined : limit)
|
|
202
|
+
.map((item) => ({
|
|
203
|
+
ref: item._ulid.slice(0, 8),
|
|
204
|
+
text: item.text,
|
|
205
|
+
created_at: item.created_at,
|
|
206
|
+
tags: item.tags,
|
|
207
|
+
added_by: item.added_by || null,
|
|
208
|
+
}));
|
|
209
|
+
// Load session context (focus, threads, questions)
|
|
210
|
+
const sessionContext = await loadSessionContext(ctx);
|
|
211
|
+
return {
|
|
212
|
+
generated_at: new Date().toISOString(),
|
|
213
|
+
branch,
|
|
214
|
+
context: sessionContext,
|
|
215
|
+
active_tasks: activeTasks,
|
|
216
|
+
pending_review_tasks: pendingReviewTasks,
|
|
217
|
+
recent_notes: recentNotes,
|
|
218
|
+
active_todos: activeTodos,
|
|
219
|
+
ready_tasks: readyTasks,
|
|
220
|
+
blocked_tasks: blockedTasks,
|
|
221
|
+
recently_completed: recentlyCompleted,
|
|
222
|
+
recent_commits: recentCommits,
|
|
223
|
+
working_tree: workingTree,
|
|
224
|
+
inbox_items: inboxSummaries,
|
|
225
|
+
stats,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Perform session checkpoint - check for uncommitted work before ending session.
|
|
230
|
+
*
|
|
231
|
+
* This is designed for use as a Claude Code stop hook. It checks for:
|
|
232
|
+
* - Uncommitted git changes (staged, unstaged, untracked)
|
|
233
|
+
* - Tasks in in_progress status
|
|
234
|
+
* - Incomplete todos on active tasks
|
|
235
|
+
*
|
|
236
|
+
* Returns a structured result indicating whether the session can end cleanly.
|
|
237
|
+
*/
|
|
238
|
+
export async function performCheckpoint(ctx, options) {
|
|
239
|
+
const issues = [];
|
|
240
|
+
const instructions = [];
|
|
241
|
+
// Load tasks
|
|
242
|
+
const allTasks = await loadAllTasks(ctx);
|
|
243
|
+
// Check for in-progress tasks
|
|
244
|
+
const inProgressTasks = allTasks.filter((t) => t.status === 'in_progress');
|
|
245
|
+
for (const task of inProgressTasks) {
|
|
246
|
+
const ref = task.slugs[0] ? `@${task.slugs[0]}` : `@${task._ulid.slice(0, 8)}`;
|
|
247
|
+
issues.push({
|
|
248
|
+
type: 'in_progress_task',
|
|
249
|
+
description: `Task ${ref} is still in progress: ${task.title}`,
|
|
250
|
+
details: {
|
|
251
|
+
ref,
|
|
252
|
+
title: task.title,
|
|
253
|
+
started_at: task.started_at,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
// Check for incomplete todos on this task
|
|
257
|
+
const incompleteTodos = task.todos.filter((t) => !t.done);
|
|
258
|
+
for (const todo of incompleteTodos) {
|
|
259
|
+
issues.push({
|
|
260
|
+
type: 'incomplete_todo',
|
|
261
|
+
description: `Incomplete todo on ${ref}: ${todo.text}`,
|
|
262
|
+
details: {
|
|
263
|
+
task_ref: ref,
|
|
264
|
+
todo_id: todo.id,
|
|
265
|
+
text: todo.text,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Check for uncommitted git changes
|
|
271
|
+
if (isGitRepo(ctx.rootDir)) {
|
|
272
|
+
const workingTree = getWorkingTreeStatus(ctx.rootDir);
|
|
273
|
+
if (!workingTree.clean) {
|
|
274
|
+
const changeCount = workingTree.staged.length +
|
|
275
|
+
workingTree.unstaged.length +
|
|
276
|
+
workingTree.untracked.length;
|
|
277
|
+
issues.push({
|
|
278
|
+
type: 'uncommitted_changes',
|
|
279
|
+
description: `${changeCount} uncommitted changes in working tree`,
|
|
280
|
+
details: {
|
|
281
|
+
staged: workingTree.staged.length,
|
|
282
|
+
unstaged: workingTree.unstaged.length,
|
|
283
|
+
untracked: workingTree.untracked.length,
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Build instructions based on issues
|
|
289
|
+
if (issues.length > 0 && !options.force) {
|
|
290
|
+
instructions.push('Before ending this session, please:');
|
|
291
|
+
const hasInProgress = issues.some((i) => i.type === 'in_progress_task');
|
|
292
|
+
const hasUncommitted = issues.some((i) => i.type === 'uncommitted_changes');
|
|
293
|
+
const hasIncompleteTodos = issues.some((i) => i.type === 'incomplete_todo');
|
|
294
|
+
let step = 1;
|
|
295
|
+
if (hasInProgress) {
|
|
296
|
+
instructions.push(`${step++}. Add notes to in-progress tasks documenting current state`);
|
|
297
|
+
instructions.push(`${step++}. Either complete the tasks or leave them in_progress with clear notes for next session`);
|
|
298
|
+
}
|
|
299
|
+
if (hasIncompleteTodos) {
|
|
300
|
+
instructions.push(`${step++}. Complete or acknowledge incomplete todos on active tasks`);
|
|
301
|
+
}
|
|
302
|
+
if (hasUncommitted) {
|
|
303
|
+
instructions.push(`${step++}. Commit your changes with a descriptive message`);
|
|
304
|
+
// Add WIP commit guidance if there are in-progress tasks
|
|
305
|
+
if (inProgressTasks.length > 0) {
|
|
306
|
+
const task = inProgressTasks[0];
|
|
307
|
+
const guidance = formatCommitGuidance(task, { wip: true });
|
|
308
|
+
instructions.push('');
|
|
309
|
+
instructions.push('Suggested WIP commit:');
|
|
310
|
+
instructions.push(` ${guidance.message}`);
|
|
311
|
+
instructions.push('');
|
|
312
|
+
for (const trailer of guidance.trailers) {
|
|
313
|
+
instructions.push(` ${trailer}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
instructions.push('');
|
|
318
|
+
instructions.push('Use: kspec task note @task "Progress notes..." to document state');
|
|
319
|
+
instructions.push('Use: kspec task complete @task --reason "Summary" if task is done');
|
|
320
|
+
}
|
|
321
|
+
// Allow stop if:
|
|
322
|
+
// - No issues found
|
|
323
|
+
// - --force flag passed
|
|
324
|
+
// - This is a retry (stop_hook_active = true from previous block)
|
|
325
|
+
const isRetry = options.stopHookActive === true;
|
|
326
|
+
const ok = issues.length === 0 || options.force === true || isRetry;
|
|
327
|
+
let message;
|
|
328
|
+
if (isRetry && issues.length > 0) {
|
|
329
|
+
message = `[kspec] Session checkpoint: ${issues.length} issue(s) acknowledged - allowing stop`;
|
|
330
|
+
}
|
|
331
|
+
else if (ok) {
|
|
332
|
+
message = '[kspec] Session checkpoint passed - ready to end session';
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
message = `[kspec] Session checkpoint: ${issues.length} issue(s) need attention`;
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
ok,
|
|
339
|
+
message,
|
|
340
|
+
issues,
|
|
341
|
+
instructions,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
// ─── Output Formatting ───────────────────────────────────────────────────────
|
|
345
|
+
function formatCheckpointResult(result) {
|
|
346
|
+
if (result.ok) {
|
|
347
|
+
console.log(chalk.green(result.message));
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
console.log(chalk.yellow(result.message));
|
|
351
|
+
console.log('');
|
|
352
|
+
for (const issue of result.issues) {
|
|
353
|
+
const icon = issue.type === 'uncommitted_changes'
|
|
354
|
+
? chalk.yellow('⚠')
|
|
355
|
+
: issue.type === 'in_progress_task'
|
|
356
|
+
? chalk.blue('●')
|
|
357
|
+
: chalk.gray('○');
|
|
358
|
+
console.log(` ${icon} ${issue.description}`);
|
|
359
|
+
}
|
|
360
|
+
if (result.instructions.length > 0) {
|
|
361
|
+
console.log('');
|
|
362
|
+
for (const instruction of result.instructions) {
|
|
363
|
+
console.log(chalk.gray(instruction));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function formatSessionContext(ctx, options) {
|
|
369
|
+
const isBrief = !options.full;
|
|
370
|
+
// Header
|
|
371
|
+
console.log(`\n${sessionHeaders.title}`);
|
|
372
|
+
const age = formatRelativeTime(new Date(ctx.generated_at));
|
|
373
|
+
if (ctx.branch) {
|
|
374
|
+
console.log(chalk.gray(`Branch: ${ctx.branch} | Generated: ${age}`));
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
console.log(chalk.gray(`Generated: ${age}`));
|
|
378
|
+
}
|
|
379
|
+
// Stats summary
|
|
380
|
+
const pendingReviewNote = ctx.stats.pending_review > 0
|
|
381
|
+
? `${ctx.stats.pending_review} awaiting review, `
|
|
382
|
+
: '';
|
|
383
|
+
const inboxNote = ctx.stats.inbox_items > 0
|
|
384
|
+
? ` | Inbox: ${ctx.stats.inbox_items}`
|
|
385
|
+
: '';
|
|
386
|
+
console.log(chalk.gray(`Tasks: ${ctx.stats.in_progress} active, ${pendingReviewNote}${ctx.stats.ready} ready, ` +
|
|
387
|
+
`${ctx.stats.blocked} blocked, ${ctx.stats.completed}/${ctx.stats.total_tasks} completed${inboxNote}`));
|
|
388
|
+
// Session context section (focus, threads, questions)
|
|
389
|
+
if (ctx.context && (ctx.context.focus || ctx.context.threads.length > 0 || ctx.context.open_questions.length > 0)) {
|
|
390
|
+
console.log('\n--- Session Context ---');
|
|
391
|
+
if (ctx.context.focus) {
|
|
392
|
+
console.log(` ${chalk.cyan('Focus:')} ${ctx.context.focus}`);
|
|
393
|
+
}
|
|
394
|
+
if (ctx.context.threads.length > 0) {
|
|
395
|
+
console.log(` ${chalk.cyan('Active Threads:')}`);
|
|
396
|
+
for (const thread of ctx.context.threads) {
|
|
397
|
+
console.log(` - ${thread}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (ctx.context.open_questions.length > 0) {
|
|
401
|
+
console.log(` ${chalk.cyan('Open Questions:')}`);
|
|
402
|
+
for (const question of ctx.context.open_questions) {
|
|
403
|
+
console.log(` - ${question}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// Active tasks section
|
|
408
|
+
if (ctx.active_tasks.length > 0) {
|
|
409
|
+
console.log(`\n${sessionHeaders.activeWork}`);
|
|
410
|
+
for (const task of ctx.active_tasks) {
|
|
411
|
+
const started = task.started_at
|
|
412
|
+
? chalk.gray(` (started ${formatRelativeTime(new Date(task.started_at))})`)
|
|
413
|
+
: '';
|
|
414
|
+
const priority = task.priority <= 2
|
|
415
|
+
? chalk.red(`P${task.priority}`)
|
|
416
|
+
: chalk.gray(`P${task.priority}`);
|
|
417
|
+
console.log(` ${chalk.blue('[in_progress]')} ${priority} ${task.ref} ${task.title}${started}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
console.log(`\n${sessionHeaders.noActiveWork}`);
|
|
422
|
+
}
|
|
423
|
+
// Awaiting review section
|
|
424
|
+
if (ctx.pending_review_tasks.length > 0) {
|
|
425
|
+
console.log(`\n${sessionHeaders.awaitingReview}`);
|
|
426
|
+
for (const task of ctx.pending_review_tasks) {
|
|
427
|
+
const priority = task.priority <= 2
|
|
428
|
+
? chalk.red(`P${task.priority}`)
|
|
429
|
+
: chalk.gray(`P${task.priority}`);
|
|
430
|
+
console.log(` ${chalk.yellow('[pending_review]')} ${priority} ${task.ref} ${task.title}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// Recently completed section
|
|
434
|
+
if (ctx.recently_completed.length > 0) {
|
|
435
|
+
console.log(`\n${sessionHeaders.recentlyCompleted}`);
|
|
436
|
+
const observationPromotedTasks = [];
|
|
437
|
+
for (const task of ctx.recently_completed) {
|
|
438
|
+
const completedAge = formatRelativeTime(new Date(task.completed_at));
|
|
439
|
+
let reason = '';
|
|
440
|
+
if (task.closed_reason) {
|
|
441
|
+
const maxLen = isBrief ? 60 : 120;
|
|
442
|
+
const truncated = task.closed_reason.length > maxLen
|
|
443
|
+
? task.closed_reason.slice(0, maxLen).trim() + '...'
|
|
444
|
+
: task.closed_reason;
|
|
445
|
+
reason = chalk.gray(` - ${truncated}`);
|
|
446
|
+
}
|
|
447
|
+
console.log(` ${chalk.green('[completed]')} ${task.ref} ${task.title} ${chalk.gray(`(${completedAge})`)}${reason}`);
|
|
448
|
+
// Track tasks that came from observations
|
|
449
|
+
if (task.origin === 'observation_promotion') {
|
|
450
|
+
observationPromotedTasks.push(task.ref);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// Show reminder about resolving observations
|
|
454
|
+
if (observationPromotedTasks.length > 0) {
|
|
455
|
+
console.log(chalk.yellow(`\n ℹ Consider resolving linked observations: ${observationPromotedTasks.join(', ')}`));
|
|
456
|
+
console.log(chalk.gray(` Run: kspec meta observations --pending-resolution`));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// Recent notes section
|
|
460
|
+
if (ctx.recent_notes.length > 0) {
|
|
461
|
+
console.log(`\n${sessionHeaders.recentNotes}`);
|
|
462
|
+
for (const note of ctx.recent_notes) {
|
|
463
|
+
const age = formatRelativeTime(new Date(note.created_at));
|
|
464
|
+
const author = note.author ? chalk.gray(` by ${note.author}`) : '';
|
|
465
|
+
console.log(` ${chalk.yellow(age)} on ${note.task_ref}${author}:`);
|
|
466
|
+
// Truncate content in brief mode
|
|
467
|
+
let content = note.content.trim();
|
|
468
|
+
if (isBrief && content.length > 200) {
|
|
469
|
+
content = content.slice(0, 200).trim() + '...';
|
|
470
|
+
}
|
|
471
|
+
// Indent content, limit lines in brief mode
|
|
472
|
+
const lines = content.split('\n');
|
|
473
|
+
const maxLines = isBrief ? 3 : lines.length;
|
|
474
|
+
for (const line of lines.slice(0, maxLines)) {
|
|
475
|
+
console.log(` ${chalk.white(line)}`);
|
|
476
|
+
}
|
|
477
|
+
if (isBrief && lines.length > maxLines) {
|
|
478
|
+
console.log(chalk.gray(` ... (${lines.length - maxLines} more lines)`));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// Incomplete todos section
|
|
483
|
+
if (ctx.active_todos.length > 0) {
|
|
484
|
+
console.log(`\n${sessionHeaders.incompleteTodos}`);
|
|
485
|
+
for (const todo of ctx.active_todos) {
|
|
486
|
+
console.log(` ${chalk.yellow('[ ]')} ${todo.task_ref}#${todo.id}: ${todo.text}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// Ready tasks section
|
|
490
|
+
if (ctx.ready_tasks.length > 0) {
|
|
491
|
+
console.log(`\n${sessionHeaders.readyTasks}`);
|
|
492
|
+
for (const task of ctx.ready_tasks) {
|
|
493
|
+
const priority = task.priority <= 2
|
|
494
|
+
? chalk.red(`P${task.priority}`)
|
|
495
|
+
: chalk.gray(`P${task.priority}`);
|
|
496
|
+
const tags = task.tags.length > 0 ? chalk.cyan(` #${task.tags.join(' #')}`) : '';
|
|
497
|
+
console.log(` ${priority} ${task.ref} ${task.title}${tags}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// Blocked tasks section
|
|
501
|
+
if (ctx.blocked_tasks.length > 0) {
|
|
502
|
+
console.log(`\n${sessionHeaders.blocked}`);
|
|
503
|
+
for (const task of ctx.blocked_tasks) {
|
|
504
|
+
console.log(` ${chalk.red('[blocked]')} ${task.ref} ${task.title}`);
|
|
505
|
+
if (task.blocked_by.length > 0) {
|
|
506
|
+
console.log(chalk.gray(` Blockers: ${task.blocked_by.join(', ')}`));
|
|
507
|
+
}
|
|
508
|
+
if (task.unmet_deps.length > 0) {
|
|
509
|
+
console.log(chalk.gray(` Waiting on: ${task.unmet_deps.join(', ')}`));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// Git commits section
|
|
514
|
+
if (ctx.recent_commits.length > 0) {
|
|
515
|
+
console.log(`\n${sessionHeaders.recentCommits}`);
|
|
516
|
+
for (const commit of ctx.recent_commits) {
|
|
517
|
+
const age = formatRelativeTime(new Date(commit.date));
|
|
518
|
+
console.log(` ${chalk.yellow(commit.hash)} ${commit.message} ${chalk.gray(`(${age}, ${commit.author})`)}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// Inbox section (oldest first to encourage triage)
|
|
522
|
+
if (ctx.inbox_items.length > 0) {
|
|
523
|
+
console.log(`\n${sessionHeaders.inbox}`);
|
|
524
|
+
for (const item of ctx.inbox_items) {
|
|
525
|
+
const age = formatRelativeTime(new Date(item.created_at));
|
|
526
|
+
const author = item.added_by ? ` by ${item.added_by}` : '';
|
|
527
|
+
const tags = item.tags.length > 0 ? chalk.cyan(` [${item.tags.join(', ')}]`) : '';
|
|
528
|
+
// Truncate text in brief mode
|
|
529
|
+
let text = item.text;
|
|
530
|
+
if (isBrief && text.length > 60) {
|
|
531
|
+
text = text.slice(0, 60).trim() + '...';
|
|
532
|
+
}
|
|
533
|
+
console.log(` ${chalk.magenta(item.ref)} ${chalk.gray(`(${age}${author})`)}${tags}`);
|
|
534
|
+
console.log(` ${text}`);
|
|
535
|
+
}
|
|
536
|
+
console.log(` ${hints.inboxPromote}`);
|
|
537
|
+
}
|
|
538
|
+
// Working tree section
|
|
539
|
+
if (ctx.working_tree && !ctx.working_tree.clean) {
|
|
540
|
+
console.log(`\n${sessionHeaders.workingTree}`);
|
|
541
|
+
if (ctx.working_tree.staged.length > 0) {
|
|
542
|
+
console.log(chalk.green(' Staged:'));
|
|
543
|
+
for (const file of ctx.working_tree.staged) {
|
|
544
|
+
console.log(` ${chalk.green(file.status[0].toUpperCase())} ${file.path}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (ctx.working_tree.unstaged.length > 0) {
|
|
548
|
+
console.log(chalk.red(' Modified:'));
|
|
549
|
+
for (const file of ctx.working_tree.unstaged) {
|
|
550
|
+
console.log(` ${chalk.red(file.status[0].toUpperCase())} ${file.path}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (ctx.working_tree.untracked.length > 0) {
|
|
554
|
+
console.log(chalk.gray(' Untracked:'));
|
|
555
|
+
const limit = isBrief ? 5 : ctx.working_tree.untracked.length;
|
|
556
|
+
for (const path of ctx.working_tree.untracked.slice(0, limit)) {
|
|
557
|
+
console.log(` ${chalk.gray('?')} ${path}`);
|
|
558
|
+
}
|
|
559
|
+
if (isBrief && ctx.working_tree.untracked.length > limit) {
|
|
560
|
+
console.log(chalk.gray(` ... and ${ctx.working_tree.untracked.length - limit} more`));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
else if (ctx.working_tree?.clean) {
|
|
565
|
+
console.log(`\n${sessionHeaders.workingTreeClean}`);
|
|
566
|
+
}
|
|
567
|
+
// Quick Commands section - contextual hints based on state
|
|
568
|
+
const quickCommands = [];
|
|
569
|
+
if (ctx.active_tasks.length > 0) {
|
|
570
|
+
const ref = ctx.active_tasks[0].ref;
|
|
571
|
+
quickCommands.push(`kspec task note @${ref} "Progress..." ${chalk.gray('# document work')}`);
|
|
572
|
+
quickCommands.push(`kspec task complete @${ref} --reason "..." ${chalk.gray('# finish task')}`);
|
|
573
|
+
}
|
|
574
|
+
else if (ctx.ready_tasks.length > 0) {
|
|
575
|
+
const ref = ctx.ready_tasks[0].ref;
|
|
576
|
+
quickCommands.push(`kspec task start @${ref} ${chalk.gray('# begin work')}`);
|
|
577
|
+
}
|
|
578
|
+
if (ctx.inbox_items.length > 0) {
|
|
579
|
+
const ref = ctx.inbox_items[0].ref;
|
|
580
|
+
quickCommands.push(`kspec inbox promote @${ref} --title "..." ${chalk.gray('# convert to task')}`);
|
|
581
|
+
}
|
|
582
|
+
if (ctx.working_tree && !ctx.working_tree.clean) {
|
|
583
|
+
quickCommands.push(`git add . && git commit -m "..." ${chalk.gray('# commit changes')}`);
|
|
584
|
+
}
|
|
585
|
+
if (quickCommands.length > 0) {
|
|
586
|
+
console.log(`\n${sessionHeaders.quickCommands}`);
|
|
587
|
+
for (const hint of quickCommands) {
|
|
588
|
+
console.log(` ${hint}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
console.log(''); // Final newline
|
|
592
|
+
}
|
|
593
|
+
// ─── Command Registration ────────────────────────────────────────────────────
|
|
594
|
+
async function sessionStartAction(options) {
|
|
595
|
+
try {
|
|
596
|
+
const ctx = await initContext();
|
|
597
|
+
// AC-2: Pull remote changes before showing session context
|
|
598
|
+
let syncResult = null;
|
|
599
|
+
if (ctx.shadow?.enabled) {
|
|
600
|
+
syncResult = await shadowPull(ctx.shadow.worktreeDir);
|
|
601
|
+
// AC-3: Warn about conflicts but continue with local state
|
|
602
|
+
if (syncResult.hadConflict) {
|
|
603
|
+
console.log(chalk.yellow('⚠ Shadow sync conflict detected. Run `kspec shadow resolve` to fix.'));
|
|
604
|
+
console.log(chalk.gray(' Continuing with local state...'));
|
|
605
|
+
console.log('');
|
|
606
|
+
}
|
|
607
|
+
else if (syncResult.pulled) {
|
|
608
|
+
console.log(chalk.gray('ℹ Synced shadow branch from remote'));
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const sessionCtx = await gatherSessionContext(ctx, options);
|
|
612
|
+
output(sessionCtx, () => formatSessionContext(sessionCtx, options));
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
error(errors.failures.gatherSessionContext, err);
|
|
616
|
+
process.exit(EXIT_CODES.ERROR);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Read stdin if available (non-blocking check for hook input)
|
|
621
|
+
*/
|
|
622
|
+
async function readStdinIfAvailable() {
|
|
623
|
+
// Check if stdin is a TTY (interactive) - if so, don't try to read
|
|
624
|
+
if (process.stdin.isTTY) {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
return new Promise((resolve) => {
|
|
628
|
+
let data = '';
|
|
629
|
+
const timeout = setTimeout(() => {
|
|
630
|
+
process.stdin.removeAllListeners();
|
|
631
|
+
resolve(data || null);
|
|
632
|
+
}, 100); // 100ms timeout for stdin
|
|
633
|
+
process.stdin.setEncoding('utf8');
|
|
634
|
+
process.stdin.on('data', (chunk) => {
|
|
635
|
+
data += chunk;
|
|
636
|
+
});
|
|
637
|
+
process.stdin.on('end', () => {
|
|
638
|
+
clearTimeout(timeout);
|
|
639
|
+
resolve(data || null);
|
|
640
|
+
});
|
|
641
|
+
process.stdin.on('error', () => {
|
|
642
|
+
clearTimeout(timeout);
|
|
643
|
+
resolve(null);
|
|
644
|
+
});
|
|
645
|
+
process.stdin.resume();
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Parse Claude Code hook input from stdin
|
|
650
|
+
*/
|
|
651
|
+
function parseHookInput(stdin) {
|
|
652
|
+
if (!stdin)
|
|
653
|
+
return null;
|
|
654
|
+
try {
|
|
655
|
+
return JSON.parse(stdin.trim());
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// ─── Prompt Check (UserPromptSubmit Hook) ────────────────────────────────────
|
|
662
|
+
/**
|
|
663
|
+
* Output spec-first reminder for UserPromptSubmit hook.
|
|
664
|
+
*
|
|
665
|
+
* This is a simple context injection - always outputs the reminder,
|
|
666
|
+
* and Claude (Opus) is smart enough to apply it when relevant.
|
|
667
|
+
*/
|
|
668
|
+
async function sessionPromptCheckAction() {
|
|
669
|
+
// Lean, instructive reminder with kspec prefix
|
|
670
|
+
console.log(sessionPrompt.specCheck);
|
|
671
|
+
}
|
|
672
|
+
async function sessionCheckpointAction(options) {
|
|
673
|
+
try {
|
|
674
|
+
// Read stdin for Claude Code hook input
|
|
675
|
+
const stdin = await readStdinIfAvailable();
|
|
676
|
+
const hookInput = parseHookInput(stdin);
|
|
677
|
+
// Check if this is a retry (stop hook already active)
|
|
678
|
+
if (hookInput?.stop_hook_active) {
|
|
679
|
+
options.stopHookActive = true;
|
|
680
|
+
}
|
|
681
|
+
const ctx = await initContext();
|
|
682
|
+
const result = await performCheckpoint(ctx, options);
|
|
683
|
+
// Output format depends on mode:
|
|
684
|
+
// - JSON mode (--json): Output Claude Code hook format {"decision": "block", "reason": "..."}
|
|
685
|
+
// - Human mode: Output formatted checkpoint result
|
|
686
|
+
if (isJsonMode()) {
|
|
687
|
+
if (!result.ok) {
|
|
688
|
+
// Build reason message with issues and instructions
|
|
689
|
+
const issueLines = result.issues.map(i => `- ${i.description}`).join('\n');
|
|
690
|
+
const instructionLines = result.instructions.filter(i => i.trim()).join('\n');
|
|
691
|
+
const reason = `${result.message}\n\nIssues:\n${issueLines}\n\n${instructionLines}`;
|
|
692
|
+
console.log(JSON.stringify({ decision: 'block', reason }));
|
|
693
|
+
}
|
|
694
|
+
// If ok, exit silently (Claude Code expects no output when allowing stop)
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
formatCheckpointResult(result);
|
|
698
|
+
if (!result.ok) {
|
|
699
|
+
process.exit(EXIT_CODES.ERROR);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
catch (err) {
|
|
704
|
+
error(errors.failures.runCheckpoint, err);
|
|
705
|
+
process.exit(EXIT_CODES.ERROR);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Register the 'session' command group and aliases
|
|
710
|
+
*/
|
|
711
|
+
export function registerSessionCommands(program) {
|
|
712
|
+
const session = program
|
|
713
|
+
.command('session')
|
|
714
|
+
.description('Session management and context');
|
|
715
|
+
session
|
|
716
|
+
.command('start')
|
|
717
|
+
.alias('resume')
|
|
718
|
+
.description('Surface relevant context for starting a new working session')
|
|
719
|
+
.option('--brief', 'Compact summary (default)')
|
|
720
|
+
.option('--full', 'Comprehensive context dump')
|
|
721
|
+
.option('--since <time>', 'Filter by recency (ISO8601 or relative: 1h, 2d, 1w)')
|
|
722
|
+
.option('--no-git', 'Skip git commit information')
|
|
723
|
+
.option('-n, --limit <n>', 'Limit items per section', '10')
|
|
724
|
+
.action(sessionStartAction);
|
|
725
|
+
session
|
|
726
|
+
.command('checkpoint')
|
|
727
|
+
.description('Pre-stop hook: check for uncommitted work before ending session')
|
|
728
|
+
.option('--force', 'Allow session end regardless of issues')
|
|
729
|
+
.action(sessionCheckpointAction);
|
|
730
|
+
session
|
|
731
|
+
.command('prompt-check')
|
|
732
|
+
.description('UserPromptSubmit hook: inject spec-first reminder')
|
|
733
|
+
.action(sessionPromptCheckAction);
|
|
734
|
+
// Top-level alias: kspec context
|
|
735
|
+
program
|
|
736
|
+
.command('context')
|
|
737
|
+
.description('Alias for session start - surface session context')
|
|
738
|
+
.option('--brief', 'Compact summary (default)')
|
|
739
|
+
.option('--full', 'Comprehensive context dump')
|
|
740
|
+
.option('--since <time>', 'Filter by recency (ISO8601 or relative: 1h, 2d, 1w)')
|
|
741
|
+
.option('--no-git', 'Skip git commit information')
|
|
742
|
+
.option('-n, --limit <n>', 'Limit items per section', '10')
|
|
743
|
+
.action(sessionStartAction);
|
|
744
|
+
}
|
|
745
|
+
//# sourceMappingURL=session.js.map
|