@ctxr/skill-llm-wiki 1.0.2 → 1.2.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.
package/scripts/cli.mjs CHANGED
@@ -326,9 +326,38 @@ Layout-mode flags (build/extend/rebuild/fix/join):
326
326
  --target <path> Explicit destination (required for hosted)
327
327
 
328
328
  Tiered-AI flags:
329
- --quality-mode tiered-fast|claude-first|tier0-only
329
+ --quality-mode tiered-fast|claude-first|deterministic
330
330
  Default: tiered-fast (TF-IDF → embeddings
331
- → Claude ladder). See guide/tiered-ai.md.
331
+ → Claude ladder). The 'deterministic' mode
332
+ resolves every decision algorithmically
333
+ (no Tier 2 calls) for byte-reproducible
334
+ builds. See guide/tiered-ai.md.
335
+
336
+ Post-convergence enforcement flags (build/rebuild):
337
+ --fanout-target <N> Post-convergence phase ATTEMPTS to
338
+ sub-cluster any directory whose movable
339
+ leaf count exceeds N × 1.5 (subdirs aren't
340
+ counted — the rebalance can only carve
341
+ clusters from leaves, so subdir-heavy
342
+ dirs are un-actionable). An overfull dir
343
+ is left unchanged when no coherent
344
+ cluster is detected (leaves too diverse
345
+ or too few). N must be an integer in
346
+ [2, 100]. No-op when omitted.
347
+ --max-depth <D> Post-convergence phase flattens any
348
+ single-child passthrough deeper than D. D
349
+ must be an integer in [1, 10]. No-op when
350
+ omitted.
351
+ --soft-dag-parents Post-convergence phase synthesises
352
+ soft-parent pointers in each leaf's
353
+ parents[] based on TF-IDF cosine
354
+ similarity against candidate category
355
+ directories. Leaves appear in multiple
356
+ index.md entries[] (DAG view) rather
357
+ than only their direct parent. Uses
358
+ SOFT_PARENT_AFFINITY_THRESHOLD (0.35)
359
+ and caps at SOFT_PARENT_MAX_PER_LEAF (3)
360
+ per leaf. No-op when omitted.
332
361
 
333
362
  UX flags:
334
363
  --no-prompt Never prompt; fail loud on ambiguity
@@ -378,6 +407,9 @@ const FLAG_WITH_VALUE = new Set([
378
407
  "--to",
379
408
  "--canonical",
380
409
  "--quality-mode",
410
+ "--fanout-target",
411
+ "--max-depth",
412
+ "--id-collision",
381
413
  ]);
382
414
  const FLAG_BOOLEAN = new Set([
383
415
  "--no-prompt",
@@ -386,6 +418,7 @@ const FLAG_BOOLEAN = new Set([
386
418
  "--accept-dirty",
387
419
  "--accept-foreign-target",
388
420
  "--review",
421
+ "--soft-dag-parents",
389
422
  ]);
390
423
 
391
424
  function parseSubArgv(raw) {
@@ -815,12 +848,69 @@ async function main() {
815
848
  }
816
849
  const opId = newOpId(cmd);
817
850
  const startedIso = new Date().toISOString();
851
+ // X.9 progress: stream each orchestrator phase's `record()`
852
+ // output to stderr as it fires. On by default for interactive
853
+ // use; suppressed in JSON mode so stderr stays reserved for
854
+ // structured JSON diagnostics/errors (build/rebuild/fix/join
855
+ // still print their human-readable summary to stdout under
856
+ // --json, but a consumer tailing stderr under --json expects
857
+ // it to be empty on success or carry only structured
858
+ // diagnostics — progress breadcrumbs would mix into that
859
+ // channel). Also suppressed via `LLM_WIKI_NO_PROGRESS=1` for
860
+ // CI / hermetic runs that want the pre-X.9 silent shape.
861
+ // Progress goes to stderr (never stdout) so consumers piping
862
+ // the command's stdout don't conflate phase chatter with the
863
+ // final op-summary payload.
864
+ const suppressProgress =
865
+ jsonMode || process.env.LLM_WIKI_NO_PROGRESS === "1";
866
+ // A closed-stderr scenario (downstream pipe consumer that
867
+ // drops the stderr pipe, e.g. `… | head`) surfaces two ways:
868
+ // a synchronous throw on `.write()` (try/catch below), and
869
+ // an async `'error'` event with code EPIPE that the default
870
+ // Node handler escalates to an unhandled exception. Attach a
871
+ // best-effort one-time 'error' listener to swallow EPIPE
872
+ // while progress is enabled; other stream errors still
873
+ // surface via Node's default handler so we aren't masking
874
+ // bugs unrelated to the progress stream.
875
+ let stderrEpipeSilenced = false;
876
+ const onProgress = suppressProgress
877
+ ? null
878
+ : (phase) => {
879
+ if (!stderrEpipeSilenced) {
880
+ stderrEpipeSilenced = true;
881
+ try {
882
+ process.stderr.on("error", (err) => {
883
+ if (err && err.code === "EPIPE") return;
884
+ // Re-emit non-EPIPE errors so we don't hide real
885
+ // problems — Node's default handler will decide.
886
+ throw err;
887
+ });
888
+ } catch {
889
+ /* attach failed — progress reporter will best-effort */
890
+ }
891
+ }
892
+ // `.destroyed` / `.writableEnded` guards let us skip the
893
+ // write before it would throw, which is cheaper than
894
+ // catching the exception and matches the behaviour on
895
+ // an already-closed stream.
896
+ if (process.stderr.destroyed || process.stderr.writableEnded) {
897
+ return;
898
+ }
899
+ try {
900
+ process.stderr.write(
901
+ `[${opId} ${phase.index}] ${phase.name}: ${phase.summary}\n`,
902
+ );
903
+ } catch {
904
+ /* Best-effort — a closed stderr shouldn't halt the op. */
905
+ }
906
+ };
818
907
  let result;
819
908
  try {
820
909
  result = await runOperation(plan, {
821
910
  opId,
822
911
  source: plan.source,
823
912
  startedIso,
913
+ onProgress,
824
914
  });
825
915
  } catch (err) {
826
916
  if (err instanceof NonInteractiveError) {