ghostrail 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) 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/board/page.js +1 -1
  7. package/dist/cli/commands.d.ts +12 -7
  8. package/dist/cli/commands.d.ts.map +1 -1
  9. package/dist/cli/commands.js +261 -43
  10. package/dist/cli/commands.js.map +1 -1
  11. package/dist/commands/help.d.ts.map +1 -1
  12. package/dist/commands/help.js +5 -3
  13. package/dist/commands/help.js.map +1 -1
  14. package/dist/config/load.d.ts.map +1 -1
  15. package/dist/config/load.js +20 -3
  16. package/dist/config/load.js.map +1 -1
  17. package/dist/config/schema.d.ts +57 -0
  18. package/dist/config/schema.d.ts.map +1 -1
  19. package/dist/config/schema.js +19 -0
  20. package/dist/config/schema.js.map +1 -1
  21. package/dist/dashboard/index.d.ts +3 -2
  22. package/dist/dashboard/index.d.ts.map +1 -1
  23. package/dist/dashboard/index.js +3 -2
  24. package/dist/dashboard/index.js.map +1 -1
  25. package/dist/dashboard/page.d.ts.map +1 -1
  26. package/dist/dashboard/page.js +39 -3
  27. package/dist/dashboard/page.js.map +1 -1
  28. package/dist/dashboard/review-page.d.ts +14 -0
  29. package/dist/dashboard/review-page.d.ts.map +1 -0
  30. package/dist/dashboard/review-page.js +256 -0
  31. package/dist/dashboard/review-page.js.map +1 -0
  32. package/dist/dashboard/routes.d.ts +18 -0
  33. package/dist/dashboard/routes.d.ts.map +1 -1
  34. package/dist/dashboard/routes.js +24 -0
  35. package/dist/dashboard/routes.js.map +1 -1
  36. package/dist/dashboard/server.d.ts +19 -0
  37. package/dist/dashboard/server.d.ts.map +1 -1
  38. package/dist/dashboard/server.js +71 -1
  39. package/dist/dashboard/server.js.map +1 -1
  40. package/dist/doctor/checks.d.ts +21 -0
  41. package/dist/doctor/checks.d.ts.map +1 -1
  42. package/dist/doctor/checks.js +44 -0
  43. package/dist/doctor/checks.js.map +1 -1
  44. package/dist/doctor/probes.d.ts +15 -0
  45. package/dist/doctor/probes.d.ts.map +1 -1
  46. package/dist/doctor/probes.js +33 -0
  47. package/dist/doctor/probes.js.map +1 -1
  48. package/dist/doctor/run.d.ts +2 -1
  49. package/dist/doctor/run.d.ts.map +1 -1
  50. package/dist/doctor/run.js +25 -2
  51. package/dist/doctor/run.js.map +1 -1
  52. package/dist/factory/build.d.ts +6 -0
  53. package/dist/factory/build.d.ts.map +1 -1
  54. package/dist/factory/build.js +17 -5
  55. package/dist/factory/build.js.map +1 -1
  56. package/dist/global/tempdir.d.ts +19 -0
  57. package/dist/global/tempdir.d.ts.map +1 -0
  58. package/dist/global/tempdir.js +47 -0
  59. package/dist/global/tempdir.js.map +1 -0
  60. package/dist/init/greenroom.d.ts +18 -0
  61. package/dist/init/greenroom.d.ts.map +1 -0
  62. package/dist/init/greenroom.js +24 -0
  63. package/dist/init/greenroom.js.map +1 -0
  64. package/dist/loop/engine.d.ts +9 -0
  65. package/dist/loop/engine.d.ts.map +1 -1
  66. package/dist/loop/engine.js +114 -7
  67. package/dist/loop/engine.js.map +1 -1
  68. package/dist/loop/index.d.ts +1 -0
  69. package/dist/loop/index.d.ts.map +1 -1
  70. package/dist/loop/index.js +1 -0
  71. package/dist/loop/index.js.map +1 -1
  72. package/dist/loop/ports.d.ts +7 -0
  73. package/dist/loop/ports.d.ts.map +1 -1
  74. package/dist/loop/record.d.ts +6 -1
  75. package/dist/loop/record.d.ts.map +1 -1
  76. package/dist/loop/retry.d.ts +50 -0
  77. package/dist/loop/retry.d.ts.map +1 -0
  78. package/dist/loop/retry.js +77 -0
  79. package/dist/loop/retry.js.map +1 -0
  80. package/dist/loop/schedule.d.ts +33 -0
  81. package/dist/loop/schedule.d.ts.map +1 -0
  82. package/dist/loop/schedule.js +28 -0
  83. package/dist/loop/schedule.js.map +1 -0
  84. package/dist/respond/authors.d.ts +38 -0
  85. package/dist/respond/authors.d.ts.map +1 -0
  86. package/dist/respond/authors.js +50 -0
  87. package/dist/respond/authors.js.map +1 -0
  88. package/dist/respond/merge.d.ts.map +1 -1
  89. package/dist/respond/merge.js +25 -19
  90. package/dist/respond/merge.js.map +1 -1
  91. package/dist/respond/respond.d.ts +7 -0
  92. package/dist/respond/respond.d.ts.map +1 -1
  93. package/dist/respond/respond.js +36 -16
  94. package/dist/respond/respond.js.map +1 -1
  95. package/dist/respond/run-id.d.ts +17 -0
  96. package/dist/respond/run-id.d.ts.map +1 -0
  97. package/dist/respond/run-id.js +23 -0
  98. package/dist/respond/run-id.js.map +1 -0
  99. package/dist/respond/run-row.d.ts +24 -0
  100. package/dist/respond/run-row.d.ts.map +1 -0
  101. package/dist/respond/run-row.js +28 -0
  102. package/dist/respond/run-row.js.map +1 -0
  103. package/dist/store/events.d.ts +12 -0
  104. package/dist/store/events.d.ts.map +1 -1
  105. package/dist/store/events.js +10 -3
  106. package/dist/store/events.js.map +1 -1
  107. package/dist/store/index.d.ts +1 -1
  108. package/dist/store/index.d.ts.map +1 -1
  109. package/dist/store/index.js.map +1 -1
  110. package/dist/triage/engine.d.ts +14 -0
  111. package/dist/triage/engine.d.ts.map +1 -1
  112. package/dist/triage/engine.js +73 -3
  113. package/dist/triage/engine.js.map +1 -1
  114. package/package.json +1 -1
  115. package/templates/code/ghostrail.toml +4 -0
  116. 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,QAwS/B,CAAC"}
@@ -34,7 +34,7 @@ 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
+ .done { color: #2a9d3a; } .failed { color: #d33; } .blocked, .abandoned { color: #d90; }
38
38
  .noop, .skipped { opacity: .6; } .muted { opacity: .6; }
39
39
  .twirl {
40
40
  background: none; border: none; cursor: pointer; font: inherit; padding: 0 .3rem;
@@ -47,6 +47,10 @@ export const DASHBOARD_PAGE_HTML = `<!doctype html>
47
47
  .detail-cell ul { margin: .25rem 0 0; padding-left: 1.2rem; }
48
48
  .subtable { border-collapse: collapse; margin-top: .25rem; }
49
49
  .subtable td { padding: .15rem .5rem .15rem 0; border-bottom: none; }
50
+ .review { margin-left: auto; font-size: .85em; }
51
+ .merge-row { display: flex; align-items: center; gap: .5rem; }
52
+ .merge-btn { font: inherit; }
53
+ .merge-status { font-size: .85em; opacity: .8; }
50
54
  </style>
51
55
  <h1>ghostrails</h1>
52
56
  <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 +128,13 @@ function prStatusHtml(status) {
124
128
  return parts.join("");
125
129
  }
126
130
 
131
+ function mergeButtonHtml(path, r) {
132
+ if (r.prNumber === undefined) return "";
133
+ return "<div class='merge-row'><button class='merge-btn' type='button' data-path='" + esc(path) +
134
+ "' data-run='" + esc(r.runId) + "'>resolve conflicts and merge</button>" +
135
+ "<span class='merge-status'></span></div>";
136
+ }
137
+
127
138
  function detailHtml(path, r) {
128
139
  const parts = [];
129
140
  parts.push(
@@ -146,6 +157,7 @@ function detailHtml(path, r) {
146
157
  "<div><b>PR</b> <a href='" + esc(r.prUrl) + "'" + NEWTAB + ">" + esc(r.prUrl) + "</a></div>",
147
158
  );
148
159
  }
160
+ parts.push(mergeButtonHtml(path, r));
149
161
  parts.push(artifactsHtml(path, r.artifacts));
150
162
  if (r.prNumber !== undefined) parts.push(prStatusHtml(prStatus.get(rowKey(path, r.runId))));
151
163
  parts.push(
@@ -199,6 +211,7 @@ function render(ghostrails) {
199
211
  "<span class='name'>" + esc(g.name) + "</span>" +
200
212
  "<span class='pill'>" + esc(g.state) + "</span>" +
201
213
  "<span class='path'>" + esc(g.path) + "</span>" +
214
+ "<a class='review' href='" + esc(api("review?path=" + enc(g.path))) + "'>review</a>" +
202
215
  "</div>" + runsHtml(g, gi) + "</div>"
203
216
  ).join("");
204
217
  }
@@ -236,11 +249,34 @@ function toggle(path, runId, buttonId) {
236
249
  if (expanded.has(k)) refreshPrStatus();
237
250
  }
238
251
 
252
+ async function postMerge(path, runId, button) {
253
+ const status = button.parentElement.querySelector(".merge-status");
254
+ button.disabled = true;
255
+ if (status) status.textContent = "posting…";
256
+ try {
257
+ const res = await fetch(
258
+ api("/api/merge?path=" + enc(path) + "&run=" + enc(runId)),
259
+ { method: "POST" },
260
+ );
261
+ const data = await res.json();
262
+ if (!res.ok || data.posted !== true) throw new Error(data.error || res.statusText);
263
+ if (status) status.textContent = "posted — the next respond pass will pick it up";
264
+ } catch (err) {
265
+ if (status) status.textContent = "failed: " + err.message;
266
+ button.disabled = false;
267
+ }
268
+ }
269
+
239
270
  const list = document.getElementById("list");
240
271
 
241
272
  list.addEventListener("click", (e) => {
242
- const btn = e.target.closest(".twirl");
243
- if (btn) toggle(btn.dataset.path, btn.dataset.run, btn.id);
273
+ const twirlBtn = e.target.closest(".twirl");
274
+ if (twirlBtn) {
275
+ toggle(twirlBtn.dataset.path, twirlBtn.dataset.run, twirlBtn.id);
276
+ return;
277
+ }
278
+ const mergeBtn = e.target.closest(".merge-btn");
279
+ if (mergeBtn) postMerge(mergeBtn.dataset.path, mergeBtn.dataset.run, mergeBtn);
244
280
  });
245
281
 
246
282
  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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mCAqKA,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"}
@@ -7,6 +7,11 @@ export function parseDashboardRoute(pathname) {
7
7
  // for something that means the root by every reading.
8
8
  if (parts.length === 0)
9
9
  return { kind: "page" };
10
+ // The review page is its own top-level page, not an /api route: it is a
11
+ // second HTML surface (own script, own polling), the same way the board's
12
+ // page and the dashboard's page are each their own top-level route.
13
+ if (parts.length === 1 && parts[0] === "review")
14
+ return { kind: "review" };
10
15
  if (parts[0] !== "api" || parts.length !== 2)
11
16
  return { kind: "notfound" };
12
17
  if (parts[1] === "ghostrails")
@@ -19,8 +24,27 @@ export function parseDashboardRoute(pathname) {
19
24
  return { kind: "prStatus" };
20
25
  if (parts[1] === "artifact")
21
26
  return { kind: "artifact" };
27
+ if (parts[1] === "runs")
28
+ return { kind: "runs" };
29
+ if (parts[1] === "merge")
30
+ return { kind: "merge" };
22
31
  return { kind: "notfound" };
23
32
  }
33
+ /**
34
+ * The ghostrail a request names, or undefined when `path` is missing or
35
+ * empty.
36
+ *
37
+ * Shares `parseDetailQuery`'s reasoning for using a query parameter rather
38
+ * than a path segment (a ghostrail's identity does not survive one), but
39
+ * `GET /api/runs` needs only `path`, not a second key naming a run or a
40
+ * hash within it.
41
+ */
42
+ export function parseGhostrailQuery(params) {
43
+ const path = params.get("path");
44
+ if (path === null || path.length === 0)
45
+ return undefined;
46
+ return { path };
47
+ }
24
48
  /**
25
49
  * The ghostrail a detail request is about, plus the run or artifact within it.
26
50
  *