novahiz 0.5.1 → 0.5.2

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/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.5.2] - 2026-10-05
9
+
10
+ Maintenance release closing the three leftovers traced right after `0.5.1`:
11
+ the installer's dead `plugin` config key, the unread `tokens` block of the
12
+ example config, and the scheduler's 0 ms wake loop on a busy task.
13
+
14
+ ### Fixed
15
+
16
+ - `install/install.mjs` no longer writes a `"plugin"` array of **file paths**
17
+ into a freshly created `opencode.jsonc`. opencode never read it (the plugins
18
+ are loaded from `<config>/plugins`, proved in E2E: the key pointed at the
19
+ repo while the loaded copies came from the plugins directory) and a file
20
+ entry triggered `configured plugin path must be a directory` at every
21
+ startup. Existing configs are left untouched.
22
+ - `novahiz.config.example.json` no longer advertises a `tokens` block
23
+ (`enabled`, `trimOutputs`, `keepHeadLines`, `dedupeReads`, `capOutputTokens`,
24
+ `trimTools`, …) that no code ever read — a leftover of a layer that was never
25
+ shipped. The example now carries exactly the keys `defaultConfig()` writes,
26
+ and `docs/TOKENS.md` states it instead of pointing at a block that should
27
+ not be there.
28
+ - `mcp/clepsydre/src/scheduler.ts`: `arm()` no longer arms a timer for a task
29
+ that is already running. A past-due schedule on a busy task used to feed a
30
+ 0 ms `wake → skip → arm` loop (one task-store read per turn) for the whole
31
+ execution, and a manual trigger could sustain it. The lock release now goes
32
+ through `release()`, which re-evaluates the calendar once the task is free,
33
+ so a due occurrence is still caught up exactly once instead of by the spin.
34
+
8
35
  ## [0.5.1] - 2026-10-05
9
36
 
10
37
  Fix-forward release: the `v0.5.0` tag was pushed but its CI never reached
package/docs/TOKENS.md CHANGED
@@ -95,9 +95,10 @@ from the local enforcement database.
95
95
 
96
96
  ## Not implemented
97
97
 
98
- `novahiz.config.example.json` still carries a `tokens` block (`enabled`,
99
- `trimOutputs`, `keepHeadLines`, `keepTailLines`, `keepErrorLines`,
100
- `dedupeReads`, `capOutputTokens`, `trimTools`, `readTools`). No code reads it:
101
- it is a leftover of a layer that was never shipped, and neither the plugin nor
102
- `novahiz tokens` consumes it. Configure the plugin with the environment
103
- variables above instead.
98
+ There is no `tokens` section in `novahiz.config.example.json`: no code ever
99
+ read the one it carried (`enabled`, `trimOutputs`, `keepHeadLines`,
100
+ `keepTailLines`, `keepErrorLines`, `dedupeReads`, `capOutputTokens`,
101
+ `trimTools`, `readTools`) — a leftover of a layer that was never shipped, and
102
+ neither the plugin nor `novahiz tokens` consumes it. It has been removed from
103
+ the example rather than left to contradict this document. Token economy is
104
+ configured with the environment variables above, and nothing else.
@@ -236,9 +236,11 @@ async function main() {
236
236
  }
237
237
  }
238
238
 
239
- // opencode charge les .ts posés dans <config>/plugins: E2E montre que le
240
- // registre des plugins déclare le chemin de ce répertoire, pas celui du
241
- // tableau "plugin" de la config — les deux restent en cohérence.
239
+ // opencode charge automatiquement les .ts de <config>/plugins. Preuve E2E:
240
+ // le tableau "plugin" de la config pointait vers ce depôt alors que les
241
+ // plugins charges venaient de <config>/plugins — le tableau est donc retire
242
+ // du modele de config, une entree fichier y declenchait l'avertissement
243
+ // "configured plugin path must be a directory" a chaque demarrage.
242
244
  for (const pluginFile of ["novahiz-plugin.ts", "novahiz-token-economy.ts"]) {
243
245
  const pluginSource = join(home, "adapters", "opencode", pluginFile);
244
246
  const pluginTarget = join(pluginsDir, pluginFile);
@@ -519,9 +521,6 @@ async function main() {
519
521
  "skills": {
520
522
  "paths": [skillsDir]
521
523
  },
522
- "plugin": [
523
- join(home, "adapters", "opencode", "novahiz-plugin.ts")
524
- ],
525
524
  "compaction": {
526
525
  "auto": true,
527
526
  "prune": true,
@@ -11,6 +11,10 @@
11
11
  // bascule toujours en traitement special : une tache n'execute jamais deux
12
12
  // fois en parallele (verrou `busy`), meme si la cadence est plus rapide que
13
13
  // la duree de l'execution.
14
+ // Le verrou ne s'arme jamais : `arm()` ignore les taches busy, sinon une
15
+ // echeance passee donnerait un timer 0 ms en boucle (`skip -> arm`) pendant
16
+ // toute l'execution ; la liberation re-evalue les echeances et rattrape
17
+ // l'occurrence due une seule fois.
14
18
 
15
19
  import { appendExecution, findTask, loadTasks, mutateTasks, newId, saveTasks, type Task } from "./store.ts";
16
20
  import { nextRun, parseSchedule, type Schedule } from "./cron.ts";
@@ -125,6 +129,11 @@ export class Scheduler {
125
129
  }
126
130
 
127
131
  /** Arme le prochain reveil sur l'echeance la plus proche.
132
+ * Une tache en execution (verrou `busy`) est ignoree : son echeance passe
133
+ * reste en file jusqu'a la liberation, et un reveil dessus ne ferait que
134
+ * boucler (`timer 0 ms -> wake -> skip -> arm`) pendant toute
135
+ * l'execution, a coup de lecture disque. Le reveil de liberation
136
+ * (`release`) re-evalue les echeances quand le verrou saute.
128
137
  * Retourne le delai retenu (null : aucune tache a armer), pour test. */
129
138
  arm(): number | null {
130
139
  this.stopTimer();
@@ -133,6 +142,7 @@ export class Scheduler {
133
142
  let earliest: number | null = null;
134
143
  for (const task of tasks) {
135
144
  if (!task.enabled || !task.schedule) continue;
145
+ if (this.busy.has(task.id)) continue;
136
146
  const due = this.dueMs(task);
137
147
  if (due === null) continue;
138
148
  if (earliest === null || due < earliest) earliest = due;
@@ -239,6 +249,17 @@ export class Scheduler {
239
249
  this.arm();
240
250
  }
241
251
 
252
+ /** Libere le verrou d'une tache puis re-evalue les echeances.
253
+ * Sans ce reveil, une occurrence devenue due pendant l'execution
254
+ * (`triggerNow` / `runNow` n'avancent pas le calendrier) resterait due sur
255
+ * disque en panne depuis que `arm()` ignore les taches busy. Le
256
+ * rattrapage reste unique : le premier reveil avance `nextRunAt`, le
257
+ * suivant ne trouve plus rien a lancer. */
258
+ private release(id: string): void {
259
+ this.busy.delete(id);
260
+ this.wake();
261
+ }
262
+
242
263
  /** Declenchement manuel DETACHE : pre-controles synchrones (tache connue,
243
264
  * aucune execution en cours), puis execution en arriere-plan. Le serveur
244
265
  * stdio reste disponible pour les autres requetes ; le resultat se lit
@@ -257,7 +278,7 @@ export class Scheduler {
257
278
  // non capte qui tuerait le serveur.
258
279
  })
259
280
  .finally(() => {
260
- this.busy.delete(task.id);
281
+ this.release(task.id);
261
282
  });
262
283
  return { ok: true };
263
284
  }
@@ -275,7 +296,7 @@ export class Scheduler {
275
296
  } catch (error) {
276
297
  return { ok: false, reason: error instanceof Error ? error.message : String(error) };
277
298
  } finally {
278
- this.busy.delete(task.id);
299
+ this.release(task.id);
279
300
  }
280
301
  }
281
302
 
@@ -300,7 +321,7 @@ export class Scheduler {
300
321
  .then(() => runOne(index + 1));
301
322
  };
302
323
  void runOne(0).finally(() => {
303
- this.busy.delete(task.id);
324
+ this.release(task.id);
304
325
  });
305
326
  }
306
327
 
@@ -19,17 +19,5 @@
19
19
  "autoRegister": true,
20
20
  "autoInstall": true,
21
21
  "disabled": []
22
- },
23
- "tokens": {
24
- "enabled": true,
25
- "trimOutputs": true,
26
- "maxOutputBytes": 40000,
27
- "keepHeadLines": 120,
28
- "keepTailLines": 40,
29
- "keepErrorLines": 40,
30
- "dedupeReads": true,
31
- "capOutputTokens": 0,
32
- "trimTools": ["read", "bash", "shell"],
33
- "readTools": ["read"]
34
22
  }
35
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novahiz",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Enforce AI agent skills before every file edit: prompt classifier, skill catalog, and pre-edit gate.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",