seihou-core-0.16.0.0: src/Seihou/Core/Scaffold.hs
module Seihou.Core.Scaffold
( moduleDhall,
readmeTemplate,
blueprintDhall,
examplePromptMarkdown,
promptDhall,
exampleAgentPromptMarkdown,
judgementDhall,
judgementExamplesJson,
)
where
import Data.Text qualified as T
import Seihou.Prelude
-- | Generate the module.dhall content for a new module.
-- The output imports the schema via URL and uses record completion (::).
moduleDhall :: Text -> Text -> Text -> Text
moduleDhall name url hash =
T.unlines
[ "let S =",
" " <> url,
" " <> hash,
"",
"in S.Module::{",
" , name = \"" <> name <> "\"",
" , version = Some \"0.1.0\"",
" , description = Some \"A new seihou module\"",
" , vars =",
" [ S.VarDecl::{",
" , name = \"project.name\"",
" , type = \"text\"",
" , description = Some \"The name of the project\"",
" , required = True",
" }",
" ]",
" , prompts =",
" [ S.Prompt::{",
" , var = \"project.name\"",
" , text = \"What is your project name?\"",
" }",
" ]",
" , steps =",
" [ S.Step::{",
" , strategy = \"template\"",
" , src = \"README.md.tpl\"",
" , dest = \"README.md\"",
" }",
" ]",
" }"
]
-- | Generate the README.md.tpl template content.
readmeTemplate :: Text
readmeTemplate = "# {{project.name}}\n\nA project generated by seihou.\n"
-- | Generate the blueprint.dhall content for a new blueprint.
-- The output imports the schema via URL and uses record completion (::).
-- The prompt body is kept in a sibling @prompt.md@ file so authors can
-- edit Markdown directly without escaping through Dhall's string literal
-- rules; the Dhall record imports it via @./prompt.md as Text@.
blueprintDhall :: Text -> Text -> Text -> Text
blueprintDhall name url hash =
T.unlines
[ "let S =",
" " <> url,
" " <> hash,
"",
"in S.Blueprint::{",
" , name = \"" <> name <> "\"",
" , version = Some \"0.1.0\"",
" , description = Some \"A new seihou blueprint\"",
" , prompt = ./prompt.md as Text",
" , vars =",
" [ S.VarDecl::{",
" , name = \"project.name\"",
" , type = \"text\"",
" , description = Some \"The name of the project\"",
" , required = True",
" }",
" ]",
" , prompts =",
" [ S.Prompt::{",
" , var = \"project.name\"",
" , text = \"What is your project name?\"",
" }",
" ]",
" , baseModules = [] : List S.Dependency.Type",
" , files = [] : List S.Blueprint.BlueprintFile.Type",
" , migrations = [] : List S.BlueprintMigration.Type",
" , tags = [] : List Text",
" -- Optional: declare the agent this blueprint was written for.",
" -- These override the invoking user's config files but lose to a",
" -- --provider / --model / --effort flag or a SEIHOU_AGENT_* variable.",
" -- , launch = Some S.Launch::{ effort = Some \"max\" }",
" }"
]
-- | Example @prompt.md@ body emitted next to a freshly scaffolded
-- @blueprint.dhall@. The body opens with a @{{project.name}}@
-- placeholder so authors can see prompt-time substitution working, then
-- describes the workflow the agent runner (EP-31) will follow at run
-- time. Authors are expected to edit this file freely; the Dhall record
-- imports it verbatim via @./prompt.md as Text@.
examplePromptMarkdown :: Text
examplePromptMarkdown =
T.unlines
[ "# {{project.name}}",
"",
"You are helping a user scaffold a new project from this blueprint.",
"",
"## What you have access to",
"",
"- The user's current working directory (where this blueprint is being",
" applied). Read, edit, and create files freely.",
"- The blueprint's `files/` directory, mounted read-only as reference",
" material. Copy or adapt snippets, examples, or partial templates",
" from there as needed.",
"- The full `seihou` and `git` toolsets. Use `seihou run`, `seihou",
" status`, and `seihou validate-module` to compose modules and check",
" state.",
"",
"## How to proceed",
"",
"1. Confirm with the user what they are trying to build. Ask clarifying",
" questions only when the answer would change a load-bearing decision.",
"2. Lay down the initial scaffolding. Prefer small, testable pieces over",
" one large drop.",
"3. Iterate with the user, making edits as the design takes shape.",
"4. Use Conventional Commits (`feat:`, `fix:`, `docs:`, …) when",
" committing.",
"5. Ask before destructive changes (deleting files, force-pushing,",
" rewriting history).",
"",
"Replace this body with the workflow that fits your blueprint. The",
"more specific the prompt, the more useful the agent's first pass."
]
-- | Generate the prompt.dhall content for a first-class agent prompt.
-- The prompt schema may be ahead of the public pinned schema import during
-- local development, so this scaffold emits a self-contained Dhall record
-- with explicit empty-list types.
promptDhall :: Text -> Text -> Text -> Text
promptDhall name _url _hash =
T.unlines
[ "let Prompt =",
" { var : Text, text : Text, when : Optional Text, choices : Optional (List Text) }",
"",
"let CommandVar =",
" { name : Text",
" , run : Text",
" , workDir : Optional Text",
" , when : Optional Text",
" , trim : Bool",
" , maxBytes : Optional Natural",
" }",
"",
"let PromptFile =",
" { src : Text, description : Optional Text }",
"",
"let PromptGuidance =",
" { title : Text, body : Text, when : Optional Text }",
"",
"let Launch =",
" { provider : Optional Text, mode : Optional Text, model : Optional Text }",
"",
"in { name = \"" <> name <> "\"",
" , version = Some \"0.1.0\"",
" , description = Some \"A reusable agent-session prompt\"",
" , prompt = ./prompt.md as Text",
" , vars =",
" [ { name = \"project.name\"",
" , type = \"text\"",
" , default = None Text",
" , description = Some \"The name of the project\"",
" , required = False",
" , validation = None Text",
" }",
" ]",
" , prompts = [] : List Prompt",
" , commandVars = [] : List CommandVar",
" , guidance =",
" [ { title = \"Repository workflow\"",
" , body = \"Inspect the project before editing, keep changes scoped, and run the smallest useful validation command.\"",
" , when = None Text",
" }",
" ]",
" , files = [] : List PromptFile",
" , allowedTools = None (List Text)",
" , tags = [] : List Text",
" , launch = None Launch",
" }"
]
-- | Example @prompt.md@ body emitted next to a freshly scaffolded
-- @prompt.dhall@. It includes one declared variable placeholder so
-- authors can verify prompt rendering through @seihou prompt run --debug@.
exampleAgentPromptMarkdown :: Text
exampleAgentPromptMarkdown =
T.unlines
[ "# {{project.name}}",
"",
"You are helping the user with a focused repository task.",
"",
"Ask before destructive changes such as deleting files, force-pushing,",
"or rewriting history."
]
-- | A new judgement with one check whose rubric is meant to be edited in
-- the @instructions@ field, rather than a vacuous "review carefully".
judgementDhall :: Text -> Text -> Text -> Text
judgementDhall name url hash =
T.unlines
[ "let S =",
" " <> url,
" " <> hash,
"",
"-- A judgement asks a model yes/no questions about the staged changes before",
"-- each commit. Edit the instructions to describe what this project cares about",
"-- and what is acceptable, then add labelled cases to examples.json.",
"--",
"-- Every threshold applies to the probability of the concern: at or above",
"-- blockAt the check blocks, at or below passAt it may pass, and in between it",
"-- needs human review. concernWhen says which answer is the concern.",
"in S.Judgement::{",
" , name = \"" <> name <> "\"",
" , version = Some \"0.1.0\"",
" , description = Some \"Checks staged changes before each commit\"",
" , instructions =",
" ''",
" You review a change that is about to be committed. Decide only the",
" question you are asked; ignore style and unrelated problems.",
"",
" Acceptable: synthetic test data, documented examples and placeholders.",
" A concern: replace this line with what this project must not commit.",
" ''",
" , evidence = S.JudgementEvidence::{ scopes = [ S.EvidenceScope.StagedChanges, S.EvidenceScope.CommitRange ] }",
" , checks =",
" [ S.Check::{",
" , name = \"concern-present\"",
" , question = \"Does this change add something the instructions describe as a concern?\"",
" , yesMeans = Some \"At least one shown line matches a concern in the instructions.\"",
" , noMeans = Some \"Everything shown is acceptable under the instructions.\"",
" , categories =",
" [ S.FindingCategory::{",
" , name = \"concern\"",
" , severity = S.Severity.High",
" , description = Some \"A place where the concern appears\"",
" }",
" ]",
" , blockAt = 0.7",
" , passAt = 0.3",
" }",
" ]",
" }"
]
-- | Synthetic labelled cases for 'judgementDhall''s single check.
judgementExamplesJson :: Text
judgementExamplesJson =
T.unlines
[ "{",
" \"version\": 1,",
" \"examples\": [",
" {",
" \"id\": \"benign-docs\",",
" \"description\": \"A documentation edit with nothing the instructions call a concern\",",
" \"files\": [{ \"path\": \"README.md\", \"content\": \"# Demo\\n\\nRun make test before opening a pull request.\\n\" }],",
" \"expect\": { \"concern-present\": \"passed\" }",
" },",
" {",
" \"id\": \"replace-me-concern\",",
" \"description\": \"Replace with a synthetic change this judgement must block\",",
" \"files\": [{ \"path\": \"notes/todo.md\", \"content\": \"Replace this file with a synthetic example of the concern.\\n\" }],",
" \"expect\": { \"concern-present\": \"blocked\" },",
" \"findings\": [{ \"check\": \"concern-present\", \"category\": \"concern\", \"path\": \"notes/todo.md\", \"line\": 1 }]",
" }",
" ]",
"}"
]