@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.
- package/README.md +6 -0
- package/package.json +5 -1
- package/src/cli/args.js +89 -0
- package/src/cli/main.js +49 -75
- package/src/context.js +16 -2
- package/src/core/stdin.js +52 -0
- package/src/core/versions.js +33 -0
- package/src/hooks/index.js +64 -0
- package/src/output/index.js +64 -4
- package/src/report/session-severity.js +213 -0
- package/src/scaffold/index.js +78 -131
- package/src/toolchain/index.js +6 -70
- package/templates/AGENTS.multiagent-safety.md +25 -0
- package/templates/scripts/agent-branch-finish.sh +30 -1
- package/templates/scripts/agent-session-state.js +62 -1
- package/templates/scripts/codex-agent.sh +38 -0
- package/templates/scripts/install-vscode-active-agents-extension.js +38 -11
- package/templates/scripts/openspec/init-plan-workspace.sh +34 -3
- package/templates/vscode/guardex-active-agents/README.md +7 -6
- package/templates/vscode/guardex-active-agents/extension.js +523 -73
- package/templates/vscode/guardex-active-agents/icon.png +0 -0
- package/templates/vscode/guardex-active-agents/package.json +13 -3
- package/templates/vscode/guardex-active-agents/session-schema.js +311 -4
|
@@ -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
|
+
};
|
package/src/scaffold/index.js
CHANGED
|
@@ -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
|
|
176
|
-
const
|
|
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
|
|
185
|
-
if (existingContent
|
|
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
|
|
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 =
|
|
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
|
|
242
|
+
const sourceContent = fs.readFileSync(sourcePath);
|
|
212
243
|
|
|
213
244
|
if (fs.existsSync(destinationPath)) {
|
|
214
|
-
const existingContent = fs.readFileSync(destinationPath
|
|
215
|
-
if (existingContent
|
|
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
|
|
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
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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,
|
package/src/toolchain/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|