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 resolvereported 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.
| Location | Role today |
|---|---|
apps/sdlc/skills/task-work/SKILL.md | Step 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#resolveTaskFile | The 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#listTaskFiles | Reads the local tasks directory; the only place the candidate filename list comes from. |
apps/sdlc/lib/model/entities/task/ops/resolve.ts#noTaskFoundMarker | Builds 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.md | Contract 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#Git | Typed 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.ts | Precedent 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.ts | Acceptance suite covering the current branches: absolute, exact, prefix glob, substring glob, ambiguous, not-found. |
docs/skills/task-work.md | Per-skill doc whose Mermaid flowchart node S1 describes Step 1’s resolution branches. |
Proposed
Section titled “Proposed”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.
Approach
Section titled “Approach”- In
apps/sdlc/lib/services/git/easy.ts, add two query verbs next tomergeBase:revListCount(range: string): number | null(git rev-list --count <range>; null on a non-zero exit) andlsTreeNames(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. - In
apps/sdlc/lib/model/entities/task/ops/resolve.ts, extract the filename-matching body ofresolveTaskFile(the exact / prefix-glob / substring-glob steps) into a purematchTaskFilenames(files: string[], arg: string): { hit: string | null; candidates: string[] }that takes a filename list instead of reading the filesystem.resolveTaskFilekeeps its signature and behaviour, now composinglistTaskFileswith the new helper. - Add
checkRemote: z.boolean().default(false)to the op’sinput(CLI flag--check-remote). Add"behind-remote"to thereasonenum and addbehind_count: z.number().nullable()andremote_candidates: z.array(z.string())tooutput, defaulting tonulland[]on every pre-existing branch so current JSON consumers see an unchanged payload. - In the handler, run the probe only when
checkRemoteis true AND the local result isresolved: falsewithreason: "not-found"— an ambiguous local match is already actionable, and a successful resolve needs no diagnosis. Throughnew Git(projectRoot, { runner: ctx.git }):fetch("origin", "main")wrapped in try/catch —fetchis a checked mutation that throwsCommandFailed, and a throw means offline or no remote, so return the unchanged not-found result.revListCount("HEAD..origin/main")—nullor0returns the unchanged not-found result (an up-to-date checkout genuinely lacks the task).lsTreeNames("origin/main", "docs/planning/tasks/"), reduced to basenames and fed tomatchTaskFilenames. No hit returns the unchanged not-found result.- Otherwise return
reason: "behind-remote"withbehind_countset andremote_candidatesholding the single hit or the ambiguous candidate list.
- Export a
behindRemoteMarker(arg, behindCount, candidates)builder besidenoTaskFoundMarker, and add abehind-remotebranch to the op’scli.renderhook that writes that marker to stderr and returns exit 1. - Extend
apps/sdlc/lib/model/entities/task/ops/tests/resolve.test.tswith the new cases, injecting a scripted fakeCommandRunnerasctx.gitvia thecreateCtxseam the suite already imports. Leave the existing cases byte unchanged. - Update
apps/sdlc/lib/model/entities/task/file-resolution.md: add theBehind remoterow to the “Outcome contract” table and a short staleness-probe subsection covering the--check-remoteopt-in and the degrade-to-not-found rule. - Update
apps/sdlc/skills/task-work/SKILL.mdStep 1: pass--check-remoteon the resolve call, and add abehind-remotebullet to the per-outcome ACTION list that reports the commit count, names theremote_candidates, points the operator atgit -C <project-root> pull --ff-only, and exits without printingNO TASK FOUND. - Refresh
docs/skills/task-work.mdby running/sdlc:update-skill-doc task-workso theS1flowchart node carries the new branch.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
apps/sdlc/lib/services/git/easy.ts | modify | Add 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.ts | modify | Extract 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.ts | modify | Add 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.md | modify | Document the fourth outcome row and the --check-remote staleness probe. |
apps/sdlc/skills/task-work/SKILL.md | modify | Step 1 passes --check-remote and gains the behind-remote operator-facing branch. |
docs/skills/task-work.md | modify | Regenerated Step 1 flowchart node reflecting the new branch. |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
sdlc task resolve <arg>without--check-remoteissues zero git invocations — asserted inresolve.test.tswith a fakectx.gitrunner 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 whoseorigin/maincarriesdocs/planning/tasks/<slug>.mdwhile the working tree does not, returnsresolved: false,reason: "behind-remote", abehind_countequal togit rev-list --count HEAD..origin/main, and aremote_candidatesarray containing that filename. - AC-3: the same invocation in TEXT mode writes one stderr line built by
behindRemoteMarkerthat contains the commit count, the literal substringcommits behind origin/main, and the candidate filenames, and exits 1; the substringNO TASK FOUNDis absent from that output. - AC-4:
sdlc task resolve <slug> --check-remotewhere<slug>matches no filename underdocs/planning/tasks/atorigin/mainreturnsreason: "not-found"and renders the unchangedNO TASK FOUND for "<arg>"marker at exit 1. - AC-5: when the fetch probe fails (fake runner returns a non-zero exit for
the
fetchargv), the op returnsreason: "not-found"and exits 1 without throwing. - AC-6:
apps/sdlc/skills/task-work/SKILL.mdStep 1 enumerates three not-resolved branches —ambiguous,not-found,behind-remote— and thebehind-remotebullet 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.mdcontains aBehind remoterow 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.tspasses, and the six pre-existing cases in that file (absolute-exists, absolute-missing, exact-filename, prefix-glob, substring-glob, ambiguous) are unmodified.
Out of scope
Section titled “Out of scope”- Fetching on every
sdlc task resolvecall. 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.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:spawn-task-pr on 2026-07-19 UTC from T-1YSW-amend-d0001-solutions-tier in https://github.com/sksizer/dev.