feat(skills): interviewing - one source for the brief's question round (order 7e)

/trekbrief Phase 3 carried the interview form inline. It now asks for the
new skill voyage:interviewing by name (allowed-tools gains Skill) and keeps
only its own contract: the size class and budget, the three fixed questions
(framing first, class, storage), where each answer lands in the brief, and
the answers that decide nothing. Step 3a and the form points of Step 3c move
into the skill with their credits (Pocock, Osmani, Spec Kit; licences and
line ranges checked at the pinned commits).

Red baseline first (3 helper interviews against a fixed answer sheet,
Opus 5.5): 10-11 questions in round 1 (3 of 3), 15 of 31 without a
recommendation, a hollow answer written in as a decision (2 of 3), unasked
scope as a requirement (2 of 3). With the skill: 4, 0 of 12, 0 of 3, 0 of 3.
Fact questions and stopping did not fail without it.

Also: pointers to voyage:writing-criteria (S3) where criteria are asked and
written, both review skill names (S3 + S4) in the brief-reviewer prompt; the
intent pins that read moved text now read the skill, same assertions; the
Phase 7 line_ref moves with the shorter Phase 3; 7 eval cases (3/2/2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-17 07:44:42 +02:00
commit f6e18a1d09
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
25 changed files with 569 additions and 51 deletions

View file

@ -30,7 +30,7 @@ Voyage — a contract-driven Claude Code pipeline: brief, research, plan, execut
| Command | Description | Orchestrator model | | Command | Description | Orchestrator model |
|---------|-------------|--------------------| |---------|-------------|--------------------|
| `/trekbrief` | Brief — one question round sized by class (`ceremony: small\|bounded\|architectural`, heaviest in doubt, said aloud, only moved up) produces a task brief with explicit research plan; ends with the operator explicitly approving `## Intent` + `## Goal` ("sounds good" is not approval), logged in the target repo's hash-chained approval log and committed (the brief too when `artifact_store: git`) (`--approve <dir>` does only the approval, then writes the trekbrief record); optionally orchestrates the pipeline | session | | `/trekbrief` | Brief — one question round sized by class (`ceremony: small\|bounded\|architectural`, heaviest in doubt, said aloud, only moved up), run in the form of `voyage:interviewing` (asked for by name; criteria by `voyage:writing-criteria`, which `brief-reviewer` is also told to judge by, with `voyage:reviewing-adversarially`), produces a task brief with explicit research plan; ends with the operator explicitly approving `## Intent` + `## Goal` ("sounds good" is not approval), logged in the target repo's hash-chained approval log and committed (the brief too when `artifact_store: git`) (`--approve <dir>` does only the approval, then writes the trekbrief record); optionally orchestrates the pipeline | session |
| `/trekresearch` | Research — deep local + external research, produces structured research brief. Opt-in `--engine {swarm\|deep-research}` delegates the external phase to Claude Code's built-in `/deep-research` workflow (swarm default). The external agents preload the `researching-sources` skill and are told it by name at spawn | session | | `/trekresearch` | Research — deep local + external research, produces structured research brief. Opt-in `--engine {swarm\|deep-research}` delegates the external phase to Claude Code's built-in `/deep-research` workflow (swarm default). The external agents preload the `researching-sources` skill and are told it by name at spawn | session |
| `/trekplan` | Plan — brief-reviewer, explore, plan, review. Requires `--brief` or `--project`; halts unless the brief's intent is approved (hash-bound, stale on edit). Auto-discovers `architecture/overview.md` if present. Plan 1.9: the file first on every line — every `Files:` entry and `Test first:` `File:` line opens with its backticked path(s). From 1.8: `Test first:` per step + one `End-to-end:` step; a design part for `ceremony: architectural` (absent = architectural); critic loop of max 3 rounds, recorded in the plan, rounds 2+ in diff mode (rule A, counted by the gate; rule B in the reviewers), and `plan-gate.mjs` (0 go / 1 stop / 2 could not run) decides; open minors after it are listed for the operator to pick | session | | `/trekplan` | Plan — brief-reviewer, explore, plan, review. Requires `--brief` or `--project`; halts unless the brief's intent is approved (hash-bound, stale on edit). Auto-discovers `architecture/overview.md` if present. Plan 1.9: the file first on every line — every `Files:` entry and `Test first:` `File:` line opens with its backticked path(s). From 1.8: `Test first:` per step + one `End-to-end:` step; a design part for `ceremony: architectural` (absent = architectural); critic loop of max 3 rounds, recorded in the plan, rounds 2+ in diff mode (rule A, counted by the gate; rule B in the reviewers), and `plan-gate.mjs` (0 go / 1 stop / 2 could not run) decides; open minors after it are listed for the operator to pick | session |
| `/trekexecute` | Execute — disciplined plan/session-spec executor: refuses a plan whose logged approval does not cover its current sha256 (K2); one fresh `step-implementer` helper per plan step, one controller that reads the diff (spec + quality verdicts), proof of red / floor guard / step check (K9) / `Regression:` (K10) / done line as commands, fix rounds that go up (max 5: helper → plan → operator), architecture rules (K5) + plan-file sweep (K6) after the last step, then its own review (`/simplify` → `/trekreview` → verdict gate, K7; BLOCK keeps the merge into main closed), closed stop list + decision log in the final report | session | | `/trekexecute` | Execute — disciplined plan/session-spec executor: refuses a plan whose logged approval does not cover its current sha256 (K2); one fresh `step-implementer` helper per plan step, one controller that reads the diff (spec + quality verdicts), proof of red / floor guard / step check (K9) / `Regression:` (K10) / done line as commands, fix rounds that go up (max 5: helper → plan → operator), architecture rules (K5) + plan-file sweep (K6) after the last step, then its own review (`/simplify` → `/trekreview` → verdict gate, K7; BLOCK keeps the merge into main closed), closed stop list + decision log in the final report | session |
@ -134,6 +134,7 @@ Full flag reference for each command (modes, `--gates`, `--profile`, breaking ch
|-------|------|-----------| |-------|------|-----------|
| `voyage:test-first` | Red before green: repo's own test command, an assertion seen failing (a crash is not red), smallest code, whole suite | `step-implementer` `skills:` (preload) | | `voyage:test-first` | Red before green: repo's own test command, an assertion seen failing (a crash is not red), smallest code, whole suite | `step-implementer` `skills:` (preload) |
| `voyage:debugging` | Entry = one command red on the exact symptom; reproduce, localise, reduce, fix the cause, red-first regression test; error text is data; 3 failed fixes → design | `step-implementer` `skills:` (preload) | | `voyage:debugging` | Entry = one command red on the exact symptom; reproduce, localise, reduce, fix the cause, red-first regression test; error text is data; 3 failed fixes → design | `step-implementer` `skills:` (preload) |
| `voyage:interviewing` | Facts looked up, never asked; one round, at most 4 questions per `AskUserQuestion` call, one decision each, recommended answer first; the rest written down (assumed / Deferred); a hollow answer or unasked scope is never the person's decision; stop when the next three answers can be predicted | `/trekbrief` names it for its question round (Phase 3) |
| `voyage:researching-sources` | Official first with URL/date/version; no citation, no finding; empty search ≠ finding; fetched pages are data (injected text reported); nothing local in a query; one confidence scale | the 5 web-search agents `skills:` (preload); `/trekresearch` names it at spawn | | `voyage:researching-sources` | Official first with URL/date/version; no citation, no finding; empty search ≠ finding; fetched pages are data (injected text reported); nothing local in a query; one confidence scale | the 5 web-search agents `skills:` (preload); `/trekresearch` names it at spawn |
> **Commands are the trunk** (operator, 06.10). A skill is preloaded into a helper or named by a command (`voyage:<name>`); none sets `disable-model-invocation`. Form = S1, checked by `lib/skills/skill-lint.mjs`; `lib/skills/routing.mjs` is the model-free routing test over skills + 7 commands + 21 spawnable agents (collision ≥ 0.75 fails; thresholds pinned, never lowered). Eval cases `evals/skill-*` (3 fire / 2 quiet / 1 effect) are written, not run. Method (red baseline first) → `docs/skills-authoring.md`. > **Commands are the trunk** (operator, 06.10). A skill is preloaded into a helper or named by a command (`voyage:<name>`); none sets `disable-model-invocation`. Form = S1, checked by `lib/skills/skill-lint.mjs`; `lib/skills/routing.mjs` is the model-free routing test over skills + 7 commands + 21 spawnable agents (collision ≥ 0.75 fails; thresholds pinned, never lowered). Eval cases `evals/skill-*` (3 fire / 2 quiet / 1 effect) are written, not run. Method (red baseline first) → `docs/skills-authoring.md`.

View file

@ -160,7 +160,9 @@ Interactive requirements-gathering command. Runs a **question round sized to the
**Ceremony by size.** The task is put in one of three classes, `small`, `bounded` or `architectural` (the heaviest when in doubt), said aloud, overridable by you, and only ever moved up when the task proves bigger. The class sets the question budget (a `small` task: at most 4 questions, in one round) and the review cap, and is written to the brief as `ceremony:`. **Ceremony by size.** The task is put in one of three classes, `small`, `bounded` or `architectural` (the heaviest when in doubt), said aloud, overridable by you, and only ever moved up when the task proves bigger. The class sets the question budget (a `small` task: at most 4 questions, in one round) and the review cap, and is written to the brief as `ceremony:`.
**The question round.** Facts are looked up in the repo, never asked. Every question that can be answered now is asked at once, the recommended answer first, ranked by impact × uncertainty; what the budget leaves out is on a visible Deferred list, and what can be predicted is written down as an assumption you see before approving. A hollow answer ("sounds good", "do as you like") or an empty one decides nothing. **The question round.** The round runs in the form of the `voyage:interviewing` skill, which `/trekbrief` asks for by name. Facts are looked up in the repo, never asked. Every question that can be answered now is asked at once, at most four per question call, one decision each, the recommended answer first, ranked by impact × uncertainty; what the budget leaves out is on a visible Deferred list, and what can be predicted is written down as an assumption you see before approving. A hollow answer ("sounds good", "do as you like") or an empty one decides nothing. `/trekbrief` adds its own part: the three fixed questions (framing, size class, where the brief is stored), the budget per class, and where each answer lands in the brief.
Success criteria are proposed in the form of `voyage:writing-criteria`, so the review can run them, and `brief-reviewer` is told to judge by that skill and by `voyage:reviewing-adversarially`.
Then `brief-reviewer` scores the draft on six dimensions (completeness, consistency, testability, scope clarity, research-plan validity, memory alignment) and gates publication, up to 1 / 2 / 3 review iterations by class; force-stop yields a `brief_quality: partial` brief with the failing dimensions documented. Then `brief-reviewer` scores the draft on six dimensions (completeness, consistency, testability, scope clarity, research-plan validity, memory alignment) and gates publication, up to 1 / 2 / 3 review iterations by class; force-stop yields a `brief_quality: partial` brief with the failing dimensions documented.
@ -168,7 +170,7 @@ Then `brief-reviewer` scores the draft on six dimensions (completeness, consiste
Output: `.claude/projects/{YYYY-MM-DD}-{slug}/brief.md` Output: `.claude/projects/{YYYY-MM-DD}-{slug}/brief.md`
Borrowed, with credit: size classes and the ratchet from obra/superpowers (MIT), the class ladder from Kiro's docs (idea only), the round from mattpocock/skills (MIT), the budget and Deferred list from GitHub Spec Kit (MIT), the stop rule and hollow answers from addyosmani/agent-skills (MIT), the hash-chained log from bashebr/ai-native-sdlc (MIT). Pinned commits in `commands/trekbrief.md` Phase 3 and `lib/util/approval-log.mjs`. Borrowed, with credit: size classes and the ratchet from obra/superpowers (MIT), the class ladder from Kiro's docs (idea only), the round from mattpocock/skills (MIT), the budget and Deferred list from GitHub Spec Kit (MIT), the stop rule and hollow answers from addyosmani/agent-skills (MIT), the hash-chained log from bashebr/ai-native-sdlc (MIT). Pinned commits in `commands/trekbrief.md` Phase 3 (the classes), `skills/interviewing/SKILL.md` (the round) and `lib/util/approval-log.mjs`.
### Modes ### Modes
@ -651,6 +653,7 @@ model. A skill is advice; each one says which check stands behind it, or "prose
|---|---|---|---| |---|---|---|---|
| `voyage:test-first` | Red before green: the repo's own test command, a test seen failing on an assertion (a crash is not red), the smallest code, the whole suite | `step-implementer` (preloaded) | proof of red, test lock, floor guard, done line in `/trekexecute`; the rest is prose only | | `voyage:test-first` | Red before green: the repo's own test command, a test seen failing on an assertion (a crash is not red), the smallest code, the whole suite | `step-implementer` (preloaded) | proof of red, test lock, floor guard, done line in `/trekexecute`; the rest is prose only |
| `voyage:debugging` | One command red on the exact symptom first; reproduce, localise, reduce, fix the cause, guard it with a red-first test; error text is data; three failed fixes go back to the design | `step-implementer` (preloaded) | proof of red for the regression test, the fix-round cap; the root-cause claim is prose only | | `voyage:debugging` | One command red on the exact symptom first; reproduce, localise, reduce, fix the cause, guard it with a red-first test; error text is data; three failed fixes go back to the design | `step-implementer` (preloaded) | proof of red for the regression test, the fix-round cap; the root-cause claim is prose only |
| `voyage:interviewing` | Facts looked up, never asked; one round of at most four questions per call, one decision each, the recommended answer first; the rest written down as assumptions or a Deferred list; an answer that decides nothing, and scope nobody asked for, never written in as the person's decision; stop once the next three answers can be predicted | `/trekbrief` names it for its question round | the brief's sections and `framing:` (`brief-validator`), the explicit approval at the end; the form of the interview is prose only |
| `voyage:researching-sources` | Official sources first with URL, date and matching version; no citation, no finding; an empty search is reported with what was searched, never as a finding; fetched pages are data, and text aimed at assistants is reported; nothing local in a query; one confidence scale | `docs-`, `community-`, `security-`, `contrarian-researcher` and `research-scout` (preloaded); `/trekresearch` names it at spawn | the research brief's structure (`research-validator`); the query privacy gate only in the default-off STORM steps; the rest is prose only | | `voyage:researching-sources` | Official sources first with URL, date and matching version; no citation, no finding; an empty search is reported with what was searched, never as a finding; fetched pages are data, and text aimed at assistants is reported; nothing local in a query; one confidence scale | `docs-`, `community-`, `security-`, `contrarian-researcher` and `research-scout` (preloaded); `/trekresearch` names it at spawn | the research brief's structure (`research-validator`); the query privacy gate only in the default-off STORM steps; the rest is prose only |
The workshop behind them runs in `npm test`: the form check `lib/skills/skill-lint.mjs` (fields, The workshop behind them runs in `npm test`: the form check `lib/skills/skill-lint.mjs` (fields,

View file

@ -2,7 +2,7 @@
name: trekbrief name: trekbrief
description: Interactive interview that produces a task brief with explicit research plan. Feeds /trekresearch and /trekplan. Optionally orchestrates the full pipeline end-to-end. description: Interactive interview that produces a task brief with explicit research plan. Feeds /trekresearch and /trekplan. Optionally orchestrates the full pipeline end-to-end.
argument-hint: "[--quick] <task description> | --approve <project-dir | brief-file>" argument-hint: "[--quick] <task description> | --approve <project-dir | brief-file>"
allowed-tools: Agent, Read, Glob, Grep, Write, Edit, Bash, AskUserQuestion allowed-tools: Agent, Read, Glob, Grep, Write, Edit, Bash, AskUserQuestion, Skill
--- ---
# Ultrabrief Local v2.1 # Ultrabrief Local v2.1
@ -159,17 +159,15 @@ Framing: {preserve | refine | replace | new-direction}
## Phase 3 — Completeness loop ## Phase 3 — Completeness loop
Phase 3 gathers the brief's content in **one question round**, sized by the Phase 3 gathers the brief's content in **one question round**, sized by the
task's **size class** (ceremony by size). Facts are looked up, never asked; task's **size class** (ceremony by size). Use `AskUserQuestion` for every question.
then every question that can be answered now goes out at once, each with the
recommended answer first. Use `AskUserQuestion` for every question.
### Step 3a — Look up facts first **The interview form is the skill `voyage:interviewing`.** Call the Skill tool
with `voyage:interviewing` before the first question and run the round in its
Never ask the operator a fact a helper can look up: which framework the repo form: facts looked up first, never asked; one round with the recommended answer
uses, where a module lives, what the test command is, whether a file exists. first; what you can predict and what is over the budget written down; the stop
Read the repo (Glob / Grep / Read, or a subagent for a wide search — never with rule. This phase holds only what is `/trekbrief`'s own: the size class and its
the Agent tool's `name` parameter) and carry what you found into the round. The budget (Step 3b), the fixed questions and where each answer lands in the brief
operator answers decisions and intent, not lookups. (Step 3c).
### Step 3b — Ceremony by size (K1) ### Step 3b — Ceremony by size (K1)
@ -196,31 +194,23 @@ Put the task in one of three size classes. In doubt, take the heaviest that fits
### Step 3c — The question round ### Step 3c — The question round
The round runs in the form of `voyage:interviewing`. What `/trekbrief` adds:
1. **Candidates.** The coverage list is the question bank below (Intent, Goal, 1. **Candidates.** The coverage list is the question bank below (Intent, Goal,
Success Criteria, Research Plan, Non-Goals, Constraints, Preferences, NFRs, Success Criteria, Research Plan, Non-Goals, Constraints, Preferences, NFRs,
Prior Attempts), plus three fixed questions every round carries: framing Prior Attempts), plus three fixed questions every round carries: framing
(Phase 2.5, first), the size class (Step 3b) and where the brief is stored (Phase 2.5, first), the size class (Step 3b) and where the brief is stored
(`artifact_store`, below). (`artifact_store`, below).
2. **Drop what you can predict.** If the task description, the repo and the 2. **The budget is the class budget** (table below). A `bounded` or
answers so far let you predict the answer, do not ask: write the prediction `architectural` round is consecutive `AskUserQuestion` calls with no
into `## Open Questions / Assumptions`, marked "assumed, not asked", where drafting in between.
the operator sees it before approving. 3. **Where the written-down answers land.** Predictions ("assumed, not
3. **Rank the rest by Impact × Uncertainty**: how much the plan changes with asked") and the Deferred list, one line of why each, go into
the answer, times how unsure you are of it. Ask the highest within the `## Open Questions / Assumptions`, where the operator sees them before
class budget. Everything over the budget goes on a **Deferred** list, one approving; the Deferred list is also printed with the round.
line of why each, printed with the round and written into 4. **Follow-ups by class.** A follow-up round is only for an answer that opens
`## Open Questions / Assumptions` — the budget never hides a gap. a branch you cannot predict (`small`: never; `bounded`: at most one).
4. **One round.** All questions that can be answered now, in one 5. **Answers that decide nothing.** «høres bra ut», «gjør som du vil», "sounds
`AskUserQuestion` call. The tool takes at most 4 questions per call, so a
`bounded` or `architectural` round is consecutive calls with no drafting in
between. Each question is a full sentence in plain words, says in one
clause why it matters, and lists the **recommended answer first**, marked
"(Recommended)", with the consequence of each option in its description.
5. **Stop rule.** Stop asking as soon as you can predict the operator's
next three answers; write those predictions down as assumptions instead. A
follow-up round is only for an answer that opens a branch you cannot
predict (`small`: never; `bounded`: at most one).
6. **Answers that decide nothing.** «høres bra ut», «gjør som du vil», "sounds
good", "whatever you think" and an empty reply are not a decision. Record good", "whatever you think" and an empty reply are not a decision. Record
the recommended answer as an assumption ("not confirmed — the answer was the recommended answer as an assumption ("not confirmed — the answer was
«…»"), never as the operator's choice. «…»"), never as the operator's choice.
@ -251,12 +241,9 @@ names, never the text.
one-way ratchet: obra/superpowers@8ca22db (MIT, © 2025 Jesse Vincent), one-way ratchet: obra/superpowers@8ca22db (MIT, © 2025 Jesse Vincent),
skills/brainstorming. The class ladder: Kiro's docs (kiro.dev — Plan mode, skills/brainstorming. The class ladder: Kiro's docs (kiro.dev — Plan mode,
Quick Spec, full spec; read 2026-10-06; an idea only, Kiro is a closed Quick Spec, full spec; read 2026-10-06; an idea only, Kiro is a closed
product). The round, the recommended answer first and "never ask a fact": product). The answers that decide nothing (Step 3c, point 5):
mattpocock/skills@c55ee46 (MIT), productivity/grilling. The budget, addyosmani/agent-skills@9d0c60d (MIT, © 2025 Addy Osmani), skills/interview-me.
Impact × Uncertainty and the Deferred list: GitHub Spec Kit (MIT), The interview form's own credits are in `voyage:interviewing`.
templates/commands/clarify.md at 2e941fe. The stop rule and the answers that
decide nothing: addyosmani/agent-skills@9d0c60d (MIT, © 2025 Addy Osmani),
skills/interview-me.
### Internal state ### Internal state
@ -327,7 +314,8 @@ sentence with your recommended answer first.
- _Follow-up_: "How would you recognize this is done when looking at - _Follow-up_: "How would you recognize this is done when looking at
the UI / API / codebase?" the UI / API / codebase?"
**Success Criteria** (required): **Success Criteria** (required): how a criterion is written so the review can
run it is the skill `voyage:writing-criteria`; call it before proposing one.
- _Anchor_: "How do we verify it is actually done? List 2–4 concrete, - _Anchor_: "How do we verify it is actually done? List 2–4 concrete,
testable conditions — commands to run, observations, or metrics." testable conditions — commands to run, observations, or metrics."
A command the review can run is one test-runner call (`npm test`, A command the review can run is one test-runner call (`npm test`,
@ -622,7 +610,8 @@ Build the brief text from Phase 3 state by filling the template:
- **Non-Goals:** from state, or "- None explicitly stated" bullet if empty. - **Non-Goals:** from state, or "- None explicitly stated" bullet if empty.
- **Constraints / Preferences / NFRs:** from state, or "Not discussed — no - **Constraints / Preferences / NFRs:** from state, or "Not discussed — no
constraints assumed" note if empty. constraints assumed" note if empty.
- **Success Criteria:** falsifiable commands/observations from state. - **Success Criteria:** falsifiable commands/observations from state, each
written in the form of `voyage:writing-criteria`.
- **Research Plan:** one `### Topic N: {title}` section per topic with the - **Research Plan:** one `### Topic N: {title}` section per topic with the
full structure from the template. If 0 topics: write the "No external full structure from the template. If 0 topics: write the "No external
research needed — user confirmed solid knowledge of all plan research needed — user confirmed solid knowledge of all plan
@ -664,8 +653,9 @@ Launch the `brief-reviewer` agent with the prompt below, then wait for its resul
> "Review this task brief for quality: `{PROJECT_DIR}/brief.md.draft`. > "Review this task brief for quality: `{PROJECT_DIR}/brief.md.draft`.
> Check completeness, consistency, testability, scope clarity, > Check completeness, consistency, testability, scope clarity,
> research-plan validity, and memory alignment. Report findings, verdict, and > research-plan validity, and memory alignment. Judge the success criteria by
> the required machine-readable JSON block. > `voyage:writing-criteria` and review by `voyage:reviewing-adversarially`.
> Report findings, verdict, and the required machine-readable JSON block.
> Operator memory context (compare the brief's Intent/Goal and its declared > Operator memory context (compare the brief's Intent/Goal and its declared
> `framing:` value against these for EXPLICIT contradictions only): > `framing:` value against these for EXPLICIT contradictions only):
> {memory paths or excerpts, or "none supplied"}." > {memory paths or excerpts, or "none supplied"}."

View file

@ -0,0 +1,6 @@
---
type: llm
focus: last_message
---
Answers 3, 4 and 5 decided nothing ("Whatever you think is best", "Sounds good"). PASS only if whatever the reply writes for points 3, 4 and 5 is marked as an assumption or as not confirmed by the person (or is left open or deferred), and is visibly kept apart from what the person decided in answers 1, 2 and 6. FAIL if any of points 3, 4 or 5 is presented as the person's decision, or listed among the person's explicit choices, limits or exclusions without such a mark.

View file

@ -0,0 +1,6 @@
---
type: llm
focus: last_message
---
PASS only if every requirement in the reply traces to one of the six questions and answers. A new command, data field, rule or behaviour that no question raised may appear only as a marked assumption, a deferred item or a question back to the person. FAIL if the reply adds such a thing as a plain requirement.

View file

@ -0,0 +1,20 @@
---
description: "Effect case for interviewing (two arms: with and without the plugin). The write-up after a round where several answers decided nothing. Without the skill, 2 of 3 of Voyage's baseline helpers wrote such answers into the requirements as the person's own decisions (one listed them under \"operator's explicit no\"), and 2 of 3 wrote in scope nobody asked for; with it (measured in session 17.10 in the full relayed interview, not in this one-prompt form), 0 of 3 did either. The operator grants no tools beyond Skill, and sets the judge model explicitly (never Haiku)."
expected_outcome: "The requirements keep three things apart: what the person decided, what was assumed for an answer that decided nothing (each marked not confirmed), and what is deferred; nothing the person never asked for appears as a requirement."
tags: [skill-effect, skill=interviewing, from=baseline-ivw-hollow-answer]
runs: 3
max_turns: 6
timeout_seconds: 300
allowed_tools: [Skill]
---
You are interviewing me about a feature for our garden club's seed-library app: members forget to bring seeds back after harvest. You already asked one round of questions, and here are my answers. Write the requirements now, as a list I can approve.
Your questions and my answers:
1. Q: How should reminders reach members? (Recommended: the printed meeting sheet the app already writes to.) A: Yes, go with your suggestion.
2. Q: When should reminders go out? (Recommended: once a week for every loan overdue or due within two weeks.) A: Once a week, overdue or due within two weeks. Never the same reminder twice in one week.
3. Q: Should a member who is still late keep getting reminders forever, or should they stop after a while? A: Whatever you think is best.
4. Q: Should due dates depend on the crop, since kale only sets seed in its second year? A: Whatever you think is best.
5. Q: What should happen when a member's harvest failed? A: Sounds good.
6. Q: Any consequence for late members, such as blocking new loans? A: No fines and no blocking anyone. Just a friendly nudge.

View file

@ -0,0 +1,4 @@
schema_version: "1.1"
name: skill-interviewing-effect-round
context:
scaffold_script: scaffold.sh

View file

@ -0,0 +1,6 @@
---
type: llm
focus: last_message
---
PASS only if the reply asks the person at most four questions, and every question gives a recommended answer before or as its first option (marked "Recommended", "Suggested", "I recommend" or similar). A question that bundles two or more separate decisions counts as that many questions. FAIL if the reply asks five or more questions, or if any question has no recommended answer.

View file

@ -0,0 +1,6 @@
---
type: llm
focus: last_message
---
PASS only if the reply writes down what it did not ask: assumptions it made (marked as assumed, not decided by the person) or a deferred list of open points, or says plainly that nothing else is open. FAIL if the reply leaves open points neither asked nor written down, or presents its own assumptions as the person's decisions.

View file

@ -0,0 +1,15 @@
---
description: "Effect case for interviewing (two arms: with and without the plugin). The first round where Voyage's baseline helpers failed without the skill: 3 of 3 sent 10 or 11 questions in one block, and 15 of 31 questions carried no recommended answer; with it (measured in session 17.10), 3 of 3 sent 4, and 0 of 12 lacked one. None asked a fact the project could answer, so that is not graded here. The operator grants Read, Glob and Grep, and sets the judge model explicitly (never Haiku)."
expected_outcome: "The reply asks at most four questions, each with a recommended answer first, none about something the project files answer; what it does not ask is written down as an assumption or a deferred item."
tags: [skill-effect, skill=interviewing, from=baseline-ivw-round-size]
runs: 3
max_turns: 20
timeout_seconds: 600
allowed_tools: [Read, Glob, Grep, Skill]
---
I run a small seed library for my garden club. The app is the project in the current directory.
Members keep forgetting to bring seeds back after harvest, and I want the app to do something about that. Before anyone writes code, interview me so we get the requirements right.
How this works: you cannot reach me directly. Put whatever you want to ask me in your reply, and I will send my answers back to you in a message. Do not change any files in the project.

View file

@ -0,0 +1,92 @@
#!/bin/bash
# Seeds the empty run workspace with a small seed-library app (Node, node --test),
# made up from scratch. Self-contained on purpose: the run cannot read the eval
# directory, and the fixture is written inline, with no path outside the
# workspace and no git. The same project the red baseline ran against.
set -eu
mkdir -p src tests data
cat > package.json <<'J'
{ "name": "seedswap", "version": "0.4.1", "type": "module", "private": true,
"engines": { "node": ">=20" },
"scripts": { "test": "node --test", "start": "node src/cli.mjs" } }
J
cat > README.md <<'J'
# seedswap
A seed library for a garden club. Members borrow seed packets in spring and
bring back seeds from their own harvest in autumn. One volunteer runs this
command-line tool on the club laptop.
node src/cli.mjs borrow <member> <variety>
node src/cli.mjs return <member> <variety>
node src/cli.mjs list
Data lives in `data/*.json`. Messages to members go to `data/outbox.txt`,
which the volunteer prints and hands out at the monthly meeting.
J
cat > src/store.mjs <<'J'
import { readFileSync, writeFileSync } from 'node:fs';
const path = (name) => new URL(`../data/${name}.json`, import.meta.url);
export const load = (name) => JSON.parse(readFileSync(path(name), 'utf8'));
export const save = (name, value) => writeFileSync(path(name), JSON.stringify(value, null, 2) + '\n');
J
cat > src/members.mjs <<'J'
import { load } from './store.mjs';
// A member: { id, name, joined }. No email or phone is stored.
export const members = () => load('members');
export const byId = (id) => members().find((m) => m.id === id);
J
cat > src/loans.mjs <<'J'
import { load, save } from './store.mjs';
// A loan: { member, variety, borrowed: 'YYYY-MM-DD', due: 'YYYY-MM-DD', returned: null | 'YYYY-MM-DD' }.
// `due` is set to 15 October of the borrowing year (end of harvest).
export function borrow(member, variety, today) {
const loans = load('loans');
const due = `${today.slice(0, 4)}-10-15`;
loans.push({ member, variety, borrowed: today, due, returned: null });
save('loans', loans);
}
export function giveBack(member, variety, today) {
const loans = load('loans');
const open = loans.find((l) => l.member === member && l.variety === variety && !l.returned);
if (!open) throw new Error(`no open loan of ${variety} for ${member}`);
open.returned = today;
save('loans', loans);
}
export const openLoans = () => load('loans').filter((l) => !l.returned);
J
cat > src/notify.mjs <<'J'
import { appendFileSync } from 'node:fs';
// Appends one message for a member to data/outbox.txt (printed at meetings).
export function notify(member, text, today) {
appendFileSync(new URL('../data/outbox.txt', import.meta.url), `${today}\t${member.name}\t${text}\n`);
}
J
cat > src/cli.mjs <<'J'
import { borrow, giveBack, openLoans } from './loans.mjs';
const [cmd, member, variety] = process.argv.slice(2);
const today = new Date().toISOString().slice(0, 10);
if (cmd === 'borrow') borrow(member, variety, today);
else if (cmd === 'return') giveBack(member, variety, today);
else if (cmd === 'list') for (const l of openLoans()) console.log(`${l.member}\t${l.variety}\tdue ${l.due}`);
else { console.error('usage: borrow|return|list'); process.exit(2); }
J
cat > tests/loans.test.mjs <<'J'
import { test } from 'node:test';
import assert from 'node:assert/strict';
test('loans module loads', async () => {
const m = await import('../src/loans.mjs');
assert.equal(typeof m.borrow, 'function');
});
J
cat > data/members.json <<'J'
[ { "id": "m1", "name": "Ada", "joined": "2023-03-02" },
{ "id": "m2", "name": "Bo", "joined": "2024-04-11" },
{ "id": "m3", "name": "Cy", "joined": "2025-02-20" } ]
J
cat > data/loans.json <<'J'
[ { "member": "m1", "variety": "tomato-moneymaker", "borrowed": "2026-04-02", "due": "2026-10-15", "returned": null },
{ "member": "m2", "variety": "bean-borlotti", "borrowed": "2026-04-09", "due": "2026-10-15", "returned": "2026-09-30" },
{ "member": "m3", "variety": "kale-nero", "borrowed": "2025-04-20", "due": "2025-10-15", "returned": null } ]
J
: > data/outbox.txt

View file

@ -0,0 +1,5 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?interviewing"'
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the interviewing skill should fire on this request (the reserve path outside the pipeline)."
expected_outcome: "Claude calls the Skill tool for voyage:interviewing at least once."
tags: [skill-trigger, skill=interviewing, from=baseline-ivw-round-size]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
Before anyone writes code, interview me about this feature so we get the requirements right.

View file

@ -0,0 +1,5 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?interviewing"'
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the interviewing skill should fire on this request (the reserve path outside the pipeline)."
expected_outcome: "Claude calls the Skill tool for voyage:interviewing at least once."
tags: [skill-trigger, skill=interviewing, from=skillsanalyse-4.7]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
Grill me on this idea: ask me the questions you need answered, each with your recommended answer, and stop when you can predict what I will say.

View file

@ -0,0 +1,5 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?interviewing"'
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the interviewing skill should fire on this request (the reserve path outside the pipeline)."
expected_outcome: "Claude calls the Skill tool for voyage:interviewing at least once."
tags: [skill-trigger, skill=interviewing, from=baseline-ivw-hollow-answer]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
What I want from the reminder feature is still vague. Draw the requirements out of me with questions, and keep apart what I decided from what you assumed.

View file

@ -0,0 +1,8 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?interviewing"'
min: 0
max: 0
arm: both
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the interviewing skill must stay quiet here; /trekbrief owns this request (it runs the interview itself and asks for the skill by name)."
expected_outcome: "Claude never calls the Skill tool for voyage:interviewing."
tags: [skill-no-trigger, skill=interviewing, owner=trekbrief, from=order-7e-no-collision]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
Produce a task brief with an explicit research plan for adding a dark-mode setting, ready to feed research and planning.

View file

@ -0,0 +1,8 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?interviewing"'
min: 0
max: 0
arm: both
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the interviewing skill must stay quiet here; brief-reviewer owns this request (reviewing a finished brief is not interviewing anyone)."
expected_outcome: "Claude never calls the Skill tool for voyage:interviewing."
tags: [skill-no-trigger, skill=interviewing, owner=brief-reviewer, from=skillsanalyse-4.7]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
Review this task brief for completeness, consistency, testability and scope clarity before exploration begins.

View file

@ -0,0 +1,103 @@
---
name: interviewing
description: "Interview form for drawing out what someone wants before work starts: look facts up in the project instead of asking, put the open decisions in one numbered round of at most four questions per call, each with the recommended answer first and the consequence of each option, write what can be predicted down as an assumption and the rest on a deferred list, and stop once the next three answers can be predicted. An answer that decides nothing, and scope nobody asked for, are recorded as assumptions, never as the person's decisions. Use when someone asks to be interviewed or grilled about a feature, change or idea before it is built, when requirements are vague and must be drawn out with questions, or when a command runs its question round in this form."
---
# Interviewing
## Overview
An interview is for the decisions only the person can make. Everything else is looked up or
written down where the person sees it before approving: facts the project already holds, answers
that can be predicted, points that matter less than the budget allows. The open decisions go out
in one round, each with a recommended answer the person can simply accept. The result keeps three
piles apart: what the person decided, what was assumed, and what is deferred.
## When to use
- Someone asks to be interviewed, grilled or questioned before something is built.
- A request is underspecified, and the requirements must be drawn out before a plan or code.
- `/trekbrief` runs its question round in this form. It adds its own fixed questions (framing,
size class, storage), the budget per size class, and where each pile lands in the brief.
- Not for: reviewing a finished brief or plan (a review, not an interview), or a question a single
lookup settles.
## Process
1. **Look facts up first; never ask the person a fact a lookup can answer.** Which framework the
project uses, where a module lives, what the test command is, what the app does today, whether
a file exists: read the project (Glob, Grep, Read, or a helper for a wide search, never with the
Agent tool's `name` parameter). Say in a few lines what was found, so the person sees what the
questions rest on.
2. **List what is still open**: why the work is wanted, the end state, how to tell it is done,
scope, constraints, and whatever the facts left open.
3. **Drop what can be predicted.** When the request, the project and the answers so far predict an
answer, do not ask it: write the prediction down as an assumption, marked "assumed, not asked",
where the person sees it before approving.
4. **Rank the rest by Impact × Uncertainty**: how much the work changes with the answer, times how
unsure the answer is. Ask the highest within the budget (the caller's; with none given, the
four of one call). Everything over the budget goes on a **Deferred** list, one line of why
each, shown with the round. The budget never hides a gap.
5. **One round, at most 4 questions per `AskUserQuestion` call.** Every question that can be
answered now goes out in one round; more than four means consecutive calls with nothing
drafted in between. Without the tool (an interview relayed in writing), a message counts as a
call: at most four questions in it. A question that waits on another open answer belongs to a
later round.
6. **One decision per question, recommended answer first.** Each question is a full sentence in
plain words, asks one thing, says in one clause why it matters, and lists the recommended
answer first, marked "(Recommended)", with the consequence of each option in its description.
Two decisions in one question are two questions. A question without a recommendation is not
ready to ask.
7. **An answer that decides nothing is not a decision.** "Whatever you think is best", "sounds
good", "up to you" and an empty reply: record the recommended answer as an assumption, "not
confirmed — the answer was «…»", never as the person's choice. "Yes, go with your suggestion"
to a stated recommendation is a decision.
8. **Add nothing nobody asked for.** A command, field, rule or behaviour that seems a good idea
goes in as a question, an assumption or a Deferred item, never as a requirement.
9. **Follow up only on what cannot be predicted**: an answer that opens a new branch, or two
answers that contradict each other. Put a contradiction as one question with a recommendation.
10. **Stop rule.** Stop asking as soon as the person's next three answers can be predicted; write
those predictions down as assumptions instead.
11. **Write the result in three labelled piles**: decided by the person; assumed (each with why,
and "not confirmed" where the answer decided nothing); deferred. Never merge them into one
list of requirements.
## Red flags
| Thought | What it means |
|---|---|
| "Ten questions now saves a round later" | The baseline sent 10 or 11 questions in one block, with more questions folded inside them (3 of 3 runs). At most four per call; the rest is predicted, deferred or the next call. |
| "This one is open, it needs no recommendation" | 15 of 31 baseline questions had none, so the person had to invent the answer. Give one. |
| "They said whatever you think, so it is settled" | It is an assumption. 2 of 3 baseline runs wrote such answers in as the person's decision, one under "operator's explicit no". |
| "An extra command for this would help them" | Scope nobody asked for (2 of 3 baseline runs wrote some in). A question, an assumption or a Deferred item. |
| "I'll ask which test framework they use" | A fact. Look it up. |
| "One more round, to be safe" | If the next three answers can be predicted, stop and write them down. |
## Enforced by
- `lib/validators/brief-validator.mjs` (for a `/trekbrief` brief): checks the result, not the form
of the interview: the sections `## Intent`, `## Goal`, `## Success Criteria` and `## TL;DR`, and
the `framing:` field. It does not read the brief's assumptions or Deferred list.
- `lib/util/approval-log.mjs` (`answer`, in `/trekbrief` Phase 4h): refuses an answer that is not an
explicit approval, the empty reply included, at the approval step only.
- prose only: the round, at most four questions per call, the recommended answer first, facts
looked up, no unasked scope, the three piles and the stop rule. Outside `/trekbrief` none of the
checks above run.
## Credits
Ideas adapted, text rewritten; no code or prose copied.
- mattpocock/skills @c55ee46, `skills/productivity/grilling/SKILL.md:6-28`: the whole frontier in
one numbered round with a recommended answer per question; facts are the interviewer's job and
decisions the person's; a question that waits on an open answer belongs to a later round. MIT,
Copyright (c) 2026 Matt Pocock.
- addyosmani/agent-skills @9d0c60d, `skills/interview-me/SKILL.md:40-77, 132-140`: a guess attached
to every question, the polite person who agrees to be agreeable, and the stop test (can the next
three reactions be predicted). Voyage asks in a round, not one question at a time. MIT,
Copyright (c) 2025 Addy Osmani.
- github/spec-kit @2e941fe (GitHub Spec Kit), `templates/commands/clarify.md:129-146, 237`: a
question budget, the Impact × Uncertainty ranking, a why-it-matters line, the recommended option
first, and the Deferred list. MIT, Copyright GitHub, Inc.
- The targets in Red flags come from Voyage's own red baseline without this skill (three
interviews against a helper with a fixed answer sheet); see `docs/skills-authoring.md`.

View file

@ -19,7 +19,7 @@
| schema_id | fields | writer_path | line_ref | v4.1 additive | PII | | schema_id | fields | writer_path | line_ref | v4.1 additive | PII |
|-----------|--------|-------------|----------|---------------|-----| |-----------|--------|-------------|----------|---------------|-----|
| trekbrief-stats | ts, task, slug, mode, interview_turns, review_iterations, brief_quality, research_topics, auto_research, auto_result, intent_approved, project_dir, order_id, order_claimed_at, order_reason | commands/trekbrief.md (orchestrator-emit Phase 7; order_* added by lib/stats/stats-append.mjs) | trekbrief.md:1137-1161 | profile, phase_models, profile_source | none | | trekbrief-stats | ts, task, slug, mode, interview_turns, review_iterations, brief_quality, research_topics, auto_research, auto_result, intent_approved, project_dir, order_id, order_claimed_at, order_reason | commands/trekbrief.md (orchestrator-emit Phase 7; order_* added by lib/stats/stats-append.mjs) | trekbrief.md:1127-1151 | profile, phase_models, profile_source | none |
| trekresearch-stats | ts, question, mode, scope, engine, slug, project_dir, brief_path, dimensions, dimensions_baseline, dimensions_baseline_preserved, effort, conv_turns, empty_turns, unique_sources, agents_local, agents_external, gemini_used, confidence, contradictions, open_questions | lib/stats/stats-append.mjs (trekresearch.md Stats tracking) | trekresearch.md:782-808 | profile, phase_models, parallel_agents, external_research_enabled, profile_source | none | | trekresearch-stats | ts, question, mode, scope, engine, slug, project_dir, brief_path, dimensions, dimensions_baseline, dimensions_baseline_preserved, effort, conv_turns, empty_turns, unique_sources, agents_local, agents_external, gemini_used, confidence, contradictions, open_questions | lib/stats/stats-append.mjs (trekresearch.md Stats tracking) | trekresearch.md:782-808 | profile, phase_models, parallel_agents, external_research_enabled, profile_source | none |
| trekplan-stats | ts, task, mode, slug, brief_path, project_dir, codebase_size, codebase_files, agents_deployed, deep_dives, research_briefs_used, research_scout_used, critic_verdict, guardian_verdict, outcome | lib/stats/stats-append.mjs (trekplan.md Phase 12) | trekplan.md:1271-1299 | profile, phase_models, parallel_agents, profile_source | none | | trekplan-stats | ts, task, mode, slug, brief_path, project_dir, codebase_size, codebase_files, agents_deployed, deep_dives, research_briefs_used, research_scout_used, critic_verdict, guardian_verdict, outcome | lib/stats/stats-append.mjs (trekplan.md Phase 12) | trekplan.md:1271-1299 | profile, phase_models, parallel_agents, profile_source | none |
| trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | lib/stats/stats-append.mjs (trekexecute.md Phase 9) | trekexecute.md:2892-2914 | profile, phase_models, profile_source | none | | trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | lib/stats/stats-append.mjs (trekexecute.md Phase 9) | trekexecute.md:2892-2914 | profile, phase_models, profile_source | none |

View file

@ -53,16 +53,24 @@ test('trekbrief: three classes, heaviest in doubt, said aloud, overridable, only
assert.match(p3, /`ceremony:/, 'the class is written to the brief (K1)'); assert.match(p3, /`ceremony:/, 'the class is written to the brief (K1)');
}); });
// The interview form moved into the skill /trekbrief asks for by name (order
// 7e, one source): the same assertions, now read where the text lives.
const interviewing = () => read('skills/interviewing/SKILL.md');
test('trekbrief: the round — all answerable questions at once, recommendation first, no fact questions', () => { test('trekbrief: the round — all answerable questions at once, recommendation first, no fact questions', () => {
const p3 = section(brief, 'Phase 3'); const p3 = section(brief, 'Phase 3');
const iv = interviewing();
assert.match(p3, /Use `AskUserQuestion` for every question/); assert.match(p3, /Use `AskUserQuestion` for every question/);
assert.match(p3, /`voyage:interviewing`/, 'Phase 3 asks for the interview form by name');
assert.doesNotMatch(brief, /Ask one question at a time/, 'the one-at-a-time rule is replaced by the round'); assert.doesNotMatch(brief, /Ask one question at a time/, 'the one-at-a-time rule is replaced by the round');
assert.doesNotMatch(iv, /Ask one question at a time/, 'the one-at-a-time rule is replaced by the round');
assert.match(p3, /one round/i); assert.match(p3, /one round/i);
assert.match(p3, /recommend(ed|ation)[^.\n]*first/i); assert.match(iv, /one round/i);
assert.match(p3, /never ask[^.\n]*fact/i); assert.match(iv, /recommend(ed|ation)[^.\n]*first/i);
assert.match(p3, /Impact × Uncertainty/); assert.match(iv, /never ask[^.\n]*fact/i);
assert.match(p3, /[Dd]eferred/); assert.match(iv, /Impact × Uncertainty/);
assert.match(p3, /next three answers/); assert.match(iv, /[Dd]eferred/);
assert.match(iv, /next three answers/);
}); });
test('trekbrief: the budget for a small task is at most 5 questions and fits one AskUserQuestion call', () => { test('trekbrief: the budget for a small task is at most 5 questions and fits one AskUserQuestion call', () => {
@ -75,10 +83,16 @@ test('trekbrief: the budget for a small task is at most 5 questions and fits one
}); });
test('trekbrief: credits every borrowing of the round and the classes', () => { test('trekbrief: credits every borrowing of the round and the classes', () => {
// The classes stay in /trekbrief with their credits; the round's credits
// moved with the round into the skill.
const p3 = section(brief, 'Phase 3'); const p3 = section(brief, 'Phase 3');
for (const src of ['obra/superpowers@8ca22db', 'mattpocock/skills@c55ee46', 'GitHub Spec Kit', 'addyosmani/agent-skills@9d0c60d', 'kiro.dev']) { for (const src of ['obra/superpowers@8ca22db', 'kiro.dev']) {
assert.ok(p3.includes(src), `credit for ${src}`); assert.ok(p3.includes(src), `credit for ${src}`);
} }
const iv = interviewing();
for (const re of [/mattpocock\/skills ?@c55ee46/, /GitHub Spec Kit/, /addyosmani\/agent-skills ?@9d0c60d/]) {
assert.match(iv, re, `credit for ${re}`);
}
}); });
test('trekbrief: hard rule 8 no longer says Phase 3 has no cap', () => { test('trekbrief: hard rule 8 no longer says Phase 3 has no cap', () => {

View file

@ -0,0 +1,171 @@
// tests/lib/skills-brief-prose-pins.test.mjs
// Wave 3, order 7e: the `interviewing` skill and its wiring into /trekbrief.
//
// What is pinned, and why:
// • the skill's form is not pinned here: the form check
// (lib/skills/skill-lint.mjs) and the routing test (lib/skills/routing.mjs)
// run over every skill in `npm test` and are the authority (contract S1);
// the routing test is also what shows the skill does not collide with
// /trekbrief (cosine >= 0.75 fails, never lowered);
// • the interview form lives in the skill: at most four questions per call,
// the recommended answer first, facts looked up, the rest written down,
// the stop rule, and the two failures the red baseline showed (an answer
// that decides nothing written in as a decision; scope nobody asked for);
// • /trekbrief asks for it by name, and for `voyage:writing-criteria` (S3,
// order 7c) where the success criteria are written, and names both
// review skills (S3 + S4) where it starts brief-reviewer;
// • the text that moved is gone from /trekbrief (one source), while what is
// /trekbrief's own contract stays: the size class and its budget, the
// framing question, the storage question, the approval and the answers
// that decide nothing;
// • the skill says plainly that the interview's form is prose only.
//
// S3 and S4 are made by orders 7c and 7d. Their skills need not exist in this
// branch: the names are the contract, written here before the merge.
// Counted as doc pins by lib/util/test-census.mjs (file name *-prose-pins).
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
const SKILL = 'skills/interviewing/SKILL.md';
const skill = () => {
assert.ok(existsSync(join(ROOT, SKILL)), `${SKILL} missing`);
return read(SKILL);
};
const brief = read('commands/trekbrief.md');
/** Text from `## <heading>` up to the next `## ` heading. */
function section(text, heading) {
const start = text.indexOf(`\n## ${heading}`);
assert.ok(start >= 0, `missing ## ${heading}`);
const next = text.indexOf('\n## ', start + 4);
return text.slice(start, next < 0 ? undefined : next);
}
/** Text from `### <heading>` up to the next `### ` or `## ` heading. */
function subsection(text, heading) {
const start = text.indexOf(`\n### ${heading}`);
assert.ok(start >= 0, `missing ### ${heading}`);
const rest = text.slice(start + 5);
const m = rest.match(/\n#{2,3} /);
return text.slice(start, m ? start + 5 + m.index : undefined);
}
const flat = (t) => t.replace(/\s+/g, ' ');
// ---- the skill carries the interview form ----
test('pin: the interviewing skill exists', () => {
skill();
});
test('pin: interviewing — at most four questions per AskUserQuestion call, recommended answer first', () => {
const p = flat(section(skill(), 'Process'));
assert.match(p, /at most (4|four) questions per `AskUserQuestion` call/i);
assert.match(p, /recommended answer first/i);
assert.match(p, /\(Recommended\)/);
assert.match(p, /consequence of each option/i);
});
test('pin: interviewing — facts are looked up, never asked', () => {
const p = flat(section(skill(), 'Process'));
assert.match(p, /never ask[^.]*fact/i);
});
test('pin: interviewing — the rest is ranked and written down: assumptions and a Deferred list', () => {
const p = flat(section(skill(), 'Process'));
assert.match(p, /Impact × Uncertainty/);
assert.match(p, /[Dd]eferred/);
assert.match(p, /assumed, not asked/);
});
test('pin: interviewing — the stop rule: the next three answers can be predicted', () => {
assert.match(flat(section(skill(), 'Process')), /next three answers/);
});
test('pin: interviewing — the two measured failures: an answer that decides nothing, and unasked scope', () => {
const p = flat(section(skill(), 'Process'));
assert.match(p, /not confirmed — the answer was/);
assert.match(p, /never as the person's (choice|decision)/i);
assert.match(p, /nobody asked for/i);
const flags = section(skill(), 'Red flags');
assert.match(flags, /[Ww]hatever you think/);
});
test('pin: interviewing — the interview form is prose only; the brief validator checks the result', () => {
const enforced = flat(section(skill(), 'Enforced by'));
assert.ok(enforced.includes('lib/validators/brief-validator.mjs'), 'names the brief validator');
assert.match(enforced, /prose only/);
assert.match(enforced, /not the form of the interview/);
});
test('pin: interviewing — credits its sources with commit and licence', () => {
const credits = section(skill(), 'Credits');
for (const src of [
'mattpocock/skills @c55ee46', 'skills/productivity/grilling/SKILL.md',
'addyosmani/agent-skills @9d0c60d', 'skills/interview-me/SKILL.md',
'github/spec-kit @2e941fe', 'templates/commands/clarify.md',
]) assert.ok(credits.includes(src), `credit ${src}`);
assert.equal((credits.match(/\bMIT\b/g) || []).length, 3, 'one licence per source');
});
// ---- /trekbrief asks for the skills by name (S3, S4, interviewing) ----
test('pin: /trekbrief Phase 3 asks for voyage:interviewing by name, and the command may use the Skill tool', () => {
assert.match(section(brief, 'Phase 3'), /`voyage:interviewing`/);
assert.match(section(brief, 'Phase 3'), /Call the Skill tool/);
assert.match(brief.split('\n---\n')[0], /\nallowed-tools:[^\n]*\bSkill\b/, 'the command may use the Skill tool');
});
test('pin: /trekbrief names voyage:writing-criteria (S3) where the success criteria are asked and written', () => {
const bank = subsection(brief, 'Question bank');
const sc = bank.slice(bank.indexOf('**Success Criteria**'), bank.indexOf('**Research Plan**'));
assert.match(sc, /`voyage:writing-criteria`/);
const draft = brief.slice(brief.indexOf('**Step 4a'), brief.indexOf('**Step 4b'));
assert.match(draft, /\*\*Success Criteria:\*\*[^\n]*\n?[^\n]*`voyage:writing-criteria`/);
});
test('pin: /trekbrief names both review skills where it starts brief-reviewer (S3 + S4)', () => {
const step = brief.slice(brief.indexOf('**Step 4c'), brief.indexOf('**Step 4d'));
assert.match(step, /`voyage:writing-criteria`/);
assert.match(step, /`voyage:reviewing-adversarially`/);
});
// ---- one source: the moved text is gone from /trekbrief ----
const MOVED = [
'Never ask the operator a fact a helper can look up',
'Rank the rest by Impact × Uncertainty',
'The tool takes at most 4 questions per call',
"Stop asking as soon as you can predict the operator's next three answers",
'Each question is a full sentence in plain words',
'The round, the recommended answer first and "never ask a fact": mattpocock/skills@c55ee46',
'Impact × Uncertainty and the Deferred list: GitHub Spec Kit',
'The stop rule and the answers that decide nothing: addyosmani/agent-skills@9d0c60d',
];
test('pin: the interview text that moved into the skill is no longer in /trekbrief', () => {
const b = flat(brief);
for (const s of MOVED) assert.ok(!b.includes(s), `still in commands/trekbrief.md: «${s}»`);
});
// ---- what stays: /trekbrief's own contract ----
test('pin: /trekbrief keeps its own contract — class, budget, framing first, storage, hollow answers, approval', () => {
const p3 = flat(section(brief, 'Phase 3'));
assert.match(p3, /### Step 3b — Ceremony by size/);
assert.match(p3, /\*\*Budget per class\*\*/);
assert.match(p3, /framing[^\n]*first/i);
assert.match(p3, /artifact_store/);
for (const w of ['høres bra ut', 'gjør som du vil']) assert.ok(p3.includes(w), `Phase 3 keeps «${w}»`);
assert.match(p3, /empty reply/);
assert.match(section(brief, 'Phase 2.5'), /hollow answer/i);
assert.match(section(brief, 'Phase 4h'), /høres bra ut/);
});