meerkat-taskpanel 0.1.0 → 0.1.1

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.
@@ -3,7 +3,7 @@
3
3
  "owner": { "name": "tanyu216" },
4
4
  "metadata": {
5
5
  "description": "Task management board for AI Agent teams.",
6
- "version": "0.1.0"
6
+ "version": "0.1.1"
7
7
  },
8
8
  "plugins": [
9
9
  {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meerkat-taskpanel",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Task management board for AI Agent teams.",
5
5
  "author": { "name": "tanyu216" },
6
6
  "license": "MIT",
package/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.1] - 2026-10-27
11
+
12
+ ### Fixed
13
+
14
+ - The `taskd` keepalive guard now writes its launchd/systemd units under the install root
15
+ (`<home>/.meerkat-taskpanel/scheduling/`) instead of a stray `<home>/taskpanel`
16
+ directory beside it.
17
+ - The guard is no longer loaded when a board is already answering `/health` at the runtime
18
+ pointer's address. A second `taskd` on that port only restarted into `EADDRINUSE`; the
19
+ install now leaves the running board alone and reports what it found.
20
+
10
21
  ## [0.1.0] - 2026-10-27
11
22
 
12
23
  First public release.
@@ -117,5 +128,6 @@ First public release.
117
128
  - `issue get` shows the card title.
118
129
  - The subscriber unit points at the event hub instead of a direct stream.
119
130
 
120
- [Unreleased]: https://github.com/tanyu216/meerkat-taskpanel/compare/v0.1.0...HEAD
131
+ [0.1.1]: https://github.com/tanyu216/meerkat-taskpanel/releases/tag/v0.1.1
132
+ [Unreleased]: https://github.com/tanyu216/meerkat-taskpanel/compare/v0.1.1...HEAD
121
133
  [0.1.0]: https://github.com/tanyu216/meerkat-taskpanel/releases/tag/v0.1.0
package/docs/install.md CHANGED
@@ -344,12 +344,12 @@ a daemon for you (the [Scheduling units](#scheduling-units-claude-codex-pi) abov
344
344
  applies **only to the `taskd` service guard**: the `meerkat-loop` supervisor's units are
345
345
  still written and left for you to load, and OpenClaw still gets none.
346
346
 
347
- **Where it lands** — a neutral directory, deliberately *not* the loop units'
347
+ **Where it lands** — under the install root, deliberately *not* the loop units'
348
348
  `<agent-dir>/scheduling/`, so re-installing an agent kind's loop units never touches it:
349
349
 
350
350
  ```text
351
- <prefix>/taskpanel/scheduling/launchd/com.meerkat-taskpanel.taskd.plist
352
- <prefix>/taskpanel/scheduling/systemd/meerkat-taskpanel-taskd.service
351
+ <prefix>/.meerkat-taskpanel/scheduling/launchd/com.meerkat-taskpanel.taskd.plist
352
+ <prefix>/.meerkat-taskpanel/scheduling/systemd/meerkat-taskpanel-taskd.service
353
353
  ```
354
354
 
355
355
  `<prefix>` is `--prefix` (default `$HOME`). There is **one guard per machine** — and
@@ -362,10 +362,10 @@ separate flag:
362
362
 
363
363
  ```bash
364
364
  # macOS
365
- launchctl bootstrap gui/$UID <prefix>/taskpanel/scheduling/launchd/com.meerkat-taskpanel.taskd.plist
365
+ launchctl bootstrap gui/$UID <prefix>/.meerkat-taskpanel/scheduling/launchd/com.meerkat-taskpanel.taskd.plist
366
366
  # Linux (user unit)
367
367
  mkdir -p ~/.config/systemd/user
368
- cp <prefix>/taskpanel/scheduling/systemd/meerkat-taskpanel-taskd.service ~/.config/systemd/user/
368
+ cp <prefix>/.meerkat-taskpanel/scheduling/systemd/meerkat-taskpanel-taskd.service ~/.config/systemd/user/
369
369
  systemctl --user enable --now meerkat-taskpanel-taskd.service
370
370
  ```
371
371
 
@@ -383,6 +383,15 @@ and names the log to read:
383
383
  taskd guard: loaded (com.meerkat-taskpanel.taskd, pid …); /health did not answer within 10000ms — read <dataDir>/logs/taskd.log
384
384
  ```
385
385
 
386
+ A board that is **already serving** — the runtime pointer's address answers `/health`
387
+ before the guard is loaded — is left alone. Loading a second `taskd` on that port is not a
388
+ keepalive; it is an `EADDRINUSE` restart loop. The units are still written, nothing is
389
+ loaded, and the install says:
390
+
391
+ ```text
392
+ taskd guard: not loaded — a board is already serving http://127.0.0.1:9527 (pid …); stop it and re-run to install the guard
393
+ ```
394
+
386
395
  Neither line fails the install. The unit names the **absolute** node that ran the install,
387
396
  so a node that lives only under nvm or Homebrew still starts the server — see
388
397
  [`scheduling.md § The taskd guard (keepalive)`](scheduling.md#the-taskd-guard-keepalive).
@@ -428,7 +437,7 @@ plain `kill` is not enough — use the supervisor's own stop:
428
437
  ```bash
429
438
  # macOS
430
439
  launchctl bootout gui/$UID/com.meerkat-taskpanel.taskd
431
- rm <prefix>/taskpanel/scheduling/launchd/com.meerkat-taskpanel.taskd.plist
440
+ rm <prefix>/.meerkat-taskpanel/scheduling/launchd/com.meerkat-taskpanel.taskd.plist
432
441
  # Linux
433
442
  systemctl --user disable --now meerkat-taskpanel-taskd.service
434
443
  rm ~/.config/systemd/user/meerkat-taskpanel-taskd.service
@@ -844,12 +844,12 @@ write lock (R1: one `taskd` per data directory).
844
844
  #### Where the unit lands (ownership separation)
845
845
 
846
846
  ```text
847
- <prefix>/taskpanel/scheduling/launchd/com.meerkat-taskpanel.taskd.plist
848
- <prefix>/taskpanel/scheduling/systemd/meerkat-taskpanel-taskd.service
847
+ <prefix>/.meerkat-taskpanel/scheduling/launchd/com.meerkat-taskpanel.taskd.plist
848
+ <prefix>/.meerkat-taskpanel/scheduling/systemd/meerkat-taskpanel-taskd.service
849
849
  ```
850
850
 
851
- A **neutral** directory (`<prefix>` is `install.sh --prefix`, default `$HOME`),
852
- deliberately **not** `<agent-dir>/scheduling/` where the loop's `ping`/`continue` units
851
+ The install root (`<prefix>/.meerkat-taskpanel/`; `<prefix>` is `install.sh --prefix`,
852
+ default `$HOME`), deliberately **not** `<agent-dir>/scheduling/` where the loop's `ping`/`continue` units
853
853
  live ([Scheduling units](install.md#scheduling-units-claude-codex-pi)). Re-installing a
854
854
  agent kind's loop units never touches this directory, and the two are never confused for one
855
855
  another.
@@ -980,7 +980,7 @@ nothing.
980
980
  # ~/.config/systemd/user/ and enables it immediately.
981
981
  #
982
982
  # mkdir -p ~/.config/systemd/user
983
- # cp <prefix>/taskpanel/scheduling/systemd/meerkat-taskpanel-taskd.service ~/.config/systemd/user/
983
+ # cp <prefix>/.meerkat-taskpanel/scheduling/systemd/meerkat-taskpanel-taskd.service ~/.config/systemd/user/
984
984
  # systemctl --user enable --now meerkat-taskpanel-taskd.service
985
985
  #
986
986
  # ExecStart names the ABSOLUTE node that rendered this unit — never
@@ -1039,11 +1039,11 @@ user switch**. `install.sh` writes the unit and then loads/enables it:
1039
1039
 
1040
1040
  ```bash
1041
1041
  # macOS
1042
- launchctl bootstrap gui/$UID <prefix>/taskpanel/scheduling/launchd/com.meerkat-taskpanel.taskd.plist
1042
+ launchctl bootstrap gui/$UID <prefix>/.meerkat-taskpanel/scheduling/launchd/com.meerkat-taskpanel.taskd.plist
1043
1043
  # (older systems: launchctl load -w <plist>)
1044
1044
  # Linux (user unit)
1045
1045
  mkdir -p ~/.config/systemd/user
1046
- cp <prefix>/taskpanel/scheduling/systemd/meerkat-taskpanel-taskd.service ~/.config/systemd/user/
1046
+ cp <prefix>/.meerkat-taskpanel/scheduling/systemd/meerkat-taskpanel-taskd.service ~/.config/systemd/user/
1047
1047
  systemctl --user enable --now meerkat-taskpanel-taskd.service
1048
1048
  ```
1049
1049
 
@@ -1078,6 +1078,15 @@ taskd guard: loaded (com.meerkat-taskpanel.taskd, pid 17388); /health did not an
1078
1078
  Neither line fails the install (the agent-kind skills are installed either way), but only the
1079
1079
  first one claims the guard works.
1080
1080
 
1081
+ A board that is **already answering** at the runtime pointer's address is a third case, and
1082
+ it is not loaded over: a second `taskd` on the same port is an `EADDRINUSE` restart loop, not
1083
+ a keepalive. The units are written, the load is skipped, and the report names the running
1084
+ board:
1085
+
1086
+ ```text
1087
+ taskd guard: not loaded — a board is already serving http://127.0.0.1:9527 (pid …); stop it and re-run to install the guard
1088
+ ```
1089
+
1081
1090
  A **second board** is where the install has to be careful, because the guard is one per
1082
1091
  machine ([One guard per machine](#one-guard-per-machine)): on Linux the file systemd reads
1083
1092
  `~/.config/systemd/user/meerkat-taskpanel-taskd.service`, and that file *is* the machine's
@@ -1112,7 +1121,7 @@ To stop and remove the guard:
1112
1121
  ```bash
1113
1122
  # macOS
1114
1123
  launchctl bootout gui/$UID/com.meerkat-taskpanel.taskd
1115
- rm <prefix>/taskpanel/scheduling/launchd/com.meerkat-taskpanel.taskd.plist
1124
+ rm <prefix>/.meerkat-taskpanel/scheduling/launchd/com.meerkat-taskpanel.taskd.plist
1116
1125
  # Linux
1117
1126
  systemctl --user disable --now meerkat-taskpanel-taskd.service
1118
1127
  rm ~/.config/systemd/user/meerkat-taskpanel-taskd.service
package/install.sh CHANGED
@@ -206,7 +206,7 @@ Environment (set for each agent-kind installer):
206
206
  (internal — for tests, not a user option)
207
207
 
208
208
  The taskd keepalive guard is written and loaded on every install (one unit per
209
- machine, at <prefix>/taskpanel/scheduling/) — see docs/scheduling.md. It has no
209
+ machine, at <prefix>/.meerkat-taskpanel/scheduling/) — see docs/scheduling.md. It has no
210
210
  user flag and no prompt; the bypasses are --no-automation, --dry-run, and the
211
211
  test-only MEERKAT_TASKPANEL_SKIP_TASKD_SERVICE above.
212
212
  EOF
@@ -219,8 +219,8 @@ die() {
219
219
 
220
220
  # install_taskd_guard — write the `taskd` keepalive unit and load it (docs/scheduling.md).
221
221
  #
222
- # One unit per **machine**, not per agent kind: it is written once here, to the neutral
223
- # `<prefix>/taskpanel/scheduling/` directory, and never touched by an agent kind's own
222
+ # One unit per **machine**, not per agent kind: it is written once here, under the install
223
+ # root `<prefix>/.meerkat-taskpanel/scheduling/`, and never touched by an agent kind's own
224
224
  # scheduling install. It is also the one unit this installer *loads* rather than
225
225
  # only printing a command for — a deliberate, narrow exception to "write the unit,
226
226
  # never start a daemon for you". The loop supervisor's units keep that rule.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meerkat-taskpanel",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  "web"
@@ -3,8 +3,8 @@
3
3
  * The `taskd` OS-level keepalive unit — "the guard" (`docs/scheduling.md`).
4
4
  *
5
5
  * `install.sh` calls this once per install, because the guard is **one per
6
- * machine**, not one per agent kind: it writes both unit templates into a neutral
7
- * `<prefix>/taskpanel/scheduling/` directory (deliberately *not* the loop units'
6
+ * machine**, not one per agent kind: it writes both unit templates under the install root
7
+ * `<prefix>/.meerkat-taskpanel/scheduling/` (deliberately *not* the loop units'
8
8
  * `<agent-dir>/scheduling/`, so re-installing an agent kind never touches it) and then
9
9
  * loads the one for this platform. It is the single **explicit exception** to
10
10
  * the installer's rule that it writes a unit and prints the load command but
@@ -50,6 +50,12 @@
50
50
  * exactly the readiness test the CLI applies. A unit that is loaded but not
51
51
  * serving reports `loaded … /health did not answer` instead, with the log to read.
52
52
  *
53
+ * **A board that is already running is not loaded over.** Before loading, the address the
54
+ * runtime pointer names is knocked once. A `/health` answer means a second `taskd` would
55
+ * only collide on that port (`EADDRINUSE`, a keepalive restart loop), so the units are
56
+ * written and the load is skipped, with the running board named. The same rule applies to
57
+ * the guard's own board lazily started by a CLI call: stop it and re-run to hand it over.
58
+ *
53
59
  * **The guard is one per machine, so a second install may have to refuse**
54
60
  * (`docs/scheduling.md § One guard per machine`). Because the label and the unit name
55
61
  * carry no board segment, the machine has exactly one guard job, and on Linux the file
@@ -175,8 +181,8 @@ export function guardPaths(options = {}) {
175
181
  nodeDir: dirname(node),
176
182
  serverEntry: join(root, "src", "server", "main.mjs"),
177
183
  logFile: join(dataDir, LOGS_DIRNAME, LOG_FILENAME),
178
- launchdUnit: join(prefix, "taskpanel", "scheduling", "launchd", `${TASKD_LABEL}.plist`),
179
- systemdUnit: join(prefix, "taskpanel", "scheduling", "systemd", TASKD_UNIT),
184
+ launchdUnit: join(prefix, ".meerkat-taskpanel", "scheduling", "launchd", `${TASKD_LABEL}.plist`),
185
+ systemdUnit: join(prefix, ".meerkat-taskpanel", "scheduling", "systemd", TASKD_UNIT),
180
186
  pointerFile: resolveRuntimePointerPath({ env }),
181
187
  };
182
188
  }
@@ -531,6 +537,26 @@ export async function installGuard(options = {}) {
531
537
  const already = runStep(plan.loadedProbe, run);
532
538
  if (already.ok) report.pid = waitForPid(plan, run, pidWaitMs);
533
539
 
540
+ // A board that is **already answering** owns the address this unit would bind. Loading
541
+ // the guard then does not create a keepalive — it starts a second server on an occupied
542
+ // port, which is an EADDRINUSE restart loop, not a board. The runtime pointer is the
543
+ // running board's own statement of its address, and `/health` is the same readiness test
544
+ // the CLI trusts, so one knock settles it. The unit files stay written (the operator can
545
+ // load them after stopping the board); nothing is loaded and nothing is claimed.
546
+ if (!already.ok) {
547
+ const running = (options.readPointerFn ?? readPointer)({ path: paths.pointerFile });
548
+ const runningUrl = running !== null && typeof running.url === "string" ? running.url : null;
549
+ if (runningUrl !== null && (await (options.healthProbe ?? probeHealth)(runningUrl))) {
550
+ report.state = "running";
551
+ report.url = runningUrl;
552
+ report.reason = `a board is already serving ${runningUrl}`;
553
+ log(
554
+ `taskd guard: not loaded — a board is already serving ${runningUrl}${running.pid ? ` (pid ${running.pid})` : ""}; stop it and re-run to install the guard`,
555
+ );
556
+ return report;
557
+ }
558
+ }
559
+
534
560
  // The board this run would install, read back from the file that would be copied —
535
561
  // what actually lands, not a re-derivation of it.
536
562
  const mine = unitBoard(readFileSync(kind === "launchd" ? paths.launchdUnit : paths.systemdUnit, "utf8"));
@@ -805,7 +831,7 @@ function waitForPid(plan, run, budgetMs) {
805
831
  const USAGE = `Usage: node scripts/install/lib/taskd-guard.mjs install --prefix <dir> [--root <tree>] [--data-dir <dir>] [--force]
806
832
 
807
833
  Writes the taskd keepalive unit (launchd + systemd) under
808
- <dir>/taskpanel/scheduling/, then loads this platform's one, waits for its
834
+ <dir>/.meerkat-taskpanel/scheduling/, then loads this platform's one, waits for its
809
835
  board to answer /health, and reports what happened.
810
836
  --root names the program tree the unit runs (default: this checkout);
811
837
  --data-dir pins TASKD_DATA_DIR in the unit (used by --target server).
@@ -10,7 +10,7 @@
10
10
  # ~/.config/systemd/user/ and enables it immediately.
11
11
  #
12
12
  # mkdir -p ~/.config/systemd/user
13
- # cp <prefix>/taskpanel/scheduling/systemd/meerkat-taskpanel-taskd.service ~/.config/systemd/user/
13
+ # cp <prefix>/.meerkat-taskpanel/scheduling/systemd/meerkat-taskpanel-taskd.service ~/.config/systemd/user/
14
14
  # systemctl --user enable --now meerkat-taskpanel-taskd.service
15
15
  #
16
16
  # ExecStart names the ABSOLUTE node that rendered this unit — never
@@ -12,7 +12,7 @@
12
12
 
13
13
  export const CLI_NAME = "taskctl";
14
14
  export const SKILL_NAME = "meerkat-taskpanel";
15
- export const VERSION = "0.1.0";
15
+ export const VERSION = "0.1.1";
16
16
 
17
17
  /**
18
18
  * The board's **contract schema generation** — which generation of the API/wire