kanbango 2.4.0 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/kanban.js CHANGED
@@ -3,10 +3,6 @@ const path = require('path');
3
3
 
4
4
  const BACKLOG = path.join(process.cwd(), 'backlog');
5
5
  const COLS = ['active', 'planned', 'icebox', 'done'];
6
- const GUI_PORT_FILE = '.kanbango-gui.json';
7
- const GUI_PORT_MIN = 5510;
8
- const GUI_PORT_MAX = 5999;
9
- const GUI_PORT_SPAN = GUI_PORT_MAX - GUI_PORT_MIN + 1;
10
6
  const STATUS_MAP = {
11
7
  active: 'in_progress',
12
8
  planned: 'planned',
@@ -62,6 +58,16 @@ const VIEW_FIELDS = {
62
58
  ]
63
59
  };
64
60
 
61
+ // Hard-required on create: title only (keeps GUI/CLI quick-add usable).
62
+ // Strongly recommended for agent/planned work — missing ones yield warnings, not errors.
63
+ const RECOMMENDED_CREATE_FIELDS = [
64
+ 'description',
65
+ 'specs',
66
+ 'in_scope',
67
+ 'out_of_scope',
68
+ 'acceptance_criteria'
69
+ ];
70
+
65
71
  function createKanbanError(code, message, hint, details = {}, retryable = false, status = 400) {
66
72
  const error = new Error(message);
67
73
  error.code = code;
@@ -96,6 +102,36 @@ function normalizeStringArray(value) {
96
102
  .filter(Boolean);
97
103
  }
98
104
 
105
+ function isPresentCreateField(field, value) {
106
+ if (field === 'description' || field === 'specs' || field === 'notes') {
107
+ return Boolean(normalizeString(value));
108
+ }
109
+ if (
110
+ field === 'in_scope' ||
111
+ field === 'out_of_scope' ||
112
+ field === 'acceptance_criteria' ||
113
+ field === 'test_cases' ||
114
+ field === 'subtasks'
115
+ ) {
116
+ if (field === 'subtasks') return normalizeSubtasks(value).length > 0;
117
+ return normalizeStringArray(value).length > 0;
118
+ }
119
+ return value !== undefined && value !== null && value !== '';
120
+ }
121
+
122
+ function missingRecommendedCreateFields(payload = {}) {
123
+ return RECOMMENDED_CREATE_FIELDS.filter((field) => !isPresentCreateField(field, payload[field]));
124
+ }
125
+
126
+ function createFieldWarnings(payload = {}) {
127
+ const missing = missingRecommendedCreateFields(payload);
128
+ if (missing.length === 0) return [];
129
+ return [
130
+ `Strongly recommended fields missing: ${missing.join(', ')}. ` +
131
+ 'Fill them on create (or via update) so scope and done-criteria are explicit.'
132
+ ];
133
+ }
134
+
99
135
  function normalizeSubtasks(value) {
100
136
  if (!Array.isArray(value)) return [];
101
137
  return value.map((subtask, idx) => ({
@@ -343,7 +379,7 @@ async function findFile(epicId) {
343
379
  const candidates = files
344
380
  .filter((file) => (file.endsWith('.json') || file.endsWith('.md'))
345
381
  && path.basename(file, path.extname(file)) === epicId)
346
- .sort((left, right) => (left.endsWith('.json') ? -1 : 1));
382
+ .sort((left, _right) => (left.endsWith('.json') ? -1 : 1));
347
383
  if (candidates[0]) {
348
384
  return path.join(colDir, candidates[0]);
349
385
  }
@@ -524,11 +560,6 @@ async function doCreate(title, column = 'planned', epicGroup = '—', extra = {}
524
560
  validateColumn(column, 'col');
525
561
 
526
562
  const nextId = await nextTaskNumber();
527
- const slug = title
528
- .toLowerCase()
529
- .replace(/[^a-z0-9]+/g, '-')
530
- .replace(/^-+|-+$/g, '')
531
- .substring(0, 25);
532
563
  const task = normalizeTask({
533
564
  id: String(nextId).padStart(3, '0'),
534
565
  title,
@@ -729,118 +760,6 @@ async function doUpdate(epicId, newTitle, newTasks) {
729
760
  }
730
761
  }
731
762
 
732
- function guiPortFilePath() {
733
- return path.join(BACKLOG, GUI_PORT_FILE);
734
- }
735
-
736
- function hashCwdToPort(cwd = process.cwd()) {
737
- let hash = 0;
738
- const input = String(cwd);
739
- for (let i = 0; i < input.length; i++) {
740
- hash = ((hash << 5) - hash + input.charCodeAt(i)) | 0;
741
- }
742
- return GUI_PORT_MIN + (Math.abs(hash) % GUI_PORT_SPAN);
743
- }
744
-
745
- function normalizeGuiPort(value) {
746
- const parsed = Number.parseInt(value, 10);
747
- if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
748
- return null;
749
- }
750
- return parsed;
751
- }
752
-
753
- function resolvePreferredGuiPort(explicitPort) {
754
- if (explicitPort !== undefined && explicitPort !== null && explicitPort !== '') {
755
- const fromArg = normalizeGuiPort(explicitPort);
756
- if (fromArg) return fromArg;
757
- }
758
-
759
- const fromEnv = normalizeGuiPort(process.env.KANBANGO_GUI_PORT);
760
- if (fromEnv) return fromEnv;
761
-
762
- return hashCwdToPort(process.cwd());
763
- }
764
-
765
- function isPidAlive(pid) {
766
- const n = Number.parseInt(pid, 10);
767
- if (!Number.isFinite(n) || n <= 0) return false;
768
- try {
769
- process.kill(n, 0);
770
- return true;
771
- } catch {
772
- return false;
773
- }
774
- }
775
-
776
- async function writeGuiPortFile({ port, pid = process.pid } = {}) {
777
- const normalizedPort = normalizeGuiPort(port);
778
- if (!normalizedPort) {
779
- throw createKanbanError(
780
- 'VALIDATION_ERROR',
781
- 'Invalid GUI port',
782
- 'Use an integer between 1 and 65535',
783
- { port },
784
- false,
785
- 400
786
- );
787
- }
788
-
789
- await ensureBacklogDir();
790
- const data = {
791
- port: normalizedPort,
792
- pid,
793
- url: `http://localhost:${normalizedPort}`,
794
- cwd: process.cwd(),
795
- started_at: new Date().toISOString()
796
- };
797
- await fs.writeFile(guiPortFilePath(), JSON.stringify(data, null, 2), 'utf-8');
798
- return data;
799
- }
800
-
801
- async function readGuiPortFile() {
802
- try {
803
- const raw = await fs.readFile(guiPortFilePath(), 'utf-8');
804
- const data = JSON.parse(raw);
805
- if (!data || !normalizeGuiPort(data.port)) return null;
806
- return data;
807
- } catch (error) {
808
- if (error.code === 'ENOENT') return null;
809
- return null;
810
- }
811
- }
812
-
813
- async function clearGuiPortFile({ pid, force = false } = {}) {
814
- const info = await readGuiPortFile();
815
- if (!info) return false;
816
- if (!force && pid !== undefined && info.pid !== pid) return false;
817
- if (!force && pid === undefined && info.pid !== process.pid) return false;
818
-
819
- try {
820
- await fs.unlink(guiPortFilePath());
821
- return true;
822
- } catch (error) {
823
- if (error.code === 'ENOENT') return false;
824
- throw error;
825
- }
826
- }
827
-
828
- async function discoverRunningGui() {
829
- const info = await readGuiPortFile();
830
- if (!info || !isPidAlive(info.pid)) {
831
- if (info) await clearGuiPortFile({ force: true });
832
- return null;
833
- }
834
- return {
835
- status: 'running',
836
- port: info.port,
837
- pid: info.pid,
838
- url: info.url || `http://localhost:${info.port}`,
839
- cwd: info.cwd,
840
- started_at: info.started_at
841
- };
842
- }
843
-
844
763
  module.exports = {
845
764
  ensureBacklogDir,
846
765
  parseEpic,
@@ -855,20 +774,12 @@ module.exports = {
855
774
  doUpdate,
856
775
  doCreate,
857
776
  createKanbanError,
777
+ createFieldWarnings,
778
+ missingRecommendedCreateFields,
858
779
  getProgress,
859
780
  resolveTaskId,
860
- hashCwdToPort,
861
- normalizeGuiPort,
862
- resolvePreferredGuiPort,
863
- isPidAlive,
864
- writeGuiPortFile,
865
- readGuiPortFile,
866
- clearGuiPortFile,
867
- discoverRunningGui,
868
- guiPortFilePath,
869
781
  COLS,
870
782
  STATUS_MAP,
871
783
  VIEW_FIELDS,
872
- GUI_PORT_MIN,
873
- GUI_PORT_MAX
784
+ RECOMMENDED_CREATE_FIELDS
874
785
  };