aiwcli 0.12.6 → 0.12.8
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/bin/dev.cmd +3 -3
- package/bin/dev.js +16 -16
- package/bin/run.cmd +3 -3
- package/bin/run.js +21 -21
- package/dist/commands/branch.js +7 -2
- package/dist/lib/bmad-installer.js +37 -37
- package/dist/lib/terminal.d.ts +2 -0
- package/dist/lib/terminal.js +57 -7
- package/dist/templates/CLAUDE.md +232 -205
- package/dist/templates/_shared/.claude/settings.json +65 -65
- package/dist/templates/_shared/.claude/{commands/handoff.md → skills/handoff/SKILL.md} +13 -12
- package/dist/templates/_shared/.claude/{commands/handoff-resume.md → skills/handoff-resume/SKILL.md} +13 -12
- package/dist/templates/_shared/.codex/workflows/handoff.md +226 -226
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +226 -226
- package/dist/templates/_shared/handoff-system/CLAUDE.md +15 -3
- package/dist/templates/_shared/handoff-system/lib/document-generator.ts +215 -215
- package/dist/templates/_shared/handoff-system/lib/handoff-reader.ts +158 -158
- package/dist/templates/_shared/handoff-system/scripts/resume_handoff.ts +373 -373
- package/dist/templates/_shared/handoff-system/scripts/save_handoff.ts +469 -469
- package/dist/templates/_shared/handoff-system/workflows/handoff-resume.md +66 -66
- package/dist/templates/_shared/handoff-system/workflows/handoff.md +254 -254
- package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -2
- package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -159
- package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -147
- package/dist/templates/_shared/hooks-ts/file-suggestion.ts +128 -128
- package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -49
- package/dist/templates/_shared/hooks-ts/session_end.ts +196 -196
- package/dist/templates/_shared/hooks-ts/session_start.ts +163 -163
- package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -48
- package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -74
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +93 -93
- package/dist/templates/_shared/lib-ts/CLAUDE.md +367 -367
- package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -138
- package/dist/templates/_shared/lib-ts/base/constants.ts +24 -6
- package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -58
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +582 -582
- package/dist/templates/_shared/lib-ts/base/inference.ts +301 -301
- package/dist/templates/_shared/lib-ts/base/logger.ts +247 -247
- package/dist/templates/_shared/lib-ts/base/state-io.ts +202 -202
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -184
- package/dist/templates/_shared/lib-ts/base/utils.ts +184 -184
- package/dist/templates/_shared/lib-ts/context/CLAUDE.md +134 -0
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +566 -566
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +524 -524
- package/dist/templates/_shared/lib-ts/context/context-store.ts +712 -712
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +312 -312
- package/dist/templates/_shared/lib-ts/context/task-tracker.ts +185 -185
- package/dist/templates/_shared/lib-ts/package.json +20 -20
- package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -102
- package/dist/templates/_shared/lib-ts/templates/plan-context.ts +58 -58
- package/dist/templates/_shared/lib-ts/tsconfig.json +13 -13
- package/dist/templates/_shared/lib-ts/types.ts +186 -186
- package/dist/templates/_shared/scripts/resolve_context.ts +33 -33
- package/dist/templates/_shared/scripts/status_line.ts +687 -690
- package/dist/templates/cc-native/.claude/commands/cc-native/rlm/ask.md +136 -136
- package/dist/templates/cc-native/.claude/commands/cc-native/rlm/index.md +21 -21
- package/dist/templates/cc-native/.claude/commands/cc-native/rlm/overview.md +56 -56
- package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -10
- package/dist/templates/cc-native/.claude/settings.json +3 -2
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -8
- package/dist/templates/cc-native/CC-NATIVE-README.md +189 -189
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +304 -304
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +143 -143
- package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +213 -213
- package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -70
- package/dist/templates/cc-native/_cc-native/artifacts/CLAUDE.md +64 -0
- package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/format.ts +1 -1
- package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/write.ts +2 -2
- package/dist/templates/cc-native/_cc-native/cc-native.config.json +96 -96
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +14 -24
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +1 -1
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +54 -54
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -51
- package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -53
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -61
- package/dist/templates/cc-native/_cc-native/hooks/validate_task_prompt.ts +76 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +9 -2
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +319 -319
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -144
- package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -57
- package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -83
- package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +79 -79
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +4 -4
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -168
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -80
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -41
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/CLAUDE.md +480 -480
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +287 -287
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +148 -148
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +54 -54
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +58 -58
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +208 -208
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +460 -460
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +446 -446
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +280 -280
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +274 -274
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +201 -201
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +278 -278
- package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -184
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +275 -275
- package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -18
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +1 -1
- package/dist/templates/cc-native/_cc-native/plan-review/CLAUDE.md +149 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/CLAUDE.md +143 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/PLAN-ORCHESTRATOR.md +213 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-questions/PLAN-QUESTIONER.md +70 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ARCH-EVOLUTION.md +62 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ARCH-PATTERNS.md +61 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ARCH-STRUCTURE.md +62 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ASSUMPTION-TRACER.md +56 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/CLARITY-AUDITOR.md +53 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/COMPLETENESS-FEASIBILITY.md +66 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/COMPLETENESS-GAPS.md +70 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/COMPLETENESS-ORDERING.md +62 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/CONSTRAINT-VALIDATOR.md +72 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DESIGN-ADR-VALIDATOR.md +61 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DESIGN-SCALE-MATCHER.md +64 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DEVILS-ADVOCATE.md +56 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DOCUMENTATION-PHILOSOPHY.md +86 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/HANDOFF-READINESS.md +59 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/HIDDEN-COMPLEXITY.md +58 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/INCREMENTAL-DELIVERY.md +66 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-DEPENDENCY.md +62 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-FMEA.md +66 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-PREMORTEM.md +71 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-REVERSIBILITY.md +74 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/SCOPE-BOUNDARY.md +77 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/SIMPLICITY-GUARDIAN.md +62 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/SKEPTIC.md +68 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-BEHAVIOR-AUDITOR.md +61 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-CHARACTERIZATION.md +71 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-FIRST-VALIDATOR.md +61 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-PYRAMID-ANALYZER.md +61 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TRADEOFF-COSTS.md +67 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TRADEOFF-STAKEHOLDERS.md +65 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/VERIFY-COVERAGE.md +74 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/VERIFY-STRENGTH.md +69 -0
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/agent-selection.ts +3 -3
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/corroboration.ts +1 -1
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/graduation.ts +1 -1
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/orchestrator.ts +2 -2
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/output-builder.ts +3 -3
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/plan-questions.ts +6 -6
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/review-pipeline.ts +15 -15
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/agent.ts +5 -5
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/base/base-agent.ts +4 -4
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/claude-agent.ts +4 -4
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/codex-agent.ts +6 -6
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/gemini-agent.ts +1 -1
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/orchestrator-claude-agent.ts +4 -4
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/types.ts +3 -3
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/verdict.ts +1 -1
- package/oclif.manifest.json +1 -1
- package/package.json +108 -108
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +0 -21
- package/dist/templates/cc-native/_cc-native/lib-ts/nul +0 -3
- /package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/index.ts +0 -0
- /package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/tracker.ts +0 -0
- /package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/index.ts +0 -0
- /package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/schemas.ts +0 -0
- /package/dist/templates/cc-native/_cc-native/{workflows → plan-review/workflows}/specdev.md +0 -0
|
@@ -1,566 +1,566 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Formatting module for context display output.
|
|
3
|
-
* See SPEC.md §11
|
|
4
|
-
*
|
|
5
|
-
* All functions accept a ContextState with fields:
|
|
6
|
-
* id, summary, mode, last_active, plan_path, handoff_path,
|
|
7
|
-
* tasks[], last_session, session_ids, status, method, tags
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import * as fs from "node:fs";
|
|
11
|
-
import * as path from "node:path";
|
|
12
|
-
import { parseIsoTimestamp } from "../base/utils.js";
|
|
13
|
-
import { getContextDir } from "../base/constants.js";
|
|
14
|
-
import type { ContextState, Task } from "../types.js";
|
|
15
|
-
|
|
16
|
-
const MAX_PLAN_INLINE_CHARS = 30_000;
|
|
17
|
-
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
// Mode display
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
const MODE_DISPLAY_MAP: Record<string, string> = {
|
|
23
|
-
idle: "",
|
|
24
|
-
has_staged_work: "[Staged]", // CHANGED: unified mode (plan or handoff)
|
|
25
|
-
active: "[Active]",
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Get bracketed display string for mode, or empty for idle.
|
|
30
|
-
* See SPEC.md §11.2
|
|
31
|
-
*/
|
|
32
|
-
export function getModeDisplay(mode: string): string {
|
|
33
|
-
return MODE_DISPLAY_MAP[mode] ?? "";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
// Time formatting
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Format ISO timestamp as '2 hours ago', 'yesterday', etc.
|
|
42
|
-
* See SPEC.md §11.3
|
|
43
|
-
*/
|
|
44
|
-
export function formatRelativeTime(isoTimestamp: string | null): string {
|
|
45
|
-
if (!isoTimestamp) return "unknown";
|
|
46
|
-
|
|
47
|
-
let dt = parseIsoTimestamp(isoTimestamp);
|
|
48
|
-
if (!dt) return isoTimestamp.slice(0, 16);
|
|
49
|
-
|
|
50
|
-
const now = new Date();
|
|
51
|
-
|
|
52
|
-
// Strip timezone info for comparison
|
|
53
|
-
const diffMs = now.getTime() - dt.getTime();
|
|
54
|
-
const diffSec = Math.floor(diffMs / 1000);
|
|
55
|
-
const diffMin = Math.floor(diffSec / 60);
|
|
56
|
-
const diffHours = Math.floor(diffMin / 60);
|
|
57
|
-
const diffDays = Math.floor(diffHours / 24);
|
|
58
|
-
|
|
59
|
-
if (diffDays === 0) {
|
|
60
|
-
if (diffHours === 0) {
|
|
61
|
-
if (diffMin === 0) return "just now";
|
|
62
|
-
return diffMin === 1 ? "1 minute ago" : `${diffMin} minutes ago`;
|
|
63
|
-
}
|
|
64
|
-
return diffHours === 1 ? "1 hour ago" : `${diffHours} hours ago`;
|
|
65
|
-
}
|
|
66
|
-
if (diffDays === 1) return "yesterday";
|
|
67
|
-
if (diffDays < 7) return `${diffDays} days ago`;
|
|
68
|
-
|
|
69
|
-
// Older: show date
|
|
70
|
-
const year = dt.getFullYear();
|
|
71
|
-
const month = String(dt.getMonth() + 1).padStart(2, "0");
|
|
72
|
-
const day = String(dt.getDate()).padStart(2, "0");
|
|
73
|
-
return `${year}-${month}-${day}`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
// Internal helpers
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
|
|
80
|
-
function taskAttr(task: Task | Record<string, any>, key: string, defaultVal = ""): string {
|
|
81
|
-
if (typeof task === "object" && task !== null) {
|
|
82
|
-
return (task as any)[key] ?? defaultVal;
|
|
83
|
-
}
|
|
84
|
-
return defaultVal;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function readPlanContent(planPath: string): [string | null, boolean, number] {
|
|
88
|
-
try {
|
|
89
|
-
if (!fs.existsSync(planPath)) return [null, false, 0];
|
|
90
|
-
const content = fs.readFileSync(planPath, "utf-8");
|
|
91
|
-
const total = content.length;
|
|
92
|
-
if (total > MAX_PLAN_INLINE_CHARS) {
|
|
93
|
-
return [content.slice(0, MAX_PLAN_INLINE_CHARS), true, total];
|
|
94
|
-
}
|
|
95
|
-
return [content, false, total];
|
|
96
|
-
} catch {
|
|
97
|
-
return [null, false, 0];
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function modeLabel(ctx: ContextState): string {
|
|
102
|
-
const d = getModeDisplay(ctx.mode ?? "idle");
|
|
103
|
-
return d ? d.replace(/^\[|\]$/g, "") : "Active";
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Build restore sections from last_session, tasks, and plan_path.
|
|
108
|
-
* See SPEC.md §11.4
|
|
109
|
-
*/
|
|
110
|
-
export function buildRestoreSections(
|
|
111
|
-
ctx: ContextState,
|
|
112
|
-
projectRoot?: string,
|
|
113
|
-
inlinePlan = false,
|
|
114
|
-
): string {
|
|
115
|
-
const sections: string[] = [];
|
|
116
|
-
const lastSession = ctx.last_session ?? {};
|
|
117
|
-
|
|
118
|
-
if (lastSession) {
|
|
119
|
-
const savedAt = lastSession.saved_at ?? "";
|
|
120
|
-
if (savedAt) {
|
|
121
|
-
const reason = lastSession.save_reason ?? "";
|
|
122
|
-
const reasonDisplay = reason ? reason.replace(/_/g, " ") : "unknown";
|
|
123
|
-
sections.push(`**Last session ended:** ${formatRelativeTime(savedAt)} (${reasonDisplay})`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const tasks = ctx.tasks ?? [];
|
|
128
|
-
if (tasks.length > 0) {
|
|
129
|
-
const buckets: Record<string, string[]> = {
|
|
130
|
-
completed: [],
|
|
131
|
-
in_progress: [],
|
|
132
|
-
pending: [],
|
|
133
|
-
blocked: [],
|
|
134
|
-
};
|
|
135
|
-
for (const t of tasks) {
|
|
136
|
-
const s = taskAttr(t, "status", "pending");
|
|
137
|
-
if (buckets[s]) {
|
|
138
|
-
buckets[s]!.push(taskAttr(t, "subject"));
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
if (Object.values(buckets).some(b => b.length > 0)) {
|
|
142
|
-
sections.push("", `### Previous Work (${tasks.length} tasks)`, "");
|
|
143
|
-
const marks: Record<string, string> = {
|
|
144
|
-
completed: "[x]",
|
|
145
|
-
in_progress: "[~]",
|
|
146
|
-
pending: "[ ]",
|
|
147
|
-
blocked: "[!]",
|
|
148
|
-
};
|
|
149
|
-
for (const [status, mark] of Object.entries(marks)) {
|
|
150
|
-
for (const subj of buckets[status] ?? []) {
|
|
151
|
-
sections.push(`- ${mark} ${subj}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const planPath = ctx.plan_path;
|
|
158
|
-
if (planPath) {
|
|
159
|
-
if (inlinePlan) {
|
|
160
|
-
const [content, truncated, totalChars] = readPlanContent(planPath);
|
|
161
|
-
if (content) {
|
|
162
|
-
let header = `Plan loaded from: \`${planPath}\``;
|
|
163
|
-
if (truncated) header += ` (truncated, ${totalChars} chars total)`;
|
|
164
|
-
sections.push("", "### Plan", header, "", content);
|
|
165
|
-
if (truncated) {
|
|
166
|
-
sections.push(`\n*Plan truncated at ${MAX_PLAN_INLINE_CHARS} characters. Full plan at: \`${planPath}\`*`);
|
|
167
|
-
}
|
|
168
|
-
} else {
|
|
169
|
-
sections.push("", "### Plan", `*Plan file not found at \`${planPath}\`.*`);
|
|
170
|
-
}
|
|
171
|
-
} else {
|
|
172
|
-
sections.push("", "### Plan", `Read the plan at: \`${planPath}\``);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const gitState = lastSession?.git_state ?? {};
|
|
177
|
-
if (gitState && Object.keys(gitState).length > 0) {
|
|
178
|
-
const branch = gitState.branch ?? "unknown";
|
|
179
|
-
const uncommitted: string[] = gitState.uncommitted_files ?? [];
|
|
180
|
-
const lastCommit = gitState.last_commit_short ?? "";
|
|
181
|
-
let uncStr = uncommitted.length > 0 ? uncommitted.slice(0, 5).join(", ") : "none";
|
|
182
|
-
if (uncommitted.length > 5) uncStr += ` (+${uncommitted.length - 5} more)`;
|
|
183
|
-
sections.push("", "### Git State", `Branch: ${branch} | Uncommitted: ${uncStr}`);
|
|
184
|
-
if (lastCommit) sections.push(`Last commit: ${lastCommit}`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return sections.join("\n");
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function resumeBlock(ctx: ContextState, projectRoot: string | undefined, modeText: string, instructions: string[]): string {
|
|
191
|
-
const lines = [
|
|
192
|
-
`## Resuming Context: ${ctx.id}`, "",
|
|
193
|
-
`**Summary:** ${ctx.summary}`,
|
|
194
|
-
`**Mode:** ${modeText}`,
|
|
195
|
-
];
|
|
196
|
-
const restore = buildRestoreSections(ctx, projectRoot, true);
|
|
197
|
-
if (restore) lines.push(restore);
|
|
198
|
-
lines.push("", "---", "", "**Instructions:**");
|
|
199
|
-
lines.push(...instructions);
|
|
200
|
-
return lines.join("\n");
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// ---------------------------------------------------------------------------
|
|
204
|
-
// Public formatters
|
|
205
|
-
// ---------------------------------------------------------------------------
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Format output when resuming a context with a pending handoff.
|
|
209
|
-
* See SPEC.md §11.5
|
|
210
|
-
*/
|
|
211
|
-
export function formatHandoffContinuation(ctx: ContextState, projectRoot?: string): string {
|
|
212
|
-
const handoffPath = ctx.handoff_path ?? "";
|
|
213
|
-
const lines = [
|
|
214
|
-
`## Resuming Context: ${ctx.id} (Handoff Available)`, "",
|
|
215
|
-
`**Summary:** ${ctx.summary}`,
|
|
216
|
-
`**Mode:** Implementing (handoff from previous session)`, "",
|
|
217
|
-
];
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
if (handoffPath && fs.existsSync(handoffPath)) {
|
|
221
|
-
lines.push("### Previous Session Handoff", "", fs.readFileSync(handoffPath, "utf-8"), "");
|
|
222
|
-
} else {
|
|
223
|
-
lines.push(`*Handoff document not found at \`${handoffPath}\`*`, "");
|
|
224
|
-
}
|
|
225
|
-
} catch (e: any) {
|
|
226
|
-
lines.push(`*Handoff document at \`${handoffPath}\` could not be read: ${e}*`, "");
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const restore = buildRestoreSections(ctx, projectRoot, true);
|
|
230
|
-
if (restore) lines.push(restore);
|
|
231
|
-
|
|
232
|
-
lines.push("", "---", "", "**Instructions:**",
|
|
233
|
-
"1. Review the handoff document above - especially dead ends",
|
|
234
|
-
"2. Check the plan file for remaining tasks",
|
|
235
|
-
"3. Continue implementation from where the previous session left off");
|
|
236
|
-
return lines.join("\n");
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Format output for pending plan implementation (mode=has_plan).
|
|
241
|
-
* See SPEC.md §11.6
|
|
242
|
-
*/
|
|
243
|
-
export function formatPlanContinuation(ctx: ContextState, projectRoot?: string): string {
|
|
244
|
-
return resumeBlock(ctx, projectRoot, "Pending Implementation", [
|
|
245
|
-
"1. Review the plan and previous work above",
|
|
246
|
-
"2. Continue from where the previous session left off",
|
|
247
|
-
]);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Format output for ongoing implementation (mode=active).
|
|
252
|
-
* See SPEC.md §11.7
|
|
253
|
-
*/
|
|
254
|
-
export function formatActiveContinuation(ctx: ContextState, projectRoot?: string): string {
|
|
255
|
-
return resumeBlock(ctx, projectRoot, "Implementing", [
|
|
256
|
-
"1. Review the plan and previous work above",
|
|
257
|
-
"2. Continue from where the previous session left off",
|
|
258
|
-
]);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Format list of contexts for display.
|
|
263
|
-
* See SPEC.md §11.8
|
|
264
|
-
*/
|
|
265
|
-
export function formatContextList(contexts: ContextState[]): string {
|
|
266
|
-
if (contexts.length === 0) return "No active contexts found.";
|
|
267
|
-
|
|
268
|
-
const lines = ["## Active Contexts\n"];
|
|
269
|
-
for (let i = 0; i < contexts.length; i++) {
|
|
270
|
-
const ctx = contexts[i]!;
|
|
271
|
-
const timeStr = formatRelativeTime(ctx.last_active);
|
|
272
|
-
const md = getModeDisplay(ctx.mode ?? "idle");
|
|
273
|
-
const si = md ? ` ${md}` : "";
|
|
274
|
-
lines.push(`**${i + 1}. ${ctx.id}**${si}`);
|
|
275
|
-
lines.push(` ${ctx.summary}`);
|
|
276
|
-
if (ctx.method) {
|
|
277
|
-
lines.push(` Method: ${ctx.method} | Last active: ${timeStr}`);
|
|
278
|
-
} else {
|
|
279
|
-
lines.push(` Last active: ${timeStr}`);
|
|
280
|
-
}
|
|
281
|
-
lines.push("");
|
|
282
|
-
}
|
|
283
|
-
return lines.join("\n");
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Format notification for a newly created context.
|
|
288
|
-
* See SPEC.md §11.9
|
|
289
|
-
*/
|
|
290
|
-
export function formatContextCreated(ctx: ContextState): string {
|
|
291
|
-
return [
|
|
292
|
-
`## Context Created: ${ctx.id}`, "",
|
|
293
|
-
`**Summary:** ${ctx.summary}`, "",
|
|
294
|
-
"A new context has been created for this work.",
|
|
295
|
-
"Tasks created with TaskCreate will be persisted to this context.",
|
|
296
|
-
].join("\n");
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Format system reminder: lightweight (per-prompt) or rich (first-bind restore).
|
|
301
|
-
* See SPEC.md §11.10
|
|
302
|
-
*/
|
|
303
|
-
export function formatActiveContextReminder(
|
|
304
|
-
ctx: ContextState,
|
|
305
|
-
projectRoot?: string,
|
|
306
|
-
includeRestore = false,
|
|
307
|
-
): string {
|
|
308
|
-
const timeStr = formatRelativeTime(ctx.last_active);
|
|
309
|
-
const label = modeLabel(ctx);
|
|
310
|
-
|
|
311
|
-
if (includeRestore) {
|
|
312
|
-
const lines = [
|
|
313
|
-
`## Resuming Context: ${ctx.id}`, "",
|
|
314
|
-
`**Summary:** ${ctx.summary}`,
|
|
315
|
-
`**Mode:** ${label}`,
|
|
316
|
-
];
|
|
317
|
-
const restore = buildRestoreSections(ctx, projectRoot, true);
|
|
318
|
-
if (restore) lines.push(restore);
|
|
319
|
-
lines.push("", "---", "", "**Instructions:**",
|
|
320
|
-
"1. Review the previous work above",
|
|
321
|
-
"2. Continue from where the previous session left off");
|
|
322
|
-
return lines.join("\n");
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
return [
|
|
326
|
-
`## Active Context: ${ctx.id}`, "",
|
|
327
|
-
`**Summary:** ${ctx.summary}`,
|
|
328
|
-
`**Mode:** ${label}`,
|
|
329
|
-
`**Last Active:** ${timeStr}`, "",
|
|
330
|
-
`All work belongs to context "${ctx.id}".`,
|
|
331
|
-
"Tasks created with TaskCreate will be persisted to this context.",
|
|
332
|
-
].join("\n");
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// ---------------------------------------------------------------------------
|
|
336
|
-
// Picker / command feedback
|
|
337
|
-
// ---------------------------------------------------------------------------
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Format the boxed picker shown on stderr when blocking for selection.
|
|
341
|
-
* See SPEC.md §11.11
|
|
342
|
-
*/
|
|
343
|
-
export function formatContextPickerStderr(contexts: ContextState[]): string {
|
|
344
|
-
const lines = [
|
|
345
|
-
"",
|
|
346
|
-
"+----------------------------------------------------------------+",
|
|
347
|
-
"| CONTEXT SELECTION REQUIRED |",
|
|
348
|
-
"+----------------------------------------------------------------+",
|
|
349
|
-
];
|
|
350
|
-
|
|
351
|
-
let selectableCount = 0;
|
|
352
|
-
for (let i = 0; i < contexts.length; i++) {
|
|
353
|
-
const ctx = contexts[i]!;
|
|
354
|
-
const timeStr = formatRelativeTime(ctx.last_active);
|
|
355
|
-
const mode = ctx.mode ?? "idle";
|
|
356
|
-
const isSelectable = mode === "active" || !!ctx.handoff_path;
|
|
357
|
-
if (isSelectable) selectableCount++;
|
|
358
|
-
|
|
359
|
-
let status = "";
|
|
360
|
-
if (ctx.handoff_path) {
|
|
361
|
-
status = " [Handoff Ready]";
|
|
362
|
-
} else if (getModeDisplay(mode)) {
|
|
363
|
-
status = ` ${getModeDisplay(mode)}`;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const summary = ctx.summary.length > 48 ? ctx.summary.slice(0, 45) + "..." : ctx.summary;
|
|
367
|
-
const selTag = isSelectable ? " [selectable]" : " [end only]";
|
|
368
|
-
|
|
369
|
-
lines.push(`| ^${i + 1} ${ctx.id}${status}${selTag}`);
|
|
370
|
-
lines.push(`| ${summary}`);
|
|
371
|
-
lines.push(`| [${timeStr}]`);
|
|
372
|
-
lines.push("|");
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
lines.push(
|
|
376
|
-
"+----------------------------------------------------------------+",
|
|
377
|
-
"| Usage: |",
|
|
378
|
-
"| ^S<N> - Select context by number |",
|
|
379
|
-
"| ^E<N> - End/complete context by number |",
|
|
380
|
-
"| ^S:query - Select by ID match (race-safe) |",
|
|
381
|
-
"| ^E:query - End by ID match (race-safe) |",
|
|
382
|
-
"| ^E<N>+ - End context N and all after |",
|
|
383
|
-
"| ^E* - End ALL contexts |",
|
|
384
|
-
"| ^E1E2S3 - End #1 and #2, select #3 |",
|
|
385
|
-
"| ^E:fooS:bar - End 'foo...', select 'bar...' |",
|
|
386
|
-
"| ^0 work description - Create new context (10+ chars) |",
|
|
387
|
-
"+----------------------------------------------------------------+",
|
|
388
|
-
);
|
|
389
|
-
|
|
390
|
-
if (selectableCount === 0) {
|
|
391
|
-
lines.push(
|
|
392
|
-
"| NOTE: No selectable contexts. |",
|
|
393
|
-
"| Use ^E<N> to end old contexts, then ^0 to create new. |",
|
|
394
|
-
"+----------------------------------------------------------------+",
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
lines.push("");
|
|
398
|
-
return lines.join("\n");
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Format feedback about caret command operations performed.
|
|
403
|
-
* See SPEC.md §11.12
|
|
404
|
-
*/
|
|
405
|
-
export function formatCommandFeedback(
|
|
406
|
-
endedContexts: ContextState[],
|
|
407
|
-
selectedContext: ContextState | null,
|
|
408
|
-
): string {
|
|
409
|
-
const lines: string[] = [];
|
|
410
|
-
if (endedContexts.length > 0) {
|
|
411
|
-
lines.push("## Contexts Ended", "");
|
|
412
|
-
for (const ctx of endedContexts) {
|
|
413
|
-
const s = ctx.summary.length > 50 ? ctx.summary.slice(0, 50) + "..." : ctx.summary;
|
|
414
|
-
lines.push(`- **${ctx.id}**: ${s}`);
|
|
415
|
-
}
|
|
416
|
-
lines.push("");
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (selectedContext) {
|
|
420
|
-
const label = modeLabel(selectedContext);
|
|
421
|
-
const timeStr = formatRelativeTime(selectedContext.last_active);
|
|
422
|
-
lines.push(
|
|
423
|
-
`## Active Context: ${selectedContext.id}`, "",
|
|
424
|
-
`**Summary:** ${selectedContext.summary}`,
|
|
425
|
-
`**Mode:** ${label}`,
|
|
426
|
-
`**Last Active:** ${timeStr}`, "",
|
|
427
|
-
`All work belongs to context "${selectedContext.id}".`,
|
|
428
|
-
"Tasks created with TaskCreate will be persisted to this context.",
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
return lines.join("\n");
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// ---------------------------------------------------------------------------
|
|
435
|
-
// Context Inventory
|
|
436
|
-
// ---------------------------------------------------------------------------
|
|
437
|
-
|
|
438
|
-
/** Collector function: scans one aspect of the context folder, returns markdown or null. */
|
|
439
|
-
type InventoryCollector = (
|
|
440
|
-
contextId: string,
|
|
441
|
-
contextDir: string,
|
|
442
|
-
state: ContextState,
|
|
443
|
-
) => string | null;
|
|
444
|
-
|
|
445
|
-
/** Descriptions for known context subfolders. */
|
|
446
|
-
const KNOWN_FOLDERS: Record<string, string> = {
|
|
447
|
-
"plans": "Archived implementation plans from plan mode",
|
|
448
|
-
"session-transcripts": "JSONL records of previous agent sessions — read these to understand prior work",
|
|
449
|
-
"handoffs": "Structured briefing documents for session continuity",
|
|
450
|
-
"reviews": "Plan review artifacts (reviewer verdicts, corroboration reports)",
|
|
451
|
-
"notes": "Analysis files, reports, and documentation that don't belong in the codebase",
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
function collectFolderPath(contextId: string, contextDir: string, state: ContextState): string | null {
|
|
455
|
-
if (!fs.existsSync(contextDir)) return null;
|
|
456
|
-
return `**Context folder:** \`${contextDir}\`\n**State file:** \`${path.join(contextDir, "state.json")}\` — contains session history, task records, plan/handoff metadata`;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function collectStatePointers(contextId: string, contextDir: string, state: ContextState): string | null {
|
|
460
|
-
const pointers: string[] = [];
|
|
461
|
-
if (state.plan_path) {
|
|
462
|
-
const exists = fs.existsSync(state.plan_path);
|
|
463
|
-
pointers.push(`- **Active plan:** \`${state.plan_path}\`${exists ? "" : " (not found)"}`);
|
|
464
|
-
}
|
|
465
|
-
if (state.handoff_path) {
|
|
466
|
-
const exists = fs.existsSync(state.handoff_path);
|
|
467
|
-
pointers.push(`- **Active handoff:** \`${state.handoff_path}\`${exists ? "" : " (not found)"}`);
|
|
468
|
-
}
|
|
469
|
-
if (pointers.length === 0) return null;
|
|
470
|
-
return "**Key artifacts:**\n" + pointers.join("\n");
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function countFilesRecursive(dirPath: string): number {
|
|
474
|
-
let count = 0;
|
|
475
|
-
try {
|
|
476
|
-
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
477
|
-
for (const entry of entries) {
|
|
478
|
-
if (entry.isFile()) {
|
|
479
|
-
count++;
|
|
480
|
-
} else if (entry.isDirectory()) {
|
|
481
|
-
count += countFilesRecursive(path.join(dirPath, entry.name));
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
} catch { /* permission errors, etc. */ }
|
|
485
|
-
return count;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function collectFolderInventory(contextId: string, contextDir: string, state: ContextState): string | null {
|
|
489
|
-
if (!fs.existsSync(contextDir)) return null;
|
|
490
|
-
let entries: fs.Dirent[];
|
|
491
|
-
try {
|
|
492
|
-
entries = fs.readdirSync(contextDir, { withFileTypes: true });
|
|
493
|
-
} catch { return null; }
|
|
494
|
-
|
|
495
|
-
const dirs = entries.filter(e => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
|
|
496
|
-
if (dirs.length === 0) return null;
|
|
497
|
-
|
|
498
|
-
const lines: string[] = ["**Available folders:**"];
|
|
499
|
-
for (const dir of dirs) {
|
|
500
|
-
const dirPath = path.join(contextDir, dir.name);
|
|
501
|
-
const desc = KNOWN_FOLDERS[dir.name] ?? "Project-specific artifacts";
|
|
502
|
-
const fileCount = countFilesRecursive(dirPath);
|
|
503
|
-
lines.push(`- \`${dir.name}/\` — ${desc} (${fileCount} file${fileCount !== 1 ? "s" : ""})`);
|
|
504
|
-
}
|
|
505
|
-
return lines.join("\n");
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function collectSessionStats(contextId: string, contextDir: string, state: ContextState): string | null {
|
|
509
|
-
const sessionCount = (state.session_ids ?? []).length;
|
|
510
|
-
if (sessionCount === 0) return null;
|
|
511
|
-
|
|
512
|
-
const transcriptsDir = path.join(contextDir, "session-transcripts");
|
|
513
|
-
let transcriptCount = 0;
|
|
514
|
-
let timeRange = "";
|
|
515
|
-
|
|
516
|
-
if (fs.existsSync(transcriptsDir)) {
|
|
517
|
-
try {
|
|
518
|
-
const files = fs.readdirSync(transcriptsDir).filter(f => f.endsWith(".jsonl")).sort();
|
|
519
|
-
transcriptCount = files.length;
|
|
520
|
-
if (files.length > 1) {
|
|
521
|
-
const oldest = files[0]!.slice(0, 10);
|
|
522
|
-
const newest = files[files.length - 1]!.slice(0, 10);
|
|
523
|
-
if (oldest !== newest) timeRange = ` (${oldest} to ${newest})`;
|
|
524
|
-
}
|
|
525
|
-
} catch { /* ignore */ }
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
let line = `**Sessions:** ${sessionCount} total`;
|
|
529
|
-
if (transcriptCount > 0) {
|
|
530
|
-
line += `, ${transcriptCount} transcript${transcriptCount !== 1 ? "s" : ""} archived${timeRange}`;
|
|
531
|
-
}
|
|
532
|
-
return line;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function collectNotesGuidance(contextId: string, contextDir: string, state: ContextState): string | null {
|
|
536
|
-
const notesDir = path.join(contextDir, "notes");
|
|
537
|
-
return `**Notes:** Put notes and files that don't belong in the codebase here. Reference them in other documents as needed: \`${notesDir}\``;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/** Ordered list of inventory collectors. Append new collectors here. */
|
|
541
|
-
const INVENTORY_COLLECTORS: InventoryCollector[] = [
|
|
542
|
-
collectFolderPath,
|
|
543
|
-
collectStatePointers,
|
|
544
|
-
collectFolderInventory,
|
|
545
|
-
collectSessionStats,
|
|
546
|
-
collectNotesGuidance,
|
|
547
|
-
];
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Build a markdown inventory of resources available in the context folder.
|
|
551
|
-
* Returns null if the context folder doesn't exist yet (brand new context).
|
|
552
|
-
*/
|
|
553
|
-
export function buildContextInventory(
|
|
554
|
-
state: ContextState,
|
|
555
|
-
projectRoot: string,
|
|
556
|
-
): string | null {
|
|
557
|
-
const contextDir = getContextDir(state.id, projectRoot);
|
|
558
|
-
if (!fs.existsSync(contextDir)) return null;
|
|
559
|
-
|
|
560
|
-
const sections = INVENTORY_COLLECTORS
|
|
561
|
-
.map(c => c(state.id, contextDir, state))
|
|
562
|
-
.filter((s): s is string => s !== null);
|
|
563
|
-
|
|
564
|
-
if (sections.length === 0) return null;
|
|
565
|
-
return "### Context Resources\n\n" + sections.join("\n\n");
|
|
566
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Formatting module for context display output.
|
|
3
|
+
* See SPEC.md §11
|
|
4
|
+
*
|
|
5
|
+
* All functions accept a ContextState with fields:
|
|
6
|
+
* id, summary, mode, last_active, plan_path, handoff_path,
|
|
7
|
+
* tasks[], last_session, session_ids, status, method, tags
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { parseIsoTimestamp } from "../base/utils.js";
|
|
13
|
+
import { getContextDir } from "../base/constants.js";
|
|
14
|
+
import type { ContextState, Task } from "../types.js";
|
|
15
|
+
|
|
16
|
+
const MAX_PLAN_INLINE_CHARS = 30_000;
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Mode display
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
const MODE_DISPLAY_MAP: Record<string, string> = {
|
|
23
|
+
idle: "",
|
|
24
|
+
has_staged_work: "[Staged]", // CHANGED: unified mode (plan or handoff)
|
|
25
|
+
active: "[Active]",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get bracketed display string for mode, or empty for idle.
|
|
30
|
+
* See SPEC.md §11.2
|
|
31
|
+
*/
|
|
32
|
+
export function getModeDisplay(mode: string): string {
|
|
33
|
+
return MODE_DISPLAY_MAP[mode] ?? "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Time formatting
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Format ISO timestamp as '2 hours ago', 'yesterday', etc.
|
|
42
|
+
* See SPEC.md §11.3
|
|
43
|
+
*/
|
|
44
|
+
export function formatRelativeTime(isoTimestamp: string | null): string {
|
|
45
|
+
if (!isoTimestamp) return "unknown";
|
|
46
|
+
|
|
47
|
+
let dt = parseIsoTimestamp(isoTimestamp);
|
|
48
|
+
if (!dt) return isoTimestamp.slice(0, 16);
|
|
49
|
+
|
|
50
|
+
const now = new Date();
|
|
51
|
+
|
|
52
|
+
// Strip timezone info for comparison
|
|
53
|
+
const diffMs = now.getTime() - dt.getTime();
|
|
54
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
55
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
56
|
+
const diffHours = Math.floor(diffMin / 60);
|
|
57
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
58
|
+
|
|
59
|
+
if (diffDays === 0) {
|
|
60
|
+
if (diffHours === 0) {
|
|
61
|
+
if (diffMin === 0) return "just now";
|
|
62
|
+
return diffMin === 1 ? "1 minute ago" : `${diffMin} minutes ago`;
|
|
63
|
+
}
|
|
64
|
+
return diffHours === 1 ? "1 hour ago" : `${diffHours} hours ago`;
|
|
65
|
+
}
|
|
66
|
+
if (diffDays === 1) return "yesterday";
|
|
67
|
+
if (diffDays < 7) return `${diffDays} days ago`;
|
|
68
|
+
|
|
69
|
+
// Older: show date
|
|
70
|
+
const year = dt.getFullYear();
|
|
71
|
+
const month = String(dt.getMonth() + 1).padStart(2, "0");
|
|
72
|
+
const day = String(dt.getDate()).padStart(2, "0");
|
|
73
|
+
return `${year}-${month}-${day}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Internal helpers
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
function taskAttr(task: Task | Record<string, any>, key: string, defaultVal = ""): string {
|
|
81
|
+
if (typeof task === "object" && task !== null) {
|
|
82
|
+
return (task as any)[key] ?? defaultVal;
|
|
83
|
+
}
|
|
84
|
+
return defaultVal;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readPlanContent(planPath: string): [string | null, boolean, number] {
|
|
88
|
+
try {
|
|
89
|
+
if (!fs.existsSync(planPath)) return [null, false, 0];
|
|
90
|
+
const content = fs.readFileSync(planPath, "utf-8");
|
|
91
|
+
const total = content.length;
|
|
92
|
+
if (total > MAX_PLAN_INLINE_CHARS) {
|
|
93
|
+
return [content.slice(0, MAX_PLAN_INLINE_CHARS), true, total];
|
|
94
|
+
}
|
|
95
|
+
return [content, false, total];
|
|
96
|
+
} catch {
|
|
97
|
+
return [null, false, 0];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function modeLabel(ctx: ContextState): string {
|
|
102
|
+
const d = getModeDisplay(ctx.mode ?? "idle");
|
|
103
|
+
return d ? d.replace(/^\[|\]$/g, "") : "Active";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build restore sections from last_session, tasks, and plan_path.
|
|
108
|
+
* See SPEC.md §11.4
|
|
109
|
+
*/
|
|
110
|
+
export function buildRestoreSections(
|
|
111
|
+
ctx: ContextState,
|
|
112
|
+
projectRoot?: string,
|
|
113
|
+
inlinePlan = false,
|
|
114
|
+
): string {
|
|
115
|
+
const sections: string[] = [];
|
|
116
|
+
const lastSession = ctx.last_session ?? {};
|
|
117
|
+
|
|
118
|
+
if (lastSession) {
|
|
119
|
+
const savedAt = lastSession.saved_at ?? "";
|
|
120
|
+
if (savedAt) {
|
|
121
|
+
const reason = lastSession.save_reason ?? "";
|
|
122
|
+
const reasonDisplay = reason ? reason.replace(/_/g, " ") : "unknown";
|
|
123
|
+
sections.push(`**Last session ended:** ${formatRelativeTime(savedAt)} (${reasonDisplay})`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const tasks = ctx.tasks ?? [];
|
|
128
|
+
if (tasks.length > 0) {
|
|
129
|
+
const buckets: Record<string, string[]> = {
|
|
130
|
+
completed: [],
|
|
131
|
+
in_progress: [],
|
|
132
|
+
pending: [],
|
|
133
|
+
blocked: [],
|
|
134
|
+
};
|
|
135
|
+
for (const t of tasks) {
|
|
136
|
+
const s = taskAttr(t, "status", "pending");
|
|
137
|
+
if (buckets[s]) {
|
|
138
|
+
buckets[s]!.push(taskAttr(t, "subject"));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (Object.values(buckets).some(b => b.length > 0)) {
|
|
142
|
+
sections.push("", `### Previous Work (${tasks.length} tasks)`, "");
|
|
143
|
+
const marks: Record<string, string> = {
|
|
144
|
+
completed: "[x]",
|
|
145
|
+
in_progress: "[~]",
|
|
146
|
+
pending: "[ ]",
|
|
147
|
+
blocked: "[!]",
|
|
148
|
+
};
|
|
149
|
+
for (const [status, mark] of Object.entries(marks)) {
|
|
150
|
+
for (const subj of buckets[status] ?? []) {
|
|
151
|
+
sections.push(`- ${mark} ${subj}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const planPath = ctx.plan_path;
|
|
158
|
+
if (planPath) {
|
|
159
|
+
if (inlinePlan) {
|
|
160
|
+
const [content, truncated, totalChars] = readPlanContent(planPath);
|
|
161
|
+
if (content) {
|
|
162
|
+
let header = `Plan loaded from: \`${planPath}\``;
|
|
163
|
+
if (truncated) header += ` (truncated, ${totalChars} chars total)`;
|
|
164
|
+
sections.push("", "### Plan", header, "", content);
|
|
165
|
+
if (truncated) {
|
|
166
|
+
sections.push(`\n*Plan truncated at ${MAX_PLAN_INLINE_CHARS} characters. Full plan at: \`${planPath}\`*`);
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
sections.push("", "### Plan", `*Plan file not found at \`${planPath}\`.*`);
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
sections.push("", "### Plan", `Read the plan at: \`${planPath}\``);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const gitState = lastSession?.git_state ?? {};
|
|
177
|
+
if (gitState && Object.keys(gitState).length > 0) {
|
|
178
|
+
const branch = gitState.branch ?? "unknown";
|
|
179
|
+
const uncommitted: string[] = gitState.uncommitted_files ?? [];
|
|
180
|
+
const lastCommit = gitState.last_commit_short ?? "";
|
|
181
|
+
let uncStr = uncommitted.length > 0 ? uncommitted.slice(0, 5).join(", ") : "none";
|
|
182
|
+
if (uncommitted.length > 5) uncStr += ` (+${uncommitted.length - 5} more)`;
|
|
183
|
+
sections.push("", "### Git State", `Branch: ${branch} | Uncommitted: ${uncStr}`);
|
|
184
|
+
if (lastCommit) sections.push(`Last commit: ${lastCommit}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return sections.join("\n");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function resumeBlock(ctx: ContextState, projectRoot: string | undefined, modeText: string, instructions: string[]): string {
|
|
191
|
+
const lines = [
|
|
192
|
+
`## Resuming Context: ${ctx.id}`, "",
|
|
193
|
+
`**Summary:** ${ctx.summary}`,
|
|
194
|
+
`**Mode:** ${modeText}`,
|
|
195
|
+
];
|
|
196
|
+
const restore = buildRestoreSections(ctx, projectRoot, true);
|
|
197
|
+
if (restore) lines.push(restore);
|
|
198
|
+
lines.push("", "---", "", "**Instructions:**");
|
|
199
|
+
lines.push(...instructions);
|
|
200
|
+
return lines.join("\n");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Public formatters
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Format output when resuming a context with a pending handoff.
|
|
209
|
+
* See SPEC.md §11.5
|
|
210
|
+
*/
|
|
211
|
+
export function formatHandoffContinuation(ctx: ContextState, projectRoot?: string): string {
|
|
212
|
+
const handoffPath = ctx.handoff_path ?? "";
|
|
213
|
+
const lines = [
|
|
214
|
+
`## Resuming Context: ${ctx.id} (Handoff Available)`, "",
|
|
215
|
+
`**Summary:** ${ctx.summary}`,
|
|
216
|
+
`**Mode:** Implementing (handoff from previous session)`, "",
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
if (handoffPath && fs.existsSync(handoffPath)) {
|
|
221
|
+
lines.push("### Previous Session Handoff", "", fs.readFileSync(handoffPath, "utf-8"), "");
|
|
222
|
+
} else {
|
|
223
|
+
lines.push(`*Handoff document not found at \`${handoffPath}\`*`, "");
|
|
224
|
+
}
|
|
225
|
+
} catch (e: any) {
|
|
226
|
+
lines.push(`*Handoff document at \`${handoffPath}\` could not be read: ${e}*`, "");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const restore = buildRestoreSections(ctx, projectRoot, true);
|
|
230
|
+
if (restore) lines.push(restore);
|
|
231
|
+
|
|
232
|
+
lines.push("", "---", "", "**Instructions:**",
|
|
233
|
+
"1. Review the handoff document above - especially dead ends",
|
|
234
|
+
"2. Check the plan file for remaining tasks",
|
|
235
|
+
"3. Continue implementation from where the previous session left off");
|
|
236
|
+
return lines.join("\n");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Format output for pending plan implementation (mode=has_plan).
|
|
241
|
+
* See SPEC.md §11.6
|
|
242
|
+
*/
|
|
243
|
+
export function formatPlanContinuation(ctx: ContextState, projectRoot?: string): string {
|
|
244
|
+
return resumeBlock(ctx, projectRoot, "Pending Implementation", [
|
|
245
|
+
"1. Review the plan and previous work above",
|
|
246
|
+
"2. Continue from where the previous session left off",
|
|
247
|
+
]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Format output for ongoing implementation (mode=active).
|
|
252
|
+
* See SPEC.md §11.7
|
|
253
|
+
*/
|
|
254
|
+
export function formatActiveContinuation(ctx: ContextState, projectRoot?: string): string {
|
|
255
|
+
return resumeBlock(ctx, projectRoot, "Implementing", [
|
|
256
|
+
"1. Review the plan and previous work above",
|
|
257
|
+
"2. Continue from where the previous session left off",
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Format list of contexts for display.
|
|
263
|
+
* See SPEC.md §11.8
|
|
264
|
+
*/
|
|
265
|
+
export function formatContextList(contexts: ContextState[]): string {
|
|
266
|
+
if (contexts.length === 0) return "No active contexts found.";
|
|
267
|
+
|
|
268
|
+
const lines = ["## Active Contexts\n"];
|
|
269
|
+
for (let i = 0; i < contexts.length; i++) {
|
|
270
|
+
const ctx = contexts[i]!;
|
|
271
|
+
const timeStr = formatRelativeTime(ctx.last_active);
|
|
272
|
+
const md = getModeDisplay(ctx.mode ?? "idle");
|
|
273
|
+
const si = md ? ` ${md}` : "";
|
|
274
|
+
lines.push(`**${i + 1}. ${ctx.id}**${si}`);
|
|
275
|
+
lines.push(` ${ctx.summary}`);
|
|
276
|
+
if (ctx.method) {
|
|
277
|
+
lines.push(` Method: ${ctx.method} | Last active: ${timeStr}`);
|
|
278
|
+
} else {
|
|
279
|
+
lines.push(` Last active: ${timeStr}`);
|
|
280
|
+
}
|
|
281
|
+
lines.push("");
|
|
282
|
+
}
|
|
283
|
+
return lines.join("\n");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Format notification for a newly created context.
|
|
288
|
+
* See SPEC.md §11.9
|
|
289
|
+
*/
|
|
290
|
+
export function formatContextCreated(ctx: ContextState): string {
|
|
291
|
+
return [
|
|
292
|
+
`## Context Created: ${ctx.id}`, "",
|
|
293
|
+
`**Summary:** ${ctx.summary}`, "",
|
|
294
|
+
"A new context has been created for this work.",
|
|
295
|
+
"Tasks created with TaskCreate will be persisted to this context.",
|
|
296
|
+
].join("\n");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Format system reminder: lightweight (per-prompt) or rich (first-bind restore).
|
|
301
|
+
* See SPEC.md §11.10
|
|
302
|
+
*/
|
|
303
|
+
export function formatActiveContextReminder(
|
|
304
|
+
ctx: ContextState,
|
|
305
|
+
projectRoot?: string,
|
|
306
|
+
includeRestore = false,
|
|
307
|
+
): string {
|
|
308
|
+
const timeStr = formatRelativeTime(ctx.last_active);
|
|
309
|
+
const label = modeLabel(ctx);
|
|
310
|
+
|
|
311
|
+
if (includeRestore) {
|
|
312
|
+
const lines = [
|
|
313
|
+
`## Resuming Context: ${ctx.id}`, "",
|
|
314
|
+
`**Summary:** ${ctx.summary}`,
|
|
315
|
+
`**Mode:** ${label}`,
|
|
316
|
+
];
|
|
317
|
+
const restore = buildRestoreSections(ctx, projectRoot, true);
|
|
318
|
+
if (restore) lines.push(restore);
|
|
319
|
+
lines.push("", "---", "", "**Instructions:**",
|
|
320
|
+
"1. Review the previous work above",
|
|
321
|
+
"2. Continue from where the previous session left off");
|
|
322
|
+
return lines.join("\n");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return [
|
|
326
|
+
`## Active Context: ${ctx.id}`, "",
|
|
327
|
+
`**Summary:** ${ctx.summary}`,
|
|
328
|
+
`**Mode:** ${label}`,
|
|
329
|
+
`**Last Active:** ${timeStr}`, "",
|
|
330
|
+
`All work belongs to context "${ctx.id}".`,
|
|
331
|
+
"Tasks created with TaskCreate will be persisted to this context.",
|
|
332
|
+
].join("\n");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// Picker / command feedback
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Format the boxed picker shown on stderr when blocking for selection.
|
|
341
|
+
* See SPEC.md §11.11
|
|
342
|
+
*/
|
|
343
|
+
export function formatContextPickerStderr(contexts: ContextState[]): string {
|
|
344
|
+
const lines = [
|
|
345
|
+
"",
|
|
346
|
+
"+----------------------------------------------------------------+",
|
|
347
|
+
"| CONTEXT SELECTION REQUIRED |",
|
|
348
|
+
"+----------------------------------------------------------------+",
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
let selectableCount = 0;
|
|
352
|
+
for (let i = 0; i < contexts.length; i++) {
|
|
353
|
+
const ctx = contexts[i]!;
|
|
354
|
+
const timeStr = formatRelativeTime(ctx.last_active);
|
|
355
|
+
const mode = ctx.mode ?? "idle";
|
|
356
|
+
const isSelectable = mode === "active" || !!ctx.handoff_path;
|
|
357
|
+
if (isSelectable) selectableCount++;
|
|
358
|
+
|
|
359
|
+
let status = "";
|
|
360
|
+
if (ctx.handoff_path) {
|
|
361
|
+
status = " [Handoff Ready]";
|
|
362
|
+
} else if (getModeDisplay(mode)) {
|
|
363
|
+
status = ` ${getModeDisplay(mode)}`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const summary = ctx.summary.length > 48 ? ctx.summary.slice(0, 45) + "..." : ctx.summary;
|
|
367
|
+
const selTag = isSelectable ? " [selectable]" : " [end only]";
|
|
368
|
+
|
|
369
|
+
lines.push(`| ^${i + 1} ${ctx.id}${status}${selTag}`);
|
|
370
|
+
lines.push(`| ${summary}`);
|
|
371
|
+
lines.push(`| [${timeStr}]`);
|
|
372
|
+
lines.push("|");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
lines.push(
|
|
376
|
+
"+----------------------------------------------------------------+",
|
|
377
|
+
"| Usage: |",
|
|
378
|
+
"| ^S<N> - Select context by number |",
|
|
379
|
+
"| ^E<N> - End/complete context by number |",
|
|
380
|
+
"| ^S:query - Select by ID match (race-safe) |",
|
|
381
|
+
"| ^E:query - End by ID match (race-safe) |",
|
|
382
|
+
"| ^E<N>+ - End context N and all after |",
|
|
383
|
+
"| ^E* - End ALL contexts |",
|
|
384
|
+
"| ^E1E2S3 - End #1 and #2, select #3 |",
|
|
385
|
+
"| ^E:fooS:bar - End 'foo...', select 'bar...' |",
|
|
386
|
+
"| ^0 work description - Create new context (10+ chars) |",
|
|
387
|
+
"+----------------------------------------------------------------+",
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
if (selectableCount === 0) {
|
|
391
|
+
lines.push(
|
|
392
|
+
"| NOTE: No selectable contexts. |",
|
|
393
|
+
"| Use ^E<N> to end old contexts, then ^0 to create new. |",
|
|
394
|
+
"+----------------------------------------------------------------+",
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
lines.push("");
|
|
398
|
+
return lines.join("\n");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Format feedback about caret command operations performed.
|
|
403
|
+
* See SPEC.md §11.12
|
|
404
|
+
*/
|
|
405
|
+
export function formatCommandFeedback(
|
|
406
|
+
endedContexts: ContextState[],
|
|
407
|
+
selectedContext: ContextState | null,
|
|
408
|
+
): string {
|
|
409
|
+
const lines: string[] = [];
|
|
410
|
+
if (endedContexts.length > 0) {
|
|
411
|
+
lines.push("## Contexts Ended", "");
|
|
412
|
+
for (const ctx of endedContexts) {
|
|
413
|
+
const s = ctx.summary.length > 50 ? ctx.summary.slice(0, 50) + "..." : ctx.summary;
|
|
414
|
+
lines.push(`- **${ctx.id}**: ${s}`);
|
|
415
|
+
}
|
|
416
|
+
lines.push("");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (selectedContext) {
|
|
420
|
+
const label = modeLabel(selectedContext);
|
|
421
|
+
const timeStr = formatRelativeTime(selectedContext.last_active);
|
|
422
|
+
lines.push(
|
|
423
|
+
`## Active Context: ${selectedContext.id}`, "",
|
|
424
|
+
`**Summary:** ${selectedContext.summary}`,
|
|
425
|
+
`**Mode:** ${label}`,
|
|
426
|
+
`**Last Active:** ${timeStr}`, "",
|
|
427
|
+
`All work belongs to context "${selectedContext.id}".`,
|
|
428
|
+
"Tasks created with TaskCreate will be persisted to this context.",
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
return lines.join("\n");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
// Context Inventory
|
|
436
|
+
// ---------------------------------------------------------------------------
|
|
437
|
+
|
|
438
|
+
/** Collector function: scans one aspect of the context folder, returns markdown or null. */
|
|
439
|
+
type InventoryCollector = (
|
|
440
|
+
contextId: string,
|
|
441
|
+
contextDir: string,
|
|
442
|
+
state: ContextState,
|
|
443
|
+
) => string | null;
|
|
444
|
+
|
|
445
|
+
/** Descriptions for known context subfolders. */
|
|
446
|
+
const KNOWN_FOLDERS: Record<string, string> = {
|
|
447
|
+
"plans": "Archived implementation plans from plan mode",
|
|
448
|
+
"session-transcripts": "JSONL records of previous agent sessions — read these to understand prior work",
|
|
449
|
+
"handoffs": "Structured briefing documents for session continuity",
|
|
450
|
+
"reviews": "Plan review artifacts (reviewer verdicts, corroboration reports)",
|
|
451
|
+
"notes": "Analysis files, reports, and documentation that don't belong in the codebase",
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
function collectFolderPath(contextId: string, contextDir: string, state: ContextState): string | null {
|
|
455
|
+
if (!fs.existsSync(contextDir)) return null;
|
|
456
|
+
return `**Context folder:** \`${contextDir}\`\n**State file:** \`${path.join(contextDir, "state.json")}\` — contains session history, task records, plan/handoff metadata`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function collectStatePointers(contextId: string, contextDir: string, state: ContextState): string | null {
|
|
460
|
+
const pointers: string[] = [];
|
|
461
|
+
if (state.plan_path) {
|
|
462
|
+
const exists = fs.existsSync(state.plan_path);
|
|
463
|
+
pointers.push(`- **Active plan:** \`${state.plan_path}\`${exists ? "" : " (not found)"}`);
|
|
464
|
+
}
|
|
465
|
+
if (state.handoff_path) {
|
|
466
|
+
const exists = fs.existsSync(state.handoff_path);
|
|
467
|
+
pointers.push(`- **Active handoff:** \`${state.handoff_path}\`${exists ? "" : " (not found)"}`);
|
|
468
|
+
}
|
|
469
|
+
if (pointers.length === 0) return null;
|
|
470
|
+
return "**Key artifacts:**\n" + pointers.join("\n");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function countFilesRecursive(dirPath: string): number {
|
|
474
|
+
let count = 0;
|
|
475
|
+
try {
|
|
476
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
477
|
+
for (const entry of entries) {
|
|
478
|
+
if (entry.isFile()) {
|
|
479
|
+
count++;
|
|
480
|
+
} else if (entry.isDirectory()) {
|
|
481
|
+
count += countFilesRecursive(path.join(dirPath, entry.name));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
} catch { /* permission errors, etc. */ }
|
|
485
|
+
return count;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function collectFolderInventory(contextId: string, contextDir: string, state: ContextState): string | null {
|
|
489
|
+
if (!fs.existsSync(contextDir)) return null;
|
|
490
|
+
let entries: fs.Dirent[];
|
|
491
|
+
try {
|
|
492
|
+
entries = fs.readdirSync(contextDir, { withFileTypes: true });
|
|
493
|
+
} catch { return null; }
|
|
494
|
+
|
|
495
|
+
const dirs = entries.filter(e => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
|
|
496
|
+
if (dirs.length === 0) return null;
|
|
497
|
+
|
|
498
|
+
const lines: string[] = ["**Available folders:**"];
|
|
499
|
+
for (const dir of dirs) {
|
|
500
|
+
const dirPath = path.join(contextDir, dir.name);
|
|
501
|
+
const desc = KNOWN_FOLDERS[dir.name] ?? "Project-specific artifacts";
|
|
502
|
+
const fileCount = countFilesRecursive(dirPath);
|
|
503
|
+
lines.push(`- \`${dir.name}/\` — ${desc} (${fileCount} file${fileCount !== 1 ? "s" : ""})`);
|
|
504
|
+
}
|
|
505
|
+
return lines.join("\n");
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function collectSessionStats(contextId: string, contextDir: string, state: ContextState): string | null {
|
|
509
|
+
const sessionCount = (state.session_ids ?? []).length;
|
|
510
|
+
if (sessionCount === 0) return null;
|
|
511
|
+
|
|
512
|
+
const transcriptsDir = path.join(contextDir, "session-transcripts");
|
|
513
|
+
let transcriptCount = 0;
|
|
514
|
+
let timeRange = "";
|
|
515
|
+
|
|
516
|
+
if (fs.existsSync(transcriptsDir)) {
|
|
517
|
+
try {
|
|
518
|
+
const files = fs.readdirSync(transcriptsDir).filter(f => f.endsWith(".jsonl")).sort();
|
|
519
|
+
transcriptCount = files.length;
|
|
520
|
+
if (files.length > 1) {
|
|
521
|
+
const oldest = files[0]!.slice(0, 10);
|
|
522
|
+
const newest = files[files.length - 1]!.slice(0, 10);
|
|
523
|
+
if (oldest !== newest) timeRange = ` (${oldest} to ${newest})`;
|
|
524
|
+
}
|
|
525
|
+
} catch { /* ignore */ }
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
let line = `**Sessions:** ${sessionCount} total`;
|
|
529
|
+
if (transcriptCount > 0) {
|
|
530
|
+
line += `, ${transcriptCount} transcript${transcriptCount !== 1 ? "s" : ""} archived${timeRange}`;
|
|
531
|
+
}
|
|
532
|
+
return line;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function collectNotesGuidance(contextId: string, contextDir: string, state: ContextState): string | null {
|
|
536
|
+
const notesDir = path.join(contextDir, "notes");
|
|
537
|
+
return `**Notes:** Put notes and files that don't belong in the codebase here. Reference them in other documents as needed: \`${notesDir}\``;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Ordered list of inventory collectors. Append new collectors here. */
|
|
541
|
+
const INVENTORY_COLLECTORS: InventoryCollector[] = [
|
|
542
|
+
collectFolderPath,
|
|
543
|
+
collectStatePointers,
|
|
544
|
+
collectFolderInventory,
|
|
545
|
+
collectSessionStats,
|
|
546
|
+
collectNotesGuidance,
|
|
547
|
+
];
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Build a markdown inventory of resources available in the context folder.
|
|
551
|
+
* Returns null if the context folder doesn't exist yet (brand new context).
|
|
552
|
+
*/
|
|
553
|
+
export function buildContextInventory(
|
|
554
|
+
state: ContextState,
|
|
555
|
+
projectRoot: string,
|
|
556
|
+
): string | null {
|
|
557
|
+
const contextDir = getContextDir(state.id, projectRoot);
|
|
558
|
+
if (!fs.existsSync(contextDir)) return null;
|
|
559
|
+
|
|
560
|
+
const sections = INVENTORY_COLLECTORS
|
|
561
|
+
.map(c => c(state.id, contextDir, state))
|
|
562
|
+
.filter((s): s is string => s !== null);
|
|
563
|
+
|
|
564
|
+
if (sections.length === 0) return null;
|
|
565
|
+
return "### Context Resources\n\n" + sections.join("\n\n");
|
|
566
|
+
}
|