Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
49 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "agenthands/helix", "files": {"test/oracle/llm/client.go": {"9dd32f9689bf4716486b52d2f5efcb6c387b9318": {"t": 1785745650.9018784, "n": 4964, "h": "de092c634ebc5082", "c": "//go:build llm || llmjudge\n\npackage llm\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/anthropics/anthropic-sdk-go\"\n\toai \"github.com/openai/openai-go\"\n\t\"github.com/openai/openai-go/option\"\n)\n\nconst (\n\t// EnvAPIKey is the environment variable for the Anthropic API key.\n\tEnvAPIKey = \"ANTHROPIC_API_KEY\"\n\t// EnvDeepSeekKey is the environment variable for the DeepSeek API key.\n\tEnvDeepSeekKey = \"DEEPSEEK_API_KEY\"\n\t// EnvTestModel overrides the subject model for behavioral tests.\n\tEnvTestModel = \"HELIX_TEST_MODEL\"\n\t// EnvJudgeModel overrides the judge model for scoring.\n\tEnvJudgeModel = \"HELIX_JUDGE_MODEL\"\n\t// EnvInlineJudge enables inline judge mode when set to \"1\".\n\tEnvInlineJudge = \"HELIX_INLINE_JUDGE\"\n\t// EnvLLMProvider selects the LLM provider: \"anthropic\" (default) or \"deepseek\".\n\tEnvLLMProvider = \"HELIX_LLM_PROVIDER\"\n\t// DefaultSubjectModel is the cheapest Claude model for behavioral tests (D-11).\n\tDefaultSubjectModel = \"claude-haiku-4-5-20251001\"\n\t// DefaultDeepSeekModel is the default DeepSeek model.\n\tDefaultDeepSeekModel = \"deepseek-chat\"\n)\n\n// Provider returns the active LLM provider name.\nfunc Provider() string {\n\tif p := os.Getenv(EnvLLMProvider); p != \"\" {\n\t\treturn p\n\t}\n\t// Auto-detect: if DeepSeek key is set but Anthropic isn't, use DeepSeek.\n\tif os.Getenv(EnvAPIKey) == \"\" && os.Getenv(EnvDeepSeekKey) != \"\" {\n\t\treturn \"deepseek\"\n\t}\n\treturn \"anthropic\"\n}\n\n// SkipWithoutAPIKey skips the test if no LLM API key is available (D-18).\n// Never logs the key value to avoid information disclosure (T-21-01).\nfunc SkipWithoutAPIKey(t *testing.T) {\n\tt.Helper()\n\tp := Provider()\n\tswitch p {\n\tcase \"deepseek\":\n\t\tif os.Getenv(EnvDeepSeekKey) == \"\" {\n\t\t\tt.Skip(\"DEEPSEEK_API_KEY not set\")\n\t\t}\n\tdefault:\n\t\tif os.Getenv(EnvAPIKey) == \"\" {\n\t\t\tt.Skip(\"ANTHROPIC_API_KEY not set\")\n\t\t}\n\t}\n}\n\n// NewClient creates an Anthropic client that reads ANTHROPIC_API_KEY from env.\n// Only used when provider is \"anthropic\".\nfunc NewClient() anthropic.Client {\n\treturn anthropic.NewClient()\n}\n\n// SubjectModel returns the model ID for behavioral tests.\n// Uses HELIX_TEST_MODEL if set, otherwise defaults per provider (D-10/D-11).\nfunc SubjectModel() string {\n\tif m := os.Getenv(EnvTestModel); m != \"\" {\n\t\treturn m\n\t}\n\tif Provider() == \"deepseek\" {\n\t\treturn DefaultDeepSeekModel\n\t}\n\treturn DefaultSubjectModel\n}\n\n// JudgeModel returns the model ID for judge scoring and whether the run is self-judged.\n// Uses HELIX_JUDGE_MODEL if set, otherwise falls back to SubjectModel (D-10/D-12).\nfunc JudgeModel() (model string, selfJudged bool) {\n\tif m := os.Getenv(EnvJudgeModel); m != \"\" {\n\t\treturn m, false\n\t}\n\treturn SubjectModel(), true\n}\n\n// AskSingleTurn sends a single-turn prompt and returns the response text,\n// stop reason, and any error. Routes to the active provider.\nfunc AskSingleTurn(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (responseText string, stopReason string, err error) {\n\tif Provider() == \"deepseek\" {\n\t\treturn askDeepSeek(ctx, model, system, userPrompt)\n\t}\n\treturn askAnthropic(ctx, client, model, system, userPrompt)\n}\n\n// askAnthropic sends via Anthropic API. Uses MaxTokens: 1024 (D-01).\nfunc askAnthropic(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (string, string, error) {\n\tparams := anthropic.MessageNewParams{\n\t\tModel: model,\n\t\tMaxTokens: 1024,\n\t\tMessages: []anthropic.MessageParam{\n\t\t\tanthropic.NewUserMessage(anthropic.NewTextBlock(userPrompt)),\n\t\t},\n\t}\n\tif system != \"\" {\n\t\tparams.System = []anthropic.TextBlockParam{\n\t\t\t{Text: system},\n\t\t}\n\t}\n\n\tmessage, err := client.Messages.New(ctx, params)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"anthropic API call failed: %w\", err)\n\t}\n\n\tfor _, block := range message.Content {\n\t\tif v, ok := block.AsAny().(anthropic.TextBlock); ok {\n\t\t\treturn v.Text, string(message.StopReason), nil\n\t\t}\n\t}\n\n\treturn \"\", string(message.StopReason), fmt.Errorf(\"no text content in response\")\n}\n\n// askDeepSeek sends via DeepSeek's OpenAI-compatible API.\nfunc askDeepSeek(ctx context.Context, model, system, userPrompt string) (string, string, error) {\n\tclient := oai.NewClient(\n\t\toption.WithAPIKey(os.Getenv(EnvDeepSeekKey)),\n\t\toption.WithBaseURL(\"https://api.deepseek.com\"),\n\t)\n\n\tvar messages []oai.ChatCompletionMessageParamUnion\n\tif system != \"\" {\n\t\tmessages = append(messages, oai.SystemMessage(system))\n\t}\n\tmessages = append(messages, oai.UserMessage(userPrompt))\n\n\tresp, err := client.Chat.Completions.New(ctx, oai.ChatCompletionNewParams{\n\t\tModel: model,\n\t\tMessages: messages,\n\t})\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"deepseek API call failed: %w\", err)\n\t}\n\n\tif len(resp.Choices) == 0 {\n\t\treturn \"\", \"\", fmt.Errorf(\"no choices in deepseek response\")\n\t}\n\n\tchoice := resp.Choices[0]\n\treturn choice.Message.Content, string(choice.FinishReason), nil\n}\n\n// InterCallDelay sleeps 250ms between API calls to avoid rate limits (D-19).\nfunc InterCallDelay() {\n\ttime.Sleep(250 * time.Millisecond)\n}\n"}, "6501092f14b36d1e467bb71a2ae21db2a850acd3": {"t": 1785755391.6387677, "n": 4964, "h": "de092c634ebc5082", "c": "//go:build llm || llmjudge\n\npackage llm\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/anthropics/anthropic-sdk-go\"\n\toai \"github.com/openai/openai-go\"\n\t\"github.com/openai/openai-go/option\"\n)\n\nconst (\n\t// EnvAPIKey is the environment variable for the Anthropic API key.\n\tEnvAPIKey = \"ANTHROPIC_API_KEY\"\n\t// EnvDeepSeekKey is the environment variable for the DeepSeek API key.\n\tEnvDeepSeekKey = \"DEEPSEEK_API_KEY\"\n\t// EnvTestModel overrides the subject model for behavioral tests.\n\tEnvTestModel = \"HELIX_TEST_MODEL\"\n\t// EnvJudgeModel overrides the judge model for scoring.\n\tEnvJudgeModel = \"HELIX_JUDGE_MODEL\"\n\t// EnvInlineJudge enables inline judge mode when set to \"1\".\n\tEnvInlineJudge = \"HELIX_INLINE_JUDGE\"\n\t// EnvLLMProvider selects the LLM provider: \"anthropic\" (default) or \"deepseek\".\n\tEnvLLMProvider = \"HELIX_LLM_PROVIDER\"\n\t// DefaultSubjectModel is the cheapest Claude model for behavioral tests (D-11).\n\tDefaultSubjectModel = \"claude-haiku-4-5-20251001\"\n\t// DefaultDeepSeekModel is the default DeepSeek model.\n\tDefaultDeepSeekModel = \"deepseek-chat\"\n)\n\n// Provider returns the active LLM provider name.\nfunc Provider() string {\n\tif p := os.Getenv(EnvLLMProvider); p != \"\" {\n\t\treturn p\n\t}\n\t// Auto-detect: if DeepSeek key is set but Anthropic isn't, use DeepSeek.\n\tif os.Getenv(EnvAPIKey) == \"\" && os.Getenv(EnvDeepSeekKey) != \"\" {\n\t\treturn \"deepseek\"\n\t}\n\treturn \"anthropic\"\n}\n\n// SkipWithoutAPIKey skips the test if no LLM API key is available (D-18).\n// Never logs the key value to avoid information disclosure (T-21-01).\nfunc SkipWithoutAPIKey(t *testing.T) {\n\tt.Helper()\n\tp := Provider()\n\tswitch p {\n\tcase \"deepseek\":\n\t\tif os.Getenv(EnvDeepSeekKey) == \"\" {\n\t\t\tt.Skip(\"DEEPSEEK_API_KEY not set\")\n\t\t}\n\tdefault:\n\t\tif os.Getenv(EnvAPIKey) == \"\" {\n\t\t\tt.Skip(\"ANTHROPIC_API_KEY not set\")\n\t\t}\n\t}\n}\n\n// NewClient creates an Anthropic client that reads ANTHROPIC_API_KEY from env.\n// Only used when provider is \"anthropic\".\nfunc NewClient() anthropic.Client {\n\treturn anthropic.NewClient()\n}\n\n// SubjectModel returns the model ID for behavioral tests.\n// Uses HELIX_TEST_MODEL if set, otherwise defaults per provider (D-10/D-11).\nfunc SubjectModel() string {\n\tif m := os.Getenv(EnvTestModel); m != \"\" {\n\t\treturn m\n\t}\n\tif Provider() == \"deepseek\" {\n\t\treturn DefaultDeepSeekModel\n\t}\n\treturn DefaultSubjectModel\n}\n\n// JudgeModel returns the model ID for judge scoring and whether the run is self-judged.\n// Uses HELIX_JUDGE_MODEL if set, otherwise falls back to SubjectModel (D-10/D-12).\nfunc JudgeModel() (model string, selfJudged bool) {\n\tif m := os.Getenv(EnvJudgeModel); m != \"\" {\n\t\treturn m, false\n\t}\n\treturn SubjectModel(), true\n}\n\n// AskSingleTurn sends a single-turn prompt and returns the response text,\n// stop reason, and any error. Routes to the active provider.\nfunc AskSingleTurn(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (responseText string, stopReason string, err error) {\n\tif Provider() == \"deepseek\" {\n\t\treturn askDeepSeek(ctx, model, system, userPrompt)\n\t}\n\treturn askAnthropic(ctx, client, model, system, userPrompt)\n}\n\n// askAnthropic sends via Anthropic API. Uses MaxTokens: 1024 (D-01).\nfunc askAnthropic(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (string, string, error) {\n\tparams := anthropic.MessageNewParams{\n\t\tModel: model,\n\t\tMaxTokens: 1024,\n\t\tMessages: []anthropic.MessageParam{\n\t\t\tanthropic.NewUserMessage(anthropic.NewTextBlock(userPrompt)),\n\t\t},\n\t}\n\tif system != \"\" {\n\t\tparams.System = []anthropic.TextBlockParam{\n\t\t\t{Text: system},\n\t\t}\n\t}\n\n\tmessage, err := client.Messages.New(ctx, params)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"anthropic API call failed: %w\", err)\n\t}\n\n\tfor _, block := range message.Content {\n\t\tif v, ok := block.AsAny().(anthropic.TextBlock); ok {\n\t\t\treturn v.Text, string(message.StopReason), nil\n\t\t}\n\t}\n\n\treturn \"\", string(message.StopReason), fmt.Errorf(\"no text content in response\")\n}\n\n// askDeepSeek sends via DeepSeek's OpenAI-compatible API.\nfunc askDeepSeek(ctx context.Context, model, system, userPrompt string) (string, string, error) {\n\tclient := oai.NewClient(\n\t\toption.WithAPIKey(os.Getenv(EnvDeepSeekKey)),\n\t\toption.WithBaseURL(\"https://api.deepseek.com\"),\n\t)\n\n\tvar messages []oai.ChatCompletionMessageParamUnion\n\tif system != \"\" {\n\t\tmessages = append(messages, oai.SystemMessage(system))\n\t}\n\tmessages = append(messages, oai.UserMessage(userPrompt))\n\n\tresp, err := client.Chat.Completions.New(ctx, oai.ChatCompletionNewParams{\n\t\tModel: model,\n\t\tMessages: messages,\n\t})\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"deepseek API call failed: %w\", err)\n\t}\n\n\tif len(resp.Choices) == 0 {\n\t\treturn \"\", \"\", fmt.Errorf(\"no choices in deepseek response\")\n\t}\n\n\tchoice := resp.Choices[0]\n\treturn choice.Message.Content, string(choice.FinishReason), nil\n}\n\n// InterCallDelay sleeps 250ms between API calls to avoid rate limits (D-19).\nfunc InterCallDelay() {\n\ttime.Sleep(250 * time.Millisecond)\n}\n"}, "95c0025c346d6e20b1e0b99cfab075f6db5cc991": {"t": 1785755392.8431807, "n": 4970, "h": "7b138b582c2c0009", "c": "//go:build llm || llmjudge\n\npackage llm\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/anthropics/anthropic-sdk-go\"\n\toai \"github.com/openai/openai-go\"\n\t\"github.com/openai/openai-go/option\"\n)\n\nconst (\n\t// EnvAPIKey is the environment variable for the Anthropic API key.\n\tEnvAPIKey = \"ANTHROPIC_API_KEY\"\n\t// EnvDeepSeekKey is the environment variable for the DeepSeek API key.\n\tEnvDeepSeekKey = \"DEEPSEEK_API_KEY\"\n\t// EnvTestModel overrides the subject model for behavioral tests.\n\tEnvTestModel = \"SERENA_TEST_MODEL\"\n\t// EnvJudgeModel overrides the judge model for scoring.\n\tEnvJudgeModel = \"SERENA_JUDGE_MODEL\"\n\t// EnvInlineJudge enables inline judge mode when set to \"1\".\n\tEnvInlineJudge = \"SERENA_INLINE_JUDGE\"\n\t// EnvLLMProvider selects the LLM provider: \"anthropic\" (default) or \"deepseek\".\n\tEnvLLMProvider = \"SERENA_LLM_PROVIDER\"\n\t// DefaultSubjectModel is the cheapest Claude model for behavioral tests (D-11).\n\tDefaultSubjectModel = \"claude-haiku-4-5-20251001\"\n\t// DefaultDeepSeekModel is the default DeepSeek model.\n\tDefaultDeepSeekModel = \"deepseek-chat\"\n)\n\n// Provider returns the active LLM provider name.\nfunc Provider() string {\n\tif p := os.Getenv(EnvLLMProvider); p != \"\" {\n\t\treturn p\n\t}\n\t// Auto-detect: if DeepSeek key is set but Anthropic isn't, use DeepSeek.\n\tif os.Getenv(EnvAPIKey) == \"\" && os.Getenv(EnvDeepSeekKey) != \"\" {\n\t\treturn \"deepseek\"\n\t}\n\treturn \"anthropic\"\n}\n\n// SkipWithoutAPIKey skips the test if no LLM API key is available (D-18).\n// Never logs the key value to avoid information disclosure (T-21-01).\nfunc SkipWithoutAPIKey(t *testing.T) {\n\tt.Helper()\n\tp := Provider()\n\tswitch p {\n\tcase \"deepseek\":\n\t\tif os.Getenv(EnvDeepSeekKey) == \"\" {\n\t\t\tt.Skip(\"DEEPSEEK_API_KEY not set\")\n\t\t}\n\tdefault:\n\t\tif os.Getenv(EnvAPIKey) == \"\" {\n\t\t\tt.Skip(\"ANTHROPIC_API_KEY not set\")\n\t\t}\n\t}\n}\n\n// NewClient creates an Anthropic client that reads ANTHROPIC_API_KEY from env.\n// Only used when provider is \"anthropic\".\nfunc NewClient() anthropic.Client {\n\treturn anthropic.NewClient()\n}\n\n// SubjectModel returns the model ID for behavioral tests.\n// Uses SERENA_TEST_MODEL if set, otherwise defaults per provider (D-10/D-11).\nfunc SubjectModel() string {\n\tif m := os.Getenv(EnvTestModel); m != \"\" {\n\t\treturn m\n\t}\n\tif Provider() == \"deepseek\" {\n\t\treturn DefaultDeepSeekModel\n\t}\n\treturn DefaultSubjectModel\n}\n\n// JudgeModel returns the model ID for judge scoring and whether the run is self-judged.\n// Uses SERENA_JUDGE_MODEL if set, otherwise falls back to SubjectModel (D-10/D-12).\nfunc JudgeModel() (model string, selfJudged bool) {\n\tif m := os.Getenv(EnvJudgeModel); m != \"\" {\n\t\treturn m, false\n\t}\n\treturn SubjectModel(), true\n}\n\n// AskSingleTurn sends a single-turn prompt and returns the response text,\n// stop reason, and any error. Routes to the active provider.\nfunc AskSingleTurn(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (responseText string, stopReason string, err error) {\n\tif Provider() == \"deepseek\" {\n\t\treturn askDeepSeek(ctx, model, system, userPrompt)\n\t}\n\treturn askAnthropic(ctx, client, model, system, userPrompt)\n}\n\n// askAnthropic sends via Anthropic API. Uses MaxTokens: 1024 (D-01).\nfunc askAnthropic(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (string, string, error) {\n\tparams := anthropic.MessageNewParams{\n\t\tModel: model,\n\t\tMaxTokens: 1024,\n\t\tMessages: []anthropic.MessageParam{\n\t\t\tanthropic.NewUserMessage(anthropic.NewTextBlock(userPrompt)),\n\t\t},\n\t}\n\tif system != \"\" {\n\t\tparams.System = []anthropic.TextBlockParam{\n\t\t\t{Text: system},\n\t\t}\n\t}\n\n\tmessage, err := client.Messages.New(ctx, params)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"anthropic API call failed: %w\", err)\n\t}\n\n\tfor _, block := range message.Content {\n\t\tif v, ok := block.AsAny().(anthropic.TextBlock); ok {\n\t\t\treturn v.Text, string(message.StopReason), nil\n\t\t}\n\t}\n\n\treturn \"\", string(message.StopReason), fmt.Errorf(\"no text content in response\")\n}\n\n// askDeepSeek sends via DeepSeek's OpenAI-compatible API.\nfunc askDeepSeek(ctx context.Context, model, system, userPrompt string) (string, string, error) {\n\tclient := oai.NewClient(\n\t\toption.WithAPIKey(os.Getenv(EnvDeepSeekKey)),\n\t\toption.WithBaseURL(\"https://api.deepseek.com\"),\n\t)\n\n\tvar messages []oai.ChatCompletionMessageParamUnion\n\tif system != \"\" {\n\t\tmessages = append(messages, oai.SystemMessage(system))\n\t}\n\tmessages = append(messages, oai.UserMessage(userPrompt))\n\n\tresp, err := client.Chat.Completions.New(ctx, oai.ChatCompletionNewParams{\n\t\tModel: model,\n\t\tMessages: messages,\n\t})\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"deepseek API call failed: %w\", err)\n\t}\n\n\tif len(resp.Choices) == 0 {\n\t\treturn \"\", \"\", fmt.Errorf(\"no choices in deepseek response\")\n\t}\n\n\tchoice := resp.Choices[0]\n\treturn choice.Message.Content, string(choice.FinishReason), nil\n}\n\n// InterCallDelay sleeps 250ms between API calls to avoid rate limits (D-19).\nfunc InterCallDelay() {\n\ttime.Sleep(250 * time.Millisecond)\n}\n"}, "49b599317bdb461e4c8a0fbd4fd7eb0a45d5bd5f": {"t": 1785755393.9997447, "n": 4968, "h": "688776e9b5332fcc", "c": "//go:build llm || llmjudge\n\npackage llm\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/anthropics/anthropic-sdk-go\"\n\toai \"github.com/openai/openai-go\"\n\t\"github.com/openai/openai-go/option\"\n)\n\nconst (\n\t// EnvAPIKey is the environment variable for the Anthropic API key.\n\tEnvAPIKey = \"ANTHROPIC_API_KEY\"\n\t// EnvDeepSeekKey is the environment variable for the DeepSeek API key.\n\tEnvDeepSeekKey = \"DEEPSEEK_API_KEY\"\n\t// EnvTestModel overrides the subject model for behavioral tests.\n\tEnvTestModel = \"SERENA_TEST_MODEL\"\n\t// EnvJudgeModel overrides the judge model for scoring.\n\tEnvJudgeModel = \"SERENA_JUDGE_MODEL\"\n\t// EnvInlineJudge enables inline judge mode when set to \"1\".\n\tEnvInlineJudge = \"SERENA_INLINE_JUDGE\"\n\t// EnvLLMProvider selects the LLM provider: \"anthropic\" (default) or \"deepseek\".\n\tEnvLLMProvider = \"SERENA_LLM_PROVIDER\"\n\t// DefaultSubjectModel is the cheapest Claude model for behavioral tests (D-11).\n\tDefaultSubjectModel = \"claude-haiku-4-20250414\"\n\t// DefaultDeepSeekModel is the default DeepSeek model.\n\tDefaultDeepSeekModel = \"deepseek-chat\"\n)\n\n// Provider returns the active LLM provider name.\nfunc Provider() string {\n\tif p := os.Getenv(EnvLLMProvider); p != \"\" {\n\t\treturn p\n\t}\n\t// Auto-detect: if DeepSeek key is set but Anthropic isn't, use DeepSeek.\n\tif os.Getenv(EnvAPIKey) == \"\" && os.Getenv(EnvDeepSeekKey) != \"\" {\n\t\treturn \"deepseek\"\n\t}\n\treturn \"anthropic\"\n}\n\n// SkipWithoutAPIKey skips the test if no LLM API key is available (D-18).\n// Never logs the key value to avoid information disclosure (T-21-01).\nfunc SkipWithoutAPIKey(t *testing.T) {\n\tt.Helper()\n\tp := Provider()\n\tswitch p {\n\tcase \"deepseek\":\n\t\tif os.Getenv(EnvDeepSeekKey) == \"\" {\n\t\t\tt.Skip(\"DEEPSEEK_API_KEY not set\")\n\t\t}\n\tdefault:\n\t\tif os.Getenv(EnvAPIKey) == \"\" {\n\t\t\tt.Skip(\"ANTHROPIC_API_KEY not set\")\n\t\t}\n\t}\n}\n\n// NewClient creates an Anthropic client that reads ANTHROPIC_API_KEY from env.\n// Only used when provider is \"anthropic\".\nfunc NewClient() anthropic.Client {\n\treturn anthropic.NewClient()\n}\n\n// SubjectModel returns the model ID for behavioral tests.\n// Uses SERENA_TEST_MODEL if set, otherwise defaults per provider (D-10/D-11).\nfunc SubjectModel() string {\n\tif m := os.Getenv(EnvTestModel); m != \"\" {\n\t\treturn m\n\t}\n\tif Provider() == \"deepseek\" {\n\t\treturn DefaultDeepSeekModel\n\t}\n\treturn DefaultSubjectModel\n}\n\n// JudgeModel returns the model ID for judge scoring and whether the run is self-judged.\n// Uses SERENA_JUDGE_MODEL if set, otherwise falls back to SubjectModel (D-10/D-12).\nfunc JudgeModel() (model string, selfJudged bool) {\n\tif m := os.Getenv(EnvJudgeModel); m != \"\" {\n\t\treturn m, false\n\t}\n\treturn SubjectModel(), true\n}\n\n// AskSingleTurn sends a single-turn prompt and returns the response text,\n// stop reason, and any error. Routes to the active provider.\nfunc AskSingleTurn(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (responseText string, stopReason string, err error) {\n\tif Provider() == \"deepseek\" {\n\t\treturn askDeepSeek(ctx, model, system, userPrompt)\n\t}\n\treturn askAnthropic(ctx, client, model, system, userPrompt)\n}\n\n// askAnthropic sends via Anthropic API. Uses MaxTokens: 1024 (D-01).\nfunc askAnthropic(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (string, string, error) {\n\tparams := anthropic.MessageNewParams{\n\t\tModel: model,\n\t\tMaxTokens: 1024,\n\t\tMessages: []anthropic.MessageParam{\n\t\t\tanthropic.NewUserMessage(anthropic.NewTextBlock(userPrompt)),\n\t\t},\n\t}\n\tif system != \"\" {\n\t\tparams.System = []anthropic.TextBlockParam{\n\t\t\t{Text: system},\n\t\t}\n\t}\n\n\tmessage, err := client.Messages.New(ctx, params)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"anthropic API call failed: %w\", err)\n\t}\n\n\tfor _, block := range message.Content {\n\t\tif v, ok := block.AsAny().(anthropic.TextBlock); ok {\n\t\t\treturn v.Text, string(message.StopReason), nil\n\t\t}\n\t}\n\n\treturn \"\", string(message.StopReason), fmt.Errorf(\"no text content in response\")\n}\n\n// askDeepSeek sends via DeepSeek's OpenAI-compatible API.\nfunc askDeepSeek(ctx context.Context, model, system, userPrompt string) (string, string, error) {\n\tclient := oai.NewClient(\n\t\toption.WithAPIKey(os.Getenv(EnvDeepSeekKey)),\n\t\toption.WithBaseURL(\"https://api.deepseek.com\"),\n\t)\n\n\tvar messages []oai.ChatCompletionMessageParamUnion\n\tif system != \"\" {\n\t\tmessages = append(messages, oai.SystemMessage(system))\n\t}\n\tmessages = append(messages, oai.UserMessage(userPrompt))\n\n\tresp, err := client.Chat.Completions.New(ctx, oai.ChatCompletionNewParams{\n\t\tModel: model,\n\t\tMessages: messages,\n\t})\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"deepseek API call failed: %w\", err)\n\t}\n\n\tif len(resp.Choices) == 0 {\n\t\treturn \"\", \"\", fmt.Errorf(\"no choices in deepseek response\")\n\t}\n\n\tchoice := resp.Choices[0]\n\treturn choice.Message.Content, string(choice.FinishReason), nil\n}\n\n// InterCallDelay sleeps 250ms between API calls to avoid rate limits (D-19).\nfunc InterCallDelay() {\n\ttime.Sleep(250 * time.Millisecond)\n}\n"}, "c4a7271ffc2a563b769dcf3bd46aa21c43ef17a2": {"t": 1785755396.3486178, "n": 2830, "h": "5621b7dd4980a2a1", "c": "//go:build llm || llmjudge\n\npackage llm\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/anthropics/anthropic-sdk-go\"\n)\n\nconst (\n\t// EnvAPIKey is the environment variable for the Anthropic API key.\n\tEnvAPIKey = \"ANTHROPIC_API_KEY\"\n\t// EnvTestModel overrides the subject model for behavioral tests.\n\tEnvTestModel = \"SERENA_TEST_MODEL\"\n\t// EnvJudgeModel overrides the judge model for scoring.\n\tEnvJudgeModel = \"SERENA_JUDGE_MODEL\"\n\t// EnvInlineJudge enables inline judge mode when set to \"1\".\n\tEnvInlineJudge = \"SERENA_INLINE_JUDGE\"\n\t// DefaultSubjectModel is the cheapest Claude model for behavioral tests (D-11).\n\tDefaultSubjectModel = \"claude-haiku-4-20250414\"\n)\n\n// SkipWithoutAPIKey skips the test if ANTHROPIC_API_KEY is not set (D-18).\n// Never logs the key value to avoid information disclosure (T-21-01).\nfunc SkipWithoutAPIKey(t *testing.T) {\n\tt.Helper()\n\tif os.Getenv(EnvAPIKey) == \"\" {\n\t\tt.Skip(\"ANTHROPIC_API_KEY not set\")\n\t}\n}\n\n// NewClient creates an Anthropic client that reads ANTHROPIC_API_KEY from env.\nfunc NewClient() anthropic.Client {\n\treturn anthropic.NewClient()\n}\n\n// SubjectModel returns the model ID for behavioral tests.\n// Uses SERENA_TEST_MODEL if set, otherwise DefaultSubjectModel (D-10/D-11).\nfunc SubjectModel() string {\n\tif m := os.Getenv(EnvTestModel); m != \"\" {\n\t\treturn m\n\t}\n\treturn DefaultSubjectModel\n}\n\n// JudgeModel returns the model ID for judge scoring and whether the run is self-judged.\n// Uses SERENA_JUDGE_MODEL if set, otherwise falls back to SubjectModel (D-10/D-12).\nfunc JudgeModel() (model string, selfJudged bool) {\n\tif m := os.Getenv(EnvJudgeModel); m != \"\" {\n\t\treturn m, false\n\t}\n\treturn SubjectModel(), true\n}\n\n// AskSingleTurn sends a single-turn prompt and returns the response text,\n// stop reason, and any error. Uses MaxTokens: 1024 (D-01).\nfunc AskSingleTurn(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (responseText string, stopReason string, err error) {\n\tparams := anthropic.MessageNewParams{\n\t\tModel: model,\n\t\tMaxTokens: 1024,\n\t\tMessages: []anthropic.MessageParam{\n\t\t\tanthropic.NewUserMessage(anthropic.NewTextBlock(userPrompt)),\n\t\t},\n\t}\n\tif system != \"\" {\n\t\tparams.System = []anthropic.TextBlockParam{\n\t\t\t{Text: system},\n\t\t}\n\t}\n\n\tmessage, err := client.Messages.New(ctx, params)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"anthropic API call failed: %w\", err)\n\t}\n\n\t// Extract text from first TextBlock in response content.\n\tfor _, block := range message.Content {\n\t\tif v, ok := block.AsAny().(anthropic.TextBlock); ok {\n\t\t\treturn v.Text, string(message.StopReason), nil\n\t\t}\n\t}\n\n\treturn \"\", string(message.StopReason), fmt.Errorf(\"no text content in response\")\n}\n\n// InterCallDelay sleeps 250ms between API calls to avoid rate limits (D-19).\nfunc InterCallDelay() {\n\ttime.Sleep(250 * time.Millisecond)\n}\n"}}, ".planning/milestones/v1.10-research/SUMMARY.md": {"9dd32f9689bf4716486b52d2f5efcb6c387b9318": {"t": 1786068555.470047, "n": 24315, "h": "99f171ae602d3369", "c": "# Project Research Summary — Helix v1.10 Live Semantic Index\n\n**Domain:** Live, evidence-backed semantic graph layered onto a shipped LSP-backed code intelligence daemon (DuckDB committed snapshots ⊕ live overlay ⊕ LSP validation ⊕ agent-facing context tools ⊕ guardrails ⊕ eval harness)\n**Researched:** 2026-05-03\n**Confidence:** HIGH overall — STACK and ARCHITECTURE anchored in v1.9 source, FEATURES in adjacent-product docs, PITFALLS in shipped Helix bugs and upstream issues. MEDIUM on guardrail-receipt schemas (genuinely novel) and dynamic-language type-resolution conventions.\n\n## Executive Summary\n\nHelix v1.10 turns the v1.9 online LSP/tree-sitter capabilities into a durable, live, evidence-backed semantic graph. The decision-defining shape is **DuckDB committed snapshots + fsnotify live overlay + effective-read semantics + LSP revalidation queue**, exposed to agents as 10 new MCP tools, gated by a guardrail/receipt policy engine, and validated end-to-end by an out-of-process eval harness comparing four modes (`baseline / native / semantic / semantic_guarded`). The adjacent-product survey shows this combination is **not** the dominant pattern in any shipping product: SCIP/Glean/Stack-Graphs ship batch graphs without live overlay; Aider/Cursor/Cody ship live retrieval without a durable graph. **Freshness-as-API** (every response carries `freshness` + `score_status` + `pending_lsp_files`) and **semantic safety receipts** (destructive edits gated on prior `find_references`/`analyze_blast_radius` evidence) are genuine differentiators with no published prior art.\n\nThe recommended approach lands in 13 phases under a clean dependency order: store first → tree-sitter extraction + stable symbol IDs → live overlay → LSP enrichment → graph scores → clustering → 10 MCP tools → existing-tool integration via strangler fig → compaction → type resolution → guardrails → eval. New code lives in **Layer 1.5** (`internal/semantic/`, `internal/phasegraph/`, `internal/guardrails/`, `internal/eval/`), between the kernel and skills, with strict acyclic imports. Integration with kernel-edit and repomap is via injected callbacks (`postEditHook`, `SetSemanticLookup`) so the kernel never imports semantic. The DuckDB binding is `github.com/duckdb/duckdb-go v2.10502.0` (CGO=1 required, single-binary preserved via bundled static libs); the v1.9 Phase 51.1 stub policy extends to auto-disable `semantic_index.enabled` on CGO=0 builds.\n\nTop risks concentrate in **state coherence under concurrency** (overlay × graph-cache × compaction race; fsnotify dropping watches on editor atomic-rename; PageRank score instability while LSP enrichment lands asynchronously) and **agent-shaped failure modes** (receipts lost to context compaction, eval prompt leakage to commercial LLMs). Mitigations are concrete and architectural: epoch-CAS contract between overlay writes and compaction, directory-watching with content-hash scrub, `graph_version` + monotone-frontier rule on tool envelopes, server-side receipt store with ID-only forwarding. **Must-not-regress invariants from v1.9** — `LazyInit`-last middleware order, single canonical `GrammarRegistry`, structured LS readiness gates (rust-analyzer `experimental/serverStatus`, jdtls `WaitUntilJavaReady`), bounded-label metrics with no source content — all carry forward and are explicitly preserved.\n\n## Key Findings\n\n### Recommended Stack (additions only; v1.9 stack fixed)\n\n- **`github.com/duckdb/duckdb-go` v2.10502.0** (DuckDB 1.5.2): fact-store driver, official post-donation repo, `database/sql` + Appender, bundled static libs cover the 6-archive matrix. **CGO=1 required** — extends Phase 51.1 stub policy.\n- **`gonum.org/v1/gonum` v0.16.0 test-only**: oracle for hand-rolled PageRank/components; never imported from production per ADR-005.\n- **`github.com/tiktoken-go/tokenizer` v0.6.x**: pure-Go embedded vocab; rejected `pkoukk/tiktoken-go` because it network-downloads vocab.\n- **`github.com/bluekeyes/go-gitdiff` v0.8.x**: pure-Go patch parse + apply; rejected `sourcegraph/go-diff` as parser-only.\n- **`fsnotify` v1.9.0** (already vendored): kept; build recursive walker, atomic-rename re-attach, `ENOSPC` degraded-mode in-tree.\n- **Pipeline DAG**: stdlib only (~80 LOC Kahn's algorithm).\n- **Type resolution / fixpoint**: stdlib only; comment parsers regex-tractable.\n\n**CGO posture (load-bearing):** `semantic_index.enabled=true` requires CGO=1. CGO=0 builds compile via `//go:build !cgo` stubs that auto-return `Kind: Unsupported`. Daemon must auto-set `cfg.SemanticIndex.Enabled = false` when `treesitter.Available == false`. **What NOT to pull in:** any KG/RDF stack, embedding/vector libs, heavyweight DAG/workflow engines, gonum-in-production, recursive-watch wrappers.\n\n### Expected Features\n\n**Must have (table stakes):** durable queryable symbol+reference graph with stable IDs across renames; incremental update without full reindex; cross-file find-references / call-hierarchy; token-budgeted ranked context; tree-sitter-first extraction with LSP enrichment; PageRank importance ranking; indexing-progress + status; diagnostics integration after edits.\n\n**Should have (differentiators):**\n- Effective-read semantics (snapshot ⊕ overlay) — addresses Cursor's top complaint\n- First-class freshness on every response\n- Tiered confidence on edges with explicit evidence kinds (1.00 LSP / 0.90 annotation / … / 0.20 unknown) + `validation_state`\n- Live LSP revalidation queue with priority boosts on edit\n- Multi-projection PageRank with per-projection freshness\n- Cluster maps + cluster explanations\n- Guardrail policy engine + safety receipts (semantic correlation across tool calls; novel)\n- Eval harness with 4 modes\n- Type resolution with bounded fixpoint for dynamic languages (capped, comment-fallback ≤ 0.60)\n- `validate_graph_edge` (agent challenges the index)\n- Typed pipeline DAG with phase validation at startup\n\n**Defer (v1.10.x / v1.11+):** multi-projection PageRank, clustering tools, P1 retrieval companions (`find_related_symbols` / `get_change_impact_graph` / `explain_symbol_deep` / `validate_graph_edge`), `semantic_guarded` mode + G-006..G-010, type resolution + access chains, comment-based fallback, idle compaction tuning, Java/Rust first-class extraction, cross-repo graphs, v1.9 carry-over (PKG-01 SC-3, distros, Phase 51 reproducibility scope, Phase 55 forwarder span).\n\n**Anti-features (refusals):** vector/embedding search; RDF/SPARQL KG; \"real-time across whole repo\" marketing; auto-execute when guardrails pass; in-process LSP servers; soundness-grade type inference; SWE-bench leaderboard chase; LLM-generated edge explanations; persistent receipts detached from `graph_version`; full-rebuild on every Helix start.\n\n### Architecture Approach\n\n**Layer 1.5 placement** between kernel and skills. Strict acyclic imports — kernel never depends on semantic; coupling inverted via callbacks.\n\n**Major components:**\n1. **Semantic Service** owns `*sql.DB` for DuckDB at `<workspace>/.helix/semantic.duckdb`; opens at new daemon bootstrap step **2.5** between language registry and kernel creation; disable path returns `nil`, all callsites guard.\n2. **Live Update Pipeline** — fsnotify directory-watcher with atomic-rename re-attach + content-hash scrub; coalescer (250ms debounce, 200-event bulk threshold); overlay writer with `overlay_epoch`; idle compaction goroutine.\n3. **LSP Enrichment Worker** — single goroutine with `container/heap` priority queue; reuses `kernel.Pool().AcquireLease(...)` via small `LeaseAcquirer` interface (no kernel import); honors v1.9 readiness gates.\n4. **Graph + Rank + Cluster** — in-memory cache loaded from DuckDB; hand-rolled multi-projection weighted PageRank with personalized restart and incremental local repair.\n5. **MCP Tools + Skill** — 10 new tools registered via existing skill adapter pattern (new daemon step 13.5).\n6. **Existing-Tool Integration (strangler fig)** — `repomapSkill.SetSemanticLookup(lookup)` mirrors `SetEnrichFn`; `get_repo_map` / `get_context` / `analyze_blast_radius` / `get_health` consult semantic when available, fall back to v1.9. Zero source change to `internal/repomap` engine.\n7. **Edit-Tool Hook (callback inversion)** — `edit.RegisterTools` gains `postEditHook func(ctx, []string, source)`; daemon wires `d.semantic.LiveQueue().EnqueueChangeEventsFunc()`. Threaded through `fileops.replace_in_file` / `fuzzy_edit` too. `nil`-safe.\n8. **Guardrail Middleware** — installed at new step **14b.5** between Suggestion and LazyInit. Execution order: `LazyInit → Guardrail → Suggestion → ProfileFilter → Telemetry → handler`. **LazyInit MUST remain installed last (executes first).** Telemetry gains `guardrail_blocked` / `guardrail_warned` outcome classes.\n9. **Pipeline DAG** — strangler fig: stdlib library used immediately for new graphs (semantic indexing, live update, eval); imperative `daemon.New` bootstrap stays with `// TODO(v1.11): migrate` marker.\n10. **Eval Harness** — out-of-process by default; subprocess `helix daemon` per task with isolated config; agent (Anthropic / DeepSeek refactored from `test/oracle/llm/`) over stdio forwarder; in-process variant for `make eval-quick`. Each mode is a config preset. New `--profile=baseline` strips Helix tools entirely.\n\n**Shutdown ordering:** semantic.Run under daemon errgroup; on cancel: stop accepting events → drain coalescer → drain LSP queue → flush overlay (no compaction) → close DuckDB → kernel shuts down LS workers (v1.9 ordering preserved last).\n\n**DuckDB cross-process rule:** all store access via daemon. CLI subcommands and eval harness MUST NOT reopen the `.duckdb` file (DuckDB single-writer). Lock with vet rule forbidding `duckdb-go` import outside `internal/semantic/store/`.\n\n### Critical Pitfalls (top 5; full set in PITFALLS.md)\n\n1. **C1 — Overlay × graph-cache × compaction race.** Three writers, three independent monotonic clocks, no single linearization point; ClearOverlay can drop overlay rows committed after MergeBaseAndOverlay. **Mitigation:** epoch-CAS — compaction snapshots `overlay_epoch`, ClearOverlay deletes rows ≤ captured epoch. Property test in P8.\n2. **C2 — fsnotify silent watcher death on atomic-rename.** Vim/JetBrains/VS Code save patterns kill inode-attached watchers; Linux inotify per-user limit blows up on >8k-dir repos. **Mitigation:** watch directories not files; eager re-attach on RENAME/REMOVE; required (not best-effort) 5-min content-hash scrub; ENOSPC → manifest-poll fallback per workspace; surface in `get_semantic_graph_status`.\n3. **C3 — PageRank score instability while LSP enrichment lands.** Async confidence promotion (0.4 → 1.0) re-ranks the graph between calls. **Mitigation:** every ranked envelope returns `graph_version` + `enrichment_level`; default to last-committed-snapshot scores; bound incremental repair frontier (`max_repair_nodes`, fail-closed); determinism test (NodeID-sorted tiebreak).\n4. **C8 — LSP enrichment death spiral.** Post-`git checkout` flood buries jdtls (just stabilized in Phase 56); interactive p95 climbs as Helix indexes more. **Mitigation:** revalidation queue strictly lower priority via separate token bucket + concurrency cap (default 1 worker); interactive deadline preempts; do NOT flood after `bulk_update` (lazy-on-tool-call drives enrichment); honor v1.9 readiness gates before enqueueing.\n5. **C7 — Stable symbol ID drift on rename / generics / overloads / anonymous symbols.** Breaks `analyze_blast_radius`, `get_change_impact_graph`, `find_related_symbols`. **Mitigation:** key contract `repo_id || package_path || enclosing_chain || name || arity || receiver_type` — explicitly NOT line/column, NOT `signature_hash`, NOT generic type-args. AST-shape hash for anonymous. 30+ before/after test matrix per language. Lock contract in P1.\n\n**Other notable risks:** C5 cross-process DuckDB lock (architectural rule + vet lint); C6 DuckDB JSON growth (CHECKPOINT + weekly VACUUM, evidence size cap); M1 overlay rows lost in compaction crash (single-tx OR `compaction_journal`); M4 receipts lost to context compaction (server-side receipt store, ID-only forwarding); M5 eval prompt leakage (synthetic-only corpora, retention-zero); M6 metrics cardinality (extend v1.2 bounded-label CI lint); M7 fallback path drift (`source` field in every envelope; index-disabled goldens).\n\n### Must-Not-Regress Invariants (v1.9 carryover)\n\n- **Middleware install LIFO order:** `LazyInit` MUST remain installed last (executes first). Guardrail inserts at 14b.5 between Suggestion and LazyInit.\n- **Single canonical `GrammarRegistry`** (BUG-04, Phase 49) injected from daemon bootstrap; semantic extractors consume the same instance.\n- **Structured LS readiness gates** (BUG-02 rust-analyzer `experimental/serverStatus`, Phase 56 jdtls `JdtlsAdapter.WaitUntilJavaReady`, `Worker.Start` `OnNotification` wiring) — semantic LSP enrichment honors these.\n- **Bounded-label metrics with no source content** in metrics or traces — extend v1.2 cardinality allowlist; PromQL validator (registry-driven, fail-closed) covers new families.\n- **Single-binary + CGO=0 stub policy** — daemon refuses semantic with `Kind: Unsupported` and remediation text.\n\n## Implications for Roadmap\n\n13 phases honoring the dependency order from ARCHITECTURE.md \"Suggested build order\". Deviations from SPEC §32: pipeline DAG library split out as P0.5 so P1/P2/P10 can consume it; type resolution (P11) moved between P8 and P9 (improves edge precision; doesn't gate other phases).\n\n### Phase P0: Schema & Store + Config + Cardinality\n**Rationale:** Foundation; locks cross-process access rule (C5), JSON-vs-typed-column discipline (C6), cardinality allowlist (M6) before any consumer depends on the store.\n**Delivers:** `internal/semantic/{store,types,config}`; DuckDB schema; schema versioning; `semantic_index.enabled` config + 4-layer precedence; vet rule forbidding `duckdb-go` outside `internal/semantic/store/`; CGO=0 stub.\n**Avoids:** C5, C6, M5, M6.\n\n### Phase P0.5: Pipeline DAG Library\n**Rationale:** Consumed by P1/P2/P10 planners. Cheap (~80 LOC stdlib).\n**Delivers:** `internal/phasegraph/`.\n**Avoids:** m1 half-migration (bootstrap stays imperative with TODO(v1.11) marker).\n\n### Phase P1: Tree-sitter Extraction + Stable Symbol IDs\n**Rationale:** Symbol identity is the foundational graph contract — must lock before live overlay or any consumer.\n**Delivers:** `internal/semantic/{extract,resolve}`; Go / TS+JS / Python first-class extraction; key contract; 30+ before/after test matrix per language.\n**Avoids:** C7.\n**Research flag:** language-specific edge cases (Go generics, TS overloads, Python decorators).\n\n### Phase P2: Live Overlay + Watcher\n**Rationale:** First daemon integration; introduces `postEditHook`. Must define epoch contract before P8 depends on it.\n**Delivers:** `internal/semantic/live/*`; directory-watcher + atomic-rename re-attach + ENOSPC fallback + scrub; coalescer; overlay writer with `overlay_epoch`; `postEditHook` wired into `edit.RegisterTools` and `fileops.RegisterTools`; daemon bootstrap step 9.5.\n**Avoids:** C1 race, C2 watcher misses, M2 bulk threshold.\n**Research flag:** fsnotify edge cases across editors require concrete fixture validation.\n\n### Phase P3: LSP Enrichment Worker\n**Rationale:** Depends on lspool lease API; consumes v1.9 readiness gates.\n**Delivers:** `internal/semantic/lspenrich/*`; priority queue; `LeaseAcquirer` interface (no kernel import); concurrency cap; readiness-gate integration.\n**Avoids:** C8 death spiral; preserves BUG-02 / Phase 56 invariants.\n\n### Phase P4: Graph Scores (single-projection MVP)\n**Rationale:** Reuses v1.6 RepoMap PageRank; multi-projection deferred.\n**Delivers:** `internal/semantic/{graph,rank}/*`; weighted PageRank with shared `applyDanglingMass`; `graph_version`; bounded incremental repair; determinism test.\n**Avoids:** C3, C4.\n\n### Phase P5: Clustering (defer to v1.10.x)\n**Delivers:** `internal/semantic/cluster/*`; cluster snapshot keyed on `graph_version`.\n**Avoids:** C3 (cluster drift mirrors score drift).\n\n### Phase P6: 10 New MCP Tools\n**Rationale:** Existing skill registration pattern; `get_semantic_context` is the headline tool.\n**Delivers:** `internal/semantic/tools/*` + `internal/skill/semantic/*`; P0 set of 4 (`index_semantic_graph`, `refresh_semantic_graph`, `get_semantic_graph_status`, `get_semantic_context`); remaining 6 in v1.10.x; `freshness` field on every response; stable-key tiebreak.\n**Avoids:** M8 non-determinism.\n\n### Phase P7: Existing-Tool Integration (strangler fig)\n**Rationale:** Lowest-risk integration — zero source change to `internal/repomap` engine.\n**Delivers:** `repomapSkill.SetSemanticLookup(lookup)`; `get_repo_map` / `get_context` / `analyze_blast_radius` / `get_health` consult semantic-when-available with automatic v1.9 fallback; `source` field in result envelopes; index-disabled goldens preserved.\n**Avoids:** M7.\n\n### Phase P8: Compaction & Retention\n**Rationale:** Honors P2 epoch contract.\n**Delivers:** idle-debounced compaction; `CHECKPOINT` end-of-compact; weekly `VACUUM` (config-gated); single-tx commit OR `compaction_journal`; long-repo bench fixture.\n**Avoids:** C1 (epoch CAS), M1, C6.\n\n### Phase P11: Type Resolution (out-of-order, between P8 and P9)\n**Rationale:** Improves edge precision; doesn't gate other phases. Comment-fallback work load-bearing for JS/Python eval signal.\n**Delivers:** `internal/semantic/typeresolve/*`; bounded fixpoint with hard iteration cap (`max_fixpoint_iters=8`) + early-exit on no progress; conservative emission on non-convergence; comment-derived evidence capped at 0.7.\n**Avoids:** M3, m3.\n**Research flag:** comment-format grammar subset (JSDoc / PHPDoc / YARD / Python typing comments).\n\n### Phase P9: Guardrails + Middleware\n**Rationale:** Depends on graph + receipts; must come after graph scores so policy can read freshness/`score_status`.\n**Delivers:** `internal/guardrails/*`; `internal/mcp/guardrail_middleware.go`; daemon step 14b.5; G-001..G-005 with `warn` default; server-side receipt store (5-min TTL + `graph_version` invalidation); ID-only forwarding; destructive tools look up claims server-side; telemetry outcomes; `DoD.md` + `GUARDRAILS.md`.\n**Avoids:** M4.\n**Research flag:** receipt-schema design has no published prior art.\n\n### Phase P10: Eval Harness\n**Rationale:** Depends on a working semantic stack + guardrails.\n**Delivers:** `internal/eval/*` + `internal/cli/eval/*`; subprocess daemon orchestration with isolated config dirs; agent over stdio forwarder; 4 modes; cross-model judging (informational, never CI gate); synthetic-only corpora + OSS Helix repo; retention-zero provider config; `make eval-quick` in-process.\n**Avoids:** M5, m4.\n**Research flag:** task-class DoD definitions and tool-behavior scoring rubric calibration; provider data-retention TOS re-verification.\n\n### Phase P12: Pipeline DAG bootstrap migration → v1.11\nv1.10 lands `phasegraph` library at P0.5 and uses it for new graphs only. Migrating imperative `daemon.New` 16-step bootstrap touches every test that constructs a daemon; deferred per strangler fig.\n\n### v1.9 Carry-over (folded into v1.10)\n\n- **PKG-01 SC-3** — cut first signed release with real minisign keypair (deployment-gated; engineering-side checks already pass).\n- **PKG-DEFER-03/04/05** — Homebrew tap, Scoop bucket, native Linux package; re-scope based on first signed release feedback.\n- **Phase 51 reproducibility-gate architectural fix** — real-release-vs-Pass-3 OR CONTRIBUTING.md wording softening.\n- **Phase 55 `forwarder.tools.call` span unification** with gRPC server span.\n\nThese four items can land in parallel with the early P0–P3 semantic phases (different files, different reviewers) — schedule before semantic stack is feature-complete to avoid release risk concentration.\n\n### Research Flags\n\n**Phases needing deeper research during planning:**\n- **P1:** stable symbol ID edge cases per language (overloads, generics, anonymous closures, decorators).\n- **P2:** fsnotify atomic-rename behavior across Vim/JetBrains/VS Code; ENOSPC fallback ergonomics.\n- **P9:** safety-receipt schema is novel; design must hold up against agent context-compaction and `enforce`-mode false-positive rate.\n- **P10:** task-class DoD definitions and scoring rubric; provider data-retention TOS re-verification.\n- **P11:** comment-format grammar subset selection.\n\n**Standard patterns (skip deep research):** P0, P0.5, P3, P4 (MVP), P6, P7, P8.\n\n## Confidence Assessment\n\n| Area | Confidence | Notes |\n|------|------------|-------|\n| Stack | HIGH | DuckDB binding, fsnotify gaps, gonum-vs-hand-rolled, eval libs verified at file/issue level. MEDIUM only on `tiktoken-go/tokenizer` and `bluekeyes/go-gitdiff` (READMEs not deep-verified). |\n| Features | HIGH on adjacent products + eval conventions; MEDIUM on Cursor/Cody/Augment freshness behavior (closed source); LOW on guardrail-receipt schema (no prior art). |\n| Architecture | HIGH | Verified against `internal/daemon/daemon.go`, SPEC §5/6/24/36/39, v1.9 callback patterns. MEDIUM on test-harness `:memory:` ergonomics and forwarder gRPC propagation for eval subprocess. |\n| Pitfalls | HIGH for items grounded in shipped Helix bugs (BUG-01..04, Phase 56) and upstream issues (fsnotify #17/#80/#214/#254/#372, DuckDB #77/#4899); MEDIUM on cross-product war stories. |\n\n**Overall confidence:** HIGH. Decision-defining choices (Layer 1.5 placement, DuckDB CGO posture + auto-disable, postEditHook callback, lookup-callback inversion for repomap, guardrail middleware position 14b.5, strangler-fig phasegraph for new graphs only, out-of-process eval harness) all anchored in v1.9 source and SPEC contracts. Unknowns isolated to novel surfaces (guardrail receipts, eval scoring rubric) and dynamic-language type-resolution edges.\n\n### Gaps to Address During Planning\n\n- **DuckDB driver canonical path.** STACK.md notes `marcboeker/go-duckdb` → `duckdb/duckdb-go` donation; ARCHITECTURE.md still references the legacy path. Lock import path during P0.\n- **Test harness DuckDB strategy.** `test/harness/Runner` constructs daemons in-process; with semantic enabled every test creates a `.duckdb` file unless `:memory:` is used. Confirm at P0.\n- **Forwarder gRPC propagation of semantic config** for eval subprocess (likely `HELIX_CONFIG_PATH`); confirm at P10.\n- **Bootstrap step renumbering convention.** Decimal additions (2.5 / 12e–12i / 14b.5) cross 16+ steps; reviewer ergonomics may justify a one-time renumber.\n- **Receipt TTL × `graph_version` invalidation interplay.** Long-running guardrail-warned operations need P9 prototype.\n- **Eval task corpus.** Synthetic-only is the threat-model rule but has lower discriminating power; OSS Helix repo + permissively-licensed SWE-bench Verified subset are options. Lock at P10.\n- **Provider data-retention shifts.** Re-verify Anthropic/OpenAI/DeepSeek TOS at P10 implementation.\n\n## Sources\n\n**Primary (HIGH):** `SPEC-DRAFT.md` (40 sections), `.planning/PROJECT.md`, `internal/daemon/daemon.go` (lines 199-203, 241-255, 296-336, 349-380, 481-525, 765-825), `internal/mcp/lazy_init.go:106-108`, `CLAUDE.md`. [duckdb/duckdb-go](https://github.com/duckdb/duckdb-go), [DuckDB Concurrency](https://duckdb.org/docs/current/connect/concurrency), [fsnotify](https://github.com/fsnotify/fsnotify) issues #17/#80/#214/#254/#372, [gonum graph/network](https://pkg.go.dev/gonum.org/v1/gonum/graph/network), [SCIP](https://sourcegraph.com/blog/announcing-scip), [Glean](https://engineering.fb.com/2024/12/19/developer-tools/glean-open-source-code-indexing/), [Stack graphs](https://github.blog/open-source/introducing-stack-graphs/), [Aider repomap](https://aider.chat/docs/repomap.html), [SWE-bench](https://www.swebench.com/).\n\n**Secondary (MEDIUM):** [tiktoken-go/tokenizer](https://github.com/tiktoken-go/tokenizer), [bluekeyes/go-gitdiff](https://github.com/bluekeyes/go-gitdiff) (READMEs only), [Cursor indexing](https://towardsdatascience.com/how-cursor-actually-indexes-your-codebase/), [Copilot guardrails](https://docs.github.com/en/copilot/tutorials/cloud-agent/build-guardrails), pyright/sorbet/Phan/Psalm tier conventions.\n\n**Tertiary (LOW — needs validation):** Sourcegraph zoekt / gopls overlay-correctness post-mortems; Anthropic/OpenAI/DeepSeek TOS (shifting); guardrail-receipt prior art (none found, novel design needs P9 prototype).\n"}}}}