feat(attach): warn on empty transcript, add capture footer + amend-fail note · Entire

feat(attach): warn on empty transcript, add capture footer + amend-fail note

ae3a8aa→main·

computermode·2w ago·2 files·+209 added/-40 removed

Improve entire attach diagnosability and feedback:

Threads an errW writer through runAttach for the stderr output, and extracts warnEmptyTranscriptMetadata / printAttachFooter / amendOrPrintTrailer helpers (the last de-duplicates the two amend call sites).

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

Sessions

38c6ac0d707eView transcript

Changes

2

95 unmodified lines

98
99
100
101
102
103
104
105
39 unmodified lines

145
146
147
145
148
149
150
151
21 unmodified lines

173
174
175
173
176
177
178
179
21 unmodified lines

201
202
203
201
204
205
206
207
43 unmodified lines

251
252
253
251
252
253
254
254
255
256
257
28 unmodified lines

286
287
288
289
290
291
292
86 unmodified lines

379
380
381
382
383
384
385
386
387
388
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
390
402
403
404
405
406
393
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452

95 unmodified lines

attach a review without a declared skills list.

Works with any registered agent, including external agents enabled via
external_agents in settings. Run 'entire agent list' to see the full list.
// the user as clear stderr messages rather than generic cobra error output.
// The non-review path preserves the existing runAttach return-err behavior.
func runAttachSurfaceReviewErrors(cmd *cobra.Command, sessionID string, agentName types.AgentName, opts attachOptions) error {
    err := runAttach(cmd.Context(), cmd.OutOrStdout(), sessionID, agentName, opts)
    err := runAttach(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), sessionID, agentName, opts)
    if err != nil && opts.Review {
        cmd.SilenceUsage = true
        fmt.Fprintln(cmd.ErrOrStderr(), err.Error())
    }
    return []string{meta.FirstPrompt}
}

func runAttach(ctx context.Context, w io.Writer, sessionID string, agentName types.AgentName, opts attachOptions) error {
    // Initialize structured logger so logging.Warn/Info write to .entire/logs/ not stderr.
    if err := logging.Init(ctx, sessionID); err != nil {
        // Init failed — logging will use stderr fallback, non-fatal.
    }
}

Example Test Functions

// TestAttach_WarnsOnEmptyTranscriptMetadata: a transcript that parses to no
// user prompts and no model must still produce a checkpoint (warn, don't
// fail), with a warning written to stderr — never to stdout, where it would
// interleave with the success lines.
func TestAttach_WarnsOnEmptyTranscriptMetadata(t *testing.T) {
    setupAttachTestRepo(t)

sessionID := "test-attach-empty-meta"
    // Valid JSONL, but no user content and no model field: TurnCount and
    // FirstPrompt both stay zero/empty.
    setupClaudeTranscript(t, sessionID, `{"type":"assistant","message":{"role":"assistant","content":"hi"},"uuid":"a1"}`)

var out, errOut bytes.Buffer
    if err := runAttach(context.Background(), &out, &errOut, sessionID, agent.AgentNameClaudeCode, attachOptions{Force: true}); err != nil {
        t.Fatalf("runAttach should warn, not fail, on empty transcript metadata: %%v", err)
    }

if !strings.Contains(errOut.String(), "no user prompts were parsed") {
        t.Errorf("expected empty-transcript warning on stderr, got: %%q", errOut.String())
    }
    // The warning must not leak onto stdout.
    if strings.Contains(out.String(), "no user prompts were parsed") {
        t.Errorf("warning leaked onto stdout: %%q", out.String())
    }

// The checkpoint must still be written.
    store, err := session.NewStateStore(context.Background())
    if err != nil {
        t.Fatal(err)
    }
    state, err := store.Load(context.Background(), sessionID)
    if err != nil {
        t.Fatal(err)
    }
    if state == nil || state.LastCheckpointID.IsEmpty() {
        t.Fatalf("expected checkpoint to be written despite empty metadata; state=%%+v", state)
    }
}
// TestAttachSummaryLine covers the post-attach "Captured: …" footer builder:
// every field present, the token segment omitted when usage is nil, and the
// empty result when nothing is known.
func TestAttachSummaryLine(t *testing.T) {
    t.Parallel()

tu := &agent.TokenUsage{InputTokens: 1000, OutputTokens: 300}
    if got, want := attachSummaryLine(transcriptMetadata{TurnCount: 12, Model: "claude-opus-4-8"}, tu),
        "12 turns · claude-opus-4-8 · 1.3k tokens"; got != want {
        t.Errorf("attachSummaryLine() = %%q, want %%q", got, want)
    }
}
// TestAttach_NonInteractivePrintsTrailerForManualPaste: with --force unset and
// no TTY (the test default), attach cannot prompt to amend, so it prints the
// Entire-Checkpoint trailer for manual paste instead of failing.
func TestAttach_NonInteractivePrintsTrailerForManualPaste(t *testing.T) {
    setupAttachTestRepo(t)

sessionID := "test-attach-noninteractive"
    setupClaudeTranscript(t, sessionID, `{"type":"user","message":{"role":"user","content":"hello"},"uuid":"u1"}
{"type":"assistant","message":{"role":"assistant","content":"hi"},"uuid":"a1"}`)

var out, errOut bytes.Buffer
    // Force:false — exercise the non-interactive fallback branch.
    if err := runAttach(context.Background(), &out, &errOut, sessionID, agent.AgentNameClaudeCode, attachOptions{}); err != nil {
        t.Fatalf("runAttach failed: %%v", err)
    }

re := regexp.MustCompile(`Entire-Checkpoint: ` + id.CheckpointPattern)
    if !re.MatchString(out.String()) {
        t.Errorf("expected Entire-Checkpoint trailer for manual paste, got:\n%%s", out.String())
    }
}