p-backlog 0.4.0 → 0.5.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +50 -30
  3. package/README.ru.md +50 -30
  4. package/dist/cli.js +670 -265
  5. package/dist/web/assets/{CodeTab-C87LaJFD.js → CodeTab-D3I5lIJJ.js} +1 -1
  6. package/dist/web/assets/CostTab-8f2AC5ER.js +1 -0
  7. package/dist/web/assets/EffectTab-1jObLK9a.js +1 -0
  8. package/dist/web/assets/Figure-BHBCd7Dt.js +1 -0
  9. package/dist/web/assets/OverviewTab-DjNyxzJq.js +1 -0
  10. package/dist/web/assets/QualityTab-W2NdYBhI.js +1 -0
  11. package/dist/web/assets/StatsPage-rJH3k8Ye.js +1 -0
  12. package/dist/web/assets/StatsTabState-BoimPoKm.js +1 -0
  13. package/dist/web/assets/{StatsTable-BXG0IS8p.js → StatsTable-B0tbx68M.js} +1 -1
  14. package/dist/web/assets/{UnavailableRepos-KiTvsm1X.js → UnavailableRepos-SpC6Dzwu.js} +1 -1
  15. package/dist/web/assets/cx-DDl6tKXq.js +58 -0
  16. package/dist/web/assets/{index-AJ1Qzly1.js → index-C8xqRmo-.js} +2 -2
  17. package/dist/web/assets/start-YlC0KTe5.js +38 -0
  18. package/dist/web/assets/use-grain-series-Bx70IZvb.js +55 -0
  19. package/dist/web/assets/value-dot-B7BsbWBD.js +1 -0
  20. package/dist/web/index.html +1 -1
  21. package/package.json +2 -1
  22. package/skill/backlog/SKILL.md +2 -2
  23. package/skill/backlog-en/SKILL.md +2 -2
  24. package/dist/web/assets/ChartTooltip-DWaDSRan.js +0 -55
  25. package/dist/web/assets/CostTab-D4ByOnHX.js +0 -1
  26. package/dist/web/assets/EffectTab-Dq06ghtZ.js +0 -1
  27. package/dist/web/assets/Figure-BC1eGh3_.js +0 -1
  28. package/dist/web/assets/OverviewTab-BBmohHJR.js +0 -1
  29. package/dist/web/assets/QualityTab-Rb7zRcGH.js +0 -1
  30. package/dist/web/assets/StatsPage-Dtyc1gAc.js +0 -1
  31. package/dist/web/assets/StatsTabState-Br-8FgKd.js +0 -1
  32. package/dist/web/assets/cx-CNNf0q6V.js +0 -58
  33. package/dist/web/assets/rolldown-runtime-hePW80VL.js +0 -1
  34. package/dist/web/assets/start-BGXTwsB0.js +0 -38
  35. package/dist/web/assets/value-dot-W-8PsGpd.js +0 -1
  36. /package/dist/web/assets/{ChartTooltip-tQC8-pw2.css → use-grain-series-tQC8-pw2.css} +0 -0
package/dist/cli.js CHANGED
@@ -127,6 +127,10 @@ function percent(part, total) {
127
127
  // src/core/model/lifecycle.ts
128
128
  var RETENTION_DAYS = 7;
129
129
  var DAY_MS = 24 * 60 * 60 * 1e3;
130
+ var DAYS_PER_WEEK = 7;
131
+ var WEEK_MS = DAYS_PER_WEEK * DAY_MS;
132
+ var STATS_WEEKS = 12;
133
+ var STATS_HISTORY_DAYS = STATS_WEEKS * DAYS_PER_WEEK;
130
134
  var RESOLUTION_STATUS = {
131
135
  fixed: "done",
132
136
  obsolete: "cancelled",
@@ -251,7 +255,7 @@ async function listDir(path, { recursive = false } = {}) {
251
255
  throw error;
252
256
  }
253
257
  }
254
- var REPLACE_RETRY_DELAYS_MS = [10, 20, 40, 80, 160, 320, 640];
258
+ var REPLACE_RETRY_DELAYS_MS = [10, 20, 40, 80, 160, 320, 640, 1e3, 1e3, 1e3, 1e3];
255
259
  var REPLACE_BLOCKED_CODES = ["EPERM", "EACCES", "EBUSY"];
256
260
  async function writeFileAtomic(path, content, mode) {
257
261
  await viaTemporaryFile(path, content, (temporary) => replaceFile(temporary, path), mode);
@@ -382,7 +386,7 @@ async function release({ lock, token }) {
382
386
 
383
387
  // src/core/store/runs.ts
384
388
  var RUNS_FILE = ".runs.jsonl";
385
- var RUNS_KEPT_DAYS = 30;
389
+ var RUNS_KEPT_DAYS = STATS_HISTORY_DAYS;
386
390
  var STALE_RUN_SLACK_DAYS = 1;
387
391
  var FIRST_LINE_BYTES = 4096;
388
392
  var cliRunSchema = z.object({
@@ -555,7 +559,7 @@ var cliEn = {
555
559
  runNotRecorded: (error) => `Could not record the run: ${error}`,
556
560
  runsNotTrimmed: (error) => `Could not trim the run log: ${error}`,
557
561
  settingsFileInvalid: (path) => `${path} could not be parsed, the language for this run was detected automatically. The file was not changed \u2014 fix it manually.`,
558
- optionLabel: { status: "status", category: "category", priority: "priority", language: "language" },
562
+ optionLabel: { status: "status", category: "category", priority: "priority", language: "language", agent: "agent" },
559
563
  skillForeign: (target) => `${target} is a foreign directory, the skill link was not moved`,
560
564
  installSkillLinked: (target, source) => `Skill installed: ${target} \u2192 ${source}`,
561
565
  installSkillKept: (target) => `Skill already installed: ${target}`,
@@ -563,8 +567,26 @@ var cliEn = {
563
567
  installSkillLinkFailed: (target, detail) => `Could not create the symlink ${target} (${detail}).`,
564
568
  installHookExists: (settingsPath) => `Stop hook is already present in ${settingsPath}`,
565
569
  installHookAdded: (settingsPath) => `Stop hook added to ${settingsPath}`,
566
- installSettingsUnreadable: (settingsPath, detail) => `Could not read ${settingsPath} (${detail}), Stop hook not added.`,
567
- installSettingsInvalid: (settingsPath) => `${settingsPath} is not a JSON object, Stop hook not added. Fix the file and try again.`,
570
+ installHookUpdated: (settingsPath) => `Stop hook updated in ${settingsPath}`,
571
+ codexHookApproval: "approve the hook in Codex: /hooks",
572
+ installHookConfigUnreadable: (path, detail) => `Could not read ${path} (${detail}), Stop hook not added.`,
573
+ installHookConfigInvalid: (path) => `${path} is not a JSON object, Stop hook not added. Fix the file and try again.`,
574
+ agentNotFound: (dir) => `not found (${dir})`,
575
+ pluginManages: (plugin) => `the ${plugin} plugin provides the skill and the hook. Remove the manual ones with: backlog setup --remove-manual`,
576
+ pluginLanguageHint: (installed, wanted) => `the skill comes from the ${installed} plugin; for this language switch plugins: /plugin uninstall ${installed}, then /plugin install ${wanted}`,
577
+ removeManualWithService: "--remove-manual cannot be combined with --service: remove the service with backlog service uninstall",
578
+ manualSkillRemoval: {
579
+ removed: (target) => `skill link removed: ${target}`,
580
+ absent: (target) => `no skill link: ${target}`,
581
+ foreign: (target) => `${target} is not a p-backlog link, left as is`
582
+ },
583
+ manualSkillShared: (target, agent) => `skill link kept, ${agent} uses it: ${target}`,
584
+ manualHookRemoval: {
585
+ removed: (path) => `Stop hook removed from ${path}`,
586
+ absent: (path) => `no p-backlog Stop hook in ${path}`
587
+ },
588
+ removeHookConfigUnreadable: (path, detail) => `Could not read ${path} (${detail}), the hook was not removed.`,
589
+ removeHookConfigInvalid: (path) => `${path} is not a JSON object, the hook was not removed. Fix the file and try again.`,
568
590
  serviceInstalled: (file) => `Service installed: ${file}`,
569
591
  serviceLogs: (path) => `Logs: ${path}`,
570
592
  serviceUninstalled: "Service removed",
@@ -659,11 +681,12 @@ var cliEn = {
659
681
  alreadyInStatus: (id, status) => `${id} is already ${status}`,
660
682
  blockedByOpenTasks: (id) => `${id} is blocked by open tasks:`,
661
683
  noTakeableInProject: (id) => `Project ${id} has no tasks that can be taken`,
662
- hookUsage: (stopEvent) => `${stopEvent} (for the Stop hook in Claude Code, reads the event from stdin)`,
684
+ hookUsage: (stopEvent, agents) => `${stopEvent} [--agent ${agents.join("|")}] (for the Stop hook in Claude Code, Codex or Cursor, reads the event from stdin)`,
663
685
  sessionShownReadFailed: (error) => `Could not read the tasks already shown this session: ${error}`,
664
686
  sessionShownWriteFailed: (error) => `Could not remember the tasks shown this session: ${error}`,
665
687
  alertsComputeFailed: (error) => `Could not compute alerts: ${error}`,
666
688
  alertsShownWriteFailed: (error) => `Could not save the shown alerts: ${error}`,
689
+ hookTurnClaimFailed: (error) => `Could not record the turn for the double-hook guard: ${error}`,
667
690
  newUsage: (types, priorities, found) => [
668
691
  `--title <title> [--type ${types}] [--priority ${priorities}] [--tags a,b]`,
669
692
  `--category <category> (required for tasks) [--found ${found}]`,
@@ -713,7 +736,7 @@ var cliRu = {
713
736
  runNotRecorded: (error) => `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C \u0437\u0430\u043F\u0443\u0441\u043A: ${error}`,
714
737
  runsNotTrimmed: (error) => `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0431\u0440\u0435\u0437\u0430\u0442\u044C \u0436\u0443\u0440\u043D\u0430\u043B \u0437\u0430\u043F\u0443\u0441\u043A\u043E\u0432: ${error}`,
715
738
  settingsFileInvalid: (path) => `${path} \u043D\u0435 \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u043D, \u044F\u0437\u044B\u043A \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u0437\u0430\u043F\u0443\u0441\u043A\u0430 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0451\u043D \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438. \u0424\u0430\u0439\u043B \u043D\u0435 \u0438\u0437\u043C\u0435\u043D\u0451\u043D \u2014 \u043F\u043E\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u0435\u0433\u043E \u0432\u0440\u0443\u0447\u043D\u0443\u044E.`,
716
- optionLabel: { status: "\u0441\u0442\u0430\u0442\u0443\u0441", category: "\u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u044F", priority: "\u043F\u0440\u0438\u043E\u0440\u0438\u0442\u0435\u0442", language: "\u044F\u0437\u044B\u043A" },
739
+ optionLabel: { status: "\u0441\u0442\u0430\u0442\u0443\u0441", category: "\u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u044F", priority: "\u043F\u0440\u0438\u043E\u0440\u0438\u0442\u0435\u0442", language: "\u044F\u0437\u044B\u043A", agent: "\u0430\u0433\u0435\u043D\u0442" },
717
740
  skillForeign: (target) => `${target} \u2014 \u0447\u0443\u0436\u043E\u0439 \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u0441\u043A\u0438\u043B\u043B \u043D\u0435 \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u0432\u043B\u0435\u043D`,
718
741
  installSkillLinked: (target, source) => `\u0421\u043A\u0438\u043B\u043B \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D: ${target} \u2192 ${source}`,
719
742
  installSkillKept: (target) => `\u0421\u043A\u0438\u043B\u043B \u0443\u0436\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D: ${target}`,
@@ -721,8 +744,26 @@ var cliRu = {
721
744
  installSkillLinkFailed: (target, detail) => `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0443 ${target} (${detail}).`,
722
745
  installHookExists: (settingsPath) => `\u0425\u0443\u043A Stop \u0443\u0436\u0435 \u0435\u0441\u0442\u044C \u0432 ${settingsPath}`,
723
746
  installHookAdded: (settingsPath) => `\u0425\u0443\u043A Stop \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D \u0432 ${settingsPath}`,
724
- installSettingsUnreadable: (settingsPath, detail) => `${settingsPath} \u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C (${detail}), \u0445\u0443\u043A Stop \u043D\u0435 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D.`,
725
- installSettingsInvalid: (settingsPath) => `${settingsPath} \u2014 \u043D\u0435 \u043E\u0431\u044A\u0435\u043A\u0442 JSON, \u0445\u0443\u043A Stop \u043D\u0435 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D. \u0418\u0441\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u0444\u0430\u0439\u043B \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0435.`,
747
+ installHookUpdated: (settingsPath) => `\u0425\u0443\u043A Stop \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D \u0432 ${settingsPath}`,
748
+ codexHookApproval: "\u043E\u0434\u043E\u0431\u0440\u0438\u0442\u0435 \u0445\u0443\u043A \u0432 Codex: /hooks",
749
+ installHookConfigUnreadable: (path, detail) => `${path} \u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C (${detail}), \u0445\u0443\u043A Stop \u043D\u0435 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D.`,
750
+ installHookConfigInvalid: (path) => `${path} \u2014 \u043D\u0435 \u043E\u0431\u044A\u0435\u043A\u0442 JSON, \u0445\u0443\u043A Stop \u043D\u0435 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D. \u0418\u0441\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u0444\u0430\u0439\u043B \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0435.`,
751
+ agentNotFound: (dir) => `\u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D (${dir})`,
752
+ pluginManages: (plugin) => `\u0441\u043A\u0438\u043B\u043B \u0438 \u0445\u0443\u043A \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u0435\u0442 \u043F\u043B\u0430\u0433\u0438\u043D ${plugin}. \u0420\u0443\u0447\u043D\u044B\u0435 \u043C\u043E\u0436\u043D\u043E \u0443\u0431\u0440\u0430\u0442\u044C: backlog setup --remove-manual`,
753
+ pluginLanguageHint: (installed, wanted) => `\u0441\u043A\u0438\u043B\u043B \u0434\u0430\u0451\u0442 \u043F\u043B\u0430\u0433\u0438\u043D ${installed}; \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u044F\u0437\u044B\u043A\u0430 \u043F\u043E\u0441\u0442\u0430\u0432\u044C\u0442\u0435 \u0434\u0440\u0443\u0433\u043E\u0439: /plugin uninstall ${installed}, \u0437\u0430\u0442\u0435\u043C /plugin install ${wanted}`,
754
+ removeManualWithService: "--remove-manual \u043D\u0435 \u0441\u043E\u0447\u0435\u0442\u0430\u0435\u0442\u0441\u044F \u0441 --service: \u0441\u043B\u0443\u0436\u0431\u0443 \u0441\u043D\u0438\u043C\u0430\u0435\u0442 backlog service uninstall",
755
+ manualSkillRemoval: {
756
+ removed: (target) => `\u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u0441\u043A\u0438\u043B\u043B \u0441\u043D\u044F\u0442\u0430: ${target}`,
757
+ absent: (target) => `\u0441\u0441\u044B\u043B\u043A\u0438 \u043D\u0430 \u0441\u043A\u0438\u043B\u043B \u043D\u0435\u0442: ${target}`,
758
+ foreign: (target) => `${target} \u2014 \u043D\u0435 \u0441\u0441\u044B\u043B\u043A\u0430 p-backlog, \u043D\u0435 \u0442\u0440\u043E\u043D\u0443\u0442`
759
+ },
760
+ manualSkillShared: (target, agent) => `\u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u0441\u043A\u0438\u043B\u043B \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u0430, \u0435\u044E \u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F ${agent}: ${target}`,
761
+ manualHookRemoval: {
762
+ removed: (path) => `\u0445\u0443\u043A Stop \u0441\u043D\u044F\u0442 \u0438\u0437 ${path}`,
763
+ absent: (path) => `\u0445\u0443\u043A\u0430 Stop p-backlog \u0432 ${path} \u043D\u0435\u0442`
764
+ },
765
+ removeHookConfigUnreadable: (path, detail) => `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C ${path} (${detail}), \u0445\u0443\u043A \u043D\u0435 \u0441\u043D\u044F\u0442.`,
766
+ removeHookConfigInvalid: (path) => `${path} \u2014 \u043D\u0435 \u043E\u0431\u044A\u0435\u043A\u0442 JSON, \u0445\u0443\u043A \u043D\u0435 \u0441\u043D\u044F\u0442. \u0418\u0441\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u0444\u0430\u0439\u043B \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0435.`,
726
767
  serviceInstalled: (file) => `\u0421\u043B\u0443\u0436\u0431\u0430 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430: ${file}`,
727
768
  serviceLogs: (path) => `\u041B\u043E\u0433\u0438: ${path}`,
728
769
  serviceUninstalled: "\u0421\u043B\u0443\u0436\u0431\u0430 \u0443\u0434\u0430\u043B\u0435\u043D\u0430",
@@ -829,11 +870,12 @@ var cliRu = {
829
870
  alreadyInStatus: (id, status) => `${id} \u0443\u0436\u0435 \u0432 \u0441\u0442\u0430\u0442\u0443\u0441\u0435 ${status}`,
830
871
  blockedByOpenTasks: (id) => `${id} \u0437\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043D\u0430 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u043C\u0438 \u0437\u0430\u0434\u0430\u0447\u0430\u043C\u0438:`,
831
872
  noTakeableInProject: (id) => `\u0412 \u043F\u0440\u043E\u0435\u043A\u0442\u0435 ${id} \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u043C\u043E\u0436\u043D\u043E \u0432\u0437\u044F\u0442\u044C \u0432 \u0440\u0430\u0431\u043E\u0442\u0443`,
832
- hookUsage: (stopEvent) => `${stopEvent} (\u0434\u043B\u044F \u0445\u0443\u043A\u0430 Stop \u0432 Claude Code, \u0441\u043E\u0431\u044B\u0442\u0438\u0435 \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F \u0438\u0437 stdin)`,
873
+ hookUsage: (stopEvent, agents) => `${stopEvent} [--agent ${agents.join("|")}] (\u0434\u043B\u044F \u0445\u0443\u043A\u0430 Stop \u0432 Claude Code, Codex \u0438\u043B\u0438 Cursor, \u0441\u043E\u0431\u044B\u0442\u0438\u0435 \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F \u0438\u0437 stdin)`,
833
874
  sessionShownReadFailed: (error) => `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0441\u0435\u0441\u0441\u0438\u0438: ${error}`,
834
875
  sessionShownWriteFailed: (error) => `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u043E\u043C\u043D\u0438\u0442\u044C \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0441\u0435\u0441\u0441\u0438\u0438: ${error}`,
835
876
  alertsComputeFailed: (error) => `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u0441\u0447\u0438\u0442\u0430\u0442\u044C \u0442\u0440\u0435\u0432\u043E\u0433\u0438: ${error}`,
836
877
  alertsShownWriteFailed: (error) => `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u043D\u044B\u0435 \u0442\u0440\u0435\u0432\u043E\u0433\u0438: ${error}`,
878
+ hookTurnClaimFailed: (error) => `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043C\u0435\u0442\u0438\u0442\u044C \u0445\u043E\u0434 \u0432 \u0437\u0430\u0449\u0438\u0442\u0435 \u043E\u0442 \u0434\u0432\u043E\u0439\u043D\u043E\u0433\u043E \u0445\u0443\u043A\u0430: ${error}`,
837
879
  newUsage: (types, priorities, found) => [
838
880
  `--title <\u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A> [--type ${types}] [--priority ${priorities}] [--tags a,b]`,
839
881
  `--category <\u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u044F> (\u0434\u043B\u044F \u0437\u0430\u0434\u0430\u0447 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D) [--found ${found}]`,
@@ -1321,9 +1363,6 @@ function consecutivePeriods(starts, lastTo) {
1321
1363
  }
1322
1364
 
1323
1365
  // src/core/stats/weeks.ts
1324
- var STATS_WEEKS = 12;
1325
- var DAYS_PER_WEEK = 7;
1326
- var WEEK_MS = DAYS_PER_WEEK * DAY_MS;
1327
1366
  var MONDAY = 1;
1328
1367
  function weekStarts(now, count3) {
1329
1368
  const monday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - (now.getDay() + DAYS_PER_WEEK - MONDAY) % DAYS_PER_WEEK);
@@ -1335,15 +1374,18 @@ function statsPeriod(now) {
1335
1374
  function weekWindows(now, count3 = STATS_WEEKS) {
1336
1375
  return consecutivePeriods(weekStarts(now, count3), now.getTime());
1337
1376
  }
1338
- function weeklyFlow(histories, now) {
1377
+ function flowOver(periods, histories) {
1339
1378
  const closings = histories.flatMap(closingsOf);
1340
- return weekWindows(now).map((week) => ({
1341
- start: formatLocalIso(new Date(week.from)),
1342
- created: histories.filter((history) => week.contains(history.createdAt)).length,
1343
- closed: closings.filter((closing) => week.contains(closing.at)).length,
1344
- openAtEnd: histories.filter((history) => isOpenAt(history, week.to)).length
1379
+ return periods.map((span) => ({
1380
+ start: formatLocalIso(new Date(span.from)),
1381
+ created: histories.filter((history) => span.contains(history.createdAt)).length,
1382
+ closed: closings.filter((closing) => span.contains(closing.at)).length,
1383
+ openAtEnd: histories.filter((history) => isOpenAt(history, span.to)).length
1345
1384
  }));
1346
1385
  }
1386
+ function weeklyFlow(histories, now) {
1387
+ return flowOver(weekWindows(now), histories);
1388
+ }
1347
1389
 
1348
1390
  // src/core/messages/zod.ts
1349
1391
  var TYPE_WITNESSES = {
@@ -3874,7 +3916,11 @@ async function fixCommitFound(loaded, task, reason, io) {
3874
3916
  }
3875
3917
 
3876
3918
  // src/cli/commands/config.ts
3877
- import { join as join17 } from "node:path";
3919
+ import { join as join18 } from "node:path";
3920
+
3921
+ // src/cli/agents/agent.ts
3922
+ import { stat as stat4 } from "node:fs/promises";
3923
+ import { join as join16 } from "node:path";
3878
3924
 
3879
3925
  // src/core/claude-dir.ts
3880
3926
  import { join as join15 } from "node:path";
@@ -3891,18 +3937,65 @@ function claudeProjectsDir(env, home2) {
3891
3937
  return join15(claudeDir(env, home2), "projects");
3892
3938
  }
3893
3939
 
3940
+ // src/cli/agents/agent.ts
3941
+ var AGENTS = ["claude", "codex", "cursor"];
3942
+ var AGENT_LABELS = { claude: "Claude Code", codex: "Codex", cursor: "Cursor" };
3943
+ function agentHomeDir(agent, { env, home: home2 }) {
3944
+ if (agent === "claude") return claudeDir(env, home2);
3945
+ if (agent === "codex") return env.CODEX_HOME || join16(home2, ".codex");
3946
+ return join16(home2, ".cursor");
3947
+ }
3948
+ function agentSkillsDir(agent, places) {
3949
+ return agent === "claude" ? claudeSkillsDir(places.env, places.home) : join16(places.home, ".agents", "skills");
3950
+ }
3951
+ function legacySkillsDirs(agent, places) {
3952
+ return agent === "claude" ? [] : [join16(agentHomeDir(agent, places), "skills")];
3953
+ }
3954
+ async function detectAgents(places) {
3955
+ const detection = { found: [], missing: [] };
3956
+ for (const agent of AGENTS) {
3957
+ const dir = agentHomeDir(agent, places);
3958
+ const present = agent === "claude" || ((await stat4(dir).catch(() => null))?.isDirectory() ?? false);
3959
+ if (present) detection.found.push(agent);
3960
+ else detection.missing.push({ agent, dir });
3961
+ }
3962
+ return detection;
3963
+ }
3964
+
3965
+ // src/cli/agents/claude-plugin.ts
3966
+ import { z as z8 } from "zod";
3967
+ var PLUGIN_BY_LANGUAGE = { en: "p-backlog", ru: "p-backlog-ru" };
3968
+ var PLUGIN_NAMES = new Set(Object.values(PLUGIN_BY_LANGUAGE));
3969
+ var MARKETPLACE_SEPARATOR = "@";
3970
+ var enabledPluginsSchema = z8.object({ enabledPlugins: z8.record(z8.string(), z8.unknown()).optional() });
3971
+ async function agentPlugin(agent, { env, home: home2 }) {
3972
+ if (agent !== "claude") return null;
3973
+ const settings = await readJsonFile(claudeSettingsPath(env, home2), enabledPluginsSchema);
3974
+ const enabled = Object.entries(settings?.enabledPlugins ?? {}).find(([id, on]) => on === true && PLUGIN_NAMES.has(pluginName(id)));
3975
+ return enabled?.[0] ?? null;
3976
+ }
3977
+ function pluginToSwitchTo(pluginId, language) {
3978
+ const wanted = PLUGIN_BY_LANGUAGE[language];
3979
+ if (pluginName(pluginId) === wanted) return null;
3980
+ const marketplace = pluginId.slice(pluginId.indexOf(MARKETPLACE_SEPARATOR) + 1);
3981
+ return `${wanted}${MARKETPLACE_SEPARATOR}${marketplace}`;
3982
+ }
3983
+ function pluginName(pluginId) {
3984
+ return pluginId.split(MARKETPLACE_SEPARATOR)[0] ?? pluginId;
3985
+ }
3986
+
3894
3987
  // src/cli/skill-link.ts
3895
3988
  import { lstat, mkdir as mkdir4, readlink, symlink, unlink } from "node:fs/promises";
3896
- import { basename as basename8, dirname as dirname5, join as join16, resolve as resolve3 } from "node:path";
3897
- import { z as z8 } from "zod";
3989
+ import { basename as basename8, dirname as dirname5, join as join17, resolve as resolve3 } from "node:path";
3990
+ import { z as z9 } from "zod";
3898
3991
  var SKILL_VARIANTS = /* @__PURE__ */ new Set(["backlog", "backlog-en"]);
3899
- var packageManifestSchema = z8.object({ name: z8.string() });
3992
+ var packageManifestSchema = z9.object({ name: z9.string() });
3900
3993
  function skillSourceDir(packageRoot2, language) {
3901
- return join16(packageRoot2, "skill", language === "ru" ? "backlog" : "backlog-en");
3994
+ return join17(packageRoot2, "skill", language === "ru" ? "backlog" : "backlog-en");
3902
3995
  }
3903
3996
  async function isPBacklogSkill(path) {
3904
3997
  if (!SKILL_VARIANTS.has(basename8(path)) || basename8(dirname5(path)) !== "skill") return false;
3905
- const manifest = await readTextOrNull(join16(dirname5(dirname5(path)), "package.json"));
3998
+ const manifest = await readTextOrNull(join17(dirname5(dirname5(path)), "package.json"));
3906
3999
  if (manifest !== null) return parseJson(manifest, packageManifestSchema)?.name === "p-backlog";
3907
4000
  return !await pathExists(path);
3908
4001
  }
@@ -3910,7 +4003,7 @@ async function pathExists(path) {
3910
4003
  return await lstat(path).catch(() => null) !== null;
3911
4004
  }
3912
4005
  async function linkSkillFor(language, { skillsDir, packageRoot: packageRoot2, platform }) {
3913
- const target = join16(skillsDir, "backlog");
4006
+ const target = join17(skillsDir, "backlog");
3914
4007
  const source = skillSourceDir(packageRoot2, language);
3915
4008
  const existing = await lstat(target).catch(() => null);
3916
4009
  if (existing !== null && !existing.isSymbolicLink()) return "foreign";
@@ -3924,6 +4017,18 @@ async function linkSkillFor(language, { skillsDir, packageRoot: packageRoot2, pl
3924
4017
  await symlink(source, target, platform === "win32" ? "junction" : "dir");
3925
4018
  return "linked";
3926
4019
  }
4020
+ async function relinkExistingSkill(language, options) {
4021
+ if (!await pathExists(join17(options.skillsDir, "backlog"))) return "absent";
4022
+ return linkSkillFor(language, options);
4023
+ }
4024
+ async function unlinkOurSkill(skillsDir) {
4025
+ const target = join17(skillsDir, "backlog");
4026
+ const existing = await lstat(target).catch(() => null);
4027
+ if (existing === null) return "absent";
4028
+ if (!existing.isSymbolicLink() || !await isPBacklogSkill(resolve3(skillsDir, await readlink(target)))) return "foreign";
4029
+ await unlink(target);
4030
+ return "removed";
4031
+ }
3927
4032
 
3928
4033
  // src/cli/commands/config.ts
3929
4034
  var configCommand = {
@@ -3947,11 +4052,29 @@ async function runLanguage(positionals, io) {
3947
4052
  const language = parseChoice(io.language, value, LANGUAGES, cliMessages(io.language).optionLabel.language);
3948
4053
  await writeSettings(io.backlogRoot, { language });
3949
4054
  io.print(`${io.language} \u2192 ${language}`);
3950
- const skillsDir = claudeSkillsDir(io.env, io.home);
3951
- const result = await linkSkillFor(language, { skillsDir, packageRoot: io.packageRoot, platform: io.platform });
3952
- if (result === "foreign") io.warn(cliMessages(language).skillForeign(join17(skillsDir, "backlog")));
4055
+ const { found } = await detectAgents(io);
4056
+ for (const agent of found) await relinkSkill(agent, language, io);
3953
4057
  return EXIT.ok;
3954
4058
  }
4059
+ async function relinkSkill(agent, language, io) {
4060
+ const messages = cliMessages(language);
4061
+ const label = AGENT_LABELS[agent];
4062
+ const plugin = await agentPlugin(agent, io);
4063
+ if (plugin !== null) {
4064
+ const wanted = pluginToSwitchTo(plugin, language);
4065
+ if (wanted !== null) io.print(`${label}: ${messages.pluginLanguageHint(plugin, wanted)}`);
4066
+ return;
4067
+ }
4068
+ const skillsDir = agentSkillsDir(agent, io);
4069
+ const target = join18(skillsDir, "backlog");
4070
+ const options = { skillsDir, packageRoot: io.packageRoot, platform: io.platform };
4071
+ try {
4072
+ const result = agent === "claude" ? await linkSkillFor(language, options) : await relinkExistingSkill(language, options);
4073
+ if (result === "foreign") io.warn(`${label}: ${messages.skillForeign(target)}`);
4074
+ } catch (error) {
4075
+ io.warn(`${label}: ${messages.installSkillLinkFailed(target, errorCodeOrText(error))}`);
4076
+ }
4077
+ }
3955
4078
 
3956
4079
  // src/cli/apply-all.ts
3957
4080
  async function applyAll(items, apply) {
@@ -4004,31 +4127,47 @@ function epicWord(cli, epic) {
4004
4127
 
4005
4128
  // src/cli/commands/hook.ts
4006
4129
  import { dirname as dirname6 } from "node:path";
4007
- import { z as z11 } from "zod";
4130
+
4131
+ // src/core/store/hook-turns.ts
4132
+ import { join as join19 } from "node:path";
4133
+ import { z as z10 } from "zod";
4134
+ var HOOK_TURNS_FILE = ".hook-turns.json";
4135
+ var CLAIM_WINDOW_MS = 6e4;
4136
+ var hookTurnsSchema = z10.record(z10.string(), z10.iso.datetime({ offset: true }));
4137
+ async function claimHookTurn(backlogRoot2, turnKey, now) {
4138
+ const path = join19(backlogRoot2, HOOK_TURNS_FILE);
4139
+ return withFileLock(path, async () => {
4140
+ const turns = await readJsonFile(path, hookTurnsSchema) ?? {};
4141
+ const claimed = Object.entries(turns).filter(([, at]) => now.getTime() - Date.parse(at) < CLAIM_WINDOW_MS);
4142
+ if (claimed.some(([key]) => key === turnKey)) return false;
4143
+ await writeJsonFile(path, { ...Object.fromEntries(claimed), [turnKey]: now.toISOString() });
4144
+ return true;
4145
+ });
4146
+ }
4008
4147
 
4009
4148
  // src/core/store/signals-shown.ts
4010
- import { join as join18 } from "node:path";
4011
- import { z as z9 } from "zod";
4149
+ import { join as join20 } from "node:path";
4150
+ import { z as z11 } from "zod";
4012
4151
  var SIGNALS_SHOWN_FILE = ".signals-shown.json";
4013
- var shownSchema = z9.record(z9.string(), z9.string());
4152
+ var shownSchema = z11.record(z11.string(), z11.string());
4014
4153
  async function readSignalsShown(projectDir2) {
4015
- return await readJsonFile(join18(projectDir2, SIGNALS_SHOWN_FILE), shownSchema) ?? {};
4154
+ return await readJsonFile(join20(projectDir2, SIGNALS_SHOWN_FILE), shownSchema) ?? {};
4016
4155
  }
4017
4156
  async function writeSignalsShown(projectDir2, shown) {
4018
- await writeJsonFile(join18(projectDir2, SIGNALS_SHOWN_FILE), shown);
4157
+ await writeJsonFile(join20(projectDir2, SIGNALS_SHOWN_FILE), shown);
4019
4158
  }
4020
4159
 
4021
4160
  // src/core/store/session-shown.ts
4022
- import { join as join19 } from "node:path";
4023
- import { z as z10 } from "zod";
4161
+ import { join as join21 } from "node:path";
4162
+ import { z as z12 } from "zod";
4024
4163
  var SESSION_SHOWN_FILE = ".candidates-shown.json";
4025
4164
  var SESSION_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
4026
- var sessionsShownSchema = z10.record(z10.string(), z10.object({ tasks: z10.array(z10.string()), at: z10.iso.datetime({ offset: true }) }));
4165
+ var sessionsShownSchema = z12.record(z12.string(), z12.object({ tasks: z12.array(z12.string()), at: z12.iso.datetime({ offset: true }) }));
4027
4166
  async function readSessionShown(projectDir2, session) {
4028
4167
  return (await readSessions(projectDir2))[session]?.tasks ?? [];
4029
4168
  }
4030
4169
  async function rememberSessionShown(projectDir2, session, taskIds, now) {
4031
- const path = join19(projectDir2, SESSION_SHOWN_FILE);
4170
+ const path = join21(projectDir2, SESSION_SHOWN_FILE);
4032
4171
  await withFileLock(path, async () => {
4033
4172
  const sessions = await readSessions(projectDir2);
4034
4173
  const recent = Object.entries(sessions).filter(([, shown]) => now.getTime() - Date.parse(shown.at) < SESSION_RETENTION_MS);
@@ -4037,7 +4176,7 @@ async function rememberSessionShown(projectDir2, session, taskIds, now) {
4037
4176
  });
4038
4177
  }
4039
4178
  async function readSessions(projectDir2) {
4040
- return await readJsonFile(join19(projectDir2, SESSION_SHOWN_FILE), sessionsShownSchema) ?? {};
4179
+ return await readJsonFile(join21(projectDir2, SESSION_SHOWN_FILE), sessionsShownSchema) ?? {};
4041
4180
  }
4042
4181
 
4043
4182
  // src/core/stats/numbers.ts
@@ -4163,6 +4302,21 @@ function isWorkStatus(status) {
4163
4302
  return status === "in-progress" || status === "blocked";
4164
4303
  }
4165
4304
 
4305
+ // src/core/stats/days.ts
4306
+ var STATS_DAYS = 30;
4307
+ function dayRange(now, count3) {
4308
+ return dayStarts(now, count3).map(formatLocalDay);
4309
+ }
4310
+ function dailyFlow(histories, now) {
4311
+ return flowOver(dayWindows(now), histories);
4312
+ }
4313
+ function dayWindows(now, count3 = STATS_DAYS) {
4314
+ return consecutivePeriods(dayStarts(now, count3), now.getTime());
4315
+ }
4316
+ function dayStarts(now, count3) {
4317
+ return Array.from({ length: count3 }, (_, index) => new Date(now.getFullYear(), now.getMonth(), now.getDate() - (count3 - 1 - index)));
4318
+ }
4319
+
4166
4320
  // src/core/stats/quality/accuracy.ts
4167
4321
  function accuracy(histories, period2) {
4168
4322
  const episodes = histories.flatMap(
@@ -4175,15 +4329,21 @@ function accuracy(histories, period2) {
4175
4329
  });
4176
4330
  return [...byEvidence, { evidence: "total", ...outcomeCounts(episodes) }];
4177
4331
  }
4178
- function accuracyWeeks(histories, now) {
4179
- return weekWindows(now).map((week) => {
4332
+ function accuracyOver(periods, histories) {
4333
+ return periods.map((span) => {
4180
4334
  const decided = histories.flatMap(
4181
- (history) => history.candidates.filter((candidate) => week.contains(candidate.at) && candidate.evidence !== "no-source").map((candidate) => outcomeAfter(history, candidate.at)).filter((outcome) => outcome !== "open")
4335
+ (history) => history.candidates.filter((candidate) => span.contains(candidate.at) && candidate.evidence !== "no-source").map((candidate) => outcomeAfter(history, candidate.at)).filter((outcome) => outcome !== "open")
4182
4336
  );
4183
4337
  const closed = decided.filter((outcome) => outcome === "closed").length;
4184
- return { start: formatLocalIso(new Date(week.from)), decided: decided.length, precision: decided.length === 0 ? null : closed / decided.length };
4338
+ return { start: formatLocalIso(new Date(span.from)), decided: decided.length, precision: decided.length === 0 ? null : closed / decided.length };
4185
4339
  });
4186
4340
  }
4341
+ function accuracyWeeks(histories, now) {
4342
+ return accuracyOver(weekWindows(now), histories);
4343
+ }
4344
+ function accuracyDays(histories, now) {
4345
+ return accuracyOver(dayWindows(now), histories);
4346
+ }
4187
4347
  function methodAccuracy(histories, period2) {
4188
4348
  return splitAccuracy(histories, period2, { evidence: "source-changed", keys: RECORDED_METHODS, keyOf: (candidate) => candidate.method });
4189
4349
  }
@@ -4315,6 +4475,56 @@ function markShown(shown, signals, today) {
4315
4475
  return { ...shown, ...Object.fromEntries(signals.map((signal3) => [signal3.kind, today])) };
4316
4476
  }
4317
4477
 
4478
+ // src/cli/agents/stop-event.ts
4479
+ import { createHash as createHash4 } from "node:crypto";
4480
+ import { z as z13 } from "zod";
4481
+ var STOP_PROTOCOL = { claude: "claude", codex: "claude", cursor: "cursor" };
4482
+ var TURN_DIGEST_LENGTH = 16;
4483
+ var CURSOR_COMPLETED = "completed";
4484
+ var claudeEventSchema = z13.object({
4485
+ cwd: z13.string(),
4486
+ session_id: z13.string().optional(),
4487
+ turn_id: z13.string().optional(),
4488
+ stop_hook_active: z13.boolean().optional(),
4489
+ last_assistant_message: z13.string().nullish()
4490
+ });
4491
+ var cursorEventSchema = z13.object({
4492
+ workspace_roots: z13.tuple([z13.string()], z13.string()),
4493
+ conversation_id: z13.string().optional(),
4494
+ generation_id: z13.string().optional(),
4495
+ status: z13.string().optional(),
4496
+ loop_count: z13.number().optional()
4497
+ });
4498
+ function parseStopEvent(agent, text) {
4499
+ return STOP_PROTOCOL[agent] === "cursor" ? parseCursorEvent(text) : parseClaudeEvent(text);
4500
+ }
4501
+ function carriesSystemMessage(agent) {
4502
+ return STOP_PROTOCOL[agent] === "claude";
4503
+ }
4504
+ function formatStopAnswer(agent, answer) {
4505
+ if (STOP_PROTOCOL[agent] === "cursor") return answer.reason === null ? null : JSON.stringify({ followup_message: answer.reason });
4506
+ const response = {
4507
+ ...answer.reason !== null ? { decision: "block", reason: answer.reason } : {},
4508
+ ...answer.systemMessage !== null ? { systemMessage: answer.systemMessage } : {}
4509
+ };
4510
+ return Object.keys(response).length > 0 ? JSON.stringify(response) : null;
4511
+ }
4512
+ function parseClaudeEvent(text) {
4513
+ const event = parseJson(text, claudeEventSchema);
4514
+ if (event === null) return null;
4515
+ return { cwd: event.cwd, session: event.session_id, turn: event.turn_id ?? digest(event.last_assistant_message), skip: event.stop_hook_active === true };
4516
+ }
4517
+ function parseCursorEvent(text) {
4518
+ const event = parseJson(text, cursorEventSchema);
4519
+ if (event === null) return null;
4520
+ const followsOurFollowUp = (event.loop_count ?? 0) > 0;
4521
+ const interrupted = (event.status ?? CURSOR_COMPLETED) !== CURSOR_COMPLETED;
4522
+ return { cwd: event.workspace_roots[0], session: event.conversation_id, turn: event.generation_id, skip: followsOurFollowUp || interrupted };
4523
+ }
4524
+ function digest(text) {
4525
+ return text ? createHash4("sha256").update(text).digest("hex").slice(0, TURN_DIGEST_LENGTH) : void 0;
4526
+ }
4527
+
4318
4528
  // src/cli/stop-reason.ts
4319
4529
  var STOP_REASON_LIMIT = 500;
4320
4530
  function stopReason(language, projectId, candidates) {
@@ -4343,37 +4553,50 @@ function briefEvidence(cli, candidate) {
4343
4553
  }
4344
4554
 
4345
4555
  // src/cli/commands/hook.ts
4346
- var stopEventSchema = z11.object({ cwd: z11.string(), session_id: z11.string().optional(), stop_hook_active: z11.boolean().optional() });
4556
+ var DEFAULT_AGENT = "claude";
4347
4557
  var hookCommand = {
4348
4558
  name: "hook",
4349
- usage: (language) => [cliMessages(language).hookUsage(HOOK_STOP_EVENT)],
4559
+ usage: (language) => [cliMessages(language).hookUsage(HOOK_STOP_EVENT, AGENTS)],
4350
4560
  run: runHook,
4351
4561
  failureExit: EXIT.ok
4352
4562
  };
4353
4563
  async function runHook(args, io) {
4354
- if (args.length !== 1 || args[0] !== HOOK_STOP_EVENT) throw usageError(hookCommand, io.language);
4355
- const event = parseJson(await io.readStdin(), stopEventSchema);
4356
- if (event === null || event.stop_hook_active === true) return EXIT.ok;
4564
+ const { values, positionals } = parseCommandArgs(io.language, args, { agent: { type: "string" } });
4565
+ if (positionals.length !== 1 || positionals[0] !== HOOK_STOP_EVENT) throw usageError(hookCommand, io.language);
4566
+ const agent = values.agent === void 0 ? DEFAULT_AGENT : parseChoice(io.language, values.agent, AGENTS, cliMessages(io.language).optionLabel.agent);
4567
+ const event = parseStopEvent(agent, await io.readStdin());
4568
+ if (event === null || event.skip) return EXIT.ok;
4357
4569
  const loaded = await loadBacklog(io.backlogRoot);
4358
4570
  const project = findProjectForDir(loaded.projects, event.cwd, io.home);
4359
4571
  if (!project) return EXIT.ok;
4572
+ if (!await isFirstHookOfTurn(agent, event, io)) return EXIT.ok;
4360
4573
  const messages = coreMessages(io.language);
4361
4574
  const { candidates } = await checkBacklog(io.backlogRoot, loaded, { projectIds: [project.id], mode: "changed", now: io.now(), home: io.home, messages, workingDir: event.cwd });
4362
4575
  const lowPriority = new Set(loaded.tasks.filter((task) => task.priority === "low").map((task) => task.id));
4363
4576
  const worthTelling = candidates.filter((candidate) => !lowPriority.has(candidate.task.id));
4364
4577
  const lowCount = candidates.length - worthTelling.length;
4365
- const session = await sessionMemory(project, event.session_id, io);
4578
+ const session = await sessionMemory(project, event.session, io);
4366
4579
  const blocking = worthTelling.filter((candidate) => !session.told.has(candidate.task.id));
4367
- const signals = await freshSignals(project, loaded.tasks.filter((task) => task.projectId === project.id), lowChangedSignals(lowCount), io);
4368
- const response = {
4369
- ...blocking.length > 0 ? { decision: "block", reason: stopReason(io.language, project.id, blocking) } : {},
4370
- ...signals.fresh.length > 0 ? { systemMessage: hookMessage(io.language, project.id, signals.fresh.map((signal3) => messages.signal(signal3)).join("; ")) } : {}
4371
- };
4372
- if (Object.keys(response).length > 0) io.print(JSON.stringify(response));
4580
+ const signals = carriesSystemMessage(agent) ? await freshSignals(project, loaded.tasks.filter((task) => task.projectId === project.id), lowChangedSignals(lowCount), io) : NO_SIGNALS;
4581
+ const answer = formatStopAnswer(agent, {
4582
+ reason: blocking.length > 0 ? stopReason(io.language, project.id, blocking) : null,
4583
+ systemMessage: signals.fresh.length > 0 ? hookMessage(io.language, project.id, signals.fresh.map((signal3) => messages.signal(signal3)).join("; ")) : null
4584
+ });
4585
+ if (answer !== null) io.print(answer);
4373
4586
  await signals.remember();
4374
4587
  await session.remember(blocking.map((candidate) => candidate.task.id));
4375
4588
  return EXIT.ok;
4376
4589
  }
4590
+ async function isFirstHookOfTurn(agent, event, io) {
4591
+ if (event.session === void 0 || event.turn === void 0) return true;
4592
+ try {
4593
+ return await claimHookTurn(io.backlogRoot, `${agent}:${event.session}:${event.turn}`, io.now());
4594
+ } catch (error) {
4595
+ if (error instanceof FileBusyError) return false;
4596
+ io.warn(cliMessages(io.language).hookTurnClaimFailed(errorText(error)));
4597
+ return true;
4598
+ }
4599
+ }
4377
4600
  async function sessionMemory(project, session, io) {
4378
4601
  if (session === void 0) return { told: /* @__PURE__ */ new Set(), remember: () => Promise.resolve() };
4379
4602
  const projectDir2 = dirname6(project.path);
@@ -4389,6 +4612,7 @@ async function sessionMemory(project, session, io) {
4389
4612
  return { told: /* @__PURE__ */ new Set(), remember: () => Promise.resolve() };
4390
4613
  }
4391
4614
  }
4615
+ var NO_SIGNALS = { fresh: [], remember: () => Promise.resolve() };
4392
4616
  function lowChangedSignals(count3) {
4393
4617
  if (count3 === 0) return [];
4394
4618
  return [{ kind: "low-changed", params: { count: count3 } }];
@@ -4788,10 +5012,10 @@ function listenFailure(error, port, messages) {
4788
5012
  // src/server/start.ts
4789
5013
  import { serve } from "@hono/node-server";
4790
5014
  import { mkdir as mkdir6, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
4791
- import { join as join25 } from "node:path";
5015
+ import { join as join27 } from "node:path";
4792
5016
 
4793
5017
  // src/core/store/sweep.ts
4794
- import { dirname as dirname7, join as join20 } from "node:path";
5018
+ import { dirname as dirname7, join as join22 } from "node:path";
4795
5019
  async function sweepClosed(root, now, messages) {
4796
5020
  const initial = await loadBacklog(root);
4797
5021
  const reopening = await reopenEpicsWithOpenTasks(initial, now);
@@ -4814,7 +5038,7 @@ async function sweepClosed(root, now, messages) {
4814
5038
  }
4815
5039
  async function removeAbandonedTemporaries(root, now) {
4816
5040
  const cutoff = new Date(now.getTime() - DAY_MS);
4817
- const projectDirs = (await listDir(root)).filter((entry) => entry.isDirectory()).map((entry) => join20(root, entry.name));
5041
+ const projectDirs = (await listDir(root)).filter((entry) => entry.isDirectory()).map((entry) => join22(root, entry.name));
4818
5042
  for (const dir of [root, ...projectDirs]) await removeTemporariesBefore(dir, cutoff);
4819
5043
  }
4820
5044
  async function removeWithReferences(tasks, removable, now) {
@@ -4938,49 +5162,49 @@ async function reserveNumbers(projects, expired) {
4938
5162
  import { serveStatic } from "@hono/node-server/serve-static";
4939
5163
  import { Hono as Hono3 } from "hono";
4940
5164
  import { secureHeaders } from "hono/secure-headers";
4941
- import { join as join22 } from "node:path";
5165
+ import { join as join24 } from "node:path";
4942
5166
 
4943
5167
  // src/server/api.ts
4944
5168
  import { Hono as Hono2 } from "hono";
4945
5169
  import { streamSSE } from "hono/streaming";
4946
5170
 
4947
5171
  // src/core/api/contract.ts
4948
- import { z as z12 } from "zod";
4949
- var idList = z12.array(taskIdSchema);
4950
- var taskChangesSchema = z12.strictObject({
4951
- title: z12.string().optional(),
4952
- type: z12.enum(TASK_TYPES).optional(),
4953
- status: z12.enum(TASK_STATUSES).optional(),
4954
- priority: z12.enum(PRIORITIES).optional(),
4955
- tags: z12.array(z12.string()).optional(),
5172
+ import { z as z14 } from "zod";
5173
+ var idList = z14.array(taskIdSchema);
5174
+ var taskChangesSchema = z14.strictObject({
5175
+ title: z14.string().optional(),
5176
+ type: z14.enum(TASK_TYPES).optional(),
5177
+ status: z14.enum(TASK_STATUSES).optional(),
5178
+ priority: z14.enum(PRIORITIES).optional(),
5179
+ tags: z14.array(z14.string()).optional(),
4956
5180
  epic: taskIdSchema.nullable().optional(),
4957
5181
  blockedBy: idList.optional(),
4958
5182
  related: idList.optional(),
4959
- body: z12.string().optional(),
4960
- category: z12.enum(TASK_CATEGORIES).nullable().optional()
5183
+ body: z14.string().optional(),
5184
+ category: z14.enum(TASK_CATEGORIES).nullable().optional()
4961
5185
  });
4962
- var projectActiveSchema = z12.strictObject({ active: z12.boolean() });
4963
- var projectDeleteSchema = z12.strictObject({ confirm: z12.string() });
4964
- var settingsRequestSchema = z12.strictObject(settingsSchema.shape);
4965
- var updateTaskRequestSchema = z12.strictObject({
4966
- version: z12.string().min(1),
5186
+ var projectActiveSchema = z14.strictObject({ active: z14.boolean() });
5187
+ var projectDeleteSchema = z14.strictObject({ confirm: z14.string() });
5188
+ var settingsRequestSchema = z14.strictObject(settingsSchema.shape);
5189
+ var updateTaskRequestSchema = z14.strictObject({
5190
+ version: z14.string().min(1),
4967
5191
  changes: taskChangesSchema
4968
5192
  });
4969
- var batchPreviousSchema = z12.strictObject({
4970
- status: z12.enum(TASK_STATUSES),
4971
- priority: z12.enum(PRIORITIES),
5193
+ var batchPreviousSchema = z14.strictObject({
5194
+ status: z14.enum(TASK_STATUSES),
5195
+ priority: z14.enum(PRIORITIES),
4972
5196
  epic: taskIdSchema.nullable(),
4973
- resolution: z12.enum(RESOLUTIONS).nullable(),
4974
- reason: z12.string().nullable()
5197
+ resolution: z14.enum(RESOLUTIONS).nullable(),
5198
+ reason: z14.string().nullable()
4975
5199
  });
4976
5200
  var BATCH_TASKS_LIMIT = 500;
4977
- var batchRequestSchema = z12.strictObject({
4978
- tasks: z12.array(z12.strictObject({ id: taskIdSchema, version: z12.string().min(1) })).min(1).max(BATCH_TASKS_LIMIT).refine((tasks) => new Set(tasks.map((task) => task.id)).size === tasks.length),
4979
- action: z12.discriminatedUnion("kind", [
4980
- z12.strictObject({ kind: z12.literal("close"), reason: z12.string().transform((reason) => reason.replace(/\s*\n\s*/g, " ").trim()).pipe(z12.string().min(1)) }),
4981
- z12.strictObject({ kind: z12.literal("priority"), priority: z12.enum(PRIORITIES) }),
4982
- z12.strictObject({ kind: z12.literal("epic"), epic: taskIdSchema.nullable() }),
4983
- z12.strictObject({ kind: z12.literal("restore"), changes: z12.record(taskIdSchema, batchPreviousSchema) })
5201
+ var batchRequestSchema = z14.strictObject({
5202
+ tasks: z14.array(z14.strictObject({ id: taskIdSchema, version: z14.string().min(1) })).min(1).max(BATCH_TASKS_LIMIT).refine((tasks) => new Set(tasks.map((task) => task.id)).size === tasks.length),
5203
+ action: z14.discriminatedUnion("kind", [
5204
+ z14.strictObject({ kind: z14.literal("close"), reason: z14.string().transform((reason) => reason.replace(/\s*\n\s*/g, " ").trim()).pipe(z14.string().min(1)) }),
5205
+ z14.strictObject({ kind: z14.literal("priority"), priority: z14.enum(PRIORITIES) }),
5206
+ z14.strictObject({ kind: z14.literal("epic"), epic: taskIdSchema.nullable() }),
5207
+ z14.strictObject({ kind: z14.literal("restore"), changes: z14.record(taskIdSchema, batchPreviousSchema) })
4984
5208
  ])
4985
5209
  });
4986
5210
 
@@ -5085,7 +5309,7 @@ function createReportCache({ ttlMs, now }) {
5085
5309
 
5086
5310
  // src/server/revisions.ts
5087
5311
  import { randomUUID as randomUUID3 } from "node:crypto";
5088
- import { stat as stat4 } from "node:fs/promises";
5312
+ import { stat as stat5 } from "node:fs/promises";
5089
5313
  import { basename as basename9 } from "node:path";
5090
5314
  function createRevisions(boot = randomUUID3()) {
5091
5315
  let seq = 0;
@@ -5117,7 +5341,7 @@ function createRevisions(boot = randomUUID3()) {
5117
5341
  };
5118
5342
  }
5119
5343
  async function markOf(path) {
5120
- const [text, stats] = await Promise.all([readTextOrNull(path), stat4(path).catch(() => null)]);
5344
+ const [text, stats] = await Promise.all([readTextOrNull(path), stat5(path).catch(() => null)]);
5121
5345
  return text === null || stats === null ? null : { version: contentVersion(text), mtimeMs: stats.mtimeMs };
5122
5346
  }
5123
5347
 
@@ -5125,26 +5349,26 @@ async function markOf(path) {
5125
5349
  import { Hono } from "hono";
5126
5350
 
5127
5351
  // src/core/code/code-cache.ts
5128
- import { join as join21 } from "node:path";
5129
- import { z as z13 } from "zod";
5352
+ import { join as join23 } from "node:path";
5353
+ import { z as z15 } from "zod";
5130
5354
  var CODE_CACHE_FILE = ".code-cache.json";
5131
5355
  var CODE_CACHE_VERSION = 1;
5132
- var repoCodeSchema = z13.object({
5133
- commits: z13.array(z13.array(z13.string())),
5134
- lines: z13.array(z13.object({ path: z13.string(), lines: z13.number() })),
5135
- units: z13.array(z13.object({ date: z13.string(), lines: z13.number() }))
5356
+ var repoCodeSchema = z15.object({
5357
+ commits: z15.array(z15.array(z15.string())),
5358
+ lines: z15.array(z15.object({ path: z15.string(), lines: z15.number() })),
5359
+ units: z15.array(z15.object({ date: z15.string(), lines: z15.number() }))
5136
5360
  });
5137
- var fixCommitSchema = z13.object({ date: z13.string(), landedAt: z13.string().optional(), byAgent: z13.boolean(), lines: z13.number(), testLines: z13.number() });
5138
- var snapshotSchema = z13.object({
5139
- version: z13.literal(CODE_CACHE_VERSION),
5140
- repos: z13.record(z13.string(), z13.object({ key: z13.string(), code: repoCodeSchema })),
5141
- fixes: z13.record(z13.string(), fixCommitSchema)
5361
+ var fixCommitSchema = z15.object({ date: z15.string(), landedAt: z15.string().optional(), byAgent: z15.boolean(), lines: z15.number(), testLines: z15.number() });
5362
+ var snapshotSchema = z15.object({
5363
+ version: z15.literal(CODE_CACHE_VERSION),
5364
+ repos: z15.record(z15.string(), z15.object({ key: z15.string(), code: repoCodeSchema })),
5365
+ fixes: z15.record(z15.string(), fixCommitSchema)
5142
5366
  });
5143
5367
  function emptyCodeCache() {
5144
5368
  return { repos: {}, fixes: {} };
5145
5369
  }
5146
5370
  function createCodeCacheFile(root) {
5147
- const path = join21(root, CODE_CACHE_FILE);
5371
+ const path = join23(root, CODE_CACHE_FILE);
5148
5372
  return {
5149
5373
  read: async () => {
5150
5374
  const snapshot = await readJsonFile(path, snapshotSchema);
@@ -5541,13 +5765,13 @@ function codeFixRequests(input, base = reportBase(input)) {
5541
5765
  }
5542
5766
 
5543
5767
  // src/core/stats/cost/token-counts.ts
5544
- import { z as z14 } from "zod";
5545
- var tokenCountsSchema = z14.object({
5546
- input: z14.number(),
5547
- cacheWrite5m: z14.number(),
5548
- cacheWrite1h: z14.number(),
5549
- cacheRead: z14.number(),
5550
- output: z14.number()
5768
+ import { z as z16 } from "zod";
5769
+ var tokenCountsSchema = z16.object({
5770
+ input: z16.number(),
5771
+ cacheWrite5m: z16.number(),
5772
+ cacheWrite1h: z16.number(),
5773
+ cacheRead: z16.number(),
5774
+ output: z16.number()
5551
5775
  });
5552
5776
  var ZERO_TOKENS = { input: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0, output: 0 };
5553
5777
  function addTokens(a, b) {
@@ -5562,24 +5786,24 @@ function totalTokens(tokens) {
5562
5786
  }
5563
5787
 
5564
5788
  // src/core/stats/cost/usage-state.ts
5565
- import { z as z15 } from "zod";
5789
+ import { z as z17 } from "zod";
5566
5790
  var COST_REPORT_DAYS = 30;
5567
- var estimatedKindSchema = z15.enum(["cli", "skill"]);
5568
- var usageBucketSchema = z15.object({
5569
- slot: z15.string(),
5570
- cwd: z15.string(),
5571
- model: z15.string(),
5572
- kind: z15.enum(["hook", ...estimatedKindSchema.options]),
5791
+ var estimatedKindSchema = z17.enum(["cli", "skill"]);
5792
+ var usageBucketSchema = z17.object({
5793
+ slot: z17.string(),
5794
+ cwd: z17.string(),
5795
+ model: z17.string(),
5796
+ kind: z17.enum(["hook", ...estimatedKindSchema.options]),
5573
5797
  tokens: tokenCountsSchema,
5574
- hookTurns: z15.number()
5798
+ hookTurns: z17.number()
5575
5799
  });
5576
- var transcriptStateSchema = z15.object({
5577
- hookOpen: z15.boolean(),
5578
- lastModel: z15.string().nullable(),
5579
- lastMessageId: z15.string().nullable(),
5800
+ var transcriptStateSchema = z17.object({
5801
+ hookOpen: z17.boolean(),
5802
+ lastModel: z17.string().nullable(),
5803
+ lastMessageId: z17.string().nullable(),
5580
5804
  lastMessageTokens: tokenCountsSchema.nullable().optional(),
5581
- pending: z15.record(z15.string(), estimatedKindSchema),
5582
- pendingEstimates: z15.array(z15.object({ kind: estimatedKindSchema, chars: z15.number(), slot: z15.string(), cwd: z15.string() }))
5805
+ pending: z17.record(z17.string(), estimatedKindSchema),
5806
+ pendingEstimates: z17.array(z17.object({ kind: estimatedKindSchema, chars: z17.number(), slot: z17.string(), cwd: z17.string() }))
5583
5807
  });
5584
5808
 
5585
5809
  // src/core/stats/cost/pricing.ts
@@ -5631,26 +5855,6 @@ function costOf(model, tokens) {
5631
5855
  return total / 1e6;
5632
5856
  }
5633
5857
 
5634
- // src/core/stats/days.ts
5635
- var STATS_DAYS = 30;
5636
- function dayRange(now, count3) {
5637
- return dayStarts(now, count3).map(formatLocalDay);
5638
- }
5639
- function dailyIntake(histories, now) {
5640
- const created = /* @__PURE__ */ new Map();
5641
- for (const history of histories) {
5642
- const day = formatLocalDay(new Date(history.createdAt));
5643
- created.set(day, (created.get(day) ?? 0) + 1);
5644
- }
5645
- return dayRange(now, STATS_DAYS).map((day) => ({ day, created: created.get(day) ?? 0 }));
5646
- }
5647
- function dayWindows(now, count3 = STATS_DAYS) {
5648
- return consecutivePeriods(dayStarts(now, count3), now.getTime());
5649
- }
5650
- function dayStarts(now, count3) {
5651
- return Array.from({ length: count3 }, (_, index) => new Date(now.getFullYear(), now.getMonth(), now.getDate() - (count3 - 1 - index)));
5652
- }
5653
-
5654
5858
  // src/core/stats/cost/cost-report.ts
5655
5859
  var COST_TOTALS_DAYS = 7;
5656
5860
  function costReport({ buckets, runs, projectOf: projectOf2, projectId, now, scan }) {
@@ -5668,6 +5872,10 @@ function costReport({ buckets, runs, projectOf: projectOf2, projectId, now, scan
5668
5872
  since: sinceOf(scopedBuckets),
5669
5873
  totals: totalsOf(inDays(bucketsByDay, totalsDays), inDays(runsByDay, totalsDays)),
5670
5874
  days: days3.map((day) => dayRow(day, bucketsByDay.get(day) ?? [], runsByDay.get(day) ?? [])),
5875
+ weeks: weekWindows(now).map((week) => {
5876
+ const weekDays = daysOf(week);
5877
+ return periodRow(formatLocalIso(new Date(week.from)), inDays(bucketsByDay, weekDays), inDays(runsByDay, weekDays));
5878
+ }),
5671
5879
  models: modelsOf(scopedBuckets),
5672
5880
  commands: commandsOf(inDays(runsByDay, days3))
5673
5881
  };
@@ -5679,6 +5887,10 @@ function memoizedByCwd(projectOf2) {
5679
5887
  return known.get(cwd) ?? null;
5680
5888
  };
5681
5889
  }
5890
+ function daysOf(week) {
5891
+ const monday = new Date(week.from);
5892
+ return dayRange(new Date(monday.getFullYear(), monday.getMonth(), monday.getDate() + DAYS_PER_WEEK - 1), DAYS_PER_WEEK);
5893
+ }
5682
5894
  function localDay(at) {
5683
5895
  const moment = Date.parse(at);
5684
5896
  return Number.isNaN(moment) ? "" : formatLocalDay(new Date(moment));
@@ -5714,19 +5926,24 @@ function runCounts(runs) {
5714
5926
  const hookRuns = runs.filter((run) => run.command === HOOK_STOP_COMMAND).length;
5715
5927
  return { cliRuns: runs.length - hookRuns, hookRuns };
5716
5928
  }
5717
- function dayRow(day, dayBuckets, dayRuns) {
5718
- const hookBuckets = dayBuckets.filter((bucket) => bucket.kind === "hook");
5719
- const cliBuckets = dayBuckets.filter((bucket) => bucket.kind === "cli" || bucket.kind === "skill");
5929
+ function costRowNumbers(buckets, runs) {
5930
+ const hookBuckets = buckets.filter((bucket) => bucket.kind === "hook");
5931
+ const cliBuckets = buckets.filter((bucket) => bucket.kind === "cli" || bucket.kind === "skill");
5720
5932
  return {
5721
- day,
5722
5933
  hookTokens: tokensTotalOf(hookBuckets),
5723
5934
  cliTokens: tokensTotalOf(cliBuckets),
5724
- cost: costOfBuckets(dayBuckets),
5725
- hasUnpricedTokens: hasUnpricedTokens(dayBuckets),
5726
- hookTurns: sum(dayBuckets.map((bucket) => bucket.hookTurns)),
5727
- ...runCounts(dayRuns)
5935
+ cost: costOfBuckets(buckets),
5936
+ hasUnpricedTokens: hasUnpricedTokens(buckets),
5937
+ hookTurns: sum(buckets.map((bucket) => bucket.hookTurns)),
5938
+ ...runCounts(runs)
5728
5939
  };
5729
5940
  }
5941
+ function dayRow(day, dayBuckets, dayRuns) {
5942
+ return { day, ...costRowNumbers(dayBuckets, dayRuns) };
5943
+ }
5944
+ function periodRow(start, periodBuckets, periodRuns) {
5945
+ return { start, ...costRowNumbers(periodBuckets, periodRuns) };
5946
+ }
5730
5947
  function modelsOf(buckets) {
5731
5948
  return [...groupBy(buckets, (bucket) => bucket.model).entries()].map(([key, modelBuckets]) => ({ ...splitFastModel(key), tokens: tokensTotalOf(modelBuckets), cost: costOfBuckets(modelBuckets) })).filter((row) => row.tokens > 0).sort((a, b) => b.tokens - a.tokens);
5732
5949
  }
@@ -5950,6 +6167,7 @@ function qualityReport(input, base = reportBase(input), graphs = []) {
5950
6167
  ...base.head,
5951
6168
  accuracy: accuracy(histories, period2),
5952
6169
  accuracyWeeks: accuracyWeeks(histories, now),
6170
+ accuracyDays: accuracyDays(histories, now),
5953
6171
  methodAccuracy: methodAccuracy(histories, period2),
5954
6172
  matchAccuracy: matchAccuracy(histories, period2),
5955
6173
  graph: { projects: graphs, filter: graphFilterEffect(histories, period2) },
@@ -5969,7 +6187,7 @@ function statsReport(input, base = reportBase(input)) {
5969
6187
  ...base.head,
5970
6188
  totals: totals(histories, now, period2, base.scope.journalStart),
5971
6189
  weeks: weeklyFlow(histories, now),
5972
- days: dailyIntake(histories, now),
6190
+ days: dailyFlow(histories, now),
5973
6191
  hotspots: hotspots(openTasks, scopeLabel(projectId)),
5974
6192
  age: ageBreakdown(openTasks, now),
5975
6193
  closing: closingBreakdown(histories, period2)
@@ -6289,7 +6507,7 @@ function createApp({ root, readLanguage: readLanguage2, changes, allowedHosts, h
6289
6507
  });
6290
6508
  if (staticDir !== void 0) {
6291
6509
  app.use("*", serveStatic({ root: staticDir }));
6292
- app.get("*", serveStatic({ path: join22(staticDir, "index.html") }));
6510
+ app.get("*", serveStatic({ path: join24(staticDir, "index.html") }));
6293
6511
  }
6294
6512
  return app;
6295
6513
  }
@@ -6401,12 +6619,12 @@ function logReport({ closedEpics, reopenedEpics, blockingFiles, deleted, conflic
6401
6619
  }
6402
6620
 
6403
6621
  // src/core/usage/transcripts.ts
6404
- import { createHash as createHash4 } from "node:crypto";
6405
- import { open as open2, stat as stat5 } from "node:fs/promises";
6406
- import { basename as basename10, join as join24 } from "node:path";
6622
+ import { createHash as createHash5 } from "node:crypto";
6623
+ import { open as open2, stat as stat6 } from "node:fs/promises";
6624
+ import { basename as basename10, join as join26 } from "node:path";
6407
6625
 
6408
6626
  // src/core/stats/cost/attribute.ts
6409
- import { z as z16 } from "zod";
6627
+ import { z as z18 } from "zod";
6410
6628
 
6411
6629
  // src/core/stats/cost/shell-commands.ts
6412
6630
  var SEPARATORS = ["&&", "||", ";", "|", "&", "\n", "`"];
@@ -6599,21 +6817,21 @@ var FAST_SPEED = "fast";
6599
6817
  var CHARS_PER_TOKEN = 3;
6600
6818
  var PENDING_TOOL_LIMIT = 64;
6601
6819
  var ZONE_OFFSET_STEP_MS = 15 * 60 * 1e3;
6602
- var usageSchema = z16.object({
6603
- input_tokens: z16.number().optional(),
6604
- output_tokens: z16.number().optional(),
6605
- cache_read_input_tokens: z16.number().optional(),
6606
- cache_creation_input_tokens: z16.number().optional(),
6607
- speed: z16.string().optional(),
6608
- cache_creation: z16.object({ ephemeral_5m_input_tokens: z16.number().optional(), ephemeral_1h_input_tokens: z16.number().optional() }).passthrough().optional()
6820
+ var usageSchema = z18.object({
6821
+ input_tokens: z18.number().optional(),
6822
+ output_tokens: z18.number().optional(),
6823
+ cache_read_input_tokens: z18.number().optional(),
6824
+ cache_creation_input_tokens: z18.number().optional(),
6825
+ speed: z18.string().optional(),
6826
+ cache_creation: z18.object({ ephemeral_5m_input_tokens: z18.number().optional(), ephemeral_1h_input_tokens: z18.number().optional() }).passthrough().optional()
6609
6827
  }).passthrough();
6610
- var assistantMessageSchema = z16.object({ id: z16.string().optional(), model: z16.string().optional(), usage: usageSchema.optional(), content: z16.array(z16.unknown()).optional() }).passthrough();
6611
- var userMessageSchema = z16.object({ content: z16.unknown().optional() }).passthrough();
6612
- var toolUseBlockSchema = z16.object({ type: z16.literal("tool_use"), id: z16.string(), name: z16.string(), input: z16.unknown().optional() }).passthrough();
6613
- var toolResultBlockSchema = z16.object({ type: z16.literal("tool_result"), tool_use_id: z16.string().optional(), content: z16.unknown().optional() }).passthrough();
6614
- var textBlockSchema = z16.object({ type: z16.literal("text"), text: z16.string() }).passthrough();
6615
- var bashInputSchema = z16.object({ command: z16.string() }).passthrough();
6616
- var lineSchema = z16.object({ type: z16.string().optional(), timestamp: z16.string().optional(), cwd: z16.string().optional(), isMeta: z16.boolean().optional(), message: z16.unknown().optional() }).passthrough();
6828
+ var assistantMessageSchema = z18.object({ id: z18.string().optional(), model: z18.string().optional(), usage: usageSchema.optional(), content: z18.array(z18.unknown()).optional() }).passthrough();
6829
+ var userMessageSchema = z18.object({ content: z18.unknown().optional() }).passthrough();
6830
+ var toolUseBlockSchema = z18.object({ type: z18.literal("tool_use"), id: z18.string(), name: z18.string(), input: z18.unknown().optional() }).passthrough();
6831
+ var toolResultBlockSchema = z18.object({ type: z18.literal("tool_result"), tool_use_id: z18.string().optional(), content: z18.unknown().optional() }).passthrough();
6832
+ var textBlockSchema = z18.object({ type: z18.literal("text"), text: z18.string() }).passthrough();
6833
+ var bashInputSchema = z18.object({ command: z18.string() }).passthrough();
6834
+ var lineSchema = z18.object({ type: z18.string().optional(), timestamp: z18.string().optional(), cwd: z18.string().optional(), isMeta: z18.boolean().optional(), message: z18.unknown().optional() }).passthrough();
6617
6835
  function newTranscriptState() {
6618
6836
  return { hookOpen: false, lastModel: null, lastMessageId: null, lastMessageTokens: null, pending: {}, pendingEstimates: [] };
6619
6837
  }
@@ -6737,53 +6955,53 @@ function slotOf(timestamp) {
6737
6955
 
6738
6956
  // src/core/usage/usage-cache.ts
6739
6957
  import { mkdir as mkdir5 } from "node:fs/promises";
6740
- import { join as join23 } from "node:path";
6741
- import { z as z17 } from "zod";
6958
+ import { join as join25 } from "node:path";
6959
+ import { z as z19 } from "zod";
6742
6960
  var USAGE_CACHE_FILE = ".usage-cache.json";
6743
6961
  var USAGE_CACHE_VERSION = 6;
6744
- var usageCacheEntrySchema = z17.object({
6745
- size: z17.number(),
6746
- mtimeMs: z17.number().optional(),
6747
- offset: z17.number(),
6748
- fingerprint: z17.string(),
6962
+ var usageCacheEntrySchema = z19.object({
6963
+ size: z19.number(),
6964
+ mtimeMs: z19.number().optional(),
6965
+ offset: z19.number(),
6966
+ fingerprint: z19.string(),
6749
6967
  state: transcriptStateSchema,
6750
- buckets: z17.array(usageBucketSchema)
6968
+ buckets: z19.array(usageBucketSchema)
6751
6969
  });
6752
- var usageCacheSchema = z17.object({
6753
- version: z17.literal(USAGE_CACHE_VERSION),
6754
- files: z17.record(z17.string(), usageCacheEntrySchema)
6970
+ var usageCacheSchema = z19.object({
6971
+ version: z19.literal(USAGE_CACHE_VERSION),
6972
+ files: z19.record(z19.string(), usageCacheEntrySchema)
6755
6973
  });
6756
6974
  function emptyUsageCache() {
6757
6975
  return { version: USAGE_CACHE_VERSION, files: {} };
6758
6976
  }
6759
6977
  async function readUsageCache(root) {
6760
- return await readJsonFile(join23(root, USAGE_CACHE_FILE), usageCacheSchema) ?? emptyUsageCache();
6978
+ return await readJsonFile(join25(root, USAGE_CACHE_FILE), usageCacheSchema) ?? emptyUsageCache();
6761
6979
  }
6762
6980
  async function writeUsageCache(root, cache) {
6763
6981
  await mkdir5(root, { recursive: true });
6764
- await writeFileAtomic(join23(root, USAGE_CACHE_FILE), JSON.stringify(cache));
6982
+ await writeFileAtomic(join25(root, USAGE_CACHE_FILE), JSON.stringify(cache));
6765
6983
  }
6766
6984
 
6767
6985
  // src/core/usage/transcripts.ts
6768
6986
  var NEWLINE = 10;
6769
6987
  var FINGERPRINT_BYTES = 256;
6770
6988
  var ABANDONED_LINE_MS = 10 * 60 * 1e3;
6771
- var EMPTY_FINGERPRINT = createHash4("sha1").digest("hex");
6989
+ var EMPTY_FINGERPRINT = createHash5("sha1").digest("hex");
6772
6990
  var COUNTED_LINE_MARKERS = ['"type":"assistant"', '"type":"user"'];
6773
6991
  async function listTranscripts(claudeProjectsDir2) {
6774
6992
  const files = [];
6775
6993
  for (const projectEntry of await listDir(claudeProjectsDir2)) {
6776
6994
  if (!projectEntry.isDirectory()) continue;
6777
- const projectDir2 = join24(claudeProjectsDir2, projectEntry.name);
6995
+ const projectDir2 = join26(claudeProjectsDir2, projectEntry.name);
6778
6996
  for (const entry of await listDir(projectDir2)) {
6779
- const entryPath = join24(projectDir2, entry.name);
6997
+ const entryPath = join26(projectDir2, entry.name);
6780
6998
  if (entry.isFile() && entry.name.endsWith(".jsonl")) {
6781
6999
  files.push(...await listedFile(entryPath));
6782
7000
  continue;
6783
7001
  }
6784
7002
  if (!entry.isDirectory()) continue;
6785
- for (const subagentEntry of await listDir(join24(entryPath, "subagents"), { recursive: true })) {
6786
- if (subagentEntry.isFile() && subagentEntry.name.endsWith(".jsonl")) files.push(...await listedFile(join24(subagentEntry.parentPath, subagentEntry.name)));
7003
+ for (const subagentEntry of await listDir(join26(entryPath, "subagents"), { recursive: true })) {
7004
+ if (subagentEntry.isFile() && subagentEntry.name.endsWith(".jsonl")) files.push(...await listedFile(join26(subagentEntry.parentPath, subagentEntry.name)));
6787
7005
  }
6788
7006
  }
6789
7007
  }
@@ -6791,7 +7009,7 @@ async function listTranscripts(claudeProjectsDir2) {
6791
7009
  }
6792
7010
  async function listedFile(path) {
6793
7011
  try {
6794
- const { size, mtimeMs } = await stat5(path);
7012
+ const { size, mtimeMs } = await stat6(path);
6795
7013
  return [{ path, size, mtimeMs }];
6796
7014
  } catch {
6797
7015
  return [];
@@ -6819,7 +7037,7 @@ async function scanTranscripts({ files, cache, byteBudget, now }) {
6819
7037
  };
6820
7038
  }
6821
7039
  function deletedStillReported(cache, listedFiles, now) {
6822
- const reportStart = now.getTime() - COST_REPORT_DAYS * DAY_MS;
7040
+ const reportStart = now.getTime() - STATS_HISTORY_DAYS * DAY_MS;
6823
7041
  const listedSessions = new Set(Object.keys(listedFiles).map((path) => basename10(path)));
6824
7042
  return Object.fromEntries(
6825
7043
  Object.entries(cache.files).filter(([path, entry]) => !listedSessions.has(basename10(path)) && entry.buckets.some((bucket) => Date.parse(bucket.slot) >= reportStart))
@@ -6856,7 +7074,7 @@ async function fingerprintOf(path, offset) {
6856
7074
  return withFile(path, async (handle) => {
6857
7075
  const head = await readAt(handle, 0, edge);
6858
7076
  const tail = await readAt(handle, offset - edge, edge);
6859
- return createHash4("sha1").update(head).update(tail).digest("hex");
7077
+ return createHash5("sha1").update(head).update(tail).digest("hex");
6860
7078
  });
6861
7079
  }
6862
7080
  function readChunk(path, position, length) {
@@ -6974,7 +7192,7 @@ function progressBefore(files, cache) {
6974
7192
  // src/server/start.ts
6975
7193
  var SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
6976
7194
  var STOP_SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"];
6977
- var BUNDLED_WEB_DIR = join25(import.meta.dirname, "web");
7195
+ var BUNDLED_WEB_DIR = join27(import.meta.dirname, "web");
6978
7196
  var log = (line) => void process.stdout.write(`${line}
6979
7197
  `);
6980
7198
  var warn = (line) => void process.stderr.write(`${line}
@@ -7075,7 +7293,7 @@ async function runServe(args, io) {
7075
7293
 
7076
7294
  // src/cli/service/launchd.ts
7077
7295
  import { mkdir as mkdir7, rm as rm5, writeFile as writeFile4 } from "node:fs/promises";
7078
- import { dirname as dirname8, join as join26, posix } from "node:path";
7296
+ import { dirname as dirname8, join as join28, posix } from "node:path";
7079
7297
  import { setTimeout as sleep3 } from "node:timers/promises";
7080
7298
 
7081
7299
  // src/cli/service/service.ts
@@ -7148,7 +7366,7 @@ ${entries}
7148
7366
  `;
7149
7367
  }
7150
7368
  function launchdManager(context, delay = sleep3) {
7151
- const file = join26(context.home, "Library/LaunchAgents", `${LABEL}.plist`);
7369
+ const file = join28(context.home, "Library/LaunchAgents", `${LABEL}.plist`);
7152
7370
  const domain = `gui/${context.uid}`;
7153
7371
  const bootout = () => context.exec("launchctl", ["bootout", `${domain}/${LABEL}`]);
7154
7372
  const bootstrap = () => context.exec("launchctl", ["bootstrap", domain, file]);
@@ -7183,7 +7401,7 @@ function launchdManager(context, delay = sleep3) {
7183
7401
 
7184
7402
  // src/cli/service/startup-folder.ts
7185
7403
  import { mkdir as mkdir8, rm as rm6, writeFile as writeFile5 } from "node:fs/promises";
7186
- import { dirname as dirname9, join as join27, win32 } from "node:path";
7404
+ import { dirname as dirname9, join as join29, win32 } from "node:path";
7187
7405
  var SCRIPT_NAME = "p-backlog.vbs";
7188
7406
  var COMMAND_LINE_ARGUMENT = /"[^"]*"|\S+/g;
7189
7407
  var PROCESS_QUERY_TIMEOUT_MS = 15e3;
@@ -7191,16 +7409,16 @@ function vbsString(value) {
7191
7409
  return `"${value.replaceAll('"', '""')}"`;
7192
7410
  }
7193
7411
  function appDataDir(context) {
7194
- return join27(context.env.LOCALAPPDATA ?? join27(context.home, "AppData", "Local"), "p-backlog");
7412
+ return join29(context.env.LOCALAPPDATA ?? join29(context.home, "AppData", "Local"), "p-backlog");
7195
7413
  }
7196
7414
  function startupFolder(context) {
7197
- return join27(context.env.APPDATA ?? join27(context.home, "AppData", "Roaming"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
7415
+ return join29(context.env.APPDATA ?? join29(context.home, "AppData", "Roaming"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
7198
7416
  }
7199
7417
  function logPath2(context) {
7200
- return join27(appDataDir(context), "p-backlog.log");
7418
+ return join29(appDataDir(context), "p-backlog.log");
7201
7419
  }
7202
7420
  function pidFilePath(context) {
7203
- return join27(appDataDir(context), "server.pid");
7421
+ return join29(appDataDir(context), "server.pid");
7204
7422
  }
7205
7423
  var NODE_PATH_ENV = "P_BACKLOG_NODE";
7206
7424
  var CLI_PATH_ENV = "P_BACKLOG_CLI";
@@ -7251,7 +7469,7 @@ function sameWindowsPath(left, right) {
7251
7469
  return win32.normalize(left).toLowerCase() === win32.normalize(right).toLowerCase();
7252
7470
  }
7253
7471
  function startupFolderManager(context) {
7254
- const file = join27(startupFolder(context), SCRIPT_NAME);
7472
+ const file = join29(startupFolder(context), SCRIPT_NAME);
7255
7473
  return {
7256
7474
  file,
7257
7475
  logs: logPath2(context),
@@ -7278,7 +7496,7 @@ function startupFolderManager(context) {
7278
7496
 
7279
7497
  // src/cli/service/systemd.ts
7280
7498
  import { mkdir as mkdir9, rm as rm7, writeFile as writeFile6 } from "node:fs/promises";
7281
- import { dirname as dirname10, join as join28 } from "node:path";
7499
+ import { dirname as dirname10, join as join30 } from "node:path";
7282
7500
  var UNIT = "p-backlog.service";
7283
7501
  function quoted(value) {
7284
7502
  return `"${value.replace(/[\\"]/g, "\\$&").replaceAll("%", "%%")}"`;
@@ -7312,7 +7530,7 @@ async function systemctlSteps(exec, steps) {
7312
7530
  return "done";
7313
7531
  }
7314
7532
  function systemdManager(context) {
7315
- const file = join28(context.home, ".config/systemd/user", UNIT);
7533
+ const file = join30(context.home, ".config/systemd/user", UNIT);
7316
7534
  return {
7317
7535
  file,
7318
7536
  logs: "journalctl --user -u p-backlog",
@@ -7430,50 +7648,43 @@ function reportFailure({ failed, code, output }, io) {
7430
7648
  }
7431
7649
 
7432
7650
  // src/cli/commands/setup.ts
7433
- import { join as join29 } from "node:path";
7651
+ import { join as join32 } from "node:path";
7434
7652
 
7435
- // src/cli/stop-hook.ts
7436
- import { mkdir as mkdir10, readFile as readFile5, readlink as readlink2, realpath as realpath2, stat as stat6 } from "node:fs/promises";
7653
+ // src/cli/agents/agent-hooks.ts
7654
+ import { join as join31 } from "node:path";
7655
+
7656
+ // src/cli/agents/grouped-stop-hooks.ts
7657
+ import { z as z20 } from "zod";
7658
+
7659
+ // src/cli/agents/json-config.ts
7660
+ import { mkdir as mkdir10, readFile as readFile5, readlink as readlink2, realpath as realpath2, stat as stat7 } from "node:fs/promises";
7437
7661
  import { dirname as dirname11, resolve as resolve4 } from "node:path";
7438
- import { z as z18 } from "zod";
7439
- var POSIX_COMMAND = "command -v backlog >/dev/null && backlog hook stop || true";
7440
- var POWERSHELL_COMMAND = "if (Get-Command backlog.cmd -ErrorAction SilentlyContinue) { backlog.cmd hook stop }";
7441
7662
  var MAX_LINK_HOPS = 40;
7442
- var stopHookGroupSchema = z18.object({ hooks: z18.array(z18.unknown()).optional() }).passthrough();
7443
- var settingsSchema2 = z18.object({ hooks: z18.object({ Stop: z18.array(stopHookGroupSchema).optional() }).passthrough().optional() }).passthrough();
7444
- function stopHookFor(platform) {
7445
- return platform === "win32" ? { type: "command", shell: "powershell", command: POWERSHELL_COMMAND } : { type: "command", command: POSIX_COMMAND };
7446
- }
7447
- function isOurStopHook(hook) {
7448
- if (typeof hook !== "object" || hook === null) return false;
7449
- const command = hook.command;
7450
- return command === POSIX_COMMAND || command === POWERSHELL_COMMAND;
7451
- }
7452
- async function addStopHook(settingsPath, platform) {
7453
- const text = await readSettingsText(settingsPath);
7663
+ async function readJsonConfig(path, schema) {
7664
+ const text = await readConfigText(path);
7454
7665
  if (typeof text !== "string") return text;
7455
- const settings = validatedSettings(text);
7456
- if (settings === null) return { failed: "invalid" };
7457
- const hooks = settings.hooks ??= {};
7458
- const stopGroups = hooks.Stop ??= [];
7459
- const installed = stopGroups.some((group) => group.hooks?.some(isOurStopHook) ?? false);
7460
- if (installed) return "exists";
7461
- stopGroups.push({ hooks: [stopHookFor(platform)] });
7462
- await mkdir10(dirname11(settingsPath), { recursive: true });
7463
- const target = await writeTargetOf(settingsPath);
7464
- const mode = await stat6(target).then(({ mode: mode2 }) => mode2 & 511, () => void 0);
7465
- await writeFileAtomic(target, `${JSON.stringify(settings, null, 2)}
7466
- `, mode);
7467
- return "added";
7468
- }
7469
- function validatedSettings(text) {
7470
7666
  let value;
7471
7667
  try {
7472
7668
  value = JSON.parse(text);
7473
7669
  } catch {
7474
- return null;
7670
+ return { failed: "invalid" };
7671
+ }
7672
+ return schema.safeParse(value).success ? { config: value } : { failed: "invalid" };
7673
+ }
7674
+ async function writeJsonConfig(path, config) {
7675
+ await mkdir10(dirname11(path), { recursive: true });
7676
+ const target = await writeTargetOf(path);
7677
+ const mode = await stat7(target).then(({ mode: mode2 }) => mode2 & 511, () => void 0);
7678
+ await writeFileAtomic(target, `${JSON.stringify(config, null, 2)}
7679
+ `, mode);
7680
+ }
7681
+ async function readConfigText(path) {
7682
+ try {
7683
+ return await readFile5(path, "utf8");
7684
+ } catch (error) {
7685
+ if (hasErrorCode(error, "ENOENT")) return "{}";
7686
+ return { failed: "unreadable", code: errorCodeOrText(error) };
7475
7687
  }
7476
- return settingsSchema2.safeParse(value).success ? value : null;
7477
7688
  }
7478
7689
  async function writeTargetOf(path) {
7479
7690
  try {
@@ -7492,62 +7703,256 @@ async function danglingLinkTarget(path) {
7492
7703
  }
7493
7704
  return current;
7494
7705
  }
7495
- async function readSettingsText(settingsPath) {
7496
- try {
7497
- return await readFile5(settingsPath, "utf8");
7498
- } catch (error) {
7499
- if (hasErrorCode(error, "ENOENT")) return "{}";
7500
- return { failed: "unreadable", code: errorCodeOrText(error) };
7706
+
7707
+ // src/cli/agents/grouped-stop-hooks.ts
7708
+ var stopGroupSchema = z20.object({ hooks: z20.array(z20.unknown()).optional() }).passthrough();
7709
+ var groupedConfigSchema = z20.object({ hooks: z20.object({ Stop: z20.array(stopGroupSchema).optional() }).passthrough().optional() }).passthrough();
7710
+ function refreshOurHook(lists, hook, { isOurs, isCurrent }) {
7711
+ const ours = lists.flatMap((list) => list.flatMap((entry, index) => isOurs(entry) ? [{ list, index, entry }] : []));
7712
+ if (ours.some(({ entry }) => isCurrent(entry))) return "exists";
7713
+ const stale = ours[0];
7714
+ if (stale === void 0) return "missing";
7715
+ stale.list[stale.index] = { ...stale.entry, ...hook };
7716
+ return "updated";
7717
+ }
7718
+ async function addGroupedStopHook(path, hook, ourHook) {
7719
+ const read = await readJsonConfig(path, groupedConfigSchema);
7720
+ if (!("config" in read)) return read;
7721
+ const stopGroups = (read.config.hooks ??= {}).Stop ??= [];
7722
+ const refreshed = refreshOurHook(stopGroups.flatMap((group) => group.hooks === void 0 ? [] : [group.hooks]), hook, ourHook);
7723
+ if (refreshed === "exists") return "exists";
7724
+ if (refreshed === "missing") stopGroups.push({ hooks: [hook] });
7725
+ await writeJsonConfig(path, read.config);
7726
+ return refreshed === "missing" ? "added" : "updated";
7727
+ }
7728
+ async function removeGroupedStopHook(path, isOurs) {
7729
+ const read = await readJsonConfig(path, groupedConfigSchema);
7730
+ if (!("config" in read)) return read;
7731
+ const hooks = read.config.hooks;
7732
+ if (hooks?.Stop === void 0 || !hooks.Stop.some((group) => hasOurHook(group, isOurs))) return "absent";
7733
+ const kept = hooks.Stop.flatMap((group) => {
7734
+ if (!hasOurHook(group, isOurs)) return [group];
7735
+ const others = (group.hooks ?? []).filter((hook) => !isOurs(hook));
7736
+ return others.length > 0 ? [{ ...group, hooks: others }] : [];
7737
+ });
7738
+ if (kept.length > 0) hooks.Stop = kept;
7739
+ else delete hooks.Stop;
7740
+ await writeJsonConfig(path, read.config);
7741
+ return "removed";
7742
+ }
7743
+ function hasOurHook(group, isOurs) {
7744
+ return group.hooks?.some(isOurs) ?? false;
7745
+ }
7746
+
7747
+ // src/cli/stop-hook.ts
7748
+ var POSIX_COMMAND = guardedPosixCommand(HOOK_STOP_COMMAND);
7749
+ var POWERSHELL_COMMAND = "if (Get-Command backlog.cmd -ErrorAction SilentlyContinue) { backlog.cmd hook stop }";
7750
+ function stopHookFor(platform) {
7751
+ return platform === "win32" ? { type: "command", shell: "powershell", command: POWERSHELL_COMMAND } : { type: "command", command: POSIX_COMMAND };
7752
+ }
7753
+ function guardedPosixCommand(backlogArgs) {
7754
+ return `command -v backlog >/dev/null && backlog ${backlogArgs} || true`;
7755
+ }
7756
+ function hookCommand2(hook) {
7757
+ const command = typeof hook === "object" && hook !== null ? hook.command : void 0;
7758
+ return typeof command === "string" ? command : void 0;
7759
+ }
7760
+ function isOurStopHook(hook) {
7761
+ const command = hookCommand2(hook);
7762
+ return command === POSIX_COMMAND || command === POWERSHELL_COMMAND;
7763
+ }
7764
+ function addStopHook(settingsPath, platform) {
7765
+ return addGroupedStopHook(settingsPath, stopHookFor(platform), { isOurs: isOurStopHook, isCurrent: isOurStopHook });
7766
+ }
7767
+ function removeStopHook(settingsPath) {
7768
+ return removeGroupedStopHook(settingsPath, isOurStopHook);
7769
+ }
7770
+
7771
+ // src/cli/agents/cursor-hooks.ts
7772
+ import { z as z21 } from "zod";
7773
+ var CURSOR_HOOKS_VERSION = 1;
7774
+ var cursorConfigSchema = z21.object({ version: z21.number().optional(), hooks: z21.object({ stop: z21.array(z21.unknown()).optional() }).passthrough().optional() }).passthrough();
7775
+ async function addCursorStopHook(path, hook, ourHook) {
7776
+ const read = await readJsonConfig(path, cursorConfigSchema);
7777
+ if (!("config" in read)) return read;
7778
+ const stop = (read.config.hooks ??= {}).stop ??= [];
7779
+ const refreshed = refreshOurHook([stop], hook, ourHook);
7780
+ if (refreshed === "exists") return "exists";
7781
+ if (refreshed === "missing") stop.push(hook);
7782
+ read.config.version ??= CURSOR_HOOKS_VERSION;
7783
+ await writeJsonConfig(path, read.config);
7784
+ return refreshed === "missing" ? "added" : "updated";
7785
+ }
7786
+ async function removeCursorStopHook(path, isOurs) {
7787
+ const read = await readJsonConfig(path, cursorConfigSchema);
7788
+ if (!("config" in read)) return read;
7789
+ const hooks = read.config.hooks;
7790
+ if (hooks?.stop === void 0 || !hooks.stop.some(isOurs)) return "absent";
7791
+ const kept = hooks.stop.filter((hook) => !isOurs(hook));
7792
+ if (kept.length > 0) hooks.stop = kept;
7793
+ else delete hooks.stop;
7794
+ await writeJsonConfig(path, read.config);
7795
+ return "removed";
7796
+ }
7797
+
7798
+ // src/cli/agents/agent-hooks.ts
7799
+ var CODEX_HOOK_TIMEOUT_SECONDS = 30;
7800
+ var AGENT_HOOKS_FILE = "hooks.json";
7801
+ function agentHookConfigPath(agent, places) {
7802
+ return agent === "claude" ? claudeSettingsPath(places.env, places.home) : join31(agentHomeDir(agent, places), AGENT_HOOKS_FILE);
7803
+ }
7804
+ function installAgentHook(agent, site) {
7805
+ const path = agentHookConfigPath(agent, site);
7806
+ switch (agent) {
7807
+ case "claude":
7808
+ return addStopHook(path, site.platform);
7809
+ case "codex": {
7810
+ const hook = { type: "command", command: posixCommand(agent), commandWindows: windowsCommand(agent, site.cliPath), timeout: CODEX_HOOK_TIMEOUT_SECONDS };
7811
+ return addGroupedStopHook(path, hook, ourCurrentHook(agent, site.cliPath, hook));
7812
+ }
7813
+ case "cursor": {
7814
+ const hook = { command: site.platform === "win32" ? windowsCommand(agent, site.cliPath) : posixCommand(agent) };
7815
+ return addCursorStopHook(path, hook, ourCurrentHook(agent, site.cliPath, hook));
7816
+ }
7817
+ }
7818
+ }
7819
+ function removeAgentHook(agent, site) {
7820
+ const path = agentHookConfigPath(agent, site);
7821
+ switch (agent) {
7822
+ case "claude":
7823
+ return removeStopHook(path);
7824
+ case "codex":
7825
+ return removeGroupedStopHook(path, ourHookOf(agent));
7826
+ case "cursor":
7827
+ return removeCursorStopHook(path, ourHookOf(agent));
7501
7828
  }
7502
7829
  }
7830
+ function agentStopCommand(agent) {
7831
+ return `${HOOK_STOP_COMMAND} --agent ${agent}`;
7832
+ }
7833
+ function posixCommand(agent) {
7834
+ return guardedPosixCommand(agentStopCommand(agent));
7835
+ }
7836
+ function windowsCommand(agent, cliPath2) {
7837
+ return `node "${cliPath2}" ${agentStopCommand(agent)}`;
7838
+ }
7839
+ function windowsCommandPattern(agent) {
7840
+ return new RegExp(`^node "[^"]*cli\\.js" ${agentStopCommand(agent)}$`);
7841
+ }
7842
+ function ourHookOf(agent, cliPath2) {
7843
+ return (hook) => {
7844
+ const command = hookCommand2(hook);
7845
+ if (command === void 0) return false;
7846
+ return command === posixCommand(agent) || cliPath2 !== void 0 && command === windowsCommand(agent, cliPath2) || windowsCommandPattern(agent).test(command);
7847
+ };
7848
+ }
7849
+ function ourCurrentHook(agent, cliPath2, wanted) {
7850
+ return {
7851
+ isOurs: ourHookOf(agent, cliPath2),
7852
+ isCurrent: (hook) => Object.entries(wanted).every(([key, value]) => hook[key] === value)
7853
+ };
7854
+ }
7503
7855
 
7504
7856
  // src/cli/commands/setup.ts
7505
7857
  var setupCommand = {
7506
7858
  name: "setup",
7507
- usage: () => ["[--service]"],
7859
+ usage: () => [`[--agent ${AGENTS.join("|")}] [--service]`, `--remove-manual [--agent ${AGENTS.join("|")}]`],
7508
7860
  run: runSetup
7509
7861
  };
7510
7862
  async function runSetup(args, io) {
7511
- const { service } = parseOptions(io.language, args, { service: { type: "boolean" } });
7512
- const setupDone = await runSetupSteps(io);
7513
- const serviceCode = service ? await installService(io) : EXIT.ok;
7514
- return setupDone ? serviceCode : EXIT.failed;
7863
+ const options = parseOptions(io.language, args, { service: { type: "boolean" }, agent: { type: "string" }, "remove-manual": { type: "boolean" } });
7864
+ if (options["remove-manual"] && options.service) throw new UsageError(cliMessages(io.language).removeManualWithService);
7865
+ const agents = await targetAgents(options.agent, io);
7866
+ const outcomes = [];
7867
+ for (const agent of agents) outcomes.push(options["remove-manual"] ? await removeManualSetup(agent, agents, io) : await setUpAgent(agent, io));
7868
+ const serviceCode = options.service ? await installService(io) : EXIT.ok;
7869
+ return outcomes.every(Boolean) ? serviceCode : EXIT.failed;
7515
7870
  }
7516
- async function runSetupSteps(io) {
7871
+ async function targetAgents(option, io) {
7517
7872
  const messages = cliMessages(io.language);
7518
- const skillsDir = claudeSkillsDir(io.env, io.home);
7519
- const target = join29(skillsDir, "backlog");
7873
+ if (option !== void 0) return [parseChoice(io.language, option, AGENTS, messages.optionLabel.agent)];
7874
+ const detection = await detectAgents(io);
7875
+ for (const { agent, dir } of detection.missing) agentVoice(agent, io).print(messages.agentNotFound(dir));
7876
+ return detection.found;
7877
+ }
7878
+ function agentVoice(agent, io) {
7879
+ const label = AGENT_LABELS[agent];
7880
+ return { print: (line) => io.print(`${label}: ${line}`), warn: (line) => io.warn(`${label}: ${line}`) };
7881
+ }
7882
+ async function setUpAgent(agent, io) {
7883
+ const voice = agentVoice(agent, io);
7884
+ const plugin = await agentPlugin(agent, io);
7885
+ if (plugin !== null) {
7886
+ voice.print(cliMessages(io.language).pluginManages(plugin));
7887
+ return true;
7888
+ }
7889
+ if (!await linkAgentSkill(agent, io, voice)) return false;
7890
+ const hook = await installAgentHook(agent, io);
7891
+ const reported = reportHook(hook, agentHookConfigPath(agent, io), io, voice);
7892
+ if (agent === "codex" && (hook === "added" || hook === "updated")) voice.print(cliMessages(io.language).codexHookApproval);
7893
+ return reported;
7894
+ }
7895
+ async function linkAgentSkill(agent, io, voice) {
7896
+ const messages = cliMessages(io.language);
7897
+ const skillsDir = agentSkillsDir(agent, io);
7898
+ const target = join32(skillsDir, "backlog");
7520
7899
  const source = skillSourceDir(io.packageRoot, io.language);
7521
7900
  let link2;
7522
7901
  try {
7523
7902
  link2 = await linkSkillFor(io.language, { skillsDir, packageRoot: io.packageRoot, platform: io.platform });
7524
7903
  } catch (error) {
7525
- io.warn(messages.installSkillLinkFailed(target, errorCodeOrText(error)));
7904
+ voice.warn(messages.installSkillLinkFailed(target, errorCodeOrText(error)));
7526
7905
  return false;
7527
7906
  }
7528
7907
  if (link2 === "foreign") {
7529
- io.warn(messages.installSkillForeign(target, source));
7908
+ voice.warn(messages.installSkillForeign(target, source));
7530
7909
  return false;
7531
7910
  }
7532
- io.print(link2 === "linked" ? messages.installSkillLinked(target, source) : messages.installSkillKept(target));
7533
- const settingsPath = claudeSettingsPath(io.env, io.home);
7534
- return reportHook(await addStopHook(settingsPath, io.platform), settingsPath, io);
7911
+ voice.print(link2 === "linked" ? messages.installSkillLinked(target, source) : messages.installSkillKept(target));
7912
+ await removeLegacySkillLinks(agent, io, voice);
7913
+ return true;
7535
7914
  }
7536
- function reportHook(result, settingsPath, io) {
7537
- const messages = cliMessages(io.language);
7538
- if (result === "added") {
7539
- io.print(messages.installHookAdded(settingsPath));
7540
- return true;
7915
+ async function removeLegacySkillLinks(agent, io, voice) {
7916
+ for (const legacyDir of legacySkillsDirs(agent, io)) {
7917
+ if (await unlinkOurSkill(legacyDir) === "removed") voice.print(cliMessages(io.language).manualSkillRemoval.removed(join32(legacyDir, "backlog")));
7541
7918
  }
7542
- if (result === "exists") {
7543
- io.print(messages.installHookExists(settingsPath));
7919
+ }
7920
+ function reportHook(result, configPath, io, voice) {
7921
+ const messages = cliMessages(io.language);
7922
+ if (typeof result === "string") {
7923
+ const report = { added: messages.installHookAdded, exists: messages.installHookExists, updated: messages.installHookUpdated }[result];
7924
+ voice.print(report(configPath));
7544
7925
  return true;
7545
7926
  }
7546
7927
  if (result.failed === "unreadable") {
7547
- io.warn(messages.installSettingsUnreadable(settingsPath, result.code));
7928
+ voice.warn(messages.installHookConfigUnreadable(configPath, result.code));
7548
7929
  return false;
7549
7930
  }
7550
- io.warn(messages.installSettingsInvalid(settingsPath));
7931
+ voice.warn(messages.installHookConfigInvalid(configPath));
7932
+ return false;
7933
+ }
7934
+ async function removeManualSetup(agent, removing, io) {
7935
+ const voice = agentVoice(agent, io);
7936
+ const messages = cliMessages(io.language);
7937
+ const skillsDir = agentSkillsDir(agent, io);
7938
+ const target = join32(skillsDir, "backlog");
7939
+ const sharer = await remainingSkillDirUser(agent, removing, io);
7940
+ voice.print(sharer === null ? messages.manualSkillRemoval[await unlinkOurSkill(skillsDir)](target) : messages.manualSkillShared(target, AGENT_LABELS[sharer]));
7941
+ await removeLegacySkillLinks(agent, io, voice);
7942
+ return reportHookRemoval(await removeAgentHook(agent, io), agentHookConfigPath(agent, io), io, voice);
7943
+ }
7944
+ async function remainingSkillDirUser(agent, removing, io) {
7945
+ const skillsDir = agentSkillsDir(agent, io);
7946
+ const { found } = await detectAgents(io);
7947
+ return found.find((other) => !removing.includes(other) && agentSkillsDir(other, io) === skillsDir) ?? null;
7948
+ }
7949
+ function reportHookRemoval(result, configPath, io, voice) {
7950
+ const messages = cliMessages(io.language);
7951
+ if (result === "removed" || result === "absent") {
7952
+ voice.print(messages.manualHookRemoval[result](configPath));
7953
+ return true;
7954
+ }
7955
+ voice.warn(result.failed === "unreadable" ? messages.removeHookConfigUnreadable(configPath, result.code) : messages.removeHookConfigInvalid(configPath));
7551
7956
  return false;
7552
7957
  }
7553
7958