@magnusekdahl/parallix 1.2.0 → 1.3.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/index.js +190 -185
- package/lib/agents/agents.js +737 -838
- package/lib/agents/agents.ts +930 -0
- package/lib/agents/claude-telemetry.js +149 -154
- package/lib/agents/claude-telemetry.ts +233 -0
- package/lib/agents/claude.js +121 -131
- package/lib/agents/claude.ts +156 -0
- package/lib/agents/codex-telemetry.js +176 -164
- package/lib/agents/codex-telemetry.ts +205 -0
- package/lib/agents/codex.js +168 -201
- package/lib/agents/codex.ts +236 -0
- package/lib/agents/limit-hit.js +216 -226
- package/lib/agents/limit-hit.ts +262 -0
- package/lib/agents/mistral-telemetry.js +7 -10
- package/lib/agents/mistral-telemetry.ts +44 -0
- package/lib/agents/mistral.js +40 -58
- package/lib/agents/mistral.ts +84 -0
- package/lib/agents/opencode-export.js +126 -127
- package/lib/agents/opencode-export.ts +160 -0
- package/lib/agents/opencode-telemetry.js +282 -261
- package/lib/agents/opencode-telemetry.ts +350 -0
- package/lib/agents/opencode.js +263 -305
- package/lib/agents/opencode.ts +370 -0
- package/lib/agents/stage-telemetry.js +23 -24
- package/lib/agents/stage-telemetry.ts +47 -0
- package/lib/commands/active.js +502 -579
- package/lib/commands/active.ts +665 -0
- package/lib/commands/checkpoint.js +98 -74
- package/lib/commands/checkpoint.ts +79 -0
- package/lib/commands/config.js +61 -31
- package/lib/commands/config.ts +47 -0
- package/lib/commands/coverage-gate.js +271 -246
- package/lib/commands/coverage-gate.ts +362 -0
- package/lib/commands/diff.js +140 -119
- package/lib/commands/diff.ts +125 -0
- package/lib/commands/draft.js +770 -923
- package/lib/commands/draft.ts +1021 -0
- package/lib/commands/handoff.js +588 -608
- package/lib/commands/handoff.ts +694 -0
- package/lib/commands/integrate.js +1332 -1463
- package/lib/commands/integrate.ts +1688 -0
- package/lib/commands/mission-start.js +279 -237
- package/lib/commands/mission-start.ts +263 -0
- package/lib/commands/rebase.js +606 -582
- package/lib/commands/rebase.ts +635 -0
- package/lib/commands/repair-handoff.js +181 -179
- package/lib/commands/repair-handoff.ts +228 -0
- package/lib/commands/resolve-conflict.js +121 -102
- package/lib/commands/resolve-conflict.ts +112 -0
- package/lib/commands/review.js +3 -4
- package/lib/commands/review.ts +14 -0
- package/lib/commands/setup-review.js +4 -5
- package/lib/commands/setup-review.ts +14 -0
- package/lib/commands/setup.js +3 -3
- package/lib/commands/setup.ts +2 -0
- package/lib/commands/stats-backfill.js +394 -359
- package/lib/commands/stats-backfill.ts +418 -0
- package/lib/commands/stats.js +1424 -1665
- package/lib/commands/stats.ts +2186 -0
- package/lib/commands/status.js +219 -200
- package/lib/commands/status.ts +219 -0
- package/lib/commands/verify.js +3 -1
- package/lib/commands/verify.ts +2 -0
- package/lib/core/fmt.js +140 -211
- package/lib/core/fmt.ts +189 -0
- package/lib/core/git.js +121 -102
- package/lib/core/git.ts +146 -0
- package/lib/core/gitignore.js +59 -106
- package/lib/core/gitignore.ts +103 -0
- package/lib/core/mission-utils.js +833 -875
- package/lib/core/mission-utils.ts +998 -0
- package/lib/core/nels.js +112 -127
- package/lib/core/nels.ts +189 -0
- package/lib/core/persistent-data-migration.js +247 -205
- package/lib/core/persistent-data-migration.ts +255 -0
- package/lib/core/product-config.js +400 -476
- package/lib/core/product-config.ts +518 -0
- package/lib/core/runtime-matrix.js +41 -87
- package/lib/core/runtime-matrix.ts +84 -0
- package/lib/core/spawn-tee.js +152 -192
- package/lib/core/spawn-tee.ts +202 -0
- package/lib/core/state-map.js +92 -92
- package/lib/core/state-map.ts +115 -0
- package/lib/core/storage.js +101 -129
- package/lib/core/storage.ts +182 -0
- package/lib/core/subagent-limit.js +15 -16
- package/lib/core/subagent-limit.ts +25 -0
- package/lib/core/verification.js +148 -138
- package/lib/core/verification.ts +200 -0
- package/lib/index.js +142 -59
- package/lib/index.ts +134 -0
- package/lib/review/rebase.js +142 -145
- package/lib/review/rebase.ts +188 -0
- package/lib/review/review-adapter.js +99 -87
- package/lib/review/review-adapter.ts +198 -0
- package/lib/review/review-artifacts.js +486 -629
- package/lib/review/review-artifacts.ts +622 -0
- package/lib/review/review-commands.js +1220 -1373
- package/lib/review/review-commands.ts +1579 -0
- package/lib/review/review-events.js +544 -886
- package/lib/review/review-events.ts +954 -0
- package/lib/review/review-loop.js +945 -1020
- package/lib/review/review-loop.ts +1174 -0
- package/lib/review/review-polling.js +86 -115
- package/lib/review/review-polling.ts +194 -0
- package/lib/review/review-prompts.js +154 -189
- package/lib/review/review-prompts.ts +182 -0
- package/lib/review/review-state.js +246 -225
- package/lib/review/review-state.ts +353 -0
- package/lib/review/review.js +63 -84
- package/lib/review/review.ts +138 -0
- package/lib/tools/backlog.js +694 -680
- package/lib/tools/backlog.ts +835 -0
- package/lib/tools/forgejo.js +1270 -1405
- package/lib/tools/forgejo.ts +1793 -0
- package/lib/tools/gatekeeper.js +106 -96
- package/lib/tools/gatekeeper.ts +124 -0
- package/lib/tools/redgreen.js +200 -181
- package/lib/tools/redgreen.ts +221 -0
- package/lib/tools/sessions.js +58 -52
- package/lib/tools/sessions.ts +81 -0
- package/lib/tools/setup-review.js +902 -975
- package/lib/tools/setup-review.ts +1152 -0
- package/package.json +7 -5
- package/prompts/draft.md +1 -0
- package/prompts/portfolio.md +1 -1
- package/px.js +226 -210
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
const REQUIRED_ADAPTER_KEYS = ['tasks', 'missions', 'verification', 'review', 'agents'] as const;
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CONFIG = Object.freeze({
|
|
8
|
+
product: {
|
|
9
|
+
name: 'Workflow',
|
|
10
|
+
targetUser: 'Engineering teams using git, task tracking, and code review',
|
|
11
|
+
},
|
|
12
|
+
adapters: {
|
|
13
|
+
tasks: { provider: 'backlog-md', storage: 'backlog', stateMap: 'state-map.json' },
|
|
14
|
+
missions: {
|
|
15
|
+
baseDir: 'missions',
|
|
16
|
+
branchPrefix: 'mission/',
|
|
17
|
+
worktreePattern: '../<repo>-<slug>',
|
|
18
|
+
},
|
|
19
|
+
verification: { defaultArea: 'docs' },
|
|
20
|
+
stats: { path: 'stats.csv' },
|
|
21
|
+
review: {},
|
|
22
|
+
agents: {},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
type PlainObject = Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
export function isPlainObject(value: unknown): value is PlainObject {
|
|
29
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function deepMerge(base: PlainObject, override: PlainObject): PlainObject {
|
|
33
|
+
if (!isPlainObject(override)) {return base;}
|
|
34
|
+
const out = { ...base };
|
|
35
|
+
for (const [key, value] of Object.entries(override)) {
|
|
36
|
+
const val = out[key] as PlainObject | undefined;
|
|
37
|
+
out[key] = isPlainObject(value) && isPlainObject(val)
|
|
38
|
+
? deepMerge(val, value)
|
|
39
|
+
: value;
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function defaultConfig(): typeof DEFAULT_CONFIG {
|
|
45
|
+
return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function configCandidates(rootDir: string = process.cwd()): string[] {
|
|
49
|
+
return [path.join(rootDir, 'workflow.config.json')];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function findWorkflowConfig(rootDir: string = process.cwd()): string | null {
|
|
53
|
+
return configCandidates(rootDir).find(candidate => fs.existsSync(candidate)) || null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface LoadedConfigResult {
|
|
57
|
+
found: boolean;
|
|
58
|
+
configPath: string | null;
|
|
59
|
+
config: unknown;
|
|
60
|
+
parseError: Error | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function loadWorkflowConfig(rootDir: string = process.cwd()): LoadedConfigResult {
|
|
64
|
+
const configPath = findWorkflowConfig(rootDir);
|
|
65
|
+
if (!configPath) {
|
|
66
|
+
return { found: false, configPath: null, config: null, parseError: null };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let raw: string;
|
|
70
|
+
try {
|
|
71
|
+
raw = fs.readFileSync(configPath, 'utf8');
|
|
72
|
+
} catch (error: unknown) {
|
|
73
|
+
const e = error as Error & { code?: string };
|
|
74
|
+
if (e && e.code === 'ENOENT') {
|
|
75
|
+
return { found: false, configPath: null, config: null, parseError: null };
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
return {
|
|
81
|
+
found: true,
|
|
82
|
+
configPath,
|
|
83
|
+
config: JSON.parse(raw),
|
|
84
|
+
parseError: null,
|
|
85
|
+
};
|
|
86
|
+
} catch (error: unknown) {
|
|
87
|
+
return {
|
|
88
|
+
found: true,
|
|
89
|
+
configPath,
|
|
90
|
+
config: null,
|
|
91
|
+
parseError: error as Error,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function loadEffectiveConfig(rootDir: string = process.cwd()): typeof DEFAULT_CONFIG {
|
|
97
|
+
const loaded = loadWorkflowConfig(rootDir);
|
|
98
|
+
if (!loaded.found || loaded.parseError || !isPlainObject(loaded.config)) {
|
|
99
|
+
return defaultConfig();
|
|
100
|
+
}
|
|
101
|
+
if (validateWorkflowConfig(loaded.config).length > 0) {
|
|
102
|
+
return defaultConfig();
|
|
103
|
+
}
|
|
104
|
+
return deepMerge(defaultConfig(), loaded.config as PlainObject) as typeof DEFAULT_CONFIG;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function validateWorkflowConfig(config: unknown): string[] {
|
|
108
|
+
if (!isPlainObject(config)) {
|
|
109
|
+
return ['top-level JSON object is required'];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const cfg = config as PlainObject;
|
|
113
|
+
const issues: string[] = [];
|
|
114
|
+
if ('product' in cfg && !isPlainObject(cfg.product)) {
|
|
115
|
+
issues.push('product must be an object');
|
|
116
|
+
}
|
|
117
|
+
if ('adapters' in cfg) {
|
|
118
|
+
const adapters = cfg.adapters as PlainObject | undefined;
|
|
119
|
+
if (!isPlainObject(adapters)) {
|
|
120
|
+
issues.push('adapters must be an object');
|
|
121
|
+
} else {
|
|
122
|
+
for (const [key, value] of Object.entries(adapters)) {
|
|
123
|
+
if (!isPlainObject(value)) {
|
|
124
|
+
issues.push(`adapters.${key} must be an object`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return issues;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function detectLegacyRepoLayout(rootDir: string = process.cwd()): boolean {
|
|
133
|
+
const backlogDir = path.join(rootDir, 'backlog');
|
|
134
|
+
const missionDocsDir = path.join(rootDir, 'docs', 'missions');
|
|
135
|
+
const verifyScript = path.join(rootDir, 'scripts', 'verify-local.sh');
|
|
136
|
+
|
|
137
|
+
return fs.existsSync(backlogDir) && fs.existsSync(missionDocsDir) && fs.existsSync(verifyScript);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function isStandaloneWorkflowLayout(rootDir: string = process.cwd()): boolean {
|
|
141
|
+
const workflowIndex = path.join(rootDir, 'workflow', 'index.js');
|
|
142
|
+
const workflowConfig = path.join(rootDir, 'workflow.config.json');
|
|
143
|
+
|
|
144
|
+
return fs.existsSync(workflowIndex) && fs.existsSync(workflowConfig);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function hasGitRepository(rootDir: string = process.cwd()): boolean {
|
|
148
|
+
return fs.existsSync(path.join(rootDir, '.git'));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface InitializeGitOptions {
|
|
152
|
+
spawnSyncFn?: typeof spawnSync;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function initializeGitRepository(rootDir: string = process.cwd(), options: InitializeGitOptions = {}): { ok: boolean; branch: string; mode: string } | { ok: boolean; message: string } {
|
|
156
|
+
const spawnSyncFn = options.spawnSyncFn || spawnSync;
|
|
157
|
+
const initMain = spawnSyncFn('git', ['init', '-b', 'main'], {
|
|
158
|
+
cwd: rootDir,
|
|
159
|
+
encoding: 'utf8',
|
|
160
|
+
});
|
|
161
|
+
if (initMain.status === 0) {
|
|
162
|
+
return { ok: true, branch: 'main', mode: 'init-main' };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const initFallback = spawnSyncFn('git', ['init'], {
|
|
166
|
+
cwd: rootDir,
|
|
167
|
+
encoding: 'utf8',
|
|
168
|
+
});
|
|
169
|
+
if (initFallback.status !== 0) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
message: (initFallback.stderr || initFallback.stdout || initMain.stderr || initMain.stdout || 'git init failed').trim(),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
spawnSyncFn('git', ['symbolic-ref', 'HEAD', 'refs/heads/main'], {
|
|
177
|
+
cwd: rootDir,
|
|
178
|
+
encoding: 'utf8',
|
|
179
|
+
});
|
|
180
|
+
return { ok: true, branch: 'main', mode: 'init-fallback' };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function gitIdentityEnv(): NodeJS.ProcessEnv {
|
|
184
|
+
const env = { ...process.env };
|
|
185
|
+
if (!env.GIT_AUTHOR_NAME) {env.GIT_AUTHOR_NAME = 'Workflow Setup';}
|
|
186
|
+
if (!env.GIT_AUTHOR_EMAIL) {env.GIT_AUTHOR_EMAIL = 'workflow@example.invalid';}
|
|
187
|
+
if (!env.GIT_COMMITTER_NAME) {env.GIT_COMMITTER_NAME = env.GIT_AUTHOR_NAME!;}
|
|
188
|
+
if (!env.GIT_COMMITTER_EMAIL) {env.GIT_COMMITTER_EMAIL = env.GIT_AUTHOR_EMAIL!;}
|
|
189
|
+
return env;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface CommitWorkflowOptions {
|
|
193
|
+
spawnSyncFn?: typeof spawnSync;
|
|
194
|
+
existsSyncFn?: typeof fs.existsSync;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function commitWorkflowBaseline(rootDir: string, options: CommitWorkflowOptions = {}): { ok: boolean; committed: boolean; files?: string[]; message?: string; reason?: string } {
|
|
198
|
+
const spawnSyncFn = options.spawnSyncFn || spawnSync;
|
|
199
|
+
const existsSyncFn = options.existsSyncFn || fs.existsSync;
|
|
200
|
+
const candidates = ['workflow', 'workflow.config.json'];
|
|
201
|
+
const present = candidates.filter(name => existsSyncFn(path.join(rootDir, name)));
|
|
202
|
+
if (present.length === 0) {
|
|
203
|
+
return { ok: true, committed: false, reason: 'no-workflow-files' };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const addResult = spawnSyncFn('git', ['add', '-A', '.'], {
|
|
207
|
+
cwd: rootDir,
|
|
208
|
+
encoding: 'utf8',
|
|
209
|
+
});
|
|
210
|
+
if (addResult.status !== 0) {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
committed: false,
|
|
214
|
+
message: (addResult.stderr || addResult.stdout || 'git add failed').trim(),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const commitResult = spawnSyncFn(
|
|
219
|
+
'git',
|
|
220
|
+
['commit', '-m', 'workflow: initial setup'],
|
|
221
|
+
{ cwd: rootDir, encoding: 'utf8', env: gitIdentityEnv() },
|
|
222
|
+
);
|
|
223
|
+
if (commitResult.status !== 0) {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
committed: false,
|
|
227
|
+
message: (commitResult.stderr || commitResult.stdout || 'git commit failed').trim(),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { ok: true, committed: true, files: present };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
interface StandaloneBaselineOptions {
|
|
235
|
+
spawnSyncFn?: typeof spawnSync;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function ensureStandaloneMissionBaseline(rootDir: string = process.cwd(), { spawnSyncFn = spawnSync }: StandaloneBaselineOptions = {}): { changed: boolean; committed: boolean; skipped?: boolean; failed?: boolean; message?: string; entries?: string[] } {
|
|
239
|
+
if (!isStandaloneWorkflowLayout(rootDir) || !hasGitRepository(rootDir)) {
|
|
240
|
+
return { changed: false, committed: false, skipped: true };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const statusResult = spawnSyncFn('git', ['status', '--porcelain'], {
|
|
244
|
+
cwd: rootDir,
|
|
245
|
+
encoding: 'utf8',
|
|
246
|
+
});
|
|
247
|
+
if (statusResult.status !== 0) {
|
|
248
|
+
return {
|
|
249
|
+
changed: false,
|
|
250
|
+
committed: false,
|
|
251
|
+
failed: true,
|
|
252
|
+
message: (statusResult.stderr || statusResult.stdout || 'git status failed').trim(),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const dirtyEntries = (statusResult.stdout || '')
|
|
257
|
+
.split('\n')
|
|
258
|
+
.map(line => line.trimEnd())
|
|
259
|
+
.filter(Boolean);
|
|
260
|
+
const relevantEntries = dirtyEntries.filter(line => {
|
|
261
|
+
const filePath = line.slice(3).trim();
|
|
262
|
+
return !filePath.startsWith('.workflow/') && !filePath.startsWith('.sessions/');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
if (relevantEntries.length === 0) {
|
|
266
|
+
return { changed: dirtyEntries.length > 0, committed: false, skipped: false };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const conflicted = relevantEntries.filter(line => {
|
|
270
|
+
const state = line.slice(0, 2).trim();
|
|
271
|
+
return ['DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'].includes(state);
|
|
272
|
+
});
|
|
273
|
+
if (conflicted.length > 0) {
|
|
274
|
+
return {
|
|
275
|
+
changed: true,
|
|
276
|
+
committed: false,
|
|
277
|
+
failed: true,
|
|
278
|
+
message: `conflicted files block mission baseline commit: ${conflicted.map(line => line.slice(3).trim()).join(', ')}`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const addResult = spawnSyncFn('git', ['add', '-A', '.'], {
|
|
283
|
+
cwd: rootDir,
|
|
284
|
+
encoding: 'utf8',
|
|
285
|
+
});
|
|
286
|
+
if (addResult.status !== 0) {
|
|
287
|
+
return {
|
|
288
|
+
changed: true,
|
|
289
|
+
committed: false,
|
|
290
|
+
failed: true,
|
|
291
|
+
message: (addResult.stderr || addResult.stdout || 'git add failed').trim(),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const commitResult = spawnSyncFn('git', ['commit', '-m', 'workflow: prepare standalone mission baseline'], {
|
|
296
|
+
cwd: rootDir,
|
|
297
|
+
encoding: 'utf8',
|
|
298
|
+
env: gitIdentityEnv(),
|
|
299
|
+
});
|
|
300
|
+
if (commitResult.status !== 0) {
|
|
301
|
+
return {
|
|
302
|
+
changed: true,
|
|
303
|
+
committed: false,
|
|
304
|
+
failed: true,
|
|
305
|
+
message: (commitResult.stderr || commitResult.stdout || 'git commit failed').trim(),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
changed: true,
|
|
311
|
+
committed: true,
|
|
312
|
+
entries: relevantEntries.map(line => line.slice(3).trim()),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
interface EnsureStandaloneGitRepoOptions {
|
|
317
|
+
isStandaloneWorkflowLayoutFn?: typeof isStandaloneWorkflowLayout;
|
|
318
|
+
hasGitRepositoryFn?: typeof hasGitRepository;
|
|
319
|
+
initializeGitRepositoryFn?: typeof initializeGitRepository;
|
|
320
|
+
commitWorkflowBaselineFn?: typeof commitWorkflowBaseline;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function ensureStandaloneGitRepo(rootDir: string = process.cwd(), options: EnsureStandaloneGitRepoOptions = {}): { changed: boolean; initialized: boolean; branch?: string; mode?: string; baselineCommit?: { ok: boolean; committed: boolean; files?: string[]; message?: string; reason?: string }; failed?: boolean; message?: string } {
|
|
324
|
+
const opts = options;
|
|
325
|
+
const isStandaloneWorkflowLayoutFn = opts.isStandaloneWorkflowLayoutFn || isStandaloneWorkflowLayout;
|
|
326
|
+
const hasGitRepositoryFn = opts.hasGitRepositoryFn || hasGitRepository;
|
|
327
|
+
const initializeGitRepositoryFn = opts.initializeGitRepositoryFn || initializeGitRepository;
|
|
328
|
+
const commitWorkflowBaselineFn = opts.commitWorkflowBaselineFn || commitWorkflowBaseline;
|
|
329
|
+
|
|
330
|
+
if (!isStandaloneWorkflowLayoutFn(rootDir) || hasGitRepositoryFn(rootDir)) {
|
|
331
|
+
return { changed: false, initialized: false };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const initOpts = { spawnSyncFn: (options as unknown as InitializeGitOptions).spawnSyncFn };
|
|
335
|
+
const result = initializeGitRepositoryFn(rootDir, initOpts);
|
|
336
|
+
if (!result.ok) {
|
|
337
|
+
return {
|
|
338
|
+
changed: false,
|
|
339
|
+
initialized: false,
|
|
340
|
+
failed: true,
|
|
341
|
+
message: (result as { message: string }).message || 'git init failed',
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const commitOpts = { spawnSyncFn: (options as unknown as CommitWorkflowOptions).spawnSyncFn, existsSyncFn: (options as unknown as CommitWorkflowOptions).existsSyncFn };
|
|
346
|
+
const commit = commitWorkflowBaselineFn(rootDir, commitOpts);
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
changed: true,
|
|
350
|
+
initialized: true,
|
|
351
|
+
branch: (result as { branch: string }).branch || 'main',
|
|
352
|
+
mode: (result as { mode: string }).mode || 'init-main',
|
|
353
|
+
baselineCommit: commit,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function adapterChecklist(): string[] {
|
|
358
|
+
return [
|
|
359
|
+
'Workflow runs on built-in defaults; no config file is required to start',
|
|
360
|
+
'Create workflow.config.json only to override a default (schema: workflow/config/workflow.config.schema.json)',
|
|
361
|
+
'Run `px config` to print the effective configuration',
|
|
362
|
+
'Override adapters.tasks for a different task tracker or storage path',
|
|
363
|
+
'Override adapters.missions for mission document layout and branch/worktree conventions',
|
|
364
|
+
'Override adapters.verification for your repo gate command',
|
|
365
|
+
'Override adapters.review to enable a review provider and remote naming',
|
|
366
|
+
];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function loadAdapterConfig(rootDir: string = process.cwd()): PlainObject {
|
|
370
|
+
const explicit = loadWorkflowConfig(rootDir);
|
|
371
|
+
if (!explicit.found || explicit.parseError || !isPlainObject(explicit.config)) {
|
|
372
|
+
return {};
|
|
373
|
+
}
|
|
374
|
+
return isPlainObject(explicit.config.adapters) ? explicit.config.adapters as PlainObject : {};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
interface TaskStorageResult {
|
|
378
|
+
baseDir: string;
|
|
379
|
+
tasksDir: string;
|
|
380
|
+
completedDir: string;
|
|
381
|
+
archiveTasksDir: string;
|
|
382
|
+
draftsDir: string;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function resolveTaskStorage(rootDir: string = process.cwd()): TaskStorageResult {
|
|
386
|
+
const fallbackBaseDir = path.join(rootDir, 'backlog');
|
|
387
|
+
const fallback: TaskStorageResult = {
|
|
388
|
+
baseDir: fallbackBaseDir,
|
|
389
|
+
tasksDir: path.join(fallbackBaseDir, 'tasks'),
|
|
390
|
+
completedDir: path.join(fallbackBaseDir, 'completed'),
|
|
391
|
+
archiveTasksDir: path.join(fallbackBaseDir, 'archive', 'tasks'),
|
|
392
|
+
draftsDir: path.join(fallbackBaseDir, 'drafts'),
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const tasksAdapter = loadAdapterConfig(rootDir).tasks as PlainObject | undefined || {};
|
|
396
|
+
const storage = (tasksAdapter.storagePath as string) || (tasksAdapter.storage as string);
|
|
397
|
+
if (!storage) {
|
|
398
|
+
return fallback;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (typeof storage === 'string') {
|
|
402
|
+
const storageDir = path.resolve(rootDir, storage);
|
|
403
|
+
const storageName = path.basename(storageDir);
|
|
404
|
+
if (storageName === 'tasks') {
|
|
405
|
+
const baseDir = path.dirname(storageDir);
|
|
406
|
+
return {
|
|
407
|
+
baseDir,
|
|
408
|
+
tasksDir: storageDir,
|
|
409
|
+
completedDir: path.join(baseDir, 'completed'),
|
|
410
|
+
archiveTasksDir: path.join(baseDir, 'archive', 'tasks'),
|
|
411
|
+
draftsDir: path.join(baseDir, 'drafts'),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
baseDir: storageDir,
|
|
417
|
+
tasksDir: path.join(storageDir, 'tasks'),
|
|
418
|
+
completedDir: path.join(storageDir, 'completed'),
|
|
419
|
+
archiveTasksDir: path.join(storageDir, 'archive', 'tasks'),
|
|
420
|
+
draftsDir: path.join(storageDir, 'drafts'),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (typeof storage === 'object' && !Array.isArray(storage)) {
|
|
425
|
+
const storageObj = storage as PlainObject;
|
|
426
|
+
const tasksDir = storageObj.tasksDir
|
|
427
|
+
? path.resolve(rootDir, storageObj.tasksDir as string)
|
|
428
|
+
: fallback.tasksDir;
|
|
429
|
+
const completedDir = storageObj.completedDir
|
|
430
|
+
? path.resolve(rootDir, storageObj.completedDir as string)
|
|
431
|
+
: path.join(path.dirname(tasksDir), 'completed');
|
|
432
|
+
const baseDir = path.dirname(tasksDir);
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
baseDir,
|
|
436
|
+
tasksDir,
|
|
437
|
+
completedDir,
|
|
438
|
+
archiveTasksDir: storageObj.archiveTasksDir
|
|
439
|
+
? path.resolve(rootDir, storageObj.archiveTasksDir as string)
|
|
440
|
+
: path.join(baseDir, 'archive', 'tasks'),
|
|
441
|
+
draftsDir: path.join(baseDir, 'drafts'),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return fallback;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
interface ReviewAdapterResult {
|
|
449
|
+
provider: string | null;
|
|
450
|
+
remote: string | null;
|
|
451
|
+
baseUrl: string | null;
|
|
452
|
+
repo: string | null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export function resolveReviewAdapter(rootDir: string = process.cwd()): ReviewAdapterResult {
|
|
456
|
+
const review = loadAdapterConfig(rootDir).review as PlainObject | undefined || {};
|
|
457
|
+
return {
|
|
458
|
+
provider: (review.provider as string) || null,
|
|
459
|
+
remote: (review.remote as string) || null,
|
|
460
|
+
baseUrl: (review.baseUrl as string) || null,
|
|
461
|
+
repo: (review.repo as string) || null,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function isForgejoReviewEnabled(rootDir: string = process.cwd()): boolean {
|
|
466
|
+
const review = resolveReviewAdapter(rootDir);
|
|
467
|
+
if (review.provider === null) {return false;}
|
|
468
|
+
return review.provider === 'forgejo';
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function resolveAgentAdapter(): PlainObject {
|
|
472
|
+
return {};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function resolveAgentModel(agentFamily: string, rootDir: string = process.cwd()): string | null {
|
|
476
|
+
if (!agentFamily || typeof agentFamily !== 'string') {return null;}
|
|
477
|
+
const agents = loadEffectiveConfig(rootDir).adapters.agents as PlainObject | undefined;
|
|
478
|
+
if (!isPlainObject(agents)) {return null;}
|
|
479
|
+
const models = agents.models as PlainObject | undefined;
|
|
480
|
+
if (!isPlainObject(models)) {return null;}
|
|
481
|
+
const model = models[agentFamily];
|
|
482
|
+
return typeof model === 'string' && model.length > 0 ? model : null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
interface RepositoryReadinessResult {
|
|
486
|
+
mode: string;
|
|
487
|
+
configPath: string | null;
|
|
488
|
+
issues: string[];
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function evaluateRepositoryReadiness(rootDir: string = process.cwd()): RepositoryReadinessResult {
|
|
492
|
+
const explicit = loadWorkflowConfig(rootDir);
|
|
493
|
+
|
|
494
|
+
if (!explicit.found) {
|
|
495
|
+
return {
|
|
496
|
+
mode: 'default',
|
|
497
|
+
configPath: null,
|
|
498
|
+
issues: [],
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (explicit.parseError) {
|
|
503
|
+
return {
|
|
504
|
+
mode: 'invalid',
|
|
505
|
+
configPath: explicit.configPath,
|
|
506
|
+
issues: [`invalid JSON: ${(explicit.parseError as Error).message}`],
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const issues = validateWorkflowConfig(explicit.config);
|
|
511
|
+
return {
|
|
512
|
+
mode: issues.length === 0 ? 'configured' : 'invalid',
|
|
513
|
+
configPath: explicit.configPath,
|
|
514
|
+
issues,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export { REQUIRED_ADAPTER_KEYS, DEFAULT_CONFIG };
|
|
@@ -1,95 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// instead of a divergent default-path mechanism.
|
|
15
|
-
/**
|
|
16
|
-
* @param {string} agent
|
|
17
|
-
* @param {{workflowLauncherStatusFn?: typeof workflowLauncherStatus}} [options]
|
|
18
|
-
*/
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.launcherStatus = launcherStatus;
|
|
7
|
+
exports.buildAutonomousReviewMatrix = buildAutonomousReviewMatrix;
|
|
8
|
+
exports.formatMatrixSummary = formatMatrixSummary;
|
|
9
|
+
exports.runnableDifferentFamilyExists = runnableDifferentFamilyExists;
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const agents_js_1 = require("../agents/agents.js");
|
|
13
|
+
const CONFIG_PATH = node_path_1.default.join(__dirname, '..', '..', 'config', 'agents.json');
|
|
19
14
|
function launcherStatus(agent, options = {}) {
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
const { workflowLauncherStatusFn = agents_js_1.workflowLauncherStatus } = options;
|
|
16
|
+
return workflowLauncherStatusFn(agent);
|
|
22
17
|
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @param {{step?: string, eligibleAgentsForStepFn?: typeof eligibleAgentsForStep, workflowLauncherStatusFn?: typeof workflowLauncherStatus, configPath?: string, existsSyncFn?: typeof fs.existsSync}} [options]
|
|
26
|
-
*/
|
|
27
18
|
function buildAutonomousReviewMatrix(options = {}) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const launchers = Object.fromEntries(
|
|
39
|
-
agents.map((/** @type {string} */ agent) => [agent, workflowLauncherStatusFn(agent)])
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
step,
|
|
44
|
-
agents,
|
|
45
|
-
configPath,
|
|
46
|
-
configPresent: existsSyncFn(configPath),
|
|
47
|
-
launchers
|
|
48
|
-
};
|
|
19
|
+
const { step = 'review', eligibleAgentsForStepFn = agents_js_1.eligibleAgentsForStep, workflowLauncherStatusFn = agents_js_1.workflowLauncherStatus, configPath = CONFIG_PATH, existsSyncFn = node_fs_1.default.existsSync } = options;
|
|
20
|
+
const agents = eligibleAgentsForStepFn(step);
|
|
21
|
+
const launchers = Object.fromEntries(agents.map((agent) => [agent, workflowLauncherStatusFn(agent)]));
|
|
22
|
+
return {
|
|
23
|
+
step,
|
|
24
|
+
agents,
|
|
25
|
+
configPath,
|
|
26
|
+
configPresent: existsSyncFn(configPath),
|
|
27
|
+
launchers
|
|
28
|
+
};
|
|
49
29
|
}
|
|
50
|
-
|
|
51
|
-
/** @param {{step: string, agents: string[], configPath: string, configPresent: boolean, launchers: Record<string, any>}} matrix */
|
|
52
30
|
function formatMatrixSummary(matrix) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
'excluding the implementer (config-driven via agents.json; no hardcoded routing).'
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
return lines;
|
|
31
|
+
const lines = [];
|
|
32
|
+
lines.push(`Agent eligibility config: ${matrix.configPresent ? 'present' : 'missing'} (${matrix.configPath})`);
|
|
33
|
+
lines.push(`Launcher support matrix (step: ${matrix.step}):`);
|
|
34
|
+
for (const agent of matrix.agents) {
|
|
35
|
+
const launcher = matrix.launchers[agent];
|
|
36
|
+
const status = launcher.supported ? 'supported' : 'blocked';
|
|
37
|
+
const health = launcher.health ? `, ${launcher.health}` : '';
|
|
38
|
+
const reason = launcher.reason ? `; ${launcher.reason}` : '';
|
|
39
|
+
lines.push(` - ${agent}: ${status} (${launcher.detail}${health}${reason})`);
|
|
40
|
+
}
|
|
41
|
+
lines.push('Reviewer is chosen at runtime from the eligible-and-supported pool, ' +
|
|
42
|
+
'excluding the implementer (config-driven via agents.json; no hardcoded routing).');
|
|
43
|
+
return lines;
|
|
70
44
|
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Returns true if any eligible agent other than `implementer` has a supported launcher.
|
|
74
|
-
* Used by the review loop to decide whether single-family fallback is authorized.
|
|
75
|
-
*/
|
|
76
|
-
/**
|
|
77
|
-
* @param {string} implementer
|
|
78
|
-
* @param {{step?: string, eligibleAgentsForStepFn?: typeof eligibleAgentsForStep, workflowLauncherStatusFn?: typeof workflowLauncherStatus}} [options]
|
|
79
|
-
*/
|
|
80
45
|
function runnableDifferentFamilyExists(implementer, options = {}) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
workflowLauncherStatusFn = workflowLauncherStatus
|
|
85
|
-
} = options;
|
|
86
|
-
const agents = eligibleAgentsForStepFn(step);
|
|
87
|
-
return agents.some((/** @type {string} */ a) => a !== implementer && workflowLauncherStatusFn(a).supported);
|
|
46
|
+
const { step = 'review', eligibleAgentsForStepFn = agents_js_1.eligibleAgentsForStep, workflowLauncherStatusFn = agents_js_1.workflowLauncherStatus } = options;
|
|
47
|
+
const agents = eligibleAgentsForStepFn(step);
|
|
48
|
+
return agents.some((a) => a !== implementer && workflowLauncherStatusFn(a).supported);
|
|
88
49
|
}
|
|
89
|
-
|
|
90
|
-
module.exports = {
|
|
91
|
-
launcherStatus,
|
|
92
|
-
buildAutonomousReviewMatrix,
|
|
93
|
-
formatMatrixSummary,
|
|
94
|
-
runnableDifferentFamilyExists
|
|
95
|
-
};
|