Skip to content

T-B5ZA-task-resolve-reports-behind-main

Status: closed/superseded · Impact: medium · Complexity: small

AUTO-DEFINED: this spec was best-effort machine-authored by /sdlc:task-auto-define on 2026-07-19 because the task is autonomy: autonomous/pr. Review the Goal, Approach, Today, Files-to-touch, and Acceptance-criteria carefully before trusting it.

/sdlc:task-work Step 1 resolves task ids against the local checkout only, so a stale checkout makes existing tasks look nonexistent. The fix is to fetch and compare HEAD against origin/main before resolving, and to report a commits-behind diagnosis naming the tasks when they are absent locally but present on the remote.

Spawned from T-1YSW-amend-d0001-solutions-tier on sksizer/dev.

/sdlc:task-work Step 1 resolves task ids against the local checkout only. During T-1YSW pickup ran against a checkout 38 commits behind origin/main, so sdlc task resolve reported NO TASK FOUND for all three requested tasks — they existed, just only on the remote. Step 1 should fetch and compare HEAD against origin/main before resolving, and when a requested task is absent locally but present on origin/main, report ‘N commits behind origin/main’ naming the tasks, rather than the misleading ‘NO TASK FOUND’. Touchpoints: apps/sdlc/skills/task-work/SKILL.md (Step 1) and apps/sdlc/lib/model/entities/task/ops/resolve.ts.

T-1YSW-amend-d0001-solutions-tier

LocationRole today
apps/sdlc/skills/task-work/SKILL.mdStep 1 resolves the argument with sdlc task resolve <arg> --output json and branches on the parsed resolved / reason. The not-found branch surfaces NO TASK FOUND for "<arg>" and exits — no remote is consulted.
apps/sdlc/lib/model/entities/task/ops/resolve.ts#resolveTaskFileThe pure resolver: absolute path, then exact filename, then prefix glob, then substring glob — every lookup against the working tree’s docs/planning/tasks/. Documented as pure (no shell, no LLM).
apps/sdlc/lib/model/entities/task/ops/resolve.ts#listTaskFilesReads the local tasks directory; the only place the candidate filename list comes from.
apps/sdlc/lib/model/entities/task/ops/resolve.ts#noTaskFoundMarkerBuilds the NO TASK FOUND for "<arg>" string the op’s cli.render hook writes to stderr at exit 1.
apps/sdlc/lib/model/entities/task/file-resolution.mdContract doc for the op: the three-row outcome table (resolved / not found / ambiguous) and the interactive vs non-interactive ambiguity policy.
apps/sdlc/lib/services/git/easy.ts#GitTyped git client over the CommandRunner seam. Carries fetch, revParse, mergeBase, showAtRev and the raw escape hatch, but no ahead/behind count and no tree listing.
apps/sdlc/lib/model/entities/task/ops/_probe_core.tsPrecedent for a task op doing git work through new Git(projectRoot, { runner: ctx.git }) rather than an ambient spawn.
apps/sdlc/lib/model/entities/task/ops/tests/resolve.test.tsAcceptance suite covering the current branches: absolute, exact, prefix glob, substring glob, ambiguous, not-found.
docs/skills/task-work.mdPer-skill doc whose Mermaid flowchart node S1 describes Step 1’s resolution branches.

sdlc task resolve gains an opt-in staleness probe. When the caller passes --check-remote and local resolution comes back not-found, the op fetches origin main, counts how far HEAD trails origin/main, and re-runs the same filename matcher against the task filenames present at origin/main. A hit there becomes a fourth outcome — behind-remote — carrying the commit count and the matching filenames, so /sdlc:task-work Step 1 can tell the operator “your checkout is N commits behind origin/main and these tasks only exist upstream” instead of the misleading “NO TASK FOUND”. Every path that cannot produce that evidence (probe off, fetch fails, checkout up to date, no match upstream) degrades to today’s exact not-found behaviour.

  1. In apps/sdlc/lib/services/git/easy.ts, add two query verbs next to mergeBase: revListCount(range: string): number | null (git rev-list --count <range>; null on a non-zero exit) and lsTreeNames(rev: string, path: string): string[] (git ls-tree --name-only <rev> -- <path>; [] on a non-zero exit). Both are QUERIES per the module’s tri-modal contract — they return absence rather than throwing.
  2. In apps/sdlc/lib/model/entities/task/ops/resolve.ts, extract the filename-matching body of resolveTaskFile (the exact / prefix-glob / substring-glob steps) into a pure matchTaskFilenames(files: string[], arg: string): { hit: string | null; candidates: string[] } that takes a filename list instead of reading the filesystem. resolveTaskFile keeps its signature and behaviour, now composing listTaskFiles with the new helper.
  3. Add checkRemote: z.boolean().default(false) to the op’s input (CLI flag --check-remote). Add "behind-remote" to the reason enum and add behind_count: z.number().nullable() and remote_candidates: z.array(z.string()) to output, defaulting to null and [] on every pre-existing branch so current JSON consumers see an unchanged payload.
  4. In the handler, run the probe only when checkRemote is true AND the local result is resolved: false with reason: "not-found" — an ambiguous local match is already actionable, and a successful resolve needs no diagnosis. Through new Git(projectRoot, { runner: ctx.git }):
    1. fetch("origin", "main") wrapped in try/catch — fetch is a checked mutation that throws CommandFailed, and a throw means offline or no remote, so return the unchanged not-found result.
    2. revListCount("HEAD..origin/main")null or 0 returns the unchanged not-found result (an up-to-date checkout genuinely lacks the task).
    3. lsTreeNames("origin/main", "docs/planning/tasks/"), reduced to basenames and fed to matchTaskFilenames. No hit returns the unchanged not-found result.
    4. Otherwise return reason: "behind-remote" with behind_count set and remote_candidates holding the single hit or the ambiguous candidate list.
  5. Export a behindRemoteMarker(arg, behindCount, candidates) builder beside noTaskFoundMarker, and add a behind-remote branch to the op’s cli.render hook that writes that marker to stderr and returns exit 1.
  6. Extend apps/sdlc/lib/model/entities/task/ops/tests/resolve.test.ts with the new cases, injecting a scripted fake CommandRunner as ctx.git via the createCtx seam the suite already imports. Leave the existing cases byte unchanged.
  7. Update apps/sdlc/lib/model/entities/task/file-resolution.md: add the Behind remote row to the “Outcome contract” table and a short staleness-probe subsection covering the --check-remote opt-in and the degrade-to-not-found rule.
  8. Update apps/sdlc/skills/task-work/SKILL.md Step 1: pass --check-remote on the resolve call, and add a behind-remote bullet to the per-outcome ACTION list that reports the commit count, names the remote_candidates, points the operator at git -C <project-root> pull --ff-only, and exits without printing NO TASK FOUND.
  9. Refresh docs/skills/task-work.md by running /sdlc:update-skill-doc task-work so the S1 flowchart node carries the new branch.
LocationKindChange
apps/sdlc/lib/services/git/easy.tsmodifyAdd the revListCount and lsTreeNames query verbs so the resolve op reads ahead/behind counts and remote tree listings through the typed client instead of raw argv.
apps/sdlc/lib/model/entities/task/ops/resolve.tsmodifyExtract matchTaskFilenames; add the checkRemote input, the behind-remote reason, the behind_count / remote_candidates output fields, the behindRemoteMarker builder, and the render branch.
apps/sdlc/lib/model/entities/task/ops/tests/resolve.test.tsmodifyAdd coverage for the probe-off no-git-calls case, the behind-and-present-upstream case, the behind-but-absent-upstream case, and the fetch-failure degrade case.
apps/sdlc/lib/model/entities/task/file-resolution.mdmodifyDocument the fourth outcome row and the --check-remote staleness probe.
apps/sdlc/skills/task-work/SKILL.mdmodifyStep 1 passes --check-remote and gains the behind-remote operator-facing branch.
docs/skills/task-work.mdmodifyRegenerated Step 1 flowchart node reflecting the new branch.
  • AC-1: sdlc task resolve <arg> without --check-remote issues zero git invocations — asserted in resolve.test.ts with a fake ctx.git runner that records argv and is expected to have an empty call list.
  • AC-2: sdlc task resolve <slug> --check-remote --output json, on a checkout whose origin/main carries docs/planning/tasks/<slug>.md while the working tree does not, returns resolved: false, reason: "behind-remote", a behind_count equal to git rev-list --count HEAD..origin/main, and a remote_candidates array containing that filename.
  • AC-3: the same invocation in TEXT mode writes one stderr line built by behindRemoteMarker that contains the commit count, the literal substring commits behind origin/main, and the candidate filenames, and exits 1; the substring NO TASK FOUND is absent from that output.
  • AC-4: sdlc task resolve <slug> --check-remote where <slug> matches no filename under docs/planning/tasks/ at origin/main returns reason: "not-found" and renders the unchanged NO TASK FOUND for "<arg>" marker at exit 1.
  • AC-5: when the fetch probe fails (fake runner returns a non-zero exit for the fetch argv), the op returns reason: "not-found" and exits 1 without throwing.
  • AC-6: apps/sdlc/skills/task-work/SKILL.md Step 1 enumerates three not-resolved branches — ambiguous, not-found, behind-remote — and the behind-remote bullet names both the commit count and the operator’s sync command.
  • AC-7: the “Outcome contract” table in apps/sdlc/lib/model/entities/task/file-resolution.md contains a Behind remote row whose Output column reproduces the marker text asserted in AC-3.
  • AC-8: bun test apps/sdlc/lib/model/entities/task/ops/tests/resolve.test.ts passes, and the six pre-existing cases in that file (absolute-exists, absolute-missing, exact-filename, prefix-glob, substring-glob, ambiguous) are unmodified.
  • Fetching on every sdlc task resolve call. The probe fires only on the not-found path, where the diagnosis is needed; an unconditional fetch would put a network round-trip in front of every skill that resolves a task.
  • The no-argument pickup path (sdlc task next), which has the same stale-checkout blind spot but a different remedy (roster-level, not per-argument).
  • Auto-syncing the checkout. The op reports the staleness; pulling stays an operator decision.
  • Remotes and base branches other than origin / main.
  • none

Spawned by /sdlc:spawn-task-pr on 2026-07-19 UTC from T-1YSW-amend-d0001-solutions-tier in https://github.com/sksizer/dev.


← Back to Tasks