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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ $ i "check if google's dns server is online"

It is **local-first** by default (no network required after first run, no prompts leave your machine), **safe by construction** (risk-classified, deterministic guards, audit log), and **composable** (`i "ping google's dns" | i "if reachable exit 0 else exit 1"`).

That composability applies to subcommands that consume natural language too: `i report "first problem" < extra-notes.txt` appends the piped text after the command-line text before proposing issues.

> **Status: pre-alpha.** The binary builds and the mock backend round-trips the full prompt → propose → confirm → run loop, but the local model runtime, daemon, and self-update flows are still being wired up. See [`INTENT.md`](./INTENT.md) for the full project charter, [`docs/SPEC.md`](./docs/SPEC.md) for the implementation contract, and [open issues](https://github.com/CoreyRDean/intent/issues) for the roadmap.

## Building from source
Expand Down
2 changes: 2 additions & 0 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,8 @@ Update behavior:

Converts natural language into one or more GitHub issues against `CoreyRDean/intent`:

`i report` accepts natural language from argv, stdin, or both. When both are present, argv text comes first and stdin is appended after it.

1. Parse the user's input. The model returns an array of `{title, body, labels, kind}` proposals.
2. For each proposal, query GitHub Search (`is:issue repo:CoreyRDean/intent <terms>`) for the top 5 candidates.
3. For each candidate, compute a similarity score (token-set ratio + embedding-similarity if a small embedding model is available; otherwise just token-set ratio). If ≥0.85, treat as duplicate.
Expand Down
27 changes: 25 additions & 2 deletions internal/cli/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ func cmdReport(ctx context.Context, args []string) int {
prompt = append(prompt, a)
}
}
if len(prompt) == 0 {
stdinData, _ := readStdinIfPiped(reportStdinWait(ctx))
userInput := buildReportUserInput(prompt, stdinData)
if userInput == "" {
errf("usage: i report <natural language describing one or more bugs/features>")
return 1
}
Expand All @@ -54,7 +56,6 @@ func cmdReport(ctx context.Context, args []string) int {
errf("i report requires a real backend — run 'i doctor' to diagnose")
return 3
}
userInput := strings.Join(prompt, " ")

vl := verbose.FromContext(ctx)
vl.Section("report")
Expand Down Expand Up @@ -248,6 +249,28 @@ func trim(s string, n int) string {
return s[:n] + "..."
}

func reportStdinWait(ctx context.Context) time.Duration {
if deadline, ok := ctx.Deadline(); ok {
if wait := time.Until(deadline); wait > 0 {
return wait
}
}
return 60 * time.Second
}

func buildReportUserInput(promptArgs []string, stdinData string) string {
argText := strings.TrimSpace(strings.Join(promptArgs, " "))
stdinText := strings.TrimSpace(stdinData)
switch {
case argText == "":
return stdinText
case stdinText == "":
return argText
default:
return argText + "\n\n" + stdinText
}
}

// reportSchemaJSON is the JSON schema the model's output must conform
// to when we go through the structured path. The backend's grammar/
// response_format enforces this at token generation, so the model
Expand Down
39 changes: 39 additions & 0 deletions internal/cli/report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,42 @@ func TestSynthesizeTitle(t *testing.T) {
}
}
}

func TestBuildReportUserInput(t *testing.T) {
cases := []struct {
name string
args []string
stdin string
want string
}{
{
name: "args_only",
args: []string{"first", "natural", "language"},
want: "first natural language",
},
{
name: "stdin_only",
stdin: "second natural language\n",
want: "second natural language",
},
{
name: "args_then_stdin",
args: []string{"first", "natural", "language"},
stdin: "second natural language\n",
want: "first natural language\n\nsecond natural language",
},
{
name: "ignores_blank_stdin",
args: []string{"first", "natural", "language"},
stdin: "\n\n",
want: "first natural language",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := buildReportUserInput(tc.args, tc.stdin); got != tc.want {
t.Fatalf("buildReportUserInput(%q, %q)=%q want %q", tc.args, tc.stdin, got, tc.want)
}
})
}
}
Loading