@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.
Files changed (127) hide show
  1. package/index.js +190 -185
  2. package/lib/agents/agents.js +737 -838
  3. package/lib/agents/agents.ts +930 -0
  4. package/lib/agents/claude-telemetry.js +149 -154
  5. package/lib/agents/claude-telemetry.ts +233 -0
  6. package/lib/agents/claude.js +121 -131
  7. package/lib/agents/claude.ts +156 -0
  8. package/lib/agents/codex-telemetry.js +176 -164
  9. package/lib/agents/codex-telemetry.ts +205 -0
  10. package/lib/agents/codex.js +168 -201
  11. package/lib/agents/codex.ts +236 -0
  12. package/lib/agents/limit-hit.js +216 -226
  13. package/lib/agents/limit-hit.ts +262 -0
  14. package/lib/agents/mistral-telemetry.js +7 -10
  15. package/lib/agents/mistral-telemetry.ts +44 -0
  16. package/lib/agents/mistral.js +40 -58
  17. package/lib/agents/mistral.ts +84 -0
  18. package/lib/agents/opencode-export.js +126 -127
  19. package/lib/agents/opencode-export.ts +160 -0
  20. package/lib/agents/opencode-telemetry.js +282 -261
  21. package/lib/agents/opencode-telemetry.ts +350 -0
  22. package/lib/agents/opencode.js +263 -305
  23. package/lib/agents/opencode.ts +370 -0
  24. package/lib/agents/stage-telemetry.js +23 -24
  25. package/lib/agents/stage-telemetry.ts +47 -0
  26. package/lib/commands/active.js +502 -579
  27. package/lib/commands/active.ts +665 -0
  28. package/lib/commands/checkpoint.js +98 -74
  29. package/lib/commands/checkpoint.ts +79 -0
  30. package/lib/commands/config.js +61 -31
  31. package/lib/commands/config.ts +47 -0
  32. package/lib/commands/coverage-gate.js +271 -246
  33. package/lib/commands/coverage-gate.ts +362 -0
  34. package/lib/commands/diff.js +140 -119
  35. package/lib/commands/diff.ts +125 -0
  36. package/lib/commands/draft.js +770 -923
  37. package/lib/commands/draft.ts +1021 -0
  38. package/lib/commands/handoff.js +588 -608
  39. package/lib/commands/handoff.ts +694 -0
  40. package/lib/commands/integrate.js +1332 -1463
  41. package/lib/commands/integrate.ts +1688 -0
  42. package/lib/commands/mission-start.js +279 -237
  43. package/lib/commands/mission-start.ts +263 -0
  44. package/lib/commands/rebase.js +606 -582
  45. package/lib/commands/rebase.ts +635 -0
  46. package/lib/commands/repair-handoff.js +181 -179
  47. package/lib/commands/repair-handoff.ts +228 -0
  48. package/lib/commands/resolve-conflict.js +121 -102
  49. package/lib/commands/resolve-conflict.ts +112 -0
  50. package/lib/commands/review.js +3 -4
  51. package/lib/commands/review.ts +14 -0
  52. package/lib/commands/setup-review.js +4 -5
  53. package/lib/commands/setup-review.ts +14 -0
  54. package/lib/commands/setup.js +3 -3
  55. package/lib/commands/setup.ts +2 -0
  56. package/lib/commands/stats-backfill.js +394 -359
  57. package/lib/commands/stats-backfill.ts +418 -0
  58. package/lib/commands/stats.js +1424 -1665
  59. package/lib/commands/stats.ts +2186 -0
  60. package/lib/commands/status.js +219 -200
  61. package/lib/commands/status.ts +219 -0
  62. package/lib/commands/verify.js +3 -1
  63. package/lib/commands/verify.ts +2 -0
  64. package/lib/core/fmt.js +140 -211
  65. package/lib/core/fmt.ts +189 -0
  66. package/lib/core/git.js +121 -102
  67. package/lib/core/git.ts +146 -0
  68. package/lib/core/gitignore.js +59 -106
  69. package/lib/core/gitignore.ts +103 -0
  70. package/lib/core/mission-utils.js +833 -875
  71. package/lib/core/mission-utils.ts +998 -0
  72. package/lib/core/nels.js +112 -127
  73. package/lib/core/nels.ts +189 -0
  74. package/lib/core/persistent-data-migration.js +247 -205
  75. package/lib/core/persistent-data-migration.ts +255 -0
  76. package/lib/core/product-config.js +400 -476
  77. package/lib/core/product-config.ts +518 -0
  78. package/lib/core/runtime-matrix.js +41 -87
  79. package/lib/core/runtime-matrix.ts +84 -0
  80. package/lib/core/spawn-tee.js +152 -192
  81. package/lib/core/spawn-tee.ts +202 -0
  82. package/lib/core/state-map.js +92 -92
  83. package/lib/core/state-map.ts +115 -0
  84. package/lib/core/storage.js +101 -129
  85. package/lib/core/storage.ts +182 -0
  86. package/lib/core/subagent-limit.js +15 -16
  87. package/lib/core/subagent-limit.ts +25 -0
  88. package/lib/core/verification.js +148 -138
  89. package/lib/core/verification.ts +200 -0
  90. package/lib/index.js +142 -59
  91. package/lib/index.ts +134 -0
  92. package/lib/review/rebase.js +142 -145
  93. package/lib/review/rebase.ts +188 -0
  94. package/lib/review/review-adapter.js +99 -87
  95. package/lib/review/review-adapter.ts +198 -0
  96. package/lib/review/review-artifacts.js +486 -629
  97. package/lib/review/review-artifacts.ts +622 -0
  98. package/lib/review/review-commands.js +1220 -1373
  99. package/lib/review/review-commands.ts +1579 -0
  100. package/lib/review/review-events.js +544 -886
  101. package/lib/review/review-events.ts +954 -0
  102. package/lib/review/review-loop.js +945 -1020
  103. package/lib/review/review-loop.ts +1174 -0
  104. package/lib/review/review-polling.js +86 -115
  105. package/lib/review/review-polling.ts +194 -0
  106. package/lib/review/review-prompts.js +154 -189
  107. package/lib/review/review-prompts.ts +182 -0
  108. package/lib/review/review-state.js +246 -225
  109. package/lib/review/review-state.ts +353 -0
  110. package/lib/review/review.js +63 -84
  111. package/lib/review/review.ts +138 -0
  112. package/lib/tools/backlog.js +694 -680
  113. package/lib/tools/backlog.ts +835 -0
  114. package/lib/tools/forgejo.js +1270 -1405
  115. package/lib/tools/forgejo.ts +1793 -0
  116. package/lib/tools/gatekeeper.js +106 -96
  117. package/lib/tools/gatekeeper.ts +124 -0
  118. package/lib/tools/redgreen.js +200 -181
  119. package/lib/tools/redgreen.ts +221 -0
  120. package/lib/tools/sessions.js +58 -52
  121. package/lib/tools/sessions.ts +81 -0
  122. package/lib/tools/setup-review.js +902 -975
  123. package/lib/tools/setup-review.ts +1152 -0
  124. package/package.json +7 -5
  125. package/prompts/draft.md +1 -0
  126. package/prompts/portfolio.md +1 -1
  127. 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
- const fs = require('fs');
2
- const path = require('path');
3
- const { eligibleAgentsForStep, workflowLauncherStatus } = require('../agents/agents');
4
-
5
- // Agent eligibility and selection are config-driven: parallix/config/agents.json
6
- // declares which agents are eligible per step, and each agent's launcher is
7
- // discovered by the RESOLVERS in agents.js (which resolve launchers as bare
8
- // executable names on PATH). This module no longer ships hardcoded
9
- // binary paths.
10
- const CONFIG_PATH = path.join(__dirname, '..', '..', 'config', 'agents.json');
11
-
12
- // Thin diagnostic wrapper over agents.js launcher discovery. Kept so the runtime
13
- // matrix reports exactly the discovery the launcher actually uses (RESOLVERS),
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
- const { workflowLauncherStatusFn = workflowLauncherStatus } = options;
21
- return workflowLauncherStatusFn(agent);
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
- const {
29
- step = 'review',
30
- eligibleAgentsForStepFn = eligibleAgentsForStep,
31
- workflowLauncherStatusFn = workflowLauncherStatus,
32
- configPath = CONFIG_PATH,
33
- existsSyncFn = fs.existsSync
34
- } = options;
35
-
36
- const agents = eligibleAgentsForStepFn(step);
37
- /** @param {string} agent */
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
- const lines = [];
54
-
55
- lines.push(`Agent eligibility config: ${matrix.configPresent ? 'present' : 'missing'} (${matrix.configPath})`);
56
- lines.push(`Launcher support matrix (step: ${matrix.step}):`);
57
- for (const agent of matrix.agents) {
58
- const launcher = matrix.launchers[agent];
59
- const status = launcher.supported ? 'supported' : 'blocked';
60
- const health = launcher.health ? `, ${launcher.health}` : '';
61
- const reason = launcher.reason ? `; ${launcher.reason}` : '';
62
- lines.push(` - ${agent}: ${status} (${launcher.detail}${health}${reason})`);
63
- }
64
- lines.push(
65
- 'Reviewer is chosen at runtime from the eligible-and-supported pool, ' +
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
- const {
82
- step = 'review',
83
- eligibleAgentsForStepFn = eligibleAgentsForStep,
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
- };