diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9398942..21fe5cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ # Contributing -Boatstack is a generated content distribution. Propose changes to workflow semantics, templates, evidence rules, or generated presentation in [Intelligence Flow](https://github.com/operatorstack/intelligence-flow/tree/b37bfc311634ab20082ce1d768f0c953530d72cc/labs/12-product-engineering-loop). +Boatstack is a generated content distribution. Propose changes to workflow semantics, templates, evidence rules, or generated presentation in [Intelligence Flow](https://github.com/operatorstack/intelligence-flow/tree/c99b5f302c89934cc18926e19c7f49eb5e6e01cf/labs/12-product-engineering-loop). The Boatstack repository receives product/runtime changes through a generated pull request. Review the PR's `UPSTREAM.json`, tests, adapter diff, and context-size change; do not hand-edit generated output on `main`. `.github/workflows` is the exception: it is Boatstack's executable control plane, excluded from scheduled projection and changed only through a separate manually reviewed Boatstack PR. diff --git a/UPSTREAM.json b/UPSTREAM.json index 8d00aa3..de92e16 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -12,14 +12,14 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "dde868caa9c38db8ceaa264ee034fdd33eb413e57b570845dfeb5d44c1e9d8d7", + "CONTRIBUTING.md": "d751bf5c5f125b5909a23fad896a4b4700370a9bf5659b15020bfc0a2da8f20d", "README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad", "assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63", "assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5", "assets/boatstack-portability.svg": "66dfdfa85db857b3bd18b32047a6975f1fbbfc4dc091158e8277193f9969a346", "boatstack/AGENTS.md": "bc76221e1fe90a91afbacd7c6bc9b41a70e6c10fc128c275a6a0b9bc094d9506", "boatstack/BUG-worktree-delivery-state.md": "02469cf51c3849dad5743783e248e5c04583e4240507fbef0e3f890cd6a95724", - "boatstack/SKILL.md": "4b25970267c3ce508bf2dc8cf6d25e443a6ea6a7b905c170e5dc081ff2fb2be3", + "boatstack/SKILL.md": "634be319004241bf6f6c6674b20350923e7fb7b6f8329ff0c1e31dabfd7df623", "boatstack/activation.go": "deb7712d20aed37371254596613bfaccfc05fcb59634408b03378d1a0bf693ea", "boatstack/agents/gemini.yaml": "cbf43b387399e456fa6178f86d83e6e35567e6142ff800f8de6ffca306fa963e", "boatstack/agents/openai.yaml": "68a30a60859556c5a26e16d184594ca243a6043d99c8cf7d66b5dd6d50a93cd1", @@ -42,10 +42,11 @@ "boatstack/capture_test.go": "63fa1177738081f1e862364d7a4257f5e259f8e9c36276ba1775b8085b277105", "boatstack/changelog.go": "6b06be7cd9738de29ba6e87aa2569f3b027a2e618b04524f5abd7abaa17945bf", "boatstack/changelog_test.go": "ce792f23a7fe1e09fb3096cd1314130a6ab69321d4877b12a8e994027541baf7", - "boatstack/cmd/boatstack-helper/coverage_conformance_test.go": "347810fec8cc65300ad58cf84570040a001f6dffd9d464f21038034bec6f00e9", + "boatstack/cmd/boatstack-helper/coverage_conformance_test.go": "f5a931d2b5cdb0d32af85acbbd499fa0f509ed36d62e772e6b6072e348200aca", "boatstack/cmd/boatstack-helper/flow.go": "0d41c7a86b49004e897f59551780220841d5941396202c81de97a4d52f593520", - "boatstack/cmd/boatstack-helper/main.go": "9e0712b0a3936a3066a33b9c97f92d8ebc07f6e200664b21e6a3af03a00d9f3b", + "boatstack/cmd/boatstack-helper/main.go": "81f10044f507a6d62646a2f0220595323823ab60f4921c2a0e925b88b0f2301a", "boatstack/cmd/boatstack-helper/main_test.go": "b36c52d6d5c9dd2428730de10ff18194b7e32a98722e41341c301c6f7a04cad5", + "boatstack/cmd/boatstack-helper/retro.go": "68b83e33ade5b5fec126c70ec798fdcbed22fda755dc1cad2758143fead8e187", "boatstack/command.go": "4726ac515dedab4947be7eb48f88c6cb8b53d674124504b69f03e6396b080ee8", "boatstack/command_test.go": "9f707abba3640add81c3e97ba7e72fedbf98f3394b1c060a9ca4b4a28e919968", "boatstack/compiled_artifact_resolution_test.go": "0748d67643263e698211eb04d46464e1dd3db15d94537f5fd5092b5aa689745b", @@ -130,8 +131,10 @@ "boatstack/internal/deliverycontrol/trajectorylog_test.go": "227dd6ed9ce181d517a37b67ef4d64dd93779a533eae804798ab54de35c7f13e", "boatstack/internal/deliverycontrol/transition.go": "b43abb0e99d29697b27b0bb8ee2e2f5f31f3471a2983f25d18ae3564ee246775", "boatstack/internal/retromine/adapters.go": "7736ebe7fe200dc6aca2799dab964a49031e4f840a049dc872f0bb62053a1ed8", + "boatstack/internal/retromine/classify.go": "060404bfbea3ccc7115dedea5af9c02e53f92c4042a027e68c17042ca996a2cf", "boatstack/internal/retromine/cluster.go": "ddb6b3506ac192ab37f0341e57437ddf365207c42163da423a06bbfd856758f5", "boatstack/internal/retromine/event.go": "4d41cbf37209d01bc5cc083d144f835950700aa7f6e39d0661b525b5e045227a", + "boatstack/internal/retromine/report.go": "60a2ecc1416d4b6d71e359aafba7e2a8a80b12dd20d733dddcd90fcd691a9155", "boatstack/internal/retromine/retromine_conformance_test.go": "33883893db1e455901d9c0e93767c39b475fae728aa9ee9dd925a2c21e5b0ec4", "boatstack/internal/retromine/testdata/session-alpha.jsonl": "45ed8fa691af9db3f957e87175dbbeffd8fdd2f001324e86a5fd68e29ffbe244", "boatstack/internal/retromine/testdata/session-beta.txt": "8e85e6a4442e3c892166df97ec879d998b3a47bdad5ef2bf785674b3149c36c2", @@ -184,6 +187,8 @@ "boatstack/release.go": "82dcb4ca59e8c79a68d5333d650f90e64abd448d04e0c6f504fdf07f42b5ed76", "boatstack/release_test.go": "5cf2d76fe9b836a91ca68eba53d5585e2c4be5b9421aaf939ea0723063a24690", "boatstack/repair_state_test.go": "f3779ac47c3db3927175a545728d3b2e020dbc85f41394d8235753b52afc3739", + "boatstack/retro.go": "8a6f13b948574c90d0f06c3b9f5570d08931e66a4c78dbcc208da8c696c2a42b", + "boatstack/retro_conformance_test.go": "827250d2fc49717fb5e58a4cf79e1d5c489c37574d8a2cf9348fa1cd8c328713", "boatstack/run.go": "3127e8c054b80e41413011e423ba9463fe06a7e6c9e1e71750ab98587871d89d", "boatstack/run_test.go": "5b291510fa90cefdc26eb89e18a3443385456a6ebc73408325ac1945b7c084d6", "boatstack/runtime.go": "368bb43a0e3042bde2d4bab1b62df560a93f5928384c7c8f5e27e8a836628af4", @@ -221,10 +226,10 @@ "docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6", "docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79", "docs/configuration.md": "221f979506a3a9de357e5277f1329c345bf175346ec8dfc8fdd1212fb100dea1", - "docs/evidence-engineered-coding.md": "2a2b905e044d89cad2855f10c45261e980b0507c60e275165c8de8ed2a663f42", + "docs/evidence-engineered-coding.md": "9e343b431d4fa013c094b252fb47eadcf06810f11c730538f1cafb19c0d802b7", "docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3", "docs/getting-started.md": "51c2823f21e35140d31e6d5083dc4b89fddd24721ac6acc474154a4da53ee9f8", - "docs/public-claims.json": "6f78d9f8b0ce76079f23a43d84008045dc7e51e5004a2f3e394784e44a3b752e", + "docs/public-claims.json": "b843e27a0ff7ce283a8ca5cdf7e54244c98dd551e9b35c0c9757845907dc6dd3", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", "docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", @@ -238,7 +243,7 @@ "labs/diagram-json/compiled/evidence.md": "1ba1c989ade070a8ef9a508fbd788d100d7292f2dbacbb2bce895468019f619d", "labs/diagram-json/compiled/tasks.json": "88f60851abf79d851e9fccc754ff3040034ae595306bc87d64784c19eb403e71", "labs/diagram-json/compiled/test-matrix.json": "424657ff505768e50fa113801fd8363364a18269d5297480907a993d44063a39", - "labs/diagram-json/plan.lock.json": "2c2182f842a9ab47814c685ad0762bb63da4859fc27f583a25a32597e97d7b79", + "labs/diagram-json/plan.lock.json": "356bd3d6143508b68469cd651410e773f32614f71f5eaf906aa47f3921428047", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -377,12 +382,13 @@ "release-notes/2026-07-28-post-publish-prescriptions.md": "a0b728df569bdba40e3b6107179b35873bad00e1ed1c2574e7eecf438b6962f4", "release-notes/2026-07-28-pr-phase-observation.md": "8c5615013eb88ce9561d30897e47f6fa967e0157c4c245f0c35a1f31e4e132e2", "release-notes/2026-07-28-protected-native-auto-merge.md": "67dc76a6e7ce51034a0eadc541ba7a8946cfcabe5433cedc25db55321dfb8b62", + "release-notes/2026-07-28-retro-derive-proposals.md": "c90797d76fb00816340475620b584ad150fa32d7d10bc14997a614529ef9b558", "release-notes/2026-07-28-retromine-recurrence-detector.md": "27790993a02e73f3a2700dce7340d044aeb0e52f785ded68271ff6673add9e25" }, "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "b37bfc311634ab20082ce1d768f0c953530d72cc", + "commit": "c99b5f302c89934cc18926e19c7f49eb5e6e01cf", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/SKILL.md b/boatstack/SKILL.md index f73a12c..a933250 100644 --- a/boatstack/SKILL.md +++ b/boatstack/SKILL.md @@ -254,6 +254,8 @@ This is a two-slice ZCA projection: the reviewer brief minimizes review effort, Read [failure-moves.md](references/failure-moves.md) before proposing a loop change. +For a retro over past sessions, run the read-only `.product-loop/bin/boatstack-helper retro derive --input [--input ...]`. It detects operator instructions that recur across sessions and classifies each as a missing observation, verb, setpoint, or guard, with a suggested typed promotion. It reads only the transcript files the user names, works fully offline, and writes nothing. A recurring instruction is evidence of a missing typed control — promote it by hand through the normal reviewed delivery flow; never turn it into a saved prompt, and never apply a proposal automatically. + 1. Classify the observed failure below the surface symptom. 2. State a mechanism and the exact failure population the move targets. 3. Estimate cost, risk, and possible regressions. diff --git a/boatstack/cmd/boatstack-helper/coverage_conformance_test.go b/boatstack/cmd/boatstack-helper/coverage_conformance_test.go index 3257765..cfce070 100644 --- a/boatstack/cmd/boatstack-helper/coverage_conformance_test.go +++ b/boatstack/cmd/boatstack-helper/coverage_conformance_test.go @@ -82,6 +82,10 @@ var nonDeliveryVerbs = map[string]bool{ "workspace-sync": true, // Flow layer itself is read-only navigation over the machine, not a transition. "flow": true, + // Retro derivation reads operator-supplied transcripts and proposes typed + // promotions; it mutates nothing, so it registers no delivery transition. + // control-law: retro-proposes-never-enforces + "retro": true, } // dispatchVerbs parses main.go and returns the set of command verbs the run() diff --git a/boatstack/cmd/boatstack-helper/main.go b/boatstack/cmd/boatstack-helper/main.go index cd1e014..076e1f3 100644 --- a/boatstack/cmd/boatstack-helper/main.go +++ b/boatstack/cmd/boatstack-helper/main.go @@ -1439,7 +1439,7 @@ func workspaceSyncCommand(arguments []string) int { func run() int { if len(os.Args) < 2 { - fmt.Fprintln(os.Stderr, "usage: boatstack-helper ") + fmt.Fprintln(os.Stderr, "usage: boatstack-helper ") return 2 } switch os.Args[1] { @@ -1553,6 +1553,8 @@ func run() int { return migrateConfigCommand(os.Args[2:]) case "flow": return flowCommand(os.Args[2:]) + case "retro": + return retroCommand(os.Args[2:]) case "version": fmt.Printf("Boatstack %s (%s)\n", boatstack.Version, boatstack.SourceCommit) return 0 diff --git a/boatstack/cmd/boatstack-helper/retro.go b/boatstack/cmd/boatstack-helper/retro.go new file mode 100644 index 0000000..8363674 --- /dev/null +++ b/boatstack/cmd/boatstack-helper/retro.go @@ -0,0 +1,66 @@ +package main + +import ( + "flag" + "fmt" + "os" + + boatstack "github.com/operatorstack/boatstack/boatstack" +) + +// retroCommand is the derive-only entry point for transcript mining. The CLI +// boundary owns the ONLY I/O in the pipeline: it reads the operator-supplied +// paths and prints the report to stdout. Below this boundary the derivation +// is capability-free (no filesystem, network, subprocess, or clock), and +// nothing anywhere in the pipeline writes, mutates state, or runs a command. +// control-law: retro-proposes-never-enforces +func retroCommand(arguments []string) int { + if len(arguments) == 0 || arguments[0] != "derive" { + fmt.Fprintln(os.Stderr, "usage: boatstack-helper retro derive --input [--input ...] [--format events|claudecode|plaintext] [--json]") + return 2 + } + flags := flag.NewFlagSet("retro derive", flag.ContinueOnError) + var inputs stringList + flags.Var(&inputs, "input", "transcript file to mine (repeatable)") + format := flags.String("format", "", "transcript format: events, claudecode, or plaintext (default: sniff per file)") + jsonOutput := flags.Bool("json", false, "print the structured derivation report") + if err := flags.Parse(arguments[1:]); err != nil { + return 2 + } + inputs = append(inputs, flags.Args()...) + if len(inputs) == 0 { + fmt.Fprintln(os.Stderr, "retro derive requires at least one --input transcript; Boatstack never scans for transcripts on its own") + return 2 + } + loaded := make([]boatstack.RetroInput, 0, len(inputs)) + for _, path := range inputs { + content, err := os.ReadFile(path) + if err != nil { + return fail(err) + } + loaded = append(loaded, boatstack.RetroInput{Name: path, Content: content}) + } + report, err := boatstack.RetroDerive(*format, loaded) + if err != nil { + return fail(err) + } + if *jsonOutput { + value, marshalErr := boatstack.MarshalJSON(report) + if marshalErr != nil { + return fail(marshalErr) + } + fmt.Print(string(value)) + } else { + fmt.Print(boatstack.FormatRetroReport(report)) + } + return 0 +} + +// stringList is a repeatable string flag. +type stringList []string + +func (s *stringList) String() string { return fmt.Sprint([]string(*s)) } +func (s *stringList) Set(value string) error { + *s = append(*s, value) + return nil +} diff --git a/boatstack/internal/retromine/classify.go b/boatstack/internal/retromine/classify.go new file mode 100644 index 0000000..37a5984 --- /dev/null +++ b/boatstack/internal/retromine/classify.go @@ -0,0 +1,94 @@ +package retromine + +import "strings" + +// Gap classification names WHICH typed construct a recurring instruction is +// compensating for. The four gap types are the four ways a controller can be +// missing a term: +// +// missing_observation — the operator keeps asking what the system could show +// missing_verb — the operator keeps describing an action to take +// missing_setpoint — the operator keeps restating a goal or condition to +// pursue ("until", "every time", "at least") +// missing_guard — the operator keeps warning what must not happen +// +// The classifier is a deterministic keyword lexicon over the normalized +// instruction, with fixed precedence guard > setpoint > observation > verb: +// a guard misclassified as a verb could become an action proposal, so the +// constraining readings win. Anything the lexicon cannot place lands in +// unclassified, which is REPORTED but never generates a proposal. +// control-law: retro-proposes-never-enforces +const ( + GapObservation = "missing_observation" + GapVerb = "missing_verb" + GapSetpoint = "missing_setpoint" + GapGuard = "missing_guard" + GapUnclassified = "unclassified" +) + +// The lexicons match either whole tokens or normalized phrases. Normalization +// has already lowered the text and stripped punctuation ("don't" → "don t"). +var ( + guardPhrases = []string{"don t", "do not", "make sure not", "must not", "never", "only if", "unless", "be careful", "avoid", "without asking", "instead of"} + guardTokens = []string{"dont", "stop"} + + setpointPhrases = []string{"until", "at least", "at most", "within", "every time", "each time", "whenever", "keep doing", "always", "from now on", "before you finish", "when green", "when it passes"} + + observationPhrases = []string{"check the", "check whether", "check if", "what is the", "what s the", "show me", "look at", "status of", "is it", "did it", "how is", "where is", "monitor"} + + verbTokens = []string{"run", "merge", "publish", "push", "rerun", "retry", "open", "record", "fix", "update", "deploy", "rebase", "commit", "create", "install", "sync", "clean", "make"} +) + +// ClassifyGap places one normalized instruction into a gap type. +func ClassifyGap(normalized string) string { + padded := " " + normalized + " " + containsPhrase := func(phrases []string) bool { + for _, phrase := range phrases { + if strings.Contains(padded, " "+phrase+" ") { + return true + } + } + return false + } + tokens := map[string]bool{} + for _, token := range strings.Fields(normalized) { + tokens[token] = true + } + containsToken := func(list []string) bool { + for _, token := range list { + if tokens[token] { + return true + } + } + return false + } + switch { + case containsPhrase(guardPhrases) || containsToken(guardTokens): + return GapGuard + case containsPhrase(setpointPhrases): + return GapSetpoint + case containsPhrase(observationPhrases): + return GapObservation + case containsToken(verbTokens): + return GapVerb + default: + return GapUnclassified + } +} + +// SuggestedShape names the typed construct to add for a gap type — prose +// pointing a human at the right kind of promotion, never a diff. +func SuggestedShape(gapType string) string { + switch gapType { + case GapObservation: + return "Add a typed observation: a read-only status or frontier field that answers this without being asked." + case GapVerb: + return "Add or prescribe a typed verb: a deterministic command the flow names at the right state." + case GapSetpoint: + return "Add a typed setpoint: a persisted goal or condition (like delivery.terminal) the flow pursues so this stops being restated." + case GapGuard: + return "Add a typed guard: an enforced precondition or denial (a gate or policy) instead of a remembered warning." + default: + return "" + } +} diff --git a/boatstack/internal/retromine/report.go b/boatstack/internal/retromine/report.go new file mode 100644 index 0000000..6305340 --- /dev/null +++ b/boatstack/internal/retromine/report.go @@ -0,0 +1,57 @@ +package retromine + +// The report is the miner's entire output surface: typed proposals for the +// classified recurrences, and the unclassified recurrences named so nothing +// is silently dropped. It is data for a human to review — the derivation +// proposes, and promotion into a real state, verb, setpoint, or guard is +// always a reviewed change made by hand. +// control-law: retro-proposes-never-enforces +const ReportSchemaVersion = 1 + +// Proposal is one recurring instruction promoted to a typed suggestion. +type Proposal struct { + GapType string `json:"gap_type"` + Occurrences int `json:"occurrences"` + Sessions []string `json:"sessions"` + Exemplar string `json:"exemplar"` + SuggestedShape string `json:"suggested_shape"` + Evidence []EventRef `json:"evidence"` +} + +// Report is the full derivation result over one set of transcripts. +type Report struct { + SchemaVersion int `json:"schema_version"` + EventsScanned int `json:"events_scanned"` + OperatorEvents int `json:"operator_events"` + Proposals []Proposal `json:"proposals"` + // Unclassified recurrences are surfaced — a recurrence the lexicon cannot + // place is still steady-state error worth a human look — but they never + // become proposals (fail-closed). + Unclassified []Cluster `json:"unclassified,omitempty"` +} + +// BuildReport mines the events and classifies every recurrence. +func BuildReport(events []Event) Report { + report := Report{SchemaVersion: ReportSchemaVersion, EventsScanned: len(events), Proposals: []Proposal{}} + for _, event := range events { + if event.Role == RoleOperator { + report.OperatorEvents++ + } + } + for _, cluster := range DetectRecurrence(events) { + gapType := ClassifyGap(cluster.Normalized) + if gapType == GapUnclassified { + report.Unclassified = append(report.Unclassified, cluster) + continue + } + report.Proposals = append(report.Proposals, Proposal{ + GapType: gapType, + Occurrences: cluster.Occurrences, + Sessions: cluster.Sessions, + Exemplar: cluster.Exemplar, + SuggestedShape: SuggestedShape(gapType), + Evidence: cluster.Evidence, + }) + } + return report +} diff --git a/boatstack/retro.go b/boatstack/retro.go new file mode 100644 index 0000000..8309766 --- /dev/null +++ b/boatstack/retro.go @@ -0,0 +1,61 @@ +package boatstack + +import ( + "fmt" + "strings" + + "github.com/operatorstack/boatstack/boatstack/internal/retromine" +) + +// RetroInput is one transcript handed to the retro derivation: a name (for +// evidence references and per-file session identity) and its raw content. +// The derivation layer takes bytes, never paths — every capability the miner +// lacks (filesystem, network, subprocess, clock) stays lacking here; only +// the CLI boundary reads files, from operator-supplied paths only. +// control-law: retro-derivation-is-offline-and-deterministic +type RetroInput struct { + Name string + Content []byte +} + +// RetroDerive parses every input with the named adapter format ("" sniffs +// per file: events | claudecode | plaintext) and mines the combined events +// for recurring operator instructions, classified into typed-gap proposals. +// It proposes only: no file is written, no state is touched, no command is +// run, and nothing is enforced — promotion is always a reviewed change made +// by hand. control-law: retro-proposes-never-enforces +func RetroDerive(format string, inputs []RetroInput) (retromine.Report, error) { + events := []retromine.Event{} + for _, input := range inputs { + parsed, err := retromine.ParseTranscript(format, input.Name, input.Content) + if err != nil { + return retromine.Report{}, err + } + events = append(events, parsed...) + } + return retromine.BuildReport(events), nil +} + +// FormatRetroReport renders the derivation for a human reviewer. +func FormatRetroReport(report retromine.Report) string { + var b strings.Builder + fmt.Fprintf(&b, "Retro derivation: %d event(s) scanned, %d from the operator.\n", + report.EventsScanned, report.OperatorEvents) + if len(report.Proposals) == 0 && len(report.Unclassified) == 0 { + b.WriteString("No recurring operator instruction found across sessions. Nothing to promote.\n") + return b.String() + } + for i, proposal := range report.Proposals { + fmt.Fprintf(&b, "\n%d. [%s] seen %d time(s) across %d session(s)\n", i+1, + proposal.GapType, proposal.Occurrences, len(proposal.Sessions)) + fmt.Fprintf(&b, " Instruction: %q\n", proposal.Exemplar) + fmt.Fprintf(&b, " Promote it: %s\n", proposal.SuggestedShape) + } + for _, cluster := range report.Unclassified { + fmt.Fprintf(&b, "\n?. [unclassified] seen %d time(s) across %d session(s): %q\n", + cluster.Occurrences, len(cluster.Sessions), cluster.Exemplar) + b.WriteString(" Recurs, but no gap type matched; review it by hand. No proposal is generated.\n") + } + b.WriteString("\nDerivation proposes; it never enforces. Promote a proposal by hand through the normal reviewed delivery flow.\n") + return b.String() +} diff --git a/boatstack/retro_conformance_test.go b/boatstack/retro_conformance_test.go new file mode 100644 index 0000000..8979e60 --- /dev/null +++ b/boatstack/retro_conformance_test.go @@ -0,0 +1,140 @@ +package boatstack + +// control-law: retro-proposes-never-enforces +// +// `retro derive` closes the loop the whole program serves: a recurring +// operator instruction is steady-state error, and the remedy is a TYPED +// promotion — an observation, verb, setpoint, or guard — never a saved +// prompt and never an automatic change. The derivation therefore only ever +// produces a report: it writes no file, mutates no state, runs no command, +// and an unclassifiable recurrence is surfaced without a proposal +// (fail-closed). Below the CLI's read-only file loading, the pipeline is +// capability-free (pinned structurally in the retromine conformance suite). +// +// Test classes: positive (each gap type classifies from planted recurring +// phrasing, with a suggested typed shape), negative (an unmatched recurrence +// lands in unclassified with zero proposals), bypass (derivation leaves the +// filesystem byte-identical), failure-state (empty input → empty report; +// a malformed transcript is a typed error, not a partial report). + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" +) + +func neutralTranscript(instruction string) []byte { + var b strings.Builder + for _, session := range []string{"s1", "s2", "s3"} { + fmt.Fprintf(&b, `{"session_id":%q,"role":"operator","text":%q}`+"\n", session, instruction) + fmt.Fprintf(&b, `{"session_id":%q,"role":"agent","text":"done"}`+"\n", session) + } + return []byte(b.String()) +} + +// Positive: each gap type classifies from its phrasing and carries a +// suggested typed shape. +func TestRetroDeriveClassifiesEachGapType(t *testing.T) { + for _, test := range []struct { + instruction string + wantGap string + }{ + {"never force push to the main branch", "missing_guard"}, + {"watch the checks until every one passes then merge", "missing_setpoint"}, + {"check the status of the deployment pipeline", "missing_observation"}, + {"run the full test suite again please", "missing_verb"}, + } { + t.Run(test.wantGap, func(t *testing.T) { + report, err := RetroDerive("events", []RetroInput{{Name: "t.jsonl", Content: neutralTranscript(test.instruction)}}) + if err != nil { + t.Fatal(err) + } + if len(report.Proposals) != 1 { + t.Fatalf("proposals = %#v, want exactly one", report.Proposals) + } + proposal := report.Proposals[0] + if proposal.GapType != test.wantGap { + t.Fatalf("gap = %q, want %q", proposal.GapType, test.wantGap) + } + if proposal.Occurrences != 3 || len(proposal.Sessions) != 3 { + t.Fatalf("unexpected recurrence evidence: %#v", proposal) + } + if proposal.SuggestedShape == "" { + t.Fatal("proposal carries no suggested typed shape") + } + rendered := FormatRetroReport(report) + if !strings.Contains(rendered, test.wantGap) || !strings.Contains(rendered, "never enforces") { + t.Fatalf("rendering incomplete:\n%s", rendered) + } + }) + } +} + +// Negative: a recurrence the lexicon cannot place is surfaced as +// unclassified and generates zero proposals. +func TestUnclassifiedRecurrenceGeneratesNoProposal(t *testing.T) { + report, err := RetroDerive("events", []RetroInput{{Name: "t.jsonl", Content: neutralTranscript("the quarterly numbers look pretty good overall")}}) + if err != nil { + t.Fatal(err) + } + if len(report.Proposals) != 0 { + t.Fatalf("unclassified recurrence produced proposals: %#v", report.Proposals) + } + if len(report.Unclassified) != 1 { + t.Fatalf("unclassified recurrence not surfaced: %#v", report) + } + if rendered := FormatRetroReport(report); !strings.Contains(rendered, "No proposal is generated") { + t.Fatalf("unclassified recurrence not explained:\n%s", rendered) + } +} + +// Bypass: derivation leaves the filesystem byte-identical — it consumes +// bytes and produces a report, nothing else. +func TestRetroDeriveWritesNothing(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "transcript.jsonl") + if err := os.WriteFile(path, neutralTranscript("never force push to the main branch"), 0o644); err != nil { + t.Fatal(err) + } + content, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if _, err := RetroDerive("", []RetroInput{{Name: path, Content: content}}); err != nil { + t.Fatal(err) + } + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatal(err) + } + if len(entries) != 1 { + t.Fatalf("derivation changed the directory: %v", entries) + } + after, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if string(after) != string(content) { + t.Fatal("derivation modified its input") + } +} + +// Failure-state: empty input yields an empty report; a malformed transcript +// is a typed error, never a partial report. +func TestRetroDeriveFailureStates(t *testing.T) { + report, err := RetroDerive("events", nil) + if err != nil { + t.Fatal(err) + } + if report.EventsScanned != 0 || len(report.Proposals) != 0 { + t.Fatalf("empty input produced content: %#v", report) + } + if rendered := FormatRetroReport(report); !strings.Contains(rendered, "Nothing to promote") { + t.Fatalf("empty report not explained:\n%s", rendered) + } + if _, err := RetroDerive("events", []RetroInput{{Name: "bad.jsonl", Content: []byte("not json\n")}}); err == nil { + t.Fatal("malformed transcript accepted") + } +} diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index 50bf412..64de45a 100644 --- a/docs/evidence-engineered-coding.md +++ b/docs/evidence-engineered-coding.md @@ -146,6 +146,6 @@ Delivery and system improvement also remain separate. A failed task may suggest ## What is evidence-backed -The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`b37bfc311634ab20082ce1d768f0c953530d72cc`](https://github.com/operatorstack/intelligence-flow/tree/b37bfc311634ab20082ce1d768f0c953530d72cc/labs/12-product-engineering-loop). +The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`c99b5f302c89934cc18926e19c7f49eb5e6e01cf`](https://github.com/operatorstack/intelligence-flow/tree/c99b5f302c89934cc18926e19c7f49eb5e6e01cf/labs/12-product-engineering-loop). The evidence supports specific failure mechanisms and guardrails. It does not establish that Boatstack is optimal, that control-theory notation proves software quality, or that one workflow dominates every team. Those are evaluation questions, so the distribution preserves measurements, provenance, gaps, and negative results. diff --git a/docs/public-claims.json b/docs/public-claims.json index f7e6236..fc11539 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "b37bfc311634ab20082ce1d768f0c953530d72cc", + "source_commit": "c99b5f302c89934cc18926e19c7f49eb5e6e01cf", "statuses": ["verified", "observed", "still_being_evaluated"], "claims": [ { @@ -12,7 +12,7 @@ "readable_evidence": "why-these-steps.md#portable-workflow-and-state", "implementation": ["../boatstack/export.go", "../boatstack/references/artifacts.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go"], - "last_verified_version": "source:b37bfc311634ab20082ce1d768f0c953530d72cc" + "last_verified_version": "source:c99b5f302c89934cc18926e19c7f49eb5e6e01cf" }, { "id": "human-decisions", @@ -23,7 +23,7 @@ "readable_evidence": "why-these-steps.md#human-decisions", "implementation": ["../boatstack/references/workflow.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:b37bfc311634ab20082ce1d768f0c953530d72cc" + "last_verified_version": "source:c99b5f302c89934cc18926e19c7f49eb5e6e01cf" }, { "id": "validation-provenance", @@ -34,7 +34,7 @@ "readable_evidence": "why-these-steps.md#validation-provenance", "implementation": ["validation-and-evidence.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go"], - "last_verified_version": "source:b37bfc311634ab20082ce1d768f0c953530d72cc" + "last_verified_version": "source:c99b5f302c89934cc18926e19c7f49eb5e6e01cf" }, { "id": "irreversible-operations", @@ -46,7 +46,7 @@ "readable_evidence": "why-these-steps.md#irreversible-operations", "implementation": ["safety.md", "../boatstack/safety.go", "../boatstack/hooks.go"], "verification": ["../boatstack/safety_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:b37bfc311634ab20082ce1d768f0c953530d72cc" + "last_verified_version": "source:c99b5f302c89934cc18926e19c7f49eb5e6e01cf" }, { "id": "reviewer-ready-pr", @@ -57,7 +57,7 @@ "readable_evidence": "why-these-steps.md#reviewer-ready-pr", "implementation": ["../boatstack/pr.go", "getting-started.md"], "verification": ["../boatstack/pr_test.go"], - "last_verified_version": "source:b37bfc311634ab20082ce1d768f0c953530d72cc" + "last_verified_version": "source:c99b5f302c89934cc18926e19c7f49eb5e6e01cf" }, { "id": "phase-scoped-delivery", @@ -68,7 +68,7 @@ "readable_evidence": "why-these-steps.md#phase-scoped-delivery", "implementation": ["../boatstack/delivery.go", "../boatstack/safety.go", "../boatstack/hooks.go", "../boatstack/references/workflow.md"], "verification": ["../boatstack/delivery_test.go", "../boatstack/pr_test.go"], - "last_verified_version": "source:b37bfc311634ab20082ce1d768f0c953530d72cc" + "last_verified_version": "source:c99b5f302c89934cc18926e19c7f49eb5e6e01cf" }, { "id": "model-neutral-contract", @@ -79,7 +79,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:b37bfc311634ab20082ce1d768f0c953530d72cc" + "last_verified_version": "source:c99b5f302c89934cc18926e19c7f49eb5e6e01cf" }, { "id": "cross-model-failures", @@ -90,7 +90,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:b37bfc311634ab20082ce1d768f0c953530d72cc" + "last_verified_version": "source:c99b5f302c89934cc18926e19c7f49eb5e6e01cf" }, { "id": "lower-cost-outcomes", @@ -101,7 +101,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:b37bfc311634ab20082ce1d768f0c953530d72cc" + "last_verified_version": "source:c99b5f302c89934cc18926e19c7f49eb5e6e01cf" }, { "id": "git-worktree-activation", @@ -112,7 +112,7 @@ "readable_evidence": "why-these-steps.md#git-worktree-activation", "implementation": ["../boatstack/runtime_cache.go", "../boatstack/hooks.go"], "verification": ["../boatstack/runtime_cache_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:b37bfc311634ab20082ce1d768f0c953530d72cc" + "last_verified_version": "source:c99b5f302c89934cc18926e19c7f49eb5e6e01cf" }, { "id": "visible-updates", @@ -123,7 +123,7 @@ "readable_evidence": "why-these-steps.md#visible-updates", "implementation": ["../boatstack/update.go", "../boatstack/init.go"], "verification": ["../boatstack/update_test.go", "../boatstack/init_test.go", "../boatstack/export_test.go"], - "last_verified_version": "source:b37bfc311634ab20082ce1d768f0c953530d72cc" + "last_verified_version": "source:c99b5f302c89934cc18926e19c7f49eb5e6e01cf" } ] } diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index 878af88..99a5162 100644 --- a/labs/diagram-json/plan.lock.json +++ b/labs/diagram-json/plan.lock.json @@ -6,7 +6,7 @@ "plan_path": "labs/diagram-json/plan.md", "plan_sha256": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "schema_version": 1, - "source_commit": "b37bfc311634ab20082ce1d768f0c953530d72cc", + "source_commit": "c99b5f302c89934cc18926e19c7f49eb5e6e01cf", "source_plan_path": "labs/diagram-json/source-plan.md", "source_plan_sha256": "e10593ddaa7522ab80cc991d0a09399257139799e37f737794cd49d68a39985b", "spec_path": "labs/diagram-json/spec.md", diff --git a/release-notes/2026-07-28-retro-derive-proposals.md b/release-notes/2026-07-28-retro-derive-proposals.md new file mode 100644 index 0000000..72195cb --- /dev/null +++ b/release-notes/2026-07-28-retro-derive-proposals.md @@ -0,0 +1,5 @@ +### `retro derive` turns your repeated instructions into reviewable proposals + +The new `retro derive` command reads the transcript files you name — Claude Code sessions, plain-text logs, or a neutral event format — finds the instructions you keep giving across sessions, and classifies each one as a missing observation, verb, setpoint, or guard, with a suggested typed promotion. An instruction the classifier cannot place is still shown, marked unclassified, and generates no proposal. + +The command only proposes: it writes no file, changes no state, and runs nothing, and it reads only the transcripts you explicitly pass — Boatstack never scans for transcripts on its own. Promote a proposal by hand through the normal reviewed delivery flow. The idea behind it: an instruction you keep repeating is evidence your system is missing a typed control, and the fix is to add that control — not to save the prompt.