@krtclcdy/tersio 2.25.6 → 2.25.7

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.
Files changed (188) hide show
  1. package/README.md +30 -7
  2. package/dist/bench/code-out/pfull-0/debounce.js +13 -0
  3. package/dist/bench/code-out/pfull-0/deepClone.js +18 -0
  4. package/dist/bench/code-out/pfull-0/groupBy.js +15 -0
  5. package/dist/bench/code-out/pfull-1/debounce.js +13 -0
  6. package/dist/bench/code-out/pfull-1/deepClone.js +17 -0
  7. package/dist/bench/code-out/pfull-1/groupBy.js +14 -0
  8. package/dist/bench/code-out/pfull-2/debounce.js +13 -0
  9. package/dist/bench/code-out/pfull-2/deepClone.js +18 -0
  10. package/dist/bench/code-out/pfull-2/groupBy.js +15 -0
  11. package/dist/bench/code-out/pfull-3/debounce.js +12 -0
  12. package/dist/bench/code-out/pfull-3/deepClone.js +17 -0
  13. package/dist/bench/code-out/pfull-3/groupBy.js +7 -0
  14. package/dist/bench/code-out/pfull-4/debounce.js +10 -0
  15. package/dist/bench/code-out/pfull-4/deepClone.js +17 -0
  16. package/dist/bench/code-out/pfull-4/groupBy.js +14 -0
  17. package/dist/bench/code-out/plite-0/debounce.js +13 -0
  18. package/dist/bench/code-out/plite-0/deepClone.js +18 -0
  19. package/dist/bench/code-out/plite-0/groupBy.js +15 -0
  20. package/dist/bench/code-out/plite-1/debounce.js +13 -0
  21. package/dist/bench/code-out/plite-1/deepClone.js +17 -0
  22. package/dist/bench/code-out/plite-1/groupBy.js +11 -0
  23. package/dist/bench/code-out/plite-2/debounce.js +12 -0
  24. package/dist/bench/code-out/plite-2/deepClone.js +17 -0
  25. package/dist/bench/code-out/plite-2/groupBy.js +14 -0
  26. package/dist/bench/code-out/plite-3/debounce.js +12 -0
  27. package/dist/bench/code-out/plite-3/deepClone.js +17 -0
  28. package/dist/bench/code-out/plite-3/groupBy.js +14 -0
  29. package/dist/bench/code-out/plite-4/debounce.js +12 -0
  30. package/dist/bench/code-out/plite-4/deepClone.js +17 -0
  31. package/dist/bench/code-out/plite-4/groupBy.js +14 -0
  32. package/dist/bench/code-out/pultra-0/debounce.js +12 -0
  33. package/dist/bench/code-out/pultra-0/deepClone.js +20 -0
  34. package/dist/bench/code-out/pultra-0/groupBy.js +14 -0
  35. package/dist/bench/code-out/pultra-1/debounce.js +10 -0
  36. package/dist/bench/code-out/pultra-1/deepClone.js +17 -0
  37. package/dist/bench/code-out/pultra-1/groupBy.js +7 -0
  38. package/dist/bench/code-out/pultra-2/debounce.js +13 -0
  39. package/dist/bench/code-out/pultra-2/deepClone.js +18 -0
  40. package/dist/bench/code-out/pultra-2/groupBy.js +15 -0
  41. package/dist/bench/code-out/pultra-3/debounce.js +13 -0
  42. package/dist/bench/code-out/pultra-3/deepClone.js +18 -0
  43. package/dist/bench/code-out/pultra-3/groupBy.js +15 -0
  44. package/dist/bench/code-out/pultra-4/debounce.js +13 -0
  45. package/dist/bench/code-out/pultra-4/deepClone.js +18 -0
  46. package/dist/bench/code-out/pultra-4/groupBy.js +15 -0
  47. package/dist/bench/code-out/run-0/debounce.js +12 -0
  48. package/dist/bench/code-out/run-0/deepClone.js +17 -0
  49. package/dist/bench/code-out/run-0/groupBy.js +14 -0
  50. package/dist/bench/code-out/run-1/debounce.js +13 -0
  51. package/dist/bench/code-out/run-1/deepClone.js +15 -0
  52. package/dist/bench/code-out/run-1/groupBy.js +13 -0
  53. package/dist/bench/code-out/run-2/debounce.js +12 -0
  54. package/dist/bench/code-out/run-2/deepClone.js +18 -0
  55. package/dist/bench/code-out/run-2/groupBy.js +13 -0
  56. package/dist/bench/code-out/run-3/debounce.js +12 -0
  57. package/dist/bench/code-out/run-3/deepClone.js +18 -0
  58. package/dist/bench/code-out/run-3/groupBy.js +14 -0
  59. package/dist/bench/code-out/run-4/debounce.js +12 -0
  60. package/dist/bench/code-out/run-4/deepClone.js +16 -0
  61. package/dist/bench/code-out/run-4/groupBy.js +13 -0
  62. package/dist/cli/banner.js +1 -2
  63. package/dist/cli/common.js +1 -1
  64. package/dist/cli/doctor.js +48 -12
  65. package/dist/cli/hosts.js +1 -2
  66. package/dist/cli/install.js +7 -2
  67. package/dist/cli/manifest.js +25 -5
  68. package/dist/cli/uninstall.js +1 -1
  69. package/dist/extensions/caveman/index.js +6 -2
  70. package/dist/extensions/caveman/rule-lite.md +77 -0
  71. package/dist/extensions/caveman/rule-megacave-lite.md +59 -0
  72. package/dist/extensions/caveman/rule-megacave-ultra.md +59 -0
  73. package/dist/extensions/caveman/rule.md +1 -1
  74. package/dist/extensions/combo/index.js +26 -7
  75. package/dist/extensions/pstack/index.js +3 -1
  76. package/dist/extensions/pstack/rule.md +88 -12
  77. package/dist/extensions/rtk/session.js +18 -10
  78. package/dist/extensions/shared/rtk-eligible.js +84 -0
  79. package/dist/extensions/shared/rtk-gain.js +1 -1
  80. package/dist/extensions/shared/sqlite.js +7 -13
  81. package/dist/extensions/shared/status.js +1 -2
  82. package/dist/extensions/shared/usage-ledger.js +102 -20
  83. package/extensions/caveman/index.ts +6 -2
  84. package/extensions/caveman/rule-lite.md +77 -0
  85. package/extensions/caveman/rule-megacave-lite.md +59 -0
  86. package/extensions/caveman/rule-megacave-ultra.md +59 -0
  87. package/extensions/caveman/rule.md +1 -1
  88. package/extensions/combo/index.ts +27 -7
  89. package/extensions/pstack/index.ts +3 -1
  90. package/extensions/pstack/playbooks/authoring-a-skill.md +12 -0
  91. package/extensions/pstack/playbooks/autonomous-run.md +13 -0
  92. package/extensions/pstack/playbooks/autopilot-full.md +13 -0
  93. package/extensions/pstack/playbooks/autopilot-stack.md +16 -0
  94. package/extensions/pstack/playbooks/babysit.md +23 -0
  95. package/extensions/pstack/playbooks/bug-fix.md +15 -0
  96. package/extensions/pstack/playbooks/eval.md +25 -0
  97. package/extensions/pstack/playbooks/feature.md +21 -0
  98. package/extensions/pstack/playbooks/hillclimb.md +21 -0
  99. package/extensions/pstack/playbooks/investigation.md +14 -0
  100. package/extensions/pstack/playbooks/multi-phase-plan.md +155 -0
  101. package/extensions/pstack/playbooks/opening-a-pr.md +34 -0
  102. package/extensions/pstack/playbooks/orchestrate.md +111 -0
  103. package/extensions/pstack/playbooks/pause-safely.md +10 -0
  104. package/extensions/pstack/playbooks/perf-issue.md +25 -0
  105. package/extensions/pstack/playbooks/prototype.md +14 -0
  106. package/extensions/pstack/playbooks/refactoring.md +16 -0
  107. package/extensions/pstack/playbooks/runtime-forensics.md +11 -0
  108. package/extensions/pstack/playbooks/session-pickup.md +11 -0
  109. package/extensions/pstack/playbooks/shipping.md +17 -0
  110. package/extensions/pstack/playbooks/trace-forensics.md +14 -0
  111. package/extensions/pstack/playbooks/visual-parity.md +11 -0
  112. package/extensions/pstack/playbooks/worktree-cleanup.md +12 -0
  113. package/extensions/pstack/principles/attack-the-premise.md +22 -0
  114. package/extensions/pstack/principles/boundary-discipline.md +33 -0
  115. package/extensions/pstack/principles/build-the-lever.md +22 -0
  116. package/extensions/pstack/principles/encode-lessons-in-structure.md +10 -0
  117. package/extensions/pstack/principles/exhaust-the-design-space.md +8 -0
  118. package/extensions/pstack/principles/experience-first.md +18 -0
  119. package/extensions/pstack/principles/explain-the-number.md +16 -0
  120. package/extensions/pstack/principles/fix-root-causes.md +23 -0
  121. package/extensions/pstack/principles/foundational-thinking.md +20 -0
  122. package/extensions/pstack/principles/guard-the-context-window.md +11 -0
  123. package/extensions/pstack/principles/laziness-protocol.md +9 -0
  124. package/extensions/pstack/principles/make-operations-idempotent.md +23 -0
  125. package/extensions/pstack/principles/migrate-callers-then-delete-legacy-apis.md +21 -0
  126. package/extensions/pstack/principles/minimize-reader-load.md +17 -0
  127. package/extensions/pstack/principles/model-the-domain.md +23 -0
  128. package/extensions/pstack/principles/never-block-on-the-human.md +14 -0
  129. package/extensions/pstack/principles/outcome-oriented-execution.md +20 -0
  130. package/extensions/pstack/principles/prove-it-works.md +21 -0
  131. package/extensions/pstack/principles/redesign-from-first-principles.md +15 -0
  132. package/extensions/pstack/principles/separate-before-serializing-shared-state.md +15 -0
  133. package/extensions/pstack/principles/sequence-verifiable-units.md +16 -0
  134. package/extensions/pstack/principles/subtract-before-you-add.md +17 -0
  135. package/extensions/pstack/principles/test-behavior-not-implementation.md +20 -0
  136. package/extensions/pstack/principles/type-system-discipline.md +17 -0
  137. package/extensions/pstack/references/bugbot-triage.md +142 -0
  138. package/extensions/pstack/rule.md +88 -12
  139. package/extensions/pstack/skills/architect/SKILL.md +78 -0
  140. package/extensions/pstack/skills/architect/references/design-red-flags.md +57 -0
  141. package/extensions/pstack/skills/architect/references/rationale-template.md +35 -0
  142. package/extensions/pstack/skills/architect/references/runner-prompt.md +20 -0
  143. package/extensions/pstack/skills/arena/SKILL.md +47 -0
  144. package/extensions/pstack/skills/benchmark-checklist/SKILL.md +38 -0
  145. package/extensions/pstack/skills/blast-radius/SKILL.md +49 -0
  146. package/extensions/pstack/skills/bro/SKILL.md +6 -0
  147. package/extensions/pstack/skills/correct/SKILL.md +31 -0
  148. package/extensions/pstack/skills/figure-it-out/SKILL.md +51 -0
  149. package/extensions/pstack/skills/how/SKILL.md +35 -0
  150. package/extensions/pstack/skills/how/references/explainer-prompt.md +55 -0
  151. package/extensions/pstack/skills/how/references/explorer-prompt.md +35 -0
  152. package/extensions/pstack/skills/interrogate/SKILL.md +73 -0
  153. package/extensions/pstack/skills/interrogate/references/code-quality-review.md +47 -0
  154. package/extensions/pstack/skills/interrogate/references/lead-judgment.md +58 -0
  155. package/extensions/pstack/skills/interrogate/references/reviewer-prompt.md +62 -0
  156. package/extensions/pstack/skills/interrogate/references/rubric.md +77 -0
  157. package/extensions/pstack/skills/no-comments/SKILL.md +23 -0
  158. package/extensions/pstack/skills/reflect/SKILL.md +66 -0
  159. package/extensions/pstack/skills/reflect/references/divergent-reviewer.md +38 -0
  160. package/extensions/pstack/skills/reflect/references/judgment-reviewer.md +37 -0
  161. package/extensions/pstack/skills/reflect/references/synthesizer.md +56 -0
  162. package/extensions/pstack/skills/reflect/references/tooling-reviewer.md +48 -0
  163. package/extensions/pstack/skills/show-me-your-work/SKILL.md +81 -0
  164. package/extensions/pstack/skills/swarm/SKILL.md +45 -0
  165. package/extensions/pstack/skills/tdd/SKILL.md +41 -0
  166. package/extensions/pstack/skills/technical-writing/SKILL.md +105 -0
  167. package/extensions/pstack/skills/unslop/SKILL.md +61 -0
  168. package/extensions/pstack/skills/why/SKILL.md +97 -0
  169. package/extensions/pstack/skills/why/references/epistemics.md +71 -0
  170. package/extensions/pstack/skills/why/references/investigator-prompt.md +84 -0
  171. package/extensions/pstack/skills/why/references/source-playbook.md +17 -0
  172. package/extensions/pstack/skills/why/references/sources/code-archaeology.md +51 -0
  173. package/extensions/pstack/skills/why/references/sources/databricks.md +70 -0
  174. package/extensions/pstack/skills/why/references/sources/datadog.md +97 -0
  175. package/extensions/pstack/skills/why/references/sources/incident-postmortem.md +5 -0
  176. package/extensions/pstack/skills/why/references/sources/linear.md +48 -0
  177. package/extensions/pstack/skills/why/references/sources/notion.md +55 -0
  178. package/extensions/pstack/skills/why/references/sources/sentry.md +66 -0
  179. package/extensions/pstack/skills/why/references/sources/slack.md +17 -0
  180. package/extensions/pstack/skills/why/references/synthesizer-prompt.md +129 -0
  181. package/extensions/rtk/session.ts +17 -9
  182. package/extensions/shared/rtk-eligible.ts +83 -0
  183. package/extensions/shared/rtk-gain.ts +1 -1
  184. package/extensions/shared/sqlite.ts +7 -13
  185. package/extensions/shared/status.ts +1 -2
  186. package/extensions/shared/usage-ledger.ts +97 -19
  187. package/extensions/timing/index.ts +1 -2
  188. package/package.json +7 -4
@@ -0,0 +1,18 @@
1
+ // deepClone: recursive clone of plain objects, arrays, Date, and primitives. Shares no mutable structure with the input; no structuredClone or JSON round-trip.
2
+ export function deepClone(value) {
3
+ if (value === null || typeof value !== 'object') {
4
+ return value;
5
+ }
6
+ if (value instanceof Date) {
7
+ return new Date(value.getTime());
8
+ }
9
+ if (Array.isArray(value)) {
10
+ return value.map((item) => deepClone(item));
11
+ }
12
+ const source = value;
13
+ const clone = {};
14
+ for (const key of Object.keys(source)) {
15
+ clone[key] = deepClone(source[key]);
16
+ }
17
+ return clone;
18
+ }
@@ -0,0 +1,15 @@
1
+ // groupBy: buckets items by keyFn. Keys land in first-seen order and each bucket keeps the input order.
2
+ export function groupBy(items, keyFn) {
3
+ const groups = {};
4
+ for (const item of items) {
5
+ const key = keyFn(item);
6
+ const bucket = groups[key];
7
+ if (bucket === undefined) {
8
+ groups[key] = [item];
9
+ }
10
+ else {
11
+ bucket.push(item);
12
+ }
13
+ }
14
+ return groups;
15
+ }
@@ -0,0 +1,13 @@
1
+ // Debounce: fires fn ms after the last call with that call's arguments; a call during the wait restarts it.
2
+ export function debounce(fn, ms) {
3
+ let timer;
4
+ return (...args) => {
5
+ if (timer !== undefined) {
6
+ clearTimeout(timer);
7
+ }
8
+ timer = setTimeout(() => {
9
+ timer = undefined;
10
+ fn(...args);
11
+ }, ms);
12
+ };
13
+ }
@@ -0,0 +1,18 @@
1
+ // deepClone: recursive clone of plain objects, arrays, Date, and primitives; shares no mutable structure and never round-trips through structuredClone or JSON.
2
+ export function deepClone(value) {
3
+ if (value === null || typeof value !== 'object') {
4
+ return value;
5
+ }
6
+ if (value instanceof Date) {
7
+ return new Date(value.getTime());
8
+ }
9
+ if (Array.isArray(value)) {
10
+ return value.map((item) => deepClone(item));
11
+ }
12
+ const source = value;
13
+ const clone = {};
14
+ for (const key of Object.keys(source)) {
15
+ clone[key] = deepClone(source[key]);
16
+ }
17
+ return clone;
18
+ }
@@ -0,0 +1,15 @@
1
+ // groupBy: buckets items by keyFn; keys land in first-seen order and each bucket preserves input order.
2
+ export function groupBy(items, keyFn) {
3
+ const groups = {};
4
+ for (const item of items) {
5
+ const key = keyFn(item);
6
+ const bucket = groups[key];
7
+ if (bucket === undefined) {
8
+ groups[key] = [item];
9
+ }
10
+ else {
11
+ bucket.push(item);
12
+ }
13
+ }
14
+ return groups;
15
+ }
@@ -0,0 +1,13 @@
1
+ // Debounce: fires fn once, ms after the last call, using that last call's arguments. A call during the wait restarts it.
2
+ export function debounce(fn, ms) {
3
+ let timer;
4
+ return (...args) => {
5
+ if (timer !== undefined) {
6
+ clearTimeout(timer);
7
+ }
8
+ timer = setTimeout(() => {
9
+ timer = undefined;
10
+ fn(...args);
11
+ }, ms);
12
+ };
13
+ }
@@ -0,0 +1,18 @@
1
+ // deepClone: recursive clone of plain objects, arrays, Date, and primitives. Shares no mutable structure with the input; no structuredClone or JSON round-trip.
2
+ export function deepClone(value) {
3
+ if (value === null || typeof value !== 'object') {
4
+ return value;
5
+ }
6
+ if (value instanceof Date) {
7
+ return new Date(value.getTime());
8
+ }
9
+ if (Array.isArray(value)) {
10
+ return value.map((item) => deepClone(item));
11
+ }
12
+ const source = value;
13
+ const clone = {};
14
+ for (const key of Object.keys(source)) {
15
+ clone[key] = deepClone(source[key]);
16
+ }
17
+ return clone;
18
+ }
@@ -0,0 +1,15 @@
1
+ // groupBy: buckets items by keyFn. Keys land in first-seen order and each bucket keeps the input order.
2
+ export function groupBy(items, keyFn) {
3
+ const groups = {};
4
+ for (const item of items) {
5
+ const key = keyFn(item);
6
+ const bucket = groups[key];
7
+ if (bucket === undefined) {
8
+ groups[key] = [item];
9
+ }
10
+ else {
11
+ bucket.push(item);
12
+ }
13
+ }
14
+ return groups;
15
+ }
@@ -0,0 +1,12 @@
1
+ export function debounce(fn, ms) {
2
+ let timer;
3
+ return (...args) => {
4
+ if (timer !== undefined) {
5
+ clearTimeout(timer);
6
+ }
7
+ timer = setTimeout(() => {
8
+ timer = undefined;
9
+ fn(...args);
10
+ }, ms);
11
+ };
12
+ }
@@ -0,0 +1,17 @@
1
+ export function deepClone(value) {
2
+ if (value === null || typeof value !== 'object') {
3
+ return value;
4
+ }
5
+ if (value instanceof Date) {
6
+ return new Date(value.getTime());
7
+ }
8
+ if (Array.isArray(value)) {
9
+ return value.map((item) => deepClone(item));
10
+ }
11
+ const source = value;
12
+ const clone = {};
13
+ for (const key of Object.keys(source)) {
14
+ clone[key] = deepClone(source[key]);
15
+ }
16
+ return clone;
17
+ }
@@ -0,0 +1,14 @@
1
+ export function groupBy(items, keyFn) {
2
+ const groups = {};
3
+ for (const item of items) {
4
+ const key = keyFn(item);
5
+ const existing = groups[key];
6
+ if (existing === undefined) {
7
+ groups[key] = [item];
8
+ }
9
+ else {
10
+ existing.push(item);
11
+ }
12
+ }
13
+ return groups;
14
+ }
@@ -0,0 +1,13 @@
1
+ // Debounce: the returned wrapper fires fn once, after ms milliseconds of quiet, using the last call's arguments.
2
+ export function debounce(fn, ms) {
3
+ let timer;
4
+ // Each call rebinds args, so the closure the timeout reads is always the latest one.
5
+ return (...args) => {
6
+ if (timer !== undefined)
7
+ clearTimeout(timer);
8
+ timer = setTimeout(() => {
9
+ timer = undefined;
10
+ fn(...args);
11
+ }, ms);
12
+ };
13
+ }
@@ -0,0 +1,15 @@
1
+ // deepClone: recursive clone for plain objects, arrays, Date, and primitives. No structuredClone or JSON round-trip.
2
+ export function deepClone(value) {
3
+ if (value === null || typeof value !== 'object')
4
+ return value;
5
+ if (value instanceof Date)
6
+ return new Date(value.getTime());
7
+ if (Array.isArray(value))
8
+ return value.map((item) => deepClone(item));
9
+ const source = value;
10
+ const clone = {};
11
+ for (const key of Object.keys(source)) {
12
+ clone[key] = deepClone(source[key]);
13
+ }
14
+ return clone;
15
+ }
@@ -0,0 +1,13 @@
1
+ // groupBy: buckets items by keyFn. Keys land in first-seen order and each bucket keeps the input order.
2
+ export function groupBy(items, keyFn) {
3
+ const groups = {};
4
+ for (const item of items) {
5
+ const key = keyFn(item);
6
+ const bucket = groups[key];
7
+ if (bucket === undefined)
8
+ groups[key] = [item];
9
+ else
10
+ bucket.push(item);
11
+ }
12
+ return groups;
13
+ }
@@ -0,0 +1,12 @@
1
+ // Trailing-edge debounce: `fn` runs only once `ms` passes with no further call, and always with the last call's arguments.
2
+ export function debounce(fn, ms) {
3
+ let timer;
4
+ return (...args) => {
5
+ if (timer !== undefined)
6
+ clearTimeout(timer);
7
+ timer = setTimeout(() => {
8
+ timer = undefined;
9
+ fn(...args);
10
+ }, ms);
11
+ };
12
+ }
@@ -0,0 +1,18 @@
1
+ // Recursive structural copy: plain objects, arrays, Dates, and primitives. No shared mutable structure with the input.
2
+ export function deepClone(value) {
3
+ if (value === null || typeof value !== 'object')
4
+ return value;
5
+ // SAFETY: a Date owns no enumerable props, so it must be rebuilt from its epoch ms or it clones to `{}`.
6
+ if (value instanceof Date)
7
+ return new Date(value.getTime());
8
+ // SAFETY: Array.isArray above proves an array, so mapping deepClone over the elements preserves its shape.
9
+ if (Array.isArray(value)) {
10
+ const items = value;
11
+ return items.map((item) => deepClone(item));
12
+ }
13
+ // SAFETY: Object.entries copies every own enumerable key, so the plain-object form of T is reproduced.
14
+ const clone = {};
15
+ for (const [key, item] of Object.entries(value))
16
+ clone[key] = deepClone(item);
17
+ return clone;
18
+ }
@@ -0,0 +1,13 @@
1
+ // Buckets items by `keyFn`, preserving input order inside each group.
2
+ export function groupBy(items, keyFn) {
3
+ const groups = {};
4
+ for (const item of items) {
5
+ const key = keyFn(item);
6
+ const bucket = groups[key];
7
+ if (bucket === undefined)
8
+ groups[key] = [item];
9
+ else
10
+ bucket.push(item);
11
+ }
12
+ return groups;
13
+ }
@@ -0,0 +1,12 @@
1
+ export function debounce(fn, ms) {
2
+ let timer;
3
+ return (...args) => {
4
+ if (timer !== undefined) {
5
+ clearTimeout(timer);
6
+ }
7
+ timer = setTimeout(() => {
8
+ timer = undefined;
9
+ fn(...args);
10
+ }, ms);
11
+ };
12
+ }
@@ -0,0 +1,18 @@
1
+ export function deepClone(value) {
2
+ if (value === null || typeof value !== 'object') {
3
+ return value;
4
+ }
5
+ if (value instanceof Date) {
6
+ return new Date(value.getTime());
7
+ }
8
+ if (Array.isArray(value)) {
9
+ const source = value;
10
+ return source.map((item) => deepClone(item));
11
+ }
12
+ const source = value;
13
+ const clone = {};
14
+ for (const [key, item] of Object.entries(source)) {
15
+ clone[key] = deepClone(item);
16
+ }
17
+ return clone;
18
+ }
@@ -0,0 +1,14 @@
1
+ export function groupBy(items, keyFn) {
2
+ const groups = {};
3
+ for (const item of items) {
4
+ const key = keyFn(item);
5
+ const existing = groups[key];
6
+ if (existing === undefined) {
7
+ groups[key] = [item];
8
+ }
9
+ else {
10
+ existing.push(item);
11
+ }
12
+ }
13
+ return groups;
14
+ }
@@ -0,0 +1,12 @@
1
+ export function debounce(fn, ms) {
2
+ let timer;
3
+ return (...args) => {
4
+ if (timer !== undefined) {
5
+ clearTimeout(timer);
6
+ }
7
+ timer = setTimeout(() => {
8
+ timer = undefined;
9
+ fn(...args);
10
+ }, ms);
11
+ };
12
+ }
@@ -0,0 +1,16 @@
1
+ export function deepClone(value) {
2
+ if (value === null || typeof value !== 'object') {
3
+ return value;
4
+ }
5
+ if (value instanceof Date) {
6
+ return new Date(value.getTime());
7
+ }
8
+ if (Array.isArray(value)) {
9
+ return value.map((item) => deepClone(item));
10
+ }
11
+ const clone = {};
12
+ for (const [key, item] of Object.entries(value)) {
13
+ clone[key] = deepClone(item);
14
+ }
15
+ return clone;
16
+ }
@@ -0,0 +1,13 @@
1
+ export function groupBy(items, keyFn) {
2
+ const groups = {};
3
+ for (const item of items) {
4
+ const key = keyFn(item);
5
+ if (Object.hasOwn(groups, key)) {
6
+ groups[key].push(item);
7
+ }
8
+ else {
9
+ groups[key] = [item];
10
+ }
11
+ }
12
+ return groups;
13
+ }
@@ -81,8 +81,7 @@ function printWelcome() {
81
81
  'tersio update refresh CLI + add-ons',
82
82
  'tersio doctor verify the install',
83
83
  ];
84
- // Below 26 rows the note scrolls off the top and slices the art, and the screen must also hold the
85
- // update prompt and every option, so the tips take one line there.
84
+ // Below 26 rows the tips scroll off the top and slice the art, so they take one line there.
86
85
  if (process.stdout.rows < 26) {
87
86
  clackNote(` ${tips.join(' · ')}`);
88
87
  return;
@@ -131,7 +131,7 @@ async function writeIfChanged(dest, content, options = {}) {
131
131
  return true;
132
132
  }
133
133
  await fs.mkdir(path.dirname(dest), { recursive: true });
134
- if (existing !== null) {
134
+ if (existing !== null && options.backup === true) {
135
135
  await fs.copyFile(dest, `${dest}.bak`);
136
136
  debug(`${path.basename(dest)} → ${path.basename(dest)}.bak`);
137
137
  }
@@ -4,13 +4,14 @@ import { existsSync, readdirSync, readFileSync, statSync, promises as fs } from
4
4
  import path from 'node:path';
5
5
  import { HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, args, dryRun, fix, yes, BUN_BIN_DIR, OMP_BIN, execP, parseJsonObject, relTime, } from './common.js';
6
6
  import { detectHosts, hostExtensionsDir, ompPackageDir, piPonytailPackageDir } from './hosts.js';
7
+ import { GENERATED_TREE_FILES } from './manifest.js';
7
8
  import { askInteractiveChoice, askInteractiveConfirm, runInteractivePhase } from './interactive.js';
8
9
  import { usageDbPath } from '../extensions/shared/usage-store.js';
9
10
  import { pricesCachePath } from '../extensions/shared/pricing.js';
10
11
  import { findHoistedPackage, fetchJson, piAgentDir, readTextIfExists, resolveHostBinary, resolveRtkBinary, ruleBodyProblem } from '../extensions/shared/utils.js';
11
12
  import { sqliteScript, statsDbPath } from '../extensions/shared/agent-stats.js';
12
13
  import { OMP_SUBAGENT_MARKER } from '../extensions/shared/session-state.js';
13
- import { PI_PONYTAIL_FILES, PONYTAIL_BUNDLE_FILES, TREE_FILES, isObsoleteTreeFile, treeFileText, treeFilesFor } from './manifest.js';
14
+ import { PI_PONYTAIL_FILES, PONYTAIL_BUNDLE_FILES, TREE_FILES, TREE_PSTACK_FILES, isObsoleteTreeFile, treeFileText, treeFilesFor } from './manifest.js';
14
15
  import { serviceRunning, serviceState } from './doctor-fix.js';
15
16
  /** Bytes at the scale a transcript gap reads in: `5.6M`, not `5571992`. */
16
17
  function shortBytes(bytes) {
@@ -42,8 +43,7 @@ async function runDoctor(recheck = false) {
42
43
  const updaterIndex = path.join(tersioPluginDir, 'extensions', 'updater', 'index.ts');
43
44
  // Probes start concurrently and report in fixed order as data settles; each resolves instead of rejecting.
44
45
  const hosts = detectHosts();
45
- // Every host keeps a copy, and the copy omp loads is the reference itself, so only the hosts that get
46
- // their own copy are compared below. The package root, not the hook's directory, is what the paths hang off.
46
+ // Only hosts with their own copy are compared: omp's copy is the reference. Paths hang off the package root.
47
47
  const ponytailLoads = {
48
48
  pi: { dir: piPonytailPackageDir(piAgentDir()), files: PI_PONYTAIL_FILES },
49
49
  opencode: { dir: path.join(hostExtensionsDir('opencode'), 'ponytail-bundle'), files: PONYTAIL_BUNDLE_FILES },
@@ -70,9 +70,7 @@ async function runDoctor(recheck = false) {
70
70
  const treeOrphan = [];
71
71
  // The tree a host loads: omp reads the plugin package, every other host its own directory.
72
72
  const treeBaseFor = (host) => host.id === 'omp' ? path.join(tersioPluginDir, 'extensions') : hostExtensionsDir(host.id);
73
- // A tree holding at least one file of this build is compared even when the host reads as not
74
- // installed. pi auto-discovers every index.ts it finds, so a half-written tree needs reporting,
75
- // and `detectHosts` reads one as absent: it wants every file or a declared package.
73
+ // A tree holding any file of this build is compared even when the host reads as not installed: pi auto-discovers every index.ts it finds, so a half-written tree needs reporting.
76
74
  const treeHoldsOwnFiles = (host) => treeFilesFor(host.id === 'omp' ? 'opencode' : host.id).some((rel) => existsSync(path.join(treeBaseFor(host), ...rel.split('/'))));
77
75
  for (const host of hosts.filter((h) => h.installed || treeHoldsOwnFiles(h))) {
78
76
  const treeId = host.id === 'omp' ? 'opencode' : host.id;
@@ -103,6 +101,22 @@ async function runDoctor(recheck = false) {
103
101
  }
104
102
  }
105
103
  }
104
+ // A host reading as installed whose tree is short of this build's declared files: the pair loop above cannot see a file that is absent on both sides.
105
+ const unwrittenTreeFiles = async () => {
106
+ const out = [];
107
+ for (const host of hosts.filter((h) => h.installed)) {
108
+ const treeId = host.id === 'omp' ? 'opencode' : host.id;
109
+ const base = treeBaseFor(host);
110
+ for (const rel of treeFilesFor(treeId)) {
111
+ if (existsSync(path.join(base, ...rel.split('/'))))
112
+ continue;
113
+ if (host.id === 'omp' && GENERATED_TREE_FILES[rel] !== undefined)
114
+ continue;
115
+ out.push(`${host.id}/${rel}`);
116
+ }
117
+ }
118
+ return out;
119
+ };
106
120
  const probes = {
107
121
  ompPkgText: readTextIfExists(path.join(ompPackageDir(), 'package.json')),
108
122
  configText: readTextIfExists(path.join(OMP_AGENT_DIR, 'config.yml')),
@@ -110,9 +124,13 @@ async function runDoctor(recheck = false) {
110
124
  rtkBinText: rtkBin ? readTextIfExists(rtkBin) : Promise.resolve(null),
111
125
  cavemanIndexText: readTextIfExists(cavemanIndex),
112
126
  cavemanRuleText: readTextIfExists(cavemanRule('rule.md')),
127
+ cavemanLiteText: readTextIfExists(cavemanRule('rule-lite.md')),
128
+ cavemanMegacaveLiteText: readTextIfExists(cavemanRule('rule-megacave-lite.md')),
129
+ cavemanMegacaveUltraText: readTextIfExists(cavemanRule('rule-megacave-ultra.md')),
113
130
  cavemanUltraText: readTextIfExists(cavemanRule('rule-ultra.md')),
114
131
  cavemanMegacaveText: readTextIfExists(cavemanRule('rule-megacave.md')),
115
132
  pstackRuleText: readTextIfExists(pstackRule),
133
+ pstackTreeFiles: Promise.all(TREE_PSTACK_FILES.map((rel) => readTextIfExists(path.join(tersioPluginDir, "extensions", ...rel.split("/"))))),
116
134
  rtkIndexText: readTextIfExists(rtkIndex),
117
135
  updaterIndexText: readTextIfExists(updaterIndex),
118
136
  rtkMtime: rtkBin ? fs.stat(rtkBin).catch(() => null) : Promise.resolve(null),
@@ -128,7 +146,7 @@ async function runDoctor(recheck = false) {
128
146
  const opencodeServiceProbe = opencodeBin
129
147
  ? Promise.resolve(serviceState())
130
148
  : Promise.resolve(null);
131
- const [[ompPkgText, configText], [cavemanIndexText, rtkIndexText, updaterIndexText, ponytailPkgText], [cavemanRuleText, cavemanUltraText, cavemanMegacaveText, pstackRuleText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, ponytailMtime, pricesMtime], [opencodeService]] = await runInteractivePhase('Checking installation', () => Promise.all([
149
+ const [[ompPkgText, configText], [cavemanIndexText, rtkIndexText, updaterIndexText, ponytailPkgText], [cavemanRuleText, cavemanLiteText, cavemanMegacaveLiteText, cavemanMegacaveUltraText, cavemanUltraText, cavemanMegacaveText, pstackRuleText, pstackTreeFiles, ruleMtime, rtkBinText, rtkMtime, rtkVersion, ponytailMtime, pricesMtime], [opencodeService]] = await runInteractivePhase('Checking installation', () => Promise.all([
132
150
  Promise.all([
133
151
  probes.ompPkgText,
134
152
  probes.configText,
@@ -141,9 +159,13 @@ async function runDoctor(recheck = false) {
141
159
  ]),
142
160
  Promise.all([
143
161
  probes.cavemanRuleText,
162
+ probes.cavemanLiteText,
163
+ probes.cavemanMegacaveLiteText,
164
+ probes.cavemanMegacaveUltraText,
144
165
  probes.cavemanUltraText,
145
166
  probes.cavemanMegacaveText,
146
167
  probes.pstackRuleText,
168
+ probes.pstackTreeFiles,
147
169
  probes.ruleMtime,
148
170
  probes.rtkBinText,
149
171
  probes.rtkMtime,
@@ -201,8 +223,7 @@ async function runDoctor(recheck = false) {
201
223
  const kind = host.via === 'tree' ? 'extensions' : 'package';
202
224
  check(host.label, true, `${kind} ${PACKAGE_NAME}${host.version ? ` ${host.version}` : ''}`);
203
225
  }
204
- // A healthy service adds no row: the host row already names the install with its version, and
205
- // this row earns its place only when the manager's report and the live process disagree.
226
+ // A healthy service adds no row: the host row already names the install, and this row only shows when the manager's report and the live process disagree.
206
227
  if (opencodeService) {
207
228
  const running = serviceRunning(opencodeService.status);
208
229
  if (!running && opencodeService.live) {
@@ -236,7 +257,7 @@ async function runDoctor(recheck = false) {
236
257
  warnLine('omp stats index', indexLag);
237
258
  section('Add-ons');
238
259
  const ruleAge = ruleMtime ? `(updated ${relTime(Date.now() - ruleMtime.mtimeMs)} · ${absDate(ruleMtime.mtimeMs)})` : '';
239
- const cavemanRules = [['rule.md', cavemanRuleText], ['rule-ultra.md', cavemanUltraText], ['rule-megacave.md', cavemanMegacaveText]];
260
+ const cavemanRules = [['rule.md', cavemanRuleText], ['rule-lite.md', cavemanLiteText], ['rule-megacave-lite.md', cavemanMegacaveLiteText], ['rule-megacave-ultra.md', cavemanMegacaveUltraText], ['rule-ultra.md', cavemanUltraText], ['rule-megacave.md', cavemanMegacaveText]];
240
261
  const missingRules = cavemanRules.filter(([, text]) => text === null).map(([name]) => name);
241
262
  check('Caveman rule', missingRules.length === 0, missingRules.length ? `missing: ${missingRules.join(', ')}` : ruleAge);
242
263
  for (const [name, text] of cavemanRules) {
@@ -253,6 +274,16 @@ async function runDoctor(recheck = false) {
253
274
  if (problem)
254
275
  warnLine('pstack rule.md', `unusable body (${problem}) — run: tersio install`);
255
276
  }
277
+ // The index routes to files, so a tree with the rule but no leaves serves a mode that cannot apply.
278
+ const pstackOnDisk = {};
279
+ TREE_PSTACK_FILES.forEach((rel, i) => {
280
+ if (pstackTreeFiles[i] !== null)
281
+ pstackOnDisk[rel] = true;
282
+ });
283
+ const pstackMissing = TREE_PSTACK_FILES.filter((rel) => !pstackOnDisk[rel]).map((rel) => rel.replace(/^pstack\//, ''));
284
+ if (pstackMissing.length > 0) {
285
+ warnLine('pstack tree', `missing ${pstackMissing.length} file${pstackMissing.length === 1 ? '' : 's'}: ${pstackMissing.slice(0, 6).join(', ')}${pstackMissing.length > 6 ? ` +${pstackMissing.length - 6} more` : ''} — run: tersio install`);
286
+ }
256
287
  // A verbatim sentence read out of omp's own prompt, so a re-worded release stops Caveman reaching subagents.
257
288
  const subagentGap = subagentMarkerStatus(ompBinary());
258
289
  if (subagentGap)
@@ -270,6 +301,11 @@ async function runDoctor(recheck = false) {
270
301
  warnLine('Per-host add-on files', `orphaned in ${treeOrphan.length} file${treeOrphan.length === 1 ? '' : 's'}: ${treeOrphan.join(', ')}` +
271
302
  ` — run: tersio doctor --fix extensions`);
272
303
  }
304
+ // A count of pairs compared cannot show a file that is absent from disk and absent from the declaration, so assert every declared file landed.
305
+ const unwritten = await unwrittenTreeFiles();
306
+ if (unwritten.length > 0) {
307
+ check('Add-on tree content', false, `${unwritten.length} declared file${unwritten.length === 1 ? '' : 's'} not written for a host reading as installed: ${unwritten.slice(0, 6).join(', ')}${unwritten.length > 6 ? ` +${unwritten.length - 6} more` : ''} — run: tersio doctor --fix extensions`);
308
+ }
273
309
  const rtkAge = rtkMtime ? `(updated ${relTime(Date.now() - rtkMtime.mtimeMs)} · ${absDate(rtkMtime.mtimeMs)})` : '';
274
310
  check('RTK binary', rtkBin !== null, rtkBinText === null ? 'not found in PATH' : [rtkVersion, rtkAge].filter(Boolean).join(' '));
275
311
  // rtk_run needs a host exec API, which doctor cannot assume.
@@ -336,8 +372,8 @@ function ompBinary() {
336
372
  const managed = path.join(BUN_BIN_DIR, OMP_BIN);
337
373
  return existsSync(managed) ? managed : null;
338
374
  }
339
- // The rule bodies a per-host tree carries. rule.md is the bundled fork; the others track upstream.
340
- const CAVEMAN_BODIES = ['rule.md', 'rule-ultra.md', 'rule-megacave.md'];
375
+ // The rule bodies a per-host tree carries. rule.md and rule-lite.md are bundled forks; the others track upstream.
376
+ const CAVEMAN_BODIES = ['rule.md', 'rule-lite.md', 'rule-megacave-lite.md', 'rule-megacave-ultra.md', 'rule-ultra.md', 'rule-megacave.md'];
341
377
  /** `"installed < latest"` when npm answers, else null: silence means "no row", not "stale". */
342
378
  async function ponytailLatestGap(installedPkgText) {
343
379
  const local = parseJsonObject(installedPkgText)?.version;
package/dist/cli/hosts.js CHANGED
@@ -86,8 +86,7 @@ function detect(agentDir = piAgentDir()) {
86
86
  return HOSTS.map((host) => {
87
87
  const pkgDir = host.id === 'omp' ? ompPackageDir() : host.id === 'pi' ? piPackageDir(agentDir) : null;
88
88
  const tree = hostExtensionsDir(host.id, agentDir);
89
- // A tree wins: it is what the installer writes, and it carries no package.json, so its version is ours.
90
- // OpenCode keeps no package to fall back to, so a present tree is the install; its drifted files get the per-host warning instead.
89
+ // A tree wins: the installer writes it and it carries no package.json, so its version is ours. OpenCode keeps no package to fall back to, so a present tree is the install and drift gets the per-host warning.
91
90
  const via = missingTreeFiles(host.id, agentDir).length === 0 || (host.id === 'opencode' && existsSync(tree))
92
91
  ? 'tree'
93
92
  : pkgDir && existsSync(pkgDir) ? 'package' : null;
@@ -411,9 +411,12 @@ async function stepRtkFilter(extDir, options) {
411
411
  console.log(' RTK filter — install built-in tool output filtering');
412
412
  await copySources(extDir, filesUnder('rtk'), 'rtk/filter.ts', options);
413
413
  }
414
- // rule.md is a deliberate fork of skills/caveman/SKILL.md: a blind sync would drop the mode switch line.
414
+ // rule.md and rule-lite.md are our forks and always ship bundled; the other two track upstream and keep a usable body when the fetch fails.
415
415
  const CAVEMAN_RULES = [
416
416
  ['rule.md', null],
417
+ ['rule-lite.md', null],
418
+ ['rule-megacave-lite.md', null],
419
+ ['rule-megacave-ultra.md', null],
417
420
  ['rule-ultra.md', CAVEMAN_REMOTE_ULTRA],
418
421
  ['rule-megacave.md', CAVEMAN_REMOTE_MEGACAVE],
419
422
  ];
@@ -545,7 +548,8 @@ async function stepOpencode(options) {
545
548
  await fs.mkdir(path.dirname(configPath), { recursive: true });
546
549
  await fs.rm(path.join(HOME, '.config', 'opencode', 'plugins', 'rtk.ts'), { force: true }).catch(() => { });
547
550
  }
548
- await writeIfChanged(configPath, `${JSON.stringify(config, null, 2)}\n`, options);
551
+ // The user's own config, so keep the previous copy beside it.
552
+ await writeIfChanged(configPath, `${JSON.stringify(config, null, 2)}\n`, { ...options, backup: true });
549
553
  }
550
554
  async function resolveProfile(opts = {}) {
551
555
  // Seed from the stored defaults so flag-less runs keep them.
@@ -1007,6 +1011,7 @@ async function runInstall(forcedHost) {
1007
1011
  await capture('shared', () => stepSharedSessionState(extDir, installOptions));
1008
1012
  await capture('rtk session', () => stepRtkSession(extDir, installOptions));
1009
1013
  await capture('rtk filter', () => stepRtkFilter(extDir, installOptions));
1014
+ await capture('timing', () => stepTiming(extDir, installOptions));
1010
1015
  await capture('caveman', () => stepCaveman(extDir, installOptions));
1011
1016
  await capture('combo', () => stepCombo(extDir, installOptions));
1012
1017
  await capture('pstack', () => stepPstack(extDir, installOptions));