@damian87/omp 0.10.0 → 0.13.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/.github/copilot-instructions.md +16 -0
- package/.github/skills/jira-ticket/SKILL.md +4 -4
- package/.github/skills/omp-autopilot/SKILL.md +4 -0
- package/.github/skills/research-codebase/SKILL.md +4 -0
- package/.github/skills/schedule/SKILL.md +4 -0
- package/.github/skills/team/SKILL.md +4 -0
- package/.github/skills/ultrawork/SKILL.md +4 -0
- package/.github/skills/weighted-consensus/SKILL.md +4 -0
- package/README.md +4 -1
- package/dist/src/cli.js +134 -4
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands/comms.d.ts +2 -0
- package/dist/src/commands/comms.js +110 -0
- package/dist/src/commands/comms.js.map +1 -0
- package/dist/src/commands/council.d.ts +2 -0
- package/dist/src/commands/council.js +77 -0
- package/dist/src/commands/council.js.map +1 -0
- package/dist/src/commands/env.d.ts +2 -0
- package/dist/src/commands/env.js +95 -0
- package/dist/src/commands/env.js.map +1 -0
- package/dist/src/commands/gateway.d.ts +3 -0
- package/dist/src/commands/gateway.js +129 -0
- package/dist/src/commands/gateway.js.map +1 -0
- package/dist/src/commands/memory.d.ts +7 -0
- package/dist/src/commands/memory.js +202 -0
- package/dist/src/commands/memory.js.map +1 -0
- package/dist/src/commands/mode.d.ts +4 -0
- package/dist/src/commands/mode.js +119 -0
- package/dist/src/commands/mode.js.map +1 -0
- package/dist/src/commands/schedule.d.ts +2 -0
- package/dist/src/commands/schedule.js +91 -0
- package/dist/src/commands/schedule.js.map +1 -0
- package/dist/src/commands/team.d.ts +2 -0
- package/dist/src/commands/team.js +146 -0
- package/dist/src/commands/team.js.map +1 -0
- package/dist/src/commands/utils.d.ts +13 -0
- package/dist/src/commands/utils.js +68 -0
- package/dist/src/commands/utils.js.map +1 -0
- package/dist/src/copilot/doctor.d.ts +1 -0
- package/dist/src/copilot/doctor.js +226 -27
- package/dist/src/copilot/doctor.js.map +1 -1
- package/dist/src/copilot/launch.js +13 -5
- package/dist/src/copilot/launch.js.map +1 -1
- package/dist/src/copilot/setup.js +13 -0
- package/dist/src/copilot/setup.js.map +1 -1
- package/dist/src/cost/index.d.ts +3 -0
- package/dist/src/cost/index.js +4 -0
- package/dist/src/cost/index.js.map +1 -0
- package/dist/src/cost/ledger.d.ts +21 -0
- package/dist/src/cost/ledger.js +72 -0
- package/dist/src/cost/ledger.js.map +1 -0
- package/dist/src/cost/summary.d.ts +22 -0
- package/dist/src/cost/summary.js +68 -0
- package/dist/src/cost/summary.js.map +1 -0
- package/dist/src/cost/tokenize.d.ts +7 -0
- package/dist/src/cost/tokenize.js +24 -0
- package/dist/src/cost/tokenize.js.map +1 -0
- package/dist/src/goal.js +6 -8
- package/dist/src/goal.js.map +1 -1
- package/dist/src/instructions-memory.js +26 -3
- package/dist/src/instructions-memory.js.map +1 -1
- package/dist/src/memory-review/apply.d.ts +7 -0
- package/dist/src/memory-review/apply.js +75 -0
- package/dist/src/memory-review/apply.js.map +1 -0
- package/dist/src/memory-review/config.d.ts +22 -0
- package/dist/src/memory-review/config.js +54 -0
- package/dist/src/memory-review/config.js.map +1 -0
- package/dist/src/memory-review/guard.d.ts +5 -0
- package/dist/src/memory-review/guard.js +37 -0
- package/dist/src/memory-review/guard.js.map +1 -0
- package/dist/src/memory-review/index.d.ts +17 -0
- package/dist/src/memory-review/index.js +87 -0
- package/dist/src/memory-review/index.js.map +1 -0
- package/dist/src/memory-review/prompt.d.ts +18 -0
- package/dist/src/memory-review/prompt.js +89 -0
- package/dist/src/memory-review/prompt.js.map +1 -0
- package/dist/src/memory-review/spawn.d.ts +2 -0
- package/dist/src/memory-review/spawn.js +51 -0
- package/dist/src/memory-review/spawn.js.map +1 -0
- package/dist/src/memory-review/transcript.d.ts +24 -0
- package/dist/src/memory-review/transcript.js +212 -0
- package/dist/src/memory-review/transcript.js.map +1 -0
- package/dist/src/memory-review/trigger.d.ts +21 -0
- package/dist/src/memory-review/trigger.js +27 -0
- package/dist/src/memory-review/trigger.js.map +1 -0
- package/dist/src/project-memory.d.ts +9 -0
- package/dist/src/project-memory.js +72 -1
- package/dist/src/project-memory.js.map +1 -1
- package/dist/src/state.js +25 -37
- package/dist/src/state.js.map +1 -1
- package/dist/src/utils/fs.d.ts +14 -0
- package/dist/src/utils/fs.js +32 -0
- package/dist/src/utils/fs.js.map +1 -0
- package/dist/src/utils/paths.d.ts +14 -0
- package/dist/src/utils/paths.js +21 -0
- package/dist/src/utils/paths.js.map +1 -0
- package/docs/general-skills.md +1 -0
- package/docs/memory-mode.md +94 -0
- package/hooks/hooks.json +9 -2
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/error.mjs +9 -7
- package/scripts/lib/cost-ledger.mjs +91 -0
- package/scripts/lib/hook-input.mjs +51 -0
- package/scripts/lib/hook-output.mjs +53 -11
- package/scripts/lib/memory-review-trigger.mjs +59 -0
- package/scripts/lib/minify.mjs +80 -0
- package/scripts/lib/pending-directives.mjs +36 -0
- package/scripts/post-tool-use-failure.mjs +21 -0
- package/scripts/post-tool-use.mjs +71 -8
- package/scripts/pre-tool-use.mjs +8 -6
- package/scripts/prompt-submit.mjs +12 -5
- package/scripts/session-end.mjs +15 -5
- package/scripts/session-start.mjs +9 -4
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
function parseMaybeJson(value) {
|
|
2
|
+
if (typeof value !== "string") return value;
|
|
3
|
+
const trimmed = value.trim();
|
|
4
|
+
if (!trimmed) return value;
|
|
5
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return value;
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(trimmed);
|
|
8
|
+
} catch {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeResult(raw) {
|
|
14
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
15
|
+
const resultType = raw.resultType ?? raw.result_type;
|
|
16
|
+
const textResultForLlm = raw.textResultForLlm ?? raw.text_result_for_llm;
|
|
17
|
+
if (resultType == null && textResultForLlm == null) return undefined;
|
|
18
|
+
return {
|
|
19
|
+
resultType: resultType ?? "success",
|
|
20
|
+
textResultForLlm: textResultForLlm == null ? "" : String(textResultForLlm),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function normalizeHookInput(data = {}, options = {}) {
|
|
25
|
+
const payload = data && typeof data === "object" ? data : {};
|
|
26
|
+
const cwd = payload.cwd ?? payload.directory ?? options.cwd ?? process.cwd();
|
|
27
|
+
const toolResult = normalizeResult(payload.toolResult ?? payload.tool_result ?? payload.toolOutput);
|
|
28
|
+
const error = payload.error?.message ?? payload.error ?? payload.message;
|
|
29
|
+
return {
|
|
30
|
+
raw: payload,
|
|
31
|
+
hookEventName: payload.hookEventName ?? payload.hook_event_name,
|
|
32
|
+
sessionId: payload.sessionId ?? payload.session_id ?? "unknown",
|
|
33
|
+
timestamp: payload.timestamp,
|
|
34
|
+
cwd,
|
|
35
|
+
directory: cwd,
|
|
36
|
+
prompt: payload.prompt ?? payload.message?.content ?? "",
|
|
37
|
+
toolName: payload.toolName ?? payload.tool_name ?? "unknown",
|
|
38
|
+
toolArgs: parseMaybeJson(payload.toolArgs ?? payload.tool_input ?? payload.toolInput),
|
|
39
|
+
toolResult,
|
|
40
|
+
error: error == null ? undefined : String(error),
|
|
41
|
+
transcriptPath: payload.transcriptPath ?? payload.transcript_path,
|
|
42
|
+
stopReason: payload.stopReason ?? payload.stop_reason,
|
|
43
|
+
trigger: payload.trigger,
|
|
44
|
+
customInstructions: payload.customInstructions ?? payload.custom_instructions,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseHookInput(raw, options = {}) {
|
|
49
|
+
const data = raw ? JSON.parse(raw) : {};
|
|
50
|
+
return normalizeHookInput(data, options);
|
|
51
|
+
}
|
|
@@ -4,23 +4,65 @@ import { ompRoot } from "./omp-root.mjs";
|
|
|
4
4
|
|
|
5
5
|
// Hook scripts run under BOTH GitHub Copilot CLI (camelCase events, top-level
|
|
6
6
|
// `additionalContext` / `{decision,reason}` / `{permissionDecision}`) and Claude
|
|
7
|
-
// Code (`{continue, hookSpecificOutput}` / `{decision, reason}`).
|
|
8
|
-
// every output object carries both
|
|
9
|
-
// it does not recognize. See
|
|
7
|
+
// Code (`{continue, hookSpecificOutput}` / `{decision, reason}`). The injection
|
|
8
|
+
// path (`printContinue`) dual-emits: every output object carries both
|
|
9
|
+
// vocabularies, and each host ignores the keys it does not recognize. See
|
|
10
|
+
// docs/plans/copilot-native-hooks.md.
|
|
11
|
+
//
|
|
12
|
+
// The cost/minification path uses the documented Copilot builder shapes
|
|
13
|
+
// (`buildContinueOutput`/`buildAdditionalContextOutput`/`buildModifiedResultOutput`/
|
|
14
|
+
// `buildPermissionDecisionOutput`). An empty `{}` is a no-op "continue" for both
|
|
15
|
+
// hosts, so these builders coexist with the dual-emit injection path.
|
|
10
16
|
|
|
11
17
|
/** Project directory from hook input — Copilot sends `cwd`, Claude sends `directory`. */
|
|
12
18
|
export function hookCwd(data) {
|
|
13
19
|
return data?.cwd ?? data?.directory ?? process.cwd();
|
|
14
20
|
}
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
export function buildContinueOutput() {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildAdditionalContextOutput(additionalContext = "") {
|
|
27
|
+
return additionalContext ? { additionalContext } : buildContinueOutput();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildModifiedResultOutput(textResultForLlm, additionalContext = "", resultType = "success") {
|
|
31
|
+
return {
|
|
32
|
+
modifiedResult: {
|
|
33
|
+
resultType,
|
|
34
|
+
textResultForLlm,
|
|
35
|
+
},
|
|
36
|
+
...(additionalContext ? { additionalContext } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildPermissionDecisionOutput(permissionDecision, permissionDecisionReason, modifiedArgs) {
|
|
41
|
+
return {
|
|
42
|
+
permissionDecision,
|
|
43
|
+
...(permissionDecisionReason ? { permissionDecisionReason } : {}),
|
|
44
|
+
...(modifiedArgs == null ? {} : { modifiedArgs }),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* sessionStart / userPromptSubmitted injection. When there is context to inject,
|
|
50
|
+
* dual-emit it for both hosts (Copilot top-level `additionalContext` + Claude
|
|
51
|
+
* `continue`/`hookSpecificOutput`). With nothing to inject, emit an empty `{}` —
|
|
52
|
+
* a no-op "continue" understood by both hosts and the zero-cost default.
|
|
53
|
+
*/
|
|
17
54
|
export function printContinue(hookEventName, additionalContext = "") {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
output.hookSpecificOutput = { hookEventName, additionalContext }; // Claude Code
|
|
55
|
+
if (!additionalContext) {
|
|
56
|
+
console.log(JSON.stringify(buildContinueOutput()));
|
|
57
|
+
return;
|
|
22
58
|
}
|
|
23
|
-
console.log(
|
|
59
|
+
console.log(
|
|
60
|
+
JSON.stringify({
|
|
61
|
+
continue: true,
|
|
62
|
+
additionalContext, // Copilot CLI
|
|
63
|
+
hookSpecificOutput: { hookEventName, additionalContext }, // Claude Code
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
24
66
|
}
|
|
25
67
|
|
|
26
68
|
/** agentStop (Copilot) / Stop (Claude): both honor {decision, reason}. */
|
|
@@ -39,11 +81,11 @@ export function printPermission(permissionDecision, reason = "", modifiedArgs) {
|
|
|
39
81
|
}
|
|
40
82
|
|
|
41
83
|
export function printBlock(reason) {
|
|
42
|
-
console.log(JSON.stringify(
|
|
84
|
+
console.log(JSON.stringify(buildPermissionDecisionOutput("deny", reason)));
|
|
43
85
|
}
|
|
44
86
|
|
|
45
87
|
export function failOpen() {
|
|
46
|
-
console.log(JSON.stringify(
|
|
88
|
+
console.log(JSON.stringify(buildContinueOutput()));
|
|
47
89
|
}
|
|
48
90
|
|
|
49
91
|
export function appendHookLog(directory, hookName, payload) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { ompRoot } from "./omp-root.mjs";
|
|
6
|
+
|
|
7
|
+
// sessionEnd hook → end-of-session memory review. The hook must return fast
|
|
8
|
+
// (5s timeout), so this only DETACHES `omp memory-review` and returns. The
|
|
9
|
+
// downstream claim guard de-dupes against the wrapper fallback. Fail-open:
|
|
10
|
+
// any error means "don't trigger", never throw into the hook.
|
|
11
|
+
|
|
12
|
+
function readMemoryMode(cwd) {
|
|
13
|
+
const env = process.env.OMP_MEMORY_MODE;
|
|
14
|
+
if (env === "on") return "on";
|
|
15
|
+
if (env === "off") return "off";
|
|
16
|
+
try {
|
|
17
|
+
const p = join(ompRoot(cwd), ".omp", "config.json");
|
|
18
|
+
if (!existsSync(p)) return "off";
|
|
19
|
+
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
20
|
+
return raw && raw.memoryMode === "on" ? "on" : "off";
|
|
21
|
+
} catch {
|
|
22
|
+
return "off";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function defaultDistPath() {
|
|
27
|
+
// scripts/lib/ -> packageRoot/dist/src/cli.js (present in the npm package and
|
|
28
|
+
// dev builds, but NOT in a plugin installed from GitHub — dist is gitignored).
|
|
29
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
return join(here, "..", "..", "dist", "src", "cli.js");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Decide how to invoke the omp CLI. An explicit cliPath (tests/dev) or a
|
|
34
|
+
* bundled dist runs via `node <path>`; otherwise — the normal install, where
|
|
35
|
+
* the plugin (from GitHub) has no dist but `omp` is installed globally via npm —
|
|
36
|
+
* invoke `omp` from PATH. Exported for testing. */
|
|
37
|
+
export function resolveMemoryReviewInvocation({ sessionId, cwd, cliPath, distPath = defaultDistPath(), exists = existsSync } = {}) {
|
|
38
|
+
const args = ["memory-review", "--session", sessionId, "--root", cwd];
|
|
39
|
+
if (cliPath) return { command: process.execPath, args: [cliPath, ...args] };
|
|
40
|
+
if (exists(distPath)) return { command: process.execPath, args: [distPath, ...args] };
|
|
41
|
+
return { command: "omp", args }; // plugin-from-GitHub: rely on the global omp CLI
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function triggerMemoryReview(options = {}) {
|
|
45
|
+
const { cwd, sessionId, spawn = nodeSpawn, cliPath, mode } = options;
|
|
46
|
+
const resolvedMode = mode ?? readMemoryMode(cwd);
|
|
47
|
+
if (resolvedMode !== "on") return { triggered: false, reason: "memory-mode off" };
|
|
48
|
+
if (!sessionId || sessionId === "unknown") return { triggered: false, reason: "no session id" };
|
|
49
|
+
try {
|
|
50
|
+
const { command, args } = resolveMemoryReviewInvocation({ sessionId, cwd, cliPath });
|
|
51
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
52
|
+
// Handle async spawn errors so they never surface as unhandled (fail-open).
|
|
53
|
+
if (child && typeof child.on === "function") child.on("error", () => {});
|
|
54
|
+
if (child && typeof child.unref === "function") child.unref();
|
|
55
|
+
return { triggered: true };
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return { triggered: false, reason: String(err?.message ?? err) };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { countTokens } from "./cost-ledger.mjs";
|
|
2
|
+
|
|
3
|
+
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
4
|
+
const DIAGNOSTIC_RE = /\b(fail(?:ed|ure)?|error|exception|assertion|traceback|expected|cannot find|no-undef|TS\d{4})\b/i;
|
|
5
|
+
|
|
6
|
+
function stripAnsi(text) {
|
|
7
|
+
return text.replace(ANSI_RE, "");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function dedupConsecutiveLines(lines) {
|
|
11
|
+
const out = [];
|
|
12
|
+
let previous = "";
|
|
13
|
+
let repeated = 0;
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
if (line === previous) {
|
|
16
|
+
repeated += 1;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (repeated > 0) out.push(`[omp] repeated previous line ${repeated} time${repeated === 1 ? "" : "s"}`);
|
|
20
|
+
out.push(line);
|
|
21
|
+
previous = line;
|
|
22
|
+
repeated = 0;
|
|
23
|
+
}
|
|
24
|
+
if (repeated > 0) out.push(`[omp] repeated previous line ${repeated} time${repeated === 1 ? "" : "s"}`);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function minifyToolOutput(value, options = {}) {
|
|
29
|
+
const rawText = value == null ? "" : String(value);
|
|
30
|
+
const rawTokens = countTokens(rawText);
|
|
31
|
+
const thresholdTokens = options.thresholdTokens ?? 800;
|
|
32
|
+
if (rawTokens <= thresholdTokens) {
|
|
33
|
+
return {
|
|
34
|
+
changed: false,
|
|
35
|
+
text: rawText,
|
|
36
|
+
rawTokens,
|
|
37
|
+
modelTokens: rawTokens,
|
|
38
|
+
savedTokens: 0,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const headLines = options.headLines ?? 80;
|
|
43
|
+
const tailLines = options.tailLines ?? 40;
|
|
44
|
+
const normalized = dedupConsecutiveLines(stripAnsi(rawText).split("\n"));
|
|
45
|
+
const omitted = Math.max(0, normalized.length - headLines - tailLines);
|
|
46
|
+
const head = normalized.slice(0, headLines);
|
|
47
|
+
const tail = omitted > 0 ? normalized.slice(-tailLines) : [];
|
|
48
|
+
const tailStart = omitted > 0 ? normalized.length - tailLines : normalized.length;
|
|
49
|
+
const diagnosticLines = normalized
|
|
50
|
+
.map((line, index) => ({ line, index }))
|
|
51
|
+
.filter(({ line, index }) => index >= headLines && index < tailStart && DIAGNOSTIC_RE.test(line))
|
|
52
|
+
.slice(0, options.maxDiagnosticLines ?? 40)
|
|
53
|
+
.map(({ line }) => line);
|
|
54
|
+
const text = [
|
|
55
|
+
`[omp] output trimmed from ${rawTokens} estimated tokens; full raw output is saved on disk.`,
|
|
56
|
+
...head,
|
|
57
|
+
...(omitted > 0 ? [`[omp] … omitted ${omitted} middle line${omitted === 1 ? "" : "s"} …`] : []),
|
|
58
|
+
...(diagnosticLines.length > 0 ? ["[omp] preserved diagnostic lines from omitted output:", ...diagnosticLines] : []),
|
|
59
|
+
...tail,
|
|
60
|
+
].join("\n");
|
|
61
|
+
const modelTokens = countTokens(text);
|
|
62
|
+
|
|
63
|
+
if (modelTokens >= rawTokens) {
|
|
64
|
+
return {
|
|
65
|
+
changed: false,
|
|
66
|
+
text: rawText,
|
|
67
|
+
rawTokens,
|
|
68
|
+
modelTokens: rawTokens,
|
|
69
|
+
savedTokens: 0,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
changed: true,
|
|
75
|
+
text,
|
|
76
|
+
rawTokens,
|
|
77
|
+
modelTokens,
|
|
78
|
+
savedTokens: rawTokens - modelTokens,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { ompRoot } from "./omp-root.mjs";
|
|
4
|
+
|
|
5
|
+
// Memory-review writes proposed directives to a GATED pending queue (never
|
|
6
|
+
// auto-applied). Without a nudge that queue is invisible and rots, so the
|
|
7
|
+
// sessionStart hook surfaces a count + how to promote. Promotion stays manual:
|
|
8
|
+
// `omp project-memory add-directive "<rule>"` then remove the line.
|
|
9
|
+
|
|
10
|
+
function pendingPath(cwd) {
|
|
11
|
+
return join(ompRoot(cwd), ".oh-my-copilot", "memory-review", "pending-directives.md");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Count unchecked ("- [ ]") items in the pending-directives queue. */
|
|
15
|
+
export function countPendingDirectives(cwd) {
|
|
16
|
+
const p = pendingPath(cwd);
|
|
17
|
+
if (!existsSync(p)) return 0;
|
|
18
|
+
try {
|
|
19
|
+
return readFileSync(p, "utf8")
|
|
20
|
+
.split("\n")
|
|
21
|
+
.filter((line) => /^\s*-\s*\[\s*\]\s+\S/.test(line)).length;
|
|
22
|
+
} catch {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** SessionStart nudge string, or "" when nothing is pending. */
|
|
28
|
+
export function pendingDirectivesNudge(cwd) {
|
|
29
|
+
const n = countPendingDirectives(cwd);
|
|
30
|
+
if (n === 0) return "";
|
|
31
|
+
return (
|
|
32
|
+
`[MEMORY REVIEW] ${n} proposed directive${n === 1 ? "" : "s"} await your review in ` +
|
|
33
|
+
`.oh-my-copilot/memory-review/pending-directives.md — promote the ones you want with ` +
|
|
34
|
+
"`omp project-memory add-directive \"<rule>\"`, then delete the line."
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readStdin } from "./lib/stdin.mjs";
|
|
3
|
+
import { parseHookInput } from "./lib/hook-input.mjs";
|
|
4
|
+
import { appendHookLog, failOpen } from "./lib/hook-output.mjs";
|
|
5
|
+
|
|
6
|
+
const HOOK_NAME = "postToolUseFailure";
|
|
7
|
+
|
|
8
|
+
(async () => {
|
|
9
|
+
try {
|
|
10
|
+
const input = parseHookInput(await readStdin());
|
|
11
|
+
appendHookLog(input.cwd, HOOK_NAME, {
|
|
12
|
+
sessionId: input.sessionId,
|
|
13
|
+
toolName: input.toolName,
|
|
14
|
+
error: input.error ?? "unknown",
|
|
15
|
+
});
|
|
16
|
+
failOpen();
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.error(`[hook ${HOOK_NAME}] failed: ${err?.message ?? err}`);
|
|
19
|
+
failOpen();
|
|
20
|
+
}
|
|
21
|
+
})();
|
|
@@ -1,31 +1,94 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { readStdin } from "./lib/stdin.mjs";
|
|
5
|
+
import { buildModifiedResultOutput, failOpen } from "./lib/hook-output.mjs";
|
|
6
|
+
import { parseHookInput } from "./lib/hook-input.mjs";
|
|
7
|
+
import { appendCostRecord, countTokens } from "./lib/cost-ledger.mjs";
|
|
8
|
+
import { minifyToolOutput } from "./lib/minify.mjs";
|
|
5
9
|
|
|
6
10
|
const HOOK_NAME = "PostToolUse";
|
|
11
|
+
const NOISY_COMMAND_RE = /\b(npm|pnpm|yarn|bun|vitest|jest|mocha|pytest|cargo|go|tsc|eslint|biome|ruff|mypy|make|gradle|mvn)\b/i;
|
|
12
|
+
|
|
13
|
+
function safePathPart(value) {
|
|
14
|
+
return (
|
|
15
|
+
String(value || "unknown")
|
|
16
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
17
|
+
.replace(/^-+|-+$/g, "") || "unknown"
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function shouldMinify(input) {
|
|
22
|
+
if (process.env.OMP_MINIFY === "0") return false;
|
|
23
|
+
const toolName = String(input.toolName || "").toLowerCase();
|
|
24
|
+
const command = String(input.toolArgs?.command ?? input.toolArgs?.cmd ?? "");
|
|
25
|
+
return (toolName === "bash" || toolName === "shell" || toolName === "terminal") && NOISY_COMMAND_RE.test(command);
|
|
26
|
+
}
|
|
7
27
|
|
|
8
28
|
(async () => {
|
|
9
29
|
try {
|
|
10
30
|
const raw = await readStdin();
|
|
11
|
-
const
|
|
12
|
-
const sessionId =
|
|
13
|
-
const directory =
|
|
14
|
-
const toolName =
|
|
15
|
-
const ok =
|
|
31
|
+
const input = parseHookInput(raw);
|
|
32
|
+
const sessionId = input.sessionId;
|
|
33
|
+
const directory = input.cwd;
|
|
34
|
+
const toolName = input.toolName;
|
|
35
|
+
const ok = input.toolResult != null;
|
|
36
|
+
const rawText = input.toolResult?.textResultForLlm ?? "";
|
|
37
|
+
const minified = !shouldMinify(input) ? {
|
|
38
|
+
changed: false,
|
|
39
|
+
text: rawText,
|
|
40
|
+
rawTokens: countTokens(rawText),
|
|
41
|
+
modelTokens: countTokens(rawText),
|
|
42
|
+
savedTokens: 0,
|
|
43
|
+
} : minifyToolOutput(rawText);
|
|
44
|
+
let rawPath;
|
|
16
45
|
const logFile = join(directory, ".omp", "state", "hooks.log");
|
|
46
|
+
let canModifyResult = minified.changed;
|
|
17
47
|
try {
|
|
18
48
|
mkdirSync(dirname(logFile), { recursive: true });
|
|
19
49
|
appendFileSync(
|
|
20
50
|
logFile,
|
|
21
51
|
`${JSON.stringify({ ts: new Date().toISOString(), hook: HOOK_NAME, sessionId, toolName, ok })}\n`,
|
|
22
52
|
);
|
|
53
|
+
if (minified.changed) {
|
|
54
|
+
rawPath = join(
|
|
55
|
+
directory,
|
|
56
|
+
".omp",
|
|
57
|
+
"state",
|
|
58
|
+
"cost",
|
|
59
|
+
"raw",
|
|
60
|
+
`${safePathPart(sessionId)}-${Date.now()}-${safePathPart(toolName)}.txt`,
|
|
61
|
+
);
|
|
62
|
+
mkdirSync(dirname(rawPath), { recursive: true });
|
|
63
|
+
writeFileSync(rawPath, rawText, "utf8");
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
canModifyResult = false;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
appendCostRecord(directory, {
|
|
70
|
+
sessionId,
|
|
71
|
+
event: "postToolUse",
|
|
72
|
+
toolName,
|
|
73
|
+
inTokens: countTokens(input.toolArgs),
|
|
74
|
+
outTokens: canModifyResult ? minified.modelTokens : minified.rawTokens,
|
|
75
|
+
rawOutTokens: minified.rawTokens,
|
|
76
|
+
savedTokens: canModifyResult ? minified.savedTokens : 0,
|
|
77
|
+
rawPath,
|
|
78
|
+
});
|
|
23
79
|
} catch {
|
|
24
80
|
// best effort
|
|
25
81
|
}
|
|
26
|
-
|
|
82
|
+
if (canModifyResult) {
|
|
83
|
+
console.log(JSON.stringify(buildModifiedResultOutput(
|
|
84
|
+
minified.text,
|
|
85
|
+
`[omp] output trimmed ${minified.rawTokens}→${minified.modelTokens} tokens; full output at ${rawPath}`,
|
|
86
|
+
)));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
failOpen();
|
|
27
90
|
} catch (err) {
|
|
28
91
|
console.error(`[hook ${HOOK_NAME}] failed: ${err?.message ?? err}`);
|
|
29
|
-
|
|
92
|
+
failOpen();
|
|
30
93
|
}
|
|
31
94
|
})();
|
package/scripts/pre-tool-use.mjs
CHANGED
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { readStdin } from "./lib/stdin.mjs";
|
|
5
|
+
import { failOpen } from "./lib/hook-output.mjs";
|
|
6
|
+
import { parseHookInput } from "./lib/hook-input.mjs";
|
|
5
7
|
|
|
6
8
|
const HOOK_NAME = "PreToolUse";
|
|
7
9
|
|
|
8
10
|
(async () => {
|
|
9
11
|
try {
|
|
10
12
|
const raw = await readStdin();
|
|
11
|
-
const
|
|
12
|
-
const sessionId =
|
|
13
|
-
const directory =
|
|
14
|
-
const toolName =
|
|
13
|
+
const input = parseHookInput(raw);
|
|
14
|
+
const sessionId = input.sessionId;
|
|
15
|
+
const directory = input.cwd;
|
|
16
|
+
const toolName = input.toolName;
|
|
15
17
|
const logFile = join(directory, ".omp", "state", "hooks.log");
|
|
16
18
|
try {
|
|
17
19
|
mkdirSync(dirname(logFile), { recursive: true });
|
|
@@ -22,9 +24,9 @@ const HOOK_NAME = "PreToolUse";
|
|
|
22
24
|
} catch {
|
|
23
25
|
// best effort
|
|
24
26
|
}
|
|
25
|
-
|
|
27
|
+
failOpen();
|
|
26
28
|
} catch (err) {
|
|
27
29
|
console.error(`[hook ${HOOK_NAME}] failed: ${err?.message ?? err}`);
|
|
28
|
-
|
|
30
|
+
failOpen();
|
|
29
31
|
}
|
|
30
32
|
})();
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { readStdin } from "./lib/stdin.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { failOpen, printContinue } from "./lib/hook-output.mjs";
|
|
6
6
|
import { recordPrompt } from "./lib/daily-log.mjs";
|
|
7
7
|
import { ompRoot } from "./lib/omp-root.mjs";
|
|
8
|
+
import { parseHookInput } from "./lib/hook-input.mjs";
|
|
9
|
+
import { appendCostRecord, countTokens } from "./lib/cost-ledger.mjs";
|
|
8
10
|
|
|
9
11
|
const HOOK_NAME = "UserPromptSubmit";
|
|
10
12
|
|
|
@@ -52,11 +54,16 @@ function appendLog(directory, payload) {
|
|
|
52
54
|
(async () => {
|
|
53
55
|
try {
|
|
54
56
|
const raw = await readStdin();
|
|
55
|
-
const
|
|
56
|
-
const sessionId =
|
|
57
|
-
const directory =
|
|
58
|
-
const prompt =
|
|
57
|
+
const input = parseHookInput(raw);
|
|
58
|
+
const sessionId = input.sessionId;
|
|
59
|
+
const directory = input.cwd;
|
|
60
|
+
const prompt = input.prompt;
|
|
59
61
|
appendLog(directory, { sessionId, promptBytes: String(prompt).length });
|
|
62
|
+
appendCostRecord(directory, {
|
|
63
|
+
sessionId,
|
|
64
|
+
event: "userPromptSubmitted",
|
|
65
|
+
inTokens: countTokens(prompt),
|
|
66
|
+
});
|
|
60
67
|
// Count this prompt as session work (signals the SessionEnd nudge logic).
|
|
61
68
|
// Injects nothing — keeps per-turn token cost at zero.
|
|
62
69
|
try {
|
package/scripts/session-end.mjs
CHANGED
|
@@ -4,15 +4,18 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { readStdin } from "./lib/stdin.mjs";
|
|
5
5
|
import { endSession } from "./lib/daily-log.mjs";
|
|
6
6
|
import { ompRoot } from "./lib/omp-root.mjs";
|
|
7
|
+
import { failOpen } from "./lib/hook-output.mjs";
|
|
8
|
+
import { parseHookInput } from "./lib/hook-input.mjs";
|
|
9
|
+
import { triggerMemoryReview } from "./lib/memory-review-trigger.mjs";
|
|
7
10
|
|
|
8
11
|
const HOOK_NAME = "SessionEnd";
|
|
9
12
|
|
|
10
13
|
(async () => {
|
|
11
14
|
try {
|
|
12
15
|
const raw = await readStdin();
|
|
13
|
-
const
|
|
14
|
-
const sessionId =
|
|
15
|
-
const directory =
|
|
16
|
+
const input = parseHookInput(raw);
|
|
17
|
+
const sessionId = input.sessionId;
|
|
18
|
+
const directory = input.cwd;
|
|
16
19
|
const logFile = join(ompRoot(directory), ".omp", "state", "hooks.log");
|
|
17
20
|
try {
|
|
18
21
|
mkdirSync(dirname(logFile), { recursive: true });
|
|
@@ -26,9 +29,16 @@ const HOOK_NAME = "SessionEnd";
|
|
|
26
29
|
// Arm a daily-log nudge for the next session if this one did work but
|
|
27
30
|
// logged nothing. endSession never throws.
|
|
28
31
|
endSession(directory);
|
|
29
|
-
|
|
32
|
+
// Memory mode (opt-in): detach the cheap-model end-of-session review.
|
|
33
|
+
// Best-effort and non-blocking — never delays the hook return.
|
|
34
|
+
try {
|
|
35
|
+
triggerMemoryReview({ cwd: directory, sessionId });
|
|
36
|
+
} catch {
|
|
37
|
+
// never block session end on the review trigger
|
|
38
|
+
}
|
|
39
|
+
failOpen();
|
|
30
40
|
} catch (err) {
|
|
31
41
|
console.error(`[hook ${HOOK_NAME}] failed: ${err?.message ?? err}`);
|
|
32
|
-
|
|
42
|
+
failOpen();
|
|
33
43
|
}
|
|
34
44
|
})();
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { readStdin } from "./lib/stdin.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { failOpen, printContinue } from "./lib/hook-output.mjs";
|
|
6
6
|
import { checkForUpdate, formatUpdateNotice } from "./lib/version-check.mjs";
|
|
7
7
|
import { scanScheduleResults } from "./lib/schedule-results.mjs";
|
|
8
8
|
import { readRepoGoal, readTodayGoal, recentEntryStats, startSession } from "./lib/daily-log.mjs";
|
|
9
9
|
import { readDirectives } from "./lib/project-memory.mjs";
|
|
10
|
+
import { pendingDirectivesNudge } from "./lib/pending-directives.mjs";
|
|
10
11
|
import { ompRoot } from "./lib/omp-root.mjs";
|
|
12
|
+
import { parseHookInput } from "./lib/hook-input.mjs";
|
|
11
13
|
|
|
12
14
|
const HOOK_NAME = "SessionStart";
|
|
13
15
|
|
|
@@ -31,9 +33,9 @@ function buildDailyLogBreadcrumb(directory) {
|
|
|
31
33
|
(async () => {
|
|
32
34
|
try {
|
|
33
35
|
const raw = await readStdin();
|
|
34
|
-
const
|
|
35
|
-
const sessionId =
|
|
36
|
-
const directory =
|
|
36
|
+
const input = parseHookInput(raw);
|
|
37
|
+
const sessionId = input.sessionId;
|
|
38
|
+
const directory = input.cwd;
|
|
37
39
|
const stateDir = join(ompRoot(directory), ".omp", "state");
|
|
38
40
|
const logFile = join(stateDir, "hooks.log");
|
|
39
41
|
mkdirSync(dirname(logFile), { recursive: true });
|
|
@@ -77,6 +79,9 @@ function buildDailyLogBreadcrumb(directory) {
|
|
|
77
79
|
}
|
|
78
80
|
const repoGoal = readRepoGoal(directory);
|
|
79
81
|
if (repoGoal) parts.push(`[REPO GOAL] ${repoGoal}`);
|
|
82
|
+
// Memory-review's gated directive queue is invisible without a nudge.
|
|
83
|
+
const pendingNudge = pendingDirectivesNudge(directory);
|
|
84
|
+
if (pendingNudge) parts.push(pendingNudge);
|
|
80
85
|
const breadcrumb = buildDailyLogBreadcrumb(directory);
|
|
81
86
|
if (breadcrumb) parts.push(breadcrumb);
|
|
82
87
|
// Resets the per-session baseline and flushes a nudge when the prior session
|