@imdeadpool/guardex 7.0.22 → 7.0.23

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,213 @@
1
+ const TASK_SIZE_UPPER_BOUNDS = {
2
+ 'narrow-patch': 1_800_000,
3
+ 'medium-change': 4_000_000,
4
+ 'large-change': 8_000_000,
5
+ };
6
+
7
+ const TASK_SIZE_VALUES = new Set(Object.keys(TASK_SIZE_UPPER_BOUNDS));
8
+ const FRAGMENTATION_PRESET_SCORES = {
9
+ clean: 0,
10
+ 'few-extra-checks': 5,
11
+ 'repeated-follow-ups': 10,
12
+ looping: 18,
13
+ 'dominant-loop': 25,
14
+ };
15
+ const FINISH_PATH_PRESET_SCORES = {
16
+ 'clear-early': 0,
17
+ 'minor-hesitation': 5,
18
+ 'late-decision': 10,
19
+ reopening: 15,
20
+ };
21
+ const POST_PROOF_PRESET_SCORES = {
22
+ 'stops-soon': 0,
23
+ 'small-tail': 5,
24
+ 'notable-tail': 10,
25
+ 'heavy-tail': 15,
26
+ };
27
+ const DRIVER_TIE_BREAK = ['fragmentation', 'writeStdin', 'finishPath', 'postProof', 'cost'];
28
+ const DRIVER_LABELS = {
29
+ cost: 'cost vs expected scope',
30
+ fragmentation: 'turn fragmentation',
31
+ writeStdin: 'write_stdin churn',
32
+ finishPath: 'finish-path discipline',
33
+ postProof: 'post-proof drift',
34
+ };
35
+
36
+ function parseRequiredPositiveInteger(name, rawValue, { allowZero = true } = {}) {
37
+ const parsed = Number.parseInt(String(rawValue || ''), 10);
38
+ if (!Number.isFinite(parsed) || (!allowZero && parsed <= 0) || (allowZero && parsed < 0)) {
39
+ throw new Error(`${name} requires ${allowZero ? 'a non-negative integer' : 'a positive integer'} value`);
40
+ }
41
+ return parsed;
42
+ }
43
+
44
+ function parseBooleanFlag(name, rawValue) {
45
+ const normalized = String(rawValue || '').trim().toLowerCase();
46
+ if (normalized === 'yes' || normalized === 'true' || normalized === '1') {
47
+ return true;
48
+ }
49
+ if (normalized === 'no' || normalized === 'false' || normalized === '0') {
50
+ return false;
51
+ }
52
+ throw new Error(`${name} requires yes/no (or true/false, 1/0)`);
53
+ }
54
+
55
+ function clampScore(value, min, max) {
56
+ return Math.max(min, Math.min(max, Math.round(value)));
57
+ }
58
+
59
+ function parseTaskSize(rawTaskSize) {
60
+ const normalized = String(rawTaskSize || '').trim();
61
+ if (!TASK_SIZE_VALUES.has(normalized)) {
62
+ throw new Error(`--task-size must be one of: ${Array.from(TASK_SIZE_VALUES).join(', ')}`);
63
+ }
64
+ return normalized;
65
+ }
66
+
67
+ function resolveExpectedUpperBound(taskSize, rawExpectedBound) {
68
+ if (rawExpectedBound) {
69
+ return parseRequiredPositiveInteger('--expected-bound', rawExpectedBound, { allowZero: false });
70
+ }
71
+ return TASK_SIZE_UPPER_BOUNDS[taskSize];
72
+ }
73
+
74
+ function scoreCost(tokens, expectedUpperBound) {
75
+ const ratio = tokens / expectedUpperBound;
76
+ if (ratio <= 1.0) return 0;
77
+ if (ratio <= 1.5) return 5;
78
+ if (ratio <= 2.5) return 10;
79
+ if (ratio <= 4.0) return 18;
80
+ if (ratio <= 6.0) return 24;
81
+ return 30;
82
+ }
83
+
84
+ function scoreFragmentation(execCount, override) {
85
+ if (override) {
86
+ if (Object.prototype.hasOwnProperty.call(FRAGMENTATION_PRESET_SCORES, override)) {
87
+ return FRAGMENTATION_PRESET_SCORES[override];
88
+ }
89
+ return clampScore(parseRequiredPositiveInteger('--fragmentation', override), 0, 25);
90
+ }
91
+ if (execCount <= 4) return 0;
92
+ if (execCount <= 8) return 5;
93
+ if (execCount <= 16) return 10;
94
+ if (execCount <= 28) return 18;
95
+ return 25;
96
+ }
97
+
98
+ function scoreWriteStdin(writeStdinCount) {
99
+ if (writeStdinCount <= 0) return 0;
100
+ if (writeStdinCount <= 3) return 5;
101
+ if (writeStdinCount <= 6) return 10;
102
+ return 15;
103
+ }
104
+
105
+ function scoreFinishPath(completionBeforeTail, override) {
106
+ if (override) {
107
+ if (Object.prototype.hasOwnProperty.call(FINISH_PATH_PRESET_SCORES, override)) {
108
+ return FINISH_PATH_PRESET_SCORES[override];
109
+ }
110
+ return clampScore(parseRequiredPositiveInteger('--finish-path', override), 0, 15);
111
+ }
112
+ return completionBeforeTail ? 0 : 5;
113
+ }
114
+
115
+ function scorePostProof(completionBeforeTail, override) {
116
+ if (override) {
117
+ if (Object.prototype.hasOwnProperty.call(POST_PROOF_PRESET_SCORES, override)) {
118
+ return POST_PROOF_PRESET_SCORES[override];
119
+ }
120
+ return clampScore(parseRequiredPositiveInteger('--post-proof', override), 0, 15);
121
+ }
122
+ return completionBeforeTail ? 0 : 10;
123
+ }
124
+
125
+ function labelForTotal(total) {
126
+ if (total <= 15) return 'Healthy';
127
+ if (total <= 30) return 'Mildly fragmented';
128
+ if (total <= 50) return 'Inefficient';
129
+ if (total <= 75) return 'Runaway';
130
+ return 'Catastrophic';
131
+ }
132
+
133
+ function buildSessionSeverityReport(options) {
134
+ const taskSize = parseTaskSize(options.taskSize);
135
+ const tokens = parseRequiredPositiveInteger('--tokens', options.tokens);
136
+ const execCount = parseRequiredPositiveInteger('--exec-count', options.execCount);
137
+ const writeStdinCount = parseRequiredPositiveInteger('--write-stdin-count', options.writeStdinCount);
138
+ const completionBeforeTail = parseBooleanFlag('--completion-before-tail', options.completionBeforeTail);
139
+ const expectedUpperBound = resolveExpectedUpperBound(taskSize, options.expectedBound);
140
+ const costRatio = tokens / expectedUpperBound;
141
+ const scores = {
142
+ cost: scoreCost(tokens, expectedUpperBound),
143
+ fragmentation: scoreFragmentation(execCount, options.fragmentation),
144
+ writeStdin: scoreWriteStdin(writeStdinCount),
145
+ finishPath: scoreFinishPath(completionBeforeTail, options.finishPath),
146
+ postProof: scorePostProof(completionBeforeTail, options.postProof),
147
+ };
148
+ const total = scores.cost + scores.fragmentation + scores.writeStdin + scores.finishPath + scores.postProof;
149
+ const label = labelForTotal(total);
150
+ const rankedDimensions = Object.entries(scores)
151
+ .map(([key, score]) => ({ key, score, label: DRIVER_LABELS[key] }))
152
+ .filter((entry) => entry.score > 0)
153
+ .sort((left, right) => {
154
+ if (right.score !== left.score) {
155
+ return right.score - left.score;
156
+ }
157
+ return DRIVER_TIE_BREAK.indexOf(left.key) - DRIVER_TIE_BREAK.indexOf(right.key);
158
+ });
159
+ const primaryDriver = rankedDimensions[0] ? rankedDimensions[0].label : 'none';
160
+ const secondaries = rankedDimensions.slice(1).map((entry) => entry.label);
161
+
162
+ return {
163
+ taskSize,
164
+ expectedUpperBound,
165
+ tokens,
166
+ execCount,
167
+ writeStdinCount,
168
+ completionBeforeTail,
169
+ costRatio,
170
+ scores: {
171
+ ...scores,
172
+ total,
173
+ },
174
+ label,
175
+ primaryDriver,
176
+ secondaries,
177
+ outputLine: `Score ${total}/100 — ${label}. Primary: ${primaryDriver}. Secondaries: ${
178
+ secondaries.length > 0 ? secondaries.join(', ') : 'none'
179
+ }.`,
180
+ };
181
+ }
182
+
183
+ function renderSessionSeverityReport(report) {
184
+ return [
185
+ report.outputLine,
186
+ '',
187
+ `Task size: ${report.taskSize}`,
188
+ `Expected upper bound: ${report.expectedUpperBound}`,
189
+ `Actual tokens: ${report.tokens}`,
190
+ `Exec count: ${report.execCount}`,
191
+ `write_stdin count: ${report.writeStdinCount}`,
192
+ `Completion before tail churn: ${report.completionBeforeTail ? 'yes' : 'no'}`,
193
+ `Cost ratio: ${report.costRatio.toFixed(2)}x`,
194
+ '',
195
+ `A. Cost vs expected scope: ${report.scores.cost}`,
196
+ `B. Turn fragmentation: ${report.scores.fragmentation}`,
197
+ `C. write_stdin churn: ${report.scores.writeStdin}`,
198
+ `D. Finish-path discipline: ${report.scores.finishPath}`,
199
+ `E. Post-proof drift: ${report.scores.postProof}`,
200
+ '',
201
+ `Total: ${report.scores.total}`,
202
+ `Label: ${report.label}`,
203
+ `Primary driver: ${report.primaryDriver}`,
204
+ `Secondary drivers: ${report.secondaries.length > 0 ? report.secondaries.join(', ') : 'none'}`,
205
+ ].join('\n');
206
+ }
207
+
208
+ module.exports = {
209
+ TASK_SIZE_UPPER_BOUNDS,
210
+ buildSessionSeverityReport,
211
+ renderSessionSeverityReport,
212
+ labelForTotal,
213
+ };
@@ -1,6 +1,7 @@
1
1
  const {
2
2
  fs,
3
3
  path,
4
+ PACKAGE_ROOT,
4
5
  TOOL_NAME,
5
6
  SHORT_TOOL_NAME,
6
7
  GUARDEX_HOME_DIR,
@@ -9,6 +10,7 @@ const {
9
10
  HOOK_NAMES,
10
11
  LOCK_FILE_RELATIVE,
11
12
  LEGACY_MANAGED_PACKAGE_SCRIPTS,
13
+ PACKAGE_ROOT_SOURCE_OVERRIDES,
12
14
  USER_LEVEL_SKILL_ASSETS,
13
15
  AGENTS_MARKER_START,
14
16
  AGENTS_MARKER_END,
@@ -24,6 +26,7 @@ const {
24
26
  EXECUTABLE_RELATIVE_PATHS,
25
27
  CRITICAL_GUARDRAIL_PATHS,
26
28
  } = require('../context');
29
+ const { parse: parseJsonc, printParseErrorCode } = require('jsonc-parser');
27
30
  const { run } = require('../core/runtime');
28
31
 
29
32
  function ensureParentDir(repoRoot, filePath, dryRun) {
@@ -172,17 +175,13 @@ function ensureHookShim(repoRoot, hookName, options = {}) {
172
175
  );
173
176
  }
174
177
 
175
- function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
176
- const sourcePath = path.join(TEMPLATE_ROOT, relativeTemplatePath);
177
- const destinationRelativePath = toDestinationPath(relativeTemplatePath);
178
- const destinationPath = path.join(repoRoot, destinationRelativePath);
179
-
180
- const sourceContent = fs.readFileSync(sourcePath, 'utf8');
178
+ function copyManagedSourceFile(repoRoot, sourcePath, destinationPath, destinationRelativePath, force, dryRun) {
179
+ const sourceContent = fs.readFileSync(sourcePath);
181
180
  const destinationExists = fs.existsSync(destinationPath);
182
181
 
183
182
  if (destinationExists) {
184
- const existingContent = fs.readFileSync(destinationPath, 'utf8');
185
- if (existingContent === sourceContent) {
183
+ const existingContent = fs.readFileSync(destinationPath);
184
+ if (existingContent.equals(sourceContent)) {
186
185
  ensureExecutable(destinationPath, destinationRelativePath, dryRun);
187
186
  return { status: 'unchanged', file: destinationRelativePath };
188
187
  }
@@ -193,7 +192,7 @@ function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
193
192
 
194
193
  ensureParentDir(repoRoot, destinationPath, dryRun);
195
194
  if (!dryRun) {
196
- fs.writeFileSync(destinationPath, sourceContent, 'utf8');
195
+ fs.writeFileSync(destinationPath, sourceContent);
197
196
  ensureExecutable(destinationPath, destinationRelativePath, dryRun);
198
197
  }
199
198
 
@@ -204,22 +203,54 @@ function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
204
203
  return { status: destinationExists ? 'overwritten' : 'created', file: destinationRelativePath };
205
204
  }
206
205
 
206
+ function normalizeTemplatePath(relativeTemplatePath) {
207
+ return String(relativeTemplatePath).replace(/\\/g, '/');
208
+ }
209
+
210
+ function usesPackageRootSource(repoRoot, relativeTemplatePath) {
211
+ return (
212
+ path.resolve(repoRoot) === PACKAGE_ROOT &&
213
+ PACKAGE_ROOT_SOURCE_OVERRIDES.has(normalizeTemplatePath(relativeTemplatePath))
214
+ );
215
+ }
216
+
217
+ function resolveTemplateSourcePath(repoRoot, relativeTemplatePath) {
218
+ if (usesPackageRootSource(repoRoot, relativeTemplatePath)) {
219
+ return path.join(PACKAGE_ROOT, relativeTemplatePath);
220
+ }
221
+ return path.join(TEMPLATE_ROOT, relativeTemplatePath);
222
+ }
223
+
224
+ function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
225
+ const sourcePath = resolveTemplateSourcePath(repoRoot, relativeTemplatePath);
226
+ const destinationRelativePath = toDestinationPath(relativeTemplatePath);
227
+ const destinationPath = path.join(repoRoot, destinationRelativePath);
228
+ return copyManagedSourceFile(
229
+ repoRoot,
230
+ sourcePath,
231
+ destinationPath,
232
+ destinationRelativePath,
233
+ force,
234
+ dryRun,
235
+ );
236
+ }
237
+
207
238
  function ensureTemplateFilePresent(repoRoot, relativeTemplatePath, dryRun) {
208
- const sourcePath = path.join(TEMPLATE_ROOT, relativeTemplatePath);
239
+ const sourcePath = resolveTemplateSourcePath(repoRoot, relativeTemplatePath);
209
240
  const destinationRelativePath = toDestinationPath(relativeTemplatePath);
210
241
  const destinationPath = path.join(repoRoot, destinationRelativePath);
211
- const sourceContent = fs.readFileSync(sourcePath, 'utf8');
242
+ const sourceContent = fs.readFileSync(sourcePath);
212
243
 
213
244
  if (fs.existsSync(destinationPath)) {
214
- const existingContent = fs.readFileSync(destinationPath, 'utf8');
215
- if (existingContent === sourceContent) {
245
+ const existingContent = fs.readFileSync(destinationPath);
246
+ if (existingContent.equals(sourceContent)) {
216
247
  ensureExecutable(destinationPath, destinationRelativePath, dryRun);
217
248
  return { status: 'unchanged', file: destinationRelativePath };
218
249
  }
219
250
 
220
251
  if (isCriticalGuardrailPath(destinationRelativePath)) {
221
252
  if (!dryRun) {
222
- fs.writeFileSync(destinationPath, sourceContent, 'utf8');
253
+ fs.writeFileSync(destinationPath, sourceContent);
223
254
  ensureExecutable(destinationPath, destinationRelativePath, dryRun);
224
255
  }
225
256
  return { status: dryRun ? 'would-repair-critical' : 'repaired-critical', file: destinationRelativePath };
@@ -230,13 +261,38 @@ function ensureTemplateFilePresent(repoRoot, relativeTemplatePath, dryRun) {
230
261
 
231
262
  ensureParentDir(repoRoot, destinationPath, dryRun);
232
263
  if (!dryRun) {
233
- fs.writeFileSync(destinationPath, sourceContent, 'utf8');
264
+ fs.writeFileSync(destinationPath, sourceContent);
234
265
  ensureExecutable(destinationPath, destinationRelativePath, dryRun);
235
266
  }
236
267
 
237
268
  return { status: 'created', file: destinationRelativePath };
238
269
  }
239
270
 
271
+ function materializePackageRepoTemplateFiles(repoRoot, relativeTemplatePaths, dryRun) {
272
+ if (path.resolve(repoRoot) !== PACKAGE_ROOT) {
273
+ return [];
274
+ }
275
+
276
+ const operations = [];
277
+ for (const relativeTemplatePath of relativeTemplatePaths) {
278
+ if (!PACKAGE_ROOT_SOURCE_OVERRIDES.has(normalizeTemplatePath(relativeTemplatePath))) {
279
+ continue;
280
+ }
281
+ const templateRelativePath = path.posix.join('templates', normalizeTemplatePath(relativeTemplatePath));
282
+ operations.push(
283
+ copyManagedSourceFile(
284
+ PACKAGE_ROOT,
285
+ path.join(PACKAGE_ROOT, relativeTemplatePath),
286
+ path.join(PACKAGE_ROOT, templateRelativePath),
287
+ templateRelativePath,
288
+ true,
289
+ dryRun,
290
+ ),
291
+ );
292
+ }
293
+ return operations;
294
+ }
295
+
240
296
  function lockFilePath(repoRoot) {
241
297
  return path.join(repoRoot, LOCK_FILE_RELATIVE);
242
298
  }
@@ -521,121 +577,13 @@ function ensureManagedGitignore(repoRoot, dryRun) {
521
577
  return { status: 'updated', file: '.gitignore', note: 'appended gitguardex-managed entries' };
522
578
  }
523
579
 
524
- function stripJsonComments(source) {
525
- let result = '';
526
- let inString = false;
527
- let escapeNext = false;
528
- let inLineComment = false;
529
- let inBlockComment = false;
530
-
531
- for (let index = 0; index < source.length; index += 1) {
532
- const current = source[index];
533
- const next = source[index + 1];
534
-
535
- if (inLineComment) {
536
- if (current === '\n' || current === '\r') {
537
- inLineComment = false;
538
- result += current;
539
- }
540
- continue;
541
- }
542
-
543
- if (inBlockComment) {
544
- if (current === '*' && next === '/') {
545
- inBlockComment = false;
546
- index += 1;
547
- continue;
548
- }
549
- if (current === '\n' || current === '\r') {
550
- result += current;
551
- }
552
- continue;
553
- }
554
-
555
- if (inString) {
556
- result += current;
557
- if (escapeNext) {
558
- escapeNext = false;
559
- } else if (current === '\\') {
560
- escapeNext = true;
561
- } else if (current === '"') {
562
- inString = false;
563
- }
564
- continue;
565
- }
566
-
567
- if (current === '"') {
568
- inString = true;
569
- result += current;
570
- continue;
571
- }
572
-
573
- if (current === '/' && next === '/') {
574
- inLineComment = true;
575
- index += 1;
576
- continue;
577
- }
578
-
579
- if (current === '/' && next === '*') {
580
- inBlockComment = true;
581
- index += 1;
582
- continue;
583
- }
584
-
585
- result += current;
586
- }
587
-
588
- return result;
589
- }
590
-
591
- function stripJsonTrailingCommas(source) {
592
- let result = '';
593
- let inString = false;
594
- let escapeNext = false;
595
-
596
- for (let index = 0; index < source.length; index += 1) {
597
- const current = source[index];
598
-
599
- if (inString) {
600
- result += current;
601
- if (escapeNext) {
602
- escapeNext = false;
603
- } else if (current === '\\') {
604
- escapeNext = true;
605
- } else if (current === '"') {
606
- inString = false;
607
- }
608
- continue;
609
- }
610
-
611
- if (current === '"') {
612
- inString = true;
613
- result += current;
614
- continue;
615
- }
616
-
617
- if (current === ',') {
618
- let lookahead = index + 1;
619
- while (lookahead < source.length && /\s/.test(source[lookahead])) {
620
- lookahead += 1;
621
- }
622
- if (source[lookahead] === '}' || source[lookahead] === ']') {
623
- continue;
624
- }
625
- }
626
-
627
- result += current;
628
- }
629
-
630
- return result;
631
- }
632
-
633
580
  function parseJsonObjectLikeFile(source, relativePath) {
634
- let parsed;
635
- try {
636
- parsed = JSON.parse(stripJsonTrailingCommas(stripJsonComments(source)));
637
- } catch (error) {
638
- throw new Error(`Unable to parse ${relativePath} as JSON or JSONC: ${error.message}`);
581
+ const errors = [];
582
+ const parsed = parseJsonc(source, errors, { allowTrailingComma: true });
583
+
584
+ if (errors.length > 0) {
585
+ const formattedErrors = errors.map((entry) => printParseErrorCode(entry.error)).join(', ');
586
+ throw new Error(`Unable to parse ${relativePath} as JSON or JSONC: ${formattedErrors}`);
639
587
  }
640
588
 
641
589
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
@@ -806,6 +754,7 @@ module.exports = {
806
754
  ensureHookShim,
807
755
  copyTemplateFile,
808
756
  ensureTemplateFilePresent,
757
+ materializePackageRepoTemplateFiles,
809
758
  ensureOmxScaffold,
810
759
  ensureLockRegistry,
811
760
  lockStateOrError,
@@ -815,8 +764,6 @@ module.exports = {
815
764
  removeLegacyManagedRepoFile,
816
765
  ensureAgentsSnippet,
817
766
  ensureManagedGitignore,
818
- stripJsonComments,
819
- stripJsonTrailingCommas,
820
767
  parseJsonObjectLikeFile,
821
768
  buildRepoVscodeSettings,
822
769
  ensureRepoVscodeSettings,
@@ -17,50 +17,18 @@ const {
17
17
  envFlagIsTruthy,
18
18
  } = require('../context');
19
19
  const { run } = require('../core/runtime');
20
+ const {
21
+ parseVersionString,
22
+ compareParsedVersions,
23
+ isNewerVersion,
24
+ } = require('../core/versions');
25
+ const { readSingleLineFromStdin } = require('../core/stdin');
20
26
  const { colorize } = require('../output');
21
27
 
22
28
  function isInteractiveTerminal() {
23
29
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
24
30
  }
25
31
 
26
- const stdinWaitArray = new Int32Array(new SharedArrayBuffer(4));
27
-
28
- function sleepSyncMs(milliseconds) {
29
- Atomics.wait(stdinWaitArray, 0, 0, milliseconds);
30
- }
31
-
32
- function readSingleLineFromStdin() {
33
- let input = '';
34
- const buffer = Buffer.alloc(1);
35
-
36
- while (true) {
37
- let bytesRead = 0;
38
- try {
39
- bytesRead = fs.readSync(process.stdin.fd, buffer, 0, 1);
40
- } catch (error) {
41
- if (error && ['EAGAIN', 'EWOULDBLOCK', 'EINTR'].includes(error.code)) {
42
- sleepSyncMs(15);
43
- continue;
44
- }
45
- return input;
46
- }
47
-
48
- if (bytesRead === 0) {
49
- if (process.stdin.isTTY) {
50
- sleepSyncMs(15);
51
- continue;
52
- }
53
- return input;
54
- }
55
-
56
- const char = buffer.toString('utf8', 0, bytesRead);
57
- if (char === '\n' || char === '\r') {
58
- return input;
59
- }
60
- input += char;
61
- }
62
- }
63
-
64
32
  function parseAutoApproval(name) {
65
33
  const raw = process.env[name];
66
34
  if (raw == null) return null;
@@ -70,38 +38,6 @@ function parseAutoApproval(name) {
70
38
  return null;
71
39
  }
72
40
 
73
- function parseVersionString(version) {
74
- const match = String(version || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)/);
75
- if (!match) return null;
76
- return [
77
- Number.parseInt(match[1], 10),
78
- Number.parseInt(match[2], 10),
79
- Number.parseInt(match[3], 10),
80
- ];
81
- }
82
-
83
- function compareParsedVersions(left, right) {
84
- if (!left || !right) return 0;
85
- for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
86
- const leftValue = left[index] || 0;
87
- const rightValue = right[index] || 0;
88
- if (leftValue > rightValue) return 1;
89
- if (leftValue < rightValue) return -1;
90
- }
91
- return 0;
92
- }
93
-
94
- function isNewerVersion(latest, current) {
95
- const latestParts = parseVersionString(latest);
96
- const currentParts = parseVersionString(current);
97
-
98
- if (!latestParts || !currentParts) {
99
- return String(latest || '').trim() !== String(current || '').trim();
100
- }
101
-
102
- return compareParsedVersions(latestParts, currentParts) > 0;
103
- }
104
-
105
41
  function parseNpmVersionOutput(stdout) {
106
42
  const trimmed = String(stdout || '').trim();
107
43
  if (!trimmed) return '';
@@ -10,6 +10,29 @@
10
10
  **Task-size routing.** Small tasks stay in direct caveman-only mode. For typos, single-file tweaks, one-liners, version bumps, or similarly bounded asks, solve directly and do not escalate into heavy OMX orchestration just because a keyword appears. Treat `quick:`, `simple:`, `tiny:`, `minor:`, `small:`, `just:`, and `only:` as explicit lightweight escape hatches.
11
11
  Promote to OMX orchestration only when the task is medium/large: multi-file behavior changes, API/schema work, refactors, migrations, architecture, cross-cutting scope, or long prompts. Heavy OMX modes (`ralph`, `autopilot`, `team`, `ultrawork`, `swarm`, `ralplan`) are for that larger scope. If the task grows while working, upgrade then.
12
12
 
13
+ ## Token / Context Budget
14
+
15
+ Default: less word, same proof.
16
+
17
+ - For prompts about `token inefficiency`, `reviewer mode`, `minimal token overhead`, or session waste patterns, switch into low-overhead mode: plan in at most 4 bullets, execute by phase, batch related reads/commands, avoid duplicate reads and interactive loops, keep outputs compact, and verify once per phase.
18
+ - Low output alone is not a defect. A bounded run that finishes in roughly <=10 steps is usually fine; low output spread across 20+ steps with rising per-turn input is fragmentation and should be treated as context growth first.
19
+ - Startup / resume summaries stay tiny: `branch`, `task`, `blocker`, `next step`, and `evidence`.
20
+ - Memory-driven starts stay ordered: read active `.omx/state` first, then one live `.omx/notepad.md` handoff, then external memory only when the task depends on prior repo decisions, a previous lane, or ambiguous continuity. Stop after the first 1-2 relevant hits.
21
+ - Front-load scaffold/path discovery into one grouped inspection pass. Avoid serial `ls` / `find` / `rg` / `cat` retries that only rediscover the same path state.
22
+ - Treat repeated `write_stdin`, repeated `sed` / `cat` peeks, and tiny diagnostic follow-up checks as strong negative signals. If they appear alongside climbing input cost, stop the probe loop and batch the next phase.
23
+ - Tool / hook summaries stay tiny: command, status, last meaningful lines only. Drop routine hook boilerplate.
24
+ - Treat local edit/commit, remote publish/PR, CI diagnosis, and cleanup as bounded phases. Do not spend fresh narration or approval turns on obvious safe follow-ons inside an already authorized phase unless the risk changes.
25
+ - When a session turns fragmented, collapse back to inspect once, patch once, verify once, and summarize once.
26
+ - Keep `.omx/notepad.md` lean: live handoffs only. Use exactly `branch`, `task`, `blocker`, `next step`, and `evidence`; move narrative proof into OpenSpec artifacts, PRs, or command output.
27
+
28
+ ## OMX Caveman Style
29
+
30
+ - Commentary and progress updates use smart-caveman `ultra` by default: drop articles, filler, pleasantries, and hedging. Fragments are fine when they stay clear.
31
+ - Answer order stays fixed: answer first, cause next, fix or next step last. If yes/no fits, say yes/no first.
32
+ - Keep literals exact: code, commands, file paths, flags, env vars, URLs, numbers, timestamps, and error text are never caveman-compressed.
33
+ - Auto-clarity wins: switch back to `lite` or normal wording for security warnings, irreversible actions, privacy/compliance notes, ordered instructions where fragments may confuse, or when the user is confused and needs more detail.
34
+ - Boundaries stay normal/exact for code, commits, PR text, specs, logs, and blocker evidence.
35
+
13
36
  **Isolation.** Every task runs on a dedicated `agent/*` branch + worktree. Start with `gx branch start "<task>" "<agent-name>"`. Treat the base branch (`main`/`dev`) as read-only while an agent branch is active. Never `git checkout <branch>` on a primary working tree (including nested repos); use `git worktree add` instead. The `.githooks/post-checkout` hook auto-reverts primary-branch switches during agent sessions - bypass only with `GUARDEX_ALLOW_PRIMARY_BRANCH_SWITCH=1`.
14
37
  For every new task, including follow-up work in the same chat/session, if an assigned agent sub-branch/worktree is already open, continue in that sub-branch instead of creating a fresh lane unless the user explicitly redirects scope.
15
38
  Never implement directly on the local/base branch checkout; keep it unchanged and perform all edits in the agent sub-branch/worktree.
@@ -25,6 +48,8 @@ OMX completion policy: when a task is done, the agent must commit the task chang
25
48
 
26
49
  **Reporting.** Every completion handoff includes: files changed, behavior touched, verification commands + results, risks/follow-ups.
27
50
 
51
+ **Open questions.** If Codex/Claude hits an unresolved question, branching decision, or blocker that should survive chat, record it in `openspec/plan/<plan-slug>/open-questions.md` as an unchecked `- [ ]` item. Resolve it in-place when answered instead of burying it in chat-only notes.
52
+
28
53
  **OpenSpec (when change-driven).** Keep `openspec/changes/<slug>/tasks.md` checkboxes current during work, not batched at the end. Task scaffolds and manual task edits must include an explicit final completion/cleanup section that ends with PR merge + sandbox cleanup (`gx finish --via-pr --wait-for-merge --cleanup` or `gx branch finish ... --cleanup`) and records PR URL + final `MERGED` evidence. Verify specs with `openspec validate --specs` before archive. Don't archive unverified.
29
54
 
30
55
  **Version bumps.** If a change bumps a published version, the same PR updates release notes/changelog.
@@ -409,6 +409,33 @@ is_remote_branch_missing_error() {
409
409
  return 1
410
410
  }
411
411
 
412
+ local_branch_exists() {
413
+ local branch="$1"
414
+ git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch}"
415
+ }
416
+
417
+ delete_local_branch_for_cleanup() {
418
+ local branch="$1"
419
+ local delete_output=""
420
+
421
+ if ! local_branch_exists "$branch"; then
422
+ echo "[agent-branch-finish] Local branch '${branch}' was already deleted; continuing cleanup." >&2
423
+ return 0
424
+ fi
425
+
426
+ if delete_output="$(git -C "$repo_root" branch -d "$branch" 2>&1)"; then
427
+ return 0
428
+ fi
429
+
430
+ if ! local_branch_exists "$branch"; then
431
+ echo "[agent-branch-finish] Local branch '${branch}' was already deleted; continuing cleanup." >&2
432
+ return 0
433
+ fi
434
+
435
+ echo "$delete_output" >&2
436
+ return 1
437
+ }
438
+
412
439
  read_pr_state() {
413
440
  local state_line
414
441
  state_line="$("$GH_BIN" pr view "$SOURCE_BRANCH" --json state,mergedAt,url --jq '[.state, (.mergedAt // ""), (.url // "")] | join("\u001f")' 2>/dev/null || true)"
@@ -607,7 +634,9 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
607
634
  git -C "$repo_root" worktree remove "$source_worktree" --force >/dev/null 2>&1 || true
608
635
  fi
609
636
 
610
- git -C "$repo_root" branch -d "$SOURCE_BRANCH"
637
+ if ! delete_local_branch_for_cleanup "$SOURCE_BRANCH"; then
638
+ exit 1
639
+ fi
611
640
 
612
641
  if [[ "$PUSH_ENABLED" -eq 1 && "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
613
642
  if git -C "$repo_root" ls-remote --exit-code --heads origin "$SOURCE_BRANCH" >/dev/null 2>&1; then