@vpxa/aikit 0.1.145 → 0.1.146
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 +1 -1
- package/packages/server/dist/index.js +1 -1
- package/packages/server/dist/{server-Mioq3dZQ.js → server-3YlZan29.js} +164 -164
- package/packages/store/dist/index.js +2 -2
- package/packages/store/dist/{sqlite-vec-store-C-GvCcJH.js → sqlite-vec-store-CrvQ06f8.js} +6 -6
- package/scaffold/dist/definitions/bodies.mjs +12 -0
- package/scaffold/dist/definitions/flows.mjs +114 -3
- package/scaffold/dist/definitions/hooks.mjs +1 -1
- package/scaffold/dist/definitions/skills/requirements-clarity.mjs +81 -297
- package/scaffold/dist/definitions/skills/session-handoff.mjs +258 -64
|
@@ -230,7 +230,8 @@ If multiple aspects "Need Exploration", spend time re-exploring the codebase bef
|
|
|
230
230
|
*
|
|
231
231
|
* Usage:
|
|
232
232
|
* node check_staleness.js <handoff-file>
|
|
233
|
-
* node check_staleness.js .handoffs/2024-01-15-143022-auth.md
|
|
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
|
|
234
235
|
*/
|
|
235
236
|
|
|
236
237
|
const fs = require('node:fs');
|
|
@@ -367,6 +368,15 @@ function calculateStaleness(daysOld, commitsSince, filesChanged, branchMatches,
|
|
|
367
368
|
return { level, recommendation, issues };
|
|
368
369
|
}
|
|
369
370
|
|
|
371
|
+
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, '..');
|
|
378
|
+
}
|
|
379
|
+
|
|
370
380
|
function checkStaleness(handoffPath) {
|
|
371
381
|
if (!fs.existsSync(handoffPath)) return { error: \`Handoff file not found: \${handoffPath}\` };
|
|
372
382
|
|
|
@@ -374,7 +384,7 @@ function checkStaleness(handoffPath) {
|
|
|
374
384
|
const projectPath =
|
|
375
385
|
meta.projectPath && fs.existsSync(meta.projectPath)
|
|
376
386
|
? meta.projectPath
|
|
377
|
-
:
|
|
387
|
+
: resolveProjectRootFromHandoff(handoffPath);
|
|
378
388
|
|
|
379
389
|
const isGitRepo = runCmd('git rev-parse --git-dir', projectPath).ok;
|
|
380
390
|
|
|
@@ -475,7 +485,8 @@ function printReport(result) {
|
|
|
475
485
|
const args = process.argv.slice(2);
|
|
476
486
|
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
|
477
487
|
console.log('Usage: node check_staleness.js <handoff-file>');
|
|
478
|
-
console.log(' node check_staleness.js .handoffs
|
|
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');
|
|
479
490
|
process.exit(args.length === 0 ? 1 : 0);
|
|
480
491
|
}
|
|
481
492
|
|
|
@@ -502,6 +513,7 @@ if (args.includes('--json')) {
|
|
|
502
513
|
* node create_handoff.js [task-slug]
|
|
503
514
|
* node create_handoff.js "implementing-auth"
|
|
504
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
|
|
505
517
|
* node create_handoff.js # auto-generates slug from timestamp
|
|
506
518
|
*/
|
|
507
519
|
|
|
@@ -549,48 +561,118 @@ function getGitInfo(projectPath) {
|
|
|
549
561
|
return info;
|
|
550
562
|
}
|
|
551
563
|
|
|
552
|
-
function
|
|
553
|
-
const
|
|
554
|
-
if (!fs.existsSync(
|
|
564
|
+
function getFlowDirectories(projectPath) {
|
|
565
|
+
const flowsDir = path.join(projectPath, '.flows');
|
|
566
|
+
if (!fs.existsSync(flowsDir)) return [];
|
|
555
567
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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;
|
|
560
594
|
|
|
561
|
-
let title = name;
|
|
562
595
|
try {
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
if (m) title = m[1].trim();
|
|
596
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
597
|
+
return meta.status === 'active';
|
|
566
598
|
} catch {
|
|
567
|
-
|
|
599
|
+
return false;
|
|
568
600
|
}
|
|
601
|
+
});
|
|
569
602
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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;
|
|
573
627
|
try {
|
|
574
|
-
|
|
628
|
+
const content = fs.readFileSync(fp, 'utf-8');
|
|
629
|
+
const m = content.match(/^#\\s+(?:Handoff:\\s*)?(.+)$/m);
|
|
630
|
+
if (m) title = m[1].trim();
|
|
575
631
|
} catch {
|
|
576
632
|
/* ignore */
|
|
577
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 });
|
|
578
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
|
+
});
|
|
579
660
|
|
|
580
|
-
|
|
661
|
+
for (const fd of flowDirs) {
|
|
662
|
+
collectFromDir(path.join(baseDir, fd), fd === '_standalone' ? null : fd);
|
|
581
663
|
}
|
|
582
664
|
|
|
583
665
|
handoffs.sort((a, b) => (b.date || 0) - (a.date || 0));
|
|
584
666
|
return handoffs;
|
|
585
667
|
}
|
|
586
668
|
|
|
587
|
-
function getPreviousHandoffInfo(projectPath, continuesFrom) {
|
|
588
|
-
const handoffs = findPreviousHandoffs(projectPath);
|
|
669
|
+
function getPreviousHandoffInfo(projectPath, continuesFrom, flowSlug) {
|
|
670
|
+
const handoffs = findPreviousHandoffs(projectPath, flowSlug);
|
|
589
671
|
|
|
590
672
|
if (continuesFrom) {
|
|
591
673
|
const found = handoffs.find((h) => h.filename.includes(continuesFrom));
|
|
592
674
|
return found
|
|
593
|
-
? { exists: true, filename: found.filename, title: found.title }
|
|
675
|
+
? { exists: true, filename: found.filename, title: found.title, flow: found.flow }
|
|
594
676
|
: { exists: false, filename: continuesFrom, title: 'Not found' };
|
|
595
677
|
}
|
|
596
678
|
|
|
@@ -599,6 +681,7 @@ function getPreviousHandoffInfo(projectPath, continuesFrom) {
|
|
|
599
681
|
exists: true,
|
|
600
682
|
filename: handoffs[0].filename,
|
|
601
683
|
title: handoffs[0].title,
|
|
684
|
+
flow: handoffs[0].flow,
|
|
602
685
|
suggested: true,
|
|
603
686
|
};
|
|
604
687
|
}
|
|
@@ -613,7 +696,7 @@ function sanitizeSlug(slug) {
|
|
|
613
696
|
.replace(/[^a-z0-9-]/g, '');
|
|
614
697
|
}
|
|
615
698
|
|
|
616
|
-
function generateHandoff(projectPath, slug, continuesFrom) {
|
|
699
|
+
function generateHandoff(projectPath, slug, continuesFrom, flowSlug) {
|
|
617
700
|
const now = new Date();
|
|
618
701
|
const timestamp = now.toISOString().replace('T', ' ').slice(0, 19);
|
|
619
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')}\`;
|
|
@@ -622,12 +705,13 @@ function generateHandoff(projectPath, slug, continuesFrom) {
|
|
|
622
705
|
slug = sanitizeSlug(slug);
|
|
623
706
|
const filename = \`\${fileTs}-\${slug}.md\`;
|
|
624
707
|
|
|
625
|
-
const
|
|
708
|
+
const resolvedFlowSlug = flowSlug || getActiveFlowSlug(projectPath);
|
|
709
|
+
const handoffsDir = getHandoffsDir(projectPath, flowSlug);
|
|
626
710
|
fs.mkdirSync(handoffsDir, { recursive: true });
|
|
627
711
|
const filepath = path.join(handoffsDir, filename);
|
|
628
712
|
|
|
629
713
|
const git = getGitInfo(projectPath);
|
|
630
|
-
const prev = getPreviousHandoffInfo(projectPath, continuesFrom);
|
|
714
|
+
const prev = getPreviousHandoffInfo(projectPath, continuesFrom, resolvedFlowSlug);
|
|
631
715
|
|
|
632
716
|
const branchLine = git.branch || '[not a git repo or detached HEAD]';
|
|
633
717
|
|
|
@@ -651,9 +735,13 @@ function generateHandoff(projectPath, slug, continuesFrom) {
|
|
|
651
735
|
|
|
652
736
|
let chainSection;
|
|
653
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}\`;
|
|
654
742
|
chainSection = \`## Handoff Chain
|
|
655
743
|
|
|
656
|
-
- **Continues from**: [\${prev.filename}](
|
|
744
|
+
- **Continues from**: [\${prev.filename}](\${prevPath})
|
|
657
745
|
- Previous title: \${prev.title || 'Unknown'}
|
|
658
746
|
- **Supersedes**: [list any older handoffs this replaces, or "None"]
|
|
659
747
|
|
|
@@ -758,39 +846,68 @@ function generateHandoff(projectPath, slug, continuesFrom) {
|
|
|
758
846
|
\`;
|
|
759
847
|
|
|
760
848
|
fs.writeFileSync(filepath, content, 'utf-8');
|
|
761
|
-
|
|
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 };
|
|
762
870
|
}
|
|
763
871
|
|
|
764
872
|
// --- Main ---
|
|
765
873
|
const args = process.argv.slice(2);
|
|
766
874
|
if (args.includes('--help') || args.includes('-h')) {
|
|
767
|
-
console.log('Usage: node create_handoff.js [task-slug] [--continues-from <previous>]');
|
|
875
|
+
console.log('Usage: node create_handoff.js [task-slug] [--continues-from <previous>] [--flow <flow-slug>]');
|
|
768
876
|
console.log(' node create_handoff.js "implementing-auth"');
|
|
769
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');
|
|
770
879
|
process.exit(0);
|
|
771
880
|
}
|
|
772
881
|
|
|
773
882
|
let slug = null;
|
|
774
883
|
let continuesFrom = null;
|
|
884
|
+
let flowSlug = null;
|
|
775
885
|
for (let i = 0; i < args.length; i++) {
|
|
776
886
|
if (args[i] === '--continues-from' && i + 1 < args.length) {
|
|
777
887
|
continuesFrom = args[++i];
|
|
888
|
+
} else if (args[i] === '--flow' && i + 1 < args.length) {
|
|
889
|
+
flowSlug = sanitizeSlug(args[++i]);
|
|
778
890
|
} else if (!args[i].startsWith('-')) {
|
|
779
891
|
slug = args[i];
|
|
780
892
|
}
|
|
781
893
|
}
|
|
782
894
|
|
|
783
895
|
const cwd = process.cwd();
|
|
784
|
-
const { filepath } = generateHandoff(cwd, slug, continuesFrom);
|
|
896
|
+
const { filepath, compactContent } = generateHandoff(cwd, slug, continuesFrom, flowSlug);
|
|
785
897
|
console.log(\`Created handoff: \${filepath}\`);
|
|
898
|
+
console.log('\\nCompact flow knowledge entry:');
|
|
899
|
+
console.log('---');
|
|
900
|
+
console.log(compactContent);
|
|
901
|
+
console.log('---');
|
|
786
902
|
console.log(\`\\nNext: Open the file and fill in all [TODO: ...] sections.\`);
|
|
787
903
|
console.log(\`Then validate: node scripts/validate_handoff.js \${filepath}\`);
|
|
788
904
|
`},{file:`scripts/list_handoffs.js`,content:`#!/usr/bin/env node
|
|
789
905
|
/**
|
|
790
906
|
* List available handoff documents in the current project.
|
|
791
907
|
*
|
|
792
|
-
* Searches for handoff documents in .handoffs/ and displays:
|
|
908
|
+
* Searches for handoff documents in .aikit-state/handoffs/<flow-slug>/ and .aikit-state/handoffs/_standalone/ and displays:
|
|
793
909
|
* - Filename with date
|
|
910
|
+
* - Flow scope
|
|
794
911
|
* - Title extracted from document
|
|
795
912
|
* - Status (complete / in progress / needs work)
|
|
796
913
|
*
|
|
@@ -842,23 +959,50 @@ function parseDateFromFilename(filename) {
|
|
|
842
959
|
}
|
|
843
960
|
|
|
844
961
|
function listHandoffs(projectPath) {
|
|
845
|
-
const dir = path.join(projectPath, '.handoffs');
|
|
846
|
-
if (!fs.existsSync(dir)) return [];
|
|
847
|
-
|
|
848
962
|
const handoffs = [];
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
+
}
|
|
862
1006
|
}
|
|
863
1007
|
|
|
864
1008
|
handoffs.sort((a, b) => (b.date || 0) - (a.date || 0));
|
|
@@ -882,17 +1026,19 @@ const handoffs = listHandoffs(projectPath);
|
|
|
882
1026
|
|
|
883
1027
|
if (handoffs.length === 0) {
|
|
884
1028
|
console.log('No handoff documents found.');
|
|
885
|
-
console.log(\`Looked in: \${path.join(projectPath, '.handoffs')}\`);
|
|
1029
|
+
console.log(\`Looked in: \${path.join(projectPath, '.aikit-state', 'handoffs', '*')}\`);
|
|
1030
|
+
console.log(\` \${path.join(projectPath, '.aikit-state', 'handoffs', '_standalone')}\`);
|
|
886
1031
|
console.log('\\nCreate one with: node scripts/create_handoff.js [task-slug]');
|
|
887
1032
|
process.exit(0);
|
|
888
1033
|
}
|
|
889
1034
|
|
|
890
1035
|
console.log(\`\\nFound \${handoffs.length} handoff(s) in \${projectPath}:\\n\`);
|
|
891
|
-
console.log(\`\${'Date'.padEnd(18)} \${'Status'.padEnd(25)} Title\`);
|
|
892
|
-
console.log(\`\${'-'.repeat(18)} \${'-'.repeat(25)} \${'-'.repeat(40)}\`);
|
|
1036
|
+
console.log(\`\${'Date'.padEnd(18)} \${'Flow'.padEnd(18)} \${'Status'.padEnd(25)} Title\`);
|
|
1037
|
+
console.log(\`\${'-'.repeat(18)} \${'-'.repeat(18)} \${'-'.repeat(25)} \${'-'.repeat(40)}\`);
|
|
893
1038
|
|
|
894
1039
|
for (const h of handoffs) {
|
|
895
|
-
|
|
1040
|
+
const flowLabel = h.flow || '[standalone]';
|
|
1041
|
+
console.log(\`\${formatDate(h.date).padEnd(18)} \${flowLabel.padEnd(18)} \${h.status.padEnd(25)} \${h.title}\`);
|
|
896
1042
|
}
|
|
897
1043
|
|
|
898
1044
|
if (args.includes('--json')) {
|
|
@@ -911,7 +1057,8 @@ if (args.includes('--json')) {
|
|
|
911
1057
|
*
|
|
912
1058
|
* Usage:
|
|
913
1059
|
* node validate_handoff.js <handoff-file>
|
|
914
|
-
* node validate_handoff.js .handoffs/2024-01-15-143022-auth.md
|
|
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
|
|
915
1062
|
*/
|
|
916
1063
|
|
|
917
1064
|
const fs = require('node:fs');
|
|
@@ -1039,11 +1186,20 @@ function calculateScore(todosClear, reqComplete, missingReq, missingRec, secrets
|
|
|
1039
1186
|
return { score, rating };
|
|
1040
1187
|
}
|
|
1041
1188
|
|
|
1189
|
+
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, '..');
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1042
1198
|
function validateHandoff(filepath) {
|
|
1043
1199
|
if (!fs.existsSync(filepath)) return { error: \`File not found: \${filepath}\` };
|
|
1044
1200
|
|
|
1045
1201
|
const content = fs.readFileSync(filepath, 'utf-8');
|
|
1046
|
-
const basePath =
|
|
1202
|
+
const basePath = resolveProjectRootFromHandoff(filepath);
|
|
1047
1203
|
|
|
1048
1204
|
const { clear: todosClear, todos } = checkTodos(content);
|
|
1049
1205
|
const { complete: reqComplete, missing: missingReq } = checkRequiredSections(content);
|
|
@@ -1128,7 +1284,8 @@ function printReport(result) {
|
|
|
1128
1284
|
const args = process.argv.slice(2);
|
|
1129
1285
|
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
|
1130
1286
|
console.log('Usage: node validate_handoff.js <handoff-file>');
|
|
1131
|
-
console.log(' node validate_handoff.js .handoffs
|
|
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');
|
|
1132
1289
|
process.exit(args.length === 0 ? 1 : 0);
|
|
1133
1290
|
}
|
|
1134
1291
|
|
|
@@ -1147,7 +1304,7 @@ metadata:
|
|
|
1147
1304
|
domain: general
|
|
1148
1305
|
applicability: always
|
|
1149
1306
|
inputs: [session-state, decisions]
|
|
1150
|
-
outputs: [handoff-document]
|
|
1307
|
+
outputs: [handoff-document, flow-knowledge-entry]
|
|
1151
1308
|
requires: [aikit]
|
|
1152
1309
|
relatedSkills: [lesson-learned]
|
|
1153
1310
|
---
|
|
@@ -1169,7 +1326,23 @@ Determine which mode applies:
|
|
|
1169
1326
|
**Proactive suggestion?** After substantial work (5+ file edits, complex debugging, major decisions), suggest:
|
|
1170
1327
|
> "We've made significant progress. Consider creating a handoff document to preserve this context for future sessions. Say 'create handoff' when ready."
|
|
1171
1328
|
|
|
1172
|
-
> **aikit Integration:**
|
|
1329
|
+
> **aikit Integration:** Handoffs use dual storage — file in \`.aikit-state/handoffs/{flow-slug}/\` (gitignored, browsable) + compact knowledge entry (\`knowledge({ scope: "flow" })\`). On resume, \`withdraw\` retrieves the compact version automatically. For full context, read the file from \`.aikit-state/\`.
|
|
1330
|
+
|
|
1331
|
+
## Dual Format Storage
|
|
1332
|
+
|
|
1333
|
+
Each handoff creates TWO artifacts:
|
|
1334
|
+
|
|
1335
|
+
1. **Full document** → \`.aikit-state/handoffs/{flow-slug}/<timestamp>-<slug>.md\`
|
|
1336
|
+
- Complete template with all sections
|
|
1337
|
+
- Browsable on disk but NOT committed to git
|
|
1338
|
+
- Auto-cleaned when \`.aikit-state/\` is cleared
|
|
1339
|
+
|
|
1340
|
+
2. **Compact knowledge entry** → \`knowledge({ action: "remember", scope: "flow", category: "session", title: "Session Handoff: <slug>", content: "<compact format>" })\`
|
|
1341
|
+
- ~1-2K chars, optimized for \`withdraw\` retrieval
|
|
1342
|
+
- Contains: State, Decisions, Next Steps, Blockers, Assumptions
|
|
1343
|
+
- Auto-retrieved by resuming agents via \`withdraw({ scope: "flow", profile: "implementer", budget: 6000 })\`
|
|
1344
|
+
|
|
1345
|
+
The compact format ensures continuity even if the next session doesn't explicitly load the full handoff file.
|
|
1173
1346
|
|
|
1174
1347
|
## CREATE Workflow
|
|
1175
1348
|
|
|
@@ -1179,6 +1352,7 @@ Run the smart scaffold script to create a pre-filled handoff document:
|
|
|
1179
1352
|
|
|
1180
1353
|
\`\`\`bash
|
|
1181
1354
|
node scripts/create_handoff.js [task-slug]
|
|
1355
|
+
node scripts/create_handoff.js [task-slug] --flow <flow-slug>
|
|
1182
1356
|
\`\`\`
|
|
1183
1357
|
|
|
1184
1358
|
Example: \`node scripts/create_handoff.js implementing-user-auth\`
|
|
@@ -1188,12 +1362,16 @@ Example: \`node scripts/create_handoff.js implementing-user-auth\`
|
|
|
1188
1362
|
node scripts/create_handoff.js "auth-part-2" --continues-from 2024-01-15-auth.md
|
|
1189
1363
|
\`\`\`
|
|
1190
1364
|
|
|
1191
|
-
The script
|
|
1192
|
-
-
|
|
1193
|
-
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
-
|
|
1365
|
+
The script generates:
|
|
1366
|
+
- Full handoff file in \`.aikit-state/handoffs/{active-flow}/\`
|
|
1367
|
+
- Compact format string (printed to stdout for pasting into \`knowledge({ action: "remember", ... })\`)
|
|
1368
|
+
|
|
1369
|
+
It also:
|
|
1370
|
+
- Uses \`--flow\` to override the active flow slug
|
|
1371
|
+
- Falls back to \`.aikit-state/handoffs/_standalone/\` when no flow is active
|
|
1372
|
+
- Pre-fills: timestamp, project path, git branch, recent commits, modified files
|
|
1373
|
+
- Adds handoff chain links if continuing from previous
|
|
1374
|
+
- Outputs file path for editing
|
|
1197
1375
|
|
|
1198
1376
|
### Step 2: Complete the Handoff Document
|
|
1199
1377
|
|
|
@@ -1264,6 +1442,19 @@ The script checks:
|
|
|
1264
1442
|
- Branch divergence
|
|
1265
1443
|
- Missing referenced files
|
|
1266
1444
|
|
|
1445
|
+
### Step 2b: Check Flow Knowledge
|
|
1446
|
+
|
|
1447
|
+
If a flow is active, check for compact handoffs in flow knowledge:
|
|
1448
|
+
\`\`\`
|
|
1449
|
+
knowledge({ action: "withdraw", scope: "flow", profile: "implementer", budget: 6000 })
|
|
1450
|
+
\`\`\`
|
|
1451
|
+
|
|
1452
|
+
If a "Session Handoff:" entry appears but is truncated:
|
|
1453
|
+
\`\`\`
|
|
1454
|
+
knowledge({ action: "list", category: "session", scope: "flow" })
|
|
1455
|
+
knowledge({ action: "read", path: "<handoff-entry>" })
|
|
1456
|
+
\`\`\`
|
|
1457
|
+
|
|
1267
1458
|
### Step 3: Load the Handoff
|
|
1268
1459
|
|
|
1269
1460
|
Read the relevant handoff document completely before taking any action.
|
|
@@ -1317,7 +1508,10 @@ When resuming from a chain, read the most recent handoff first, then reference p
|
|
|
1317
1508
|
|
|
1318
1509
|
## Storage Location
|
|
1319
1510
|
|
|
1320
|
-
|
|
1511
|
+
**Primary (flow-scoped):** \`.aikit-state/handoffs/{flow-slug}/\`
|
|
1512
|
+
**Standalone fallback:** \`.aikit-state/handoffs/_standalone/\` (when no flow is active)
|
|
1513
|
+
|
|
1514
|
+
All handoffs live in \`.aikit-state/\` which is gitignored — handoffs are ephemeral context, not project history.
|
|
1321
1515
|
|
|
1322
1516
|
Naming convention: \`YYYY-MM-DD-HHMMSS-[slug].md\`
|
|
1323
1517
|
|
|
@@ -1329,7 +1523,7 @@ Example: \`2024-01-15-143022-implementing-auth.md\`
|
|
|
1329
1523
|
|
|
1330
1524
|
| Script | Purpose |
|
|
1331
1525
|
|--------|---------|
|
|
1332
|
-
| \`create_handoff.js [slug] [--continues-from <file>]\` | Generate new handoff with smart scaffolding |
|
|
1526
|
+
| \`create_handoff.js [slug] [--continues-from <file>] [--flow <flow-slug>]\` | Generate new handoff with smart scaffolding |
|
|
1333
1527
|
| \`list_handoffs.js [path]\` | List available handoffs in a project |
|
|
1334
1528
|
| \`validate_handoff.js <file>\` | Check completeness, quality, and security |
|
|
1335
1529
|
| \`check_staleness.js <file>\` | Assess if handoff context is still current |
|