portable-agent-layer 0.47.0 → 0.49.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/README.md +1 -0
- package/assets/STATUSLINE.md +102 -0
- package/assets/skills/create-skill/SKILL.md +6 -1
- package/assets/statusline.ps1 +142 -0
- package/assets/statusline.sh +167 -0
- package/assets/templates/PAL/ALGORITHM.md +16 -1
- package/assets/templates/settings.claude.json +21 -0
- package/package.json +1 -1
- package/src/cli/index.ts +2 -0
- package/src/cli/skill.ts +18 -3
- package/src/hooks/lib/algorithm-review.ts +85 -0
- package/src/hooks/lib/context.ts +4 -0
- package/src/hooks/lib/learning-store.ts +45 -0
- package/src/hooks/lib/paths.ts +5 -0
- package/src/hooks/lib/retrieval-index.ts +32 -5
- package/src/hooks/lib/retrieval.ts +2 -1
- package/src/hooks/lib/semi-static.ts +29 -6
- package/src/targets/claude/install.ts +9 -1
- package/src/targets/claude/uninstall.ts +9 -1
- package/src/targets/lib.ts +216 -0
- package/src/tools/agent/algorithm-reflect.ts +10 -5
- package/src/tools/agent/algorithm-synthesize.ts +218 -0
- package/src/tools/skill-doctor.ts +254 -0
package/src/hooks/lib/context.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
|
+
import { loadAlgorithmReviewNudge } from "./algorithm-review";
|
|
9
10
|
import { readLearnings } from "./learning-store";
|
|
10
11
|
import { loadOpinionContext } from "./opinions";
|
|
11
12
|
import { paths } from "./paths";
|
|
@@ -312,9 +313,12 @@ export function buildSystemReminder(opts: { agent?: AgentTarget } = {}): string
|
|
|
312
313
|
? loadSessionIntelligence()
|
|
313
314
|
: "";
|
|
314
315
|
const handoff = settings.isEnabled("handoff") ? loadHandoff() : "";
|
|
316
|
+
// Maintainer-only: self-gates to a repo checkout, "" for everyone else.
|
|
317
|
+
const algoReview = loadAlgorithmReviewNudge();
|
|
315
318
|
const parts: string[] = [];
|
|
316
319
|
if (startup) parts.push(startup);
|
|
317
320
|
if (handoff) parts.push(handoff);
|
|
321
|
+
if (algoReview) parts.push(algoReview);
|
|
318
322
|
if (selfModel) parts.push(selfModel);
|
|
319
323
|
if (wisdom) parts.push(wisdom);
|
|
320
324
|
if (opinions) parts.push(opinions);
|
|
@@ -158,3 +158,48 @@ export function readLearnings(baseDir: string, limit?: number): LearningEntry[]
|
|
|
158
158
|
|
|
159
159
|
return entries;
|
|
160
160
|
}
|
|
161
|
+
|
|
162
|
+
// ── Reflections ──
|
|
163
|
+
|
|
164
|
+
export interface ReflectionEntry {
|
|
165
|
+
ts: string;
|
|
166
|
+
cwd: string;
|
|
167
|
+
task: string;
|
|
168
|
+
sentiment: number;
|
|
169
|
+
q1: string;
|
|
170
|
+
q2: string;
|
|
171
|
+
q3: string;
|
|
172
|
+
/** Whether the improvement ideas generalize to the algorithm, or are task-bound. */
|
|
173
|
+
scope: "general" | "task-specific";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Read algorithm reflections from the JSONL store, newest first. Each line is one
|
|
178
|
+
* reflection produced at the end of an Algorithm run (q1 = what I'd do differently,
|
|
179
|
+
* q2 = algorithm improvement, q3 = AI-level insight).
|
|
180
|
+
*/
|
|
181
|
+
export function readReflections(file: string, limit?: number): ReflectionEntry[] {
|
|
182
|
+
if (!existsSync(file)) return [];
|
|
183
|
+
const entries: ReflectionEntry[] = [];
|
|
184
|
+
for (const line of readFileSync(file, "utf-8").split("\n")) {
|
|
185
|
+
if (!line.trim()) continue;
|
|
186
|
+
try {
|
|
187
|
+
const o = JSON.parse(line);
|
|
188
|
+
entries.push({
|
|
189
|
+
ts: o.timestamp || "",
|
|
190
|
+
cwd: o.cwd || "",
|
|
191
|
+
task: o.task || "",
|
|
192
|
+
sentiment: typeof o.sentiment === "number" ? o.sentiment : 0,
|
|
193
|
+
q1: o.q1 || "",
|
|
194
|
+
q2: o.q2 || "",
|
|
195
|
+
q3: o.q3 || "",
|
|
196
|
+
// Untagged (e.g. the existing backlog) defaults to general — the common case.
|
|
197
|
+
scope: o.scope === "task-specific" ? "task-specific" : "general",
|
|
198
|
+
});
|
|
199
|
+
} catch {
|
|
200
|
+
/* skip malformed line */
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
entries.reverse(); // JSONL is appended chronologically → newest first
|
|
204
|
+
return limit ? entries.slice(0, limit) : entries;
|
|
205
|
+
}
|
package/src/hooks/lib/paths.ts
CHANGED
|
@@ -50,6 +50,9 @@ export const paths = {
|
|
|
50
50
|
knowledge: () => ensureDir(home("memory", "knowledge")),
|
|
51
51
|
knowledgeDomain: (d: string) => ensureDir(home("memory", "knowledge", d)),
|
|
52
52
|
failures: () => ensureDir(home("memory", "learning", "failures")),
|
|
53
|
+
reflections: () => ensureDir(home("memory", "learning", "reflections")),
|
|
54
|
+
reflectionsFile: () =>
|
|
55
|
+
home("memory", "learning", "reflections", "algorithm-reflections.jsonl"),
|
|
53
56
|
retrievalIndex: () => home("memory", "learning", ".retrieval-index.json"),
|
|
54
57
|
progress: () => ensureDir(home("memory", "state", "progress")),
|
|
55
58
|
projectHistory: () => ensureDir(home("memory", "projects")),
|
|
@@ -82,6 +85,8 @@ export const assets = {
|
|
|
82
85
|
copilotHooksTemplate: () => pkg("assets", "templates", "hooks.copilot.json"),
|
|
83
86
|
codexHooksTemplate: () => pkg("assets", "templates", "hooks.codex.json"),
|
|
84
87
|
codexRulesTemplate: () => pkg("assets", "templates", "rules.codex.rules"),
|
|
88
|
+
statuslineScriptBash: () => pkg("assets", "statusline.sh"),
|
|
89
|
+
statuslineScriptPs1: () => pkg("assets", "statusline.ps1"),
|
|
85
90
|
agentTools: () => pkg("src", "tools", "agent"),
|
|
86
91
|
tools: () => pkg("src", "tools"),
|
|
87
92
|
palDocs: () => pkg("assets", "templates", "PAL"),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Retrieval index — single JSON file with per-doc term-frequency vectors and a global
|
|
3
|
-
* document-frequency table over the failures + wisdom-frames corpus.
|
|
3
|
+
* document-frequency table over the failures + wisdom-frames + reflections corpus.
|
|
4
4
|
*
|
|
5
5
|
* Built once on first read, rebuilt in the background when source dirs change.
|
|
6
6
|
* Read by the UserPromptSubmit retrieval handler; written by ensureIndex (sync bootstrap)
|
|
@@ -11,17 +11,18 @@ import { spawn } from "node:child_process";
|
|
|
11
11
|
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { resolve } from "node:path";
|
|
13
13
|
import { SIMILARITY_THRESHOLD } from "./graduation";
|
|
14
|
-
import { readFailures } from "./learning-store";
|
|
14
|
+
import { readFailures, readReflections } from "./learning-store";
|
|
15
15
|
import { logDebug, logError } from "./log";
|
|
16
16
|
import { palPkg, paths } from "./paths";
|
|
17
17
|
import { similarity, tokenize } from "./text-similarity";
|
|
18
18
|
import { readFramesForRetrieval } from "./wisdom";
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// v2: added "reflection" source — bump invalidates v1 caches so they rebuild with it.
|
|
21
|
+
const INDEX_VERSION = 2;
|
|
21
22
|
|
|
22
23
|
export interface IndexedDoc {
|
|
23
24
|
id: string;
|
|
24
|
-
source: "failure" | "wisdom";
|
|
25
|
+
source: "failure" | "wisdom" | "reflection";
|
|
25
26
|
path: string;
|
|
26
27
|
rating: number;
|
|
27
28
|
ts: string;
|
|
@@ -144,6 +145,32 @@ export function buildIndex(): RetrievalIndex {
|
|
|
144
145
|
});
|
|
145
146
|
}
|
|
146
147
|
|
|
148
|
+
// Reflections — each Algorithm run's self-observations. q1 ("what I'd do
|
|
149
|
+
// differently") is the actionable lesson; q2/q3 add process and reasoning signal.
|
|
150
|
+
for (const r of readReflections(paths.reflectionsFile())) {
|
|
151
|
+
const tokens = buildDocTokens([
|
|
152
|
+
{ text: r.q1, weight: 3 },
|
|
153
|
+
{ text: r.task, weight: 2 },
|
|
154
|
+
{ text: r.q2, weight: 2 },
|
|
155
|
+
{ text: r.q3, weight: 1 },
|
|
156
|
+
]);
|
|
157
|
+
if (tokens.length === 0) continue;
|
|
158
|
+
const { tf, len } = buildTermFreq(tokens);
|
|
159
|
+
for (const term of Object.keys(tf)) df[term] = (df[term] ?? 0) + 1;
|
|
160
|
+
docs.push({
|
|
161
|
+
id: `reflect:${r.ts}`,
|
|
162
|
+
source: "reflection",
|
|
163
|
+
path: paths.reflectionsFile(),
|
|
164
|
+
rating: r.sentiment,
|
|
165
|
+
ts: r.ts,
|
|
166
|
+
tf,
|
|
167
|
+
len,
|
|
168
|
+
displayPrinciple: r.q1,
|
|
169
|
+
displayContext: r.task,
|
|
170
|
+
cwd: r.cwd || undefined,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
147
174
|
return {
|
|
148
175
|
version: INDEX_VERSION,
|
|
149
176
|
builtAt: new Date().toISOString(),
|
|
@@ -173,7 +200,7 @@ export function readIndex(): RetrievalIndex | null {
|
|
|
173
200
|
export function isStale(index: RetrievalIndex): boolean {
|
|
174
201
|
try {
|
|
175
202
|
const builtMs = new Date(index.builtAt).getTime();
|
|
176
|
-
for (const dir of [paths.failures(), paths.wisdom()]) {
|
|
203
|
+
for (const dir of [paths.failures(), paths.wisdom(), paths.reflections()]) {
|
|
177
204
|
if (!existsSync(dir)) continue;
|
|
178
205
|
if (statSync(dir).mtimeMs > builtMs) return true;
|
|
179
206
|
}
|
|
@@ -131,7 +131,8 @@ function formatLine(s: ScoredDoc): string {
|
|
|
131
131
|
return `- ${tag} ${principle} (CRYSTAL ${s.doc.rating}%)`;
|
|
132
132
|
}
|
|
133
133
|
const ago = formatAgo(s.doc.ts);
|
|
134
|
-
const
|
|
134
|
+
const kind = s.doc.source === "reflection" ? "reflection" : "rating";
|
|
135
|
+
const meta = ago ? `${kind} ${s.doc.rating}/10, ${ago}` : `${kind} ${s.doc.rating}/10`;
|
|
135
136
|
return `- ${tag} ${principle} (${meta})`;
|
|
136
137
|
}
|
|
137
138
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
11
|
import { parse } from "./frontmatter";
|
|
12
|
-
import { readFailures } from "./learning-store";
|
|
12
|
+
import { type FailureEntry, readFailures } from "./learning-store";
|
|
13
13
|
import { loadOpinionContext } from "./opinions";
|
|
14
14
|
import { palHome, paths } from "./paths";
|
|
15
15
|
import { readFramePrinciples } from "./wisdom";
|
|
@@ -100,16 +100,39 @@ export function loadSynthesisRecommendations(): string {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Rank failures for injection: same-project (cwd match) first, then by recency.
|
|
105
|
+
* Project-relevant lessons are never crowded out by more-recent other-project
|
|
106
|
+
* ones — fixing the case where (e.g.) a web-app's cache lessons dominate a CLI
|
|
107
|
+
* session purely because they were logged last. Returns the top `limit`.
|
|
108
|
+
*/
|
|
109
|
+
export function rankFailures(
|
|
110
|
+
entries: FailureEntry[],
|
|
111
|
+
cwd: string,
|
|
112
|
+
limit = 5
|
|
113
|
+
): FailureEntry[] {
|
|
114
|
+
return [...entries]
|
|
115
|
+
.sort((a, b) => {
|
|
116
|
+
const am = a.cwd === cwd;
|
|
117
|
+
const bm = b.cwd === cwd;
|
|
118
|
+
if (am !== bm) return am ? -1 : 1;
|
|
119
|
+
return (b.ts || "").localeCompare(a.ts || ""); // recency desc within a group
|
|
120
|
+
})
|
|
121
|
+
.slice(0, limit);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Build the failure avoid-list, prioritized by relevance to the current project. */
|
|
104
125
|
export function loadFailurePatterns(): string {
|
|
105
126
|
try {
|
|
106
|
-
const
|
|
107
|
-
|
|
127
|
+
const cwd = process.cwd();
|
|
128
|
+
const all = readFailures(paths.failures());
|
|
129
|
+
if (all.length === 0) return "";
|
|
108
130
|
|
|
109
|
-
const lines =
|
|
131
|
+
const lines = rankFailures(all, cwd).map((e) => {
|
|
110
132
|
const label = e.rating ? `[${e.rating}/10]` : "";
|
|
133
|
+
const tag = e.cwd === cwd ? "[project]" : "[other]";
|
|
111
134
|
const text = e.principle || e.context;
|
|
112
|
-
return `- ${label} ${text}`.trim();
|
|
135
|
+
return `- ${label} ${tag} ${text}`.trim();
|
|
113
136
|
});
|
|
114
137
|
|
|
115
138
|
return ["## Lessons from Recent Failures — Apply These Now", ...lines].join("\n");
|
|
@@ -9,9 +9,11 @@ import { resolve } from "node:path";
|
|
|
9
9
|
import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
|
|
10
10
|
import { assets, palHome, palPkg, platform } from "../../hooks/lib/paths";
|
|
11
11
|
import {
|
|
12
|
+
addStatuslineConfig,
|
|
12
13
|
copyAgents,
|
|
13
14
|
copyPalDocs,
|
|
14
15
|
copySkills,
|
|
16
|
+
copyStatusline,
|
|
15
17
|
countAgents,
|
|
16
18
|
countMd,
|
|
17
19
|
countSkills,
|
|
@@ -43,7 +45,10 @@ log.info("Backed up settings.json");
|
|
|
43
45
|
// --- Load template and merge into existing settings ---
|
|
44
46
|
const template = loadSettingsTemplate(assets.claudeSettingsTemplate(), PKG_ROOT);
|
|
45
47
|
const existing = readJson<Record<string, unknown>>(SETTINGS, {});
|
|
46
|
-
|
|
48
|
+
let merged = mergeSettings(existing, template);
|
|
49
|
+
|
|
50
|
+
// Add platform-specific statusLine config
|
|
51
|
+
merged = addStatuslineConfig(merged);
|
|
47
52
|
|
|
48
53
|
writeJson(SETTINGS, merged);
|
|
49
54
|
log.success("Merged PAL settings into settings.json");
|
|
@@ -56,6 +61,9 @@ generateSkillIndex();
|
|
|
56
61
|
// --- Copy agents ---
|
|
57
62
|
copyAgents();
|
|
58
63
|
|
|
64
|
+
// --- Copy statusline script ---
|
|
65
|
+
copyStatusline();
|
|
66
|
+
|
|
59
67
|
// --- Copy PAL system docs ---
|
|
60
68
|
const palDocsCount = copyPalDocs();
|
|
61
69
|
log.success(`Installed ${palDocsCount} PAL docs to ~/.pal/docs/`);
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
removeAgents,
|
|
14
14
|
removePalDocs,
|
|
15
15
|
removeSkills,
|
|
16
|
+
removeStatusline,
|
|
17
|
+
removeStatuslineConfig,
|
|
16
18
|
unmergeSettings,
|
|
17
19
|
writeJson,
|
|
18
20
|
} from "../lib";
|
|
@@ -33,7 +35,10 @@ log.info("Backed up settings.json");
|
|
|
33
35
|
// --- Load template and unmerge from existing settings ---
|
|
34
36
|
const template = loadSettingsTemplate(assets.claudeSettingsTemplate(), PKG_ROOT);
|
|
35
37
|
const existing = readJson<Record<string, unknown>>(SETTINGS, {});
|
|
36
|
-
|
|
38
|
+
let cleaned = unmergeSettings(existing, template);
|
|
39
|
+
|
|
40
|
+
// Remove statusLine config
|
|
41
|
+
cleaned = removeStatuslineConfig(cleaned);
|
|
37
42
|
|
|
38
43
|
writeJson(SETTINGS, cleaned);
|
|
39
44
|
log.success("Removed PAL settings from settings.json");
|
|
@@ -54,6 +59,9 @@ if (removedAgents.length > 0) {
|
|
|
54
59
|
log.info("No PAL agents found");
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
// --- Remove statusline script ---
|
|
63
|
+
removeStatusline();
|
|
64
|
+
|
|
57
65
|
// --- Remove PAL system docs ---
|
|
58
66
|
removePalDocs();
|
|
59
67
|
|
package/src/targets/lib.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
chmodSync,
|
|
6
7
|
copyFileSync,
|
|
7
8
|
existsSync,
|
|
8
9
|
lstatSync,
|
|
@@ -123,6 +124,65 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
|
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
// Merge skillOverrides (object with skill name keys, add if not present)
|
|
128
|
+
if (template.skillOverrides && typeof template.skillOverrides === "object") {
|
|
129
|
+
result.skillOverrides ??= {};
|
|
130
|
+
const resultOverrides = result.skillOverrides as Record<string, unknown>;
|
|
131
|
+
for (const [skill, value] of Object.entries(template.skillOverrides)) {
|
|
132
|
+
if (!(skill in resultOverrides)) {
|
|
133
|
+
resultOverrides[skill] = value;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Merge attribution (object with commit/pr keys, add if not present)
|
|
139
|
+
if (template.attribution && typeof template.attribution === "object") {
|
|
140
|
+
if (!("attribution" in result)) {
|
|
141
|
+
result.attribution = template.attribution;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Merge showClearContextOnPlanAccept (boolean, add if not present)
|
|
146
|
+
if ("showClearContextOnPlanAccept" in template) {
|
|
147
|
+
if (!("showClearContextOnPlanAccept" in result)) {
|
|
148
|
+
result.showClearContextOnPlanAccept = template.showClearContextOnPlanAccept;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Merge respectGitignore (boolean, add if not present)
|
|
153
|
+
if ("respectGitignore" in template) {
|
|
154
|
+
if (!("respectGitignore" in result)) {
|
|
155
|
+
result.respectGitignore = template.respectGitignore;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Merge spinnerTipsEnabled (boolean, add if not present)
|
|
160
|
+
if ("spinnerTipsEnabled" in template) {
|
|
161
|
+
if (!("spinnerTipsEnabled" in result)) {
|
|
162
|
+
result.spinnerTipsEnabled = template.spinnerTipsEnabled;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Merge spinnerTipsOverride (merge tips array, deduplicate by content)
|
|
167
|
+
if (
|
|
168
|
+
template.spinnerTipsOverride &&
|
|
169
|
+
typeof template.spinnerTipsOverride === "object" &&
|
|
170
|
+
"tips" in template.spinnerTipsOverride &&
|
|
171
|
+
Array.isArray((template.spinnerTipsOverride as Record<string, unknown>).tips)
|
|
172
|
+
) {
|
|
173
|
+
result.spinnerTipsOverride ??= { tips: [] };
|
|
174
|
+
const resultOverride = result.spinnerTipsOverride as Record<string, unknown>;
|
|
175
|
+
resultOverride.tips ??= [];
|
|
176
|
+
const tips = resultOverride.tips as string[];
|
|
177
|
+
const templateTips = (template.spinnerTipsOverride as Record<string, unknown>)
|
|
178
|
+
.tips as string[];
|
|
179
|
+
for (const tip of templateTips) {
|
|
180
|
+
if (typeof tip === "string" && !tips.includes(tip)) {
|
|
181
|
+
tips.push(tip);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
126
186
|
return result;
|
|
127
187
|
}
|
|
128
188
|
|
|
@@ -163,6 +223,68 @@ export function unmergeSettings(existing: Settings, template: Settings): Setting
|
|
|
163
223
|
if (Object.keys(result.permissions).length === 0) delete result.permissions;
|
|
164
224
|
}
|
|
165
225
|
|
|
226
|
+
// Remove PAL skillOverrides (remove keys that appear in template)
|
|
227
|
+
if (
|
|
228
|
+
template.skillOverrides &&
|
|
229
|
+
typeof template.skillOverrides === "object" &&
|
|
230
|
+
result.skillOverrides &&
|
|
231
|
+
typeof result.skillOverrides === "object"
|
|
232
|
+
) {
|
|
233
|
+
const palSkills = new Set(Object.keys(template.skillOverrides));
|
|
234
|
+
for (const skill of palSkills) {
|
|
235
|
+
delete (result.skillOverrides as Record<string, unknown>)[skill];
|
|
236
|
+
}
|
|
237
|
+
if (Object.keys(result.skillOverrides).length === 0) delete result.skillOverrides;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Remove PAL attribution if it matches template structure
|
|
241
|
+
if (template.attribution && "attribution" in result) {
|
|
242
|
+
delete result.attribution;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Remove PAL showClearContextOnPlanAccept
|
|
246
|
+
if (
|
|
247
|
+
"showClearContextOnPlanAccept" in template &&
|
|
248
|
+
"showClearContextOnPlanAccept" in result
|
|
249
|
+
) {
|
|
250
|
+
delete result.showClearContextOnPlanAccept;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Remove PAL respectGitignore
|
|
254
|
+
if ("respectGitignore" in template && "respectGitignore" in result) {
|
|
255
|
+
delete result.respectGitignore;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Remove PAL spinnerTipsEnabled
|
|
259
|
+
if ("spinnerTipsEnabled" in template && "spinnerTipsEnabled" in result) {
|
|
260
|
+
delete result.spinnerTipsEnabled;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Remove PAL spinnerTipsOverride (remove only template tips, preserve user tips)
|
|
264
|
+
if (
|
|
265
|
+
template.spinnerTipsOverride &&
|
|
266
|
+
typeof template.spinnerTipsOverride === "object" &&
|
|
267
|
+
"tips" in template.spinnerTipsOverride &&
|
|
268
|
+
Array.isArray((template.spinnerTipsOverride as Record<string, unknown>).tips) &&
|
|
269
|
+
result.spinnerTipsOverride &&
|
|
270
|
+
typeof result.spinnerTipsOverride === "object" &&
|
|
271
|
+
"tips" in result.spinnerTipsOverride &&
|
|
272
|
+
Array.isArray((result.spinnerTipsOverride as Record<string, unknown>).tips)
|
|
273
|
+
) {
|
|
274
|
+
const templateTipsArray = (template.spinnerTipsOverride as Record<string, unknown>)
|
|
275
|
+
.tips as string[];
|
|
276
|
+
const templateTips = new Set(templateTipsArray);
|
|
277
|
+
const resultOverride = result.spinnerTipsOverride as Record<string, unknown>;
|
|
278
|
+
const tips = resultOverride.tips as string[];
|
|
279
|
+
resultOverride.tips = tips.filter((tip) => !templateTips.has(tip));
|
|
280
|
+
if (
|
|
281
|
+
(resultOverride.tips as string[]).length === 0 &&
|
|
282
|
+
Object.keys(resultOverride).length === 1
|
|
283
|
+
) {
|
|
284
|
+
delete result.spinnerTipsOverride;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
166
288
|
return result;
|
|
167
289
|
}
|
|
168
290
|
|
|
@@ -768,6 +890,100 @@ export function loadCopilotHooksTemplate(templatePath: string, pkgRoot: string):
|
|
|
768
890
|
}
|
|
769
891
|
}
|
|
770
892
|
|
|
893
|
+
// --- Statusline ---
|
|
894
|
+
|
|
895
|
+
/** Copy statusline script to ~/.claude/ for the current platform */
|
|
896
|
+
export function copyStatusline(): boolean {
|
|
897
|
+
const claudeDir = platform.claudeDir();
|
|
898
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
899
|
+
|
|
900
|
+
const isPlatformWin32 = process.platform === "win32";
|
|
901
|
+
const sourcePath = isPlatformWin32
|
|
902
|
+
? assets.statuslineScriptPs1()
|
|
903
|
+
: assets.statuslineScriptBash();
|
|
904
|
+
const scriptName = isPlatformWin32 ? "statusline.ps1" : "statusline.sh";
|
|
905
|
+
const destPath = resolve(claudeDir, scriptName);
|
|
906
|
+
|
|
907
|
+
if (!existsSync(sourcePath)) {
|
|
908
|
+
log.warn(`Statusline script not found at ${sourcePath}`);
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
try {
|
|
913
|
+
copyFileSync(sourcePath, destPath);
|
|
914
|
+
|
|
915
|
+
// Make executable on Unix
|
|
916
|
+
if (process.platform !== "win32") {
|
|
917
|
+
chmodSync(destPath, 0o755);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
log.success(`Statusline installed to ${destPath}`);
|
|
921
|
+
return true;
|
|
922
|
+
} catch (e) {
|
|
923
|
+
log.warn(`Failed to copy statusline script: ${e}`);
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/** Remove statusline script from ~/.claude/ */
|
|
929
|
+
export function removeStatusline(): boolean {
|
|
930
|
+
const claudeDir = platform.claudeDir();
|
|
931
|
+
const isPlatformWin32 = process.platform === "win32";
|
|
932
|
+
const scriptName = isPlatformWin32 ? "statusline.ps1" : "statusline.sh";
|
|
933
|
+
const scriptPath = resolve(claudeDir, scriptName);
|
|
934
|
+
|
|
935
|
+
if (!existsSync(scriptPath)) {
|
|
936
|
+
log.info("Statusline script not found, nothing to remove");
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
try {
|
|
941
|
+
unlinkSync(scriptPath);
|
|
942
|
+
log.success(`Removed statusline script from ${scriptPath}`);
|
|
943
|
+
return true;
|
|
944
|
+
} catch (e) {
|
|
945
|
+
log.warn(`Failed to remove statusline script: ${e}`);
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/** Add statusLine config to settings if not already present or if using old broken command */
|
|
951
|
+
export function addStatuslineConfig(
|
|
952
|
+
settings: Record<string, unknown>
|
|
953
|
+
): Record<string, unknown> {
|
|
954
|
+
const statusLine = settings.statusLine as Record<string, unknown> | undefined;
|
|
955
|
+
|
|
956
|
+
// Check if already configured with a valid command (not the old broken one)
|
|
957
|
+
if (statusLine && typeof statusLine === "object" && statusLine.command) {
|
|
958
|
+
const cmd = statusLine.command as string;
|
|
959
|
+
// Keep existing config unless it's the old broken Get-Content pattern
|
|
960
|
+
if (!cmd.includes("Get-Content -Raw")) {
|
|
961
|
+
return settings;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const isPlatformWin32 = process.platform === "win32";
|
|
966
|
+
const command = isPlatformWin32
|
|
967
|
+
? "powershell -NoProfile -File ~/.claude/statusline.ps1"
|
|
968
|
+
: "~/.claude/statusline.sh";
|
|
969
|
+
|
|
970
|
+
settings.statusLine = {
|
|
971
|
+
type: "command",
|
|
972
|
+
command,
|
|
973
|
+
padding: 2,
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
return settings;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/** Remove statusLine config from settings */
|
|
980
|
+
export function removeStatuslineConfig(
|
|
981
|
+
settings: Record<string, unknown>
|
|
982
|
+
): Record<string, unknown> {
|
|
983
|
+
delete settings.statusLine;
|
|
984
|
+
return settings;
|
|
985
|
+
}
|
|
986
|
+
|
|
771
987
|
// --- Skill Index ---
|
|
772
988
|
|
|
773
989
|
interface SkillIndexEntry {
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
* --q3 "Missed the implicit constraint about cross-platform"
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { appendFileSync
|
|
16
|
-
import { resolve } from "node:path";
|
|
15
|
+
import { appendFileSync } from "node:fs";
|
|
17
16
|
import { parseArgs } from "node:util";
|
|
18
17
|
import { paths } from "../../hooks/lib/paths";
|
|
19
18
|
|
|
@@ -30,14 +29,15 @@ interface AlgorithmReflection {
|
|
|
30
29
|
q1: string;
|
|
31
30
|
q2: string;
|
|
32
31
|
q3: string;
|
|
32
|
+
/** general = the improvement ideas generalize to the algorithm; task-specific = bound to this task. */
|
|
33
|
+
scope: "general" | "task-specific";
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
// ── Core ──
|
|
36
37
|
|
|
37
38
|
function reflectionsPath(): string {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return resolve(dir, "algorithm-reflections.jsonl");
|
|
39
|
+
paths.reflections(); // ensures the directory exists
|
|
40
|
+
return paths.reflectionsFile();
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function appendReflection(reflection: AlgorithmReflection): {
|
|
@@ -70,6 +70,7 @@ function run() {
|
|
|
70
70
|
q1: { type: "string" },
|
|
71
71
|
q2: { type: "string" },
|
|
72
72
|
q3: { type: "string" },
|
|
73
|
+
scope: { type: "string" },
|
|
73
74
|
help: { type: "boolean", short: "h" },
|
|
74
75
|
},
|
|
75
76
|
});
|
|
@@ -91,6 +92,7 @@ Arguments:
|
|
|
91
92
|
--q1 Q1 — Self: what I'd do differently
|
|
92
93
|
--q2 Q2 — Algorithm: structural improvement
|
|
93
94
|
--q3 Q3 — AI: reasoning blind spot
|
|
95
|
+
--scope general (default) | task-specific — is the algorithm idea reusable or task-bound?
|
|
94
96
|
|
|
95
97
|
Output: algorithm-reflections.jsonl in memory/learning/reflections/
|
|
96
98
|
`);
|
|
@@ -113,6 +115,9 @@ Output: algorithm-reflections.jsonl in memory/learning/reflections/
|
|
|
113
115
|
q1: values.q1,
|
|
114
116
|
q2: values.q2,
|
|
115
117
|
q3: values.q3,
|
|
118
|
+
// Default to general (the ~94% case); only "task-specific" suppresses it
|
|
119
|
+
// from algorithm-update clustering.
|
|
120
|
+
scope: values.scope === "task-specific" ? "task-specific" : "general",
|
|
116
121
|
};
|
|
117
122
|
|
|
118
123
|
const result = appendReflection(reflection);
|