|
1 | 1 | package main |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "bufio" |
4 | 5 | "context" |
5 | 6 | "encoding/json" |
6 | 7 | "fmt" |
@@ -58,6 +59,8 @@ func run(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int |
58 | 59 | return runPrecedent(args[2:], stdin, stdout, stderr) |
59 | 60 | case "outcome": |
60 | 61 | return runOutcome(args[2:], stdout, stderr) |
| 62 | + case "clarify": |
| 63 | + return runClarify(args[2:], stdin, stdout, stderr) |
61 | 64 | } |
62 | 65 | } |
63 | 66 |
|
@@ -848,6 +851,22 @@ func runSubmit(args []string, stdin io.Reader, stdout io.Writer, stderr io.Write |
848 | 851 | } |
849 | 852 | } |
850 | 853 |
|
| 854 | + // Check for escalation questions. |
| 855 | + escalation, caseID := tui.ParseEscalation(raw) |
| 856 | + if escalation != nil && escalation.Mode == "human_clarification" && len(escalation.Questions) > 0 { |
| 857 | + _, _ = fmt.Fprintf(stderr, "\n⚠ clarification needed (%d questions):\n", len(escalation.Questions)) |
| 858 | + for i, q := range escalation.Questions { |
| 859 | + _, _ = fmt.Fprintf(stderr, " %d. [%s] %s\n", i+1, q.ImpactOnVerdict, q.Question) |
| 860 | + if q.Context != "" { |
| 861 | + _, _ = fmt.Fprintf(stderr, " %s\n", q.Context) |
| 862 | + } |
| 863 | + if q.DefaultIfUnanswered != "" { |
| 864 | + _, _ = fmt.Fprintf(stderr, " default: %s\n", q.DefaultIfUnanswered) |
| 865 | + } |
| 866 | + } |
| 867 | + _, _ = fmt.Fprintf(stderr, "\nrun: vectorpad clarify %s\n", caseID) |
| 868 | + } |
| 869 | + |
851 | 870 | // Pretty-print the response. |
852 | 871 | var formatted []byte |
853 | 872 | var pretty json.RawMessage |
@@ -1207,6 +1226,128 @@ complete -c vectorpad -n '__fish_use_subcommand' -a outcome -d 'Report outcome f |
1207 | 1226 | complete -c vectorpad -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish' |
1208 | 1227 | ` |
1209 | 1228 |
|
| 1229 | +func runClarify(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int { |
| 1230 | + jsonMode := false |
| 1231 | + var caseID string |
| 1232 | + |
| 1233 | + for i := 0; i < len(args); i++ { |
| 1234 | + switch args[i] { |
| 1235 | + case "--json": |
| 1236 | + jsonMode = true |
| 1237 | + default: |
| 1238 | + if caseID == "" { |
| 1239 | + caseID = args[i] |
| 1240 | + } |
| 1241 | + } |
| 1242 | + } |
| 1243 | + |
| 1244 | + if caseID == "" { |
| 1245 | + _, _ = fmt.Fprintln(stderr, "usage: vectorpad clarify <case-id> [--json]") |
| 1246 | + return 1 |
| 1247 | + } |
| 1248 | + |
| 1249 | + cfg, err := config.Load() |
| 1250 | + if err != nil { |
| 1251 | + _, _ = fmt.Fprintf(stderr, "error: %v\n", err) |
| 1252 | + return 1 |
| 1253 | + } |
| 1254 | + if cfg.VectorCourt.APIKey == "" { |
| 1255 | + _, _ = fmt.Fprintln(stderr, "error: no API key configured (run: vectorpad config set vectorcourt.api_key <key>)") |
| 1256 | + return 1 |
| 1257 | + } |
| 1258 | + |
| 1259 | + // JSON mode: read answers from stdin. |
| 1260 | + if jsonMode { |
| 1261 | + data, err := io.ReadAll(stdin) |
| 1262 | + if err != nil { |
| 1263 | + _, _ = fmt.Fprintf(stderr, "error: %v\n", err) |
| 1264 | + return 1 |
| 1265 | + } |
| 1266 | + var req vectorcourt.ClarifyRequest |
| 1267 | + if err := json.Unmarshal(data, &req); err != nil { |
| 1268 | + _, _ = fmt.Fprintf(stderr, "error: invalid JSON: %v\n", err) |
| 1269 | + return 1 |
| 1270 | + } |
| 1271 | + |
| 1272 | + client := vectorcourt.NewClient(cfg.Endpoint(), cfg.VectorCourt.APIKey) |
| 1273 | + _, _ = fmt.Fprintln(stderr, "submitting clarification...") |
| 1274 | + raw, err := client.SubmitClarification(context.Background(), caseID, &req) |
| 1275 | + if err != nil { |
| 1276 | + _, _ = fmt.Fprintf(stderr, "error: %v\n", err) |
| 1277 | + return 1 |
| 1278 | + } |
| 1279 | + |
| 1280 | + var pretty json.RawMessage |
| 1281 | + if json.Unmarshal(raw, &pretty) == nil { |
| 1282 | + if f, err := json.MarshalIndent(pretty, "", " "); err == nil { |
| 1283 | + _, _ = fmt.Fprintln(stdout, string(f)) |
| 1284 | + return 0 |
| 1285 | + } |
| 1286 | + } |
| 1287 | + _, _ = fmt.Fprintln(stdout, string(raw)) |
| 1288 | + return 0 |
| 1289 | + } |
| 1290 | + |
| 1291 | + // Interactive mode: prompt for each answer. |
| 1292 | + f, ok := stdin.(*os.File) |
| 1293 | + if !ok || !isTerminal(f) { |
| 1294 | + _, _ = fmt.Fprintln(stderr, "error: interactive mode requires a terminal (use --json for pipe input)") |
| 1295 | + return 1 |
| 1296 | + } |
| 1297 | + |
| 1298 | + // Fetch the case to get pending questions. |
| 1299 | + // We don't have a "get case" endpoint, so prompt the user to provide question IDs. |
| 1300 | + _, _ = fmt.Fprintln(stderr, "Enter answers for case "+caseID) |
| 1301 | + _, _ = fmt.Fprintln(stderr, "Format: one answer per line as question_id=answer (empty line to submit)") |
| 1302 | + _, _ = fmt.Fprintln(stderr, "") |
| 1303 | + |
| 1304 | + scanner := bufio.NewScanner(stdin) |
| 1305 | + var answers []vectorcourt.ClarificationAnswer |
| 1306 | + for { |
| 1307 | + _, _ = fmt.Fprint(stderr, "> ") |
| 1308 | + if !scanner.Scan() { |
| 1309 | + break |
| 1310 | + } |
| 1311 | + line := strings.TrimSpace(scanner.Text()) |
| 1312 | + if line == "" { |
| 1313 | + break |
| 1314 | + } |
| 1315 | + parts := strings.SplitN(line, "=", 2) |
| 1316 | + if len(parts) != 2 { |
| 1317 | + _, _ = fmt.Fprintln(stderr, " invalid format, use: question_id=answer") |
| 1318 | + continue |
| 1319 | + } |
| 1320 | + answers = append(answers, vectorcourt.ClarificationAnswer{ |
| 1321 | + QuestionID: strings.TrimSpace(parts[0]), |
| 1322 | + Answer: strings.TrimSpace(parts[1]), |
| 1323 | + Confidence: "firm", |
| 1324 | + }) |
| 1325 | + } |
| 1326 | + |
| 1327 | + if len(answers) == 0 { |
| 1328 | + _, _ = fmt.Fprintln(stderr, "no answers provided") |
| 1329 | + return 1 |
| 1330 | + } |
| 1331 | + |
| 1332 | + client := vectorcourt.NewClient(cfg.Endpoint(), cfg.VectorCourt.APIKey) |
| 1333 | + _, _ = fmt.Fprintf(stderr, "submitting %d answers...\n", len(answers)) |
| 1334 | + raw, err := client.SubmitClarification(context.Background(), caseID, &vectorcourt.ClarifyRequest{Answers: answers}) |
| 1335 | + if err != nil { |
| 1336 | + _, _ = fmt.Fprintf(stderr, "error: %v\n", err) |
| 1337 | + return 1 |
| 1338 | + } |
| 1339 | + |
| 1340 | + var pretty json.RawMessage |
| 1341 | + if json.Unmarshal(raw, &pretty) == nil { |
| 1342 | + if f, err := json.MarshalIndent(pretty, "", " "); err == nil { |
| 1343 | + _, _ = fmt.Fprintln(stdout, string(f)) |
| 1344 | + return 0 |
| 1345 | + } |
| 1346 | + } |
| 1347 | + _, _ = fmt.Fprintln(stdout, string(raw)) |
| 1348 | + return 0 |
| 1349 | +} |
| 1350 | + |
1210 | 1351 | func isTerminal(f *os.File) bool { |
1211 | 1352 | info, err := f.Stat() |
1212 | 1353 | if err != nil { |
|
0 commit comments