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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,27 @@ All notable changes to the SIN-Code unified binary will be documented in this fi

## [Unreleased] - 2026-06-16

### 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):
- `goal status <id>` — show one goal with subtasks (children)
- `goal complete <id>` — mark a goal as verified/done
- `goal subtask <parent-id> <prompt>` — add a subtask
- `goal report [--format md|json]` — progress report
- **Mapping to the 8 external tools** (issue body):
- `goal_start` → existing `goal add`
- `goal_status` → NEW `goal status`
- `goal_list` → existing `goal list`
- `goal_complete` → NEW `goal complete`
- `goal_subtask` → NEW `goal subtask`
- `goal_report` → NEW `goal report`
- `goal_checkpoint` / `goal_rollback` — **deferred to v1** (no
storage yet; the Queue has no `Checkpoint` table)
- **`parseGoalID` helper** — accepts both `42` and `#42` (with
optional whitespace); used by all four new subcommands
- **11 tests** (1 unit test for `parseGoalID`, all autonomy
tests pass under `go test -race -count=1`)

### Added — Skill lifecycle markers (issue #139)
- **`scripts/lifecycle_map.yaml`** — single source of truth for the
lifecycle of every bundled skill. Maps each of the 34 skills to
Expand Down
176 changes: 175 additions & 1 deletion cmd/sin-code/goal_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"encoding/json"
"fmt"
"os"
"strconv"
"strings"

"github.com/spf13/cobra"

Expand Down Expand Up @@ -152,6 +154,178 @@
discoverCmd.Flags().BoolVar(&dryRun, "dry-run", false, "list findings without enqueueing")
discoverCmd.Flags().IntVar(&discoverRetries, "retries", 3, "retry budget for enqueued goals")

cmd.AddCommand(addCmd, listCmd, discoverCmd)
// goal status <id> — show one goal with subtasks (issue #140 fusion).
var statusJsonOut bool
statusCmd := &cobra.Command{
Use: "status <id>",
Short: "Show one goal's progress, attempts, and children (issue #140 fusion)",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, perr := parseGoalID(args[0])
if perr != nil {
return perr
}
q, err := autonomy.Open(autonomy.DefaultPath())
if err != nil {
return err
}
defer q.Close()
g, err := q.Get(cmd.Context(), id)
if err != nil {
return err
}
children, err := q.Children(cmd.Context(), id)
if err != nil {
return err
}
if statusJsonOut {
payload := map[string]any{
"goal": g,
"children": children,
}
enc := json.NewEncoder(cmd.OutOrStdout())
enc.SetIndent("", " ")
return enc.Encode(payload)
}
fmt.Fprintf(cmd.OutOrStdout(), "Goal %d [%s] attempts=%d/%d priority=%d\n",
g.ID, g.Status, g.Attempts, g.MaxRetries, g.Priority)
fmt.Fprintf(cmd.OutOrStdout(), " prompt: %s\n", g.Prompt)
if len(g.LastError) > 0 {
fmt.Fprintf(cmd.OutOrStdout(), " last_error: %s\n", g.LastError)
}
if len(children) == 0 {
fmt.Fprintln(cmd.OutOrStdout(), " (no subtasks)")
return nil
}
fmt.Fprintf(cmd.OutOrStdout(), " subtasks (%d):\n", len(children))
for _, c := range children {
fmt.Fprintf(cmd.OutOrStdout(), " %-5d [%-10s] %.60s\n", c.ID, c.Status, c.Prompt)
}
return nil
},
}
statusCmd.Flags().BoolVar(&statusJsonOut, "json", false, "emit JSON")

// goal complete <id> — mark a goal as verified/done (issue #140 fusion).
// Mirrors goal_complete in the external Python MCP server.
var completeSession string
completeCmd := &cobra.Command{
Use: "complete <id>",
Short: "Mark a goal as verified/done (issue #140 fusion; maps to q.Complete)",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, perr := parseGoalID(args[0])
if perr != nil {
return perr
}
q, err := autonomy.Open(autonomy.DefaultPath())
if err != nil {
return err
}
defer q.Close()
if err := q.Complete(cmd.Context(), id, completeSession); err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "goal %d marked complete (session %q)\n", id, completeSession)
return nil
},
}
completeCmd.Flags().StringVar(&completeSession, "session", "", "session id of the worker that completed the goal")

// goal subtask <parent-id> <prompt> — add a subtask to a parent (issue #140 fusion).
// Mirrors goal_subtask in the external Python MCP server.
var subtaskPriority, subtaskRetries int
var subtaskCriteria []string
subtaskCmd := &cobra.Command{
Use: "subtask <parent-id> <prompt>",
Short: "Add a subtask under a parent goal (issue #140 fusion; maps to q.AddSub)",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
parentID, perr := parseGoalID(args[0])
if perr != nil {
return perr
}
contractJSON := ""
if len(subtaskCriteria) > 0 {
c := &goalcontract.GoalContract{SemanticCriteria: subtaskCriteria}
contractJSON, _ = c.Marshal()
}
q, err := autonomy.Open(autonomy.DefaultPath())
if err != nil {
return err
}
defer q.Close()
id, err := q.AddSub(cmd.Context(), parentID, args[1], subtaskPriority, subtaskRetries, contractJSON)
if err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "subtask %d enqueued under parent %d\n", id, parentID)
return nil
},
}
subtaskCmd.Flags().IntVar(&subtaskPriority, "priority", 0, "higher runs sooner")
subtaskCmd.Flags().IntVar(&subtaskRetries, "retries", 3, "retry budget")
subtaskCmd.Flags().StringArrayVar(&subtaskCriteria, "criteria", nil, "acceptance criterion (repeatable)")

// goal report — emit a JSON or Markdown progress report (issue #140 fusion).
// Maps to goal_report in the external Python MCP server. v0 ships the
// JSON variant; Markdown rendering is a follow-up.
var reportFormat string
reportCmd := &cobra.Command{
Use: "report",
Short: "Generate a progress report across all goals (issue #140 fusion)",
RunE: func(cmd *cobra.Command, args []string) error {
q, err := autonomy.Open(autonomy.DefaultPath())
if err != nil {
return err
}
defer q.Close()
goals, err := q.List(cmd.Context(), "")
if err != nil {
return err
}
byStatus := map[string]int{}
for _, g := range goals {
byStatus[string(g.Status)]++
}
switch reportFormat {
case "json":
payload := map[string]any{
"total": len(goals),

Check failure on line 294 in cmd/sin-code/goal_cmd.go

View workflow job for this annotation

GitHub Actions / golangci-lint

File is not properly formatted (gofmt)
"by_status": byStatus,
"goals": goals,
}
enc := json.NewEncoder(cmd.OutOrStdout())
enc.SetIndent("", " ")
return enc.Encode(payload)
case "md", "markdown", "":
fmt.Fprintf(cmd.OutOrStdout(), "# Goal Report\n\n")
fmt.Fprintf(cmd.OutOrStdout(), "Total: %d goal(s)\n\n", len(goals))
fmt.Fprintf(cmd.OutOrStdout(), "## By status\n\n")
for status, n := range byStatus {
fmt.Fprintf(cmd.OutOrStdout(), "- **%s**: %d\n", status, n)
}
return nil
default:
return fmt.Errorf("unsupported format %q (json|md)", reportFormat)
}
},
}
reportCmd.Flags().StringVar(&reportFormat, "format", "md", "output format: md|json")

cmd.AddCommand(addCmd, listCmd, discoverCmd, statusCmd, completeCmd, subtaskCmd, reportCmd)
return cmd
}

// parseGoalID parses a numeric goal id. Strings like "#42" or "42" are
// both accepted (the "#" prefix is tolerated for operator ergonomics).
func parseGoalID(s string) (int64, error) {
// Order matters: TrimSpace first, then TrimPrefix("#") — otherwise
// " #42 " would not match the leading "#".
s = strings.TrimPrefix(strings.TrimSpace(s), "#")
id, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid goal id %q: %w", s, err)
}
return id, nil
}
41 changes: 41 additions & 0 deletions cmd/sin-code/goal_fusion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
// Purpose: tests for parseGoalID (issue #140 fusion helper).
package main

import "testing"

func TestParseGoalID(t *testing.T) {
cases := []struct {
in string
want int64
err bool
}{
{"42", 42, false},
{"#42", 42, false},
{" 42 ", 42, false},
{" #42 ", 42, false},
{"0", 0, false},
{"9999999999", 9999999999, false},
{"abc", 0, true},
{"", 0, true},
{"#abc", 0, true},
{"-1", -1, false}, // negative is allowed (id is int64)
{"3.14", 0, true},
}
for _, c := range cases {
got, err := parseGoalID(c.in)
if c.err {
if err == nil {
t.Errorf("parseGoalID(%q): expected error, got %d", c.in, got)
}
continue
}
if err != nil {
t.Errorf("parseGoalID(%q): unexpected error %v", c.in, err)
continue
}
if got != c.want {
t.Errorf("parseGoalID(%q) = %d, want %d", c.in, got, c.want)
}
}
}
Loading