@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
@@ -1,536 +1,460 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { spawnSync } = require('child_process');
4
-
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.DEFAULT_CONFIG = exports.REQUIRED_ADAPTER_KEYS = void 0;
7
+ exports.isPlainObject = isPlainObject;
8
+ exports.deepMerge = deepMerge;
9
+ exports.defaultConfig = defaultConfig;
10
+ exports.configCandidates = configCandidates;
11
+ exports.findWorkflowConfig = findWorkflowConfig;
12
+ exports.loadWorkflowConfig = loadWorkflowConfig;
13
+ exports.loadEffectiveConfig = loadEffectiveConfig;
14
+ exports.validateWorkflowConfig = validateWorkflowConfig;
15
+ exports.detectLegacyRepoLayout = detectLegacyRepoLayout;
16
+ exports.isStandaloneWorkflowLayout = isStandaloneWorkflowLayout;
17
+ exports.hasGitRepository = hasGitRepository;
18
+ exports.initializeGitRepository = initializeGitRepository;
19
+ exports.gitIdentityEnv = gitIdentityEnv;
20
+ exports.commitWorkflowBaseline = commitWorkflowBaseline;
21
+ exports.ensureStandaloneMissionBaseline = ensureStandaloneMissionBaseline;
22
+ exports.ensureStandaloneGitRepo = ensureStandaloneGitRepo;
23
+ exports.adapterChecklist = adapterChecklist;
24
+ exports.loadAdapterConfig = loadAdapterConfig;
25
+ exports.resolveTaskStorage = resolveTaskStorage;
26
+ exports.resolveReviewAdapter = resolveReviewAdapter;
27
+ exports.isForgejoReviewEnabled = isForgejoReviewEnabled;
28
+ exports.resolveAgentAdapter = resolveAgentAdapter;
29
+ exports.resolveAgentModel = resolveAgentModel;
30
+ exports.evaluateRepositoryReadiness = evaluateRepositoryReadiness;
31
+ const node_fs_1 = __importDefault(require("node:fs"));
32
+ const node_path_1 = __importDefault(require("node:path"));
33
+ const node_child_process_1 = require("node:child_process");
5
34
  const REQUIRED_ADAPTER_KEYS = ['tasks', 'missions', 'verification', 'review', 'agents'];
6
-
7
- // Code-owned defaults. An absent workflow.config.json yields a working tool;
8
- // the optional override file overrides only the keys it sets. These defaults
9
- // are the single source of truth — there is no shipped example config, so there
10
- // is no second source to drift from (see task-1233 Scope Amendment).
35
+ exports.REQUIRED_ADAPTER_KEYS = REQUIRED_ADAPTER_KEYS;
11
36
  const DEFAULT_CONFIG = Object.freeze({
12
- product: {
13
- name: 'Workflow',
14
- targetUser: 'Engineering teams using git, task tracking, and code review',
15
- },
16
- adapters: {
17
- tasks: { provider: 'backlog-md', storage: 'backlog', stateMap: 'state-map.json' },
18
- missions: {
19
- baseDir: 'missions',
20
- branchPrefix: 'mission/',
21
- worktreePattern: '../<repo>-<slug>',
37
+ product: {
38
+ name: 'Workflow',
39
+ targetUser: 'Engineering teams using git, task tracking, and code review',
40
+ },
41
+ adapters: {
42
+ tasks: { provider: 'backlog-md', storage: 'backlog', stateMap: 'state-map.json' },
43
+ missions: {
44
+ baseDir: 'missions',
45
+ branchPrefix: 'mission/',
46
+ worktreePattern: '../<repo>-<slug>',
47
+ },
48
+ verification: { defaultArea: 'docs' },
49
+ stats: { path: 'stats.csv' },
50
+ review: {},
51
+ agents: {},
22
52
  },
23
- // No universal cross-repo gate exists, so the default is no validation
24
- // (no command). A repository opts into a gate by declaring
25
- // adapters.verification.command in workflow.config.json.
26
- verification: { defaultArea: 'docs' },
27
- stats: { path: 'stats.csv' },
28
- // Unset provider currently keeps Forgejo enabled (isForgejoReviewEnabled
29
- // treats null as enabled) — a WrGroceries-ism. Flipping review off-by-default
30
- // for external repos has a large review-subsystem blast radius and is deferred
31
- // to TASK-1244; WrGroceries already declares review.provider in its own config.
32
- review: {},
33
- agents: {},
34
- },
35
53
  });
36
-
37
- /** @param {unknown} value */
54
+ exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
38
55
  function isPlainObject(value) {
39
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
56
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
40
57
  }
41
-
42
- /** @param {Record<string, unknown>} base @param {Record<string, unknown>} override */
43
58
  function deepMerge(base, override) {
44
- if (!isPlainObject(override)) {return base;}
45
- const out = { ...base };
46
- for (const [key, value] of Object.entries(override)) {
47
- const val = /** @type {Record<string, unknown>} */(/** @type {unknown} */(out[key]));
48
- out[key] = isPlainObject(value) && isPlainObject(val)
49
- ? deepMerge(val, /** @type {Record<string, unknown>} */(value))
50
- : value;
51
- }
52
- return out;
59
+ if (!isPlainObject(override)) {
60
+ return base;
61
+ }
62
+ const out = { ...base };
63
+ for (const [key, value] of Object.entries(override)) {
64
+ const val = out[key];
65
+ out[key] = isPlainObject(value) && isPlainObject(val)
66
+ ? deepMerge(val, value)
67
+ : value;
68
+ }
69
+ return out;
53
70
  }
54
-
55
- // A fresh deep clone of the defaults so callers can never mutate the frozen
56
- // source.
57
71
  function defaultConfig() {
58
- return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
72
+ return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
59
73
  }
60
-
61
74
  function configCandidates(rootDir = process.cwd()) {
62
- return [path.join(rootDir, 'workflow.config.json')];
75
+ return [node_path_1.default.join(rootDir, 'workflow.config.json')];
63
76
  }
64
-
65
77
  function findWorkflowConfig(rootDir = process.cwd()) {
66
- return configCandidates(rootDir).find(candidate => fs.existsSync(candidate)) || null;
78
+ return configCandidates(rootDir).find(candidate => node_fs_1.default.existsSync(candidate)) || null;
67
79
  }
68
-
69
80
  function loadWorkflowConfig(rootDir = process.cwd()) {
70
- const configPath = findWorkflowConfig(rootDir);
71
- if (!configPath) {
72
- return { found: false, configPath: null, config: null };
73
- }
74
-
75
- let raw;
76
- try {
77
- raw = fs.readFileSync(configPath, 'utf8');
78
- } catch (error) {
79
- /** @type {Error & {code?: string}} */
80
- const e = /** @type {Error & {code?: string}} */(error);
81
- if (e && e.code === 'ENOENT') {
82
- return { found: false, configPath: null, config: null };
83
- }
84
- throw error;
85
- }
86
- try {
87
- return {
88
- found: true,
89
- configPath,
90
- config: JSON.parse(raw),
91
- parseError: null,
92
- };
93
- } catch (error) {
94
- return {
95
- found: true,
96
- configPath,
97
- config: null,
98
- parseError: error,
99
- };
100
- }
81
+ const configPath = findWorkflowConfig(rootDir);
82
+ if (!configPath) {
83
+ return { found: false, configPath: null, config: null, parseError: null };
84
+ }
85
+ let raw;
86
+ try {
87
+ raw = node_fs_1.default.readFileSync(configPath, 'utf8');
88
+ }
89
+ catch (error) {
90
+ const e = error;
91
+ if (e && e.code === 'ENOENT') {
92
+ return { found: false, configPath: null, config: null, parseError: null };
93
+ }
94
+ throw error;
95
+ }
96
+ try {
97
+ return {
98
+ found: true,
99
+ configPath,
100
+ config: JSON.parse(raw),
101
+ parseError: null,
102
+ };
103
+ }
104
+ catch (error) {
105
+ return {
106
+ found: true,
107
+ configPath,
108
+ config: null,
109
+ parseError: error,
110
+ };
111
+ }
101
112
  }
102
-
103
- // The effective config a command sees: code-owned defaults with any override
104
- // file merged on top. A missing or malformed override falls back to defaults.
105
113
  function loadEffectiveConfig(rootDir = process.cwd()) {
106
- const loaded = loadWorkflowConfig(rootDir);
107
- if (!loaded.found || loaded.parseError || !isPlainObject(loaded.config)) {
108
- return defaultConfig();
109
- }
110
- // A structurally invalid override (e.g. adapters not an object) falls back to
111
- // defaults rather than merging a bad shape into the effective config.
112
- if (validateWorkflowConfig(loaded.config).length > 0) {
113
- return defaultConfig();
114
- }
115
- return deepMerge(defaultConfig(), loaded.config);
114
+ const loaded = loadWorkflowConfig(rootDir);
115
+ if (!loaded.found || loaded.parseError || !isPlainObject(loaded.config)) {
116
+ return defaultConfig();
117
+ }
118
+ if (validateWorkflowConfig(loaded.config).length > 0) {
119
+ return defaultConfig();
120
+ }
121
+ return deepMerge(defaultConfig(), loaded.config);
116
122
  }
117
-
118
- // The override file is optional and partial: validate only the shape of what it
119
- // provides. Missing sections are filled by code-owned defaults, so absent
120
- // sections are not errors (and there are no placeholder sentinels to detect).
121
- /** @param {unknown} config */
122
123
  function validateWorkflowConfig(config) {
123
- if (!isPlainObject(config)) {
124
- return ['top-level JSON object is required'];
125
- }
126
-
127
- const cfg = /** @type {Record<string, unknown>} */(config);
128
- const issues = [];
129
- if ('product' in /** @type{Record<string,unknown>} */(cfg) && !isPlainObject(/** @type{Record<string,unknown>} */(cfg).product)) {
130
- issues.push('product must be an object');
131
- }
132
- if ('adapters' in /** @type{Record<string,unknown>} */(cfg)) {
133
- /** @type {Record<string, unknown>} */
134
- const adapters = /** @type {Record<string, unknown>} */(/** @type{Record<string,unknown>} */(cfg).adapters);
135
- if (!isPlainObject(adapters)) {
136
- issues.push('adapters must be an object');
137
- } else {
138
- for (const [key, value] of Object.entries(adapters)) {
139
- if (!isPlainObject(value)) {
140
- issues.push(`adapters.${key} must be an object`);
124
+ if (!isPlainObject(config)) {
125
+ return ['top-level JSON object is required'];
126
+ }
127
+ const cfg = config;
128
+ const issues = [];
129
+ if ('product' in cfg && !isPlainObject(cfg.product)) {
130
+ issues.push('product must be an object');
131
+ }
132
+ if ('adapters' in cfg) {
133
+ const adapters = cfg.adapters;
134
+ if (!isPlainObject(adapters)) {
135
+ issues.push('adapters must be an object');
136
+ }
137
+ else {
138
+ for (const [key, value] of Object.entries(adapters)) {
139
+ if (!isPlainObject(value)) {
140
+ issues.push(`adapters.${key} must be an object`);
141
+ }
142
+ }
141
143
  }
142
- }
143
144
  }
144
- }
145
- return issues;
145
+ return issues;
146
146
  }
147
-
148
147
  function detectLegacyRepoLayout(rootDir = process.cwd()) {
149
- const backlogDir = path.join(rootDir, 'backlog');
150
- const missionDocsDir = path.join(rootDir, 'docs', 'missions');
151
- const verifyScript = path.join(rootDir, 'scripts', 'verify-local.sh');
152
-
153
- return fs.existsSync(backlogDir) && fs.existsSync(missionDocsDir) && fs.existsSync(verifyScript);
148
+ const backlogDir = node_path_1.default.join(rootDir, 'backlog');
149
+ const missionDocsDir = node_path_1.default.join(rootDir, 'docs', 'missions');
150
+ const verifyScript = node_path_1.default.join(rootDir, 'scripts', 'verify-local.sh');
151
+ return node_fs_1.default.existsSync(backlogDir) && node_fs_1.default.existsSync(missionDocsDir) && node_fs_1.default.existsSync(verifyScript);
154
152
  }
155
-
156
153
  function isStandaloneWorkflowLayout(rootDir = process.cwd()) {
157
- const workflowIndex = path.join(rootDir, 'workflow', 'index.js');
158
- const workflowConfig = path.join(rootDir, 'workflow.config.json');
159
-
160
- return fs.existsSync(workflowIndex) && fs.existsSync(workflowConfig);
154
+ const workflowIndex = node_path_1.default.join(rootDir, 'workflow', 'index.js');
155
+ const workflowConfig = node_path_1.default.join(rootDir, 'workflow.config.json');
156
+ return node_fs_1.default.existsSync(workflowIndex) && node_fs_1.default.existsSync(workflowConfig);
161
157
  }
162
-
163
158
  function hasGitRepository(rootDir = process.cwd()) {
164
- return fs.existsSync(path.join(rootDir, '.git'));
159
+ return node_fs_1.default.existsSync(node_path_1.default.join(rootDir, '.git'));
165
160
  }
166
-
167
- /** @param {string} [rootDir] @param {{spawnSyncFn?: typeof spawnSync}} [options] */
168
161
  function initializeGitRepository(rootDir = process.cwd(), options = {}) {
169
- /** @type {typeof spawnSync} */
170
- const spawnSyncFn = options.spawnSyncFn || spawnSync;
171
- const initMain = spawnSyncFn('git', ['init', '-b', 'main'], {
172
- cwd: rootDir,
173
- encoding: 'utf8',
174
- });
175
- if (initMain.status === 0) {
176
- return { ok: true, branch: 'main', mode: 'init-main' };
177
- }
178
-
179
- const initFallback = spawnSyncFn('git', ['init'], {
180
- cwd: rootDir,
181
- encoding: 'utf8',
182
- });
183
- if (initFallback.status !== 0) {
184
- return {
185
- ok: false,
186
- message: (initFallback.stderr || initFallback.stdout || initMain.stderr || initMain.stdout || 'git init failed').trim(),
187
- };
188
- }
189
-
190
- spawnSyncFn('git', ['symbolic-ref', 'HEAD', 'refs/heads/main'], {
191
- cwd: rootDir,
192
- encoding: 'utf8',
193
- });
194
- return { ok: true, branch: 'main', mode: 'init-fallback' };
162
+ const spawnSyncFn = options.spawnSyncFn || node_child_process_1.spawnSync;
163
+ const initMain = spawnSyncFn('git', ['init', '-b', 'main'], {
164
+ cwd: rootDir,
165
+ encoding: 'utf8',
166
+ });
167
+ if (initMain.status === 0) {
168
+ return { ok: true, branch: 'main', mode: 'init-main' };
169
+ }
170
+ const initFallback = spawnSyncFn('git', ['init'], {
171
+ cwd: rootDir,
172
+ encoding: 'utf8',
173
+ });
174
+ if (initFallback.status !== 0) {
175
+ return {
176
+ ok: false,
177
+ message: (initFallback.stderr || initFallback.stdout || initMain.stderr || initMain.stdout || 'git init failed').trim(),
178
+ };
179
+ }
180
+ spawnSyncFn('git', ['symbolic-ref', 'HEAD', 'refs/heads/main'], {
181
+ cwd: rootDir,
182
+ encoding: 'utf8',
183
+ });
184
+ return { ok: true, branch: 'main', mode: 'init-fallback' };
195
185
  }
196
-
197
- // Commits the workflow files and any config that exists so the freshly
198
- // initialized repo has a baseline commit. Without this, mission worktrees
199
- // created via `git worktree add` would not have workflow/ or
200
- // workflow.config.json checked out and could not run any workflow command.
201
186
  function gitIdentityEnv() {
202
- const env = { ...process.env };
203
- if (!env.GIT_AUTHOR_NAME) {env.GIT_AUTHOR_NAME = 'Workflow Setup';}
204
- if (!env.GIT_AUTHOR_EMAIL) {env.GIT_AUTHOR_EMAIL = 'workflow@example.invalid';}
205
- if (!env.GIT_COMMITTER_NAME) {env.GIT_COMMITTER_NAME = env.GIT_AUTHOR_NAME;}
206
- if (!env.GIT_COMMITTER_EMAIL) {env.GIT_COMMITTER_EMAIL = env.GIT_AUTHOR_EMAIL;}
207
- return env;
187
+ const env = { ...process.env };
188
+ if (!env.GIT_AUTHOR_NAME) {
189
+ env.GIT_AUTHOR_NAME = 'Workflow Setup';
190
+ }
191
+ if (!env.GIT_AUTHOR_EMAIL) {
192
+ env.GIT_AUTHOR_EMAIL = 'workflow@example.invalid';
193
+ }
194
+ if (!env.GIT_COMMITTER_NAME) {
195
+ env.GIT_COMMITTER_NAME = env.GIT_AUTHOR_NAME;
196
+ }
197
+ if (!env.GIT_COMMITTER_EMAIL) {
198
+ env.GIT_COMMITTER_EMAIL = env.GIT_AUTHOR_EMAIL;
199
+ }
200
+ return env;
208
201
  }
209
-
210
- /** @param {string} rootDir @param {{spawnSyncFn?: typeof spawnSync, existsSyncFn?: typeof fs.existsSync}} [options] */
211
202
  function commitWorkflowBaseline(rootDir, options = {}) {
212
- /** @type {typeof spawnSync} */
213
- const spawnSyncFn = options.spawnSyncFn || spawnSync;
214
- /** @type {typeof fs.existsSync} */
215
- const existsSyncFn = options.existsSyncFn || fs.existsSync;
216
- const candidates = ['workflow', 'workflow.config.json'];
217
- const present = candidates.filter(name => existsSyncFn(path.join(rootDir, name)));
218
- if (present.length === 0) {
219
- return { ok: true, committed: false, reason: 'no-workflow-files' };
220
- }
221
-
222
- const addResult = spawnSyncFn('git', ['add', '-A', '.'], {
223
- cwd: rootDir,
224
- encoding: 'utf8',
225
- });
226
- if (addResult.status !== 0) {
227
- return {
228
- ok: false,
229
- committed: false,
230
- message: (addResult.stderr || addResult.stdout || 'git add failed').trim(),
231
- };
232
- }
233
-
234
- const commitResult = spawnSyncFn(
235
- 'git',
236
- ['commit', '-m', 'workflow: initial setup'],
237
- { cwd: rootDir, encoding: 'utf8', env: gitIdentityEnv() },
238
- );
239
- if (commitResult.status !== 0) {
240
- return {
241
- ok: false,
242
- committed: false,
243
- message: (commitResult.stderr || commitResult.stdout || 'git commit failed').trim(),
244
- };
245
- }
246
-
247
- return { ok: true, committed: true, files: present };
203
+ const spawnSyncFn = options.spawnSyncFn || node_child_process_1.spawnSync;
204
+ const existsSyncFn = options.existsSyncFn || node_fs_1.default.existsSync;
205
+ const candidates = ['workflow', 'workflow.config.json'];
206
+ const present = candidates.filter(name => existsSyncFn(node_path_1.default.join(rootDir, name)));
207
+ if (present.length === 0) {
208
+ return { ok: true, committed: false, reason: 'no-workflow-files' };
209
+ }
210
+ const addResult = spawnSyncFn('git', ['add', '-A', '.'], {
211
+ cwd: rootDir,
212
+ encoding: 'utf8',
213
+ });
214
+ if (addResult.status !== 0) {
215
+ return {
216
+ ok: false,
217
+ committed: false,
218
+ message: (addResult.stderr || addResult.stdout || 'git add failed').trim(),
219
+ };
220
+ }
221
+ const commitResult = spawnSyncFn('git', ['commit', '-m', 'workflow: initial setup'], { cwd: rootDir, encoding: 'utf8', env: gitIdentityEnv() });
222
+ if (commitResult.status !== 0) {
223
+ return {
224
+ ok: false,
225
+ committed: false,
226
+ message: (commitResult.stderr || commitResult.stdout || 'git commit failed').trim(),
227
+ };
228
+ }
229
+ return { ok: true, committed: true, files: present };
248
230
  }
249
-
250
- function ensureStandaloneMissionBaseline(rootDir = process.cwd(), { spawnSyncFn = spawnSync } = {}) {
251
- if (!isStandaloneWorkflowLayout(rootDir) || !hasGitRepository(rootDir)) {
252
- return { changed: false, committed: false, skipped: true };
253
- }
254
-
255
- const statusResult = spawnSyncFn('git', ['status', '--porcelain'], {
256
- cwd: rootDir,
257
- encoding: 'utf8',
258
- });
259
- if (statusResult.status !== 0) {
260
- return {
261
- changed: false,
262
- committed: false,
263
- failed: true,
264
- message: (statusResult.stderr || statusResult.stdout || 'git status failed').trim(),
265
- };
266
- }
267
-
268
- const dirtyEntries = (statusResult.stdout || '')
269
- .split('\n')
270
- .map(line => line.trimEnd())
271
- .filter(Boolean);
272
- const relevantEntries = dirtyEntries.filter(line => {
273
- const filePath = line.slice(3).trim();
274
- return !filePath.startsWith('.workflow/') && !filePath.startsWith('.sessions/');
275
- });
276
-
277
- if (relevantEntries.length === 0) {
278
- return { changed: dirtyEntries.length > 0, committed: false, skipped: false };
279
- }
280
-
281
- const conflicted = relevantEntries.filter(line => {
282
- const state = line.slice(0, 2).trim();
283
- return ['DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'].includes(state);
284
- });
285
- if (conflicted.length > 0) {
286
- return {
287
- changed: true,
288
- committed: false,
289
- failed: true,
290
- message: `conflicted files block mission baseline commit: ${conflicted.map(line => line.slice(3).trim()).join(', ')}`,
291
- };
292
- }
293
-
294
- const addResult = spawnSyncFn('git', ['add', '-A', '.'], {
295
- cwd: rootDir,
296
- encoding: 'utf8',
297
- });
298
- if (addResult.status !== 0) {
299
- return {
300
- changed: true,
301
- committed: false,
302
- failed: true,
303
- message: (addResult.stderr || addResult.stdout || 'git add failed').trim(),
304
- };
305
- }
306
-
307
- const commitResult = spawnSyncFn('git', ['commit', '-m', 'workflow: prepare standalone mission baseline'], {
308
- cwd: rootDir,
309
- encoding: 'utf8',
310
- env: gitIdentityEnv(),
311
- });
312
- if (commitResult.status !== 0) {
231
+ function ensureStandaloneMissionBaseline(rootDir = process.cwd(), { spawnSyncFn = node_child_process_1.spawnSync } = {}) {
232
+ if (!isStandaloneWorkflowLayout(rootDir) || !hasGitRepository(rootDir)) {
233
+ return { changed: false, committed: false, skipped: true };
234
+ }
235
+ const statusResult = spawnSyncFn('git', ['status', '--porcelain'], {
236
+ cwd: rootDir,
237
+ encoding: 'utf8',
238
+ });
239
+ if (statusResult.status !== 0) {
240
+ return {
241
+ changed: false,
242
+ committed: false,
243
+ failed: true,
244
+ message: (statusResult.stderr || statusResult.stdout || 'git status failed').trim(),
245
+ };
246
+ }
247
+ const dirtyEntries = (statusResult.stdout || '')
248
+ .split('\n')
249
+ .map(line => line.trimEnd())
250
+ .filter(Boolean);
251
+ const relevantEntries = dirtyEntries.filter(line => {
252
+ const filePath = line.slice(3).trim();
253
+ return !filePath.startsWith('.workflow/') && !filePath.startsWith('.sessions/');
254
+ });
255
+ if (relevantEntries.length === 0) {
256
+ return { changed: dirtyEntries.length > 0, committed: false, skipped: false };
257
+ }
258
+ const conflicted = relevantEntries.filter(line => {
259
+ const state = line.slice(0, 2).trim();
260
+ return ['DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'].includes(state);
261
+ });
262
+ if (conflicted.length > 0) {
263
+ return {
264
+ changed: true,
265
+ committed: false,
266
+ failed: true,
267
+ message: `conflicted files block mission baseline commit: ${conflicted.map(line => line.slice(3).trim()).join(', ')}`,
268
+ };
269
+ }
270
+ const addResult = spawnSyncFn('git', ['add', '-A', '.'], {
271
+ cwd: rootDir,
272
+ encoding: 'utf8',
273
+ });
274
+ if (addResult.status !== 0) {
275
+ return {
276
+ changed: true,
277
+ committed: false,
278
+ failed: true,
279
+ message: (addResult.stderr || addResult.stdout || 'git add failed').trim(),
280
+ };
281
+ }
282
+ const commitResult = spawnSyncFn('git', ['commit', '-m', 'workflow: prepare standalone mission baseline'], {
283
+ cwd: rootDir,
284
+ encoding: 'utf8',
285
+ env: gitIdentityEnv(),
286
+ });
287
+ if (commitResult.status !== 0) {
288
+ return {
289
+ changed: true,
290
+ committed: false,
291
+ failed: true,
292
+ message: (commitResult.stderr || commitResult.stdout || 'git commit failed').trim(),
293
+ };
294
+ }
313
295
  return {
314
- changed: true,
315
- committed: false,
316
- failed: true,
317
- message: (commitResult.stderr || commitResult.stdout || 'git commit failed').trim(),
296
+ changed: true,
297
+ committed: true,
298
+ entries: relevantEntries.map(line => line.slice(3).trim()),
318
299
  };
319
- }
320
-
321
- return {
322
- changed: true,
323
- committed: true,
324
- entries: relevantEntries.map(line => line.slice(3).trim()),
325
- };
326
300
  }
327
-
328
- /** @param {string} [rootDir] @param {{isStandaloneWorkflowLayoutFn?: Function, hasGitRepositoryFn?: Function, initializeGitRepositoryFn?: Function, commitWorkflowBaselineFn?: Function}} [options] */
329
301
  function ensureStandaloneGitRepo(rootDir = process.cwd(), options = {}) {
330
- /** @type {{isStandaloneWorkflowLayoutFn?: Function, hasGitRepositoryFn?: Function, initializeGitRepositoryFn?: Function, commitWorkflowBaselineFn?: Function}} */
331
- const opts = options;
332
- const isStandaloneWorkflowLayoutFn = opts.isStandaloneWorkflowLayoutFn || isStandaloneWorkflowLayout;
333
- const hasGitRepositoryFn = opts.hasGitRepositoryFn || hasGitRepository;
334
- const initializeGitRepositoryFn = opts.initializeGitRepositoryFn || initializeGitRepository;
335
- const commitWorkflowBaselineFn = opts.commitWorkflowBaselineFn || commitWorkflowBaseline;
336
-
337
- if (!isStandaloneWorkflowLayoutFn(rootDir) || hasGitRepositoryFn(rootDir)) {
338
- return { changed: false, initialized: false };
339
- }
340
-
341
- const result = initializeGitRepositoryFn(rootDir, options);
342
- if (!result.ok) {
302
+ const opts = options;
303
+ const isStandaloneWorkflowLayoutFn = opts.isStandaloneWorkflowLayoutFn || isStandaloneWorkflowLayout;
304
+ const hasGitRepositoryFn = opts.hasGitRepositoryFn || hasGitRepository;
305
+ const initializeGitRepositoryFn = opts.initializeGitRepositoryFn || initializeGitRepository;
306
+ const commitWorkflowBaselineFn = opts.commitWorkflowBaselineFn || commitWorkflowBaseline;
307
+ if (!isStandaloneWorkflowLayoutFn(rootDir) || hasGitRepositoryFn(rootDir)) {
308
+ return { changed: false, initialized: false };
309
+ }
310
+ const initOpts = { spawnSyncFn: options.spawnSyncFn };
311
+ const result = initializeGitRepositoryFn(rootDir, initOpts);
312
+ if (!result.ok) {
313
+ return {
314
+ changed: false,
315
+ initialized: false,
316
+ failed: true,
317
+ message: result.message || 'git init failed',
318
+ };
319
+ }
320
+ const commitOpts = { spawnSyncFn: options.spawnSyncFn, existsSyncFn: options.existsSyncFn };
321
+ const commit = commitWorkflowBaselineFn(rootDir, commitOpts);
343
322
  return {
344
- changed: false,
345
- initialized: false,
346
- failed: true,
347
- message: result.message || 'git init failed',
323
+ changed: true,
324
+ initialized: true,
325
+ branch: result.branch || 'main',
326
+ mode: result.mode || 'init-main',
327
+ baselineCommit: commit,
348
328
  };
349
- }
350
-
351
- const commit = commitWorkflowBaselineFn(rootDir, options);
352
-
353
- return {
354
- changed: true,
355
- initialized: true,
356
- branch: result.branch || 'main',
357
- mode: result.mode || 'init-main',
358
- baselineCommit: commit,
359
- };
360
329
  }
361
-
362
330
  function adapterChecklist() {
363
- return [
364
- 'Workflow runs on built-in defaults; no config file is required to start',
365
- 'Create workflow.config.json only to override a default (schema: workflow/config/workflow.config.schema.json)',
366
- 'Run `px config` to print the effective configuration',
367
- 'Override adapters.tasks for a different task tracker or storage path',
368
- 'Override adapters.missions for mission document layout and branch/worktree conventions',
369
- 'Override adapters.verification for your repo gate command',
370
- 'Override adapters.review to enable a review provider and remote naming',
371
- ];
331
+ return [
332
+ 'Workflow runs on built-in defaults; no config file is required to start',
333
+ 'Create workflow.config.json only to override a default (schema: workflow/config/workflow.config.schema.json)',
334
+ 'Run `px config` to print the effective configuration',
335
+ 'Override adapters.tasks for a different task tracker or storage path',
336
+ 'Override adapters.missions for mission document layout and branch/worktree conventions',
337
+ 'Override adapters.verification for your repo gate command',
338
+ 'Override adapters.review to enable a review provider and remote naming',
339
+ ];
372
340
  }
373
-
374
- // Returns the override file's adapters object, or {} when there is no override.
375
- // Each resolver (resolveTaskStorage, resolveVerificationAdapter, …) applies its
376
- // own code-owned fallback on top of this, so an absent config still yields a
377
- // fully working tool. loadEffectiveConfig (used by `node parallix config`) is
378
- // the document-level view; the per-resolver fallbacks remain the runtime source
379
- // of truth so this change does not shadow them.
380
341
  function loadAdapterConfig(rootDir = process.cwd()) {
381
- const explicit = loadWorkflowConfig(rootDir);
382
- if (!explicit.found || explicit.parseError || !isPlainObject(explicit.config)) {
383
- return {};
384
- }
385
- return isPlainObject(explicit.config.adapters) ? explicit.config.adapters : {};
342
+ const explicit = loadWorkflowConfig(rootDir);
343
+ if (!explicit.found || explicit.parseError || !isPlainObject(explicit.config)) {
344
+ return {};
345
+ }
346
+ return isPlainObject(explicit.config.adapters) ? explicit.config.adapters : {};
386
347
  }
387
-
388
348
  function resolveTaskStorage(rootDir = process.cwd()) {
389
- const fallbackBaseDir = path.join(rootDir, 'backlog');
390
- const fallback = {
391
- baseDir: fallbackBaseDir,
392
- tasksDir: path.join(fallbackBaseDir, 'tasks'),
393
- completedDir: path.join(fallbackBaseDir, 'completed'),
394
- archiveTasksDir: path.join(fallbackBaseDir, 'archive', 'tasks'),
395
- draftsDir: path.join(fallbackBaseDir, 'drafts'),
396
- };
397
-
398
- const tasksAdapter = loadAdapterConfig(rootDir).tasks || {};
399
- const storage = tasksAdapter.storagePath || tasksAdapter.storage;
400
- if (!storage) {
401
- return fallback;
402
- }
403
-
404
- if (typeof storage === 'string') {
405
- const storageDir = path.resolve(rootDir, storage);
406
- const storageName = path.basename(storageDir);
407
- if (storageName === 'tasks') {
408
- const baseDir = path.dirname(storageDir);
409
- return {
410
- baseDir,
411
- tasksDir: storageDir,
412
- completedDir: path.join(baseDir, 'completed'),
413
- archiveTasksDir: path.join(baseDir, 'archive', 'tasks'),
414
- draftsDir: path.join(baseDir, 'drafts'),
415
- };
416
- }
417
-
418
- return {
419
- baseDir: storageDir,
420
- tasksDir: path.join(storageDir, 'tasks'),
421
- completedDir: path.join(storageDir, 'completed'),
422
- archiveTasksDir: path.join(storageDir, 'archive', 'tasks'),
423
- draftsDir: path.join(storageDir, 'drafts'),
424
- };
425
- }
426
-
427
- if (typeof storage === 'object' && !Array.isArray(storage)) {
428
- const tasksDir = storage.tasksDir
429
- ? path.resolve(rootDir, storage.tasksDir)
430
- : fallback.tasksDir;
431
- const completedDir = storage.completedDir
432
- ? path.resolve(rootDir, storage.completedDir)
433
- : path.join(path.dirname(tasksDir), 'completed');
434
- const baseDir = path.dirname(tasksDir);
435
-
436
- return {
437
- baseDir,
438
- tasksDir,
439
- completedDir,
440
- archiveTasksDir: storage.archiveTasksDir
441
- ? path.resolve(rootDir, storage.archiveTasksDir)
442
- : path.join(baseDir, 'archive', 'tasks'),
443
- draftsDir: path.join(baseDir, 'drafts'),
349
+ const fallbackBaseDir = node_path_1.default.join(rootDir, 'backlog');
350
+ const fallback = {
351
+ baseDir: fallbackBaseDir,
352
+ tasksDir: node_path_1.default.join(fallbackBaseDir, 'tasks'),
353
+ completedDir: node_path_1.default.join(fallbackBaseDir, 'completed'),
354
+ archiveTasksDir: node_path_1.default.join(fallbackBaseDir, 'archive', 'tasks'),
355
+ draftsDir: node_path_1.default.join(fallbackBaseDir, 'drafts'),
444
356
  };
445
- }
446
-
447
- return fallback;
357
+ const tasksAdapter = loadAdapterConfig(rootDir).tasks || {};
358
+ const storage = tasksAdapter.storagePath || tasksAdapter.storage;
359
+ if (!storage) {
360
+ return fallback;
361
+ }
362
+ if (typeof storage === 'string') {
363
+ const storageDir = node_path_1.default.resolve(rootDir, storage);
364
+ const storageName = node_path_1.default.basename(storageDir);
365
+ if (storageName === 'tasks') {
366
+ const baseDir = node_path_1.default.dirname(storageDir);
367
+ return {
368
+ baseDir,
369
+ tasksDir: storageDir,
370
+ completedDir: node_path_1.default.join(baseDir, 'completed'),
371
+ archiveTasksDir: node_path_1.default.join(baseDir, 'archive', 'tasks'),
372
+ draftsDir: node_path_1.default.join(baseDir, 'drafts'),
373
+ };
374
+ }
375
+ return {
376
+ baseDir: storageDir,
377
+ tasksDir: node_path_1.default.join(storageDir, 'tasks'),
378
+ completedDir: node_path_1.default.join(storageDir, 'completed'),
379
+ archiveTasksDir: node_path_1.default.join(storageDir, 'archive', 'tasks'),
380
+ draftsDir: node_path_1.default.join(storageDir, 'drafts'),
381
+ };
382
+ }
383
+ if (typeof storage === 'object' && !Array.isArray(storage)) {
384
+ const storageObj = storage;
385
+ const tasksDir = storageObj.tasksDir
386
+ ? node_path_1.default.resolve(rootDir, storageObj.tasksDir)
387
+ : fallback.tasksDir;
388
+ const completedDir = storageObj.completedDir
389
+ ? node_path_1.default.resolve(rootDir, storageObj.completedDir)
390
+ : node_path_1.default.join(node_path_1.default.dirname(tasksDir), 'completed');
391
+ const baseDir = node_path_1.default.dirname(tasksDir);
392
+ return {
393
+ baseDir,
394
+ tasksDir,
395
+ completedDir,
396
+ archiveTasksDir: storageObj.archiveTasksDir
397
+ ? node_path_1.default.resolve(rootDir, storageObj.archiveTasksDir)
398
+ : node_path_1.default.join(baseDir, 'archive', 'tasks'),
399
+ draftsDir: node_path_1.default.join(baseDir, 'drafts'),
400
+ };
401
+ }
402
+ return fallback;
448
403
  }
449
-
450
404
  function resolveReviewAdapter(rootDir = process.cwd()) {
451
- const review = loadAdapterConfig(rootDir).review || {};
452
- return {
453
- provider: review.provider || null,
454
- remote: review.remote || null,
455
- baseUrl: review.baseUrl || null,
456
- repo: review.repo || null,
457
- };
405
+ const review = loadAdapterConfig(rootDir).review || {};
406
+ return {
407
+ provider: review.provider || null,
408
+ remote: review.remote || null,
409
+ baseUrl: review.baseUrl || null,
410
+ repo: review.repo || null,
411
+ };
458
412
  }
459
-
460
413
  function isForgejoReviewEnabled(rootDir = process.cwd()) {
461
- const review = resolveReviewAdapter(rootDir);
462
- if (review.provider === null) {return false;}
463
- return review.provider === 'forgejo';
414
+ const review = resolveReviewAdapter(rootDir);
415
+ if (review.provider === null) {
416
+ return false;
417
+ }
418
+ return review.provider === 'forgejo';
464
419
  }
465
-
466
420
  function resolveAgentAdapter() {
467
- return {};
421
+ return {};
468
422
  }
469
-
470
- // Returns the configured LLM model string for an agent family, or null when the
471
- // family is not listed under adapters.agents.models. A null return means the
472
- // launcher omits the model parameter entirely so the agent uses its own default.
473
- /** @param {string} agentFamily @param {string} [rootDir] */
474
423
  function resolveAgentModel(agentFamily, rootDir = process.cwd()) {
475
- if (!agentFamily || typeof agentFamily !== 'string') {return null;}
476
- const agents = loadEffectiveConfig(rootDir).adapters.agents;
477
- if (!isPlainObject(agents) || !isPlainObject(agents.models)) {return null;}
478
- const model = agents.models[agentFamily];
479
- return typeof model === 'string' && model.length > 0 ? model : null;
424
+ if (!agentFamily || typeof agentFamily !== 'string') {
425
+ return null;
426
+ }
427
+ const agents = loadEffectiveConfig(rootDir).adapters.agents;
428
+ if (!isPlainObject(agents)) {
429
+ return null;
430
+ }
431
+ const models = agents.models;
432
+ if (!isPlainObject(models)) {
433
+ return null;
434
+ }
435
+ const model = models[agentFamily];
436
+ return typeof model === 'string' && model.length > 0 ? model : null;
480
437
  }
481
-
482
438
  function evaluateRepositoryReadiness(rootDir = process.cwd()) {
483
- const explicit = loadWorkflowConfig(rootDir);
484
-
485
- // No override file: the tool runs on code-owned defaults. This is a ready
486
- // state, not an "unconfigured" failure — an absent config is valid.
487
- if (!explicit.found) {
488
- return {
489
- mode: 'default',
490
- configPath: null,
491
- issues: [],
492
- };
493
- }
494
-
495
- if (explicit.parseError) {
439
+ const explicit = loadWorkflowConfig(rootDir);
440
+ if (!explicit.found) {
441
+ return {
442
+ mode: 'default',
443
+ configPath: null,
444
+ issues: [],
445
+ };
446
+ }
447
+ if (explicit.parseError) {
448
+ return {
449
+ mode: 'invalid',
450
+ configPath: explicit.configPath,
451
+ issues: [`invalid JSON: ${explicit.parseError.message}`],
452
+ };
453
+ }
454
+ const issues = validateWorkflowConfig(explicit.config);
496
455
  return {
497
- mode: 'invalid',
498
- configPath: explicit.configPath,
499
- issues: [`invalid JSON: ${/** @type{Error} */(explicit.parseError).message}`],
456
+ mode: issues.length === 0 ? 'configured' : 'invalid',
457
+ configPath: explicit.configPath,
458
+ issues,
500
459
  };
501
- }
502
-
503
- const issues = validateWorkflowConfig(explicit.config);
504
- return {
505
- mode: issues.length === 0 ? 'configured' : 'invalid',
506
- configPath: explicit.configPath,
507
- issues,
508
- };
509
460
  }
510
-
511
- module.exports = {
512
- REQUIRED_ADAPTER_KEYS,
513
- DEFAULT_CONFIG,
514
- defaultConfig,
515
- adapterChecklist,
516
- commitWorkflowBaseline,
517
- configCandidates,
518
- deepMerge,
519
- detectLegacyRepoLayout,
520
- evaluateRepositoryReadiness,
521
- findWorkflowConfig,
522
- hasGitRepository,
523
- initializeGitRepository,
524
- isForgejoReviewEnabled,
525
- isStandaloneWorkflowLayout,
526
- loadWorkflowConfig,
527
- loadEffectiveConfig,
528
- loadAdapterConfig,
529
- resolveAgentAdapter,
530
- resolveAgentModel,
531
- resolveReviewAdapter,
532
- resolveTaskStorage,
533
- ensureStandaloneMissionBaseline,
534
- ensureStandaloneGitRepo,
535
- validateWorkflowConfig,
536
- };