create-claude-cabinet 0.52.0 → 0.53.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 -2
- package/package.json +1 -1
- package/templates/CLAUDE.md +164 -20
- package/templates/cabinet/watchtower-contracts.md +202 -10
- package/templates/mux/bin/mux +5 -0
- package/templates/mux/config/mux-server.py +7 -5
- package/templates/scripts/__tests__/category-expiry.test.mjs +130 -0
- package/templates/scripts/__tests__/detector-registry.test.mjs +41 -7
- package/templates/scripts/__tests__/extraction-classification.test.mjs +87 -0
- package/templates/scripts/__tests__/friction-actionability.test.mjs +180 -0
- package/templates/scripts/__tests__/memory-budget-debt.test.mjs +264 -0
- package/templates/scripts/__tests__/ring2-pattern-gate.test.mjs +315 -0
- package/templates/scripts/__tests__/ring2-roster-review.test.mjs +75 -16
- package/templates/scripts/__tests__/ring3-completion-filter.test.mjs +167 -105
- package/templates/scripts/__tests__/roster-ambient-render.test.mjs +132 -0
- package/templates/scripts/__tests__/routine-dispatch.test.mjs +29 -1
- package/templates/scripts/__tests__/routine-moment-expiry.test.mjs +192 -0
- package/templates/scripts/skill-validator.sh +7 -4
- package/templates/scripts/watchtower-lib.mjs +58 -5
- package/templates/scripts/watchtower-queue.mjs +54 -7
- package/templates/scripts/watchtower-ring1.mjs +74 -0
- package/templates/scripts/watchtower-ring2.mjs +442 -81
- package/templates/scripts/watchtower-ring3-close.mjs +271 -82
- package/templates/scripts/watchtower-routines.mjs +107 -1
- package/templates/skills/cabinet-anthropic-insider/SKILL.md +35 -2
- package/templates/skills/cabinet-anti-confirmation/SKILL.md +24 -0
- package/templates/skills/cabinet-cc-health/SKILL.md +22 -0
- package/templates/skills/cabinet-data-integrity/SKILL.md +19 -0
- package/templates/skills/cabinet-debugger/SKILL.md +36 -0
- package/templates/skills/cabinet-deployment/SKILL.md +22 -0
- package/templates/skills/cabinet-elegance/SKILL.md +12 -0
- package/templates/skills/cabinet-goal-alignment/SKILL.md +11 -0
- package/templates/skills/cabinet-historian/SKILL.md +33 -0
- package/templates/skills/cabinet-organized-mind/SKILL.md +11 -0
- package/templates/skills/cabinet-process-therapist/SKILL.md +132 -6
- package/templates/skills/cabinet-qa/SKILL.md +63 -0
- package/templates/skills/cabinet-roster-check/SKILL.md +12 -0
- package/templates/skills/cabinet-workflow-cop/SKILL.md +60 -0
|
@@ -994,14 +994,14 @@ Rules:
|
|
|
994
994
|
// Phase 2c: Work item closure
|
|
995
995
|
// ---------------------------------------------------------------------------
|
|
996
996
|
|
|
997
|
+
// `sessionStartIso` is accepted for call-site compatibility and is no longer
|
|
998
|
+
// read: act:9eebbac4's create-vs-complete day-window filter — the only consumer
|
|
999
|
+
// of the session's own date — was deleted in act:ea23b3a5 after measurement
|
|
1000
|
+
// showed it suppressed the highest-precision cohort and exempted the lowest.
|
|
1001
|
+
// See the calibration note on completionReviewEmitGuard.
|
|
997
1002
|
async function workItemClosure(compressed, project, threadIds = [], sessionStartIso = null) {
|
|
998
1003
|
const projectPath = project.path;
|
|
999
1004
|
log('Phase 2c: Work item closure');
|
|
1000
|
-
// The transcript's own day — the create-vs-complete filter compares the
|
|
1001
|
-
// action row's `created` date against the SESSION's date, so a reprocess
|
|
1002
|
-
// of an old transcript filters against that day, never today.
|
|
1003
|
-
const sessionDate = typeof sessionStartIso === 'string' && sessionStartIso
|
|
1004
|
-
? sessionStartIso.slice(0, 10) : null;
|
|
1005
1005
|
|
|
1006
1006
|
// No resolved project directory (unresolved identity) — there is no
|
|
1007
1007
|
// right pib.db to evaluate against; evaluating the RUNNER's would close
|
|
@@ -1058,10 +1058,16 @@ async function workItemClosure(compressed, project, threadIds = [], sessionStart
|
|
|
1058
1058
|
|
|
1059
1059
|
An action that was merely CREATED during this session is NOT completed — creating or filing a work item is not doing it. Use "none" for actions whose only appearance is their own creation, unless the transcript also shows the work itself being done.
|
|
1060
1060
|
|
|
1061
|
-
|
|
1061
|
+
Also return "quote": a VERBATIM span copied from the session transcript that states this work is finished. Copy it exactly — do not paraphrase, summarize, or reconstruct it. Quote from the transcript itself, not from the list of open actions above. If no such span exists, return "" — an empty quote is a valid and useful answer, and inventing one is worse than none.
|
|
1062
|
+
|
|
1063
|
+
Output JSON array: [{"fid":"act:XXXXXXXX","confidence":"high|medium|low|none","evidence":"brief reason","quote":"verbatim span or empty string"}]
|
|
1062
1064
|
Output ONLY the JSON array, no other text.`;
|
|
1063
1065
|
|
|
1064
|
-
|
|
1066
|
+
// The SAME slice is the model's haystack and the quote checker's — captured
|
|
1067
|
+
// once so the two can never drift apart (a quote can only be verified against
|
|
1068
|
+
// the text the model was actually shown).
|
|
1069
|
+
const transcriptSlice = recentSlice(compressed, COMPLETION_TRANSCRIPT_BUDGET);
|
|
1070
|
+
const userMessage = `Open actions:\n${actionList}\n\nSession transcript:\n${transcriptSlice}`;
|
|
1065
1071
|
|
|
1066
1072
|
let evaluations = [];
|
|
1067
1073
|
try {
|
|
@@ -1082,16 +1088,12 @@ Output ONLY the JSON array, no other text.`;
|
|
|
1082
1088
|
|
|
1083
1089
|
// Emit guards (act:ec508dbe): one prepared status re-check statement,
|
|
1084
1090
|
// reused across evaluations, and a per-fid dedup corpus built once from
|
|
1085
|
-
// pending ∪ recently-
|
|
1091
|
+
// pending ∪ recently-terminal completion-review items. Both setups FAIL OPEN
|
|
1086
1092
|
// (distinct logError, empty guard) — never wholesale suppression.
|
|
1087
|
-
// The SELECT also carries `created` for the create-vs-complete filter
|
|
1088
|
-
// (act:9eebbac4): the mechanical "was this action born this session?"
|
|
1089
|
-
// fact — the compressed transcript strips tool_result bodies, so a
|
|
1090
|
-
// transcript scan cannot reliably see creations; the db row can.
|
|
1091
1093
|
let statusStmt = null;
|
|
1092
1094
|
try {
|
|
1093
1095
|
statusStmt = db.prepare(
|
|
1094
|
-
'SELECT status
|
|
1096
|
+
'SELECT status FROM actions WHERE fid = ? AND deleted_at IS NULL');
|
|
1095
1097
|
} catch (e) {
|
|
1096
1098
|
logError(`Phase 2c: could not prepare emit-time status re-check (${e.message}) — failing open`);
|
|
1097
1099
|
}
|
|
@@ -1104,7 +1106,14 @@ Output ONLY the JSON array, no other text.`;
|
|
|
1104
1106
|
...listItems({
|
|
1105
1107
|
project: project.name,
|
|
1106
1108
|
category: 'completion-review',
|
|
1107
|
-
|
|
1109
|
+
// `expired` and `superseded` are load-bearing since act:ea23b3a5 gave
|
|
1110
|
+
// this category a 14d expiry EQUAL to COMPLETION_REVIEW_DEDUP_DAYS:
|
|
1111
|
+
// without them a fid leaves every corpus at exactly the moment its
|
|
1112
|
+
// item expires, and the next session refiles it — forever. That is
|
|
1113
|
+
// Detector Symmetry §2's named failure ("dedup consults only pending
|
|
1114
|
+
// items, so an excluded state that files at all refiles after every
|
|
1115
|
+
// dismissal").
|
|
1116
|
+
statuses: ['resolved', 'dismissed', 'expired', 'superseded'],
|
|
1108
1117
|
since,
|
|
1109
1118
|
}),
|
|
1110
1119
|
];
|
|
@@ -1112,13 +1121,20 @@ Output ONLY the JSON array, no other text.`;
|
|
|
1112
1121
|
logError(`Phase 2c: could not load existing completion-review items (${e.message}) — failing open`);
|
|
1113
1122
|
}
|
|
1114
1123
|
|
|
1124
|
+
// Quote instrumentation (act:ea23b3a5) — counted for EVERY evaluation,
|
|
1125
|
+
// including suppressed ones, because "would a gate have been safe?" is a
|
|
1126
|
+
// question about the items we stop filing as much as the ones we file.
|
|
1127
|
+
const quoteTally = { found: 0, 'not-found': 0, 'absent-or-too-short': 0, 'no-decodable-transcript': 0 };
|
|
1128
|
+
|
|
1115
1129
|
for (const evalItem of evaluations) {
|
|
1116
1130
|
if (!evalItem.fid || evalItem.confidence === 'none') continue;
|
|
1117
1131
|
|
|
1132
|
+
const quoteCheck = verifyCompletionQuote(evalItem.quote, transcriptSlice);
|
|
1133
|
+
quoteTally[quoteCheck.reason] = (quoteTally[quoteCheck.reason] || 0) + 1;
|
|
1134
|
+
|
|
1118
1135
|
const guard = completionReviewEmitGuard(evalItem.fid, {
|
|
1119
1136
|
statusStmt,
|
|
1120
1137
|
existingItems: existingCompletionItems,
|
|
1121
|
-
sessionDate,
|
|
1122
1138
|
confidence: evalItem.confidence,
|
|
1123
1139
|
});
|
|
1124
1140
|
if (!guard.emit) {
|
|
@@ -1147,6 +1163,11 @@ Output ONLY the JSON array, no other text.`;
|
|
|
1147
1163
|
fid: evalItem.fid,
|
|
1148
1164
|
confidence: evalItem.confidence,
|
|
1149
1165
|
reason: evalItem.evidence,
|
|
1166
|
+
// Recorded, never acted on (act:ea23b3a5) — the corpus a future
|
|
1167
|
+
// round needs to judge whether a real evidence gate is buildable.
|
|
1168
|
+
quote: typeof evalItem.quote === 'string' ? evalItem.quote : null,
|
|
1169
|
+
quote_verified: quoteCheck.verified,
|
|
1170
|
+
quote_check: quoteCheck.reason,
|
|
1150
1171
|
closed_by: 'ring3-close',
|
|
1151
1172
|
},
|
|
1152
1173
|
options: [
|
|
@@ -1164,6 +1185,10 @@ Output ONLY the JSON array, no other text.`;
|
|
|
1164
1185
|
|
|
1165
1186
|
db.close();
|
|
1166
1187
|
log(`Phase 2c: ${queued} completion candidates queued for review${skipped ? ` (${skipped} skipped by emit guards)` : ''}`);
|
|
1188
|
+
// Positive confirmation, not silence: a run with no verified quotes must be
|
|
1189
|
+
// distinguishable from a run where the instrumentation never fired.
|
|
1190
|
+
log(`Phase 2c: quote check (instrumentation only, gates nothing) — `
|
|
1191
|
+
+ Object.entries(quoteTally).map(([k, n]) => `${k}=${n}`).join(', '));
|
|
1167
1192
|
return { closed: 0, queued };
|
|
1168
1193
|
}
|
|
1169
1194
|
|
|
@@ -1676,37 +1701,25 @@ function buildExtractionCorpora(project, { phase } = {}) {
|
|
|
1676
1701
|
// item emits anyway — a DB hiccup degrades to today's behavior, never to
|
|
1677
1702
|
// silent wholesale suppression. The try/catch is the guard's own so a
|
|
1678
1703
|
// throw can't abort the remaining evaluations.
|
|
1679
|
-
//
|
|
1680
|
-
//
|
|
1681
|
-
//
|
|
1682
|
-
//
|
|
1683
|
-
//
|
|
1684
|
-
//
|
|
1685
|
-
//
|
|
1686
|
-
//
|
|
1687
|
-
//
|
|
1688
|
-
//
|
|
1689
|
-
//
|
|
1690
|
-
//
|
|
1691
|
-
// created column, or a thrown lookup all emit.
|
|
1692
|
-
// 2. Per-fid dedup: skip when an existing completion-review item for the
|
|
1693
|
-
// same fid is in `existingItems` (the caller builds that set from
|
|
1694
|
-
// pending ∪ resolved/dismissed-within-COMPLETION_REVIEW_DEDUP_DAYS).
|
|
1704
|
+
// 2. Confidence inversion (act:ea23b3a5): skip when the model rated this fid
|
|
1705
|
+
// 'high'. The full calibration note sits on the check itself. This is the
|
|
1706
|
+
// ONE fail-closed arm in the guard, and it REPLACED act:9eebbac4's
|
|
1707
|
+
// create-vs-complete day-window filter, which encoded the opposite theory
|
|
1708
|
+
// and measured net-negative.
|
|
1709
|
+
// 3. Per-fid dedup: skip when an existing completion-review item for the
|
|
1710
|
+
// same fid is in `existingItems` (the caller builds that set from pending
|
|
1711
|
+
// ∪ resolved/dismissed/expired/superseded within
|
|
1712
|
+
// COMPLETION_REVIEW_DEDUP_DAYS — `expired` and `superseded` are
|
|
1713
|
+
// load-bearing since the category's expiry became 14d, equal to the dedup
|
|
1714
|
+
// window: without them an item drops out of every corpus at exactly the
|
|
1715
|
+
// moment it expires and refiles forever).
|
|
1695
1716
|
//
|
|
1696
|
-
// `
|
|
1697
|
-
//
|
|
1698
|
-
//
|
|
1699
|
-
// untouched (both checks self-skip).
|
|
1717
|
+
// `confidence` is the model's rating for this fid. Every param is optional and
|
|
1718
|
+
// each check self-skips, so callers/tests using the original shape are
|
|
1719
|
+
// untouched.
|
|
1700
1720
|
//
|
|
1701
1721
|
// Returns { emit: true } or { emit: false, reason }.
|
|
1702
|
-
function
|
|
1703
|
-
const t = Date.parse(`${dateStr}T00:00:00Z`);
|
|
1704
|
-
return Number.isFinite(t)
|
|
1705
|
-
? new Date(t + 24 * 60 * 60 * 1000).toISOString().slice(0, 10)
|
|
1706
|
-
: null;
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
function completionReviewEmitGuard(fid, { statusStmt, existingItems = [], sessionDate = null, confidence = null } = {}) {
|
|
1722
|
+
function completionReviewEmitGuard(fid, { statusStmt, existingItems = [], confidence = null } = {}) {
|
|
1710
1723
|
if (statusStmt) {
|
|
1711
1724
|
try {
|
|
1712
1725
|
const row = statusStmt.get(fid);
|
|
@@ -1714,32 +1727,36 @@ function completionReviewEmitGuard(fid, { statusStmt, existingItems = [], sessio
|
|
|
1714
1727
|
if (status !== 'open' && status !== 'in-progress') {
|
|
1715
1728
|
return { emit: false, reason: `status now '${status ?? 'gone'}'` };
|
|
1716
1729
|
}
|
|
1717
|
-
const created = row ? row.created : null;
|
|
1718
|
-
// Day window [sessionDate, sessionDate+1]: pib-db `created` is
|
|
1719
|
-
// day-granular (schema CHECK), so "born this session" is really "born
|
|
1720
|
-
// on the session's day" (+1 covers a past-midnight spill). The upper
|
|
1721
|
-
// bound matters at reprocess: an action created in a LATER session
|
|
1722
|
-
// must not be suppressed by an old transcript's re-run. A sibling
|
|
1723
|
-
// same-day session's creations do fall in the window — the below-high
|
|
1724
|
-
// conjunct is the designed escape (real completion evidence emits).
|
|
1725
|
-
// windowEnd is a REQUIRED conjunct: an unparseable-but-truthy
|
|
1726
|
-
// sessionDate must self-skip the filter (fail OPEN), not silently drop
|
|
1727
|
-
// the upper bound and lexically suppress everything below it.
|
|
1728
|
-
const windowEnd = sessionDate ? dayAfter(sessionDate) : null;
|
|
1729
|
-
if (sessionDate && windowEnd && created
|
|
1730
|
-
&& String(confidence).toLowerCase() !== 'high'
|
|
1731
|
-
&& String(created) >= sessionDate
|
|
1732
|
-
&& String(created) <= windowEnd) {
|
|
1733
|
-
return {
|
|
1734
|
-
emit: false,
|
|
1735
|
-
reason: `created ${created}, within this session's day window (${sessionDate}/+1) — creation is not completion`,
|
|
1736
|
-
};
|
|
1737
|
-
}
|
|
1738
1730
|
} catch (e) {
|
|
1739
1731
|
// FAIL-OPEN: emit despite the failed re-check.
|
|
1740
1732
|
logError(`Phase 2c: emit-time status re-check threw for ${fid} (${e.message}) — failing open, item will emit`);
|
|
1741
1733
|
}
|
|
1742
1734
|
}
|
|
1735
|
+
// CONFIDENCE IS ANTI-PREDICTIVE HERE — measured, not assumed (act:ea23b3a5,
|
|
1736
|
+
// 2026-07-26, over all 223 items this category had ever filed, each joined
|
|
1737
|
+
// against the cited action's OWN project db):
|
|
1738
|
+
//
|
|
1739
|
+
// high 12 acted-on / 28 dismissed-noise 30% precision
|
|
1740
|
+
// medium+low 48 acted-on / 32 dismissed-noise 60% precision
|
|
1741
|
+
//
|
|
1742
|
+
// The model rates 'high' when a session TALKS about an action — files it,
|
|
1743
|
+
// plans it, discusses it — not when it quietly finishes one. act:9eebbac4's
|
|
1744
|
+
// create-vs-complete filter encoded the opposite theory (it exempted 'high'
|
|
1745
|
+
// from the born-this-session skip), so it suppressed the BEST cohort
|
|
1746
|
+
// (same-day creation + medium/low: 74% precision, 20 acted-on / 7 noise)
|
|
1747
|
+
// while passing the worst (same-day + high: 33%). Split at that filter's
|
|
1748
|
+
// 2026-07-12 ship date, the category went 0-for-23 — zero acted-on, 19
|
|
1749
|
+
// dismissed as noise. The filter is deleted; this replaces it.
|
|
1750
|
+
//
|
|
1751
|
+
// CALIBRATION FENCE: this holds for the Phase 2c confidence rubric AS
|
|
1752
|
+
// WRITTEN. Rewording that rubric changes what 'high' means and invalidates
|
|
1753
|
+
// the measurement — re-run it before touching the prompt's confidence text.
|
|
1754
|
+
if (String(confidence).toLowerCase() === 'high') {
|
|
1755
|
+
return {
|
|
1756
|
+
emit: false,
|
|
1757
|
+
reason: "confidence 'high' is anti-predictive for completion (30% vs 60%) — see the calibration note",
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1743
1760
|
const existing = existingItems.find(
|
|
1744
1761
|
i => i.plan_fid === fid || i.evidence?.fid === fid);
|
|
1745
1762
|
if (existing) {
|
|
@@ -1751,6 +1768,78 @@ function completionReviewEmitGuard(fid, { statusStmt, existingItems = [], sessio
|
|
|
1751
1768
|
return { emit: true };
|
|
1752
1769
|
}
|
|
1753
1770
|
|
|
1771
|
+
// ---------------------------------------------------------------------------
|
|
1772
|
+
// Completion-quote verification — INSTRUMENTATION ONLY (act:ea23b3a5)
|
|
1773
|
+
// ---------------------------------------------------------------------------
|
|
1774
|
+
//
|
|
1775
|
+
// Does the model's claimed completion span actually appear in the transcript it
|
|
1776
|
+
// was shown? This records an answer; it deliberately does NOT gate emission,
|
|
1777
|
+
// for two reasons worth keeping written down.
|
|
1778
|
+
//
|
|
1779
|
+
// (1) The haystack is not prose. `preprocessTranscript` ends with
|
|
1780
|
+
// `kept.map(e => JSON.stringify(e)).join('\n')`, so a real newline inside a
|
|
1781
|
+
// message is the two characters `\` + `n`, and a quote character is `\"`. A
|
|
1782
|
+
// substring test against those bytes REJECTS ordinary multi-line prose —
|
|
1783
|
+
// the shape most genuine completion statements take — while reliably
|
|
1784
|
+
// ACCEPTING serialized tool arguments: every TodoWrite call leaves
|
|
1785
|
+
// `{"todos":[{"content":"…","status":"completed"` in the haystack,
|
|
1786
|
+
// pre-escaped and quotable verbatim. So the matcher decodes first, and
|
|
1787
|
+
// `decodeTranscriptText` drops tool_use `input_summary` blobs on purpose.
|
|
1788
|
+
// (2) Even decoded, containment proves the SPAN EXISTS — never that the span
|
|
1789
|
+
// means the work is done. The 2026-07-26 corpus contains a dismissal whose
|
|
1790
|
+
// own reason quotes "act:a587b30f — filed (…)": a verbatim quote of a
|
|
1791
|
+
// FILING. A gate built on this would launder that into evidence.
|
|
1792
|
+
//
|
|
1793
|
+
// So: measure first (the discipline act:de4a7020 stalled on and this program
|
|
1794
|
+
// finally ran). The stamped `evidence.quote_verified` plus the per-session
|
|
1795
|
+
// counts are the data a future round needs to decide whether a real gate —
|
|
1796
|
+
// semantic, not lexical — is worth building.
|
|
1797
|
+
|
|
1798
|
+
const COMPLETION_QUOTE_MIN_CHARS = 20;
|
|
1799
|
+
|
|
1800
|
+
function normalizeQuoteText(s) {
|
|
1801
|
+
return typeof s === 'string' ? s.replace(/\s+/g, ' ').trim().toLowerCase() : '';
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
// decodeTranscriptText — recover human-readable text from a preprocessTranscript
|
|
1805
|
+
// haystack. Each line is one JSON message whose `content` is a string or an
|
|
1806
|
+
// array of blocks; only plain strings and `text` blocks count. Tool-use
|
|
1807
|
+
// summaries are EXCLUDED by design (see note 1 above). An unparseable line is
|
|
1808
|
+
// skipped, never raw-matched — matching the raw line would reintroduce the
|
|
1809
|
+
// escaped-bytes problem this function exists to remove.
|
|
1810
|
+
export function decodeTranscriptText(jsonl) {
|
|
1811
|
+
if (typeof jsonl !== 'string' || !jsonl) return '';
|
|
1812
|
+
const out = [];
|
|
1813
|
+
for (const line of jsonl.split('\n')) {
|
|
1814
|
+
const trimmed = line.trim();
|
|
1815
|
+
if (!trimmed) continue;
|
|
1816
|
+
let entry;
|
|
1817
|
+
try { entry = JSON.parse(trimmed); } catch { continue; }
|
|
1818
|
+
const content = entry && entry.content;
|
|
1819
|
+
if (typeof content === 'string') { out.push(content); continue; }
|
|
1820
|
+
if (!Array.isArray(content)) continue;
|
|
1821
|
+
for (const block of content) {
|
|
1822
|
+
if (typeof block === 'string') out.push(block);
|
|
1823
|
+
else if (block && block.type === 'text' && typeof block.text === 'string') out.push(block.text);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
return out.join('\n');
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// Returns { verified: boolean, reason: 'found' | 'not-found' | 'absent-or-too-short'
|
|
1830
|
+
// | 'no-decodable-transcript' }. Never throws; never affects `emit`.
|
|
1831
|
+
export function verifyCompletionQuote(quote, transcriptJsonl) {
|
|
1832
|
+
const needle = normalizeQuoteText(quote);
|
|
1833
|
+
if (needle.length < COMPLETION_QUOTE_MIN_CHARS) {
|
|
1834
|
+
return { verified: false, reason: 'absent-or-too-short' };
|
|
1835
|
+
}
|
|
1836
|
+
const haystack = normalizeQuoteText(decodeTranscriptText(transcriptJsonl));
|
|
1837
|
+
if (!haystack) return { verified: false, reason: 'no-decodable-transcript' };
|
|
1838
|
+
return haystack.includes(needle)
|
|
1839
|
+
? { verified: true, reason: 'found' }
|
|
1840
|
+
: { verified: false, reason: 'not-found' };
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1754
1843
|
// ---------------------------------------------------------------------------
|
|
1755
1844
|
// Phase 2d: Knowledge extraction → inbox
|
|
1756
1845
|
// ---------------------------------------------------------------------------
|
|
@@ -1795,11 +1884,14 @@ async function decisionExtraction(compressed, project, sessionId, transcriptPath
|
|
|
1795
1884
|
- "pib-db-trigger" = a deferred action with a trigger condition
|
|
1796
1885
|
- "upstream-feedback" = friction with Claude Code itself
|
|
1797
1886
|
- "derivation" = the fact is DERIVABLE (see DERIVABLE below) — its home is the recomputation itself, not a written record
|
|
1887
|
+
- "session-record" = the fact is PERISHABLE (see PERISHABLE below) — its only durable home is the record of the session that observed it, never memory
|
|
1798
1888
|
|
|
1799
1889
|
Only extract items that represent NEW knowledge — things learned or decided in this session that aren't yet captured. Skip items that are routine, obvious, or just restating existing conventions.
|
|
1800
1890
|
|
|
1801
1891
|
DERIVABLE: a fact is derivable when it can be recomputed on demand from a live source rather than remembered — a memory of a derivable fact is a stale cache that goes wrong the moment reality moves on. Examples: "prod is at commit 35a5d15" (recompute: check the deploy log or git log on the remote), "the census run already executed" (recompute: check the run log or the action's status in pib-db), "invoice #1's boundary is 95.25h through July 12" (recompute: re-run the timelog query). When an item is derivable, set "derivable": true and "derivation" to a short instruction for HOW to recompute it (a command, a query, a file to check), and set "home" to "derivation". NEVER omit a derivable item from the output — it is always included, just routed to its derivation instead of memory.
|
|
1802
1892
|
|
|
1893
|
+
PERISHABLE: a fact can be non-derivable and STILL not be durable — true at the moment of this session and destined to become false as work proceeds, with no live source to recompute it from. Examples: "phase 7 hasn't started yet" (false the day phase 7 starts), "usage this month is 42 runs" (a count that moves), "the fix is deployed but not yet verified" (a transition state). A perishable fact is a point-in-time snapshot, NOT a lesson or a durable decision — writing it to memory files a sentence that silently goes wrong. The durability test: would this sentence still be true a month from now if nobody touched anything? A durable lesson would; a snapshot would not. When an item is perishable, set "perishable": true and "perishes_when" to a short statement of WHAT event or time makes it false, and set "home" to "session-record". If a fact is BOTH recomputable and perishable (a deploy SHA is both), classify it DERIVABLE — the recomputation instruction is the better record. NEVER omit a perishable item from the output — it is always included, just routed to the session record instead of memory.
|
|
1894
|
+
|
|
1803
1895
|
SUBJECT: for every item, set "subject" to the actual project, tool, or system the knowledge is ABOUT. This is usually the same as the project you're working in, but flag it when it differs — a lesson about Claude Code itself learned while doing client work, or a lesson about a third-party API or service, belongs to THAT subject, not the filing project.
|
|
1804
1896
|
|
|
1805
1897
|
UNCLASSIFIABLE: if an item is clearly worth surfacing but does not cleanly fit any type or home above, set "type" to "unclassifiable" and "unclassifiable_reason" to a one-line reason why — this makes your uncertainty visible instead of forcing a confident wrong label. Still include "title" and "content"; "home" and "derivable" are not required for an unclassifiable item.
|
|
@@ -1815,7 +1907,7 @@ For each item, assess how time-sensitive routing is. Urgency means HOW FAST THE
|
|
|
1815
1907
|
|
|
1816
1908
|
Lessons and preferences are almost NEVER urgent, no matter how important they are — an important-but-durable item is "normal", not "urgent".
|
|
1817
1909
|
|
|
1818
|
-
Output JSON array: [{"type":"decision|constraint|lesson|preference|unclassifiable","home":"memory|claude-md|pib-db-trigger|upstream-feedback|derivation","urgency":"urgent|normal|low","title":"short title","content":"detailed description","covered_by":"exact already-saved title that covers this, or \\"none\\"","subject":"the actual project, tool, or system this is about","derivable":false,"derivation":null,"unclassifiable_reason":null,"relates_to":"exact pending title that this relates to, or \\"none\\"","relation":"pairs_with|answers|contradicts|none"}]
|
|
1910
|
+
Output JSON array: [{"type":"decision|constraint|lesson|preference|unclassifiable","home":"memory|claude-md|pib-db-trigger|upstream-feedback|derivation|session-record","urgency":"urgent|normal|low","title":"short title","content":"detailed description","covered_by":"exact already-saved title that covers this, or \\"none\\"","subject":"the actual project, tool, or system this is about","derivable":false,"derivation":null,"perishable":false,"perishes_when":null,"unclassifiable_reason":null,"relates_to":"exact pending title that this relates to, or \\"none\\"","relation":"pairs_with|answers|contradicts|none"}]
|
|
1819
1911
|
Output ONLY the JSON array, no other text. If nothing found, output [].`;
|
|
1820
1912
|
|
|
1821
1913
|
// M1b injection: show the model what's already saved so it self-filters dupes
|
|
@@ -1910,14 +2002,18 @@ Output ONLY the JSON array, no other text. If nothing found, output [].`;
|
|
|
1910
2002
|
}
|
|
1911
2003
|
|
|
1912
2004
|
try {
|
|
1913
|
-
//
|
|
2005
|
+
// Four routing shapes (act:471dd701 N2 + act:a69c21f8 follow-up):
|
|
1914
2006
|
// unclassifiable (model uncertainty made visible, never a forced
|
|
1915
2007
|
// home guess), derivable (routed to its recomputation, never
|
|
1916
|
-
// written to memory),
|
|
2008
|
+
// written to memory), perishable (a point-in-time snapshot routed
|
|
2009
|
+
// to the session record, never written to memory; derivable WINS
|
|
2010
|
+
// when both flags are set — the derivation is the better record),
|
|
2011
|
+
// and the four pre-existing homes.
|
|
1917
2012
|
const isUnclassifiable = item.type === 'unclassifiable';
|
|
1918
2013
|
const isDerivable = item.derivable === true;
|
|
1919
|
-
const
|
|
1920
|
-
const
|
|
2014
|
+
const isPerishable = !isDerivable && item.perishable === true;
|
|
2015
|
+
const home = isDerivable ? 'derivation' : isPerishable ? 'session-record' : item.home;
|
|
2016
|
+
const isMemory = !isUnclassifiable && !isDerivable && !isPerishable && home === 'memory';
|
|
1921
2017
|
|
|
1922
2018
|
let options;
|
|
1923
2019
|
let draftArtifact = null;
|
|
@@ -1933,6 +2029,12 @@ Output ONLY the JSON array, no other text. If nothing found, output [].`;
|
|
|
1933
2029
|
{ key: 'dismiss', label: 'Dismiss' },
|
|
1934
2030
|
];
|
|
1935
2031
|
summary = item.derivation ? `${item.content}\n\nDerivation: ${item.derivation}` : item.content;
|
|
2032
|
+
} else if (isPerishable) {
|
|
2033
|
+
options = [
|
|
2034
|
+
{ key: 'acknowledge', label: 'Acknowledge — point-in-time snapshot, not durable' },
|
|
2035
|
+
{ key: 'dismiss', label: 'Dismiss' },
|
|
2036
|
+
];
|
|
2037
|
+
summary = item.perishes_when ? `${item.content}\n\nPerishes: ${item.perishes_when}` : item.content;
|
|
1936
2038
|
} else if (isMemory) {
|
|
1937
2039
|
options = [
|
|
1938
2040
|
{ key: 'write', label: 'Write to memory' },
|
|
@@ -1962,6 +2064,7 @@ Output ONLY the JSON array, no other text. If nothing found, output [].`;
|
|
|
1962
2064
|
session_id: sessionId,
|
|
1963
2065
|
...(item.subject ? { subject: item.subject } : {}),
|
|
1964
2066
|
...(isDerivable ? { derivable: true, derivation: item.derivation } : {}),
|
|
2067
|
+
...(isPerishable ? { perishable: true, perishes_when: item.perishes_when } : {}),
|
|
1965
2068
|
...(isUnclassifiable ? { unclassifiable_reason: item.unclassifiable_reason } : {}),
|
|
1966
2069
|
},
|
|
1967
2070
|
options,
|
|
@@ -2008,6 +2111,22 @@ Output ONLY the JSON array, no other text. If nothing found, output [].`;
|
|
|
2008
2111
|
// Phase 2e: Audit pattern capture
|
|
2009
2112
|
// ---------------------------------------------------------------------------
|
|
2010
2113
|
|
|
2114
|
+
// Exported so the hermetic suite can assert the exclusions survive edits
|
|
2115
|
+
// (act:09184ad7). Ring 2's shape gate (patternHasRequiredShape in
|
|
2116
|
+
// watchtower-ring2.mjs) enforces the Evidence/Gap half mechanically; the
|
|
2117
|
+
// three exclusions below are prompt-only — the 2026-07-26 consolidation
|
|
2118
|
+
// found all three classes filed as pattern promotions (no-gap positives,
|
|
2119
|
+
// truncated-transcript disclaimers, document echoes), and without the test
|
|
2120
|
+
// they are one refactor away from silently vanishing.
|
|
2121
|
+
export const QUALITY_PATTERN_SYSTEM_PROMPT = `You are analyzing a Claude Code session transcript for recurring quality patterns — issues, gaps, friction, or anti-patterns that surface during any kind of work (coding, debugging, planning, auditing, reviewing). Identify patterns worth learning from: things that keep going wrong, systematic gaps, workflow friction, or quality issues that a team member should watch for in future sessions.
|
|
2122
|
+
|
|
2123
|
+
Only report PROBLEMATIC patterns:
|
|
2124
|
+
- Do NOT emit a section for behavior that was correct — a "no gap" observation is not a pattern.
|
|
2125
|
+
- Do NOT emit observations about the transcript artifact itself (truncated, compressed, or cut off mid-record); those describe the recording, not the work.
|
|
2126
|
+
- Do NOT reproduce document content the session merely read or wrote (status tables, wave ledgers, dependency graphs, checklists, setup recipes) as a pattern — a pattern is a recurring behavioral tendency, not a document excerpt.
|
|
2127
|
+
|
|
2128
|
+
Output as markdown with ## headers for each pattern found. Include **Evidence:** (what you observed) and **Gap:** (what's missing or broken) for each — sections missing either block are discarded downstream. If no meaningful patterns, output "No recurring patterns detected."`;
|
|
2129
|
+
|
|
2011
2130
|
async function qualityPatternCapture(compressed, projectPath) {
|
|
2012
2131
|
log('Phase 2e: Quality pattern capture');
|
|
2013
2132
|
|
|
@@ -2027,7 +2146,7 @@ async function qualityPatternCapture(compressed, projectPath) {
|
|
|
2027
2146
|
}
|
|
2028
2147
|
}
|
|
2029
2148
|
|
|
2030
|
-
const systemPrompt =
|
|
2149
|
+
const systemPrompt = QUALITY_PATTERN_SYSTEM_PROMPT;
|
|
2031
2150
|
|
|
2032
2151
|
const userMessage = triageHistory
|
|
2033
2152
|
? `Session transcript:\n${recentSlice(compressed, 30000)}\n\nAudit triage history:\n${triageHistory.slice(0, 10000)}`
|
|
@@ -2130,19 +2249,47 @@ Output ONLY the JSON object.`,
|
|
|
2130
2249
|
// Phase 2g: Upstream friction
|
|
2131
2250
|
// ---------------------------------------------------------------------------
|
|
2132
2251
|
|
|
2133
|
-
|
|
2252
|
+
// The closed vocabulary of actionable routes (act:ea23b3a5). A friction report
|
|
2253
|
+
// with no named party who could act on it is not a report, it is a mood: five
|
|
2254
|
+
// of the 2026-07-26 dismissals were observations like "Claude made iterative
|
|
2255
|
+
// browser tweaks without writing to file" — true, and addressed to nobody.
|
|
2256
|
+
//
|
|
2257
|
+
// This is Phase 2f's verification gate applied to a second lens. That gate
|
|
2258
|
+
// (act:3f3f9a31) files a methodology capture only when the claimed artifact
|
|
2259
|
+
// verifiably exists on disk, after 11 of 11 unverified captures were dismissed
|
|
2260
|
+
// over three weeks. Same shape here: file only when the finding names its route.
|
|
2261
|
+
export const FRICTION_ROUTES = ['cc-feedback', 'project-tracker', 'config-change'];
|
|
2262
|
+
|
|
2263
|
+
export function frictionRouteIsActionable(route) {
|
|
2264
|
+
return !!route
|
|
2265
|
+
&& FRICTION_ROUTES.includes(route.type)
|
|
2266
|
+
&& typeof route.detail === 'string'
|
|
2267
|
+
&& route.detail.trim().length > 0;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
async function upstreamFriction(compressed, project, threadIds = [], { callFn = claudeCall } = {}) {
|
|
2134
2271
|
const projectPath = project.path;
|
|
2135
2272
|
log('Phase 2g: Upstream friction');
|
|
2136
2273
|
|
|
2137
2274
|
const systemPrompt = `You are analyzing a Claude Code session transcript for friction with Claude Code itself (bugs, limitations, confusing behavior, missing features, workarounds). Only report genuine CC friction, not user errors or project-specific issues.
|
|
2138
2275
|
|
|
2139
|
-
|
|
2276
|
+
For each finding, decide what can actually be DONE about it, and route it:
|
|
2277
|
+
|
|
2278
|
+
- "route": {"type":"cc-feedback","detail":"..."} — a fix belongs in the Claude Cabinet repo. detail names what would change there.
|
|
2279
|
+
- "route": {"type":"project-tracker","detail":"..."} — a fix belongs in THIS project. detail names the work.
|
|
2280
|
+
- "route": {"type":"config-change","detail":"..."} — a setting, hook, or doc the operator controls. detail names it.
|
|
2281
|
+
- "durable": true — no one can fix it (a platform constraint, an environment quirk), but the WORKAROUND is worth remembering next time. Say the workaround in "description".
|
|
2282
|
+
- Neither — a transient incident (a network blip, a credit limit, a one-off tool error) or a general observation about how Claude behaved. These are NOT upstream friction. Return them with no route and durable:false; they will be counted and dropped.
|
|
2283
|
+
|
|
2284
|
+
An observation with no named party who could act on it and nothing durable to remember is not a report. Do not invent a route to make something filable.
|
|
2285
|
+
|
|
2286
|
+
Output JSON: [{"title":"short title","description":"what happened","severity":"high|medium|low","route":{"type":"...","detail":"..."}|null,"durable":true|false}]
|
|
2140
2287
|
If NO friction found, output exactly: []
|
|
2141
2288
|
|
|
2142
2289
|
Be conservative. False positives waste time. Output ONLY the JSON array.`;
|
|
2143
2290
|
|
|
2144
2291
|
try {
|
|
2145
|
-
const response = await
|
|
2292
|
+
const response = await callFn(systemPrompt, recentSlice(compressed, 40000));
|
|
2146
2293
|
const jsonMatch = response.match(/\[[\s\S]*\]/);
|
|
2147
2294
|
if (!jsonMatch) {
|
|
2148
2295
|
log('Phase 2g: No upstream friction (parse failure)');
|
|
@@ -2156,23 +2303,59 @@ Be conservative. False positives waste time. Output ONLY the JSON array.`;
|
|
|
2156
2303
|
return;
|
|
2157
2304
|
}
|
|
2158
2305
|
|
|
2306
|
+
const tally = { routed: 0, durable: 0, dropped: 0 };
|
|
2307
|
+
|
|
2159
2308
|
for (const item of frictionItems) {
|
|
2160
|
-
|
|
2309
|
+
const base = {
|
|
2161
2310
|
project: project.name,
|
|
2162
2311
|
project_path: projectPath,
|
|
2163
2312
|
...(project.unresolved ? { project_unresolved: true } : {}),
|
|
2164
|
-
category: 'upstream-friction',
|
|
2165
|
-
urgency: item.severity === 'high' ? 'urgent' : 'normal',
|
|
2166
|
-
title: item.title,
|
|
2167
|
-
summary: item.description,
|
|
2168
|
-
context_anchor: 'ring3-close friction scan',
|
|
2169
|
-
evidence: { severity: item.severity },
|
|
2170
2313
|
filed_by: 'ring3-close',
|
|
2171
2314
|
thread_ids: threadIds,
|
|
2172
|
-
}
|
|
2315
|
+
};
|
|
2316
|
+
|
|
2317
|
+
if (frictionRouteIsActionable(item.route)) {
|
|
2318
|
+
createItem({
|
|
2319
|
+
...base,
|
|
2320
|
+
category: 'upstream-friction',
|
|
2321
|
+
urgency: item.severity === 'high' ? 'urgent' : 'normal',
|
|
2322
|
+
title: item.title,
|
|
2323
|
+
summary: `${item.description}\n\nRoute: ${item.route.type} — ${item.route.detail}`,
|
|
2324
|
+
context_anchor: 'ring3-close friction scan',
|
|
2325
|
+
evidence: { severity: item.severity, route: item.route },
|
|
2326
|
+
});
|
|
2327
|
+
tally.routed++;
|
|
2328
|
+
continue;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// The third arm, and it comes from the data: of the five friction items
|
|
2332
|
+
// the operator KEPT, four exited as `captured-to-memory` — platform
|
|
2333
|
+
// constraints with a durable workaround ("Claude-in-Chrome cannot connect
|
|
2334
|
+
// while Claude Desktop is running"). Those have no upstream route and
|
|
2335
|
+
// never will, and dropping them would discard the useful half of this
|
|
2336
|
+
// lens. They are knowledge, so they go where knowledge goes.
|
|
2337
|
+
if (item.durable === true) {
|
|
2338
|
+
createItem({
|
|
2339
|
+
...base,
|
|
2340
|
+
category: 'knowledge-extraction',
|
|
2341
|
+
urgency: 'low',
|
|
2342
|
+
title: item.title,
|
|
2343
|
+
summary: item.description,
|
|
2344
|
+
context_anchor: 'ring3-close friction scan (durable constraint, no upstream route)',
|
|
2345
|
+
evidence: { type: 'constraint', home: 'memory', severity: item.severity, source: 'friction-lens' },
|
|
2346
|
+
});
|
|
2347
|
+
tally.durable++;
|
|
2348
|
+
continue;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
tally.dropped++;
|
|
2173
2352
|
}
|
|
2174
2353
|
|
|
2175
|
-
|
|
2354
|
+
// All three counts, always. Silence here would be indistinguishable from
|
|
2355
|
+
// the lens never running — and the drop count is the number this change
|
|
2356
|
+
// exists to move, so it must be observable.
|
|
2357
|
+
log(`Phase 2g: friction — ${tally.routed} routed, ${tally.durable} durable→knowledge, `
|
|
2358
|
+
+ `${tally.dropped} dropped (no route, nothing durable)`);
|
|
2176
2359
|
} catch (e) {
|
|
2177
2360
|
logError(`Phase 2g: Friction detection failed: ${e.message}`);
|
|
2178
2361
|
}
|
|
@@ -3861,6 +4044,12 @@ export {
|
|
|
3861
4044
|
threadCursorLines,
|
|
3862
4045
|
buildExtractionCorpora,
|
|
3863
4046
|
completionReviewEmitGuard,
|
|
4047
|
+
// Exported for the quote-instrumentation suite (act:ea23b3a5): a fixture
|
|
4048
|
+
// must be built by the REAL preprocessor, because its JSON-serialized output
|
|
4049
|
+
// — not plain prose — is what the matcher actually faces.
|
|
4050
|
+
preprocessTranscript,
|
|
4051
|
+
// Friction actionability gate (act:ea23b3a5)
|
|
4052
|
+
upstreamFriction,
|
|
3864
4053
|
discoverSessionAdvisors,
|
|
3865
4054
|
parseAdvisorFindings,
|
|
3866
4055
|
advisorPass,
|