agileflow 2.97.0 → 2.98.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/CHANGELOG.md +5 -0
- package/lib/dashboard-server.js +14 -8
- package/lib/errors.js +2 -2
- package/lib/file-cache.js +2 -2
- package/lib/gate-runner.js +14 -4
- package/lib/git-operations.js +12 -6
- package/lib/merge-operations.js +12 -3
- package/lib/registry-cache.js +114 -0
- package/lib/template-loader.js +141 -0
- package/package.json +1 -1
- package/scripts/agent-loop.js +1 -1
- package/scripts/agileflow-welcome.js +16 -12
- package/scripts/auto-self-improve.js +9 -5
- package/scripts/batch-pmap-loop.js +15 -9
- package/scripts/context-loader.js +4 -4
- package/scripts/dependency-check.js +7 -6
- package/scripts/generators/agent-registry.js +21 -6
- package/scripts/generators/command-registry.js +21 -6
- package/scripts/generators/index.js +17 -15
- package/scripts/generators/inject-babysit.js +8 -4
- package/scripts/generators/inject-help.js +8 -4
- package/scripts/generators/inject-readme.js +14 -8
- package/scripts/generators/skill-registry.js +25 -8
- package/scripts/get-env.js +4 -4
- package/scripts/lib/configure-detect.js +3 -2
- package/scripts/lib/configure-repair.js +5 -2
- package/scripts/lib/process-cleanup.js +3 -2
- package/scripts/lib/story-claiming.js +10 -2
- package/scripts/lib/task-sync.js +20 -12
- package/scripts/messaging-bridge.js +14 -8
- package/scripts/obtain-context.js +2 -2
- package/scripts/ralph-loop.js +38 -11
- package/scripts/session-manager.js +15 -6
- package/scripts/spawn-parallel.js +12 -8
- package/scripts/task-completed-gate.js +16 -6
- package/scripts/teammate-idle-gate.js +11 -4
- package/tools/cli/commands/doctor.js +2 -2
- package/tools/cli/commands/serve.js +4 -2
- package/tools/cli/commands/session.js +11 -6
- package/tools/postinstall.js +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.98.0] - 2026-02-07
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Shell injection cleanup, shared lib tests, and async I/O pipeline
|
|
14
|
+
|
|
10
15
|
## [2.97.0] - 2026-02-07
|
|
11
16
|
|
|
12
17
|
### Added
|
package/lib/dashboard-server.js
CHANGED
|
@@ -42,7 +42,7 @@ const {
|
|
|
42
42
|
serializeMessage,
|
|
43
43
|
} = require('./dashboard-protocol');
|
|
44
44
|
const { getProjectRoot, isAgileflowProject, getAgentsDir } = require('./paths');
|
|
45
|
-
const {
|
|
45
|
+
const { execFileSync, spawn } = require('child_process');
|
|
46
46
|
const os = require('os');
|
|
47
47
|
|
|
48
48
|
// Lazy-load automation modules to avoid circular dependencies
|
|
@@ -910,7 +910,11 @@ class DashboardServer extends EventEmitter {
|
|
|
910
910
|
|
|
911
911
|
// Validate commit message
|
|
912
912
|
if (commitMessage !== undefined && commitMessage !== null) {
|
|
913
|
-
if (
|
|
913
|
+
if (
|
|
914
|
+
typeof commitMessage !== 'string' ||
|
|
915
|
+
commitMessage.length > 10000 ||
|
|
916
|
+
commitMessage.includes('\0')
|
|
917
|
+
) {
|
|
914
918
|
session.send(createError('GIT_ERROR', 'Invalid commit message'));
|
|
915
919
|
return;
|
|
916
920
|
}
|
|
@@ -929,7 +933,9 @@ class DashboardServer extends EventEmitter {
|
|
|
929
933
|
break;
|
|
930
934
|
case InboundMessageType.GIT_UNSTAGE:
|
|
931
935
|
if (fileArgs) {
|
|
932
|
-
execFileSync('git', ['restore', '--staged', '--', ...fileArgs], {
|
|
936
|
+
execFileSync('git', ['restore', '--staged', '--', ...fileArgs], {
|
|
937
|
+
cwd: this.projectRoot,
|
|
938
|
+
});
|
|
933
939
|
} else {
|
|
934
940
|
execFileSync('git', ['restore', '--staged', '.'], { cwd: this.projectRoot });
|
|
935
941
|
}
|
|
@@ -975,14 +981,16 @@ class DashboardServer extends EventEmitter {
|
|
|
975
981
|
*/
|
|
976
982
|
getGitStatus() {
|
|
977
983
|
try {
|
|
978
|
-
const branch =
|
|
984
|
+
const branch = execFileSync('git', ['branch', '--show-current'], {
|
|
979
985
|
cwd: this.projectRoot,
|
|
980
986
|
encoding: 'utf8',
|
|
987
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
981
988
|
}).trim();
|
|
982
989
|
|
|
983
|
-
const statusOutput =
|
|
990
|
+
const statusOutput = execFileSync('git', ['status', '--porcelain'], {
|
|
984
991
|
cwd: this.projectRoot,
|
|
985
992
|
encoding: 'utf8',
|
|
993
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
986
994
|
});
|
|
987
995
|
|
|
988
996
|
const staged = [];
|
|
@@ -1066,9 +1074,7 @@ class DashboardServer extends EventEmitter {
|
|
|
1066
1074
|
*/
|
|
1067
1075
|
getFileDiff(filePath, staged = false) {
|
|
1068
1076
|
try {
|
|
1069
|
-
const diffArgs = staged
|
|
1070
|
-
? ['diff', '--cached', '--', filePath]
|
|
1071
|
-
: ['diff', '--', filePath];
|
|
1077
|
+
const diffArgs = staged ? ['diff', '--cached', '--', filePath] : ['diff', '--', filePath];
|
|
1072
1078
|
|
|
1073
1079
|
const diff = execFileSync('git', diffArgs, {
|
|
1074
1080
|
cwd: this.projectRoot,
|
package/lib/errors.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
19
|
-
const {
|
|
19
|
+
const { execFileSync } = require('child_process');
|
|
20
20
|
|
|
21
21
|
// Debug logging (opt-in via AGILEFLOW_DEBUG=1)
|
|
22
22
|
const DEBUG = process.env.AGILEFLOW_DEBUG === '1';
|
|
@@ -421,7 +421,7 @@ function safeExec(command, options = {}) {
|
|
|
421
421
|
}
|
|
422
422
|
|
|
423
423
|
try {
|
|
424
|
-
const output =
|
|
424
|
+
const output = execFileSync('bash', ['-c', command], {
|
|
425
425
|
cwd,
|
|
426
426
|
encoding: 'utf8',
|
|
427
427
|
timeout,
|
package/lib/file-cache.js
CHANGED
|
@@ -340,7 +340,7 @@ function readProjectFiles(rootDir, options = {}) {
|
|
|
340
340
|
// Command Caching (for git and other shell commands)
|
|
341
341
|
// =============================================================================
|
|
342
342
|
|
|
343
|
-
const {
|
|
343
|
+
const { execFileSync } = require('child_process');
|
|
344
344
|
|
|
345
345
|
// Separate cache for command output with shorter TTL
|
|
346
346
|
const commandCache = new LRUCache({
|
|
@@ -372,7 +372,7 @@ function execCached(command, options = {}) {
|
|
|
372
372
|
|
|
373
373
|
// Execute command
|
|
374
374
|
try {
|
|
375
|
-
const output =
|
|
375
|
+
const output = execFileSync('bash', ['-c', command], {
|
|
376
376
|
cwd,
|
|
377
377
|
encoding: 'utf8',
|
|
378
378
|
stdio: ['pipe', 'pipe', 'pipe'],
|
package/lib/gate-runner.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* const result = await evaluateGate('tests', rootDir, { timeout: 300000 });
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
const {
|
|
17
|
+
const { execFileSync } = require('child_process');
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
|
|
@@ -106,7 +106,7 @@ function runCommand(command, rootDir, timeout) {
|
|
|
106
106
|
const result = { passed: false, output: '', duration: 0 };
|
|
107
107
|
|
|
108
108
|
try {
|
|
109
|
-
const output =
|
|
109
|
+
const output = execFileSync('bash', ['-c', command], {
|
|
110
110
|
cwd: rootDir,
|
|
111
111
|
encoding: 'utf8',
|
|
112
112
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -155,7 +155,12 @@ function evaluateGate(gateName, rootDir, options = {}) {
|
|
|
155
155
|
case 'lint': {
|
|
156
156
|
command = command || detectLintCommand(rootDir);
|
|
157
157
|
if (!command) {
|
|
158
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
gate: 'lint',
|
|
160
|
+
passed: true,
|
|
161
|
+
message: 'No lint command found (skipped)',
|
|
162
|
+
duration: 0,
|
|
163
|
+
};
|
|
159
164
|
}
|
|
160
165
|
const result = runCommand(command, rootDir, timeout);
|
|
161
166
|
return {
|
|
@@ -171,7 +176,12 @@ function evaluateGate(gateName, rootDir, options = {}) {
|
|
|
171
176
|
case 'types': {
|
|
172
177
|
command = command || detectTypeCheckCommand(rootDir);
|
|
173
178
|
if (!command) {
|
|
174
|
-
return {
|
|
179
|
+
return {
|
|
180
|
+
gate: 'types',
|
|
181
|
+
passed: true,
|
|
182
|
+
message: 'No type-check command found (skipped)',
|
|
183
|
+
duration: 0,
|
|
184
|
+
};
|
|
175
185
|
}
|
|
176
186
|
const result = runCommand(command, rootDir, timeout);
|
|
177
187
|
return {
|
package/lib/git-operations.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Provides cached git operations and session phase detection for Kanban visualization.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const {
|
|
7
|
+
const { execFileSync, spawnSync, spawn } = require('child_process');
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
|
|
10
10
|
const { getProjectRoot } = require('./paths');
|
|
@@ -80,7 +80,7 @@ function getCurrentBranch(cwd = ROOT) {
|
|
|
80
80
|
if (cached !== null) return cached;
|
|
81
81
|
|
|
82
82
|
try {
|
|
83
|
-
const branch =
|
|
83
|
+
const branch = execFileSync('git', ['branch', '--show-current'], { cwd, encoding: 'utf8' }).trim();
|
|
84
84
|
gitCache.set(cacheKey, branch);
|
|
85
85
|
return branch;
|
|
86
86
|
} catch (e) {
|
|
@@ -189,10 +189,16 @@ function getSessionPhase(session) {
|
|
|
189
189
|
}
|
|
190
190
|
const commits = parseInt(commitCount, 10);
|
|
191
191
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
let status = '';
|
|
193
|
+
try {
|
|
194
|
+
status = execFileSync('git', ['status', '--porcelain'], {
|
|
195
|
+
cwd: sessionPath,
|
|
196
|
+
encoding: 'utf8',
|
|
197
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
198
|
+
}).trim();
|
|
199
|
+
} catch {
|
|
200
|
+
// git status failed, treat as no changes
|
|
201
|
+
}
|
|
196
202
|
|
|
197
203
|
const phase = determinePhaseFromGitState(commits, status !== '');
|
|
198
204
|
gitCache.set(cacheKey, phase);
|
package/lib/merge-operations.js
CHANGED
|
@@ -270,7 +270,10 @@ function integrateSession(sessionId, options = {}, loadRegistry, saveRegistry, r
|
|
|
270
270
|
result.worktreeDeleted = true;
|
|
271
271
|
} catch (e) {
|
|
272
272
|
try {
|
|
273
|
-
execFileSync('git', ['worktree', 'remove', '--force', session.path], {
|
|
273
|
+
execFileSync('git', ['worktree', 'remove', '--force', session.path], {
|
|
274
|
+
cwd: ROOT,
|
|
275
|
+
encoding: 'utf8',
|
|
276
|
+
});
|
|
274
277
|
result.worktreeDeleted = true;
|
|
275
278
|
} catch (e2) {
|
|
276
279
|
result.worktreeDeleted = false;
|
|
@@ -649,10 +652,16 @@ function resolveConflict(resolution) {
|
|
|
649
652
|
fs.unlinkSync(tmpOurs);
|
|
650
653
|
fs.unlinkSync(tmpTheirs);
|
|
651
654
|
} else {
|
|
652
|
-
execFileSync('git', ['checkout', '--theirs', '--', file], {
|
|
655
|
+
execFileSync('git', ['checkout', '--theirs', '--', file], {
|
|
656
|
+
cwd: ROOT,
|
|
657
|
+
encoding: 'utf8',
|
|
658
|
+
});
|
|
653
659
|
}
|
|
654
660
|
} catch (unionError) {
|
|
655
|
-
execFileSync('git', ['checkout', '--theirs', '--', file], {
|
|
661
|
+
execFileSync('git', ['checkout', '--theirs', '--', file], {
|
|
662
|
+
cwd: ROOT,
|
|
663
|
+
encoding: 'utf8',
|
|
664
|
+
});
|
|
656
665
|
}
|
|
657
666
|
break;
|
|
658
667
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Registry scanning cache
|
|
7
|
+
* Caches directory scan results keyed by path + mtime.
|
|
8
|
+
* TTL-based expiration (default 60s).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const _cache = new Map();
|
|
12
|
+
const DEFAULT_TTL_MS = 60 * 1000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get cached scan result for a directory
|
|
16
|
+
* @param {string} dirPath - Directory path
|
|
17
|
+
* @param {Object} [options]
|
|
18
|
+
* @param {number} [options.ttlMs] - TTL in milliseconds (default 60000)
|
|
19
|
+
* @param {boolean} [options.noCache] - Bypass cache entirely
|
|
20
|
+
* @returns {*|null} Cached result or null if miss/expired
|
|
21
|
+
*/
|
|
22
|
+
function getCached(dirPath, options = {}) {
|
|
23
|
+
const { ttlMs = DEFAULT_TTL_MS, noCache = false } = options;
|
|
24
|
+
|
|
25
|
+
if (noCache) return null;
|
|
26
|
+
|
|
27
|
+
const entry = _cache.get(dirPath);
|
|
28
|
+
if (!entry) return null;
|
|
29
|
+
|
|
30
|
+
// Check TTL
|
|
31
|
+
if (Date.now() - entry.cachedAt > ttlMs) {
|
|
32
|
+
_cache.delete(dirPath);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check directory mtime hasn't changed
|
|
37
|
+
try {
|
|
38
|
+
const stat = fs.statSync(dirPath);
|
|
39
|
+
const currentMtime = stat.mtimeMs;
|
|
40
|
+
if (currentMtime !== entry.mtimeMs) {
|
|
41
|
+
_cache.delete(dirPath);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
_cache.delete(dirPath);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return entry.data;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Store scan result in cache
|
|
54
|
+
* @param {string} dirPath - Directory path
|
|
55
|
+
* @param {*} data - Data to cache
|
|
56
|
+
*/
|
|
57
|
+
function setCached(dirPath, data) {
|
|
58
|
+
try {
|
|
59
|
+
const stat = fs.statSync(dirPath);
|
|
60
|
+
_cache.set(dirPath, {
|
|
61
|
+
data,
|
|
62
|
+
mtimeMs: stat.mtimeMs,
|
|
63
|
+
cachedAt: Date.now(),
|
|
64
|
+
});
|
|
65
|
+
} catch {
|
|
66
|
+
// Can't stat directory, don't cache
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Clear all cached entries
|
|
72
|
+
*/
|
|
73
|
+
function clearCache() {
|
|
74
|
+
_cache.clear();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get cache stats
|
|
79
|
+
* @returns {{ size: number, keys: string[] }}
|
|
80
|
+
*/
|
|
81
|
+
function getCacheStats() {
|
|
82
|
+
return {
|
|
83
|
+
size: _cache.size,
|
|
84
|
+
keys: Array.from(_cache.keys()),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Wrap a scan function with caching
|
|
90
|
+
* @param {Function} scanFn - Function that takes (dirPath, ...args) and returns results
|
|
91
|
+
* @param {Object} [options]
|
|
92
|
+
* @param {number} [options.ttlMs] - TTL in ms
|
|
93
|
+
* @returns {Function} Cached version of scanFn
|
|
94
|
+
*/
|
|
95
|
+
function withCache(scanFn, options = {}) {
|
|
96
|
+
return function cachedScan(dirPath, ...args) {
|
|
97
|
+
const noCache = process.argv.includes('--no-cache') || options.noCache;
|
|
98
|
+
const cached = getCached(dirPath, { ...options, noCache });
|
|
99
|
+
if (cached !== null) return cached;
|
|
100
|
+
|
|
101
|
+
const result = scanFn(dirPath, ...args);
|
|
102
|
+
setCached(dirPath, result);
|
|
103
|
+
return result;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
getCached,
|
|
109
|
+
setCached,
|
|
110
|
+
clearCache,
|
|
111
|
+
getCacheStats,
|
|
112
|
+
withCache,
|
|
113
|
+
DEFAULT_TTL_MS,
|
|
114
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Template Loader Factory
|
|
8
|
+
*
|
|
9
|
+
* Centralized template reading with caching and frontmatter parsing.
|
|
10
|
+
* Replaces scattered readFileSync + parse patterns in generator scripts.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const _templateCache = new Map();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read a template file with optional caching
|
|
17
|
+
* @param {string} filePath - Absolute path to template file
|
|
18
|
+
* @param {Object} [options]
|
|
19
|
+
* @param {boolean} [options.cache] - Enable caching (default true)
|
|
20
|
+
* @param {boolean} [options.parseFrontmatter] - Parse YAML frontmatter (default false)
|
|
21
|
+
* @returns {{ content: string, frontmatter: Object|null, raw: string }}
|
|
22
|
+
*/
|
|
23
|
+
function loadTemplate(filePath, options = {}) {
|
|
24
|
+
const { cache = true, parseFrontmatter = false } = options;
|
|
25
|
+
|
|
26
|
+
// Check cache
|
|
27
|
+
if (cache) {
|
|
28
|
+
const cached = _templateCache.get(filePath);
|
|
29
|
+
if (cached) {
|
|
30
|
+
try {
|
|
31
|
+
const stat = fs.statSync(filePath);
|
|
32
|
+
if (stat.mtimeMs === cached.mtimeMs) {
|
|
33
|
+
return cached.result;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// File changed or gone, fall through to reload
|
|
37
|
+
}
|
|
38
|
+
_templateCache.delete(filePath);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Read file
|
|
43
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
44
|
+
|
|
45
|
+
let content = raw;
|
|
46
|
+
let frontmatter = null;
|
|
47
|
+
|
|
48
|
+
if (parseFrontmatter) {
|
|
49
|
+
const parsed = extractSimpleFrontmatter(raw);
|
|
50
|
+
content = parsed.content;
|
|
51
|
+
frontmatter = parsed.frontmatter;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const result = { content, frontmatter, raw };
|
|
55
|
+
|
|
56
|
+
// Cache with mtime
|
|
57
|
+
if (cache) {
|
|
58
|
+
try {
|
|
59
|
+
const stat = fs.statSync(filePath);
|
|
60
|
+
_templateCache.set(filePath, { result, mtimeMs: stat.mtimeMs });
|
|
61
|
+
} catch {
|
|
62
|
+
// Can't stat, don't cache
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Simple frontmatter extraction (no js-yaml dependency)
|
|
71
|
+
* Handles key: value pairs in --- delimited blocks
|
|
72
|
+
* @param {string} raw - Raw file content
|
|
73
|
+
* @returns {{ frontmatter: Object, content: string }}
|
|
74
|
+
*/
|
|
75
|
+
function extractSimpleFrontmatter(raw) {
|
|
76
|
+
const fmRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
77
|
+
const match = raw.match(fmRegex);
|
|
78
|
+
|
|
79
|
+
if (!match) {
|
|
80
|
+
return { frontmatter: {}, content: raw };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const fmBlock = match[1];
|
|
84
|
+
const content = match[2];
|
|
85
|
+
const frontmatter = {};
|
|
86
|
+
|
|
87
|
+
for (const line of fmBlock.split('\n')) {
|
|
88
|
+
const colonIdx = line.indexOf(':');
|
|
89
|
+
if (colonIdx > 0) {
|
|
90
|
+
const key = line.substring(0, colonIdx).trim();
|
|
91
|
+
let value = line.substring(colonIdx + 1).trim();
|
|
92
|
+
// Remove quotes if present
|
|
93
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
94
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
95
|
+
value = value.slice(1, -1);
|
|
96
|
+
}
|
|
97
|
+
frontmatter[key] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { frontmatter, content };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create a cached template loader for a specific directory
|
|
106
|
+
* @param {string} baseDir - Base directory for templates
|
|
107
|
+
* @param {Object} [defaultOptions] - Default options for loadTemplate
|
|
108
|
+
* @returns {Function} Loader function: (relativePath) => templateResult
|
|
109
|
+
*/
|
|
110
|
+
function createTemplateLoader(baseDir, defaultOptions = {}) {
|
|
111
|
+
return function load(relativePath, overrides = {}) {
|
|
112
|
+
const filePath = path.join(baseDir, relativePath);
|
|
113
|
+
return loadTemplate(filePath, { ...defaultOptions, ...overrides });
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Clear the template cache
|
|
119
|
+
*/
|
|
120
|
+
function clearTemplateCache() {
|
|
121
|
+
_templateCache.clear();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get template cache stats
|
|
126
|
+
* @returns {{ size: number, keys: string[] }}
|
|
127
|
+
*/
|
|
128
|
+
function getTemplateCacheStats() {
|
|
129
|
+
return {
|
|
130
|
+
size: _templateCache.size,
|
|
131
|
+
keys: Array.from(_templateCache.keys()),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
loadTemplate,
|
|
137
|
+
createTemplateLoader,
|
|
138
|
+
clearTemplateCache,
|
|
139
|
+
getTemplateCacheStats,
|
|
140
|
+
extractSimpleFrontmatter,
|
|
141
|
+
};
|
package/package.json
CHANGED
package/scripts/agent-loop.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
|
-
const {
|
|
16
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
17
17
|
|
|
18
18
|
// Shared utilities
|
|
19
19
|
const { c, box } = require('../lib/colors');
|
|
@@ -178,7 +178,7 @@ function detectPlatform() {
|
|
|
178
178
|
*/
|
|
179
179
|
function checkTmuxAvailability() {
|
|
180
180
|
try {
|
|
181
|
-
|
|
181
|
+
execFileSync('which', ['tmux'], { encoding: 'utf8', stdio: 'pipe' });
|
|
182
182
|
return { available: true };
|
|
183
183
|
} catch (e) {
|
|
184
184
|
const platform = detectPlatform();
|
|
@@ -197,8 +197,9 @@ function checkTmuxAvailability() {
|
|
|
197
197
|
*/
|
|
198
198
|
function getGitInfo(rootDir) {
|
|
199
199
|
try {
|
|
200
|
-
const output =
|
|
201
|
-
'
|
|
200
|
+
const output = execFileSync(
|
|
201
|
+
'bash',
|
|
202
|
+
['-c', 'git branch --show-current && git rev-parse --short HEAD && git log -1 --format="%s"'],
|
|
202
203
|
{ cwd: rootDir, encoding: 'utf8', timeout: 5000 }
|
|
203
204
|
);
|
|
204
205
|
const lines = output.trim().split('\n');
|
|
@@ -376,7 +377,7 @@ function runArchival(rootDir, cache = null) {
|
|
|
376
377
|
if (toArchiveCount > 0) {
|
|
377
378
|
// Run archival
|
|
378
379
|
try {
|
|
379
|
-
|
|
380
|
+
execFileSync('bash', ['scripts/archive-completed-stories.sh'], {
|
|
380
381
|
cwd: rootDir,
|
|
381
382
|
encoding: 'utf8',
|
|
382
383
|
stdio: 'pipe',
|
|
@@ -493,7 +494,7 @@ function checkParallelSessions(rootDir) {
|
|
|
493
494
|
|
|
494
495
|
try {
|
|
495
496
|
// PERFORMANCE: Single subprocess call instead of 3 (register + count + status)
|
|
496
|
-
const fullStatusOutput =
|
|
497
|
+
const fullStatusOutput = execFileSync('node', [scriptPath, 'full-status'], {
|
|
497
498
|
cwd: rootDir,
|
|
498
499
|
encoding: 'utf8',
|
|
499
500
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -515,7 +516,7 @@ function checkParallelSessions(rootDir) {
|
|
|
515
516
|
} catch (e) {
|
|
516
517
|
// Fall back to individual calls if full-status not available (older version)
|
|
517
518
|
try {
|
|
518
|
-
const registerOutput =
|
|
519
|
+
const registerOutput = execFileSync('node', [scriptPath, 'register'], {
|
|
519
520
|
cwd: rootDir,
|
|
520
521
|
encoding: 'utf8',
|
|
521
522
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -528,7 +529,7 @@ function checkParallelSessions(rootDir) {
|
|
|
528
529
|
}
|
|
529
530
|
|
|
530
531
|
try {
|
|
531
|
-
const countOutput =
|
|
532
|
+
const countOutput = execFileSync('node', [scriptPath, 'count'], {
|
|
532
533
|
cwd: rootDir,
|
|
533
534
|
encoding: 'utf8',
|
|
534
535
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -540,7 +541,7 @@ function checkParallelSessions(rootDir) {
|
|
|
540
541
|
}
|
|
541
542
|
|
|
542
543
|
try {
|
|
543
|
-
const statusOutput =
|
|
544
|
+
const statusOutput = execFileSync('node', [scriptPath, 'status'], {
|
|
544
545
|
cwd: rootDir,
|
|
545
546
|
encoding: 'utf8',
|
|
546
547
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -1076,7 +1077,7 @@ function getChangelogEntries(version) {
|
|
|
1076
1077
|
async function runAutoUpdate(rootDir, fromVersion, toVersion) {
|
|
1077
1078
|
const runUpdate = () => {
|
|
1078
1079
|
// Use stdio: 'pipe' to capture output instead of showing everything
|
|
1079
|
-
return
|
|
1080
|
+
return execFileSync('npx', ['agileflow@latest', 'update', '--force'], {
|
|
1080
1081
|
cwd: rootDir,
|
|
1081
1082
|
encoding: 'utf8',
|
|
1082
1083
|
stdio: 'pipe',
|
|
@@ -1097,7 +1098,7 @@ async function runAutoUpdate(rootDir, fromVersion, toVersion) {
|
|
|
1097
1098
|
if (e.message && (e.message.includes('ETARGET') || e.message.includes('notarget'))) {
|
|
1098
1099
|
console.log(`${c.dim} Clearing npm cache and retrying...${c.reset}`);
|
|
1099
1100
|
try {
|
|
1100
|
-
|
|
1101
|
+
execFileSync('npm', ['cache', 'clean', '--force'], { stdio: 'pipe', timeout: 30000 });
|
|
1101
1102
|
runUpdate();
|
|
1102
1103
|
console.log(`${c.mintGreen}✓ Update complete${c.reset}`);
|
|
1103
1104
|
return true;
|
|
@@ -1705,7 +1706,10 @@ async function main() {
|
|
|
1705
1706
|
let agentTeamsInfo = {};
|
|
1706
1707
|
if (featureFlags) {
|
|
1707
1708
|
try {
|
|
1708
|
-
agentTeamsInfo = featureFlags.getAgentTeamsDisplayInfo({
|
|
1709
|
+
agentTeamsInfo = featureFlags.getAgentTeamsDisplayInfo({
|
|
1710
|
+
rootDir,
|
|
1711
|
+
metadata: cache?.metadata,
|
|
1712
|
+
});
|
|
1709
1713
|
} catch (e) {
|
|
1710
1714
|
// Silently fail - Agent Teams info is non-critical
|
|
1711
1715
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
const fs = require('fs');
|
|
21
21
|
const path = require('path');
|
|
22
|
-
const {
|
|
22
|
+
const { execFileSync } = require('child_process');
|
|
23
23
|
|
|
24
24
|
// Shared utilities
|
|
25
25
|
const { c } = require('../lib/colors');
|
|
@@ -79,27 +79,30 @@ function getSessionState(rootDir) {
|
|
|
79
79
|
function getGitDiff(rootDir) {
|
|
80
80
|
try {
|
|
81
81
|
// Get list of changed files (staged and unstaged)
|
|
82
|
-
const diffFiles =
|
|
82
|
+
const diffFiles = execFileSync('git', ['diff', '--name-only', 'HEAD'], {
|
|
83
83
|
cwd: rootDir,
|
|
84
84
|
encoding: 'utf8',
|
|
85
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
85
86
|
})
|
|
86
87
|
.trim()
|
|
87
88
|
.split('\n')
|
|
88
89
|
.filter(Boolean);
|
|
89
90
|
|
|
90
91
|
// Get staged files
|
|
91
|
-
const stagedFiles =
|
|
92
|
+
const stagedFiles = execFileSync('git', ['diff', '--cached', '--name-only'], {
|
|
92
93
|
cwd: rootDir,
|
|
93
94
|
encoding: 'utf8',
|
|
95
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
94
96
|
})
|
|
95
97
|
.trim()
|
|
96
98
|
.split('\n')
|
|
97
99
|
.filter(Boolean);
|
|
98
100
|
|
|
99
101
|
// Get untracked files
|
|
100
|
-
const untrackedFiles =
|
|
102
|
+
const untrackedFiles = execFileSync('git', ['ls-files', '--others', '--exclude-standard'], {
|
|
101
103
|
cwd: rootDir,
|
|
102
104
|
encoding: 'utf8',
|
|
105
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
103
106
|
})
|
|
104
107
|
.trim()
|
|
105
108
|
.split('\n')
|
|
@@ -112,9 +115,10 @@ function getGitDiff(rootDir) {
|
|
|
112
115
|
let additions = 0;
|
|
113
116
|
let deletions = 0;
|
|
114
117
|
try {
|
|
115
|
-
const stats =
|
|
118
|
+
const stats = execFileSync('git', ['diff', '--shortstat', 'HEAD'], {
|
|
116
119
|
cwd: rootDir,
|
|
117
120
|
encoding: 'utf8',
|
|
121
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
118
122
|
});
|
|
119
123
|
const addMatch = stats.match(/(\d+) insertion/);
|
|
120
124
|
const delMatch = stats.match(/(\d+) deletion/);
|