Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,32 @@ All notable changes to the SIN-Code unified binary will be documented in this fi

## [Unreleased] - 2026-06-16

### Added — `sin-code grill` (issue #141 fusion, native implementation)
- **New package** `cmd/sin-code/internal/grill/` with 4 source
files (`types.go`, `catalog.go`, `manager.go`, `grill_test.go`)
+ 14 race-clean unit tests. The native Go implementation of
the external `SIN-Code-Grill-Me-Skill` Python MCP server
(38 KB). Ships in v0 with JSON-file storage; SQLite session
storage is a v1 follow-up.
- **New subcommand** `sin-code grill` (5 subcommands):
- `grill start <topic>` — begin a grilling session, print the id
- `grill next <id>` — ask the next adversarial question
- `grill answer <id> <d-id> <text>` — record the response
(use "done" to resolve a decision)
- `grill status <id>` — show resolved + open decisions
- `grill synthesize <id> [--json]` — produce a structured
summary of decisions, assumptions, and open questions
- **Question catalog** — 8 seed anti-patterns (Hidden
Assumptions, Rollback Plan, Failure Modes, Operator Cost,
Premature Optimization, Scope Creep, Single Point of Failure,
Verification Gap). Each has 2-3 example questions. The CLI
picks one per `grill next` call (hash-seeded for determinism).
- **Storage** — `$SIN_CODE_HOME/grill/<id>.json`, atomic writes
(temp + rename). v1 will migrate to SQLite via the existing
`internal/session/store`.
- **14 race-clean tests** covering the full flow (Start → Next →
Answer → Synthesize → round-trip across restarts).

### Added — `sin-code goal` fusion (issue #140, v0.5)
- **Four new subcommands** under `sin-code goal` (issue #140 fusion
with the external `SIN-Code-Goal-Mode-Skill` Python MCP server):
Expand Down
249 changes: 249 additions & 0 deletions cmd/sin-code/grill_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
// SPDX-License-Identifier: MIT
// Purpose: `sin-code grill` — native adversarial design-review interview
// (issue #141 fusion with the external SIN-Code-Grill-Me-Skill
// Python MCP server). Replaces the bundled SKILL.md doc with a
// real subcommand.
//
// Subcommands:
//
// sin-code grill start <topic> begin a grilling session

Check failure on line 9 in cmd/sin-code/grill_cmd.go

View workflow job for this annotation

GitHub Actions / golangci-lint

File is not properly formatted (gofmt)
// sin-code grill next <id> ask the next adversarial question
// sin-code grill answer <id> <d-id> <text> record the operator's response
// sin-code grill status <id> show resolved + open decision branches
// sin-code grill synthesize <id> produce a summary of decisions
//
// Sessions are stored in $SIN_CODE_HOME/grill/<id>.json as
// content-addressed JSON files. v0 ships in-memory + JSON storage;
// the issue's "SQLite session/ledger DB" target is a follow-up
// (issue #141 follow-up #1: rewire to internal/session/store).
//
// Docs: cmd/sin-code/internal/grill/grill.doc.md
package main

import (
"encoding/json"
"fmt"
"os"
"path/filepath"

"github.com/spf13/cobra"

"github.com/OpenSIN-Code/SIN-Code/cmd/sin-code/internal/grill"
)

// grillDir returns the directory for grilling sessions. Honors
// SIN_CODE_HOME, then XDG_DATA_HOME, then ~/.local/share/sin-code/grill.
func grillDir() (string, error) {
if v := os.Getenv("SIN_CODE_HOME"); v != "" {
return filepath.Join(v, "grill"), nil
}
if v := os.Getenv("XDG_DATA_HOME"); v != "" {
return filepath.Join(v, "sin-code", "grill"), nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".local", "share", "sin-code", "grill"), nil
}

// NewGrillCmd builds the `grill` cobra subcommand.
func NewGrillCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "grill",
Short: "Native adversarial design-review interview (issue #141 fusion)",
Long: `sin-code grill is the native Go implementation of the
external SIN-Code-Grill-Me-Skill Python MCP server. Use it to
stress-test a plan, design, or decision before building it.

Subcommands:
start <topic> begin a grilling session, print the session id
next <id> ask the next adversarial question
answer <id> <d-id> <text> record the operator's response
(use "done" to resolve a decision)
status <id> show resolved + open decision branches
synthesize <id> produce a summary of decisions + assumptions`,
}
cmd.AddCommand(newGrillStartCmd())
cmd.AddCommand(newGrillNextCmd())
cmd.AddCommand(newGrillAnswerCmd())
cmd.AddCommand(newGrillStatusCmd())
cmd.AddCommand(newGrillSynthesizeCmd())
return cmd
}

func newGrillStartCmd() *cobra.Command {
return &cobra.Command{
Use: "start <topic>",
Short: "Begin a grilling session on the given topic",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
dir, err := grillDir()
if err != nil {
return err
}
m, err := grill.NewManager(dir)
if err != nil {
return err
}
s, err := m.Start(args[0])
if err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "started session %s\n", s.ID)
fmt.Fprintf(cmd.OutOrStdout(), " topic: %s\n", s.Topic)
fmt.Fprintf(cmd.OutOrStdout(), " seed question: %s\n", s.Decisions[0].Question)
fmt.Fprintln(cmd.OutOrStdout(), "")
fmt.Fprintf(cmd.OutOrStdout(), " next: sin-code grill next %s\n", s.ID)
return nil
},
}
}

func newGrillNextCmd() *cobra.Command {
return &cobra.Command{
Use: "next <id>",
Short: "Ask the next adversarial question",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
dir, err := grillDir()
if err != nil {
return err
}
m, err := grill.NewManager(dir)
if err != nil {
return err
}
child, parent, err := m.Next(args[0])
if err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "sub-question under %s:\n", parent)
fmt.Fprintf(cmd.OutOrStdout(), " %s: %s\n", child.ID, child.Question)
fmt.Fprintln(cmd.OutOrStdout(), "")
fmt.Fprintf(cmd.OutOrStdout(), " answer: sin-code grill answer %s %s \"<your response>\"\n", args[0], child.ID)
fmt.Fprintf(cmd.OutOrStdout(), " resolve: sin-code grill answer %s %s done\n", args[0], child.ID)
return nil
},
}
}

func newGrillAnswerCmd() *cobra.Command {
return &cobra.Command{
Use: "answer <id> <decision-id> <text>",
Short: "Record the operator's response to a decision (use 'done' to resolve)",
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
dir, err := grillDir()
if err != nil {
return err
}
m, err := grill.NewManager(dir)
if err != nil {
return err
}
if err := m.Answer(args[0], args[1], args[2]); err != nil {
return err
}
action := "answered"
if args[2] == "done" || args[2] == "skip" {
action = "resolved"
}
fmt.Fprintf(cmd.OutOrStdout(), "%s decision %s in session %s\n", action, args[1], args[0])
return nil
},
}
}

func newGrillStatusCmd() *cobra.Command {
return &cobra.Command{
Use: "status <id>",
Short: "Show resolved + open decision branches",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
dir, err := grillDir()
if err != nil {
return err
}
m, err := grill.NewManager(dir)
if err != nil {
return err
}
s, err := m.Get(args[0])
if err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "Session %s\n", s.ID)
fmt.Fprintf(cmd.OutOrStdout(), " topic: %s\n", s.Topic)
fmt.Fprintf(cmd.OutOrStdout(), " started: %s\n", s.StartedAt.Format("2006-01-02 15:04:05"))
fmt.Fprintf(cmd.OutOrStdout(), " decisions: %d (open=%d)\n", len(s.Decisions), s.OpenQuestions)
for _, d := range s.Decisions {
marker := "[ ]"
switch d.Status {
case "answered":
marker = "[~]"
case "resolved":
marker = "[x]"
case "deferred":
marker = "[>]"
}
fmt.Fprintf(cmd.OutOrStdout(), " %s %s: %s\n", marker, d.ID, d.Question)
if d.Answer != "" {
fmt.Fprintf(cmd.OutOrStdout(), " answer: %s\n", d.Answer)
}
}
return nil
},
}
}

func newGrillSynthesizeCmd() *cobra.Command {
var jsonOut bool
cmd := &cobra.Command{
Use: "synthesize <id>",
Short: "Produce a summary of decisions, assumptions, and open questions",
Args: cobra.ExactArgs(1),
RunE: func(c *cobra.Command, args []string) error {
dir, err := grillDir()
if err != nil {
return err
}
m, err := grill.NewManager(dir)
if err != nil {
return err
}
syn, err := m.Synthesize(args[0])
if err != nil {
return err
}
if jsonOut {
enc := json.NewEncoder(c.OutOrStdout())
enc.SetIndent("", " ")
return enc.Encode(syn)
}
fmt.Fprintln(c.OutOrStdout(), "# Grilling Synthesis")
fmt.Fprintln(c.OutOrStdout(), "")
fmt.Fprintln(c.OutOrStdout(), "## Resolved")
for _, r := range syn.Resolved {
fmt.Fprintf(c.OutOrStdout(), "- %s\n", r)
}
if len(syn.Assumptions) > 0 {
fmt.Fprintln(c.OutOrStdout(), "")
fmt.Fprintln(c.OutOrStdout(), "## Assumptions")
for _, a := range syn.Assumptions {
fmt.Fprintf(c.OutOrStdout(), "- %s\n", a)
}
}
if len(syn.Open) > 0 {
fmt.Fprintln(c.OutOrStdout(), "")
fmt.Fprintln(c.OutOrStdout(), "## Open")
for _, o := range syn.Open {
fmt.Fprintf(c.OutOrStdout(), "- %s\n", o)
}
}
return nil
},
}
cmd.Flags().BoolVar(&jsonOut, "json", false, "emit JSON")
return cmd
}
99 changes: 99 additions & 0 deletions cmd/sin-code/internal/grill/catalog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// SPDX-License-Identifier: MIT
// Purpose: question catalog — the canonical anti-patterns and
// grilling questions (issue #141). Ported from the external
// SIN-Code-Grill-Me-Skill catalog. v0 ships a small seed; v1 will
// import the full list from the upstream SKILL.md.
//
// The catalog is intentionally a Go slice (not a YAML file) so
// that:
// 1. The CLI has zero file-deps for `grill next`.
// 2. The questions are typed (no string-template magic).
// 3. Operators can grep the binary for the question set.
package grill

// AntiPattern is one entry in the catalog: the "what to look for"
// plus 2-3 example questions. The CLI picks one anti-pattern at
// random (or round-robin) per `grill next` call.
type AntiPattern struct {
Name string
Description string
Questions []string
}

// Catalog is the seed of anti-patterns the v0 grill ships with.
// v1 will import the full upstream catalog and add domain-specific
// patterns (security, perf, ergonomics, etc.).
var Catalog = []AntiPattern{
{
Name: "Hidden Assumptions",
Description: "The plan depends on conditions the operator hasn't verified yet.",
Questions: []string{
"What does this plan assume about the operator's environment that you have not verified?",
"If the assumption is wrong, what's the failure mode?",
"Could you state the assumption in a single sentence and label it 'unverified'?",
},
},
{
Name: "Rollback Plan",
Description: "The plan has no path back if it goes wrong.",
Questions: []string{
"If the first step of this plan goes wrong, what do you do?",
"Can the operator undo step 1 cleanly, or is it a one-way door?",
"Would adding a checkpoint before step 2 change the operator's confidence?",
},
},
{
Name: "Failure Modes",
Description: "The plan only describes the happy path.",
Questions: []string{
"List the three most likely ways this plan fails.",
"For each failure, is it silent (no signal) or loud (error/log)?",
"Which failure would the operator discover last?",
},
},
{
Name: "Operator Cost",
Description: "The plan costs the operator more than it returns.",
Questions: []string{
"How many operator-hours does this plan consume?",
"How many operator-hours does it save per occurrence?",
"At what frequency of occurrence is the saving worth the cost?",
},
},
{
Name: "Premature Optimization",
Description: "The plan optimizes a non-bottleneck.",
Questions: []string{
"What is the slowest step of this plan?",
"Is that the same as the most expensive step?",
"Would removing the optimization make the plan simpler without losing the win?",
},
},
{
Name: "Scope Creep",
Description: "The plan grows beyond its stated goal.",
Questions: []string{
"What is the smallest version of this plan that delivers the stated value?",
"Which of the 'nice to have' items can be deferred?",
"If you could only ship one feature, which one is it?",
},
},
{
Name: "Single Point of Failure",
Description: "The plan has a single point whose failure cascades.",
Questions: []string{
"Which component, if it fails, takes down the whole plan?",
"Is the SPOF documented in the runbook?",
"Could you remove the SPOF or replace it with a redundant subsystem?",
},
},
{
Name: "Verification Gap",
Description: "The plan claims to verify, but the verification is itself unverified.",
Questions: []string{
"How do you know the verification works?",
"Has the verification ever caught a failure?",
"What would a failure of the verification look like?",
},
},
}
Loading
Loading