agentsys 5.0.2 → 5.1.0
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/.claude-plugin/marketplace.json +21 -14
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +24 -1
- package/README.md +7 -6
- package/adapters/codex/skills/agnix/SKILL.md +0 -1
- package/adapters/codex/skills/audit-project/SKILL.md +0 -1
- package/adapters/codex/skills/audit-project-agents/SKILL.md +0 -1
- package/adapters/codex/skills/audit-project-github/SKILL.md +0 -1
- package/adapters/codex/skills/consult/SKILL.md +133 -59
- package/adapters/codex/skills/debate/SKILL.md +214 -0
- package/adapters/codex/skills/delivery-approval/SKILL.md +0 -1
- package/adapters/codex/skills/deslop/SKILL.md +0 -1
- package/adapters/codex/skills/drift-detect/SKILL.md +0 -1
- package/adapters/codex/skills/enhance/SKILL.md +0 -1
- package/adapters/codex/skills/learn/SKILL.md +0 -1
- package/adapters/codex/skills/next-task/SKILL.md +0 -1
- package/adapters/codex/skills/perf/SKILL.md +0 -1
- package/adapters/codex/skills/repo-map/SKILL.md +0 -1
- package/adapters/codex/skills/ship/SKILL.md +0 -1
- package/adapters/codex/skills/ship-ci-review-loop/SKILL.md +0 -1
- package/adapters/codex/skills/ship-deployment/SKILL.md +0 -1
- package/adapters/codex/skills/ship-error-handling/SKILL.md +0 -1
- package/adapters/codex/skills/sync-docs/SKILL.md +0 -1
- package/adapters/opencode/agents/agent-enhancer.md +0 -1
- package/adapters/opencode/agents/agnix-agent.md +0 -1
- package/adapters/opencode/agents/ci-fixer.md +0 -1
- package/adapters/opencode/agents/ci-monitor.md +0 -1
- package/adapters/opencode/agents/claudemd-enhancer.md +0 -1
- package/adapters/opencode/agents/consult-agent.md +123 -31
- package/adapters/opencode/agents/cross-file-enhancer.md +0 -1
- package/adapters/opencode/agents/debate-orchestrator.md +169 -0
- package/adapters/opencode/agents/delivery-validator.md +0 -1
- package/adapters/opencode/agents/deslop-agent.md +0 -1
- package/adapters/opencode/agents/docs-enhancer.md +0 -1
- package/adapters/opencode/agents/exploration-agent.md +0 -1
- package/adapters/opencode/agents/hooks-enhancer.md +0 -1
- package/adapters/opencode/agents/implementation-agent.md +0 -1
- package/adapters/opencode/agents/learn-agent.md +0 -1
- package/adapters/opencode/agents/map-validator.md +0 -1
- package/adapters/opencode/agents/perf-analyzer.md +0 -1
- package/adapters/opencode/agents/perf-code-paths.md +0 -1
- package/adapters/opencode/agents/perf-investigation-logger.md +0 -1
- package/adapters/opencode/agents/perf-orchestrator.md +0 -1
- package/adapters/opencode/agents/perf-theory-gatherer.md +0 -1
- package/adapters/opencode/agents/perf-theory-tester.md +0 -1
- package/adapters/opencode/agents/plan-synthesizer.md +0 -1
- package/adapters/opencode/agents/planning-agent.md +0 -1
- package/adapters/opencode/agents/plugin-enhancer.md +0 -1
- package/adapters/opencode/agents/prompt-enhancer.md +0 -1
- package/adapters/opencode/agents/simple-fixer.md +0 -1
- package/adapters/opencode/agents/skills-enhancer.md +0 -1
- package/adapters/opencode/agents/sync-docs-agent.md +0 -1
- package/adapters/opencode/agents/task-discoverer.md +0 -1
- package/adapters/opencode/agents/test-coverage-checker.md +0 -1
- package/adapters/opencode/agents/worktree-manager.md +0 -1
- package/adapters/opencode/commands/agnix.md +0 -1
- package/adapters/opencode/commands/audit-project-agents.md +0 -1
- package/adapters/opencode/commands/audit-project-github.md +0 -1
- package/adapters/opencode/commands/audit-project.md +0 -1
- package/adapters/opencode/commands/consult.md +134 -59
- package/adapters/opencode/commands/debate.md +224 -0
- package/adapters/opencode/commands/delivery-approval.md +0 -1
- package/adapters/opencode/commands/deslop.md +0 -1
- package/adapters/opencode/commands/drift-detect.md +0 -1
- package/adapters/opencode/commands/enhance.md +0 -1
- package/adapters/opencode/commands/learn.md +0 -1
- package/adapters/opencode/commands/next-task.md +0 -1
- package/adapters/opencode/commands/perf.md +0 -1
- package/adapters/opencode/commands/repo-map.md +0 -1
- package/adapters/opencode/commands/ship-ci-review-loop.md +0 -1
- package/adapters/opencode/commands/ship-deployment.md +0 -1
- package/adapters/opencode/commands/ship-error-handling.md +0 -1
- package/adapters/opencode/commands/ship.md +0 -1
- package/adapters/opencode/commands/sync-docs.md +0 -1
- package/adapters/opencode/skills/agnix/SKILL.md +1 -2
- package/adapters/opencode/skills/consult/SKILL.md +41 -27
- package/adapters/opencode/skills/debate/SKILL.md +245 -0
- package/adapters/opencode/skills/deslop/SKILL.md +1 -2
- package/adapters/opencode/skills/discover-tasks/SKILL.md +1 -2
- package/adapters/opencode/skills/drift-analysis/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-agent-prompts/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-claude-memory/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-cross-file/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-docs/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-hooks/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-orchestrator/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-plugins/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-prompts/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-skills/SKILL.md +1 -2
- package/adapters/opencode/skills/learn/SKILL.md +1 -2
- package/adapters/opencode/skills/orchestrate-review/SKILL.md +0 -1
- package/adapters/opencode/skills/perf-analyzer/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-baseline-manager/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-benchmarker/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-code-paths/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-investigation-logger/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-profiler/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-theory-gatherer/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-theory-tester/SKILL.md +1 -2
- package/adapters/opencode/skills/repo-mapping/SKILL.md +1 -2
- package/adapters/opencode/skills/sync-docs/SKILL.md +1 -2
- package/adapters/opencode/skills/validate-delivery/SKILL.md +1 -2
- package/lib/adapter-transforms.js +24 -4
- package/package.json +1 -1
- package/plugins/agnix/.claude-plugin/plugin.json +1 -1
- package/plugins/agnix/skills/agnix/SKILL.md +1 -1
- package/plugins/audit-project/.claude-plugin/plugin.json +1 -1
- package/plugins/audit-project/lib/adapter-transforms.js +24 -4
- package/plugins/consult/.claude-plugin/plugin.json +1 -1
- package/plugins/consult/agents/consult-agent.md +123 -30
- package/plugins/consult/commands/consult.md +136 -60
- package/plugins/consult/skills/consult/SKILL.md +39 -24
- package/plugins/debate/.claude-plugin/plugin.json +21 -0
- package/plugins/debate/agents/debate-orchestrator.md +175 -0
- package/plugins/debate/commands/debate.md +221 -0
- package/plugins/debate/lib/adapter-transforms.js +298 -0
- package/plugins/debate/lib/collectors/codebase.js +392 -0
- package/plugins/debate/lib/collectors/docs-patterns.js +713 -0
- package/plugins/debate/lib/collectors/documentation.js +219 -0
- package/plugins/debate/lib/collectors/github.js +330 -0
- package/plugins/debate/lib/collectors/index.js +126 -0
- package/plugins/debate/lib/config/index.js +14 -0
- package/plugins/debate/lib/cross-platform/index.js +539 -0
- package/plugins/debate/lib/discovery/index.js +352 -0
- package/plugins/debate/lib/drift-detect/collectors.js +37 -0
- package/plugins/debate/lib/enhance/agent-analyzer.js +421 -0
- package/plugins/debate/lib/enhance/agent-patterns.js +571 -0
- package/plugins/debate/lib/enhance/auto-suppression.js +622 -0
- package/plugins/debate/lib/enhance/benchmark.js +417 -0
- package/plugins/debate/lib/enhance/cross-file-analyzer.js +930 -0
- package/plugins/debate/lib/enhance/cross-file-patterns.js +370 -0
- package/plugins/debate/lib/enhance/docs-analyzer.js +325 -0
- package/plugins/debate/lib/enhance/docs-patterns.js +671 -0
- package/plugins/debate/lib/enhance/fixer.js +721 -0
- package/plugins/debate/lib/enhance/hook-analyzer.js +135 -0
- package/plugins/debate/lib/enhance/hook-patterns.js +40 -0
- package/plugins/debate/lib/enhance/index.js +127 -0
- package/plugins/debate/lib/enhance/plugin-analyzer.js +402 -0
- package/plugins/debate/lib/enhance/plugin-patterns.js +326 -0
- package/plugins/debate/lib/enhance/projectmemory-analyzer.js +551 -0
- package/plugins/debate/lib/enhance/projectmemory-patterns.js +617 -0
- package/plugins/debate/lib/enhance/prompt-analyzer.js +457 -0
- package/plugins/debate/lib/enhance/prompt-patterns.js +1484 -0
- package/plugins/debate/lib/enhance/reporter.js +1348 -0
- package/plugins/debate/lib/enhance/security-patterns.js +284 -0
- package/plugins/debate/lib/enhance/skill-analyzer.js +182 -0
- package/plugins/debate/lib/enhance/skill-patterns.js +147 -0
- package/plugins/debate/lib/enhance/suppression.js +352 -0
- package/plugins/debate/lib/enhance/tool-patterns.js +373 -0
- package/plugins/debate/lib/index.js +270 -0
- package/plugins/debate/lib/patterns/cli-enhancers.js +611 -0
- package/plugins/debate/lib/patterns/pipeline.js +948 -0
- package/plugins/debate/lib/patterns/review-patterns.js +558 -0
- package/plugins/debate/lib/patterns/slop-analyzers.js +2305 -0
- package/plugins/debate/lib/patterns/slop-patterns.js +1187 -0
- package/plugins/debate/lib/perf/analyzer/index.js +22 -0
- package/plugins/debate/lib/perf/argument-parser.js +105 -0
- package/plugins/debate/lib/perf/baseline-comparator.js +50 -0
- package/plugins/debate/lib/perf/baseline-store.js +127 -0
- package/plugins/debate/lib/perf/benchmark-runner.js +404 -0
- package/plugins/debate/lib/perf/breaking-point-finder.js +52 -0
- package/plugins/debate/lib/perf/breaking-point-runner.js +60 -0
- package/plugins/debate/lib/perf/checkpoint.js +123 -0
- package/plugins/debate/lib/perf/code-paths.js +86 -0
- package/plugins/debate/lib/perf/consolidation.js +37 -0
- package/plugins/debate/lib/perf/constraint-runner.js +71 -0
- package/plugins/debate/lib/perf/experiment-runner.js +32 -0
- package/plugins/debate/lib/perf/index.js +41 -0
- package/plugins/debate/lib/perf/investigation-state.js +874 -0
- package/plugins/debate/lib/perf/optimization-runner.js +79 -0
- package/plugins/debate/lib/perf/profilers/go.js +22 -0
- package/plugins/debate/lib/perf/profilers/index.js +46 -0
- package/plugins/debate/lib/perf/profilers/java.js +23 -0
- package/plugins/debate/lib/perf/profilers/node.js +27 -0
- package/plugins/debate/lib/perf/profilers/python.js +23 -0
- package/plugins/debate/lib/perf/profilers/rust.js +23 -0
- package/plugins/debate/lib/perf/profiling-runner.js +75 -0
- package/plugins/debate/lib/perf/schemas.js +140 -0
- package/plugins/debate/lib/platform/detect-platform.js +413 -0
- package/plugins/debate/lib/platform/detection-configs.js +93 -0
- package/plugins/debate/lib/platform/state-dir.js +132 -0
- package/plugins/debate/lib/platform/verify-tools.js +182 -0
- package/plugins/debate/lib/repo-map/cache.js +152 -0
- package/plugins/debate/lib/repo-map/concurrency.js +29 -0
- package/plugins/debate/lib/repo-map/index.js +222 -0
- package/plugins/debate/lib/repo-map/installer.js +212 -0
- package/plugins/debate/lib/repo-map/queries/go.js +27 -0
- package/plugins/debate/lib/repo-map/queries/index.js +100 -0
- package/plugins/debate/lib/repo-map/queries/java.js +38 -0
- package/plugins/debate/lib/repo-map/queries/javascript.js +55 -0
- package/plugins/debate/lib/repo-map/queries/python.js +24 -0
- package/plugins/debate/lib/repo-map/queries/rust.js +73 -0
- package/plugins/debate/lib/repo-map/queries/typescript.js +38 -0
- package/plugins/debate/lib/repo-map/runner.js +1364 -0
- package/plugins/debate/lib/repo-map/updater.js +562 -0
- package/plugins/debate/lib/repo-map/usage-analyzer.js +407 -0
- package/plugins/debate/lib/schemas/plugin-manifest.schema.json +57 -0
- package/plugins/debate/lib/schemas/validator.js +247 -0
- package/plugins/debate/lib/sources/custom-handler.js +199 -0
- package/plugins/debate/lib/sources/policy-questions.js +246 -0
- package/plugins/debate/lib/sources/source-cache.js +165 -0
- package/plugins/debate/lib/state/workflow-state.js +576 -0
- package/plugins/debate/lib/types/agent-frontmatter.d.ts +134 -0
- package/plugins/debate/lib/types/command-frontmatter.d.ts +107 -0
- package/plugins/debate/lib/types/hook-frontmatter.d.ts +115 -0
- package/plugins/debate/lib/types/index.d.ts +84 -0
- package/plugins/debate/lib/types/plugin-manifest.d.ts +102 -0
- package/plugins/debate/lib/types/skill-frontmatter.d.ts +89 -0
- package/plugins/debate/lib/utils/atomic-write.js +94 -0
- package/plugins/debate/lib/utils/cache-manager.js +159 -0
- package/plugins/debate/lib/utils/command-parser.js +0 -0
- package/plugins/debate/lib/utils/context-optimizer.js +300 -0
- package/plugins/debate/lib/utils/deprecation.js +37 -0
- package/plugins/debate/lib/utils/shell-escape.js +88 -0
- package/plugins/debate/lib/utils/state-helpers.js +61 -0
- package/plugins/debate/skills/debate/SKILL.md +264 -0
- package/plugins/deslop/.claude-plugin/plugin.json +1 -1
- package/plugins/deslop/lib/adapter-transforms.js +24 -4
- package/plugins/deslop/skills/deslop/SKILL.md +1 -1
- package/plugins/drift-detect/.claude-plugin/plugin.json +1 -1
- package/plugins/drift-detect/lib/adapter-transforms.js +24 -4
- package/plugins/drift-detect/skills/drift-analysis/SKILL.md +1 -1
- package/plugins/enhance/.claude-plugin/plugin.json +1 -1
- package/plugins/enhance/lib/adapter-transforms.js +24 -4
- package/plugins/enhance/skills/enhance-agent-prompts/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-claude-memory/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-cross-file/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-docs/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-hooks/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-orchestrator/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-plugins/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-prompts/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-skills/SKILL.md +1 -1
- package/plugins/learn/.claude-plugin/plugin.json +1 -1
- package/plugins/learn/agents/learn-agent.md +1 -1
- package/plugins/learn/lib/adapter-transforms.js +24 -4
- package/plugins/learn/skills/learn/SKILL.md +1 -1
- package/plugins/next-task/.claude-plugin/plugin.json +1 -1
- package/plugins/next-task/agents/exploration-agent.md +1 -1
- package/plugins/next-task/lib/adapter-transforms.js +24 -4
- package/plugins/next-task/skills/discover-tasks/SKILL.md +1 -1
- package/plugins/next-task/skills/validate-delivery/SKILL.md +1 -1
- package/plugins/perf/.claude-plugin/plugin.json +1 -1
- package/plugins/perf/lib/adapter-transforms.js +24 -4
- package/plugins/perf/skills/perf-analyzer/SKILL.md +1 -1
- package/plugins/perf/skills/perf-baseline-manager/SKILL.md +1 -1
- package/plugins/perf/skills/perf-benchmarker/SKILL.md +1 -1
- package/plugins/perf/skills/perf-code-paths/SKILL.md +1 -1
- package/plugins/perf/skills/perf-investigation-logger/SKILL.md +1 -1
- package/plugins/perf/skills/perf-profiler/SKILL.md +1 -1
- package/plugins/perf/skills/perf-theory-gatherer/SKILL.md +1 -1
- package/plugins/perf/skills/perf-theory-tester/SKILL.md +1 -1
- package/plugins/repo-map/.claude-plugin/plugin.json +1 -1
- package/plugins/repo-map/lib/adapter-transforms.js +24 -4
- package/plugins/ship/.claude-plugin/plugin.json +1 -1
- package/plugins/ship/lib/adapter-transforms.js +24 -4
- package/plugins/sync-docs/.claude-plugin/plugin.json +1 -1
- package/plugins/sync-docs/lib/adapter-transforms.js +24 -4
- package/plugins/sync-docs/skills/sync-docs/SKILL.md +1 -1
- package/scripts/gen-adapters.js +6 -7
- package/scripts/generate-docs.js +4 -2
- package/scripts/plugins.txt +1 -0
- package/site/content.json +6 -6
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simplified workflow state management
|
|
3
|
+
*
|
|
4
|
+
* Two files:
|
|
5
|
+
* - Main project: {stateDir}/tasks.json (tracks active worktree/task)
|
|
6
|
+
* - Worktree: {stateDir}/flow.json (tracks workflow progress)
|
|
7
|
+
*
|
|
8
|
+
* State directory is platform-aware:
|
|
9
|
+
* - Claude Code: .claude/
|
|
10
|
+
* - OpenCode: .opencode/
|
|
11
|
+
* - Codex CLI: .codex/
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
const { getStateDir } = require('../platform/state-dir');
|
|
18
|
+
const { writeJsonAtomic } = require('../utils/atomic-write');
|
|
19
|
+
const { isPlainObject, updatesApplied, sleepForRetry } = require('../utils/state-helpers');
|
|
20
|
+
|
|
21
|
+
// File paths
|
|
22
|
+
const TASKS_FILE = 'tasks.json';
|
|
23
|
+
const FLOW_FILE = 'flow.json';
|
|
24
|
+
/**
|
|
25
|
+
* Validate and resolve path to prevent path traversal attacks
|
|
26
|
+
* @param {string} basePath - Base directory path
|
|
27
|
+
* @returns {string} Validated absolute path
|
|
28
|
+
* @throws {Error} If path is invalid
|
|
29
|
+
*/
|
|
30
|
+
function validatePath(basePath) {
|
|
31
|
+
if (typeof basePath !== 'string' || basePath.length === 0) {
|
|
32
|
+
throw new Error('Path must be a non-empty string');
|
|
33
|
+
}
|
|
34
|
+
const resolved = path.resolve(basePath);
|
|
35
|
+
if (resolved.includes('\0')) {
|
|
36
|
+
throw new Error('Path contains invalid null byte');
|
|
37
|
+
}
|
|
38
|
+
return resolved;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate that target path is within base directory
|
|
43
|
+
* @param {string} targetPath - Target file path
|
|
44
|
+
* @param {string} basePath - Base directory
|
|
45
|
+
* @throws {Error} If path traversal detected
|
|
46
|
+
*/
|
|
47
|
+
function validatePathWithinBase(targetPath, basePath) {
|
|
48
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
49
|
+
const resolvedBase = path.resolve(basePath);
|
|
50
|
+
if (!resolvedTarget.startsWith(resolvedBase + path.sep) && resolvedTarget !== resolvedBase) {
|
|
51
|
+
throw new Error('Path traversal detected');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generate a unique workflow ID
|
|
57
|
+
* @returns {string} Workflow ID
|
|
58
|
+
*/
|
|
59
|
+
function generateWorkflowId() {
|
|
60
|
+
const now = new Date();
|
|
61
|
+
const date = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
62
|
+
const time = now.toISOString().slice(11, 19).replace(/:/g, '');
|
|
63
|
+
const random = crypto.randomBytes(4).toString('hex');
|
|
64
|
+
return `workflow-${date}-${time}-${random}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Valid phases for the workflow
|
|
68
|
+
const PHASES = [
|
|
69
|
+
'policy-selection',
|
|
70
|
+
'task-discovery',
|
|
71
|
+
'worktree-setup',
|
|
72
|
+
'exploration',
|
|
73
|
+
'planning',
|
|
74
|
+
'user-approval',
|
|
75
|
+
'implementation',
|
|
76
|
+
'review-loop',
|
|
77
|
+
'delivery-validation',
|
|
78
|
+
'shipping',
|
|
79
|
+
'complete'
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Ensure state directory exists (platform-aware)
|
|
84
|
+
*/
|
|
85
|
+
function ensureStateDir(basePath) {
|
|
86
|
+
const stateDir = path.join(basePath, getStateDir(basePath));
|
|
87
|
+
if (!fs.existsSync(stateDir)) {
|
|
88
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
return stateDir;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// =============================================================================
|
|
94
|
+
// TASKS.JSON - Main project directory
|
|
95
|
+
// =============================================================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get path to tasks.json with validation
|
|
99
|
+
*/
|
|
100
|
+
function getTasksPath(projectPath = process.cwd()) {
|
|
101
|
+
const validatedBase = validatePath(projectPath);
|
|
102
|
+
const tasksPath = path.join(validatedBase, getStateDir(projectPath), TASKS_FILE);
|
|
103
|
+
validatePathWithinBase(tasksPath, validatedBase);
|
|
104
|
+
return tasksPath;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Read tasks.json from main project
|
|
109
|
+
* Returns { active: null } if file doesn't exist or is corrupted
|
|
110
|
+
* Logs critical error on corruption to prevent silent data loss
|
|
111
|
+
*/
|
|
112
|
+
function readTasks(projectPath = process.cwd()) {
|
|
113
|
+
const tasksPath = getTasksPath(projectPath);
|
|
114
|
+
if (!fs.existsSync(tasksPath)) {
|
|
115
|
+
return { active: null };
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const data = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
|
|
119
|
+
// Normalize legacy format that may not have 'active' field
|
|
120
|
+
if (!Object.prototype.hasOwnProperty.call(data, 'active')) {
|
|
121
|
+
return { active: null };
|
|
122
|
+
}
|
|
123
|
+
return data;
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.error(`[CRITICAL] Corrupted tasks.json at ${tasksPath}: ${e.message}`);
|
|
126
|
+
return { active: null };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Write tasks.json to main project
|
|
132
|
+
*/
|
|
133
|
+
function writeTasks(tasks, projectPath = process.cwd()) {
|
|
134
|
+
ensureStateDir(projectPath);
|
|
135
|
+
const tasksPath = getTasksPath(projectPath);
|
|
136
|
+
writeJsonAtomic(tasksPath, tasks);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Set active task in main project
|
|
142
|
+
*/
|
|
143
|
+
function setActiveTask(task, projectPath = process.cwd()) {
|
|
144
|
+
const tasks = readTasks(projectPath);
|
|
145
|
+
tasks.active = {
|
|
146
|
+
...task,
|
|
147
|
+
startedAt: new Date().toISOString()
|
|
148
|
+
};
|
|
149
|
+
return writeTasks(tasks, projectPath);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Clear active task
|
|
154
|
+
*/
|
|
155
|
+
function clearActiveTask(projectPath = process.cwd()) {
|
|
156
|
+
const tasks = readTasks(projectPath);
|
|
157
|
+
tasks.active = null;
|
|
158
|
+
return writeTasks(tasks, projectPath);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check if there's an active task
|
|
163
|
+
* Uses != null to catch both null and undefined (legacy format safety)
|
|
164
|
+
*/
|
|
165
|
+
function hasActiveTask(projectPath = process.cwd()) {
|
|
166
|
+
const tasks = readTasks(projectPath);
|
|
167
|
+
return tasks.active != null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// =============================================================================
|
|
171
|
+
// FLOW.JSON - Worktree directory
|
|
172
|
+
// =============================================================================
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get path to flow.json with validation
|
|
176
|
+
*/
|
|
177
|
+
function getFlowPath(worktreePath = process.cwd()) {
|
|
178
|
+
const validatedBase = validatePath(worktreePath);
|
|
179
|
+
const flowPath = path.join(validatedBase, getStateDir(worktreePath), FLOW_FILE);
|
|
180
|
+
validatePathWithinBase(flowPath, validatedBase);
|
|
181
|
+
return flowPath;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Read flow.json from worktree
|
|
186
|
+
* Returns null if file doesn't exist or is corrupted
|
|
187
|
+
* Logs critical error on corruption to prevent silent data loss
|
|
188
|
+
*/
|
|
189
|
+
function readFlow(worktreePath = process.cwd()) {
|
|
190
|
+
const flowPath = getFlowPath(worktreePath);
|
|
191
|
+
if (!fs.existsSync(flowPath)) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
return JSON.parse(fs.readFileSync(flowPath, 'utf8'));
|
|
196
|
+
} catch (e) {
|
|
197
|
+
console.error(`[CRITICAL] Corrupted flow.json at ${flowPath}: ${e.message}`);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Write flow.json to worktree
|
|
204
|
+
* Creates a copy to avoid mutating the original object
|
|
205
|
+
* Increments version for optimistic locking
|
|
206
|
+
*/
|
|
207
|
+
function writeFlow(flow, worktreePath = process.cwd()) {
|
|
208
|
+
ensureStateDir(worktreePath);
|
|
209
|
+
// Clone to avoid mutating the original object
|
|
210
|
+
const flowCopy = structuredClone(flow);
|
|
211
|
+
flowCopy.lastUpdate = new Date().toISOString();
|
|
212
|
+
// Increment version for optimistic locking (initialize if missing)
|
|
213
|
+
flowCopy._version = (flowCopy._version || 0) + 1;
|
|
214
|
+
const flowPath = getFlowPath(worktreePath);
|
|
215
|
+
writeJsonAtomic(flowPath, flowCopy);
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Update flow.json with partial updates
|
|
221
|
+
* Handles null values correctly (null overwrites existing values)
|
|
222
|
+
* Deep merges nested objects when both exist
|
|
223
|
+
* Uses optimistic locking with version check and retry
|
|
224
|
+
*/
|
|
225
|
+
function updateFlow(updates, worktreePath = process.cwd()) {
|
|
226
|
+
const MAX_RETRIES = 5;
|
|
227
|
+
|
|
228
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
229
|
+
const flow = readFlow(worktreePath) || {};
|
|
230
|
+
const initialVersion = flow._version || 0;
|
|
231
|
+
|
|
232
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
233
|
+
// Skip internal version field from updates
|
|
234
|
+
if (key === '_version') continue;
|
|
235
|
+
|
|
236
|
+
// Null explicitly overwrites
|
|
237
|
+
if (value === null) {
|
|
238
|
+
flow[key] = null;
|
|
239
|
+
}
|
|
240
|
+
// Deep merge if both source and target are non-null objects
|
|
241
|
+
else if (isPlainObject(value) && isPlainObject(flow[key])) {
|
|
242
|
+
flow[key] = { ...flow[key], ...value };
|
|
243
|
+
}
|
|
244
|
+
// Otherwise direct assignment
|
|
245
|
+
else {
|
|
246
|
+
flow[key] = value;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Preserve version for write (writeFlow will increment it)
|
|
251
|
+
flow._version = initialVersion;
|
|
252
|
+
|
|
253
|
+
// Write and verify version wasn't changed by another process
|
|
254
|
+
writeFlow(flow, worktreePath);
|
|
255
|
+
|
|
256
|
+
// Re-read to verify our write succeeded
|
|
257
|
+
const afterWrite = readFlow(worktreePath);
|
|
258
|
+
if (afterWrite && afterWrite._version >= initialVersion + 1 && updatesApplied(afterWrite, updates)) {
|
|
259
|
+
return true; // Success
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Version conflict or overwrite - retry after brief delay
|
|
263
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
264
|
+
const delay = Math.floor(Math.random() * 50) + 10;
|
|
265
|
+
sleepForRetry(delay);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// All retries exhausted. One final read can detect if another writer
|
|
270
|
+
// applied the same updates while we were retrying.
|
|
271
|
+
const latest = readFlow(worktreePath);
|
|
272
|
+
if (latest && updatesApplied(latest, updates)) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.error('[ERROR] updateFlow: failed to apply updates after max retries');
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Create initial flow for a new task
|
|
282
|
+
* Also registers the task as active in the main project's tasks.json
|
|
283
|
+
* @param {Object} task - Task object with id, title, source, url
|
|
284
|
+
* @param {Object} policy - Policy object with stoppingPoint
|
|
285
|
+
* @param {string} worktreePath - Path to worktree
|
|
286
|
+
* @param {string} projectPath - Path to main project (for tasks.json registration)
|
|
287
|
+
*/
|
|
288
|
+
function createFlow(task, policy, worktreePath = process.cwd(), projectPath = null) {
|
|
289
|
+
const flow = {
|
|
290
|
+
task: {
|
|
291
|
+
id: task.id,
|
|
292
|
+
title: task.title,
|
|
293
|
+
source: task.source,
|
|
294
|
+
url: task.url || null
|
|
295
|
+
},
|
|
296
|
+
policy: {
|
|
297
|
+
stoppingPoint: policy.stoppingPoint || 'merged'
|
|
298
|
+
},
|
|
299
|
+
phase: 'policy-selection',
|
|
300
|
+
status: 'in_progress',
|
|
301
|
+
lastUpdate: new Date().toISOString(),
|
|
302
|
+
userNotes: '',
|
|
303
|
+
git: {
|
|
304
|
+
branch: null,
|
|
305
|
+
baseBranch: 'main'
|
|
306
|
+
},
|
|
307
|
+
pr: null,
|
|
308
|
+
exploration: null,
|
|
309
|
+
plan: null,
|
|
310
|
+
// Store projectPath so completeWorkflow knows where to clear the task
|
|
311
|
+
projectPath: projectPath
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
writeFlow(flow, worktreePath);
|
|
315
|
+
|
|
316
|
+
// Register task as active in main project
|
|
317
|
+
if (projectPath) {
|
|
318
|
+
setActiveTask({
|
|
319
|
+
taskId: task.id,
|
|
320
|
+
title: task.title,
|
|
321
|
+
worktree: worktreePath,
|
|
322
|
+
branch: flow.git.branch
|
|
323
|
+
}, projectPath);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return flow;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Delete flow.json
|
|
331
|
+
*/
|
|
332
|
+
function deleteFlow(worktreePath = process.cwd()) {
|
|
333
|
+
const flowPath = getFlowPath(worktreePath);
|
|
334
|
+
if (fs.existsSync(flowPath)) {
|
|
335
|
+
fs.unlinkSync(flowPath);
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// =============================================================================
|
|
342
|
+
// PHASE MANAGEMENT
|
|
343
|
+
// =============================================================================
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Check if phase is valid
|
|
347
|
+
*/
|
|
348
|
+
function isValidPhase(phase) {
|
|
349
|
+
return PHASES.includes(phase);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Set current phase
|
|
354
|
+
*/
|
|
355
|
+
function setPhase(phase, worktreePath = process.cwd()) {
|
|
356
|
+
if (!isValidPhase(phase)) {
|
|
357
|
+
throw new Error(`Invalid phase: ${phase}`);
|
|
358
|
+
}
|
|
359
|
+
return updateFlow({ phase, status: 'in_progress' }, worktreePath);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Start a phase (alias for setPhase, for backwards compatibility)
|
|
364
|
+
*/
|
|
365
|
+
function startPhase(phase, worktreePath = process.cwd()) {
|
|
366
|
+
return setPhase(phase, worktreePath);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Fail the current phase
|
|
371
|
+
*/
|
|
372
|
+
function failPhase(reason, context = {}, worktreePath = process.cwd()) {
|
|
373
|
+
const flow = readFlow(worktreePath);
|
|
374
|
+
if (!flow) return null;
|
|
375
|
+
|
|
376
|
+
return updateFlow({
|
|
377
|
+
status: 'failed',
|
|
378
|
+
error: reason,
|
|
379
|
+
failContext: context
|
|
380
|
+
}, worktreePath);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Skip to a specific phase
|
|
385
|
+
*/
|
|
386
|
+
function skipToPhase(phase, reason = 'manual skip', worktreePath = process.cwd()) {
|
|
387
|
+
if (!isValidPhase(phase)) {
|
|
388
|
+
throw new Error(`Invalid phase: ${phase}`);
|
|
389
|
+
}
|
|
390
|
+
return updateFlow({
|
|
391
|
+
phase,
|
|
392
|
+
status: 'in_progress',
|
|
393
|
+
skipReason: reason
|
|
394
|
+
}, worktreePath);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Complete current phase and move to next
|
|
399
|
+
* Uses updateFlow pattern to avoid direct mutation issues
|
|
400
|
+
*/
|
|
401
|
+
function completePhase(result = null, worktreePath = process.cwd()) {
|
|
402
|
+
const flow = readFlow(worktreePath);
|
|
403
|
+
if (!flow) return null;
|
|
404
|
+
|
|
405
|
+
const currentIndex = PHASES.indexOf(flow.phase);
|
|
406
|
+
const nextPhase = PHASES[currentIndex + 1] || 'complete';
|
|
407
|
+
|
|
408
|
+
// Build updates object
|
|
409
|
+
const updates = {
|
|
410
|
+
phase: nextPhase,
|
|
411
|
+
status: nextPhase === 'complete' ? 'completed' : 'in_progress'
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// Store result in appropriate field
|
|
415
|
+
if (result) {
|
|
416
|
+
const resultField = getResultField(flow.phase);
|
|
417
|
+
if (resultField) {
|
|
418
|
+
updates[resultField] = result;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const updated = updateFlow(updates, worktreePath);
|
|
423
|
+
return updated ? readFlow(worktreePath) : null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Map phase to result field
|
|
428
|
+
*/
|
|
429
|
+
function getResultField(phase) {
|
|
430
|
+
const mapping = {
|
|
431
|
+
'exploration': 'exploration',
|
|
432
|
+
'planning': 'plan',
|
|
433
|
+
'review-loop': 'reviewResult'
|
|
434
|
+
};
|
|
435
|
+
return mapping[phase] || null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Mark workflow as failed
|
|
440
|
+
*/
|
|
441
|
+
function failWorkflow(error, worktreePath = process.cwd()) {
|
|
442
|
+
return updateFlow({
|
|
443
|
+
status: 'failed',
|
|
444
|
+
error: error?.message || String(error)
|
|
445
|
+
}, worktreePath);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Mark workflow as complete
|
|
450
|
+
* Automatically clears the active task from tasks.json using stored projectPath
|
|
451
|
+
* @param {string} worktreePath - Path to worktree
|
|
452
|
+
*/
|
|
453
|
+
function completeWorkflow(worktreePath = process.cwd()) {
|
|
454
|
+
const flow = readFlow(worktreePath);
|
|
455
|
+
|
|
456
|
+
const updated = updateFlow({
|
|
457
|
+
phase: 'complete',
|
|
458
|
+
status: 'completed',
|
|
459
|
+
completedAt: new Date().toISOString()
|
|
460
|
+
}, worktreePath);
|
|
461
|
+
|
|
462
|
+
if (updated && flow && flow.projectPath) {
|
|
463
|
+
clearActiveTask(flow.projectPath);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return updated;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Abort workflow
|
|
471
|
+
* Also clears the active task from tasks.json using stored projectPath
|
|
472
|
+
*/
|
|
473
|
+
function abortWorkflow(reason, worktreePath = process.cwd()) {
|
|
474
|
+
const flow = readFlow(worktreePath);
|
|
475
|
+
|
|
476
|
+
const updated = updateFlow({
|
|
477
|
+
status: 'aborted',
|
|
478
|
+
abortReason: reason,
|
|
479
|
+
abortedAt: new Date().toISOString()
|
|
480
|
+
}, worktreePath);
|
|
481
|
+
|
|
482
|
+
if (updated && flow && flow.projectPath) {
|
|
483
|
+
clearActiveTask(flow.projectPath);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return updated;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// =============================================================================
|
|
490
|
+
// CONVENIENCE FUNCTIONS
|
|
491
|
+
// =============================================================================
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Get workflow summary for display
|
|
495
|
+
*/
|
|
496
|
+
function getFlowSummary(worktreePath = process.cwd()) {
|
|
497
|
+
const flow = readFlow(worktreePath);
|
|
498
|
+
if (!flow) return null;
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
task: flow.task?.title || 'Unknown',
|
|
502
|
+
taskId: flow.task?.id,
|
|
503
|
+
phase: flow.phase,
|
|
504
|
+
status: flow.status,
|
|
505
|
+
lastUpdate: flow.lastUpdate,
|
|
506
|
+
pr: flow.pr?.number ? `#${flow.pr.number}` : null
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Check if workflow can be resumed
|
|
512
|
+
*/
|
|
513
|
+
function canResume(worktreePath = process.cwd()) {
|
|
514
|
+
const flow = readFlow(worktreePath);
|
|
515
|
+
if (!flow) return false;
|
|
516
|
+
return flow.status === 'in_progress' && flow.phase !== 'complete';
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// =============================================================================
|
|
520
|
+
// BACKWARDS COMPATIBILITY ALIASES
|
|
521
|
+
// =============================================================================
|
|
522
|
+
|
|
523
|
+
// These maintain compatibility with existing agent code
|
|
524
|
+
const readState = readFlow;
|
|
525
|
+
const writeState = writeFlow;
|
|
526
|
+
const updateState = updateFlow;
|
|
527
|
+
const createState = (type, policy) => createFlow({ id: 'manual', title: 'Manual task', source: 'manual' }, policy);
|
|
528
|
+
const deleteState = deleteFlow;
|
|
529
|
+
const hasActiveWorkflow = hasActiveTask;
|
|
530
|
+
const getWorkflowSummary = getFlowSummary;
|
|
531
|
+
|
|
532
|
+
module.exports = {
|
|
533
|
+
// Constants
|
|
534
|
+
PHASES,
|
|
535
|
+
|
|
536
|
+
// Tasks (main project)
|
|
537
|
+
getTasksPath,
|
|
538
|
+
readTasks,
|
|
539
|
+
writeTasks,
|
|
540
|
+
setActiveTask,
|
|
541
|
+
clearActiveTask,
|
|
542
|
+
hasActiveTask,
|
|
543
|
+
|
|
544
|
+
// Flow (worktree)
|
|
545
|
+
getFlowPath,
|
|
546
|
+
readFlow,
|
|
547
|
+
writeFlow,
|
|
548
|
+
updateFlow,
|
|
549
|
+
createFlow,
|
|
550
|
+
deleteFlow,
|
|
551
|
+
|
|
552
|
+
// Phase management
|
|
553
|
+
isValidPhase,
|
|
554
|
+
setPhase,
|
|
555
|
+
startPhase,
|
|
556
|
+
completePhase,
|
|
557
|
+
failPhase,
|
|
558
|
+
skipToPhase,
|
|
559
|
+
failWorkflow,
|
|
560
|
+
completeWorkflow,
|
|
561
|
+
abortWorkflow,
|
|
562
|
+
|
|
563
|
+
// Convenience
|
|
564
|
+
getFlowSummary,
|
|
565
|
+
canResume,
|
|
566
|
+
generateWorkflowId,
|
|
567
|
+
|
|
568
|
+
// Backwards compatibility
|
|
569
|
+
readState,
|
|
570
|
+
writeState,
|
|
571
|
+
updateState,
|
|
572
|
+
createState,
|
|
573
|
+
deleteState,
|
|
574
|
+
hasActiveWorkflow,
|
|
575
|
+
getWorkflowSummary
|
|
576
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Frontmatter Type Definitions
|
|
3
|
+
* Defines the structure of YAML frontmatter in agent markdown files
|
|
4
|
+
*
|
|
5
|
+
* @module lib/types/agent-frontmatter
|
|
6
|
+
* @author Avi Fenesh
|
|
7
|
+
* @license MIT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Tool permission for agent
|
|
12
|
+
*/
|
|
13
|
+
export type AgentTool =
|
|
14
|
+
| 'Bash'
|
|
15
|
+
| 'Bash(git:*)'
|
|
16
|
+
| 'Bash(gh:*)'
|
|
17
|
+
| 'Bash(npm:*)'
|
|
18
|
+
| 'Bash(node:*)'
|
|
19
|
+
| 'Bash(deployment:*)'
|
|
20
|
+
| 'Read'
|
|
21
|
+
| 'Write'
|
|
22
|
+
| 'Edit'
|
|
23
|
+
| 'Glob'
|
|
24
|
+
| 'Grep'
|
|
25
|
+
| 'Task'
|
|
26
|
+
| 'LSP'
|
|
27
|
+
| 'EnterPlanMode'
|
|
28
|
+
| 'ExitPlanMode'
|
|
29
|
+
| 'AskUserQuestion'
|
|
30
|
+
| 'TodoWrite'
|
|
31
|
+
| 'WebFetch'
|
|
32
|
+
| 'WebSearch'
|
|
33
|
+
| string; // Allow custom tools
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Agent color for UI identification
|
|
37
|
+
*/
|
|
38
|
+
export type AgentColor =
|
|
39
|
+
| 'blue'
|
|
40
|
+
| 'green'
|
|
41
|
+
| 'purple'
|
|
42
|
+
| 'orange'
|
|
43
|
+
| 'red'
|
|
44
|
+
| 'yellow'
|
|
45
|
+
| 'pink'
|
|
46
|
+
| 'cyan';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Agent frontmatter structure
|
|
50
|
+
* YAML metadata at the top of agent markdown files
|
|
51
|
+
*/
|
|
52
|
+
export interface AgentFrontmatter {
|
|
53
|
+
/** Agent unique identifier (kebab-case) */
|
|
54
|
+
agent: string;
|
|
55
|
+
|
|
56
|
+
/** Short description of agent purpose */
|
|
57
|
+
description: string;
|
|
58
|
+
|
|
59
|
+
/** Tools the agent has access to */
|
|
60
|
+
tools?: AgentTool[];
|
|
61
|
+
|
|
62
|
+
/** Preferred model for this agent (sonnet, opus, haiku) */
|
|
63
|
+
model?: 'sonnet' | 'opus' | 'haiku';
|
|
64
|
+
|
|
65
|
+
/** Maximum number of turns before stopping */
|
|
66
|
+
maxTurns?: number;
|
|
67
|
+
|
|
68
|
+
/** UI color for agent identification */
|
|
69
|
+
color?: AgentColor;
|
|
70
|
+
|
|
71
|
+
/** Whether agent can run in background */
|
|
72
|
+
canRunInBackground?: boolean;
|
|
73
|
+
|
|
74
|
+
/** Whether agent requires user approval before running */
|
|
75
|
+
requiresApproval?: boolean;
|
|
76
|
+
|
|
77
|
+
/** Agent category for organization */
|
|
78
|
+
category?: string;
|
|
79
|
+
|
|
80
|
+
/** Tags for searchability */
|
|
81
|
+
tags?: string[];
|
|
82
|
+
|
|
83
|
+
/** When this agent should be used (triggering conditions) */
|
|
84
|
+
'when-to-use'?: string[];
|
|
85
|
+
|
|
86
|
+
/** Example usage scenarios */
|
|
87
|
+
examples?: string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Type guard to check if an object is valid AgentFrontmatter
|
|
92
|
+
*/
|
|
93
|
+
export function isAgentFrontmatter(obj: unknown): obj is AgentFrontmatter {
|
|
94
|
+
if (typeof obj !== 'object' || obj === null) return false;
|
|
95
|
+
const fm = obj as Partial<AgentFrontmatter>;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
typeof fm.agent === 'string' &&
|
|
99
|
+
fm.agent.length > 0 &&
|
|
100
|
+
typeof fm.description === 'string' &&
|
|
101
|
+
fm.description.length > 0
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validates agent frontmatter
|
|
107
|
+
* @throws {Error} If frontmatter is invalid
|
|
108
|
+
*/
|
|
109
|
+
export function validateAgentFrontmatter(
|
|
110
|
+
frontmatter: unknown
|
|
111
|
+
): asserts frontmatter is AgentFrontmatter {
|
|
112
|
+
if (!isAgentFrontmatter(frontmatter)) {
|
|
113
|
+
throw new Error('Invalid agent frontmatter: missing required fields');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Additional validations
|
|
117
|
+
if (frontmatter.tools && !Array.isArray(frontmatter.tools)) {
|
|
118
|
+
throw new Error('Invalid agent frontmatter: tools must be an array');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (frontmatter.model && !['sonnet', 'opus', 'haiku'].includes(frontmatter.model)) {
|
|
122
|
+
throw new Error('Invalid agent frontmatter: model must be sonnet, opus, or haiku');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (frontmatter.maxTurns !== undefined &&
|
|
126
|
+
(typeof frontmatter.maxTurns !== 'number' || frontmatter.maxTurns < 1)) {
|
|
127
|
+
throw new Error('Invalid agent frontmatter: maxTurns must be a positive number');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const validColors: AgentColor[] = ['blue', 'green', 'purple', 'orange', 'red', 'yellow', 'pink', 'cyan'];
|
|
131
|
+
if (frontmatter.color && !validColors.includes(frontmatter.color)) {
|
|
132
|
+
throw new Error(`Invalid agent frontmatter: color must be one of ${validColors.join(', ')}`);
|
|
133
|
+
}
|
|
134
|
+
}
|