portable-agent-layer 0.71.0 → 0.72.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/package.json +1 -1
- package/src/cli/migrate.ts +1 -1
- package/src/cli/skill.ts +1 -1
- package/src/hooks/CompactRecover.ts +28 -86
- package/src/hooks/LedgerUnapplied.ts +3 -28
- package/src/hooks/LoadContext.ts +33 -60
- package/src/hooks/SecurityValidator.ts +16 -109
- package/src/hooks/handlers/failure-principle.ts +19 -44
- package/src/hooks/handlers/session-intelligence.ts +13 -70
- package/src/hooks/lib/capture-store.ts +103 -0
- package/src/hooks/lib/compact-recall.ts +89 -0
- package/src/hooks/lib/failure-principle.ts +98 -0
- package/src/hooks/lib/ledger-hook.ts +35 -0
- package/src/hooks/lib/ledger.ts +48 -1
- package/src/hooks/lib/security-gate.ts +159 -0
- package/src/hooks/lib/session-context.ts +74 -0
- package/src/tools/agent/algorithm-reflect.ts +28 -97
- package/src/tools/agent/analyze.ts +19 -120
- package/src/tools/agent/handoff-note.ts +29 -77
- package/src/tools/agent/project.ts +13 -134
- package/src/tools/agent/relationship-note.ts +27 -46
- package/src/tools/agent/synthesize.ts +1 -1
- package/src/tools/agent/thread.ts +43 -123
- package/src/tools/control-room/data.ts +2 -2
- package/src/tools/control-room/matrix.ts +1 -1
- package/src/tools/control-room/ui/ledger.tsx +2 -1
- package/src/tools/ledger/view.ts +3 -0
- package/src/tools/lib/algorithm-reflect.ts +84 -0
- package/src/tools/lib/analyze-report.ts +120 -0
- package/src/tools/lib/handoff-note.ts +88 -0
- package/src/tools/lib/note-flags.ts +59 -0
- package/src/tools/lib/project-isc.ts +151 -0
- package/src/tools/lib/relationship-reflect.ts +402 -0
- package/src/tools/lib/self-model.ts +499 -0
- package/src/tools/lib/session-usage.ts +216 -0
- package/src/tools/lib/skill-doctor.ts +457 -0
- package/src/tools/lib/thread.ts +119 -0
- package/src/tools/lib/token-report.ts +173 -0
- package/src/tools/lib/transcript-usage.ts +42 -0
- package/src/tools/lib/usage-buckets.ts +329 -0
- package/src/tools/relationship-reflect.ts +48 -412
- package/src/tools/self-model.ts +76 -558
- package/src/tools/session-summary.ts +8 -215
- package/src/tools/skill-doctor.ts +9 -444
- package/src/tools/token-cost.ts +18 -428
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate's decision, separated from the process that carries it out.
|
|
3
|
+
*
|
|
4
|
+
* SecurityValidator is spawned, so nothing can import it and nothing measures
|
|
5
|
+
* it — every rule about which tool names run a shell, which argument spells the
|
|
6
|
+
* path, and what the agent is told was unreachable from a test. The decision
|
|
7
|
+
* lives here instead; the entrypoint is left with stdin, stdout and the ledger.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { normalizeToolUse } from "./agent";
|
|
11
|
+
import { logDebug } from "./log";
|
|
12
|
+
import { checkBashCommand, checkFilePath } from "./security";
|
|
13
|
+
|
|
14
|
+
/** beforeShellExecution (Cursor only) — flat, no tool-name wrapper. */
|
|
15
|
+
interface ShellExecInput {
|
|
16
|
+
command: string;
|
|
17
|
+
sandbox?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type SecurityInput = Record<string, unknown> | ShellExecInput;
|
|
21
|
+
|
|
22
|
+
export interface GateRefusal {
|
|
23
|
+
tool: string;
|
|
24
|
+
/** The file, or the directory a refused command would have run in. */
|
|
25
|
+
target: string;
|
|
26
|
+
/** Set only when a shell was refused: it has no file to name. */
|
|
27
|
+
command?: string;
|
|
28
|
+
/** Why, for the ledger. */
|
|
29
|
+
reason: string;
|
|
30
|
+
/** Why, in the words the agent is given — the two differ only by framing. */
|
|
31
|
+
message: string;
|
|
32
|
+
hookEventName?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A name this list misses is a command this hook waves through, so both sets mirror
|
|
36
|
+
// the tool names VS Code's own Copilot build ships in its shell and edit tool sets.
|
|
37
|
+
const SHELL_TOOLS = [
|
|
38
|
+
"bash",
|
|
39
|
+
"shell",
|
|
40
|
+
"powershell",
|
|
41
|
+
"local_shell",
|
|
42
|
+
"runinterminal",
|
|
43
|
+
"run_in_terminal",
|
|
44
|
+
"terminal",
|
|
45
|
+
"execute_command",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const FILE_WRITE_TOOLS = [
|
|
49
|
+
"write",
|
|
50
|
+
"edit",
|
|
51
|
+
"multiedit",
|
|
52
|
+
"write_file",
|
|
53
|
+
"apply_patch",
|
|
54
|
+
"applypatch",
|
|
55
|
+
"create",
|
|
56
|
+
"create_file",
|
|
57
|
+
"createfile",
|
|
58
|
+
"str_replace",
|
|
59
|
+
"str_replace_editor",
|
|
60
|
+
"insert",
|
|
61
|
+
"insert_edit_into_file",
|
|
62
|
+
"replace_string_in_file",
|
|
63
|
+
"multi_replace_string_in_file",
|
|
64
|
+
"replacestring",
|
|
65
|
+
"edit_notebook_file",
|
|
66
|
+
"notebookedit",
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
function isShellExec(input: SecurityInput): input is ShellExecInput {
|
|
70
|
+
return !("tool_name" in input) && !("toolName" in input) && "command" in input;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** First of `keys` present as a non-empty string — agents disagree on argument spelling. */
|
|
74
|
+
function firstStringArg(
|
|
75
|
+
args: Record<string, unknown>,
|
|
76
|
+
keys: string[]
|
|
77
|
+
): string | undefined {
|
|
78
|
+
for (const key of keys) {
|
|
79
|
+
const value = args[key];
|
|
80
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Tool names that run a shell command, across every agent's naming. */
|
|
86
|
+
function runsShellCommand(toolName: string): boolean {
|
|
87
|
+
return SHELL_TOOLS.includes(toolName.toLowerCase());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Tool names that write to a file, across every agent's naming. */
|
|
91
|
+
function writesFile(toolName: string): boolean {
|
|
92
|
+
return FILE_WRITE_TOOLS.includes(toolName.toLowerCase());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* What the gate would do with this call, or null to let it through.
|
|
97
|
+
*
|
|
98
|
+
* `cwd` is passed rather than read so a test can pin the target of a refused
|
|
99
|
+
* command, which has no file of its own to name.
|
|
100
|
+
*/
|
|
101
|
+
export function decideRefusal(input: SecurityInput, cwd: string): GateRefusal | null {
|
|
102
|
+
if (isShellExec(input)) {
|
|
103
|
+
const reason = checkBashCommand(input.command);
|
|
104
|
+
if (!reason) return null;
|
|
105
|
+
return {
|
|
106
|
+
tool: "shell",
|
|
107
|
+
target: cwd,
|
|
108
|
+
command: input.command,
|
|
109
|
+
reason,
|
|
110
|
+
message: `Blocked: ${reason}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const toolUse = normalizeToolUse(input);
|
|
115
|
+
if (!toolUse) return null;
|
|
116
|
+
|
|
117
|
+
// Each agent names its shell/write tools differently; log the real name so an
|
|
118
|
+
// unrecognized one shows up here instead of silently skipping the check.
|
|
119
|
+
logDebug(
|
|
120
|
+
"SecurityValidator",
|
|
121
|
+
`toolName=${toolUse.toolName} args=${Object.keys(toolUse.toolInput).join(",")}`
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const command = firstStringArg(toolUse.toolInput, ["command", "commandLine", "script"]);
|
|
125
|
+
if (runsShellCommand(toolUse.toolName) && command) {
|
|
126
|
+
const reason = checkBashCommand(command);
|
|
127
|
+
// "No output" from a downstream tool is indistinguishable between "denied,
|
|
128
|
+
// never ran" and "ran, produced nothing" — logging the verdict here, next
|
|
129
|
+
// to the literal command, is what actually tells the two apart.
|
|
130
|
+
const verdict = reason ? `BLOCK(${reason})` : "ALLOW";
|
|
131
|
+
logDebug("SecurityValidator", `bashVerdict=${verdict} command=${command}`);
|
|
132
|
+
if (reason) {
|
|
133
|
+
return {
|
|
134
|
+
tool: toolUse.toolName,
|
|
135
|
+
target: cwd,
|
|
136
|
+
command,
|
|
137
|
+
reason,
|
|
138
|
+
message: `Blocked: ${reason}`,
|
|
139
|
+
hookEventName: toolUse.hookEventName,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const filePath = firstStringArg(toolUse.toolInput, ["file_path", "filePath", "path"]);
|
|
145
|
+
if (writesFile(toolUse.toolName) && filePath) {
|
|
146
|
+
const reason = checkFilePath(filePath);
|
|
147
|
+
if (reason) {
|
|
148
|
+
return {
|
|
149
|
+
tool: toolUse.toolName,
|
|
150
|
+
target: filePath,
|
|
151
|
+
reason,
|
|
152
|
+
message: reason,
|
|
153
|
+
hookEventName: toolUse.hookEventName,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each agent is handed at session start, and in which envelope.
|
|
3
|
+
*
|
|
4
|
+
* The four runtimes disagree on all of it: whether AGENTS.md is already loaded,
|
|
5
|
+
* which JSON key carries injected context, and whether it is read from stdout at
|
|
6
|
+
* all. This has been wrong in production before — Copilot silently received no
|
|
7
|
+
* context for weeks — and none of it was reachable from a test.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface ContextEnvelope {
|
|
11
|
+
/** Whether the agent parses stdout as JSON or reads it as raw text. */
|
|
12
|
+
kind: "json" | "text";
|
|
13
|
+
payload: string;
|
|
14
|
+
/** Copilot only: the same context, also written where its extension reads. */
|
|
15
|
+
file?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A subagent gets none of this. Its parent already carries the context, and
|
|
20
|
+
* paying for it again on every spawn is the whole cost of a cheap subagent.
|
|
21
|
+
*/
|
|
22
|
+
export function isSubagentSession(env: NodeJS.ProcessEnv): boolean {
|
|
23
|
+
if (env.CLAUDE_AGENT_TYPE !== undefined) return true;
|
|
24
|
+
return env.CLAUDE_PROJECT_DIR?.includes("/.claude/Agents/") ?? false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Copilot and Cursor read AGENTS.md natively from nothing, so it is prepended
|
|
29
|
+
* here; Codex reaches it through a symlink and Claude Code loads it itself.
|
|
30
|
+
*/
|
|
31
|
+
export function needsAgentsMd(agent: string): boolean {
|
|
32
|
+
return agent === "copilot" || agent === "cursor";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Copilot also writes the same text to a file, because its CLI reads stdout
|
|
37
|
+
* while the VS Code extension reads only ~/.copilot/instructions/.
|
|
38
|
+
*/
|
|
39
|
+
export function contextEnvelope(
|
|
40
|
+
agent: string,
|
|
41
|
+
reminder: string,
|
|
42
|
+
agentsMd: string
|
|
43
|
+
): ContextEnvelope | null {
|
|
44
|
+
const merged = needsAgentsMd(agent)
|
|
45
|
+
? [agentsMd, reminder].filter(Boolean).join("\n\n")
|
|
46
|
+
: reminder;
|
|
47
|
+
if (!merged) return null;
|
|
48
|
+
|
|
49
|
+
if (agent === "copilot") {
|
|
50
|
+
return {
|
|
51
|
+
kind: "json",
|
|
52
|
+
payload: JSON.stringify({ additionalContext: merged }),
|
|
53
|
+
file: merged,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (agent === "cursor") {
|
|
57
|
+
return { kind: "json", payload: JSON.stringify({ additional_context: merged }) };
|
|
58
|
+
}
|
|
59
|
+
if (agent === "codex") {
|
|
60
|
+
return {
|
|
61
|
+
kind: "json",
|
|
62
|
+
payload: JSON.stringify({
|
|
63
|
+
hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: merged },
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Claude Code, and opencode which uses the plugin path rather than this hook.
|
|
68
|
+
return { kind: "text", payload: merged };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The file Copilot's VS Code extension reads, with the applyTo header it needs. */
|
|
72
|
+
export function copilotInstructions(context: string): string {
|
|
73
|
+
return `---\napplyTo: "**"\n---\n\n${context}`;
|
|
74
|
+
}
|
|
@@ -15,87 +15,36 @@
|
|
|
15
15
|
|
|
16
16
|
import { appendFileSync } from "node:fs";
|
|
17
17
|
import { parseArgs } from "node:util";
|
|
18
|
-
import { currentAttribution, type RecordAttribution } from "../../hooks/lib/actor";
|
|
19
|
-
import { encodeAnchor } from "../../hooks/lib/anchor";
|
|
20
18
|
import { paths } from "../../hooks/lib/paths";
|
|
19
|
+
import { buildReflection, intOr, reflectionLine } from "../lib/algorithm-reflect";
|
|
21
20
|
import { emit } from "../lib/emit";
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
const HELP = `
|
|
23
|
+
AlgorithmReflect — Log algorithm performance after LEARN phase
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
Usage:
|
|
26
|
+
bun run tool:algorithm-reflect --task "description" --criteria N --passed N --failed N --sentiment 1-10 \\
|
|
27
|
+
--q1 "self reflection" --q2 "algorithm reflection" --q3 "AI reflection"
|
|
28
|
+
|
|
29
|
+
Arguments:
|
|
30
|
+
--task Brief task description
|
|
31
|
+
--criteria Total criteria count
|
|
32
|
+
--passed Criteria passed
|
|
33
|
+
--failed Criteria failed
|
|
34
|
+
--sentiment Implied satisfaction 1-10
|
|
35
|
+
--q1 Q1 — Self: what I'd do differently
|
|
36
|
+
--q2 Q2 — Algorithm: structural improvement
|
|
37
|
+
--q3 Q3 — AI: reasoning blind spot
|
|
38
|
+
--scope general (default) | task-specific — is the algorithm idea reusable or task-bound?
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
Output: algorithm-reflections.jsonl in memory/learning/reflections/
|
|
41
|
+
`;
|
|
41
42
|
|
|
42
43
|
function reflectionsPath(): string {
|
|
43
|
-
paths.reflections();
|
|
44
|
+
paths.reflections();
|
|
44
45
|
return paths.reflectionsFile();
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
/**
|
|
48
|
-
* Assemble a reflection record from CLI-style input, stamping the current
|
|
49
|
-
* cwd (anchored) and this machine's id. Exported so the stamping logic is
|
|
50
|
-
* directly testable without going through argv parsing.
|
|
51
|
-
*/
|
|
52
|
-
export function buildReflection(input: {
|
|
53
|
-
task: string;
|
|
54
|
-
q1: string;
|
|
55
|
-
q2: string;
|
|
56
|
-
q3: string;
|
|
57
|
-
criteria_count?: number;
|
|
58
|
-
criteria_passed?: number;
|
|
59
|
-
criteria_failed?: number;
|
|
60
|
-
sentiment?: number;
|
|
61
|
-
scope?: string;
|
|
62
|
-
}): AlgorithmReflection {
|
|
63
|
-
return {
|
|
64
|
-
timestamp: new Date().toISOString(),
|
|
65
|
-
cwd: encodeAnchor(process.cwd()),
|
|
66
|
-
...currentAttribution(),
|
|
67
|
-
task: input.task,
|
|
68
|
-
criteria_count: input.criteria_count ?? 0,
|
|
69
|
-
criteria_passed: input.criteria_passed ?? 0,
|
|
70
|
-
criteria_failed: input.criteria_failed ?? 0,
|
|
71
|
-
sentiment: Math.max(1, Math.min(10, input.sentiment ?? 5)),
|
|
72
|
-
q1: input.q1,
|
|
73
|
-
q2: input.q2,
|
|
74
|
-
q3: input.q3,
|
|
75
|
-
// Default to general (the ~94% case); only "task-specific" suppresses it
|
|
76
|
-
// from algorithm-update clustering.
|
|
77
|
-
scope: input.scope === "task-specific" ? "task-specific" : "general",
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function appendReflection(reflection: AlgorithmReflection): {
|
|
82
|
-
success: boolean;
|
|
83
|
-
message: string;
|
|
84
|
-
path: string;
|
|
85
|
-
} {
|
|
86
|
-
const p = reflectionsPath();
|
|
87
|
-
const line = `${JSON.stringify(reflection)}\n`;
|
|
88
|
-
appendFileSync(p, line, "utf-8");
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
success: true,
|
|
92
|
-
message: `Reflection logged: ${reflection.criteria_passed}/${reflection.criteria_count} passed, sentiment ${reflection.sentiment}/10`,
|
|
93
|
-
path: p,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ── CLI ──
|
|
98
|
-
|
|
99
48
|
function run() {
|
|
100
49
|
const { values } = parseArgs({
|
|
101
50
|
args: Bun.argv.slice(2),
|
|
@@ -114,26 +63,7 @@ function run() {
|
|
|
114
63
|
});
|
|
115
64
|
|
|
116
65
|
if (values.help) {
|
|
117
|
-
console.log(
|
|
118
|
-
AlgorithmReflect — Log algorithm performance after LEARN phase
|
|
119
|
-
|
|
120
|
-
Usage:
|
|
121
|
-
bun run tool:algorithm-reflect --task "description" --criteria N --passed N --failed N --sentiment 1-10 \\
|
|
122
|
-
--q1 "self reflection" --q2 "algorithm reflection" --q3 "AI reflection"
|
|
123
|
-
|
|
124
|
-
Arguments:
|
|
125
|
-
--task Brief task description
|
|
126
|
-
--criteria Total criteria count
|
|
127
|
-
--passed Criteria passed
|
|
128
|
-
--failed Criteria failed
|
|
129
|
-
--sentiment Implied satisfaction 1-10
|
|
130
|
-
--q1 Q1 — Self: what I'd do differently
|
|
131
|
-
--q2 Q2 — Algorithm: structural improvement
|
|
132
|
-
--q3 Q3 — AI: reasoning blind spot
|
|
133
|
-
--scope general (default) | task-specific — is the algorithm idea reusable or task-bound?
|
|
134
|
-
|
|
135
|
-
Output: algorithm-reflections.jsonl in memory/learning/reflections/
|
|
136
|
-
`);
|
|
66
|
+
console.log(HELP);
|
|
137
67
|
process.exit(0);
|
|
138
68
|
}
|
|
139
69
|
|
|
@@ -147,15 +77,16 @@ Output: algorithm-reflections.jsonl in memory/learning/reflections/
|
|
|
147
77
|
q1: values.q1,
|
|
148
78
|
q2: values.q2,
|
|
149
79
|
q3: values.q3,
|
|
150
|
-
criteria_count:
|
|
151
|
-
criteria_passed:
|
|
152
|
-
criteria_failed:
|
|
153
|
-
sentiment:
|
|
80
|
+
criteria_count: intOr(values.criteria, 0),
|
|
81
|
+
criteria_passed: intOr(values.passed, 0),
|
|
82
|
+
criteria_failed: intOr(values.failed, 0),
|
|
83
|
+
sentiment: intOr(values.sentiment, 5),
|
|
154
84
|
scope: values.scope,
|
|
155
85
|
});
|
|
156
86
|
|
|
157
|
-
const
|
|
158
|
-
|
|
87
|
+
const path = reflectionsPath();
|
|
88
|
+
appendFileSync(path, reflectionLine(reflection), "utf-8");
|
|
89
|
+
emit.receipt(path, {
|
|
159
90
|
passed: reflection.criteria_passed,
|
|
160
91
|
of: reflection.criteria_count,
|
|
161
92
|
scope: reflection.scope,
|
|
@@ -5,130 +5,17 @@
|
|
|
5
5
|
* Reads failures and session learnings, finds recurring patterns,
|
|
6
6
|
* summarizes ratings, and generates recommendations.
|
|
7
7
|
*
|
|
8
|
+
* What the report says is in lib/analyze-report.ts.
|
|
9
|
+
*
|
|
8
10
|
* Usage: bun run tool:analyze
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { parseArgs } from "node:util";
|
|
12
14
|
import { writeLastAnalyzeDate } from "../../hooks/lib/analyze-nudge";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
// ── ANSI Colors ──
|
|
16
|
-
|
|
17
|
-
const c = {
|
|
18
|
-
bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
|
|
19
|
-
dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
|
|
20
|
-
cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
|
|
21
|
-
yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
|
|
22
|
-
green: (s: string) => `\x1b[32m${s}\x1b[0m`,
|
|
23
|
-
red: (s: string) => `\x1b[31m${s}\x1b[0m`,
|
|
24
|
-
magenta: (s: string) => `\x1b[35m${s}\x1b[0m`,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
function printReport(result: AnalysisResult): void {
|
|
28
|
-
const hasPatterns = result.candidates.length > 0 || result.emerging.length > 0;
|
|
29
|
-
const hasRatings = result.ratings !== null;
|
|
30
|
-
|
|
31
|
-
if (!hasPatterns && !hasRatings) {
|
|
32
|
-
console.log("\n No patterns or ratings data found.\n");
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (result.ratings) {
|
|
37
|
-
const r = result.ratings;
|
|
38
|
-
const lowOrMid = r.average <= 4 ? c.red : c.yellow;
|
|
39
|
-
const avgColor = r.average >= 7 ? c.green : lowOrMid;
|
|
40
|
-
const ratingStr = `${r.average.toFixed(1)}/10`;
|
|
41
|
-
const lowStr = `Low (≤4): ${r.low.count}`;
|
|
42
|
-
const highStr = `High (≥7): ${r.high.count}`;
|
|
43
|
-
console.log(
|
|
44
|
-
`\n ${c.bold("Ratings:")} ${avgColor(ratingStr)} avg (${r.total} total)`
|
|
45
|
-
);
|
|
46
|
-
console.log(` ${c.red(lowStr)} | ${c.green(highStr)}`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (result.candidates.length > 0) {
|
|
50
|
-
const graduationHeader = `Graduation Report — ${result.candidates.length} pattern(s) detected`;
|
|
51
|
-
console.log(`\n ${c.bold(c.green(graduationHeader))}\n`);
|
|
52
|
-
console.log(` ${c.dim("─────────────────────────────────────────────────")}\n`);
|
|
53
|
-
|
|
54
|
-
for (const candidate of result.candidates) {
|
|
55
|
-
const domain = `[${candidate.domain}]`;
|
|
56
|
-
const count = `${candidate.entries.length}x`;
|
|
57
|
-
console.log(` ${c.cyan(domain)} ${c.bold(count)} occurrences`);
|
|
58
|
-
console.log("");
|
|
59
|
-
|
|
60
|
-
for (const entry of candidate.entries) {
|
|
61
|
-
const sourceType = entry.source.startsWith("failure:") ? "failure" : "learning";
|
|
62
|
-
const tag =
|
|
63
|
-
sourceType === "failure"
|
|
64
|
-
? c.red(`[${sourceType}]`)
|
|
65
|
-
: c.yellow(`[${sourceType}]`);
|
|
66
|
-
console.log(
|
|
67
|
-
` ${c.dim(entry.date || "unknown")} ${tag} ${entry.text.slice(0, 100)}`
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
console.log(`\n ${c.dim("Files:")}`);
|
|
72
|
-
for (const entry of candidate.entries) {
|
|
73
|
-
console.log(` ${c.dim(entry.path)}`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
console.log("");
|
|
77
|
-
const framePath = `memory/wisdom/frames/${candidate.domain}.md`;
|
|
78
|
-
console.log(` Target frame: ${c.magenta(framePath)}`);
|
|
79
|
-
console.log(` ${c.dim("─────────────────────────────────────────────────")}\n`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (result.emerging.length > 0) {
|
|
84
|
-
console.log(` ${c.bold(c.yellow("Emerging (2x — one more to graduate)"))}\n`);
|
|
85
|
-
for (const group of result.emerging) {
|
|
86
|
-
const domain = `[${group.domain}]`;
|
|
87
|
-
const count = `${group.entries.length}x`;
|
|
88
|
-
console.log(` ${c.cyan(domain)} ${c.bold(count)}`);
|
|
89
|
-
for (const entry of group.entries) {
|
|
90
|
-
const sourceType = entry.source.startsWith("failure:") ? "failure" : "learning";
|
|
91
|
-
const tag =
|
|
92
|
-
sourceType === "failure"
|
|
93
|
-
? c.red(`[${sourceType}]`)
|
|
94
|
-
: c.yellow(`[${sourceType}]`);
|
|
95
|
-
console.log(
|
|
96
|
-
` ${c.dim(entry.date || "unknown")} ${tag} ${entry.text.slice(0, 80)}`
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
console.log(" Files:");
|
|
100
|
-
for (const entry of group.entries) {
|
|
101
|
-
console.log(` ${c.dim(entry.path)}`);
|
|
102
|
-
}
|
|
103
|
-
console.log("");
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (result.recommendations.length > 0) {
|
|
108
|
-
console.log(` ${c.bold("Recommendations:")}\n`);
|
|
109
|
-
for (const rec of result.recommendations) {
|
|
110
|
-
console.log(` ${rec}`);
|
|
111
|
-
}
|
|
112
|
-
console.log("");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (result.candidates.length > 0) {
|
|
116
|
-
console.log(` To crystallize: add a line to the wisdom frame file.`);
|
|
117
|
-
console.log(` Format: ${c.green("- Your principle here [CRYSTAL: 85%]")}\n`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
15
|
+
import { analyze } from "../../hooks/lib/graduation";
|
|
16
|
+
import { reportLines } from "../lib/analyze-report";
|
|
120
17
|
|
|
121
|
-
|
|
122
|
-
const { values } = parseArgs({
|
|
123
|
-
args: argv,
|
|
124
|
-
options: {
|
|
125
|
-
help: { type: "boolean", short: "h" },
|
|
126
|
-
actionable: { type: "boolean", short: "a" },
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
if (values.help) {
|
|
131
|
-
console.log(`
|
|
18
|
+
const HELP = `
|
|
132
19
|
PAL Learning Analysis — unified graduation + ratings report
|
|
133
20
|
|
|
134
21
|
Reads all captured failures (rating ≤3) and session learnings,
|
|
@@ -147,12 +34,24 @@ export async function run(argv: string[] = Bun.argv.slice(2)) {
|
|
|
147
34
|
- Your principle here [CRYSTAL: 85%]
|
|
148
35
|
|
|
149
36
|
Usage: pal cli analyze [--actionable]
|
|
150
|
-
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
export async function run(argv: string[] = Bun.argv.slice(2)) {
|
|
40
|
+
const { values } = parseArgs({
|
|
41
|
+
args: argv,
|
|
42
|
+
options: {
|
|
43
|
+
help: { type: "boolean", short: "h" },
|
|
44
|
+
actionable: { type: "boolean", short: "a" },
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (values.help) {
|
|
49
|
+
console.log(HELP);
|
|
151
50
|
process.exit(0);
|
|
152
51
|
}
|
|
153
52
|
|
|
154
53
|
const result = await analyze({ actionable: values.actionable });
|
|
155
|
-
|
|
54
|
+
for (const line of reportLines(result)) console.log(line);
|
|
156
55
|
writeLastAnalyzeDate(new Date().toISOString());
|
|
157
56
|
}
|
|
158
57
|
|