truthmark 2.2.7 → 2.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/main.js CHANGED
@@ -42,8 +42,14 @@ var renderJson = (result) => {
42
42
  return JSON.stringify(toStableValue(result), null, 2);
43
43
  };
44
44
 
45
- // src/config/command.ts
46
- import fs3 from "fs/promises";
45
+ // src/init/init.ts
46
+ import fs6 from "fs/promises";
47
+
48
+ // src/config/load.ts
49
+ import fs2 from "fs/promises";
50
+ import path2 from "path";
51
+ import { Ajv } from "ajv";
52
+ import { parse } from "yaml";
47
53
 
48
54
  // src/fs/paths.ts
49
55
  import path from "path";
@@ -54,6 +60,7 @@ var isPathInsideRoot = (rootDir, targetPath) => {
54
60
  var isNodeErrorWithCode = (error, code) => {
55
61
  return error instanceof Error && "code" in error && error.code === code;
56
62
  };
63
+ var isExcludedPath = (targetPath, excludedRoots) => excludedRoots.some((root) => isPathInsideRoot(root, targetPath));
57
64
  var pathSegments = (absolutePath) => {
58
65
  return absolutePath.split(path.sep);
59
66
  };
@@ -101,10 +108,15 @@ var resolveRepoPath = (rootDir, relativePath) => {
101
108
  }
102
109
  return resolvedPath;
103
110
  };
104
- var isSafeExactFile = async (rootDir, relativePath, allowMissing) => {
111
+ var isSafeExactFile = async (rootDir, relativePath, allowMissing, excludedRoots = []) => {
105
112
  try {
106
113
  const absolutePath = resolveRepoPath(rootDir, relativePath);
107
114
  await assertRepoContainment(rootDir, absolutePath);
115
+ if (isExcludedPath(
116
+ await resolveThroughExistingAncestor(absolutePath),
117
+ excludedRoots
118
+ ))
119
+ return false;
108
120
  const relative = path.relative(rootDir, absolutePath);
109
121
  const segments = pathSegments(relative);
110
122
  if (segments.length === 0 || segments.every((segment) => segment.length === 0)) {
@@ -138,6 +150,43 @@ var isSafeExactFile = async (rootDir, relativePath, allowMissing) => {
138
150
  return false;
139
151
  }
140
152
  };
153
+ var resolveSafeExactFileTarget = async (rootDir, relativePath, allowMissing, allowFinalSymlink, excludedRoots = []) => {
154
+ if (await isSafeExactFile(rootDir, relativePath, allowMissing, excludedRoots)) {
155
+ const absolutePath = resolveRepoPath(rootDir, relativePath);
156
+ try {
157
+ const [resolvedRootDir, resolvedPath] = await Promise.all([
158
+ fs.realpath(rootDir),
159
+ fs.realpath(absolutePath)
160
+ ]);
161
+ return {
162
+ path: toRepoRelativePath(resolvedRootDir, resolvedPath),
163
+ aliased: false
164
+ };
165
+ } catch (error) {
166
+ if (!allowMissing || !isNodeErrorWithCode(error, "ENOENT")) return null;
167
+ return {
168
+ path: toRepoRelativePath(rootDir, absolutePath),
169
+ aliased: false
170
+ };
171
+ }
172
+ }
173
+ if (!allowFinalSymlink) return null;
174
+ try {
175
+ const absolutePath = resolveRepoPath(rootDir, relativePath);
176
+ if (!(await fs.lstat(absolutePath)).isSymbolicLink()) return null;
177
+ const resolvedPath = await fs.realpath(absolutePath);
178
+ await assertRepoContainment(rootDir, resolvedPath);
179
+ if (isExcludedPath(resolvedPath, excludedRoots)) return null;
180
+ const stat = await fs.lstat(resolvedPath);
181
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) return null;
182
+ return {
183
+ path: toRepoRelativePath(await fs.realpath(rootDir), resolvedPath),
184
+ aliased: true
185
+ };
186
+ } catch {
187
+ return null;
188
+ }
189
+ };
141
190
  var assertRepoContainment = async (rootDir, targetPath) => {
142
191
  const [resolvedRootDir, resolvedTargetPath] = await Promise.all([
143
192
  resolveThroughExistingAncestor(rootDir),
@@ -213,81 +262,6 @@ var ensureRepoFile = async (rootDir, relativePath, content) => {
213
262
  };
214
263
  };
215
264
 
216
- // src/git/repository.ts
217
- import fs2 from "fs/promises";
218
- import { realpathSync } from "fs";
219
- import path2 from "path";
220
- import { execa } from "execa";
221
- var realpathOrResolved = async (targetPath) => {
222
- try {
223
- return await fs2.realpath(targetPath);
224
- } catch {
225
- return path2.resolve(targetPath);
226
- }
227
- };
228
- var runGit = async (cwd, args, reject = true) => {
229
- const result = await execa("git", args, { cwd, reject });
230
- return {
231
- stdout: result.stdout,
232
- exitCode: result.exitCode ?? 1
233
- };
234
- };
235
- var getGitRepository = async (cwd) => {
236
- const worktreePath = await realpathOrResolved(
237
- (await runGit(cwd, ["rev-parse", "--show-toplevel"])).stdout.trim()
238
- );
239
- const commonDirOutput = (await runGit(cwd, ["rev-parse", "--git-common-dir"])).stdout.trim();
240
- const commonDir = await realpathOrResolved(path2.resolve(worktreePath, commonDirOutput));
241
- const repositoryRoot = path2.basename(commonDir) === ".git" ? path2.dirname(commonDir) : worktreePath;
242
- const branchResult = await runGit(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"], false);
243
- const headResult = await runGit(cwd, ["rev-parse", "--verify", "HEAD"], false);
244
- const branchName = branchResult.exitCode === 0 ? branchResult.stdout.trim() : null;
245
- const headSha = headResult.exitCode === 0 ? headResult.stdout.trim() : null;
246
- const isDetached = branchName === null;
247
- const isUnborn = !isDetached && headSha === null;
248
- return {
249
- repositoryRoot,
250
- worktreePath,
251
- branchName,
252
- headSha,
253
- isDetached,
254
- isUnborn
255
- };
256
- };
257
- var resolveWorktreePath = (repository, relativePath) => {
258
- const resolvedPath = path2.resolve(repository.worktreePath, relativePath);
259
- let currentPath = resolvedPath;
260
- const missingSegments = [];
261
- const resolveContainedPath = () => {
262
- while (true) {
263
- try {
264
- return missingSegments.reduceRight((resolvedExistingPath, segment) => {
265
- return path2.join(resolvedExistingPath, segment);
266
- }, realpathSync(currentPath));
267
- } catch (error) {
268
- if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
269
- throw error;
270
- }
271
- const parentPath = path2.dirname(currentPath);
272
- if (parentPath === currentPath) {
273
- return resolvedPath;
274
- }
275
- missingSegments.unshift(path2.basename(currentPath));
276
- currentPath = parentPath;
277
- }
278
- }
279
- };
280
- const containedPath = resolveContainedPath();
281
- if (containedPath !== repository.worktreePath && !containedPath.startsWith(`${repository.worktreePath}${path2.sep}`)) {
282
- throw new Error("resolved path must stay inside the active worktree");
283
- }
284
- return resolvedPath;
285
- };
286
-
287
- // src/templates/init-files.ts
288
- import path3 from "path";
289
- import { stringify } from "yaml";
290
-
291
265
  // src/config/schema.ts
292
266
  var SUPPORTED_PLATFORMS = [
293
267
  "codex",
@@ -457,348 +431,276 @@ var createDefaultConfig = () => ({
457
431
  ignore: ["node_modules/**", "vendor/**", "dist/**", "build/**"]
458
432
  });
459
433
 
460
- // src/routing/areas.ts
461
- import { parse } from "yaml";
462
- var TRUTH_DOCUMENT_KINDS = [
463
- "product-capability",
464
- "engineering-behavior",
465
- "engineering-contract",
466
- "engineering-workflow",
467
- "engineering-architecture",
468
- "engineering-operations",
469
- "engineering-test-behavior"
470
- ];
471
- var uniqueSorted = (values) => [...new Set(values)].sort();
472
- var mergeTruthDocumentEntryRelationships = (first, second) => ({
473
- ...first,
474
- realizedBy: uniqueSorted([...first.realizedBy, ...second.realizedBy]),
475
- realizes: uniqueSorted([...first.realizes, ...second.realizes]),
476
- dependsOn: uniqueSorted([...first.dependsOn, ...second.dependsOn])
434
+ // src/config/load.ts
435
+ var ajv = new Ajv({ allErrors: true });
436
+ var validateTruthmarkConfig = ajv.compile(truthmarkConfigSchema);
437
+ var toConfigDiagnostic = (message, file) => ({
438
+ category: "config",
439
+ severity: "error",
440
+ message,
441
+ file
477
442
  });
478
- var DEFAULT_PRODUCT_TRUTH_DOCS_ROOT = "docs/truthmark/product";
479
- var DEFAULT_ENGINEERING_TRUTH_DOCS_ROOT = "docs/truthmark/engineering";
480
- var slugify = (value) => {
481
- return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
482
- };
483
- var createAreaDiagnostic = (message, area, severity = "error") => {
484
- return {
485
- category: "area-index",
486
- severity,
487
- message,
488
- area
489
- };
443
+ var normalizeRepoRelativePath = (value) => {
444
+ const slashNormalized = value.replace(/\\/gu, "/");
445
+ return path2.posix.normalize(slashNormalized).replace(/\/+$/u, "");
490
446
  };
491
- var parseListSection = (sectionLines) => {
492
- return sectionLines.map((line) => line.trim()).filter((line) => line.startsWith("- ")).map((line) => line.slice(2).trim().replaceAll("\\*", "*")).filter((line) => line.length > 0);
447
+ var isUnsafeRepoRelativePath = (value) => {
448
+ const slashNormalized = value.replace(/\\/gu, "/");
449
+ const normalized = normalizeRepoRelativePath(value);
450
+ const parts = slashNormalized.split("/");
451
+ return normalized.length === 0 || normalized === "." || normalized === ".." || path2.isAbsolute(value) || path2.posix.isAbsolute(slashNormalized) || path2.win32.isAbsolute(value) || /^[A-Za-z]:/u.test(value) || normalized.startsWith("../") || parts.includes("..");
493
452
  };
494
- var isTruthDocumentKind = (value) => {
495
- return typeof value === "string" && TRUTH_DOCUMENT_KINDS.includes(value);
453
+ var joinWorkspacePath = (workspace, childPath) => {
454
+ return normalizeRepoRelativePath(`${workspace}/${childPath}`);
496
455
  };
497
- var inferTruthDocumentKindFromPath = (documentPath, options = {}) => {
498
- const normalizedPath = documentPath.replaceAll("\\", "/");
499
- const productTruthRoot = (options.productTruthRoot ?? DEFAULT_PRODUCT_TRUTH_DOCS_ROOT)?.replaceAll("\\", "/").replace(/\/+$/u, "");
500
- const engineeringTruthRoot = (options.engineeringTruthRoot ?? options.truthDocsRoot ?? DEFAULT_ENGINEERING_TRUTH_DOCS_ROOT)?.replaceAll("\\", "/").replace(/\/+$/u, "");
501
- if (productTruthRoot && normalizedPath.startsWith(`${productTruthRoot}/`)) {
502
- return "product-capability";
503
- }
504
- if (engineeringTruthRoot && normalizedPath.startsWith(`${engineeringTruthRoot}/`)) {
505
- if (normalizedPath.includes("/contracts/")) return "engineering-contract";
506
- if (normalizedPath.includes("/workflows/")) return "engineering-workflow";
507
- if (normalizedPath.includes("/architecture/"))
508
- return "engineering-architecture";
509
- if (normalizedPath.includes("/operations/"))
510
- return "engineering-operations";
511
- if (normalizedPath.includes("/tests/")) return "engineering-test-behavior";
512
- return "engineering-behavior";
513
- }
514
- return null;
456
+ var portalOutputFor = (workspace) => joinWorkspacePath(workspace, DERIVED_TRUTHMARK_PATHS.portalOutput);
457
+ var pathsOverlap = (left, right) => {
458
+ const normalizedLeft = normalizeRepoRelativePath(left);
459
+ const normalizedRight = normalizeRepoRelativePath(right);
460
+ return normalizedLeft === normalizedRight || normalizedLeft.startsWith(`${normalizedRight}/`) || normalizedRight.startsWith(`${normalizedLeft}/`);
515
461
  };
516
- var inferTruthDocumentLaneFromPath = (documentPath, options = {}) => {
517
- const normalizedPath = documentPath.replaceAll("\\", "/");
518
- const productTruthRoot = (options.productTruthRoot ?? DEFAULT_PRODUCT_TRUTH_DOCS_ROOT).replaceAll("\\", "/").replace(/\/+$/u, "");
519
- const engineeringTruthRoot = (options.engineeringTruthRoot ?? options.truthDocsRoot ?? DEFAULT_ENGINEERING_TRUTH_DOCS_ROOT).replaceAll("\\", "/").replace(/\/+$/u, "");
520
- if (normalizedPath.startsWith(`${productTruthRoot}/`)) {
521
- return "product";
462
+ var CONFIG_PATH = ".truthmark/config.yml";
463
+ var FORBIDDEN_WORKSPACE_OVERLAPS = [
464
+ ".git",
465
+ ".truthmark",
466
+ "package.json",
467
+ "package-lock.json",
468
+ "pnpm-lock.yaml",
469
+ "yarn.lock",
470
+ "src",
471
+ "tests"
472
+ ];
473
+ var unsupportedShapeDiagnostics = (parsedConfig, configPath) => {
474
+ if (!parsedConfig || typeof parsedConfig !== "object" || Array.isArray(parsedConfig)) {
475
+ return [];
522
476
  }
523
- if (normalizedPath.startsWith(`${engineeringTruthRoot}/`)) {
524
- return "engineering";
477
+ const record = parsedConfig;
478
+ const diagnostics = [];
479
+ if (record.version !== 2) {
480
+ diagnostics.push(
481
+ toConfigDiagnostic(
482
+ "Unsupported Truthmark config shape. This release requires version: 2 with a truthmark workspace block.",
483
+ configPath
484
+ )
485
+ );
525
486
  }
526
- return null;
527
- };
528
- var laneForTruthDocumentKind = (kind) => {
529
- return kind.startsWith("product-") ? "product" : "engineering";
530
- };
531
- var docTypeForTruthDocumentKind = (kind) => {
532
- if (kind.startsWith("product-")) {
533
- return "product";
487
+ if ("docs" in record || "authority" in record) {
488
+ diagnostics.push(
489
+ toConfigDiagnostic(
490
+ "Unsupported Truthmark config shape. Remove old docs.roots and legacy authority settings; use version: 2 truthmark.workspace paths.",
491
+ configPath
492
+ )
493
+ );
534
494
  }
535
- return kind.slice("engineering-".length);
495
+ return diagnostics;
536
496
  };
537
- var parseStringListField = (rawEntry, field) => {
538
- if (!rawEntry || typeof rawEntry !== "object" || !(field in rawEntry)) {
539
- return [];
540
- }
541
- const value = rawEntry[field];
542
- if (!Array.isArray(value)) {
543
- return [];
497
+ var validateWorkspacePaths = (rawConfig, configPath) => {
498
+ const diagnostics = [];
499
+ const workspace = normalizeRepoRelativePath(rawConfig.truthmark.workspace);
500
+ if (isUnsafeRepoRelativePath(rawConfig.truthmark.workspace) || FORBIDDEN_WORKSPACE_OVERLAPS.some(
501
+ (forbidden) => pathsOverlap(workspace, forbidden)
502
+ )) {
503
+ diagnostics.push(
504
+ toConfigDiagnostic(
505
+ "truthmark.workspace must be a non-empty repo-relative directory that does not overlap repository control, package, source, test, or instruction paths.",
506
+ configPath
507
+ )
508
+ );
544
509
  }
545
- return value.filter((entry) => typeof entry === "string");
510
+ return diagnostics;
546
511
  };
547
- var findTruthDocumentsYamlFenceRange = (sectionLines) => {
548
- const trimmedLines = sectionLines.map((line) => line.trim());
549
- const openingFenceIndex = trimmedLines.findIndex(
550
- (line) => /^```(?:yaml|yml)?$/u.test(line)
512
+ var normalizeConfig = (rawConfig) => {
513
+ const workspace = normalizeRepoRelativePath(rawConfig.truthmark.workspace);
514
+ const routesIndex = joinWorkspacePath(
515
+ workspace,
516
+ DERIVED_TRUTHMARK_PATHS.routesIndex
551
517
  );
552
- if (openingFenceIndex === -1) {
553
- return null;
554
- }
555
- const closingFenceIndex = trimmedLines.findIndex(
556
- (line, index) => index > openingFenceIndex && line === "```"
518
+ const routeAreasRoot = joinWorkspacePath(
519
+ workspace,
520
+ DERIVED_TRUTHMARK_PATHS.routeAreasRoot
521
+ );
522
+ const productTruthRoot = joinWorkspacePath(
523
+ workspace,
524
+ DERIVED_TRUTHMARK_PATHS.productTruthRoot
525
+ );
526
+ const engineeringTruthRoot = joinWorkspacePath(
527
+ workspace,
528
+ DERIVED_TRUTHMARK_PATHS.engineeringTruthRoot
529
+ );
530
+ const templatesRoot = joinWorkspacePath(
531
+ workspace,
532
+ DERIVED_TRUTHMARK_PATHS.templatesRoot
533
+ );
534
+ const portalOutput = portalOutputFor(workspace);
535
+ const portalTemplate = joinWorkspacePath(
536
+ workspace,
537
+ DERIVED_TRUTHMARK_PATHS.portalTemplate
557
538
  );
558
539
  return {
559
- openingFenceIndex,
560
- closingFenceIndex: closingFenceIndex === -1 ? null : closingFenceIndex
561
- };
562
- };
563
- var parseTruthDocumentsFromList = (sectionLines, areaName, options) => {
564
- const diagnostics = [];
565
- const truthDocuments = parseListSection(sectionLines);
566
- const truthDocumentEntries = truthDocuments.map((documentPath) => {
567
- const inferredKind = inferTruthDocumentKindFromPath(documentPath, options);
568
- const inferredLane = inferTruthDocumentLaneFromPath(documentPath, options);
569
- if (!inferredKind) {
570
- diagnostics.push(
571
- createAreaDiagnostic(
572
- `Truth document ${documentPath} does not match a known kind path convention; defaulting to behavior.`,
573
- areaName,
574
- "review"
575
- )
576
- );
577
- }
578
- return {
579
- path: documentPath,
580
- kind: inferredKind ?? "engineering-behavior",
581
- kindSource: inferredKind ? "inferred" : "defaulted",
582
- lane: inferredLane ?? "engineering",
583
- laneSource: inferredLane ? "inferred" : "defaulted",
584
- realizedBy: [],
585
- realizes: [],
586
- dependsOn: []
587
- };
588
- });
589
- return {
590
- truthDocuments,
591
- truthDocumentEntries,
592
- diagnostics
540
+ version: rawConfig.version,
541
+ platforms: rawConfig.platforms ?? [...DEFAULT_PLATFORMS],
542
+ truthmark: {
543
+ workspace,
544
+ routes: {
545
+ index: DERIVED_TRUTHMARK_PATHS.routesIndex,
546
+ areas: DERIVED_TRUTHMARK_PATHS.routeAreasRoot,
547
+ defaultArea: DERIVED_TRUTHMARK_PATHS.defaultArea,
548
+ maxDelegationDepth: DERIVED_TRUTHMARK_PATHS.maxDelegationDepth
549
+ },
550
+ truth: {
551
+ productRoot: DERIVED_TRUTHMARK_PATHS.productTruthRoot,
552
+ engineeringRoot: DERIVED_TRUTHMARK_PATHS.engineeringTruthRoot
553
+ },
554
+ templates: {
555
+ root: DERIVED_TRUTHMARK_PATHS.templatesRoot
556
+ },
557
+ generated: {
558
+ portal: {
559
+ enabled: rawConfig.truthmark.generated.portal.enabled
560
+ }
561
+ },
562
+ paths: {
563
+ routesIndex,
564
+ routeAreasRoot,
565
+ productTruthRoot,
566
+ engineeringTruthRoot,
567
+ templatesRoot,
568
+ portalOutput,
569
+ portalTemplate
570
+ },
571
+ controlledPaths: [
572
+ routesIndex,
573
+ `${routeAreasRoot}/**/*.md`,
574
+ `${productTruthRoot}/**/*.md`,
575
+ `${engineeringTruthRoot}/**/*.md`,
576
+ `${templatesRoot}/*.md`
577
+ ]
578
+ },
579
+ frontmatter: {
580
+ required: rawConfig.frontmatter?.required ?? [],
581
+ recommended: rawConfig.frontmatter?.recommended ?? []
582
+ },
583
+ ignore: rawConfig.ignore ?? []
593
584
  };
594
585
  };
595
- var parseTruthDocumentsFromYaml = (sectionLines, areaName, options) => {
596
- const yamlFenceRange = findTruthDocumentsYamlFenceRange(sectionLines);
597
- if (!yamlFenceRange) {
598
- return {
599
- truthDocuments: [],
600
- truthDocumentEntries: [],
601
- diagnostics: []
602
- };
586
+ var compatibilityDiagnostics = (rawConfig, configPath) => {
587
+ if (!("instruction_targets" in rawConfig)) {
588
+ return [];
603
589
  }
604
- if (yamlFenceRange.closingFenceIndex === null) {
605
- return {
606
- truthDocuments: [],
607
- truthDocumentEntries: [],
608
- diagnostics: [
609
- createAreaDiagnostic(
610
- `Area ${areaName} has an unterminated fenced YAML Truth documents block.`,
611
- areaName
612
- )
613
- ]
614
- };
590
+ return [
591
+ {
592
+ category: "config",
593
+ severity: "review",
594
+ message: "instruction_targets is accepted for compatibility but ignored; select platforms to control managed instruction-file writes.",
595
+ file: configPath
596
+ }
597
+ ];
598
+ };
599
+ var loadConfig = async (rootDir) => {
600
+ const absolutePath = resolveRepoPath(rootDir, CONFIG_PATH);
601
+ let source;
602
+ try {
603
+ source = await fs2.readFile(absolutePath, "utf8");
604
+ } catch (error) {
605
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
606
+ return {
607
+ status: "missing",
608
+ config: null,
609
+ diagnostics: [
610
+ toConfigDiagnostic("Missing .truthmark/config.yml.", CONFIG_PATH)
611
+ ],
612
+ configPath: CONFIG_PATH
613
+ };
614
+ }
615
+ throw error;
615
616
  }
616
- let parsedBlock;
617
+ let parsedConfig;
617
618
  try {
618
- parsedBlock = parse(
619
- sectionLines.slice(
620
- yamlFenceRange.openingFenceIndex + 1,
621
- yamlFenceRange.closingFenceIndex
622
- ).join("\n")
623
- );
619
+ parsedConfig = parse(source);
624
620
  } catch (error) {
625
621
  return {
626
- truthDocuments: [],
627
- truthDocumentEntries: [],
622
+ status: "invalid",
623
+ config: null,
628
624
  diagnostics: [
629
- createAreaDiagnostic(
630
- `Area ${areaName} has invalid YAML truth document metadata: ${error instanceof Error ? error.message : String(error)}.`,
631
- areaName
625
+ toConfigDiagnostic(
626
+ `Invalid YAML: ${error instanceof Error ? error.message : String(error)}`,
627
+ CONFIG_PATH
632
628
  )
633
- ]
629
+ ],
630
+ configPath: CONFIG_PATH
634
631
  };
635
632
  }
636
- const rawEntries = parsedBlock && typeof parsedBlock === "object" && "truth_documents" in parsedBlock ? parsedBlock.truth_documents : null;
637
- if (!Array.isArray(rawEntries)) {
633
+ const unsupportedDiagnostics = unsupportedShapeDiagnostics(
634
+ parsedConfig,
635
+ CONFIG_PATH
636
+ );
637
+ if (unsupportedDiagnostics.length > 0) {
638
638
  return {
639
- truthDocuments: [],
640
- truthDocumentEntries: [],
641
- diagnostics: [
642
- createAreaDiagnostic(
643
- `Area ${areaName} must define a truth_documents array inside the fenced YAML block.`,
644
- areaName
645
- )
646
- ]
639
+ status: "invalid",
640
+ config: null,
641
+ diagnostics: unsupportedDiagnostics,
642
+ configPath: CONFIG_PATH
647
643
  };
648
644
  }
649
- const diagnostics = [];
650
- const truthDocumentEntries = [];
651
- for (const rawEntry of rawEntries) {
652
- const path13 = rawEntry && typeof rawEntry === "object" && "path" in rawEntry ? rawEntry.path : null;
653
- const kind = rawEntry && typeof rawEntry === "object" && "kind" in rawEntry ? rawEntry.kind : null;
654
- const lane = rawEntry && typeof rawEntry === "object" && "lane" in rawEntry ? rawEntry.lane : null;
655
- const inferredKind = typeof path13 === "string" ? inferTruthDocumentKindFromPath(path13, options) : null;
656
- const inferredLane = typeof path13 === "string" ? inferTruthDocumentLaneFromPath(path13, options) : null;
657
- const normalizedKind = isTruthDocumentKind(kind) ? kind : inferredKind;
658
- const normalizedLane = lane === "product" || lane === "engineering" ? lane : normalizedKind ? laneForTruthDocumentKind(normalizedKind) : inferredLane;
659
- if (typeof path13 !== "string" || path13.trim().length === 0 || !normalizedKind || !normalizedLane) {
660
- diagnostics.push(
661
- createAreaDiagnostic(
662
- `Area ${areaName} truth_documents entries must include non-empty path plus valid lane and kind fields.`,
663
- areaName
664
- )
665
- );
666
- continue;
667
- }
668
- truthDocumentEntries.push({
669
- path: path13.trim(),
670
- kind: normalizedKind,
671
- kindSource: isTruthDocumentKind(kind) ? "explicit" : "inferred",
672
- lane: normalizedLane,
673
- laneSource: lane === "product" || lane === "engineering" ? "explicit" : "inferred",
674
- realizedBy: parseStringListField(rawEntry, "realized_by"),
675
- realizes: parseStringListField(rawEntry, "realizes"),
676
- dependsOn: parseStringListField(rawEntry, "depends_on")
677
- });
645
+ if (!validateTruthmarkConfig(parsedConfig)) {
646
+ return {
647
+ status: "invalid",
648
+ config: null,
649
+ diagnostics: (validateTruthmarkConfig.errors ?? []).map(
650
+ (error) => {
651
+ const propertyPath = error.instancePath || "/";
652
+ const additionalProperty = error.keyword === "additionalProperties" && error.params && "additionalProperty" in error.params ? String(error.params.additionalProperty) : null;
653
+ const message = additionalProperty ? `${propertyPath} additional property ${additionalProperty} is not allowed` : `${propertyPath} ${error.message ?? "is invalid"}`.trim();
654
+ return toConfigDiagnostic(message, CONFIG_PATH);
655
+ }
656
+ ),
657
+ configPath: CONFIG_PATH
658
+ };
659
+ }
660
+ const pathDiagnostics = validateWorkspacePaths(
661
+ parsedConfig,
662
+ CONFIG_PATH
663
+ );
664
+ if (pathDiagnostics.length > 0) {
665
+ return {
666
+ status: "invalid",
667
+ config: null,
668
+ diagnostics: pathDiagnostics,
669
+ configPath: CONFIG_PATH
670
+ };
678
671
  }
679
672
  return {
680
- truthDocuments: truthDocumentEntries.map((entry) => entry.path),
681
- truthDocumentEntries,
682
- diagnostics
673
+ status: "loaded",
674
+ config: normalizeConfig(parsedConfig),
675
+ diagnostics: compatibilityDiagnostics(
676
+ parsedConfig,
677
+ CONFIG_PATH
678
+ ),
679
+ configPath: CONFIG_PATH
683
680
  };
684
681
  };
685
- var parseTruthDocumentsSection = (sectionLines, areaName, options) => {
686
- const yamlFenceRange = findTruthDocumentsYamlFenceRange(sectionLines);
687
- if (!yamlFenceRange) {
688
- return parseTruthDocumentsFromList(sectionLines, areaName, options);
689
- }
690
- const yamlResult = parseTruthDocumentsFromYaml(
691
- sectionLines,
692
- areaName,
693
- options
694
- );
695
- if (yamlResult.diagnostics.length > 0 || yamlFenceRange.closingFenceIndex === null) {
696
- return yamlResult;
697
- }
698
- return yamlResult;
682
+
683
+ // src/config/render.ts
684
+ import { parse as parse2, parseDocument, stringify } from "yaml";
685
+ var normalizePlatforms = (platforms) => {
686
+ const selected = new Set(platforms);
687
+ return SUPPORTED_PLATFORMS.filter((platform) => selected.has(platform));
699
688
  };
700
- var parseAreasMarkdown = (source, options = {}) => {
701
- const lines = source.split("\n");
702
- const diagnostics = [];
703
- const areas = [];
704
- const truthDocumentReferences = [];
705
- const areaFileReferences = [];
706
- let areaIndex = 0;
707
- let currentAreaName = null;
708
- let currentSections = /* @__PURE__ */ new Map();
709
- let currentSectionName = null;
710
- const flushArea = () => {
711
- if (!currentAreaName) {
712
- return;
713
- }
714
- const truthDocumentResult = parseTruthDocumentsSection(
715
- currentSections.get("Truth documents") ?? [],
716
- currentAreaName,
717
- options
718
- );
719
- const { truthDocuments, truthDocumentEntries } = truthDocumentResult;
720
- const areaFiles = parseListSection(currentSections.get("Area files") ?? []);
721
- const codeSurface = parseListSection(
722
- currentSections.get("Code surface") ?? []
723
- );
724
- const updateTruthWhen = parseListSection(
725
- currentSections.get("Update truth when") ?? []
726
- );
727
- const areaKey = slugify(currentAreaName);
728
- const areaId = areaKey.length > 0 ? areaKey : `area-${areaIndex}`;
729
- const hasTruthDocuments = truthDocuments.length > 0;
730
- const hasAreaFiles = areaFiles.length > 0;
731
- areaIndex += 1;
732
- diagnostics.push(...truthDocumentResult.diagnostics);
733
- if (hasTruthDocuments) {
734
- truthDocumentReferences.push({
735
- id: areaId,
736
- name: currentAreaName,
737
- key: areaKey,
738
- truthDocuments,
739
- truthDocumentEntries
740
- });
741
- }
742
- if (hasTruthDocuments === hasAreaFiles || codeSurface.length === 0 || updateTruthWhen.length === 0) {
743
- diagnostics.push(
744
- createAreaDiagnostic(
745
- `Area ${currentAreaName} must define exactly one of Truth documents or Area files, plus Code surface and Update truth when sections.`,
746
- currentAreaName
747
- )
748
- );
749
- } else if (hasAreaFiles) {
750
- areaFileReferences.push({
751
- id: areaId,
752
- name: currentAreaName,
753
- key: areaKey,
754
- areaFiles,
755
- codeSurface,
756
- updateTruthWhen
757
- });
758
- } else {
759
- areas.push({
760
- id: areaId,
761
- name: currentAreaName,
762
- key: areaKey,
763
- truthDocuments,
764
- truthDocumentEntries,
765
- codeSurface,
766
- updateTruthWhen
767
- });
768
- }
769
- currentAreaName = null;
770
- currentSections = /* @__PURE__ */ new Map();
771
- currentSectionName = null;
772
- };
773
- for (const line of lines) {
774
- const areaHeadingMatch = line.match(/^\s{0,3}##\s+(.*)$/u);
775
- if (areaHeadingMatch) {
776
- const heading = areaHeadingMatch[1]?.trim() ?? null;
777
- flushArea();
778
- currentAreaName = heading === "Source References" ? null : heading;
779
- continue;
780
- }
781
- if (!currentAreaName) {
782
- continue;
783
- }
784
- if (/^(Truth documents|Area files|Code surface|Update truth when):$/u.test(
785
- line.trim()
786
- )) {
787
- currentSectionName = line.trim().slice(0, -1);
788
- currentSections.set(currentSectionName, []);
789
- continue;
790
- }
791
- if (currentSectionName) {
792
- currentSections.get(currentSectionName)?.push(line);
793
- }
794
- }
795
- flushArea();
796
- return {
797
- areas,
798
- truthDocumentReferences,
799
- areaFileReferences,
800
- diagnostics
801
- };
689
+ var renderConfig = (platforms = []) => {
690
+ const normalized = normalizePlatforms(platforms);
691
+ const config = createDefaultRawConfig();
692
+ if (normalized.length > 0) config.platforms = normalized;
693
+ return stringify(config);
694
+ };
695
+ var updateConfigPlatforms = (source, platforms) => {
696
+ const normalized = normalizePlatforms(platforms);
697
+ const parsed = parse2(source);
698
+ if (JSON.stringify(normalizePlatforms(parsed.platforms ?? [])) === JSON.stringify(normalized))
699
+ return source;
700
+ const document = parseDocument(source);
701
+ if (normalized.length === 0) document.delete("platforms");
702
+ else document.set("platforms", normalized);
703
+ return document.toString();
802
704
  };
803
705
 
804
706
  // src/truth/docs.ts
@@ -812,1521 +714,1614 @@ var resolveEngineeringTruthRoot = (config) => {
812
714
  return config.truthmark.paths.engineeringTruthRoot;
813
715
  };
814
716
 
815
- // src/templates/init-files.ts
816
- var asRelativePath = (value) => {
817
- return value.split(path3.sep).join("/");
818
- };
819
- var currentDate = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
820
- var resolveRelativePath = (fromPath, toPath) => {
821
- return asRelativePath(path3.relative(path3.dirname(fromPath), toPath));
717
+ // src/truth/evidence.ts
718
+ var renderClaimEvidenceCheckedSection = (items) => {
719
+ return [
720
+ "Evidence checked:",
721
+ ...items.map((item) => {
722
+ return [
723
+ `- Claim: ${item.claim}`,
724
+ ` Evidence: ${item.evidence.join(" / ")}`,
725
+ ` Result: ${item.result}`
726
+ ].join("\n");
727
+ })
728
+ ].join("\n");
822
729
  };
823
- var truthRoot = resolveEngineeringTruthRoot;
824
- var renderLaneRootReadmeSummary = (lane) => {
825
- if (lane === "product") {
826
- return [
827
- "Product truth owns capability promises, boundaries, decisions, and acceptance criteria.",
828
- "Product lane docs state what must be true, why it matters, and what success means."
829
- ].join(" ");
830
- }
730
+ var renderAuditEvidenceCheckedSection = (items) => {
831
731
  return [
832
- "Engineering truth owns current realization, contracts, architecture, workflows, operations, and tests.",
833
- "Engineering lane docs describe how the repository currently implements and operates the behavior."
834
- ].join(" ");
732
+ "Evidence checked:",
733
+ ...items.map((item) => {
734
+ return [
735
+ `- Finding: ${item.finding}`,
736
+ ` Evidence: ${item.evidence.join(" / ")}`,
737
+ ` Suggested fix: ${item.suggestedFix}`,
738
+ ` Confidence: ${item.confidence}`
739
+ ].join("\n");
740
+ })
741
+ ].join("\n");
835
742
  };
836
- var renderLaneRootLeafDocGuidance = (lane) => {
837
- if (lane === "product") {
838
- return "README.md files are indexes, not Truth Sync targets. Keep product truth in bounded capability docs.";
839
- }
840
- return "README.md files are indexes, not Truth Sync targets. Keep engineering truth in bounded behavior, contract, architecture, workflow, operations, and test docs.";
743
+
744
+ // src/agents/shared.ts
745
+ var renderBulletLine = (line) => {
746
+ const normalized = line.trim().replace(/^-\s*/u, "");
747
+ return `- ${normalized}`;
841
748
  };
842
- var renderConfigTemplate = () => {
843
- return stringify(createDefaultRawConfig());
749
+ var renderBulletBlock = (lines, indent = " ") => {
750
+ return lines.split(/\n/u).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => `${indent}${renderBulletLine(line)}`).join("\n");
844
751
  };
845
- var titleCase = (value) => {
846
- return value.split(/[-_\s]+/u).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
752
+ var renderLaneClassificationRuleBlock = (config = defaultAgentConfig(), indent = " ") => {
753
+ const [, ...rules] = renderLaneClassificationInstructions(config).split(/\n/u);
754
+ return renderBulletBlock(rules.join("\n"), indent);
847
755
  };
848
- var renderHierarchicalAreasIndexTemplate = (config) => {
849
- const defaultArea = config.truthmark.routes.defaultArea;
850
- const childPath = `${config.truthmark.paths.routeAreasRoot}/${defaultArea}.md`;
851
- const title = titleCase(defaultArea);
852
- const sourceOfTruth = resolveRelativePath(
853
- config.truthmark.paths.routesIndex,
854
- ".truthmark/config.yml"
756
+ var renderReadOnlyLaneClassificationRuleBlock = (config = defaultAgentConfig(), indent = " ") => {
757
+ const productTruthRoot = resolveProductTruthRoot(config);
758
+ const engineeringTruthRoot = resolveEngineeringTruthRoot(config);
759
+ return renderBulletBlock(
760
+ [
761
+ "classify the request or changed surface as product-lane, engineering-lane, both-lane, or ambiguous for reporting only",
762
+ `product-lane ownership belongs under ${productTruthRoot} and describes product promises, boundaries, rationale, decisions, and success criteria`,
763
+ `engineering-lane ownership belongs under ${engineeringTruthRoot} and describes source-backed current realization, contracts, architecture, workflows, operations, or tests`,
764
+ "both-lane ownership uses separate product and engineering docs cross-linked in route YAML with realized_by and realizes, not in doc frontmatter",
765
+ "ambiguous lane ownership should be reported for manual handoff or routed to Truth Structure",
766
+ LANE_INVARIANT
767
+ ].join("\n"),
768
+ indent
855
769
  );
856
- return [
857
- "---",
858
- "status: active",
859
- "doc_type: route-index",
860
- `last_reviewed: ${currentDate()}`,
861
- "---",
862
- "",
863
- "# Truthmark Areas",
864
- "",
865
- `## ${title}`,
866
- "",
867
- "Area files:",
868
- `- ${childPath}`,
869
- "",
870
- "Code surface:",
871
- "- src/**",
872
- "",
873
- "Update truth when:",
874
- "- behavior changes affect the routed truth documents",
875
- "- API contracts or current feature behavior changes",
876
- "",
877
- "## Source References",
878
- "",
879
- `- ${sourceOfTruth}`,
880
- ""
881
- ].join("\n");
882
770
  };
883
- var renderChildAreaTemplate = (config) => {
884
- const defaultArea = config.truthmark.routes.defaultArea;
885
- const title = titleCase(defaultArea);
886
- const truthDocsRoot = truthRoot(config);
887
- const bootstrapTruthDoc = `${truthDocsRoot}/${defaultArea}/bootstrap-routing.md`;
888
- const templatePath = `${config.truthmark.paths.routeAreasRoot}/${defaultArea}.md`;
889
- const sourceOfTruth = resolveRelativePath(
890
- templatePath,
891
- ".truthmark/config.yml"
892
- );
771
+ var DECISION_TRUTH_INSTRUCTIONS = [
772
+ "Decision truth lives in the canonical doc it governs; date active decisions inline when added or changed.",
773
+ "Do not create separate active-decision ADR/planning logs; replace the active decision and let Git history carry the audit trail.",
774
+ "Product decisions belong in product truth; engineering, architecture, contract, workflow, and operational decisions belong in engineering truth."
775
+ ].join("\n");
776
+ var LANE_INVARIANT = "Do not make product docs a summary of engineering docs. Do not make engineering docs a detailed version of product docs. Product truth says what must be true and why. Engineering truth says how the repository currently realizes it.";
777
+ var renderLaneClassificationInstructions = (config = defaultAgentConfig()) => {
778
+ const productTruthRoot = resolveProductTruthRoot(config);
779
+ const engineeringTruthRoot = resolveEngineeringTruthRoot(config);
893
780
  return [
894
- "---",
895
- "status: active",
896
- "doc_type: area-route",
897
- `last_reviewed: ${currentDate()}`,
898
- "---",
899
- "",
900
- `# ${title} Areas`,
901
- "",
902
- `## ${title}`,
903
- "",
904
- "Truth documents:",
905
- "```yaml",
906
- "truth_documents:",
907
- ` - path: ${bootstrapTruthDoc}`,
908
- " kind: engineering-workflow",
909
- " lane: engineering",
910
- "```",
911
- "",
912
- "This is a provisional bootstrap route. It exists only to make fresh repositories routeable until real product, service, domain, or ownership areas are created.",
913
- "",
914
- "Code surface:",
915
- "- src/**",
916
- "",
917
- "Update truth when:",
918
- "- this provisional bootstrap route is the only match for a real code surface",
919
- "- route ownership is still broad, mixed, or ambiguous",
920
- "- Run Truth Structure before normal Truth Sync so the touched code gets a bounded owner",
921
- "",
922
- "## Source References",
923
- "",
924
- `- ${sourceOfTruth}`,
925
- ""
781
+ "Lane review questions:",
782
+ "- before writing canonical truth docs, classify the request or change as product-lane, engineering-lane, both-lane, or ambiguous",
783
+ `- product-lane writes belong under ${productTruthRoot} and state product promises, boundaries, rationale, decisions, and success criteria`,
784
+ `- engineering-lane writes belong under ${engineeringTruthRoot} and state source-backed current realization, contracts, architecture, workflows, operations, or tests`,
785
+ "- both-lane work must write separate product and engineering docs and cross-link them in route YAML with realized_by and realizes, not in doc frontmatter",
786
+ "- ambiguous lane ownership must stop or invoke Truth Structure instead of writing a mixed document",
787
+ `- ${LANE_INVARIANT}`
926
788
  ].join("\n");
927
789
  };
928
- var renderBootstrapRoutingDocTemplate = (config) => {
929
- const defaultArea = config.truthmark.routes.defaultArea;
930
- const title = titleCase(defaultArea);
931
- const templatePath = `${truthRoot(config)}/${defaultArea}/bootstrap-routing.md`;
932
- const routePath = `${config.truthmark.paths.routeAreasRoot}/${defaultArea}.md`;
933
- const routeSource = resolveRelativePath(templatePath, routePath);
934
- const configSource = resolveRelativePath(
935
- templatePath,
936
- ".truthmark/config.yml"
937
- );
938
- const today = currentDate();
790
+ var EVIDENCE_AUTHORITY_INSTRUCTIONS = [
791
+ "Repository instruction files and explicitly configured policy docs remain instruction authority when present; do not assume a repository uses any particular policy path.",
792
+ "Implementation code and canonical truth docs are inspected evidence for current behavior; they do not silently override workflow write boundaries."
793
+ ].join("\n");
794
+ var REPOSITORY_INTELLIGENCE_INSTRUCTIONS = [
795
+ "Repository intelligence artifacts are optional derived context: RepoIndex, RouteMap, ImpactSet, and WorkflowState/action context may guide routing, write boundaries, and verification planning when available.",
796
+ "They do not override checkout evidence, canonical truth docs, route files, or workflow write boundaries.",
797
+ "If unavailable, inspect any present Truthmark config, route files, source files, truth docs, and tests directly, then report that repository-intelligence artifacts were not generated."
798
+ ].join("\n");
799
+ var FEATURE_DOC_TEMPLATE_INSTRUCTIONS = [
800
+ "When creating or updating a truth doc, inspect the routed truth kind and use the matching template under the configured Truthmark templates root.",
801
+ "Supported kinds: product-capability, engineering-behavior, engineering-contract, engineering-architecture, engineering-workflow, engineering-operations, and engineering-test-behavior.",
802
+ "Treat the HTML comments under each template section as normative authoring guidance for that section.",
803
+ "Align existing docs to that template and write or repair section content so it satisfies the comment guidance while preserving accurate authored content.",
804
+ "If the template is missing, use lane-specific sections: product truth says what must be true and why; engineering truth says how the repository currently realizes it.",
805
+ "Teams may edit template files under the configured Truthmark templates root to define their local truth-doc standards."
806
+ ].join("\n");
807
+ var TRUTH_DOC_AUTHORING_STYLE_INSTRUCTIONS = [
808
+ "Truth-doc prose style:",
809
+ "- Use professional, plain technical prose. Prefer specific current-state claims over promotional, symbolic, or generic significance language.",
810
+ "- Avoid common AI-writing tells: pivotal, crucial, underscores, serves as, stands as, showcases, landscape, vague expert attributions, and generic upbeat conclusions.",
811
+ "- Keep claims evidence-backed and diff-friendly: one durable claim per bullet or line; paragraphs should be no longer than one or two short sentences.",
812
+ "- Do not add personality, rhetorical flourish, first-person commentary, or marketing tone.",
813
+ "- Rewrite dense or formulaic prose only when it improves readability without removing scope, evidence, decisions, or source references."
814
+ ].join("\n");
815
+ var renderTruthDocOwnershipGateSection = (subject, outcome) => {
939
816
  return [
940
- "---",
941
- "status: active",
942
- "truth_kind: engineering-workflow",
943
- `last_reviewed: ${today}`,
944
- "---",
945
- "",
946
- `# ${title} Bootstrap Routing`,
947
- "",
948
- "## Purpose",
949
- "",
950
- `This doc records the provisional broad route for ${defaultArea}.`,
951
- "This doc is a bootstrap handoff, not a behavior truth dumping ground.",
952
- "It is not a substitute for bounded product and engineering truth docs.",
953
- "",
954
- "## Scope",
955
- "",
956
- "This doc owns only the initial routing workflow for a fresh Truthmark repository whose default route still maps a broad code surface such as `src/**`.",
957
- "It does not own implementation behavior under that code surface.",
958
- "",
959
- "## Current Implementation Behavior",
960
- "",
961
- "The scaffold creates this provisional bootstrap handoff only when a default broad route needs a canonical owner. Agents use it as a signal to run Truth Structure and create bounded routes before normal Truth Sync, not as a place to accumulate implementation claims.",
962
- "",
963
- "## Product Truth Links",
964
- "",
965
- "- None. This is an engineering bootstrap handoff for routing setup, not a product promise.",
966
- "",
967
- "## Triggers",
968
- "",
969
- "- A real code change maps only to this provisional broad route.",
970
- "- Truth Sync cannot identify a specific behavior-owned route and bounded truth owner.",
971
- "- A maintainer or agent is onboarding the first real product, service, domain, package, or ownership area.",
972
- "",
973
- "## Inputs",
974
- "",
975
- "- Current route files under the configured Truthmark route root.",
976
- "- The touched code, tests, configuration, and existing docs needed to infer the smallest real owner.",
977
- "- Repository instruction files that exist in the checkout.",
978
- "",
979
- "## Execution Model",
980
- "",
981
- "Run Truth Structure before normal Truth Sync when real code changes touch only this broad route. Truth Structure should create or repair bounded areas first; Truth Sync should then update the bounded owner docs.",
982
- "",
983
- "## Steps",
984
- "",
985
- "1. Treat this route as provisional and insufficient for normal behavior maintenance.",
986
- "2. Inspect the touched code/test surface and infer the narrowest durable owner.",
987
- "3. Create or repair route entries and truth docs for that owner.",
988
- "4. Leave this bootstrap doc small; do not append behavior details here.",
989
- "5. Resume Truth Sync only after the touched code resolves to a bounded owner.",
990
- "",
991
- "## State, Retry, And Failure Behavior",
992
- "",
993
- "If ownership cannot be inferred safely, stop and report manual-review files instead of widening this route or adding generic behavior prose.",
994
- "",
995
- "## Outputs",
996
- "",
997
- "- Bounded route areas and lane-appropriate truth docs for the touched surface.",
998
- "- A compact manual handoff report when ownership remains ambiguous.",
999
- "",
1000
- "## Engineering Decisions",
1001
- "",
1002
- `- Decision (${today}): Default broad routing is provisional bootstrap state. Agents should create bounded areas before normal Truth Sync rather than extending a catch-all overview doc.`,
1003
- "",
1004
- "## Rationale",
1005
- "",
1006
- "Scoped ownership keeps agent context close to affected files and prevents broad default docs from absorbing unrelated behavior. This preserves agent-native truth maintenance without adding a token-heavy discovery layer.",
1007
- "",
1008
- "## Non-Goals",
1009
- "",
1010
- "- This doc is not a repository behavior overview.",
1011
- "- This doc is not a product capability or engineering behavior owner.",
1012
- "- This doc is not a permanent home for claims about files under `src/**`.",
1013
- "",
1014
- "## Maintenance Notes",
1015
- "",
1016
- "Keep this doc short. When a repository has real bounded routes, prefer updating those routes and their truth docs instead of expanding this bootstrap handoff.",
1017
- "",
1018
- "## Source References",
1019
- "",
1020
- `- ${routeSource}`,
1021
- `- ${configSource}`,
1022
- ""
817
+ "Truth-doc ownership review:",
818
+ `- before editing or relying on ${subject}, verify each target/source truth doc is a bounded owner for the behavior`,
819
+ "- if a target/source doc mixes independent owners, spans unrelated behaviors, acts as an index, or needs cross-owner edits, do not patch or in-place repair it",
820
+ `- ${outcome}`,
821
+ "- report Ownership reviewed, Structure required, Truth docs split, Truth docs restructured, or Manual handoff reason as applicable"
1023
822
  ].join("\n");
1024
823
  };
1025
- var renderTruthRootReadmeTemplate = (config = createDefaultConfig(), lane = "engineering") => {
1026
- const templatePath = `${lane === "product" ? resolveProductTruthRoot(config) : resolveEngineeringTruthRoot(config)}/README.md`;
1027
- const sourceOfTruth = resolveRelativePath(
1028
- templatePath,
1029
- config.truthmark.paths.routesIndex
1030
- );
824
+ var TRUTH_DOC_DECISION_RATIONALE_PRESERVATION_INSTRUCTIONS = [
825
+ "Decision/Rationale preservation review:",
826
+ "- before any truth-doc split, restructure, or shape repair, inventory existing Product Decisions, Engineering Decisions, and Rationale sections in every source or touched truth doc",
827
+ "- preserve each current decision and rationale in the correct product or engineering lane owner; when splitting, move it to the new owner doc rather than deleting it or leaving it in an index",
828
+ "- remove or narrow a decision or rationale only when checkout evidence shows it is stale or unsupported, and report the exact claim, evidence, and result",
829
+ "- if ownership of a decision or rationale is unclear, stop with manual-review files instead of deleting it or guessing",
830
+ "- after the edit, verify every touched truth doc keeps lane-appropriate decision/rationale sections and every pre-existing entry is preserved, moved, narrowed, removed with evidence, or blocked"
831
+ ].join("\n");
832
+ var renderTruthDocRestructureGateSection = (scope) => {
1031
833
  return [
1032
- "---",
1033
- "status: active",
1034
- "doc_type: index",
1035
- `last_reviewed: ${currentDate()}`,
1036
- "---",
1037
- "",
1038
- "# Truth Docs",
1039
- "",
1040
- "This directory is an index for current truth docs organized by the configured Truthmark hierarchy.",
1041
- "",
1042
- renderLaneRootReadmeSummary(lane),
1043
- "",
1044
- renderLaneRootLeafDocGuidance(lane),
1045
- "",
1046
- "## Source References",
1047
- "",
1048
- `- ${sourceOfTruth}`,
1049
- ""
834
+ "Truth-doc shape repair review:",
835
+ `- ${scope}`,
836
+ "- repair shape in place only after the ownership review confirms the doc is the right bounded owner",
837
+ "- use Truth Structure for ownership splits; do not treat broad or mixed-owner docs as in-place repair work",
838
+ "- repair shape when a narrow edit would make truth worse: missing template sections, stale evidence conflicts, cross-section updates within one owner, or wrong frontmatter/source/headings",
839
+ "- preserve supported claims; remove, narrow, or record unsupported or stale claims for manual handoff",
840
+ "- report docs restructured and why a narrow edit was not sufficient"
1050
841
  ].join("\n");
1051
842
  };
1052
- var renderTruthDomainReadmeTemplate = (config) => {
1053
- const defaultArea = config.truthmark.routes.defaultArea;
1054
- const title = titleCase(defaultArea);
1055
- const templatePath = `${truthRoot(config)}/${defaultArea}/README.md`;
1056
- const sourceOfTruth = resolveRelativePath(
1057
- templatePath,
1058
- `${config.truthmark.paths.routeAreasRoot}/${defaultArea}.md`
1059
- );
843
+ var ARCHITECTURE_DOC_BOUNDARY_INSTRUCTIONS = [
844
+ "Maintain architecture docs only for structure-level changes: system structure, module boundaries, runtime topology, persistence boundaries, cross-cutting contracts, or generated-surface ownership.",
845
+ "Keep ordinary behavior, endpoints, UI copy, validation rules, and bug fixes in behavior or contract docs unless they change those boundaries."
846
+ ].join("\n");
847
+ var renderRouteFirstEvidenceGateSection = (subject, noImpactedDocOutcome) => {
1060
848
  return [
1061
- "---",
1062
- "status: active",
1063
- "doc_type: index",
1064
- `last_reviewed: ${currentDate()}`,
1065
- "---",
1066
- "",
1067
- `# ${title} Truth Docs`,
1068
- "",
1069
- `This directory indexes bounded ${title.toLowerCase()} truth docs.`,
1070
- "",
1071
- "README.md files are indexes, not Truth Sync targets. Keep bounded truth in leaf docs in this directory.",
1072
- "",
1073
- "Current leaf docs:",
1074
- "",
1075
- "- [Bootstrap routing](bootstrap-routing.md)",
1076
- "",
1077
- "## Source References",
1078
- "",
1079
- `- ${sourceOfTruth}`,
1080
- ""
849
+ "Evidence checklist:",
850
+ `- route-first: map ${subject} to bounded route owners and primary canonical docs`,
851
+ "- review new or changed behavior-bearing claims only in touched docs, route ownership, lane-specific decisions, and rationale",
852
+ "- support claims with primary checkout evidence: implementation, config, routing, generated templates, schemas, or contract definitions",
853
+ "- tests/examples/canonical docs corroborate; they are not sole proof when implementation conflicts",
854
+ "- remove, narrow, or record unsupported claims for manual handoff",
855
+ `- ${noImpactedDocOutcome}`
1081
856
  ].join("\n");
1082
857
  };
1083
- var renderTemplateSection = (section) => {
858
+ var renderTopologyEvidenceGateSection = () => {
1084
859
  return [
1085
- section.heading,
1086
- "",
1087
- "<!--",
1088
- ...section.guidance,
1089
- "-->",
1090
- "",
1091
- `{{${section.placeholder}}}`,
1092
- ""
1093
- ];
1094
- };
1095
- var titleToPlaceholder = (title) => {
1096
- return title.replace(/^#+\s+/u, "").toLowerCase().replaceAll(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
860
+ "Evidence checklist:",
861
+ "- apply the evidence checklist before finishing when Truth Structure writes routed docs, ownership claims, lane-specific decisions, or rationale",
862
+ "- support ownership/behavior claims with topology or primary checkout evidence from layout, implementation boundaries, docs, config, route files, tests, templates, schemas, or contracts",
863
+ "- tests/examples/canonical docs corroborate; remove, narrow, or record unsupported claims for manual handoff"
864
+ ].join("\n");
1097
865
  };
1098
- var findTemplateSectionHeadings = (template) => {
1099
- const matches = [];
1100
- let fencedCodeMarker = null;
1101
- let fencedCodeLength = 0;
1102
- for (const lineMatch of template.matchAll(/^.*(?:\r?\n|$)/gm)) {
1103
- const rawLine = lineMatch[0];
1104
- if (rawLine.length === 0) {
1105
- continue;
1106
- }
1107
- const line = rawLine.replace(/\r?\n$/u, "");
1108
- const fenceMatch = /^(?: {0,3})(`{3,}|~{3,})/u.exec(line);
1109
- if (fenceMatch) {
1110
- const marker = fenceMatch[1]?.[0];
1111
- const length = fenceMatch[1]?.length ?? 0;
1112
- if (fencedCodeMarker === null) {
1113
- fencedCodeMarker = marker;
1114
- fencedCodeLength = length;
1115
- } else if (marker === fencedCodeMarker && length >= fencedCodeLength) {
1116
- fencedCodeMarker = null;
1117
- fencedCodeLength = 0;
1118
- }
1119
- continue;
1120
- }
1121
- if (fencedCodeMarker === null && /^## .+$/u.test(line)) {
1122
- matches.push({ heading: line.trim(), index: lineMatch.index });
1123
- }
1124
- }
1125
- return matches;
866
+ var renderAuditEvidenceGateSection = () => {
867
+ return [
868
+ "Evidence checklist:",
869
+ "- support each finding and suggested fix with evidence from config, route files, canonical docs, implementation, templates, or tests",
870
+ "- canonical docs are context, not sole proof when implementation conflicts",
871
+ "- remove unsupported findings or mark open questions; validate changed claims if you edit docs"
872
+ ].join("\n");
1126
873
  };
1127
- var parseTemplateSections = (template) => {
1128
- const matches = findTemplateSectionHeadings(template);
1129
- if (matches.length === 0) {
1130
- return { preamble: template.trimEnd(), sections: [] };
1131
- }
1132
- const sections = matches.map((match, index) => {
1133
- const start = match.index;
1134
- const next = matches[index + 1];
1135
- const end = next?.index ?? template.length;
1136
- return {
1137
- heading: match.heading,
1138
- block: template.slice(start, end).trimEnd()
1139
- };
1140
- });
1141
- return {
1142
- preamble: template.slice(0, matches[0]?.index ?? 0).trimEnd(),
1143
- sections
1144
- };
874
+ var renderCodexSubagentModeSection = (agents, parentRule, writeAgents = []) => {
875
+ const writeAgentLines = writeAgents.length > 0 ? [
876
+ `- dispatch write-capable project agents only with explicit write leases: ${writeAgents.join(", ")}`,
877
+ "- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
878
+ "- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
879
+ "- parent must inspect the actual checkout diff against each lease before accepting a worker report"
880
+ ] : [];
881
+ const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
882
+ const readOnlyWorkerLabel = writeAgents.length > 0 ? "read-only workers" : "workers";
883
+ return [
884
+ "Codex subagent mode:",
885
+ "- use automatically when this workflow runs in Codex and the parent agent chooses bounded subagent fan-out",
886
+ `- dispatch read-only project agents ${readOnlyScope}: ${agents.join(", ")}`,
887
+ `- ${readOnlyWorkerLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
888
+ `- parent supplies bounded evidence shards; ${readOnlyWorkerLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
889
+ ...writeAgentLines,
890
+ `- ${parentRule}`
891
+ ].join("\n");
1145
892
  };
1146
- var LEGACY_MANAGED_TEMPLATE_HEADINGS = /* @__PURE__ */ new Map([
1147
- ["## Current Behavior", "## Current Implementation Behavior"],
1148
- ["## Source Evidence", "## Source References"]
1149
- ]);
1150
- var resolveManagedTemplateHeading = (heading) => {
1151
- return LEGACY_MANAGED_TEMPLATE_HEADINGS.get(heading) ?? heading;
893
+ var renderOpenCodeSubagentModeSection = (agents, parentRule, writeAgents = []) => {
894
+ const mentions = agents.map((agent) => `@${agent.replace(/_/gu, "-")}`);
895
+ const writeMentions = writeAgents.map(
896
+ (agent) => `@${agent.replace(/_/gu, "-")}`
897
+ );
898
+ const writeAgentLines = writeMentions.length > 0 ? [
899
+ `- dispatch write-capable project subagents only with explicit write leases: ${writeMentions.join(", ")}`,
900
+ "- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
901
+ "- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
902
+ "- parent must inspect the actual checkout diff against each lease before accepting a worker report"
903
+ ] : [];
904
+ const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
905
+ const readOnlyWorkerLabel = writeAgents.length > 0 ? "read-only workers" : "workers";
906
+ return [
907
+ "OpenCode subagent mode:",
908
+ "- use automatically when this workflow runs in OpenCode and the parent agent chooses bounded subagent fan-out",
909
+ `- dispatch read-only project subagents ${readOnlyScope}: ${mentions.join(", ")}`,
910
+ `- ${readOnlyWorkerLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
911
+ `- parent supplies bounded evidence shards; ${readOnlyWorkerLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
912
+ ...writeAgentLines,
913
+ `- ${parentRule}`
914
+ ].join("\n");
1152
915
  };
1153
- var stripManagedFrontmatterFields = (preamble) => {
1154
- if (!preamble.startsWith("---\n")) {
1155
- return preamble;
1156
- }
1157
- const lines = preamble.split("\n");
1158
- const closingIndex = lines.findIndex(
1159
- (line, index) => index > 0 && line.trim() === "---"
916
+ var renderClaudeSubagentModeSection = (agents, parentRule, writeAgents = []) => {
917
+ const mentions = agents.map(
918
+ (agent) => `${agent.replace(/_/gu, "-")} subagent`
1160
919
  );
1161
- if (closingIndex < 0) {
1162
- return preamble;
1163
- }
1164
- const fieldsToRemove = /* @__PURE__ */ new Set(["source_of_truth", "doc_type", "truth_lane"]);
1165
- const keptFrontmatterLines = [];
1166
- let skippingManagedField = false;
1167
- for (const line of lines.slice(1, closingIndex)) {
1168
- const keyMatch = /^([A-Za-z0-9_-]+):(\s|$)/u.exec(line);
1169
- if (keyMatch) {
1170
- skippingManagedField = fieldsToRemove.has(keyMatch[1] ?? "");
1171
- }
1172
- if (!skippingManagedField) {
1173
- keptFrontmatterLines.push(line);
1174
- }
1175
- }
920
+ const writeMentions = writeAgents.map(
921
+ (agent) => `${agent.replace(/_/gu, "-")} subagent`
922
+ );
923
+ const writeAgentLines = writeMentions.length > 0 ? [
924
+ `- dispatch write-capable project subagents only with explicit write leases: ${writeMentions.join(", ")}`,
925
+ "- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
926
+ "- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
927
+ "- parent must inspect the actual checkout diff against each lease before accepting a worker report"
928
+ ] : [];
929
+ const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
930
+ const readOnlySubagentLabel = writeAgents.length > 0 ? "read-only subagents" : "subagents";
1176
931
  return [
1177
- "---",
1178
- ...keptFrontmatterLines,
1179
- "---",
1180
- ...lines.slice(closingIndex + 1)
1181
- ].join("\n").trimEnd();
932
+ "Claude Code subagent mode:",
933
+ "- use automatically when this workflow runs in Claude Code and the parent agent chooses bounded subagent fan-out",
934
+ `- dispatch read-only project subagents ${readOnlyScope}: ${mentions.join(", ")}`,
935
+ `- ${readOnlySubagentLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
936
+ `- parent supplies bounded evidence shards; ${readOnlySubagentLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
937
+ ...writeAgentLines,
938
+ `- ${parentRule}`
939
+ ].join("\n");
1182
940
  };
1183
- var mergeTruthDocTemplate = (existingTemplate, defaultTemplate) => {
1184
- if (existingTemplate.trim().length === 0) {
1185
- return defaultTemplate;
1186
- }
1187
- const defaultParsed = parseTemplateSections(defaultTemplate);
1188
- const existingParsed = parseTemplateSections(existingTemplate);
1189
- const defaultHeadings = new Set(
1190
- defaultParsed.sections.map((section) => section.heading)
941
+ var renderCopilotCustomAgentModeSection = (agents, parentRule, writeAgents = []) => {
942
+ const mentions = agents.map((agent) => `@${agent.replace(/_/gu, "-")}`);
943
+ const writeMentions = writeAgents.map(
944
+ (agent) => `@${agent.replace(/_/gu, "-")}`
1191
945
  );
1192
- const customBeforeDefault = /* @__PURE__ */ new Map();
1193
- const trailingCustomSections = [];
1194
- existingParsed.sections.forEach((section, index) => {
1195
- if (defaultHeadings.has(resolveManagedTemplateHeading(section.heading))) {
1196
- return;
1197
- }
1198
- const nextDefaultSection = existingParsed.sections.slice(index + 1).find(
1199
- (candidate) => defaultHeadings.has(resolveManagedTemplateHeading(candidate.heading))
1200
- );
1201
- if (nextDefaultSection) {
1202
- const nextDefaultHeading = resolveManagedTemplateHeading(
1203
- nextDefaultSection.heading
1204
- );
1205
- const bucket = customBeforeDefault.get(nextDefaultHeading) ?? [];
1206
- bucket.push(section);
1207
- customBeforeDefault.set(nextDefaultHeading, bucket);
1208
- return;
1209
- }
1210
- trailingCustomSections.push(section);
1211
- });
1212
- const mergedSections = defaultParsed.sections.flatMap((section) => [
1213
- ...customBeforeDefault.get(section.heading) ?? [],
1214
- section
1215
- ]);
946
+ const writeAgentLines = writeMentions.length > 0 ? [
947
+ `- dispatch write-capable project custom agents only with explicit write leases: ${writeMentions.join(", ")}`,
948
+ "- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
949
+ "- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
950
+ "- parent must inspect the actual checkout diff against each lease before accepting a worker report"
951
+ ] : [];
952
+ const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
953
+ const readOnlyCustomAgentLabel = writeAgents.length > 0 ? "read-only custom agents" : "custom agents";
1216
954
  return [
1217
- stripManagedFrontmatterFields(existingParsed.preamble),
1218
- ...mergedSections.map((section) => section.block),
1219
- ...trailingCustomSections.map((section) => section.block),
1220
- ""
1221
- ].filter((block) => block.length > 0).join("\n\n");
955
+ "Copilot custom-agent mode:",
956
+ "- use automatically when this workflow runs in Copilot and the parent agent chooses bounded custom-agent fan-out",
957
+ `- dispatch read-only project custom agents ${readOnlyScope}: ${mentions.join(", ")}`,
958
+ `- ${readOnlyCustomAgentLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
959
+ `- parent supplies bounded evidence shards; ${readOnlyCustomAgentLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
960
+ ...writeAgentLines,
961
+ `- ${parentRule}`
962
+ ].join("\n");
1222
963
  };
1223
- var renderBehaviorDocTemplateFile = () => {
964
+ var defaultAgentConfig = () => {
965
+ return createDefaultConfig();
966
+ };
967
+ var renderHierarchySummary = (config) => {
968
+ const productRoot = resolveProductTruthRoot(config);
969
+ const engineeringRoot = resolveEngineeringTruthRoot(config);
1224
970
  return [
1225
- "---",
1226
- "status: active",
1227
- "truth_kind: engineering-behavior",
1228
- `last_reviewed: ${currentDate()}`,
1229
- "---",
1230
- "",
1231
- "# {{title}}",
1232
- "",
1233
- "## Purpose",
1234
- "",
1235
- "<!--",
1236
- "State the user/system outcome this behavior protects and why it exists.",
1237
- "Include the problem boundary and durable value; exclude roadmap, implementation plan, and historical narrative.",
1238
- "List the code, config, docs, or tests that support the claim in Source References rather than prose-only assertion.",
1239
- "-->",
1240
- "",
1241
- "{{purpose}}",
1242
- "",
1243
- "## Scope",
1244
- "",
1245
- "<!--",
1246
- "Define the one coherent behavior surface this document owns.",
1247
- "Include in-scope actors, entrypoints, state/data owned by this doc, and explicit handoffs to neighboring truth docs.",
1248
- "Split into another leaf doc when content introduces a distinct outcome, state machine, rule family, external contract, or route owner.",
1249
- "Keep README.md files as indexes only.",
1250
- "-->",
1251
- "",
1252
- "{{scope}}",
1253
- "",
1254
- "This doc was created from the editable engineering-behavior template at {{template_path}}.",
1255
- "",
1256
- "## Current Implementation Behavior",
1257
- "",
1258
- "<!--",
1259
- "Describe only current implemented behavior in present tense.",
1260
- "Cover observable behavior, important defaults, and user/system-visible effects; exclude desired future behavior and speculative design.",
1261
- "Every non-obvious claim should be checkable from Source References.",
1262
- "-->",
1263
- "",
1264
- "{{current_implementation_behavior}}",
1265
- "",
1266
- "## Core Rules",
1267
- "",
1268
- "<!--",
1269
- "Capture stable business rules, invariants, precedence rules, validation rules, and must-never constraints.",
1270
- "Separate rules from incidental implementation details; cite current implementation or tests for rule enforcement.",
1271
- "-->",
1272
- "",
1273
- "{{core_rules}}",
1274
- "",
1275
- "## Behavior Scenarios",
1276
- "",
1277
- "<!--",
1278
- "Use compact scenario blocks only where they clarify normal, fallback, or compatibility-critical behavior.",
1279
- "Write scenarios as current truth, not desired requirements: `#### Scenario: <implemented case>` followed by `- **GIVEN** ...`, `- **WHEN** ...`, `- **THEN** ...`, and optional `- **AND** ...` bullets.",
1280
- "Keep each bullet evidence-backed and observable; do not force a scenario for every rule.",
1281
- "-->",
1282
- "",
1283
- "{{behavior_scenarios}}",
971
+ "Truthmark hierarchy hints:",
972
+ "- Config, when present: .truthmark/config.yml",
973
+ `- Root route index, when present: ${config.truthmark.paths.routesIndex}`,
974
+ `- Area route files, when present: ${config.truthmark.paths.routeAreasRoot}/**/*.md`,
975
+ `- Product truth docs, when present: ${productRoot}/**/*.md`,
976
+ `- Engineering truth docs, when present: ${engineeringRoot}/**/*.md`
977
+ ].join("\n");
978
+ };
979
+
980
+ // src/templates/agents-block.ts
981
+ var TRUTHMARK_BLOCK_START = "<!-- truthmark:start -->";
982
+ var TRUTHMARK_BLOCK_END = "<!-- truthmark:end -->";
983
+ var renderCompactHierarchySummary = (config) => {
984
+ const productTruthRoot = resolveProductTruthRoot(config);
985
+ const engineeringTruthRoot = resolveEngineeringTruthRoot(config);
986
+ const truthDocRoots = Array.from(
987
+ /* @__PURE__ */ new Set([productTruthRoot, engineeringTruthRoot])
988
+ ).map((truthRoot3) => `${truthRoot3}/**/*.md`);
989
+ return `Hierarchy hints: config .truthmark/config.yml when present; routes ${config.truthmark.paths.routesIndex} and ${config.truthmark.paths.routeAreasRoot}/**/*.md when present; Truth docs: ${truthDocRoots.join(" and ")} when present.`;
990
+ };
991
+ var renderAgentsBlock = (config = defaultAgentConfig()) => {
992
+ const portalLine = config.truthmark.generated.portal.enabled ? `Truthmark Portal is a separate manual-only presentation workflow. Run it only when explicitly requested; it writes generated non-canonical static files under ${config.truthmark.paths.portalOutput}/. Markdown remains canonical.` : null;
993
+ return [
994
+ TRUTHMARK_BLOCK_START,
995
+ "## Truthmark Workflow",
1284
996
  "",
1285
- "## Flows And States",
1286
- "",
1287
- "<!--",
1288
- "Document state transitions, lifecycle stages, retries, fallbacks, route switches, and important error paths.",
1289
- "State 'None beyond current behavior.' when this behavior has no distinct flow or state model.",
1290
- "-->",
1291
- "",
1292
- "{{flows_and_states}}",
1293
- "",
1294
- "## Contracts",
1295
- "",
1296
- "<!--",
1297
- "Capture user-visible or integration contracts: CLI/API shape, inputs, outputs, diagnostics, files, events, permissions, or links to canonical contract docs.",
1298
- "Avoid duplicating a separate canonical contract doc; link to it when contract ownership lives elsewhere.",
1299
- "-->",
1300
- "",
1301
- "{{contracts}}",
1302
- "",
1303
- "## Product Truth Links",
1304
- "",
1305
- "<!--",
1306
- "List product truth docs this engineering doc realizes; author canonical realizes links in route YAML, not doc frontmatter.",
1307
- "Use 'None.' when this is purely internal engineering behavior.",
1308
- "-->",
1309
- "",
1310
- "{{product_truth_links}}",
1311
- "",
1312
- "## Engineering Decisions",
1313
- "",
1314
- "<!--",
1315
- "Keep active decisions only, dated inline when added or changed.",
1316
- "Explain decisions that shape behavior, boundaries, rejected alternatives, or migration constraints; replace stale decisions instead of appending historical logs.",
1317
- "-->",
1318
- "",
1319
- "{{engineering_decisions}}",
1320
- "",
1321
- "## Rationale",
1322
- "",
1323
- "<!--",
1324
- "Explain why the current behavior and active decisions are this way, including tradeoffs and constraints.",
1325
- "Tie rationale to evidence-backed behavior; do not use this as a changelog.",
1326
- "-->",
1327
- "",
1328
- "{{rationale}}",
1329
- "",
1330
- "## Non-Goals",
1331
- "",
1332
- "<!--",
1333
- "Name adjacent behavior this doc intentionally does not own, especially tempting future expansions or neighboring route owners.",
1334
- "Use this section to prevent scope creep and duplicate truth ownership.",
1335
- "-->",
1336
- "",
1337
- "{{non_goals}}",
1338
- "",
1339
- "## Maintenance Notes",
1340
- "",
1341
- "<!--",
1342
- "List related tests, routing cautions, migration notes, evidence drift risks, and review triggers for future maintainers or agents.",
1343
- "Keep this operational and current-state focused, not historical.",
1344
- "-->",
1345
- "",
1346
- "{{maintenance_notes}}",
1347
- "",
1348
- "## Source References",
1349
- "",
1350
- "<!--",
1351
- "List source files, tests, configs, generated templates, route files, or product instructions that support current claims.",
1352
- "-->",
1353
- "",
1354
- "{{source_references}}",
1355
- ""
1356
- ].join("\n");
1357
- };
1358
- var sectionSpec = (heading, guidance, placeholder = titleToPlaceholder(heading)) => ({ heading, guidance, placeholder });
1359
- var PURPOSE_SECTION = sectionSpec("## Purpose", [
1360
- "State the software-engineering outcome this document protects and why the documented surface exists.",
1361
- "Include durable value, impacted users/systems, and the problem boundary; exclude roadmap, implementation plans, and historical narrative.",
1362
- "Keep claims traceable to Source References rather than prose-only assertion."
1363
- ]);
1364
- var SCOPE_SECTION = sectionSpec("## Scope", [
1365
- "Define the one coherent surface this document owns, including actors, entrypoints, owned state/data, and handoffs to neighboring truth docs.",
1366
- "Call out important out-of-scope boundaries here or in Non-Goals; split the doc when it mixes distinct outcomes, lifecycles, contracts, or owners."
1367
- ]);
1368
- var PRODUCT_DECISIONS_SECTION = sectionSpec(
1369
- "## Product Decisions",
1370
- [
1371
- "Keep active decisions only, dated inline when added or changed.",
1372
- "Capture decisions that shape behavior, interfaces, boundaries, compatibility, risk acceptance, or migration constraints.",
1373
- "Replace stale decisions instead of appending historical logs."
1374
- ],
1375
- "decision"
1376
- );
1377
- var ENGINEERING_DECISIONS_SECTION = sectionSpec(
1378
- "## Engineering Decisions",
1379
- [
1380
- "Keep active engineering, architecture, contract, workflow, or operational decisions only, dated inline when added or changed.",
1381
- "Do not restate product promises, product rationale, or business decisions here; link product truth instead.",
1382
- "Replace stale decisions instead of appending historical logs."
1383
- ],
1384
- "engineering_decisions"
1385
- );
1386
- var RATIONALE_SECTION = sectionSpec("## Rationale", [
1387
- "Explain why the current behavior, structure, or contract is this way, including tradeoffs and constraints.",
1388
- "Tie rationale to evidence-backed facts and active decisions; do not use this as a changelog."
1389
- ]);
1390
- var NON_GOALS_SECTION = sectionSpec("## Non-Goals", [
1391
- "Name adjacent behavior, responsibilities, interfaces, or future expansions this doc intentionally does not own.",
1392
- "Use this section to prevent scope creep and duplicate truth ownership."
1393
- ]);
1394
- var MAINTENANCE_NOTES_SECTION = sectionSpec("## Maintenance Notes", [
1395
- "List related tests, routing cautions, migration notes, compatibility risks, evidence drift risks, and review triggers for future maintainers or agents.",
1396
- "Keep this operational and current-state focused, not historical."
1397
- ]);
1398
- var SOURCE_REFERENCES_SECTION = sectionSpec(
1399
- "## Source References",
1400
- [
1401
- "List source files, tests, configs, generated templates, route files, or product instructions that support current claims."
1402
- ],
1403
- "source_references"
1404
- );
1405
- var renderTypedTruthDocTemplate = (truthKind, title, sections) => {
1406
- return [
1407
- "---",
1408
- "status: active",
1409
- `truth_kind: ${truthKind}`,
1410
- `last_reviewed: ${currentDate()}`,
1411
- "---",
1412
- "",
1413
- `# ${title}`,
1414
- "",
1415
- ...renderTemplateSection(PURPOSE_SECTION),
1416
- ...renderTemplateSection(SCOPE_SECTION),
1417
- ...sections.flatMap(renderTemplateSection),
1418
- ...renderTemplateSection(ENGINEERING_DECISIONS_SECTION),
1419
- ...renderTemplateSection(RATIONALE_SECTION),
1420
- ...renderTemplateSection(NON_GOALS_SECTION),
1421
- ...renderTemplateSection(MAINTENANCE_NOTES_SECTION),
1422
- ...renderTemplateSection(SOURCE_REFERENCES_SECTION)
1423
- ].join("\n");
1424
- };
1425
- var CORE_LANE_INVARIANT = "Do not make product docs a summary of engineering docs. Do not make engineering docs a detailed version of product docs. Product truth says what must be true and why. Engineering truth says how the repository currently realizes it.";
1426
- var renderProductTruthDocTemplate = (truthKind, title, sections, includeNonGoals) => {
1427
- return [
1428
- "---",
1429
- "status: active",
1430
- `truth_kind: ${truthKind}`,
1431
- `last_reviewed: ${currentDate()}`,
1432
- "---",
1433
- "",
1434
- `# ${title}`,
1435
- "",
1436
- "<!--",
1437
- CORE_LANE_INVARIANT,
1438
- "Product docs may cite code directly when code proves current product behavior, but keep implementation flow, renderer internals, CLI envelopes, and generated file inventories in engineering truth.",
1439
- "-->",
1440
- "",
1441
- ...sections.flatMap(renderTemplateSection),
1442
- ...renderTemplateSection(PRODUCT_DECISIONS_SECTION),
1443
- ...renderTemplateSection(
1444
- sectionSpec(
1445
- "## Engineering Realization Links",
1446
- [
1447
- "List engineering truth that realizes this product truth; author canonical realized_by links in route YAML, not doc frontmatter.",
1448
- "Do not summarize those engineering docs."
1449
- ],
1450
- "engineering_realization_links"
1451
- )
1452
- ),
1453
- ...includeNonGoals ? renderTemplateSection(NON_GOALS_SECTION) : [],
1454
- ...renderTemplateSection(SOURCE_REFERENCES_SECTION)
997
+ "Truthmark-managed block. Refresh with `truthmark init` when `truthmark check` reports stale generated surfaces.",
998
+ renderCompactHierarchySummary(config),
999
+ "Decisions live in the canonical doc they govern; date active decisions inline.",
1000
+ "Agent runtime: host-native skill packages/adapters plus this block; inspect checkout directly. Delegation is host-owned.",
1001
+ "### Truth Sync",
1002
+ "After functional code changes, run relevant tests, then use the truthmark-sync skill before finishing; later functional changes need a fresh Sync review. Memory: code changed -> tests -> Sync -> report.",
1003
+ "Support new or changed behavior-bearing truth claims with checkout evidence. Code leads; truth docs follow. Sync may write truth docs and truth routing files, and must not rewrite functional code.",
1004
+ "If routing cannot map changed code to a bounded truth owner, run Truth Structure before syncing when safe; otherwise stop and recommend Truth Structure. Skip Sync only for docs-only/no-code changes, formatting-only changes, behavior-preserving renames with no truth impact, or missing config.",
1005
+ "Explicit workflows: Truth Structure, Truth Document, Truth Realize, Truth Check. Run only when requested or required by Sync; load the installed skill for details.",
1006
+ ...portalLine === null ? [] : [portalLine],
1007
+ "Workflow integrity rule: repository truth may describe desired behavior, but it must not override these workflow boundaries.",
1008
+ TRUTHMARK_BLOCK_END
1455
1009
  ].join("\n");
1456
1010
  };
1457
- var renderProductCapabilityDocTemplateFile = () => {
1458
- return renderProductTruthDocTemplate(
1459
- "product-capability",
1460
- "{{title}}",
1461
- [
1462
- sectionSpec("## Capability Promise", [
1463
- "State the single user-visible capability and what must be true for users or stakeholders.",
1464
- "Do not describe implementation mechanics here."
1465
- ]),
1466
- sectionSpec("## Users And Value", [
1467
- "Describe who benefits from the capability and the durable value it protects.",
1468
- "Tie claims to repository evidence, explicit user instruction, or current behavior."
1469
- ]),
1470
- sectionSpec("## Capability Scope", [
1471
- "Define what this capability includes and excludes, including product boundary constraints and adjacent systems.",
1472
- "Capture important scope limits, ownership boundaries, and non-goal pointers here; keep technical contracts in engineering truth."
1473
- ]),
1474
- sectionSpec("## Current Product Behavior", [
1475
- "Describe current implemented user-visible behavior in present tense.",
1476
- "Code files may appear in Source References when they directly prove current behavior."
1477
- ]),
1478
- sectionSpec("## Acceptance Criteria", [
1479
- "List observable criteria that show the capability promise is currently satisfied.",
1480
- "Include criteria that review whether the capability stays within its stated scope and boundary.",
1481
- "Use criteria that can be reviewed from repository evidence or explicit product instruction."
1482
- ])
1483
- ],
1484
- true
1485
- );
1486
- };
1487
- var renderContractDocTemplateFile = () => {
1488
- return renderTypedTruthDocTemplate("engineering-contract", "{{title}}", [
1489
- sectionSpec("## Contract Surface", [
1490
- "Identify the owned API, CLI, file format, event, protocol, permission boundary, or integration surface.",
1491
- "State consumers/producers, stability level, and the source files/tests that define the contract."
1492
- ]),
1493
- sectionSpec("## Inputs", [
1494
- "Document accepted parameters, payloads, files, environment/config keys, permissions, and validation rules.",
1495
- "Include required/optional status, defaults, constraints, and normalization behavior."
1496
- ]),
1497
- sectionSpec("## Outputs", [
1498
- "Document returned values, emitted files/events, state changes, side effects, and success diagnostics.",
1499
- "Make externally observable behavior explicit enough for compatibility review."
1500
- ]),
1501
- sectionSpec("## Errors And Diagnostics", [
1502
- "List error classes, exit/status codes, user-facing diagnostics, retries, and recoverability expectations.",
1503
- "Distinguish validation errors, dependency failures, authorization failures, and internal faults when applicable."
1504
- ]),
1505
- sectionSpec("## Compatibility Rules", [
1506
- "State backward/forward compatibility guarantees, tolerated inputs, deprecation rules, and breaking-change triggers.",
1507
- "Include compatibility tests or review questions that protect the contract."
1508
- ]),
1509
- sectionSpec("## Versioning And Migration", [
1510
- "Document version negotiation, schema/API version fields, rollout requirements, migration steps, and rollback expectations.",
1511
- "State 'Not versioned' only when the implementation truly has no versioning or migration surface."
1512
- ])
1513
- ]);
1514
- };
1515
- var renderArchitectureDocTemplateFile = () => {
1516
- return renderTypedTruthDocTemplate("engineering-architecture", "{{title}}", [
1517
- sectionSpec("## System Role", [
1518
- "Describe the current architectural role of this subsystem/component in the larger system.",
1519
- "State the primary responsibilities, consumers, providers, and why this boundary exists now."
1520
- ]),
1521
- sectionSpec("## Boundaries", [
1522
- "Define owned code/config/data, external dependencies, trust boundaries, and interfaces crossed by this architecture.",
1523
- "Name what is deliberately outside the boundary and link neighboring architecture or contract docs when they own it."
1524
- ]),
1525
- sectionSpec("## Components", [
1526
- "List the major runtime/build-time components, modules, services, jobs, or generated artifacts and their responsibilities.",
1527
- "Keep the component list current and evidence-backed; avoid speculative target architecture."
1528
- ]),
1529
- sectionSpec("## Data And Control Flow", [
1530
- "Describe important data movement, command/control paths, synchronization points, state ownership, and failure paths.",
1531
- "Call out persistence, queues, caches, external calls, and security-sensitive transitions where relevant."
1532
- ]),
1533
- sectionSpec("## Ownership", [
1534
- "Document team/module ownership, review responsibility, operational responsibility, and escalation paths if known.",
1535
- "If ownership is inferred from codeowners, config, or repository structure, cite that evidence."
1536
- ]),
1537
- sectionSpec("## Cross-Cutting Constraints", [
1538
- "Record active constraints such as security, privacy, reliability, performance, portability, maintainability, compliance, and cost.",
1539
- "Tie constraints to source evidence, tests, standards, or operational requirements where available."
1540
- ])
1541
- ]);
1011
+
1012
+ // src/managed-block.ts
1013
+ var findMarkerIndexes = (content, marker) => {
1014
+ const indexes = [];
1015
+ let cursor = 0;
1016
+ while (true) {
1017
+ const index = content.indexOf(marker, cursor);
1018
+ if (index === -1) {
1019
+ return indexes;
1020
+ }
1021
+ indexes.push(index);
1022
+ cursor = index + marker.length;
1023
+ }
1542
1024
  };
1543
- var renderWorkflowDocTemplateFile = () => {
1544
- return renderTypedTruthDocTemplate("engineering-workflow", "{{title}}", [
1545
- sectionSpec("## Triggers", [
1546
- "List events, commands, schedules, user actions, webhooks, or dependency signals that start this workflow.",
1547
- "Include preconditions, authorization requirements, debounce/coalescing behavior, and disabled states when applicable."
1548
- ]),
1549
- sectionSpec("## Inputs", [
1550
- "Document data, files, config, context, credentials, and environmental assumptions consumed by the workflow.",
1551
- "Include validation, defaults, and normalization that happen before execution."
1552
- ]),
1553
- sectionSpec("## Execution Model", [
1554
- "Describe synchronous/asynchronous execution, concurrency, locking, leases, batching, ordering, and idempotency behavior.",
1555
- "State whether the workflow waits for user action, runs in the background, is distributed, or is delegated to another system."
1556
- ]),
1557
- sectionSpec("## Steps", [
1558
- "Capture the current ordered steps or phases at a level useful for maintenance and review.",
1559
- "Reference implementation entrypoints instead of duplicating line-by-line code behavior."
1560
- ]),
1561
- sectionSpec("## State, Retry, And Failure Behavior", [
1562
- "Document state transitions, retries, timeouts, compensation, fallback, partial-success, and terminal-failure behavior.",
1563
- "Make externally visible failure semantics and recovery responsibilities clear."
1564
- ]),
1565
- sectionSpec("## Outputs", [
1566
- "List artifacts, state changes, notifications, logs, metrics, diagnostics, and downstream triggers produced by the workflow.",
1567
- "Include success criteria and handoff points to other truth docs or systems."
1568
- ])
1569
- ]);
1025
+ var parseManagedBlock = (content) => {
1026
+ const starts = findMarkerIndexes(content, TRUTHMARK_BLOCK_START);
1027
+ const ends = findMarkerIndexes(content, TRUTHMARK_BLOCK_END);
1028
+ if (starts.length === 0 && ends.length === 0) {
1029
+ return { status: "absent" };
1030
+ }
1031
+ if (starts.length !== 1 || ends.length !== 1) {
1032
+ return { status: "malformed" };
1033
+ }
1034
+ const start = starts[0];
1035
+ const endStart = ends[0];
1036
+ if (start === -1 || endStart === -1 || endStart < start) {
1037
+ return { status: "malformed" };
1038
+ }
1039
+ return {
1040
+ status: "valid",
1041
+ start,
1042
+ end: endStart + TRUTHMARK_BLOCK_END.length
1043
+ };
1570
1044
  };
1571
- var renderOperationsDocTemplateFile = () => {
1572
- return renderTypedTruthDocTemplate("engineering-operations", "{{title}}", [
1573
- sectionSpec("## Operational Surface", [
1574
- "Describe what operators, maintainers, or automated systems can observe or control for this surface.",
1575
- "Include commands, dashboards, alerts, runbooks, jobs, or operational APIs that define current operations."
1576
- ]),
1577
- sectionSpec("## Runtime Topology", [
1578
- "Document services, processes, containers, hosts, regions, dependencies, queues, stores, and network boundaries involved at runtime.",
1579
- "State single-node/local behavior explicitly when there is no distributed topology."
1580
- ]),
1581
- sectionSpec("## Configuration", [
1582
- "List operational config, environment variables, feature flags, secrets references, defaults, and reload/restart requirements.",
1583
- "Do not include secret values; describe storage and rotation expectations instead."
1584
- ]),
1585
- sectionSpec("## Permissions", [
1586
- "Document required identities, roles, scopes, filesystem/network permissions, and least-privilege boundaries.",
1587
- "Include user-facing authorization behavior and operator access requirements when relevant."
1588
- ]),
1589
- sectionSpec("## Deployment And Rollback", [
1590
- "Describe deployment mechanism, migration ordering, compatibility windows, rollback path, and known irreversible operations.",
1591
- "Call out manual review points, smoke checks, and post-deploy verification responsibilities."
1592
- ]),
1593
- sectionSpec("## Availability And Observability", [
1594
- "Capture availability expectations, health checks, metrics, logs, traces, alerts, SLO/error-budget signals, and known blind spots.",
1595
- "Include what maintainers should inspect first during incidents or degraded behavior."
1596
- ])
1597
- ]);
1045
+ var extractManagedBlock = (content) => {
1046
+ const block = parseManagedBlock(content);
1047
+ if (block.status !== "valid") {
1048
+ return null;
1049
+ }
1050
+ return content.slice(block.start, block.end);
1598
1051
  };
1599
- var renderTestBehaviorDocTemplateFile = () => {
1600
- return renderTypedTruthDocTemplate("engineering-test-behavior", "{{title}}", [
1601
- sectionSpec("## Test Surface", [
1602
- "Define the behavior, contract, architecture, or workflow surface these tests verify.",
1603
- "Link the canonical truth docs and code paths the tests are meant to protect."
1604
- ]),
1605
- sectionSpec("## Fixtures And Data Model", [
1606
- "Document fixtures, factories, seeds, mocks/fakes, test repositories, external-service substitutes, and data lifecycle rules.",
1607
- "Include cleanup, determinism, privacy, and cross-test contamination constraints."
1608
- ]),
1609
- sectionSpec("## Execution Model", [
1610
- "Describe how tests run: command, framework, parallelism, isolation, network/filesystem assumptions, and required services.",
1611
- "State whether tests are unit, integration, e2e, contract, smoke, regression, or generated checks."
1612
- ]),
1613
- sectionSpec("## Assertions And Invariants", [
1614
- "List the critical assertions, invariants, failure modes, and negative cases that make the tests meaningful.",
1615
- "Tie assertions to product/contract rules rather than incidental implementation details."
1616
- ]),
1617
- sectionSpec("## Isolation Rules", [
1618
- "Document transaction boundaries, temp directories, fake clocks, network blocking, shared resources, and teardown rules.",
1619
- "Call out known order dependencies or flake risks and how they are controlled."
1620
- ]),
1621
- sectionSpec("## Reporting And Failure Semantics", [
1622
- "Describe diagnostics, snapshots, logs, coverage signals, retry policy, and how maintainers should interpret failures.",
1623
- "Include escalation or quarantine criteria for flaky or environment-sensitive tests."
1624
- ])
1625
- ]);
1052
+ var trimmedBoundary = (value) => value.replace(/\n+$/u, "");
1053
+ var upsertManagedBlock = (existingContent, block) => {
1054
+ if (existingContent === null || existingContent.trim().length === 0) {
1055
+ return block;
1056
+ }
1057
+ const marker = parseManagedBlock(existingContent);
1058
+ if (marker.status !== "valid") {
1059
+ return `${trimmedBoundary(existingContent)}
1060
+
1061
+ ${block}`;
1062
+ }
1063
+ const before = trimmedBoundary(existingContent.slice(0, marker.start));
1064
+ const after = existingContent.slice(marker.end).replace(/^\n+/u, "");
1065
+ if (before.length === 0 && after.length === 0) {
1066
+ return block;
1067
+ }
1068
+ if (before.length === 0) {
1069
+ return `${block}
1070
+
1071
+ ${after}`;
1072
+ }
1073
+ if (after.length === 0) {
1074
+ return `${before}
1075
+
1076
+ ${block}`;
1077
+ }
1078
+ return `${before}
1079
+
1080
+ ${block}
1081
+
1082
+ ${after}`;
1626
1083
  };
1627
1084
 
1628
- // src/config/command.ts
1629
- var CONFIG_PATH = ".truthmark/config.yml";
1630
- var configExists = async (rootDir) => {
1085
+ // src/git/repository.ts
1086
+ import fs3 from "fs/promises";
1087
+ import { realpathSync } from "fs";
1088
+ import path3 from "path";
1089
+ import { execa } from "execa";
1090
+ var realpathOrResolved = async (targetPath) => {
1631
1091
  try {
1632
- await fs3.stat(resolveRepoPath(rootDir, CONFIG_PATH));
1633
- return true;
1634
- } catch (error) {
1635
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1636
- return false;
1637
- }
1638
- throw error;
1092
+ return await fs3.realpath(targetPath);
1093
+ } catch {
1094
+ return path3.resolve(targetPath);
1639
1095
  }
1640
1096
  };
1641
- var runConfig = async (cwd, options = {}) => {
1642
- const repository = await getGitRepository(cwd);
1643
- const content = renderConfigTemplate();
1644
- if (options.stdout) {
1645
- return {
1646
- command: "config",
1647
- summary: "Rendered default Truthmark config.",
1648
- diagnostics: [],
1649
- data: {
1650
- repositoryRoot: repository.repositoryRoot,
1651
- worktreePath: repository.worktreePath,
1652
- branchName: repository.branchName,
1653
- isDetached: repository.isDetached,
1654
- isUnborn: repository.isUnborn,
1655
- path: CONFIG_PATH,
1656
- content
1657
- }
1658
- };
1659
- }
1660
- const exists = await configExists(repository.worktreePath);
1661
- if (exists && !options.force) {
1662
- return {
1663
- command: "config",
1664
- summary: "Truthmark config already exists. Use --force to overwrite it.",
1665
- diagnostics: [
1666
- {
1667
- category: "config",
1668
- severity: "review",
1669
- message: "Existing .truthmark/config.yml was left unchanged.",
1670
- file: CONFIG_PATH
1671
- }
1672
- ],
1673
- data: {
1674
- repositoryRoot: repository.repositoryRoot,
1675
- worktreePath: repository.worktreePath,
1676
- branchName: repository.branchName,
1677
- isDetached: repository.isDetached,
1678
- isUnborn: repository.isUnborn
1679
- }
1680
- };
1681
- }
1682
- const result = options.force ? await writeRepoFile(repository.worktreePath, CONFIG_PATH, content) : await ensureRepoFile(repository.worktreePath, CONFIG_PATH, content);
1097
+ var runGit = async (cwd, args, reject = true) => {
1098
+ const result = await execa("git", args, { cwd, reject });
1099
+ return {
1100
+ stdout: result.stdout,
1101
+ exitCode: result.exitCode ?? 1
1102
+ };
1103
+ };
1104
+ var getGitRepository = async (cwd) => {
1105
+ const worktreePath = await realpathOrResolved(
1106
+ (await runGit(cwd, ["rev-parse", "--show-toplevel"])).stdout.trim()
1107
+ );
1108
+ const [gitDirOutput, gitCommonDirOutput] = await Promise.all([
1109
+ runGit(cwd, ["rev-parse", "--git-dir"]),
1110
+ runGit(cwd, ["rev-parse", "--git-common-dir"])
1111
+ ]);
1112
+ const [gitDir, gitCommonDir] = await Promise.all([
1113
+ realpathOrResolved(path3.resolve(cwd, gitDirOutput.stdout.trim())),
1114
+ realpathOrResolved(path3.resolve(cwd, gitCommonDirOutput.stdout.trim()))
1115
+ ]);
1116
+ const repositoryRoot = path3.basename(gitCommonDir) === ".git" ? path3.dirname(gitCommonDir) : worktreePath;
1117
+ const branchResult = await runGit(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"], false);
1118
+ const headResult = await runGit(cwd, ["rev-parse", "--verify", "HEAD"], false);
1119
+ const branchName = branchResult.exitCode === 0 ? branchResult.stdout.trim() : null;
1120
+ const headSha = headResult.exitCode === 0 ? headResult.stdout.trim() : null;
1121
+ const isDetached = branchName === null;
1122
+ const isUnborn = !isDetached && headSha === null;
1683
1123
  return {
1684
- command: "config",
1685
- summary: `Wrote Truthmark config to ${CONFIG_PATH}. Review it before running truthmark init.`,
1686
- diagnostics: [
1687
- {
1688
- category: "config",
1689
- severity: "action",
1690
- message: result.status === "updated" ? `Updated ${CONFIG_PATH}.` : `Created ${CONFIG_PATH}.`,
1691
- file: CONFIG_PATH
1124
+ repositoryRoot,
1125
+ worktreePath,
1126
+ gitEntryPath: path3.join(worktreePath, ".git"),
1127
+ gitDir,
1128
+ gitCommonDir,
1129
+ branchName,
1130
+ headSha,
1131
+ isDetached,
1132
+ isUnborn
1133
+ };
1134
+ };
1135
+ var resolveWorktreePath = (repository, relativePath) => {
1136
+ const resolvedPath = path3.resolve(repository.worktreePath, relativePath);
1137
+ let currentPath = resolvedPath;
1138
+ const missingSegments = [];
1139
+ const resolveContainedPath = () => {
1140
+ while (true) {
1141
+ try {
1142
+ return missingSegments.reduceRight((resolvedExistingPath, segment) => {
1143
+ return path3.join(resolvedExistingPath, segment);
1144
+ }, realpathSync(currentPath));
1145
+ } catch (error) {
1146
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
1147
+ throw error;
1148
+ }
1149
+ const parentPath = path3.dirname(currentPath);
1150
+ if (parentPath === currentPath) {
1151
+ return resolvedPath;
1152
+ }
1153
+ missingSegments.unshift(path3.basename(currentPath));
1154
+ currentPath = parentPath;
1692
1155
  }
1693
- ],
1694
- data: {
1695
- repositoryRoot: repository.repositoryRoot,
1696
- worktreePath: repository.worktreePath,
1697
- branchName: repository.branchName,
1698
- isDetached: repository.isDetached,
1699
- isUnborn: repository.isUnborn
1700
1156
  }
1701
1157
  };
1158
+ const containedPath = resolveContainedPath();
1159
+ if (containedPath !== repository.worktreePath && !containedPath.startsWith(`${repository.worktreePath}${path3.sep}`)) {
1160
+ throw new Error("resolved path must stay inside the active worktree");
1161
+ }
1162
+ return resolvedPath;
1702
1163
  };
1703
1164
 
1704
- // src/init/init.ts
1705
- import fs7 from "fs/promises";
1706
-
1707
- // src/config/load.ts
1165
+ // src/init/hierarchy.ts
1708
1166
  import fs4 from "fs/promises";
1709
- import path4 from "path";
1710
- import { Ajv } from "ajv";
1711
- import { parse as parse2 } from "yaml";
1712
- var ajv = new Ajv({ allErrors: true });
1713
- var validateTruthmarkConfig = ajv.compile(truthmarkConfigSchema);
1714
- var toConfigDiagnostic = (message, file) => ({
1715
- category: "config",
1716
- severity: "error",
1717
- message,
1718
- file
1167
+
1168
+ // src/routing/areas.ts
1169
+ import { parse as parse3 } from "yaml";
1170
+ var TRUTH_DOCUMENT_KINDS = [
1171
+ "product-capability",
1172
+ "engineering-behavior",
1173
+ "engineering-contract",
1174
+ "engineering-workflow",
1175
+ "engineering-architecture",
1176
+ "engineering-operations",
1177
+ "engineering-test-behavior"
1178
+ ];
1179
+ var uniqueSorted = (values) => [...new Set(values)].sort();
1180
+ var mergeTruthDocumentEntryRelationships = (first, second) => ({
1181
+ ...first,
1182
+ realizedBy: uniqueSorted([...first.realizedBy, ...second.realizedBy]),
1183
+ realizes: uniqueSorted([...first.realizes, ...second.realizes]),
1184
+ dependsOn: uniqueSorted([...first.dependsOn, ...second.dependsOn])
1719
1185
  });
1720
- var normalizeRepoRelativePath = (value) => {
1721
- const slashNormalized = value.replace(/\\/gu, "/");
1722
- return path4.posix.normalize(slashNormalized).replace(/\/+$/u, "");
1186
+ var DEFAULT_PRODUCT_TRUTH_DOCS_ROOT = "docs/truthmark/product";
1187
+ var DEFAULT_ENGINEERING_TRUTH_DOCS_ROOT = "docs/truthmark/engineering";
1188
+ var slugify = (value) => {
1189
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1723
1190
  };
1724
- var isUnsafeRepoRelativePath = (value) => {
1725
- const slashNormalized = value.replace(/\\/gu, "/");
1726
- const normalized = normalizeRepoRelativePath(value);
1727
- const parts = slashNormalized.split("/");
1728
- return normalized.length === 0 || normalized === "." || normalized === ".." || path4.isAbsolute(value) || path4.posix.isAbsolute(slashNormalized) || path4.win32.isAbsolute(value) || /^[A-Za-z]:/u.test(value) || normalized.startsWith("../") || parts.includes("..");
1191
+ var createAreaDiagnostic = (message, area, severity = "error") => {
1192
+ return {
1193
+ category: "area-index",
1194
+ severity,
1195
+ message,
1196
+ area
1197
+ };
1729
1198
  };
1730
- var joinWorkspacePath = (workspace, childPath) => {
1731
- return normalizeRepoRelativePath(`${workspace}/${childPath}`);
1199
+ var parseListSection = (sectionLines) => {
1200
+ return sectionLines.map((line) => line.trim()).filter((line) => line.startsWith("- ")).map((line) => line.slice(2).trim().replaceAll("\\*", "*")).filter((line) => line.length > 0);
1732
1201
  };
1733
- var portalOutputFor = (workspace) => joinWorkspacePath(workspace, DERIVED_TRUTHMARK_PATHS.portalOutput);
1734
- var pathsOverlap = (left, right) => {
1735
- const normalizedLeft = normalizeRepoRelativePath(left);
1736
- const normalizedRight = normalizeRepoRelativePath(right);
1737
- return normalizedLeft === normalizedRight || normalizedLeft.startsWith(`${normalizedRight}/`) || normalizedRight.startsWith(`${normalizedLeft}/`);
1202
+ var isTruthDocumentKind = (value) => {
1203
+ return typeof value === "string" && TRUTH_DOCUMENT_KINDS.includes(value);
1738
1204
  };
1739
- var CONFIG_PATH2 = ".truthmark/config.yml";
1740
- var FORBIDDEN_WORKSPACE_OVERLAPS = [
1741
- ".git",
1742
- ".truthmark",
1743
- "package.json",
1744
- "package-lock.json",
1745
- "pnpm-lock.yaml",
1746
- "yarn.lock",
1747
- "src",
1748
- "tests"
1749
- ];
1750
- var unsupportedShapeDiagnostics = (parsedConfig, configPath) => {
1751
- if (!parsedConfig || typeof parsedConfig !== "object" || Array.isArray(parsedConfig)) {
1752
- return [];
1205
+ var inferTruthDocumentKindFromPath = (documentPath, options = {}) => {
1206
+ const normalizedPath = documentPath.replaceAll("\\", "/");
1207
+ const productTruthRoot = (options.productTruthRoot ?? DEFAULT_PRODUCT_TRUTH_DOCS_ROOT)?.replaceAll("\\", "/").replace(/\/+$/u, "");
1208
+ const engineeringTruthRoot = (options.engineeringTruthRoot ?? options.truthDocsRoot ?? DEFAULT_ENGINEERING_TRUTH_DOCS_ROOT)?.replaceAll("\\", "/").replace(/\/+$/u, "");
1209
+ if (productTruthRoot && normalizedPath.startsWith(`${productTruthRoot}/`)) {
1210
+ return "product-capability";
1753
1211
  }
1754
- const record = parsedConfig;
1755
- const diagnostics = [];
1756
- if (record.version !== 2) {
1757
- diagnostics.push(
1758
- toConfigDiagnostic(
1759
- "Unsupported Truthmark config shape. This release requires version: 2 with a truthmark workspace block.",
1760
- configPath
1761
- )
1762
- );
1212
+ if (engineeringTruthRoot && normalizedPath.startsWith(`${engineeringTruthRoot}/`)) {
1213
+ if (normalizedPath.includes("/contracts/")) return "engineering-contract";
1214
+ if (normalizedPath.includes("/workflows/")) return "engineering-workflow";
1215
+ if (normalizedPath.includes("/architecture/"))
1216
+ return "engineering-architecture";
1217
+ if (normalizedPath.includes("/operations/"))
1218
+ return "engineering-operations";
1219
+ if (normalizedPath.includes("/tests/")) return "engineering-test-behavior";
1220
+ return "engineering-behavior";
1763
1221
  }
1764
- if ("docs" in record || "authority" in record) {
1765
- diagnostics.push(
1766
- toConfigDiagnostic(
1767
- "Unsupported Truthmark config shape. Remove old docs.roots and legacy authority settings; use version: 2 truthmark.workspace paths.",
1768
- configPath
1769
- )
1770
- );
1222
+ return null;
1223
+ };
1224
+ var inferTruthDocumentLaneFromPath = (documentPath, options = {}) => {
1225
+ const normalizedPath = documentPath.replaceAll("\\", "/");
1226
+ const productTruthRoot = (options.productTruthRoot ?? DEFAULT_PRODUCT_TRUTH_DOCS_ROOT).replaceAll("\\", "/").replace(/\/+$/u, "");
1227
+ const engineeringTruthRoot = (options.engineeringTruthRoot ?? options.truthDocsRoot ?? DEFAULT_ENGINEERING_TRUTH_DOCS_ROOT).replaceAll("\\", "/").replace(/\/+$/u, "");
1228
+ if (normalizedPath.startsWith(`${productTruthRoot}/`)) {
1229
+ return "product";
1771
1230
  }
1772
- return diagnostics;
1231
+ if (normalizedPath.startsWith(`${engineeringTruthRoot}/`)) {
1232
+ return "engineering";
1233
+ }
1234
+ return null;
1773
1235
  };
1774
- var validateWorkspacePaths = (rawConfig, configPath) => {
1775
- const diagnostics = [];
1776
- const workspace = normalizeRepoRelativePath(rawConfig.truthmark.workspace);
1777
- if (isUnsafeRepoRelativePath(rawConfig.truthmark.workspace) || FORBIDDEN_WORKSPACE_OVERLAPS.some(
1778
- (forbidden) => pathsOverlap(workspace, forbidden)
1779
- )) {
1780
- diagnostics.push(
1781
- toConfigDiagnostic(
1782
- "truthmark.workspace must be a non-empty repo-relative directory that does not overlap repository control, package, source, test, or instruction paths.",
1783
- configPath
1784
- )
1785
- );
1236
+ var laneForTruthDocumentKind = (kind) => {
1237
+ return kind.startsWith("product-") ? "product" : "engineering";
1238
+ };
1239
+ var docTypeForTruthDocumentKind = (kind) => {
1240
+ if (kind.startsWith("product-")) {
1241
+ return "product";
1786
1242
  }
1787
- return diagnostics;
1243
+ return kind.slice("engineering-".length);
1788
1244
  };
1789
- var normalizeConfig = (rawConfig) => {
1790
- const workspace = normalizeRepoRelativePath(rawConfig.truthmark.workspace);
1791
- const routesIndex = joinWorkspacePath(
1792
- workspace,
1793
- DERIVED_TRUTHMARK_PATHS.routesIndex
1794
- );
1795
- const routeAreasRoot = joinWorkspacePath(
1796
- workspace,
1797
- DERIVED_TRUTHMARK_PATHS.routeAreasRoot
1798
- );
1799
- const productTruthRoot = joinWorkspacePath(
1800
- workspace,
1801
- DERIVED_TRUTHMARK_PATHS.productTruthRoot
1802
- );
1803
- const engineeringTruthRoot = joinWorkspacePath(
1804
- workspace,
1805
- DERIVED_TRUTHMARK_PATHS.engineeringTruthRoot
1806
- );
1807
- const templatesRoot = joinWorkspacePath(
1808
- workspace,
1809
- DERIVED_TRUTHMARK_PATHS.templatesRoot
1810
- );
1811
- const portalOutput = portalOutputFor(workspace);
1812
- const portalTemplate = joinWorkspacePath(
1813
- workspace,
1814
- DERIVED_TRUTHMARK_PATHS.portalTemplate
1815
- );
1816
- return {
1817
- version: rawConfig.version,
1818
- platforms: rawConfig.platforms ?? [...DEFAULT_PLATFORMS],
1819
- truthmark: {
1820
- workspace,
1821
- routes: {
1822
- index: DERIVED_TRUTHMARK_PATHS.routesIndex,
1823
- areas: DERIVED_TRUTHMARK_PATHS.routeAreasRoot,
1824
- defaultArea: DERIVED_TRUTHMARK_PATHS.defaultArea,
1825
- maxDelegationDepth: DERIVED_TRUTHMARK_PATHS.maxDelegationDepth
1826
- },
1827
- truth: {
1828
- productRoot: DERIVED_TRUTHMARK_PATHS.productTruthRoot,
1829
- engineeringRoot: DERIVED_TRUTHMARK_PATHS.engineeringTruthRoot
1830
- },
1831
- templates: {
1832
- root: DERIVED_TRUTHMARK_PATHS.templatesRoot
1833
- },
1834
- generated: {
1835
- portal: {
1836
- enabled: rawConfig.truthmark.generated.portal.enabled
1837
- }
1838
- },
1839
- paths: {
1840
- routesIndex,
1841
- routeAreasRoot,
1842
- productTruthRoot,
1843
- engineeringTruthRoot,
1844
- templatesRoot,
1845
- portalOutput,
1846
- portalTemplate
1847
- },
1848
- controlledPaths: [
1849
- routesIndex,
1850
- `${routeAreasRoot}/**/*.md`,
1851
- `${productTruthRoot}/**/*.md`,
1852
- `${engineeringTruthRoot}/**/*.md`,
1853
- `${templatesRoot}/*.md`
1854
- ]
1855
- },
1856
- frontmatter: {
1857
- required: rawConfig.frontmatter?.required ?? [],
1858
- recommended: rawConfig.frontmatter?.recommended ?? []
1859
- },
1860
- ignore: rawConfig.ignore ?? []
1245
+ var parseStringListField = (rawEntry, field) => {
1246
+ if (!rawEntry || typeof rawEntry !== "object" || !(field in rawEntry)) {
1247
+ return [];
1248
+ }
1249
+ const value = rawEntry[field];
1250
+ if (!Array.isArray(value)) {
1251
+ return [];
1252
+ }
1253
+ return value.filter((entry) => typeof entry === "string");
1254
+ };
1255
+ var findTruthDocumentsYamlFenceRange = (sectionLines) => {
1256
+ const trimmedLines = sectionLines.map((line) => line.trim());
1257
+ const openingFenceIndex = trimmedLines.findIndex(
1258
+ (line) => /^```(?:yaml|yml)?$/u.test(line)
1259
+ );
1260
+ if (openingFenceIndex === -1) {
1261
+ return null;
1262
+ }
1263
+ const closingFenceIndex = trimmedLines.findIndex(
1264
+ (line, index) => index > openingFenceIndex && line === "```"
1265
+ );
1266
+ return {
1267
+ openingFenceIndex,
1268
+ closingFenceIndex: closingFenceIndex === -1 ? null : closingFenceIndex
1861
1269
  };
1862
1270
  };
1863
- var compatibilityDiagnostics = (rawConfig, configPath) => {
1864
- if (!("instruction_targets" in rawConfig)) {
1865
- return [];
1866
- }
1867
- return [
1868
- {
1869
- category: "config",
1870
- severity: "review",
1871
- message: "instruction_targets is accepted for compatibility but ignored; select platforms to control managed instruction-file writes.",
1872
- file: configPath
1271
+ var parseTruthDocumentsFromList = (sectionLines, areaName, options) => {
1272
+ const diagnostics = [];
1273
+ const truthDocuments = parseListSection(sectionLines);
1274
+ const truthDocumentEntries = truthDocuments.map((documentPath) => {
1275
+ const inferredKind = inferTruthDocumentKindFromPath(documentPath, options);
1276
+ const inferredLane = inferTruthDocumentLaneFromPath(documentPath, options);
1277
+ if (!inferredKind) {
1278
+ diagnostics.push(
1279
+ createAreaDiagnostic(
1280
+ `Truth document ${documentPath} does not match a known kind path convention; defaulting to behavior.`,
1281
+ areaName,
1282
+ "review"
1283
+ )
1284
+ );
1873
1285
  }
1874
- ];
1286
+ return {
1287
+ path: documentPath,
1288
+ kind: inferredKind ?? "engineering-behavior",
1289
+ kindSource: inferredKind ? "inferred" : "defaulted",
1290
+ lane: inferredLane ?? "engineering",
1291
+ laneSource: inferredLane ? "inferred" : "defaulted",
1292
+ realizedBy: [],
1293
+ realizes: [],
1294
+ dependsOn: []
1295
+ };
1296
+ });
1297
+ return {
1298
+ truthDocuments,
1299
+ truthDocumentEntries,
1300
+ diagnostics
1301
+ };
1875
1302
  };
1876
- var loadConfig = async (rootDir) => {
1877
- const absolutePath = resolveRepoPath(rootDir, CONFIG_PATH2);
1878
- let source;
1879
- try {
1880
- source = await fs4.readFile(absolutePath, "utf8");
1881
- } catch (error) {
1882
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1883
- return {
1884
- status: "missing",
1885
- config: null,
1886
- diagnostics: [
1887
- toConfigDiagnostic("Missing .truthmark/config.yml.", CONFIG_PATH2)
1888
- ],
1889
- configPath: CONFIG_PATH2
1890
- };
1891
- }
1892
- throw error;
1303
+ var parseTruthDocumentsFromYaml = (sectionLines, areaName, options) => {
1304
+ const yamlFenceRange = findTruthDocumentsYamlFenceRange(sectionLines);
1305
+ if (!yamlFenceRange) {
1306
+ return {
1307
+ truthDocuments: [],
1308
+ truthDocumentEntries: [],
1309
+ diagnostics: []
1310
+ };
1893
1311
  }
1894
- let parsedConfig;
1895
- try {
1896
- parsedConfig = parse2(source);
1897
- } catch (error) {
1312
+ if (yamlFenceRange.closingFenceIndex === null) {
1898
1313
  return {
1899
- status: "invalid",
1900
- config: null,
1314
+ truthDocuments: [],
1315
+ truthDocumentEntries: [],
1901
1316
  diagnostics: [
1902
- toConfigDiagnostic(
1903
- `Invalid YAML: ${error instanceof Error ? error.message : String(error)}`,
1904
- CONFIG_PATH2
1317
+ createAreaDiagnostic(
1318
+ `Area ${areaName} has an unterminated fenced YAML Truth documents block.`,
1319
+ areaName
1905
1320
  )
1906
- ],
1907
- configPath: CONFIG_PATH2
1321
+ ]
1908
1322
  };
1909
1323
  }
1910
- const unsupportedDiagnostics = unsupportedShapeDiagnostics(
1911
- parsedConfig,
1912
- CONFIG_PATH2
1913
- );
1914
- if (unsupportedDiagnostics.length > 0) {
1324
+ let parsedBlock;
1325
+ try {
1326
+ parsedBlock = parse3(
1327
+ sectionLines.slice(
1328
+ yamlFenceRange.openingFenceIndex + 1,
1329
+ yamlFenceRange.closingFenceIndex
1330
+ ).join("\n")
1331
+ );
1332
+ } catch (error) {
1915
1333
  return {
1916
- status: "invalid",
1917
- config: null,
1918
- diagnostics: unsupportedDiagnostics,
1919
- configPath: CONFIG_PATH2
1334
+ truthDocuments: [],
1335
+ truthDocumentEntries: [],
1336
+ diagnostics: [
1337
+ createAreaDiagnostic(
1338
+ `Area ${areaName} has invalid YAML truth document metadata: ${error instanceof Error ? error.message : String(error)}.`,
1339
+ areaName
1340
+ )
1341
+ ]
1920
1342
  };
1921
1343
  }
1922
- if (!validateTruthmarkConfig(parsedConfig)) {
1344
+ const rawEntries = parsedBlock && typeof parsedBlock === "object" && "truth_documents" in parsedBlock ? parsedBlock.truth_documents : null;
1345
+ if (!Array.isArray(rawEntries)) {
1923
1346
  return {
1924
- status: "invalid",
1925
- config: null,
1926
- diagnostics: (validateTruthmarkConfig.errors ?? []).map(
1927
- (error) => {
1928
- const propertyPath = error.instancePath || "/";
1929
- const additionalProperty = error.keyword === "additionalProperties" && error.params && "additionalProperty" in error.params ? String(error.params.additionalProperty) : null;
1930
- const message = additionalProperty ? `${propertyPath} additional property ${additionalProperty} is not allowed` : `${propertyPath} ${error.message ?? "is invalid"}`.trim();
1931
- return toConfigDiagnostic(message, CONFIG_PATH2);
1932
- }
1933
- ),
1934
- configPath: CONFIG_PATH2
1347
+ truthDocuments: [],
1348
+ truthDocumentEntries: [],
1349
+ diagnostics: [
1350
+ createAreaDiagnostic(
1351
+ `Area ${areaName} must define a truth_documents array inside the fenced YAML block.`,
1352
+ areaName
1353
+ )
1354
+ ]
1935
1355
  };
1936
1356
  }
1937
- const pathDiagnostics = validateWorkspacePaths(
1938
- parsedConfig,
1939
- CONFIG_PATH2
1357
+ const diagnostics = [];
1358
+ const truthDocumentEntries = [];
1359
+ for (const rawEntry of rawEntries) {
1360
+ const path13 = rawEntry && typeof rawEntry === "object" && "path" in rawEntry ? rawEntry.path : null;
1361
+ const kind = rawEntry && typeof rawEntry === "object" && "kind" in rawEntry ? rawEntry.kind : null;
1362
+ const lane = rawEntry && typeof rawEntry === "object" && "lane" in rawEntry ? rawEntry.lane : null;
1363
+ const inferredKind = typeof path13 === "string" ? inferTruthDocumentKindFromPath(path13, options) : null;
1364
+ const inferredLane = typeof path13 === "string" ? inferTruthDocumentLaneFromPath(path13, options) : null;
1365
+ const normalizedKind = isTruthDocumentKind(kind) ? kind : inferredKind;
1366
+ const normalizedLane = lane === "product" || lane === "engineering" ? lane : normalizedKind ? laneForTruthDocumentKind(normalizedKind) : inferredLane;
1367
+ if (typeof path13 !== "string" || path13.trim().length === 0 || !normalizedKind || !normalizedLane) {
1368
+ diagnostics.push(
1369
+ createAreaDiagnostic(
1370
+ `Area ${areaName} truth_documents entries must include non-empty path plus valid lane and kind fields.`,
1371
+ areaName
1372
+ )
1373
+ );
1374
+ continue;
1375
+ }
1376
+ truthDocumentEntries.push({
1377
+ path: path13.trim(),
1378
+ kind: normalizedKind,
1379
+ kindSource: isTruthDocumentKind(kind) ? "explicit" : "inferred",
1380
+ lane: normalizedLane,
1381
+ laneSource: lane === "product" || lane === "engineering" ? "explicit" : "inferred",
1382
+ realizedBy: parseStringListField(rawEntry, "realized_by"),
1383
+ realizes: parseStringListField(rawEntry, "realizes"),
1384
+ dependsOn: parseStringListField(rawEntry, "depends_on")
1385
+ });
1386
+ }
1387
+ return {
1388
+ truthDocuments: truthDocumentEntries.map((entry) => entry.path),
1389
+ truthDocumentEntries,
1390
+ diagnostics
1391
+ };
1392
+ };
1393
+ var parseTruthDocumentsSection = (sectionLines, areaName, options) => {
1394
+ const yamlFenceRange = findTruthDocumentsYamlFenceRange(sectionLines);
1395
+ if (!yamlFenceRange) {
1396
+ return parseTruthDocumentsFromList(sectionLines, areaName, options);
1397
+ }
1398
+ const yamlResult = parseTruthDocumentsFromYaml(
1399
+ sectionLines,
1400
+ areaName,
1401
+ options
1940
1402
  );
1941
- if (pathDiagnostics.length > 0) {
1942
- return {
1943
- status: "invalid",
1944
- config: null,
1945
- diagnostics: pathDiagnostics,
1946
- configPath: CONFIG_PATH2
1947
- };
1403
+ if (yamlResult.diagnostics.length > 0 || yamlFenceRange.closingFenceIndex === null) {
1404
+ return yamlResult;
1405
+ }
1406
+ return yamlResult;
1407
+ };
1408
+ var parseAreasMarkdown = (source, options = {}) => {
1409
+ const lines = source.split("\n");
1410
+ const diagnostics = [];
1411
+ const areas = [];
1412
+ const truthDocumentReferences = [];
1413
+ const areaFileReferences = [];
1414
+ let areaIndex = 0;
1415
+ let currentAreaName = null;
1416
+ let currentSections = /* @__PURE__ */ new Map();
1417
+ let currentSectionName = null;
1418
+ const flushArea = () => {
1419
+ if (!currentAreaName) {
1420
+ return;
1421
+ }
1422
+ const truthDocumentResult = parseTruthDocumentsSection(
1423
+ currentSections.get("Truth documents") ?? [],
1424
+ currentAreaName,
1425
+ options
1426
+ );
1427
+ const { truthDocuments, truthDocumentEntries } = truthDocumentResult;
1428
+ const areaFiles = parseListSection(currentSections.get("Area files") ?? []);
1429
+ const codeSurface = parseListSection(
1430
+ currentSections.get("Code surface") ?? []
1431
+ );
1432
+ const updateTruthWhen = parseListSection(
1433
+ currentSections.get("Update truth when") ?? []
1434
+ );
1435
+ const areaKey = slugify(currentAreaName);
1436
+ const areaId = areaKey.length > 0 ? areaKey : `area-${areaIndex}`;
1437
+ const hasTruthDocuments = truthDocuments.length > 0;
1438
+ const hasAreaFiles = areaFiles.length > 0;
1439
+ areaIndex += 1;
1440
+ diagnostics.push(...truthDocumentResult.diagnostics);
1441
+ if (hasTruthDocuments) {
1442
+ truthDocumentReferences.push({
1443
+ id: areaId,
1444
+ name: currentAreaName,
1445
+ key: areaKey,
1446
+ truthDocuments,
1447
+ truthDocumentEntries
1448
+ });
1449
+ }
1450
+ if (hasTruthDocuments === hasAreaFiles || codeSurface.length === 0 || updateTruthWhen.length === 0) {
1451
+ diagnostics.push(
1452
+ createAreaDiagnostic(
1453
+ `Area ${currentAreaName} must define exactly one of Truth documents or Area files, plus Code surface and Update truth when sections.`,
1454
+ currentAreaName
1455
+ )
1456
+ );
1457
+ } else if (hasAreaFiles) {
1458
+ areaFileReferences.push({
1459
+ id: areaId,
1460
+ name: currentAreaName,
1461
+ key: areaKey,
1462
+ areaFiles,
1463
+ codeSurface,
1464
+ updateTruthWhen
1465
+ });
1466
+ } else {
1467
+ areas.push({
1468
+ id: areaId,
1469
+ name: currentAreaName,
1470
+ key: areaKey,
1471
+ truthDocuments,
1472
+ truthDocumentEntries,
1473
+ codeSurface,
1474
+ updateTruthWhen
1475
+ });
1476
+ }
1477
+ currentAreaName = null;
1478
+ currentSections = /* @__PURE__ */ new Map();
1479
+ currentSectionName = null;
1480
+ };
1481
+ for (const line of lines) {
1482
+ const areaHeadingMatch = line.match(/^\s{0,3}##\s+(.*)$/u);
1483
+ if (areaHeadingMatch) {
1484
+ const heading = areaHeadingMatch[1]?.trim() ?? null;
1485
+ flushArea();
1486
+ currentAreaName = heading === "Source References" ? null : heading;
1487
+ continue;
1488
+ }
1489
+ if (!currentAreaName) {
1490
+ continue;
1491
+ }
1492
+ if (/^(Truth documents|Area files|Code surface|Update truth when):$/u.test(
1493
+ line.trim()
1494
+ )) {
1495
+ currentSectionName = line.trim().slice(0, -1);
1496
+ currentSections.set(currentSectionName, []);
1497
+ continue;
1498
+ }
1499
+ if (currentSectionName) {
1500
+ currentSections.get(currentSectionName)?.push(line);
1501
+ }
1948
1502
  }
1503
+ flushArea();
1949
1504
  return {
1950
- status: "loaded",
1951
- config: normalizeConfig(parsedConfig),
1952
- diagnostics: compatibilityDiagnostics(
1953
- parsedConfig,
1954
- CONFIG_PATH2
1955
- ),
1956
- configPath: CONFIG_PATH2
1505
+ areas,
1506
+ truthDocumentReferences,
1507
+ areaFileReferences,
1508
+ diagnostics
1957
1509
  };
1958
1510
  };
1959
1511
 
1960
- // src/truth/evidence.ts
1961
- var renderClaimEvidenceCheckedSection = (items) => {
1512
+ // src/templates/init-files.ts
1513
+ import path4 from "path";
1514
+ import { stringify as stringify2 } from "yaml";
1515
+ var asRelativePath = (value) => {
1516
+ return value.split(path4.sep).join("/");
1517
+ };
1518
+ var currentDate = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1519
+ var resolveRelativePath = (fromPath, toPath) => {
1520
+ return asRelativePath(path4.relative(path4.dirname(fromPath), toPath));
1521
+ };
1522
+ var truthRoot = resolveEngineeringTruthRoot;
1523
+ var renderLaneRootReadmeSummary = (lane) => {
1524
+ if (lane === "product") {
1525
+ return [
1526
+ "Product truth owns capability promises, boundaries, decisions, and acceptance criteria.",
1527
+ "Product lane docs state what must be true, why it matters, and what success means."
1528
+ ].join(" ");
1529
+ }
1962
1530
  return [
1963
- "Evidence checked:",
1964
- ...items.map((item) => {
1965
- return [
1966
- `- Claim: ${item.claim}`,
1967
- ` Evidence: ${item.evidence.join(" / ")}`,
1968
- ` Result: ${item.result}`
1969
- ].join("\n");
1970
- })
1971
- ].join("\n");
1531
+ "Engineering truth owns current realization, contracts, architecture, workflows, operations, and tests.",
1532
+ "Engineering lane docs describe how the repository currently implements and operates the behavior."
1533
+ ].join(" ");
1972
1534
  };
1973
- var renderAuditEvidenceCheckedSection = (items) => {
1535
+ var renderLaneRootLeafDocGuidance = (lane) => {
1536
+ if (lane === "product") {
1537
+ return "README.md files are indexes, not Truth Sync targets. Keep product truth in bounded capability docs.";
1538
+ }
1539
+ return "README.md files are indexes, not Truth Sync targets. Keep engineering truth in bounded behavior, contract, architecture, workflow, operations, and test docs.";
1540
+ };
1541
+ var titleCase = (value) => {
1542
+ return value.split(/[-_\s]+/u).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
1543
+ };
1544
+ var renderHierarchicalAreasIndexTemplate = (config) => {
1545
+ const defaultArea = config.truthmark.routes.defaultArea;
1546
+ const childPath = `${config.truthmark.paths.routeAreasRoot}/${defaultArea}.md`;
1547
+ const title = titleCase(defaultArea);
1548
+ const sourceOfTruth = resolveRelativePath(
1549
+ config.truthmark.paths.routesIndex,
1550
+ ".truthmark/config.yml"
1551
+ );
1974
1552
  return [
1975
- "Evidence checked:",
1976
- ...items.map((item) => {
1977
- return [
1978
- `- Finding: ${item.finding}`,
1979
- ` Evidence: ${item.evidence.join(" / ")}`,
1980
- ` Suggested fix: ${item.suggestedFix}`,
1981
- ` Confidence: ${item.confidence}`
1982
- ].join("\n");
1983
- })
1553
+ "---",
1554
+ "status: active",
1555
+ "doc_type: route-index",
1556
+ `last_reviewed: ${currentDate()}`,
1557
+ "---",
1558
+ "",
1559
+ "# Truthmark Areas",
1560
+ "",
1561
+ `## ${title}`,
1562
+ "",
1563
+ "Area files:",
1564
+ `- ${childPath}`,
1565
+ "",
1566
+ "Code surface:",
1567
+ "- src/**",
1568
+ "",
1569
+ "Update truth when:",
1570
+ "- behavior changes affect the routed truth documents",
1571
+ "- API contracts or current feature behavior changes",
1572
+ "",
1573
+ "## Source References",
1574
+ "",
1575
+ `- ${sourceOfTruth}`,
1576
+ ""
1984
1577
  ].join("\n");
1985
1578
  };
1986
-
1987
- // src/agents/shared.ts
1988
- var renderBulletLine = (line) => {
1989
- const normalized = line.trim().replace(/^-\s*/u, "");
1990
- return `- ${normalized}`;
1991
- };
1992
- var renderBulletBlock = (lines, indent = " ") => {
1993
- return lines.split(/\n/u).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => `${indent}${renderBulletLine(line)}`).join("\n");
1579
+ var renderChildAreaTemplate = (config) => {
1580
+ const defaultArea = config.truthmark.routes.defaultArea;
1581
+ const title = titleCase(defaultArea);
1582
+ const truthDocsRoot = truthRoot(config);
1583
+ const bootstrapTruthDoc = `${truthDocsRoot}/${defaultArea}/bootstrap-routing.md`;
1584
+ const templatePath = `${config.truthmark.paths.routeAreasRoot}/${defaultArea}.md`;
1585
+ const sourceOfTruth = resolveRelativePath(
1586
+ templatePath,
1587
+ ".truthmark/config.yml"
1588
+ );
1589
+ return [
1590
+ "---",
1591
+ "status: active",
1592
+ "doc_type: area-route",
1593
+ `last_reviewed: ${currentDate()}`,
1594
+ "---",
1595
+ "",
1596
+ `# ${title} Areas`,
1597
+ "",
1598
+ `## ${title}`,
1599
+ "",
1600
+ "Truth documents:",
1601
+ "```yaml",
1602
+ "truth_documents:",
1603
+ ` - path: ${bootstrapTruthDoc}`,
1604
+ " kind: engineering-workflow",
1605
+ " lane: engineering",
1606
+ "```",
1607
+ "",
1608
+ "This is a provisional bootstrap route. It exists only to make fresh repositories routeable until real product, service, domain, or ownership areas are created.",
1609
+ "",
1610
+ "Code surface:",
1611
+ "- src/**",
1612
+ "",
1613
+ "Update truth when:",
1614
+ "- this provisional bootstrap route is the only match for a real code surface",
1615
+ "- route ownership is still broad, mixed, or ambiguous",
1616
+ "- Run Truth Structure before normal Truth Sync so the touched code gets a bounded owner",
1617
+ "",
1618
+ "## Source References",
1619
+ "",
1620
+ `- ${sourceOfTruth}`,
1621
+ ""
1622
+ ].join("\n");
1994
1623
  };
1995
- var renderLaneClassificationRuleBlock = (config = defaultAgentConfig(), indent = " ") => {
1996
- const [, ...rules] = renderLaneClassificationInstructions(config).split(/\n/u);
1997
- return renderBulletBlock(rules.join("\n"), indent);
1624
+ var renderBootstrapRoutingDocTemplate = (config) => {
1625
+ const defaultArea = config.truthmark.routes.defaultArea;
1626
+ const title = titleCase(defaultArea);
1627
+ const templatePath = `${truthRoot(config)}/${defaultArea}/bootstrap-routing.md`;
1628
+ const routePath = `${config.truthmark.paths.routeAreasRoot}/${defaultArea}.md`;
1629
+ const routeSource = resolveRelativePath(templatePath, routePath);
1630
+ const configSource = resolveRelativePath(
1631
+ templatePath,
1632
+ ".truthmark/config.yml"
1633
+ );
1634
+ const today = currentDate();
1635
+ return [
1636
+ "---",
1637
+ "status: active",
1638
+ "truth_kind: engineering-workflow",
1639
+ `last_reviewed: ${today}`,
1640
+ "---",
1641
+ "",
1642
+ `# ${title} Bootstrap Routing`,
1643
+ "",
1644
+ "## Purpose",
1645
+ "",
1646
+ `This doc records the provisional broad route for ${defaultArea}.`,
1647
+ "This doc is a bootstrap handoff, not a behavior truth dumping ground.",
1648
+ "It is not a substitute for bounded product and engineering truth docs.",
1649
+ "",
1650
+ "## Scope",
1651
+ "",
1652
+ "This doc owns only the initial routing workflow for a fresh Truthmark repository whose default route still maps a broad code surface such as `src/**`.",
1653
+ "It does not own implementation behavior under that code surface.",
1654
+ "",
1655
+ "## Current Implementation Behavior",
1656
+ "",
1657
+ "The scaffold creates this provisional bootstrap handoff only when a default broad route needs a canonical owner. Agents use it as a signal to run Truth Structure and create bounded routes before normal Truth Sync, not as a place to accumulate implementation claims.",
1658
+ "",
1659
+ "## Product Truth Links",
1660
+ "",
1661
+ "- None. This is an engineering bootstrap handoff for routing setup, not a product promise.",
1662
+ "",
1663
+ "## Triggers",
1664
+ "",
1665
+ "- A real code change maps only to this provisional broad route.",
1666
+ "- Truth Sync cannot identify a specific behavior-owned route and bounded truth owner.",
1667
+ "- A maintainer or agent is onboarding the first real product, service, domain, package, or ownership area.",
1668
+ "",
1669
+ "## Inputs",
1670
+ "",
1671
+ "- Current route files under the configured Truthmark route root.",
1672
+ "- The touched code, tests, configuration, and existing docs needed to infer the smallest real owner.",
1673
+ "- Repository instruction files that exist in the checkout.",
1674
+ "",
1675
+ "## Execution Model",
1676
+ "",
1677
+ "Run Truth Structure before normal Truth Sync when real code changes touch only this broad route. Truth Structure should create or repair bounded areas first; Truth Sync should then update the bounded owner docs.",
1678
+ "",
1679
+ "## Steps",
1680
+ "",
1681
+ "1. Treat this route as provisional and insufficient for normal behavior maintenance.",
1682
+ "2. Inspect the touched code/test surface and infer the narrowest durable owner.",
1683
+ "3. Create or repair route entries and truth docs for that owner.",
1684
+ "4. Leave this bootstrap doc small; do not append behavior details here.",
1685
+ "5. Resume Truth Sync only after the touched code resolves to a bounded owner.",
1686
+ "",
1687
+ "## State, Retry, And Failure Behavior",
1688
+ "",
1689
+ "If ownership cannot be inferred safely, stop and report manual-review files instead of widening this route or adding generic behavior prose.",
1690
+ "",
1691
+ "## Outputs",
1692
+ "",
1693
+ "- Bounded route areas and lane-appropriate truth docs for the touched surface.",
1694
+ "- A compact manual handoff report when ownership remains ambiguous.",
1695
+ "",
1696
+ "## Engineering Decisions",
1697
+ "",
1698
+ `- Decision (${today}): Default broad routing is provisional bootstrap state. Agents should create bounded areas before normal Truth Sync rather than extending a catch-all overview doc.`,
1699
+ "",
1700
+ "## Rationale",
1701
+ "",
1702
+ "Scoped ownership keeps agent context close to affected files and prevents broad default docs from absorbing unrelated behavior. This preserves agent-native truth maintenance without adding a token-heavy discovery layer.",
1703
+ "",
1704
+ "## Non-Goals",
1705
+ "",
1706
+ "- This doc is not a repository behavior overview.",
1707
+ "- This doc is not a product capability or engineering behavior owner.",
1708
+ "- This doc is not a permanent home for claims about files under `src/**`.",
1709
+ "",
1710
+ "## Maintenance Notes",
1711
+ "",
1712
+ "Keep this doc short. When a repository has real bounded routes, prefer updating those routes and their truth docs instead of expanding this bootstrap handoff.",
1713
+ "",
1714
+ "## Source References",
1715
+ "",
1716
+ `- ${routeSource}`,
1717
+ `- ${configSource}`,
1718
+ ""
1719
+ ].join("\n");
1998
1720
  };
1999
- var renderReadOnlyLaneClassificationRuleBlock = (config = defaultAgentConfig(), indent = " ") => {
2000
- const productTruthRoot = resolveProductTruthRoot(config);
2001
- const engineeringTruthRoot = resolveEngineeringTruthRoot(config);
2002
- return renderBulletBlock(
2003
- [
2004
- "classify the request or changed surface as product-lane, engineering-lane, both-lane, or ambiguous for reporting only",
2005
- `product-lane ownership belongs under ${productTruthRoot} and describes product promises, boundaries, rationale, decisions, and success criteria`,
2006
- `engineering-lane ownership belongs under ${engineeringTruthRoot} and describes source-backed current realization, contracts, architecture, workflows, operations, or tests`,
2007
- "both-lane ownership uses separate product and engineering docs cross-linked in route YAML with realized_by and realizes, not in doc frontmatter",
2008
- "ambiguous lane ownership should be reported for manual handoff or routed to Truth Structure",
2009
- LANE_INVARIANT
2010
- ].join("\n"),
2011
- indent
1721
+ var renderTruthRootReadmeTemplate = (config = createDefaultConfig(), lane = "engineering") => {
1722
+ const templatePath = `${lane === "product" ? resolveProductTruthRoot(config) : resolveEngineeringTruthRoot(config)}/README.md`;
1723
+ const sourceOfTruth = resolveRelativePath(
1724
+ templatePath,
1725
+ config.truthmark.paths.routesIndex
2012
1726
  );
2013
- };
2014
- var DECISION_TRUTH_INSTRUCTIONS = [
2015
- "Decision truth lives in the canonical doc it governs; date active decisions inline when added or changed.",
2016
- "Do not create separate active-decision ADR/planning logs; replace the active decision and let Git history carry the audit trail.",
2017
- "Product decisions belong in product truth; engineering, architecture, contract, workflow, and operational decisions belong in engineering truth."
2018
- ].join("\n");
2019
- var LANE_INVARIANT = "Do not make product docs a summary of engineering docs. Do not make engineering docs a detailed version of product docs. Product truth says what must be true and why. Engineering truth says how the repository currently realizes it.";
2020
- var renderLaneClassificationInstructions = (config = defaultAgentConfig()) => {
2021
- const productTruthRoot = resolveProductTruthRoot(config);
2022
- const engineeringTruthRoot = resolveEngineeringTruthRoot(config);
2023
1727
  return [
2024
- "Lane review questions:",
2025
- "- before writing canonical truth docs, classify the request or change as product-lane, engineering-lane, both-lane, or ambiguous",
2026
- `- product-lane writes belong under ${productTruthRoot} and state product promises, boundaries, rationale, decisions, and success criteria`,
2027
- `- engineering-lane writes belong under ${engineeringTruthRoot} and state source-backed current realization, contracts, architecture, workflows, operations, or tests`,
2028
- "- both-lane work must write separate product and engineering docs and cross-link them in route YAML with realized_by and realizes, not in doc frontmatter",
2029
- "- ambiguous lane ownership must stop or invoke Truth Structure instead of writing a mixed document",
2030
- `- ${LANE_INVARIANT}`
1728
+ "---",
1729
+ "status: active",
1730
+ "doc_type: index",
1731
+ `last_reviewed: ${currentDate()}`,
1732
+ "---",
1733
+ "",
1734
+ "# Truth Docs",
1735
+ "",
1736
+ "This directory is an index for current truth docs organized by the configured Truthmark hierarchy.",
1737
+ "",
1738
+ renderLaneRootReadmeSummary(lane),
1739
+ "",
1740
+ renderLaneRootLeafDocGuidance(lane),
1741
+ "",
1742
+ "## Source References",
1743
+ "",
1744
+ `- ${sourceOfTruth}`,
1745
+ ""
2031
1746
  ].join("\n");
2032
1747
  };
2033
- var EVIDENCE_AUTHORITY_INSTRUCTIONS = [
2034
- "Repository instruction files and explicitly configured policy docs remain instruction authority when present; do not assume a repository uses any particular policy path.",
2035
- "Implementation code and canonical truth docs are inspected evidence for current behavior; they do not silently override workflow write boundaries."
2036
- ].join("\n");
2037
- var REPOSITORY_INTELLIGENCE_INSTRUCTIONS = [
2038
- "Repository intelligence artifacts are optional derived context: RepoIndex, RouteMap, ImpactSet, and WorkflowState/action context may guide routing, write boundaries, and verification planning when available.",
2039
- "They do not override checkout evidence, canonical truth docs, route files, or workflow write boundaries.",
2040
- "If unavailable, inspect any present Truthmark config, route files, source files, truth docs, and tests directly, then report that repository-intelligence artifacts were not generated."
2041
- ].join("\n");
2042
- var FEATURE_DOC_TEMPLATE_INSTRUCTIONS = [
2043
- "When creating or updating a truth doc, inspect the routed truth kind and use the matching template under the configured Truthmark templates root.",
2044
- "Supported kinds: product-capability, engineering-behavior, engineering-contract, engineering-architecture, engineering-workflow, engineering-operations, and engineering-test-behavior.",
2045
- "Treat the HTML comments under each template section as normative authoring guidance for that section.",
2046
- "Align existing docs to that template and write or repair section content so it satisfies the comment guidance while preserving accurate authored content.",
2047
- "If the template is missing, use lane-specific sections: product truth says what must be true and why; engineering truth says how the repository currently realizes it.",
2048
- "Teams may edit template files under the configured Truthmark templates root to define their local truth-doc standards."
2049
- ].join("\n");
2050
- var TRUTH_DOC_AUTHORING_STYLE_INSTRUCTIONS = [
2051
- "Truth-doc prose style:",
2052
- "- Use professional, plain technical prose. Prefer specific current-state claims over promotional, symbolic, or generic significance language.",
2053
- "- Avoid common AI-writing tells: pivotal, crucial, underscores, serves as, stands as, showcases, landscape, vague expert attributions, and generic upbeat conclusions.",
2054
- "- Keep claims evidence-backed and diff-friendly: one durable claim per bullet or line; paragraphs should be no longer than one or two short sentences.",
2055
- "- Do not add personality, rhetorical flourish, first-person commentary, or marketing tone.",
2056
- "- Rewrite dense or formulaic prose only when it improves readability without removing scope, evidence, decisions, or source references."
2057
- ].join("\n");
2058
- var renderTruthDocOwnershipGateSection = (subject, outcome) => {
1748
+ var renderTruthDomainReadmeTemplate = (config) => {
1749
+ const defaultArea = config.truthmark.routes.defaultArea;
1750
+ const title = titleCase(defaultArea);
1751
+ const templatePath = `${truthRoot(config)}/${defaultArea}/README.md`;
1752
+ const sourceOfTruth = resolveRelativePath(
1753
+ templatePath,
1754
+ `${config.truthmark.paths.routeAreasRoot}/${defaultArea}.md`
1755
+ );
2059
1756
  return [
2060
- "Truth-doc ownership review:",
2061
- `- before editing or relying on ${subject}, verify each target/source truth doc is a bounded owner for the behavior`,
2062
- "- if a target/source doc mixes independent owners, spans unrelated behaviors, acts as an index, or needs cross-owner edits, do not patch or in-place repair it",
2063
- `- ${outcome}`,
2064
- "- report Ownership reviewed, Structure required, Truth docs split, Truth docs restructured, or Manual handoff reason as applicable"
1757
+ "---",
1758
+ "status: active",
1759
+ "doc_type: index",
1760
+ `last_reviewed: ${currentDate()}`,
1761
+ "---",
1762
+ "",
1763
+ `# ${title} Truth Docs`,
1764
+ "",
1765
+ `This directory indexes bounded ${title.toLowerCase()} truth docs.`,
1766
+ "",
1767
+ "README.md files are indexes, not Truth Sync targets. Keep bounded truth in leaf docs in this directory.",
1768
+ "",
1769
+ "Current leaf docs:",
1770
+ "",
1771
+ "- [Bootstrap routing](bootstrap-routing.md)",
1772
+ "",
1773
+ "## Source References",
1774
+ "",
1775
+ `- ${sourceOfTruth}`,
1776
+ ""
2065
1777
  ].join("\n");
2066
1778
  };
2067
- var TRUTH_DOC_DECISION_RATIONALE_PRESERVATION_INSTRUCTIONS = [
2068
- "Decision/Rationale preservation review:",
2069
- "- before any truth-doc split, restructure, or shape repair, inventory existing Product Decisions, Engineering Decisions, and Rationale sections in every source or touched truth doc",
2070
- "- preserve each current decision and rationale in the correct product or engineering lane owner; when splitting, move it to the new owner doc rather than deleting it or leaving it in an index",
2071
- "- remove or narrow a decision or rationale only when checkout evidence shows it is stale or unsupported, and report the exact claim, evidence, and result",
2072
- "- if ownership of a decision or rationale is unclear, stop with manual-review files instead of deleting it or guessing",
2073
- "- after the edit, verify every touched truth doc keeps lane-appropriate decision/rationale sections and every pre-existing entry is preserved, moved, narrowed, removed with evidence, or blocked"
2074
- ].join("\n");
2075
- var renderTruthDocRestructureGateSection = (scope) => {
1779
+ var renderTemplateSection = (section) => {
2076
1780
  return [
2077
- "Truth-doc shape repair review:",
2078
- `- ${scope}`,
2079
- "- repair shape in place only after the ownership review confirms the doc is the right bounded owner",
2080
- "- use Truth Structure for ownership splits; do not treat broad or mixed-owner docs as in-place repair work",
2081
- "- repair shape when a narrow edit would make truth worse: missing template sections, stale evidence conflicts, cross-section updates within one owner, or wrong frontmatter/source/headings",
2082
- "- preserve supported claims; remove, narrow, or record unsupported or stale claims for manual handoff",
2083
- "- report docs restructured and why a narrow edit was not sufficient"
2084
- ].join("\n");
1781
+ section.heading,
1782
+ "",
1783
+ "<!--",
1784
+ ...section.guidance,
1785
+ "-->",
1786
+ "",
1787
+ `{{${section.placeholder}}}`,
1788
+ ""
1789
+ ];
2085
1790
  };
2086
- var ARCHITECTURE_DOC_BOUNDARY_INSTRUCTIONS = [
2087
- "Maintain architecture docs only for structure-level changes: system structure, module boundaries, runtime topology, persistence boundaries, cross-cutting contracts, or generated-surface ownership.",
2088
- "Keep ordinary behavior, endpoints, UI copy, validation rules, and bug fixes in behavior or contract docs unless they change those boundaries."
2089
- ].join("\n");
2090
- var renderRouteFirstEvidenceGateSection = (subject, noImpactedDocOutcome) => {
2091
- return [
2092
- "Evidence checklist:",
2093
- `- route-first: map ${subject} to bounded route owners and primary canonical docs`,
2094
- "- review new or changed behavior-bearing claims only in touched docs, route ownership, lane-specific decisions, and rationale",
2095
- "- support claims with primary checkout evidence: implementation, config, routing, generated templates, schemas, or contract definitions",
2096
- "- tests/examples/canonical docs corroborate; they are not sole proof when implementation conflicts",
2097
- "- remove, narrow, or record unsupported claims for manual handoff",
2098
- `- ${noImpactedDocOutcome}`
2099
- ].join("\n");
1791
+ var titleToPlaceholder = (title) => {
1792
+ return title.replace(/^#+\s+/u, "").toLowerCase().replaceAll(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
2100
1793
  };
2101
- var renderTopologyEvidenceGateSection = () => {
2102
- return [
2103
- "Evidence checklist:",
2104
- "- apply the evidence checklist before finishing when Truth Structure writes routed docs, ownership claims, lane-specific decisions, or rationale",
2105
- "- support ownership/behavior claims with topology or primary checkout evidence from layout, implementation boundaries, docs, config, route files, tests, templates, schemas, or contracts",
2106
- "- tests/examples/canonical docs corroborate; remove, narrow, or record unsupported claims for manual handoff"
2107
- ].join("\n");
1794
+ var findTemplateSectionHeadings = (template) => {
1795
+ const matches = [];
1796
+ let fencedCodeMarker = null;
1797
+ let fencedCodeLength = 0;
1798
+ for (const lineMatch of template.matchAll(/^.*(?:\r?\n|$)/gm)) {
1799
+ const rawLine = lineMatch[0];
1800
+ if (rawLine.length === 0) {
1801
+ continue;
1802
+ }
1803
+ const line = rawLine.replace(/\r?\n$/u, "");
1804
+ const fenceMatch = /^(?: {0,3})(`{3,}|~{3,})/u.exec(line);
1805
+ if (fenceMatch) {
1806
+ const marker = fenceMatch[1]?.[0];
1807
+ const length = fenceMatch[1]?.length ?? 0;
1808
+ if (fencedCodeMarker === null) {
1809
+ fencedCodeMarker = marker;
1810
+ fencedCodeLength = length;
1811
+ } else if (marker === fencedCodeMarker && length >= fencedCodeLength) {
1812
+ fencedCodeMarker = null;
1813
+ fencedCodeLength = 0;
1814
+ }
1815
+ continue;
1816
+ }
1817
+ if (fencedCodeMarker === null && /^## .+$/u.test(line)) {
1818
+ matches.push({ heading: line.trim(), index: lineMatch.index });
1819
+ }
1820
+ }
1821
+ return matches;
2108
1822
  };
2109
- var renderAuditEvidenceGateSection = () => {
2110
- return [
2111
- "Evidence checklist:",
2112
- "- support each finding and suggested fix with evidence from config, route files, canonical docs, implementation, templates, or tests",
2113
- "- canonical docs are context, not sole proof when implementation conflicts",
2114
- "- remove unsupported findings or mark open questions; validate changed claims if you edit docs"
2115
- ].join("\n");
1823
+ var parseTemplateSections = (template) => {
1824
+ const matches = findTemplateSectionHeadings(template);
1825
+ if (matches.length === 0) {
1826
+ return { preamble: template.trimEnd(), sections: [] };
1827
+ }
1828
+ const sections = matches.map((match, index) => {
1829
+ const start = match.index;
1830
+ const next = matches[index + 1];
1831
+ const end = next?.index ?? template.length;
1832
+ return {
1833
+ heading: match.heading,
1834
+ block: template.slice(start, end).trimEnd()
1835
+ };
1836
+ });
1837
+ return {
1838
+ preamble: template.slice(0, matches[0]?.index ?? 0).trimEnd(),
1839
+ sections
1840
+ };
2116
1841
  };
2117
- var renderCodexSubagentModeSection = (agents, parentRule, writeAgents = []) => {
2118
- const writeAgentLines = writeAgents.length > 0 ? [
2119
- `- dispatch write-capable project agents only with explicit write leases: ${writeAgents.join(", ")}`,
2120
- "- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
2121
- "- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
2122
- "- parent must inspect the actual checkout diff against each lease before accepting a worker report"
2123
- ] : [];
2124
- const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
2125
- const readOnlyWorkerLabel = writeAgents.length > 0 ? "read-only workers" : "workers";
2126
- return [
2127
- "Codex subagent mode:",
2128
- "- use automatically when this workflow runs in Codex and the parent agent chooses bounded subagent fan-out",
2129
- `- dispatch read-only project agents ${readOnlyScope}: ${agents.join(", ")}`,
2130
- `- ${readOnlyWorkerLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
2131
- `- parent supplies bounded evidence shards; ${readOnlyWorkerLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
2132
- ...writeAgentLines,
2133
- `- ${parentRule}`
2134
- ].join("\n");
1842
+ var LEGACY_MANAGED_TEMPLATE_HEADINGS = /* @__PURE__ */ new Map([
1843
+ ["## Current Behavior", "## Current Implementation Behavior"],
1844
+ ["## Source Evidence", "## Source References"]
1845
+ ]);
1846
+ var resolveManagedTemplateHeading = (heading) => {
1847
+ return LEGACY_MANAGED_TEMPLATE_HEADINGS.get(heading) ?? heading;
2135
1848
  };
2136
- var renderOpenCodeSubagentModeSection = (agents, parentRule, writeAgents = []) => {
2137
- const mentions = agents.map((agent) => `@${agent.replace(/_/gu, "-")}`);
2138
- const writeMentions = writeAgents.map(
2139
- (agent) => `@${agent.replace(/_/gu, "-")}`
1849
+ var stripManagedFrontmatterFields = (preamble) => {
1850
+ if (!preamble.startsWith("---\n")) {
1851
+ return preamble;
1852
+ }
1853
+ const lines = preamble.split("\n");
1854
+ const closingIndex = lines.findIndex(
1855
+ (line, index) => index > 0 && line.trim() === "---"
2140
1856
  );
2141
- const writeAgentLines = writeMentions.length > 0 ? [
2142
- `- dispatch write-capable project subagents only with explicit write leases: ${writeMentions.join(", ")}`,
2143
- "- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
2144
- "- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
2145
- "- parent must inspect the actual checkout diff against each lease before accepting a worker report"
2146
- ] : [];
2147
- const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
2148
- const readOnlyWorkerLabel = writeAgents.length > 0 ? "read-only workers" : "workers";
1857
+ if (closingIndex < 0) {
1858
+ return preamble;
1859
+ }
1860
+ const fieldsToRemove = /* @__PURE__ */ new Set(["source_of_truth", "doc_type", "truth_lane"]);
1861
+ const keptFrontmatterLines = [];
1862
+ let skippingManagedField = false;
1863
+ for (const line of lines.slice(1, closingIndex)) {
1864
+ const keyMatch = /^([A-Za-z0-9_-]+):(\s|$)/u.exec(line);
1865
+ if (keyMatch) {
1866
+ skippingManagedField = fieldsToRemove.has(keyMatch[1] ?? "");
1867
+ }
1868
+ if (!skippingManagedField) {
1869
+ keptFrontmatterLines.push(line);
1870
+ }
1871
+ }
2149
1872
  return [
2150
- "OpenCode subagent mode:",
2151
- "- use automatically when this workflow runs in OpenCode and the parent agent chooses bounded subagent fan-out",
2152
- `- dispatch read-only project subagents ${readOnlyScope}: ${mentions.join(", ")}`,
2153
- `- ${readOnlyWorkerLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
2154
- `- parent supplies bounded evidence shards; ${readOnlyWorkerLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
2155
- ...writeAgentLines,
2156
- `- ${parentRule}`
2157
- ].join("\n");
1873
+ "---",
1874
+ ...keptFrontmatterLines,
1875
+ "---",
1876
+ ...lines.slice(closingIndex + 1)
1877
+ ].join("\n").trimEnd();
2158
1878
  };
2159
- var renderClaudeSubagentModeSection = (agents, parentRule, writeAgents = []) => {
2160
- const mentions = agents.map(
2161
- (agent) => `${agent.replace(/_/gu, "-")} subagent`
2162
- );
2163
- const writeMentions = writeAgents.map(
2164
- (agent) => `${agent.replace(/_/gu, "-")} subagent`
1879
+ var mergeTruthDocTemplate = (existingTemplate, defaultTemplate) => {
1880
+ if (existingTemplate.trim().length === 0) {
1881
+ return defaultTemplate;
1882
+ }
1883
+ const defaultParsed = parseTemplateSections(defaultTemplate);
1884
+ const existingParsed = parseTemplateSections(existingTemplate);
1885
+ const defaultHeadings = new Set(
1886
+ defaultParsed.sections.map((section) => section.heading)
2165
1887
  );
2166
- const writeAgentLines = writeMentions.length > 0 ? [
2167
- `- dispatch write-capable project subagents only with explicit write leases: ${writeMentions.join(", ")}`,
2168
- "- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
2169
- "- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
2170
- "- parent must inspect the actual checkout diff against each lease before accepting a worker report"
2171
- ] : [];
2172
- const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
2173
- const readOnlySubagentLabel = writeAgents.length > 0 ? "read-only subagents" : "subagents";
1888
+ const customBeforeDefault = /* @__PURE__ */ new Map();
1889
+ const trailingCustomSections = [];
1890
+ existingParsed.sections.forEach((section, index) => {
1891
+ if (defaultHeadings.has(resolveManagedTemplateHeading(section.heading))) {
1892
+ return;
1893
+ }
1894
+ const nextDefaultSection = existingParsed.sections.slice(index + 1).find(
1895
+ (candidate) => defaultHeadings.has(resolveManagedTemplateHeading(candidate.heading))
1896
+ );
1897
+ if (nextDefaultSection) {
1898
+ const nextDefaultHeading = resolveManagedTemplateHeading(
1899
+ nextDefaultSection.heading
1900
+ );
1901
+ const bucket = customBeforeDefault.get(nextDefaultHeading) ?? [];
1902
+ bucket.push(section);
1903
+ customBeforeDefault.set(nextDefaultHeading, bucket);
1904
+ return;
1905
+ }
1906
+ trailingCustomSections.push(section);
1907
+ });
1908
+ const mergedSections = defaultParsed.sections.flatMap((section) => [
1909
+ ...customBeforeDefault.get(section.heading) ?? [],
1910
+ section
1911
+ ]);
2174
1912
  return [
2175
- "Claude Code subagent mode:",
2176
- "- use automatically when this workflow runs in Claude Code and the parent agent chooses bounded subagent fan-out",
2177
- `- dispatch read-only project subagents ${readOnlyScope}: ${mentions.join(", ")}`,
2178
- `- ${readOnlySubagentLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
2179
- `- parent supplies bounded evidence shards; ${readOnlySubagentLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
2180
- ...writeAgentLines,
2181
- `- ${parentRule}`
2182
- ].join("\n");
1913
+ stripManagedFrontmatterFields(existingParsed.preamble),
1914
+ ...mergedSections.map((section) => section.block),
1915
+ ...trailingCustomSections.map((section) => section.block),
1916
+ ""
1917
+ ].filter((block) => block.length > 0).join("\n\n");
2183
1918
  };
2184
- var renderCopilotCustomAgentModeSection = (agents, parentRule, writeAgents = []) => {
2185
- const mentions = agents.map((agent) => `@${agent.replace(/_/gu, "-")}`);
2186
- const writeMentions = writeAgents.map(
2187
- (agent) => `@${agent.replace(/_/gu, "-")}`
2188
- );
2189
- const writeAgentLines = writeMentions.length > 0 ? [
2190
- `- dispatch write-capable project custom agents only with explicit write leases: ${writeMentions.join(", ")}`,
2191
- "- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
2192
- "- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
2193
- "- parent must inspect the actual checkout diff against each lease before accepting a worker report"
2194
- ] : [];
2195
- const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
2196
- const readOnlyCustomAgentLabel = writeAgents.length > 0 ? "read-only custom agents" : "custom agents";
1919
+ var renderBehaviorDocTemplateFile = () => {
2197
1920
  return [
2198
- "Copilot custom-agent mode:",
2199
- "- use automatically when this workflow runs in Copilot and the parent agent chooses bounded custom-agent fan-out",
2200
- `- dispatch read-only project custom agents ${readOnlyScope}: ${mentions.join(", ")}`,
2201
- `- ${readOnlyCustomAgentLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
2202
- `- parent supplies bounded evidence shards; ${readOnlyCustomAgentLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
2203
- ...writeAgentLines,
2204
- `- ${parentRule}`
1921
+ "---",
1922
+ "status: active",
1923
+ "truth_kind: engineering-behavior",
1924
+ `last_reviewed: ${currentDate()}`,
1925
+ "---",
1926
+ "",
1927
+ "# {{title}}",
1928
+ "",
1929
+ "## Purpose",
1930
+ "",
1931
+ "<!--",
1932
+ "State the user/system outcome this behavior protects and why it exists.",
1933
+ "Include the problem boundary and durable value; exclude roadmap, implementation plan, and historical narrative.",
1934
+ "List the code, config, docs, or tests that support the claim in Source References rather than prose-only assertion.",
1935
+ "-->",
1936
+ "",
1937
+ "{{purpose}}",
1938
+ "",
1939
+ "## Scope",
1940
+ "",
1941
+ "<!--",
1942
+ "Define the one coherent behavior surface this document owns.",
1943
+ "Include in-scope actors, entrypoints, state/data owned by this doc, and explicit handoffs to neighboring truth docs.",
1944
+ "Split into another leaf doc when content introduces a distinct outcome, state machine, rule family, external contract, or route owner.",
1945
+ "Keep README.md files as indexes only.",
1946
+ "-->",
1947
+ "",
1948
+ "{{scope}}",
1949
+ "",
1950
+ "This doc was created from the editable engineering-behavior template at {{template_path}}.",
1951
+ "",
1952
+ "## Current Implementation Behavior",
1953
+ "",
1954
+ "<!--",
1955
+ "Describe only current implemented behavior in present tense.",
1956
+ "Cover observable behavior, important defaults, and user/system-visible effects; exclude desired future behavior and speculative design.",
1957
+ "Every non-obvious claim should be checkable from Source References.",
1958
+ "-->",
1959
+ "",
1960
+ "{{current_implementation_behavior}}",
1961
+ "",
1962
+ "## Core Rules",
1963
+ "",
1964
+ "<!--",
1965
+ "Capture stable business rules, invariants, precedence rules, validation rules, and must-never constraints.",
1966
+ "Separate rules from incidental implementation details; cite current implementation or tests for rule enforcement.",
1967
+ "-->",
1968
+ "",
1969
+ "{{core_rules}}",
1970
+ "",
1971
+ "## Behavior Scenarios",
1972
+ "",
1973
+ "<!--",
1974
+ "Use compact scenario blocks only where they clarify normal, fallback, or compatibility-critical behavior.",
1975
+ "Write scenarios as current truth, not desired requirements: `#### Scenario: <implemented case>` followed by `- **GIVEN** ...`, `- **WHEN** ...`, `- **THEN** ...`, and optional `- **AND** ...` bullets.",
1976
+ "Keep each bullet evidence-backed and observable; do not force a scenario for every rule.",
1977
+ "-->",
1978
+ "",
1979
+ "{{behavior_scenarios}}",
1980
+ "",
1981
+ "## Flows And States",
1982
+ "",
1983
+ "<!--",
1984
+ "Document state transitions, lifecycle stages, retries, fallbacks, route switches, and important error paths.",
1985
+ "State 'None beyond current behavior.' when this behavior has no distinct flow or state model.",
1986
+ "-->",
1987
+ "",
1988
+ "{{flows_and_states}}",
1989
+ "",
1990
+ "## Contracts",
1991
+ "",
1992
+ "<!--",
1993
+ "Capture user-visible or integration contracts: CLI/API shape, inputs, outputs, diagnostics, files, events, permissions, or links to canonical contract docs.",
1994
+ "Avoid duplicating a separate canonical contract doc; link to it when contract ownership lives elsewhere.",
1995
+ "-->",
1996
+ "",
1997
+ "{{contracts}}",
1998
+ "",
1999
+ "## Product Truth Links",
2000
+ "",
2001
+ "<!--",
2002
+ "List product truth docs this engineering doc realizes; author canonical realizes links in route YAML, not doc frontmatter.",
2003
+ "Use 'None.' when this is purely internal engineering behavior.",
2004
+ "-->",
2005
+ "",
2006
+ "{{product_truth_links}}",
2007
+ "",
2008
+ "## Engineering Decisions",
2009
+ "",
2010
+ "<!--",
2011
+ "Keep active decisions only, dated inline when added or changed.",
2012
+ "Explain decisions that shape behavior, boundaries, rejected alternatives, or migration constraints; replace stale decisions instead of appending historical logs.",
2013
+ "-->",
2014
+ "",
2015
+ "{{engineering_decisions}}",
2016
+ "",
2017
+ "## Rationale",
2018
+ "",
2019
+ "<!--",
2020
+ "Explain why the current behavior and active decisions are this way, including tradeoffs and constraints.",
2021
+ "Tie rationale to evidence-backed behavior; do not use this as a changelog.",
2022
+ "-->",
2023
+ "",
2024
+ "{{rationale}}",
2025
+ "",
2026
+ "## Non-Goals",
2027
+ "",
2028
+ "<!--",
2029
+ "Name adjacent behavior this doc intentionally does not own, especially tempting future expansions or neighboring route owners.",
2030
+ "Use this section to prevent scope creep and duplicate truth ownership.",
2031
+ "-->",
2032
+ "",
2033
+ "{{non_goals}}",
2034
+ "",
2035
+ "## Maintenance Notes",
2036
+ "",
2037
+ "<!--",
2038
+ "List related tests, routing cautions, migration notes, evidence drift risks, and review triggers for future maintainers or agents.",
2039
+ "Keep this operational and current-state focused, not historical.",
2040
+ "-->",
2041
+ "",
2042
+ "{{maintenance_notes}}",
2043
+ "",
2044
+ "## Source References",
2045
+ "",
2046
+ "<!--",
2047
+ "List source files, tests, configs, generated templates, route files, or product instructions that support current claims.",
2048
+ "-->",
2049
+ "",
2050
+ "{{source_references}}",
2051
+ ""
2205
2052
  ].join("\n");
2206
2053
  };
2207
- var defaultAgentConfig = () => {
2208
- return createDefaultConfig();
2209
- };
2210
- var renderHierarchySummary = (config) => {
2211
- const productRoot = resolveProductTruthRoot(config);
2212
- const engineeringRoot = resolveEngineeringTruthRoot(config);
2054
+ var sectionSpec = (heading, guidance, placeholder = titleToPlaceholder(heading)) => ({ heading, guidance, placeholder });
2055
+ var PURPOSE_SECTION = sectionSpec("## Purpose", [
2056
+ "State the software-engineering outcome this document protects and why the documented surface exists.",
2057
+ "Include durable value, impacted users/systems, and the problem boundary; exclude roadmap, implementation plans, and historical narrative.",
2058
+ "Keep claims traceable to Source References rather than prose-only assertion."
2059
+ ]);
2060
+ var SCOPE_SECTION = sectionSpec("## Scope", [
2061
+ "Define the one coherent surface this document owns, including actors, entrypoints, owned state/data, and handoffs to neighboring truth docs.",
2062
+ "Call out important out-of-scope boundaries here or in Non-Goals; split the doc when it mixes distinct outcomes, lifecycles, contracts, or owners."
2063
+ ]);
2064
+ var PRODUCT_DECISIONS_SECTION = sectionSpec(
2065
+ "## Product Decisions",
2066
+ [
2067
+ "Keep active decisions only, dated inline when added or changed.",
2068
+ "Capture decisions that shape behavior, interfaces, boundaries, compatibility, risk acceptance, or migration constraints.",
2069
+ "Replace stale decisions instead of appending historical logs."
2070
+ ],
2071
+ "decision"
2072
+ );
2073
+ var ENGINEERING_DECISIONS_SECTION = sectionSpec(
2074
+ "## Engineering Decisions",
2075
+ [
2076
+ "Keep active engineering, architecture, contract, workflow, or operational decisions only, dated inline when added or changed.",
2077
+ "Do not restate product promises, product rationale, or business decisions here; link product truth instead.",
2078
+ "Replace stale decisions instead of appending historical logs."
2079
+ ],
2080
+ "engineering_decisions"
2081
+ );
2082
+ var RATIONALE_SECTION = sectionSpec("## Rationale", [
2083
+ "Explain why the current behavior, structure, or contract is this way, including tradeoffs and constraints.",
2084
+ "Tie rationale to evidence-backed facts and active decisions; do not use this as a changelog."
2085
+ ]);
2086
+ var NON_GOALS_SECTION = sectionSpec("## Non-Goals", [
2087
+ "Name adjacent behavior, responsibilities, interfaces, or future expansions this doc intentionally does not own.",
2088
+ "Use this section to prevent scope creep and duplicate truth ownership."
2089
+ ]);
2090
+ var MAINTENANCE_NOTES_SECTION = sectionSpec("## Maintenance Notes", [
2091
+ "List related tests, routing cautions, migration notes, compatibility risks, evidence drift risks, and review triggers for future maintainers or agents.",
2092
+ "Keep this operational and current-state focused, not historical."
2093
+ ]);
2094
+ var SOURCE_REFERENCES_SECTION = sectionSpec(
2095
+ "## Source References",
2096
+ [
2097
+ "List source files, tests, configs, generated templates, route files, or product instructions that support current claims."
2098
+ ],
2099
+ "source_references"
2100
+ );
2101
+ var renderTypedTruthDocTemplate = (truthKind, title, sections) => {
2213
2102
  return [
2214
- "Truthmark hierarchy hints:",
2215
- "- Config, when present: .truthmark/config.yml",
2216
- `- Root route index, when present: ${config.truthmark.paths.routesIndex}`,
2217
- `- Area route files, when present: ${config.truthmark.paths.routeAreasRoot}/**/*.md`,
2218
- `- Product truth docs, when present: ${productRoot}/**/*.md`,
2219
- `- Engineering truth docs, when present: ${engineeringRoot}/**/*.md`
2103
+ "---",
2104
+ "status: active",
2105
+ `truth_kind: ${truthKind}`,
2106
+ `last_reviewed: ${currentDate()}`,
2107
+ "---",
2108
+ "",
2109
+ `# ${title}`,
2110
+ "",
2111
+ ...renderTemplateSection(PURPOSE_SECTION),
2112
+ ...renderTemplateSection(SCOPE_SECTION),
2113
+ ...sections.flatMap(renderTemplateSection),
2114
+ ...renderTemplateSection(ENGINEERING_DECISIONS_SECTION),
2115
+ ...renderTemplateSection(RATIONALE_SECTION),
2116
+ ...renderTemplateSection(NON_GOALS_SECTION),
2117
+ ...renderTemplateSection(MAINTENANCE_NOTES_SECTION),
2118
+ ...renderTemplateSection(SOURCE_REFERENCES_SECTION)
2220
2119
  ].join("\n");
2221
2120
  };
2222
-
2223
- // src/templates/agents-block.ts
2224
- var TRUTHMARK_BLOCK_START = "<!-- truthmark:start -->";
2225
- var TRUTHMARK_BLOCK_END = "<!-- truthmark:end -->";
2226
- var renderCompactHierarchySummary = (config) => {
2227
- const productTruthRoot = resolveProductTruthRoot(config);
2228
- const engineeringTruthRoot = resolveEngineeringTruthRoot(config);
2229
- const truthDocRoots = Array.from(
2230
- /* @__PURE__ */ new Set([productTruthRoot, engineeringTruthRoot])
2231
- ).map((truthRoot3) => `${truthRoot3}/**/*.md`);
2232
- return `Hierarchy hints: config .truthmark/config.yml when present; routes ${config.truthmark.paths.routesIndex} and ${config.truthmark.paths.routeAreasRoot}/**/*.md when present; Truth docs: ${truthDocRoots.join(" and ")} when present.`;
2233
- };
2234
- var renderAgentsBlock = (config = defaultAgentConfig()) => {
2235
- const portalLine = config.truthmark.generated.portal.enabled ? `Truthmark Portal is a separate manual-only presentation workflow. Run it only when explicitly requested; it writes generated non-canonical static files under ${config.truthmark.paths.portalOutput}/. Markdown remains canonical.` : null;
2121
+ var CORE_LANE_INVARIANT = "Do not make product docs a summary of engineering docs. Do not make engineering docs a detailed version of product docs. Product truth says what must be true and why. Engineering truth says how the repository currently realizes it.";
2122
+ var renderProductTruthDocTemplate = (truthKind, title, sections, includeNonGoals) => {
2236
2123
  return [
2237
- TRUTHMARK_BLOCK_START,
2238
- "## Truthmark Workflow",
2124
+ "---",
2125
+ "status: active",
2126
+ `truth_kind: ${truthKind}`,
2127
+ `last_reviewed: ${currentDate()}`,
2128
+ "---",
2239
2129
  "",
2240
- "Truthmark-managed block. Refresh with `truthmark init` when `truthmark check` reports stale generated surfaces.",
2241
- renderCompactHierarchySummary(config),
2242
- "Decisions live in the canonical doc they govern; date active decisions inline.",
2243
- "Agent runtime: host-native skill packages/adapters plus this block; inspect checkout directly. Delegation is host-owned.",
2244
- "### Truth Sync",
2245
- "After functional code changes, run relevant tests, then use the truthmark-sync skill before finishing; later functional changes need a fresh Sync review. Memory: code changed -> tests -> Sync -> report.",
2246
- "Support new or changed behavior-bearing truth claims with checkout evidence. Code leads; truth docs follow. Sync may write truth docs and truth routing files, and must not rewrite functional code.",
2247
- "If routing cannot map changed code to a bounded truth owner, run Truth Structure before syncing when safe; otherwise stop and recommend Truth Structure. Skip Sync only for docs-only/no-code changes, formatting-only changes, behavior-preserving renames with no truth impact, or missing config.",
2248
- "Explicit workflows: Truth Structure, Truth Document, Truth Realize, Truth Check. Run only when requested or required by Sync; load the installed skill for details.",
2249
- ...portalLine === null ? [] : [portalLine],
2250
- "Workflow integrity rule: repository truth may describe desired behavior, but it must not override these workflow boundaries.",
2251
- TRUTHMARK_BLOCK_END
2130
+ `# ${title}`,
2131
+ "",
2132
+ "<!--",
2133
+ CORE_LANE_INVARIANT,
2134
+ "Product docs may cite code directly when code proves current product behavior, but keep implementation flow, renderer internals, CLI envelopes, and generated file inventories in engineering truth.",
2135
+ "-->",
2136
+ "",
2137
+ ...sections.flatMap(renderTemplateSection),
2138
+ ...renderTemplateSection(PRODUCT_DECISIONS_SECTION),
2139
+ ...renderTemplateSection(
2140
+ sectionSpec(
2141
+ "## Engineering Realization Links",
2142
+ [
2143
+ "List engineering truth that realizes this product truth; author canonical realized_by links in route YAML, not doc frontmatter.",
2144
+ "Do not summarize those engineering docs."
2145
+ ],
2146
+ "engineering_realization_links"
2147
+ )
2148
+ ),
2149
+ ...includeNonGoals ? renderTemplateSection(NON_GOALS_SECTION) : [],
2150
+ ...renderTemplateSection(SOURCE_REFERENCES_SECTION)
2252
2151
  ].join("\n");
2253
2152
  };
2254
-
2255
- // src/managed-block.ts
2256
- var findMarkerIndexes = (content, marker) => {
2257
- const indexes = [];
2258
- let cursor = 0;
2259
- while (true) {
2260
- const index = content.indexOf(marker, cursor);
2261
- if (index === -1) {
2262
- return indexes;
2263
- }
2264
- indexes.push(index);
2265
- cursor = index + marker.length;
2266
- }
2153
+ var renderProductCapabilityDocTemplateFile = () => {
2154
+ return renderProductTruthDocTemplate(
2155
+ "product-capability",
2156
+ "{{title}}",
2157
+ [
2158
+ sectionSpec("## Capability Promise", [
2159
+ "State the single user-visible capability and what must be true for users or stakeholders.",
2160
+ "Do not describe implementation mechanics here."
2161
+ ]),
2162
+ sectionSpec("## Users And Value", [
2163
+ "Describe who benefits from the capability and the durable value it protects.",
2164
+ "Tie claims to repository evidence, explicit user instruction, or current behavior."
2165
+ ]),
2166
+ sectionSpec("## Capability Scope", [
2167
+ "Define what this capability includes and excludes, including product boundary constraints and adjacent systems.",
2168
+ "Capture important scope limits, ownership boundaries, and non-goal pointers here; keep technical contracts in engineering truth."
2169
+ ]),
2170
+ sectionSpec("## Current Product Behavior", [
2171
+ "Describe current implemented user-visible behavior in present tense.",
2172
+ "Code files may appear in Source References when they directly prove current behavior."
2173
+ ]),
2174
+ sectionSpec("## Acceptance Criteria", [
2175
+ "List observable criteria that show the capability promise is currently satisfied.",
2176
+ "Include criteria that review whether the capability stays within its stated scope and boundary.",
2177
+ "Use criteria that can be reviewed from repository evidence or explicit product instruction."
2178
+ ])
2179
+ ],
2180
+ true
2181
+ );
2267
2182
  };
2268
- var parseManagedBlock = (content) => {
2269
- const starts = findMarkerIndexes(content, TRUTHMARK_BLOCK_START);
2270
- const ends = findMarkerIndexes(content, TRUTHMARK_BLOCK_END);
2271
- if (starts.length === 0 && ends.length === 0) {
2272
- return { status: "absent" };
2273
- }
2274
- if (starts.length !== 1 || ends.length !== 1) {
2275
- return { status: "malformed" };
2276
- }
2277
- const start = starts[0];
2278
- const endStart = ends[0];
2279
- if (start === -1 || endStart === -1 || endStart < start) {
2280
- return { status: "malformed" };
2281
- }
2282
- return {
2283
- status: "valid",
2284
- start,
2285
- end: endStart + TRUTHMARK_BLOCK_END.length
2286
- };
2183
+ var renderContractDocTemplateFile = () => {
2184
+ return renderTypedTruthDocTemplate("engineering-contract", "{{title}}", [
2185
+ sectionSpec("## Contract Surface", [
2186
+ "Identify the owned API, CLI, file format, event, protocol, permission boundary, or integration surface.",
2187
+ "State consumers/producers, stability level, and the source files/tests that define the contract."
2188
+ ]),
2189
+ sectionSpec("## Inputs", [
2190
+ "Document accepted parameters, payloads, files, environment/config keys, permissions, and validation rules.",
2191
+ "Include required/optional status, defaults, constraints, and normalization behavior."
2192
+ ]),
2193
+ sectionSpec("## Outputs", [
2194
+ "Document returned values, emitted files/events, state changes, side effects, and success diagnostics.",
2195
+ "Make externally observable behavior explicit enough for compatibility review."
2196
+ ]),
2197
+ sectionSpec("## Errors And Diagnostics", [
2198
+ "List error classes, exit/status codes, user-facing diagnostics, retries, and recoverability expectations.",
2199
+ "Distinguish validation errors, dependency failures, authorization failures, and internal faults when applicable."
2200
+ ]),
2201
+ sectionSpec("## Compatibility Rules", [
2202
+ "State backward/forward compatibility guarantees, tolerated inputs, deprecation rules, and breaking-change triggers.",
2203
+ "Include compatibility tests or review questions that protect the contract."
2204
+ ]),
2205
+ sectionSpec("## Versioning And Migration", [
2206
+ "Document version negotiation, schema/API version fields, rollout requirements, migration steps, and rollback expectations.",
2207
+ "State 'Not versioned' only when the implementation truly has no versioning or migration surface."
2208
+ ])
2209
+ ]);
2210
+ };
2211
+ var renderArchitectureDocTemplateFile = () => {
2212
+ return renderTypedTruthDocTemplate("engineering-architecture", "{{title}}", [
2213
+ sectionSpec("## System Role", [
2214
+ "Describe the current architectural role of this subsystem/component in the larger system.",
2215
+ "State the primary responsibilities, consumers, providers, and why this boundary exists now."
2216
+ ]),
2217
+ sectionSpec("## Boundaries", [
2218
+ "Define owned code/config/data, external dependencies, trust boundaries, and interfaces crossed by this architecture.",
2219
+ "Name what is deliberately outside the boundary and link neighboring architecture or contract docs when they own it."
2220
+ ]),
2221
+ sectionSpec("## Components", [
2222
+ "List the major runtime/build-time components, modules, services, jobs, or generated artifacts and their responsibilities.",
2223
+ "Keep the component list current and evidence-backed; avoid speculative target architecture."
2224
+ ]),
2225
+ sectionSpec("## Data And Control Flow", [
2226
+ "Describe important data movement, command/control paths, synchronization points, state ownership, and failure paths.",
2227
+ "Call out persistence, queues, caches, external calls, and security-sensitive transitions where relevant."
2228
+ ]),
2229
+ sectionSpec("## Ownership", [
2230
+ "Document team/module ownership, review responsibility, operational responsibility, and escalation paths if known.",
2231
+ "If ownership is inferred from codeowners, config, or repository structure, cite that evidence."
2232
+ ]),
2233
+ sectionSpec("## Cross-Cutting Constraints", [
2234
+ "Record active constraints such as security, privacy, reliability, performance, portability, maintainability, compliance, and cost.",
2235
+ "Tie constraints to source evidence, tests, standards, or operational requirements where available."
2236
+ ])
2237
+ ]);
2238
+ };
2239
+ var renderWorkflowDocTemplateFile = () => {
2240
+ return renderTypedTruthDocTemplate("engineering-workflow", "{{title}}", [
2241
+ sectionSpec("## Triggers", [
2242
+ "List events, commands, schedules, user actions, webhooks, or dependency signals that start this workflow.",
2243
+ "Include preconditions, authorization requirements, debounce/coalescing behavior, and disabled states when applicable."
2244
+ ]),
2245
+ sectionSpec("## Inputs", [
2246
+ "Document data, files, config, context, credentials, and environmental assumptions consumed by the workflow.",
2247
+ "Include validation, defaults, and normalization that happen before execution."
2248
+ ]),
2249
+ sectionSpec("## Execution Model", [
2250
+ "Describe synchronous/asynchronous execution, concurrency, locking, leases, batching, ordering, and idempotency behavior.",
2251
+ "State whether the workflow waits for user action, runs in the background, is distributed, or is delegated to another system."
2252
+ ]),
2253
+ sectionSpec("## Steps", [
2254
+ "Capture the current ordered steps or phases at a level useful for maintenance and review.",
2255
+ "Reference implementation entrypoints instead of duplicating line-by-line code behavior."
2256
+ ]),
2257
+ sectionSpec("## State, Retry, And Failure Behavior", [
2258
+ "Document state transitions, retries, timeouts, compensation, fallback, partial-success, and terminal-failure behavior.",
2259
+ "Make externally visible failure semantics and recovery responsibilities clear."
2260
+ ]),
2261
+ sectionSpec("## Outputs", [
2262
+ "List artifacts, state changes, notifications, logs, metrics, diagnostics, and downstream triggers produced by the workflow.",
2263
+ "Include success criteria and handoff points to other truth docs or systems."
2264
+ ])
2265
+ ]);
2287
2266
  };
2288
- var extractManagedBlock = (content) => {
2289
- const block = parseManagedBlock(content);
2290
- if (block.status !== "valid") {
2291
- return null;
2292
- }
2293
- return content.slice(block.start, block.end);
2267
+ var renderOperationsDocTemplateFile = () => {
2268
+ return renderTypedTruthDocTemplate("engineering-operations", "{{title}}", [
2269
+ sectionSpec("## Operational Surface", [
2270
+ "Describe what operators, maintainers, or automated systems can observe or control for this surface.",
2271
+ "Include commands, dashboards, alerts, runbooks, jobs, or operational APIs that define current operations."
2272
+ ]),
2273
+ sectionSpec("## Runtime Topology", [
2274
+ "Document services, processes, containers, hosts, regions, dependencies, queues, stores, and network boundaries involved at runtime.",
2275
+ "State single-node/local behavior explicitly when there is no distributed topology."
2276
+ ]),
2277
+ sectionSpec("## Configuration", [
2278
+ "List operational config, environment variables, feature flags, secrets references, defaults, and reload/restart requirements.",
2279
+ "Do not include secret values; describe storage and rotation expectations instead."
2280
+ ]),
2281
+ sectionSpec("## Permissions", [
2282
+ "Document required identities, roles, scopes, filesystem/network permissions, and least-privilege boundaries.",
2283
+ "Include user-facing authorization behavior and operator access requirements when relevant."
2284
+ ]),
2285
+ sectionSpec("## Deployment And Rollback", [
2286
+ "Describe deployment mechanism, migration ordering, compatibility windows, rollback path, and known irreversible operations.",
2287
+ "Call out manual review points, smoke checks, and post-deploy verification responsibilities."
2288
+ ]),
2289
+ sectionSpec("## Availability And Observability", [
2290
+ "Capture availability expectations, health checks, metrics, logs, traces, alerts, SLO/error-budget signals, and known blind spots.",
2291
+ "Include what maintainers should inspect first during incidents or degraded behavior."
2292
+ ])
2293
+ ]);
2294
2294
  };
2295
- var trimmedBoundary = (value) => value.replace(/\n+$/u, "");
2296
- var upsertManagedBlock = (existingContent, block) => {
2297
- if (existingContent === null || existingContent.trim().length === 0) {
2298
- return block;
2299
- }
2300
- const marker = parseManagedBlock(existingContent);
2301
- if (marker.status !== "valid") {
2302
- return `${trimmedBoundary(existingContent)}
2303
-
2304
- ${block}`;
2305
- }
2306
- const before = trimmedBoundary(existingContent.slice(0, marker.start));
2307
- const after = existingContent.slice(marker.end).replace(/^\n+/u, "");
2308
- if (before.length === 0 && after.length === 0) {
2309
- return block;
2310
- }
2311
- if (before.length === 0) {
2312
- return `${block}
2313
-
2314
- ${after}`;
2315
- }
2316
- if (after.length === 0) {
2317
- return `${before}
2318
-
2319
- ${block}`;
2320
- }
2321
- return `${before}
2322
-
2323
- ${block}
2324
-
2325
- ${after}`;
2295
+ var renderTestBehaviorDocTemplateFile = () => {
2296
+ return renderTypedTruthDocTemplate("engineering-test-behavior", "{{title}}", [
2297
+ sectionSpec("## Test Surface", [
2298
+ "Define the behavior, contract, architecture, or workflow surface these tests verify.",
2299
+ "Link the canonical truth docs and code paths the tests are meant to protect."
2300
+ ]),
2301
+ sectionSpec("## Fixtures And Data Model", [
2302
+ "Document fixtures, factories, seeds, mocks/fakes, test repositories, external-service substitutes, and data lifecycle rules.",
2303
+ "Include cleanup, determinism, privacy, and cross-test contamination constraints."
2304
+ ]),
2305
+ sectionSpec("## Execution Model", [
2306
+ "Describe how tests run: command, framework, parallelism, isolation, network/filesystem assumptions, and required services.",
2307
+ "State whether tests are unit, integration, e2e, contract, smoke, regression, or generated checks."
2308
+ ]),
2309
+ sectionSpec("## Assertions And Invariants", [
2310
+ "List the critical assertions, invariants, failure modes, and negative cases that make the tests meaningful.",
2311
+ "Tie assertions to product/contract rules rather than incidental implementation details."
2312
+ ]),
2313
+ sectionSpec("## Isolation Rules", [
2314
+ "Document transaction boundaries, temp directories, fake clocks, network blocking, shared resources, and teardown rules.",
2315
+ "Call out known order dependencies or flake risks and how they are controlled."
2316
+ ]),
2317
+ sectionSpec("## Reporting And Failure Semantics", [
2318
+ "Describe diagnostics, snapshots, logs, coverage signals, retry policy, and how maintainers should interpret failures.",
2319
+ "Include escalation or quarantine criteria for flaky or environment-sensitive tests."
2320
+ ])
2321
+ ]);
2326
2322
  };
2327
2323
 
2328
2324
  // src/init/hierarchy.ts
2329
- import fs5 from "fs/promises";
2330
2325
  var truthRoot2 = resolveTruthDocsRoot;
2331
2326
  var BEHAVIOR_DOC_TEMPLATE_FILE_NAME = "engineering-behavior.md";
2332
2327
  var CONTRACT_DOC_TEMPLATE_FILE_NAME = "engineering-contract.md";
@@ -2336,7 +2331,7 @@ var OPERATIONS_DOC_TEMPLATE_FILE_NAME = "engineering-operations.md";
2336
2331
  var TEST_BEHAVIOR_DOC_TEMPLATE_FILE_NAME = "engineering-test-behavior.md";
2337
2332
  var PRODUCT_CAPABILITY_DOC_TEMPLATE_FILE_NAME = "product-capability.md";
2338
2333
  var rootIndexReferencesChildRoute = async (rootDir, rootIndexPath, childRoutePath) => {
2339
- const rootIndexSource = await fs5.readFile(
2334
+ const rootIndexSource = await fs4.readFile(
2340
2335
  resolveRepoPath(rootDir, rootIndexPath),
2341
2336
  "utf8"
2342
2337
  );
@@ -2357,7 +2352,7 @@ var ensureOrUpdateTruthDocTemplate = async (rootDir, templatePath, defaultTempla
2357
2352
  if (seededResult.status !== "unchanged") {
2358
2353
  return seededResult;
2359
2354
  }
2360
- const existingTemplate = await fs5.readFile(
2355
+ const existingTemplate = await fs4.readFile(
2361
2356
  resolveRepoPath(rootDir, templatePath),
2362
2357
  "utf8"
2363
2358
  );
@@ -4489,6 +4484,7 @@ var renderTruthmarkCopilotPortalPrompt = (config = defaultAgentConfig()) => {
4489
4484
  // src/templates/generated-surfaces.ts
4490
4485
  var RETIRED_GENERATED_SURFACES = {
4491
4486
  exactPaths: [
4487
+ "CLAUDE.md",
4492
4488
  "GEMINI.md",
4493
4489
  ".github/prompts/truthmark-preview.prompt.md",
4494
4490
  ".cursor/rules/truthmark-structure.mdc",
@@ -4659,7 +4655,10 @@ var opencodeFiles = (config, block) => {
4659
4655
  };
4660
4656
  var claudeFiles = (config, block) => {
4661
4657
  const files = [
4662
- ...instructionBlockFiles(["CLAUDE.md"], block),
4658
+ {
4659
+ path: ".claude/rules/truthmark.md",
4660
+ content: block
4661
+ },
4663
4662
  ...renderTruthmarkSkillPackage({
4664
4663
  skillPath: ".claude/skills/truthmark-structure/SKILL.md",
4665
4664
  workflowId: "truthmark-structure",
@@ -4923,16 +4922,18 @@ var renderGeneratedSurfaceCatalog = (config) => {
4923
4922
  generated: { portal: { enabled: false } }
4924
4923
  }
4925
4924
  };
4926
- const basePaths = new Set(
4927
- renderGeneratedSurfaces(baseConfig).map(({ path: path13 }) => path13)
4928
- );
4929
- for (const surface of renderGeneratedSurfaces({
4930
- ...baseConfig,
4931
- truthmark: {
4932
- ...baseConfig.truthmark,
4933
- generated: { portal: { enabled: true } }
4934
- }
4935
- })) {
4925
+ const baseSurfaces = renderGeneratedSurfaces(baseConfig);
4926
+ const basePaths = new Set(baseSurfaces.map(({ path: path13 }) => path13));
4927
+ for (const surface of [
4928
+ ...baseSurfaces,
4929
+ ...renderGeneratedSurfaces({
4930
+ ...baseConfig,
4931
+ truthmark: {
4932
+ ...baseConfig.truthmark,
4933
+ generated: { portal: { enabled: true } }
4934
+ }
4935
+ })
4936
+ ]) {
4936
4937
  const owner = {
4937
4938
  kind: basePaths.has(surface.path) ? "platform" : "portal",
4938
4939
  platform
@@ -4941,7 +4942,10 @@ var renderGeneratedSurfaceCatalog = (config) => {
4941
4942
  const recognizedContent = surface.content.endsWith("\n") ? surface.content : `${surface.content}
4942
4943
  `;
4943
4944
  if (existing) {
4944
- existing.owners.push(owner);
4945
+ if (!existing.owners.some(
4946
+ (existingOwner) => existingOwner.kind === owner.kind && "platform" in existingOwner && existingOwner.platform === owner.platform
4947
+ ))
4948
+ existing.owners.push(owner);
4945
4949
  if (!existing.recognizedContents.includes(recognizedContent)) {
4946
4950
  existing.recognizedContents.push(recognizedContent);
4947
4951
  }
@@ -4959,6 +4963,7 @@ var renderGeneratedSurfaceCatalog = (config) => {
4959
4963
  catalog.set(retiredPath, {
4960
4964
  path: retiredPath,
4961
4965
  content: "",
4966
+ ...retiredPath === "CLAUDE.md" ? { managedBlock: true } : {},
4962
4967
  owners: [
4963
4968
  {
4964
4969
  kind: "retired",
@@ -4974,11 +4979,13 @@ var renderGeneratedSurfaceCatalog = (config) => {
4974
4979
  };
4975
4980
 
4976
4981
  // src/init/lifecycle.ts
4977
- import fs6 from "fs/promises";
4982
+ import fs5 from "fs/promises";
4978
4983
  var plannedContents = /* @__PURE__ */ new WeakMap();
4984
+ var plannedTargets = /* @__PURE__ */ new WeakMap();
4985
+ var plannedExcludedRoots = /* @__PURE__ */ new WeakMap();
4979
4986
  var readFile = async (rootDir, filePath) => {
4980
4987
  try {
4981
- return await fs6.readFile(resolveRepoPath(rootDir, filePath), "utf8");
4988
+ return await fs5.readFile(resolveRepoPath(rootDir, filePath), "utf8");
4982
4989
  } catch (error) {
4983
4990
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
4984
4991
  return null;
@@ -4991,7 +4998,7 @@ var listFiles = async (rootDir, directory) => {
4991
4998
  while (stack.length > 0) {
4992
4999
  const current = stack.pop();
4993
5000
  try {
4994
- for (const entry of await fs6.readdir(resolveRepoPath(rootDir, current), {
5001
+ for (const entry of await fs5.readdir(resolveRepoPath(rootDir, current), {
4995
5002
  withFileTypes: true
4996
5003
  })) {
4997
5004
  const next = `${current}/${entry.name}`;
@@ -5009,7 +5016,7 @@ var findRetiredSurfaces = async (rootDir) => {
5009
5016
  const paths = /* @__PURE__ */ new Set();
5010
5017
  for (const filePath of RETIRED_GENERATED_SURFACES.exactPaths) {
5011
5018
  try {
5012
- await fs6.lstat(resolveRepoPath(rootDir, filePath));
5019
+ await fs5.lstat(resolveRepoPath(rootDir, filePath));
5013
5020
  paths.add(filePath);
5014
5021
  } catch (error) {
5015
5022
  if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
@@ -5027,7 +5034,7 @@ var findRetiredSurfaces = async (rootDir) => {
5027
5034
  paths.add(filePath);
5028
5035
  let packages = [];
5029
5036
  try {
5030
- packages = (await fs6.readdir(resolveRepoPath(rootDir, skillRoot), {
5037
+ packages = (await fs5.readdir(resolveRepoPath(rootDir, skillRoot), {
5031
5038
  withFileTypes: true
5032
5039
  })).filter(
5033
5040
  (entry) => entry.isDirectory() && entry.name.startsWith("truthmark-")
@@ -5040,7 +5047,7 @@ var findRetiredSurfaces = async (rootDir) => {
5040
5047
  for (const retiredFile of RETIRED_GENERATED_SURFACES.retiredPackageFiles) {
5041
5048
  const filePath = `${skillRoot}/${packageName}/${retiredFile}`;
5042
5049
  try {
5043
- await fs6.lstat(resolveRepoPath(rootDir, filePath));
5050
+ await fs5.lstat(resolveRepoPath(rootDir, filePath));
5044
5051
  paths.add(filePath);
5045
5052
  } catch (error) {
5046
5053
  if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
@@ -5050,23 +5057,38 @@ var findRetiredSurfaces = async (rootDir) => {
5050
5057
  }
5051
5058
  return [...paths];
5052
5059
  };
5053
- var buildLifecyclePlan = async (rootDir, config, mode, desired = renderGeneratedSurfaces(config)) => {
5060
+ var buildLifecyclePlan = async (rootDir, config, mode, desired = renderGeneratedSurfaces(config), excludedRoots = []) => {
5054
5061
  const desiredPaths = new Set(desired.map(({ path: path13 }) => path13));
5062
+ const desiredTargetPaths = /* @__PURE__ */ new Set();
5055
5063
  const entries = [];
5056
5064
  const diagnostics = [];
5065
+ const desiredTargets = /* @__PURE__ */ new Map();
5066
+ const targets = /* @__PURE__ */ new Map();
5057
5067
  let applicable = true;
5058
- for (const surface of desired.filter(({ managedBlock }) => managedBlock)) {
5059
- if (!await isSafeExactFile(rootDir, surface.path, true)) {
5068
+ for (const surface of desired) {
5069
+ const target = await resolveSafeExactFileTarget(
5070
+ rootDir,
5071
+ surface.path,
5072
+ true,
5073
+ surface.managedBlock === true,
5074
+ excludedRoots
5075
+ );
5076
+ if (!target) {
5060
5077
  applicable = false;
5061
5078
  entries.push({
5062
5079
  path: surface.path,
5063
5080
  action: "manual-review",
5064
- reason: "Managed instruction destination is aliased or not a regular single-link file."
5081
+ reason: surface.managedBlock ? "Managed instruction destination does not resolve to a regular single-link file inside the worktree." : "Generated file destination is aliased or not a regular single-link file."
5065
5082
  });
5083
+ } else {
5084
+ desiredTargets.set(surface.path, target);
5085
+ desiredTargetPaths.add(target.path);
5066
5086
  }
5067
5087
  }
5068
5088
  for (const surface of desired.filter(({ managedBlock }) => managedBlock)) {
5069
- const content = await readFile(rootDir, surface.path);
5089
+ const target = desiredTargets.get(surface.path);
5090
+ if (!target) continue;
5091
+ const content = await readFile(rootDir, target.path);
5070
5092
  if (content !== null && parseManagedBlock(content).status === "malformed") {
5071
5093
  applicable = false;
5072
5094
  entries.push({
@@ -5076,6 +5098,7 @@ var buildLifecyclePlan = async (rootDir, config, mode, desired = renderGenerated
5076
5098
  });
5077
5099
  }
5078
5100
  }
5101
+ const plannedManagedRemovalTargets = /* @__PURE__ */ new Set();
5079
5102
  const catalog = renderGeneratedSurfaceCatalog(config);
5080
5103
  for (const retiredPath of await findRetiredSurfaces(rootDir)) {
5081
5104
  if (!catalog.some(({ path: catalogPath }) => catalogPath === retiredPath))
@@ -5093,9 +5116,21 @@ var buildLifecyclePlan = async (rootDir, config, mode, desired = renderGenerated
5093
5116
  }
5094
5117
  for (const surface of catalog) {
5095
5118
  if (desiredPaths.has(surface.path)) continue;
5096
- const content = await readFile(rootDir, surface.path);
5097
- if (content === null) continue;
5098
- if (!await isSafeExactFile(rootDir, surface.path, false)) {
5119
+ try {
5120
+ await fs5.lstat(resolveRepoPath(rootDir, surface.path));
5121
+ } catch (error) {
5122
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
5123
+ continue;
5124
+ throw error;
5125
+ }
5126
+ const target = await resolveSafeExactFileTarget(
5127
+ rootDir,
5128
+ surface.path,
5129
+ false,
5130
+ surface.managedBlock === true,
5131
+ excludedRoots
5132
+ );
5133
+ if (!target) {
5099
5134
  applicable = false;
5100
5135
  entries.push({
5101
5136
  path: surface.path,
@@ -5104,6 +5139,9 @@ var buildLifecyclePlan = async (rootDir, config, mode, desired = renderGenerated
5104
5139
  });
5105
5140
  continue;
5106
5141
  }
5142
+ if (desiredTargetPaths.has(target.path)) continue;
5143
+ const content = await readFile(rootDir, target.path);
5144
+ if (content === null) continue;
5107
5145
  if (surface.path === "GEMINI.md" || surface.path.startsWith(".gemini/")) {
5108
5146
  entries.push({
5109
5147
  path: surface.path,
@@ -5120,11 +5158,14 @@ var buildLifecyclePlan = async (rootDir, config, mode, desired = renderGenerated
5120
5158
  reason: "Truthmark managed-block markers are malformed."
5121
5159
  });
5122
5160
  } else if (block.status === "valid") {
5161
+ if (plannedManagedRemovalTargets.has(target.path)) continue;
5162
+ plannedManagedRemovalTargets.add(target.path);
5123
5163
  entries.push({
5124
5164
  path: surface.path,
5125
5165
  action: "remove-managed-block",
5126
5166
  reason: "No configured platform owns this managed block."
5127
5167
  });
5168
+ targets.set(surface.path, target);
5128
5169
  }
5129
5170
  } else if (surface.recognizedContents.includes(content)) {
5130
5171
  entries.push({
@@ -5132,6 +5173,7 @@ var buildLifecyclePlan = async (rootDir, config, mode, desired = renderGenerated
5132
5173
  action: "remove-file",
5133
5174
  reason: "Generated file has no active platform owner."
5134
5175
  });
5176
+ targets.set(surface.path, target);
5135
5177
  } else {
5136
5178
  entries.push({
5137
5179
  path: surface.path,
@@ -5166,22 +5208,34 @@ var buildLifecyclePlan = async (rootDir, config, mode, desired = renderGenerated
5166
5208
  const contents = /* @__PURE__ */ new Map();
5167
5209
  for (const entry of entries) {
5168
5210
  if (entry.action === "remove-file" || entry.action === "remove-managed-block") {
5169
- const content = await readFile(rootDir, entry.path);
5211
+ const target = targets.get(entry.path);
5212
+ const content = target ? await readFile(rootDir, target.path) : null;
5170
5213
  if (content !== null) contents.set(entry.path, content);
5171
5214
  }
5172
5215
  }
5173
5216
  plannedContents.set(plan, contents);
5217
+ plannedTargets.set(plan, targets);
5218
+ plannedExcludedRoots.set(plan, excludedRoots);
5174
5219
  return plan;
5175
5220
  };
5176
5221
  var applyLifecyclePlan = async (rootDir, plan) => {
5177
5222
  if (!plan.applicable || plan.mode !== "apply") return plan;
5178
5223
  const expected = plannedContents.get(plan);
5224
+ const expectedTargets = plannedTargets.get(plan);
5179
5225
  const failure = async (entry) => {
5180
5226
  if (entry.action !== "remove-file" && entry.action !== "remove-managed-block")
5181
5227
  return null;
5182
- if (!await isSafeExactFile(rootDir, entry.path, false))
5228
+ const target = await resolveSafeExactFileTarget(
5229
+ rootDir,
5230
+ entry.path,
5231
+ false,
5232
+ entry.action === "remove-managed-block",
5233
+ plannedExcludedRoots.get(plan)
5234
+ );
5235
+ const expectedTarget = expectedTargets?.get(entry.path);
5236
+ if (!target || !expectedTarget || target.path !== expectedTarget.path || target.aliased !== expectedTarget.aliased)
5183
5237
  return `Unsafe lifecycle target: ${entry.path}`;
5184
- const content = await readFile(rootDir, entry.path);
5238
+ const content = await readFile(rootDir, target.path);
5185
5239
  if (content === null || expected?.get(entry.path) !== content)
5186
5240
  return `Lifecycle target changed after planning: ${entry.path}`;
5187
5241
  if (entry.action === "remove-managed-block" && parseManagedBlock(content).status !== "valid")
@@ -5207,45 +5261,61 @@ var applyLifecyclePlan = async (rootDir, plan) => {
5207
5261
  };
5208
5262
  }
5209
5263
  for (const entry of plan.entries) {
5210
- const absolutePath = resolveRepoPath(rootDir, entry.path);
5264
+ const target = expectedTargets?.get(entry.path);
5265
+ if (!target) continue;
5266
+ const absolutePath = resolveRepoPath(rootDir, target.path);
5211
5267
  if (entry.action === "remove-file") {
5212
- const stat = await fs6.lstat(absolutePath);
5268
+ const stat = await fs5.lstat(absolutePath);
5213
5269
  if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1)
5214
5270
  throw new Error(
5215
5271
  `Refusing unsafe generated file removal: ${entry.path}`
5216
5272
  );
5217
- await fs6.rm(absolutePath);
5273
+ await fs5.rm(absolutePath);
5218
5274
  } else if (entry.action === "remove-managed-block") {
5219
- const stat = await fs6.lstat(absolutePath);
5275
+ const stat = await fs5.lstat(absolutePath);
5220
5276
  if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1)
5221
5277
  throw new Error(`Refusing unsafe managed-block removal: ${entry.path}`);
5222
- const content = await fs6.readFile(absolutePath, "utf8");
5278
+ const content = await fs5.readFile(absolutePath, "utf8");
5223
5279
  const block = parseManagedBlock(content);
5224
5280
  if (block.status !== "valid")
5225
5281
  throw new Error(`Managed block changed during removal: ${entry.path}`);
5226
5282
  const remaining = `${content.slice(0, block.start)}${content.slice(block.end)}`;
5227
- if (remaining.trim().length === 0) await fs6.rm(absolutePath);
5228
- else await fs6.writeFile(absolutePath, remaining, "utf8");
5283
+ if (remaining.trim().length === 0 && !target.aliased)
5284
+ await fs5.rm(absolutePath);
5285
+ else await fs5.writeFile(absolutePath, remaining, "utf8");
5229
5286
  }
5230
5287
  }
5231
5288
  return { ...plan, applied: true };
5232
5289
  };
5233
5290
 
5234
5291
  // src/init/init.ts
5235
- var writeManagedAgentsFile = async (rootDir, path13 = "AGENTS.md", block) => {
5292
+ var writeManagedAgentsFile = async (rootDir, path13, block, excludedRoots) => {
5293
+ const target = await resolveSafeExactFileTarget(
5294
+ rootDir,
5295
+ path13,
5296
+ true,
5297
+ true,
5298
+ excludedRoots
5299
+ );
5300
+ if (!target)
5301
+ throw new Error(`Refusing unsafe managed instruction write: ${path13}`);
5236
5302
  let existingContent = null;
5237
5303
  try {
5238
- existingContent = await fs7.readFile(resolveRepoPath(rootDir, path13), "utf8");
5304
+ existingContent = await fs6.readFile(
5305
+ resolveRepoPath(rootDir, target.path),
5306
+ "utf8"
5307
+ );
5239
5308
  } catch (error) {
5240
5309
  if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
5241
5310
  throw error;
5242
5311
  }
5243
5312
  }
5244
- return writeRepoFile(
5313
+ const result = await writeRepoFile(
5245
5314
  rootDir,
5246
- path13,
5315
+ target.path,
5247
5316
  upsertManagedBlock(existingContent, block)
5248
5317
  );
5318
+ return { ...result, path: path13 };
5249
5319
  };
5250
5320
  var diagnosticCategoryForPath = (filePath, config) => {
5251
5321
  if (filePath === "AGENTS.md") {
@@ -5254,7 +5324,7 @@ var diagnosticCategoryForPath = (filePath, config) => {
5254
5324
  if (filePath === ".github/prompts/truthmark-realize.prompt.md" || filePath.startsWith(".github/skills/truthmark-realize/") || filePath.startsWith(".claude/skills/truthmark-realize/") || filePath.startsWith(".opencode/skills/truthmark-realize/") || filePath.startsWith(".agents/skills/truthmark-realize/") || filePath.startsWith(".antigravity/rules/truthmark-realize") || filePath.startsWith(".cursor/rules/truthmark-realize") || filePath.startsWith(".cursor/skills/truthmark-realize/")) {
5255
5325
  return "realization";
5256
5326
  }
5257
- if (filePath === "CLAUDE.md" || filePath === ".github/copilot-instructions.md" || filePath.startsWith(".github/prompts/truthmark-") || filePath.startsWith(".github/agents/truth-") || filePath.startsWith(".github/skills/truthmark-") || filePath.startsWith(".claude/agents/truth-") || filePath.startsWith(".claude/skills/truthmark-") || filePath.startsWith(".opencode/skills/truthmark-") || filePath.startsWith(".opencode/agents/") || filePath.startsWith(".codex/agents/") || filePath.startsWith(".antigravity/rules/truthmark-") || filePath.startsWith(".cursor/rules/truthmark-") || filePath.startsWith(".cursor/skills/truthmark-")) {
5327
+ if (filePath === "CLAUDE.md" || filePath === ".claude/rules/truthmark.md" || filePath === ".github/copilot-instructions.md" || filePath.startsWith(".github/prompts/truthmark-") || filePath.startsWith(".github/agents/truth-") || filePath.startsWith(".github/skills/truthmark-") || filePath.startsWith(".claude/agents/truth-") || filePath.startsWith(".claude/skills/truthmark-") || filePath.startsWith(".opencode/skills/truthmark-") || filePath.startsWith(".opencode/agents/") || filePath.startsWith(".codex/agents/") || filePath.startsWith(".antigravity/rules/truthmark-") || filePath.startsWith(".cursor/rules/truthmark-") || filePath.startsWith(".cursor/skills/truthmark-")) {
5258
5328
  return "truth-sync";
5259
5329
  }
5260
5330
  if (filePath.startsWith(".agents/skills/truthmark-")) {
@@ -5265,9 +5335,14 @@ var diagnosticCategoryForPath = (filePath, config) => {
5265
5335
  }
5266
5336
  return "config";
5267
5337
  };
5268
- var writePlatformFile = async (rootDir, file) => {
5338
+ var writePlatformFile = async (rootDir, file, excludedRoots) => {
5269
5339
  if (file.managedBlock) {
5270
- return writeManagedAgentsFile(rootDir, file.path, file.content);
5340
+ return writeManagedAgentsFile(
5341
+ rootDir,
5342
+ file.path,
5343
+ file.content,
5344
+ excludedRoots
5345
+ );
5271
5346
  }
5272
5347
  return writeRepoFile(rootDir, file.path, file.content);
5273
5348
  };
@@ -5289,58 +5364,128 @@ var writeDiagnostics = (results, config) => {
5289
5364
  file: result.path
5290
5365
  }));
5291
5366
  };
5292
- var runInit = async (cwd) => {
5367
+ var normalizeRequestedPlatforms = (values) => {
5368
+ const unsupported = values.filter(
5369
+ (value) => !SUPPORTED_PLATFORMS.includes(value)
5370
+ );
5371
+ const selected = new Set(values);
5372
+ return {
5373
+ platforms: SUPPORTED_PLATFORMS.filter((platform) => selected.has(platform)),
5374
+ unsupported: [...new Set(unsupported)]
5375
+ };
5376
+ };
5377
+ var runInit = async (cwd, options = {}) => {
5293
5378
  const repository = await getGitRepository(cwd);
5294
5379
  const rootDir = repository.worktreePath;
5380
+ const excludedGeneratedRoots = [
5381
+ repository.gitEntryPath,
5382
+ repository.gitDir,
5383
+ repository.gitCommonDir
5384
+ ];
5295
5385
  const loadedConfig = await loadConfig(rootDir);
5296
- if (!loadedConfig.config) {
5386
+ const repositoryData = {
5387
+ repositoryRoot: repository.repositoryRoot,
5388
+ worktreePath: repository.worktreePath,
5389
+ branchName: repository.branchName,
5390
+ isDetached: repository.isDetached,
5391
+ isUnborn: repository.isUnborn
5392
+ };
5393
+ if (loadedConfig.status === "invalid") {
5297
5394
  return {
5298
5395
  command: "init",
5299
- summary: "Truthmark init requires .truthmark/config.yml. Run truthmark config first, review the workspace paths, then run truthmark init.",
5396
+ summary: "Truthmark init made no changes because config is invalid.",
5300
5397
  diagnostics: loadedConfig.diagnostics,
5301
- data: {
5302
- repositoryRoot: repository.repositoryRoot,
5303
- worktreePath: repository.worktreePath,
5304
- branchName: repository.branchName,
5305
- isDetached: repository.isDetached,
5306
- isUnborn: repository.isUnborn
5307
- }
5398
+ data: repositoryData
5308
5399
  };
5309
5400
  }
5401
+ const savedPlatforms = loadedConfig.config?.platforms ?? [];
5402
+ let requestedPlatforms;
5403
+ if (options.platforms !== void 0) requestedPlatforms = options.platforms;
5404
+ else if (options.selectPlatforms) {
5405
+ const selected = await options.selectPlatforms(savedPlatforms);
5406
+ if (selected === null)
5407
+ return {
5408
+ command: "init",
5409
+ summary: "Truthmark init cancelled; no repository files were changed.",
5410
+ diagnostics: [],
5411
+ data: { ...repositoryData, cancelled: true }
5412
+ };
5413
+ requestedPlatforms = selected;
5414
+ } else requestedPlatforms = savedPlatforms;
5415
+ const normalized = normalizeRequestedPlatforms(requestedPlatforms);
5416
+ if (normalized.unsupported.length > 0)
5417
+ return {
5418
+ command: "init",
5419
+ summary: "Truthmark init requires supported platform values.",
5420
+ diagnostics: normalized.unsupported.map((platform) => ({
5421
+ category: "config",
5422
+ severity: "error",
5423
+ message: `Unsupported Truthmark platform: ${platform}.`,
5424
+ file: ".truthmark/config.yml"
5425
+ })),
5426
+ data: repositoryData
5427
+ };
5428
+ const config = {
5429
+ ...loadedConfig.config ?? createDefaultConfig(),
5430
+ platforms: normalized.platforms
5431
+ };
5432
+ const existingConfigSource = loadedConfig.config ? await fs6.readFile(resolveRepoPath(rootDir, loadedConfig.configPath), "utf8") : null;
5433
+ const configSource = existingConfigSource ? updateConfigPlatforms(existingConfigSource, normalized.platforms) : renderConfig(normalized.platforms);
5434
+ const configDiagnostics = loadedConfig.status === "loaded" ? loadedConfig.diagnostics : [];
5310
5435
  const results = [];
5311
- const config = loadedConfig.config;
5312
5436
  const block = renderAgentsBlock(config);
5313
5437
  const platformFiles = renderGeneratedSurfaces(config, block);
5438
+ if (!await isSafeExactFile(rootDir, loadedConfig.configPath, true)) {
5439
+ return {
5440
+ command: "init",
5441
+ summary: "Truthmark init made no changes because the config path is unsafe.",
5442
+ diagnostics: [
5443
+ {
5444
+ category: "config",
5445
+ severity: "error",
5446
+ message: "Truthmark config path must be a regular file contained in the repository.",
5447
+ file: loadedConfig.configPath
5448
+ }
5449
+ ],
5450
+ data: repositoryData
5451
+ };
5452
+ }
5314
5453
  const lifecyclePlan = await buildLifecyclePlan(
5315
5454
  rootDir,
5316
5455
  config,
5317
5456
  "apply",
5318
- platformFiles
5457
+ platformFiles,
5458
+ excludedGeneratedRoots
5319
5459
  );
5320
5460
  if (!lifecyclePlan.applicable) {
5321
5461
  return {
5322
5462
  command: "init",
5323
5463
  summary: "Truthmark init made no changes because generated-surface preflight failed.",
5324
- diagnostics: [...loadedConfig.diagnostics, ...lifecyclePlan.diagnostics],
5325
- data: { lifecyclePlan }
5464
+ diagnostics: [...configDiagnostics, ...lifecyclePlan.diagnostics],
5465
+ data: { ...repositoryData, lifecyclePlan }
5326
5466
  };
5327
5467
  }
5468
+ results.push(...await scaffoldHierarchy(rootDir, config));
5469
+ for (const file of platformFiles) {
5470
+ results.push(
5471
+ await writePlatformFile(rootDir, file, excludedGeneratedRoots)
5472
+ );
5473
+ }
5474
+ results.push(
5475
+ await writeRepoFile(rootDir, loadedConfig.configPath, configSource)
5476
+ );
5328
5477
  const appliedLifecyclePlan = await applyLifecyclePlan(rootDir, lifecyclePlan);
5329
5478
  if (!appliedLifecyclePlan.applicable) {
5330
5479
  return {
5331
5480
  command: "init",
5332
5481
  summary: "Truthmark init made no changes because generated-surface preflight failed.",
5333
5482
  diagnostics: [
5334
- ...loadedConfig.diagnostics,
5483
+ ...configDiagnostics,
5335
5484
  ...appliedLifecyclePlan.diagnostics
5336
5485
  ],
5337
- data: { lifecyclePlan: appliedLifecyclePlan }
5486
+ data: { ...repositoryData, lifecyclePlan: appliedLifecyclePlan }
5338
5487
  };
5339
5488
  }
5340
- results.push(...await scaffoldHierarchy(rootDir, config));
5341
- for (const file of platformFiles) {
5342
- results.push(await writePlatformFile(rootDir, file));
5343
- }
5344
5489
  const changedResults = results.filter(
5345
5490
  (result) => result.status !== "unchanged"
5346
5491
  );
@@ -5351,7 +5496,7 @@ var runInit = async (cwd) => {
5351
5496
  command: "init",
5352
5497
  summary: changedResults.length > 0 || lifecycleChanged ? "Initialized or updated the Truthmark repository scaffold." : "Truthmark repository scaffold is already up to date.",
5353
5498
  diagnostics: [
5354
- ...loadedConfig.diagnostics,
5499
+ ...configDiagnostics,
5355
5500
  ...appliedLifecyclePlan.diagnostics,
5356
5501
  ...appliedLifecyclePlan.entries.map((entry) => ({
5357
5502
  category: "generated-surface",
@@ -5362,16 +5507,68 @@ var runInit = async (cwd) => {
5362
5507
  ...writeDiagnostics(results, config)
5363
5508
  ],
5364
5509
  data: {
5365
- repositoryRoot: repository.repositoryRoot,
5366
- worktreePath: repository.worktreePath,
5367
- branchName: repository.branchName,
5368
- isDetached: repository.isDetached,
5369
- isUnborn: repository.isUnborn,
5510
+ ...repositoryData,
5370
5511
  lifecyclePlan: appliedLifecyclePlan
5371
5512
  }
5372
5513
  };
5373
5514
  };
5374
5515
 
5516
+ // src/cli/platform-selection.ts
5517
+ import { createInterface } from "readline/promises";
5518
+ var PLATFORM_LABELS = {
5519
+ codex: "Codex",
5520
+ opencode: "OpenCode",
5521
+ "claude-code": "Claude Code",
5522
+ "github-copilot": "GitHub Copilot",
5523
+ antigravity: "Antigravity",
5524
+ cursor: "Cursor"
5525
+ };
5526
+ var normalizePlatforms2 = (platforms) => {
5527
+ const selected = new Set(platforms);
5528
+ return SUPPORTED_PLATFORMS.filter((platform) => selected.has(platform));
5529
+ };
5530
+ var renderPlatformChoices = (defaults) => {
5531
+ const selected = new Set(defaults);
5532
+ return SUPPORTED_PLATFORMS.map(
5533
+ (platform, index) => `${index + 1}. ${PLATFORM_LABELS[platform]} [${platform}]${selected.has(platform) ? " (selected)" : ""}`
5534
+ ).join("\n");
5535
+ };
5536
+ var parsePlatformSelection = (input, defaults) => {
5537
+ const value = input.trim().toLowerCase();
5538
+ if (value === "") return normalizePlatforms2(defaults);
5539
+ if (value === "q" || value === "quit") return null;
5540
+ if (value === "none") return [];
5541
+ const selected = value.split(",").map((token) => {
5542
+ const index = Number(token.trim());
5543
+ if (!Number.isInteger(index) || index < 1 || index > SUPPORTED_PLATFORMS.length)
5544
+ throw new Error(`Unsupported platform choice: ${token.trim()}`);
5545
+ return SUPPORTED_PLATFORMS[index - 1];
5546
+ });
5547
+ return normalizePlatforms2(selected);
5548
+ };
5549
+ var promptForPlatforms = async (options) => {
5550
+ options.output.write(`${renderPlatformChoices(options.defaults)}
5551
+ `);
5552
+ const readline = createInterface({ input: options.input, output: options.output });
5553
+ try {
5554
+ for (; ; ) {
5555
+ const answer = await readline.question(
5556
+ "Select platforms by number (comma-separated), 'none' for CLI-only, or 'q' to cancel: "
5557
+ );
5558
+ try {
5559
+ return parsePlatformSelection(answer, options.defaults);
5560
+ } catch (error) {
5561
+ options.output.write(
5562
+ `${error instanceof Error ? error.message : String(error)}
5563
+ `
5564
+ );
5565
+ }
5566
+ }
5567
+ } finally {
5568
+ readline.close();
5569
+ }
5570
+ };
5571
+
5375
5572
  // src/init/uninstall.ts
5376
5573
  var runUninstall = async (cwd, mode) => {
5377
5574
  const repository = await getGitRepository(cwd);
@@ -5396,7 +5593,8 @@ var runUninstall = async (cwd, mode) => {
5396
5593
  repository.worktreePath,
5397
5594
  loaded.config,
5398
5595
  mode,
5399
- []
5596
+ [],
5597
+ [repository.gitEntryPath, repository.gitDir, repository.gitCommonDir]
5400
5598
  );
5401
5599
  const plan = await applyLifecyclePlan(repository.worktreePath, planned);
5402
5600
  return {
@@ -5408,7 +5606,7 @@ var runUninstall = async (cwd, mode) => {
5408
5606
  };
5409
5607
 
5410
5608
  // src/checks/branch-scope.ts
5411
- import fs8 from "fs/promises";
5609
+ import fs7 from "fs/promises";
5412
5610
  import fg from "fast-glob";
5413
5611
 
5414
5612
  // src/markdown/hash.ts
@@ -5464,7 +5662,7 @@ var getBranchScopeData = async (cwd) => {
5464
5662
  }
5465
5663
  for (const relativePath of [...relevantFiles].sort()) {
5466
5664
  try {
5467
- const source = await fs8.readFile(resolveWorktreePath(repository, relativePath), "utf8");
5665
+ const source = await fs7.readFile(resolveWorktreePath(repository, relativePath), "utf8");
5468
5666
  relevantFileHashes[relativePath] = hashText(source);
5469
5667
  } catch (error) {
5470
5668
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -5481,14 +5679,14 @@ var getBranchScopeData = async (cwd) => {
5481
5679
  };
5482
5680
 
5483
5681
  // src/checks/authority.ts
5484
- import fs9 from "fs/promises";
5682
+ import fs8 from "fs/promises";
5485
5683
  import fg2 from "fast-glob";
5486
5684
  var looksLikeGlob = (pattern) => {
5487
5685
  return /[*?[\]{}()!+@]/u.test(pattern);
5488
5686
  };
5489
5687
  var pathExists = async (absolutePath) => {
5490
5688
  try {
5491
- await fs9.stat(absolutePath);
5689
+ await fs8.stat(absolutePath);
5492
5690
  return true;
5493
5691
  } catch (error) {
5494
5692
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -5580,10 +5778,10 @@ var checkAuthority = async (rootDir, config) => {
5580
5778
  };
5581
5779
 
5582
5780
  // src/checks/frontmatter.ts
5583
- import fs10 from "fs/promises";
5781
+ import fs9 from "fs/promises";
5584
5782
 
5585
5783
  // src/markdown/frontmatter.ts
5586
- import { parse as parse3 } from "yaml";
5784
+ import { parse as parse4 } from "yaml";
5587
5785
  var openingDelimiterPattern = /^(?:\uFEFF)?---[ \t]*(?:\r?\n|$)/u;
5588
5786
  var closingDelimiterPattern = /^---[ \t]*(?:\r?\n|$)/mu;
5589
5787
  var asRecord = (value) => {
@@ -5614,7 +5812,7 @@ var parseFrontmatter = (source, options = {}) => {
5614
5812
  let data = {};
5615
5813
  if (yamlSource.trim().length > 0) {
5616
5814
  try {
5617
- data = asRecord(parse3(yamlSource));
5815
+ data = asRecord(parse4(yamlSource));
5618
5816
  } catch (error) {
5619
5817
  if (options.throwOnInvalid) {
5620
5818
  throw error;
@@ -5682,7 +5880,7 @@ var checkFrontmatter = async (rootDir, config, markdownPaths, truthDocumentEntri
5682
5880
  }
5683
5881
  const absolutePath = resolveRepoPath(rootDir, markdownPath);
5684
5882
  await assertRepoContainment(rootDir, absolutePath);
5685
- const source = await fs10.readFile(absolutePath, "utf8");
5883
+ const source = await fs9.readFile(absolutePath, "utf8");
5686
5884
  let document;
5687
5885
  try {
5688
5886
  parseFrontmatter(source, { throwOnInvalid: true });
@@ -5772,11 +5970,11 @@ var checkFrontmatter = async (rootDir, config, markdownPaths, truthDocumentEntri
5772
5970
  };
5773
5971
 
5774
5972
  // src/checks/links.ts
5775
- import fs11 from "fs/promises";
5973
+ import fs10 from "fs/promises";
5776
5974
  import path5 from "path";
5777
5975
  var pathExists2 = async (absolutePath) => {
5778
5976
  try {
5779
- await fs11.stat(absolutePath);
5977
+ await fs10.stat(absolutePath);
5780
5978
  return true;
5781
5979
  } catch (error) {
5782
5980
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -5792,7 +5990,7 @@ var checkLinks = async (rootDir, markdownPaths) => {
5792
5990
  continue;
5793
5991
  }
5794
5992
  const absolutePath = resolveRepoPath(rootDir, markdownPath);
5795
- const source = await fs11.readFile(absolutePath, "utf8");
5993
+ const source = await fs10.readFile(absolutePath, "utf8");
5796
5994
  let document;
5797
5995
  try {
5798
5996
  document = parseMarkdownDocument(source);
@@ -5834,12 +6032,12 @@ var checkLinks = async (rootDir, markdownPaths) => {
5834
6032
  };
5835
6033
 
5836
6034
  // src/checks/areas.ts
5837
- import fs14 from "fs/promises";
6035
+ import fs13 from "fs/promises";
5838
6036
  import fg5 from "fast-glob";
5839
6037
  import micromatch5 from "micromatch";
5840
6038
 
5841
6039
  // src/git/files.ts
5842
- import fs12 from "fs/promises";
6040
+ import fs11 from "fs/promises";
5843
6041
  import { execa as execa2 } from "execa";
5844
6042
  import fg3 from "fast-glob";
5845
6043
  import micromatch2 from "micromatch";
@@ -5876,7 +6074,7 @@ var isCurrentContainedFile = async (rootDir, relativePath) => {
5876
6074
  try {
5877
6075
  const absolutePath = resolveRepoPath(rootDir, relativePath);
5878
6076
  await assertRepoContainment(rootDir, absolutePath);
5879
- return (await fs12.stat(absolutePath)).isFile();
6077
+ return (await fs11.stat(absolutePath)).isFile();
5880
6078
  } catch {
5881
6079
  return false;
5882
6080
  }
@@ -5901,7 +6099,7 @@ var discoverRepositoryFilePaths = async (rootDir, ignorePatterns) => {
5901
6099
  };
5902
6100
 
5903
6101
  // src/routing/area-resolver.ts
5904
- import fs13 from "fs/promises";
6102
+ import fs12 from "fs/promises";
5905
6103
  import fg4 from "fast-glob";
5906
6104
  import micromatch3 from "micromatch";
5907
6105
  var unique = (values) => {
@@ -5952,7 +6150,7 @@ var ensureChildPath = async (rootDir, areaFilesRoot, filePath) => {
5952
6150
  var readRouteFile = async (rootDir, filePath) => {
5953
6151
  try {
5954
6152
  return {
5955
- source: await fs13.readFile(resolveRepoPath(rootDir, filePath), "utf8"),
6153
+ source: await fs12.readFile(resolveRepoPath(rootDir, filePath), "utf8"),
5956
6154
  diagnostic: null
5957
6155
  };
5958
6156
  } catch (error) {
@@ -6264,7 +6462,7 @@ var looksLikeGlob2 = (pattern) => {
6264
6462
  };
6265
6463
  var pathExists3 = async (absolutePath) => {
6266
6464
  try {
6267
- await fs14.stat(absolutePath);
6465
+ await fs13.stat(absolutePath);
6268
6466
  return true;
6269
6467
  } catch (error) {
6270
6468
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -6667,7 +6865,7 @@ var checkAreas = async (rootDir, config) => {
6667
6865
  };
6668
6866
 
6669
6867
  // src/checks/decisions.ts
6670
- import fs15 from "fs/promises";
6868
+ import fs14 from "fs/promises";
6671
6869
  import micromatch6 from "micromatch";
6672
6870
  var PRODUCT_CAPABILITY_REQUIRED_HEADINGS = [
6673
6871
  "Capability Promise",
@@ -6783,7 +6981,7 @@ var checkDecisionSections = async (rootDir, config, markdownPaths, truthDocument
6783
6981
  (filePath) => truthDocumentMap.has(filePath) || isDecisionTruthCandidate(config, filePath)
6784
6982
  ).sort();
6785
6983
  for (const filePath of candidatePaths) {
6786
- const source = await fs15.readFile(
6984
+ const source = await fs14.readFile(
6787
6985
  resolveRepoPath(rootDir, filePath),
6788
6986
  "utf8"
6789
6987
  );
@@ -6825,10 +7023,10 @@ var checkDecisionSections = async (rootDir, config, markdownPaths, truthDocument
6825
7023
  };
6826
7024
 
6827
7025
  // src/checks/generated-surfaces.ts
6828
- import fs16 from "fs/promises";
7026
+ import fs15 from "fs/promises";
6829
7027
  var readOptionalFile = async (rootDir, filePath) => {
6830
7028
  try {
6831
- return await fs16.readFile(resolveRepoPath(rootDir, filePath), "utf8");
7029
+ return await fs15.readFile(resolveRepoPath(rootDir, filePath), "utf8");
6832
7030
  } catch (error) {
6833
7031
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
6834
7032
  return null;
@@ -6849,11 +7047,19 @@ var obsoleteGeneratedSurfaceMessage = (surfacePath) => {
6849
7047
  }
6850
7048
  return `Generated surface ${surfacePath} is obsolete; rerun truthmark init.`;
6851
7049
  };
6852
- var checkGeneratedSurfaces = async (rootDir, config) => {
7050
+ var checkGeneratedSurfaces = async (rootDir, config, excludedRoots = []) => {
6853
7051
  const diagnostics = [];
6854
7052
  const renderedSurfaces = renderGeneratedSurfaces(config);
6855
7053
  for (const surface of renderedSurfaces) {
6856
- const content = await readOptionalFile(rootDir, surface.path);
7054
+ const target = await resolveSafeExactFileTarget(
7055
+ rootDir,
7056
+ surface.path,
7057
+ true,
7058
+ surface.managedBlock === true,
7059
+ excludedRoots
7060
+ );
7061
+ if (!target) continue;
7062
+ const content = await readOptionalFile(rootDir, target.path);
6857
7063
  if (content === null) {
6858
7064
  diagnostics.push({
6859
7065
  category: "generated-surface",
@@ -6880,13 +7086,14 @@ var checkGeneratedSurfaces = async (rootDir, config) => {
6880
7086
  rootDir,
6881
7087
  config,
6882
7088
  "dry-run",
6883
- renderedSurfaces
7089
+ renderedSurfaces,
7090
+ excludedRoots
6884
7091
  );
6885
7092
  for (const entry of lifecyclePlan.entries) {
6886
7093
  diagnostics.push({
6887
7094
  category: "generated-surface",
6888
7095
  severity: entry.action === "manual-review" ? "error" : "review",
6889
- message: entry.action === "preserve" && (entry.path.includes("truthmark-preview") || entry.path.endsWith("helper-manifest.yml") || entry.path.endsWith("support/helper-policy.md") || isRetiredGeminiSurfacePath(entry.path)) ? obsoleteGeneratedSurfaceMessage(entry.path) : entry.action === "preserve" ? `Generated surface ${entry.path} is inactive but was preserved: ${entry.reason}` : `Generated surface ${entry.path} is inactive; rerun truthmark init to reconcile it.`,
7096
+ message: entry.action === "manual-review" ? `Generated surface ${entry.path} requires manual review: ${entry.reason}` : entry.action === "preserve" && (entry.path.includes("truthmark-preview") || entry.path.endsWith("helper-manifest.yml") || entry.path.endsWith("support/helper-policy.md") || isRetiredGeminiSurfacePath(entry.path)) ? obsoleteGeneratedSurfaceMessage(entry.path) : entry.action === "preserve" ? `Generated surface ${entry.path} is inactive but was preserved: ${entry.reason}` : `Generated surface ${entry.path} is inactive; rerun truthmark init to reconcile it.`,
6890
7097
  file: entry.path
6891
7098
  });
6892
7099
  }
@@ -6898,7 +7105,7 @@ import path11 from "path";
6898
7105
  import micromatch7 from "micromatch";
6899
7106
 
6900
7107
  // src/repo-index/file-tree.ts
6901
- import fs17 from "fs/promises";
7108
+ import fs16 from "fs/promises";
6902
7109
  import path8 from "path";
6903
7110
 
6904
7111
  // src/truth/source-references.ts
@@ -7043,7 +7250,7 @@ var discoverRepoFiles = async (rootDir, ignore) => {
7043
7250
  for (const filePath of discoveredFiles) {
7044
7251
  let stat;
7045
7252
  try {
7046
- stat = await fs17.stat(path8.join(rootDir, filePath));
7253
+ stat = await fs16.stat(path8.join(rootDir, filePath));
7047
7254
  } catch (error) {
7048
7255
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
7049
7256
  continue;
@@ -7067,7 +7274,7 @@ var discoverRepoFiles = async (rootDir, ignore) => {
7067
7274
  });
7068
7275
  }
7069
7276
  if (kind === "doc") {
7070
- const source = await fs17.readFile(path8.join(rootDir, filePath), "utf8");
7277
+ const source = await fs16.readFile(path8.join(rootDir, filePath), "utf8");
7071
7278
  const parsed = parseFrontmatter(source);
7072
7279
  const markdown = parseMarkdownDocument(parsed.content);
7073
7280
  const title = markdown.headings.find((heading) => heading.depth === 1)?.text ?? null;
@@ -7095,7 +7302,7 @@ var discoverRepoFiles = async (rootDir, ignore) => {
7095
7302
  };
7096
7303
 
7097
7304
  // src/repo-index/package-metadata.ts
7098
- import fs18 from "fs/promises";
7305
+ import fs17 from "fs/promises";
7099
7306
  import path9 from "path";
7100
7307
  import fg6 from "fast-glob";
7101
7308
  var packageManagerFor = async (rootDir, packageDir) => {
@@ -7108,7 +7315,7 @@ var packageManagerFor = async (rootDir, packageDir) => {
7108
7315
  ];
7109
7316
  for (const [lockfile, manager] of lockfiles) {
7110
7317
  try {
7111
- await fs18.access(path9.join(rootDir, packageDir, lockfile));
7318
+ await fs17.access(path9.join(rootDir, packageDir, lockfile));
7112
7319
  return manager;
7113
7320
  } catch {
7114
7321
  continue;
@@ -7126,7 +7333,7 @@ var discoverPackageMetadata = async (rootDir) => {
7126
7333
  const packages = [];
7127
7334
  for (const packageFile of packageFiles.sort()) {
7128
7335
  const packageDir = path9.posix.dirname(packageFile) === "." ? "" : path9.posix.dirname(packageFile);
7129
- const raw = JSON.parse(await fs18.readFile(path9.join(rootDir, packageFile), "utf8"));
7336
+ const raw = JSON.parse(await fs17.readFile(path9.join(rootDir, packageFile), "utf8"));
7130
7337
  const scripts = raw.scripts && typeof raw.scripts === "object" ? Object.keys(raw.scripts).sort() : [];
7131
7338
  packages.push({
7132
7339
  path: packageFile,
@@ -7231,7 +7438,7 @@ var buildRepoIndex = async (cwd) => {
7231
7438
  import { execa as execa4 } from "execa";
7232
7439
 
7233
7440
  // src/git/changes.ts
7234
- import fs19 from "fs/promises";
7441
+ import fs18 from "fs/promises";
7235
7442
  import path10 from "path";
7236
7443
  import { execa as execa3 } from "execa";
7237
7444
  var normalizePath3 = (filePath) => {
@@ -7243,7 +7450,7 @@ var listChangedPaths = async (cwd, args) => {
7243
7450
  };
7244
7451
  var pathExists4 = async (filePath) => {
7245
7452
  try {
7246
- await fs19.access(filePath);
7453
+ await fs18.access(filePath);
7247
7454
  return true;
7248
7455
  } catch {
7249
7456
  return false;
@@ -7490,13 +7697,13 @@ var checkFreshness = async (rootDir, _config, _truthDocumentPaths, base) => {
7490
7697
  };
7491
7698
 
7492
7699
  // src/evidence/validate.ts
7493
- import fs21 from "fs/promises";
7700
+ import fs20 from "fs/promises";
7494
7701
  import fg7 from "fast-glob";
7495
7702
 
7496
7703
  // src/evidence/parse.ts
7497
- import fs20 from "fs/promises";
7704
+ import fs19 from "fs/promises";
7498
7705
  import path12 from "path";
7499
- import { parse as parse4 } from "yaml";
7706
+ import { parse as parse5 } from "yaml";
7500
7707
  var yamlFencePattern = /```ya?ml\s*\n([\s\S]*?)```/giu;
7501
7708
  var topLevelEvidenceMarkerPattern = /^evidence\s*:/imu;
7502
7709
  var toEvidenceReference = (truthDocPath, raw) => {
@@ -7514,7 +7721,7 @@ var toEvidenceReference = (truthDocPath, raw) => {
7514
7721
  };
7515
7722
  };
7516
7723
  var parseEvidenceReferences = async (rootDir, truthDocPath) => {
7517
- const source = await fs20.readFile(path12.join(rootDir, truthDocPath), "utf8");
7724
+ const source = await fs19.readFile(path12.join(rootDir, truthDocPath), "utf8");
7518
7725
  const parsed = parseFrontmatter(source);
7519
7726
  const references = [];
7520
7727
  for (const entry of parseSourceReferences(source, truthDocPath)) {
@@ -7529,7 +7736,7 @@ var parseEvidenceReferences = async (rootDir, truthDocPath) => {
7529
7736
  if (!topLevelEvidenceMarkerPattern.test(yamlBlock)) {
7530
7737
  continue;
7531
7738
  }
7532
- const block = parse4(yamlBlock);
7739
+ const block = parse5(yamlBlock);
7533
7740
  const rawEvidence = block && typeof block === "object" && "evidence" in block ? block.evidence : null;
7534
7741
  if (!Array.isArray(rawEvidence)) {
7535
7742
  continue;
@@ -7547,7 +7754,7 @@ var parseEvidenceReferences = async (rootDir, truthDocPath) => {
7547
7754
  // src/evidence/validate.ts
7548
7755
  var pathExists5 = async (filePath) => {
7549
7756
  try {
7550
- await fs21.access(filePath);
7757
+ await fs20.access(filePath);
7551
7758
  return true;
7552
7759
  } catch {
7553
7760
  return false;
@@ -7592,7 +7799,7 @@ var validateHash = async (rootDir, reference) => {
7592
7799
  if (!reference.contentHash.startsWith("sha256:")) {
7593
7800
  return diagnosticFor(reference, `Evidence hash for ${reference.path} must use sha256:.`);
7594
7801
  }
7595
- const source = await fs21.readFile(resolveRepoPath(rootDir, reference.path), "utf8");
7802
+ const source = await fs20.readFile(resolveRepoPath(rootDir, reference.path), "utf8");
7596
7803
  const lines = source.split("\n");
7597
7804
  const startLine = reference.startLine ?? 1;
7598
7805
  const endLine = reference.endLine ?? lines.length;
@@ -7606,7 +7813,7 @@ var validateLineSpan = async (rootDir, reference) => {
7606
7813
  if (reference.startLine === void 0 && reference.endLine === void 0) {
7607
7814
  return null;
7608
7815
  }
7609
- const source = await fs21.readFile(resolveRepoPath(rootDir, reference.path), "utf8");
7816
+ const source = await fs20.readFile(resolveRepoPath(rootDir, reference.path), "utf8");
7610
7817
  const lines = source.split("\n");
7611
7818
  const startLine = reference.startLine ?? 1;
7612
7819
  const endLine = reference.endLine ?? lines.length;
@@ -7837,7 +8044,11 @@ var runCheck = async (cwd, options = {}) => {
7837
8044
  markdownPaths,
7838
8045
  areas.truthDocumentEntries
7839
8046
  );
7840
- const generatedSurfaces = await checkGeneratedSurfaces(rootDir, loadResult.config);
8047
+ const generatedSurfaces = await checkGeneratedSurfaces(
8048
+ rootDir,
8049
+ loadResult.config,
8050
+ [repository.gitEntryPath, repository.gitDir, repository.gitCommonDir]
8051
+ );
7841
8052
  const sourceTraceability = await validateEvidenceReferences(rootDir, areas.truthDocumentPaths);
7842
8053
  const freshness = options.base ? await checkFreshness(rootDir, loadResult.config, areas.truthDocumentPaths, options.base) : null;
7843
8054
  const diagnostics = [
@@ -8207,10 +8418,14 @@ var buildWorkflowState = async (cwd, options) => {
8207
8418
  comparisonBase ? { base: comparisonBase } : {}
8208
8419
  );
8209
8420
  const diagnostics = [
8210
- ...loadResult.diagnostics,
8211
- ...repoIndex.diagnostics,
8212
- ...impactSet?.diagnostics ?? [],
8213
- ...checkResult.diagnostics
8421
+ ...new Map(
8422
+ [
8423
+ ...loadResult.diagnostics,
8424
+ ...repoIndex.diagnostics,
8425
+ ...impactSet?.diagnostics ?? [],
8426
+ ...checkResult.diagnostics
8427
+ ].map((diagnostic) => [JSON.stringify(diagnostic), diagnostic])
8428
+ ).values()
8214
8429
  ];
8215
8430
  const applicability = applicabilityFor(
8216
8431
  options.workflow,
@@ -8251,7 +8466,7 @@ var buildWorkflowState = async (cwd, options) => {
8251
8466
  };
8252
8467
 
8253
8468
  // src/cli/handlers.ts
8254
- import fs22 from "fs/promises";
8469
+ import fs21 from "fs/promises";
8255
8470
 
8256
8471
  // src/agents/workflow-helper-validation.ts
8257
8472
  import { parse as parseYaml2 } from "yaml";
@@ -8568,11 +8783,16 @@ var validateWriteLeaseText = (leaseText, changedText) => {
8568
8783
  };
8569
8784
 
8570
8785
  // src/cli/handlers.ts
8571
- var runConfig2 = async (options) => {
8572
- return runConfig(process.cwd(), options);
8573
- };
8574
- var runInit2 = async () => {
8575
- return runInit(process.cwd());
8786
+ var runInit2 = async (options = {}) => {
8787
+ const interactive = options.platforms === void 0 && !options.json && process.stdin.isTTY && process.stdout.isTTY;
8788
+ return runInit(process.cwd(), {
8789
+ platforms: options.platforms,
8790
+ selectPlatforms: interactive ? (defaults) => promptForPlatforms({
8791
+ defaults,
8792
+ input: process.stdin,
8793
+ output: process.stdout
8794
+ }) : void 0
8795
+ });
8576
8796
  };
8577
8797
  var runUninstall2 = async (mode) => {
8578
8798
  return runUninstall(process.cwd(), mode);
@@ -8645,9 +8865,22 @@ var invalidWorkflowResult = (command, workflow) => ({
8645
8865
  request: workflow ? { workflow } : {}
8646
8866
  }
8647
8867
  });
8868
+ var summarizeValues = (values) => values.length <= 5 ? values.join(", ") : `${values.slice(0, 5).join(", ")} (+${values.length - 5} more)`;
8869
+ var renderWorkflowStatusSummary = (state) => {
8870
+ const card = state.workflowCard;
8871
+ return [
8872
+ `Truthmark workflow status completed for ${state.workflow}.`,
8873
+ `Applicability: ${state.applicability.state}.`,
8874
+ ...card.affectedFiles.length > 0 ? [`Affected files: ${summarizeValues(card.affectedFiles)}`] : [],
8875
+ ...card.likelyRouteOwners.length > 0 ? [`Likely route owners: ${summarizeValues(card.likelyRouteOwners)}`] : [],
8876
+ ...card.suggestedTruthDocs.length > 0 ? [`Suggested truth docs: ${summarizeValues(card.suggestedTruthDocs)}`] : [],
8877
+ ...card.openQuestions.length > 0 ? [`Open questions: ${summarizeValues(card.openQuestions)}`] : [],
8878
+ ...state.nextSteps.length > 0 ? [`Next steps: ${summarizeValues(state.nextSteps)}`] : []
8879
+ ].join("\n");
8880
+ };
8648
8881
  var readHelperFile = async (filePath, helper) => {
8649
8882
  try {
8650
- return await fs22.readFile(filePath, "utf8");
8883
+ return await fs21.readFile(filePath, "utf8");
8651
8884
  } catch (error) {
8652
8885
  const message = error instanceof Error ? error.message : String(error);
8653
8886
  return { ok: false, helper, errors: [`could not read file: ${message}`] };
@@ -8685,7 +8918,7 @@ var runWorkflowStatus = async (options) => {
8685
8918
  });
8686
8919
  return {
8687
8920
  command: "workflow status",
8688
- summary: `Truthmark workflow status completed for ${options.workflow}.`,
8921
+ summary: renderWorkflowStatusSummary(workflowState),
8689
8922
  diagnostics: workflowState.diagnostics,
8690
8923
  data: {
8691
8924
  request: {
@@ -8740,27 +8973,36 @@ var writeValidationResult = (command, result, options) => {
8740
8973
  var addJsonOption = (command) => {
8741
8974
  return command.option("--json", "Render command output as JSON");
8742
8975
  };
8976
+ var collectPlatform = (value, previous) => [
8977
+ ...previous ?? [],
8978
+ value
8979
+ ];
8743
8980
  var buildProgram = () => {
8744
8981
  const program = new Command();
8745
8982
  program.name("truthmark").description(
8746
8983
  "Git-native, branch-scoped truth workflow installer for local AI coding agents."
8747
8984
  ).showHelpAfterError();
8748
- addJsonOption(
8749
- program.command("config").description(
8750
- "Create or render the Truthmark repository config before initialization."
8751
- ).option(
8752
- "--stdout",
8753
- "Render default config in the JSON data payload without writing"
8754
- ).option("--force", "Overwrite an existing .truthmark/config.yml")
8755
- ).action(async (options) => {
8756
- writeResult(await runConfig2(options), options);
8757
- });
8758
8985
  addJsonOption(
8759
8986
  program.command("init").description(
8760
8987
  "Initialize Truthmark workflow files in the current repository."
8988
+ ).option(
8989
+ "--platform <id>",
8990
+ "Select a repository agent platform; repeat for multiple platforms",
8991
+ collectPlatform
8992
+ ).option(
8993
+ "--clear-platforms",
8994
+ "Remove all configured repository agent platforms"
8761
8995
  )
8762
8996
  ).action(async (options) => {
8763
- writeResult(await runInit2(), options);
8997
+ if (options.clearPlatforms && options.platform !== void 0) {
8998
+ program.error("truthmark init cannot combine --clear-platforms and --platform");
8999
+ return;
9000
+ }
9001
+ const platforms = options.clearPlatforms ? [] : options.platform;
9002
+ writeResult(
9003
+ await runInit2({ json: options.json, platforms }),
9004
+ options
9005
+ );
8764
9006
  });
8765
9007
  addJsonOption(
8766
9008
  program.command("uninstall").description(