Skip to content

feat(decisions): promote inbound decisions to first-class decision records - #651

Draft
DKuleshov wants to merge 6 commits into
mainfrom
claude/eager-panini-b73c04
Draft

feat(decisions): promote inbound decisions to first-class decision records#651
DKuleshov wants to merge 6 commits into
mainfrom
claude/eager-panini-b73c04

Conversation

@DKuleshov

Copy link
Copy Markdown
Collaborator

Linked issue

Closes #416

Summary of changes

Promotes inbound decisions to first-class decision records. Adds Dotbot.Decision with 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:

Source Hook site Tag
mothership answers NotificationPoller.psm1 (needs-input) + Dotbot.Task.psm1 (interview loop) inbound:mothership
workflow registry workflow-add.ps1 / workflow-remove.ps1 inbound:registry
material settings SettingsAPI.psm1 inbound:settings

Per 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, accepted by default and proposed for free-text (Q2); failures are best-effort, never atomic-abort (Q3); dedup bookkeeping lives under gitignored .bot/.control/ with external_ref.key on the committed record as the durable source of truth (Q5); the question-type mapping is a rigid table (Q8). Mothership decisions carry related_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.ps1 covers the funnel (all three sources, idempotency/dedup, the settings allow-list including negative cases, and option-key to "<key> - <label>" resolution) and the enter-needs-input transition hook.

pwsh tests/Run-Tests.ps1 -Layer 2

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.

# Gap Symptom Closed by
1 needs-input questions were never dispatched to mothership Task paused, no question ever reached Teams, so no answer existed to funnel. Mothership leg untestable. 5f61593
2 decision_* MCP tools resolved state off cwd (the worktree), not the main state root Funnel wrote to main while the task read the worktree. Product Documents listed 0 accepted decisions, then re-derived them. Contradicts #515. b0ed52c
3 Funnel and 01-plan-product Phase 4 both recorded the same answer Duplicate decisions per answered question (observed 11 records, 4 of them dups; after the fix 7, 0 dups). 25c83cd
4 Interview prompt template never loaded - hardcoded <BotRoot>/recipes/prompts/00-interview.md, which no content layer ships Interview ran with an empty template, so the model emitted options as strings and the validator rejected clarification-questions.json. Interview task failed; interview leg untestable. e8c998b
5 Free-text reply to a singleChoice question resolved to null Get-NotificationEnvelopeAnswer projected only selectedKey. A typed (not clicked) answer was dropped, so the interview loop waited forever at 2/3 answers and recorded no decision for that question. 3fbed92

Core implementation: 16790e4.

E2E coverage

All three inbound sources exercised against a live mothership (Teams) on a sample project.

Source Trigger Expected Result
mothership - needs-input Task pauses, question to Teams, answer it One inbound:mothership decision per answer pass
mothership - interview loop Run an interview workflow, answer 3 questions in Teams (mix button + free text) 3 inbound:mothership decisions, workflow proceeds pass
registry dotbot workflow add <name>, then remove One inbound:registry per action pass
registry dedup Repeat the same add No second decision (key registry:add::<name>) pass
settings Change a material key (e.g. mothership.enabled) One inbound:settings decision pass
settings negative Change a non-allow-listed key (costs.*, editor.*, logging.*) No decision pass

Reproduce

  1. Init a sample project against this branch (DOTBOT_HOME = this checkout) with mothership enabled.
  2. Run an interview-type workflow. Answer the questions in Teams - answer at least one with free text rather than a button (that is gap 5).
  3. Verify one inbound:mothership decision per answer in .bot/workspace/decisions/ of the main repo (not the worktree - gap 2), each with related_task_ids set and option answers rendered as "B - <label>" rather than a bare key.
  4. Confirm no duplicate decision for any answered question (gap 3).
  5. dotbot workflow add <name> then remove -> two inbound:registry decisions; repeat the add -> no third.
  6. Change a material setting -> one inbound:settings; change a non-listed key -> none.

Evidence from the last full run

3 questions asked; answered B, E (buttons) and simple +-/* (free text) -> 3 inbound:mothership decisions, related_task_ids populated, and the run proceeded through both downstream tasks. Audit logging on decision_get / decision_list shows each task surfacing and reading the decision set.

Checklist

  • Tests added or updated
  • Docs updated (if behaviour changed)
  • Linked issue exists
  • Follows the contribution guide

DKuleshov and others added 6 commits July 10, 2026 14:49
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Decision with New-DecisionRecord (write primitive) and New-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.
}
@DKuleshov

Copy link
Copy Markdown
Collaborator Author

It is preliminary draft PR. Real destination 4.1 branch. Will be rebased / closed on to 4.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Inbox

Development

Successfully merging this pull request may close these issues.

Promote inbound decisions to first-class decision records

2 participants