@popcorn245/cornfield 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/agent-contract.js +5 -2
- package/core/agent-contract.schema.json +9 -9
- package/core/config.js +59 -5
- package/explorer/explorer.css +3 -1
- package/explorer/forge-panel.js +203 -5
- package/explorer/index.html +28 -0
- package/explorer/review-tokens-model.js +5 -1
- package/explorer/review-tokens.js +11 -7
- package/explorer/runs-model.js +107 -0
- package/package.json +1 -1
- package/server/agent-api.js +227 -0
- package/server/agent-runs.js +769 -0
- package/server/db.js +53 -0
- package/server/forge-forgejo.js +7 -0
- package/server/forge-github.js +7 -0
- package/server/forge.js +5 -1
- package/server/hooks.js +19 -0
- package/server/review-api.js +26 -12
- package/server/review-tokens.js +42 -25
- package/server/server.js +29 -7
- package/server/spec-poll.js +41 -15
package/core/agent-contract.js
CHANGED
|
@@ -118,7 +118,8 @@ export const CREDENTIAL_KEY_SOURCE = `(?:${['token', 'secret', 'passw', 'credent
|
|
|
118
118
|
const EDGE = '(?<![A-Za-z0-9])';
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
* A value shaped like a credential: a forge or cloud token,
|
|
121
|
+
* A value shaped like a credential: a forge or cloud token, cornfield's own API tokens (`cf_rt_…` review, `cf_at_…` agent), a JWT, a
|
|
122
|
+
* private key, a bearer header, a user and password in a link.
|
|
122
123
|
* Prose that only talks about tokens and passwords passes; `sk-` keys and bearer values need a digit in their first 41
|
|
123
124
|
* characters, as real ones have, so a long hyphenated word after "Bearer" or "sk-" is not taken for one. Each alternative tries a
|
|
124
125
|
* bounded stretch from each place it may start, so a 20000 character brief is read in linear time whatever it holds.
|
|
@@ -128,6 +129,7 @@ export const CREDENTIAL_VALUE_SOURCE = [
|
|
|
128
129
|
`${EDGE}github_pat_[A-Za-z0-9_]{20,}`,
|
|
129
130
|
`${EDGE}glpat-[A-Za-z0-9_-]{15,}`,
|
|
130
131
|
`${EDGE}gto-[A-Za-z0-9]{20,}`,
|
|
132
|
+
`${EDGE}cf_(?:rt|at)_[A-Za-z0-9_-]{20,}`,
|
|
131
133
|
`${EDGE}sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]`,
|
|
132
134
|
`${EDGE}AKIA[0-9A-Z]{16}`,
|
|
133
135
|
`${EDGE}AIza[0-9A-Za-z_-]{35}`,
|
|
@@ -624,7 +626,8 @@ export function runBlocksOf(comments, { trust, profiles } = /** @type {any} */ (
|
|
|
624
626
|
const by = c.author?.login ?? null;
|
|
625
627
|
const comment = { id: String(c.id), url: c.url ?? null };
|
|
626
628
|
for (const r of read.refused) out.refused.push({ ...r, by, comment });
|
|
627
|
-
|
|
629
|
+
// A time that does not parse is not taken for "never edited": only a comment the forge says it left alone counts
|
|
630
|
+
const edited = c.updatedAt !== undefined && !(Date.parse(c.updatedAt) - Date.parse(c.createdAt) <= EDIT_GRACE_MS);
|
|
628
631
|
for (const b of read.blocks) {
|
|
629
632
|
const found = { ...b, by, at: c.createdAt, comment };
|
|
630
633
|
if (by === null || !trust(by, b)) out.ignored.push({ ...found, why: 'author' });
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"type": "string",
|
|
58
58
|
"maxLength": 64,
|
|
59
59
|
"not": {
|
|
60
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
60
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
61
61
|
},
|
|
62
62
|
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$",
|
|
63
63
|
"description": "A name the runner maps to a model, a machine and a budget. When the developer configured a list, only those names."
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"type": "string",
|
|
76
76
|
"maxLength": 120,
|
|
77
77
|
"not": {
|
|
78
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
78
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
79
79
|
},
|
|
80
80
|
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,119}$"
|
|
81
81
|
},
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"type": "string",
|
|
119
119
|
"maxLength": 200,
|
|
120
120
|
"not": {
|
|
121
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
121
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
122
122
|
},
|
|
123
123
|
"minLength": 1,
|
|
124
124
|
"description": "A hint: a safe git ref name.",
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
"type": "string",
|
|
138
138
|
"maxLength": 20000,
|
|
139
139
|
"not": {
|
|
140
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
140
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
141
141
|
}
|
|
142
142
|
},
|
|
143
143
|
"limits": {
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
"type": "string",
|
|
166
166
|
"maxLength": 200,
|
|
167
167
|
"not": {
|
|
168
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
168
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
169
169
|
},
|
|
170
170
|
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$"
|
|
171
171
|
}
|
|
@@ -174,7 +174,7 @@
|
|
|
174
174
|
"type": "string",
|
|
175
175
|
"maxLength": 40,
|
|
176
176
|
"not": {
|
|
177
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
177
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
178
178
|
},
|
|
179
179
|
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$"
|
|
180
180
|
},
|
|
@@ -205,14 +205,14 @@
|
|
|
205
205
|
"type": "string",
|
|
206
206
|
"maxLength": 4000,
|
|
207
207
|
"not": {
|
|
208
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
208
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
209
209
|
}
|
|
210
210
|
},
|
|
211
211
|
"url": {
|
|
212
212
|
"type": "string",
|
|
213
213
|
"maxLength": 2000,
|
|
214
214
|
"not": {
|
|
215
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
215
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
216
216
|
},
|
|
217
217
|
"pattern": "^https?://[^\\s/@?#]+([/?#]\\S*)?$",
|
|
218
218
|
"description": "Where the runner keeps its logs.",
|
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
"type": "string",
|
|
251
251
|
"maxLength": 4000,
|
|
252
252
|
"not": {
|
|
253
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
253
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
}
|
package/core/config.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* },
|
|
14
14
|
* forge: { type: 'github', url: 'https://github.com', repo: 'owner/name' }, // or type 'forgejo'; mergeMethod: 'merge' | 'squash' | 'rebase'; baseBranch: 'main'
|
|
15
15
|
* deploys: { workflows: ['*deploy*'], restarts: { 'Deploy web': ['stack-web'] } }, // which workflow runs are deploys, and what they restart (docs/deploys.md)
|
|
16
|
-
* agents: { authors: [], labels: [], branchPrefixes: [], bots: true }, // what marks work as made by an agent (docs/agents.md)
|
|
16
|
+
* agents: { authors: [], labels: [], branchPrefixes: [], bots: true }, // what marks work as made by an agent (docs/agents.md); and how runs are asked for and followed: transport, maxOpen, profiles, runners, quietAfter, limits (docs/config.md#agents)
|
|
17
17
|
* port: 3350,
|
|
18
18
|
* host: '127.0.0.1', // loopback, unless sign-in is configured with an https publicUrl (server/bind.js)
|
|
19
19
|
* publicUrl: null, // the origin people open it at; sign-in needs it (server/auth.js)
|
|
@@ -40,6 +40,7 @@ import fs from 'node:fs';
|
|
|
40
40
|
import os from 'node:os';
|
|
41
41
|
import path from 'node:path';
|
|
42
42
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
43
|
+
import { PROFILE_SOURCE, MAX_NUMBER } from './agent-contract.js';
|
|
43
44
|
|
|
44
45
|
export const CONFIG_FILE = 'cornfield.config.js';
|
|
45
46
|
export const CORNFIELD_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -67,7 +68,7 @@ export const FORGE_TYPES = ['github', 'forgejo'];
|
|
|
67
68
|
/** How a pull request cornfield lands is merged (`forge.mergeMethod`): the way the repo lands its pull requests */
|
|
68
69
|
export const MERGE_METHODS = ['merge', 'squash', 'rebase'];
|
|
69
70
|
/** The events a hook may ask for (`hooks[].events`), and what each one means: docs/config.md#hooks */
|
|
70
|
-
export const HOOK_EVENTS = Object.freeze(['spec.ready', 'spec.approved', 'spec.sentBack', 'build.opened', 'pr.landed', 'session.expiring', 'thread.opened', 'thread.message', 'thread.linked']);
|
|
71
|
+
export const HOOK_EVENTS = Object.freeze(['spec.ready', 'spec.approved', 'spec.sentBack', 'build.opened', 'pr.landed', 'session.expiring', 'thread.opened', 'thread.message', 'thread.linked', 'agent.requested', 'run.claimed', 'run.state', 'run.stopRequested', 'run.finished']);
|
|
71
72
|
/** The events that come from looking at threads (server/threads.js): a poll that wants any of them looks at threads too */
|
|
72
73
|
export const THREAD_EVENTS = Object.freeze(['thread.opened', 'thread.message', 'thread.linked']);
|
|
73
74
|
/** The keys of `threads` when it is an object, and the label's longest length */
|
|
@@ -115,6 +116,25 @@ export function specPollMs(config) {
|
|
|
115
116
|
return wanted && every !== false ? parseDuration(every ?? DEFAULTS.hookPoll.specs) : null;
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
/** How a request reaches a runner: the forge only for now (spec 0005 D2; push and pull come with #81) */
|
|
120
|
+
export const AGENT_TRANSPORTS = Object.freeze(['forge']);
|
|
121
|
+
/** `agents.maxOpen`: the most open runs per repo cornfield will ask for, by default and at most */
|
|
122
|
+
export const DEFAULT_MAX_OPEN = 3;
|
|
123
|
+
export const MAX_OPEN_CEILING = 100;
|
|
124
|
+
/** `agents.quietAfter`: how long a run may show no report, push or pull request activity before it is quiet; its bounds in milliseconds */
|
|
125
|
+
export const DEFAULT_QUIET_AFTER = '24h';
|
|
126
|
+
export const QUIET_MIN_MS = 5 * 60_000;
|
|
127
|
+
export const QUIET_MAX_MS = 30 * 24 * 3_600_000;
|
|
128
|
+
/** The most profiles and runners a config lists */
|
|
129
|
+
export const MAX_PROFILES = 20;
|
|
130
|
+
export const MAX_RUNNERS = 50;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* How long a run may be silent before it is quiet, in milliseconds: `agents.quietAfter`, '24h' unless set.
|
|
134
|
+
* @param {{ agents?: { quietAfter?: string } }} config a loaded config
|
|
135
|
+
*/
|
|
136
|
+
export const quietAfterMs = (config) => parseDuration(config.agents?.quietAfter ?? DEFAULT_QUIET_AFTER) ?? parseDuration(DEFAULT_QUIET_AFTER);
|
|
137
|
+
|
|
118
138
|
export const DEFAULTS = Object.freeze({
|
|
119
139
|
title: null,
|
|
120
140
|
derivers: ['starter', 'personas', 'cloud'],
|
|
@@ -130,7 +150,7 @@ export const DEFAULTS = Object.freeze({
|
|
|
130
150
|
personas: { roles: null, parent: 'root' },
|
|
131
151
|
cloud: { parent: 'root' },
|
|
132
152
|
releaseNotes: { breakingLabel: 'breaking' },
|
|
133
|
-
agents: { authors: [], labels: [], branchPrefixes: [], bots: true },
|
|
153
|
+
agents: { authors: [], labels: [], branchPrefixes: [], bots: true, transport: 'forge', maxOpen: DEFAULT_MAX_OPEN, profiles: [], runners: [], quietAfter: DEFAULT_QUIET_AFTER, limits: {} },
|
|
134
154
|
hooks: [],
|
|
135
155
|
hookPoll: { specs: '5m' },
|
|
136
156
|
threads: { label: 'idea' },
|
|
@@ -143,7 +163,8 @@ export const DEFAULTS = Object.freeze({
|
|
|
143
163
|
const KEYS = Object.keys(DEFAULTS);
|
|
144
164
|
/** The keys of `proposals` when it is an object: the folder, and whether an approval also lands the spec PR (opt-in, off unless true) */
|
|
145
165
|
export const PROPOSALS_KEYS = Object.freeze(['dir', 'approveLands']);
|
|
146
|
-
const AGENT_KEYS = ['authors', 'labels', 'branchPrefixes', 'bots'];
|
|
166
|
+
const AGENT_KEYS = ['authors', 'labels', 'branchPrefixes', 'bots', 'transport', 'maxOpen', 'profiles', 'runners', 'quietAfter', 'limits'];
|
|
167
|
+
const PROFILE_NAME = new RegExp(PROFILE_SOURCE);
|
|
147
168
|
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
148
169
|
const isText = (v) => typeof v === 'string' && v.trim() !== '';
|
|
149
170
|
/** A path inside the repo: relative, forward slashes, no `..` segment */
|
|
@@ -203,7 +224,7 @@ export function configProblems(c) {
|
|
|
203
224
|
}
|
|
204
225
|
}
|
|
205
226
|
if (c.agents !== undefined) {
|
|
206
|
-
if (!isObject(c.agents)) out.push(
|
|
227
|
+
if (!isObject(c.agents)) out.push(`"agents" is { ${AGENT_KEYS.join(', ')} }.`);
|
|
207
228
|
else {
|
|
208
229
|
for (const k of Object.keys(c.agents)) if (!AGENT_KEYS.includes(k)) out.push(`agents.${k}: the keys are ${AGENT_KEYS.join(', ')}.`);
|
|
209
230
|
for (const k of ['authors', 'labels', 'branchPrefixes']) {
|
|
@@ -215,6 +236,7 @@ export function configProblems(c) {
|
|
|
215
236
|
}
|
|
216
237
|
}
|
|
217
238
|
if (c.agents.bots !== undefined && typeof c.agents.bots !== 'boolean') out.push('agents.bots is true or false: whether a bot account ("[bot]" or the forge\'s Bot type) counts as an agent.');
|
|
239
|
+
out.push(...agentRunProblems(c.agents));
|
|
218
240
|
}
|
|
219
241
|
}
|
|
220
242
|
if (c.deploys !== undefined && c.deploys !== null) {
|
|
@@ -306,6 +328,38 @@ export function configProblems(c) {
|
|
|
306
328
|
return out;
|
|
307
329
|
}
|
|
308
330
|
|
|
331
|
+
/** What is wrong with the keys of `agents` that ask for and follow runs (spec 0005), in words */
|
|
332
|
+
function agentRunProblems(a) {
|
|
333
|
+
const out = [];
|
|
334
|
+
if (a.transport !== undefined && !AGENT_TRANSPORTS.includes(a.transport)) out.push(`agents.transport is ${AGENT_TRANSPORTS.map((t) => JSON.stringify(t)).join(' or ')}: a request is posted on the forge (push and pull come later).`);
|
|
335
|
+
if (a.maxOpen !== undefined && !(Number.isInteger(a.maxOpen) && a.maxOpen >= 1 && a.maxOpen <= MAX_OPEN_CEILING)) out.push(`agents.maxOpen is a whole number from 1 to ${MAX_OPEN_CEILING}: the most open runs cornfield asks for at once (${DEFAULT_MAX_OPEN} unless set).`);
|
|
336
|
+
if (a.profiles !== undefined) {
|
|
337
|
+
if (!Array.isArray(a.profiles) || a.profiles.length > MAX_PROFILES || !a.profiles.every((p) => typeof p === 'string' && PROFILE_NAME.test(p)) || new Set(a.profiles).size !== a.profiles.length) {
|
|
338
|
+
out.push(`agents.profiles is a list of at most ${MAX_PROFILES} different names ("claude", "local-qwen"): letters, digits and . _ -, at most 64 characters each. The runner maps each to a model; cornfield only knows the name.`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (a.runners !== undefined) {
|
|
342
|
+
if (!Array.isArray(a.runners) || a.runners.length > MAX_RUNNERS || !a.runners.every((r) => isText(r) && r === r.trim() && !/[\s,]/.test(r))) {
|
|
343
|
+
out.push(`agents.runners is a list of at most ${MAX_RUNNERS} forge logins ("claude-bot"), the runners' own accounts whose reports count besides a maintainer's.`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (a.quietAfter !== undefined) {
|
|
347
|
+
const ms = parseDuration(a.quietAfter);
|
|
348
|
+
if (ms === null) out.push('agents.quietAfter is a duration like "90m", "24h" or "72h".');
|
|
349
|
+
else if (ms < QUIET_MIN_MS) out.push(`agents.quietAfter ${JSON.stringify(a.quietAfter)} is under the floor of 5m; use "5m" or more.`);
|
|
350
|
+
else if (ms > QUIET_MAX_MS) out.push(`agents.quietAfter ${JSON.stringify(a.quietAfter)} is over the ceiling of 30 days; use "720h" or less.`);
|
|
351
|
+
}
|
|
352
|
+
if (a.limits !== undefined) {
|
|
353
|
+
if (!isObject(a.limits)) out.push('agents.limits is { minutes?, turns?, cost? }: hints the runner may enforce.');
|
|
354
|
+
else {
|
|
355
|
+
for (const k of Object.keys(a.limits)) if (!['minutes', 'turns', 'cost'].includes(k)) out.push(`agents.limits.${k}: the keys are minutes, turns, cost.`);
|
|
356
|
+
for (const k of ['minutes', 'cost']) if (a.limits[k] !== undefined && !(typeof a.limits[k] === 'number' && Number.isFinite(a.limits[k]) && a.limits[k] > 0)) out.push(`agents.limits.${k} is a number above zero.`);
|
|
357
|
+
if (a.limits.turns !== undefined && !(Number.isInteger(a.limits.turns) && a.limits.turns > 0 && a.limits.turns <= MAX_NUMBER)) out.push('agents.limits.turns is a whole number above zero.');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
|
|
309
363
|
/** Whether a host is the machine itself: `localhost`, 127.x.x.x or ::1 */
|
|
310
364
|
const isLoopbackHost = (host) => host === 'localhost' || host === '[::1]' || /^127(?:\.\d{1,3}){3}$/.test(host);
|
|
311
365
|
|
package/explorer/explorer.css
CHANGED
|
@@ -852,7 +852,7 @@ a.hx-forge-row:hover, button.hx-forge-row:hover { background: var(--hx-control-h
|
|
|
852
852
|
.hx-forge-detail { display: flex; flex-direction: column; gap: 6px; padding: 10px; border: 1px solid var(--hx-line); border-left: 4px solid var(--hx-indigo); border-radius: 10px; background: var(--hx-well-soft); }
|
|
853
853
|
.hx-forge-detail-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
854
854
|
.hx-forge-detail-title { min-width: 0; font-size: 13px; font-weight: 700; color: var(--hx-ink); overflow-wrap: anywhere; }
|
|
855
|
-
.hx-forge-sub, .hx-forge-subact, .hx-forge-confirm-title { margin: 4px 0 0; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--hx-slate-300); }
|
|
855
|
+
.hx-forge-sub, .hx-forge-subact, .hx-forge-runs-title, .hx-forge-confirm-title { margin: 4px 0 0; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--hx-slate-300); }
|
|
856
856
|
.hx-forge-check { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 6px; }
|
|
857
857
|
.hx-forge-check .hx-link { display: inline-flex; align-items: center; min-height: 24px; }
|
|
858
858
|
.hx-forge-lights { flex-basis: 100%; font-size: 11px; line-height: 1.5; }
|
|
@@ -913,6 +913,8 @@ a.hx-forge-row:hover, button.hx-forge-row:hover { background: var(--hx-control-h
|
|
|
913
913
|
:root[data-theme="light"] .hx-forge-tab[aria-pressed="true"] { box-shadow: 0 1px 3px rgba(15, 23, 42, 0.25); }
|
|
914
914
|
:root[data-theme="light"] .hx-forge { box-shadow: 10px 0 30px -12px rgba(15, 23, 42, 0.18); }
|
|
915
915
|
:root[data-theme="light"] .hx-forge:not(.forge-open) { box-shadow: none; }
|
|
916
|
+
.hx-forge-runs { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 8px; }
|
|
917
|
+
.hx-forge-runs[hidden] { display: none; }
|
|
916
918
|
|
|
917
919
|
/* Threads (forge-panel.js, docs/threads.md): messages as written, read-only; a node's "Mentioned in" opens one */
|
|
918
920
|
.hx-thread-msgs { display: flex; flex-direction: column; gap: 8px; margin: 0; padding: 0; list-style: none; }
|
package/explorer/forge-panel.js
CHANGED
|
@@ -51,6 +51,12 @@
|
|
|
51
51
|
* notes again itself and has the forge keep them as a DRAFT release; nothing is published and no tag is made. The
|
|
52
52
|
* page never sends the text of the notes, only the tag. The result, or the forge's refusal, is a line in words.
|
|
53
53
|
*
|
|
54
|
+
* - Agent runs (issue #80, spec 0005, explorer/runs-model.js): **Start an agent** on an open issue and on an issue or proposal group of the Agents
|
|
55
|
+
* tab opens a dialog (kind, a profile when the config lists any, a brief, and in words what it will do) and POSTs once to
|
|
56
|
+
* `/api/agent-runs`; the Agents tab lists the runs with their state, links and a **Stop**, which asks the runner to stop. cornfield only
|
|
57
|
+
* asks: the request is a comment on the forge, as the maintainer. A refusal (the limit, a run already open) is said in the dialog, with
|
|
58
|
+
* a way to ask on purpose where the rule allows it.
|
|
59
|
+
*
|
|
54
60
|
* What is asked, which words and which nodes are forge-panel-model.js, tested without a page.
|
|
55
61
|
*/
|
|
56
62
|
|
|
@@ -62,6 +68,9 @@ import { isPhone, registerSheet, expandSheet, collapseSheet } from './sheets.js'
|
|
|
62
68
|
import { relativeTime } from './review-model.js';
|
|
63
69
|
import { LAYER_DEFS, toggleLayer } from './layers.js';
|
|
64
70
|
import { WORK_URL, groupWork, filterGroups, groupWords, workStatus, markWords } from './agents-model.js';
|
|
71
|
+
import {
|
|
72
|
+
RUNS_URL, runsUrl, stopUrl, KINDS, aboutWords, statusWords, statusWhy, runMeta, canStop, runsStatus, startBody, startWords, stopWords, confirmStartWords,
|
|
73
|
+
} from './runs-model.js';
|
|
65
74
|
import {
|
|
66
75
|
STATES, defaultFilters, listUrl, pullUrl, serverState, filterItems, facetsOf, stateWords, metaWords, checkWords, checksWords, litFor, errorWords,
|
|
67
76
|
landingUrl, cancelLandingUrl, landingWords, mergeBlock, rerunWhy, confirmMerge, mergeBody, actionUrl, actionWords,
|
|
@@ -105,6 +114,9 @@ const ui = {
|
|
|
105
114
|
confirm: null, // { opener: selector, kind: 'merge' | 'spec' | 'build', plan?, n?, slug? } while the confirm dialog is open
|
|
106
115
|
actNote: null, // { text, ok, url }: what the last pipeline step came to
|
|
107
116
|
notes: freshNotes(), // the Release notes view
|
|
117
|
+
runs: null, // { data, error } of the Agents tab's runs: what /api/agent-runs answered
|
|
118
|
+
runNote: null, // { text, ok }: what the last Stop came to
|
|
119
|
+
agent: null, // { opener, refs, posting } while the Start an agent dialog is open
|
|
108
120
|
};
|
|
109
121
|
|
|
110
122
|
/** The Release notes view: the draft loaded for a tag, and where the confirm step is */
|
|
@@ -161,7 +173,7 @@ async function loadWork(refresh, token) {
|
|
|
161
173
|
ui.loading = true;
|
|
162
174
|
ui.error = '';
|
|
163
175
|
paint();
|
|
164
|
-
const res = await request(WORK_URL, refresh);
|
|
176
|
+
const [res] = await Promise.all([request(WORK_URL, refresh), loadRuns(refresh)]);
|
|
165
177
|
if (token !== listToken) return;
|
|
166
178
|
ui.loading = false;
|
|
167
179
|
if (res.ok) {
|
|
@@ -175,6 +187,12 @@ async function loadWork(refresh, token) {
|
|
|
175
187
|
paint();
|
|
176
188
|
}
|
|
177
189
|
|
|
190
|
+
/** Loads the runs for the Agents tab (a refresh also has cornfield look at the forge for their reports first); a failure is a line, never a blank panel */
|
|
191
|
+
async function loadRuns(refresh) {
|
|
192
|
+
const res = await request(runsUrl(refresh), refresh);
|
|
193
|
+
ui.runs = res.ok && Array.isArray(res.body.runs) ? { data: res.body, error: '' } : { data: ui.runs && ui.runs.data, error: errorWords(res.status, res.body) };
|
|
194
|
+
}
|
|
195
|
+
|
|
178
196
|
/** Loads the list the kind and state name (again, past the server's cache, on a refresh) */
|
|
179
197
|
async function loadList(refresh = false, quiet = false) {
|
|
180
198
|
const token = ++listToken;
|
|
@@ -732,9 +750,12 @@ function itemHtml(item, before = '', after = '') {
|
|
|
732
750
|
const spec = specBlock(item) === null
|
|
733
751
|
? `<button type="button" class="hx-btn hx-forge-act hx-forge-startspec" data-forge-spec="${item.number}" aria-label="Start a spec for issue ${item.number}"${ui.busy ? ' disabled' : ''}>Start a spec …</button>`
|
|
734
752
|
: '';
|
|
753
|
+
const agent = item.state === 'open'
|
|
754
|
+
? `<button type="button" class="hx-btn hx-forge-act hx-forge-startspec" data-forge-agent="issue:${item.number}" aria-label="Start an agent on issue ${item.number}">Start an agent …</button>`
|
|
755
|
+
: '';
|
|
735
756
|
return link
|
|
736
|
-
? `<li class="hx-forge-item"><a class="hx-forge-row" href="${escHtml(link)}" target="_blank" rel="noreferrer">${inner}<span class="hx-sr-only"> (opens ${escHtml(forgeName())} in a new tab)</span></a>${spec}</li>`
|
|
737
|
-
: `<li class="hx-forge-item"><div class="hx-forge-row">${inner}</div>${spec}</li>`;
|
|
757
|
+
? `<li class="hx-forge-item"><a class="hx-forge-row" href="${escHtml(link)}" target="_blank" rel="noreferrer">${inner}<span class="hx-sr-only"> (opens ${escHtml(forgeName())} in a new tab)</span></a>${spec}${agent}</li>`
|
|
758
|
+
: `<li class="hx-forge-item"><div class="hx-forge-row">${inner}</div>${spec}${agent}</li>`;
|
|
738
759
|
}
|
|
739
760
|
|
|
740
761
|
/** One piece of work in a group: a pull request (a row that selects it, as in the Pull requests tab) or a branch with none */
|
|
@@ -752,7 +773,9 @@ function workHtml(e) {
|
|
|
752
773
|
function groupsHtml(groups) {
|
|
753
774
|
return groups.map((g, i) => {
|
|
754
775
|
const link = g.issue && safeUrl(g.issue.url) ? ` <a class="hx-link hx-forge-gout" href="${escHtml(g.issue.url)}" target="_blank" rel="noreferrer" aria-label="Open issue ${g.number} on ${escHtml(forgeName())} (new tab)">↗</a>` : '';
|
|
755
|
-
|
|
776
|
+
const startRef = g.kind === 'issue' && g.issue && g.issue.state === 'open' ? `issue:${g.number}` : g.kind === 'proposal' ? `proposal:${g.slug}` : null;
|
|
777
|
+
const start = startRef ? ` <button type="button" class="hx-btn hx-forge-act" data-forge-agent="${escHtml(startRef)}" aria-label="Start an agent on ${escHtml(g.kind === 'issue' ? `issue ${g.number}` : `proposal ${g.slug}`)}">Start an agent …</button>` : '';
|
|
778
|
+
return `<li class="hx-forge-group"><h3 class="hx-forge-gtitle" id="forge-group-${i}">${escHtml(g.title)}${link}</h3>${start}<p class="hx-forge-meta">${escHtml(groupWords(g))}</p><ul class="hx-forge-list" aria-labelledby="forge-group-${i}">${g.entries.map(workHtml).join('')}</ul></li>`;
|
|
756
779
|
}).join('');
|
|
757
780
|
}
|
|
758
781
|
|
|
@@ -899,6 +922,8 @@ function findAgain(el) {
|
|
|
899
922
|
if (el.dataset.forgeSpec) return `[data-forge-spec="${el.dataset.forgeSpec}"]`;
|
|
900
923
|
if (el.dataset.forgeBuild) return `[data-forge-build="${el.dataset.forgeBuild}"]`;
|
|
901
924
|
if (el.dataset.forgeRerun) return `[data-forge-rerun="${el.dataset.forgeRerun}"]`;
|
|
925
|
+
if (el.dataset.forgeAgent) return `[data-forge-agent="${el.dataset.forgeAgent}"]`;
|
|
926
|
+
if (el.dataset.forgeStop) return `[data-forge-stop="${el.dataset.forgeStop}"]`;
|
|
902
927
|
return null;
|
|
903
928
|
}
|
|
904
929
|
|
|
@@ -960,9 +985,41 @@ function draw(panel) {
|
|
|
960
985
|
paintIdea();
|
|
961
986
|
const rate = (ui.threadDetail && ui.threadDetail.rate) || (ui.detail && ui.detail.rate) || (list && list.rate) || '';
|
|
962
987
|
$('forge-rate').textContent = rate;
|
|
988
|
+
paintRuns();
|
|
963
989
|
paintButton();
|
|
964
990
|
}
|
|
965
991
|
|
|
992
|
+
/** The runs of the Agents tab: state, what each is about, links, and Stop. A runner's words are escaped like any text from the forge. */
|
|
993
|
+
function paintRuns() {
|
|
994
|
+
const box = $('forge-runs');
|
|
995
|
+
if (!box) return;
|
|
996
|
+
box.hidden = ui.kind !== 'work';
|
|
997
|
+
if (box.hidden) return;
|
|
998
|
+
const data = ui.runs && ui.runs.data;
|
|
999
|
+
$('forge-runs-status').textContent = data ? runsStatus(data) : ui.loading ? 'Loading the runs …' : '';
|
|
1000
|
+
const note = $('forge-runs-note');
|
|
1001
|
+
const words = (ui.runs && ui.runs.error) || (ui.runNote ? ui.runNote.text : '');
|
|
1002
|
+
note.hidden = !words;
|
|
1003
|
+
note.className = `hx-forge-notice${!(ui.runs && ui.runs.error) && ui.runNote && ui.runNote.ok ? ' hx-forge-notice--ok' : ''}`;
|
|
1004
|
+
note.textContent = words;
|
|
1005
|
+
const list = $('forge-runs-list');
|
|
1006
|
+
if (!data) { list.innerHTML = ''; return; }
|
|
1007
|
+
list.innerHTML = data.runs.length ? data.runs.map(runHtml).join('') : '<li class="hx-forge-empty hx-faint">No agent has been asked for yet. Start one from an issue or a proposal.</li>';
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function runHtml(run) {
|
|
1011
|
+
const links = [
|
|
1012
|
+
safeUrl(run.comment && run.comment.url) ? `<a class="hx-link" href="${escHtml(run.comment.url)}" target="_blank" rel="noreferrer">Request on ${escHtml(forgeName())} <span class="hx-sr-only">(new tab)</span></a>` : '',
|
|
1013
|
+
run.pull && safeUrl(run.pull.url) ? `<a class="hx-link" href="${escHtml(run.pull.url)}" target="_blank" rel="noreferrer">Pull request #${run.pull.number} <span class="hx-sr-only">(new tab)</span></a>${run.pull.trusted ? '' : ' <span class="hx-faint">(not from a runner, so it moves nothing)</span>'}` : '',
|
|
1014
|
+
run.report && safeUrl(run.report.url) ? `<a class="hx-link" href="${escHtml(run.report.url)}" target="_blank" rel="noreferrer">Runner\'s logs <span class="hx-sr-only">(new tab)</span></a>` : '',
|
|
1015
|
+
].filter(Boolean).join(' · ');
|
|
1016
|
+
const why = statusWhy(run);
|
|
1017
|
+
const note = run.report && run.report.note ? `<p class="hx-forge-meta">Runner: ${escHtml(run.report.note)}</p>` : '';
|
|
1018
|
+
const stop = canStop(run) ? `<button type="button" class="hx-btn hx-forge-act" data-forge-stop="${escHtml(run.runId)}" aria-label="Stop run ${escHtml(run.runId)}"${ui.busy ? ' disabled' : ''}>Stop</button>` : '';
|
|
1019
|
+
return `<li class="hx-forge-file"><span class="hx-forge-state hx-forge-state--${escHtml(run.status === 'done' || run.status === 'merged' ? 'success' : run.status === 'failed' ? 'failure' : run.status === 'quiet' || run.status === 'stop-requested' ? 'queued' : 'running')}">${escHtml(statusWords(run))}</span> <span class="hx-name">${escHtml(run.kind)} on ${escHtml(aboutWords(run.refs))}</span> ${stop}
|
|
1020
|
+
<p class="hx-forge-meta">${escHtml(runMeta(run, now()))}</p>${why ? `<p class="hx-forge-why hx-faint">${escHtml(why)}</p>` : ''}${note}${links ? `<p class="hx-forge-meta">${links}</p>` : ''}</li>`;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
966
1023
|
/** The panel's lines for the Release notes view: the status, a failure, the rate limit; the rest is paintNotes */
|
|
967
1024
|
function drawNotes() {
|
|
968
1025
|
const n = ui.notes;
|
|
@@ -1191,6 +1248,136 @@ function onConfirmClick(e) {
|
|
|
1191
1248
|
if (action.dataset.action === 'forge-confirm-cancel') { if (!ui.busy) closeConfirm(); } else if (action.dataset.action === 'forge-confirm-go') { if (ui.confirm && ui.confirm.kind === 'merge') confirmedMerge(); else confirmedStep(); }
|
|
1192
1249
|
}
|
|
1193
1250
|
|
|
1251
|
+
// ---------------------------------------------------------------------------
|
|
1252
|
+
// Agent runs: Start an agent, and Stop
|
|
1253
|
+
// ---------------------------------------------------------------------------
|
|
1254
|
+
|
|
1255
|
+
const agentEl = () => $('agent-start');
|
|
1256
|
+
|
|
1257
|
+
/** While the dialog is open the focus cannot leave it */
|
|
1258
|
+
function keepFocusInAgent(e) {
|
|
1259
|
+
const el = agentEl();
|
|
1260
|
+
if (ui.agent && el && !el.contains(e.target)) $('agent-kind').focus();
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/** Opens the Start an agent dialog for `issue:<n>` or `proposal:<slug>`; the focus goes to the first field */
|
|
1264
|
+
async function openAgent(ref, opener) {
|
|
1265
|
+
if (!agentEl() || ui.agent) return;
|
|
1266
|
+
const at = ref.indexOf(':');
|
|
1267
|
+
const type = ref.slice(0, at);
|
|
1268
|
+
const value = ref.slice(at + 1);
|
|
1269
|
+
const refs = type === 'proposal' ? { proposal: value } : { issue: Number(value) };
|
|
1270
|
+
ui.agent = { opener, refs, posting: false };
|
|
1271
|
+
// The profiles the config lists come with the runs: asked for if the Agents tab has not been opened yet
|
|
1272
|
+
if (!ui.runs) { await loadRuns(false); if (ui.kind === 'work') paint(); }
|
|
1273
|
+
if (!ui.agent) return;
|
|
1274
|
+
const profiles = (ui.runs && ui.runs.data && ui.runs.data.profiles) || [];
|
|
1275
|
+
$('agent-start-about').textContent = `About ${aboutWords(refs)}`;
|
|
1276
|
+
$('agent-kind').innerHTML = KINDS.map(([v, l]) => option(v, l, v === 'code')).join('');
|
|
1277
|
+
$('agent-profile').innerHTML = option('', 'No profile', true) + profiles.map((p) => option(p, p, false)).join('');
|
|
1278
|
+
$('agent-profile-field').hidden = !profiles.length;
|
|
1279
|
+
$('agent-brief').value = '';
|
|
1280
|
+
$('agent-force').checked = false;
|
|
1281
|
+
$('agent-force-field').hidden = true;
|
|
1282
|
+
$('agent-start-msg').hidden = true;
|
|
1283
|
+
$('agent-start-msg').textContent = '';
|
|
1284
|
+
$('agent-start-words').textContent = confirmStartWords(refs, forgeName());
|
|
1285
|
+
const go = $('agent-start-go');
|
|
1286
|
+
go.disabled = false;
|
|
1287
|
+
go.removeAttribute('aria-busy');
|
|
1288
|
+
go.textContent = 'Ask the runner';
|
|
1289
|
+
document.addEventListener('focusin', keepFocusInAgent);
|
|
1290
|
+
agentEl().hidden = false;
|
|
1291
|
+
$('agent-kind').focus();
|
|
1292
|
+
announce(`Start an agent on ${aboutWords(refs)}. ${confirmStartWords(refs, forgeName())}`);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/** Closes the dialog; the focus goes back to the button that opened it unless told not to */
|
|
1296
|
+
function closeAgent({ restoreFocus = true } = {}) {
|
|
1297
|
+
const el = agentEl();
|
|
1298
|
+
if (!el || !ui.agent) return;
|
|
1299
|
+
const { opener } = ui.agent;
|
|
1300
|
+
ui.agent = null;
|
|
1301
|
+
el.hidden = true;
|
|
1302
|
+
document.removeEventListener('focusin', keepFocusInAgent);
|
|
1303
|
+
if (!restoreFocus) return;
|
|
1304
|
+
const back = $('forge-panel').querySelector(opener) || $('forge-title');
|
|
1305
|
+
if (back && !back.disabled) back.focus();
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/** The one write: posts the choices once; a refusal stays in the dialog, in the server's words */
|
|
1309
|
+
async function submitAgent(e) {
|
|
1310
|
+
e.preventDefault();
|
|
1311
|
+
if (!ui.agent || ui.agent.posting) return;
|
|
1312
|
+
ui.agent.posting = true;
|
|
1313
|
+
const go = $('agent-start-go');
|
|
1314
|
+
go.disabled = true;
|
|
1315
|
+
go.setAttribute('aria-busy', 'true');
|
|
1316
|
+
go.textContent = 'Asking …';
|
|
1317
|
+
$('agent-start-msg').hidden = true;
|
|
1318
|
+
const res = await post(RUNS_URL, startBody({
|
|
1319
|
+
kind: $('agent-kind').value, profile: $('agent-profile-field').hidden ? '' : $('agent-profile').value, brief: $('agent-brief').value, refs: ui.agent.refs, force: $('agent-force').checked,
|
|
1320
|
+
}));
|
|
1321
|
+
if (!ui.agent) return;
|
|
1322
|
+
ui.agent.posting = false;
|
|
1323
|
+
const text = startWords(res.status, res.body);
|
|
1324
|
+
if (res.ok) {
|
|
1325
|
+
closeAgent();
|
|
1326
|
+
ui.runNote = { text, ok: true };
|
|
1327
|
+
announce(text);
|
|
1328
|
+
await loadRuns(false);
|
|
1329
|
+
paint();
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
go.disabled = false;
|
|
1333
|
+
go.removeAttribute('aria-busy');
|
|
1334
|
+
go.textContent = 'Ask the runner';
|
|
1335
|
+
const msg = $('agent-start-msg');
|
|
1336
|
+
msg.textContent = text;
|
|
1337
|
+
msg.hidden = false;
|
|
1338
|
+
// The one-per-issue rule can be set aside on purpose; the cap cannot
|
|
1339
|
+
if (res.body && res.body.code === 'run-open') $('agent-force-field').hidden = false;
|
|
1340
|
+
announce(text);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/** Keys inside the dialog: Escape cancels, Tab stays inside, nothing reaches the map behind it */
|
|
1344
|
+
function onAgentKey(e) {
|
|
1345
|
+
e.stopPropagation();
|
|
1346
|
+
if (e.key === 'Escape') {
|
|
1347
|
+
e.preventDefault();
|
|
1348
|
+
if (!ui.agent || !ui.agent.posting) closeAgent();
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
if (e.key !== 'Tab') return;
|
|
1352
|
+
const fields = [...agentEl().querySelectorAll('select, textarea, input, button')].filter((el) => !el.disabled && !el.closest('[hidden]'));
|
|
1353
|
+
if (!fields.length) { e.preventDefault(); return; }
|
|
1354
|
+
const at = fields.indexOf(document.activeElement);
|
|
1355
|
+
const next = e.shiftKey ? (at <= 0 ? fields.length - 1 : at - 1) : (at === -1 || at === fields.length - 1 ? 0 : at + 1);
|
|
1356
|
+
e.preventDefault();
|
|
1357
|
+
fields[next].focus();
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function onAgentClick(e) {
|
|
1361
|
+
const t = e.target;
|
|
1362
|
+
if (t === agentEl()) { if (ui.agent && !ui.agent.posting) closeAgent(); return; } // the scrim
|
|
1363
|
+
if (t.closest && t.closest('[data-action="agent-start-cancel"]') && ui.agent && !ui.agent.posting) closeAgent();
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/** Asks a run to stop: one POST, then the runs are read again */
|
|
1367
|
+
async function stopRun(runId) {
|
|
1368
|
+
if (ui.busy) return;
|
|
1369
|
+
ui.busy = 'stop';
|
|
1370
|
+
ui.runNote = null;
|
|
1371
|
+
announce(`Asking ${runId} to stop`);
|
|
1372
|
+
paint();
|
|
1373
|
+
const res = await post(stopUrl(runId), {});
|
|
1374
|
+
ui.busy = '';
|
|
1375
|
+
ui.runNote = { text: stopWords(res.status, res.body, runId), ok: res.ok };
|
|
1376
|
+
announce(ui.runNote.text);
|
|
1377
|
+
await loadRuns(false);
|
|
1378
|
+
paint();
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1194
1381
|
// ---------------------------------------------------------------------------
|
|
1195
1382
|
// Opening and closing
|
|
1196
1383
|
// ---------------------------------------------------------------------------
|
|
@@ -1317,6 +1504,10 @@ function onClick(e) {
|
|
|
1317
1504
|
announce(`${node.label}, ${node.type}`);
|
|
1318
1505
|
return;
|
|
1319
1506
|
}
|
|
1507
|
+
const agent = t.closest('[data-forge-agent]');
|
|
1508
|
+
if (agent) { openAgent(agent.dataset.forgeAgent, `[data-forge-agent="${agent.dataset.forgeAgent}"]`); return; }
|
|
1509
|
+
const stop = t.closest('[data-forge-stop]');
|
|
1510
|
+
if (stop) { stopRun(stop.dataset.forgeStop); return; }
|
|
1320
1511
|
const unland = t.closest('[data-forge-landing-cancel]');
|
|
1321
1512
|
if (unland) { cancelLanding(Number(unland.dataset.forgeLandingCancel)); return; }
|
|
1322
1513
|
const merge = t.closest('[data-forge-merge]');
|
|
@@ -1420,6 +1611,12 @@ export function initForgePanel(config, opts = {}) {
|
|
|
1420
1611
|
panel.addEventListener('change', onChange);
|
|
1421
1612
|
panel.addEventListener('input', onInput);
|
|
1422
1613
|
panel.addEventListener('keydown', onKey);
|
|
1614
|
+
const agent = agentEl();
|
|
1615
|
+
if (agent) {
|
|
1616
|
+
agent.addEventListener('click', onAgentClick);
|
|
1617
|
+
agent.addEventListener('keydown', onAgentKey);
|
|
1618
|
+
agent.querySelector('form').addEventListener('submit', submitAgent);
|
|
1619
|
+
}
|
|
1423
1620
|
const confirm = confirmEl();
|
|
1424
1621
|
if (confirm) {
|
|
1425
1622
|
confirm.addEventListener('click', onConfirmClick);
|
|
@@ -1439,6 +1636,7 @@ export function initForgePanel(config, opts = {}) {
|
|
|
1439
1636
|
/** For tests: forget the page this was wired to, and everything shown */
|
|
1440
1637
|
export function resetForgePanelForTests(clock = () => Date.now()) {
|
|
1441
1638
|
if (ui.confirm) closeConfirm({ restoreFocus: false });
|
|
1639
|
+
if (ui.agent) closeAgent({ restoreFocus: false });
|
|
1442
1640
|
if (offScene) offScene();
|
|
1443
1641
|
offScene = null;
|
|
1444
1642
|
clearTimeout(searchTimer);
|
|
@@ -1453,5 +1651,5 @@ export function resetForgePanelForTests(clock = () => Date.now()) {
|
|
|
1453
1651
|
indexCache = null;
|
|
1454
1652
|
state.forgeLit = null;
|
|
1455
1653
|
state.threadIndex = null;
|
|
1456
|
-
Object.assign(ui, { thread: null, threadDetail: null, threadError: '', litThread: null, idea: freshIdea(), config: null, open: false, kind: 'pulls', filters: defaultFilters(), lists: {}, work: null, workFilters: { agentsOnly: false, finished: false }, loading: false, error: '', selected: null, detail: null, landing: null, detailError: '', lit: null, deploy: null, deployItem: null, litDeploy: null, nodesSeen: null, busy: '', note: null, confirm: null, actNote: null, notes: freshNotes() });
|
|
1654
|
+
Object.assign(ui, { thread: null, threadDetail: null, threadError: '', litThread: null, idea: freshIdea(), config: null, open: false, kind: 'pulls', filters: defaultFilters(), lists: {}, work: null, workFilters: { agentsOnly: false, finished: false }, loading: false, error: '', selected: null, detail: null, landing: null, detailError: '', lit: null, deploy: null, deployItem: null, litDeploy: null, nodesSeen: null, busy: '', note: null, confirm: null, actNote: null, notes: freshNotes(), runs: null, runNote: null, agent: null });
|
|
1457
1655
|
}
|
package/explorer/index.html
CHANGED
|
@@ -265,6 +265,13 @@
|
|
|
265
265
|
</div>
|
|
266
266
|
</div>
|
|
267
267
|
</section>
|
|
268
|
+
<!-- Agent runs (forge-panel.js, runs-model.js): shown on the Agents tab; each run's state, what it is about, links, and Stop -->
|
|
269
|
+
<section id="forge-runs" class="hx-forge-runs" aria-labelledby="forge-runs-title" hidden>
|
|
270
|
+
<h3 id="forge-runs-title" class="hx-forge-runs-title">Agent runs</h3>
|
|
271
|
+
<p id="forge-runs-status" class="hx-forge-meta hx-faint"></p>
|
|
272
|
+
<p id="forge-runs-note" class="hx-forge-notice" hidden></p>
|
|
273
|
+
<ul id="forge-runs-list" class="hx-forge-files" aria-label="Runs"></ul>
|
|
274
|
+
</section>
|
|
268
275
|
<section id="forge-detail" class="hx-forge-detail" aria-label="Selected pull request" hidden></section>
|
|
269
276
|
<ul id="forge-list" class="hx-forge-list" aria-label="Results"></ul>
|
|
270
277
|
<p id="forge-rate" class="hx-forge-rate hx-faint"></p>
|
|
@@ -339,6 +346,26 @@
|
|
|
339
346
|
</div>
|
|
340
347
|
</div>
|
|
341
348
|
|
|
349
|
+
<!-- Start an agent (forge-panel.js): kind, profile, a brief, and a confirm step; a modal dialog over a scrim, the focus stays in it -->
|
|
350
|
+
<div id="agent-start" class="hx-confirm-scrim" hidden>
|
|
351
|
+
<div class="hx-confirm glass" role="dialog" aria-modal="true" aria-labelledby="agent-start-title" aria-describedby="agent-start-words">
|
|
352
|
+
<h2 id="agent-start-title" class="hx-confirm-title">Start an agent</h2>
|
|
353
|
+
<p id="agent-start-about" class="hx-forge-meta"></p>
|
|
354
|
+
<form id="agent-start-form" class="hx-forge-confirm" novalidate>
|
|
355
|
+
<label class="hx-forge-field">Kind of run<select id="agent-kind" class="hx-forge-select"></select></label>
|
|
356
|
+
<label id="agent-profile-field" class="hx-forge-field" hidden>Profile<select id="agent-profile" class="hx-forge-select"></select></label>
|
|
357
|
+
<label class="hx-forge-field">Brief for the runner<textarea id="agent-brief" class="hx-forge-preview" rows="6" maxlength="20000" spellcheck="true"></textarea></label>
|
|
358
|
+
<label id="agent-force-field" class="hx-forge-toggle" hidden><input type="checkbox" id="agent-force" /> Ask for another run on purpose</label>
|
|
359
|
+
<p id="agent-start-words" class="hx-confirm-note"></p>
|
|
360
|
+
<p id="agent-start-msg" class="hx-forge-msg hx-forge-msg--bad" role="alert" hidden></p>
|
|
361
|
+
<div class="hx-confirm-actions">
|
|
362
|
+
<button type="button" class="hx-btn" data-action="agent-start-cancel">Cancel</button>
|
|
363
|
+
<button type="submit" class="hx-btn hx-btn--approve" id="agent-start-go">Ask the runner</button>
|
|
364
|
+
</div>
|
|
365
|
+
</form>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
|
|
342
369
|
<!-- Review tokens (review-tokens.js): make one (shown once), list them, revoke; a modal dialog over a scrim, the focus stays in it -->
|
|
343
370
|
<div id="tokens-dialog" class="hx-confirm-scrim" hidden>
|
|
344
371
|
<div class="hx-confirm glass" role="dialog" aria-modal="true" aria-labelledby="tokens-title" aria-describedby="tokens-intro">
|
|
@@ -354,6 +381,7 @@
|
|
|
354
381
|
</div>
|
|
355
382
|
<ul id="tokens-list" class="hx-tokens-list" aria-label="Your review tokens"></ul>
|
|
356
383
|
<form id="tokens-form" class="hx-tokens-form" autocomplete="off">
|
|
384
|
+
<label class="hx-forge-field">Kind of token<select id="tokens-scope" class="hx-forge-select"><option value="review">Review: approve or send back proposals</option><option value="agent">Agent: ask for agent runs (POST /api/agent-requests)</option></select></label>
|
|
357
385
|
<label class="hx-forge-field">Label<input id="tokens-label" class="hx-forge-input" type="text" maxlength="60" placeholder="chat bot" /></label>
|
|
358
386
|
<label class="hx-forge-field">Days<input id="tokens-days" class="hx-forge-input" type="text" inputmode="numeric" maxlength="2" aria-describedby="tokens-days-hint" /></label>
|
|
359
387
|
<span id="tokens-days-hint" class="hx-sr-only"></span>
|