@jonit-dev/night-watch-cli 1.8.3 → 1.8.4-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/cli.js +776 -324
  2. package/dist/commands/doctor.d.ts.map +1 -1
  3. package/dist/commands/doctor.js +8 -12
  4. package/dist/commands/doctor.js.map +1 -1
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +101 -2
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/plan.d.ts +19 -0
  9. package/dist/commands/plan.d.ts.map +1 -0
  10. package/dist/commands/plan.js +88 -0
  11. package/dist/commands/plan.js.map +1 -0
  12. package/dist/commands/prd.d.ts +8 -14
  13. package/dist/commands/prd.d.ts.map +1 -1
  14. package/dist/commands/prd.js +238 -145
  15. package/dist/commands/prd.js.map +1 -1
  16. package/dist/scripts/night-watch-plan-cron.sh +130 -0
  17. package/dist/scripts/publish.sh +94 -26
  18. package/dist/templates/pr-reviewer.md +1 -1
  19. package/dist/templates/prd-creator.md +330 -0
  20. package/dist/templates/prd-executor.md +1 -0
  21. package/dist/templates/qa.md +8 -4
  22. package/dist/templates/skills/_codex-block.md +58 -0
  23. package/dist/templates/skills/nw-add-issue.md +39 -0
  24. package/dist/templates/skills/nw-board-sync.md +47 -0
  25. package/dist/templates/skills/nw-create-prd.md +61 -0
  26. package/dist/templates/skills/nw-review.md +39 -0
  27. package/dist/templates/skills/nw-run.md +39 -0
  28. package/dist/templates/skills/nw-slice.md +33 -0
  29. package/dist/templates/skills/skills/_codex-block.md +58 -0
  30. package/dist/templates/skills/skills/nw-add-issue.md +39 -0
  31. package/dist/templates/skills/skills/nw-board-sync.md +47 -0
  32. package/dist/templates/skills/skills/nw-create-prd.md +61 -0
  33. package/dist/templates/skills/skills/nw-review.md +39 -0
  34. package/dist/templates/skills/skills/nw-run.md +39 -0
  35. package/dist/templates/skills/skills/nw-slice.md +33 -0
  36. package/dist/web/assets/index-Cp7RYjoy.css +1 -0
  37. package/dist/web/assets/index-DTsfDC7m.js +381 -0
  38. package/dist/web/assets/index-ZABWMEZR.js +381 -0
  39. package/dist/web/index.html +2 -2
  40. package/package.json +12 -3
package/dist/cli.js CHANGED
@@ -17,6 +17,283 @@ var init_types = __esm({
17
17
  }
18
18
  });
19
19
 
20
+ // ../core/dist/jobs/job-registry.js
21
+ function getJobDef(id) {
22
+ return JOB_MAP.get(id);
23
+ }
24
+ function getAllJobDefs() {
25
+ return [...JOB_REGISTRY];
26
+ }
27
+ function getJobDefByCommand(command) {
28
+ return JOB_REGISTRY.find((job) => job.cliCommand === command);
29
+ }
30
+ function getJobDefByLogName(logName) {
31
+ return JOB_REGISTRY.find((job) => job.logName === logName);
32
+ }
33
+ function getValidJobTypes() {
34
+ return JOB_REGISTRY.map((job) => job.id);
35
+ }
36
+ function getDefaultQueuePriority() {
37
+ const result = {};
38
+ for (const job of JOB_REGISTRY) {
39
+ result[job.id] = job.queuePriority;
40
+ }
41
+ return result;
42
+ }
43
+ function getLogFileNames() {
44
+ const result = {};
45
+ for (const job of JOB_REGISTRY) {
46
+ result[job.id] = job.logName;
47
+ if (job.cliCommand !== job.id) {
48
+ result[job.cliCommand] = job.logName;
49
+ }
50
+ }
51
+ return result;
52
+ }
53
+ function getLockSuffix(jobId) {
54
+ return getJobDef(jobId)?.lockSuffix ?? ".lock";
55
+ }
56
+ function normalizeJobConfig(raw, jobDef) {
57
+ const readBoolean = (v) => typeof v === "boolean" ? v : void 0;
58
+ const readString = (v) => typeof v === "string" ? v : void 0;
59
+ const readNumber = (v) => typeof v === "number" && !Number.isNaN(v) ? v : void 0;
60
+ const readStringArray = (v) => Array.isArray(v) && v.every((s) => typeof s === "string") ? v : void 0;
61
+ const defaults = jobDef.defaultConfig;
62
+ const result = {
63
+ enabled: readBoolean(raw.enabled) ?? defaults.enabled,
64
+ schedule: readString(raw.schedule) ?? defaults.schedule,
65
+ maxRuntime: readNumber(raw.maxRuntime) ?? defaults.maxRuntime
66
+ };
67
+ for (const field of jobDef.extraFields ?? []) {
68
+ switch (field.type) {
69
+ case "boolean":
70
+ result[field.name] = readBoolean(raw[field.name]) ?? field.defaultValue;
71
+ break;
72
+ case "string":
73
+ result[field.name] = readString(raw[field.name]) ?? field.defaultValue;
74
+ break;
75
+ case "number":
76
+ result[field.name] = readNumber(raw[field.name]) ?? field.defaultValue;
77
+ break;
78
+ case "string[]":
79
+ result[field.name] = readStringArray(raw[field.name]) ?? field.defaultValue;
80
+ break;
81
+ case "enum": {
82
+ const val = readString(raw[field.name]);
83
+ result[field.name] = val && field.enumValues?.includes(val) ? val : field.defaultValue;
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ return result;
89
+ }
90
+ function camelToUpperSnake(name) {
91
+ return name.replace(/([A-Z])/g, "_$1").toUpperCase();
92
+ }
93
+ function buildJobEnvOverrides(envPrefix, currentBase, extraFields) {
94
+ const parseBoolean2 = (value) => {
95
+ const v = value.toLowerCase().trim();
96
+ if (v === "true" || v === "1")
97
+ return true;
98
+ if (v === "false" || v === "0")
99
+ return false;
100
+ return null;
101
+ };
102
+ const result = { ...currentBase };
103
+ let changed = false;
104
+ const enabledVal = process.env[`${envPrefix}_ENABLED`];
105
+ if (enabledVal) {
106
+ const v = parseBoolean2(enabledVal);
107
+ if (v !== null) {
108
+ result.enabled = v;
109
+ changed = true;
110
+ }
111
+ }
112
+ const scheduleVal = process.env[`${envPrefix}_SCHEDULE`];
113
+ if (scheduleVal) {
114
+ result.schedule = scheduleVal;
115
+ changed = true;
116
+ }
117
+ const maxRuntimeVal = process.env[`${envPrefix}_MAX_RUNTIME`];
118
+ if (maxRuntimeVal) {
119
+ const v = parseInt(maxRuntimeVal, 10);
120
+ if (!isNaN(v) && v > 0) {
121
+ result.maxRuntime = v;
122
+ changed = true;
123
+ }
124
+ }
125
+ for (const field of extraFields ?? []) {
126
+ const envKey = `${envPrefix}_${camelToUpperSnake(field.name)}`;
127
+ const envVal = process.env[envKey];
128
+ if (!envVal)
129
+ continue;
130
+ switch (field.type) {
131
+ case "boolean": {
132
+ const v = parseBoolean2(envVal);
133
+ if (v !== null) {
134
+ result[field.name] = v;
135
+ changed = true;
136
+ }
137
+ break;
138
+ }
139
+ case "string":
140
+ result[field.name] = envVal;
141
+ changed = true;
142
+ break;
143
+ case "number": {
144
+ const v = parseInt(envVal, 10);
145
+ if (!isNaN(v) && v > 0) {
146
+ result[field.name] = v;
147
+ changed = true;
148
+ }
149
+ break;
150
+ }
151
+ case "string[]": {
152
+ const patterns = envVal.split(",").map((s) => s.trim()).filter(Boolean);
153
+ if (patterns.length > 0) {
154
+ result[field.name] = patterns;
155
+ changed = true;
156
+ }
157
+ break;
158
+ }
159
+ case "enum":
160
+ if (field.enumValues?.includes(envVal)) {
161
+ result[field.name] = envVal;
162
+ changed = true;
163
+ }
164
+ break;
165
+ }
166
+ }
167
+ return changed ? result : null;
168
+ }
169
+ var JOB_REGISTRY, JOB_MAP;
170
+ var init_job_registry = __esm({
171
+ "../core/dist/jobs/job-registry.js"() {
172
+ "use strict";
173
+ JOB_REGISTRY = [
174
+ {
175
+ id: "executor",
176
+ name: "Executor",
177
+ description: "Creates implementation PRs from PRDs",
178
+ cliCommand: "run",
179
+ logName: "executor",
180
+ lockSuffix: ".lock",
181
+ queuePriority: 50,
182
+ envPrefix: "NW_EXECUTOR",
183
+ defaultConfig: {
184
+ enabled: true,
185
+ schedule: "5 */2 * * *",
186
+ maxRuntime: 7200
187
+ }
188
+ },
189
+ {
190
+ id: "reviewer",
191
+ name: "Reviewer",
192
+ description: "Reviews and improves PRs on night-watch branches",
193
+ cliCommand: "review",
194
+ logName: "reviewer",
195
+ lockSuffix: "-r.lock",
196
+ queuePriority: 40,
197
+ envPrefix: "NW_REVIEWER",
198
+ defaultConfig: {
199
+ enabled: true,
200
+ schedule: "25 */3 * * *",
201
+ maxRuntime: 3600
202
+ }
203
+ },
204
+ {
205
+ id: "slicer",
206
+ name: "Slicer",
207
+ description: "Generates PRDs from roadmap items",
208
+ cliCommand: "planner",
209
+ logName: "slicer",
210
+ lockSuffix: "-slicer.lock",
211
+ queuePriority: 30,
212
+ envPrefix: "NW_SLICER",
213
+ defaultConfig: {
214
+ enabled: true,
215
+ schedule: "35 */6 * * *",
216
+ maxRuntime: 600
217
+ }
218
+ },
219
+ {
220
+ id: "qa",
221
+ name: "QA",
222
+ description: "Runs end-to-end tests on PRs",
223
+ cliCommand: "qa",
224
+ logName: "night-watch-qa",
225
+ lockSuffix: "-qa.lock",
226
+ queuePriority: 20,
227
+ envPrefix: "NW_QA",
228
+ extraFields: [
229
+ { name: "branchPatterns", type: "string[]", defaultValue: [] },
230
+ {
231
+ name: "artifacts",
232
+ type: "enum",
233
+ enumValues: ["screenshot", "video", "both"],
234
+ defaultValue: "both"
235
+ },
236
+ { name: "skipLabel", type: "string", defaultValue: "skip-qa" },
237
+ { name: "autoInstallPlaywright", type: "boolean", defaultValue: true }
238
+ ],
239
+ defaultConfig: {
240
+ enabled: true,
241
+ schedule: "45 2,10,18 * * *",
242
+ maxRuntime: 3600,
243
+ branchPatterns: [],
244
+ artifacts: "both",
245
+ skipLabel: "skip-qa",
246
+ autoInstallPlaywright: true
247
+ }
248
+ },
249
+ {
250
+ id: "audit",
251
+ name: "Auditor",
252
+ description: "Performs code audits and creates issues for findings",
253
+ cliCommand: "audit",
254
+ logName: "audit",
255
+ lockSuffix: "-audit.lock",
256
+ queuePriority: 10,
257
+ envPrefix: "NW_AUDIT",
258
+ defaultConfig: {
259
+ enabled: true,
260
+ schedule: "50 3 * * 1",
261
+ maxRuntime: 1800
262
+ }
263
+ },
264
+ {
265
+ id: "analytics",
266
+ name: "Analytics",
267
+ description: "Analyzes product analytics and creates issues for trends",
268
+ cliCommand: "analytics",
269
+ logName: "analytics",
270
+ lockSuffix: "-analytics.lock",
271
+ queuePriority: 10,
272
+ envPrefix: "NW_ANALYTICS",
273
+ extraFields: [
274
+ { name: "lookbackDays", type: "number", defaultValue: 7 },
275
+ {
276
+ name: "targetColumn",
277
+ type: "enum",
278
+ enumValues: ["Draft", "Ready", "In Progress", "Done", "Closed"],
279
+ defaultValue: "Draft"
280
+ },
281
+ { name: "analysisPrompt", type: "string", defaultValue: "" }
282
+ ],
283
+ defaultConfig: {
284
+ enabled: false,
285
+ schedule: "0 6 * * 1",
286
+ maxRuntime: 900,
287
+ lookbackDays: 7,
288
+ targetColumn: "Draft",
289
+ analysisPrompt: ""
290
+ }
291
+ }
292
+ ];
293
+ JOB_MAP = new Map(JOB_REGISTRY.map((job) => [job.id, job]));
294
+ }
295
+ });
296
+
20
297
  // ../core/dist/constants.js
21
298
  function resolveProviderBucketKey(provider, providerEnv) {
22
299
  if (provider === "codex")
@@ -35,6 +312,7 @@ var DEFAULT_DEFAULT_BRANCH, DEFAULT_PRD_DIR, DEFAULT_MAX_RUNTIME, DEFAULT_REVIEW
35
312
  var init_constants = __esm({
36
313
  "../core/dist/constants.js"() {
37
314
  "use strict";
315
+ init_job_registry();
38
316
  DEFAULT_DEFAULT_BRANCH = "";
39
317
  DEFAULT_PRD_DIR = "docs/prds";
40
318
  DEFAULT_MAX_RUNTIME = 7200;
@@ -131,14 +409,7 @@ If no issues are warranted, output an empty array: []`;
131
409
  PLANNER_LOG_NAME = "slicer";
132
410
  ANALYTICS_LOG_NAME = "analytics";
133
411
  VALID_PROVIDERS = ["claude", "codex"];
134
- VALID_JOB_TYPES = [
135
- "executor",
136
- "reviewer",
137
- "qa",
138
- "audit",
139
- "slicer",
140
- "analytics"
141
- ];
412
+ VALID_JOB_TYPES = getValidJobTypes();
142
413
  DEFAULT_JOB_PROVIDERS = {};
143
414
  BUILT_IN_PRESETS = {
144
415
  claude: {
@@ -212,14 +483,7 @@ If no issues are warranted, output an empty array: []`;
212
483
  REVIEWER_LOG_NAME = "reviewer";
213
484
  EXECUTOR_LOG_FILE = "executor.log";
214
485
  REVIEWER_LOG_FILE = "reviewer.log";
215
- LOG_FILE_NAMES = {
216
- executor: EXECUTOR_LOG_NAME,
217
- reviewer: REVIEWER_LOG_NAME,
218
- qa: QA_LOG_NAME,
219
- audit: AUDIT_LOG_NAME,
220
- planner: PLANNER_LOG_NAME,
221
- analytics: ANALYTICS_LOG_NAME
222
- };
486
+ LOG_FILE_NAMES = getLogFileNames();
223
487
  GLOBAL_CONFIG_DIR = ".night-watch";
224
488
  REGISTRY_FILE_NAME = "projects.json";
225
489
  HISTORY_FILE_NAME = "history.json";
@@ -231,14 +495,7 @@ If no issues are warranted, output an empty array: []`;
231
495
  DEFAULT_QUEUE_MODE = "conservative";
232
496
  DEFAULT_QUEUE_MAX_CONCURRENCY = 1;
233
497
  DEFAULT_QUEUE_MAX_WAIT_TIME = 7200;
234
- DEFAULT_QUEUE_PRIORITY = {
235
- executor: 50,
236
- reviewer: 40,
237
- slicer: 30,
238
- qa: 20,
239
- audit: 10,
240
- analytics: 10
241
- };
498
+ DEFAULT_QUEUE_PRIORITY = getDefaultQueuePriority();
242
499
  DEFAULT_QUEUE = {
243
500
  enabled: DEFAULT_QUEUE_ENABLED,
244
501
  mode: DEFAULT_QUEUE_MODE,
@@ -252,15 +509,6 @@ If no issues are warranted, output an empty array: []`;
252
509
  }
253
510
  });
254
511
 
255
- // ../core/dist/board/types.js
256
- var BOARD_COLUMNS;
257
- var init_types2 = __esm({
258
- "../core/dist/board/types.js"() {
259
- "use strict";
260
- BOARD_COLUMNS = ["Draft", "Ready", "In Progress", "Review", "Done"];
261
- }
262
- });
263
-
264
512
  // ../core/dist/config-normalize.js
265
513
  function validateProvider(value) {
266
514
  const trimmed = value.trim();
@@ -432,43 +680,14 @@ function normalizeConfig(rawConfig) {
432
680
  if (mergeMethod && VALID_MERGE_METHODS.includes(mergeMethod)) {
433
681
  normalized.autoMergeMethod = mergeMethod;
434
682
  }
435
- const rawQa = readObject(rawConfig.qa);
436
- if (rawQa) {
437
- const artifactsValue = readString(rawQa.artifacts);
438
- const artifacts = artifactsValue && ["screenshot", "video", "both"].includes(artifactsValue) ? artifactsValue : DEFAULT_QA.artifacts;
439
- const qa = {
440
- enabled: readBoolean(rawQa.enabled) ?? DEFAULT_QA.enabled,
441
- schedule: readString(rawQa.schedule) ?? DEFAULT_QA.schedule,
442
- maxRuntime: readNumber(rawQa.maxRuntime) ?? DEFAULT_QA.maxRuntime,
443
- branchPatterns: readStringArray(rawQa.branchPatterns) ?? DEFAULT_QA.branchPatterns,
444
- artifacts,
445
- skipLabel: readString(rawQa.skipLabel) ?? DEFAULT_QA.skipLabel,
446
- autoInstallPlaywright: readBoolean(rawQa.autoInstallPlaywright) ?? DEFAULT_QA.autoInstallPlaywright
447
- };
448
- normalized.qa = qa;
449
- }
450
- const rawAudit = readObject(rawConfig.audit);
451
- if (rawAudit) {
452
- const audit = {
453
- enabled: readBoolean(rawAudit.enabled) ?? DEFAULT_AUDIT.enabled,
454
- schedule: readString(rawAudit.schedule) ?? DEFAULT_AUDIT.schedule,
455
- maxRuntime: readNumber(rawAudit.maxRuntime) ?? DEFAULT_AUDIT.maxRuntime
456
- };
457
- normalized.audit = audit;
458
- }
459
- const rawAnalytics = readObject(rawConfig.analytics);
460
- if (rawAnalytics) {
461
- const targetColumnRaw = readString(rawAnalytics.targetColumn);
462
- const targetColumn = targetColumnRaw && BOARD_COLUMNS.includes(targetColumnRaw) ? targetColumnRaw : DEFAULT_ANALYTICS.targetColumn;
463
- const analytics = {
464
- enabled: readBoolean(rawAnalytics.enabled) ?? DEFAULT_ANALYTICS.enabled,
465
- schedule: readString(rawAnalytics.schedule) ?? DEFAULT_ANALYTICS.schedule,
466
- maxRuntime: readNumber(rawAnalytics.maxRuntime) ?? DEFAULT_ANALYTICS.maxRuntime,
467
- lookbackDays: readNumber(rawAnalytics.lookbackDays) ?? DEFAULT_ANALYTICS.lookbackDays,
468
- targetColumn,
469
- analysisPrompt: readString(rawAnalytics.analysisPrompt) ?? DEFAULT_ANALYTICS.analysisPrompt
470
- };
471
- normalized.analytics = analytics;
683
+ for (const jobId of ["qa", "audit", "analytics"]) {
684
+ const jobDef = getJobDef(jobId);
685
+ if (!jobDef)
686
+ continue;
687
+ const rawJob = readObject(rawConfig[jobId]);
688
+ if (rawJob) {
689
+ normalized[jobId] = normalizeJobConfig(rawJob, jobDef);
690
+ }
472
691
  }
473
692
  const rawJobProviders = readObject(rawConfig.jobProviders);
474
693
  if (rawJobProviders) {
@@ -529,8 +748,8 @@ function normalizeConfig(rawConfig) {
529
748
  var init_config_normalize = __esm({
530
749
  "../core/dist/config-normalize.js"() {
531
750
  "use strict";
532
- init_types2();
533
751
  init_constants();
752
+ init_job_registry();
534
753
  }
535
754
  });
536
755
 
@@ -706,72 +925,16 @@ function buildEnvOverrideConfig(fileConfig) {
706
925
  env.claudeModel = model;
707
926
  }
708
927
  }
709
- const qaBase = () => env.qa ?? fileConfig?.qa ?? DEFAULT_QA;
710
- if (process.env.NW_QA_ENABLED) {
711
- const v = parseBoolean(process.env.NW_QA_ENABLED);
712
- if (v !== null)
713
- env.qa = { ...qaBase(), enabled: v };
714
- }
715
- if (process.env.NW_QA_SCHEDULE) {
716
- env.qa = { ...qaBase(), schedule: process.env.NW_QA_SCHEDULE };
717
- }
718
- if (process.env.NW_QA_MAX_RUNTIME) {
719
- const v = parseInt(process.env.NW_QA_MAX_RUNTIME, 10);
720
- if (!isNaN(v) && v > 0)
721
- env.qa = { ...qaBase(), maxRuntime: v };
722
- }
723
- if (process.env.NW_QA_ARTIFACTS) {
724
- const a = process.env.NW_QA_ARTIFACTS;
725
- if (["screenshot", "video", "both"].includes(a)) {
726
- env.qa = { ...qaBase(), artifacts: a };
928
+ for (const jobId of ["qa", "audit", "analytics"]) {
929
+ const jobDef = getJobDef(jobId);
930
+ if (!jobDef)
931
+ continue;
932
+ const currentBase = env[jobId] ?? fileConfig?.[jobId] ?? jobDef.defaultConfig;
933
+ const overrides = buildJobEnvOverrides(jobDef.envPrefix, currentBase, jobDef.extraFields);
934
+ if (overrides) {
935
+ env[jobId] = overrides;
727
936
  }
728
937
  }
729
- if (process.env.NW_QA_SKIP_LABEL) {
730
- env.qa = { ...qaBase(), skipLabel: process.env.NW_QA_SKIP_LABEL };
731
- }
732
- if (process.env.NW_QA_AUTO_INSTALL_PLAYWRIGHT) {
733
- const v = parseBoolean(process.env.NW_QA_AUTO_INSTALL_PLAYWRIGHT);
734
- if (v !== null)
735
- env.qa = { ...qaBase(), autoInstallPlaywright: v };
736
- }
737
- if (process.env.NW_QA_BRANCH_PATTERNS) {
738
- const patterns = process.env.NW_QA_BRANCH_PATTERNS.split(",").map((s) => s.trim()).filter(Boolean);
739
- if (patterns.length > 0)
740
- env.qa = { ...qaBase(), branchPatterns: patterns };
741
- }
742
- const auditBase = () => env.audit ?? fileConfig?.audit ?? DEFAULT_AUDIT;
743
- if (process.env.NW_AUDIT_ENABLED) {
744
- const v = parseBoolean(process.env.NW_AUDIT_ENABLED);
745
- if (v !== null)
746
- env.audit = { ...auditBase(), enabled: v };
747
- }
748
- if (process.env.NW_AUDIT_SCHEDULE) {
749
- env.audit = { ...auditBase(), schedule: process.env.NW_AUDIT_SCHEDULE };
750
- }
751
- if (process.env.NW_AUDIT_MAX_RUNTIME) {
752
- const v = parseInt(process.env.NW_AUDIT_MAX_RUNTIME, 10);
753
- if (!isNaN(v) && v > 0)
754
- env.audit = { ...auditBase(), maxRuntime: v };
755
- }
756
- const analyticsBase = () => env.analytics ?? fileConfig?.analytics ?? DEFAULT_ANALYTICS;
757
- if (process.env.NW_ANALYTICS_ENABLED) {
758
- const v = parseBoolean(process.env.NW_ANALYTICS_ENABLED);
759
- if (v !== null)
760
- env.analytics = { ...analyticsBase(), enabled: v };
761
- }
762
- if (process.env.NW_ANALYTICS_SCHEDULE) {
763
- env.analytics = { ...analyticsBase(), schedule: process.env.NW_ANALYTICS_SCHEDULE };
764
- }
765
- if (process.env.NW_ANALYTICS_MAX_RUNTIME) {
766
- const v = parseInt(process.env.NW_ANALYTICS_MAX_RUNTIME, 10);
767
- if (!isNaN(v) && v > 0)
768
- env.analytics = { ...analyticsBase(), maxRuntime: v };
769
- }
770
- if (process.env.NW_ANALYTICS_LOOKBACK_DAYS) {
771
- const v = parseInt(process.env.NW_ANALYTICS_LOOKBACK_DAYS, 10);
772
- if (!isNaN(v) && v > 0)
773
- env.analytics = { ...analyticsBase(), lookbackDays: v };
774
- }
775
938
  const jobProvidersEnv = {};
776
939
  for (const jobType of VALID_JOB_TYPES) {
777
940
  const val = process.env[`NW_JOB_PROVIDER_${jobType.toUpperCase()}`];
@@ -821,6 +984,7 @@ var init_config_env = __esm({
821
984
  "use strict";
822
985
  init_constants();
823
986
  init_config_normalize();
987
+ init_job_registry();
824
988
  }
825
989
  });
826
990
 
@@ -1016,6 +1180,15 @@ var init_config = __esm({
1016
1180
  }
1017
1181
  });
1018
1182
 
1183
+ // ../core/dist/board/types.js
1184
+ var BOARD_COLUMNS;
1185
+ var init_types2 = __esm({
1186
+ "../core/dist/board/types.js"() {
1187
+ "use strict";
1188
+ BOARD_COLUMNS = ["Draft", "Ready", "In Progress", "Review", "Done"];
1189
+ }
1190
+ });
1191
+
1019
1192
  // ../core/dist/storage/repositories/sqlite/execution-history.repository.js
1020
1193
  import Database from "better-sqlite3";
1021
1194
  import { inject, injectable } from "tsyringe";
@@ -3619,7 +3792,7 @@ function getLockFilePaths(projectDir) {
3619
3792
  };
3620
3793
  }
3621
3794
  function sleep(ms) {
3622
- return new Promise((resolve10) => setTimeout(resolve10, ms));
3795
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
3623
3796
  }
3624
3797
  async function cancelProcess(processType, lockPath, force = false) {
3625
3798
  const lockStatus = checkLockFile(lockPath);
@@ -4921,6 +5094,26 @@ function validateRegistry() {
4921
5094
  }
4922
5095
  return { valid, invalid };
4923
5096
  }
5097
+ function pruneProjectData(projectDir) {
5098
+ const resolvedPath = path12.resolve(projectDir);
5099
+ const db = getDb();
5100
+ db.transaction(() => {
5101
+ db.prepare("DELETE FROM execution_history WHERE project_path = ?").run(resolvedPath);
5102
+ db.prepare("DELETE FROM prd_states WHERE project_path = ?").run(resolvedPath);
5103
+ db.prepare("DELETE FROM job_queue WHERE project_path = ?").run(resolvedPath);
5104
+ db.prepare("DELETE FROM job_runs WHERE project_path = ?").run(resolvedPath);
5105
+ db.prepare("DELETE FROM roadmap_states WHERE prd_dir LIKE ?").run(`${resolvedPath}%`);
5106
+ })();
5107
+ }
5108
+ function removeProject(projectDir) {
5109
+ const resolvedPath = path12.resolve(projectDir);
5110
+ const projectName = getProjectName(resolvedPath);
5111
+ const marker = generateMarker(projectName);
5112
+ const cronEntriesRemoved = removeEntriesForProject(resolvedPath, marker);
5113
+ pruneProjectData(resolvedPath);
5114
+ const unregistered = unregisterProject(resolvedPath);
5115
+ return { cronEntriesRemoved, unregistered, dataPruned: true };
5116
+ }
4924
5117
  var init_registry = __esm({
4925
5118
  "../core/dist/utils/registry.js"() {
4926
5119
  "use strict";
@@ -4928,6 +5121,7 @@ var init_registry = __esm({
4928
5121
  init_repositories();
4929
5122
  init_client();
4930
5123
  init_client();
5124
+ init_crontab();
4931
5125
  init_status_data();
4932
5126
  }
4933
5127
  });
@@ -5196,6 +5390,7 @@ var init_roadmap_state = __esm({
5196
5390
  // ../core/dist/templates/slicer-prompt.js
5197
5391
  import * as fs15 from "fs";
5198
5392
  import * as path14 from "path";
5393
+ import { fileURLToPath as fileURLToPath2 } from "url";
5199
5394
  function loadSlicerTemplate(templateDir) {
5200
5395
  if (cachedTemplate) {
5201
5396
  return cachedTemplate;
@@ -5231,10 +5426,12 @@ function createSlicerPromptVars(title, section, description, prdDir, prdFilename
5231
5426
  prdDir
5232
5427
  };
5233
5428
  }
5234
- var DEFAULT_SLICER_TEMPLATE, cachedTemplate;
5429
+ var __filename, __dirname, DEFAULT_SLICER_TEMPLATE, cachedTemplate;
5235
5430
  var init_slicer_prompt = __esm({
5236
5431
  "../core/dist/templates/slicer-prompt.js"() {
5237
5432
  "use strict";
5433
+ __filename = fileURLToPath2(import.meta.url);
5434
+ __dirname = path14.dirname(__filename);
5238
5435
  DEFAULT_SLICER_TEMPLATE = `You are a **PRD Creator Agent**. Your job: analyze the codebase and write a complete Product Requirements Document (PRD) for a feature.
5239
5436
 
5240
5437
  When this activates: \`PRD Creator: Initializing\`
@@ -5508,11 +5705,11 @@ function scanExistingPrdSlugs(prdDir) {
5508
5705
  }
5509
5706
  return slugs;
5510
5707
  }
5511
- function buildProviderArgs(provider, prompt2, workingDir) {
5708
+ function buildProviderArgs(provider, prompt, workingDir) {
5512
5709
  if (provider === "codex") {
5513
- return ["exec", "-C", workingDir, "--yolo", prompt2];
5710
+ return ["exec", "-C", workingDir, "--yolo", prompt];
5514
5711
  }
5515
- return ["-p", prompt2, "--dangerously-skip-permissions"];
5712
+ return ["-p", prompt, "--dangerously-skip-permissions"];
5516
5713
  }
5517
5714
  async function sliceRoadmapItem(projectDir, prdDir, item, config) {
5518
5715
  const itemSlug = slugify(item.title);
@@ -5532,9 +5729,9 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
5532
5729
  fs16.mkdirSync(prdDir, { recursive: true });
5533
5730
  }
5534
5731
  const promptVars = createSlicerPromptVars(item.title, item.section, item.description, prdDir, filename);
5535
- const prompt2 = renderSlicerPrompt(promptVars);
5732
+ const prompt = renderSlicerPrompt(promptVars);
5536
5733
  const provider = resolveJobProvider(config, "slicer");
5537
- const providerArgs = buildProviderArgs(provider, prompt2, projectDir);
5734
+ const providerArgs = buildProviderArgs(provider, prompt, projectDir);
5538
5735
  const logDir = path15.join(projectDir, "logs");
5539
5736
  if (!fs16.existsSync(logDir)) {
5540
5737
  fs16.mkdirSync(logDir, { recursive: true });
@@ -5543,7 +5740,7 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
5543
5740
  const logStream = fs16.createWriteStream(logFile, { flags: "w" });
5544
5741
  logStream.on("error", () => {
5545
5742
  });
5546
- return new Promise((resolve10) => {
5743
+ return new Promise((resolve9) => {
5547
5744
  const childEnv = {
5548
5745
  ...process.env,
5549
5746
  ...config.providerEnv
@@ -5561,7 +5758,7 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
5561
5758
  });
5562
5759
  child.on("error", (error2) => {
5563
5760
  logStream.end();
5564
- resolve10({
5761
+ resolve9({
5565
5762
  sliced: false,
5566
5763
  error: `Failed to spawn provider: ${error2.message}`,
5567
5764
  item
@@ -5570,7 +5767,7 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
5570
5767
  child.on("close", (code) => {
5571
5768
  logStream.end();
5572
5769
  if (code !== 0) {
5573
- resolve10({
5770
+ resolve9({
5574
5771
  sliced: false,
5575
5772
  error: `Provider exited with code ${code ?? 1}`,
5576
5773
  item
@@ -5578,14 +5775,14 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
5578
5775
  return;
5579
5776
  }
5580
5777
  if (!fs16.existsSync(filePath)) {
5581
- resolve10({
5778
+ resolve9({
5582
5779
  sliced: false,
5583
5780
  error: `Provider did not create expected file: ${filePath}`,
5584
5781
  item
5585
5782
  });
5586
5783
  return;
5587
5784
  }
5588
- resolve10({
5785
+ resolve9({
5589
5786
  sliced: true,
5590
5787
  file: filename,
5591
5788
  item
@@ -5746,7 +5943,7 @@ async function executeScript(scriptPath, args = [], env = {}, options = {}) {
5746
5943
  return result.exitCode;
5747
5944
  }
5748
5945
  async function executeScriptWithOutput(scriptPath, args = [], env = {}, options = {}) {
5749
- return new Promise((resolve10, reject) => {
5946
+ return new Promise((resolve9, reject) => {
5750
5947
  const childEnv = {
5751
5948
  ...process.env,
5752
5949
  ...env
@@ -5772,7 +5969,7 @@ async function executeScriptWithOutput(scriptPath, args = [], env = {}, options
5772
5969
  reject(error2);
5773
5970
  });
5774
5971
  child.on("close", (code) => {
5775
- resolve10({
5972
+ resolve9({
5776
5973
  exitCode: code ?? 1,
5777
5974
  stdout: stdoutChunks.join(""),
5778
5975
  stderr: stderrChunks.join("")
@@ -6167,6 +6364,7 @@ function getLockPathForJob(projectPath, jobType) {
6167
6364
  case "audit":
6168
6365
  return auditLockPath(projectPath);
6169
6366
  case "slicer":
6367
+ case "planner":
6170
6368
  return plannerLockPath(projectPath);
6171
6369
  case "analytics":
6172
6370
  return analyticsLockPath(projectPath);
@@ -6723,13 +6921,13 @@ async function runAnalytics(config, projectDir) {
6723
6921
  logger3.info("Fetching Amplitude data", { lookbackDays: config.analytics.lookbackDays });
6724
6922
  const data = await fetchAmplitudeData(apiKey, secretKey, config.analytics.lookbackDays);
6725
6923
  const systemPrompt = config.analytics.analysisPrompt?.trim() || DEFAULT_ANALYTICS_PROMPT;
6726
- const prompt2 = `${systemPrompt}
6924
+ const prompt = `${systemPrompt}
6727
6925
 
6728
6926
  --- AMPLITUDE DATA ---
6729
6927
  ${JSON.stringify(data, null, 2)}`;
6730
6928
  const tmpDir = fs19.mkdtempSync(path19.join(os8.tmpdir(), "nw-analytics-"));
6731
6929
  const promptFile = path19.join(tmpDir, "analytics-prompt.md");
6732
- fs19.writeFileSync(promptFile, prompt2, "utf-8");
6930
+ fs19.writeFileSync(promptFile, prompt, "utf-8");
6733
6931
  try {
6734
6932
  const provider = resolveJobProvider(config, "analytics");
6735
6933
  const providerCmd = PROVIDER_COMMANDS[provider];
@@ -6986,6 +7184,14 @@ sequenceDiagram
6986
7184
  }
6987
7185
  });
6988
7186
 
7187
+ // ../core/dist/jobs/index.js
7188
+ var init_jobs = __esm({
7189
+ "../core/dist/jobs/index.js"() {
7190
+ "use strict";
7191
+ init_job_registry();
7192
+ }
7193
+ });
7194
+
6989
7195
  // ../core/dist/index.js
6990
7196
  var dist_exports = {};
6991
7197
  __export(dist_exports, {
@@ -7067,6 +7273,7 @@ __export(dist_exports, {
7067
7273
  HISTORY_FILE_NAME: () => HISTORY_FILE_NAME,
7068
7274
  HORIZON_LABELS: () => HORIZON_LABELS,
7069
7275
  HORIZON_LABEL_INFO: () => HORIZON_LABEL_INFO,
7276
+ JOB_REGISTRY: () => JOB_REGISTRY,
7070
7277
  LOCK_FILE_PREFIX: () => LOCK_FILE_PREFIX,
7071
7278
  LOG_DIR: () => LOG_DIR,
7072
7279
  LOG_FILE_NAMES: () => LOG_FILE_NAMES,
@@ -7098,7 +7305,9 @@ __export(dist_exports, {
7098
7305
  analyticsLockPath: () => analyticsLockPath,
7099
7306
  auditLockPath: () => auditLockPath,
7100
7307
  buildDescription: () => buildDescription,
7308
+ buildJobEnvOverrides: () => buildJobEnvOverrides,
7101
7309
  calculateStringSimilarity: () => calculateStringSimilarity,
7310
+ camelToUpperSnake: () => camelToUpperSnake,
7102
7311
  canStartJob: () => canStartJob,
7103
7312
  cancelProcess: () => cancelProcess,
7104
7313
  checkConfigFile: () => checkConfigFile,
@@ -7163,23 +7372,30 @@ __export(dist_exports, {
7163
7372
  formatTelegramPayload: () => formatTelegramPayload,
7164
7373
  generateItemHash: () => generateItemHash,
7165
7374
  generateMarker: () => generateMarker,
7375
+ getAllJobDefs: () => getAllJobDefs,
7166
7376
  getBranchTipTimestamp: () => getBranchTipTimestamp,
7167
7377
  getCrontabInfo: () => getCrontabInfo,
7168
7378
  getDb: () => getDb,
7169
7379
  getDbPath: () => getDbPath,
7170
7380
  getDefaultConfig: () => getDefaultConfig,
7381
+ getDefaultQueuePriority: () => getDefaultQueuePriority,
7171
7382
  getEntries: () => getEntries,
7172
7383
  getEventColor: () => getEventColor,
7173
7384
  getEventEmoji: () => getEventEmoji,
7174
7385
  getEventTitle: () => getEventTitle,
7175
7386
  getHistoryPath: () => getHistoryPath,
7176
7387
  getInFlightCount: () => getInFlightCount,
7388
+ getJobDef: () => getJobDef,
7389
+ getJobDefByCommand: () => getJobDefByCommand,
7390
+ getJobDefByLogName: () => getJobDefByLogName,
7177
7391
  getJobPriority: () => getJobPriority,
7178
7392
  getJobRunsAnalytics: () => getJobRunsAnalytics,
7179
7393
  getLabelsForSection: () => getLabelsForSection,
7180
7394
  getLastExecution: () => getLastExecution,
7181
7395
  getLastLogLines: () => getLastLogLines,
7182
7396
  getLockFilePaths: () => getLockFilePaths,
7397
+ getLockSuffix: () => getLockSuffix,
7398
+ getLogFileNames: () => getLogFileNames,
7183
7399
  getLogInfo: () => getLogInfo,
7184
7400
  getNextPendingJob: () => getNextPendingJob,
7185
7401
  getNextPrdNumber: () => getNextPrdNumber,
@@ -7200,6 +7416,7 @@ __export(dist_exports, {
7200
7416
  getStateFilePath: () => getStateFilePath,
7201
7417
  getStateItem: () => getStateItem,
7202
7418
  getUncheckedItems: () => getUncheckedItems,
7419
+ getValidJobTypes: () => getValidJobTypes,
7203
7420
  groupBySection: () => groupBySection2,
7204
7421
  hasEntry: () => hasEntry,
7205
7422
  hasNewItems: () => hasNewItems,
@@ -7226,6 +7443,7 @@ __export(dist_exports, {
7226
7443
  markJobRunning: () => markJobRunning,
7227
7444
  markPrdDone: () => markPrdDone,
7228
7445
  migrateJsonToSqlite: () => migrateJsonToSqlite,
7446
+ normalizeJobConfig: () => normalizeJobConfig,
7229
7447
  normalizeSchedulingPriority: () => normalizeSchedulingPriority,
7230
7448
  parsePrdDependencies: () => parsePrdDependencies,
7231
7449
  parseRoadmap: () => parseRoadmap,
@@ -7235,6 +7453,7 @@ __export(dist_exports, {
7235
7453
  prepareBranchWorktree: () => prepareBranchWorktree,
7236
7454
  prepareDetachedWorktree: () => prepareDetachedWorktree,
7237
7455
  projectRuntimeKey: () => projectRuntimeKey,
7456
+ pruneProjectData: () => pruneProjectData,
7238
7457
  qaLockPath: () => qaLockPath,
7239
7458
  readCrontab: () => readCrontab,
7240
7459
  readPrdStates: () => readPrdStates,
@@ -7245,6 +7464,7 @@ __export(dist_exports, {
7245
7464
  removeEntries: () => removeEntries,
7246
7465
  removeEntriesForProject: () => removeEntriesForProject,
7247
7466
  removeJob: () => removeJob,
7467
+ removeProject: () => removeProject,
7248
7468
  renderPrdTemplate: () => renderPrdTemplate,
7249
7469
  renderSlicerPrompt: () => renderSlicerPrompt,
7250
7470
  resetRepositories: () => resetRepositories,
@@ -7329,6 +7549,7 @@ var init_dist = __esm({
7329
7549
  init_analytics();
7330
7550
  init_prd_template();
7331
7551
  init_slicer_prompt();
7552
+ init_jobs();
7332
7553
  }
7333
7554
  });
7334
7555
 
@@ -7336,19 +7557,19 @@ var init_dist = __esm({
7336
7557
  import "reflect-metadata";
7337
7558
  import { Command as Command3 } from "commander";
7338
7559
  import { existsSync as existsSync30, readFileSync as readFileSync19 } from "fs";
7339
- import { fileURLToPath as fileURLToPath4 } from "url";
7340
- import { dirname as dirname9, join as join36 } from "path";
7560
+ import { fileURLToPath as fileURLToPath6 } from "url";
7561
+ import { dirname as dirname12, join as join36 } from "path";
7341
7562
 
7342
7563
  // src/commands/init.ts
7343
7564
  init_dist();
7344
7565
  import fs20 from "fs";
7345
7566
  import path20 from "path";
7346
7567
  import { execSync as execSync3 } from "child_process";
7347
- import { fileURLToPath as fileURLToPath2 } from "url";
7348
- import { dirname as dirname5, join as join18 } from "path";
7568
+ import { fileURLToPath as fileURLToPath3 } from "url";
7569
+ import { dirname as dirname6, join as join18 } from "path";
7349
7570
  import * as readline from "readline";
7350
- var __filename = fileURLToPath2(import.meta.url);
7351
- var __dirname2 = dirname5(__filename);
7571
+ var __filename2 = fileURLToPath3(import.meta.url);
7572
+ var __dirname2 = dirname6(__filename2);
7352
7573
  function findTemplatesDir(startDir) {
7353
7574
  let d = startDir;
7354
7575
  for (let i = 0; i < 8; i++) {
@@ -7356,11 +7577,19 @@ function findTemplatesDir(startDir) {
7356
7577
  if (fs20.existsSync(candidate) && fs20.statSync(candidate).isDirectory()) {
7357
7578
  return candidate;
7358
7579
  }
7359
- d = dirname5(d);
7580
+ d = dirname6(d);
7360
7581
  }
7361
7582
  return join18(startDir, "templates");
7362
7583
  }
7363
7584
  var TEMPLATES_DIR = findTemplatesDir(__dirname2);
7585
+ var NW_SKILLS = [
7586
+ "nw-create-prd",
7587
+ "nw-add-issue",
7588
+ "nw-run",
7589
+ "nw-slice",
7590
+ "nw-board-sync",
7591
+ "nw-review"
7592
+ ];
7364
7593
  function hasPlaywrightDependency(cwd) {
7365
7594
  const packageJsonPath = path20.join(cwd, "package.json");
7366
7595
  if (!fs20.existsSync(packageJsonPath)) {
@@ -7407,7 +7636,7 @@ function promptYesNo(question, defaultNo = true) {
7407
7636
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
7408
7637
  return Promise.resolve(false);
7409
7638
  }
7410
- return new Promise((resolve10) => {
7639
+ return new Promise((resolve9) => {
7411
7640
  const rl = readline.createInterface({
7412
7641
  input: process.stdin,
7413
7642
  output: process.stdout
@@ -7417,10 +7646,10 @@ function promptYesNo(question, defaultNo = true) {
7417
7646
  rl.close();
7418
7647
  const normalized = answer.trim().toLowerCase();
7419
7648
  if (normalized === "") {
7420
- resolve10(!defaultNo);
7649
+ resolve9(!defaultNo);
7421
7650
  return;
7422
7651
  }
7423
- resolve10(normalized === "y" || normalized === "yes");
7652
+ resolve9(normalized === "y" || normalized === "yes");
7424
7653
  });
7425
7654
  });
7426
7655
  }
@@ -7523,7 +7752,7 @@ function getDefaultBranch(cwd) {
7523
7752
  }
7524
7753
  }
7525
7754
  function promptProviderSelection(providers) {
7526
- return new Promise((resolve10, reject) => {
7755
+ return new Promise((resolve9, reject) => {
7527
7756
  const rl = readline.createInterface({
7528
7757
  input: process.stdin,
7529
7758
  output: process.stdout
@@ -7539,7 +7768,7 @@ function promptProviderSelection(providers) {
7539
7768
  reject(new Error("Invalid selection. Please run init again and select a valid number."));
7540
7769
  return;
7541
7770
  }
7542
- resolve10(providers[selection - 1]);
7771
+ resolve9(providers[selection - 1]);
7543
7772
  });
7544
7773
  });
7545
7774
  }
@@ -7656,12 +7885,67 @@ function addToGitignore(cwd) {
7656
7885
  fs20.writeFileSync(gitignorePath, newContent);
7657
7886
  console.log(` Updated: ${gitignorePath} (added ${missing.map((e) => e.label).join(", ")})`);
7658
7887
  }
7888
+ function installSkills(cwd, provider, force, templatesDir) {
7889
+ const skillsTemplatesDir = path20.join(templatesDir, "skills");
7890
+ if (!fs20.existsSync(skillsTemplatesDir)) {
7891
+ return { location: "", installed: 0, skipped: 0, type: "none" };
7892
+ }
7893
+ const isClaudeProvider = provider === "claude" || provider.startsWith("claude");
7894
+ const isCodexProvider = provider === "codex";
7895
+ const claudeDir = path20.join(cwd, ".claude");
7896
+ if (isClaudeProvider || fs20.existsSync(claudeDir)) {
7897
+ ensureDir(claudeDir);
7898
+ const skillsDir = path20.join(claudeDir, "skills");
7899
+ ensureDir(skillsDir);
7900
+ let installed = 0;
7901
+ let skipped = 0;
7902
+ for (const skillName of NW_SKILLS) {
7903
+ const templateFile = path20.join(skillsTemplatesDir, `${skillName}.md`);
7904
+ if (!fs20.existsSync(templateFile)) continue;
7905
+ const skillDir = path20.join(skillsDir, skillName);
7906
+ ensureDir(skillDir);
7907
+ const target = path20.join(skillDir, "SKILL.md");
7908
+ if (fs20.existsSync(target) && !force) {
7909
+ skipped++;
7910
+ continue;
7911
+ }
7912
+ fs20.copyFileSync(templateFile, target);
7913
+ installed++;
7914
+ }
7915
+ return { location: ".claude/skills/", installed, skipped, type: "claude" };
7916
+ }
7917
+ if (isCodexProvider) {
7918
+ const agentsFile = path20.join(cwd, "AGENTS.md");
7919
+ const blockFile = path20.join(skillsTemplatesDir, "_codex-block.md");
7920
+ if (!fs20.existsSync(blockFile)) {
7921
+ return { location: "", installed: 0, skipped: 0, type: "none" };
7922
+ }
7923
+ const block = fs20.readFileSync(blockFile, "utf-8");
7924
+ const marker = "## Night Watch Skills";
7925
+ if (!fs20.existsSync(agentsFile)) {
7926
+ fs20.writeFileSync(agentsFile, block);
7927
+ return { location: "AGENTS.md", installed: NW_SKILLS.length, skipped: 0, type: "codex" };
7928
+ }
7929
+ const existing = fs20.readFileSync(agentsFile, "utf-8");
7930
+ if (existing.includes(marker)) {
7931
+ if (!force) {
7932
+ return { location: "AGENTS.md", installed: 0, skipped: NW_SKILLS.length, type: "codex" };
7933
+ }
7934
+ const withoutSection = existing.replace(/\n\n## Night Watch Skills[\s\S]*$/, "");
7935
+ fs20.writeFileSync(agentsFile, withoutSection + "\n\n" + block);
7936
+ } else {
7937
+ fs20.appendFileSync(agentsFile, "\n\n" + block);
7938
+ }
7939
+ return { location: "AGENTS.md", installed: NW_SKILLS.length, skipped: 0, type: "codex" };
7940
+ }
7941
+ return { location: "", installed: 0, skipped: 0, type: "none" };
7942
+ }
7659
7943
  function initCommand(program2) {
7660
7944
  program2.command("init").description("Initialize night-watch in the current project").option("-f, --force", "Overwrite existing configuration").option("-d, --prd-dir <path>", "Path to PRD directory").option("-p, --provider <name>", "AI provider to use (claude or codex)").option("--no-reviewer", "Disable reviewer cron job").action(async (options) => {
7661
7945
  const cwd = process.cwd();
7662
7946
  const force = options.force || false;
7663
7947
  const prdDir = options.prdDir || DEFAULT_PRD_DIR;
7664
- const totalSteps = 12;
7948
+ const totalSteps = 13;
7665
7949
  const interactive = isInteractiveInitSession();
7666
7950
  console.log();
7667
7951
  header("Night Watch CLI - Initializing");
@@ -7846,6 +8130,20 @@ function initCommand(program2) {
7846
8130
  auditResolution.source
7847
8131
  );
7848
8132
  templateSources.push({ name: "audit.md", source: auditResult.source });
8133
+ const plannerResolution = resolveTemplatePath(
8134
+ "prd-creator.md",
8135
+ customTemplatesDir,
8136
+ TEMPLATES_DIR
8137
+ );
8138
+ const plannerResult = processTemplate(
8139
+ "prd-creator.md",
8140
+ path20.join(instructionsDir, "prd-creator.md"),
8141
+ replacements,
8142
+ force,
8143
+ plannerResolution.path,
8144
+ plannerResolution.source
8145
+ );
8146
+ templateSources.push({ name: "prd-creator.md", source: plannerResult.source });
7849
8147
  step(9, totalSteps, "Creating configuration file...");
7850
8148
  const configPath = path20.join(cwd, CONFIG_FILE_NAME);
7851
8149
  if (fs20.existsSync(configPath) && !force) {
@@ -7912,7 +8210,19 @@ function initCommand(program2) {
7912
8210
  ` Warning: Could not register in global registry: ${regErr instanceof Error ? regErr.message : String(regErr)}`
7913
8211
  );
7914
8212
  }
7915
- step(12, totalSteps, "Initialization complete!");
8213
+ step(12, totalSteps, "Installing Night Watch skills...");
8214
+ const skillsResult = installSkills(cwd, selectedProvider, force, TEMPLATES_DIR);
8215
+ if (skillsResult.installed > 0) {
8216
+ success(`Installed ${skillsResult.installed} skills to ${skillsResult.location}`);
8217
+ for (const skillName of NW_SKILLS) {
8218
+ console.log(` /${skillName}`);
8219
+ }
8220
+ } else if (skillsResult.skipped > 0) {
8221
+ info(`Skills already installed (use --force to overwrite)`);
8222
+ } else if (skillsResult.type === "none") {
8223
+ info("No compatible AI skills directory detected \u2014 skipping.");
8224
+ }
8225
+ step(13, totalSteps, "Initialization complete!");
7916
8226
  header("Initialization Complete");
7917
8227
  const filesTable = createTable({ head: ["Created Files", ""] });
7918
8228
  filesTable.push(["PRD Directory", `${prdDir}/done/`]);
@@ -7922,9 +8232,19 @@ function initCommand(program2) {
7922
8232
  filesTable.push(["", `instructions/pr-reviewer.md (${templateSources[2].source})`]);
7923
8233
  filesTable.push(["", `instructions/qa.md (${templateSources[3].source})`]);
7924
8234
  filesTable.push(["", `instructions/audit.md (${templateSources[4].source})`]);
8235
+ filesTable.push(["", `instructions/prd-creator.md (${templateSources[5].source})`]);
7925
8236
  filesTable.push(["Config File", CONFIG_FILE_NAME]);
7926
8237
  filesTable.push(["Board Setup", boardSetupStatus]);
7927
8238
  filesTable.push(["Global Registry", "~/.night-watch/projects.json"]);
8239
+ let skillsSummary;
8240
+ if (skillsResult.installed > 0) {
8241
+ skillsSummary = `${skillsResult.installed} skills \u2192 ${skillsResult.location}`;
8242
+ } else if (skillsResult.skipped > 0) {
8243
+ skillsSummary = `Already installed (${skillsResult.location})`;
8244
+ } else {
8245
+ skillsSummary = "Skipped";
8246
+ }
8247
+ filesTable.push(["Skills", skillsSummary]);
7928
8248
  console.log(filesTable.toString());
7929
8249
  header("Configuration");
7930
8250
  label("Provider", selectedProvider);
@@ -7936,6 +8256,9 @@ function initCommand(program2) {
7936
8256
  info("2. Run `night-watch install` to set up cron jobs");
7937
8257
  info("3. Run `night-watch doctor` to verify the full setup");
7938
8258
  info("4. Or run `night-watch run` to execute PRDs manually");
8259
+ if (skillsResult.installed > 0) {
8260
+ info(`5. Use /nw-create-prd, /nw-run, /nw-add-issue and more in your AI assistant`);
8261
+ }
7939
8262
  console.log();
7940
8263
  });
7941
8264
  }
@@ -7991,7 +8314,7 @@ async function maybeApplyCronSchedulingDelay(config, jobType, projectDir) {
7991
8314
  return plan;
7992
8315
  }
7993
8316
  if (plan.totalDelayMinutes > 0) {
7994
- await new Promise((resolve10) => setTimeout(resolve10, plan.totalDelayMinutes * 6e4));
8317
+ await new Promise((resolve9) => setTimeout(resolve9, plan.totalDelayMinutes * 6e4));
7995
8318
  }
7996
8319
  return getSchedulingPlan(projectDir, config, jobType);
7997
8320
  }
@@ -9734,9 +10057,25 @@ function logsCommand(program2) {
9734
10057
 
9735
10058
  // src/commands/prd.ts
9736
10059
  init_dist();
10060
+ import { execSync as execSync5, spawn as spawn4, spawnSync } from "child_process";
9737
10061
  import * as fs26 from "fs";
9738
10062
  import * as path28 from "path";
9739
- import * as readline2 from "readline";
10063
+ import { fileURLToPath as fileURLToPath4 } from "url";
10064
+ import { dirname as dirname9 } from "path";
10065
+ var __filename3 = fileURLToPath4(import.meta.url);
10066
+ var __dirname3 = dirname9(__filename3);
10067
+ function findTemplatesDir2(startDir) {
10068
+ let current = startDir;
10069
+ for (let i = 0; i < 8; i++) {
10070
+ const candidate = path28.join(current, "templates");
10071
+ if (fs26.existsSync(candidate) && fs26.statSync(candidate).isDirectory()) {
10072
+ return candidate;
10073
+ }
10074
+ current = path28.dirname(current);
10075
+ }
10076
+ return path28.join(startDir, "templates");
10077
+ }
10078
+ var TEMPLATES_DIR2 = findTemplatesDir2(__dirname3);
9740
10079
  function slugify2(name) {
9741
10080
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
9742
10081
  }
@@ -9749,13 +10088,186 @@ function getNextPrdNumber2(prdDir) {
9749
10088
  });
9750
10089
  return Math.max(0, ...numbers) + 1;
9751
10090
  }
9752
- function prompt(rl, question) {
9753
- return new Promise((resolve10) => {
9754
- rl.question(question, (answer) => {
9755
- resolve10(answer.trim());
10091
+ function extractPrdMarkdown(response) {
10092
+ const match = response.match(/(^#\s+[\s\S]*)/m);
10093
+ return match ? match[1].trim() : response.trim();
10094
+ }
10095
+ function extractPrdTitle(markdown) {
10096
+ const match = markdown.match(/^#\s+PRD:\s*(.+)/m);
10097
+ return match ? match[1].trim() : null;
10098
+ }
10099
+ function buildPrdPrompt(description, projectDir, planningPrinciples) {
10100
+ return `You are generating a PRD markdown file for Night Watch.
10101
+
10102
+ Return only the final PRD markdown.
10103
+
10104
+ Hard requirements:
10105
+ - Start with: # PRD: <title>
10106
+ - Do not ask follow-up questions
10107
+ - Do not add any preamble, commentary, or code fences
10108
+ - Do not describe what you are going to do
10109
+ - Do not mention these instructions
10110
+ - Treat the planning guide below as mandatory instructions, not background context
10111
+
10112
+ Project directory: ${projectDir}
10113
+
10114
+ Planning guide:
10115
+ ${planningPrinciples}
10116
+
10117
+ User request:
10118
+ ${description}
10119
+
10120
+ Now write the complete PRD markdown file.`;
10121
+ }
10122
+ function buildNativeClaudeEnv(baseEnv = process.env) {
10123
+ const env = { ...baseEnv };
10124
+ delete env.ANTHROPIC_BASE_URL;
10125
+ delete env.ANTHROPIC_API_KEY;
10126
+ delete env.ANTHROPIC_AUTH_TOKEN;
10127
+ delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
10128
+ delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
10129
+ delete env.API_TIMEOUT_MS;
10130
+ delete env.CLAUDE_CODE_SSE_PORT;
10131
+ delete env.CLAUDE_NIGHTS_WATCH_DIR;
10132
+ delete env.NW_CLAUDE_MODEL_ID;
10133
+ delete env.NW_CLAUDE_PRIMARY_MODEL_ID;
10134
+ delete env.NW_CLAUDE_SECONDARY_MODEL_ID;
10135
+ delete env.NW_PROVIDER_CMD;
10136
+ delete env.NW_PROVIDER_SUBCOMMAND;
10137
+ delete env.NW_PROVIDER_PROMPT_FLAG;
10138
+ delete env.NW_PROVIDER_APPROVE_FLAG;
10139
+ delete env.NW_PROVIDER_WORKDIR_FLAG;
10140
+ delete env.NW_PROVIDER_MODEL_FLAG;
10141
+ delete env.NW_PROVIDER_MODEL;
10142
+ delete env.NW_PROVIDER_LABEL;
10143
+ return env;
10144
+ }
10145
+ function resolvePrdCreateDir() {
10146
+ return "docs/PRDs";
10147
+ }
10148
+ function resolveGitHubBlobUrl(projectDir, relPath) {
10149
+ try {
10150
+ const remoteUrl = execSync5("git remote get-url origin", {
10151
+ cwd: projectDir,
10152
+ encoding: "utf-8",
10153
+ stdio: ["ignore", "pipe", "pipe"]
10154
+ }).trim();
10155
+ const branch = execSync5("git rev-parse --abbrev-ref HEAD", {
10156
+ cwd: projectDir,
10157
+ encoding: "utf-8",
10158
+ stdio: ["ignore", "pipe", "pipe"]
10159
+ }).trim();
10160
+ const httpsBase = remoteUrl.replace(/^git@github\.com:/, "https://github.com/").replace(/^ssh:\/\/git@github\.com\//, "https://github.com/").replace(/\.git$/, "");
10161
+ if (!httpsBase.startsWith("https://github.com/")) {
10162
+ return null;
10163
+ }
10164
+ const ref = branch && branch !== "HEAD" ? branch : "main";
10165
+ return `${httpsBase}/blob/${encodeURIComponent(ref).replace(/%2F/g, "/")}/${relPath.split(path28.sep).map((segment) => encodeURIComponent(segment)).join("/")}`;
10166
+ } catch {
10167
+ return null;
10168
+ }
10169
+ }
10170
+ function buildGithubIssueBody(prdPath, projectDir, prdContent) {
10171
+ const relPath = path28.relative(projectDir, prdPath);
10172
+ const blobUrl = resolveGitHubBlobUrl(projectDir, relPath);
10173
+ const fileLine = blobUrl ? `PRD file: [\`${relPath}\`](${blobUrl})` : `PRD file: \`${relPath}\``;
10174
+ return `${fileLine}
10175
+
10176
+ ${prdContent}
10177
+
10178
+ ---
10179
+ Created via \`night-watch prd create\`.`;
10180
+ }
10181
+ async function generatePrdWithClaude(description, projectDir, model) {
10182
+ const bundledTemplatePath = path28.join(TEMPLATES_DIR2, "prd-creator.md");
10183
+ const installedTemplatePath = path28.join(projectDir, "instructions", "prd-creator.md");
10184
+ const templatePath = fs26.existsSync(installedTemplatePath) ? installedTemplatePath : bundledTemplatePath;
10185
+ if (!fs26.existsSync(templatePath)) {
10186
+ return null;
10187
+ }
10188
+ const planningPrinciples = fs26.readFileSync(templatePath, "utf-8");
10189
+ const prompt = buildPrdPrompt(description, projectDir, planningPrinciples);
10190
+ const modelId = model ?? CLAUDE_MODEL_IDS.opus;
10191
+ const env = buildNativeClaudeEnv(process.env);
10192
+ return await new Promise((resolve9) => {
10193
+ const child = spawn4(
10194
+ "claude",
10195
+ [
10196
+ "-p",
10197
+ "--verbose",
10198
+ "--output-format",
10199
+ "stream-json",
10200
+ "--include-partial-messages",
10201
+ "--model",
10202
+ modelId,
10203
+ prompt
10204
+ ],
10205
+ { env, stdio: ["ignore", "pipe", "pipe"] }
10206
+ );
10207
+ let stdoutBuffer = "";
10208
+ let finalResult = "";
10209
+ child.stdout.on("data", (chunk) => {
10210
+ stdoutBuffer += chunk.toString("utf-8");
10211
+ while (stdoutBuffer.includes("\n")) {
10212
+ const newlineIndex = stdoutBuffer.indexOf("\n");
10213
+ const line = stdoutBuffer.slice(0, newlineIndex).trim();
10214
+ stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
10215
+ if (!line) continue;
10216
+ try {
10217
+ const payload = JSON.parse(line);
10218
+ if (payload.type === "stream_event") {
10219
+ const event = payload.event;
10220
+ const delta = event?.delta;
10221
+ if (delta?.type === "text_delta" && typeof delta.text === "string") {
10222
+ process.stdout.write(delta.text);
10223
+ }
10224
+ continue;
10225
+ }
10226
+ if (payload.type === "result" && typeof payload.result === "string") {
10227
+ finalResult = payload.result;
10228
+ }
10229
+ } catch {
10230
+ }
10231
+ }
9756
10232
  });
10233
+ child.stderr.on("data", (chunk) => process.stderr.write(chunk));
10234
+ child.on("close", (code) => {
10235
+ if (stdoutBuffer.trim().length > 0) {
10236
+ try {
10237
+ const payload = JSON.parse(stdoutBuffer.trim());
10238
+ if (payload.type === "result" && typeof payload.result === "string") {
10239
+ finalResult = payload.result;
10240
+ }
10241
+ } catch {
10242
+ }
10243
+ }
10244
+ process.stdout.write("\n");
10245
+ resolve9(code === 0 && finalResult ? extractPrdMarkdown(finalResult) : null);
10246
+ });
10247
+ child.on("error", () => resolve9(null));
9757
10248
  });
9758
10249
  }
10250
+ function runGh(args, cwd) {
10251
+ const result = spawnSync("gh", args, { cwd, encoding: "utf-8" });
10252
+ if (result.status === 0) return (result.stdout ?? "").trim();
10253
+ return null;
10254
+ }
10255
+ function createGithubIssue(title, prdPath, projectDir, prdContent) {
10256
+ const tmpFile = path28.join(projectDir, `.prd-issue-body-${Date.now()}.tmp`);
10257
+ try {
10258
+ const body = buildGithubIssueBody(prdPath, projectDir, prdContent);
10259
+ fs26.writeFileSync(tmpFile, body, "utf-8");
10260
+ const baseArgs = ["issue", "create", "--title", `PRD: ${title}`, "--body-file", tmpFile];
10261
+ return runGh([...baseArgs, "--label", "prd"], projectDir) ?? runGh(baseArgs, projectDir);
10262
+ } catch {
10263
+ return null;
10264
+ } finally {
10265
+ try {
10266
+ fs26.unlinkSync(tmpFile);
10267
+ } catch {
10268
+ }
10269
+ }
10270
+ }
9759
10271
  function parseDependencies(content) {
9760
10272
  const match = content.match(/\*\*Depends on:\*\*\s*(.+)/i) || content.match(/Depends on:\s*(.+)/i);
9761
10273
  if (!match) return [];
@@ -9779,101 +10291,38 @@ function isClaimActive(claimPath, maxRuntime) {
9779
10291
  }
9780
10292
  function prdCommand(program2) {
9781
10293
  const prd = program2.command("prd").description("Manage PRD files");
9782
- prd.command("create").description("Generate a new PRD markdown file from template").argument("<name>", "PRD name (used for title and filename)").option("-i, --interactive", "Prompt for complexity, dependencies, and phase count", false).option("-t, --template <path>", "Path to a custom template file").option("--deps <files>", "Comma-separated dependency filenames").option("--phases <count>", "Number of execution phases", "3").option("--no-number", "Skip auto-numbering prefix").action(async (name, options) => {
10294
+ prd.command("create").description("Generate a new PRD markdown file using Claude").argument("<name>", "PRD description").option("--number", "Add auto-numbering prefix to the filename", false).option("--model <model>", "Claude model to use (e.g. sonnet, opus, or a full model ID)").action(async (name, options) => {
9783
10295
  const projectDir = process.cwd();
9784
- const config = loadConfig(projectDir);
9785
- const prdDir = path28.join(projectDir, config.prdDir);
10296
+ const prdDir = path28.join(projectDir, resolvePrdCreateDir());
9786
10297
  if (!fs26.existsSync(prdDir)) {
9787
10298
  fs26.mkdirSync(prdDir, { recursive: true });
9788
10299
  }
9789
- let complexityScore = 5;
9790
- let dependsOn = [];
9791
- let phaseCount = parseInt(options.phases ?? "3", 10);
9792
- if (isNaN(phaseCount) || phaseCount < 1) {
9793
- phaseCount = 3;
9794
- }
9795
- if (options.deps) {
9796
- dependsOn = options.deps.split(",").map((d) => d.trim()).filter((d) => d.length > 0);
9797
- }
9798
- if (options.interactive) {
9799
- const rl = readline2.createInterface({
9800
- input: process.stdin,
9801
- output: process.stdout
9802
- });
9803
- try {
9804
- const complexityInput = await prompt(rl, "Complexity score (1-10, default 5): ");
9805
- if (complexityInput) {
9806
- const parsed = parseInt(complexityInput, 10);
9807
- if (!isNaN(parsed) && parsed >= 1 && parsed <= 10) {
9808
- complexityScore = parsed;
9809
- }
9810
- }
9811
- const depsInput = await prompt(
9812
- rl,
9813
- "Dependencies (comma-separated filenames, or empty): "
9814
- );
9815
- if (depsInput) {
9816
- dependsOn = depsInput.split(",").map((d) => d.trim()).filter((d) => d.length > 0);
9817
- }
9818
- const phasesInput = await prompt(rl, `Number of phases (default ${phaseCount}): `);
9819
- if (phasesInput) {
9820
- const parsed = parseInt(phasesInput, 10);
9821
- if (!isNaN(parsed) && parsed >= 1) {
9822
- phaseCount = parsed;
9823
- }
9824
- }
9825
- } finally {
9826
- rl.close();
9827
- }
9828
- }
9829
- let complexityLevel;
9830
- if (complexityScore <= 3) {
9831
- complexityLevel = "LOW";
9832
- } else if (complexityScore <= 7) {
9833
- complexityLevel = "MEDIUM";
9834
- } else {
9835
- complexityLevel = "HIGH";
9836
- }
9837
- const slug = slugify2(name);
9838
- let filename;
9839
- if (options.number) {
9840
- const nextNum = getNextPrdNumber2(prdDir);
9841
- const padded = String(nextNum).padStart(2, "0");
9842
- filename = `${padded}-${slug}.md`;
9843
- } else {
9844
- filename = `${slug}.md`;
10300
+ const resolvedModel = options.model ? CLAUDE_MODEL_IDS[options.model] ?? options.model : void 0;
10301
+ const modelLabel = resolvedModel ?? CLAUDE_MODEL_IDS.opus;
10302
+ dim(`Calling Claude (${modelLabel}) to generate the PRD. It can take several minutes, please hang on!
10303
+ `);
10304
+ const generated = await generatePrdWithClaude(name, projectDir, resolvedModel);
10305
+ if (!generated) {
10306
+ error("Claude generation failed. Is the provider configured and available?");
10307
+ process.exit(1);
9845
10308
  }
10309
+ const prdTitle = extractPrdTitle(generated) ?? name;
10310
+ const slug = slugify2(prdTitle);
10311
+ const filename = options.number ? `${String(getNextPrdNumber2(prdDir)).padStart(2, "0")}-${slug}.md` : `${slug}.md`;
9846
10312
  const filePath = path28.join(prdDir, filename);
9847
10313
  if (fs26.existsSync(filePath)) {
9848
10314
  error(`File already exists: ${filePath}`);
9849
10315
  dim("Use a different name or remove the existing file.");
9850
10316
  process.exit(1);
9851
10317
  }
9852
- let customTemplate;
9853
- if (options.template) {
9854
- const templatePath = path28.resolve(options.template);
9855
- if (!fs26.existsSync(templatePath)) {
9856
- error(`Template file not found: ${templatePath}`);
9857
- process.exit(1);
9858
- }
9859
- customTemplate = fs26.readFileSync(templatePath, "utf-8");
9860
- }
9861
- const vars = {
9862
- title: name,
9863
- dependsOn,
9864
- complexityScore,
9865
- complexityLevel,
9866
- complexityBreakdown: [],
9867
- phaseCount
9868
- };
9869
- const content = renderPrdTemplate(vars, customTemplate);
9870
- fs26.writeFileSync(filePath, content, "utf-8");
10318
+ fs26.writeFileSync(filePath, generated, "utf-8");
9871
10319
  header("PRD Created");
9872
10320
  success(`Created: ${filePath}`);
9873
- info(`Title: ${name}`);
9874
- dim(`Phases: ${phaseCount}`);
9875
- if (dependsOn.length > 0) {
9876
- dim(`Dependencies: ${dependsOn.join(", ")}`);
10321
+ const issueUrl = createGithubIssue(prdTitle, filePath, projectDir, generated);
10322
+ if (issueUrl) {
10323
+ info(`Issue: ${issueUrl}`);
10324
+ } else {
10325
+ dim("GitHub issue creation skipped (gh not available or not in a GitHub repo).");
9877
10326
  }
9878
10327
  });
9879
10328
  prd.command("list").description("List all PRDs with status").option("--json", "Output as JSON").action(async (options) => {
@@ -9915,16 +10364,14 @@ function prdCommand(program2) {
9915
10364
  dim(" No PRDs found.");
9916
10365
  return;
9917
10366
  }
9918
- const table = createTable({
9919
- head: ["Name", "Status", "Dependencies"]
9920
- });
9921
- for (const prd2 of pending) {
9922
- const status = prd2.claimed ? "claimed" : "pending";
9923
- const statusDisplay = prd2.claimed && prd2.claimInfo ? `claimed (${prd2.claimInfo.hostname}:${prd2.claimInfo.pid})` : status;
9924
- table.push([prd2.name, statusDisplay, prd2.dependencies.join(", ") || "-"]);
10367
+ const table = createTable({ head: ["Name", "Status", "Dependencies"] });
10368
+ for (const prdEntry of pending) {
10369
+ const status = prdEntry.claimed ? "claimed" : "pending";
10370
+ const statusDisplay = prdEntry.claimed && prdEntry.claimInfo ? `claimed (${prdEntry.claimInfo.hostname}:${prdEntry.claimInfo.pid})` : status;
10371
+ table.push([prdEntry.name, statusDisplay, prdEntry.dependencies.join(", ") || "-"]);
9925
10372
  }
9926
- for (const prd2 of done) {
9927
- table.push([prd2.name, "done", prd2.dependencies.join(", ") || "-"]);
10373
+ for (const prdEntry of done) {
10374
+ table.push([prdEntry.name, "done", prdEntry.dependencies.join(", ") || "-"]);
9928
10375
  }
9929
10376
  console.log(table.toString());
9930
10377
  const claimedCount = pending.filter((p) => p.claimed).length;
@@ -11444,11 +11891,11 @@ function createSchedulesTab() {
11444
11891
 
11445
11892
  // src/commands/dashboard/tab-actions.ts
11446
11893
  import blessed4 from "blessed";
11447
- import { spawn as spawn4 } from "child_process";
11894
+ import { spawn as spawn5 } from "child_process";
11448
11895
  function spawnAction(args, ctx, outputBox, onDone) {
11449
11896
  outputBox.setContent("{cyan-fg}Starting...{/cyan-fg}\n");
11450
11897
  ctx.screen.render();
11451
- const child = spawn4("npx", ["tsx", "src/cli.ts", ...args], {
11898
+ const child = spawn5("npx", ["tsx", "src/cli.ts", ...args], {
11452
11899
  cwd: ctx.projectDir,
11453
11900
  stdio: ["ignore", "pipe", "pipe"],
11454
11901
  env: { ...process.env }
@@ -12159,7 +12606,7 @@ function doctorCommand(program2) {
12159
12606
  program2.command("doctor").description("Check Night Watch configuration and system health").option("--fix", "Automatically fix fixable issues").action(async (options) => {
12160
12607
  const projectDir = process.cwd();
12161
12608
  const config = loadConfig(projectDir);
12162
- const totalChecks = 8;
12609
+ const totalChecks = 7;
12163
12610
  let checkNum = 1;
12164
12611
  let passedChecks = 0;
12165
12612
  let fixedChecks = 0;
@@ -12185,11 +12632,12 @@ function doctorCommand(program2) {
12185
12632
  const ghResult = runCheck(checkNum++, totalChecks, "GitHub CLI", () => checkGhCli(), options);
12186
12633
  if (ghResult.passed) passedChecks++;
12187
12634
  if (ghResult.fixed) fixedChecks++;
12635
+ const resolvedProviderCli = BUILT_IN_PRESETS[config.provider]?.command ?? config.provider;
12188
12636
  const providerResult = runCheck(
12189
12637
  checkNum++,
12190
12638
  totalChecks,
12191
12639
  "provider CLI",
12192
- () => checkProviderCli(config.provider),
12640
+ () => checkProviderCli(resolvedProviderCli),
12193
12641
  options
12194
12642
  );
12195
12643
  if (providerResult.passed) passedChecks++;
@@ -12203,15 +12651,6 @@ function doctorCommand(program2) {
12203
12651
  );
12204
12652
  if (configResult.passed) passedChecks++;
12205
12653
  if (configResult.fixed) fixedChecks++;
12206
- const prdResult = runCheck(
12207
- checkNum++,
12208
- totalChecks,
12209
- "PRD directory",
12210
- () => checkPrdDirectory(projectDir, config.prdDir),
12211
- options
12212
- );
12213
- if (prdResult.passed) passedChecks++;
12214
- if (prdResult.fixed) fixedChecks++;
12215
12654
  const logsResult = runCheck(
12216
12655
  checkNum++,
12217
12656
  totalChecks,
@@ -12269,8 +12708,8 @@ import * as fs33 from "fs";
12269
12708
  init_dist();
12270
12709
  import * as fs32 from "fs";
12271
12710
  import * as path35 from "path";
12272
- import { dirname as dirname8 } from "path";
12273
- import { fileURLToPath as fileURLToPath3 } from "url";
12711
+ import { dirname as dirname11 } from "path";
12712
+ import { fileURLToPath as fileURLToPath5 } from "url";
12274
12713
  import cors from "cors";
12275
12714
  import express from "express";
12276
12715
 
@@ -12411,7 +12850,7 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
12411
12850
  init_dist();
12412
12851
  import * as fs30 from "fs";
12413
12852
  import * as path31 from "path";
12414
- import { execSync as execSync5, spawn as spawn5 } from "child_process";
12853
+ import { execSync as execSync6, spawn as spawn6 } from "child_process";
12415
12854
  import { Router } from "express";
12416
12855
 
12417
12856
  // ../server/dist/helpers.js
@@ -12498,7 +12937,7 @@ function spawnAction2(projectDir, command, req, res, onSpawned) {
12498
12937
  if (prdName) {
12499
12938
  extraEnv.NW_PRD_PRIORITY = prdName;
12500
12939
  }
12501
- const child = spawn5("night-watch", command, {
12940
+ const child = spawn6("night-watch", command, {
12502
12941
  detached: true,
12503
12942
  stdio: "ignore",
12504
12943
  cwd: projectDir,
@@ -12530,7 +12969,7 @@ function formatCommandError(error2) {
12530
12969
  return output || error2.message;
12531
12970
  }
12532
12971
  function runCliCommand(projectDir, args) {
12533
- execSync5(`night-watch ${args.join(" ")}`, {
12972
+ execSync6(`night-watch ${args.join(" ")}`, {
12534
12973
  cwd: projectDir,
12535
12974
  encoding: "utf-8",
12536
12975
  stdio: "pipe",
@@ -12549,18 +12988,16 @@ function createActionRouteHandlers(ctx) {
12549
12988
  router.post(`/${p}review`, (req, res) => {
12550
12989
  spawnAction2(ctx.getProjectDir(req), ["review"], req, res);
12551
12990
  });
12552
- router.post(`/${p}qa`, (req, res) => {
12553
- spawnAction2(ctx.getProjectDir(req), ["qa"], req, res);
12554
- });
12555
- router.post(`/${p}audit`, (req, res) => {
12556
- spawnAction2(ctx.getProjectDir(req), ["audit"], req, res);
12557
- });
12558
- router.post(`/${p}analytics`, (req, res) => {
12559
- spawnAction2(ctx.getProjectDir(req), ["analytics"], req, res);
12560
- });
12561
- router.post(`/${p}planner`, (req, res) => {
12562
- spawnAction2(ctx.getProjectDir(req), ["planner"], req, res);
12563
- });
12991
+ for (const jobDef of JOB_REGISTRY) {
12992
+ if (jobDef.id === "executor")
12993
+ continue;
12994
+ if (jobDef.id === "reviewer")
12995
+ continue;
12996
+ const cmd = jobDef.cliCommand;
12997
+ router.post(`/${p}${cmd}`, (req, res) => {
12998
+ spawnAction2(ctx.getProjectDir(req), [cmd], req, res);
12999
+ });
13000
+ }
12564
13001
  router.post(`/${p}install-cron`, (req, res) => {
12565
13002
  const projectDir = ctx.getProjectDir(req);
12566
13003
  try {
@@ -13363,12 +13800,12 @@ function createProjectConfigRoutes() {
13363
13800
  init_dist();
13364
13801
  import * as fs31 from "fs";
13365
13802
  import * as path32 from "path";
13366
- import { execSync as execSync6 } from "child_process";
13803
+ import { execSync as execSync7 } from "child_process";
13367
13804
  import { Router as Router4 } from "express";
13368
13805
  function runDoctorChecks(projectDir, config) {
13369
13806
  const checks = [];
13370
13807
  try {
13371
- execSync6("git rev-parse --is-inside-work-tree", {
13808
+ execSync7("git rev-parse --is-inside-work-tree", {
13372
13809
  cwd: projectDir,
13373
13810
  stdio: "pipe"
13374
13811
  });
@@ -13379,7 +13816,7 @@ function runDoctorChecks(projectDir, config) {
13379
13816
  try {
13380
13817
  const preset = resolvePreset(config, config.provider);
13381
13818
  const command = preset?.command ?? config.provider;
13382
- execSync6(`which ${command}`, { stdio: "pipe" });
13819
+ execSync7(`which ${command}`, { stdio: "pipe" });
13383
13820
  checks.push({
13384
13821
  name: "provider",
13385
13822
  status: "pass",
@@ -13894,13 +14331,13 @@ function createQueueRoutes(deps) {
13894
14331
  }
13895
14332
 
13896
14333
  // ../server/dist/index.js
13897
- var __filename2 = fileURLToPath3(import.meta.url);
13898
- var __dirname3 = dirname8(__filename2);
14334
+ var __filename4 = fileURLToPath5(import.meta.url);
14335
+ var __dirname4 = dirname11(__filename4);
13899
14336
  function resolveWebDistPath() {
13900
- const bundled = path35.join(__dirname3, "web");
14337
+ const bundled = path35.join(__dirname4, "web");
13901
14338
  if (fs32.existsSync(path35.join(bundled, "index.html")))
13902
14339
  return bundled;
13903
- let d = __dirname3;
14340
+ let d = __dirname4;
13904
14341
  for (let i = 0; i < 8; i++) {
13905
14342
  if (fs32.existsSync(path35.join(d, "turbo.json"))) {
13906
14343
  const dev = path35.join(d, "web/dist");
@@ -13908,7 +14345,7 @@ function resolveWebDistPath() {
13908
14345
  return dev;
13909
14346
  break;
13910
14347
  }
13911
- d = dirname8(d);
14348
+ d = dirname11(d);
13912
14349
  }
13913
14350
  return bundled;
13914
14351
  }
@@ -14033,6 +14470,21 @@ function createGlobalApp() {
14033
14470
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
14034
14471
  }
14035
14472
  });
14473
+ app.delete("/api/projects/:projectId", (req, res) => {
14474
+ try {
14475
+ const rawId = decodeURIComponent(String(req.params.projectId)).replace(/~/g, "/");
14476
+ const entries = loadRegistry();
14477
+ const entry = entries.find((e) => e.name === rawId);
14478
+ if (!entry) {
14479
+ res.status(404).json({ error: "Project not found" });
14480
+ return;
14481
+ }
14482
+ const result = removeProject(entry.path);
14483
+ res.json(result);
14484
+ } catch (error2) {
14485
+ res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
14486
+ }
14487
+ });
14036
14488
  app.use("/api/queue", createGlobalQueueRoutes());
14037
14489
  app.use("/api/global-notifications", createGlobalNotificationsRoutes());
14038
14490
  app.use("/api/projects/:projectId", resolveProject, createProjectRouter());
@@ -14252,7 +14704,7 @@ function historyCommand(program2) {
14252
14704
 
14253
14705
  // src/commands/update.ts
14254
14706
  init_dist();
14255
- import { spawnSync } from "child_process";
14707
+ import { spawnSync as spawnSync2 } from "child_process";
14256
14708
  import * as fs34 from "fs";
14257
14709
  import * as path36 from "path";
14258
14710
  var DEFAULT_GLOBAL_SPEC = "@jonit-dev/night-watch-cli@latest";
@@ -14267,7 +14719,7 @@ function shouldInstallGlobal(options) {
14267
14719
  return options.global !== false;
14268
14720
  }
14269
14721
  function runCommand2(command, args, cwd) {
14270
- const result = spawnSync(command, args, {
14722
+ const result = spawnSync2(command, args, {
14271
14723
  cwd,
14272
14724
  env: process.env,
14273
14725
  stdio: "inherit"
@@ -14281,7 +14733,7 @@ function runCommand2(command, args, cwd) {
14281
14733
  }
14282
14734
  }
14283
14735
  function resolveNightWatchBin() {
14284
- const result = spawnSync("which", ["night-watch"], {
14736
+ const result = spawnSync2("which", ["night-watch"], {
14285
14737
  encoding: "utf-8",
14286
14738
  env: process.env
14287
14739
  });
@@ -14479,10 +14931,10 @@ function prsCommand(program2) {
14479
14931
  // src/commands/prds.ts
14480
14932
  init_dist();
14481
14933
  import chalk4 from "chalk";
14482
- import { execSync as execSync7 } from "child_process";
14934
+ import { execSync as execSync8 } from "child_process";
14483
14935
  function getOpenPrBranches(projectDir) {
14484
14936
  try {
14485
- execSync7("git rev-parse --git-dir", {
14937
+ execSync8("git rev-parse --git-dir", {
14486
14938
  cwd: projectDir,
14487
14939
  encoding: "utf-8",
14488
14940
  stdio: ["pipe", "pipe", "pipe"]
@@ -14491,12 +14943,12 @@ function getOpenPrBranches(projectDir) {
14491
14943
  return /* @__PURE__ */ new Set();
14492
14944
  }
14493
14945
  try {
14494
- execSync7("which gh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
14946
+ execSync8("which gh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
14495
14947
  } catch {
14496
14948
  return /* @__PURE__ */ new Set();
14497
14949
  }
14498
14950
  try {
14499
- const output = execSync7("gh pr list --state open --json headRefName", {
14951
+ const output = execSync8("gh pr list --state open --json headRefName", {
14500
14952
  cwd: projectDir,
14501
14953
  encoding: "utf-8",
14502
14954
  stdio: ["pipe", "pipe", "pipe"]
@@ -14627,7 +15079,7 @@ function prdsCommand(program2) {
14627
15079
  // src/commands/cancel.ts
14628
15080
  init_dist();
14629
15081
  import * as fs36 from "fs";
14630
- import * as readline3 from "readline";
15082
+ import * as readline2 from "readline";
14631
15083
  function getLockFilePaths2(projectDir) {
14632
15084
  const runtimeKey = projectRuntimeKey(projectDir);
14633
15085
  return {
@@ -14635,24 +15087,24 @@ function getLockFilePaths2(projectDir) {
14635
15087
  reviewer: `${LOCK_FILE_PREFIX}pr-reviewer-${runtimeKey}.lock`
14636
15088
  };
14637
15089
  }
14638
- async function promptConfirmation(prompt2) {
15090
+ async function promptConfirmation(prompt) {
14639
15091
  if (!process.stdin.isTTY) {
14640
15092
  return false;
14641
15093
  }
14642
- const rl = readline3.createInterface({
15094
+ const rl = readline2.createInterface({
14643
15095
  input: process.stdin,
14644
15096
  output: process.stdout
14645
15097
  });
14646
- return new Promise((resolve10) => {
14647
- rl.question(`${prompt2} `, (answer) => {
15098
+ return new Promise((resolve9) => {
15099
+ rl.question(`${prompt} `, (answer) => {
14648
15100
  rl.close();
14649
15101
  const normalized = answer.toLowerCase().trim();
14650
- resolve10(normalized === "y" || normalized === "yes");
15102
+ resolve9(normalized === "y" || normalized === "yes");
14651
15103
  });
14652
15104
  });
14653
15105
  }
14654
15106
  function sleep2(ms) {
14655
- return new Promise((resolve10) => setTimeout(resolve10, ms));
15107
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
14656
15108
  }
14657
15109
  function isProcessRunning3(pid) {
14658
15110
  try {
@@ -15134,7 +15586,7 @@ init_dist();
15134
15586
  import { execFileSync as execFileSync6 } from "child_process";
15135
15587
  import * as fs38 from "fs";
15136
15588
  import * as path40 from "path";
15137
- import * as readline4 from "readline";
15589
+ import * as readline3 from "readline";
15138
15590
  import chalk6 from "chalk";
15139
15591
  async function run(fn) {
15140
15592
  try {
@@ -15182,14 +15634,14 @@ async function ensureBoardConfigured(config, cwd, provider, options) {
15182
15634
  }
15183
15635
  }
15184
15636
  async function confirmPrompt(question) {
15185
- const rl = readline4.createInterface({
15637
+ const rl = readline3.createInterface({
15186
15638
  input: process.stdin,
15187
15639
  output: process.stdout
15188
15640
  });
15189
- return new Promise((resolve10) => {
15641
+ return new Promise((resolve9) => {
15190
15642
  rl.question(question, (answer) => {
15191
15643
  rl.close();
15192
- resolve10(answer.trim().toLowerCase() === "y");
15644
+ resolve9(answer.trim().toLowerCase() === "y");
15193
15645
  });
15194
15646
  });
15195
15647
  }
@@ -15729,7 +16181,7 @@ function boardCommand(program2) {
15729
16181
  init_dist();
15730
16182
  init_dist();
15731
16183
  import * as path41 from "path";
15732
- import { spawn as spawn6 } from "child_process";
16184
+ import { spawn as spawn7 } from "child_process";
15733
16185
  import chalk7 from "chalk";
15734
16186
  import { Command as Command2 } from "commander";
15735
16187
  var logger4 = createLogger("queue");
@@ -15883,7 +16335,7 @@ function createQueueCommand() {
15883
16335
  const scriptPath = getScriptPath(scriptName);
15884
16336
  logger4.info(`Spawning: ${scriptPath} ${entry.projectPath}`);
15885
16337
  try {
15886
- const child = spawn6("bash", [scriptPath, entry.projectPath], {
16338
+ const child = spawn7("bash", [scriptPath, entry.projectPath], {
15887
16339
  detached: true,
15888
16340
  stdio: "ignore",
15889
16341
  env,
@@ -16003,17 +16455,17 @@ function notifyCommand(program2) {
16003
16455
  }
16004
16456
 
16005
16457
  // src/cli.ts
16006
- var __filename3 = fileURLToPath4(import.meta.url);
16007
- var __dirname4 = dirname9(__filename3);
16458
+ var __filename5 = fileURLToPath6(import.meta.url);
16459
+ var __dirname5 = dirname12(__filename5);
16008
16460
  function findPackageRoot(dir) {
16009
16461
  let d = dir;
16010
16462
  for (let i = 0; i < 5; i++) {
16011
16463
  if (existsSync30(join36(d, "package.json"))) return d;
16012
- d = dirname9(d);
16464
+ d = dirname12(d);
16013
16465
  }
16014
16466
  return dir;
16015
16467
  }
16016
- var packageRoot = findPackageRoot(__dirname4);
16468
+ var packageRoot = findPackageRoot(__dirname5);
16017
16469
  var packageJson = JSON.parse(readFileSync19(join36(packageRoot, "package.json"), "utf-8"));
16018
16470
  var program = new Command3();
16019
16471
  program.name("night-watch").description("Autonomous PRD execution using Claude CLI + cron").version(packageJson.version);