truthmark 1.4.0 → 1.6.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
@@ -334,6 +334,22 @@ var truthmarkConfigSchema = {
334
334
  type: "string"
335
335
  }
336
336
  },
337
+ "truthmark-portal": {
338
+ type: "object",
339
+ additionalProperties: false,
340
+ required: [],
341
+ properties: {
342
+ enabled: {
343
+ type: "boolean"
344
+ },
345
+ output: {
346
+ type: "string"
347
+ },
348
+ template: {
349
+ type: "string"
350
+ }
351
+ }
352
+ },
337
353
  frontmatter: {
338
354
  type: "object",
339
355
  nullable: true,
@@ -391,6 +407,11 @@ var DEFAULT_AUTHORITY = [
391
407
  `${DEFAULT_DOCS_HIERARCHY.roots.truth}/**/*.md`
392
408
  ];
393
409
  var DEFAULT_INSTRUCTION_TARGETS = ["AGENTS.md"];
410
+ var DEFAULT_TRUTHMARK_PORTAL = {
411
+ enabled: false,
412
+ output: "docs/truthmark-portal",
413
+ template: "default"
414
+ };
394
415
  var createDefaultRawConfig = () => ({
395
416
  version: 1,
396
417
  platforms: [...DEFAULT_PLATFORMS],
@@ -422,6 +443,7 @@ var createDefaultConfig = () => ({
422
443
  },
423
444
  authority: [...DEFAULT_AUTHORITY],
424
445
  instructionTargets: [...DEFAULT_INSTRUCTION_TARGETS],
446
+ truthmarkPortal: { ...DEFAULT_TRUTHMARK_PORTAL },
425
447
  frontmatter: {
426
448
  required: [],
427
449
  recommended: ["status", "doc_type", "last_reviewed", "source_of_truth"]
@@ -578,9 +600,9 @@ var parseTruthDocumentsFromYaml = (sectionLines, areaName) => {
578
600
  const diagnostics = [];
579
601
  const truthDocumentEntries = [];
580
602
  for (const rawEntry of rawEntries) {
581
- const path12 = rawEntry && typeof rawEntry === "object" && "path" in rawEntry ? rawEntry.path : null;
603
+ const path13 = rawEntry && typeof rawEntry === "object" && "path" in rawEntry ? rawEntry.path : null;
582
604
  const kind = rawEntry && typeof rawEntry === "object" && "kind" in rawEntry ? rawEntry.kind : null;
583
- if (typeof path12 !== "string" || path12.trim().length === 0 || !isTruthDocumentKind(kind)) {
605
+ if (typeof path13 !== "string" || path13.trim().length === 0 || !isTruthDocumentKind(kind)) {
584
606
  diagnostics.push(
585
607
  createAreaDiagnostic(
586
608
  `Area ${areaName} truth_documents entries must include non-empty path and valid kind fields.`,
@@ -590,7 +612,7 @@ var parseTruthDocumentsFromYaml = (sectionLines, areaName) => {
590
612
  continue;
591
613
  }
592
614
  truthDocumentEntries.push({
593
- path: path12.trim(),
615
+ path: path13.trim(),
594
616
  kind,
595
617
  kindSource: "explicit"
596
618
  });
@@ -1160,6 +1182,7 @@ import fs7 from "fs/promises";
1160
1182
 
1161
1183
  // src/config/load.ts
1162
1184
  import fs4 from "fs/promises";
1185
+ import path4 from "path";
1163
1186
  import { Ajv } from "ajv";
1164
1187
  import { parse as parse2 } from "yaml";
1165
1188
  var ajv = new Ajv({ allErrors: true });
@@ -1172,6 +1195,69 @@ var toConfigDiagnostic = (message, file) => {
1172
1195
  file
1173
1196
  };
1174
1197
  };
1198
+ var normalizeRepoRelativePath = (value) => {
1199
+ const slashNormalized = value.replace(/\\/gu, "/");
1200
+ const pathNormalized = path4.posix.normalize(slashNormalized).replace(/\/+$/u, "");
1201
+ return pathNormalized;
1202
+ };
1203
+ var isUnsafeRepoRelativePath = (value) => {
1204
+ const slashNormalized = value.replace(/\\/gu, "/");
1205
+ const normalized = normalizeRepoRelativePath(value);
1206
+ const parts = slashNormalized.split("/");
1207
+ 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("..");
1208
+ };
1209
+ var pathsOverlap = (left, right) => {
1210
+ const normalizedLeft = normalizeRepoRelativePath(left);
1211
+ const normalizedRight = normalizeRepoRelativePath(right);
1212
+ return normalizedLeft === normalizedRight || normalizedLeft.startsWith(`${normalizedRight}/`) || normalizedRight.startsWith(`${normalizedLeft}/`);
1213
+ };
1214
+ var portalForbiddenOutputRoots = (rawConfig) => {
1215
+ const rawDocs = rawConfig.docs;
1216
+ const docsRoots = rawDocs?.roots ?? {};
1217
+ const routing = rawDocs?.routing ?? DEFAULT_DOCS_HIERARCHY.routing;
1218
+ return [
1219
+ "src",
1220
+ DEFAULT_DOCS_HIERARCHY.roots.ai,
1221
+ DEFAULT_DOCS_HIERARCHY.roots.standards,
1222
+ DEFAULT_DOCS_HIERARCHY.roots.architecture,
1223
+ DEFAULT_DOCS_HIERARCHY.roots.truth,
1224
+ ...Object.values(docsRoots),
1225
+ routing.root_index,
1226
+ routing.area_files_root,
1227
+ ".truthmark/config.yml",
1228
+ "AGENTS.md",
1229
+ "CLAUDE.md",
1230
+ "GEMINI.md",
1231
+ ".github/copilot-instructions.md",
1232
+ ...rawConfig.instruction_targets ?? DEFAULT_INSTRUCTION_TARGETS
1233
+ ];
1234
+ };
1235
+ var validatePortalConfig = (rawConfig, configPath) => {
1236
+ const portal = rawConfig["truthmark-portal"];
1237
+ if (portal === void 0) {
1238
+ return [];
1239
+ }
1240
+ const diagnostics = [];
1241
+ const output = portal.output ?? DEFAULT_TRUTHMARK_PORTAL.output;
1242
+ const template = portal.template ?? DEFAULT_TRUTHMARK_PORTAL.template;
1243
+ if (isUnsafeRepoRelativePath(output) || portalForbiddenOutputRoots(rawConfig).some((forbidden) => pathsOverlap(output, forbidden))) {
1244
+ diagnostics.push(
1245
+ toConfigDiagnostic(
1246
+ "truthmark-portal.output must be a non-empty repo-relative directory that does not overlap source, instruction, routing, or canonical docs roots.",
1247
+ configPath
1248
+ )
1249
+ );
1250
+ }
1251
+ if (template !== "default" && isUnsafeRepoRelativePath(template)) {
1252
+ diagnostics.push(
1253
+ toConfigDiagnostic(
1254
+ "truthmark-portal.template must be 'default' or a non-empty repo-relative template path without absolute or parent traversal segments.",
1255
+ configPath
1256
+ )
1257
+ );
1258
+ }
1259
+ return diagnostics;
1260
+ };
1175
1261
  var normalizeConfig = (rawConfig) => {
1176
1262
  const rawDocs = rawConfig.docs ?? {
1177
1263
  layout: DEFAULT_DOCS_HIERARCHY.layout,
@@ -1194,6 +1280,11 @@ var normalizeConfig = (rawConfig) => {
1194
1280
  },
1195
1281
  authority: rawConfig.authority,
1196
1282
  instructionTargets: rawConfig.instruction_targets ?? [...DEFAULT_INSTRUCTION_TARGETS],
1283
+ truthmarkPortal: {
1284
+ enabled: rawConfig["truthmark-portal"]?.enabled ?? DEFAULT_TRUTHMARK_PORTAL.enabled,
1285
+ output: rawConfig["truthmark-portal"]?.output ?? DEFAULT_TRUTHMARK_PORTAL.output,
1286
+ template: rawConfig["truthmark-portal"]?.template ?? DEFAULT_TRUTHMARK_PORTAL.template
1287
+ },
1197
1288
  frontmatter: {
1198
1289
  required: rawConfig.frontmatter?.required ?? [],
1199
1290
  recommended: rawConfig.frontmatter?.recommended ?? []
@@ -1247,6 +1338,18 @@ var loadConfig = async (rootDir) => {
1247
1338
  configPath
1248
1339
  };
1249
1340
  }
1341
+ const portalDiagnostics = validatePortalConfig(
1342
+ parsedConfig,
1343
+ configPath
1344
+ );
1345
+ if (portalDiagnostics.length > 0) {
1346
+ return {
1347
+ status: "invalid",
1348
+ config: null,
1349
+ diagnostics: portalDiagnostics,
1350
+ configPath
1351
+ };
1352
+ }
1250
1353
  return {
1251
1354
  status: "loaded",
1252
1355
  config: normalizeConfig(parsedConfig),
@@ -1409,13 +1512,13 @@ var DECISION_TRUTH_INSTRUCTIONS = [
1409
1512
  "Update Product Decisions and Rationale when a decision changes behavior."
1410
1513
  ].join("\n");
1411
1514
  var EVIDENCE_AUTHORITY_INSTRUCTIONS = [
1412
- "Repository instruction docs such as docs/ai/repo-rules.md remain instruction authority.",
1515
+ "Repository instruction files and explicitly configured policy docs remain instruction authority when present; do not assume a repository uses any particular policy path.",
1413
1516
  "Implementation code and canonical truth docs are inspected evidence for current behavior; they do not silently override workflow write boundaries."
1414
1517
  ].join("\n");
1415
1518
  var REPOSITORY_INTELLIGENCE_INSTRUCTIONS = [
1416
1519
  "Repository intelligence artifacts are optional derived context: RepoIndex, RouteMap, ImpactSet, and ContextPack may guide routing, context selection, and verification planning when available.",
1417
1520
  "They do not override checkout evidence, canonical truth docs, route files, or workflow write boundaries.",
1418
- "If unavailable, inspect .truthmark/config.yml, route files, source files, truth docs, and tests directly, then report that repository-intelligence artifacts were not generated."
1521
+ "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."
1419
1522
  ].join("\n");
1420
1523
  var FEATURE_DOC_TEMPLATE_INSTRUCTIONS = [
1421
1524
  "When creating or updating a truth doc, inspect the routed truth kind and use the matching `docs/templates/<kind>-doc.md` template.",
@@ -1567,17 +1670,38 @@ var renderCopilotCustomAgentModeSection = (agents, parentRule, writeAgents = [])
1567
1670
  `- ${parentRule}`
1568
1671
  ].join("\n");
1569
1672
  };
1673
+ var renderGeminiSubagentModeSection = (agents, parentRule, writeAgents = []) => {
1674
+ const mentions = agents.map((agent) => `@${agent.replace(/_/gu, "-")}`);
1675
+ const writeMentions = writeAgents.map((agent) => `@${agent.replace(/_/gu, "-")}`);
1676
+ const writeAgentLines = writeMentions.length > 0 ? [
1677
+ `- dispatch write-capable project subagents only with explicit write leases: ${writeMentions.join(", ")}`,
1678
+ "- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
1679
+ "- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
1680
+ "- parent must inspect the actual checkout diff against each lease before accepting a worker report"
1681
+ ] : [];
1682
+ const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
1683
+ const readOnlySubagentLabel = writeAgents.length > 0 ? "read-only subagents" : "subagents";
1684
+ return [
1685
+ "Gemini CLI subagent mode:",
1686
+ "- use automatically when this workflow runs in Gemini CLI and the parent agent chooses bounded project subagent fan-out",
1687
+ `- dispatch read-only project subagents ${readOnlyScope}: ${mentions.join(", ")}`,
1688
+ `- ${readOnlySubagentLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
1689
+ `- parent supplies bounded evidence shards; ${readOnlySubagentLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
1690
+ ...writeAgentLines,
1691
+ `- ${parentRule}`
1692
+ ].join("\n");
1693
+ };
1570
1694
  var defaultAgentConfig = () => {
1571
1695
  return createDefaultConfig();
1572
1696
  };
1573
1697
  var renderHierarchySummary = (config) => {
1574
1698
  const truthRoot3 = resolveTruthDocsRoot(config);
1575
1699
  return [
1576
- "Truthmark hierarchy:",
1577
- "- Config: .truthmark/config.yml",
1578
- `- Root route index: ${config.docs.routing.rootIndex}`,
1579
- `- Area route files: ${config.docs.routing.areaFilesRoot}/**/*.md`,
1580
- `- Truth docs: ${truthRoot3}/**/*.md`
1700
+ "Truthmark hierarchy hints:",
1701
+ "- Config, when present: .truthmark/config.yml",
1702
+ `- Root route index, when present: ${config.docs.routing.rootIndex}`,
1703
+ `- Area route files, when present: ${config.docs.routing.areaFilesRoot}/**/*.md`,
1704
+ `- Truth docs, when present: ${truthRoot3}/**/*.md`
1581
1705
  ].join("\n");
1582
1706
  };
1583
1707
 
@@ -1593,9 +1717,10 @@ var TRUTHMARK_BLOCK_START = "<!-- truthmark:start -->";
1593
1717
  var TRUTHMARK_BLOCK_END = "<!-- truthmark:end -->";
1594
1718
  var renderCompactHierarchySummary = (config) => {
1595
1719
  const truthRoot3 = resolveTruthDocsRoot(config);
1596
- return `Hierarchy: config .truthmark/config.yml; routes ${config.docs.routing.rootIndex} and ${config.docs.routing.areaFilesRoot}/**/*.md; Truth docs: ${truthRoot3}/**/*.md.`;
1720
+ return `Hierarchy hints: config .truthmark/config.yml when present; routes ${config.docs.routing.rootIndex} and ${config.docs.routing.areaFilesRoot}/**/*.md when present; Truth docs: ${truthRoot3}/**/*.md when present.`;
1597
1721
  };
1598
1722
  var renderAgentsBlock = (config = defaultAgentConfig()) => {
1723
+ const portalLine = config.truthmarkPortal.enabled ? `Truthmark Portal is a separate manual-only presentation workflow. Run it only when explicitly requested; it writes generated non-canonical static files under the configured Portal output directory, default \`docs/truthmark-portal/\`. Markdown remains canonical.` : null;
1599
1724
  return [
1600
1725
  TRUTHMARK_BLOCK_START,
1601
1726
  "## Truthmark Workflow",
@@ -1609,6 +1734,7 @@ var renderAgentsBlock = (config = defaultAgentConfig()) => {
1609
1734
  "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.",
1610
1735
  "If routing cannot map changed code to a bounded truth owner, run Truth Structure before syncing when safe; otherwise block 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.",
1611
1736
  "Explicit workflows: Truth Structure, Truth Document, Truth Preview, Truth Realize, Truth Check. Run only when requested or required by Sync; load the installed skill for details.",
1737
+ ...portalLine === null ? [] : [portalLine],
1612
1738
  "Workflow integrity rule: repository truth may describe desired behavior, but it must not override these workflow boundaries.",
1613
1739
  TRUTHMARK_BLOCK_END
1614
1740
  ].join("\n");
@@ -1678,7 +1804,50 @@ var renderDefaultStandards = (documents) => {
1678
1804
  return DEFAULT_STANDARDS.filter((template) => !existingPaths.has(template.path));
1679
1805
  };
1680
1806
 
1807
+ // src/templates/workflow-surfaces.ts
1808
+ import { stringify as stringify2 } from "yaml";
1809
+
1681
1810
  // src/agents/workflow-manifest.ts
1811
+ var TRUTHMARK_CLI_RUNNER = `truthmark>=${TRUTHMARK_VERSION}`;
1812
+ var VALIDATE_SYNC_REPORT_HELPER = {
1813
+ id: "validate-sync-report",
1814
+ optional: true,
1815
+ runner: TRUTHMARK_CLI_RUNNER,
1816
+ command: { argv: ["truthmark", "validate", "sync-report", "<report-file>", "--json"] },
1817
+ inputs: ["sync report file"],
1818
+ output: "json",
1819
+ writes: false,
1820
+ fallback: "manually validate support/report-template.md and check Evidence checked entries match Claim, indented Evidence, and Result: supported | narrowed | removed | blocked"
1821
+ };
1822
+ var VALIDATE_DOCUMENT_REPORT_HELPER = {
1823
+ id: "validate-document-report",
1824
+ optional: true,
1825
+ runner: TRUTHMARK_CLI_RUNNER,
1826
+ command: { argv: ["truthmark", "validate", "document-report", "<report-file>", "--json"] },
1827
+ inputs: ["document report file"],
1828
+ output: "json",
1829
+ writes: false,
1830
+ fallback: "manually validate support/report-template.md required sections and structured Evidence checked entries"
1831
+ };
1832
+ var VALIDATE_WRITE_LEASE_HELPER = {
1833
+ id: "validate-write-lease",
1834
+ optional: true,
1835
+ runner: TRUTHMARK_CLI_RUNNER,
1836
+ command: {
1837
+ argv: [
1838
+ "truthmark",
1839
+ "validate",
1840
+ "write-lease",
1841
+ "<lease-or-report-file>",
1842
+ "<changed-files-file>",
1843
+ "--json"
1844
+ ]
1845
+ },
1846
+ inputs: ["lease or worker report yaml", "changed file list"],
1847
+ output: "json",
1848
+ writes: false,
1849
+ fallback: "manually compare declared allowedWrites and forbiddenWrites with the actual changed files"
1850
+ };
1682
1851
  var TRUTHMARK_WORKFLOW_MANIFEST = {
1683
1852
  "truthmark-sync": {
1684
1853
  id: "truthmark-sync",
@@ -1717,10 +1886,12 @@ var TRUTHMARK_WORKFLOW_MANIFEST = {
1717
1886
  "Truth docs updated",
1718
1887
  "Truth docs split",
1719
1888
  "Evidence checked",
1889
+ "Helper scripts",
1720
1890
  "Notes"
1721
1891
  ],
1722
1892
  subagents: ["truth_route_auditor", "truth_claim_verifier"],
1723
- writeSubagents: ["truth_doc_writer"]
1893
+ writeSubagents: ["truth_doc_writer"],
1894
+ helpers: [VALIDATE_SYNC_REPORT_HELPER, VALIDATE_WRITE_LEASE_HELPER]
1724
1895
  },
1725
1896
  "truthmark-structure": {
1726
1897
  id: "truthmark-structure",
@@ -1801,10 +1972,12 @@ var TRUTHMARK_WORKFLOW_MANIFEST = {
1801
1972
  "Truth docs restructured",
1802
1973
  "Routing updated",
1803
1974
  "Evidence checked",
1975
+ "Helper scripts",
1804
1976
  "Notes"
1805
1977
  ],
1806
1978
  subagents: ["truth_route_auditor", "truth_claim_verifier"],
1807
- writeSubagents: ["truth_doc_writer"]
1979
+ writeSubagents: ["truth_doc_writer"],
1980
+ helpers: [VALIDATE_DOCUMENT_REPORT_HELPER, VALIDATE_WRITE_LEASE_HELPER]
1808
1981
  },
1809
1982
  "truthmark-realize": {
1810
1983
  id: "truthmark-realize",
@@ -1902,6 +2075,50 @@ var TRUTHMARK_WORKFLOW_MANIFEST = {
1902
2075
  "truth_claim_verifier",
1903
2076
  "truth_doc_reviewer"
1904
2077
  ]
2078
+ },
2079
+ "truthmark-portal": {
2080
+ id: "truthmark-portal",
2081
+ displayName: "Truthmark Portal",
2082
+ description: "Use when the user explicitly asks to generate, refresh, or update the Truthmark Portal static HTML site. Not for code change sync, route repair, truth validation/checking, documenting behavior, realizing docs into code, or machine-readable agent context.",
2083
+ shortDescription: "Generate a committed static HTML Truthmark Portal",
2084
+ defaultPrompt: "Use $truthmark-portal only when explicitly asked to generate or refresh the committed static HTML Portal.",
2085
+ allowImplicitInvocation: false,
2086
+ positiveTriggers: [
2087
+ "generate the Truthmark Portal",
2088
+ "refresh the committed HTML docs site",
2089
+ "create a browsable project map from Truthmark docs",
2090
+ "update docs/truthmark-portal",
2091
+ "make a human-readable static site from the truth docs"
2092
+ ],
2093
+ negativeTriggers: [
2094
+ "code change sync",
2095
+ "route ownership repair",
2096
+ "truth validation or checking",
2097
+ "document implemented behavior",
2098
+ "realize docs into code",
2099
+ "machine-readable agent context"
2100
+ ],
2101
+ forbiddenAdjacency: [
2102
+ "must not run as a completion gate",
2103
+ "must not replace Truth Sync, Truth Check, Truth Document, Truth Realize, or Truth Structure",
2104
+ "must not write outside the configured Portal output directory unless the user changes scope"
2105
+ ],
2106
+ requiredGates: [
2107
+ "manual-only invocation",
2108
+ "Portal output containment",
2109
+ "Markdown canonical statement",
2110
+ "source provenance"
2111
+ ],
2112
+ allowedWrites: ["configured Portal output directory only"],
2113
+ reportSections: [
2114
+ "Output path",
2115
+ "Page count",
2116
+ "Diagrams/assets",
2117
+ "Source docs reviewed",
2118
+ "Skipped/ambiguous docs",
2119
+ "Validation",
2120
+ "Markdown canonical statement"
2121
+ ]
1905
2122
  }
1906
2123
  };
1907
2124
  var TRUTHMARK_WORKFLOW_IDS = Object.keys(
@@ -1932,7 +2149,7 @@ Fixes suggested:
1932
2149
  ${renderAuditEvidenceCheckedSection([
1933
2150
  {
1934
2151
  finding: "The root route index is present and maps repository truth owners.",
1935
- evidence: [".truthmark/config.yml:1", `${rootRouteIndex}:1`],
2152
+ evidence: [`${rootRouteIndex}:1`],
1936
2153
  suggestedFix: "none",
1937
2154
  confidence: "high"
1938
2155
  }
@@ -1984,11 +2201,11 @@ Invocations: ${TRUTH_CHECK_EXPLICIT_INVOCATIONS}
1984
2201
 
1985
2202
  Truth Check is agent-led:
1986
2203
 
1987
- - inspect .truthmark/config.yml, ${config.docs.routing.rootIndex}, canonical docs, and relevant implementation directly
2204
+ - inspect .truthmark/config.yml and configured route files only when they exist; then inspect canonical docs and relevant implementation directly
1988
2205
  - ${EVIDENCE_AUTHORITY_INSTRUCTIONS}
1989
- - inspect the configured root route index at ${config.docs.routing.rootIndex} and relevant child route files under ${config.docs.routing.areaFilesRoot}/
2206
+ - inspect the configured root route index at ${config.docs.routing.rootIndex} and relevant child route files under ${config.docs.routing.areaFilesRoot}/ when they exist
1990
2207
  - check that current docs describe current code rather than historical plans
1991
- - check that ${config.docs.routing.rootIndex} routes code surfaces to canonical truth docs
2208
+ - check that route files map code surfaces to canonical truth docs when route files exist
1992
2209
  - check for broad, catch-all, index-like, or mixed-owner truth docs and report them as topology issues requiring Truth Structure
1993
2210
  - check that canonical behavior docs keep active Product Decisions and Rationale sections
1994
2211
  - optionally run truthmark check when local tooling is available
@@ -2012,11 +2229,15 @@ var renderMarkdownExample2 = (content) => {
2012
2229
  var TRUTH_DOCUMENT_EXPLICIT_INVOCATIONS = "OpenCode /skill truthmark-document; Codex /truthmark-document or $truthmark-document; Claude Code /truthmark-document; GitHub Copilot /truthmark-document; Gemini CLI /truthmark:document.";
2013
2230
  var renderTruthDocumentReportExample = (config = defaultAgentConfig()) => {
2014
2231
  const truthDocsRoot = resolveTruthDocsRoot(config);
2232
+ const helperScripts = ["validate-write-lease: skipped, no write lease used"];
2015
2233
  return `Truth Document: completed
2016
2234
 
2017
2235
  Implementation reviewed:
2018
2236
  - src/routing/area-resolver.ts
2019
2237
 
2238
+ Ownership reviewed:
2239
+ - ${config.docs.routing.rootIndex}
2240
+
2020
2241
  Truth docs created:
2021
2242
  - ${truthDocsRoot}/contracts.md
2022
2243
 
@@ -2040,6 +2261,9 @@ ${renderClaimEvidenceCheckedSection([
2040
2261
  }
2041
2262
  ])}
2042
2263
 
2264
+ Helper scripts:
2265
+ ${helperScripts.map((helperScript) => `- ${helperScript}`).join("\n")}
2266
+
2043
2267
  Notes:
2044
2268
  - Documented routing and behavior from route handlers and tests.`;
2045
2269
  };
@@ -2086,7 +2310,7 @@ Invocations: ${TRUTH_DOCUMENT_EXPLICIT_INVOCATIONS}
2086
2310
  Truth Document is manual and implementation-first:
2087
2311
 
2088
2312
  - run only when the user explicitly asks to generate or update truth docs for existing behavior, or when Truth Sync, Truth Check, or Truth Structure reports implemented behavior that lacks canonical truth docs
2089
- - inspect .truthmark/config.yml, ${config.docs.routing.rootIndex}, relevant child route files under ${config.docs.routing.areaFilesRoot}/, existing canonical docs, implementation code, and tests directly
2313
+ - inspect .truthmark/config.yml and configured route files only when they exist; then inspect existing canonical docs, implementation code, and tests directly
2090
2314
  - ${EVIDENCE_AUTHORITY_INSTRUCTIONS}
2091
2315
  - document current implemented behavior; do not invent future behavior or planned endpoints
2092
2316
  - may write canonical truth docs and ${config.docs.routing.rootIndex} or relevant child route files only
@@ -2115,6 +2339,12 @@ ${renderTruthDocRestructureGateSection(
2115
2339
  ${ARCHITECTURE_DOC_BOUNDARY_INSTRUCTIONS}
2116
2340
  ${renderHierarchySummary(config)}
2117
2341
  ${DECISION_TRUTH_INSTRUCTIONS}
2342
+ Helper status reporting:
2343
+ - Validate the report body before adding this validator's own success status; the body may omit \`validate-document-report\` while validation is pending.
2344
+ - After \`truthmark validate document-report <report-file> --json\` returns \`data.validation.ok: true\`, append or update \`validate-document-report: ran, passed\` in the final report.
2345
+ - If the installed Truthmark CLI is unavailable or the helper is skipped, record \`validate-document-report: skipped, <reason>\` and manually validate the report shape.
2346
+ - Record \`validate-write-lease: ran, passed\` only after validating a concrete write lease; otherwise use a truthful skipped status such as \`skipped, no write lease used\`.
2347
+ - Helper output is derived evidence and never replaces direct checkout inspection, evidence review, or parent acceptance.
2118
2348
  Parent post-document verification:
2119
2349
  - verify only truth docs and leased truth routing files changed during document work
2120
2350
  - block on functional code, generated host surfaces, or unrelated diffs caused by document work
@@ -2189,9 +2419,9 @@ Purpose:
2189
2419
  - keep the selector thin so agents can avoid loading or acting through heavier workflows prematurely
2190
2420
 
2191
2421
  Read:
2192
- - .truthmark/config.yml
2193
- - ${config.docs.routing.rootIndex}
2194
- - relevant child route files under ${config.docs.routing.areaFilesRoot}/
2422
+ - .truthmark/config.yml, only when present
2423
+ - ${config.docs.routing.rootIndex}, only when present
2424
+ - relevant child route files under ${config.docs.routing.areaFilesRoot}/, only when present
2195
2425
  - relevant truth docs and implementation files needed to preview ownership
2196
2426
  - ${EVIDENCE_AUTHORITY_INSTRUCTIONS}
2197
2427
 
@@ -2216,6 +2446,82 @@ Report completion in this shape:
2216
2446
  ${renderMarkdownExample3(renderTruthPreviewReportExample(config))}`;
2217
2447
  };
2218
2448
 
2449
+ // src/agents/truthmark-portal.ts
2450
+ var TRUTHMARK_PORTAL_EXPLICIT_INVOCATIONS = "OpenCode /skill truthmark-portal; Codex /truthmark-portal or $truthmark-portal; Claude Code /truthmark-portal; GitHub Copilot /truthmark-portal; Gemini CLI /truthmark:portal.";
2451
+ var renderTruthmarkPortalSkillBody = (config = defaultAgentConfig()) => {
2452
+ const workflow = getTruthmarkWorkflow("truthmark-portal");
2453
+ const output = config.truthmarkPortal.output;
2454
+ const template = config.truthmarkPortal.template;
2455
+ return `---
2456
+ name: truthmark-portal
2457
+ description: ${workflow.description}
2458
+ argument-hint: Optional output path, template, or portal generation focus
2459
+ user-invocable: true
2460
+ truthmark-version: ${TRUTHMARK_VERSION}
2461
+ ---
2462
+
2463
+ # Truthmark Portal
2464
+
2465
+ Truthmark Portal is a manual-only presentation workflow. It is never a completion gate, never Truth Sync, and runs only when the user explicitly asks to generate, refresh, or update the committed static HTML Portal.
2466
+
2467
+ Invocations: ${TRUTHMARK_PORTAL_EXPLICIT_INVOCATIONS}
2468
+
2469
+ Core rules:
2470
+
2471
+ - Markdown remains canonical; generated HTML is presentation only.
2472
+ - Read Markdown directly from the checkout; the workflow does not require the truthmark CLI or package.
2473
+ - truthmark check/index may be used only as optional supporting evidence when available.
2474
+ - Default output is docs/truthmark-portal; configured output is ${output}.
2475
+ - Configured template is ${template}; use default built-in template instructions when the template is default.
2476
+ - The workflow may replace the entire output directory, but writes are limited to the configured Portal output directory only unless the user changes scope.
2477
+ - Portal writes are generated non-canonical static files for human browsing.
2478
+ - Generate a committed multi-page static HTML site with local CSS, JavaScript, assets, and search metadata under the output directory.
2479
+ - Use no remote dependencies by default: no remote scripts, analytics, fonts, CSS, or CDN assets.
2480
+ - Include source provenance and the Markdown canonical disclaimer on every page.
2481
+ - Store manifest and search data under output/assets only.
2482
+ - There is no .truthmark/index.json dependency; do not require or create it as infrastructure.
2483
+ - Pictures and screenshots require an explicit user or template request.
2484
+
2485
+ Workflow:
2486
+
2487
+ 1. Confirm the user explicitly requested Portal generation or refresh.
2488
+ 2. Inspect .truthmark/config.yml and configured route docs only when they exist; read repository instruction files when present, truth docs, architecture docs, standards docs, and the configured Portal template when it is a repo-relative file.
2489
+ 3. Validate the selected output path is repo-relative, non-empty, inside the repository, and does not overlap canonical docs, source roots, routing files, or instruction targets.
2490
+ 4. Plan the generated page inventory, diagrams/assets, source docs reviewed, and skipped or ambiguous docs.
2491
+ 5. Replace or write only under ${output}; do not edit canonical Markdown, routing, source code, or instruction files unless the user explicitly changes scope.
2492
+ 6. Generate the multi-page static site with local assets/search metadata and visible source provenance.
2493
+ 7. Validate entry page, links where practical, provenance/disclaimers, local-only assets, and that metadata remains under ${output}/assets.
2494
+ ${renderHierarchySummary(config)}
2495
+
2496
+ Report completion in this shape:
2497
+
2498
+ \`\`\`md
2499
+ Truthmark Portal: completed
2500
+
2501
+ Output path:
2502
+ - ${output}
2503
+
2504
+ Page count:
2505
+ - <count>
2506
+
2507
+ Diagrams/assets:
2508
+ - <generated diagrams/assets or none>
2509
+
2510
+ Source docs reviewed:
2511
+ - <source markdown paths>
2512
+
2513
+ Skipped/ambiguous docs:
2514
+ - <paths and reason, or none>
2515
+
2516
+ Validation:
2517
+ - <checks performed>
2518
+
2519
+ Markdown canonical statement:
2520
+ - Markdown remains canonical; generated Portal HTML is non-canonical presentation only.
2521
+ \`\`\`
2522
+ `;
2523
+ };
2524
+
2219
2525
  // src/agents/truth-structure.ts
2220
2526
  var renderMarkdownExample4 = (content) => {
2221
2527
  return ["```md", content, "```"].join("\n");
@@ -2280,9 +2586,9 @@ truthmark-version: ${TRUTHMARK_VERSION}
2280
2586
  Use this skill to design or repair Truthmark area structure.
2281
2587
  Invocations: ${TRUTH_STRUCTURE_EXPLICIT_INVOCATIONS}
2282
2588
  Truth Structure is agent-native:
2283
- - inspect repository layout, current docs, .truthmark/config.yml, ${config.docs.routing.rootIndex}, and relevant code directly
2589
+ - inspect repository layout, current docs, Truthmark config and route files when present, and relevant code directly
2284
2590
  - ${EVIDENCE_AUTHORITY_INSTRUCTIONS}
2285
- - inspect the configured root route index at ${config.docs.routing.rootIndex} and relevant child route files under ${config.docs.routing.areaFilesRoot}/
2591
+ - inspect the configured root route index at ${config.docs.routing.rootIndex} and relevant child route files under ${config.docs.routing.areaFilesRoot}/ when they exist
2286
2592
  - define areas by product or behavior ownership, not by mechanical directory mirroring
2287
2593
  - create or repair ${config.docs.routing.rootIndex}
2288
2594
  - create starter truth docs when useful and when they belong in the canonical current-truth surface
@@ -2353,7 +2659,7 @@ ${ARCHITECTURE_DOC_BOUNDARY_INSTRUCTIONS}
2353
2659
  Portable fallback:
2354
2660
  - If this skill surface is unavailable, perform the same workflow directly from committed repository files.
2355
2661
  - Do not require the truthmark CLI.
2356
- - Read .truthmark/config.yml, ${config.docs.routing.rootIndex}, relevant child route files under ${config.docs.routing.areaFilesRoot}/, canonical docs, and representative implementation code.
2662
+ - Inspect .truthmark/config.yml and configured route files only when they exist; then inspect canonical docs and representative implementation code.
2357
2663
  - Use a subagent only when the host supports that pattern; otherwise perform the topology repair inline.
2358
2664
  ${renderHierarchySummary(config)}
2359
2665
  ${DECISION_TRUTH_INSTRUCTIONS}
@@ -2366,24 +2672,105 @@ var renderBulletSection = (title, items) => {
2366
2672
  return `${title}:
2367
2673
  ${items.map((item) => `- ${item}`).join("\n")}`;
2368
2674
  };
2675
+ var findSection = (source, title) => {
2676
+ return source.split("\n\n").find((candidate) => candidate.startsWith(`${title}:
2677
+ `));
2678
+ };
2679
+ var parseBulletLines = (section) => {
2680
+ return section.split("\n").slice(1).map((line) => {
2681
+ const match = line.match(/^-\s+(.*)$/);
2682
+ return match?.[1];
2683
+ }).filter((line) => line !== void 0);
2684
+ };
2685
+ var parseBulletSection = (source, title) => {
2686
+ const section = findSection(source, title);
2687
+ if (!section) {
2688
+ return [];
2689
+ }
2690
+ return parseBulletLines(section);
2691
+ };
2692
+ var parseOptionalBulletSection = (source, title) => {
2693
+ const section = findSection(source, title);
2694
+ if (!section) {
2695
+ return void 0;
2696
+ }
2697
+ return parseBulletLines(section);
2698
+ };
2699
+ var isClaimEvidenceResult = (value) => {
2700
+ return ["supported", "narrowed", "removed", "blocked"].includes(value);
2701
+ };
2702
+ var hasContent = (value) => value.trim().length > 0;
2703
+ var parseEvidenceCheckedSection = (source) => {
2704
+ const section = source.split("\n\n").find((candidate) => candidate.startsWith("Evidence checked:\n"));
2705
+ if (!section) {
2706
+ throw new Error("Evidence checked section is required.");
2707
+ }
2708
+ const lines = section.split("\n").slice(1);
2709
+ const items = [];
2710
+ for (let index = 0; index < lines.length; index += 3) {
2711
+ const claimLine = lines[index];
2712
+ const evidenceLine = lines[index + 1];
2713
+ const resultLine = lines[index + 2];
2714
+ if (!claimLine?.startsWith("- Claim: ") || !evidenceLine?.startsWith(" Evidence: ") || !resultLine?.startsWith(" Result: ")) {
2715
+ throw new Error("Evidence checked entries must include Claim, Evidence, and Result fields.");
2716
+ }
2717
+ const result = resultLine.slice(" Result: ".length);
2718
+ const claim = claimLine.slice("- Claim: ".length).trim();
2719
+ const evidence = evidenceLine.slice(" Evidence: ".length).split(" / ").map((value) => value.trim());
2720
+ if (!hasContent(claim)) {
2721
+ throw new Error("Evidence checked claim is required.");
2722
+ }
2723
+ if (evidence.length === 0 || evidence.some((value) => !hasContent(value))) {
2724
+ throw new Error("Evidence checked evidence is required.");
2725
+ }
2726
+ if (!isClaimEvidenceResult(result)) {
2727
+ throw new Error("Evidence checked result is invalid.");
2728
+ }
2729
+ items.push({
2730
+ claim,
2731
+ evidence,
2732
+ result
2733
+ });
2734
+ }
2735
+ return items;
2736
+ };
2369
2737
  var renderTruthSyncCompletedReport = (input) => {
2370
2738
  return [
2371
2739
  "Truth Sync: completed",
2372
2740
  renderBulletSection("Changed code reviewed", input.changedCode),
2741
+ renderBulletSection("Ownership reviewed", input.ownershipReviewed),
2373
2742
  renderBulletSection("Truth docs updated", input.truthDocsUpdated),
2374
2743
  renderClaimEvidenceCheckedSection(input.evidenceChecked),
2744
+ ...input.helperScripts === void 0 ? [] : [renderBulletSection("Helper scripts", input.helperScripts)],
2375
2745
  renderBulletSection("Notes", input.notes)
2376
2746
  ].join("\n\n");
2377
2747
  };
2748
+ var parseTruthSyncReport = (source) => {
2749
+ if (!source.startsWith("Truth Sync: completed")) {
2750
+ throw new Error("Only completed Truth Sync reports can be parsed.");
2751
+ }
2752
+ const helperScripts = parseOptionalBulletSection(source, "Helper scripts");
2753
+ return {
2754
+ status: "completed",
2755
+ changedCode: parseBulletSection(source, "Changed code reviewed"),
2756
+ ownershipReviewed: parseBulletSection(source, "Ownership reviewed"),
2757
+ truthDocsUpdated: parseBulletSection(source, "Truth docs updated"),
2758
+ evidenceChecked: parseEvidenceCheckedSection(source),
2759
+ ...helperScripts === void 0 ? {} : { helperScripts },
2760
+ notes: parseBulletSection(source, "Notes")
2761
+ };
2762
+ };
2378
2763
  var renderTruthSyncBlockedReport = (input) => {
2764
+ const manualReviewFiles = input.manualReviewFiles.filter((file) => file.trim().length > 0);
2765
+ if (manualReviewFiles.length === 0) {
2766
+ throw new Error("Files requiring manual review must include at least one file.");
2767
+ }
2379
2768
  const sections = [
2380
2769
  "Truth Sync: blocked",
2381
- renderBulletSection("Reason", [input.reason])
2770
+ renderBulletSection("Reason", [input.reason]),
2771
+ renderBulletSection("Files requiring manual review", manualReviewFiles),
2772
+ renderBulletSection("Next action", [input.nextAction])
2382
2773
  ];
2383
- if ((input.manualReviewFiles?.length ?? 0) > 0) {
2384
- sections.push(renderBulletSection("Files requiring manual review", input.manualReviewFiles));
2385
- }
2386
- sections.push(renderBulletSection("Next action", [input.nextAction]));
2387
2774
  return [
2388
2775
  ...sections
2389
2776
  ].join("\n\n");
@@ -2397,6 +2784,7 @@ var renderMarkdownExample5 = (content) => {
2397
2784
  var renderTruthSyncSkillBody = (config = defaultAgentConfig(), options = {}) => {
2398
2785
  const truthDocsRoot = resolveTruthDocsRoot(config);
2399
2786
  const workflow = getTruthmarkWorkflow("truthmark-sync");
2787
+ const helperScripts = ["validate-write-lease: skipped, no write lease used"];
2400
2788
  const claudeSubagentMode = options.includeClaudeSubagentMode ? `${renderClaudeSubagentModeSection(
2401
2789
  workflow.subagents ?? [],
2402
2790
  "Parent agent owns Truth Sync acceptance, lease validation, and final report",
@@ -2436,7 +2824,7 @@ Explicit invocation runs immediately. Later functional-code changes reopen the f
2436
2824
  Skip when changes are documentation-only, formatting-only, clearly behavior-preserving renames with no truth impact, when no Truthmark config exists yet, or when there are no functional code changes.
2437
2825
  Parent workflow:
2438
2826
  1. Inspect git status, staged changes, unstaged changes, and untracked files directly.
2439
- 2. Read .truthmark/config.yml, the configured root route index at ${config.docs.routing.rootIndex}, relevant child route files under ${config.docs.routing.areaFilesRoot}/, and relevant canonical docs.
2827
+ 2. Inspect .truthmark/config.yml and configured route files only when they exist; then inspect relevant canonical docs.
2440
2828
  3. Identify functional-code changes and the nearest truth docs or routing repairs.
2441
2829
  4. ${EVIDENCE_AUTHORITY_INSTRUCTIONS}
2442
2830
  5. Code verification is parent-owned: follow repository instructions and task context, and report what ran or why it did not run.
@@ -2470,6 +2858,12 @@ Optional validation tooling:
2470
2858
  - do not require the truthmark binary; direct checkout inspection is the canonical path
2471
2859
  - optional validation must not replace agent judgment about docs and routing
2472
2860
  - update Product Decisions and Rationale when a behavior change comes from a decision change
2861
+ Helper status reporting:
2862
+ - Validate the report body before adding this validator's own success status; the body may omit \`validate-sync-report\` while validation is pending.
2863
+ - After \`truthmark validate sync-report <report-file> --json\` returns \`data.validation.ok: true\`, append or update \`validate-sync-report: ran, passed\` in the final report.
2864
+ - If the installed Truthmark CLI is unavailable or the helper is skipped, record \`validate-sync-report: skipped, <reason>\` and manually validate the report shape.
2865
+ - Record \`validate-write-lease: ran, passed\` only after validating a concrete write lease; otherwise use a truthful skipped status such as \`skipped, no write lease used\`.
2866
+ - Helper output is derived evidence and never replaces direct checkout inspection, evidence review, or parent acceptance.
2473
2867
  ${renderHierarchySummary(config)}
2474
2868
  ${DECISION_TRUTH_INSTRUCTIONS}
2475
2869
  Parent post-sync verification:
@@ -2477,7 +2871,7 @@ Parent post-sync verification:
2477
2871
  - block on any unrelated diff caused by the sync step
2478
2872
  - block if functional code changed during sync
2479
2873
  - for each write lease, validate the worker report against the actual worker diff, allowedWrites, forbiddenWrites, identity fields, filesChanged, offLeaseChanges, blockers, and required report fields before accepting it
2480
- - validate the final report against the structured Truth Sync report contract, including Claim, Evidence, and Result entries under Evidence checked
2874
+ - validate the final report against the structured Truth Sync report contract, including Claim, indented Evidence, and Result values supported, narrowed, removed, or blocked under Evidence checked
2481
2875
  - verify the updated docs correspond to the reviewed changed-code surface
2482
2876
  - verify the final report records ownership review, structure requirement, split, restructure, or blocked reason when the ownership gate fired
2483
2877
  - blocked outcomes must preserve the working tree as-is: no rollback, no post-block cleanup edits, and manual-review reporting of any remaining files
@@ -2485,6 +2879,7 @@ Report completion in this shape:
2485
2879
  ${renderMarkdownExample5(
2486
2880
  renderTruthSyncCompletedReport({
2487
2881
  changedCode: ["src/auth/session.ts"],
2882
+ ownershipReviewed: [config.docs.routing.rootIndex],
2488
2883
  truthDocsUpdated: [`${truthDocsRoot}/repository/overview.md`],
2489
2884
  evidenceChecked: [
2490
2885
  {
@@ -2493,6 +2888,7 @@ ${renderMarkdownExample5(
2493
2888
  result: "supported"
2494
2889
  }
2495
2890
  ],
2891
+ helperScripts,
2496
2892
  notes: ["Updated session timeout behavior."]
2497
2893
  })
2498
2894
  )}
@@ -2535,6 +2931,8 @@ var TRUTHMARK_CHECK_SKILL_PATH = ".codex/skills/truthmark-check/SKILL.md";
2535
2931
  var TRUTHMARK_CHECK_SKILL_METADATA_PATH = ".codex/skills/truthmark-check/agents/openai.yaml";
2536
2932
  var TRUTHMARK_PREVIEW_SKILL_PATH = ".codex/skills/truthmark-preview/SKILL.md";
2537
2933
  var TRUTHMARK_PREVIEW_SKILL_METADATA_PATH = ".codex/skills/truthmark-preview/agents/openai.yaml";
2934
+ var TRUTHMARK_PORTAL_SKILL_PATH = ".codex/skills/truthmark-portal/SKILL.md";
2935
+ var TRUTHMARK_PORTAL_SKILL_METADATA_PATH = ".codex/skills/truthmark-portal/agents/openai.yaml";
2538
2936
  var TRUTHMARK_ROUTE_AUDITOR_AGENT_PATH = ".codex/agents/truth-route-auditor.toml";
2539
2937
  var TRUTHMARK_CLAIM_VERIFIER_AGENT_PATH = ".codex/agents/truth-claim-verifier.toml";
2540
2938
  var TRUTHMARK_DOC_REVIEWER_AGENT_PATH = ".codex/agents/truth-doc-reviewer.toml";
@@ -2553,20 +2951,28 @@ var TRUTHMARK_GEMINI_SYNC_COMMAND_PATH = ".gemini/commands/truthmark/sync.toml";
2553
2951
  var TRUTHMARK_GEMINI_REALIZE_COMMAND_PATH = ".gemini/commands/truthmark/realize.toml";
2554
2952
  var TRUTHMARK_GEMINI_CHECK_COMMAND_PATH = ".gemini/commands/truthmark/check.toml";
2555
2953
  var TRUTHMARK_GEMINI_PREVIEW_COMMAND_PATH = ".gemini/commands/truthmark/preview.toml";
2954
+ var TRUTHMARK_GEMINI_PORTAL_COMMAND_PATH = ".gemini/commands/truthmark/portal.toml";
2955
+ var TRUTHMARK_GEMINI_ROUTE_AUDITOR_AGENT_PATH = ".gemini/agents/truth-route-auditor.md";
2956
+ var TRUTHMARK_GEMINI_CLAIM_VERIFIER_AGENT_PATH = ".gemini/agents/truth-claim-verifier.md";
2957
+ var TRUTHMARK_GEMINI_DOC_REVIEWER_AGENT_PATH = ".gemini/agents/truth-doc-reviewer.md";
2958
+ var TRUTHMARK_GEMINI_DOC_WRITER_AGENT_PATH = ".gemini/agents/truth-doc-writer.md";
2556
2959
  var TRUTHMARK_COPILOT_STRUCTURE_PROMPT_PATH = ".github/prompts/truthmark-structure.prompt.md";
2557
2960
  var TRUTHMARK_COPILOT_DOCUMENT_PROMPT_PATH = ".github/prompts/truthmark-document.prompt.md";
2558
2961
  var TRUTHMARK_COPILOT_SYNC_PROMPT_PATH = ".github/prompts/truthmark-sync.prompt.md";
2559
2962
  var TRUTHMARK_COPILOT_REALIZE_PROMPT_PATH = ".github/prompts/truthmark-realize.prompt.md";
2560
2963
  var TRUTHMARK_COPILOT_CHECK_PROMPT_PATH = ".github/prompts/truthmark-check.prompt.md";
2561
2964
  var TRUTHMARK_COPILOT_PREVIEW_PROMPT_PATH = ".github/prompts/truthmark-preview.prompt.md";
2965
+ var TRUTHMARK_COPILOT_PORTAL_PROMPT_PATH = ".github/prompts/truthmark-portal.prompt.md";
2562
2966
  var TRUTHMARK_COPILOT_ROUTE_AUDITOR_AGENT_PATH = ".github/agents/truth-route-auditor.agent.md";
2563
2967
  var TRUTHMARK_COPILOT_CLAIM_VERIFIER_AGENT_PATH = ".github/agents/truth-claim-verifier.agent.md";
2564
2968
  var TRUTHMARK_COPILOT_DOC_REVIEWER_AGENT_PATH = ".github/agents/truth-doc-reviewer.agent.md";
2565
2969
  var TRUTHMARK_COPILOT_DOC_WRITER_AGENT_PATH = ".github/agents/truth-doc-writer.agent.md";
2566
2970
  var renderGeminiCommand = (description, prompt) => {
2971
+ const promptWithArgs = `${prompt.trimEnd()}
2972
+ User focus or arguments: {{args}}`;
2567
2973
  return `description = "${description}"
2568
2974
  prompt = '''
2569
- ${prompt}
2975
+ ${promptWithArgs}
2570
2976
  '''
2571
2977
  `;
2572
2978
  };
@@ -2586,6 +2992,7 @@ var renderTomlStringArray = (values) => {
2586
2992
  return `[${values.map(renderTomlString).join(", ")}]`;
2587
2993
  };
2588
2994
  var TRUTH_REALIZE_EXPLICIT_INVOCATIONS = "OpenCode /skill truthmark-realize; Codex /truthmark-realize or $truthmark-realize; Claude Code /truthmark-realize; GitHub Copilot /truthmark-realize; Gemini CLI /truthmark:realize.";
2995
+ var routeFilesHint = (config) => `${config.docs.routing.rootIndex}; ${config.docs.routing.areaFilesRoot}/`;
2589
2996
  var WORKFLOW_PACKAGE_DEFINITIONS = {
2590
2997
  "truthmark-structure": {
2591
2998
  title: "Truthmark Structure",
@@ -2593,8 +3000,8 @@ var WORKFLOW_PACKAGE_DEFINITIONS = {
2593
3000
  invocations: TRUTH_STRUCTURE_EXPLICIT_INVOCATIONS,
2594
3001
  use: () => "Use this skill to design or repair Truthmark area structure.",
2595
3002
  quickRules: (config) => [
2596
- "Follow docs/ai/repo-rules.md as the repository instruction authority.",
2597
- `Read .truthmark/config.yml, ${config.docs.routing.rootIndex}, relevant child route files under ${config.docs.routing.areaFilesRoot}/, current docs, and relevant code directly.`,
3003
+ "Follow repository instruction files that exist in this checkout; do not assume any optional policy path exists.",
3004
+ `Inspect .truthmark/config.yml and configured route files (${routeFilesHint(config)}) only when they exist; then inspect current docs and relevant code directly.`,
2598
3005
  "Define areas by product or behavior ownership, not by mechanical directory mirroring.",
2599
3006
  "Do not edit functional code.",
2600
3007
  "Read support/procedure.md before writing route or starter truth-doc changes.",
@@ -2608,8 +3015,8 @@ var WORKFLOW_PACKAGE_DEFINITIONS = {
2608
3015
  invocations: TRUTH_DOCUMENT_EXPLICIT_INVOCATIONS,
2609
3016
  use: () => "Use this skill to document existing implemented behavior when no functional-code changes are required for the task.",
2610
3017
  quickRules: (config) => [
2611
- "Follow docs/ai/repo-rules.md as the repository instruction authority.",
2612
- `Read .truthmark/config.yml, ${config.docs.routing.rootIndex}, relevant child route files under ${config.docs.routing.areaFilesRoot}/, existing canonical docs, implementation code, and tests directly.`,
3018
+ "Follow repository instruction files that exist in this checkout; do not assume any optional policy path exists.",
3019
+ `Inspect .truthmark/config.yml and configured route files (${routeFilesHint(config)}) only when they exist; then inspect existing canonical docs, implementation code, and tests directly.`,
2613
3020
  "Document current implemented behavior; do not invent future behavior.",
2614
3021
  "May write canonical truth docs and truth routing files only; must not write functional code.",
2615
3022
  "Read support/procedure.md before editing truth docs.",
@@ -2624,9 +3031,9 @@ var WORKFLOW_PACKAGE_DEFINITIONS = {
2624
3031
  invocations: TRUTH_SYNC_EXPLICIT_INVOCATIONS,
2625
3032
  use: () => "Use this skill automatically before finishing when functional code changed since the last successful Truth Sync. Also run it immediately when the user explicitly invokes Truth Sync.",
2626
3033
  quickRules: (config) => [
2627
- "Follow docs/ai/repo-rules.md as the repository instruction authority.",
3034
+ "Follow repository instruction files that exist in this checkout; do not assume any optional policy path exists.",
2628
3035
  "Skip docs-only, formatting-only, behavior-preserving renames with no truth impact, missing config, and no-code changes.",
2629
- `Read .truthmark/config.yml, the configured root route index at ${config.docs.routing.rootIndex}, relevant child route files under ${config.docs.routing.areaFilesRoot}/, and relevant canonical docs.`,
3036
+ `Inspect .truthmark/config.yml and configured route files (${routeFilesHint(config)}) only when they exist; then inspect relevant canonical docs directly.`,
2630
3037
  "direct checkout inspection is the canonical path; do not require the truthmark binary.",
2631
3038
  "May write canonical truth docs and truth routing files only; must not rewrite functional code.",
2632
3039
  "Read support/procedure.md before editing truth docs.",
@@ -2641,8 +3048,8 @@ var WORKFLOW_PACKAGE_DEFINITIONS = {
2641
3048
  invocations: TRUTH_PREVIEW_EXPLICIT_INVOCATIONS,
2642
3049
  use: () => "Use this skill only when the user explicitly asks to preview Truthmark routing or workflow choice before edits.",
2643
3050
  quickRules: (config) => [
2644
- "Follow docs/ai/repo-rules.md as the repository instruction authority.",
2645
- `Read .truthmark/config.yml, ${config.docs.routing.rootIndex}, relevant child route files under ${config.docs.routing.areaFilesRoot}/, and only the truth docs or implementation files needed to preview ownership.`,
3051
+ "Follow repository instruction files that exist in this checkout; do not assume any optional policy path exists.",
3052
+ `Inspect .truthmark/config.yml and configured route files (${routeFilesHint(config)}) only when they exist; then inspect only the truth docs or implementation files needed to preview ownership.`,
2646
3053
  "Truth Preview is read-only; this report is intended, not authorized.",
2647
3054
  "must not edit files and must not issue write leases; do not run Truth Sync automatically, replace Truth Check, claim final correctness, or mutate code.",
2648
3055
  "Use optional read-only route-auditor evidence only when it reduces context or clarifies ownership.",
@@ -2656,8 +3063,8 @@ var WORKFLOW_PACKAGE_DEFINITIONS = {
2656
3063
  invocations: TRUTH_REALIZE_EXPLICIT_INVOCATIONS,
2657
3064
  use: () => "Use this skill only when the user explicitly asks to realize truth docs into code.",
2658
3065
  quickRules: (config) => [
2659
- "Follow docs/ai/repo-rules.md as the repository instruction authority.",
2660
- `Read the source truth docs, .truthmark/config.yml, ${config.docs.routing.rootIndex}, relevant child route files, tests, and relevant functional code directly.`,
3066
+ "Follow repository instruction files that exist in this checkout; do not assume any optional policy path exists.",
3067
+ `Read the source truth docs, inspect .truthmark/config.yml and configured route files (${routeFilesHint(config)}) only when they exist, then inspect tests and relevant functional code directly.`,
2661
3068
  "Truth docs lead; code follows.",
2662
3069
  "may write functional code only; must not edit truth docs or truth routing while realizing those docs.",
2663
3070
  "Read support/procedure.md before changing code.",
@@ -2670,8 +3077,8 @@ var WORKFLOW_PACKAGE_DEFINITIONS = {
2670
3077
  invocations: TRUTH_CHECK_EXPLICIT_INVOCATIONS,
2671
3078
  use: () => "Use this skill to audit repository truth health.",
2672
3079
  quickRules: (config) => [
2673
- "Follow docs/ai/repo-rules.md as the repository instruction authority.",
2674
- `Read .truthmark/config.yml, ${config.docs.routing.rootIndex}, relevant child route files under ${config.docs.routing.areaFilesRoot}/, canonical docs, and relevant implementation directly.`,
3080
+ "Follow repository instruction files that exist in this checkout; do not assume any optional policy path exists.",
3081
+ `Inspect .truthmark/config.yml and configured route files (${routeFilesHint(config)}) only when they exist; then inspect canonical docs and relevant implementation directly.`,
2675
3082
  "Report issues and suggested fixes; do not silently rewrite unrelated files.",
2676
3083
  "Direct checkout inspection is valid even when local tooling is unavailable.",
2677
3084
  "Read support/procedure.md before auditing details.",
@@ -2679,6 +3086,24 @@ var WORKFLOW_PACKAGE_DEFINITIONS = {
2679
3086
  "Read support/report-template.md before the final report."
2680
3087
  ],
2681
3088
  parentRule: "Parent agent owns the final Truth Check report"
3089
+ },
3090
+ "truthmark-portal": {
3091
+ title: "Truthmark Portal",
3092
+ argumentHint: "Optional output path, template, or portal generation focus",
3093
+ invocations: TRUTHMARK_PORTAL_EXPLICIT_INVOCATIONS,
3094
+ use: () => "Use this skill only when the user explicitly asks to generate or refresh the committed static HTML Truthmark Portal.",
3095
+ quickRules: (config) => [
3096
+ "Follow repository instruction files that exist in this checkout; do not assume any optional policy path exists.",
3097
+ "Truthmark Portal is manual-only; never run it as a completion gate and never treat it as Truth Sync.",
3098
+ "Markdown remains canonical; generated HTML is non-canonical presentation only.",
3099
+ "Read Markdown directly; the workflow does not require the truthmark CLI or package.",
3100
+ "Generate committed, generated non-canonical static files for humans.",
3101
+ `Write only under configured Portal output ${config.truthmarkPortal.output}; default output is docs/truthmark-portal.`,
3102
+ `Use configured Portal template ${config.truthmarkPortal.template}; no .truthmark/index.json dependency.`,
3103
+ "Use no remote dependencies by default and include source provenance on every page.",
3104
+ "Read support/procedure.md before generating Portal output.",
3105
+ "Read support/report-template.md before the final report."
3106
+ ]
2682
3107
  }
2683
3108
  };
2684
3109
  var stripWorkflowSkillFrontmatter = (body) => {
@@ -2707,6 +3132,56 @@ Generated by Truthmark ${TRUTHMARK_VERSION}. Rerun truthmark init after upgrades
2707
3132
  ${body}
2708
3133
  `;
2709
3134
  };
3135
+ var renderHelperManifest = (helpers) => {
3136
+ const manifest = {
3137
+ helpers: Object.fromEntries(
3138
+ helpers.map((helper) => [
3139
+ helper.id,
3140
+ {
3141
+ optional: helper.optional,
3142
+ runner: helper.runner,
3143
+ command: helper.command,
3144
+ inputs: helper.inputs,
3145
+ output: helper.output,
3146
+ writes: helper.writes,
3147
+ ...helper.allowedWrites === void 0 ? {} : { allowedWrites: helper.allowedWrites },
3148
+ fallback: helper.fallback
3149
+ }
3150
+ ])
3151
+ )
3152
+ };
3153
+ return [
3154
+ `# Generated by Truthmark ${TRUTHMARK_VERSION}. Rerun truthmark init after upgrades.`,
3155
+ stringify2(manifest, { lineWidth: 0 })
3156
+ ].join("\n");
3157
+ };
3158
+ var renderHelperPolicySupport = (helpers) => {
3159
+ const reportHelperId = helpers.find((helper) => helper.id.endsWith("-report"))?.id ?? helpers[0]?.id;
3160
+ const helperLines = helpers.map(
3161
+ (helper) => `- ${helper.id}: optional ${helper.runner}; manual fallback: ${helper.fallback}`
3162
+ ).join("\n");
3163
+ return renderSkillSupportFile(
3164
+ "Optional Helper CLI Policy",
3165
+ `Optional helper CLI commands may collect deterministic checkout facts or validate artifacts. If the Truthmark CLI is unavailable or too old for a declared helper, continue manually using this procedure and report which helper was skipped. Helper output is derived evidence; it does not override direct checkout inspection, workflow write boundaries, or parent acceptance.
3166
+
3167
+ Runner detection:
3168
+ - Check the declared Truthmark CLI runner before invoking a helper.
3169
+ - Invoke helpers through the installed \`truthmark validate ... --json\` CLI command using argv-style arguments from helper-manifest.yml.
3170
+ - If unavailable or version-mismatched, treat the helper as skipped and use the manual fallback.
3171
+ - Do not fail the workflow solely because a helper cannot run.
3172
+
3173
+ Available helpers:
3174
+ ${helperLines}
3175
+
3176
+ Final reports should include helper status when helpers are declared for this workflow:
3177
+
3178
+ \`\`\`md
3179
+ Helper scripts:
3180
+ - ${reportHelperId}: ran, passed
3181
+ - validate-write-lease: skipped, no write lease used
3182
+ \`\`\``
3183
+ );
3184
+ };
2710
3185
  var renderStandaloneWorkflowSkillBody = (workflowId, config) => {
2711
3186
  switch (workflowId) {
2712
3187
  case "truthmark-structure":
@@ -2721,12 +3196,15 @@ var renderStandaloneWorkflowSkillBody = (workflowId, config) => {
2721
3196
  return renderTruthmarkRealizeSkillBody(config);
2722
3197
  case "truthmark-check":
2723
3198
  return renderTruthCheckSkillBody(config);
3199
+ case "truthmark-portal":
3200
+ return renderTruthmarkPortalSkillBody(config);
2724
3201
  }
2725
3202
  };
2726
- var renderWorkflowEntrypoint = (workflowId, config, supportFiles) => {
3203
+ var renderWorkflowEntrypoint = (workflowId, config, supportFiles, host) => {
2727
3204
  const workflow = getTruthmarkWorkflow(workflowId);
2728
3205
  const definition = WORKFLOW_PACKAGE_DEFINITIONS[workflowId];
2729
3206
  const supportFileList = supportFiles.map((supportFile) => `- ${supportFile}`).join("\n");
3207
+ const hostUsage = host === "github-copilot" ? "Use as a Copilot agent skill. Prompt files remain available under `.github/prompts/` for command-style invocation in supported Copilot IDEs." : host === "gemini-cli" ? "Use as a Gemini CLI Agent Skill; commands remain available under `/truthmark:*` for command-first invocation." : void 0;
2730
3208
  return `---
2731
3209
  name: ${workflowId}
2732
3210
  description: ${workflow.description}
@@ -2738,6 +3216,9 @@ truthmark-version: ${TRUTHMARK_VERSION}
2738
3216
  # ${definition.title}
2739
3217
 
2740
3218
  ${definition.use(config)}
3219
+ ${hostUsage === void 0 ? "" : `
3220
+ ${hostUsage}
3221
+ `}
2741
3222
 
2742
3223
  Invocations: ${definition.invocations}
2743
3224
 
@@ -2778,6 +3259,18 @@ var renderWorkflowSubagentSupport = (workflowId, host) => {
2778
3259
  definition.parentRule,
2779
3260
  writeAgents
2780
3261
  );
3262
+ case "github-copilot":
3263
+ return renderCopilotCustomAgentModeSection(
3264
+ readAgents,
3265
+ definition.parentRule,
3266
+ writeAgents
3267
+ );
3268
+ case "gemini-cli":
3269
+ return renderGeminiSubagentModeSection(
3270
+ readAgents,
3271
+ definition.parentRule,
3272
+ writeAgents
3273
+ );
2781
3274
  }
2782
3275
  };
2783
3276
  var renderTruthmarkSkillPackage = ({
@@ -2792,16 +3285,18 @@ var renderTruthmarkSkillPackage = ({
2792
3285
  renderStandaloneWorkflowSkillBody(workflowId, config)
2793
3286
  );
2794
3287
  const subagents = renderWorkflowSubagentSupport(workflowId, host);
3288
+ const helpers = getTruthmarkWorkflow(workflowId).helpers ?? [];
2795
3289
  const supportFiles = [
2796
3290
  "support/procedure.md",
2797
3291
  "support/report-template.md",
2798
- ...subagents === void 0 ? [] : ["support/subagents-and-leases.md"]
3292
+ ...subagents === void 0 ? [] : ["support/subagents-and-leases.md"],
3293
+ ...helpers.length === 0 ? [] : ["helper-manifest.yml", "support/helper-policy.md"]
2799
3294
  ];
2800
3295
  const definition = WORKFLOW_PACKAGE_DEFINITIONS[workflowId];
2801
3296
  const files = [
2802
3297
  {
2803
3298
  path: skillPath,
2804
- content: renderWorkflowEntrypoint(workflowId, config, supportFiles)
3299
+ content: renderWorkflowEntrypoint(workflowId, config, supportFiles, host)
2805
3300
  },
2806
3301
  {
2807
3302
  path: `${supportDirectory}/procedure.md`,
@@ -2827,10 +3322,22 @@ var renderTruthmarkSkillPackage = ({
2827
3322
  )
2828
3323
  });
2829
3324
  }
3325
+ if (helpers.length > 0) {
3326
+ files.push(
3327
+ {
3328
+ path: `${skillDirectory}/helper-manifest.yml`,
3329
+ content: renderHelperManifest(helpers)
3330
+ },
3331
+ {
3332
+ path: `${supportDirectory}/helper-policy.md`,
3333
+ content: renderHelperPolicySupport(helpers)
3334
+ }
3335
+ );
3336
+ }
2830
3337
  return files;
2831
3338
  };
2832
- var normalizeOpenCodePermissionPath = (path12) => {
2833
- const normalized = path12.replace(/\\/gu, "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
3339
+ var normalizeOpenCodePermissionPath = (path13) => {
3340
+ const normalized = path13.replace(/\\/gu, "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
2834
3341
  return normalized === "" ? "." : normalized;
2835
3342
  };
2836
3343
  var appendOpenCodePermissionGlob = (root, glob) => {
@@ -2869,7 +3376,7 @@ var TRUTHMARK_SUBAGENT_PROFILES = {
2869
3376
  nicknameCandidates: ["Route Audit", "Route Trace", "Route Check"],
2870
3377
  instructions: `Stay read-only.
2871
3378
  Audit one bounded Truthmark route, area, or doc shard assigned by the parent.
2872
- Read .truthmark/config.yml, the root route index, relevant child route files, mapped truth docs, and relevant implementation files directly.
3379
+ Inspect .truthmark/config.yml and route files only when they exist; then inspect mapped truth docs and relevant implementation files directly.
2873
3380
  Find missing, stale, broad, overloaded, catch-all, mixed-owner, or unrouteable ownership.
2874
3381
  Do not edit files, stage changes, or propose broad rewrites.
2875
3382
  Return JSON only with keys: scope, filesReviewed, findings, evidence, confidence, recommendedWorkflow, notes.
@@ -2982,6 +3489,45 @@ tools: [read, search, edit]
2982
3489
 
2983
3490
  # Generated by Truthmark ${TRUTHMARK_VERSION}. Rerun truthmark init after upgrades.
2984
3491
 
3492
+ ${instructions}
3493
+ `;
3494
+ };
3495
+ var renderGeminiReadOnlyAgent = ({
3496
+ copilotName,
3497
+ description,
3498
+ instructions
3499
+ }) => {
3500
+ const agentInstructions = renderReadOnlySubagentInstructions(instructions);
3501
+ return `---
3502
+ name: ${copilotName}
3503
+ description: ${description}
3504
+ kind: local
3505
+ tools: [read_file, grep_search]
3506
+ ---
3507
+
3508
+ # Generated by Truthmark ${TRUTHMARK_VERSION}. Rerun truthmark init after upgrades.
3509
+
3510
+ Manual invocation: @${copilotName}
3511
+
3512
+ ${agentInstructions}
3513
+ `;
3514
+ };
3515
+ var renderGeminiWriteAgent = ({
3516
+ copilotName,
3517
+ description,
3518
+ instructions
3519
+ }) => {
3520
+ return `---
3521
+ name: ${copilotName}
3522
+ description: ${description}
3523
+ kind: local
3524
+ tools: [read_file, grep_search, write_file]
3525
+ ---
3526
+
3527
+ # Generated by Truthmark ${TRUTHMARK_VERSION}. Rerun truthmark init after upgrades.
3528
+
3529
+ Manual invocation: @${copilotName} with an explicit parent write lease.
3530
+
2985
3531
  ${instructions}
2986
3532
  `;
2987
3533
  };
@@ -3084,6 +3630,26 @@ var renderTruthmarkCopilotDocWriterAgent = () => {
3084
3630
  TRUTHMARK_WRITE_SUBAGENT_PROFILES.truth_doc_writer
3085
3631
  );
3086
3632
  };
3633
+ var renderTruthmarkGeminiRouteAuditorAgent = () => {
3634
+ return renderGeminiReadOnlyAgent(
3635
+ TRUTHMARK_SUBAGENT_PROFILES.truth_route_auditor
3636
+ );
3637
+ };
3638
+ var renderTruthmarkGeminiClaimVerifierAgent = () => {
3639
+ return renderGeminiReadOnlyAgent(
3640
+ TRUTHMARK_SUBAGENT_PROFILES.truth_claim_verifier
3641
+ );
3642
+ };
3643
+ var renderTruthmarkGeminiDocReviewerAgent = () => {
3644
+ return renderGeminiReadOnlyAgent(
3645
+ TRUTHMARK_SUBAGENT_PROFILES.truth_doc_reviewer
3646
+ );
3647
+ };
3648
+ var renderTruthmarkGeminiDocWriterAgent = () => {
3649
+ return renderGeminiWriteAgent(
3650
+ TRUTHMARK_WRITE_SUBAGENT_PROFILES.truth_doc_writer
3651
+ );
3652
+ };
3087
3653
  var renderTruthmarkClaudeRouteAuditorAgent = () => {
3088
3654
  return renderClaudeReadOnlyAgent(
3089
3655
  TRUTHMARK_SUBAGENT_PROFILES.truth_route_auditor
@@ -3273,8 +3839,8 @@ Truth Realize is doc-first:
3273
3839
 
3274
3840
  Workflow:
3275
3841
 
3276
- 1. Read the updated truth docs named by the user, or infer the relevant docs from ${config.docs.routing.rootIndex}.
3277
- 2. Read .truthmark/config.yml, ${config.docs.routing.rootIndex}, relevant child route files, tests, and the relevant functional code.
3842
+ 1. Read the updated truth docs named by the user, or infer the relevant docs from configured route files when present.
3843
+ 2. Inspect .truthmark/config.yml and configured route files (${routeFilesHint(config)}) only when they exist; then read tests and the relevant functional code.
3278
3844
  3. ${EVIDENCE_AUTHORITY_INSTRUCTIONS}
3279
3845
  ${renderTruthDocOwnershipGateSection(
3280
3846
  "source truth docs before writing code",
@@ -3348,6 +3914,21 @@ var renderTruthmarkCheckSkillMetadata = () => {
3348
3914
  policy:
3349
3915
  allow_implicit_invocation: ${workflow.allowImplicitInvocation}
3350
3916
 
3917
+ truthmark:
3918
+ version: "${TRUTHMARK_VERSION}"
3919
+ refresh_command: "truthmark init"
3920
+ `;
3921
+ };
3922
+ var renderTruthmarkPortalSkillMetadata = () => {
3923
+ const workflow = getTruthmarkWorkflow("truthmark-portal");
3924
+ return `interface:
3925
+ display_name: "${workflow.displayName}"
3926
+ short_description: "${workflow.shortDescription}"
3927
+ default_prompt: "${workflow.defaultPrompt}"
3928
+
3929
+ policy:
3930
+ allow_implicit_invocation: ${workflow.allowImplicitInvocation}
3931
+
3351
3932
  truthmark:
3352
3933
  version: "${TRUTHMARK_VERSION}"
3353
3934
  refresh_command: "truthmark init"
@@ -3395,6 +3976,13 @@ var renderTruthmarkGeminiPreviewCommand = (config = defaultAgentConfig()) => {
3395
3976
  renderTruthPreviewSkillBody(config)
3396
3977
  );
3397
3978
  };
3979
+ var renderTruthmarkGeminiPortalCommand = (config = defaultAgentConfig()) => {
3980
+ const workflow = getTruthmarkWorkflow("truthmark-portal");
3981
+ return renderGeminiCommand(
3982
+ workflow.description,
3983
+ renderTruthmarkPortalSkillBody(config)
3984
+ );
3985
+ };
3398
3986
  var renderTruthmarkCopilotStructurePrompt = (config = defaultAgentConfig()) => {
3399
3987
  const workflow = getTruthmarkWorkflow("truthmark-structure");
3400
3988
  return renderCopilotPromptFile(
@@ -3445,6 +4033,13 @@ var renderTruthmarkCopilotPreviewPrompt = (config = defaultAgentConfig()) => {
3445
4033
  renderTruthPreviewSkillBody(config)
3446
4034
  );
3447
4035
  };
4036
+ var renderTruthmarkCopilotPortalPrompt = (config = defaultAgentConfig()) => {
4037
+ const workflow = getTruthmarkWorkflow("truthmark-portal");
4038
+ return renderCopilotPromptFile(
4039
+ workflow.description,
4040
+ renderTruthmarkPortalSkillBody(config)
4041
+ );
4042
+ };
3448
4043
 
3449
4044
  // src/templates/generated-surfaces.ts
3450
4045
  var codexFiles = (config) => {
@@ -3526,10 +4121,24 @@ var codexFiles = (config) => {
3526
4121
  content: renderTruthmarkDocWriterAgent()
3527
4122
  }
3528
4123
  ];
4124
+ if (config.truthmarkPortal.enabled) {
4125
+ files.push(
4126
+ ...renderTruthmarkSkillPackage({
4127
+ skillPath: TRUTHMARK_PORTAL_SKILL_PATH,
4128
+ workflowId: "truthmark-portal",
4129
+ host: "codex",
4130
+ config
4131
+ }),
4132
+ {
4133
+ path: TRUTHMARK_PORTAL_SKILL_METADATA_PATH,
4134
+ content: renderTruthmarkPortalSkillMetadata()
4135
+ }
4136
+ );
4137
+ }
3529
4138
  return files;
3530
4139
  };
3531
4140
  var opencodeFiles = (config) => {
3532
- return [
4141
+ const files = [
3533
4142
  ...renderTruthmarkSkillPackage({
3534
4143
  skillPath: ".opencode/skills/truthmark-structure/SKILL.md",
3535
4144
  workflowId: "truthmark-structure",
@@ -3583,9 +4192,20 @@ var opencodeFiles = (config) => {
3583
4192
  content: renderTruthmarkOpenCodeDocWriterAgent(config)
3584
4193
  }
3585
4194
  ];
4195
+ if (config.truthmarkPortal.enabled) {
4196
+ files.push(
4197
+ ...renderTruthmarkSkillPackage({
4198
+ skillPath: ".opencode/skills/truthmark-portal/SKILL.md",
4199
+ workflowId: "truthmark-portal",
4200
+ host: "opencode",
4201
+ config
4202
+ })
4203
+ );
4204
+ }
4205
+ return files;
3586
4206
  };
3587
4207
  var claudeFiles = (config, block) => {
3588
- return [
4208
+ const files = [
3589
4209
  ...instructionBlockFiles(["CLAUDE.md"], block),
3590
4210
  ...renderTruthmarkSkillPackage({
3591
4211
  skillPath: ".claude/skills/truthmark-structure/SKILL.md",
@@ -3640,10 +4260,57 @@ var claudeFiles = (config, block) => {
3640
4260
  content: renderTruthmarkClaudeDocWriterAgent()
3641
4261
  }
3642
4262
  ];
4263
+ if (config.truthmarkPortal.enabled) {
4264
+ files.push(
4265
+ ...renderTruthmarkSkillPackage({
4266
+ skillPath: ".claude/skills/truthmark-portal/SKILL.md",
4267
+ workflowId: "truthmark-portal",
4268
+ host: "claude-code",
4269
+ config
4270
+ })
4271
+ );
4272
+ }
4273
+ return files;
3643
4274
  };
3644
4275
  var copilotFiles = (config, block) => {
3645
4276
  const files = [
3646
4277
  ...instructionBlockFiles([".github/copilot-instructions.md"], block),
4278
+ ...renderTruthmarkSkillPackage({
4279
+ skillPath: ".github/skills/truthmark-structure/SKILL.md",
4280
+ workflowId: "truthmark-structure",
4281
+ host: "github-copilot",
4282
+ config
4283
+ }),
4284
+ ...renderTruthmarkSkillPackage({
4285
+ skillPath: ".github/skills/truthmark-document/SKILL.md",
4286
+ workflowId: "truthmark-document",
4287
+ host: "github-copilot",
4288
+ config
4289
+ }),
4290
+ ...renderTruthmarkSkillPackage({
4291
+ skillPath: ".github/skills/truthmark-sync/SKILL.md",
4292
+ workflowId: "truthmark-sync",
4293
+ host: "github-copilot",
4294
+ config
4295
+ }),
4296
+ ...renderTruthmarkSkillPackage({
4297
+ skillPath: ".github/skills/truthmark-preview/SKILL.md",
4298
+ workflowId: "truthmark-preview",
4299
+ host: "github-copilot",
4300
+ config
4301
+ }),
4302
+ ...renderTruthmarkSkillPackage({
4303
+ skillPath: ".github/skills/truthmark-check/SKILL.md",
4304
+ workflowId: "truthmark-check",
4305
+ host: "github-copilot",
4306
+ config
4307
+ }),
4308
+ ...renderTruthmarkSkillPackage({
4309
+ skillPath: ".github/skills/truthmark-realize/SKILL.md",
4310
+ workflowId: "truthmark-realize",
4311
+ host: "github-copilot",
4312
+ config
4313
+ }),
3647
4314
  {
3648
4315
  path: TRUTHMARK_COPILOT_STRUCTURE_PROMPT_PATH,
3649
4316
  content: renderTruthmarkCopilotStructurePrompt(config)
@@ -3685,11 +4352,121 @@ var copilotFiles = (config, block) => {
3685
4352
  content: renderTruthmarkCopilotDocWriterAgent()
3686
4353
  }
3687
4354
  ];
4355
+ if (config.truthmarkPortal.enabled) {
4356
+ files.push(
4357
+ ...renderTruthmarkSkillPackage({
4358
+ skillPath: ".github/skills/truthmark-portal/SKILL.md",
4359
+ workflowId: "truthmark-portal",
4360
+ host: "github-copilot",
4361
+ config
4362
+ }),
4363
+ {
4364
+ path: TRUTHMARK_COPILOT_PORTAL_PROMPT_PATH,
4365
+ content: renderTruthmarkCopilotPortalPrompt(config)
4366
+ }
4367
+ );
4368
+ }
4369
+ return files;
4370
+ };
4371
+ var geminiFiles = (config, block) => {
4372
+ const files = [
4373
+ ...instructionBlockFiles(["GEMINI.md"], block),
4374
+ ...renderTruthmarkSkillPackage({
4375
+ skillPath: ".gemini/skills/truthmark-structure/SKILL.md",
4376
+ workflowId: "truthmark-structure",
4377
+ host: "gemini-cli",
4378
+ config
4379
+ }),
4380
+ ...renderTruthmarkSkillPackage({
4381
+ skillPath: ".gemini/skills/truthmark-document/SKILL.md",
4382
+ workflowId: "truthmark-document",
4383
+ host: "gemini-cli",
4384
+ config
4385
+ }),
4386
+ ...renderTruthmarkSkillPackage({
4387
+ skillPath: ".gemini/skills/truthmark-sync/SKILL.md",
4388
+ workflowId: "truthmark-sync",
4389
+ host: "gemini-cli",
4390
+ config
4391
+ }),
4392
+ ...renderTruthmarkSkillPackage({
4393
+ skillPath: ".gemini/skills/truthmark-preview/SKILL.md",
4394
+ workflowId: "truthmark-preview",
4395
+ host: "gemini-cli",
4396
+ config
4397
+ }),
4398
+ ...renderTruthmarkSkillPackage({
4399
+ skillPath: ".gemini/skills/truthmark-check/SKILL.md",
4400
+ workflowId: "truthmark-check",
4401
+ host: "gemini-cli",
4402
+ config
4403
+ }),
4404
+ ...renderTruthmarkSkillPackage({
4405
+ skillPath: ".gemini/skills/truthmark-realize/SKILL.md",
4406
+ workflowId: "truthmark-realize",
4407
+ host: "gemini-cli",
4408
+ config
4409
+ }),
4410
+ {
4411
+ path: TRUTHMARK_GEMINI_STRUCTURE_COMMAND_PATH,
4412
+ content: renderTruthmarkGeminiStructureCommand(config)
4413
+ },
4414
+ {
4415
+ path: TRUTHMARK_GEMINI_DOCUMENT_COMMAND_PATH,
4416
+ content: renderTruthmarkGeminiDocumentCommand(config)
4417
+ },
4418
+ {
4419
+ path: TRUTHMARK_GEMINI_SYNC_COMMAND_PATH,
4420
+ content: renderTruthmarkGeminiSyncCommand(config)
4421
+ },
4422
+ {
4423
+ path: TRUTHMARK_GEMINI_PREVIEW_COMMAND_PATH,
4424
+ content: renderTruthmarkGeminiPreviewCommand(config)
4425
+ },
4426
+ {
4427
+ path: TRUTHMARK_GEMINI_CHECK_COMMAND_PATH,
4428
+ content: renderTruthmarkGeminiCheckCommand(config)
4429
+ },
4430
+ {
4431
+ path: TRUTHMARK_GEMINI_REALIZE_COMMAND_PATH,
4432
+ content: renderTruthmarkGeminiRealizeCommand(config)
4433
+ },
4434
+ {
4435
+ path: TRUTHMARK_GEMINI_ROUTE_AUDITOR_AGENT_PATH,
4436
+ content: renderTruthmarkGeminiRouteAuditorAgent()
4437
+ },
4438
+ {
4439
+ path: TRUTHMARK_GEMINI_CLAIM_VERIFIER_AGENT_PATH,
4440
+ content: renderTruthmarkGeminiClaimVerifierAgent()
4441
+ },
4442
+ {
4443
+ path: TRUTHMARK_GEMINI_DOC_REVIEWER_AGENT_PATH,
4444
+ content: renderTruthmarkGeminiDocReviewerAgent()
4445
+ },
4446
+ {
4447
+ path: TRUTHMARK_GEMINI_DOC_WRITER_AGENT_PATH,
4448
+ content: renderTruthmarkGeminiDocWriterAgent()
4449
+ }
4450
+ ];
4451
+ if (config.truthmarkPortal.enabled) {
4452
+ files.push(
4453
+ ...renderTruthmarkSkillPackage({
4454
+ skillPath: ".gemini/skills/truthmark-portal/SKILL.md",
4455
+ workflowId: "truthmark-portal",
4456
+ host: "gemini-cli",
4457
+ config
4458
+ }),
4459
+ {
4460
+ path: TRUTHMARK_GEMINI_PORTAL_COMMAND_PATH,
4461
+ content: renderTruthmarkGeminiPortalCommand(config)
4462
+ }
4463
+ );
4464
+ }
3688
4465
  return files;
3689
4466
  };
3690
4467
  var instructionBlockFiles = (paths, block) => {
3691
- return paths.map((path12) => ({
3692
- path: path12,
4468
+ return paths.map((path13) => ({
4469
+ path: path13,
3693
4470
  content: block,
3694
4471
  managedBlock: true
3695
4472
  }));
@@ -3705,33 +4482,7 @@ var filesForPlatform = (platform, config, block) => {
3705
4482
  case "github-copilot":
3706
4483
  return copilotFiles(config, block);
3707
4484
  case "gemini-cli":
3708
- return [
3709
- ...instructionBlockFiles(["GEMINI.md"], block),
3710
- {
3711
- path: TRUTHMARK_GEMINI_STRUCTURE_COMMAND_PATH,
3712
- content: renderTruthmarkGeminiStructureCommand(config)
3713
- },
3714
- {
3715
- path: TRUTHMARK_GEMINI_DOCUMENT_COMMAND_PATH,
3716
- content: renderTruthmarkGeminiDocumentCommand(config)
3717
- },
3718
- {
3719
- path: TRUTHMARK_GEMINI_SYNC_COMMAND_PATH,
3720
- content: renderTruthmarkGeminiSyncCommand(config)
3721
- },
3722
- {
3723
- path: TRUTHMARK_GEMINI_PREVIEW_COMMAND_PATH,
3724
- content: renderTruthmarkGeminiPreviewCommand(config)
3725
- },
3726
- {
3727
- path: TRUTHMARK_GEMINI_CHECK_COMMAND_PATH,
3728
- content: renderTruthmarkGeminiCheckCommand(config)
3729
- },
3730
- {
3731
- path: TRUTHMARK_GEMINI_REALIZE_COMMAND_PATH,
3732
- content: renderTruthmarkGeminiRealizeCommand(config)
3733
- }
3734
- ];
4485
+ return geminiFiles(config, block);
3735
4486
  }
3736
4487
  };
3737
4488
  var renderGeneratedSurfaces = (config, block = renderAgentsBlock(config)) => {
@@ -3790,16 +4541,39 @@ var removeTrailingManagedChunk = (preservedLines) => {
3790
4541
  preservedLines.splice(startIndex);
3791
4542
  }
3792
4543
  };
4544
+ var LEGACY_REPO_RULES_PATH = ["docs", "ai", `repo-${"rules.md"}`].join("/");
4545
+ var LEGACY_AGENT_ONBOARDING_PATH = ["docs", "ai", `agent-${"onboarding.md"}`].join(
4546
+ "/"
4547
+ );
4548
+ var LEGACY_PRIMARY_REPO_INSTRUCTION_PHRASE = [
4549
+ "primary repository",
4550
+ "instruction source"
4551
+ ].join(" ");
3793
4552
  var normalizeLegacyInstructionPreamble = (content) => {
3794
4553
  return content.replaceAll(
3795
- "Use that file as the primary repository instruction source for Codex.",
3796
- "Use that file as the primary repository instruction source for this agent."
4554
+ `Follow \`${LEGACY_REPO_RULES_PATH}\`.`,
4555
+ "Follow repository instruction files that are present in this checkout; do not assume optional policy docs exist."
4556
+ ).replaceAll(
4557
+ `Follow \`${LEGACY_REPO_RULES_PATH}\` as the ${LEGACY_PRIMARY_REPO_INSTRUCTION_PHRASE}.`,
4558
+ "Follow repository instruction files that are present in this checkout; do not assume optional policy docs exist."
4559
+ ).replaceAll(
4560
+ `Use that file as the ${LEGACY_PRIMARY_REPO_INSTRUCTION_PHRASE} for Codex.`,
4561
+ "Use explicitly configured repository policy docs only when they exist in this checkout."
4562
+ ).replaceAll(
4563
+ `Use that file as the ${LEGACY_PRIMARY_REPO_INSTRUCTION_PHRASE} for this agent.`,
4564
+ "Use explicitly configured repository policy docs only when they exist in this checkout."
3797
4565
  ).replaceAll("Codex-specific:", "Agent-specific:").replaceAll(
3798
4566
  "- Read `docs/README.md` for the canonical docs map.",
3799
- "- Read `docs/README.md` only when choosing or updating canonical docs."
4567
+ "- Read the configured Truthmark routing files when choosing or updating canonical docs."
4568
+ ).replaceAll(
4569
+ "- Read `docs/README.md` only when choosing or updating canonical docs.",
4570
+ "- Read the configured Truthmark routing files when choosing or updating canonical docs."
3800
4571
  ).replaceAll(
3801
- "- Use `docs/ai/agent-onboarding.md` for quick task routing.",
3802
- "- Use `docs/ai/agent-onboarding.md` only when task routing is unclear or cross-area."
4572
+ `- Use \`${LEGACY_AGENT_ONBOARDING_PATH}\` for quick task routing.`,
4573
+ "- Use repository onboarding or docs-map files only when present and needed for unclear or cross-area routing."
4574
+ ).replaceAll(
4575
+ `- Use \`${LEGACY_AGENT_ONBOARDING_PATH}\` only when task routing is unclear or cross-area.`,
4576
+ "- Use repository onboarding or docs-map files only when present and needed for unclear or cross-area routing."
3803
4577
  );
3804
4578
  };
3805
4579
  var upsertManagedBlock = (existingContent, block) => {
@@ -3860,48 +4634,36 @@ var upsertManagedBlock = (existingContent, block) => {
3860
4634
 
3861
4635
  ${block}`;
3862
4636
  };
3863
- var writeManagedAgentsFile = async (rootDir, path12 = "AGENTS.md", block) => {
4637
+ var writeManagedAgentsFile = async (rootDir, path13 = "AGENTS.md", block) => {
3864
4638
  let existingContent = null;
3865
4639
  try {
3866
- existingContent = await fs7.readFile(resolveRepoPath(rootDir, path12), "utf8");
4640
+ existingContent = await fs7.readFile(resolveRepoPath(rootDir, path13), "utf8");
3867
4641
  } catch (error) {
3868
4642
  if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
3869
4643
  throw error;
3870
4644
  }
3871
4645
  }
3872
- return writeRepoFile(rootDir, path12, upsertManagedBlock(existingContent, block));
4646
+ return writeRepoFile(rootDir, path13, upsertManagedBlock(existingContent, block));
3873
4647
  };
3874
4648
  var diagnosticCategoryForPath = (filePath, config) => {
3875
4649
  if (filePath === "AGENTS.md") {
3876
4650
  return "truth-sync";
3877
4651
  }
3878
- if (filePath === "CLAUDE.md" || filePath === "GEMINI.md" || filePath === ".github/copilot-instructions.md" || filePath.startsWith(".github/prompts/truthmark-") || filePath.startsWith(".github/agents/truth-") || filePath.startsWith(".claude/agents/truth-") || filePath.startsWith(".claude/skills/truthmark-") || filePath.startsWith(".opencode/skills/truthmark-") || filePath.startsWith(".opencode/agents/") || filePath.startsWith(".codex/agents/")) {
3879
- return "truth-sync";
3880
- }
3881
- if (filePath.startsWith(".codex/skills/truthmark-structure/")) {
3882
- return "truth-sync";
3883
- }
3884
- if (filePath.startsWith(".codex/skills/truthmark-document/")) {
3885
- return "truth-sync";
4652
+ if (filePath === ".github/prompts/truthmark-realize.prompt.md" || filePath.startsWith(".github/skills/truthmark-realize/") || filePath.startsWith(".claude/skills/truthmark-realize/") || filePath.startsWith(".opencode/skills/truthmark-realize/") || filePath.startsWith(".codex/skills/truthmark-realize/") || filePath.startsWith(".gemini/skills/truthmark-realize/")) {
4653
+ return "realization";
3886
4654
  }
3887
- if (filePath.startsWith(".codex/skills/truthmark-sync/")) {
4655
+ if (filePath === "CLAUDE.md" || filePath === "GEMINI.md" || filePath === ".github/copilot-instructions.md" || filePath.startsWith(".github/prompts/truthmark-") || filePath.startsWith(".github/agents/truth-") || filePath.startsWith(".github/skills/truthmark-") || filePath.startsWith(".claude/agents/truth-") || filePath.startsWith(".claude/skills/truthmark-") || filePath.startsWith(".opencode/skills/truthmark-") || filePath.startsWith(".opencode/agents/") || filePath.startsWith(".codex/agents/") || filePath.startsWith(".gemini/agents/truth-") || filePath.startsWith(".gemini/skills/truthmark-")) {
3888
4656
  return "truth-sync";
3889
4657
  }
3890
- if (filePath.startsWith(".codex/skills/truthmark-preview/")) {
4658
+ if (filePath.startsWith(".codex/skills/truthmark-")) {
3891
4659
  return "truth-sync";
3892
4660
  }
3893
- if (filePath.startsWith(".codex/skills/truthmark-realize/")) {
3894
- return "realization";
3895
- }
3896
4661
  if (filePath.startsWith(".gemini/commands/truthmark/realize")) {
3897
4662
  return "realization";
3898
4663
  }
3899
4664
  if (filePath.startsWith(".gemini/commands/truthmark/")) {
3900
4665
  return "truth-sync";
3901
4666
  }
3902
- if (filePath.startsWith(".codex/skills/truthmark-check/")) {
3903
- return "truth-sync";
3904
- }
3905
4667
  if (filePath === config.docs.routing.rootIndex) {
3906
4668
  return "authority";
3907
4669
  }
@@ -4258,7 +5020,7 @@ var checkFrontmatter = async (rootDir, config, markdownPaths, truthDocumentEntri
4258
5020
 
4259
5021
  // src/checks/links.ts
4260
5022
  import fs11 from "fs/promises";
4261
- import path4 from "path";
5023
+ import path5 from "path";
4262
5024
  var pathExists2 = async (absolutePath) => {
4263
5025
  try {
4264
5026
  await fs11.stat(absolutePath);
@@ -4292,7 +5054,7 @@ var checkLinks = async (rootDir, markdownPaths) => {
4292
5054
  if (targetPath.length === 0) {
4293
5055
  continue;
4294
5056
  }
4295
- const absoluteTarget = path4.resolve(path4.dirname(absolutePath), targetPath);
5057
+ const absoluteTarget = path5.resolve(path5.dirname(absolutePath), targetPath);
4296
5058
  const relativeTarget = toRepoRelativePath(rootDir, absoluteTarget);
4297
5059
  try {
4298
5060
  await assertRepoContainment(rootDir, absoluteTarget);
@@ -5164,16 +5926,16 @@ var checkGeneratedSurfaces = async (rootDir, config) => {
5164
5926
  };
5165
5927
 
5166
5928
  // src/impact/build.ts
5167
- import path9 from "path";
5929
+ import path10 from "path";
5168
5930
  import micromatch7 from "micromatch";
5169
5931
 
5170
5932
  // src/repo-index/build.ts
5171
5933
  import fs18 from "fs/promises";
5172
- import path7 from "path";
5934
+ import path8 from "path";
5173
5935
 
5174
5936
  // src/repo-index/file-tree.ts
5175
5937
  import fs16 from "fs/promises";
5176
- import path5 from "path";
5938
+ import path6 from "path";
5177
5939
  import { execa as execa2 } from "execa";
5178
5940
  import fg6 from "fast-glob";
5179
5941
  import matter2 from "gray-matter";
@@ -5193,10 +5955,10 @@ var languageByExtension = /* @__PURE__ */ new Map([
5193
5955
  ]);
5194
5956
  var sourceExtensions = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
5195
5957
  var isJavaScriptLikePath = (filePath) => {
5196
- return sourceExtensions.has(path5.posix.extname(filePath));
5958
+ return sourceExtensions.has(path6.posix.extname(filePath));
5197
5959
  };
5198
5960
  var isTestPath = (filePath) => {
5199
- return filePath.startsWith("tests/") || filePath.includes("/__tests__/") || /(?:^|[./-])(test|spec)\.[cm]?[jt]sx?$/u.test(path5.posix.basename(filePath));
5961
+ return filePath.startsWith("tests/") || filePath.includes("/__tests__/") || /(?:^|[./-])(test|spec)\.[cm]?[jt]sx?$/u.test(path6.posix.basename(filePath));
5200
5962
  };
5201
5963
  var fileKind = (filePath, ignore) => {
5202
5964
  const classification = classifyPath(filePath, ignore);
@@ -5222,7 +5984,7 @@ var fileKind = (filePath, ignore) => {
5222
5984
  };
5223
5985
  var targetHintsForTest = (filePath) => {
5224
5986
  const hints = /* @__PURE__ */ new Set();
5225
- const basename = path5.posix.basename(filePath).replace(/\.(test|spec)\.[cm]?[jt]sx?$/u, "");
5987
+ const basename = path6.posix.basename(filePath).replace(/\.(test|spec)\.[cm]?[jt]sx?$/u, "");
5226
5988
  if (basename.length > 0) {
5227
5989
  hints.add(basename);
5228
5990
  }
@@ -5268,7 +6030,19 @@ var discoverRepoFiles = async (rootDir, ignore) => {
5268
6030
  const docs = [];
5269
6031
  const tests = [];
5270
6032
  for (const filePath of discoveredFiles.filter((entry) => !isIgnoredPath(entry, ignore)).sort()) {
5271
- const extension = path5.posix.extname(filePath);
6033
+ let stat;
6034
+ try {
6035
+ stat = await fs16.stat(path6.join(rootDir, filePath));
6036
+ } catch (error) {
6037
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
6038
+ continue;
6039
+ }
6040
+ throw error;
6041
+ }
6042
+ if (!stat.isFile()) {
6043
+ continue;
6044
+ }
6045
+ const extension = path6.posix.extname(filePath);
5272
6046
  const kind = fileKind(filePath, ignore);
5273
6047
  files.push({
5274
6048
  path: filePath,
@@ -5282,7 +6056,7 @@ var discoverRepoFiles = async (rootDir, ignore) => {
5282
6056
  });
5283
6057
  }
5284
6058
  if (kind === "doc") {
5285
- const source = await fs16.readFile(path5.join(rootDir, filePath), "utf8");
6059
+ const source = await fs16.readFile(path6.join(rootDir, filePath), "utf8");
5286
6060
  const parsed = matter2(source);
5287
6061
  const markdown = parseMarkdownDocument(parsed.content);
5288
6062
  const title = markdown.headings.find((heading) => heading.depth === 1)?.text ?? null;
@@ -5305,7 +6079,7 @@ var discoverRepoFiles = async (rootDir, ignore) => {
5305
6079
 
5306
6080
  // src/repo-index/package-metadata.ts
5307
6081
  import fs17 from "fs/promises";
5308
- import path6 from "path";
6082
+ import path7 from "path";
5309
6083
  import fg7 from "fast-glob";
5310
6084
  var packageManagerFor = async (rootDir, packageDir) => {
5311
6085
  const lockfiles = [
@@ -5317,7 +6091,7 @@ var packageManagerFor = async (rootDir, packageDir) => {
5317
6091
  ];
5318
6092
  for (const [lockfile, manager] of lockfiles) {
5319
6093
  try {
5320
- await fs17.access(path6.join(rootDir, packageDir, lockfile));
6094
+ await fs17.access(path7.join(rootDir, packageDir, lockfile));
5321
6095
  return manager;
5322
6096
  } catch {
5323
6097
  continue;
@@ -5334,8 +6108,8 @@ var discoverPackageMetadata = async (rootDir) => {
5334
6108
  });
5335
6109
  const packages = [];
5336
6110
  for (const packageFile of packageFiles.sort()) {
5337
- const packageDir = path6.posix.dirname(packageFile) === "." ? "" : path6.posix.dirname(packageFile);
5338
- const raw = JSON.parse(await fs17.readFile(path6.join(rootDir, packageFile), "utf8"));
6111
+ const packageDir = path7.posix.dirname(packageFile) === "." ? "" : path7.posix.dirname(packageFile);
6112
+ const raw = JSON.parse(await fs17.readFile(path7.join(rootDir, packageFile), "utf8"));
5339
6113
  const scripts = raw.scripts && typeof raw.scripts === "object" ? Object.keys(raw.scripts).sort() : [];
5340
6114
  packages.push({
5341
6115
  path: packageFile,
@@ -5394,16 +6168,16 @@ var declarationName = (node) => {
5394
6168
  }
5395
6169
  return node.name.text;
5396
6170
  };
5397
- var addExport = (exports, publicSymbols, path12, name, kind) => {
6171
+ var addExport = (exports, publicSymbols, path13, name, kind) => {
5398
6172
  if (!name) {
5399
6173
  return;
5400
6174
  }
5401
- const entry = { path: path12, name, kind };
6175
+ const entry = { path: path13, name, kind };
5402
6176
  exports.push(entry);
5403
6177
  publicSymbols.push(entry);
5404
6178
  };
5405
- var analyzeTypeScriptSource = (path12, source) => {
5406
- const sourceFile = ts.createSourceFile(path12, source, ts.ScriptTarget.Latest, true);
6179
+ var analyzeTypeScriptSource = (path13, source) => {
6180
+ const sourceFile = ts.createSourceFile(path13, source, ts.ScriptTarget.Latest, true);
5407
6181
  const imports = [];
5408
6182
  const exports = [];
5409
6183
  const publicSymbols = [];
@@ -5423,7 +6197,7 @@ var analyzeTypeScriptSource = (path12, source) => {
5423
6197
  }
5424
6198
  }
5425
6199
  imports.push({
5426
- from: path12,
6200
+ from: path13,
5427
6201
  specifier: statement.moduleSpecifier.text,
5428
6202
  imported: sortStrings(imported)
5429
6203
  });
@@ -5432,34 +6206,34 @@ var analyzeTypeScriptSource = (path12, source) => {
5432
6206
  if (ts.isExportDeclaration(statement)) {
5433
6207
  if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
5434
6208
  for (const element of statement.exportClause.elements) {
5435
- addExport(exports, publicSymbols, path12, element.name.text, "re-export");
6209
+ addExport(exports, publicSymbols, path13, element.name.text, "re-export");
5436
6210
  }
5437
6211
  }
5438
6212
  continue;
5439
6213
  }
5440
6214
  if (ts.isFunctionDeclaration(statement) && hasExportModifier(statement)) {
5441
- addExport(exports, publicSymbols, path12, declarationName(statement), "function");
6215
+ addExport(exports, publicSymbols, path13, declarationName(statement), "function");
5442
6216
  continue;
5443
6217
  }
5444
6218
  if (ts.isClassDeclaration(statement) && hasExportModifier(statement)) {
5445
- addExport(exports, publicSymbols, path12, declarationName(statement), "class");
6219
+ addExport(exports, publicSymbols, path13, declarationName(statement), "class");
5446
6220
  continue;
5447
6221
  }
5448
6222
  if (ts.isInterfaceDeclaration(statement) && hasExportModifier(statement)) {
5449
- addExport(exports, publicSymbols, path12, declarationName(statement), "interface");
6223
+ addExport(exports, publicSymbols, path13, declarationName(statement), "interface");
5450
6224
  continue;
5451
6225
  }
5452
6226
  if (ts.isTypeAliasDeclaration(statement) && hasExportModifier(statement)) {
5453
- addExport(exports, publicSymbols, path12, declarationName(statement), "type");
6227
+ addExport(exports, publicSymbols, path13, declarationName(statement), "type");
5454
6228
  continue;
5455
6229
  }
5456
6230
  if (ts.isEnumDeclaration(statement) && hasExportModifier(statement)) {
5457
- addExport(exports, publicSymbols, path12, declarationName(statement), "enum");
6231
+ addExport(exports, publicSymbols, path13, declarationName(statement), "enum");
5458
6232
  continue;
5459
6233
  }
5460
6234
  if (ts.isVariableStatement(statement) && hasExportModifier(statement)) {
5461
6235
  for (const declaration of statement.declarationList.declarations) {
5462
- addExport(exports, publicSymbols, path12, declarationName(declaration), "const");
6236
+ addExport(exports, publicSymbols, path13, declarationName(declaration), "const");
5463
6237
  }
5464
6238
  }
5465
6239
  }
@@ -5490,7 +6264,7 @@ var buildRepoIndex = async (cwd) => {
5490
6264
  if (!isJavaScriptLikePath(file.path)) {
5491
6265
  continue;
5492
6266
  }
5493
- const source = await fs18.readFile(path7.join(rootDir, file.path), "utf8");
6267
+ const source = await fs18.readFile(path8.join(rootDir, file.path), "utf8");
5494
6268
  const analysis = analyzeTypeScriptSource(file.path, source);
5495
6269
  imports.push(...analysis.imports);
5496
6270
  exports.push(...analysis.exports);
@@ -5522,7 +6296,7 @@ import { execa as execa4 } from "execa";
5522
6296
 
5523
6297
  // src/git/changes.ts
5524
6298
  import fs19 from "fs/promises";
5525
- import path8 from "path";
6299
+ import path9 from "path";
5526
6300
  import { execa as execa3 } from "execa";
5527
6301
  var normalizePath3 = (filePath) => {
5528
6302
  return filePath.replaceAll("\\", "/").replace(/^\.\//u, "");
@@ -5577,7 +6351,7 @@ var getUncommittedChanges = async (cwd) => {
5577
6351
  const deletedPathCandidates = /* @__PURE__ */ new Set([...stagedDeletedPaths, ...unstagedDeletedPaths]);
5578
6352
  for (const deletedPath of deletedPathCandidates) {
5579
6353
  const change = getOrCreateChange(changesByPath, deletedPath);
5580
- change.deleted = !await pathExists4(path8.join(rootDir, deletedPath));
6354
+ change.deleted = !await pathExists4(path9.join(rootDir, deletedPath));
5581
6355
  }
5582
6356
  return Array.from(changesByPath.values()).sort((left, right) => {
5583
6357
  return left.path.localeCompare(right.path);
@@ -5698,7 +6472,7 @@ var resolveImportPath = (importEdge) => {
5698
6472
  if (!importEdge.specifier.startsWith(".")) {
5699
6473
  return null;
5700
6474
  }
5701
- const basePath = path9.posix.normalize(path9.posix.join(path9.posix.dirname(importEdge.from), importEdge.specifier));
6475
+ const basePath = path10.posix.normalize(path10.posix.join(path10.posix.dirname(importEdge.from), importEdge.specifier));
5702
6476
  const withoutExtension = basePath.replace(/\.[cm]?[jt]sx?$/u, "");
5703
6477
  return withoutExtension;
5704
6478
  };
@@ -5711,7 +6485,7 @@ var importTargetsChangedFile = (importEdge, changedPath) => {
5711
6485
  };
5712
6486
  var pathSegments = (filePath) => filePath.split("/").filter(Boolean);
5713
6487
  var testHintMatchesChangedFile = (hints, changedPath) => {
5714
- const changedBaseName = path9.posix.basename(changedPath);
6488
+ const changedBaseName = path10.posix.basename(changedPath);
5715
6489
  const changedSegments = pathSegments(changedPath);
5716
6490
  return hints.some((hint) => changedBaseName.startsWith(hint) || changedSegments.includes(hint));
5717
6491
  };
@@ -5872,7 +6646,7 @@ import fg8 from "fast-glob";
5872
6646
 
5873
6647
  // src/evidence/parse.ts
5874
6648
  import fs20 from "fs/promises";
5875
- import path10 from "path";
6649
+ import path11 from "path";
5876
6650
  import matter3 from "gray-matter";
5877
6651
  import { parse as parse3 } from "yaml";
5878
6652
  var evidenceBlockPattern = /```ya?ml\s*\n([\s\S]*?)```/giu;
@@ -5881,9 +6655,9 @@ var normalizeReferencePath = (truthDocPath, referencePath) => {
5881
6655
  const strippedPath = referencePath.split("#")[0]?.trim() ?? "";
5882
6656
  const isRepoRelative = repoRootPrefixes.some((prefix) => strippedPath.startsWith(prefix));
5883
6657
  if (!isRepoRelative && (strippedPath.startsWith(".") || !strippedPath.includes("/"))) {
5884
- return path10.posix.normalize(path10.posix.join(path10.posix.dirname(truthDocPath), strippedPath));
6658
+ return path11.posix.normalize(path11.posix.join(path11.posix.dirname(truthDocPath), strippedPath));
5885
6659
  }
5886
- return path10.posix.normalize(strippedPath);
6660
+ return path11.posix.normalize(strippedPath);
5887
6661
  };
5888
6662
  var toEvidenceReference = (truthDocPath, raw) => {
5889
6663
  if (!raw || typeof raw !== "object" || !("path" in raw) || typeof raw.path !== "string") {
@@ -5900,7 +6674,7 @@ var toEvidenceReference = (truthDocPath, raw) => {
5900
6674
  };
5901
6675
  };
5902
6676
  var parseEvidenceReferences = async (rootDir, truthDocPath) => {
5903
- const source = await fs20.readFile(path10.join(rootDir, truthDocPath), "utf8");
6677
+ const source = await fs20.readFile(path11.join(rootDir, truthDocPath), "utf8");
5904
6678
  const parsed = matter3(source);
5905
6679
  const references = [];
5906
6680
  const sourceOfTruth = Array.isArray(parsed.data.source_of_truth) ? parsed.data.source_of_truth : [];
@@ -6138,7 +6912,7 @@ var runCheck = async (cwd, options = {}) => {
6138
6912
 
6139
6913
  // src/context-pack/build.ts
6140
6914
  import fs22 from "fs/promises";
6141
- import path11 from "path";
6915
+ import path12 from "path";
6142
6916
  import fg9 from "fast-glob";
6143
6917
  var uniqueSorted2 = (values) => [...new Set(values)].sort();
6144
6918
  var repoRootPrefixes2 = [".codex/", ".github/", ".truthmark/", "docs/", "src/", "tests/"];
@@ -6149,12 +6923,12 @@ var normalizeDocReferencePath = (docPath, referencePath) => {
6149
6923
  return null;
6150
6924
  }
6151
6925
  const isRepoRelative = repoRootPrefixes2.some((prefix) => strippedPath.startsWith(prefix));
6152
- const normalized = isRepoRelative ? path11.posix.normalize(strippedPath) : path11.posix.normalize(path11.posix.join(path11.posix.dirname(docPath), strippedPath));
6926
+ const normalized = isRepoRelative ? path12.posix.normalize(strippedPath) : path12.posix.normalize(path12.posix.join(path12.posix.dirname(docPath), strippedPath));
6153
6927
  return normalized === ".." || normalized.startsWith("../") ? null : normalized;
6154
6928
  };
6155
6929
  var readIfExists = async (rootDir, filePath) => {
6156
6930
  try {
6157
- return await fs22.readFile(path11.join(rootDir, filePath), "utf8");
6931
+ return await fs22.readFile(path12.join(rootDir, filePath), "utf8");
6158
6932
  } catch {
6159
6933
  return null;
6160
6934
  }
@@ -6294,6 +7068,319 @@ var renderContextPackMarkdown = (pack) => {
6294
7068
  `;
6295
7069
  };
6296
7070
 
7071
+ // src/cli/handlers.ts
7072
+ import fs23 from "fs/promises";
7073
+
7074
+ // src/agents/workflow-helper-validation.ts
7075
+ import { parse as parseYaml2 } from "yaml";
7076
+ var escapeRegex = (value) => value.split("").map((char) => ".+*?^$()[]{}|\\".includes(char) ? `\\${char}` : char).join("");
7077
+ var hasLabel = (text, label) => {
7078
+ const escaped = escapeRegex(label);
7079
+ return new RegExp(String.raw`(^|\n)\s*(?:#{1,6}\s*)?${escaped}\s*:?\s*(\n|$)`, "iu").test(
7080
+ text
7081
+ );
7082
+ };
7083
+ var getSection = (text, label) => {
7084
+ const lines = text.split(/\r?\n/u);
7085
+ const labelPattern = new RegExp(String.raw`^(?:#{1,6}\s*)?${escapeRegex(label)}\s*:?\s*$`, "iu");
7086
+ const sectionHeaderPattern = /^(?:#{1,6}\s*)?[A-Z][A-Za-z0-9 ]+:\s*$/u;
7087
+ const startIndex = lines.findIndex((line) => labelPattern.test(line));
7088
+ if (startIndex === -1) {
7089
+ return null;
7090
+ }
7091
+ const nextSectionOffset = lines.slice(startIndex + 1).findIndex((line) => sectionHeaderPattern.test(line));
7092
+ const endIndex = nextSectionOffset === -1 ? lines.length : startIndex + 1 + nextSectionOffset;
7093
+ return lines.slice(startIndex + 1, endIndex).join("\n").trim();
7094
+ };
7095
+ var requireBulletSection = (text, label, errors, checks) => {
7096
+ const section = getSection(text, label);
7097
+ if (section === null) {
7098
+ errors.push(`missing required section: ${label}`);
7099
+ return;
7100
+ }
7101
+ if (!/^-\s+\S/mu.test(section)) {
7102
+ errors.push(`${label} must include at least one bullet`);
7103
+ return;
7104
+ }
7105
+ checks.push(label);
7106
+ };
7107
+ var validateEvidenceChecked = (text, errors, checks) => {
7108
+ const section = getSection(text, "Evidence checked");
7109
+ if (section === null || section === "") {
7110
+ errors.push("Evidence checked must include at least one structured entry");
7111
+ return;
7112
+ }
7113
+ const entries = section.split(/\n(?=-\s+)/u).map((entry) => entry.trim()).filter(Boolean);
7114
+ if (entries.length === 0) {
7115
+ errors.push("Evidence checked must include at least one structured entry");
7116
+ return;
7117
+ }
7118
+ const entryPattern = /^- Claim:\s*\S[^\n]*\n {2,}Evidence:\s*\S[^\n]*\n {2,}Result:\s*(supported|narrowed|removed|blocked)\s*$/iu;
7119
+ for (const [index, entry] of entries.entries()) {
7120
+ if (!entryPattern.test(entry)) {
7121
+ errors.push(
7122
+ `Evidence checked entry ${index + 1} must match '- Claim: ...' followed by indented 'Evidence: ...' and 'Result: supported | narrowed | removed | blocked'`
7123
+ );
7124
+ }
7125
+ }
7126
+ if (errors.length === 0) {
7127
+ checks.push(
7128
+ "Evidence checked entries include Claim:, Evidence:, and Result: supported | narrowed | removed | blocked"
7129
+ );
7130
+ }
7131
+ };
7132
+ var validateHelperScriptEntries = (entries, requiredHelpers, errors, checks) => {
7133
+ if (entries === void 0 || entries.length === 0) {
7134
+ errors.push("Helper scripts must include status for optional helpers");
7135
+ return;
7136
+ }
7137
+ const statusPattern = /^(?:-\s*)?([a-z0-9-]+):\s*(?:ran,\s*passed|skipped,\s*\S.*)$/iu;
7138
+ const validHelperIds = /* @__PURE__ */ new Set();
7139
+ for (const entry of entries) {
7140
+ const match = entry.trim().match(statusPattern);
7141
+ if (match === null) {
7142
+ errors.push(
7143
+ "Helper scripts entries must match '- helper-id: ran, passed' or '- helper-id: skipped, reason'; ran, failed is not valid for completed reports"
7144
+ );
7145
+ continue;
7146
+ }
7147
+ validHelperIds.add(match[1].toLowerCase());
7148
+ }
7149
+ for (const helperId of requiredHelpers) {
7150
+ if (!validHelperIds.has(helperId.toLowerCase())) {
7151
+ errors.push(`missing Helper scripts status: ${helperId}`);
7152
+ }
7153
+ }
7154
+ if (errors.length === 0) {
7155
+ checks.push(`Helper scripts statuses include ${requiredHelpers.join(", ")}`);
7156
+ }
7157
+ };
7158
+ var validateHelperScripts = (text, requiredHelpers, errors, checks) => {
7159
+ const section = getSection(text, "Helper scripts");
7160
+ const entries = section?.split(/\n/u).map((entry) => entry.trim()).filter(Boolean);
7161
+ validateHelperScriptEntries(entries, requiredHelpers, errors, checks);
7162
+ };
7163
+ var validateTruthSyncReportText = (text) => {
7164
+ const helper = "validate-sync-report";
7165
+ const statusMatch = text.match(/^\s*Truth Sync:\s*(completed|blocked|skipped)\b/imu);
7166
+ if (statusMatch === null) {
7167
+ return {
7168
+ ok: false,
7169
+ helper,
7170
+ errors: ["missing Truth Sync status: expected completed, blocked, or skipped"]
7171
+ };
7172
+ }
7173
+ const status = statusMatch[1].toLowerCase();
7174
+ const checks = [`status: ${status}`];
7175
+ const errors = [];
7176
+ if (status === "completed") {
7177
+ try {
7178
+ const report = parseTruthSyncReport(text.trimStart());
7179
+ for (const [label, items] of [
7180
+ ["Changed code reviewed", report.changedCode],
7181
+ ["Ownership reviewed", report.ownershipReviewed],
7182
+ ["Truth docs updated", report.truthDocsUpdated],
7183
+ ["Notes", report.notes]
7184
+ ]) {
7185
+ if (items.length === 0) {
7186
+ errors.push(`${label} must include at least one bullet`);
7187
+ } else {
7188
+ checks.push(label);
7189
+ }
7190
+ }
7191
+ if (report.evidenceChecked.length === 0) {
7192
+ errors.push("Evidence checked must include at least one structured entry");
7193
+ } else {
7194
+ checks.push(
7195
+ "Evidence checked entries include Claim:, Evidence:, and Result: supported | narrowed | removed | blocked"
7196
+ );
7197
+ }
7198
+ validateHelperScriptEntries(
7199
+ report.helperScripts,
7200
+ ["validate-write-lease"],
7201
+ errors,
7202
+ checks
7203
+ );
7204
+ } catch (error) {
7205
+ errors.push(error instanceof Error ? error.message : "invalid Truth Sync report");
7206
+ }
7207
+ } else if (status === "skipped") {
7208
+ requireBulletSection(text, "Reason", errors, checks);
7209
+ } else if (status === "blocked") {
7210
+ requireBulletSection(text, "Reason", errors, checks);
7211
+ requireBulletSection(text, "Files requiring manual review", errors, checks);
7212
+ requireBulletSection(text, "Next action", errors, checks);
7213
+ }
7214
+ return errors.length > 0 ? { ok: false, helper, errors } : { ok: true, helper, checks };
7215
+ };
7216
+ var validateTruthDocumentReportText = (text) => {
7217
+ const helper = "validate-document-report";
7218
+ const statusMatch = text.match(/^\s*Truth Document:\s*(completed|blocked)\b/imu);
7219
+ if (statusMatch === null) {
7220
+ return {
7221
+ ok: false,
7222
+ helper,
7223
+ errors: ["missing Truth Document status: expected completed or blocked"]
7224
+ };
7225
+ }
7226
+ const status = statusMatch[1].toLowerCase();
7227
+ const checks = [`status: ${status}`];
7228
+ const errors = [];
7229
+ if (status === "completed") {
7230
+ for (const label of ["Implementation reviewed", "Ownership reviewed", "Notes"]) {
7231
+ requireBulletSection(text, label, errors, checks);
7232
+ }
7233
+ if (hasLabel(text, "Evidence checked")) {
7234
+ checks.push("Evidence checked");
7235
+ } else {
7236
+ errors.push("missing required section: Evidence checked");
7237
+ }
7238
+ if (hasLabel(text, "Helper scripts")) {
7239
+ checks.push("Helper scripts");
7240
+ } else {
7241
+ errors.push("missing required section: Helper scripts");
7242
+ }
7243
+ const truthDocsUpdated = getSection(text, "Truth docs updated");
7244
+ const truthDocsCreated = getSection(text, "Truth docs created");
7245
+ if (truthDocsUpdated === null && truthDocsCreated === null) {
7246
+ errors.push("missing required section: Truth docs updated or Truth docs created");
7247
+ } else if (![truthDocsUpdated, truthDocsCreated].some(
7248
+ (section) => section !== null && /^-\s+\S/mu.test(section)
7249
+ )) {
7250
+ errors.push("Truth docs updated or Truth docs created must include at least one bullet");
7251
+ } else {
7252
+ checks.push("Truth docs updated or created");
7253
+ }
7254
+ validateEvidenceChecked(text, errors, checks);
7255
+ validateHelperScripts(text, ["validate-write-lease"], errors, checks);
7256
+ } else if (status === "blocked") {
7257
+ requireBulletSection(text, "Reason", errors, checks);
7258
+ }
7259
+ return errors.length > 0 ? { ok: false, helper, errors } : { ok: true, helper, checks };
7260
+ };
7261
+ var cleanPathValue = (value) => value.trim().replace(/^["']|["']$/gu, "");
7262
+ var normalizePath5 = (value) => cleanPathValue(value).replace(/^\.\//u, "");
7263
+ var windowsDriveAbsolutePattern = /^[A-Za-z]:[\\/]/u;
7264
+ var uncPathPattern = /^[/\\]{2}[^/\\]+[/\\]+[^/\\]+/u;
7265
+ var isUnsafePathValue = (value) => {
7266
+ const cleanValue = cleanPathValue(value);
7267
+ const pathValue = cleanValue.endsWith("/**") ? cleanValue.slice(0, -3) : cleanValue;
7268
+ return pathValue.startsWith("/") || pathValue.startsWith("\\") || windowsDriveAbsolutePattern.test(pathValue) || uncPathPattern.test(pathValue) || pathValue.split(/[\\/]+/u).includes("..");
7269
+ };
7270
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7271
+ var findWriteLeaseRecord = (value) => {
7272
+ if (!isRecord(value)) {
7273
+ return null;
7274
+ }
7275
+ if (Object.prototype.hasOwnProperty.call(value, "allowedWrites") || Object.prototype.hasOwnProperty.call(value, "forbiddenWrites")) {
7276
+ return value;
7277
+ }
7278
+ for (const nestedKey of ["writeLease", "lease"]) {
7279
+ const nested = value[nestedKey];
7280
+ if (isRecord(nested)) {
7281
+ return nested;
7282
+ }
7283
+ }
7284
+ return value;
7285
+ };
7286
+ var readStringArray = (record, field, errors) => {
7287
+ const value = record[field];
7288
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
7289
+ errors.push(`${field} must be an array of strings`);
7290
+ return [];
7291
+ }
7292
+ return value;
7293
+ };
7294
+ var parseListFields = (text) => {
7295
+ const errors = [];
7296
+ let parsed;
7297
+ try {
7298
+ parsed = parseYaml2(text);
7299
+ } catch (error) {
7300
+ return {
7301
+ allowedWrites: [],
7302
+ forbiddenWrites: [],
7303
+ errors: [
7304
+ `manual-validation required: invalid write lease YAML${error instanceof Error ? `: ${error.message}` : ""}`
7305
+ ]
7306
+ };
7307
+ }
7308
+ const record = findWriteLeaseRecord(parsed);
7309
+ if (record === null) {
7310
+ return {
7311
+ allowedWrites: [],
7312
+ forbiddenWrites: [],
7313
+ errors: ["write lease YAML must be an object"]
7314
+ };
7315
+ }
7316
+ return {
7317
+ allowedWrites: readStringArray(record, "allowedWrites", errors),
7318
+ forbiddenWrites: readStringArray(record, "forbiddenWrites", errors),
7319
+ errors
7320
+ };
7321
+ };
7322
+ var isSupportedPattern = (pattern) => {
7323
+ const withoutTrailingGlob = pattern.endsWith("/**") ? pattern.slice(0, -3) : pattern;
7324
+ return !/[?*[\]{}]/u.test(withoutTrailingGlob);
7325
+ };
7326
+ var matchesPattern = (filePath, pattern) => {
7327
+ if (pattern.endsWith("/**")) {
7328
+ const prefix = pattern.slice(0, -3).replace(/\/+$/u, "");
7329
+ return filePath === prefix || filePath.startsWith(`${prefix}/`);
7330
+ }
7331
+ return filePath === pattern;
7332
+ };
7333
+ var validateWriteLeaseText = (leaseText, changedText) => {
7334
+ const helper = "validate-write-lease";
7335
+ const parsed = parseListFields(leaseText);
7336
+ const rawAllowedWrites = parsed.allowedWrites.map(cleanPathValue).filter(Boolean);
7337
+ const rawForbiddenWrites = parsed.forbiddenWrites.map(cleanPathValue).filter(Boolean);
7338
+ const rawChangedFiles = changedText.split(/\r?\n/u).map(cleanPathValue).filter(Boolean);
7339
+ const allowedWrites = rawAllowedWrites.map(normalizePath5).filter(Boolean);
7340
+ const forbiddenWrites = rawForbiddenWrites.map(normalizePath5).filter(Boolean);
7341
+ const changedFiles = rawChangedFiles.map(normalizePath5).filter(Boolean);
7342
+ const checks = [];
7343
+ const errors = [...parsed.errors];
7344
+ for (const pattern of rawAllowedWrites) {
7345
+ if (isUnsafePathValue(pattern)) {
7346
+ errors.push(`invalid allowedWrites path: ${pattern}`);
7347
+ }
7348
+ }
7349
+ for (const pattern of rawForbiddenWrites) {
7350
+ if (isUnsafePathValue(pattern)) {
7351
+ errors.push(`invalid forbiddenWrites path: ${pattern}`);
7352
+ }
7353
+ }
7354
+ for (const filePath of rawChangedFiles) {
7355
+ if (isUnsafePathValue(filePath)) {
7356
+ errors.push(`invalid changed file path: ${filePath}`);
7357
+ }
7358
+ }
7359
+ for (const pattern of [...allowedWrites, ...forbiddenWrites]) {
7360
+ if (!isSupportedPattern(pattern)) {
7361
+ errors.push(`manual-validation required: unsupported write pattern ${pattern}`);
7362
+ }
7363
+ }
7364
+ if (allowedWrites.length === 0) {
7365
+ errors.push("manual-validation required: no allowedWrites entries found");
7366
+ }
7367
+ if (errors.length === 0) {
7368
+ for (const filePath of changedFiles) {
7369
+ if (!allowedWrites.some((pattern) => matchesPattern(filePath, pattern))) {
7370
+ errors.push(`${filePath} is outside allowedWrites`);
7371
+ continue;
7372
+ }
7373
+ const forbiddenPattern = forbiddenWrites.find((pattern) => matchesPattern(filePath, pattern));
7374
+ if (forbiddenPattern !== void 0) {
7375
+ errors.push(`${filePath} matches forbiddenWrites pattern ${forbiddenPattern}`);
7376
+ continue;
7377
+ }
7378
+ checks.push(filePath);
7379
+ }
7380
+ }
7381
+ return errors.length > 0 ? { ok: false, helper, errors } : { ok: true, helper, checks };
7382
+ };
7383
+
6297
7384
  // src/cli/handlers.ts
6298
7385
  var runConfig2 = async (options) => {
6299
7386
  return runConfig(process.cwd(), options);
@@ -6350,6 +7437,33 @@ var isContextPackWorkflow = (value) => {
6350
7437
  var isContextPackFormat = (value) => {
6351
7438
  return value === void 0 || value === "json" || value === "markdown";
6352
7439
  };
7440
+ var readHelperFile = async (filePath, helper) => {
7441
+ try {
7442
+ return await fs23.readFile(filePath, "utf8");
7443
+ } catch (error) {
7444
+ const message = error instanceof Error ? error.message : String(error);
7445
+ return { ok: false, helper, errors: [`could not read file: ${message}`] };
7446
+ }
7447
+ };
7448
+ var runValidateSyncReport = async (reportFile) => {
7449
+ const text = await readHelperFile(reportFile, "validate-sync-report");
7450
+ return typeof text === "string" ? validateTruthSyncReportText(text) : text;
7451
+ };
7452
+ var runValidateDocumentReport = async (reportFile) => {
7453
+ const text = await readHelperFile(reportFile, "validate-document-report");
7454
+ return typeof text === "string" ? validateTruthDocumentReportText(text) : text;
7455
+ };
7456
+ var runValidateWriteLease = async (leaseFile, changedFilesFile) => {
7457
+ const leaseText = await readHelperFile(leaseFile, "validate-write-lease");
7458
+ if (typeof leaseText !== "string") {
7459
+ return leaseText;
7460
+ }
7461
+ const changedText = await readHelperFile(changedFilesFile, "validate-write-lease");
7462
+ if (typeof changedText !== "string") {
7463
+ return changedText;
7464
+ }
7465
+ return validateWriteLeaseText(leaseText, changedText);
7466
+ };
6353
7467
  var runContext = async (options) => {
6354
7468
  if (!isContextPackWorkflow(options.workflow)) {
6355
7469
  return {
@@ -6406,6 +7520,28 @@ var writeContextResult = (result, options) => {
6406
7520
  }
6407
7521
  writeResult(result, options);
6408
7522
  };
7523
+ var renderValidationHuman = (result) => {
7524
+ if (result.ok === true) {
7525
+ return [`${result.helper}: ok`, ...result.checks.map((check) => `- ${check}`)].join("\n");
7526
+ }
7527
+ return [`${result.helper}: failed`, ...result.errors.map((error) => `- ${error}`)].join("\n");
7528
+ };
7529
+ var toValidationCommandResult = (command, result) => ({
7530
+ command,
7531
+ summary: result.ok ? "Validation passed" : "Validation failed",
7532
+ diagnostics: [],
7533
+ data: {
7534
+ validation: result
7535
+ }
7536
+ });
7537
+ var writeValidationResult = (command, result, options) => {
7538
+ const output = options.json ? renderJson(toValidationCommandResult(command, result)) : renderValidationHuman(result);
7539
+ process.stdout.write(`${output}
7540
+ `);
7541
+ if (!result.ok) {
7542
+ process.exitCode = 1;
7543
+ }
7544
+ };
6409
7545
  var addJsonOption = (command) => {
6410
7546
  return command.option("--json", "Render command output as JSON");
6411
7547
  };
@@ -6449,6 +7585,32 @@ var buildProgram = () => {
6449
7585
  options
6450
7586
  );
6451
7587
  });
7588
+ const validate = program.command("validate").description("Run optional Truthmark workflow helper validators from the installed CLI.");
7589
+ addJsonOption(
7590
+ validate.command("sync-report").description("Validate a Truth Sync report file.").argument("<report-file>", "Truth Sync report file")
7591
+ ).action(async (reportFile, options) => {
7592
+ writeValidationResult("validate sync-report", await runValidateSyncReport(reportFile), options);
7593
+ });
7594
+ addJsonOption(
7595
+ validate.command("document-report").description("Validate a Truth Document report file.").argument("<report-file>", "Truth Document report file")
7596
+ ).action(async (reportFile, options) => {
7597
+ writeValidationResult(
7598
+ "validate document-report",
7599
+ await runValidateDocumentReport(reportFile),
7600
+ options
7601
+ );
7602
+ });
7603
+ addJsonOption(
7604
+ validate.command("write-lease").description("Validate a workflow write lease or worker report against changed files.").argument("<lease-or-report-file>", "Lease or worker report file").argument("<changed-files-file>", "Newline-separated changed file list")
7605
+ ).action(
7606
+ async (leaseOrReportFile, changedFilesFile, options) => {
7607
+ writeValidationResult(
7608
+ "validate write-lease",
7609
+ await runValidateWriteLease(leaseOrReportFile, changedFilesFile),
7610
+ options
7611
+ );
7612
+ }
7613
+ );
6452
7614
  return program;
6453
7615
  };
6454
7616