@erclx/aitk 0.59.0 → 0.61.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-memory-capture/SKILL.md +10 -24
- package/claude/skills/claude-memory-review/SKILL.md +3 -1
- package/claude/skills/claude-orchestrate/SKILL.md +3 -1
- package/claude/skills/claude-orchestrate/references/orchestrator-poll.md +44 -0
- package/claude/skills/claude-orchestrate/scripts/poll.sh +202 -0
- package/docs/agents/commands.md +1 -1
- package/docs/agents/records.md +7 -3
- package/docs/ai-workflow.md +1 -1
- package/governance/rules/claude/559-memory.md +20 -0
- package/package.json +4 -4
- package/src/commands/records.ts +2 -0
- package/src/records/validate.ts +177 -9
- package/standards/index.md +1 -0
- package/standards/memory.md +131 -0
- package/tooling/claude/seeds/CLAUDE.md +2 -5
|
@@ -9,6 +9,8 @@ Scan the current session for patterns worth persisting, send each to the surface
|
|
|
9
9
|
|
|
10
10
|
A fact about a domain belongs in that domain's context entry, which the three-tier model already loads on demand. Writing it to memory instead puts it in a folder nothing opens. Routing is therefore the point of this skill and the memory file is the fallback.
|
|
11
11
|
|
|
12
|
+
The filename and its type prefix, the frontmatter, the body shape each type carries, and the lifecycle are fixed by `.claude/standards/memory.md`, or `${CLAUDE_SKILL_DIR}/../../standards/memory.md` when the project does not have it. Read it before writing an entry and follow it rather than working the shape from memory.
|
|
13
|
+
|
|
12
14
|
## Guards
|
|
13
15
|
|
|
14
16
|
- All `.claude/memory/` reads and writes resolve at the main worktree root, not the current worktree. See Worktrees in `CLAUDE.md`.
|
|
@@ -20,7 +22,8 @@ A fact about a domain belongs in that domain's context entry, which the three-ti
|
|
|
20
22
|
|
|
21
23
|
Read in parallel from the project root, skipping any that do not exist:
|
|
22
24
|
|
|
23
|
-
-
|
|
25
|
+
- `.claude/standards/memory.md`: the filename, frontmatter, body shape, and lifecycle every entry follows
|
|
26
|
+
- `CLAUDE.md`: the project's write location and any rule it states over the folder
|
|
24
27
|
- `.claude/memory/index.md`: existing index, to avoid duplicates
|
|
25
28
|
- `.claude/context/index.md`: the domain catalog Step 3 routes against
|
|
26
29
|
- `.claude/standards/prose.md`: voice and banned words applied to memory file bodies
|
|
@@ -30,14 +33,9 @@ Read a standard from `${CLAUDE_SKILL_DIR}/../../standards/` instead when the pro
|
|
|
30
33
|
|
|
31
34
|
## Step 2: classify candidates
|
|
32
35
|
|
|
33
|
-
Scan the session and group candidate patterns
|
|
34
|
-
|
|
35
|
-
- **feedback**: explicit user corrections, stated preferences, or non-obvious confirmations
|
|
36
|
-
- **project**: decisions, initiatives, deadlines, or motivations not derivable from git or code
|
|
37
|
-
- **user**: role, expertise, responsibilities, or working preferences
|
|
38
|
-
- **reference**: pointers to external systems (dashboards, trackers, channels)
|
|
36
|
+
Scan the session and group candidate patterns as `feedback`, `project`, `user`, or `reference`. What each type holds and what makes one fire are the Types table in `.claude/standards/memory.md`. Read the table and classify against it rather than against a recollection of the four names.
|
|
39
37
|
|
|
40
|
-
|
|
38
|
+
Scan the whole session rather than its last exchange. A rule the user stated early and you followed since reads as settled and is exactly the one no file records.
|
|
41
39
|
|
|
42
40
|
## Step 3: route what a context entry owns
|
|
43
41
|
|
|
@@ -65,26 +63,14 @@ For each remaining candidate, grep `.claude/memory/` for an existing file on the
|
|
|
65
63
|
|
|
66
64
|
## Step 5: write the residue
|
|
67
65
|
|
|
68
|
-
|
|
66
|
+
Write each remaining candidate to `.claude/memory/<type>-<slug>.md`, following the template and the shape rules in `.claude/standards/memory.md`. Copy the shape from there rather than from this body, so one edit to the standard moves every entry.
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
---
|
|
72
|
-
title: <one-line human title, as it should read in the index>
|
|
73
|
-
description: <one-line description per .claude/standards/prose.md § Frontmatter descriptions>
|
|
74
|
-
category: <Feedback|Project|User|Reference>
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
<memory body>
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
`category` is the type in sentence case, which is what the index renderer groups on, while the filename keeps the lowercase `<type>-` prefix. A description opening with a backtick or a colon needs single quotes, or the frontmatter fails to parse and the index goes stale.
|
|
81
|
-
|
|
82
|
-
Feedback and project bodies must be three lines: the rule or fact on one line, a `**Why:**` line naming the session signal, and a `**How to apply:**` line for when the rule fires next. Keep each line tight. No narrative.
|
|
83
|
-
|
|
84
|
-
User and reference bodies are a single sentence each.
|
|
68
|
+
Two of its rules are the ones a capture pass gets wrong under time pressure. State the rule rather than the incident that produced it, since the session ending is the only reader who has the narrative. Write the `title` as the rule itself, never as the filename stem.
|
|
85
69
|
|
|
86
70
|
Do not edit the index. `.claude/memory/index.md` is generated from sibling frontmatter by a `PostToolUse` hook, the same way the task board's index is, so a hand-appended row is drift the next regeneration discards.
|
|
87
71
|
|
|
72
|
+
Run `aitk records validate memory` when the writes are done and fix what it names. It reads the whole pen rather than this session's writes, so treat a finding on a carried entry as one to fix in place rather than as a reason to stop.
|
|
73
|
+
|
|
88
74
|
## Output
|
|
89
75
|
|
|
90
76
|
Respond with one line per fact routed, written, or updated:
|
|
@@ -7,6 +7,8 @@ description: Reviews `.claude/memory/` and proposes per-entry actions (promote t
|
|
|
7
7
|
|
|
8
8
|
This skill drives the full memory review lifecycle in five phases. Pick the phase from what the user said and whether a review receipt already exists at `<main-root>/.claude/review/memory-review-*.md`.
|
|
9
9
|
|
|
10
|
+
What an entry looks like and why a retired one is moved rather than deleted are fixed by `.claude/standards/memory.md`, or `${CLAUDE_SKILL_DIR}/../../standards/memory.md` when the project does not have it. Read it before rewriting an entry, since a promotion rewrites the rule and a rewrite has to leave the entry conforming.
|
|
11
|
+
|
|
10
12
|
| User intent | Phase | Mutates |
|
|
11
13
|
| ---------------------------------------------------------------------- | --------- | ---------------------------- |
|
|
12
14
|
| "review memory", "promote memory", "sweep stale memories" (no receipt) | Propose | review file only |
|
|
@@ -68,7 +70,7 @@ For each in-scope entry (see Scope), pick one action:
|
|
|
68
70
|
- **Hand off to governance**: the rule is coding-standards class (typescript, testing, naming, error-handling, performance, logging, concurrency, planning). Do not author the rule file inline. In the toolkit repo, point the user at `aitk-governance` and `.claude/standards/rule.md`, which own the source-of-truth rules under `governance/rules/`. In a target project, point the user at the `create-rule` skill, which scaffolds a project-local rule under `.claude/rules/`. Never edit the synced `.claude/rules/` copies of toolkit rules, because `aitk gov sync` overwrites them. Stop at handoff.
|
|
69
71
|
- **Retire**: the rule is stale, already absorbed into a durable surface, too vague to phrase as a rule, or a one-time incident narrative. Apply moves the file to `.claude/.tmp/memory-archive/` rather than deleting it.
|
|
70
72
|
|
|
71
|
-
Retire is an archive, not a deletion
|
|
73
|
+
Retire is an archive, not a deletion, which `.claude/standards/memory.md` states as the rule and this skill executes. The archive is worth less than a plan's, since a promoted entry survives in its destination and a stale one is discarded on purpose, which is why the move is cheap rather than free.
|
|
72
74
|
|
|
73
75
|
When two or more memories collapse into one rule on the same target, propose them as a single merged edit under the matching promote category. The consolidate case is a variant of promote, not a separate action.
|
|
74
76
|
|
|
@@ -37,6 +37,8 @@ The roadmap is optional and this skill does not require it. It carries why a seq
|
|
|
37
37
|
|
|
38
38
|
A compaction is a moment this skill cannot detect, so the human asks for each side of it and this skill reads the matching runbook when they do. On a request to write the handoff or save the session, read `${CLAUDE_SKILL_DIR}/references/orchestrator-handoff.md` and follow it. It writes `.claude/tasks/session.md` with the state of play, the decisions taken under delegated authority, the mistakes worth not repeating, and the standing cautions. On a request to resume after a compaction, read `${CLAUDE_SKILL_DIR}/references/orchestrator-resume.md`, which reads that file back with the board and the groundwork behind the live work. Write nothing to the handoff that the board, a task file, or a groundwork folder already carries.
|
|
39
39
|
|
|
40
|
+
The review trigger takes the same shape. `references/orchestrator-poll.md` holds the loop prompt and the condition under which the poll runs, and `scripts/poll.sh` is what the prompt invokes. Nothing starts or stops that loop on its own, so the condition holds only while a session applies it.
|
|
41
|
+
|
|
40
42
|
That routing lives in this body and this skill is user-invoked, so a session that has dropped the body routes nothing and the request lands as ordinary conversation. Approaching a compaction is when a long session is likeliest to have dropped it, which is the same moment the handoff exists for. Re-invoke `/aitk:claude-orchestrate` first whenever the session has run long or the ask goes unanswered. The two runbooks sit at `references/orchestrator-handoff.md` and `references/orchestrator-resume.md` inside this skill's own folder, so a person who knows their plugin root opens either one directly and follows it without this skill loaded at all.
|
|
41
43
|
|
|
42
44
|
## Output
|
|
@@ -96,7 +98,7 @@ Write no shape for a correction. A correction is a sentence, and a format for ad
|
|
|
96
98
|
3. Decide parallelism and merge order. Note which plans touch a shared wiring seam so their PRs merge in sequence, not at once.
|
|
97
99
|
4. Verify the plan against the tree. Reading it is not enough, since a plan goes stale from whatever merged after it was written. Grep for each construct it names and count the sites against the count it claims. Check that every phase label it cites is still open. Open each file it describes rather than trusting its account of the contents. Correct the plan before handing it over.
|
|
98
100
|
5. Hand off. The human opens a worker worktree with `claude-worktree` and runs `claude-autoship` against the plan. The orchestrator does not spawn workers.
|
|
99
|
-
6. Review the PR. When a worker opens a PR, run `claude-pr-review` to post findings to it. This is the deep, independent pass. The worker's autoship self-review was only the green gate.
|
|
101
|
+
6. Review the PR. When a worker opens a PR, run `claude-pr-review` to post findings to it. This is the deep, independent pass. The worker's autoship self-review was only the green gate. Learning that a PR moved is the mechanical half, so read `${CLAUDE_SKILL_DIR}/references/orchestrator-poll.md` and start the poll it carries on the first dispatch rather than checking the board by hand. It routes a moved or answered pull request straight to a re-review and reports an opened one without acting, which keeps every first pass a batched judgment this session triggers.
|
|
100
102
|
7. Close the loop. After the worker runs `claude-address-review`, re-review if needed, then the human merges. Tell the trailing worker to rebase when its branch shares a seam with the merged one.
|
|
101
103
|
|
|
102
104
|
## Boundaries
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Orchestrator poll runbook
|
|
3
|
+
description: The review trigger, the condition under which it runs, and how to read what it reports
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Run the orchestrator's review trigger. The poll reports pull request movement and the session acts on what it reports. It reads only, and it never starts a first-pass review.
|
|
7
|
+
|
|
8
|
+
`${CLAUDE_SKILL_DIR}/scripts/poll.sh` is the script. It needs `gh` authenticated against the remote and `jq` on the path, and it reads the base branch from `origin/HEAD` rather than assuming a name.
|
|
9
|
+
|
|
10
|
+
## When to run it
|
|
11
|
+
|
|
12
|
+
Start the poll on a dispatch and stop it when the last pull request merges with nothing else out. An open pull request or a dispatched worker is the condition, and both resolve from the board plus `gh pr list` without asking the operator. A release pull request alone does not qualify, since its sweep carries no findings.
|
|
13
|
+
|
|
14
|
+
Nothing enforces this. No hook starts the poll and no check stops it, so the condition holds only while a session applies it. Left always-on it fires into an empty board through every gap between a dispatch and its push, which leaves stopping it to the operator.
|
|
15
|
+
|
|
16
|
+
The poll is session-scoped and dies with the session that started it. Restart it after a compaction, and take the prompt from this file rather than from a transcript, since a running loop holds whatever wording it was started with and a correction here does not reach it.
|
|
17
|
+
|
|
18
|
+
## The prompt
|
|
19
|
+
|
|
20
|
+
Resolve `${CLAUDE_SKILL_DIR}/scripts/poll.sh` to an absolute path and paste that in place of `<POLL_SCRIPT>` below. The variable expands while this runbook renders and not in a `/loop` turn, which arrives as a standalone prompt, so a block carrying the variable reaches the session as a literal string and the run improvises a substitute.
|
|
21
|
+
|
|
22
|
+
```plaintext
|
|
23
|
+
/loop 3m Poll GitHub for pull request movement by running <POLL_SCRIPT>, then act on what it reports.
|
|
24
|
+
|
|
25
|
+
- MOVED or RESPONSE on a pull request I have already reviewed: run the aitk:claude-pr-review skill on it immediately, narrow pass. Re-reviews read prior..head and gain nothing from waiting.
|
|
26
|
+
- OPENED, or a pull request with no prior review pass: report it and stop. First passes wait for the operator, because reading several together is what surfaces cross-PR findings.
|
|
27
|
+
- SEEN: report it and stop. A pass already covers that head, whether it arrived out of band or before the poll first saw the pull request, so no review follows.
|
|
28
|
+
- CONFLICT: report it and stop. The branch owner rebases, not this session.
|
|
29
|
+
- GONE: report it, then sweep the board by invoking the aitk:claude-orchestrate skill and following its queue-refill sweep.
|
|
30
|
+
- A line starting `poll:`: report it verbatim and treat that pull request as unread this run. It is a failed query, not a state.
|
|
31
|
+
- Nothing changed: say exactly "No movement." and nothing else.
|
|
32
|
+
|
|
33
|
+
Never start a first-pass review on your own.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Reading the output
|
|
37
|
+
|
|
38
|
+
Every classification line names a pull request and a state. A line starting `poll:` is not a classification. It means a query failed and the script declined to guess, so that pull request keeps its last known state and is neither reported as moved nor swept as merged. Treat it as unread and let the next run classify it.
|
|
39
|
+
|
|
40
|
+
The script exits non-zero and classifies nothing when the open pull request list itself fails to load. That case would otherwise report every tracked pull request as merged, so the baseline is left untouched and the run says so.
|
|
41
|
+
|
|
42
|
+
The baseline lives at `.claude/.tmp/pr-poll/baseline.txt` under the main worktree root and is per-machine. A first run against a board already in flight reports each open pull request once before it settles.
|
|
43
|
+
|
|
44
|
+
The four review headings the script matches are written by `claude-pr-review` and `claude-address-review`. A project that posts its reviews under different headings edits the two jq filters in the script to match, or every pull request reads as never reviewed.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Reports pull request movement since the last run. Reads only.
|
|
3
|
+
#
|
|
4
|
+
# The shebang is load-bearing. An earlier version ran under the operator's zsh,
|
|
5
|
+
# where an unquoted parameter expansion does not word-split, so `set -- $line`
|
|
6
|
+
# left every field but the first empty and every head compared unequal. The
|
|
7
|
+
# poll reported movement that had not happened, which is the one failure that
|
|
8
|
+
# makes a detection tool worth less than no tool.
|
|
9
|
+
set -e
|
|
10
|
+
set -o pipefail
|
|
11
|
+
|
|
12
|
+
# The baseline is per-machine mutable state, so it stays in gitignored scratch
|
|
13
|
+
# even though the script is tracked. Resolving the main worktree root rather
|
|
14
|
+
# than this file's own folder keeps a poll started from a linked worktree
|
|
15
|
+
# reading the baseline a poll started from main wrote.
|
|
16
|
+
MAIN_ROOT="$(git worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}')"
|
|
17
|
+
if [ -z "$MAIN_ROOT" ]; then
|
|
18
|
+
echo "poll: not a git repository, so nothing is classified" >&2
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
STATE_DIR="$MAIN_ROOT/.claude/.tmp/pr-poll"
|
|
22
|
+
mkdir -p "$STATE_DIR"
|
|
23
|
+
STATE="$STATE_DIR/baseline.txt"
|
|
24
|
+
touch "$STATE"
|
|
25
|
+
|
|
26
|
+
# The base branch is read rather than assumed, since this ships to projects that
|
|
27
|
+
# do not all call it `main`. A wrong base is not a visible failure: merge-tree
|
|
28
|
+
# reports every pull request as conflicted against a ref that does not resolve.
|
|
29
|
+
BASE_REF="$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)"
|
|
30
|
+
if [ -z "$BASE_REF" ]; then
|
|
31
|
+
BASE_REF="origin/$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name 2>/dev/null || echo main)"
|
|
32
|
+
fi
|
|
33
|
+
BASE_BRANCH="${BASE_REF#origin/}"
|
|
34
|
+
|
|
35
|
+
# These four strings are owned elsewhere and pinned here. `claude-pr-review`
|
|
36
|
+
# writes `## Review` and `## Review closed`, and `claude-address-review` writes
|
|
37
|
+
# `## Review response` and `## Rebase`. All three surfaces ship separately, so a
|
|
38
|
+
# heading added in either skill breaks a test here that no check reaches across.
|
|
39
|
+
#
|
|
40
|
+
# Both families match on the first line alone so the two tests stay symmetric.
|
|
41
|
+
# The reply family carries `## Rebase` because a run sent straight to the rebase
|
|
42
|
+
# step posts under a heading deliberately kept outside the `## Review` family.
|
|
43
|
+
# Widening one family without the other is what left the reply test narrow, so
|
|
44
|
+
# a fifth heading is added here beside its sibling.
|
|
45
|
+
JQ_LAST_REVIEWED_HEAD='
|
|
46
|
+
[ .reviews[]
|
|
47
|
+
| select((.body // "") | split("\n")[0] | rtrimstr("\r")
|
|
48
|
+
| . == "## Review" or . == "## Review closed")
|
|
49
|
+
] | last | .commit.oid // empty
|
|
50
|
+
'
|
|
51
|
+
JQ_REPLY_COUNT='
|
|
52
|
+
[ .comments[]
|
|
53
|
+
| select((.body // "") | split("\n")[0] | rtrimstr("\r")
|
|
54
|
+
| . == "## Review response" or . == "## Rebase")
|
|
55
|
+
] | length
|
|
56
|
+
'
|
|
57
|
+
|
|
58
|
+
# A pull request this run could not read keeps the line it had, so the GONE
|
|
59
|
+
# sweep below does not read the gap as a merge and the baseline does not lose
|
|
60
|
+
# the head it already knew. Saying so on stderr is the point: a silent skip is
|
|
61
|
+
# how the failure this script was fixed for went unnoticed for a session.
|
|
62
|
+
carry_forward() {
|
|
63
|
+
local n=$1 reason=$2 old
|
|
64
|
+
old=$(grep "^$n " "$STATE" || true)
|
|
65
|
+
if [ -n "$old" ]; then
|
|
66
|
+
echo "$old"
|
|
67
|
+
echo "poll: #$n $reason, so it keeps its last known state and goes unclassified" >&2
|
|
68
|
+
else
|
|
69
|
+
echo "poll: #$n $reason, and it has no last known state, so it goes unclassified" >&2
|
|
70
|
+
fi
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
snapshot() {
|
|
74
|
+
local numbers n payload head prior resp merges
|
|
75
|
+
git fetch -q origin "$BASE_BRANCH" 2>/dev/null || true
|
|
76
|
+
|
|
77
|
+
# A failed list reaches the caller as no open pull requests, and that reports
|
|
78
|
+
# every tracked one as GONE. It is a louder wrong answer than the one this
|
|
79
|
+
# script was fixed for, so the run aborts rather than classify on it.
|
|
80
|
+
if ! numbers=$(gh pr list --state open --json number --jq '.[].number' 2>/dev/null); then
|
|
81
|
+
echo "poll: the open pull request list could not be read" >&2
|
|
82
|
+
return 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
for n in $numbers; do
|
|
86
|
+
# A query that failed and a pull request with genuinely no reviews both
|
|
87
|
+
# arrive as an empty result. Reading the first as the second reported a
|
|
88
|
+
# reviewed pull request as never reviewed, which routes the re-review to
|
|
89
|
+
# the first-pass branch, where it reports and stops, so the movement sits
|
|
90
|
+
# unreviewed until a person notices. One query per pull request gives that
|
|
91
|
+
# failure a single place to surface.
|
|
92
|
+
if ! payload=$(gh pr view "$n" --json headRefOid,reviews,comments 2>/dev/null); then
|
|
93
|
+
carry_forward "$n" "could not be read"
|
|
94
|
+
continue
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
head=$(jq -r '.headRefOid // empty' <<<"$payload")
|
|
98
|
+
if [ -z "$head" ]; then
|
|
99
|
+
carry_forward "$n" "returned no head"
|
|
100
|
+
continue
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
prior=$(jq -r "$JQ_LAST_REVIEWED_HEAD" <<<"$payload")
|
|
104
|
+
resp=$(jq -r "$JQ_REPLY_COUNT" <<<"$payload")
|
|
105
|
+
|
|
106
|
+
# `gh pr view --json mergeable` reports UNKNOWN until GitHub finishes
|
|
107
|
+
# computing it, which is exactly when a poll asks. merge-tree answers
|
|
108
|
+
# locally against the base this machine has, so it never returns UNKNOWN.
|
|
109
|
+
git fetch -q origin "pull/$n/head" 2>/dev/null || true
|
|
110
|
+
if ! git cat-file -e "${head}^{commit}" 2>/dev/null ||
|
|
111
|
+
! git rev-parse --verify -q "$BASE_REF" >/dev/null; then
|
|
112
|
+
# merge-tree exits non-zero on a ref it cannot resolve as well as on a
|
|
113
|
+
# real conflict, and the two are indistinguishable from its status. Both
|
|
114
|
+
# sides are checked, because an unresolvable base reports every pull
|
|
115
|
+
# request as conflicted rather than one, which is the louder half.
|
|
116
|
+
merges=unknown
|
|
117
|
+
elif git merge-tree --write-tree "$BASE_REF" "$head" >/dev/null 2>&1; then
|
|
118
|
+
merges=clean
|
|
119
|
+
else
|
|
120
|
+
merges=conflict
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
echo "$n $head ${prior:-none} $resp $merges"
|
|
124
|
+
done
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# The reason is printed by whichever branch failed, since this one cannot tell
|
|
128
|
+
# a list query from a parse and naming either would misattribute the other.
|
|
129
|
+
if ! NEW=$(snapshot); then
|
|
130
|
+
echo "poll: nothing is classified this run and the baseline is unchanged" >&2
|
|
131
|
+
exit 1
|
|
132
|
+
fi
|
|
133
|
+
CHANGED=0
|
|
134
|
+
|
|
135
|
+
while read -r n head prior resp merges; do
|
|
136
|
+
[ -z "$n" ] && continue
|
|
137
|
+
old=$(grep "^$n " "$STATE" || true)
|
|
138
|
+
if [ -z "$old" ]; then
|
|
139
|
+
# A pull request first seen here may already carry a pass, when it opened
|
|
140
|
+
# and was reviewed between two runs. Reporting it as new invites a first
|
|
141
|
+
# pass the thread already has.
|
|
142
|
+
if [ "$prior" = "$head" ]; then
|
|
143
|
+
echo "SEEN #$n at ${head:0:7}, first sighting, already covered by a pass"
|
|
144
|
+
elif [ "$merges" = unknown ]; then
|
|
145
|
+
# `unknown` means no merge was attempted, so it is left off the line
|
|
146
|
+
# rather than printed where a verdict belongs.
|
|
147
|
+
echo "OPENED #$n at ${head:0:7}"
|
|
148
|
+
else
|
|
149
|
+
echo "OPENED #$n at ${head:0:7}, $merges against $BASE_BRANCH"
|
|
150
|
+
fi
|
|
151
|
+
CHANGED=1
|
|
152
|
+
continue
|
|
153
|
+
fi
|
|
154
|
+
old_head=$(echo "$old" | cut -d' ' -f2)
|
|
155
|
+
old_resp=$(echo "$old" | cut -d' ' -f4)
|
|
156
|
+
old_merges=$(echo "$old" | cut -d' ' -f5)
|
|
157
|
+
|
|
158
|
+
# A conflict arrives from the base moving, not from the branch, so it is
|
|
159
|
+
# reported on the transition rather than only when the head changes.
|
|
160
|
+
if [ "$merges" = conflict ] && [ "$old_merges" != conflict ]; then
|
|
161
|
+
echo "CONFLICT #$n no longer merges into $BASE_BRANCH"
|
|
162
|
+
CHANGED=1
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
if [ "$head" != "$old_head" ]; then
|
|
166
|
+
if [ "$prior" = "none" ]; then
|
|
167
|
+
echo "MOVED #$n -> ${head:0:7}, never reviewed"
|
|
168
|
+
elif [ "$prior" = "$head" ]; then
|
|
169
|
+
# An out-of-band pass reviewed this head before the poll saw it move, so
|
|
170
|
+
# the range is empty because it is covered rather than because it broke.
|
|
171
|
+
# Without this the force-push branch below claims a rewrite that never
|
|
172
|
+
# happened, which is the class of false report the shebang note names.
|
|
173
|
+
echo "SEEN #$n -> ${head:0:7}, already covered by the last pass"
|
|
174
|
+
else
|
|
175
|
+
# The range needs both commits local, and a force-push leaves the prior
|
|
176
|
+
# one unreachable. Count what resolves and say nothing when it does not,
|
|
177
|
+
# rather than reporting a zero that reads as no new work.
|
|
178
|
+
git fetch -q origin "pull/$n/head" 2>/dev/null || true
|
|
179
|
+
since=$(git log --oneline "${prior}..${head}" 2>/dev/null | wc -l | tr -d ' ' || true)
|
|
180
|
+
if [ -n "$since" ] && [ "$since" != "0" ]; then
|
|
181
|
+
echo "MOVED #$n -> ${head:0:7}, $since commit(s) since your last pass"
|
|
182
|
+
else
|
|
183
|
+
echo "MOVED #$n -> ${head:0:7}, range unresolved, likely force-pushed"
|
|
184
|
+
fi
|
|
185
|
+
fi
|
|
186
|
+
CHANGED=1
|
|
187
|
+
elif [ "$resp" -gt "$old_resp" ]; then
|
|
188
|
+
echo "RESPONSE #$n answered with no new commit"
|
|
189
|
+
CHANGED=1
|
|
190
|
+
fi
|
|
191
|
+
done <<<"$NEW"
|
|
192
|
+
|
|
193
|
+
while read -r n _rest; do
|
|
194
|
+
[ -z "$n" ] && continue
|
|
195
|
+
echo "$NEW" | grep -q "^$n " || {
|
|
196
|
+
echo "GONE #$n merged or closed"
|
|
197
|
+
CHANGED=1
|
|
198
|
+
}
|
|
199
|
+
done <"$STATE"
|
|
200
|
+
|
|
201
|
+
[ "$CHANGED" -eq 0 ] && echo "No movement."
|
|
202
|
+
echo "$NEW" >"$STATE"
|
package/docs/agents/commands.md
CHANGED
|
@@ -28,7 +28,7 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
|
|
|
28
28
|
| `aitk transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
|
|
29
29
|
| `aitk tasks archive` | Move a shipped task off the board, clear its ordering row, and regenerate the index |
|
|
30
30
|
| `aitk tasks validate` | Report board rows whose plan, task file, group, or file set does not hold (`--json`) |
|
|
31
|
-
| `aitk records validate` | Report a
|
|
31
|
+
| `aitk records validate` | Report a session record against the standard governing it, per kind (`--json`) |
|
|
32
32
|
| `aitk comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
|
|
33
33
|
| `aitk context audit` | Report required sections, length, depth, bullet weight, cited paths, provenance, and drift |
|
|
34
34
|
| `aitk claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
|
package/docs/agents/records.md
CHANGED
|
@@ -7,10 +7,11 @@ description: Validating the gitignored session records under .claude/, the per-k
|
|
|
7
7
|
|
|
8
8
|
## Validate
|
|
9
9
|
|
|
10
|
-
`aitk records validate <kind>` reports where a session record and the standard governing it disagree. The
|
|
10
|
+
`aitk records validate <kind>` reports where a session record and the standard governing it disagree. The four kinds are `plans`, `groundwork`, `intake`, and `memory`, each a gitignored folder under `.claude/`.
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
aitk records validate plans
|
|
14
|
+
aitk records validate memory
|
|
14
15
|
aitk records validate intake --json
|
|
15
16
|
```
|
|
16
17
|
|
|
@@ -30,10 +31,13 @@ Nothing fires it automatically. The folders are gitignored, so the standards-aud
|
|
|
30
31
|
| `plans` | A filename that is not `feature-<slug>.md`, a missing `# Feature:` heading, a missing required section, a files-to-touch entry naming no file or saying nothing about one, and a question carrying no suggestion or no answer slot |
|
|
31
32
|
| `groundwork` | A track with no `README.md` or no `01-current-state.md`, a file missing `title` or `description`, a `README.md` with no `date` as `YYYY-MM-DD`, an unnumbered file, and a track holding a decision without its handoff or the reverse |
|
|
32
33
|
| `intake` | A dump with no `00-overview.md`, the same frontmatter and numbering checks, an item missing any of `Problem`, `Fix`, `Worth it`, or `You`, and an item carrying `Open` with no `Suggested` |
|
|
34
|
+
| `memory` | A filename whose prefix names none of the four types, an entry missing `title`, `description`, or `category`, a `category` disagreeing with that prefix, a title repeating the filename, and a rule-bearing body missing a part |
|
|
33
35
|
|
|
34
36
|
The half-closed track is the groundwork check a reader cannot run by eye. A folder holding `06` without `07` reads as closed to anyone scanning filenames while the file a returning session actually opens is absent.
|
|
35
37
|
|
|
36
|
-
The item check skips `00-overview.md` and `99-next-session.md`, since neither holds items and running it over the handoff would report every heading it carries.
|
|
38
|
+
The item check skips `00-overview.md` and `99-next-session.md`, since neither holds items and running it over the handoff would report every heading it carries. The memory walk skips `index.md` for the same reason, since the catalog is generated from its siblings rather than authored as an entry.
|
|
39
|
+
|
|
40
|
+
A memory `category` is compared against the sentence-case form of the filename prefix rather than checked field by field, so one finding covers a prefix outside the four types, a field disagreeing with the prefix, and a casing drift that would open a second group in the catalog. The body check runs on `feedback` and `project` entries alone, because a `user` or `reference` entry is a single sentence by design and has no rule to apply.
|
|
37
41
|
|
|
38
42
|
A plan section opens as a bold label or as an H2 and the check counts both, naming the standard's spelling when it reports one missing. The corpus splits roughly four to one between the two forms, so failing the variant would report nearly every plan on a rule that costs a reader nothing.
|
|
39
43
|
|
|
@@ -51,4 +55,4 @@ Skills branch on the findings rather than on the exit code:
|
|
|
51
55
|
aitk records validate plans --json | jq -r '.findings[] | "\(.kind): \(.subject)"'
|
|
52
56
|
```
|
|
53
57
|
|
|
54
|
-
For the shapes each check enforces, see `.claude/standards/plan.md`, `.claude/standards/groundwork.md`, and `.claude/standards/
|
|
58
|
+
For the shapes each check enforces, see `.claude/standards/plan.md`, `.claude/standards/groundwork.md`, `.claude/standards/intake.md`, and `.claude/standards/memory.md`.
|
package/docs/ai-workflow.md
CHANGED
|
@@ -81,7 +81,7 @@ Before a handoff, the orchestrator checks the plan against the tree rather than
|
|
|
81
81
|
|
|
82
82
|
The plan's shape is fixed by `.claude/standards/plan.md`: the section list, the filename, the lifecycle, and the contract its questions keep. Every question carries a `- Suggested:` line and an empty `- Answer:` slot, and a blank answer accepts the suggestion at execution time. That default is what makes a plan decision-ready in one pass, and it is the opposite of the contract an intake folder keeps, where an empty slot means nobody reached the item.
|
|
83
83
|
|
|
84
|
-
`aitk records validate plans` reports where a plan and that standard disagree: a filename that is not `feature-<slug>.md`, a missing required section, a files-to-touch entry naming no file, and a question carrying a suggestion with no answer slot. The same verb takes `groundwork` and `
|
|
84
|
+
`aitk records validate plans` reports where a plan and that standard disagree: a filename that is not `feature-<slug>.md`, a missing required section, a files-to-touch entry naming no file, and a question carrying a suggestion with no answer slot. The same verb takes `groundwork`, `intake`, and `memory`, which are governed the same way and were unreachable for the same reason. Nothing fires it automatically, because all four folders are gitignored and every check the repository runs reads changed files from git. It reports and never writes, since the folders are per-machine scratch with no history to recover a wrong repair from.
|
|
85
85
|
|
|
86
86
|
A plan that ships is archived, never deleted. `aitk:claude-docs` moves it to `.claude/plans-archive/` and retargets the task file's `Plan:` line at the new location, so a completed task still leads to the reasoning behind it. Both folders are gitignored, which is why a deleted plan had no recovery path. A plan cited by more than one task stays put until the last of them closes, since moving it early would strand every other pointer.
|
|
87
87
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Route .claude/memory/ edits to the memory standard for the filename, frontmatter, body shape, and lifecycle
|
|
3
|
+
paths:
|
|
4
|
+
- '.claude/memory/**'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Memory standards
|
|
8
|
+
|
|
9
|
+
## Routing
|
|
10
|
+
|
|
11
|
+
- Write no memory entry for a fact a per-domain context entry already owns. Memory keeps the residue, which in practice is feedback about how to work.
|
|
12
|
+
|
|
13
|
+
## The pen
|
|
14
|
+
|
|
15
|
+
- Never delete a memory entry. Retire one by moving it to an archive under its own name, since the folder is gitignored and a wrong call has no undo.
|
|
16
|
+
- Never hand-edit `.claude/memory/index.md`. A hook regenerates it from sibling frontmatter.
|
|
17
|
+
|
|
18
|
+
## Authority
|
|
19
|
+
|
|
20
|
+
- Follow `.claude/standards/memory.md` for the filename and type prefix, the frontmatter, the body shape per type, links between entries, and the lifecycle. It is the single source.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erclx/aitk",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.61.0",
|
|
5
5
|
"description": "Infrastructure and quality tooling for developer workflows",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"check:spell": "cspell '**' '.*/**' '.*' --no-progress --color --show-context",
|
|
30
|
-
"format": "prettier --write --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && shfmt --write --indent 2 scripts/ tooling/",
|
|
31
|
-
"check:format": "prettier --check --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && shfmt --diff --indent 2 scripts/ tooling/",
|
|
32
|
-
"check:shell": "find scripts tooling .claude/hooks -name '*.sh' -exec shellcheck --severity=warning {} +",
|
|
30
|
+
"format": "prettier --write --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && shfmt --write --indent 2 scripts/ tooling/ claude/",
|
|
31
|
+
"check:format": "prettier --check --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && shfmt --diff --indent 2 scripts/ tooling/ claude/",
|
|
32
|
+
"check:shell": "find scripts tooling claude .claude/hooks -name '*.sh' -exec shellcheck --severity=warning {} +",
|
|
33
33
|
"check:types": "tsc --noEmit",
|
|
34
34
|
"check:install": "./scripts/core/install-check.sh",
|
|
35
35
|
"test": "bun --bun vitest run",
|
package/src/commands/records.ts
CHANGED
|
@@ -55,6 +55,7 @@ export function register(program: Command): void {
|
|
|
55
55
|
' plans filename, required sections, and the suggested-and-answer contract',
|
|
56
56
|
' groundwork README and current-state files, numbering, dating, and a half-closed track',
|
|
57
57
|
' intake overview file, numbering, dating, and the four bullets every item carries',
|
|
58
|
+
' memory filename and type prefix, frontmatter, and the body shape each type carries',
|
|
58
59
|
'',
|
|
59
60
|
'Exit codes:',
|
|
60
61
|
' 0 every check passed',
|
|
@@ -66,6 +67,7 @@ export function register(program: Command): void {
|
|
|
66
67
|
'',
|
|
67
68
|
'Examples:',
|
|
68
69
|
' aitk records validate plans',
|
|
70
|
+
' aitk records validate memory',
|
|
69
71
|
' aitk records validate intake --json',
|
|
70
72
|
'',
|
|
71
73
|
].join('\n'),
|
package/src/records/validate.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { readdir, readFile } from 'node:fs/promises'
|
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { parseFrontmatter, readField } from '@/indexes/frontmatter'
|
|
5
5
|
|
|
6
|
-
export const RECORD_KINDS = ['plans', 'groundwork', 'intake'] as const
|
|
6
|
+
export const RECORD_KINDS = ['plans', 'groundwork', 'intake', 'memory'] as const
|
|
7
7
|
|
|
8
8
|
export type RecordKind = (typeof RECORD_KINDS)[number]
|
|
9
9
|
|
|
@@ -11,6 +11,7 @@ const FOLDER_BY_KIND: Readonly<Record<RecordKind, string>> = {
|
|
|
11
11
|
plans: join('.claude', 'plans'),
|
|
12
12
|
groundwork: join('.claude', 'groundwork'),
|
|
13
13
|
intake: join('.claude', 'intake'),
|
|
14
|
+
memory: join('.claude', 'memory'),
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -26,6 +27,7 @@ export type ValidateRefusal = (typeof VALIDATE_REFUSALS)[number]
|
|
|
26
27
|
export const FINDING_KINDS = [
|
|
27
28
|
'name-malformed',
|
|
28
29
|
'title-missing',
|
|
30
|
+
'title-is-slug',
|
|
29
31
|
'section-missing',
|
|
30
32
|
'entry-unreasoned',
|
|
31
33
|
'suggestion-missing',
|
|
@@ -36,6 +38,7 @@ export const FINDING_KINDS = [
|
|
|
36
38
|
'state-missing',
|
|
37
39
|
'closing-partial',
|
|
38
40
|
'item-incomplete',
|
|
41
|
+
'category-mismatch',
|
|
39
42
|
] as const
|
|
40
43
|
|
|
41
44
|
export type FindingKind = (typeof FINDING_KINDS)[number]
|
|
@@ -559,10 +562,175 @@ async function checkDump(dir: string, slug: string): Promise<Finding[]> {
|
|
|
559
562
|
return [...findings, ...perCluster.flat()]
|
|
560
563
|
}
|
|
561
564
|
|
|
565
|
+
const MEMORY_INDEX = 'index.md'
|
|
566
|
+
const MEMORY_FIELDS = ['title', 'description', 'category'] as const
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* The filename prefix and the `category` field are one fact in two spellings,
|
|
570
|
+
* so the map is the whole type list and the comparison against it is what
|
|
571
|
+
* catches a prefix outside the set, a field disagreeing with the prefix, and a
|
|
572
|
+
* casing drift that would open a second group in the catalog.
|
|
573
|
+
*/
|
|
574
|
+
const CATEGORY_BY_TYPE = {
|
|
575
|
+
feedback: 'Feedback',
|
|
576
|
+
project: 'Project',
|
|
577
|
+
user: 'User',
|
|
578
|
+
reference: 'Reference',
|
|
579
|
+
} as const
|
|
580
|
+
|
|
581
|
+
type MemoryType = keyof typeof CATEGORY_BY_TYPE
|
|
582
|
+
|
|
583
|
+
const MEMORY_TYPES = Object.keys(CATEGORY_BY_TYPE) as readonly MemoryType[]
|
|
584
|
+
|
|
585
|
+
const MEMORY_NAME = /^([a-z]+)-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/
|
|
586
|
+
|
|
587
|
+
/** The two markers a rule-bearing body carries, on top of the rule line itself. */
|
|
588
|
+
const MEMORY_MARKERS = ['**Why:**', '**How to apply:**'] as const
|
|
589
|
+
|
|
590
|
+
function memoryType(value: string): MemoryType | undefined {
|
|
591
|
+
return MEMORY_TYPES.find((type) => type === value)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export function checkMemory(name: string, text: string): Finding[] {
|
|
595
|
+
const findings: Finding[] = []
|
|
596
|
+
const match = MEMORY_NAME.exec(name)
|
|
597
|
+
const named = match ? memoryType(match[1]) : undefined
|
|
598
|
+
|
|
599
|
+
if (!named) {
|
|
600
|
+
findings.push(
|
|
601
|
+
finding(
|
|
602
|
+
'name-malformed',
|
|
603
|
+
name,
|
|
604
|
+
name,
|
|
605
|
+
`is not named <type>-<slug>.md with a type of ${MEMORY_TYPES.join(', ')}.`,
|
|
606
|
+
),
|
|
607
|
+
)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const frontmatter = parseFrontmatter(text)
|
|
611
|
+
const missing = MEMORY_FIELDS.filter(
|
|
612
|
+
(field) => !readField(frontmatter, field),
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
if (missing.length > 0) {
|
|
616
|
+
findings.push(
|
|
617
|
+
finding(
|
|
618
|
+
'frontmatter-incomplete',
|
|
619
|
+
name,
|
|
620
|
+
name,
|
|
621
|
+
`carries no ${missing.join(' and no ')}.`,
|
|
622
|
+
),
|
|
623
|
+
)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Its own kind rather than `title-missing`, which means an absent heading on a
|
|
627
|
+
// plan. One kind covering both leaves a caller filtering the JSON unable to
|
|
628
|
+
// tell a record with no title from one whose title is its own slug.
|
|
629
|
+
if (readField(frontmatter, 'title') === name.replace(/\.md$/, '')) {
|
|
630
|
+
findings.push(
|
|
631
|
+
finding(
|
|
632
|
+
'title-is-slug',
|
|
633
|
+
name,
|
|
634
|
+
name,
|
|
635
|
+
'is titled with its own filename, so the catalog renders a slug where the rule belongs.',
|
|
636
|
+
),
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const category = readField(frontmatter, 'category')
|
|
641
|
+
|
|
642
|
+
// Reported against the prefix alone. A name the prefix rule already failed
|
|
643
|
+
// has no type to compare against, and reporting it twice names one defect as
|
|
644
|
+
// two.
|
|
645
|
+
if (named && category && category !== CATEGORY_BY_TYPE[named]) {
|
|
646
|
+
findings.push(
|
|
647
|
+
finding(
|
|
648
|
+
'category-mismatch',
|
|
649
|
+
name,
|
|
650
|
+
category,
|
|
651
|
+
`is not ${CATEGORY_BY_TYPE[named]}, which the filename prefix declares.`,
|
|
652
|
+
),
|
|
653
|
+
)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return [
|
|
657
|
+
...findings,
|
|
658
|
+
...checkMemoryBody(
|
|
659
|
+
name,
|
|
660
|
+
text.slice(frontmatter?.raw.length ?? 0),
|
|
661
|
+
named ?? category,
|
|
662
|
+
),
|
|
663
|
+
]
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* A `user` or `reference` entry is a single sentence by design, so the markers
|
|
668
|
+
* are checked only where a rule is being stated. The type is read off the
|
|
669
|
+
* prefix, falling back to the category so a misnamed file is still checked
|
|
670
|
+
* against the shape it claims.
|
|
671
|
+
*/
|
|
672
|
+
function checkMemoryBody(
|
|
673
|
+
name: string,
|
|
674
|
+
text: string,
|
|
675
|
+
claimed: string | undefined,
|
|
676
|
+
): Finding[] {
|
|
677
|
+
const type = claimed && memoryType(claimed.toLowerCase())
|
|
678
|
+
if (type !== 'feedback' && type !== 'project') return []
|
|
679
|
+
|
|
680
|
+
const body = linesOutsideFences(text).filter((line) => line.trim().length > 0)
|
|
681
|
+
|
|
682
|
+
const findings: Finding[] = []
|
|
683
|
+
const opening = body[0]
|
|
684
|
+
|
|
685
|
+
if (!opening || MEMORY_MARKERS.some((marker) => opening.startsWith(marker))) {
|
|
686
|
+
findings.push(
|
|
687
|
+
finding(
|
|
688
|
+
'section-missing',
|
|
689
|
+
name,
|
|
690
|
+
'the rule line',
|
|
691
|
+
'is absent, so the entry carries a rationale with no rule to apply.',
|
|
692
|
+
),
|
|
693
|
+
)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
for (const marker of MEMORY_MARKERS) {
|
|
697
|
+
if (!body.some((line) => line.startsWith(marker))) {
|
|
698
|
+
findings.push(
|
|
699
|
+
finding(
|
|
700
|
+
'section-missing',
|
|
701
|
+
name,
|
|
702
|
+
marker,
|
|
703
|
+
`is required on a ${type} entry and the body carries no such line.`,
|
|
704
|
+
),
|
|
705
|
+
)
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return findings
|
|
710
|
+
}
|
|
711
|
+
|
|
562
712
|
function refuse(reason: ValidateRefusal, message: string): ValidateRefused {
|
|
563
713
|
return { ok: false, reason, message }
|
|
564
714
|
}
|
|
565
715
|
|
|
716
|
+
/** The walk for a kind whose records are files in one flat folder. */
|
|
717
|
+
async function validateFiles(
|
|
718
|
+
dir: string,
|
|
719
|
+
kind: RecordKind,
|
|
720
|
+
check: (name: string, text: string) => Finding[],
|
|
721
|
+
skip: (file: string) => boolean = () => false,
|
|
722
|
+
): Promise<ValidateReport> {
|
|
723
|
+
const files = (await listMarkdown(dir)).filter((file) => !skip(file))
|
|
724
|
+
|
|
725
|
+
const perFile = await Promise.all(
|
|
726
|
+
files.map(async (file) =>
|
|
727
|
+
check(file, await readFile(join(dir, file), 'utf8')),
|
|
728
|
+
),
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
return { ok: true, kind, records: files.length, findings: perFile.flat() }
|
|
732
|
+
}
|
|
733
|
+
|
|
566
734
|
/**
|
|
567
735
|
* Reports what every record in one gitignored folder claims against the shape
|
|
568
736
|
* its standard fixes. It writes nothing: the folder is per-machine scratch with
|
|
@@ -578,15 +746,15 @@ export async function validateRecords(
|
|
|
578
746
|
return refuse('no-folder', `No ${kind} folder at ${dir}.`)
|
|
579
747
|
}
|
|
580
748
|
|
|
581
|
-
if (kind === 'plans')
|
|
582
|
-
const files = await listMarkdown(dir)
|
|
583
|
-
const perFile = await Promise.all(
|
|
584
|
-
files.map(async (file) =>
|
|
585
|
-
checkPlan(file, await readFile(join(dir, file), 'utf8')),
|
|
586
|
-
),
|
|
587
|
-
)
|
|
749
|
+
if (kind === 'plans') return validateFiles(dir, kind, checkPlan)
|
|
588
750
|
|
|
589
|
-
|
|
751
|
+
if (kind === 'memory') {
|
|
752
|
+
return validateFiles(
|
|
753
|
+
dir,
|
|
754
|
+
kind,
|
|
755
|
+
checkMemory,
|
|
756
|
+
(file) => file === MEMORY_INDEX,
|
|
757
|
+
)
|
|
590
758
|
}
|
|
591
759
|
|
|
592
760
|
const folders = await listFolders(dir)
|
package/standards/index.md
CHANGED
|
@@ -14,6 +14,7 @@ Reference docs for consistent authoring across the toolkit and target projects.
|
|
|
14
14
|
- [Groundwork reference](groundwork.md): Folder layout, reserved numbering, frontmatter and dating, required file contents, and conventions for a measurement track
|
|
15
15
|
- [Intake reference](intake.md): Folder layout, reserved index number, frontmatter and dating, the item template, the answer contract, and retrieval
|
|
16
16
|
- [Markdown reference](markdown.md): Headings, paragraph and list structure, code spans, punctuation, emphasis, and file references
|
|
17
|
+
- [Memory reference](memory.md): Filename and type prefix, frontmatter, the body shape per type, links between entries, and the lifecycle from write to retire
|
|
17
18
|
- [Plan reference](plan.md): Filename and slug, required sections, the suggested-and-answer contract, and the lifecycle from the live folder to the archive
|
|
18
19
|
- [Prose reference](prose.md): Voice, language, and frontmatter wording for reference markdown
|
|
19
20
|
- [Publish reference](publish.md): Scan run against finished text leaving through a channel no automated check covers
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Memory reference
|
|
3
|
+
description: Filename and type prefix, frontmatter, the body shape per type, links between entries, and the lifecycle from write to retire
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Memory reference
|
|
7
|
+
|
|
8
|
+
Applies to a memory entry at `.claude/memory/<type>-<slug>.md`. One file holds one rule or one fact, written at the end of the session that produced it and read by a session that holds none of it. Which surface owns a given fact is settled before an entry is written at all, and that routing is project policy rather than a shape rule.
|
|
9
|
+
|
|
10
|
+
The folder is gitignored and unbacked. Nothing recovers a deleted entry, which is why the retire step below is a move rather than a cleanup.
|
|
11
|
+
|
|
12
|
+
## Scope
|
|
13
|
+
|
|
14
|
+
Governs a memory entry under `.claude/memory/<type>-<slug>.md`: the filename and its type prefix, the frontmatter, the body shape each type carries, links between entries, and the lifecycle from the first write to retirement.
|
|
15
|
+
|
|
16
|
+
Does not govern:
|
|
17
|
+
|
|
18
|
+
- The per-domain entry a domain fact is routed to instead of memory: `context.md`
|
|
19
|
+
- The feature plan a session executes, and its answer contract: `plan.md`
|
|
20
|
+
- The task file tracking what is being built: `tasks.md`
|
|
21
|
+
- Voice and word choice: `prose.md`
|
|
22
|
+
- Headings, punctuation, and file references: `markdown.md`
|
|
23
|
+
- Which facts a project captures at all, and where the folder sits, which are project policy
|
|
24
|
+
|
|
25
|
+
## What a working entry looks like
|
|
26
|
+
|
|
27
|
+
An entry works when a session holding none of the conversation that produced it can act on the rule from the file alone:
|
|
28
|
+
|
|
29
|
+
- What is the rule or the fact, stated in one line?
|
|
30
|
+
- What happened in a session to earn it, so a later reader can tell whether it still holds?
|
|
31
|
+
- When does it fire next, and what does the reader do at that moment?
|
|
32
|
+
- Does another surface already own this, which would make the entry a duplicate of something sessions trust more?
|
|
33
|
+
|
|
34
|
+
An entry failing these is non-conforming even when it satisfies every shape rule below.
|
|
35
|
+
|
|
36
|
+
## Types
|
|
37
|
+
|
|
38
|
+
Four types, and the type decides both the filename prefix and the body shape.
|
|
39
|
+
|
|
40
|
+
| Type | Holds | Fires on |
|
|
41
|
+
| ----------- | -------------------------------------------------------------------- | ------------------------------------------- |
|
|
42
|
+
| `feedback` | a correction or a confirmed approach governing how the agent works | explicit correction, or the same slip twice |
|
|
43
|
+
| `project` | a decision, constraint, or measured fact not derivable from the code | first disclosure |
|
|
44
|
+
| `user` | role, expertise, responsibilities, or working preferences | first disclosure |
|
|
45
|
+
| `reference` | a pointer to an external system, repository, or channel | first disclosure |
|
|
46
|
+
|
|
47
|
+
Hold a feedback entry to the higher bar. A first-occurrence slip is noise, and a folder that records every one of them buries the rules that were paid for.
|
|
48
|
+
|
|
49
|
+
Write no entry for a fact another surface already owns. A fact about a domain belongs in that domain's entry, where sessions working the domain already read it, and the same fact in memory sits in a folder nothing opens. Memory keeps the residue, which in practice is feedback about how to work.
|
|
50
|
+
|
|
51
|
+
## Filename
|
|
52
|
+
|
|
53
|
+
- Name the file `<type>-<slug>.md`, with `<type>` one of the four above and `<slug>` kebab-case naming the rule rather than the incident.
|
|
54
|
+
- Write one rule per file. Two rules under one slug cannot be retired or promoted separately, and one of them always outlives the other.
|
|
55
|
+
- Keep the prefix and the `category` field the same fact in two spellings. A prefix outside the four types reads as a fifth type to anything grouping the folder, and it silently belongs to none.
|
|
56
|
+
|
|
57
|
+
## Frontmatter
|
|
58
|
+
|
|
59
|
+
Every entry carries all three fields.
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
---
|
|
63
|
+
title: A scope glob wide enough for a correct run can be wide enough for every wrong one
|
|
64
|
+
description: Omit a declaration key whose only passing value admits the whole tree
|
|
65
|
+
category: Project
|
|
66
|
+
---
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `title` (required): the rule stated as a line a reader can act on, in sentence case.
|
|
70
|
+
- `description` (required): one line naming what the entry settles, so a reader scanning the catalog decides whether to open it.
|
|
71
|
+
- `category` (required): the type in sentence case, which is what groups the catalog.
|
|
72
|
+
|
|
73
|
+
Never write the filename stem as the `title`. The stem is a slug, and an entry titled with it reads as an unwritten file in every catalog that renders the field.
|
|
74
|
+
|
|
75
|
+
Quote a `description` opening with a backtick or a colon. An unquoted one fails to parse and takes the whole folder's catalog with it.
|
|
76
|
+
|
|
77
|
+
## Body
|
|
78
|
+
|
|
79
|
+
A `feedback` or `project` body carries three parts in this order. Blank lines between them are optional and the three parts are not.
|
|
80
|
+
|
|
81
|
+
- The rule or the fact, stated in one or two sentences as something to do rather than something that happened.
|
|
82
|
+
- A `**Why:**` line naming the session signal that earned it. This is what a later reader tests the rule against when the tree has moved.
|
|
83
|
+
- A `**How to apply:**` line naming the next moment the rule fires and what to do then.
|
|
84
|
+
|
|
85
|
+
A `user` or `reference` body is a single sentence and carries neither marker. There is no rule to apply and no signal to date, so the two lines would be filler.
|
|
86
|
+
|
|
87
|
+
Capture the pattern rather than the recovery. What was tried, what failed, and who noticed belong to the session that is ending, and a body carrying them is a story where the next reader needs an instruction.
|
|
88
|
+
|
|
89
|
+
## Links
|
|
90
|
+
|
|
91
|
+
Link a related entry as `[[name]]`, where `name` is the target's filename stem without the extension. Link freely: the folder is flat and the links are the only structure it has.
|
|
92
|
+
|
|
93
|
+
- Place links inside the body part they support, not in a list of their own at the end.
|
|
94
|
+
- A link naming an entry nobody has written yet is legal, and it marks a rule worth writing rather than a defect.
|
|
95
|
+
- A bracketed token inside a code span is not a link. Backticked syntax from another language routinely reads as one.
|
|
96
|
+
|
|
97
|
+
## Lifecycle
|
|
98
|
+
|
|
99
|
+
- Check the folder for an entry on the same topic before writing a new one, and update that entry in place when one exists. Two entries on one rule disagree the moment either is edited.
|
|
100
|
+
- Rewrite an entry the tree has moved under rather than appending a second passage narrating the change. A reader cannot tell which of two claims is current.
|
|
101
|
+
- Never delete an entry. Retire one by moving it to an archive under its own name, because the folder is unbacked and a bulk judgment has no undo behind it.
|
|
102
|
+
- Treat the folder as a holding pen rather than a destination. An entry whose rule belongs on a durable surface is promoted there and retired here, and the rest is what the pen is for.
|
|
103
|
+
|
|
104
|
+
The catalog is generated from sibling frontmatter rather than authored. Never hand-edit it, since the next regeneration discards whatever was added by hand.
|
|
105
|
+
|
|
106
|
+
## Anti-patterns
|
|
107
|
+
|
|
108
|
+
- **The domain fact filed as memory.** It reads as a capture and lands in the one folder no session opens while working that domain.
|
|
109
|
+
- **The incident narrative.** A body recounting what went wrong states no rule, so the next reader has to infer one and infers a different one.
|
|
110
|
+
- **The entry titled with its own slug.** Every catalog rendering the field shows a filename where the rule should be.
|
|
111
|
+
- **The duplicate written beside the original.** Two entries on one topic drift, and nothing says which is current.
|
|
112
|
+
- **The first-occurrence capture.** A folder recording every slip buries the rules that repeated.
|
|
113
|
+
- **The entry deleted on retire.** The folder has no history, so the judgment that discarded it cannot be reviewed or reversed.
|
|
114
|
+
|
|
115
|
+
## Template
|
|
116
|
+
|
|
117
|
+
```markdown
|
|
118
|
+
---
|
|
119
|
+
title: <the rule, stated as a line a reader can act on>
|
|
120
|
+
description: <one line naming what the entry settles>
|
|
121
|
+
category: <Feedback|Project|User|Reference>
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
<the rule or fact, in one or two sentences, as something to do>
|
|
125
|
+
|
|
126
|
+
**Why:** <the session signal that earned it, with the measurement where one exists>
|
|
127
|
+
|
|
128
|
+
**How to apply:** <the next moment it fires, and what to do then> See [[related-entry]].
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
A `user` or `reference` entry carries the same frontmatter and a single sentence in place of the three parts.
|
|
@@ -73,11 +73,8 @@
|
|
|
73
73
|
|
|
74
74
|
- Write all memory files to `.claude/memory/`, not `~/.claude/projects/`
|
|
75
75
|
- A fact about a domain goes to that domain's `.claude/context/` entry, not to memory. `claude-memory-capture` routes it there and `claude-docs` folds it in. Memory keeps only what no context entry owns.
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
- Before creating a new memory file, check for an existing one on the same topic. Update rather than duplicate.
|
|
79
|
-
- Give every entry `title`, `description`, and a sentence-case `category`. Never hand-edit `.claude/memory/index.md`. A hook regenerates it from sibling frontmatter.
|
|
80
|
-
- Never delete a memory entry. `claude-memory-review` moves a retired one to `.claude/.tmp/memory-archive/`.
|
|
76
|
+
- Never delete a memory entry. Retire one by moving it to `.claude/.tmp/memory-archive/`. A bulk retire runs through the shell, where no file edit fires a path-scoped rule, and the folder is gitignored with nothing to recover from.
|
|
77
|
+
- Follow `.claude/standards/memory.md` for the filename and type prefix, the frontmatter, the body shape each type carries, and the lifecycle. Run `aitk records validate memory` to check the pen against it.
|
|
81
78
|
|
|
82
79
|
## Scratch
|
|
83
80
|
|