From 9f0605a33d1664b0477460a5463a2b19805f5e78 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 14:28:09 +0000 Subject: [PATCH] agent: remove the deterministic project-facts prompt section The ## Project block guessed build/test commands from marker files (bare tests/ dir => "pytest") and stated them as instructions, and a live run showed the model obeying a wrong guess into a misleading ImportError. Heuristic facts that are occasionally confidently wrong are worse than none: the model trusts prompt content over its own exploration. AGENTS.md (## Project Rules) remains the way to hand the agent per-project build/test truth. Removes PasClaw.Agent.ProjectFacts + its tests, the prompt injection, and the bench fixture/assertions that pinned the block. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LGQKz579j1ZnDRwmr6h1V6 --- Makefile | 8 +- bench/agentloop/agentloop_bench.pas | 22 -- src/pkg/agent/PasClaw.Agent.ProjectFacts.pas | 272 ------------------- src/pkg/agent/PasClaw.Agent.Prompt.pas | 22 -- src/tests/project_facts_tests.pas | 135 --------- 5 files changed, 1 insertion(+), 458 deletions(-) delete mode 100644 src/pkg/agent/PasClaw.Agent.ProjectFacts.pas delete mode 100644 src/tests/project_facts_tests.pas diff --git a/Makefile b/Makefile index 2009563..f7354e0 100644 --- a/Makefile +++ b/Makefile @@ -767,12 +767,6 @@ test-progress-ledger: | $(BUILDDIR) $(FPC) $(FPCFLAGS) src/tests/progress_ledger_tests.pas -o$(BUILDDIR)/progress_ledger_tests @$(BUILDDIR)/progress_ledger_tests -# Deterministic ## Project prompt section (stack/build/test/git from file reads). -test-project-facts: | $(BUILDDIR) - @mkdir -p $(BUILDDIR)/lib - $(FPC) $(FPCFLAGS) src/tests/project_facts_tests.pas -o$(BUILDDIR)/project_facts_tests - @$(BUILDDIR)/project_facts_tests - # Deterministic agent-loop harness bench (bench/agentloop) -- pure FPC, no # external runtime. Spawns the built binary, plays the scripted model over # the relay, asserts on what the loop did. Not part of `make test` (binds a @@ -852,4 +846,4 @@ test-fs-grep-tier5-6: | $(BUILDDIR) $(FPC) $(FPCFLAGS) src/tests/fs_grep_tier5_6_tests.pas -o$(BUILDDIR)/fs_grep_tier5_6_tests @$(BUILDDIR)/fs_grep_tier5_6_tests -test: smoke test-hashline test-toolview test-anthropic-server-tools test-openai-server-tools test-tool-choice test-responses-tool-choice test-println-helper test-utf8-codepage-tag test-gemini-schema-strip test-markdown-render test-json-utf8-roundtrip test-model-discovery test-cron-tool test-provider-catalog test-output-cache test-working-state test-ansi-width test-shell-filters test-learn test-export test-execute-code test-session-stats test-auto-router test-gateway-stats-buckets test-session-list-filter test-session-endpoints test-config-secret-merge test-skills-install test-self-improving-skills test-loop-shaping-defaults test-max-iter-notice test-max-iter-notice test-plan-build-mode test-config-profile test-tool-rpc test-session-search test-subagent-bg test-subagent-default test-stream-reliability test-mcp-server test-mcp-hub-projection test-checkpoints test-condense-json test-goals-runner test-kb-index test-kb-pdf test-agents-md test-checkpoints-zpaq test-orient-preamble test-component-config test-autoroute-apply test-fallback-models test-memory-distill test-memory-facts test-memory-autodistill test-checkpoints-redo test-build-roundtrip test-promptware test-orient test-condense-reversible test-heartbeat test-shell-backend test-env-inject test-run-timeout test-shell-output-decode test-fs-grep-tier1-4 test-fs-grep-tier5-6 test-fs-tool-naming test-workspace-paths test-progress-ledger test-project-facts test-otel test-logger-level-quiet test-gateway-token test-fs-secret-gate test-config-env-subst test-delphi-build +test: smoke test-hashline test-toolview test-anthropic-server-tools test-openai-server-tools test-tool-choice test-responses-tool-choice test-println-helper test-utf8-codepage-tag test-gemini-schema-strip test-markdown-render test-json-utf8-roundtrip test-model-discovery test-cron-tool test-provider-catalog test-output-cache test-working-state test-ansi-width test-shell-filters test-learn test-export test-execute-code test-session-stats test-auto-router test-gateway-stats-buckets test-session-list-filter test-session-endpoints test-config-secret-merge test-skills-install test-self-improving-skills test-loop-shaping-defaults test-max-iter-notice test-max-iter-notice test-plan-build-mode test-config-profile test-tool-rpc test-session-search test-subagent-bg test-subagent-default test-stream-reliability test-mcp-server test-mcp-hub-projection test-checkpoints test-condense-json test-goals-runner test-kb-index test-kb-pdf test-agents-md test-checkpoints-zpaq test-orient-preamble test-component-config test-autoroute-apply test-fallback-models test-memory-distill test-memory-facts test-memory-autodistill test-checkpoints-redo test-build-roundtrip test-promptware test-orient test-condense-reversible test-heartbeat test-shell-backend test-env-inject test-run-timeout test-shell-output-decode test-fs-grep-tier1-4 test-fs-grep-tier5-6 test-fs-tool-naming test-workspace-paths test-progress-ledger test-otel test-logger-level-quiet test-gateway-token test-fs-secret-gate test-config-env-subst test-delphi-build diff --git a/bench/agentloop/agentloop_bench.pas b/bench/agentloop/agentloop_bench.pas index 6222151..249d790 100644 --- a/bench/agentloop/agentloop_bench.pas +++ b/bench/agentloop/agentloop_bench.pas @@ -463,28 +463,9 @@ function H_BuildSite(N: Integer; const EnvJSON: string): string; procedure ScenarioBuildSite; var Answer, SP1, SP2: string; - S: TStringList; begin WriteLn; WriteLn('== scenario: build-site (goal anchor + ledger fold + deliverable) =='); - { B1 fixture: a recognisable project in the workspace -- the ## Project - facts (stack, make test, git branch) must reach iteration 1's system - prompt with zero exploration calls spent discovering them. } - S := TStringList.Create; - try - S.Text := 'all: build'#10#9'echo hi'#10'build:'#10#9'echo b'#10'test: build'#10#9'echo t'#10; - S.SaveToFile(GHomeDir + '/workspace/Makefile'); - finally - S.Free; - end; - ForceDirectories(GHomeDir + '/workspace/.git'); - S := TStringList.Create; - try - S.Text := 'ref: refs/heads/bench-main'#10; - S.SaveToFile(GHomeDir + '/workspace/.git/HEAD'); - finally - S.Free; - end; ResetScenario(H_BuildSite); Answer := Chat('[' + MsgObjJSON('user', 'build a small landing page named bench-demo.html for the demo project') + ']', @@ -494,9 +475,6 @@ procedure ScenarioBuildSite; SP1 := EnvSystemPrompt(EnvAt(0)); Check(not Has(SP1, '[progress ledger'), 'iteration 1 system prompt is pristine (prefix-cache preserved)'); - Check(Has(SP1, '## Project'), 'project facts reach iteration 1'); - Check(Has(SP1, 'Test: make test'), 'facts name the verify command (make test)'); - Check(Has(SP1, 'Git branch: bench-main'), 'facts carry the git branch'); SP2 := EnvSystemPrompt(EnvAt(1)); Check(Has(SP2, '[progress ledger'), 'iteration 2 carries the progress ledger'); Check(Has(SP2, 'bench-demo.html'), 'ledger lists the written file'); diff --git a/src/pkg/agent/PasClaw.Agent.ProjectFacts.pas b/src/pkg/agent/PasClaw.Agent.ProjectFacts.pas deleted file mode 100644 index 4d7f420..0000000 --- a/src/pkg/agent/PasClaw.Agent.ProjectFacts.pas +++ /dev/null @@ -1,272 +0,0 @@ -(* - PasClaw.Agent.ProjectFacts -- deterministic "## Project" system-prompt - section: what stack is this, how do I build it, how do I test it, what - git branch am I on. - - Why: a coding agent dropped into a repo burns its first 5-10 tool calls - rediscovering facts the filesystem states outright (the observed - pasclaw.dev-build transcript opened with a wall of fs_list / probe - calls). Claude Code ships git state in every session env for the same - reason. With the commands in the prompt, Rule 3's "verify changes" can - point at THE actual command instead of hand-waving. - - Design constraints: - * PURE FILE READS -- no LLM call, no process exec. `pasclaw runbook` - does the LLM-authored deep version; this is the zero-cost floor - that works on every turn, offline, on both compilers (no TProcess). - Git branch comes from .git/HEAD; working-tree state is the model's - to query (`git status`) -- the block says so. - * BOUNDED -- small fixed probe list, first ~40 Makefile targets, one - package.json parse. Reading these per prompt build is microseconds. - * DEFERENT -- BuildSystemPrompt skips this section entirely when the - project has an AGENTS.md (the operator-authored doc wins; this is - the floor for repos that have nothing). -*) -unit PasClaw.Agent.ProjectFacts; - -{$IFDEF FPC}{$MODE DELPHI}{$ENDIF} -{$H+} - -interface - -{ The "## Project" section for Dir, or '' when nothing recognisable is - there (an empty gateway workspace, a scratch dir). Dir is normally - CurrentWorkspace -- the directory relative tool paths resolve to. } -function BuildProjectFactsSection(const Dir: string): string; - -implementation - -uses - SysUtils, Classes, - PasClaw.JSON; - -function ReadSmallFile(const Path: string; MaxBytes: Integer): string; -var - FS: TFileStream; - Bytes: TBytes; - N: Integer; -begin - Result := ''; - if not FileExists(Path) then Exit; - try - FS := TFileStream.Create(Path, fmOpenRead or fmShareDenyWrite); - try - N := FS.Size; - if N > MaxBytes then N := MaxBytes; - SetLength(Bytes, N); - if N > 0 then FS.ReadBuffer(Bytes[0], N); - Result := TEncoding.UTF8.GetString(Bytes); - finally - FS.Free; - end; - except - Result := ''; - end; -end; - -function MakefileTargets(const Body: string; out HasTest, HasBuildish: Boolean): string; -{ First ~8 plain target names from a Makefile ("name:" at column 0, - skipping dot-targets, pattern rules and variable lines). Enough for the - model to see the vocabulary; the file itself is one read_file away. } -var - Lines: TStringList; - i, Shown: Integer; - L, Name: string; - P: Integer; -begin - Result := ''; - HasTest := False; - HasBuildish := False; - Shown := 0; - Lines := TStringList.Create; - try - Lines.Text := Body; - for i := 0 to Lines.Count - 1 do - begin - L := Lines[i]; - if (L = '') or (L[1] = #9) or (L[1] = ' ') or (L[1] = '.') or (L[1] = '#') then - Continue; - P := Pos(':', L); - if P < 2 then Continue; - { Variable assignments, not rules: "CC := gcc" (the '=' follows the - colon) and "CC=..." with an embedded colon later. } - if (P < Length(L)) and (L[P + 1] = '=') then Continue; - if Pos('=', Copy(L, 1, P)) > 0 then Continue; - Name := Trim(Copy(L, 1, P - 1)); - if (Name = '') or (Pos(' ', Name) > 0) or (Pos('%', Name) > 0) - or (Pos('$', Name) > 0) then Continue; - if SameText(Name, 'test') or (Pos('test', LowerCase(Name)) = 1) then - HasTest := True; - if SameText(Name, 'all') or SameText(Name, 'build') then - HasBuildish := True; - if Shown < 8 then - begin - if Result <> '' then Result := Result + ', '; - Result := Result + Name; - Inc(Shown); - end; - end; - finally - Lines.Free; - end; -end; - -function PackageJsonScripts(const Body: string): string; -{ "name: command" lines for up to 6 package.json scripts, build/test/ - start/dev first. } -const - Preferred: array[0..3] of string = ('build', 'test', 'start', 'dev'); -var - Root, Scripts: TJsonObject; - i, Shown: Integer; - K, V: string; -begin - Result := ''; - Root := nil; - try - Root := TJsonObject.Parse(Body); - except - Root := nil; - end; - if Root = nil then Exit; - try - Scripts := Root.ChildObject('scripts'); - if Scripts = nil then Exit; - try - Shown := 0; - for i := 0 to High(Preferred) do - begin - V := Scripts.GetStr(Preferred[i], ''); - if V = '' then Continue; - K := 'npm run ' + Preferred[i]; - if Preferred[i] = 'test' then K := 'npm test'; - if Result <> '' then Result := Result + '; '; - Result := Result + K; - Inc(Shown); - if Shown >= 6 then Break; - end; - finally - Scripts.Free; - end; - finally - Root.Free; - end; -end; - -function GitBranch(const Dir: string): string; -{ Branch name from .git/HEAD ("ref: refs/heads/"); a detached - HEAD (bare hash) is reported as such. Pure read -- working-tree state - is deliberately NOT computed here (that would need exec); the section - tells the model to run `git status` itself. } -var - Head: string; -begin - Result := ''; - Head := Trim(ReadSmallFile(Dir + PathDelim + '.git' + PathDelim + 'HEAD', 512)); - if Head = '' then Exit; - if Copy(Head, 1, 16) = 'ref: refs/heads/' then - Result := Copy(Head, 17, MaxInt) - else - Result := '(detached: ' + Copy(Head, 1, 12) + ')'; -end; - -function HasAny(const Dir: string; const Names: array of string): Boolean; -var - i: Integer; -begin - Result := False; - for i := 0 to High(Names) do - if FileExists(Dir + PathDelim + Names[i]) then Exit(True); -end; - -function HasExt(const Dir, Ext: string): Boolean; -var - SR: TSearchRec; -begin - Result := FindFirst(Dir + PathDelim + '*' + Ext, faAnyFile, SR) = 0; - FindClose(SR); -end; - -function BuildProjectFactsSection(const Dir: string): string; -var - Stack, BuildCmd, TestCmd, Targets, Scripts, Branch: string; - MkBody: string; - HasTest, HasBuildish: Boolean; - - procedure AddStack(const S: string); - begin - if Stack <> '' then Stack := Stack + ', '; - Stack := Stack + S; - end; - -begin - Result := ''; - if (Dir = '') or (not DirectoryExists(Dir)) then Exit; - Stack := ''; BuildCmd := ''; TestCmd := ''; Targets := ''; Scripts := ''; - - MkBody := ReadSmallFile(Dir + PathDelim + 'Makefile', 128 * 1024); - if MkBody <> '' then - begin - AddStack('Makefile'); - Targets := MakefileTargets(MkBody, HasTest, HasBuildish); - if BuildCmd = '' then BuildCmd := 'make'; - if HasTest then TestCmd := 'make test'; - end; - - if FileExists(Dir + PathDelim + 'package.json') then - begin - AddStack('Node (package.json)'); - Scripts := PackageJsonScripts(ReadSmallFile(Dir + PathDelim + 'package.json', 64 * 1024)); - if (BuildCmd = '') and (Pos('npm run build', Scripts) > 0) then BuildCmd := 'npm run build'; - if (TestCmd = '') and (Pos('npm test', Scripts) > 0) then TestCmd := 'npm test'; - end; - - if FileExists(Dir + PathDelim + 'Cargo.toml') then - begin - AddStack('Rust (Cargo.toml)'); - if BuildCmd = '' then BuildCmd := 'cargo build'; - if TestCmd = '' then TestCmd := 'cargo test'; - end; - - if FileExists(Dir + PathDelim + 'go.mod') then - begin - AddStack('Go (go.mod)'); - if BuildCmd = '' then BuildCmd := 'go build ./...'; - if TestCmd = '' then TestCmd := 'go test ./...'; - end; - - if HasExt(Dir, '.dpr') or HasExt(Dir, '.lpi') then - AddStack('Pascal (FPC/Delphi project)'); - - if HasAny(Dir, ['pyproject.toml', 'requirements.txt']) then - begin - AddStack('Python'); - if (TestCmd = '') and (HasAny(Dir, ['pytest.ini']) or - DirectoryExists(Dir + PathDelim + 'tests')) then - TestCmd := 'pytest'; - end; - - Branch := GitBranch(Dir); - - { Nothing recognisable -> no section (an empty workspace stays quiet). } - if (Stack = '') and (Branch = '') then Exit; - - Result := '## Project' + sLineBreak; - if Stack <> '' then - Result := Result + 'Stack: ' + Stack + sLineBreak; - if BuildCmd <> '' then - Result := Result + 'Build: ' + BuildCmd + sLineBreak; - if TestCmd <> '' then - Result := Result + 'Test: ' + TestCmd + - ' <- run this to verify after code edits' + sLineBreak; - if Targets <> '' then - Result := Result + 'Make targets: ' + Targets + sLineBreak; - if Scripts <> '' then - Result := Result + 'npm scripts: ' + Scripts + sLineBreak; - if Branch <> '' then - Result := Result + 'Git branch: ' + Branch + - ' (run `git status` for working-tree state)' + sLineBreak; - Result := TrimRight(Result); -end; - -end. diff --git a/src/pkg/agent/PasClaw.Agent.Prompt.pas b/src/pkg/agent/PasClaw.Agent.Prompt.pas index f657dc1..6ea22c0 100644 --- a/src/pkg/agent/PasClaw.Agent.Prompt.pas +++ b/src/pkg/agent/PasClaw.Agent.Prompt.pas @@ -178,7 +178,6 @@ implementation PasClaw.Agent.Orient, { task-aware MEMORY slicing (Cfg.OrientTaskAware) } PasClaw.Memory.Facts, { distilled-fact injection (Cfg.MemoryDistillEnabled) } PasClaw.Tools.Sandbox, { CurrentWorkspace -- the working dir the prompt advertises } - PasClaw.Agent.ProjectFacts, { deterministic ## Project section (stack/build/test/git) } PasClaw.MCP.Disclosure; { deferred-tools section (Cfg.MCPProgressiveDisclosure) } const @@ -239,14 +238,6 @@ function BuildIdentitySection: string; RuntimeString; end; -function PromptWorkDir: string; -{ The directory relative tool paths resolve to -- the same value the - Workspace section advertises (configured workspace, or launch dir). } -begin - Result := CurrentWorkspace; - if Trim(Result) = '' then Result := GetCurrentDir; -end; - function BuildWorkspaceSection: string; var Home, WorkDir: string; @@ -855,19 +846,6 @@ function BuildSystemPrompt(Cfg: TConfig; const UserSys: string; Result := AppendSection(Result, BuildPlanModeSection); Result := AppendSection(Result, BuildIdentitySection); Result := AppendSection(Result, BuildWorkspaceSection); - { Deterministic project facts (stack / build / test commands / git - branch) detected from the working directory by pure file reads -- - saves the model its usual "what kind of repo is this" exploration - prefix and gives Rule 3's verify-your-changes a concrete command. - Skipped when the project carries an AGENTS.md: the operator-authored - doc (emitted below as Project Rules) states this better; facts are - the zero-config floor. } - { The AGENTS.md check must walk the SAME tree the facts are read from - (the workspace) -- with '' it walked the launch cwd, so a workspace - AGENTS.md failed to suppress the facts and an unrelated launch-dir - AGENTS.md could suppress useful workspace facts. } - if FindProjectAgentsMd(PromptWorkDir) = '' then - Result := AppendSection(Result, BuildProjectFactsSection(PromptWorkDir)); Result := AppendSection(Result, BuildMemorySection((Cfg <> nil) and Cfg.OrientTaskAware, TaskHint)); { Distilled facts (Cfg.MemoryDistillEnabled). Wholesale, after the .md diff --git a/src/tests/project_facts_tests.pas b/src/tests/project_facts_tests.pas deleted file mode 100644 index 5c2b3e1..0000000 --- a/src/tests/project_facts_tests.pas +++ /dev/null @@ -1,135 +0,0 @@ -program project_facts_tests; -(* - Covers PasClaw.Agent.ProjectFacts -- the deterministic "## Project" - system-prompt section (stack / build / test commands / git branch from - pure file reads). Pins: - * Makefile detection: target names surface, `make` as build, a test - target promotes `make test` as THE verify command. - * package.json scripts surface (npm run build / npm test). - * Cargo.toml / go.mod fill build/test only when nothing better claimed - them (Makefile wins). - * Git branch parsed from .git/HEAD; detached HEAD reported as such. - * An empty / unrecognisable directory yields NO section. -*) - -{$IFDEF FPC}{$MODE DELPHI}{$ENDIF} -{$H+} -{$IFDEF FPC}{$CODEPAGE UTF8}{$ENDIF} - -uses - {$IFDEF UNIX}cthreads,{$ENDIF} - SysUtils, Classes, - PasClaw.Agent.ProjectFacts; - -procedure Fail_(const Msg: string); -begin WriteLn('FAIL: ' + Msg); Halt(1); end; - -procedure AssertTrue(Cond: Boolean; const Msg: string); -begin if not Cond then Fail_(Msg); end; - -procedure AssertHas(const Hay, Needle, Msg: string); -begin - if Pos(Needle, Hay) <= 0 then - Fail_(Msg + ' (needle "' + Needle + '" missing from "' + Copy(Hay, 1, 300) + '")'); -end; - -procedure AssertNot(const Hay, Needle, Msg: string); -begin - if Pos(Needle, Hay) > 0 then - Fail_(Msg + ' (unexpected "' + Needle + '")'); -end; - -procedure WriteText(const Path, Body: string); -var - S: TStringList; -begin - S := TStringList.Create; - try - S.Text := Body; - S.SaveToFile(Path); - finally - S.Free; - end; -end; - -procedure Nuke(const Dir: string); -var - SR: TSearchRec; -begin - if FindFirst(Dir + '/*', faAnyFile, SR) = 0 then - begin - repeat - if (SR.Name <> '.') and (SR.Name <> '..') then - begin - if (SR.Attr and faDirectory) <> 0 then Nuke(Dir + '/' + SR.Name) - else DeleteFile(Dir + '/' + SR.Name); - end; - until FindNext(SR) <> 0; - FindClose(SR); - end; - RemoveDir(Dir); -end; - -var - Dir, S: string; -begin - Dir := IncludeTrailingPathDelimiter(GetTempDir) + 'pcfacts'; - if DirectoryExists(Dir) then Nuke(Dir); - ForceDirectories(Dir); - - { Empty dir -> no section. } - AssertTrue(BuildProjectFactsSection(Dir) = '', 'empty dir yields no section'); - AssertTrue(BuildProjectFactsSection('') = '', 'blank dir yields no section'); - - { Makefile with test target + git branch. } - WriteText(Dir + '/Makefile', - 'CC := gcc'#10 + - 'all: build'#10#9'echo hi'#10 + - 'build:'#10#9'echo build'#10 + - 'test: build'#10#9'echo test'#10 + - '.PHONY: all'#10 + - '%.o: %.c'#10#9'cc'#10); - ForceDirectories(Dir + '/.git'); - WriteText(Dir + '/.git/HEAD', 'ref: refs/heads/feature/x'#10); - S := BuildProjectFactsSection(Dir); - AssertHas(S, '## Project', 'section header'); - AssertHas(S, 'Makefile', 'stack names Makefile'); - AssertHas(S, 'Build: make', 'build command is make'); - AssertHas(S, 'Test: make test', 'test target promotes make test'); - AssertHas(S, 'verify after code edits', 'test line carries the verify nudge'); - AssertHas(S, 'all, build, test', 'target names listed'); - AssertNot(S, '%', 'pattern rules are not listed as targets'); - AssertNot(S, 'CC', 'variable lines are not targets'); - AssertHas(S, 'Git branch: feature/x', 'branch parsed from .git/HEAD'); - AssertHas(S, 'git status', 'points the model at git status for tree state'); - WriteLn(' ok: Makefile + git facts'); - - { package.json scripts; Makefile still owns Build/Test. } - WriteText(Dir + '/package.json', - '{"name":"x","scripts":{"build":"vite build","test":"vitest run","dev":"vite"}}'); - S := BuildProjectFactsSection(Dir); - AssertHas(S, 'Node (package.json)', 'stack names Node'); - AssertHas(S, 'npm run build', 'npm build script listed'); - AssertHas(S, 'npm test', 'npm test script listed'); - AssertHas(S, 'Build: make', 'Makefile still owns the build command'); - WriteLn(' ok: package.json scripts'); - - { Cargo-only dir: cargo fills build/test. } - Nuke(Dir); ForceDirectories(Dir); - WriteText(Dir + '/Cargo.toml', '[package]'#10'name = "x"'#10); - S := BuildProjectFactsSection(Dir); - AssertHas(S, 'Rust (Cargo.toml)', 'stack names Rust'); - AssertHas(S, 'Build: cargo build', 'cargo build'); - AssertHas(S, 'Test: cargo test', 'cargo test'); - WriteLn(' ok: Cargo fallback'); - - { Detached HEAD. } - ForceDirectories(Dir + '/.git'); - WriteText(Dir + '/.git/HEAD', 'deadbeefcafe1234'#10); - S := BuildProjectFactsSection(Dir); - AssertHas(S, '(detached: deadbeefcafe', 'detached HEAD reported'); - WriteLn(' ok: detached HEAD'); - - Nuke(Dir); - WriteLn('PASS'); -end.