portable-agent-layer 0.78.1 → 0.78.3
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 +3 -2
- package/assets/skills/pal-reflect/SKILL.md +2 -2
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +4 -4
- package/package.json +2 -2
- package/src/cli/builtin-tools.ts +1 -0
- package/src/cli/index.ts +3 -48
- package/src/cli/setup-auto-update.ts +7 -7
- package/src/hooks/AutoUpdate.ts +1 -1
- package/src/hooks/handlers/persist-last-exchange.ts +39 -24
- package/src/hooks/handlers/session-intelligence.ts +6 -2
- package/src/hooks/lib/algorithm-review.ts +12 -7
- package/src/hooks/lib/auto-update.ts +16 -6
- package/src/hooks/lib/context.ts +7 -1
- package/src/hooks/lib/inference.ts +12 -45
- package/src/hooks/lib/log.ts +44 -3
- package/src/hooks/lib/paths.ts +7 -2
- package/src/hooks/lib/relationship.ts +2 -2
- package/src/hooks/lib/retrieval-index.ts +13 -6
- package/src/hooks/lib/security.ts +117 -73
- package/src/hooks/lib/semi-static.ts +10 -3
- package/src/hooks/lib/settings.ts +11 -12
- package/src/tools/{relationship-reflect.ts → agent/relationship-reflect.ts} +13 -12
- package/src/tools/control-room/ui/dist/assets/{index-DyhnNmdi.js → index-D_o9Usd_.js} +1 -1
- package/src/tools/control-room/ui/dist/index.html +1 -1
- package/src/tools/control-room/ui/screens/settings.tsx +2 -2
- package/src/tools/self-model.ts +1 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
|
-
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { resolve } from "node:path";
|
|
13
13
|
import { SIMILARITY_THRESHOLD } from "./graduation";
|
|
14
14
|
import { readFailures, readReflections } from "./learning-store";
|
|
@@ -197,14 +197,21 @@ export function readIndex(): RetrievalIndex | null {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
/** True if any source directory was modified after the index was built. */
|
|
200
|
+
function newestFileMtime(dir: string): number {
|
|
201
|
+
let newest = 0;
|
|
202
|
+
for (const entry of readdirSync(dir, { recursive: true, withFileTypes: true })) {
|
|
203
|
+
if (!entry.isFile()) continue;
|
|
204
|
+
newest = Math.max(newest, statSync(resolve(entry.parentPath, entry.name)).mtimeMs);
|
|
205
|
+
}
|
|
206
|
+
return newest;
|
|
207
|
+
}
|
|
208
|
+
|
|
200
209
|
export function isStale(index: RetrievalIndex): boolean {
|
|
201
210
|
try {
|
|
202
211
|
const builtMs = new Date(index.builtAt).getTime();
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
return false;
|
|
212
|
+
return [paths.failures(), paths.wisdom(), paths.reflections()].some(
|
|
213
|
+
(dir) => newestFileMtime(dir) > builtMs
|
|
214
|
+
);
|
|
208
215
|
} catch {
|
|
209
216
|
return false;
|
|
210
217
|
}
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { lstatSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { palHome, platform } from "./paths";
|
|
7
10
|
|
|
8
11
|
// PowerShell aliases rm, rmdir, del, erase, rd and ri all to Remove-Item, and
|
|
9
12
|
// cmd ships its own rd and del — so the verb alone never says which shell ran it.
|
|
@@ -76,9 +79,14 @@ const WIN_ELEVATED_THREATS: [RegExp, string][] = [
|
|
|
76
79
|
[/\bdiskpart\b/i, "Disk partitioning"],
|
|
77
80
|
];
|
|
78
81
|
|
|
82
|
+
const POSIX_ROOT_TARGET = String.raw`["']?(?:(?:~|\$HOME|\$\{HOME\})/?|/)\*?["']?(?=\s|[;&|]|$)`;
|
|
83
|
+
|
|
79
84
|
/** Dangerous command patterns — always blocked */
|
|
80
85
|
const BLOCKED_COMMANDS: [RegExp, string][] = [
|
|
81
|
-
[
|
|
86
|
+
[
|
|
87
|
+
new RegExp(String.raw`rm\s+-rf\s+${POSIX_ROOT_TARGET}`),
|
|
88
|
+
"Recursive delete of root or home",
|
|
89
|
+
],
|
|
82
90
|
[/mkfs\./, "Filesystem format"],
|
|
83
91
|
[/dd\s+if=.*of=\/dev\//, "Raw disk write"],
|
|
84
92
|
[/>\s*\/dev\/sd/, "Direct device write"],
|
|
@@ -137,13 +145,8 @@ const HOOK_MANAGED_DIRS = [
|
|
|
137
145
|
"debug",
|
|
138
146
|
];
|
|
139
147
|
|
|
140
|
-
/** Escape a string for use in a RegExp */
|
|
141
|
-
function escapeRegExp(s: string): string {
|
|
142
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
148
|
/** PAL-deployed dirs — engine-managed, overwritten on every `pal install` */
|
|
146
|
-
const
|
|
149
|
+
const PAL_INSTALLED_DIRS = ["docs", "skills", "tools"];
|
|
147
150
|
|
|
148
151
|
/** Paths that should never be written to */
|
|
149
152
|
const PROTECTED_PATHS: RegExp[] = [
|
|
@@ -154,31 +157,108 @@ const PROTECTED_PATHS: RegExp[] = [
|
|
|
154
157
|
/\.gnupg\//,
|
|
155
158
|
// Claude Code auto-memory — PAL owns memory; writes here indicate wrong system is being used
|
|
156
159
|
/\.claude\/projects\/[^/]+\/memory\//,
|
|
157
|
-
// PAL_INSTALLED_DIRS_RE is enforced by the dedicated branch in checkFilePath
|
|
158
|
-
// (which exempts personal skill dirs); keeping it here would re-block them.
|
|
159
|
-
// Derived from HOOK_MANAGED_FILES — scoped to managed roots only
|
|
160
|
-
...HOOK_MANAGED_FILES.map(
|
|
161
|
-
(name) =>
|
|
162
|
-
new RegExp(
|
|
163
|
-
String.raw`[/\\]\.(?:pal|claude|agents|cursor)[/\\].*${escapeRegExp(name)}$`
|
|
164
|
-
)
|
|
165
|
-
),
|
|
166
160
|
];
|
|
167
161
|
|
|
168
|
-
|
|
169
|
-
const
|
|
162
|
+
function comparable(path: string): string {
|
|
163
|
+
const forward = path.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
164
|
+
return process.platform === "win32" ? forward.toLowerCase() : forward;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function absoluteComparable(path: string): string {
|
|
168
|
+
return comparable(resolve(path.replaceAll("\\", "/")));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The part of `path` below `root`, or null when `path` is outside it. */
|
|
172
|
+
function pathBelow(root: string, path: string): string | null {
|
|
173
|
+
const base = absoluteComparable(root);
|
|
174
|
+
const target = absoluteComparable(path);
|
|
175
|
+
return target.startsWith(`${base}/`) ? target.slice(base.length + 1) : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The real directories PAL writes user state into, not any folder that shares their name. */
|
|
179
|
+
function managedRoots(): string[] {
|
|
180
|
+
return [
|
|
181
|
+
palHome(),
|
|
182
|
+
platform.claudeDir(),
|
|
183
|
+
platform.agentsDir(),
|
|
184
|
+
platform.cursorDir(),
|
|
185
|
+
platform.opencodeDir(),
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function pathInsideManagedRoot(path: string): string | null {
|
|
190
|
+
for (const root of managedRoots()) {
|
|
191
|
+
const below = pathBelow(root, path);
|
|
192
|
+
if (below !== null) return below;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
170
196
|
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
);
|
|
197
|
+
function managedFileAt(below: string): string | undefined {
|
|
198
|
+
return HOOK_MANAGED_FILES.find((name) => {
|
|
199
|
+
const file = comparable(name);
|
|
200
|
+
return below === file || below.endsWith(`/${file}`);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function managedDirAt(below: string): string | undefined {
|
|
205
|
+
return HOOK_MANAGED_DIRS.find((dir) => `/${below}/`.includes(`/${comparable(dir)}/`));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function managedPathReason(path: string): string | null {
|
|
209
|
+
const below = pathInsideManagedRoot(path);
|
|
210
|
+
if (below === null) return null;
|
|
211
|
+
const file = managedFileAt(below);
|
|
212
|
+
if (file) return `${file} is managed automatically by hooks — do not edit directly`;
|
|
213
|
+
const dir = managedDirAt(below);
|
|
214
|
+
if (dir) return `${dir}/ is managed automatically by hooks — do not edit directly`;
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const HOME_PREFIX =
|
|
219
|
+
/^(?:~|\$HOME|\$\{HOME\}|\$env:USERPROFILE|%USERPROFILE%)(?=[/\\]|$)/i;
|
|
220
|
+
const COMMAND_TOKEN = /[^\s'"`<>|;&()=]+/g;
|
|
221
|
+
|
|
222
|
+
function pathsNamedIn(segment: string): string[] {
|
|
223
|
+
return Array.from(segment.matchAll(COMMAND_TOKEN), (m) => m[0])
|
|
224
|
+
.filter((token) => HOME_PREFIX.test(token) || /[/\\]/.test(token))
|
|
225
|
+
.map((token) => token.replace(HOME_PREFIX, () => homedir()));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function managedReasonInSegment(segment: string): string | null {
|
|
229
|
+
for (const path of pathsNamedIn(segment)) {
|
|
230
|
+
const reason = managedPathReason(path);
|
|
231
|
+
if (reason) return reason;
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
176
234
|
}
|
|
177
235
|
|
|
178
236
|
/** Read-only commands allowed to reference protected files */
|
|
179
237
|
const READ_ONLY_COMMANDS =
|
|
180
238
|
/^\s*(?:cat|head|tail|less|more|grep|rg|wc|diff|stat|file|ls|dir|find|git\s+(?:log|diff|blame|show|status)|bat)\b/;
|
|
181
239
|
|
|
240
|
+
const COMMAND_SEPARATORS = new Set(["|", ";", "&"]);
|
|
241
|
+
|
|
242
|
+
function unquotedSegments(cmd: string): string[] {
|
|
243
|
+
const segments: string[] = [];
|
|
244
|
+
let current = "";
|
|
245
|
+
let quote: string | null = null;
|
|
246
|
+
for (const char of cmd) {
|
|
247
|
+
if (quote) {
|
|
248
|
+
if (char === quote) quote = null;
|
|
249
|
+
} else if (char === "'" || char === '"') {
|
|
250
|
+
quote = char;
|
|
251
|
+
} else if (COMMAND_SEPARATORS.has(char)) {
|
|
252
|
+
segments.push(current.trim());
|
|
253
|
+
current = "";
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
current += char;
|
|
257
|
+
}
|
|
258
|
+
segments.push(current.trim());
|
|
259
|
+
return segments;
|
|
260
|
+
}
|
|
261
|
+
|
|
182
262
|
/** Check a bash command against blocked patterns. Returns reason string or null. */
|
|
183
263
|
export function checkBashCommand(cmd: string): string | null {
|
|
184
264
|
for (const [pattern, reason] of BLOCKED_COMMANDS) {
|
|
@@ -189,26 +269,9 @@ export function checkBashCommand(cmd: string): string | null {
|
|
|
189
269
|
if (pattern.test(cmd)) return reason;
|
|
190
270
|
}
|
|
191
271
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
for (const name of HOOK_MANAGED_FILES) {
|
|
196
|
-
const pattern = new RegExp(
|
|
197
|
-
String.raw`\.(?:pal|claude|agents|cursor|config/opencode)[/\\]\S*${escapeRegExp(name)}`
|
|
198
|
-
);
|
|
199
|
-
const managed = segments.filter((s) => pattern.test(s));
|
|
200
|
-
if (managed.length > 0 && !managed.every((s) => READ_ONLY_COMMANDS.test(s))) {
|
|
201
|
-
return `${name} is managed automatically by hooks — do not edit directly`;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
for (const dir of HOOK_MANAGED_DIRS) {
|
|
205
|
-
const pattern = new RegExp(
|
|
206
|
-
String.raw`\.(?:pal|claude|agents|cursor|config/opencode)[/\\]\S*${escapeRegExp(dir)}`
|
|
207
|
-
);
|
|
208
|
-
const managed = segments.filter((s) => pattern.test(s));
|
|
209
|
-
if (managed.length > 0 && !managed.every((s) => READ_ONLY_COMMANDS.test(s))) {
|
|
210
|
-
return `${dir} is managed automatically by hooks — do not edit directly`;
|
|
211
|
-
}
|
|
272
|
+
for (const segment of unquotedSegments(cmd)) {
|
|
273
|
+
const reason = managedReasonInSegment(segment);
|
|
274
|
+
if (reason && !READ_ONLY_COMMANDS.test(segment)) return reason;
|
|
212
275
|
}
|
|
213
276
|
return null;
|
|
214
277
|
}
|
|
@@ -218,44 +281,25 @@ export function checkBashCommand(cmd: string): string | null {
|
|
|
218
281
|
* skills are real dirs authored in place. A not-yet-created skill dir is also
|
|
219
282
|
* personal (scaffolding). So: symlink → shipped/protected, otherwise → personal.
|
|
220
283
|
*/
|
|
221
|
-
function
|
|
222
|
-
const m = /\.pal\/skills\/([^/]+)/.exec(normalized);
|
|
223
|
-
if (!m) return false;
|
|
224
|
-
const skillRoot = normalized.slice(0, m.index + m[0].length);
|
|
284
|
+
function isShippedSkill(name: string): boolean {
|
|
225
285
|
try {
|
|
226
|
-
return lstatSync(
|
|
286
|
+
return lstatSync(resolve(palHome(), "skills", name)).isSymbolicLink();
|
|
227
287
|
} catch {
|
|
228
288
|
return false;
|
|
229
289
|
}
|
|
230
290
|
}
|
|
231
291
|
|
|
292
|
+
function palInstalledReason(path: string): string | null {
|
|
293
|
+
const [dir, entry] = pathBelow(palHome(), path)?.split("/") ?? [];
|
|
294
|
+
if (!dir || !entry || !PAL_INSTALLED_DIRS.includes(dir)) return null;
|
|
295
|
+
if (dir === "skills" && !isShippedSkill(entry)) return null;
|
|
296
|
+
return `~/.pal/${dir}/ is managed by 'pal install' — edit the source in the PAL repo instead`;
|
|
297
|
+
}
|
|
298
|
+
|
|
232
299
|
/** Check a file path against protected patterns. Returns a reason string or null. */
|
|
233
300
|
export function checkFilePath(filePath: string): string | null {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
if (isUnderManagedRoot(normalized)) {
|
|
237
|
-
const matchedFile = HOOK_MANAGED_FILES.find((name) =>
|
|
238
|
-
normalized.endsWith(`/${name}`)
|
|
239
|
-
);
|
|
240
|
-
if (matchedFile) {
|
|
241
|
-
return `${matchedFile} is managed automatically by hooks — do not edit directly`;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
// Check hook-managed directories
|
|
245
|
-
const matchedDir = HOOK_MANAGED_DIRS.find((dir) => normalized.includes(`/${dir}/`));
|
|
246
|
-
if (matchedDir) {
|
|
247
|
-
return `${matchedDir}/ is managed automatically by hooks — do not edit directly`;
|
|
248
|
-
}
|
|
249
|
-
// PAL-deployed dirs — edit source in the PAL repo, not the installed copy
|
|
250
|
-
if (PAL_INSTALLED_DIRS_RE.test(normalized)) {
|
|
251
|
-
const match = new RegExp(/\.pal[/\\](docs|skills|tools)/).exec(normalized);
|
|
252
|
-
const dir = match ? match[1] : "docs/skills/tools";
|
|
253
|
-
const isPersonalSkill = dir === "skills" && !isShippedSkillPath(normalized);
|
|
254
|
-
if (!isPersonalSkill) {
|
|
255
|
-
return `~/.pal/${dir}/ is managed by 'pal install' — edit the source in the PAL repo instead`;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
// Check remaining system-protected paths
|
|
301
|
+
const reason = managedPathReason(filePath) ?? palInstalledReason(filePath);
|
|
302
|
+
if (reason) return reason;
|
|
259
303
|
if (PROTECTED_PATHS.some((pattern) => pattern.test(filePath))) {
|
|
260
304
|
return `Protected path: ${filePath}`;
|
|
261
305
|
}
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
-
import { resolve } from "node:path";
|
|
10
|
+
import { basename, resolve } from "node:path";
|
|
11
11
|
import { type FailureEntry, readFailures } from "./learning-store";
|
|
12
12
|
import { loadOpinionContext } from "./opinions";
|
|
13
13
|
import { palHome, paths } from "./paths";
|
|
14
|
+
import { type ProjectProgress, readAllProjects, resolveProjectFromCwd } from "./projects";
|
|
14
15
|
import { readFramePrinciples } from "./wisdom";
|
|
15
16
|
|
|
16
17
|
/** A single semi-static context source — built at session stop, loaded natively at session start. */
|
|
@@ -67,6 +68,12 @@ export function rankFailures(
|
|
|
67
68
|
.slice(0, limit);
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
function projectTag(cwd: string, projects: ProjectProgress[]): string {
|
|
72
|
+
if (!cwd) return "";
|
|
73
|
+
const name = resolveProjectFromCwd(cwd, projects)?.name ?? basename(cwd);
|
|
74
|
+
return `[${name}]`;
|
|
75
|
+
}
|
|
76
|
+
|
|
70
77
|
/** Build the failure avoid-list, prioritized by relevance to the current project. */
|
|
71
78
|
export function loadFailurePatterns(): string {
|
|
72
79
|
try {
|
|
@@ -74,11 +81,11 @@ export function loadFailurePatterns(): string {
|
|
|
74
81
|
const all = readFailures(paths.failures());
|
|
75
82
|
if (all.length === 0) return "";
|
|
76
83
|
|
|
84
|
+
const projects = readAllProjects();
|
|
77
85
|
const lines = rankFailures(all, cwd).map((e) => {
|
|
78
86
|
const label = e.rating ? `[${e.rating}/10]` : "";
|
|
79
|
-
const tag = e.cwd === cwd ? "[project]" : "[other]";
|
|
80
87
|
const text = e.principle || e.context;
|
|
81
|
-
return
|
|
88
|
+
return ["-", label, projectTag(e.cwd, projects), text].filter(Boolean).join(" ");
|
|
82
89
|
});
|
|
83
90
|
|
|
84
91
|
return ["## Lessons from Recent Failures — Apply These Now", ...lines].join("\n");
|
|
@@ -54,28 +54,27 @@ const IDENTITY_DEFAULTS: Identity = {
|
|
|
54
54
|
|
|
55
55
|
// ── Singleton ──
|
|
56
56
|
|
|
57
|
-
let cached: PalSettingsData | null = null;
|
|
57
|
+
let cached: { path: string; data: PalSettingsData } | null = null;
|
|
58
58
|
|
|
59
59
|
function settingsPath(): string {
|
|
60
60
|
return resolve(paths.memory(), "pal-settings.json");
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function
|
|
64
|
-
if (
|
|
65
|
-
const p = settingsPath();
|
|
66
|
-
if (!existsSync(p)) {
|
|
67
|
-
cached = {};
|
|
68
|
-
return cached;
|
|
69
|
-
}
|
|
63
|
+
function readSettingsFile(p: string): PalSettingsData {
|
|
64
|
+
if (!existsSync(p)) return {};
|
|
70
65
|
try {
|
|
71
|
-
|
|
72
|
-
return cached;
|
|
66
|
+
return JSON.parse(readFileSync(p, "utf-8")) as PalSettingsData;
|
|
73
67
|
} catch {
|
|
74
|
-
|
|
75
|
-
return cached;
|
|
68
|
+
return {};
|
|
76
69
|
}
|
|
77
70
|
}
|
|
78
71
|
|
|
72
|
+
function load(): PalSettingsData {
|
|
73
|
+
const p = settingsPath();
|
|
74
|
+
if (cached?.path !== p) cached = { path: p, data: readSettingsFile(p) };
|
|
75
|
+
return cached.data;
|
|
76
|
+
}
|
|
77
|
+
|
|
79
78
|
/** Force re-read from disk (useful after writes) */
|
|
80
79
|
export function reload(): PalSettingsData {
|
|
81
80
|
cached = null;
|
|
@@ -8,17 +8,17 @@
|
|
|
8
8
|
* - Generate a summary report
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* pal cli relationship-reflect # Reflect on last 7 days
|
|
12
|
+
* pal cli relationship-reflect --month # Reflect on last 30 days
|
|
13
|
+
* pal cli relationship-reflect --dry-run # Preview without writing
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { resolve } from "node:path";
|
|
18
18
|
import { parseArgs } from "node:util";
|
|
19
|
-
import { readOpinions, saveOpinion, setLastReflectDate } from "
|
|
20
|
-
import { palHome } from "
|
|
21
|
-
import { emit } from "
|
|
19
|
+
import { readOpinions, saveOpinion, setLastReflectDate } from "../../hooks/lib/opinions";
|
|
20
|
+
import { palHome } from "../../hooks/lib/paths";
|
|
21
|
+
import { emit } from "../lib/emit";
|
|
22
22
|
import {
|
|
23
23
|
consoleLines,
|
|
24
24
|
formatReport,
|
|
@@ -27,7 +27,8 @@ import {
|
|
|
27
27
|
loadRatings,
|
|
28
28
|
planPromotions,
|
|
29
29
|
reportPath,
|
|
30
|
-
} from "
|
|
30
|
+
} from "../lib/relationship-reflect";
|
|
31
|
+
import { scriptArgs } from "../lib/script-args";
|
|
31
32
|
|
|
32
33
|
const HELP = `
|
|
33
34
|
RelationshipReflect — Periodic reflection + opinion promotion
|
|
@@ -36,9 +37,9 @@ Reads recent relationship notes and ratings. Promotes recurring
|
|
|
36
37
|
observations (O type) into tracked opinions with confidence scoring.
|
|
37
38
|
|
|
38
39
|
Usage:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
pal cli relationship-reflect Reflect on last 7 days (default)
|
|
41
|
+
pal cli relationship-reflect --month Reflect on last 30 days
|
|
42
|
+
pal cli relationship-reflect --dry-run Preview without writing
|
|
42
43
|
|
|
43
44
|
Output:
|
|
44
45
|
- Updates memory/relationship/opinions.json (confidence tracking)
|
|
@@ -56,9 +57,9 @@ function saveReport(report: string, period: string): string {
|
|
|
56
57
|
return filepath;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
function run() {
|
|
60
|
+
export function run(argv: string[] = scriptArgs()) {
|
|
60
61
|
const { values } = parseArgs({
|
|
61
|
-
args:
|
|
62
|
+
args: argv,
|
|
62
63
|
options: {
|
|
63
64
|
month: { type: "boolean" },
|
|
64
65
|
"dry-run": { type: "boolean" },
|