@agent-profile/cli 0.2.0 → 0.3.1
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/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2836 -216
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import fsPromises4 from "node:fs/promises";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
-
import
|
|
6
|
+
import path6 from "node:path";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
8
9
|
import { createServer } from "node:net";
|
|
9
10
|
import { pathToFileURL } from "node:url";
|
|
10
11
|
|
|
@@ -31,8 +32,8 @@ function getDefaultTargetIds() {
|
|
|
31
32
|
"claude-subagents"
|
|
32
33
|
];
|
|
33
34
|
}
|
|
34
|
-
function createGeneratedTextFile(
|
|
35
|
-
const safePath = safeOutputPath(
|
|
35
|
+
function createGeneratedTextFile(path7, target, templateId, text) {
|
|
36
|
+
const safePath = safeOutputPath(path7);
|
|
36
37
|
const bytes = Buffer.from(normalizeGeneratedText(text), "utf8");
|
|
37
38
|
return {
|
|
38
39
|
path: safePath,
|
|
@@ -53,17 +54,17 @@ function normalizeGeneratedText(text) {
|
|
|
53
54
|
return `${lines.join("\n").replace(/\n*$/u, "")}
|
|
54
55
|
`;
|
|
55
56
|
}
|
|
56
|
-
function safeOutputPath(
|
|
57
|
-
if (
|
|
58
|
-
throw new Error(`Invalid generated output path: ${
|
|
57
|
+
function safeOutputPath(path7) {
|
|
58
|
+
if (path7.length === 0 || path7.includes("\\") || path7.startsWith("/") || /^[A-Za-z]:/u.test(path7)) {
|
|
59
|
+
throw new Error(`Invalid generated output path: ${path7}`);
|
|
59
60
|
}
|
|
60
|
-
const segments =
|
|
61
|
+
const segments = path7.split("/");
|
|
61
62
|
if (segments.some(
|
|
62
63
|
(segment) => segment.length === 0 || segment === "." || segment === ".."
|
|
63
64
|
)) {
|
|
64
|
-
throw new Error(`Invalid generated output path: ${
|
|
65
|
+
throw new Error(`Invalid generated output path: ${path7}`);
|
|
65
66
|
}
|
|
66
|
-
return
|
|
67
|
+
return path7;
|
|
67
68
|
}
|
|
68
69
|
function sha256Hex(bytes) {
|
|
69
70
|
return createHash("sha256").update(bytes).digest("hex");
|
|
@@ -79,10 +80,17 @@ function compareText(left, right) {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
// ../../packages/compiler/src/lockfile.ts
|
|
83
|
+
var SUPPORTED_VERSIONS = /* @__PURE__ */ new Set([1, 2]);
|
|
82
84
|
function buildLockfile(input) {
|
|
83
85
|
const profilePath = safeOutputPath(input.profilePath ?? "ai-profile.yaml");
|
|
86
|
+
const mixedByPath = new Map(
|
|
87
|
+
(input.mixedOutputs ?? []).map((entry) => [
|
|
88
|
+
safeOutputPath(entry.path),
|
|
89
|
+
entry
|
|
90
|
+
])
|
|
91
|
+
);
|
|
84
92
|
return {
|
|
85
|
-
version:
|
|
93
|
+
version: 2,
|
|
86
94
|
profile: {
|
|
87
95
|
path: profilePath,
|
|
88
96
|
schemaVersion: 1,
|
|
@@ -90,7 +98,7 @@ function buildLockfile(input) {
|
|
|
90
98
|
},
|
|
91
99
|
compiler: input.compiler ?? AGENT_PROFILE_COMPILER,
|
|
92
100
|
templates: input.templates.map(toLockTemplate).sort(compareLockTemplates),
|
|
93
|
-
outputs: input.files.map(
|
|
101
|
+
outputs: input.files.map((file) => toLockOutputV2(file, mixedByPath)).sort(compareLockOutputsV2)
|
|
94
102
|
};
|
|
95
103
|
}
|
|
96
104
|
function serializeLockfile(lockfile) {
|
|
@@ -105,6 +113,21 @@ function createLockfileFile(input) {
|
|
|
105
113
|
serializeLockfile(buildLockfile(input))
|
|
106
114
|
);
|
|
107
115
|
}
|
|
116
|
+
function migrateLockfileV1ToV2(lockfile) {
|
|
117
|
+
return {
|
|
118
|
+
version: 2,
|
|
119
|
+
profile: lockfile.profile,
|
|
120
|
+
compiler: lockfile.compiler,
|
|
121
|
+
templates: [...lockfile.templates].sort(compareLockTemplates),
|
|
122
|
+
outputs: lockfile.outputs.map((output) => ({
|
|
123
|
+
path: output.path,
|
|
124
|
+
target: output.target,
|
|
125
|
+
templateId: output.templateId,
|
|
126
|
+
ownership: "generated-owned",
|
|
127
|
+
sha256: output.sha256
|
|
128
|
+
})).sort(compareLockOutputsV2)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
108
131
|
function validateLockfileText(source) {
|
|
109
132
|
try {
|
|
110
133
|
return validateLockfileValue(JSON.parse(source));
|
|
@@ -125,17 +148,33 @@ function validateLockfileText(source) {
|
|
|
125
148
|
}
|
|
126
149
|
function validateLockfileValue(value) {
|
|
127
150
|
const issues = [];
|
|
128
|
-
|
|
151
|
+
if (!isRecord(value)) {
|
|
152
|
+
issues.push(schemaIssue("/", "object", describeValue(value)));
|
|
153
|
+
return { ok: false, issues };
|
|
154
|
+
}
|
|
155
|
+
const version = value.version;
|
|
156
|
+
if (typeof version !== "number" || !SUPPORTED_VERSIONS.has(version)) {
|
|
157
|
+
issues.push({
|
|
158
|
+
code: "lockfile_unsupported_version",
|
|
159
|
+
path: "/version",
|
|
160
|
+
expected: "supported lockfile version (1 or 2)",
|
|
161
|
+
actual: describeValue(version),
|
|
162
|
+
message: `/version is not a supported lockfile version.`
|
|
163
|
+
});
|
|
164
|
+
return { ok: false, issues };
|
|
165
|
+
}
|
|
166
|
+
if (version === 1) {
|
|
167
|
+
validateLockfileV1Object(value, issues);
|
|
168
|
+
if (issues.length > 0) {
|
|
169
|
+
return { ok: false, issues: issues.sort(compareLockfileIssues) };
|
|
170
|
+
}
|
|
171
|
+
return { ok: true, lockfile: value, version: 1 };
|
|
172
|
+
}
|
|
173
|
+
validateLockfileV2Object(value, issues);
|
|
129
174
|
if (issues.length > 0) {
|
|
130
|
-
return {
|
|
131
|
-
ok: false,
|
|
132
|
-
issues: issues.sort(compareLockfileIssues)
|
|
133
|
-
};
|
|
175
|
+
return { ok: false, issues: issues.sort(compareLockfileIssues) };
|
|
134
176
|
}
|
|
135
|
-
return {
|
|
136
|
-
ok: true,
|
|
137
|
-
lockfile: value
|
|
138
|
-
};
|
|
177
|
+
return { ok: true, lockfile: value, version: 2 };
|
|
139
178
|
}
|
|
140
179
|
function toLockTemplate(template) {
|
|
141
180
|
return {
|
|
@@ -145,7 +184,7 @@ function toLockTemplate(template) {
|
|
|
145
184
|
sha256: template.sha256
|
|
146
185
|
};
|
|
147
186
|
}
|
|
148
|
-
function
|
|
187
|
+
function validateLockfileV1Object(value, issues) {
|
|
149
188
|
if (!isRecord(value)) {
|
|
150
189
|
issues.push(schemaIssue("/", "object", describeValue(value)));
|
|
151
190
|
return;
|
|
@@ -164,7 +203,28 @@ function validateLockfileObject(value, issues) {
|
|
|
164
203
|
validateProfile(value.profile, issues);
|
|
165
204
|
validateCompiler(value.compiler, issues);
|
|
166
205
|
validateTemplates(value.templates, issues);
|
|
167
|
-
|
|
206
|
+
validateOutputsV1(value.outputs, issues);
|
|
207
|
+
}
|
|
208
|
+
function validateLockfileV2Object(value, issues) {
|
|
209
|
+
if (!isRecord(value)) {
|
|
210
|
+
issues.push(schemaIssue("/", "object", describeValue(value)));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
requireExactKeys(
|
|
214
|
+
value,
|
|
215
|
+
"/",
|
|
216
|
+
["version", "profile", "compiler", "templates", "outputs"],
|
|
217
|
+
issues
|
|
218
|
+
);
|
|
219
|
+
if (value.version !== 2) {
|
|
220
|
+
issues.push(
|
|
221
|
+
schemaIssue("/version", "constant 2", describeValue(value.version))
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
validateProfile(value.profile, issues);
|
|
225
|
+
validateCompiler(value.compiler, issues);
|
|
226
|
+
validateTemplates(value.templates, issues);
|
|
227
|
+
validateOutputsV2(value.outputs, issues);
|
|
168
228
|
}
|
|
169
229
|
function validateProfile(value, issues) {
|
|
170
230
|
if (!isRecord(value)) {
|
|
@@ -213,71 +273,190 @@ function validateTemplates(value, issues) {
|
|
|
213
273
|
}
|
|
214
274
|
let previous;
|
|
215
275
|
value.forEach((item, index) => {
|
|
216
|
-
const
|
|
276
|
+
const path7 = `/templates/${index}`;
|
|
217
277
|
if (!isRecord(item)) {
|
|
218
|
-
issues.push(schemaIssue(
|
|
278
|
+
issues.push(schemaIssue(path7, "object", describeValue(item)));
|
|
219
279
|
return;
|
|
220
280
|
}
|
|
221
|
-
requireExactKeys(item,
|
|
222
|
-
validateString(item.id, `${
|
|
223
|
-
validateString(item.target, `${
|
|
224
|
-
validateString(item.version, `${
|
|
225
|
-
validateHash(item.sha256, `${
|
|
281
|
+
requireExactKeys(item, path7, ["id", "target", "version", "sha256"], issues);
|
|
282
|
+
validateString(item.id, `${path7}/id`, issues);
|
|
283
|
+
validateString(item.target, `${path7}/target`, issues);
|
|
284
|
+
validateString(item.version, `${path7}/version`, issues);
|
|
285
|
+
validateHash(item.sha256, `${path7}/sha256`, issues);
|
|
226
286
|
const current = item;
|
|
227
287
|
if (previous && compareLockTemplates(previous, current) > 0) {
|
|
228
288
|
issues.push({
|
|
229
289
|
code: "lockfile_order_error",
|
|
230
|
-
path:
|
|
290
|
+
path: path7,
|
|
231
291
|
expected: "templates sorted by id then target",
|
|
232
292
|
actual: "out of order",
|
|
233
|
-
message: `${
|
|
293
|
+
message: `${path7} is not in deterministic order.`
|
|
234
294
|
});
|
|
235
295
|
}
|
|
236
296
|
previous = current;
|
|
237
297
|
});
|
|
238
298
|
}
|
|
239
|
-
function
|
|
299
|
+
function validateOutputsV1(value, issues) {
|
|
240
300
|
if (!Array.isArray(value)) {
|
|
241
301
|
issues.push(schemaIssue("/outputs", "array", describeValue(value)));
|
|
242
302
|
return;
|
|
243
303
|
}
|
|
244
304
|
let previous;
|
|
245
305
|
value.forEach((item, index) => {
|
|
246
|
-
const
|
|
306
|
+
const path7 = `/outputs/${index}`;
|
|
247
307
|
if (!isRecord(item)) {
|
|
248
|
-
issues.push(schemaIssue(
|
|
308
|
+
issues.push(schemaIssue(path7, "object", describeValue(item)));
|
|
249
309
|
return;
|
|
250
310
|
}
|
|
251
311
|
requireExactKeys(
|
|
252
312
|
item,
|
|
253
|
-
|
|
313
|
+
path7,
|
|
254
314
|
["path", "target", "templateId", "sha256"],
|
|
255
315
|
issues
|
|
256
316
|
);
|
|
257
|
-
validatePath(item.path, `${
|
|
258
|
-
validateString(item.target, `${
|
|
259
|
-
validateString(item.templateId, `${
|
|
260
|
-
validateHash(item.sha256, `${
|
|
317
|
+
validatePath(item.path, `${path7}/path`, issues);
|
|
318
|
+
validateString(item.target, `${path7}/target`, issues);
|
|
319
|
+
validateString(item.templateId, `${path7}/templateId`, issues);
|
|
320
|
+
validateHash(item.sha256, `${path7}/sha256`, issues);
|
|
261
321
|
const current = item;
|
|
262
322
|
if (previous && compareLockOutputs(previous, current) > 0) {
|
|
263
323
|
issues.push({
|
|
264
324
|
code: "lockfile_order_error",
|
|
265
|
-
path:
|
|
325
|
+
path: path7,
|
|
326
|
+
expected: "outputs sorted by path then target",
|
|
327
|
+
actual: "out of order",
|
|
328
|
+
message: `${path7} is not in deterministic order.`
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
previous = current;
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
function validateOutputsV2(value, issues) {
|
|
335
|
+
if (!Array.isArray(value)) {
|
|
336
|
+
issues.push(schemaIssue("/outputs", "array", describeValue(value)));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
let previous;
|
|
340
|
+
value.forEach((item, index) => {
|
|
341
|
+
const path7 = `/outputs/${index}`;
|
|
342
|
+
if (!isRecord(item)) {
|
|
343
|
+
issues.push(schemaIssue(path7, "object", describeValue(item)));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const ownership = item.ownership;
|
|
347
|
+
if (ownership === "generated-owned") {
|
|
348
|
+
requireExactKeys(
|
|
349
|
+
item,
|
|
350
|
+
path7,
|
|
351
|
+
["path", "target", "templateId", "ownership", "sha256"],
|
|
352
|
+
issues
|
|
353
|
+
);
|
|
354
|
+
validatePath(item.path, `${path7}/path`, issues);
|
|
355
|
+
validateString(item.target, `${path7}/target`, issues);
|
|
356
|
+
validateString(item.templateId, `${path7}/templateId`, issues);
|
|
357
|
+
validateHash(item.sha256, `${path7}/sha256`, issues);
|
|
358
|
+
} else if (ownership === "mixed") {
|
|
359
|
+
requireExactKeys(
|
|
360
|
+
item,
|
|
361
|
+
path7,
|
|
362
|
+
["path", "target", "templateId", "ownership", "regions"],
|
|
363
|
+
issues
|
|
364
|
+
);
|
|
365
|
+
validatePath(item.path, `${path7}/path`, issues);
|
|
366
|
+
validateString(item.target, `${path7}/target`, issues);
|
|
367
|
+
validateString(item.templateId, `${path7}/templateId`, issues);
|
|
368
|
+
validateRegions(item.regions, `${path7}/regions`, issues);
|
|
369
|
+
} else if (ownership === "manual-owned") {
|
|
370
|
+
requireExactKeys(
|
|
371
|
+
item,
|
|
372
|
+
path7,
|
|
373
|
+
["path", "target", "templateId", "ownership"],
|
|
374
|
+
issues
|
|
375
|
+
);
|
|
376
|
+
validatePath(item.path, `${path7}/path`, issues);
|
|
377
|
+
if (item.target !== "manual") {
|
|
378
|
+
issues.push(
|
|
379
|
+
schemaIssue(`${path7}/target`, '"manual"', describeValue(item.target))
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
if (item.templateId !== "manual") {
|
|
383
|
+
issues.push(
|
|
384
|
+
schemaIssue(
|
|
385
|
+
`${path7}/templateId`,
|
|
386
|
+
'"manual"',
|
|
387
|
+
describeValue(item.templateId)
|
|
388
|
+
)
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
issues.push(
|
|
393
|
+
schemaIssue(
|
|
394
|
+
`${path7}/ownership`,
|
|
395
|
+
'"generated-owned" | "mixed" | "manual-owned"',
|
|
396
|
+
describeValue(ownership)
|
|
397
|
+
)
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
const current = item;
|
|
401
|
+
if (previous && compareLockOutputsV2(previous, current) > 0) {
|
|
402
|
+
issues.push({
|
|
403
|
+
code: "lockfile_order_error",
|
|
404
|
+
path: path7,
|
|
266
405
|
expected: "outputs sorted by path then target",
|
|
267
406
|
actual: "out of order",
|
|
268
|
-
message: `${
|
|
407
|
+
message: `${path7} is not in deterministic order.`
|
|
269
408
|
});
|
|
270
409
|
}
|
|
271
410
|
previous = current;
|
|
272
411
|
});
|
|
273
412
|
}
|
|
274
|
-
function
|
|
413
|
+
function validateRegions(value, pathPrefix, issues) {
|
|
414
|
+
if (!Array.isArray(value)) {
|
|
415
|
+
issues.push(schemaIssue(pathPrefix, "array", describeValue(value)));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (value.length !== 1) {
|
|
419
|
+
issues.push(
|
|
420
|
+
schemaIssue(
|
|
421
|
+
pathPrefix,
|
|
422
|
+
"exactly one generated region",
|
|
423
|
+
`${value.length} regions`
|
|
424
|
+
)
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
value.forEach((item, index) => {
|
|
428
|
+
const path7 = `${pathPrefix}/${index}`;
|
|
429
|
+
if (!isRecord(item)) {
|
|
430
|
+
issues.push(schemaIssue(path7, "object", describeValue(item)));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
requireExactKeys(
|
|
434
|
+
item,
|
|
435
|
+
path7,
|
|
436
|
+
["id", "target", "templateId", "sha256"],
|
|
437
|
+
issues
|
|
438
|
+
);
|
|
439
|
+
if (item.id !== "agent-profile:generated") {
|
|
440
|
+
issues.push(
|
|
441
|
+
schemaIssue(
|
|
442
|
+
`${path7}/id`,
|
|
443
|
+
'"agent-profile:generated"',
|
|
444
|
+
describeValue(item.id)
|
|
445
|
+
)
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
validateString(item.target, `${path7}/target`, issues);
|
|
449
|
+
validateString(item.templateId, `${path7}/templateId`, issues);
|
|
450
|
+
validateHash(item.sha256, `${path7}/sha256`, issues);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
function requireExactKeys(value, path7, keys, issues) {
|
|
275
454
|
const allowed = new Set(keys);
|
|
276
455
|
for (const key of keys) {
|
|
277
456
|
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
278
457
|
issues.push(
|
|
279
458
|
schemaIssue(
|
|
280
|
-
`${
|
|
459
|
+
`${path7}/${escapeJsonPointerSegment(key)}`,
|
|
281
460
|
"required property",
|
|
282
461
|
"missing"
|
|
283
462
|
)
|
|
@@ -288,7 +467,7 @@ function requireExactKeys(value, path6, keys, issues) {
|
|
|
288
467
|
if (!allowed.has(key)) {
|
|
289
468
|
issues.push(
|
|
290
469
|
schemaIssue(
|
|
291
|
-
`${
|
|
470
|
+
`${path7}/${escapeJsonPointerSegment(key)}`,
|
|
292
471
|
"no additional properties",
|
|
293
472
|
"present"
|
|
294
473
|
)
|
|
@@ -296,26 +475,26 @@ function requireExactKeys(value, path6, keys, issues) {
|
|
|
296
475
|
}
|
|
297
476
|
}
|
|
298
477
|
}
|
|
299
|
-
function validateString(value,
|
|
478
|
+
function validateString(value, path7, issues) {
|
|
300
479
|
if (typeof value !== "string") {
|
|
301
|
-
issues.push(schemaIssue(
|
|
480
|
+
issues.push(schemaIssue(path7, "string", describeValue(value)));
|
|
302
481
|
}
|
|
303
482
|
}
|
|
304
|
-
function validateHash(value,
|
|
483
|
+
function validateHash(value, path7, issues) {
|
|
305
484
|
if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) {
|
|
306
485
|
issues.push({
|
|
307
486
|
code: "lockfile_hash_error",
|
|
308
|
-
path:
|
|
487
|
+
path: path7,
|
|
309
488
|
expected: "lowercase sha256 hex",
|
|
310
489
|
actual: describeValue(value),
|
|
311
|
-
message: `${
|
|
490
|
+
message: `${path7} must be a lowercase SHA-256 hex digest.`
|
|
312
491
|
});
|
|
313
492
|
}
|
|
314
493
|
}
|
|
315
|
-
function validatePath(value,
|
|
494
|
+
function validatePath(value, path7, issues) {
|
|
316
495
|
if (typeof value !== "string") {
|
|
317
496
|
issues.push(
|
|
318
|
-
schemaIssue(
|
|
497
|
+
schemaIssue(path7, "repository-relative path", describeValue(value))
|
|
319
498
|
);
|
|
320
499
|
return;
|
|
321
500
|
}
|
|
@@ -324,20 +503,20 @@ function validatePath(value, path6, issues) {
|
|
|
324
503
|
} catch {
|
|
325
504
|
issues.push({
|
|
326
505
|
code: "lockfile_path_error",
|
|
327
|
-
path:
|
|
506
|
+
path: path7,
|
|
328
507
|
expected: "repository-relative forward-slash path",
|
|
329
508
|
actual: "unsafe path",
|
|
330
|
-
message: `${
|
|
509
|
+
message: `${path7} must be a safe repository-relative path.`
|
|
331
510
|
});
|
|
332
511
|
}
|
|
333
512
|
}
|
|
334
|
-
function schemaIssue(
|
|
513
|
+
function schemaIssue(path7, expected, actual) {
|
|
335
514
|
return {
|
|
336
515
|
code: "lockfile_schema_error",
|
|
337
|
-
path:
|
|
516
|
+
path: path7,
|
|
338
517
|
expected,
|
|
339
518
|
actual,
|
|
340
|
-
message: `${
|
|
519
|
+
message: `${path7} does not match the lockfile schema.`
|
|
341
520
|
};
|
|
342
521
|
}
|
|
343
522
|
function compareLockfileIssues(left, right) {
|
|
@@ -358,11 +537,30 @@ function escapeJsonPointerSegment(segment) {
|
|
|
358
537
|
function isRecord(value) {
|
|
359
538
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
360
539
|
}
|
|
361
|
-
function
|
|
540
|
+
function toLockOutputV2(file, mixedByPath) {
|
|
541
|
+
const safePath = safeOutputPath(file.path);
|
|
542
|
+
const mixed = mixedByPath.get(safePath);
|
|
543
|
+
if (mixed) {
|
|
544
|
+
return {
|
|
545
|
+
path: safePath,
|
|
546
|
+
target: file.target,
|
|
547
|
+
templateId: file.templateId,
|
|
548
|
+
ownership: "mixed",
|
|
549
|
+
regions: [
|
|
550
|
+
{
|
|
551
|
+
id: "agent-profile:generated",
|
|
552
|
+
target: mixed.target,
|
|
553
|
+
templateId: mixed.templateId,
|
|
554
|
+
sha256: mixed.regionHash
|
|
555
|
+
}
|
|
556
|
+
]
|
|
557
|
+
};
|
|
558
|
+
}
|
|
362
559
|
return {
|
|
363
|
-
path:
|
|
560
|
+
path: safePath,
|
|
364
561
|
target: file.target,
|
|
365
562
|
templateId: file.templateId,
|
|
563
|
+
ownership: "generated-owned",
|
|
366
564
|
sha256: file.sha256
|
|
367
565
|
};
|
|
368
566
|
}
|
|
@@ -372,6 +570,229 @@ function compareLockTemplates(left, right) {
|
|
|
372
570
|
function compareLockOutputs(left, right) {
|
|
373
571
|
return compareText(left.path, right.path) || compareText(left.target, right.target);
|
|
374
572
|
}
|
|
573
|
+
function compareLockOutputsV2(left, right) {
|
|
574
|
+
return compareText(left.path, right.path) || compareText(left.target, right.target);
|
|
575
|
+
}
|
|
576
|
+
function toLockfileV2View(lockfile) {
|
|
577
|
+
if (lockfile.version === 2) return lockfile;
|
|
578
|
+
return migrateLockfileV1ToV2(lockfile);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ../../packages/compiler/src/regions.ts
|
|
582
|
+
var GENERATED_START_MARKER = "<!-- agent-profile:generated:start -->";
|
|
583
|
+
var GENERATED_END_MARKER = "<!-- agent-profile:generated:end -->";
|
|
584
|
+
var MANUAL_START_MARKER = "<!-- agent-profile:manual:start -->";
|
|
585
|
+
var MANUAL_END_MARKER = "<!-- agent-profile:manual:end -->";
|
|
586
|
+
var REGION_PRECEDENCE_TEXT = "If generated and manual instructions conflict, follow the manual project instructions unless they would weaken safety, privacy, or permission limits. Safety, privacy, and explicit deny rules always win.";
|
|
587
|
+
var LEGACY_GENERATED_MARKER = "<!-- Generated by Agent Profile Compiler. Do not edit by hand. -->";
|
|
588
|
+
var GENERATED_START_RE = /^<!-- agent-profile:generated:start -->$/u;
|
|
589
|
+
var GENERATED_END_RE = /^<!-- agent-profile:generated:end -->$/u;
|
|
590
|
+
var MANUAL_START_RE = /^<!-- agent-profile:manual:start -->$/u;
|
|
591
|
+
var MANUAL_END_RE = /^<!-- agent-profile:manual:end -->$/u;
|
|
592
|
+
function parseMixedFile(bytes) {
|
|
593
|
+
const text = bytes.toString("utf8");
|
|
594
|
+
const lineEndings = [];
|
|
595
|
+
const lines = [];
|
|
596
|
+
let position = 0;
|
|
597
|
+
while (position < text.length) {
|
|
598
|
+
const newlineIndex = text.indexOf("\n", position);
|
|
599
|
+
if (newlineIndex === -1) {
|
|
600
|
+
lines.push(text.slice(position));
|
|
601
|
+
lineEndings.push("");
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
let lineEnd = newlineIndex;
|
|
605
|
+
let ending = "\n";
|
|
606
|
+
if (lineEnd > position && text[lineEnd - 1] === "\r") {
|
|
607
|
+
lineEnd = newlineIndex - 1;
|
|
608
|
+
ending = "\r\n";
|
|
609
|
+
}
|
|
610
|
+
lines.push(text.slice(position, lineEnd));
|
|
611
|
+
lineEndings.push(ending);
|
|
612
|
+
position = newlineIndex + 1;
|
|
613
|
+
}
|
|
614
|
+
const inFence = computeCodeFenceMask(lines);
|
|
615
|
+
const matches = {
|
|
616
|
+
generatedStart: [],
|
|
617
|
+
generatedEnd: [],
|
|
618
|
+
manualStart: [],
|
|
619
|
+
manualEnd: []
|
|
620
|
+
};
|
|
621
|
+
lines.forEach((line, index) => {
|
|
622
|
+
if (inFence[index]) return;
|
|
623
|
+
if (GENERATED_START_RE.test(line)) matches.generatedStart.push(index);
|
|
624
|
+
if (GENERATED_END_RE.test(line)) matches.generatedEnd.push(index);
|
|
625
|
+
if (MANUAL_START_RE.test(line)) matches.manualStart.push(index);
|
|
626
|
+
if (MANUAL_END_RE.test(line)) matches.manualEnd.push(index);
|
|
627
|
+
});
|
|
628
|
+
const issues = [];
|
|
629
|
+
if (matches.generatedStart.length > 1 || matches.generatedEnd.length > 1 || matches.manualStart.length > 1 || matches.manualEnd.length > 1) {
|
|
630
|
+
issues.push({
|
|
631
|
+
code: "duplicate-markers",
|
|
632
|
+
message: "Mixed instruction file contains duplicate generated/manual region markers."
|
|
633
|
+
});
|
|
634
|
+
return { ok: false, issues };
|
|
635
|
+
}
|
|
636
|
+
if (matches.generatedStart.length !== 1 || matches.generatedEnd.length !== 1 || matches.manualStart.length !== 1 || matches.manualEnd.length !== 1) {
|
|
637
|
+
issues.push({
|
|
638
|
+
code: "missing-markers",
|
|
639
|
+
message: "Mixed instruction file is missing one or more required region markers."
|
|
640
|
+
});
|
|
641
|
+
return { ok: false, issues };
|
|
642
|
+
}
|
|
643
|
+
const gs = matches.generatedStart[0];
|
|
644
|
+
const ge = matches.generatedEnd[0];
|
|
645
|
+
const ms = matches.manualStart[0];
|
|
646
|
+
const me = matches.manualEnd[0];
|
|
647
|
+
if (!(gs < ge && ge < ms && ms < me)) {
|
|
648
|
+
issues.push({
|
|
649
|
+
code: "out-of-order",
|
|
650
|
+
message: "Mixed instruction file region markers must appear in generated-start, generated-end, manual-start, manual-end order."
|
|
651
|
+
});
|
|
652
|
+
return { ok: false, issues };
|
|
653
|
+
}
|
|
654
|
+
if (gs !== 0) {
|
|
655
|
+
issues.push({
|
|
656
|
+
code: "preamble",
|
|
657
|
+
message: "Mixed instruction file must begin with the generated start marker; no preamble bytes are allowed."
|
|
658
|
+
});
|
|
659
|
+
return { ok: false, issues };
|
|
660
|
+
}
|
|
661
|
+
if (ms !== ge + 2 || (lines[ge + 1] ?? "") !== "") {
|
|
662
|
+
issues.push({
|
|
663
|
+
code: "missing-blank-line",
|
|
664
|
+
message: "Mixed instruction file must have exactly one blank line between the generated and manual regions."
|
|
665
|
+
});
|
|
666
|
+
return { ok: false, issues };
|
|
667
|
+
}
|
|
668
|
+
const trailingOk = lines.length === me + 1 && (lineEndings[me] ?? "").length > 0;
|
|
669
|
+
if (!trailingOk) {
|
|
670
|
+
issues.push({
|
|
671
|
+
code: "missing-trailing-newline",
|
|
672
|
+
message: "Mixed instruction file must end with the manual end marker followed by exactly one trailing newline."
|
|
673
|
+
});
|
|
674
|
+
return { ok: false, issues };
|
|
675
|
+
}
|
|
676
|
+
const generatedInnerBytes = computeRangeBytes(bytes, lines, lineEndings, gs, ge);
|
|
677
|
+
const manualInnerBytes = computeRangeBytes(bytes, lines, lineEndings, ms, me);
|
|
678
|
+
return {
|
|
679
|
+
ok: true,
|
|
680
|
+
generatedInner: generatedInnerBytes,
|
|
681
|
+
manualInner: manualInnerBytes,
|
|
682
|
+
generatedInnerHash: sha256Hex(generatedInnerBytes)
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function computeCodeFenceMask(lines) {
|
|
686
|
+
const mask = new Array(lines.length).fill(false);
|
|
687
|
+
let open = false;
|
|
688
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
689
|
+
const line = lines[index] ?? "";
|
|
690
|
+
if (/^\s{0,3}(?:```|~~~)/u.test(line)) {
|
|
691
|
+
mask[index] = open || true;
|
|
692
|
+
open = !open;
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
mask[index] = open;
|
|
696
|
+
}
|
|
697
|
+
return mask;
|
|
698
|
+
}
|
|
699
|
+
function computeRangeBytes(bytes, lines, lineEndings, startMarkerIndex, endMarkerIndex) {
|
|
700
|
+
const startByte = computeByteOffsetAfterLine(lines, lineEndings, startMarkerIndex);
|
|
701
|
+
const endByte = computeByteOffsetAtLineStart(lines, lineEndings, endMarkerIndex);
|
|
702
|
+
return bytes.subarray(startByte, endByte);
|
|
703
|
+
}
|
|
704
|
+
function computeByteOffsetAfterLine(lines, lineEndings, lineIndex) {
|
|
705
|
+
let offset = 0;
|
|
706
|
+
for (let index = 0; index <= lineIndex; index += 1) {
|
|
707
|
+
offset += Buffer.byteLength(lines[index] ?? "", "utf8");
|
|
708
|
+
offset += Buffer.byteLength(lineEndings[index] ?? "", "utf8");
|
|
709
|
+
}
|
|
710
|
+
return offset;
|
|
711
|
+
}
|
|
712
|
+
function computeByteOffsetAtLineStart(lines, lineEndings, lineIndex) {
|
|
713
|
+
let offset = 0;
|
|
714
|
+
for (let index = 0; index < lineIndex; index += 1) {
|
|
715
|
+
offset += Buffer.byteLength(lines[index] ?? "", "utf8");
|
|
716
|
+
offset += Buffer.byteLength(lineEndings[index] ?? "", "utf8");
|
|
717
|
+
}
|
|
718
|
+
return offset;
|
|
719
|
+
}
|
|
720
|
+
function hasAnyRegionMarker(bytes) {
|
|
721
|
+
const lines = bytes.toString("utf8").split(/\r?\n/u);
|
|
722
|
+
const fence = computeCodeFenceMask(lines);
|
|
723
|
+
return lines.some(
|
|
724
|
+
(line, index) => !fence[index] && (GENERATED_START_RE.test(line) || GENERATED_END_RE.test(line) || MANUAL_START_RE.test(line) || MANUAL_END_RE.test(line))
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
function hasAllRegionMarkers(bytes) {
|
|
728
|
+
const lines = bytes.toString("utf8").split(/\r?\n/u);
|
|
729
|
+
const fence = computeCodeFenceMask(lines);
|
|
730
|
+
const present = (re) => lines.some((line, index) => !fence[index] && re.test(line));
|
|
731
|
+
return present(GENERATED_START_RE) && present(GENERATED_END_RE) && present(MANUAL_START_RE) && present(MANUAL_END_RE);
|
|
732
|
+
}
|
|
733
|
+
function hasLegacyGeneratedMarker(bytes) {
|
|
734
|
+
const text = bytes.toString("utf8");
|
|
735
|
+
return text.split(/\r?\n/u).some((line) => line === LEGACY_GENERATED_MARKER);
|
|
736
|
+
}
|
|
737
|
+
function serializeMixedFile(input) {
|
|
738
|
+
const manualInner = endsWithNewline(input.manualInner) ? input.manualInner : Buffer.concat([input.manualInner, Buffer.from("\n", "utf8")]);
|
|
739
|
+
const parts = [
|
|
740
|
+
Buffer.from(`${GENERATED_START_MARKER}
|
|
741
|
+
`, "utf8"),
|
|
742
|
+
input.generatedInner,
|
|
743
|
+
Buffer.from(`${GENERATED_END_MARKER}
|
|
744
|
+
`, "utf8"),
|
|
745
|
+
Buffer.from("\n", "utf8"),
|
|
746
|
+
Buffer.from(`${MANUAL_START_MARKER}
|
|
747
|
+
`, "utf8"),
|
|
748
|
+
manualInner,
|
|
749
|
+
Buffer.from(`${MANUAL_END_MARKER}
|
|
750
|
+
`, "utf8")
|
|
751
|
+
];
|
|
752
|
+
return Buffer.concat(parts);
|
|
753
|
+
}
|
|
754
|
+
function endsWithNewline(bytes) {
|
|
755
|
+
if (bytes.length === 0) return false;
|
|
756
|
+
return bytes[bytes.length - 1] === 10;
|
|
757
|
+
}
|
|
758
|
+
function replaceGeneratedRegion(existing, newGeneratedInner) {
|
|
759
|
+
const parsed = parseMixedFile(existing);
|
|
760
|
+
if (!parsed.ok) return void 0;
|
|
761
|
+
const text = existing.toString("utf8");
|
|
762
|
+
let cursor = 0;
|
|
763
|
+
let position = 0;
|
|
764
|
+
let generatedStartInnerByte = -1;
|
|
765
|
+
let generatedEndLineStartByte = -1;
|
|
766
|
+
while (position < text.length) {
|
|
767
|
+
const newlineIndex = text.indexOf("\n", position);
|
|
768
|
+
const hasNewline = newlineIndex !== -1;
|
|
769
|
+
let lineEnd = hasNewline ? newlineIndex : text.length;
|
|
770
|
+
let endingLength = 0;
|
|
771
|
+
if (hasNewline) {
|
|
772
|
+
endingLength = 1;
|
|
773
|
+
if (lineEnd > position && text[lineEnd - 1] === "\r") {
|
|
774
|
+
lineEnd -= 1;
|
|
775
|
+
endingLength = 2;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const line = text.slice(position, lineEnd);
|
|
779
|
+
const lineBytes = Buffer.byteLength(line, "utf8");
|
|
780
|
+
if (line === GENERATED_START_MARKER && generatedStartInnerByte === -1) {
|
|
781
|
+
generatedStartInnerByte = cursor + lineBytes + endingLength;
|
|
782
|
+
}
|
|
783
|
+
if (line === GENERATED_END_MARKER && generatedEndLineStartByte === -1) {
|
|
784
|
+
generatedEndLineStartByte = cursor;
|
|
785
|
+
}
|
|
786
|
+
cursor += lineBytes + endingLength;
|
|
787
|
+
position = hasNewline ? newlineIndex + 1 : text.length;
|
|
788
|
+
}
|
|
789
|
+
if (generatedStartInnerByte === -1 || generatedEndLineStartByte === -1) {
|
|
790
|
+
return void 0;
|
|
791
|
+
}
|
|
792
|
+
const before = existing.subarray(0, generatedStartInnerByte);
|
|
793
|
+
const after = existing.subarray(generatedEndLineStartByte);
|
|
794
|
+
return Buffer.concat([before, newGeneratedInner, after]);
|
|
795
|
+
}
|
|
375
796
|
|
|
376
797
|
// ../../packages/core/src/profile.ts
|
|
377
798
|
import {
|
|
@@ -1151,14 +1572,14 @@ function toValidationIssues(errors, rootValue) {
|
|
|
1151
1572
|
return errors.map((error) => toValidationIssue(error, rootValue)).sort((left, right) => compareIssues(left, right));
|
|
1152
1573
|
}
|
|
1153
1574
|
function toValidationIssue(error, rootValue) {
|
|
1154
|
-
const
|
|
1155
|
-
const code =
|
|
1575
|
+
const path7 = getErrorPath(error);
|
|
1576
|
+
const code = path7 === "/version" && hasOwnProperty(rootValue, "version") ? "unsupported_schema_version" : "schema_validation_error";
|
|
1156
1577
|
return {
|
|
1157
1578
|
code,
|
|
1158
|
-
path:
|
|
1579
|
+
path: path7,
|
|
1159
1580
|
expected: getExpected(error),
|
|
1160
1581
|
actual: getActual(error, rootValue),
|
|
1161
|
-
message: getMessage(error,
|
|
1582
|
+
message: getMessage(error, path7)
|
|
1162
1583
|
};
|
|
1163
1584
|
}
|
|
1164
1585
|
function getErrorPath(error) {
|
|
@@ -1209,20 +1630,20 @@ function getActual(error, rootValue) {
|
|
|
1209
1630
|
}
|
|
1210
1631
|
return describeValue2(getValueAtJsonPointer(rootValue, error.instancePath));
|
|
1211
1632
|
}
|
|
1212
|
-
function getMessage(error,
|
|
1633
|
+
function getMessage(error, path7) {
|
|
1213
1634
|
switch (error.keyword) {
|
|
1214
1635
|
case "required":
|
|
1215
|
-
return `${
|
|
1636
|
+
return `${path7} is required.`;
|
|
1216
1637
|
case "additionalProperties":
|
|
1217
|
-
return `${
|
|
1638
|
+
return `${path7} is not allowed.`;
|
|
1218
1639
|
case "const":
|
|
1219
|
-
return `${
|
|
1640
|
+
return `${path7} must match the supported constant.`;
|
|
1220
1641
|
case "enum":
|
|
1221
|
-
return `${
|
|
1642
|
+
return `${path7} must be one of the supported values.`;
|
|
1222
1643
|
case "type":
|
|
1223
|
-
return `${
|
|
1644
|
+
return `${path7} has the wrong type.`;
|
|
1224
1645
|
default:
|
|
1225
|
-
return `${
|
|
1646
|
+
return `${path7} ${error.message ?? "is invalid"}.`;
|
|
1226
1647
|
}
|
|
1227
1648
|
}
|
|
1228
1649
|
function compareIssues(left, right) {
|
|
@@ -2072,6 +2493,8 @@ ${topic.agentsMdChecklistReference}
|
|
|
2072
2493
|
|
|
2073
2494
|
// ../../packages/compiler/src/compiler.ts
|
|
2074
2495
|
var WORKFLOW_SKILLS = [
|
|
2496
|
+
{ id: "grill-change", workflowFlag: "sdd" },
|
|
2497
|
+
{ id: "request-to-spec-issues", workflowFlag: "sdd" },
|
|
2075
2498
|
{ id: "sdd-change", workflowFlag: "sdd" },
|
|
2076
2499
|
{ id: "tdd-change", workflowFlag: "tdd" },
|
|
2077
2500
|
{ id: "final-review", workflowFlag: "finalReview" },
|
|
@@ -2110,6 +2533,10 @@ var TEMPLATE_SOURCES = [
|
|
|
2110
2533
|
"targets/tabnine-guidelines/00-general-agent-behavior@1",
|
|
2111
2534
|
renderGeneralAgentBehaviorTemplateSource
|
|
2112
2535
|
),
|
|
2536
|
+
guidelineTemplateSource(
|
|
2537
|
+
"targets/tabnine-guidelines/05-planning-workflow@1",
|
|
2538
|
+
renderPlanningWorkflowGuideline
|
|
2539
|
+
),
|
|
2113
2540
|
guidelineTemplateSource(
|
|
2114
2541
|
"targets/tabnine-guidelines/10-sdd-workflow@1",
|
|
2115
2542
|
renderSddWorkflowGuideline
|
|
@@ -2162,6 +2589,16 @@ var TEMPLATE_SOURCES = [
|
|
|
2162
2589
|
version: "1",
|
|
2163
2590
|
source: renderCodexConfigToml()
|
|
2164
2591
|
},
|
|
2592
|
+
workflowSkillTemplateSource(
|
|
2593
|
+
"targets/codex-workflow-skills/grill-change@1",
|
|
2594
|
+
"codex-workflow-skills",
|
|
2595
|
+
"grill-change"
|
|
2596
|
+
),
|
|
2597
|
+
workflowSkillTemplateSource(
|
|
2598
|
+
"targets/codex-workflow-skills/request-to-spec-issues@1",
|
|
2599
|
+
"codex-workflow-skills",
|
|
2600
|
+
"request-to-spec-issues"
|
|
2601
|
+
),
|
|
2165
2602
|
workflowSkillTemplateSource(
|
|
2166
2603
|
"targets/codex-workflow-skills/sdd-change@1",
|
|
2167
2604
|
"codex-workflow-skills",
|
|
@@ -2200,6 +2637,16 @@ var TEMPLATE_SOURCES = [
|
|
|
2200
2637
|
version: "1",
|
|
2201
2638
|
source: renderClaudeMd()
|
|
2202
2639
|
},
|
|
2640
|
+
workflowSkillTemplateSource(
|
|
2641
|
+
"targets/claude-workflow-skills/grill-change@1",
|
|
2642
|
+
"claude-workflow-skills",
|
|
2643
|
+
"grill-change"
|
|
2644
|
+
),
|
|
2645
|
+
workflowSkillTemplateSource(
|
|
2646
|
+
"targets/claude-workflow-skills/request-to-spec-issues@1",
|
|
2647
|
+
"claude-workflow-skills",
|
|
2648
|
+
"request-to-spec-issues"
|
|
2649
|
+
),
|
|
2203
2650
|
workflowSkillTemplateSource(
|
|
2204
2651
|
"targets/claude-workflow-skills/sdd-change@1",
|
|
2205
2652
|
"claude-workflow-skills",
|
|
@@ -2233,7 +2680,10 @@ function compileProfile(request) {
|
|
|
2233
2680
|
const targets = uniqueTargets(
|
|
2234
2681
|
request.targets ?? getEnabledTargetIds(request.profile)
|
|
2235
2682
|
);
|
|
2236
|
-
const templates = request.templates ?? mergeTemplates(
|
|
2683
|
+
const templates = request.templates ?? mergeTemplates(
|
|
2684
|
+
getDefaultTemplates(),
|
|
2685
|
+
getSubagentTemplates(request.profile)
|
|
2686
|
+
);
|
|
2237
2687
|
const issues = validateTargets(request.profile, targets, templates);
|
|
2238
2688
|
if (issues.length > 0) {
|
|
2239
2689
|
return {
|
|
@@ -2244,7 +2694,10 @@ function compileProfile(request) {
|
|
|
2244
2694
|
const files = targets.flatMap(
|
|
2245
2695
|
(target) => renderTarget(target, request.profile)
|
|
2246
2696
|
);
|
|
2247
|
-
const requiredTemplateIds = getRequiredTemplateIdSet(
|
|
2697
|
+
const requiredTemplateIds = getRequiredTemplateIdSet(
|
|
2698
|
+
targets,
|
|
2699
|
+
request.profile
|
|
2700
|
+
);
|
|
2248
2701
|
return {
|
|
2249
2702
|
ok: true,
|
|
2250
2703
|
files: files.sort(compareGeneratedFiles),
|
|
@@ -2321,6 +2774,10 @@ ${renderTopicAsAgentsMdSection(REFACTORING_TOPIC)}` : "";
|
|
|
2321
2774
|
${renderTopicAsAgentsMdSection(DOCUMENTATION_TOPIC)}` : "";
|
|
2322
2775
|
return normalizeGeneratedText(`# AGENTS.md
|
|
2323
2776
|
|
|
2777
|
+
## Instruction Precedence
|
|
2778
|
+
|
|
2779
|
+
If generated and manual instructions conflict, follow the manual project instructions unless they would weaken safety, privacy, or permission limits. Safety, privacy, and explicit deny rules always win.
|
|
2780
|
+
|
|
2324
2781
|
## Project
|
|
2325
2782
|
|
|
2326
2783
|
Name: ${escapeMarkdownText(profile.profile.name)}
|
|
@@ -2380,6 +2837,10 @@ Cursor, Aider, Copilot, hosted gateways, enterprise RBAC, SIEM integrations, and
|
|
|
2380
2837
|
function renderClaudeMd() {
|
|
2381
2838
|
return `<!-- Generated by Agent Profile Compiler. Do not edit by hand. -->
|
|
2382
2839
|
|
|
2840
|
+
## Instruction Precedence
|
|
2841
|
+
|
|
2842
|
+
If generated and manual instructions conflict, follow the manual project instructions unless they would weaken safety, privacy, or permission limits. Safety, privacy, and explicit deny rules always win.
|
|
2843
|
+
|
|
2383
2844
|
@AGENTS.md
|
|
2384
2845
|
|
|
2385
2846
|
# Claude Code
|
|
@@ -2498,6 +2959,14 @@ function renderTabnineGuidelines(profile) {
|
|
|
2498
2959
|
)
|
|
2499
2960
|
];
|
|
2500
2961
|
if (profile.workflow.sdd) {
|
|
2962
|
+
files.push(
|
|
2963
|
+
createGeneratedTextFile(
|
|
2964
|
+
".tabnine/guidelines/05-planning-workflow.md",
|
|
2965
|
+
common.target,
|
|
2966
|
+
"targets/tabnine-guidelines/05-planning-workflow@1",
|
|
2967
|
+
renderPlanningWorkflowGuideline()
|
|
2968
|
+
)
|
|
2969
|
+
);
|
|
2501
2970
|
files.push(
|
|
2502
2971
|
createGeneratedTextFile(
|
|
2503
2972
|
".tabnine/guidelines/10-sdd-workflow.md",
|
|
@@ -2617,6 +3086,195 @@ function getWorkflowSkillTemplateId(target, skill) {
|
|
|
2617
3086
|
}
|
|
2618
3087
|
function renderWorkflowSkill(skill) {
|
|
2619
3088
|
switch (skill) {
|
|
3089
|
+
case "grill-change":
|
|
3090
|
+
return `---
|
|
3091
|
+
name: grill-change
|
|
3092
|
+
description: Use when a stakeholder request is rough, ambiguous, or underspecified and needs clarification before planning, writing a spec, or creating issues.
|
|
3093
|
+
---
|
|
3094
|
+
|
|
3095
|
+
<!-- Generated by Agent Profile Compiler. Do not edit by hand. -->
|
|
3096
|
+
|
|
3097
|
+
# Grill Change
|
|
3098
|
+
|
|
3099
|
+
## Purpose
|
|
3100
|
+
|
|
3101
|
+
Clarify a stakeholder request before any spec, issue plan, or implementation work starts. The output is an agreement record for a follow-up synthesis workflow; it is not an implementation plan.
|
|
3102
|
+
|
|
3103
|
+
## Operating Rules
|
|
3104
|
+
|
|
3105
|
+
1. Ask one focused question, then wait for the user's answer.
|
|
3106
|
+
2. Include a recommended answer with the question and explain why that answer is safest for the project.
|
|
3107
|
+
3. Inspect relevant local specs, ADRs, docs, fixtures, and code before asking questions that local context can answer.
|
|
3108
|
+
4. Challenge vague terms, overloaded words, and claims that conflict with repository evidence.
|
|
3109
|
+
5. Prefer concrete examples, edge cases, and tradeoff choices over broad brainstorming.
|
|
3110
|
+
6. Keep implementation details provisional until the product intent and non-goals are settled.
|
|
3111
|
+
7. Capture durable terms and hard-to-reverse decisions in the agreement record when they crystallize.
|
|
3112
|
+
8. Do not create issue lists, edit files, or start implementation during the grill.
|
|
3113
|
+
|
|
3114
|
+
## Question Format
|
|
3115
|
+
|
|
3116
|
+
Use this shape for each question:
|
|
3117
|
+
|
|
3118
|
+
Question: \`<one decision or missing fact>\`
|
|
3119
|
+
Recommended answer: \`<the default direction you would choose>\`
|
|
3120
|
+
Why: \`<short reason based on product intent, safety, contracts, or repo evidence>\`
|
|
3121
|
+
|
|
3122
|
+
## Decision Checks
|
|
3123
|
+
|
|
3124
|
+
Before ending the grill, confirm:
|
|
3125
|
+
|
|
3126
|
+
- the problem in the stakeholder's terms
|
|
3127
|
+
- the desired outcome
|
|
3128
|
+
- explicit non-goals
|
|
3129
|
+
- product intent and why the change matters
|
|
3130
|
+
- tradeoff direction for ambiguous implementation choices
|
|
3131
|
+
- user-visible behavior changes
|
|
3132
|
+
- compatibility and migration expectations
|
|
3133
|
+
- durable terminology and hard-to-reverse decisions
|
|
3134
|
+
- safety and privacy constraints
|
|
3135
|
+
- unresolved unknowns, if any
|
|
3136
|
+
|
|
3137
|
+
## Output
|
|
3138
|
+
|
|
3139
|
+
When the user agrees the grill is complete, return:
|
|
3140
|
+
|
|
3141
|
+
- Problem
|
|
3142
|
+
- Intent
|
|
3143
|
+
- Non-goals
|
|
3144
|
+
- Decisions made
|
|
3145
|
+
- Durable terms and hard-to-reverse decisions
|
|
3146
|
+
- Decision rules for implementation tradeoffs
|
|
3147
|
+
- Open questions or risks
|
|
3148
|
+
- Confirmation that post-grill synthesis can run next
|
|
3149
|
+
|
|
3150
|
+
## Safety
|
|
3151
|
+
|
|
3152
|
+
- Do not upload source code.
|
|
3153
|
+
- Do not read or print secrets.
|
|
3154
|
+
- Do not ask for credentials, environment values, production data, or private endpoints.
|
|
3155
|
+
- Do not propose \`bypassPermissions\`, tool pre-approval, dependency auto-installation, hosted execution, or remote MCP behavior.
|
|
3156
|
+
- Do not write files, create issues, commit changes, or run implementation commands during the grill.
|
|
3157
|
+
`;
|
|
3158
|
+
case "request-to-spec-issues":
|
|
3159
|
+
return `---
|
|
3160
|
+
name: request-to-spec-issues
|
|
3161
|
+
description: Use after a grill-change session is complete to turn the agreement record into an intent-first spec candidate and vertical TDD-ready issue briefs.
|
|
3162
|
+
---
|
|
3163
|
+
|
|
3164
|
+
<!-- Generated by Agent Profile Compiler. Do not edit by hand. -->
|
|
3165
|
+
|
|
3166
|
+
# Request To Spec Issues
|
|
3167
|
+
|
|
3168
|
+
## Purpose
|
|
3169
|
+
|
|
3170
|
+
Convert a completed \`grill-change\` agreement record into an intent-first spec candidate and vertical TDD-ready issue briefs. Preserve product intent and decision rules so implementation choices follow the agreed direction.
|
|
3171
|
+
|
|
3172
|
+
## Preconditions
|
|
3173
|
+
|
|
3174
|
+
- The grill is complete.
|
|
3175
|
+
- The user has confirmed the direction is ready for synthesis.
|
|
3176
|
+
- Relevant local specs, ADRs, docs, fixtures, and code context have been checked.
|
|
3177
|
+
- If there is no completed grill agreement, stop and run \`grill-change\` first.
|
|
3178
|
+
|
|
3179
|
+
## Synthesis Rules
|
|
3180
|
+
|
|
3181
|
+
1. Do not re-interview the user unless the grill record contains a contradiction or a genuinely missing decision.
|
|
3182
|
+
2. Keep product intent, non-goals, durable terms, and hard-to-reverse decisions above implementation mechanics.
|
|
3183
|
+
3. Preserve existing spec contracts and safety rules unless the user explicitly approved changing them.
|
|
3184
|
+
4. Split work into vertical behavior slices, not file layers.
|
|
3185
|
+
5. Make every issue small enough for a focused RED, GREEN, refactor loop.
|
|
3186
|
+
6. Mark dependencies and parallel-safe work explicitly.
|
|
3187
|
+
7. Propose architecture rescue candidates before feature slices when a change would deepen an already fragmented codebase.
|
|
3188
|
+
|
|
3189
|
+
## Architecture Rescue Candidates
|
|
3190
|
+
|
|
3191
|
+
When architecture rescue is needed, propose candidates before feature issues. Each candidate must include:
|
|
3192
|
+
|
|
3193
|
+
- Files or modules involved
|
|
3194
|
+
- Current friction
|
|
3195
|
+
- Proposed deeper module or clearer interface
|
|
3196
|
+
- Expected locality and leverage improvement
|
|
3197
|
+
- Expected test improvement
|
|
3198
|
+
- ADR or spec conflicts
|
|
3199
|
+
- Recommended dependency state: prerequisite, parallel, or later cleanup
|
|
3200
|
+
|
|
3201
|
+
## Spec Candidate
|
|
3202
|
+
|
|
3203
|
+
Include these sections:
|
|
3204
|
+
|
|
3205
|
+
- Status
|
|
3206
|
+
- Problem
|
|
3207
|
+
- Goal
|
|
3208
|
+
- Intent
|
|
3209
|
+
- Decision Rules
|
|
3210
|
+
- Non-Goals
|
|
3211
|
+
- User Flow
|
|
3212
|
+
- Inputs
|
|
3213
|
+
- Outputs
|
|
3214
|
+
- Contracts
|
|
3215
|
+
- Security Rules
|
|
3216
|
+
- Acceptance Criteria
|
|
3217
|
+
- Tests
|
|
3218
|
+
- TDD Strategy
|
|
3219
|
+
- Issue Plan
|
|
3220
|
+
- Documentation Updates
|
|
3221
|
+
- Final Review Checklist
|
|
3222
|
+
|
|
3223
|
+
\`TDD Strategy\` complements \`Tests\`; it must not replace the required \`Tests\`
|
|
3224
|
+
section from \`docs/specs/SPEC_TEMPLATE.md\`.
|
|
3225
|
+
|
|
3226
|
+
## Issue Brief Format
|
|
3227
|
+
|
|
3228
|
+
Each issue brief must include:
|
|
3229
|
+
|
|
3230
|
+
- Title
|
|
3231
|
+
- Parent spec or request
|
|
3232
|
+
- Intent summary
|
|
3233
|
+
- Behavior slice
|
|
3234
|
+
- Non-goals
|
|
3235
|
+
- Acceptance criteria
|
|
3236
|
+
- Expected RED proof
|
|
3237
|
+
- Expected GREEN proof
|
|
3238
|
+
- Test command guidance
|
|
3239
|
+
- Likely file ownership
|
|
3240
|
+
- Dependencies
|
|
3241
|
+
- Parallelism notes
|
|
3242
|
+
- Contract impact
|
|
3243
|
+
- Security impact
|
|
3244
|
+
- Documentation impact
|
|
3245
|
+
- Implementation context
|
|
3246
|
+
- Review expectations
|
|
3247
|
+
|
|
3248
|
+
## Dependency States
|
|
3249
|
+
|
|
3250
|
+
Use these states:
|
|
3251
|
+
|
|
3252
|
+
- \`ready\`
|
|
3253
|
+
- \`blocked\`
|
|
3254
|
+
- \`parallel-safe\`
|
|
3255
|
+
- \`sequenced\`
|
|
3256
|
+
- \`human-gate\`
|
|
3257
|
+
|
|
3258
|
+
## Output
|
|
3259
|
+
|
|
3260
|
+
Return:
|
|
3261
|
+
|
|
3262
|
+
- Spec candidate or spec patch
|
|
3263
|
+
- Vertical issue briefs
|
|
3264
|
+
- Dependency map
|
|
3265
|
+
- Parallelism map
|
|
3266
|
+
- Human gates
|
|
3267
|
+
- Recommended next step
|
|
3268
|
+
|
|
3269
|
+
## Safety
|
|
3270
|
+
|
|
3271
|
+
- Do not upload source code.
|
|
3272
|
+
- Do not read or print secrets.
|
|
3273
|
+
- Do not include credentials, environment values, production data, or private endpoints.
|
|
3274
|
+
- Do not create GitHub issues, labels, projects, or milestones.
|
|
3275
|
+
- Do not write files unless the user explicitly asks for local file writes after reviewing the synthesis.
|
|
3276
|
+
- Do not propose \`bypassPermissions\`, tool pre-approval, dependency auto-installation, hosted execution, or remote MCP behavior.
|
|
3277
|
+
`;
|
|
2620
3278
|
case "sdd-change":
|
|
2621
3279
|
return `---
|
|
2622
3280
|
name: sdd-change
|
|
@@ -2790,6 +3448,23 @@ Use the repository-local profile as the source of truth for agent behavior.
|
|
|
2790
3448
|
- Ask before mutating files, running shell commands, installing dependencies, or using external network access.
|
|
2791
3449
|
`;
|
|
2792
3450
|
}
|
|
3451
|
+
function renderPlanningWorkflowGuideline() {
|
|
3452
|
+
return `<!-- Generated by Agent Profile Compiler. Do not edit by hand. -->
|
|
3453
|
+
|
|
3454
|
+
# Planning Workflow
|
|
3455
|
+
|
|
3456
|
+
Use this before implementation when a stakeholder request is rough, ambiguous, or not yet tied to an approved spec.
|
|
3457
|
+
|
|
3458
|
+
- Clarify the request one decision at a time before writing or changing specs.
|
|
3459
|
+
- Provide a recommended answer and short rationale for each open decision.
|
|
3460
|
+
- Check local specs, ADRs, docs, fixtures, and generated artifacts before asking questions.
|
|
3461
|
+
- Preserve product intent, non-goals, tradeoffs, durable terms, and hard-to-reverse decisions.
|
|
3462
|
+
- If no completed clarification exists, complete the grill-style clarification first.
|
|
3463
|
+
- After clarification, prepare an intent-first spec candidate and vertical TDD-ready issue briefs.
|
|
3464
|
+
- Include dependencies, expected RED proof, expected GREEN proof, file ownership, contract impact, security impact, and review expectations in each issue brief.
|
|
3465
|
+
- Do not create GitHub issues, write files, upload source, read secrets, install dependencies, or change runtime permissions unless explicitly requested and allowed.
|
|
3466
|
+
`;
|
|
3467
|
+
}
|
|
2793
3468
|
function renderSddWorkflowGuideline() {
|
|
2794
3469
|
return `<!-- Generated by Agent Profile Compiler. Do not edit by hand. -->
|
|
2795
3470
|
|
|
@@ -2953,13 +3628,13 @@ function renderClaudeSubagent(agent, effective) {
|
|
|
2953
3628
|
lines.push("tools: Read, Glob, Grep");
|
|
2954
3629
|
} else {
|
|
2955
3630
|
const tools = ["Read", "Glob", "Grep"];
|
|
2956
|
-
if (effective.filesystem.write
|
|
3631
|
+
if (effective.filesystem.write !== "deny") {
|
|
2957
3632
|
tools.push("Edit", "Write");
|
|
2958
3633
|
}
|
|
2959
|
-
if (effective.shell.run
|
|
3634
|
+
if (effective.shell.run !== "deny") {
|
|
2960
3635
|
tools.push("Bash");
|
|
2961
3636
|
}
|
|
2962
|
-
if (effective.network.external
|
|
3637
|
+
if (effective.network.external !== "deny") {
|
|
2963
3638
|
tools.push("WebFetch");
|
|
2964
3639
|
}
|
|
2965
3640
|
lines.push(`tools: ${tools.join(", ")}`);
|
|
@@ -2973,7 +3648,9 @@ function renderClaudeSubagent(agent, effective) {
|
|
|
2973
3648
|
lines.push(`maxTurns: ${agent.maxTurns}`);
|
|
2974
3649
|
}
|
|
2975
3650
|
lines.push("---", "");
|
|
2976
|
-
lines.push(
|
|
3651
|
+
lines.push(
|
|
3652
|
+
"<!-- Generated by Agent Profile Compiler. Do not edit by hand. -->"
|
|
3653
|
+
);
|
|
2977
3654
|
lines.push("");
|
|
2978
3655
|
lines.push(`# ${titleCaseFromKebab(agent.name)}`);
|
|
2979
3656
|
lines.push("");
|
|
@@ -3021,7 +3698,9 @@ function renderTabnineSubagent(agent) {
|
|
|
3021
3698
|
lines.push(`timeout_mins: ${agent.timeoutMinutes}`);
|
|
3022
3699
|
}
|
|
3023
3700
|
lines.push("---", "");
|
|
3024
|
-
lines.push(
|
|
3701
|
+
lines.push(
|
|
3702
|
+
"<!-- Generated by Agent Profile Compiler. Do not edit by hand. -->"
|
|
3703
|
+
);
|
|
3025
3704
|
lines.push("");
|
|
3026
3705
|
lines.push(`# ${titleCaseFromKebab(agent.name)}`);
|
|
3027
3706
|
lines.push("");
|
|
@@ -3163,7 +3842,9 @@ function validateTargets(profile, targets, templates) {
|
|
|
3163
3842
|
const enabledTargets = new Set(getEnabledTargetIds(profile));
|
|
3164
3843
|
const issues = [];
|
|
3165
3844
|
if (profile.workflow.subagentDrivenDevelopment === true) {
|
|
3166
|
-
const refs = new Set(
|
|
3845
|
+
const refs = new Set(
|
|
3846
|
+
getSubagentTemplateRefs(profile)
|
|
3847
|
+
);
|
|
3167
3848
|
const missing = SUBAGENT_TEMPLATE_NAMES.filter((name) => !refs.has(name));
|
|
3168
3849
|
if (missing.length > 0) {
|
|
3169
3850
|
issues.push({
|
|
@@ -3320,6 +4001,7 @@ function getRequiredAgentsMdTopicTemplateIds(profile) {
|
|
|
3320
4001
|
function getRequiredTabnineGuidelineTemplateIds(profile) {
|
|
3321
4002
|
const ids = ["targets/tabnine-guidelines/00-general-agent-behavior@1"];
|
|
3322
4003
|
if (profile.workflow.sdd) {
|
|
4004
|
+
ids.push("targets/tabnine-guidelines/05-planning-workflow@1");
|
|
3323
4005
|
ids.push("targets/tabnine-guidelines/10-sdd-workflow@1");
|
|
3324
4006
|
}
|
|
3325
4007
|
if (profile.workflow.tdd) {
|
|
@@ -3469,6 +4151,11 @@ async function assertWritePathContained(rootRealPath, relativePath) {
|
|
|
3469
4151
|
}
|
|
3470
4152
|
const existingTarget = await lstatOptional(absolutePath);
|
|
3471
4153
|
if (existingTarget) {
|
|
4154
|
+
if (existingTarget.isSymbolicLink()) {
|
|
4155
|
+
throw new Error(
|
|
4156
|
+
`Planned write target is a symlink (refusing to follow): ${safePath}`
|
|
4157
|
+
);
|
|
4158
|
+
}
|
|
3472
4159
|
const targetRealPath = await fsPromises.realpath(absolutePath);
|
|
3473
4160
|
if (!isContainedBy(rootRealPath, targetRealPath)) {
|
|
3474
4161
|
throw new Error(`Planned write target escapes root: ${safePath}`);
|
|
@@ -3484,7 +4171,8 @@ async function assertWritePathContained(rootRealPath, relativePath) {
|
|
|
3484
4171
|
async function findExistingAncestor(startPath) {
|
|
3485
4172
|
let current = startPath;
|
|
3486
4173
|
while (true) {
|
|
3487
|
-
|
|
4174
|
+
const stat = await lstatOptional(current);
|
|
4175
|
+
if (stat) {
|
|
3488
4176
|
return current;
|
|
3489
4177
|
}
|
|
3490
4178
|
const parent = path.dirname(current);
|
|
@@ -3506,8 +4194,7 @@ async function readOptionalFile(absolutePath) {
|
|
|
3506
4194
|
}
|
|
3507
4195
|
async function lstatOptional(absolutePath) {
|
|
3508
4196
|
try {
|
|
3509
|
-
await fsPromises.lstat(absolutePath);
|
|
3510
|
-
return true;
|
|
4197
|
+
return await fsPromises.lstat(absolutePath);
|
|
3511
4198
|
} catch (error) {
|
|
3512
4199
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
3513
4200
|
return void 0;
|
|
@@ -3523,28 +4210,648 @@ function isNodeError(error) {
|
|
|
3523
4210
|
return error instanceof Error && "code" in error;
|
|
3524
4211
|
}
|
|
3525
4212
|
|
|
3526
|
-
// ../../packages/
|
|
4213
|
+
// ../../packages/compiler/src/import-report.ts
|
|
3527
4214
|
import fsPromises2 from "node:fs/promises";
|
|
3528
4215
|
import path2 from "node:path";
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
"
|
|
3539
|
-
"
|
|
3540
|
-
"
|
|
3541
|
-
"vite.config.mts",
|
|
3542
|
-
"vite.config.cts"
|
|
4216
|
+
var PHASE_14_SUPPORTED_PATHS = [
|
|
4217
|
+
{ path: "AGENTS.md", kind: "root-instructions", isLocalRuntime: false },
|
|
4218
|
+
{ path: "CLAUDE.md", kind: "root-instructions", isLocalRuntime: false },
|
|
4219
|
+
{ path: ".claude/settings.json", kind: "client-config", isLocalRuntime: false },
|
|
4220
|
+
{
|
|
4221
|
+
path: ".claude/settings.local.json",
|
|
4222
|
+
kind: "client-config",
|
|
4223
|
+
isLocalRuntime: true
|
|
4224
|
+
},
|
|
4225
|
+
{ path: ".codex/config.toml", kind: "client-config", isLocalRuntime: true },
|
|
4226
|
+
{ path: ".codex/hooks.json", kind: "client-config", isLocalRuntime: true },
|
|
4227
|
+
{ path: ".mcp.json", kind: "mcp-config", isLocalRuntime: true }
|
|
3543
4228
|
];
|
|
3544
|
-
var
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
4229
|
+
var PHASE_14_SCAN_DIRS = [
|
|
4230
|
+
{
|
|
4231
|
+
root: ".agents/skills",
|
|
4232
|
+
kind: "workflow-skill",
|
|
4233
|
+
fileFilter: (rel) => rel.endsWith("/SKILL.md"),
|
|
4234
|
+
recursive: true
|
|
4235
|
+
},
|
|
4236
|
+
{
|
|
4237
|
+
root: ".claude/skills",
|
|
4238
|
+
kind: "workflow-skill",
|
|
4239
|
+
fileFilter: (rel) => rel.endsWith("/SKILL.md"),
|
|
4240
|
+
recursive: true
|
|
4241
|
+
},
|
|
4242
|
+
{
|
|
4243
|
+
root: ".claude/agents",
|
|
4244
|
+
kind: "subagent",
|
|
4245
|
+
fileFilter: (rel) => rel.endsWith(".md"),
|
|
4246
|
+
recursive: false
|
|
4247
|
+
},
|
|
4248
|
+
{
|
|
4249
|
+
root: ".codex/agents",
|
|
4250
|
+
kind: "subagent",
|
|
4251
|
+
fileFilter: (rel) => rel.endsWith(".toml"),
|
|
4252
|
+
recursive: false
|
|
4253
|
+
},
|
|
4254
|
+
{
|
|
4255
|
+
root: ".tabnine/agent/agents",
|
|
4256
|
+
kind: "subagent",
|
|
4257
|
+
fileFilter: (rel) => rel.endsWith(".md"),
|
|
4258
|
+
recursive: false
|
|
4259
|
+
}
|
|
4260
|
+
];
|
|
4261
|
+
var RECOMMENDED_IGNORE_LINES = [
|
|
4262
|
+
".cce/",
|
|
4263
|
+
".mcp.json",
|
|
4264
|
+
".claude/settings.local.json",
|
|
4265
|
+
".claude/worktrees/",
|
|
4266
|
+
".codex/config.toml",
|
|
4267
|
+
".codex/hooks.json"
|
|
4268
|
+
];
|
|
4269
|
+
async function buildPhase14ImportReport(input) {
|
|
4270
|
+
const files = [];
|
|
4271
|
+
let wouldUpdateRegions = 0;
|
|
4272
|
+
let preservedManualFiles = 0;
|
|
4273
|
+
let conflicts = 0;
|
|
4274
|
+
const collisionsByKey = /* @__PURE__ */ new Map();
|
|
4275
|
+
const lockfile = await readLockfileForRegions(input.rootDir);
|
|
4276
|
+
const ownershipByPath = /* @__PURE__ */ new Map();
|
|
4277
|
+
if (lockfile) {
|
|
4278
|
+
for (const output of lockfile.outputs) {
|
|
4279
|
+
ownershipByPath.set(output.path, output.ownership);
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
for (const entry of PHASE_14_SUPPORTED_PATHS) {
|
|
4283
|
+
const read = await readRegionAwareFile(input.rootDir, entry.path);
|
|
4284
|
+
if (read.refused) {
|
|
4285
|
+
files.push({
|
|
4286
|
+
path: entry.path,
|
|
4287
|
+
exists: true,
|
|
4288
|
+
kind: entry.kind,
|
|
4289
|
+
ownership: "unknown",
|
|
4290
|
+
tags: [],
|
|
4291
|
+
action: "refuse-conflict",
|
|
4292
|
+
notes: ["symlinked; Phase 14 refuses to follow file symlinks"]
|
|
4293
|
+
});
|
|
4294
|
+
conflicts += 1;
|
|
4295
|
+
continue;
|
|
4296
|
+
}
|
|
4297
|
+
const existing = read.bytes;
|
|
4298
|
+
if (!existing) {
|
|
4299
|
+
if (entry.kind === "root-instructions") {
|
|
4300
|
+
files.push({
|
|
4301
|
+
path: entry.path,
|
|
4302
|
+
exists: false,
|
|
4303
|
+
kind: entry.kind,
|
|
4304
|
+
ownership: "unknown",
|
|
4305
|
+
tags: [],
|
|
4306
|
+
action: "create",
|
|
4307
|
+
notes: []
|
|
4308
|
+
});
|
|
4309
|
+
}
|
|
4310
|
+
continue;
|
|
4311
|
+
}
|
|
4312
|
+
const bytes = Buffer.from(existing);
|
|
4313
|
+
const tags = [];
|
|
4314
|
+
if (entry.isLocalRuntime) {
|
|
4315
|
+
tags.push("local-runtime");
|
|
4316
|
+
}
|
|
4317
|
+
if (containsAbsolutePathLiteral(bytes)) {
|
|
4318
|
+
tags.push("contains-absolute-path");
|
|
4319
|
+
}
|
|
4320
|
+
if (entry.kind === "client-config" && !entry.isLocalRuntime) {
|
|
4321
|
+
const ownership = ownershipByPath.get(entry.path) ?? "generated-owned";
|
|
4322
|
+
files.push({
|
|
4323
|
+
path: entry.path,
|
|
4324
|
+
exists: true,
|
|
4325
|
+
kind: entry.kind,
|
|
4326
|
+
ownership,
|
|
4327
|
+
tags,
|
|
4328
|
+
action: "preserve",
|
|
4329
|
+
notes: [
|
|
4330
|
+
"generated client config; refresh via `agent-profile compile --write`"
|
|
4331
|
+
]
|
|
4332
|
+
});
|
|
4333
|
+
preservedManualFiles += 1;
|
|
4334
|
+
continue;
|
|
4335
|
+
}
|
|
4336
|
+
if (entry.kind !== "root-instructions") {
|
|
4337
|
+
files.push({
|
|
4338
|
+
path: entry.path,
|
|
4339
|
+
exists: true,
|
|
4340
|
+
kind: entry.kind,
|
|
4341
|
+
ownership: "manual-owned",
|
|
4342
|
+
tags,
|
|
4343
|
+
action: "ignore-local-runtime",
|
|
4344
|
+
notes: entry.kind === "mcp-config" ? [
|
|
4345
|
+
"contains MCP entries; not imported into ai-profile.yaml in Phase 14"
|
|
4346
|
+
] : ["local runtime config; not adopted by Phase 14"]
|
|
4347
|
+
});
|
|
4348
|
+
preservedManualFiles += 1;
|
|
4349
|
+
continue;
|
|
4350
|
+
}
|
|
4351
|
+
if (hasAllRegionMarkers(bytes)) {
|
|
4352
|
+
const parsed = parseMixedFile(bytes);
|
|
4353
|
+
if (!parsed.ok) {
|
|
4354
|
+
files.push({
|
|
4355
|
+
path: entry.path,
|
|
4356
|
+
exists: true,
|
|
4357
|
+
kind: entry.kind,
|
|
4358
|
+
ownership: "unknown",
|
|
4359
|
+
tags,
|
|
4360
|
+
action: "refuse-conflict",
|
|
4361
|
+
notes: parsed.issues.map((item) => item.message)
|
|
4362
|
+
});
|
|
4363
|
+
conflicts += 1;
|
|
4364
|
+
continue;
|
|
4365
|
+
}
|
|
4366
|
+
files.push({
|
|
4367
|
+
path: entry.path,
|
|
4368
|
+
exists: true,
|
|
4369
|
+
kind: entry.kind,
|
|
4370
|
+
ownership: "mixed",
|
|
4371
|
+
tags,
|
|
4372
|
+
action: "update-generated-region",
|
|
4373
|
+
notes: []
|
|
4374
|
+
});
|
|
4375
|
+
if (input.strategy === "regions") {
|
|
4376
|
+
wouldUpdateRegions += 1;
|
|
4377
|
+
}
|
|
4378
|
+
continue;
|
|
4379
|
+
}
|
|
4380
|
+
if (hasAnyRegionMarker(bytes)) {
|
|
4381
|
+
files.push({
|
|
4382
|
+
path: entry.path,
|
|
4383
|
+
exists: true,
|
|
4384
|
+
kind: entry.kind,
|
|
4385
|
+
ownership: "unknown",
|
|
4386
|
+
tags,
|
|
4387
|
+
action: "refuse-conflict",
|
|
4388
|
+
notes: ["partial region markers; manual repair required"]
|
|
4389
|
+
});
|
|
4390
|
+
conflicts += 1;
|
|
4391
|
+
continue;
|
|
4392
|
+
}
|
|
4393
|
+
if (hasLegacyGeneratedMarker(bytes)) {
|
|
4394
|
+
tags.push("generated-looking");
|
|
4395
|
+
}
|
|
4396
|
+
if (input.strategy === "regions") {
|
|
4397
|
+
files.push({
|
|
4398
|
+
path: entry.path,
|
|
4399
|
+
exists: true,
|
|
4400
|
+
kind: entry.kind,
|
|
4401
|
+
ownership: "unknown",
|
|
4402
|
+
tags,
|
|
4403
|
+
action: "insert-regions",
|
|
4404
|
+
notes: ["existing content will be preserved in manual region"]
|
|
4405
|
+
});
|
|
4406
|
+
wouldUpdateRegions += 1;
|
|
4407
|
+
} else {
|
|
4408
|
+
files.push({
|
|
4409
|
+
path: entry.path,
|
|
4410
|
+
exists: true,
|
|
4411
|
+
kind: entry.kind,
|
|
4412
|
+
ownership: "unknown",
|
|
4413
|
+
tags,
|
|
4414
|
+
action: "preserve",
|
|
4415
|
+
notes: [
|
|
4416
|
+
"Run init --import --strategy regions --write to adopt into mixed ownership"
|
|
4417
|
+
]
|
|
4418
|
+
});
|
|
4419
|
+
preservedManualFiles += 1;
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
for (const scan of PHASE_14_SCAN_DIRS) {
|
|
4423
|
+
const { files: discovered, refusals: scanRefusals } = await listFilesUnder(
|
|
4424
|
+
input.rootDir,
|
|
4425
|
+
scan.root,
|
|
4426
|
+
scan.recursive
|
|
4427
|
+
);
|
|
4428
|
+
for (const refusedPath of scanRefusals) {
|
|
4429
|
+
files.push({
|
|
4430
|
+
path: refusedPath,
|
|
4431
|
+
exists: true,
|
|
4432
|
+
kind: scan.kind,
|
|
4433
|
+
ownership: "unknown",
|
|
4434
|
+
tags: [],
|
|
4435
|
+
action: "refuse-conflict",
|
|
4436
|
+
notes: ["symlinked; Phase 14 refuses to follow file symlinks"]
|
|
4437
|
+
});
|
|
4438
|
+
conflicts += 1;
|
|
4439
|
+
}
|
|
4440
|
+
for (const relativePath of discovered) {
|
|
4441
|
+
if (!scan.fileFilter(relativePath)) continue;
|
|
4442
|
+
const read = await readRegionAwareFile(input.rootDir, relativePath);
|
|
4443
|
+
const tags = [];
|
|
4444
|
+
if (read.bytes) {
|
|
4445
|
+
const name = extractDeclaredName(
|
|
4446
|
+
Buffer.from(read.bytes),
|
|
4447
|
+
relativePath
|
|
4448
|
+
);
|
|
4449
|
+
if (name) {
|
|
4450
|
+
recordName(
|
|
4451
|
+
scan.kind === "workflow-skill" ? "workflow-skill" : "subagent",
|
|
4452
|
+
name,
|
|
4453
|
+
relativePath,
|
|
4454
|
+
collisionsByKey
|
|
4455
|
+
);
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
if (read.refused) {
|
|
4459
|
+
files.push({
|
|
4460
|
+
path: relativePath,
|
|
4461
|
+
exists: true,
|
|
4462
|
+
kind: scan.kind,
|
|
4463
|
+
ownership: "unknown",
|
|
4464
|
+
tags,
|
|
4465
|
+
action: "refuse-conflict",
|
|
4466
|
+
notes: ["symlinked; Phase 14 refuses to follow file symlinks"]
|
|
4467
|
+
});
|
|
4468
|
+
conflicts += 1;
|
|
4469
|
+
continue;
|
|
4470
|
+
}
|
|
4471
|
+
if (!read.bytes) continue;
|
|
4472
|
+
const lockOwnership = ownershipByPath.get(relativePath);
|
|
4473
|
+
if (lockOwnership === "generated-owned") {
|
|
4474
|
+
files.push({
|
|
4475
|
+
path: relativePath,
|
|
4476
|
+
exists: true,
|
|
4477
|
+
kind: scan.kind,
|
|
4478
|
+
ownership: "generated-owned",
|
|
4479
|
+
tags,
|
|
4480
|
+
action: "preserve",
|
|
4481
|
+
notes: [
|
|
4482
|
+
"lockfile-owned generated output; refresh via `agent-profile compile --write`"
|
|
4483
|
+
]
|
|
4484
|
+
});
|
|
4485
|
+
preservedManualFiles += 1;
|
|
4486
|
+
continue;
|
|
4487
|
+
}
|
|
4488
|
+
if (lockOwnership === "mixed") {
|
|
4489
|
+
files.push({
|
|
4490
|
+
path: relativePath,
|
|
4491
|
+
exists: true,
|
|
4492
|
+
kind: scan.kind,
|
|
4493
|
+
ownership: "mixed",
|
|
4494
|
+
tags,
|
|
4495
|
+
action: "update-generated-region",
|
|
4496
|
+
notes: [
|
|
4497
|
+
"lockfile-owned mixed file; generated region is updated on compile --write"
|
|
4498
|
+
]
|
|
4499
|
+
});
|
|
4500
|
+
if (input.strategy === "regions") {
|
|
4501
|
+
wouldUpdateRegions += 1;
|
|
4502
|
+
}
|
|
4503
|
+
continue;
|
|
4504
|
+
}
|
|
4505
|
+
files.push({
|
|
4506
|
+
path: relativePath,
|
|
4507
|
+
exists: true,
|
|
4508
|
+
kind: scan.kind,
|
|
4509
|
+
ownership: "manual-owned",
|
|
4510
|
+
tags,
|
|
4511
|
+
action: "preserve",
|
|
4512
|
+
notes: [
|
|
4513
|
+
scan.kind === "workflow-skill" ? "existing workflow skill; not adopted as generated output" : "existing subagent file; not adopted as generated output"
|
|
4514
|
+
]
|
|
4515
|
+
});
|
|
4516
|
+
preservedManualFiles += 1;
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4519
|
+
const gitignoreFindings = await getLocalRuntimeGitignoreFindings(
|
|
4520
|
+
input.rootDir
|
|
4521
|
+
);
|
|
4522
|
+
const gitignore = gitignoreFindings.map(
|
|
4523
|
+
(finding) => ({
|
|
4524
|
+
path: ".gitignore",
|
|
4525
|
+
line: finding.line,
|
|
4526
|
+
action: finding.action === "already-present" ? "already-present" : input.mode === "write" ? "would-add" : "suggest-add",
|
|
4527
|
+
reason: finding.reason
|
|
4528
|
+
})
|
|
4529
|
+
);
|
|
4530
|
+
const collisions = [];
|
|
4531
|
+
for (const [key, paths] of collisionsByKey) {
|
|
4532
|
+
if (paths.length < 2) continue;
|
|
4533
|
+
const [kindPart, name] = key.split("\0");
|
|
4534
|
+
collisions.push({
|
|
4535
|
+
name,
|
|
4536
|
+
kind: kindPart === "workflow-skill" ? "workflow-skill" : "subagent",
|
|
4537
|
+
paths: [...paths].sort()
|
|
4538
|
+
});
|
|
4539
|
+
for (const collidingPath of paths) {
|
|
4540
|
+
const row = files.find((f) => f.path === collidingPath);
|
|
4541
|
+
if (row && !row.notes.some((n) => n.startsWith("name collision"))) {
|
|
4542
|
+
const others = paths.filter((p) => p !== collidingPath).sort();
|
|
4543
|
+
row.notes.push(
|
|
4544
|
+
`name collision: "${name}" also declared by ${others.join(", ")}`
|
|
4545
|
+
);
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
collisions.sort((left, right) => {
|
|
4550
|
+
if (left.kind !== right.kind) {
|
|
4551
|
+
return left.kind === "workflow-skill" ? -1 : 1;
|
|
4552
|
+
}
|
|
4553
|
+
return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
4554
|
+
});
|
|
4555
|
+
return {
|
|
4556
|
+
command: "init",
|
|
4557
|
+
mode: input.mode,
|
|
4558
|
+
strategy: input.strategy,
|
|
4559
|
+
root: ".",
|
|
4560
|
+
profilePath: input.profilePath,
|
|
4561
|
+
stack: {
|
|
4562
|
+
languages: [...input.stack.languages].sort(),
|
|
4563
|
+
frameworks: [...input.stack.frameworks].sort(),
|
|
4564
|
+
packageManagers: [...input.stack.packageManagers].sort(),
|
|
4565
|
+
testing: [...input.stack.testing].sort()
|
|
4566
|
+
},
|
|
4567
|
+
files: files.sort(
|
|
4568
|
+
(left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0
|
|
4569
|
+
),
|
|
4570
|
+
gitignore,
|
|
4571
|
+
collisions,
|
|
4572
|
+
summary: {
|
|
4573
|
+
wouldCreateProfile: input.wouldCreateProfile,
|
|
4574
|
+
wouldUpdateRegions,
|
|
4575
|
+
preservedManualFiles,
|
|
4576
|
+
conflicts,
|
|
4577
|
+
nameCollisions: collisions.length
|
|
4578
|
+
}
|
|
4579
|
+
};
|
|
4580
|
+
}
|
|
4581
|
+
function recordName(kind, name, path7, map) {
|
|
4582
|
+
const key = `${kind}\0${name}`;
|
|
4583
|
+
const existing = map.get(key);
|
|
4584
|
+
if (existing) {
|
|
4585
|
+
existing.push(path7);
|
|
4586
|
+
} else {
|
|
4587
|
+
map.set(key, [path7]);
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
function extractDeclaredName(bytes, relativePath) {
|
|
4591
|
+
const text = bytes.toString("utf8");
|
|
4592
|
+
if (relativePath.endsWith(".toml")) {
|
|
4593
|
+
return findTomlTopLevelName(text);
|
|
4594
|
+
}
|
|
4595
|
+
if (relativePath.endsWith(".md")) {
|
|
4596
|
+
return findYamlFrontmatterName(text);
|
|
4597
|
+
}
|
|
4598
|
+
return void 0;
|
|
4599
|
+
}
|
|
4600
|
+
function findYamlFrontmatterName(text) {
|
|
4601
|
+
const lines = text.split(/\r?\n/u);
|
|
4602
|
+
if (lines.length === 0 || lines[0].trim() !== "---") return void 0;
|
|
4603
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
4604
|
+
const line = lines[index];
|
|
4605
|
+
const trimmed = line.trim();
|
|
4606
|
+
if (trimmed === "---") return void 0;
|
|
4607
|
+
if (!line.startsWith("name")) continue;
|
|
4608
|
+
const match = line.match(/^name\s*:\s*(.+?)\s*$/u);
|
|
4609
|
+
if (!match) continue;
|
|
4610
|
+
return stripYamlQuotes(match[1]);
|
|
4611
|
+
}
|
|
4612
|
+
return void 0;
|
|
4613
|
+
}
|
|
4614
|
+
function findTomlTopLevelName(text) {
|
|
4615
|
+
const lines = text.split(/\r?\n/u);
|
|
4616
|
+
for (const line of lines) {
|
|
4617
|
+
const trimmed = line.trim();
|
|
4618
|
+
if (trimmed.startsWith("#") || trimmed === "") continue;
|
|
4619
|
+
if (trimmed.startsWith("[")) break;
|
|
4620
|
+
const match = trimmed.match(/^name\s*=\s*(.+?)\s*$/u);
|
|
4621
|
+
if (!match) continue;
|
|
4622
|
+
return stripTomlQuotes(stripInlineTomlComment(match[1]));
|
|
4623
|
+
}
|
|
4624
|
+
return void 0;
|
|
4625
|
+
}
|
|
4626
|
+
function stripInlineTomlComment(value) {
|
|
4627
|
+
let inDoubleQuote = false;
|
|
4628
|
+
let inSingleQuote = false;
|
|
4629
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
4630
|
+
const char = value[index];
|
|
4631
|
+
if (char === '"' && !inSingleQuote) {
|
|
4632
|
+
if (inDoubleQuote && value[index - 1] === "\\") continue;
|
|
4633
|
+
inDoubleQuote = !inDoubleQuote;
|
|
4634
|
+
continue;
|
|
4635
|
+
}
|
|
4636
|
+
if (char === "'" && !inDoubleQuote) {
|
|
4637
|
+
inSingleQuote = !inSingleQuote;
|
|
4638
|
+
continue;
|
|
4639
|
+
}
|
|
4640
|
+
if (char === "#" && !inDoubleQuote && !inSingleQuote) {
|
|
4641
|
+
return value.slice(0, index).replace(/\s+$/u, "");
|
|
4642
|
+
}
|
|
4643
|
+
}
|
|
4644
|
+
return value.replace(/\s+$/u, "");
|
|
4645
|
+
}
|
|
4646
|
+
function stripYamlQuotes(value) {
|
|
4647
|
+
if (value.length >= 2) {
|
|
4648
|
+
const first = value[0];
|
|
4649
|
+
const last = value[value.length - 1];
|
|
4650
|
+
if (first === '"' && last === '"' || first === "'" && last === "'") {
|
|
4651
|
+
return value.slice(1, -1);
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
return value;
|
|
4655
|
+
}
|
|
4656
|
+
function stripTomlQuotes(value) {
|
|
4657
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
4658
|
+
return value.slice(1, -1);
|
|
4659
|
+
}
|
|
4660
|
+
if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
|
|
4661
|
+
return value.slice(1, -1);
|
|
4662
|
+
}
|
|
4663
|
+
return value;
|
|
4664
|
+
}
|
|
4665
|
+
async function readRegionAwareFile(rootDir, relativePath) {
|
|
4666
|
+
const safePath = safeOutputPath(relativePath);
|
|
4667
|
+
const absolutePath = path2.resolve(rootDir, ...safePath.split("/"));
|
|
4668
|
+
let stat;
|
|
4669
|
+
try {
|
|
4670
|
+
stat = await fsPromises2.lstat(absolutePath);
|
|
4671
|
+
} catch (error) {
|
|
4672
|
+
if (isNodeNotFound(error)) {
|
|
4673
|
+
return { refused: false, bytes: void 0 };
|
|
4674
|
+
}
|
|
4675
|
+
throw error;
|
|
4676
|
+
}
|
|
4677
|
+
if (stat.isSymbolicLink()) {
|
|
4678
|
+
return { refused: true };
|
|
4679
|
+
}
|
|
4680
|
+
if (!stat.isFile()) {
|
|
4681
|
+
return { refused: false, bytes: void 0 };
|
|
4682
|
+
}
|
|
4683
|
+
return { refused: false, bytes: await fsPromises2.readFile(absolutePath) };
|
|
4684
|
+
}
|
|
4685
|
+
async function listFilesUnder(rootDir, relativeRoot, recursive) {
|
|
4686
|
+
const files = [];
|
|
4687
|
+
const refusals = [];
|
|
4688
|
+
const rootStat = await lstatOptional2(path2.join(rootDir, relativeRoot));
|
|
4689
|
+
if (!rootStat) {
|
|
4690
|
+
return { files: [], refusals: [] };
|
|
4691
|
+
}
|
|
4692
|
+
if (rootStat.isSymbolicLink()) {
|
|
4693
|
+
return { files: [], refusals: [relativeRoot] };
|
|
4694
|
+
}
|
|
4695
|
+
if (!rootStat.isDirectory()) {
|
|
4696
|
+
return { files: [], refusals: [] };
|
|
4697
|
+
}
|
|
4698
|
+
await walk(rootDir, relativeRoot, recursive, files, refusals);
|
|
4699
|
+
return { files: files.sort(), refusals: refusals.sort() };
|
|
4700
|
+
}
|
|
4701
|
+
async function walk(rootDir, relativeRoot, recursive, out, refusals) {
|
|
4702
|
+
let entries;
|
|
4703
|
+
try {
|
|
4704
|
+
entries = await fsPromises2.readdir(path2.join(rootDir, relativeRoot), {
|
|
4705
|
+
withFileTypes: true
|
|
4706
|
+
});
|
|
4707
|
+
} catch (error) {
|
|
4708
|
+
if (isNodeNotFound(error)) return;
|
|
4709
|
+
throw error;
|
|
4710
|
+
}
|
|
4711
|
+
for (const entry of entries) {
|
|
4712
|
+
const child = `${relativeRoot}/${entry.name}`;
|
|
4713
|
+
if (entry.isSymbolicLink()) {
|
|
4714
|
+
refusals.push(child);
|
|
4715
|
+
continue;
|
|
4716
|
+
}
|
|
4717
|
+
if (entry.isDirectory() && recursive) {
|
|
4718
|
+
await walk(rootDir, child, recursive, out, refusals);
|
|
4719
|
+
continue;
|
|
4720
|
+
}
|
|
4721
|
+
if (entry.isFile()) {
|
|
4722
|
+
out.push(child);
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
async function lstatOptional2(absolutePath) {
|
|
4727
|
+
try {
|
|
4728
|
+
return await fsPromises2.lstat(absolutePath);
|
|
4729
|
+
} catch (error) {
|
|
4730
|
+
if (isNodeNotFound(error)) return void 0;
|
|
4731
|
+
throw error;
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
async function readOptionalBytes(rootDir, relativePath) {
|
|
4735
|
+
const result = await readRegionAwareFile(rootDir, relativePath);
|
|
4736
|
+
if (result.refused) return void 0;
|
|
4737
|
+
return result.bytes;
|
|
4738
|
+
}
|
|
4739
|
+
async function readOptionalText(rootDir, relativePath) {
|
|
4740
|
+
const bytes = await readOptionalBytes(rootDir, relativePath);
|
|
4741
|
+
if (!bytes) return void 0;
|
|
4742
|
+
return Buffer.from(bytes).toString("utf8");
|
|
4743
|
+
}
|
|
4744
|
+
async function readLockfileForRegions(rootDir) {
|
|
4745
|
+
const bytes = await readOptionalBytes(rootDir, "ai-profile.lock");
|
|
4746
|
+
if (!bytes) return void 0;
|
|
4747
|
+
const result = validateLockfileText(Buffer.from(bytes).toString("utf8"));
|
|
4748
|
+
if (!result.ok) return void 0;
|
|
4749
|
+
return toLockfileV2View(result.lockfile);
|
|
4750
|
+
}
|
|
4751
|
+
async function getLocalRuntimeGitignoreFindings(rootDir) {
|
|
4752
|
+
const gitignore = await readOptionalText(rootDir, ".gitignore").catch(
|
|
4753
|
+
() => void 0
|
|
4754
|
+
);
|
|
4755
|
+
const lines = (gitignore ?? "").split(/\r?\n/u).map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("#"));
|
|
4756
|
+
const findings = [];
|
|
4757
|
+
for (const recommended of RECOMMENDED_IGNORE_LINES) {
|
|
4758
|
+
const present = lines.some((line) => {
|
|
4759
|
+
const a = line.replace(/^\//u, "").replace(/\/$/u, "");
|
|
4760
|
+
const b = recommended.replace(/\/$/u, "");
|
|
4761
|
+
return a === b;
|
|
4762
|
+
});
|
|
4763
|
+
findings.push({
|
|
4764
|
+
line: recommended,
|
|
4765
|
+
action: present ? "already-present" : "would-add",
|
|
4766
|
+
reason: "local runtime or machine-specific path"
|
|
4767
|
+
});
|
|
4768
|
+
}
|
|
4769
|
+
return findings;
|
|
4770
|
+
}
|
|
4771
|
+
function containsAbsolutePathLiteral(bytes) {
|
|
4772
|
+
const text = bytes.toString("utf8");
|
|
4773
|
+
return /[A-Z]:\\\\|"\/[A-Za-z]/u.test(text);
|
|
4774
|
+
}
|
|
4775
|
+
function isNodeNotFound(error) {
|
|
4776
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
4777
|
+
}
|
|
4778
|
+
var REGION_AWARE_ROOT_PATHS = ["AGENTS.md", "CLAUDE.md"];
|
|
4779
|
+
async function planRootInstructionsAdoption(rootDir, generatedBytesByPath = /* @__PURE__ */ new Map()) {
|
|
4780
|
+
const results = [];
|
|
4781
|
+
for (const relativePath of REGION_AWARE_ROOT_PATHS) {
|
|
4782
|
+
const read = await readRegionAwareFile(rootDir, relativePath);
|
|
4783
|
+
if (read.refused) {
|
|
4784
|
+
results.push({ ok: false, path: relativePath, reason: "symlink" });
|
|
4785
|
+
continue;
|
|
4786
|
+
}
|
|
4787
|
+
const existing = read.bytes;
|
|
4788
|
+
if (!existing) {
|
|
4789
|
+
results.push({ ok: false, path: relativePath, reason: "missing-file" });
|
|
4790
|
+
continue;
|
|
4791
|
+
}
|
|
4792
|
+
const compiled = generatedBytesByPath.get(relativePath);
|
|
4793
|
+
if (!compiled) {
|
|
4794
|
+
results.push({
|
|
4795
|
+
ok: false,
|
|
4796
|
+
path: relativePath,
|
|
4797
|
+
reason: "missing-generated-bytes"
|
|
4798
|
+
});
|
|
4799
|
+
continue;
|
|
4800
|
+
}
|
|
4801
|
+
const generatedInner = Buffer.from(compiled);
|
|
4802
|
+
const existingBuffer = Buffer.from(existing);
|
|
4803
|
+
if (hasAllRegionMarkers(existingBuffer)) {
|
|
4804
|
+
const updated = replaceGeneratedRegion(existingBuffer, generatedInner);
|
|
4805
|
+
if (updated) {
|
|
4806
|
+
results.push({ ok: true, path: relativePath, bytes: updated });
|
|
4807
|
+
} else {
|
|
4808
|
+
results.push({
|
|
4809
|
+
ok: false,
|
|
4810
|
+
path: relativePath,
|
|
4811
|
+
reason: "duplicate-markers"
|
|
4812
|
+
});
|
|
4813
|
+
}
|
|
4814
|
+
continue;
|
|
4815
|
+
}
|
|
4816
|
+
if (hasAnyRegionMarker(existingBuffer)) {
|
|
4817
|
+
results.push({
|
|
4818
|
+
ok: false,
|
|
4819
|
+
path: relativePath,
|
|
4820
|
+
reason: "partial-markers"
|
|
4821
|
+
});
|
|
4822
|
+
continue;
|
|
4823
|
+
}
|
|
4824
|
+
const mixed = serializeMixedFile({
|
|
4825
|
+
generatedInner,
|
|
4826
|
+
manualInner: existingBuffer
|
|
4827
|
+
});
|
|
4828
|
+
results.push({ ok: true, path: relativePath, bytes: mixed });
|
|
4829
|
+
}
|
|
4830
|
+
return results;
|
|
4831
|
+
}
|
|
4832
|
+
|
|
4833
|
+
// ../../packages/scanner/src/stack.ts
|
|
4834
|
+
import fsPromises3 from "node:fs/promises";
|
|
4835
|
+
import path3 from "node:path";
|
|
4836
|
+
import { parse as parseYaml } from "yaml";
|
|
4837
|
+
var EMPTY_STACK = {
|
|
4838
|
+
languages: [],
|
|
4839
|
+
frameworks: [],
|
|
4840
|
+
packageManagers: [],
|
|
4841
|
+
testing: []
|
|
4842
|
+
};
|
|
4843
|
+
var VITE_CONFIGS = [
|
|
4844
|
+
"vite.config.js",
|
|
4845
|
+
"vite.config.mjs",
|
|
4846
|
+
"vite.config.cjs",
|
|
4847
|
+
"vite.config.ts",
|
|
4848
|
+
"vite.config.mts",
|
|
4849
|
+
"vite.config.cts"
|
|
4850
|
+
];
|
|
4851
|
+
var SVELTE_CONFIGS = [
|
|
4852
|
+
"svelte.config.js",
|
|
4853
|
+
"svelte.config.mjs",
|
|
4854
|
+
"svelte.config.cjs",
|
|
3548
4855
|
"svelte.config.ts"
|
|
3549
4856
|
];
|
|
3550
4857
|
var PLAYWRIGHT_CONFIGS = [
|
|
@@ -3556,7 +4863,7 @@ var PLAYWRIGHT_CONFIGS = [
|
|
|
3556
4863
|
"playwright.config.cts"
|
|
3557
4864
|
];
|
|
3558
4865
|
async function detectStack(rootDir) {
|
|
3559
|
-
const rootPath =
|
|
4866
|
+
const rootPath = path3.resolve(rootDir);
|
|
3560
4867
|
const stack = cloneStack(EMPTY_STACK);
|
|
3561
4868
|
const warnings = [];
|
|
3562
4869
|
if (await fileExists(rootPath, "tsconfig.json")) {
|
|
@@ -3599,7 +4906,7 @@ async function detectPackageJson(rootPath, stack, warnings) {
|
|
|
3599
4906
|
let value;
|
|
3600
4907
|
try {
|
|
3601
4908
|
value = JSON.parse(
|
|
3602
|
-
await
|
|
4909
|
+
await fsPromises3.readFile(path3.join(rootPath, "package.json"), "utf8")
|
|
3603
4910
|
);
|
|
3604
4911
|
} catch {
|
|
3605
4912
|
warnings.push({
|
|
@@ -3686,7 +4993,7 @@ async function detectPubspecYaml(rootPath, stack, warnings) {
|
|
|
3686
4993
|
let value;
|
|
3687
4994
|
try {
|
|
3688
4995
|
value = parseYaml(
|
|
3689
|
-
await
|
|
4996
|
+
await fsPromises3.readFile(path3.join(rootPath, "pubspec.yaml"), "utf8")
|
|
3690
4997
|
);
|
|
3691
4998
|
} catch {
|
|
3692
4999
|
warnings.push({
|
|
@@ -3762,8 +5069,8 @@ function hasAny(keys, allowlist) {
|
|
|
3762
5069
|
return false;
|
|
3763
5070
|
}
|
|
3764
5071
|
async function detectJavaMetadata(rootPath, relativePath, stack) {
|
|
3765
|
-
const source = await
|
|
3766
|
-
|
|
5072
|
+
const source = await fsPromises3.readFile(
|
|
5073
|
+
path3.join(rootPath, relativePath),
|
|
3767
5074
|
"utf8"
|
|
3768
5075
|
);
|
|
3769
5076
|
if (source.includes("spring-boot-starter")) {
|
|
@@ -3783,7 +5090,7 @@ async function anyFileExists(rootPath, relativePaths) {
|
|
|
3783
5090
|
}
|
|
3784
5091
|
async function fileExists(rootPath, relativePath) {
|
|
3785
5092
|
try {
|
|
3786
|
-
const stats = await
|
|
5093
|
+
const stats = await fsPromises3.lstat(path3.join(rootPath, relativePath));
|
|
3787
5094
|
return stats.isFile();
|
|
3788
5095
|
} catch (error) {
|
|
3789
5096
|
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
@@ -3855,7 +5162,7 @@ function isNodeError2(error) {
|
|
|
3855
5162
|
|
|
3856
5163
|
// ../../packages/scanner/src/import-artifacts.ts
|
|
3857
5164
|
import { lstat, readdir, readFile, realpath } from "node:fs/promises";
|
|
3858
|
-
import
|
|
5165
|
+
import path4 from "node:path";
|
|
3859
5166
|
var GENERATED_MARKDOWN_MARKER = "<!-- Generated by Agent Profile Compiler. Do not edit by hand. -->";
|
|
3860
5167
|
var SUPPORTED_CONFIG_PATHS = [
|
|
3861
5168
|
"AGENTS.md",
|
|
@@ -3866,7 +5173,7 @@ var SUPPORTED_CONFIG_PATHS = [
|
|
|
3866
5173
|
".mcp.json"
|
|
3867
5174
|
];
|
|
3868
5175
|
async function analyzeExistingArtifacts(rootDir) {
|
|
3869
|
-
const rootPath = await realpath(
|
|
5176
|
+
const rootPath = await realpath(path4.resolve(rootDir));
|
|
3870
5177
|
const findings = [];
|
|
3871
5178
|
const clients = {
|
|
3872
5179
|
tabnine: false,
|
|
@@ -3878,7 +5185,7 @@ async function analyzeExistingArtifacts(rootDir) {
|
|
|
3878
5185
|
...await collectTabnineGuidelines(rootPath, findings)
|
|
3879
5186
|
].sort(compareText3);
|
|
3880
5187
|
for (const relativePath of paths) {
|
|
3881
|
-
const source = await
|
|
5188
|
+
const source = await readOptionalText2(rootPath, relativePath, findings);
|
|
3882
5189
|
if (source === void 0) {
|
|
3883
5190
|
continue;
|
|
3884
5191
|
}
|
|
@@ -3932,7 +5239,7 @@ async function collectTabnineGuidelines(rootPath, findings) {
|
|
|
3932
5239
|
}
|
|
3933
5240
|
return entries.filter((entry) => entry.endsWith(".md")).map((entry) => `.tabnine/guidelines/${entry}`);
|
|
3934
5241
|
}
|
|
3935
|
-
async function
|
|
5242
|
+
async function readOptionalText2(rootPath, relativePath, findings) {
|
|
3936
5243
|
const artifactPath = resolveArtifactPath(rootPath, relativePath);
|
|
3937
5244
|
try {
|
|
3938
5245
|
const stats = await lstat(artifactPath);
|
|
@@ -3957,7 +5264,7 @@ async function readOptionalText(rootPath, relativePath, findings) {
|
|
|
3957
5264
|
}
|
|
3958
5265
|
}
|
|
3959
5266
|
function resolveArtifactPath(rootPath, relativePath) {
|
|
3960
|
-
return
|
|
5267
|
+
return path4.join(rootPath, ...relativePath.split("/"));
|
|
3961
5268
|
}
|
|
3962
5269
|
function unsafeSymlinkFinding(relativePath) {
|
|
3963
5270
|
return {
|
|
@@ -4028,8 +5335,8 @@ function compareText3(left, right) {
|
|
|
4028
5335
|
return 0;
|
|
4029
5336
|
}
|
|
4030
5337
|
function isContainedBy2(rootPath, candidatePath) {
|
|
4031
|
-
const relative =
|
|
4032
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
5338
|
+
const relative = path4.relative(rootPath, candidatePath);
|
|
5339
|
+
return relative === "" || !relative.startsWith("..") && !path4.isAbsolute(relative);
|
|
4033
5340
|
}
|
|
4034
5341
|
function getRecord2(value) {
|
|
4035
5342
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
@@ -4042,8 +5349,8 @@ function isNodeError3(error) {
|
|
|
4042
5349
|
}
|
|
4043
5350
|
|
|
4044
5351
|
// ../../packages/doctor/src/doctor.ts
|
|
4045
|
-
import { readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
|
|
4046
|
-
import
|
|
5352
|
+
import { lstat as lstat2, readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
|
|
5353
|
+
import path5 from "node:path";
|
|
4047
5354
|
var PROFILE_PATH = "ai-profile.yaml";
|
|
4048
5355
|
var LOCKFILE_PATH = "ai-profile.lock";
|
|
4049
5356
|
var SKILL_ROOTS = [".agents/skills", ".claude/skills"];
|
|
@@ -4053,7 +5360,7 @@ var SEVERITY_ORDER = {
|
|
|
4053
5360
|
info: 2
|
|
4054
5361
|
};
|
|
4055
5362
|
async function runDoctor(request = {}) {
|
|
4056
|
-
const rootDir =
|
|
5363
|
+
const rootDir = path5.resolve(request.rootDir ?? ".");
|
|
4057
5364
|
const issues = [];
|
|
4058
5365
|
const profileBytes = await readKnownFile(rootDir, PROFILE_PATH);
|
|
4059
5366
|
if (!profileBytes.ok) {
|
|
@@ -4113,13 +5420,15 @@ async function runDoctor(request = {}) {
|
|
|
4113
5420
|
await checkSemanticWarnings(rootDir, compileResult.files, issues);
|
|
4114
5421
|
await checkGitignoreSecretHygiene(rootDir, issues);
|
|
4115
5422
|
const lockfile = await readAndValidateLockfile(rootDir, issues);
|
|
4116
|
-
|
|
5423
|
+
const lockfileV2 = lockfile ? toLockfileV2View(lockfile) : void 0;
|
|
5424
|
+
await checkRegionFiles(rootDir, compileResult.files, lockfileV2, issues);
|
|
5425
|
+
if (lockfileV2) {
|
|
4117
5426
|
await checkLockfileDrift({
|
|
4118
5427
|
rootDir,
|
|
4119
5428
|
profileBytes: profileBytes.bytes,
|
|
4120
5429
|
templates: compileResult.templates,
|
|
4121
5430
|
files: compileResult.files,
|
|
4122
|
-
lockfile,
|
|
5431
|
+
lockfile: lockfileV2,
|
|
4123
5432
|
issues
|
|
4124
5433
|
});
|
|
4125
5434
|
}
|
|
@@ -4129,9 +5438,16 @@ async function runDoctor(request = {}) {
|
|
|
4129
5438
|
profile: profileResult.profile,
|
|
4130
5439
|
effective: deriveEffectivePermissions(profileResult.profile),
|
|
4131
5440
|
generatedFiles: compileResult.files,
|
|
4132
|
-
lockfile,
|
|
5441
|
+
lockfile: lockfileV2,
|
|
4133
5442
|
issues
|
|
4134
5443
|
});
|
|
5444
|
+
await checkLocalRuntimeGitignore(rootDir, issues);
|
|
5445
|
+
await checkForeignSkillAndSubagentCollisions(
|
|
5446
|
+
rootDir,
|
|
5447
|
+
compileResult.files,
|
|
5448
|
+
lockfileV2,
|
|
5449
|
+
issues
|
|
5450
|
+
);
|
|
4135
5451
|
return toResult(issues);
|
|
4136
5452
|
}
|
|
4137
5453
|
async function checkGeneratedArtifactsExist(rootDir, files, issues) {
|
|
@@ -4358,7 +5674,8 @@ async function checkLockfileDrift(input) {
|
|
|
4358
5674
|
const expectedLockfileText = createLockfileFile({
|
|
4359
5675
|
profileBytes: input.profileBytes,
|
|
4360
5676
|
templates: input.templates,
|
|
4361
|
-
files: input.files
|
|
5677
|
+
files: input.files,
|
|
5678
|
+
mixedOutputs: collectMixedOutputDescriptorsFromLockfile(input.lockfile)
|
|
4362
5679
|
});
|
|
4363
5680
|
const expectedLockfile = JSON.parse(
|
|
4364
5681
|
Buffer.from(expectedLockfileText.bytes).toString("utf8")
|
|
@@ -4391,23 +5708,51 @@ async function checkLockfileDrift(input) {
|
|
|
4391
5708
|
input.lockfile.outputs.map((output) => [outputKey(output), output])
|
|
4392
5709
|
);
|
|
4393
5710
|
for (const output of expectedLockfile.outputs) {
|
|
5711
|
+
if (output.ownership === "manual-owned") {
|
|
5712
|
+
continue;
|
|
5713
|
+
}
|
|
4394
5714
|
const lockOutput = lockOutputsByKey.get(outputKey(output));
|
|
4395
|
-
const expectedHash = lockOutput?.sha256 ?? output.sha256;
|
|
4396
5715
|
const current = await readKnownFile(input.rootDir, output.path);
|
|
4397
5716
|
if (!current.ok) {
|
|
5717
|
+
const isLocalRuntime = LOCAL_RUNTIME_IGNORE_PATHS.some(
|
|
5718
|
+
(line) => line.replace(/\/$/u, "") === output.path
|
|
5719
|
+
);
|
|
4398
5720
|
input.issues.push(
|
|
4399
5721
|
issue(
|
|
4400
5722
|
"LINT-LOCK-006",
|
|
4401
|
-
"error",
|
|
5723
|
+
isLocalRuntime ? "warning" : "error",
|
|
4402
5724
|
output.path,
|
|
4403
5725
|
"generated file present",
|
|
4404
5726
|
"missing",
|
|
4405
|
-
`${output.path} is listed in ai-profile.lock but is missing.`,
|
|
4406
|
-
"Run the compiler after reviewing generated file changes."
|
|
5727
|
+
`${output.path} is listed in ai-profile.lock but is missing${isLocalRuntime ? " (local-runtime path; regenerated by compile --write)" : ""}.`,
|
|
5728
|
+
isLocalRuntime ? "Run agent-profile compile --write to materialize the local-runtime file; it is intentionally not committed." : "Run the compiler after reviewing generated file changes."
|
|
4407
5729
|
)
|
|
4408
5730
|
);
|
|
4409
5731
|
continue;
|
|
4410
5732
|
}
|
|
5733
|
+
if (output.ownership === "mixed") {
|
|
5734
|
+
const lockRegion = lockOutput && lockOutput.ownership === "mixed" ? lockOutput.regions[0] : output.regions[0];
|
|
5735
|
+
const expectedHash2 = lockRegion?.sha256 ?? output.regions[0]?.sha256 ?? "";
|
|
5736
|
+
const parsed = parseMixedFile(Buffer.from(current.bytes));
|
|
5737
|
+
if (!parsed.ok) {
|
|
5738
|
+
continue;
|
|
5739
|
+
}
|
|
5740
|
+
if (parsed.generatedInnerHash !== expectedHash2) {
|
|
5741
|
+
input.issues.push(
|
|
5742
|
+
issue(
|
|
5743
|
+
"LINT-REGION-004",
|
|
5744
|
+
"error",
|
|
5745
|
+
output.path,
|
|
5746
|
+
expectedHash2,
|
|
5747
|
+
parsed.generatedInnerHash,
|
|
5748
|
+
`${output.path} generated region hash differs from ai-profile.lock.`,
|
|
5749
|
+
"Run the compiler after reviewing the generated region diff."
|
|
5750
|
+
)
|
|
5751
|
+
);
|
|
5752
|
+
}
|
|
5753
|
+
continue;
|
|
5754
|
+
}
|
|
5755
|
+
const expectedHash = lockOutput && lockOutput.ownership === "generated-owned" ? lockOutput.sha256 : output.sha256;
|
|
4411
5756
|
const currentHash = sha256Hex(current.bytes);
|
|
4412
5757
|
if (currentHash !== expectedHash) {
|
|
4413
5758
|
input.issues.push(
|
|
@@ -4424,6 +5769,21 @@ async function checkLockfileDrift(input) {
|
|
|
4424
5769
|
}
|
|
4425
5770
|
}
|
|
4426
5771
|
}
|
|
5772
|
+
function collectMixedOutputDescriptorsFromLockfile(lockfile) {
|
|
5773
|
+
const result = [];
|
|
5774
|
+
for (const output of lockfile.outputs) {
|
|
5775
|
+
if (output.ownership !== "mixed") continue;
|
|
5776
|
+
const region = output.regions[0];
|
|
5777
|
+
if (!region) continue;
|
|
5778
|
+
result.push({
|
|
5779
|
+
path: output.path,
|
|
5780
|
+
target: region.target,
|
|
5781
|
+
templateId: region.templateId,
|
|
5782
|
+
regionHash: region.sha256
|
|
5783
|
+
});
|
|
5784
|
+
}
|
|
5785
|
+
return result;
|
|
5786
|
+
}
|
|
4427
5787
|
function compareTemplates2(actual, expected, issues) {
|
|
4428
5788
|
const actualMap = new Map(
|
|
4429
5789
|
actual.map((template) => [templateKey(template), template])
|
|
@@ -4500,14 +5860,16 @@ function compareOutputs(actual, expected, issues) {
|
|
|
4500
5860
|
);
|
|
4501
5861
|
continue;
|
|
4502
5862
|
}
|
|
4503
|
-
|
|
5863
|
+
const expectedSig = describeOutputSignature(expectedOutput);
|
|
5864
|
+
const actualSig = describeOutputSignature(actualOutput);
|
|
5865
|
+
if (expectedSig !== actualSig) {
|
|
4504
5866
|
issues.push(
|
|
4505
5867
|
issue(
|
|
4506
5868
|
"LINT-LOCK-005",
|
|
4507
5869
|
"error",
|
|
4508
5870
|
expectedOutput.path,
|
|
4509
|
-
|
|
4510
|
-
|
|
5871
|
+
expectedSig,
|
|
5872
|
+
actualSig,
|
|
4511
5873
|
`${expectedOutput.path} lockfile output metadata differs from current compiler output.`,
|
|
4512
5874
|
"Regenerate generated outputs and ai-profile.lock after reviewing changes."
|
|
4513
5875
|
)
|
|
@@ -4530,6 +5892,16 @@ function compareOutputs(actual, expected, issues) {
|
|
|
4530
5892
|
}
|
|
4531
5893
|
}
|
|
4532
5894
|
}
|
|
5895
|
+
function describeOutputSignature(output) {
|
|
5896
|
+
if (output.ownership === "mixed") {
|
|
5897
|
+
const region = output.regions[0];
|
|
5898
|
+
return `mixed/${output.templateId}/${region?.sha256 ?? ""}`;
|
|
5899
|
+
}
|
|
5900
|
+
if (output.ownership === "manual-owned") {
|
|
5901
|
+
return "manual-owned/manual/-";
|
|
5902
|
+
}
|
|
5903
|
+
return `generated/${output.templateId}/${output.sha256}`;
|
|
5904
|
+
}
|
|
4533
5905
|
async function checkPermissionPosture(rootDir, profile, issues) {
|
|
4534
5906
|
const safety = normalizeSafety(profile);
|
|
4535
5907
|
const effective = deriveEffectivePermissions(profile);
|
|
@@ -5281,42 +6653,42 @@ function checkClaudeSubagentFile(relativePath, text, effective, issues) {
|
|
|
5281
6653
|
);
|
|
5282
6654
|
}
|
|
5283
6655
|
const tools = parseClaudeToolList(frontmatter.tools);
|
|
5284
|
-
if (tools.includes("Bash") && effective.shell.run
|
|
6656
|
+
if (tools.includes("Bash") && effective.shell.run === "deny") {
|
|
5285
6657
|
issues.push(
|
|
5286
6658
|
issue(
|
|
5287
6659
|
"LINT-SUBAGENT-001",
|
|
5288
6660
|
"error",
|
|
5289
6661
|
relativePath,
|
|
5290
|
-
"tools narrower than effectivePermissions.shell.run",
|
|
6662
|
+
"tools narrower than effectivePermissions.shell.run=deny",
|
|
5291
6663
|
"Bash tool granted",
|
|
5292
|
-
`${relativePath} grants the Bash tool while effectivePermissions.shell.run is
|
|
5293
|
-
"Remove Bash from the Claude subagent tools list or
|
|
6664
|
+
`${relativePath} grants the Bash tool while effectivePermissions.shell.run is deny.`,
|
|
6665
|
+
"Remove Bash from the Claude subagent tools list or relax shell.run from deny."
|
|
5294
6666
|
)
|
|
5295
6667
|
);
|
|
5296
6668
|
}
|
|
5297
|
-
if (tools.some((tool) => CLAUDE_WRITE_TOOLS.has(tool)) && effective.filesystem.write
|
|
6669
|
+
if (tools.some((tool) => CLAUDE_WRITE_TOOLS.has(tool)) && effective.filesystem.write === "deny") {
|
|
5298
6670
|
issues.push(
|
|
5299
6671
|
issue(
|
|
5300
6672
|
"LINT-SUBAGENT-001",
|
|
5301
6673
|
"error",
|
|
5302
6674
|
relativePath,
|
|
5303
|
-
"tools narrower than effectivePermissions.filesystem.write",
|
|
6675
|
+
"tools narrower than effectivePermissions.filesystem.write=deny",
|
|
5304
6676
|
"Edit/Write tool granted",
|
|
5305
|
-
`${relativePath} grants write tools while effectivePermissions.filesystem.write is
|
|
5306
|
-
"Remove Edit/Write from the Claude subagent tools list or
|
|
6677
|
+
`${relativePath} grants write tools while effectivePermissions.filesystem.write is deny.`,
|
|
6678
|
+
"Remove Edit/Write from the Claude subagent tools list or relax filesystem.write from deny."
|
|
5307
6679
|
)
|
|
5308
6680
|
);
|
|
5309
6681
|
}
|
|
5310
|
-
if (tools.includes("WebFetch") && effective.network.external
|
|
6682
|
+
if (tools.includes("WebFetch") && effective.network.external === "deny") {
|
|
5311
6683
|
issues.push(
|
|
5312
6684
|
issue(
|
|
5313
6685
|
"LINT-SUBAGENT-001",
|
|
5314
6686
|
"error",
|
|
5315
6687
|
relativePath,
|
|
5316
|
-
"tools narrower than effectivePermissions.network.external",
|
|
6688
|
+
"tools narrower than effectivePermissions.network.external=deny",
|
|
5317
6689
|
"WebFetch tool granted",
|
|
5318
|
-
`${relativePath} grants the WebFetch tool while effectivePermissions.network.external is
|
|
5319
|
-
"Remove WebFetch from the Claude subagent tools list or
|
|
6690
|
+
`${relativePath} grants the WebFetch tool while effectivePermissions.network.external is deny.`,
|
|
6691
|
+
"Remove WebFetch from the Claude subagent tools list or relax network.external from deny."
|
|
5320
6692
|
)
|
|
5321
6693
|
);
|
|
5322
6694
|
}
|
|
@@ -5419,7 +6791,7 @@ function parseTabnineToolList(text, inlineValue) {
|
|
|
5419
6791
|
async function collectSubagentFilesUnder(rootDir, relativeRoot) {
|
|
5420
6792
|
let entries;
|
|
5421
6793
|
try {
|
|
5422
|
-
entries = await readdir2(
|
|
6794
|
+
entries = await readdir2(path5.join(rootDir, relativeRoot), {
|
|
5423
6795
|
withFileTypes: true
|
|
5424
6796
|
});
|
|
5425
6797
|
} catch (error) {
|
|
@@ -5445,7 +6817,7 @@ async function collectSkillFiles(rootDir) {
|
|
|
5445
6817
|
async function collectSkillFilesUnder(rootDir, relativeRoot, files) {
|
|
5446
6818
|
let entries;
|
|
5447
6819
|
try {
|
|
5448
|
-
entries = await readdir2(
|
|
6820
|
+
entries = await readdir2(path5.join(rootDir, relativeRoot), {
|
|
5449
6821
|
withFileTypes: true
|
|
5450
6822
|
});
|
|
5451
6823
|
} catch (error) {
|
|
@@ -5468,10 +6840,22 @@ async function readKnownFile(rootDir, relativePath) {
|
|
|
5468
6840
|
if (safePath === ".env" || safePath.startsWith(".env.")) {
|
|
5469
6841
|
return { ok: false };
|
|
5470
6842
|
}
|
|
6843
|
+
const absolutePath = path5.join(rootDir, safePath);
|
|
6844
|
+
try {
|
|
6845
|
+
const stat = await lstat2(absolutePath);
|
|
6846
|
+
if (stat.isSymbolicLink()) {
|
|
6847
|
+
return { ok: false };
|
|
6848
|
+
}
|
|
6849
|
+
} catch (error) {
|
|
6850
|
+
if (isNodeError4(error) && error.code === "ENOENT") {
|
|
6851
|
+
return { ok: false };
|
|
6852
|
+
}
|
|
6853
|
+
throw error;
|
|
6854
|
+
}
|
|
5471
6855
|
try {
|
|
5472
6856
|
return {
|
|
5473
6857
|
ok: true,
|
|
5474
|
-
bytes: await readFile2(
|
|
6858
|
+
bytes: await readFile2(absolutePath)
|
|
5475
6859
|
};
|
|
5476
6860
|
} catch (error) {
|
|
5477
6861
|
if (isNodeError4(error) && error.code === "ENOENT") {
|
|
@@ -5646,19 +7030,662 @@ function decodeUtf8(bytes) {
|
|
|
5646
7030
|
function isNodeError4(error) {
|
|
5647
7031
|
return error instanceof Error && "code" in error;
|
|
5648
7032
|
}
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
7033
|
+
var REGION_FILE_PATHS = ["AGENTS.md", "CLAUDE.md"];
|
|
7034
|
+
var LOCAL_RUNTIME_IGNORE_PATHS = [
|
|
7035
|
+
".cce/",
|
|
7036
|
+
".mcp.json",
|
|
7037
|
+
".claude/settings.local.json",
|
|
7038
|
+
".claude/worktrees/",
|
|
7039
|
+
".codex/config.toml",
|
|
7040
|
+
".codex/hooks.json"
|
|
7041
|
+
];
|
|
7042
|
+
async function checkRegionFiles(rootDir, generatedFiles, lockfile, issues) {
|
|
7043
|
+
const ownershipByPath = new Map(
|
|
7044
|
+
(lockfile?.outputs ?? []).map((output) => [output.path, output.ownership])
|
|
7045
|
+
);
|
|
7046
|
+
for (const relativePath of REGION_FILE_PATHS) {
|
|
7047
|
+
const current = await readKnownFile(rootDir, relativePath);
|
|
7048
|
+
if (!current.ok) continue;
|
|
7049
|
+
const bytes = Buffer.from(current.bytes);
|
|
7050
|
+
const ownership = ownershipByPath.get(relativePath);
|
|
7051
|
+
const allMarkers = hasAllRegionMarkers(bytes);
|
|
7052
|
+
const anyMarkers = hasAnyRegionMarker(bytes);
|
|
7053
|
+
const generatedFile = generatedFiles.find(
|
|
7054
|
+
(file) => file.path === relativePath
|
|
7055
|
+
);
|
|
7056
|
+
if (ownership === "mixed") {
|
|
7057
|
+
const parsed = parseMixedFile(bytes);
|
|
7058
|
+
if (!parsed.ok) {
|
|
7059
|
+
const code = parsed.issues.some(
|
|
7060
|
+
(item) => item.code === "duplicate-markers"
|
|
7061
|
+
) ? "LINT-REGION-002" : "LINT-REGION-001";
|
|
7062
|
+
issues.push(
|
|
7063
|
+
issue(
|
|
7064
|
+
code,
|
|
7065
|
+
"error",
|
|
7066
|
+
relativePath,
|
|
7067
|
+
"valid generated/manual region markers",
|
|
7068
|
+
"invalid region markers",
|
|
7069
|
+
`${relativePath} region markers are not valid for a mixed-ownership file.`,
|
|
7070
|
+
"Move or remove the file and re-run init --import --strategy regions --write."
|
|
7071
|
+
)
|
|
7072
|
+
);
|
|
7073
|
+
continue;
|
|
7074
|
+
}
|
|
7075
|
+
if (!parsed.generatedInner.toString("utf8").includes(REGION_PRECEDENCE_TEXT)) {
|
|
7076
|
+
issues.push(
|
|
7077
|
+
issue(
|
|
7078
|
+
"LINT-REGION-003",
|
|
7079
|
+
"warning",
|
|
7080
|
+
relativePath,
|
|
7081
|
+
"required instruction precedence text in generated region",
|
|
7082
|
+
"missing precedence text",
|
|
7083
|
+
`${relativePath} generated region is missing the required precedence sentence.`,
|
|
7084
|
+
"Re-run compile --write to refresh the generated region."
|
|
7085
|
+
)
|
|
7086
|
+
);
|
|
7087
|
+
}
|
|
7088
|
+
continue;
|
|
7089
|
+
}
|
|
7090
|
+
if (anyMarkers && !allMarkers) {
|
|
7091
|
+
issues.push(
|
|
7092
|
+
issue(
|
|
7093
|
+
"LINT-REGION-001",
|
|
7094
|
+
"error",
|
|
7095
|
+
relativePath,
|
|
7096
|
+
"valid generated/manual region markers",
|
|
7097
|
+
"partial region markers",
|
|
7098
|
+
`${relativePath} contains partial region markers.`,
|
|
7099
|
+
"Move or remove the file and re-run init --import --strategy regions --write; the compiler will not auto-repair markers."
|
|
7100
|
+
)
|
|
7101
|
+
);
|
|
7102
|
+
continue;
|
|
7103
|
+
}
|
|
7104
|
+
if (allMarkers) {
|
|
7105
|
+
const parsed = parseMixedFile(bytes);
|
|
7106
|
+
if (!parsed.ok) {
|
|
7107
|
+
const code = parsed.issues.some(
|
|
7108
|
+
(item) => item.code === "duplicate-markers"
|
|
7109
|
+
) ? "LINT-REGION-002" : "LINT-REGION-001";
|
|
7110
|
+
issues.push(
|
|
7111
|
+
issue(
|
|
7112
|
+
code,
|
|
7113
|
+
"error",
|
|
7114
|
+
relativePath,
|
|
7115
|
+
"valid generated/manual region markers",
|
|
7116
|
+
"invalid region markers",
|
|
7117
|
+
`${relativePath} region markers are not valid.`,
|
|
7118
|
+
"Move or remove the file and re-run init --import --strategy regions --write."
|
|
7119
|
+
)
|
|
7120
|
+
);
|
|
7121
|
+
}
|
|
7122
|
+
continue;
|
|
7123
|
+
}
|
|
7124
|
+
if (generatedFile && ownership !== "generated-owned" && !allMarkers && hasLegacyGeneratedMarker(bytes)) {
|
|
7125
|
+
issues.push(
|
|
7126
|
+
issue(
|
|
7127
|
+
"LINT-OWN-002",
|
|
7128
|
+
"warning",
|
|
7129
|
+
relativePath,
|
|
7130
|
+
"lockfile-owned generated file",
|
|
7131
|
+
"generated-looking file without lockfile ownership",
|
|
7132
|
+
`${relativePath} contains the legacy generated marker but is not lockfile-owned.`,
|
|
7133
|
+
"Run init --import --strategy regions --write to adopt the file into mixed ownership, or remove and re-run compile --write."
|
|
7134
|
+
)
|
|
7135
|
+
);
|
|
7136
|
+
continue;
|
|
7137
|
+
}
|
|
7138
|
+
if (generatedFile && ownership !== "generated-owned" && !allMarkers) {
|
|
7139
|
+
issues.push(
|
|
7140
|
+
issue(
|
|
7141
|
+
"LINT-OWN-001",
|
|
7142
|
+
"error",
|
|
7143
|
+
relativePath,
|
|
7144
|
+
"lockfile-owned or mixed ownership",
|
|
7145
|
+
"unknown ownership conflicts with generated output",
|
|
7146
|
+
`${relativePath} exists but ownership cannot be proven; the compiler will refuse to write it.`,
|
|
7147
|
+
"Run init --import --strategy regions --write to adopt the file into mixed ownership."
|
|
7148
|
+
)
|
|
7149
|
+
);
|
|
7150
|
+
}
|
|
7151
|
+
}
|
|
7152
|
+
}
|
|
7153
|
+
async function checkLocalRuntimeGitignore(rootDir, issues) {
|
|
7154
|
+
const gitignore = await readKnownFile(rootDir, ".gitignore");
|
|
7155
|
+
const lines = gitignore.ok ? decodeUtf8(gitignore.bytes).split(/\r?\n/u).map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("#")) : [];
|
|
7156
|
+
for (const recommended of LOCAL_RUNTIME_IGNORE_PATHS) {
|
|
7157
|
+
const targetPath = recommended.replace(/\/$/u, "");
|
|
7158
|
+
const exists = await pathExists(rootDir, targetPath);
|
|
7159
|
+
if (!exists) continue;
|
|
7160
|
+
if (!isIgnoreLinePresent(lines, recommended)) {
|
|
7161
|
+
issues.push(
|
|
7162
|
+
issue(
|
|
7163
|
+
"LINT-GITIGNORE-002",
|
|
7164
|
+
"warning",
|
|
7165
|
+
".gitignore",
|
|
7166
|
+
`ignore line for ${recommended}`,
|
|
7167
|
+
"missing",
|
|
7168
|
+
`${recommended} exists locally but is not listed in .gitignore.`,
|
|
7169
|
+
"Run agent-profile init --update-gitignore --write to add recommended ignore lines."
|
|
7170
|
+
)
|
|
7171
|
+
);
|
|
7172
|
+
}
|
|
7173
|
+
}
|
|
7174
|
+
}
|
|
7175
|
+
async function pathExists(rootDir, relativePath) {
|
|
7176
|
+
try {
|
|
7177
|
+
safeOutputPath(relativePath);
|
|
7178
|
+
} catch {
|
|
7179
|
+
return false;
|
|
7180
|
+
}
|
|
7181
|
+
try {
|
|
7182
|
+
const { lstat: lstat3 } = await import("node:fs/promises");
|
|
7183
|
+
await lstat3(path5.join(rootDir, relativePath));
|
|
7184
|
+
return true;
|
|
7185
|
+
} catch (error) {
|
|
7186
|
+
if (isNodeError4(error) && error.code === "ENOENT") {
|
|
7187
|
+
return false;
|
|
7188
|
+
}
|
|
7189
|
+
throw error;
|
|
7190
|
+
}
|
|
7191
|
+
}
|
|
7192
|
+
function isIgnoreLinePresent(lines, target) {
|
|
7193
|
+
const trimmed = target.replace(/\/$/u, "");
|
|
7194
|
+
return lines.some((line) => {
|
|
7195
|
+
const normalized = line.replace(/^\//u, "").replace(/\/$/u, "");
|
|
7196
|
+
return normalized === trimmed;
|
|
7197
|
+
});
|
|
7198
|
+
}
|
|
7199
|
+
async function checkForeignSkillAndSubagentCollisions(rootDir, generatedFiles, lockfile, issues) {
|
|
7200
|
+
const ownedPaths = new Set(
|
|
7201
|
+
(lockfile?.outputs ?? []).map((output) => output.path)
|
|
7202
|
+
);
|
|
7203
|
+
const generatedSkillByName = /* @__PURE__ */ new Map();
|
|
7204
|
+
const generatedSubagentByName = /* @__PURE__ */ new Map();
|
|
7205
|
+
const pushByName = (map, name, entry) => {
|
|
7206
|
+
const list = map.get(name) ?? [];
|
|
7207
|
+
list.push(entry);
|
|
7208
|
+
map.set(name, list);
|
|
7209
|
+
};
|
|
7210
|
+
for (const file of generatedFiles) {
|
|
7211
|
+
const name = parseSkillOrSubagentName(decodeUtf8(file.bytes));
|
|
7212
|
+
if (!name) continue;
|
|
7213
|
+
if (file.target === "codex-workflow-skills" || file.target === "claude-workflow-skills") {
|
|
7214
|
+
pushByName(generatedSkillByName, name, {
|
|
7215
|
+
path: file.path,
|
|
7216
|
+
runtime: pathRuntime(file.path)
|
|
7217
|
+
});
|
|
7218
|
+
}
|
|
7219
|
+
if (file.target === "claude-subagents" || file.target === "codex-subagents" || file.target === "tabnine-subagents") {
|
|
7220
|
+
pushByName(generatedSubagentByName, name, {
|
|
7221
|
+
path: file.path,
|
|
7222
|
+
runtime: pathRuntime(file.path)
|
|
7223
|
+
});
|
|
7224
|
+
}
|
|
7225
|
+
}
|
|
7226
|
+
const skillFiles = await collectSkillFiles(rootDir);
|
|
7227
|
+
for (const skillPath of skillFiles) {
|
|
7228
|
+
if (ownedPaths.has(skillPath)) continue;
|
|
7229
|
+
const generatedPath = generatedFiles.find(
|
|
7230
|
+
(file2) => file2.path === skillPath
|
|
7231
|
+
);
|
|
7232
|
+
const file = await readKnownFile(rootDir, skillPath);
|
|
7233
|
+
if (!file.ok) continue;
|
|
7234
|
+
const text = decodeUtf8(file.bytes);
|
|
7235
|
+
const name = parseSkillOrSubagentName(text);
|
|
7236
|
+
if (generatedPath) {
|
|
7237
|
+
issues.push(
|
|
7238
|
+
issue(
|
|
7239
|
+
"LINT-OWN-001",
|
|
7240
|
+
"error",
|
|
7241
|
+
skillPath,
|
|
7242
|
+
"lockfile-owned generated skill",
|
|
7243
|
+
"foreign skill at generated path",
|
|
7244
|
+
`${skillPath} exists at a generated output path but is not lockfile-owned.`,
|
|
7245
|
+
"Move or rename the existing skill, or run compile --write --force only after reviewing the diff."
|
|
7246
|
+
)
|
|
7247
|
+
);
|
|
7248
|
+
continue;
|
|
7249
|
+
}
|
|
7250
|
+
if (name && generatedSkillByName.has(name)) {
|
|
7251
|
+
const entries = generatedSkillByName.get(name);
|
|
7252
|
+
const others = entries.filter((entry) => entry.path !== skillPath);
|
|
7253
|
+
if (others.length > 0) {
|
|
7254
|
+
const foreignRuntime = pathRuntime(skillPath);
|
|
7255
|
+
const matching = others.find(
|
|
7256
|
+
(entry) => entry.runtime !== void 0 && entry.runtime === foreignRuntime
|
|
7257
|
+
);
|
|
7258
|
+
const referencePath = (matching ?? others[0]).path;
|
|
7259
|
+
const severity = matching ? "error" : "warning";
|
|
7260
|
+
issues.push(
|
|
7261
|
+
issue(
|
|
7262
|
+
"LINT-SKILL-009",
|
|
7263
|
+
severity,
|
|
7264
|
+
skillPath,
|
|
7265
|
+
"skill name distinct from generated skills",
|
|
7266
|
+
`name collides with generated ${referencePath}`,
|
|
7267
|
+
`${skillPath} declares skill name ${name}, which collides with generated ${referencePath}${matching ? ` for the ${matching.runtime} runtime` : ""}.`,
|
|
7268
|
+
matching ? `Rename the foreign skill; both files target the ${matching.runtime} runtime and would load the same name.` : "Rename the foreign skill or move it to a different name to avoid runtime collision."
|
|
7269
|
+
)
|
|
7270
|
+
);
|
|
7271
|
+
}
|
|
7272
|
+
}
|
|
7273
|
+
}
|
|
7274
|
+
const subagentFiles = [];
|
|
7275
|
+
for (const root of SUBAGENT_ROOTS) {
|
|
7276
|
+
const files = await collectSubagentFilesUnder(rootDir, root);
|
|
7277
|
+
for (const file of files) subagentFiles.push(file);
|
|
7278
|
+
}
|
|
7279
|
+
for (const subagentPath of subagentFiles) {
|
|
7280
|
+
if (ownedPaths.has(subagentPath)) continue;
|
|
7281
|
+
const generatedPath = generatedFiles.find(
|
|
7282
|
+
(file2) => file2.path === subagentPath
|
|
7283
|
+
);
|
|
7284
|
+
const file = await readKnownFile(rootDir, subagentPath);
|
|
7285
|
+
if (!file.ok) continue;
|
|
7286
|
+
const text = decodeUtf8(file.bytes);
|
|
7287
|
+
const name = parseSkillOrSubagentName(text);
|
|
7288
|
+
if (generatedPath) {
|
|
7289
|
+
issues.push(
|
|
7290
|
+
issue(
|
|
7291
|
+
"LINT-OWN-001",
|
|
7292
|
+
"error",
|
|
7293
|
+
subagentPath,
|
|
7294
|
+
"lockfile-owned generated subagent",
|
|
7295
|
+
"foreign subagent at generated path",
|
|
7296
|
+
`${subagentPath} exists at a generated output path but is not lockfile-owned.`,
|
|
7297
|
+
"Move or rename the existing subagent, or run compile --write --force only after reviewing the diff."
|
|
7298
|
+
)
|
|
7299
|
+
);
|
|
7300
|
+
continue;
|
|
7301
|
+
}
|
|
7302
|
+
if (name && generatedSubagentByName.has(name)) {
|
|
7303
|
+
const entries = generatedSubagentByName.get(name);
|
|
7304
|
+
const others = entries.filter((entry) => entry.path !== subagentPath);
|
|
7305
|
+
if (others.length > 0) {
|
|
7306
|
+
const foreignRuntime = pathRuntime(subagentPath);
|
|
7307
|
+
const matching = others.find(
|
|
7308
|
+
(entry) => entry.runtime !== void 0 && entry.runtime === foreignRuntime
|
|
7309
|
+
);
|
|
7310
|
+
const referencePath = (matching ?? others[0]).path;
|
|
7311
|
+
const severity = matching ? "error" : "warning";
|
|
7312
|
+
issues.push(
|
|
7313
|
+
issue(
|
|
7314
|
+
"LINT-SUBAGENT-009",
|
|
7315
|
+
severity,
|
|
7316
|
+
subagentPath,
|
|
7317
|
+
"subagent name distinct from generated subagents",
|
|
7318
|
+
`name collides with generated ${referencePath}`,
|
|
7319
|
+
`${subagentPath} declares subagent name ${name}, which collides with generated ${referencePath}${matching ? ` for the ${matching.runtime} runtime` : ""}.`,
|
|
7320
|
+
matching ? `Rename the foreign subagent; both files target the ${matching.runtime} runtime and would load the same name.` : "Rename the foreign subagent to avoid runtime collision."
|
|
7321
|
+
)
|
|
7322
|
+
);
|
|
7323
|
+
}
|
|
7324
|
+
}
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
function pathRuntime(filePath) {
|
|
7328
|
+
if (filePath.startsWith(".agents/skills/") || filePath.startsWith(".codex/agents/")) {
|
|
7329
|
+
return "codex";
|
|
7330
|
+
}
|
|
7331
|
+
if (filePath.startsWith(".claude/skills/") || filePath.startsWith(".claude/agents/")) {
|
|
7332
|
+
return "claude";
|
|
7333
|
+
}
|
|
7334
|
+
if (filePath.startsWith(".tabnine/agent/agents/")) {
|
|
7335
|
+
return "tabnine";
|
|
7336
|
+
}
|
|
7337
|
+
return void 0;
|
|
7338
|
+
}
|
|
7339
|
+
function parseSkillOrSubagentName(text) {
|
|
7340
|
+
const fm = parseMarkdownFrontmatter(text);
|
|
7341
|
+
if (typeof fm.name === "string" && fm.name.length > 0) return fm.name;
|
|
7342
|
+
const tomlMatch = /^\s*name\s*=\s*"([^"]+)"/mu.exec(text);
|
|
7343
|
+
if (tomlMatch && tomlMatch[1]) return tomlMatch[1];
|
|
7344
|
+
return void 0;
|
|
7345
|
+
}
|
|
7346
|
+
|
|
7347
|
+
// src/wizard.ts
|
|
7348
|
+
import { createInterface } from "node:readline/promises";
|
|
7349
|
+
var WIZARD_CLIENT_IDS = [
|
|
7350
|
+
"tabnine",
|
|
7351
|
+
"codex",
|
|
7352
|
+
"claude"
|
|
7353
|
+
];
|
|
7354
|
+
function isNonInteractive(input) {
|
|
7355
|
+
if (input.flag) return true;
|
|
7356
|
+
if (input.override === true) return true;
|
|
7357
|
+
if (input.override === false) return false;
|
|
7358
|
+
if (input.env.CI === "true" || input.env.CI === "1") return true;
|
|
7359
|
+
if (!input.stdin || input.stdin.isTTY !== true) return true;
|
|
7360
|
+
if (!input.stdout || input.stdout.isTTY !== true) return true;
|
|
7361
|
+
return false;
|
|
7362
|
+
}
|
|
7363
|
+
function parseWizardClientSelection(raw) {
|
|
7364
|
+
const normalized = raw.trim().toLowerCase();
|
|
7365
|
+
if (normalized === "" || normalized === "none") return [];
|
|
7366
|
+
const selected = [];
|
|
7367
|
+
for (const token of normalized.split(/[;,]/u).map((item) => item.trim()).filter((item) => item !== "")) {
|
|
7368
|
+
let client;
|
|
7369
|
+
if (/^[0-9]+$/u.test(token)) {
|
|
7370
|
+
client = WIZARD_CLIENT_IDS[Number.parseInt(token, 10) - 1];
|
|
7371
|
+
} else if (WIZARD_CLIENT_IDS.includes(token)) {
|
|
7372
|
+
client = token;
|
|
7373
|
+
}
|
|
7374
|
+
if (client && !selected.includes(client)) {
|
|
7375
|
+
selected.push(client);
|
|
7376
|
+
}
|
|
7377
|
+
}
|
|
7378
|
+
return WIZARD_CLIENT_IDS.filter((id) => selected.includes(id));
|
|
7379
|
+
}
|
|
7380
|
+
function formatWizardClientSelectionQuestion(defaults) {
|
|
7381
|
+
const summary = WIZARD_CLIENT_IDS.map((id, index) => {
|
|
7382
|
+
const enabled = defaults.includes(id);
|
|
7383
|
+
return ` ${index + 1}) ${id}${enabled ? " (default)" : ""}`;
|
|
7384
|
+
}).join("\n");
|
|
7385
|
+
const fallback = defaults.length > 0 ? defaults.join(",") : "none";
|
|
7386
|
+
return `Which clients should this profile enable?
|
|
7387
|
+
${summary}
|
|
7388
|
+
Select multiple with commas or semicolons, for example 2,3.
|
|
7389
|
+
Enter numbers or names, or press enter for defaults [${fallback}]: `;
|
|
7390
|
+
}
|
|
7391
|
+
function recommendStrategy(report) {
|
|
7392
|
+
const warnings = [];
|
|
7393
|
+
const rootFiles = report.files.filter(
|
|
7394
|
+
(file) => file.kind === "root-instructions" && file.exists
|
|
7395
|
+
);
|
|
7396
|
+
const hasConflict = report.files.some(
|
|
7397
|
+
(file) => file.action === "refuse-conflict"
|
|
7398
|
+
);
|
|
7399
|
+
if (hasConflict) {
|
|
7400
|
+
warnings.push(
|
|
7401
|
+
"foreign skill or subagent path conflict detected; review the wizard report before writing."
|
|
7402
|
+
);
|
|
7403
|
+
}
|
|
7404
|
+
const hasLegacyMarker = rootFiles.some(
|
|
7405
|
+
(file) => file.tags.includes("generated-looking")
|
|
7406
|
+
);
|
|
7407
|
+
if (hasLegacyMarker) {
|
|
7408
|
+
warnings.push(
|
|
7409
|
+
"existing instruction file looks generated by a previous compiler version; preserved as manual content."
|
|
7410
|
+
);
|
|
7411
|
+
}
|
|
7412
|
+
const hasUnmarkedSupported = rootFiles.some(
|
|
7413
|
+
(file) => file.ownership !== "mixed" && file.ownership !== "generated-owned" && !file.tags.includes("generated-looking") && file.action !== "refuse-conflict"
|
|
7414
|
+
);
|
|
7415
|
+
if (hasConflict || hasLegacyMarker) {
|
|
7416
|
+
return {
|
|
7417
|
+
strategy: "preserve",
|
|
7418
|
+
reason: hasConflict ? "conflicts must be resolved before adopting region ownership." : "legacy generated marker detected; preserve until lockfile v2 records ownership.",
|
|
7419
|
+
warnings
|
|
7420
|
+
};
|
|
7421
|
+
}
|
|
7422
|
+
if (hasUnmarkedSupported) {
|
|
7423
|
+
return {
|
|
7424
|
+
strategy: "regions",
|
|
7425
|
+
reason: "existing root instruction files can be adopted into mixed ownership and preserved in a manual region.",
|
|
7426
|
+
warnings
|
|
7427
|
+
};
|
|
7428
|
+
}
|
|
7429
|
+
const onlyMixed = rootFiles.length > 0 && rootFiles.every(
|
|
7430
|
+
(file) => file.ownership === "mixed" || file.ownership === "generated-owned"
|
|
7431
|
+
);
|
|
7432
|
+
if (onlyMixed) {
|
|
7433
|
+
return {
|
|
7434
|
+
strategy: "preserve",
|
|
7435
|
+
reason: "existing instruction files already use mixed ownership.",
|
|
7436
|
+
warnings
|
|
7437
|
+
};
|
|
7438
|
+
}
|
|
7439
|
+
return {
|
|
7440
|
+
strategy: "preserve",
|
|
7441
|
+
reason: "no existing agent instruction files detected; create profile only.",
|
|
7442
|
+
warnings
|
|
7443
|
+
};
|
|
7444
|
+
}
|
|
7445
|
+
function formatWizardIntro(context, recommendation) {
|
|
7446
|
+
const lines = [];
|
|
7447
|
+
lines.push("Agent Profile Init", "");
|
|
7448
|
+
lines.push("Detected:");
|
|
7449
|
+
lines.push(`- languages: ${formatList(context.stack.languages)}`);
|
|
7450
|
+
lines.push(
|
|
7451
|
+
`- package managers: ${formatList(context.stack.packageManagers)}`
|
|
7452
|
+
);
|
|
7453
|
+
lines.push(
|
|
7454
|
+
`- clients from existing files: ${formatList(context.detectedClients)}`
|
|
7455
|
+
);
|
|
7456
|
+
lines.push(
|
|
7457
|
+
`- existing instruction files: ${formatList(existingRootInstructions(context.report))}`
|
|
7458
|
+
);
|
|
7459
|
+
lines.push(`- local runtime files: ${formatList(localRuntimePaths(context.report))}`);
|
|
7460
|
+
lines.push(
|
|
7461
|
+
`- generated client config: ${formatList(generatedClientConfigPaths(context.report))}`
|
|
7462
|
+
);
|
|
7463
|
+
const foreign = foreignWorkflowPaths(context.report);
|
|
7464
|
+
if (foreign.length > 0) {
|
|
7465
|
+
lines.push(`- foreign skills/subagents: ${formatList(foreign)}`);
|
|
7466
|
+
}
|
|
7467
|
+
lines.push("");
|
|
7468
|
+
lines.push(`Recommended strategy: ${formatStrategyLabel(recommendation.strategy)}`);
|
|
7469
|
+
lines.push(`Reason: ${recommendation.reason}`);
|
|
7470
|
+
const conflictRows = context.report.files.filter(
|
|
7471
|
+
(file) => file.action === "refuse-conflict"
|
|
7472
|
+
);
|
|
7473
|
+
if (conflictRows.length > 0) {
|
|
7474
|
+
lines.push("", "Conflicts (must be resolved before writing):");
|
|
7475
|
+
for (const row of conflictRows) {
|
|
7476
|
+
lines.push(` - ${row.path}: ${row.notes.join("; ") || "conflict"}`);
|
|
7477
|
+
}
|
|
7478
|
+
}
|
|
7479
|
+
if (recommendation.warnings.length > 0) {
|
|
7480
|
+
lines.push("", "Warnings:");
|
|
7481
|
+
for (const warning of recommendation.warnings) {
|
|
7482
|
+
lines.push(` - ${warning}`);
|
|
7483
|
+
}
|
|
7484
|
+
}
|
|
7485
|
+
lines.push("");
|
|
7486
|
+
return `${lines.join("\n")}
|
|
7487
|
+
`;
|
|
7488
|
+
}
|
|
7489
|
+
function formatWizardPlan(context, outcome) {
|
|
7490
|
+
const lines = [];
|
|
7491
|
+
lines.push("Write plan:");
|
|
7492
|
+
if (!context.hasExistingProfile) {
|
|
7493
|
+
lines.push("- create ai-profile.yaml");
|
|
7494
|
+
} else {
|
|
7495
|
+
lines.push(
|
|
7496
|
+
"- preserve ai-profile.yaml (already exists; init does not edit existing profiles)"
|
|
7497
|
+
);
|
|
7498
|
+
}
|
|
7499
|
+
for (const file of context.report.files) {
|
|
7500
|
+
if (file.kind === "root-instructions") {
|
|
7501
|
+
if (!file.exists) {
|
|
7502
|
+
continue;
|
|
7503
|
+
}
|
|
7504
|
+
if (outcome.strategy === "regions" && file.action === "insert-regions") {
|
|
7505
|
+
lines.push(`- adopt ${file.path} into mixed ownership (manual region preserves existing content)`);
|
|
7506
|
+
} else if (file.action === "update-generated-region") {
|
|
7507
|
+
lines.push(`- update generated region in ${file.path}`);
|
|
7508
|
+
} else {
|
|
7509
|
+
lines.push(`- preserve ${file.path}`);
|
|
7510
|
+
}
|
|
7511
|
+
continue;
|
|
7512
|
+
}
|
|
7513
|
+
if (file.action === "refuse-conflict") {
|
|
7514
|
+
lines.push(`- refuse ${file.path} (${file.notes.join("; ") || "conflict"})`);
|
|
7515
|
+
continue;
|
|
7516
|
+
}
|
|
7517
|
+
if (file.tags.includes("local-runtime")) {
|
|
7518
|
+
lines.push(`- preserve ${file.path} (local runtime)`);
|
|
7519
|
+
continue;
|
|
7520
|
+
}
|
|
7521
|
+
if (file.kind === "client-config") {
|
|
7522
|
+
lines.push(`- preserve ${file.path} (refresh via \`agent-profile compile --write\`)`);
|
|
7523
|
+
}
|
|
7524
|
+
}
|
|
7525
|
+
if (outcome.updateGitignore) {
|
|
7526
|
+
const adds = context.report.gitignore.filter(
|
|
7527
|
+
(item) => item.action !== "already-present"
|
|
7528
|
+
);
|
|
7529
|
+
if (adds.length > 0) {
|
|
7530
|
+
lines.push(
|
|
7531
|
+
`- update .gitignore (${adds.map((item) => item.line).join(", ")})`
|
|
7532
|
+
);
|
|
7533
|
+
}
|
|
7534
|
+
}
|
|
7535
|
+
lines.push("");
|
|
7536
|
+
lines.push(`Strategy: ${formatStrategyLabel(outcome.strategy)}`);
|
|
7537
|
+
if (context.hasExistingProfile) {
|
|
7538
|
+
lines.push("Clients: unchanged (existing profile)");
|
|
7539
|
+
} else {
|
|
7540
|
+
lines.push(`Clients to enable: ${formatList(outcome.clients)}`);
|
|
7541
|
+
}
|
|
7542
|
+
lines.push(`Update .gitignore: ${outcome.updateGitignore ? "yes" : "no"}`);
|
|
7543
|
+
lines.push("");
|
|
7544
|
+
return `${lines.join("\n")}
|
|
7545
|
+
`;
|
|
7546
|
+
}
|
|
7547
|
+
function formatWizardDeclined() {
|
|
7548
|
+
return [
|
|
7549
|
+
"No files written.",
|
|
7550
|
+
"Re-run with --write or confirm in the wizard to write.",
|
|
7551
|
+
""
|
|
7552
|
+
].join("\n");
|
|
7553
|
+
}
|
|
7554
|
+
async function runInitWizard(input) {
|
|
7555
|
+
const recommendation = input.recommendation ?? recommendStrategy(input.context.report);
|
|
7556
|
+
input.io.stdout(formatWizardIntro(input.context, recommendation));
|
|
7557
|
+
const strategy = await input.prompts.selectStrategy({
|
|
7558
|
+
default: recommendation.strategy,
|
|
7559
|
+
recommendation
|
|
7560
|
+
});
|
|
7561
|
+
let context = input.context;
|
|
7562
|
+
if (input.rebuildReport && strategy !== "preserve") {
|
|
7563
|
+
const refreshed = await input.rebuildReport(strategy);
|
|
7564
|
+
context = { ...input.context, report: refreshed };
|
|
7565
|
+
}
|
|
7566
|
+
let normalizedClients;
|
|
7567
|
+
if (context.hasExistingProfile) {
|
|
7568
|
+
normalizedClients = WIZARD_CLIENT_IDS.filter(
|
|
7569
|
+
(id) => context.detectedClients.includes(id)
|
|
7570
|
+
);
|
|
7571
|
+
} else {
|
|
7572
|
+
const clientDefaults = context.detectedClients.length > 0 ? [...context.detectedClients] : [];
|
|
7573
|
+
const clients = await input.prompts.selectClients({
|
|
7574
|
+
defaults: clientDefaults
|
|
7575
|
+
});
|
|
7576
|
+
normalizedClients = WIZARD_CLIENT_IDS.filter(
|
|
7577
|
+
(client) => clients.includes(client)
|
|
7578
|
+
);
|
|
7579
|
+
}
|
|
7580
|
+
const gitignoreNeedsRecommendation = context.report.gitignore.some(
|
|
7581
|
+
(item) => item.action !== "already-present"
|
|
7582
|
+
);
|
|
7583
|
+
const updateGitignore = gitignoreNeedsRecommendation ? await input.prompts.confirmGitignore({ default: false }) : false;
|
|
7584
|
+
const outcomeDraft = {
|
|
7585
|
+
confirmed: false,
|
|
7586
|
+
strategy,
|
|
7587
|
+
clients: normalizedClients,
|
|
7588
|
+
updateGitignore
|
|
7589
|
+
};
|
|
7590
|
+
input.io.stdout(formatWizardPlan(context, outcomeDraft));
|
|
7591
|
+
const confirmed = await input.prompts.confirmWritePlan({ default: false });
|
|
7592
|
+
if (!confirmed) {
|
|
7593
|
+
input.io.stdout(formatWizardDeclined());
|
|
7594
|
+
}
|
|
7595
|
+
return {
|
|
7596
|
+
...outcomeDraft,
|
|
7597
|
+
confirmed
|
|
7598
|
+
};
|
|
7599
|
+
}
|
|
7600
|
+
function createDefaultPrompts(io) {
|
|
7601
|
+
const rl = createInterface({
|
|
7602
|
+
input: process.stdin,
|
|
7603
|
+
output: process.stdout,
|
|
7604
|
+
terminal: true
|
|
7605
|
+
});
|
|
7606
|
+
const ask = async (question, fallback) => {
|
|
7607
|
+
io.stdout(question);
|
|
7608
|
+
const answer = await rl.question("");
|
|
7609
|
+
return answer.trim() === "" ? fallback : answer.trim();
|
|
7610
|
+
};
|
|
7611
|
+
const confirm = async (question, def) => {
|
|
7612
|
+
const suffix = def ? "[Y/n]" : "[y/N]";
|
|
7613
|
+
const fallback = def ? "y" : "n";
|
|
7614
|
+
const raw = await ask(`${question} ${suffix} `, fallback);
|
|
7615
|
+
const lower = raw.toLowerCase();
|
|
7616
|
+
if (lower === "y" || lower === "yes") return true;
|
|
7617
|
+
if (lower === "n" || lower === "no") return false;
|
|
7618
|
+
return def;
|
|
7619
|
+
};
|
|
7620
|
+
return {
|
|
7621
|
+
async selectStrategy({ default: def }) {
|
|
7622
|
+
const raw = await ask(
|
|
7623
|
+
`How should existing agent instruction files be handled?
|
|
7624
|
+
1) Preserve existing files${def === "preserve" ? " (default)" : ""}
|
|
7625
|
+
2) Add generated regions${def === "regions" ? " (default)" : ""}
|
|
7626
|
+
Choose [1/2]: `,
|
|
7627
|
+
def === "preserve" ? "1" : "2"
|
|
7628
|
+
);
|
|
7629
|
+
if (raw === "2" || raw.toLowerCase().startsWith("r")) return "regions";
|
|
7630
|
+
if (raw === "1" || raw.toLowerCase().startsWith("p")) return "preserve";
|
|
7631
|
+
return def;
|
|
7632
|
+
},
|
|
7633
|
+
async selectClients({ defaults }) {
|
|
7634
|
+
const raw = await ask(
|
|
7635
|
+
formatWizardClientSelectionQuestion(defaults),
|
|
7636
|
+
defaults.join(",")
|
|
7637
|
+
);
|
|
7638
|
+
return parseWizardClientSelection(raw);
|
|
7639
|
+
},
|
|
7640
|
+
async confirmGitignore({ default: def }) {
|
|
7641
|
+
return confirm(
|
|
7642
|
+
"Add recommended local-runtime ignore entries to .gitignore?",
|
|
7643
|
+
def
|
|
7644
|
+
);
|
|
7645
|
+
},
|
|
7646
|
+
async confirmWritePlan({ default: def }) {
|
|
7647
|
+
try {
|
|
7648
|
+
return await confirm("Write this plan?", def);
|
|
7649
|
+
} finally {
|
|
7650
|
+
rl.close();
|
|
7651
|
+
}
|
|
7652
|
+
}
|
|
7653
|
+
};
|
|
7654
|
+
}
|
|
7655
|
+
function formatList(values) {
|
|
7656
|
+
return values.length === 0 ? "(none)" : values.join(", ");
|
|
7657
|
+
}
|
|
7658
|
+
function formatStrategyLabel(strategy) {
|
|
7659
|
+
return strategy === "regions" ? "Add generated regions" : "Preserve existing files";
|
|
7660
|
+
}
|
|
7661
|
+
function existingRootInstructions(report) {
|
|
7662
|
+
return report.files.filter((file) => file.kind === "root-instructions" && file.exists).map((file) => file.path);
|
|
7663
|
+
}
|
|
7664
|
+
function localRuntimePaths(report) {
|
|
7665
|
+
return report.files.filter((file) => file.tags.includes("local-runtime") && file.exists).map((file) => file.path);
|
|
7666
|
+
}
|
|
7667
|
+
function generatedClientConfigPaths(report) {
|
|
7668
|
+
return report.files.filter(
|
|
7669
|
+
(file) => file.kind === "client-config" && !file.tags.includes("local-runtime") && file.exists
|
|
7670
|
+
).map((file) => file.path);
|
|
7671
|
+
}
|
|
7672
|
+
function foreignWorkflowPaths(report) {
|
|
7673
|
+
return report.files.filter(
|
|
7674
|
+
(file) => (file.kind === "workflow-skill" || file.kind === "subagent") && file.exists && file.ownership !== "generated-owned"
|
|
7675
|
+
).map((file) => file.path);
|
|
7676
|
+
}
|
|
7677
|
+
|
|
7678
|
+
// src/index.ts
|
|
7679
|
+
var DEFAULT_UI_HOST = "127.0.0.1";
|
|
7680
|
+
var require2 = createRequire(import.meta.url);
|
|
7681
|
+
var CLIENT_IDS = ["tabnine", "codex", "claude"];
|
|
7682
|
+
var DEFAULT_IO = {
|
|
7683
|
+
stdout: (text) => process.stdout.write(text),
|
|
7684
|
+
stderr: (text) => process.stderr.write(text)
|
|
7685
|
+
};
|
|
5659
7686
|
async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
5660
7687
|
const io = { ...DEFAULT_IO, ...options.io };
|
|
5661
|
-
const cwd =
|
|
7688
|
+
const cwd = path6.resolve(options.cwd ?? process.cwd());
|
|
5662
7689
|
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
5663
7690
|
io.stdout(formatHelp());
|
|
5664
7691
|
return 0;
|
|
@@ -5670,13 +7697,12 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
5670
7697
|
case "doctor":
|
|
5671
7698
|
return runDoctorCommand(rest, cwd, io);
|
|
5672
7699
|
case "init":
|
|
5673
|
-
return runInit(
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
options.
|
|
5678
|
-
|
|
5679
|
-
);
|
|
7700
|
+
return runInit(rest, cwd, io, {
|
|
7701
|
+
...options.presetNow ? { presetNow: options.presetNow } : {},
|
|
7702
|
+
...options.presetVerificationKeys ? { presetVerificationKeys: options.presetVerificationKeys } : {},
|
|
7703
|
+
...options.prompts ? { prompts: options.prompts } : {},
|
|
7704
|
+
...options.nonInteractive !== void 0 ? { nonInteractive: options.nonInteractive } : {}
|
|
7705
|
+
});
|
|
5680
7706
|
case "ui":
|
|
5681
7707
|
return runUi(rest, cwd, io, options.launchUi ?? launchPublishedUi);
|
|
5682
7708
|
default:
|
|
@@ -5698,16 +7724,32 @@ ${formatHelp()}`);
|
|
|
5698
7724
|
io.stdout(formatHelp());
|
|
5699
7725
|
return 0;
|
|
5700
7726
|
}
|
|
5701
|
-
const rootDir =
|
|
5702
|
-
|
|
5703
|
-
if (
|
|
5704
|
-
|
|
5705
|
-
|
|
7727
|
+
const rootDir = path6.resolve(cwd, parsed.root);
|
|
7728
|
+
let resolvedPort;
|
|
7729
|
+
if (parsed.port === "auto") {
|
|
7730
|
+
const ephemeral = await reserveEphemeralPort(parsed.host);
|
|
7731
|
+
if (!ephemeral.ok) {
|
|
7732
|
+
io.stderr(
|
|
7733
|
+
`No ephemeral loopback port could be reserved on ${parsed.host}. Specify --port <number> explicitly.
|
|
5706
7734
|
`
|
|
5707
|
-
|
|
5708
|
-
|
|
7735
|
+
);
|
|
7736
|
+
return 1;
|
|
7737
|
+
}
|
|
7738
|
+
resolvedPort = ephemeral.port;
|
|
7739
|
+
} else {
|
|
7740
|
+
const portCheck = await assertPortAvailable(parsed.host, parsed.port);
|
|
7741
|
+
if (!portCheck.ok) {
|
|
7742
|
+
io.stderr(
|
|
7743
|
+
`Port ${parsed.port} is not available on ${parsed.host}. Another process may be listening; choose a different port with --port <number>.
|
|
7744
|
+
`
|
|
7745
|
+
);
|
|
7746
|
+
return 1;
|
|
7747
|
+
}
|
|
7748
|
+
resolvedPort = parsed.port;
|
|
5709
7749
|
}
|
|
5710
|
-
const
|
|
7750
|
+
const sessionToken = generateSessionToken();
|
|
7751
|
+
const url = formatUiUrl(parsed.host, resolvedPort, sessionToken);
|
|
7752
|
+
const open = parsed.open ?? isInteractiveTty(io);
|
|
5711
7753
|
io.stdout(`Agent Profile UI
|
|
5712
7754
|
`);
|
|
5713
7755
|
io.stdout(`url: ${url}
|
|
@@ -5721,8 +7763,9 @@ ${formatHelp()}`);
|
|
|
5721
7763
|
return launchUi({
|
|
5722
7764
|
rootDir,
|
|
5723
7765
|
host: parsed.host,
|
|
5724
|
-
port:
|
|
5725
|
-
open
|
|
7766
|
+
port: resolvedPort,
|
|
7767
|
+
open,
|
|
7768
|
+
sessionToken
|
|
5726
7769
|
});
|
|
5727
7770
|
}
|
|
5728
7771
|
async function runDoctorCommand(args, cwd, io) {
|
|
@@ -5738,7 +7781,7 @@ ${formatHelp()}`);
|
|
|
5738
7781
|
return 0;
|
|
5739
7782
|
}
|
|
5740
7783
|
const result = await runDoctor({
|
|
5741
|
-
rootDir:
|
|
7784
|
+
rootDir: path6.resolve(cwd, parsed.root)
|
|
5742
7785
|
});
|
|
5743
7786
|
if (parsed.json) {
|
|
5744
7787
|
io.stdout(`${JSON.stringify(result, null, 2)}
|
|
@@ -5760,7 +7803,7 @@ ${formatHelp()}`);
|
|
|
5760
7803
|
io.stdout(formatHelp());
|
|
5761
7804
|
return 0;
|
|
5762
7805
|
}
|
|
5763
|
-
const rootDir =
|
|
7806
|
+
const rootDir = path6.resolve(cwd, parsed.root);
|
|
5764
7807
|
const safeProfilePath = toSafeCliPath(parsed.profile);
|
|
5765
7808
|
if (!safeProfilePath.ok) {
|
|
5766
7809
|
io.stderr(`${safeProfilePath.message}
|
|
@@ -5769,7 +7812,7 @@ ${formatHelp()}`);
|
|
|
5769
7812
|
}
|
|
5770
7813
|
let profileBytes;
|
|
5771
7814
|
try {
|
|
5772
|
-
profileBytes = await
|
|
7815
|
+
profileBytes = await readOptionalBytes2(rootDir, safeProfilePath.path);
|
|
5773
7816
|
} catch (error) {
|
|
5774
7817
|
if (!isUnsafePathError(error)) {
|
|
5775
7818
|
io.stderr(
|
|
@@ -5821,13 +7864,60 @@ ${formatHelp()}`);
|
|
|
5821
7864
|
io.stderr(formatCompileIssues(compileResult.issues));
|
|
5822
7865
|
return 1;
|
|
5823
7866
|
}
|
|
7867
|
+
let regionPlan;
|
|
7868
|
+
try {
|
|
7869
|
+
regionPlan = await planRegionAwareWrites(rootDir, compileResult.files, {
|
|
7870
|
+
force: parsed.force
|
|
7871
|
+
});
|
|
7872
|
+
} catch {
|
|
7873
|
+
io.stderr(
|
|
7874
|
+
formatSimpleError(
|
|
7875
|
+
"generated outputs",
|
|
7876
|
+
"safe repository-local readable paths",
|
|
7877
|
+
"unsafe path",
|
|
7878
|
+
"Existing generated output paths could not be safely read under --root."
|
|
7879
|
+
)
|
|
7880
|
+
);
|
|
7881
|
+
return 1;
|
|
7882
|
+
}
|
|
7883
|
+
if (regionPlan.refusals.length > 0) {
|
|
7884
|
+
const hashMismatches = regionPlan.refusals.filter(
|
|
7885
|
+
(item) => item.reason === "hash-mismatch"
|
|
7886
|
+
);
|
|
7887
|
+
const adoptionRefusals = regionPlan.refusals.filter(
|
|
7888
|
+
(item) => item.reason !== "hash-mismatch"
|
|
7889
|
+
);
|
|
7890
|
+
const lines = [];
|
|
7891
|
+
if (adoptionRefusals.length > 0) {
|
|
7892
|
+
lines.push(
|
|
7893
|
+
"Refusing to overwrite region-aware instruction files without explicit adoption:",
|
|
7894
|
+
...adoptionRefusals.map(
|
|
7895
|
+
(item) => `- ${item.path} (${item.reason})`
|
|
7896
|
+
),
|
|
7897
|
+
"Run `agent-profile init --import --strategy regions --write` to adopt existing files into mixed ownership."
|
|
7898
|
+
);
|
|
7899
|
+
}
|
|
7900
|
+
if (hashMismatches.length > 0) {
|
|
7901
|
+
lines.push(
|
|
7902
|
+
"Refusing to overwrite lockfile-owned generated region files that differ from ai-profile.lock:",
|
|
7903
|
+
...hashMismatches.map(
|
|
7904
|
+
(item) => `- ${item.path} (${item.reason})`
|
|
7905
|
+
),
|
|
7906
|
+
"Re-run with --force after reviewing the diff, or regenerate ai-profile.lock to record the new bytes."
|
|
7907
|
+
);
|
|
7908
|
+
}
|
|
7909
|
+
io.stderr(`${lines.join("\n")}
|
|
7910
|
+
`);
|
|
7911
|
+
return 3;
|
|
7912
|
+
}
|
|
5824
7913
|
const lockfile = createLockfileFile({
|
|
5825
7914
|
profilePath: safeProfilePath.path,
|
|
5826
7915
|
profileBytes,
|
|
5827
7916
|
templates: compileResult.templates,
|
|
5828
|
-
files: compileResult.files
|
|
7917
|
+
files: compileResult.files,
|
|
7918
|
+
mixedOutputs: regionPlan.mixedOutputs
|
|
5829
7919
|
});
|
|
5830
|
-
const writes =
|
|
7920
|
+
const writes = [...regionPlan.writes, { path: lockfile.path, bytes: lockfile.bytes }];
|
|
5831
7921
|
if (parsed.write && !parsed.force) {
|
|
5832
7922
|
let protectedPaths;
|
|
5833
7923
|
try {
|
|
@@ -5862,7 +7952,7 @@ ${protectedPaths.map((item) => `- ${item.path} (${item.reason})`).join("\n")}
|
|
|
5862
7952
|
io.stdout(formatWritePlan("Agent Profile Compile", parsed.write, plan));
|
|
5863
7953
|
return 0;
|
|
5864
7954
|
}
|
|
5865
|
-
async function runInit(args, cwd, io,
|
|
7955
|
+
async function runInit(args, cwd, io, options = {}) {
|
|
5866
7956
|
const parsed = parseInitArgs(args);
|
|
5867
7957
|
if (!parsed.ok) {
|
|
5868
7958
|
io.stderr(`${parsed.message}
|
|
@@ -5874,7 +7964,9 @@ ${formatHelp()}`);
|
|
|
5874
7964
|
io.stdout(formatHelp());
|
|
5875
7965
|
return 0;
|
|
5876
7966
|
}
|
|
5877
|
-
const
|
|
7967
|
+
const presetNow = options.presetNow;
|
|
7968
|
+
const presetVerificationKeys = options.presetVerificationKeys;
|
|
7969
|
+
const rootDir = path6.resolve(cwd, parsed.root);
|
|
5878
7970
|
const safeProfilePath = toSafeCliPath(parsed.profile);
|
|
5879
7971
|
if (!safeProfilePath.ok) {
|
|
5880
7972
|
emitInitOutput(
|
|
@@ -5902,7 +7994,7 @@ ${formatHelp()}`);
|
|
|
5902
7994
|
}
|
|
5903
7995
|
let existingProfileBytes;
|
|
5904
7996
|
try {
|
|
5905
|
-
existingProfileBytes = await
|
|
7997
|
+
existingProfileBytes = await readOptionalBytes2(
|
|
5906
7998
|
rootDir,
|
|
5907
7999
|
safeProfilePath.path
|
|
5908
8000
|
);
|
|
@@ -5918,11 +8010,77 @@ ${formatHelp()}`);
|
|
|
5918
8010
|
);
|
|
5919
8011
|
return 1;
|
|
5920
8012
|
}
|
|
8013
|
+
if (isWizardEligibleArgs(args) && presetPayload === void 0) {
|
|
8014
|
+
const dispatch = await dispatchInitWizard({
|
|
8015
|
+
args: parsed,
|
|
8016
|
+
rootDir,
|
|
8017
|
+
profilePath: safeProfilePath.path,
|
|
8018
|
+
existingProfileBytes,
|
|
8019
|
+
io,
|
|
8020
|
+
promptsOverride: options.prompts,
|
|
8021
|
+
nonInteractiveOverride: options.nonInteractive
|
|
8022
|
+
});
|
|
8023
|
+
if (dispatch === "declined") {
|
|
8024
|
+
return 0;
|
|
8025
|
+
}
|
|
8026
|
+
}
|
|
5921
8027
|
if (existingProfileBytes) {
|
|
5922
8028
|
const existingClients = getExistingProfileClients(
|
|
5923
8029
|
existingProfileBytes,
|
|
5924
8030
|
safeProfilePath.path
|
|
5925
8031
|
);
|
|
8032
|
+
if (parsed.importExisting && parsed.strategy === "regions" && parsed.write) {
|
|
8033
|
+
const parsedProfile = parseProfileYaml(
|
|
8034
|
+
Buffer.from(existingProfileBytes).toString("utf8"),
|
|
8035
|
+
{ sourcePath: safeProfilePath.path }
|
|
8036
|
+
);
|
|
8037
|
+
if (parsedProfile.ok) {
|
|
8038
|
+
const result = await planRegionAdoptions(
|
|
8039
|
+
rootDir,
|
|
8040
|
+
parsedProfile.profile
|
|
8041
|
+
);
|
|
8042
|
+
if (result.refusals.length > 0) {
|
|
8043
|
+
io.stderr(formatRegionAdoptionRefusals(result.refusals));
|
|
8044
|
+
return 3;
|
|
8045
|
+
}
|
|
8046
|
+
if (result.adoptions.length > 0) {
|
|
8047
|
+
await applyWritePlan({
|
|
8048
|
+
rootDir,
|
|
8049
|
+
writes: result.adoptions.map((adoption) => ({
|
|
8050
|
+
path: adoption.path,
|
|
8051
|
+
bytes: adoption.bytes
|
|
8052
|
+
}))
|
|
8053
|
+
});
|
|
8054
|
+
}
|
|
8055
|
+
}
|
|
8056
|
+
}
|
|
8057
|
+
if (parsed.updateGitignore && parsed.write) {
|
|
8058
|
+
const findings = await getLocalRuntimeGitignoreFindings(rootDir);
|
|
8059
|
+
try {
|
|
8060
|
+
await appendMissingGitignoreLines(
|
|
8061
|
+
rootDir,
|
|
8062
|
+
findings.filter((finding) => finding.action === "would-add").map((finding) => finding.line)
|
|
8063
|
+
);
|
|
8064
|
+
} catch {
|
|
8065
|
+
}
|
|
8066
|
+
}
|
|
8067
|
+
let importReport2;
|
|
8068
|
+
if (parsed.importExisting) {
|
|
8069
|
+
const parsedProfile = parseProfileYaml(
|
|
8070
|
+
Buffer.from(existingProfileBytes).toString("utf8"),
|
|
8071
|
+
{ sourcePath: safeProfilePath.path }
|
|
8072
|
+
);
|
|
8073
|
+
const existingStack = await detectStack(rootDir);
|
|
8074
|
+
importReport2 = await buildPhase14ImportReport({
|
|
8075
|
+
rootDir,
|
|
8076
|
+
mode: parsed.write ? "write" : "dry-run",
|
|
8077
|
+
strategy: parsed.strategy,
|
|
8078
|
+
profilePath: safeProfilePath.path,
|
|
8079
|
+
profile: parsedProfile.ok ? parsedProfile.profile : void 0,
|
|
8080
|
+
wouldCreateProfile: false,
|
|
8081
|
+
stack: existingStack.stack
|
|
8082
|
+
});
|
|
8083
|
+
}
|
|
5926
8084
|
emitInitOutput(parsed, io, {
|
|
5927
8085
|
mode: parsed.write ? "write" : "dry-run",
|
|
5928
8086
|
status: "ok",
|
|
@@ -5936,7 +8094,8 @@ ${formatHelp()}`);
|
|
|
5936
8094
|
ignoredClientFlags: parsed.clients.length > 0 || parsed.noClients.length > 0,
|
|
5937
8095
|
stackWarnings: [],
|
|
5938
8096
|
importFindings: [],
|
|
5939
|
-
gitignoreSuggestions: []
|
|
8097
|
+
gitignoreSuggestions: [],
|
|
8098
|
+
...importReport2 ? { import: importReport2 } : {}
|
|
5940
8099
|
});
|
|
5941
8100
|
return 0;
|
|
5942
8101
|
}
|
|
@@ -5985,6 +8144,35 @@ ${formatHelp()}`);
|
|
|
5985
8144
|
bytes: profileText
|
|
5986
8145
|
}
|
|
5987
8146
|
];
|
|
8147
|
+
let regionAdoptions = [];
|
|
8148
|
+
if (parsed.importExisting && parsed.strategy === "regions") {
|
|
8149
|
+
let result;
|
|
8150
|
+
try {
|
|
8151
|
+
result = await planRegionAdoptions(rootDir, validation.profile);
|
|
8152
|
+
} catch (error) {
|
|
8153
|
+
const reason = classifyInitWriteFailure(error);
|
|
8154
|
+
emitInitOutput(
|
|
8155
|
+
parsed,
|
|
8156
|
+
io,
|
|
8157
|
+
createInitRefusal({
|
|
8158
|
+
profilePath: safeProfilePath.path,
|
|
8159
|
+
reason,
|
|
8160
|
+
message: formatInitFailureMessage(reason),
|
|
8161
|
+
clients,
|
|
8162
|
+
detectedStack: stackResult.stack.languages
|
|
8163
|
+
})
|
|
8164
|
+
);
|
|
8165
|
+
return 1;
|
|
8166
|
+
}
|
|
8167
|
+
if (result.refusals.length > 0) {
|
|
8168
|
+
io.stderr(formatRegionAdoptionRefusals(result.refusals));
|
|
8169
|
+
return 3;
|
|
8170
|
+
}
|
|
8171
|
+
regionAdoptions = result.adoptions;
|
|
8172
|
+
for (const adoption of regionAdoptions) {
|
|
8173
|
+
writes.push({ path: adoption.path, bytes: adoption.bytes });
|
|
8174
|
+
}
|
|
8175
|
+
}
|
|
5988
8176
|
let plan;
|
|
5989
8177
|
try {
|
|
5990
8178
|
plan = parsed.write ? await applyWritePlan({ rootDir, writes }) : await planWrites({ rootDir, writes });
|
|
@@ -6005,7 +8193,7 @@ ${formatHelp()}`);
|
|
|
6005
8193
|
}
|
|
6006
8194
|
if (parsed.write) {
|
|
6007
8195
|
try {
|
|
6008
|
-
const written = await
|
|
8196
|
+
const written = await readOptionalBytes2(rootDir, safeProfilePath.path);
|
|
6009
8197
|
if (!written || !Buffer.from(written).equals(Buffer.from(profileText, "utf8"))) {
|
|
6010
8198
|
emitInitOutput(
|
|
6011
8199
|
parsed,
|
|
@@ -6036,6 +8224,28 @@ ${formatHelp()}`);
|
|
|
6036
8224
|
}
|
|
6037
8225
|
}
|
|
6038
8226
|
const suggestions = presetPayload === void 0 ? await getGitignoreSuggestions(rootDir) : [];
|
|
8227
|
+
const gitignoreFindings = presetPayload === void 0 ? await getLocalRuntimeGitignoreFindings(rootDir) : [];
|
|
8228
|
+
if (parsed.updateGitignore && parsed.write) {
|
|
8229
|
+
try {
|
|
8230
|
+
await appendMissingGitignoreLines(
|
|
8231
|
+
rootDir,
|
|
8232
|
+
gitignoreFindings.filter((finding) => finding.action === "would-add").map((finding) => finding.line)
|
|
8233
|
+
);
|
|
8234
|
+
} catch {
|
|
8235
|
+
}
|
|
8236
|
+
}
|
|
8237
|
+
let importReport;
|
|
8238
|
+
if (parsed.importExisting) {
|
|
8239
|
+
importReport = await buildPhase14ImportReport({
|
|
8240
|
+
rootDir,
|
|
8241
|
+
mode: parsed.write ? "write" : "dry-run",
|
|
8242
|
+
strategy: parsed.strategy,
|
|
8243
|
+
profilePath: safeProfilePath.path,
|
|
8244
|
+
profile: validation.profile,
|
|
8245
|
+
wouldCreateProfile: !parsed.write,
|
|
8246
|
+
stack: stackResult.stack
|
|
8247
|
+
});
|
|
8248
|
+
}
|
|
6039
8249
|
emitInitOutput(parsed, io, {
|
|
6040
8250
|
mode: parsed.write ? "write" : "dry-run",
|
|
6041
8251
|
status: "ok",
|
|
@@ -6049,10 +8259,114 @@ ${formatHelp()}`);
|
|
|
6049
8259
|
preset: presetPayload,
|
|
6050
8260
|
stackWarnings: stackResult.warnings,
|
|
6051
8261
|
importFindings: importResult?.findings ?? [],
|
|
6052
|
-
gitignoreSuggestions: suggestions
|
|
8262
|
+
gitignoreSuggestions: suggestions,
|
|
8263
|
+
...importReport ? { import: importReport } : {}
|
|
6053
8264
|
});
|
|
6054
8265
|
return 0;
|
|
6055
8266
|
}
|
|
8267
|
+
async function dispatchInitWizard(input) {
|
|
8268
|
+
const nonInteractive = isNonInteractive({
|
|
8269
|
+
env: process.env,
|
|
8270
|
+
stdin: process.stdin,
|
|
8271
|
+
stdout: process.stdout,
|
|
8272
|
+
flag: input.args.nonInteractive,
|
|
8273
|
+
...input.nonInteractiveOverride !== void 0 ? { override: input.nonInteractiveOverride } : {}
|
|
8274
|
+
});
|
|
8275
|
+
if (nonInteractive) {
|
|
8276
|
+
input.args.importExisting = true;
|
|
8277
|
+
return "non-interactive";
|
|
8278
|
+
}
|
|
8279
|
+
let profileForReport;
|
|
8280
|
+
if (input.existingProfileBytes) {
|
|
8281
|
+
const parsedProfile = parseProfileYaml(
|
|
8282
|
+
Buffer.from(input.existingProfileBytes).toString("utf8"),
|
|
8283
|
+
{ sourcePath: input.profilePath }
|
|
8284
|
+
);
|
|
8285
|
+
if (parsedProfile.ok) {
|
|
8286
|
+
profileForReport = parsedProfile.profile;
|
|
8287
|
+
}
|
|
8288
|
+
}
|
|
8289
|
+
const stackForWizard = await detectStack(input.rootDir);
|
|
8290
|
+
const importForWizard = await analyzeExistingArtifacts(input.rootDir);
|
|
8291
|
+
const wizardPhaseReport = await buildPhase14ImportReport({
|
|
8292
|
+
rootDir: input.rootDir,
|
|
8293
|
+
mode: "dry-run",
|
|
8294
|
+
strategy: "preserve",
|
|
8295
|
+
profilePath: input.profilePath,
|
|
8296
|
+
profile: profileForReport,
|
|
8297
|
+
wouldCreateProfile: input.existingProfileBytes === void 0,
|
|
8298
|
+
stack: stackForWizard.stack
|
|
8299
|
+
});
|
|
8300
|
+
const detectedClients = WIZARD_CLIENT_IDS.filter(
|
|
8301
|
+
(id) => importForWizard.clients[id]
|
|
8302
|
+
);
|
|
8303
|
+
const context = {
|
|
8304
|
+
stack: {
|
|
8305
|
+
languages: stackForWizard.stack.languages,
|
|
8306
|
+
frameworks: stackForWizard.stack.frameworks,
|
|
8307
|
+
packageManagers: stackForWizard.stack.packageManagers,
|
|
8308
|
+
testing: stackForWizard.stack.testing
|
|
8309
|
+
},
|
|
8310
|
+
detectedClients,
|
|
8311
|
+
hasExistingProfile: input.existingProfileBytes !== void 0,
|
|
8312
|
+
report: toWizardImportReport(wizardPhaseReport)
|
|
8313
|
+
};
|
|
8314
|
+
const prompts = input.promptsOverride ?? createDefaultPrompts(input.io);
|
|
8315
|
+
const outcome = await runInitWizard({
|
|
8316
|
+
context,
|
|
8317
|
+
io: input.io,
|
|
8318
|
+
prompts,
|
|
8319
|
+
rebuildReport: async (strategy) => {
|
|
8320
|
+
const refreshed = await buildPhase14ImportReport({
|
|
8321
|
+
rootDir: input.rootDir,
|
|
8322
|
+
mode: "dry-run",
|
|
8323
|
+
strategy,
|
|
8324
|
+
profilePath: input.profilePath,
|
|
8325
|
+
profile: profileForReport,
|
|
8326
|
+
wouldCreateProfile: input.existingProfileBytes === void 0,
|
|
8327
|
+
stack: stackForWizard.stack
|
|
8328
|
+
});
|
|
8329
|
+
return toWizardImportReport(refreshed);
|
|
8330
|
+
}
|
|
8331
|
+
});
|
|
8332
|
+
if (!outcome.confirmed) {
|
|
8333
|
+
return "declined";
|
|
8334
|
+
}
|
|
8335
|
+
input.args.importExisting = true;
|
|
8336
|
+
input.args.strategy = outcome.strategy;
|
|
8337
|
+
input.args.updateGitignore = outcome.updateGitignore;
|
|
8338
|
+
input.args.write = true;
|
|
8339
|
+
input.args.clients = WIZARD_CLIENT_IDS.filter(
|
|
8340
|
+
(id) => outcome.clients.includes(id)
|
|
8341
|
+
);
|
|
8342
|
+
input.args.noClients = WIZARD_CLIENT_IDS.filter(
|
|
8343
|
+
(id) => !outcome.clients.includes(id)
|
|
8344
|
+
);
|
|
8345
|
+
return "confirmed";
|
|
8346
|
+
}
|
|
8347
|
+
function toWizardImportReport(report) {
|
|
8348
|
+
return {
|
|
8349
|
+
files: report.files.map((file) => ({
|
|
8350
|
+
path: file.path,
|
|
8351
|
+
exists: file.exists,
|
|
8352
|
+
kind: file.kind,
|
|
8353
|
+
ownership: file.ownership,
|
|
8354
|
+
tags: [...file.tags],
|
|
8355
|
+
action: file.action,
|
|
8356
|
+
notes: [...file.notes]
|
|
8357
|
+
})),
|
|
8358
|
+
gitignore: report.gitignore.map((finding) => ({
|
|
8359
|
+
line: finding.line,
|
|
8360
|
+
action: finding.action
|
|
8361
|
+
})),
|
|
8362
|
+
summary: {
|
|
8363
|
+
wouldCreateProfile: report.summary.wouldCreateProfile,
|
|
8364
|
+
wouldUpdateRegions: report.summary.wouldUpdateRegions,
|
|
8365
|
+
preservedManualFiles: report.summary.preservedManualFiles,
|
|
8366
|
+
conflicts: report.summary.conflicts
|
|
8367
|
+
}
|
|
8368
|
+
};
|
|
8369
|
+
}
|
|
6056
8370
|
function parseDoctorArgs(args) {
|
|
6057
8371
|
let root = ".";
|
|
6058
8372
|
let json = false;
|
|
@@ -6153,11 +8467,15 @@ function parseInitArgs(args) {
|
|
|
6153
8467
|
let dryRun = false;
|
|
6154
8468
|
let write = false;
|
|
6155
8469
|
let importExisting = false;
|
|
8470
|
+
let strategy = "preserve";
|
|
8471
|
+
let strategyProvided = false;
|
|
8472
|
+
let updateGitignore = false;
|
|
6156
8473
|
const clients = [];
|
|
6157
8474
|
const noClients = [];
|
|
6158
8475
|
let json = false;
|
|
6159
8476
|
let quiet = false;
|
|
6160
8477
|
let help = false;
|
|
8478
|
+
let nonInteractive = false;
|
|
6161
8479
|
for (let index = 0; index < args.length; index += 1) {
|
|
6162
8480
|
const arg = args[index];
|
|
6163
8481
|
switch (arg) {
|
|
@@ -6201,6 +8519,28 @@ function parseInitArgs(args) {
|
|
|
6201
8519
|
case "--import":
|
|
6202
8520
|
importExisting = true;
|
|
6203
8521
|
break;
|
|
8522
|
+
case "--strategy": {
|
|
8523
|
+
const value = args[index + 1];
|
|
8524
|
+
if (!value || value.startsWith("--")) {
|
|
8525
|
+
return {
|
|
8526
|
+
ok: false,
|
|
8527
|
+
message: "--strategy requires preserve or regions."
|
|
8528
|
+
};
|
|
8529
|
+
}
|
|
8530
|
+
if (value !== "preserve" && value !== "regions") {
|
|
8531
|
+
return {
|
|
8532
|
+
ok: false,
|
|
8533
|
+
message: `--strategy must be preserve or regions. Got: ${value}.`
|
|
8534
|
+
};
|
|
8535
|
+
}
|
|
8536
|
+
strategy = value;
|
|
8537
|
+
strategyProvided = true;
|
|
8538
|
+
index += 1;
|
|
8539
|
+
break;
|
|
8540
|
+
}
|
|
8541
|
+
case "--update-gitignore":
|
|
8542
|
+
updateGitignore = true;
|
|
8543
|
+
break;
|
|
6204
8544
|
case "--client": {
|
|
6205
8545
|
const value = args[index + 1];
|
|
6206
8546
|
if (!value || value.startsWith("--")) {
|
|
@@ -6239,6 +8579,9 @@ function parseInitArgs(args) {
|
|
|
6239
8579
|
case "--quiet":
|
|
6240
8580
|
quiet = true;
|
|
6241
8581
|
break;
|
|
8582
|
+
case "--non-interactive":
|
|
8583
|
+
nonInteractive = true;
|
|
8584
|
+
break;
|
|
6242
8585
|
case "--interactive":
|
|
6243
8586
|
return {
|
|
6244
8587
|
ok: false,
|
|
@@ -6270,6 +8613,18 @@ function parseInitArgs(args) {
|
|
|
6270
8613
|
message: "--preset cannot be used with --profile in Phase 9; preset init writes only ai-profile.yaml."
|
|
6271
8614
|
};
|
|
6272
8615
|
}
|
|
8616
|
+
if (strategyProvided && !importExisting) {
|
|
8617
|
+
return {
|
|
8618
|
+
ok: false,
|
|
8619
|
+
message: "--strategy is only valid with --import."
|
|
8620
|
+
};
|
|
8621
|
+
}
|
|
8622
|
+
if (updateGitignore && !write) {
|
|
8623
|
+
return {
|
|
8624
|
+
ok: false,
|
|
8625
|
+
message: "--update-gitignore requires --write."
|
|
8626
|
+
};
|
|
8627
|
+
}
|
|
6273
8628
|
return {
|
|
6274
8629
|
ok: true,
|
|
6275
8630
|
root,
|
|
@@ -6279,13 +8634,32 @@ function parseInitArgs(args) {
|
|
|
6279
8634
|
dryRun,
|
|
6280
8635
|
write,
|
|
6281
8636
|
importExisting,
|
|
8637
|
+
strategy,
|
|
8638
|
+
updateGitignore,
|
|
6282
8639
|
clients: uniqueClients(clients),
|
|
6283
8640
|
noClients: uniqueClients(noClients),
|
|
6284
8641
|
json,
|
|
6285
8642
|
quiet,
|
|
6286
|
-
help
|
|
8643
|
+
help,
|
|
8644
|
+
nonInteractive
|
|
6287
8645
|
};
|
|
6288
8646
|
}
|
|
8647
|
+
var WIZARD_BYPASS_FLAGS = /* @__PURE__ */ new Set([
|
|
8648
|
+
"--preset",
|
|
8649
|
+
"--dry-run",
|
|
8650
|
+
"--write",
|
|
8651
|
+
"--import",
|
|
8652
|
+
"--strategy",
|
|
8653
|
+
"--update-gitignore",
|
|
8654
|
+
"--client",
|
|
8655
|
+
"--no-client",
|
|
8656
|
+
"--profile",
|
|
8657
|
+
"--json",
|
|
8658
|
+
"--quiet"
|
|
8659
|
+
]);
|
|
8660
|
+
function isWizardEligibleArgs(args) {
|
|
8661
|
+
return !args.some((arg) => WIZARD_BYPASS_FLAGS.has(arg));
|
|
8662
|
+
}
|
|
6289
8663
|
function parseClientList(value, optionName) {
|
|
6290
8664
|
if (value.trim() === "") {
|
|
6291
8665
|
return {
|
|
@@ -6325,8 +8699,8 @@ function uniqueClients(clients) {
|
|
|
6325
8699
|
function parseUiArgs(args) {
|
|
6326
8700
|
let root = ".";
|
|
6327
8701
|
let host = DEFAULT_UI_HOST;
|
|
6328
|
-
let port =
|
|
6329
|
-
let open
|
|
8702
|
+
let port = "auto";
|
|
8703
|
+
let open;
|
|
6330
8704
|
let help = false;
|
|
6331
8705
|
for (let index = 0; index < args.length; index += 1) {
|
|
6332
8706
|
const arg = args[index];
|
|
@@ -6358,22 +8732,37 @@ function parseUiArgs(args) {
|
|
|
6358
8732
|
case "--port": {
|
|
6359
8733
|
const value = args[index + 1];
|
|
6360
8734
|
if (!value || value.startsWith("--")) {
|
|
6361
|
-
return {
|
|
8735
|
+
return {
|
|
8736
|
+
ok: false,
|
|
8737
|
+
message: "--port requires a number or the literal 'auto'."
|
|
8738
|
+
};
|
|
8739
|
+
}
|
|
8740
|
+
if (value === "auto") {
|
|
8741
|
+
port = "auto";
|
|
8742
|
+
index += 1;
|
|
8743
|
+
break;
|
|
6362
8744
|
}
|
|
6363
8745
|
const parsed = Number(value);
|
|
6364
8746
|
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
6365
8747
|
return {
|
|
6366
8748
|
ok: false,
|
|
6367
|
-
message: "--port must be an integer between 1 and 65535."
|
|
8749
|
+
message: "--port must be 'auto' or an integer between 1 and 65535."
|
|
6368
8750
|
};
|
|
6369
8751
|
}
|
|
6370
8752
|
port = parsed;
|
|
6371
8753
|
index += 1;
|
|
6372
8754
|
break;
|
|
6373
8755
|
}
|
|
6374
|
-
case "--open":
|
|
6375
|
-
|
|
8756
|
+
case "--open": {
|
|
8757
|
+
const value = args[index + 1];
|
|
8758
|
+
if (value === "true" || value === "false") {
|
|
8759
|
+
open = value === "true";
|
|
8760
|
+
index += 1;
|
|
8761
|
+
} else {
|
|
8762
|
+
open = true;
|
|
8763
|
+
}
|
|
6376
8764
|
break;
|
|
8765
|
+
}
|
|
6377
8766
|
case "--help":
|
|
6378
8767
|
case "-h":
|
|
6379
8768
|
help = true;
|
|
@@ -6414,7 +8803,7 @@ function renderInitialProfile(input) {
|
|
|
6414
8803
|
};
|
|
6415
8804
|
return `version: 1
|
|
6416
8805
|
profile:
|
|
6417
|
-
name: ${slugifyProfileName(
|
|
8806
|
+
name: ${slugifyProfileName(path6.basename(input.rootDir))}
|
|
6418
8807
|
description: Local AI-agent setup.
|
|
6419
8808
|
stack:
|
|
6420
8809
|
languages:
|
|
@@ -6474,6 +8863,11 @@ function createInitRefusal(input) {
|
|
|
6474
8863
|
}
|
|
6475
8864
|
function emitInitOutput(parsed, io, report) {
|
|
6476
8865
|
if (parsed.json) {
|
|
8866
|
+
if (parsed.importExisting && report.import) {
|
|
8867
|
+
io.stdout(`${JSON.stringify(report.import)}
|
|
8868
|
+
`);
|
|
8869
|
+
return;
|
|
8870
|
+
}
|
|
6477
8871
|
io.stdout(`${JSON.stringify(toInitJson(report))}
|
|
6478
8872
|
`);
|
|
6479
8873
|
return;
|
|
@@ -6502,6 +8896,9 @@ function toInitJson(report) {
|
|
|
6502
8896
|
if (report.error) {
|
|
6503
8897
|
summary.error = report.error;
|
|
6504
8898
|
}
|
|
8899
|
+
if (report.import) {
|
|
8900
|
+
summary.import = report.import;
|
|
8901
|
+
}
|
|
6505
8902
|
return summary;
|
|
6506
8903
|
}
|
|
6507
8904
|
function defaultClients() {
|
|
@@ -6623,7 +9020,8 @@ function slugifyProfileName(value) {
|
|
|
6623
9020
|
async function getProtectedGeneratedPaths(rootDir, files) {
|
|
6624
9021
|
const existingFiles = [];
|
|
6625
9022
|
for (const file of files) {
|
|
6626
|
-
|
|
9023
|
+
if (REGION_AWARE_PATHS.has(file.path)) continue;
|
|
9024
|
+
const current = await readOptionalBytes2(rootDir, file.path);
|
|
6627
9025
|
if (current) {
|
|
6628
9026
|
existingFiles.push({ file, bytes: current });
|
|
6629
9027
|
}
|
|
@@ -6631,7 +9029,7 @@ async function getProtectedGeneratedPaths(rootDir, files) {
|
|
|
6631
9029
|
if (existingFiles.length === 0) {
|
|
6632
9030
|
return [];
|
|
6633
9031
|
}
|
|
6634
|
-
const lockfileBytes = await
|
|
9032
|
+
const lockfileBytes = await readOptionalBytes2(rootDir, "ai-profile.lock");
|
|
6635
9033
|
if (!lockfileBytes) {
|
|
6636
9034
|
return existingFiles.map((item) => ({
|
|
6637
9035
|
path: item.file.path,
|
|
@@ -6647,8 +9045,9 @@ async function getProtectedGeneratedPaths(rootDir, files) {
|
|
|
6647
9045
|
reason: "invalid lockfile"
|
|
6648
9046
|
})).sort(compareProtectedPaths);
|
|
6649
9047
|
}
|
|
9048
|
+
const lockfileV2 = toLockfileV2View(lockfileResult.lockfile);
|
|
6650
9049
|
const outputsByPath = new Map(
|
|
6651
|
-
|
|
9050
|
+
lockfileV2.outputs.map((output) => [output.path, output])
|
|
6652
9051
|
);
|
|
6653
9052
|
const protectedPaths = [];
|
|
6654
9053
|
for (const item of existingFiles) {
|
|
@@ -6658,7 +9057,23 @@ async function getProtectedGeneratedPaths(rootDir, files) {
|
|
|
6658
9057
|
path: item.file.path,
|
|
6659
9058
|
reason: "missing lockfile entry"
|
|
6660
9059
|
});
|
|
6661
|
-
|
|
9060
|
+
continue;
|
|
9061
|
+
}
|
|
9062
|
+
if (lockOutput.ownership === "manual-owned") {
|
|
9063
|
+
continue;
|
|
9064
|
+
}
|
|
9065
|
+
if (lockOutput.ownership === "mixed") {
|
|
9066
|
+
const parsed = parseMixedFile(Buffer.from(item.bytes));
|
|
9067
|
+
const expectedHash = lockOutput.regions[0]?.sha256;
|
|
9068
|
+
if (!parsed.ok || !expectedHash || parsed.generatedInnerHash !== expectedHash) {
|
|
9069
|
+
protectedPaths.push({
|
|
9070
|
+
path: item.file.path,
|
|
9071
|
+
reason: "hash mismatch"
|
|
9072
|
+
});
|
|
9073
|
+
}
|
|
9074
|
+
continue;
|
|
9075
|
+
}
|
|
9076
|
+
if (sha256Hex(item.bytes) !== lockOutput.sha256) {
|
|
6662
9077
|
protectedPaths.push({
|
|
6663
9078
|
path: item.file.path,
|
|
6664
9079
|
reason: "hash mismatch"
|
|
@@ -6670,7 +9085,7 @@ async function getProtectedGeneratedPaths(rootDir, files) {
|
|
|
6670
9085
|
async function getGitignoreSuggestions(rootDir) {
|
|
6671
9086
|
let gitignore;
|
|
6672
9087
|
try {
|
|
6673
|
-
gitignore = await
|
|
9088
|
+
gitignore = await readOptionalText3(rootDir, ".gitignore");
|
|
6674
9089
|
} catch {
|
|
6675
9090
|
gitignore = void 0;
|
|
6676
9091
|
}
|
|
@@ -6689,19 +9104,13 @@ async function getGitignoreSuggestions(rootDir) {
|
|
|
6689
9104
|
}
|
|
6690
9105
|
return suggestions;
|
|
6691
9106
|
}
|
|
6692
|
-
function
|
|
6693
|
-
return files.map((file) => ({
|
|
6694
|
-
path: file.path,
|
|
6695
|
-
bytes: file.bytes
|
|
6696
|
-
}));
|
|
6697
|
-
}
|
|
6698
|
-
async function readOptionalBytes(rootDir, relativePath) {
|
|
9107
|
+
async function readOptionalBytes2(rootDir, relativePath) {
|
|
6699
9108
|
const safePath = safeOutputPath(relativePath);
|
|
6700
|
-
const rootRealPath = await
|
|
6701
|
-
const absolutePath =
|
|
9109
|
+
const rootRealPath = await fsPromises4.realpath(path6.resolve(rootDir));
|
|
9110
|
+
const absolutePath = path6.resolve(rootRealPath, ...safePath.split("/"));
|
|
6702
9111
|
try {
|
|
6703
9112
|
await assertReadPathContained(rootRealPath, absolutePath);
|
|
6704
|
-
return await
|
|
9113
|
+
return await fsPromises4.readFile(absolutePath);
|
|
6705
9114
|
} catch (error) {
|
|
6706
9115
|
if (isNodeError5(error) && error.code === "ENOENT") {
|
|
6707
9116
|
return void 0;
|
|
@@ -6736,6 +9145,29 @@ async function assertPortAvailable(host, port) {
|
|
|
6736
9145
|
server.listen(port, host);
|
|
6737
9146
|
});
|
|
6738
9147
|
}
|
|
9148
|
+
async function reserveEphemeralPort(host) {
|
|
9149
|
+
return new Promise((resolve) => {
|
|
9150
|
+
const server = createServer();
|
|
9151
|
+
server.once("error", () => resolve({ ok: false }));
|
|
9152
|
+
server.once("listening", () => {
|
|
9153
|
+
const address = server.address();
|
|
9154
|
+
if (address && typeof address === "object") {
|
|
9155
|
+
const port = address.port;
|
|
9156
|
+
server.close(() => resolve({ ok: true, port }));
|
|
9157
|
+
} else {
|
|
9158
|
+
server.close(() => resolve({ ok: false }));
|
|
9159
|
+
}
|
|
9160
|
+
});
|
|
9161
|
+
server.listen(0, host);
|
|
9162
|
+
});
|
|
9163
|
+
}
|
|
9164
|
+
function generateSessionToken() {
|
|
9165
|
+
return randomBytes(24).toString("base64url");
|
|
9166
|
+
}
|
|
9167
|
+
function isInteractiveTty(io) {
|
|
9168
|
+
if (io.stdout !== DEFAULT_IO.stdout) return false;
|
|
9169
|
+
return Boolean(process.stdout.isTTY) && process.env.CI !== "true";
|
|
9170
|
+
}
|
|
6739
9171
|
async function launchPublishedUi(request) {
|
|
6740
9172
|
let serverEntry;
|
|
6741
9173
|
try {
|
|
@@ -6750,6 +9182,7 @@ async function launchPublishedUi(request) {
|
|
|
6750
9182
|
env: {
|
|
6751
9183
|
...process.env,
|
|
6752
9184
|
AGENT_PROFILE_ROOT: request.rootDir,
|
|
9185
|
+
AGENT_PROFILE_SESSION_TOKEN: request.sessionToken,
|
|
6753
9186
|
HOST: request.host,
|
|
6754
9187
|
PORT: String(request.port)
|
|
6755
9188
|
},
|
|
@@ -6758,7 +9191,9 @@ async function launchPublishedUi(request) {
|
|
|
6758
9191
|
if (request.open) {
|
|
6759
9192
|
void waitForPortInUse(request.host, request.port).then((ready) => {
|
|
6760
9193
|
if (ready) {
|
|
6761
|
-
openInBrowser(
|
|
9194
|
+
openInBrowser(
|
|
9195
|
+
formatUiUrl(request.host, request.port, request.sessionToken)
|
|
9196
|
+
);
|
|
6762
9197
|
}
|
|
6763
9198
|
});
|
|
6764
9199
|
}
|
|
@@ -6795,8 +9230,10 @@ async function waitForPortInUse(host, port, timeoutMs = 5e3, intervalMs = 50) {
|
|
|
6795
9230
|
}
|
|
6796
9231
|
return false;
|
|
6797
9232
|
}
|
|
6798
|
-
function formatUiUrl(host, port) {
|
|
6799
|
-
|
|
9233
|
+
function formatUiUrl(host, port, sessionToken) {
|
|
9234
|
+
const base = `http://${host === "::1" ? "[::1]" : host}:${port}`;
|
|
9235
|
+
if (!sessionToken) return base;
|
|
9236
|
+
return `${base}/?session=${encodeURIComponent(sessionToken)}`;
|
|
6800
9237
|
}
|
|
6801
9238
|
function openInBrowser(url) {
|
|
6802
9239
|
const command = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
@@ -6811,8 +9248,8 @@ async function assertReadPathContained(rootRealPath, absolutePath) {
|
|
|
6811
9248
|
if (!isContainedBy3(rootRealPath, absolutePath)) {
|
|
6812
9249
|
throw new UnsafePathError("Read path escapes root.");
|
|
6813
9250
|
}
|
|
6814
|
-
await
|
|
6815
|
-
const targetRealPath = await
|
|
9251
|
+
await fsPromises4.lstat(absolutePath);
|
|
9252
|
+
const targetRealPath = await fsPromises4.realpath(absolutePath);
|
|
6816
9253
|
if (!isContainedBy3(rootRealPath, targetRealPath)) {
|
|
6817
9254
|
throw new UnsafePathError("Read path target escapes root.");
|
|
6818
9255
|
}
|
|
@@ -6826,11 +9263,11 @@ function compareProtectedPaths(left, right) {
|
|
|
6826
9263
|
return compareText5(left.path, right.path) || compareText5(left.reason, right.reason);
|
|
6827
9264
|
}
|
|
6828
9265
|
function isContainedBy3(rootPath, candidatePath) {
|
|
6829
|
-
const relative =
|
|
6830
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
9266
|
+
const relative = path6.relative(rootPath, candidatePath);
|
|
9267
|
+
return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
|
|
6831
9268
|
}
|
|
6832
|
-
async function
|
|
6833
|
-
const bytes = await
|
|
9269
|
+
async function readOptionalText3(rootDir, relativePath) {
|
|
9270
|
+
const bytes = await readOptionalBytes2(rootDir, relativePath);
|
|
6834
9271
|
return bytes ? Buffer.from(bytes).toString("utf8") : void 0;
|
|
6835
9272
|
}
|
|
6836
9273
|
function toSafeCliPath(value) {
|
|
@@ -6849,15 +9286,21 @@ function formatHelp() {
|
|
|
6849
9286
|
Usage:
|
|
6850
9287
|
agent-profile compile [--root <path>] [--profile <path>] [--target <id>] [--dry-run|--write] [--force]
|
|
6851
9288
|
agent-profile doctor [--root <path>] [--json]
|
|
6852
|
-
agent-profile init [--root <path>] [--profile <path>] [--import] [--preset <token>] [--client <list>] [--no-client <list>] [--json] [--quiet] [--dry-run|--write]
|
|
6853
|
-
agent-profile ui [--root <path>] [--host <host>] [--port
|
|
9289
|
+
agent-profile init [--root <path>] [--profile <path>] [--import] [--strategy preserve|regions] [--update-gitignore] [--preset <token>] [--client <list>] [--no-client <list>] [--non-interactive] [--json] [--quiet] [--dry-run|--write]
|
|
9290
|
+
agent-profile ui [--root <path>] [--host <host>] [--port auto|<number>] [--open true|false]
|
|
6854
9291
|
|
|
6855
9292
|
Commands:
|
|
6856
9293
|
compile Preview or write generated agent artifacts.
|
|
6857
9294
|
doctor Run local profile, lockfile, and permission checks.
|
|
6858
|
-
init Create a starting ai-profile.yaml.
|
|
9295
|
+
init Create a starting ai-profile.yaml (interactive wizard with no args).
|
|
6859
9296
|
ui Start the local read-only UI.
|
|
6860
9297
|
|
|
9298
|
+
Init wizard (Phase 15):
|
|
9299
|
+
Plain \`agent-profile init\` opens a friendly wizard that maps to Phase 14
|
|
9300
|
+
--import / --strategy / --update-gitignore / --write. In non-interactive
|
|
9301
|
+
environments (no TTY, CI=true, or --non-interactive), init defaults to
|
|
9302
|
+
--import --strategy preserve --dry-run and writes nothing.
|
|
9303
|
+
|
|
6861
9304
|
Init presets:
|
|
6862
9305
|
--preset verifies a short-lived hosted preset token offline. Repository
|
|
6863
9306
|
analysis happens locally and no source code is uploaded. Dry-run is the
|
|
@@ -6907,6 +9350,49 @@ function formatWritePlan(title, wrote, plan) {
|
|
|
6907
9350
|
return `${lines.join("\n").replace(/\n*$/u, "")}
|
|
6908
9351
|
`;
|
|
6909
9352
|
}
|
|
9353
|
+
function formatPhase14ImportReportLines(report) {
|
|
9354
|
+
const lines = ["", "Phase 14 import report:"];
|
|
9355
|
+
lines.push(` command: ${report.command}`);
|
|
9356
|
+
lines.push(` strategy: ${report.strategy}`);
|
|
9357
|
+
lines.push(` mode: ${report.mode}`);
|
|
9358
|
+
lines.push(` root: ${report.root}`);
|
|
9359
|
+
lines.push(` profile: ${report.profilePath}`);
|
|
9360
|
+
lines.push(
|
|
9361
|
+
` stack: languages=[${report.stack.languages.join(", ")}] frameworks=[${report.stack.frameworks.join(", ")}] packageManagers=[${report.stack.packageManagers.join(", ")}] testing=[${report.stack.testing.join(", ")}]`
|
|
9362
|
+
);
|
|
9363
|
+
if (report.files.length > 0) {
|
|
9364
|
+
lines.push(" files:");
|
|
9365
|
+
for (const finding of report.files) {
|
|
9366
|
+
const tags = finding.tags.length > 0 ? ` [${finding.tags.join(", ")}]` : "";
|
|
9367
|
+
lines.push(
|
|
9368
|
+
` - ${finding.path} (${finding.kind}, ${finding.ownership}, action=${finding.action})${tags}`
|
|
9369
|
+
);
|
|
9370
|
+
for (const note of finding.notes) {
|
|
9371
|
+
lines.push(` note: ${note}`);
|
|
9372
|
+
}
|
|
9373
|
+
}
|
|
9374
|
+
}
|
|
9375
|
+
if (report.gitignore.length > 0) {
|
|
9376
|
+
lines.push(" gitignore:");
|
|
9377
|
+
for (const finding of report.gitignore) {
|
|
9378
|
+
lines.push(
|
|
9379
|
+
` - ${finding.line}: ${finding.action} (${finding.reason})`
|
|
9380
|
+
);
|
|
9381
|
+
}
|
|
9382
|
+
}
|
|
9383
|
+
if (report.collisions.length > 0) {
|
|
9384
|
+
lines.push(" collisions:");
|
|
9385
|
+
for (const collision of report.collisions) {
|
|
9386
|
+
lines.push(
|
|
9387
|
+
` - ${collision.kind} name="${collision.name}" in [${collision.paths.join(", ")}]`
|
|
9388
|
+
);
|
|
9389
|
+
}
|
|
9390
|
+
}
|
|
9391
|
+
lines.push(
|
|
9392
|
+
` summary: wouldCreateProfile=${report.summary.wouldCreateProfile} wouldUpdateRegions=${report.summary.wouldUpdateRegions} preservedManualFiles=${report.summary.preservedManualFiles} conflicts=${report.summary.conflicts} nameCollisions=${report.summary.nameCollisions}`
|
|
9393
|
+
);
|
|
9394
|
+
return lines;
|
|
9395
|
+
}
|
|
6910
9396
|
function formatInitText(input) {
|
|
6911
9397
|
const lines = [];
|
|
6912
9398
|
if (input.preset !== void 0) {
|
|
@@ -6925,6 +9411,9 @@ function formatInitText(input) {
|
|
|
6925
9411
|
if (input.ignoredClientFlags) {
|
|
6926
9412
|
lines.push("client flags ignored: init does not edit existing profiles.");
|
|
6927
9413
|
}
|
|
9414
|
+
if (input.import) {
|
|
9415
|
+
lines.push(...formatPhase14ImportReportLines(input.import));
|
|
9416
|
+
}
|
|
6928
9417
|
lines.push(
|
|
6929
9418
|
"run `agent-profile compile --dry-run` to inspect compiled artifacts."
|
|
6930
9419
|
);
|
|
@@ -6958,6 +9447,9 @@ function formatInitText(input) {
|
|
|
6958
9447
|
lines.push(` .gitignore: add \`${suggestion}\``);
|
|
6959
9448
|
}
|
|
6960
9449
|
}
|
|
9450
|
+
if (input.import) {
|
|
9451
|
+
lines.push(...formatPhase14ImportReportLines(input.import));
|
|
9452
|
+
}
|
|
6961
9453
|
lines.push(
|
|
6962
9454
|
"",
|
|
6963
9455
|
input.wrote ? "run `agent-profile compile --dry-run` to inspect compiled artifacts." : "run `agent-profile init --write` to create the profile."
|
|
@@ -7046,6 +9538,134 @@ function compareText5(left, right) {
|
|
|
7046
9538
|
function isNodeError5(error) {
|
|
7047
9539
|
return error instanceof Error && "code" in error;
|
|
7048
9540
|
}
|
|
9541
|
+
async function planRegionAdoptions(rootDir, profile) {
|
|
9542
|
+
const compileResult = compileProfile({ profile });
|
|
9543
|
+
if (!compileResult.ok) return { adoptions: [], refusals: [] };
|
|
9544
|
+
const generatedBytesByPath = /* @__PURE__ */ new Map();
|
|
9545
|
+
for (const file of compileResult.files) {
|
|
9546
|
+
if (file.path !== "AGENTS.md" && file.path !== "CLAUDE.md") continue;
|
|
9547
|
+
generatedBytesByPath.set(file.path, file.bytes);
|
|
9548
|
+
}
|
|
9549
|
+
const outcomes = await planRootInstructionsAdoption(
|
|
9550
|
+
rootDir,
|
|
9551
|
+
generatedBytesByPath
|
|
9552
|
+
);
|
|
9553
|
+
const adoptions = [];
|
|
9554
|
+
const refusals = [];
|
|
9555
|
+
for (const outcome of outcomes) {
|
|
9556
|
+
if (outcome.ok) {
|
|
9557
|
+
adoptions.push({ path: outcome.path, bytes: outcome.bytes });
|
|
9558
|
+
continue;
|
|
9559
|
+
}
|
|
9560
|
+
if (outcome.reason === "missing-file" || outcome.reason === "missing-generated-bytes") {
|
|
9561
|
+
continue;
|
|
9562
|
+
}
|
|
9563
|
+
refusals.push({ path: outcome.path, reason: outcome.reason });
|
|
9564
|
+
}
|
|
9565
|
+
return { adoptions, refusals };
|
|
9566
|
+
}
|
|
9567
|
+
async function appendMissingGitignoreLines(rootDir, lines) {
|
|
9568
|
+
if (lines.length === 0) return;
|
|
9569
|
+
const existing = await readOptionalText3(rootDir, ".gitignore") ?? "";
|
|
9570
|
+
const trailing = existing.endsWith("\n") || existing === "" ? "" : "\n";
|
|
9571
|
+
const addition = `${lines.join("\n")}
|
|
9572
|
+
`;
|
|
9573
|
+
const next = `${existing}${trailing}${addition}`;
|
|
9574
|
+
await applyWritePlan({
|
|
9575
|
+
rootDir,
|
|
9576
|
+
writes: [{ path: ".gitignore", bytes: next }]
|
|
9577
|
+
});
|
|
9578
|
+
}
|
|
9579
|
+
function formatRegionAdoptionRefusals(refusals) {
|
|
9580
|
+
const lines = [
|
|
9581
|
+
"Refusing to adopt region-aware instruction files:",
|
|
9582
|
+
...refusals.map((item) => `- ${item.path} (${item.reason})`),
|
|
9583
|
+
"Repair the listed files (move, rename, or remove the symlink / partial markers) and re-run init --import --strategy regions --write.",
|
|
9584
|
+
""
|
|
9585
|
+
];
|
|
9586
|
+
return lines.join("\n");
|
|
9587
|
+
}
|
|
9588
|
+
var REGION_AWARE_PATHS = /* @__PURE__ */ new Set(["AGENTS.md", "CLAUDE.md"]);
|
|
9589
|
+
async function planRegionAwareWrites(rootDir, files, options = { force: false }) {
|
|
9590
|
+
const lockfile = await readLockfileForRegions(rootDir);
|
|
9591
|
+
const writes = [];
|
|
9592
|
+
const mixedOutputs = [];
|
|
9593
|
+
const refusals = [];
|
|
9594
|
+
for (const file of files) {
|
|
9595
|
+
if (!REGION_AWARE_PATHS.has(file.path)) {
|
|
9596
|
+
writes.push({ path: file.path, bytes: file.bytes });
|
|
9597
|
+
continue;
|
|
9598
|
+
}
|
|
9599
|
+
const existingRead = await readRegionAwareFile(rootDir, file.path);
|
|
9600
|
+
if (existingRead.refused) {
|
|
9601
|
+
refusals.push({ path: file.path, reason: "symlink" });
|
|
9602
|
+
continue;
|
|
9603
|
+
}
|
|
9604
|
+
const existing = existingRead.bytes;
|
|
9605
|
+
const lockOutput = lockfile?.outputs.find(
|
|
9606
|
+
(output) => output.path === file.path
|
|
9607
|
+
);
|
|
9608
|
+
if (!existing) {
|
|
9609
|
+
writes.push({ path: file.path, bytes: file.bytes });
|
|
9610
|
+
continue;
|
|
9611
|
+
}
|
|
9612
|
+
if (lockOutput?.ownership === "generated-owned") {
|
|
9613
|
+
if (!options.force && sha256Hex(existing) !== lockOutput.sha256) {
|
|
9614
|
+
refusals.push({ path: file.path, reason: "hash-mismatch" });
|
|
9615
|
+
continue;
|
|
9616
|
+
}
|
|
9617
|
+
writes.push({ path: file.path, bytes: file.bytes });
|
|
9618
|
+
continue;
|
|
9619
|
+
}
|
|
9620
|
+
if (lockOutput?.ownership === "mixed") {
|
|
9621
|
+
const buffer = Buffer.from(existing);
|
|
9622
|
+
if (!hasAllRegionMarkers(buffer)) {
|
|
9623
|
+
refusals.push({ path: file.path, reason: "partial-markers" });
|
|
9624
|
+
continue;
|
|
9625
|
+
}
|
|
9626
|
+
const generatedInner = generatedInnerBytesFor(file);
|
|
9627
|
+
const updated = replaceGeneratedRegion(buffer, generatedInner);
|
|
9628
|
+
if (!updated) {
|
|
9629
|
+
refusals.push({ path: file.path, reason: "duplicate-markers" });
|
|
9630
|
+
continue;
|
|
9631
|
+
}
|
|
9632
|
+
writes.push({ path: file.path, bytes: updated });
|
|
9633
|
+
mixedOutputs.push({
|
|
9634
|
+
path: file.path,
|
|
9635
|
+
target: file.target,
|
|
9636
|
+
templateId: file.templateId,
|
|
9637
|
+
regionHash: sha256Hex(generatedInner)
|
|
9638
|
+
});
|
|
9639
|
+
continue;
|
|
9640
|
+
}
|
|
9641
|
+
const existingBuffer = Buffer.from(existing);
|
|
9642
|
+
if (hasAllRegionMarkers(existingBuffer)) {
|
|
9643
|
+
const generatedInner = generatedInnerBytesFor(file);
|
|
9644
|
+
const updated = replaceGeneratedRegion(existingBuffer, generatedInner);
|
|
9645
|
+
if (!updated) {
|
|
9646
|
+
refusals.push({ path: file.path, reason: "duplicate-markers" });
|
|
9647
|
+
continue;
|
|
9648
|
+
}
|
|
9649
|
+
writes.push({ path: file.path, bytes: updated });
|
|
9650
|
+
mixedOutputs.push({
|
|
9651
|
+
path: file.path,
|
|
9652
|
+
target: file.target,
|
|
9653
|
+
templateId: file.templateId,
|
|
9654
|
+
regionHash: sha256Hex(generatedInner)
|
|
9655
|
+
});
|
|
9656
|
+
continue;
|
|
9657
|
+
}
|
|
9658
|
+
if (hasAnyRegionMarker(existingBuffer)) {
|
|
9659
|
+
refusals.push({ path: file.path, reason: "partial-markers" });
|
|
9660
|
+
continue;
|
|
9661
|
+
}
|
|
9662
|
+
refusals.push({ path: file.path, reason: "unknown-ownership" });
|
|
9663
|
+
}
|
|
9664
|
+
return { writes, mixedOutputs, refusals };
|
|
9665
|
+
}
|
|
9666
|
+
function generatedInnerBytesFor(file) {
|
|
9667
|
+
return Buffer.from(file.bytes);
|
|
9668
|
+
}
|
|
7049
9669
|
async function main() {
|
|
7050
9670
|
process.exitCode = await runCli();
|
|
7051
9671
|
}
|