truthmark 1.4.0 → 1.5.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
@@ -1567,6 +1567,27 @@ var renderCopilotCustomAgentModeSection = (agents, parentRule, writeAgents = [])
1567
1567
  `- ${parentRule}`
1568
1568
  ].join("\n");
1569
1569
  };
1570
+ var renderGeminiSubagentModeSection = (agents, parentRule, writeAgents = []) => {
1571
+ const mentions = agents.map((agent) => `@${agent.replace(/_/gu, "-")}`);
1572
+ const writeMentions = writeAgents.map((agent) => `@${agent.replace(/_/gu, "-")}`);
1573
+ const writeAgentLines = writeMentions.length > 0 ? [
1574
+ `- dispatch write-capable project subagents only with explicit write leases: ${writeMentions.join(", ")}`,
1575
+ "- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
1576
+ "- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
1577
+ "- parent must inspect the actual checkout diff against each lease before accepting a worker report"
1578
+ ] : [];
1579
+ const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
1580
+ const readOnlySubagentLabel = writeAgents.length > 0 ? "read-only subagents" : "subagents";
1581
+ return [
1582
+ "Gemini CLI subagent mode:",
1583
+ "- use automatically when this workflow runs in Gemini CLI and the parent agent chooses bounded project subagent fan-out",
1584
+ `- dispatch read-only project subagents ${readOnlyScope}: ${mentions.join(", ")}`,
1585
+ `- ${readOnlySubagentLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
1586
+ `- parent supplies bounded evidence shards; ${readOnlySubagentLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
1587
+ ...writeAgentLines,
1588
+ `- ${parentRule}`
1589
+ ].join("\n");
1590
+ };
1570
1591
  var defaultAgentConfig = () => {
1571
1592
  return createDefaultConfig();
1572
1593
  };
@@ -1678,7 +1699,50 @@ var renderDefaultStandards = (documents) => {
1678
1699
  return DEFAULT_STANDARDS.filter((template) => !existingPaths.has(template.path));
1679
1700
  };
1680
1701
 
1702
+ // src/templates/workflow-surfaces.ts
1703
+ import { stringify as stringify2 } from "yaml";
1704
+
1681
1705
  // src/agents/workflow-manifest.ts
1706
+ var TRUTHMARK_CLI_RUNNER = `truthmark>=${TRUTHMARK_VERSION}`;
1707
+ var VALIDATE_SYNC_REPORT_HELPER = {
1708
+ id: "validate-sync-report",
1709
+ optional: true,
1710
+ runner: TRUTHMARK_CLI_RUNNER,
1711
+ command: { argv: ["truthmark", "validate", "sync-report", "<report-file>", "--json"] },
1712
+ inputs: ["sync report file"],
1713
+ output: "json",
1714
+ writes: false,
1715
+ fallback: "manually validate support/report-template.md and check Evidence checked entries match Claim, indented Evidence, and Result: supported | narrowed | removed | blocked"
1716
+ };
1717
+ var VALIDATE_DOCUMENT_REPORT_HELPER = {
1718
+ id: "validate-document-report",
1719
+ optional: true,
1720
+ runner: TRUTHMARK_CLI_RUNNER,
1721
+ command: { argv: ["truthmark", "validate", "document-report", "<report-file>", "--json"] },
1722
+ inputs: ["document report file"],
1723
+ output: "json",
1724
+ writes: false,
1725
+ fallback: "manually validate support/report-template.md required sections and structured Evidence checked entries"
1726
+ };
1727
+ var VALIDATE_WRITE_LEASE_HELPER = {
1728
+ id: "validate-write-lease",
1729
+ optional: true,
1730
+ runner: TRUTHMARK_CLI_RUNNER,
1731
+ command: {
1732
+ argv: [
1733
+ "truthmark",
1734
+ "validate",
1735
+ "write-lease",
1736
+ "<lease-or-report-file>",
1737
+ "<changed-files-file>",
1738
+ "--json"
1739
+ ]
1740
+ },
1741
+ inputs: ["lease or worker report yaml", "changed file list"],
1742
+ output: "json",
1743
+ writes: false,
1744
+ fallback: "manually compare declared allowedWrites and forbiddenWrites with the actual changed files"
1745
+ };
1682
1746
  var TRUTHMARK_WORKFLOW_MANIFEST = {
1683
1747
  "truthmark-sync": {
1684
1748
  id: "truthmark-sync",
@@ -1717,10 +1781,12 @@ var TRUTHMARK_WORKFLOW_MANIFEST = {
1717
1781
  "Truth docs updated",
1718
1782
  "Truth docs split",
1719
1783
  "Evidence checked",
1784
+ "Helper scripts",
1720
1785
  "Notes"
1721
1786
  ],
1722
1787
  subagents: ["truth_route_auditor", "truth_claim_verifier"],
1723
- writeSubagents: ["truth_doc_writer"]
1788
+ writeSubagents: ["truth_doc_writer"],
1789
+ helpers: [VALIDATE_SYNC_REPORT_HELPER, VALIDATE_WRITE_LEASE_HELPER]
1724
1790
  },
1725
1791
  "truthmark-structure": {
1726
1792
  id: "truthmark-structure",
@@ -1801,10 +1867,12 @@ var TRUTHMARK_WORKFLOW_MANIFEST = {
1801
1867
  "Truth docs restructured",
1802
1868
  "Routing updated",
1803
1869
  "Evidence checked",
1870
+ "Helper scripts",
1804
1871
  "Notes"
1805
1872
  ],
1806
1873
  subagents: ["truth_route_auditor", "truth_claim_verifier"],
1807
- writeSubagents: ["truth_doc_writer"]
1874
+ writeSubagents: ["truth_doc_writer"],
1875
+ helpers: [VALIDATE_DOCUMENT_REPORT_HELPER, VALIDATE_WRITE_LEASE_HELPER]
1808
1876
  },
1809
1877
  "truthmark-realize": {
1810
1878
  id: "truthmark-realize",
@@ -2012,11 +2080,15 @@ var renderMarkdownExample2 = (content) => {
2012
2080
  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
2081
  var renderTruthDocumentReportExample = (config = defaultAgentConfig()) => {
2014
2082
  const truthDocsRoot = resolveTruthDocsRoot(config);
2083
+ const helperScripts = ["validate-write-lease: skipped, no write lease used"];
2015
2084
  return `Truth Document: completed
2016
2085
 
2017
2086
  Implementation reviewed:
2018
2087
  - src/routing/area-resolver.ts
2019
2088
 
2089
+ Ownership reviewed:
2090
+ - ${config.docs.routing.rootIndex}
2091
+
2020
2092
  Truth docs created:
2021
2093
  - ${truthDocsRoot}/contracts.md
2022
2094
 
@@ -2040,6 +2112,9 @@ ${renderClaimEvidenceCheckedSection([
2040
2112
  }
2041
2113
  ])}
2042
2114
 
2115
+ Helper scripts:
2116
+ ${helperScripts.map((helperScript) => `- ${helperScript}`).join("\n")}
2117
+
2043
2118
  Notes:
2044
2119
  - Documented routing and behavior from route handlers and tests.`;
2045
2120
  };
@@ -2115,6 +2190,12 @@ ${renderTruthDocRestructureGateSection(
2115
2190
  ${ARCHITECTURE_DOC_BOUNDARY_INSTRUCTIONS}
2116
2191
  ${renderHierarchySummary(config)}
2117
2192
  ${DECISION_TRUTH_INSTRUCTIONS}
2193
+ Helper status reporting:
2194
+ - Validate the report body before adding this validator's own success status; the body may omit \`validate-document-report\` while validation is pending.
2195
+ - 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.
2196
+ - If the installed Truthmark CLI is unavailable or the helper is skipped, record \`validate-document-report: skipped, <reason>\` and manually validate the report shape.
2197
+ - 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\`.
2198
+ - Helper output is derived evidence and never replaces direct checkout inspection, evidence review, or parent acceptance.
2118
2199
  Parent post-document verification:
2119
2200
  - verify only truth docs and leased truth routing files changed during document work
2120
2201
  - block on functional code, generated host surfaces, or unrelated diffs caused by document work
@@ -2366,24 +2447,105 @@ var renderBulletSection = (title, items) => {
2366
2447
  return `${title}:
2367
2448
  ${items.map((item) => `- ${item}`).join("\n")}`;
2368
2449
  };
2450
+ var findSection = (source, title) => {
2451
+ return source.split("\n\n").find((candidate) => candidate.startsWith(`${title}:
2452
+ `));
2453
+ };
2454
+ var parseBulletLines = (section) => {
2455
+ return section.split("\n").slice(1).map((line) => {
2456
+ const match = line.match(/^-\s+(.*)$/);
2457
+ return match?.[1];
2458
+ }).filter((line) => line !== void 0);
2459
+ };
2460
+ var parseBulletSection = (source, title) => {
2461
+ const section = findSection(source, title);
2462
+ if (!section) {
2463
+ return [];
2464
+ }
2465
+ return parseBulletLines(section);
2466
+ };
2467
+ var parseOptionalBulletSection = (source, title) => {
2468
+ const section = findSection(source, title);
2469
+ if (!section) {
2470
+ return void 0;
2471
+ }
2472
+ return parseBulletLines(section);
2473
+ };
2474
+ var isClaimEvidenceResult = (value) => {
2475
+ return ["supported", "narrowed", "removed", "blocked"].includes(value);
2476
+ };
2477
+ var hasContent = (value) => value.trim().length > 0;
2478
+ var parseEvidenceCheckedSection = (source) => {
2479
+ const section = source.split("\n\n").find((candidate) => candidate.startsWith("Evidence checked:\n"));
2480
+ if (!section) {
2481
+ throw new Error("Evidence checked section is required.");
2482
+ }
2483
+ const lines = section.split("\n").slice(1);
2484
+ const items = [];
2485
+ for (let index = 0; index < lines.length; index += 3) {
2486
+ const claimLine = lines[index];
2487
+ const evidenceLine = lines[index + 1];
2488
+ const resultLine = lines[index + 2];
2489
+ if (!claimLine?.startsWith("- Claim: ") || !evidenceLine?.startsWith(" Evidence: ") || !resultLine?.startsWith(" Result: ")) {
2490
+ throw new Error("Evidence checked entries must include Claim, Evidence, and Result fields.");
2491
+ }
2492
+ const result = resultLine.slice(" Result: ".length);
2493
+ const claim = claimLine.slice("- Claim: ".length).trim();
2494
+ const evidence = evidenceLine.slice(" Evidence: ".length).split(" / ").map((value) => value.trim());
2495
+ if (!hasContent(claim)) {
2496
+ throw new Error("Evidence checked claim is required.");
2497
+ }
2498
+ if (evidence.length === 0 || evidence.some((value) => !hasContent(value))) {
2499
+ throw new Error("Evidence checked evidence is required.");
2500
+ }
2501
+ if (!isClaimEvidenceResult(result)) {
2502
+ throw new Error("Evidence checked result is invalid.");
2503
+ }
2504
+ items.push({
2505
+ claim,
2506
+ evidence,
2507
+ result
2508
+ });
2509
+ }
2510
+ return items;
2511
+ };
2369
2512
  var renderTruthSyncCompletedReport = (input) => {
2370
2513
  return [
2371
2514
  "Truth Sync: completed",
2372
2515
  renderBulletSection("Changed code reviewed", input.changedCode),
2516
+ renderBulletSection("Ownership reviewed", input.ownershipReviewed),
2373
2517
  renderBulletSection("Truth docs updated", input.truthDocsUpdated),
2374
2518
  renderClaimEvidenceCheckedSection(input.evidenceChecked),
2519
+ ...input.helperScripts === void 0 ? [] : [renderBulletSection("Helper scripts", input.helperScripts)],
2375
2520
  renderBulletSection("Notes", input.notes)
2376
2521
  ].join("\n\n");
2377
2522
  };
2523
+ var parseTruthSyncReport = (source) => {
2524
+ if (!source.startsWith("Truth Sync: completed")) {
2525
+ throw new Error("Only completed Truth Sync reports can be parsed.");
2526
+ }
2527
+ const helperScripts = parseOptionalBulletSection(source, "Helper scripts");
2528
+ return {
2529
+ status: "completed",
2530
+ changedCode: parseBulletSection(source, "Changed code reviewed"),
2531
+ ownershipReviewed: parseBulletSection(source, "Ownership reviewed"),
2532
+ truthDocsUpdated: parseBulletSection(source, "Truth docs updated"),
2533
+ evidenceChecked: parseEvidenceCheckedSection(source),
2534
+ ...helperScripts === void 0 ? {} : { helperScripts },
2535
+ notes: parseBulletSection(source, "Notes")
2536
+ };
2537
+ };
2378
2538
  var renderTruthSyncBlockedReport = (input) => {
2539
+ const manualReviewFiles = input.manualReviewFiles.filter((file) => file.trim().length > 0);
2540
+ if (manualReviewFiles.length === 0) {
2541
+ throw new Error("Files requiring manual review must include at least one file.");
2542
+ }
2379
2543
  const sections = [
2380
2544
  "Truth Sync: blocked",
2381
- renderBulletSection("Reason", [input.reason])
2545
+ renderBulletSection("Reason", [input.reason]),
2546
+ renderBulletSection("Files requiring manual review", manualReviewFiles),
2547
+ renderBulletSection("Next action", [input.nextAction])
2382
2548
  ];
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
2549
  return [
2388
2550
  ...sections
2389
2551
  ].join("\n\n");
@@ -2397,6 +2559,7 @@ var renderMarkdownExample5 = (content) => {
2397
2559
  var renderTruthSyncSkillBody = (config = defaultAgentConfig(), options = {}) => {
2398
2560
  const truthDocsRoot = resolveTruthDocsRoot(config);
2399
2561
  const workflow = getTruthmarkWorkflow("truthmark-sync");
2562
+ const helperScripts = ["validate-write-lease: skipped, no write lease used"];
2400
2563
  const claudeSubagentMode = options.includeClaudeSubagentMode ? `${renderClaudeSubagentModeSection(
2401
2564
  workflow.subagents ?? [],
2402
2565
  "Parent agent owns Truth Sync acceptance, lease validation, and final report",
@@ -2470,6 +2633,12 @@ Optional validation tooling:
2470
2633
  - do not require the truthmark binary; direct checkout inspection is the canonical path
2471
2634
  - optional validation must not replace agent judgment about docs and routing
2472
2635
  - update Product Decisions and Rationale when a behavior change comes from a decision change
2636
+ Helper status reporting:
2637
+ - Validate the report body before adding this validator's own success status; the body may omit \`validate-sync-report\` while validation is pending.
2638
+ - 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.
2639
+ - If the installed Truthmark CLI is unavailable or the helper is skipped, record \`validate-sync-report: skipped, <reason>\` and manually validate the report shape.
2640
+ - 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\`.
2641
+ - Helper output is derived evidence and never replaces direct checkout inspection, evidence review, or parent acceptance.
2473
2642
  ${renderHierarchySummary(config)}
2474
2643
  ${DECISION_TRUTH_INSTRUCTIONS}
2475
2644
  Parent post-sync verification:
@@ -2477,7 +2646,7 @@ Parent post-sync verification:
2477
2646
  - block on any unrelated diff caused by the sync step
2478
2647
  - block if functional code changed during sync
2479
2648
  - 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
2649
+ - 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
2650
  - verify the updated docs correspond to the reviewed changed-code surface
2482
2651
  - verify the final report records ownership review, structure requirement, split, restructure, or blocked reason when the ownership gate fired
2483
2652
  - 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 +2654,7 @@ Report completion in this shape:
2485
2654
  ${renderMarkdownExample5(
2486
2655
  renderTruthSyncCompletedReport({
2487
2656
  changedCode: ["src/auth/session.ts"],
2657
+ ownershipReviewed: [config.docs.routing.rootIndex],
2488
2658
  truthDocsUpdated: [`${truthDocsRoot}/repository/overview.md`],
2489
2659
  evidenceChecked: [
2490
2660
  {
@@ -2493,6 +2663,7 @@ ${renderMarkdownExample5(
2493
2663
  result: "supported"
2494
2664
  }
2495
2665
  ],
2666
+ helperScripts,
2496
2667
  notes: ["Updated session timeout behavior."]
2497
2668
  })
2498
2669
  )}
@@ -2553,6 +2724,10 @@ var TRUTHMARK_GEMINI_SYNC_COMMAND_PATH = ".gemini/commands/truthmark/sync.toml";
2553
2724
  var TRUTHMARK_GEMINI_REALIZE_COMMAND_PATH = ".gemini/commands/truthmark/realize.toml";
2554
2725
  var TRUTHMARK_GEMINI_CHECK_COMMAND_PATH = ".gemini/commands/truthmark/check.toml";
2555
2726
  var TRUTHMARK_GEMINI_PREVIEW_COMMAND_PATH = ".gemini/commands/truthmark/preview.toml";
2727
+ var TRUTHMARK_GEMINI_ROUTE_AUDITOR_AGENT_PATH = ".gemini/agents/truth-route-auditor.md";
2728
+ var TRUTHMARK_GEMINI_CLAIM_VERIFIER_AGENT_PATH = ".gemini/agents/truth-claim-verifier.md";
2729
+ var TRUTHMARK_GEMINI_DOC_REVIEWER_AGENT_PATH = ".gemini/agents/truth-doc-reviewer.md";
2730
+ var TRUTHMARK_GEMINI_DOC_WRITER_AGENT_PATH = ".gemini/agents/truth-doc-writer.md";
2556
2731
  var TRUTHMARK_COPILOT_STRUCTURE_PROMPT_PATH = ".github/prompts/truthmark-structure.prompt.md";
2557
2732
  var TRUTHMARK_COPILOT_DOCUMENT_PROMPT_PATH = ".github/prompts/truthmark-document.prompt.md";
2558
2733
  var TRUTHMARK_COPILOT_SYNC_PROMPT_PATH = ".github/prompts/truthmark-sync.prompt.md";
@@ -2564,9 +2739,11 @@ var TRUTHMARK_COPILOT_CLAIM_VERIFIER_AGENT_PATH = ".github/agents/truth-claim-ve
2564
2739
  var TRUTHMARK_COPILOT_DOC_REVIEWER_AGENT_PATH = ".github/agents/truth-doc-reviewer.agent.md";
2565
2740
  var TRUTHMARK_COPILOT_DOC_WRITER_AGENT_PATH = ".github/agents/truth-doc-writer.agent.md";
2566
2741
  var renderGeminiCommand = (description, prompt) => {
2742
+ const promptWithArgs = `${prompt.trimEnd()}
2743
+ User focus or arguments: {{args}}`;
2567
2744
  return `description = "${description}"
2568
2745
  prompt = '''
2569
- ${prompt}
2746
+ ${promptWithArgs}
2570
2747
  '''
2571
2748
  `;
2572
2749
  };
@@ -2707,6 +2884,56 @@ Generated by Truthmark ${TRUTHMARK_VERSION}. Rerun truthmark init after upgrades
2707
2884
  ${body}
2708
2885
  `;
2709
2886
  };
2887
+ var renderHelperManifest = (helpers) => {
2888
+ const manifest = {
2889
+ helpers: Object.fromEntries(
2890
+ helpers.map((helper) => [
2891
+ helper.id,
2892
+ {
2893
+ optional: helper.optional,
2894
+ runner: helper.runner,
2895
+ command: helper.command,
2896
+ inputs: helper.inputs,
2897
+ output: helper.output,
2898
+ writes: helper.writes,
2899
+ ...helper.allowedWrites === void 0 ? {} : { allowedWrites: helper.allowedWrites },
2900
+ fallback: helper.fallback
2901
+ }
2902
+ ])
2903
+ )
2904
+ };
2905
+ return [
2906
+ `# Generated by Truthmark ${TRUTHMARK_VERSION}. Rerun truthmark init after upgrades.`,
2907
+ stringify2(manifest, { lineWidth: 0 })
2908
+ ].join("\n");
2909
+ };
2910
+ var renderHelperPolicySupport = (helpers) => {
2911
+ const reportHelperId = helpers.find((helper) => helper.id.endsWith("-report"))?.id ?? helpers[0]?.id;
2912
+ const helperLines = helpers.map(
2913
+ (helper) => `- ${helper.id}: optional ${helper.runner}; manual fallback: ${helper.fallback}`
2914
+ ).join("\n");
2915
+ return renderSkillSupportFile(
2916
+ "Optional Helper CLI Policy",
2917
+ `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.
2918
+
2919
+ Runner detection:
2920
+ - Check the declared Truthmark CLI runner before invoking a helper.
2921
+ - Invoke helpers through the installed \`truthmark validate ... --json\` CLI command using argv-style arguments from helper-manifest.yml.
2922
+ - If unavailable or version-mismatched, treat the helper as skipped and use the manual fallback.
2923
+ - Do not fail the workflow solely because a helper cannot run.
2924
+
2925
+ Available helpers:
2926
+ ${helperLines}
2927
+
2928
+ Final reports should include helper status when helpers are declared for this workflow:
2929
+
2930
+ \`\`\`md
2931
+ Helper scripts:
2932
+ - ${reportHelperId}: ran, passed
2933
+ - validate-write-lease: skipped, no write lease used
2934
+ \`\`\``
2935
+ );
2936
+ };
2710
2937
  var renderStandaloneWorkflowSkillBody = (workflowId, config) => {
2711
2938
  switch (workflowId) {
2712
2939
  case "truthmark-structure":
@@ -2723,10 +2950,11 @@ var renderStandaloneWorkflowSkillBody = (workflowId, config) => {
2723
2950
  return renderTruthCheckSkillBody(config);
2724
2951
  }
2725
2952
  };
2726
- var renderWorkflowEntrypoint = (workflowId, config, supportFiles) => {
2953
+ var renderWorkflowEntrypoint = (workflowId, config, supportFiles, host) => {
2727
2954
  const workflow = getTruthmarkWorkflow(workflowId);
2728
2955
  const definition = WORKFLOW_PACKAGE_DEFINITIONS[workflowId];
2729
2956
  const supportFileList = supportFiles.map((supportFile) => `- ${supportFile}`).join("\n");
2957
+ 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
2958
  return `---
2731
2959
  name: ${workflowId}
2732
2960
  description: ${workflow.description}
@@ -2738,6 +2966,9 @@ truthmark-version: ${TRUTHMARK_VERSION}
2738
2966
  # ${definition.title}
2739
2967
 
2740
2968
  ${definition.use(config)}
2969
+ ${hostUsage === void 0 ? "" : `
2970
+ ${hostUsage}
2971
+ `}
2741
2972
 
2742
2973
  Invocations: ${definition.invocations}
2743
2974
 
@@ -2778,6 +3009,18 @@ var renderWorkflowSubagentSupport = (workflowId, host) => {
2778
3009
  definition.parentRule,
2779
3010
  writeAgents
2780
3011
  );
3012
+ case "github-copilot":
3013
+ return renderCopilotCustomAgentModeSection(
3014
+ readAgents,
3015
+ definition.parentRule,
3016
+ writeAgents
3017
+ );
3018
+ case "gemini-cli":
3019
+ return renderGeminiSubagentModeSection(
3020
+ readAgents,
3021
+ definition.parentRule,
3022
+ writeAgents
3023
+ );
2781
3024
  }
2782
3025
  };
2783
3026
  var renderTruthmarkSkillPackage = ({
@@ -2792,16 +3035,18 @@ var renderTruthmarkSkillPackage = ({
2792
3035
  renderStandaloneWorkflowSkillBody(workflowId, config)
2793
3036
  );
2794
3037
  const subagents = renderWorkflowSubagentSupport(workflowId, host);
3038
+ const helpers = getTruthmarkWorkflow(workflowId).helpers ?? [];
2795
3039
  const supportFiles = [
2796
3040
  "support/procedure.md",
2797
3041
  "support/report-template.md",
2798
- ...subagents === void 0 ? [] : ["support/subagents-and-leases.md"]
3042
+ ...subagents === void 0 ? [] : ["support/subagents-and-leases.md"],
3043
+ ...helpers.length === 0 ? [] : ["helper-manifest.yml", "support/helper-policy.md"]
2799
3044
  ];
2800
3045
  const definition = WORKFLOW_PACKAGE_DEFINITIONS[workflowId];
2801
3046
  const files = [
2802
3047
  {
2803
3048
  path: skillPath,
2804
- content: renderWorkflowEntrypoint(workflowId, config, supportFiles)
3049
+ content: renderWorkflowEntrypoint(workflowId, config, supportFiles, host)
2805
3050
  },
2806
3051
  {
2807
3052
  path: `${supportDirectory}/procedure.md`,
@@ -2827,6 +3072,18 @@ var renderTruthmarkSkillPackage = ({
2827
3072
  )
2828
3073
  });
2829
3074
  }
3075
+ if (helpers.length > 0) {
3076
+ files.push(
3077
+ {
3078
+ path: `${skillDirectory}/helper-manifest.yml`,
3079
+ content: renderHelperManifest(helpers)
3080
+ },
3081
+ {
3082
+ path: `${supportDirectory}/helper-policy.md`,
3083
+ content: renderHelperPolicySupport(helpers)
3084
+ }
3085
+ );
3086
+ }
2830
3087
  return files;
2831
3088
  };
2832
3089
  var normalizeOpenCodePermissionPath = (path12) => {
@@ -2982,6 +3239,45 @@ tools: [read, search, edit]
2982
3239
 
2983
3240
  # Generated by Truthmark ${TRUTHMARK_VERSION}. Rerun truthmark init after upgrades.
2984
3241
 
3242
+ ${instructions}
3243
+ `;
3244
+ };
3245
+ var renderGeminiReadOnlyAgent = ({
3246
+ copilotName,
3247
+ description,
3248
+ instructions
3249
+ }) => {
3250
+ const agentInstructions = renderReadOnlySubagentInstructions(instructions);
3251
+ return `---
3252
+ name: ${copilotName}
3253
+ description: ${description}
3254
+ kind: local
3255
+ tools: [read_file, grep_search]
3256
+ ---
3257
+
3258
+ # Generated by Truthmark ${TRUTHMARK_VERSION}. Rerun truthmark init after upgrades.
3259
+
3260
+ Manual invocation: @${copilotName}
3261
+
3262
+ ${agentInstructions}
3263
+ `;
3264
+ };
3265
+ var renderGeminiWriteAgent = ({
3266
+ copilotName,
3267
+ description,
3268
+ instructions
3269
+ }) => {
3270
+ return `---
3271
+ name: ${copilotName}
3272
+ description: ${description}
3273
+ kind: local
3274
+ tools: [read_file, grep_search, write_file]
3275
+ ---
3276
+
3277
+ # Generated by Truthmark ${TRUTHMARK_VERSION}. Rerun truthmark init after upgrades.
3278
+
3279
+ Manual invocation: @${copilotName} with an explicit parent write lease.
3280
+
2985
3281
  ${instructions}
2986
3282
  `;
2987
3283
  };
@@ -3084,6 +3380,26 @@ var renderTruthmarkCopilotDocWriterAgent = () => {
3084
3380
  TRUTHMARK_WRITE_SUBAGENT_PROFILES.truth_doc_writer
3085
3381
  );
3086
3382
  };
3383
+ var renderTruthmarkGeminiRouteAuditorAgent = () => {
3384
+ return renderGeminiReadOnlyAgent(
3385
+ TRUTHMARK_SUBAGENT_PROFILES.truth_route_auditor
3386
+ );
3387
+ };
3388
+ var renderTruthmarkGeminiClaimVerifierAgent = () => {
3389
+ return renderGeminiReadOnlyAgent(
3390
+ TRUTHMARK_SUBAGENT_PROFILES.truth_claim_verifier
3391
+ );
3392
+ };
3393
+ var renderTruthmarkGeminiDocReviewerAgent = () => {
3394
+ return renderGeminiReadOnlyAgent(
3395
+ TRUTHMARK_SUBAGENT_PROFILES.truth_doc_reviewer
3396
+ );
3397
+ };
3398
+ var renderTruthmarkGeminiDocWriterAgent = () => {
3399
+ return renderGeminiWriteAgent(
3400
+ TRUTHMARK_WRITE_SUBAGENT_PROFILES.truth_doc_writer
3401
+ );
3402
+ };
3087
3403
  var renderTruthmarkClaudeRouteAuditorAgent = () => {
3088
3404
  return renderClaudeReadOnlyAgent(
3089
3405
  TRUTHMARK_SUBAGENT_PROFILES.truth_route_auditor
@@ -3644,6 +3960,42 @@ var claudeFiles = (config, block) => {
3644
3960
  var copilotFiles = (config, block) => {
3645
3961
  const files = [
3646
3962
  ...instructionBlockFiles([".github/copilot-instructions.md"], block),
3963
+ ...renderTruthmarkSkillPackage({
3964
+ skillPath: ".github/skills/truthmark-structure/SKILL.md",
3965
+ workflowId: "truthmark-structure",
3966
+ host: "github-copilot",
3967
+ config
3968
+ }),
3969
+ ...renderTruthmarkSkillPackage({
3970
+ skillPath: ".github/skills/truthmark-document/SKILL.md",
3971
+ workflowId: "truthmark-document",
3972
+ host: "github-copilot",
3973
+ config
3974
+ }),
3975
+ ...renderTruthmarkSkillPackage({
3976
+ skillPath: ".github/skills/truthmark-sync/SKILL.md",
3977
+ workflowId: "truthmark-sync",
3978
+ host: "github-copilot",
3979
+ config
3980
+ }),
3981
+ ...renderTruthmarkSkillPackage({
3982
+ skillPath: ".github/skills/truthmark-preview/SKILL.md",
3983
+ workflowId: "truthmark-preview",
3984
+ host: "github-copilot",
3985
+ config
3986
+ }),
3987
+ ...renderTruthmarkSkillPackage({
3988
+ skillPath: ".github/skills/truthmark-check/SKILL.md",
3989
+ workflowId: "truthmark-check",
3990
+ host: "github-copilot",
3991
+ config
3992
+ }),
3993
+ ...renderTruthmarkSkillPackage({
3994
+ skillPath: ".github/skills/truthmark-realize/SKILL.md",
3995
+ workflowId: "truthmark-realize",
3996
+ host: "github-copilot",
3997
+ config
3998
+ }),
3647
3999
  {
3648
4000
  path: TRUTHMARK_COPILOT_STRUCTURE_PROMPT_PATH,
3649
4001
  content: renderTruthmarkCopilotStructurePrompt(config)
@@ -3687,6 +4039,87 @@ var copilotFiles = (config, block) => {
3687
4039
  ];
3688
4040
  return files;
3689
4041
  };
4042
+ var geminiFiles = (config, block) => {
4043
+ return [
4044
+ ...instructionBlockFiles(["GEMINI.md"], block),
4045
+ ...renderTruthmarkSkillPackage({
4046
+ skillPath: ".gemini/skills/truthmark-structure/SKILL.md",
4047
+ workflowId: "truthmark-structure",
4048
+ host: "gemini-cli",
4049
+ config
4050
+ }),
4051
+ ...renderTruthmarkSkillPackage({
4052
+ skillPath: ".gemini/skills/truthmark-document/SKILL.md",
4053
+ workflowId: "truthmark-document",
4054
+ host: "gemini-cli",
4055
+ config
4056
+ }),
4057
+ ...renderTruthmarkSkillPackage({
4058
+ skillPath: ".gemini/skills/truthmark-sync/SKILL.md",
4059
+ workflowId: "truthmark-sync",
4060
+ host: "gemini-cli",
4061
+ config
4062
+ }),
4063
+ ...renderTruthmarkSkillPackage({
4064
+ skillPath: ".gemini/skills/truthmark-preview/SKILL.md",
4065
+ workflowId: "truthmark-preview",
4066
+ host: "gemini-cli",
4067
+ config
4068
+ }),
4069
+ ...renderTruthmarkSkillPackage({
4070
+ skillPath: ".gemini/skills/truthmark-check/SKILL.md",
4071
+ workflowId: "truthmark-check",
4072
+ host: "gemini-cli",
4073
+ config
4074
+ }),
4075
+ ...renderTruthmarkSkillPackage({
4076
+ skillPath: ".gemini/skills/truthmark-realize/SKILL.md",
4077
+ workflowId: "truthmark-realize",
4078
+ host: "gemini-cli",
4079
+ config
4080
+ }),
4081
+ {
4082
+ path: TRUTHMARK_GEMINI_STRUCTURE_COMMAND_PATH,
4083
+ content: renderTruthmarkGeminiStructureCommand(config)
4084
+ },
4085
+ {
4086
+ path: TRUTHMARK_GEMINI_DOCUMENT_COMMAND_PATH,
4087
+ content: renderTruthmarkGeminiDocumentCommand(config)
4088
+ },
4089
+ {
4090
+ path: TRUTHMARK_GEMINI_SYNC_COMMAND_PATH,
4091
+ content: renderTruthmarkGeminiSyncCommand(config)
4092
+ },
4093
+ {
4094
+ path: TRUTHMARK_GEMINI_PREVIEW_COMMAND_PATH,
4095
+ content: renderTruthmarkGeminiPreviewCommand(config)
4096
+ },
4097
+ {
4098
+ path: TRUTHMARK_GEMINI_CHECK_COMMAND_PATH,
4099
+ content: renderTruthmarkGeminiCheckCommand(config)
4100
+ },
4101
+ {
4102
+ path: TRUTHMARK_GEMINI_REALIZE_COMMAND_PATH,
4103
+ content: renderTruthmarkGeminiRealizeCommand(config)
4104
+ },
4105
+ {
4106
+ path: TRUTHMARK_GEMINI_ROUTE_AUDITOR_AGENT_PATH,
4107
+ content: renderTruthmarkGeminiRouteAuditorAgent()
4108
+ },
4109
+ {
4110
+ path: TRUTHMARK_GEMINI_CLAIM_VERIFIER_AGENT_PATH,
4111
+ content: renderTruthmarkGeminiClaimVerifierAgent()
4112
+ },
4113
+ {
4114
+ path: TRUTHMARK_GEMINI_DOC_REVIEWER_AGENT_PATH,
4115
+ content: renderTruthmarkGeminiDocReviewerAgent()
4116
+ },
4117
+ {
4118
+ path: TRUTHMARK_GEMINI_DOC_WRITER_AGENT_PATH,
4119
+ content: renderTruthmarkGeminiDocWriterAgent()
4120
+ }
4121
+ ];
4122
+ };
3690
4123
  var instructionBlockFiles = (paths, block) => {
3691
4124
  return paths.map((path12) => ({
3692
4125
  path: path12,
@@ -3705,33 +4138,7 @@ var filesForPlatform = (platform, config, block) => {
3705
4138
  case "github-copilot":
3706
4139
  return copilotFiles(config, block);
3707
4140
  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
- ];
4141
+ return geminiFiles(config, block);
3735
4142
  }
3736
4143
  };
3737
4144
  var renderGeneratedSurfaces = (config, block = renderAgentsBlock(config)) => {
@@ -5268,6 +5675,18 @@ var discoverRepoFiles = async (rootDir, ignore) => {
5268
5675
  const docs = [];
5269
5676
  const tests = [];
5270
5677
  for (const filePath of discoveredFiles.filter((entry) => !isIgnoredPath(entry, ignore)).sort()) {
5678
+ let stat;
5679
+ try {
5680
+ stat = await fs16.stat(path5.join(rootDir, filePath));
5681
+ } catch (error) {
5682
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
5683
+ continue;
5684
+ }
5685
+ throw error;
5686
+ }
5687
+ if (!stat.isFile()) {
5688
+ continue;
5689
+ }
5271
5690
  const extension = path5.posix.extname(filePath);
5272
5691
  const kind = fileKind(filePath, ignore);
5273
5692
  files.push({
@@ -6294,6 +6713,319 @@ var renderContextPackMarkdown = (pack) => {
6294
6713
  `;
6295
6714
  };
6296
6715
 
6716
+ // src/cli/handlers.ts
6717
+ import fs23 from "fs/promises";
6718
+
6719
+ // src/agents/workflow-helper-validation.ts
6720
+ import { parse as parseYaml2 } from "yaml";
6721
+ var escapeRegex = (value) => value.split("").map((char) => ".+*?^$()[]{}|\\".includes(char) ? `\\${char}` : char).join("");
6722
+ var hasLabel = (text, label) => {
6723
+ const escaped = escapeRegex(label);
6724
+ return new RegExp(String.raw`(^|\n)\s*(?:#{1,6}\s*)?${escaped}\s*:?\s*(\n|$)`, "iu").test(
6725
+ text
6726
+ );
6727
+ };
6728
+ var getSection = (text, label) => {
6729
+ const lines = text.split(/\r?\n/u);
6730
+ const labelPattern = new RegExp(String.raw`^(?:#{1,6}\s*)?${escapeRegex(label)}\s*:?\s*$`, "iu");
6731
+ const sectionHeaderPattern = /^(?:#{1,6}\s*)?[A-Z][A-Za-z0-9 ]+:\s*$/u;
6732
+ const startIndex = lines.findIndex((line) => labelPattern.test(line));
6733
+ if (startIndex === -1) {
6734
+ return null;
6735
+ }
6736
+ const nextSectionOffset = lines.slice(startIndex + 1).findIndex((line) => sectionHeaderPattern.test(line));
6737
+ const endIndex = nextSectionOffset === -1 ? lines.length : startIndex + 1 + nextSectionOffset;
6738
+ return lines.slice(startIndex + 1, endIndex).join("\n").trim();
6739
+ };
6740
+ var requireBulletSection = (text, label, errors, checks) => {
6741
+ const section = getSection(text, label);
6742
+ if (section === null) {
6743
+ errors.push(`missing required section: ${label}`);
6744
+ return;
6745
+ }
6746
+ if (!/^-\s+\S/mu.test(section)) {
6747
+ errors.push(`${label} must include at least one bullet`);
6748
+ return;
6749
+ }
6750
+ checks.push(label);
6751
+ };
6752
+ var validateEvidenceChecked = (text, errors, checks) => {
6753
+ const section = getSection(text, "Evidence checked");
6754
+ if (section === null || section === "") {
6755
+ errors.push("Evidence checked must include at least one structured entry");
6756
+ return;
6757
+ }
6758
+ const entries = section.split(/\n(?=-\s+)/u).map((entry) => entry.trim()).filter(Boolean);
6759
+ if (entries.length === 0) {
6760
+ errors.push("Evidence checked must include at least one structured entry");
6761
+ return;
6762
+ }
6763
+ const entryPattern = /^- Claim:\s*\S[^\n]*\n {2,}Evidence:\s*\S[^\n]*\n {2,}Result:\s*(supported|narrowed|removed|blocked)\s*$/iu;
6764
+ for (const [index, entry] of entries.entries()) {
6765
+ if (!entryPattern.test(entry)) {
6766
+ errors.push(
6767
+ `Evidence checked entry ${index + 1} must match '- Claim: ...' followed by indented 'Evidence: ...' and 'Result: supported | narrowed | removed | blocked'`
6768
+ );
6769
+ }
6770
+ }
6771
+ if (errors.length === 0) {
6772
+ checks.push(
6773
+ "Evidence checked entries include Claim:, Evidence:, and Result: supported | narrowed | removed | blocked"
6774
+ );
6775
+ }
6776
+ };
6777
+ var validateHelperScriptEntries = (entries, requiredHelpers, errors, checks) => {
6778
+ if (entries === void 0 || entries.length === 0) {
6779
+ errors.push("Helper scripts must include status for optional helpers");
6780
+ return;
6781
+ }
6782
+ const statusPattern = /^(?:-\s*)?([a-z0-9-]+):\s*(?:ran,\s*passed|skipped,\s*\S.*)$/iu;
6783
+ const validHelperIds = /* @__PURE__ */ new Set();
6784
+ for (const entry of entries) {
6785
+ const match = entry.trim().match(statusPattern);
6786
+ if (match === null) {
6787
+ errors.push(
6788
+ "Helper scripts entries must match '- helper-id: ran, passed' or '- helper-id: skipped, reason'; ran, failed is not valid for completed reports"
6789
+ );
6790
+ continue;
6791
+ }
6792
+ validHelperIds.add(match[1].toLowerCase());
6793
+ }
6794
+ for (const helperId of requiredHelpers) {
6795
+ if (!validHelperIds.has(helperId.toLowerCase())) {
6796
+ errors.push(`missing Helper scripts status: ${helperId}`);
6797
+ }
6798
+ }
6799
+ if (errors.length === 0) {
6800
+ checks.push(`Helper scripts statuses include ${requiredHelpers.join(", ")}`);
6801
+ }
6802
+ };
6803
+ var validateHelperScripts = (text, requiredHelpers, errors, checks) => {
6804
+ const section = getSection(text, "Helper scripts");
6805
+ const entries = section?.split(/\n/u).map((entry) => entry.trim()).filter(Boolean);
6806
+ validateHelperScriptEntries(entries, requiredHelpers, errors, checks);
6807
+ };
6808
+ var validateTruthSyncReportText = (text) => {
6809
+ const helper = "validate-sync-report";
6810
+ const statusMatch = text.match(/^\s*Truth Sync:\s*(completed|blocked|skipped)\b/imu);
6811
+ if (statusMatch === null) {
6812
+ return {
6813
+ ok: false,
6814
+ helper,
6815
+ errors: ["missing Truth Sync status: expected completed, blocked, or skipped"]
6816
+ };
6817
+ }
6818
+ const status = statusMatch[1].toLowerCase();
6819
+ const checks = [`status: ${status}`];
6820
+ const errors = [];
6821
+ if (status === "completed") {
6822
+ try {
6823
+ const report = parseTruthSyncReport(text.trimStart());
6824
+ for (const [label, items] of [
6825
+ ["Changed code reviewed", report.changedCode],
6826
+ ["Ownership reviewed", report.ownershipReviewed],
6827
+ ["Truth docs updated", report.truthDocsUpdated],
6828
+ ["Notes", report.notes]
6829
+ ]) {
6830
+ if (items.length === 0) {
6831
+ errors.push(`${label} must include at least one bullet`);
6832
+ } else {
6833
+ checks.push(label);
6834
+ }
6835
+ }
6836
+ if (report.evidenceChecked.length === 0) {
6837
+ errors.push("Evidence checked must include at least one structured entry");
6838
+ } else {
6839
+ checks.push(
6840
+ "Evidence checked entries include Claim:, Evidence:, and Result: supported | narrowed | removed | blocked"
6841
+ );
6842
+ }
6843
+ validateHelperScriptEntries(
6844
+ report.helperScripts,
6845
+ ["validate-write-lease"],
6846
+ errors,
6847
+ checks
6848
+ );
6849
+ } catch (error) {
6850
+ errors.push(error instanceof Error ? error.message : "invalid Truth Sync report");
6851
+ }
6852
+ } else if (status === "skipped") {
6853
+ requireBulletSection(text, "Reason", errors, checks);
6854
+ } else if (status === "blocked") {
6855
+ requireBulletSection(text, "Reason", errors, checks);
6856
+ requireBulletSection(text, "Files requiring manual review", errors, checks);
6857
+ requireBulletSection(text, "Next action", errors, checks);
6858
+ }
6859
+ return errors.length > 0 ? { ok: false, helper, errors } : { ok: true, helper, checks };
6860
+ };
6861
+ var validateTruthDocumentReportText = (text) => {
6862
+ const helper = "validate-document-report";
6863
+ const statusMatch = text.match(/^\s*Truth Document:\s*(completed|blocked)\b/imu);
6864
+ if (statusMatch === null) {
6865
+ return {
6866
+ ok: false,
6867
+ helper,
6868
+ errors: ["missing Truth Document status: expected completed or blocked"]
6869
+ };
6870
+ }
6871
+ const status = statusMatch[1].toLowerCase();
6872
+ const checks = [`status: ${status}`];
6873
+ const errors = [];
6874
+ if (status === "completed") {
6875
+ for (const label of ["Implementation reviewed", "Ownership reviewed", "Notes"]) {
6876
+ requireBulletSection(text, label, errors, checks);
6877
+ }
6878
+ if (hasLabel(text, "Evidence checked")) {
6879
+ checks.push("Evidence checked");
6880
+ } else {
6881
+ errors.push("missing required section: Evidence checked");
6882
+ }
6883
+ if (hasLabel(text, "Helper scripts")) {
6884
+ checks.push("Helper scripts");
6885
+ } else {
6886
+ errors.push("missing required section: Helper scripts");
6887
+ }
6888
+ const truthDocsUpdated = getSection(text, "Truth docs updated");
6889
+ const truthDocsCreated = getSection(text, "Truth docs created");
6890
+ if (truthDocsUpdated === null && truthDocsCreated === null) {
6891
+ errors.push("missing required section: Truth docs updated or Truth docs created");
6892
+ } else if (![truthDocsUpdated, truthDocsCreated].some(
6893
+ (section) => section !== null && /^-\s+\S/mu.test(section)
6894
+ )) {
6895
+ errors.push("Truth docs updated or Truth docs created must include at least one bullet");
6896
+ } else {
6897
+ checks.push("Truth docs updated or created");
6898
+ }
6899
+ validateEvidenceChecked(text, errors, checks);
6900
+ validateHelperScripts(text, ["validate-write-lease"], errors, checks);
6901
+ } else if (status === "blocked") {
6902
+ requireBulletSection(text, "Reason", errors, checks);
6903
+ }
6904
+ return errors.length > 0 ? { ok: false, helper, errors } : { ok: true, helper, checks };
6905
+ };
6906
+ var cleanPathValue = (value) => value.trim().replace(/^["']|["']$/gu, "");
6907
+ var normalizePath5 = (value) => cleanPathValue(value).replace(/^\.\//u, "");
6908
+ var windowsDriveAbsolutePattern = /^[A-Za-z]:[\\/]/u;
6909
+ var uncPathPattern = /^[/\\]{2}[^/\\]+[/\\]+[^/\\]+/u;
6910
+ var isUnsafePathValue = (value) => {
6911
+ const cleanValue = cleanPathValue(value);
6912
+ const pathValue = cleanValue.endsWith("/**") ? cleanValue.slice(0, -3) : cleanValue;
6913
+ return pathValue.startsWith("/") || pathValue.startsWith("\\") || windowsDriveAbsolutePattern.test(pathValue) || uncPathPattern.test(pathValue) || pathValue.split(/[\\/]+/u).includes("..");
6914
+ };
6915
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
6916
+ var findWriteLeaseRecord = (value) => {
6917
+ if (!isRecord(value)) {
6918
+ return null;
6919
+ }
6920
+ if (Object.prototype.hasOwnProperty.call(value, "allowedWrites") || Object.prototype.hasOwnProperty.call(value, "forbiddenWrites")) {
6921
+ return value;
6922
+ }
6923
+ for (const nestedKey of ["writeLease", "lease"]) {
6924
+ const nested = value[nestedKey];
6925
+ if (isRecord(nested)) {
6926
+ return nested;
6927
+ }
6928
+ }
6929
+ return value;
6930
+ };
6931
+ var readStringArray = (record, field, errors) => {
6932
+ const value = record[field];
6933
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
6934
+ errors.push(`${field} must be an array of strings`);
6935
+ return [];
6936
+ }
6937
+ return value;
6938
+ };
6939
+ var parseListFields = (text) => {
6940
+ const errors = [];
6941
+ let parsed;
6942
+ try {
6943
+ parsed = parseYaml2(text);
6944
+ } catch (error) {
6945
+ return {
6946
+ allowedWrites: [],
6947
+ forbiddenWrites: [],
6948
+ errors: [
6949
+ `manual-validation required: invalid write lease YAML${error instanceof Error ? `: ${error.message}` : ""}`
6950
+ ]
6951
+ };
6952
+ }
6953
+ const record = findWriteLeaseRecord(parsed);
6954
+ if (record === null) {
6955
+ return {
6956
+ allowedWrites: [],
6957
+ forbiddenWrites: [],
6958
+ errors: ["write lease YAML must be an object"]
6959
+ };
6960
+ }
6961
+ return {
6962
+ allowedWrites: readStringArray(record, "allowedWrites", errors),
6963
+ forbiddenWrites: readStringArray(record, "forbiddenWrites", errors),
6964
+ errors
6965
+ };
6966
+ };
6967
+ var isSupportedPattern = (pattern) => {
6968
+ const withoutTrailingGlob = pattern.endsWith("/**") ? pattern.slice(0, -3) : pattern;
6969
+ return !/[?*[\]{}]/u.test(withoutTrailingGlob);
6970
+ };
6971
+ var matchesPattern = (filePath, pattern) => {
6972
+ if (pattern.endsWith("/**")) {
6973
+ const prefix = pattern.slice(0, -3).replace(/\/+$/u, "");
6974
+ return filePath === prefix || filePath.startsWith(`${prefix}/`);
6975
+ }
6976
+ return filePath === pattern;
6977
+ };
6978
+ var validateWriteLeaseText = (leaseText, changedText) => {
6979
+ const helper = "validate-write-lease";
6980
+ const parsed = parseListFields(leaseText);
6981
+ const rawAllowedWrites = parsed.allowedWrites.map(cleanPathValue).filter(Boolean);
6982
+ const rawForbiddenWrites = parsed.forbiddenWrites.map(cleanPathValue).filter(Boolean);
6983
+ const rawChangedFiles = changedText.split(/\r?\n/u).map(cleanPathValue).filter(Boolean);
6984
+ const allowedWrites = rawAllowedWrites.map(normalizePath5).filter(Boolean);
6985
+ const forbiddenWrites = rawForbiddenWrites.map(normalizePath5).filter(Boolean);
6986
+ const changedFiles = rawChangedFiles.map(normalizePath5).filter(Boolean);
6987
+ const checks = [];
6988
+ const errors = [...parsed.errors];
6989
+ for (const pattern of rawAllowedWrites) {
6990
+ if (isUnsafePathValue(pattern)) {
6991
+ errors.push(`invalid allowedWrites path: ${pattern}`);
6992
+ }
6993
+ }
6994
+ for (const pattern of rawForbiddenWrites) {
6995
+ if (isUnsafePathValue(pattern)) {
6996
+ errors.push(`invalid forbiddenWrites path: ${pattern}`);
6997
+ }
6998
+ }
6999
+ for (const filePath of rawChangedFiles) {
7000
+ if (isUnsafePathValue(filePath)) {
7001
+ errors.push(`invalid changed file path: ${filePath}`);
7002
+ }
7003
+ }
7004
+ for (const pattern of [...allowedWrites, ...forbiddenWrites]) {
7005
+ if (!isSupportedPattern(pattern)) {
7006
+ errors.push(`manual-validation required: unsupported write pattern ${pattern}`);
7007
+ }
7008
+ }
7009
+ if (allowedWrites.length === 0) {
7010
+ errors.push("manual-validation required: no allowedWrites entries found");
7011
+ }
7012
+ if (errors.length === 0) {
7013
+ for (const filePath of changedFiles) {
7014
+ if (!allowedWrites.some((pattern) => matchesPattern(filePath, pattern))) {
7015
+ errors.push(`${filePath} is outside allowedWrites`);
7016
+ continue;
7017
+ }
7018
+ const forbiddenPattern = forbiddenWrites.find((pattern) => matchesPattern(filePath, pattern));
7019
+ if (forbiddenPattern !== void 0) {
7020
+ errors.push(`${filePath} matches forbiddenWrites pattern ${forbiddenPattern}`);
7021
+ continue;
7022
+ }
7023
+ checks.push(filePath);
7024
+ }
7025
+ }
7026
+ return errors.length > 0 ? { ok: false, helper, errors } : { ok: true, helper, checks };
7027
+ };
7028
+
6297
7029
  // src/cli/handlers.ts
6298
7030
  var runConfig2 = async (options) => {
6299
7031
  return runConfig(process.cwd(), options);
@@ -6350,6 +7082,33 @@ var isContextPackWorkflow = (value) => {
6350
7082
  var isContextPackFormat = (value) => {
6351
7083
  return value === void 0 || value === "json" || value === "markdown";
6352
7084
  };
7085
+ var readHelperFile = async (filePath, helper) => {
7086
+ try {
7087
+ return await fs23.readFile(filePath, "utf8");
7088
+ } catch (error) {
7089
+ const message = error instanceof Error ? error.message : String(error);
7090
+ return { ok: false, helper, errors: [`could not read file: ${message}`] };
7091
+ }
7092
+ };
7093
+ var runValidateSyncReport = async (reportFile) => {
7094
+ const text = await readHelperFile(reportFile, "validate-sync-report");
7095
+ return typeof text === "string" ? validateTruthSyncReportText(text) : text;
7096
+ };
7097
+ var runValidateDocumentReport = async (reportFile) => {
7098
+ const text = await readHelperFile(reportFile, "validate-document-report");
7099
+ return typeof text === "string" ? validateTruthDocumentReportText(text) : text;
7100
+ };
7101
+ var runValidateWriteLease = async (leaseFile, changedFilesFile) => {
7102
+ const leaseText = await readHelperFile(leaseFile, "validate-write-lease");
7103
+ if (typeof leaseText !== "string") {
7104
+ return leaseText;
7105
+ }
7106
+ const changedText = await readHelperFile(changedFilesFile, "validate-write-lease");
7107
+ if (typeof changedText !== "string") {
7108
+ return changedText;
7109
+ }
7110
+ return validateWriteLeaseText(leaseText, changedText);
7111
+ };
6353
7112
  var runContext = async (options) => {
6354
7113
  if (!isContextPackWorkflow(options.workflow)) {
6355
7114
  return {
@@ -6406,6 +7165,28 @@ var writeContextResult = (result, options) => {
6406
7165
  }
6407
7166
  writeResult(result, options);
6408
7167
  };
7168
+ var renderValidationHuman = (result) => {
7169
+ if (result.ok === true) {
7170
+ return [`${result.helper}: ok`, ...result.checks.map((check) => `- ${check}`)].join("\n");
7171
+ }
7172
+ return [`${result.helper}: failed`, ...result.errors.map((error) => `- ${error}`)].join("\n");
7173
+ };
7174
+ var toValidationCommandResult = (command, result) => ({
7175
+ command,
7176
+ summary: result.ok ? "Validation passed" : "Validation failed",
7177
+ diagnostics: [],
7178
+ data: {
7179
+ validation: result
7180
+ }
7181
+ });
7182
+ var writeValidationResult = (command, result, options) => {
7183
+ const output = options.json ? renderJson(toValidationCommandResult(command, result)) : renderValidationHuman(result);
7184
+ process.stdout.write(`${output}
7185
+ `);
7186
+ if (!result.ok) {
7187
+ process.exitCode = 1;
7188
+ }
7189
+ };
6409
7190
  var addJsonOption = (command) => {
6410
7191
  return command.option("--json", "Render command output as JSON");
6411
7192
  };
@@ -6449,6 +7230,32 @@ var buildProgram = () => {
6449
7230
  options
6450
7231
  );
6451
7232
  });
7233
+ const validate = program.command("validate").description("Run optional Truthmark workflow helper validators from the installed CLI.");
7234
+ addJsonOption(
7235
+ validate.command("sync-report").description("Validate a Truth Sync report file.").argument("<report-file>", "Truth Sync report file")
7236
+ ).action(async (reportFile, options) => {
7237
+ writeValidationResult("validate sync-report", await runValidateSyncReport(reportFile), options);
7238
+ });
7239
+ addJsonOption(
7240
+ validate.command("document-report").description("Validate a Truth Document report file.").argument("<report-file>", "Truth Document report file")
7241
+ ).action(async (reportFile, options) => {
7242
+ writeValidationResult(
7243
+ "validate document-report",
7244
+ await runValidateDocumentReport(reportFile),
7245
+ options
7246
+ );
7247
+ });
7248
+ addJsonOption(
7249
+ 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")
7250
+ ).action(
7251
+ async (leaseOrReportFile, changedFilesFile, options) => {
7252
+ writeValidationResult(
7253
+ "validate write-lease",
7254
+ await runValidateWriteLease(leaseOrReportFile, changedFilesFile),
7255
+ options
7256
+ );
7257
+ }
7258
+ );
6452
7259
  return program;
6453
7260
  };
6454
7261