| name | research-scope |
| description | Use when defining or revising Project, Direction, or Task Scope intent, or when execution discovers a needed scope change. The agent only proposes through Triage and must show every proposed Scope clearly to the PM with the next ratification step; accepted Scope writes route through research-op's scope-transition op. Never invokes git. |
| allowed-tools | Bash(python3 *), Read, Edit, Write, Grep, Glob |
| disable-model-invocation | false |
research-scope (R1 · scope + Triage admission gate)
The agent proposes; the PM disposes. This separation is how "user-monitored" and "autonomous" coexist:
the Scope SSOT is never mutated by agent action alone — every write is either a user-committed
scope-transition (accepted Triage item) or rejected with the SSOT left untouched.
The PM visibility invariant is mandatory: never ask the PM to accept, reject, or revise a Scope
proposal without showing the exact Scope content first. Every Project, Direction, or Task proposal
must be presented as a clear review block that names the current state and the next PM action.
If the user has only a vague idea and cannot yet state a clear Direction spec
(hypothesis / metric / baselines / success_gate), use /research-brainstorm first — it shapes
the idea into pre-package brainstorms and converges them into a Direction proposal. Use this skill
directly when the Direction is already clear.
Resources
Pipeline root: /home/uqzzha35/Project/Trustworthy-Research-Pipeline/Trustworthy-Research-Pipeline
| Resource | Path |
|---|
| Scope SSOT lib | <pipeline-root>/lib/scope_ssot/__init__.py |
| Triage CLI | <pipeline-root>/skills/research-scope/scripts/triage.py |
| Transition log (SSOT commits) | outputs/_scope/transitions.jsonl |
| Triage queue (pending/disposed) | outputs/_scope/triage.jsonl |
| research-op entrypoint | <pipeline-root>/skills/research-op/scripts/research_op.py |
| Milestone planner | <pipeline-root>/skills/research-scope/scripts/plan_milestones.py |
| Scope name migration | <pipeline-root>/skills/research-scope/scripts/migrate_scope_names.py |
| Direction→package materializer | <pipeline-root>/skills/research-package/scripts/create_from_scope.py |
Import pattern for the lib:
import sys
sys.path.insert(0, "<pipeline-root>/lib")
import scope_ssot
Triage CLI commands:
python3 skills/research-scope/scripts/triage.py propose \
--log outputs/_scope/triage.jsonl \
--item '<json>'
python3 skills/research-scope/scripts/triage.py pending \
--log outputs/_scope/triage.jsonl
python3 skills/research-scope/scripts/triage.py dispose \
--log outputs/_scope/triage.jsonl \
--id <item-id> \
--decision ACCEPTED|REJECTED
On accept, the human then commits the transition (agent does NOT do this). Prefer committing from the
accepted Triage item so the SSOT payload is bound to what the PM saw:
python3 skills/research-op/scripts/research_op.py \
--pkg _scope --op scope-transition --from-triage <item-id>
The explicit payload form is available for structured callers, but then the payload must carry all
seven node fields plus op (one of create / revise / supersede / reopen / archive) and
gate (the required gate for the node's level — see the gate table below); research_op.py reads
op and gate out of the payload and passes them to scope_ssot.propose_transition, which rejects a
missing/illegal op or a mismatched gate.
Node shape
A node has these required fields:
{
"id": "<unique-string>",
"level": "project|direction|task",
"parents": ["<parent-id>"],
"version": 1,
"status": "ACTIVE",
"spec": { ... },
"source": "<free text or reference>"
}
Spec fields differ by level. Supply all fields for the relevant level, no others:
| level | required spec fields and form |
|---|
project | goal: string, 3-100 words so exact short objectives can be ratified. contributions: non-empty list of strings, each 5-50 words. out_of_scope: non-empty list of strings, each 5-50 words. |
direction | hypothesis: string, 20-100 words. metric: non-empty object, or string with 20-100 words. baselines: non-empty list of strings, each 5-50 words. success_gate: string, 20-100 words. |
task | experiment: string, 20-100 words. config: non-empty reference string. gate: string, 20-100 words. control_mode: one of SUPERVISED, CHECKPOINTED, DEFERRED, AUTONOMOUS. |
A spec must not contain readings (measured values, results, verdicts). Those live in results surfaces, not in scope. config is a ref/path and control_mode is an enum, so the 20-100 word scalar-text rule does not apply to them.
Required gate per level — the gate field passed to scope_ssot.propose_transition:
| level | gate |
|---|
project | USER_ONLY |
direction | USER_CROSS_MODEL_AUDIT |
task | AGENT_DEFERRED_ACK |
Procedure
1. Read the learning context gate, active Scope, and pending Triage.
Before drafting any Project, Direction, or Task proposal, load the project learning summary:
python3 research_html/scripts/learning_context_gate.py --root research_html --json
If the gate fails, repair the malformed learning source before proposing Scope. A zero count is not a
problem; an unreadable rules or package source is.
import sys; sys.path.insert(0, "<pipeline-root>/lib"); import scope_ssot
records = scope_ssot.read_log("outputs/_scope/transitions.jsonl")
projection = scope_ssot.fold(records)
history = scope_ssot.history("<node-id>", records)
If the log does not exist or is empty, there is no committed scope yet — the first proposal creates it.
Also inspect pending proposals before drafting a new one:
python3 skills/research-scope/scripts/triage.py pending \
--log outputs/_scope/triage.jsonl
Use pending items only as collision warnings. They are not accepted intent and must not be treated as
Scope authority. If a pending item targets the same node, level, or substantially the same change, surface
that conflict instead of submitting a duplicate proposal.
2. Validate the proposed node.
Build the node dict according to the shape above, then call:
scope_ssot.validate_node(node)
Fix any RuleViolation before proceeding. Do not hand-edit log files to work around a violation.
Migration from old Scope names.
If a live project still has old yardstick / provenance records, migrate them explicitly:
python3 skills/research-scope/scripts/migrate_scope_names.py \
--transitions outputs/_scope/transitions.jsonl \
--triage outputs/_scope/triage.jsonl \
--inventory research_html/data/research-packages.js \
--write
Run it without --write first for a dry run. Normal Scope validation rejects the old node shape.
3. Build the Triage item.
The item dict passed to triage.py propose must include:
{
"id": "<unique-item-id>",
"level": "project|direction|task",
"change": "<one-sentence description of what changes>",
"rationale": "<why this change is needed>",
"proposed_spec": { ... },
"post_accept_actions": []
}
For a level == "direction" proposal, ask this QA before calling triage.py propose:
This Direction is still a pending proposal. If you accept it into the Scope SSOT, should I then propose high-level validation milestones before any package is generated?
If the user answers yes, set:
"post_accept_actions": ["plan_validation_milestones"]
If the user answers no or later, leave post_accept_actions empty. Never create the package from a pending Triage item, and never let package surfaces invent high-level validation goals.
4. Submit the proposal.
python3 skills/research-scope/scripts/triage.py propose \
--log outputs/_scope/triage.jsonl \
--item '{"id":"scope-001","level":"direction","change":"...","rationale":"...","proposed_spec":{...}}'
5. Show pending items to the user and STOP.
python3 skills/research-scope/scripts/triage.py pending \
--log outputs/_scope/triage.jsonl
Display the pending list. Do not proceed further. The agent's work ends here — scope commitment is the human PM's decision.
Use this PM-facing format for every proposed Scope item:
**Scope Review**
- Status: Pending Triage, not yet committed
- Level: project | direction | task
- Node: <node id>
- Parents: <parent ids, or [] for a Project>
- Operation / Gate: <create|revise|...> / <required gate>
- Spec: <each spec field exactly as it would enter the Scope node>
- Rationale: <why this Scope change is proposed>
- Post-Accept Actions: <planned follow-up, or []>
- Next Step: ACCEPT to ratify via `research-op --pkg _scope --op scope-transition --from-triage <item-id>`, REVISE with field changes, or REJECT to archive
If the user supplied exact text for any spec field, show that text verbatim. Agent-authored
interpretation or rationale must be visibly separate from the spec content that would enter the SSOT.
Human accept path (PM action, not agent):
- PM runs
triage.py dispose --decision ACCEPTED.
- PM commits via
research-op --pkg _scope --op scope-transition --from-triage <item-id>.
- The transition is appended to
outputs/_scope/transitions.jsonl, then the dashboard
projection is refreshed when research_html/data/ exists.
- If the accepted item has
post_accept_actions containing plan_validation_milestones, ask one short confirmation: "Direction is now committed. Propose high-level validation milestones for it?" On yes, invoke plan_milestones.py with the committed direction node id. On no, stop and report that plan_milestones.py --direction-id <direction-id> can be run later.
Milestone proposal command:
python3 skills/research-scope/scripts/plan_milestones.py \
--direction-id <direction-node-id> \
--transitions outputs/_scope/transitions.jsonl \
--triage outputs/_scope/triage.jsonl
After the PM accepts/revises those milestone proposals and commits each Task/Milestone node with research-op --pkg _scope --op scope-transition --from-triage <item-id>, ask: "Milestones are now committed. Generate the research package from the Direction plus accepted milestones?" On yes, use /research-package from-scope <direction-id>. Script-level execution first checks readiness:
python3 skills/research-package/scripts/create_from_scope.py \
--check --json \
--direction-id <direction-node-id> \
--root research_html \
--transitions outputs/_scope/transitions.jsonl
If materializable is false, stop and hand off to the returned nextSkill. If it is true, invoke the
materializer:
python3 skills/research-package/scripts/create_from_scope.py \
--direction-id <direction-node-id> \
--root research_html \
--transitions outputs/_scope/transitions.jsonl
Human reject path: PM runs triage.py dispose --decision REJECTED. The item is archived in triage.jsonl; the SSOT is untouched.
Example — proposing a direction node:
python3 skills/research-scope/scripts/triage.py propose \
--log outputs/_scope/triage.jsonl \
--item '{
"id": "dir-retrieval-v2",
"level": "direction",
"change": "Narrow retrieval target to zero-shot cross-modal setting only",
"rationale": "In-distribution results are at ceiling; zero-shot gap is the open problem",
"proposed_spec": {
"hypothesis": "Cross-modal zero-shot retrieval can improve held-out Recall at one without supervised fine tuning by aligning text and video representations more consistently.",
"metric": {"name": "R@1", "split": "MSRVTT zero-shot", "dir": "higher"},
"baselines": ["CLIP zero-shot retrieval baseline on the same held-out split."],
"success_gate": "Recall at one must reach at least forty eight on the held-out seed before this Direction can be adopted safely."
},
"post_accept_actions": ["plan_validation_milestones"]
}'
Output contract
| Path | Written by | Contents |
|---|
outputs/_scope/triage.jsonl | Agent (propose) + PM (dispose) | Pending and disposed Triage items, including optional post-accept milestone-planning intent, one JSON object per line |
outputs/_scope/transitions.jsonl | PM only (via research-op scope-transition) | Committed scope transitions, one JSON object per line |
outputs/_scope/_actions.jsonl | research-op | Audit line for synthetic _scope scope-transition ops |
The agent appends to triage.jsonl only. It never writes to transitions.jsonl directly.
Done condition
The skill is done when the pending Triage item is visible in triage.jsonl, has been shown to the user, and any direction-level milestone-planning QA has been answered and recorded in post_accept_actions. The scope change is not yet in effect — it takes effect only after PM acceptance and the research-op --pkg _scope --op scope-transition --from-triage <item-id> commit. Package files are created only after the Direction and its accepted high-level validation milestones are committed.
Error path
| Error | Meaning | Action |
|---|
RuleViolation from validate_node | The node dict violates the schema (missing field, wrong level, reading in spec, etc.) | Fix the node dict and retry validate_node before calling triage.py propose. Never hand-edit the log. |
RuleViolation from scope_ssot.propose_transition (human path) | The transition op was refused by the gate check | Confirm the gate value matches scope_ssot.REQUIRED_GATE[node["level"]] (e.g. USER_CROSS_MODEL_AUDIT for direction) and retry. |
triage.py propose exits non-zero | Item JSON is malformed, or the id key is missing (the script enforces only id) | Check the --item JSON parses and carries id. level, change, rationale, and proposed_spec are required by this contract (downstream consumers need them) but are not validated by the script — include them anyway. |
| Triage item sits pending indefinitely | PM has not disposed it | Surface the pending list again; do not re-propose the same change. |