@damian87/omp 0.4.1 → 0.5.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.
Files changed (56) hide show
  1. package/.github/agents/researcher.md +7 -6
  2. package/.github/copilot-instructions.md +23 -0
  3. package/.github/skills/daily-log/SKILL.md +64 -0
  4. package/.github/skills/goal/SKILL.md +33 -0
  5. package/dist/src/cli.js +190 -9
  6. package/dist/src/cli.js.map +1 -1
  7. package/dist/src/comms/index.d.ts +116 -0
  8. package/dist/src/comms/index.js +258 -0
  9. package/dist/src/comms/index.js.map +1 -0
  10. package/dist/src/comms/resolve-session.d.ts +35 -0
  11. package/dist/src/comms/resolve-session.js +53 -0
  12. package/dist/src/comms/resolve-session.js.map +1 -0
  13. package/dist/src/daily-log.d.ts +18 -0
  14. package/dist/src/daily-log.js +138 -0
  15. package/dist/src/daily-log.js.map +1 -0
  16. package/dist/src/goal.d.ts +4 -0
  17. package/dist/src/goal.js +44 -0
  18. package/dist/src/goal.js.map +1 -0
  19. package/dist/src/instructions-memory.d.ts +9 -0
  20. package/dist/src/instructions-memory.js +72 -0
  21. package/dist/src/instructions-memory.js.map +1 -0
  22. package/dist/src/mcp/tools/daily-log.d.ts +2 -0
  23. package/dist/src/mcp/tools/daily-log.js +148 -0
  24. package/dist/src/mcp/tools/daily-log.js.map +1 -0
  25. package/dist/src/omp-root.d.ts +1 -0
  26. package/dist/src/omp-root.js +19 -0
  27. package/dist/src/omp-root.js.map +1 -0
  28. package/dist/src/project-memory.d.ts +13 -0
  29. package/dist/src/project-memory.js +105 -0
  30. package/dist/src/project-memory.js.map +1 -0
  31. package/dist/src/state.d.ts +17 -0
  32. package/dist/src/state.js +101 -0
  33. package/dist/src/state.js.map +1 -0
  34. package/dist/src/trace.d.ts +19 -0
  35. package/dist/src/trace.js +74 -0
  36. package/dist/src/trace.js.map +1 -0
  37. package/dist/test/catalog.test.d.ts +1 -0
  38. package/dist/test/catalog.test.js +21 -0
  39. package/dist/test/catalog.test.js.map +1 -0
  40. package/dist/test/jira.test.d.ts +1 -0
  41. package/dist/test/jira.test.js +26 -0
  42. package/dist/test/jira.test.js.map +1 -0
  43. package/dist/test/lint.test.d.ts +1 -0
  44. package/dist/test/lint.test.js +9 -0
  45. package/dist/test/lint.test.js.map +1 -0
  46. package/dist/test/sync.test.d.ts +1 -0
  47. package/dist/test/sync.test.js +15 -0
  48. package/dist/test/sync.test.js.map +1 -0
  49. package/package.json +1 -1
  50. package/scripts/lib/daily-log.mjs +155 -0
  51. package/scripts/lib/hook-output.mjs +2 -1
  52. package/scripts/lib/omp-root.mjs +15 -0
  53. package/scripts/lib/project-memory.mjs +21 -0
  54. package/scripts/prompt-submit.mjs +14 -2
  55. package/scripts/session-end.mjs +6 -1
  56. package/scripts/session-start.mjs +52 -2
@@ -0,0 +1,74 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { ompRoot } from "./omp-root.js";
4
+ function traceDir(cwd) {
5
+ return join(ompRoot(cwd), ".omp", "state", "trace");
6
+ }
7
+ function tracePath(cwd, sessionId) {
8
+ if (!/^[\w.-]+$/.test(sessionId))
9
+ throw new Error(`invalid sessionId: ${sessionId}`);
10
+ return join(traceDir(cwd), `${sessionId}.jsonl`);
11
+ }
12
+ export function appendTraceEntry(cwd, sessionId, entry) {
13
+ const path = tracePath(cwd, sessionId);
14
+ mkdirSync(dirname(path), { recursive: true });
15
+ appendFileSync(path, `${JSON.stringify({ ts: new Date().toISOString(), sessionId, ...entry })}\n`, "utf8");
16
+ }
17
+ function readEntries(path) {
18
+ if (!existsSync(path))
19
+ return [];
20
+ return readFileSync(path, "utf8")
21
+ .split("\n")
22
+ .filter((l) => l.trim().length > 0)
23
+ .map((l) => {
24
+ try {
25
+ return JSON.parse(l);
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ })
31
+ .filter((e) => Boolean(e));
32
+ }
33
+ function pickSessionId(cwd, sessionId) {
34
+ if (sessionId)
35
+ return sessionId;
36
+ const dir = traceDir(cwd);
37
+ if (!existsSync(dir))
38
+ return undefined;
39
+ const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
40
+ if (files.length === 0)
41
+ return undefined;
42
+ // pick the most recently modified session
43
+ return files
44
+ .map((f) => ({ name: f.replace(/\.jsonl$/, ""), path: join(dir, f) }))
45
+ .sort((a, b) => {
46
+ try {
47
+ return statSync(b.path).mtimeMs - statSync(a.path).mtimeMs;
48
+ }
49
+ catch {
50
+ return 0;
51
+ }
52
+ })[0]?.name;
53
+ }
54
+ /** Last `limit` entries of a session (default 50; most recent session if omitted). */
55
+ export function traceTimeline(cwd, sessionId, limit = 50) {
56
+ const sid = pickSessionId(cwd, sessionId);
57
+ if (!sid)
58
+ return { entries: [] };
59
+ return { sessionId: sid, entries: readEntries(tracePath(cwd, sid)).slice(-Math.max(1, limit)) };
60
+ }
61
+ /** Event-name counts for a session. */
62
+ export function traceSummary(cwd, sessionId) {
63
+ const sid = pickSessionId(cwd, sessionId);
64
+ if (!sid)
65
+ return { total: 0, counts: {} };
66
+ const entries = readEntries(tracePath(cwd, sid));
67
+ const counts = {};
68
+ for (const e of entries) {
69
+ const key = e.event ?? "unknown";
70
+ counts[key] = (counts[key] ?? 0) + 1;
71
+ }
72
+ return { sessionId: sid, total: entries.length, counts };
73
+ }
74
+ //# sourceMappingURL=trace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trace.js","sourceRoot":"","sources":["../../src/trace.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { loadCapabilityCatalog, loadSkillCatalog, validateCatalog } from '../src/catalog.js';
4
+ test('catalog contains approved MVP skills and valid projections', () => {
5
+ const catalog = loadSkillCatalog();
6
+ assert.deepEqual(validateCatalog(), []);
7
+ const names = catalog.skills.map((skill) => skill.name);
8
+ assert.deepEqual(names, ['grill', 'verify', 'jira-ticket', 'code-review', 'qa']);
9
+ assert.ok(catalog.projectionCommands.includes('grill-me'));
10
+ assert.ok(catalog.projectionCommands.includes('team'));
11
+ assert.ok(catalog.projectionCommands.includes('ralph'));
12
+ });
13
+ test('team and ralph stay thin handoff capabilities for Copilot', () => {
14
+ const capabilities = loadCapabilityCatalog();
15
+ for (const name of ['team', 'ralph']) {
16
+ const capability = capabilities.capabilities.find((entry) => entry.name === name);
17
+ assert.ok(capability, `${name} capability exists`);
18
+ assert.equal(capability.support.copilot, 'thin-handoff');
19
+ }
20
+ });
21
+ //# sourceMappingURL=catalog.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.test.js","sourceRoot":"","sources":["../../test/catalog.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAE7F,IAAI,CAAC,4DAA4D,EAAE,GAAG,EAAE;IACtE,MAAM,OAAO,GAAG,gBAAgB,EAAE,CAAC;IACnC,MAAM,CAAC,SAAS,CAAC,eAAe,EAAE,EAAE,EAAE,CAAC,CAAC;IAExC,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACxD,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,IAAI,CAAC,CAAC,CAAC;IACjF,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,kBAAkB,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC;IAC3D,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,kBAAkB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IACvD,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,kBAAkB,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;AAC1D,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,2DAA2D,EAAE,GAAG,EAAE;IACrE,MAAM,YAAY,GAAG,qBAAqB,EAAE,CAAC;IAC7C,KAAK,MAAM,IAAI,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;QACrC,MAAM,UAAU,GAAG,YAAY,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;QAClF,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,IAAI,oBAAoB,CAAC,CAAC;QACnD,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { commentPayload, createIssuePayload, discoverJiraConfig, isJiraConfigured, linkFallbackPayload, safeUpdatePayload, transitionFallbackPayload } from '../src/jira.js';
4
+ test('jira config discovery supports env and configuration detection', () => {
5
+ const config = discoverJiraConfig('/tmp/no-such-root', {
6
+ JIRA_SITE_URL: 'https://example.atlassian.net',
7
+ JIRA_EMAIL: 'agent@example.com',
8
+ JIRA_API_TOKEN: 'secret-token',
9
+ JIRA_PROJECT_KEY: 'OMC'
10
+ });
11
+ assert.equal(config.siteUrl, 'https://example.atlassian.net');
12
+ assert.equal(config.projectKey, 'OMC');
13
+ assert.equal(isJiraConfigured(config), true);
14
+ });
15
+ test('jira adapter renders create, comment, update, and fallback payloads', () => {
16
+ const config = discoverJiraConfig('/tmp/no-such-root', { JIRA_PROJECT_KEY: 'OMC' });
17
+ const create = createIssuePayload(config, { summary: 'Implement slice', description: 'Body' });
18
+ assert.equal(create.operation, 'create');
19
+ assert.equal(create.configured, false);
20
+ assert.match(JSON.stringify(create.body), /Implement slice/);
21
+ assert.equal(commentPayload(config, 'OMC-1', 'Evidence').operation, 'comment');
22
+ assert.equal(safeUpdatePayload(config, 'OMC-1', { summary: 'New' }).method, 'PUT');
23
+ assert.equal(transitionFallbackPayload(config, 'OMC-1', 'Done').configured, false);
24
+ assert.equal(linkFallbackPayload(config, 'OMC-1', 'OMC-2').operation, 'link-fallback');
25
+ });
26
+ //# sourceMappingURL=jira.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jira.test.js","sourceRoot":"","sources":["../../test/jira.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAE7K,IAAI,CAAC,gEAAgE,EAAE,GAAG,EAAE;IAC1E,MAAM,MAAM,GAAG,kBAAkB,CAAC,mBAAmB,EAAE;QACrD,aAAa,EAAE,+BAA+B;QAC9C,UAAU,EAAE,mBAAmB;QAC/B,cAAc,EAAE,cAAc;QAC9B,gBAAgB,EAAE,KAAK;KACxB,CAAC,CAAC;IAEH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,+BAA+B,CAAC,CAAC;IAC9D,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IACvC,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC;AAC/C,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,qEAAqE,EAAE,GAAG,EAAE;IAC/E,MAAM,MAAM,GAAG,kBAAkB,CAAC,mBAAmB,EAAE,EAAE,gBAAgB,EAAE,KAAK,EAAE,CAAC,CAAC;IAEpF,MAAM,MAAM,GAAG,kBAAkB,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,CAAC;IAC/F,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACzC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IACvC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,iBAAiB,CAAC,CAAC;IAE7D,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAC/E,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACnF,MAAM,CAAC,KAAK,CAAC,yBAAyB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IACnF,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;AACzF,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { lintSkills } from '../src/lint.js';
4
+ import { workspaceRoot } from '../src/project.js';
5
+ test('workspace skills satisfy Phase 1 catalog lint', () => {
6
+ const issues = lintSkills(workspaceRoot('..'));
7
+ assert.deepEqual(issues.filter((issue) => issue.level === 'error'), []);
8
+ });
9
+ //# sourceMappingURL=lint.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lint.test.js","sourceRoot":"","sources":["../../test/lint.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD,IAAI,CAAC,+CAA+C,EAAE,GAAG,EAAE;IACzD,MAAM,MAAM,GAAG,UAAU,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/C,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;AAC1E,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { projectCopilotCommands } from '../src/sync.js';
4
+ test('dry-run projects skill aliases and thin runtime handoffs', () => {
5
+ const files = projectCopilotCommands();
6
+ const paths = files.map((file) => file.path);
7
+ assert.ok(paths.includes('.github/copilot/commands/grill.md'));
8
+ assert.ok(paths.includes('.github/copilot/commands/grill-me.md'));
9
+ assert.ok(paths.includes('.github/copilot/commands/team.md'));
10
+ assert.ok(paths.includes('.github/copilot/commands/ralph.md'));
11
+ const team = files.find((file) => file.path.endsWith('/team.md'));
12
+ assert.ok(team?.content.includes('thin capability handoff'));
13
+ assert.ok(team?.content.includes('not a Copilot-native durable runtime'));
14
+ });
15
+ //# sourceMappingURL=sync.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.test.js","sourceRoot":"","sources":["../../test/sync.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAExD,IAAI,CAAC,0DAA0D,EAAE,GAAG,EAAE;IACpE,MAAM,KAAK,GAAG,sBAAsB,EAAE,CAAC;IACvC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAE7C,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,mCAAmC,CAAC,CAAC,CAAC;IAC/D,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,sCAAsC,CAAC,CAAC,CAAC;IAClE,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,kCAAkC,CAAC,CAAC,CAAC;IAC9D,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,mCAAmC,CAAC,CAAC,CAAC;IAE/D,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC;IAClE,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAC,CAAC,CAAC;IAC7D,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,sCAAsC,CAAC,CAAC,CAAC;AAC5E,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damian87/omp",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "GitHub Copilot project skills catalog and Jira handoff tools.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -0,0 +1,155 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { ompRoot } from "./omp-root.mjs";
4
+
5
+ const DAY_FILE_RE = /^\d{4}-\d{2}-\d{2}\.md$/;
6
+ const DEFAULT_NUDGE =
7
+ 'Your last session made progress but recorded nothing in the daily log — run `omp daily-log add "<text>"` to capture what changed and any key decisions, so this session has that context.';
8
+
9
+ // A session with at least this many user prompts counts as "did real work".
10
+ const WORK_THRESHOLD = 3;
11
+
12
+ function pad(n) {
13
+ return String(n).padStart(2, "0");
14
+ }
15
+
16
+ export function todayStr(d = new Date()) {
17
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
18
+ }
19
+
20
+ function dailyDir(directory) {
21
+ return join(ompRoot(directory), ".omp", "memory", "daily");
22
+ }
23
+
24
+ function dayFile(directory, date = todayStr()) {
25
+ return join(dailyDir(directory), `${date}.md`);
26
+ }
27
+
28
+ /** Today's Goal section text, or null when unset/empty. */
29
+ export function readTodayGoal(directory) {
30
+ try {
31
+ const p = dayFile(directory);
32
+ if (!existsSync(p)) return null;
33
+ const text = readFileSync(p, "utf8");
34
+ const m = text.match(/##\s+Goal\s*\n([\s\S]*?)(?=\n##\s|\n#\s|$)/i);
35
+ const goal = m ? m[1].trim() : "";
36
+ return goal || null;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /** The repo's durable objective from .omp/goal.md, or null when unset. */
43
+ export function readRepoGoal(directory) {
44
+ try {
45
+ const p = join(ompRoot(directory), ".omp", "goal.md");
46
+ if (!existsSync(p)) return null;
47
+ const text = readFileSync(p, "utf8");
48
+ const lines = (text.charCodeAt(0) === 0xfeff ? text.slice(1) : text).split("\n");
49
+ // Strip only our own `# Repo Goal` header so a hand-authored goal isn't lost.
50
+ if (/^#\s+Repo Goal\s*$/i.test(lines[0] ?? "")) lines.shift();
51
+ const goal = lines.join("\n").trim();
52
+ return goal || null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /** Count day-files + total log bullets within the last `days` days (inclusive). */
59
+ export function recentEntryStats(directory, days = 7) {
60
+ try {
61
+ const dir = dailyDir(directory);
62
+ if (!existsSync(dir)) return { files: 0, entries: 0 };
63
+ const cutoff = todayStr(new Date(Date.now() - days * 86400000));
64
+ const files = readdirSync(dir).filter((f) => DAY_FILE_RE.test(f) && f.slice(0, 10) >= cutoff);
65
+ let entries = 0;
66
+ for (const f of files) {
67
+ try {
68
+ entries += (readFileSync(join(dir, f), "utf8").match(/^\s*-\s+/gm) || []).length;
69
+ } catch {
70
+ // skip unreadable day file
71
+ }
72
+ }
73
+ return { files: files.length, entries };
74
+ } catch {
75
+ return { files: 0, entries: 0 };
76
+ }
77
+ }
78
+
79
+ function statePath(directory) {
80
+ return join(ompRoot(directory), ".omp", "state", "daily-log.json");
81
+ }
82
+
83
+ function readState(directory) {
84
+ const fresh = { date: todayStr(), prompts: 0, entriesAtStart: 0, pendingNudge: false, pendingReason: "" };
85
+ try {
86
+ const p = statePath(directory);
87
+ if (!existsSync(p)) return fresh;
88
+ const parsed = JSON.parse(readFileSync(p, "utf8"));
89
+ // Ignore valid-but-wrong JSON (null, number, array) so callers can't throw.
90
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return { ...fresh, ...parsed };
91
+ } catch {
92
+ // start fresh on read/parse failure
93
+ }
94
+ return fresh;
95
+ }
96
+
97
+ function writeState(directory, state) {
98
+ try {
99
+ const p = statePath(directory);
100
+ mkdirSync(dirname(p), { recursive: true });
101
+ const tmp = `${p}.tmp.${process.pid}.${Date.now()}`;
102
+ writeFileSync(tmp, JSON.stringify(state, null, 2), "utf8");
103
+ renameSync(tmp, p);
104
+ } catch {
105
+ // best effort
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Called at SessionStart ("a new session continuing from an existing one").
111
+ * Returns a one-line flush nudge when the PRIOR session did work but logged
112
+ * nothing (else ""), then resets the per-session baseline. Never throws.
113
+ */
114
+ export function startSession(directory) {
115
+ const prior = readState(directory);
116
+ const flush = prior.pendingNudge ? prior.pendingReason || DEFAULT_NUDGE : "";
117
+ writeState(directory, {
118
+ date: todayStr(),
119
+ prompts: 0,
120
+ entriesAtStart: recentEntryStats(directory, 0).entries,
121
+ pendingNudge: false,
122
+ pendingReason: "",
123
+ });
124
+ return flush;
125
+ }
126
+
127
+ /**
128
+ * Called at UserPromptSubmit. Increments the per-session work counter.
129
+ * Deliberately does NOT reset on day rollover — startSession owns the baseline,
130
+ * so a session that spans midnight still counts all its prompts as one session.
131
+ */
132
+ export function recordPrompt(directory) {
133
+ const state = readState(directory);
134
+ state.prompts = (state.prompts || 0) + 1;
135
+ writeState(directory, state);
136
+ }
137
+
138
+ /**
139
+ * Called at SessionEnd. Arms a nudge for the NEXT SessionStart when this session
140
+ * did real work (>= WORK_THRESHOLD prompts) but added no daily-log entries.
141
+ * Never throws.
142
+ */
143
+ export function endSession(directory) {
144
+ const state = readState(directory);
145
+ const sameDay = state.date === todayStr();
146
+ const added = recentEntryStats(directory, 0).entries - (state.entriesAtStart || 0);
147
+ const didWork = (state.prompts || 0) >= WORK_THRESHOLD;
148
+ // Only arm when the session started and ended on the same calendar day. Across
149
+ // a midnight boundary the entriesAtStart baseline refers to a different
150
+ // day-file, so the delta is unreliable — stay quiet rather than risk a
151
+ // spurious nudge.
152
+ state.pendingNudge = sameDay && didWork && added <= 0;
153
+ state.pendingReason = state.pendingNudge ? DEFAULT_NUDGE : "";
154
+ writeState(directory, state);
155
+ }
@@ -1,5 +1,6 @@
1
1
  import { appendFileSync, mkdirSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
+ import { ompRoot } from "./omp-root.mjs";
3
4
 
4
5
  export function printContinue(hookEventName, additionalContext = "") {
5
6
  const output = additionalContext
@@ -17,7 +18,7 @@ export function failOpen() {
17
18
  }
18
19
 
19
20
  export function appendHookLog(directory, hookName, payload) {
20
- const logFile = join(directory, ".omp", "state", "hooks.log");
21
+ const logFile = join(ompRoot(directory), ".omp", "state", "hooks.log");
21
22
  try {
22
23
  mkdirSync(dirname(logFile), { recursive: true });
23
24
  appendFileSync(
@@ -0,0 +1,15 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+
4
+ // Plain-Node mirror of src/omp-root.ts for the hooks: walk up from `start` to the
5
+ // nearest project marker (.git, then package.json) so memory is scoped to the
6
+ // real project, not the literal cwd. Falls back to `start` when none is found.
7
+ export function ompRoot(start) {
8
+ let dir = resolve(start);
9
+ while (true) {
10
+ if (existsSync(join(dir, ".git")) || existsSync(join(dir, "package.json"))) return dir;
11
+ const parent = dirname(dir);
12
+ if (parent === dir) return resolve(start);
13
+ dir = parent;
14
+ }
15
+ }
@@ -0,0 +1,21 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { ompRoot } from "./omp-root.mjs";
4
+
5
+ /**
6
+ * Must-follow directives from .omp/project-memory.json. These are injected at
7
+ * SessionStart unconditionally — rules are never relevance-gated, so the agent
8
+ * cannot skip them. (Notes stay on-demand; only directives are pushed.)
9
+ * Best-effort, never throws.
10
+ */
11
+ export function readDirectives(directory) {
12
+ try {
13
+ const p = join(ompRoot(directory), ".omp", "project-memory.json");
14
+ if (!existsSync(p)) return [];
15
+ const data = JSON.parse(readFileSync(p, "utf8"));
16
+ const list = Array.isArray(data?.directives) ? data.directives : [];
17
+ return list.filter((d) => typeof d === "string" && d.trim() !== "").map((d) => d.trim());
18
+ } catch {
19
+ return [];
20
+ }
21
+ }
@@ -2,6 +2,8 @@
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 { recordPrompt } from "./lib/daily-log.mjs";
6
+ import { ompRoot } from "./lib/omp-root.mjs";
5
7
 
6
8
  const HOOK_NAME = "UserPromptSubmit";
7
9
 
@@ -34,7 +36,7 @@ function buildContinuationContext(directory) {
34
36
  }
35
37
 
36
38
  function appendLog(directory, payload) {
37
- const logFile = join(directory, ".omp", "state", "hooks.log");
39
+ const logFile = join(ompRoot(directory), ".omp", "state", "hooks.log");
38
40
  try {
39
41
  mkdirSync(dirname(logFile), { recursive: true });
40
42
  appendFileSync(
@@ -54,7 +56,17 @@ function appendLog(directory, payload) {
54
56
  const directory = data.directory ?? process.cwd();
55
57
  const prompt = data.prompt ?? data.message?.content ?? "";
56
58
  appendLog(directory, { sessionId, promptBytes: String(prompt).length });
57
- const additionalContext = buildContinuationContext(directory);
59
+ // Count this prompt as session work (signals the SessionEnd nudge logic).
60
+ // Injects nothing — keeps per-turn token cost at zero.
61
+ try {
62
+ recordPrompt(directory);
63
+ } catch {
64
+ // best effort: counting must never block the prompt
65
+ }
66
+ const parts = [];
67
+ const cont = buildContinuationContext(directory);
68
+ if (cont) parts.push(cont);
69
+ const additionalContext = parts.join("\n\n---\n\n");
58
70
  const output = additionalContext
59
71
  ? { continue: true, hookSpecificOutput: { hookEventName: HOOK_NAME, additionalContext } }
60
72
  : { continue: true };
@@ -2,6 +2,8 @@
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 { endSession } from "./lib/daily-log.mjs";
6
+ import { ompRoot } from "./lib/omp-root.mjs";
5
7
 
6
8
  const HOOK_NAME = "SessionEnd";
7
9
 
@@ -11,7 +13,7 @@ const HOOK_NAME = "SessionEnd";
11
13
  const data = raw ? JSON.parse(raw) : {};
12
14
  const sessionId = data.sessionId ?? data.session_id ?? "unknown";
13
15
  const directory = data.directory ?? process.cwd();
14
- const logFile = join(directory, ".omp", "state", "hooks.log");
16
+ const logFile = join(ompRoot(directory), ".omp", "state", "hooks.log");
15
17
  try {
16
18
  mkdirSync(dirname(logFile), { recursive: true });
17
19
  appendFileSync(
@@ -21,6 +23,9 @@ const HOOK_NAME = "SessionEnd";
21
23
  } catch {
22
24
  // best effort
23
25
  }
26
+ // Arm a daily-log nudge for the next session if this one did work but
27
+ // logged nothing. endSession never throws.
28
+ endSession(directory);
24
29
  console.log(JSON.stringify({ continue: true }));
25
30
  } catch (err) {
26
31
  console.error(`[hook ${HOOK_NAME}] failed: ${err?.message ?? err}`);
@@ -3,16 +3,36 @@ import { appendFileSync, mkdirSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { readStdin } from "./lib/stdin.mjs";
5
5
  import { checkForUpdate, formatUpdateNotice } from "./lib/version-check.mjs";
6
+ import { readRepoGoal, readTodayGoal, recentEntryStats, startSession } from "./lib/daily-log.mjs";
7
+ import { readDirectives } from "./lib/project-memory.mjs";
8
+ import { ompRoot } from "./lib/omp-root.mjs";
6
9
 
7
10
  const HOOK_NAME = "SessionStart";
8
11
 
12
+ function buildDailyLogBreadcrumb(directory) {
13
+ try {
14
+ const goal = readTodayGoal(directory);
15
+ const { entries } = recentEntryStats(directory, 7);
16
+ if (!goal && entries === 0) return "";
17
+ const lines = ["[DAILY LOG]"];
18
+ if (goal) lines.push(`Goal: ${goal}`);
19
+ if (entries > 0)
20
+ lines.push(
21
+ `${entries} ${entries === 1 ? "entry" : "entries"} logged in the last 7 days — run \`omp daily-log read\` to load if relevant.`,
22
+ );
23
+ return lines.join("\n");
24
+ } catch {
25
+ return "";
26
+ }
27
+ }
28
+
9
29
  (async () => {
10
30
  try {
11
31
  const raw = await readStdin();
12
32
  const data = raw ? JSON.parse(raw) : {};
13
33
  const sessionId = data.sessionId ?? data.session_id ?? "unknown";
14
34
  const directory = data.directory ?? process.cwd();
15
- const stateDir = join(directory, ".omp", "state");
35
+ const stateDir = join(ompRoot(directory), ".omp", "state");
16
36
  const logFile = join(stateDir, "hooks.log");
17
37
  mkdirSync(dirname(logFile), { recursive: true });
18
38
  const line = JSON.stringify({
@@ -23,8 +43,38 @@ const HOOK_NAME = "SessionStart";
23
43
  });
24
44
  appendFileSync(logFile, `${line}\n`);
25
45
 
46
+ const parts = [];
26
47
  const update = await checkForUpdate({ stateDir });
27
- const additionalContext = update ? formatUpdateNotice(update.current, update.latest) : "";
48
+ if (update) parts.push(formatUpdateNotice(update.current, update.latest));
49
+ // Directives are must-follow rules — injected unconditionally (never on-demand)
50
+ // so the agent can't skip a rule by judging it "unrelated". Capped by count +
51
+ // chars so a bloated directive list can't balloon the start message; overflow
52
+ // is summarized with a pointer (mirrors OpenClaw's injection budget).
53
+ const directives = readDirectives(directory);
54
+ if (directives.length > 0) {
55
+ const MAX_DIRECTIVES = 12;
56
+ const MAX_DIRECTIVE_CHARS = 1200;
57
+ const shown = [];
58
+ let chars = 0;
59
+ for (const d of directives) {
60
+ if (shown.length >= MAX_DIRECTIVES || chars + d.length > MAX_DIRECTIVE_CHARS) break;
61
+ shown.push(d);
62
+ chars += d.length;
63
+ }
64
+ const more = directives.length - shown.length;
65
+ const body = shown.map((d) => `- ${d}`).join("\n");
66
+ const tail = more > 0 ? `\n- (+${more} more — run \`omp project-memory read\` to see all)` : "";
67
+ parts.push(`[DIRECTIVES] Follow these this session:\n${body}${tail}`);
68
+ }
69
+ const repoGoal = readRepoGoal(directory);
70
+ if (repoGoal) parts.push(`[REPO GOAL] ${repoGoal}`);
71
+ const breadcrumb = buildDailyLogBreadcrumb(directory);
72
+ if (breadcrumb) parts.push(breadcrumb);
73
+ // Resets the per-session baseline and flushes a nudge when the prior session
74
+ // did work but logged nothing. startSession never throws.
75
+ const flush = startSession(directory);
76
+ if (flush) parts.push(`[DAILY LOG] ${flush}`);
77
+ const additionalContext = parts.join("\n\n---\n\n");
28
78
 
29
79
  console.log(
30
80
  JSON.stringify({