ghostrail 0.17.1 → 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 (119) 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.d.ts.map +1 -1
  7. package/dist/board/page.js +44 -10
  8. package/dist/board/page.js.map +1 -1
  9. package/dist/cli/commands.d.ts +12 -7
  10. package/dist/cli/commands.d.ts.map +1 -1
  11. package/dist/cli/commands.js +261 -43
  12. package/dist/cli/commands.js.map +1 -1
  13. package/dist/commands/help.d.ts.map +1 -1
  14. package/dist/commands/help.js +5 -3
  15. package/dist/commands/help.js.map +1 -1
  16. package/dist/config/load.d.ts.map +1 -1
  17. package/dist/config/load.js +20 -3
  18. package/dist/config/load.js.map +1 -1
  19. package/dist/config/schema.d.ts +57 -0
  20. package/dist/config/schema.d.ts.map +1 -1
  21. package/dist/config/schema.js +19 -0
  22. package/dist/config/schema.js.map +1 -1
  23. package/dist/dashboard/index.d.ts +3 -2
  24. package/dist/dashboard/index.d.ts.map +1 -1
  25. package/dist/dashboard/index.js +3 -2
  26. package/dist/dashboard/index.js.map +1 -1
  27. package/dist/dashboard/page.d.ts.map +1 -1
  28. package/dist/dashboard/page.js +64 -10
  29. package/dist/dashboard/page.js.map +1 -1
  30. package/dist/dashboard/review-page.d.ts +14 -0
  31. package/dist/dashboard/review-page.d.ts.map +1 -0
  32. package/dist/dashboard/review-page.js +256 -0
  33. package/dist/dashboard/review-page.js.map +1 -0
  34. package/dist/dashboard/routes.d.ts +18 -0
  35. package/dist/dashboard/routes.d.ts.map +1 -1
  36. package/dist/dashboard/routes.js +24 -0
  37. package/dist/dashboard/routes.js.map +1 -1
  38. package/dist/dashboard/server.d.ts +19 -0
  39. package/dist/dashboard/server.d.ts.map +1 -1
  40. package/dist/dashboard/server.js +71 -1
  41. package/dist/dashboard/server.js.map +1 -1
  42. package/dist/doctor/checks.d.ts +21 -0
  43. package/dist/doctor/checks.d.ts.map +1 -1
  44. package/dist/doctor/checks.js +44 -0
  45. package/dist/doctor/checks.js.map +1 -1
  46. package/dist/doctor/probes.d.ts +15 -0
  47. package/dist/doctor/probes.d.ts.map +1 -1
  48. package/dist/doctor/probes.js +33 -0
  49. package/dist/doctor/probes.js.map +1 -1
  50. package/dist/doctor/run.d.ts +2 -1
  51. package/dist/doctor/run.d.ts.map +1 -1
  52. package/dist/doctor/run.js +25 -2
  53. package/dist/doctor/run.js.map +1 -1
  54. package/dist/factory/build.d.ts +6 -0
  55. package/dist/factory/build.d.ts.map +1 -1
  56. package/dist/factory/build.js +17 -4
  57. package/dist/factory/build.js.map +1 -1
  58. package/dist/global/tempdir.d.ts +19 -0
  59. package/dist/global/tempdir.d.ts.map +1 -0
  60. package/dist/global/tempdir.js +47 -0
  61. package/dist/global/tempdir.js.map +1 -0
  62. package/dist/init/greenroom.d.ts +18 -0
  63. package/dist/init/greenroom.d.ts.map +1 -0
  64. package/dist/init/greenroom.js +24 -0
  65. package/dist/init/greenroom.js.map +1 -0
  66. package/dist/loop/engine.d.ts +10 -1
  67. package/dist/loop/engine.d.ts.map +1 -1
  68. package/dist/loop/engine.js +116 -18
  69. package/dist/loop/engine.js.map +1 -1
  70. package/dist/loop/index.d.ts +1 -0
  71. package/dist/loop/index.d.ts.map +1 -1
  72. package/dist/loop/index.js +1 -0
  73. package/dist/loop/index.js.map +1 -1
  74. package/dist/loop/ports.d.ts +7 -0
  75. package/dist/loop/ports.d.ts.map +1 -1
  76. package/dist/loop/record.d.ts +6 -1
  77. package/dist/loop/record.d.ts.map +1 -1
  78. package/dist/loop/retry.d.ts +50 -0
  79. package/dist/loop/retry.d.ts.map +1 -0
  80. package/dist/loop/retry.js +77 -0
  81. package/dist/loop/retry.js.map +1 -0
  82. package/dist/loop/schedule.d.ts +33 -0
  83. package/dist/loop/schedule.d.ts.map +1 -0
  84. package/dist/loop/schedule.js +28 -0
  85. package/dist/loop/schedule.js.map +1 -0
  86. package/dist/respond/authors.d.ts +38 -0
  87. package/dist/respond/authors.d.ts.map +1 -0
  88. package/dist/respond/authors.js +50 -0
  89. package/dist/respond/authors.js.map +1 -0
  90. package/dist/respond/merge.d.ts.map +1 -1
  91. package/dist/respond/merge.js +47 -0
  92. package/dist/respond/merge.js.map +1 -1
  93. package/dist/respond/respond.d.ts +10 -0
  94. package/dist/respond/respond.d.ts.map +1 -1
  95. package/dist/respond/respond.js +62 -5
  96. package/dist/respond/respond.js.map +1 -1
  97. package/dist/respond/run-id.d.ts +17 -0
  98. package/dist/respond/run-id.d.ts.map +1 -0
  99. package/dist/respond/run-id.js +23 -0
  100. package/dist/respond/run-id.js.map +1 -0
  101. package/dist/respond/run-row.d.ts +24 -0
  102. package/dist/respond/run-row.d.ts.map +1 -0
  103. package/dist/respond/run-row.js +28 -0
  104. package/dist/respond/run-row.js.map +1 -0
  105. package/dist/store/events.d.ts +25 -0
  106. package/dist/store/events.d.ts.map +1 -1
  107. package/dist/store/events.js +30 -3
  108. package/dist/store/events.js.map +1 -1
  109. package/dist/store/index.d.ts +1 -1
  110. package/dist/store/index.d.ts.map +1 -1
  111. package/dist/store/index.js +1 -1
  112. package/dist/store/index.js.map +1 -1
  113. package/dist/triage/engine.d.ts +14 -0
  114. package/dist/triage/engine.d.ts.map +1 -1
  115. package/dist/triage/engine.js +73 -3
  116. package/dist/triage/engine.js.map +1 -1
  117. package/package.json +1 -1
  118. package/templates/code/ghostrail.toml +4 -0
  119. 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":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,YAAY,+CAAgD,CAAC;AAC1E,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD,eAAO,MAAM,aAAa,0EAMhB,CAAC;AACX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC;AAEpC,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,CAAC;IACjB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,sFAAsF;IACtF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,WAAW,CAAC;IAClB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;OAQG;IACH,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,6EAA6E;AAC7E,MAAM,WAAW,oBAAoB;IACnC,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,MAAM,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,QAAQ,EAAE,cAAc,CAAC;IACzB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7C,MAAM,WAAW,UAAU;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,SAAS,CAAC;IAC5B,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,SAAS,CAAC;IAC5B,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8HAA8H;IAC9H,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,4DAA4D;IAC5D,KAAK,EAAE,OAAO,CAAC;IACf,wDAAwD;IACxD,SAAS,EAAE,OAAO,CAAC;IACnB,kFAAkF;IAClF,YAAY,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,mEAAmE;AACnE,eAAO,MAAM,qBAAqB,cAAc,CAAC;AACjD,eAAO,MAAM,mBAAmB,UAAU,CAAC;AAE3C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,aAAa,CAAC;IACvB,IAAI,EAAE,UAAU,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;IACrB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,oFAAoF;IACpF,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,oEAAoE;IACpE,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED,kFAAkF;AAClF,eAAO,MAAM,qBAAqB,EAAE,SAAS,MAAM,EAOlD,CAAC;AAEF,0CAA0C;AAC1C,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,mDAAmD;AACnD,eAAO,MAAM,8BAA8B,IAAI,CAAC;AAChD,6CAA6C;AAC7C,eAAO,MAAM,wBAAwB,QAAc,CAAC;AACpD,gDAAgD;AAChD,eAAO,MAAM,4BAA4B,QAAgB,CAAC;AAE1D,mEAAmE;AACnE,eAAO,MAAM,kBAAkB,EAAE,UAWhC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,QAAa,CAAC;AACnD,eAAO,MAAM,uBAAuB,QAAc,CAAC;AAEnD,eAAO,MAAM,mBAAmB,SAAS,CAAC;AAC1C,eAAO,MAAM,oBAAoB,EAAE,WAA8B,CAAC;AAElE;;;GAGG;AACH,eAAO,MAAM,wBAAwB,OAAO,CAAC"}
@@ -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,QAkP/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>
@@ -57,6 +61,14 @@ const api = (p) => p + (tok ? (p.includes("?") ? "&" : "?") + "token=" + encodeU
57
61
  const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) =>
58
62
  ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
59
63
  const enc = encodeURIComponent;
64
+ // Every link the dashboard renders leaves the page, so it opens in a new tab.
65
+ // \`rel\` goes with it: without it the opened page's script can reach back
66
+ // through \`window.opener\` and repoint the dashboard's own tab.
67
+ const NEWTAB = " target='_blank' rel='noopener noreferrer'";
68
+ // The item's label, linked to whatever the run's \`url\` points at (the tracker
69
+ // issue for a fix-loop run, the PR for a respond pass) when the row has one.
70
+ const itemLink = (r, label) =>
71
+ r.url ? "<a href='" + esc(r.url) + "'" + NEWTAB + ">" + esc(label) + "</a>" : esc(label);
60
72
 
61
73
  // Expanded rows and fetched PR status outlive a re-render, so the 10s poll does
62
74
  // not collapse what the operator opened. Keyed on ghostrail path plus run id,
@@ -84,7 +96,7 @@ function artifactsHtml(path, artifacts) {
84
96
  if (!artifacts || artifacts.length === 0) return "";
85
97
  const items = artifacts.map((a) => {
86
98
  const href = api("/api/artifact?path=" + enc(path) + "&hash=" + enc(a.hash));
87
- return "<li><a href='" + esc(href) + "'>" + esc(a.name) + "</a></li>";
99
+ return "<li><a href='" + esc(href) + "'" + NEWTAB + ">" + esc(a.name) + "</a></li>";
88
100
  }).join("");
89
101
  return "<div><b>Artifacts</b><ul>" + items + "</ul></div>";
90
102
  }
@@ -92,7 +104,9 @@ function artifactsHtml(path, artifacts) {
92
104
  function checkLineHtml(c) {
93
105
  const label = c.conclusion || c.status;
94
106
  const cls = c.conclusion === "success" ? "done" : c.conclusion ? "failed" : "muted";
95
- const name = c.url ? "<a href='" + esc(c.url) + "'>" + esc(c.name) + "</a>" : esc(c.name);
107
+ const name = c.url
108
+ ? "<a href='" + esc(c.url) + "'" + NEWTAB + ">" + esc(c.name) + "</a>"
109
+ : esc(c.name);
96
110
  return "<li>" + name + ": <span class='" + cls + "'>" + esc(label) + "</span></li>";
97
111
  }
98
112
 
@@ -114,9 +128,19 @@ function prStatusHtml(status) {
114
128
  return parts.join("");
115
129
  }
116
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
+
117
138
  function detailHtml(path, r) {
118
139
  const parts = [];
119
- parts.push("<div><b>Item</b> " + esc(r.itemId || "") + (r.kind ? " (" + esc(r.kind) + ")" : "") + "</div>");
140
+ parts.push(
141
+ "<div><b>Item</b> " + itemLink(r, r.itemId || "") +
142
+ (r.kind ? " (" + esc(r.kind) + ")" : "") + "</div>",
143
+ );
120
144
  if (r.branch) parts.push("<div><b>Branch</b> " + esc(r.branch) + "</div>");
121
145
  if (r.backend) parts.push("<div><b>Backend</b> " + esc(r.backend) + "</div>");
122
146
  if (r.costUsd !== undefined) {
@@ -129,13 +153,17 @@ function detailHtml(path, r) {
129
153
  if (r.error) parts.push("<div><b>Error</b><pre class=failed>" + esc(r.error) + "</pre></div>");
130
154
  if (r.summary) parts.push("<div><b>Comment</b><pre>" + esc(r.summary) + "</pre></div>");
131
155
  if (r.prUrl) {
132
- parts.push("<div><b>PR</b> <a href='" + esc(r.prUrl) + "'>" + esc(r.prUrl) + "</a></div>");
156
+ parts.push(
157
+ "<div><b>PR</b> <a href='" + esc(r.prUrl) + "'" + NEWTAB + ">" + esc(r.prUrl) + "</a></div>",
158
+ );
133
159
  }
160
+ parts.push(mergeButtonHtml(path, r));
134
161
  parts.push(artifactsHtml(path, r.artifacts));
135
162
  if (r.prNumber !== undefined) parts.push(prStatusHtml(prStatus.get(rowKey(path, r.runId))));
136
163
  parts.push(
137
164
  "<div class=muted>started " + esc(fmtWhen(r.startedAt)) +
138
- ", finished " + esc(fmtWhen(r.finishedAt)) + "</div>",
165
+ (r.finishedAt ? ", finished " + esc(fmtWhen(r.finishedAt)) : ", still running") +
166
+ "</div>",
139
167
  );
140
168
  return parts.join("");
141
169
  }
@@ -147,14 +175,16 @@ function runsHtml(g, gi) {
147
175
  const body = g.runs.map((r, ri) => {
148
176
  const open = expanded.has(rowKey(g.path, r.runId));
149
177
  const gate = (r.gate || []).map((s) => (s.exitCode === 0 ? "✓" : "✗")).join("");
150
- const pr = r.prUrl ? "<a href='" + esc(r.prUrl) + "'>#" + esc(r.prNumber) + "</a>" : "";
178
+ const pr = r.prUrl
179
+ ? "<a href='" + esc(r.prUrl) + "'" + NEWTAB + ">#" + esc(r.prNumber) + "</a>"
180
+ : "";
151
181
  const state = esc(r.state || ${JSON.stringify(IN_PROGRESS_STATE)});
152
182
  const main = "<tr>" +
153
183
  "<td><button class='twirl" + (open ? " open" : "") + "' id='t-" + gi + "-" + ri + "'" +
154
184
  " aria-expanded='" + open + "' data-path='" + esc(g.path) + "' data-run='" + esc(r.runId) + "'" +
155
185
  " title='show detail'>▶</button></td>" +
156
186
  "<td class=muted>" + esc(r.runId.slice(0, 8)) + "</td>" +
157
- "<td>" + esc(r.title || r.itemId || "") + "</td>" +
187
+ "<td>" + itemLink(r, r.title || r.itemId || "") + "</td>" +
158
188
  "<td class='state " + state + "'>" + state + "</td>" +
159
189
  "<td>" + esc(r.backend || "") + "</td>" +
160
190
  "<td>" + gate + "</td>" +
@@ -181,6 +211,7 @@ function render(ghostrails) {
181
211
  "<span class='name'>" + esc(g.name) + "</span>" +
182
212
  "<span class='pill'>" + esc(g.state) + "</span>" +
183
213
  "<span class='path'>" + esc(g.path) + "</span>" +
214
+ "<a class='review' href='" + esc(api("review?path=" + enc(g.path))) + "'>review</a>" +
184
215
  "</div>" + runsHtml(g, gi) + "</div>"
185
216
  ).join("");
186
217
  }
@@ -218,11 +249,34 @@ function toggle(path, runId, buttonId) {
218
249
  if (expanded.has(k)) refreshPrStatus();
219
250
  }
220
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
+
221
270
  const list = document.getElementById("list");
222
271
 
223
272
  list.addEventListener("click", (e) => {
224
- const btn = e.target.closest(".twirl");
225
- 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);
226
280
  });
227
281
 
228
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mCAuIA,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"}