truthmark 2.2.7 → 2.3.0

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";
@@ -213,81 +219,6 @@ var ensureRepoFile = async (rootDir, relativePath, content) => {
213
219
  };
214
220
  };
215
221
 
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
222
  // src/config/schema.ts
292
223
  var SUPPORTED_PLATFORMS = [
293
224
  "codex",
@@ -457,348 +388,276 @@ var createDefaultConfig = () => ({
457
388
  ignore: ["node_modules/**", "vendor/**", "dist/**", "build/**"]
458
389
  });
459
390
 
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])
391
+ // src/config/load.ts
392
+ var ajv = new Ajv({ allErrors: true });
393
+ var validateTruthmarkConfig = ajv.compile(truthmarkConfigSchema);
394
+ var toConfigDiagnostic = (message, file) => ({
395
+ category: "config",
396
+ severity: "error",
397
+ message,
398
+ file
477
399
  });
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
- };
400
+ var normalizeRepoRelativePath = (value) => {
401
+ const slashNormalized = value.replace(/\\/gu, "/");
402
+ return path2.posix.normalize(slashNormalized).replace(/\/+$/u, "");
490
403
  };
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);
404
+ var isUnsafeRepoRelativePath = (value) => {
405
+ const slashNormalized = value.replace(/\\/gu, "/");
406
+ const normalized = normalizeRepoRelativePath(value);
407
+ const parts = slashNormalized.split("/");
408
+ 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
409
  };
494
- var isTruthDocumentKind = (value) => {
495
- return typeof value === "string" && TRUTH_DOCUMENT_KINDS.includes(value);
410
+ var joinWorkspacePath = (workspace, childPath) => {
411
+ return normalizeRepoRelativePath(`${workspace}/${childPath}`);
496
412
  };
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;
413
+ var portalOutputFor = (workspace) => joinWorkspacePath(workspace, DERIVED_TRUTHMARK_PATHS.portalOutput);
414
+ var pathsOverlap = (left, right) => {
415
+ const normalizedLeft = normalizeRepoRelativePath(left);
416
+ const normalizedRight = normalizeRepoRelativePath(right);
417
+ return normalizedLeft === normalizedRight || normalizedLeft.startsWith(`${normalizedRight}/`) || normalizedRight.startsWith(`${normalizedLeft}/`);
515
418
  };
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";
419
+ var CONFIG_PATH = ".truthmark/config.yml";
420
+ var FORBIDDEN_WORKSPACE_OVERLAPS = [
421
+ ".git",
422
+ ".truthmark",
423
+ "package.json",
424
+ "package-lock.json",
425
+ "pnpm-lock.yaml",
426
+ "yarn.lock",
427
+ "src",
428
+ "tests"
429
+ ];
430
+ var unsupportedShapeDiagnostics = (parsedConfig, configPath) => {
431
+ if (!parsedConfig || typeof parsedConfig !== "object" || Array.isArray(parsedConfig)) {
432
+ return [];
522
433
  }
523
- if (normalizedPath.startsWith(`${engineeringTruthRoot}/`)) {
524
- return "engineering";
434
+ const record = parsedConfig;
435
+ const diagnostics = [];
436
+ if (record.version !== 2) {
437
+ diagnostics.push(
438
+ toConfigDiagnostic(
439
+ "Unsupported Truthmark config shape. This release requires version: 2 with a truthmark workspace block.",
440
+ configPath
441
+ )
442
+ );
525
443
  }
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";
444
+ if ("docs" in record || "authority" in record) {
445
+ diagnostics.push(
446
+ toConfigDiagnostic(
447
+ "Unsupported Truthmark config shape. Remove old docs.roots and legacy authority settings; use version: 2 truthmark.workspace paths.",
448
+ configPath
449
+ )
450
+ );
534
451
  }
535
- return kind.slice("engineering-".length);
452
+ return diagnostics;
536
453
  };
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 [];
454
+ var validateWorkspacePaths = (rawConfig, configPath) => {
455
+ const diagnostics = [];
456
+ const workspace = normalizeRepoRelativePath(rawConfig.truthmark.workspace);
457
+ if (isUnsafeRepoRelativePath(rawConfig.truthmark.workspace) || FORBIDDEN_WORKSPACE_OVERLAPS.some(
458
+ (forbidden) => pathsOverlap(workspace, forbidden)
459
+ )) {
460
+ diagnostics.push(
461
+ toConfigDiagnostic(
462
+ "truthmark.workspace must be a non-empty repo-relative directory that does not overlap repository control, package, source, test, or instruction paths.",
463
+ configPath
464
+ )
465
+ );
544
466
  }
545
- return value.filter((entry) => typeof entry === "string");
467
+ return diagnostics;
546
468
  };
547
- var findTruthDocumentsYamlFenceRange = (sectionLines) => {
548
- const trimmedLines = sectionLines.map((line) => line.trim());
549
- const openingFenceIndex = trimmedLines.findIndex(
550
- (line) => /^```(?:yaml|yml)?$/u.test(line)
469
+ var normalizeConfig = (rawConfig) => {
470
+ const workspace = normalizeRepoRelativePath(rawConfig.truthmark.workspace);
471
+ const routesIndex = joinWorkspacePath(
472
+ workspace,
473
+ DERIVED_TRUTHMARK_PATHS.routesIndex
551
474
  );
552
- if (openingFenceIndex === -1) {
553
- return null;
554
- }
555
- const closingFenceIndex = trimmedLines.findIndex(
556
- (line, index) => index > openingFenceIndex && line === "```"
475
+ const routeAreasRoot = joinWorkspacePath(
476
+ workspace,
477
+ DERIVED_TRUTHMARK_PATHS.routeAreasRoot
478
+ );
479
+ const productTruthRoot = joinWorkspacePath(
480
+ workspace,
481
+ DERIVED_TRUTHMARK_PATHS.productTruthRoot
482
+ );
483
+ const engineeringTruthRoot = joinWorkspacePath(
484
+ workspace,
485
+ DERIVED_TRUTHMARK_PATHS.engineeringTruthRoot
486
+ );
487
+ const templatesRoot = joinWorkspacePath(
488
+ workspace,
489
+ DERIVED_TRUTHMARK_PATHS.templatesRoot
490
+ );
491
+ const portalOutput = portalOutputFor(workspace);
492
+ const portalTemplate = joinWorkspacePath(
493
+ workspace,
494
+ DERIVED_TRUTHMARK_PATHS.portalTemplate
557
495
  );
558
496
  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
497
+ version: rawConfig.version,
498
+ platforms: rawConfig.platforms ?? [...DEFAULT_PLATFORMS],
499
+ truthmark: {
500
+ workspace,
501
+ routes: {
502
+ index: DERIVED_TRUTHMARK_PATHS.routesIndex,
503
+ areas: DERIVED_TRUTHMARK_PATHS.routeAreasRoot,
504
+ defaultArea: DERIVED_TRUTHMARK_PATHS.defaultArea,
505
+ maxDelegationDepth: DERIVED_TRUTHMARK_PATHS.maxDelegationDepth
506
+ },
507
+ truth: {
508
+ productRoot: DERIVED_TRUTHMARK_PATHS.productTruthRoot,
509
+ engineeringRoot: DERIVED_TRUTHMARK_PATHS.engineeringTruthRoot
510
+ },
511
+ templates: {
512
+ root: DERIVED_TRUTHMARK_PATHS.templatesRoot
513
+ },
514
+ generated: {
515
+ portal: {
516
+ enabled: rawConfig.truthmark.generated.portal.enabled
517
+ }
518
+ },
519
+ paths: {
520
+ routesIndex,
521
+ routeAreasRoot,
522
+ productTruthRoot,
523
+ engineeringTruthRoot,
524
+ templatesRoot,
525
+ portalOutput,
526
+ portalTemplate
527
+ },
528
+ controlledPaths: [
529
+ routesIndex,
530
+ `${routeAreasRoot}/**/*.md`,
531
+ `${productTruthRoot}/**/*.md`,
532
+ `${engineeringTruthRoot}/**/*.md`,
533
+ `${templatesRoot}/*.md`
534
+ ]
535
+ },
536
+ frontmatter: {
537
+ required: rawConfig.frontmatter?.required ?? [],
538
+ recommended: rawConfig.frontmatter?.recommended ?? []
539
+ },
540
+ ignore: rawConfig.ignore ?? []
593
541
  };
594
542
  };
595
- var parseTruthDocumentsFromYaml = (sectionLines, areaName, options) => {
596
- const yamlFenceRange = findTruthDocumentsYamlFenceRange(sectionLines);
597
- if (!yamlFenceRange) {
598
- return {
599
- truthDocuments: [],
600
- truthDocumentEntries: [],
601
- diagnostics: []
602
- };
543
+ var compatibilityDiagnostics = (rawConfig, configPath) => {
544
+ if (!("instruction_targets" in rawConfig)) {
545
+ return [];
603
546
  }
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
- };
547
+ return [
548
+ {
549
+ category: "config",
550
+ severity: "review",
551
+ message: "instruction_targets is accepted for compatibility but ignored; select platforms to control managed instruction-file writes.",
552
+ file: configPath
553
+ }
554
+ ];
555
+ };
556
+ var loadConfig = async (rootDir) => {
557
+ const absolutePath = resolveRepoPath(rootDir, CONFIG_PATH);
558
+ let source;
559
+ try {
560
+ source = await fs2.readFile(absolutePath, "utf8");
561
+ } catch (error) {
562
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
563
+ return {
564
+ status: "missing",
565
+ config: null,
566
+ diagnostics: [
567
+ toConfigDiagnostic("Missing .truthmark/config.yml.", CONFIG_PATH)
568
+ ],
569
+ configPath: CONFIG_PATH
570
+ };
571
+ }
572
+ throw error;
615
573
  }
616
- let parsedBlock;
574
+ let parsedConfig;
617
575
  try {
618
- parsedBlock = parse(
619
- sectionLines.slice(
620
- yamlFenceRange.openingFenceIndex + 1,
621
- yamlFenceRange.closingFenceIndex
622
- ).join("\n")
623
- );
576
+ parsedConfig = parse(source);
624
577
  } catch (error) {
625
578
  return {
626
- truthDocuments: [],
627
- truthDocumentEntries: [],
579
+ status: "invalid",
580
+ config: null,
628
581
  diagnostics: [
629
- createAreaDiagnostic(
630
- `Area ${areaName} has invalid YAML truth document metadata: ${error instanceof Error ? error.message : String(error)}.`,
631
- areaName
582
+ toConfigDiagnostic(
583
+ `Invalid YAML: ${error instanceof Error ? error.message : String(error)}`,
584
+ CONFIG_PATH
632
585
  )
633
- ]
586
+ ],
587
+ configPath: CONFIG_PATH
634
588
  };
635
589
  }
636
- const rawEntries = parsedBlock && typeof parsedBlock === "object" && "truth_documents" in parsedBlock ? parsedBlock.truth_documents : null;
637
- if (!Array.isArray(rawEntries)) {
590
+ const unsupportedDiagnostics = unsupportedShapeDiagnostics(
591
+ parsedConfig,
592
+ CONFIG_PATH
593
+ );
594
+ if (unsupportedDiagnostics.length > 0) {
638
595
  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
- ]
596
+ status: "invalid",
597
+ config: null,
598
+ diagnostics: unsupportedDiagnostics,
599
+ configPath: CONFIG_PATH
647
600
  };
648
601
  }
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
- });
602
+ if (!validateTruthmarkConfig(parsedConfig)) {
603
+ return {
604
+ status: "invalid",
605
+ config: null,
606
+ diagnostics: (validateTruthmarkConfig.errors ?? []).map(
607
+ (error) => {
608
+ const propertyPath = error.instancePath || "/";
609
+ const additionalProperty = error.keyword === "additionalProperties" && error.params && "additionalProperty" in error.params ? String(error.params.additionalProperty) : null;
610
+ const message = additionalProperty ? `${propertyPath} additional property ${additionalProperty} is not allowed` : `${propertyPath} ${error.message ?? "is invalid"}`.trim();
611
+ return toConfigDiagnostic(message, CONFIG_PATH);
612
+ }
613
+ ),
614
+ configPath: CONFIG_PATH
615
+ };
616
+ }
617
+ const pathDiagnostics = validateWorkspacePaths(
618
+ parsedConfig,
619
+ CONFIG_PATH
620
+ );
621
+ if (pathDiagnostics.length > 0) {
622
+ return {
623
+ status: "invalid",
624
+ config: null,
625
+ diagnostics: pathDiagnostics,
626
+ configPath: CONFIG_PATH
627
+ };
678
628
  }
679
629
  return {
680
- truthDocuments: truthDocumentEntries.map((entry) => entry.path),
681
- truthDocumentEntries,
682
- diagnostics
630
+ status: "loaded",
631
+ config: normalizeConfig(parsedConfig),
632
+ diagnostics: compatibilityDiagnostics(
633
+ parsedConfig,
634
+ CONFIG_PATH
635
+ ),
636
+ configPath: CONFIG_PATH
683
637
  };
684
638
  };
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;
639
+
640
+ // src/config/render.ts
641
+ import { parse as parse2, parseDocument, stringify } from "yaml";
642
+ var normalizePlatforms = (platforms) => {
643
+ const selected = new Set(platforms);
644
+ return SUPPORTED_PLATFORMS.filter((platform) => selected.has(platform));
699
645
  };
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
- };
646
+ var renderConfig = (platforms = []) => {
647
+ const normalized = normalizePlatforms(platforms);
648
+ const config = createDefaultRawConfig();
649
+ if (normalized.length > 0) config.platforms = normalized;
650
+ return stringify(config);
651
+ };
652
+ var updateConfigPlatforms = (source, platforms) => {
653
+ const normalized = normalizePlatforms(platforms);
654
+ const parsed = parse2(source);
655
+ if (JSON.stringify(normalizePlatforms(parsed.platforms ?? [])) === JSON.stringify(normalized))
656
+ return source;
657
+ const document = parseDocument(source);
658
+ if (normalized.length === 0) document.delete("platforms");
659
+ else document.set("platforms", normalized);
660
+ return document.toString();
802
661
  };
803
662
 
804
663
  // src/truth/docs.ts
@@ -812,1521 +671,1605 @@ var resolveEngineeringTruthRoot = (config) => {
812
671
  return config.truthmark.paths.engineeringTruthRoot;
813
672
  };
814
673
 
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));
674
+ // src/truth/evidence.ts
675
+ var renderClaimEvidenceCheckedSection = (items) => {
676
+ return [
677
+ "Evidence checked:",
678
+ ...items.map((item) => {
679
+ return [
680
+ `- Claim: ${item.claim}`,
681
+ ` Evidence: ${item.evidence.join(" / ")}`,
682
+ ` Result: ${item.result}`
683
+ ].join("\n");
684
+ })
685
+ ].join("\n");
822
686
  };
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
- }
687
+ var renderAuditEvidenceCheckedSection = (items) => {
831
688
  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(" ");
689
+ "Evidence checked:",
690
+ ...items.map((item) => {
691
+ return [
692
+ `- Finding: ${item.finding}`,
693
+ ` Evidence: ${item.evidence.join(" / ")}`,
694
+ ` Suggested fix: ${item.suggestedFix}`,
695
+ ` Confidence: ${item.confidence}`
696
+ ].join("\n");
697
+ })
698
+ ].join("\n");
835
699
  };
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.";
700
+
701
+ // src/agents/shared.ts
702
+ var renderBulletLine = (line) => {
703
+ const normalized = line.trim().replace(/^-\s*/u, "");
704
+ return `- ${normalized}`;
841
705
  };
842
- var renderConfigTemplate = () => {
843
- return stringify(createDefaultRawConfig());
706
+ var renderBulletBlock = (lines, indent = " ") => {
707
+ return lines.split(/\n/u).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => `${indent}${renderBulletLine(line)}`).join("\n");
844
708
  };
845
- var titleCase = (value) => {
846
- return value.split(/[-_\s]+/u).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
709
+ var renderLaneClassificationRuleBlock = (config = defaultAgentConfig(), indent = " ") => {
710
+ const [, ...rules] = renderLaneClassificationInstructions(config).split(/\n/u);
711
+ return renderBulletBlock(rules.join("\n"), indent);
847
712
  };
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"
713
+ var renderReadOnlyLaneClassificationRuleBlock = (config = defaultAgentConfig(), indent = " ") => {
714
+ const productTruthRoot = resolveProductTruthRoot(config);
715
+ const engineeringTruthRoot = resolveEngineeringTruthRoot(config);
716
+ return renderBulletBlock(
717
+ [
718
+ "classify the request or changed surface as product-lane, engineering-lane, both-lane, or ambiguous for reporting only",
719
+ `product-lane ownership belongs under ${productTruthRoot} and describes product promises, boundaries, rationale, decisions, and success criteria`,
720
+ `engineering-lane ownership belongs under ${engineeringTruthRoot} and describes source-backed current realization, contracts, architecture, workflows, operations, or tests`,
721
+ "both-lane ownership uses separate product and engineering docs cross-linked in route YAML with realized_by and realizes, not in doc frontmatter",
722
+ "ambiguous lane ownership should be reported for manual handoff or routed to Truth Structure",
723
+ LANE_INVARIANT
724
+ ].join("\n"),
725
+ indent
855
726
  );
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
727
  };
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
- );
728
+ var DECISION_TRUTH_INSTRUCTIONS = [
729
+ "Decision truth lives in the canonical doc it governs; date active decisions inline when added or changed.",
730
+ "Do not create separate active-decision ADR/planning logs; replace the active decision and let Git history carry the audit trail.",
731
+ "Product decisions belong in product truth; engineering, architecture, contract, workflow, and operational decisions belong in engineering truth."
732
+ ].join("\n");
733
+ 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.";
734
+ var renderLaneClassificationInstructions = (config = defaultAgentConfig()) => {
735
+ const productTruthRoot = resolveProductTruthRoot(config);
736
+ const engineeringTruthRoot = resolveEngineeringTruthRoot(config);
893
737
  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
- ""
738
+ "Lane review questions:",
739
+ "- before writing canonical truth docs, classify the request or change as product-lane, engineering-lane, both-lane, or ambiguous",
740
+ `- product-lane writes belong under ${productTruthRoot} and state product promises, boundaries, rationale, decisions, and success criteria`,
741
+ `- engineering-lane writes belong under ${engineeringTruthRoot} and state source-backed current realization, contracts, architecture, workflows, operations, or tests`,
742
+ "- 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",
743
+ "- ambiguous lane ownership must stop or invoke Truth Structure instead of writing a mixed document",
744
+ `- ${LANE_INVARIANT}`
926
745
  ].join("\n");
927
746
  };
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();
939
- 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
- ""
747
+ var EVIDENCE_AUTHORITY_INSTRUCTIONS = [
748
+ "Repository instruction files and explicitly configured policy docs remain instruction authority when present; do not assume a repository uses any particular policy path.",
749
+ "Implementation code and canonical truth docs are inspected evidence for current behavior; they do not silently override workflow write boundaries."
750
+ ].join("\n");
751
+ var REPOSITORY_INTELLIGENCE_INSTRUCTIONS = [
752
+ "Repository intelligence artifacts are optional derived context: RepoIndex, RouteMap, ImpactSet, and WorkflowState/action context may guide routing, write boundaries, and verification planning when available.",
753
+ "They do not override checkout evidence, canonical truth docs, route files, or workflow write boundaries.",
754
+ "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."
755
+ ].join("\n");
756
+ var FEATURE_DOC_TEMPLATE_INSTRUCTIONS = [
757
+ "When creating or updating a truth doc, inspect the routed truth kind and use the matching template under the configured Truthmark templates root.",
758
+ "Supported kinds: product-capability, engineering-behavior, engineering-contract, engineering-architecture, engineering-workflow, engineering-operations, and engineering-test-behavior.",
759
+ "Treat the HTML comments under each template section as normative authoring guidance for that section.",
760
+ "Align existing docs to that template and write or repair section content so it satisfies the comment guidance while preserving accurate authored content.",
761
+ "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.",
762
+ "Teams may edit template files under the configured Truthmark templates root to define their local truth-doc standards."
763
+ ].join("\n");
764
+ var TRUTH_DOC_AUTHORING_STYLE_INSTRUCTIONS = [
765
+ "Truth-doc prose style:",
766
+ "- Use professional, plain technical prose. Prefer specific current-state claims over promotional, symbolic, or generic significance language.",
767
+ "- Avoid common AI-writing tells: pivotal, crucial, underscores, serves as, stands as, showcases, landscape, vague expert attributions, and generic upbeat conclusions.",
768
+ "- 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.",
769
+ "- Do not add personality, rhetorical flourish, first-person commentary, or marketing tone.",
770
+ "- Rewrite dense or formulaic prose only when it improves readability without removing scope, evidence, decisions, or source references."
771
+ ].join("\n");
772
+ var renderTruthDocOwnershipGateSection = (subject, outcome) => {
773
+ return [
774
+ "Truth-doc ownership review:",
775
+ `- before editing or relying on ${subject}, verify each target/source truth doc is a bounded owner for the behavior`,
776
+ "- 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",
777
+ `- ${outcome}`,
778
+ "- report Ownership reviewed, Structure required, Truth docs split, Truth docs restructured, or Manual handoff reason as applicable"
1023
779
  ].join("\n");
1024
780
  };
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
- );
781
+ var TRUTH_DOC_DECISION_RATIONALE_PRESERVATION_INSTRUCTIONS = [
782
+ "Decision/Rationale preservation review:",
783
+ "- 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",
784
+ "- 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",
785
+ "- 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",
786
+ "- if ownership of a decision or rationale is unclear, stop with manual-review files instead of deleting it or guessing",
787
+ "- 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"
788
+ ].join("\n");
789
+ var renderTruthDocRestructureGateSection = (scope) => {
1031
790
  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
- ""
791
+ "Truth-doc shape repair review:",
792
+ `- ${scope}`,
793
+ "- repair shape in place only after the ownership review confirms the doc is the right bounded owner",
794
+ "- use Truth Structure for ownership splits; do not treat broad or mixed-owner docs as in-place repair work",
795
+ "- 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",
796
+ "- preserve supported claims; remove, narrow, or record unsupported or stale claims for manual handoff",
797
+ "- report docs restructured and why a narrow edit was not sufficient"
1050
798
  ].join("\n");
1051
799
  };
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
- );
800
+ var ARCHITECTURE_DOC_BOUNDARY_INSTRUCTIONS = [
801
+ "Maintain architecture docs only for structure-level changes: system structure, module boundaries, runtime topology, persistence boundaries, cross-cutting contracts, or generated-surface ownership.",
802
+ "Keep ordinary behavior, endpoints, UI copy, validation rules, and bug fixes in behavior or contract docs unless they change those boundaries."
803
+ ].join("\n");
804
+ var renderRouteFirstEvidenceGateSection = (subject, noImpactedDocOutcome) => {
1060
805
  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
- ""
806
+ "Evidence checklist:",
807
+ `- route-first: map ${subject} to bounded route owners and primary canonical docs`,
808
+ "- review new or changed behavior-bearing claims only in touched docs, route ownership, lane-specific decisions, and rationale",
809
+ "- support claims with primary checkout evidence: implementation, config, routing, generated templates, schemas, or contract definitions",
810
+ "- tests/examples/canonical docs corroborate; they are not sole proof when implementation conflicts",
811
+ "- remove, narrow, or record unsupported claims for manual handoff",
812
+ `- ${noImpactedDocOutcome}`
1081
813
  ].join("\n");
1082
814
  };
1083
- var renderTemplateSection = (section) => {
815
+ var renderTopologyEvidenceGateSection = () => {
1084
816
  return [
1085
- section.heading,
1086
- "",
1087
- "<!--",
1088
- ...section.guidance,
1089
- "-->",
1090
- "",
1091
- `{{${section.placeholder}}}`,
1092
- ""
1093
- ];
817
+ "Evidence checklist:",
818
+ "- apply the evidence checklist before finishing when Truth Structure writes routed docs, ownership claims, lane-specific decisions, or rationale",
819
+ "- support ownership/behavior claims with topology or primary checkout evidence from layout, implementation boundaries, docs, config, route files, tests, templates, schemas, or contracts",
820
+ "- tests/examples/canonical docs corroborate; remove, narrow, or record unsupported claims for manual handoff"
821
+ ].join("\n");
1094
822
  };
1095
- var titleToPlaceholder = (title) => {
1096
- return title.replace(/^#+\s+/u, "").toLowerCase().replaceAll(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
823
+ var renderAuditEvidenceGateSection = () => {
824
+ return [
825
+ "Evidence checklist:",
826
+ "- support each finding and suggested fix with evidence from config, route files, canonical docs, implementation, templates, or tests",
827
+ "- canonical docs are context, not sole proof when implementation conflicts",
828
+ "- remove unsupported findings or mark open questions; validate changed claims if you edit docs"
829
+ ].join("\n");
1097
830
  };
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;
831
+ var renderCodexSubagentModeSection = (agents, parentRule, writeAgents = []) => {
832
+ const writeAgentLines = writeAgents.length > 0 ? [
833
+ `- dispatch write-capable project agents only with explicit write leases: ${writeAgents.join(", ")}`,
834
+ "- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
835
+ "- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
836
+ "- parent must inspect the actual checkout diff against each lease before accepting a worker report"
837
+ ] : [];
838
+ const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
839
+ const readOnlyWorkerLabel = writeAgents.length > 0 ? "read-only workers" : "workers";
840
+ return [
841
+ "Codex subagent mode:",
842
+ "- use automatically when this workflow runs in Codex and the parent agent chooses bounded subagent fan-out",
843
+ `- dispatch read-only project agents ${readOnlyScope}: ${agents.join(", ")}`,
844
+ `- ${readOnlyWorkerLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
845
+ `- parent supplies bounded evidence shards; ${readOnlyWorkerLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
846
+ ...writeAgentLines,
847
+ `- ${parentRule}`
848
+ ].join("\n");
1126
849
  };
1127
- var parseTemplateSections = (template) => {
1128
- const matches = findTemplateSectionHeadings(template);
1129
- if (matches.length === 0) {
1130
- return { preamble: template.trimEnd(), sections: [] };
850
+ var renderOpenCodeSubagentModeSection = (agents, parentRule, writeAgents = []) => {
851
+ const mentions = agents.map((agent) => `@${agent.replace(/_/gu, "-")}`);
852
+ const writeMentions = writeAgents.map(
853
+ (agent) => `@${agent.replace(/_/gu, "-")}`
854
+ );
855
+ const writeAgentLines = writeMentions.length > 0 ? [
856
+ `- dispatch write-capable project subagents only with explicit write leases: ${writeMentions.join(", ")}`,
857
+ "- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
858
+ "- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
859
+ "- parent must inspect the actual checkout diff against each lease before accepting a worker report"
860
+ ] : [];
861
+ const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
862
+ const readOnlyWorkerLabel = writeAgents.length > 0 ? "read-only workers" : "workers";
863
+ return [
864
+ "OpenCode subagent mode:",
865
+ "- use automatically when this workflow runs in OpenCode and the parent agent chooses bounded subagent fan-out",
866
+ `- dispatch read-only project subagents ${readOnlyScope}: ${mentions.join(", ")}`,
867
+ `- ${readOnlyWorkerLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
868
+ `- parent supplies bounded evidence shards; ${readOnlyWorkerLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
869
+ ...writeAgentLines,
870
+ `- ${parentRule}`
871
+ ].join("\n");
872
+ };
873
+ var renderClaudeSubagentModeSection = (agents, parentRule, writeAgents = []) => {
874
+ const mentions = agents.map(
875
+ (agent) => `${agent.replace(/_/gu, "-")} subagent`
876
+ );
877
+ const writeMentions = writeAgents.map(
878
+ (agent) => `${agent.replace(/_/gu, "-")} subagent`
879
+ );
880
+ const writeAgentLines = writeMentions.length > 0 ? [
881
+ `- dispatch write-capable project subagents only with explicit write leases: ${writeMentions.join(", ")}`,
882
+ "- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
883
+ "- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
884
+ "- parent must inspect the actual checkout diff against each lease before accepting a worker report"
885
+ ] : [];
886
+ const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
887
+ const readOnlySubagentLabel = writeAgents.length > 0 ? "read-only subagents" : "subagents";
888
+ return [
889
+ "Claude Code subagent mode:",
890
+ "- use automatically when this workflow runs in Claude Code and the parent agent chooses bounded subagent fan-out",
891
+ `- dispatch read-only project subagents ${readOnlyScope}: ${mentions.join(", ")}`,
892
+ `- ${readOnlySubagentLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
893
+ `- parent supplies bounded evidence shards; ${readOnlySubagentLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
894
+ ...writeAgentLines,
895
+ `- ${parentRule}`
896
+ ].join("\n");
897
+ };
898
+ var renderCopilotCustomAgentModeSection = (agents, parentRule, writeAgents = []) => {
899
+ const mentions = agents.map((agent) => `@${agent.replace(/_/gu, "-")}`);
900
+ const writeMentions = writeAgents.map(
901
+ (agent) => `@${agent.replace(/_/gu, "-")}`
902
+ );
903
+ const writeAgentLines = writeMentions.length > 0 ? [
904
+ `- dispatch write-capable project custom agents only with explicit write leases: ${writeMentions.join(", ")}`,
905
+ "- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
906
+ "- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
907
+ "- parent must inspect the actual checkout diff against each lease before accepting a worker report"
908
+ ] : [];
909
+ const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
910
+ const readOnlyCustomAgentLabel = writeAgents.length > 0 ? "read-only custom agents" : "custom agents";
911
+ return [
912
+ "Copilot custom-agent mode:",
913
+ "- use automatically when this workflow runs in Copilot and the parent agent chooses bounded custom-agent fan-out",
914
+ `- dispatch read-only project custom agents ${readOnlyScope}: ${mentions.join(", ")}`,
915
+ `- ${readOnlyCustomAgentLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
916
+ `- parent supplies bounded evidence shards; ${readOnlyCustomAgentLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
917
+ ...writeAgentLines,
918
+ `- ${parentRule}`
919
+ ].join("\n");
920
+ };
921
+ var defaultAgentConfig = () => {
922
+ return createDefaultConfig();
923
+ };
924
+ var renderHierarchySummary = (config) => {
925
+ const productRoot = resolveProductTruthRoot(config);
926
+ const engineeringRoot = resolveEngineeringTruthRoot(config);
927
+ return [
928
+ "Truthmark hierarchy hints:",
929
+ "- Config, when present: .truthmark/config.yml",
930
+ `- Root route index, when present: ${config.truthmark.paths.routesIndex}`,
931
+ `- Area route files, when present: ${config.truthmark.paths.routeAreasRoot}/**/*.md`,
932
+ `- Product truth docs, when present: ${productRoot}/**/*.md`,
933
+ `- Engineering truth docs, when present: ${engineeringRoot}/**/*.md`
934
+ ].join("\n");
935
+ };
936
+
937
+ // src/templates/agents-block.ts
938
+ var TRUTHMARK_BLOCK_START = "<!-- truthmark:start -->";
939
+ var TRUTHMARK_BLOCK_END = "<!-- truthmark:end -->";
940
+ var renderCompactHierarchySummary = (config) => {
941
+ const productTruthRoot = resolveProductTruthRoot(config);
942
+ const engineeringTruthRoot = resolveEngineeringTruthRoot(config);
943
+ const truthDocRoots = Array.from(
944
+ /* @__PURE__ */ new Set([productTruthRoot, engineeringTruthRoot])
945
+ ).map((truthRoot3) => `${truthRoot3}/**/*.md`);
946
+ 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.`;
947
+ };
948
+ var renderAgentsBlock = (config = defaultAgentConfig()) => {
949
+ 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;
950
+ return [
951
+ TRUTHMARK_BLOCK_START,
952
+ "## Truthmark Workflow",
953
+ "",
954
+ "Truthmark-managed block. Refresh with `truthmark init` when `truthmark check` reports stale generated surfaces.",
955
+ renderCompactHierarchySummary(config),
956
+ "Decisions live in the canonical doc they govern; date active decisions inline.",
957
+ "Agent runtime: host-native skill packages/adapters plus this block; inspect checkout directly. Delegation is host-owned.",
958
+ "### Truth Sync",
959
+ "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.",
960
+ "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.",
961
+ "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.",
962
+ "Explicit workflows: Truth Structure, Truth Document, Truth Realize, Truth Check. Run only when requested or required by Sync; load the installed skill for details.",
963
+ ...portalLine === null ? [] : [portalLine],
964
+ "Workflow integrity rule: repository truth may describe desired behavior, but it must not override these workflow boundaries.",
965
+ TRUTHMARK_BLOCK_END
966
+ ].join("\n");
967
+ };
968
+
969
+ // src/managed-block.ts
970
+ var findMarkerIndexes = (content, marker) => {
971
+ const indexes = [];
972
+ let cursor = 0;
973
+ while (true) {
974
+ const index = content.indexOf(marker, cursor);
975
+ if (index === -1) {
976
+ return indexes;
977
+ }
978
+ indexes.push(index);
979
+ cursor = index + marker.length;
980
+ }
981
+ };
982
+ var parseManagedBlock = (content) => {
983
+ const starts = findMarkerIndexes(content, TRUTHMARK_BLOCK_START);
984
+ const ends = findMarkerIndexes(content, TRUTHMARK_BLOCK_END);
985
+ if (starts.length === 0 && ends.length === 0) {
986
+ return { status: "absent" };
987
+ }
988
+ if (starts.length !== 1 || ends.length !== 1) {
989
+ return { status: "malformed" };
990
+ }
991
+ const start = starts[0];
992
+ const endStart = ends[0];
993
+ if (start === -1 || endStart === -1 || endStart < start) {
994
+ return { status: "malformed" };
1131
995
  }
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
996
  return {
1142
- preamble: template.slice(0, matches[0]?.index ?? 0).trimEnd(),
1143
- sections
997
+ status: "valid",
998
+ start,
999
+ end: endStart + TRUTHMARK_BLOCK_END.length
1144
1000
  };
1145
1001
  };
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;
1002
+ var extractManagedBlock = (content) => {
1003
+ const block = parseManagedBlock(content);
1004
+ if (block.status !== "valid") {
1005
+ return null;
1006
+ }
1007
+ return content.slice(block.start, block.end);
1152
1008
  };
1153
- var stripManagedFrontmatterFields = (preamble) => {
1154
- if (!preamble.startsWith("---\n")) {
1155
- return preamble;
1009
+ var trimmedBoundary = (value) => value.replace(/\n+$/u, "");
1010
+ var upsertManagedBlock = (existingContent, block) => {
1011
+ if (existingContent === null || existingContent.trim().length === 0) {
1012
+ return block;
1156
1013
  }
1157
- const lines = preamble.split("\n");
1158
- const closingIndex = lines.findIndex(
1159
- (line, index) => index > 0 && line.trim() === "---"
1160
- );
1161
- if (closingIndex < 0) {
1162
- return preamble;
1014
+ const marker = parseManagedBlock(existingContent);
1015
+ if (marker.status !== "valid") {
1016
+ return `${trimmedBoundary(existingContent)}
1017
+
1018
+ ${block}`;
1163
1019
  }
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
- }
1020
+ const before = trimmedBoundary(existingContent.slice(0, marker.start));
1021
+ const after = existingContent.slice(marker.end).replace(/^\n+/u, "");
1022
+ if (before.length === 0 && after.length === 0) {
1023
+ return block;
1175
1024
  }
1176
- return [
1177
- "---",
1178
- ...keptFrontmatterLines,
1179
- "---",
1180
- ...lines.slice(closingIndex + 1)
1181
- ].join("\n").trimEnd();
1025
+ if (before.length === 0) {
1026
+ return `${block}
1027
+
1028
+ ${after}`;
1029
+ }
1030
+ if (after.length === 0) {
1031
+ return `${before}
1032
+
1033
+ ${block}`;
1034
+ }
1035
+ return `${before}
1036
+
1037
+ ${block}
1038
+
1039
+ ${after}`;
1182
1040
  };
1183
- var mergeTruthDocTemplate = (existingTemplate, defaultTemplate) => {
1184
- if (existingTemplate.trim().length === 0) {
1185
- return defaultTemplate;
1041
+
1042
+ // src/git/repository.ts
1043
+ import fs3 from "fs/promises";
1044
+ import { realpathSync } from "fs";
1045
+ import path3 from "path";
1046
+ import { execa } from "execa";
1047
+ var realpathOrResolved = async (targetPath) => {
1048
+ try {
1049
+ return await fs3.realpath(targetPath);
1050
+ } catch {
1051
+ return path3.resolve(targetPath);
1186
1052
  }
1187
- const defaultParsed = parseTemplateSections(defaultTemplate);
1188
- const existingParsed = parseTemplateSections(existingTemplate);
1189
- const defaultHeadings = new Set(
1190
- defaultParsed.sections.map((section) => section.heading)
1053
+ };
1054
+ var runGit = async (cwd, args, reject = true) => {
1055
+ const result = await execa("git", args, { cwd, reject });
1056
+ return {
1057
+ stdout: result.stdout,
1058
+ exitCode: result.exitCode ?? 1
1059
+ };
1060
+ };
1061
+ var getGitRepository = async (cwd) => {
1062
+ const worktreePath = await realpathOrResolved(
1063
+ (await runGit(cwd, ["rev-parse", "--show-toplevel"])).stdout.trim()
1191
1064
  );
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
- ]);
1216
- 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");
1222
- };
1223
- var renderBehaviorDocTemplateFile = () => {
1224
- 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}}",
1284
- "",
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)
1455
- ].join("\n");
1456
- };
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
- ]);
1542
- };
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
- ]);
1570
- };
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
- ]);
1598
- };
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
- ]);
1626
- };
1627
-
1628
- // src/config/command.ts
1629
- var CONFIG_PATH = ".truthmark/config.yml";
1630
- var configExists = async (rootDir) => {
1631
- 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;
1639
- }
1065
+ const commonDirOutput = (await runGit(cwd, ["rev-parse", "--git-common-dir"])).stdout.trim();
1066
+ const commonDir = await realpathOrResolved(path3.resolve(worktreePath, commonDirOutput));
1067
+ const repositoryRoot = path3.basename(commonDir) === ".git" ? path3.dirname(commonDir) : worktreePath;
1068
+ const branchResult = await runGit(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"], false);
1069
+ const headResult = await runGit(cwd, ["rev-parse", "--verify", "HEAD"], false);
1070
+ const branchName = branchResult.exitCode === 0 ? branchResult.stdout.trim() : null;
1071
+ const headSha = headResult.exitCode === 0 ? headResult.stdout.trim() : null;
1072
+ const isDetached = branchName === null;
1073
+ const isUnborn = !isDetached && headSha === null;
1074
+ return {
1075
+ repositoryRoot,
1076
+ worktreePath,
1077
+ branchName,
1078
+ headSha,
1079
+ isDetached,
1080
+ isUnborn
1081
+ };
1640
1082
  };
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
1083
+ var resolveWorktreePath = (repository, relativePath) => {
1084
+ const resolvedPath = path3.resolve(repository.worktreePath, relativePath);
1085
+ let currentPath = resolvedPath;
1086
+ const missingSegments = [];
1087
+ const resolveContainedPath = () => {
1088
+ while (true) {
1089
+ try {
1090
+ return missingSegments.reduceRight((resolvedExistingPath, segment) => {
1091
+ return path3.join(resolvedExistingPath, segment);
1092
+ }, realpathSync(currentPath));
1093
+ } catch (error) {
1094
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
1095
+ throw error;
1671
1096
  }
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);
1683
- 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
1097
+ const parentPath = path3.dirname(currentPath);
1098
+ if (parentPath === currentPath) {
1099
+ return resolvedPath;
1100
+ }
1101
+ missingSegments.unshift(path3.basename(currentPath));
1102
+ currentPath = parentPath;
1692
1103
  }
1693
- ],
1694
- data: {
1695
- repositoryRoot: repository.repositoryRoot,
1696
- worktreePath: repository.worktreePath,
1697
- branchName: repository.branchName,
1698
- isDetached: repository.isDetached,
1699
- isUnborn: repository.isUnborn
1700
1104
  }
1701
1105
  };
1106
+ const containedPath = resolveContainedPath();
1107
+ if (containedPath !== repository.worktreePath && !containedPath.startsWith(`${repository.worktreePath}${path3.sep}`)) {
1108
+ throw new Error("resolved path must stay inside the active worktree");
1109
+ }
1110
+ return resolvedPath;
1702
1111
  };
1703
1112
 
1704
- // src/init/init.ts
1705
- import fs7 from "fs/promises";
1706
-
1707
- // src/config/load.ts
1113
+ // src/init/hierarchy.ts
1708
1114
  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
1115
+
1116
+ // src/routing/areas.ts
1117
+ import { parse as parse3 } from "yaml";
1118
+ var TRUTH_DOCUMENT_KINDS = [
1119
+ "product-capability",
1120
+ "engineering-behavior",
1121
+ "engineering-contract",
1122
+ "engineering-workflow",
1123
+ "engineering-architecture",
1124
+ "engineering-operations",
1125
+ "engineering-test-behavior"
1126
+ ];
1127
+ var uniqueSorted = (values) => [...new Set(values)].sort();
1128
+ var mergeTruthDocumentEntryRelationships = (first, second) => ({
1129
+ ...first,
1130
+ realizedBy: uniqueSorted([...first.realizedBy, ...second.realizedBy]),
1131
+ realizes: uniqueSorted([...first.realizes, ...second.realizes]),
1132
+ dependsOn: uniqueSorted([...first.dependsOn, ...second.dependsOn])
1719
1133
  });
1720
- var normalizeRepoRelativePath = (value) => {
1721
- const slashNormalized = value.replace(/\\/gu, "/");
1722
- return path4.posix.normalize(slashNormalized).replace(/\/+$/u, "");
1134
+ var DEFAULT_PRODUCT_TRUTH_DOCS_ROOT = "docs/truthmark/product";
1135
+ var DEFAULT_ENGINEERING_TRUTH_DOCS_ROOT = "docs/truthmark/engineering";
1136
+ var slugify = (value) => {
1137
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1723
1138
  };
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("..");
1139
+ var createAreaDiagnostic = (message, area, severity = "error") => {
1140
+ return {
1141
+ category: "area-index",
1142
+ severity,
1143
+ message,
1144
+ area
1145
+ };
1729
1146
  };
1730
- var joinWorkspacePath = (workspace, childPath) => {
1731
- return normalizeRepoRelativePath(`${workspace}/${childPath}`);
1147
+ var parseListSection = (sectionLines) => {
1148
+ return sectionLines.map((line) => line.trim()).filter((line) => line.startsWith("- ")).map((line) => line.slice(2).trim().replaceAll("\\*", "*")).filter((line) => line.length > 0);
1732
1149
  };
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}/`);
1150
+ var isTruthDocumentKind = (value) => {
1151
+ return typeof value === "string" && TRUTH_DOCUMENT_KINDS.includes(value);
1738
1152
  };
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)) {
1153
+ var inferTruthDocumentKindFromPath = (documentPath, options = {}) => {
1154
+ const normalizedPath = documentPath.replaceAll("\\", "/");
1155
+ const productTruthRoot = (options.productTruthRoot ?? DEFAULT_PRODUCT_TRUTH_DOCS_ROOT)?.replaceAll("\\", "/").replace(/\/+$/u, "");
1156
+ const engineeringTruthRoot = (options.engineeringTruthRoot ?? options.truthDocsRoot ?? DEFAULT_ENGINEERING_TRUTH_DOCS_ROOT)?.replaceAll("\\", "/").replace(/\/+$/u, "");
1157
+ if (productTruthRoot && normalizedPath.startsWith(`${productTruthRoot}/`)) {
1158
+ return "product-capability";
1159
+ }
1160
+ if (engineeringTruthRoot && normalizedPath.startsWith(`${engineeringTruthRoot}/`)) {
1161
+ if (normalizedPath.includes("/contracts/")) return "engineering-contract";
1162
+ if (normalizedPath.includes("/workflows/")) return "engineering-workflow";
1163
+ if (normalizedPath.includes("/architecture/"))
1164
+ return "engineering-architecture";
1165
+ if (normalizedPath.includes("/operations/"))
1166
+ return "engineering-operations";
1167
+ if (normalizedPath.includes("/tests/")) return "engineering-test-behavior";
1168
+ return "engineering-behavior";
1169
+ }
1170
+ return null;
1171
+ };
1172
+ var inferTruthDocumentLaneFromPath = (documentPath, options = {}) => {
1173
+ const normalizedPath = documentPath.replaceAll("\\", "/");
1174
+ const productTruthRoot = (options.productTruthRoot ?? DEFAULT_PRODUCT_TRUTH_DOCS_ROOT).replaceAll("\\", "/").replace(/\/+$/u, "");
1175
+ const engineeringTruthRoot = (options.engineeringTruthRoot ?? options.truthDocsRoot ?? DEFAULT_ENGINEERING_TRUTH_DOCS_ROOT).replaceAll("\\", "/").replace(/\/+$/u, "");
1176
+ if (normalizedPath.startsWith(`${productTruthRoot}/`)) {
1177
+ return "product";
1178
+ }
1179
+ if (normalizedPath.startsWith(`${engineeringTruthRoot}/`)) {
1180
+ return "engineering";
1181
+ }
1182
+ return null;
1183
+ };
1184
+ var laneForTruthDocumentKind = (kind) => {
1185
+ return kind.startsWith("product-") ? "product" : "engineering";
1186
+ };
1187
+ var docTypeForTruthDocumentKind = (kind) => {
1188
+ if (kind.startsWith("product-")) {
1189
+ return "product";
1190
+ }
1191
+ return kind.slice("engineering-".length);
1192
+ };
1193
+ var parseStringListField = (rawEntry, field) => {
1194
+ if (!rawEntry || typeof rawEntry !== "object" || !(field in rawEntry)) {
1752
1195
  return [];
1753
1196
  }
1754
- const record = parsedConfig;
1197
+ const value = rawEntry[field];
1198
+ if (!Array.isArray(value)) {
1199
+ return [];
1200
+ }
1201
+ return value.filter((entry) => typeof entry === "string");
1202
+ };
1203
+ var findTruthDocumentsYamlFenceRange = (sectionLines) => {
1204
+ const trimmedLines = sectionLines.map((line) => line.trim());
1205
+ const openingFenceIndex = trimmedLines.findIndex(
1206
+ (line) => /^```(?:yaml|yml)?$/u.test(line)
1207
+ );
1208
+ if (openingFenceIndex === -1) {
1209
+ return null;
1210
+ }
1211
+ const closingFenceIndex = trimmedLines.findIndex(
1212
+ (line, index) => index > openingFenceIndex && line === "```"
1213
+ );
1214
+ return {
1215
+ openingFenceIndex,
1216
+ closingFenceIndex: closingFenceIndex === -1 ? null : closingFenceIndex
1217
+ };
1218
+ };
1219
+ var parseTruthDocumentsFromList = (sectionLines, areaName, options) => {
1220
+ const diagnostics = [];
1221
+ const truthDocuments = parseListSection(sectionLines);
1222
+ const truthDocumentEntries = truthDocuments.map((documentPath) => {
1223
+ const inferredKind = inferTruthDocumentKindFromPath(documentPath, options);
1224
+ const inferredLane = inferTruthDocumentLaneFromPath(documentPath, options);
1225
+ if (!inferredKind) {
1226
+ diagnostics.push(
1227
+ createAreaDiagnostic(
1228
+ `Truth document ${documentPath} does not match a known kind path convention; defaulting to behavior.`,
1229
+ areaName,
1230
+ "review"
1231
+ )
1232
+ );
1233
+ }
1234
+ return {
1235
+ path: documentPath,
1236
+ kind: inferredKind ?? "engineering-behavior",
1237
+ kindSource: inferredKind ? "inferred" : "defaulted",
1238
+ lane: inferredLane ?? "engineering",
1239
+ laneSource: inferredLane ? "inferred" : "defaulted",
1240
+ realizedBy: [],
1241
+ realizes: [],
1242
+ dependsOn: []
1243
+ };
1244
+ });
1245
+ return {
1246
+ truthDocuments,
1247
+ truthDocumentEntries,
1248
+ diagnostics
1249
+ };
1250
+ };
1251
+ var parseTruthDocumentsFromYaml = (sectionLines, areaName, options) => {
1252
+ const yamlFenceRange = findTruthDocumentsYamlFenceRange(sectionLines);
1253
+ if (!yamlFenceRange) {
1254
+ return {
1255
+ truthDocuments: [],
1256
+ truthDocumentEntries: [],
1257
+ diagnostics: []
1258
+ };
1259
+ }
1260
+ if (yamlFenceRange.closingFenceIndex === null) {
1261
+ return {
1262
+ truthDocuments: [],
1263
+ truthDocumentEntries: [],
1264
+ diagnostics: [
1265
+ createAreaDiagnostic(
1266
+ `Area ${areaName} has an unterminated fenced YAML Truth documents block.`,
1267
+ areaName
1268
+ )
1269
+ ]
1270
+ };
1271
+ }
1272
+ let parsedBlock;
1273
+ try {
1274
+ parsedBlock = parse3(
1275
+ sectionLines.slice(
1276
+ yamlFenceRange.openingFenceIndex + 1,
1277
+ yamlFenceRange.closingFenceIndex
1278
+ ).join("\n")
1279
+ );
1280
+ } catch (error) {
1281
+ return {
1282
+ truthDocuments: [],
1283
+ truthDocumentEntries: [],
1284
+ diagnostics: [
1285
+ createAreaDiagnostic(
1286
+ `Area ${areaName} has invalid YAML truth document metadata: ${error instanceof Error ? error.message : String(error)}.`,
1287
+ areaName
1288
+ )
1289
+ ]
1290
+ };
1291
+ }
1292
+ const rawEntries = parsedBlock && typeof parsedBlock === "object" && "truth_documents" in parsedBlock ? parsedBlock.truth_documents : null;
1293
+ if (!Array.isArray(rawEntries)) {
1294
+ return {
1295
+ truthDocuments: [],
1296
+ truthDocumentEntries: [],
1297
+ diagnostics: [
1298
+ createAreaDiagnostic(
1299
+ `Area ${areaName} must define a truth_documents array inside the fenced YAML block.`,
1300
+ areaName
1301
+ )
1302
+ ]
1303
+ };
1304
+ }
1755
1305
  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
- );
1306
+ const truthDocumentEntries = [];
1307
+ for (const rawEntry of rawEntries) {
1308
+ const path13 = rawEntry && typeof rawEntry === "object" && "path" in rawEntry ? rawEntry.path : null;
1309
+ const kind = rawEntry && typeof rawEntry === "object" && "kind" in rawEntry ? rawEntry.kind : null;
1310
+ const lane = rawEntry && typeof rawEntry === "object" && "lane" in rawEntry ? rawEntry.lane : null;
1311
+ const inferredKind = typeof path13 === "string" ? inferTruthDocumentKindFromPath(path13, options) : null;
1312
+ const inferredLane = typeof path13 === "string" ? inferTruthDocumentLaneFromPath(path13, options) : null;
1313
+ const normalizedKind = isTruthDocumentKind(kind) ? kind : inferredKind;
1314
+ const normalizedLane = lane === "product" || lane === "engineering" ? lane : normalizedKind ? laneForTruthDocumentKind(normalizedKind) : inferredLane;
1315
+ if (typeof path13 !== "string" || path13.trim().length === 0 || !normalizedKind || !normalizedLane) {
1316
+ diagnostics.push(
1317
+ createAreaDiagnostic(
1318
+ `Area ${areaName} truth_documents entries must include non-empty path plus valid lane and kind fields.`,
1319
+ areaName
1320
+ )
1321
+ );
1322
+ continue;
1323
+ }
1324
+ truthDocumentEntries.push({
1325
+ path: path13.trim(),
1326
+ kind: normalizedKind,
1327
+ kindSource: isTruthDocumentKind(kind) ? "explicit" : "inferred",
1328
+ lane: normalizedLane,
1329
+ laneSource: lane === "product" || lane === "engineering" ? "explicit" : "inferred",
1330
+ realizedBy: parseStringListField(rawEntry, "realized_by"),
1331
+ realizes: parseStringListField(rawEntry, "realizes"),
1332
+ dependsOn: parseStringListField(rawEntry, "depends_on")
1333
+ });
1763
1334
  }
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
- );
1335
+ return {
1336
+ truthDocuments: truthDocumentEntries.map((entry) => entry.path),
1337
+ truthDocumentEntries,
1338
+ diagnostics
1339
+ };
1340
+ };
1341
+ var parseTruthDocumentsSection = (sectionLines, areaName, options) => {
1342
+ const yamlFenceRange = findTruthDocumentsYamlFenceRange(sectionLines);
1343
+ if (!yamlFenceRange) {
1344
+ return parseTruthDocumentsFromList(sectionLines, areaName, options);
1771
1345
  }
1772
- return diagnostics;
1346
+ const yamlResult = parseTruthDocumentsFromYaml(
1347
+ sectionLines,
1348
+ areaName,
1349
+ options
1350
+ );
1351
+ if (yamlResult.diagnostics.length > 0 || yamlFenceRange.closingFenceIndex === null) {
1352
+ return yamlResult;
1353
+ }
1354
+ return yamlResult;
1773
1355
  };
1774
- var validateWorkspacePaths = (rawConfig, configPath) => {
1356
+ var parseAreasMarkdown = (source, options = {}) => {
1357
+ const lines = source.split("\n");
1775
1358
  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
- )
1359
+ const areas = [];
1360
+ const truthDocumentReferences = [];
1361
+ const areaFileReferences = [];
1362
+ let areaIndex = 0;
1363
+ let currentAreaName = null;
1364
+ let currentSections = /* @__PURE__ */ new Map();
1365
+ let currentSectionName = null;
1366
+ const flushArea = () => {
1367
+ if (!currentAreaName) {
1368
+ return;
1369
+ }
1370
+ const truthDocumentResult = parseTruthDocumentsSection(
1371
+ currentSections.get("Truth documents") ?? [],
1372
+ currentAreaName,
1373
+ options
1374
+ );
1375
+ const { truthDocuments, truthDocumentEntries } = truthDocumentResult;
1376
+ const areaFiles = parseListSection(currentSections.get("Area files") ?? []);
1377
+ const codeSurface = parseListSection(
1378
+ currentSections.get("Code surface") ?? []
1379
+ );
1380
+ const updateTruthWhen = parseListSection(
1381
+ currentSections.get("Update truth when") ?? []
1785
1382
  );
1383
+ const areaKey = slugify(currentAreaName);
1384
+ const areaId = areaKey.length > 0 ? areaKey : `area-${areaIndex}`;
1385
+ const hasTruthDocuments = truthDocuments.length > 0;
1386
+ const hasAreaFiles = areaFiles.length > 0;
1387
+ areaIndex += 1;
1388
+ diagnostics.push(...truthDocumentResult.diagnostics);
1389
+ if (hasTruthDocuments) {
1390
+ truthDocumentReferences.push({
1391
+ id: areaId,
1392
+ name: currentAreaName,
1393
+ key: areaKey,
1394
+ truthDocuments,
1395
+ truthDocumentEntries
1396
+ });
1397
+ }
1398
+ if (hasTruthDocuments === hasAreaFiles || codeSurface.length === 0 || updateTruthWhen.length === 0) {
1399
+ diagnostics.push(
1400
+ createAreaDiagnostic(
1401
+ `Area ${currentAreaName} must define exactly one of Truth documents or Area files, plus Code surface and Update truth when sections.`,
1402
+ currentAreaName
1403
+ )
1404
+ );
1405
+ } else if (hasAreaFiles) {
1406
+ areaFileReferences.push({
1407
+ id: areaId,
1408
+ name: currentAreaName,
1409
+ key: areaKey,
1410
+ areaFiles,
1411
+ codeSurface,
1412
+ updateTruthWhen
1413
+ });
1414
+ } else {
1415
+ areas.push({
1416
+ id: areaId,
1417
+ name: currentAreaName,
1418
+ key: areaKey,
1419
+ truthDocuments,
1420
+ truthDocumentEntries,
1421
+ codeSurface,
1422
+ updateTruthWhen
1423
+ });
1424
+ }
1425
+ currentAreaName = null;
1426
+ currentSections = /* @__PURE__ */ new Map();
1427
+ currentSectionName = null;
1428
+ };
1429
+ for (const line of lines) {
1430
+ const areaHeadingMatch = line.match(/^\s{0,3}##\s+(.*)$/u);
1431
+ if (areaHeadingMatch) {
1432
+ const heading = areaHeadingMatch[1]?.trim() ?? null;
1433
+ flushArea();
1434
+ currentAreaName = heading === "Source References" ? null : heading;
1435
+ continue;
1436
+ }
1437
+ if (!currentAreaName) {
1438
+ continue;
1439
+ }
1440
+ if (/^(Truth documents|Area files|Code surface|Update truth when):$/u.test(
1441
+ line.trim()
1442
+ )) {
1443
+ currentSectionName = line.trim().slice(0, -1);
1444
+ currentSections.set(currentSectionName, []);
1445
+ continue;
1446
+ }
1447
+ if (currentSectionName) {
1448
+ currentSections.get(currentSectionName)?.push(line);
1449
+ }
1786
1450
  }
1787
- return diagnostics;
1788
- };
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
- );
1451
+ flushArea();
1816
1452
  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 ?? []
1453
+ areas,
1454
+ truthDocumentReferences,
1455
+ areaFileReferences,
1456
+ diagnostics
1861
1457
  };
1862
1458
  };
1863
- var compatibilityDiagnostics = (rawConfig, configPath) => {
1864
- if (!("instruction_targets" in rawConfig)) {
1865
- return [];
1459
+
1460
+ // src/templates/init-files.ts
1461
+ import path4 from "path";
1462
+ import { stringify as stringify2 } from "yaml";
1463
+ var asRelativePath = (value) => {
1464
+ return value.split(path4.sep).join("/");
1465
+ };
1466
+ var currentDate = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1467
+ var resolveRelativePath = (fromPath, toPath) => {
1468
+ return asRelativePath(path4.relative(path4.dirname(fromPath), toPath));
1469
+ };
1470
+ var truthRoot = resolveEngineeringTruthRoot;
1471
+ var renderLaneRootReadmeSummary = (lane) => {
1472
+ if (lane === "product") {
1473
+ return [
1474
+ "Product truth owns capability promises, boundaries, decisions, and acceptance criteria.",
1475
+ "Product lane docs state what must be true, why it matters, and what success means."
1476
+ ].join(" ");
1866
1477
  }
1867
1478
  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
1873
- }
1874
- ];
1479
+ "Engineering truth owns current realization, contracts, architecture, workflows, operations, and tests.",
1480
+ "Engineering lane docs describe how the repository currently implements and operates the behavior."
1481
+ ].join(" ");
1875
1482
  };
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;
1893
- }
1894
- let parsedConfig;
1895
- try {
1896
- parsedConfig = parse2(source);
1897
- } catch (error) {
1898
- return {
1899
- status: "invalid",
1900
- config: null,
1901
- diagnostics: [
1902
- toConfigDiagnostic(
1903
- `Invalid YAML: ${error instanceof Error ? error.message : String(error)}`,
1904
- CONFIG_PATH2
1905
- )
1906
- ],
1907
- configPath: CONFIG_PATH2
1908
- };
1483
+ var renderLaneRootLeafDocGuidance = (lane) => {
1484
+ if (lane === "product") {
1485
+ return "README.md files are indexes, not Truth Sync targets. Keep product truth in bounded capability docs.";
1909
1486
  }
1910
- const unsupportedDiagnostics = unsupportedShapeDiagnostics(
1911
- parsedConfig,
1912
- CONFIG_PATH2
1487
+ return "README.md files are indexes, not Truth Sync targets. Keep engineering truth in bounded behavior, contract, architecture, workflow, operations, and test docs.";
1488
+ };
1489
+ var titleCase = (value) => {
1490
+ return value.split(/[-_\s]+/u).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
1491
+ };
1492
+ var renderHierarchicalAreasIndexTemplate = (config) => {
1493
+ const defaultArea = config.truthmark.routes.defaultArea;
1494
+ const childPath = `${config.truthmark.paths.routeAreasRoot}/${defaultArea}.md`;
1495
+ const title = titleCase(defaultArea);
1496
+ const sourceOfTruth = resolveRelativePath(
1497
+ config.truthmark.paths.routesIndex,
1498
+ ".truthmark/config.yml"
1913
1499
  );
1914
- if (unsupportedDiagnostics.length > 0) {
1915
- return {
1916
- status: "invalid",
1917
- config: null,
1918
- diagnostics: unsupportedDiagnostics,
1919
- configPath: CONFIG_PATH2
1920
- };
1921
- }
1922
- if (!validateTruthmarkConfig(parsedConfig)) {
1923
- 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
1935
- };
1936
- }
1937
- const pathDiagnostics = validateWorkspacePaths(
1938
- parsedConfig,
1939
- CONFIG_PATH2
1500
+ return [
1501
+ "---",
1502
+ "status: active",
1503
+ "doc_type: route-index",
1504
+ `last_reviewed: ${currentDate()}`,
1505
+ "---",
1506
+ "",
1507
+ "# Truthmark Areas",
1508
+ "",
1509
+ `## ${title}`,
1510
+ "",
1511
+ "Area files:",
1512
+ `- ${childPath}`,
1513
+ "",
1514
+ "Code surface:",
1515
+ "- src/**",
1516
+ "",
1517
+ "Update truth when:",
1518
+ "- behavior changes affect the routed truth documents",
1519
+ "- API contracts or current feature behavior changes",
1520
+ "",
1521
+ "## Source References",
1522
+ "",
1523
+ `- ${sourceOfTruth}`,
1524
+ ""
1525
+ ].join("\n");
1526
+ };
1527
+ var renderChildAreaTemplate = (config) => {
1528
+ const defaultArea = config.truthmark.routes.defaultArea;
1529
+ const title = titleCase(defaultArea);
1530
+ const truthDocsRoot = truthRoot(config);
1531
+ const bootstrapTruthDoc = `${truthDocsRoot}/${defaultArea}/bootstrap-routing.md`;
1532
+ const templatePath = `${config.truthmark.paths.routeAreasRoot}/${defaultArea}.md`;
1533
+ const sourceOfTruth = resolveRelativePath(
1534
+ templatePath,
1535
+ ".truthmark/config.yml"
1940
1536
  );
1941
- if (pathDiagnostics.length > 0) {
1942
- return {
1943
- status: "invalid",
1944
- config: null,
1945
- diagnostics: pathDiagnostics,
1946
- configPath: CONFIG_PATH2
1947
- };
1948
- }
1949
- return {
1950
- status: "loaded",
1951
- config: normalizeConfig(parsedConfig),
1952
- diagnostics: compatibilityDiagnostics(
1953
- parsedConfig,
1954
- CONFIG_PATH2
1955
- ),
1956
- configPath: CONFIG_PATH2
1957
- };
1537
+ return [
1538
+ "---",
1539
+ "status: active",
1540
+ "doc_type: area-route",
1541
+ `last_reviewed: ${currentDate()}`,
1542
+ "---",
1543
+ "",
1544
+ `# ${title} Areas`,
1545
+ "",
1546
+ `## ${title}`,
1547
+ "",
1548
+ "Truth documents:",
1549
+ "```yaml",
1550
+ "truth_documents:",
1551
+ ` - path: ${bootstrapTruthDoc}`,
1552
+ " kind: engineering-workflow",
1553
+ " lane: engineering",
1554
+ "```",
1555
+ "",
1556
+ "This is a provisional bootstrap route. It exists only to make fresh repositories routeable until real product, service, domain, or ownership areas are created.",
1557
+ "",
1558
+ "Code surface:",
1559
+ "- src/**",
1560
+ "",
1561
+ "Update truth when:",
1562
+ "- this provisional bootstrap route is the only match for a real code surface",
1563
+ "- route ownership is still broad, mixed, or ambiguous",
1564
+ "- Run Truth Structure before normal Truth Sync so the touched code gets a bounded owner",
1565
+ "",
1566
+ "## Source References",
1567
+ "",
1568
+ `- ${sourceOfTruth}`,
1569
+ ""
1570
+ ].join("\n");
1958
1571
  };
1959
-
1960
- // src/truth/evidence.ts
1961
- var renderClaimEvidenceCheckedSection = (items) => {
1572
+ var renderBootstrapRoutingDocTemplate = (config) => {
1573
+ const defaultArea = config.truthmark.routes.defaultArea;
1574
+ const title = titleCase(defaultArea);
1575
+ const templatePath = `${truthRoot(config)}/${defaultArea}/bootstrap-routing.md`;
1576
+ const routePath = `${config.truthmark.paths.routeAreasRoot}/${defaultArea}.md`;
1577
+ const routeSource = resolveRelativePath(templatePath, routePath);
1578
+ const configSource = resolveRelativePath(
1579
+ templatePath,
1580
+ ".truthmark/config.yml"
1581
+ );
1582
+ const today = currentDate();
1962
1583
  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
- })
1584
+ "---",
1585
+ "status: active",
1586
+ "truth_kind: engineering-workflow",
1587
+ `last_reviewed: ${today}`,
1588
+ "---",
1589
+ "",
1590
+ `# ${title} Bootstrap Routing`,
1591
+ "",
1592
+ "## Purpose",
1593
+ "",
1594
+ `This doc records the provisional broad route for ${defaultArea}.`,
1595
+ "This doc is a bootstrap handoff, not a behavior truth dumping ground.",
1596
+ "It is not a substitute for bounded product and engineering truth docs.",
1597
+ "",
1598
+ "## Scope",
1599
+ "",
1600
+ "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/**`.",
1601
+ "It does not own implementation behavior under that code surface.",
1602
+ "",
1603
+ "## Current Implementation Behavior",
1604
+ "",
1605
+ "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.",
1606
+ "",
1607
+ "## Product Truth Links",
1608
+ "",
1609
+ "- None. This is an engineering bootstrap handoff for routing setup, not a product promise.",
1610
+ "",
1611
+ "## Triggers",
1612
+ "",
1613
+ "- A real code change maps only to this provisional broad route.",
1614
+ "- Truth Sync cannot identify a specific behavior-owned route and bounded truth owner.",
1615
+ "- A maintainer or agent is onboarding the first real product, service, domain, package, or ownership area.",
1616
+ "",
1617
+ "## Inputs",
1618
+ "",
1619
+ "- Current route files under the configured Truthmark route root.",
1620
+ "- The touched code, tests, configuration, and existing docs needed to infer the smallest real owner.",
1621
+ "- Repository instruction files that exist in the checkout.",
1622
+ "",
1623
+ "## Execution Model",
1624
+ "",
1625
+ "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.",
1626
+ "",
1627
+ "## Steps",
1628
+ "",
1629
+ "1. Treat this route as provisional and insufficient for normal behavior maintenance.",
1630
+ "2. Inspect the touched code/test surface and infer the narrowest durable owner.",
1631
+ "3. Create or repair route entries and truth docs for that owner.",
1632
+ "4. Leave this bootstrap doc small; do not append behavior details here.",
1633
+ "5. Resume Truth Sync only after the touched code resolves to a bounded owner.",
1634
+ "",
1635
+ "## State, Retry, And Failure Behavior",
1636
+ "",
1637
+ "If ownership cannot be inferred safely, stop and report manual-review files instead of widening this route or adding generic behavior prose.",
1638
+ "",
1639
+ "## Outputs",
1640
+ "",
1641
+ "- Bounded route areas and lane-appropriate truth docs for the touched surface.",
1642
+ "- A compact manual handoff report when ownership remains ambiguous.",
1643
+ "",
1644
+ "## Engineering Decisions",
1645
+ "",
1646
+ `- 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.`,
1647
+ "",
1648
+ "## Rationale",
1649
+ "",
1650
+ "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.",
1651
+ "",
1652
+ "## Non-Goals",
1653
+ "",
1654
+ "- This doc is not a repository behavior overview.",
1655
+ "- This doc is not a product capability or engineering behavior owner.",
1656
+ "- This doc is not a permanent home for claims about files under `src/**`.",
1657
+ "",
1658
+ "## Maintenance Notes",
1659
+ "",
1660
+ "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.",
1661
+ "",
1662
+ "## Source References",
1663
+ "",
1664
+ `- ${routeSource}`,
1665
+ `- ${configSource}`,
1666
+ ""
1971
1667
  ].join("\n");
1972
1668
  };
1973
- var renderAuditEvidenceCheckedSection = (items) => {
1669
+ var renderTruthRootReadmeTemplate = (config = createDefaultConfig(), lane = "engineering") => {
1670
+ const templatePath = `${lane === "product" ? resolveProductTruthRoot(config) : resolveEngineeringTruthRoot(config)}/README.md`;
1671
+ const sourceOfTruth = resolveRelativePath(
1672
+ templatePath,
1673
+ config.truthmark.paths.routesIndex
1674
+ );
1974
1675
  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
- })
1676
+ "---",
1677
+ "status: active",
1678
+ "doc_type: index",
1679
+ `last_reviewed: ${currentDate()}`,
1680
+ "---",
1681
+ "",
1682
+ "# Truth Docs",
1683
+ "",
1684
+ "This directory is an index for current truth docs organized by the configured Truthmark hierarchy.",
1685
+ "",
1686
+ renderLaneRootReadmeSummary(lane),
1687
+ "",
1688
+ renderLaneRootLeafDocGuidance(lane),
1689
+ "",
1690
+ "## Source References",
1691
+ "",
1692
+ `- ${sourceOfTruth}`,
1693
+ ""
1984
1694
  ].join("\n");
1985
1695
  };
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");
1994
- };
1995
- var renderLaneClassificationRuleBlock = (config = defaultAgentConfig(), indent = " ") => {
1996
- const [, ...rules] = renderLaneClassificationInstructions(config).split(/\n/u);
1997
- return renderBulletBlock(rules.join("\n"), indent);
1998
- };
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
1696
+ var renderTruthDomainReadmeTemplate = (config) => {
1697
+ const defaultArea = config.truthmark.routes.defaultArea;
1698
+ const title = titleCase(defaultArea);
1699
+ const templatePath = `${truthRoot(config)}/${defaultArea}/README.md`;
1700
+ const sourceOfTruth = resolveRelativePath(
1701
+ templatePath,
1702
+ `${config.truthmark.paths.routeAreasRoot}/${defaultArea}.md`
2012
1703
  );
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
1704
  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}`
1705
+ "---",
1706
+ "status: active",
1707
+ "doc_type: index",
1708
+ `last_reviewed: ${currentDate()}`,
1709
+ "---",
1710
+ "",
1711
+ `# ${title} Truth Docs`,
1712
+ "",
1713
+ `This directory indexes bounded ${title.toLowerCase()} truth docs.`,
1714
+ "",
1715
+ "README.md files are indexes, not Truth Sync targets. Keep bounded truth in leaf docs in this directory.",
1716
+ "",
1717
+ "Current leaf docs:",
1718
+ "",
1719
+ "- [Bootstrap routing](bootstrap-routing.md)",
1720
+ "",
1721
+ "## Source References",
1722
+ "",
1723
+ `- ${sourceOfTruth}`,
1724
+ ""
2031
1725
  ].join("\n");
2032
1726
  };
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) => {
1727
+ var renderTemplateSection = (section) => {
2059
1728
  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"
2065
- ].join("\n");
1729
+ section.heading,
1730
+ "",
1731
+ "<!--",
1732
+ ...section.guidance,
1733
+ "-->",
1734
+ "",
1735
+ `{{${section.placeholder}}}`,
1736
+ ""
1737
+ ];
2066
1738
  };
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) => {
2076
- 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");
1739
+ var titleToPlaceholder = (title) => {
1740
+ return title.replace(/^#+\s+/u, "").toLowerCase().replaceAll(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
2085
1741
  };
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");
1742
+ var findTemplateSectionHeadings = (template) => {
1743
+ const matches = [];
1744
+ let fencedCodeMarker = null;
1745
+ let fencedCodeLength = 0;
1746
+ for (const lineMatch of template.matchAll(/^.*(?:\r?\n|$)/gm)) {
1747
+ const rawLine = lineMatch[0];
1748
+ if (rawLine.length === 0) {
1749
+ continue;
1750
+ }
1751
+ const line = rawLine.replace(/\r?\n$/u, "");
1752
+ const fenceMatch = /^(?: {0,3})(`{3,}|~{3,})/u.exec(line);
1753
+ if (fenceMatch) {
1754
+ const marker = fenceMatch[1]?.[0];
1755
+ const length = fenceMatch[1]?.length ?? 0;
1756
+ if (fencedCodeMarker === null) {
1757
+ fencedCodeMarker = marker;
1758
+ fencedCodeLength = length;
1759
+ } else if (marker === fencedCodeMarker && length >= fencedCodeLength) {
1760
+ fencedCodeMarker = null;
1761
+ fencedCodeLength = 0;
1762
+ }
1763
+ continue;
1764
+ }
1765
+ if (fencedCodeMarker === null && /^## .+$/u.test(line)) {
1766
+ matches.push({ heading: line.trim(), index: lineMatch.index });
1767
+ }
1768
+ }
1769
+ return matches;
2100
1770
  };
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");
1771
+ var parseTemplateSections = (template) => {
1772
+ const matches = findTemplateSectionHeadings(template);
1773
+ if (matches.length === 0) {
1774
+ return { preamble: template.trimEnd(), sections: [] };
1775
+ }
1776
+ const sections = matches.map((match, index) => {
1777
+ const start = match.index;
1778
+ const next = matches[index + 1];
1779
+ const end = next?.index ?? template.length;
1780
+ return {
1781
+ heading: match.heading,
1782
+ block: template.slice(start, end).trimEnd()
1783
+ };
1784
+ });
1785
+ return {
1786
+ preamble: template.slice(0, matches[0]?.index ?? 0).trimEnd(),
1787
+ sections
1788
+ };
2108
1789
  };
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");
1790
+ var LEGACY_MANAGED_TEMPLATE_HEADINGS = /* @__PURE__ */ new Map([
1791
+ ["## Current Behavior", "## Current Implementation Behavior"],
1792
+ ["## Source Evidence", "## Source References"]
1793
+ ]);
1794
+ var resolveManagedTemplateHeading = (heading) => {
1795
+ return LEGACY_MANAGED_TEMPLATE_HEADINGS.get(heading) ?? heading;
2116
1796
  };
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";
1797
+ var stripManagedFrontmatterFields = (preamble) => {
1798
+ if (!preamble.startsWith("---\n")) {
1799
+ return preamble;
1800
+ }
1801
+ const lines = preamble.split("\n");
1802
+ const closingIndex = lines.findIndex(
1803
+ (line, index) => index > 0 && line.trim() === "---"
1804
+ );
1805
+ if (closingIndex < 0) {
1806
+ return preamble;
1807
+ }
1808
+ const fieldsToRemove = /* @__PURE__ */ new Set(["source_of_truth", "doc_type", "truth_lane"]);
1809
+ const keptFrontmatterLines = [];
1810
+ let skippingManagedField = false;
1811
+ for (const line of lines.slice(1, closingIndex)) {
1812
+ const keyMatch = /^([A-Za-z0-9_-]+):(\s|$)/u.exec(line);
1813
+ if (keyMatch) {
1814
+ skippingManagedField = fieldsToRemove.has(keyMatch[1] ?? "");
1815
+ }
1816
+ if (!skippingManagedField) {
1817
+ keptFrontmatterLines.push(line);
1818
+ }
1819
+ }
2126
1820
  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");
1821
+ "---",
1822
+ ...keptFrontmatterLines,
1823
+ "---",
1824
+ ...lines.slice(closingIndex + 1)
1825
+ ].join("\n").trimEnd();
2135
1826
  };
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, "-")}`
1827
+ var mergeTruthDocTemplate = (existingTemplate, defaultTemplate) => {
1828
+ if (existingTemplate.trim().length === 0) {
1829
+ return defaultTemplate;
1830
+ }
1831
+ const defaultParsed = parseTemplateSections(defaultTemplate);
1832
+ const existingParsed = parseTemplateSections(existingTemplate);
1833
+ const defaultHeadings = new Set(
1834
+ defaultParsed.sections.map((section) => section.heading)
2140
1835
  );
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";
1836
+ const customBeforeDefault = /* @__PURE__ */ new Map();
1837
+ const trailingCustomSections = [];
1838
+ existingParsed.sections.forEach((section, index) => {
1839
+ if (defaultHeadings.has(resolveManagedTemplateHeading(section.heading))) {
1840
+ return;
1841
+ }
1842
+ const nextDefaultSection = existingParsed.sections.slice(index + 1).find(
1843
+ (candidate) => defaultHeadings.has(resolveManagedTemplateHeading(candidate.heading))
1844
+ );
1845
+ if (nextDefaultSection) {
1846
+ const nextDefaultHeading = resolveManagedTemplateHeading(
1847
+ nextDefaultSection.heading
1848
+ );
1849
+ const bucket = customBeforeDefault.get(nextDefaultHeading) ?? [];
1850
+ bucket.push(section);
1851
+ customBeforeDefault.set(nextDefaultHeading, bucket);
1852
+ return;
1853
+ }
1854
+ trailingCustomSections.push(section);
1855
+ });
1856
+ const mergedSections = defaultParsed.sections.flatMap((section) => [
1857
+ ...customBeforeDefault.get(section.heading) ?? [],
1858
+ section
1859
+ ]);
2149
1860
  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");
1861
+ stripManagedFrontmatterFields(existingParsed.preamble),
1862
+ ...mergedSections.map((section) => section.block),
1863
+ ...trailingCustomSections.map((section) => section.block),
1864
+ ""
1865
+ ].filter((block) => block.length > 0).join("\n\n");
2158
1866
  };
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`
2165
- );
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";
1867
+ var renderBehaviorDocTemplateFile = () => {
2174
1868
  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}`
1869
+ "---",
1870
+ "status: active",
1871
+ "truth_kind: engineering-behavior",
1872
+ `last_reviewed: ${currentDate()}`,
1873
+ "---",
1874
+ "",
1875
+ "# {{title}}",
1876
+ "",
1877
+ "## Purpose",
1878
+ "",
1879
+ "<!--",
1880
+ "State the user/system outcome this behavior protects and why it exists.",
1881
+ "Include the problem boundary and durable value; exclude roadmap, implementation plan, and historical narrative.",
1882
+ "List the code, config, docs, or tests that support the claim in Source References rather than prose-only assertion.",
1883
+ "-->",
1884
+ "",
1885
+ "{{purpose}}",
1886
+ "",
1887
+ "## Scope",
1888
+ "",
1889
+ "<!--",
1890
+ "Define the one coherent behavior surface this document owns.",
1891
+ "Include in-scope actors, entrypoints, state/data owned by this doc, and explicit handoffs to neighboring truth docs.",
1892
+ "Split into another leaf doc when content introduces a distinct outcome, state machine, rule family, external contract, or route owner.",
1893
+ "Keep README.md files as indexes only.",
1894
+ "-->",
1895
+ "",
1896
+ "{{scope}}",
1897
+ "",
1898
+ "This doc was created from the editable engineering-behavior template at {{template_path}}.",
1899
+ "",
1900
+ "## Current Implementation Behavior",
1901
+ "",
1902
+ "<!--",
1903
+ "Describe only current implemented behavior in present tense.",
1904
+ "Cover observable behavior, important defaults, and user/system-visible effects; exclude desired future behavior and speculative design.",
1905
+ "Every non-obvious claim should be checkable from Source References.",
1906
+ "-->",
1907
+ "",
1908
+ "{{current_implementation_behavior}}",
1909
+ "",
1910
+ "## Core Rules",
1911
+ "",
1912
+ "<!--",
1913
+ "Capture stable business rules, invariants, precedence rules, validation rules, and must-never constraints.",
1914
+ "Separate rules from incidental implementation details; cite current implementation or tests for rule enforcement.",
1915
+ "-->",
1916
+ "",
1917
+ "{{core_rules}}",
1918
+ "",
1919
+ "## Behavior Scenarios",
1920
+ "",
1921
+ "<!--",
1922
+ "Use compact scenario blocks only where they clarify normal, fallback, or compatibility-critical behavior.",
1923
+ "Write scenarios as current truth, not desired requirements: `#### Scenario: <implemented case>` followed by `- **GIVEN** ...`, `- **WHEN** ...`, `- **THEN** ...`, and optional `- **AND** ...` bullets.",
1924
+ "Keep each bullet evidence-backed and observable; do not force a scenario for every rule.",
1925
+ "-->",
1926
+ "",
1927
+ "{{behavior_scenarios}}",
1928
+ "",
1929
+ "## Flows And States",
1930
+ "",
1931
+ "<!--",
1932
+ "Document state transitions, lifecycle stages, retries, fallbacks, route switches, and important error paths.",
1933
+ "State 'None beyond current behavior.' when this behavior has no distinct flow or state model.",
1934
+ "-->",
1935
+ "",
1936
+ "{{flows_and_states}}",
1937
+ "",
1938
+ "## Contracts",
1939
+ "",
1940
+ "<!--",
1941
+ "Capture user-visible or integration contracts: CLI/API shape, inputs, outputs, diagnostics, files, events, permissions, or links to canonical contract docs.",
1942
+ "Avoid duplicating a separate canonical contract doc; link to it when contract ownership lives elsewhere.",
1943
+ "-->",
1944
+ "",
1945
+ "{{contracts}}",
1946
+ "",
1947
+ "## Product Truth Links",
1948
+ "",
1949
+ "<!--",
1950
+ "List product truth docs this engineering doc realizes; author canonical realizes links in route YAML, not doc frontmatter.",
1951
+ "Use 'None.' when this is purely internal engineering behavior.",
1952
+ "-->",
1953
+ "",
1954
+ "{{product_truth_links}}",
1955
+ "",
1956
+ "## Engineering Decisions",
1957
+ "",
1958
+ "<!--",
1959
+ "Keep active decisions only, dated inline when added or changed.",
1960
+ "Explain decisions that shape behavior, boundaries, rejected alternatives, or migration constraints; replace stale decisions instead of appending historical logs.",
1961
+ "-->",
1962
+ "",
1963
+ "{{engineering_decisions}}",
1964
+ "",
1965
+ "## Rationale",
1966
+ "",
1967
+ "<!--",
1968
+ "Explain why the current behavior and active decisions are this way, including tradeoffs and constraints.",
1969
+ "Tie rationale to evidence-backed behavior; do not use this as a changelog.",
1970
+ "-->",
1971
+ "",
1972
+ "{{rationale}}",
1973
+ "",
1974
+ "## Non-Goals",
1975
+ "",
1976
+ "<!--",
1977
+ "Name adjacent behavior this doc intentionally does not own, especially tempting future expansions or neighboring route owners.",
1978
+ "Use this section to prevent scope creep and duplicate truth ownership.",
1979
+ "-->",
1980
+ "",
1981
+ "{{non_goals}}",
1982
+ "",
1983
+ "## Maintenance Notes",
1984
+ "",
1985
+ "<!--",
1986
+ "List related tests, routing cautions, migration notes, evidence drift risks, and review triggers for future maintainers or agents.",
1987
+ "Keep this operational and current-state focused, not historical.",
1988
+ "-->",
1989
+ "",
1990
+ "{{maintenance_notes}}",
1991
+ "",
1992
+ "## Source References",
1993
+ "",
1994
+ "<!--",
1995
+ "List source files, tests, configs, generated templates, route files, or product instructions that support current claims.",
1996
+ "-->",
1997
+ "",
1998
+ "{{source_references}}",
1999
+ ""
2182
2000
  ].join("\n");
2183
2001
  };
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";
2002
+ var sectionSpec = (heading, guidance, placeholder = titleToPlaceholder(heading)) => ({ heading, guidance, placeholder });
2003
+ var PURPOSE_SECTION = sectionSpec("## Purpose", [
2004
+ "State the software-engineering outcome this document protects and why the documented surface exists.",
2005
+ "Include durable value, impacted users/systems, and the problem boundary; exclude roadmap, implementation plans, and historical narrative.",
2006
+ "Keep claims traceable to Source References rather than prose-only assertion."
2007
+ ]);
2008
+ var SCOPE_SECTION = sectionSpec("## Scope", [
2009
+ "Define the one coherent surface this document owns, including actors, entrypoints, owned state/data, and handoffs to neighboring truth docs.",
2010
+ "Call out important out-of-scope boundaries here or in Non-Goals; split the doc when it mixes distinct outcomes, lifecycles, contracts, or owners."
2011
+ ]);
2012
+ var PRODUCT_DECISIONS_SECTION = sectionSpec(
2013
+ "## Product Decisions",
2014
+ [
2015
+ "Keep active decisions only, dated inline when added or changed.",
2016
+ "Capture decisions that shape behavior, interfaces, boundaries, compatibility, risk acceptance, or migration constraints.",
2017
+ "Replace stale decisions instead of appending historical logs."
2018
+ ],
2019
+ "decision"
2020
+ );
2021
+ var ENGINEERING_DECISIONS_SECTION = sectionSpec(
2022
+ "## Engineering Decisions",
2023
+ [
2024
+ "Keep active engineering, architecture, contract, workflow, or operational decisions only, dated inline when added or changed.",
2025
+ "Do not restate product promises, product rationale, or business decisions here; link product truth instead.",
2026
+ "Replace stale decisions instead of appending historical logs."
2027
+ ],
2028
+ "engineering_decisions"
2029
+ );
2030
+ var RATIONALE_SECTION = sectionSpec("## Rationale", [
2031
+ "Explain why the current behavior, structure, or contract is this way, including tradeoffs and constraints.",
2032
+ "Tie rationale to evidence-backed facts and active decisions; do not use this as a changelog."
2033
+ ]);
2034
+ var NON_GOALS_SECTION = sectionSpec("## Non-Goals", [
2035
+ "Name adjacent behavior, responsibilities, interfaces, or future expansions this doc intentionally does not own.",
2036
+ "Use this section to prevent scope creep and duplicate truth ownership."
2037
+ ]);
2038
+ var MAINTENANCE_NOTES_SECTION = sectionSpec("## Maintenance Notes", [
2039
+ "List related tests, routing cautions, migration notes, compatibility risks, evidence drift risks, and review triggers for future maintainers or agents.",
2040
+ "Keep this operational and current-state focused, not historical."
2041
+ ]);
2042
+ var SOURCE_REFERENCES_SECTION = sectionSpec(
2043
+ "## Source References",
2044
+ [
2045
+ "List source files, tests, configs, generated templates, route files, or product instructions that support current claims."
2046
+ ],
2047
+ "source_references"
2048
+ );
2049
+ var renderTypedTruthDocTemplate = (truthKind, title, sections) => {
2197
2050
  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}`
2051
+ "---",
2052
+ "status: active",
2053
+ `truth_kind: ${truthKind}`,
2054
+ `last_reviewed: ${currentDate()}`,
2055
+ "---",
2056
+ "",
2057
+ `# ${title}`,
2058
+ "",
2059
+ ...renderTemplateSection(PURPOSE_SECTION),
2060
+ ...renderTemplateSection(SCOPE_SECTION),
2061
+ ...sections.flatMap(renderTemplateSection),
2062
+ ...renderTemplateSection(ENGINEERING_DECISIONS_SECTION),
2063
+ ...renderTemplateSection(RATIONALE_SECTION),
2064
+ ...renderTemplateSection(NON_GOALS_SECTION),
2065
+ ...renderTemplateSection(MAINTENANCE_NOTES_SECTION),
2066
+ ...renderTemplateSection(SOURCE_REFERENCES_SECTION)
2205
2067
  ].join("\n");
2206
2068
  };
2207
- var defaultAgentConfig = () => {
2208
- return createDefaultConfig();
2209
- };
2210
- var renderHierarchySummary = (config) => {
2211
- const productRoot = resolveProductTruthRoot(config);
2212
- const engineeringRoot = resolveEngineeringTruthRoot(config);
2069
+ 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.";
2070
+ var renderProductTruthDocTemplate = (truthKind, title, sections, includeNonGoals) => {
2213
2071
  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`
2072
+ "---",
2073
+ "status: active",
2074
+ `truth_kind: ${truthKind}`,
2075
+ `last_reviewed: ${currentDate()}`,
2076
+ "---",
2077
+ "",
2078
+ `# ${title}`,
2079
+ "",
2080
+ "<!--",
2081
+ CORE_LANE_INVARIANT,
2082
+ "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.",
2083
+ "-->",
2084
+ "",
2085
+ ...sections.flatMap(renderTemplateSection),
2086
+ ...renderTemplateSection(PRODUCT_DECISIONS_SECTION),
2087
+ ...renderTemplateSection(
2088
+ sectionSpec(
2089
+ "## Engineering Realization Links",
2090
+ [
2091
+ "List engineering truth that realizes this product truth; author canonical realized_by links in route YAML, not doc frontmatter.",
2092
+ "Do not summarize those engineering docs."
2093
+ ],
2094
+ "engineering_realization_links"
2095
+ )
2096
+ ),
2097
+ ...includeNonGoals ? renderTemplateSection(NON_GOALS_SECTION) : [],
2098
+ ...renderTemplateSection(SOURCE_REFERENCES_SECTION)
2220
2099
  ].join("\n");
2221
2100
  };
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.`;
2101
+ var renderProductCapabilityDocTemplateFile = () => {
2102
+ return renderProductTruthDocTemplate(
2103
+ "product-capability",
2104
+ "{{title}}",
2105
+ [
2106
+ sectionSpec("## Capability Promise", [
2107
+ "State the single user-visible capability and what must be true for users or stakeholders.",
2108
+ "Do not describe implementation mechanics here."
2109
+ ]),
2110
+ sectionSpec("## Users And Value", [
2111
+ "Describe who benefits from the capability and the durable value it protects.",
2112
+ "Tie claims to repository evidence, explicit user instruction, or current behavior."
2113
+ ]),
2114
+ sectionSpec("## Capability Scope", [
2115
+ "Define what this capability includes and excludes, including product boundary constraints and adjacent systems.",
2116
+ "Capture important scope limits, ownership boundaries, and non-goal pointers here; keep technical contracts in engineering truth."
2117
+ ]),
2118
+ sectionSpec("## Current Product Behavior", [
2119
+ "Describe current implemented user-visible behavior in present tense.",
2120
+ "Code files may appear in Source References when they directly prove current behavior."
2121
+ ]),
2122
+ sectionSpec("## Acceptance Criteria", [
2123
+ "List observable criteria that show the capability promise is currently satisfied.",
2124
+ "Include criteria that review whether the capability stays within its stated scope and boundary.",
2125
+ "Use criteria that can be reviewed from repository evidence or explicit product instruction."
2126
+ ])
2127
+ ],
2128
+ true
2129
+ );
2233
2130
  };
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;
2236
- return [
2237
- TRUTHMARK_BLOCK_START,
2238
- "## Truthmark Workflow",
2239
- "",
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
2252
- ].join("\n");
2131
+ var renderContractDocTemplateFile = () => {
2132
+ return renderTypedTruthDocTemplate("engineering-contract", "{{title}}", [
2133
+ sectionSpec("## Contract Surface", [
2134
+ "Identify the owned API, CLI, file format, event, protocol, permission boundary, or integration surface.",
2135
+ "State consumers/producers, stability level, and the source files/tests that define the contract."
2136
+ ]),
2137
+ sectionSpec("## Inputs", [
2138
+ "Document accepted parameters, payloads, files, environment/config keys, permissions, and validation rules.",
2139
+ "Include required/optional status, defaults, constraints, and normalization behavior."
2140
+ ]),
2141
+ sectionSpec("## Outputs", [
2142
+ "Document returned values, emitted files/events, state changes, side effects, and success diagnostics.",
2143
+ "Make externally observable behavior explicit enough for compatibility review."
2144
+ ]),
2145
+ sectionSpec("## Errors And Diagnostics", [
2146
+ "List error classes, exit/status codes, user-facing diagnostics, retries, and recoverability expectations.",
2147
+ "Distinguish validation errors, dependency failures, authorization failures, and internal faults when applicable."
2148
+ ]),
2149
+ sectionSpec("## Compatibility Rules", [
2150
+ "State backward/forward compatibility guarantees, tolerated inputs, deprecation rules, and breaking-change triggers.",
2151
+ "Include compatibility tests or review questions that protect the contract."
2152
+ ]),
2153
+ sectionSpec("## Versioning And Migration", [
2154
+ "Document version negotiation, schema/API version fields, rollout requirements, migration steps, and rollback expectations.",
2155
+ "State 'Not versioned' only when the implementation truly has no versioning or migration surface."
2156
+ ])
2157
+ ]);
2253
2158
  };
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
- }
2159
+ var renderArchitectureDocTemplateFile = () => {
2160
+ return renderTypedTruthDocTemplate("engineering-architecture", "{{title}}", [
2161
+ sectionSpec("## System Role", [
2162
+ "Describe the current architectural role of this subsystem/component in the larger system.",
2163
+ "State the primary responsibilities, consumers, providers, and why this boundary exists now."
2164
+ ]),
2165
+ sectionSpec("## Boundaries", [
2166
+ "Define owned code/config/data, external dependencies, trust boundaries, and interfaces crossed by this architecture.",
2167
+ "Name what is deliberately outside the boundary and link neighboring architecture or contract docs when they own it."
2168
+ ]),
2169
+ sectionSpec("## Components", [
2170
+ "List the major runtime/build-time components, modules, services, jobs, or generated artifacts and their responsibilities.",
2171
+ "Keep the component list current and evidence-backed; avoid speculative target architecture."
2172
+ ]),
2173
+ sectionSpec("## Data And Control Flow", [
2174
+ "Describe important data movement, command/control paths, synchronization points, state ownership, and failure paths.",
2175
+ "Call out persistence, queues, caches, external calls, and security-sensitive transitions where relevant."
2176
+ ]),
2177
+ sectionSpec("## Ownership", [
2178
+ "Document team/module ownership, review responsibility, operational responsibility, and escalation paths if known.",
2179
+ "If ownership is inferred from codeowners, config, or repository structure, cite that evidence."
2180
+ ]),
2181
+ sectionSpec("## Cross-Cutting Constraints", [
2182
+ "Record active constraints such as security, privacy, reliability, performance, portability, maintainability, compliance, and cost.",
2183
+ "Tie constraints to source evidence, tests, standards, or operational requirements where available."
2184
+ ])
2185
+ ]);
2267
2186
  };
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
- };
2187
+ var renderWorkflowDocTemplateFile = () => {
2188
+ return renderTypedTruthDocTemplate("engineering-workflow", "{{title}}", [
2189
+ sectionSpec("## Triggers", [
2190
+ "List events, commands, schedules, user actions, webhooks, or dependency signals that start this workflow.",
2191
+ "Include preconditions, authorization requirements, debounce/coalescing behavior, and disabled states when applicable."
2192
+ ]),
2193
+ sectionSpec("## Inputs", [
2194
+ "Document data, files, config, context, credentials, and environmental assumptions consumed by the workflow.",
2195
+ "Include validation, defaults, and normalization that happen before execution."
2196
+ ]),
2197
+ sectionSpec("## Execution Model", [
2198
+ "Describe synchronous/asynchronous execution, concurrency, locking, leases, batching, ordering, and idempotency behavior.",
2199
+ "State whether the workflow waits for user action, runs in the background, is distributed, or is delegated to another system."
2200
+ ]),
2201
+ sectionSpec("## Steps", [
2202
+ "Capture the current ordered steps or phases at a level useful for maintenance and review.",
2203
+ "Reference implementation entrypoints instead of duplicating line-by-line code behavior."
2204
+ ]),
2205
+ sectionSpec("## State, Retry, And Failure Behavior", [
2206
+ "Document state transitions, retries, timeouts, compensation, fallback, partial-success, and terminal-failure behavior.",
2207
+ "Make externally visible failure semantics and recovery responsibilities clear."
2208
+ ]),
2209
+ sectionSpec("## Outputs", [
2210
+ "List artifacts, state changes, notifications, logs, metrics, diagnostics, and downstream triggers produced by the workflow.",
2211
+ "Include success criteria and handoff points to other truth docs or systems."
2212
+ ])
2213
+ ]);
2287
2214
  };
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);
2215
+ var renderOperationsDocTemplateFile = () => {
2216
+ return renderTypedTruthDocTemplate("engineering-operations", "{{title}}", [
2217
+ sectionSpec("## Operational Surface", [
2218
+ "Describe what operators, maintainers, or automated systems can observe or control for this surface.",
2219
+ "Include commands, dashboards, alerts, runbooks, jobs, or operational APIs that define current operations."
2220
+ ]),
2221
+ sectionSpec("## Runtime Topology", [
2222
+ "Document services, processes, containers, hosts, regions, dependencies, queues, stores, and network boundaries involved at runtime.",
2223
+ "State single-node/local behavior explicitly when there is no distributed topology."
2224
+ ]),
2225
+ sectionSpec("## Configuration", [
2226
+ "List operational config, environment variables, feature flags, secrets references, defaults, and reload/restart requirements.",
2227
+ "Do not include secret values; describe storage and rotation expectations instead."
2228
+ ]),
2229
+ sectionSpec("## Permissions", [
2230
+ "Document required identities, roles, scopes, filesystem/network permissions, and least-privilege boundaries.",
2231
+ "Include user-facing authorization behavior and operator access requirements when relevant."
2232
+ ]),
2233
+ sectionSpec("## Deployment And Rollback", [
2234
+ "Describe deployment mechanism, migration ordering, compatibility windows, rollback path, and known irreversible operations.",
2235
+ "Call out manual review points, smoke checks, and post-deploy verification responsibilities."
2236
+ ]),
2237
+ sectionSpec("## Availability And Observability", [
2238
+ "Capture availability expectations, health checks, metrics, logs, traces, alerts, SLO/error-budget signals, and known blind spots.",
2239
+ "Include what maintainers should inspect first during incidents or degraded behavior."
2240
+ ])
2241
+ ]);
2294
2242
  };
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}`;
2243
+ var renderTestBehaviorDocTemplateFile = () => {
2244
+ return renderTypedTruthDocTemplate("engineering-test-behavior", "{{title}}", [
2245
+ sectionSpec("## Test Surface", [
2246
+ "Define the behavior, contract, architecture, or workflow surface these tests verify.",
2247
+ "Link the canonical truth docs and code paths the tests are meant to protect."
2248
+ ]),
2249
+ sectionSpec("## Fixtures And Data Model", [
2250
+ "Document fixtures, factories, seeds, mocks/fakes, test repositories, external-service substitutes, and data lifecycle rules.",
2251
+ "Include cleanup, determinism, privacy, and cross-test contamination constraints."
2252
+ ]),
2253
+ sectionSpec("## Execution Model", [
2254
+ "Describe how tests run: command, framework, parallelism, isolation, network/filesystem assumptions, and required services.",
2255
+ "State whether tests are unit, integration, e2e, contract, smoke, regression, or generated checks."
2256
+ ]),
2257
+ sectionSpec("## Assertions And Invariants", [
2258
+ "List the critical assertions, invariants, failure modes, and negative cases that make the tests meaningful.",
2259
+ "Tie assertions to product/contract rules rather than incidental implementation details."
2260
+ ]),
2261
+ sectionSpec("## Isolation Rules", [
2262
+ "Document transaction boundaries, temp directories, fake clocks, network blocking, shared resources, and teardown rules.",
2263
+ "Call out known order dependencies or flake risks and how they are controlled."
2264
+ ]),
2265
+ sectionSpec("## Reporting And Failure Semantics", [
2266
+ "Describe diagnostics, snapshots, logs, coverage signals, retry policy, and how maintainers should interpret failures.",
2267
+ "Include escalation or quarantine criteria for flaky or environment-sensitive tests."
2268
+ ])
2269
+ ]);
2326
2270
  };
2327
2271
 
2328
2272
  // src/init/hierarchy.ts
2329
- import fs5 from "fs/promises";
2330
2273
  var truthRoot2 = resolveTruthDocsRoot;
2331
2274
  var BEHAVIOR_DOC_TEMPLATE_FILE_NAME = "engineering-behavior.md";
2332
2275
  var CONTRACT_DOC_TEMPLATE_FILE_NAME = "engineering-contract.md";
@@ -2336,7 +2279,7 @@ var OPERATIONS_DOC_TEMPLATE_FILE_NAME = "engineering-operations.md";
2336
2279
  var TEST_BEHAVIOR_DOC_TEMPLATE_FILE_NAME = "engineering-test-behavior.md";
2337
2280
  var PRODUCT_CAPABILITY_DOC_TEMPLATE_FILE_NAME = "product-capability.md";
2338
2281
  var rootIndexReferencesChildRoute = async (rootDir, rootIndexPath, childRoutePath) => {
2339
- const rootIndexSource = await fs5.readFile(
2282
+ const rootIndexSource = await fs4.readFile(
2340
2283
  resolveRepoPath(rootDir, rootIndexPath),
2341
2284
  "utf8"
2342
2285
  );
@@ -2357,7 +2300,7 @@ var ensureOrUpdateTruthDocTemplate = async (rootDir, templatePath, defaultTempla
2357
2300
  if (seededResult.status !== "unchanged") {
2358
2301
  return seededResult;
2359
2302
  }
2360
- const existingTemplate = await fs5.readFile(
2303
+ const existingTemplate = await fs4.readFile(
2361
2304
  resolveRepoPath(rootDir, templatePath),
2362
2305
  "utf8"
2363
2306
  );
@@ -4974,11 +4917,11 @@ var renderGeneratedSurfaceCatalog = (config) => {
4974
4917
  };
4975
4918
 
4976
4919
  // src/init/lifecycle.ts
4977
- import fs6 from "fs/promises";
4920
+ import fs5 from "fs/promises";
4978
4921
  var plannedContents = /* @__PURE__ */ new WeakMap();
4979
4922
  var readFile = async (rootDir, filePath) => {
4980
4923
  try {
4981
- return await fs6.readFile(resolveRepoPath(rootDir, filePath), "utf8");
4924
+ return await fs5.readFile(resolveRepoPath(rootDir, filePath), "utf8");
4982
4925
  } catch (error) {
4983
4926
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
4984
4927
  return null;
@@ -4991,7 +4934,7 @@ var listFiles = async (rootDir, directory) => {
4991
4934
  while (stack.length > 0) {
4992
4935
  const current = stack.pop();
4993
4936
  try {
4994
- for (const entry of await fs6.readdir(resolveRepoPath(rootDir, current), {
4937
+ for (const entry of await fs5.readdir(resolveRepoPath(rootDir, current), {
4995
4938
  withFileTypes: true
4996
4939
  })) {
4997
4940
  const next = `${current}/${entry.name}`;
@@ -5009,7 +4952,7 @@ var findRetiredSurfaces = async (rootDir) => {
5009
4952
  const paths = /* @__PURE__ */ new Set();
5010
4953
  for (const filePath of RETIRED_GENERATED_SURFACES.exactPaths) {
5011
4954
  try {
5012
- await fs6.lstat(resolveRepoPath(rootDir, filePath));
4955
+ await fs5.lstat(resolveRepoPath(rootDir, filePath));
5013
4956
  paths.add(filePath);
5014
4957
  } catch (error) {
5015
4958
  if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
@@ -5027,7 +4970,7 @@ var findRetiredSurfaces = async (rootDir) => {
5027
4970
  paths.add(filePath);
5028
4971
  let packages = [];
5029
4972
  try {
5030
- packages = (await fs6.readdir(resolveRepoPath(rootDir, skillRoot), {
4973
+ packages = (await fs5.readdir(resolveRepoPath(rootDir, skillRoot), {
5031
4974
  withFileTypes: true
5032
4975
  })).filter(
5033
4976
  (entry) => entry.isDirectory() && entry.name.startsWith("truthmark-")
@@ -5040,7 +4983,7 @@ var findRetiredSurfaces = async (rootDir) => {
5040
4983
  for (const retiredFile of RETIRED_GENERATED_SURFACES.retiredPackageFiles) {
5041
4984
  const filePath = `${skillRoot}/${packageName}/${retiredFile}`;
5042
4985
  try {
5043
- await fs6.lstat(resolveRepoPath(rootDir, filePath));
4986
+ await fs5.lstat(resolveRepoPath(rootDir, filePath));
5044
4987
  paths.add(filePath);
5045
4988
  } catch (error) {
5046
4989
  if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
@@ -5209,23 +5152,23 @@ var applyLifecyclePlan = async (rootDir, plan) => {
5209
5152
  for (const entry of plan.entries) {
5210
5153
  const absolutePath = resolveRepoPath(rootDir, entry.path);
5211
5154
  if (entry.action === "remove-file") {
5212
- const stat = await fs6.lstat(absolutePath);
5155
+ const stat = await fs5.lstat(absolutePath);
5213
5156
  if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1)
5214
5157
  throw new Error(
5215
5158
  `Refusing unsafe generated file removal: ${entry.path}`
5216
5159
  );
5217
- await fs6.rm(absolutePath);
5160
+ await fs5.rm(absolutePath);
5218
5161
  } else if (entry.action === "remove-managed-block") {
5219
- const stat = await fs6.lstat(absolutePath);
5162
+ const stat = await fs5.lstat(absolutePath);
5220
5163
  if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1)
5221
5164
  throw new Error(`Refusing unsafe managed-block removal: ${entry.path}`);
5222
- const content = await fs6.readFile(absolutePath, "utf8");
5165
+ const content = await fs5.readFile(absolutePath, "utf8");
5223
5166
  const block = parseManagedBlock(content);
5224
5167
  if (block.status !== "valid")
5225
5168
  throw new Error(`Managed block changed during removal: ${entry.path}`);
5226
5169
  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");
5170
+ if (remaining.trim().length === 0) await fs5.rm(absolutePath);
5171
+ else await fs5.writeFile(absolutePath, remaining, "utf8");
5229
5172
  }
5230
5173
  }
5231
5174
  return { ...plan, applied: true };
@@ -5235,7 +5178,7 @@ var applyLifecyclePlan = async (rootDir, plan) => {
5235
5178
  var writeManagedAgentsFile = async (rootDir, path13 = "AGENTS.md", block) => {
5236
5179
  let existingContent = null;
5237
5180
  try {
5238
- existingContent = await fs7.readFile(resolveRepoPath(rootDir, path13), "utf8");
5181
+ existingContent = await fs6.readFile(resolveRepoPath(rootDir, path13), "utf8");
5239
5182
  } catch (error) {
5240
5183
  if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
5241
5184
  throw error;
@@ -5289,28 +5232,87 @@ var writeDiagnostics = (results, config) => {
5289
5232
  file: result.path
5290
5233
  }));
5291
5234
  };
5292
- var runInit = async (cwd) => {
5235
+ var normalizeRequestedPlatforms = (values) => {
5236
+ const unsupported = values.filter(
5237
+ (value) => !SUPPORTED_PLATFORMS.includes(value)
5238
+ );
5239
+ const selected = new Set(values);
5240
+ return {
5241
+ platforms: SUPPORTED_PLATFORMS.filter((platform) => selected.has(platform)),
5242
+ unsupported: [...new Set(unsupported)]
5243
+ };
5244
+ };
5245
+ var runInit = async (cwd, options = {}) => {
5293
5246
  const repository = await getGitRepository(cwd);
5294
5247
  const rootDir = repository.worktreePath;
5295
5248
  const loadedConfig = await loadConfig(rootDir);
5296
- if (!loadedConfig.config) {
5249
+ const repositoryData = {
5250
+ repositoryRoot: repository.repositoryRoot,
5251
+ worktreePath: repository.worktreePath,
5252
+ branchName: repository.branchName,
5253
+ isDetached: repository.isDetached,
5254
+ isUnborn: repository.isUnborn
5255
+ };
5256
+ if (loadedConfig.status === "invalid") {
5297
5257
  return {
5298
5258
  command: "init",
5299
- summary: "Truthmark init requires .truthmark/config.yml. Run truthmark config first, review the workspace paths, then run truthmark init.",
5259
+ summary: "Truthmark init made no changes because config is invalid.",
5300
5260
  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
- }
5261
+ data: repositoryData
5308
5262
  };
5309
5263
  }
5264
+ const savedPlatforms = loadedConfig.config?.platforms ?? [];
5265
+ let requestedPlatforms;
5266
+ if (options.platforms !== void 0) requestedPlatforms = options.platforms;
5267
+ else if (options.selectPlatforms) {
5268
+ const selected = await options.selectPlatforms(savedPlatforms);
5269
+ if (selected === null)
5270
+ return {
5271
+ command: "init",
5272
+ summary: "Truthmark init cancelled; no repository files were changed.",
5273
+ diagnostics: [],
5274
+ data: { ...repositoryData, cancelled: true }
5275
+ };
5276
+ requestedPlatforms = selected;
5277
+ } else requestedPlatforms = savedPlatforms;
5278
+ const normalized = normalizeRequestedPlatforms(requestedPlatforms);
5279
+ if (normalized.unsupported.length > 0)
5280
+ return {
5281
+ command: "init",
5282
+ summary: "Truthmark init requires supported platform values.",
5283
+ diagnostics: normalized.unsupported.map((platform) => ({
5284
+ category: "config",
5285
+ severity: "error",
5286
+ message: `Unsupported Truthmark platform: ${platform}.`,
5287
+ file: ".truthmark/config.yml"
5288
+ })),
5289
+ data: repositoryData
5290
+ };
5291
+ const config = {
5292
+ ...loadedConfig.config ?? createDefaultConfig(),
5293
+ platforms: normalized.platforms
5294
+ };
5295
+ const existingConfigSource = loadedConfig.config ? await fs6.readFile(resolveRepoPath(rootDir, loadedConfig.configPath), "utf8") : null;
5296
+ const configSource = existingConfigSource ? updateConfigPlatforms(existingConfigSource, normalized.platforms) : renderConfig(normalized.platforms);
5297
+ const configDiagnostics = loadedConfig.status === "loaded" ? loadedConfig.diagnostics : [];
5310
5298
  const results = [];
5311
- const config = loadedConfig.config;
5312
5299
  const block = renderAgentsBlock(config);
5313
5300
  const platformFiles = renderGeneratedSurfaces(config, block);
5301
+ if (!await isSafeExactFile(rootDir, loadedConfig.configPath, true)) {
5302
+ return {
5303
+ command: "init",
5304
+ summary: "Truthmark init made no changes because the config path is unsafe.",
5305
+ diagnostics: [
5306
+ {
5307
+ category: "config",
5308
+ severity: "error",
5309
+ message: "Truthmark config path must be a regular file contained in the repository.",
5310
+ file: loadedConfig.configPath
5311
+ }
5312
+ ],
5313
+ data: repositoryData
5314
+ };
5315
+ }
5314
5316
  const lifecyclePlan = await buildLifecyclePlan(
5315
5317
  rootDir,
5316
5318
  config,
@@ -5321,26 +5323,29 @@ var runInit = async (cwd) => {
5321
5323
  return {
5322
5324
  command: "init",
5323
5325
  summary: "Truthmark init made no changes because generated-surface preflight failed.",
5324
- diagnostics: [...loadedConfig.diagnostics, ...lifecyclePlan.diagnostics],
5325
- data: { lifecyclePlan }
5326
+ diagnostics: [...configDiagnostics, ...lifecyclePlan.diagnostics],
5327
+ data: { ...repositoryData, lifecyclePlan }
5326
5328
  };
5327
5329
  }
5330
+ results.push(...await scaffoldHierarchy(rootDir, config));
5331
+ for (const file of platformFiles) {
5332
+ results.push(await writePlatformFile(rootDir, file));
5333
+ }
5334
+ results.push(
5335
+ await writeRepoFile(rootDir, loadedConfig.configPath, configSource)
5336
+ );
5328
5337
  const appliedLifecyclePlan = await applyLifecyclePlan(rootDir, lifecyclePlan);
5329
5338
  if (!appliedLifecyclePlan.applicable) {
5330
5339
  return {
5331
5340
  command: "init",
5332
5341
  summary: "Truthmark init made no changes because generated-surface preflight failed.",
5333
5342
  diagnostics: [
5334
- ...loadedConfig.diagnostics,
5343
+ ...configDiagnostics,
5335
5344
  ...appliedLifecyclePlan.diagnostics
5336
5345
  ],
5337
- data: { lifecyclePlan: appliedLifecyclePlan }
5346
+ data: { ...repositoryData, lifecyclePlan: appliedLifecyclePlan }
5338
5347
  };
5339
5348
  }
5340
- results.push(...await scaffoldHierarchy(rootDir, config));
5341
- for (const file of platformFiles) {
5342
- results.push(await writePlatformFile(rootDir, file));
5343
- }
5344
5349
  const changedResults = results.filter(
5345
5350
  (result) => result.status !== "unchanged"
5346
5351
  );
@@ -5351,7 +5356,7 @@ var runInit = async (cwd) => {
5351
5356
  command: "init",
5352
5357
  summary: changedResults.length > 0 || lifecycleChanged ? "Initialized or updated the Truthmark repository scaffold." : "Truthmark repository scaffold is already up to date.",
5353
5358
  diagnostics: [
5354
- ...loadedConfig.diagnostics,
5359
+ ...configDiagnostics,
5355
5360
  ...appliedLifecyclePlan.diagnostics,
5356
5361
  ...appliedLifecyclePlan.entries.map((entry) => ({
5357
5362
  category: "generated-surface",
@@ -5362,16 +5367,68 @@ var runInit = async (cwd) => {
5362
5367
  ...writeDiagnostics(results, config)
5363
5368
  ],
5364
5369
  data: {
5365
- repositoryRoot: repository.repositoryRoot,
5366
- worktreePath: repository.worktreePath,
5367
- branchName: repository.branchName,
5368
- isDetached: repository.isDetached,
5369
- isUnborn: repository.isUnborn,
5370
+ ...repositoryData,
5370
5371
  lifecyclePlan: appliedLifecyclePlan
5371
5372
  }
5372
5373
  };
5373
5374
  };
5374
5375
 
5376
+ // src/cli/platform-selection.ts
5377
+ import { createInterface } from "readline/promises";
5378
+ var PLATFORM_LABELS = {
5379
+ codex: "Codex",
5380
+ opencode: "OpenCode",
5381
+ "claude-code": "Claude Code",
5382
+ "github-copilot": "GitHub Copilot",
5383
+ antigravity: "Antigravity",
5384
+ cursor: "Cursor"
5385
+ };
5386
+ var normalizePlatforms2 = (platforms) => {
5387
+ const selected = new Set(platforms);
5388
+ return SUPPORTED_PLATFORMS.filter((platform) => selected.has(platform));
5389
+ };
5390
+ var renderPlatformChoices = (defaults) => {
5391
+ const selected = new Set(defaults);
5392
+ return SUPPORTED_PLATFORMS.map(
5393
+ (platform, index) => `${index + 1}. ${PLATFORM_LABELS[platform]} [${platform}]${selected.has(platform) ? " (selected)" : ""}`
5394
+ ).join("\n");
5395
+ };
5396
+ var parsePlatformSelection = (input, defaults) => {
5397
+ const value = input.trim().toLowerCase();
5398
+ if (value === "") return normalizePlatforms2(defaults);
5399
+ if (value === "q" || value === "quit") return null;
5400
+ if (value === "none") return [];
5401
+ const selected = value.split(",").map((token) => {
5402
+ const index = Number(token.trim());
5403
+ if (!Number.isInteger(index) || index < 1 || index > SUPPORTED_PLATFORMS.length)
5404
+ throw new Error(`Unsupported platform choice: ${token.trim()}`);
5405
+ return SUPPORTED_PLATFORMS[index - 1];
5406
+ });
5407
+ return normalizePlatforms2(selected);
5408
+ };
5409
+ var promptForPlatforms = async (options) => {
5410
+ options.output.write(`${renderPlatformChoices(options.defaults)}
5411
+ `);
5412
+ const readline = createInterface({ input: options.input, output: options.output });
5413
+ try {
5414
+ for (; ; ) {
5415
+ const answer = await readline.question(
5416
+ "Select platforms by number (comma-separated), 'none' for CLI-only, or 'q' to cancel: "
5417
+ );
5418
+ try {
5419
+ return parsePlatformSelection(answer, options.defaults);
5420
+ } catch (error) {
5421
+ options.output.write(
5422
+ `${error instanceof Error ? error.message : String(error)}
5423
+ `
5424
+ );
5425
+ }
5426
+ }
5427
+ } finally {
5428
+ readline.close();
5429
+ }
5430
+ };
5431
+
5375
5432
  // src/init/uninstall.ts
5376
5433
  var runUninstall = async (cwd, mode) => {
5377
5434
  const repository = await getGitRepository(cwd);
@@ -5408,7 +5465,7 @@ var runUninstall = async (cwd, mode) => {
5408
5465
  };
5409
5466
 
5410
5467
  // src/checks/branch-scope.ts
5411
- import fs8 from "fs/promises";
5468
+ import fs7 from "fs/promises";
5412
5469
  import fg from "fast-glob";
5413
5470
 
5414
5471
  // src/markdown/hash.ts
@@ -5464,7 +5521,7 @@ var getBranchScopeData = async (cwd) => {
5464
5521
  }
5465
5522
  for (const relativePath of [...relevantFiles].sort()) {
5466
5523
  try {
5467
- const source = await fs8.readFile(resolveWorktreePath(repository, relativePath), "utf8");
5524
+ const source = await fs7.readFile(resolveWorktreePath(repository, relativePath), "utf8");
5468
5525
  relevantFileHashes[relativePath] = hashText(source);
5469
5526
  } catch (error) {
5470
5527
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -5481,14 +5538,14 @@ var getBranchScopeData = async (cwd) => {
5481
5538
  };
5482
5539
 
5483
5540
  // src/checks/authority.ts
5484
- import fs9 from "fs/promises";
5541
+ import fs8 from "fs/promises";
5485
5542
  import fg2 from "fast-glob";
5486
5543
  var looksLikeGlob = (pattern) => {
5487
5544
  return /[*?[\]{}()!+@]/u.test(pattern);
5488
5545
  };
5489
5546
  var pathExists = async (absolutePath) => {
5490
5547
  try {
5491
- await fs9.stat(absolutePath);
5548
+ await fs8.stat(absolutePath);
5492
5549
  return true;
5493
5550
  } catch (error) {
5494
5551
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -5580,10 +5637,10 @@ var checkAuthority = async (rootDir, config) => {
5580
5637
  };
5581
5638
 
5582
5639
  // src/checks/frontmatter.ts
5583
- import fs10 from "fs/promises";
5640
+ import fs9 from "fs/promises";
5584
5641
 
5585
5642
  // src/markdown/frontmatter.ts
5586
- import { parse as parse3 } from "yaml";
5643
+ import { parse as parse4 } from "yaml";
5587
5644
  var openingDelimiterPattern = /^(?:\uFEFF)?---[ \t]*(?:\r?\n|$)/u;
5588
5645
  var closingDelimiterPattern = /^---[ \t]*(?:\r?\n|$)/mu;
5589
5646
  var asRecord = (value) => {
@@ -5614,7 +5671,7 @@ var parseFrontmatter = (source, options = {}) => {
5614
5671
  let data = {};
5615
5672
  if (yamlSource.trim().length > 0) {
5616
5673
  try {
5617
- data = asRecord(parse3(yamlSource));
5674
+ data = asRecord(parse4(yamlSource));
5618
5675
  } catch (error) {
5619
5676
  if (options.throwOnInvalid) {
5620
5677
  throw error;
@@ -5682,7 +5739,7 @@ var checkFrontmatter = async (rootDir, config, markdownPaths, truthDocumentEntri
5682
5739
  }
5683
5740
  const absolutePath = resolveRepoPath(rootDir, markdownPath);
5684
5741
  await assertRepoContainment(rootDir, absolutePath);
5685
- const source = await fs10.readFile(absolutePath, "utf8");
5742
+ const source = await fs9.readFile(absolutePath, "utf8");
5686
5743
  let document;
5687
5744
  try {
5688
5745
  parseFrontmatter(source, { throwOnInvalid: true });
@@ -5772,11 +5829,11 @@ var checkFrontmatter = async (rootDir, config, markdownPaths, truthDocumentEntri
5772
5829
  };
5773
5830
 
5774
5831
  // src/checks/links.ts
5775
- import fs11 from "fs/promises";
5832
+ import fs10 from "fs/promises";
5776
5833
  import path5 from "path";
5777
5834
  var pathExists2 = async (absolutePath) => {
5778
5835
  try {
5779
- await fs11.stat(absolutePath);
5836
+ await fs10.stat(absolutePath);
5780
5837
  return true;
5781
5838
  } catch (error) {
5782
5839
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -5792,7 +5849,7 @@ var checkLinks = async (rootDir, markdownPaths) => {
5792
5849
  continue;
5793
5850
  }
5794
5851
  const absolutePath = resolveRepoPath(rootDir, markdownPath);
5795
- const source = await fs11.readFile(absolutePath, "utf8");
5852
+ const source = await fs10.readFile(absolutePath, "utf8");
5796
5853
  let document;
5797
5854
  try {
5798
5855
  document = parseMarkdownDocument(source);
@@ -5834,12 +5891,12 @@ var checkLinks = async (rootDir, markdownPaths) => {
5834
5891
  };
5835
5892
 
5836
5893
  // src/checks/areas.ts
5837
- import fs14 from "fs/promises";
5894
+ import fs13 from "fs/promises";
5838
5895
  import fg5 from "fast-glob";
5839
5896
  import micromatch5 from "micromatch";
5840
5897
 
5841
5898
  // src/git/files.ts
5842
- import fs12 from "fs/promises";
5899
+ import fs11 from "fs/promises";
5843
5900
  import { execa as execa2 } from "execa";
5844
5901
  import fg3 from "fast-glob";
5845
5902
  import micromatch2 from "micromatch";
@@ -5876,7 +5933,7 @@ var isCurrentContainedFile = async (rootDir, relativePath) => {
5876
5933
  try {
5877
5934
  const absolutePath = resolveRepoPath(rootDir, relativePath);
5878
5935
  await assertRepoContainment(rootDir, absolutePath);
5879
- return (await fs12.stat(absolutePath)).isFile();
5936
+ return (await fs11.stat(absolutePath)).isFile();
5880
5937
  } catch {
5881
5938
  return false;
5882
5939
  }
@@ -5901,7 +5958,7 @@ var discoverRepositoryFilePaths = async (rootDir, ignorePatterns) => {
5901
5958
  };
5902
5959
 
5903
5960
  // src/routing/area-resolver.ts
5904
- import fs13 from "fs/promises";
5961
+ import fs12 from "fs/promises";
5905
5962
  import fg4 from "fast-glob";
5906
5963
  import micromatch3 from "micromatch";
5907
5964
  var unique = (values) => {
@@ -5952,7 +6009,7 @@ var ensureChildPath = async (rootDir, areaFilesRoot, filePath) => {
5952
6009
  var readRouteFile = async (rootDir, filePath) => {
5953
6010
  try {
5954
6011
  return {
5955
- source: await fs13.readFile(resolveRepoPath(rootDir, filePath), "utf8"),
6012
+ source: await fs12.readFile(resolveRepoPath(rootDir, filePath), "utf8"),
5956
6013
  diagnostic: null
5957
6014
  };
5958
6015
  } catch (error) {
@@ -6264,7 +6321,7 @@ var looksLikeGlob2 = (pattern) => {
6264
6321
  };
6265
6322
  var pathExists3 = async (absolutePath) => {
6266
6323
  try {
6267
- await fs14.stat(absolutePath);
6324
+ await fs13.stat(absolutePath);
6268
6325
  return true;
6269
6326
  } catch (error) {
6270
6327
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -6667,7 +6724,7 @@ var checkAreas = async (rootDir, config) => {
6667
6724
  };
6668
6725
 
6669
6726
  // src/checks/decisions.ts
6670
- import fs15 from "fs/promises";
6727
+ import fs14 from "fs/promises";
6671
6728
  import micromatch6 from "micromatch";
6672
6729
  var PRODUCT_CAPABILITY_REQUIRED_HEADINGS = [
6673
6730
  "Capability Promise",
@@ -6783,7 +6840,7 @@ var checkDecisionSections = async (rootDir, config, markdownPaths, truthDocument
6783
6840
  (filePath) => truthDocumentMap.has(filePath) || isDecisionTruthCandidate(config, filePath)
6784
6841
  ).sort();
6785
6842
  for (const filePath of candidatePaths) {
6786
- const source = await fs15.readFile(
6843
+ const source = await fs14.readFile(
6787
6844
  resolveRepoPath(rootDir, filePath),
6788
6845
  "utf8"
6789
6846
  );
@@ -6825,10 +6882,10 @@ var checkDecisionSections = async (rootDir, config, markdownPaths, truthDocument
6825
6882
  };
6826
6883
 
6827
6884
  // src/checks/generated-surfaces.ts
6828
- import fs16 from "fs/promises";
6885
+ import fs15 from "fs/promises";
6829
6886
  var readOptionalFile = async (rootDir, filePath) => {
6830
6887
  try {
6831
- return await fs16.readFile(resolveRepoPath(rootDir, filePath), "utf8");
6888
+ return await fs15.readFile(resolveRepoPath(rootDir, filePath), "utf8");
6832
6889
  } catch (error) {
6833
6890
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
6834
6891
  return null;
@@ -6898,7 +6955,7 @@ import path11 from "path";
6898
6955
  import micromatch7 from "micromatch";
6899
6956
 
6900
6957
  // src/repo-index/file-tree.ts
6901
- import fs17 from "fs/promises";
6958
+ import fs16 from "fs/promises";
6902
6959
  import path8 from "path";
6903
6960
 
6904
6961
  // src/truth/source-references.ts
@@ -7043,7 +7100,7 @@ var discoverRepoFiles = async (rootDir, ignore) => {
7043
7100
  for (const filePath of discoveredFiles) {
7044
7101
  let stat;
7045
7102
  try {
7046
- stat = await fs17.stat(path8.join(rootDir, filePath));
7103
+ stat = await fs16.stat(path8.join(rootDir, filePath));
7047
7104
  } catch (error) {
7048
7105
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
7049
7106
  continue;
@@ -7067,7 +7124,7 @@ var discoverRepoFiles = async (rootDir, ignore) => {
7067
7124
  });
7068
7125
  }
7069
7126
  if (kind === "doc") {
7070
- const source = await fs17.readFile(path8.join(rootDir, filePath), "utf8");
7127
+ const source = await fs16.readFile(path8.join(rootDir, filePath), "utf8");
7071
7128
  const parsed = parseFrontmatter(source);
7072
7129
  const markdown = parseMarkdownDocument(parsed.content);
7073
7130
  const title = markdown.headings.find((heading) => heading.depth === 1)?.text ?? null;
@@ -7095,7 +7152,7 @@ var discoverRepoFiles = async (rootDir, ignore) => {
7095
7152
  };
7096
7153
 
7097
7154
  // src/repo-index/package-metadata.ts
7098
- import fs18 from "fs/promises";
7155
+ import fs17 from "fs/promises";
7099
7156
  import path9 from "path";
7100
7157
  import fg6 from "fast-glob";
7101
7158
  var packageManagerFor = async (rootDir, packageDir) => {
@@ -7108,7 +7165,7 @@ var packageManagerFor = async (rootDir, packageDir) => {
7108
7165
  ];
7109
7166
  for (const [lockfile, manager] of lockfiles) {
7110
7167
  try {
7111
- await fs18.access(path9.join(rootDir, packageDir, lockfile));
7168
+ await fs17.access(path9.join(rootDir, packageDir, lockfile));
7112
7169
  return manager;
7113
7170
  } catch {
7114
7171
  continue;
@@ -7126,7 +7183,7 @@ var discoverPackageMetadata = async (rootDir) => {
7126
7183
  const packages = [];
7127
7184
  for (const packageFile of packageFiles.sort()) {
7128
7185
  const packageDir = path9.posix.dirname(packageFile) === "." ? "" : path9.posix.dirname(packageFile);
7129
- const raw = JSON.parse(await fs18.readFile(path9.join(rootDir, packageFile), "utf8"));
7186
+ const raw = JSON.parse(await fs17.readFile(path9.join(rootDir, packageFile), "utf8"));
7130
7187
  const scripts = raw.scripts && typeof raw.scripts === "object" ? Object.keys(raw.scripts).sort() : [];
7131
7188
  packages.push({
7132
7189
  path: packageFile,
@@ -7231,7 +7288,7 @@ var buildRepoIndex = async (cwd) => {
7231
7288
  import { execa as execa4 } from "execa";
7232
7289
 
7233
7290
  // src/git/changes.ts
7234
- import fs19 from "fs/promises";
7291
+ import fs18 from "fs/promises";
7235
7292
  import path10 from "path";
7236
7293
  import { execa as execa3 } from "execa";
7237
7294
  var normalizePath3 = (filePath) => {
@@ -7243,7 +7300,7 @@ var listChangedPaths = async (cwd, args) => {
7243
7300
  };
7244
7301
  var pathExists4 = async (filePath) => {
7245
7302
  try {
7246
- await fs19.access(filePath);
7303
+ await fs18.access(filePath);
7247
7304
  return true;
7248
7305
  } catch {
7249
7306
  return false;
@@ -7490,13 +7547,13 @@ var checkFreshness = async (rootDir, _config, _truthDocumentPaths, base) => {
7490
7547
  };
7491
7548
 
7492
7549
  // src/evidence/validate.ts
7493
- import fs21 from "fs/promises";
7550
+ import fs20 from "fs/promises";
7494
7551
  import fg7 from "fast-glob";
7495
7552
 
7496
7553
  // src/evidence/parse.ts
7497
- import fs20 from "fs/promises";
7554
+ import fs19 from "fs/promises";
7498
7555
  import path12 from "path";
7499
- import { parse as parse4 } from "yaml";
7556
+ import { parse as parse5 } from "yaml";
7500
7557
  var yamlFencePattern = /```ya?ml\s*\n([\s\S]*?)```/giu;
7501
7558
  var topLevelEvidenceMarkerPattern = /^evidence\s*:/imu;
7502
7559
  var toEvidenceReference = (truthDocPath, raw) => {
@@ -7514,7 +7571,7 @@ var toEvidenceReference = (truthDocPath, raw) => {
7514
7571
  };
7515
7572
  };
7516
7573
  var parseEvidenceReferences = async (rootDir, truthDocPath) => {
7517
- const source = await fs20.readFile(path12.join(rootDir, truthDocPath), "utf8");
7574
+ const source = await fs19.readFile(path12.join(rootDir, truthDocPath), "utf8");
7518
7575
  const parsed = parseFrontmatter(source);
7519
7576
  const references = [];
7520
7577
  for (const entry of parseSourceReferences(source, truthDocPath)) {
@@ -7529,7 +7586,7 @@ var parseEvidenceReferences = async (rootDir, truthDocPath) => {
7529
7586
  if (!topLevelEvidenceMarkerPattern.test(yamlBlock)) {
7530
7587
  continue;
7531
7588
  }
7532
- const block = parse4(yamlBlock);
7589
+ const block = parse5(yamlBlock);
7533
7590
  const rawEvidence = block && typeof block === "object" && "evidence" in block ? block.evidence : null;
7534
7591
  if (!Array.isArray(rawEvidence)) {
7535
7592
  continue;
@@ -7547,7 +7604,7 @@ var parseEvidenceReferences = async (rootDir, truthDocPath) => {
7547
7604
  // src/evidence/validate.ts
7548
7605
  var pathExists5 = async (filePath) => {
7549
7606
  try {
7550
- await fs21.access(filePath);
7607
+ await fs20.access(filePath);
7551
7608
  return true;
7552
7609
  } catch {
7553
7610
  return false;
@@ -7592,7 +7649,7 @@ var validateHash = async (rootDir, reference) => {
7592
7649
  if (!reference.contentHash.startsWith("sha256:")) {
7593
7650
  return diagnosticFor(reference, `Evidence hash for ${reference.path} must use sha256:.`);
7594
7651
  }
7595
- const source = await fs21.readFile(resolveRepoPath(rootDir, reference.path), "utf8");
7652
+ const source = await fs20.readFile(resolveRepoPath(rootDir, reference.path), "utf8");
7596
7653
  const lines = source.split("\n");
7597
7654
  const startLine = reference.startLine ?? 1;
7598
7655
  const endLine = reference.endLine ?? lines.length;
@@ -7606,7 +7663,7 @@ var validateLineSpan = async (rootDir, reference) => {
7606
7663
  if (reference.startLine === void 0 && reference.endLine === void 0) {
7607
7664
  return null;
7608
7665
  }
7609
- const source = await fs21.readFile(resolveRepoPath(rootDir, reference.path), "utf8");
7666
+ const source = await fs20.readFile(resolveRepoPath(rootDir, reference.path), "utf8");
7610
7667
  const lines = source.split("\n");
7611
7668
  const startLine = reference.startLine ?? 1;
7612
7669
  const endLine = reference.endLine ?? lines.length;
@@ -8207,10 +8264,14 @@ var buildWorkflowState = async (cwd, options) => {
8207
8264
  comparisonBase ? { base: comparisonBase } : {}
8208
8265
  );
8209
8266
  const diagnostics = [
8210
- ...loadResult.diagnostics,
8211
- ...repoIndex.diagnostics,
8212
- ...impactSet?.diagnostics ?? [],
8213
- ...checkResult.diagnostics
8267
+ ...new Map(
8268
+ [
8269
+ ...loadResult.diagnostics,
8270
+ ...repoIndex.diagnostics,
8271
+ ...impactSet?.diagnostics ?? [],
8272
+ ...checkResult.diagnostics
8273
+ ].map((diagnostic) => [JSON.stringify(diagnostic), diagnostic])
8274
+ ).values()
8214
8275
  ];
8215
8276
  const applicability = applicabilityFor(
8216
8277
  options.workflow,
@@ -8251,7 +8312,7 @@ var buildWorkflowState = async (cwd, options) => {
8251
8312
  };
8252
8313
 
8253
8314
  // src/cli/handlers.ts
8254
- import fs22 from "fs/promises";
8315
+ import fs21 from "fs/promises";
8255
8316
 
8256
8317
  // src/agents/workflow-helper-validation.ts
8257
8318
  import { parse as parseYaml2 } from "yaml";
@@ -8568,11 +8629,16 @@ var validateWriteLeaseText = (leaseText, changedText) => {
8568
8629
  };
8569
8630
 
8570
8631
  // 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());
8632
+ var runInit2 = async (options = {}) => {
8633
+ const interactive = options.platforms === void 0 && !options.json && process.stdin.isTTY && process.stdout.isTTY;
8634
+ return runInit(process.cwd(), {
8635
+ platforms: options.platforms,
8636
+ selectPlatforms: interactive ? (defaults) => promptForPlatforms({
8637
+ defaults,
8638
+ input: process.stdin,
8639
+ output: process.stdout
8640
+ }) : void 0
8641
+ });
8576
8642
  };
8577
8643
  var runUninstall2 = async (mode) => {
8578
8644
  return runUninstall(process.cwd(), mode);
@@ -8645,9 +8711,22 @@ var invalidWorkflowResult = (command, workflow) => ({
8645
8711
  request: workflow ? { workflow } : {}
8646
8712
  }
8647
8713
  });
8714
+ var summarizeValues = (values) => values.length <= 5 ? values.join(", ") : `${values.slice(0, 5).join(", ")} (+${values.length - 5} more)`;
8715
+ var renderWorkflowStatusSummary = (state) => {
8716
+ const card = state.workflowCard;
8717
+ return [
8718
+ `Truthmark workflow status completed for ${state.workflow}.`,
8719
+ `Applicability: ${state.applicability.state}.`,
8720
+ ...card.affectedFiles.length > 0 ? [`Affected files: ${summarizeValues(card.affectedFiles)}`] : [],
8721
+ ...card.likelyRouteOwners.length > 0 ? [`Likely route owners: ${summarizeValues(card.likelyRouteOwners)}`] : [],
8722
+ ...card.suggestedTruthDocs.length > 0 ? [`Suggested truth docs: ${summarizeValues(card.suggestedTruthDocs)}`] : [],
8723
+ ...card.openQuestions.length > 0 ? [`Open questions: ${summarizeValues(card.openQuestions)}`] : [],
8724
+ ...state.nextSteps.length > 0 ? [`Next steps: ${summarizeValues(state.nextSteps)}`] : []
8725
+ ].join("\n");
8726
+ };
8648
8727
  var readHelperFile = async (filePath, helper) => {
8649
8728
  try {
8650
- return await fs22.readFile(filePath, "utf8");
8729
+ return await fs21.readFile(filePath, "utf8");
8651
8730
  } catch (error) {
8652
8731
  const message = error instanceof Error ? error.message : String(error);
8653
8732
  return { ok: false, helper, errors: [`could not read file: ${message}`] };
@@ -8685,7 +8764,7 @@ var runWorkflowStatus = async (options) => {
8685
8764
  });
8686
8765
  return {
8687
8766
  command: "workflow status",
8688
- summary: `Truthmark workflow status completed for ${options.workflow}.`,
8767
+ summary: renderWorkflowStatusSummary(workflowState),
8689
8768
  diagnostics: workflowState.diagnostics,
8690
8769
  data: {
8691
8770
  request: {
@@ -8740,27 +8819,36 @@ var writeValidationResult = (command, result, options) => {
8740
8819
  var addJsonOption = (command) => {
8741
8820
  return command.option("--json", "Render command output as JSON");
8742
8821
  };
8822
+ var collectPlatform = (value, previous) => [
8823
+ ...previous ?? [],
8824
+ value
8825
+ ];
8743
8826
  var buildProgram = () => {
8744
8827
  const program = new Command();
8745
8828
  program.name("truthmark").description(
8746
8829
  "Git-native, branch-scoped truth workflow installer for local AI coding agents."
8747
8830
  ).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
8831
  addJsonOption(
8759
8832
  program.command("init").description(
8760
8833
  "Initialize Truthmark workflow files in the current repository."
8834
+ ).option(
8835
+ "--platform <id>",
8836
+ "Select a repository agent platform; repeat for multiple platforms",
8837
+ collectPlatform
8838
+ ).option(
8839
+ "--clear-platforms",
8840
+ "Remove all configured repository agent platforms"
8761
8841
  )
8762
8842
  ).action(async (options) => {
8763
- writeResult(await runInit2(), options);
8843
+ if (options.clearPlatforms && options.platform !== void 0) {
8844
+ program.error("truthmark init cannot combine --clear-platforms and --platform");
8845
+ return;
8846
+ }
8847
+ const platforms = options.clearPlatforms ? [] : options.platform;
8848
+ writeResult(
8849
+ await runInit2({ json: options.json, platforms }),
8850
+ options
8851
+ );
8764
8852
  });
8765
8853
  addJsonOption(
8766
8854
  program.command("uninstall").description(