workflow-ai 1.0.58 → 1.0.59

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.
@@ -552,6 +552,8 @@ pipeline:
552
552
  protected_files:
553
553
  - ".workflow/plans/**" # Планы — только для чтения
554
554
  - ".workflow/config/**" # Конфигурация
555
+ - pattern: ".workflow/tickets/**"
556
+ mode: structure # Защита только структуры (создание/удаление файлов)
555
557
 
556
558
  # ===========================================================================
557
559
  # Доверенные агенты (trusted_agents)
@@ -561,6 +563,17 @@ pipeline:
561
563
  # ===========================================================================
562
564
  trusted_agents:
563
565
  - "script-check-templates" # Создаёт планы в plans/current/
566
+ - "script-*" # Все скрипты
567
+
568
+ # ===========================================================================
569
+ # Доверенные стейджи (trusted_stages)
570
+ # ===========================================================================
571
+ # Стейджи, которые могут создавать/удалять файлы в protected_files.
572
+ # Например, decompose-plan и decompose-gaps создают тикеты.
573
+ # ===========================================================================
574
+ trusted_stages:
575
+ - "decompose-plan"
576
+ - "decompose-gaps"
564
577
 
565
578
  # ===========================================================================
566
579
  # Настройки выполнения (execution)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow-ai",
3
- "version": "1.0.58",
3
+ "version": "1.0.59",
4
4
  "description": "AI Agent Workflow Coordinator — kanban-based pipeline for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/src/runner.mjs CHANGED
@@ -543,30 +543,46 @@ class ResultParser {
543
543
  // FileGuard — защита файлов от несанкционированного изменения агентами
544
544
  // ============================================================================
545
545
  class FileGuard {
546
- constructor(patterns, projectRoot = process.cwd(), trustedAgents = []) {
547
- // Паттерны указаны относительно корня проекта
548
- this.patterns = (patterns || []).map(p => p.replace(/\\/g, '/'));
549
- this.enabled = this.patterns.length > 0;
546
+ constructor(patterns, projectRoot = process.cwd(), trustedAgents = [], trustedStages = []) {
547
+ this.enabled = patterns && patterns.length > 0;
550
548
  this.snapshots = new Map();
549
+ this.patterns = (patterns || []).map(p => {
550
+ if (typeof p === 'string') {
551
+ return { pattern: p.replace(/\\/g, '/'), mode: 'full' };
552
+ }
553
+ return { pattern: p.pattern.replace(/\\/g, '/'), mode: p.mode || 'full' };
554
+ });
551
555
  // projectRoot — корневая директория проекта, относительно которой указаны паттерны
552
556
  this.projectRoot = projectRoot;
553
557
  // Доверенные агенты — для них FileGuard не откатывает изменения
554
558
  this.trustedAgents = trustedAgents;
559
+ // Доверенные стейджи — для них FileGuard не откатывает изменения
560
+ this.trustedStages = trustedStages;
555
561
  }
556
562
 
557
563
  /**
558
- * Проверяет, является ли агент доверенным (пропускает FileGuard)
564
+ * Проверяет, является ли агент или стейдж доверенным (пропускает FileGuard)
559
565
  * Поддерживает glob-паттерны: "script-*" соответствует "script-move", "script-pick" и т.д.
560
566
  * @param {string} agentId - ID агента
567
+ * @param {string} [stageId] - ID стейджа (опционально)
561
568
  * @returns {boolean}
562
569
  */
563
- isTrusted(agentId) {
564
- return this.trustedAgents.some(pattern => {
570
+ isTrusted(agentId, stageId) {
571
+ // Проверка по trustedAgents (glob-паттерны)
572
+ const agentMatch = this.trustedAgents.some(pattern => {
565
573
  if (pattern.endsWith('*')) {
566
574
  return agentId.startsWith(pattern.slice(0, -1));
567
575
  }
568
576
  return agentId === pattern;
569
577
  });
578
+ if (agentMatch) return true;
579
+
580
+ // Проверка по trustedStages (точное совпадение)
581
+ if (stageId && this.trustedStages.includes(stageId)) {
582
+ return true;
583
+ }
584
+
585
+ return false;
570
586
  }
571
587
 
572
588
  /**
@@ -575,9 +591,8 @@ class FileGuard {
575
591
  * @returns {boolean}
576
592
  */
577
593
  matchesProtected(filePath) {
578
- // Получаем относительный путь от projectRoot для сопоставления с паттерном
579
594
  const relativePath = path.relative(this.projectRoot, filePath).replace(/\\/g, '/');
580
- return this.patterns.some(pattern => this._matchGlob(relativePath, pattern));
595
+ return this.patterns.some(p => this._matchGlob(relativePath, p.pattern));
581
596
  }
582
597
 
583
598
  /**
@@ -649,20 +664,26 @@ class FileGuard {
649
664
  if (!this.enabled) return;
650
665
  this.snapshots.clear();
651
666
 
652
- for (const pattern of this.patterns) {
667
+ for (const { pattern, mode } of this.patterns) {
653
668
  if (!pattern.includes('*')) {
654
- // Прямой путь к файлу — преобразуем в абсолютный
655
669
  const absolutePath = path.resolve(this.projectRoot, pattern);
656
670
  if (fs.existsSync(absolutePath)) {
657
- this.snapshots.set(absolutePath, this._hashFile(absolutePath));
671
+ if (mode === 'structure') {
672
+ this.snapshots.set(absolutePath, { hash: this._hashFile(absolutePath), content: fs.readFileSync(absolutePath, null), mode: 'structure' });
673
+ } else {
674
+ this.snapshots.set(absolutePath, this._hashFile(absolutePath));
675
+ }
658
676
  }
659
677
  } else {
660
- // Glob-паттерн: сканируем базовую директорию относительно projectRoot
661
678
  const baseDir = path.resolve(this.projectRoot, this._getBaseDir(pattern));
662
679
  const files = this._getAllFiles(baseDir);
663
680
  for (const filePath of files) {
664
681
  if (this.matchesProtected(filePath)) {
665
- this.snapshots.set(filePath, this._hashFile(filePath));
682
+ if (mode === 'structure') {
683
+ this.snapshots.set(filePath, { hash: this._hashFile(filePath), content: fs.readFileSync(filePath, null), mode: 'structure' });
684
+ } else {
685
+ this.snapshots.set(filePath, this._hashFile(filePath));
686
+ }
666
687
  }
667
688
  }
668
689
  }
@@ -681,18 +702,30 @@ class FileGuard {
681
702
 
682
703
  const violations = [];
683
704
 
684
- // 1. Проверяем файлы из снимка (изменённые или удалённые)
685
- for (const [filePath, originalHash] of this.snapshots) {
686
- const currentHash = this._hashFile(filePath);
687
- if (currentHash !== originalHash) {
688
- violations.push(filePath);
689
- console.warn(`[FileGuard] WARNING: Protected file modified: ${filePath}`);
690
- this._rollbackFile(filePath);
705
+ for (const [filePath, snapshot] of this.snapshots) {
706
+ const mode = snapshot.mode || 'full';
707
+ if (mode === 'structure') {
708
+ if (!fs.existsSync(filePath)) {
709
+ violations.push(filePath);
710
+ console.warn(`[FileGuard] WARNING: Protected file deleted: ${filePath}`);
711
+ try {
712
+ fs.writeFileSync(filePath, snapshot.content);
713
+ console.warn(`[FileGuard] WARNING: Restored deleted file: ${filePath}`);
714
+ } catch (err) {
715
+ console.error(`[FileGuard] ERROR: Failed to restore ${filePath}: ${err.message}`);
716
+ }
717
+ }
718
+ } else {
719
+ const currentHash = this._hashFile(filePath);
720
+ if (currentHash !== snapshot) {
721
+ violations.push(filePath);
722
+ console.warn(`[FileGuard] WARNING: Protected file modified: ${filePath}`);
723
+ this._rollbackFile(filePath);
724
+ }
691
725
  }
692
726
  }
693
727
 
694
- // 2. Обнаруживаем новые файлы в защищённых директориях
695
- for (const pattern of this.patterns) {
728
+ for (const { pattern, mode } of this.patterns) {
696
729
  const baseDir = pattern.includes('*')
697
730
  ? path.resolve(this.projectRoot, this._getBaseDir(pattern))
698
731
  : path.resolve(this.projectRoot, pattern);
@@ -848,8 +881,8 @@ class StageExecutor {
848
881
  console.log(` Skill: ${stage.skill}`);
849
882
  }
850
883
 
851
- // Снимаем snapshot защищённых файлов перед выполнением (кроме trusted agents)
852
- const skipGuard = this.fileGuard && this.fileGuard.isTrusted(agentId);
884
+ // Снимаем snapshot защищённых файлов перед выполнением (кроме trusted agents и trusted stages)
885
+ const skipGuard = this.fileGuard && this.fileGuard.isTrusted(agentId, stageId);
853
886
  if (this.fileGuard && !skipGuard) {
854
887
  this.fileGuard.takeSnapshot();
855
888
  }
@@ -1195,7 +1228,8 @@ class PipelineRunner {
1195
1228
  // Инициализация FileGuard для защиты файлов от изменений агентами
1196
1229
  const protectedPatterns = this.pipeline.protected_files || [];
1197
1230
  const trustedAgents = this.pipeline.trusted_agents || [];
1198
- this.fileGuard = new FileGuard(protectedPatterns, projectRoot, trustedAgents);
1231
+ const trustedStages = this.pipeline.trusted_stages || [];
1232
+ this.fileGuard = new FileGuard(protectedPatterns, projectRoot, trustedAgents, trustedStages);
1199
1233
  this.projectRoot = projectRoot;
1200
1234
  this.currentExecutor = null;
1201
1235
 
@@ -543,6 +543,25 @@ function pickNextTicket(planId) {
543
543
  return false;
544
544
  }
545
545
 
546
+ // Обнаружение и удаление дубликатов: тикет не должен существовать в других колонках
547
+ const ticketFileName = `${ticket.id}.md`;
548
+ const otherDirs = [DONE_DIR, IN_PROGRESS_DIR, REVIEW_DIR, BLOCKED_DIR];
549
+ const duplicateDir = otherDirs.find(dir =>
550
+ fs.existsSync(path.join(dir, ticketFileName))
551
+ );
552
+ if (duplicateDir) {
553
+ const dirName = path.basename(duplicateDir);
554
+ logger.warn(`Duplicate detected: ${ticket.id} exists in ready/ and ${dirName}/. Moving ready/ copy to archive/`);
555
+ const archivePath = path.join(ARCHIVE_DIR, ticketFileName);
556
+ try {
557
+ fs.mkdirSync(ARCHIVE_DIR, { recursive: true });
558
+ fs.renameSync(ticket.filePath, archivePath);
559
+ } catch (err) {
560
+ logger.error(`Failed to archive duplicate ${ticket.id}: ${err.message}`);
561
+ }
562
+ return false;
563
+ }
564
+
546
565
  return true;
547
566
  });
548
567