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.
- package/CHANGELOG.md +31 -0
- package/README.md +50 -30
- package/README.ru.md +50 -30
- package/dist/cli.js +670 -265
- package/dist/web/assets/{CodeTab-C87LaJFD.js → CodeTab-D3I5lIJJ.js} +1 -1
- package/dist/web/assets/CostTab-8f2AC5ER.js +1 -0
- package/dist/web/assets/EffectTab-1jObLK9a.js +1 -0
- package/dist/web/assets/Figure-BHBCd7Dt.js +1 -0
- package/dist/web/assets/OverviewTab-DjNyxzJq.js +1 -0
- package/dist/web/assets/QualityTab-W2NdYBhI.js +1 -0
- package/dist/web/assets/StatsPage-rJH3k8Ye.js +1 -0
- package/dist/web/assets/StatsTabState-BoimPoKm.js +1 -0
- package/dist/web/assets/{StatsTable-BXG0IS8p.js → StatsTable-B0tbx68M.js} +1 -1
- package/dist/web/assets/{UnavailableRepos-KiTvsm1X.js → UnavailableRepos-SpC6Dzwu.js} +1 -1
- package/dist/web/assets/cx-DDl6tKXq.js +58 -0
- package/dist/web/assets/{index-AJ1Qzly1.js → index-C8xqRmo-.js} +2 -2
- package/dist/web/assets/start-YlC0KTe5.js +38 -0
- package/dist/web/assets/use-grain-series-Bx70IZvb.js +55 -0
- package/dist/web/assets/value-dot-B7BsbWBD.js +1 -0
- package/dist/web/index.html +1 -1
- package/package.json +2 -1
- package/skill/backlog/SKILL.md +2 -2
- package/skill/backlog-en/SKILL.md +2 -2
- package/dist/web/assets/ChartTooltip-DWaDSRan.js +0 -55
- package/dist/web/assets/CostTab-D4ByOnHX.js +0 -1
- package/dist/web/assets/EffectTab-Dq06ghtZ.js +0 -1
- package/dist/web/assets/Figure-BC1eGh3_.js +0 -1
- package/dist/web/assets/OverviewTab-BBmohHJR.js +0 -1
- package/dist/web/assets/QualityTab-Rb7zRcGH.js +0 -1
- package/dist/web/assets/StatsPage-Dtyc1gAc.js +0 -1
- package/dist/web/assets/StatsTabState-Br-8FgKd.js +0 -1
- package/dist/web/assets/cx-CNNf0q6V.js +0 -58
- package/dist/web/assets/rolldown-runtime-hePW80VL.js +0 -1
- package/dist/web/assets/start-BGXTwsB0.js +0 -38
- package/dist/web/assets/value-dot-W-8PsGpd.js +0 -1
- /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 =
|
|
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
|
-
|
|
567
|
-
|
|
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
|
-
|
|
725
|
-
|
|
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
|
|
1377
|
+
function flowOver(periods, histories) {
|
|
1339
1378
|
const closings = histories.flatMap(closingsOf);
|
|
1340
|
-
return
|
|
1341
|
-
start: formatLocalIso(new Date(
|
|
1342
|
-
created: histories.filter((history) =>
|
|
1343
|
-
closed: closings.filter((closing) =>
|
|
1344
|
-
openAtEnd: histories.filter((history) => isOpenAt(history,
|
|
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
|
|
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
|
|
3897
|
-
import { z as
|
|
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 =
|
|
3992
|
+
var packageManifestSchema = z9.object({ name: z9.string() });
|
|
3900
3993
|
function skillSourceDir(packageRoot2, language) {
|
|
3901
|
-
return
|
|
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(
|
|
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 =
|
|
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
|
|
3951
|
-
const
|
|
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
|
-
|
|
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
|
|
4011
|
-
import { z as
|
|
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 =
|
|
4152
|
+
var shownSchema = z11.record(z11.string(), z11.string());
|
|
4014
4153
|
async function readSignalsShown(projectDir2) {
|
|
4015
|
-
return await readJsonFile(
|
|
4154
|
+
return await readJsonFile(join20(projectDir2, SIGNALS_SHOWN_FILE), shownSchema) ?? {};
|
|
4016
4155
|
}
|
|
4017
4156
|
async function writeSignalsShown(projectDir2, shown) {
|
|
4018
|
-
await writeJsonFile(
|
|
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
|
|
4023
|
-
import { z as
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
4179
|
-
return
|
|
4332
|
+
function accuracyOver(periods, histories) {
|
|
4333
|
+
return periods.map((span) => {
|
|
4180
4334
|
const decided = histories.flatMap(
|
|
4181
|
-
(history) => history.candidates.filter((candidate) =>
|
|
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(
|
|
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
|
|
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
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
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.
|
|
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
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
};
|
|
4372
|
-
if (
|
|
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
|
|
5015
|
+
import { join as join27 } from "node:path";
|
|
4792
5016
|
|
|
4793
5017
|
// src/core/store/sweep.ts
|
|
4794
|
-
import { dirname as dirname7, join as
|
|
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) =>
|
|
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
|
|
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
|
|
4949
|
-
var idList =
|
|
4950
|
-
var taskChangesSchema =
|
|
4951
|
-
title:
|
|
4952
|
-
type:
|
|
4953
|
-
status:
|
|
4954
|
-
priority:
|
|
4955
|
-
tags:
|
|
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:
|
|
4960
|
-
category:
|
|
5183
|
+
body: z14.string().optional(),
|
|
5184
|
+
category: z14.enum(TASK_CATEGORIES).nullable().optional()
|
|
4961
5185
|
});
|
|
4962
|
-
var projectActiveSchema =
|
|
4963
|
-
var projectDeleteSchema =
|
|
4964
|
-
var settingsRequestSchema =
|
|
4965
|
-
var updateTaskRequestSchema =
|
|
4966
|
-
version:
|
|
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 =
|
|
4970
|
-
status:
|
|
4971
|
-
priority:
|
|
5193
|
+
var batchPreviousSchema = z14.strictObject({
|
|
5194
|
+
status: z14.enum(TASK_STATUSES),
|
|
5195
|
+
priority: z14.enum(PRIORITIES),
|
|
4972
5196
|
epic: taskIdSchema.nullable(),
|
|
4973
|
-
resolution:
|
|
4974
|
-
reason:
|
|
5197
|
+
resolution: z14.enum(RESOLUTIONS).nullable(),
|
|
5198
|
+
reason: z14.string().nullable()
|
|
4975
5199
|
});
|
|
4976
5200
|
var BATCH_TASKS_LIMIT = 500;
|
|
4977
|
-
var batchRequestSchema =
|
|
4978
|
-
tasks:
|
|
4979
|
-
action:
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
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
|
|
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),
|
|
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
|
|
5129
|
-
import { z as
|
|
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 =
|
|
5133
|
-
commits:
|
|
5134
|
-
lines:
|
|
5135
|
-
units:
|
|
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 =
|
|
5138
|
-
var snapshotSchema =
|
|
5139
|
-
version:
|
|
5140
|
-
repos:
|
|
5141
|
-
fixes:
|
|
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 =
|
|
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
|
|
5545
|
-
var tokenCountsSchema =
|
|
5546
|
-
input:
|
|
5547
|
-
cacheWrite5m:
|
|
5548
|
-
cacheWrite1h:
|
|
5549
|
-
cacheRead:
|
|
5550
|
-
output:
|
|
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
|
|
5789
|
+
import { z as z17 } from "zod";
|
|
5566
5790
|
var COST_REPORT_DAYS = 30;
|
|
5567
|
-
var estimatedKindSchema =
|
|
5568
|
-
var usageBucketSchema =
|
|
5569
|
-
slot:
|
|
5570
|
-
cwd:
|
|
5571
|
-
model:
|
|
5572
|
-
kind:
|
|
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:
|
|
5798
|
+
hookTurns: z17.number()
|
|
5575
5799
|
});
|
|
5576
|
-
var transcriptStateSchema =
|
|
5577
|
-
hookOpen:
|
|
5578
|
-
lastModel:
|
|
5579
|
-
lastMessageId:
|
|
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:
|
|
5582
|
-
pendingEstimates:
|
|
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
|
|
5718
|
-
const hookBuckets =
|
|
5719
|
-
const cliBuckets =
|
|
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(
|
|
5725
|
-
hasUnpricedTokens: hasUnpricedTokens(
|
|
5726
|
-
hookTurns: sum(
|
|
5727
|
-
...runCounts(
|
|
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:
|
|
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:
|
|
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
|
|
6405
|
-
import { open as open2, stat as
|
|
6406
|
-
import { basename as basename10, join as
|
|
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
|
|
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 =
|
|
6603
|
-
input_tokens:
|
|
6604
|
-
output_tokens:
|
|
6605
|
-
cache_read_input_tokens:
|
|
6606
|
-
cache_creation_input_tokens:
|
|
6607
|
-
speed:
|
|
6608
|
-
cache_creation:
|
|
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 =
|
|
6611
|
-
var userMessageSchema =
|
|
6612
|
-
var toolUseBlockSchema =
|
|
6613
|
-
var toolResultBlockSchema =
|
|
6614
|
-
var textBlockSchema =
|
|
6615
|
-
var bashInputSchema =
|
|
6616
|
-
var lineSchema =
|
|
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
|
|
6741
|
-
import { z as
|
|
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 =
|
|
6745
|
-
size:
|
|
6746
|
-
mtimeMs:
|
|
6747
|
-
offset:
|
|
6748
|
-
fingerprint:
|
|
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:
|
|
6968
|
+
buckets: z19.array(usageBucketSchema)
|
|
6751
6969
|
});
|
|
6752
|
-
var usageCacheSchema =
|
|
6753
|
-
version:
|
|
6754
|
-
files:
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
6995
|
+
const projectDir2 = join26(claudeProjectsDir2, projectEntry.name);
|
|
6778
6996
|
for (const entry of await listDir(projectDir2)) {
|
|
6779
|
-
const entryPath =
|
|
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(
|
|
6786
|
-
if (subagentEntry.isFile() && subagentEntry.name.endsWith(".jsonl")) files.push(...await listedFile(
|
|
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
|
|
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() -
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
7412
|
+
return join29(context.env.LOCALAPPDATA ?? join29(context.home, "AppData", "Local"), "p-backlog");
|
|
7195
7413
|
}
|
|
7196
7414
|
function startupFolder(context) {
|
|
7197
|
-
return
|
|
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
|
|
7418
|
+
return join29(appDataDir(context), "p-backlog.log");
|
|
7201
7419
|
}
|
|
7202
7420
|
function pidFilePath(context) {
|
|
7203
|
-
return
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
7651
|
+
import { join as join32 } from "node:path";
|
|
7434
7652
|
|
|
7435
|
-
// src/cli/
|
|
7436
|
-
import {
|
|
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
|
-
|
|
7443
|
-
|
|
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
|
|
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
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
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
|
|
7512
|
-
|
|
7513
|
-
const
|
|
7514
|
-
|
|
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
|
|
7871
|
+
async function targetAgents(option, io) {
|
|
7517
7872
|
const messages = cliMessages(io.language);
|
|
7518
|
-
|
|
7519
|
-
const
|
|
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
|
-
|
|
7904
|
+
voice.warn(messages.installSkillLinkFailed(target, errorCodeOrText(error)));
|
|
7526
7905
|
return false;
|
|
7527
7906
|
}
|
|
7528
7907
|
if (link2 === "foreign") {
|
|
7529
|
-
|
|
7908
|
+
voice.warn(messages.installSkillForeign(target, source));
|
|
7530
7909
|
return false;
|
|
7531
7910
|
}
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
return
|
|
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
|
|
7537
|
-
const
|
|
7538
|
-
|
|
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
|
-
|
|
7543
|
-
|
|
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
|
-
|
|
7928
|
+
voice.warn(messages.installHookConfigUnreadable(configPath, result.code));
|
|
7548
7929
|
return false;
|
|
7549
7930
|
}
|
|
7550
|
-
|
|
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
|
|