ghostrail 0.18.0 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +29 -0
  2. package/dist/agent/claude-code.d.ts +18 -1
  3. package/dist/agent/claude-code.d.ts.map +1 -1
  4. package/dist/agent/claude-code.js +36 -4
  5. package/dist/agent/claude-code.js.map +1 -1
  6. package/dist/backend/attach.d.ts +49 -0
  7. package/dist/backend/attach.d.ts.map +1 -0
  8. package/dist/backend/attach.js +66 -0
  9. package/dist/backend/attach.js.map +1 -0
  10. package/dist/backend/container.d.ts.map +1 -1
  11. package/dist/backend/container.js +20 -30
  12. package/dist/backend/container.js.map +1 -1
  13. package/dist/backend/local-worktree.d.ts.map +1 -1
  14. package/dist/backend/local-worktree.js +6 -4
  15. package/dist/backend/local-worktree.js.map +1 -1
  16. package/dist/board/page.d.ts.map +1 -1
  17. package/dist/board/page.js +39 -2
  18. package/dist/board/page.js.map +1 -1
  19. package/dist/cli/commands.d.ts +12 -7
  20. package/dist/cli/commands.d.ts.map +1 -1
  21. package/dist/cli/commands.js +261 -43
  22. package/dist/cli/commands.js.map +1 -1
  23. package/dist/commands/help.d.ts.map +1 -1
  24. package/dist/commands/help.js +5 -3
  25. package/dist/commands/help.js.map +1 -1
  26. package/dist/config/load.d.ts.map +1 -1
  27. package/dist/config/load.js +20 -3
  28. package/dist/config/load.js.map +1 -1
  29. package/dist/config/schema.d.ts +57 -0
  30. package/dist/config/schema.d.ts.map +1 -1
  31. package/dist/config/schema.js +19 -0
  32. package/dist/config/schema.js.map +1 -1
  33. package/dist/dashboard/index.d.ts +3 -2
  34. package/dist/dashboard/index.d.ts.map +1 -1
  35. package/dist/dashboard/index.js +3 -2
  36. package/dist/dashboard/index.js.map +1 -1
  37. package/dist/dashboard/page.d.ts.map +1 -1
  38. package/dist/dashboard/page.js +72 -4
  39. package/dist/dashboard/page.js.map +1 -1
  40. package/dist/dashboard/review-page.d.ts +14 -0
  41. package/dist/dashboard/review-page.d.ts.map +1 -0
  42. package/dist/dashboard/review-page.js +256 -0
  43. package/dist/dashboard/review-page.js.map +1 -0
  44. package/dist/dashboard/routes.d.ts +18 -0
  45. package/dist/dashboard/routes.d.ts.map +1 -1
  46. package/dist/dashboard/routes.js +24 -0
  47. package/dist/dashboard/routes.js.map +1 -1
  48. package/dist/dashboard/server.d.ts +19 -0
  49. package/dist/dashboard/server.d.ts.map +1 -1
  50. package/dist/dashboard/server.js +71 -1
  51. package/dist/dashboard/server.js.map +1 -1
  52. package/dist/doctor/checks.d.ts +21 -0
  53. package/dist/doctor/checks.d.ts.map +1 -1
  54. package/dist/doctor/checks.js +44 -0
  55. package/dist/doctor/checks.js.map +1 -1
  56. package/dist/doctor/probes.d.ts +15 -0
  57. package/dist/doctor/probes.d.ts.map +1 -1
  58. package/dist/doctor/probes.js +33 -0
  59. package/dist/doctor/probes.js.map +1 -1
  60. package/dist/doctor/run.d.ts +2 -1
  61. package/dist/doctor/run.d.ts.map +1 -1
  62. package/dist/doctor/run.js +25 -2
  63. package/dist/doctor/run.js.map +1 -1
  64. package/dist/factory/build.d.ts +6 -0
  65. package/dist/factory/build.d.ts.map +1 -1
  66. package/dist/factory/build.js +17 -5
  67. package/dist/factory/build.js.map +1 -1
  68. package/dist/global/tempdir.d.ts +19 -0
  69. package/dist/global/tempdir.d.ts.map +1 -0
  70. package/dist/global/tempdir.js +47 -0
  71. package/dist/global/tempdir.js.map +1 -0
  72. package/dist/init/greenroom.d.ts +18 -0
  73. package/dist/init/greenroom.d.ts.map +1 -0
  74. package/dist/init/greenroom.js +24 -0
  75. package/dist/init/greenroom.js.map +1 -0
  76. package/dist/loop/engine.d.ts +9 -0
  77. package/dist/loop/engine.d.ts.map +1 -1
  78. package/dist/loop/engine.js +114 -7
  79. package/dist/loop/engine.js.map +1 -1
  80. package/dist/loop/index.d.ts +1 -0
  81. package/dist/loop/index.d.ts.map +1 -1
  82. package/dist/loop/index.js +1 -0
  83. package/dist/loop/index.js.map +1 -1
  84. package/dist/loop/ports.d.ts +7 -0
  85. package/dist/loop/ports.d.ts.map +1 -1
  86. package/dist/loop/record.d.ts +6 -1
  87. package/dist/loop/record.d.ts.map +1 -1
  88. package/dist/loop/retry.d.ts +50 -0
  89. package/dist/loop/retry.d.ts.map +1 -0
  90. package/dist/loop/retry.js +77 -0
  91. package/dist/loop/retry.js.map +1 -0
  92. package/dist/loop/schedule.d.ts +33 -0
  93. package/dist/loop/schedule.d.ts.map +1 -0
  94. package/dist/loop/schedule.js +28 -0
  95. package/dist/loop/schedule.js.map +1 -0
  96. package/dist/respond/authors.d.ts +38 -0
  97. package/dist/respond/authors.d.ts.map +1 -0
  98. package/dist/respond/authors.js +50 -0
  99. package/dist/respond/authors.js.map +1 -0
  100. package/dist/respond/merge.d.ts.map +1 -1
  101. package/dist/respond/merge.js +25 -19
  102. package/dist/respond/merge.js.map +1 -1
  103. package/dist/respond/respond.d.ts +7 -0
  104. package/dist/respond/respond.d.ts.map +1 -1
  105. package/dist/respond/respond.js +36 -16
  106. package/dist/respond/respond.js.map +1 -1
  107. package/dist/respond/run-id.d.ts +17 -0
  108. package/dist/respond/run-id.d.ts.map +1 -0
  109. package/dist/respond/run-id.js +23 -0
  110. package/dist/respond/run-id.js.map +1 -0
  111. package/dist/respond/run-row.d.ts +24 -0
  112. package/dist/respond/run-row.d.ts.map +1 -0
  113. package/dist/respond/run-row.js +28 -0
  114. package/dist/respond/run-row.js.map +1 -0
  115. package/dist/store/events.d.ts +30 -0
  116. package/dist/store/events.d.ts.map +1 -1
  117. package/dist/store/events.js +29 -7
  118. package/dist/store/events.js.map +1 -1
  119. package/dist/store/index.d.ts +1 -1
  120. package/dist/store/index.d.ts.map +1 -1
  121. package/dist/store/index.js.map +1 -1
  122. package/dist/triage/engine.d.ts +14 -0
  123. package/dist/triage/engine.d.ts.map +1 -1
  124. package/dist/triage/engine.js +73 -3
  125. package/dist/triage/engine.js.map +1 -1
  126. package/package.json +1 -1
  127. package/templates/code/ghostrail.toml +4 -0
  128. package/templates/content/ghostrail.toml +4 -0
@@ -74,6 +74,18 @@ export interface GateConfig {
74
74
  export interface RespondConfigSection {
75
75
  /** Prompt file for the respond agent (reads `.ghostrail/feedback.md`). */
76
76
  prompt: string;
77
+ /**
78
+ * Comment authors the respond pass ignores, on top of the source kind's
79
+ * built-in defaults and `GHOSTRAIL_SELF_LOGINS`. Bot logins, not
80
+ * credentials, so they live in the config rather than the secrets file.
81
+ */
82
+ ignoreAuthors: string[];
83
+ /**
84
+ * How often `watch` runs this pass, in milliseconds. Undefined when
85
+ * `every = "off"`, which keeps `ghostrail respond` working by hand while
86
+ * leaving it out of the watch loop.
87
+ */
88
+ everyMs?: number;
77
89
  }
78
90
  /** Optional triage stage: decompose large issues into ordered sub-issues. */
79
91
  export interface TriageConfig {
@@ -81,6 +93,12 @@ export interface TriageConfig {
81
93
  prompt: string;
82
94
  /** Which issues form the triage queue (typically a `triage` label + assignee). */
83
95
  eligible: EligibleFilter;
96
+ /**
97
+ * How often `watch` runs this pass, in milliseconds. Undefined when
98
+ * `every = "off"`, which keeps `ghostrail triage` working by hand while
99
+ * leaving it out of the watch loop.
100
+ */
101
+ everyMs?: number;
84
102
  }
85
103
  /**
86
104
  * A guardrail spending cap: a positive USD amount, or `"unlimited"` to
@@ -102,6 +120,30 @@ export interface Guardrails {
102
120
  * concurrently; the per-tick total is still bounded by `maxItemsPerRun`.
103
121
  */
104
122
  concurrency: number;
123
+ /**
124
+ * How many consecutive "persistent" failures (a red gate, or the agent
125
+ * itself reporting failure) an item may accrue before the factory stops
126
+ * claiming it and hands it to a human. Optional so ad hoc `Guardrails`
127
+ * literals built before this field existed keep compiling; a missing value
128
+ * falls back to {@link DEFAULT_MAX_FAILURES} wherever retry decisions are made.
129
+ */
130
+ maxFailures?: number;
131
+ /**
132
+ * Same allowance as {@link Guardrails.maxFailures}, but for "transient"
133
+ * failures (a provisioning error, an agent timeout) — counted separately so
134
+ * a flake does not spend down the allowance a real dead end needs, and does
135
+ * not give up too early on it either. Falls back to
136
+ * {@link DEFAULT_MAX_TRANSIENT_FAILURES} when unset.
137
+ */
138
+ maxTransientFailures?: number;
139
+ /**
140
+ * Base delay, in milliseconds, of the exponential backoff applied after a
141
+ * failed attempt: `retryBackoffMs * 2^(failures - 1)`, capped at
142
+ * `retryBackoffCapMs`. Falls back to {@link DEFAULT_RETRY_BACKOFF_MS}.
143
+ */
144
+ retryBackoffMs?: number;
145
+ /** Cap on the backoff computed from {@link Guardrails.retryBackoffMs}. Falls back to {@link DEFAULT_RETRY_BACKOFF_CAP_MS}. */
146
+ retryBackoffCapMs?: number;
105
147
  }
106
148
  export interface OutputConfig {
107
149
  openPr: boolean;
@@ -134,8 +176,23 @@ export interface GhostrailConfig {
134
176
  }
135
177
  /** Default agent tool allowlist: read + write files and run shell/search only. */
136
178
  export declare const DEFAULT_ALLOWED_TOOLS: readonly string[];
179
+ /** See {@link Guardrails.maxFailures}. */
180
+ export declare const DEFAULT_MAX_FAILURES = 3;
181
+ /** See {@link Guardrails.maxTransientFailures}. */
182
+ export declare const DEFAULT_MAX_TRANSIENT_FAILURES = 5;
183
+ /** See {@link Guardrails.retryBackoffMs}. */
184
+ export declare const DEFAULT_RETRY_BACKOFF_MS: number;
185
+ /** See {@link Guardrails.retryBackoffCapMs}. */
186
+ export declare const DEFAULT_RETRY_BACKOFF_CAP_MS: number;
137
187
  /** Guardrail defaults, carried over from the reference factory. */
138
188
  export declare const DEFAULT_GUARDRAILS: Guardrails;
189
+ /**
190
+ * How often `watch` runs each schedulable pass when its section sets no
191
+ * `every`. Respond answers a human who is waiting, so it runs often; triage
192
+ * costs an agent run and decomposing an issue is never urgent, so it does not.
193
+ */
194
+ export declare const DEFAULT_RESPOND_EVERY_MS: number;
195
+ export declare const DEFAULT_TRIAGE_EVERY_MS: number;
139
196
  export declare const DEFAULT_BASE_BRANCH = "main";
140
197
  export declare const DEFAULT_BACKEND_KIND: BackendKind;
141
198
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"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"}
@@ -25,6 +25,14 @@ export const DEFAULT_ALLOWED_TOOLS = [
25
25
  "Grep",
26
26
  "Glob",
27
27
  ];
28
+ /** See {@link Guardrails.maxFailures}. */
29
+ export const DEFAULT_MAX_FAILURES = 3;
30
+ /** See {@link Guardrails.maxTransientFailures}. */
31
+ export const DEFAULT_MAX_TRANSIENT_FAILURES = 5;
32
+ /** See {@link Guardrails.retryBackoffMs}. */
33
+ export const DEFAULT_RETRY_BACKOFF_MS = 10 * 60_000;
34
+ /** See {@link Guardrails.retryBackoffCapMs}. */
35
+ export const DEFAULT_RETRY_BACKOFF_CAP_MS = 4 * 3_600_000;
28
36
  /** Guardrail defaults, carried over from the reference factory. */
29
37
  export const DEFAULT_GUARDRAILS = {
30
38
  maxItemsPerRun: 3,
@@ -33,7 +41,18 @@ export const DEFAULT_GUARDRAILS = {
33
41
  maxCostUsdPerRun: 5,
34
42
  claimTtlMs: 2 * 3_600_000,
35
43
  concurrency: 1,
44
+ maxFailures: DEFAULT_MAX_FAILURES,
45
+ maxTransientFailures: DEFAULT_MAX_TRANSIENT_FAILURES,
46
+ retryBackoffMs: DEFAULT_RETRY_BACKOFF_MS,
47
+ retryBackoffCapMs: DEFAULT_RETRY_BACKOFF_CAP_MS,
36
48
  };
49
+ /**
50
+ * How often `watch` runs each schedulable pass when its section sets no
51
+ * `every`. Respond answers a human who is waiting, so it runs often; triage
52
+ * costs an agent run and decomposing an issue is never urgent, so it does not.
53
+ */
54
+ export const DEFAULT_RESPOND_EVERY_MS = 5 * 60_000;
55
+ export const DEFAULT_TRIAGE_EVERY_MS = 30 * 60_000;
37
56
  export const DEFAULT_BASE_BRANCH = "main";
38
57
  export const DEFAULT_BACKEND_KIND = "local-worktree";
39
58
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,eAAe,EAAE,OAAO,CAAU,CAAC;AAG1E,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,gBAAgB;IAChB,WAAW;IACX,aAAa;IACb,KAAK;IACL,QAAQ;CACA,CAAC;AAwHX,mEAAmE;AACnE,MAAM,CAAC,MAAM,qBAAqB,GAAG,WAAW,CAAC;AACjD,MAAM,CAAC,MAAM,mBAAmB,GAAG,OAAO,CAAC;AAkB3C,kFAAkF;AAClF,MAAM,CAAC,MAAM,qBAAqB,GAAsB;IACtD,MAAM;IACN,MAAM;IACN,OAAO;IACP,MAAM;IACN,MAAM;IACN,MAAM;CACP,CAAC;AAEF,mEAAmE;AACnE,MAAM,CAAC,MAAM,kBAAkB,GAAe;IAC5C,cAAc,EAAE,CAAC;IACjB,gBAAgB,EAAE,CAAC;IACnB,SAAS,EAAE,EAAE,GAAG,MAAM;IACtB,gBAAgB,EAAE,CAAC;IACnB,UAAU,EAAE,CAAC,GAAG,SAAS;IACzB,WAAW,EAAE,CAAC;CACf,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAC1C,MAAM,CAAC,MAAM,oBAAoB,GAAgB,gBAAgB,CAAC;AAElE;;;GAGG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,CAAC"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,eAAe,EAAE,OAAO,CAAU,CAAC;AAG1E,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,gBAAgB;IAChB,WAAW;IACX,aAAa;IACb,KAAK;IACL,QAAQ;CACA,CAAC;AAkKX,mEAAmE;AACnE,MAAM,CAAC,MAAM,qBAAqB,GAAG,WAAW,CAAC;AACjD,MAAM,CAAC,MAAM,mBAAmB,GAAG,OAAO,CAAC;AAkB3C,kFAAkF;AAClF,MAAM,CAAC,MAAM,qBAAqB,GAAsB;IACtD,MAAM;IACN,MAAM;IACN,OAAO;IACP,MAAM;IACN,MAAM;IACN,MAAM;CACP,CAAC;AAEF,0CAA0C;AAC1C,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AACtC,mDAAmD;AACnD,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,CAAC;AAChD,6CAA6C;AAC7C,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,GAAG,MAAM,CAAC;AACpD,gDAAgD;AAChD,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,GAAG,SAAS,CAAC;AAE1D,mEAAmE;AACnE,MAAM,CAAC,MAAM,kBAAkB,GAAe;IAC5C,cAAc,EAAE,CAAC;IACjB,gBAAgB,EAAE,CAAC;IACnB,SAAS,EAAE,EAAE,GAAG,MAAM;IACtB,gBAAgB,EAAE,CAAC;IACnB,UAAU,EAAE,CAAC,GAAG,SAAS;IACzB,WAAW,EAAE,CAAC;IACd,WAAW,EAAE,oBAAoB;IACjC,oBAAoB,EAAE,8BAA8B;IACpD,cAAc,EAAE,wBAAwB;IACxC,iBAAiB,EAAE,4BAA4B;CAChD,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,GAAG,MAAM,CAAC;AACnD,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,GAAG,MAAM,CAAC;AAEnD,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAC1C,MAAM,CAAC,MAAM,oBAAoB,GAAgB,gBAAgB,CAAC;AAElE;;;GAGG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,CAAC"}
@@ -1,4 +1,5 @@
1
1
  export { DASHBOARD_PAGE_HTML } from "./page.js";
2
- export { type DashboardDeps, type DashboardOptions, type GhostrailHealth, type GhostrailView, collectViews, createDashboardServer, defaultStateDir, } from "./server.js";
3
- export { type DashboardRoute, isCrossOriginWrite, parseDashboardRoute, parseWatchUpdate, } from "./routes.js";
2
+ export { REVIEW_PAGE_HTML } from "./review-page.js";
3
+ export { type DashboardDeps, type DashboardOptions, type GhostrailHealth, type GhostrailView, MERGE_TRIGGER_COMMENT, collectViews, createDashboardServer, defaultStateDir, } from "./server.js";
4
+ export { type DashboardRoute, isCrossOriginWrite, parseDashboardRoute, parseGhostrailQuery, parseWatchUpdate, } from "./routes.js";
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dashboard/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,YAAY,EACZ,qBAAqB,EACrB,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,KAAK,cAAc,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dashboard/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,qBAAqB,EACrB,YAAY,EACZ,qBAAqB,EACrB,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,KAAK,cAAc,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,aAAa,CAAC"}
@@ -1,4 +1,5 @@
1
1
  export { DASHBOARD_PAGE_HTML } from "./page.js";
2
- export { collectViews, createDashboardServer, defaultStateDir, } from "./server.js";
3
- export { isCrossOriginWrite, parseDashboardRoute, parseWatchUpdate, } from "./routes.js";
2
+ export { REVIEW_PAGE_HTML } from "./review-page.js";
3
+ export { MERGE_TRIGGER_COMMENT, collectViews, createDashboardServer, defaultStateDir, } from "./server.js";
4
+ export { isCrossOriginWrite, parseDashboardRoute, parseGhostrailQuery, parseWatchUpdate, } from "./routes.js";
4
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/dashboard/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAKL,YAAY,EACZ,qBAAqB,EACrB,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EAEL,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/dashboard/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAKL,qBAAqB,EACrB,YAAY,EACZ,qBAAqB,EACrB,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EAEL,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,aAAa,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/dashboard/page.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,QAoQ/B,CAAC"}
1
+ {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/dashboard/page.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,QAwU/B,CAAC"}
@@ -34,7 +34,9 @@ export const DASHBOARD_PAGE_HTML = `<!doctype html>
34
34
  table.runs th { font-weight: 600; opacity: .7; font-size: .85em; }
35
35
  a { color: inherit; }
36
36
  .state { font-weight: 600; }
37
- .done { color: #2a9d3a; } .failed { color: #d33; } .blocked { color: #d90; }
37
+ .hist-entry { margin: 6px 0 0 0; padding-left: 8px; border-left: 2px solid #ddd; }
38
+ .row-err { font-size: 12px; color: #b3261e; margin-top: 2px; overflow-wrap: anywhere; }
39
+ .done { color: #2a9d3a; } .failed { color: #d33; } .blocked, .abandoned { color: #d90; }
38
40
  .noop, .skipped { opacity: .6; } .muted { opacity: .6; }
39
41
  .twirl {
40
42
  background: none; border: none; cursor: pointer; font: inherit; padding: 0 .3rem;
@@ -47,6 +49,10 @@ export const DASHBOARD_PAGE_HTML = `<!doctype html>
47
49
  .detail-cell ul { margin: .25rem 0 0; padding-left: 1.2rem; }
48
50
  .subtable { border-collapse: collapse; margin-top: .25rem; }
49
51
  .subtable td { padding: .15rem .5rem .15rem 0; border-bottom: none; }
52
+ .review { margin-left: auto; font-size: .85em; }
53
+ .merge-row { display: flex; align-items: center; gap: .5rem; }
54
+ .merge-btn { font: inherit; }
55
+ .merge-status { font-size: .85em; opacity: .8; }
50
56
  </style>
51
57
  <h1>ghostrails</h1>
52
58
  <p class="sub">The checkbox is what <code>ghostrail start</code> should bring up. The pill beside it is what is running right now.</p>
@@ -124,6 +130,34 @@ function prStatusHtml(status) {
124
130
  return parts.join("");
125
131
  }
126
132
 
133
+ function mergeButtonHtml(path, r) {
134
+ if (r.prNumber === undefined) return "";
135
+ return "<div class='merge-row'><button class='merge-btn' type='button' data-path='" + esc(path) +
136
+ "' data-run='" + esc(r.runId) + "'>resolve conflicts and merge</button>" +
137
+ "<span class='merge-status'></span></div>";
138
+ }
139
+
140
+ /**
141
+ * What earlier passes said, once a later one has superseded them. The row's own
142
+ * Error and Comment above are always the latest pass's; this is the record of
143
+ * the ones before it, so a comment posted on the PR two passes ago is still
144
+ * readable here (TJ-1676).
145
+ */
146
+ function outcomeHistoryHtml(row) {
147
+ const all = row.outcomes || [];
148
+ if (all.length < 2) return "";
149
+ const earlier = all.slice(0, -1).reverse();
150
+ const items = earlier.map((o) => {
151
+ const said = o.error || o.summary || "";
152
+ return "<div class=hist-entry>" +
153
+ "<span class='state " + esc(o.state) + "'>" + esc(o.state) + "</span> " +
154
+ "<span class=muted>" + esc(fmtWhen(o.ts)) + "</span>" +
155
+ (said ? "<pre>" + esc(said) + "</pre>" : "") +
156
+ "</div>";
157
+ }).join("");
158
+ return "<div><b>Earlier passes</b>" + items + "</div>";
159
+ }
160
+
127
161
  function detailHtml(path, r) {
128
162
  const parts = [];
129
163
  parts.push(
@@ -141,11 +175,13 @@ function detailHtml(path, r) {
141
175
  parts.push("<div><b>Gate</b>" + gateStepsHtml(r.gate) + "</div>");
142
176
  if (r.error) parts.push("<div><b>Error</b><pre class=failed>" + esc(r.error) + "</pre></div>");
143
177
  if (r.summary) parts.push("<div><b>Comment</b><pre>" + esc(r.summary) + "</pre></div>");
178
+ parts.push(outcomeHistoryHtml(r));
144
179
  if (r.prUrl) {
145
180
  parts.push(
146
181
  "<div><b>PR</b> <a href='" + esc(r.prUrl) + "'" + NEWTAB + ">" + esc(r.prUrl) + "</a></div>",
147
182
  );
148
183
  }
184
+ parts.push(mergeButtonHtml(path, r));
149
185
  parts.push(artifactsHtml(path, r.artifacts));
150
186
  if (r.prNumber !== undefined) parts.push(prStatusHtml(prStatus.get(rowKey(path, r.runId))));
151
187
  parts.push(
@@ -156,6 +192,14 @@ function detailHtml(path, r) {
156
192
  return parts.join("");
157
193
  }
158
194
 
195
+ /** A failed row's reason on the summary line, truncated, full text on hover (TJ-1676). */
196
+ function errLine(r) {
197
+ if (!r.error) return "";
198
+ const oneLine = String(r.error).replace(/\\s+/g, " ").trim();
199
+ const short = oneLine.length > 120 ? oneLine.slice(0, 119) + "…" : oneLine;
200
+ return "<div class='row-err' title='" + esc(oneLine) + "'>" + esc(short) + "</div>";
201
+ }
202
+
159
203
  function runsHtml(g, gi) {
160
204
  if (!g.runs.length) {
161
205
  return "<p class='none'>" + (g.health === "ok" ? "no runs yet" : esc(g.health)) + "</p>";
@@ -172,7 +216,7 @@ function runsHtml(g, gi) {
172
216
  " aria-expanded='" + open + "' data-path='" + esc(g.path) + "' data-run='" + esc(r.runId) + "'" +
173
217
  " title='show detail'>▶</button></td>" +
174
218
  "<td class=muted>" + esc(r.runId.slice(0, 8)) + "</td>" +
175
- "<td>" + itemLink(r, r.title || r.itemId || "") + "</td>" +
219
+ "<td>" + itemLink(r, r.title || r.itemId || "") + errLine(r) + "</td>" +
176
220
  "<td class='state " + state + "'>" + state + "</td>" +
177
221
  "<td>" + esc(r.backend || "") + "</td>" +
178
222
  "<td>" + gate + "</td>" +
@@ -199,6 +243,7 @@ function render(ghostrails) {
199
243
  "<span class='name'>" + esc(g.name) + "</span>" +
200
244
  "<span class='pill'>" + esc(g.state) + "</span>" +
201
245
  "<span class='path'>" + esc(g.path) + "</span>" +
246
+ "<a class='review' href='" + esc(api("review?path=" + enc(g.path))) + "'>review</a>" +
202
247
  "</div>" + runsHtml(g, gi) + "</div>"
203
248
  ).join("");
204
249
  }
@@ -236,11 +281,34 @@ function toggle(path, runId, buttonId) {
236
281
  if (expanded.has(k)) refreshPrStatus();
237
282
  }
238
283
 
284
+ async function postMerge(path, runId, button) {
285
+ const status = button.parentElement.querySelector(".merge-status");
286
+ button.disabled = true;
287
+ if (status) status.textContent = "posting…";
288
+ try {
289
+ const res = await fetch(
290
+ api("/api/merge?path=" + enc(path) + "&run=" + enc(runId)),
291
+ { method: "POST" },
292
+ );
293
+ const data = await res.json();
294
+ if (!res.ok || data.posted !== true) throw new Error(data.error || res.statusText);
295
+ if (status) status.textContent = "posted — the next respond pass will pick it up";
296
+ } catch (err) {
297
+ if (status) status.textContent = "failed: " + err.message;
298
+ button.disabled = false;
299
+ }
300
+ }
301
+
239
302
  const list = document.getElementById("list");
240
303
 
241
304
  list.addEventListener("click", (e) => {
242
- const btn = e.target.closest(".twirl");
243
- if (btn) toggle(btn.dataset.path, btn.dataset.run, btn.id);
305
+ const twirlBtn = e.target.closest(".twirl");
306
+ if (twirlBtn) {
307
+ toggle(twirlBtn.dataset.path, twirlBtn.dataset.run, twirlBtn.id);
308
+ return;
309
+ }
310
+ const mergeBtn = e.target.closest(".merge-btn");
311
+ if (mergeBtn) postMerge(mergeBtn.dataset.path, mergeBtn.dataset.run, mergeBtn);
244
312
  });
245
313
 
246
314
  list.addEventListener("change", async (e) => {
@@ -1 +1 @@
1
- {"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/dashboard/page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mCAyJA,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2GnE,CAAC"}
1
+ {"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/dashboard/page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mCAqMA,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmInE,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The review-mode page: one ghostrail's runs, one at a time, with Prev/Next.
3
+ *
4
+ * Inline, like the dashboard's own page, so the package ships one file and the
5
+ * server needs no static asset handling. It reads `path` (which ghostrail),
6
+ * `run` (which run to open first), and `token` from `location.search`, the
7
+ * same way the dashboard page reads `token`.
8
+ *
9
+ * The detail rendering (gate steps, artifacts, PR status) intentionally mirrors
10
+ * `page.ts`'s: same information, same shape, because this is the same detail
11
+ * panel shown one run at a time instead of behind a twirl.
12
+ */
13
+ export declare const REVIEW_PAGE_HTML: string;
14
+ //# sourceMappingURL=review-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-page.d.ts","sourceRoot":"","sources":["../../src/dashboard/review-page.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,QAiP5B,CAAC"}
@@ -0,0 +1,256 @@
1
+ import { IN_PROGRESS_STATE } from "../store/events.js";
2
+ /**
3
+ * The review-mode page: one ghostrail's runs, one at a time, with Prev/Next.
4
+ *
5
+ * Inline, like the dashboard's own page, so the package ships one file and the
6
+ * server needs no static asset handling. It reads `path` (which ghostrail),
7
+ * `run` (which run to open first), and `token` from `location.search`, the
8
+ * same way the dashboard page reads `token`.
9
+ *
10
+ * The detail rendering (gate steps, artifacts, PR status) intentionally mirrors
11
+ * `page.ts`'s: same information, same shape, because this is the same detail
12
+ * panel shown one run at a time instead of behind a twirl.
13
+ */
14
+ export const REVIEW_PAGE_HTML = `<!doctype html>
15
+ <meta charset="utf-8">
16
+ <title>ghostrail review</title>
17
+ <style>
18
+ :root { color-scheme: light dark; }
19
+ body { font: 15px/1.5 ui-sans-serif, system-ui, sans-serif; margin: 2rem auto; max-width: 56rem; padding: 0 1rem; }
20
+ h1 { font-size: 1.3rem; margin: 0 0 .25rem; }
21
+ .sub { opacity: .7; margin: 0 0 1.5rem; }
22
+ .back { display: inline-block; margin-bottom: 1rem; }
23
+ .nav { display: flex; align-items: center; gap: .75rem; margin-bottom: 1rem; }
24
+ .nav button { font: inherit; padding: .3rem .8rem; }
25
+ .pos { opacity: .7; font-size: .9em; }
26
+ .panel { border: 1px solid color-mix(in oklab, currentColor 20%, transparent); border-radius: 8px; padding: 1rem 1.25rem; }
27
+ .panel > div { margin-bottom: .5rem; }
28
+ .panel pre { white-space: pre-wrap; word-break: break-word; margin: .25rem 0 0; font: inherit; }
29
+ .panel ul { margin: .25rem 0 0; padding-left: 1.2rem; }
30
+ .title { font-weight: 600; font-size: 1.1em; }
31
+ .state { font-weight: 600; }
32
+ .done { color: #2a9d3a; } .failed { color: #d33; } .blocked, .abandoned { color: #d90; }
33
+ .noop, .skipped { opacity: .6; } .muted { opacity: .6; }
34
+ a { color: inherit; }
35
+ .subtable { border-collapse: collapse; margin-top: .25rem; }
36
+ .subtable td { padding: .15rem .5rem .15rem 0; border-bottom: none; }
37
+ .merge-btn { font: inherit; }
38
+ .merge-status { margin-left: .5rem; font-size: .85em; }
39
+ .none { opacity: .55; }
40
+ .err { color: #b00; }
41
+ </style>
42
+ <a class="back" href="./">&larr; back to the dashboard</a>
43
+ <h1 id="heading">review</h1>
44
+ <p class="sub" id="sub"></p>
45
+ <div class="nav" id="nav" hidden>
46
+ <button id="prev" type="button">&larr; prev</button>
47
+ <span class="pos" id="pos"></span>
48
+ <button id="next" type="button">next &rarr;</button>
49
+ </div>
50
+ <div id="body">loading…</div>
51
+ <script>
52
+ const params = new URLSearchParams(location.search);
53
+ const ghostrailPath = params.get("path") || "";
54
+ const tok = params.get("token");
55
+ const api = (p) => p + (tok ? (p.includes("?") ? "&" : "?") + "token=" + encodeURIComponent(tok) : "");
56
+ const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) =>
57
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c] || c));
58
+ const enc = encodeURIComponent;
59
+ // Every link this page renders leaves it, so it opens in a new tab, with the
60
+ // \`rel\` that stops the opened page reaching back through \`window.opener\`. Same
61
+ // rule, same reason, as the dashboard page (TJ-1653).
62
+ const NEWTAB = " target='_blank' rel='noopener noreferrer'";
63
+ // The item's label, linked to whatever the run's \`url\` points at when the row
64
+ // carries one.
65
+ const itemLink = (r, label) =>
66
+ r.url ? "<a href='" + esc(r.url) + "'" + NEWTAB + ">" + esc(label) + "</a>" : esc(label);
67
+
68
+ let runs = [];
69
+ let index = 0;
70
+ let name = "";
71
+ const prStatus = new Map();
72
+
73
+ function fmtWhen(ts) {
74
+ return ts ? new Date(ts).toLocaleString() : "";
75
+ }
76
+
77
+ function gateStepsHtml(gate) {
78
+ if (!gate || gate.length === 0) return "<div class=muted>no gate steps</div>";
79
+ const rows = gate.map((s) =>
80
+ "<tr><td>" + esc(s.name) + "</td><td class='" + (s.exitCode === 0 ? "done" : "failed") + "'>" +
81
+ (s.exitCode === 0 ? "passed" : "failed (exit " + esc(s.exitCode) + ")") + "</td><td class=muted>" +
82
+ esc(s.durationMs) + "ms</td></tr>"
83
+ ).join("");
84
+ return "<table class=subtable>" + rows + "</table>";
85
+ }
86
+
87
+ function artifactsHtml(artifacts) {
88
+ if (!artifacts || artifacts.length === 0) return "";
89
+ const items = artifacts.map((a) => {
90
+ const href = api("/api/artifact?path=" + enc(ghostrailPath) + "&hash=" + enc(a.hash));
91
+ return "<li><a href='" + esc(href) + "'" + NEWTAB + ">" + esc(a.name) + "</a></li>";
92
+ }).join("");
93
+ return "<div><b>Artifacts</b><ul>" + items + "</ul></div>";
94
+ }
95
+
96
+ function checkLineHtml(c) {
97
+ const label = c.conclusion || c.status;
98
+ const cls = c.conclusion === "success" ? "done" : c.conclusion ? "failed" : "muted";
99
+ const label2 = c.url
100
+ ? "<a href='" + esc(c.url) + "'" + NEWTAB + ">" + esc(c.name) + "</a>"
101
+ : esc(c.name);
102
+ return "<li>" + label2 + ": <span class='" + cls + "'>" + esc(label) + "</span></li>";
103
+ }
104
+
105
+ function prStatusHtml(status) {
106
+ if (!status) return "<div class=muted>loading CI status…</div>";
107
+ if (!status.available) {
108
+ return "<div class=muted>PR status unknown" + (status.error ? ": " + esc(status.error) : "") + "</div>";
109
+ }
110
+ const merged = status.state === "merged";
111
+ const parts = [
112
+ "<div><b>PR state:</b> " + esc(status.state) +
113
+ (merged && status.mergedAt ? " at " + esc(new Date(status.mergedAt).toLocaleString()) : "") + "</div>",
114
+ ];
115
+ if (status.checks && status.checks.length > 0) {
116
+ parts.push("<div><b>CI checks</b><ul>" + status.checks.map(checkLineHtml).join("") + "</ul></div>");
117
+ } else {
118
+ parts.push("<div class=muted>no CI checks reported</div>");
119
+ }
120
+ return parts.join("");
121
+ }
122
+
123
+ function mergeButtonHtml(r) {
124
+ if (r.prNumber === undefined) return "";
125
+ return "<div><button class='merge-btn' type='button' data-run='" + esc(r.runId) +
126
+ "'>resolve conflicts and merge</button><span class='merge-status' id='merge-status'></span></div>";
127
+ }
128
+
129
+ function detailHtml(r) {
130
+ const parts = [];
131
+ parts.push(
132
+ "<div class=title>" + itemLink(r, r.title || r.itemId || "(untitled run)") + "</div>",
133
+ );
134
+ parts.push(
135
+ "<div><b>Item</b> " + itemLink(r, r.itemId || "") +
136
+ (r.kind ? " (" + esc(r.kind) + ")" : "") + "</div>",
137
+ );
138
+ const state = esc(r.state || ${JSON.stringify(IN_PROGRESS_STATE)});
139
+ parts.push("<div><b>State</b> <span class='state " + state + "'>" + state + "</span></div>");
140
+ if (r.branch) parts.push("<div><b>Branch</b> " + esc(r.branch) + "</div>");
141
+ if (r.backend) parts.push("<div><b>Backend</b> " + esc(r.backend) + "</div>");
142
+ if (r.costUsd !== undefined) {
143
+ parts.push("<div><b>Cost</b> $" + esc(Number(r.costUsd).toFixed(4)) + "</div>");
144
+ }
145
+ if (r.tokensIn !== undefined || r.tokensOut !== undefined) {
146
+ parts.push("<div><b>Tokens</b> in " + esc(r.tokensIn ?? 0) + " / out " + esc(r.tokensOut ?? 0) + "</div>");
147
+ }
148
+ parts.push("<div><b>Gate</b>" + gateStepsHtml(r.gate) + "</div>");
149
+ if (r.error) parts.push("<div><b>Error</b><pre class=failed>" + esc(r.error) + "</pre></div>");
150
+ if (r.summary) parts.push("<div><b>Comment</b><pre>" + esc(r.summary) + "</pre></div>");
151
+ if (r.prUrl) {
152
+ parts.push(
153
+ "<div><b>PR</b> <a href='" + esc(r.prUrl) + "'" + NEWTAB + ">" + esc(r.prUrl) + "</a></div>",
154
+ );
155
+ }
156
+ parts.push(mergeButtonHtml(r));
157
+ parts.push(artifactsHtml(r.artifacts));
158
+ if (r.prNumber !== undefined) parts.push(prStatusHtml(prStatus.get(r.runId)));
159
+ parts.push(
160
+ "<div class=muted>started " + esc(fmtWhen(r.startedAt)) +
161
+ (r.finishedAt ? ", finished " + esc(fmtWhen(r.finishedAt)) : ", still running") +
162
+ "</div>",
163
+ );
164
+ return parts.join("");
165
+ }
166
+
167
+ function render() {
168
+ document.getElementById("sub").textContent = name ? name + " — " + ghostrailPath : ghostrailPath;
169
+ const nav = document.getElementById("nav");
170
+ const body = document.getElementById("body");
171
+ if (runs.length === 0) {
172
+ nav.hidden = true;
173
+ body.innerHTML = "<p class=none>no runs yet</p>";
174
+ return;
175
+ }
176
+ nav.hidden = false;
177
+ document.getElementById("pos").textContent = "run " + (index + 1) + " of " + runs.length;
178
+ document.getElementById("prev").disabled = index <= 0;
179
+ document.getElementById("next").disabled = index >= runs.length - 1;
180
+ body.innerHTML = "<div class=panel>" + detailHtml(runs[index]) + "</div>";
181
+ const run = runs[index];
182
+ const next = new URLSearchParams(location.search);
183
+ next.set("run", run.runId);
184
+ history.replaceState(null, "", location.pathname + "?" + next.toString());
185
+ // Fetched once per run and cached: without the guard, a successful fetch
186
+ // triggers a re-render, which (with no guard) would ask for status again,
187
+ // forever.
188
+ if (run.prNumber !== undefined && !prStatus.has(run.runId)) loadPrStatus(run.runId);
189
+ }
190
+
191
+ async function loadPrStatus(runId) {
192
+ let status;
193
+ try {
194
+ status = await (
195
+ await fetch(api("/api/pr-status?path=" + enc(ghostrailPath) + "&run=" + enc(runId)))
196
+ ).json();
197
+ } catch {
198
+ return;
199
+ }
200
+ prStatus.set(runId, status);
201
+ if (runs[index] && runs[index].runId === runId) render();
202
+ }
203
+
204
+ async function postMerge(runId, button) {
205
+ const status = document.getElementById("merge-status");
206
+ button.disabled = true;
207
+ if (status) status.textContent = "posting…";
208
+ try {
209
+ const res = await fetch(
210
+ api("/api/merge?path=" + enc(ghostrailPath) + "&run=" + enc(runId)),
211
+ { method: "POST" },
212
+ );
213
+ const data = await res.json();
214
+ if (!res.ok || data.posted !== true) throw new Error(data.error || res.statusText);
215
+ if (status) status.textContent = "posted — the next respond pass will pick it up";
216
+ } catch (err) {
217
+ if (status) status.textContent = "failed: " + err.message;
218
+ button.disabled = false;
219
+ }
220
+ }
221
+
222
+ document.getElementById("body").addEventListener("click", (e) => {
223
+ const btn = e.target.closest(".merge-btn");
224
+ if (btn) postMerge(btn.dataset.run, btn);
225
+ });
226
+
227
+ document.getElementById("prev").addEventListener("click", () => {
228
+ if (index > 0) { index -= 1; render(); }
229
+ });
230
+ document.getElementById("next").addEventListener("click", () => {
231
+ if (index < runs.length - 1) { index += 1; render(); }
232
+ });
233
+
234
+ async function load() {
235
+ if (!ghostrailPath) {
236
+ document.getElementById("body").innerHTML = "<p class=err>no ?path= given</p>";
237
+ return;
238
+ }
239
+ try {
240
+ const res = await fetch(api("/api/runs?path=" + enc(ghostrailPath)));
241
+ if (!res.ok) throw new Error((await res.json()).error || res.statusText);
242
+ const data = await res.json();
243
+ name = data.name || "";
244
+ runs = data.runs || [];
245
+ const wanted = params.get("run");
246
+ const found = wanted ? runs.findIndex((r) => r.runId === wanted) : -1;
247
+ index = found >= 0 ? found : 0;
248
+ render();
249
+ } catch (err) {
250
+ document.getElementById("body").innerHTML = "<p class=err>" + esc(err.message) + "</p>";
251
+ }
252
+ }
253
+ load();
254
+ </script>
255
+ `;
256
+ //# sourceMappingURL=review-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-page.js","sourceRoot":"","sources":["../../src/dashboard/review-page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iCA4HC,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqHjE,CAAC"}
@@ -6,6 +6,8 @@ import type { IncomingHttpHeaders } from "node:http";
6
6
  */
7
7
  export type DashboardRoute = {
8
8
  kind: "page";
9
+ } | {
10
+ kind: "review";
9
11
  } | {
10
12
  kind: "ghostrails";
11
13
  } | {
@@ -14,11 +16,27 @@ export type DashboardRoute = {
14
16
  kind: "prStatus";
15
17
  } | {
16
18
  kind: "artifact";
19
+ } | {
20
+ kind: "runs";
21
+ } | {
22
+ kind: "merge";
17
23
  } | {
18
24
  kind: "notfound";
19
25
  };
20
26
  /** Map a request pathname to a route. Total; never throws. */
21
27
  export declare function parseDashboardRoute(pathname: string): DashboardRoute;
28
+ /**
29
+ * The ghostrail a request names, or undefined when `path` is missing or
30
+ * empty.
31
+ *
32
+ * Shares `parseDetailQuery`'s reasoning for using a query parameter rather
33
+ * than a path segment (a ghostrail's identity does not survive one), but
34
+ * `GET /api/runs` needs only `path`, not a second key naming a run or a
35
+ * hash within it.
36
+ */
37
+ export declare function parseGhostrailQuery(params: URLSearchParams): {
38
+ path: string;
39
+ } | undefined;
22
40
  /**
23
41
  * The ghostrail a detail request is about, plus the run or artifact within it.
24
42
  *
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/dashboard/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAGrD;;;;GAIG;AAEH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AAEzB,8DAA8D;AAC9D,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAcpE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,KAAK,GAAG,MAAM,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAM7C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAI7E;AA2BD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAYxE;AAOD;;;;;;GAMG;AACH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAY3F"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/dashboard/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAGrD;;;;GAIG;AAEH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AAEzB,8DAA8D;AAC9D,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAoBpE;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAIzF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,KAAK,GAAG,MAAM,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAM7C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAI7E;AA2BD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAYxE;AAOD;;;;;;GAMG;AACH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAY3F"}