@vpxa/aikit 0.1.213 → 0.1.215

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.
@@ -1,1304 +1,616 @@
1
1
  var e=[{file:`references/handoff-template.md`,content:`# Handoff Template
2
-
3
- Use this template structure when creating handoff documents. The smart scaffold script will pre-fill metadata sections; complete the remaining sections based on session context.
4
-
2
+ Use this template.
5
3
  ## Table of Contents
6
-
7
- - [Session Metadata](#session-metadata)
8
- - [Current State Summary](#current-state-summary)
9
- - [Codebase Understanding](#codebase-understanding)
10
- - [Architecture Overview](#architecture-overview)
11
- - [Critical Files](#critical-files)
12
- - [Key Patterns Discovered](#key-patterns-discovered)
13
- - [Work Completed](#work-completed)
14
- - [Tasks Finished](#tasks-finished)
15
- - [Files Modified](#files-modified)
16
- - [Decisions Made](#decisions-made)
17
- - [Pending Work](#pending-work)
18
- - [Immediate Next Steps](#immediate-next-steps)
19
- - [Blockers/Open Questions](#blockersopen-questions)
20
- - [Deferred Items](#deferred-items)
21
- - [Context for Resuming Agent](#context-for-resuming-agent)
22
- - [Important Context](#important-context)
23
- - [Assumptions Made](#assumptions-made)
24
- - [Potential Gotchas](#potential-gotchas)
25
- - [Environment State](#environment-state)
26
- - [Related Resources](#related-resources)
27
- - [Template Usage Notes](#template-usage-notes)
28
-
4
+ Use headings
29
5
  ---
30
-
31
6
  # Handoff: [TASK_TITLE]
32
-
33
7
  ## Session Metadata
34
8
  - Created: [TIMESTAMP]
35
9
  - Project: [PROJECT_PATH]
36
10
  - Branch: [GIT_BRANCH]
37
11
  - Session duration: [APPROX_DURATION]
38
-
39
12
  ## Current State Summary
40
-
41
- [One paragraph: What was being worked on, current status, and where things left off]
42
-
13
+ [One paragraph: work, status, handoff point]
43
14
  ## Codebase Understanding
44
-
45
15
  ### Architecture Overview
46
-
47
- [Key architectural insights discovered during this session - how the system is structured, main components, data flow]
48
-
16
+ [Key architecture insights]
49
17
  ### Critical Files
50
-
51
18
  | File | Purpose | Relevance |
52
19
  |------|---------|-----------|
53
- | path/to/file | What this file does | Why it matters for this task |
54
-
20
+ | path/to/file | Purpose | Why it matters now |
55
21
  ### Key Patterns Discovered
56
-
57
- [Important patterns, conventions, or idioms found in this codebase that the next agent should follow]
58
-
22
+ [Patterns next agent should follow]
59
23
  ## Work Completed
60
-
61
24
  ### Tasks Finished
62
-
63
- - [x] Task 1 - brief description of what was done
25
+ - [x] Task 1 - what was done
64
26
  - [x] Task 2 - brief description
65
-
66
27
  ### Files Modified
67
-
68
28
  | File | Changes | Rationale |
69
29
  |------|---------|-----------|
70
- | path/to/file | Description of changes | Why this change was made |
71
-
30
+ | path/to/file | Changes | Why |
72
31
  ### Decisions Made
73
-
74
32
  | Decision | Options Considered | Rationale |
75
33
  |----------|-------------------|-----------|
76
- | Chose X over Y | X, Y, Z | Why X was chosen |
77
-
34
+ | Chose X over Y | X, Y, Z | Why |
78
35
  ## Pending Work
79
-
80
36
  ### Immediate Next Steps
81
-
82
- 1. [Most critical next action - what to do first]
83
- 2. [Second priority]
84
- 3. [Third priority]
85
-
37
+ 1. [First action]
38
+ 2. [Second action]
39
+ 3. [Third action]
86
40
  ### Blockers/Open Questions
87
-
88
- - [ ] Blocker: [description] - Needs: [what's required to unblock]
89
- - [ ] Question: [unclear aspect] - Suggested: [potential resolution]
90
-
41
+ - [ ] Blocker: [description] - Needs: [unblocker]
42
+ - [ ] Question: [unclear aspect] - Suggested: [resolution]
91
43
  ### Deferred Items
92
-
93
- - Item 1 (deferred because: [reason, e.g., out of scope, needs user input])
94
-
44
+ - Item 1 (deferred: [reason])
95
45
  ## Context for Resuming Agent
96
-
97
46
  ### Important Context
98
-
99
- [Critical information the next agent MUST know to continue effectively - this is the most important section for handoff]
100
-
47
+ [Critical information next agent MUST know]
101
48
  ### Assumptions Made
102
-
103
- - Assumption 1: [what was assumed to be true]
49
+ - Assumption 1: [assumed true]
104
50
  - Assumption 2: [another assumption]
105
-
106
51
  ### Potential Gotchas
107
-
108
- - [Things that might trip up a new agent - edge cases, quirks, non-obvious behavior]
109
-
52
+ - [Things that might trip up next agent]
110
53
  ## Environment State
111
-
112
54
  ### Tools/Services Used
113
-
114
- - [Tool/Service]: [relevant configuration or state]
115
-
55
+ - [Tool/Service]: [relevant state]
116
56
  ### Active Processes
117
-
118
- - [Any background processes, dev servers, watchers that may be running]
119
-
57
+ - [Background processes, dev servers, watchers]
120
58
  ### Environment Variables
121
-
122
- - [Key env vars that matter for this work - DO NOT include secrets/values, just names]
123
-
59
+ - [Env vars that matter - names only, no values]
124
60
  ## Related Resources
125
-
126
- - [Link to relevant documentation]
61
+ - [Relevant docs]
127
62
  - [Related file paths]
128
63
  - [External resources consulted]
129
-
130
64
  ---
131
-
132
65
  ## Template Usage Notes
133
-
134
- When filling this template:
135
- 1. Be specific and concrete - vague descriptions don't help the next agent
136
- 2. Include file paths with line numbers where relevant (e.g., \`src/auth.ts:142\`)
137
- 3. Prioritize the "Important Context" and "Immediate Next Steps" sections
138
- 4. Don't include sensitive data (API keys, passwords, tokens)
139
- 5. Focus on WHAT and WHY, not just WHAT - rationale is crucial for handoffs
66
+ Be specific. Use file refs when useful. Exclude secrets. Capture WHAT and WHY.
140
67
  `},{file:`references/resume-checklist.md`,content:`# Resume Checklist
141
-
142
- Follow this checklist when resuming work from a handoff document to ensure zero-ambiguity continuation.
143
-
68
+ On resume.
144
69
  ## Pre-Resume Verification
145
-
146
- - [ ] Read the entire handoff document before taking any action
147
- - [ ] Verify you are in the correct project directory
148
- - [ ] Confirm the git branch matches (or understand why it might differ)
149
- - [ ] Check the handoff timestamp - how stale is this context?
150
-
70
+ - [ ] Read handoff before acting
71
+ - [ ] Verify project directory
72
+ - [ ] Confirm branch matches or note why it differs
73
+ - [ ] Check handoff timestamp
151
74
  ## Context Validation
152
-
153
- - [ ] Review "Important Context" section thoroughly
154
- - [ ] Understand all assumptions listed - are they still valid?
155
- - [ ] Check if any blockers have been resolved since handoff
156
- - [ ] Review "Potential Gotchas" to avoid known pitfalls
157
-
75
+ - [ ] Review important context
76
+ - [ ] Check assumptions
77
+ - [ ] Check resolved blockers
78
+ - [ ] Review gotchas
158
79
  ## State Verification
159
-
160
- - [ ] Run \`git status\` to see current file state
161
- - [ ] Compare modified files list in handoff vs current state
162
- - [ ] Check if any environment variables need to be set
163
- - [ ] Verify any required services/processes are running
164
-
80
+ - [ ] Run \`git status\`
81
+ - [ ] Compare modified files vs current state
82
+ - [ ] Check required env vars
83
+ - [ ] Verify required services/processes
165
84
  ## Resume Execution
166
-
167
- - [ ] Start with "Immediate Next Steps" item #1
168
- - [ ] Reference "Files Modified" table for context on recent changes
169
- - [ ] Apply patterns documented in "Key Patterns Discovered"
170
- - [ ] Follow architectural insights from "Architecture Overview"
171
-
85
+ - [ ] Start with next step #1
86
+ - [ ] Apply documented patterns
172
87
  ## During Work
173
-
174
- - [ ] Update handoff document if major new context is discovered
175
- - [ ] Mark completed items in "Pending Work" as you finish them
176
- - [ ] Add new blockers/questions as they arise
177
- - [ ] Consider creating a new handoff if session becomes long
178
-
88
+ - [ ] Update handoff if major context changes
89
+ - [ ] Add new blockers/questions
179
90
  ## Red Flags - Stop and Verify
180
-
181
- If you encounter any of these, pause and verify context before proceeding:
182
-
183
- 1. **Files mentioned in handoff don't exist** - codebase may have changed significantly
184
- 2. **Branch has diverged substantially** - check git log for recent commits
185
- 3. **Assumptions are clearly invalid** - reassess the approach
186
- 4. **Blockers marked as unresolved are now blocking you** - escalate to user
187
- 5. **Architecture has changed** - re-explore before continuing
188
-
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
189
96
  ## Quick Start Commands
190
-
191
- After reading the handoff, these commands help verify state:
192
-
193
97
  \`\`\`bash
194
- # Check current branch and status
195
98
  git branch --show-current
196
99
  git status
197
-
198
- # See recent commits (compare with handoff)
199
100
  git log --oneline -10
200
-
201
- # Check for any running processes mentioned
202
101
  ps aux | grep [process-name]
203
-
204
- # Verify environment
205
102
  env | grep [relevant-var]
206
103
  \`\`\`
207
-
208
104
  ## Handoff Quality Assessment
209
-
210
- Rate the handoff quality to identify if more exploration is needed:
211
-
212
- | Aspect | Good | Needs Exploration |
213
- |--------|------|-------------------|
214
- | Next steps | Clear, actionable | Vague or missing |
215
- | File references | Specific paths/lines | General descriptions |
216
- | Decisions | Rationale included | Just outcomes |
217
- | Context | Complete picture | Gaps or assumptions |
218
-
219
- If multiple aspects "Need Exploration", spend time re-exploring the codebase before continuing implementation.
105
+ Re-explore if weak.
220
106
  `},{file:`scripts/check_staleness.js`,content:`#!/usr/bin/env node
221
- /**
222
- * Check staleness of a handoff document compared to current project state.
223
- *
224
- * Analyzes:
225
- * - Time since handoff was created
226
- * - Git commits since handoff
227
- * - Files that changed since handoff
228
- * - Branch divergence
229
- * - Modified files status
230
- *
231
- * Usage:
232
- * node check_staleness.js <handoff-file>
233
- * node check_staleness.js .aikit-state/handoffs/add-auth/2024-01-15-143022-auth.md
234
- * node check_staleness.js .aikit-state/handoffs/_standalone/2024-01-15-143022-auth.md
235
- */
236
-
237
107
  const fs = require('node:fs');
238
108
  const path = require('node:path');
239
109
  const { execSync } = require('node:child_process');
240
-
241
- function _die(msg) {
242
- process.stderr.write(\`\${msg}\\n\`);
243
- process.exit(1);
244
- }
245
-
246
- function runCmd(cmd, cwd) {
247
- try {
248
- return { ok: true, out: execSync(cmd, { cwd, timeout: 10_000, encoding: 'utf-8' }).trim() };
249
- } catch {
250
- return { ok: false, out: '' };
251
- }
252
- }
253
-
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
+ };
254
114
  function parseHandoffMetadata(filepath) {
255
- const content = fs.readFileSync(filepath, 'utf-8');
256
- const meta = { created: null, branch: null, projectPath: null, modifiedFiles: [] };
257
-
258
- const createdMatch = content.match(/Created:\\s*(\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}:\\d{2})/);
259
- if (createdMatch) meta.created = new Date(createdMatch[1].replace(' ', 'T'));
260
-
261
- const branchMatch = content.match(/Branch:\\s*(\\S+)/);
262
- if (branchMatch && !branchMatch[1].startsWith('[')) meta.branch = branchMatch[1];
263
-
264
- const projMatch = content.match(/Project:\\s*(.+?)(?:\\n|$)/);
265
- if (projMatch) meta.projectPath = projMatch[1].trim();
266
-
267
- const fileMatches = content.matchAll(/\\|\\s*([a-zA-Z0-9_\\-./]+\\.[a-zA-Z]+)\\s*\\|/g);
268
- for (const m of fileMatches) {
269
- if (m[1].includes('/') && !m[1].startsWith('[')) meta.modifiedFiles.push(m[1]);
270
- }
271
-
272
- return meta;
273
- }
274
-
275
- function getCommitsSince(timestamp, cwd) {
276
- if (!timestamp) return [];
277
- const iso = timestamp.toISOString();
278
- const { ok, out } = runCmd(\`git log --since="\${iso}" --oneline --no-decorate\`, cwd);
279
- return ok && out ? out.split('\\n') : [];
280
- }
281
-
282
- function getChangedFilesSince(timestamp, cwd) {
283
- if (!timestamp) return [];
284
- const iso = timestamp.toISOString();
285
- const { ok, out } = runCmd(\`git log --since="\${iso}" --name-only --pretty=format:\`, cwd);
286
- if (ok && out) {
287
- return [
288
- ...new Set(
289
- out
290
- .split('\\n')
291
- .map((f) => f.trim())
292
- .filter(Boolean),
293
- ),
294
- ];
295
- }
296
- return [];
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))] : [];
297
133
  }
298
-
299
134
  function checkFilesExist(files, cwd) {
300
- const existing = [];
301
- const missing = [];
302
- for (const f of files) {
303
- if (fs.existsSync(path.join(cwd, f))) existing.push(f);
304
- else missing.push(f);
305
- }
306
- return { existing, missing };
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 };
307
138
  }
308
-
309
139
  function calculateStaleness(daysOld, commitsSince, filesChanged, branchMatches, filesMissing) {
310
- const issues = [];
311
- let score = 0;
312
-
313
- if (daysOld > 30) {
314
- score += 3;
315
- issues.push(\`Handoff is \${Math.floor(daysOld)} days old\`);
316
- } else if (daysOld > 7) {
317
- score += 2;
318
- issues.push(\`Handoff is \${Math.floor(daysOld)} days old\`);
319
- } else if (daysOld > 1) {
320
- score += 1;
321
- }
322
-
323
- if (commitsSince > 50) {
324
- score += 3;
325
- issues.push(\`\${commitsSince} commits since handoff - significant changes\`);
326
- } else if (commitsSince > 20) {
327
- score += 2;
328
- issues.push(\`\${commitsSince} commits since handoff\`);
329
- } else if (commitsSince > 5) {
330
- score += 1;
331
- }
332
-
333
- if (!branchMatches) {
334
- score += 2;
335
- issues.push('Current branch differs from handoff branch');
336
- }
337
-
338
- if (filesMissing > 5) {
339
- score += 2;
340
- issues.push(\`\${filesMissing} referenced files no longer exist\`);
341
- } else if (filesMissing > 0) {
342
- score += 1;
343
- issues.push(\`\${filesMissing} referenced file(s) missing\`);
344
- }
345
-
346
- if (filesChanged > 20) {
347
- score += 2;
348
- issues.push(\`\${filesChanged} files changed since handoff\`);
349
- } else if (filesChanged > 5) {
350
- score += 1;
351
- }
352
-
353
- let level, recommendation;
354
- if (score === 0) {
355
- level = 'FRESH';
356
- recommendation = 'Safe to resume - minimal changes since handoff';
357
- } else if (score <= 2) {
358
- level = 'SLIGHTLY_STALE';
359
- recommendation = 'Generally safe to resume - review changes before continuing';
360
- } else if (score <= 4) {
361
- level = 'STALE';
362
- recommendation = 'Proceed with caution - significant changes may affect context';
363
- } else {
364
- level = 'VERY_STALE';
365
- recommendation = 'Consider creating new handoff - too many changes since original';
366
- }
367
-
368
- return { level, recommendation, issues };
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 };
369
156
  }
370
-
371
157
  function resolveProjectRootFromHandoff(handoffPath) {
372
- const handoffsDir = path.dirname(handoffPath);
373
- const handoffMarkerDir = path.basename(path.dirname(handoffsDir));
374
- const stateMarkerDir = path.basename(path.dirname(path.dirname(handoffsDir)));
375
- const isFlowScoped = handoffMarkerDir === 'handoffs' && stateMarkerDir === '.aikit-state';
376
-
377
- return isFlowScoped ? path.resolve(handoffsDir, '..', '..', '..') : path.resolve(handoffsDir, '..');
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, "..");
378
162
  }
379
-
380
163
  function checkStaleness(handoffPath) {
381
- if (!fs.existsSync(handoffPath)) return { error: \`Handoff file not found: \${handoffPath}\` };
382
-
383
- const meta = parseHandoffMetadata(handoffPath);
384
- const projectPath =
385
- meta.projectPath && fs.existsSync(meta.projectPath)
386
- ? meta.projectPath
387
- : resolveProjectRootFromHandoff(handoffPath);
388
-
389
- const isGitRepo = runCmd('git rev-parse --git-dir', projectPath).ok;
390
-
391
- const result = {
392
- handoffFile: handoffPath,
393
- projectPath,
394
- isGitRepo,
395
- created: meta.created,
396
- handoffBranch: meta.branch,
397
- };
398
-
399
- if (meta.created) {
400
- const age = Date.now() - meta.created.getTime();
401
- result.daysOld = age / 86_400_000;
402
- result.hoursOld = age / 3_600_000;
403
- }
404
-
405
- if (isGitRepo) {
406
- const { out: currentBranch } = runCmd('git branch --show-current', projectPath);
407
- result.currentBranch = currentBranch;
408
- result.branchMatches = meta.branch ? currentBranch === meta.branch : true;
409
-
410
- const commits = getCommitsSince(meta.created, projectPath);
411
- result.commitsSince = commits.length;
412
- result.recentCommits = commits.slice(0, 5);
413
-
414
- const changedFiles = getChangedFilesSince(meta.created, projectPath);
415
- result.filesChangedCount = changedFiles.length;
416
- result.filesChanged = changedFiles.slice(0, 10);
417
-
418
- const { existing, missing } = checkFilesExist(meta.modifiedFiles, projectPath);
419
- result.referencedFilesExist = existing.length;
420
- result.referencedFilesMissing = missing;
421
-
422
- const { level, recommendation, issues } = calculateStaleness(
423
- result.daysOld || 0,
424
- result.commitsSince,
425
- result.filesChangedCount,
426
- result.branchMatches,
427
- missing.length,
428
- );
429
- result.stalenessLevel = level;
430
- result.recommendation = recommendation;
431
- result.issues = issues;
432
- } else {
433
- result.stalenessLevel = 'UNKNOWN';
434
- result.recommendation = 'Not a git repo - unable to detect changes';
435
- result.issues = ['Project is not a git repository'];
436
- }
437
-
438
- return result;
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;
439
182
  }
440
-
441
183
  function printReport(result) {
442
- if (result.error) {
443
- console.log(\`Error: \${result.error}\`);
444
- return;
445
- }
446
-
447
- console.log(\`\\n\${'='.repeat(60)}\`);
448
- console.log('Handoff Staleness Report');
449
- console.log('='.repeat(60));
450
- console.log(\`File: \${result.handoffFile}\`);
451
- console.log(\`Project: \${result.projectPath}\`);
452
-
453
- if (result.created) {
454
- console.log(\`Created: \${result.created.toISOString().replace('T', ' ').slice(0, 19)}\`);
455
- if (result.daysOld != null) {
456
- console.log(
457
- result.daysOld < 1
458
- ? \`Age: \${result.hoursOld.toFixed(1)} hours\`
459
- : \`Age: \${result.daysOld.toFixed(1)} days\`,
460
- );
461
- }
462
- }
463
-
464
- console.log(\`\\nStaleness: \${result.stalenessLevel}\`);
465
- console.log(\`Recommendation: \${result.recommendation}\`);
466
-
467
- if (result.issues && result.issues.length > 0) {
468
- console.log('\\nIssues:');
469
- for (const issue of result.issues) console.log(\` - \${issue}\`);
470
- }
471
-
472
- if (result.isGitRepo) {
473
- console.log(
474
- \`\\nBranch: \${result.currentBranch || 'detached'}\${result.branchMatches ? ' (matches handoff)' : \` (handoff: \${result.handoffBranch})\`}\`,
475
- );
476
- console.log(\`Commits since handoff: \${result.commitsSince}\`);
477
- console.log(\`Files changed since handoff: \${result.filesChangedCount}\`);
478
- if (result.referencedFilesMissing.length > 0) {
479
- console.log(\`Missing referenced files: \${result.referencedFilesMissing.join(', ')}\`);
480
- }
481
- }
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(", "));
482
202
  }
483
-
484
- // --- Main ---
485
- const args = process.argv.slice(2);
486
- if (args.includes('--help') || args.includes('-h') || args.length === 0) {
487
- console.log('Usage: node check_staleness.js <handoff-file>');
488
- console.log(' node check_staleness.js .aikit-state/handoffs/<flow-slug>/2024-01-15-143022-auth.md');
489
- console.log(' node check_staleness.js .aikit-state/handoffs/_standalone/2024-01-15-143022-auth.md');
490
- process.exit(args.length === 0 ? 1 : 0);
491
- }
492
-
493
- if (args.includes('--json')) {
494
- const result = checkStaleness(args.find((a) => !a.startsWith('-')));
495
- console.log(JSON.stringify(result, null, 2));
496
- } else {
497
- const result = checkStaleness(args[0]);
498
- printReport(result);
499
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);
500
213
  `},{file:`scripts/create_handoff.js`,content:`#!/usr/bin/env node
501
- /**
502
- * Smart scaffold generator for handoff documents.
503
- *
504
- * Creates a new handoff document with auto-detected metadata:
505
- * - Current timestamp
506
- * - Project path
507
- * - Git branch (if available)
508
- * - Recent git log
509
- * - Modified/staged files
510
- * - Handoff chain linking
511
- *
512
- * Usage:
513
- * node create_handoff.js [task-slug]
514
- * node create_handoff.js "implementing-auth"
515
- * node create_handoff.js "auth-part-2" --continues-from 2024-01-15-auth.md
516
- * node create_handoff.js "auth-part-2" --flow add-auth
517
- * node create_handoff.js # auto-generates slug from timestamp
518
- */
519
-
520
214
  const fs = require('node:fs');
521
215
  const path = require('node:path');
522
216
  const { execSync } = require('node:child_process');
523
-
524
- function _die(msg) {
525
- process.stderr.write(\`\${msg}\\n\`);
526
- process.exit(1);
527
- }
528
-
529
- function runCmd(cmd, cwd) {
530
- try {
531
- return { ok: true, out: execSync(cmd, { cwd, timeout: 10_000, encoding: 'utf-8' }).trim() };
532
- } catch {
533
- return { ok: false, out: '' };
534
- }
535
- }
536
-
537
- function getGitInfo(projectPath) {
538
- const info = {
539
- isGitRepo: false,
540
- branch: null,
541
- recentCommits: [],
542
- modifiedFiles: [],
543
- stagedFiles: [],
544
- };
545
-
546
- if (!runCmd('git rev-parse --git-dir', projectPath).ok) return info;
547
- info.isGitRepo = true;
548
-
549
- const { ok: bOk, out: branch } = runCmd('git branch --show-current', projectPath);
550
- if (bOk && branch) info.branch = branch;
551
-
552
- const { ok: lOk, out: log } = runCmd('git log --oneline -5 --no-decorate', projectPath);
553
- if (lOk && log) info.recentCommits = log.split('\\n');
554
-
555
- const { ok: mOk, out: modified } = runCmd('git diff --name-only', projectPath);
556
- if (mOk && modified) info.modifiedFiles = modified.split('\\n');
557
-
558
- const { ok: sOk, out: staged } = runCmd('git diff --name-only --cached', projectPath);
559
- if (sOk && staged) info.stagedFiles = staged.split('\\n');
560
-
561
- return info;
562
- }
563
-
564
- function getFlowDirectories(projectPath) {
565
- const flowsDir = path.join(projectPath, '.flows');
566
- if (!fs.existsSync(flowsDir)) return [];
567
-
568
- return fs
569
- .readdirSync(flowsDir)
570
- .map((name) => ({ name, path: path.join(flowsDir, name) }))
571
- .filter(({ name, path: flowPath }) => {
572
- try {
573
- return !name.startsWith('.') && fs.statSync(flowPath).isDirectory();
574
- } catch {
575
- return false;
576
- }
577
- })
578
- .sort((a, b) => {
579
- try {
580
- return fs.statSync(b.path).mtimeMs - fs.statSync(a.path).mtimeMs;
581
- } catch {
582
- return 0;
583
- }
584
- });
585
- }
586
-
587
- function getActiveFlowSlug(projectPath) {
588
- const flowDirs = getFlowDirectories(projectPath);
589
- if (flowDirs.length === 0) return null;
590
-
591
- const activeFlow = flowDirs.find(({ path: flowPath }) => {
592
- const metaPath = path.join(flowPath, 'meta.json');
593
- if (!fs.existsSync(metaPath)) return false;
594
-
595
- try {
596
- const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
597
- return meta.status === 'active';
598
- } catch {
599
- return false;
600
- }
601
- });
602
-
603
- return activeFlow ? activeFlow.name : null;
604
- }
605
-
606
- function getHandoffsDir(projectPath, flowSlug) {
607
- if (flowSlug) return path.join(projectPath, '.aikit-state', 'handoffs', flowSlug);
608
-
609
- const activeFlowSlug = getActiveFlowSlug(projectPath);
610
- if (activeFlowSlug) return path.join(projectPath, '.aikit-state', 'handoffs', activeFlowSlug);
611
-
612
- return path.join(projectPath, '.aikit-state', 'handoffs', '_standalone');
613
- }
614
-
615
- function findPreviousHandoffs(projectPath, flowSlug) {
616
- const handoffs = [];
617
- const baseDir = path.join(projectPath, '.aikit-state', 'handoffs');
618
-
619
- function collectFromDir(dir, flow) {
620
- if (!fs.existsSync(dir)) return;
621
-
622
- for (const name of fs.readdirSync(dir)) {
623
- if (!name.endsWith('.md')) continue;
624
- const fp = path.join(dir, name);
625
-
626
- let title = name;
627
- try {
628
- const content = fs.readFileSync(fp, 'utf-8');
629
- const m = content.match(/^#\\s+(?:Handoff:\\s*)?(.+)$/m);
630
- if (m) title = m[1].trim();
631
- } catch {
632
- /* ignore */
633
- }
634
-
635
- let date = null;
636
- const dm = name.match(/^(\\d{4}-\\d{2}-\\d{2})-(\\d{6})/);
637
- if (dm) {
638
- try {
639
- date = new Date(\`\${dm[1]}T\${dm[2].slice(0, 2)}:\${dm[2].slice(2, 4)}:\${dm[2].slice(4, 6)}\`);
640
- } catch {
641
- /* ignore */
642
- }
643
- }
644
-
645
- handoffs.push({ filename: name, path: fp, title, date, flow });
646
- }
647
- }
648
-
649
- if (!fs.existsSync(baseDir)) return handoffs;
650
-
651
- const flowDirs = flowSlug
652
- ? [flowSlug]
653
- : fs.readdirSync(baseDir).filter((name) => {
654
- try {
655
- return fs.statSync(path.join(baseDir, name)).isDirectory();
656
- } catch {
657
- return false;
658
- }
659
- });
660
-
661
- for (const fd of flowDirs) {
662
- collectFromDir(path.join(baseDir, fd), fd === '_standalone' ? null : fd);
663
- }
664
-
665
- handoffs.sort((a, b) => (b.date || 0) - (a.date || 0));
666
- return handoffs;
667
- }
668
-
669
- function getPreviousHandoffInfo(projectPath, continuesFrom, flowSlug) {
670
- const handoffs = findPreviousHandoffs(projectPath, flowSlug);
671
-
672
- if (continuesFrom) {
673
- const found = handoffs.find((h) => h.filename.includes(continuesFrom));
674
- return found
675
- ? { exists: true, filename: found.filename, title: found.title, flow: found.flow }
676
- : { exists: false, filename: continuesFrom, title: 'Not found' };
677
- }
678
-
679
- if (handoffs.length > 0) {
680
- return {
681
- exists: true,
682
- filename: handoffs[0].filename,
683
- title: handoffs[0].title,
684
- flow: handoffs[0].flow,
685
- suggested: true,
686
- };
687
- }
688
-
689
- return { exists: false };
690
- }
691
-
692
- function sanitizeSlug(slug) {
693
- return slug
694
- .toLowerCase()
695
- .replace(/[\\s_]/g, '-')
696
- .replace(/[^a-z0-9-]/g, '');
697
- }
698
-
699
- function generateHandoff(projectPath, slug, continuesFrom, flowSlug) {
700
- const now = new Date();
701
- const timestamp = now.toISOString().replace('T', ' ').slice(0, 19);
702
- 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')}\`;
703
-
704
- if (!slug) slug = 'handoff';
705
- slug = sanitizeSlug(slug);
706
- const filename = \`\${fileTs}-\${slug}.md\`;
707
-
708
- const resolvedFlowSlug = flowSlug || getActiveFlowSlug(projectPath);
709
- const handoffsDir = getHandoffsDir(projectPath, flowSlug);
710
- fs.mkdirSync(handoffsDir, { recursive: true });
711
- const filepath = path.join(handoffsDir, filename);
712
-
713
- const git = getGitInfo(projectPath);
714
- const prev = getPreviousHandoffInfo(projectPath, continuesFrom, resolvedFlowSlug);
715
-
716
- const branchLine = git.branch || '[not a git repo or detached HEAD]';
717
-
718
- const commitsSection =
719
- git.recentCommits.length > 0
720
- ? git.recentCommits.map((c) => \` - \${c}\`).join('\\n')
721
- : ' - [no recent commits or not a git repo]';
722
-
723
- const allModified = [...new Set([...git.modifiedFiles, ...git.stagedFiles])];
724
- let modifiedSection;
725
- if (allModified.length > 0) {
726
- modifiedSection = allModified
727
- .slice(0, 10)
728
- .map((f) => \`| \${f} | [describe changes] | [why changed] |\`)
729
- .join('\\n');
730
- if (allModified.length > 10)
731
- modifiedSection += \`\\n| ... and \${allModified.length - 10} more files | | |\`;
732
- } else {
733
- modifiedSection = '| [no modified files detected] | | |';
734
- }
735
-
736
- let chainSection;
737
- if (prev.exists) {
738
- const currentFlowDir = resolvedFlowSlug || '_standalone';
739
- const prevFlowDir = prev.flow || '_standalone';
740
- const prevPath =
741
- prevFlowDir === currentFlowDir ? \`./\${prev.filename}\` : \`../\${prevFlowDir}/\${prev.filename}\`;
742
- chainSection = \`## Handoff Chain
743
-
744
- - **Continues from**: [\${prev.filename}](\${prevPath})
745
- - Previous title: \${prev.title || 'Unknown'}
746
- - **Supersedes**: [list any older handoffs this replaces, or "None"]
747
-
748
- > Review the previous handoff for full context before filling this one.\`;
749
- } else {
750
- chainSection = \`## Handoff Chain
751
-
752
- - **Continues from**: None (fresh start)
753
- - **Supersedes**: None
754
-
755
- > This is the first handoff for this task.\`;
756
- }
757
-
758
- const content = \`# Handoff: [TASK_TITLE - replace this]
759
-
760
- ## Session Metadata
761
- - Created: \${timestamp}
762
- - Project: \${projectPath}
763
- - Branch: \${branchLine}
764
- - Session duration: [estimate how long you worked]
765
-
766
- ### Recent Commits (for context)
767
- \${commitsSection}
768
-
769
- \${chainSection}
770
-
771
- ## Current State Summary
772
-
773
- [TODO: Write one paragraph describing what was being worked on, current status, and where things left off]
774
-
775
- ## Codebase Understanding
776
-
777
- ### Architecture Overview
778
-
779
- [TODO: Document key architectural insights discovered during this session]
780
-
781
- ### Critical Files
782
-
783
- | File | Purpose | Relevance |
784
- |------|---------|-----------|
785
- | [TODO: Add critical files] | | |
786
-
787
- ### Key Patterns Discovered
788
-
789
- [TODO: Document important patterns, conventions, or idioms found in this codebase]
790
-
791
- ## Work Completed
792
-
793
- ### Tasks Finished
794
-
795
- - [ ] [TODO: List completed tasks]
796
-
797
- ### Files Modified
798
-
799
- | File | Changes | Rationale |
800
- |------|---------|-----------|
801
- \${modifiedSection}
802
-
803
- ### Decisions Made
804
-
805
- | Decision | Options Considered | Rationale |
806
- |----------|-------------------|-----------|
807
- | [TODO: Document key decisions] | | |
808
-
809
- ## Pending Work
810
-
811
- ### Immediate Next Steps
812
-
813
- 1. [TODO: Most critical next action]
814
- 2. [TODO: Second priority]
815
- 3. [TODO: Third priority]
816
-
817
- ### Blockers/Open Questions
818
-
819
- - [ ] [TODO: List blockers or open questions]
820
-
821
- ### Deferred Items
822
-
823
- - [TODO: Items deferred and why]
824
-
825
- ## Context for Resuming Agent
826
-
827
- ### Important Context
828
-
829
- [TODO: Critical information the next agent MUST know]
830
-
831
- ### Assumptions Made
832
-
833
- [TODO: List assumptions that might not be obvious]
834
-
835
- ### Potential Gotchas
836
-
837
- [TODO: Things that could trip up the next agent]
838
-
839
- ## Environment State
840
-
841
- [TODO: Required env vars, running processes, DB state]
842
-
843
- ## Related Resources
844
-
845
- [TODO: Links to docs, PRs, issues, Slack threads]
846
- \`;
847
-
848
- fs.writeFileSync(filepath, content, 'utf-8');
849
-
850
- const compactContent = \`## Handoff: \${slug}
851
- Branch: \${branchLine} | Created: \${timestamp}
852
-
853
- ### State
854
- [TODO: 1-2 sentences — what's happening, where we left off]
855
-
856
- ### Decisions
857
- - [TODO: key decisions with rationale]
858
-
859
- ### Next Steps
860
- 1. [TODO: most critical action]
861
- 2. [TODO: second priority]
862
-
863
- ### Blockers
864
- - [TODO: what's blocking progress]
865
-
866
- ### Assumptions
867
- - [TODO: assumptions that if wrong, change the approach]\`;
868
-
869
- return { filepath, filename, compactContent };
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 };
870
416
  }
871
-
872
- // --- Main ---
873
417
  const args = process.argv.slice(2);
874
- if (args.includes('--help') || args.includes('-h')) {
875
- console.log('Usage: node create_handoff.js [task-slug] [--continues-from <previous>] [--flow <flow-slug>]');
876
- console.log(' node create_handoff.js "implementing-auth"');
877
- console.log(' node create_handoff.js "auth-part-2" --continues-from 2024-01-15-auth.md');
878
- console.log(' node create_handoff.js "auth-part-2" --flow add-auth');
879
- process.exit(0);
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);
880
423
  }
881
-
882
- let slug = null;
883
- let continuesFrom = null;
884
- let flowSlug = null;
424
+ let slug = null; let from = null; let flow = null;
885
425
  for (let i = 0; i < args.length; i++) {
886
- if (args[i] === '--continues-from' && i + 1 < args.length) {
887
- continuesFrom = args[++i];
888
- } else if (args[i] === '--flow' && i + 1 < args.length) {
889
- flowSlug = sanitizeSlug(args[++i]);
890
- } else if (!args[i].startsWith('-')) {
891
- slug = args[i];
892
- }
893
- }
894
-
895
- const cwd = process.cwd();
896
- const { filepath, compactContent } = generateHandoff(cwd, slug, continuesFrom, flowSlug);
897
- console.log(\`Created handoff: \${filepath}\`);
898
- console.log('\\nCompact flow knowledge entry:');
899
- console.log('---');
900
- console.log(compactContent);
901
- console.log('---');
902
- console.log(\`\\nNext: Open the file and fill in all [TODO: ...] sections.\`);
903
- console.log(\`Then validate: node scripts/validate_handoff.js \${filepath}\`);
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);
904
438
  `},{file:`scripts/list_handoffs.js`,content:`#!/usr/bin/env node
905
- /**
906
- * List available handoff documents in the current project.
907
- *
908
- * Searches for handoff documents in .aikit-state/handoffs/<flow-slug>/ and .aikit-state/handoffs/_standalone/ and displays:
909
- * - Filename with date
910
- * - Flow scope
911
- * - Title extracted from document
912
- * - Status (complete / in progress / needs work)
913
- *
914
- * Usage:
915
- * node list_handoffs.js # List handoffs in current project
916
- * node list_handoffs.js /path # List handoffs in specified path
917
- */
918
-
919
439
  const fs = require('node:fs');
920
440
  const path = require('node:path');
921
-
922
441
  function extractTitle(filepath) {
923
- try {
924
- const content = fs.readFileSync(filepath, 'utf-8');
925
- const m = content.match(/^#\\s+(?:Handoff:\\s*)?(.+)$/m);
926
- if (m) {
927
- const title = m[1].trim();
928
- if (title.startsWith('[') && title.endsWith(']')) return '[Untitled - needs completion]';
929
- return title.length > 50 ? \`\${title.slice(0, 50)}...\` : title;
930
- }
931
- } catch {
932
- /* ignore */
933
- }
934
- return '[Unable to read title]';
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]";
935
448
  }
936
-
937
449
  function checkCompletion(filepath) {
938
- try {
939
- const content = fs.readFileSync(filepath, 'utf-8');
940
- const count = (content.match(/\\[TODO:/g) || []).length;
941
- if (count === 0) return 'Complete';
942
- if (count <= 3) return \`In Progress (\${count} TODOs)\`;
943
- return \`Needs Work (\${count} TODOs)\`;
944
- } catch {
945
- return 'Unknown';
946
- }
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"; }
947
456
  }
948
-
949
457
  function parseDateFromFilename(filename) {
950
- const m = filename.match(/^(\\d{4}-\\d{2}-\\d{2})-(\\d{6})/);
951
- if (m) {
952
- try {
953
- return new Date(\`\${m[1]}T\${m[2].slice(0, 2)}:\${m[2].slice(2, 4)}:\${m[2].slice(4, 6)}\`);
954
- } catch {
955
- /* ignore */
956
- }
957
- }
958
- return null;
959
- }
960
-
961
- function listHandoffs(projectPath) {
962
- const handoffs = [];
963
- const baseDir = path.join(projectPath, '.aikit-state', 'handoffs');
964
-
965
- function collectFromDir(dir, flow) {
966
- if (!fs.existsSync(dir)) return;
967
-
968
- for (const name of fs.readdirSync(dir)) {
969
- if (!name.endsWith('.md')) continue;
970
- const fp = path.join(dir, name);
971
- const stat = fs.statSync(fp);
972
-
973
- handoffs.push({
974
- path: fp,
975
- filename: name,
976
- flow,
977
- title: extractTitle(fp),
978
- status: checkCompletion(fp),
979
- date: parseDateFromFilename(name),
980
- size: stat.size,
981
- });
982
- }
983
- }
984
-
985
- if (fs.existsSync(baseDir)) {
986
- const flowDirs = fs
987
- .readdirSync(baseDir)
988
- .filter((name) => {
989
- try {
990
- return fs.statSync(path.join(baseDir, name)).isDirectory();
991
- } catch {
992
- return false;
993
- }
994
- })
995
- .sort((a, b) => {
996
- try {
997
- return fs.statSync(path.join(baseDir, b)).mtimeMs - fs.statSync(path.join(baseDir, a)).mtimeMs;
998
- } catch {
999
- return 0;
1000
- }
1001
- });
1002
-
1003
- for (const flow of flowDirs) {
1004
- collectFromDir(path.join(baseDir, flow), flow === '_standalone' ? null : flow);
1005
- }
1006
- }
1007
-
1008
- handoffs.sort((a, b) => (b.date || 0) - (a.date || 0));
1009
- return handoffs;
1010
- }
1011
-
1012
- function formatDate(dt) {
1013
- if (!dt) return 'Unknown date';
1014
- return dt.toISOString().replace('T', ' ').slice(0, 16);
1015
- }
1016
-
1017
- // --- Main ---
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";
1018
481
  const args = process.argv.slice(2);
1019
- if (args.includes('--help') || args.includes('-h')) {
1020
- console.log('Usage: node list_handoffs.js [project-path]');
1021
- process.exit(0);
1022
- }
1023
-
482
+ if (args.includes("--help") || args.includes("-h")) { console.log("Usage: node list_handoffs.js [project-path]"); process.exit(0); }
1024
483
  const projectPath = args[0] || process.cwd();
1025
484
  const handoffs = listHandoffs(projectPath);
1026
-
1027
- if (handoffs.length === 0) {
1028
- console.log('No handoff documents found.');
1029
- console.log(\`Looked in: \${path.join(projectPath, '.aikit-state', 'handoffs', '*')}\`);
1030
- console.log(\` \${path.join(projectPath, '.aikit-state', 'handoffs', '_standalone')}\`);
1031
- console.log('\\nCreate one with: node scripts/create_handoff.js [task-slug]');
1032
- process.exit(0);
1033
- }
1034
-
1035
- console.log(\`\\nFound \${handoffs.length} handoff(s) in \${projectPath}:\\n\`);
1036
- console.log(\`\${'Date'.padEnd(18)} \${'Flow'.padEnd(18)} \${'Status'.padEnd(25)} Title\`);
1037
- console.log(\`\${'-'.repeat(18)} \${'-'.repeat(18)} \${'-'.repeat(25)} \${'-'.repeat(40)}\`);
1038
-
1039
- for (const h of handoffs) {
1040
- const flowLabel = h.flow || '[standalone]';
1041
- console.log(\`\${formatDate(h.date).padEnd(18)} \${flowLabel.padEnd(18)} \${h.status.padEnd(25)} \${h.title}\`);
1042
- }
1043
-
1044
- if (args.includes('--json')) {
1045
- console.log(\`\\n\${JSON.stringify(handoffs, null, 2)}\`);
1046
- }
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));
1047
497
  `},{file:`scripts/validate_handoff.js`,content:`#!/usr/bin/env node
1048
- /**
1049
- * Validate a handoff document for completeness and quality.
1050
- *
1051
- * Checks:
1052
- * - No TODO placeholders remaining
1053
- * - Required sections present and populated
1054
- * - No potential secrets detected
1055
- * - Referenced files exist
1056
- * - Quality scoring (0-100)
1057
- *
1058
- * Usage:
1059
- * node validate_handoff.js <handoff-file>
1060
- * node validate_handoff.js .aikit-state/handoffs/add-auth/2024-01-15-143022-auth.md
1061
- * node validate_handoff.js .aikit-state/handoffs/_standalone/2024-01-15-143022-auth.md
1062
- */
1063
-
1064
498
  const fs = require('node:fs');
1065
499
  const path = require('node:path');
1066
-
1067
- // Secret detection patterns
1068
500
  const SECRET_PATTERNS = [
1069
- [/["']?[a-zA-Z_]*api[_-]?key["']?\\s*[:=]\\s*["'][^"']{10,}["']/gi, 'API key'],
1070
- [/["']?[a-zA-Z_]*password["']?\\s*[:=]\\s*["'][^"']+["']/gi, 'Password'],
1071
- [/["']?[a-zA-Z_]*secret["']?\\s*[:=]\\s*["'][^"']{10,}["']/gi, 'Secret'],
1072
- [/["']?[a-zA-Z_]*token["']?\\s*[:=]\\s*["'][^"']{20,}["']/gi, 'Token'],
1073
- [/["']?[a-zA-Z_]*private[_-]?key["']?\\s*[:=]/gi, 'Private key'],
1074
- [/-----BEGIN [A-Z]+ PRIVATE KEY-----/g, 'PEM private key'],
1075
- [/mongodb(\\+srv)?:\\/\\/[^/\\s]+:[^@\\s]+@/gi, 'MongoDB connection string with password'],
1076
- [/postgres:\\/\\/[^/\\s]+:[^@\\s]+@/gi, 'PostgreSQL connection string with password'],
1077
- [/mysql:\\/\\/[^/\\s]+:[^@\\s]+@/gi, 'MySQL connection string with password'],
1078
- [/Bearer\\s+[a-zA-Z0-9_\\-.]+/g, 'Bearer token'],
1079
- [/ghp_[a-zA-Z0-9]{36}/g, 'GitHub personal access token'],
1080
- [/sk-[a-zA-Z0-9]{48}/g, 'OpenAI API key'],
1081
- [/xox[baprs]-[a-zA-Z0-9-]+/g, 'Slack token'],
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'],
1082
511
  ];
1083
-
1084
512
  const REQUIRED_SECTIONS = ['Current State Summary', 'Important Context', 'Immediate Next Steps'];
1085
- const RECOMMENDED_SECTIONS = [
1086
- 'Architecture Overview',
1087
- 'Critical Files',
1088
- 'Files Modified',
1089
- 'Decisions Made',
1090
- 'Assumptions Made',
1091
- 'Potential Gotchas',
1092
- ];
1093
-
1094
- function checkTodos(content) {
1095
- const todos = content.match(/\\[TODO:[^\\]]*\\]/g) || [];
1096
- return { clear: todos.length === 0, todos };
1097
- }
1098
-
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 }; }
1099
516
  function checkRequiredSections(content) {
1100
- const missing = [];
1101
- for (const section of REQUIRED_SECTIONS) {
1102
- const pattern = new RegExp(
1103
- \`(?:^|\\\\n)##?\\\\s*\${section.replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&')}\`,
1104
- 'i',
1105
- );
1106
- const match = pattern.exec(content);
1107
- if (!match) {
1108
- missing.push(\`\${section} (missing)\`);
1109
- } else {
1110
- const start = match.index + match[0].length;
1111
- const nextSection = content.slice(start).search(/\\n##?\\s+/);
1112
- const end = nextSection >= 0 ? start + nextSection : content.length;
1113
- const sectionContent = content.slice(start, end).trim();
1114
- if (sectionContent.length < 50 || sectionContent.includes('[TODO')) {
1115
- missing.push(\`\${section} (incomplete)\`);
1116
- }
1117
- }
1118
- }
1119
- return { complete: missing.length === 0, missing };
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 };
1120
529
  }
1121
-
1122
530
  function checkRecommendedSections(content) {
1123
- const missing = [];
1124
- for (const section of RECOMMENDED_SECTIONS) {
1125
- const pattern = new RegExp(
1126
- \`(?:^|\\\\n)##?\\\\s*\${section.replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&')}\`,
1127
- 'i',
1128
- );
1129
- if (!pattern.test(content)) missing.push(section);
1130
- }
1131
- return missing;
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;
1132
537
  }
1133
-
1134
538
  function scanForSecrets(content) {
1135
- const findings = [];
1136
- for (const [pattern, description] of SECRET_PATTERNS) {
1137
- const matches = content.match(pattern);
1138
- if (matches) findings.push({ description, count: matches.length });
1139
- }
1140
- return findings;
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;
1141
542
  }
1142
-
1143
543
  function checkFileReferences(content, basePath) {
1144
- const patterns = [
1145
- /\\|\\s*([a-zA-Z0-9_\\-./]+\\.[a-zA-Z]+)\\s*\\|/g,
1146
- /\`([a-zA-Z0-9_\\-./]+\\.[a-zA-Z]+(?::\\d+)?)\`/g,
1147
- /(?:^|\\s)([a-zA-Z0-9_\\-./]+\\.[a-zA-Z]+:\\d+)/gm,
1148
- ];
1149
-
1150
- const foundFiles = new Set();
1151
- for (const pattern of patterns) {
1152
- for (const m of content.matchAll(pattern)) {
1153
- const filepath = m[1].split(':')[0];
1154
- if (filepath && !filepath.startsWith('http') && filepath.includes('/')) {
1155
- foundFiles.add(filepath);
1156
- }
1157
- }
1158
- }
1159
-
1160
- const existing = [];
1161
- const missing = [];
1162
- for (const f of foundFiles) {
1163
- if (fs.existsSync(path.join(basePath, f))) existing.push(f);
1164
- else missing.push(f);
1165
- }
1166
- return { existing, missing };
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' };
1167
565
  }
1168
-
1169
- function calculateScore(todosClear, reqComplete, missingReq, missingRec, secrets, filesMissing) {
1170
- let score = 100;
1171
-
1172
- if (!todosClear) score -= 30;
1173
- if (!reqComplete) score -= 10 * missingReq.length;
1174
- if (secrets.length > 0) score -= 20;
1175
- if (filesMissing.length > 0) score -= 5 * Math.min(filesMissing.length, 4);
1176
- score -= 2 * missingRec.length;
1177
-
1178
- score = Math.max(0, score);
1179
-
1180
- let rating;
1181
- if (score >= 90) rating = 'Excellent - Ready for handoff';
1182
- else if (score >= 70) rating = 'Good - Minor improvements suggested';
1183
- else if (score >= 50) rating = 'Fair - Needs attention before handoff';
1184
- else rating = 'Poor - Significant work needed';
1185
-
1186
- return { score, rating };
1187
- }
1188
-
1189
566
  function resolveProjectRootFromHandoff(filepath) {
1190
- const handoffsDir = path.dirname(filepath);
1191
- const handoffMarkerDir = path.basename(path.dirname(handoffsDir));
1192
- const stateMarkerDir = path.basename(path.dirname(path.dirname(handoffsDir)));
1193
- const isFlowScoped = handoffMarkerDir === 'handoffs' && stateMarkerDir === '.aikit-state';
1194
-
1195
- return isFlowScoped ? path.resolve(handoffsDir, '..', '..', '..') : path.resolve(handoffsDir, '..');
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, '..');
1196
571
  }
1197
-
1198
572
  function validateHandoff(filepath) {
1199
- if (!fs.existsSync(filepath)) return { error: \`File not found: \${filepath}\` };
1200
-
1201
- const content = fs.readFileSync(filepath, 'utf-8');
1202
- const basePath = resolveProjectRootFromHandoff(filepath);
1203
-
1204
- const { clear: todosClear, todos } = checkTodos(content);
1205
- const { complete: reqComplete, missing: missingReq } = checkRequiredSections(content);
1206
- const missingRec = checkRecommendedSections(content);
1207
- const secrets = scanForSecrets(content);
1208
- const { existing: existingFiles, missing: missingFiles } = checkFileReferences(content, basePath);
1209
-
1210
- const { score, rating } = calculateScore(
1211
- todosClear,
1212
- reqComplete,
1213
- missingReq,
1214
- missingRec,
1215
- secrets,
1216
- missingFiles,
1217
- );
1218
-
1219
- return {
1220
- filepath,
1221
- score,
1222
- rating,
1223
- todosClear,
1224
- remainingTodos: todos.slice(0, 5),
1225
- todoCount: todos.length,
1226
- requiredComplete: reqComplete,
1227
- missingRequired: missingReq,
1228
- missingRecommended: missingRec,
1229
- secretsFound: secrets,
1230
- filesVerified: existingFiles.length,
1231
- filesMissing: missingFiles.slice(0, 5),
1232
- };
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) };
1233
583
  }
1234
-
1235
584
  function printReport(result) {
1236
- if (result.error) {
1237
- console.log(\`Error: \${result.error}\`);
1238
- return;
1239
- }
1240
-
1241
- console.log(\`\\n\${'='.repeat(60)}\`);
1242
- console.log('Handoff Validation Report');
1243
- console.log('='.repeat(60));
1244
- console.log(\`File: \${result.filepath}\`);
1245
- console.log(\`\\nQuality Score: \${result.score}/100 - \${result.rating}\`);
1246
- console.log('='.repeat(60));
1247
-
1248
- if (result.todosClear) {
1249
- console.log('\\n[PASS] No TODO placeholders remaining');
1250
- } else {
1251
- console.log(\`\\n[FAIL] \${result.todoCount} TODO placeholders found:\`);
1252
- for (const t of result.remainingTodos) console.log(\` - \${t.slice(0, 50)}...\`);
1253
- }
1254
-
1255
- if (result.requiredComplete) {
1256
- console.log('\\n[PASS] All required sections complete');
1257
- } else {
1258
- console.log('\\n[FAIL] Missing/incomplete required sections:');
1259
- for (const s of result.missingRequired) console.log(\` - \${s}\`);
1260
- }
1261
-
1262
- if (result.secretsFound.length === 0) {
1263
- console.log('\\n[PASS] No potential secrets detected');
1264
- } else {
1265
- console.log('\\n[WARN] Potential secrets detected:');
1266
- for (const s of result.secretsFound)
1267
- console.log(\` - \${s.description} (\${s.count} match(es))\`);
1268
- }
1269
-
1270
- console.log(
1271
- \`\\n[INFO] File references: \${result.filesVerified} verified, \${result.filesMissing.length} missing\`,
1272
- );
1273
- if (result.filesMissing.length > 0) {
1274
- for (const f of result.filesMissing) console.log(\` - \${f}\`);
1275
- }
1276
-
1277
- if (result.missingRecommended.length > 0) {
1278
- console.log('\\n[INFO] Missing recommended sections:');
1279
- for (const s of result.missingRecommended) console.log(\` - \${s}\`);
1280
- }
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); }
1281
601
  }
1282
-
1283
- // --- Main ---
1284
602
  const args = process.argv.slice(2);
1285
- if (args.includes('--help') || args.includes('-h') || args.length === 0) {
1286
- console.log('Usage: node validate_handoff.js <handoff-file>');
1287
- console.log(' node validate_handoff.js .aikit-state/handoffs/<flow-slug>/2024-01-15-143022-auth.md');
1288
- console.log(' node validate_handoff.js .aikit-state/handoffs/_standalone/2024-01-15-143022-auth.md');
1289
- process.exit(args.length === 0 ? 1 : 0);
1290
- }
1291
-
1292
- if (args.includes('--json')) {
1293
- const result = validateHandoff(args.find((a) => !a.startsWith('-')));
1294
- console.log(JSON.stringify(result, null, 2));
1295
- } else {
1296
- const result = validateHandoff(args[0]);
1297
- printReport(result);
1298
- }
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);
1299
611
  `},{file:`SKILL.md`,content:`---
1300
612
  name: session-handoff
1301
- description: "Creates comprehensive handoff documents for seamless AI agent session transfers. Triggered when: (1) user requests handoff/memory/context save, (2) context window approaches capacity (>70% pressure), (3) major task milestone completed, (4) work session ending, (5) user says 'save state', 'create handoff', 'I need to pause', 'context is getting full', (6) resuming work with 'load handoff', 'resume from', 'continue where we left off'. Proactively suggests handoffs after substantial work (multiple file edits, complex debugging, architecture decisions). Solves long-running agent context exhaustion by enabling fresh agents to continue with zero ambiguity."
613
+ description: "Creates handoff docs for session transfer. Trigger on save-context, high context pressure, milestones, session end, or resume requests."
1302
614
  metadata:
1303
615
  category: cross-cutting
1304
616
  domain: general
@@ -1308,121 +620,68 @@ metadata:
1308
620
  requires: [aikit]
1309
621
  relatedSkills: [lesson-learned]
1310
622
  ---
1311
-
1312
623
  # Session Handoff
1313
-
1314
- Creates compact, high-signal handoffs so a fresh agent can resume work without rereading the full conversation.
1315
-
624
+ Handoffs.
1316
625
  ## Mindset
1317
-
1318
- Session handoffs solve the "context amnesia" problem — you have 200K tokens of accumulated understanding that will vanish. Your job is compression with zero information loss on the critical path.
1319
-
1320
- Think of it as writing a briefing for a colleague who will take over your shift:
1321
- - What's the current state? (not how we got here)
1322
- - What decisions were made and WHY? (not what was considered)
1323
- - What's blocked and needs action? (not what's going fine)
1324
- - What will surprise them? (gotchas, non-obvious constraints)
1325
-
1326
- The 80/20 rule applies brutally: 80% of context is irrelevant to the next step. Identify the 20% that matters and make it crystal clear.
1327
-
626
+ Keep only state, decisions + why, blockers, next step, gotchas.
1328
627
  ## Quick Reference
1329
-
1330
- Use this skill in two modes:
1331
- - **CREATE** when user asks to save context, context pressure exceeds ~70%, a milestone lands, or the session is ending
1332
- - **RESUME** when user asks to continue prior work, load saved context, or pick up from an existing handoff
1333
-
1334
- Every good handoff stores context in two forms:
1335
- 1. **Full markdown file** at \`.aikit-state/handoffs/{flow-slug}/YYYY-MM-DD-HHMMSS-[slug].md\`
1336
- 2. **Compact flow knowledge entry** via \`knowledge({ action: "remember", scope: "flow", category: "session", title: "Session Handoff: <slug>", content })\`
1337
-
1338
- On resume, start with \`knowledge({ action: "withdraw", scope: "flow", profile: "implementer", budget: 6000 })\`. Only open the full handoff file if the compact entry is insufficient.
1339
-
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 })\`
1340
633
  ## NEVER
1341
-
1342
- - **NEVER include full file contents in handoffs** — reference file paths + what changed, not entire files. Handoffs that paste 1000 lines of code are useless because the next agent will need to re-read them anyway.
1343
- - **NEVER omit the "why" behind decisions** — "chose JWT" is worthless without "chose JWT because: stateless, API consumers, no session store." The next agent WILL re-examine your decisions without the rationale.
1344
- - **NEVER create a handoff without blockers/next-steps** these are the single most important sections. Everything else provides context; these drive action.
1345
- - **NEVER hand off mid-failure** without capturing: error message, what was tried, what was NOT tried yet. Incomplete failure context causes the next agent to repeat failed approaches.
1346
- - **NEVER include conversation history** — handoffs summarize OUTCOMES not PROCESS. "We discussed 3 options and chose X" not "I said... user said... I said..."
1347
- - **NEVER create handoffs larger than 500 words** — if it's longer, you're pasting instead of summarizing. Compress ruthlessly.
1348
- - **NEVER skip the architecture context** — the next agent doesn't know the codebase. Include: key files, patterns used, critical constraints.
1349
-
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**
1350
641
  ## Handoff Quality Gate
1351
-
1352
- A good handoff passes the "cold start" test: could a brand-new agent, with NO prior context, pick up where you left off and produce correct output on the FIRST attempt?
1353
-
1354
- Score your handoff (must be 4/4):
1355
- - [ ] **Actionable** — next steps are specific enough to execute without interpretation
1356
- - [ ] **Self-contained** — doesn't require reading the conversation to understand
1357
- - [ ] **Minimal** — no content that doesn't directly serve the next agent's work
1358
- - [ ] **Verified** — decisions cite evidence (file:line, test output, tool result)
1359
-
1360
- If any criterion fails, compress further or add the missing context.
1361
-
642
+ Cold-start test: can new agent resume correctly on first attempt?
643
+ - [ ] **Actionable**
644
+ - [ ] **Self-contained**
645
+ - [ ] **Minimal**
646
+ - [ ] **Verified**
1362
647
  ## CREATE Workflow
1363
-
1364
648
  ### 1. Choose storage target
1365
-
1366
- Save the full handoff at one of these locations:
1367
649
  - Flow-scoped: \`.aikit-state/handoffs/{flow-slug}/YYYY-MM-DD-HHMMSS-[slug].md\`
1368
650
  - Standalone: \`.aikit-state/handoffs/_standalone/YYYY-MM-DD-HHMMSS-[slug].md\`
1369
-
1370
- Use a slug that describes the active task, not the conversation.
1371
-
1372
651
  ### 2. Gather only critical-path facts
1373
-
1374
- Before writing, capture the minimum context needed for a cold start:
1375
- - Current state: what is done, in progress, failing, or pending
1376
- - Key files: exact paths and why they matter
1377
- - Decisions: what was chosen and why
1378
- - Verification: tests, checks, tool results, or specific evidence
1379
- - Blockers: error text, attempted fixes, untried options
1380
- - Next steps: ordered, concrete actions
1381
-
1382
- Use AI Kit tools inline instead of external scripts:
1383
- - \`stash({ action: "set", key: "handoff:<slug>:notes", value })\` for temporary compression while drafting
1384
- - \`checkpoint({ action: "save", label: "handoff-<slug>", notes })\` when session state itself matters
1385
- - \`knowledge({ action: "list", category: "decisions" })\` and \`search({ query })\` to pull prior decisions into the handoff when relevant
1386
-
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 })\`
1387
660
  ### 3. Write the full handoff
1388
-
1389
- Use this structure:
1390
-
1391
661
  \`\`\`md
1392
662
  # Handoff: <task>
1393
-
1394
663
  ## Current State
1395
664
  - Status:
1396
665
  - Last completed step:
1397
666
  - Current failure or pending task:
1398
-
1399
667
  ## Critical Files
1400
668
  - path/to/file - purpose + why it matters now
1401
-
1402
669
  ## Decisions
1403
670
  - Decision: why
1404
671
  - Evidence: file:line | test output | tool result
1405
-
1406
672
  ## Next Steps
1407
673
  1. First action
1408
674
  2. Second action
1409
675
  3. Third action
1410
-
1411
676
  ## Blockers
1412
677
  - Blocker:
1413
678
  - Tried:
1414
679
  - Not tried yet:
1415
-
1416
680
  ## Gotchas
1417
681
  - Non-obvious constraint or pattern
1418
682
  \`\`\`
1419
-
1420
- Use [references/handoff-template.md](references/handoff-template.md) when you need a fuller structure, but delete sections that add no value.
1421
-
683
+ Use [references/handoff-template.md](references/handoff-template.md) if needed.
1422
684
  ### 4. Save compact flow knowledge
1423
-
1424
- Store a compressed version for automatic resume:
1425
-
1426
685
  \`\`\`
1427
686
  knowledge({
1428
687
  action: "remember",
@@ -1436,70 +695,38 @@ Blockers: ...
1436
695
  Evidence: ..."
1437
696
  })
1438
697
  \`\`\`
1439
-
1440
- Target 1-2K characters. This is the resume fast-path.
1441
-
698
+ Target 1-2K chars.
1442
699
  ### 5. Self-review before finalizing
1443
-
1444
- Confirm all of the following:
1445
- - No secrets, tokens, or raw credentials
1446
- - No pasted file dumps
1447
- - Every decision includes rationale
1448
- - Every blocker includes attempted and untried paths
1449
- - Next step #1 is immediately executable
1450
- - Quality Gate score is 4/4
1451
-
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
1452
705
  ## RESUME Workflow
1453
-
1454
706
  ### 1. Pull compact context first
1455
-
1456
- Start with:
1457
-
1458
707
  \`\`\`
1459
708
  knowledge({ action: "withdraw", scope: "flow", profile: "implementer", budget: 6000 })
1460
709
  \`\`\`
1461
-
1462
- If the entry is truncated or multiple handoffs exist, use:
1463
-
710
+ If entry is truncated or multiple handoffs exist, use:
1464
711
  \`\`\`
1465
712
  knowledge({ action: "list", category: "session", scope: "flow" })
1466
713
  knowledge({ action: "read", path: "<handoff-entry>" })
1467
714
  \`\`\`
1468
-
1469
715
  ### 2. Load the full handoff only when needed
1470
-
1471
- Open the markdown file in \`.aikit-state/handoffs/\` if the compact entry lacks enough detail to act safely.
1472
-
716
+ Open markdown file in \`.aikit-state/handoffs/\` only if compact entry is not enough.
1473
717
  ### 3. Verify live state before coding
1474
-
1475
- Do a fast reality check:
1476
- - Does current branch still match the handoff assumptions?
1477
- - Do referenced files still exist?
1478
- - Have blockers already been resolved?
1479
- - Do stated next steps still make sense against current diff and tests?
1480
-
1481
- Use the checklist in [references/resume-checklist.md](references/resume-checklist.md) for the verification pass.
1482
-
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).
1483
723
  ### 4. Resume from the first actionable step
1484
-
1485
- Begin with Next Step #1. If reality differs from the handoff, update the handoff or create a successor handoff instead of silently continuing on stale assumptions.
1486
-
724
+ Begin with Next Step #1. If reality differs, update handoff or create successor handoff.
1487
725
  ## Chaining Rule
1488
-
1489
- For long-running work, create a successor handoff instead of overwriting history. Each new handoff should state what it supersedes and what changed since the previous handoff.
1490
-
726
+ Create successor handoff instead of overwriting history.
1491
727
  ## Output Expectations
1492
-
1493
- When asked to create a handoff, return:
1494
- - Handoff status
1495
- - File path written
1496
- - Key decisions captured
1497
- - Top blocker or next step
1498
-
1499
- Do not return conversation transcripts or visual dashboards.
1500
-
728
+ Return status, file path, decisions, blocker/next step.
1501
729
  ## Resources
1502
-
1503
- - [references/handoff-template.md](references/handoff-template.md) - full markdown template when a simple structure is insufficient
1504
- - [references/resume-checklist.md](references/resume-checklist.md) - verification checklist before resuming from saved context
730
+ - [references/handoff-template.md](references/handoff-template.md)
731
+ - [references/resume-checklist.md](references/resume-checklist.md)
1505
732
  `}];export{e as default};