create-claude-cabinet 0.42.0 → 0.43.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/lib/cli.js +2 -9
- package/lib/engagement-setup.js +1 -1
- package/package.json +1 -1
- package/templates/engagement/pib-db-patches/pib-db-schema.sql +1 -1
- package/templates/mux/bin/mux +28 -10
- package/templates/mux/config/worktree-session-health.sh +66 -12
- package/templates/scripts/watchtower-build-context.mjs +4 -3
- package/templates/scripts/watchtower-lib.mjs +54 -0
- package/templates/scripts/watchtower-ring1.mjs +20 -0
- package/templates/scripts/watchtower-ring2.mjs +3 -2
- package/templates/scripts/watchtower-ring3-close.mjs +14 -6
- package/templates/scripts/watchtower-status.sh +11 -2
- package/templates/scripts/watchtower-validate.mjs +1 -0
- package/templates/skills/briefing/SKILL.md +5 -1
- package/templates/skills/debrief/phases/methodology-capture.md +13 -0
- package/templates/skills/inbox/SKILL.md +6 -0
- package/templates/skills/qa-handoff/SKILL.md +192 -0
- package/templates/skills/threads/SKILL.md +14 -8
- package/templates/skills/validate/phases/validators.md +34 -0
- package/templates/watchtower/queue/items/item.json.schema +2 -1
- package/templates/skills/decisions/SKILL.md +0 -13
- package/templates/skills/engagement/SKILL.md +0 -9
- package/templates/skills/engagement-add/SKILL.md +0 -7
- package/templates/skills/engagement-edit/SKILL.md +0 -7
- package/templates/skills/engagement-message/SKILL.md +0 -7
- package/templates/skills/engagement-progress/SKILL.md +0 -7
- package/templates/skills/engagement-status/SKILL.md +0 -7
- package/templates/skills/engagement-sync/SKILL.md +0 -7
package/lib/cli.js
CHANGED
|
@@ -609,15 +609,8 @@ const MODULES = {
|
|
|
609
609
|
templates: [
|
|
610
610
|
'skills/collab-client',
|
|
611
611
|
'skills/collab-consultant',
|
|
612
|
-
'skills/engagement',
|
|
613
|
-
'skills/engagement-progress',
|
|
614
612
|
'skills/engagement-help',
|
|
615
|
-
'skills/engagement-message',
|
|
616
613
|
'skills/engagement-create',
|
|
617
|
-
'skills/engagement-edit',
|
|
618
|
-
'skills/engagement-add',
|
|
619
|
-
'skills/engagement-status',
|
|
620
|
-
'skills/engagement-sync',
|
|
621
614
|
'skills/setup-accounts',
|
|
622
615
|
'skills/guide',
|
|
623
616
|
'engagement',
|
|
@@ -635,7 +628,6 @@ const MODULES = {
|
|
|
635
628
|
'scripts/watchtower-lib.mjs',
|
|
636
629
|
'scripts/watchtower-queue.mjs',
|
|
637
630
|
'skills/inbox',
|
|
638
|
-
'skills/decisions',
|
|
639
631
|
'hooks/watchtower-session-start.sh',
|
|
640
632
|
'scripts/watchtower-build-context.mjs',
|
|
641
633
|
'scripts/watchtower-ring1.mjs',
|
|
@@ -657,6 +649,7 @@ const MODULES = {
|
|
|
657
649
|
'scripts/watchtower-status.sh',
|
|
658
650
|
'skills/briefing',
|
|
659
651
|
'skills/threads',
|
|
652
|
+
'skills/qa-handoff',
|
|
660
653
|
],
|
|
661
654
|
},
|
|
662
655
|
mux: {
|
|
@@ -666,7 +659,7 @@ const MODULES = {
|
|
|
666
659
|
default: false,
|
|
667
660
|
lean: false,
|
|
668
661
|
postInstall: 'mux-setup',
|
|
669
|
-
templates: ['skills/orient/phases/dx-captures.md'],
|
|
662
|
+
templates: ['skills/orient/phases/dx-captures.md', 'skills/dx-feedback'],
|
|
670
663
|
},
|
|
671
664
|
'engagement-server': {
|
|
672
665
|
name: 'Engagement Server',
|
package/lib/engagement-setup.js
CHANGED
|
@@ -177,7 +177,7 @@ function setupEngagement({ dryRun, projectDir } = {}) {
|
|
|
177
177
|
target_fid TEXT,
|
|
178
178
|
packet_id TEXT,
|
|
179
179
|
kind TEXT NOT NULL
|
|
180
|
-
CHECK(kind IN ('client_feedback','status_push','delegation','approval','note','packet_sent')),
|
|
180
|
+
CHECK(kind IN ('client_feedback','status_push','delegation','approval','note','packet_sent','packet_opened')),
|
|
181
181
|
author TEXT NOT NULL,
|
|
182
182
|
verdict TEXT CHECK(verdict IS NULL OR verdict IN ('approve','object','comment','none')),
|
|
183
183
|
body TEXT CHECK(body IS NULL OR length(body) <= 10000),
|
package/package.json
CHANGED
|
@@ -114,7 +114,7 @@ CREATE TABLE IF NOT EXISTS engagement_events (
|
|
|
114
114
|
target_fid TEXT,
|
|
115
115
|
packet_id TEXT,
|
|
116
116
|
kind TEXT NOT NULL
|
|
117
|
-
CHECK(kind IN ('client_feedback','status_push','delegation','approval','note','packet_sent')),
|
|
117
|
+
CHECK(kind IN ('client_feedback','status_push','delegation','approval','note','packet_sent','packet_opened')),
|
|
118
118
|
author TEXT NOT NULL,
|
|
119
119
|
verdict TEXT CHECK(verdict IS NULL OR verdict IN ('approve','object','comment','none')),
|
|
120
120
|
body TEXT CHECK(body IS NULL OR length(body) <= 10000),
|
package/templates/mux/bin/mux
CHANGED
|
@@ -227,19 +227,37 @@ create_worktree() {
|
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
# Copy .claude/ into worktree so CC sees worktree-local paths in
|
|
231
|
-
# system context
|
|
232
|
-
#
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
230
|
+
# Copy .claude/ infra into the worktree so CC sees worktree-local paths in
|
|
231
|
+
# its system context (a symlink makes CC resolve through to the main repo
|
|
232
|
+
# path, leaking it into every Read/Edit/Write). EXCEPT the authored project
|
|
233
|
+
# record — .claude/plans/ and .claude/methodology/ — which is real work that
|
|
234
|
+
# must commit from the worktree. Those stay as normal git-tracked files,
|
|
235
|
+
# left exactly as `git worktree add` checked them out from HEAD. See
|
|
236
|
+
# .claude/rules/artifacts-of-thought.md.
|
|
237
|
+
git -C "$wt_path" ls-files .claude/ 2>/dev/null \
|
|
238
|
+
| grep -vE '^\.claude/(plans|methodology)/' \
|
|
239
|
+
| while IFS= read -r f; do
|
|
240
|
+
git -C "$wt_path" update-index --assume-unchanged "$f" 2>/dev/null || true
|
|
241
|
+
done
|
|
236
242
|
if [[ -d "$proj_path/.claude" ]]; then
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
243
|
+
# Refresh infra subdirs from main, one top-level entry at a time, never
|
|
244
|
+
# the authored-record dirs (leave the worktree's HEAD-checked-out copies).
|
|
245
|
+
local entry name
|
|
246
|
+
for entry in "$proj_path"/.claude/*; do
|
|
247
|
+
[[ -e "$entry" ]] || continue
|
|
248
|
+
name=$(basename "$entry")
|
|
249
|
+
[[ "$name" == "plans" || "$name" == "methodology" ]] && continue
|
|
250
|
+
rm -rf "$wt_path/.claude/$name"
|
|
251
|
+
cp -R "$entry" "$wt_path/.claude/$name"
|
|
252
|
+
done
|
|
253
|
+
# Hide infra from the worktree's git status, but keep authored records
|
|
254
|
+
# visible so new design docs created in a worktree show up and commit.
|
|
240
255
|
local wt_gitdir
|
|
241
256
|
wt_gitdir=$(git -C "$wt_path" rev-parse --git-dir 2>/dev/null)
|
|
242
|
-
[[ -n "$wt_gitdir" ]]
|
|
257
|
+
if [[ -n "$wt_gitdir" ]]; then
|
|
258
|
+
mkdir -p "$wt_gitdir/info"
|
|
259
|
+
{ echo '.claude/*'; echo '!.claude/plans/'; echo '!.claude/methodology/'; } >> "$wt_gitdir/info/exclude"
|
|
260
|
+
fi
|
|
243
261
|
fi
|
|
244
262
|
for f in .mcp.json .claudeignore; do
|
|
245
263
|
[[ -f "$proj_path/$f" ]] && ln -sf "$proj_path/$f" "$wt_path/$f"
|
|
@@ -22,27 +22,78 @@ fi
|
|
|
22
22
|
passes=()
|
|
23
23
|
fails=()
|
|
24
24
|
|
|
25
|
+
# .claude/plans/ and .claude/methodology/ are authored project record — real
|
|
26
|
+
# work that must commit from a worktree. Everything else under .claude/ is
|
|
27
|
+
# regenerated infra (skills, agents, settings) that's copied per worktree and
|
|
28
|
+
# must NOT churn or commit. The helpers below keep that line. See
|
|
29
|
+
# .claude/rules/artifacts-of-thought.md.
|
|
30
|
+
|
|
31
|
+
# Hide .claude/ infra from the worktree's git status, but keep authored records
|
|
32
|
+
# visible so new design docs / plan specs created in a worktree show and commit.
|
|
25
33
|
exclude_claude_dir() {
|
|
26
|
-
local wp="$1"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
local wp="$1" gd
|
|
35
|
+
gd=$(git -C "$wp" rev-parse --git-dir 2>/dev/null) || return 0
|
|
36
|
+
[[ -n "$gd" ]] || return 0
|
|
37
|
+
mkdir -p "$gd/info"
|
|
38
|
+
local ex="$gd/info/exclude"
|
|
39
|
+
touch "$ex"
|
|
40
|
+
# Migrate the old wholesale rule, which hid authored records too.
|
|
41
|
+
if grep -qxF '.claude/' "$ex" 2>/dev/null; then
|
|
42
|
+
sed -i.bak '/^\.claude\/$/d' "$ex" && rm -f "$ex.bak"
|
|
43
|
+
fi
|
|
44
|
+
grep -qxF '.claude/*' "$ex" 2>/dev/null \
|
|
45
|
+
|| printf '%s\n' '.claude/*' '!.claude/plans/' '!.claude/methodology/' >> "$ex"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Clear git's assume-unchanged bit on authored records so worktree edits to
|
|
49
|
+
# plans/methodology actually commit (mux historically set it wholesale).
|
|
50
|
+
unprotect_authored_records() {
|
|
51
|
+
local wp="$1" f
|
|
52
|
+
git -C "$wp" ls-files .claude/plans .claude/methodology 2>/dev/null | while IFS= read -r f; do
|
|
53
|
+
git -C "$wp" update-index --no-assume-unchanged "$f" 2>/dev/null || true
|
|
54
|
+
done
|
|
30
55
|
}
|
|
31
56
|
|
|
32
|
-
#
|
|
57
|
+
# Refresh .claude/ INFRA from main, one top-level entry at a time, never
|
|
58
|
+
# touching the authored-record dirs (preserve the worktree's own tracked
|
|
59
|
+
# plans/ and methodology/, including uncommitted edits).
|
|
60
|
+
sync_claude_infra() {
|
|
61
|
+
local sp="$1" wp="$2" entry name
|
|
62
|
+
mkdir -p "$wp/.claude"
|
|
63
|
+
for entry in "$sp/.claude"/*; do
|
|
64
|
+
[[ -e "$entry" ]] || continue
|
|
65
|
+
name=$(basename "$entry")
|
|
66
|
+
[[ "$name" == "plans" || "$name" == "methodology" ]] && continue
|
|
67
|
+
rm -rf "$wp/.claude/$name"
|
|
68
|
+
cp -R "$entry" "$wp/.claude/$name"
|
|
69
|
+
done
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# 1. .claude/ must be a local copy, not a symlink. Infra is refreshed from
|
|
73
|
+
# main; authored records (plans/methodology) stay as the worktree's own
|
|
74
|
+
# git-tracked files, restored from HEAD if a prior wipe lost them.
|
|
33
75
|
if [[ -L "$wt_path/.claude" ]]; then
|
|
34
76
|
if [[ -d "$proj_path/.claude" ]]; then
|
|
35
77
|
rm -f "$wt_path/.claude"
|
|
36
|
-
|
|
78
|
+
mkdir -p "$wt_path/.claude"
|
|
79
|
+
unprotect_authored_records "$wt_path"
|
|
80
|
+
git -C "$wt_path" checkout -- .claude/plans .claude/methodology 2>/dev/null || true
|
|
81
|
+
sync_claude_infra "$proj_path" "$wt_path"
|
|
37
82
|
exclude_claude_dir "$wt_path"
|
|
38
83
|
passes+=("✓ .claude/ auto-fixed: was symlink, now local copy (path isolation restored)")
|
|
39
84
|
else
|
|
40
85
|
fails+=("⚠ .claude/ is a symlink and main repo has no .claude/ to copy from")
|
|
41
86
|
fi
|
|
42
87
|
elif [[ -d "$wt_path/.claude" ]]; then
|
|
88
|
+
# Heal existing worktrees: un-freeze authored records + migrate the exclude.
|
|
89
|
+
unprotect_authored_records "$wt_path"
|
|
90
|
+
exclude_claude_dir "$wt_path"
|
|
43
91
|
passes+=("✓ .claude/ is a local copy (path isolation intact)")
|
|
44
92
|
elif [[ -d "$proj_path/.claude" ]]; then
|
|
45
|
-
|
|
93
|
+
mkdir -p "$wt_path/.claude"
|
|
94
|
+
unprotect_authored_records "$wt_path"
|
|
95
|
+
git -C "$wt_path" checkout -- .claude/plans .claude/methodology 2>/dev/null || true
|
|
96
|
+
sync_claude_infra "$proj_path" "$wt_path"
|
|
46
97
|
exclude_claude_dir "$wt_path"
|
|
47
98
|
passes+=("✓ .claude/ auto-fixed: was missing, copied from main repo")
|
|
48
99
|
else
|
|
@@ -71,14 +122,17 @@ else
|
|
|
71
122
|
fails+=("⚠ Identity link missing and main project identity not found")
|
|
72
123
|
fi
|
|
73
124
|
|
|
74
|
-
# 4. .claude/ freshness — any file in main newer than the worktree
|
|
125
|
+
# 4. .claude/ INFRA freshness — any infra file in main newer than the worktree
|
|
126
|
+
# copy. Authored records are excluded: they sync via git (merge main), never
|
|
127
|
+
# a file copy that could clobber uncommitted worktree edits.
|
|
75
128
|
if [[ -d "$proj_path/.claude" ]] && [[ -d "$wt_path/.claude" ]] && [[ ! -L "$wt_path/.claude" ]]; then
|
|
76
|
-
stale_count=$(find "$proj_path/.claude" -type f -newer "$wt_path/.claude"
|
|
129
|
+
stale_count=$(find "$proj_path/.claude" -type f -newer "$wt_path/.claude" \
|
|
130
|
+
-not -path '*/.claude/plans/*' -not -path '*/.claude/methodology/*' 2>/dev/null | wc -l | tr -d ' ')
|
|
77
131
|
if [[ "$stale_count" -gt 0 ]]; then
|
|
78
|
-
|
|
79
|
-
cp -R "$proj_path/.claude" "$wt_path/.claude"
|
|
132
|
+
sync_claude_infra "$proj_path" "$wt_path"
|
|
80
133
|
exclude_claude_dir "$wt_path"
|
|
81
|
-
|
|
134
|
+
unprotect_authored_records "$wt_path"
|
|
135
|
+
passes+=("✓ .claude/ infra auto-refreshed ($stale_count file(s) updated; records preserved)")
|
|
82
136
|
else
|
|
83
137
|
passes+=("✓ .claude/ current with main repo")
|
|
84
138
|
fi
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { readFileSync, readdirSync, existsSync, statSync, mkdirSync } from 'fs';
|
|
14
14
|
import { join, resolve, basename } from 'path';
|
|
15
|
+
import { currentCursor } from './watchtower-lib.mjs';
|
|
15
16
|
|
|
16
17
|
const WATCHTOWER_DIR = process.env.WATCHTOWER_DIR
|
|
17
18
|
|| join(process.env.HOME, '.claude-cabinet', 'watchtower');
|
|
@@ -143,7 +144,7 @@ function renderFocalZoom(threads, projectSlug) {
|
|
|
143
144
|
// Cursor level: primary thread for this project, full detail
|
|
144
145
|
if (projectThreads.length > 0) {
|
|
145
146
|
const primary = projectThreads[0];
|
|
146
|
-
const c = primary
|
|
147
|
+
const c = currentCursor(primary);
|
|
147
148
|
lines.push(`**${primary.thread}** (primary)`);
|
|
148
149
|
if (c.what) lines.push(` What: ${c.what}`);
|
|
149
150
|
if (c.why) lines.push(` Why: ${c.why}`);
|
|
@@ -160,7 +161,7 @@ function renderFocalZoom(threads, projectSlug) {
|
|
|
160
161
|
|
|
161
162
|
// Thread level: other project threads, one line each
|
|
162
163
|
for (const t of projectThreads.slice(1)) {
|
|
163
|
-
const what = t.
|
|
164
|
+
const what = currentCursor(t).what || '';
|
|
164
165
|
const age = t.last_updated?.slice(0, 10) || '?';
|
|
165
166
|
lines.push(`${t.thread}: ${what} (${age})`);
|
|
166
167
|
}
|
|
@@ -170,7 +171,7 @@ function renderFocalZoom(threads, projectSlug) {
|
|
|
170
171
|
lines.push('');
|
|
171
172
|
lines.push('Other active threads:');
|
|
172
173
|
for (const t of otherThreads.slice(0, 5)) {
|
|
173
|
-
const what = t.
|
|
174
|
+
const what = currentCursor(t).what || '';
|
|
174
175
|
const proj = t.sessions?.[t.sessions.length - 1]?.project || '?';
|
|
175
176
|
lines.push(` ${t.thread} [${proj}]: ${what}`);
|
|
176
177
|
}
|
|
@@ -66,6 +66,60 @@ export function slugify(text) {
|
|
|
66
66
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Thread cursor history (schema v2)
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// A thread's cursor is no longer a single overwritten object; it is an
|
|
73
|
+
// append-only `cursor_history` array of point-in-time snapshots, one per
|
|
74
|
+
// session that advanced the thread. Each entry is
|
|
75
|
+
// { date, session_id, cursor: { what, why, where_left_off, open_questions,
|
|
76
|
+
// next_steps } }
|
|
77
|
+
// The "current" cursor is always the last entry. Overwriting threw away the
|
|
78
|
+
// journey (symptom → diagnosis → solution → abstraction); the history keeps it.
|
|
79
|
+
// `cursor_history` is the thread's first sibling timeline — the QA-handoff
|
|
80
|
+
// protocol (.claude/plans/qa-handoff-protocol.md) adds a parallel
|
|
81
|
+
// point-in-time event sibling rather than nesting inside the cursor.
|
|
82
|
+
|
|
83
|
+
// migrateThreadCursor — convert a legacy (schema v1) thread object that has a
|
|
84
|
+
// single `cursor` field into the v2 `cursor_history` shape, in place.
|
|
85
|
+
// Idempotent: a thread already carrying `cursor_history` is left alone (beyond
|
|
86
|
+
// stripping any stale `cursor` field and bumping the version). The single
|
|
87
|
+
// migrated entry inherits the date/session_id of the thread's most recent
|
|
88
|
+
// session, since the legacy cursor reflected the latest understanding.
|
|
89
|
+
export function migrateThreadCursor(threadData) {
|
|
90
|
+
if (!threadData || typeof threadData !== 'object') return threadData;
|
|
91
|
+
if (threadData.schema_version === undefined) threadData.schema_version = 1;
|
|
92
|
+
|
|
93
|
+
if (Array.isArray(threadData.cursor_history)) {
|
|
94
|
+
delete threadData.cursor; // drop any stale legacy field
|
|
95
|
+
if (threadData.schema_version < 2) threadData.schema_version = 2;
|
|
96
|
+
return threadData;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const legacy = threadData.cursor;
|
|
100
|
+
const sessions = Array.isArray(threadData.sessions) ? threadData.sessions : [];
|
|
101
|
+
const last = sessions.length ? sessions[sessions.length - 1] : null;
|
|
102
|
+
threadData.cursor_history = legacy
|
|
103
|
+
? [{
|
|
104
|
+
date: last?.date || (threadData.last_updated || '').slice(0, 10) || null,
|
|
105
|
+
session_id: last?.id || null,
|
|
106
|
+
cursor: legacy,
|
|
107
|
+
}]
|
|
108
|
+
: [];
|
|
109
|
+
delete threadData.cursor;
|
|
110
|
+
threadData.schema_version = 2;
|
|
111
|
+
return threadData;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// currentCursor — the most recent cursor snapshot for a thread. Reads the last
|
|
115
|
+
// `cursor_history` entry, falling back to a legacy `cursor` field so consumers
|
|
116
|
+
// stay correct against any un-migrated thread file. Always returns an object.
|
|
117
|
+
export function currentCursor(thread) {
|
|
118
|
+
const hist = thread?.cursor_history;
|
|
119
|
+
if (Array.isArray(hist) && hist.length) return hist[hist.length - 1].cursor || {};
|
|
120
|
+
return thread?.cursor || {};
|
|
121
|
+
}
|
|
122
|
+
|
|
69
123
|
// ---------------------------------------------------------------------------
|
|
70
124
|
// loadBetterSqlite3 — resolve better-sqlite3 from wherever it actually lives
|
|
71
125
|
// ---------------------------------------------------------------------------
|
|
@@ -396,10 +396,30 @@ function countRealUncommitted(wtPath) {
|
|
|
396
396
|
if (/(?:^|[\s/])yarn\.lock$/.test(l)) return false;
|
|
397
397
|
if (/(?:^|[\s/])pnpm-lock\.yaml$/.test(l)) return false;
|
|
398
398
|
if (/(?:^|[\s/])bun\.lockb$/.test(l)) return false;
|
|
399
|
+
if (isMainShadow(l, wtPath)) return false;
|
|
399
400
|
return true;
|
|
400
401
|
}).length;
|
|
401
402
|
}
|
|
402
403
|
|
|
404
|
+
// A worktree whose HEAD predates a file committed to main shows that file as
|
|
405
|
+
// untracked ("?? path") even though main already owns it — a stale-worktree
|
|
406
|
+
// shadow, not work to lose. (The mux/devex false-alarm: scripts/skill-usage.mjs
|
|
407
|
+
// was added to main after devex branched, so it counted as a "real" uncommitted
|
|
408
|
+
// change and blocked the merged branch from ever auto-resolving.) Exclude an
|
|
409
|
+
// untracked file only when main holds the SAME path with byte-identical content
|
|
410
|
+
// — a genuinely new untracked file, or a modified shadow, still counts, so real
|
|
411
|
+
// work-to-lose still alarms.
|
|
412
|
+
function isMainShadow(porcelainLine, wtPath) {
|
|
413
|
+
const m = porcelainLine.match(/^\?\?\s+(.+)$/);
|
|
414
|
+
if (!m) return false;
|
|
415
|
+
let p = m[1].trim();
|
|
416
|
+
if (p.startsWith('"') && p.endsWith('"')) p = p.slice(1, -1);
|
|
417
|
+
const mainBlob = safeExec(`git rev-parse --verify --quiet "main:${p}"`, { cwd: wtPath });
|
|
418
|
+
if (!mainBlob) return false;
|
|
419
|
+
const wtBlob = safeExec(`git hash-object "${p}"`, { cwd: wtPath });
|
|
420
|
+
return !!wtBlob && wtBlob === mainBlob;
|
|
421
|
+
}
|
|
422
|
+
|
|
403
423
|
// Does a worktree-unmerged item belong to this project? Can't trust
|
|
404
424
|
// item.project_path alone — Ring 3 historically attributed items to the
|
|
405
425
|
// worktree path rather than the project root. Resolve ownership through
|
|
@@ -1048,8 +1048,9 @@ async function absorbThreadSessions(config) {
|
|
|
1048
1048
|
continue;
|
|
1049
1049
|
}
|
|
1050
1050
|
|
|
1051
|
-
// Prune old session detail files that are already captured in the thread
|
|
1052
|
-
// The session entry in the thread's sessions array (with
|
|
1051
|
+
// Prune old session detail files that are already captured in the thread's
|
|
1052
|
+
// cursor_history. The session entry in the thread's sessions array (with
|
|
1053
|
+
// transcript pointer) survives.
|
|
1053
1054
|
if (!thread.sessions || thread.sessions.length === 0) continue;
|
|
1054
1055
|
|
|
1055
1056
|
const projects = config.projects || {};
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
atomicWrite, loadConfig, slugify,
|
|
35
35
|
log as _log, logError as _logError,
|
|
36
36
|
getWatchtowerDir, createItem, listPending, loadBetterSqlite3,
|
|
37
|
+
migrateThreadCursor, currentCursor,
|
|
37
38
|
} from './watchtower-lib.mjs';
|
|
38
39
|
|
|
39
40
|
const require = createRequire(import.meta.url);
|
|
@@ -398,7 +399,7 @@ async function threadCapture(compressed, projectSlug, sessionId, summary, transc
|
|
|
398
399
|
if (thread.status === 'active') {
|
|
399
400
|
existingThreads.push({
|
|
400
401
|
id: thread.thread,
|
|
401
|
-
what: thread.
|
|
402
|
+
what: currentCursor(thread).what || '',
|
|
402
403
|
});
|
|
403
404
|
}
|
|
404
405
|
} catch { /* skip malformed */ }
|
|
@@ -475,12 +476,19 @@ Rules:
|
|
|
475
476
|
threadIds.push(threadSlug);
|
|
476
477
|
const threadPath = join(threadsDir, `${threadSlug}.json`);
|
|
477
478
|
|
|
479
|
+
// One cursor snapshot per session that advanced this thread — appended,
|
|
480
|
+
// never overwritten (see migrateThreadCursor / cursor_history in
|
|
481
|
+
// watchtower-lib.mjs). The qa_handoff payload is a future SIBLING of this
|
|
482
|
+
// history, not a field inside the cursor (qa-handoff-protocol.md).
|
|
483
|
+
const cursorEntry = { date, session_id: sessionId, cursor: t.cursor };
|
|
484
|
+
|
|
478
485
|
let threadData;
|
|
479
486
|
if (existsSync(threadPath) && !t.is_new) {
|
|
480
487
|
threadData = JSON.parse(readFileSync(threadPath, 'utf8'));
|
|
481
|
-
// Heal pre-versioning
|
|
482
|
-
|
|
483
|
-
threadData
|
|
488
|
+
// Heal pre-versioning + legacy single-cursor files into cursor_history
|
|
489
|
+
// (watchtower-contracts.md §Schema Versioning).
|
|
490
|
+
migrateThreadCursor(threadData);
|
|
491
|
+
threadData.cursor_history.push(cursorEntry);
|
|
484
492
|
if (t.display_name) threadData.display_name = t.display_name;
|
|
485
493
|
threadData.last_updated = now;
|
|
486
494
|
threadData.sessions.push({
|
|
@@ -493,10 +501,10 @@ Rules:
|
|
|
493
501
|
});
|
|
494
502
|
} else {
|
|
495
503
|
threadData = {
|
|
496
|
-
schema_version:
|
|
504
|
+
schema_version: 2,
|
|
497
505
|
thread: threadSlug,
|
|
498
506
|
display_name: t.display_name || threadSlug,
|
|
499
|
-
|
|
507
|
+
cursor_history: [cursorEntry],
|
|
500
508
|
sessions: [{
|
|
501
509
|
id: sessionId,
|
|
502
510
|
contribution: t.contribution || '',
|
|
@@ -21,6 +21,15 @@ json_field() {
|
|
|
21
21
|
node -p "try{JSON.parse(require('fs').readFileSync('$file','utf8'))${field}}catch{}" 2>/dev/null
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
# Read a field off a thread's CURRENT cursor (last cursor_history entry),
|
|
25
|
+
# falling back to a legacy single `cursor` field for un-migrated thread files.
|
|
26
|
+
# Mirrors currentCursor() in watchtower-lib.mjs. Returns '' when absent.
|
|
27
|
+
current_cursor_field() {
|
|
28
|
+
local file="$1" sub="$2"
|
|
29
|
+
[ -f "$file" ] || return 1
|
|
30
|
+
node -p "try{const t=JSON.parse(require('fs').readFileSync('$file','utf8'));const h=t.cursor_history;const c=(Array.isArray(h)&&h.length?h[h.length-1].cursor:t.cursor)||{};c['$sub']??''}catch{}" 2>/dev/null
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
ts_to_epoch() {
|
|
25
34
|
local ts="${1%%.*}"
|
|
26
35
|
ts="${ts%%Z}"
|
|
@@ -226,9 +235,9 @@ if [ "$VERBOSE" = "--verbose" ] || [ "$VERBOSE" = "-v" ]; then
|
|
|
226
235
|
status=$(json_field "$f" ".status")
|
|
227
236
|
[ "$status" != "active" ] && continue
|
|
228
237
|
slug=$(json_field "$f" ".thread")
|
|
229
|
-
what=$(
|
|
238
|
+
what=$(current_cursor_field "$f" "what" 2>/dev/null || echo "")
|
|
230
239
|
[ "$what" = "undefined" ] && what=""
|
|
231
|
-
left_off=$(
|
|
240
|
+
left_off=$(current_cursor_field "$f" "where_left_off" 2>/dev/null || echo "")
|
|
232
241
|
[ "$left_off" = "undefined" ] && left_off=""
|
|
233
242
|
echo " $slug"
|
|
234
243
|
[ -n "$what" ] && echo " $what"
|
|
@@ -54,6 +54,7 @@ const VALID_CATEGORIES = [
|
|
|
54
54
|
'deferred-trigger', 'routing-decision', 'knowledge-extraction', 'methodology-capture',
|
|
55
55
|
'upstream-friction', 'project-completion', 'completion-review', 'branch-diverged',
|
|
56
56
|
'stale-project', 'pattern-promotion', 'watchtower-health', 'worktree-unmerged',
|
|
57
|
+
'qa-handoff',
|
|
57
58
|
];
|
|
58
59
|
const VALID_ENRICHMENT = ['bare', 'in-progress', 'complete'];
|
|
59
60
|
const VALID_URGENCY = ['urgent', 'normal', 'low'];
|
|
@@ -94,7 +94,11 @@ project's file.
|
|
|
94
94
|
### 1e. Active Threads
|
|
95
95
|
|
|
96
96
|
Read `state/threads/*.json`. For each thread file, parse the JSON and
|
|
97
|
-
collect threads with `status: "active"`.
|
|
97
|
+
collect threads with `status: "active"`. The **current cursor** is the
|
|
98
|
+
last entry of the `cursor_history` array
|
|
99
|
+
(`cursor_history[length-1].cursor`); older thread files may instead carry
|
|
100
|
+
a single `cursor` object — treat that lone object as the current cursor.
|
|
101
|
+
For each active thread, extract from the current cursor:
|
|
98
102
|
- `thread` (slug name)
|
|
99
103
|
- `cursor.what` (one-line description)
|
|
100
104
|
- `cursor.where_left_off` (current state)
|
|
@@ -140,6 +140,19 @@ Structure (flexible — adapt to what the work is):
|
|
|
140
140
|
Format so it could be pasted into a pitch deck section, an about page,
|
|
141
141
|
an investor update, a collaborator email, or a customer FAQ.
|
|
142
142
|
|
|
143
|
+
### 3c. Index the new artifacts (required)
|
|
144
|
+
|
|
145
|
+
These records are version-controlled **project thought-record**, not
|
|
146
|
+
ephemera (see `.claude/rules/artifacts-of-thought.md`). In the same
|
|
147
|
+
change that writes a doc, add an index line for it to
|
|
148
|
+
`.claude/methodology/README.md` — date, title, one-line hook, and a
|
|
149
|
+
link. Create that index file if it doesn't exist yet. An unindexed
|
|
150
|
+
record is an invisible record, and the `/validate`
|
|
151
|
+
`artifacts-of-thought` check fails on any methodology doc git isn't
|
|
152
|
+
tracking — so a doc left unindexed-and-uncommitted will break
|
|
153
|
+
validation. If the project routes output to a custom location (see
|
|
154
|
+
Path override), index it there instead.
|
|
155
|
+
|
|
143
156
|
### 4. Integration with the debrief report
|
|
144
157
|
|
|
145
158
|
At the end of the report phase, surface any methodology artifacts
|
|
@@ -147,6 +147,12 @@ Based on the chosen action:
|
|
|
147
147
|
- `knowledge-extraction` -- run `/cc-remember` to capture the knowledge
|
|
148
148
|
- `methodology-capture` -- run `/cc-remember` to capture the methodology
|
|
149
149
|
- `completion-review` -- close the action in pib-db if confirmed
|
|
150
|
+
- `qa-handoff` -- the post-merge QA work for a merged branch. Read
|
|
151
|
+
`evidence.verification.runtime_needed` and run those named checks on
|
|
152
|
+
main (e2e, manual flow, deploy smoke); if `evidence.publish.needed`,
|
|
153
|
+
prepare the publish/deploy command and stop for the operator. Resolve
|
|
154
|
+
only after the runtime checks pass — never flip a handoff to verified
|
|
155
|
+
on a static claim.
|
|
150
156
|
- `pattern-promotion` -- item has pre-authored options (write/promote/dismiss):
|
|
151
157
|
- **write**: Read `evidence.target_member`, `evidence.target_project_path`,
|
|
152
158
|
and `evidence.pattern_text` from the item. Write the pattern to
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qa-handoff
|
|
3
|
+
description: |
|
|
4
|
+
Package a just-merged worktree session into a QA handoff — the bridge
|
|
5
|
+
between "worktree work is done" and the post-merge work that can only
|
|
6
|
+
happen on main (e2e tests against a live server, npm publish, deploy).
|
|
7
|
+
Writes an operator-level handoff (what merged, what the worktree could
|
|
8
|
+
NOT verify, what hangs on the next step) to the watchtower inbox so it
|
|
9
|
+
surfaces and ages until QA happens. Stage 1 of the QA-handoff protocol:
|
|
10
|
+
this PACKAGES the handoff; it does not yet dispatch it to window 1.
|
|
11
|
+
Use when: "qa-handoff", "/qa-handoff", "hand off to QA", "package the
|
|
12
|
+
handoff", or right after merging a worktree branch into main.
|
|
13
|
+
argument-hint: "optional branch — e.g., (none = most recent merge), 'mux/my-branch'"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# /qa-handoff — Package a merge for post-merge QA
|
|
17
|
+
|
|
18
|
+
Working in a worktree exposes a real gap: a worktree can't do anything
|
|
19
|
+
that depends on the main checkout — e2e tests need a live dev server
|
|
20
|
+
bound to one checkout, `npm publish` needs the main branch, deploy needs
|
|
21
|
+
main. These are all **post-merge** activities, and there's no bridge
|
|
22
|
+
between "the worktree work is merged" and "QA / publish / deploy runs on
|
|
23
|
+
main." This skill builds the handoff that crosses that gap.
|
|
24
|
+
|
|
25
|
+
Full design: `.claude/plans/qa-handoff-protocol.md`. This skill is
|
|
26
|
+
**stage 1** — it packages a merge into a durable, surfaced handoff. The
|
|
27
|
+
dispatch half (mux poking window 1, pane-state detection, the on-disk
|
|
28
|
+
queue) is a later stage; for now the handoff lands in the inbox and is
|
|
29
|
+
surfaced by `/briefing` until the operator runs QA.
|
|
30
|
+
|
|
31
|
+
## The verification contract this enforces
|
|
32
|
+
|
|
33
|
+
A worktree session **cannot** runtime-verify e2e — the dev stack only
|
|
34
|
+
runs from main. So a handoff records two honest states, never one
|
|
35
|
+
overclaimed `verified`:
|
|
36
|
+
|
|
37
|
+
1. **`statically-validated`** — what the worktree DID check without a
|
|
38
|
+
live stack: `node -c` / type-check, unit tests, `cucumber --dry-run`,
|
|
39
|
+
duplicate-step scans, a clean build. Real, but not runtime.
|
|
40
|
+
2. **`runtime-needed`** — what only post-merge QA on main can confirm:
|
|
41
|
+
e2e scenarios against the live server, a manual flow, a deploy smoke
|
|
42
|
+
test. The handoff names these; it never claims them done.
|
|
43
|
+
|
|
44
|
+
The flip to `runtime-verified` is owned by the post-merge QA session
|
|
45
|
+
citing named checks that passed — not by this skill.
|
|
46
|
+
|
|
47
|
+
## When to run
|
|
48
|
+
|
|
49
|
+
Right after you merge a worktree branch into main (this repo's
|
|
50
|
+
convention: Claude merges worktree → main). Run it from the main
|
|
51
|
+
checkout or the worktree you just merged from.
|
|
52
|
+
|
|
53
|
+
## Step 1: Identify what merged
|
|
54
|
+
|
|
55
|
+
Resolve the merge being handed off:
|
|
56
|
+
|
|
57
|
+
- **No argument:** the most recent merge into `main`. Find it with
|
|
58
|
+
`git log --merges -1 --format='%H %s' main`, and the work range with
|
|
59
|
+
`git log main@{1}..main` if a reflog entry exists, else the merged
|
|
60
|
+
branch's commits.
|
|
61
|
+
- **`<branch>` argument:** that branch's contribution —
|
|
62
|
+
`git log --oneline main..<branch>` won't help post-merge (it's 0), so
|
|
63
|
+
use `git log <branch> --not $(git merge-base main <branch>)~1` or the
|
|
64
|
+
merge commit's `^1..^2` range.
|
|
65
|
+
|
|
66
|
+
Collect: the merge/HEAD commit SHA on main, a `--stat` of the diff, and
|
|
67
|
+
the list of changed files. Keep the file list — capability detection and
|
|
68
|
+
the verification split both key off it.
|
|
69
|
+
|
|
70
|
+
## Step 2: Pull the acceptance criteria
|
|
71
|
+
|
|
72
|
+
The handoff's value is naming what still needs checking. Gather the AC
|
|
73
|
+
from the work:
|
|
74
|
+
|
|
75
|
+
1. Scan the merged commit messages for `act:` / `prj:` fids.
|
|
76
|
+
2. For each, read the action from pib-db (`pib_get_action` MCP, or
|
|
77
|
+
`node scripts/pib-db.mjs get <fid>`) and extract its Acceptance
|
|
78
|
+
Criteria / Verify Plan.
|
|
79
|
+
3. Mark each AC `statically-validated` (the worktree proved it without a
|
|
80
|
+
live stack) or `runtime-needed` (only post-merge QA can confirm).
|
|
81
|
+
When unsure, default to `runtime-needed` — overclaiming is the
|
|
82
|
+
failure this protocol exists to prevent.
|
|
83
|
+
|
|
84
|
+
If no fids are referenced, derive the criteria from the diff itself —
|
|
85
|
+
what a reviewer on main would want to confirm still works.
|
|
86
|
+
|
|
87
|
+
## Step 3: Detect capabilities (what post-merge work this project supports)
|
|
88
|
+
|
|
89
|
+
The handoff degrades to what the project can actually do. Detect from
|
|
90
|
+
the repo root:
|
|
91
|
+
|
|
92
|
+
- **e2e:** an `e2e/` dir, `cucumber.*`/`*.feature` files, a Playwright
|
|
93
|
+
config, or `cabinet-verify` in package.json → e2e applies.
|
|
94
|
+
- **publish:** a publishable npm package (package.json, not `private:
|
|
95
|
+
true`) or a `/cc-publish` skill present → publish applies.
|
|
96
|
+
- **deploy:** `railway.toml` → Railway; `fly.toml` → Fly; `vercel.json`
|
|
97
|
+
/ `.vercel/` → Vercel; `netlify.toml` → Netlify; else none.
|
|
98
|
+
|
|
99
|
+
A project with none of these gets a handoff that's purely "here's what
|
|
100
|
+
merged, here's what to eyeball" — that's fine; silence is better than a
|
|
101
|
+
fake gate.
|
|
102
|
+
|
|
103
|
+
## Step 4: Compose the handoff payload
|
|
104
|
+
|
|
105
|
+
Build one operator-level object. Reuse the thread-cursor's voice (plain
|
|
106
|
+
English, what a colleague needs to pick this up) — this is the QA
|
|
107
|
+
sibling of the cursor, a point-in-time event, not a field on it:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"merged_branch": "mux/<branch>",
|
|
112
|
+
"merged_commit": "<sha on main>",
|
|
113
|
+
"merged_into": "main",
|
|
114
|
+
"date": "YYYY-MM-DD",
|
|
115
|
+
"session_id": "<this session's id, if known>",
|
|
116
|
+
"what": "One line: what this work was.",
|
|
117
|
+
"why": "Why it happened — the motivation.",
|
|
118
|
+
"what_merged": ["The substantive changes, a few bullets."],
|
|
119
|
+
"could_not_verify": [
|
|
120
|
+
"What the worktree could not runtime-verify and post-merge QA must — be specific (which e2e scenario, which flow)."
|
|
121
|
+
],
|
|
122
|
+
"hangs_on": [
|
|
123
|
+
"What the next step depends on — a publish decision, a deploy, a follow-up act: fid."
|
|
124
|
+
],
|
|
125
|
+
"acceptance_criteria": [
|
|
126
|
+
{"fid": "act:xxxxxxxx", "text": "...", "state": "statically-validated | runtime-needed"}
|
|
127
|
+
],
|
|
128
|
+
"capabilities": {"e2e": true, "publish": true, "deploy": "railway | fly | vercel | netlify | none"},
|
|
129
|
+
"verification": {
|
|
130
|
+
"statically_validated": ["What the worktree DID verify."],
|
|
131
|
+
"runtime_needed": ["What main must verify, by name."]
|
|
132
|
+
},
|
|
133
|
+
"publish": {"needed": true, "command": "the exact publish/deploy command, prepared not run"}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Keep `could_not_verify` and `hangs_on` honest and short. An empty
|
|
138
|
+
`runtime_needed` with `publish.needed: false` is a valid handoff — it
|
|
139
|
+
just says "merged, nothing hangs on it."
|
|
140
|
+
|
|
141
|
+
## Step 5: File the handoff to the inbox
|
|
142
|
+
|
|
143
|
+
File it as a `qa-handoff` inbox item. Urgency is `normal` by default —
|
|
144
|
+
reserve `urgent` for a handoff that genuinely blocks (a publish the
|
|
145
|
+
operator is waiting on). Better dark than wrong: a wall of urgent
|
|
146
|
+
handoffs trains the operator to ignore them.
|
|
147
|
+
|
|
148
|
+
Write the payload to a temp file, then file via the installed queue API:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
WT="${WATCHTOWER_DIR:-$HOME/.claude-cabinet/watchtower}"
|
|
152
|
+
# 1. Write the createItem args to /tmp/qa-handoff-item.json:
|
|
153
|
+
# { project, project_path, category: "qa-handoff", urgency,
|
|
154
|
+
# title, summary, context_anchor, evidence: <the payload above>,
|
|
155
|
+
# thread_ids: [<linked thread slugs, if known>],
|
|
156
|
+
# filed_by: "manual" }
|
|
157
|
+
node --input-type=module -e '
|
|
158
|
+
import { readFileSync } from "fs";
|
|
159
|
+
import { createItem } from "'"$WT"'/scripts/watchtower-queue.mjs";
|
|
160
|
+
const args = JSON.parse(readFileSync(process.argv[1], "utf8"));
|
|
161
|
+
console.log("Filed qa-handoff item:", createItem(args));
|
|
162
|
+
' /tmp/qa-handoff-item.json
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- `title`: `QA handoff: <what>, merged <short-sha>`
|
|
166
|
+
- `summary`: one line — what merged and what QA must confirm.
|
|
167
|
+
- `context_anchor`: `git show <merged_commit>` (so QA can reconstruct).
|
|
168
|
+
- `thread_ids`: the thread slug(s) this work advanced, if you know them
|
|
169
|
+
(links the handoff to its cursor history — the sibling relationship).
|
|
170
|
+
Ring 3 mints threads at session close, so a fresh thread may not exist
|
|
171
|
+
yet; leave `[]` rather than guessing.
|
|
172
|
+
|
|
173
|
+
## Step 6: Report to the operator
|
|
174
|
+
|
|
175
|
+
Tell the operator, plainly:
|
|
176
|
+
- What was handed off (the `what` + merged commit).
|
|
177
|
+
- What QA on main needs to do — the `runtime_needed` list, named.
|
|
178
|
+
- Whether a publish/deploy is staged and waiting on their go.
|
|
179
|
+
- That it's in the inbox and `/briefing` will resurface it until acted
|
|
180
|
+
on.
|
|
181
|
+
|
|
182
|
+
Do **not** run the publish or deploy. This skill prepares and stops;
|
|
183
|
+
promotion is an explicit operator decision (and, later, a dispatched
|
|
184
|
+
window-1 step).
|
|
185
|
+
|
|
186
|
+
## Scope boundary (stage 1)
|
|
187
|
+
|
|
188
|
+
This skill does NOT poke window 1, detect pane state, or auto-run QA.
|
|
189
|
+
Those are the dispatch stages in `.claude/plans/qa-handoff-protocol.md`.
|
|
190
|
+
What it guarantees today: every merge can be packaged into one honest,
|
|
191
|
+
surfaced, ageable handoff that names what could not be verified from the
|
|
192
|
+
worktree — so nothing silently ships unverified.
|
|
@@ -50,11 +50,15 @@ Pure filesystem — no API calls. Read every `*.json` under:
|
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
Ignore any `threads-test/` directory (fixtures). For each thread, you have:
|
|
53
|
-
`thread` (slug), `display_name` (optional, falls back to slug),
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
`thread` (slug), `display_name` (optional, falls back to slug),
|
|
54
|
+
`cursor_history` (append-only array of `{date, session_id, cursor}`
|
|
55
|
+
snapshots — the CURRENT cursor is the last entry's `cursor`, carrying
|
|
56
|
+
`what`, `why`, `where_left_off`, `open_questions`, `next_steps`; the
|
|
57
|
+
earlier entries are the trail of how understanding evolved), `sessions`
|
|
58
|
+
(append-only log, each with `id`, `date`, `project`, `summary`,
|
|
56
59
|
`transcript`), `related_fids`, `lineage` (optional), `last_updated`,
|
|
57
|
-
`status`.
|
|
60
|
+
`status`. Older thread files may still carry a single `cursor` object
|
|
61
|
+
instead of `cursor_history` — treat that lone object as the current cursor.
|
|
58
62
|
|
|
59
63
|
If the directory is missing or empty, say so plainly ("No threads
|
|
60
64
|
captured yet — watchtower's Ring 3 writes these at session close") and
|
|
@@ -87,9 +91,11 @@ in the output. (This is the house style — see the memory
|
|
|
87
91
|
### Arc mode (`arc <slug>`)
|
|
88
92
|
|
|
89
93
|
Tell the story of one thread across its `sessions` in order: how the
|
|
90
|
-
framing (`what`) evolved
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
framing (`what`) evolved — `cursor_history` records this directly, one
|
|
95
|
+
snapshot per session, so walk the entries in sequence rather than
|
|
96
|
+
inferring the arc — the turning points, where it stands now, and its
|
|
97
|
+
`lineage` if present (what it grew out of, what it merged into). End
|
|
98
|
+
with the current cursor's open questions and next steps.
|
|
93
99
|
|
|
94
100
|
### Connections mode
|
|
95
101
|
|
|
@@ -124,7 +130,7 @@ any of these smells:
|
|
|
124
130
|
- **Drift** — a `display_name`/`what` that no longer matches what the
|
|
125
131
|
recent sessions are actually about.
|
|
126
132
|
- **Orphans** — sessions referenced but missing transcripts, or threads
|
|
127
|
-
with an empty
|
|
133
|
+
with an empty `cursor_history`.
|
|
128
134
|
|
|
129
135
|
If the threads look well-segmented, say so in one line. If they don't,
|
|
130
136
|
name the specific threads and suggest the fix is a re-sort
|
|
@@ -147,6 +147,40 @@ values (must be high|moderate|info), invalid tags (must be run|review).
|
|
|
147
147
|
Exits 0 silently if qa-dimensions.yaml doesn't exist (the checklist
|
|
148
148
|
engine is opt-in).
|
|
149
149
|
|
|
150
|
+
### artifacts-of-thought
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo "not a git work tree — skipping"; exit 0; }
|
|
154
|
+
untracked=0
|
|
155
|
+
for dir in .claude/methodology .claude/plans; do
|
|
156
|
+
[ -d "$dir" ] || continue
|
|
157
|
+
for f in "$dir"/*.md; do
|
|
158
|
+
[ -e "$f" ] || continue
|
|
159
|
+
if ! git ls-files --error-unmatch "$f" >/dev/null 2>&1; then
|
|
160
|
+
echo "UNTRACKED: $f"
|
|
161
|
+
untracked=$((untracked + 1))
|
|
162
|
+
fi
|
|
163
|
+
done
|
|
164
|
+
done
|
|
165
|
+
if [ "$untracked" -gt 0 ]; then
|
|
166
|
+
echo ""
|
|
167
|
+
echo "$untracked design record(s)/plan(s) are not tracked by git."
|
|
168
|
+
echo "These are project thought-record, not ephemera. Commit them, or if"
|
|
169
|
+
echo "one is genuinely disposable, move it out of these dirs. Never resolve"
|
|
170
|
+
echo "this by gitignoring the directory — see .claude/rules/artifacts-of-thought.md."
|
|
171
|
+
exit 1
|
|
172
|
+
fi
|
|
173
|
+
echo "All methodology records and plans are tracked."
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Catches the silent-loss failure mode: a design record in
|
|
177
|
+
`.claude/methodology/` or a plan in `.claude/plans/` that git isn't
|
|
178
|
+
tracking — because it was just written and not committed, or because the
|
|
179
|
+
directory was gitignored (which un-tracks every *new* doc while older
|
|
180
|
+
committed ones survive, masking the problem). `README.md` indexes are
|
|
181
|
+
tracked, so they don't flag. Skips silently outside a git work tree or
|
|
182
|
+
when neither directory exists.
|
|
183
|
+
|
|
150
184
|
## Example Validators (commented — enable for your project)
|
|
151
185
|
|
|
152
186
|
<!--
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"enum": [
|
|
34
34
|
"deferred-trigger", "routing-decision", "knowledge-extraction", "methodology-capture",
|
|
35
35
|
"upstream-friction", "project-completion", "completion-review", "branch-diverged",
|
|
36
|
-
"stale-project", "pattern-promotion", "watchtower-health", "worktree-unmerged"
|
|
36
|
+
"stale-project", "pattern-promotion", "watchtower-health", "worktree-unmerged",
|
|
37
|
+
"qa-handoff"
|
|
37
38
|
]
|
|
38
39
|
},
|
|
39
40
|
"urgency": {
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: decisions
|
|
3
|
-
description: "Redirect: use /inbox instead"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# /decisions → /inbox
|
|
7
|
-
|
|
8
|
-
This skill has been renamed to `/inbox`. Run `/inbox` instead.
|
|
9
|
-
|
|
10
|
-
The watchtower inbox holds items that background rings noticed and want
|
|
11
|
-
your attention on — knowledge to route, completion candidates, methodology,
|
|
12
|
-
and friction reports. `/decisions` was misleading because the items aren't
|
|
13
|
-
decisions awaiting judgment; they're incoming signals awaiting triage.
|