@kynetic-ai/spec 0.3.0 → 0.5.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/dist/cli/batch-exec.d.ts +0 -9
- package/dist/cli/batch-exec.d.ts.map +1 -1
- package/dist/cli/batch-exec.js +16 -4
- package/dist/cli/batch-exec.js.map +1 -1
- package/dist/cli/commands/derive.d.ts.map +1 -1
- package/dist/cli/commands/derive.js +2 -1
- package/dist/cli/commands/derive.js.map +1 -1
- package/dist/cli/commands/guard.d.ts +43 -0
- package/dist/cli/commands/guard.d.ts.map +1 -0
- package/dist/cli/commands/guard.js +200 -0
- package/dist/cli/commands/guard.js.map +1 -0
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/item.d.ts.map +1 -1
- package/dist/cli/commands/item.js +18 -0
- package/dist/cli/commands/item.js.map +1 -1
- package/dist/cli/commands/log.d.ts.map +1 -1
- package/dist/cli/commands/log.js +5 -4
- package/dist/cli/commands/log.js.map +1 -1
- package/dist/cli/commands/meta.d.ts.map +1 -1
- package/dist/cli/commands/meta.js +2 -1
- package/dist/cli/commands/meta.js.map +1 -1
- package/dist/cli/commands/plan-import.d.ts.map +1 -1
- package/dist/cli/commands/plan-import.js +100 -30
- package/dist/cli/commands/plan-import.js.map +1 -1
- package/dist/cli/commands/ralph.d.ts.map +1 -1
- package/dist/cli/commands/ralph.js +143 -330
- package/dist/cli/commands/ralph.js.map +1 -1
- package/dist/cli/commands/session.d.ts +73 -1
- package/dist/cli/commands/session.d.ts.map +1 -1
- package/dist/cli/commands/session.js +607 -162
- package/dist/cli/commands/session.js.map +1 -1
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +97 -217
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/skill-install.d.ts +4 -1
- package/dist/cli/commands/skill-install.d.ts.map +1 -1
- package/dist/cli/commands/skill-install.js +62 -5
- package/dist/cli/commands/skill-install.js.map +1 -1
- package/dist/cli/commands/task.d.ts.map +1 -1
- package/dist/cli/commands/task.js +128 -59
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/cli/commands/tasks.d.ts.map +1 -1
- package/dist/cli/commands/tasks.js +2 -4
- package/dist/cli/commands/tasks.js.map +1 -1
- package/dist/cli/commands/triage.d.ts.map +1 -1
- package/dist/cli/commands/triage.js +12 -98
- package/dist/cli/commands/triage.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +18 -4
- package/dist/cli/output.js.map +1 -1
- package/dist/daemon/routes/triage.ts +4 -70
- package/dist/parser/config.d.ts +106 -0
- package/dist/parser/config.d.ts.map +1 -1
- package/dist/parser/config.js +47 -0
- package/dist/parser/config.js.map +1 -1
- package/dist/parser/file-lock.d.ts +14 -0
- package/dist/parser/file-lock.d.ts.map +1 -0
- package/dist/parser/file-lock.js +124 -0
- package/dist/parser/file-lock.js.map +1 -0
- package/dist/parser/index.d.ts +1 -0
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +1 -0
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/plan-document.d.ts +44 -0
- package/dist/parser/plan-document.d.ts.map +1 -1
- package/dist/parser/plan-document.js +76 -8
- package/dist/parser/plan-document.js.map +1 -1
- package/dist/parser/plans.d.ts.map +1 -1
- package/dist/parser/plans.js +28 -102
- package/dist/parser/plans.js.map +1 -1
- package/dist/parser/shadow.d.ts.map +1 -1
- package/dist/parser/shadow.js +11 -7
- package/dist/parser/shadow.js.map +1 -1
- package/dist/parser/yaml.d.ts.map +1 -1
- package/dist/parser/yaml.js +322 -297
- package/dist/parser/yaml.js.map +1 -1
- package/dist/ralph/events.d.ts.map +1 -1
- package/dist/ralph/events.js +24 -0
- package/dist/ralph/events.js.map +1 -1
- package/dist/ralph/index.d.ts +1 -1
- package/dist/ralph/index.d.ts.map +1 -1
- package/dist/ralph/index.js +1 -1
- package/dist/ralph/index.js.map +1 -1
- package/dist/ralph/subagent.d.ts +12 -1
- package/dist/ralph/subagent.d.ts.map +1 -1
- package/dist/ralph/subagent.js +22 -3
- package/dist/ralph/subagent.js.map +1 -1
- package/dist/schema/batch.d.ts +2 -0
- package/dist/schema/batch.d.ts.map +1 -1
- package/dist/schema/common.d.ts +6 -0
- package/dist/schema/common.d.ts.map +1 -1
- package/dist/schema/common.js +8 -0
- package/dist/schema/common.js.map +1 -1
- package/dist/schema/task.d.ts +22 -0
- package/dist/schema/task.d.ts.map +1 -1
- package/dist/schema/task.js +7 -0
- package/dist/schema/task.js.map +1 -1
- package/dist/sessions/store.d.ts +226 -1
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +712 -38
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/types.d.ts +51 -2
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js +25 -0
- package/dist/sessions/types.js.map +1 -1
- package/dist/strings/errors.d.ts +4 -0
- package/dist/strings/errors.d.ts.map +1 -1
- package/dist/strings/errors.js +2 -0
- package/dist/strings/errors.js.map +1 -1
- package/dist/strings/labels.d.ts +2 -0
- package/dist/strings/labels.d.ts.map +1 -1
- package/dist/strings/labels.js +2 -0
- package/dist/strings/labels.js.map +1 -1
- package/dist/triage/actions.d.ts +27 -0
- package/dist/triage/actions.d.ts.map +1 -0
- package/dist/triage/actions.js +95 -0
- package/dist/triage/actions.js.map +1 -0
- package/dist/triage/constants.d.ts +6 -0
- package/dist/triage/constants.d.ts.map +1 -0
- package/dist/triage/constants.js +7 -0
- package/dist/triage/constants.js.map +1 -0
- package/dist/triage/index.d.ts +3 -0
- package/dist/triage/index.d.ts.map +1 -0
- package/dist/triage/index.js +3 -0
- package/dist/triage/index.js.map +1 -0
- package/dist/utils/git.d.ts +2 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +21 -5
- package/dist/utils/git.js.map +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/marketplace.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/kspec/skills/create-workflow/SKILL.md +235 -0
- package/plugin/plugins/kspec/skills/observations/SKILL.md +143 -0
- package/plugin/plugins/kspec/skills/plan/SKILL.md +343 -0
- package/plugin/plugins/kspec/skills/reflect/SKILL.md +161 -0
- package/plugin/plugins/kspec/skills/review/SKILL.md +230 -0
- package/plugin/plugins/kspec/skills/task-work/SKILL.md +319 -0
- package/plugin/plugins/kspec/skills/triage-automation/SKILL.md +140 -0
- package/plugin/plugins/kspec/skills/triage-inbox/SKILL.md +232 -0
- package/plugin/plugins/kspec/skills/writing-specs/SKILL.md +354 -0
- package/templates/agents-sections/03-task-lifecycle.md +2 -2
- package/templates/agents-sections/04-pr-workflow.md +3 -3
- package/templates/agents-sections/05-commit-convention.md +14 -0
- package/templates/skills/create-workflow/SKILL.md +228 -0
- package/templates/skills/manifest.yaml +45 -0
- package/templates/skills/observations/SKILL.md +137 -0
- package/templates/skills/plan/SKILL.md +336 -0
- package/templates/skills/reflect/SKILL.md +155 -0
- package/templates/skills/review/SKILL.md +223 -0
- package/templates/skills/task-work/SKILL.md +312 -0
- package/templates/skills/triage-automation/SKILL.md +134 -0
- package/templates/skills/triage-inbox/SKILL.md +225 -0
- package/templates/skills/writing-specs/SKILL.md +347 -0
|
@@ -4,20 +4,46 @@
|
|
|
4
4
|
* Provides context for starting/resuming work sessions.
|
|
5
5
|
*/
|
|
6
6
|
import chalk from "chalk";
|
|
7
|
-
import { getReadyTasks, initContext, loadAllItems, loadAllTasks, loadInboxItems, loadSessionContext, ReferenceIndex, } from "../../parser/index.js";
|
|
7
|
+
import { getReadyTasks, initContext, loadAllItems, loadAllTasks, loadInboxItems, loadSessionContext, loadTriageRecords, ReferenceIndex, } from "../../parser/index.js";
|
|
8
8
|
import { ShadowError, shadowPull, } from "../../parser/shadow.js";
|
|
9
|
-
import {
|
|
9
|
+
import { loadMetaContext } from "../../parser/meta.js";
|
|
10
|
+
import { ulid } from "ulid";
|
|
11
|
+
import { getAllSessionLogSummaries, getSessionLogDetail, resolveSessionId, readEvents, readSessionContext, computeSessionLogStats, computeToolUsageStats, computeTimePeriodStats, searchSessionEvents, deduplicatePhasedToolCalls, createSessionWithBudget, validateSessionId, injectClaudeCodeEnv, injectCodexEnv, injectGeminiEnv, injectOpenCodeEnv, getFallbackInjectionInstructions, } from "../../sessions/store.js";
|
|
10
12
|
import { errors, hints, sessionHeaders, sessionPrompt, } from "../../strings/index.js";
|
|
11
13
|
import { formatCommitGuidance, formatRelativeTime, getCurrentBranch, getRecentCommits, getWorkingTreeStatus, isGitRepo, parseTimeSpec, } from "../../utils/index.js";
|
|
14
|
+
import { markMutating } from "../command-annotations.js";
|
|
12
15
|
import { EXIT_CODES } from "../exit-codes.js";
|
|
13
|
-
import { error, isJsonMode, isNoteSuperseded, output } from "../output.js";
|
|
16
|
+
import { error, info, isJsonMode, isNoteSuperseded, output, success, warn } from "../output.js";
|
|
14
17
|
// ─── Data Gathering ──────────────────────────────────────────────────────────
|
|
18
|
+
/**
|
|
19
|
+
* Build a reverse dependency map: for each task ULID, count how many
|
|
20
|
+
* pending tasks depend on it. Unresolvable refs are silently skipped.
|
|
21
|
+
*/
|
|
22
|
+
function computeUnlocksMap(allTasks, index) {
|
|
23
|
+
const counts = new Map();
|
|
24
|
+
for (const task of allTasks) {
|
|
25
|
+
// Only count pending tasks as "unlockable" downstream work per spec
|
|
26
|
+
if (task.status !== "pending")
|
|
27
|
+
continue;
|
|
28
|
+
for (const depRef of task.depends_on) {
|
|
29
|
+
const result = index.resolve(depRef);
|
|
30
|
+
if (!result.ok)
|
|
31
|
+
continue; // AC: unresolvable refs silently skipped
|
|
32
|
+
const depUlid = result.item._ulid;
|
|
33
|
+
counts.set(depUlid, (counts.get(depUlid) || 0) + 1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return counts;
|
|
37
|
+
}
|
|
15
38
|
function toActiveTaskSummary(task, index) {
|
|
16
39
|
const lastNote = task.notes.length > 0 ? task.notes[task.notes.length - 1] : null;
|
|
17
40
|
const incompleteTodos = task.todos.filter((t) => !t.done).length;
|
|
18
41
|
return {
|
|
19
42
|
ref: index.shortUlid(task._ulid),
|
|
43
|
+
slug: task.slugs.length > 0 ? task.slugs[0] : null,
|
|
20
44
|
title: task.title,
|
|
45
|
+
description: task.description || null,
|
|
46
|
+
status: task.status,
|
|
21
47
|
started_at: task.started_at || null,
|
|
22
48
|
priority: task.priority,
|
|
23
49
|
spec_ref: task.spec_ref || null,
|
|
@@ -27,16 +53,19 @@ function toActiveTaskSummary(task, index) {
|
|
|
27
53
|
incomplete_todos: incompleteTodos,
|
|
28
54
|
};
|
|
29
55
|
}
|
|
30
|
-
function toReadyTaskSummary(task, index) {
|
|
56
|
+
function toReadyTaskSummary(task, index, unlocksMap) {
|
|
31
57
|
return {
|
|
32
58
|
ref: index.shortUlid(task._ulid),
|
|
59
|
+
slug: task.slugs.length > 0 ? task.slugs[0] : null,
|
|
33
60
|
title: task.title,
|
|
61
|
+
description: task.description || null,
|
|
34
62
|
priority: task.priority,
|
|
35
63
|
spec_ref: task.spec_ref || null,
|
|
36
64
|
tags: task.tags,
|
|
65
|
+
unlocks: unlocksMap.get(task._ulid) || 0,
|
|
37
66
|
};
|
|
38
67
|
}
|
|
39
|
-
function toBlockedTaskSummary(task, _allTasks, index) {
|
|
68
|
+
function toBlockedTaskSummary(task, _allTasks, index, unlocksMap) {
|
|
40
69
|
// Find unmet dependencies
|
|
41
70
|
const unmetDeps = [];
|
|
42
71
|
for (const depRef of task.depends_on) {
|
|
@@ -50,14 +79,18 @@ function toBlockedTaskSummary(task, _allTasks, index) {
|
|
|
50
79
|
}
|
|
51
80
|
return {
|
|
52
81
|
ref: index.shortUlid(task._ulid),
|
|
82
|
+
slug: task.slugs.length > 0 ? task.slugs[0] : null,
|
|
53
83
|
title: task.title,
|
|
84
|
+
description: task.description || null,
|
|
54
85
|
blocked_by: task.blocked_by,
|
|
55
86
|
unmet_deps: unmetDeps,
|
|
87
|
+
unlocks: unlocksMap.get(task._ulid) || 0,
|
|
56
88
|
};
|
|
57
89
|
}
|
|
58
90
|
function toCompletedTaskSummary(task, index) {
|
|
59
91
|
return {
|
|
60
92
|
ref: index.shortUlid(task._ulid),
|
|
93
|
+
slug: task.slugs.length > 0 ? task.slugs[0] : null,
|
|
61
94
|
title: task.title,
|
|
62
95
|
completed_at: task.completed_at || "",
|
|
63
96
|
closed_reason: task.closed_reason || null,
|
|
@@ -120,6 +153,80 @@ function collectIncompleteTodos(tasks, index, options) {
|
|
|
120
153
|
.sort((a, b) => new Date(b.added_at).getTime() - new Date(a.added_at).getTime())
|
|
121
154
|
.slice(0, options.limit);
|
|
122
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Build a unified activity timeline from completed tasks and git commits.
|
|
158
|
+
*
|
|
159
|
+
* - Commits with Task: @slug trailers are matched to completed tasks and shown
|
|
160
|
+
* as combined "linked_commit" entries (AC: ac-activity-trailer-link, ac-activity-dedup)
|
|
161
|
+
* - Unlinked commits appear as standalone "commit" entries
|
|
162
|
+
* - Tasks not linked to any commit appear as standalone "task_completion" entries
|
|
163
|
+
* - All items sorted most recent first (AC: ac-activity-sort)
|
|
164
|
+
*
|
|
165
|
+
* @param completedTasks - Completed task summaries
|
|
166
|
+
* @param commits - Recent commit summaries (with task_refs parsed from trailers)
|
|
167
|
+
* @param taskRefResolver - Maps a trailer ref (slug or ULID prefix) to a completed task's ref (short ULID)
|
|
168
|
+
*/
|
|
169
|
+
function buildActivityTimeline(completedTasks, commits, taskRefResolver) {
|
|
170
|
+
const items = [];
|
|
171
|
+
// Build lookup from short ULID ref to CompletedTaskSummary
|
|
172
|
+
const taskByRef = new Map();
|
|
173
|
+
for (const task of completedTasks) {
|
|
174
|
+
taskByRef.set(task.ref, task);
|
|
175
|
+
}
|
|
176
|
+
// Track which tasks have been linked to a commit (for dedup)
|
|
177
|
+
const linkedTaskRefs = new Set();
|
|
178
|
+
for (const commit of commits) {
|
|
179
|
+
if (commit.task_refs.length > 0) {
|
|
180
|
+
let linkedTask;
|
|
181
|
+
for (const trailerRef of commit.task_refs) {
|
|
182
|
+
// Resolve the trailer ref (slug or ULID) to the short ULID ref
|
|
183
|
+
const resolvedRef = taskRefResolver.get(trailerRef);
|
|
184
|
+
if (resolvedRef) {
|
|
185
|
+
linkedTask = taskByRef.get(resolvedRef);
|
|
186
|
+
}
|
|
187
|
+
// Also try direct match on short ULID ref
|
|
188
|
+
if (!linkedTask) {
|
|
189
|
+
linkedTask = taskByRef.get(trailerRef);
|
|
190
|
+
}
|
|
191
|
+
if (linkedTask) {
|
|
192
|
+
linkedTaskRefs.add(linkedTask.ref);
|
|
193
|
+
// Use the later of commit date and task completion date for sort accuracy
|
|
194
|
+
const commitTime = new Date(commit.date).getTime();
|
|
195
|
+
const taskTime = new Date(linkedTask.completed_at).getTime();
|
|
196
|
+
const laterDate = taskTime > commitTime ? linkedTask.completed_at : commit.date;
|
|
197
|
+
items.push({
|
|
198
|
+
type: "linked_commit",
|
|
199
|
+
date: laterDate,
|
|
200
|
+
commit,
|
|
201
|
+
task: linkedTask,
|
|
202
|
+
});
|
|
203
|
+
break; // One linked entry per commit
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (!linkedTask) {
|
|
207
|
+
// Task ref in trailer but no matching completed task found
|
|
208
|
+
items.push({ type: "commit", date: commit.date, commit });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
items.push({ type: "commit", date: commit.date, commit });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Add task completions not already linked to a commit
|
|
216
|
+
for (const task of completedTasks) {
|
|
217
|
+
if (!linkedTaskRefs.has(task.ref)) {
|
|
218
|
+
items.push({
|
|
219
|
+
type: "task_completion",
|
|
220
|
+
date: task.completed_at,
|
|
221
|
+
task,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// AC: @session-start-activity-timeline ac-activity-sort
|
|
226
|
+
// Sort most recent first
|
|
227
|
+
items.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
228
|
+
return items;
|
|
229
|
+
}
|
|
123
230
|
/**
|
|
124
231
|
* Gather session context data
|
|
125
232
|
*/
|
|
@@ -131,7 +238,14 @@ export async function gatherSessionContext(ctx, options) {
|
|
|
131
238
|
const allTasks = await loadAllTasks(ctx);
|
|
132
239
|
const items = await loadAllItems(ctx);
|
|
133
240
|
const inboxItems = await loadInboxItems(ctx);
|
|
241
|
+
const triageRecords = await loadTriageRecords(ctx);
|
|
134
242
|
const index = new ReferenceIndex(allTasks, items);
|
|
243
|
+
// AC: @session-start-inbox-triage ac-inbox-untriaged-def
|
|
244
|
+
// Build lookup: inbox ULID → triage record (most recent if multiple)
|
|
245
|
+
const triageByInboxRef = new Map();
|
|
246
|
+
for (const record of triageRecords) {
|
|
247
|
+
triageByInboxRef.set(record.inbox_ref, { action: record.action });
|
|
248
|
+
}
|
|
135
249
|
// Compute stats
|
|
136
250
|
const stats = {
|
|
137
251
|
total_tasks: allTasks.length,
|
|
@@ -178,16 +292,21 @@ export async function gatherSessionContext(ctx, options) {
|
|
|
178
292
|
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
179
293
|
// Get incomplete todos from active tasks
|
|
180
294
|
const activeTodos = collectIncompleteTodos(allTasks.filter((t) => t.status === "in_progress" || t.status === "needs_work"), index, { limit: options.full ? limit * 2 : limit });
|
|
181
|
-
//
|
|
295
|
+
// Compute reverse dependency map for "unlocks N" annotations
|
|
296
|
+
const unlocksMap = computeUnlocksMap(allTasks, index);
|
|
297
|
+
// AC: @cmd-session-start ac-primer-default, ac-full-sections
|
|
298
|
+
// Primer: top 5 ready tasks; Full: all ready tasks
|
|
299
|
+
// Respect --limit as upper bound when provided
|
|
300
|
+
const readyLimit = options.full ? undefined : Math.min(limit, 5);
|
|
182
301
|
const readyTasks = getReadyTasks(allTasks)
|
|
183
302
|
.filter((t) => !options.eligible || t.automation === "eligible")
|
|
184
|
-
.slice(0,
|
|
185
|
-
.map((t) => toReadyTaskSummary(t, index));
|
|
303
|
+
.slice(0, readyLimit)
|
|
304
|
+
.map((t) => toReadyTaskSummary(t, index, unlocksMap));
|
|
186
305
|
// Get blocked tasks
|
|
187
306
|
const blockedTasks = allTasks
|
|
188
307
|
.filter((t) => t.status === "blocked")
|
|
189
308
|
.slice(0, options.full ? undefined : limit)
|
|
190
|
-
.map((t) => toBlockedTaskSummary(t, allTasks, index));
|
|
309
|
+
.map((t) => toBlockedTaskSummary(t, allTasks, index, unlocksMap));
|
|
191
310
|
// Get recently completed tasks
|
|
192
311
|
const recentlyCompleted = allTasks
|
|
193
312
|
.filter((t) => {
|
|
@@ -223,22 +342,93 @@ export async function gatherSessionContext(ctx, options) {
|
|
|
223
342
|
date: c.date.toISOString(),
|
|
224
343
|
message: c.message,
|
|
225
344
|
author: c.author,
|
|
345
|
+
task_refs: c.taskRefs,
|
|
226
346
|
}));
|
|
227
347
|
workingTree = getWorkingTreeStatus(ctx.rootDir);
|
|
228
348
|
}
|
|
229
|
-
// Get inbox items (oldest first to encourage triage)
|
|
230
|
-
|
|
349
|
+
// Get inbox items with triage status (oldest first to encourage triage)
|
|
350
|
+
// AC: @session-start-inbox-triage ac-inbox-untriaged-def
|
|
351
|
+
const allInboxSummaries = inboxItems
|
|
231
352
|
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())
|
|
232
|
-
.
|
|
233
|
-
.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
353
|
+
.map((item) => {
|
|
354
|
+
const triageInfo = triageByInboxRef.get(item._ulid);
|
|
355
|
+
return {
|
|
356
|
+
ref: item._ulid.slice(0, 8),
|
|
357
|
+
text: item.text,
|
|
358
|
+
created_at: item.created_at,
|
|
359
|
+
tags: item.tags,
|
|
360
|
+
added_by: item.added_by || null,
|
|
361
|
+
triaged: !!triageInfo,
|
|
362
|
+
triage_action: triageInfo?.action ?? null,
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
// AC: @session-start-inbox-triage ac-inbox-stat-line, ac-inbox-all-triaged
|
|
366
|
+
const inboxStats = {
|
|
367
|
+
total: allInboxSummaries.length,
|
|
368
|
+
untriaged: allInboxSummaries.filter((i) => !i.triaged).length,
|
|
369
|
+
deferred: allInboxSummaries.filter((i) => i.triage_action === "defer")
|
|
370
|
+
.length,
|
|
371
|
+
triaged: allInboxSummaries.filter((i) => i.triaged).length,
|
|
372
|
+
};
|
|
373
|
+
// JSON always gets full list with triage status; human display filters in formatSessionContext
|
|
374
|
+
const inboxSummaries = allInboxSummaries;
|
|
240
375
|
// Load session context (focus, threads, questions)
|
|
241
376
|
const sessionContext = await loadSessionContext(ctx);
|
|
377
|
+
// Build task ref resolver for activity timeline: maps slug/ULID to short ref
|
|
378
|
+
// This allows commits with Task: @task-slug trailers to match completed tasks
|
|
379
|
+
const taskRefResolver = new Map();
|
|
380
|
+
for (const task of allTasks) {
|
|
381
|
+
if (task.status !== "completed")
|
|
382
|
+
continue;
|
|
383
|
+
const shortRef = index.shortUlid(task._ulid);
|
|
384
|
+
// Map each slug to the short ref
|
|
385
|
+
for (const slug of task.slugs) {
|
|
386
|
+
taskRefResolver.set(slug, shortRef);
|
|
387
|
+
}
|
|
388
|
+
// Also map the full ULID and short ULID to itself
|
|
389
|
+
taskRefResolver.set(task._ulid, shortRef);
|
|
390
|
+
taskRefResolver.set(shortRef, shortRef);
|
|
391
|
+
}
|
|
392
|
+
// Build unified activity timeline
|
|
393
|
+
// AC: @session-start-activity-timeline ac-activity-merge
|
|
394
|
+
// AC: @cmd-session-start ac-primer-default, ac-full-sections
|
|
395
|
+
// Primer: 10 items; Full: 20 items
|
|
396
|
+
// Respect --limit as upper bound when provided
|
|
397
|
+
const activityLimit = options.full ? Math.min(limit * 2, 20) : Math.min(limit, 10);
|
|
398
|
+
const activityTimeline = buildActivityTimeline(recentlyCompleted, recentCommits, taskRefResolver).slice(0, activityLimit);
|
|
399
|
+
// AC: @cmd-session-start ac-full-sections — observations section (full mode only)
|
|
400
|
+
let observations = [];
|
|
401
|
+
if (options.full) {
|
|
402
|
+
const metaCtx = await loadMetaContext(ctx);
|
|
403
|
+
observations = metaCtx.observations
|
|
404
|
+
.filter((o) => !o.resolved)
|
|
405
|
+
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
|
406
|
+
.map((o) => ({
|
|
407
|
+
ref: o._ulid.slice(0, 8),
|
|
408
|
+
type: o.type,
|
|
409
|
+
content: o.content,
|
|
410
|
+
created_at: o.created_at,
|
|
411
|
+
author: o.author || null,
|
|
412
|
+
resolved: o.resolved,
|
|
413
|
+
workflow_ref: o.workflow_ref || null,
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
// AC: @session-start-computed-json ac-computed-unlocks
|
|
417
|
+
// Build task_unlocks map: short ULID ref → count of pending dependents
|
|
418
|
+
const taskUnlocks = {};
|
|
419
|
+
for (const [taskUlid, count] of unlocksMap) {
|
|
420
|
+
if (count > 0) {
|
|
421
|
+
taskUnlocks[index.shortUlid(taskUlid)] = count;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// AC: @session-start-computed-json ac-computed-inbox, ac-computed-unlocks, ac-computed-activity
|
|
425
|
+
const computed = {
|
|
426
|
+
inbox_untriaged_count: inboxStats.untriaged,
|
|
427
|
+
inbox_deferred_count: inboxStats.deferred,
|
|
428
|
+
inbox_total: inboxStats.total,
|
|
429
|
+
task_unlocks: taskUnlocks,
|
|
430
|
+
recent_activity: activityTimeline,
|
|
431
|
+
};
|
|
242
432
|
return {
|
|
243
433
|
generated_at: new Date().toISOString(),
|
|
244
434
|
branch,
|
|
@@ -251,9 +441,13 @@ export async function gatherSessionContext(ctx, options) {
|
|
|
251
441
|
blocked_tasks: blockedTasks,
|
|
252
442
|
recently_completed: recentlyCompleted,
|
|
253
443
|
recent_commits: recentCommits,
|
|
444
|
+
activity_timeline: activityTimeline,
|
|
254
445
|
working_tree: workingTree,
|
|
255
446
|
inbox_items: inboxSummaries,
|
|
447
|
+
inbox_stats: inboxStats,
|
|
448
|
+
observations,
|
|
256
449
|
stats,
|
|
450
|
+
computed,
|
|
257
451
|
};
|
|
258
452
|
}
|
|
259
453
|
/**
|
|
@@ -262,10 +456,17 @@ export async function gatherSessionContext(ctx, options) {
|
|
|
262
456
|
*/
|
|
263
457
|
export async function getIterationStats(ctx, since) {
|
|
264
458
|
const allTasks = await loadAllTasks(ctx);
|
|
459
|
+
// Count both completed and pending_review (submitted) tasks toward the limit.
|
|
460
|
+
// Submit means the agent's work is done — it should count the same as complete.
|
|
461
|
+
// AC: @ralph-task-limit ac-detection
|
|
265
462
|
const completedSince = allTasks.filter((t) => {
|
|
266
|
-
if (t.status
|
|
267
|
-
return
|
|
268
|
-
|
|
463
|
+
if (t.status === "completed" && t.completed_at) {
|
|
464
|
+
return new Date(t.completed_at) >= since;
|
|
465
|
+
}
|
|
466
|
+
if (t.status === "pending_review" && t.submitted_at) {
|
|
467
|
+
return new Date(t.submitted_at) >= since;
|
|
468
|
+
}
|
|
469
|
+
return false;
|
|
269
470
|
});
|
|
270
471
|
const startedSince = allTasks.filter((t) => {
|
|
271
472
|
if (!t.started_at)
|
|
@@ -427,7 +628,10 @@ function formatCheckpointResult(result) {
|
|
|
427
628
|
}
|
|
428
629
|
}
|
|
429
630
|
function formatSessionContext(ctx, options) {
|
|
430
|
-
const
|
|
631
|
+
const isFull = !!options.full;
|
|
632
|
+
// AC: @cmd-session-start ac-slug-display, ac-slug-fallback
|
|
633
|
+
// Display ref as @slug when available, @short-ulid when not
|
|
634
|
+
const displayRef = (task) => task.slug ? `@${task.slug}` : `@${task.ref}`;
|
|
431
635
|
// Header
|
|
432
636
|
console.log(`\n${sessionHeaders.title}`);
|
|
433
637
|
const age = formatRelativeTime(new Date(ctx.generated_at));
|
|
@@ -466,9 +670,15 @@ function formatSessionContext(ctx, options) {
|
|
|
466
670
|
}
|
|
467
671
|
}
|
|
468
672
|
}
|
|
469
|
-
//
|
|
673
|
+
// ── Section ordering per AC: @cmd-session-start ac-section-order ──
|
|
674
|
+
// active tasks → pending review → blocked → ready → recent activity → inbox → working tree → quick commands
|
|
675
|
+
// AC: @cmd-session-start ac-empty-skip — empty sections omitted entirely
|
|
676
|
+
// ── Active tasks section ──
|
|
677
|
+
// AC: @cmd-session-start ac-active-detail, ac-needs-work-indicator
|
|
470
678
|
if (ctx.active_tasks.length > 0) {
|
|
471
679
|
console.log(`\n${sessionHeaders.activeWork}`);
|
|
680
|
+
// Collect notes relevant to active tasks for inline display
|
|
681
|
+
const activeTaskNotes = ctx.recent_notes.filter((n) => n.task_status === "in_progress" || n.task_status === "needs_work");
|
|
472
682
|
for (const task of ctx.active_tasks) {
|
|
473
683
|
const started = task.started_at
|
|
474
684
|
? chalk.gray(` (started ${formatRelativeTime(new Date(task.started_at))})`)
|
|
@@ -476,106 +686,88 @@ function formatSessionContext(ctx, options) {
|
|
|
476
686
|
const priority = task.priority <= 2
|
|
477
687
|
? chalk.red(`P${task.priority}`)
|
|
478
688
|
: chalk.gray(`P${task.priority}`);
|
|
479
|
-
|
|
689
|
+
// AC: @cmd-session-start ac-needs-work-indicator
|
|
690
|
+
const statusLabel = task.status === "needs_work"
|
|
691
|
+
? chalk.red("[needs_work]")
|
|
692
|
+
: chalk.blue("[in_progress]");
|
|
693
|
+
console.log(` ${statusLabel} ${priority} ${displayRef(task)} ${task.title}${started}`);
|
|
694
|
+
// AC: @cmd-session-start ac-active-detail — show description
|
|
695
|
+
if (task.description) {
|
|
696
|
+
console.log(chalk.gray(` ${task.description}`));
|
|
697
|
+
}
|
|
698
|
+
// AC: @cmd-session-start ac-active-detail — show recent notes inline
|
|
699
|
+
const taskNotes = activeTaskNotes.filter((n) => n.task_ref === task.ref);
|
|
700
|
+
if (taskNotes.length > 0) {
|
|
701
|
+
const latestNote = taskNotes[0]; // already sorted most recent first
|
|
702
|
+
const noteAge = formatRelativeTime(new Date(latestNote.created_at));
|
|
703
|
+
const author = latestNote.author
|
|
704
|
+
? chalk.gray(` by ${latestNote.author}`)
|
|
705
|
+
: "";
|
|
706
|
+
console.log(` ${chalk.yellow("Note")} ${chalk.gray(`(${noteAge}${author})`)}`);
|
|
707
|
+
let content = latestNote.content.trim();
|
|
708
|
+
if (!isFull && content.length > 200) {
|
|
709
|
+
content = `${content.slice(0, 200).trim()}...`;
|
|
710
|
+
}
|
|
711
|
+
const lines = content.split("\n");
|
|
712
|
+
const maxLines = isFull ? lines.length : 3;
|
|
713
|
+
for (const line of lines.slice(0, maxLines)) {
|
|
714
|
+
console.log(` ${chalk.white(line)}`);
|
|
715
|
+
}
|
|
716
|
+
if (!isFull && lines.length > maxLines) {
|
|
717
|
+
console.log(chalk.gray(` ... (${lines.length - maxLines} more lines)`));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
480
720
|
}
|
|
481
721
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
// Awaiting review section
|
|
722
|
+
// ── Awaiting review section ──
|
|
723
|
+
// AC: @cmd-session-start ac-review-detail
|
|
486
724
|
if (ctx.pending_review_tasks.length > 0) {
|
|
487
725
|
console.log(`\n${sessionHeaders.awaitingReview}`);
|
|
726
|
+
const reviewNotes = ctx.recent_notes.filter((n) => n.task_status === "pending_review");
|
|
488
727
|
for (const task of ctx.pending_review_tasks) {
|
|
489
728
|
const priority = task.priority <= 2
|
|
490
729
|
? chalk.red(`P${task.priority}`)
|
|
491
730
|
: chalk.gray(`P${task.priority}`);
|
|
492
|
-
console.log(` ${chalk.yellow("[pending_review]")} ${priority} ${task
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
731
|
+
console.log(` ${chalk.yellow("[pending_review]")} ${priority} ${displayRef(task)} ${task.title}`);
|
|
732
|
+
// AC: @cmd-session-start ac-review-detail — show recent notes
|
|
733
|
+
const taskNotes = reviewNotes.filter((n) => n.task_ref === task.ref);
|
|
734
|
+
if (taskNotes.length > 0) {
|
|
735
|
+
const latestNote = taskNotes[0];
|
|
736
|
+
const noteAge = formatRelativeTime(new Date(latestNote.created_at));
|
|
737
|
+
const author = latestNote.author
|
|
738
|
+
? chalk.gray(` by ${latestNote.author}`)
|
|
739
|
+
: "";
|
|
740
|
+
console.log(` ${chalk.yellow("Note")} ${chalk.gray(`(${noteAge}${author})`)}`);
|
|
741
|
+
let content = latestNote.content.trim();
|
|
742
|
+
if (!isFull && content.length > 200) {
|
|
743
|
+
content = `${content.slice(0, 200).trim()}...`;
|
|
744
|
+
}
|
|
745
|
+
const lines = content.split("\n");
|
|
746
|
+
const maxLines = isFull ? lines.length : 3;
|
|
747
|
+
for (const line of lines.slice(0, maxLines)) {
|
|
748
|
+
console.log(` ${chalk.white(line)}`);
|
|
749
|
+
}
|
|
750
|
+
if (!isFull && lines.length > maxLines) {
|
|
751
|
+
console.log(chalk.gray(` ... (${lines.length - maxLines} more lines)`));
|
|
752
|
+
}
|
|
513
753
|
}
|
|
514
754
|
}
|
|
515
|
-
// Show reminder about resolving observations
|
|
516
|
-
if (observationPromotedTasks.length > 0) {
|
|
517
|
-
console.log(chalk.yellow(`\n ℹ Consider resolving linked observations: ${observationPromotedTasks.join(", ")}`));
|
|
518
|
-
console.log(chalk.gray(` Run: kspec meta observations --pending-resolution`));
|
|
519
|
-
}
|
|
520
755
|
}
|
|
521
|
-
//
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
// Helper to format a single note
|
|
530
|
-
const formatNote = (note) => {
|
|
531
|
-
const age = formatRelativeTime(new Date(note.created_at));
|
|
532
|
-
const author = note.author ? chalk.gray(` by ${note.author}`) : "";
|
|
533
|
-
console.log(` ${chalk.yellow(age)} on ${note.task_ref}${author}:`);
|
|
534
|
-
// Truncate content in brief mode
|
|
535
|
-
let content = note.content.trim();
|
|
536
|
-
if (isBrief && content.length > 200) {
|
|
537
|
-
content = `${content.slice(0, 200).trim()}...`;
|
|
538
|
-
}
|
|
539
|
-
// Indent content, limit lines in brief mode
|
|
540
|
-
const lines = content.split("\n");
|
|
541
|
-
const maxLines = isBrief ? 3 : lines.length;
|
|
542
|
-
for (const line of lines.slice(0, maxLines)) {
|
|
543
|
-
console.log(` ${chalk.white(line)}`);
|
|
544
|
-
}
|
|
545
|
-
if (isBrief && lines.length > maxLines) {
|
|
546
|
-
console.log(chalk.gray(` ... (${lines.length - maxLines} more lines)`));
|
|
547
|
-
}
|
|
548
|
-
};
|
|
549
|
-
// AC: @cmd-session-start ac-1 - In Progress notes
|
|
550
|
-
if (inProgressNotes.length > 0) {
|
|
551
|
-
console.log(` ${chalk.blue("In Progress:")}`);
|
|
552
|
-
for (const note of inProgressNotes) {
|
|
553
|
-
formatNote(note);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
// AC: @cmd-session-start ac-1 - Pending Review notes (grouped separately)
|
|
557
|
-
if (pendingReviewNotes.length > 0) {
|
|
558
|
-
console.log(` ${chalk.yellow("Pending Review:")}`);
|
|
559
|
-
for (const note of pendingReviewNotes) {
|
|
560
|
-
formatNote(note);
|
|
756
|
+
// ── Blocked tasks section ──
|
|
757
|
+
if (ctx.blocked_tasks.length > 0) {
|
|
758
|
+
console.log(`\n${sessionHeaders.blocked}`);
|
|
759
|
+
for (const task of ctx.blocked_tasks) {
|
|
760
|
+
const unlocks = task.unlocks > 0 ? chalk.green(` unlocks ${task.unlocks}`) : "";
|
|
761
|
+
console.log(` ${chalk.red("[blocked]")} ${displayRef(task)} ${task.title}${unlocks}`);
|
|
762
|
+
if (task.blocked_by.length > 0) {
|
|
763
|
+
console.log(chalk.gray(` Blockers: ${task.blocked_by.join(", ")}`));
|
|
561
764
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
if (completedNotes.length > 0) {
|
|
565
|
-
console.log(` ${chalk.green("Recently Completed:")}`);
|
|
566
|
-
for (const note of completedNotes) {
|
|
567
|
-
formatNote(note);
|
|
765
|
+
if (task.unmet_deps.length > 0) {
|
|
766
|
+
console.log(chalk.gray(` Waiting on: ${task.unmet_deps.join(", ")}`));
|
|
568
767
|
}
|
|
569
768
|
}
|
|
570
769
|
}
|
|
571
|
-
//
|
|
572
|
-
if (ctx.active_todos.length > 0) {
|
|
573
|
-
console.log(`\n${sessionHeaders.incompleteTodos}`);
|
|
574
|
-
for (const todo of ctx.active_todos) {
|
|
575
|
-
console.log(` ${chalk.yellow("[ ]")} ${todo.task_ref}#${todo.id}: ${todo.text}`);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
// Ready tasks section
|
|
770
|
+
// ── Ready tasks section ──
|
|
579
771
|
if (ctx.ready_tasks.length > 0) {
|
|
580
772
|
console.log(`\n${sessionHeaders.readyTasks}`);
|
|
581
773
|
for (const task of ctx.ready_tasks) {
|
|
@@ -583,48 +775,159 @@ function formatSessionContext(ctx, options) {
|
|
|
583
775
|
? chalk.red(`P${task.priority}`)
|
|
584
776
|
: chalk.gray(`P${task.priority}`);
|
|
585
777
|
const tags = task.tags.length > 0 ? chalk.cyan(` #${task.tags.join(" #")}`) : "";
|
|
586
|
-
|
|
778
|
+
const unlocks = task.unlocks > 0 ? chalk.green(` unlocks ${task.unlocks}`) : "";
|
|
779
|
+
console.log(` ${priority} ${displayRef(task)} ${task.title}${unlocks}${tags}`);
|
|
587
780
|
}
|
|
588
781
|
}
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
782
|
+
// ── Recent Activity timeline ──
|
|
783
|
+
// AC: @session-start-activity-timeline ac-activity-merge
|
|
784
|
+
if (ctx.activity_timeline.length > 0) {
|
|
785
|
+
console.log(`\n${sessionHeaders.recentActivity}`);
|
|
786
|
+
const observationPromotedTasks = [];
|
|
787
|
+
const taskGroups = new Map();
|
|
788
|
+
const groups = [];
|
|
789
|
+
for (const item of ctx.activity_timeline) {
|
|
790
|
+
if (item.type === "linked_commit") {
|
|
791
|
+
const key = item.task.ref;
|
|
792
|
+
let group = taskGroups.get(key);
|
|
793
|
+
if (!group) {
|
|
794
|
+
group = { task: item.task, commits: [], sortDate: item.date };
|
|
795
|
+
taskGroups.set(key, group);
|
|
796
|
+
}
|
|
797
|
+
group.commits.push({ commit: item.commit, date: item.commit.date });
|
|
798
|
+
// Update sortDate to the most recent event in the group
|
|
799
|
+
if (new Date(item.date).getTime() > new Date(group.sortDate).getTime()) {
|
|
800
|
+
group.sortDate = item.date;
|
|
801
|
+
}
|
|
596
802
|
}
|
|
597
|
-
if (
|
|
598
|
-
|
|
803
|
+
else if (item.type === "task_completion") {
|
|
804
|
+
groups.push({ kind: "task_completion", task: item.task, date: item.date });
|
|
805
|
+
}
|
|
806
|
+
else if (item.type === "commit") {
|
|
807
|
+
groups.push({ kind: "orphan_commit", commit: item.commit, date: item.date });
|
|
599
808
|
}
|
|
600
809
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
810
|
+
// Add task groups to the groups array
|
|
811
|
+
for (const group of taskGroups.values()) {
|
|
812
|
+
// AC: @session-start-activity-timeline ac-activity-sort
|
|
813
|
+
// Sort commits within a group chronologically (oldest first)
|
|
814
|
+
group.commits.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
|
815
|
+
groups.push({ kind: "task_group", ...group });
|
|
816
|
+
}
|
|
817
|
+
// AC: @session-start-activity-timeline ac-activity-sort
|
|
818
|
+
// Sort groups by most recent event, most recent first
|
|
819
|
+
groups.sort((a, b) => new Date(b.kind === "task_group" ? b.sortDate : b.date).getTime() -
|
|
820
|
+
new Date(a.kind === "task_group" ? a.sortDate : a.date).getTime());
|
|
821
|
+
for (const group of groups) {
|
|
822
|
+
if (group.kind === "task_completion") {
|
|
823
|
+
// Standalone completed task (not linked to any commit)
|
|
824
|
+
let reason = "";
|
|
825
|
+
if (group.task.closed_reason) {
|
|
826
|
+
const maxLen = isFull ? 120 : 60;
|
|
827
|
+
const truncated = group.task.closed_reason.length > maxLen
|
|
828
|
+
? `${group.task.closed_reason.slice(0, maxLen).trim()}...`
|
|
829
|
+
: group.task.closed_reason;
|
|
830
|
+
reason = chalk.gray(` - ${truncated}`);
|
|
831
|
+
}
|
|
832
|
+
// AC: @cmd-session-start ac-slug-display
|
|
833
|
+
const taskDisplay = group.task.slug
|
|
834
|
+
? `@${group.task.slug}`
|
|
835
|
+
: `@${group.task.ref}`;
|
|
836
|
+
// AC: @cmd-session-start ac-relative-time-human
|
|
837
|
+
const itemAge = formatRelativeTime(new Date(group.date));
|
|
838
|
+
console.log(` ${chalk.green("✓")} ${taskDisplay} ${group.task.title} ${chalk.gray(`(${itemAge})`)}${reason}`);
|
|
839
|
+
if (group.task.origin === "observation_promotion") {
|
|
840
|
+
observationPromotedTasks.push(taskDisplay);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
else if (group.kind === "task_group") {
|
|
844
|
+
// AC: @session-start-activity-timeline ac-activity-hierarchy, ac-activity-trailer-link
|
|
845
|
+
// Task as top-level entry with linked commits nested beneath
|
|
846
|
+
const taskDisplay = group.task.slug
|
|
847
|
+
? `@${group.task.slug}`
|
|
848
|
+
: `@${group.task.ref}`;
|
|
849
|
+
const groupAge = formatRelativeTime(new Date(group.sortDate));
|
|
850
|
+
console.log(` ${chalk.green("✓")} ${taskDisplay} ${group.task.title} ${chalk.gray(`(${groupAge})`)}`);
|
|
851
|
+
if (group.task.origin === "observation_promotion") {
|
|
852
|
+
observationPromotedTasks.push(taskDisplay);
|
|
853
|
+
}
|
|
854
|
+
// Render nested commits with visual connectors
|
|
855
|
+
for (let i = 0; i < group.commits.length; i++) {
|
|
856
|
+
const { commit, date } = group.commits[i];
|
|
857
|
+
const isLast = i === group.commits.length - 1;
|
|
858
|
+
const connector = isLast ? "└─" : "├─";
|
|
859
|
+
const commitAge = formatRelativeTime(new Date(date));
|
|
860
|
+
console.log(` ${chalk.gray(connector)} ${chalk.yellow(commit.hash)} ${commit.message} ${chalk.gray(`(${commitAge}, ${commit.author})`)}`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
else if (group.kind === "orphan_commit") {
|
|
864
|
+
// AC: @session-start-activity-timeline ac-activity-orphan
|
|
865
|
+
// Orphan commit: visually distinct from task entries
|
|
866
|
+
const commitAge = formatRelativeTime(new Date(group.date));
|
|
867
|
+
console.log(` ${chalk.gray("○")} ${chalk.yellow(group.commit.hash)} ${group.commit.message} ${chalk.gray(`(${commitAge}, ${group.commit.author})`)}`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
// Show reminder about resolving observations
|
|
871
|
+
if (observationPromotedTasks.length > 0) {
|
|
872
|
+
console.log(chalk.yellow(`\n ℹ Consider resolving linked observations: ${observationPromotedTasks.join(", ")}`));
|
|
873
|
+
console.log(chalk.gray(` Run: kspec meta observations --pending-resolution`));
|
|
608
874
|
}
|
|
609
875
|
}
|
|
610
|
-
// Inbox section
|
|
611
|
-
|
|
876
|
+
// ── Inbox section ──
|
|
877
|
+
// AC: @session-start-inbox-triage ac-inbox-stat-line, ac-inbox-full-list, ac-inbox-all-triaged
|
|
878
|
+
if (ctx.inbox_stats.total > 0) {
|
|
612
879
|
console.log(`\n${sessionHeaders.inbox}`);
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
880
|
+
// Stat line always shown (primer and full)
|
|
881
|
+
const statParts = [
|
|
882
|
+
`${ctx.inbox_stats.untriaged} untriaged`,
|
|
883
|
+
`${ctx.inbox_stats.deferred} deferred`,
|
|
884
|
+
`${ctx.inbox_stats.total} total`,
|
|
885
|
+
];
|
|
886
|
+
console.log(` ${statParts.join(" | ")}`);
|
|
887
|
+
// AC: @cmd-session-start ac-full-sections, @session-start-inbox-triage ac-inbox-full-list
|
|
888
|
+
// Full mode: list untriaged items (up to 20)
|
|
889
|
+
// AC: @cmd-session-start ac-slug-fallback — inbox uses @short-ulid
|
|
890
|
+
const untriagedItems = ctx.inbox_items
|
|
891
|
+
.filter((i) => !i.triaged)
|
|
892
|
+
.slice(0, 20);
|
|
893
|
+
if (isFull && untriagedItems.length > 0) {
|
|
894
|
+
console.log("");
|
|
895
|
+
for (const item of untriagedItems) {
|
|
896
|
+
const itemAge = formatRelativeTime(new Date(item.created_at));
|
|
897
|
+
const author = item.added_by ? ` by ${item.added_by}` : "";
|
|
898
|
+
const tags = item.tags.length > 0
|
|
899
|
+
? chalk.cyan(` [${item.tags.join(", ")}]`)
|
|
900
|
+
: "";
|
|
901
|
+
console.log(` ${chalk.magenta(`@${item.ref}`)} ${chalk.gray(`(${itemAge}${author})`)}${tags}`);
|
|
902
|
+
console.log(` ${item.text}`);
|
|
621
903
|
}
|
|
622
|
-
console.log(` ${chalk.magenta(item.ref)} ${chalk.gray(`(${age}${author})`)}${tags}`);
|
|
623
|
-
console.log(` ${text}`);
|
|
624
904
|
}
|
|
625
|
-
|
|
905
|
+
if (ctx.inbox_stats.untriaged > 0) {
|
|
906
|
+
console.log(` ${hints.inboxTriage}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
// ── Observations section (full mode only) ──
|
|
910
|
+
// AC: @cmd-session-start ac-full-sections
|
|
911
|
+
if (isFull && ctx.observations.length > 0) {
|
|
912
|
+
console.log(`\n${chalk.yellow.bold("--- Observations (unresolved) ---")}`);
|
|
913
|
+
for (const obs of ctx.observations) {
|
|
914
|
+
const obsAge = formatRelativeTime(new Date(obs.created_at));
|
|
915
|
+
const author = obs.author ? ` by ${obs.author}` : "";
|
|
916
|
+
const typeLabel = chalk.cyan(`[${obs.type}]`);
|
|
917
|
+
console.log(` ${typeLabel} ${chalk.gray(`@${obs.ref}`)} ${chalk.gray(`(${obsAge}${author})`)}`);
|
|
918
|
+
console.log(` ${obs.content}`);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
// ── Session metadata section (full mode only) ──
|
|
922
|
+
// AC: @cmd-session-start ac-full-sections
|
|
923
|
+
if (isFull &&
|
|
924
|
+
ctx.context &&
|
|
925
|
+
ctx.context.updated_at) {
|
|
926
|
+
console.log(`\n${chalk.gray.bold("--- Session Metadata ---")}`);
|
|
927
|
+
console.log(chalk.gray(` Last updated: ${formatRelativeTime(new Date(ctx.context.updated_at))}`));
|
|
626
928
|
}
|
|
627
|
-
// Working tree section
|
|
929
|
+
// ── Working tree section ──
|
|
930
|
+
// AC: @cmd-session-start ac-dirty-tree-only — only shown when dirty
|
|
628
931
|
if (ctx.working_tree && !ctx.working_tree.clean) {
|
|
629
932
|
console.log(`\n${sessionHeaders.workingTree}`);
|
|
630
933
|
if (ctx.working_tree.staged.length > 0) {
|
|
@@ -641,32 +944,30 @@ function formatSessionContext(ctx, options) {
|
|
|
641
944
|
}
|
|
642
945
|
if (ctx.working_tree.untracked.length > 0) {
|
|
643
946
|
console.log(chalk.gray(" Untracked:"));
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
947
|
+
const untrackedLimit = isFull
|
|
948
|
+
? ctx.working_tree.untracked.length
|
|
949
|
+
: 5;
|
|
950
|
+
for (const filePath of ctx.working_tree.untracked.slice(0, untrackedLimit)) {
|
|
951
|
+
console.log(` ${chalk.gray("?")} ${filePath}`);
|
|
647
952
|
}
|
|
648
|
-
if (
|
|
649
|
-
console.log(chalk.gray(` ... and ${ctx.working_tree.untracked.length -
|
|
953
|
+
if (!isFull && ctx.working_tree.untracked.length > untrackedLimit) {
|
|
954
|
+
console.log(chalk.gray(` ... and ${ctx.working_tree.untracked.length - untrackedLimit} more`));
|
|
650
955
|
}
|
|
651
956
|
}
|
|
652
957
|
}
|
|
653
|
-
|
|
654
|
-
console.log(`\n${sessionHeaders.workingTreeClean}`);
|
|
655
|
-
}
|
|
656
|
-
// Quick Commands section - contextual hints based on state
|
|
958
|
+
// ── Quick Commands section ──
|
|
657
959
|
const quickCommands = [];
|
|
658
960
|
if (ctx.active_tasks.length > 0) {
|
|
659
|
-
const ref = ctx.active_tasks[0]
|
|
660
|
-
quickCommands.push(`kspec task note
|
|
661
|
-
quickCommands.push(`kspec task complete
|
|
961
|
+
const ref = displayRef(ctx.active_tasks[0]);
|
|
962
|
+
quickCommands.push(`kspec task note ${ref} "Progress..." ${chalk.gray("# document work")}`);
|
|
963
|
+
quickCommands.push(`kspec task complete ${ref} --reason "..." ${chalk.gray("# finish task")}`);
|
|
662
964
|
}
|
|
663
965
|
else if (ctx.ready_tasks.length > 0) {
|
|
664
|
-
const ref = ctx.ready_tasks[0]
|
|
665
|
-
quickCommands.push(`kspec task start
|
|
966
|
+
const ref = displayRef(ctx.ready_tasks[0]);
|
|
967
|
+
quickCommands.push(`kspec task start ${ref} ${chalk.gray("# begin work")}`);
|
|
666
968
|
}
|
|
667
|
-
if (ctx.
|
|
668
|
-
|
|
669
|
-
quickCommands.push(`kspec inbox promote @${ref} --title "..." ${chalk.gray("# convert to task")}`);
|
|
969
|
+
if (ctx.inbox_stats.untriaged > 0) {
|
|
970
|
+
quickCommands.push(`kspec triage inbox ${chalk.gray("# triage untriaged inbox items")}`);
|
|
670
971
|
}
|
|
671
972
|
if (ctx.working_tree && !ctx.working_tree.clean) {
|
|
672
973
|
quickCommands.push(`git add . && git commit -m "..." ${chalk.gray("# commit changes")}`);
|
|
@@ -689,12 +990,11 @@ async function sessionStartAction(options) {
|
|
|
689
990
|
syncResult = await shadowPull(ctx.shadow.worktreeDir);
|
|
690
991
|
// AC: @shadow-sync ac-3 - Warn about conflicts but continue with local state
|
|
691
992
|
if (syncResult.hadConflict) {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
console.log("");
|
|
993
|
+
warn("Shadow sync conflict detected. Run `kspec shadow resolve` to fix.");
|
|
994
|
+
info("Continuing with local state...");
|
|
695
995
|
}
|
|
696
996
|
else if (syncResult.pulled) {
|
|
697
|
-
|
|
997
|
+
info("Synced shadow branch from remote");
|
|
698
998
|
}
|
|
699
999
|
}
|
|
700
1000
|
const sessionCtx = await gatherSessionContext(ctx, options);
|
|
@@ -1095,7 +1395,7 @@ async function sessionLogShowAction(sessionRef, options) {
|
|
|
1095
1395
|
// AC: @session-log-show ac-3, ac-4, ac-5 - Event timeline
|
|
1096
1396
|
let events = null;
|
|
1097
1397
|
if (options.events) {
|
|
1098
|
-
let allEvents = await readEvents(ctx.specDir, sessionId);
|
|
1398
|
+
let allEvents = deduplicatePhasedToolCalls(await readEvents(ctx.specDir, sessionId));
|
|
1099
1399
|
// AC: @session-log-show ac-4 - Filter by type
|
|
1100
1400
|
if (options.type) {
|
|
1101
1401
|
const typeFilter = options.type;
|
|
@@ -1350,6 +1650,143 @@ async function sessionLogSearchAction(pattern, options) {
|
|
|
1350
1650
|
process.exit(EXIT_CODES.ERROR);
|
|
1351
1651
|
}
|
|
1352
1652
|
}
|
|
1653
|
+
// ─── Session Create Action ─────────────────────────────────────────────────
|
|
1654
|
+
/**
|
|
1655
|
+
* Action handler for `kspec session create`.
|
|
1656
|
+
*
|
|
1657
|
+
* Creates a new session with optional budget and environment injection.
|
|
1658
|
+
*
|
|
1659
|
+
* AC: @session-creation-and-env-injection ac-create
|
|
1660
|
+
* AC: @session-creation-and-env-injection ac-budget
|
|
1661
|
+
* AC: @session-creation-and-env-injection ac-budget-local
|
|
1662
|
+
* AC: @session-creation-and-env-injection ac-inject-claude
|
|
1663
|
+
* AC: @session-creation-and-env-injection ac-inject-codex
|
|
1664
|
+
* AC: @session-creation-and-env-injection ac-inject-fallback
|
|
1665
|
+
*
|
|
1666
|
+
* Exit codes documented per @trait-semantic-exit-codes ac-8:
|
|
1667
|
+
* - 0: Session created successfully
|
|
1668
|
+
* - 1: Validation error (invalid budget value)
|
|
1669
|
+
* - 3: Runtime error (filesystem failure)
|
|
1670
|
+
*/
|
|
1671
|
+
async function sessionCreateAction(options) {
|
|
1672
|
+
try {
|
|
1673
|
+
const ctx = await initContext();
|
|
1674
|
+
// AC: @session-creation-and-env-injection ac-invalid-session
|
|
1675
|
+
// Validate existing KSPEC_SESSION_ID if set — warn user if it's stale/corrupt
|
|
1676
|
+
const existingSessionId = process.env.KSPEC_SESSION_ID;
|
|
1677
|
+
if (existingSessionId) {
|
|
1678
|
+
const validation = await validateSessionId(ctx.specDir, existingSessionId);
|
|
1679
|
+
if (!validation.valid) {
|
|
1680
|
+
warn(`Current KSPEC_SESSION_ID (${existingSessionId}) is invalid: ${validation.error}`);
|
|
1681
|
+
info(validation.suggestion || "Creating a new session will generate a fresh ID.");
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
// Validate budget if provided
|
|
1685
|
+
// AC: @trait-error-guidance ac-5 - indicate which field/value failed
|
|
1686
|
+
let budgetNum;
|
|
1687
|
+
if (options.budget !== undefined) {
|
|
1688
|
+
// Use Number() instead of parseInt to reject "3.5", "3abc", "1e2" etc.
|
|
1689
|
+
budgetNum = Number(options.budget);
|
|
1690
|
+
if (isNaN(budgetNum) || budgetNum <= 0 || !Number.isInteger(budgetNum) || !/^\d+$/.test(options.budget)) {
|
|
1691
|
+
// AC: @trait-error-guidance ac-2, ac-5 - include suggested action and field info
|
|
1692
|
+
// AC: @trait-error-guidance ac-6 - guidance included in structured error
|
|
1693
|
+
error(`Invalid budget value: "${options.budget}". Must be a positive integer.`, { suggestion: "Usage: kspec session create --budget <positive-integer>" });
|
|
1694
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
// Generate session ID
|
|
1698
|
+
const sessionId = ulid();
|
|
1699
|
+
// AC: @session-creation-and-env-injection ac-create, ac-budget, ac-budget-local
|
|
1700
|
+
const result = await createSessionWithBudget(ctx.specDir, {
|
|
1701
|
+
id: sessionId,
|
|
1702
|
+
agent_type: options.agentType,
|
|
1703
|
+
task_id: options.taskId,
|
|
1704
|
+
budget: budgetNum,
|
|
1705
|
+
});
|
|
1706
|
+
// Handle environment injection if requested
|
|
1707
|
+
let injection = null;
|
|
1708
|
+
if (options.inject) {
|
|
1709
|
+
injection = await performEnvInjection(sessionId);
|
|
1710
|
+
}
|
|
1711
|
+
// Build output data
|
|
1712
|
+
const outputData = {
|
|
1713
|
+
session_id: result.session_id,
|
|
1714
|
+
agent_type: result.session.agent_type,
|
|
1715
|
+
status: result.session.status,
|
|
1716
|
+
started_at: result.session.started_at,
|
|
1717
|
+
};
|
|
1718
|
+
if (result.session.task_id) {
|
|
1719
|
+
outputData.task_id = result.session.task_id;
|
|
1720
|
+
}
|
|
1721
|
+
if (result.budget) {
|
|
1722
|
+
outputData.budget = {
|
|
1723
|
+
max_per_cycle: result.budget.max_per_cycle,
|
|
1724
|
+
started_this_cycle: result.budget.started_this_cycle,
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
if (injection) {
|
|
1728
|
+
outputData.env_injection = {
|
|
1729
|
+
method: injection.method,
|
|
1730
|
+
injected: injection.injected,
|
|
1731
|
+
description: injection.description,
|
|
1732
|
+
...(injection.path ? { path: injection.path } : {}),
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
// AC: @trait-json-output ac-1, ac-2, ac-5 - JSON with all data, ISO timestamps
|
|
1736
|
+
output(outputData, () => {
|
|
1737
|
+
// AC: @session-creation-and-env-injection ac-create - print session ID to stdout
|
|
1738
|
+
success(`Created session: ${sessionId}`, { session_id: sessionId });
|
|
1739
|
+
info(`Agent type: ${options.agentType}`);
|
|
1740
|
+
if (result.budget) {
|
|
1741
|
+
info(`Budget: ${result.budget.max_per_cycle} tasks per cycle`);
|
|
1742
|
+
}
|
|
1743
|
+
if (injection) {
|
|
1744
|
+
if (injection.injected) {
|
|
1745
|
+
info(injection.description);
|
|
1746
|
+
}
|
|
1747
|
+
else {
|
|
1748
|
+
// AC: @session-creation-and-env-injection ac-inject-fallback
|
|
1749
|
+
console.log(injection.description);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
catch (err) {
|
|
1755
|
+
// AC: @trait-error-guidance ac-1 - describe what went wrong
|
|
1756
|
+
// AC: @trait-json-output ac-3 - error as JSON object
|
|
1757
|
+
error("Failed to create session", err);
|
|
1758
|
+
process.exit(EXIT_CODES.ERROR);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Detect agent harness and perform environment injection.
|
|
1763
|
+
*
|
|
1764
|
+
* AC: @session-creation-and-env-injection ac-inject-claude
|
|
1765
|
+
* AC: @session-creation-and-env-injection ac-inject-codex
|
|
1766
|
+
* AC: @session-creation-and-env-injection ac-inject-fallback
|
|
1767
|
+
*/
|
|
1768
|
+
async function performEnvInjection(sessionId) {
|
|
1769
|
+
// Detect Claude Code
|
|
1770
|
+
if (process.env.CLAUDECODE === "1" ||
|
|
1771
|
+
process.env.CLAUDE_CODE_ENTRYPOINT ||
|
|
1772
|
+
process.env.CLAUDE_PROJECT_DIR) {
|
|
1773
|
+
return injectClaudeCodeEnv(sessionId);
|
|
1774
|
+
}
|
|
1775
|
+
// Detect Codex CLI
|
|
1776
|
+
if (process.env.CODEX_SANDBOX) {
|
|
1777
|
+
return injectCodexEnv(sessionId);
|
|
1778
|
+
}
|
|
1779
|
+
// Detect Gemini CLI
|
|
1780
|
+
if (process.env.GEMINI_CLI === "1") {
|
|
1781
|
+
return injectGeminiEnv(sessionId);
|
|
1782
|
+
}
|
|
1783
|
+
// Detect OpenCode
|
|
1784
|
+
if (process.env.OPENCODE_CONFIG_DIR || process.env.OPENCODE_CONFIG) {
|
|
1785
|
+
return injectOpenCodeEnv(sessionId);
|
|
1786
|
+
}
|
|
1787
|
+
// Fallback for unknown harnesses
|
|
1788
|
+
return getFallbackInjectionInstructions(sessionId);
|
|
1789
|
+
}
|
|
1353
1790
|
/**
|
|
1354
1791
|
* Register the 'session' command group and aliases
|
|
1355
1792
|
*/
|
|
@@ -1357,6 +1794,14 @@ export function registerSessionCommands(program) {
|
|
|
1357
1794
|
const session = program
|
|
1358
1795
|
.command("session")
|
|
1359
1796
|
.description("Session management and context");
|
|
1797
|
+
// Session create subcommand
|
|
1798
|
+
markMutating(session.command("create"))
|
|
1799
|
+
.description("Create a new kspec session with optional budget")
|
|
1800
|
+
.option("--agent-type <type>", "Agent type (e.g., claude-code, codex-cli)", "claude-code")
|
|
1801
|
+
.option("--budget <n>", "Maximum tasks per cycle (positive integer)")
|
|
1802
|
+
.option("--inject", "Inject KSPEC_SESSION_ID into agent environment")
|
|
1803
|
+
.option("--task-id <id>", "Optional task ID being worked on")
|
|
1804
|
+
.action(sessionCreateAction);
|
|
1360
1805
|
session
|
|
1361
1806
|
.command("start")
|
|
1362
1807
|
.alias("resume")
|