# free-triager

Evidence-based bug bounty report triage against live program rules, scope, known issues, exploitability, PoC requirements, and submission standards. Use when a researcher wants to reduce rejection risk before submitting or revising a vulnerability report.

- **Kind:** skill
- **Source:** https://github.com/Hsnmughal/Free-Triager
- **Page:** https://forefy.com/skills/f4b7cfb5-328f-487d-9e19-4c7431f31134
- **API (JSON + files):** https://forefy.com/api/asr/f4b7cfb5-328f-487d-9e19-4c7431f31134

---

## .github

```

```

## .github/ISSUE_TEMPLATE

```

```

## .github/ISSUE_TEMPLATE/assessment_feedback.yml

```yaml
name: Assessment feedback
description: Report a false positive, false negative, or unclear triage decision
title: "[Assessment]: "
labels:
  - assessment-feedback
body:
  - type: markdown
    attributes:
      value: Share only synthetic, public, or fully anonymized evidence. Never disclose an active vulnerability report here.
  - type: dropdown
    id: category
    attributes:
      label: Feedback category
      options:
        - False rejection risk
        - Missed rejection risk
        - Incorrect technical assessment
        - Incorrect scope or rules assessment
        - Incorrect severity assessment
        - Unclear explanation
    validations:
      required: true
  - type: input
    id: platform
    attributes:
      label: Platform
      placeholder: Immunefi
    validations:
      required: true
  - type: textarea
    id: sanitized_case
    attributes:
      label: Sanitized case
      description: Describe the relevant facts without exposing confidential details.
    validations:
      required: true
  - type: textarea
    id: correction
    attributes:
      label: Expected assessment and supporting evidence
    validations:
      required: true
```

## .github/ISSUE_TEMPLATE/bug_report.yml

```yaml
name: Bug report
description: Report incorrect behavior in Free Triager
title: "[Bug]: "
labels:
  - bug
body:
  - type: markdown
    attributes:
      value: Do not include undisclosed vulnerabilities, private reports, credentials, or confidential platform communications.
  - type: input
    id: version
    attributes:
      label: Free Triager version
      placeholder: 0.1.0
    validations:
      required: true
  - type: input
    id: provider
    attributes:
      label: Agent or provider
      placeholder: Codex, Claude Code, Cursor, or another provider
    validations:
      required: true
  - type: dropdown
    id: mode
    attributes:
      label: Workflow mode
      options:
        - oneshot
        - dynamic
        - not applicable
    validations:
      required: true
  - type: textarea
    id: behavior
    attributes:
      label: What happened?
      description: Use synthetic or fully anonymized inputs.
    validations:
      required: true
  - type: textarea
    id: expected
    attributes:
      label: What did you expect?
    validations:
      required: true
  - type: textarea
    id: reproduction
    attributes:
      label: Reproduction steps
    validations:
      required: true
```

## .github/ISSUE_TEMPLATE/config.yml

```yaml
blank_issues_enabled: false
```

## .github/ISSUE_TEMPLATE/platform_request.yml

```yaml
name: Platform adapter request
description: Propose support for another bug bounty platform
title: "[Platform]: "
labels:
  - platform-request
body:
  - type: input
    id: platform
    attributes:
      label: Platform name
    validations:
      required: true
  - type: input
    id: public_docs
    attributes:
      label: Public rules or documentation URL
    validations:
      required: true
  - type: textarea
    id: differences
    attributes:
      label: Important platform-specific behavior
      description: Describe scope, severity, proof-of-concept, report, or triage differences supported by public sources.
    validations:
      required: true
```

## .github/PULL_REQUEST_TEMPLATE.md

## Summary

Describe the problem and the proposed change.

## Evidence

Explain the documented rule, observed failure, or reproducible behavior supporting this change.

## Affected Areas

- Platform(s):
- Technology profile(s):
- Workflow mode(s):
- Provider(s), if applicable:

## Validation

- [ ] `node --check scripts/session.mjs`
- [ ] `node --test tests/session.test.mjs`
- [ ] No confidential report data or credentials are included
- [ ] Platform-specific behavior remains inside its adapter
- [ ] Documentation is updated where behavior changed

## .github/workflows

```

```

## .github/workflows/validate.yml

```yaml
name: Validate

on:
  push:
  pull_request:

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Check session helper syntax
        run: node --check scripts/session.mjs

      - name: Run session tests
        run: node --test tests/session.test.mjs

      - name: Validate JSON schema syntax
        run: node -e "JSON.parse(require('node:fs').readFileSync('schemas/session-event.schema.json', 'utf8'))"
```

## .gitignore

```

```

## CHANGELOG.md

# Changelog

All notable changes to Free Triager will be documented in this file.

## 0.1.0 - Unreleased

- Added the technology-neutral triage workflow.
- Added the Immunefi v1 platform adapter.
- Added memory-only oneshot mode.
- Added checkpointed dynamic mode with verified JSONL resume state.
- Added smart-contract, web/app, Blockchain/DLT, and mixed-system routing.

## CODE_OF_CONDUCT.md

# Code of Conduct

## Our Commitment

Free Triager welcomes researchers, developers, platform representatives, and contributors of every background and experience level. We are committed to maintaining a respectful, professional, and harassment-free community.

## Expected Behavior

- Be respectful, constructive, and patient.
- Critique ideas, evidence, and implementation rather than people.
- Treat unpublished vulnerability information as confidential.
- Respect platform rules, coordinated-disclosure requirements, and project boundaries.
- Clearly distinguish verified facts from opinions and assumptions.
- Accept corrections and help improve the project for everyone.

## Unacceptable Behavior

- Harassment, discrimination, threats, or personal attacks.
- Publishing confidential vulnerability reports, private communications, credentials, or personal information without permission.
- Using project channels to coordinate unauthorized exploitation or evade platform safeguards.
- Knowingly submitting fabricated evidence, misleading claims, or plagiarized work.
- Disruptive conduct that prevents productive technical discussion.

## Enforcement

Project maintainers may edit or remove contributions, comments, issues, or other participation that violates this policy. Serious or repeated violations may result in a temporary or permanent ban from project spaces.

Report conduct concerns privately through the contact methods in [SECURITY.md](SECURITY.md). Maintainers will handle reports as confidentially and fairly as practical.

## CONTRIBUTING.md

# Contributing to Free Triager

Contributions that improve triage accuracy, reduce false rejections, add platform support, or strengthen provider compatibility are welcome.

## Before You Contribute

- Do not include undisclosed vulnerabilities, private reports, credentials, authentication material, or confidential platform communications.
- Use synthetic or fully anonymized fixtures unless every affected party has authorized publication.
- Preserve the distinction between technical validity, program eligibility, submission readiness, and automated-triage risk.
- Do not turn one platform's terminology or behavior into a universal rule.
- Cite the provenance of platform rules and label user-supplied private guidance separately.

## Development

Free Triager requires no package installation for its current test suite. Use a maintained Node.js release and run:

```text
node --check scripts/session.mjs
node --test tests/session.test.mjs
```

Changes to the skill entrypoint should also be validated with the skill validator available in your agent environment, when applicable.

## Adding a Platform

Platform-specific behavior belongs under `platforms/<platform>/`. A new adapter must:

1. Declare its URL hosts and source-discovery behavior.
2. Define rule precedence without overriding explicit program terms.
3. Map platform terminology to the shared asset, impact, severity, and submission concepts.
4. Document proof-of-concept and report requirements using attributable sources.
5. Keep automated triage behavior platform-specific and separate from human-review merits.
6. Handle programs where primacy rules are absent.
7. Add synthetic evaluation cases and conformance tests.
8. Avoid changes to the core architecture unless the platform exposes a genuinely universal missing concept.

## Pull Requests

Keep pull requests focused. Explain:

- The behavior being changed and why.
- The evidence or real failure mode supporting the change.
- Which platforms, technologies, modes, and providers are affected.
- How the change was tested.
- Whether it changes persisted dynamic-session data or compatibility.

Do not edit expected output merely to make a failing test pass. Tests should protect observable behavior and workflow invariants rather than exact generated prose.

By participating, you agree to follow the [Code of Conduct](CODE_OF_CONDUCT.md).

## LICENSE

```

```

## README.md

# Free Triager

Free pre-submission triage for vulnerability reports.

[![Version](https://img.shields.io/badge/version-0.1.0-2ea44f?style=flat-square)](VERSION) [![MIT License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE) [![Validate](https://github.com/Hsnmughal/Free-Triager/actions/workflows/validate.yml/badge.svg)](https://github.com/Hsnmughal/Free-Triager/actions/workflows/validate.yml)

[![Claude Code](https://img.shields.io/badge/Claude_Code-F5E6D0?style=for-the-badge&logo=anthropic&logoColor=1a1a1a)](https://claude.ai/download) [![Cursor](https://img.shields.io/badge/Cursor-000000?style=for-the-badge&logo=cursor&logoColor=white)](https://cursor.com/) [![Codex](https://img.shields.io/badge/Codex-000000?style=for-the-badge&logo=openai&logoColor=white)](https://openai.com/index/introducing-codex/) [![GitHub Copilot](https://img.shields.io/badge/GitHub_Copilot-000000?style=for-the-badge&logo=githubcopilot&logoColor=white)](https://github.com/features/copilot) [![Windsurf](https://img.shields.io/badge/Windsurf-0062FF?style=for-the-badge&logo=windsurf&logoColor=white)](https://www.windsurf.com/)

---

Free Triager checks whether a finding is technically valid, eligible under a bug bounty program's rules and scope, and ready for the platform's submission process. Its purpose is to catch preventable rejection risks before a security researcher pays a submission fee.

> [!IMPORTANT]
> Free Triager provides an evidence-based assessment, not a guarantee that a platform or project will accept a report.

## Why I Made Free Triager

I made Free Triager after seeing fellow security researchers lose submission fees before their reports received meaningful human review. A technically valid vulnerability can still be rejected because its proof of concept does not follow the platform's requirements, its claimed asset or impact is outside the program's scope, or the report conflicts with a program-specific rule.

This has become especially important on Immunefi, where automated front-line triage such as Andrew can reject a submission before a human reviews its technical merits. Submission fees can range from tens to hundreds of dollars, depending on the platform and submission tier. Discovering a formatting, scope, or proof-of-concept problem only after submission wastes the researcher's money and can also create avoidable appeals and manual review work for the platform.

Free Triager performs that review before submission. It is designed to help researchers find genuine weaknesses in their report, fix eligible reports, and avoid paying to submit reports that are known issues, out of scope, technically unsupported, or inconsistent with the applicable rules.

## What It Checks

Free Triager evaluates four separate questions:

<table>
<tr><td><strong>Technical validity:</strong></td><td>Does the reported behavior exist, and is the claimed attack path realistic and reproducible?</td></tr>
<tr><td><strong>Program eligibility:</strong></td><td>Are the affected asset, impact, assumptions, and attack conditions covered by the program's current rules?</td></tr>
<tr><td><strong>Submission readiness:</strong></td><td>Does the report and its proof of concept satisfy the platform's submission requirements?</td></tr>
<tr><td><strong>Rejection risk:</strong></td><td>Which specific issues could cause automated or human triage to reject or downgrade the report, and how should they be corrected?</td></tr>
</table>

It keeps automated-triage readiness separate from the underlying technical merits. A valid but poorly presented report is not treated as an invalid vulnerability.

## Supported Platforms

| Platform | Status | Platform-specific behavior |
|---|---|---|
| Immunefi | v1 supported | Scope and impact rules, known issues, proof-of-concept requirements, report structure, and Andrew-facing submission readiness |

The architecture uses independent platform adapters. Future platforms can be added without changing the core triage workflow.

## Supported Technologies

Free Triager is technology-neutral. It derives the relevant technology and validation approach from the report, program, and available evidence.

<table>
<tr><td>Smart contracts, regardless of language or virtual machine</td><td>Web and application targets</td></tr>
<tr><td>Blockchain and distributed-ledger implementations</td><td>Mixed findings that cross contracts, applications, nodes, bridges, or other system boundaries</td></tr>
</table>

A local codebase is useful but not mandatory when a black-box report contains sufficient reproducible evidence.

## Required Inputs

<table>
<tr><td>The report, supplied as a file path or pasted text</td><td>The program's public URL</td></tr>
<tr><td>A workflow mode: <code>oneshot</code> or <code>dynamic</code></td><td>An optional path to the codebase or other technical evidence; the current directory is used by default when applicable</td></tr>
</table>

Private or authenticated platform guidance may also be supplied by the user. Free Triager records it as user-supplied material and does not claim independent access to private pages.

## Workflow Modes

| Mode | Best for | Persistence |
|---|---|---|
| `oneshot` | Models and plans with enough context for the complete assessment | Memory only; creates no session, ledger, checkpoint, cache, or copied report |
| `dynamic` | Smaller context windows, compaction, or migration to another task | Completes one phase per invocation and saves an append-only JSONL checkpoint |

In dynamic mode, resume with:

```text
$free-triager resume=<absolute-session-directory>
```

The saved program URL is reopened before every resumed phase so the next task uses fresh program evidence instead of relying on compacted conversation history.

## Installation

Install or copy this repository into a coding agent's supported skills directory. The skill entrypoint is [`SKILL.md`](SKILL.md), and its name is `free-triager`.

The instructions and workflow are provider-neutral. Provider-specific metadata under `agents/` is optional and does not control the core assessment.

## Usage

Provide every known input in the initial invocation. Free Triager asks only for required information that is missing.

```text
$free-triager "C:\path\to\report.md" https://immunefi.com/bug-bounty/example/information/ oneshot "C:\path\to\codebase"
```

For a checkpointed run:

```text
$free-triager "C:\path\to\report.md" https://immunefi.com/bug-bounty/example/information/ dynamic "C:\path\to\codebase"
```

The final assessment distinguishes technical validity, eligibility, severity, automated-triage readiness, human-review merits, unresolved evidence, and recommended report improvements.

Free Triager reviews reports by default. It modifies a report only when the user explicitly requests a revision after reviewing the assessment, and it preserves the original unless overwrite is explicitly requested.

## Privacy

Vulnerability reports and dynamic session data may be highly confidential. Dynamic runs are stored under `.free-triager/`, which is excluded from version control by this repository's [`.gitignore`](.gitignore).

Before sharing logs, fixtures, issues, or pull requests:

> [!CAUTION]
> Remove undisclosed vulnerability details and proof-of-concept secrets.  
> Remove session data, authentication material, cookies, and tokens.  
> Obtain permission before publishing real reports or platform communications.  
> Prefer synthetic or fully anonymized examples.

## ☕ Free Triager Saved You a Fee?

> [!TIP]
> **If Free Triager helped you avoid a submission fee, consider buying me a coffee. ☕**
>
> **Evm:** `0xcc785a11f063c589946e413686a63c313b161e4b`
> **Solana:** `5WnXPkxEnHVk1CdTcSbyA4YJqFSijecU4vFe28ZXydR8` 
>
> Thanks for supporting the project! ❤️

---

## Contributing · Security · License · Contact

See [CONTRIBUTING.md](CONTRIBUTING.md) before proposing workflow, platform, or policy changes. Please follow the [Code of Conduct](CODE_OF_CONDUCT.md).

Do not disclose vulnerabilities or confidential reports in a public issue. Follow [SECURITY.md](SECURITY.md) for private reporting guidance.

Free Triager is available under the [MIT License](LICENSE).

[![Website](https://img.shields.io/badge/Website-FF5722?style=for-the-badge&logo=googlechrome&logoColor=white)](https://hsnmughal.surge.sh/) [![X](https://img.shields.io/badge/@hsnmughal__-000000?style=for-the-badge&logo=x&logoColor=white)](https://x.com/hsnmughal_) ![Discord](https://img.shields.io/badge/Discord-hsnmughal-5865F2?style=for-the-badge&logo=discord&logoColor=white)

## SECURITY.md

# Security Policy

## Reporting a Security Issue

Do not open a public GitHub issue containing an undisclosed vulnerability, private bug bounty report, proof of concept, credential, token, session ledger, or confidential platform communication.

Use GitHub's private vulnerability-reporting feature when it is available. Otherwise, contact the maintainer through one of the channels below and request a private reporting channel before sharing sensitive details:

- X: [@hsnmughal_](https://x.com/hsnmughal_)
- Discord: `hsnmughal`
- Website: [hsnmughal.surge.sh](https://hsnmughal.surge.sh/)

Include the affected version, impact, reproduction steps, and a proposed mitigation when possible. Remove unrelated secrets and third-party report data.

## Scope

Security reports may cover the skill instructions, session-state handling, accidental data disclosure, unsafe command guidance, platform-rule provenance, or another weakness in Free Triager itself.

Free Triager does not operate a bug bounty and cannot accept responsibility for reports submitted to third-party programs. Follow each program's official disclosure process for vulnerabilities in its assets.

## SKILL.md

---
name: free-triager
description: Evidence-based bug bounty report triage against live program rules, scope, known issues, exploitability, PoC requirements, and submission standards. Use when a researcher wants to reduce rejection risk before submitting or revising a vulnerability report.
---

# Free Triager

Triage a vulnerability report as a strict but constructive pre-submission reviewer. Minimize avoidable automated-triage rejection and wasted submission fees without stretching scope, severity, or technical claims.

## Required inputs

For a new run, collect these once. Do not ask again for values already supplied:

- report: file path or pasted report;
- program URL;
- workflow mode: `dynamic` or `oneshot`;
- technical-evidence/codebase path, defaulting to the current working directory when applicable. A local codebase is not mandatory for a valid black-box report.

Also accept optional user-supplied submission standards, authenticated/private policy text, team responses, or attachments. Label every source by provenance. Never claim access to private pages unless the user supplied their contents.

If the invocation supplies `resume=<absolute session directory>`, it is a dynamic resume. Do not ask for the report, program URL, mode, or evidence root again; recover them from the verified ledger.

## Entrypoint

1. Read [core/workflow.md](core/workflow.md) and [core/worker-contract.md](core/worker-contract.md).
2. If `resume=<session-dir>` is present, follow the dynamic resume protocol before doing any phase work. Use the saved platform and URL to resolve the adapter; do not initialize a second session.
3. For a new run, detect the platform from the supplied program URL. Resolve `platforms/<platform>/adapter.yaml`; v1 supports only `immunefi`. If no adapter exists, stop with `unsupported_platform` and identify the missing adapter rather than borrowing another platform's rules.
4. Branch on mode before any persistence action. In `oneshot`, do not initialize a session, invoke `scripts/session.mjs`, create `.free-triager`, copy inputs, or write checkpoints. In `dynamic`, initialize a session using `scripts/session.mjs` when Node is available. Otherwise implement the same append-only schema and transition checks with the harness's filesystem tools. Store dynamic runtime state under `<workspace-or-evidence-root>/.free-triager/runs/<session-id>/`.
5. The orchestrator performs coordination only. Delegate each phase to an isolated worker when the harness supports workers/subagents. If isolated workers are unavailable, execute the phase logic serially using one compact in-memory state; do not simulate workers with separate long transcripts or repeat shared inputs.
6. In dynamic mode, the orchestrator alone appends validated worker results directly to `session.jsonl`. Workers never write the canonical ledger or intermediate payload files.

## Reference routing

All supporting instructions are linked directly from this entrypoint. Read only the references required for the current platform, phase, and technical classification.

### Phase workers

- `prior_art`: [workers/01-prior-art.md](workers/01-prior-art.md)
- `program_policy`: [workers/02-program-policy.md](workers/02-program-policy.md)
- `eligibility_gate`: [workers/03-eligibility-gate.md](workers/03-eligibility-gate.md)
- technology-neutral classifier used by technical validation: [workers/04-target-classifier.md](workers/04-target-classifier.md)
- `technical_validation`: [workers/04-technical-validation.md](workers/04-technical-validation.md)
- `verdict_and_improvement`: [workers/05-verdict-and-improvement.md](workers/05-verdict-and-improvement.md)
- optional, explicitly authorized `report_revision`: [workers/06-report-revision.md](workers/06-report-revision.md)

### Technical profiles

After classification, read only the applicable profile or profiles:

- smart contracts: [core/technical-profiles/smart-contract.md](core/technical-profiles/smart-contract.md)
- web and applications: [core/technical-profiles/web-app.md](core/technical-profiles/web-app.md)
- Blockchain/DLT: [core/technical-profiles/blockchain-dlt.md](core/technical-profiles/blockchain-dlt.md)
- cross-category findings: [core/technical-profiles/mixed.md](core/technical-profiles/mixed.md), in addition to each applicable category profile

### Immunefi adapter references

When the active adapter is Immunefi, use these references only in the named phases:

- adapter interpretation: [platforms/immunefi/triage-rules.md](platforms/immunefi/triage-rules.md) during program research and policy extraction
- PoC requirements: [platforms/immunefi/poc-guidelines.md](platforms/immunefi/poc-guidelines.md) during policy extraction, technical validation, and an authorized revision
- report fields and structure: [platforms/immunefi/report-template.md](platforms/immunefi/report-template.md) during verdict/improvement and an authorized revision
- observed automated-triage checks: [platforms/immunefi/automated-triage-readiness.md](platforms/immunefi/automated-triage-readiness.md) during verdict/improvement and an authorized revision

For dynamic sessions, enforce [schemas/session-event.schema.json](schemas/session-event.schema.json) through the session helper or an equivalent implementation when Node is unavailable.

## Modes

- `dynamic`: create or resume persistent state, complete exactly one pending phase, append one checkpoint, print the resume handoff, and stop. A fresh chat must be able to continue from disk alone. On every `resume=<session-dir>` invocation, re-open the saved program URL before running the pending phase.
- `oneshot`: run phases sequentially using only the current invocation's context. Create nothing on disk: no session, copied report, checkpoint, worker-output file, cache, or ledger. Fetch the program page at the start and reuse that evidence across phases. Stop immediately on a terminal rejection gate, unresolved required input, or unsafe/unsupported action.

Both modes use the same state machine and evidence requirements. Mode changes pacing, not judgment.

## Non-negotiable judgment rules

- In dynamic mode, re-open the saved program URL at initialization and at the start of every resumed invocation, before the pending phase. Treat checkpointed extracts as routing context, not fresh authority. In oneshot mode, read it once at the start and retain that fresh evidence in the current context; do not repeatedly fetch the same page unless access failed or a newly discovered linked source must be opened.
- Apply rules in this order: explicit program-specific terms; the severity system/version selected by that program; platform-wide defaults only where the program is silent.
- Check known issues before spending tokens on deep validation. Match root cause and material impact, not keywords alone.
- Never mark an issue known without a source and a concrete equivalence explanation.
- Keep automated-triage readiness separate from human merits. A bot-risk warning must not silently invalidate a technically sound report.
- Distinguish an ineligible disclosed known issue from an earlier-report match. Apply duplicate consequences only when the program or competition rules make that match disqualifying.
- Never invent scope, impact, PoC, anti-bot, traffic, disclosure, or submission rules.
- Determine the affected category and technology from the report, program, and evidence. Support smart contracts, web/apps, Blockchain/DLT, and mixed paths without assuming a language, framework, virtual machine, client architecture, or source-availability model.
- Distinguish `rejected` from `needs_information`. Missing evidence is not proof that a claim is false.
- Validate the strongest realistic version of the report, but do not repair an impossible exploit path by introducing unreported assumptions.
- Severity follows the demonstrated in-scope impact and applicable program rubric.
- Do not blend impact severity and exploit likelihood into a lower severity unless the applicable rubric explicitly requires that combination. Report likelihood separately.
- Do not help conceal AI assistance or evade automation detection. Surface the applicable automation policy and require the researcher to review, verify, and take responsibility for the submission.
- Do not modify the submitted report unless the user explicitly requests revision after seeing the triage result. Write a revised copy by default; preserve the original.

## Completion

Return the current verdict, decisive evidence, unresolved items, and exact next action. Do not report or create a session directory in oneshot mode. In dynamic mode always include:

```text
CHECKPOINT_SAVED: <phase> | session=<absolute session directory>
RESUME: invoke $free-triager with resume=<absolute session directory>
```

## VERSION

```

```

## agents

```

```

## agents/openai.yaml

```yaml
interface:
  display_name: "Free Triager"
  short_description: "Pre-submission bug bounty report triage"
  default_prompt: "Use $free-triager to triage my vulnerability report against the live program rules and reduce rejection risk."

policy:
  allow_implicit_invocation: true
```

## core

```

```

## core/technical-profiles

```

```

## core/technical-profiles/blockchain-dlt.md

# Technical Profile: Blockchain or DLT

Apply when `target_classification.required_profiles` includes `blockchain_dlt`. Derive protocol, client implementation, language, topology, version, configuration, and tooling from evidence; do not assume a particular chain design.

Validate the decisive path across the relevant layers:

- transaction/block/state-transition validity and deterministic execution;
- consensus, fork choice, finality, liveness, safety, and validator assumptions;
- peer-to-peer propagation, peer scoring, eclipse/partition conditions, and resource exhaustion;
- mempool, RPC, synchronization, state/storage, cryptographic verification, and upgrade boundaries;
- attacker resources, node roles, topology, timing, affected network fraction, persistence, and recovery;
- reproducibility on a local network, simulator, fixture, or other program-permitted environment;
- PoC end effect against the program's Blockchain/DLT-specific requirement.

Inspect only the layers implicated by the claim. If a multi-node or specialized environment is unavailable, separate static support from unexecuted network behavior and state precisely what remains unproven.

## core/technical-profiles/mixed.md

# Technical Profile: Mixed Boundary

Apply only when multiple target categories are materially involved. Read each selected category profile; do not load unrelated profiles.

Validate each category's local claim, then validate the boundary between them:

- representation and identity of data, value, messages, permissions, and users across components;
- ordering, replay, confirmation/finality, timeout, retry, and failure semantics;
- which component contains the root cause versus where the end impact appears;
- whether the complete end-to-end path is reproducible under one coherent set of assumptions;
- which program category and impact rule governs each material impact.

Do not duplicate the same evidence under multiple categories or inflate severity by stacking impacts. Identify the primary root-cause category and separately record supporting categories and cross-boundary effects.

## core/technical-profiles/smart-contract.md

# Technical Profile: Smart Contract

Apply when `target_classification.required_profiles` includes `smart_contract`. Derive chain, execution model, language, framework, and tooling from evidence; do not assume EVM or Solidity.

Validate the decisive path across:

- deployed/source version correspondence and relevant configuration;
- entry points, caller capabilities, authorization, and privileged preconditions;
- state/storage transitions, ordering, atomicity, and cross-component calls;
- value/accounting invariants, asset movement, recovery paths, and repeatability;
- upgrade, governance, oracle, bridge, or external-system assumptions only when material;
- PoC end effect against the program's category-specific requirement.

Choose analysis and execution tools from the derived technology context. If suitable tooling is unavailable, perform bounded static validation and record the limitation; never reinterpret tool absence as a failed PoC.

## core/technical-profiles/web-app.md

# Technical Profile: Web or App

Apply when `target_classification.required_profiles` includes `web_app`. Support `source_assisted`, `black_box`, and `hybrid` evidence without assuming a particular frontend, backend, API, or mobile stack.

Validate the decisive path across:

- exact authorized test target, environment, version, account roles, and asset ownership;
- request/action sequence, inputs, responses, state changes, and reproducibility;
- authentication, authorization, session, identity, tenant, and privilege boundaries when relevant;
- browser, mobile, API, backend, storage, upload, rendering, or integration behavior implicated by evidence;
- required victim interaction, attacker prerequisites, rate/traffic constraints, and realistic end effect;
- sensitive-data classification, persistence, scope, and affected users or funds;
- PoC form and end effect required by the program.

Never test production destructively, access unrelated user data, exceed authorized traffic, or infer vulnerability from scanner output alone. For black-box reports, validate from permitted request/response evidence and reproducible behavior rather than demanding source code.

## core/worker-contract.md

# Worker Contract

Use this contract for every delegated phase, regardless of harness.

## Inputs

A worker receives only:

- phase name and phase prompt;
- platform, workflow mode, and workspace/evidence root;
- session ID only in dynamic mode;
- report source;
- program URL;
- latest fresh page retrieval for this invocation;
- prerequisite checkpoint records explicitly named by the orchestrator;
- active platform adapter and relevant user-supplied guidance;

Do not depend on conversation history or load unrelated phase outputs.

In `oneshot`, the orchestrator passes only the compact predecessor results needed by the phase and retains returned results in memory. In `dynamic`, the worker receives the checkpoint records needed for the pending phase after the orchestrator has freshly re-opened the saved program URL.

## Processing rules

1. Stay within the assigned phase.
2. Separate facts, inferences, and unresolved questions.
3. Attach provenance to every external rule or known-issue claim.
4. Quote minimally; prefer precise paraphrases with URLs and retrieval timestamps.
5. Make code claims only after reading the cited implementation and necessary dependencies.
6. Record contradictory evidence rather than hiding it.
7. Use `needs_information` when the missing fact could reverse the decision.
8. Do not write files or the canonical session ledger. Return the result directly to the orchestrator.
9. Do not echo the report, source pages, adapter, or prerequisite checkpoints. Refer to them by stable evidence IDs, URLs, and code locations.
10. Emit no progress transcript or methodology recap. Keep `summary`, facts, and inferences to decision-bearing information needed by a later phase or the final verdict.
11. In oneshot mode, use the supplied evidence map and do not re-open the saved program URL. In dynamic mode, include the exact saved program URL in `sources` with the current invocation's retrieval timestamp; a linked page or generic platform URL does not satisfy this freshness record.

## Output contract

Return one JSON object:

```json
{
  "phase": "program_policy",
  "summary": "short factual summary",
  "decision": "continue",
  "facts": [],
  "inferences": [],
  "unresolved": [],
  "sources": [
    {
      "url": "https://example.invalid",
      "retrieved_at": "RFC3339 timestamp",
      "source_type": "program|platform|linked_issue|audit|user_supplied_private"
    }
  ],
  "result": {}
}
```

`decision` must be one of `continue`, `terminal`, or `needs_information`. Phase-specific required fields belong inside `result`.

## Completion

Return exactly the JSON object as the worker's final output. The orchestrator validates it in memory. In dynamic mode, it appends that object directly to `session.jsonl`; in oneshot mode, it keeps only the compact result required by later phases in the current context. The orchestrator retries malformed output once with the validation error. A second malformed result stops with `error`.

## core/workflow.md

# Orchestrator Workflow

## Table of Contents

- [Purpose](#purpose)
- [Intake and mode selection](#intake-and-mode-selection)
- [Oneshot protocol: memory only](#oneshot-protocol-memory-only)
- [Dynamic initialization](#dynamic-initialization)
- [Dynamic resume protocol](#dynamic-resume-protocol)
- [State machine](#state-machine)
- [Phase dispatch](#phase-dispatch)
  - [Prior art](#1-prior-art)
  - [Program policy](#2-program-policy)
  - [Eligibility gate](#3-eligibility-gate)
  - [Technical validation](#4-technical-validation)
  - [Verdict and improvement](#5-verdict-and-improvement)
- [Final verdict vocabulary](#final-verdict-vocabulary)

## Purpose

Determine whether a report is likely to be accepted under the named program's current rules, and identify the smallest truthful changes that reduce rejection risk.

## Intake and mode selection

If `resume=<session-dir>` is present, skip new-run intake and go directly to the dynamic resume protocol. Otherwise collect only missing required inputs:

1. report path or report text;
2. program URL;
3. workflow mode: `dynamic` or `oneshot`;
4. technical-evidence/codebase path, default current directory when applicable; source is optional for black-box evidence.

Optional inputs include private submission guidance, anti-bot/traffic policy text, team responses, and report attachments. Treat user-supplied material as `source_type: user_supplied_private`; do not browse for or infer inaccessible content.

Resolve the mode before creating files or invoking the session helper.

## Oneshot protocol: memory only

`oneshot` has a hard no-persistence invariant for triage. Do not create `.free-triager`, initialize a session, copy the report, write JSONL, create worker payloads, cache web pages, or emit any other triage artifact. Read a report path in place or consume pasted text directly.

1. Open the program URL once at the start of the invocation. Follow relevant public links needed for known issues, audits, scope, rules, or standards.
2. Retain a compact evidence map in current context: source URL, retrieval timestamp, provenance, and the exact facts needed by the five phases.
3. Run the five phases sequentially. Pass each isolated worker only the report, relevant evidence map entries, codebase material, adapter, and prerequisite compact phase results. If isolated workers are unavailable, update one compact in-memory state object in place and emit no intermediate phase transcripts.
4. Stop immediately on a terminal outcome or `needs_information`; otherwise continue through the verdict.
5. Return the result directly. Do not include a session or resume flag.

Do not repeatedly re-open the same program page between oneshot phases. Re-open only if the initial fetch failed, the page changed during the run, or a phase identifies a new linked source that must be inspected.

## Dynamic initialization

`dynamic` is the only persistent mode. If `resume=<session-dir>` is absent, initialize with:

```text
node <skill>/scripts/session.mjs init \
  --root <absolute workspace or evidence root> \
  --platform immunefi \
  --mode dynamic \
  --program-url <url> \
  --report <report-path>
```

For pasted reports, materialize the report only as part of dynamic session initialization. The session directory may contain confidential material and must remain ignored by version control.

Without Node, create the same dynamic-only directory structure and `init` event defined by the session-event schema routed from the entrypoint. Writes must be atomic, sequence numbers contiguous, and phase transitions identical to the session helper.

After initialization, open the saved program URL, run only `prior_art`, append its checkpoint, print the resume flag, and halt.

## Dynamic resume protocol

The public resume interface is:

```text
$free-triager resume=<absolute session directory>
```

On every resumed invocation, in this order:

1. Run `session.mjs resume --session <session-dir>`; this verifies the ledger and returns the saved inputs, pending phase, and `fresh_program_read_required: true`.
2. If the session is closed, report its terminal state and stop.
3. Re-open the saved program URL before doing phase work. Record the fresh retrieval timestamp and URLs consulted. This is mandatory even if a checkpoint contains extracts from the same page.
4. Read only the checkpoint records required by the pending phase. Never reconstruct dynamic state from conversation memory.
5. Run exactly the returned pending phase.
6. Validate the worker's returned JSON object. For every triage phase, its `sources` must contain the saved program URL with a `retrieved_at` timestamp at or after the preceding checkpoint; the session helper rejects stale or different-page evidence. Append the object directly through standard input:

```text
node <skill>/scripts/session.mjs append \
  --session <session-dir> \
  --phase <phase> \
  --status <complete|terminal|needs_information|error>
```

7. Print `CHECKPOINT_SAVED` and the exact `resume=<session-dir>` flag, then halt. Never begin another phase in the same invocation.

If the live page is unavailable, do not silently rely on a stale extract. Append `needs_information` with the access failure, ask for an exported page or pasted contents, and halt.

## State machine

```text
init
  -> prior_art
  -> program_policy
  -> eligibility_gate
  -> technical_validation
  -> verdict_and_improvement
  -> complete
```

Terminal outcomes may stop either mode early:

- `known_issue`;
- `out_of_scope`;
- `rules_violation`;
- `invalid_claim`;
- `unsupported_platform`.

Use `needs_information` for missing or ambiguous evidence that could materially change the outcome. This is resumable in dynamic mode; it simply stops the current invocation in oneshot mode.

## Phase dispatch

### 1. Prior art

Use the prior-art worker instructions routed from the entrypoint. Inspect the program's disclosed known issues, linked issue trackers, prior audits, disclosures, and documented mitigations. Preserve any project explanation for leaving an issue unfixed.

Required result fields:

- known issue registry with title, root cause, affected component, impact, disposition, and source;
- audit/disclosure registry;
- access gaps;
- comparison hooks for the submitted report.

### 2. Program policy

Use the program-policy worker instructions and active platform adapter routed from the entrypoint. Extract assets, impacts, exclusions, selected severity system, PoC requirements, primacy rules when present, reward constraints relevant to eligibility, and other submission-affecting terms. Preserve category-specific rules for smart contracts, web/apps, and Blockchain/DLT rather than merging them.

Every extracted rule must carry source URL, retrieval timestamp, exact or tightly paraphrased rule text, and whether it is program-specific, platform-default, or user-supplied. Absence of a Primacy of Rules or Primacy of Impact clause is a recorded absence, not a missing-input error; apply ordinary asset and impact scope instead.

### 3. Eligibility gate

Use the eligibility-gate worker instructions routed from the entrypoint. Apply the gates across every report category plausibly implicated by the program asset and claimed impact; do not force an ambiguous or mixed report into one category merely to reject it. Apply gates in this exact order:

1. Known issue or duplicate classification.
2. Asset scope.
3. Program-specific/Primacy of Rules compliance, if such rules exist.
4. Impact scope.
5. Primacy of Impact escape path only when explicitly enabled and the asset is out of scope.

Decision branch:

```text
program-disclosed issue explicitly ineligible -> known_issue (terminal)
earlier-report match -> apply the program's duplicate rule; otherwise informational
asset in scope
  -> applicable rules satisfied?
     -> impact in scope? -> technical_validation
asset out of scope
  -> claimed severity/impact explicitly covered by Primacy of Impact?
     -> affected asset belongs to the program/project and no stated exception applies?
        -> technical_validation
otherwise -> out_of_scope or rules_violation (terminal)
```

Do not treat Primacy of Impact as universal. Use only the severities and impact classes explicitly enabled by the program, and apply stated exceptions such as test/mock assets or separate programs.

### 4. Technical validation

Use the technical-validation worker instructions routed from the entrypoint. It first runs the technology-neutral target classifier, then loads only the required smart-contract, web/app, Blockchain/DLT, or mixed technical profiles. Validate citations, execution path, preconditions, violated invariant, exploitability, claimed impact, and PoC against the available evidence and the program's category-specific requirements.

```text
shared policy + eligibility
          -> target classifier
             -> smart_contract profile
             -> web_app profile
             -> blockchain_dlt profile
             -> selected profiles + boundary validation for mixed findings
          -> shared verdict
```

Classify each material claim as `proven`, `supported`, `uncertain`, or `refuted`. A broken PoC or impossible path may produce `invalid_claim`; missing evidence that is actually required to establish the claim produces `needs_information`. Source-code absence alone is not a failure for a reproducible black-box report.

### 5. Verdict and improvement

Use the verdict-and-improvement worker instructions routed from the entrypoint. Resolve validity, eligibility, severity, rejection risk, and precise report improvements using the classified target category and technical profile results. Apply public standards and any user-supplied private guidance with separate provenance.

For platforms with automated front-line triage, issue two independent assessments:

1. automated-triage readiness and likely failure points;
2. human-review technical and policy merits.

The submission recommendation combines both but never rewrites one as the other. A valid report that is bot-fragile should be `ready_with_changes`, not `invalid_claim`.

Default behavior is review-only. If the user explicitly asks for changes, run a separate `report_revision` action after the verdict and write a new file unless overwrite was explicitly requested. This authorized revision is not a triage checkpoint; in a later oneshot turn, request the original report and verdict again if they are no longer in context.

For revision, use the report-revision worker instructions routed from the entrypoint. It is outside the mandatory five-phase triage path and must never run from inferred consent.

## Final verdict vocabulary

- `ready`: eligible and technically substantiated; only editorial improvements remain.
- `ready_with_changes`: likely eligible but material report/PoC changes should be made before submission.
- `needs_information`: evidence is insufficient for a reliable decision.
- `known_issue`: materially equivalent prior issue makes rejection likely.
- `out_of_scope`: neither normal asset/impact scope nor an applicable Primacy of Impact path covers it.
- `rules_violation`: conflicts with a decisive program or platform rule.
- `invalid_claim`: the claimed exploit or impact is refuted under the stated conditions.

The skill provides a reasoned pre-submission assessment, not a guarantee of platform or project acceptance.

## platforms

```

```

## platforms/immunefi

```

```

## platforms/immunefi/adapter.yaml

```yaml
schema_version: "1.0"
platform: "immunefi"
url_hosts:
  - "immunefi.com"
program_page_sections:
  - "information"
  - "scope"
target_categories:
  - "smart_contract"
  - "web_app"
  - "blockchain_dlt"
mixed_category_policy: "Classify each material boundary and apply the program rule governing each claimed impact."
technical_profiles:
  smart_contract: "core/technical-profiles/smart-contract.md"
  web_app: "core/technical-profiles/web-app.md"
  blockchain_dlt: "core/technical-profiles/blockchain-dlt.md"
  mixed: "core/technical-profiles/mixed.md"
program_source_refresh:
  oneshot: "once_at_start_of_invocation"
  dynamic: "after_init_and_before_each_resumed_phase"
rule_precedence:
  - "program_specific"
  - "program_selected_severity_system"
  - "platform_default_when_program_silent"
known_issue_sources:
  - "program_public_disclosure"
  - "program_linked_issue_trackers"
  - "program_linked_audits_and_disclosures"
primacy_modes:
  - "primacy_of_rules"
  - "primacy_of_impact"
private_guidance_policy: "Accept only user-supplied content and preserve its provenance."
references:
  instructions: "triage-rules.md"
  poc_guidelines: "poc-guidelines.md"
  report_template: "report-template.md"
  automated_triage_readiness: "automated-triage-readiness.md"
  calibration_cases: "calibration/cases.jsonl"
```

## platforms/immunefi/automated-triage-readiness.md

# Immunefi Automated-Triage Readiness

Provenance: calibrated from a user-supplied report transcript dated 2026-08-25. The automated triager escalated the report, the project confirmed it, and the project raised severity from the submitted Medium to High. This is observed behavior from one case, not a complete or permanent Immunefi policy.

Read this file only during `verdict_and_improvement` or an authorized report revision.

## Two-gate model

Assess independently:

1. `automated_triage_readiness`: whether the submission appears likely to pass the front-line completeness, relevance, scope, and custom-rule checks;
2. `human_merits`: whether the report is technically valid, policy-eligible, and correctly classified after deeper review.

A report can be technically valid but bot-fragile. Recommend concrete changes before submission rather than misclassifying it as invalid.

## Observed front-line checks

The supplied transcript says the triager checked:

- selected impact matches the report and is in scope;
- selected asset matches the report and is in scope;
- required sections contain relevant information rather than filler or junk;
- a PoC is attached;
- the PoC contains code that appears relevant to the reported impact and attack description;
- the PoC and Bug Description contain enough detail to evaluate the code;
- client-specific program requirements are satisfied;
- out-of-scope rules are not violated;
- whether automation tools were used to create the report.

Treat these as a minimum observed checklist. Re-read the live program and current applicable platform rules for additional requirements.

## Readiness classification

- `pass`: every observed and live-program gate has evidence, and no unresolved issue is likely to trigger automatic closure.
- `at_risk`: the underlying report may be valid, but one or more presentation, PoC, asset/impact-selection, custom-rule, or automation-policy questions could block escalation.
- `fail`: a decisive gate is unsatisfied and the current submission is likely to close before human merits review.
- `unknown`: current policy or required evidence could not be obtained.

For each failed or uncertain item, state the exact report field to change and the evidence required. Do not promise escalation.

## Calibration lessons from the accepted High case

### Normal privileged operation is not necessarily privileged exploitation

The accepted path included blocklisting, rescue, un-blocklisting, and a later user withdrawal. The privileged actions were ordinary non-malicious operations; the defect poisoned state later consumed by an unprivileged user. Ask who exploits the bug and who suffers harm. Do not apply a privileged-attacker exclusion merely because an authorized role appears in the setup.

### Earlier-report matching is not always terminal

The triager matched the finding to an earlier report with the same root cause but explicitly said the match was informational and did not affect escalation. Therefore, record the platform/program's actual duplicate consequence before halting.

### Impact mapping controls severity unless a rule says otherwise

The submitted report used Medium, but the project raised it to High because the demonstrated freeze matched a High in-scope impact. Keep likelihood separate from impact severity, and do not create an undocumented combined scoring model.

### Static verification is different from execution failure

An assessing environment may lack Node, npm, Foundry, or Hardhat. If the PoC code and state transition can be inspected but not executed, report `statically_verified_not_executed`; do not say the PoC failed. Provide the exact remaining execution step and its effect on confidence.

## Automation integrity

Do not help a researcher evade an automation-policy check. Free Triager may organize, test, and critique user-provided research, but it must preserve provenance, avoid fabricated personal authorship, and require the researcher to understand and verify the final report.

## platforms/immunefi/calibration

```

```

## platforms/immunefi/calibration/cases.jsonl

```

```

## platforms/immunefi/poc-guidelines.md

# Immunefi PoC Guidelines

Provenance: concise operational extraction from a user-supplied Immunefi Help Center snapshot labeled “Updated” and supplied on 2026-09-15. Treat this as platform guidance, subordinate to the live program page and any newer official rule presented during a run.

Read this file during `program_policy` when extracting PoC requirements and during `technical_validation` when judging PoC compliance.

## Requirement gate

- Determine whether a PoC is required from the specific program page and its severity table. Do not assume the requirement applies to every program or every severity.
- Program-specific additional requirements override this baseline.
- If the project asks for verification information and the researcher refuses to provide it, treat the PoC as invalid under this snapshot.

## Smart-contract PoCs

A compliant PoC should:

- demonstrate the vulnerability and claimed impact with runnable exploit code;
- run locally against a fork of the relevant mainnet state, commonly with Foundry or Hardhat;
- avoid testing against public mainnet or public testnets;
- identify dependencies, configuration, environment variables, fork block, addresses, and exact run command;
- use comments or output statements to show each attack step and relevant before/after state, including funds stolen, frozen, or otherwise affected;
- be complete rather than a partial sketch, pseudocode, screenshot of code, or prose-only call sequence;
- include an evidence-based funds-at-risk calculation when relevant.

The supplied sources say both that unit-test PoCs are not accepted and that a Hardhat/Foundry test file can be a valid attack script. Apply the narrow reconciliation: a test-shaped file is acceptable when it executes a runnable exploit against a local mainnet fork; a mocked-only unit test that merely asserts isolated project logic is not sufficient. If the live program or current platform rule clarifies this differently, follow that rule and record the conflict.

For a DoS demonstration, require prior permission from the project through the dashboard. Never run a DoS merely to validate a report.

## Blockchain/DLT PoCs

Apply the live program's Blockchain/DLT requirement without forcing smart-contract fork tooling onto node or protocol findings. A reproducible demonstration should identify the affected client/version, configuration and roles, required topology or timing, exact messages/transactions/actions, run commands, and observable logs or state showing the claimed network effect. Use a local network, simulator, fixture, or other program-permitted environment; never test disruptive behavior on a public network.

When the required multi-node or specialized environment is unavailable, distinguish static support from unexecuted behavior and identify the exact evidence still needed. Tool or infrastructure unavailability is not itself a failed PoC.

## Web/app PoCs

Acceptable forms under the supplied snapshot may include:

- a minimally invasive video with a brief explanation and the corresponding HTTP request;
- screenshots demonstrating the final result;
- HTTP requests;
- command-line or other runnable code;
- minimally invasive proof of a subdomain or external-link takeover.

Black-box testing must not take down the application. Obtain project permission first if a proposed demonstration may do so.

## Safety rules

- Use harmless JavaScript payloads that do not affect other users or disrupt the application.
- Make only the smallest necessary website edit; prefer non-visible proof such as an HTML comment.
- Never upload a webshell. Use an empty or non-executable text file when upload behavior must be demonstrated.
- Do not access sensitive information, change state, or disclose the vulnerability beyond what is necessary to prove and submit it.
- Do not execute untrusted report code before inspecting it, and never use Free Triager to test live targets.

## Compliance output

For each applicable item, record `pass`, `fail`, `not_applicable`, or `unknown`, with evidence. A missing required artifact should normally produce `needs_information`; an explicitly noncompliant or non-runnable submitted PoC may support `ready_with_changes` or rejection depending on the live program rule.

## platforms/immunefi/report-template.md

# Immunefi Bug Report Template

Provenance: concise operational extraction from a user-supplied Immunefi Help Center snapshot labeled “Updated” and supplied on 2026-09-15. Treat this as submission-format guidance, subordinate to the live program page and current dashboard fields.

Read this file during `verdict_and_improvement` and the optional `report_revision` action.

## Dashboard requirements

- Fill every applicable dashboard section instead of placing the whole report in Bug Description.
- Put PoC material in the dedicated Proof of Concept field when the dashboard provides one.
- Select an impact from the program's listed in-scope impacts; do not submit a custom impact.
- Select an asset from the program's listed assets, or the program's explicit Primacy of Impact placeholder when that path applies; do not invent a custom asset.
- Use Markdown for readable structure. Title and reference screenshots clearly.
- For a hosted PoC video, provide access instructions, including a password when needed.

## Required report structure

### Title

Name the vulnerability class or root cause, the affected component/interface, and the concrete impact. Avoid generic titles such as “vulnerability in contract/app/node.”

### Bug Description

Describe the vulnerability and its impact clearly, accurately, and without unnecessary assumptions.

### Brief/Intro

Use one short paragraph stating the defect and the consequence if exploited.

### Details

Explain the exact root cause, affected component, preconditions, execution path, violated invariant, and why existing controls do not prevent the issue. Include focused source, request, trace, configuration, protocol, or other evidence appropriate to the target without overcrowding the report.

### Impact

Map the demonstrated outcome to the exact selected in-scope impact. Quantify affected funds or users when evidence permits. Separate current evidence from assumptions and avoid unsupported worst-case claims.

### Risk Breakdown

Explain attacker access, capital, timing, user interaction, privileges, repeatability, and other exploit constraints. Use the program-selected Immunefi severity classification rather than substituting CVSS for blockchain/DLT findings.

### Recommendation

Suggest a concrete fix or mitigation that addresses the root cause. Keep it technically feasible and distinguish immediate mitigation from a complete fix.

### References

Link the relevant source files, contracts, endpoints, applications, clients, deployments, documentation, transactions, traces, and supporting material. Keep the dedicated PoC field separate when the dashboard requires it.

### Proof of Concept

Provide the applicable evidence required by the PoC-guideline reference routed from the entrypoint and the live program. A prose sequence, pseudocode, project source copied without an exploit, or an empty PoC field is insufficient when runnable proof is required.

## Rejection-risk checklist

Before marking a report `ready`, verify:

- selected asset and impact exactly match the program options;
- title communicates root cause and impact;
- technical details are reproducible and grounded in the strongest evidence available for the target;
- impact is concrete and not merely asserted;
- exploit difficulty and assumptions are disclosed;
- recommendation addresses the root cause;
- references resolve;
- required PoC is runnable, complete, safe, and placed in the correct field;
- no required dashboard section is empty.

## Automation-policy check

Observed Immunefi triage text may include a check for reports created with automation tools. Before submission, extract the current applicable rule and tell the researcher about any restriction or disclosure obligation. Never offer stylistic obfuscation, detector evasion, or false claims of authorship. Require human review of every technical claim and the final submission.

## platforms/immunefi/triage-rules.md

# Immunefi Adapter Rules

This adapter defines how to read Immunefi programs; it does not freeze a global policy snapshot. Program pages and severity systems change, so every run must use live program-specific evidence.

## Source collection

Open the supplied program URL and inspect both information and scope views when available. Follow program-linked known-issue trackers, audit reports, disclosures, and the severity-classification version selected by that program.

Useful public starting points, subordinate to the program page:

- `https://immunefi.com/severity-classification-systems/`
- the exact severity-system URL named by the program;
- program-linked Help Center or rules pages.

Do not automatically apply the newest classification system. Apply the version the program uses.

## Primacy handling

Primacy of Impact can allow an impact to qualify even when the particular asset is not listed, but only for the severities/impacts explicitly named by the program and subject to its stated ownership and exclusion conditions. All remaining cases stay under the program's normal rules.

Record:

- enabled impact/severity classes;
- asset ownership requirement;
- separate-program limitation;
- testnet/mock or other exceptions;
- submission placeholder/instructions when stated.

## Known issues

Program-disclosed known issues may include bugs the project knows about but has not fixed, necessary future changes, or operational mitigations. Preserve the project's stated rationale. A report is equivalent only when its root cause and material impact substantially match, or when the disclosed mitigation explicitly covers the reported path.

## PoC and submission rules

Read the exact program requirement. Some programs require executable code or a particular demonstration; do not substitute a generic remembered guideline. User-provided private PoC, anti-bot, or traffic guidance may be applied only as `user_supplied_private` evidence.

Use the platform-level PoC checklist routed from the entrypoint only during policy extraction and technical PoC validation. Use the report-template reference only during verdict/improvement or an authorized revision.

Use the observed front-line triage reference routed from the entrypoint only during final readiness assessment or report revision. Do not treat a single observed transcript as universal policy.

## Severity

Map severity from the demonstrated impact using the program-selected classification. Preconditions, privileges, uncommon interaction, feasibility, and program exclusions may affect eligibility or severity only when supported by applicable rules and technical evidence.

Keep mapped impact severity and likelihood separate. Do not invent a combined severity model when the program table maps the demonstrated impact directly.

## schemas

```

```

## schemas/session-event.schema.json

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://local.free-triager/session-event.schema.json",
  "title": "Free Triager session event",
  "type": "object",
  "required": [
    "schema_version",
    "seq",
    "timestamp",
    "session_id",
    "platform",
    "mode",
    "phase",
    "status",
    "payload"
  ],
  "properties": {
    "schema_version": { "const": "1.0" },
    "seq": { "type": "integer", "minimum": 1 },
    "timestamp": { "type": "string", "format": "date-time" },
    "session_id": { "type": "string", "minLength": 1 },
    "platform": { "type": "string", "minLength": 1 },
    "mode": {
      "const": "dynamic",
      "description": "Only dynamic runs may have persisted session events."
    },
    "phase": {
      "enum": [
        "init",
        "prior_art",
        "program_policy",
        "eligibility_gate",
        "technical_validation",
        "verdict_and_improvement",
        "report_revision"
      ]
    },
    "status": {
      "enum": ["complete", "terminal", "needs_information", "error"]
    },
    "payload": { "type": "object" }
  },
  "additionalProperties": false
}
```

## scripts

```

```

## scripts/session.mjs

```js
#!/usr/bin/env node

import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { fileURLToPath } from "node:url";

const PHASES = [
  "prior_art",
  "program_policy",
  "eligibility_gate",
  "technical_validation",
  "verdict_and_improvement",
];
const STATUSES = new Set(["complete", "terminal", "needs_information", "error"]);
const DECISIONS = new Set(["continue", "terminal", "needs_information"]);
const SKILL_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const REQUIRED_RESULTS = {
  prior_art: ["known_issues", "prior_submissions_or_duplicates", "audits_and_disclosures", "access_gaps", "comparison_hooks"],
  program_policy: ["assets", "impacts", "exclusions", "primacy", "severity_system", "poc_requirements", "automation_policy", "submission_rules", "category_rule_matrix"],
  eligibility_gate: ["gates", "known_issue_classification", "applicable_scope_path", "viable_category_paths", "disposition"],
  technical_validation: ["target_classification", "profile_results", "claim_matrix", "exploit_path", "violated_invariant", "privileged_precondition_analysis", "poc_validation", "poc_execution_status", "poc_compliance_checklist", "steelman", "skeptic", "technical_verdict"],
  verdict_and_improvement: ["target_category", "profile_summary", "verdict", "severity", "severity_basis", "likelihood", "automated_triage_readiness", "human_merits", "submission_recommendation", "fee_risk", "rejection_risks", "improvements", "report_outline", "template_compliance_checklist", "confidence"],
  report_revision: ["original_path", "revised_path", "change_log", "remaining_risks"],
};

function fail(message, code = 2) {
  console.error(`ERROR: ${message}`);
  process.exit(code);
}

function parseArgs(argv) {
  const [command, ...rest] = argv;
  const values = {};
  for (let i = 0; i < rest.length; i += 1) {
    const token = rest[i];
    if (!token.startsWith("--")) fail(`unexpected argument: ${token}`);
    const key = token.slice(2);
    const value = rest[i + 1];
    if (!value || value.startsWith("--")) fail(`missing value for --${key}`);
    values[key] = value;
    i += 1;
  }
  return { command, values };
}

function required(values, names) {
  for (const name of names) {
    if (!values[name]) fail(`missing --${name}`);
  }
}

function readEvents(sessionDir) {
  const ledger = path.join(sessionDir, "session.jsonl");
  if (!fs.existsSync(ledger)) fail(`session ledger not found: ${ledger}`);
  const lines = fs.readFileSync(ledger, "utf8").split(/\r?\n/).filter(Boolean);
  return lines.map((line, index) => {
    try {
      return JSON.parse(line);
    } catch (error) {
      fail(`invalid JSONL at line ${index + 1}: ${error.message}`);
    }
  });
}

function writeAtomic(file, content) {
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
  fs.writeFileSync(tmp, content, { encoding: "utf8", flag: "wx" });
  fs.renameSync(tmp, file);
}

function appendAtomic(sessionDir, event) {
  const ledger = path.join(sessionDir, "session.jsonl");
  const existing = fs.existsSync(ledger) ? fs.readFileSync(ledger, "utf8") : "";
  const prefix = existing && !existing.endsWith("\n") ? `${existing}\n` : existing;
  writeAtomic(ledger, `${prefix}${JSON.stringify(event)}\n`);
}

function safeId(value) {
  return value.replace(/[^a-zA-Z0-9._-]/g, "-");
}

function digestFile(file) {
  return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
}

function platformAdapter(platform) {
  const safePlatform = safeId(platform);
  if (safePlatform !== platform) fail(`invalid platform id: ${platform}`);
  const adapter = path.join(SKILL_ROOT, "platforms", platform, "adapter.yaml");
  if (!fs.existsSync(adapter)) fail(`unsupported platform; adapter not found: ${adapter}`);
  return adapter;
}

function validatePayload(payload, phase, status) {
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return ["payload must be a JSON object"];
  const errors = [];
  for (const field of ["phase", "summary", "decision", "facts", "inferences", "unresolved", "sources", "result"]) {
    if (!(field in payload)) errors.push(`missing payload field: ${field}`);
  }
  if (payload.phase !== phase) errors.push("payload phase does not match --phase");
  if (!DECISIONS.has(payload.decision)) errors.push(`invalid payload decision: ${payload.decision}`);
  for (const field of ["facts", "inferences", "unresolved", "sources"]) {
    if (!Array.isArray(payload[field])) errors.push(`payload ${field} must be an array`);
  }
  if (Array.isArray(payload.sources) && payload.sources.length === 0) errors.push("payload sources must include the fresh program-page read");
  if (Array.isArray(payload.sources)) {
    payload.sources.forEach((source, index) => {
      if (!source || typeof source !== "object") errors.push(`payload source ${index} must be an object`);
      else for (const field of ["url", "retrieved_at", "source_type"]) if (!source[field]) errors.push(`payload source ${index} missing ${field}`);
    });
  }
  if (!payload.result || typeof payload.result !== "object" || Array.isArray(payload.result)) errors.push("payload result must be an object");
  if (payload.result && typeof payload.result === "object") {
    for (const field of REQUIRED_RESULTS[phase] ?? []) {
      if (!(field in payload.result)) errors.push(`missing phase result field: ${field}`);
    }
    if (phase === "technical_validation") {
      const classification = payload.result.target_classification;
      const categories = new Set(["smart_contract", "web_app", "blockchain_dlt"]);
      if (!classification || typeof classification !== "object" || Array.isArray(classification)) {
        errors.push("target_classification must be an object");
      } else {
        for (const field of ["primary_category", "applicable_categories", "evidence_mode", "system_boundary", "components", "interfaces", "actors_and_trust_boundaries", "technology_context", "required_profiles", "classification_evidence", "uncertainties", "confidence"]) {
          if (!(field in classification)) errors.push(`target_classification missing ${field}`);
        }
        for (const field of ["components", "interfaces", "actors_and_trust_boundaries", "classification_evidence", "uncertainties"]) {
          if (!Array.isArray(classification[field])) errors.push(`target_classification ${field} must be an array`);
        }
        for (const field of ["system_boundary", "technology_context"]) {
          if (!classification[field] || typeof classification[field] !== "object" || Array.isArray(classification[field])) {
            errors.push(`target_classification ${field} must be an object`);
          }
        }
        if (![...categories, "mixed"].includes(classification.primary_category)) errors.push("invalid target_classification primary_category");
        if (!["source_assisted", "black_box", "hybrid"].includes(classification.evidence_mode)) errors.push("invalid target_classification evidence_mode");
        if (!Array.isArray(classification.applicable_categories) || classification.applicable_categories.length === 0) {
          errors.push("target_classification applicable_categories must be a non-empty array");
        } else if (classification.applicable_categories.some((category) => !categories.has(category))) {
          errors.push("target_classification contains an unknown applicable category");
        }
        if (!Array.isArray(classification.required_profiles) || classification.required_profiles.length === 0) {
          errors.push("target_classification required_profiles must be a non-empty array");
        } else if (classification.required_profiles.some((profile) => !categories.has(profile))) {
          errors.push("target_classification contains an unknown required profile");
        }
        if (Array.isArray(classification.applicable_categories) && classification.applicable_categories.length > 1 && classification.primary_category !== "mixed") {
          errors.push("multiple applicable categories require primary_category=mixed");
        }
        if (Array.isArray(classification.applicable_categories) && Array.isArray(classification.required_profiles)) {
          const applicable = [...new Set(classification.applicable_categories)].sort();
          const requiredProfiles = [...new Set(classification.required_profiles)].sort();
          if (JSON.stringify(applicable) !== JSON.stringify(requiredProfiles)) errors.push("required_profiles must cover exactly the applicable categories");
        }
        if (Array.isArray(classification.required_profiles) && classification.required_profiles.length > 0) {
          const profileResults = payload.result.profile_results;
          if (!profileResults || typeof profileResults !== "object" || Array.isArray(profileResults)) {
            errors.push("profile_results must be an object");
          } else {
            for (const profile of classification.required_profiles) {
              if (!(profile in profileResults)) errors.push(`profile_results missing selected profile: ${profile}`);
            }
          }
        }
      }
    }
  }
  if (status === "complete" && payload.decision !== "continue") errors.push("complete status requires decision=continue");
  if (status === "terminal" && payload.decision !== "terminal") errors.push("terminal status requires decision=terminal");
  if (status === "needs_information" && payload.decision !== "needs_information") errors.push("needs_information status requires matching decision");
  return errors;
}

function normalizeUrl(value) {
  try {
    const parsed = new URL(value);
    parsed.hash = "";
    parsed.pathname = parsed.pathname.replace(/\/+$/, "") || "/";
    return parsed.toString();
  } catch {
    return null;
  }
}

function validateFreshProgramRead(payload, initEvent, latestEvent, phase) {
  if (phase === "report_revision" || !Array.isArray(payload.sources)) return [];
  const expectedUrl = normalizeUrl(initEvent.payload.program_url);
  if (!expectedUrl) return ["saved program URL is invalid"];
  const programSources = payload.sources.filter((source) =>
    source?.source_type === "program" && normalizeUrl(source.url) === expectedUrl
  );
  if (programSources.length === 0) {
    return [`payload sources must include the saved program URL: ${initEvent.payload.program_url}`];
  }

  const checkpointTime = Date.parse(latestEvent.timestamp);
  const latestAllowed = Date.now() + 5 * 60 * 1000;
  const hasFreshRead = programSources.some((source) => {
    const retrievedAt = Date.parse(source.retrieved_at);
    return Number.isFinite(retrievedAt) && retrievedAt >= checkpointTime && retrievedAt <= latestAllowed;
  });
  return hasFreshRead
    ? []
    : ["saved program URL must have a valid retrieved_at timestamp at or after the previous checkpoint"];
}

function nextPhase(events) {
  const completed = new Set(events.filter((e) => e.status === "complete").map((e) => e.phase));
  return PHASES.find((phase) => !completed.has(phase)) ?? "complete";
}

function validationErrors(events) {
  const errors = [];
  const first = events[0];
  if (first?.phase !== "init") errors.push("first event is not init");
  if (first?.mode !== "dynamic") errors.push("persisted session mode must be dynamic");
  if (!(first?.payload?.evidence_root || first?.payload?.codebase_root)) errors.push("init payload missing evidence_root");
  for (const field of ["program_url", "report_path"]) if (!first?.payload?.[field]) errors.push(`init payload missing ${field}`);
  for (let i = 0; i < events.length; i += 1) {
    const event = events[i];
    if (event.seq !== i + 1) errors.push(`line ${i + 1}: expected seq ${i + 1}, found ${event.seq}`);
    for (const field of ["schema_version", "timestamp", "session_id", "platform", "mode", "phase", "status", "payload"]) {
      if (!(field in event)) errors.push(`line ${i + 1}: missing ${field}`);
    }
    if (event.schema_version !== "1.0") errors.push(`line ${i + 1}: unsupported schema version`);
    if (!STATUSES.has(event.status)) errors.push(`line ${i + 1}: invalid status ${event.status}`);
    if (event.session_id !== first?.session_id) errors.push(`line ${i + 1}: session_id drift`);
    if (event.platform !== first?.platform) errors.push(`line ${i + 1}: platform drift`);
    if (event.mode !== first?.mode) errors.push(`line ${i + 1}: mode drift`);
    if (i > 0 && [...PHASES, "report_revision"].includes(event.phase) && STATUSES.has(event.status)) {
      for (const error of validatePayload(event.payload, event.phase, event.status)) {
        errors.push(`line ${i + 1}: ${error}`);
      }
      for (const error of validateFreshProgramRead(event.payload, first, events[i - 1], event.phase)) {
        errors.push(`line ${i + 1}: ${error}`);
      }
    }
  }
  let phaseIndex = 0;
  let closed = false;
  for (const event of events.slice(1)) {
    if (closed) errors.push(`event after closed session: ${event.phase}`);
    if (event.phase === "report_revision") continue;
    const expected = PHASES[phaseIndex];
    if (event.phase !== expected) errors.push(`phase order mismatch: expected ${expected}, found ${event.phase}`);
    if (event.status === "complete") phaseIndex += 1;
    if (["terminal", "error"].includes(event.status)) closed = true;
  }
  return errors;
}

function readPayload(values) {
  let raw;
  if (values.payload) {
    const payloadPath = path.resolve(values.payload);
    if (!fs.existsSync(payloadPath)) fail(`payload not found: ${payloadPath}`);
    raw = fs.readFileSync(payloadPath, "utf8");
  } else {
    raw = fs.readFileSync(0, "utf8");
    if (!raw.trim()) fail("payload JSON is required on standard input or via --payload FILE");
  }
  try {
    return JSON.parse(raw);
  } catch (error) {
    fail(`invalid payload JSON: ${error.message}`);
  }
}

function init(values) {
  required(values, ["root", "platform", "mode", "program-url", "report"]);
  if (values.mode !== "dynamic") fail("oneshot is memory-only and must not initialize a session; mode must be dynamic");
  platformAdapter(values.platform);

  const root = path.resolve(values.root);
  const report = path.resolve(values.report);
  if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) fail(`codebase root not found: ${root}`);
  if (!fs.existsSync(report) || !fs.statSync(report).isFile()) fail(`report file not found: ${report}`);

  const sessionId = safeId(values.session ?? `ft-${new Date().toISOString().replace(/[:.]/g, "-")}`);
  const workRoot = path.join(root, ".free-triager");
  const sessionDir = path.join(workRoot, "runs", sessionId);
  if (fs.existsSync(sessionDir)) fail(`session already exists: ${sessionDir}`);

  fs.mkdirSync(path.join(sessionDir, "inputs"), { recursive: true });
  fs.mkdirSync(workRoot, { recursive: true });
  const ignore = path.join(workRoot, ".gitignore");
  if (!fs.existsSync(ignore)) writeAtomic(ignore, "*\n!.gitignore\n");

  const copiedReport = path.join(sessionDir, "inputs", path.basename(report));
  fs.copyFileSync(report, copiedReport, fs.constants.COPYFILE_EXCL);
  let copiedGuidance = null;
  let guidanceDigest = null;
  if (values.guidance) {
    const guidance = path.resolve(values.guidance);
    if (!fs.existsSync(guidance) || !fs.statSync(guidance).isFile()) fail(`guidance file not found: ${guidance}`);
    copiedGuidance = path.join(sessionDir, "inputs", `guidance-${path.basename(guidance)}`);
    fs.copyFileSync(guidance, copiedGuidance, fs.constants.COPYFILE_EXCL);
    guidanceDigest = digestFile(copiedGuidance);
  }

  const event = {
    schema_version: "1.0",
    seq: 1,
    timestamp: new Date().toISOString(),
    session_id: sessionId,
    platform: values.platform,
    mode: values.mode,
    phase: "init",
    status: "complete",
    payload: {
      evidence_root: root,
      codebase_root: root,
      program_url: values["program-url"],
      report_path: copiedReport,
      report_sha256: digestFile(copiedReport),
      optional_guidance_path: copiedGuidance,
      optional_guidance_sha256: guidanceDigest,
    },
  };
  appendAtomic(sessionDir, event);
  console.log(JSON.stringify({
    status: "initialized",
    session: sessionDir,
    next_phase: "prior_art",
    fresh_program_read_required: true,
    resume_flag: `resume=${sessionDir}`,
  }, null, 2));
}

function append(values) {
  required(values, ["session", "phase", "status"]);
  const sessionDir = path.resolve(values.session);
  const events = readEvents(sessionDir);
  const ledgerErrors = validationErrors(events);
  if (ledgerErrors.length) fail(`session verification failed: ${ledgerErrors.join("; ")}`);
  const initEvent = events[0];
  const latest = events.at(-1);
  if (!initEvent || initEvent.phase !== "init") fail("first ledger event must be init");
  if (initEvent.mode !== "dynamic") fail("persisted appends are disabled for oneshot sessions");
  if (["terminal", "error"].includes(latest.status)) fail(`session is closed by ${latest.status} event at phase ${latest.phase}`);
  if (![...PHASES, "report_revision"].includes(values.phase)) fail(`unknown phase: ${values.phase}`);
  if (!STATUSES.has(values.status)) fail(`unknown status: ${values.status}`);

  const expected = nextPhase(events);
  if (values.phase !== "report_revision" && values.phase !== expected) {
    fail(`illegal transition: expected ${expected}, received ${values.phase}`);
  }
  if (values.phase === "report_revision") {
    const verdictDone = events.some((event) => event.phase === "verdict_and_improvement" && event.status === "complete");
    if (!verdictDone) fail("report_revision requires a completed verdict_and_improvement phase");
  }

  const payload = readPayload(values);
  const payloadErrors = [
    ...validatePayload(payload, values.phase, values.status),
    ...validateFreshProgramRead(payload, initEvent, latest, values.phase),
  ];
  if (payloadErrors.length) fail(payloadErrors.join("; "));

  const event = {
    schema_version: "1.0",
    seq: events.length + 1,
    timestamp: new Date().toISOString(),
    session_id: initEvent.session_id,
    platform: initEvent.platform,
    mode: initEvent.mode,
    phase: values.phase,
    status: values.status,
    payload,
  };
  appendAtomic(sessionDir, event);
  const updated = [...events, event];
  console.log(JSON.stringify({
    status: "appended",
    seq: event.seq,
    phase: event.phase,
    next_phase: ["terminal", "error"].includes(event.status) ? null : nextPhase(updated),
    must_halt: true,
    fresh_program_read_on_resume: !["terminal", "error"].includes(event.status),
    resume_flag: `resume=${sessionDir}`,
  }, null, 2));
}

function status(values) {
  required(values, ["session"]);
  const sessionDir = path.resolve(values.session);
  const events = readEvents(sessionDir);
  const initEvent = events[0];
  const latest = events.at(-1);
  console.log(JSON.stringify({
    session: sessionDir,
    session_id: initEvent.session_id,
    platform: initEvent.platform,
    mode: initEvent.mode,
    program_url: initEvent.payload.program_url,
    report_path: initEvent.payload.report_path,
    event_count: events.length,
    latest: { phase: latest.phase, status: latest.status, timestamp: latest.timestamp },
    next_phase: ["terminal", "error"].includes(latest.status) ? null : nextPhase(events),
  }, null, 2));
}

function verify(values) {
  required(values, ["session"]);
  const sessionDir = path.resolve(values.session);
  const events = readEvents(sessionDir);
  const errors = validationErrors(events);
  const result = { status: errors.length ? "fail" : "pass", errors, events: events.length, next_phase: nextPhase(events) };
  console.log(JSON.stringify(result, null, 2));
  if (errors.length) process.exit(1);
}

function resume(values) {
  required(values, ["session"]);
  const sessionDir = path.resolve(values.session);
  const events = readEvents(sessionDir);
  const errors = validationErrors(events);
  if (errors.length) fail(`session verification failed: ${errors.join("; ")}`);
  const initEvent = events[0];
  const latest = events.at(-1);
  if (initEvent.mode !== "dynamic") fail("only dynamic sessions can be resumed");
  const closed = ["terminal", "error"].includes(latest.status) || nextPhase(events) === "complete";
  console.log(JSON.stringify({
    status: closed ? "closed" : "resumable",
    session: sessionDir,
    session_id: initEvent.session_id,
    platform: initEvent.platform,
    mode: initEvent.mode,
    evidence_root: initEvent.payload.evidence_root ?? initEvent.payload.codebase_root,
    codebase_root: initEvent.payload.codebase_root,
    program_url: initEvent.payload.program_url,
    report_path: initEvent.payload.report_path,
    optional_guidance_path: initEvent.payload.optional_guidance_path,
    latest: { phase: latest.phase, status: latest.status, timestamp: latest.timestamp },
    next_phase: closed ? null : nextPhase(events),
    fresh_program_read_required: !closed,
    resume_flag: `resume=${sessionDir}`,
  }, null, 2));
}

function usage() {
  console.log(`Free Triager dynamic session ledger\n\nCommands:\n  init --root WORKSPACE_OR_EVIDENCE_PATH --platform immunefi --mode dynamic --program-url URL --report FILE [--session ID] [--guidance FILE]\n  append --session DIR --phase PHASE --status STATUS [--payload FILE]  # otherwise reads JSON from stdin\n  resume --session DIR\n  status --session DIR\n  verify --session DIR`);
}

const { command, values } = parseArgs(process.argv.slice(2));
if (!command || command === "help" || command === "--help") usage();
else if (command === "init") init(values);
else if (command === "append") append(values);
else if (command === "status") status(values);
else if (command === "verify") verify(values);
else if (command === "resume") resume(values);
else fail(`unknown command: ${command}`);
```

## skill.yaml

```yaml
schema_version: "1.0"
id: "free-triager"
entrypoint: "SKILL.md"
platform_discovery: "platforms/<platform>/adapter.yaml"
workflow:
  phases:
    - "prior_art"
    - "program_policy"
    - "eligibility_gate"
    - "technical_validation"
    - "verdict_and_improvement"
technical_routing:
  classifier: "workers/04-target-classifier.md"
  profiles:
    smart_contract: "core/technical-profiles/smart-contract.md"
    web_app: "core/technical-profiles/web-app.md"
    blockchain_dlt: "core/technical-profiles/blockchain-dlt.md"
    mixed: "core/technical-profiles/mixed.md"
  technology_discovery: "derive_from_report_program_and_evidence"
modes:
  oneshot:
    persistence: "forbidden"
    program_source_refresh: "once_at_start"
    state: "current_context_only"
  dynamic:
    persistence: "required"
    program_source_refresh: "on_init_and_before_each_resumed_phase"
    state: "append_only_jsonl"
    resume_flag: "resume=<absolute-session-directory>"
capabilities:
  web_read:
    requirement: "required"
    purpose: "Read live program evidence once per oneshot invocation or before each dynamic phase invocation."
  filesystem_read:
    requirement: "required"
    purpose: "Read the report and available source, request, trace, configuration, protocol, or binary evidence; read a session ledger only in dynamic mode."
  filesystem_write:
    requirement: "conditional"
    purpose: "Required for dynamic checkpoints and an explicitly authorized report revision; forbidden for oneshot triage."
  node_runtime:
    requirement: "conditional"
    purpose: "Run the deterministic session ledger helper in dynamic mode only."
    fallback: "Use harness filesystem tools to append the same session-event schema and enforce identical transitions."
  code_execution:
    requirement: "conditional"
    purpose: "Validate a PoC when required, safe, and locally reproducible."
  isolated_workers:
    requirement: "optional"
    fallback: "Execute phases serially using one compact in-memory state; do not emit simulated worker transcripts or repeat shared inputs."
```

## tests

```

```

## tests/session.test.mjs

```js
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";

const script = path.resolve(import.meta.dirname, "..", "scripts", "session.mjs");
const PROGRAM_URL = "https://immunefi.com/bug-bounty/example/information/";

function run(args, cwd, input) {
  return spawnSync(process.execPath, [script, ...args], { cwd, encoding: "utf8", input });
}

function workerPayload(phase, decision = "continue", source = {}) {
  const results = {
    prior_art: { known_issues: [], prior_submissions_or_duplicates: [], audits_and_disclosures: [], access_gaps: [], comparison_hooks: [] },
    program_policy: { assets: [], impacts: [], exclusions: [], primacy: {}, severity_system: {}, poc_requirements: {}, automation_policy: {}, submission_rules: [], category_rule_matrix: {} },
    eligibility_gate: { gates: [], known_issue_classification: {}, applicable_scope_path: {}, viable_category_paths: [], disposition: "continue" },
    technical_validation: {
      target_classification: {
        primary_category: "smart_contract",
        applicable_categories: ["smart_contract"],
        evidence_mode: "source_assisted",
        system_boundary: {},
        components: [],
        interfaces: [],
        actors_and_trust_boundaries: [],
        technology_context: { language: "derived-by-model" },
        required_profiles: ["smart_contract"],
        classification_evidence: [],
        uncertainties: [],
        confidence: "high",
      },
      profile_results: { smart_contract: {} },
      claim_matrix: [], exploit_path: [], violated_invariant: {}, privileged_precondition_analysis: {},
      poc_validation: {}, poc_execution_status: "statically_verified_not_executed", poc_compliance_checklist: [],
      steelman: {}, skeptic: {}, technical_verdict: "supported",
    },
    verdict_and_improvement: {
      target_category: "smart_contract", profile_summary: {}, verdict: "ready", severity: "High", severity_basis: {},
      likelihood: {}, automated_triage_readiness: {}, human_merits: {}, submission_recommendation: "submit",
      fee_risk: {}, rejection_risks: [], improvements: [], report_outline: [], template_compliance_checklist: [], confidence: "high",
    },
  };
  return {
    phase,
    summary: "test payload",
    decision,
    facts: [],
    inferences: [],
    unresolved: [],
    sources: [{ url: PROGRAM_URL, retrieved_at: new Date().toISOString(), source_type: "program", ...source }],
    result: results[phase] ?? {},
  };
}

test("initializes, appends, reports status, and verifies a session", () => {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "free-triager-test-"));
  const report = path.join(root, "report.md");
  fs.writeFileSync(report, "# Finding\n", "utf8");

  const initialized = run([
    "init", "--root", root, "--platform", "immunefi", "--mode", "dynamic",
    "--program-url", PROGRAM_URL,
    "--report", report, "--session", "test-run",
  ], root);
  assert.equal(initialized.status, 0, initialized.stderr);

  const session = path.join(root, ".free-triager", "runs", "test-run");
  assert.equal(fs.existsSync(path.join(session, "worker-output")), false);
  const appended = run(
    ["append", "--session", session, "--phase", "prior_art", "--status", "complete"],
    root,
    JSON.stringify(workerPayload("prior_art")),
  );
  assert.equal(appended.status, 0, appended.stderr);
  assert.equal(JSON.parse(appended.stdout).must_halt, true);

  const status = run(["status", "--session", session], root);
  assert.equal(status.status, 0, status.stderr);
  assert.equal(JSON.parse(status.stdout).next_phase, "program_policy");

  const resumed = run(["resume", "--session", session], root);
  assert.equal(resumed.status, 0, resumed.stderr);
  assert.equal(JSON.parse(resumed.stdout).next_phase, "program_policy");
  assert.equal(JSON.parse(resumed.stdout).fresh_program_read_required, true);

  const verified = run(["verify", "--session", session], root);
  assert.equal(verified.status, 0, verified.stderr);
  assert.equal(JSON.parse(verified.stdout).status, "pass");
});

test("rejects out-of-order phase transitions", () => {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "free-triager-test-"));
  const report = path.join(root, "report.md");
  fs.writeFileSync(report, "# Finding\n", "utf8");
  run([
    "init", "--root", root, "--platform", "immunefi", "--mode", "dynamic",
    "--program-url", PROGRAM_URL,
    "--report", report, "--session", "test-run",
  ], root);

  const session = path.join(root, ".free-triager", "runs", "test-run");
  const result = run(
    ["append", "--session", session, "--phase", "program_policy", "--status", "complete"],
    root,
    JSON.stringify(workerPayload("program_policy")),
  );
  assert.notEqual(result.status, 0);
  assert.match(result.stderr, /illegal transition/);
});

test("allows a needs-information phase to be retried", () => {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "free-triager-test-"));
  const report = path.join(root, "report.md");
  fs.writeFileSync(report, "# Finding\n", "utf8");
  run([
    "init", "--root", root, "--platform", "immunefi", "--mode", "dynamic",
    "--program-url", PROGRAM_URL,
    "--report", report, "--session", "test-run",
  ], root);

  const session = path.join(root, ".free-triager", "runs", "test-run");
  assert.equal(run(
    ["append", "--session", session, "--phase", "prior_art", "--status", "needs_information"],
    root,
    JSON.stringify(workerPayload("prior_art", "needs_information")),
  ).status, 0);

  assert.equal(run(
    ["append", "--session", session, "--phase", "prior_art", "--status", "complete"],
    root,
    JSON.stringify(workerPayload("prior_art")),
  ).status, 0);
  assert.equal(run(["verify", "--session", session], root).status, 0);
});

test("oneshot cannot initialize persistence and leaves no runtime directory", () => {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "free-triager-test-"));
  const report = path.join(root, "report.md");
  fs.writeFileSync(report, "# Finding\n", "utf8");

  const result = run([
    "init", "--root", root, "--platform", "immunefi", "--mode", "oneshot",
    "--program-url", PROGRAM_URL,
    "--report", report, "--session", "must-not-exist",
  ], root);

  assert.notEqual(result.status, 0);
  assert.match(result.stderr, /oneshot is memory-only/);
  assert.equal(fs.existsSync(path.join(root, ".free-triager")), false);
});

test("verification rejects a legacy persisted oneshot ledger", () => {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "free-triager-test-"));
  const session = path.join(root, "legacy-oneshot");
  fs.mkdirSync(session);
  fs.writeFileSync(path.join(session, "session.jsonl"), `${JSON.stringify({
    schema_version: "1.0",
    seq: 1,
    timestamp: new Date().toISOString(),
    session_id: "legacy-oneshot",
    platform: "immunefi",
    mode: "oneshot",
    phase: "init",
    status: "complete",
    payload: { codebase_root: root, program_url: PROGRAM_URL, report_path: "report.md" },
  })}\n`, "utf8");

  const verified = run(["verify", "--session", session], root);
  assert.notEqual(verified.status, 0);
  assert.match(verified.stdout, /persisted session mode must be dynamic/);
});

test("resume returns saved routing data and requires a fresh program read", () => {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "free-triager-test-"));
  const report = path.join(root, "report.md");
  const programUrl = PROGRAM_URL;
  fs.writeFileSync(report, "# Finding\n", "utf8");

  assert.equal(run([
    "init", "--root", root, "--platform", "immunefi", "--mode", "dynamic",
    "--program-url", programUrl, "--report", report, "--session", "resume-run",
  ], root).status, 0);

  const session = path.join(root, ".free-triager", "runs", "resume-run");
  const resumed = run(["resume", "--session", session], root);
  assert.equal(resumed.status, 0, resumed.stderr);
  const state = JSON.parse(resumed.stdout);
  assert.equal(state.status, "resumable");
  assert.equal(state.program_url, programUrl);
  assert.equal(state.next_phase, "prior_art");
  assert.equal(state.fresh_program_read_required, true);
  assert.equal(state.resume_flag, `resume=${session}`);
});

test("rejects a checkpoint that cites a different program page", () => {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "free-triager-test-"));
  const report = path.join(root, "report.md");
  fs.writeFileSync(report, "# Finding\n", "utf8");
  assert.equal(run([
    "init", "--root", root, "--platform", "immunefi", "--mode", "dynamic",
    "--program-url", PROGRAM_URL, "--report", report, "--session", "wrong-url-run",
  ], root).status, 0);

  const session = path.join(root, ".free-triager", "runs", "wrong-url-run");
  const result = run(
    ["append", "--session", session, "--phase", "prior_art", "--status", "complete"],
    root,
    JSON.stringify(workerPayload("prior_art", "continue", { url: "https://immunefi.com/" })),
  );
  assert.notEqual(result.status, 0);
  assert.match(result.stderr, /must include the saved program URL/);
});

test("rejects stale program evidence from before the previous checkpoint", () => {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "free-triager-test-"));
  const report = path.join(root, "report.md");
  fs.writeFileSync(report, "# Finding\n", "utf8");
  assert.equal(run([
    "init", "--root", root, "--platform", "immunefi", "--mode", "dynamic",
    "--program-url", PROGRAM_URL, "--report", report, "--session", "stale-run",
  ], root).status, 0);

  const session = path.join(root, ".free-triager", "runs", "stale-run");
  const result = run(
    ["append", "--session", session, "--phase", "prior_art", "--status", "complete"],
    root,
    JSON.stringify(workerPayload("prior_art", "continue", { retrieved_at: "2000-01-01T00:00:00.000Z" })),
  );
  assert.notEqual(result.status, 0);
  assert.match(result.stderr, /at or after the previous checkpoint/);
});

test("accepts technology-neutral routing for every target category and mixed findings", () => {
  const routes = [
    { primary: "smart_contract", categories: ["smart_contract"] },
    { primary: "web_app", categories: ["web_app"] },
    { primary: "blockchain_dlt", categories: ["blockchain_dlt"] },
    { primary: "mixed", categories: ["smart_contract", "web_app", "blockchain_dlt"] },
  ];

  for (const route of routes) {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "free-triager-route-test-"));
    const report = path.join(root, "report.md");
    fs.writeFileSync(report, "# Technology-neutral finding\n", "utf8");
    assert.equal(run([
      "init", "--root", root, "--platform", "immunefi", "--mode", "dynamic",
      "--program-url", PROGRAM_URL, "--report", report, "--session", "route-run",
    ], root).status, 0);
    const session = path.join(root, ".free-triager", "runs", "route-run");

    for (const phase of ["prior_art", "program_policy", "eligibility_gate"]) {
      const appended = run(
        ["append", "--session", session, "--phase", phase, "--status", "complete"],
        root,
        JSON.stringify(workerPayload(phase)),
      );
      assert.equal(appended.status, 0, appended.stderr);
    }

    const technical = workerPayload("technical_validation");
    technical.result.target_classification.primary_category = route.primary;
    technical.result.target_classification.applicable_categories = route.categories;
    technical.result.target_classification.required_profiles = route.categories;
    technical.result.profile_results = Object.fromEntries(route.categories.map((category) => [category, {}]));
    if (route.primary === "mixed") {
      const incomplete = JSON.parse(JSON.stringify(technical));
      delete incomplete.result.profile_results.web_app;
      const rejected = run(
        ["append", "--session", session, "--phase", "technical_validation", "--status", "complete"],
        root,
        JSON.stringify(incomplete),
      );
      assert.notEqual(rejected.status, 0);
      assert.match(rejected.stderr, /profile_results missing selected profile: web_app/);
    }
    const appended = run(
      ["append", "--session", session, "--phase", "technical_validation", "--status", "complete"],
      root,
      JSON.stringify(technical),
    );
    assert.equal(appended.status, 0, `${route.primary}: ${appended.stderr}`);
    assert.equal(run(["verify", "--session", session], root).status, 0);
  }
});
```

## workers

```

```

## workers/01-prior-art.md

# Worker: Prior Art

Goal: build the evidence set used to detect known or previously accepted risk before technical validation.

Use the fresh program-page retrieval supplied for this invocation and follow only links relevant to:

- public disclosure of known issues;
- GitHub/GitLab issue trackers named by the program;
- prior audit reports and security disclosures;
- acknowledged risks, operational mitigations, or conscious decisions not to fix;
- team responses supplied by the user.

For each item record:

- stable ID;
- title and source;
- affected asset/component and code location when available;
- root cause;
- impact;
- status or mitigation;
- explanation for non-fix, if stated;
- confidence and missing details.

Classify each match as one of:

- `program_disclosed_ineligible_known_issue`;
- `program_disclosed_issue_eligibility_unclear`;
- `earlier_report_or_duplicate_candidate`;
- `prior_audit_finding`;
- `related_but_distinct`.

An earlier report match is not automatically a terminal known issue. Preserve the exact duplicate or competition rule and whether prior ordering affects eligibility.

Do not classify the submitted report as a known issue from a title match. Produce comparison hooks: root cause, affected state, required fix, and material impact.

If the page advertises known issues but the linked source cannot be accessed, return `needs_information` unless the program page itself provides enough detail for reliable comparison.

Required `result` keys: `known_issues`, `prior_submissions_or_duplicates`, `audits_and_disclosures`, `access_gaps`, and `comparison_hooks`; each value is an array.

## workers/02-program-policy.md

# Worker: Program Policy

Goal: produce a source-grounded eligibility and severity policy for this exact program.

For Immunefi, use the PoC-guideline reference routed from the entrypoint to extract the applicable platform baseline, but first determine from the fresh program evidence supplied for this invocation whether a PoC is required and whether program-specific terms override it. Do not re-open a page already present in the supplied evidence map.

Extract:

- in-scope assets with their program category and identifiers, repositories, contracts, URLs, applications, services, clients, networks, versions, and dates where available;
- in-scope impacts and mapped severity, preserving separate smart-contract, web/app, and Blockchain/DLT tables;
- explicit out-of-scope assets, impacts, attack methods, and assumptions;
- Primacy of Rules language;
- Primacy of Impact severities/impacts and exceptions;
- selected platform severity-classification version;
- PoC form and execution requirements by category;
- disclosure, testing, traffic, anti-bot, and submission rules visible to the worker;
- automation/AI-assistance restrictions, disclosure duties, and source confidence;
- reward constraints only where they affect report framing or eligibility.

Resolve conflicts by precedence:

1. explicit program-specific rule;
2. program-selected platform classification/version;
3. current platform default only where the program is silent;
4. user-supplied private guidance, applied only to the purpose and provenance stated by the user.

Do not merge old and current severity systems. Save ambiguity explicitly if the program does not identify its version.

Required `result` keys: `assets`, `impacts`, `exclusions`, `primacy`, `severity_system`, `poc_requirements`, `automation_policy`, `submission_rules`, and `category_rule_matrix`. The matrix maps every category present on the program page to its assets, impacts, exclusions, severity rules, primacy mode, and PoC requirements without filling absent categories.

## workers/03-eligibility-gate.md

# Worker: Eligibility Gate

Goal: stop early when a decisive known-issue, scope, impact, or rules gate makes rejection likely.

Use the report, prior-art registry, and policy checkpoint. Evaluate every category plausibly implicated by the affected asset and claimed impact. If category ambiguity could change eligibility, preserve the viable paths for technical classification instead of selecting the harshest one.

## Gate order

1. Compare known issues and duplicate candidates by root cause, affected state, material impact, and conceptual fix; then apply the exact consequence specified by the program.
2. Determine whether the exact affected asset/version is in scope.
3. Check program-specific requirements and Primacy of Rules restrictions.
4. Determine whether the demonstrated impact is listed in scope.
5. If the asset is out of scope, test only the program's explicit Primacy of Impact path and exceptions.

For every gate return `pass`, `fail`, or `uncertain`, with decisive rule/evidence references.

Known issue equivalence requires a reasoned mapping and source. Similar bug class alone is insufficient.

Do not collapse these outcomes:

- a program-disclosed issue explicitly declared ineligible may terminate as `known_issue`;
- an earlier report may be a duplicate only under the applicable competition/program rule;
- an informational match that the platform still escalates must not become a rejection gate;
- a related prior issue with a different state transition, victim path, impact, or fix remains distinct.

If both asset and impact fail and no Primacy of Impact exception applies, return terminal `out_of_scope`. If policy wording is materially ambiguous, return `needs_information` instead of choosing the harsher interpretation.

Required `result` keys: `gates`, `known_issue_classification`, `applicable_scope_path`, `viable_category_paths`, and `disposition`. `gates` is an ordered array matching the gate order above. `viable_category_paths` preserves each program category that remains eligible for technical classification and the evidence supporting that path.

## workers/04-target-classifier.md

# Worker: Target Classifier

Goal: derive the minimum technological context and validation route needed to test the report, without relying on a fixed language or framework catalog.

Use the report, program-policy result, eligibility result, and available technical evidence. Classify by the affected system boundary and exploit path—not merely by repository language or project labels.

Choose one or more applicable categories:

- `smart_contract` for execution whose decisive vulnerable state and impact occur in on-chain program logic;
- `web_app` for browser, mobile, API, backend, identity, session, or other application-layer behavior;
- `blockchain_dlt` for node/client, consensus, finality, networking, mempool, RPC, state synchronization, storage, or protocol-level behavior;
- `mixed` only when the exploit crosses multiple categories and each boundary is material to validity or impact.

Derive, rather than assume:

- languages, runtimes, execution environments, frameworks, protocols, and relevant tools;
- deployed version, commit, configuration, network, and component identity;
- actors, trust boundaries, interfaces, data/state transitions, and external dependencies;
- evidence mode: `source_assisted`, `black_box`, or `hybrid`;
- which technical profile files are required.

Unknown technology is not a rejection. Record it as unresolved only when it prevents a material claim from being validated. Do not require a local codebase for a valid black-box report.

Return a compact classification object with these required keys: `primary_category`, `applicable_categories`, `evidence_mode`, `system_boundary`, `components`, `interfaces`, `actors_and_trust_boundaries`, `technology_context`, `required_profiles`, `classification_evidence`, `uncertainties`, and `confidence`.

## workers/04-technical-validation.md

# Worker: Technical Validation

Goal: determine whether the report's exploit and impact hold in the overall program and technological context under realistic stated conditions.

For Immunefi, use the PoC-guideline reference routed from the entrypoint and apply only the requirements activated by the live program-policy checkpoint.

## Technology-neutral routing

1. Use the target-classifier instructions routed from the entrypoint and derive `target_classification` from the report, program policy, eligibility result, and available evidence.
2. Read only the technical profiles routed from the entrypoint and named by `target_classification.required_profiles`.
3. If more than one category is material, also use the mixed profile and validate the cross-category boundary.
4. Derive languages, runtimes, frameworks, protocols, versions, configuration, and tools from the evidence. Never reject a claim because its technology is absent from a predefined list.

Validate:

- cited assets, components, interfaces, versions, configurations, files, functions, endpoints, or protocol elements;
- actor permissions, trust boundaries, and attacker capabilities;
- preconditions and whether each is proven, realistic, or speculative;
- exact actions, messages, requests, calls, and state/data transitions;
- relevant guards, recovery paths, and counterexamples;
- violated invariant;
- victim and measurable impact;
- exploit repeatability and economic/operational feasibility;
- PoC correctness against the program's actual category-specific requirement.

## Privileged-precondition analysis

Separate the actor causing the bug from a privileged actor performing an ordinary authorized operation. A sequence such as a normal blocklist, rescue, configuration update, or later restoration is not automatically “privileged exploitation” when the code defect subsequently harms an unprivileged user. Apply a privileged-action exclusion only when its wording actually covers the scenario.

Record:

- which actor performs each transition;
- whether each privileged action is malicious, mistaken, or normal operation;
- whether the attacker needs the privilege;
- whether an unprivileged victim is harmed later;
- the exact policy clause that includes or excludes the path.

Use source, local code, test environments, request/response evidence, traces, captures, screenshots, logs, configurations, binaries, or protocol artifacts as appropriate to the derived evidence mode. Run code or tests only when authorized, safe, and useful. Do not access live funds, production credentials, unrelated user data, or attack public systems. Inspect report-provided PoCs before execution; do not execute untrusted code blindly.

Keep PoC states precise: `executed_pass`, `executed_fail`, `statically_verified_not_executed`, `not_runnable`, or `not_inspected`. Tool unavailability is not a failed PoC. It is an execution-confidence limitation that must be disclosed and, where practical, handed to the user with an exact local run command.

Steelman the strongest realistic interpretation, then run an independent skeptic pass. Record both. Do not introduce new assumptions solely to rescue the report.

Return a claim matrix with `proven`, `supported`, `uncertain`, or `refuted`, plus the most precise available code, request, trace, configuration, or protocol citations. Use `invalid_claim` only when decisive evidence refutes the required path or impact.

Required `result` keys: `target_classification`, `profile_results`, `claim_matrix`, `exploit_path`, `violated_invariant`, `privileged_precondition_analysis`, `poc_validation`, `poc_execution_status`, `poc_compliance_checklist`, `steelman`, `skeptic`, and `technical_verdict`. Keep the derived technology context inside `target_classification`; do not duplicate it at the result root.

## workers/05-verdict-and-improvement.md

# Worker: Verdict and Improvement

Goal: issue the final pre-submission assessment and improve acceptance odds without overstating the finding.

For Immunefi, use the report-template reference routed from the entrypoint and check every applicable dashboard field and rejection-risk item.

Also use the automated-triage-readiness reference routed from the entrypoint. Treat its observed checks as calibration evidence, not as a guaranteed or exhaustive policy.

Resolve:

- target category or mixed boundary, derived technology context, and profile-specific technical result;
- final verdict vocabulary from the orchestrator workflow;
- applicable severity and the exact impact mapping;
- exploit likelihood/preconditions as a separate axis;
- strongest rejection reasons in priority order;
- confidence and unresolved assumptions;
- minimal technical, PoC, scope, and presentation improvements;
- claims that should be narrowed or removed;
- evidence/citations that should be added;
- required private-guidance gaps, if any.

Severity procedure:

1. Select the exact demonstrated impact from the applicable program category table.
2. Record the table-mapped severity.
3. Record likelihood and preconditions separately.
4. Apply a downgrade only when an applicable rule expressly authorizes it and cite that rule.

Do not convert “High impact, Medium likelihood” into Medium by intuition. If permanent versus temporary freezing or another boundary is uncertain, state what each classification requires and recommend the highest tier directly supported by current evidence.

Produce separate fields for `automated_triage_readiness`, `human_merits`, and `submission_recommendation`. The recommendation is one of `submit`, `submit_after_changes`, `needs_information`, or `do_not_submit`.

Use public program/platform rules and user-supplied private guidance as separately attributed sources. Do not present private guidance as public Immunefi policy.

Produce a suggested report outline aligned with the available submission standard. Do not rewrite the report in this phase.

If the user later requests revision, preserve technical meaning, create a revised copy by default, and include a concise change log. Never inflate severity or hide a failed gate.

Required `result` keys: `target_category`, `profile_summary`, `verdict`, `severity`, `severity_basis`, `likelihood`, `automated_triage_readiness`, `human_merits`, `submission_recommendation`, `fee_risk`, `rejection_risks`, `improvements`, `report_outline`, `template_compliance_checklist`, and `confidence`.

## workers/06-report-revision.md

# Worker: Report Revision

Run only after a completed verdict and an explicit user request to revise the report.

For Immunefi, use the report-template and PoC-guideline references routed from the entrypoint before revising.

Inputs:

- original report from the session input directory;
- completed verdict and improvement checkpoint;
- applicable public and user-supplied private submission guidance;
- destination path approved or implied by the user's request.

Preserve the original report. Unless the user explicitly requests overwrite, write `<original-name>.free-triager-revised.<ext>` beside the user's chosen output location or inside the session directory.

Apply only evidence-supported changes:

- correct inaccurate code or scope claims;
- narrow unsupported impact or severity language;
- add missing preconditions, exploit steps, citations, and PoC instructions;
- improve title, structure, reproducibility, and submission-field fit;
- keep private guidance out of the report unless it is appropriate to include.

Make the report complete and naturally specific to the researched code. Do not add wording whose purpose is to disguise AI assistance, manipulate an automated classifier, or evade platform automation checks. The researcher must review and verify the revised report before submission.

Do not invent evidence, execute a new exploit, conceal a failed gate, or convert `needs_information` into certainty.

Required `result` keys: `original_path`, `revised_path`, `change_log`, and `remaining_risks`.

