@erclx/aitk 0.99.1 → 0.101.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-orchestrate/SKILL.md +2 -2
- package/claude/skills/claude-orchestrate/references/orchestrator-poll.md +4 -4
- package/claude/skills/claude-orchestrate/scripts/poll.sh +11 -10
- package/claude/skills/claude-pr-review/SKILL.md +16 -16
- package/claude/skills/claude-teach/REQUIREMENT.md +46 -0
- package/claude/skills/claude-teach/SKILL.md +115 -0
- package/claude/skills/claude-teach/references/lesson-craft.md +52 -0
- package/claude/skills/claude-teach/references/pedagogy.md +67 -0
- package/docs/agents/commands.md +5 -0
- package/docs/agents/index.md +1 -0
- package/docs/agents/records.md +10 -9
- package/docs/agents/teach.md +119 -0
- package/docs/ai-workflow.md +4 -3
- package/docs/operating-model.md +9 -8
- package/package.json +1 -1
- package/src/cli.ts +4 -0
- package/src/commands/teach.ts +650 -0
- package/src/records/backup.ts +4 -2
- package/src/records/validate.ts +190 -3
- package/src/teach/workspace.ts +797 -0
- package/standards/index.md +1 -0
- package/standards/teach.md +155 -0
package/src/records/validate.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
1
|
+
import { existsSync, statSync } from 'node:fs'
|
|
2
2
|
import { readdir, readFile } from 'node:fs/promises'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { parseFrontmatter, readField } from '@/indexes/frontmatter'
|
|
5
5
|
import { linesOutsideFences } from '@/markdown/scan'
|
|
6
|
+
import {
|
|
7
|
+
TEACH_GLOSSARY,
|
|
8
|
+
TEACH_MISSION,
|
|
9
|
+
TEACH_RECORDS,
|
|
10
|
+
TEACH_REFERENCE,
|
|
11
|
+
TEACH_RESOURCES,
|
|
12
|
+
WORKSPACE_NAME,
|
|
13
|
+
} from '@/teach/workspace'
|
|
6
14
|
|
|
7
15
|
export const RECORD_KINDS = [
|
|
8
16
|
'plans',
|
|
@@ -10,6 +18,7 @@ export const RECORD_KINDS = [
|
|
|
10
18
|
'intake',
|
|
11
19
|
'memory',
|
|
12
20
|
'standards',
|
|
21
|
+
'teach',
|
|
13
22
|
] as const
|
|
14
23
|
|
|
15
24
|
export type RecordKind = (typeof RECORD_KINDS)[number]
|
|
@@ -29,6 +38,7 @@ const FOLDERS_BY_KIND: Readonly<Record<RecordKind, readonly string[]>> = {
|
|
|
29
38
|
intake: [join('.claude', 'intake')],
|
|
30
39
|
memory: [join('.claude', 'memory')],
|
|
31
40
|
standards: ['standards', join('.claude', 'standards')],
|
|
41
|
+
teach: [join('.claude', 'teach')],
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
/**
|
|
@@ -106,7 +116,7 @@ export function isRecordKind(value: string): value is RecordKind {
|
|
|
106
116
|
/**
|
|
107
117
|
* Whether a kind's folder is shared session scratch at the main worktree root.
|
|
108
118
|
*
|
|
109
|
-
* The
|
|
119
|
+
* The five record folders are, so every session validates the records every
|
|
110
120
|
* other session reads. The corpus is tracked instead, so a linked worktree holds
|
|
111
121
|
* its own edited copy, and defaulting that kind to the main root would report on
|
|
112
122
|
* a tree the session never touched and say nothing about which one it read.
|
|
@@ -566,6 +576,167 @@ async function checkDump(dir: string, slug: string): Promise<Finding[]> {
|
|
|
566
576
|
return [...findings, ...perCluster.flat()]
|
|
567
577
|
}
|
|
568
578
|
|
|
579
|
+
const TEACH_SUCCESS = /^##[ \t]+Success looks like[ \t]*$/
|
|
580
|
+
const NUMBERED_RECORD = /^\d{4}-[a-z0-9]+(-[a-z0-9]+)*\.md$/
|
|
581
|
+
/**
|
|
582
|
+
* A kebab slug that does not open with an ordinal. The lookahead rejects a
|
|
583
|
+
* leading run of digits followed by a hyphen and nothing else, so a subject
|
|
584
|
+
* whose own name starts with a digit still passes.
|
|
585
|
+
*/
|
|
586
|
+
const REFERENCE_NAME = /^(?!\d+-)[a-z0-9]+(-[a-z0-9]+)*\.md$/
|
|
587
|
+
|
|
588
|
+
/** The two fields every markdown file in a workspace carries. */
|
|
589
|
+
async function checkTeachFile(
|
|
590
|
+
dir: string,
|
|
591
|
+
slug: string,
|
|
592
|
+
file: string,
|
|
593
|
+
subject: string,
|
|
594
|
+
): Promise<Finding[]> {
|
|
595
|
+
const frontmatter = parseFrontmatter(await readFile(join(dir, file), 'utf8'))
|
|
596
|
+
|
|
597
|
+
const missing = ['title', 'description'].filter(
|
|
598
|
+
(field) => !readField(frontmatter, field),
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
if (missing.length === 0) return []
|
|
602
|
+
|
|
603
|
+
return [
|
|
604
|
+
finding(
|
|
605
|
+
'frontmatter-incomplete',
|
|
606
|
+
slug,
|
|
607
|
+
subject,
|
|
608
|
+
`carries no ${missing.join(' and no ')}.`,
|
|
609
|
+
),
|
|
610
|
+
]
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* One markdown subfolder of a workspace. `lessons/` and `assets/` are never
|
|
615
|
+
* reached, because a lesson is generated markup carrying no frontmatter and a
|
|
616
|
+
* walk over it would report every one as malformed.
|
|
617
|
+
*/
|
|
618
|
+
async function checkTeachSubfolder(
|
|
619
|
+
dir: string,
|
|
620
|
+
slug: string,
|
|
621
|
+
folder: string,
|
|
622
|
+
name: RegExp,
|
|
623
|
+
message: string,
|
|
624
|
+
): Promise<Finding[]> {
|
|
625
|
+
const path = join(dir, folder)
|
|
626
|
+
|
|
627
|
+
// Tested as a directory rather than for presence. Every other walk in this
|
|
628
|
+
// module takes its path from `listFolders`, and this one is built from a
|
|
629
|
+
// fixed name, so a workspace holding a plain file called `reference` would
|
|
630
|
+
// reach `readdir` and take the whole run down with `ENOTDIR`.
|
|
631
|
+
if (!statSync(path, { throwIfNoEntry: false })?.isDirectory()) return []
|
|
632
|
+
|
|
633
|
+
const files = await listMarkdown(path)
|
|
634
|
+
|
|
635
|
+
const malformed = files
|
|
636
|
+
.filter((file) => !name.test(file))
|
|
637
|
+
.map((file) =>
|
|
638
|
+
finding('name-malformed', slug, `${folder}/${file}`, message),
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
const perFile = await Promise.all(
|
|
642
|
+
files.map((file) => checkTeachFile(path, slug, file, `${folder}/${file}`)),
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
return [...malformed, ...perFile.flat()]
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async function checkWorkspace(dir: string, slug: string): Promise<Finding[]> {
|
|
649
|
+
const findings: Finding[] = []
|
|
650
|
+
|
|
651
|
+
if (!WORKSPACE_NAME.test(slug)) {
|
|
652
|
+
findings.push(
|
|
653
|
+
finding(
|
|
654
|
+
'name-malformed',
|
|
655
|
+
slug,
|
|
656
|
+
slug,
|
|
657
|
+
'is not named NN-<topic> with a two-digit ordinal, so a listing sorts alphabetically rather than by when each workspace opened.',
|
|
658
|
+
),
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const files = await listMarkdown(dir)
|
|
663
|
+
|
|
664
|
+
if (!files.includes(TEACH_MISSION)) {
|
|
665
|
+
findings.push(
|
|
666
|
+
finding(
|
|
667
|
+
'index-missing',
|
|
668
|
+
slug,
|
|
669
|
+
TEACH_MISSION,
|
|
670
|
+
'is absent, so the workspace states no subject and no success to finish against.',
|
|
671
|
+
),
|
|
672
|
+
)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
for (const required of [TEACH_RESOURCES, TEACH_GLOSSARY]) {
|
|
676
|
+
if (!files.includes(required)) {
|
|
677
|
+
findings.push(
|
|
678
|
+
finding(
|
|
679
|
+
'section-missing',
|
|
680
|
+
slug,
|
|
681
|
+
required,
|
|
682
|
+
'is required and the workspace carries no such file.',
|
|
683
|
+
),
|
|
684
|
+
)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const perFile = await Promise.all(
|
|
689
|
+
files.map((file) => checkTeachFile(dir, slug, file, file)),
|
|
690
|
+
)
|
|
691
|
+
findings.push(...perFile.flat())
|
|
692
|
+
|
|
693
|
+
if (files.includes(TEACH_MISSION)) {
|
|
694
|
+
const text = await readFile(join(dir, TEACH_MISSION), 'utf8')
|
|
695
|
+
|
|
696
|
+
if (!hasOpeningDate(parseFrontmatter(text)?.raw ?? '')) {
|
|
697
|
+
findings.push(
|
|
698
|
+
finding(
|
|
699
|
+
'date-malformed',
|
|
700
|
+
slug,
|
|
701
|
+
TEACH_MISSION,
|
|
702
|
+
'carries no date field as YYYY-MM-DD, so the workspace states no opening day.',
|
|
703
|
+
),
|
|
704
|
+
)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (
|
|
708
|
+
!linesOutsideFences(text).some((line) => TEACH_SUCCESS.test(line.trim()))
|
|
709
|
+
) {
|
|
710
|
+
findings.push(
|
|
711
|
+
finding(
|
|
712
|
+
'section-missing',
|
|
713
|
+
slug,
|
|
714
|
+
'## Success looks like',
|
|
715
|
+
'is absent, so the mission names no observable thing the learner will be able to do.',
|
|
716
|
+
),
|
|
717
|
+
)
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return [
|
|
722
|
+
...findings,
|
|
723
|
+
...(await checkTeachSubfolder(
|
|
724
|
+
dir,
|
|
725
|
+
slug,
|
|
726
|
+
TEACH_REFERENCE,
|
|
727
|
+
REFERENCE_NAME,
|
|
728
|
+
'is not named <slug>.md as a kebab slug opening with no ordinal, so a page looked up rather than worked through implies an order no reader follows.',
|
|
729
|
+
)),
|
|
730
|
+
...(await checkTeachSubfolder(
|
|
731
|
+
dir,
|
|
732
|
+
slug,
|
|
733
|
+
TEACH_RECORDS,
|
|
734
|
+
NUMBERED_RECORD,
|
|
735
|
+
'is not numbered NNNN-<slug>.md, so the records carry no read order.',
|
|
736
|
+
)),
|
|
737
|
+
]
|
|
738
|
+
}
|
|
739
|
+
|
|
569
740
|
const MEMORY_INDEX = 'index.md'
|
|
570
741
|
const MEMORY_FIELDS = ['title', 'description', 'category'] as const
|
|
571
742
|
|
|
@@ -872,6 +1043,22 @@ export function checkStandard(name: string, text: string): Finding[] {
|
|
|
872
1043
|
return [...findings, ...checkStandardName(name, scope.statement)]
|
|
873
1044
|
}
|
|
874
1045
|
|
|
1046
|
+
/**
|
|
1047
|
+
* The three kinds whose records are folders rather than files. Keyed by kind so
|
|
1048
|
+
* a seventh arrives as an entry here and the compiler names the walk it owes,
|
|
1049
|
+
* where a ternary chain would silently fall through to whichever branch is last.
|
|
1050
|
+
*/
|
|
1051
|
+
const FOLDER_CHECK: Readonly<
|
|
1052
|
+
Record<
|
|
1053
|
+
Exclude<RecordKind, 'plans' | 'memory' | 'standards'>,
|
|
1054
|
+
(dir: string, slug: string) => Promise<Finding[]>
|
|
1055
|
+
>
|
|
1056
|
+
> = {
|
|
1057
|
+
groundwork: checkTrack,
|
|
1058
|
+
intake: checkDump,
|
|
1059
|
+
teach: checkWorkspace,
|
|
1060
|
+
}
|
|
1061
|
+
|
|
875
1062
|
function refuse(reason: ValidateRefusal, message: string): ValidateRefused {
|
|
876
1063
|
return { ok: false, reason, message }
|
|
877
1064
|
}
|
|
@@ -939,7 +1126,7 @@ export async function validateRecords(
|
|
|
939
1126
|
}
|
|
940
1127
|
|
|
941
1128
|
const folders = await listFolders(dir)
|
|
942
|
-
const check = kind
|
|
1129
|
+
const check = FOLDER_CHECK[kind]
|
|
943
1130
|
const perFolder = await Promise.all(
|
|
944
1131
|
folders.map((slug) => check(join(dir, slug), slug)),
|
|
945
1132
|
)
|