feat: initial commit — plugin manifest and build brief
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
05a7aa1d6f
7 changed files with 575 additions and 0 deletions
7
.claude-plugin/plugin.json
Normal file
7
.claude-plugin/plugin.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "jobbsok",
|
||||
"version": "0.1.0",
|
||||
"description": "Local-first job search workspace: sourcing, scoring, case tracking, applications and interview prep.",
|
||||
"author": { "name": "Kjell Tore Guttormsen" },
|
||||
"license": "MIT"
|
||||
}
|
||||
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Operator-private continuity state. This repo has a PUBLIC remote, so STATE.md
|
||||
# is LOCAL-ONLY here — it must never reach the mirror. (Private-remote repos
|
||||
# track theirs; do not carry that habit across.)
|
||||
STATE.md
|
||||
*.local.md
|
||||
|
||||
.env
|
||||
.env.*
|
||||
.DS_Store
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
7
CHANGELOG.md
Normal file
7
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Repository created; build brief in `docs/build-brief.md`; manifest only.
|
||||
63
CLAUDE.md
Normal file
63
CLAUDE.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# jobbsok
|
||||
|
||||
Local-first, read-only job search workspace for a single operator, packaged as
|
||||
a marketplace plugin. The build brief is the contract:
|
||||
`docs/build-brief.md` — read it whole before writing any file.
|
||||
|
||||
## Context
|
||||
|
||||
One plugin for Claude Code and Claude Cowork. Cowork is the day-to-day operator
|
||||
surface; Claude Code is the development surface. Skills under `skills/*/SKILL.md`
|
||||
(no `commands/`), deterministic logic in `scripts/*.py`, MCP servers declared in
|
||||
`.mcp.json` (browser over CDP, read-only IMAP) — both optional; every skill
|
||||
degrades to manual paste.
|
||||
|
||||
Operator-facing content (skill output, `sak.md`, drafts) is Norwegian bokmål.
|
||||
Code, identifiers, file names and docs are English.
|
||||
|
||||
## Invariants
|
||||
|
||||
- **This repo has a PUBLIC remote (`open/jobbsok`).** `STATE.md` is LOCAL-ONLY
|
||||
and gitignored from the first commit. Repos on a private remote track theirs.
|
||||
- **Read-only against external sites.** No form submit, no send, no apply.
|
||||
- **No credential handling.** Attaches to an already-authenticated browser
|
||||
over CDP. Mail credentials only via environment variables.
|
||||
- **Human approval before any profile mutation.** `laering` proposes diffs.
|
||||
- **Local-only data.** Workspace never committed; no telemetry.
|
||||
- **Untrusted content stays untrusted.** Every listing and email body passes
|
||||
`scripts/guard_ingest.py` (llm-ingestion-guard, pinned tag) before persist.
|
||||
The trust boundary is the write.
|
||||
- **Append-only logs.** `beslutninger.jsonl`, `logg.jsonl`: corrections are new
|
||||
entries, never edits.
|
||||
- **No autonomous scheduling.** Nothing polls, nothing runs in background.
|
||||
- **Build in milestone order (brief §11, M1–M6).** Do not skip ahead.
|
||||
- **`${CLAUDE_PLUGIN_ROOT}` for every intra-plugin path.** Never hardcode.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
python3 -m pytest tests/ # zero-network tests (brief §12)
|
||||
```
|
||||
|
||||
## Release
|
||||
|
||||
Polyrepo rule: a version bump is not finished until the tag `vX.Y.Z` is pushed
|
||||
**and** the catalog `ref` is bumped to it. Use `release-plugin.mjs`, never a
|
||||
hand-edited `ref`. The catalog README entry for a new plugin is added by hand once.
|
||||
|
||||
## Communication patterns
|
||||
|
||||
### Linking to local files
|
||||
|
||||
When pointing to local files in responses, always use markdown link syntax with a descriptive name:
|
||||
|
||||
- Use `[Human-friendly name](file:///absolute/path)` — never bare `file:///...` URLs or autolinks `<file://...>`.
|
||||
- Always use absolute paths. Never `~/` or relative paths.
|
||||
- For multiple files, render as a bullet list of named markdown links.
|
||||
|
||||
Why: bare `file://` URLs only render the first as clickable across multiple lines. Named markdown links make each entry independently clickable and look cleaner.
|
||||
|
||||
Example:
|
||||
|
||||
- [Brief](file:///Users/ktg/.../brief.html)
|
||||
- [Research summary](file:///Users/ktg/.../research/summary.md)
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Kjell Tore Guttormsen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
45
README.md
Normal file
45
README.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# jobbsok
|
||||
Local-first job search workspace: sourcing, scoring, case tracking, applications and interview prep.
|
||||
|
||||
A read-only job search operating system for one person searching for their own
|
||||
job. It reads listings the operator is already looking at, scores them against
|
||||
a maintained candidate profile, records every accept/reject decision with a
|
||||
reason, keeps each case's full correspondence in one folder, drafts
|
||||
applications as files, and flags threads that have gone silent. It never
|
||||
submits, never sends, never stores a credential.
|
||||
|
||||
> **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted.
|
||||
|
||||
*AI-generated: all code produced by Claude Code through dialog-driven development.*
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
**Status:** pre-release. Nothing is built yet beyond the manifest; the build
|
||||
brief in `docs/build-brief.md` is the contract, and milestones ship in order
|
||||
(M1–M6).
|
||||
|
||||
## Install
|
||||
|
||||
Not installable yet. When released, the plugin is pinned by tag in the
|
||||
`ktg-plugin-marketplace` catalog:
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
|
||||
claude plugin install jobbsok@ktg-plugin-marketplace
|
||||
```
|
||||
|
||||
## Non-goals
|
||||
|
||||
Submitting applications. Sending email. Any write to Finn.no or LinkedIn.
|
||||
Credential storage. Background jobs. Multi-user support. Recruiter-side
|
||||
features. Bulk listing harvest.
|
||||
|
||||
## Changelog
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
417
docs/build-brief.md
Normal file
417
docs/build-brief.md
Normal file
|
|
@ -0,0 +1,417 @@
|
|||
# Build brief — `jobbsok` plugin
|
||||
|
||||
A local-first, read-only job search operating system for a single operator.
|
||||
Written for Claude Code as the implementing agent. Read the whole brief before
|
||||
writing any file. Build in milestone order; do not skip ahead.
|
||||
|
||||
**Language convention:** code, identifiers, file names and this brief are
|
||||
English. All operator-facing content — skill output, `sak.md` bodies,
|
||||
application drafts — is Norwegian (bokmål). The job market being served is
|
||||
Norwegian.
|
||||
|
||||
---
|
||||
|
||||
## 1. Mission
|
||||
|
||||
Compress the job-search loop from scattered manual work into one auditable local
|
||||
workspace: find candidate roles, score them against a maintained candidate
|
||||
profile, record accept/reject decisions with reasons, learn from the pattern,
|
||||
draft applications, keep every case's full correspondence in one folder, prepare
|
||||
interviews, and never lose a thread to silence.
|
||||
|
||||
The operator is a single person searching for their own job. Not a recruiting
|
||||
tool, not multi-tenant, not a SaaS.
|
||||
|
||||
---
|
||||
|
||||
## 2. Non-negotiable constraints
|
||||
|
||||
These are architectural, not preferences. Violating any of them is a defect.
|
||||
|
||||
1. **Read-only against external sites.** The browser layer navigates and reads.
|
||||
It never submits a form, never sends a message, never applies. Applications
|
||||
are prepared as files; the operator sends them by hand.
|
||||
2. **No credential handling.** The plugin attaches to an already-authenticated
|
||||
browser session over CDP. It never sees, stores, or prompts for a password.
|
||||
3. **Human approval before any profile mutation.** Learned changes to the
|
||||
candidate profile are proposed as diffs and applied only on explicit approval.
|
||||
4. **Local-only data.** Everything lives in the operator's workspace. Nothing is
|
||||
transmitted to a third party beyond the model call itself. No telemetry.
|
||||
5. **Untrusted content stays untrusted.** Every byte read from a job listing or
|
||||
an inbound email passes the ingestion guard before it is persisted into a
|
||||
file that a later skill will read as context. See §9.
|
||||
6. **Append-only decision and event logs.** Corrections are new entries, never
|
||||
edits.
|
||||
7. **No autonomous scheduling.** Every skill runs because the operator invoked
|
||||
it. Nothing polls, nothing runs in the background.
|
||||
|
||||
---
|
||||
|
||||
## 3. Target surface
|
||||
|
||||
Build **one plugin** that installs into both Claude Code and Claude Cowork —
|
||||
they share the plugin schema. Cowork is the intended day-to-day operator
|
||||
surface; Claude Code is the development and maintenance surface.
|
||||
|
||||
Consequences for implementation:
|
||||
|
||||
- Use `skills/*/SKILL.md` for every component. Do **not** create a `commands/`
|
||||
directory — the legacy single-file command format is deprecated for new
|
||||
plugins, and Cowork presents skills and commands as one concept anyway.
|
||||
- Skill bodies are instructions **for Claude**, written imperatively
|
||||
("Parse the snapshot", not "You should parse the snapshot"). They are not
|
||||
documentation for the operator to read.
|
||||
- Frontmatter descriptions are third person and contain concrete Norwegian
|
||||
trigger phrases the operator would actually type.
|
||||
- Keep each `SKILL.md` body under ~3000 words. Detailed schemas, enums and
|
||||
worked examples go in `references/` inside the skill directory.
|
||||
- Use `${CLAUDE_PLUGIN_ROOT}` for every intra-plugin path. Never hardcode.
|
||||
|
||||
---
|
||||
|
||||
## 4. Plugin layout
|
||||
|
||||
```
|
||||
jobbsok/
|
||||
├── .claude-plugin/
|
||||
│ └── plugin.json
|
||||
├── skills/
|
||||
│ ├── kandidatprofil/
|
||||
│ ├── annonse-uttrekk/
|
||||
│ ├── kandidatvurdering/
|
||||
│ ├── beslutning/
|
||||
│ ├── sak/
|
||||
│ ├── korrespondanse/
|
||||
│ ├── soknad/
|
||||
│ ├── intervju/
|
||||
│ ├── referanser/
|
||||
│ ├── forhandling/
|
||||
│ ├── dagens/
|
||||
│ ├── utfall/
|
||||
│ ├── laering/
|
||||
│ └── datahygiene/
|
||||
├── scripts/
|
||||
│ ├── guard_ingest.py # wraps llm-ingestion-guard
|
||||
│ ├── sak_status.py # deterministic status/SLA computation
|
||||
│ └── dashboard.py # renders HTML dashboard
|
||||
├── templates/
|
||||
├── .mcp.json
|
||||
├── README.md
|
||||
└── CONNECTORS.md # only if tool categories are genericised
|
||||
```
|
||||
|
||||
`plugin.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "jobbsok",
|
||||
"version": "0.1.0",
|
||||
"description": "Local-first job search workspace: sourcing, scoring, case tracking, applications and interview prep.",
|
||||
"author": { "name": "<operator>" },
|
||||
"license": "MIT"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Workspace data model
|
||||
|
||||
The plugin operates on a workspace directory chosen by the operator, default
|
||||
`~/jobbsok-workspace`. The plugin never writes outside it.
|
||||
|
||||
```
|
||||
workspace/
|
||||
├── profil/
|
||||
│ ├── kandidat.md # single source of truth
|
||||
│ └── cv/
|
||||
│ ├── cv-generell-v3.pdf
|
||||
│ └── varianter.jsonl # which variant went where
|
||||
├── saker/
|
||||
│ └── 2026-09-eksempel-as-ai-radgiver/
|
||||
│ ├── sak.md
|
||||
│ ├── annonse.md
|
||||
│ ├── soknad-v1.md
|
||||
│ ├── korrespondanse/
|
||||
│ ├── kontakter.md
|
||||
│ ├── intervju/
|
||||
│ └── logg.jsonl
|
||||
├── beslutninger.jsonl
|
||||
├── referanser.md
|
||||
├── arkiv/
|
||||
└── .gitignore # workspace is never committed
|
||||
```
|
||||
|
||||
### 5.1 `profil/kandidat.md`
|
||||
|
||||
YAML frontmatter carries hard filters, read mechanically by scoring. Body is
|
||||
prose, read by the model. The boundary is: **frontmatter is absolute, body is
|
||||
judgement.**
|
||||
|
||||
Frontmatter keys: `oppdatert`, `geografi` (base, maks_reisetid_min,
|
||||
hjemmekontor_min_dager, vurderer_flytting), `lonn` (gulv_nok, onsket_nok,
|
||||
kommentar), `ansettelsesform` (aksepterer, avviser), `absolutte_nei`,
|
||||
`senioritet` (min, maks).
|
||||
|
||||
Body sections, in order: Kjerne (one sentence), Kompetanse (Dybde with evidence
|
||||
per item / Bredde / Under oppbygging), Retning (Mot / Bort fra),
|
||||
Signaturprosjekter (situasjon, bidrag, målbart utfall, hvilke krav det dekker),
|
||||
Kjente svakheter (table: svakhet, kompensasjon, tiltak), Formuleringer som
|
||||
virker, Profilgap.
|
||||
|
||||
The operator will supply an existing `kandidat.md`. Treat its structure as the
|
||||
contract and validate against it rather than regenerating it.
|
||||
|
||||
### 5.2 `saker/<sak-id>/sak.md`
|
||||
|
||||
Case front page. Every skill reads it; several write to it. `sak-id` format:
|
||||
`YYYY-MM-<arbeidsgiver-slug>-<rolle-slug>`.
|
||||
|
||||
Frontmatter: `sak_id`, `arbeidsgiver`, `rolle`, `kilde`, `url`, `opprettet`,
|
||||
`status`, `neste_frist`, `ventende_part` (`meg` | `dem` | `ingen`),
|
||||
`sist_aktivitet`, `score`, `cv_variant`, `soknad_versjon`.
|
||||
|
||||
Body: Hvorfor denne (from the decision reason), Status nå, Neste handling,
|
||||
Åpne spørsmål, Tidslinje (human-readable, generated from `logg.jsonl`).
|
||||
|
||||
### 5.3 `saker/<sak-id>/logg.jsonl`
|
||||
|
||||
Append-only events. `{ts, hendelse, kilde, ref, notat}`. Event vocabulary is a
|
||||
closed enum: `opprettet`, `soknad_sendt`, `bekreftelse_mottatt`,
|
||||
`henvendelse_mottatt`, `svar_sendt`, `intervju_avtalt`, `intervju_gjennomfort`,
|
||||
`tilbud_mottatt`, `avslag`, `trukket`, `stille`.
|
||||
|
||||
### 5.4 `beslutninger.jsonl`
|
||||
|
||||
Append-only, one JSON object per sourcing decision.
|
||||
|
||||
Fields: `id`, `dato`, `kilde`, `url`, `tittel`, `arbeidsgiver`, `beslutning`
|
||||
(`ja`|`nei`), `arsak` (1–3 values from closed enum), `notat` (max 15 words),
|
||||
`score_da` (integer, the score before the human decided), `korrigerer`
|
||||
(nullable id).
|
||||
|
||||
`arsak` enum: `lonn`, `geografi`, `arbeidsform`, `fagomrade`, `oppgavetype`,
|
||||
`teknologi`, `senioritetsniva`, `selskapstype`, `arbeidsgiverrykte`,
|
||||
`tidspunkt`, `konkurranse`, `annet`.
|
||||
|
||||
`score_da` is the learning signal. High score with `nei` means the profile is
|
||||
wrong about something; that divergence is what §5.7 mines.
|
||||
|
||||
### 5.5 `profil/cv/varianter.jsonl`
|
||||
|
||||
`{variant_id, fil, sak_id, sendt_dato, tilpasninger}`. The operator must be able
|
||||
to answer "which CV did they actually receive" before walking into an interview.
|
||||
|
||||
### 5.6 `referanser.md`
|
||||
|
||||
Per referee: name, relation, what they can vouch for, last asked, cases used on,
|
||||
consent status. Skills warn before proposing the same referee a third time
|
||||
inside six months.
|
||||
|
||||
### 5.7 Outcome records
|
||||
|
||||
Outcomes are appended to the case log **and** mirrored into
|
||||
`beslutninger.jsonl` as an outcome line so that learning sees entry and exit
|
||||
together. Rejection stage is captured explicitly: `screening`, `forste_intervju`,
|
||||
`andre_intervju`, `referansesjekk`, `tilbud_avslatt`. Screening rejections and
|
||||
post-interview rejections are different diagnoses — the first says the profile
|
||||
targets wrong, the second says the presentation does.
|
||||
|
||||
---
|
||||
|
||||
## 6. Status machine
|
||||
|
||||
Deterministic. Implement in `scripts/sak_status.py`, not in prose, so status is
|
||||
reproducible and testable.
|
||||
|
||||
States: `vurderer` → `soker` → `sendt` → `dialog` → `intervju` → `tilbud` →
|
||||
`avsluttet`. Terminal side-states: `avslag`, `trukket`.
|
||||
|
||||
Rules:
|
||||
|
||||
- Every state change appends to `logg.jsonl` and updates `sak.md` frontmatter.
|
||||
- `ventende_part` is recomputed on every transition. It drives the daily view.
|
||||
- Silence thresholds: `sendt` with no inbound for 14 days → flag. `dialog` with
|
||||
`ventende_part: dem` and no inbound for 7 days → flag. `intervju`
|
||||
gjennomført with no outcome after 10 days → flag.
|
||||
- Flags surface in `dagens`. Nothing auto-escalates, auto-sends, or auto-closes.
|
||||
- Never invent a transition from inferred email sentiment. State changes on
|
||||
explicit operator confirmation or on an unambiguous structural signal.
|
||||
|
||||
---
|
||||
|
||||
## 7. Skills
|
||||
|
||||
Each skill directory gets `SKILL.md` plus `references/` where the schema is
|
||||
non-trivial. For each, the frontmatter `description` must name the trigger
|
||||
phrases in Norwegian.
|
||||
|
||||
| Skill | Purpose | Writes |
|
||||
|---|---|---|
|
||||
| `kandidatprofil` | Read, validate, edit `kandidat.md`. Enforces frontmatter schema. | `profil/kandidat.md` |
|
||||
| `annonse-uttrekk` | The **only** skill that touches the browser. Reads an open listing, guards it, writes `annonse.md` snapshot. | snapshot |
|
||||
| `kandidatvurdering` | Score a snapshot against `kandidat.md`. Hard filters first, then weighted judgement. Outputs score + reasoning + concerns. | nothing |
|
||||
| `beslutning` | Record ja/nei with enum reason. On `ja`, hands off to `sak`. | `beslutninger.jsonl` |
|
||||
| `sak` | Create and maintain case folders. Owns the status machine. | case folder |
|
||||
| `korrespondanse` | Ingest inbound email, guard it, match to case, file it, append event. | `korrespondanse/`, `logg.jsonl` |
|
||||
| `soknad` | Draft application from snapshot + profile + signature projects. Versioned, never sent. | `soknad-vN.md` |
|
||||
| `intervju` | Produce likely questions, three weakest points with prepared answers, questions to ask them. | `intervju/` |
|
||||
| `referanser` | Maintain referee ledger, warn on overuse. | `referanser.md` |
|
||||
| `forhandling` | Prepare negotiation using floor from profile and BATNA from live pipeline. | `intervju/forhandling.md` |
|
||||
| `dagens` | Daily operating view: what needs action, what has gone silent, upcoming deadlines. | nothing |
|
||||
| `utfall` | Record outcome with rejection stage. | logs |
|
||||
| `laering` | The profile-update loop. Explicit invocation only. | proposes diffs |
|
||||
| `datahygiene` | Export, archive, and delete case data including third-party personal data. | archive/delete |
|
||||
|
||||
### 7.1 `annonse-uttrekk` specifics
|
||||
|
||||
Extract: title, employer, org number if present, location, deadline, employment
|
||||
type, stated requirements (split must-have / nice-to-have), full listing body,
|
||||
contact person, listing URL, capture timestamp.
|
||||
|
||||
The snapshot is essential because listings are deleted — usually right when
|
||||
interview prep needs them.
|
||||
|
||||
DOM selectors are the only brittle surface in the system. Isolate every selector
|
||||
in `references/selectors.md` with a fallback strategy, so a site change is a
|
||||
one-file fix and never touches scoring or drafting.
|
||||
|
||||
### 7.2 `laering` specifics
|
||||
|
||||
- Never runs automatically.
|
||||
- Requires ≥10 total decisions before proposing anything.
|
||||
- Reads the last 20 decisions plus all recorded outcomes.
|
||||
- Proposes only on clear signal: same `arsak` in ≥4 of 20, or ≥3 cases of
|
||||
`score_da` > 70 paired with `nei`.
|
||||
- **Never tightens hard filters automatically.** Salary floor, geography and
|
||||
absolute nos change only by direct operator edit. A run of salary rejections
|
||||
is as likely to mean a weak market as a wrong floor.
|
||||
- If the last 20 contain fewer than 2 `ja`, propose widening the search rather
|
||||
than narrowing the profile. A loop that learns only from rejection converges
|
||||
on a profile that rejects everything.
|
||||
- Every proposal lists the decision ids it rests on.
|
||||
|
||||
---
|
||||
|
||||
## 8. MCP servers
|
||||
|
||||
`.mcp.json` declares two, both local, both least-privilege.
|
||||
|
||||
1. **Browser** — Playwright MCP attached over CDP to an existing Chrome instance
|
||||
started with `--remote-debugging-port=9222` against the operator's normal
|
||||
profile. Do not launch a fresh browser; do not manage profiles. Document the
|
||||
launch command in `README.md`. Configure with navigation and read tools only.
|
||||
|
||||
2. **Mail** — read-only IMAP against a dedicated folder the operator filters
|
||||
into. Credentials via environment variables, never in the repo. Matching is
|
||||
by employer domain plus thread key. Read scope only: the plugin must be
|
||||
incapable of sending.
|
||||
|
||||
Both are optional at install time. Every skill degrades to manual paste when its
|
||||
server is absent, and the plugin must be fully usable that way.
|
||||
|
||||
---
|
||||
|
||||
## 9. Ingestion security
|
||||
|
||||
Wire `llm-ingestion-guard`
|
||||
(`git+https://git.fromaitochitta.com/open/llm-ingestion-pipeline-security.git`)
|
||||
into `scripts/guard_ingest.py`. Pin an exact release tag; do not vendor the
|
||||
source.
|
||||
|
||||
Guard placement — the trust boundary is the **write**, because a poisoned
|
||||
`annonse.md` is read weeks later by `intervju` as trusted context, and that
|
||||
reader has no idea where the text came from:
|
||||
|
||||
```
|
||||
untrusted text → prepare_input() → tool-less model transform → screen_output() → persist
|
||||
```
|
||||
|
||||
Requirements:
|
||||
|
||||
- The extraction/transform step runs **tool-less**. A successful injection then
|
||||
has nothing to act with.
|
||||
- `FAIL_SECURE` halts the write and alerts with a gate code only, never content.
|
||||
- Apply to both listing bodies and inbound email bodies. Email is the higher-risk
|
||||
channel: arbitrary senders, and the operator is primed to trust it.
|
||||
- Expect false positives. Job listings are dense in the constructs the gate
|
||||
grades. Route non-WARN dispositions to a review queue that shows the operator
|
||||
the flagged region and asks for a decision — do not silently drop, and do not
|
||||
silently persist.
|
||||
- Capability isolation: the skill that reads the browser holds no mail
|
||||
credential, and vice versa.
|
||||
|
||||
---
|
||||
|
||||
## 10. Visual output
|
||||
|
||||
Cowork renders rich output well; use it, but keep files as the source of truth.
|
||||
|
||||
- `dagens` renders a self-contained HTML dashboard: pipeline by state, actions
|
||||
due, silence flags, deadlines. Regenerated on each run from the case files.
|
||||
It is a view, never a store.
|
||||
- Optional xlsx export of the pipeline for operators who want it in a
|
||||
spreadsheet.
|
||||
- Every visual must be reproducible from the workspace files alone.
|
||||
|
||||
---
|
||||
|
||||
## 11. Build order
|
||||
|
||||
Ship each milestone working before starting the next.
|
||||
|
||||
**M1 — Spine.** `plugin.json`, workspace scaffold, `kandidatprofil`,
|
||||
`kandidatvurdering` against pasted listing text. No browser, no mail. Proves
|
||||
scoring works before any automation exists.
|
||||
|
||||
**M2 — Case folders.** `sak`, status machine in `sak_status.py`, `beslutning`,
|
||||
`dagens` as text output. The system now remembers.
|
||||
|
||||
**M3 — Ingestion.** `annonse-uttrekk` with browser MCP, `guard_ingest.py`,
|
||||
snapshotting. First untrusted input enters the system, already guarded.
|
||||
|
||||
**M4 — Communication.** `korrespondanse`, mail MCP, silence flags wired into
|
||||
`dagens`. "Everything in one place" becomes true here.
|
||||
|
||||
**M5 — Output.** `soknad`, `intervju`, `referanser`, CV variant tracking.
|
||||
|
||||
**M6 — Learning.** `utfall`, `laering`, HTML dashboard, `forhandling`,
|
||||
`datahygiene`.
|
||||
|
||||
---
|
||||
|
||||
## 12. Testing
|
||||
|
||||
- Fixture corpus of anonymised listings and emails under `tests/fixtures/`.
|
||||
Every skill except `annonse-uttrekk` must be testable with zero network.
|
||||
- `sak_status.py` gets real unit tests: every transition, every silence
|
||||
threshold, every recomputation of `ventende_part`.
|
||||
- Adversarial fixtures: a listing with an embedded instruction override, an
|
||||
email with a zero-click exfil carrier, a listing with hidden Unicode. Assert
|
||||
the guard halts and nothing is persisted.
|
||||
- Golden-file tests for `kandidat.md` frontmatter validation.
|
||||
- No test may require the operator's real data.
|
||||
|
||||
---
|
||||
|
||||
## 13. Non-goals
|
||||
|
||||
Submitting applications. Sending email. Any write to Finn.no or LinkedIn.
|
||||
Credential storage. Background jobs. Multi-user support. Recruiter-side
|
||||
features. Bulk listing harvest — the system reads listings the operator is
|
||||
looking at, not the database behind them.
|
||||
|
||||
---
|
||||
|
||||
## 14. Decisions to confirm with the operator before M1
|
||||
|
||||
Ask these; do not assume defaults.
|
||||
|
||||
1. Workspace path, and whether it is a git repository (if yes, `.gitignore`
|
||||
must exclude case folders containing third-party personal data).
|
||||
2. Which mail account and folder, or whether M4 is deferred entirely.
|
||||
3. Whether the scoring weights are fixed in the skill or configurable in
|
||||
`kandidat.md` frontmatter.
|
||||
4. Retention: how long archived cases are kept before `datahygiene` proposes
|
||||
deletion.
|
||||
5. Whether the plugin repo ships with a redacted example workspace for other
|
||||
users, or workspace-shaped documentation only.
|
||||
Loading…
Add table
Add a link
Reference in a new issue