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.
- package/configs/pipeline.yaml +13 -0
- package/package.json +1 -1
- package/src/runner.mjs +60 -26
- package/src/scripts/pick-next-task.js +19 -0
package/configs/pipeline.yaml
CHANGED
|
@@ -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
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|