From 1c2b56b275add3e9308c322baa507fdb2077e1d5 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Fri, 31 Jul 2026 21:10:13 -0400 Subject: [PATCH 1/8] refactor: delegate crontab sync to Cron.sync() in index.js Remove duplicate reflection job creation from index.js. The file now only calls Cron.sync() which reads all JSON files from the schedules directory and reconciles them with the system crontab. The reflection job is created by autoSchedule.js during first-time onboarding, which is the correct responsibility boundary. --- src/index.js | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/src/index.js b/src/index.js index dcc34f3b..3be99c26 100644 --- a/src/index.js +++ b/src/index.js @@ -50,37 +50,6 @@ if (config.schedules.syncOnInit !== false) { `[scheduler] Crontab sync complete: +${result.added} added, -${result.removed} removed, ~${result.updated} updated, =${result.skipped} skipped`, ); } - - // Ensure the daily reflection job exists in crontab and persisted (covers upgrading users - // who have no reflection-daily.json on disk). Cron.add() is idempotent. - const cwd = config.cwd; - const jobResult = Cron.add({ - name: "reflection-daily", - cron: "0 2 * * *", - command: `cd ${cwd} && node index.js --message "Run the reflection skill"`, - }); - if (jobResult.added || !jobResult.error) { - try { - const { existsSync, mkdirSync, writeFileSync } = await import("node:fs"); - const { join } = await import("node:path"); - const schedulesDir = config.memory?.schedulesDir || "memory/schedules/"; - const filePath = join(schedulesDir, "reflection-daily.json"); - if (!existsSync(filePath)) { - mkdirSync(schedulesDir, { recursive: true }); - const jobData = { - name: "reflection-daily", - cron: "0 2 * * *", - command: `cd ${cwd} && node index.js --message "Run the reflection skill"`, - enabled: true, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; - writeFileSync(filePath, JSON.stringify(jobData, null, 2)); - } - } catch (err) { - logger.warn(`[scheduler] Failed to persist reflection-daily job file: ${err.message}`); - } - } } catch (err) { logger.warn(`[scheduler] Crontab sync error: ${err.message}`); } From ad205aa0e7b09ac88420685d7f3ba0733729ab8a Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Fri, 31 Jul 2026 21:46:06 -0400 Subject: [PATCH 2/8] refactor: move index.js and config.yaml to project root Move index.js and config.yaml from src/ to the project root to align with earlier versions and Dockerfile expectations. Update all references in package.json, Dockerfile, autoSchedule.js, tests, and docs. --- AGENTS.md | 2 +- CONTRIBUTING.md | 2 +- Dockerfile | 2 ++ src/config.yaml => config.yaml | 0 src/index.js => index.js | 0 package.json | 6 +++--- src/config/loader.js | 2 +- src/scheduler/autoSchedule.js | 4 ++-- tests/unit/autoSchedule.test.js | 4 ++-- 9 files changed, 12 insertions(+), 10 deletions(-) rename src/config.yaml => config.yaml (100%) rename src/index.js => index.js (100%) diff --git a/AGENTS.md b/AGENTS.md index 51902024..223f151e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -170,7 +170,7 @@ Node.js-based AI harness application using LangGraph for state machines and Open └── schedules/ # Scheduled job output files ``` -Misc details: The `config.yaml` file is the single source of project configuration, loaded by `src/config/loader.js`. All subsystems wire into the entry point `index.js`. +Misc details: The `config.yaml` file is the single source of project configuration, loaded by `src/config/loader.js`. All subsystems wire into the entry point `index.js` at the project root. ### 2.1 Quick Commands diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a5211f1d..a4a02690 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,7 +105,7 @@ chore: pin dependencies in package.json ### Architecture Notes -- `config.yaml` + `src/config/loader.js` is the single source of truth for configuration +- `config.yaml` at the project root + `src/config/loader.js` is the single source of truth for configuration - All subsystems wire into `index.js` via `src/` modules - Each public function or class needs test coverage - Test files mirror source: `src/memory/reader.js` → `tests/unit/memory.test.js` diff --git a/Dockerfile b/Dockerfile index 59ee04bb..629fab93 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,7 +35,9 @@ WORKDIR /app COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package*.json ./ COPY LICENSE ./ +COPY index.js ./ COPY src/ ./src/ +COPY config.yaml ./ COPY prompts/ ./prompts/ COPY system-skills/ ./system-skills/ COPY docker-entrypoint.sh /docker-entrypoint.sh diff --git a/src/config.yaml b/config.yaml similarity index 100% rename from src/config.yaml rename to config.yaml diff --git a/src/index.js b/index.js similarity index 100% rename from src/index.js rename to index.js diff --git a/package.json b/package.json index 952aa05d..cc09d94f 100644 --- a/package.json +++ b/package.json @@ -33,10 +33,10 @@ ], "author": "Jason Mulligan ", "type": "module", - "main": "src/index.js", - "bin": "src/index.js", + "main": "index.js", + "bin": "index.js", "scripts": { - "start": "node src/index.js --mode interactive", + "start": "node index.js --mode interactive", "test": "node --test tests/**/*.test.js", "coverage": "node --test --experimental-test-coverage --test-coverage-exclude=dist/** --test-coverage-exclude=tests/** --test-reporter=spec tests/**/*.test.js 2>&1 | grep -A 1000 \"start of coverage report\" > coverage.txt", "fix": "oxlint --fix *.js src tests/unit && oxfmt *.js src tests/unit --write", diff --git a/src/config/loader.js b/src/config/loader.js index c43165c7..6b9edf03 100644 --- a/src/config/loader.js +++ b/src/config/loader.js @@ -9,7 +9,7 @@ const _require = createRequire(import.meta.url); import { load, dump } from "js-yaml"; const PROJECT_ROOT = dirname(fileURLToPath(import.meta.url)); -const CONFIG_PATH = join(PROJECT_ROOT, "../config.yaml"); +const CONFIG_PATH = join(PROJECT_ROOT, "../../config.yaml"); /// -- Convert camelCase or kebab-case to SNAKE_CASE --- diff --git a/src/scheduler/autoSchedule.js b/src/scheduler/autoSchedule.js index b0b8f3a9..1c24f616 100644 --- a/src/scheduler/autoSchedule.js +++ b/src/scheduler/autoSchedule.js @@ -28,7 +28,7 @@ function createJobDefinition(cwd) { return { name: "reflection-daily", cron: JOB_CRON, - command: `cd ${cwd} && timeout 300 node src/index.js --message "Run the reflection skill"`, + command: `cd ${cwd} && timeout 300 node index.js --message "Run the reflection skill"`, }; } @@ -59,7 +59,7 @@ function persistJobFile(jobName, job, cwd) { const jobData = Object.freeze({ name: job.name, cron: job.cron, - command: `cd ${cwd} && timeout 300 node src/index.js --message "Run the reflection skill"`, + command: `cd ${cwd} && timeout 300 node index.js --message "Run the reflection skill"`, enabled: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), diff --git a/tests/unit/autoSchedule.test.js b/tests/unit/autoSchedule.test.js index eb6d91e5..6abcefec 100644 --- a/tests/unit/autoSchedule.test.js +++ b/tests/unit/autoSchedule.test.js @@ -65,7 +65,7 @@ describe("setupAutoSchedule", () => { assert.strictEqual(capturedJob.cron, "0 2 * * *"); assert.ok(typeof capturedJob.command === "string"); assert.ok( - capturedJob.command.includes('node src/index.js --message "Run the reflection skill"'), + capturedJob.command.includes('node index.js --message "Run the reflection skill"'), "Command should include the --message /reflection command", ); }); @@ -99,7 +99,7 @@ describe("setupAutoSchedule", () => { assert.strictEqual(content.cron, "0 2 * * *"); assert.ok( content.command.startsWith("cd ") && - content.command.includes('node src/index.js --message "Run the reflection skill"'), + content.command.includes('node index.js --message "Run the reflection skill"'), ); assert.strictEqual(content.enabled, true); assert.ok(content.createdAt); From 34b8d1f11ce082620c3d91d69dd0e0bb16ae352d Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sat, 1 Aug 2026 08:06:57 -0400 Subject: [PATCH 3/8] refactor: remove timeout 300 from reflection job command --- src/scheduler/autoSchedule.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scheduler/autoSchedule.js b/src/scheduler/autoSchedule.js index 1c24f616..7f947dab 100644 --- a/src/scheduler/autoSchedule.js +++ b/src/scheduler/autoSchedule.js @@ -28,7 +28,7 @@ function createJobDefinition(cwd) { return { name: "reflection-daily", cron: JOB_CRON, - command: `cd ${cwd} && timeout 300 node index.js --message "Run the reflection skill"`, + command: `cd ${cwd} && node index.js --message "Run the reflection skill"`, }; } @@ -59,7 +59,7 @@ function persistJobFile(jobName, job, cwd) { const jobData = Object.freeze({ name: job.name, cron: job.cron, - command: `cd ${cwd} && timeout 300 node index.js --message "Run the reflection skill"`, + command: `cd ${cwd} && node index.js --message "Run the reflection skill"`, enabled: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), From af6ff80ce2df2bcb3a2ccc3542d758a9bca733f6 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sat, 1 Aug 2026 08:43:09 -0400 Subject: [PATCH 4/8] fix: update imports to ./src/ after moving index.js to project root --- index.js | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/index.js b/index.js index 3be99c26..4a752802 100644 --- a/index.js +++ b/index.js @@ -18,18 +18,18 @@ const parsed = yargs(process.argv.slice(2)) }).argv; // Load config -import { loadConfig } from "./config/loader.js"; +import { loadConfig } from "./src/config/loader.js"; const config = loadConfig(); import { fileURLToPath } from "node:url"; -import { loadSession } from "./session/loader.js"; +import { loadSession } from "./src/session/loader.js"; import React from "react"; -const { setConfigValue } = await import("./config/loader.js"); -const { createDeepAgentsOrchestrator } = await import("./agent/deepAgents.js"); -const { logger } = await import("./logger.js"); +const { setConfigValue } = await import("./src/config/loader.js"); +const { createDeepAgentsOrchestrator } = await import("./src/agent/deepAgents.js"); +const { logger } = await import("./src/logger.js"); -const { default: pkg } = await import(new URL("../package.json", import.meta.url).href, { +const { default: pkg } = await import(new URL("./package.json", import.meta.url).href, { with: { type: "json" }, }); @@ -37,7 +37,7 @@ const { default: pkg } = await import(new URL("../package.json", import.meta.url // Sync crontab from persisted job definitions (runs before any subsystem) if (config.schedules.syncOnInit !== false) { try { - const { Cron } = await import("./scheduler/cron.js"); + const { Cron } = await import("./src/scheduler/cron.js"); if (config.schedules.logPath) { Cron.setLogPath(config.schedules.logPath); } @@ -56,16 +56,16 @@ if (config.schedules.syncOnInit !== false) { } // Ensure sessions directory exists before any subsystem initialization -const { ensureSessionsDir } = await import("./session/index.js"); +const { ensureSessionsDir } = await import("./src/session/index.js"); await ensureSessionsDir(config.cwd + "/" + "memory/sessions/"); // Initialize contextual onboarding if profile is missing (with graceful degradation) let onboardingInstance = null; try { - const { hasProfile, ATTRIBUTES } = await import("./memory/profile.js"); + const { hasProfile, ATTRIBUTES } = await import("./src/memory/profile.js"); if (!hasProfile()) { - const { createOnboarding } = await import("./session/onboarding.js"); - const { setupAutoSchedule } = await import("./scheduler/autoSchedule.js"); + const { createOnboarding } = await import("./src/session/onboarding.js"); + const { setupAutoSchedule } = await import("./src/scheduler/autoSchedule.js"); const autoSchedule = setupAutoSchedule(); onboardingInstance = createOnboarding(ATTRIBUTES, { onSave: autoSchedule }); } @@ -77,27 +77,27 @@ try { let tracer = null; let shutdownFn = null; if (config.telemetry.enabled) { - const { initTelemetry, getTracer, shutdownTelemetry } = await import("./telemetry/provider.js"); + const { initTelemetry, getTracer, shutdownTelemetry } = await import("./src/telemetry/provider.js"); await initTelemetry(config.telemetry); tracer = getTracer(); shutdownFn = shutdownTelemetry; } // Initialize skill registry -const { SkillRegistry, resolvePermissions, ensureSkillsDir } = await import("./skills/index.js"); +const { SkillRegistry, resolvePermissions, ensureSkillsDir } = await import("./src/skills/index.js"); const registry = new SkillRegistry(); await ensureSkillsDir(config.cwd + "/" + "skills/"); registry.discover(); // Initialize memory system -const { writeMemoryFile, readMemoryFile, loadContext } = await import("./memory/index.js"); +const { writeMemoryFile, readMemoryFile, loadContext } = await import("./src/memory/index.js"); // Initialize GC manager (if enabled) let gcManager = null; let gcTrace = null; let maxGcPerHour = 4; try { - const { initGC, gc: gcFn, isAvailable } = await import("./memory/gc.js"); + const { initGC, gc: gcFn, isAvailable } = await import("./src/memory/gc.js"); const gcConfig = config.memory?.gc; if (gcConfig?.enabled !== false) { const idleTimeoutMs = gcConfig.idleTimeoutMs ?? 300000; @@ -121,11 +121,11 @@ try { // Initialize session const { createSession, SessionStateManager, saveSession, handleShutdown, registerShutdownHandler } = - await import("./session/index.js"); -const { flush: flushLogger } = await import("./logger.js"); + await import("./src/session/index.js"); +const { flush: flushLogger } = await import("./src/logger.js"); // Initialize scheduler -const { ScheduleManager } = await import("./scheduler/index.js"); +const { ScheduleManager } = await import("./src/scheduler/index.js"); const scheduleManager = new ScheduleManager(); // Create or restore session @@ -137,7 +137,7 @@ const sessionState = new SessionStateManager(initialState); // Session-init: asynchronously clean up expired ephemeral memories (non-blocking) try { - const { expireEphemeralMemories } = await import("./memory/expireEphemeral.js"); + const { expireEphemeralMemories } = await import("./src/memory/expireEphemeral.js"); queueMicrotask(() => expireEphemeralMemories(config.cwd + "/" + config.memory.contextDir).catch(() => {}), ); @@ -146,7 +146,7 @@ try { } // Create checkpointer before tools so compactContext can access it -const { createCheckpointer } = await import("./session/checkpointer.js"); +const { createCheckpointer } = await import("./src/session/checkpointer.js"); const checkpointer = createCheckpointer(config.persistence); // Provider config for TUI @@ -300,7 +300,7 @@ if (isMain) { process.exit(0); } else { const { render } = await import("ink"); - const App = (await import("./tui/app.js")).default; + const App = (await import("./src/tui/app.js")).default; const appInfo = { name: config.tui.name, version: pkg.version }; render( React.createElement(App, { @@ -325,7 +325,7 @@ if (isMain) { { // Restore terminal with newline when app exits onExit: async () => { - const shutdown = (await import("./session/index.js")).handleShutdown; + const shutdown = (await import("./src/session/index.js")).handleShutdown; if (shutdown) await shutdown(); await flushLogger(); process.stdout.write("\n"); From e0b1349db4df8ce92a6bc6aee98c9705a455441c Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sat, 1 Aug 2026 08:45:43 -0400 Subject: [PATCH 5/8] fix: format index.js with oxfmt --- index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 4a752802..e0c06ccc 100644 --- a/index.js +++ b/index.js @@ -77,14 +77,16 @@ try { let tracer = null; let shutdownFn = null; if (config.telemetry.enabled) { - const { initTelemetry, getTracer, shutdownTelemetry } = await import("./src/telemetry/provider.js"); + const { initTelemetry, getTracer, shutdownTelemetry } = + await import("./src/telemetry/provider.js"); await initTelemetry(config.telemetry); tracer = getTracer(); shutdownFn = shutdownTelemetry; } // Initialize skill registry -const { SkillRegistry, resolvePermissions, ensureSkillsDir } = await import("./src/skills/index.js"); +const { SkillRegistry, resolvePermissions, ensureSkillsDir } = + await import("./src/skills/index.js"); const registry = new SkillRegistry(); await ensureSkillsDir(config.cwd + "/" + "skills/"); registry.discover(); From e730a1eb489942e0f699590c8db2084e6e385c18 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sat, 1 Aug 2026 08:53:11 -0400 Subject: [PATCH 6/8] fix: expand lint/fix to cover all test directories (integration, tui) --- package.json | 4 ++-- tests/integration/full-flow.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index cc09d94f..c1ae5c78 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,8 @@ "start": "node index.js --mode interactive", "test": "node --test tests/**/*.test.js", "coverage": "node --test --experimental-test-coverage --test-coverage-exclude=dist/** --test-coverage-exclude=tests/** --test-reporter=spec tests/**/*.test.js 2>&1 | grep -A 1000 \"start of coverage report\" > coverage.txt", - "fix": "oxlint --fix *.js src tests/unit && oxfmt *.js src tests/unit --write", - "lint": "oxlint *.js src tests/unit && oxfmt *.js src/*.js tests/unit/*.js --check", + "fix": "oxlint --fix *.js src tests/unit tests/integration tests/tui && oxfmt *.js src tests/unit tests/integration tests/tui --write", + "lint": "oxlint *.js src tests/unit tests/integration tests/tui && oxfmt *.js src tests/unit tests/integration tests/tui --check", "docker:build": "docker build -t madz:latest .", "docker:tag": "docker tag madz:latest $DOCKER_USER/madz:$npm_package_version", "docker:push": "docker push $DOCKER_USER/madz:$npm_package_version", diff --git a/tests/integration/full-flow.test.js b/tests/integration/full-flow.test.js index 437814c1..bbfb70ae 100644 --- a/tests/integration/full-flow.test.js +++ b/tests/integration/full-flow.test.js @@ -193,7 +193,7 @@ describe("integration - scheduler execution", () => { it("enforces concurrent execution limits", async () => { const maxConcurrent = 1; let concurrent = 0; - const scheduleTasks = [ + const _scheduleTasks = [ { name: "A", cron: "0 9 * * *", skill: "x" }, { name: "B", cron: "0 9 * * *", skill: "y" }, ]; From bcc97c90432025afe8b5fbf6af20a01301add66b Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sat, 1 Aug 2026 09:09:55 -0400 Subject: [PATCH 7/8] fix: overwrite reflection job file when command has drifted --- src/scheduler/autoSchedule.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/scheduler/autoSchedule.js b/src/scheduler/autoSchedule.js index 7f947dab..2a05f7cc 100644 --- a/src/scheduler/autoSchedule.js +++ b/src/scheduler/autoSchedule.js @@ -1,4 +1,4 @@ -import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { Cron } from "./cron.js"; import { logger } from "../logger.js"; @@ -45,7 +45,14 @@ function persistJobFile(jobName, job, cwd) { const filePath = join(schedulesDir, `${jobName}.json`); if (existsSync(filePath)) { - return { written: true }; + try { + const existing = JSON.parse(readFileSync(filePath, "utf8")); + if (existing.command === job.command) { + return { written: true }; + } + } catch { + /* malformed JSON — fall through to overwrite */ + } } try { From 3387f828696a9584c4b74910b4d2acc797565ef0 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sat, 1 Aug 2026 09:20:51 -0400 Subject: [PATCH 8/8] chore: remove redundant log file creation from Dockerfile --- Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 629fab93..57b803f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,9 +43,7 @@ COPY system-skills/ ./system-skills/ COPY docker-entrypoint.sh /docker-entrypoint.sh RUN chmod +x /docker-entrypoint.sh -RUN mkdir -p /home/madz/.cache/madz/logs && \ - touch /home/madz/.cache/madz/logs/madz_cron.log && \ - chown -R madz:node /app /home/madz && \ +RUN chown -R madz:node /app /home/madz && \ chmod -R g+rwX /app /home/madz ENTRYPOINT ["/docker-entrypoint.sh"]