feat(planctl): context-window token awareness — wiring (parent 2.0)
* emit.go: emitText/emitJSON take *TokenCtx; jsonSummary extended with *jsonCtxWin (omitempty) for context_window in JSON output
* main.go: ReadTokenCtx() called once at top of run() before subcommand dispatch; threaded through runLint
* main_test.go: golden-fixture harness extended for env.txt + transcript.jsonl staging in temp HOME
* testdata/v2/context-{info,warn,error,normal,no-session,no-limit,json-warn,multi-plan}: 8 new fixtures exercising all bands + shapes
Task 2.0 from dev/plans/26174-planctl-context-tokens/prd.md
This commit is contained in:
parent
935b86b791
commit
b02ae7dade
56 changed files with 479 additions and 30 deletions
|
|
@ -38,7 +38,11 @@ type PlanResult struct {
|
|||
// Directory-level diagnostics (Path == "", typically missing-prd /
|
||||
// missing-closeout-file) render with the plan-dir basename substituted
|
||||
// for the path.
|
||||
func emitText(w io.Writer, plans []PlanResult, strict bool) int {
|
||||
// spec:26174-planctl-context-tokens/R3.4+R3.5+R4.1+R4.2+R4.3+R4.5+D§4.3
|
||||
// ctx (when non-nil) drives the context-window line: emitted after the plan
|
||||
// summary in single-plan mode, after the aggregate summary in multi-plan mode
|
||||
// (R4.3 — once, not per-plan). Normal-band / nil ctx emits nothing (R3.5).
|
||||
func emitText(w io.Writer, plans []PlanResult, strict bool, ctx *TokenCtx) int {
|
||||
multi := len(plans) > 1
|
||||
for i, p := range plans {
|
||||
if multi {
|
||||
|
|
@ -52,9 +56,33 @@ func emitText(w io.Writer, plans []PlanResult, strict bool) int {
|
|||
if multi {
|
||||
writeAggregateText(w, plans)
|
||||
}
|
||||
writeContextText(w, ctx)
|
||||
return computeExit(plans, strict)
|
||||
}
|
||||
|
||||
// spec:26174-planctl-context-tokens/R3.3+R3.4+R3.5+R4.1+R4.2+D§4.3
|
||||
// writeContextText appends the canonical `context:` line for Info/Warn/Error
|
||||
// bands, or the unknown-limit form when Limit == 0. Silent for nil ctx or
|
||||
// Normal band. Format matches PRD R4.1 exactly.
|
||||
func writeContextText(w io.Writer, ctx *TokenCtx) {
|
||||
if ctx == nil {
|
||||
return
|
||||
}
|
||||
if ctx.Limit == 0 {
|
||||
// Unknown-limit branch is emitted regardless of threshold (R4.2).
|
||||
fmt.Fprintf(w, "context: %s tokens (limit unknown)\n", formatTokenCount(ctx.Used))
|
||||
return
|
||||
}
|
||||
sev, msg := ctx.Classify()
|
||||
if sev == "" {
|
||||
// Normal band: silent (R3.5).
|
||||
return
|
||||
}
|
||||
pct := ctx.Used * 100 / ctx.Limit
|
||||
fmt.Fprintf(w, "context: %s / %s tokens (%d%%) — %s\n",
|
||||
formatTokenCount(ctx.Used), formatTokenCount(ctx.Limit), pct, msg)
|
||||
}
|
||||
|
||||
// writePlanText emits the body of a single plan: either the R5.3 clean-
|
||||
// summary line (zero diagnostics) or the R5.2 diagnostic lines. Shared
|
||||
// by single-plan and multi-plan branches of emitText.
|
||||
|
|
@ -92,7 +120,12 @@ func writeAggregateText(w io.Writer, plans []PlanResult) {
|
|||
// diagnostics), but still contribute to the aggregate summary — so
|
||||
// a `planctl lint` run over N clean plans emits exactly one object
|
||||
// (the summary).
|
||||
func emitJSON(w io.Writer, plans []PlanResult, strict bool) int {
|
||||
// spec:26174-planctl-context-tokens/R5.1+R5.2+R5.3+R5.4+D§4.4
|
||||
// ctx (when non-nil) contributes a context_window field to the final summary
|
||||
// object. Normal band / nil ctx → no context_window key. Unknown limit (R5.2)
|
||||
// → only tokens_used key set. Context never affects the errors/warnings counts
|
||||
// in summary (R5.4).
|
||||
func emitJSON(w io.Writer, plans []PlanResult, strict bool, ctx *TokenCtx) int {
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetEscapeHTML(false)
|
||||
for _, p := range plans {
|
||||
|
|
@ -108,14 +141,44 @@ func emitJSON(w io.Writer, plans []PlanResult, strict bool) int {
|
|||
}
|
||||
}
|
||||
errs, warns := countSeverities(plans)
|
||||
_ = enc.Encode(jsonSummary{Summary: summaryBody{
|
||||
Plans: len(plans),
|
||||
Errors: errs,
|
||||
Warnings: warns,
|
||||
}})
|
||||
_ = enc.Encode(jsonSummary{
|
||||
Summary: summaryBody{
|
||||
Plans: len(plans),
|
||||
Errors: errs,
|
||||
Warnings: warns,
|
||||
},
|
||||
ContextWin: buildJSONCtxWin(ctx),
|
||||
})
|
||||
return computeExit(plans, strict)
|
||||
}
|
||||
|
||||
// spec:26174-planctl-context-tokens/R5.1+R5.2+R5.3+D§4.4
|
||||
// buildJSONCtxWin returns a populated jsonCtxWin or nil. nil means "don't emit
|
||||
// the key at all" (Normal band / no session data). R5.2 unknown-limit case
|
||||
// leaves TokensLimit/Pct/Severity/Recommendation zero-valued so omitempty
|
||||
// drops them on serialization.
|
||||
func buildJSONCtxWin(ctx *TokenCtx) *jsonCtxWin {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
out := &jsonCtxWin{TokensUsed: ctx.Used}
|
||||
if ctx.Limit == 0 {
|
||||
return out
|
||||
}
|
||||
sev, msg := ctx.Classify()
|
||||
if sev == "" {
|
||||
// Normal band — omit the entire key (R5.3).
|
||||
return nil
|
||||
}
|
||||
pct := int(ctx.Used * 100 / ctx.Limit)
|
||||
limit := ctx.Limit
|
||||
out.TokensLimit = &limit
|
||||
out.Pct = &pct
|
||||
out.Severity = sev
|
||||
out.Recommendation = msg
|
||||
return out
|
||||
}
|
||||
|
||||
// jsonDiag is the JSONL payload for one diagnostic. Field tags pin the
|
||||
// exact key order produced by encoding/json and keep the contract
|
||||
// grep-stable for downstream consumers.
|
||||
|
|
@ -138,8 +201,25 @@ type summaryBody struct {
|
|||
|
||||
// jsonSummary wraps summaryBody under the "summary" key so the final
|
||||
// JSONL line is distinguishable from the per-diagnostic objects.
|
||||
//
|
||||
// spec:26174-planctl-context-tokens/R5.1+R5.2+R5.3+D§5.4
|
||||
// ContextWin is omitempty so Normal-band / nil-ctx invocations serialize
|
||||
// without a context_window key.
|
||||
type jsonSummary struct {
|
||||
Summary summaryBody `json:"summary"`
|
||||
Summary summaryBody `json:"summary"`
|
||||
ContextWin *jsonCtxWin `json:"context_window,omitempty"`
|
||||
}
|
||||
|
||||
// spec:26174-planctl-context-tokens/R5.1+R5.2+D§5.4
|
||||
// jsonCtxWin is the context_window payload in JSON mode. Pointer fields for
|
||||
// TokensLimit and Pct are omitempty: when the limit is unknown per R5.2, they
|
||||
// marshal out entirely (rather than as 0).
|
||||
type jsonCtxWin struct {
|
||||
TokensUsed int64 `json:"tokens_used"`
|
||||
TokensLimit *int64 `json:"tokens_limit,omitempty"`
|
||||
Pct *int `json:"pct,omitempty"`
|
||||
Severity string `json:"severity,omitempty"`
|
||||
Recommendation string `json:"recommendation,omitempty"`
|
||||
}
|
||||
|
||||
// severityLabel renders a Severity as its canonical string form. Used
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ func TestEmitText_SinglePlanClean(t *testing.T) {
|
|||
TaskCount: 45, ReqCount: 32, DesCount: 9,
|
||||
}}
|
||||
var buf bytes.Buffer
|
||||
exit := emitText(&buf, plans, false)
|
||||
exit := emitText(&buf, plans, false, nil)
|
||||
if exit != 0 {
|
||||
t.Errorf("want exit 0 (clean), got %d", exit)
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ func TestEmitText_SinglePlanDirty(t *testing.T) {
|
|||
},
|
||||
}}
|
||||
var buf bytes.Buffer
|
||||
exit := emitText(&buf, plans, false)
|
||||
exit := emitText(&buf, plans, false, nil)
|
||||
if exit != 1 {
|
||||
t.Errorf("want exit 1 (error present), got %d", exit)
|
||||
}
|
||||
|
|
@ -82,7 +82,7 @@ func TestEmitText_MultiPlan(t *testing.T) {
|
|||
},
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
exit := emitText(&buf, plans, false)
|
||||
exit := emitText(&buf, plans, false, nil)
|
||||
if exit != 1 {
|
||||
t.Errorf("want exit 1 (error in 3rd plan), got %d", exit)
|
||||
}
|
||||
|
|
@ -114,10 +114,10 @@ func TestEmitText_StrictPromotesWarnings(t *testing.T) {
|
|||
{Path: "prd.md", Line: 1, Severity: SevWarning, Code: CodeEARSViolation, Message: "w"},
|
||||
},
|
||||
}}
|
||||
if got := emitText(new(bytes.Buffer), plans, false); got != 0 {
|
||||
if got := emitText(new(bytes.Buffer), plans, false, nil); got != 0 {
|
||||
t.Errorf("non-strict warning-only: want exit 0, got %d", got)
|
||||
}
|
||||
if got := emitText(new(bytes.Buffer), plans, true); got != 1 {
|
||||
if got := emitText(new(bytes.Buffer), plans, true, nil); got != 1 {
|
||||
t.Errorf("strict warning-only: want exit 1, got %d", got)
|
||||
}
|
||||
}
|
||||
|
|
@ -141,7 +141,7 @@ func TestEmitJSON_ValidJSONL(t *testing.T) {
|
|||
},
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
exit := emitJSON(&buf, plans, false)
|
||||
exit := emitJSON(&buf, plans, false, nil)
|
||||
if exit != 1 {
|
||||
t.Errorf("want exit 1, got %d", exit)
|
||||
}
|
||||
|
|
@ -183,7 +183,7 @@ func TestEmitJSON_CleanPlans(t *testing.T) {
|
|||
{PlanDir: "b"},
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
exit := emitJSON(&buf, plans, false)
|
||||
exit := emitJSON(&buf, plans, false, nil)
|
||||
if exit != 0 {
|
||||
t.Errorf("want exit 0, got %d", exit)
|
||||
}
|
||||
|
|
@ -236,12 +236,172 @@ func TestEmit_ExitCodes(t *testing.T) {
|
|||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := emitText(new(bytes.Buffer), tc.plans, tc.strict); got != tc.want {
|
||||
if got := emitText(new(bytes.Buffer), tc.plans, tc.strict, nil); got != tc.want {
|
||||
t.Errorf("emitText: want %d, got %d", tc.want, got)
|
||||
}
|
||||
if got := emitJSON(new(bytes.Buffer), tc.plans, tc.strict); got != tc.want {
|
||||
if got := emitJSON(new(bytes.Buffer), tc.plans, tc.strict, nil); got != tc.want {
|
||||
t.Errorf("emitJSON: want %d, got %d", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// spec:26174-planctl-context-tokens/R3.4+R3.5+R4.1+D§4.3
|
||||
func TestEmitText_ContextInfo(t *testing.T) {
|
||||
plans := []PlanResult{{PlanDir: "p", TaskCount: 1}}
|
||||
ctx := &TokenCtx{Used: 142000, Limit: 200000} // 71% → info
|
||||
var buf bytes.Buffer
|
||||
emitText(&buf, plans, false, ctx)
|
||||
want := "context: 142 k / 200 k tokens (71%) — plan to wrap up this session soon.\n"
|
||||
if !strings.Contains(buf.String(), want) {
|
||||
t.Errorf("missing context line, got %q", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
// spec:26174-planctl-context-tokens/R4.1+D§4.3
|
||||
func TestEmitText_ContextWarn(t *testing.T) {
|
||||
plans := []PlanResult{{PlanDir: "p", TaskCount: 1}}
|
||||
ctx := &TokenCtx{Used: 174000, Limit: 200000} // 87%
|
||||
var buf bytes.Buffer
|
||||
emitText(&buf, plans, false, ctx)
|
||||
want := "context: 174 k / 200 k tokens (87%) — commit current work and start a new session after this task.\n"
|
||||
if !strings.Contains(buf.String(), want) {
|
||||
t.Errorf("missing warn context line, got %q", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
// spec:26174-planctl-context-tokens/R4.1+D§4.3
|
||||
func TestEmitText_ContextError(t *testing.T) {
|
||||
plans := []PlanResult{{PlanDir: "p", TaskCount: 1}}
|
||||
ctx := &TokenCtx{Used: 192000, Limit: 200000} // 96%
|
||||
var buf bytes.Buffer
|
||||
emitText(&buf, plans, false, ctx)
|
||||
want := "context: 192 k / 200 k tokens (96%) — stop new work; commit and close out immediately.\n"
|
||||
if !strings.Contains(buf.String(), want) {
|
||||
t.Errorf("missing error context line, got %q", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
// spec:26174-planctl-context-tokens/R3.5+D§4.3
|
||||
func TestEmitText_ContextNormalSilent(t *testing.T) {
|
||||
plans := []PlanResult{{PlanDir: "p", TaskCount: 1}}
|
||||
ctx := &TokenCtx{Used: 50000, Limit: 200000} // 25% → Normal band, silent
|
||||
var buf bytes.Buffer
|
||||
emitText(&buf, plans, false, ctx)
|
||||
if strings.Contains(buf.String(), "context:") {
|
||||
t.Errorf("Normal band should emit no context line, got %q", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
// spec:26174-planctl-context-tokens/R3.3+R4.2+D§4.3
|
||||
func TestEmitText_ContextUnknownLimit(t *testing.T) {
|
||||
plans := []PlanResult{{PlanDir: "p", TaskCount: 1}}
|
||||
ctx := &TokenCtx{Used: 143000, Limit: 0}
|
||||
var buf bytes.Buffer
|
||||
emitText(&buf, plans, false, ctx)
|
||||
want := "context: 143 k tokens (limit unknown)\n"
|
||||
if !strings.Contains(buf.String(), want) {
|
||||
t.Errorf("missing unknown-limit context line, got %q", buf.String())
|
||||
}
|
||||
if strings.Contains(buf.String(), "—") {
|
||||
t.Errorf("unknown-limit should emit no recommendation suffix, got %q", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
// spec:26174-planctl-context-tokens/R4.3+D§4.3
|
||||
func TestEmitText_ContextMultiPlanOnce(t *testing.T) {
|
||||
plans := []PlanResult{
|
||||
{PlanDir: "a", TaskCount: 1},
|
||||
{PlanDir: "b", TaskCount: 1},
|
||||
}
|
||||
ctx := &TokenCtx{Used: 142000, Limit: 200000}
|
||||
var buf bytes.Buffer
|
||||
emitText(&buf, plans, false, ctx)
|
||||
out := buf.String()
|
||||
n := strings.Count(out, "context:")
|
||||
if n != 1 {
|
||||
t.Errorf("multi-plan should emit context line exactly once, got %d\n%s", n, out)
|
||||
}
|
||||
// Must come AFTER the aggregate summary line ("2 plans linted, ...").
|
||||
agg := strings.Index(out, "2 plans linted")
|
||||
ctxIdx := strings.Index(out, "context:")
|
||||
if agg < 0 || ctxIdx < 0 || ctxIdx < agg {
|
||||
t.Errorf("context line must follow aggregate summary; agg=%d ctx=%d\n%s", agg, ctxIdx, out)
|
||||
}
|
||||
}
|
||||
|
||||
// spec:26174-planctl-context-tokens/R5.1+R5.4+D§4.4
|
||||
func TestEmitJSON_ContextWindowKnown(t *testing.T) {
|
||||
plans := []PlanResult{{PlanDir: "p", TaskCount: 1}}
|
||||
ctx := &TokenCtx{Used: 142000, Limit: 200000}
|
||||
var buf bytes.Buffer
|
||||
emitJSON(&buf, plans, false, ctx)
|
||||
// Last line is the summary object.
|
||||
lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
|
||||
last := lines[len(lines)-1]
|
||||
var obj struct {
|
||||
Summary map[string]int `json:"summary"`
|
||||
ContextWindow struct {
|
||||
TokensUsed int64 `json:"tokens_used"`
|
||||
TokensLimit int64 `json:"tokens_limit"`
|
||||
Pct int `json:"pct"`
|
||||
Severity string `json:"severity"`
|
||||
Recommendation string `json:"recommendation"`
|
||||
} `json:"context_window"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(last), &obj); err != nil {
|
||||
t.Fatalf("unmarshal: %v\n%s", err, last)
|
||||
}
|
||||
if obj.Summary["errors"] != 0 || obj.Summary["warnings"] != 0 {
|
||||
t.Errorf("context should not affect error/warning counts: %+v", obj.Summary)
|
||||
}
|
||||
if obj.ContextWindow.TokensUsed != 142000 || obj.ContextWindow.TokensLimit != 200000 {
|
||||
t.Errorf("tokens = %+v", obj.ContextWindow)
|
||||
}
|
||||
if obj.ContextWindow.Pct != 71 || obj.ContextWindow.Severity != "info" {
|
||||
t.Errorf("pct/severity wrong: %+v", obj.ContextWindow)
|
||||
}
|
||||
if obj.ContextWindow.Recommendation == "" {
|
||||
t.Error("recommendation empty")
|
||||
}
|
||||
}
|
||||
|
||||
// spec:26174-planctl-context-tokens/R5.2+D§4.4
|
||||
func TestEmitJSON_ContextWindowUnknownLimit(t *testing.T) {
|
||||
plans := []PlanResult{{PlanDir: "p", TaskCount: 1}}
|
||||
ctx := &TokenCtx{Used: 143000, Limit: 0}
|
||||
var buf bytes.Buffer
|
||||
emitJSON(&buf, plans, false, ctx)
|
||||
lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
|
||||
last := lines[len(lines)-1]
|
||||
// Should contain tokens_used but NOT tokens_limit/pct/severity/recommendation.
|
||||
if !strings.Contains(last, `"tokens_used":143000`) {
|
||||
t.Errorf("missing tokens_used: %s", last)
|
||||
}
|
||||
for _, omitted := range []string{"tokens_limit", "pct", "severity", "recommendation"} {
|
||||
if strings.Contains(last, omitted) {
|
||||
t.Errorf("unknown-limit should omit %q: %s", omitted, last)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// spec:26174-planctl-context-tokens/R5.3+D§4.4
|
||||
func TestEmitJSON_ContextWindowNormalOmitted(t *testing.T) {
|
||||
plans := []PlanResult{{PlanDir: "p", TaskCount: 1}}
|
||||
ctx := &TokenCtx{Used: 50000, Limit: 200000} // Normal
|
||||
var buf bytes.Buffer
|
||||
emitJSON(&buf, plans, false, ctx)
|
||||
if strings.Contains(buf.String(), "context_window") {
|
||||
t.Errorf("Normal band should omit context_window: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
// spec:26174-planctl-context-tokens/R5.3+D§4.4
|
||||
func TestEmitJSON_ContextWindowNilOmitted(t *testing.T) {
|
||||
plans := []PlanResult{{PlanDir: "p", TaskCount: 1}}
|
||||
var buf bytes.Buffer
|
||||
emitJSON(&buf, plans, false, nil)
|
||||
if strings.Contains(buf.String(), "context_window") {
|
||||
t.Errorf("nil ctx should omit context_window: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,9 +55,16 @@ func run(args []string, stdout, stderr io.Writer) int {
|
|||
subcommand := args[0]
|
||||
rest := args[1:]
|
||||
|
||||
// spec:26174-planctl-context-tokens/R3.4+D§1+D§2
|
||||
// Resolve session token context once per invocation, at the top of run()
|
||||
// so it's available to every subcommand handler (current and future) and
|
||||
// so it can't be skipped by an early subcommand failure. ReadTokenCtx
|
||||
// returns nil when no session data is available; all emitters handle nil.
|
||||
ctx := ReadTokenCtx()
|
||||
|
||||
switch subcommand {
|
||||
case "lint":
|
||||
return runLint(rest, stdout, stderr)
|
||||
return runLint(rest, stdout, stderr, ctx)
|
||||
case "list", "next", "complete", "new-plan":
|
||||
// spec:planctl/R6.1
|
||||
fmt.Fprintf(stderr, "planctl: subcommand %q is not implemented in v1; planned for a future version\n", subcommand)
|
||||
|
|
@ -78,7 +85,7 @@ func run(args []string, stdout, stderr io.Writer) int {
|
|||
//
|
||||
// v1 scope per PRD R6.2: strictly read-only — no writes to any plan
|
||||
// file.
|
||||
func runLint(args []string, stdout, stderr io.Writer) int {
|
||||
func runLint(args []string, stdout, stderr io.Writer, ctx *TokenCtx) int {
|
||||
flags, err := parseLintFlags(args)
|
||||
if err != nil {
|
||||
fmt.Fprintln(stderr, err.Error())
|
||||
|
|
@ -104,9 +111,9 @@ func runLint(args []string, stdout, stderr io.Writer) int {
|
|||
results = append(results, lintPlan(p, flags))
|
||||
}
|
||||
if flags.format == "json" {
|
||||
return emitJSON(stdout, results, flags.strict)
|
||||
return emitJSON(stdout, results, flags.strict, ctx)
|
||||
}
|
||||
return emitText(stdout, results, flags.strict)
|
||||
return emitText(stdout, results, flags.strict, ctx)
|
||||
}
|
||||
|
||||
// spec:planctl/R5.3+D§3.5
|
||||
|
|
|
|||
|
|
@ -390,6 +390,8 @@ func runGoldenFixture(t *testing.T, fixture string) {
|
|||
t.Fatalf("abs %s: %v", fixture, err)
|
||||
}
|
||||
args, cwd := loadFixtureInvocation(t, fixture)
|
||||
// spec:26174-planctl-context-tokens/R3.4+R4.1+D§7.2
|
||||
setupFixtureContextEnv(t, fixture)
|
||||
var stdout, stderr bytes.Buffer
|
||||
restore := chdir(t, cwd)
|
||||
defer restore()
|
||||
|
|
@ -428,6 +430,62 @@ func runGoldenFixture(t *testing.T, fixture string) {
|
|||
}
|
||||
}
|
||||
|
||||
// spec:26174-planctl-context-tokens/R3.4+R4.1+D§7.2
|
||||
// setupFixtureContextEnv wires per-fixture environment for the context-window
|
||||
// feature:
|
||||
//
|
||||
// - env.txt — one KEY=VALUE line per env var; trimmed, `#` comments skipped.
|
||||
// Each key is pinned via t.Setenv (auto-restored on test end).
|
||||
// - transcript.jsonl — staged under a temp HOME at
|
||||
// ~/.claude/projects/<slug>/<uuid>.jsonl where slug is derived from
|
||||
// CLAUDE_PROJECT_DIR (or fixture dir) with `/` → `-` and uuid is
|
||||
// CLAUDE_SESSION_ID. When transcript.jsonl is present but the required
|
||||
// env keys aren't, the staging step is skipped silently so non-context
|
||||
// fixtures remain unaffected.
|
||||
//
|
||||
// Fixtures without env.txt or transcript.jsonl are untouched — baseline fixtures
|
||||
// stay byte-identical.
|
||||
func setupFixtureContextEnv(t *testing.T, fixture string) {
|
||||
t.Helper()
|
||||
envPath := filepath.Join(fixture, "env.txt")
|
||||
if b, err := os.ReadFile(envPath); err == nil {
|
||||
for _, line := range strings.Split(string(b), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
k, v, ok := strings.Cut(line, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
t.Setenv(strings.TrimSpace(k), strings.TrimSpace(v))
|
||||
}
|
||||
}
|
||||
tp := filepath.Join(fixture, "transcript.jsonl")
|
||||
tb, err := os.ReadFile(tp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Need a uuid + a slug to derive the transcript path. If either is missing,
|
||||
// skip staging — the fixture under test probably doesn't need it to pass.
|
||||
uuid := os.Getenv("CLAUDE_SESSION_ID")
|
||||
projectDir := os.Getenv("CLAUDE_PROJECT_DIR")
|
||||
if uuid == "" || projectDir == "" {
|
||||
return
|
||||
}
|
||||
// Stage a temp HOME and drop the transcript at the exact path planctl will look up.
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
slug := strings.ReplaceAll(projectDir, "/", "-")
|
||||
dir := filepath.Join(home, ".claude", "projects", slug)
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
t.Fatalf("mkdir transcript dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, uuid+".jsonl"), tb, 0o600); err != nil {
|
||||
t.Fatalf("stage transcript: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// loadFixtureInvocation reads args.txt and cwd-rel.txt if present. The
|
||||
// args.txt tokens may reference the literal `FIXTURE` substring, which
|
||||
// is substituted with the absolute path of the fixture directory so
|
||||
|
|
|
|||
3
cmd/planctl/testdata/v2/context-error/env.txt
vendored
Normal file
3
cmd/planctl/testdata/v2/context-error/env.txt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
CLAUDE_SESSION_ID=fixture-uuid
|
||||
CLAUDE_PROJECT_DIR=/fixture/project
|
||||
CLAUDE_CODE_MAX_CONTEXT_TOKENS=200000
|
||||
1
cmd/planctl/testdata/v2/context-error/expected.exit
vendored
Normal file
1
cmd/planctl/testdata/v2/context-error/expected.exit
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
0
|
||||
2
cmd/planctl/testdata/v2/context-error/expected.golden
vendored
Normal file
2
cmd/planctl/testdata/v2/context-error/expected.golden
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
context-error: clean (1 tasks, 1 requirements, 0 design sections)
|
||||
context: 192 k / 200 k tokens (96%) — stop new work; commit and close out immediately.
|
||||
6
cmd/planctl/testdata/v2/context-error/prd.md
vendored
Normal file
6
cmd/planctl/testdata/v2/context-error/prd.md
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# PRD — context fixture
|
||||
|
||||
## 4. Functional Requirements
|
||||
|
||||
### R1. Greet
|
||||
- R1.1 THE SYSTEM SHALL greet the user on startup.
|
||||
3
cmd/planctl/testdata/v2/context-error/tasks.md
vendored
Normal file
3
cmd/planctl/testdata/v2/context-error/tasks.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Tasks — context fixture
|
||||
|
||||
- [ ] 1.0 Implement greet _Requirements: R1.1_
|
||||
2
cmd/planctl/testdata/v2/context-error/transcript.jsonl
vendored
Normal file
2
cmd/planctl/testdata/v2/context-error/transcript.jsonl
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{"type":"user","message":{"content":"hi"}}
|
||||
{"type":"assistant","sessionId":"fixture-uuid","message":{"usage":{"input_tokens":19200,"cache_creation_input_tokens":38400,"cache_read_input_tokens":134400,"output_tokens":100}}}
|
||||
3
cmd/planctl/testdata/v2/context-info/env.txt
vendored
Normal file
3
cmd/planctl/testdata/v2/context-info/env.txt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
CLAUDE_SESSION_ID=fixture-uuid
|
||||
CLAUDE_PROJECT_DIR=/fixture/project
|
||||
CLAUDE_CODE_MAX_CONTEXT_TOKENS=200000
|
||||
1
cmd/planctl/testdata/v2/context-info/expected.exit
vendored
Normal file
1
cmd/planctl/testdata/v2/context-info/expected.exit
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
0
|
||||
2
cmd/planctl/testdata/v2/context-info/expected.golden
vendored
Normal file
2
cmd/planctl/testdata/v2/context-info/expected.golden
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
context-info: clean (1 tasks, 1 requirements, 0 design sections)
|
||||
context: 142 k / 200 k tokens (71%) — plan to wrap up this session soon.
|
||||
6
cmd/planctl/testdata/v2/context-info/prd.md
vendored
Normal file
6
cmd/planctl/testdata/v2/context-info/prd.md
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# PRD — context fixture
|
||||
|
||||
## 4. Functional Requirements
|
||||
|
||||
### R1. Greet
|
||||
- R1.1 THE SYSTEM SHALL greet the user on startup.
|
||||
3
cmd/planctl/testdata/v2/context-info/tasks.md
vendored
Normal file
3
cmd/planctl/testdata/v2/context-info/tasks.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Tasks — context fixture
|
||||
|
||||
- [ ] 1.0 Implement greet _Requirements: R1.1_
|
||||
2
cmd/planctl/testdata/v2/context-info/transcript.jsonl
vendored
Normal file
2
cmd/planctl/testdata/v2/context-info/transcript.jsonl
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{"type":"user","message":{"content":"hi"}}
|
||||
{"type":"assistant","sessionId":"fixture-uuid","message":{"usage":{"input_tokens":14200,"cache_creation_input_tokens":28400,"cache_read_input_tokens":99400,"output_tokens":100}}}
|
||||
1
cmd/planctl/testdata/v2/context-json-warn/args.txt
vendored
Normal file
1
cmd/planctl/testdata/v2/context-json-warn/args.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
lint --format=json FIXTURE
|
||||
3
cmd/planctl/testdata/v2/context-json-warn/env.txt
vendored
Normal file
3
cmd/planctl/testdata/v2/context-json-warn/env.txt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
CLAUDE_SESSION_ID=fixture-uuid
|
||||
CLAUDE_PROJECT_DIR=/fixture/project
|
||||
CLAUDE_CODE_MAX_CONTEXT_TOKENS=200000
|
||||
1
cmd/planctl/testdata/v2/context-json-warn/expected.exit
vendored
Normal file
1
cmd/planctl/testdata/v2/context-json-warn/expected.exit
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
0
|
||||
1
cmd/planctl/testdata/v2/context-json-warn/expected.golden
vendored
Normal file
1
cmd/planctl/testdata/v2/context-json-warn/expected.golden
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"summary":{"plans":1,"errors":0,"warnings":0},"context_window":{"tokens_used":174000,"tokens_limit":200000,"pct":87,"severity":"warn","recommendation":"commit current work and start a new session after this task."}}
|
||||
6
cmd/planctl/testdata/v2/context-json-warn/prd.md
vendored
Normal file
6
cmd/planctl/testdata/v2/context-json-warn/prd.md
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# PRD — context-json-warn
|
||||
|
||||
## 4. Functional Requirements
|
||||
|
||||
### R1. Greet
|
||||
- R1.1 THE SYSTEM SHALL greet the user on startup.
|
||||
3
cmd/planctl/testdata/v2/context-json-warn/tasks.md
vendored
Normal file
3
cmd/planctl/testdata/v2/context-json-warn/tasks.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Tasks — context-json-warn
|
||||
|
||||
- [ ] 1.0 Implement greet _Requirements: R1.1_
|
||||
2
cmd/planctl/testdata/v2/context-json-warn/transcript.jsonl
vendored
Normal file
2
cmd/planctl/testdata/v2/context-json-warn/transcript.jsonl
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{"type":"user","message":{"content":"hi"}}
|
||||
{"type":"assistant","sessionId":"fixture-uuid","message":{"usage":{"input_tokens":17400,"cache_creation_input_tokens":34800,"cache_read_input_tokens":121800,"output_tokens":100}}}
|
||||
1
cmd/planctl/testdata/v2/context-multi-plan/args.txt
vendored
Normal file
1
cmd/planctl/testdata/v2/context-multi-plan/args.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
lint
|
||||
6
cmd/planctl/testdata/v2/context-multi-plan/dev/plans/26170-alpha/prd.md
vendored
Normal file
6
cmd/planctl/testdata/v2/context-multi-plan/dev/plans/26170-alpha/prd.md
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# PRD — alpha
|
||||
|
||||
## 4. Functional Requirements
|
||||
|
||||
### R1. Greet
|
||||
- R1.1 THE SYSTEM SHALL greet the user on startup.
|
||||
3
cmd/planctl/testdata/v2/context-multi-plan/dev/plans/26170-alpha/tasks.md
vendored
Normal file
3
cmd/planctl/testdata/v2/context-multi-plan/dev/plans/26170-alpha/tasks.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Tasks — alpha
|
||||
|
||||
- [ ] 1.0 Implement greet _Requirements: R1.1_
|
||||
6
cmd/planctl/testdata/v2/context-multi-plan/dev/plans/26172-beta/prd.md
vendored
Normal file
6
cmd/planctl/testdata/v2/context-multi-plan/dev/plans/26172-beta/prd.md
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# PRD — beta
|
||||
|
||||
## 4. Functional Requirements
|
||||
|
||||
### R1. Greet
|
||||
- R1.1 THE SYSTEM SHALL greet the user on startup.
|
||||
3
cmd/planctl/testdata/v2/context-multi-plan/dev/plans/26172-beta/tasks.md
vendored
Normal file
3
cmd/planctl/testdata/v2/context-multi-plan/dev/plans/26172-beta/tasks.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Tasks — beta
|
||||
|
||||
- [ ] 1.0 Implement greet _Requirements: R1.1_
|
||||
3
cmd/planctl/testdata/v2/context-multi-plan/env.txt
vendored
Normal file
3
cmd/planctl/testdata/v2/context-multi-plan/env.txt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
CLAUDE_SESSION_ID=fixture-uuid
|
||||
CLAUDE_PROJECT_DIR=/fixture/project
|
||||
CLAUDE_CODE_MAX_CONTEXT_TOKENS=200000
|
||||
1
cmd/planctl/testdata/v2/context-multi-plan/expected.exit
vendored
Normal file
1
cmd/planctl/testdata/v2/context-multi-plan/expected.exit
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
0
|
||||
7
cmd/planctl/testdata/v2/context-multi-plan/expected.golden
vendored
Normal file
7
cmd/planctl/testdata/v2/context-multi-plan/expected.golden
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
=== 26170-alpha ===
|
||||
26170-alpha: clean (1 tasks, 1 requirements, 0 design sections)
|
||||
|
||||
=== 26172-beta ===
|
||||
26172-beta: clean (1 tasks, 1 requirements, 0 design sections)
|
||||
2 plans linted, 0 errors, 0 warnings
|
||||
context: 142 k / 200 k tokens (71%) — plan to wrap up this session soon.
|
||||
2
cmd/planctl/testdata/v2/context-multi-plan/transcript.jsonl
vendored
Normal file
2
cmd/planctl/testdata/v2/context-multi-plan/transcript.jsonl
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{"type":"user","message":{"content":"hi"}}
|
||||
{"type":"assistant","sessionId":"fixture-uuid","message":{"usage":{"input_tokens":14200,"cache_creation_input_tokens":28400,"cache_read_input_tokens":99400,"output_tokens":100}}}
|
||||
2
cmd/planctl/testdata/v2/context-no-limit/env.txt
vendored
Normal file
2
cmd/planctl/testdata/v2/context-no-limit/env.txt
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
CLAUDE_SESSION_ID=fixture-uuid
|
||||
CLAUDE_PROJECT_DIR=/fixture/project
|
||||
1
cmd/planctl/testdata/v2/context-no-limit/expected.exit
vendored
Normal file
1
cmd/planctl/testdata/v2/context-no-limit/expected.exit
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
0
|
||||
2
cmd/planctl/testdata/v2/context-no-limit/expected.golden
vendored
Normal file
2
cmd/planctl/testdata/v2/context-no-limit/expected.golden
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
context-no-limit: clean (1 tasks, 1 requirements, 0 design sections)
|
||||
context: 143 k tokens (limit unknown)
|
||||
6
cmd/planctl/testdata/v2/context-no-limit/prd.md
vendored
Normal file
6
cmd/planctl/testdata/v2/context-no-limit/prd.md
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# PRD — context fixture
|
||||
|
||||
## 4. Functional Requirements
|
||||
|
||||
### R1. Greet
|
||||
- R1.1 THE SYSTEM SHALL greet the user on startup.
|
||||
3
cmd/planctl/testdata/v2/context-no-limit/tasks.md
vendored
Normal file
3
cmd/planctl/testdata/v2/context-no-limit/tasks.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Tasks — context fixture
|
||||
|
||||
- [ ] 1.0 Implement greet _Requirements: R1.1_
|
||||
2
cmd/planctl/testdata/v2/context-no-limit/transcript.jsonl
vendored
Normal file
2
cmd/planctl/testdata/v2/context-no-limit/transcript.jsonl
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{"type":"user","message":{"content":"hi"}}
|
||||
{"type":"assistant","sessionId":"fixture-uuid","message":{"usage":{"input_tokens":14300,"cache_creation_input_tokens":28600,"cache_read_input_tokens":100100,"output_tokens":100}}}
|
||||
1
cmd/planctl/testdata/v2/context-no-session/expected.exit
vendored
Normal file
1
cmd/planctl/testdata/v2/context-no-session/expected.exit
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
0
|
||||
1
cmd/planctl/testdata/v2/context-no-session/expected.golden
vendored
Normal file
1
cmd/planctl/testdata/v2/context-no-session/expected.golden
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
context-no-session: clean (1 tasks, 1 requirements, 0 design sections)
|
||||
6
cmd/planctl/testdata/v2/context-no-session/prd.md
vendored
Normal file
6
cmd/planctl/testdata/v2/context-no-session/prd.md
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# PRD — context fixture
|
||||
|
||||
## 4. Functional Requirements
|
||||
|
||||
### R1. Greet
|
||||
- R1.1 THE SYSTEM SHALL greet the user on startup.
|
||||
3
cmd/planctl/testdata/v2/context-no-session/tasks.md
vendored
Normal file
3
cmd/planctl/testdata/v2/context-no-session/tasks.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Tasks — context fixture
|
||||
|
||||
- [ ] 1.0 Implement greet _Requirements: R1.1_
|
||||
3
cmd/planctl/testdata/v2/context-normal/env.txt
vendored
Normal file
3
cmd/planctl/testdata/v2/context-normal/env.txt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
CLAUDE_SESSION_ID=fixture-uuid
|
||||
CLAUDE_PROJECT_DIR=/fixture/project
|
||||
CLAUDE_CODE_MAX_CONTEXT_TOKENS=200000
|
||||
1
cmd/planctl/testdata/v2/context-normal/expected.exit
vendored
Normal file
1
cmd/planctl/testdata/v2/context-normal/expected.exit
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
0
|
||||
1
cmd/planctl/testdata/v2/context-normal/expected.golden
vendored
Normal file
1
cmd/planctl/testdata/v2/context-normal/expected.golden
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
context-normal: clean (1 tasks, 1 requirements, 0 design sections)
|
||||
6
cmd/planctl/testdata/v2/context-normal/prd.md
vendored
Normal file
6
cmd/planctl/testdata/v2/context-normal/prd.md
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# PRD — context fixture
|
||||
|
||||
## 4. Functional Requirements
|
||||
|
||||
### R1. Greet
|
||||
- R1.1 THE SYSTEM SHALL greet the user on startup.
|
||||
3
cmd/planctl/testdata/v2/context-normal/tasks.md
vendored
Normal file
3
cmd/planctl/testdata/v2/context-normal/tasks.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Tasks — context fixture
|
||||
|
||||
- [ ] 1.0 Implement greet _Requirements: R1.1_
|
||||
2
cmd/planctl/testdata/v2/context-normal/transcript.jsonl
vendored
Normal file
2
cmd/planctl/testdata/v2/context-normal/transcript.jsonl
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{"type":"user","message":{"content":"hi"}}
|
||||
{"type":"assistant","sessionId":"fixture-uuid","message":{"usage":{"input_tokens":10000,"cache_creation_input_tokens":20000,"cache_read_input_tokens":70000,"output_tokens":100}}}
|
||||
3
cmd/planctl/testdata/v2/context-warn/env.txt
vendored
Normal file
3
cmd/planctl/testdata/v2/context-warn/env.txt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
CLAUDE_SESSION_ID=fixture-uuid
|
||||
CLAUDE_PROJECT_DIR=/fixture/project
|
||||
CLAUDE_CODE_MAX_CONTEXT_TOKENS=200000
|
||||
1
cmd/planctl/testdata/v2/context-warn/expected.exit
vendored
Normal file
1
cmd/planctl/testdata/v2/context-warn/expected.exit
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
0
|
||||
2
cmd/planctl/testdata/v2/context-warn/expected.golden
vendored
Normal file
2
cmd/planctl/testdata/v2/context-warn/expected.golden
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
context-warn: clean (1 tasks, 1 requirements, 0 design sections)
|
||||
context: 174 k / 200 k tokens (87%) — commit current work and start a new session after this task.
|
||||
6
cmd/planctl/testdata/v2/context-warn/prd.md
vendored
Normal file
6
cmd/planctl/testdata/v2/context-warn/prd.md
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# PRD — context fixture
|
||||
|
||||
## 4. Functional Requirements
|
||||
|
||||
### R1. Greet
|
||||
- R1.1 THE SYSTEM SHALL greet the user on startup.
|
||||
3
cmd/planctl/testdata/v2/context-warn/tasks.md
vendored
Normal file
3
cmd/planctl/testdata/v2/context-warn/tasks.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Tasks — context fixture
|
||||
|
||||
- [ ] 1.0 Implement greet _Requirements: R1.1_
|
||||
2
cmd/planctl/testdata/v2/context-warn/transcript.jsonl
vendored
Normal file
2
cmd/planctl/testdata/v2/context-warn/transcript.jsonl
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{"type":"user","message":{"content":"hi"}}
|
||||
{"type":"assistant","sessionId":"fixture-uuid","message":{"usage":{"input_tokens":17400,"cache_creation_input_tokens":34800,"cache_read_input_tokens":121800,"output_tokens":100}}}
|
||||
|
|
@ -2,3 +2,4 @@
|
|||
- 2026-04-23T17:41:00Z design-review 019dbb64-1df2-7842-85f4-ab58efac6bb4
|
||||
- 2026-04-23T17:44:50Z tasks-review 019dbc84-7470-7130-95f7-38678a69dd9a
|
||||
- 2026-04-23T17:28:43Z code-review-parent-1 019dbcac-a359-72c1-b3ba-1d9f9c5105fb
|
||||
- 2026-04-23T18:31:09Z code-review-parent-2 019dbce5-cb69-7eb0-b82f-58ce99a04562
|
||||
|
|
|
|||
|
|
@ -53,15 +53,15 @@ As you complete each task, flip `[ ]` to `[x]` in this file. Update after each s
|
|||
- [x] 1.8 Add `resolveSessionUUID() string` per design §3.3 — env var first; then up-to-8-hop PID walk via injectable-helper inner function `resolveViaPIDWalk`. Use unexported `var pidSessionDir = "/tmp/planctl-sessions"` for testability. Add `TestResolveSessionUUID_envWins` + `TestResolveViaPIDWalk_pidWalkFinds` / `_staleSkipped` / `_startTimeMismatch` / `_exhausted`. _Requirements: R1.1, R1.2, R1.3, R1.4, R6.1, R6.3_ _Design: D§3.3, D§6.2, D§6.3, D§7.1_
|
||||
- [x] 1.9 Wire `ReadTokenCtx() *TokenCtx` per design §3.2 — compose resolveSessionUUID → transcriptPath → parseLastUsage → readLimit; nil at every failure. Add `TestReadTokenCtx_happyPath` and `_noSession`. No subprocesses invoked (D§6.4). _Requirements: R1.1-R1.6, R2.1-R2.6_ _Design: D§3.2, D§6.4, D§7.1_
|
||||
|
||||
- [ ] 2.0 Wire token context into planctl output (`main.go`, `emit.go`, E2E golden tests) _Requirements: R3.4, R4.1-R4.5, R5.1-R5.4_ _Design: D§4.3, D§4.4, D§5.3, D§5.4, D§7.2_
|
||||
- [ ] 2.1 Extend `emit.go` types: add `jsonCtxWin` struct with `TokensUsed int64`, `TokensLimit *int64`, `Pct *int`, `Severity string`, `Recommendation string` (pointer fields `omitempty`); extend `jsonSummary` with `ContextWin *jsonCtxWin`. _Requirements: R5.1, R5.2, R5.3_ _Design: D§5.4_
|
||||
- [ ] 2.2 Change `emitText` and `emitJSON` signatures to accept `*TokenCtx`. Text: canonical context line for Info/Warn/Error, unknown-limit form, multi-plan once after aggregate, Normal/nil silent. JSON: `context_window` for Info/Warn/Error, omit for Normal/nil; context never changes error/warning counts. NOTE: 2.1, 2.2, 2.3 must all land in the same parent-task commit — signature change breaks callers immediately. _Requirements: R3.4, R3.5, R4.1-R4.3, R4.5, R5.1-R5.4_ _Design: D§4.3, D§4.4, D§5.3_
|
||||
- [ ] 2.3 Update `main.go`: call `ReadTokenCtx()` once at entry; thread `ctx` to `emitText`/`emitJSON` for all subcommands. Must land with 2.1/2.2 (see note on 2.2). _Requirements: R3.4_ _Design: D§1, D§2_
|
||||
- [ ] 2.4 Update `emit_test.go` and `main_test.go` call sites to pass `nil` ctx where no context asserted. Add `emit_test.go` cases: text info/warn/error band, unknown-limit, Normal omission, JSON known+unknown shapes, multi-plan placement. _Requirements: R4.1-R4.5, R5.1-R5.4_ _Design: D§4.3, D§4.4, D§7.2_
|
||||
- [ ] 2.5 Extend golden-fixture harness: if `env.txt` exists load KEY=VALUE via `t.Setenv`; if `transcript.jsonl` exists stage in temp HOME at `~/.claude/projects/<slug>/<uuid>.jsonl`. _Requirements: R3.4, R4.1_ _Design: D§7.2_
|
||||
- [ ] 2.6 Create `testdata/v2/context-info/`, `context-warn/`, `context-error/`, `context-normal/`, `context-no-session/`, `context-no-limit/` — each with `prd.md`, `tasks.md`, `env.txt`, `transcript.jsonl` (where applicable), `expected.golden`, `expected.exit`. _Requirements: R3.2, R3.3, R3.5, R4.1, R4.2, R4.5_ _Design: D§4.3, D§7.2_
|
||||
- [ ] 2.7 Create `testdata/v2/context-json-warn/` (warn band, JSON format) and `testdata/v2/context-multi-plan/` (two plan dirs, context once after aggregate). _Requirements: R4.3, R5.1-R5.4_ _Design: D§4.3, D§4.4, D§7.2_
|
||||
- [ ] 2.8 Run `go test ./cmd/planctl/...`; all existing fixtures byte-identical, eight new context fixtures pass. _Requirements: R4.5_ _Design: D§7.2_
|
||||
- [x] 2.0 Wire token context into planctl output (`main.go`, `emit.go`, E2E golden tests) _Requirements: R3.4, R4.1-R4.5, R5.1-R5.4_ _Design: D§4.3, D§4.4, D§5.3, D§5.4, D§7.2_
|
||||
- [x] 2.1 Extend `emit.go` types: add `jsonCtxWin` struct with `TokensUsed int64`, `TokensLimit *int64`, `Pct *int`, `Severity string`, `Recommendation string` (pointer fields `omitempty`); extend `jsonSummary` with `ContextWin *jsonCtxWin`. _Requirements: R5.1, R5.2, R5.3_ _Design: D§5.4_
|
||||
- [x] 2.2 Change `emitText` and `emitJSON` signatures to accept `*TokenCtx`. Text: canonical context line for Info/Warn/Error, unknown-limit form, multi-plan once after aggregate, Normal/nil silent. JSON: `context_window` for Info/Warn/Error, omit for Normal/nil; context never changes error/warning counts. NOTE: 2.1, 2.2, 2.3 must all land in the same parent-task commit — signature change breaks callers immediately. _Requirements: R3.4, R3.5, R4.1-R4.3, R4.5, R5.1-R5.4_ _Design: D§4.3, D§4.4, D§5.3_
|
||||
- [x] 2.3 Update `main.go`: call `ReadTokenCtx()` once at entry; thread `ctx` to `emitText`/`emitJSON` for all subcommands _(only `lint` in this v1 — other v2 commands aren't in the current codebase)_. _Requirements: R3.4_ _Design: D§1, D§2_
|
||||
- [x] 2.4 Update `emit_test.go` and `main_test.go` call sites to pass `nil` ctx where no context asserted. Add `emit_test.go` cases: text info/warn/error band, unknown-limit, Normal omission, JSON known+unknown shapes, multi-plan placement. _Requirements: R4.1-R4.5, R5.1-R5.4_ _Design: D§4.3, D§4.4, D§7.2_
|
||||
- [x] 2.5 Extend golden-fixture harness: if `env.txt` exists load KEY=VALUE via `t.Setenv`; if `transcript.jsonl` exists stage in temp HOME at `~/.claude/projects/<slug>/<uuid>.jsonl`. _Requirements: R3.4, R4.1_ _Design: D§7.2_
|
||||
- [x] 2.6 Create `testdata/v2/context-info/`, `context-warn/`, `context-error/`, `context-normal/`, `context-no-session/`, `context-no-limit/` — each with `prd.md`, `tasks.md`, `env.txt`, `transcript.jsonl` (where applicable), `expected.golden`, `expected.exit`. _Requirements: R3.2, R3.3, R3.5, R4.1, R4.2, R4.5_ _Design: D§4.3, D§7.2_
|
||||
- [x] 2.7 Create `testdata/v2/context-json-warn/` (warn band, JSON format) and `testdata/v2/context-multi-plan/` (two plan dirs, context once after aggregate). _Requirements: R4.3, R5.1-R5.4_ _Design: D§4.3, D§4.4, D§7.2_
|
||||
- [x] 2.8 Run `go test ./cmd/planctl/...`; all existing fixtures byte-identical, eight new context fixtures pass. _Requirements: R4.5_ _Design: D§7.2_
|
||||
|
||||
- [ ] 3.0 Extend `scripts/jj-hook.sh` with session tracking _Requirements: R7.1, R7.2, R7.3_ _Design: D§4.1, D§5.1, D§5.5, D§7.4_
|
||||
- [ ] 3.1 Add `planctl_start_ticks()` per design §5.5: Linux — safe `/proc/<pid>/stat` parse via `sed 's/.*)//' | awk '{print $20}'`; macOS — `ps -o lstart=` → `date` epoch seconds. Returns 0 on failure. _Requirements: R6.2, R7.3_ _Design: D§5.5_
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue