feat(decisions): promote inbound decisions to first-class decision records - #651
Draft
DKuleshov wants to merge 6 commits into
Draft
feat(decisions): promote inbound decisions to first-class decision records#651DKuleshov wants to merge 6 commits into
DKuleshov wants to merge 6 commits into
Conversation
…ons to mothership
…istency across tools
…und funnel responses
Interview is a runtime task type, so its prompt lives globally at content/prompts/00-interview.md and resolves through the content hierarchy (project override, then user, then framework/DOTBOT_HOME) via Resolve-DotbotContentReference. The loop previously read a hardcoded recipes/prompts/00-interview.md that no layer ships, so it ran with an empty template and the model produced an invalid clarification-questions.json (options as strings). A missing template now throws instead of running with no guidance. Add content/prompts/00-interview.md carrying the clarification-question schema: options as objects, key one of A-E and unique, 2-5 options per question, recommendation matching an option key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Get-NotificationEnvelopeAnswer projected only selectedKey for a singleChoice question, so a free-text reply (no button click, freeText populated) resolved to null. Resolve-NotificationAnswer then returned null, the interview loop never counted the answer, and it waited indefinitely while recording no inbound decision for that question. Fall back to freeText when selectedKey is absent, matching the untyped default branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Promotes inbound “decision-shaped” events (mothership answers, workflow add/remove, material settings changes) into first-class decision records via a new Dotbot.Decision module, and wires the funnel into the runtime/UI/CLI so decisions are captured best-effort without blocking primary actions.
Changes:
- Added
Dotbot.DecisionwithNew-DecisionRecord(write primitive) andNew-InboundDecision(mapping + idempotency/dedup funnel). - Wired inbound decision recording into mothership polling + interview loop, workflow add/remove, and settings saves; added tests for funnel + needs-input dispatch hook.
- Updated decision MCP tools/state handling and refreshed prompts/docs to reflect single-session execution and inbound decision visibility/dedup.
Reviewed changes
Copilot reviewed 37 out of 37 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/Test-WorkflowManifest.ps1 | Updates prompt assertions to the new single-session prompt and removes obsolete 98/99 prompt checks. |
| tests/Test-Components.ps1 | Adds component tests for inbound decision funnel behavior and the enter-needs-input transition hook. |
| src/ui/modules/SettingsAPI.psm1 | Diffs prior vs new overrides and routes changed settings keys through the inbound decision funnel (best-effort). |
| src/ui/modules/NotificationPoller.psm1 | Funnels mothership answers into decision records after task mutation. |
| src/runtime/Scripts/Invoke-WorkflowProcess.ps1 | Adds state_root to executor run context and improves executor failure logging to activity/logs. |
| src/runtime/Plugins/Hooks/Transitions/enter-needs-input/script.ps1 | New transition hook to dispatch pending questions to mothership and persist notification correlation data. |
| src/runtime/Plugins/Hooks/Transitions/enter-needs-input/metadata.json | Registers the new needs-input transition hook as best-effort (non-aborting). |
| src/runtime/Plugins/Executors/interview/script.ps1 | Ensures child runspace has module autoload path + pins globals/env so interview IO and decision funnel resolve correctly. |
| src/runtime/Modules/Dotbot.Task/Dotbot.Task.psm1 | Fixes interview prompt resolution via content resolver and funnels interview answers to decisions (best-effort). |
| src/runtime/Modules/Dotbot.Runtime/Private/HttpServer.psm1 | Threads TaskPath into run context for transition hooks that need to persist task mutations. |
| src/runtime/Modules/Dotbot.Notification/Private/Envelope.ps1 | Fixes singleChoice envelope parsing to fall back to free-text when no selectedKey exists. |
| src/runtime/Modules/Dotbot.Decision/Private/Imports.ps1 | Adds global imports for core path/state helpers required by the decision module. |
| src/runtime/Modules/Dotbot.Decision/Dotbot.Decision.psm1 | New decision write primitive + inbound decision funnel (mapping, dedup, cache bookkeeping). |
| src/runtime/Modules/Dotbot.Decision/Dotbot.Decision.psd1 | Module manifest for Dotbot.Decision. |
| src/runtime/Modules/Dotbot.Content/Dotbot.Content.psm1 | Updates inline docs to reference the new 100-single-session task prompt. |
| src/README.md | Updates framework description to single-session execution. |
| src/packaging/scoop/dotbot.json | Updates package description to single-session execution. |
| src/packaging/homebrew/dotbot.rb | Updates formula description to single-session execution. |
| src/mcp/tools/decision-update/script.ps1 | Uses resolved state bot root ($global:DotbotBotRoot fallback) for decision mutations. |
| src/mcp/tools/decision-mark-superseded/script.ps1 | Uses resolved state bot root for decision mutations. |
| src/mcp/tools/decision-mark-deprecated/script.ps1 | Uses resolved state bot root for decision mutations. |
| src/mcp/tools/decision-mark-accepted/script.ps1 | Uses resolved state bot root for decision mutations. |
| src/mcp/tools/decision-list/script.ps1 | Uses resolved state bot root and adds audit logging for decision scans. |
| src/mcp/tools/decision-get/script.ps1 | Uses resolved state bot root and adds audit logging for per-decision reads. |
| src/mcp/tools/decision-create/script.ps1 | Delegates all record generation to New-DecisionRecord in Dotbot.Decision. |
| src/cli/workflow-remove.ps1 | Records workflow removal via inbound decision funnel (best-effort). |
| src/cli/workflow-add.ps1 | Records workflow adoption via inbound decision funnel (best-effort). |
| README.md | Updates top-level README to describe single-session execution. |
| content/workflows/start-from-prompt/prompts/01b-generate-decisions.md | Updates dedup guidance to account for inbound mothership decisions. |
| content/workflows/start-from-prompt/prompts/01-plan-product.md | Updates Phase 4 decision recording guidance to avoid duplicating funnel-captured mothership answers. |
| content/workflows/start-from-jira/prompts/99-autonomous-task.md | Removes obsolete multi-repo autonomous override prompt. |
| content/workflows/start-from-jira/prompts/98-analyse-task.md | Removes obsolete multi-repo analysis override prompt. |
| content/prompts/99-autonomous-task.md | Removes obsolete two-phase execution prompt. |
| content/prompts/98-analyse-task.md | Removes obsolete two-phase analysis prompt. |
| content/prompts/100-single-session-task.md | Adds decision-loading instructions and expands Bash-vs-PowerShell guidance; includes inbound decisions. |
| content/prompts/00-interview.md | Adds interview prompt template with strict clarification-questions schema guidance. |
| AGENTS.md | Updates repository agent guidance to single-session execution model. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+230
to
+247
| $dir = Get-InboundDedupDir -BotPath $BotPath | ||
| if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } | ||
| $cacheFile = Join-Path $dir ((Get-InboundKeyHash -Key $Key) + ".json") | ||
|
|
||
| $payload = @{ key = $Key; decision_id = $DecisionId; written_at = (Get-Date).ToUniversalTime().ToString("o") } | | ||
| ConvertTo-Json -Depth 5 | ||
| $bytes = [System.Text.Encoding]::UTF8.GetBytes($payload) | ||
|
|
||
| try { | ||
| $fs = [System.IO.File]::Open($cacheFile, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None) | ||
| try { | ||
| $fs.Write($bytes, 0, $bytes.Length) | ||
| } finally { | ||
| $fs.Close() | ||
| } | ||
| } catch [System.IO.IOException] { | ||
| # Another run created the cache file first -- harmless. | ||
| } |
Collaborator
Author
|
It is preliminary draft PR. Real destination 4.1 branch. Will be rebased / closed on to 4.1. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linked issue
Closes #416
Summary of changes
Promotes inbound decisions to first-class decision records. Adds
Dotbot.Decisionwith two primitives:New-DecisionRecord- the write primitive.New-InboundDecision- the funnel. Maps an external event to a decision record, owns idempotency/dedup, and is best-effort (logs and swallows its own failures so the originating action always completes, per Promote inbound decisions to first-class decision records #416 Q3).Wired at three inbound sources:
NotificationPoller.psm1(needs-input) +Dotbot.Task.psm1(interview loop)inbound:mothershipworkflow-add.ps1/workflow-remove.ps1inbound:registrySettingsAPI.psm1inbound:settingsPer the issue's resolved questions: settings use a narrow per-key allow-list (Q1) and are append-only with no supersede-on-change (Q4/Q6); mothership records are created one-shot when the answer lands,
acceptedby default andproposedfor free-text (Q2); failures are best-effort, never atomic-abort (Q3); dedup bookkeeping lives under gitignored.bot/.control/withexternal_ref.keyon the committed record as the durable source of truth (Q5); the question-type mapping is a rigid table (Q8). Mothership decisions carryrelated_task_ids, and the task prompt filters the decision log by task relevance rather than dumping the whole accepted set.Decision state resolves to the main repo, consistent with #515.
Testing notes
Unit
tests/Test-Components.ps1covers the funnel (all three sources, idempotency/dedup, the settings allow-list including negative cases, and option-key to"<key> - <label>"resolution) and theenter-needs-inputtransition hook.E2E gaps and the commits that closed them
The funnel could not be tested end-to-end as-authored. Each gap below blocked a leg of the e2e path and is closed by a commit in this PR.
5f61593decision_*MCP tools resolved state off cwd (the worktree), not the main state rootb0ed52c01-plan-productPhase 4 both recorded the same answer25c83cd<BotRoot>/recipes/prompts/00-interview.md, which no content layer shipsoptionsas strings and the validator rejectedclarification-questions.json. Interview task failed; interview leg untestable.e8c998bsingleChoicequestion resolved tonullGet-NotificationEnvelopeAnswerprojected onlyselectedKey. A typed (not clicked) answer was dropped, so the interview loop waited forever at 2/3 answers and recorded no decision for that question.3fbed92Core implementation:
16790e4.E2E coverage
All three inbound sources exercised against a live mothership (Teams) on a sample project.
inbound:mothershipdecision per answerinbound:mothershipdecisions, workflow proceedsdotbot workflow add <name>, thenremoveinbound:registryper actionaddregistry:add::<name>)mothership.enabled)inbound:settingsdecisioncosts.*,editor.*,logging.*)Reproduce
DOTBOT_HOME= this checkout) with mothership enabled.inbound:mothershipdecision per answer in.bot/workspace/decisions/of the main repo (not the worktree - gap 2), each withrelated_task_idsset and option answers rendered as"B - <label>"rather than a bare key.dotbot workflow add <name>thenremove-> twoinbound:registrydecisions; repeat theadd-> no third.inbound:settings; change a non-listed key -> none.Evidence from the last full run
3 questions asked; answered
B,E(buttons) andsimple +-/*(free text) -> 3inbound:mothershipdecisions,related_task_idspopulated, and the run proceeded through both downstream tasks. Audit logging ondecision_get/decision_listshows each task surfacing and reading the decision set.Checklist