@vpxa/aikit 0.1.274 → 0.1.275
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/package.json +7 -1
- package/packages/cli/dist/index.js +14 -14
- package/packages/cli/dist/{init-BgelSos0.js → init-B3a2fygD.js} +1 -1
- package/packages/cli/dist/{scaffold-Cxjwg531.js → scaffold-BNPHP-QC.js} +1 -1
- package/packages/cli/dist/{templates-C92mODRl.js → templates-CDa0UuoE.js} +19 -19
- package/packages/core/dist/index.d.ts +45 -17
- package/packages/core/dist/index.js +1 -1
- package/packages/flows/dist/index.d.ts +23 -2
- package/packages/flows/dist/index.js +1 -1
- package/packages/server/dist/bin.js +6 -6
- package/packages/server/dist/config-DZ-6Zy94.js +2 -0
- package/packages/server/dist/config-DxWyWSb9.js +1 -0
- package/packages/server/dist/curated-manager-C5uOPept.js +7 -0
- package/packages/server/dist/index.js +1 -1
- package/packages/server/dist/{promotion-BNEScZVD.js → promotion-D9anNXv8.js} +1 -1
- package/packages/server/dist/{routes-CR3fI-HJ.js → routes-1wkXLxXe.js} +1 -1
- package/packages/server/dist/{routes-Afg7J7xK.js → routes-KC-D2U8n.js} +1 -1
- package/packages/server/dist/{server-4h0Cclv3.js → server-CkCRBlz4.js} +93 -93
- package/packages/server/dist/{server-DIz2FGOX.js → server-DlE6A6sd.js} +93 -93
- package/packages/server/dist/{version-check-gazMo-D4.js → version-check-CgfflkJX.js} +1 -1
- package/packages/server/dist/{version-check-BgHzxxCW.js → version-check-ruLtfyDd.js} +1 -1
- package/packages/server/viewers/canvas.html +2 -1
- package/packages/server/viewers/task-plan-static.html +2 -1
- package/packages/tools/dist/index.d.ts +5 -5
- package/packages/tools/dist/index.js +72 -72
- package/scaffold/dist/definitions/hooks.mjs +1 -1
- package/scaffold/dist/definitions/skills/c4-architecture.mjs +1 -1
- package/scaffold/dist/definitions/skills/session-handoff.mjs +2 -732
- package/packages/server/dist/config-CZuVxRpX.js +0 -1
- package/packages/server/dist/config-WpN5CWM7.js +0 -2
- package/packages/server/dist/curated-manager-CfwN96rp.js +0 -7
|
@@ -1,732 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
## Table of Contents
|
|
4
|
-
Use headings
|
|
5
|
-
---
|
|
6
|
-
# Handoff: [TASK_TITLE]
|
|
7
|
-
## Session Metadata
|
|
8
|
-
- Created: [TIMESTAMP]
|
|
9
|
-
- Project: [PROJECT_PATH]
|
|
10
|
-
- Branch: [GIT_BRANCH]
|
|
11
|
-
- Session duration: [APPROX_DURATION]
|
|
12
|
-
## Current State Summary
|
|
13
|
-
[One paragraph: work, status, handoff point]
|
|
14
|
-
## Codebase Understanding
|
|
15
|
-
### Architecture Overview
|
|
16
|
-
[Key architecture insights]
|
|
17
|
-
### Critical Files
|
|
18
|
-
| File | Purpose | Relevance |
|
|
19
|
-
|------|---------|-----------|
|
|
20
|
-
| path/to/file | Purpose | Why it matters now |
|
|
21
|
-
### Key Patterns Discovered
|
|
22
|
-
[Patterns next agent should follow]
|
|
23
|
-
## Work Completed
|
|
24
|
-
### Tasks Finished
|
|
25
|
-
- [x] Task 1 - what was done
|
|
26
|
-
- [x] Task 2 - brief description
|
|
27
|
-
### Files Modified
|
|
28
|
-
| File | Changes | Rationale |
|
|
29
|
-
|------|---------|-----------|
|
|
30
|
-
| path/to/file | Changes | Why |
|
|
31
|
-
### Decisions Made
|
|
32
|
-
| Decision | Options Considered | Rationale |
|
|
33
|
-
|----------|-------------------|-----------|
|
|
34
|
-
| Chose X over Y | X, Y, Z | Why |
|
|
35
|
-
## Pending Work
|
|
36
|
-
### Immediate Next Steps
|
|
37
|
-
1. [First action]
|
|
38
|
-
2. [Second action]
|
|
39
|
-
3. [Third action]
|
|
40
|
-
### Blockers/Open Questions
|
|
41
|
-
- [ ] Blocker: [description] - Needs: [unblocker]
|
|
42
|
-
- [ ] Question: [unclear aspect] - Suggested: [resolution]
|
|
43
|
-
### Deferred Items
|
|
44
|
-
- Item 1 (deferred: [reason])
|
|
45
|
-
## Context for Resuming Agent
|
|
46
|
-
### Important Context
|
|
47
|
-
[Critical information next agent MUST know]
|
|
48
|
-
### Assumptions Made
|
|
49
|
-
- Assumption 1: [assumed true]
|
|
50
|
-
- Assumption 2: [another assumption]
|
|
51
|
-
### Potential Gotchas
|
|
52
|
-
- [Things that might trip up next agent]
|
|
53
|
-
## Environment State
|
|
54
|
-
### Tools/Services Used
|
|
55
|
-
- [Tool/Service]: [relevant state]
|
|
56
|
-
### Active Processes
|
|
57
|
-
- [Background processes, dev servers, watchers]
|
|
58
|
-
### Environment Variables
|
|
59
|
-
- [Env vars that matter - names only, no values]
|
|
60
|
-
## Related Resources
|
|
61
|
-
- [Relevant docs]
|
|
62
|
-
- [Related file paths]
|
|
63
|
-
- [External resources consulted]
|
|
64
|
-
---
|
|
65
|
-
## Template Usage Notes
|
|
66
|
-
Be specific. Use file refs when useful. Exclude secrets. Capture WHAT and WHY.
|
|
67
|
-
`},{file:`references/resume-checklist.md`,content:`# Resume Checklist
|
|
68
|
-
On resume.
|
|
69
|
-
## Pre-Resume Verification
|
|
70
|
-
- [ ] Read handoff before acting
|
|
71
|
-
- [ ] Verify project directory
|
|
72
|
-
- [ ] Confirm branch matches or note why it differs
|
|
73
|
-
- [ ] Check handoff timestamp
|
|
74
|
-
## Context Validation
|
|
75
|
-
- [ ] Review important context
|
|
76
|
-
- [ ] Check assumptions
|
|
77
|
-
- [ ] Check resolved blockers
|
|
78
|
-
- [ ] Review gotchas
|
|
79
|
-
## State Verification
|
|
80
|
-
- [ ] Run \`git status\`
|
|
81
|
-
- [ ] Compare modified files vs current state
|
|
82
|
-
- [ ] Check required env vars
|
|
83
|
-
- [ ] Verify required services/processes
|
|
84
|
-
## Resume Execution
|
|
85
|
-
- [ ] Start with next step #1
|
|
86
|
-
- [ ] Apply documented patterns
|
|
87
|
-
## During Work
|
|
88
|
-
- [ ] Update handoff if major context changes
|
|
89
|
-
- [ ] Add new blockers/questions
|
|
90
|
-
## Red Flags - Stop and Verify
|
|
91
|
-
1. **Files mentioned in handoff don't exist** - codebase may have changed
|
|
92
|
-
2. **Branch has diverged substantially** - check git log
|
|
93
|
-
3. **Assumptions are clearly invalid** - reassess
|
|
94
|
-
4. **Unresolved blockers are still blocking you** - escalate
|
|
95
|
-
5. **Architecture has changed** - re-explore
|
|
96
|
-
## Quick Start Commands
|
|
97
|
-
\`\`\`bash
|
|
98
|
-
git branch --show-current
|
|
99
|
-
git status
|
|
100
|
-
git log --oneline -10
|
|
101
|
-
ps aux | grep [process-name]
|
|
102
|
-
env | grep [relevant-var]
|
|
103
|
-
\`\`\`
|
|
104
|
-
## Handoff Quality Assessment
|
|
105
|
-
Re-explore if weak.
|
|
106
|
-
`},{file:`scripts/check_staleness.js`,content:`#!/usr/bin/env node
|
|
107
|
-
const fs = require('node:fs');
|
|
108
|
-
const path = require('node:path');
|
|
109
|
-
const { execSync } = require('node:child_process');
|
|
110
|
-
const runCmd = (cmd, cwd) => {
|
|
111
|
-
try { return { ok: true, out: execSync(cmd, { cwd, timeout: 10000, encoding: "utf-8" }).trim() }; }
|
|
112
|
-
catch { return { ok: false, out: "" }; }
|
|
113
|
-
};
|
|
114
|
-
function parseHandoffMetadata(filepath) {
|
|
115
|
-
const content = fs.readFileSync(filepath, "utf-8");
|
|
116
|
-
const meta = { created: null, branch: null, projectPath: null, modifiedFiles: [] };
|
|
117
|
-
const created = content.match(/Created:\\s*(\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}:\\d{2})/);
|
|
118
|
-
if (created) meta.created = new Date(created[1].replace(" ", "T"));
|
|
119
|
-
const branch = content.match(/Branch:\\s*(\\S+)/);
|
|
120
|
-
if (branch && !branch[1].startsWith("[")) meta.branch = branch[1];
|
|
121
|
-
const proj = content.match(/Project:\\s*(.+?)(?:\\n|$)/);
|
|
122
|
-
if (proj) meta.projectPath = proj[1].trim();
|
|
123
|
-
for (const match of content.matchAll(/\\|\\s*([a-zA-Z0-9_\\-./]+\\.[a-zA-Z]+)\\s*\\|/g)) {
|
|
124
|
-
if (match[1].includes("/") && !match[1].startsWith("[")) meta.modifiedFiles.push(match[1]);
|
|
125
|
-
}
|
|
126
|
-
return meta;
|
|
127
|
-
}
|
|
128
|
-
const getCommitsSince = (ts, cwd) => !ts ? [] : ((r) => r.ok && r.out ? r.out.split("\\n") : [])(runCmd("git log --since=\\"" + ts.toISOString() + "\\" --oneline --no-decorate", cwd));
|
|
129
|
-
function getChangedFilesSince(ts, cwd) {
|
|
130
|
-
if (!ts) return [];
|
|
131
|
-
const result = runCmd("git log --since=\\"" + ts.toISOString() + "\\" --name-only --pretty=format:", cwd);
|
|
132
|
-
return result.ok && result.out ? [...new Set(result.out.split("\\n").map((f) => f.trim()).filter(Boolean))] : [];
|
|
133
|
-
}
|
|
134
|
-
function checkFilesExist(files, cwd) {
|
|
135
|
-
const existing = []; const missing = [];
|
|
136
|
-
for (const file of files) (fs.existsSync(path.join(cwd, file)) ? existing : missing).push(file);
|
|
137
|
-
return { existing, missing };
|
|
138
|
-
}
|
|
139
|
-
function calculateStaleness(daysOld, commitsSince, filesChanged, branchMatches, filesMissing) {
|
|
140
|
-
const issues = []; let score = 0;
|
|
141
|
-
if (daysOld > 30) { score += 3; issues.push("Handoff is " + Math.floor(daysOld) + "d old"); }
|
|
142
|
-
else if (daysOld > 7) { score += 2; issues.push("Handoff is " + Math.floor(daysOld) + "d old"); }
|
|
143
|
-
else if (daysOld > 1) score += 1;
|
|
144
|
-
if (commitsSince > 50) { score += 3; issues.push(commitsSince + " commits since handoff - significant changes"); }
|
|
145
|
-
else if (commitsSince > 20) { score += 2; issues.push(commitsSince + " commits since handoff"); }
|
|
146
|
-
else if (commitsSince > 5) score += 1;
|
|
147
|
-
if (!branchMatches) { score += 2; issues.push("Current branch differs from handoff branch"); }
|
|
148
|
-
if (filesMissing > 5) { score += 2; issues.push(filesMissing + " referenced files no longer exist"); }
|
|
149
|
-
else if (filesMissing > 0) { score += 1; issues.push(filesMissing + " referenced file(s) missing"); }
|
|
150
|
-
if (filesChanged > 20) { score += 2; issues.push(filesChanged + " files changed since handoff"); }
|
|
151
|
-
else if (filesChanged > 5) score += 1;
|
|
152
|
-
if (score === 0) return { level: "FRESH", recommendation: "Safe to resume - minimal change", issues };
|
|
153
|
-
if (score <= 2) return { level: "SLIGHTLY_STALE", recommendation: "Generally safe - review changes", issues };
|
|
154
|
-
if (score <= 4) return { level: "STALE", recommendation: "Proceed with caution - changes may affect context", issues };
|
|
155
|
-
return { level: "VERY_STALE", recommendation: "Consider new handoff - too many changes", issues };
|
|
156
|
-
}
|
|
157
|
-
function resolveProjectRootFromHandoff(handoffPath) {
|
|
158
|
-
const handoffsDir = path.dirname(handoffPath);
|
|
159
|
-
const handoffMarkerDir = path.basename(path.dirname(handoffsDir));
|
|
160
|
-
const stateMarkerDir = path.basename(path.dirname(path.dirname(handoffsDir)));
|
|
161
|
-
return handoffMarkerDir === "handoffs" && stateMarkerDir === ".aikit-state" ? path.resolve(handoffsDir, "..", "..", "..") : path.resolve(handoffsDir, "..");
|
|
162
|
-
}
|
|
163
|
-
function checkStaleness(handoffPath) {
|
|
164
|
-
if (!fs.existsSync(handoffPath)) return { error: "Handoff file not found: " + handoffPath };
|
|
165
|
-
const meta = parseHandoffMetadata(handoffPath);
|
|
166
|
-
const projectPath = meta.projectPath && fs.existsSync(meta.projectPath) ? meta.projectPath : resolveProjectRootFromHandoff(handoffPath);
|
|
167
|
-
const isGitRepo = runCmd("git rev-parse --git-dir", projectPath).ok;
|
|
168
|
-
const result = { handoffFile: handoffPath, projectPath, isGitRepo, created: meta.created, handoffBranch: meta.branch };
|
|
169
|
-
if (meta.created) { const age = Date.now() - meta.created.getTime(); result.daysOld = age / 86400000; result.hoursOld = age / 3600000; }
|
|
170
|
-
if (!isGitRepo) { result.stalenessLevel = "UNKNOWN"; result.recommendation = "Not a git repo - unable to detect changes"; result.issues = ["Project is not a git repository"]; return result; }
|
|
171
|
-
const currentBranch = runCmd("git branch --show-current", projectPath).out;
|
|
172
|
-
result.currentBranch = currentBranch; result.branchMatches = meta.branch ? currentBranch === meta.branch : true;
|
|
173
|
-
const commits = getCommitsSince(meta.created, projectPath);
|
|
174
|
-
result.commitsSince = commits.length; result.recentCommits = commits.slice(0, 5);
|
|
175
|
-
const changedFiles = getChangedFilesSince(meta.created, projectPath);
|
|
176
|
-
result.filesChangedCount = changedFiles.length; result.filesChanged = changedFiles.slice(0, 10);
|
|
177
|
-
const refs = checkFilesExist(meta.modifiedFiles, projectPath);
|
|
178
|
-
result.referencedFilesExist = refs.existing.length; result.referencedFilesMissing = refs.missing;
|
|
179
|
-
const stale = calculateStaleness(result.daysOld || 0, result.commitsSince, result.filesChangedCount, result.branchMatches, refs.missing.length);
|
|
180
|
-
result.stalenessLevel = stale.level; result.recommendation = stale.recommendation; result.issues = stale.issues;
|
|
181
|
-
return result;
|
|
182
|
-
}
|
|
183
|
-
function printReport(result) {
|
|
184
|
-
if (result.error) return void console.log("Error: " + result.error);
|
|
185
|
-
console.log("\\n" + "=".repeat(60));
|
|
186
|
-
console.log("Handoff Staleness");
|
|
187
|
-
console.log("=".repeat(60));
|
|
188
|
-
console.log("File: " + result.handoffFile);
|
|
189
|
-
console.log("Project: " + result.projectPath);
|
|
190
|
-
if (result.created) {
|
|
191
|
-
console.log("Created: " + result.created.toISOString().replace("T", " " ).slice(0, 19));
|
|
192
|
-
if (result.daysOld != null) console.log(result.daysOld < 1 ? ("Age: " + result.hoursOld.toFixed(1) + "h") : ("Age: " + result.daysOld.toFixed(1) + "d"));
|
|
193
|
-
}
|
|
194
|
-
console.log("\\nStaleness: " + result.stalenessLevel);
|
|
195
|
-
console.log("Next: " + result.recommendation);
|
|
196
|
-
if (result.issues && result.issues.length) { console.log("\\nIssues:"); for (const issue of result.issues) console.log(" - " + issue); }
|
|
197
|
-
if (result.isGitRepo) {
|
|
198
|
-
console.log("\\nBranch: " + (result.currentBranch || "detached") + (result.branchMatches ? " (matches handoff)" : (" (handoff: " + result.handoffBranch + ")")));
|
|
199
|
-
console.log("Commits since handoff: " + result.commitsSince);
|
|
200
|
-
console.log("Files changed since handoff: " + result.filesChangedCount);
|
|
201
|
-
if (result.referencedFilesMissing.length) console.log("Missing referenced files: " + result.referencedFilesMissing.join(", "));
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
const args = process.argv.slice(2);
|
|
205
|
-
if (args.includes("--help") || args.includes("-h") || !args.length) {
|
|
206
|
-
console.log("Usage: node check_staleness.js <handoff-file>");
|
|
207
|
-
console.log(" node check_staleness.js .aikit-state/handoffs/<flow-slug>/file.md");
|
|
208
|
-
process.exit(!args.length ? 1 : 0);
|
|
209
|
-
}
|
|
210
|
-
const result = checkStaleness(args.find((arg) => !arg.startsWith("-")));
|
|
211
|
-
if (args.includes("--json")) console.log(JSON.stringify(result, null, 2));
|
|
212
|
-
else printReport(result);
|
|
213
|
-
`},{file:`scripts/create_handoff.js`,content:`#!/usr/bin/env node
|
|
214
|
-
const fs = require('node:fs');
|
|
215
|
-
const path = require('node:path');
|
|
216
|
-
const { execSync } = require('node:child_process');
|
|
217
|
-
const runCmd = (cmd, cwd) => {
|
|
218
|
-
try { return { ok: true, out: execSync(cmd, { cwd, timeout: 10000, encoding: "utf-8" }).trim() }; }
|
|
219
|
-
catch { return { ok: false, out: "" }; }
|
|
220
|
-
};
|
|
221
|
-
function getGitInfo(root) {
|
|
222
|
-
const info = { isGitRepo: false, branch: null, recentCommits: [], modifiedFiles: [], stagedFiles: [] };
|
|
223
|
-
if (!runCmd("git rev-parse --git-dir", root).ok) return info;
|
|
224
|
-
info.isGitRepo = true;
|
|
225
|
-
const branch = runCmd("git branch --show-current", root); if (branch.ok && branch.out) info.branch = branch.out;
|
|
226
|
-
const log = runCmd("git log --oneline -5 --no-decorate", root); if (log.ok && log.out) info.recentCommits = log.out.split("\\n");
|
|
227
|
-
const modified = runCmd("git diff --name-only", root); if (modified.ok && modified.out) info.modifiedFiles = modified.out.split("\\n");
|
|
228
|
-
const staged = runCmd("git diff --name-only --cached", root); if (staged.ok && staged.out) info.stagedFiles = staged.out.split("\\n");
|
|
229
|
-
return info;
|
|
230
|
-
}
|
|
231
|
-
function getActiveFlowSlug(root) {
|
|
232
|
-
const flowsDir = path.join(root, ".flows");
|
|
233
|
-
if (!fs.existsSync(flowsDir)) return null;
|
|
234
|
-
for (const name of fs.readdirSync(flowsDir)) {
|
|
235
|
-
const flowPath = path.join(flowsDir, name);
|
|
236
|
-
const metaPath = path.join(flowPath, "meta.json");
|
|
237
|
-
try {
|
|
238
|
-
if (!name.startsWith(".") && fs.statSync(flowPath).isDirectory() && fs.existsSync(metaPath)) {
|
|
239
|
-
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
240
|
-
if (meta.status === "active") return name;
|
|
241
|
-
}
|
|
242
|
-
} catch {}
|
|
243
|
-
}
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
function getHandoffsDir(root, flow) {
|
|
247
|
-
const active = flow || getActiveFlowSlug(root);
|
|
248
|
-
return path.join(root, ".aikit-state", "handoffs", active || "_standalone");
|
|
249
|
-
}
|
|
250
|
-
function listPrevious(root, flow) {
|
|
251
|
-
const baseDir = path.join(root, ".aikit-state", "handoffs");
|
|
252
|
-
const handoffs = [];
|
|
253
|
-
const collect = (dir, flowName) => {
|
|
254
|
-
if (!fs.existsSync(dir)) return;
|
|
255
|
-
for (const name of fs.readdirSync(dir)) {
|
|
256
|
-
if (!name.endsWith(".md")) continue;
|
|
257
|
-
const file = path.join(dir, name);
|
|
258
|
-
let title = name; let date = null;
|
|
259
|
-
try { const content = fs.readFileSync(file, "utf-8"); const match = content.match(/^#\\s+(?:Handoff:\\s*)?(.+)$/m); if (match) title = match[1].trim(); } catch {}
|
|
260
|
-
const dm = name.match(/^(\\d{4}-\\d{2}-\\d{2})-(\\d{6})/);
|
|
261
|
-
if (dm) { try { date = new Date(dm[1] + "T" + dm[2].slice(0,2) + ":" + dm[2].slice(2,4) + ":" + dm[2].slice(4,6)); } catch {} }
|
|
262
|
-
handoffs.push({ filename: name, path: file, title, date, flow: flowName });
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
if (!fs.existsSync(baseDir)) return handoffs;
|
|
266
|
-
const dirs = flow ? [flow] : fs.readdirSync(baseDir).filter((name) => { try { return fs.statSync(path.join(baseDir, name)).isDirectory(); } catch { return false; } });
|
|
267
|
-
for (const dir of dirs) collect(path.join(baseDir, dir), dir === "_standalone" ? null : dir);
|
|
268
|
-
handoffs.sort((a, b) => (b.date || 0) - (a.date || 0));
|
|
269
|
-
return handoffs;
|
|
270
|
-
}
|
|
271
|
-
const sanitizeSlug = (slug) => slug.toLowerCase().replace(/[\\s_]/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
272
|
-
function getPrevious(root, from, flow) {
|
|
273
|
-
const handoffs = listPrevious(root, flow);
|
|
274
|
-
if (from) {
|
|
275
|
-
const found = handoffs.find((item) => item.filename.includes(from));
|
|
276
|
-
return found ? { exists: true, filename: found.filename, title: found.title, flow: found.flow } : { exists: false };
|
|
277
|
-
}
|
|
278
|
-
return handoffs[0] ? { exists: true, filename: handoffs[0].filename, title: handoffs[0].title, flow: handoffs[0].flow } : { exists: false };
|
|
279
|
-
}
|
|
280
|
-
function generateHandoff(root, slug, from, flow) {
|
|
281
|
-
const now = new Date();
|
|
282
|
-
const stamp = now.toISOString().replace("T", " ").slice(0, 19);
|
|
283
|
-
const fileTs = now.toISOString().slice(0,10) + "-" + String(now.getHours()).padStart(2,"0") + String(now.getMinutes()).padStart(2,"0") + String(now.getSeconds()).padStart(2,"0");
|
|
284
|
-
const safeSlug = sanitizeSlug(slug || "handoff");
|
|
285
|
-
const filename = fileTs + "-" + safeSlug + ".md";
|
|
286
|
-
const activeFlow = flow || getActiveFlowSlug(root);
|
|
287
|
-
const dir = getHandoffsDir(root, flow);
|
|
288
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
289
|
-
const filepath = path.join(dir, filename);
|
|
290
|
-
const git = getGitInfo(root);
|
|
291
|
-
const prev = getPrevious(root, from, activeFlow);
|
|
292
|
-
const branch = git.branch || "[not a git repo or detached HEAD]";
|
|
293
|
-
const commits = git.recentCommits.length ? git.recentCommits.map((c) => " - " + c).join("\\n") : " - [none]";
|
|
294
|
-
const files = [...new Set([...git.modifiedFiles, ...git.stagedFiles])];
|
|
295
|
-
let modified = files.length ? files.slice(0, 10).map((f) => "| " + f + " | [changes] | [why] |").join("\\n") : "| [no modified files detected] | | |";
|
|
296
|
-
if (files.length > 10) modified += "\\n| ... and " + (files.length - 10) + " more files | | |";
|
|
297
|
-
let chain;
|
|
298
|
-
if (prev.exists) {
|
|
299
|
-
const currentFlow = activeFlow || "_standalone";
|
|
300
|
-
const prevFlow = prev.flow || "_standalone";
|
|
301
|
-
const prevPath = prevFlow === currentFlow ? "./" + prev.filename : "../" + prevFlow + "/" + prev.filename;
|
|
302
|
-
chain = ["## Handoff Chain", "", "- **Continues from**: [" + prev.filename + "](" + prevPath + ")", " - Previous title: " + (prev.title || "Unknown"), "- **Supersedes**: [list older handoffs replaced, or \\"None\\"]", "", "> Review previous handoff before filling this one."].join("\\n");
|
|
303
|
-
} else chain = ["## Handoff Chain", "", "- **Continues from**: None (fresh start)", "- **Supersedes**: None", "", "> First handoff for this task."].join("\\n");
|
|
304
|
-
const content = [
|
|
305
|
-
"# Handoff: [TASK_TITLE - replace this]",
|
|
306
|
-
"",
|
|
307
|
-
"## Session Metadata",
|
|
308
|
-
"- Created: " + stamp,
|
|
309
|
-
"- Project: " + root,
|
|
310
|
-
"- Branch: " + branch,
|
|
311
|
-
"- Session duration: [estimate]",
|
|
312
|
-
"",
|
|
313
|
-
"### Recent Commits",
|
|
314
|
-
commits,
|
|
315
|
-
"",
|
|
316
|
-
chain,
|
|
317
|
-
"",
|
|
318
|
-
"## Current State Summary",
|
|
319
|
-
"",
|
|
320
|
-
"[TODO: Summarize work, status, handoff point]",
|
|
321
|
-
"",
|
|
322
|
-
"## Codebase Understanding",
|
|
323
|
-
"",
|
|
324
|
-
"### Architecture Overview",
|
|
325
|
-
"",
|
|
326
|
-
"[TODO: Note key architecture insights]",
|
|
327
|
-
"",
|
|
328
|
-
"### Critical Files",
|
|
329
|
-
"",
|
|
330
|
-
"| File | Purpose | Relevance |",
|
|
331
|
-
"|------|---------|-----------|",
|
|
332
|
-
"| [TODO: Add critical files] | | |",
|
|
333
|
-
"",
|
|
334
|
-
"### Key Patterns Discovered",
|
|
335
|
-
"",
|
|
336
|
-
"[TODO: Note key patterns and conventions]",
|
|
337
|
-
"",
|
|
338
|
-
"## Work Completed",
|
|
339
|
-
"",
|
|
340
|
-
"### Tasks Finished",
|
|
341
|
-
"",
|
|
342
|
-
"- [ ] [TODO: List completed tasks]",
|
|
343
|
-
"",
|
|
344
|
-
"### Files Modified",
|
|
345
|
-
"",
|
|
346
|
-
"| File | Changes | Rationale |",
|
|
347
|
-
"|------|---------|-----------|",
|
|
348
|
-
modified,
|
|
349
|
-
"",
|
|
350
|
-
"### Decisions Made",
|
|
351
|
-
"",
|
|
352
|
-
"| Decision | Options Considered | Rationale |",
|
|
353
|
-
"|----------|-------------------|-----------|",
|
|
354
|
-
"| [TODO: Key decisions] | | |",
|
|
355
|
-
"",
|
|
356
|
-
"## Pending Work",
|
|
357
|
-
"",
|
|
358
|
-
"### Immediate Next Steps",
|
|
359
|
-
"",
|
|
360
|
-
"1. [TODO: First action]",
|
|
361
|
-
"2. [TODO: Second action]",
|
|
362
|
-
"3. [TODO: Third action]",
|
|
363
|
-
"",
|
|
364
|
-
"### Blockers/Open Questions",
|
|
365
|
-
"",
|
|
366
|
-
"- [ ] [TODO: Blockers or open questions]",
|
|
367
|
-
"",
|
|
368
|
-
"### Deferred Items",
|
|
369
|
-
"",
|
|
370
|
-
"- [TODO: Deferred items + why]",
|
|
371
|
-
"",
|
|
372
|
-
"## Context for Resuming Agent",
|
|
373
|
-
"",
|
|
374
|
-
"### Important Context",
|
|
375
|
-
"",
|
|
376
|
-
"[TODO: Critical context next agent MUST know]",
|
|
377
|
-
"",
|
|
378
|
-
"### Assumptions Made",
|
|
379
|
-
"",
|
|
380
|
-
"[TODO: Assumptions]",
|
|
381
|
-
"",
|
|
382
|
-
"### Potential Gotchas",
|
|
383
|
-
"",
|
|
384
|
-
"[TODO: Gotchas]",
|
|
385
|
-
"",
|
|
386
|
-
"## Environment State",
|
|
387
|
-
"",
|
|
388
|
-
"[TODO: Env vars, processes, DB state]",
|
|
389
|
-
"",
|
|
390
|
-
"## Related Resources",
|
|
391
|
-
"",
|
|
392
|
-
"[TODO: Docs, PRs, issues, threads]"
|
|
393
|
-
].join("\\n");
|
|
394
|
-
fs.writeFileSync(filepath, content, "utf-8");
|
|
395
|
-
const compact = [
|
|
396
|
-
"## Handoff: " + safeSlug,
|
|
397
|
-
"Branch: " + branch + " | Created: " + stamp,
|
|
398
|
-
"",
|
|
399
|
-
"### State",
|
|
400
|
-
"[TODO: 1-2 sentences — state + handoff point]",
|
|
401
|
-
"",
|
|
402
|
-
"### Decisions",
|
|
403
|
-
"- [TODO: key decisions + rationale]",
|
|
404
|
-
"",
|
|
405
|
-
"### Next Steps",
|
|
406
|
-
"1. [TODO: first action]",
|
|
407
|
-
"2. [TODO: second action]",
|
|
408
|
-
"",
|
|
409
|
-
"### Blockers",
|
|
410
|
-
"- [TODO: blockers]",
|
|
411
|
-
"",
|
|
412
|
-
"### Assumptions",
|
|
413
|
-
"- [TODO: assumptions that could change approach]"
|
|
414
|
-
].join("\\n");
|
|
415
|
-
return { filepath, compact };
|
|
416
|
-
}
|
|
417
|
-
const args = process.argv.slice(2);
|
|
418
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
419
|
-
console.log("Usage: node create_handoff.js [task-slug] [--continues-from <previous>] [--flow <flow-slug>]");
|
|
420
|
-
console.log(" node create_handoff.js --continues-from 2024-01-15-auth.md");
|
|
421
|
-
console.log(" node create_handoff.js \\"auth-part-2\\" --flow add-auth");
|
|
422
|
-
process.exit(0);
|
|
423
|
-
}
|
|
424
|
-
let slug = null; let from = null; let flow = null;
|
|
425
|
-
for (let i = 0; i < args.length; i++) {
|
|
426
|
-
if (args[i] === "--continues-from" && i + 1 < args.length) from = args[++i];
|
|
427
|
-
else if (args[i] === "--flow" && i + 1 < args.length) flow = sanitizeSlug(args[++i]);
|
|
428
|
-
else if (!args[i].startsWith("-")) slug = args[i];
|
|
429
|
-
}
|
|
430
|
-
const result = generateHandoff(process.cwd(), slug, from, flow);
|
|
431
|
-
console.log("Created handoff: " + result.filepath);
|
|
432
|
-
console.log("\\nCompact flow knowledge entry:");
|
|
433
|
-
console.log("---");
|
|
434
|
-
console.log(result.compact);
|
|
435
|
-
console.log("---");
|
|
436
|
-
console.log("\\nNext: Open file and fill all [TODO: ...] sections.");
|
|
437
|
-
console.log("Then validate: node scripts/validate_handoff.js " + result.filepath);
|
|
438
|
-
`},{file:`scripts/list_handoffs.js`,content:`#!/usr/bin/env node
|
|
439
|
-
const fs = require('node:fs');
|
|
440
|
-
const path = require('node:path');
|
|
441
|
-
function extractTitle(filepath) {
|
|
442
|
-
try {
|
|
443
|
-
const content = fs.readFileSync(filepath, "utf-8");
|
|
444
|
-
const match = content.match(/^#\\s+(?:Handoff:\\s*)?(.+)$/m);
|
|
445
|
-
if (match) { const title = match[1].trim(); return title.startsWith("[") && title.endsWith("]") ? "[Untitled - needs completion]" : (title.length > 50 ? title.slice(0, 50) + "..." : title); }
|
|
446
|
-
} catch {}
|
|
447
|
-
return "[Unable to read title]";
|
|
448
|
-
}
|
|
449
|
-
function checkCompletion(filepath) {
|
|
450
|
-
try {
|
|
451
|
-
const count = (fs.readFileSync(filepath, "utf-8").match(/\\[TODO:/g) || []).length;
|
|
452
|
-
if (count === 0) return "Complete";
|
|
453
|
-
if (count <= 3) return "In Progress (" + count + " TODOs)";
|
|
454
|
-
return "Needs Work (" + count + " TODOs)";
|
|
455
|
-
} catch { return "Unknown"; }
|
|
456
|
-
}
|
|
457
|
-
function parseDateFromFilename(filename) {
|
|
458
|
-
const match = filename.match(/^(\\d{4}-\\d{2}-\\d{2})-(\\d{6})/);
|
|
459
|
-
if (!match) return null;
|
|
460
|
-
try { return new Date(match[1] + "T" + match[2].slice(0,2) + ":" + match[2].slice(2,4) + ":" + match[2].slice(4,6)); } catch { return null; }
|
|
461
|
-
}
|
|
462
|
-
function listHandoffs(root) {
|
|
463
|
-
const handoffs = [];
|
|
464
|
-
const baseDir = path.join(root, ".aikit-state", "handoffs");
|
|
465
|
-
const collect = (dir, flow) => {
|
|
466
|
-
if (!fs.existsSync(dir)) return;
|
|
467
|
-
for (const name of fs.readdirSync(dir)) {
|
|
468
|
-
if (!name.endsWith(".md")) continue;
|
|
469
|
-
const fp = path.join(dir, name); const stat = fs.statSync(fp);
|
|
470
|
-
handoffs.push({ path: fp, filename: name, flow, title: extractTitle(fp), status: checkCompletion(fp), date: parseDateFromFilename(name), size: stat.size });
|
|
471
|
-
}
|
|
472
|
-
};
|
|
473
|
-
if (fs.existsSync(baseDir)) {
|
|
474
|
-
const flowDirs = fs.readdirSync(baseDir).filter((name) => { try { return fs.statSync(path.join(baseDir, name)).isDirectory(); } catch { return false; } }).sort((a, b) => { try { return fs.statSync(path.join(baseDir, b)).mtimeMs - fs.statSync(path.join(baseDir, a)).mtimeMs; } catch { return 0; } });
|
|
475
|
-
for (const flow of flowDirs) collect(path.join(baseDir, flow), flow === "_standalone" ? null : flow);
|
|
476
|
-
}
|
|
477
|
-
handoffs.sort((a, b) => (b.date || 0) - (a.date || 0));
|
|
478
|
-
return handoffs;
|
|
479
|
-
}
|
|
480
|
-
const formatDate = (dt) => dt ? dt.toISOString().replace("T", " ").slice(0, 16) : "Unknown date";
|
|
481
|
-
const args = process.argv.slice(2);
|
|
482
|
-
if (args.includes("--help") || args.includes("-h")) { console.log("Usage: node list_handoffs.js [project-path]"); process.exit(0); }
|
|
483
|
-
const projectPath = args[0] || process.cwd();
|
|
484
|
-
const handoffs = listHandoffs(projectPath);
|
|
485
|
-
if (!handoffs.length) {
|
|
486
|
-
console.log("No handoff documents found.");
|
|
487
|
-
console.log("Looked in: " + path.join(projectPath, ".aikit-state", "handoffs", "*"));
|
|
488
|
-
console.log(" " + path.join(projectPath, ".aikit-state", "handoffs", "_standalone"));
|
|
489
|
-
console.log("\\nCreate one with: node scripts/create_handoff.js [task-slug]");
|
|
490
|
-
process.exit(0);
|
|
491
|
-
}
|
|
492
|
-
console.log("\\nFound " + handoffs.length + " handoff(s) in " + projectPath + ":\\n");
|
|
493
|
-
console.log(String("Date").padEnd(18) + " " + String("Flow").padEnd(18) + " " + String("Status").padEnd(25) + " Title");
|
|
494
|
-
console.log("-".repeat(18) + " " + "-".repeat(18) + " " + "-".repeat(25) + " " + "-".repeat(40));
|
|
495
|
-
for (const handoff of handoffs) console.log(formatDate(handoff.date).padEnd(18) + " " + String(handoff.flow || "[standalone]").padEnd(18) + " " + handoff.status.padEnd(25) + " " + handoff.title);
|
|
496
|
-
if (args.includes("--json")) console.log("\\n" + JSON.stringify(handoffs, null, 2));
|
|
497
|
-
`},{file:`scripts/validate_handoff.js`,content:`#!/usr/bin/env node
|
|
498
|
-
const fs = require('node:fs');
|
|
499
|
-
const path = require('node:path');
|
|
500
|
-
const SECRET_PATTERNS = [
|
|
501
|
-
[/[\\"']?[a-zA-Z_]*api[_-]?key[\\"']?\\\\s*[:=]\\\\s*[\\"'][^\\"']{10,}[\\"']/gi, 'API key'],
|
|
502
|
-
[/[\\"']?[a-zA-Z_]*password[\\"']?\\\\s*[:=]\\\\s*[\\"'][^\\"']+[\\"']/gi, 'Password'],
|
|
503
|
-
[/[\\"']?[a-zA-Z_]*secret[\\"']?\\\\s*[:=]\\\\s*[\\"'][^\\"']{10,}[\\"']/gi, 'Secret'],
|
|
504
|
-
[/[\\"']?[a-zA-Z_]*token[\\"']?\\\\s*[:=]\\\\s*[\\"'][^\\"']{20,}[\\"']/gi, 'Token'],
|
|
505
|
-
[/[\\"']?[a-zA-Z_]*private[_-]?key[\\"']?\\\\s*[:=]/gi, 'Private key'],
|
|
506
|
-
[/-----BEGIN [A-Z]+ PRIVATE KEY-----/g, 'PEM key'],
|
|
507
|
-
[/Bearer\\\\s+[A-Za-z0-9_.-]+/g, 'Bearer token'],
|
|
508
|
-
[/ghp_[a-zA-Z0-9]{36}/g, 'GitHub PAT'],
|
|
509
|
-
[/sk-[a-zA-Z0-9]{48}/g, 'OpenAI key'],
|
|
510
|
-
[/xox[baprs]-[a-zA-Z0-9-]+/g, 'Slack token'],
|
|
511
|
-
];
|
|
512
|
-
const REQUIRED_SECTIONS = ['Current State Summary', 'Important Context', 'Immediate Next Steps'];
|
|
513
|
-
const RECOMMENDED_SECTIONS = ['Architecture Overview', 'Critical Files', 'Files Modified', 'Decisions Made', 'Assumptions Made', 'Potential Gotchas'];
|
|
514
|
-
const escapeRe = (value) => value.replace(/[.*+?^\${}()|[\\\\]\\\\]/g, '\\\\$&');
|
|
515
|
-
function checkTodos(content) { const todos = content.match(/\\\\[TODO:[^\\\\]]*\\\\]/g) || []; return { clear: todos.length === 0, todos }; }
|
|
516
|
-
function checkRequiredSections(content) {
|
|
517
|
-
const missing = [];
|
|
518
|
-
for (const section of REQUIRED_SECTIONS) {
|
|
519
|
-
const pattern = new RegExp('(?:^|\\\\n)##?\\\\s*' + escapeRe(section), 'i');
|
|
520
|
-
const match = pattern.exec(content);
|
|
521
|
-
if (!match) { missing.push(section + ' (missing)'); continue; }
|
|
522
|
-
const start = match.index + match[0].length;
|
|
523
|
-
const nextSection = content.slice(start).search(/\\\\n##?\\\\s+/);
|
|
524
|
-
const end = nextSection >= 0 ? start + nextSection : content.length;
|
|
525
|
-
const sectionContent = content.slice(start, end).trim();
|
|
526
|
-
if (sectionContent.length < 50 || sectionContent.includes('[TODO')) missing.push(section + ' (incomplete)');
|
|
527
|
-
}
|
|
528
|
-
return { complete: missing.length === 0, missing };
|
|
529
|
-
}
|
|
530
|
-
function checkRecommendedSections(content) {
|
|
531
|
-
const missing = [];
|
|
532
|
-
for (const section of RECOMMENDED_SECTIONS) {
|
|
533
|
-
const pattern = new RegExp('(?:^|\\\\n)##?\\\\s*' + escapeRe(section), 'i');
|
|
534
|
-
if (!pattern.test(content)) missing.push(section);
|
|
535
|
-
}
|
|
536
|
-
return missing;
|
|
537
|
-
}
|
|
538
|
-
function scanForSecrets(content) {
|
|
539
|
-
const findings = [];
|
|
540
|
-
for (const [pattern, description] of SECRET_PATTERNS) { const matches = content.match(pattern); if (matches) findings.push({ description, count: matches.length }); }
|
|
541
|
-
return findings;
|
|
542
|
-
}
|
|
543
|
-
function checkFileReferences(content, basePath) {
|
|
544
|
-
const patterns = [/\\\\|\\\\s*([A-Za-z0-9_./-]+\\\\.[a-zA-Z]+)\\\\s*\\\\|/g, /\`([A-Za-z0-9_./-]+\\\\.[a-zA-Z]+(?::\\\\d+)?)\`/g, /(?:^|\\\\s)([A-Za-z0-9_./-]+\\\\.[a-zA-Z]+:\\\\d+)/gm];
|
|
545
|
-
const foundFiles = new Set();
|
|
546
|
-
for (const pattern of patterns) {
|
|
547
|
-
for (const match of content.matchAll(pattern)) {
|
|
548
|
-
const filepath = match[1].split(':')[0];
|
|
549
|
-
if (filepath && !filepath.startsWith('http') && filepath.includes('/')) foundFiles.add(filepath);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
const existing = []; const missing = [];
|
|
553
|
-
for (const file of foundFiles) (fs.existsSync(path.join(basePath, file)) ? existing : missing).push(file);
|
|
554
|
-
return { existing, missing };
|
|
555
|
-
}
|
|
556
|
-
function calculateScore(todosClear, requiredComplete, missingRequired, missingRecommended, secretsFound, filesMissing) {
|
|
557
|
-
let score = 100;
|
|
558
|
-
if (!todosClear) score -= 30;
|
|
559
|
-
if (!requiredComplete) score -= 10 * missingRequired.length;
|
|
560
|
-
if (secretsFound.length) score -= 20;
|
|
561
|
-
if (filesMissing.length) score -= 5 * Math.min(filesMissing.length, 4);
|
|
562
|
-
score -= 2 * missingRecommended.length;
|
|
563
|
-
score = Math.max(0, score);
|
|
564
|
-
return { score, rating: score >= 90 ? 'Excellent' : score >= 70 ? 'Good' : score >= 50 ? 'Fair' : 'Poor' };
|
|
565
|
-
}
|
|
566
|
-
function resolveProjectRootFromHandoff(filepath) {
|
|
567
|
-
const handoffsDir = path.dirname(filepath);
|
|
568
|
-
const handoffMarkerDir = path.basename(path.dirname(handoffsDir));
|
|
569
|
-
const stateMarkerDir = path.basename(path.dirname(path.dirname(handoffsDir)));
|
|
570
|
-
return handoffMarkerDir === 'handoffs' && stateMarkerDir === '.aikit-state' ? path.resolve(handoffsDir, '..', '..', '..') : path.resolve(handoffsDir, '..');
|
|
571
|
-
}
|
|
572
|
-
function validateHandoff(filepath) {
|
|
573
|
-
if (!fs.existsSync(filepath)) return { error: 'File not found: ' + filepath };
|
|
574
|
-
const content = fs.readFileSync(filepath, 'utf-8');
|
|
575
|
-
const basePath = resolveProjectRootFromHandoff(filepath);
|
|
576
|
-
const { clear: todosClear, todos } = checkTodos(content);
|
|
577
|
-
const { complete: requiredComplete, missing: missingRequired } = checkRequiredSections(content);
|
|
578
|
-
const missingRecommended = checkRecommendedSections(content);
|
|
579
|
-
const secretsFound = scanForSecrets(content);
|
|
580
|
-
const refs = checkFileReferences(content, basePath);
|
|
581
|
-
const { score, rating } = calculateScore(todosClear, requiredComplete, missingRequired, missingRecommended, secretsFound, refs.missing);
|
|
582
|
-
return { filepath, score, rating, todosClear, remainingTodos: todos.slice(0, 5), todoCount: todos.length, requiredComplete, missingRequired, missingRecommended, secretsFound, filesVerified: refs.existing.length, filesMissing: refs.missing.slice(0, 5) };
|
|
583
|
-
}
|
|
584
|
-
function printReport(result) {
|
|
585
|
-
if (result.error) return void console.log('Error: ' + result.error);
|
|
586
|
-
console.log('\\n' + '='.repeat(60));
|
|
587
|
-
console.log('Handoff Validation');
|
|
588
|
-
console.log('='.repeat(60));
|
|
589
|
-
console.log('File: ' + result.filepath);
|
|
590
|
-
console.log('\\nScore: ' + result.score + '/100 - ' + result.rating);
|
|
591
|
-
console.log('='.repeat(60));
|
|
592
|
-
if (result.todosClear) console.log('\\n[PASS] No TODO placeholders remaining');
|
|
593
|
-
else { console.log('\\n[FAIL] ' + result.todoCount + ' TODO placeholders found:'); for (const todo of result.remainingTodos) console.log(' - ' + todo.slice(0, 50) + '...'); }
|
|
594
|
-
if (result.requiredComplete) console.log('\\n[PASS] All required sections complete');
|
|
595
|
-
else { console.log('\\n[FAIL] Missing/incomplete required sections:'); for (const section of result.missingRequired) console.log(' - ' + section); }
|
|
596
|
-
if (!result.secretsFound.length) console.log('\\n[PASS] No potential secrets detected');
|
|
597
|
-
else { console.log('\\n[WARN] Potential secrets detected:'); for (const finding of result.secretsFound) console.log(' - ' + finding.description + ' (' + finding.count + ' match(es))'); }
|
|
598
|
-
console.log('\\n[INFO] File refs: ' + result.filesVerified + ' verified, ' + result.filesMissing.length + ' missing');
|
|
599
|
-
for (const missingFile of result.filesMissing) console.log(' - ' + missingFile);
|
|
600
|
-
if (result.missingRecommended.length) { console.log('\\n[INFO] Missing recommended sections:'); for (const section of result.missingRecommended) console.log(' - ' + section); }
|
|
601
|
-
}
|
|
602
|
-
const args = process.argv.slice(2);
|
|
603
|
-
if (args.includes('--help') || args.includes('-h') || !args.length) {
|
|
604
|
-
console.log('Usage: node validate_handoff.js <handoff-file>');
|
|
605
|
-
console.log(' node validate_handoff.js .aikit-state/handoffs/<flow-slug>/file.md');
|
|
606
|
-
process.exit(!args.length ? 1 : 0);
|
|
607
|
-
}
|
|
608
|
-
const result = validateHandoff(args.find((arg) => !arg.startsWith('-')) || args[0]);
|
|
609
|
-
if (args.includes('--json')) console.log(JSON.stringify(result, null, 2));
|
|
610
|
-
else printReport(result);
|
|
611
|
-
`},{file:`SKILL.md`,content:`---
|
|
612
|
-
name: session-handoff
|
|
613
|
-
description: "Creates handoff docs for session transfer. Trigger on save-context, high context pressure, milestones, session end, or resume requests."
|
|
614
|
-
metadata:
|
|
615
|
-
category: cross-cutting
|
|
616
|
-
domain: general
|
|
617
|
-
applicability: always
|
|
618
|
-
inputs: [session-state, decisions]
|
|
619
|
-
outputs: [handoff-document, flow-knowledge-entry]
|
|
620
|
-
requires: [aikit]
|
|
621
|
-
relatedSkills: [lesson-learned]
|
|
622
|
-
---
|
|
623
|
-
# Session Handoff
|
|
624
|
-
Handoffs.
|
|
625
|
-
## Mindset
|
|
626
|
-
Keep only state, decisions + why, blockers, next step, gotchas.
|
|
627
|
-
## Quick Reference
|
|
628
|
-
- **CREATE** — save context near milestone, pause, session end, or high context pressure
|
|
629
|
-
- **RESUME** — continue prior work from handoff
|
|
630
|
-
- Full file: \`.aikit-state/handoffs/{flow-slug}/YYYY-MM-DD-HHMMSS-[slug].md\`
|
|
631
|
-
- Compact entry: \`knowledge({ action: "remember", scope: "flow", category: "session", title: "Session Handoff: <slug>", content })\`
|
|
632
|
-
- Fast-path: \`knowledge({ action: "withdraw", scope: "flow", profile: "implementer", budget: 6000 })\`
|
|
633
|
-
## NEVER
|
|
634
|
-
- **NEVER include full file contents in handoffs**
|
|
635
|
-
- **NEVER omit the "why" behind decisions**
|
|
636
|
-
- **NEVER create a handoff without blockers/next-steps**
|
|
637
|
-
- **NEVER hand off mid-failure** without error text and tried/untried paths
|
|
638
|
-
- **NEVER include conversation history**
|
|
639
|
-
- **NEVER create handoffs larger than 500 words**
|
|
640
|
-
- **NEVER skip architecture context**
|
|
641
|
-
## Handoff Quality Gate
|
|
642
|
-
Cold-start test: can new agent resume correctly on first attempt?
|
|
643
|
-
- [ ] **Actionable**
|
|
644
|
-
- [ ] **Self-contained**
|
|
645
|
-
- [ ] **Minimal**
|
|
646
|
-
- [ ] **Verified**
|
|
647
|
-
## CREATE Workflow
|
|
648
|
-
### 1. Choose storage target
|
|
649
|
-
- Flow-scoped: \`.aikit-state/handoffs/{flow-slug}/YYYY-MM-DD-HHMMSS-[slug].md\`
|
|
650
|
-
- Standalone: \`.aikit-state/handoffs/_standalone/YYYY-MM-DD-HHMMSS-[slug].md\`
|
|
651
|
-
### 2. Gather only critical-path facts
|
|
652
|
-
- Current state
|
|
653
|
-
- Key files + why they matter
|
|
654
|
-
- Decisions + why
|
|
655
|
-
- Verification evidence
|
|
656
|
-
- Blockers + tried/untried paths
|
|
657
|
-
- Ordered next steps
|
|
658
|
-
- \`stash({ action: "set", key: "handoff:<slug>:notes", value })\`
|
|
659
|
-
- \`knowledge({ action: "list", category: "decisions" })\` + \`search({ query })\`
|
|
660
|
-
### 3. Write the full handoff
|
|
661
|
-
\`\`\`md
|
|
662
|
-
# Handoff: <task>
|
|
663
|
-
## Current State
|
|
664
|
-
- Status:
|
|
665
|
-
- Last completed step:
|
|
666
|
-
- Current failure or pending task:
|
|
667
|
-
## Critical Files
|
|
668
|
-
- path/to/file - purpose + why it matters now
|
|
669
|
-
## Decisions
|
|
670
|
-
- Decision: why
|
|
671
|
-
- Evidence: file:line | test output | tool result
|
|
672
|
-
## Next Steps
|
|
673
|
-
1. First action
|
|
674
|
-
2. Second action
|
|
675
|
-
3. Third action
|
|
676
|
-
## Blockers
|
|
677
|
-
- Blocker:
|
|
678
|
-
- Tried:
|
|
679
|
-
- Not tried yet:
|
|
680
|
-
## Gotchas
|
|
681
|
-
- Non-obvious constraint or pattern
|
|
682
|
-
\`\`\`
|
|
683
|
-
Use [references/handoff-template.md](references/handoff-template.md) if needed.
|
|
684
|
-
### 4. Save compact flow knowledge
|
|
685
|
-
\`\`\`
|
|
686
|
-
knowledge({
|
|
687
|
-
action: "remember",
|
|
688
|
-
scope: "flow",
|
|
689
|
-
category: "session",
|
|
690
|
-
title: "Session Handoff: <slug>",
|
|
691
|
-
content: "State: ...
|
|
692
|
-
Decisions: ...
|
|
693
|
-
Next: ...
|
|
694
|
-
Blockers: ...
|
|
695
|
-
Evidence: ..."
|
|
696
|
-
})
|
|
697
|
-
\`\`\`
|
|
698
|
-
Target 1-2K chars.
|
|
699
|
-
### 5. Self-review before finalizing
|
|
700
|
-
- No secrets or file dumps
|
|
701
|
-
- Decisions include rationale
|
|
702
|
-
- Blockers include tried + untried paths
|
|
703
|
-
- Next step #1 is executable
|
|
704
|
-
- Quality Gate is 4/4
|
|
705
|
-
## RESUME Workflow
|
|
706
|
-
### 1. Pull compact context first
|
|
707
|
-
\`\`\`
|
|
708
|
-
knowledge({ action: "withdraw", scope: "flow", profile: "implementer", budget: 6000 })
|
|
709
|
-
\`\`\`
|
|
710
|
-
If entry is truncated or multiple handoffs exist, use:
|
|
711
|
-
\`\`\`
|
|
712
|
-
knowledge({ action: "list", category: "session", scope: "flow" })
|
|
713
|
-
knowledge({ action: "read", path: "<handoff-entry>" })
|
|
714
|
-
\`\`\`
|
|
715
|
-
### 2. Load the full handoff only when needed
|
|
716
|
-
Open markdown file in \`.aikit-state/handoffs/\` only if compact entry is not enough.
|
|
717
|
-
### 3. Verify live state before coding
|
|
718
|
-
- Branch still matches assumptions?
|
|
719
|
-
- Referenced files still exist?
|
|
720
|
-
- Blockers already resolved?
|
|
721
|
-
- Next steps still fit current diff and tests?
|
|
722
|
-
Use [references/resume-checklist.md](references/resume-checklist.md).
|
|
723
|
-
### 4. Resume from the first actionable step
|
|
724
|
-
Begin with Next Step #1. If reality differs, update handoff or create successor handoff.
|
|
725
|
-
## Chaining Rule
|
|
726
|
-
Create successor handoff instead of overwriting history.
|
|
727
|
-
## Output Expectations
|
|
728
|
-
Return status, file path, decisions, blocker/next step.
|
|
729
|
-
## Resources
|
|
730
|
-
- [references/handoff-template.md](references/handoff-template.md)
|
|
731
|
-
- [references/resume-checklist.md](references/resume-checklist.md)
|
|
732
|
-
`}];export{e as default};
|
|
1
|
+
const e=e=>e.join(`
|
|
2
|
+
`),t=[`const path = require('node:path');`,`const os = require('node:os');`,`const { createHash } = require('node:crypto');`,``,`function computePartitionKey(cwd) {`,` const absolutePath = path.resolve(cwd);`,` const baseName = path.basename(absolutePath).replace(/[^a-zA-Z0-9-]/g, '-') || 'workspace';`,` const hash = createHash('sha256').update(absolutePath).digest('hex').slice(0, 8);`,` return baseName + '-' + hash;`,`}`,``,`function getHandoffDir(cwd) {`,` return path.join(os.homedir(), '.aikit', 'workspaces', computePartitionKey(cwd), 'handoffs');`,`}`,``,`function isContainedPath(rootPath, targetPath) {`,` const relativePath = path.relative(rootPath, targetPath);`,` return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);`,`}`];var n=[{file:`references/handoff-template.md`,content:e(`# Handoff Template((Runtime path: ~/.aikit/workspaces/<workspace-hash>/handoffs/YYYY-MM-DD-HHMMSS-[slug].md(.flows/ stays workspace-local for flow state only.((## Current State(- Status:(- Last completed step:(- Current blocker:((## Decisions(- Decision:(- Why:(- Evidence:((## Next Steps(1.(2.(3.((## Blockers(- Blocker:(- Tried:(- Not tried:((## Gotchas(- Constraints, caveats, or follow-up notes.((Keep it compact, self-contained, and free of secrets.`.split(`(`))},{file:`references/resume-checklist.md`,content:e([`# Resume Checklist`,``,`- Open the latest handoff from ~/.aikit/workspaces/<workspace-hash>/handoffs/.`,`- Confirm branch and working tree.`,`- Review decisions, blockers, and gotchas.`,`- Resume from the first unfinished next step.`])},{file:`scripts/create_handoff.js`,content:e([`#!/usr/bin/env node`,`const fs = require('node:fs');`,...t,``,`function formatTimestamp(date) {`,` const iso = date.toISOString();`,` return iso.slice(0, 10) + '-' + iso.slice(11, 13) + iso.slice(14, 16) + iso.slice(17, 19);`,`}`,``,`function slugify(value) {`,` return String(value || 'handoff').toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '') || 'handoff';`,`}`,``,`const handoffDir = getHandoffDir(process.cwd());`,`fs.mkdirSync(handoffDir, { recursive: true });`,``,`const filePath = path.join(handoffDir, formatTimestamp(new Date()) + '-' + slugify(process.argv[2]) + '.md');`,`const content = [`,` '# Handoff',`,` '',`,` '## Current State',`,` '- Status: draft',`,` '- Last completed step:',`,` '- Current blocker:',`,` '',`,` '## Decisions',`,` '- Decision:',`,` '- Why:',`,` '- Evidence:',`,` '',`,` '## Next Steps',`,` '1.',`,` '2.',`,` '3.',`,` '',`,` '## Blockers',`,` '- Blocker:',`,` '- Tried:',`,` '- Not tried:',`,` '',`,` '## Gotchas',`,` '- Constraints, caveats, or follow-up notes.',`,`]).join('\\n');`,``,`fs.writeFileSync(filePath, content, 'utf8');`,`process.stdout.write(filePath + '\\n');`])},{file:`scripts/list_handoffs.js`,content:e([`#!/usr/bin/env node`,`const fs = require('node:fs');`,...t,``,`const handoffDir = getHandoffDir(process.cwd());`,``,`if (!fs.existsSync(handoffDir)) {`,` process.exit(0);`,`}`,``,`for (const entry of fs.readdirSync(handoffDir).filter((name) => name.endsWith('.md')).sort()) {`,` const filePath = path.join(handoffDir, entry);`,` const stat = fs.statSync(filePath);`,` process.stdout.write(entry + '\\t' + stat.size + '\\t' + stat.mtime.toISOString() + '\\n');`,`}`])},{file:`scripts/check_staleness.js`,content:e([`#!/usr/bin/env node`,`const fs = require('node:fs');`,...t,``,`const staleAfterDays = Number(process.argv[2] || 30);`,`const handoffDir = getHandoffDir(process.cwd());`,``,`if (!fs.existsSync(handoffDir)) {`,` process.stderr.write('Missing handoff dir: ' + handoffDir + '\\n');`,` process.exit(1);`,`}`,``,`const now = Date.now();`,``,`for (const entry of fs.readdirSync(handoffDir).filter((name) => name.endsWith('.md')).sort()) {`,` const filePath = path.join(handoffDir, entry);`,` const ageDays = (now - fs.statSync(filePath).mtimeMs) / 86400000;`,` if (ageDays > staleAfterDays) {`,` process.stdout.write(entry + '\\t' + ageDays.toFixed(1) + 'd\\n');`,` }`,`}`])},{file:`scripts/validate_handoff.js`,content:e([`#!/usr/bin/env node`,`const fs = require('node:fs');`,...t,``,`const REQUIRED_SECTIONS = [`,` '## Current State',`,` '## Decisions',`,` '## Next Steps',`,` '## Blockers',`,` '## Gotchas',`,`];`,``,`function isRuntimeHandoff(filePath) {`,` const absolutePath = path.resolve(filePath);`,` const runtimeRoot = path.join(os.homedir(), '.aikit', 'workspaces');`,``,` if (!isContainedPath(runtimeRoot, absolutePath)) {`,` return false;`,` }`,``,` const relativePath = path.relative(runtimeRoot, absolutePath);`,` const parts = relativePath.split(path.sep);`,` return parts.length >= 3 && parts[1] === 'handoffs';`,`}`,``,`function validate(filePath) {`,` const absolutePath = path.resolve(filePath);`,``,` if (!isRuntimeHandoff(absolutePath)) {`,` throw new Error('Handoff must live under ~/.aikit/workspaces/<workspace-hash>/handoffs/');`,` }`,``,` const content = fs.readFileSync(absolutePath, 'utf8');`,``,` for (const section of REQUIRED_SECTIONS) {`,` if (!content.includes(section)) {`,` throw new Error('Missing section: ' + section);`,` }`,` }`,`}`,``,`const filePath = process.argv[2];`,``,`if (!filePath) {`,` process.stderr.write('Usage: validate_handoff.js <handoff-file>\\n');`,` process.exit(1);`,`}`,``,`validate(filePath);`,`process.stdout.write('OK\\n');`])},{file:`SKILL.md`,content:e(`---(name: session-handoff(description: "Create and resume session handoff docs."(metadata:( category: cross-cutting( domain: general( applicability: always( inputs: [session-state, decisions]( outputs: [handoff-document, resume-checklist]( requires: [aikit]( relatedSkills: [lesson-learned](---(# Session Handoff((Use when work needs a durable pause point.((## Runtime Location(- Full handoff docs live at ~/.aikit/workspaces/<workspace-hash>/handoffs/YYYY-MM-DD-HHMMSS-[slug].md(- .flows/ stays workspace-local for flow state only.((## Create(- Capture state, decisions, next steps, blockers, and gotchas.(- Keep it compact, self-contained, and secret-free.(- Write the file to the runtime handoff directory.((## Resume(- Open the latest handoff for the current workspace.(- Verify branch and worktree.(- Continue from the first unfinished next step.`.split(`(`))}];export{n as default};
|