@magnusekdahl/parallix 1.1.1 → 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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "gates": {
3
+ "lib": {
4
+ "command": "./scripts/verify-local.sh static-analysis",
5
+ "order": 1,
6
+ "run_last": false
7
+ }
8
+ }
9
+ }
@@ -55,22 +55,23 @@ Also adopt one explicit place for selection metadata:
55
55
 
56
56
  `## Refinement Signals` must contain:
57
57
 
58
- - `Estimated agent % usage limit`: one of `0-25%`, `25-50%`, `50-75%`, `75-100%`, `100%+`
58
+ - `Predicted NEL bucket`: one of `Small (0–80)`, `Medium (81–235)`, `Large (235+)` per ADR 0047
59
59
  - `Confidence`: `High`, `Medium`, or `Low`
60
60
  - `Selection note`: `activate as-is`, `split first`, or `defer`, with one short reason
61
61
  - `Main drivers`: the 2-4 factors driving the estimate
62
62
 
63
- Interpretation rule for `% usage limit`:
63
+ Interpretation rule for NEL bucket:
64
64
 
65
- - it is an approximate share of a meaningful AI-assisted execution budget under the current repo workflow
65
+ - it is an approximate measure of engineering change volume (insertions + deletions, whitespace-ignored) for the mission's merge diff against the primary branch
66
66
  - it may reflect scope breadth, trust-tier sensitivity, validation burden, review/handoff overhead, context churn, and cross-surface coupling
67
67
  - it is a selection aid, not a delivery promise or pseudo-scientific schedule estimate
68
+ - NEL is computed by the reusable `nels` module (`lib/core/nels.js`) which excludes workflow/bookkeeping files per ADR 0047
68
69
 
69
70
  Default activation guidance:
70
71
 
71
- - `0-25%` or `25-50%`: normally safe to activate as-is when confidence is not low
72
- - `50-75%`: split first unless the mission is unusually high leverage and already sharply bounded
73
- - `75-100%` or `100%+`: defer or split before activation under normal conditions
72
+ - `Small (0–80)`: normally safe to activate as-is when confidence is not low
73
+ - `Medium (81–235)`: split first unless the mission is unusually high leverage and already sharply bounded
74
+ - `Large (235+)`: defer or split before activation under normal conditions
74
75
  - if confidence is `Low`, keep refining instead of treating the estimate as reliable enough for pickup
75
76
 
76
77
  Current rollout blocker:
@@ -84,14 +85,14 @@ Current rollout blocker:
84
85
 
85
86
  - The workflow now distinguishes "not ready yet" from "ready, but not yet started."
86
87
  - Mission comparison becomes more deliberate because shovel-ready candidates can be compared without immediately activating them.
87
- - The `% usage limit` signal is lightweight enough to aid selection without creating a second planning system.
88
+ - The NEL bucket signal is lightweight enough to aid selection without creating a second planning system.
88
89
  - `MISSION.md` remains the detailed execution contract while Backlog stays the current-state surface.
89
90
 
90
91
  ### Negative
91
92
 
92
93
  - The lifecycle gains one more state conceptually, so operators must learn one more transition.
93
94
  - Some older workflow docs and historical artifacts will continue to mention the earlier five-state model.
94
- - `% usage limit` still depends on judgment and may drift if agents stop recording the main drivers behind the estimate.
95
+ - NEL bucket estimates still depend on judgment and may drift if agents stop recording the main drivers behind the estimate.
95
96
 
96
97
  ## Alternatives Considered
97
98
 
@@ -132,4 +133,5 @@ Negative:
132
133
  ## Links
133
134
 
134
135
  - [ADR 0026](0026-ai-task-state-and-agent-recovery-surface.md)
136
+ - [ADR 0047: Per-Mission Change Size Budget](0047-per-mission-change-size-budget.md) — NEL bucket definitions and exclusion globs
135
137
  - [Mission](../missions/2026/task-024-mission-complexity-estimation/MISSION.md)
@@ -18,21 +18,21 @@ Introduce three sizing tracks and a dependency-wave planning heuristic.
18
18
 
19
19
  ### 1. Sizing Tracks
20
20
 
21
- | Track | Scope | Process | Agent Budget |
22
- |-------|-------|---------|--------------|
23
- | **Quick Flow** | < 3 files, < 2h estimated, or docs-only. | Minimal `MISSION.md` (Goal, Why Now, Refinement Signals, Gates). Skip detailed checkpoints if straightforward. | < 10% of agent limit |
24
- | **Full Method** | > 3 files, complex logic, or > 2h. | Standard `MISSION.md` with all sections. Detailed checkpoints. Mandatory external review (C2 review remains required for sensitive scopes per AGENTS.md). | 10-50% of agent limit |
25
- | **Multi-Wave** | Very large, high risk, or complex dependencies. | Split into multiple `Full Method` missions (waves). | Total > 50% (requiring split) |
21
+ | Track | Scope | Process | NEL Budget |
22
+ |-------|-------|---------|------------|
23
+ | **Quick Flow** | < 3 files, < 2h estimated, or docs-only. | Minimal `MISSION.md` (Goal, Why Now, Refinement Signals, Gates). Skip detailed checkpoints if straightforward. | 0–80 NEL (Small) |
24
+ | **Full Method** | > 3 files, complex logic, or > 2h. | Standard `MISSION.md` with all sections. Detailed checkpoints. Mandatory external review (C2 review remains required for sensitive scopes per AGENTS.md). | 81–235 NEL (Medium) |
25
+ | **Multi-Wave** | Very large, high risk, or complex dependencies. | Split into multiple `Full Method` missions (waves). | 235+ NEL (Large) |
26
26
 
27
- ### 2. "Too Large" Thresholds by Agent
27
+ ### 2. "Too Large" Thresholds
28
28
 
29
- A mission is "Too Large" if its `Estimated agent % usage limit` exceeds the following thresholds per session/work-unit:
29
+ A mission is "Too Large" for single-wave execution when its predicted NEL bucket exceeds the following thresholds:
30
30
 
31
- - **Claude**: > 50% of daily limit.
32
- - **Codex**: > 25% of weekly limit.
33
- - **Gemini**: > 50% of the reasonable session/context budget (approx. 50k-100k tokens for execution).
31
+ - **Small (0–80 NEL)**: suitable for Quick Flow; no decomposition needed.
32
+ - **Medium (81–235 NEL)**: suitable for Full Method; consider splitting if dependencies are complex.
33
+ - **Large (235+ NEL)**: MUST be split into dependency waves before activation.
34
34
 
35
- Missions exceeding these thresholds MUST be split into dependency waves before activation.
35
+ These thresholds are derived from empirical terciles in task-1355 data (n=29 missions) as documented in ADR 0047. Missions exceeding these thresholds MUST be split into dependency waves before activation.
36
36
 
37
37
  ### 3. Dependency-Wave Heuristic
38
38
 
@@ -70,10 +70,12 @@ Agents MUST assign themselves to a task in `Backlog.md` before beginning work in
70
70
  - Improved visibility of parallel work via mandatory assignment.
71
71
 
72
72
  ### Negative
73
+
73
74
  - Requires upfront estimation effort during the `draft` phase.
74
- - Agents must learn to judge their own usage limits accurately.
75
+ - Agents must estimate NEL buckets, which requires understanding the exclusion rules from ADR 0047.
75
76
 
76
77
  ## Links
77
78
  - [ADR 0032](0032-mission-refinement-state-and-usage-budget-signals.md)
79
+ - [ADR 0047: Per-Mission Change Size Budget](0047-per-mission-change-size-budget.md) — NEL bucket definitions and exclusion globs
78
80
  - [MISSION_FLOW.md](../../MISSION_FLOW.md)
79
81
  - [AGENTS.md](../../AGENTS.md)
@@ -145,7 +145,7 @@ Option B (Forgejo Actions) would require runner credentials for docker push and
145
145
 
146
146
  ## Deliverables
147
147
 
148
- 1. **Repo-side config:** `config/integration-pipelines.json` with entries for `server`, `auth-server`, `web-client`, `web-e2e`; `web-e2e` carries `run_last: true`
148
+ 1. **Repo-side config:** `config/integration-pipelines.json` with entries for `server`, `auth-server`, `web-client`, `web-e2e`; `web-e2e` carries `run_last: true`. Task-1362 added a `lib` entry mapping to `./scripts/verify-local.sh static-analysis` as a required gate for `lib/` changes.
149
149
  2. **Change detection:** Compute the set of top-level dirs touched by `mission/<slug>` vs the primary branch
150
150
  3. **Dispatch in `scripts/verify-local.sh`:** New `gate_integrate()` function and `integrate` area in the case statement
151
151
  4. **`node workflow integrate` preflight:** Calls the new `integrate` area as part of `printIntegrationPreflight`
@@ -12,6 +12,7 @@ const fmt = require('../core/fmt');
12
12
  const { runVerificationGate } = require('../core/verification');
13
13
  const { isForgejoReviewEnabled } = require('../core/product-config');
14
14
  const { rebaseBeforeReviewRound } = require('../review/rebase');
15
+ const nels = require('../core/nels');
15
16
 
16
17
  /**
17
18
  * Verifies that the current environment is ready for handoff.
@@ -228,6 +229,15 @@ const { rebaseBeforeReviewRound } = require('../review/rebase');
228
229
  }
229
230
  }
230
231
 
232
+ // Step 1.7: NEL capture — compute actual NEL from merge diff and persist record
233
+ log('Step 1.7: Capturing Net Engineering Lines (NEL) at handoff...');
234
+ const nelResult = captureNelAtHandoff(slug, { rootDir, missionDir: missionDirPath, log, error });
235
+ if (nelResult.ok) {
236
+ log(fmt.status('PASS', `NEL captured: ${nelResult.nel} NEL (${nelResult.bucket.label} bucket)`));
237
+ } else {
238
+ log(fmt.status('WARN', `NEL capture skipped: ${nelResult.error}`));
239
+ }
240
+
231
241
  // Step 2: Forgejo PR Update/Create (optional mirror when Forgejo is enabled)
232
242
  let token = null;
233
243
  let fallbackUser = null;
@@ -570,6 +580,89 @@ function buildAutoCheckpointContent(slug) {
570
580
  return true;
571
581
  }
572
582
 
583
+ /**
584
+ * Capture NEL (Net Engineering Lines) at handoff time.
585
+ *
586
+ * Computes actual NEL from the merge diff (primary..HEAD), reads the predicted
587
+ * bucket from the mission's Refinement Signals, resolves review rounds from
588
+ * review-state.json, and persists a per-mission NEL record as `nel-record.json`.
589
+ *
590
+ * This is purely observational — no enforcement, gates, or blocks.
591
+ *
592
+ * @param {string} slug - Mission slug
593
+ * @param {{ rootDir: string, missionDir: string, log: Function, error: Function }} options
594
+ * @returns {{ ok: boolean, nel?: number, bucket?: string, error?: string }}
595
+ */
596
+ function captureNelAtHandoff(slug, options) {
597
+ const { rootDir, missionDir, error } = options;
598
+
599
+ // 1. Determine primary branch for diff range
600
+ let primaryBranch;
601
+ try {
602
+ primaryBranch = missionUtils.getPrimaryBranch(rootDir);
603
+ } catch (_) {
604
+ return { ok: false, error: 'could not detect primary branch for NEL diff range' };
605
+ }
606
+
607
+ if (!primaryBranch) {
608
+ return { ok: false, error: 'primary branch is empty' };
609
+ }
610
+
611
+ // 2. Compute actual NEL from primary..HEAD
612
+ let nelRecord;
613
+ try {
614
+ nelRecord = nels.computeNELRecord(`${primaryBranch}..HEAD`, { cwd: rootDir });
615
+ } catch (_) {
616
+ return { ok: false, error: 'NEL computation failed' };
617
+ }
618
+
619
+ const actualNel = nelRecord.nel;
620
+ const actualBucket = nelRecord.bucket.label;
621
+
622
+ // 3. Read predicted bucket from MISSION.md Refinement Signals
623
+ const missionMdPath = path.join(missionDir, 'MISSION.md');
624
+ let predictedBucket = 'Unknown';
625
+ if (fs.existsSync(missionMdPath)) {
626
+ const content = fs.readFileSync(missionMdPath, 'utf8');
627
+ const predictedMatch = content.match(/Predicted NEL bucket:\s*(Small|Medium|Large)/i);
628
+ if (predictedMatch) {
629
+ predictedBucket = predictedMatch[1];
630
+ }
631
+ }
632
+
633
+ // 4. Read review rounds from review-state.json
634
+ let reviewRounds = 1;
635
+ const reviewStatePath = path.join(missionDir, 'review-state.json');
636
+ if (fs.existsSync(reviewStatePath)) {
637
+ try {
638
+ const rs = JSON.parse(fs.readFileSync(reviewStatePath, 'utf8'));
639
+ reviewRounds = rs.round || 1;
640
+ } catch (_) {
641
+ // ignore parse errors
642
+ }
643
+ }
644
+
645
+ // 5. Persist NEL record
646
+ const nelRecordPath = path.join(missionDir, 'nel-record.json');
647
+ const record = {
648
+ slug,
649
+ predictedBucket,
650
+ actualNel,
651
+ actualBucket,
652
+ reviewRounds,
653
+ capturedAt: new Date().toISOString(),
654
+ };
655
+
656
+ try {
657
+ fs.writeFileSync(nelRecordPath, JSON.stringify(record, null, 2) + '\n', 'utf8');
658
+ } catch (err) {
659
+ error(`Failed to write NEL record: ${err.message}`);
660
+ return { ok: false, error: `failed to write NEL record: ${err.message}` };
661
+ }
662
+
663
+ return { ok: true, nel: actualNel, bucket: actualBucket };
664
+ }
665
+
573
666
  /** @param {string[]} args */
574
667
  async function handoffCommand(args) {
575
668
  const explicitSlug = args[0];
@@ -593,3 +686,4 @@ module.exports.verifyHandoff = verifyHandoff;
593
686
  module.exports.performHandoff = performHandoff;
594
687
  module.exports.gatekeeper = gatekeeper;
595
688
  module.exports.runDeclaredGates = runDeclaredGates;
689
+ module.exports.captureNelAtHandoff = captureNelAtHandoff;
@@ -269,7 +269,7 @@ function detectChangedAreas(slug, opts = {}) {
269
269
  /** @param {string} filesOutput */
270
270
  function parseFilesToAreas(filesOutput) {
271
271
  const areas = new Set();
272
- const knownAreas = ['server', 'auth-server', 'web-client', 'docs', 'workflow', 'android', 'kubernetes'];
272
+ const knownAreas = ['lib', 'server', 'auth-server', 'web-client', 'docs', 'workflow', 'android', 'kubernetes'];
273
273
 
274
274
  filesOutput.split('\n').forEach((/** @type{string} */ file) => {
275
275
  file = file.trim();
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Net Engineering Lines (NEL) computation.
3
+ *
4
+ * Computes NEL from `git diff --numstat -w` for a given diff range,
5
+ * excluding workflow/process bookkeeping, documentation, and generated files
6
+ * per ADR 0047.
7
+ *
8
+ * Bucket thresholds (empirical terciles from task-1355 data, n=29):
9
+ * - Small: 0–80 NEL (11% rework rate)
10
+ * - Medium: 81–235 NEL (22% rework rate)
11
+ * - Large: 235+ NEL (73% rework rate)
12
+ *
13
+ * This module is purely observational — no enforcement, gates, or blocks.
14
+ *
15
+ * @module lib/core/nels
16
+ */
17
+
18
+ const { spawnSync } = require('child_process');
19
+
20
+ // ---------- exclusion globs (ADR 0047) ----------
21
+ // Each entry is a minimatch-compatible pattern. Patterns are checked in order;
22
+ // the first match wins.
23
+
24
+ const EXCLUSION_PATTERNS = [
25
+ 'missions/**', // workflow/process bookkeeping
26
+ 'backlog/**', // workflow/process bookkeeping
27
+ 'review-*', // workflow/process bookkeeping
28
+ 'CP-*', // workflow/process bookkeeping
29
+ '**/*.md', // documentation
30
+ 'docs/**', // documentation
31
+ 'package-lock.json', // generated/vendored
32
+ 'coverage/**', // generated/vendored
33
+ '*lock*', // lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml, Gemfile.lock, etc.)
34
+ '*.lock', // additional lockfile safety net
35
+ ];
36
+
37
+ /**
38
+ * Bucket constants derived from ADR 0047 empirical terciles.
39
+ */
40
+ const BUCKET_SMALL_MAX = 80;
41
+ const BUCKET_MEDIUM_MAX = 235;
42
+
43
+ /**
44
+ * Classify NEL count into a bucket label.
45
+ *
46
+ * @param {number} nel
47
+ * @returns {{ label: 'Small' | 'Medium' | 'Large', min: number, max: number }}
48
+ */
49
+ function classifyBucket(nel) {
50
+ if (nel <= BUCKET_SMALL_MAX) {
51
+ return { label: 'Small', min: 0, max: BUCKET_SMALL_MAX };
52
+ }
53
+ if (nel <= BUCKET_MEDIUM_MAX) {
54
+ return { label: 'Medium', min: BUCKET_SMALL_MAX + 1, max: BUCKET_MEDIUM_MAX };
55
+ }
56
+ return { label: 'Large', min: BUCKET_MEDIUM_MAX + 1, max: Infinity };
57
+ }
58
+
59
+ /**
60
+ * Check whether a file path matches any exclusion pattern.
61
+ *
62
+ * @param {string} filePath - Relative file path from repo root
63
+ * @returns {boolean}
64
+ */
65
+ function isExcluded(filePath) {
66
+ // Normalize to forward slashes
67
+ const normalized = filePath.replace(/\\/g, '/');
68
+
69
+ for (const pattern of EXCLUSION_PATTERNS) {
70
+ if (patternMatches(normalized, pattern)) {
71
+ return true;
72
+ }
73
+ }
74
+ return false;
75
+ }
76
+
77
+ /**
78
+ * Simple glob matcher supporting *, **, and literal segments.
79
+ * Does not require a full minimatch dependency — keeps this module light.
80
+ *
81
+ * @param {string} str
82
+ * @param {string} pattern
83
+ * @returns {boolean}
84
+ */
85
+ function patternMatches(str, pattern) {
86
+ // Convert minimatch-style glob to regex
87
+ // Escape regex special chars except * and ?
88
+ let regexStr = '';
89
+ let i = 0;
90
+ const pLen = pattern.length;
91
+
92
+ while (i < pLen) {
93
+ const ch = pattern[i];
94
+ if (ch === '*') {
95
+ if (i + 1 < pLen && pattern[i + 1] === '*') {
96
+ // ** matches everything including /
97
+ if (i + 2 < pLen && pattern[i + 2] === '/') {
98
+ regexStr += '(?:.*/)?';
99
+ i += 3;
100
+ continue;
101
+ } else {
102
+ regexStr += '.*';
103
+ i += 2;
104
+ continue;
105
+ }
106
+ } else {
107
+ regexStr += '[^/]*';
108
+ i += 1;
109
+ continue;
110
+ }
111
+ } else if (ch === '?') {
112
+ regexStr += '[^/]';
113
+ } else if ('.+^${}()|[]\\'.includes(ch)) {
114
+ regexStr += '\\' + ch;
115
+ } else {
116
+ regexStr += ch;
117
+ }
118
+ i++;
119
+ }
120
+
121
+ const re = new RegExp('^' + regexStr + '$');
122
+ return re.test(str);
123
+ }
124
+
125
+ /**
126
+ * Compute Net Engineering Lines (NEL) from a git diff range.
127
+ *
128
+ * Runs `git diff --numstat -w <range>` and sums insertions+deletions
129
+ * for files not matching exclusion globs from ADR 0047.
130
+ *
131
+ * @param {string} range - Git diff range, e.g. `main..HEAD` or `HEAD~1..HEAD`
132
+ * @param {{ cwd?: string }} [options]
133
+ * @returns {number} Total NEL count (insertions + deletions, whitespace-ignored)
134
+ */
135
+ function computeNEL(range, options = {}) {
136
+ const cwd = options.cwd || process.cwd();
137
+
138
+ const result = spawnSync('git', ['diff', '--numstat', '-w', range], {
139
+ cwd,
140
+ encoding: 'utf8',
141
+ maxBuffer: 50 * 1024 * 1024, // 50 MB
142
+ });
143
+
144
+ if (result.status !== 0) {
145
+ // Git error, empty diff, or uncomputable — return 0
146
+ return 0;
147
+ }
148
+
149
+ const stdout = result.stdout || '';
150
+ const lines = stdout.split('\n').filter(line => line.trim().length > 0);
151
+
152
+ let total = 0;
153
+
154
+ for (const line of lines) {
155
+ // numstat format: <insertions>\t<deletions>\t<filename>
156
+ // Binary files show: -\t-\t<filename>
157
+ const parts = line.split('\t');
158
+ if (parts.length < 3) { continue; }
159
+
160
+ const adds = parts[0];
161
+ const dels = parts[1];
162
+ const filePath = parts.slice(2).join('\t');
163
+
164
+ // Skip binary files
165
+ if (adds === '-' || dels === '-') { continue; }
166
+
167
+ // Apply exclusion globs
168
+ if (isExcluded(filePath)) { continue; }
169
+
170
+ const addsNum = parseInt(adds, 10) || 0;
171
+ const delsNum = parseInt(dels, 10) || 0;
172
+
173
+ total += addsNum + delsNum;
174
+ }
175
+
176
+ return total;
177
+ }
178
+
179
+ /**
180
+ * Compute NEL and return the full NEL record including bucket classification.
181
+ *
182
+ * @param {string} range - Git diff range
183
+ * @param {{ cwd?: string }} [options]
184
+ * @returns {{ nel: number, bucket: { label: string, min: number, max: number } }}
185
+ */
186
+ function computeNELRecord(range, options) {
187
+ const nel = computeNEL(range, options);
188
+ return { nel, bucket: classifyBucket(nel) };
189
+ }
190
+
191
+ module.exports = {
192
+ computeNEL,
193
+ computeNELRecord,
194
+ classifyBucket,
195
+ isExcluded,
196
+ EXCLUSION_PATTERNS,
197
+ BUCKET_SMALL_MAX,
198
+ BUCKET_MEDIUM_MAX,
199
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magnusekdahl/parallix",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "AI mission workflow toolkit with a px CLI — local-first, human-in-the-loop multi-agent development",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "private": false,
@@ -7,7 +7,7 @@
7
7
  <Why Now>
8
8
 
9
9
  ## Refinement Signals
10
- - Estimated agent % usage limit: 25-50%
10
+ - Predicted NEL bucket: Small (0–80) / Medium (81–235) / Large (235+)
11
11
  - Confidence: High
12
12
  - Selection note: activate as-is
13
13
  - Main drivers: <drivers>