@agilno-tech/rivet 0.1.0-alpha.0 → 0.1.0-alpha.2
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/README.md +5 -5
- package/dist/v2/protocols/design-authority.md +6 -0
- package/dist/v2/protocols/qa-evidence.md +14 -0
- package/dist/v2/schemas/quality.schema.json +289 -21
- package/dist/v2/templates/github-actions/rivet-deploy.yml +8 -6
- package/dist/v2/templates/harness/SKILL.md +9 -1
- package/package.json +2 -2
- package/protocols/design-authority.md +6 -0
- package/protocols/qa-evidence.md +14 -0
- package/schemas/quality.schema.json +289 -21
- package/src/cli/main.js +4 -0
- package/src/cli/parse-args.js +2 -2
- package/src/commands/human-task.js +10 -2
- package/src/commands/task-governance.js +53 -0
- package/src/commands/work.js +1 -1
- package/src/config/load.js +1 -1
- package/src/config/validate.js +2 -1
- package/src/delivery/contract.js +13 -2
- package/src/delivery/prepare.js +34 -0
- package/src/delivery/review-request.js +12 -8
- package/src/feature/decisions.js +187 -0
- package/src/feature/governance.js +203 -0
- package/src/feature/host-execution.js +11 -3
- package/src/feature/runtime-bridge.js +4 -0
- package/src/feature/workflow.js +12 -1
- package/src/git/client.js +12 -0
- package/src/quality/content-review.js +225 -0
- package/src/quality/content-scan.js +120 -0
- package/src/state/paths.js +10 -0
- package/templates/github-actions/rivet-deploy.yml +8 -6
- package/templates/harness/SKILL.md +9 -1
|
@@ -4,46 +4,314 @@
|
|
|
4
4
|
"title": "Agilno tracked quality policy",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"additionalProperties": false,
|
|
7
|
-
"required": [
|
|
7
|
+
"required": [
|
|
8
|
+
"schemaVersion",
|
|
9
|
+
"providerRefs",
|
|
10
|
+
"completionProfileRefs",
|
|
11
|
+
"commandGates",
|
|
12
|
+
"expectations",
|
|
13
|
+
"evidence"
|
|
14
|
+
],
|
|
8
15
|
"properties": {
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
"contentScan": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"properties": {
|
|
20
|
+
"secrets": {
|
|
21
|
+
"type": "boolean"
|
|
22
|
+
},
|
|
23
|
+
"unicode": {
|
|
24
|
+
"enum": [
|
|
25
|
+
"warn",
|
|
26
|
+
"required",
|
|
27
|
+
"off"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"maxFiles": {
|
|
31
|
+
"type": "integer",
|
|
32
|
+
"minimum": 1,
|
|
33
|
+
"maximum": 1000
|
|
34
|
+
},
|
|
35
|
+
"maxBytes": {
|
|
36
|
+
"type": "integer",
|
|
37
|
+
"minimum": 1,
|
|
38
|
+
"maximum": 2097152
|
|
39
|
+
},
|
|
40
|
+
"allowlist": {
|
|
41
|
+
"type": "array",
|
|
42
|
+
"maxItems": 100,
|
|
43
|
+
"items": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"additionalProperties": false,
|
|
46
|
+
"required": [
|
|
47
|
+
"ruleId",
|
|
48
|
+
"path",
|
|
49
|
+
"reason"
|
|
50
|
+
],
|
|
51
|
+
"properties": {
|
|
52
|
+
"ruleId": {
|
|
53
|
+
"enum": [
|
|
54
|
+
"private-key",
|
|
55
|
+
"github-token",
|
|
56
|
+
"npm-token",
|
|
57
|
+
"aws-access-key",
|
|
58
|
+
"unicode-bidi",
|
|
59
|
+
"unicode-mixed-identifier",
|
|
60
|
+
"type-suppression"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
"path": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"minLength": 1,
|
|
66
|
+
"maxLength": 1024,
|
|
67
|
+
"pattern": "^(?!/)(?![A-Za-z]:)(?!.*(?:^|/)\\.{1,2}(?:/|$))(?!.*//)(?!.*[/]$)[^\\\\*?\\[\\]{}\\u0000-\\u001f\\u007f]+$"
|
|
68
|
+
},
|
|
69
|
+
"reason": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"minLength": 1,
|
|
72
|
+
"maxLength": 500,
|
|
73
|
+
"pattern": "\\S"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"shortcutRules": {
|
|
79
|
+
"type": "array",
|
|
80
|
+
"maxItems": 100,
|
|
81
|
+
"items": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"additionalProperties": false,
|
|
84
|
+
"required": [
|
|
85
|
+
"ruleId",
|
|
86
|
+
"paths",
|
|
87
|
+
"reason"
|
|
88
|
+
],
|
|
89
|
+
"properties": {
|
|
90
|
+
"ruleId": {
|
|
91
|
+
"const": "type-suppression"
|
|
92
|
+
},
|
|
93
|
+
"paths": {
|
|
94
|
+
"type": "array",
|
|
95
|
+
"minItems": 1,
|
|
96
|
+
"maxItems": 100,
|
|
97
|
+
"uniqueItems": true,
|
|
98
|
+
"items": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"minLength": 1,
|
|
101
|
+
"maxLength": 1024,
|
|
102
|
+
"pattern": "^(?!/)(?![A-Za-z]:)(?!.*(?:^|/)\\.{1,2}(?:/|$))(?!.*//)(?!.*[/]$)[^\\\\*?\\[\\]{}\\u0000-\\u001f\\u007f]+$"
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"reason": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"minLength": 1,
|
|
108
|
+
"maxLength": 500,
|
|
109
|
+
"pattern": "\\S"
|
|
110
|
+
},
|
|
111
|
+
"required": {
|
|
112
|
+
"type": "boolean"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"schemaVersion": {
|
|
120
|
+
"type": "integer",
|
|
121
|
+
"const": 1
|
|
122
|
+
},
|
|
123
|
+
"providerRefs": {
|
|
124
|
+
"type": "array",
|
|
125
|
+
"minItems": 1,
|
|
126
|
+
"maxItems": 64,
|
|
127
|
+
"uniqueItems": true,
|
|
128
|
+
"items": {
|
|
129
|
+
"$ref": "#/$defs/id"
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"completionProfileRefs": {
|
|
133
|
+
"type": "array",
|
|
134
|
+
"minItems": 1,
|
|
135
|
+
"maxItems": 32,
|
|
136
|
+
"uniqueItems": true,
|
|
137
|
+
"items": {
|
|
138
|
+
"$ref": "#/$defs/id"
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"commandGates": {
|
|
142
|
+
"type": "array",
|
|
143
|
+
"minItems": 1,
|
|
144
|
+
"maxItems": 32,
|
|
145
|
+
"items": {
|
|
146
|
+
"$ref": "#/$defs/commandGate"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
13
149
|
"expectations": {
|
|
14
150
|
"type": "object",
|
|
15
151
|
"additionalProperties": false,
|
|
16
|
-
"required": [
|
|
152
|
+
"required": [
|
|
153
|
+
"storybook",
|
|
154
|
+
"playwright",
|
|
155
|
+
"accessibility",
|
|
156
|
+
"security",
|
|
157
|
+
"visual"
|
|
158
|
+
],
|
|
17
159
|
"properties": {
|
|
18
|
-
"storybook": {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
160
|
+
"storybook": {
|
|
161
|
+
"enum": [
|
|
162
|
+
"optional",
|
|
163
|
+
"required"
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
"playwright": {
|
|
167
|
+
"enum": [
|
|
168
|
+
"optional",
|
|
169
|
+
"required"
|
|
170
|
+
]
|
|
171
|
+
},
|
|
172
|
+
"accessibility": {
|
|
173
|
+
"enum": [
|
|
174
|
+
"none",
|
|
175
|
+
"wcag-a",
|
|
176
|
+
"wcag-aa"
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
"security": {
|
|
180
|
+
"enum": [
|
|
181
|
+
"required",
|
|
182
|
+
"not-applicable-with-approval"
|
|
183
|
+
]
|
|
184
|
+
},
|
|
185
|
+
"visual": {
|
|
186
|
+
"enum": [
|
|
187
|
+
"none",
|
|
188
|
+
"automated",
|
|
189
|
+
"human-baseline"
|
|
190
|
+
]
|
|
191
|
+
}
|
|
23
192
|
}
|
|
24
193
|
},
|
|
25
194
|
"evidence": {
|
|
26
195
|
"type": "object",
|
|
27
196
|
"additionalProperties": false,
|
|
28
|
-
"required": [
|
|
197
|
+
"required": [
|
|
198
|
+
"requiredTypes",
|
|
199
|
+
"requireHumanBaseline",
|
|
200
|
+
"requireHumanFinal"
|
|
201
|
+
],
|
|
29
202
|
"properties": {
|
|
30
|
-
"requiredTypes": {
|
|
31
|
-
|
|
32
|
-
|
|
203
|
+
"requiredTypes": {
|
|
204
|
+
"type": "array",
|
|
205
|
+
"minItems": 1,
|
|
206
|
+
"maxItems": 8,
|
|
207
|
+
"uniqueItems": true,
|
|
208
|
+
"items": {
|
|
209
|
+
"$ref": "#/$defs/evidenceType"
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
"requireHumanBaseline": {
|
|
213
|
+
"type": "boolean"
|
|
214
|
+
},
|
|
215
|
+
"requireHumanFinal": {
|
|
216
|
+
"type": "boolean",
|
|
217
|
+
"const": true
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
"review": {
|
|
222
|
+
"type": "object",
|
|
223
|
+
"additionalProperties": false,
|
|
224
|
+
"properties": {
|
|
225
|
+
"required": {
|
|
226
|
+
"type": "boolean"
|
|
227
|
+
},
|
|
228
|
+
"maxRounds": {
|
|
229
|
+
"type": "integer",
|
|
230
|
+
"minimum": 1,
|
|
231
|
+
"maximum": 10
|
|
232
|
+
},
|
|
233
|
+
"strictCoverage": {
|
|
234
|
+
"type": "boolean"
|
|
235
|
+
},
|
|
236
|
+
"reviewers": {
|
|
237
|
+
"type": "array",
|
|
238
|
+
"maxItems": 64,
|
|
239
|
+
"items": {
|
|
240
|
+
"type": "object",
|
|
241
|
+
"additionalProperties": false,
|
|
242
|
+
"required": [
|
|
243
|
+
"id",
|
|
244
|
+
"paths",
|
|
245
|
+
"required"
|
|
246
|
+
],
|
|
247
|
+
"properties": {
|
|
248
|
+
"id": {
|
|
249
|
+
"$ref": "#/$defs/id"
|
|
250
|
+
},
|
|
251
|
+
"paths": {
|
|
252
|
+
"type": "array",
|
|
253
|
+
"minItems": 1,
|
|
254
|
+
"maxItems": 128,
|
|
255
|
+
"uniqueItems": true,
|
|
256
|
+
"items": {
|
|
257
|
+
"type": "string",
|
|
258
|
+
"minLength": 1,
|
|
259
|
+
"maxLength": 512
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
"required": {
|
|
263
|
+
"type": "boolean"
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
33
268
|
}
|
|
34
269
|
}
|
|
35
270
|
},
|
|
36
271
|
"$defs": {
|
|
37
|
-
"id": {
|
|
38
|
-
|
|
272
|
+
"id": {
|
|
273
|
+
"type": "string",
|
|
274
|
+
"minLength": 1,
|
|
275
|
+
"maxLength": 64,
|
|
276
|
+
"pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
|
|
277
|
+
},
|
|
278
|
+
"evidenceType": {
|
|
279
|
+
"enum": [
|
|
280
|
+
"commit",
|
|
281
|
+
"test",
|
|
282
|
+
"journey",
|
|
283
|
+
"screenshot",
|
|
284
|
+
"report",
|
|
285
|
+
"review",
|
|
286
|
+
"human-approval",
|
|
287
|
+
"external-update"
|
|
288
|
+
]
|
|
289
|
+
},
|
|
39
290
|
"commandGate": {
|
|
40
291
|
"type": "object",
|
|
41
292
|
"additionalProperties": false,
|
|
42
|
-
"required": [
|
|
293
|
+
"required": [
|
|
294
|
+
"id",
|
|
295
|
+
"command",
|
|
296
|
+
"required"
|
|
297
|
+
],
|
|
43
298
|
"properties": {
|
|
44
|
-
"id": {
|
|
45
|
-
|
|
46
|
-
|
|
299
|
+
"id": {
|
|
300
|
+
"$ref": "#/$defs/id"
|
|
301
|
+
},
|
|
302
|
+
"command": {
|
|
303
|
+
"enum": [
|
|
304
|
+
"build",
|
|
305
|
+
"test",
|
|
306
|
+
"lint",
|
|
307
|
+
"typecheck",
|
|
308
|
+
"dev",
|
|
309
|
+
"check"
|
|
310
|
+
]
|
|
311
|
+
},
|
|
312
|
+
"required": {
|
|
313
|
+
"type": "boolean"
|
|
314
|
+
}
|
|
47
315
|
}
|
|
48
316
|
}
|
|
49
317
|
}
|
package/src/cli/main.js
CHANGED
|
@@ -115,6 +115,10 @@ Usage:
|
|
|
115
115
|
rivet task start [--project=<path>] [--run=<id>]
|
|
116
116
|
rivet task approve [--project=<path>] [--run=<id>]
|
|
117
117
|
rivet task resume [--project=<path>] [--run=<id>]
|
|
118
|
+
rivet task decisions [--project=<path>] [--run=<id>]
|
|
119
|
+
rivet task decide --input=<decision.json> [--project=<path>] [--run=<id>]
|
|
120
|
+
rivet task approve-decision --decision=<id> [--project=<path>] [--run=<id>]
|
|
121
|
+
rivet task review [--phase=plan|final] [--input=<report.json>] [--project=<path>] [--run=<id>]
|
|
118
122
|
rivet task deps [--project=<path>] [--run=<id>]
|
|
119
123
|
rivet protocols add <slug> [--from=<path>] [--project=<path>] [--json]
|
|
120
124
|
rivet protocols import <slug> --from=<path> [--project=<path>] [--json]
|
package/src/cli/parse-args.js
CHANGED
|
@@ -61,7 +61,7 @@ const STRICT_OPTIONS = {
|
|
|
61
61
|
},
|
|
62
62
|
task: {
|
|
63
63
|
boolean: new Set(['details']),
|
|
64
|
-
valued: new Set(['project', 'run']),
|
|
64
|
+
valued: new Set(['project', 'run', 'input', 'input-json', 'phase', 'decision']),
|
|
65
65
|
},
|
|
66
66
|
setup: {
|
|
67
67
|
boolean: new Set(['global', 'write', 'json']),
|
|
@@ -233,7 +233,7 @@ export function parseArgs(argv) {
|
|
|
233
233
|
if (command === 'work' && !['propose', 'prepare', 'next', 'status', 'submit', 'verify', 'recover'].includes(positionals[0])) {
|
|
234
234
|
throw new ArgumentError('Unsupported work subcommand');
|
|
235
235
|
}
|
|
236
|
-
if (command === 'task' && !['status', 'start', 'approve', 'resume', 'deps', 'recover'].includes(positionals[0])) {
|
|
236
|
+
if (command === 'task' && !['status', 'start', 'approve', 'resume', 'deps', 'recover', 'decisions', 'decide', 'approve-decision', 'review'].includes(positionals[0])) {
|
|
237
237
|
throw new ArgumentError('Unsupported task subcommand');
|
|
238
238
|
}
|
|
239
239
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { taskGovernanceCommand } from './task-governance.js';
|
|
1
2
|
import { taskState } from '../cli/task-presentation.js';
|
|
2
3
|
import { finishTask } from './task-completion.js';
|
|
3
4
|
import { resolveConfiguredProject } from '../cli/project-discovery.js';
|
|
@@ -172,13 +173,15 @@ function renderWorkerCheckouts(status, output) {
|
|
|
172
173
|
|
|
173
174
|
|
|
174
175
|
export async function humanTaskCommand(parsed, dependencies) {
|
|
175
|
-
if (parsed.command !== 'task' || !['status', 'start', 'approve', 'resume', 'deps', 'recover'].includes(parsed.subcommand)
|
|
176
|
-
|| parsed.operands.length !== 0 || Object.keys(parsed.flags).some(key => !['project', 'run', 'details'].includes(key))) {
|
|
176
|
+
if (parsed.command !== 'task' || !['status', 'start', 'approve', 'resume', 'deps', 'recover', 'decisions', 'decide', 'approve-decision', 'review'].includes(parsed.subcommand)
|
|
177
|
+
|| parsed.operands.length !== 0 || Object.keys(parsed.flags).some(key => !['project', 'run', 'details', 'input', 'input-json', 'phase', 'decision'].includes(key))) {
|
|
177
178
|
fail('Use rivet task status|start|approve|resume|deps|recover [--project=<path>] [--run=<id>].');
|
|
178
179
|
}
|
|
179
180
|
const project = await resolveConfiguredProject(dependencies.cwd(), parsed.flags.project, { env: dependencies.env });
|
|
180
181
|
const record = await selectedRun(project.root, parsed.flags.run, parsed.subcommand, dependencies);
|
|
181
182
|
if (!record) {dependencies.output.log('Task selection cancelled. No task action was taken.');return EXIT_CODES.SUCCESS;}
|
|
183
|
+
if (['decisions','decide','approve-decision','review'].includes(parsed.subcommand)) return taskGovernanceCommand(parsed,dependencies,project,record);
|
|
184
|
+
if (['input','input-json','phase','decision'].some(key=>parsed.flags[key]!==undefined)) fail('Evidence options require a decision or review command.');
|
|
182
185
|
if (parsed.subcommand === 'approve') return finishTask(project, record, dependencies);
|
|
183
186
|
if (parsed.subcommand === 'recover') {
|
|
184
187
|
if (record.featurePlan.client !== 'host' && !['awaiting-final-approval','completed'].includes(record.status)) fail('Lock recovery is only available for host tasks or terminal tasks at final review; it never restarts a worker.', 'REPOSITORY_CONFLICT');
|
|
@@ -203,6 +206,11 @@ export async function humanTaskCommand(parsed, dependencies) {
|
|
|
203
206
|
dependencies.output.log(`Worker harnesses: ${[...new Set(record.featurePlan.nodes.filter(node => node.role === 'worker').map(node => node.execution?.client ?? record.featurePlan.client))].join(', ')}`);
|
|
204
207
|
} else dependencies.output.log(`Harness: ${record.featurePlan.client}`);
|
|
205
208
|
dependencies.output.log(`Next: ${record.status === 'completed' ? 'Task completed. Inspect its recorded evidence and any separate delivery status.' : record.status === 'proposed' && record.featurePlan.client !== 'host' ? `rivet task start --run=${record.runId}` : record.status === 'awaiting-final-approval' && status.deliveryReady ? `rivet task approve --run=${record.runId}` : visible(status.nextAction)}`);
|
|
209
|
+
if (status.governance?.decisions.length) {
|
|
210
|
+
dependencies.output.log(`Decisions: ${status.governance.decisions.length}; use rivet task decisions to read the journal.`);
|
|
211
|
+
}
|
|
212
|
+
for (const blocker of status.governance?.blockers ?? []) dependencies.output.log(`Review: ${visible(blocker.code)}${blocker.reviewerId ? ` (${visible(blocker.reviewerId)})` : ''}`);
|
|
213
|
+
if (status.governance?.review.humanEscalation) dependencies.output.log('Review round limit reached. Ask a human to assess the findings and prepare a corrective task.');
|
|
206
214
|
if (status.verification) {
|
|
207
215
|
dependencies.output.log(`Verification: ${status.verification.status} at ${status.verification.commitSha}`);
|
|
208
216
|
for (const path of status.verification.changedPaths) dependencies.output.log(`Changed: ${visible(path)}`);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {withTerminalInterruption} from '../cli/interrupt.js';
|
|
2
|
+
import {resolve} from 'node:path';
|
|
3
|
+
import {createGitClient} from '../git/client.js';
|
|
4
|
+
import {taskGovernance} from '../feature/governance.js';
|
|
5
|
+
import {CliError,EXIT_CODES,observeOutputErrors} from '../cli/output.js';
|
|
6
|
+
import {readJson} from './work.js';
|
|
7
|
+
|
|
8
|
+
export async function taskGovernanceCommand(parsed,dependencies,project,run){
|
|
9
|
+
const {subcommand:operation,flags}=parsed;
|
|
10
|
+
const extra={decisions:[],decide:['input','input-json'],'approve-decision':['decision'],review:['input','input-json','phase']}[operation];
|
|
11
|
+
if(Object.keys(flags).some(key=>!['project','run',...extra].includes(key)))throw new CliError('Unexpected option for this task evidence command.','INVALID_INPUT');
|
|
12
|
+
if(operation==='decide'&&!flags.input&&!flags['input-json'])throw new CliError('Use rivet task decide --input=<decision.json>.','INVALID_INPUT');
|
|
13
|
+
if(operation==='approve-decision'&&!flags.decision)throw new CliError('Use rivet task approve-decision --decision=<id>.','INVALID_INPUT');
|
|
14
|
+
if(flags.phase!==undefined&&!['plan','final'].includes(flags.phase))throw new CliError('Review phase must be plan or final.','INVALID_INPUT');
|
|
15
|
+
if(flags.input!==undefined&&flags['input-json']!==undefined)throw new CliError('Choose one of --input or --input-json.','INVALID_INPUT');
|
|
16
|
+
let input;
|
|
17
|
+
if(flags['input-json']!==undefined){
|
|
18
|
+
const source=flags['input-json'];
|
|
19
|
+
if(typeof source!=='string'||!source.length||Buffer.byteLength(source)>64*1024)throw new CliError('Inline task evidence must be bounded JSON (up to 64 KiB).','INVALID_INPUT');
|
|
20
|
+
try{input=JSON.parse(source);}catch{throw new CliError('Task evidence is not valid JSON.','INVALID_INPUT');}
|
|
21
|
+
}
|
|
22
|
+
if(flags.input!==undefined)input=readJson(project.root,resolve(project.root,flags.input),dependencies.fs,'Task evidence');
|
|
23
|
+
let confirm,activeSignal;
|
|
24
|
+
if(operation==='approve-decision'){
|
|
25
|
+
if(dependencies.terminalIsInteractive?.()!==true)throw new CliError('Decision approval requires an interactive human terminal.','BLOCKED_AUTHORITY');
|
|
26
|
+
confirm=async record=>{
|
|
27
|
+
let failed=false;
|
|
28
|
+
const unobserve=observeOutputErrors(dependencies.output,()=>{failed=true;});
|
|
29
|
+
try{
|
|
30
|
+
activeSignal.throwIfAborted();
|
|
31
|
+
dependencies.output.log(JSON.stringify(record,null,2));
|
|
32
|
+
if(failed)throw new CliError('Could not display the decision. It remains pending.','REPOSITORY_CONFLICT');
|
|
33
|
+
const answer=await dependencies.taskApprovalPrompt({type:'select',signal:activeSignal,message:'Approve this exact decision? Existing execution and delivery permissions still apply.',choices:[{value:'approve',label:'Approve this decision'},{value:'leave',label:'Leave pending'}]});
|
|
34
|
+
activeSignal.throwIfAborted();
|
|
35
|
+
if(failed)throw new CliError('Decision output failed. It remains pending.','REPOSITORY_CONFLICT');
|
|
36
|
+
return answer==='approve';
|
|
37
|
+
}finally{unobserve?.();}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
try{
|
|
41
|
+
const gitClient=await createGitClient({gitExecutable:await dependencies.resolveCommandExecutable('git')});
|
|
42
|
+
const result=await withTerminalInterruption(async signal=>{
|
|
43
|
+
activeSignal=signal;
|
|
44
|
+
return taskGovernance({project:project.root,runId:run.runId,gitClient,
|
|
45
|
+
operation:operation==='review'&&input!==undefined?'review-submit':operation,input,phase:flags.phase,decisionId:flags.decision,confirm,signal});
|
|
46
|
+
});
|
|
47
|
+
dependencies.output.log(operation==='decisions'?result.markdown:JSON.stringify(result,null,2));
|
|
48
|
+
return EXIT_CODES.SUCCESS;
|
|
49
|
+
}catch(error){
|
|
50
|
+
if(error instanceof CliError)throw error;
|
|
51
|
+
throw new CliError(error.safeMessage??'Task decision or review operation failed. Inspect task status and the input report.','FAILED_GATE');
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/commands/work.js
CHANGED
|
@@ -40,7 +40,7 @@ function sameFile(left, right) {
|
|
|
40
40
|
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.nlink === right.nlink;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function readJson(project, path, fs, label) {
|
|
43
|
+
export function readJson(project, path, fs, label) {
|
|
44
44
|
const target = absolute(path, `${label} path`);
|
|
45
45
|
const contained = relative(project, target);
|
|
46
46
|
if (!contained || contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {
|
package/src/config/load.js
CHANGED
package/src/config/validate.js
CHANGED
|
@@ -79,7 +79,8 @@ function scanForSecrets(value, path = '') {
|
|
|
79
79
|
if (!value || typeof value !== 'object') return;
|
|
80
80
|
for (const [key, child] of Object.entries(value)) {
|
|
81
81
|
const childPath = `${path}/${key}`;
|
|
82
|
-
if (SECRET_KEY.test(key) && !SAFE_SECRET_KEYS.has(key)
|
|
82
|
+
if (SECRET_KEY.test(key) && !SAFE_SECRET_KEYS.has(key)
|
|
83
|
+
&& !(childPath === '/quality/contentScan/secrets' && typeof child === 'boolean')) fail(childPath, 'secret-key');
|
|
83
84
|
if (typeof child === 'string' && SECRET_VALUE.test(child)) fail(childPath, 'secret-value');
|
|
84
85
|
if (typeof child === 'string' && /^https?:\/\//i.test(child)) assertSafeUrl(child, childPath);
|
|
85
86
|
scanForSecrets(child, childPath);
|
package/src/delivery/contract.js
CHANGED
|
@@ -119,7 +119,7 @@ export function candidate(input) {
|
|
|
119
119
|
exact(
|
|
120
120
|
value,
|
|
121
121
|
['runId', 'repository', 'sourceBranch', 'targetBranch', 'localVerification'],
|
|
122
|
-
['reviewNumber']
|
|
122
|
+
['reviewNumber', 'decisionSummary']
|
|
123
123
|
);
|
|
124
124
|
id(value.runId);
|
|
125
125
|
ref(value.sourceBranch);
|
|
@@ -132,11 +132,22 @@ export function candidate(input) {
|
|
|
132
132
|
'evidenceDigest',
|
|
133
133
|
'verifiedAt',
|
|
134
134
|
'status',
|
|
135
|
-
]);
|
|
135
|
+
], ['governanceDigest', 'decisionSummaryDigest']);
|
|
136
136
|
ensure(proof.runId === value.runId && proof.status === 'passed');
|
|
137
137
|
sha(proof.headSha);
|
|
138
138
|
digest(proof.evidenceDigest);
|
|
139
139
|
timestamp(proof.verifiedAt);
|
|
140
|
+
if (proof.governanceDigest !== undefined) digest(proof.governanceDigest);
|
|
141
|
+
ensure((value.decisionSummary === undefined) === (proof.decisionSummaryDigest === undefined));
|
|
142
|
+
if (value.decisionSummary !== undefined) {
|
|
143
|
+
const summary = value.decisionSummary;
|
|
144
|
+
ensure(typeof summary === 'string' && summary.startsWith('## Decisions\n\n')
|
|
145
|
+
&& Buffer.byteLength(summary) <= 32000 && summary.length > 16
|
|
146
|
+
&& !/[\u0000-\u0009\u000b-\u001f\u007f<>[\]`@]/.test(summary), 'invalid-decision-summary');
|
|
147
|
+
digest(proof.decisionSummaryDigest);
|
|
148
|
+
ensure(proof.governanceDigest !== undefined && hash(summary) === proof.decisionSummaryDigest, 'decision-summary-mismatch');
|
|
149
|
+
}
|
|
150
|
+
|
|
140
151
|
ensure(
|
|
141
152
|
value.reviewNumber === undefined || (Number.isSafeInteger(value.reviewNumber) && value.reviewNumber > 0)
|
|
142
153
|
);
|
package/src/delivery/prepare.js
CHANGED
|
@@ -5,6 +5,9 @@ import { resolveExistingFeatureRunPaths } from '../state/paths.js';
|
|
|
5
5
|
import { loadProjectConfig } from '../config/load.js';
|
|
6
6
|
import { discoverRepositoryRemotes, selectConfiguredRepositoryRemote } from '../repositories/index.js';
|
|
7
7
|
import { immutableJson } from '../clients/contract.js';
|
|
8
|
+
import { inspectGovernance } from '../feature/governance.js';
|
|
9
|
+
import { renderDecisions } from '../feature/decisions.js';
|
|
10
|
+
import { hash } from './contract.js';
|
|
8
11
|
|
|
9
12
|
export class DeliveryPreparationError extends Error {
|
|
10
13
|
constructor(message = 'Delivery requires a verified run and unchanged integration checkout. Inspect work status and verification results.') {
|
|
@@ -18,6 +21,30 @@ function fail() {
|
|
|
18
21
|
throw new DeliveryPreparationError();
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
// Called only with application-observed governance, never a CLI-supplied summary.
|
|
25
|
+
export function deliveryGovernanceEvidence(governance, run, checkout) {
|
|
26
|
+
if (!governance || governance.ready !== true || !Array.isArray(governance.blockers) || governance.blockers.length
|
|
27
|
+
|| !Array.isArray(governance.decisions) || governance.decisions.some(value => value.status === 'pending')
|
|
28
|
+
|| governance.review?.valid !== true || !Array.isArray(governance.review.reports)
|
|
29
|
+
|| governance.review.blockers?.length || (governance.scan !== null && governance.scan?.status !== 'passed')) fail();
|
|
30
|
+
const bindings = { runId: run.runId, requestDigest: run.workRequest.digest, planDigest: run.proposalDigest };
|
|
31
|
+
if (governance.subject?.phase !== 'final' || governance.subject.headSha !== checkout.acceptedCommit
|
|
32
|
+
|| governance.subject.baseSha !== run.featurePlan.baselineCommit
|
|
33
|
+
|| Object.entries(bindings).some(([key, value]) => governance.subject[key] !== value)) fail();
|
|
34
|
+
const decisionSummary = governance.decisions.length ? renderDecisions(governance.decisions, {
|
|
35
|
+
...bindings, approvedPlanDecisions: governance.decisions.filter(value => value.status === 'approved').map(({ id, choice }) => ({ id, choice })),
|
|
36
|
+
}) : null;
|
|
37
|
+
if (decisionSummary !== null && Buffer.byteLength(decisionSummary) > 30000) {
|
|
38
|
+
throw new DeliveryPreparationError('Decision evidence exceeds the review body limit. Split the task before preparing delivery; evidence is never truncated.');
|
|
39
|
+
}
|
|
40
|
+
const hasEvidence = governance.decisions.length > 0 || governance.policy?.required === true
|
|
41
|
+
|| governance.review.reports.length > 0 || governance.scan !== null;
|
|
42
|
+
if (!hasEvidence) return Object.freeze({});
|
|
43
|
+
const governanceDigest = hash({ subject: governance.subject, policy: governance.policy, decisions: governance.decisions,
|
|
44
|
+
review: governance.review, scan: governance.scan });
|
|
45
|
+
return immutableJson({ governanceDigest, ...(decisionSummary === null ? {} : { decisionSummary, decisionSummaryDigest: hash(decisionSummary) }) });
|
|
46
|
+
}
|
|
47
|
+
|
|
21
48
|
// Only the application reads local verification. CLI inputs never stand in for evidence.
|
|
22
49
|
export async function loadDeliveryCandidate({ project, runId, remoteName, gitClient, runner }) {
|
|
23
50
|
const paths = await resolveExistingFeatureRunPaths(project, runId, runner ? { runner } : {});
|
|
@@ -28,6 +55,10 @@ export async function loadDeliveryCandidate({ project, runId, remoteName, gitCli
|
|
|
28
55
|
const observed = await execution.status({ project, runId });
|
|
29
56
|
const config = await loadProjectConfig(project);
|
|
30
57
|
const { run, verification, checkout } = observed;
|
|
58
|
+
const governance = observed.governance ?? await inspectGovernance({ project, run, gitClient, config, phase: 'final',
|
|
59
|
+
...(checkout?.acceptedCommit ? { checkout: { path: checkout.path, commitSha: checkout.acceptedCommit, branch: checkout.branch } } : {}) });
|
|
60
|
+
const governanceEvidence = deliveryGovernanceEvidence(governance, run, checkout ?? {});
|
|
61
|
+
const { decisionSummary, ...governanceProof } = governanceEvidence;
|
|
31
62
|
const source = await gitClient.inspectRepository(project);
|
|
32
63
|
if (
|
|
33
64
|
source.dirty ||
|
|
@@ -64,17 +95,20 @@ export async function loadDeliveryCandidate({ project, runId, remoteName, gitCli
|
|
|
64
95
|
return immutableJson({
|
|
65
96
|
runId,
|
|
66
97
|
repository,
|
|
98
|
+
...(decisionSummary === undefined ? {} : { decisionSummary }),
|
|
67
99
|
sourceBranch: checkout.branch,
|
|
68
100
|
targetBranch: config.project.repository.defaultBranch,
|
|
69
101
|
localVerification: {
|
|
70
102
|
runId,
|
|
71
103
|
status: 'passed',
|
|
104
|
+
...governanceProof,
|
|
72
105
|
headSha: checkout.acceptedCommit,
|
|
73
106
|
evidenceDigest: createHash('sha256')
|
|
74
107
|
.update(
|
|
75
108
|
JSON.stringify({
|
|
76
109
|
proposalDigest: run.proposalDigest,
|
|
77
110
|
verification,
|
|
111
|
+
...(governanceEvidence.governanceDigest ? { governanceDigest: governanceEvidence.governanceDigest } : {}),
|
|
78
112
|
})
|
|
79
113
|
)
|
|
80
114
|
.digest('hex'),
|
|
@@ -4,15 +4,16 @@ import { parseRepositoryRemote } from '../repositories/identity.js';
|
|
|
4
4
|
import { createTrustedDeliveryExecutor } from './service.js';
|
|
5
5
|
import { digest, ensure, exact, factsDigest, hash, plain, sha, timestamp, validateCandidate } from './contract.js';
|
|
6
6
|
|
|
7
|
+
const REVIEW_MARKER_BYTES = Buffer.byteLength('\n\n<!-- rivet-review-operation:' + '0'.repeat(64) + ' -->');
|
|
7
8
|
export function reviewRequestPayload(candidate) {
|
|
8
9
|
validateCandidate(candidate);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
});
|
|
10
|
+
const body = ['Rivet verified delivery candidate', '', `Run: ${candidate.runId}`,
|
|
11
|
+
`Source branch: ${candidate.sourceBranch}`, `Target branch: ${candidate.targetBranch}`,
|
|
12
|
+
`Verified commit: ${candidate.headSha}`, `Verification evidence: ${candidate.localVerification.evidenceDigest}`,
|
|
13
|
+
...(candidate.decisionSummary === undefined ? [] : ['', candidate.decisionSummary]),
|
|
14
|
+
'', 'Review the changes and repository checks before making a separate merge decision.'].join('\n');
|
|
15
|
+
ensure(Buffer.byteLength(body) + REVIEW_MARKER_BYTES <= 32000, 'review-content-too-large');
|
|
16
|
+
return plain({ title: `Rivet: ${candidate.runId}`, body });
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function reviewRequestContent(operation) {
|
|
@@ -20,9 +21,12 @@ export function reviewRequestContent(operation) {
|
|
|
20
21
|
digest(operation.digest);
|
|
21
22
|
const payload = plain(operation.payload);
|
|
22
23
|
exact(payload, ['title', 'body']);
|
|
24
|
+
if (operation.candidate?.decisionSummary !== undefined) {
|
|
25
|
+
ensure(hash(payload) === hash(reviewRequestPayload(operation.candidate)), 'decision-summary-mismatch');
|
|
26
|
+
}
|
|
23
27
|
ensure(typeof payload.title === 'string' && payload.title.trim() === payload.title && payload.title.length >= 1
|
|
24
28
|
&& payload.title.length <= 256 && !/[\u0000-\u001f\u007f]/.test(payload.title));
|
|
25
|
-
ensure(typeof payload.body === 'string' && payload.body.length >= 1 && payload.body
|
|
29
|
+
ensure(typeof payload.body === 'string' && payload.body.length >= 1 && Buffer.byteLength(payload.body) + REVIEW_MARKER_BYTES <= 32000
|
|
26
30
|
&& !/[\u0000-\u0008\u000b-\u001f\u007f]/.test(payload.body)
|
|
27
31
|
&& !payload.body.includes('rivet-review-operation:'));
|
|
28
32
|
return plain({ title: payload.title, body: `${payload.body}\n\n<!-- rivet-review-operation:${operation.digest} -->` });
|