p-backlog 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -555,7 +555,7 @@ var cliEn = {
555
555
  runNotRecorded: (error) => `Could not record the run: ${error}`,
556
556
  runsNotTrimmed: (error) => `Could not trim the run log: ${error}`,
557
557
  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" },
558
+ optionLabel: { status: "status", category: "category", priority: "priority", language: "language", agent: "agent" },
559
559
  skillForeign: (target) => `${target} is a foreign directory, the skill link was not moved`,
560
560
  installSkillLinked: (target, source) => `Skill installed: ${target} \u2192 ${source}`,
561
561
  installSkillKept: (target) => `Skill already installed: ${target}`,
@@ -563,8 +563,25 @@ var cliEn = {
563
563
  installSkillLinkFailed: (target, detail) => `Could not create the symlink ${target} (${detail}).`,
564
564
  installHookExists: (settingsPath) => `Stop hook is already present in ${settingsPath}`,
565
565
  installHookAdded: (settingsPath) => `Stop hook added to ${settingsPath}`,
566
+ installHookUpdated: (settingsPath) => `Stop hook updated in ${settingsPath}`,
567
+ codexHookApproval: "approve the hook in Codex: /hooks",
566
568
  installSettingsUnreadable: (settingsPath, detail) => `Could not read ${settingsPath} (${detail}), Stop hook not added.`,
567
569
  installSettingsInvalid: (settingsPath) => `${settingsPath} is not a JSON object, Stop hook not added. Fix the file and try again.`,
570
+ agentNotFound: (dir) => `not found (${dir})`,
571
+ pluginManages: (plugin) => `the ${plugin} plugin provides the skill and the hook. Remove the manual ones with: backlog setup --remove-manual`,
572
+ pluginLanguageHint: (installed, wanted) => `the skill comes from the ${installed} plugin; for this language switch plugins: /plugin uninstall ${installed}, then /plugin install ${wanted}`,
573
+ removeManualWithService: "--remove-manual cannot be combined with --service: remove the service with backlog service uninstall",
574
+ manualSkillRemoval: {
575
+ removed: (target) => `skill link removed: ${target}`,
576
+ absent: (target) => `no skill link: ${target}`,
577
+ foreign: (target) => `${target} is not a p-backlog link, left as is`
578
+ },
579
+ manualHookRemoval: {
580
+ removed: (path) => `Stop hook removed from ${path}`,
581
+ absent: (path) => `no p-backlog Stop hook in ${path}`
582
+ },
583
+ hookConfigUnreadable: (path, detail) => `Could not read ${path} (${detail}), the hook was not removed.`,
584
+ hookConfigInvalid: (path) => `${path} is not a JSON object, the hook was not removed. Fix the file and try again.`,
568
585
  serviceInstalled: (file) => `Service installed: ${file}`,
569
586
  serviceLogs: (path) => `Logs: ${path}`,
570
587
  serviceUninstalled: "Service removed",
@@ -659,11 +676,12 @@ var cliEn = {
659
676
  alreadyInStatus: (id, status) => `${id} is already ${status}`,
660
677
  blockedByOpenTasks: (id) => `${id} is blocked by open tasks:`,
661
678
  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)`,
679
+ hookUsage: (stopEvent, agents) => `${stopEvent} [--agent ${agents.join("|")}] (for the Stop hook in Claude Code, Codex or Cursor, reads the event from stdin)`,
663
680
  sessionShownReadFailed: (error) => `Could not read the tasks already shown this session: ${error}`,
664
681
  sessionShownWriteFailed: (error) => `Could not remember the tasks shown this session: ${error}`,
665
682
  alertsComputeFailed: (error) => `Could not compute alerts: ${error}`,
666
683
  alertsShownWriteFailed: (error) => `Could not save the shown alerts: ${error}`,
684
+ hookTurnClaimFailed: (error) => `Could not record the turn for the double-hook guard: ${error}`,
667
685
  newUsage: (types, priorities, found) => [
668
686
  `--title <title> [--type ${types}] [--priority ${priorities}] [--tags a,b]`,
669
687
  `--category <category> (required for tasks) [--found ${found}]`,
@@ -713,7 +731,7 @@ var cliRu = {
713
731
  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
732
  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
733
  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" },
734
+ 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
735
  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
736
  installSkillLinked: (target, source) => `\u0421\u043A\u0438\u043B\u043B \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D: ${target} \u2192 ${source}`,
719
737
  installSkillKept: (target) => `\u0421\u043A\u0438\u043B\u043B \u0443\u0436\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D: ${target}`,
@@ -721,8 +739,25 @@ var cliRu = {
721
739
  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
740
  installHookExists: (settingsPath) => `\u0425\u0443\u043A Stop \u0443\u0436\u0435 \u0435\u0441\u0442\u044C \u0432 ${settingsPath}`,
723
741
  installHookAdded: (settingsPath) => `\u0425\u0443\u043A Stop \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D \u0432 ${settingsPath}`,
742
+ installHookUpdated: (settingsPath) => `\u0425\u0443\u043A Stop \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D \u0432 ${settingsPath}`,
743
+ codexHookApproval: "\u043E\u0434\u043E\u0431\u0440\u0438\u0442\u0435 \u0445\u0443\u043A \u0432 Codex: /hooks",
724
744
  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
745
  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.`,
746
+ agentNotFound: (dir) => `\u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D (${dir})`,
747
+ 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`,
748
+ 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}`,
749
+ 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",
750
+ manualSkillRemoval: {
751
+ removed: (target) => `\u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u0441\u043A\u0438\u043B\u043B \u0441\u043D\u044F\u0442\u0430: ${target}`,
752
+ absent: (target) => `\u0441\u0441\u044B\u043B\u043A\u0438 \u043D\u0430 \u0441\u043A\u0438\u043B\u043B \u043D\u0435\u0442: ${target}`,
753
+ foreign: (target) => `${target} \u2014 \u043D\u0435 \u0441\u0441\u044B\u043B\u043A\u0430 p-backlog, \u043D\u0435 \u0442\u0440\u043E\u043D\u0443\u0442`
754
+ },
755
+ manualHookRemoval: {
756
+ removed: (path) => `\u0445\u0443\u043A Stop \u0441\u043D\u044F\u0442 \u0438\u0437 ${path}`,
757
+ absent: (path) => `\u0445\u0443\u043A\u0430 Stop p-backlog \u0432 ${path} \u043D\u0435\u0442`
758
+ },
759
+ hookConfigUnreadable: (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.`,
760
+ hookConfigInvalid: (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
761
  serviceInstalled: (file) => `\u0421\u043B\u0443\u0436\u0431\u0430 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430: ${file}`,
727
762
  serviceLogs: (path) => `\u041B\u043E\u0433\u0438: ${path}`,
728
763
  serviceUninstalled: "\u0421\u043B\u0443\u0436\u0431\u0430 \u0443\u0434\u0430\u043B\u0435\u043D\u0430",
@@ -829,11 +864,12 @@ var cliRu = {
829
864
  alreadyInStatus: (id, status) => `${id} \u0443\u0436\u0435 \u0432 \u0441\u0442\u0430\u0442\u0443\u0441\u0435 ${status}`,
830
865
  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
866
  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)`,
867
+ 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
868
  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
869
  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
870
  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
871
  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}`,
872
+ 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
873
  newUsage: (types, priorities, found) => [
838
874
  `--title <\u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A> [--type ${types}] [--priority ${priorities}] [--tags a,b]`,
839
875
  `--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}]`,
@@ -3874,7 +3910,11 @@ async function fixCommitFound(loaded, task, reason, io) {
3874
3910
  }
3875
3911
 
3876
3912
  // src/cli/commands/config.ts
3877
- import { join as join17 } from "node:path";
3913
+ import { join as join18 } from "node:path";
3914
+
3915
+ // src/cli/agents/agent.ts
3916
+ import { stat as stat4 } from "node:fs/promises";
3917
+ import { join as join16 } from "node:path";
3878
3918
 
3879
3919
  // src/core/claude-dir.ts
3880
3920
  import { join as join15 } from "node:path";
@@ -3891,18 +3931,67 @@ function claudeProjectsDir(env, home2) {
3891
3931
  return join15(claudeDir(env, home2), "projects");
3892
3932
  }
3893
3933
 
3934
+ // src/cli/agents/agent.ts
3935
+ var AGENTS = ["claude", "codex", "cursor"];
3936
+ var AGENT_LABELS = { claude: "Claude Code", codex: "Codex", cursor: "Cursor" };
3937
+ function agentHomeDir(agent, env, home2) {
3938
+ if (agent === "claude") return claudeDir(env, home2);
3939
+ if (agent === "codex") return env.CODEX_HOME || join16(home2, ".codex");
3940
+ return join16(home2, ".cursor");
3941
+ }
3942
+ function agentSkillsDir(agent, env, home2) {
3943
+ if (agent === "claude") return claudeSkillsDir(env, home2);
3944
+ if (agent === "codex") return join16(home2, ".agents", "skills");
3945
+ return join16(agentHomeDir(agent, env, home2), "skills");
3946
+ }
3947
+ function legacySkillsDirs(agent, env, home2) {
3948
+ return agent === "codex" ? [join16(agentHomeDir(agent, env, home2), "skills")] : [];
3949
+ }
3950
+ async function detectAgents(env, home2) {
3951
+ const detection = { found: [], missing: [] };
3952
+ for (const agent of AGENTS) {
3953
+ const dir = agentHomeDir(agent, env, home2);
3954
+ const present = agent === "claude" || ((await stat4(dir).catch(() => null))?.isDirectory() ?? false);
3955
+ if (present) detection.found.push(agent);
3956
+ else detection.missing.push({ agent, dir });
3957
+ }
3958
+ return detection;
3959
+ }
3960
+
3961
+ // src/cli/agents/claude-plugin.ts
3962
+ import { z as z8 } from "zod";
3963
+ var PLUGIN_BY_LANGUAGE = { en: "p-backlog", ru: "p-backlog-ru" };
3964
+ var PLUGIN_NAMES = new Set(Object.values(PLUGIN_BY_LANGUAGE));
3965
+ var MARKETPLACE_SEPARATOR = "@";
3966
+ var enabledPluginsSchema = z8.object({ enabledPlugins: z8.record(z8.string(), z8.unknown()).optional() });
3967
+ async function agentPlugin(agent, env, home2) {
3968
+ if (agent !== "claude") return null;
3969
+ const settings = await readJsonFile(claudeSettingsPath(env, home2), enabledPluginsSchema);
3970
+ const enabled = Object.entries(settings?.enabledPlugins ?? {}).find(([id, on]) => on === true && PLUGIN_NAMES.has(pluginName(id)));
3971
+ return enabled?.[0] ?? null;
3972
+ }
3973
+ function pluginToSwitchTo(pluginId, language) {
3974
+ const wanted = PLUGIN_BY_LANGUAGE[language];
3975
+ if (pluginName(pluginId) === wanted) return null;
3976
+ const marketplace = pluginId.slice(pluginId.indexOf(MARKETPLACE_SEPARATOR) + 1);
3977
+ return `${wanted}${MARKETPLACE_SEPARATOR}${marketplace}`;
3978
+ }
3979
+ function pluginName(pluginId) {
3980
+ return pluginId.split(MARKETPLACE_SEPARATOR)[0] ?? pluginId;
3981
+ }
3982
+
3894
3983
  // src/cli/skill-link.ts
3895
3984
  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";
3985
+ import { basename as basename8, dirname as dirname5, join as join17, resolve as resolve3 } from "node:path";
3986
+ import { z as z9 } from "zod";
3898
3987
  var SKILL_VARIANTS = /* @__PURE__ */ new Set(["backlog", "backlog-en"]);
3899
- var packageManifestSchema = z8.object({ name: z8.string() });
3988
+ var packageManifestSchema = z9.object({ name: z9.string() });
3900
3989
  function skillSourceDir(packageRoot2, language) {
3901
- return join16(packageRoot2, "skill", language === "ru" ? "backlog" : "backlog-en");
3990
+ return join17(packageRoot2, "skill", language === "ru" ? "backlog" : "backlog-en");
3902
3991
  }
3903
3992
  async function isPBacklogSkill(path) {
3904
3993
  if (!SKILL_VARIANTS.has(basename8(path)) || basename8(dirname5(path)) !== "skill") return false;
3905
- const manifest = await readTextOrNull(join16(dirname5(dirname5(path)), "package.json"));
3994
+ const manifest = await readTextOrNull(join17(dirname5(dirname5(path)), "package.json"));
3906
3995
  if (manifest !== null) return parseJson(manifest, packageManifestSchema)?.name === "p-backlog";
3907
3996
  return !await pathExists(path);
3908
3997
  }
@@ -3910,7 +3999,7 @@ async function pathExists(path) {
3910
3999
  return await lstat(path).catch(() => null) !== null;
3911
4000
  }
3912
4001
  async function linkSkillFor(language, { skillsDir, packageRoot: packageRoot2, platform }) {
3913
- const target = join16(skillsDir, "backlog");
4002
+ const target = join17(skillsDir, "backlog");
3914
4003
  const source = skillSourceDir(packageRoot2, language);
3915
4004
  const existing = await lstat(target).catch(() => null);
3916
4005
  if (existing !== null && !existing.isSymbolicLink()) return "foreign";
@@ -3924,6 +4013,18 @@ async function linkSkillFor(language, { skillsDir, packageRoot: packageRoot2, pl
3924
4013
  await symlink(source, target, platform === "win32" ? "junction" : "dir");
3925
4014
  return "linked";
3926
4015
  }
4016
+ async function relinkExistingSkill(language, options) {
4017
+ if (!await pathExists(join17(options.skillsDir, "backlog"))) return "absent";
4018
+ return linkSkillFor(language, options);
4019
+ }
4020
+ async function unlinkOurSkill(skillsDir) {
4021
+ const target = join17(skillsDir, "backlog");
4022
+ const existing = await lstat(target).catch(() => null);
4023
+ if (existing === null) return "absent";
4024
+ if (!existing.isSymbolicLink() || !await isPBacklogSkill(resolve3(skillsDir, await readlink(target)))) return "foreign";
4025
+ await unlink(target);
4026
+ return "removed";
4027
+ }
3927
4028
 
3928
4029
  // src/cli/commands/config.ts
3929
4030
  var configCommand = {
@@ -3947,11 +4048,29 @@ async function runLanguage(positionals, io) {
3947
4048
  const language = parseChoice(io.language, value, LANGUAGES, cliMessages(io.language).optionLabel.language);
3948
4049
  await writeSettings(io.backlogRoot, { language });
3949
4050
  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")));
4051
+ const { found } = await detectAgents(io.env, io.home);
4052
+ for (const agent of found) await relinkSkill(agent, language, io);
3953
4053
  return EXIT.ok;
3954
4054
  }
4055
+ async function relinkSkill(agent, language, io) {
4056
+ const messages = cliMessages(language);
4057
+ const label = AGENT_LABELS[agent];
4058
+ const plugin = await agentPlugin(agent, io.env, io.home);
4059
+ if (plugin !== null) {
4060
+ const wanted = pluginToSwitchTo(plugin, language);
4061
+ if (wanted !== null) io.print(`${label}: ${messages.pluginLanguageHint(plugin, wanted)}`);
4062
+ return;
4063
+ }
4064
+ const skillsDir = agentSkillsDir(agent, io.env, io.home);
4065
+ const target = join18(skillsDir, "backlog");
4066
+ const options = { skillsDir, packageRoot: io.packageRoot, platform: io.platform };
4067
+ try {
4068
+ const result = agent === "claude" ? await linkSkillFor(language, options) : await relinkExistingSkill(language, options);
4069
+ if (result === "foreign") io.warn(`${label}: ${messages.skillForeign(target)}`);
4070
+ } catch (error) {
4071
+ io.warn(`${label}: ${messages.installSkillLinkFailed(target, errorCodeOrText(error))}`);
4072
+ }
4073
+ }
3955
4074
 
3956
4075
  // src/cli/apply-all.ts
3957
4076
  async function applyAll(items, apply) {
@@ -4004,31 +4123,47 @@ function epicWord(cli, epic) {
4004
4123
 
4005
4124
  // src/cli/commands/hook.ts
4006
4125
  import { dirname as dirname6 } from "node:path";
4007
- import { z as z11 } from "zod";
4126
+
4127
+ // src/core/store/hook-turns.ts
4128
+ import { join as join19 } from "node:path";
4129
+ import { z as z10 } from "zod";
4130
+ var HOOK_TURNS_FILE = ".hook-turns.json";
4131
+ var CLAIM_WINDOW_MS = 6e4;
4132
+ var hookTurnsSchema = z10.record(z10.string(), z10.iso.datetime({ offset: true }));
4133
+ async function claimHookTurn(backlogRoot2, turnKey, now) {
4134
+ const path = join19(backlogRoot2, HOOK_TURNS_FILE);
4135
+ return withFileLock(path, async () => {
4136
+ const turns = await readJsonFile(path, hookTurnsSchema) ?? {};
4137
+ const claimed = Object.entries(turns).filter(([, at]) => now.getTime() - Date.parse(at) < CLAIM_WINDOW_MS);
4138
+ if (claimed.some(([key]) => key === turnKey)) return false;
4139
+ await writeJsonFile(path, { ...Object.fromEntries(claimed), [turnKey]: now.toISOString() });
4140
+ return true;
4141
+ });
4142
+ }
4008
4143
 
4009
4144
  // src/core/store/signals-shown.ts
4010
- import { join as join18 } from "node:path";
4011
- import { z as z9 } from "zod";
4145
+ import { join as join20 } from "node:path";
4146
+ import { z as z11 } from "zod";
4012
4147
  var SIGNALS_SHOWN_FILE = ".signals-shown.json";
4013
- var shownSchema = z9.record(z9.string(), z9.string());
4148
+ var shownSchema = z11.record(z11.string(), z11.string());
4014
4149
  async function readSignalsShown(projectDir2) {
4015
- return await readJsonFile(join18(projectDir2, SIGNALS_SHOWN_FILE), shownSchema) ?? {};
4150
+ return await readJsonFile(join20(projectDir2, SIGNALS_SHOWN_FILE), shownSchema) ?? {};
4016
4151
  }
4017
4152
  async function writeSignalsShown(projectDir2, shown) {
4018
- await writeJsonFile(join18(projectDir2, SIGNALS_SHOWN_FILE), shown);
4153
+ await writeJsonFile(join20(projectDir2, SIGNALS_SHOWN_FILE), shown);
4019
4154
  }
4020
4155
 
4021
4156
  // src/core/store/session-shown.ts
4022
- import { join as join19 } from "node:path";
4023
- import { z as z10 } from "zod";
4157
+ import { join as join21 } from "node:path";
4158
+ import { z as z12 } from "zod";
4024
4159
  var SESSION_SHOWN_FILE = ".candidates-shown.json";
4025
4160
  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 }) }));
4161
+ var sessionsShownSchema = z12.record(z12.string(), z12.object({ tasks: z12.array(z12.string()), at: z12.iso.datetime({ offset: true }) }));
4027
4162
  async function readSessionShown(projectDir2, session) {
4028
4163
  return (await readSessions(projectDir2))[session]?.tasks ?? [];
4029
4164
  }
4030
4165
  async function rememberSessionShown(projectDir2, session, taskIds, now) {
4031
- const path = join19(projectDir2, SESSION_SHOWN_FILE);
4166
+ const path = join21(projectDir2, SESSION_SHOWN_FILE);
4032
4167
  await withFileLock(path, async () => {
4033
4168
  const sessions = await readSessions(projectDir2);
4034
4169
  const recent = Object.entries(sessions).filter(([, shown]) => now.getTime() - Date.parse(shown.at) < SESSION_RETENTION_MS);
@@ -4037,7 +4172,7 @@ async function rememberSessionShown(projectDir2, session, taskIds, now) {
4037
4172
  });
4038
4173
  }
4039
4174
  async function readSessions(projectDir2) {
4040
- return await readJsonFile(join19(projectDir2, SESSION_SHOWN_FILE), sessionsShownSchema) ?? {};
4175
+ return await readJsonFile(join21(projectDir2, SESSION_SHOWN_FILE), sessionsShownSchema) ?? {};
4041
4176
  }
4042
4177
 
4043
4178
  // src/core/stats/numbers.ts
@@ -4315,6 +4450,56 @@ function markShown(shown, signals, today) {
4315
4450
  return { ...shown, ...Object.fromEntries(signals.map((signal3) => [signal3.kind, today])) };
4316
4451
  }
4317
4452
 
4453
+ // src/cli/agents/stop-event.ts
4454
+ import { createHash as createHash4 } from "node:crypto";
4455
+ import { z as z13 } from "zod";
4456
+ var STOP_PROTOCOL = { claude: "claude", codex: "claude", cursor: "cursor" };
4457
+ var TURN_DIGEST_LENGTH = 16;
4458
+ var CURSOR_COMPLETED = "completed";
4459
+ var claudeEventSchema = z13.object({
4460
+ cwd: z13.string(),
4461
+ session_id: z13.string().optional(),
4462
+ turn_id: z13.string().optional(),
4463
+ stop_hook_active: z13.boolean().optional(),
4464
+ last_assistant_message: z13.string().nullish()
4465
+ });
4466
+ var cursorEventSchema = z13.object({
4467
+ workspace_roots: z13.tuple([z13.string()], z13.string()),
4468
+ conversation_id: z13.string().optional(),
4469
+ generation_id: z13.string().optional(),
4470
+ status: z13.string().optional(),
4471
+ loop_count: z13.number().optional()
4472
+ });
4473
+ function parseStopEvent(agent, text) {
4474
+ return STOP_PROTOCOL[agent] === "cursor" ? parseCursorEvent(text) : parseClaudeEvent(text);
4475
+ }
4476
+ function carriesSystemMessage(agent) {
4477
+ return STOP_PROTOCOL[agent] === "claude";
4478
+ }
4479
+ function formatStopAnswer(agent, answer) {
4480
+ if (STOP_PROTOCOL[agent] === "cursor") return answer.reason === null ? null : JSON.stringify({ followup_message: answer.reason });
4481
+ const response = {
4482
+ ...answer.reason !== null ? { decision: "block", reason: answer.reason } : {},
4483
+ ...answer.systemMessage !== null ? { systemMessage: answer.systemMessage } : {}
4484
+ };
4485
+ return Object.keys(response).length > 0 ? JSON.stringify(response) : null;
4486
+ }
4487
+ function parseClaudeEvent(text) {
4488
+ const event = parseJson(text, claudeEventSchema);
4489
+ if (event === null) return null;
4490
+ return { cwd: event.cwd, session: event.session_id, turn: event.turn_id ?? digest(event.last_assistant_message), skip: event.stop_hook_active === true };
4491
+ }
4492
+ function parseCursorEvent(text) {
4493
+ const event = parseJson(text, cursorEventSchema);
4494
+ if (event === null) return null;
4495
+ const followsOurFollowUp = (event.loop_count ?? 0) > 0;
4496
+ const interrupted = (event.status ?? CURSOR_COMPLETED) !== CURSOR_COMPLETED;
4497
+ return { cwd: event.workspace_roots[0], session: event.conversation_id, turn: event.generation_id, skip: followsOurFollowUp || interrupted };
4498
+ }
4499
+ function digest(text) {
4500
+ return text ? createHash4("sha256").update(text).digest("hex").slice(0, TURN_DIGEST_LENGTH) : void 0;
4501
+ }
4502
+
4318
4503
  // src/cli/stop-reason.ts
4319
4504
  var STOP_REASON_LIMIT = 500;
4320
4505
  function stopReason(language, projectId, candidates) {
@@ -4343,37 +4528,50 @@ function briefEvidence(cli, candidate) {
4343
4528
  }
4344
4529
 
4345
4530
  // src/cli/commands/hook.ts
4346
- var stopEventSchema = z11.object({ cwd: z11.string(), session_id: z11.string().optional(), stop_hook_active: z11.boolean().optional() });
4531
+ var DEFAULT_AGENT = "claude";
4347
4532
  var hookCommand = {
4348
4533
  name: "hook",
4349
- usage: (language) => [cliMessages(language).hookUsage(HOOK_STOP_EVENT)],
4534
+ usage: (language) => [cliMessages(language).hookUsage(HOOK_STOP_EVENT, AGENTS)],
4350
4535
  run: runHook,
4351
4536
  failureExit: EXIT.ok
4352
4537
  };
4353
4538
  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;
4539
+ const { values, positionals } = parseCommandArgs(io.language, args, { agent: { type: "string" } });
4540
+ if (positionals.length !== 1 || positionals[0] !== HOOK_STOP_EVENT) throw usageError(hookCommand, io.language);
4541
+ const agent = values.agent === void 0 ? DEFAULT_AGENT : parseChoice(io.language, values.agent, AGENTS, cliMessages(io.language).optionLabel.agent);
4542
+ const event = parseStopEvent(agent, await io.readStdin());
4543
+ if (event === null || event.skip) return EXIT.ok;
4357
4544
  const loaded = await loadBacklog(io.backlogRoot);
4358
4545
  const project = findProjectForDir(loaded.projects, event.cwd, io.home);
4359
4546
  if (!project) return EXIT.ok;
4547
+ if (!await isFirstHookOfTurn(agent, event, io)) return EXIT.ok;
4360
4548
  const messages = coreMessages(io.language);
4361
4549
  const { candidates } = await checkBacklog(io.backlogRoot, loaded, { projectIds: [project.id], mode: "changed", now: io.now(), home: io.home, messages, workingDir: event.cwd });
4362
4550
  const lowPriority = new Set(loaded.tasks.filter((task) => task.priority === "low").map((task) => task.id));
4363
4551
  const worthTelling = candidates.filter((candidate) => !lowPriority.has(candidate.task.id));
4364
4552
  const lowCount = candidates.length - worthTelling.length;
4365
- const session = await sessionMemory(project, event.session_id, io);
4553
+ const session = await sessionMemory(project, event.session, io);
4366
4554
  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));
4555
+ const signals = carriesSystemMessage(agent) ? await freshSignals(project, loaded.tasks.filter((task) => task.projectId === project.id), lowChangedSignals(lowCount), io) : NO_SIGNALS;
4556
+ const answer = formatStopAnswer(agent, {
4557
+ reason: blocking.length > 0 ? stopReason(io.language, project.id, blocking) : null,
4558
+ systemMessage: signals.fresh.length > 0 ? hookMessage(io.language, project.id, signals.fresh.map((signal3) => messages.signal(signal3)).join("; ")) : null
4559
+ });
4560
+ if (answer !== null) io.print(answer);
4373
4561
  await signals.remember();
4374
4562
  await session.remember(blocking.map((candidate) => candidate.task.id));
4375
4563
  return EXIT.ok;
4376
4564
  }
4565
+ async function isFirstHookOfTurn(agent, event, io) {
4566
+ if (event.session === void 0 || event.turn === void 0) return true;
4567
+ try {
4568
+ return await claimHookTurn(io.backlogRoot, `${agent}:${event.session}:${event.turn}`, io.now());
4569
+ } catch (error) {
4570
+ if (error instanceof FileBusyError) return false;
4571
+ io.warn(cliMessages(io.language).hookTurnClaimFailed(errorText(error)));
4572
+ return true;
4573
+ }
4574
+ }
4377
4575
  async function sessionMemory(project, session, io) {
4378
4576
  if (session === void 0) return { told: /* @__PURE__ */ new Set(), remember: () => Promise.resolve() };
4379
4577
  const projectDir2 = dirname6(project.path);
@@ -4389,6 +4587,7 @@ async function sessionMemory(project, session, io) {
4389
4587
  return { told: /* @__PURE__ */ new Set(), remember: () => Promise.resolve() };
4390
4588
  }
4391
4589
  }
4590
+ var NO_SIGNALS = { fresh: [], remember: () => Promise.resolve() };
4392
4591
  function lowChangedSignals(count3) {
4393
4592
  if (count3 === 0) return [];
4394
4593
  return [{ kind: "low-changed", params: { count: count3 } }];
@@ -4788,10 +4987,10 @@ function listenFailure(error, port, messages) {
4788
4987
  // src/server/start.ts
4789
4988
  import { serve } from "@hono/node-server";
4790
4989
  import { mkdir as mkdir6, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
4791
- import { join as join25 } from "node:path";
4990
+ import { join as join27 } from "node:path";
4792
4991
 
4793
4992
  // src/core/store/sweep.ts
4794
- import { dirname as dirname7, join as join20 } from "node:path";
4993
+ import { dirname as dirname7, join as join22 } from "node:path";
4795
4994
  async function sweepClosed(root, now, messages) {
4796
4995
  const initial = await loadBacklog(root);
4797
4996
  const reopening = await reopenEpicsWithOpenTasks(initial, now);
@@ -4814,7 +5013,7 @@ async function sweepClosed(root, now, messages) {
4814
5013
  }
4815
5014
  async function removeAbandonedTemporaries(root, now) {
4816
5015
  const cutoff = new Date(now.getTime() - DAY_MS);
4817
- const projectDirs = (await listDir(root)).filter((entry) => entry.isDirectory()).map((entry) => join20(root, entry.name));
5016
+ const projectDirs = (await listDir(root)).filter((entry) => entry.isDirectory()).map((entry) => join22(root, entry.name));
4818
5017
  for (const dir of [root, ...projectDirs]) await removeTemporariesBefore(dir, cutoff);
4819
5018
  }
4820
5019
  async function removeWithReferences(tasks, removable, now) {
@@ -4938,49 +5137,49 @@ async function reserveNumbers(projects, expired) {
4938
5137
  import { serveStatic } from "@hono/node-server/serve-static";
4939
5138
  import { Hono as Hono3 } from "hono";
4940
5139
  import { secureHeaders } from "hono/secure-headers";
4941
- import { join as join22 } from "node:path";
5140
+ import { join as join24 } from "node:path";
4942
5141
 
4943
5142
  // src/server/api.ts
4944
5143
  import { Hono as Hono2 } from "hono";
4945
5144
  import { streamSSE } from "hono/streaming";
4946
5145
 
4947
5146
  // 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(),
5147
+ import { z as z14 } from "zod";
5148
+ var idList = z14.array(taskIdSchema);
5149
+ var taskChangesSchema = z14.strictObject({
5150
+ title: z14.string().optional(),
5151
+ type: z14.enum(TASK_TYPES).optional(),
5152
+ status: z14.enum(TASK_STATUSES).optional(),
5153
+ priority: z14.enum(PRIORITIES).optional(),
5154
+ tags: z14.array(z14.string()).optional(),
4956
5155
  epic: taskIdSchema.nullable().optional(),
4957
5156
  blockedBy: idList.optional(),
4958
5157
  related: idList.optional(),
4959
- body: z12.string().optional(),
4960
- category: z12.enum(TASK_CATEGORIES).nullable().optional()
5158
+ body: z14.string().optional(),
5159
+ category: z14.enum(TASK_CATEGORIES).nullable().optional()
4961
5160
  });
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),
5161
+ var projectActiveSchema = z14.strictObject({ active: z14.boolean() });
5162
+ var projectDeleteSchema = z14.strictObject({ confirm: z14.string() });
5163
+ var settingsRequestSchema = z14.strictObject(settingsSchema.shape);
5164
+ var updateTaskRequestSchema = z14.strictObject({
5165
+ version: z14.string().min(1),
4967
5166
  changes: taskChangesSchema
4968
5167
  });
4969
- var batchPreviousSchema = z12.strictObject({
4970
- status: z12.enum(TASK_STATUSES),
4971
- priority: z12.enum(PRIORITIES),
5168
+ var batchPreviousSchema = z14.strictObject({
5169
+ status: z14.enum(TASK_STATUSES),
5170
+ priority: z14.enum(PRIORITIES),
4972
5171
  epic: taskIdSchema.nullable(),
4973
- resolution: z12.enum(RESOLUTIONS).nullable(),
4974
- reason: z12.string().nullable()
5172
+ resolution: z14.enum(RESOLUTIONS).nullable(),
5173
+ reason: z14.string().nullable()
4975
5174
  });
4976
5175
  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) })
5176
+ var batchRequestSchema = z14.strictObject({
5177
+ 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),
5178
+ action: z14.discriminatedUnion("kind", [
5179
+ z14.strictObject({ kind: z14.literal("close"), reason: z14.string().transform((reason) => reason.replace(/\s*\n\s*/g, " ").trim()).pipe(z14.string().min(1)) }),
5180
+ z14.strictObject({ kind: z14.literal("priority"), priority: z14.enum(PRIORITIES) }),
5181
+ z14.strictObject({ kind: z14.literal("epic"), epic: taskIdSchema.nullable() }),
5182
+ z14.strictObject({ kind: z14.literal("restore"), changes: z14.record(taskIdSchema, batchPreviousSchema) })
4984
5183
  ])
4985
5184
  });
4986
5185
 
@@ -5085,7 +5284,7 @@ function createReportCache({ ttlMs, now }) {
5085
5284
 
5086
5285
  // src/server/revisions.ts
5087
5286
  import { randomUUID as randomUUID3 } from "node:crypto";
5088
- import { stat as stat4 } from "node:fs/promises";
5287
+ import { stat as stat5 } from "node:fs/promises";
5089
5288
  import { basename as basename9 } from "node:path";
5090
5289
  function createRevisions(boot = randomUUID3()) {
5091
5290
  let seq = 0;
@@ -5117,7 +5316,7 @@ function createRevisions(boot = randomUUID3()) {
5117
5316
  };
5118
5317
  }
5119
5318
  async function markOf(path) {
5120
- const [text, stats] = await Promise.all([readTextOrNull(path), stat4(path).catch(() => null)]);
5319
+ const [text, stats] = await Promise.all([readTextOrNull(path), stat5(path).catch(() => null)]);
5121
5320
  return text === null || stats === null ? null : { version: contentVersion(text), mtimeMs: stats.mtimeMs };
5122
5321
  }
5123
5322
 
@@ -5125,26 +5324,26 @@ async function markOf(path) {
5125
5324
  import { Hono } from "hono";
5126
5325
 
5127
5326
  // src/core/code/code-cache.ts
5128
- import { join as join21 } from "node:path";
5129
- import { z as z13 } from "zod";
5327
+ import { join as join23 } from "node:path";
5328
+ import { z as z15 } from "zod";
5130
5329
  var CODE_CACHE_FILE = ".code-cache.json";
5131
5330
  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() }))
5331
+ var repoCodeSchema = z15.object({
5332
+ commits: z15.array(z15.array(z15.string())),
5333
+ lines: z15.array(z15.object({ path: z15.string(), lines: z15.number() })),
5334
+ units: z15.array(z15.object({ date: z15.string(), lines: z15.number() }))
5136
5335
  });
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)
5336
+ var fixCommitSchema = z15.object({ date: z15.string(), landedAt: z15.string().optional(), byAgent: z15.boolean(), lines: z15.number(), testLines: z15.number() });
5337
+ var snapshotSchema = z15.object({
5338
+ version: z15.literal(CODE_CACHE_VERSION),
5339
+ repos: z15.record(z15.string(), z15.object({ key: z15.string(), code: repoCodeSchema })),
5340
+ fixes: z15.record(z15.string(), fixCommitSchema)
5142
5341
  });
5143
5342
  function emptyCodeCache() {
5144
5343
  return { repos: {}, fixes: {} };
5145
5344
  }
5146
5345
  function createCodeCacheFile(root) {
5147
- const path = join21(root, CODE_CACHE_FILE);
5346
+ const path = join23(root, CODE_CACHE_FILE);
5148
5347
  return {
5149
5348
  read: async () => {
5150
5349
  const snapshot = await readJsonFile(path, snapshotSchema);
@@ -5541,13 +5740,13 @@ function codeFixRequests(input, base = reportBase(input)) {
5541
5740
  }
5542
5741
 
5543
5742
  // 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()
5743
+ import { z as z16 } from "zod";
5744
+ var tokenCountsSchema = z16.object({
5745
+ input: z16.number(),
5746
+ cacheWrite5m: z16.number(),
5747
+ cacheWrite1h: z16.number(),
5748
+ cacheRead: z16.number(),
5749
+ output: z16.number()
5551
5750
  });
5552
5751
  var ZERO_TOKENS = { input: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0, output: 0 };
5553
5752
  function addTokens(a, b) {
@@ -5562,24 +5761,24 @@ function totalTokens(tokens) {
5562
5761
  }
5563
5762
 
5564
5763
  // src/core/stats/cost/usage-state.ts
5565
- import { z as z15 } from "zod";
5764
+ import { z as z17 } from "zod";
5566
5765
  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]),
5766
+ var estimatedKindSchema = z17.enum(["cli", "skill"]);
5767
+ var usageBucketSchema = z17.object({
5768
+ slot: z17.string(),
5769
+ cwd: z17.string(),
5770
+ model: z17.string(),
5771
+ kind: z17.enum(["hook", ...estimatedKindSchema.options]),
5573
5772
  tokens: tokenCountsSchema,
5574
- hookTurns: z15.number()
5773
+ hookTurns: z17.number()
5575
5774
  });
5576
- var transcriptStateSchema = z15.object({
5577
- hookOpen: z15.boolean(),
5578
- lastModel: z15.string().nullable(),
5579
- lastMessageId: z15.string().nullable(),
5775
+ var transcriptStateSchema = z17.object({
5776
+ hookOpen: z17.boolean(),
5777
+ lastModel: z17.string().nullable(),
5778
+ lastMessageId: z17.string().nullable(),
5580
5779
  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() }))
5780
+ pending: z17.record(z17.string(), estimatedKindSchema),
5781
+ pendingEstimates: z17.array(z17.object({ kind: estimatedKindSchema, chars: z17.number(), slot: z17.string(), cwd: z17.string() }))
5583
5782
  });
5584
5783
 
5585
5784
  // src/core/stats/cost/pricing.ts
@@ -6289,7 +6488,7 @@ function createApp({ root, readLanguage: readLanguage2, changes, allowedHosts, h
6289
6488
  });
6290
6489
  if (staticDir !== void 0) {
6291
6490
  app.use("*", serveStatic({ root: staticDir }));
6292
- app.get("*", serveStatic({ path: join22(staticDir, "index.html") }));
6491
+ app.get("*", serveStatic({ path: join24(staticDir, "index.html") }));
6293
6492
  }
6294
6493
  return app;
6295
6494
  }
@@ -6401,12 +6600,12 @@ function logReport({ closedEpics, reopenedEpics, blockingFiles, deleted, conflic
6401
6600
  }
6402
6601
 
6403
6602
  // 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";
6603
+ import { createHash as createHash5 } from "node:crypto";
6604
+ import { open as open2, stat as stat6 } from "node:fs/promises";
6605
+ import { basename as basename10, join as join26 } from "node:path";
6407
6606
 
6408
6607
  // src/core/stats/cost/attribute.ts
6409
- import { z as z16 } from "zod";
6608
+ import { z as z18 } from "zod";
6410
6609
 
6411
6610
  // src/core/stats/cost/shell-commands.ts
6412
6611
  var SEPARATORS = ["&&", "||", ";", "|", "&", "\n", "`"];
@@ -6599,21 +6798,21 @@ var FAST_SPEED = "fast";
6599
6798
  var CHARS_PER_TOKEN = 3;
6600
6799
  var PENDING_TOOL_LIMIT = 64;
6601
6800
  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()
6801
+ var usageSchema = z18.object({
6802
+ input_tokens: z18.number().optional(),
6803
+ output_tokens: z18.number().optional(),
6804
+ cache_read_input_tokens: z18.number().optional(),
6805
+ cache_creation_input_tokens: z18.number().optional(),
6806
+ speed: z18.string().optional(),
6807
+ cache_creation: z18.object({ ephemeral_5m_input_tokens: z18.number().optional(), ephemeral_1h_input_tokens: z18.number().optional() }).passthrough().optional()
6609
6808
  }).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();
6809
+ var assistantMessageSchema = z18.object({ id: z18.string().optional(), model: z18.string().optional(), usage: usageSchema.optional(), content: z18.array(z18.unknown()).optional() }).passthrough();
6810
+ var userMessageSchema = z18.object({ content: z18.unknown().optional() }).passthrough();
6811
+ var toolUseBlockSchema = z18.object({ type: z18.literal("tool_use"), id: z18.string(), name: z18.string(), input: z18.unknown().optional() }).passthrough();
6812
+ var toolResultBlockSchema = z18.object({ type: z18.literal("tool_result"), tool_use_id: z18.string().optional(), content: z18.unknown().optional() }).passthrough();
6813
+ var textBlockSchema = z18.object({ type: z18.literal("text"), text: z18.string() }).passthrough();
6814
+ var bashInputSchema = z18.object({ command: z18.string() }).passthrough();
6815
+ 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
6816
  function newTranscriptState() {
6618
6817
  return { hookOpen: false, lastModel: null, lastMessageId: null, lastMessageTokens: null, pending: {}, pendingEstimates: [] };
6619
6818
  }
@@ -6737,53 +6936,53 @@ function slotOf(timestamp) {
6737
6936
 
6738
6937
  // src/core/usage/usage-cache.ts
6739
6938
  import { mkdir as mkdir5 } from "node:fs/promises";
6740
- import { join as join23 } from "node:path";
6741
- import { z as z17 } from "zod";
6939
+ import { join as join25 } from "node:path";
6940
+ import { z as z19 } from "zod";
6742
6941
  var USAGE_CACHE_FILE = ".usage-cache.json";
6743
6942
  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(),
6943
+ var usageCacheEntrySchema = z19.object({
6944
+ size: z19.number(),
6945
+ mtimeMs: z19.number().optional(),
6946
+ offset: z19.number(),
6947
+ fingerprint: z19.string(),
6749
6948
  state: transcriptStateSchema,
6750
- buckets: z17.array(usageBucketSchema)
6949
+ buckets: z19.array(usageBucketSchema)
6751
6950
  });
6752
- var usageCacheSchema = z17.object({
6753
- version: z17.literal(USAGE_CACHE_VERSION),
6754
- files: z17.record(z17.string(), usageCacheEntrySchema)
6951
+ var usageCacheSchema = z19.object({
6952
+ version: z19.literal(USAGE_CACHE_VERSION),
6953
+ files: z19.record(z19.string(), usageCacheEntrySchema)
6755
6954
  });
6756
6955
  function emptyUsageCache() {
6757
6956
  return { version: USAGE_CACHE_VERSION, files: {} };
6758
6957
  }
6759
6958
  async function readUsageCache(root) {
6760
- return await readJsonFile(join23(root, USAGE_CACHE_FILE), usageCacheSchema) ?? emptyUsageCache();
6959
+ return await readJsonFile(join25(root, USAGE_CACHE_FILE), usageCacheSchema) ?? emptyUsageCache();
6761
6960
  }
6762
6961
  async function writeUsageCache(root, cache) {
6763
6962
  await mkdir5(root, { recursive: true });
6764
- await writeFileAtomic(join23(root, USAGE_CACHE_FILE), JSON.stringify(cache));
6963
+ await writeFileAtomic(join25(root, USAGE_CACHE_FILE), JSON.stringify(cache));
6765
6964
  }
6766
6965
 
6767
6966
  // src/core/usage/transcripts.ts
6768
6967
  var NEWLINE = 10;
6769
6968
  var FINGERPRINT_BYTES = 256;
6770
6969
  var ABANDONED_LINE_MS = 10 * 60 * 1e3;
6771
- var EMPTY_FINGERPRINT = createHash4("sha1").digest("hex");
6970
+ var EMPTY_FINGERPRINT = createHash5("sha1").digest("hex");
6772
6971
  var COUNTED_LINE_MARKERS = ['"type":"assistant"', '"type":"user"'];
6773
6972
  async function listTranscripts(claudeProjectsDir2) {
6774
6973
  const files = [];
6775
6974
  for (const projectEntry of await listDir(claudeProjectsDir2)) {
6776
6975
  if (!projectEntry.isDirectory()) continue;
6777
- const projectDir2 = join24(claudeProjectsDir2, projectEntry.name);
6976
+ const projectDir2 = join26(claudeProjectsDir2, projectEntry.name);
6778
6977
  for (const entry of await listDir(projectDir2)) {
6779
- const entryPath = join24(projectDir2, entry.name);
6978
+ const entryPath = join26(projectDir2, entry.name);
6780
6979
  if (entry.isFile() && entry.name.endsWith(".jsonl")) {
6781
6980
  files.push(...await listedFile(entryPath));
6782
6981
  continue;
6783
6982
  }
6784
6983
  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)));
6984
+ for (const subagentEntry of await listDir(join26(entryPath, "subagents"), { recursive: true })) {
6985
+ if (subagentEntry.isFile() && subagentEntry.name.endsWith(".jsonl")) files.push(...await listedFile(join26(subagentEntry.parentPath, subagentEntry.name)));
6787
6986
  }
6788
6987
  }
6789
6988
  }
@@ -6791,7 +6990,7 @@ async function listTranscripts(claudeProjectsDir2) {
6791
6990
  }
6792
6991
  async function listedFile(path) {
6793
6992
  try {
6794
- const { size, mtimeMs } = await stat5(path);
6993
+ const { size, mtimeMs } = await stat6(path);
6795
6994
  return [{ path, size, mtimeMs }];
6796
6995
  } catch {
6797
6996
  return [];
@@ -6856,7 +7055,7 @@ async function fingerprintOf(path, offset) {
6856
7055
  return withFile(path, async (handle) => {
6857
7056
  const head = await readAt(handle, 0, edge);
6858
7057
  const tail = await readAt(handle, offset - edge, edge);
6859
- return createHash4("sha1").update(head).update(tail).digest("hex");
7058
+ return createHash5("sha1").update(head).update(tail).digest("hex");
6860
7059
  });
6861
7060
  }
6862
7061
  function readChunk(path, position, length) {
@@ -6974,7 +7173,7 @@ function progressBefore(files, cache) {
6974
7173
  // src/server/start.ts
6975
7174
  var SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
6976
7175
  var STOP_SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"];
6977
- var BUNDLED_WEB_DIR = join25(import.meta.dirname, "web");
7176
+ var BUNDLED_WEB_DIR = join27(import.meta.dirname, "web");
6978
7177
  var log = (line) => void process.stdout.write(`${line}
6979
7178
  `);
6980
7179
  var warn = (line) => void process.stderr.write(`${line}
@@ -7075,7 +7274,7 @@ async function runServe(args, io) {
7075
7274
 
7076
7275
  // src/cli/service/launchd.ts
7077
7276
  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";
7277
+ import { dirname as dirname8, join as join28, posix } from "node:path";
7079
7278
  import { setTimeout as sleep3 } from "node:timers/promises";
7080
7279
 
7081
7280
  // src/cli/service/service.ts
@@ -7148,7 +7347,7 @@ ${entries}
7148
7347
  `;
7149
7348
  }
7150
7349
  function launchdManager(context, delay = sleep3) {
7151
- const file = join26(context.home, "Library/LaunchAgents", `${LABEL}.plist`);
7350
+ const file = join28(context.home, "Library/LaunchAgents", `${LABEL}.plist`);
7152
7351
  const domain = `gui/${context.uid}`;
7153
7352
  const bootout = () => context.exec("launchctl", ["bootout", `${domain}/${LABEL}`]);
7154
7353
  const bootstrap = () => context.exec("launchctl", ["bootstrap", domain, file]);
@@ -7183,7 +7382,7 @@ function launchdManager(context, delay = sleep3) {
7183
7382
 
7184
7383
  // src/cli/service/startup-folder.ts
7185
7384
  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";
7385
+ import { dirname as dirname9, join as join29, win32 } from "node:path";
7187
7386
  var SCRIPT_NAME = "p-backlog.vbs";
7188
7387
  var COMMAND_LINE_ARGUMENT = /"[^"]*"|\S+/g;
7189
7388
  var PROCESS_QUERY_TIMEOUT_MS = 15e3;
@@ -7191,16 +7390,16 @@ function vbsString(value) {
7191
7390
  return `"${value.replaceAll('"', '""')}"`;
7192
7391
  }
7193
7392
  function appDataDir(context) {
7194
- return join27(context.env.LOCALAPPDATA ?? join27(context.home, "AppData", "Local"), "p-backlog");
7393
+ return join29(context.env.LOCALAPPDATA ?? join29(context.home, "AppData", "Local"), "p-backlog");
7195
7394
  }
7196
7395
  function startupFolder(context) {
7197
- return join27(context.env.APPDATA ?? join27(context.home, "AppData", "Roaming"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
7396
+ return join29(context.env.APPDATA ?? join29(context.home, "AppData", "Roaming"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
7198
7397
  }
7199
7398
  function logPath2(context) {
7200
- return join27(appDataDir(context), "p-backlog.log");
7399
+ return join29(appDataDir(context), "p-backlog.log");
7201
7400
  }
7202
7401
  function pidFilePath(context) {
7203
- return join27(appDataDir(context), "server.pid");
7402
+ return join29(appDataDir(context), "server.pid");
7204
7403
  }
7205
7404
  var NODE_PATH_ENV = "P_BACKLOG_NODE";
7206
7405
  var CLI_PATH_ENV = "P_BACKLOG_CLI";
@@ -7251,7 +7450,7 @@ function sameWindowsPath(left, right) {
7251
7450
  return win32.normalize(left).toLowerCase() === win32.normalize(right).toLowerCase();
7252
7451
  }
7253
7452
  function startupFolderManager(context) {
7254
- const file = join27(startupFolder(context), SCRIPT_NAME);
7453
+ const file = join29(startupFolder(context), SCRIPT_NAME);
7255
7454
  return {
7256
7455
  file,
7257
7456
  logs: logPath2(context),
@@ -7278,7 +7477,7 @@ function startupFolderManager(context) {
7278
7477
 
7279
7478
  // src/cli/service/systemd.ts
7280
7479
  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";
7480
+ import { dirname as dirname10, join as join30 } from "node:path";
7282
7481
  var UNIT = "p-backlog.service";
7283
7482
  function quoted(value) {
7284
7483
  return `"${value.replace(/[\\"]/g, "\\$&").replaceAll("%", "%%")}"`;
@@ -7312,7 +7511,7 @@ async function systemctlSteps(exec, steps) {
7312
7511
  return "done";
7313
7512
  }
7314
7513
  function systemdManager(context) {
7315
- const file = join28(context.home, ".config/systemd/user", UNIT);
7514
+ const file = join30(context.home, ".config/systemd/user", UNIT);
7316
7515
  return {
7317
7516
  file,
7318
7517
  logs: "journalctl --user -u p-backlog",
@@ -7430,50 +7629,43 @@ function reportFailure({ failed, code, output }, io) {
7430
7629
  }
7431
7630
 
7432
7631
  // src/cli/commands/setup.ts
7433
- import { join as join29 } from "node:path";
7632
+ import { join as join32 } from "node:path";
7434
7633
 
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";
7634
+ // src/cli/agents/agent-hooks.ts
7635
+ import { join as join31 } from "node:path";
7636
+
7637
+ // src/cli/agents/grouped-stop-hooks.ts
7638
+ import { z as z20 } from "zod";
7639
+
7640
+ // src/cli/agents/json-config.ts
7641
+ import { mkdir as mkdir10, readFile as readFile5, readlink as readlink2, realpath as realpath2, stat as stat7 } from "node:fs/promises";
7437
7642
  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
7643
  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);
7644
+ async function readJsonConfig(path, schema) {
7645
+ const text = await readConfigText(path);
7454
7646
  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
7647
  let value;
7471
7648
  try {
7472
7649
  value = JSON.parse(text);
7473
7650
  } catch {
7474
- return null;
7651
+ return { failed: "invalid" };
7652
+ }
7653
+ return schema.safeParse(value).success ? { config: value } : { failed: "invalid" };
7654
+ }
7655
+ async function writeJsonConfig(path, config) {
7656
+ await mkdir10(dirname11(path), { recursive: true });
7657
+ const target = await writeTargetOf(path);
7658
+ const mode = await stat7(target).then(({ mode: mode2 }) => mode2 & 511, () => void 0);
7659
+ await writeFileAtomic(target, `${JSON.stringify(config, null, 2)}
7660
+ `, mode);
7661
+ }
7662
+ async function readConfigText(path) {
7663
+ try {
7664
+ return await readFile5(path, "utf8");
7665
+ } catch (error) {
7666
+ if (hasErrorCode(error, "ENOENT")) return "{}";
7667
+ return { failed: "unreadable", code: errorCodeOrText(error) };
7475
7668
  }
7476
- return settingsSchema2.safeParse(value).success ? value : null;
7477
7669
  }
7478
7670
  async function writeTargetOf(path) {
7479
7671
  try {
@@ -7492,62 +7684,240 @@ async function danglingLinkTarget(path) {
7492
7684
  }
7493
7685
  return current;
7494
7686
  }
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) };
7687
+
7688
+ // src/cli/agents/grouped-stop-hooks.ts
7689
+ var stopGroupSchema = z20.object({ hooks: z20.array(z20.unknown()).optional() }).passthrough();
7690
+ var groupedConfigSchema = z20.object({ hooks: z20.object({ Stop: z20.array(stopGroupSchema).optional() }).passthrough().optional() }).passthrough();
7691
+ function refreshOurHook(lists, hook, { isOurs, isCurrent }) {
7692
+ const ours = lists.flatMap((list) => list.flatMap((entry, index) => isOurs(entry) ? [{ list, index, entry }] : []));
7693
+ if (ours.some(({ entry }) => isCurrent(entry))) return "exists";
7694
+ const stale = ours[0];
7695
+ if (stale === void 0) return "missing";
7696
+ stale.list[stale.index] = { ...stale.entry, ...hook };
7697
+ return "updated";
7698
+ }
7699
+ async function addGroupedStopHook(path, hook, ourHook) {
7700
+ const read = await readJsonConfig(path, groupedConfigSchema);
7701
+ if (!("config" in read)) return read;
7702
+ const stopGroups = (read.config.hooks ??= {}).Stop ??= [];
7703
+ const refreshed = refreshOurHook(stopGroups.flatMap((group) => group.hooks === void 0 ? [] : [group.hooks]), hook, ourHook);
7704
+ if (refreshed === "exists") return "exists";
7705
+ if (refreshed === "missing") stopGroups.push({ hooks: [hook] });
7706
+ await writeJsonConfig(path, read.config);
7707
+ return refreshed === "missing" ? "added" : "updated";
7708
+ }
7709
+ async function removeGroupedStopHook(path, isOurs) {
7710
+ const read = await readJsonConfig(path, groupedConfigSchema);
7711
+ if (!("config" in read)) return read;
7712
+ const hooks = read.config.hooks;
7713
+ if (hooks?.Stop === void 0 || !hooks.Stop.some((group) => hasOurHook(group, isOurs))) return "absent";
7714
+ const kept = hooks.Stop.flatMap((group) => {
7715
+ if (!hasOurHook(group, isOurs)) return [group];
7716
+ const others = (group.hooks ?? []).filter((hook) => !isOurs(hook));
7717
+ return others.length > 0 ? [{ ...group, hooks: others }] : [];
7718
+ });
7719
+ if (kept.length > 0) hooks.Stop = kept;
7720
+ else delete hooks.Stop;
7721
+ await writeJsonConfig(path, read.config);
7722
+ return "removed";
7723
+ }
7724
+ function hasOurHook(group, isOurs) {
7725
+ return group.hooks?.some(isOurs) ?? false;
7726
+ }
7727
+
7728
+ // src/cli/stop-hook.ts
7729
+ var POSIX_COMMAND = "command -v backlog >/dev/null && backlog hook stop || true";
7730
+ var POWERSHELL_COMMAND = "if (Get-Command backlog.cmd -ErrorAction SilentlyContinue) { backlog.cmd hook stop }";
7731
+ function stopHookFor(platform) {
7732
+ return platform === "win32" ? { type: "command", shell: "powershell", command: POWERSHELL_COMMAND } : { type: "command", command: POSIX_COMMAND };
7733
+ }
7734
+ function isOurStopHook(hook) {
7735
+ if (typeof hook !== "object" || hook === null) return false;
7736
+ const command = hook.command;
7737
+ return command === POSIX_COMMAND || command === POWERSHELL_COMMAND;
7738
+ }
7739
+ function addStopHook(settingsPath, platform) {
7740
+ return addGroupedStopHook(settingsPath, stopHookFor(platform), { isOurs: isOurStopHook, isCurrent: isOurStopHook });
7741
+ }
7742
+ function removeStopHook(settingsPath) {
7743
+ return removeGroupedStopHook(settingsPath, isOurStopHook);
7744
+ }
7745
+
7746
+ // src/cli/agents/cursor-hooks.ts
7747
+ import { z as z21 } from "zod";
7748
+ var CURSOR_HOOKS_VERSION = 1;
7749
+ var cursorConfigSchema = z21.object({ version: z21.number().optional(), hooks: z21.object({ stop: z21.array(z21.unknown()).optional() }).passthrough().optional() }).passthrough();
7750
+ async function addCursorStopHook(path, hook, ourHook) {
7751
+ const read = await readJsonConfig(path, cursorConfigSchema);
7752
+ if (!("config" in read)) return read;
7753
+ const stop = (read.config.hooks ??= {}).stop ??= [];
7754
+ const refreshed = refreshOurHook([stop], hook, ourHook);
7755
+ if (refreshed === "exists") return "exists";
7756
+ if (refreshed === "missing") stop.push(hook);
7757
+ read.config.version ??= CURSOR_HOOKS_VERSION;
7758
+ await writeJsonConfig(path, read.config);
7759
+ return refreshed === "missing" ? "added" : "updated";
7760
+ }
7761
+ async function removeCursorStopHook(path, isOurs) {
7762
+ const read = await readJsonConfig(path, cursorConfigSchema);
7763
+ if (!("config" in read)) return read;
7764
+ const hooks = read.config.hooks;
7765
+ if (hooks?.stop === void 0 || !hooks.stop.some(isOurs)) return "absent";
7766
+ const kept = hooks.stop.filter((hook) => !isOurs(hook));
7767
+ if (kept.length > 0) hooks.stop = kept;
7768
+ else delete hooks.stop;
7769
+ await writeJsonConfig(path, read.config);
7770
+ return "removed";
7771
+ }
7772
+
7773
+ // src/cli/agents/agent-hooks.ts
7774
+ var CODEX_HOOK_TIMEOUT_SECONDS = 30;
7775
+ var AGENT_HOOKS_FILE = "hooks.json";
7776
+ function agentHookConfigPath(agent, env, home2) {
7777
+ return agent === "claude" ? claudeSettingsPath(env, home2) : join31(agentHomeDir(agent, env, home2), AGENT_HOOKS_FILE);
7778
+ }
7779
+ function installAgentHook(agent, site) {
7780
+ const path = agentHookConfigPath(agent, site.env, site.home);
7781
+ switch (agent) {
7782
+ case "claude":
7783
+ return addStopHook(path, site.platform);
7784
+ case "codex": {
7785
+ const hook = { type: "command", command: posixCommand(agent), commandWindows: windowsCommand(agent, site.cliPath), timeout: CODEX_HOOK_TIMEOUT_SECONDS };
7786
+ return addGroupedStopHook(path, hook, ourCurrentHook(agent, site.cliPath, hook));
7787
+ }
7788
+ case "cursor": {
7789
+ const hook = { command: site.platform === "win32" ? windowsCommand(agent, site.cliPath) : posixCommand(agent) };
7790
+ return addCursorStopHook(path, hook, ourCurrentHook(agent, site.cliPath, hook));
7791
+ }
7792
+ }
7793
+ }
7794
+ function removeAgentHook(agent, site) {
7795
+ const path = agentHookConfigPath(agent, site.env, site.home);
7796
+ switch (agent) {
7797
+ case "claude":
7798
+ return removeStopHook(path);
7799
+ case "codex":
7800
+ return removeGroupedStopHook(path, ourHookOf(agent));
7801
+ case "cursor":
7802
+ return removeCursorStopHook(path, ourHookOf(agent));
7501
7803
  }
7502
7804
  }
7805
+ function agentStopCommand(agent) {
7806
+ return `${HOOK_STOP_COMMAND} --agent ${agent}`;
7807
+ }
7808
+ function posixCommand(agent) {
7809
+ return `command -v backlog >/dev/null && backlog ${agentStopCommand(agent)} || true`;
7810
+ }
7811
+ function windowsCommand(agent, cliPath2) {
7812
+ return `node "${cliPath2}" ${agentStopCommand(agent)}`;
7813
+ }
7814
+ function windowsCommandPattern(agent) {
7815
+ return new RegExp(`^node "[^"]*cli\\.js" ${agentStopCommand(agent)}$`);
7816
+ }
7817
+ function ourHookOf(agent, cliPath2) {
7818
+ return (hook) => {
7819
+ const command = typeof hook === "object" && hook !== null ? hook.command : void 0;
7820
+ if (typeof command !== "string") return false;
7821
+ return command === posixCommand(agent) || cliPath2 !== void 0 && command === windowsCommand(agent, cliPath2) || windowsCommandPattern(agent).test(command);
7822
+ };
7823
+ }
7824
+ function ourCurrentHook(agent, cliPath2, wanted) {
7825
+ return {
7826
+ isOurs: ourHookOf(agent, cliPath2),
7827
+ isCurrent: (hook) => Object.entries(wanted).every(([key, value]) => hook[key] === value)
7828
+ };
7829
+ }
7503
7830
 
7504
7831
  // src/cli/commands/setup.ts
7505
7832
  var setupCommand = {
7506
7833
  name: "setup",
7507
- usage: () => ["[--service]"],
7834
+ usage: () => [`[--agent ${AGENTS.join("|")}] [--service]`, `--remove-manual [--agent ${AGENTS.join("|")}]`],
7508
7835
  run: runSetup
7509
7836
  };
7510
7837
  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;
7838
+ const options = parseOptions(io.language, args, { service: { type: "boolean" }, agent: { type: "string" }, "remove-manual": { type: "boolean" } });
7839
+ if (options["remove-manual"] && options.service) throw new UsageError(cliMessages(io.language).removeManualWithService);
7840
+ const agents = await targetAgents(options.agent, io);
7841
+ const step = options["remove-manual"] ? removeManualSetup : setUpAgent;
7842
+ const outcomes = [];
7843
+ for (const agent of agents) outcomes.push(await step(agent, io));
7844
+ const serviceCode = options.service ? await installService(io) : EXIT.ok;
7845
+ return outcomes.every(Boolean) ? serviceCode : EXIT.failed;
7846
+ }
7847
+ async function targetAgents(option, io) {
7848
+ const messages = cliMessages(io.language);
7849
+ if (option !== void 0) return [parseChoice(io.language, option, AGENTS, messages.optionLabel.agent)];
7850
+ const detection = await detectAgents(io.env, io.home);
7851
+ for (const { agent, dir } of detection.missing) agentVoice(agent, io).print(messages.agentNotFound(dir));
7852
+ return detection.found;
7853
+ }
7854
+ function agentVoice(agent, io) {
7855
+ const label = AGENT_LABELS[agent];
7856
+ return { print: (line) => io.print(`${label}: ${line}`), warn: (line) => io.warn(`${label}: ${line}`) };
7857
+ }
7858
+ async function setUpAgent(agent, io) {
7859
+ const voice = agentVoice(agent, io);
7860
+ const plugin = await agentPlugin(agent, io.env, io.home);
7861
+ if (plugin !== null) {
7862
+ voice.print(cliMessages(io.language).pluginManages(plugin));
7863
+ return true;
7864
+ }
7865
+ if (!await linkAgentSkill(agent, io, voice)) return false;
7866
+ const hook = await installAgentHook(agent, io);
7867
+ const reported = reportHook(hook, agentHookConfigPath(agent, io.env, io.home), io, voice);
7868
+ if (agent === "codex" && (hook === "added" || hook === "updated")) voice.print(cliMessages(io.language).codexHookApproval);
7869
+ return reported;
7515
7870
  }
7516
- async function runSetupSteps(io) {
7871
+ async function linkAgentSkill(agent, io, voice) {
7517
7872
  const messages = cliMessages(io.language);
7518
- const skillsDir = claudeSkillsDir(io.env, io.home);
7519
- const target = join29(skillsDir, "backlog");
7873
+ const skillsDir = agentSkillsDir(agent, io.env, io.home);
7874
+ const target = join32(skillsDir, "backlog");
7520
7875
  const source = skillSourceDir(io.packageRoot, io.language);
7521
7876
  let link2;
7522
7877
  try {
7523
7878
  link2 = await linkSkillFor(io.language, { skillsDir, packageRoot: io.packageRoot, platform: io.platform });
7524
7879
  } catch (error) {
7525
- io.warn(messages.installSkillLinkFailed(target, errorCodeOrText(error)));
7880
+ voice.warn(messages.installSkillLinkFailed(target, errorCodeOrText(error)));
7526
7881
  return false;
7527
7882
  }
7528
7883
  if (link2 === "foreign") {
7529
- io.warn(messages.installSkillForeign(target, source));
7884
+ voice.warn(messages.installSkillForeign(target, source));
7530
7885
  return false;
7531
7886
  }
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);
7887
+ voice.print(link2 === "linked" ? messages.installSkillLinked(target, source) : messages.installSkillKept(target));
7888
+ return true;
7535
7889
  }
7536
- function reportHook(result, settingsPath, io) {
7890
+ function reportHook(result, configPath, io, voice) {
7537
7891
  const messages = cliMessages(io.language);
7538
- if (result === "added") {
7539
- io.print(messages.installHookAdded(settingsPath));
7540
- return true;
7541
- }
7542
- if (result === "exists") {
7543
- io.print(messages.installHookExists(settingsPath));
7892
+ if (typeof result === "string") {
7893
+ const report = { added: messages.installHookAdded, exists: messages.installHookExists, updated: messages.installHookUpdated }[result];
7894
+ voice.print(report(configPath));
7544
7895
  return true;
7545
7896
  }
7546
7897
  if (result.failed === "unreadable") {
7547
- io.warn(messages.installSettingsUnreadable(settingsPath, result.code));
7898
+ voice.warn(messages.installSettingsUnreadable(configPath, result.code));
7548
7899
  return false;
7549
7900
  }
7550
- io.warn(messages.installSettingsInvalid(settingsPath));
7901
+ voice.warn(messages.installSettingsInvalid(configPath));
7902
+ return false;
7903
+ }
7904
+ async function removeManualSetup(agent, io) {
7905
+ const voice = agentVoice(agent, io);
7906
+ const messages = cliMessages(io.language);
7907
+ const skillsDir = agentSkillsDir(agent, io.env, io.home);
7908
+ voice.print(messages.manualSkillRemoval[await unlinkOurSkill(skillsDir)](join32(skillsDir, "backlog")));
7909
+ for (const legacyDir of legacySkillsDirs(agent, io.env, io.home)) {
7910
+ if (await unlinkOurSkill(legacyDir) === "removed") voice.print(messages.manualSkillRemoval.removed(join32(legacyDir, "backlog")));
7911
+ }
7912
+ return reportHookRemoval(await removeAgentHook(agent, io), agentHookConfigPath(agent, io.env, io.home), io, voice);
7913
+ }
7914
+ function reportHookRemoval(result, configPath, io, voice) {
7915
+ const messages = cliMessages(io.language);
7916
+ if (result === "removed" || result === "absent") {
7917
+ voice.print(messages.manualHookRemoval[result](configPath));
7918
+ return true;
7919
+ }
7920
+ voice.warn(result.failed === "unreadable" ? messages.hookConfigUnreadable(configPath, result.code) : messages.hookConfigInvalid(configPath));
7551
7921
  return false;
7552
7922
  }
7553
7923