2026年4月7日星期二

Wake me up, when cronjob ends


Every morning at 9:03, a cron job scans my GitHub notifications, writes a summary to disk, and exits. The file sits in gh-pending.md. Nobody opens it.

The terminal window doesn't open. There's no sound. I make coffee and forget the thing exists. Then I remember, open the file, and find yesterday's summary sitting there having waited patiently for eight hours. This is not a workflow. This is a file.

This assumes you already have a Claude Code cron skill — a slash command that runs non-interactively, writes output to disk, and exits. The one used here is gh-review, which scans GitHub notifications and writes a pending-items file. If you don't have one, there's nothing here to notify you about.

I built a fix: a shell + AppleScript pipeline that fires a persistent modal alert when the cron skill finishes, and when you click "Open CC," opens iTerm2 pre-loaded with context and ready to go.


Building it required five choices that weren't obvious in advance.

display alert, not Notification Center. Banners auto-dismiss. Modal alerts require a click. giving up after 86400 ensures it won't block forever — one day is enough.

iTerm2, not Ghostty. Windows created via open -na Ghostty don't register with macOS's Accessibility layer. System Events can't find them. I tried AXUIElement traversal, highest-PID strategy, window-x/y flags — all failed with the same result: System Events got an error: Can't get window 1 of process "Ghostty". iTerm2 has a complete, stable AppleScript dictionary. Ghostty stays on the taskbar. iTerm2 gets one job.

The temp script trick. iTerm2's command parameter accepts a single binary path — compound shell commands get truncated at the first space. Fix: write the full launch command to /tmp/cc-launch.sh, pass only that path:

cat > /tmp/cc-launch.sh << EOF
#!/bin/bash
source ~/.nvm/nvm.sh
cd ~/writer
claude --resume "$context_file"
EOF

This also solves the cron nvm problem for free.

Explicit --mode=cron. Early versions inferred run mode from session history — fragile because session files can be missing, truncated, or from a half-finished interactive run, any of which tips the inference toward "interactive."

The failure mode looked like success. The skill ran, exited clean, logged nothing anomalous. It just didn't write the summary file. I checked the script, checked permissions, re-ran it manually — worked fine. Ran it via cron — worked fine. Three mornings in a row the file sat empty, and three mornings in a row I assumed I'd looked too early or the scan had found nothing. On the fourth morning I added a debug log line and watched the skill enter the interactive code path, print a prompt, and wait — at 9:03 AM, in an unattended terminal, for a keypress that was never going to come.

Just pass the flag. The skill reads it and behaves accordingly.

Context injection based on draft state. Before launching Claude, check gh-pending.md:

if [ "$pending_count" -gt 0 ]; then
  claude --system "You have $pending_count pending items. Run /gh-review."
else
  claude -p "/gh-review"
fi

The goal is a continuous transition: notification fires, you click, Claude opens already briefed — first message something like "You have 4 pending items from this morning's scan. Ready when you are." The else branch handles the no-pending case — Claude runs a fresh scan rather than opening to an empty briefing.

It works, except when the count is stale. Walk in, Claude announces twelve pending items; you cleared ten of them yesterday. It's a confident briefing about old news. The count is frozen at cron time, not now. Close enough for a morning habit.


Known limitations: macOS-only. Requires iTerm2 Accessibility permission. /tmp/cc-open-skill.txt is a shared file — two concurrent notifications would clobber each other (not a real problem: tasks run 5+ hours apart). crontab hardcodes the nvm path.

The cron skill ran this morning at 9:03. The alert appeared at 9:04. I clicked "Open CC," iTerm2 opened, Claude was already briefed.

The job ran while I wasn't paying attention, and when it needed me, it asked.


Same logic, I'm told, applies to flossing. I'll let you know how that goes.

2015年9月21日星期一

nodejs-persistable-scheduler: A nodejs based, persist by mysql, timely and repeatable task framework

In the past months, I were working in a volunteer project, which target is to create a P2P parking slot sharing platform for IBMers in Beijing. You must know that parking slots is what kind of cherish if you have ever living here.
As I am the back-end service architect of said platform, I was responsible to design/implement most part of the back-end services and transactions.
Inside the development, one of my technical requirement is a timely scheduler framework in NODEJS, which should has the following capabilities:
  • Customizable task functions;
  • Repeatable with customized frequency;
  • Persist the task data into a mysql DB;
  • Task executing status recording;
  • Fallback after restart the service;
  • Easy to use;
By going through the public modules inside NPM, I got nothing perfect candidate.
So finally I decide to build one. Now the module is finished. Following is the introduction of said module nodejs-persistable-scheduler:
A nodejs based, persist by mysql, timely and repeatable task framework;
==============================
A nodejs based, persist by mysql, timely task framework;
$ npm install nodejs-persistable-scheduler
  1. q: A library for promises (CommonJS/Promises/A,B,D);
  2. mysql: A node.js driver for mysql. It is written in JavaScript, does not require compiling, and is 100% MIT licensed.
This module is a nodejs based, persist by mysql, timely task framework;
There are 4 components that compose the whole module:
Which is repsonsible to doing the initialization works for making the functions of whole module available. In general, the initialization works are sperated into 2 parts: A system initialization task which will create 2 database tables named "fixtimetask" and "scanrecord" in the DB reference that discribed in the configurations; and a customized initialization task which will add maintaining level tasks(not user event triggered tasks, on some other words) if you need any.
This component will be executed ONLY ONCE (Well, actually, you can execute it more than once within some tricky way, referring to the (Best Prectice)[#tests:] please);
Which is responsible to doing 2 steps of fallback work evertime the current instance of nodejs-persistable-scheduler startup: 1. Find out the in-complete executing initialization and maintainess tasks in the last running from the persistance, and run them again; And 2 start the tasks maintainess monitor;
Which is a framework for registering/executing/delaying/hastening/cancelling/pausing/restarting tasks from customers;
Which is responsible to recording the initialization/maintainess task status into persistance while executing.

var scheduler = require('nodejs-persistable-scheduler');
var q = require('q');
var methodArr = [];
/***************************
 ***************************
  DONT CHANGE THE ORDER of mehtodArr!!!!
  JUST PUSH THE NEW ADDED ONES AT THE TAIL!!!
  **************************
  **************************/
method1 = function(param) {
  var defer = Q.defer();
  //functional codes for method1...
  return defer.promise;
};

methodArr.push({name:'METHOD1',method1});

method2 = function(param) {
  var defer = Q.defer();
  //functional codes for method2...
  return defer.promise;
};
methodArr.push({name:'METHOD2',method2});

initialTask = function() {
  var defer = Q.defer();
  var AM = new Date();
  AM.setHours(12,0,0);
  scheduler.registerTask("daily",AM,'METHOD1',null,null).then(function(ID) {
    if (ID)
    {
      defer.resolve(true);
    }
  },function(err) {
    defer.reject(err);
  });
  return defer.promise;
};

var schedulerConfig = {
  initFunc: initialTask, 
  scanInterval: 24*60*60*1000//scan interval time, in MS
};

var initialConfig = {
  db:
  {
    host: '',
    user: '',
    password: '',
    database:'',
    port: 
  },//DB config
  methods:methodArr,
  scheduler:schedulerConfig
};

scheduler.initialize(initialConfig);
Caution that:
  1. Please keep available of the DB naming 'fixtimetask' and 'scanrecord' for module 'nodejs-persistable-scheduler';
  2. Inside one NODEJS instance, or multiple NODEJS instances that sharing a same DB, only 1 'nodejs-persistable-scheduler' instance allowed.
  3. For different NODEJS instances that are not sharing a same DB, there could be multiple 'nodejs-persistable-scheduler' instances.
Tips:
If you want to re-initial the whole 'nodejs-persistable-scheduler' module (run the customized initialization module more than once), you can clean up the record in DB table 'scanrecord' with recordType equals to 0;
Refer the comments inside nodejs-persistable-scheduler/index.js please;

2008年11月11日星期二

Only a start...

I will record some technicial experiences in this BLOG. For my personal review, and for sharing with you, too.