@cat-factory/gatekeeper-worker 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +276 -0
- package/dist/approvals.d.ts +90 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +193 -0
- package/dist/approvals.js.map +1 -0
- package/dist/capability.d.ts +61 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +125 -0
- package/dist/capability.js.map +1 -0
- package/dist/env.d.ts +45 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +90 -0
- package/dist/env.js.map +1 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +30 -0
- package/dist/errors.js.map +1 -0
- package/dist/gatekeeper.d.ts +78 -0
- package/dist/gatekeeper.d.ts.map +1 -0
- package/dist/gatekeeper.js +162 -0
- package/dist/gatekeeper.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +62 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +152 -0
- package/dist/keys.js.map +1 -0
- package/dist/masking.d.ts +9 -0
- package/dist/masking.d.ts.map +1 -0
- package/dist/masking.js +43 -0
- package/dist/masking.js.map +1 -0
- package/dist/policy/compile.d.ts +88 -0
- package/dist/policy/compile.d.ts.map +1 -0
- package/dist/policy/compile.js +170 -0
- package/dist/policy/compile.js.map +1 -0
- package/dist/policy/decisions.d.ts +93 -0
- package/dist/policy/decisions.d.ts.map +1 -0
- package/dist/policy/decisions.js +659 -0
- package/dist/policy/decisions.js.map +1 -0
- package/dist/policy/index.d.ts +6 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +17 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/state.d.ts +150 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +229 -0
- package/dist/state.js.map +1 -0
- package/dist/webhook/delivery.d.ts +102 -0
- package/dist/webhook/delivery.d.ts.map +1 -0
- package/dist/webhook/delivery.js +162 -0
- package/dist/webhook/delivery.js.map +1 -0
- package/dist/webhook/signature.d.ts +17 -0
- package/dist/webhook/signature.d.ts.map +1 -0
- package/dist/webhook/signature.js +73 -0
- package/dist/webhook/signature.js.map +1 -0
- package/dist/worker.d.ts +21 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +155 -0
- package/dist/worker.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
// Every park the public surface can answer, as data.
|
|
2
|
+
//
|
|
3
|
+
// A run can stop on THIRTEEN different decision kinds, not one. The first cut of this Gatekeeper
|
|
4
|
+
// understood only `approval-gate`, so a card raised for a requirements review, a fork, a judge
|
|
5
|
+
// verdict or a follow-up triage arrived in the inbox and could never be answered from it: the
|
|
6
|
+
// answer path looked for a pending approval gate, found none, and reported the card stale while
|
|
7
|
+
// the run stayed blocked. The gap was invisible because a card that reports `stale` looks like a
|
|
8
|
+
// card whose run moved on.
|
|
9
|
+
//
|
|
10
|
+
// So the answer path does not know any kind. It reads the run's live decision list, finds the
|
|
11
|
+
// entry that is actually holding the run, and looks up the ANSWERER for its `kind` here. Adding a
|
|
12
|
+
// park to the platform means adding an entry below; there is no `switch` anywhere else, and
|
|
13
|
+
// the table is a `Record` the SDK's own kind union keys, so a kind the platform gains and this
|
|
14
|
+
// table lacks fails the build rather than shipping as a card nobody can answer.
|
|
15
|
+
//
|
|
16
|
+
// Two rules bind every entry:
|
|
17
|
+
//
|
|
18
|
+
// - A VERB FORWARDS THROUGH A BINDING, never through a client of its own. `binding` names the
|
|
19
|
+
// operation, the capability's `invoke` resolves it against what policy granted, and a tier
|
|
20
|
+
// that was not granted it is refused there. The answer flow has no privilege.
|
|
21
|
+
// - A MISSING INPUT IS A REFUSAL, never a default. `request-changes` with no feedback used to
|
|
22
|
+
// forward an empty string, which the API rejects with a 422 the caller cannot read; a gate at
|
|
23
|
+
// its rework cap used to default to `proceed`, silently picking the one settlement nobody
|
|
24
|
+
// asked for. Every required field is declared and named back at the caller.
|
|
25
|
+
import { GatekeeperError } from '../errors.js';
|
|
26
|
+
// ---- Reading the caller's fields ------------------------------------------------------------
|
|
27
|
+
function refuse(field, expected) {
|
|
28
|
+
throw new GatekeeperError('invalid_answer', `This answer needs '${field}': ${expected}. Read the verb's own \`fields\` from ` +
|
|
29
|
+
'approvals_inspect() rather than guessing; the platform refuses a blank one with a 422 ' +
|
|
30
|
+
'that says less than this does.');
|
|
31
|
+
}
|
|
32
|
+
/** A required non-blank string. */
|
|
33
|
+
function text(input, field, expected) {
|
|
34
|
+
const value = input[field];
|
|
35
|
+
if (typeof value !== 'string' || value.trim().length === 0)
|
|
36
|
+
refuse(field, expected);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
/** An optional string. Absent stays absent: an empty one is a value the API would reject. */
|
|
40
|
+
function optionalText(input, field) {
|
|
41
|
+
const value = input[field];
|
|
42
|
+
return typeof value === 'string' && value.length > 0 ? { [field]: value } : {};
|
|
43
|
+
}
|
|
44
|
+
/** A required member of a closed set. */
|
|
45
|
+
function choice(input, field, allowed) {
|
|
46
|
+
const value = input[field];
|
|
47
|
+
if (typeof value !== 'string' || !allowed.includes(value)) {
|
|
48
|
+
refuse(field, `one of ${allowed.map((option) => `'${option}'`).join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
/** A required id the caller picks out of the decision (an item, a finding, a question). */
|
|
53
|
+
function pick(input, field, from) {
|
|
54
|
+
return text(input, field, `the id of the ${from} this answer addresses`);
|
|
55
|
+
}
|
|
56
|
+
/** A string field of the live decision, or a refusal naming what the platform did not send. */
|
|
57
|
+
function fromDecision(decision, field) {
|
|
58
|
+
const value = decision[field];
|
|
59
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
60
|
+
throw new GatekeeperError('malformed_decision', `The parked '${decision.kind}' decision carries no '${field}', which every answer to it has ` +
|
|
61
|
+
'to address. This is a platform-side shape this Gatekeeper does not recognise; upgrade it ' +
|
|
62
|
+
'if the deployment is newer, and report it if not.');
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
// ---- Field declarations, shared where the platform shares a body --------------------------
|
|
67
|
+
const ITERATION_CAP_CHOICES = ['extra-round', 'proceed', 'stop-reset'];
|
|
68
|
+
const CAP_FIELDS = [
|
|
69
|
+
{
|
|
70
|
+
name: 'choice',
|
|
71
|
+
required: true,
|
|
72
|
+
choices: ITERATION_CAP_CHOICES,
|
|
73
|
+
detail: 'One more pass, proceed with what the last pass produced, or stop and reset the task. ' +
|
|
74
|
+
'There is deliberately no default: a parked run waits indefinitely, so picking one for you ' +
|
|
75
|
+
'would ship work nobody approved.',
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
const FINDINGS_FIELDS = [
|
|
79
|
+
{
|
|
80
|
+
name: 'findings',
|
|
81
|
+
required: true,
|
|
82
|
+
detail: 'What is wrong. This text IS the prompt the fixer works from, so it cannot be blank.',
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
function capVerb(action, binding, summary) {
|
|
86
|
+
return {
|
|
87
|
+
action,
|
|
88
|
+
binding,
|
|
89
|
+
summary,
|
|
90
|
+
fields: CAP_FIELDS,
|
|
91
|
+
call: (decision, input) => ({
|
|
92
|
+
binding,
|
|
93
|
+
args: {
|
|
94
|
+
...pathOf(decision),
|
|
95
|
+
body: { choice: choice(input, 'choice', ITERATION_CAP_CHOICES) },
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The path arguments an entry contributes to every call against it.
|
|
102
|
+
*
|
|
103
|
+
* Only `brainstorm` has one, and it is why this exists rather than being inlined: the stage is
|
|
104
|
+
* part of the ROUTE for all six brainstorm verbs, and a block can hold a `requirements` and an
|
|
105
|
+
* `architecture` session at once, so answering without it would settle whichever the platform
|
|
106
|
+
* reached first.
|
|
107
|
+
*/
|
|
108
|
+
function pathOf(decision) {
|
|
109
|
+
return decision.kind === 'brainstorm' ? { stage: fromDecision(decision, 'stage') } : {};
|
|
110
|
+
}
|
|
111
|
+
/** The shared verb set of the three iterative review loops (requirements / clarity / brainstorm). */
|
|
112
|
+
function reviewVerbs(bindings) {
|
|
113
|
+
return [
|
|
114
|
+
{
|
|
115
|
+
action: 'reply',
|
|
116
|
+
binding: bindings.reply,
|
|
117
|
+
summary: `Answer one ${bindings.item}. This is what an incorporation folds in.`,
|
|
118
|
+
fields: [
|
|
119
|
+
{ name: 'itemId', required: true, detail: `The ${bindings.item}'s stable id.` },
|
|
120
|
+
{ name: 'reply', required: true, detail: 'The answer, in prose.' },
|
|
121
|
+
],
|
|
122
|
+
call: (decision, input) => ({
|
|
123
|
+
binding: bindings.reply,
|
|
124
|
+
args: {
|
|
125
|
+
...pathOf(decision),
|
|
126
|
+
itemId: pick(input, 'itemId', bindings.item),
|
|
127
|
+
body: { reply: text(input, 'reply', 'the answer to fold in') },
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
action: 'set-status',
|
|
133
|
+
binding: bindings.setStatus,
|
|
134
|
+
summary: `Dismiss a ${bindings.item} as not applicable, or reopen one dismissed by mistake.`,
|
|
135
|
+
fields: [
|
|
136
|
+
{ name: 'itemId', required: true, detail: `The ${bindings.item}'s stable id.` },
|
|
137
|
+
{
|
|
138
|
+
name: 'status',
|
|
139
|
+
required: true,
|
|
140
|
+
choices: ['dismissed', 'open'],
|
|
141
|
+
detail: 'The new status.',
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
call: (decision, input) => ({
|
|
145
|
+
binding: bindings.setStatus,
|
|
146
|
+
args: {
|
|
147
|
+
...pathOf(decision),
|
|
148
|
+
itemId: pick(input, 'itemId', bindings.item),
|
|
149
|
+
body: { status: choice(input, 'status', ['dismissed', 'open']) },
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
action: 'incorporate',
|
|
155
|
+
binding: bindings.incorporate,
|
|
156
|
+
summary: 'Fold the recorded answers into one standardized document and re-review it. Asynchronous: ' +
|
|
157
|
+
'the run stays parked until the next pass lands.',
|
|
158
|
+
fields: [
|
|
159
|
+
{ name: 'feedback', required: false, detail: 'Optional "do it differently" steer.' },
|
|
160
|
+
],
|
|
161
|
+
call: (decision, input) => ({
|
|
162
|
+
binding: bindings.incorporate,
|
|
163
|
+
args: { ...pathOf(decision), body: optionalText(input, 'feedback') },
|
|
164
|
+
}),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
action: 're-review',
|
|
168
|
+
binding: bindings.reReview,
|
|
169
|
+
summary: 'Run another reviewer pass over the subject as it now stands.',
|
|
170
|
+
fields: [],
|
|
171
|
+
call: (decision) => ({ binding: bindings.reReview, args: pathOf(decision) }),
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
action: 'proceed',
|
|
175
|
+
binding: bindings.proceed,
|
|
176
|
+
summary: 'Accept the subject as it stands and let the run advance.',
|
|
177
|
+
fields: [],
|
|
178
|
+
call: (decision) => ({ binding: bindings.proceed, args: pathOf(decision) }),
|
|
179
|
+
},
|
|
180
|
+
capVerb('resolve-exceeded', bindings.resolveExceeded, 'Settle a loop that spent its reviewer-pass budget.'),
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
/** Whether an iterative review has stopped on a person. */
|
|
184
|
+
function reviewPending(decision) {
|
|
185
|
+
return decision.status === 'ready' || decision.status === 'exceeded';
|
|
186
|
+
}
|
|
187
|
+
// ---- The table ------------------------------------------------------------------------------
|
|
188
|
+
/**
|
|
189
|
+
* Every park, keyed by the kind that names it.
|
|
190
|
+
*
|
|
191
|
+
* `satisfies Record<ParkedDecisionKind, DecisionAnswerer>` is what makes this exhaustive AND
|
|
192
|
+
* closed: a kind the platform adds fails the build here, and a key this file invents that the
|
|
193
|
+
* surface does not have fails it too. Both matter, because either one produces the same symptom
|
|
194
|
+
* in production (a park nobody can answer) from opposite causes.
|
|
195
|
+
*/
|
|
196
|
+
const ANSWERERS = {
|
|
197
|
+
'approval-gate': {
|
|
198
|
+
summary: 'A pipeline step finished and the run is holding its output in front of a person.',
|
|
199
|
+
pending: (decision) => decision.status === 'pending',
|
|
200
|
+
verbs: [
|
|
201
|
+
{
|
|
202
|
+
action: 'approve',
|
|
203
|
+
binding: 'decisions_approve_step',
|
|
204
|
+
summary: 'Let the output through, optionally replacing it with an edited proposal.',
|
|
205
|
+
fields: [
|
|
206
|
+
{
|
|
207
|
+
name: 'proposal',
|
|
208
|
+
required: false,
|
|
209
|
+
detail: 'Replaces the agent’s text and is what flows downstream. Omit to accept as written.',
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
call: (decision, input) => ({
|
|
213
|
+
binding: 'decisions_approve_step',
|
|
214
|
+
args: {
|
|
215
|
+
approvalId: fromDecision(decision, 'approvalId'),
|
|
216
|
+
body: optionalText(input, 'proposal'),
|
|
217
|
+
},
|
|
218
|
+
}),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
action: 'request-changes',
|
|
222
|
+
binding: 'decisions_request_step_changes',
|
|
223
|
+
summary: 'Send the step back to re-run with this guidance folded in.',
|
|
224
|
+
fields: [
|
|
225
|
+
{
|
|
226
|
+
name: 'feedback',
|
|
227
|
+
required: true,
|
|
228
|
+
detail: 'The guidance the re-run works from. The platform refuses a blank one.',
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
call: (decision, input) => ({
|
|
232
|
+
binding: 'decisions_request_step_changes',
|
|
233
|
+
args: {
|
|
234
|
+
approvalId: fromDecision(decision, 'approvalId'),
|
|
235
|
+
body: { feedback: text(input, 'feedback', 'the guidance the re-run works from') },
|
|
236
|
+
},
|
|
237
|
+
}),
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
action: 'reject',
|
|
241
|
+
binding: 'decisions_reject_step',
|
|
242
|
+
summary: 'Stop the run entirely. Terminal, though the board can retry it.',
|
|
243
|
+
fields: [{ name: 'reason', required: false, detail: 'Why the run is being stopped.' }],
|
|
244
|
+
call: (decision, input) => ({
|
|
245
|
+
binding: 'decisions_reject_step',
|
|
246
|
+
args: {
|
|
247
|
+
approvalId: fromDecision(decision, 'approvalId'),
|
|
248
|
+
body: optionalText(input, 'reason'),
|
|
249
|
+
},
|
|
250
|
+
}),
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
action: 'resolve-exceeded',
|
|
254
|
+
binding: 'decisions_resolve_step_exceeded',
|
|
255
|
+
summary: 'Settle a companion gate parked at its automatic-rework cap.',
|
|
256
|
+
fields: CAP_FIELDS,
|
|
257
|
+
call: (decision, input) => ({
|
|
258
|
+
binding: 'decisions_resolve_step_exceeded',
|
|
259
|
+
args: {
|
|
260
|
+
approvalId: fromDecision(decision, 'approvalId'),
|
|
261
|
+
body: { choice: choice(input, 'choice', ITERATION_CAP_CHOICES) },
|
|
262
|
+
},
|
|
263
|
+
}),
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
'agent-decision': {
|
|
268
|
+
summary: 'Mid-work the agent hit a fork it would not choose unilaterally and asked.',
|
|
269
|
+
// The projection carries no lifecycle field: an answered decision is not listed at all, so an
|
|
270
|
+
// entry that is present is one that is waiting.
|
|
271
|
+
pending: () => true,
|
|
272
|
+
verbs: [
|
|
273
|
+
{
|
|
274
|
+
action: 'answer',
|
|
275
|
+
binding: 'decisions_answer_agent_decision',
|
|
276
|
+
summary: 'Answer the question. The asking step re-runs with the choice folded in.',
|
|
277
|
+
fields: [
|
|
278
|
+
{
|
|
279
|
+
name: 'choice',
|
|
280
|
+
required: true,
|
|
281
|
+
detail: 'Taken verbatim, so it need not be one of the offered `options`, but answering ' +
|
|
282
|
+
'off-list hands the agent an approach it did not propose.',
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
call: (decision, input) => ({
|
|
286
|
+
binding: 'decisions_answer_agent_decision',
|
|
287
|
+
args: {
|
|
288
|
+
decisionId: fromDecision(decision, 'decisionId'),
|
|
289
|
+
body: { choice: text(input, 'choice', 'the answer the step re-runs with') },
|
|
290
|
+
},
|
|
291
|
+
}),
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
},
|
|
295
|
+
fork: {
|
|
296
|
+
summary: 'Materially different implementations were proposed before any code was written.',
|
|
297
|
+
pending: (decision) => decision.status === 'awaiting_choice',
|
|
298
|
+
verbs: [
|
|
299
|
+
{
|
|
300
|
+
action: 'choose',
|
|
301
|
+
binding: 'decisions_choose_fork',
|
|
302
|
+
summary: 'Pick a proposed approach, or supply your own.',
|
|
303
|
+
fields: [
|
|
304
|
+
{
|
|
305
|
+
name: 'forkId',
|
|
306
|
+
required: false,
|
|
307
|
+
detail: 'One of the proposed forks. Exclusive with `custom`.',
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: 'custom',
|
|
311
|
+
required: false,
|
|
312
|
+
detail: 'Your own approach, in prose. Exclusive with `forkId`.',
|
|
313
|
+
},
|
|
314
|
+
{ name: 'note', required: false, detail: 'A steering note on a picked fork.' },
|
|
315
|
+
],
|
|
316
|
+
call: (_decision, input) => {
|
|
317
|
+
const forkId = optionalText(input, 'forkId');
|
|
318
|
+
const custom = optionalText(input, 'custom');
|
|
319
|
+
if ('forkId' in forkId === 'custom' in custom) {
|
|
320
|
+
refuse('forkId', 'exactly one of `forkId` or `custom` (the platform enforces the same xor)');
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
binding: 'decisions_choose_fork',
|
|
324
|
+
args: { body: { ...forkId, ...custom, ...optionalText(input, 'note') } },
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
},
|
|
330
|
+
judge: {
|
|
331
|
+
summary: 'A rubric scored the work below the task’s threshold and the run stopped.',
|
|
332
|
+
pending: (decision) => decision.status === 'awaiting_decision',
|
|
333
|
+
verbs: [
|
|
334
|
+
{
|
|
335
|
+
action: 'resolve',
|
|
336
|
+
binding: 'decisions_resolve_judge',
|
|
337
|
+
summary: 'Proceed anyway, bounce the work back for rework, or stop the run.',
|
|
338
|
+
fields: [
|
|
339
|
+
{
|
|
340
|
+
name: 'choice',
|
|
341
|
+
required: true,
|
|
342
|
+
choices: ['proceed', 'bounce', 'stop'],
|
|
343
|
+
detail: 'What to do with the verdict.',
|
|
344
|
+
},
|
|
345
|
+
{ name: 'feedback', required: false, detail: 'Guidance a `bounce` re-runs with.' },
|
|
346
|
+
],
|
|
347
|
+
call: (_decision, input) => ({
|
|
348
|
+
binding: 'decisions_resolve_judge',
|
|
349
|
+
args: {
|
|
350
|
+
body: {
|
|
351
|
+
choice: choice(input, 'choice', ['proceed', 'bounce', 'stop']),
|
|
352
|
+
...optionalText(input, 'feedback'),
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
}),
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
},
|
|
359
|
+
'input-gate': {
|
|
360
|
+
summary: 'The task states nothing an agent could act on; the run stopped before its first dispatch.',
|
|
361
|
+
pending: (decision) => decision.status === 'blocked',
|
|
362
|
+
verbs: [
|
|
363
|
+
{
|
|
364
|
+
action: 'resolve',
|
|
365
|
+
binding: 'decisions_resolve_input_gate',
|
|
366
|
+
summary: 'Re-check the task as it now stands (fix it over tasks_update first), or waive the findings.',
|
|
367
|
+
fields: [
|
|
368
|
+
{
|
|
369
|
+
name: 'choice',
|
|
370
|
+
required: true,
|
|
371
|
+
choices: ['recheck', 'proceed'],
|
|
372
|
+
detail: '`recheck` re-evaluates; `proceed` waives the findings and records who did it.',
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
call: (_decision, input) => ({
|
|
376
|
+
binding: 'decisions_resolve_input_gate',
|
|
377
|
+
args: { body: { choice: choice(input, 'choice', ['recheck', 'proceed']) } },
|
|
378
|
+
}),
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
},
|
|
382
|
+
'requirements-review': {
|
|
383
|
+
summary: 'The reviewer raised questions about the requirements and the run is waiting on them.',
|
|
384
|
+
pending: reviewPending,
|
|
385
|
+
verbs: reviewVerbs({
|
|
386
|
+
reply: 'decisions_reply_to_finding',
|
|
387
|
+
setStatus: 'decisions_set_finding_status',
|
|
388
|
+
incorporate: 'decisions_incorporate',
|
|
389
|
+
reReview: 'decisions_re_review',
|
|
390
|
+
proceed: 'decisions_proceed',
|
|
391
|
+
resolveExceeded: 'decisions_resolve_exceeded',
|
|
392
|
+
item: 'finding',
|
|
393
|
+
}),
|
|
394
|
+
},
|
|
395
|
+
'clarity-review': {
|
|
396
|
+
summary: 'The reviewer asked whether the bug report is actually fixable.',
|
|
397
|
+
pending: reviewPending,
|
|
398
|
+
verbs: reviewVerbs({
|
|
399
|
+
reply: 'decisions_reply_to_clarity_finding',
|
|
400
|
+
setStatus: 'decisions_set_clarity_finding_status',
|
|
401
|
+
incorporate: 'decisions_incorporate_clarity',
|
|
402
|
+
reReview: 'decisions_re_review_clarity',
|
|
403
|
+
proceed: 'decisions_proceed_clarity',
|
|
404
|
+
resolveExceeded: 'decisions_resolve_clarity_exceeded',
|
|
405
|
+
item: 'finding',
|
|
406
|
+
}),
|
|
407
|
+
},
|
|
408
|
+
brainstorm: {
|
|
409
|
+
summary: 'The agent proposed directions and the run is waiting for someone to pick and steer.',
|
|
410
|
+
pending: reviewPending,
|
|
411
|
+
verbs: reviewVerbs({
|
|
412
|
+
reply: 'decisions_reply_to_brainstorm_option',
|
|
413
|
+
setStatus: 'decisions_set_brainstorm_option_status',
|
|
414
|
+
incorporate: 'decisions_incorporate_brainstorm',
|
|
415
|
+
reReview: 'decisions_re_review_brainstorm',
|
|
416
|
+
proceed: 'decisions_proceed_brainstorm',
|
|
417
|
+
resolveExceeded: 'decisions_resolve_brainstorm_exceeded',
|
|
418
|
+
item: 'option',
|
|
419
|
+
}),
|
|
420
|
+
},
|
|
421
|
+
'pr-review': {
|
|
422
|
+
summary: 'A deep review sliced an open pull request and is waiting for its findings to be curated.',
|
|
423
|
+
pending: (decision) => decision.status === 'awaiting_selection',
|
|
424
|
+
verbs: [
|
|
425
|
+
{
|
|
426
|
+
action: 'resolve',
|
|
427
|
+
binding: 'decisions_resolve_pr_review',
|
|
428
|
+
summary: 'Say what to do with the curated findings: record, hand to a fixer, or post on the PR.',
|
|
429
|
+
fields: [
|
|
430
|
+
{
|
|
431
|
+
name: 'action',
|
|
432
|
+
required: false,
|
|
433
|
+
detail: 'Omitted reads as `finish`. See the surface’s `prReviewResolution` vocabulary.',
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: 'findingIds',
|
|
437
|
+
required: false,
|
|
438
|
+
detail: 'The findings to act on. Omitted reads as an empty selection, which only `finish` accepts.',
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
call: (_decision, input) => {
|
|
442
|
+
const ids = input.findingIds;
|
|
443
|
+
return {
|
|
444
|
+
binding: 'decisions_resolve_pr_review',
|
|
445
|
+
args: {
|
|
446
|
+
body: {
|
|
447
|
+
...optionalText(input, 'action'),
|
|
448
|
+
...(Array.isArray(ids) ? { findingIds: ids } : {}),
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
action: 'dismiss-finding',
|
|
456
|
+
binding: 'decisions_dismiss_pr_review_finding',
|
|
457
|
+
summary: 'Drop one finding from consideration.',
|
|
458
|
+
fields: [{ name: 'findingId', required: true, detail: 'The finding’s stable id.' }],
|
|
459
|
+
call: (_decision, input) => ({
|
|
460
|
+
binding: 'decisions_dismiss_pr_review_finding',
|
|
461
|
+
args: { findingId: pick(input, 'findingId', 'finding') },
|
|
462
|
+
}),
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
action: 'challenge-finding',
|
|
466
|
+
binding: 'decisions_challenge_pr_review_finding',
|
|
467
|
+
summary: 'Send one finding back to a read-only investigator to uphold, amend or retract.',
|
|
468
|
+
fields: [
|
|
469
|
+
{ name: 'findingId', required: true, detail: 'The finding’s stable id.' },
|
|
470
|
+
{
|
|
471
|
+
name: 'question',
|
|
472
|
+
required: false,
|
|
473
|
+
detail: 'What to check. Omitted uses the generic prompt.',
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
call: (_decision, input) => ({
|
|
477
|
+
binding: 'decisions_challenge_pr_review_finding',
|
|
478
|
+
args: {
|
|
479
|
+
findingId: pick(input, 'findingId', 'finding'),
|
|
480
|
+
body: optionalText(input, 'question'),
|
|
481
|
+
},
|
|
482
|
+
}),
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
},
|
|
486
|
+
'human-test': {
|
|
487
|
+
summary: 'A live ephemeral environment is up and the run is waiting for someone to exercise it.',
|
|
488
|
+
pending: (decision) => decision.phase === 'awaiting_human',
|
|
489
|
+
verbs: [
|
|
490
|
+
{
|
|
491
|
+
action: 'confirm',
|
|
492
|
+
binding: 'decisions_confirm_human_test',
|
|
493
|
+
summary: 'The change works. The run advances.',
|
|
494
|
+
fields: [],
|
|
495
|
+
call: () => ({ binding: 'decisions_confirm_human_test', args: {} }),
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
action: 'request-fix',
|
|
499
|
+
binding: 'decisions_request_human_test_fix',
|
|
500
|
+
summary: 'It does not work. A fixer runs with these findings.',
|
|
501
|
+
fields: FINDINGS_FIELDS,
|
|
502
|
+
call: (_decision, input) => ({
|
|
503
|
+
binding: 'decisions_request_human_test_fix',
|
|
504
|
+
args: { body: { findings: text(input, 'findings', 'what the fixer has to fix') } },
|
|
505
|
+
}),
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
},
|
|
509
|
+
'visual-confirmation': {
|
|
510
|
+
summary: 'Screenshots are waiting to be compared against the uploaded reference designs.',
|
|
511
|
+
pending: (decision) => decision.phase === 'awaiting_human',
|
|
512
|
+
verbs: [
|
|
513
|
+
{
|
|
514
|
+
action: 'approve',
|
|
515
|
+
binding: 'decisions_approve_visual_confirmation',
|
|
516
|
+
summary: 'The screenshots match. The run advances.',
|
|
517
|
+
fields: [],
|
|
518
|
+
call: () => ({ binding: 'decisions_approve_visual_confirmation', args: {} }),
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
action: 'request-fix',
|
|
522
|
+
binding: 'decisions_request_visual_confirmation_fix',
|
|
523
|
+
summary: 'They do not match. A fixer runs with these findings.',
|
|
524
|
+
fields: FINDINGS_FIELDS,
|
|
525
|
+
call: (_decision, input) => ({
|
|
526
|
+
binding: 'decisions_request_visual_confirmation_fix',
|
|
527
|
+
args: { body: { findings: text(input, 'findings', 'what the fixer has to fix') } },
|
|
528
|
+
}),
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
},
|
|
532
|
+
'follow-ups': {
|
|
533
|
+
summary: 'The Coder surfaced forward-looking items and the run stops until each is decided.',
|
|
534
|
+
// This park accrues LIVE: items appear while the step still runs, so the run need not be
|
|
535
|
+
// blocked for them to be answerable. What holds it is a `pending` item, not the run's status.
|
|
536
|
+
pending: (decision) => Array.isArray(decision.items) &&
|
|
537
|
+
decision.items.some((item) => item.status === 'pending'),
|
|
538
|
+
verbs: [
|
|
539
|
+
{
|
|
540
|
+
action: 'file',
|
|
541
|
+
binding: 'decisions_file_follow_up',
|
|
542
|
+
summary: 'File a follow-up as a ticket.',
|
|
543
|
+
fields: [{ name: 'itemId', required: true, detail: 'The item’s stable id.' }],
|
|
544
|
+
call: (_decision, input) => ({
|
|
545
|
+
binding: 'decisions_file_follow_up',
|
|
546
|
+
args: { itemId: pick(input, 'itemId', 'item') },
|
|
547
|
+
}),
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
action: 'send-back',
|
|
551
|
+
binding: 'decisions_send_back_follow_up',
|
|
552
|
+
summary: 'Fold a follow-up into another Coder pass, if the send-back budget allows.',
|
|
553
|
+
fields: [{ name: 'itemId', required: true, detail: 'The item’s stable id.' }],
|
|
554
|
+
call: (_decision, input) => ({
|
|
555
|
+
binding: 'decisions_send_back_follow_up',
|
|
556
|
+
args: { itemId: pick(input, 'itemId', 'item') },
|
|
557
|
+
}),
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
action: 'answer',
|
|
561
|
+
binding: 'decisions_answer_follow_up',
|
|
562
|
+
summary: 'Answer a `question` item.',
|
|
563
|
+
fields: [
|
|
564
|
+
{ name: 'itemId', required: true, detail: 'The item’s stable id.' },
|
|
565
|
+
{ name: 'answer', required: true, detail: 'The answer the next pass works from.' },
|
|
566
|
+
],
|
|
567
|
+
call: (_decision, input) => ({
|
|
568
|
+
binding: 'decisions_answer_follow_up',
|
|
569
|
+
args: {
|
|
570
|
+
itemId: pick(input, 'itemId', 'item'),
|
|
571
|
+
body: { answer: text(input, 'answer', 'the answer the next pass works from') },
|
|
572
|
+
},
|
|
573
|
+
}),
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
action: 'dismiss',
|
|
577
|
+
binding: 'decisions_dismiss_follow_up',
|
|
578
|
+
summary: 'Decide an item needs nothing.',
|
|
579
|
+
fields: [{ name: 'itemId', required: true, detail: 'The item’s stable id.' }],
|
|
580
|
+
call: (_decision, input) => ({
|
|
581
|
+
binding: 'decisions_dismiss_follow_up',
|
|
582
|
+
args: { itemId: pick(input, 'itemId', 'item') },
|
|
583
|
+
}),
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
},
|
|
587
|
+
interview: {
|
|
588
|
+
summary: 'An inline interviewer asked clarifying questions and the run waits while they are answered.',
|
|
589
|
+
// An entry whose questions are all answered means the interviewer pass is IN FLIGHT, and
|
|
590
|
+
// `continue` / `proceed` still act on it, so presence is what makes it answerable.
|
|
591
|
+
pending: () => true,
|
|
592
|
+
verbs: [
|
|
593
|
+
{
|
|
594
|
+
action: 'answer',
|
|
595
|
+
binding: 'decisions_answer_interview_question',
|
|
596
|
+
summary: 'Record one answer. An empty string clears one recorded by mistake.',
|
|
597
|
+
fields: [
|
|
598
|
+
{
|
|
599
|
+
name: 'questionId',
|
|
600
|
+
required: true,
|
|
601
|
+
detail: 'The question’s id. A question whose id is null cannot be answered individually.',
|
|
602
|
+
},
|
|
603
|
+
{ name: 'answer', required: false, detail: 'The answer; an empty string clears it.' },
|
|
604
|
+
],
|
|
605
|
+
call: (_decision, input) => ({
|
|
606
|
+
binding: 'decisions_answer_interview_question',
|
|
607
|
+
args: {
|
|
608
|
+
body: {
|
|
609
|
+
questionId: pick(input, 'questionId', 'question'),
|
|
610
|
+
answer: typeof input.answer === 'string' ? input.answer : '',
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
}),
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
action: 'continue',
|
|
617
|
+
binding: 'decisions_continue_interview',
|
|
618
|
+
summary: 'Submit the answers and let the interviewer ask follow-ups.',
|
|
619
|
+
fields: [],
|
|
620
|
+
call: () => ({ binding: 'decisions_continue_interview', args: {} }),
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
action: 'proceed',
|
|
624
|
+
binding: 'decisions_proceed_interview',
|
|
625
|
+
summary: 'Force the interview to converge on what it has.',
|
|
626
|
+
fields: [],
|
|
627
|
+
call: () => ({ binding: 'decisions_proceed_interview', args: {} }),
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
},
|
|
631
|
+
};
|
|
632
|
+
/** How a kind of park is answered, or `undefined` for one this package does not model. */
|
|
633
|
+
export function answererFor(kind) {
|
|
634
|
+
return ANSWERERS[kind];
|
|
635
|
+
}
|
|
636
|
+
/** Every kind this package can answer. */
|
|
637
|
+
export const ANSWERABLE_DECISION_KINDS = Object.keys(ANSWERERS);
|
|
638
|
+
/**
|
|
639
|
+
* The binding names every answerer forwards through, plus the read that finds the park.
|
|
640
|
+
*
|
|
641
|
+
* Published so a policy author can grant "answer parked decisions" without transcribing forty
|
|
642
|
+
* operation names, and so `approvals_inspect` can say which verbs a tier actually holds. It is
|
|
643
|
+
* DERIVED from the table rather than restated, because a hand-kept copy is exactly the drift the
|
|
644
|
+
* generated binding table exists to prevent one layer down.
|
|
645
|
+
*/
|
|
646
|
+
export const DECISION_BINDINGS = [
|
|
647
|
+
'decisions_list',
|
|
648
|
+
...new Set(Object.values(ANSWERERS).flatMap((answerer) => answerer.verbs.map((verb) => verb.binding))),
|
|
649
|
+
];
|
|
650
|
+
/**
|
|
651
|
+
* The key scope every decision binding needs.
|
|
652
|
+
*
|
|
653
|
+
* Stated as a constant rather than read off the table because it is a POLICY fact a tier author
|
|
654
|
+
* needs before compiling: a tier granting these must mint `decide` keys. `policy.test.ts` pins it
|
|
655
|
+
* against the live table, so a surface that ever lowered a floor would fail there rather than
|
|
656
|
+
* leaving this comment quietly wrong.
|
|
657
|
+
*/
|
|
658
|
+
export const DECISION_KEY_SCOPE = 'decide';
|
|
659
|
+
//# sourceMappingURL=decisions.js.map
|