@exaudeus/workrail 3.47.0 → 3.49.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/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/worktrain-trigger-test.d.ts +21 -0
- package/dist/cli/commands/worktrain-trigger-test.js +123 -0
- package/dist/cli-worktrain.js +65 -0
- package/dist/console-ui/assets/{index-B77l3WBR.js → index-C8xHtRdz.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/daemon/workflow-runner.d.ts +4 -0
- package/dist/daemon/workflow-runner.js +2 -13
- package/dist/manifest.json +32 -24
- package/dist/mcp/output-schemas.d.ts +2 -2
- package/dist/trigger/delivery-action.d.ts +17 -1
- package/dist/trigger/delivery-action.js +148 -4
- package/dist/trigger/trigger-router.js +3 -0
- package/dist/trigger/trigger-store.js +7 -0
- package/dist/trigger/types.d.ts +1 -0
- package/docs/ideas/backlog.md +73 -0
- package/package.json +1 -1
package/dist/manifest.json
CHANGED
|
@@ -238,8 +238,8 @@
|
|
|
238
238
|
"bytes": 31
|
|
239
239
|
},
|
|
240
240
|
"cli-worktrain.js": {
|
|
241
|
-
"sha256": "
|
|
242
|
-
"bytes":
|
|
241
|
+
"sha256": "4f68c32b88c84d71d7a28b4ffd818d2612e5a8c61c9f59edd4415772dfd5b9ab",
|
|
242
|
+
"bytes": 50057
|
|
243
243
|
},
|
|
244
244
|
"cli.d.ts": {
|
|
245
245
|
"sha256": "43e818adf60173644896298637f47b01d5819b17eda46eaa32d0c7d64724d012",
|
|
@@ -258,12 +258,12 @@
|
|
|
258
258
|
"bytes": 745
|
|
259
259
|
},
|
|
260
260
|
"cli/commands/index.d.ts": {
|
|
261
|
-
"sha256": "
|
|
262
|
-
"bytes":
|
|
261
|
+
"sha256": "240dc5f7055f8f4d602d41c4c659c800f80207af084683b1bd0a587cf68bfac0",
|
|
262
|
+
"bytes": 2396
|
|
263
263
|
},
|
|
264
264
|
"cli/commands/index.js": {
|
|
265
|
-
"sha256": "
|
|
266
|
-
"bytes":
|
|
265
|
+
"sha256": "fee00ff3ceb1a416bc3a2cfffcabf90bd5caeb2a1c06408ec4b32acf449da577",
|
|
266
|
+
"bytes": 5490
|
|
267
267
|
},
|
|
268
268
|
"cli/commands/init.d.ts": {
|
|
269
269
|
"sha256": "b5f8b88a072c68509dab3938ba1d6b4a949ad32f8fc55e91c5039b8c77301c1b",
|
|
@@ -393,6 +393,14 @@
|
|
|
393
393
|
"sha256": "b0286fef461835a0b73070fd278e43af5f3a1fbebbe1c6de1fc39ace4075df8f",
|
|
394
394
|
"bytes": 1395
|
|
395
395
|
},
|
|
396
|
+
"cli/commands/worktrain-trigger-test.d.ts": {
|
|
397
|
+
"sha256": "3b85edacabf0657b208892f13b8fb540f794f47f18b5a1263562d3518f7fce43",
|
|
398
|
+
"bytes": 1357
|
|
399
|
+
},
|
|
400
|
+
"cli/commands/worktrain-trigger-test.js": {
|
|
401
|
+
"sha256": "d2153a2110e70cc169596c2357410dd947c4bb69bf6167b64f4bf5b16bd5b8ca",
|
|
402
|
+
"bytes": 6413
|
|
403
|
+
},
|
|
396
404
|
"cli/interpret-result.d.ts": {
|
|
397
405
|
"sha256": "255f04350df9c8cf8d5e65ed2fc11d41fa60a7b5ccc818e7728b1c081340a66a",
|
|
398
406
|
"bytes": 315
|
|
@@ -457,8 +465,8 @@
|
|
|
457
465
|
"sha256": "5fe866e54f796975dec5d8ba9983aefd86074db212d3fccd64eed04bc9f0b3da",
|
|
458
466
|
"bytes": 8011
|
|
459
467
|
},
|
|
460
|
-
"console-ui/assets/index-
|
|
461
|
-
"sha256": "
|
|
468
|
+
"console-ui/assets/index-C8xHtRdz.js": {
|
|
469
|
+
"sha256": "701034ab9759e062c9d153cc53fffcf884afda7abda0e78b8ce8ded7b4fec378",
|
|
462
470
|
"bytes": 760528
|
|
463
471
|
},
|
|
464
472
|
"console-ui/assets/index-DGj8EsFR.css": {
|
|
@@ -466,7 +474,7 @@
|
|
|
466
474
|
"bytes": 60631
|
|
467
475
|
},
|
|
468
476
|
"console-ui/index.html": {
|
|
469
|
-
"sha256": "
|
|
477
|
+
"sha256": "065c2c590747a8e525a2b48334c3427ea41a7f0cff9d9de5e1efd3137090f1e5",
|
|
470
478
|
"bytes": 417
|
|
471
479
|
},
|
|
472
480
|
"console/standalone-console.d.ts": {
|
|
@@ -614,12 +622,12 @@
|
|
|
614
622
|
"bytes": 1512
|
|
615
623
|
},
|
|
616
624
|
"daemon/workflow-runner.d.ts": {
|
|
617
|
-
"sha256": "
|
|
618
|
-
"bytes":
|
|
625
|
+
"sha256": "b85875c6f608d3694702f133d3f9776cb20d79a907aafe1299e8598ab4bc8739",
|
|
626
|
+
"bytes": 7147
|
|
619
627
|
},
|
|
620
628
|
"daemon/workflow-runner.js": {
|
|
621
|
-
"sha256": "
|
|
622
|
-
"bytes":
|
|
629
|
+
"sha256": "205daee642099a4ebac5c909d2afb05cadefb8ad053f6b8a0c7819d3067bffcf",
|
|
630
|
+
"bytes": 92628
|
|
623
631
|
},
|
|
624
632
|
"di/container.d.ts": {
|
|
625
633
|
"sha256": "003bb7fb7478d627524b9b1e76bd0a963a243794a687ff233b96dc0e33a06d9f",
|
|
@@ -1222,7 +1230,7 @@
|
|
|
1222
1230
|
"bytes": 7991
|
|
1223
1231
|
},
|
|
1224
1232
|
"mcp/output-schemas.d.ts": {
|
|
1225
|
-
"sha256": "
|
|
1233
|
+
"sha256": "e41c4f996a7de03d96e52f1812e36c2d839f69b2ba421ae1831f6ae266cf59c5",
|
|
1226
1234
|
"bytes": 93176
|
|
1227
1235
|
},
|
|
1228
1236
|
"mcp/output-schemas.js": {
|
|
@@ -1638,12 +1646,12 @@
|
|
|
1638
1646
|
"bytes": 5471
|
|
1639
1647
|
},
|
|
1640
1648
|
"trigger/delivery-action.d.ts": {
|
|
1641
|
-
"sha256": "
|
|
1642
|
-
"bytes":
|
|
1649
|
+
"sha256": "559e2b2645aa60528f73de351cd35ebf45c5b82f47797aa15ddd681319315d39",
|
|
1650
|
+
"bytes": 1759
|
|
1643
1651
|
},
|
|
1644
1652
|
"trigger/delivery-action.js": {
|
|
1645
|
-
"sha256": "
|
|
1646
|
-
"bytes":
|
|
1653
|
+
"sha256": "533ce91bfc12cd170dcda7840d369a37f50298c23eaccd515ee4ef11f5aad58e",
|
|
1654
|
+
"bytes": 15291
|
|
1647
1655
|
},
|
|
1648
1656
|
"trigger/delivery-client.d.ts": {
|
|
1649
1657
|
"sha256": "0cb2be24b854cb31e3d2fe7eeaba6032de7a9b2a5290c8bc886df94faf5306f7",
|
|
@@ -1706,20 +1714,20 @@
|
|
|
1706
1714
|
"bytes": 2671
|
|
1707
1715
|
},
|
|
1708
1716
|
"trigger/trigger-router.js": {
|
|
1709
|
-
"sha256": "
|
|
1710
|
-
"bytes":
|
|
1717
|
+
"sha256": "3365e0f0e1f9287ff4ab0cab93f448f993b78c4bc9c7edec4f3394d6e0540baa",
|
|
1718
|
+
"bytes": 19216
|
|
1711
1719
|
},
|
|
1712
1720
|
"trigger/trigger-store.d.ts": {
|
|
1713
1721
|
"sha256": "7afb05127d55bc3757a550dd15d4b797766b3fff29d1bfe76b303764b93322e7",
|
|
1714
1722
|
"bytes": 1588
|
|
1715
1723
|
},
|
|
1716
1724
|
"trigger/trigger-store.js": {
|
|
1717
|
-
"sha256": "
|
|
1718
|
-
"bytes":
|
|
1725
|
+
"sha256": "7d1a61e6f0e01fd256f128f1fc223c0846eb020a87123bd03d2c31189f65c87b",
|
|
1726
|
+
"bytes": 38830
|
|
1719
1727
|
},
|
|
1720
1728
|
"trigger/types.d.ts": {
|
|
1721
|
-
"sha256": "
|
|
1722
|
-
"bytes":
|
|
1729
|
+
"sha256": "dc80ac05c031f24d5916bf95319dbe73262e1ada5aa5f83bedbfe6851188f8b1",
|
|
1730
|
+
"bytes": 3689
|
|
1723
1731
|
},
|
|
1724
1732
|
"trigger/types.js": {
|
|
1725
1733
|
"sha256": "45b4e4f23a6d1a2b07350196871b0c53840e5d8142b47f7acedd2f40ae7a6b73",
|
|
@@ -2547,14 +2547,14 @@ export declare const CreateSessionOutputSchema: z.ZodObject<{
|
|
|
2547
2547
|
path: string;
|
|
2548
2548
|
sessionId: string;
|
|
2549
2549
|
workflowId: string;
|
|
2550
|
-
dashboardUrl: string | null;
|
|
2551
2550
|
createdAt: string;
|
|
2551
|
+
dashboardUrl: string | null;
|
|
2552
2552
|
}, {
|
|
2553
2553
|
path: string;
|
|
2554
2554
|
sessionId: string;
|
|
2555
2555
|
workflowId: string;
|
|
2556
|
-
dashboardUrl: string | null;
|
|
2557
2556
|
createdAt: string;
|
|
2557
|
+
dashboardUrl: string | null;
|
|
2558
2558
|
}>;
|
|
2559
2559
|
export declare const UpdateSessionOutputSchema: z.ZodObject<{
|
|
2560
2560
|
updatedAt: z.ZodString;
|
|
@@ -11,8 +11,15 @@ export interface HandoffArtifact {
|
|
|
11
11
|
export interface DeliveryFlags {
|
|
12
12
|
readonly autoCommit?: boolean;
|
|
13
13
|
readonly autoOpenPR?: boolean;
|
|
14
|
+
readonly secretScan?: boolean;
|
|
14
15
|
readonly sessionId?: string;
|
|
15
16
|
readonly branchPrefix?: string;
|
|
17
|
+
readonly triggerId?: string;
|
|
18
|
+
readonly workflowId?: string;
|
|
19
|
+
readonly botIdentity?: {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly email: string;
|
|
22
|
+
};
|
|
16
23
|
}
|
|
17
24
|
export type DeliveryResult = {
|
|
18
25
|
readonly _tag: 'committed';
|
|
@@ -25,7 +32,7 @@ export type DeliveryResult = {
|
|
|
25
32
|
readonly reason: string;
|
|
26
33
|
} | {
|
|
27
34
|
readonly _tag: 'error';
|
|
28
|
-
readonly phase: 'parse' | 'commit' | 'pr';
|
|
35
|
+
readonly phase: 'parse' | 'secret_scan' | 'commit' | 'pr';
|
|
29
36
|
readonly details: string;
|
|
30
37
|
};
|
|
31
38
|
export type ExecFn = (file: string, args: string[], options: {
|
|
@@ -35,5 +42,14 @@ export type ExecFn = (file: string, args: string[], options: {
|
|
|
35
42
|
stdout: string;
|
|
36
43
|
stderr: string;
|
|
37
44
|
}>;
|
|
45
|
+
export interface SecretScanResult {
|
|
46
|
+
readonly found: boolean;
|
|
47
|
+
readonly findings: ReadonlyArray<{
|
|
48
|
+
readonly name: string;
|
|
49
|
+
readonly file: string;
|
|
50
|
+
readonly lineNumber: number;
|
|
51
|
+
}>;
|
|
52
|
+
}
|
|
53
|
+
export declare function scanForSecrets(diff: string): SecretScanResult;
|
|
38
54
|
export declare function parseHandoffArtifact(notes: string): Result<HandoffArtifact, string>;
|
|
39
55
|
export declare function runDelivery(artifact: HandoffArtifact, workspacePath: string, flags: DeliveryFlags, execFn: ExecFn): Promise<DeliveryResult>;
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.scanForSecrets = scanForSecrets;
|
|
36
37
|
exports.parseHandoffArtifact = parseHandoffArtifact;
|
|
37
38
|
exports.runDelivery = runDelivery;
|
|
38
39
|
const crypto = __importStar(require("node:crypto"));
|
|
@@ -41,6 +42,68 @@ const os = __importStar(require("node:os"));
|
|
|
41
42
|
const path = __importStar(require("node:path"));
|
|
42
43
|
const result_js_1 = require("../runtime/result.js");
|
|
43
44
|
const DELIVERY_TIMEOUT_MS = 60 * 1000;
|
|
45
|
+
const SECRET_PATTERNS = [
|
|
46
|
+
{ name: 'GitHub token', pattern: /ghp_[A-Za-z0-9]{36}/g },
|
|
47
|
+
{ name: 'GitHub OAuth token', pattern: /gho_[A-Za-z0-9]{36}/g },
|
|
48
|
+
{ name: 'GitHub app token', pattern: /ghs_[A-Za-z0-9]{36}/g },
|
|
49
|
+
{ name: 'OpenAI key', pattern: /sk-[A-Za-z0-9]{48}/g },
|
|
50
|
+
{ name: 'Anthropic key', pattern: /sk-ant-[A-Za-z0-9\-_]{90,}/g },
|
|
51
|
+
{ name: 'AWS access key', pattern: /AKIA[0-9A-Z]{16}/g },
|
|
52
|
+
{ name: 'AWS secret key', pattern: /[Aa][Ww][Ss][._-]?[Ss][Ee][Cc][Rr][Ee][Tt][._-]?[Kk][Ee][Yy]\s*[:=]\s*['"]?[A-Za-z0-9+\/]{40}/g },
|
|
53
|
+
{ name: 'Slack token', pattern: /xox[aboprs]-[A-Za-z0-9\-]+/g },
|
|
54
|
+
{ name: 'Private key', pattern: /-----BEGIN [A-Z]+ PRIVATE KEY-----/g },
|
|
55
|
+
{ name: 'Generic secret assign', pattern: /(?:password|passwd|secret|api[_-]?key|auth[_-]?token)\s*[:=]\s*['"][^'"]{8,}/gi },
|
|
56
|
+
];
|
|
57
|
+
function scanForSecrets(diff) {
|
|
58
|
+
if (!diff.trim()) {
|
|
59
|
+
return { found: false, findings: [] };
|
|
60
|
+
}
|
|
61
|
+
const findings = [];
|
|
62
|
+
const lines = diff.split('\n');
|
|
63
|
+
let currentFile = '(unknown)';
|
|
64
|
+
let currentLineNumber = 0;
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
if (line.startsWith('+++ ')) {
|
|
67
|
+
const filePath = line.slice(4);
|
|
68
|
+
currentFile = filePath.startsWith('b/') ? filePath.slice(2) : filePath;
|
|
69
|
+
currentLineNumber = 0;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (line.startsWith('@@')) {
|
|
73
|
+
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
74
|
+
if (hunkMatch?.[1] !== undefined) {
|
|
75
|
+
currentLineNumber = parseInt(hunkMatch[1], 10) - 1;
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (line.startsWith('--- ') || line.startsWith('diff ') || line.startsWith('index ') || line.startsWith('\\')) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (line.startsWith(' ')) {
|
|
83
|
+
currentLineNumber++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (line.startsWith('-')) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (line.startsWith('+')) {
|
|
90
|
+
currentLineNumber++;
|
|
91
|
+
const content = line.slice(1);
|
|
92
|
+
for (const { name, pattern } of SECRET_PATTERNS) {
|
|
93
|
+
pattern.lastIndex = 0;
|
|
94
|
+
if (pattern.test(content)) {
|
|
95
|
+
findings.push({ name, file: currentFile, lineNumber: currentLineNumber });
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
pattern.lastIndex = 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
found: findings.length > 0,
|
|
104
|
+
findings,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
44
107
|
function parseHandoffArtifact(notes) {
|
|
45
108
|
if (!notes || notes.trim() === '') {
|
|
46
109
|
return (0, result_js_1.err)('notes is empty');
|
|
@@ -170,14 +233,70 @@ async function runDelivery(artifact, workspacePath, flags, execFn) {
|
|
|
170
233
|
};
|
|
171
234
|
}
|
|
172
235
|
}
|
|
173
|
-
const
|
|
236
|
+
const baseCommitMessage = artifact.commitSubject.startsWith(`${artifact.commitType}(`)
|
|
174
237
|
? artifact.commitSubject
|
|
175
238
|
: `${artifact.commitType}(${artifact.commitScope}): ${artifact.commitSubject}`;
|
|
239
|
+
const trailers = [
|
|
240
|
+
...(flags.sessionId ? [`Worktrain-Session: ${flags.sessionId}`] : []),
|
|
241
|
+
'Co-authored-by: WorkTrain <worktrain@noreply.local>',
|
|
242
|
+
].join('\n');
|
|
243
|
+
const commitMessage = `${baseCommitMessage}\n\n${trailers}`;
|
|
176
244
|
let commitStdout;
|
|
177
245
|
let commitStderr;
|
|
178
246
|
try {
|
|
179
247
|
await execFn('git', ['add', ...artifact.filesChanged], { cwd: workspacePath, timeout: DELIVERY_TIMEOUT_MS });
|
|
180
|
-
|
|
248
|
+
if (flags.secretScan !== false) {
|
|
249
|
+
let stagedDiff = '';
|
|
250
|
+
try {
|
|
251
|
+
const diffResult = await execFn('git', ['diff', '--cached'], { cwd: workspacePath, timeout: DELIVERY_TIMEOUT_MS });
|
|
252
|
+
stagedDiff = diffResult.stdout;
|
|
253
|
+
}
|
|
254
|
+
catch (e) {
|
|
255
|
+
return {
|
|
256
|
+
_tag: 'error',
|
|
257
|
+
phase: 'secret_scan',
|
|
258
|
+
details: `Failed to retrieve staged diff for secret scan: ${formatExecError(e)}\nDelivery aborted.`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const scanResult = scanForSecrets(stagedDiff);
|
|
262
|
+
if (scanResult.found) {
|
|
263
|
+
const findingLines = scanResult.findings
|
|
264
|
+
.map(f => ` - ${f.name} in ${f.file}:${f.lineNumber}`)
|
|
265
|
+
.join('\n');
|
|
266
|
+
return {
|
|
267
|
+
_tag: 'error',
|
|
268
|
+
phase: 'secret_scan',
|
|
269
|
+
details: `Secret scan detected potential secrets in staged files:\n${findingLines}\n` +
|
|
270
|
+
`Delivery aborted. Review and remove secrets before retrying.\n` +
|
|
271
|
+
`Set secretScan: false in your trigger config to bypass this check.`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
await execFn('gitleaks', ['detect', '--source', '.', '--staged', '--no-git'], { cwd: workspacePath, timeout: DELIVERY_TIMEOUT_MS });
|
|
276
|
+
}
|
|
277
|
+
catch (e) {
|
|
278
|
+
const execErr = e;
|
|
279
|
+
if (execErr.code === 'ENOENT') {
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
return {
|
|
283
|
+
_tag: 'error',
|
|
284
|
+
phase: 'secret_scan',
|
|
285
|
+
details: `gitleaks detected potential secrets in staged files.\n` +
|
|
286
|
+
`Delivery aborted. Review and remove secrets before retrying.\n` +
|
|
287
|
+
`Set secretScan: false in your trigger config to bypass this check.`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const commitArgs = flags.botIdentity
|
|
293
|
+
? [
|
|
294
|
+
'-c', `user.name=${flags.botIdentity.name}`,
|
|
295
|
+
'-c', `user.email=${flags.botIdentity.email}`,
|
|
296
|
+
'commit', '-m', commitMessage,
|
|
297
|
+
]
|
|
298
|
+
: ['commit', '-m', commitMessage];
|
|
299
|
+
const commitResult = await execFn('git', commitArgs, { cwd: workspacePath, timeout: DELIVERY_TIMEOUT_MS });
|
|
181
300
|
commitStdout = commitResult.stdout;
|
|
182
301
|
commitStderr = commitResult.stderr;
|
|
183
302
|
}
|
|
@@ -190,13 +309,24 @@ async function runDelivery(artifact, workspacePath, flags, execFn) {
|
|
|
190
309
|
if (flags.autoOpenPR !== true) {
|
|
191
310
|
return { _tag: 'committed', sha };
|
|
192
311
|
}
|
|
312
|
+
const prTitle = artifact.prTitle.startsWith('[WT] ')
|
|
313
|
+
? artifact.prTitle
|
|
314
|
+
: `[WT] ${artifact.prTitle}`;
|
|
315
|
+
const footerParts = ['---', '\u{1F916} **Automated by WorkTrain**'];
|
|
316
|
+
if (flags.sessionId)
|
|
317
|
+
footerParts.push(`Session: \`${flags.sessionId}\``);
|
|
318
|
+
if (flags.triggerId)
|
|
319
|
+
footerParts.push(`Trigger: \`${flags.triggerId}\``);
|
|
320
|
+
if (flags.workflowId)
|
|
321
|
+
footerParts.push(`Workflow: \`${flags.workflowId}\``);
|
|
322
|
+
const prBodyWithFooter = `${artifact.prBody}\n\n${footerParts.join(' | ')}`;
|
|
193
323
|
const tmpDir = os.tmpdir();
|
|
194
324
|
const tmpFile = path.join(tmpDir, `workrail-pr-body-${crypto.randomUUID()}.md`);
|
|
195
325
|
let prStdout;
|
|
196
326
|
try {
|
|
197
|
-
await fs.writeFile(tmpFile,
|
|
327
|
+
await fs.writeFile(tmpFile, prBodyWithFooter, 'utf8');
|
|
198
328
|
try {
|
|
199
|
-
const prResult = await execFn('gh', ['pr', 'create', '--title',
|
|
329
|
+
const prResult = await execFn('gh', ['pr', 'create', '--title', prTitle, '--body-file', tmpFile], { cwd: workspacePath, timeout: DELIVERY_TIMEOUT_MS });
|
|
200
330
|
prStdout = prResult.stdout;
|
|
201
331
|
}
|
|
202
332
|
catch (e) {
|
|
@@ -212,6 +342,20 @@ async function runDelivery(artifact, workspacePath, flags, execFn) {
|
|
|
212
342
|
});
|
|
213
343
|
}
|
|
214
344
|
const prUrl = prStdout.trim().split('\n').at(-1)?.trim() ?? '';
|
|
345
|
+
if (prUrl) {
|
|
346
|
+
try {
|
|
347
|
+
await execFn('gh', ['label', 'create', 'worktrain:generated', '--description', 'PR authored by WorkTrain', '--color', '0075ca'], { cwd: workspacePath, timeout: DELIVERY_TIMEOUT_MS });
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
await execFn('gh', ['pr', 'edit', prUrl, '--add-label', 'worktrain:generated'], { cwd: workspacePath, timeout: DELIVERY_TIMEOUT_MS });
|
|
353
|
+
}
|
|
354
|
+
catch (e) {
|
|
355
|
+
console.warn(`[runDelivery] WARNING: Failed to add worktrain:generated label to PR ${prUrl}: ` +
|
|
356
|
+
`${e instanceof Error ? e.message : String(e)}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
215
359
|
return { _tag: 'pr_opened', url: prUrl };
|
|
216
360
|
}
|
|
217
361
|
function formatExecError(e) {
|
|
@@ -141,6 +141,9 @@ async function maybeRunDelivery(triggerId, trigger, result, execFn) {
|
|
|
141
141
|
const deliveryResult = await (0, delivery_action_js_1.runDelivery)(parseResult.value, deliveryCwd, {
|
|
142
142
|
autoCommit: trigger.autoCommit,
|
|
143
143
|
autoOpenPR: trigger.autoOpenPR,
|
|
144
|
+
triggerId,
|
|
145
|
+
workflowId: trigger.workflowId,
|
|
146
|
+
...(result.botIdentity !== undefined ? { botIdentity: result.botIdentity } : {}),
|
|
144
147
|
...(trigger.branchStrategy === 'worktree' && result.sessionWorkspacePath
|
|
145
148
|
? {
|
|
146
149
|
sessionId: result.sessionId ?? '',
|
|
@@ -385,6 +385,9 @@ function setTriggerField(trigger, key, value) {
|
|
|
385
385
|
case 'autoOpenPR':
|
|
386
386
|
trigger.autoOpenPR = value;
|
|
387
387
|
break;
|
|
388
|
+
case 'secretScan':
|
|
389
|
+
trigger.secretScan = value;
|
|
390
|
+
break;
|
|
388
391
|
case 'workspaceName':
|
|
389
392
|
trigger.workspaceName = value;
|
|
390
393
|
break;
|
|
@@ -613,6 +616,9 @@ function validateAndResolveTrigger(raw, env, workspaces = {}) {
|
|
|
613
616
|
}
|
|
614
617
|
const autoCommit = raw.autoCommit?.trim().toLowerCase() === 'true';
|
|
615
618
|
const autoOpenPR = raw.autoOpenPR?.trim().toLowerCase() === 'true';
|
|
619
|
+
const secretScan = raw.secretScan?.trim()
|
|
620
|
+
? raw.secretScan.trim().toLowerCase() === 'true'
|
|
621
|
+
: undefined;
|
|
616
622
|
if (autoOpenPR && !autoCommit) {
|
|
617
623
|
console.warn(`[TriggerStore] Warning: trigger "${rawId}" has autoOpenPR: true but autoCommit is not true. ` +
|
|
618
624
|
`A PR requires a commit -- delivery will be skipped unless autoCommit is also set to true.`);
|
|
@@ -820,6 +826,7 @@ function validateAndResolveTrigger(raw, env, workspaces = {}) {
|
|
|
820
826
|
...(onComplete !== undefined ? { onComplete } : {}),
|
|
821
827
|
...(autoCommit ? { autoCommit } : {}),
|
|
822
828
|
...(autoOpenPR ? { autoOpenPR } : {}),
|
|
829
|
+
...(secretScan !== undefined ? { secretScan } : {}),
|
|
823
830
|
...(pollingSource !== undefined ? { pollingSource } : {}),
|
|
824
831
|
...(resolvedWorkspaceName !== undefined ? { workspaceName: resolvedWorkspaceName } : {}),
|
|
825
832
|
...(resolvedSoulFile ? { soulFile: resolvedSoulFile } : {}),
|
package/dist/trigger/types.d.ts
CHANGED
|
@@ -79,6 +79,7 @@ export interface TriggerDefinition {
|
|
|
79
79
|
readonly concurrencyMode: 'serial' | 'parallel';
|
|
80
80
|
readonly callbackUrl?: string;
|
|
81
81
|
readonly autoCommit?: boolean;
|
|
82
|
+
readonly secretScan?: boolean;
|
|
82
83
|
readonly autoOpenPR?: boolean;
|
|
83
84
|
readonly onComplete?: {
|
|
84
85
|
readonly runOn: 'success' | 'failure' | 'always';
|
package/docs/ideas/backlog.md
CHANGED
|
@@ -6555,3 +6555,76 @@ Proposed `jira_poll` config:
|
|
|
6555
6555
|
### Priority
|
|
6556
6556
|
|
|
6557
6557
|
Medium. GitLab MR review already works. Jira issue queue is the next most impactful integration for enterprise users. Design alongside the label-based GitHub queue -- the patterns are identical, just different API shapes.
|
|
6558
|
+
|
|
6559
|
+
---
|
|
6560
|
+
|
|
6561
|
+
## Queue opt-in design: unresolved decisions (Apr 20, 2026)
|
|
6562
|
+
|
|
6563
|
+
**Status: DO NOT IMPLEMENT until these questions are answered.**
|
|
6564
|
+
|
|
6565
|
+
The self-improvement queue was partially implemented using label-based opt-in, then later walked back. This section records what's actually unresolved so future work starts from the right place.
|
|
6566
|
+
|
|
6567
|
+
### What's wrong with the current state
|
|
6568
|
+
|
|
6569
|
+
The `github_queue_poll` trigger now supports both `assignee` and `label` queue types. The code is correct. But `triggers.yml` has no active queue trigger because the opt-in mechanism isn't settled -- see below.
|
|
6570
|
+
|
|
6571
|
+
The label approach was implemented as a practical fallback when "no bot account" ruled out assignee-based. But labels were what we explicitly rejected in the original design because they require humans to apply them per issue. Reversing that decision without acknowledging it was a mistake. The right answer isn't to pick one mechanism -- it's to keep the queue shape configurable (which we already designed) and pick the right shape per context.
|
|
6572
|
+
|
|
6573
|
+
### The configurable queue shape (already designed, partially implemented)
|
|
6574
|
+
|
|
6575
|
+
```
|
|
6576
|
+
{ "queue": { "type": "github_assignee", "user": "worktrain-etienneb" } }
|
|
6577
|
+
{ "queue": { "type": "github_label", "name": "worktrain:ready" } }
|
|
6578
|
+
{ "queue": { "type": "github_query", "search": "is:issue is:open ..." } }
|
|
6579
|
+
{ "queue": { "type": "jql", "query": "assignee=currentUser() AND status='Ready for Dev'" } }
|
|
6580
|
+
{ "queue": { "type": "gitlab_label", "name": "worktrain" } }
|
|
6581
|
+
```
|
|
6582
|
+
|
|
6583
|
+
For the workrail repo specifically: either `github_assignee` (accept the conflation between your personal assignments and WorkTrain's queue -- fine for a solo repo) or `github_label` (apply label per issue -- more discipline, more friction). Neither is wrong; pick based on preference.
|
|
6584
|
+
|
|
6585
|
+
### Enterprise implications that must be resolved before Zillow work
|
|
6586
|
+
|
|
6587
|
+
Three questions for the user to verify before designing any Zillow path:
|
|
6588
|
+
|
|
6589
|
+
1. **Service account process**: Does Zillow have a ServiceDesk or security review process for requesting service accounts (`worktrain-etienneb@zillow`)? If yes, request one through proper channels rather than acting under your personal identity.
|
|
6590
|
+
|
|
6591
|
+
2. **AUP check**: Does Zillow's Acceptable Use Policy permit automation acting under employee identities without an explicit security review? If not, "WorkTrain acts as you" is not viable -- a service account is required.
|
|
6592
|
+
|
|
6593
|
+
3. **Self-approval rules**: Can you approve your own MRs in Zillow's GitLab? If "no self-approval" is enforced, every WorkTrain MR needs a human reviewer. That changes the pipeline (no auto-merge under personal identity).
|
|
6594
|
+
|
|
6595
|
+
These three answers determine the entire architecture for Zillow. Do not design the Jira/GitLab path until they are known.
|
|
6596
|
+
|
|
6597
|
+
### Enterprise identity risk (important)
|
|
6598
|
+
|
|
6599
|
+
"WorkTrain acts as you" is different from "Dependabot acts as you." Dependabot does narrow, predictable operations (dependency bumps). WorkTrain does arbitrary LLM-driven code changes. Every autonomous action -- MR opened, commit pushed, comment posted -- is attributed to you in audit logs. If WorkTrain does something wrong under your identity, the audit trail points to you. Understand this risk before turning on autonomy against company repos.
|
|
6600
|
+
|
|
6601
|
+
### Jira return path (missing from current jira_poll design)
|
|
6602
|
+
|
|
6603
|
+
The `jira_poll` backlog entry describes pulling tickets from Jira. It does not describe writing back:
|
|
6604
|
+
- Moving the ticket to "In Review" when an MR is opened
|
|
6605
|
+
- Adding the MR URL to the Jira ticket (a Jira field or comment)
|
|
6606
|
+
- Reacting to Jira transitions mid-work (ticket moved back to "To Do" → WorkTrain stops)
|
|
6607
|
+
|
|
6608
|
+
The full Jira integration is a round-trip, not just a poll. Design the return path before implementing `jira_poll`.
|
|
6609
|
+
|
|
6610
|
+
---
|
|
6611
|
+
|
|
6612
|
+
## Gate 2 follow-up: per-trigger gh CLI token for delivery (Apr 20, 2026)
|
|
6613
|
+
|
|
6614
|
+
`delivery-action.ts` calls `gh pr create` using whatever `gh` CLI auth is configured globally -- it does not pass a per-trigger token. For single-identity (always acting as yourself) this is fine. For multi-identity (Zillow service account alongside personal trigger), the globally authenticated `gh` user handles all PR creation, silently using the wrong identity.
|
|
6615
|
+
|
|
6616
|
+
**Fix when multi-identity is needed:** Pass `GH_TOKEN=<triggerToken>` env override to `execFn` when calling `gh pr create` and `gh pr merge`. Not a blocker for single-identity. Prerequisite for multi-identity support.
|
|
6617
|
+
|
|
6618
|
+
---
|
|
6619
|
+
|
|
6620
|
+
## Queue config discriminated union tightening (Apr 20, 2026)
|
|
6621
|
+
|
|
6622
|
+
`GitHubQueueConfig` uses a flat interface with runtime validation. Should be a proper TypeScript discriminated union so `type: 'assignee'` requires `user` at compile time. Low priority but tracked per "make illegal states unrepresentable."
|
|
6623
|
+
|
|
6624
|
+
---
|
|
6625
|
+
|
|
6626
|
+
## Kill switch and commit signing (Apr 20, 2026)
|
|
6627
|
+
|
|
6628
|
+
**Kill switch:** `worktrain kill-sessions` -- aborts all running daemon sessions immediately. Useful when WorkTrain is doing something unexpected. Sends abort signal to all active sessions, marks them user-killed in the event log.
|
|
6629
|
+
|
|
6630
|
+
**Commit signing:** verify `git commit` honors existing `commit.gpgsign` config, or add explicit opt-out for bot identities that don't have signing keys. Empirically verify before declaring this solved.
|