foreman:survey — ground-truth the roadmap's near-term candidates
SkillAI & modelsAdvanced surface, normally reached through the `foreman` entrance's Reconcile and pick mode, which hands it a near-term set of ids to scope the pass to. Ground-truths the roadmap's near-term candidates against the actual codebase, a read-only investigation checks whether each candidate's planned_touches/depends_on still match reality, then proposes a concrete, evidence-backed repair for every finding (hidden dependency, already-done, stale description or planned files), applies only the ones you approve, and persists them back into ROADMAP.jsonl so future sessions pick them up automatically. Also retires any recorded lesson the same evidence contradicts, so a wrong claim stops being quoted into later handoffs. It costs materially more than a plain pick, which is why it is explicit: a fast pick or old roadmap alone does not trigger it.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the foreman:survey — ground-truth the roadmap's near-term candidates skill
What this skill tells your AI
The instructions your AI receives, as published by v-songbird/foreman in skills/survey/SKILL.md and read by ahel’s review.
Foreman runs in Claude Code and in Codex. Every step applies to both unless it names a host. Read the shared runtime first: it covers plugin paths, JSON payloads, questions and authorization for both hosts.
This is the advanced code investigation flow, the one Foreman flow that investigates the codebase against the roadmap. Fast pick does not run it: it reads more code and costs more than ranking stored tasks, so it runs only when someone asks for it.
Use ${CLAUDE_PLUGIN_ROOT}/scripts/roadmap.js for all roadmap reads and
mutations — never read or edit ROADMAP.jsonl directly — and run it with
--help for the command shapes. Read the Fields section of
roadmap-schema.md
(${CLAUDE_PLUGIN_ROOT}/roadmap-schema.md) for field semantics.
Pre-check: if ROADMAP.jsonl doesn't exist at the project root, offer
to set up the roadmap first (/foreman:init in Claude Code; in
Codex, the init skill) and stop here.
1. Pick the scope
If a caller handed over a set of ids, that set is the scope — args naming
specific tasks, or Reconcile and pick's near-term set (the pick flow in
pick.md derives it from one next-candidates --menu
result: the candidate rows plus the in_progress and awaiting_acceptance
rows). Run list --ids <those ids>, drop any that don't exist or are terminal
(done/dropped/rejected — history its commits already describe) and say
which you dropped, and skip the next-candidates call below. Scoping decides
which entries get investigated and nothing else: steps 2–4 run exactly as
written, on whatever the scope holds. Otherwise:
node ${CLAUDE_PLUGIN_ROOT}/scripts/roadmap.js next-candidates
(default --limit 3) — candidates already include each one's own
depends_on, no separate call needed just to get that.
Survey the top candidates only — the same 3 by default that a pick shows.
This is deliberately not the whole backlog: a hidden dependency or stale claim
matters most for what's about to be picked. If total_unblocked is larger
than what you surveyed, say so when reporting back — don't imply full
coverage silently.
Collect the exact set of dependency ids referenced across all candidates'
depends_on (dedup). If non-empty, resolve just those —
node ${CLAUDE_PLUGIN_ROOT}/scripts/roadmap.js list --ids <comma-joined ids>
— never the unfiltered list, which loads the whole file just to answer a
question about a handful of ids.
Mechanical pre-check, not an investigator's job: that list --ids call
already answered whether each entry's commits exist — every finished entry it
returns carries commit_evidence
(commit_count/resolved_count/unresolved/has_trailer_match). Read it;
do not re-derive it with git cat-file (which only ever asks the project
repo, so a commit living in a submodule comes back "missing" when it is
right there) and do not spend investigators on it. unresolved lists the shas
git could not find; has_trailer_match: true means a commit message names the
entry, which is the whole evidence a staged close leaves — an entry with
commit_count: 0 and a trailer match is recorded, not empty.
Same reasoning applies to planned_touches: collect every path named across
the candidates being surveyed (dedup), and check existence directly —
test -e <path> (Bash) / Test-Path -LiteralPath <path> (PowerShell), relative to the
project root, one call per unique path (or a short loop in one call).
Refuse paths outside the project before reading them. Build a
path_exists: true/false map from this too — no investigator needs a round
trip of its own just to learn a file isn't there. A missing path is a
question, not a verdict: planned_touches is a forward-looking best
guess written at add/init time and routinely names files the task will
create, so absence alone is not stale scope. Survey only ever ranges over
that predicted half — observed_touches is derived from commits that
already landed, so there is nothing there to ground-truth and nothing
correct could repair.
One more mechanical fact, gathered once regardless of which path above set
the scope: a not-done digest — id, title, planned_touches for
every entry currently planned, in_progress, awaiting_acceptance, or
deferred (the whole not-done backlog, not just the candidates being
surveyed) — node ${CLAUDE_PLUGIN_ROOT}/scripts/roadmap.js list --status planned,in_progress,awaiting_acceptance,deferred --summary (--summary
rows already carry exactly this: id/title/status/depends_on/
planned_touches — never the unfiltered list, which would also load the
prose fields for entries nobody is investigating). Checks 3 and 4 in step 2
compare each candidate against this digest, not against a fresh roadmap
read — it exists precisely so an investigator never has to open
ROADMAP.jsonl itself to answer "does this overlap something else
unfinished".
2. Investigate each candidate in parallel
Give each candidate one read-only investigator. In Claude Code, dispatch one
Agent (subagent_type: Explore) per candidate, in parallel (single
message, multiple tool calls). In Codex, use available collaboration
subagents for independent candidates, limiting concurrency to actual
capacity. If delegation is unavailable or there is only one small candidate,
investigate locally. Collect every result before claiming the survey
complete.
Each investigator gets a self-contained prompt built from the candidate's own fields plus the resolved-dependency, exists-map, and not-done-digest context gathered in step 1:
-
The candidate's
id,title,why,what,planned_touches,depends_on. -
For each path in
planned_touches: the pre-computedpath_existsflag from step 1 — the investigator consumes this fact, it does not re-check it. -
For each id in
depends_on: that entry'stitle,status,commits, and itscommit_evidencefrom step 1 — the investigator consumes this fact, it does not re-derive it. -
The not-done digest from step 1 —
id/title/planned_touchesfor every other not-done entry — for checks 3 and 4 below. The investigator judges hidden dependencies and overlaps against this supplied digest; it does not readROADMAP.jsonlto get it. -
Ask it to check, and report a verdict for each:
- Touches still real? A path step 1 flagged missing is
stale-touchesonly if it can be shown to have once existed and moved —git log --diff-filter=D -- <path>, or--followshowing a rename. Nothing found means the task simply hasn't created it yet: verdict staysvalid, nothing to annotate. When a path did move, the investigator returns the whole correctedplanned_touchesarray — the new path in place of the old one, retaining unaffected paths — becausecorrectreplaces that field wholesale rather than merging a diff into it. For paths confirmed to exist, does their current content still match whatwhatdescribes? (git log --oneline -- <path>plus a read of the file's current state.) Where it no longer does, that isstale-description, and the investigator returns a rewrittenwhat: the same task re-described against the code as it now stands, ready to be stored verbatim — not a summary of the drift. - Dependencies actually satisfied? If step 1's
commit_evidencealready lists adoneentry's commit asunresolved, flag it with no further check: an unresolved commit means not resolvable here, not fabricated. Otherwise, for commits that resolved, do they plausibly implement what that entry'stitle/whatclaims? (this half stays semantic — read the commit, judge the match) - Hidden dependency? Reading the code the candidate's
planned_touchespoint to, does it already reference/import/call something that another entry in the supplied not-done digest claims via its ownplanned_touches, which isn't in this candidate'sdepends_on? Then the same question in reverse: does anything outside this candidate'splanned_touchesconsume the code it changes, in a way that makes another entry in the digest depend on this one? Check against the digest handed to you, not a freshROADMAP.jsonlread. Report every relation you can see in either direction, including one you can see but cannot pin to a line — mark thatconfident: falseand say what you could not pin down. Step 3 filters; a dependency you leave out here is not recoverable there, because it is the one verdict that reorders future picks. - Already done, or duplicate? Does the working tree already contain
what
whatdescribes, or does it closely overlap another entry'stitlein the supplied not-done digest? Do not treat a similar title alone as proof.
Verdict per candidate:
valid(nothing found) |hidden-dependency|stale-description|stale-touches|already-done|duplicate. Every non-validverdict must cite the file:line or commit that grounds it, or be markedconfident: falsewith what it could not pin down — never a bare verdict carrying neither.A
stale-descriptionorstale-touchesverdict carries two things or it is not reportable as one: the evidence — the file paths and symbols it actually opened, and what it found there instead — and a concrete proposed replacement value, a finishedwhatstring or a completeplanned_touchesarray, ready to be written as-is. A vague "this looks stale" is not a finding of this kind. When the evidence is real but no replacement can be grounded, the investigator returns it withconfident: falseand says what it could not determine, keeping the evidence either way. That flag is what step 3 reads to choose between proposing a repair and leaving a breadcrumb. - Touches still real? A path step 1 flagged missing is
3. Confirm before writing anything
Present findings to the user — one line per candidate, valid ones need
no more than a mention. For anything else, ask before persisting: a
survey finding is a read of the evidence, not an automatic mutation, and a
request to inspect remains read-only for substantive changes. When the user
already explicitly authorized applying grounded repairs, apply those within
that scope without asking for the same authorization again.
For every finding that carries a concrete proposal, show three things
before asking or applying it: the entry's id and title, the current value →
proposed value, and the evidence line(s) the investigator cited. Show
planned_touches in full on both sides — correct replaces the array, so a
partial list would read as the entire new one. The user is approving a
specific string; the specific string has to be on screen.
Approval is per finding. Ask one question per entry: AskUserQuestion
in Claude Code; in Codex, the picker in questions.md;
with no usable question tool, one self-contained plain-text question. When
several fields of the same entry changed together (a rewritten what and a
corrected planned_touches — the user may well want one and not the other),
each field is its own choice; in Claude Code, use multiSelect.
Batch at most a handful of entries into one question, and only while every
option still names its own entry and field.
Never offer a single blanket "apply everything".
The echo lines show each payload's shape; when one carries evidence or
rewritten text, send it the way the shared runtime
describes rather than inside shell quotes.
-
hidden-dependency→ on confirm:echo '{"id":"<candidate>","add_depends_on":["<dep-id>"]}' | node ${CLAUDE_PLUGIN_ROOT}/scripts/roadmap.js update-depsThis is structural —next-candidateswill now correctly treat the candidate as blocked until<dep-id>isdone. It is baked into the graph the ranking algorithm reads, so a later session honors it without anyone having to remember a note. -
already-done/duplicate→ on confirm:echo '{"id":"<candidate>","status":"dropped","notes":"survey: <one-line evidence>"}' | node ${CLAUDE_PLUGIN_ROOT}/scripts/roadmap.js update-status(or"done"with the actualcommitif the evidence points to a specific commit that already did the work). Do not manufacture a completion SHA. -
stale-description/stale-toucheswith a concrete proposal → on confirm, apply it withcorrect, the one command that can replacewhat/planned_toucheson a live entry (correct.md uses the same call):node ${CLAUDE_PLUGIN_ROOT}/scripts/roadmap.js list --ids <candidate>— re-read the entry immediately before writing. Itsupdated_atis the value the write is guarded by, the investigators ran for a while in between, andnext-candidatesdoes not return that field at all.echo '{"id":"<candidate>","expected_updated_at":"<the updated_at that read just returned>","expected":{"what":"<the what that read just returned>","planned_touches":[<the planned_touches that read just returned>]},"what":"<approved what>","planned_touches":[<approved paths>]}' | node ${CLAUDE_PLUGIN_ROOT}/scripts/roadmap.js correct— only the approved fields go in the payload, each paired with its ownexpected.<field>holding the CURRENT value that same read (1) just returned; a field the user declined is simply absent from both, andplanned_touchesis sent as the whole replacement array in both places.- If the script refuses with
was last updated … , not …or… no longer matches expected.<field>, another session changed the entry between that read and this write. Re-read (1), re-show current → proposed against the newer text, and ask again — the approval covered text that no longer exists. Never re-send with theupdated_atfrom the error message to force it through.
correctrefuses terminal (done/dropped/rejected) entries and a title another entry already holds, on its own — a stale description found on a terminal entry is history its commits describe, not a repair. -
Uncertain findings are never applied. Evidence gathered but no grounded replacement (the investigator's
confident: false), or a proposal neither you nor the user can pin down here: it lands as exactly one breadcrumb, marked unconfirmed so a later session reads it as a lead and not as a fact — status untouched, no field rewritten:echo '{"id":"<candidate>","notes":"survey (unconfirmed): <one-line evidence>"}' | node ${CLAUDE_PLUGIN_ROOT}/scripts/roadmap.js annotate(the script date-stamps each appended note itself — don't write one in)annotateexists precisely for this write: unlikeupdate-status, it can't regress the entry to a status read before the survey ran (e.g. re-assertingplannedon an entry another session has since moved toin_progress). This is a soft signal, not a mechanical reorder —next-candidatesnow returns this candidate'snotes, so the next pick sees it as context, but ranking itself (unblocks_total, thenunblocks, then no-collision, thencreated_at) doesn't change. Explain that such notes inform future prompts but do not reorder the mechanical ranking. -
A read-only request writes nothing. When the user asked only to check, report uncertain findings as leads in step 4 instead of writing breadcrumbs.
-
A declined proposal writes nothing. No note, no refusal breadcrumb, no status change. It goes in the report (step 4), not in the roadmap.
Never write on an unconfirmed finding, and never touch ROADMAP.jsonl
directly — every write above goes through roadmap.js, same as every other
Foreman flow.
3b. The lessons recorded about the same files
Survey is the only flow that has already read the code a recorded lesson describes, so it is the only place a wrong one gets retired. Nothing else in Foreman can tell a claim that aged badly from one that was never true.
One call, for the candidates you surveyed, with their planned_touches joined:
node ${CLAUDE_PLUGIN_ROOT}/scripts/roadmap.js notes --paths <comma-joined paths>
Skip the whole step when it returns no records — that one call answers both "is the feature on" and "is there anything here", and it costs nothing to ask.
Two kinds of record are worth the user's attention, and no others:
staleness: "stale"whose claim the step-2 evidence contradicts. The files under it moved, and the investigators that just read those files reported something different.- Any record the step-2 evidence contradicts outright, whatever its label.
A
freshlabel says the files have not changed since the claim was recorded. It never says the claim was right when it was written.
A stale label on its own is not a finding. It is a prompt to look, and the looking already happened in step 2.
For each record worth offering, show the lesson verbatim, its label, and the evidence line that contradicts it, then ask the same way and under the same approval rule as step 3: keep it, or retire it. On retire:
echo '{"key":"<the key notes reported>","by_entry":"<the surveyed entry>"}' | node ${CLAUDE_PLUGIN_ROOT}/scripts/roadmap.js note-supersede
Retiring stops the line being served and stops it spending the handoff's
capped serving window. It does not delete it, and it does not record what the
truth is instead: the corrected fact belongs on the lesson of whichever task
next closes in that code, where it arrives with its own anchor and date.
Pruning is a separate ask and it comes last. It is the one command in Foreman that rewrites the lesson store, so it never runs on an inference about what the user probably wants:
node ${CLAUDE_PLUGIN_ROOT}/scripts/roadmap.js note-prune --dry-run
That writes nothing and reports what would go — records whose every file is
gone, and records already retired. Show the count and ask once, unless the
user already explicitly asked to prune that set. Only on a yes, or that
explicit request, run the same command without --dry-run. When the dry run
reports nothing, skip the ask entirely.
4. Report
Short summary: candidates surveyed (and how many were left unsurveyed, if any), verdicts, what got written. Lessons retired and records pruned get one line each when either happened, and no line at all when neither did. Separate the three outcomes in one line each: corrections applied, findings left unconfirmed as breadcrumbs, and proposals declined (declined ones exist only here — nothing about them was written). If no writes happened, say the roadmap is unchanged — this skill running is not itself news. For Reconcile and pick, return these results to the pick flow before it refreshes its menu; do not quietly continue on the old ranking.
Trial log
After each question interaction the user actually sees in this flow, one silent line:
node ${CLAUDE_PLUGIN_ROOT}/scripts/trial-log.js question_asked '{"flow":"survey"}'
One event per interaction, never one per question: one AskUserQuestion call
in Claude Code, however many questions it batches; one picker call or one
plain-text question in Codex. A skipped question is never logged. It is a
no-op unless the project set trialLog, so it needs no check first and never
blocks the flow.
Signals
- GitHub stars
- 35
- Forks
- 5
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
survey-v-songbird- Source
- github.com/v-songbird/foreman