@magnusekdahl/parallix 1.1.0 → 1.2.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 (68) hide show
  1. package/config/integration-pipelines.json +9 -0
  2. package/config/workflow.config.schema.json +12 -0
  3. package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +10 -8
  4. package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +14 -12
  5. package/docs/adr/0041-integration-pipeline-gates.md +1 -1
  6. package/docs/adr/0047-per-mission-change-size-budget.md +161 -0
  7. package/docs/adr/index.md +1 -0
  8. package/docs/use-cases.md +44 -0
  9. package/lib/agents/agents.js +149 -100
  10. package/lib/agents/claude-telemetry.js +14 -10
  11. package/lib/agents/claude.js +32 -11
  12. package/lib/agents/codex-telemetry.js +32 -19
  13. package/lib/agents/codex.js +36 -11
  14. package/lib/agents/limit-hit.js +42 -25
  15. package/lib/agents/mistral-telemetry.js +1 -1
  16. package/lib/agents/mistral.js +13 -3
  17. package/lib/agents/opencode-export.js +11 -5
  18. package/lib/agents/opencode-telemetry.js +75 -90
  19. package/lib/agents/opencode.js +86 -29
  20. package/lib/agents/stage-telemetry.js +3 -7
  21. package/lib/commands/active.js +139 -94
  22. package/lib/commands/checkpoint.js +3 -1
  23. package/lib/commands/config.js +5 -3
  24. package/lib/commands/coverage-gate.js +17 -10
  25. package/lib/commands/diff.js +11 -5
  26. package/lib/commands/draft.js +95 -55
  27. package/lib/commands/handoff.js +253 -65
  28. package/lib/commands/integrate.js +288 -188
  29. package/lib/commands/mission-start.js +39 -34
  30. package/lib/commands/rebase.js +32 -21
  31. package/lib/commands/repair-handoff.js +21 -18
  32. package/lib/commands/resolve-conflict.js +5 -2
  33. package/lib/commands/review.js +1 -0
  34. package/lib/commands/setup-review.js +1 -0
  35. package/lib/commands/stats-backfill.js +50 -41
  36. package/lib/commands/stats.js +653 -204
  37. package/lib/commands/status.js +33 -28
  38. package/lib/core/fmt.js +58 -15
  39. package/lib/core/git.js +7 -5
  40. package/lib/core/gitignore.js +6 -4
  41. package/lib/core/mission-utils.js +161 -88
  42. package/lib/core/nels.js +199 -0
  43. package/lib/core/persistent-data-migration.js +55 -23
  44. package/lib/core/product-config.js +44 -23
  45. package/lib/core/runtime-matrix.js +15 -2
  46. package/lib/core/spawn-tee.js +40 -13
  47. package/lib/core/state-map.js +32 -13
  48. package/lib/core/storage.js +19 -5
  49. package/lib/core/subagent-limit.js +28 -0
  50. package/lib/core/verification.js +29 -12
  51. package/lib/review/rebase.js +11 -3
  52. package/lib/review/review-adapter.js +93 -6
  53. package/lib/review/review-artifacts.js +89 -23
  54. package/lib/review/review-commands.js +139 -45
  55. package/lib/review/review-events.js +103 -52
  56. package/lib/review/review-loop.js +117 -87
  57. package/lib/review/review-polling.js +25 -5
  58. package/lib/review/review-prompts.js +36 -5
  59. package/lib/review/review-state.js +39 -15
  60. package/lib/tools/backlog.js +162 -71
  61. package/lib/tools/forgejo.js +290 -129
  62. package/lib/tools/gatekeeper.js +15 -0
  63. package/lib/tools/redgreen.js +220 -0
  64. package/lib/tools/sessions.js +12 -5
  65. package/lib/tools/setup-review.js +170 -70
  66. package/package.json +11 -3
  67. package/prompts/draft.md +6 -0
  68. package/templates/mission-scaffold.md +1 -1
@@ -11,13 +11,19 @@ const path = require('path');
11
11
  // context land in the buffer even if the agent printed a lot before exiting.
12
12
  const DEFAULT_MAX_TAIL_BYTES = 64 * 1024;
13
13
 
14
+ /** @typedef {Buffer|string} ChunkType */
14
15
  class TailBuffer {
16
+ /** @param {number} maxBytes */
15
17
  constructor(maxBytes) {
18
+ /** @type {number} */
16
19
  this.maxBytes = maxBytes;
20
+ /** @type {ChunkType[]} */
17
21
  this.chunks = [];
22
+ /** @type {number} */
18
23
  this.size = 0;
19
24
  }
20
25
 
26
+ /** @param {ChunkType} chunk */
21
27
  push(chunk) {
22
28
  this.chunks.push(chunk);
23
29
  this.size += chunk.length;
@@ -28,15 +34,20 @@ class TailBuffer {
28
34
  this.chunks.shift();
29
35
  this.size -= head.length;
30
36
  } else {
31
- this.chunks[0] = head.subarray(overflow);
37
+ if (Buffer.isBuffer(head)) {
38
+ this.chunks[0] = head.subarray(overflow);
39
+ } else {
40
+ this.chunks.shift();
41
+ this.size -= head.length;
42
+ }
32
43
  this.size -= overflow;
33
44
  }
34
45
  }
35
46
  }
36
47
 
37
48
  toString() {
38
- if (this.chunks.length === 0) return '';
39
- return Buffer.concat(this.chunks).toString('utf8');
49
+ if (this.chunks.length === 0) {return '';}
50
+ return Buffer.concat(/** @type {Buffer[]} */(this.chunks)).toString('utf8');
40
51
  }
41
52
  }
42
53
 
@@ -51,6 +62,11 @@ class TailBuffer {
51
62
  *
52
63
  * Pure I/O wrapper: no policy, no retry, no agent knowledge.
53
64
  */
65
+ /**
66
+ * @param {string} command
67
+ * @param {string[]} args
68
+ * @param {{stdoutSink?: NodeJS.WriteStream, stderrSink?: NodeJS.WriteStream, maxTailBytes?: number, noOutputWatchdog?: {onNoOutput?: Function, initialDelayMs?: number, intervalMs?: number}, cwd?: string, env?: Record<string, string>, [key: string]: any}} [options]
69
+ */
54
70
  function spawnAndTee(command, args, options = {}) {
55
71
  const {
56
72
  stdoutSink = process.stdout,
@@ -63,8 +79,11 @@ function spawnAndTee(command, args, options = {}) {
63
79
  return new Promise((resolve) => {
64
80
  const stdoutTail = new TailBuffer(maxTailBytes);
65
81
  const stderrTail = new TailBuffer(maxTailBytes);
82
+ /** @type {boolean} */
66
83
  let settled = false;
84
+ /** @type {boolean} */
67
85
  let sawOutput = false;
86
+ /** @type {ReturnType<typeof setTimeout> | null} */
68
87
  let watchdogTimer = null;
69
88
  const startTime = Date.now();
70
89
  const resolvedCwd = path.resolve(spawnOptions.cwd || process.cwd());
@@ -96,8 +115,9 @@ function spawnAndTee(command, args, options = {}) {
96
115
  }
97
116
  };
98
117
 
118
+ /** @param {{status: number|null, signal: string|null, stdout: string, stderr: string, error: any, startedAt: string, endedAt: string}} payload */
99
119
  const finish = (payload) => {
100
- if (settled) return;
120
+ if (settled) {return;}
101
121
  settled = true;
102
122
  clearWatchdog();
103
123
  payload.startedAt = new Date(startTime).toISOString();
@@ -105,19 +125,22 @@ function spawnAndTee(command, args, options = {}) {
105
125
  resolve(payload);
106
126
  };
107
127
 
128
+ /** @param {number} delayMs */
108
129
  const scheduleWatchdog = (delayMs) => {
109
- if (!noOutputWatchdog || typeof noOutputWatchdog.onNoOutput !== 'function') return;
130
+ if (!noOutputWatchdog || typeof noOutputWatchdog.onNoOutput !== 'function') {return;}
110
131
  const delay = Number.isFinite(delayMs) && delayMs >= 0 ? delayMs : 0;
111
132
  watchdogTimer = setTimeout(() => {
112
133
  watchdogTimer = null;
113
- if (settled || sawOutput) return;
114
- noOutputWatchdog.onNoOutput({
134
+ if (settled || sawOutput) {return;}
135
+ if (typeof noOutputWatchdog.onNoOutput === 'function') {
136
+ noOutputWatchdog.onNoOutput({
115
137
  command,
116
138
  args,
117
139
  pid: child.pid,
118
140
  elapsedMs: Date.now() - startTime
119
141
  });
120
- scheduleWatchdog(noOutputWatchdog.intervalMs);
142
+ }
143
+ scheduleWatchdog(noOutputWatchdog.intervalMs ?? 0);
121
144
  }, delay);
122
145
  if (typeof watchdogTimer.unref === 'function') {
123
146
  watchdogTimer.unref();
@@ -130,7 +153,7 @@ function spawnAndTee(command, args, options = {}) {
130
153
  };
131
154
 
132
155
  if (noOutputWatchdog) {
133
- scheduleWatchdog(noOutputWatchdog.initialDelayMs);
156
+ scheduleWatchdog(noOutputWatchdog.initialDelayMs ?? 0);
134
157
  }
135
158
 
136
159
  child.stdout.on('data', (chunk) => {
@@ -148,23 +171,27 @@ function spawnAndTee(command, args, options = {}) {
148
171
  }
149
172
  });
150
173
 
151
- child.on('error', (err) => {
174
+ child.on('error', (/** @type {Error} */ err) => {
152
175
  finish({
153
176
  status: null,
154
177
  signal: null,
155
178
  stdout: stdoutTail.toString(),
156
179
  stderr: stderrTail.toString(),
157
- error: err
180
+ error: err,
181
+ startedAt: new Date(startTime).toISOString(),
182
+ endedAt: new Date().toISOString()
158
183
  });
159
184
  });
160
185
 
161
- child.on('close', (code, signal) => {
186
+ child.on('close', (/** @type {number|null} */ code, /** @type {string|null} */ signal) => {
162
187
  finish({
163
188
  status: code,
164
189
  signal,
165
190
  stdout: stdoutTail.toString(),
166
191
  stderr: stderrTail.toString(),
167
- error: null
192
+ error: null,
193
+ startedAt: new Date(startTime).toISOString(),
194
+ endedAt: new Date().toISOString()
168
195
  });
169
196
  });
170
197
  });
@@ -4,25 +4,29 @@ const { loadEffectiveConfig } = require('./product-config');
4
4
 
5
5
  const SHIPPED_STATE_MAP_PATH = path.join(__dirname, '..', '..', 'config', 'state-map.json');
6
6
 
7
+ /** @param {string} rootDir @param {string} repoRelativePath */
7
8
  function resolveRepoRelativePath(rootDir, repoRelativePath) {
8
- if (!repoRelativePath || typeof repoRelativePath !== 'string') return null;
9
+ if (!repoRelativePath || typeof repoRelativePath !== 'string') {return null;}
9
10
  return path.isAbsolute(repoRelativePath)
10
11
  ? repoRelativePath
11
12
  : path.join(rootDir, repoRelativePath);
12
13
  }
13
14
 
15
+ /** @param {{rootDir?: string, config?: any, fallbackPath?: string}|string} [options] */
14
16
  function resolveStateMapPath(options = {}) {
15
- if (typeof options === 'string') return options;
17
+ if (typeof options === 'string') {return options;}
16
18
 
17
- const rootDir = options.rootDir || process.cwd();
18
- const config = options.config || loadEffectiveConfig(rootDir);
19
+ /** @type {{rootDir?: string, config?: any, fallbackPath?: string}} */
20
+ const opts = options;
21
+ const rootDir = opts.rootDir || process.cwd();
22
+ const config = opts.config || loadEffectiveConfig(rootDir);
19
23
  const configuredPath = config.adapters?.tasks?.stateMap;
20
24
  const repoPath = resolveRepoRelativePath(rootDir, configuredPath);
21
25
 
22
26
  if (repoPath && fs.existsSync(repoPath)) {
23
27
  return repoPath;
24
28
  }
25
- return options.fallbackPath || SHIPPED_STATE_MAP_PATH;
29
+ return opts.fallbackPath || SHIPPED_STATE_MAP_PATH;
26
30
  }
27
31
 
28
32
  function loadStateMap(options = {}) {
@@ -34,14 +38,16 @@ function loadStateMap(options = {}) {
34
38
  }
35
39
  }
36
40
 
41
+ /** @param {unknown} value */
37
42
  function normalizeState(value) {
38
- if (typeof value !== 'string') return value;
43
+ if (typeof value !== 'string') {return value;}
39
44
  return value.trim().toLowerCase();
40
45
  }
41
46
 
42
47
  // Virtual workflow state → actual backlog.md state name.
43
48
  // Returns null if the virtual state explicitly maps to null (no backlog.md write).
44
49
  // Returns the virtual name unchanged if it has no entry in the map (identity).
50
+ /** @param {Record<string, any> | any} mapOrOptions */
45
51
  function resolveMap(mapOrOptions) {
46
52
  if (mapOrOptions && typeof mapOrOptions === 'object' && !Array.isArray(mapOrOptions)) {
47
53
  if (Object.prototype.hasOwnProperty.call(mapOrOptions, 'rootDir')
@@ -53,33 +59,46 @@ function resolveMap(mapOrOptions) {
53
59
  return mapOrOptions || loadStateMap();
54
60
  }
55
61
 
62
+ /** @param {string} virtualState @param {Record<string, any>} [map] */
56
63
  function toActual(virtualState, map = loadStateMap()) {
57
- map = resolveMap(map);
58
- if (Object.prototype.hasOwnProperty.call(map, virtualState)) {
59
- return map[virtualState];
64
+ /** @type {Record<string, any>} */
65
+ const m = resolveMap(map) || {};
66
+ if (Object.prototype.hasOwnProperty.call(m, virtualState)) {
67
+ return m[virtualState];
60
68
  }
61
69
  return virtualState;
62
70
  }
63
71
 
64
72
  // Actual backlog.md state name → virtual workflow state name.
65
73
  // Returns the actual name unchanged if no reverse mapping exists (identity).
74
+ /** @param {string} actualState @param {Record<string, any>} [map] */
66
75
  function toVirtual(actualState, map = loadStateMap()) {
67
- map = resolveMap(map);
76
+ /** @type {Record<string, any>} */
77
+ const m = resolveMap(map) || {};
68
78
  const normalizedActual = normalizeState(actualState);
69
- for (const [virtual, actual] of Object.entries(map)) {
70
- if (normalizeState(actual) === normalizedActual) return virtual;
79
+ for (const [virtual, actual] of Object.entries(m)) {
80
+ if (normalizeState(actual) === normalizedActual) {return virtual;}
71
81
  }
72
82
  return actualState;
73
83
  }
74
84
 
75
85
  // Wraps a transitionTask call: resolves virtual → actual, then delegates.
76
86
  // If the virtual state maps to null, logs an info line and returns true (no-op).
87
+ /**
88
+ * @param {Function} transitionTaskFn
89
+ * @param {string} slug
90
+ * @param {string} virtualState
91
+ * @param {{log?: Function}} [options]
92
+ * @param {Record<string, any>} [map]
93
+ */
77
94
  function transitionVirtual(transitionTaskFn, slug, virtualState, options = {}, map = loadStateMap(options)) {
78
95
  map = resolveMap(map);
79
96
  const actual = toActual(virtualState, map);
80
97
  if (actual === null) {
81
98
  const fmt = require('./fmt');
82
- const log = options.log || fmt.log.plain;
99
+ /** @type {{log?: Function}} */
100
+ const opts = options;
101
+ const log = opts.log || fmt.log.plain;
83
102
  log(fmt.status('INFO', `Virtual state '${virtualState}' has no backlog.md mapping for this board; skipping status write.`));
84
103
  return true;
85
104
  }
@@ -17,18 +17,23 @@ const path = require('path');
17
17
  * call with `ensureDir: true`). Read-side callers may omit ensureDir so they
18
18
  * fail gracefully when PARALLIX_HOME has never been initialized.
19
19
  */
20
+ /**
21
+ * @param {{ensureDir?: boolean, platform?: string, env?: Record<string,string>, homedir?: Function} | boolean | string} [options]
22
+ */
20
23
  function resolveParallixHome(options = {}) {
21
24
  if (typeof options === 'boolean' || typeof options === 'string') {
22
25
  // Legacy shim: resolveParallixHome(true) === { ensureDir: true }
23
- options = { ensureDir: options };
26
+ options = { ensureDir: Boolean(options) };
24
27
  }
25
28
 
29
+ /** @type {{ensureDir?: boolean, platform?: string, env?: Record<string,string>, homedir?: Function}} */
30
+ const opts = /** @type {{ensureDir?: boolean, platform?: string, env?: Record<string,string>, homedir?: Function}} */(options);
26
31
  const {
27
32
  ensureDir = false,
28
33
  platform = process.platform,
29
34
  env = process.env,
30
35
  homedir = os.homedir
31
- } = options;
36
+ } = opts;
32
37
 
33
38
  let home;
34
39
 
@@ -67,8 +72,11 @@ function resolveParallixHome(options = {}) {
67
72
  * is true). Callers that pass an explicit `--csv-file` path bypass this
68
73
  * resolver entirely — the caller supplies the path before calling here.
69
74
  */
75
+ /** @param {{ensureDir?: boolean, warn?: Function}} [options] */
70
76
  function resolveStatsPath(options = {}) {
71
- const home = resolveParallixHome({ ensureDir: options.ensureDir !== false, warn: options.warn });
77
+ /** @type {{ensureDir?: boolean, warn?: Function}} */
78
+ const opts = options;
79
+ const home = resolveParallixHome({ ensureDir: opts.ensureDir !== false });
72
80
  return path.join(home, 'stats.csv');
73
81
  }
74
82
 
@@ -78,11 +86,14 @@ function resolveStatsPath(options = {}) {
78
86
  * Returns `<PARALLIX_HOME>/agents.local.json`. Callers that pass an
79
87
  * explicit `targetPath` bypass this resolver.
80
88
  */
89
+ /** @param {{ensureDir?: boolean} | string} [options] */
81
90
  function resolveAgentsLocalPath(options = {}) {
82
91
  if (typeof options === 'string') {
83
92
  return path.resolve(options);
84
93
  }
85
- const home = resolveParallixHome({ ensureDir: options.ensureDir !== false });
94
+ /** @type {{ensureDir?: boolean}} */
95
+ const opts = options;
96
+ const home = resolveParallixHome({ ensureDir: opts.ensureDir !== false });
86
97
  return path.join(home, 'agents.local.json');
87
98
  }
88
99
 
@@ -94,6 +105,7 @@ function resolveAgentsLocalPath(options = {}) {
94
105
  * Malformed JSON returns `{ ok: false, error }` rather than throwing so
95
106
  * callers can decide whether this is a hard failure.
96
107
  */
108
+ /** @param {string | (() => string)} pathOrResolution */
97
109
  function readJson(pathOrResolution) {
98
110
  let filePath;
99
111
  if (typeof pathOrResolution === 'function') {
@@ -119,6 +131,7 @@ function readJson(pathOrResolution) {
119
131
  * Write JSON to a path under PARALLIX_HOME (or an explicit path).
120
132
  * Creates parent directories as needed.
121
133
  */
134
+ /** @param {string | (() => string)} filePath @param {any} data */
122
135
  function writeJson(filePath, data) {
123
136
  if (typeof filePath === 'function') {
124
137
  filePath = filePath();
@@ -127,6 +140,7 @@ function writeJson(filePath, data) {
127
140
  return filePath;
128
141
  }
129
142
 
143
+ /** @param {string} filePath @param {string} content */
130
144
  function writeFileAtomic(filePath, content) {
131
145
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
132
146
  const tempPath = path.join(
@@ -137,7 +151,7 @@ function writeFileAtomic(filePath, content) {
137
151
  fs.writeFileSync(tempPath, content, 'utf8');
138
152
  fs.renameSync(tempPath, filePath);
139
153
  } finally {
140
- if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
154
+ if (fs.existsSync(tempPath)) {fs.unlinkSync(tempPath);}
141
155
  }
142
156
  }
143
157
 
@@ -0,0 +1,28 @@
1
+ const { loadEffectiveConfig } = require('./product-config');
2
+
3
+ // Build a prompt-injection prefix that instructs the agent to limit
4
+ // concurrent subagent forks. Returns a non-empty advisory string when
5
+ // maxParallel >= 1, or an empty string when the setting is absent, null,
6
+ // zero, or negative (backward-compatible default).
7
+ //
8
+ // Prefix length target: under 200 characters.
9
+ //
10
+ // @param {number | null | undefined} [maxParallel] — explicit value override;
11
+ // when omitted the function reads from the effective workflow config.
12
+ /** @param {number | null | undefined} [maxParallel] */
13
+ function buildSubagentLimitPrefix(maxParallel) {
14
+ let value = maxParallel;
15
+ if (value === undefined || value === null) {
16
+ const cfg = loadEffectiveConfig();
17
+ const sub = cfg.adapters?.agents?.subagents;
18
+ value = sub && typeof sub.maxParallel === 'number' ? sub.maxParallel : null;
19
+ }
20
+ if (typeof value !== 'number' || value < 1) {
21
+ return '';
22
+ }
23
+ return `Do not spawn more than ${value} parallel subagents. If you need more, pause and wait.\n`;
24
+ }
25
+
26
+ module.exports = {
27
+ buildSubagentLimitPrefix,
28
+ };
@@ -1,4 +1,6 @@
1
1
  const { git, run } = require('./git');
2
+
3
+ /** @typedef {(args: string[], options?: object) => import('child_process').SpawnSyncReturns<string>} GitFn */
2
4
  const { loadAdapterConfig } = require('./product-config');
3
5
  const fs = require('fs');
4
6
 
@@ -23,33 +25,40 @@ function resolveVerificationAdapter(rootDir = process.cwd()) {
23
25
  return { command, defaultArea };
24
26
  }
25
27
 
28
+ /** @param {string} [area] @param {string} [rootDir] */
26
29
  function formatVerificationCommand(area, rootDir = process.cwd()) {
27
30
  const { command, defaultArea } = resolveVerificationAdapter(rootDir);
28
31
  const effectiveArea = area || defaultArea;
29
32
  if (!command) {
30
33
  return NO_GATE_NOTICE;
31
34
  }
32
- return command.replaceAll('{{area}}', effectiveArea);
35
+ return command.replaceAll('{{area}}', effectiveArea || DEFAULT_AREA);
33
36
  }
34
37
 
38
+ /** @param {string} [area] @param {{rootDir?: string, log?: Function, stdio?: string, runFn?: Function}} [options] */
35
39
  function runVerificationGate(area, options = {}) {
36
- const rootDir = options.rootDir || process.cwd();
40
+ /** @type {{rootDir?: string, log?: Function, stdio?: string, runFn?: Function}} */
41
+ const opts = options;
42
+ const rootDir = opts.rootDir || process.cwd();
37
43
  const { command, defaultArea } = resolveVerificationAdapter(rootDir);
38
44
  const effectiveArea = area || defaultArea;
39
45
 
40
46
  if (!command) {
41
- const info = options.log || require('./fmt').log.info;
47
+ const info = opts.log || require('./fmt').log.info;
42
48
  info(`No verification gate configured for area: ${effectiveArea}; default is no validation. `
43
49
  + 'Set adapters.verification.command in workflow.config.json to enforce one.');
44
50
  return { status: 0 };
45
51
  }
46
52
 
47
- const stdio = options.stdio || 'inherit';
48
- const runFn = options.runFn || run;
53
+ const stdio = opts.stdio || 'inherit';
54
+ const runFn = opts.runFn || run;
49
55
  return runFn('bash', ['-lc', command.replaceAll('{{area}}', effectiveArea)], { cwd: rootDir, stdio });
50
56
  }
51
57
 
52
- function readPublishedTreeState(rootDir = process.cwd(), { gitRunner = git } = {}) {
58
+ /** @param {string} rootDir @param {{gitRunner?: GitFn}} [options] */
59
+ function readPublishedTreeState(rootDir, options = {}) {
60
+ /** @type {GitFn} */
61
+ const gitRunner = options.gitRunner || git;
53
62
  const resolvedRoot = fs.realpathSync(rootDir);
54
63
  const commitResult = gitRunner(['-C', resolvedRoot, 'rev-parse', 'HEAD']);
55
64
  const treeResult = gitRunner(['-C', resolvedRoot, 'rev-parse', 'HEAD^{tree}']);
@@ -66,6 +75,7 @@ function readPublishedTreeState(rootDir = process.cwd(), { gitRunner = git } = {
66
75
  return { ok: true, rootDir: resolvedRoot, commit, tree };
67
76
  }
68
77
 
78
+ /** @param {string} [area] @param {string} [rootDir] @param {{gitRunner?: GitFn, runFn?: Function, stdio?: string}} [options] */
69
79
  function captureVerifiedTreeProof(area, rootDir = process.cwd(), options = {}) {
70
80
  const {
71
81
  gitRunner = git,
@@ -74,7 +84,7 @@ function captureVerifiedTreeProof(area, rootDir = process.cwd(), options = {}) {
74
84
  } = options;
75
85
 
76
86
  const before = readPublishedTreeState(rootDir, { gitRunner });
77
- if (!before.ok) return before;
87
+ if (!before.ok) {return before;}
78
88
 
79
89
  const verification = runVerificationGate(area, {
80
90
  rootDir: before.rootDir,
@@ -88,8 +98,8 @@ function captureVerifiedTreeProof(area, rootDir = process.cwd(), options = {}) {
88
98
  };
89
99
  }
90
100
 
91
- const after = readPublishedTreeState(before.rootDir, { gitRunner });
92
- if (!after.ok) return after;
101
+ const after = readPublishedTreeState(/** @type {string} */(before.rootDir), { gitRunner });
102
+ if (!after.ok) {return after;}
93
103
  if (after.commit !== before.commit || after.tree !== before.tree) {
94
104
  return {
95
105
  ok: false,
@@ -113,13 +123,17 @@ function captureVerifiedTreeProof(area, rootDir = process.cwd(), options = {}) {
113
123
  };
114
124
  }
115
125
 
116
- function assertVerifiedTreeProof(proof, rootDir = process.cwd(), { gitRunner = git } = {}) {
126
+ /** @param {{rootDir?: string, commit?: string, tree?: string}} proof @param {string} [rootDir] @param {{gitRunner?: GitFn}} [opts] */
127
+ function assertVerifiedTreeProof(proof, rootDir = process.cwd(), opts = {}) {
117
128
  if (!proof || typeof proof !== 'object') {
118
129
  return { ok: false, error: 'missing verification proof' };
119
130
  }
120
131
 
132
+ /** @type {{gitRunner?: GitFn}} */
133
+ const o = opts;
134
+ const gitRunner = o.gitRunner || git;
121
135
  const current = readPublishedTreeState(rootDir, { gitRunner });
122
- if (!current.ok) return current;
136
+ if (!current.ok) {return current;}
123
137
 
124
138
  if (proof.rootDir !== current.rootDir) {
125
139
  return { ok: false, error: `verification proof was captured from a different checkout: ${proof.rootDir}` };
@@ -131,8 +145,11 @@ function assertVerifiedTreeProof(proof, rootDir = process.cwd(), { gitRunner = g
131
145
  return { ok: true, proof: current };
132
146
  }
133
147
 
148
+ /** @param {string[]} args @param {{log?: Function}} [options] */
134
149
  function runWorkflow(args, options = {}) {
135
- const log = options.log || require('./fmt').log.plain;
150
+ /** @type {{log?: Function}} */
151
+ const opts = options;
152
+ const log = opts.log || require('./fmt').log.plain;
136
153
  const area = args[0] || process.env.VERIFY_AREA || DEFAULT_AREA;
137
154
  log(`Running verification gate for area: ${area}...`);
138
155
  return runVerificationGate(area, { stdio: 'inherit' });
@@ -18,6 +18,10 @@ const SHARED_FILE_REBASE_CONFLICT_RE = /shared file(?:\(s\))? require agent-assi
18
18
  /**
19
19
  * Auto-commit safe mission artifacts before rebase.
20
20
  * Only touches mission-generated files, task files, and stats CSV.
21
+ * @param {string} slug
22
+ * @param {string} worktree
23
+ * @param {{taskFile?: string|null, gitFn?: Function, log?: Function, error?: Function, isMissionArtifactFn?: Function, isWorkflowGeneratedArtifactFn?: Function, resolveStatsRelPathFn?: Function}} [options]
24
+ * @returns {Promise<{ok: boolean, dirty: boolean, unsafe?: boolean}>}
21
25
  */
22
26
  async function commitSafeMissionArtifacts(slug, worktree, {
23
27
  taskFile = null,
@@ -34,13 +38,14 @@ async function commitSafeMissionArtifacts(slug, worktree, {
34
38
  return { ok: true, dirty: false };
35
39
  }
36
40
 
37
- const parsePorcelainZ = (stdout) => {
41
+ const parsePorcelainZ = (/** @type {string|Buffer} */ stdout) => {
38
42
  const entries = String(stdout).split('\0').filter(Boolean);
39
43
  const dirty = [];
40
44
  for (let i = 0; i < entries.length; i += 1) {
41
45
  const entry = entries[i];
42
46
  const xy = entry.slice(0, 2);
43
47
  const file = entry.slice(3);
48
+ /** @type {{xy: string, file: string, paths: string[], source?: string}} */
44
49
  const record = { xy, file, paths: [file] };
45
50
  if ((xy[0] === 'R' || xy[0] === 'C') && i + 1 < entries.length) {
46
51
  const source = entries[i + 1];
@@ -63,7 +68,7 @@ async function commitSafeMissionArtifacts(slug, worktree, {
63
68
 
64
69
  const resolvedTaskFile = taskFile ? path.relative(rootDir, taskFile) : null;
65
70
  const statsRelPath = resolveStatsRelPathFn(rootDir);
66
- const isSafeToCommit = (file) =>
71
+ const isSafeToCommit = (/** @type {string} */ file) =>
67
72
  isWorkflowGeneratedArtifactFn(file)
68
73
  || isMissionArtifactFn(file, slug, rootDir)
69
74
  || (resolvedTaskFile && file === resolvedTaskFile)
@@ -71,7 +76,7 @@ async function commitSafeMissionArtifacts(slug, worktree, {
71
76
 
72
77
  const unsafeFiles = dirtyFiles
73
78
  .flatMap(f => f.paths)
74
- .filter(file => !isSafeToCommit(file));
79
+ .filter((/** @type {string} */ file) => !isSafeToCommit(file));
75
80
  if (unsafeFiles.length > 0) {
76
81
  error(fmt.status('FAIL', `Cannot auto-commit: dirty files include non-mission paths:`));
77
82
  unsafeFiles.forEach(file => error(` - ${file}`));
@@ -100,6 +105,9 @@ async function commitSafeMissionArtifacts(slug, worktree, {
100
105
  *
101
106
  * Always commits safe worktree state first, then rebases via the workflow CLI.
102
107
  * In standalone (provider-disabled) mode, skips the rebase after committing.
108
+ * @param {string} slug
109
+ * @param {{runFn?: Function, gitFn?: Function, taskFile?: string|null, worktree?: string, log?: Function, error?: Function, isReviewProviderEnabledFn?: Function|null, legacyIsForgejoReviewEnabledFn?: Function|null, isForgejoReviewEnabledFn?: Function|null}} [options]
110
+ * @returns {Promise<{ok: boolean, sharedFileConflicts: boolean}>}
103
111
  */
104
112
  async function rebaseBeforeReviewRound(slug, {
105
113
  runFn = require('child_process').spawnSync,