@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.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import fsPromises3 from "node:fs/promises";
4
+ import fsPromises4 from "node:fs/promises";
5
5
  import { createRequire } from "node:module";
6
- import path5 from "node:path";
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(path6, target, templateId, text) {
35
- const safePath = safeOutputPath(path6);
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(path6) {
57
- if (path6.length === 0 || path6.includes("\\") || path6.startsWith("/") || /^[A-Za-z]:/u.test(path6)) {
58
- throw new Error(`Invalid generated output path: ${path6}`);
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 = path6.split("/");
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: ${path6}`);
65
+ throw new Error(`Invalid generated output path: ${path7}`);
65
66
  }
66
- return path6;
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: 1,
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(toLockOutput).sort(compareLockOutputs)
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
- validateLockfileObject(value, issues);
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 validateLockfileObject(value, issues) {
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
- validateOutputs(value.outputs, issues);
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 path6 = `/templates/${index}`;
276
+ const path7 = `/templates/${index}`;
217
277
  if (!isRecord(item)) {
218
- issues.push(schemaIssue(path6, "object", describeValue(item)));
278
+ issues.push(schemaIssue(path7, "object", describeValue(item)));
219
279
  return;
220
280
  }
221
- requireExactKeys(item, path6, ["id", "target", "version", "sha256"], issues);
222
- validateString(item.id, `${path6}/id`, issues);
223
- validateString(item.target, `${path6}/target`, issues);
224
- validateString(item.version, `${path6}/version`, issues);
225
- validateHash(item.sha256, `${path6}/sha256`, issues);
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: path6,
290
+ path: path7,
231
291
  expected: "templates sorted by id then target",
232
292
  actual: "out of order",
233
- message: `${path6} is not in deterministic order.`
293
+ message: `${path7} is not in deterministic order.`
234
294
  });
235
295
  }
236
296
  previous = current;
237
297
  });
238
298
  }
239
- function validateOutputs(value, issues) {
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 path6 = `/outputs/${index}`;
306
+ const path7 = `/outputs/${index}`;
247
307
  if (!isRecord(item)) {
248
- issues.push(schemaIssue(path6, "object", describeValue(item)));
308
+ issues.push(schemaIssue(path7, "object", describeValue(item)));
249
309
  return;
250
310
  }
251
311
  requireExactKeys(
252
312
  item,
253
- path6,
313
+ path7,
254
314
  ["path", "target", "templateId", "sha256"],
255
315
  issues
256
316
  );
257
- validatePath(item.path, `${path6}/path`, issues);
258
- validateString(item.target, `${path6}/target`, issues);
259
- validateString(item.templateId, `${path6}/templateId`, issues);
260
- validateHash(item.sha256, `${path6}/sha256`, issues);
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: path6,
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: `${path6} is not in deterministic order.`
407
+ message: `${path7} is not in deterministic order.`
269
408
  });
270
409
  }
271
410
  previous = current;
272
411
  });
273
412
  }
274
- function requireExactKeys(value, path6, keys, issues) {
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
- `${path6}/${escapeJsonPointerSegment(key)}`,
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
- `${path6}/${escapeJsonPointerSegment(key)}`,
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, path6, issues) {
478
+ function validateString(value, path7, issues) {
300
479
  if (typeof value !== "string") {
301
- issues.push(schemaIssue(path6, "string", describeValue(value)));
480
+ issues.push(schemaIssue(path7, "string", describeValue(value)));
302
481
  }
303
482
  }
304
- function validateHash(value, path6, issues) {
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: path6,
487
+ path: path7,
309
488
  expected: "lowercase sha256 hex",
310
489
  actual: describeValue(value),
311
- message: `${path6} must be a lowercase SHA-256 hex digest.`
490
+ message: `${path7} must be a lowercase SHA-256 hex digest.`
312
491
  });
313
492
  }
314
493
  }
315
- function validatePath(value, path6, issues) {
494
+ function validatePath(value, path7, issues) {
316
495
  if (typeof value !== "string") {
317
496
  issues.push(
318
- schemaIssue(path6, "repository-relative path", describeValue(value))
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: path6,
506
+ path: path7,
328
507
  expected: "repository-relative forward-slash path",
329
508
  actual: "unsafe path",
330
- message: `${path6} must be a safe repository-relative path.`
509
+ message: `${path7} must be a safe repository-relative path.`
331
510
  });
332
511
  }
333
512
  }
334
- function schemaIssue(path6, expected, actual) {
513
+ function schemaIssue(path7, expected, actual) {
335
514
  return {
336
515
  code: "lockfile_schema_error",
337
- path: path6,
516
+ path: path7,
338
517
  expected,
339
518
  actual,
340
- message: `${path6} does not match the lockfile schema.`
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 toLockOutput(file) {
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: safeOutputPath(file.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 path6 = getErrorPath(error);
1155
- const code = path6 === "/version" && hasOwnProperty(rootValue, "version") ? "unsupported_schema_version" : "schema_validation_error";
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: path6,
1579
+ path: path7,
1159
1580
  expected: getExpected(error),
1160
1581
  actual: getActual(error, rootValue),
1161
- message: getMessage(error, path6)
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, path6) {
1633
+ function getMessage(error, path7) {
1213
1634
  switch (error.keyword) {
1214
1635
  case "required":
1215
- return `${path6} is required.`;
1636
+ return `${path7} is required.`;
1216
1637
  case "additionalProperties":
1217
- return `${path6} is not allowed.`;
1638
+ return `${path7} is not allowed.`;
1218
1639
  case "const":
1219
- return `${path6} must match the supported constant.`;
1640
+ return `${path7} must match the supported constant.`;
1220
1641
  case "enum":
1221
- return `${path6} must be one of the supported values.`;
1642
+ return `${path7} must be one of the supported values.`;
1222
1643
  case "type":
1223
- return `${path6} has the wrong type.`;
1644
+ return `${path7} has the wrong type.`;
1224
1645
  default:
1225
- return `${path6} ${error.message ?? "is invalid"}.`;
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(getDefaultTemplates(), getSubagentTemplates(request.profile));
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(targets, request.profile);
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 === "allow") {
3631
+ if (effective.filesystem.write !== "deny") {
2957
3632
  tools.push("Edit", "Write");
2958
3633
  }
2959
- if (effective.shell.run === "allow") {
3634
+ if (effective.shell.run !== "deny") {
2960
3635
  tools.push("Bash");
2961
3636
  }
2962
- if (effective.network.external === "allow") {
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("<!-- Generated by Agent Profile Compiler. Do not edit by hand. -->");
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("<!-- Generated by Agent Profile Compiler. Do not edit by hand. -->");
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(getSubagentTemplateRefs(profile));
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
- if (await lstatOptional(current)) {
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/scanner/src/stack.ts
4213
+ // ../../packages/compiler/src/import-report.ts
3527
4214
  import fsPromises2 from "node:fs/promises";
3528
4215
  import path2 from "node:path";
3529
- import { parse as parseYaml } from "yaml";
3530
- var EMPTY_STACK = {
3531
- languages: [],
3532
- frameworks: [],
3533
- packageManagers: [],
3534
- testing: []
3535
- };
3536
- var VITE_CONFIGS = [
3537
- "vite.config.js",
3538
- "vite.config.mjs",
3539
- "vite.config.cjs",
3540
- "vite.config.ts",
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 SVELTE_CONFIGS = [
3545
- "svelte.config.js",
3546
- "svelte.config.mjs",
3547
- "svelte.config.cjs",
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 = path2.resolve(rootDir);
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 fsPromises2.readFile(path2.join(rootPath, "package.json"), "utf8")
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 fsPromises2.readFile(path2.join(rootPath, "pubspec.yaml"), "utf8")
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 fsPromises2.readFile(
3766
- path2.join(rootPath, relativePath),
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 fsPromises2.lstat(path2.join(rootPath, relativePath));
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 path3 from "node:path";
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(path3.resolve(rootDir));
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 readOptionalText(rootPath, relativePath, findings);
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 readOptionalText(rootPath, relativePath, findings) {
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 path3.join(rootPath, ...relativePath.split("/"));
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 = path3.relative(rootPath, candidatePath);
4032
- return relative === "" || !relative.startsWith("..") && !path3.isAbsolute(relative);
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 path4 from "node:path";
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 = path4.resolve(request.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
- if (lockfile) {
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
- if (actualOutput.templateId !== expectedOutput.templateId || actualOutput.sha256 !== expectedOutput.sha256) {
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
- `${expectedOutput.templateId}/${expectedOutput.sha256}`,
4510
- `${actualOutput.templateId}/${actualOutput.sha256}`,
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 !== "allow") {
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 ${effective.shell.run}.`,
5293
- "Remove Bash from the Claude subagent tools list or tighten effectivePermissions."
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 !== "allow") {
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 ${effective.filesystem.write}.`,
5306
- "Remove Edit/Write from the Claude subagent tools list or tighten effectivePermissions."
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 !== "allow") {
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 ${effective.network.external}.`,
5319
- "Remove WebFetch from the Claude subagent tools list or tighten effectivePermissions."
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(path4.join(rootDir, relativeRoot), {
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(path4.join(rootDir, relativeRoot), {
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(path4.join(rootDir, safePath))
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
- // src/index.ts
5651
- var DEFAULT_UI_HOST = "127.0.0.1";
5652
- var DEFAULT_UI_PORT = 5174;
5653
- var require2 = createRequire(import.meta.url);
5654
- var CLIENT_IDS = ["tabnine", "codex", "claude"];
5655
- var DEFAULT_IO = {
5656
- stdout: (text) => process.stdout.write(text),
5657
- stderr: (text) => process.stderr.write(text)
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 = path5.resolve(options.cwd ?? process.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
- rest,
5675
- cwd,
5676
- io,
5677
- options.presetNow,
5678
- options.presetVerificationKeys
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 = path5.resolve(cwd, parsed.root);
5702
- const portCheck = await assertPortAvailable(parsed.host, parsed.port);
5703
- if (!portCheck.ok) {
5704
- io.stderr(
5705
- `Port ${parsed.port} is not available on ${parsed.host}. Another process may be listening; choose a different port with --port <number>.
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
- return 1;
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 url = formatUiUrl(parsed.host, parsed.port);
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: parsed.port,
5725
- open: parsed.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: path5.resolve(cwd, parsed.root)
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 = path5.resolve(cwd, parsed.root);
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 readOptionalBytes(rootDir, safeProfilePath.path);
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 = toPlannedWrites([...compileResult.files, lockfile]);
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, presetNow, presetVerificationKeys) {
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 rootDir = path5.resolve(cwd, parsed.root);
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 readOptionalBytes(
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 readOptionalBytes(rootDir, safeProfilePath.path);
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 = DEFAULT_UI_PORT;
6329
- let open = false;
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 { ok: false, message: "--port requires a number." };
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
- open = true;
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(path5.basename(input.rootDir))}
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
- const current = await readOptionalBytes(rootDir, file.path);
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 readOptionalBytes(rootDir, "ai-profile.lock");
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
- lockfileResult.lockfile.outputs.map((output) => [output.path, output])
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
- } else if (sha256Hex(item.bytes) !== lockOutput.sha256) {
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 readOptionalText2(rootDir, ".gitignore");
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 toPlannedWrites(files) {
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 fsPromises3.realpath(path5.resolve(rootDir));
6701
- const absolutePath = path5.resolve(rootRealPath, ...safePath.split("/"));
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 fsPromises3.readFile(absolutePath);
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(formatUiUrl(request.host, request.port));
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
- return `http://${host === "::1" ? "[::1]" : host}:${port}`;
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 fsPromises3.lstat(absolutePath);
6815
- const targetRealPath = await fsPromises3.realpath(absolutePath);
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 = path5.relative(rootPath, candidatePath);
6830
- return relative === "" || !relative.startsWith("..") && !path5.isAbsolute(relative);
9266
+ const relative = path6.relative(rootPath, candidatePath);
9267
+ return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
6831
9268
  }
6832
- async function readOptionalText2(rootDir, relativePath) {
6833
- const bytes = await readOptionalBytes(rootDir, relativePath);
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 <number>] [--open]
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
  }