@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.
@@ -238,8 +238,8 @@
238
238
  "bytes": 31
239
239
  },
240
240
  "cli-worktrain.js": {
241
- "sha256": "75410de5c741ed86839f2834b69ff1dbf18b0284140a765a8a4ffd34257d19a3",
242
- "bytes": 46706
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": "7284fb3ca25bdbe6079fee4ed7f5eee9a82fff5fbc4e056010b52f4261145880",
262
- "bytes": 2251
261
+ "sha256": "240dc5f7055f8f4d602d41c4c659c800f80207af084683b1bd0a587cf68bfac0",
262
+ "bytes": 2396
263
263
  },
264
264
  "cli/commands/index.js": {
265
- "sha256": "8eef74385e22afc6313b4f7022c2e0a6be267ab55d7aa11c9a83b01a5e7c0e61",
266
- "bytes": 5186
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-B77l3WBR.js": {
461
- "sha256": "0c8b1d161e8b0cfc37d5b358b8753b3d6fa00e844bbca6ba56f7969afcc95606",
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": "93463e0e8b5f6e0fd0e5b6c5807f4ac673abfa00f35b10dea9a3b7ea0792e178",
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": "4c67cc7a44c934469c190f11a71bd18bf0dfc31f59ab0c315b98315b96d59cce",
618
- "bytes": 7048
625
+ "sha256": "b85875c6f608d3694702f133d3f9776cb20d79a907aafe1299e8598ab4bc8739",
626
+ "bytes": 7147
619
627
  },
620
628
  "daemon/workflow-runner.js": {
621
- "sha256": "a5d74ec723ff0dce45d2335b811959ff0c6e6f8851edf399a70853f6bf127893",
622
- "bytes": 93222
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": "bdc67c2adadeeca632aae3a3f0df8aa521c952194300b0bd823bdd7abed805d2",
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": "2b3f165759b0de49b7f49023a05efa50848331ab6cd9969b49c1409346959994",
1642
- "bytes": 1257
1649
+ "sha256": "559e2b2645aa60528f73de351cd35ebf45c5b82f47797aa15ddd681319315d39",
1650
+ "bytes": 1759
1643
1651
  },
1644
1652
  "trigger/delivery-action.js": {
1645
- "sha256": "1a9c0d097dc0f14e66765366f878f5f8386a4a1b0c5eb9572fa90a2b60643bab",
1646
- "bytes": 9016
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": "1f84935ad94e36be5befbef34d10deb7467c708e6d06cd36903843806f4b49a6",
1710
- "bytes": 19067
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": "8e85e0bdbd1596e70c23667e84ed380d6f0cee10028cd1a41163cda3467c2bdc",
1718
- "bytes": 38559
1725
+ "sha256": "7d1a61e6f0e01fd256f128f1fc223c0846eb020a87123bd03d2c31189f65c87b",
1726
+ "bytes": 38830
1719
1727
  },
1720
1728
  "trigger/types.d.ts": {
1721
- "sha256": "611f047631d8334a966acb7d1a71f5aa0d8cda65da127e07081b363290bcfdc2",
1722
- "bytes": 3654
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 commitMessage = artifact.commitSubject.startsWith(`${artifact.commitType}(`)
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
- const commitResult = await execFn('git', ['commit', '-m', commitMessage], { cwd: workspacePath, timeout: DELIVERY_TIMEOUT_MS });
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, artifact.prBody, 'utf8');
327
+ await fs.writeFile(tmpFile, prBodyWithFooter, 'utf8');
198
328
  try {
199
- const prResult = await execFn('gh', ['pr', 'create', '--title', artifact.prTitle, '--body-file', tmpFile], { cwd: workspacePath, timeout: DELIVERY_TIMEOUT_MS });
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 } : {}),
@@ -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';
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exaudeus/workrail",
3
- "version": "3.47.0",
3
+ "version": "3.49.0",
4
4
  "description": "Step-by-step workflow enforcement for AI agents via MCP",
5
5
  "license": "MIT",
6
6
  "repository": {