@metasession.co/devaudit-cli 0.3.9 → 0.3.11

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.
@@ -1,25 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
+ const { spawnSync } = require('child_process');
4
5
 
5
6
  const args = process.argv.slice(2);
6
- let phase = null;
7
- let viewOnly = args.includes('--view');
8
-
9
- const phaseArg = args.find(arg => arg.startsWith('--phase='));
10
- if (phaseArg) {
11
- phase = phaseArg.split('=')[1];
12
- } else {
13
- const phaseIndex = args.indexOf('--phase');
14
- if (phaseIndex !== -1 && args[phaseIndex + 1]) {
15
- phase = args[phaseIndex + 1];
16
- }
17
- }
18
-
19
- if (!phase) {
20
- console.error("❌ ERROR: Missing required configuration property. Execution syntax: devaudit-sdlc --phase=<1-5|issue>");
21
- process.exit(1);
22
- }
7
+ const sentinelPath = path.join(process.cwd(), '.sdlc-implementer-invoked');
8
+ const watchStatePath = path.join(process.cwd(), '.sdlc-pr-watch.json');
9
+ const DEFAULT_POLL_INTERVAL_SECONDS = 30;
10
+ const DEFAULT_MAX_POLLS = 40;
11
+ const DEFAULT_FLAKY_PATTERNS = [
12
+ 'playwright',
13
+ 'e2e',
14
+ 'regression',
15
+ 'smoke',
16
+ 'flaky',
17
+ 'retry',
18
+ ];
19
+ const RELEASE_APPROVAL_STATUSES = new Set([
20
+ 'uat_approved',
21
+ 'release_approved',
22
+ 'prod_review',
23
+ 'prod_approved',
24
+ 'released',
25
+ ]);
23
26
 
24
27
  const phaseMap = {
25
28
  '1': '1-plan-requirement.raw.md',
@@ -30,53 +33,115 @@ const phaseMap = {
30
33
  'issue': 'implementing-an-sdlc-issue.raw.md'
31
34
  };
32
35
 
33
- const targetBlueprint = phaseMap[phase];
34
- if (!targetBlueprint) {
35
- console.error(`❌ ERROR: Invalid phase argument [${phase}]. Supported options: 1, 2, 3, 4, 5, issue`);
36
+ function getOption(name) {
37
+ const inline = args.find(arg => arg.startsWith(`--${name}=`));
38
+ if (inline) return inline.split('=').slice(1).join('=');
39
+ const index = args.indexOf(`--${name}`);
40
+ if (index !== -1 && args[index + 1] && !args[index + 1].startsWith('--')) {
41
+ return args[index + 1];
42
+ }
43
+ return null;
44
+ }
45
+
46
+ function hasFlag(name) {
47
+ return args.includes(`--${name}`);
48
+ }
49
+
50
+ function printUsageAndExit() {
51
+ console.error("❌ ERROR: Missing required configuration property. Execution syntax: devaudit-sdlc --phase=<1-5|issue> | --watch-pr=<number>");
36
52
  process.exit(1);
37
53
  }
38
54
 
39
- const blueprintPath = path.join(__dirname, '..', 'blueprints', targetBlueprint);
40
- const sentinelPath = path.join(process.cwd(), '.sdlc-implementer-invoked');
55
+ function sleep(ms) {
56
+ return new Promise(resolve => setTimeout(resolve, ms));
57
+ }
41
58
 
42
- if (viewOnly) {
43
- if (!fs.existsSync(blueprintPath)) {
44
- console.error("❌ ERROR: Target operational asset blueprint could not be resolved.");
45
- process.exit(1);
59
+ function normalizeState(value) {
60
+ return String(value || '').trim().toLowerCase();
61
+ }
62
+
63
+ function isPendingState(value) {
64
+ const state = normalizeState(value);
65
+ return ['queued', 'in_progress', 'pending', 'waiting', 'requested', 'startup_failure', 'expected'].includes(state);
66
+ }
67
+
68
+ function isPassingState(value) {
69
+ const state = normalizeState(value);
70
+ return ['success', 'passed', 'pass', 'completed', 'neutral', 'skipped'].includes(state);
71
+ }
72
+
73
+ function isFailingState(value) {
74
+ const state = normalizeState(value);
75
+ return ['failure', 'failed', 'error', 'timed_out', 'cancelled', 'action_required'].includes(state);
76
+ }
77
+
78
+ function isReleaseApprovalState(value) {
79
+ return RELEASE_APPROVAL_STATUSES.has(normalizeState(value));
80
+ }
81
+
82
+ function isReleaseGateCheck(check) {
83
+ const haystack = `${check.name || ''} ${check.workflow || ''}`.toLowerCase();
84
+ return haystack.includes('release approval') || haystack.includes('uat approval');
85
+ }
86
+
87
+ function isFlakyCheck(check, patterns) {
88
+ const haystack = `${check.name || ''} ${check.workflow || ''}`.toLowerCase();
89
+ return patterns.some(pattern => haystack.includes(pattern));
90
+ }
91
+
92
+ function extractRunId(link) {
93
+ const match = String(link || '').match(/\/actions\/runs\/(\d+)/);
94
+ return match ? match[1] : null;
95
+ }
96
+
97
+ function formatCheckName(check) {
98
+ return check.workflow ? `${check.workflow} / ${check.name}` : String(check.name || 'unnamed-check');
99
+ }
100
+
101
+ function loadJsonFile(filePath, fallbackValue) {
102
+ if (!fs.existsSync(filePath)) return fallbackValue;
103
+ try {
104
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
105
+ } catch (_error) {
106
+ return fallbackValue;
46
107
  }
47
- const content = fs.readFileSync(blueprintPath, 'utf8');
48
- console.log(content);
49
- process.exit(0);
50
108
  }
51
109
 
52
- try {
53
- // Detect the invoking agent type (devaudit-installer#278):
54
- // - 'skill' when invoked via a skill mechanism (Claude Code Skill, etc.)
55
- // - 'native-agent' when invoked directly by the native agent (Cursor, Windsurf, etc.)
56
- // - 'cli' when invoked manually from the terminal
110
+ function saveJsonFile(filePath, value) {
111
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2), 'utf8');
112
+ }
113
+
114
+ function detectInvocationContext() {
57
115
  const invokedBySkill = !!(process.env.DEVAUDIT_SKILL_NAME || process.env.DEVAUDIT_SKILL_INVOKED);
58
116
  const invokedByAgent = !!(process.env.DEVAUDIT_AGENT || process.env.AI_AGENT_ID);
59
- const initializedBy = invokedBySkill ? 'skill' : invokedByAgent ? 'native-agent' : 'cli';
117
+ return {
118
+ invokedBySkill,
119
+ invokedByAgent,
120
+ initializedBy: invokedBySkill ? 'skill' : invokedByAgent ? 'native-agent' : 'cli',
121
+ agentType: invokedBySkill
122
+ ? (process.env.DEVAUDIT_SKILL_NAME || 'skill')
123
+ : (invokedByAgent ? (process.env.DEVAUDIT_AGENT || 'native-agent') : 'manual'),
124
+ };
125
+ }
60
126
 
61
- // Extract REQ ID from args or environment (devaudit-installer#278)
62
- let reqId = null;
63
- const reqArg = args.find(arg => arg.startsWith('--req='));
64
- if (reqArg) {
65
- reqId = reqArg.split('=')[1];
66
- } else if (process.env.DEVAUDIT_REQ_ID) {
67
- reqId = process.env.DEVAUDIT_REQ_ID;
68
- }
127
+ function resolveReqId() {
128
+ return getOption('req') || process.env.DEVAUDIT_REQ_ID || null;
129
+ }
130
+
131
+ function appendPhaseRecord(phase) {
132
+ const blueprintPath = path.join(__dirname, '..', 'blueprints', phaseMap[phase]);
133
+ const context = detectInvocationContext();
134
+ const reqId = resolveReqId();
69
135
 
70
136
  const newRecord = {
71
137
  activatedAt: new Date().toISOString(),
72
138
  currentPhase: phase,
73
- initializedBy: initializedBy,
139
+ initializedBy: context.initializedBy,
74
140
  status: 'active',
75
141
  reqId: reqId,
76
- agentType: invokedBySkill ? (process.env.DEVAUDIT_SKILL_NAME || 'skill') : (invokedByAgent ? (process.env.DEVAUDIT_AGENT || 'native-agent') : 'manual'),
142
+ agentType: context.agentType,
77
143
  };
78
144
 
79
- // Read existing sentinel and append, or create new array
80
145
  let phaseHistory = [];
81
146
  if (fs.existsSync(sentinelPath)) {
82
147
  try {
@@ -85,31 +150,402 @@ try {
85
150
  if (Array.isArray(parsed)) {
86
151
  phaseHistory = parsed;
87
152
  } else {
88
- // Legacy single-object format — migrate it into the array
89
153
  console.warn('⚠️ WARNING: Sentinel file contained a legacy single-object format. Migrating to array format.');
90
154
  phaseHistory = [parsed];
91
155
  }
92
- } catch (parseError) {
93
- // Corrupt or plain-text sentinel — start fresh
156
+ } catch (_parseError) {
94
157
  console.warn('⚠️ WARNING: Sentinel file was corrupt or in legacy plain-text format. Overwriting with fresh phase history.');
95
158
  phaseHistory = [];
96
159
  }
97
160
  }
98
161
 
99
162
  phaseHistory.push(newRecord);
100
- fs.writeFileSync(sentinelPath, JSON.stringify(phaseHistory, null, 2), 'utf8');
163
+ saveJsonFile(sentinelPath, phaseHistory);
101
164
 
102
165
  console.log(`\n✅ SDLC Gateway Initialized: Appended phase ${phase} record to sentinel at ${sentinelPath}`);
103
166
  console.log(`🚀 Phase ${phase} orchestration active. Your local git commit gates are now open.`);
104
167
  console.log(`📋 Phase history: ${phaseHistory.map(r => r.currentPhase).join(' → ')}`);
105
- console.log(`👤 Initialized by: ${initializedBy}${reqId ? ` (REQ-${reqId})` : ''}`);
168
+ console.log(`👤 Initialized by: ${context.initializedBy}${reqId ? ` (REQ-${reqId})` : ''}`);
106
169
 
107
170
  if (fs.existsSync(blueprintPath)) {
108
171
  console.log(`\n--- PHASE EXECUTION MANIFEST ---`);
109
172
  const content = fs.readFileSync(blueprintPath, 'utf8');
110
173
  console.log(content);
111
174
  }
112
- } catch (error) {
113
- console.error("❌ SYSTEM ERROR: Failed to instantiate workspace sentinel tracking state:", error.message);
114
- process.exit(1);
115
175
  }
176
+
177
+ function runBlueprintView(phase) {
178
+ const blueprintPath = path.join(__dirname, '..', 'blueprints', phaseMap[phase]);
179
+ if (!fs.existsSync(blueprintPath)) {
180
+ console.error("❌ ERROR: Target operational asset blueprint could not be resolved.");
181
+ process.exit(1);
182
+ }
183
+ console.log(fs.readFileSync(blueprintPath, 'utf8'));
184
+ }
185
+
186
+ function runGhJson(ghArgs) {
187
+ const ghCommand = process.env.DEVAUDIT_GH_BIN || 'gh';
188
+ const result = process.env.DEVAUDIT_GH_BIN
189
+ ? spawnSync(process.execPath, [ghCommand, ...ghArgs], {
190
+ cwd: process.cwd(),
191
+ encoding: 'utf8',
192
+ env: process.env,
193
+ })
194
+ : spawnSync(ghCommand, ghArgs, {
195
+ cwd: process.cwd(),
196
+ encoding: 'utf8',
197
+ env: process.env,
198
+ });
199
+ if (result.error) {
200
+ throw new Error(`gh ${ghArgs.join(' ')} failed: ${result.error.message}`);
201
+ }
202
+ if (result.status !== 0) {
203
+ throw new Error((result.stderr || result.stdout || `gh ${ghArgs.join(' ')} exited ${result.status}`).trim());
204
+ }
205
+ const stdout = (result.stdout || '').trim();
206
+ return stdout ? JSON.parse(stdout) : null;
207
+ }
208
+
209
+ function runGh(ghArgs) {
210
+ const ghCommand = process.env.DEVAUDIT_GH_BIN || 'gh';
211
+ const result = process.env.DEVAUDIT_GH_BIN
212
+ ? spawnSync(process.execPath, [ghCommand, ...ghArgs], {
213
+ cwd: process.cwd(),
214
+ encoding: 'utf8',
215
+ env: process.env,
216
+ })
217
+ : spawnSync(ghCommand, ghArgs, {
218
+ cwd: process.cwd(),
219
+ encoding: 'utf8',
220
+ env: process.env,
221
+ });
222
+ if (result.error) {
223
+ throw new Error(`gh ${ghArgs.join(' ')} failed: ${result.error.message}`);
224
+ }
225
+ if (result.status !== 0) {
226
+ throw new Error((result.stderr || result.stdout || `gh ${ghArgs.join(' ')} exited ${result.status}`).trim());
227
+ }
228
+ return result.stdout || '';
229
+ }
230
+
231
+ function resolveRepoFromGh() {
232
+ const repo = runGh(['repo', 'view', '--json', 'nameWithOwner']);
233
+ const parsed = JSON.parse(repo);
234
+ if (!parsed || !parsed.nameWithOwner) {
235
+ throw new Error('Unable to resolve current GitHub repository nameWithOwner.');
236
+ }
237
+ return parsed.nameWithOwner;
238
+ }
239
+
240
+ async function resolvePortalStatus(baseUrl, projectSlug, releaseVersion, apiKeyEnvName) {
241
+ if (!baseUrl || !projectSlug || !releaseVersion) return null;
242
+ const apiKey = process.env[apiKeyEnvName || 'DEVAUDIT_API_KEY'];
243
+ if (!apiKey) return null;
244
+
245
+ const url = `${baseUrl.replace(/\/$/, '')}/api/ci/releases/resolve?projectSlug=${encodeURIComponent(projectSlug)}&versionPrefix=${encodeURIComponent(releaseVersion)}`;
246
+ const controller = new AbortController();
247
+ const timeout = setTimeout(() => controller.abort(), 10000);
248
+ try {
249
+ const response = await fetch(url, {
250
+ headers: { Authorization: `Bearer ${apiKey}` },
251
+ signal: controller.signal,
252
+ });
253
+ if (!response.ok) return null;
254
+ const body = await response.json();
255
+ return normalizeState(body && body.latest && body.latest.status);
256
+ } catch (_error) {
257
+ return null;
258
+ } finally {
259
+ clearTimeout(timeout);
260
+ }
261
+ }
262
+
263
+ function loadWatchState() {
264
+ const parsed = loadJsonFile(watchStatePath, { watches: {} });
265
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
266
+ return { watches: {} };
267
+ }
268
+ if (!parsed.watches || typeof parsed.watches !== 'object' || Array.isArray(parsed.watches)) {
269
+ parsed.watches = {};
270
+ }
271
+ return parsed;
272
+ }
273
+
274
+ function getOrCreateWatch(state, key, seed) {
275
+ if (!state.watches[key]) {
276
+ state.watches[key] = {
277
+ prNumber: seed.prNumber,
278
+ repo: seed.repo,
279
+ reqId: seed.reqId || null,
280
+ releaseVersion: seed.releaseVersion || null,
281
+ projectSlug: seed.projectSlug || null,
282
+ createdAt: new Date().toISOString(),
283
+ updatedAt: new Date().toISOString(),
284
+ pollCount: 0,
285
+ reruns: {},
286
+ releaseGateReruns: 0,
287
+ lastClassification: null,
288
+ lastSummary: null,
289
+ lastPortalStatus: null,
290
+ history: [],
291
+ };
292
+ }
293
+ return state.watches[key];
294
+ }
295
+
296
+ function recordWatchHistory(watch, kind, summary, action) {
297
+ watch.updatedAt = new Date().toISOString();
298
+ watch.lastClassification = kind;
299
+ watch.lastSummary = summary;
300
+ watch.history = Array.isArray(watch.history) ? watch.history : [];
301
+ watch.history.push({
302
+ at: watch.updatedAt,
303
+ kind,
304
+ summary,
305
+ action: action || null,
306
+ });
307
+ if (watch.history.length > 25) {
308
+ watch.history = watch.history.slice(-25);
309
+ }
310
+ }
311
+
312
+ function classifyPrStatus(pr, checks, portalStatus, watch, options) {
313
+ const failingChecks = checks.filter(check => isFailingState(check.state || check.bucket));
314
+ const pendingChecks = checks.filter(check => isPendingState(check.state || check.bucket));
315
+ const releaseGate = checks.find(isReleaseGateCheck) || null;
316
+
317
+ if (normalizeState(pr.state) !== 'open') {
318
+ return {
319
+ kind: 'terminal',
320
+ exitCode: 0,
321
+ summary: `PR #${watch.prNumber} is ${normalizeState(pr.state) || 'not open'}. Watch complete.`,
322
+ };
323
+ }
324
+
325
+ if (pr.isDraft) {
326
+ return {
327
+ kind: 'waiting',
328
+ exitCode: 2,
329
+ summary: `PR #${watch.prNumber} is still a draft. Waiting for ready-for-review.`,
330
+ };
331
+ }
332
+
333
+ if (normalizeState(pr.reviewDecision) === 'changes_requested') {
334
+ return {
335
+ kind: 'blocked',
336
+ exitCode: 3,
337
+ summary: `PR #${watch.prNumber} has changes requested. Human action required before resume.`,
338
+ };
339
+ }
340
+
341
+ if (releaseGate && (isPendingState(releaseGate.state || releaseGate.bucket) || isFailingState(releaseGate.state || releaseGate.bucket))) {
342
+ if (portalStatus && isReleaseApprovalState(portalStatus)) {
343
+ const runId = extractRunId(releaseGate.link);
344
+ if (runId && watch.releaseGateReruns < 3) {
345
+ return {
346
+ kind: 'action',
347
+ exitCode: 4,
348
+ summary: `Release approval is already ${portalStatus} on the portal. Re-running ${formatCheckName(releaseGate)}.`,
349
+ action: { type: 'rerun-release-gate', runId, checkName: formatCheckName(releaseGate) },
350
+ };
351
+ }
352
+ return {
353
+ kind: 'blocked',
354
+ exitCode: 3,
355
+ summary: `Release approval is ${portalStatus} on the portal, but ${formatCheckName(releaseGate)} is still not green and cannot be auto-rerun further.`,
356
+ };
357
+ }
358
+ return {
359
+ kind: 'waiting',
360
+ exitCode: 2,
361
+ summary: `Release approval gate is not green yet. Portal status is ${portalStatus || 'unknown'}; waiting for review/approval on the portal.`,
362
+ };
363
+ }
364
+
365
+ for (const check of failingChecks) {
366
+ if (!isReleaseGateCheck(check) && isFlakyCheck(check, options.flakyPatterns || DEFAULT_FLAKY_PATTERNS)) {
367
+ const runId = extractRunId(check.link);
368
+ const checkKey = formatCheckName(check);
369
+ const priorReruns = Number((watch.reruns || {})[checkKey] || 0);
370
+ if (runId && priorReruns < 2) {
371
+ return {
372
+ kind: 'action',
373
+ exitCode: 4,
374
+ summary: `Detected likely flaky failure in ${checkKey}. Re-running workflow.`,
375
+ action: { type: 'rerun-flaky', runId, checkName: checkKey },
376
+ };
377
+ }
378
+ }
379
+ }
380
+
381
+ if (failingChecks.length > 0) {
382
+ return {
383
+ kind: 'blocked',
384
+ exitCode: 3,
385
+ summary: `PR #${watch.prNumber} has failing checks that need code fixes: ${failingChecks.map(formatCheckName).join(', ')}.`,
386
+ };
387
+ }
388
+
389
+ if (pendingChecks.length > 0) {
390
+ return {
391
+ kind: 'waiting',
392
+ exitCode: 2,
393
+ summary: `PR #${watch.prNumber} still has pending checks: ${pendingChecks.map(formatCheckName).join(', ')}.`,
394
+ };
395
+ }
396
+
397
+ if (normalizeState(pr.reviewDecision) !== 'approved') {
398
+ return {
399
+ kind: 'blocked',
400
+ exitCode: 3,
401
+ summary: `All checks are green, but PR #${watch.prNumber} still needs human review/approval.`,
402
+ };
403
+ }
404
+
405
+ return {
406
+ kind: 'ready',
407
+ exitCode: 0,
408
+ summary: `PR #${watch.prNumber} is approved and all observed checks are green. Ready for merge.`,
409
+ };
410
+ }
411
+
412
+ function performWatchAction(repo, watch, decision) {
413
+ if (!decision.action) return;
414
+ runGh(['run', 'rerun', decision.action.runId, '--repo', repo]);
415
+ if (decision.action.type === 'rerun-release-gate') {
416
+ watch.releaseGateReruns = Number(watch.releaseGateReruns || 0) + 1;
417
+ } else {
418
+ watch.reruns = watch.reruns || {};
419
+ watch.reruns[decision.action.checkName] = Number(watch.reruns[decision.action.checkName] || 0) + 1;
420
+ }
421
+ }
422
+
423
+ async function watchPullRequest() {
424
+ const prNumber = getOption('watch-pr');
425
+ if (!prNumber) printUsageAndExit();
426
+
427
+ const repo = getOption('repo') || resolveRepoFromGh();
428
+ const reqId = resolveReqId();
429
+ const releaseVersion = getOption('release') || (reqId ? `REQ-${reqId}` : null);
430
+ const baseUrl = getOption('base-url') || process.env.DEVAUDIT_BASE_URL || null;
431
+ const projectSlug = getOption('project-slug') || process.env.DEVAUDIT_PROJECT_SLUG || null;
432
+ const apiKeyEnvName = getOption('api-key-env') || 'DEVAUDIT_API_KEY';
433
+ const pollIntervalSeconds = Number.parseInt(getOption('poll-interval-seconds') || String(DEFAULT_POLL_INTERVAL_SECONDS), 10);
434
+ const maxPolls = Number.parseInt(getOption('max-polls') || String(DEFAULT_MAX_POLLS), 10);
435
+ const once = hasFlag('once');
436
+ const flakyPatterns = (getOption('flaky-patterns') || '')
437
+ .split(',')
438
+ .map(pattern => pattern.trim().toLowerCase())
439
+ .filter(Boolean);
440
+
441
+ const state = loadWatchState();
442
+ const watchKey = `${repo}#${prNumber}`;
443
+ const watch = getOrCreateWatch(state, watchKey, {
444
+ prNumber: Number(prNumber),
445
+ repo,
446
+ reqId,
447
+ releaseVersion,
448
+ projectSlug,
449
+ });
450
+ watch.repo = repo;
451
+ watch.reqId = reqId || watch.reqId || null;
452
+ watch.releaseVersion = releaseVersion || watch.releaseVersion || null;
453
+ watch.projectSlug = projectSlug || watch.projectSlug || null;
454
+
455
+ const options = {
456
+ flakyPatterns: flakyPatterns.length > 0 ? flakyPatterns : DEFAULT_FLAKY_PATTERNS,
457
+ };
458
+
459
+ let pollsRemaining = once ? 1 : maxPolls;
460
+ while (pollsRemaining > 0) {
461
+ watch.pollCount = Number(watch.pollCount || 0) + 1;
462
+
463
+ let pr;
464
+ let checks;
465
+ try {
466
+ pr = runGhJson(['pr', 'view', String(prNumber), '--repo', repo, '--json', 'state,isDraft,reviewDecision']);
467
+ checks = runGhJson(['pr', 'checks', String(prNumber), '--repo', repo, '--json', 'name,state,link,workflow,bucket']) || [];
468
+ if (!Array.isArray(checks)) checks = [];
469
+ } catch (error) {
470
+ recordWatchHistory(watch, 'error', `Failed to inspect PR state: ${error.message}`);
471
+ saveJsonFile(watchStatePath, state);
472
+ console.error(`❌ SYSTEM ERROR: ${error.message}`);
473
+ process.exit(1);
474
+ }
475
+
476
+ const portalStatus = await resolvePortalStatus(baseUrl, projectSlug || watch.projectSlug, releaseVersion || watch.releaseVersion, apiKeyEnvName);
477
+ watch.lastPortalStatus = portalStatus || watch.lastPortalStatus || null;
478
+
479
+ const decision = classifyPrStatus(pr || {}, checks, portalStatus, watch, options);
480
+ if (decision.kind === 'action') {
481
+ try {
482
+ performWatchAction(repo, watch, decision);
483
+ recordWatchHistory(watch, decision.kind, decision.summary, decision.action);
484
+ saveJsonFile(watchStatePath, state);
485
+ console.log(`🔁 ${decision.summary}`);
486
+ } catch (error) {
487
+ recordWatchHistory(watch, 'error', `Auto-rerun failed: ${error.message}`, decision.action);
488
+ saveJsonFile(watchStatePath, state);
489
+ console.error(`❌ SYSTEM ERROR: ${error.message}`);
490
+ process.exit(1);
491
+ }
492
+ if (once) {
493
+ process.exit(decision.exitCode);
494
+ }
495
+ } else {
496
+ recordWatchHistory(watch, decision.kind, decision.summary);
497
+ saveJsonFile(watchStatePath, state);
498
+ const prefix = decision.kind === 'ready' ? '✅' : decision.kind === 'blocked' ? '⛔' : '⏳';
499
+ console.log(`${prefix} ${decision.summary}`);
500
+ if (decision.kind === 'ready' || decision.kind === 'blocked' || decision.kind === 'terminal') {
501
+ process.exit(decision.exitCode);
502
+ }
503
+ if (once) {
504
+ process.exit(decision.exitCode);
505
+ }
506
+ }
507
+
508
+ pollsRemaining -= 1;
509
+ if (pollsRemaining <= 0) break;
510
+ await sleep(pollIntervalSeconds * 1000);
511
+ }
512
+
513
+ recordWatchHistory(watch, 'waiting', `Polling limit reached after ${watch.pollCount} observations. Re-run the watcher to continue monitoring.`);
514
+ saveJsonFile(watchStatePath, state);
515
+ console.log(`⏳ Polling limit reached after ${watch.pollCount} observations. Re-run the watcher to continue monitoring.`);
516
+ process.exit(2);
517
+ }
518
+
519
+ async function main() {
520
+ const phase = getOption('phase');
521
+ const viewOnly = hasFlag('view');
522
+ const watchPr = getOption('watch-pr');
523
+
524
+ if (!phase && !watchPr) {
525
+ printUsageAndExit();
526
+ }
527
+
528
+ if (watchPr) {
529
+ await watchPullRequest();
530
+ return;
531
+ }
532
+
533
+ if (!phaseMap[phase]) {
534
+ console.error(`❌ ERROR: Invalid phase argument [${phase}]. Supported options: 1, 2, 3, 4, 5, issue`);
535
+ process.exit(1);
536
+ }
537
+
538
+ if (viewOnly) {
539
+ runBlueprintView(phase);
540
+ return;
541
+ }
542
+
543
+ try {
544
+ appendPhaseRecord(phase);
545
+ } catch (error) {
546
+ console.error("❌ SYSTEM ERROR: Failed to instantiate workspace sentinel tracking state:", error.message);
547
+ process.exit(1);
548
+ }
549
+ }
550
+
551
+ main();