From 767e23b99ecd0885dec65df2537c02e92e38e19c Mon Sep 17 00:00:00 2001 From: zack-nova <226822369+zack-nova@users.noreply.github.com> Date: Tue, 12 May 2026 23:07:12 +0800 Subject: [PATCH] Implement strict package template references --- cmd/harness/cli/cli_integration_test.go | 136 +++++++-------- .../install_guidance_failure_command_test.go | 12 +- .../cli/commands/install_transaction_test.go | 4 +- ...stall_guidance_compose_integration_test.go | 66 +++---- cmd/harness/cli/progress_integration_test.go | 4 +- .../cli/template_publish_integration_test.go | 4 +- ...rief_materialize_check_integration_test.go | 10 +- cmd/orbit/cli/cli_integration_test.go | 28 +-- cmd/orbit/cli/guidance_integration_test.go | 54 +++--- cmd/orbit/cli/harness/bundle_shrink_test.go | 2 +- .../cli/harness/guidance_compose_test.go | 4 +- cmd/orbit/cli/harness/readiness_test.go | 4 +- cmd/orbit/cli/harness/root_agents_test.go | 4 +- .../cli/harness/template_candidate_test.go | 2 +- .../cli/harness/template_install_preview.go | 8 +- .../harness/template_install_preview_test.go | 13 +- .../harness/template_install_source_test.go | 14 +- .../harness/template_member_ownership_test.go | 4 +- cmd/orbit/cli/harness/template_merge_test.go | 2 +- cmd/orbit/cli/harness/template_remove_test.go | 8 +- cmd/orbit/cli/harness/template_save_test.go | 8 +- cmd/orbit/cli/template/apply.go | 2 + cmd/orbit/cli/template/apply_test.go | 80 +++++---- cmd/orbit/cli/template/brief_backfill.go | 2 +- cmd/orbit/cli/template/brief_backfill_test.go | 12 +- .../cli/template/content_builder_test.go | 4 +- cmd/orbit/cli/template/install_reapply.go | 2 + cmd/orbit/cli/template/install_replay.go | 2 + .../cli/template/remote_snapshot_test.go | 2 +- cmd/orbit/cli/template/render.go | 161 ++++++++++++++++-- cmd/orbit/cli/template/render_test.go | 126 ++++++++++++++ cmd/orbit/cli/template/replacement.go | 10 +- cmd/orbit/cli/template/replacement_test.go | 18 +- cmd/orbit/cli/template/save_test.go | 2 +- cmd/orbit/cli/template/scan.go | 44 ++++- cmd/orbit/cli/template/scan_test.go | 29 ++++ .../cli/template_apply_integration_test.go | 6 +- .../template_apply_remote_integration_test.go | 2 +- ...mplate_backfill_export_integration_test.go | 10 +- .../cli/template_save_integration_test.go | 14 +- 40 files changed, 629 insertions(+), 290 deletions(-) create mode 100644 cmd/orbit/cli/template/render_test.go diff --git a/cmd/harness/cli/cli_integration_test.go b/cmd/harness/cli/cli_integration_test.go index 9651fc8..599345d 100644 --- a/cmd/harness/cli/cli_integration_test.go +++ b/cmd/harness/cli/cli_integration_test.go @@ -3586,9 +3586,9 @@ func TestHarnessBindingsMissingReportsCurrentVarsGapsAcrossInstallBackedOrbits(t " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, - AgentsTemplate: "You are the $project_name docs orbit.\n", + AgentsTemplate: "You are the {{ vars.project_name }} docs orbit.\n", }, { OrbitID: "cmd", @@ -3602,9 +3602,9 @@ func TestHarnessBindingsMissingReportsCurrentVarsGapsAcrossInstallBackedOrbits(t " value: orbit\n" + " description: CLI binary\n", Files: map[string]string{ - "cmd/README.md": "Run $project_name as `$binary_name`.\n", + "cmd/README.md": "Run {{ vars.project_name }} as `{{ vars.binary_name }}`.\n", }, - AgentsTemplate: "Use $binary_name for $project_name releases.\n", + AgentsTemplate: "Use {{ vars.binary_name }} for {{ vars.project_name }} releases.\n", }, }) @@ -3708,7 +3708,7 @@ func TestHarnessBindingsMissingTextOutputMapsToReadinessReason(t *testing.T) { " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, }, }) @@ -3746,9 +3746,9 @@ func TestHarnessBindingsScanRuntimeReportsObservedPlaceholdersAndWritesInstallSn " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, - AgentsTemplate: "Follow $project_name docs workflow\n", + AgentsTemplate: "Follow {{ vars.project_name }} docs workflow\n", }, }) @@ -3761,7 +3761,7 @@ func TestHarnessBindingsScanRuntimeReportsObservedPlaceholdersAndWritesInstallSn ) require.NoError(t, err) - agentsBlock, err := orbittemplate.WrapRuntimeAgentsBlock("docs", []byte("Follow $project_name docs workflow\n")) + agentsBlock, err := orbittemplate.WrapRuntimeAgentsBlock("docs", []byte("Follow {{ vars.project_name }} docs workflow\n")) require.NoError(t, err) repo.WriteFile(t, "AGENTS.md", string(agentsBlock)) @@ -3834,16 +3834,16 @@ func TestHarnessBindingsScanRuntimeTextOutputMapsToReadinessReason(t *testing.T) " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, - AgentsTemplate: "Follow $project_name docs workflow\n", + AgentsTemplate: "Follow {{ vars.project_name }} docs workflow\n", }, }) _, _, err := executeHarnessCLI(t, repo.Root, "install", "orbit-template/docs", "--allow-unresolved-bindings") require.NoError(t, err) - agentsBlock, err := orbittemplate.WrapRuntimeAgentsBlock("docs", []byte("Follow $project_name docs workflow\n")) + agentsBlock, err := orbittemplate.WrapRuntimeAgentsBlock("docs", []byte("Follow {{ vars.project_name }} docs workflow\n")) require.NoError(t, err) repo.WriteFile(t, "AGENTS.md", string(agentsBlock)) @@ -3877,9 +3877,9 @@ func TestHarnessBindingsApplyDryRunJSONUsesCurrentVarsWithoutMutatingRuntime(t * " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, - AgentsTemplate: "Follow $project_name docs workflow\n", + AgentsTemplate: "Follow {{ vars.project_name }} docs workflow\n", }, }) @@ -3931,11 +3931,11 @@ func TestHarnessBindingsApplyDryRunJSONUsesCurrentVarsWithoutMutatingRuntime(t * guideData, err := os.ReadFile(filepath.Join(repo.Root, "docs", "guide.md")) require.NoError(t, err) - require.Equal(t, "$project_name guide\n", string(guideData)) + require.Equal(t, "{{ vars.project_name }} guide\n", string(guideData)) agentsData, err := os.ReadFile(filepath.Join(repo.Root, "AGENTS.md")) require.NoError(t, err) - require.Contains(t, string(agentsData), "Follow $project_name docs workflow\n") + require.Contains(t, string(agentsData), "Follow {{ vars.project_name }} docs workflow\n") record, err := harnesspkg.LoadInstallRecord(repo.Root, "docs") require.NoError(t, err) @@ -3956,9 +3956,9 @@ func TestHarnessBindingsApplyWritesRenderedRuntimeAndRefreshesInstallSnapshot(t " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, - AgentsTemplate: "Follow $project_name docs workflow\n", + AgentsTemplate: "Follow {{ vars.project_name }} docs workflow\n", }, }) @@ -4018,7 +4018,7 @@ func TestHarnessBindingsApplyWritesRenderedRuntimeAndRefreshesInstallSnapshot(t agentsData, err := os.ReadFile(filepath.Join(repo.Root, "AGENTS.md")) require.NoError(t, err) require.Contains(t, string(agentsData), "Follow Filled Orbit docs workflow\n") - require.NotContains(t, string(agentsData), "$project_name") + require.NotContains(t, string(agentsData), "{{ vars.project_name }}") record, err := harnesspkg.LoadInstallRecord(repo.Root, "docs") require.NoError(t, err) @@ -4054,7 +4054,7 @@ func TestHarnessBindingsApplyAllowsUnresolvedBindingsAsWarnings(t *testing.T) { " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, }, }) @@ -4102,7 +4102,7 @@ func TestHarnessBindingsApplyAllowsUnresolvedBindingsAsWarnings(t *testing.T) { guideData, err := os.ReadFile(filepath.Join(repo.Root, "docs", "guide.md")) require.NoError(t, err) - require.Equal(t, "$project_name guide\n", string(guideData)) + require.Equal(t, "{{ vars.project_name }} guide\n", string(guideData)) record, err := harnesspkg.LoadInstallRecord(repo.Root, "docs") require.NoError(t, err) @@ -4125,9 +4125,9 @@ func TestHarnessBindingsApplyTextOutputIncludesReadinessStatus(t *testing.T) { " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, - AgentsTemplate: "Follow $project_name docs workflow\n", + AgentsTemplate: "Follow {{ vars.project_name }} docs workflow\n", }, }) @@ -4167,7 +4167,7 @@ func TestHarnessBindingsApplyFailsClosedOnDriftWithoutForceAndForceRewritesInsta " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, }, }) @@ -4246,7 +4246,7 @@ func TestHarnessBindingsApplyFailsWhenInstalledVariableDeclarationBecomesIncompa " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, }, }) @@ -5053,12 +5053,12 @@ func TestHarnessRemoveDeletesTemplateMemberFromHarnessTemplate(t *testing.T) { " exported_paths:\n"+ " - docs/guide.md\n"+ " file_digests:\n"+ - " docs/guide.md: "+testContentDigest([]byte("Docs $project_name guide\n"))+"\n"+ + " docs/guide.md: "+testContentDigest([]byte("Docs {{ vars.project_name }} guide\n"))+"\n"+ " variables:\n"+ " project_name:\n"+ " description: Project name\n"+ " required: true\n") - repo.WriteFile(t, "docs/guide.md", "Docs $project_name guide\n") + repo.WriteFile(t, "docs/guide.md", "Docs {{ vars.project_name }} guide\n") repo.AddAndCommit(t, "seed harness template for remove") stdout, stderr, err := executeHarnessCLI(t, repo.Root, "remove", "docs") @@ -5141,13 +5141,13 @@ func TestHarnessRemoveTemplateMemberJSONOutputIncludesTemplateSummary(t *testing " - docs/guide.md\n"+ " file_digests:\n"+ " AGENTS.md: "+testContentDigest(agentsContent)+"\n"+ - " docs/guide.md: "+testContentDigest([]byte("Docs $project_name guide\n"))+"\n"+ + " docs/guide.md: "+testContentDigest([]byte("Docs {{ vars.project_name }} guide\n"))+"\n"+ " variables:\n"+ " project_name:\n"+ " description: Project name\n"+ " required: true\n") repo.WriteFile(t, "AGENTS.md", string(agentsContent)) - repo.WriteFile(t, "docs/guide.md", "Docs $project_name guide\n") + repo.WriteFile(t, "docs/guide.md", "Docs {{ vars.project_name }} guide\n") repo.AddAndCommit(t, "seed harness template remove json repo") stdout, stderr, err := executeHarnessCLI(t, repo.Root, "remove", "docs", "--json") @@ -5240,12 +5240,12 @@ func TestHarnessRemoveTemplateMemberJSONOutputKeepsFalseSummaryFields(t *testing " exported_paths:\n"+ " - docs/guide.md\n"+ " file_digests:\n"+ - " docs/guide.md: "+testContentDigest([]byte("Docs $project_name guide\n"))+"\n"+ + " docs/guide.md: "+testContentDigest([]byte("Docs {{ vars.project_name }} guide\n"))+"\n"+ " variables:\n"+ " project_name:\n"+ " description: Project name\n"+ " required: true\n") - repo.WriteFile(t, "docs/guide.md", "Docs $project_name guide\n") + repo.WriteFile(t, "docs/guide.md", "Docs {{ vars.project_name }} guide\n") repo.AddAndCommit(t, "seed harness template remove false summary repo") stdout, stderr, err := executeHarnessCLI(t, repo.Root, "remove", "docs", "--json") @@ -5386,7 +5386,7 @@ func seedSingleMemberHarnessTemplateRemoveCLIRepo(t *testing.T, withAgents bool) exportedPaths := []string{"docs/guide.md"} fileDigestLines := []string{ - " docs/guide.md: " + testContentDigest([]byte("Docs $project_name guide\n")), + " docs/guide.md: " + testContentDigest([]byte("Docs {{ vars.project_name }} guide\n")), } if withAgents { docsBlock, wrapErr := orbittemplate.WrapRuntimeAgentsBlock("docs", []byte("Docs guidance\n")) @@ -5413,7 +5413,7 @@ func seedSingleMemberHarnessTemplateRemoveCLIRepo(t *testing.T, withAgents bool) " project_name:\n"+ " description: Project name\n"+ " required: true\n") - repo.WriteFile(t, "docs/guide.md", "Docs $project_name guide\n") + repo.WriteFile(t, "docs/guide.md", "Docs {{ vars.project_name }} guide\n") repo.AddAndCommit(t, "seed single member harness template remove cli repo") return repo @@ -5534,7 +5534,7 @@ func TestHarnessRemoveShrinksBundleBackedRuntimeMember(t *testing.T) { cmdData, err := os.ReadFile(filepath.Join(runtimeRepo.Root, "cmd", "main.go")) require.NoError(t, err) - require.Equal(t, "package main\n\nconst name = \"orbitctl\"\n", string(cmdData)) + require.Equal(t, "package main\n\nconst name = \"orbit-installed\"\n", string(cmdData)) } func TestHarnessMemberExtractDetachedKeepsPayloadAndRemovesBundleOwnership(t *testing.T) { @@ -6582,7 +6582,7 @@ func TestHarnessInstallDefaultsToUnresolvedBindingsAndPlaceholderRuntime(t *test guideData, err := os.ReadFile(filepath.Join(repo.Root, "docs", "guide.md")) require.NoError(t, err) - require.Equal(t, "$project_name guide\n", string(guideData)) + require.Equal(t, "{{ vars.project_name }} guide\n", string(guideData)) record, err := harnesspkg.LoadInstallRecord(repo.Root, "docs") require.NoError(t, err) @@ -6631,7 +6631,7 @@ func TestHarnessInstallTreatsBlankRepoVarAsUnresolved(t *testing.T) { guideData, err := os.ReadFile(filepath.Join(repo.Root, "docs", "guide.md")) require.NoError(t, err) - require.Equal(t, "$project_name guide\n", string(guideData)) + require.Equal(t, "{{ vars.project_name }} guide\n", string(guideData)) } func TestHarnessInstallStrictBindingsFailsOnMissingBindings(t *testing.T) { @@ -6814,7 +6814,7 @@ func TestHarnessInstallBatchDryRunJSONAggregatesMultipleOrbitPreviews(t *testing " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, }, { @@ -6829,7 +6829,7 @@ func TestHarnessInstallBatchDryRunJSONAggregatesMultipleOrbitPreviews(t *testing " value: orbit\n" + " description: CLI binary\n", Files: map[string]string{ - "cmd/README.md": "Run $project_name as `$binary_name`.\n", + "cmd/README.md": "Run {{ vars.project_name }} as `{{ vars.binary_name }}`.\n", }, }, }) @@ -6913,7 +6913,7 @@ func TestHarnessInstallBatchInstallsAllItemsAfterSharedPreview(t *testing.T) { " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, }, { @@ -6928,7 +6928,7 @@ func TestHarnessInstallBatchInstallsAllItemsAfterSharedPreview(t *testing.T) { " value: orbit\n" + " description: CLI binary\n", Files: map[string]string{ - "cmd/README.md": "Run $project_name as `$binary_name`.\n", + "cmd/README.md": "Run {{ vars.project_name }} as `{{ vars.binary_name }}`.\n", }, }, }) @@ -7015,7 +7015,7 @@ func TestHarnessInstallBatchRollsBackSharedVarsAndEarlierItemsWhenLaterInstallFa " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, }, { @@ -7030,7 +7030,7 @@ func TestHarnessInstallBatchRollsBackSharedVarsAndEarlierItemsWhenLaterInstallFa " value: orbit\n" + " description: CLI binary\n", Files: map[string]string{ - "cmd/README.md": "Run $project_name as `$binary_name`.\n", + "cmd/README.md": "Run {{ vars.project_name }} as `{{ vars.binary_name }}`.\n", }, }, }) @@ -7107,7 +7107,7 @@ func TestHarnessInstallBatchDefaultsToUnresolvedBindingsAndWarnings(t *testing.T " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, }, { @@ -7122,7 +7122,7 @@ func TestHarnessInstallBatchDefaultsToUnresolvedBindingsAndWarnings(t *testing.T " value: orbit\n" + " description: CLI binary\n", Files: map[string]string{ - "cmd/README.md": "Run $project_name as `$binary_name`.\n", + "cmd/README.md": "Run {{ vars.project_name }} as `{{ vars.binary_name }}`.\n", }, }, }) @@ -7164,11 +7164,11 @@ func TestHarnessInstallBatchDefaultsToUnresolvedBindingsAndWarnings(t *testing.T docsData, err := os.ReadFile(filepath.Join(repo.Root, "docs", "guide.md")) require.NoError(t, err) - require.Equal(t, "$project_name guide\n", string(docsData)) + require.Equal(t, "{{ vars.project_name }} guide\n", string(docsData)) cmdData, err := os.ReadFile(filepath.Join(repo.Root, "cmd", "README.md")) require.NoError(t, err) - require.Equal(t, "Run $project_name as `$binary_name`.\n", string(cmdData)) + require.Equal(t, "Run {{ vars.project_name }} as `{{ vars.binary_name }}`.\n", string(cmdData)) _, err = os.Stat(filepath.Join(repo.Root, ".harness", "vars.yaml")) require.ErrorIs(t, err, os.ErrNotExist) @@ -7187,7 +7187,7 @@ func TestHarnessInstallBatchStrictBindingsFailsOnMissingBindings(t *testing.T) { " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, }, { @@ -7202,7 +7202,7 @@ func TestHarnessInstallBatchStrictBindingsFailsOnMissingBindings(t *testing.T) { " value: orbit\n" + " description: CLI binary\n", Files: map[string]string{ - "cmd/README.md": "Run $project_name as `$binary_name`.\n", + "cmd/README.md": "Run {{ vars.project_name }} as `{{ vars.binary_name }}`.\n", }, }, }) @@ -7239,7 +7239,7 @@ func TestHarnessInstallBatchNamespacesIncompatibleSharedVariableDeclarations(t * " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, }, { @@ -7251,7 +7251,7 @@ func TestHarnessInstallBatchNamespacesIncompatibleSharedVariableDeclarations(t * " value: Orbit\n" + " description: CLI title\n", Files: map[string]string{ - "cmd/README.md": "$project_name CLI\n", + "cmd/README.md": "{{ vars.project_name }} CLI\n", }, }, }) @@ -7272,10 +7272,10 @@ func TestHarnessInstallBatchNamespacesIncompatibleSharedVariableDeclarations(t * docsData, err := os.ReadFile(filepath.Join(repo.Root, "docs", "guide.md")) require.NoError(t, err) - require.Equal(t, "$project_name guide\n", string(docsData)) + require.Equal(t, "{{ vars.project_name }} guide\n", string(docsData)) cmdData, err := os.ReadFile(filepath.Join(repo.Root, "cmd", "README.md")) require.NoError(t, err) - require.Equal(t, "$project_name CLI\n", string(cmdData)) + require.Equal(t, "{{ vars.project_name }} CLI\n", string(cmdData)) docsRecord, err := orbittemplate.LoadInstallRecordFile(filepath.Join(repo.Root, ".harness", "installs", "docs.yaml")) require.NoError(t, err) @@ -7335,7 +7335,7 @@ func TestHarnessInstallBatchFailsClosedBeforeWritingWhenOrbitIDsRepeat(t *testin " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, }, }) @@ -8025,7 +8025,7 @@ func TestHarnessInstallOverwriteExistingReplacesOwnedFilesAndUpdatesInstallRecor runtimeBranch := strings.TrimSpace(repo.Run(t, "branch", "--show-current")) repo.Run(t, "checkout", "orbit-template/docs") - repo.WriteFile(t, "docs/reference.md", "$project_name reference\n") + repo.WriteFile(t, "docs/reference.md", "{{ vars.project_name }} reference\n") repo.Run(t, "rm", "-f", "docs/guide.md") repo.AddAndCommit(t, "update template branch contents") updatedCommit := strings.TrimSpace(repo.Run(t, "rev-parse", "HEAD")) @@ -8407,7 +8407,7 @@ func TestHarnessInstallHarnessTemplateOverrideBundleBackedMemberShrinksPreviousB cmdData, err := os.ReadFile(filepath.Join(runtimeRepo.Root, "cmd", "main.go")) require.NoError(t, err) - require.Equal(t, "package main\n\nconst name = \"orbitctl\"\n", string(cmdData)) + require.Equal(t, "package main\n\nconst name = \"orbit-installed\"\n", string(cmdData)) guideData, err := os.ReadFile(filepath.Join(runtimeRepo.Root, "docs", "guide.md")) require.NoError(t, err) @@ -8593,7 +8593,7 @@ func TestHarnessInstallOverwriteFailsWhenExistingOwnedFilesCannotBeSafelyReconst runtimeBranch := strings.TrimSpace(repo.Run(t, "branch", "--show-current")) repo.Run(t, "checkout", "orbit-template/docs") - repo.WriteFile(t, "docs/reference.md", "$project_name reference\n") + repo.WriteFile(t, "docs/reference.md", "{{ vars.project_name }} reference\n") repo.Run(t, "rm", "-f", "docs/guide.md") repo.AddAndCommit(t, "update template branch contents") repo.Run(t, "checkout", runtimeBranch) @@ -8648,7 +8648,7 @@ func TestHarnessInstallOverwriteFailsClosedWhenInstallRecordLacksVariablesSnapsh runtimeBranch := strings.TrimSpace(repo.Run(t, "branch", "--show-current")) repo.Run(t, "checkout", "orbit-template/docs") - repo.WriteFile(t, "docs/reference.md", "$project_name reference\n") + repo.WriteFile(t, "docs/reference.md", "{{ vars.project_name }} reference\n") repo.Run(t, "rm", "-f", "docs/guide.md") repo.AddAndCommit(t, "update template branch contents") repo.Run(t, "checkout", runtimeBranch) @@ -9060,7 +9060,7 @@ func TestHarnessInstallDryRunHarnessTemplateConflictsOnInstalledBundleVariableDe " qa_owner:\n"+ " value: release-team\n"+ " description: QA owner\n") - conflictingRepo.WriteFile(t, "qa/checklist.md", "$project_name checklist for $qa_owner\n") + conflictingRepo.WriteFile(t, "qa/checklist.md", "{{ vars.project_name }} checklist for {{ vars.qa_owner }}\n") _, _, err = executeHarnessCLI(t, conflictingRepo.Root, "add", "qa") require.NoError(t, err) conflictingRepo.AddAndCommit(t, "seed conflicting bundle source") @@ -9461,7 +9461,7 @@ func TestHarnessInstallHarnessTemplateLocalWriteJSON(t *testing.T) { cmdData, err := os.ReadFile(filepath.Join(runtimeRepo.Root, "cmd", "main.go")) require.NoError(t, err) - require.Equal(t, "package main\n\nconst name = \"orbitctl\"\n", string(cmdData)) + require.Equal(t, "package main\n\nconst name = \"orbit-installed\"\n", string(cmdData)) runtimeFile, err := harnesspkg.LoadRuntimeFile(runtimeRepo.Root) require.NoError(t, err) @@ -9544,7 +9544,7 @@ func TestHarnessInstallHarnessTemplateOverwriteExistingReplacesSameBundle(t *tes _, _, err = executeHarnessCLI(t, sourceRepo.Root, "remove", "docs") require.NoError(t, err) sourceRepo.WriteFile(t, "cmd/main.go", "package main\n\nconst name = \"orbit-next\"\n") - sourceRepo.WriteFile(t, "AGENTS.md", "Workspace guide for $project_name v2\n") + sourceRepo.WriteFile(t, "AGENTS.md", "Workspace guide for {{ vars.project_name }} v2\n") sourceRepo.AddAndCommit(t, "update harness template source") _, err = harnesspkg.SaveTemplateBranch(context.Background(), harnesspkg.TemplateSaveInput{ @@ -9766,8 +9766,8 @@ func TestHarnessTemplateSaveCreatesHarnessTemplateBranch(t *testing.T) { agentsData, err := gitpkg.ReadFileAtRev(context.Background(), repo.Root, "harness-template/workspace", "AGENTS.md") require.NoError(t, err) - require.Contains(t, string(agentsData), "$project_name") - require.Contains(t, string(agentsData), "$command_name") + require.Contains(t, string(agentsData), "{{ vars.project_name }}") + require.Contains(t, string(agentsData), "{{ vars.command_name }}") inspectStdout, inspectStderr, err := executeOrbitCLI(t, repo.Root, "branch", "inspect", "harness-template/workspace", "--json") require.NoError(t, err) @@ -10002,7 +10002,7 @@ func TestHarnessTemplateSaveOverwriteRewritesExistingBranch(t *testing.T) { require.NoError(t, err) firstCommit := strings.TrimSpace(repo.Run(t, "rev-parse", "harness-template/workspace")) - repo.WriteFile(t, "docs/guide.md", "Orbit guide v2 for $project_name\n") + repo.WriteFile(t, "docs/guide.md", "Orbit guide v2 for {{ vars.project_name }}\n") repo.AddAndCommit(t, "update runtime docs") stdout, stderr, err := executeHarnessCLI(t, repo.Root, "template", "save", "--to", "harness-template/workspace", "--overwrite", "--json") @@ -10021,7 +10021,7 @@ func TestHarnessTemplateSaveOverwriteRewritesExistingBranch(t *testing.T) { savedData, err := gitpkg.ReadFileAtRev(context.Background(), repo.Root, "harness-template/workspace", "docs/guide.md") require.NoError(t, err) - require.Contains(t, string(savedData), "$project_name") + require.Contains(t, string(savedData), "{{ vars.project_name }}") require.Contains(t, string(savedData), "v2") } @@ -10044,7 +10044,7 @@ func TestHarnessTemplateSaveEditTemplateWritesEditedTemplateWithoutMutatingRunti editorScript := filepath.Join(repo.Root, "edit-template.sh") require.NoError(t, os.WriteFile(editorScript, []byte(""+ "#!/bin/sh\n"+ - "printf '%s\\n' '$project_name guide at $service_url' > \"$1/docs/guide.md\"\n"), 0o755)) + "printf '%s\\n' '{{ vars.project_name }} guide at {{ vars.service_url }}' > \"$1/docs/guide.md\"\n"), 0o755)) t.Setenv("EDITOR", editorScript) _, _, err := executeHarnessCLI(t, repo.Root, "template", "save", "--to", "harness-template/workspace", "--edit-template") @@ -10056,7 +10056,7 @@ func TestHarnessTemplateSaveEditTemplateWritesEditedTemplateWithoutMutatingRunti templateData, err := gitpkg.ReadFileAtRev(context.Background(), repo.Root, "harness-template/workspace", "docs/guide.md") require.NoError(t, err) - require.Equal(t, "$project_name guide at $service_url\n", string(templateData)) + require.Equal(t, "{{ vars.project_name }} guide at {{ vars.service_url }}\n", string(templateData)) manifestData, err := gitpkg.ReadFileAtRev(context.Background(), repo.Root, "harness-template/workspace", ".harness/template.yaml") require.NoError(t, err) @@ -10298,7 +10298,7 @@ func createAlternateDocsTemplateBranch(t *testing.T, repo *testutil.Repo) string runtimeBranch := strings.TrimSpace(repo.Run(t, "branch", "--show-current")) repo.Run(t, "checkout", "orbit-template/docs") repo.Run(t, "checkout", "-b", "orbit-template/docs-alt") - repo.WriteFile(t, "docs/reference.md", "$project_name reference\n") + repo.WriteFile(t, "docs/reference.md", "{{ vars.project_name }} reference\n") repo.Run(t, "rm", "-f", "docs/guide.md") repo.AddAndCommit(t, "create alternate docs template branch") alternateCommit := strings.TrimSpace(repo.Run(t, "rev-parse", "HEAD")) @@ -10327,7 +10327,7 @@ func seedHarnessAgentsComposeRepo(t *testing.T) *testutil.Repo { "meta:\n"+ " file: .harness/orbits/docs.yaml\n"+ " agents_template: |\n"+ - " You are the $project_name docs orbit.\n"+ + " You are the {{ vars.project_name }} docs orbit.\n"+ " include_in_projection: true\n"+ " include_in_write: true\n"+ " include_in_export: true\n"+ @@ -10364,7 +10364,7 @@ func seedHarnessAgentsComposeRepo(t *testing.T) *testutil.Repo { "meta:\n"+ " file: .harness/orbits/cmd.yaml\n"+ " agents_template: |\n"+ - " Use $command_name for $project_name releases.\n"+ + " Use {{ vars.command_name }} for {{ vars.project_name }} releases.\n"+ " include_in_projection: true\n"+ " include_in_write: true\n"+ " include_in_export: true\n"+ diff --git a/cmd/harness/cli/commands/install_guidance_failure_command_test.go b/cmd/harness/cli/commands/install_guidance_failure_command_test.go index f24efdb..d750f8e 100644 --- a/cmd/harness/cli/commands/install_guidance_failure_command_test.go +++ b/cmd/harness/cli/commands/install_guidance_failure_command_test.go @@ -20,9 +20,9 @@ import ( func TestInstallReturnsWarningAndRollsBackGuidanceArtifactsWhenScopedGuidanceFails(t *testing.T) { repo := seedInstallGuidanceCommandRepo(t, []installGuidanceCommandTemplateSpec{{ OrbitID: "docs", - AgentsTemplate: "You are the $project_name docs orbit.\n", + AgentsTemplate: "You are the {{ vars.project_name }} docs orbit.\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, }}) bindingsPath := writeInstallGuidanceCommandBindings(t, repo.Root) @@ -79,16 +79,16 @@ func TestInstallBatchReturnsWarningAndRollsBackGuidanceArtifactsWhenScopedGuidan repo := seedInstallGuidanceCommandRepo(t, []installGuidanceCommandTemplateSpec{ { OrbitID: "docs", - AgentsTemplate: "You are the $project_name docs orbit.\n", + AgentsTemplate: "You are the {{ vars.project_name }} docs orbit.\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, }, { OrbitID: "cmd", - AgentsTemplate: "Use the $project_name cmd release flow.\n", + AgentsTemplate: "Use the {{ vars.project_name }} cmd release flow.\n", Files: map[string]string{ - "cmd/README.md": "$project_name cmd guide\n", + "cmd/README.md": "{{ vars.project_name }} cmd guide\n", }, }, }) diff --git a/cmd/harness/cli/commands/install_transaction_test.go b/cmd/harness/cli/commands/install_transaction_test.go index e76deb9..80c9f08 100644 --- a/cmd/harness/cli/commands/install_transaction_test.go +++ b/cmd/harness/cli/commands/install_transaction_test.go @@ -34,7 +34,7 @@ func TestInstallOverwriteRollsBackWhenCleanupFails(t *testing.T) { runtimeBranch := strings.TrimSpace(repo.Run(t, "branch", "--show-current")) repo.Run(t, "checkout", "orbit-template/docs") - repo.WriteFile(t, "docs/reference.md", "$project_name reference\n") + repo.WriteFile(t, "docs/reference.md", "{{ vars.project_name }} reference\n") repo.Run(t, "rm", "-f", "docs/guide.md") repo.AddAndCommit(t, "update template branch contents") repo.Run(t, "checkout", runtimeBranch) @@ -108,7 +108,7 @@ func TestInstallBatchOverwriteRollsBackWhenCleanupFails(t *testing.T) { runtimeBranch := strings.TrimSpace(repo.Run(t, "branch", "--show-current")) repo.Run(t, "checkout", "orbit-template/docs") - repo.WriteFile(t, "docs/reference.md", "$project_name reference\n") + repo.WriteFile(t, "docs/reference.md", "{{ vars.project_name }} reference\n") repo.Run(t, "rm", "-f", "docs/guide.md") repo.AddAndCommit(t, "update template branch contents for batch overwrite") repo.Run(t, "checkout", runtimeBranch) diff --git a/cmd/harness/cli/install_guidance_compose_integration_test.go b/cmd/harness/cli/install_guidance_compose_integration_test.go index 56e854d..ed213c5 100644 --- a/cmd/harness/cli/install_guidance_compose_integration_test.go +++ b/cmd/harness/cli/install_guidance_compose_integration_test.go @@ -20,11 +20,11 @@ func TestHarnessInstallDryRunPreviewIncludesScopedGuidanceArtifacts(t *testing.T repo := seedHarnessInstallGuidanceRepo(t, []installGuidanceTemplateSpec{{ OrbitID: "docs", - AgentsTemplate: "You are the $project_name docs orbit.\n", - HumansTemplate: "Run the $project_name docs workflow.\n", - BootstrapTemplate: "Bootstrap the $project_name docs orbit.\n", + AgentsTemplate: "You are the {{ vars.project_name }} docs orbit.\n", + HumansTemplate: "Run the {{ vars.project_name }} docs workflow.\n", + BootstrapTemplate: "Bootstrap the {{ vars.project_name }} docs orbit.\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, }}) bindingsPath := writeHarnessInstallGuidanceBindings(t, repo.Root) @@ -48,7 +48,7 @@ func TestHarnessInstallDryRunPreviewIncludesDescriptionBackedAgentsArtifact(t *t repo := seedHarnessInstallGuidanceRepo(t, []installGuidanceTemplateSpec{{ OrbitID: "docs", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, }}) bindingsPath := writeHarnessInstallGuidanceBindings(t, repo.Root) @@ -71,11 +71,11 @@ func TestHarnessInstallAutoComposesScopedGuidanceArtifacts(t *testing.T) { repo := seedHarnessInstallGuidanceRepo(t, []installGuidanceTemplateSpec{{ OrbitID: "docs", - AgentsTemplate: "You are the $project_name docs orbit.\n", - HumansTemplate: "Run the $project_name docs workflow.\n", - BootstrapTemplate: "Bootstrap the $project_name docs orbit.\n", + AgentsTemplate: "You are the {{ vars.project_name }} docs orbit.\n", + HumansTemplate: "Run the {{ vars.project_name }} docs workflow.\n", + BootstrapTemplate: "Bootstrap the {{ vars.project_name }} docs orbit.\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, }}) bindingsPath := writeHarnessInstallGuidanceBindings(t, repo.Root) @@ -113,7 +113,7 @@ func TestHarnessInstallAutoComposesDescriptionBackedAgentsAndKeepsHarnessCheckCl repo := seedHarnessInstallGuidanceRepo(t, []installGuidanceTemplateSpec{{ OrbitID: "docs", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, }}) bindingsPath := writeHarnessInstallGuidanceBindings(t, repo.Root) @@ -146,16 +146,16 @@ func TestHarnessInstallScopedGuidanceWarnsOnUnresolvedMarkedGuidanceDrift(t *tes repo := seedHarnessInstallGuidanceRepo(t, []installGuidanceTemplateSpec{ { OrbitID: "cmd", - HumansTemplate: "Run the $project_name cmd workflow.\n", + HumansTemplate: "Run the {{ vars.project_name }} cmd workflow.\n", Files: map[string]string{ - "cmd/README.md": "$project_name cmd guide\n", + "cmd/README.md": "{{ vars.project_name }} cmd guide\n", }, }, { OrbitID: "docs", - HumansTemplate: "Run the $project_name docs workflow.\n", + HumansTemplate: "Run the {{ vars.project_name }} docs workflow.\n", Files: map[string]string{ - "docs/guide.md": "$project_name docs guide\n", + "docs/guide.md": "{{ vars.project_name }} docs guide\n", }, }, }) @@ -195,9 +195,9 @@ func TestHarnessInstallOverwriteExistingOverwritesTouchedGuidanceDrift(t *testin repo := seedHarnessInstallGuidanceRepo(t, []installGuidanceTemplateSpec{{ OrbitID: "docs", - HumansTemplate: "Run the $project_name docs workflow.\n", + HumansTemplate: "Run the {{ vars.project_name }} docs workflow.\n", Files: map[string]string{ - "docs/guide.md": "$project_name docs guide\n", + "docs/guide.md": "{{ vars.project_name }} docs guide\n", }, }}) bindingsPath := writeHarnessInstallGuidanceBindings(t, repo.Root) @@ -232,19 +232,19 @@ func TestHarnessInstallBatchAutoComposesScopedGuidanceArtifacts(t *testing.T) { repo := seedHarnessInstallGuidanceRepo(t, []installGuidanceTemplateSpec{ { OrbitID: "docs", - AgentsTemplate: "You are the $project_name docs orbit.\n", - HumansTemplate: "Run the $project_name docs workflow.\n", - BootstrapTemplate: "Bootstrap the $project_name docs orbit.\n", + AgentsTemplate: "You are the {{ vars.project_name }} docs orbit.\n", + HumansTemplate: "Run the {{ vars.project_name }} docs workflow.\n", + BootstrapTemplate: "Bootstrap the {{ vars.project_name }} docs orbit.\n", Files: map[string]string{ - "docs/guide.md": "$project_name docs guide\n", + "docs/guide.md": "{{ vars.project_name }} docs guide\n", }, }, { OrbitID: "cmd", - AgentsTemplate: "Use $project_name cmd release flow.\n", - HumansTemplate: "Run the $project_name cmd workflow.\n", + AgentsTemplate: "Use {{ vars.project_name }} cmd release flow.\n", + HumansTemplate: "Run the {{ vars.project_name }} cmd workflow.\n", Files: map[string]string{ - "cmd/README.md": "$project_name cmd guide\n", + "cmd/README.md": "{{ vars.project_name }} cmd guide\n", }, }, }) @@ -283,23 +283,23 @@ func TestHarnessInstallBatchScopedGuidanceWarnsOnUnresolvedMarkedGuidanceDrift(t repo := seedHarnessInstallGuidanceRepo(t, []installGuidanceTemplateSpec{ { OrbitID: "cmd", - HumansTemplate: "Run the $project_name cmd workflow.\n", + HumansTemplate: "Run the {{ vars.project_name }} cmd workflow.\n", Files: map[string]string{ - "cmd/README.md": "$project_name cmd guide\n", + "cmd/README.md": "{{ vars.project_name }} cmd guide\n", }, }, { OrbitID: "docs", - HumansTemplate: "Run the $project_name docs workflow.\n", + HumansTemplate: "Run the {{ vars.project_name }} docs workflow.\n", Files: map[string]string{ - "docs/guide.md": "$project_name docs guide\n", + "docs/guide.md": "{{ vars.project_name }} docs guide\n", }, }, { OrbitID: "ops", - HumansTemplate: "Run the $project_name ops workflow.\n", + HumansTemplate: "Run the {{ vars.project_name }} ops workflow.\n", Files: map[string]string{ - "ops/runbook.md": "$project_name ops runbook\n", + "ops/runbook.md": "{{ vars.project_name }} ops runbook\n", }, }, }) @@ -341,16 +341,16 @@ func TestHarnessInstallBatchOverwriteExistingOverwritesTouchedGuidanceDrift(t *t repo := seedHarnessInstallGuidanceRepo(t, []installGuidanceTemplateSpec{ { OrbitID: "docs", - HumansTemplate: "Run the $project_name docs workflow.\n", + HumansTemplate: "Run the {{ vars.project_name }} docs workflow.\n", Files: map[string]string{ - "docs/guide.md": "$project_name docs guide\n", + "docs/guide.md": "{{ vars.project_name }} docs guide\n", }, }, { OrbitID: "ops", - HumansTemplate: "Run the $project_name ops workflow.\n", + HumansTemplate: "Run the {{ vars.project_name }} ops workflow.\n", Files: map[string]string{ - "ops/runbook.md": "$project_name ops runbook\n", + "ops/runbook.md": "{{ vars.project_name }} ops runbook\n", }, }, }) diff --git a/cmd/harness/cli/progress_integration_test.go b/cmd/harness/cli/progress_integration_test.go index cb4acea..ce94ae3 100644 --- a/cmd/harness/cli/progress_integration_test.go +++ b/cmd/harness/cli/progress_integration_test.go @@ -175,9 +175,9 @@ func seedInstalledHarnessProgressRepo(t *testing.T) *testutil.Repo { " value: Orbit\n" + " description: Product title\n", Files: map[string]string{ - "docs/guide.md": "$project_name guide\n", + "docs/guide.md": "{{ vars.project_name }} guide\n", }, - AgentsTemplate: "Follow $project_name docs workflow\n", + AgentsTemplate: "Follow {{ vars.project_name }} docs workflow\n", }, }) diff --git a/cmd/harness/cli/template_publish_integration_test.go b/cmd/harness/cli/template_publish_integration_test.go index b27de91..661e7d2 100644 --- a/cmd/harness/cli/template_publish_integration_test.go +++ b/cmd/harness/cli/template_publish_integration_test.go @@ -335,7 +335,7 @@ func TestHarnessTemplatePublishPushesWhenRemoteTemplateBranchAdvancedBeyondLocal remoteGuide, readErr := gitpkg.ReadFileAtRemoteRef(context.Background(), repo.Root, remoteURL, "refs/heads/harness-template/workspace", "docs/guide.md") require.NoError(t, readErr) - require.Equal(t, "$project_name guide v2\n", string(remoteGuide)) + require.Equal(t, "{{ vars.project_name }} guide v2\n", string(remoteGuide)) _, readErr = gitpkg.ReadFileAtRemoteRef(context.Background(), repo.Root, remoteURL, "refs/heads/harness-template/workspace", "docs/remote-note.md") require.Error(t, readErr) @@ -402,7 +402,7 @@ func TestHarnessTemplatePublishPushesWhenRemoteTemplateBranchDivergedFromMatchin remoteGuide, readErr := gitpkg.ReadFileAtRemoteRef(context.Background(), repo.Root, remoteURL, "refs/heads/harness-template/workspace", "docs/guide.md") require.NoError(t, readErr) - require.Equal(t, "$project_name guide\n", string(remoteGuide)) + require.Equal(t, "{{ vars.project_name }} guide\n", string(remoteGuide)) _, readErr = gitpkg.ReadFileAtRemoteRef(context.Background(), repo.Root, remoteURL, "refs/heads/harness-template/workspace", "docs/remote-note.md") require.Error(t, readErr) diff --git a/cmd/orbit/cli/brief_materialize_check_integration_test.go b/cmd/orbit/cli/brief_materialize_check_integration_test.go index 2e188f9..d6b23b4 100644 --- a/cmd/orbit/cli/brief_materialize_check_integration_test.go +++ b/cmd/orbit/cli/brief_materialize_check_integration_test.go @@ -15,7 +15,7 @@ func TestOrbitBriefMaterializeCheckReportsStructuredOnlyWithoutWritingContainer( t.Parallel() repo := seedBriefMaterializeRevisionRepo(t, "runtime", ""+ - "You are the $project_name docs orbit.\n"+ + "You are the {{ vars.project_name }} docs orbit.\n"+ "Keep release notes current.\n", "", ) @@ -37,7 +37,7 @@ func TestOrbitBriefMaterializeCheckReportsInSyncJSON(t *testing.T) { t.Parallel() repo := seedBriefMaterializeRevisionRepo(t, "runtime", ""+ - "You are the $project_name docs orbit.\n"+ + "You are the {{ vars.project_name }} docs orbit.\n"+ "Keep release notes current.\n", ""+ "Workspace overview.\n"+ @@ -82,7 +82,7 @@ func TestOrbitBriefMaterializeCheckReportsDriftedWithoutWriting(t *testing.T) { t.Parallel() repo := seedBriefMaterializeRevisionRepo(t, "orbit_template", ""+ - "You are the $project_name docs orbit.\n"+ + "You are the {{ vars.project_name }} docs orbit.\n"+ "Keep release notes current.\n", ""+ "Workspace overview.\n"+ @@ -113,7 +113,7 @@ func TestOrbitBriefMaterializeCheckReportsInvalidContainer(t *testing.T) { t.Parallel() repo := seedBriefMaterializeRevisionRepo(t, "source", ""+ - "You are the $project_name docs orbit.\n", + "You are the {{ vars.project_name }} docs orbit.\n", ""+ "Workspace overview.\n"+ "\n"+ @@ -164,7 +164,7 @@ func TestOrbitBriefMaterializeCheckReportsMissingTruthAndRecoverableBackfillJSON func TestOrbitBriefMaterializeCheckRejectsPlainRevision(t *testing.T) { t.Parallel() - repo := seedBriefMaterializeRevisionRepo(t, "", "You are the $project_name docs orbit.\n", "") + repo := seedBriefMaterializeRevisionRepo(t, "", "You are the {{ vars.project_name }} docs orbit.\n", "") stdout, stderr, err := executeCLI(t, repo.Root, "brief", "materialize", "--orbit", "docs", "--check") require.Error(t, err) diff --git a/cmd/orbit/cli/cli_integration_test.go b/cmd/orbit/cli/cli_integration_test.go index a0b53cd..d1b3f76 100644 --- a/cmd/orbit/cli/cli_integration_test.go +++ b/cmd/orbit/cli/cli_integration_test.go @@ -460,7 +460,7 @@ func TestOrbitBriefBackfillWritesCurrentOrbitBlockToHostedSpec(t *testing.T) { spec, err := orbitpkg.LoadHostedOrbitSpec(context.Background(), repo.Root, "docs") require.NoError(t, err) require.NotNil(t, spec.Meta) - require.Equal(t, "You are the $project_name docs orbit.\nKeep release notes current.\n", spec.Meta.AgentsTemplate) + require.Equal(t, "You are the {{ vars.project_name }} docs orbit.\nKeep release notes current.\n", spec.Meta.AgentsTemplate) agentsData, err := os.ReadFile(filepath.Join(repo.Root, "AGENTS.md")) require.NoError(t, err) @@ -518,7 +518,7 @@ func TestOrbitBriefBackfillWritesCurrentOrbitBlockOnTemplateRevision(t *testing. spec, err := orbitpkg.LoadHostedOrbitSpec(context.Background(), repo.Root, "docs") require.NoError(t, err) require.NotNil(t, spec.Meta) - require.Equal(t, "You are the $project_name docs orbit.\nKeep release notes current.\n", spec.Meta.AgentsTemplate) + require.Equal(t, "You are the {{ vars.project_name }} docs orbit.\nKeep release notes current.\n", spec.Meta.AgentsTemplate) } func TestOrbitBriefBackfillWritesCurrentOrbitBlockOnSourceRevision(t *testing.T) { @@ -534,7 +534,7 @@ func TestOrbitBriefBackfillWritesCurrentOrbitBlockOnSourceRevision(t *testing.T) spec, err := orbitpkg.LoadHostedOrbitSpec(context.Background(), repo.Root, "docs") require.NoError(t, err) require.NotNil(t, spec.Meta) - require.Equal(t, "You are the $project_name docs orbit.\nKeep release notes current.\n", spec.Meta.AgentsTemplate) + require.Equal(t, "You are the {{ vars.project_name }} docs orbit.\nKeep release notes current.\n", spec.Meta.AgentsTemplate) } func TestOrbitBriefBackfillReportsSkippedStatusWhenHostedTruthAlreadyMatches(t *testing.T) { @@ -547,7 +547,7 @@ func TestOrbitBriefBackfillReportsSkippedStatusWhenHostedTruthAlreadyMatches(t * require.NoError(t, err) spec.Description = "Docs orbit" require.NotNil(t, spec.Meta) - spec.Meta.AgentsTemplate = "You are the $project_name docs orbit.\nKeep release notes current.\n" + spec.Meta.AgentsTemplate = "You are the {{ vars.project_name }} docs orbit.\nKeep release notes current.\n" _, err = orbitpkg.WriteHostedOrbitSpec(repo.Root, spec) require.NoError(t, err) @@ -597,7 +597,7 @@ func TestOrbitBriefBackfillReportsRemovedStatusWhenClearingHostedTruth(t *testin require.NoError(t, err) spec.Description = "Docs orbit" require.NotNil(t, spec.Meta) - spec.Meta.AgentsTemplate = "You are the $project_name docs orbit.\nKeep release notes current.\n" + spec.Meta.AgentsTemplate = "You are the {{ vars.project_name }} docs orbit.\nKeep release notes current.\n" _, err = orbitpkg.WriteHostedOrbitSpec(repo.Root, spec) require.NoError(t, err) @@ -649,7 +649,7 @@ func TestOrbitBriefMaterializeWritesRenderedCurrentOrbitBlockToRootAgents(t *tes t.Parallel() repo := seedBriefMaterializeRevisionRepo(t, "runtime", ""+ - "You are the $project_name docs orbit.\n"+ + "You are the {{ vars.project_name }} docs orbit.\n"+ "Keep release notes current.\n", "", ) @@ -670,14 +670,14 @@ func TestOrbitBriefMaterializeWritesRenderedCurrentOrbitBlockToRootAgents(t *tes spec, err := orbitpkg.LoadHostedOrbitSpec(context.Background(), repo.Root, "docs") require.NoError(t, err) require.NotNil(t, spec.Meta) - require.Equal(t, "You are the $project_name docs orbit.\nKeep release notes current.\n", spec.Meta.AgentsTemplate) + require.Equal(t, "You are the {{ vars.project_name }} docs orbit.\nKeep release notes current.\n", spec.Meta.AgentsTemplate) } func TestOrbitBriefMaterializePreservesOtherBlocksAndProseOnSourceRevision(t *testing.T) { t.Parallel() repo := seedBriefMaterializeRevisionRepo(t, "source", ""+ - "You are the $project_name docs orbit.\n"+ + "You are the {{ vars.project_name }} docs orbit.\n"+ "Keep release notes current.\n", ""+ "Workspace overview.\n"+ @@ -696,7 +696,7 @@ func TestOrbitBriefMaterializePreservesOtherBlocksAndProseOnSourceRevision(t *te agents := string(agentsData) require.Contains(t, agents, "Workspace overview.\n") require.Contains(t, agents, "\nAPI brief.\n\n") - require.Contains(t, agents, "\nYou are the $project_name docs orbit.\nKeep release notes current.\n\n") + require.Contains(t, agents, "\nYou are the {{ vars.project_name }} docs orbit.\nKeep release notes current.\n\n") require.Less(t, strings.Index(agents, "Workspace overview.\n"), strings.Index(agents, "")) require.Less(t, strings.Index(agents, ""), strings.Index(agents, "")) } @@ -705,7 +705,7 @@ func TestOrbitBriefMaterializeFailsClosedWhenCurrentOrbitBlockIsDrifted(t *testi t.Parallel() repo := seedBriefMaterializeRevisionRepo(t, "orbit_template", ""+ - "You are the $project_name docs orbit.\n"+ + "You are the {{ vars.project_name }} docs orbit.\n"+ "Keep release notes current.\n", ""+ "Workspace overview.\n"+ @@ -735,7 +735,7 @@ func TestOrbitBriefMaterializeForceOverwritesDriftedCurrentOrbitBlock(t *testing t.Parallel() repo := seedBriefMaterializeRevisionRepo(t, "orbit_template", ""+ - "You are the $project_name docs orbit.\n"+ + "You are the {{ vars.project_name }} docs orbit.\n"+ "Keep release notes current.\n", ""+ "Workspace overview.\n"+ @@ -757,14 +757,14 @@ func TestOrbitBriefMaterializeForceOverwritesDriftedCurrentOrbitBlock(t *testing require.NoError(t, err) agents := string(agentsData) require.Contains(t, agents, "\nAPI brief.\n\n") - require.Contains(t, agents, "\nYou are the $project_name docs orbit.\nKeep release notes current.\n\n") + require.Contains(t, agents, "\nYou are the {{ vars.project_name }} docs orbit.\nKeep release notes current.\n\n") require.NotContains(t, agents, "You are the Drifted docs orbit.\n") } func TestOrbitBriefMaterializeRejectsPlainRevision(t *testing.T) { t.Parallel() - repo := seedBriefMaterializeRevisionRepo(t, "", "You are the $project_name docs orbit.\n", "") + repo := seedBriefMaterializeRevisionRepo(t, "", "You are the {{ vars.project_name }} docs orbit.\n", "") stdout, stderr, err := executeCLI(t, repo.Root, "brief", "materialize", "--orbit", "docs") require.Error(t, err) @@ -776,7 +776,7 @@ func TestOrbitBriefMaterializeRejectsPlainRevision(t *testing.T) { func TestOrbitBriefMaterializeRejectsHarnessTemplateRevision(t *testing.T) { t.Parallel() - repo := seedBriefMaterializeRevisionRepo(t, "harness_template", "You are the $project_name docs orbit.\n", "") + repo := seedBriefMaterializeRevisionRepo(t, "harness_template", "You are the {{ vars.project_name }} docs orbit.\n", "") stdout, stderr, err := executeCLI(t, repo.Root, "brief", "materialize", "--orbit", "docs") require.Error(t, err) diff --git a/cmd/orbit/cli/guidance_integration_test.go b/cmd/orbit/cli/guidance_integration_test.go index 763d320..64f96ff 100644 --- a/cmd/orbit/cli/guidance_integration_test.go +++ b/cmd/orbit/cli/guidance_integration_test.go @@ -21,9 +21,9 @@ func TestOrbitGuidanceMaterializeAllWritesAllRootArtifacts(t *testing.T) { t.Parallel() repo := seedOrbitGuidanceRevisionRepo(t, - "You are the $project_name docs orbit.\nKeep release notes current.\n", - "Run the $project_name docs workflow.\n", - "Bootstrap the $project_name docs orbit.\n", + "You are the {{ vars.project_name }} docs orbit.\nKeep release notes current.\n", + "Run the {{ vars.project_name }} docs workflow.\n", + "Bootstrap the {{ vars.project_name }} docs orbit.\n", nil, ) @@ -72,7 +72,7 @@ func TestOrbitGuidanceMaterializeAllSkipsMissingAuthoredTruthByDefault(t *testin t.Parallel() repo := seedOrbitGuidanceRevisionRepo(t, - "You are the $project_name docs orbit.\nKeep release notes current.\n", + "You are the {{ vars.project_name }} docs orbit.\nKeep release notes current.\n", "", "", nil, @@ -152,7 +152,7 @@ func TestOrbitGuidanceMaterializeTextOutputUsesNeutralProcessedHeadline(t *testi t.Parallel() repo := seedOrbitGuidanceRevisionRepo(t, - "You are the $project_name docs orbit.\n", + "You are the {{ vars.project_name }} docs orbit.\n", "", "", nil, @@ -171,7 +171,7 @@ func TestOrbitGuidanceMaterializeDefaultsToSourceBranchOrbitBeforeStaleCurrentOr t.Parallel() repo := seedOrbitGuidanceRevisionRepo(t, - "You are the $project_name docs orbit.\n", + "You are the {{ vars.project_name }} docs orbit.\n", "", "", nil, @@ -199,7 +199,7 @@ func TestOrbitGuidanceMaterializeDefaultsToSourceBranchOrbitBeforeStaleCurrentOr agentsData, err := os.ReadFile(filepath.Join(repo.Root, "AGENTS.md")) require.NoError(t, err) - require.Contains(t, string(agentsData), "You are the $project_name docs orbit.\n") + require.Contains(t, string(agentsData), "You are the {{ vars.project_name }} docs orbit.\n") } func TestOrbitGuidanceBackfillDefaultsToOrbitTemplateBranchOrbit(t *testing.T) { @@ -237,7 +237,7 @@ func TestOrbitGuidanceBackfillDefaultsToOrbitTemplateBranchOrbit(t *testing.T) { spec, err := orbitpkg.LoadHostedOrbitSpec(context.Background(), repo.Root, "docs") require.NoError(t, err) require.NotNil(t, spec.Meta) - require.Equal(t, "You are the $project_name docs orbit.\n", spec.Meta.AgentsTemplate) + require.Equal(t, "You are the {{ vars.project_name }} docs orbit.\n", spec.Meta.AgentsTemplate) } func TestOrbitGuidanceMaterializeRejectsMissingBootstrapTruthWithoutSeedEmpty(t *testing.T) { @@ -270,7 +270,7 @@ func TestOrbitGuidanceMaterializeStrictAllFailsBeforeWritingPartialArtifacts(t * t.Parallel() repo := seedOrbitGuidanceRevisionRepo(t, - "You are the $project_name docs orbit.\nKeep release notes current.\n", + "You are the {{ vars.project_name }} docs orbit.\nKeep release notes current.\n", "", "", nil, @@ -294,9 +294,9 @@ func TestOrbitGuidanceMaterializeAllSkipsCompletedBootstrapByDefault(t *testing. t.Parallel() repo := seedOrbitGuidanceRevisionRepo(t, - "You are the $project_name docs orbit.\n", - "Run the $project_name docs workflow.\n", - "Bootstrap the $project_name docs orbit.\n", + "You are the {{ vars.project_name }} docs orbit.\n", + "Run the {{ vars.project_name }} docs workflow.\n", + "Bootstrap the {{ vars.project_name }} docs orbit.\n", nil, ) store, err := statepkg.NewFSStore(repo.GitDir(t)) @@ -346,7 +346,7 @@ func TestOrbitGuidanceMaterializeCheckReportsSeedEmptyEligibility(t *testing.T) t.Parallel() repo := seedOrbitGuidanceRevisionRepo(t, - "You are the $project_name docs orbit.\n", + "You are the {{ vars.project_name }} docs orbit.\n", "", "", nil, @@ -464,9 +464,9 @@ func TestOrbitGuidanceMaterializeSeedEmptyCreatesEditableBlocksThatBackfillAllTe spec, err := orbitpkg.LoadHostedOrbitSpec(context.Background(), repo.Root, "docs") require.NoError(t, err) require.NotNil(t, spec.Meta) - require.Equal(t, "You are the $project_name docs orbit.\nKeep release notes current.\n", spec.Meta.AgentsTemplate) - require.Equal(t, "Run the $project_name docs workflow.\n", spec.Meta.HumansTemplate) - require.Equal(t, "Bootstrap the $project_name docs orbit.\n", spec.Meta.BootstrapTemplate) + require.Equal(t, "You are the {{ vars.project_name }} docs orbit.\nKeep release notes current.\n", spec.Meta.AgentsTemplate) + require.Equal(t, "Run the {{ vars.project_name }} docs workflow.\n", spec.Meta.HumansTemplate) + require.Equal(t, "Bootstrap the {{ vars.project_name }} docs orbit.\n", spec.Meta.BootstrapTemplate) } func TestOrbitGuidanceMaterializeSeedEmptyAppendsMissingBlockToExistingArtifact(t *testing.T) { @@ -603,7 +603,7 @@ func TestOrbitGuidanceBackfillAllDoesNotPersistEmptySeededTemplates(t *testing.T func TestOrbitGuidanceBackfillAgentsReportsSkippedWhenHostedTemplateAlreadyMatches(t *testing.T) { t.Parallel() - repo := seedOrbitGuidanceRevisionRepo(t, "You are the $project_name docs orbit.\nKeep release notes current.\n", "", "", map[string]string{ + repo := seedOrbitGuidanceRevisionRepo(t, "You are the {{ vars.project_name }} docs orbit.\nKeep release notes current.\n", "", "", map[string]string{ "AGENTS.md": "" + "Workspace overview.\n" + "\n" + @@ -680,9 +680,9 @@ func TestOrbitGuidanceBackfillAllWritesAllHostedTemplates(t *testing.T) { spec, err := orbitpkg.LoadHostedOrbitSpec(context.Background(), repo.Root, "docs") require.NoError(t, err) require.NotNil(t, spec.Meta) - require.Equal(t, "You are the $project_name docs orbit.\nKeep release notes current.\n", spec.Meta.AgentsTemplate) - require.Equal(t, "Run the $project_name docs workflow.\n", spec.Meta.HumansTemplate) - require.Equal(t, "Bootstrap the $project_name docs orbit.\n", spec.Meta.BootstrapTemplate) + require.Equal(t, "You are the {{ vars.project_name }} docs orbit.\nKeep release notes current.\n", spec.Meta.AgentsTemplate) + require.Equal(t, "Run the {{ vars.project_name }} docs workflow.\n", spec.Meta.HumansTemplate) + require.Equal(t, "Bootstrap the {{ vars.project_name }} docs orbit.\n", spec.Meta.BootstrapTemplate) } func TestOrbitGuidanceBackfillAllSkipsMissingBootstrapRootArtifact(t *testing.T) { @@ -732,8 +732,8 @@ func TestOrbitGuidanceBackfillAllSkipsMissingBootstrapRootArtifact(t *testing.T) spec, err := orbitpkg.LoadHostedOrbitSpec(context.Background(), repo.Root, "docs") require.NoError(t, err) require.NotNil(t, spec.Meta) - require.Equal(t, "You are the $project_name docs orbit.\nKeep release notes current.\n", spec.Meta.AgentsTemplate) - require.Equal(t, "Run the $project_name docs workflow.\n", spec.Meta.HumansTemplate) + require.Equal(t, "You are the {{ vars.project_name }} docs orbit.\nKeep release notes current.\n", spec.Meta.AgentsTemplate) + require.Equal(t, "Run the {{ vars.project_name }} docs workflow.\n", spec.Meta.HumansTemplate) require.Equal(t, "Keep existing bootstrap truth.\n", spec.Meta.BootstrapTemplate) } @@ -777,7 +777,7 @@ func TestOrbitGuidanceBackfillAllSkipsMissingBlocksWithoutHostedTruth(t *testing require.NoError(t, err) require.NotNil(t, spec.Meta) require.Empty(t, spec.Meta.AgentsTemplate) - require.Equal(t, "Run the $project_name docs workflow.\n", spec.Meta.HumansTemplate) + require.Equal(t, "Run the {{ vars.project_name }} docs workflow.\n", spec.Meta.HumansTemplate) require.Empty(t, spec.Meta.BootstrapTemplate) } @@ -827,7 +827,7 @@ func TestOrbitGuidanceBackfillBootstrapFailsWhenRootArtifactMissing(t *testing.T func TestOrbitGuidanceBackfillEmptyBootstrapBlockRemovesHostedTemplate(t *testing.T) { t.Parallel() - repo := seedOrbitGuidanceRevisionRepo(t, "", "", "Bootstrap the $project_name docs orbit.\n", map[string]string{ + repo := seedOrbitGuidanceRevisionRepo(t, "", "", "Bootstrap the {{ vars.project_name }} docs orbit.\n", map[string]string{ "BOOTSTRAP.md": "" + "Workspace overview.\n" + "\n" + @@ -852,7 +852,7 @@ func TestOrbitGuidanceBackfillEmptyBootstrapBlockRemovesHostedTemplate(t *testin func TestOrbitGuidanceBackfillCheckReportsBootstrapBackfillAllowedWhenInSync(t *testing.T) { t.Parallel() - repo := seedOrbitGuidanceRevisionRepo(t, "", "", "Bootstrap the $project_name docs orbit.\n", map[string]string{ + repo := seedOrbitGuidanceRevisionRepo(t, "", "", "Bootstrap the {{ vars.project_name }} docs orbit.\n", map[string]string{ "BOOTSTRAP.md": "" + "Workspace overview.\n" + "\n" + @@ -883,7 +883,7 @@ func TestOrbitGuidanceBackfillCheckReportsBootstrapBackfillAllowedWhenInSync(t * func TestOrbitGuidanceMaterializeRejectsCompletedBootstrapInRuntime(t *testing.T) { t.Parallel() - repo := seedOrbitGuidanceRevisionRepo(t, "", "", "Bootstrap the $project_name docs orbit.\n", nil) + repo := seedOrbitGuidanceRevisionRepo(t, "", "", "Bootstrap the {{ vars.project_name }} docs orbit.\n", nil) store, err := statepkg.NewFSStore(repo.GitDir(t)) require.NoError(t, err) require.NoError(t, store.WriteRuntimeStateSnapshot(statepkg.RuntimeStateSnapshot{ @@ -905,7 +905,7 @@ func TestOrbitGuidanceMaterializeRejectsCompletedBootstrapInRuntime(t *testing.T func TestOrbitGuidanceBackfillRejectsCompletedBootstrapInRuntime(t *testing.T) { t.Parallel() - repo := seedOrbitGuidanceRevisionRepo(t, "", "", "Bootstrap the $project_name docs orbit.\n", map[string]string{ + repo := seedOrbitGuidanceRevisionRepo(t, "", "", "Bootstrap the {{ vars.project_name }} docs orbit.\n", map[string]string{ "BOOTSTRAP.md": "" + "Workspace overview.\n" + "\n" + diff --git a/cmd/orbit/cli/harness/bundle_shrink_test.go b/cmd/orbit/cli/harness/bundle_shrink_test.go index bbece6f..4ccaf4b 100644 --- a/cmd/orbit/cli/harness/bundle_shrink_test.go +++ b/cmd/orbit/cli/harness/bundle_shrink_test.go @@ -127,7 +127,7 @@ func TestBuildBundleMemberShrinkPlanIgnoresSnapshotRootGuidancePaths(t *testing. " file_digests:\n"+ " BOOTSTRAP.md: "+contentDigest([]byte("Bootstrap guidance\n"))+"\n"+ " HUMANS.md: "+contentDigest([]byte("Human guidance\n"))+"\n"+ - " docs/guide.md: "+contentDigest([]byte("Docs $project_name guide\n"))+"\n"+ + " docs/guide.md: "+contentDigest([]byte("Docs {{ vars.project_name }} guide\n"))+"\n"+ " variables:\n"+ " project_name:\n"+ " description: Project name\n"+ diff --git a/cmd/orbit/cli/harness/guidance_compose_test.go b/cmd/orbit/cli/harness/guidance_compose_test.go index 38b976d..2f36a11 100644 --- a/cmd/orbit/cli/harness/guidance_compose_test.go +++ b/cmd/orbit/cli/harness/guidance_compose_test.go @@ -74,14 +74,14 @@ func seedRuntimeGuidanceComposeRepo(t *testing.T) *testutil.Repo { docsSpec, err := orbitpkg.DefaultHostedMemberSchemaSpec("docs") require.NoError(t, err) require.NotNil(t, docsSpec.Meta) - docsSpec.Meta.HumansTemplate = "Run the $project_name docs workflow.\n" + docsSpec.Meta.HumansTemplate = "Run the {{ vars.project_name }} docs workflow.\n" _, err = orbitpkg.WriteHostedOrbitSpec(repo.Root, docsSpec) require.NoError(t, err) cmdSpec, err := orbitpkg.DefaultHostedMemberSchemaSpec("cmd") require.NoError(t, err) require.NotNil(t, cmdSpec.Meta) - cmdSpec.Meta.HumansTemplate = "Run the $project_name cmd workflow.\n" + cmdSpec.Meta.HumansTemplate = "Run the {{ vars.project_name }} cmd workflow.\n" _, err = orbitpkg.WriteHostedOrbitSpec(repo.Root, cmdSpec) require.NoError(t, err) diff --git a/cmd/orbit/cli/harness/readiness_test.go b/cmd/orbit/cli/harness/readiness_test.go index 1b8bbd1..7c163b0 100644 --- a/cmd/orbit/cli/harness/readiness_test.go +++ b/cmd/orbit/cli/harness/readiness_test.go @@ -104,7 +104,7 @@ func TestEvaluateRuntimeReadinessInstallBackedOrbitWithMissingRequiredBindingsIs spec, err := orbitpkg.DefaultHostedMemberSchemaSpec("docs") require.NoError(t, err) - spec.Meta.AgentsTemplate = "Docs orbit for $project_name\n" + spec.Meta.AgentsTemplate = "Docs orbit for {{ vars.project_name }}\n" _, err = orbitpkg.WriteHostedOrbitSpec(repo.Root, spec) require.NoError(t, err) @@ -397,7 +397,7 @@ func TestEvaluateRuntimeReadinessInvalidAgentsContainerIsBroken(t *testing.T) { spec, err := orbitpkg.DefaultHostedMemberSchemaSpec("docs") require.NoError(t, err) - spec.Meta.AgentsTemplate = "Docs orbit for $project_name\n" + spec.Meta.AgentsTemplate = "Docs orbit for {{ vars.project_name }}\n" _, err = orbitpkg.WriteHostedOrbitSpec(repo.Root, spec) require.NoError(t, err) diff --git a/cmd/orbit/cli/harness/root_agents_test.go b/cmd/orbit/cli/harness/root_agents_test.go index 921ab35..1c27531 100644 --- a/cmd/orbit/cli/harness/root_agents_test.go +++ b/cmd/orbit/cli/harness/root_agents_test.go @@ -38,8 +38,8 @@ func TestBuildRootAgentsTemplateFileAppliesWholeFileReplacementAndStripsRuntimeM require.Equal(t, &orbittemplate.CandidateFile{ Path: "AGENTS.md", Content: []byte("" + - "# Rules for $project_name\n" + - "Use $project_name docs at $service_url\n" + + "# Rules for {{ vars.project_name }}\n" + + "Use {{ vars.project_name }} docs at {{ vars.service_url }}\n" + "\n"), Mode: gitpkg.FileModeRegular, }, result.File) diff --git a/cmd/orbit/cli/harness/template_candidate_test.go b/cmd/orbit/cli/harness/template_candidate_test.go index 5b32924..d416fe4 100644 --- a/cmd/orbit/cli/harness/template_candidate_test.go +++ b/cmd/orbit/cli/harness/template_candidate_test.go @@ -42,7 +42,7 @@ func TestBuildTemplateMemberCandidateBuildsFilesAndVariableSpecs(t *testing.T) { }, { Path: "docs/guide.md", - Content: []byte("$project_name guide\n"), + Content: []byte("{{ vars.project_name }} guide\n"), Mode: gitpkg.FileModeRegular, }, }, candidate.Files) diff --git a/cmd/orbit/cli/harness/template_install_preview.go b/cmd/orbit/cli/harness/template_install_preview.go index 05f2a3e..fb8f51a 100644 --- a/cmd/orbit/cli/harness/template_install_preview.go +++ b/cmd/orbit/cli/harness/template_install_preview.go @@ -650,7 +650,7 @@ func buildRenderedTemplateInstallPayload( renderValues[name] = resolved.Value } - renderedFiles, err := renderTemplateInstallFiles(ordinaryFiles, renderValues, !input.RequireResolvedBindings) + renderedFiles, err := renderTemplateInstallFiles(ordinaryFiles, renderValues, declared, !input.RequireResolvedBindings) if err != nil { return nil, nil, nil, nil, nil, nil, fmt.Errorf("render harness template files: %w", err) } @@ -659,6 +659,7 @@ func buildRenderedTemplateInstallPayload( renderedAgentsFiles, err := renderTemplateInstallFiles( []orbittemplate.CandidateFile{*rootAgentsFile}, renderValues, + declared, !input.RequireResolvedBindings, ) if err != nil { @@ -1004,16 +1005,17 @@ func sortedBundleShrinkPlanKeys(plans map[string]BundleMemberShrinkPlan) []strin func renderTemplateInstallFiles( files []orbittemplate.CandidateFile, renderValues map[string]string, + declared map[string]bindings.VariableDeclaration, allowUnresolved bool, ) ([]orbittemplate.CandidateFile, error) { if allowUnresolved { - rendered, err := orbittemplate.RenderTemplateFilesAllowingUnresolved(files, renderValues) + rendered, err := orbittemplate.RenderTemplateFilesWithDeclarationsAllowingUnresolved(files, renderValues, declared) if err != nil { return nil, fmt.Errorf("render relaxed template files: %w", err) } return rendered, nil } - rendered, err := orbittemplate.RenderTemplateFiles(files, renderValues) + rendered, err := orbittemplate.RenderTemplateFilesWithDeclarations(files, renderValues, declared) if err != nil { return nil, fmt.Errorf("render strict template files: %w", err) } diff --git a/cmd/orbit/cli/harness/template_install_preview_test.go b/cmd/orbit/cli/harness/template_install_preview_test.go index 146cf35..9a27edf 100644 --- a/cmd/orbit/cli/harness/template_install_preview_test.go +++ b/cmd/orbit/cli/harness/template_install_preview_test.go @@ -42,7 +42,7 @@ func TestBuildTemplateInstallPreviewAllowsUnresolvedRequiredBindings(t *testing. " project_name:\n"+ " description: Project title\n"+ " required: true\n") - sourceRepo.WriteFile(t, "docs/guide.md", "$project_name command $command_name\n") + sourceRepo.WriteFile(t, "docs/guide.md", "{{ vars.project_name }} command {{ vars.command_name }}\n") sourceRepo.AddAndCommit(t, "add two variable harness template") source, err := ResolveLocalTemplateInstallSource(ctx, sourceRepo.Root, "HEAD") require.NoError(t, err) @@ -73,7 +73,7 @@ func TestBuildTemplateInstallPreviewAllowsUnresolvedRequiredBindings(t *testing. }) require.NoError(t, err) require.Equal(t, []string{"install kept harness template variables unresolved: command_name"}, preview.Warnings) - require.Equal(t, "Orbit command $command_name\n", string(requireTemplateInstallFile(t, preview.RenderedFiles, "docs/guide.md").Content)) + require.Equal(t, "Orbit command {{ vars.command_name }}\n", string(requireTemplateInstallFile(t, preview.RenderedFiles, "docs/guide.md").Content)) require.Equal(t, orbittemplate.InstallVariablesSnapshot{ Declarations: map[string]bindings.VariableDeclaration{ "command_name": {Description: "CLI command name", Required: true}, @@ -91,7 +91,7 @@ func TestBuildTemplateInstallPreviewAllowsUnresolvedRequiredBindings(t *testing. require.Contains(t, result.WrittenPaths, ".harness/bundles/workspace.yaml") renderedData, err := os.ReadFile(filepath.Join(runtimeRepo.Root, "docs", "guide.md")) require.NoError(t, err) - require.Equal(t, "Orbit command $command_name\n", string(renderedData)) + require.Equal(t, "Orbit command {{ vars.command_name }}\n", string(renderedData)) bundleRecord, err := LoadBundleRecord(runtimeRepo.Root, "workspace") require.NoError(t, err) @@ -190,6 +190,7 @@ func TestBuildTemplateInstallPreviewSnapshotsEmptyVariablesContract(t *testing.T " - orbit_id: workspace\n"+ "variables: {}\n") sourceRepo.WriteFile(t, "docs/guide.md", "Static workspace guide\n") + sourceRepo.WriteFile(t, "schema/example.schema.json", "{\"title\":\"Static workspace\"}\n") sourceRepo.AddAndCommit(t, "remove harness template variables") source, err := ResolveLocalTemplateInstallSource(ctx, sourceRepo.Root, "HEAD") require.NoError(t, err) @@ -334,7 +335,7 @@ func TestApplyTemplateInstallPreviewRollsBackWhenBundleCleanupFails(t *testing.T originalVarsData, err := os.ReadFile(VarsPath(runtimeRepo.Root)) require.NoError(t, err) - sourceRepo.WriteFile(t, "docs/reference.md", "$project_name reference\n") + sourceRepo.WriteFile(t, "docs/reference.md", "{{ vars.project_name }} reference\n") sourceRepo.Run(t, "rm", "-f", "docs/guide.md") sourceRepo.AddAndCommit(t, "update harness template source contents") @@ -437,7 +438,7 @@ func TestBuildTemplateInstallPreviewConflictsWhenStaleBundlePathDrifted(t *testi require.NoError(t, err) runtimeRepo.WriteFile(t, "docs/guide.md", "Locally drifted guide\n") - sourceRepo.WriteFile(t, "docs/reference.md", "$project_name reference\n") + sourceRepo.WriteFile(t, "docs/reference.md", "{{ vars.project_name }} reference\n") sourceRepo.Run(t, "rm", "-f", "docs/guide.md") sourceRepo.AddAndCommit(t, "replace bundle docs payload") @@ -465,7 +466,7 @@ func TestBuildTemplateInstallPreviewConflictsWhenStaleBundleAgentsBlockDrifted(t ctx := context.Background() sourceRepo := seedHarnessTemplateInstallSourceRepo(t) - sourceRepo.WriteFile(t, "AGENTS.md", "Workspace guide for $project_name\n") + sourceRepo.WriteFile(t, "AGENTS.md", "Workspace guide for {{ vars.project_name }}\n") sourceRepo.AddAndCommit(t, "add bundle agents") source, err := ResolveLocalTemplateInstallSource(ctx, sourceRepo.Root, "HEAD") require.NoError(t, err) diff --git a/cmd/orbit/cli/harness/template_install_source_test.go b/cmd/orbit/cli/harness/template_install_source_test.go index 2eef2ef..2562b04 100644 --- a/cmd/orbit/cli/harness/template_install_source_test.go +++ b/cmd/orbit/cli/harness/template_install_source_test.go @@ -53,8 +53,8 @@ func seedHarnessTemplateInstallSourceRepo(t *testing.T) *testutil.Repo { "include:\n"+ " - docs/**\n"+ " - schema/**\n") - repo.WriteFile(t, "docs/guide.md", "$project_name guide\n") - repo.WriteFile(t, "schema/example.schema.json", "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"workspace/example.schema.json\",\n \"title\": \"$project_name\"\n}\n") + repo.WriteFile(t, "docs/guide.md", "{{ vars.project_name }} guide\n") + repo.WriteFile(t, "schema/example.schema.json", "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"workspace/example.schema.json\",\n \"title\": \"{{ vars.project_name }}\"\n}\n") repo.AddAndCommit(t, "seed harness template source") return repo @@ -112,8 +112,8 @@ func TestResolveLocalTemplateInstallSourceLoadsMemberSnapshotsAndSkipsControlFil " - docs/guide.md\n"+ " - schema/example.schema.json\n"+ " file_digests:\n"+ - " docs/guide.md: "+contentDigest([]byte("$project_name guide\n"))+"\n"+ - " schema/example.schema.json: "+contentDigest([]byte("{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"workspace/example.schema.json\",\n \"title\": \"$project_name\"\n}\n"))+"\n"+ + " docs/guide.md: "+contentDigest([]byte("{{ vars.project_name }} guide\n"))+"\n"+ + " schema/example.schema.json: "+contentDigest([]byte("{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"workspace/example.schema.json\",\n \"title\": \"{{ vars.project_name }}\"\n}\n"))+"\n"+ " variables:\n"+ " project_name:\n"+ " required: true\n") @@ -201,7 +201,7 @@ func TestResolveLocalTemplateInstallSourceRejectsHostedCapabilitySkillRootMissin " include:\n"+ " - docs/**\n"+ " - schema/**\n") - repo.WriteFile(t, "orbit/skills/docs-style/checklist.md", "Use $project_name style guide.\n") + repo.WriteFile(t, "orbit/skills/docs-style/checklist.md", "Use {{ vars.project_name }} style guide.\n") repo.AddAndCommit(t, "add incomplete skill capability to harness template source") _, err := ResolveLocalTemplateInstallSource(context.Background(), repo.Root, "HEAD") @@ -264,7 +264,7 @@ func TestResolveLocalTemplateInstallSourceRejectsPartialMemberSnapshotSet(t *tes " exported_paths:\n"+ " - docs/guide.md\n"+ " file_digests:\n"+ - " docs/guide.md: "+contentDigest([]byte("$project_name guide\n"))+"\n"+ + " docs/guide.md: "+contentDigest([]byte("{{ vars.project_name }} guide\n"))+"\n"+ " variables:\n"+ " project_name:\n"+ " required: true\n") @@ -290,7 +290,7 @@ func TestResolveLocalTemplateInstallSourceRejectsSnapshotVariableSummaryDrift(t " exported_paths:\n"+ " - docs/guide.md\n"+ " file_digests:\n"+ - " docs/guide.md: "+contentDigest([]byte("$project_name guide\n"))+"\n"+ + " docs/guide.md: "+contentDigest([]byte("{{ vars.project_name }} guide\n"))+"\n"+ " variables:\n"+ " wrong_name:\n"+ " required: true\n") diff --git a/cmd/orbit/cli/harness/template_member_ownership_test.go b/cmd/orbit/cli/harness/template_member_ownership_test.go index ed5b71c..63d096b 100644 --- a/cmd/orbit/cli/harness/template_member_ownership_test.go +++ b/cmd/orbit/cli/harness/template_member_ownership_test.go @@ -310,7 +310,7 @@ func TestAnalyzeTemplateMemberOwnershipRejectsSnapshotVariableManifestDrift(t *t Snapshot: TemplateMemberSnapshotData{ ExportedPaths: []string{"docs/guide.md"}, FileDigests: map[string]string{ - "docs/guide.md": contentDigest([]byte("$project_name guide\n")), + "docs/guide.md": contentDigest([]byte("{{ vars.project_name }} guide\n")), }, Variables: map[string]TemplateVariableSpec{ "wrong_name": {Required: true}, @@ -319,7 +319,7 @@ func TestAnalyzeTemplateMemberOwnershipRejectsSnapshotVariableManifestDrift(t *t }, }, Files: []orbittemplate.CandidateFile{ - {Path: "docs/guide.md", Content: []byte("$project_name guide\n")}, + {Path: "docs/guide.md", Content: []byte("{{ vars.project_name }} guide\n")}, }, } diff --git a/cmd/orbit/cli/harness/template_merge_test.go b/cmd/orbit/cli/harness/template_merge_test.go index 62c40e8..52da75a 100644 --- a/cmd/orbit/cli/harness/template_merge_test.go +++ b/cmd/orbit/cli/harness/template_merge_test.go @@ -28,7 +28,7 @@ func TestMergeTemplateMemberCandidatesMergesFilesAndVariables(t *testing.T) { }, { Path: "docs/guide.md", - Content: []byte("$project_name guide\n"), + Content: []byte("{{ vars.project_name }} guide\n"), Mode: gitpkg.FileModeRegular, }, }, diff --git a/cmd/orbit/cli/harness/template_remove_test.go b/cmd/orbit/cli/harness/template_remove_test.go index 29802cd..6bca05a 100644 --- a/cmd/orbit/cli/harness/template_remove_test.go +++ b/cmd/orbit/cli/harness/template_remove_test.go @@ -207,7 +207,7 @@ func seedHarnessTemplateRemoveRepo(t *testing.T, withAgentsBlocks bool) *testuti " - docs/guide.md\n"+ " - shared/checklist.md\n"+ " file_digests:\n"+ - " docs/guide.md: "+contentDigest([]byte("Docs $project_name guide\n"))+"\n"+ + " docs/guide.md: "+contentDigest([]byte("Docs {{ vars.project_name }} guide\n"))+"\n"+ " shared/checklist.md: "+contentDigest([]byte("Shared $shared_name checklist\n"))+"\n"+ " variables:\n"+ " project_name:\n"+ @@ -230,7 +230,7 @@ func seedHarnessTemplateRemoveRepo(t *testing.T, withAgentsBlocks bool) *testuti " shared_name:\n"+ " description: Shared name\n"+ " required: true\n") - repo.WriteFile(t, "docs/guide.md", "Docs $project_name guide\n") + repo.WriteFile(t, "docs/guide.md", "Docs {{ vars.project_name }} guide\n") repo.WriteFile(t, "shared/checklist.md", "Shared $shared_name checklist\n") if withAgentsBlocks { @@ -303,12 +303,12 @@ func seedSingleMemberHarnessTemplateRemoveRepo(t *testing.T) *testutil.Repo { " exported_paths:\n"+ " - docs/guide.md\n"+ " file_digests:\n"+ - " docs/guide.md: "+contentDigest([]byte("Docs $project_name guide\n"))+"\n"+ + " docs/guide.md: "+contentDigest([]byte("Docs {{ vars.project_name }} guide\n"))+"\n"+ " variables:\n"+ " project_name:\n"+ " description: Project name\n"+ " required: true\n") - repo.WriteFile(t, "docs/guide.md", "Docs $project_name guide\n") + repo.WriteFile(t, "docs/guide.md", "Docs {{ vars.project_name }} guide\n") repo.AddAndCommit(t, "seed single member harness template remove repo") return repo diff --git a/cmd/orbit/cli/harness/template_save_test.go b/cmd/orbit/cli/harness/template_save_test.go index 9da37b0..e621f53 100644 --- a/cmd/orbit/cli/harness/template_save_test.go +++ b/cmd/orbit/cli/harness/template_save_test.go @@ -44,7 +44,7 @@ func TestBuildTemplateSavePreviewIncludesMemberSnapshotFiles(t *testing.T) { Snapshot: TemplateMemberSnapshotData{ ExportedPaths: []string{"docs/guide.md"}, FileDigests: map[string]string{ - "docs/guide.md": contentDigest([]byte("$project_name guide\n")), + "docs/guide.md": contentDigest([]byte("{{ vars.project_name }} guide\n")), }, Variables: map[string]TemplateVariableSpec{ "project_name": { @@ -75,7 +75,7 @@ func TestBuildTemplateSavePreviewIncludesRootHumansGuidance(t *testing.T) { require.Contains(t, preview.FilePaths(), "HUMANS.md") require.Equal(t, RootGuidanceMetadata{Humans: true}, preview.Manifest.Template.RootGuidance) humansFile := requireTemplateSaveFile(t, preview.Files, "HUMANS.md") - require.Equal(t, "Workspace notes for $project_name\n\nHelp humans operate $project_name\n", string(humansFile.Content)) + require.Equal(t, "Workspace notes for {{ vars.project_name }}\n\nHelp humans operate {{ vars.project_name }}\n", string(humansFile.Content)) } func TestBuildTemplateSavePreviewIncludesPendingRootBootstrapGuidance(t *testing.T) { @@ -116,7 +116,7 @@ func TestBuildTemplateSavePreviewIncludesPendingRootBootstrapGuidance(t *testing require.Contains(t, preview.FilePaths(), "BOOTSTRAP.md") require.Equal(t, RootGuidanceMetadata{Bootstrap: true}, preview.Manifest.Template.RootGuidance) bootstrapFile := requireTemplateSaveFile(t, preview.Files, "BOOTSTRAP.md") - require.Equal(t, "Workspace bootstrap for $project_name\n\nBootstrap $project_name for humans\n", string(bootstrapFile.Content)) + require.Equal(t, "Workspace bootstrap for {{ vars.project_name }}\n\nBootstrap {{ vars.project_name }} for humans\n", string(bootstrapFile.Content)) } func TestBuildTemplateSavePreviewSkipsCompletedRootBootstrapGuidance(t *testing.T) { @@ -216,7 +216,7 @@ func TestBuildTemplateSavePreviewIncludesCompletedRootBootstrapWhenRequested(t * require.True(t, preview.Manifest.Template.RootGuidance.Bootstrap) bootstrapFile := requireTemplateSaveFile(t, preview.Files, "BOOTSTRAP.md") - require.Equal(t, "Workspace bootstrap for $project_name\n\nBootstrap $project_name for humans\n", string(bootstrapFile.Content)) + require.Equal(t, "Workspace bootstrap for {{ vars.project_name }}\n\nBootstrap {{ vars.project_name }} for humans\n", string(bootstrapFile.Content)) } func TestBuildTemplateSavePreviewSnapshotTracksEditedFiles(t *testing.T) { diff --git a/cmd/orbit/cli/template/apply.go b/cmd/orbit/cli/template/apply.go index 806f591..bb353f0 100644 --- a/cmd/orbit/cli/template/apply.go +++ b/cmd/orbit/cli/template/apply.go @@ -615,12 +615,14 @@ func buildTemplateApplyPreviewFromSourceWithLocalInputs( renderedFiles, err := renderTemplateFilesWithOptions(source.Files, renderValues, renderTemplateOptions{ AllowUnresolved: allowUnresolvedBindings, + Declared: declared, }) if err != nil { return TemplateApplyPreview{}, fmt.Errorf("render template files: %w", err) } renderedSharedAgentsFile, hasSharedAgents, err := renderSharedAgentsPayloadWithOptions(source, renderValues, renderTemplateOptions{ AllowUnresolved: allowUnresolvedBindings, + Declared: declared, }) if err != nil { return TemplateApplyPreview{}, fmt.Errorf("render shared AGENTS payload: %w", err) diff --git a/cmd/orbit/cli/template/apply_test.go b/cmd/orbit/cli/template/apply_test.go index 7fba87a..92fd36c 100644 --- a/cmd/orbit/cli/template/apply_test.go +++ b/cmd/orbit/cli/template/apply_test.go @@ -47,7 +47,7 @@ func TestResolveLocalTemplateSourceLoadsManifestDefinitionAndUserFiles(t *testin require.Equal(t, []CandidateFile{ { Path: "docs/guide.md", - Content: []byte("$project_name guide\n"), + Content: []byte("{{ vars.project_name }} guide\n"), Mode: gitpkg.FileModeRegular, }, }, source.Files) @@ -75,7 +75,7 @@ func TestResolveLocalTemplateSourceLoadsVariablesFromBranchManifestWithoutLegacy "description: Docs orbit\n"+ "include:\n"+ " - docs/**\n") - repo.WriteFile(t, "docs/guide.md", "$project_name guide\n") + repo.WriteFile(t, "docs/guide.md", "{{ vars.project_name }} guide\n") repo.AddAndCommit(t, "seed branch-manifest-only template branch") source, err := ResolveLocalTemplateSource(context.Background(), repo.Root, "HEAD") @@ -89,7 +89,7 @@ func TestResolveLocalTemplateSourceLoadsVariablesFromBranchManifestWithoutLegacy require.Equal(t, []CandidateFile{ { Path: "docs/guide.md", - Content: []byte("$project_name guide\n"), + Content: []byte("{{ vars.project_name }} guide\n"), Mode: gitpkg.FileModeRegular, }, }, source.Files) @@ -106,7 +106,7 @@ func TestResolveLocalTemplateSourceLoadsAgentsTemplateFromCompanionSpec(t *testi "meta:\n"+ " file: .harness/orbits/docs.yaml\n"+ " agents_template: |\n"+ - " Project guidance for $project_name\n"+ + " Project guidance for {{ vars.project_name }}\n"+ " include_in_projection: true\n"+ " include_in_write: true\n"+ " include_in_export: true\n"+ @@ -117,7 +117,7 @@ func TestResolveLocalTemplateSourceLoadsAgentsTemplateFromCompanionSpec(t *testi " paths:\n"+ " include:\n"+ " - docs/**\n") - repo.WriteFile(t, "docs/guide.md", "$project_name guide\n") + repo.WriteFile(t, "docs/guide.md", "{{ vars.project_name }} guide\n") repo.AddAndCommit(t, "seed template branch with structured brief") source, err := ResolveLocalTemplateSource(context.Background(), repo.Root, "HEAD") @@ -125,12 +125,12 @@ func TestResolveLocalTemplateSourceLoadsAgentsTemplateFromCompanionSpec(t *testi require.Equal(t, []CandidateFile{ { Path: "docs/guide.md", - Content: []byte("$project_name guide\n"), + Content: []byte("{{ vars.project_name }} guide\n"), Mode: gitpkg.FileModeRegular, }, }, source.Files) require.NotNil(t, source.Spec.Meta) - require.Equal(t, "Project guidance for $project_name\n", source.Spec.Meta.AgentsTemplate) + require.Equal(t, "Project guidance for {{ vars.project_name }}\n", source.Spec.Meta.AgentsTemplate) } func TestResolveLocalTemplateSourceLoadsCapabilitiesAndCapabilityAssetsFromHostedCompanionSpec(t *testing.T) { @@ -144,9 +144,9 @@ func TestResolveLocalTemplateSourceLoadsCapabilitiesAndCapabilityAssetsFromHoste "meta:\n"+ " file: .harness/orbits/docs.yaml\n"+ " agents_template: |\n"+ - " Project guidance for $project_name\n"+ + " Project guidance for {{ vars.project_name }}\n"+ " humans_template: |\n"+ - " Run docs workflow for $project_name\n"+ + " Run docs workflow for {{ vars.project_name }}\n"+ " include_in_projection: true\n"+ " include_in_write: true\n"+ " include_in_export: true\n"+ @@ -167,15 +167,15 @@ func TestResolveLocalTemplateSourceLoadsCapabilitiesAndCapabilityAssetsFromHoste " paths:\n"+ " include:\n"+ " - docs/**\n") - repo.WriteFile(t, "docs/guide.md", "$project_name guide\n") - repo.WriteFile(t, "orbit/commands/review.md", "Review $project_name docs.\n") + repo.WriteFile(t, "docs/guide.md", "{{ vars.project_name }} guide\n") + repo.WriteFile(t, "orbit/commands/review.md", "Review {{ vars.project_name }} docs.\n") repo.WriteFile(t, "orbit/skills/docs-style/SKILL.md", ""+ "---\n"+ "name: docs-style\n"+ "description: Docs style references.\n"+ "---\n"+ "# Docs Style\n") - repo.WriteFile(t, "orbit/skills/docs-style/checklist.md", "Use $project_name style guide.\n") + repo.WriteFile(t, "orbit/skills/docs-style/checklist.md", "Use {{ vars.project_name }} style guide.\n") repo.AddAndCommit(t, "seed template branch with capabilities") source, err := ResolveLocalTemplateSource(context.Background(), repo.Root, "HEAD") @@ -194,12 +194,12 @@ func TestResolveLocalTemplateSourceLoadsCapabilitiesAndCapabilityAssetsFromHoste require.Equal(t, []CandidateFile{ { Path: "docs/guide.md", - Content: []byte("$project_name guide\n"), + Content: []byte("{{ vars.project_name }} guide\n"), Mode: gitpkg.FileModeRegular, }, { Path: "orbit/commands/review.md", - Content: []byte("Review $project_name docs.\n"), + Content: []byte("Review {{ vars.project_name }} docs.\n"), Mode: gitpkg.FileModeRegular, }, { @@ -209,7 +209,7 @@ func TestResolveLocalTemplateSourceLoadsCapabilitiesAndCapabilityAssetsFromHoste }, { Path: "orbit/skills/docs-style/checklist.md", - Content: []byte("Use $project_name style guide.\n"), + Content: []byte("Use {{ vars.project_name }} style guide.\n"), Mode: gitpkg.FileModeRegular, }, }, source.Files) @@ -241,8 +241,8 @@ func TestResolveLocalTemplateSourceRejectsHostedCapabilitySkillRootMissingSkillM " paths:\n"+ " include:\n"+ " - docs/**\n") - repo.WriteFile(t, "docs/guide.md", "$project_name guide\n") - repo.WriteFile(t, "orbit/skills/docs-style/checklist.md", "Use $project_name style guide.\n") + repo.WriteFile(t, "docs/guide.md", "{{ vars.project_name }} guide\n") + repo.WriteFile(t, "orbit/skills/docs-style/checklist.md", "Use {{ vars.project_name }} style guide.\n") repo.AddAndCommit(t, "seed template branch with incomplete skill capability") _, err := ResolveLocalTemplateSource(context.Background(), repo.Root, "HEAD") @@ -261,7 +261,7 @@ func TestResolveLocalTemplateSourcePrefersHostedCompanionOverLegacyCompanion(t * "meta:\n"+ " file: .harness/orbits/docs.yaml\n"+ " agents_template: |\n"+ - " Hosted guidance for $project_name\n"+ + " Hosted guidance for {{ vars.project_name }}\n"+ " include_in_projection: true\n"+ " include_in_write: true\n"+ " include_in_export: true\n"+ @@ -278,17 +278,17 @@ func TestResolveLocalTemplateSourcePrefersHostedCompanionOverLegacyCompanion(t * "meta:\n"+ " file: .orbit/orbits/docs.yaml\n"+ " agents_template: |\n"+ - " Legacy guidance for $project_name\n"+ + " Legacy guidance for {{ vars.project_name }}\n"+ "include:\n"+ " - docs/**\n") - repo.WriteFile(t, "docs/guide.md", "$project_name guide\n") + repo.WriteFile(t, "docs/guide.md", "{{ vars.project_name }} guide\n") repo.AddAndCommit(t, "seed dual-host template branch") source, err := ResolveLocalTemplateSource(context.Background(), repo.Root, "HEAD") require.NoError(t, err) require.NotNil(t, source.Spec.Meta) require.Equal(t, ".harness/orbits/docs.yaml", source.Spec.Meta.File) - require.Equal(t, "Hosted guidance for $project_name\n", source.Spec.Meta.AgentsTemplate) + require.Equal(t, "Hosted guidance for {{ vars.project_name }}\n", source.Spec.Meta.AgentsTemplate) } func TestResolveLocalTemplateSourceRejectsNonTemplateBranchManifest(t *testing.T) { @@ -354,7 +354,7 @@ func TestResolveLocalTemplateSourceIgnoresInvalidLegacyTemplateManifestWhenBranc "description: Docs orbit\n"+ "include:\n"+ " - docs/**\n") - repo.WriteFile(t, "docs/guide.md", "$project_name guide\n") + repo.WriteFile(t, "docs/guide.md", "{{ vars.project_name }} guide\n") repo.AddAndCommit(t, "seed template branch with invalid legacy manifest") source, err := ResolveLocalTemplateSource(context.Background(), repo.Root, "HEAD") @@ -367,7 +367,7 @@ func TestResolveLocalTemplateSourceIgnoresInvalidLegacyTemplateManifestWhenBranc require.Equal(t, []CandidateFile{ { Path: "docs/guide.md", - Content: []byte("$project_name guide\n"), + Content: []byte("{{ vars.project_name }} guide\n"), Mode: gitpkg.FileModeRegular, }, }, source.Files) @@ -410,7 +410,7 @@ func TestResolveLocalTemplateSourceIgnoresLegacySharedFilesManifestEntryWhenBran "id: docs\n"+ "include:\n"+ " - docs/**\n") - repo.WriteFile(t, "docs/guide.md", "$project_name guide\n") + repo.WriteFile(t, "docs/guide.md", "{{ vars.project_name }} guide\n") repo.AddAndCommit(t, "seed legacy shared files template") source, err := ResolveLocalTemplateSource(context.Background(), repo.Root, "HEAD") @@ -423,7 +423,7 @@ func TestResolveLocalTemplateSourceIgnoresLegacySharedFilesManifestEntryWhenBran require.Equal(t, []CandidateFile{ { Path: "docs/guide.md", - Content: []byte("$project_name guide\n"), + Content: []byte("{{ vars.project_name }} guide\n"), Mode: gitpkg.FileModeRegular, }, }, source.Files) @@ -432,18 +432,24 @@ func TestResolveLocalTemplateSourceIgnoresLegacySharedFilesManifestEntryWhenBran func TestRenderTemplateFilesRequiresBindingsForReferencedVariables(t *testing.T) { t.Parallel() - _, err := RenderTemplateFiles([]CandidateFile{ + _, err := renderTemplateFilesWithOptions([]CandidateFile{ { Path: "docs/guide.md", - Content: []byte("$project_name guide\n"), + Content: []byte("{{ vars.project_name }} guide\n"), + }, + }, map[string]string{}, renderTemplateOptions{ + Declared: map[string]bindings.VariableDeclaration{ + "project_name": { + Required: true, + }, }, - }, map[string]string{}) + }) require.Error(t, err) - require.ErrorContains(t, err, "missing binding") + require.ErrorContains(t, err, "unresolved Package Variable") require.ErrorContains(t, err, "project_name") } -func TestRenderTemplateFilesIgnoresNonMarkdownFiles(t *testing.T) { +func TestRenderTemplateFilesLeavesLegacyDollarReferencesInTextFiles(t *testing.T) { t.Parallel() rendered, err := RenderTemplateFiles([]CandidateFile{ @@ -466,7 +472,7 @@ func TestRenderTemplateFilesIgnoresNonMarkdownFiles(t *testing.T) { }, { Path: "docs/guide.md", - Content: []byte("Orbit guide\n"), + Content: []byte("$project_name guide\n"), }, }, rendered) } @@ -1108,7 +1114,7 @@ func TestBuildTemplateApplyPreviewAllowsUnresolvedBindingsWhenRequested(t *testi require.Equal(t, []CandidateFile{ { Path: "docs/guide.md", - Content: []byte("$project_name guide\n"), + Content: []byte("{{ vars.project_name }} guide\n"), Mode: gitpkg.FileModeRegular, }, }, preview.RenderedFiles) @@ -1372,7 +1378,7 @@ func TestReplayInstalledTemplateUsesRecordedTemplateCommitInsteadOfCurrentBranch runtimeBranch := strings.TrimSpace(repo.Run(t, "branch", "--show-current")) repo.Run(t, "checkout", sourceRef) - repo.WriteFile(t, "docs/reference.md", "$project_name reference\n") + repo.WriteFile(t, "docs/reference.md", "{{ vars.project_name }} reference\n") repo.Run(t, "rm", "-f", "docs/guide.md") repo.AddAndCommit(t, "update template branch contents") repo.Run(t, "checkout", runtimeBranch) @@ -1445,7 +1451,7 @@ func TestReplayInstalledRemoteTemplateUsesRecordedTemplateCommitAfterRemoteForce "description: Rewritten docs orbit\n"+ "include:\n"+ " - docs/**\n") - sourceRepo.WriteFile(t, "docs/reference.md", "$project_name rewritten reference\n") + sourceRepo.WriteFile(t, "docs/reference.md", "{{ vars.project_name }} rewritten reference\n") sourceRepo.AddAndCommit(t, "rewrite remote template branch history") sourceRepo.Run(t, "push", "--force", remoteURL, "rewritten-template:"+sourceRef) @@ -1594,7 +1600,7 @@ func TestBuildInstallOwnedCleanupPlanFailsClosedWithoutVariablesSnapshot(t *test runtimeBranch := strings.TrimSpace(repo.Run(t, "branch", "--show-current")) repo.Run(t, "checkout", sourceRef) - repo.WriteFile(t, "docs/reference.md", "$project_name reference\n") + repo.WriteFile(t, "docs/reference.md", "{{ vars.project_name }} reference\n") repo.Run(t, "rm", "-f", "docs/guide.md") repo.AddAndCommit(t, "update template branch contents") repo.Run(t, "checkout", runtimeBranch) @@ -1955,7 +1961,7 @@ func seedLocalTemplateApplyRepoWithSharedAgents(t *testing.T) (*testutil.Repo, s "meta:\n"+ " file: .harness/orbits/docs.yaml\n"+ " agents_template: |\n"+ - " Docs orbit for $project_name\n"+ + " Docs orbit for {{ vars.project_name }}\n"+ " include_in_projection: true\n"+ " include_in_write: true\n"+ " include_in_export: true\n"+ @@ -1966,7 +1972,7 @@ func seedLocalTemplateApplyRepoWithSharedAgents(t *testing.T) (*testutil.Repo, s " paths:\n"+ " include:\n"+ " - docs/**\n") - repo.WriteFile(t, "docs/guide.md", "$project_name guide\n") + repo.WriteFile(t, "docs/guide.md", "{{ vars.project_name }} guide\n") repo.AddAndCommit(t, "seed template branch with structured brief") repo.Run(t, "checkout", currentBranch) diff --git a/cmd/orbit/cli/template/brief_backfill.go b/cmd/orbit/cli/template/brief_backfill.go index a365387..0ca5751 100644 --- a/cmd/orbit/cli/template/brief_backfill.go +++ b/cmd/orbit/cli/template/brief_backfill.go @@ -119,7 +119,7 @@ func ReverseVariableizeBrief(content []byte, variables map[string]bindings.Varia continue } - text = strings.ReplaceAll(text, entry.Literal, "$"+entry.Variable) + text = strings.ReplaceAll(text, entry.Literal, packageTemplateReferenceLiteral(entry.Variable)) summaries = append(summaries, ReplacementSummary{ Variable: entry.Variable, Literal: entry.Literal, diff --git a/cmd/orbit/cli/template/brief_backfill_test.go b/cmd/orbit/cli/template/brief_backfill_test.go index 6eb0aad..0d9439f 100644 --- a/cmd/orbit/cli/template/brief_backfill_test.go +++ b/cmd/orbit/cli/template/brief_backfill_test.go @@ -24,7 +24,7 @@ func TestReverseVariableizeBriefReplacesUniqueRuntimeValues(t *testing.T) { }, }) require.NoError(t, err) - require.Equal(t, "Welcome to $project_name.\nOpen $docs_url.\n", string(result.Content)) + require.Equal(t, "Welcome to {{ vars.project_name }}.\nOpen {{ vars.docs_url }}.\n", string(result.Content)) require.ElementsMatch(t, []ReplacementSummary{ { Variable: "docs_url", @@ -124,7 +124,7 @@ func TestBackfillOrbitBriefPreservesHostedDefinitionComments(t *testing.T) { require.Contains(t, string(data), "# keep meta comment") require.Contains(t, string(data), "# keep member comment") require.Contains(t, string(data), "agents_template: |") - require.Contains(t, string(data), "Docs orbit for $project_name\n") + require.Contains(t, string(data), "Docs orbit for {{ vars.project_name }}\n") } func TestBackfillOrbitBriefReturnsSkippedWhenHostedTemplateAlreadyMatches(t *testing.T) { @@ -148,7 +148,7 @@ func TestBackfillOrbitBriefReturnsSkippedWhenHostedTemplateAlreadyMatches(t *tes "meta:\n"+ " file: .harness/orbits/docs.yaml\n"+ " agents_template: |\n"+ - " Docs orbit for $project_name\n"+ + " Docs orbit for {{ vars.project_name }}\n"+ " include_in_projection: true\n"+ " include_in_write: true\n"+ " include_in_export: true\n"+ @@ -173,7 +173,7 @@ func TestBackfillOrbitBriefReturnsSkippedWhenHostedTemplateAlreadyMatches(t *tes data, err := os.ReadFile(filepath.Join(repo.Root, ".harness", "orbits", "docs.yaml")) require.NoError(t, err) require.Contains(t, string(data), "agents_template: |") - require.Contains(t, string(data), "Docs orbit for $project_name\n") + require.Contains(t, string(data), "Docs orbit for {{ vars.project_name }}\n") } func TestBackfillOrbitBriefSkipsFormatterPaddingAroundMarkers(t *testing.T) { @@ -197,7 +197,7 @@ func TestBackfillOrbitBriefSkipsFormatterPaddingAroundMarkers(t *testing.T) { "meta:\n"+ " file: .harness/orbits/docs.yaml\n"+ " agents_template: |\n"+ - " Docs orbit for $project_name\n"+ + " Docs orbit for {{ vars.project_name }}\n"+ " include_in_projection: true\n"+ " include_in_write: true\n"+ " include_in_export: true\n"+ @@ -224,7 +224,7 @@ func TestBackfillOrbitBriefSkipsFormatterPaddingAroundMarkers(t *testing.T) { specData, err := os.ReadFile(filepath.Join(repo.Root, ".harness", "orbits", "docs.yaml")) require.NoError(t, err) - require.Contains(t, string(specData), " Docs orbit for $project_name\n") + require.Contains(t, string(specData), " Docs orbit for {{ vars.project_name }}\n") require.NotContains(t, string(specData), "agents_template: |+\n\n") } diff --git a/cmd/orbit/cli/template/content_builder_test.go b/cmd/orbit/cli/template/content_builder_test.go index 156b96e..fcf4919 100644 --- a/cmd/orbit/cli/template/content_builder_test.go +++ b/cmd/orbit/cli/template/content_builder_test.go @@ -71,7 +71,7 @@ func TestBuildTemplateContentFiltersForbiddenPathsAndAddsCompanionDefinition(t * }, { Path: "docs/guide.md", - Content: []byte("$project_name guide\n"), + Content: []byte("{{ vars.project_name }} guide\n"), Mode: gitpkg.FileModeRegular, }, }, result.Files) @@ -128,7 +128,7 @@ func TestBuildTemplateContentReadsHiddenTrackedFilesFromHEAD(t *testing.T) { }, { Path: "docs/hidden.md", - Content: []byte("$project_name hidden\n"), + Content: []byte("{{ vars.project_name }} hidden\n"), Mode: gitpkg.FileModeRegular, }, }, result.Files) diff --git a/cmd/orbit/cli/template/install_reapply.go b/cmd/orbit/cli/template/install_reapply.go index 0852e74..d67d9c4 100644 --- a/cmd/orbit/cli/template/install_reapply.go +++ b/cmd/orbit/cli/template/install_reapply.go @@ -215,6 +215,7 @@ func buildInstalledTemplateBindingsPreviewFromRecord( renderedFiles, err := renderTemplateFilesWithOptions(source.Files, renderValues, renderTemplateOptions{ AllowUnresolved: true, + Declared: declared, }) if err != nil { return TemplateApplyPreview{}, fmt.Errorf("render template files: %w", err) @@ -222,6 +223,7 @@ func buildInstalledTemplateBindingsPreviewFromRecord( renderedSharedAgentsFile, hasSharedAgents, err := renderSharedAgentsPayloadWithOptions(source, renderValues, renderTemplateOptions{ AllowUnresolved: true, + Declared: declared, }) if err != nil { return TemplateApplyPreview{}, fmt.Errorf("render shared AGENTS payload: %w", err) diff --git a/cmd/orbit/cli/template/install_replay.go b/cmd/orbit/cli/template/install_replay.go index 820d89f..5a24515 100644 --- a/cmd/orbit/cli/template/install_replay.go +++ b/cmd/orbit/cli/template/install_replay.go @@ -101,12 +101,14 @@ func replayInstalledTemplateFromSnapshot(source LocalTemplateSource, record Inst allowUnresolved := len(record.Variables.UnresolvedAtApply) > 0 renderedFiles, err := renderTemplateFilesWithOptions(source.Files, renderValues, renderTemplateOptions{ AllowUnresolved: allowUnresolved, + Declared: record.Variables.Declarations, }) if err != nil { return TemplateApplyPreview{}, fmt.Errorf("render replayed template files: %w", err) } renderedSharedAgentsFile, hasSharedAgents, err := renderSharedAgentsPayloadWithOptions(source, renderValues, renderTemplateOptions{ AllowUnresolved: allowUnresolved, + Declared: record.Variables.Declarations, }) if err != nil { return TemplateApplyPreview{}, fmt.Errorf("render replayed shared AGENTS payload: %w", err) diff --git a/cmd/orbit/cli/template/remote_snapshot_test.go b/cmd/orbit/cli/template/remote_snapshot_test.go index b01d37e..a49b158 100644 --- a/cmd/orbit/cli/template/remote_snapshot_test.go +++ b/cmd/orbit/cli/template/remote_snapshot_test.go @@ -30,7 +30,7 @@ func TestResolveRemoteTemplateCandidateSnapshotLoadsManifestDefinitionAndUserFil require.Equal(t, []CandidateFile{ { Path: "docs/guide.md", - Content: []byte("$project_name guide\n"), + Content: []byte("{{ vars.project_name }} guide\n"), Mode: gitpkg.FileModeRegular, }, }, source.Files) diff --git a/cmd/orbit/cli/template/render.go b/cmd/orbit/cli/template/render.go index 31eae8e..1ee1fdf 100644 --- a/cmd/orbit/cli/template/render.go +++ b/cmd/orbit/cli/template/render.go @@ -2,34 +2,66 @@ package orbittemplate import ( "fmt" + "regexp" "sort" "strings" + + "github.com/zack-nova/harnessyard/cmd/orbit/cli/bindings" ) type renderTemplateOptions struct { AllowUnresolved bool + Declared map[string]bindings.VariableDeclaration } -// RenderTemplateFiles renders template variables in Markdown files using the -// resolved bindings. Non-Markdown files are passed through unchanged. +var packageTemplateReferenceIdentPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) + +// RenderTemplateFiles renders Package Template References in text files using +// the resolved bindings. Binary and invalid UTF-8 files are passed through unchanged. func RenderTemplateFiles(files []CandidateFile, bindings map[string]string) ([]CandidateFile, error) { return renderTemplateFilesWithOptions(files, bindings, renderTemplateOptions{}) } -// RenderTemplateFilesAllowingUnresolved renders known template variables while -// preserving unresolved references in-place. +// RenderTemplateFilesWithDeclarations renders Package Template References in text +// files while validating references against the template variable declarations. +func RenderTemplateFilesWithDeclarations( + files []CandidateFile, + bindings map[string]string, + declared map[string]bindings.VariableDeclaration, +) ([]CandidateFile, error) { + return renderTemplateFilesWithOptions(files, bindings, renderTemplateOptions{ + Declared: declared, + }) +} + +// RenderTemplateFilesAllowingUnresolved renders known Package Template References +// while preserving unresolved declared references in-place. func RenderTemplateFilesAllowingUnresolved(files []CandidateFile, bindings map[string]string) ([]CandidateFile, error) { return renderTemplateFilesWithOptions(files, bindings, renderTemplateOptions{ AllowUnresolved: true, }) } +// RenderTemplateFilesWithDeclarationsAllowingUnresolved renders known Package +// Template References and preserves unresolved declared references in-place. +func RenderTemplateFilesWithDeclarationsAllowingUnresolved( + files []CandidateFile, + bindings map[string]string, + declared map[string]bindings.VariableDeclaration, +) ([]CandidateFile, error) { + return renderTemplateFilesWithOptions(files, bindings, renderTemplateOptions{ + AllowUnresolved: true, + Declared: declared, + }) +} + func renderTemplateFilesWithOptions(files []CandidateFile, bindings map[string]string, options renderTemplateOptions) ([]CandidateFile, error) { rendered := make([]CandidateFile, 0, len(files)) missingSet := make(map[string]struct{}) + declaredSet := renderDeclaredSet(bindings, options) for _, file := range files { - if !isMarkdownTemplateFile(file.Path) || isBinaryOrInvalidText(file.Content) { + if isBinaryOrInvalidText(file.Content) { rendered = append(rendered, CandidateFile{ Path: file.Path, Content: append([]byte(nil), file.Content...), @@ -38,16 +70,10 @@ func renderTemplateFilesWithOptions(files []CandidateFile, bindings map[string]s continue } - content := variableReferencePattern.ReplaceAllStringFunc(string(file.Content), func(match string) string { - name := match[1:] - value, ok := bindings[name] - if !ok { - missingSet[name] = struct{}{} - return match - } - - return value - }) + content, err := renderTemplateText(file.Path, string(file.Content), bindings, declaredSet, options, missingSet) + if err != nil { + return nil, err + } rendered = append(rendered, CandidateFile{ Path: file.Path, @@ -67,3 +93,108 @@ func renderTemplateFilesWithOptions(files []CandidateFile, bindings map[string]s return rendered, nil } + +func renderDeclaredSet(values map[string]string, options renderTemplateOptions) map[string]struct{} { + if options.Declared != nil { + set := make(map[string]struct{}, len(options.Declared)) + for name := range options.Declared { + set[name] = struct{}{} + } + return set + } + if options.AllowUnresolved { + return nil + } + + set := make(map[string]struct{}, len(values)) + for name := range values { + set[name] = struct{}{} + } + return set +} + +func renderTemplateText( + path string, + text string, + values map[string]string, + declared map[string]struct{}, + options renderTemplateOptions, + missingSet map[string]struct{}, +) (string, error) { + var output strings.Builder + cursor := 0 + for { + startOffset := strings.Index(text[cursor:], "{{") + if startOffset < 0 { + output.WriteString(text[cursor:]) + return output.String(), nil + } + start := cursor + startOffset + if start > 0 && text[start-1] == '$' { + output.WriteString(text[cursor : start+2]) + cursor = start + 2 + continue + } + + endOffset := strings.Index(text[start+2:], "}}") + if endOffset < 0 { + return "", malformedPackageTemplateReferenceError(path, text[start:]) + } + end := start + 2 + endOffset + raw := text[start : end+2] + ref, err := parsePackageTemplateReference(strings.TrimSpace(text[start+2 : end])) + if err != nil { + return "", fmt.Errorf("%s: %w", path, err) + } + + output.WriteString(text[cursor:start]) + if ref.Namespace != "vars" { + return "", fmt.Errorf("%s: unsupported Package Template Reference namespace %q in %s", path, ref.Namespace, raw) + } + if declared != nil { + if _, ok := declared[ref.Name]; !ok { + return "", fmt.Errorf("%s: unknown Package Variable %q in %s", path, ref.Name, raw) + } + } + value, ok := values[ref.Name] + if !ok { + missingSet[ref.Name] = struct{}{} + if options.AllowUnresolved { + output.WriteString(raw) + cursor = end + 2 + continue + } + return "", fmt.Errorf("%s: unresolved Package Variable %q in %s", path, ref.Name, raw) + } + + output.WriteString(value) + cursor = end + 2 + } +} + +type packageTemplateReference struct { + Namespace string + Name string +} + +func parsePackageTemplateReference(expr string) (packageTemplateReference, error) { + parts := strings.Split(expr, ".") + if len(parts) != 2 || + !packageTemplateReferenceIdentPattern.MatchString(parts[0]) || + !packageTemplateReferenceIdentPattern.MatchString(parts[1]) { + return packageTemplateReference{}, fmt.Errorf("malformed Package Template Reference %q", "{{ "+expr+" }}") + } + + return packageTemplateReference{ + Namespace: parts[0], + Name: parts[1], + }, nil +} + +func malformedPackageTemplateReferenceError(path string, raw string) error { + snippet := strings.TrimSpace(raw) + if len(snippet) > 80 { + snippet = snippet[:80] + "..." + } + return fmt.Errorf("%s: malformed Package Template Reference %q", path, snippet) +} diff --git a/cmd/orbit/cli/template/render_test.go b/cmd/orbit/cli/template/render_test.go new file mode 100644 index 0000000..63ad355 --- /dev/null +++ b/cmd/orbit/cli/template/render_test.go @@ -0,0 +1,126 @@ +package orbittemplate + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/zack-nova/harnessyard/cmd/orbit/cli/bindings" +) + +func TestRenderTemplateFilesRendersStrictVarsReferencesInTextFiles(t *testing.T) { + t.Parallel() + + rendered, err := renderTemplateFilesWithOptions([]CandidateFile{ + { + Path: "config/app.txt", + Content: []byte("Project={{ vars.project_name }}\n"), + }, + }, map[string]string{ + "project_name": "Harness Yard", + }, renderTemplateOptions{ + Declared: map[string]bindings.VariableDeclaration{ + "project_name": { + Required: true, + }, + }, + }) + require.NoError(t, err) + require.Equal(t, []byte("Project=Harness Yard\n"), rendered[0].Content) +} + +func TestRenderTemplateFilesRejectsUnsupportedNamespace(t *testing.T) { + t.Parallel() + + _, err := renderTemplateFilesWithOptions([]CandidateFile{ + { + Path: "docs/guide.md", + Content: []byte("Token {{ secrets.GITHUB_TOKEN }}\n"), + }, + }, map[string]string{}, renderTemplateOptions{}) + require.Error(t, err) + require.ErrorContains(t, err, "docs/guide.md") + require.ErrorContains(t, err, `unsupported Package Template Reference namespace "secrets"`) +} + +func TestRenderTemplateFilesRejectsUnknownVarsReference(t *testing.T) { + t.Parallel() + + _, err := renderTemplateFilesWithOptions([]CandidateFile{ + { + Path: "docs/guide.md", + Content: []byte("Project {{ vars.missing }}\n"), + }, + }, map[string]string{}, renderTemplateOptions{ + Declared: map[string]bindings.VariableDeclaration{ + "project_name": { + Required: true, + }, + }, + }) + require.Error(t, err) + require.ErrorContains(t, err, "docs/guide.md") + require.ErrorContains(t, err, `unknown Package Variable "missing"`) +} + +func TestRenderTemplateFilesRejectsUnresolvedDeclaredReference(t *testing.T) { + t.Parallel() + + _, err := renderTemplateFilesWithOptions([]CandidateFile{ + { + Path: "docs/guide.md", + Content: []byte("Project {{ vars.project_name }}\n"), + }, + }, map[string]string{}, renderTemplateOptions{ + Declared: map[string]bindings.VariableDeclaration{ + "project_name": { + Required: true, + }, + }, + }) + require.Error(t, err) + require.ErrorContains(t, err, "docs/guide.md") + require.ErrorContains(t, err, `unresolved Package Variable "project_name"`) +} + +func TestRenderTemplateFilesRejectsMalformedReferenceWithPath(t *testing.T) { + t.Parallel() + + _, err := RenderTemplateFiles([]CandidateFile{ + { + Path: "docs/guide.md", + Content: []byte("Project {{ vars. }}\n"), + }, + }, map[string]string{}) + require.Error(t, err) + require.ErrorContains(t, err, "docs/guide.md") + require.ErrorContains(t, err, "malformed Package Template Reference") +} + +func TestRenderTemplateFilesPreservesGitHubActionsExpressions(t *testing.T) { + t.Parallel() + + rendered, err := RenderTemplateFiles([]CandidateFile{ + { + Path: ".github/workflows/ci.yml", + Content: []byte("token: ${{ secrets.GITHUB_TOKEN }}\n"), + }, + }, map[string]string{}) + require.NoError(t, err) + require.Equal(t, []byte("token: ${{ secrets.GITHUB_TOKEN }}\n"), rendered[0].Content) +} + +func TestRenderTemplateFilesLeavesLegacyDollarReferencesUnchanged(t *testing.T) { + t.Parallel() + + rendered, err := RenderTemplateFiles([]CandidateFile{ + { + Path: "docs/guide.md", + Content: []byte("$project_name guide\n"), + }, + }, map[string]string{ + "project_name": "Harness Yard", + }) + require.NoError(t, err) + require.Equal(t, []byte("$project_name guide\n"), rendered[0].Content) +} diff --git a/cmd/orbit/cli/template/replacement.go b/cmd/orbit/cli/template/replacement.go index 6ae1985..935ef78 100644 --- a/cmd/orbit/cli/template/replacement.go +++ b/cmd/orbit/cli/template/replacement.go @@ -45,10 +45,6 @@ func ReplaceRuntimeValues(file CandidateFile, variables map[string]bindings.Vari return result, nil } - if !isMarkdownTemplateFile(file.Path) { - return result, nil - } - entries, ambiguities, err := buildReplacementPlan(variables) if err != nil { return ReplacementResult{}, err @@ -66,7 +62,7 @@ func ReplaceRuntimeValues(file CandidateFile, variables map[string]bindings.Vari continue } - content = strings.ReplaceAll(content, entry.Literal, "$"+entry.Variable) + content = strings.ReplaceAll(content, entry.Literal, packageTemplateReferenceLiteral(entry.Variable)) summaries = append(summaries, ReplacementSummary{ Variable: entry.Variable, Literal: entry.Literal, @@ -120,3 +116,7 @@ func buildReplacementPlan(variables map[string]bindings.VariableBinding) ([]repl return entries, ambiguities, nil } + +func packageTemplateReferenceLiteral(name string) string { + return "{{ vars." + name + " }}" +} diff --git a/cmd/orbit/cli/template/replacement_test.go b/cmd/orbit/cli/template/replacement_test.go index 9c1d0c9..c303cf1 100644 --- a/cmd/orbit/cli/template/replacement_test.go +++ b/cmd/orbit/cli/template/replacement_test.go @@ -35,7 +35,7 @@ func TestReplaceRuntimeValuesReplacesExactLiteralsAndTracksSummary(t *testing.T) require.False(t, result.SkippedBinary) require.Empty(t, result.Ambiguities) require.Equal(t, - "$project_name lives at $service_api.\nFallback: $service_url\n", + "{{ vars.project_name }} lives at {{ vars.service_api }}.\nFallback: {{ vars.service_url }}\n", string(result.Content), ) require.Equal(t, []ReplacementSummary{ @@ -125,13 +125,13 @@ func TestReplaceRuntimeValuesRejectsEmptyLiteral(t *testing.T) { require.ErrorContains(t, err, "must not be empty") } -func TestReplaceRuntimeValuesIgnoresNonMarkdownFiles(t *testing.T) { +func TestReplaceRuntimeValuesReplacesTextFilesWithoutMarkdownExtension(t *testing.T) { t.Parallel() result, err := ReplaceRuntimeValues( CandidateFile{ Path: "schema/example.schema.json", - Content: []byte("{\"title\":\"Orbit\",\"x-template\":\"$project_name\"}\n"), + Content: []byte("{\"title\":\"Orbit\",\"x-template\":\"{{ vars.project_name }}\"}\n"), }, map[string]bindings.VariableBinding{ "project_name": { @@ -141,8 +141,14 @@ func TestReplaceRuntimeValuesIgnoresNonMarkdownFiles(t *testing.T) { ) require.NoError(t, err) require.False(t, result.SkippedBinary) - require.Contains(t, string(result.Content), "\"title\":\"Orbit\"") - require.Contains(t, string(result.Content), "\"x-template\":\"$project_name\"") - require.Empty(t, result.Replacements) + require.Contains(t, string(result.Content), "\"title\":\"{{ vars.project_name }}\"") + require.Contains(t, string(result.Content), "\"x-template\":\"{{ vars.project_name }}\"") + require.Equal(t, []ReplacementSummary{ + { + Variable: "project_name", + Literal: "Orbit", + Count: 1, + }, + }, result.Replacements) require.Empty(t, result.Ambiguities) } diff --git a/cmd/orbit/cli/template/save_test.go b/cmd/orbit/cli/template/save_test.go index 9fc72a9..f95f2fe 100644 --- a/cmd/orbit/cli/template/save_test.go +++ b/cmd/orbit/cli/template/save_test.go @@ -196,7 +196,7 @@ func TestBuildTemplateSavePreviewIgnoresNonMarkdownVariableSyntax(t *testing.T) }, { Path: "docs/guide.md", - Content: []byte("$project_name guide\n"), + Content: []byte("{{ vars.project_name }} guide\n"), Mode: gitpkg.FileModeRegular, }, { diff --git a/cmd/orbit/cli/template/scan.go b/cmd/orbit/cli/template/scan.go index fe002dd..5086b19 100644 --- a/cmd/orbit/cli/template/scan.go +++ b/cmd/orbit/cli/template/scan.go @@ -25,9 +25,9 @@ type ScanResult struct { Unused []string } -// ScanVariables scans Markdown files for $var_name references and compares them -// with the declared manifest variable set. Non-Markdown, binary, or invalid UTF-8 -// files are skipped. +// ScanVariables scans text files for Package Template References plus legacy +// $var_name placeholders and compares them with the declared manifest variable set. +// Binary or invalid UTF-8 files are skipped. func ScanVariables(files []CandidateFile, declared map[string]VariableSpec) ScanResult { referencedSet := make(map[string]struct{}) declaredSet := make(map[string]struct{}, len(declared)) @@ -37,12 +37,18 @@ func ScanVariables(files []CandidateFile, declared map[string]VariableSpec) Scan } for _, file := range files { - if !isMarkdownTemplateFile(file.Path) || isBinaryOrInvalidText(file.Content) { + if isBinaryOrInvalidText(file.Content) { continue } - for _, match := range variableReferencePattern.FindAllString(string(file.Content), -1) { - referencedSet[match[1:]] = struct{}{} + content := string(file.Content) + if isMarkdownTemplateFile(file.Path) { + for _, match := range variableReferencePattern.FindAllString(content, -1) { + referencedSet[match[1:]] = struct{}{} + } + } + for _, name := range scanPackageTemplateReferenceNames(content) { + referencedSet[name] = struct{}{} } } @@ -68,6 +74,32 @@ func ScanVariables(files []CandidateFile, declared map[string]VariableSpec) Scan } } +func scanPackageTemplateReferenceNames(text string) []string { + names := make(map[string]struct{}) + cursor := 0 + for { + startOffset := strings.Index(text[cursor:], "{{") + if startOffset < 0 { + return sortedNames(names) + } + start := cursor + startOffset + if start > 0 && text[start-1] == '$' { + cursor = start + 2 + continue + } + endOffset := strings.Index(text[start+2:], "}}") + if endOffset < 0 { + return sortedNames(names) + } + end := start + 2 + endOffset + ref, err := parsePackageTemplateReference(strings.TrimSpace(text[start+2 : end])) + if err == nil && ref.Namespace == "vars" { + names[ref.Name] = struct{}{} + } + cursor = end + 2 + } +} + func isMarkdownTemplateFile(path string) bool { return strings.EqualFold(filepath.Ext(path), ".md") } diff --git a/cmd/orbit/cli/template/scan_test.go b/cmd/orbit/cli/template/scan_test.go index 909e6ee..1eaec70 100644 --- a/cmd/orbit/cli/template/scan_test.go +++ b/cmd/orbit/cli/template/scan_test.go @@ -153,3 +153,32 @@ func TestScanVariablesIgnoresNonMarkdownFiles(t *testing.T) { require.Empty(t, result.Undeclared) require.Empty(t, result.Unused) } + +func TestScanVariablesCollectsPackageTemplateReferencesAcrossTextFiles(t *testing.T) { + t.Parallel() + + result := ScanVariables( + []CandidateFile{ + { + Path: "config/app.txt", + Content: []byte("Project {{ vars.project_name }}\n"), + }, + { + Path: "docs/guide.md", + Content: []byte("Service {{ vars.service_url }}\nToken ${{ secrets.GITHUB_TOKEN }}\n"), + }, + }, + map[string]VariableSpec{ + "project_name": { + Required: true, + }, + "service_url": { + Required: true, + }, + }, + ) + + require.Equal(t, []string{"project_name", "service_url"}, result.Referenced) + require.Empty(t, result.Undeclared) + require.Empty(t, result.Unused) +} diff --git a/cmd/orbit/cli/template_apply_integration_test.go b/cmd/orbit/cli/template_apply_integration_test.go index 0e05655..7889a9f 100644 --- a/cmd/orbit/cli/template_apply_integration_test.go +++ b/cmd/orbit/cli/template_apply_integration_test.go @@ -230,7 +230,7 @@ func TestTemplateApplyDefaultsToUnresolvedBindings(t *testing.T) { guideData, err := os.ReadFile(filepath.Join(repo.Root, "docs", "guide.md")) require.NoError(t, err) - require.Equal(t, "$project_name guide\n", string(guideData)) + require.Equal(t, "{{ vars.project_name }} guide\n", string(guideData)) } func TestTemplateApplyStrictBindingsFailsOnMissingBindings(t *testing.T) { @@ -545,7 +545,7 @@ func seedTemplateApplyRepoWithSharedAgents(t *testing.T) *testutil.Repo { "meta:\n"+ " file: .orbit/orbits/docs.yaml\n"+ " agents_template: |\n"+ - " Docs orbit for $project_name\n"+ + " Docs orbit for {{ vars.project_name }}\n"+ " include_in_projection: true\n"+ " include_in_write: true\n"+ " include_in_export: true\n"+ @@ -556,7 +556,7 @@ func seedTemplateApplyRepoWithSharedAgents(t *testing.T) *testutil.Repo { " paths:\n"+ " include:\n"+ " - docs/**\n") - repo.WriteFile(t, "docs/guide.md", "$project_name guide\n") + repo.WriteFile(t, "docs/guide.md", "{{ vars.project_name }} guide\n") repo.AddAndCommit(t, "seed template branch with structured brief") repo.Run(t, "checkout", currentBranch) diff --git a/cmd/orbit/cli/template_apply_remote_integration_test.go b/cmd/orbit/cli/template_apply_remote_integration_test.go index 0debf1b..f62c7b4 100644 --- a/cmd/orbit/cli/template_apply_remote_integration_test.go +++ b/cmd/orbit/cli/template_apply_remote_integration_test.go @@ -237,7 +237,7 @@ func TestTemplateApplyRemoteAutoSelectsUniqueDefaultTemplate(t *testing.T) { apiData, err := os.ReadFile(filepath.Join(runtimeRepo.Root, "api", "openapi.yaml")) require.NoError(t, err) - require.Equal(t, "Orbit api\n", string(apiData)) + require.Equal(t, "Remote Default Orbit api\n", string(apiData)) installData, err := os.ReadFile(filepath.Join(runtimeRepo.Root, ".harness", "installs", "api.yaml")) require.NoError(t, err) diff --git a/cmd/orbit/cli/template_backfill_export_integration_test.go b/cmd/orbit/cli/template_backfill_export_integration_test.go index 0c94dbf..521d29d 100644 --- a/cmd/orbit/cli/template_backfill_export_integration_test.go +++ b/cmd/orbit/cli/template_backfill_export_integration_test.go @@ -51,11 +51,11 @@ func TestTemplateSaveBackfillsDriftedBriefBeforeSaving(t *testing.T) { definitionData, err := os.ReadFile(filepath.Join(repo.Root, ".harness", "orbits", "docs.yaml")) require.NoError(t, err) - require.Contains(t, string(definitionData), "Drifted docs orbit for $project_name") + require.Contains(t, string(definitionData), "Drifted docs orbit for {{ vars.project_name }}") publishedData, err := gitpkg.ReadFileAtRev(context.Background(), repo.Root, "orbit-template/docs", ".harness/orbits/docs.yaml") require.NoError(t, err) - require.Contains(t, string(publishedData), "Drifted docs orbit for $project_name") + require.Contains(t, string(publishedData), "Drifted docs orbit for {{ vars.project_name }}") } func TestTemplateSaveDryRunRejectsBackfillBriefMutation(t *testing.T) { @@ -69,8 +69,8 @@ func TestTemplateSaveDryRunRejectsBackfillBriefMutation(t *testing.T) { definitionData, readErr := os.ReadFile(filepath.Join(repo.Root, ".harness", "orbits", "docs.yaml")) require.NoError(t, readErr) - require.Contains(t, string(definitionData), "Docs orbit for $project_name") - require.NotContains(t, string(definitionData), "Drifted docs orbit for $project_name") + require.Contains(t, string(definitionData), "Docs orbit for {{ vars.project_name }}") + require.NotContains(t, string(definitionData), "Drifted docs orbit for {{ vars.project_name }}") exists, existsErr := gitpkg.LocalBranchExists(context.Background(), repo.Root, "orbit-template/docs") require.NoError(t, existsErr) @@ -129,7 +129,7 @@ func seedRuntimeRepoWithDriftedBrief(t *testing.T) *testutil.Repo { repo := testutil.NewRepo(t) repo.Run(t, "branch", "-m", "main") - writeHostedDocsOrbitWithStructuredBrief(t, repo.Root, "Docs orbit for $project_name\n") + writeHostedDocsOrbitWithStructuredBrief(t, repo.Root, "Docs orbit for {{ vars.project_name }}\n") repo.WriteFile(t, ".harness/manifest.yaml", ""+ "schema_version: 1\n"+ "kind: runtime\n"+ diff --git a/cmd/orbit/cli/template_save_integration_test.go b/cmd/orbit/cli/template_save_integration_test.go index 38792f9..c70bc43 100644 --- a/cmd/orbit/cli/template_save_integration_test.go +++ b/cmd/orbit/cli/template_save_integration_test.go @@ -74,7 +74,7 @@ func TestTemplateSaveCreatesTemplateBranchWithSharedAgentsPayload(t *testing.T) "meta:\n"+ " file: .harness/orbits/docs.yaml\n"+ " agents_template: |\n"+ - " Docs orbit for $project_name\n"+ + " Docs orbit for {{ vars.project_name }}\n"+ " include_in_projection: true\n"+ " include_in_write: true\n"+ " include_in_export: true\n"+ @@ -103,7 +103,7 @@ func TestTemplateSaveCreatesTemplateBranchWithSharedAgentsPayload(t *testing.T) require.NoError(t, err) require.Contains(t, string(agentsData), "file: .harness/orbits/docs.yaml") require.Contains(t, string(agentsData), "agents_template: |") - require.Contains(t, string(agentsData), "Docs orbit for $project_name") + require.Contains(t, string(agentsData), "Docs orbit for {{ vars.project_name }}") branchManifestData, err := gitpkg.ReadFileAtRev(context.Background(), repo.Root, "orbit-template/docs", ".harness/manifest.yaml") require.NoError(t, err) @@ -747,7 +747,7 @@ func TestTemplateSaveReadsHiddenTrackedFilesFromHEAD(t *testing.T) { hiddenData, err := gitpkg.ReadFileAtRev(context.Background(), repo.Root, "orbit-template/docs", "docs/hidden.md") require.NoError(t, err) - require.Equal(t, "$project_name hidden\n", string(hiddenData)) + require.Equal(t, "{{ vars.project_name }} hidden\n", string(hiddenData)) } func TestTemplateSaveEditTemplateWritesEditedTemplateWithoutMutatingRuntimeWorktree(t *testing.T) { @@ -764,7 +764,7 @@ func TestTemplateSaveEditTemplateWritesEditedTemplateWithoutMutatingRuntimeWorkt editorScript := filepath.Join(repo.Root, "edit-template.sh") require.NoError(t, os.WriteFile(editorScript, []byte(""+ "#!/bin/sh\n"+ - "printf '%s\\n' '$project_name guide at $service_url' > \"$1/docs/guide.md\"\n"), 0o755)) + "printf '%s\\n' '{{ vars.project_name }} guide at {{ vars.service_url }}' > \"$1/docs/guide.md\"\n"), 0o755)) t.Setenv("EDITOR", editorScript) _, _, err := executeCLI(t, repo.Root, "template", "save", "docs", "--to", "orbit-template/docs", "--edit-template") @@ -776,7 +776,7 @@ func TestTemplateSaveEditTemplateWritesEditedTemplateWithoutMutatingRuntimeWorkt templateData, err := gitpkg.ReadFileAtRev(context.Background(), repo.Root, "orbit-template/docs", "docs/guide.md") require.NoError(t, err) - require.Equal(t, "$project_name guide at $service_url\n", string(templateData)) + require.Equal(t, "{{ vars.project_name }} guide at {{ vars.service_url }}\n", string(templateData)) manifestData, err := gitpkg.ReadFileAtRev(context.Background(), repo.Root, "orbit-template/docs", ".harness/manifest.yaml") require.NoError(t, err) @@ -822,7 +822,7 @@ func TestTemplateSaveEditTemplateSupportsQuotedEditorCommandWithSpacedPath(t *te "if [ \"$1\" != \"--mode\" ] || [ \"$2\" != \"template edit\" ]; then\n"+ " exit 17\n"+ "fi\n"+ - "printf '%s\\n' '$project_name hardening guide' > \"$3/docs/guide.md\"\n"), 0o755)) + "printf '%s\\n' '{{ vars.project_name }} hardening guide' > \"$3/docs/guide.md\"\n"), 0o755)) t.Setenv("EDITOR", "\""+editorScript+"\" --mode \"template edit\"") _, _, err := executeCLI(t, repo.Root, "template", "save", "docs", "--to", "orbit-template/docs", "--edit-template") @@ -830,7 +830,7 @@ func TestTemplateSaveEditTemplateSupportsQuotedEditorCommandWithSpacedPath(t *te templateData, err := gitpkg.ReadFileAtRev(context.Background(), repo.Root, "orbit-template/docs", "docs/guide.md") require.NoError(t, err) - require.Equal(t, "$project_name hardening guide\n", string(templateData)) + require.Equal(t, "{{ vars.project_name }} hardening guide\n", string(templateData)) } func TestTemplateSaveFailsClosedOnOutOfRangeLocalSkillsWithoutFlags(t *testing.T) {