Compare commits
16 Commits
7a264f1175
...
58f330fdf3
| Author | SHA1 | Date |
|---|---|---|
|
|
58f330fdf3 | |
|
|
bd7615d962 | |
|
|
0879ef10ae | |
|
|
d303b957f5 | |
|
|
b0a21d6211 | |
|
|
43932d40ab | |
|
|
3708c0099d | |
|
|
f9a81a17c5 | |
|
|
9415eb0d65 | |
|
|
49c8f5eace | |
|
|
46b0584643 | |
|
|
90db409721 | |
|
|
15807ef342 | |
|
|
ef8b7e4a52 | |
|
|
46ebfb1b2f | |
|
|
1ab1bf5f8c |
38
CLAUDE.md
38
CLAUDE.md
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this
|
||||
repository. It is orientation only — implementation history, component details, and
|
||||
operational procedures live in [docs/implementation-status.md](docs/implementation-status.md)
|
||||
(read it on demand, e.g. before touching a plugin or claiming something isn't built yet).
|
||||
operational procedures live in the [docs/implementation-status.md](docs/implementation-status.md)
|
||||
index and its `docs/implementation-status/` leaf files (read on demand, e.g. before touching
|
||||
a plugin or claiming something isn't built yet).
|
||||
|
||||
## What this repository is
|
||||
|
||||
|
|
@ -32,9 +33,10 @@ topic you need.
|
|||
is the architecture, `03-architecture-decisions.md` is the historical ADR log (see
|
||||
`docs/adr/` above), `04-build-plan.md` the build outline with step status,
|
||||
`06-graphify-evaluation.md` justifies the Graphify pivot.
|
||||
- **`docs/implementation-status.md`** — the status timeline, full per-component detail for
|
||||
every shipped plugin (os-vault, os-orchestration, os-status, os-doc-hygiene, os-adr),
|
||||
eval-harness records, and plugin rename/cache-refresh procedures.
|
||||
- **`docs/implementation-status.md`** — the status index: headline timeline + per-component
|
||||
one-liners. Full detail (per-plugin build history, eval-harness records) lives in leaf
|
||||
files under `docs/implementation-status/`, read on demand; plugin rename/cache-refresh
|
||||
runbooks are in `docs/implementation-status/operational-procedures.md`.
|
||||
- **`docs/graphify/`** — verified handbook for the Graphify knowledge-graph tool. Skim
|
||||
`00-README.md` for the model; keep `09-best-practices-checklist.md` open when running it.
|
||||
Claims are provenance-tagged (`[github]` trustworthy; `[interview]` / `[unverified claim]`
|
||||
|
|
@ -69,16 +71,19 @@ is deferred to last (ADR-013).
|
|||
|
||||
## Implemented components (inventory)
|
||||
|
||||
All build-plan steps required for the memory system are complete (2026-06-15). Full detail
|
||||
for each item is in [docs/implementation-status.md](docs/implementation-status.md) — read it
|
||||
All build-plan steps required for the memory system are complete (2026-06-15).
|
||||
[docs/implementation-status.md](docs/implementation-status.md) is the index; full detail per
|
||||
item is in its leaf files under `docs/implementation-status/` — read the relevant leaf
|
||||
before modifying any of these:
|
||||
|
||||
- **os-vault** (`plugins/os-vault/`) — vault write/query/reorganize/onboard-project/
|
||||
design-template skills, SessionStart/End hooks, memsearch + vault git sync. Write-behavior
|
||||
eval harness in `plugins/os-vault/eval/`.
|
||||
- **os-orchestration** (`plugins/os-orchestration/`) — injects `ORCHESTRATION.md`
|
||||
(session-orchestration + delegation-economics rules) into all sessions; this repo carries
|
||||
no local override. Eval harness in `plugins/os-orchestration/eval/`.
|
||||
- **os-context** (`plugins/os-context/`, renamed 2026-07-13 from os-orchestration) — a
|
||||
prompt-composer SessionStart plugin: concatenates `prompts/session-start/*.md` (currently
|
||||
just `10-orchestration.md`, the session-orchestration + delegation-economics rules) into
|
||||
one additionalContext block for all sessions; this repo carries no local override. Eval
|
||||
harness in `plugins/os-context/eval/`.
|
||||
- **os-status** (`plugins/os-status/`) — aggregated deterministic SessionStart checks
|
||||
(subagent-model env override, ADR system present, vault hub note present). ADR-022.
|
||||
- **os-doc-hygiene** (`plugins/os-doc-hygiene/`) — stale/bloated-doc monitoring;
|
||||
|
|
@ -131,10 +136,13 @@ OpenSpec artifacts comes from `docs/` and this file.
|
|||
you make such a choice → record it with `/os-adr:create`. A task that reverses an Accepted
|
||||
ADR is not complete until the superseding ADR exists.
|
||||
- **Keep records current:** when a build step completes, (a) mark it done in
|
||||
`docs/memory-system/04-build-plan.md`, (b) record it in
|
||||
`docs/implementation-status.md` (timeline entry + component pointer), and (c) update the
|
||||
design paragraph in THIS file only if the design itself changed. Never append status
|
||||
history to this file — it goes in implementation-status.md.
|
||||
`docs/memory-system/04-build-plan.md`, (b) record it per the index+progressive-disclosure
|
||||
convention: a **one-line** headline entry in `docs/implementation-status.md` plus the
|
||||
detail in the relevant `docs/implementation-status/<component>.md` leaf file (create one
|
||||
if needed; don't restate what an ADR already records), and (c) update the design paragraph
|
||||
in THIS file only if the design itself changed. Never append status history to this file,
|
||||
and never let the index file regrow into a changelog — the os-doc-hygiene `file_length`
|
||||
signal (400 lines / ~4k tokens) enforces this.
|
||||
- **Plugin and skill naming:** before naming ANY new cc-os plugin, skill, or slash command,
|
||||
Read `~/Documents/SecondBrain/cc-os-plugin-skill-naming-convention.md` and follow it. In
|
||||
brief: plugins are `os-[domain]`; skills are verb-first kebab-case, invoked as
|
||||
|
|
@ -142,4 +150,4 @@ OpenSpec artifacts comes from `docs/` and this file.
|
|||
in SKILL.md frontmatter** (it collapses the slash command to a bare unnamespaced form).
|
||||
- **After editing any plugin source** (SKILL.md, hooks, CLIs), run `bin/refresh-plugins` —
|
||||
source edits don't reach sessions until the cache is refreshed. Rename/move procedure and
|
||||
cache details: docs/implementation-status.md → "Operational procedures".
|
||||
cache details: `docs/implementation-status/operational-procedures.md`.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
---
|
||||
id: "0019"
|
||||
date: 2026-07-03
|
||||
status: Accepted
|
||||
status: Superseded
|
||||
supersedes:
|
||||
superseded-by:
|
||||
superseded-by: "0031"
|
||||
affected-paths: []
|
||||
affected-components: []
|
||||
migration_confidence: medium
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ The `planka-api` gem's tick/digest delivery was specified as Pushover-direct (`P
|
|||
|
||||
Apprise replaces the originally planned Pushover-direct integration as the single notification transport, deployed as an Apprise API server so any service (Discord, Telegram, Bark, Home Assistant, etc.) can be targeted via configuration rather than new code, with Planka's native Apprise support converging on the same server. This iteration's scope stays minimal (server + a minimal Apprise client swapped in for the planned Pushover gem code), with a fuller client gem, CLI wrapper, and an os-notify plugin captured as later backlog work; the underlying notification-blindness policy is unchanged.
|
||||
|
||||
_Addendum 2026-07-13:_ the fuller client gem and CLI wrapper backlog items are done — `~/dev/ruby-gems/apprise-api` (gem `apprise-api`, module `Apprise`, v0.2.0) is the sole Apprise client, now including the `apprise-notify` CLI executable. The interim minimal client (`~/dev/ruby-gems/apprise`, gem `apprise-client`) was retired and deleted after live verification and a no-dependents check. Remaining backlog: the `os-notify` plugin and multi-service routing/Planka-native wiring.
|
||||
|
||||
## Alternatives rejected
|
||||
|
||||
**Pushover gem as planned** — single-service dead end; every future service would be a new integration. **Deferring Apprise entirely** — the server + minimal client is comparable effort to the Pushover path, so there is no cheaper interim.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
id: "0030"
|
||||
date: 2026-07-13
|
||||
status: Accepted
|
||||
supersedes:
|
||||
superseded-by:
|
||||
affected-paths: [plugins/os-shortcuts/]
|
||||
affected-components: [os-shortcuts, plan-mode]
|
||||
---
|
||||
|
||||
# 0030 — Read-only work uses Claude Code plan mode; no /readonly shortcut command
|
||||
|
||||
## Context
|
||||
|
||||
The 2026-07-09 AI-workflow audit (finding #2) showed the user typed 'read-only, do NOT change anything' near-verbatim 15+ times. A backlog card proposed either documenting a convention or building a /readonly skill in os-shortcuts. Claude Code plan mode already enforces read-only at the permission layer, while a prompt or skill saying 'don't change anything' is only advisory; os-shortcuts has no mechanism to switch the client into plan mode or enforce filesystem permissions, so a /readonly slash command would imply stronger enforcement than it actually has.
|
||||
|
||||
## Decision
|
||||
|
||||
The convention for read-only tasks is to start them in Claude Code plan mode (shift+tab / --permission-mode plan). No /readonly skill is built in os-shortcuts. Per-task read-only expectations are expressed by entering plan mode, not by injected or skill-carried phrasing.
|
||||
|
||||
## Consequences
|
||||
|
||||
Easier: read-only is enforced at the permission layer rather than trusted to prompt compliance; no new skill to maintain or cache-refresh; the os-shortcuts surface stays small. Harder: the user must remember the plan-mode keybinding instead of a memorable slash command; any standing read-only phrasing beyond 'no changes' (reporting expectations, output format) still has to be typed per task or captured elsewhere.
|
||||
|
||||
## Alternatives rejected
|
||||
|
||||
1) Build /os-shortcuts:readonly as a thin skill that instructs the model to behave read-only and states the user's expectations - rejected: skills cannot activate plan mode, so the command would be advisory-only while its name implies enforcement; a false sense of safety is worse than the keybinding. 2) Inject standing read-only rules via os-context session-start prompts - rejected: read-only is a per-task property, not an always-true rule; injecting it globally would contradict sessions that are supposed to write.
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
id: "0031"
|
||||
date: 2026-07-13
|
||||
status: Accepted
|
||||
supersedes: "0019"
|
||||
superseded-by:
|
||||
affected-paths: [plugins/os-context/, plugins/os-status/hooks/checks.py]
|
||||
affected-components: [os-context, os-orchestration, os-status]
|
||||
---
|
||||
|
||||
# 0031 — os-context prompt-composer plugin supersedes single-file os-orchestration
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0019 shipped os-orchestration, a global plugin whose SessionStart hook injected one hardcoded file (ORCHESTRATION.md). The 2026-07-09 AI-workflow audit surfaced further always-true context worth injecting (kickoff conventions, standing safety rules, decision-memo format), and a one-file plugin cannot host multiple independently maintained rule sets or enforce an injection budget across them.
|
||||
|
||||
## Decision
|
||||
|
||||
Rename/generalize os-orchestration into os-context (plugins/os-context/): the SessionStart hook is a composer that globs prompts/session-start/*.md, concatenates in filename order (numeric prefixes), and emits one additionalContext block. ORCHESTRATION.md became prompts/session-start/10-orchestration.md with byte-identical injected output. Deterministic line budgets are enforced in the composer (warn at 120 injected lines, refuse at 240). Future event directories (e.g. prompts/user-prompt-submit/) are anticipated by directory naming but only session-start is implemented; no prompts/session-end/ directory will exist because SessionEnd hooks cannot inject model-visible context (ADR-0028 context). The audit-sessions skill moved to /os-context:audit-sessions and its state dir to ~/.local/state/os-context (old path symlinked); os-status references updated in the same change.
|
||||
|
||||
## Consequences
|
||||
|
||||
Easier: new standing rules ship as small numbered prompt files instead of edits to one monolith; injection cost is bounded by an enforced budget; the plugin name matches its actual domain (session context, not just orchestration). Harder: one-time identity cutover across marketplace/settings/installed-plugin surfaces; historical docs and frozen eval results still say os-orchestration and must be read with the rename note in mind; the OS_ORCH_AUDIT_LEDGER env var name is retained for compatibility despite the rename.
|
||||
|
||||
## Alternatives rejected
|
||||
|
||||
1) Keep os-orchestration and append new rules to ORCHESTRATION.md - rejected: one monolithic file with no per-ruleset ownership or ordering, and no budget enforcement. 2) Separate plugin per rule set - rejected: each global plugin adds cache-refresh and sync overhead; these rule sets share one event and one budget. 3) Put the new rules in ~/.claude/CLAUDE.md - rejected: CLAUDE.md is not compaction-safe the way the SessionStart matcher (startup|resume|clear|compact) is, and the single-source rule keeps injected rules in exactly one place.
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
id: "0032"
|
||||
date: 2026-07-13
|
||||
status: Accepted
|
||||
supersedes:
|
||||
superseded-by:
|
||||
affected-paths: [docs/implementation-status.md, docs/implementation-status/, plugins/os-doc-hygiene/scripts/scanner.py, CLAUDE.md]
|
||||
affected-components: [docs, os-doc-hygiene]
|
||||
---
|
||||
|
||||
# 0032 — AI-maintained knowledge files are indexes with progressive disclosure
|
||||
|
||||
## Context
|
||||
|
||||
AI-read markdown files grow unbounded when maintained append-only: docs/implementation-status.md reached 729 lines (~9k estimated tokens) and was read routinely at main-loop rates, paying the full context cost on every read even when only one component's status was needed. The os-doc-hygiene scanner gained a deterministic `file_length` signal (2026-07-13, issue #25 part 1; defaults 400 lines / 4000 estimated tokens) that now flags every oversized doc, but a signal without a target structure only reports the problem. The same growth pattern threatens CLAUDE.md and any other file the AI is instructed to keep current.
|
||||
|
||||
## Decision
|
||||
|
||||
AI-maintained knowledge files are **indexes with progressive disclosure**: the main file holds only headline entries and one-liners with pointers; full detail lives in leaf files (e.g. `docs/implementation-status/<component>.md`) read on demand. Detail that merely restates a decision already recorded in `docs/adr/` is removed, not moved — the ADR is the durable record. Operational runbooks referenced from CLAUDE.md land in a stable leaf file (`docs/implementation-status/operational-procedures.md`), never silently dropped. The os-doc-hygiene `file_length` signal (400 lines / ~4k tokens) is the enforcement mechanism: a flagged AI-maintained file is distilled back to index form rather than left to grow. Applied first to docs/implementation-status.md (2026-07-13); applies later to CLAUDE.md cleanup (see the os-context prompt-files card).
|
||||
|
||||
## Consequences
|
||||
|
||||
Easier: routine reads cost one small index instead of the whole history; per-component detail is fetched only when working on that component; the length signal makes drift objective and cheap to catch; ADRs stop being duplicated in prose. Harder: recording a completed step now touches two files (one-line index entry + leaf detail); cross-file links must be kept unbroken (a link-integrity pass belongs to any restructure); readers must follow a pointer for depth instead of scrolling one file.
|
||||
|
||||
## Alternatives rejected
|
||||
|
||||
1) Keep append-only files and rely on the classifier's bloat/distill/split taxonomy downstream — rejected: oversized files were only caught if some other signal fired, and by then the context cost had already been paid for weeks. 2) Periodic manual pruning of the monolith with no structural convention — rejected: pruning decisions are re-litigated every time and history gets lost inconsistently; an index+leaf structure makes the "where does this go" decision mechanical. 3) Move stale sections into leaf files too (archive everything) — rejected: detail already recorded in an ADR is duplication; the ADR system exists to be the durable decision record.
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
id: "0033"
|
||||
date: 2026-07-13
|
||||
status: Accepted
|
||||
supersedes:
|
||||
superseded-by:
|
||||
affected-paths: [plugins/os-backlog/skills/route/SKILL.md, plugins/os-backlog/hooks/session_start.py, plugins/os-backlog/bin/os-backlog, .cc-os/config]
|
||||
affected-components: [os-backlog, to-issues]
|
||||
---
|
||||
|
||||
# 0033 — Tracker routing: Planka is state, git issues are specs
|
||||
|
||||
## Context
|
||||
|
||||
The backlog pilot plan (vault note `vault-backlog-pilot-plan.md`) originally routed everything to Forgejo issues, later amended informally to "Forgejo = code work in a repo; Planka = everything else." The os-backlog slices then shipped a sharper boundary in practice — encoded in `plugins/os-backlog/skills/route/SKILL.md`, the `/to-issues` destination-tracker step (global skill `~/.agents/skills/to-issues/SKILL.md`, step 5), and the `plugins/os-backlog/hooks/session_start.py` injection note — but no ADR canonizes it, so the rule lives only in skill prose and could drift or be silently edited away. This ADR records already-implemented practice; it does not introduce new behavior.
|
||||
|
||||
## Decision
|
||||
|
||||
Canonize the tracker-routing boundary, amending the pilot plan's "Forgejo for everything" rule. (1) Boundary rule: Planka = task STATE (backlog/next/doing/waiting/review/done — what's happening now); git issues = durable SPECS (text that must survive and be referenced: features, migrations, architecture-level work, tracer-bullet slices from /to-issues, PRDs); `repo:<path>` is the escape hatch for projects that already track issues in-repo and want to stay there. (2) Each project declares exactly one destination via the `tracker` key in `.cc-os/config`, grammar: `planka:<board>` | `forgejo:<owner>/<repo>` | `github:<owner>/<repo>` | `repo:<path>` (validated by `os-backlog config-write`). (3) Card-as-pointer + issue-chain-as-spec: when a code effort needs both state tracking and a durable spec, the Planka card is a pointer (title + link) into the git issue chain holding the spec — never duplicate spec text into a card description. (4) Promotion rule: when a Planka-tracked effort accretes significant code/design decisions in card comments, that is the signal to promote — open a git issue (or chain) for the durable spec and turn the card into a pointer; the AI flags promotion to the user, never performs it silently. Operational detail lives in the cited sources (route SKILL.md, to-issues step 5, session_start.py note) and is not restated here; column-ownership rules are ADR-0029's.
|
||||
|
||||
## Consequences
|
||||
|
||||
Easier: one recorded boundary the route skill, /to-issues, and the SessionStart note can all be audited against; changing the boundary now requires a superseding ADR rather than a quiet skill edit; the pilot plan's informal amendment has a durable home. Harder: two tracking surfaces per code project (board + issue tracker) instead of one, mitigated by the pointer pattern and back-links; promotion is a judgment call the AI can only flag, so under-flagging is a live risk (measured via the session-audit rubric).
|
||||
|
||||
## Alternatives rejected
|
||||
|
||||
1) Keep "Forgejo for everything" (original pilot plan) — rejected: process/coordination/household-style tasks have no repo home and card state (columns, WIP) is exactly what issue trackers do badly. 2) Planka for everything, specs in card descriptions — rejected: card descriptions are not durable, linkable, or commit-referenceable; specs need git-issue permanence. 3) Leave the rule in skill prose only, no ADR — rejected: prose rules drifting out of sync with enforcement is precisely what ADR-0029 fixed for column moves; the routing boundary deserves the same durability.
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
id: "0034"
|
||||
date: 2026-07-13
|
||||
status: Accepted
|
||||
supersedes:
|
||||
superseded-by:
|
||||
affected-paths: [plugins/os-backlog/lib/backlog/project_index.rb, plugins/os-backlog/bin/os-backlog, plugins/os-backlog/hooks/session_start.py, plugins/os-backlog/skills/route/references/cross-project-filing.md]
|
||||
affected-components: [os-backlog]
|
||||
---
|
||||
|
||||
# 0034 — Cross-project filing: file-dont-fix with a derived global project index
|
||||
|
||||
## Context
|
||||
|
||||
Work discovered mid-session often belongs to a different project than the one being worked on (issue #27). Editing the other project from the wrong session loses context and bypasses its tracker; but filing requires knowing which tracker that project uses, which until now lived only in its per-project .cc-os/config (ADR-0027).
|
||||
|
||||
## Decision
|
||||
|
||||
File-don't-fix: a session never edits another project — it files an issue/card on that project's own tracker. To make the target tracker discoverable, os-backlog maintains a derived global project index at ~/.cc-os/projects.json, upserted as a side effect of every successful config-write and queried via `os-backlog projects`. This is deliberately GLOBAL state, distinct from the per-project .cc-os/ state of ADR-0027; it is derived and rebuildable (re-running route/config-write regenerates rows). Filed items carry a Discoverer metadata block (filing repo/path, Claude Code session id, date, one-line motivation); the session id comes from the SessionStart hook stdin JSON or the transcript filename and is NEVER fabricated — write "session id unavailable" if unknown. Routing inside the target project still follows its own boundary rule (ADR-0033). Filing mechanics live in plugins/os-backlog/skills/route/references/cross-project-filing.md, not restated here.
|
||||
|
||||
## Consequences
|
||||
|
||||
One compact CROSS-PROJECT rule line added to the os-backlog SessionStart note. Index rows only exist for projects that have run config-write; unrouted projects are reported, not guessed. Related: issue #27, ADR-0027, ADR-0029, ADR-0033.
|
||||
|
||||
## Alternatives rejected
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
id: "0035"
|
||||
date: 2026-07-13
|
||||
status: Accepted
|
||||
supersedes:
|
||||
superseded-by:
|
||||
affected-paths: [plugins/os-backlog/bin/wakeup-poll, plugins/os-backlog/lib/backlog/wakeup.rb]
|
||||
affected-components: [os-backlog]
|
||||
---
|
||||
|
||||
# 0035 — Issue-triggered project AI wakeup via polling, not webhooks
|
||||
|
||||
## Context
|
||||
|
||||
Issue #28 (design spike, blocked-by #27/ADR-0034): when an issue is filed with a project — often via cross-project file-don't-fix — that project's AI should be awakened to triage or act, instead of waiting for the human to open a session. The spike must pick a trigger mechanism, autonomy guardrails, credential scope, and context handoff. The human pre-chose the direction: polling trigger + tmux execution surface (ADR-0036 records the tmux convention).
|
||||
|
||||
## Decision
|
||||
|
||||
Trigger is a poller, not webhooks: a cron/heartbeat script (pilot: `plugins/os-backlog/bin/wakeup-poll`, one pass per invocation, no cron installed yet) scans opted-in projects for NEW open issues. (1) Scope: rows of the global project index `~/.cc-os/projects.json` (ADR-0034) whose tracker is `forgejo:` or `github:`, AND whose per-project `.cc-os/config` carries the explicit opt-in marker `wakeup=true` (the index row path locates the config; a later config-write slice may mirror the flag into the row — the config file remains the authority). Planka/repo-tracked projects are never polled. (2) New-ness: last-seen state persisted at `~/.cc-os/wakeup-state.json` (per-repo highest issue index seen); first scan of a repo baselines without waking anything. (3) Autonomy, in ADR-0029 vocabulary via issue labels: `afk-ready` -> spawn an acting session; `semi` -> spawn a triage-only session (label + comment, stop at proposing — never implement); anything else (hitl or unlabeled) -> notify-only. Notification policy v2 (pull-only) applies: the scan appends to a summary the human pulls (`~/.cc-os/wakeup-summary.md`) — no push notifications. (4) Credentials: the poller lists issues through tea's existing login only; it NEVER reads `~/.credentials` contents; spawned sessions get Planka creds the established way (source in a subshell). (5) Context handoff: the spawned session's initial prompt carries the issue number, title, URL, and the Discoverer block if present (ADR-0034), plus its autonomy tier. (6) Execution surface is a tmux session per ADR-0036.
|
||||
|
||||
## Consequences
|
||||
|
||||
Easier: host-agnostic (works for Forgejo and GitHub identically through tea/gh), no listener endpoint, no inbound network surface, trivially disabled (remove the cron line or the wakeup flag). Harder: latency is the poll interval, and the poller is per-machine state — two machines polling would double-wake (accepted for the single-operator pilot; state file is per-host). Non-goals, explicitly: no webhooks; no auto-merge/auto-close; no acting on hitl/unlabeled issues; wakened sessions still obey file-don't-fix (ADR-0034) — they never edit other projects; no bulk backfill of pre-existing open issues (baseline-then-watch).
|
||||
|
||||
## Alternatives rejected
|
||||
|
||||
1) Forgejo webhooks -> local listener — rejected for the first cut: needs an always-up endpoint reachable from the Forgejo host, doesn't cover GitHub without a second mechanism, and adds an inbound attack surface; polling via tea reuses existing credentials and works everywhere. Webhooks remain the natural upgrade if poll latency ever matters. 2) Opt-in as a field only in the global index row — rejected: the index is derived and rebuildable (ADR-0034); a rebuild would silently drop opt-ins. Authority lives in the per-project .cc-os/config (ADR-0027). 3) Push notifications per new issue (Apprise, ADR-0024) — rejected here: notification policy v2 is pull-only for ambient status; push stays reserved for the human explicitly asking.
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
id: "0036"
|
||||
date: 2026-07-13
|
||||
status: Accepted
|
||||
supersedes:
|
||||
superseded-by:
|
||||
affected-paths: [plugins/os-backlog/bin/wakeup-poll, plugins/os-backlog/lib/backlog/wakeup.rb]
|
||||
affected-components: [os-backlog]
|
||||
---
|
||||
|
||||
# 0036 — tmux session convention for AI-run sessions: cc-<project>-<purpose>
|
||||
|
||||
## Context
|
||||
|
||||
Issue #28 chose tmux (not headless claude -p / Agent SDK) as the execution surface for issue-triggered wakeup (ADR-0035), because the human must be able to discover, inspect, and take over an AI-run session. The naming/lifecycle convention is useful beyond wakeup — any long-running or unattended AI session benefits — so it is recorded as its own ADR. No existing tmux naming habit to preserve (no tmux server was running at spike time).
|
||||
|
||||
## Decision
|
||||
|
||||
(1) Naming: AI-run sessions are named `cc-<project>-<purpose>[-<n>]` — `cc-` prefix marks Claude-operated sessions, `<project>` is the index-row project name (ADR-0034), `<purpose>` is short kebab-case (wakeup sessions use `issue<N>`, e.g. `cc-llf-schema-issue12`), `-<n>` only disambiguates collisions. Human personal sessions never take the `cc-` prefix. (2) Creation: whoever needs the session creates it — the wakeup poller (ADR-0035) for issue wakeups, a human for anything else — via `tmux new-session -d -s <name> -c <project-root> ...` so the pane starts in the project. (3) Lifecycle: a session is killed after clean completion once its closing artifact exists (for wakeups: the triage/summary comment posted on the issue); it is kept alive on error or whenever hitl input is needed, so the evidence and the takeover point are the same pane. Discovery is `tmux ls` — the `cc-` prefix makes AI sessions self-listing; an os-backlog-adjacent listing command is an explicit non-goal for now. (4) Attach/takeover: attaching pauses nothing — the pane runs an interactive Claude Code session, so a human who attaches can simply type into it; there is no separate handover protocol.
|
||||
|
||||
## Consequences
|
||||
|
||||
Easier: one glanceable namespace (`tmux ls | grep ^cc-`) for everything the AI is running on a machine; takeover is native tmux attach, no tooling. Harder: kill-on-clean-completion means a successful session leaves no scrollback — the issue comment is the record, so sessions must actually post it before exiting (the wakeup prompt makes this the final required step); session names carry no timestamp, so a stale kept-alive session must be triaged by attaching, not by name.
|
||||
|
||||
## Alternatives rejected
|
||||
|
||||
1) Headless execution (claude -p / Agent SDK) — rejected by the human for this stage: no inspection or takeover surface; may return later for pure afk-ready work once trust is established. 2) Timestamped session names (cc-<project>-20260713-...) — rejected: names become unreadable in tmux ls and collisions are rare enough for -<n>. 3) Keep every completed session for audit — rejected: the durable record belongs on the issue/card (ADR-0033 spirit), and dead sessions accumulate into noise.
|
||||
|
|
@ -25,7 +25,7 @@ One file per decision, `NNNN-kebab-title.md`, created via `/os-adr:create`.
|
|||
| 0016 | [Memory plugin sourced from cc-os git repo; bash→Python deep-module port; memsearch-sync split; symlink cutover](0016-memory-plugin-sourced-from-cc-os-git-repo-bash-python-deep-module-port-memsearch-sync-split-symlink-cutover.md) | Accepted | 2026-06-12 |
|
||||
| 0017 | [Project graph onboarding assesses the repo and writes `.graphifyignore` before extracting](0017-project-graph-onboarding-assesses-the-repo-and-writes-graphifyignore-before-extracting.md) | Accepted | 2026-06-17 |
|
||||
| 0018 | [Plugin renames must update the marketplace manifest, not just the directory and settings.json](0018-plugin-renames-must-update-the-marketplace-manifest-not-just-the-directory-and-settings-json.md) | Accepted | 2026-07-03 |
|
||||
| 0019 | [Global os-orchestration plugin supersedes per-project orchestration text](0019-global-os-orchestration-plugin-supersedes-per-project-orchestration-text.md) | Accepted | 2026-07-03 |
|
||||
| 0019 | [Global os-orchestration plugin supersedes per-project orchestration text](0019-global-os-orchestration-plugin-supersedes-per-project-orchestration-text.md) | Superseded | 2026-07-03 |
|
||||
| 0020 | [os-adr plugin built; implementation-time resolutions of the design's open questions](0020-os-adr-plugin-built-implementation-time-resolutions-of-the-design-s-open-questions.md) | Accepted | 2026-07-03 |
|
||||
| 0021 | [Model-tier skill-execution eval for os-adr (Eval A), run in-session via subagents](0021-model-tier-skill-execution-eval-for-os-adr-eval-a-run-in-session-via-subagents.md) | Accepted | 2026-07-03 |
|
||||
| 0022 | [os-status plugin: aggregated in-process SessionStart status checks](0022-os-status-plugin-aggregated-in-process-sessionstart-status-checks.md) | Accepted | 2026-07-06 |
|
||||
|
|
@ -36,4 +36,11 @@ One file per decision, `NNNN-kebab-title.md`, created via `/os-adr:create`.
|
|||
| 0027 | [All cc-os per-project state lives under `.cc-os/<plugin>/`](0027-all-cc-os-per-project-state-lives-under-cc-os-plugin.md) | Accepted | 2026-07-12 |
|
||||
| 0028 | [os-shortcuts plugin for user-invoked QOL commands, bounded against hook-injection plugins](0028-os-shortcuts-plugin-for-user-invoked-qol-commands-bounded-against-hook-injection-plugins.md) | Accepted | 2026-07-12 |
|
||||
| 0029 | [os-backlog card-move with CLI-enforced column ownership; afk-ready cards skip Review](0029-os-backlog-card-move-with-cli-enforced-column-ownership-afk-ready-cards-skip-review.md) | Accepted | 2026-07-12 |
|
||||
| 0030 | [Read-only work uses Claude Code plan mode; no /readonly shortcut command](0030-read-only-work-uses-claude-code-plan-mode-no-readonly-shortcut-command.md) | Accepted | 2026-07-13 |
|
||||
| 0031 | [os-context prompt-composer plugin supersedes single-file os-orchestration](0031-os-context-prompt-composer-plugin-supersedes-single-file-os-orchestration.md) | Accepted | 2026-07-13 |
|
||||
| 0032 | [AI-maintained knowledge files are indexes with progressive disclosure](0032-ai-maintained-knowledge-files-are-indexes-with-progressive-disclosure.md) | Accepted | 2026-07-13 |
|
||||
| 0033 | [Tracker routing: Planka is state, git issues are specs](0033-tracker-routing-planka-is-state-git-issues-are-specs.md) | Accepted | 2026-07-13 |
|
||||
| 0034 | [Cross-project filing: file-dont-fix with a derived global project index](0034-cross-project-filing-file-dont-fix-with-a-derived-global-project-index.md) | Accepted | 2026-07-13 |
|
||||
| 0035 | [Issue-triggered project AI wakeup via polling, not webhooks](0035-issue-triggered-project-ai-wakeup-via-polling-not-webhooks.md) | Accepted | 2026-07-13 |
|
||||
| 0036 | [tmux session convention for AI-run sessions: cc-<project>-<purpose>](0036-tmux-session-convention-for-ai-run-sessions-cc-project-purpose.md) | Accepted | 2026-07-13 |
|
||||
<!-- adr-index:end -->
|
||||
|
|
|
|||
|
|
@ -1,702 +1,110 @@
|
|||
# cc-os implementation status & changelog
|
||||
# cc-os implementation status — index
|
||||
|
||||
_Last updated: 2026-07-12. This is the append-only implementation-status record moved out of
|
||||
`CLAUDE.md` (2026-07-08, task B1 of
|
||||
[system-prompt-profiles-tasks.md](plans/system-prompt-profiles-tasks.md)). CLAUDE.md keeps
|
||||
orientation only; when a build step completes, record it HERE (plus
|
||||
`docs/memory-system/04-build-plan.md`), and touch CLAUDE.md only if the current design or
|
||||
conventions changed._
|
||||
_Last updated: 2026-07-13. This file is an **index with progressive disclosure** (see the
|
||||
index+PD convention ADR): headline timeline entries and per-component one-liners live here;
|
||||
full per-component detail lives in leaf files under `docs/implementation-status/`, read on
|
||||
demand. When a build step completes, add a **one-line** timeline entry here and put the
|
||||
detail in (or create) the relevant leaf file; decisions go to `docs/adr/` — an ADR is the
|
||||
durable record, so don't duplicate its content here. Touch `CLAUDE.md` only if the current
|
||||
design or conventions changed. (History: moved out of CLAUDE.md 2026-07-08; distilled from
|
||||
the previous 729-line append-only changelog 2026-07-13, issue #25.)_
|
||||
|
||||
## Status timeline
|
||||
## Status timeline (headlines)
|
||||
|
||||
**Recent pivot (2026-06-03):** Graphify **replaces** the originally-planned Ruby/SQLite
|
||||
tag-index CLI and also covers the deferred QMD semantic layer. `04-build-plan.md` and
|
||||
`06-graphify-evaluation.md` reflect this; if an older doc still describes the Ruby CLI, defer
|
||||
to those two and fix the stale doc.
|
||||
Decisions are recorded in `docs/adr/` (see `docs/adr/README.md`); entries below that only
|
||||
restated an ADR were removed in the 2026-07-13 distill. Component-level detail for each
|
||||
entry is in the leaf file named.
|
||||
|
||||
**Decisions locked (2026-06-04):** Six-facet tag taxonomy + `scope/` (ADR-011); reuse
|
||||
`~/Documents/SecondBrain` vault rather than creating a new one (ADR-012); build-first /
|
||||
migrate-incrementally — build full system against a fixture set first, defer bulk vault
|
||||
migration to last, onboard projects one at a time (ADR-013).
|
||||
- **2026-06-03** — Graphify pivot: replaces the planned Ruby/SQLite tag-index CLI and the
|
||||
deferred QMD layer (ADR-0010; rationale: `docs/memory-system/06-graphify-evaluation.md`).
|
||||
- **2026-06-04** — Core design decisions locked: facet taxonomy, vault reuse, build-first
|
||||
order (ADR-0011/0012/0013).
|
||||
- **2026-06-05** — Empirical: Graphify is a structure extractor, not a topic clusterer;
|
||||
hubs/wikilinks must be authored (ADR-0014; findings:
|
||||
`docs/memory-system/07-graph-connectivity-findings.md`).
|
||||
- **2026-06-09** — os-vault plugin live; build-plan Steps 1/2a/2b/3/4/5a/6 complete; single
|
||||
global memsearch store decided (ADR-0015). Detail:
|
||||
[implementation-status/os-vault.md](implementation-status/os-vault.md).
|
||||
- **2026-06-12** — Plugin source moved into cc-os git; bash→Python deep-module port; symlink
|
||||
cutover (ADR-0016).
|
||||
- **2026-06-15** — Step 5b vault git sync automated; llf-schema pilot onboarded; **all
|
||||
required build steps complete**. Detail: os-vault leaf.
|
||||
- **2026-06-17** — Assessment-first project onboarding + `.graphifyignore` (ADR-0017).
|
||||
- **2026-06-30** — SB Content Plan Phase 1 complete (schema reconciliation, templates,
|
||||
design-template skill). Detail: os-vault leaf; Phase 2 (issue #7) open as steady-state
|
||||
epic.
|
||||
- **2026-07-03** — Plugin renamed `memory` → `os-vault`; marketplace-manifest failure mode
|
||||
found (ADR-0018, runbook in
|
||||
[implementation-status/operational-procedures.md](implementation-status/operational-procedures.md)).
|
||||
os-doc-hygiene and os-orchestration migrated in from cc-plugins (ADR-0019); os-adr plugin
|
||||
built + migration pilot passed + Eval A/B baselines (ADR-0020/0021). Detail: os-adr /
|
||||
os-doc-hygiene / os-context leaves.
|
||||
- **2026-07-04** — Stale-plugin-cache incident resolved; `bin/refresh-plugins` created
|
||||
(runbook: operational-procedures leaf). os-adr Eval B wording loop closed the gap (sonnet
|
||||
8/8, haiku 7/8). Detail: os-adr leaf.
|
||||
- **2026-07-06** — os-status plugin built (ADR-0022); os-vault WS2 + os-orchestration
|
||||
WS1/E1–E3 eval harnesses + baseline grids; os-adr Eval C harness + saturated frozen grid.
|
||||
Detail: os-status / os-vault / os-context / os-adr leaves.
|
||||
- **2026-07-07/08** — os-vault WS2 wording loop and os-orchestration WS4 economics loop
|
||||
complete and shipped; run-sets AND reserves now contaminated for future wording tuning —
|
||||
next signal is production IRL audits. Detail: os-vault / os-context leaves.
|
||||
- **2026-07-10** — Multi-machine freshness closed (SessionStart ff-only pull); os-backlog
|
||||
AFK slices shipped + live-verified, planka-api 0.2.0 migration (ADR-0023/0025); first
|
||||
orchestration IRL re-audit done + biweekly audit semi-automated. Detail: os-vault /
|
||||
os-backlog / os-context leaves.
|
||||
- **2026-07-12** — `/os-status:fix` unified setup/update command (ADR-0026) +
|
||||
`/os-backlog:route`; fix tune-up + `.cc-os/` state consolidation (ADR-0027); cc-os ADR
|
||||
retrofit — 27-ADR monolith migrated to `docs/adr/` (ADR-0020 amendment); os-shortcuts
|
||||
plugin + `/wrap` (ADR-0028); os-backlog card-move + afk-ready-skips-Review (ADR-0029).
|
||||
Detail: os-status / os-backlog / os-adr leaves.
|
||||
- **2026-07-13** — os-orchestration renamed/generalized to os-context prompt composer
|
||||
(ADR-0031) + three new prompt files; `/readonly` decided as plan-mode convention, no
|
||||
skill (ADR-0030); os-doc-hygiene `file_length` scanner signal shipped (issue #25 part 1);
|
||||
this file distilled to index+PD (issue #25 parts 2–3, ADR-0032). os-backlog slice 7
|
||||
shipped: SessionStart injection note + /to-issues tracker routing (issue #16); board-id
|
||||
cache via BoardResolver (issue #22); slice 8 — Operations board + tracker-routing rubric
|
||||
category + ADR-0033 canonizing Planka-state/git-issues-spec (issue #17); route inspect
|
||||
issue-shape classification + split-by-kind default (issue #26); cross-project filing
|
||||
convention + derived global project index (issue #27, ADR-0034); wakeup-polling +
|
||||
tmux-convention spike with dry-run pilot (issue #28, ADR-0035/0036). Detail:
|
||||
os-context / os-doc-hygiene / os-backlog leaves.
|
||||
|
||||
**Empirical finding locked (2026-06-05):** Graphify is a structure extractor, not a topic
|
||||
clusterer — no emergent hub nodes appear even at `--mode deep`; hub notes + wikilinks must be
|
||||
author-provided during migration (not deferred). Migration scaffolding is now a first-class
|
||||
deliverable. Open question: do facet tags create graph edges? (ADR-014; findings:
|
||||
`docs/memory-system/07-graph-connectivity-findings.md`).
|
||||
**Remaining optional items:** additional project onboarding (one at a time, per ADR-0013);
|
||||
bulk vault migration; os-adr rollout to pilot projects; os-backlog routing rollout (#14);
|
||||
cross-project filing convention (#27) and wakeup design spike (#28).
|
||||
|
||||
**Implementation status (2026-06-09):** The global Claude Code plugin is live
|
||||
(`~/.claude/plugins/os-vault/`). Steps 1, 2a, 2b, 3, and 6 of the build plan are complete
|
||||
(including the `onboard-project` skill, previously TODO under Step 6/Part D). Step 1
|
||||
completed 2026-06-09: vault-conventions.md exists, all 6 fixture notes seeded, and 14
|
||||
Graphify handbook + memory-system design notes migrated to the vault as migration
|
||||
scaffolding. Step 4 (memsearch) completed 2026-06-09: plugin installed via marketplace,
|
||||
MEMSEARCH_DIR set global (~/.memsearch), Stop hook verified producing daily memory files,
|
||||
search confirmed working. Step 5a (memsearch episodic git sync) completed 2026-06-09:
|
||||
dedicated private Forgejo repo (`forgejo.swansoncloud.com/jared/memsearch`), whitelist
|
||||
`.gitignore` (memory/*.md only), auto-commit+push via cc-os `session-end.sh` hook (ADR-015;
|
||||
relocated to `memsearch_sync.py` by ADR-016, 2026-06-12). Step 5b (Obsidian vault → VPS
|
||||
sync) remained at that date.
|
||||
## Implemented components (one-liners)
|
||||
|
||||
**Decision (2026-06-09):** Single global memsearch store for ALL clients — cross-client
|
||||
commingling is the accepted, intended design (ADR-015 resolved). One private Forgejo repo
|
||||
(`forgejo.swansoncloud.com/jared/memsearch`), not per-client repos. Forward direction:
|
||||
minimize/eliminate dedicated per-client working directories; work projects locally or use a
|
||||
single general `clients/` dir for non-local client work — memory is captured globally
|
||||
regardless of cwd. No `clients/` directory structure designed yet; open item.
|
||||
Read the leaf file before modifying a component.
|
||||
|
||||
**Implementation status (2026-06-12):** os-vault plugin source moved into git at
|
||||
`cc-os/plugins/os-vault/` and bash hooks ported to Python (deep-module architecture: shared
|
||||
`config.py`, `hook_io.py`, `session_state.py`; thin entry-point scripts). Cutover via symlink
|
||||
`~/.claude/plugins/os-vault → cc-os/plugins/os-vault/` and settings.json hook rewrite.
|
||||
memsearch sync split into dedicated `memsearch_sync.py` SessionEnd hook (relocation of
|
||||
ADR-015 behavior, not reversal). Fresh-session test passed 2026-06-12. See ADR-016.
|
||||
|
||||
**Implementation status (2026-06-15):** Step 5b done and automated — vault
|
||||
(`~/Documents/SecondBrain`) initialized as git repo, pushed to private Forgejo
|
||||
(`ssh://git@forgejo.swansoncloud.com:2222/jared/SecondBrain.git`); 52 files, git chosen over
|
||||
Syncthing. Auto-commit+push wired via `vault_sync.py` SessionEnd hook (third SessionEnd
|
||||
hook, runs after `session_end.py` so the daily journal note is included; push-only —
|
||||
SessionStart pull is an optional future item for multi-machine). Step 2e pilot done —
|
||||
llf-schema (`/home/jared/dev/llf-schema`, PHP 8.2 WordPress plugin) onboarded via
|
||||
`/os-vault:onboard-project` skill: 605 nodes / 930 edges / 52 communities; Ollama doc pass
|
||||
lossy on WordPress docs but AST pass solid. Step 2d closed — live vault `graphify-out/` is
|
||||
the baseline; fixture-only build superseded.
|
||||
|
||||
**Implementation status (2026-06-17):** `/os-vault:onboard-project` skill now uses
|
||||
assessment-first onboarding: surveys the repo structure, classifies exclude candidates by
|
||||
TYPE (11 categories: fetched deps, build output, caches, VCS internals, editor/AI-tooling
|
||||
dirs, lockfiles, coverage/logs, bulk data, binaries, secrets, graphify-out/ — illustrative
|
||||
names, not fixed templates), generates a per-project `.graphifyignore`, confirms with the
|
||||
user, then extracts the graph using `qwen25-coder-7b-16k` (config.yaml `ollama_model`;
|
||||
16k-context build of qwen2.5-coder:7b — larger context window cuts chunk count per doc, the
|
||||
main speed lever). Fixes issue where repos with large dependency trees routed non-code files
|
||||
through the Ollama doc pass (see ADR-017).
|
||||
|
||||
**Implementation status (2026-06-30):** Phase 1 of the SecondBrain Content Plan complete
|
||||
(`docs/memory-system/09-sb-content-plan.md`, issues #1–#6).
|
||||
`~/Documents/SecondBrain/vault-conventions.md` reconciled to a single authoritative typed
|
||||
frontmatter schema (removed the contradictory block; canonical filenames are slug-only,
|
||||
`source:` is a frontmatter field not a tag, `scope` is a field not a tag). New
|
||||
`/os-vault:design-template` skill added
|
||||
(`cc-os/plugins/os-vault/skills/design-template/SKILL.md`) — routes between template-design
|
||||
(4-step process + injection-economics filter) and new-type creation (9-step lifecycle).
|
||||
Three vault note templates created in `~/Documents/SecondBrain/_templates/` (`howto.md`,
|
||||
`convention.md`, `reference.md`; `reference` carries a four-subtype variant selector —
|
||||
pattern/framework, api-integration, role-definitions, design-rules), each dogfooded against
|
||||
a real note. Four proof-of-concept vault notes patched (cookbook subtype →
|
||||
pattern/framework, glossary de-duped to a wikilink, design-mode scope → project, tags +
|
||||
`last_reviewed` added). Phase 2 (issue #7) remains open as the steady-state
|
||||
migration/onboarding epic (no code). See `docs/memory-system/09-sb-content-plan.md`.
|
||||
|
||||
**Plugin renamed `memory` → `os-vault` (2026-07-03):** directory renamed in git,
|
||||
`~/.claude/plugins/os-vault` symlink and `settings.json`'s `enabledPlugins` updated — but
|
||||
the `local-plugins` marketplace manifest
|
||||
(`~/.claude/plugins/.claude-plugin/marketplace.json`) still declared the old
|
||||
`memory`/`./memory` entry, and `installed_plugins.json` still had a cached
|
||||
`memory@local-plugins` install record. Skills (and therefore slash commands) never
|
||||
registered under the new name as a result; hooks kept working because they're wired by
|
||||
absolute path in `settings.json`, independent of plugin resolution. Fixed by editing the
|
||||
marketplace manifest to `"os-vault"`/`"./os-vault"`, then
|
||||
`claude plugin marketplace update local-plugins`,
|
||||
`claude plugin install os-vault@local-plugins`,
|
||||
`claude plugin uninstall memory@local-plugins`. See ADR-018 and the "Renaming or moving a
|
||||
local plugin" procedure below.
|
||||
|
||||
**Resolved (2026-07-04):** os-adr's skills failed to register on first install due to stale
|
||||
plugin caches — the cache that was installed on 2026-07-03 17:21:48 was missing
|
||||
`hooks/hooks.json`, `bin/adr-detect`, `bin/adr-find`, `bin/adr-migrate`. Root cause:
|
||||
unknown, but the caches were restored via plugin uninstall/reinstall. A parallel issue
|
||||
affected os-doc-hygiene (cache retained deleted `commands/` directory). Fixed by manually
|
||||
refreshing both caches, then created `bin/refresh-plugins` automation to prevent future
|
||||
drift — see "Editing a local plugin (cache refresh)" below. Investigation result: stale
|
||||
caches (not manifest-naming issues) were the cause; slash command registration works
|
||||
correctly once caches are fresh.
|
||||
|
||||
**Multi-machine freshness closed (2026-07-10):** `session_start.py` gained a fast-forward-only
|
||||
`git pull` (`timeout 10 git pull --ff-only --quiet`) for both `cfg.vault_path` and
|
||||
`cfg.memsearch_dir`, run before the staleness check — silent on success, quiet one-line stderr
|
||||
note on failure (offline machine), never blocking session start. Closes the push-only gap noted
|
||||
below; mirrors the push style of `vault_sync.py`/`memsearch_sync.py` in the opposite direction.
|
||||
|
||||
**os-backlog AFK slices shipped (2026-07-10):** PRD published as Forgejo issue #9 (state
|
||||
pointer: Planka card `1816234194716592051`); sliced into issues #10–#17 via `/to-issues`.
|
||||
The five AFK slices are implemented and committed: #10 plugin scaffold + `board-ensure`
|
||||
(uniform lists/labels incl. new `semi` autonomy label, Planka 2.1.1 quirks, `archived--`
|
||||
rename convention), #11 `tracker` key in `.cc-os/config` + os-status `tracker-configured`
|
||||
check, #12 pure routing resolver CLI, #13 `/os-backlog:capture` + `/os-backlog:list`
|
||||
skills + card CLI, #15 `card-triage` + `board-audit` named agents. All deterministic parts
|
||||
covered by stubbed-client tests (34 runs os-backlog, 49 os-status). Semi slices #14
|
||||
(routing/migration skill), #16 (injection note + /to-issues routing), #17 (ops board +
|
||||
rubric category + routing ADR) await human decision gates. 2026-07-10 (later): planka-api
|
||||
gem 0.1.0 installed locally (`rake install`, rbenv 4.0.2; publish/versioning still parked
|
||||
behind gem issue 13) and live verification completed against production Planka —
|
||||
`snapshot`, `board-ensure` (created the missing `semi` label on the cc-os board), and
|
||||
`card-add` + `card-label` all succeeded with bot credentials sourced from ovh-vps
|
||||
(`~/services/planka/bot-credentials.env`) in a subshell. Follow-up captured as a `hitl`
|
||||
Backlog card: get planka_bot into credvault (credvault has no import path — rotate vs.
|
||||
add import capability is a human decision).
|
||||
|
||||
**os-backlog migrated to planka-api 0.2.0 typed returns (2026-07-10):** commit `8df0cb7`
|
||||
moved the gem from 0.1.0 hash-return to 0.2.0, which returns `Planka::Types` value objects
|
||||
instead of parsed-JSON hashes. `board_ensurer.rb` and `cards.rb` were rewritten on typed
|
||||
accessors (`card.id`, `card.list_id`, …); `Cards#snapshot` now builds explicit string-keyed
|
||||
hashes from `BoardDetail#lists`/`#cards`/`#labels`/`#card_labels` (`BoardDetail` replaces the
|
||||
old `included`-hash shape). Test fakes in `tests/test_helper.rb` were rebuilt to return real
|
||||
`Planka::Types` records instead of hashes. JSON output stays byte-compatible, so skill/agent
|
||||
contracts are unchanged. A follow-up fix (issue #18, same day) found the migration had
|
||||
silently dropped one field from `Cards#snapshot`'s per-card hash; restored the full 19-key
|
||||
camelCase card key set (`id`, `createdAt`, `updatedAt`, `type`, `position`, `name`,
|
||||
`description`, `dueDate`, `isDueCompleted`, `stopwatch`, `commentsTotal`, `isClosed`,
|
||||
`listChangedAt`, `boardId`, `listId`, `creatorUserId`, `prevListId`, `coverAttachmentId`,
|
||||
`isSubscribed`, plus `labels`) with a regression test pinning the key set including
|
||||
`listChangedAt`. Suite green at 35 runs (was 34 pre-#18).
|
||||
|
||||
**os-backlog live verification (0.2.0) (2026-07-10):** end-to-end re-verification of
|
||||
`bin/os-backlog` against the real Planka instance with the installed 0.2.0 gem (issue #20).
|
||||
Local machine had no `PLANKA_*` env/credvault entry, so the plugin tree was rsynced to
|
||||
`ovh-vps:/tmp/os-backlog-verify` (where `planka-api` 0.2.0 is also installed) and exercised
|
||||
there with bot credentials sourced from `~/services/planka/bot-credentials.env` in a
|
||||
subshell, per the established pattern — credential contents were never read or printed.
|
||||
Results: `tests/all.rb` 35 runs/0 failures on ovh-vps; `board-ensure cc-os --project Dev` run
|
||||
twice both reported `(existing)` with no lists/labels created (idempotent); `card-add`
|
||||
created a card on Backlog; `card-label` attached `afk-ready`; `card-comment` posted a
|
||||
comment; `snapshot --board cc-os` showed the card with the full 20-key set (19 card fields +
|
||||
`labels`) including `"listChangedAt": "2026-07-10T21:04:37.013Z"` and `"labels":
|
||||
["afk-ready"]`. The test card was deleted via `Planka::Client#cards.delete` (the CLI has no
|
||||
delete subcommand) and a follow-up snapshot confirmed it was gone; the rsynced verification
|
||||
tree on ovh-vps was removed afterward. No regressions vs. the pre-0.2.0 behavior.
|
||||
|
||||
**Unified setup/update command + tracker routing skill shipped (2026-07-12):** ADR-026
|
||||
decided the unified per-project setup/update command lives in os-status as `/os-status:fix`
|
||||
(rejected: new `os-project` plugin, literal `/cc-os:init`). Built in parallel (issues #21,
|
||||
#14, both sonnet-delegated): **os-status** gained the `fix` skill (runs the check registry
|
||||
via a new `python3 hooks/checks.py --json` runner, drives each failing check's owning-plugin
|
||||
remediation, idempotent — re-run is the update path), a `remediation` pointer on every
|
||||
`Check`, two new checks (`project-graph-present` → `/os-vault:onboard-project`;
|
||||
`config-version-current` vs `CURRENT_CONFIG_VERSION`, stamped as `version=` in
|
||||
`.cc-os/config` by fix via `state.write_config_value`), and a banner header naming
|
||||
`/os-status:fix` as the single entry point; suite 64 tests (was 36). **os-backlog** gained
|
||||
`/os-backlog:route` (issue #14): `inspect` + `config-write` dispatcher subcommands
|
||||
(`lib/backlog/tracker.rb` validates the four tracker formats + classifies git remotes,
|
||||
`lib/backlog/inspector.rb` finds in-repo issue files; `Config.merge` preserves other keys),
|
||||
with destination choice and live-history migration as named human gates in the skill; suite
|
||||
54 runs/110 assertions (was 35). Human gates preserved throughout: fix never fabricates a
|
||||
tracker value and never edits global settings.json. Rollout per #14 acceptance (onboard
|
||||
cc-os itself, then one more project) still pending.
|
||||
|
||||
**Fix tune-up + per-project state consolidation (2026-07-12, issues #23/#24, ADR-027):** the
|
||||
credvault smoke-test of `/os-status:fix` (session cd1ae1cd) was audited against the SKILL.md
|
||||
contract; the run was on-contract but surfaced four gaps (stale snoozes never cleared,
|
||||
`.cc-os/config` format split between the Ruby and Python writers, ad-hoc gitignore hygiene,
|
||||
an unprompted commit) plus root-dotdir sprawl. Fixes (both sonnet-delegated, in parallel):
|
||||
**os-status** — `clear_snooze` helper + self-clearing snoozes when a check evaluates ok/note
|
||||
(both SessionStart and `--json` runners); its own state (`snooze-*`, `suppress-*`,
|
||||
`state.json`) relocated to `.cc-os/status/` with legacy fallback read + migrate-on-write
|
||||
(shared `config` stays at `.cc-os/config`); `write_config_value` normalizes the whole file to
|
||||
`key=value` (no spaces); fix SKILL.md now scopes out committing, adds an explicit
|
||||
"`.cc-os/` in .gitignore" step, and clarifies when re-runs are needed; suite 80 tests (was
|
||||
64). **os-backlog** — `Config` already emitted the normalized format; locked in by new tests
|
||||
(58 runs, was 54). **os-doc-hygiene** — state moved `.dochygiene/` → `.cc-os/dochygiene/`
|
||||
(ADR-027) with canonical-then-legacy fallback reads and auto-migration (+ legacy dir removal)
|
||||
on first write; scanner self-excludes `.cc-os/`; all four SKILL.md files updated; suite 279
|
||||
passed. `graphify-out/` explicitly exempt from consolidation (ADR-027). Re-run of
|
||||
`/os-status:fix` in credvault is the pending IRL verification.
|
||||
|
||||
**cc-os ADR retrofit (2026-07-12, via `/os-status:fix`):** the 27-ADR monolithic
|
||||
`docs/memory-system/03-architecture-decisions.md` was migrated to `docs/adr/` with
|
||||
`/os-adr:migrate`, promoted ahead of the pilot projects (ADR-020 amended in place: migrate
|
||||
pilot had already passed its gate, migrate is non-destructive, and the monolith's context
|
||||
cost was real and growing). Mechanics: the detector only scans `docs/{adr,decisions,decision}/`
|
||||
+ `DECISIONS.md`, so the source was shimmed in via a format-normalized copy under
|
||||
`docs/decisions/` (bullet-label `- **Context**: …` → line-start `**Context:** …` so the
|
||||
bold-label parser could extract sections; Rationale/Cross-references left as bullets so they
|
||||
ride inside Decision/Alternatives content). Result: 27/27 converted, flag rate 3.7% (one low:
|
||||
0016), consequences filled per file from source material (sonnet-delegated), 0004/0005/0006
|
||||
marked Superseded by 0010 with frontmatter links, `migration_source` repointed at the real
|
||||
monolith path, shim removed. The monolith is retained untouched as the historical source; new
|
||||
decisions go through `/os-adr:create`. CLAUDE.md updated (docs/adr/ layout entry,
|
||||
trigger-phrased decisions convention, os-adr rollout line). Remaining os-adr rollout: pilot
|
||||
projects one at a time, then wider.
|
||||
|
||||
**os-shortcuts plugin + `/wrap` shipped (2026-07-12, ADR-028, Planka card):** new plugin
|
||||
`plugins/os-shortcuts/` for user-invoked QOL commands — the "keybindings" of cc-os; domain
|
||||
boundary vs automatic hook injection (os-orchestration / future os-context) recorded in
|
||||
ADR-028; skills only, no hooks. First resident `/os-shortcuts:wrap` encodes the session-close
|
||||
ritual from the 2026-07-09 workflow audit (typed 20+ times): git review → commits per repo
|
||||
conventions → doc/todo sweep deferring to project CLAUDE.md → Done/In-flight/Next handoff.
|
||||
Registered in the local-plugins marketplace, enabled in settings.json, installed via
|
||||
`claude plugin install os-shortcuts@local-plugins`. Sonnet-delegated build. `/readonly`
|
||||
remains a separate open card (decide vs plan mode first).
|
||||
|
||||
**os-backlog card-move + afk-ready-skips-Review (2026-07-12, ADR-029):** the process policy
|
||||
always intended the AI to move worked cards, but the CLI had no move command, so worked/blocked
|
||||
cards accumulated in place with comments. Added `card-move --board --card --to` over
|
||||
planka-api `Cards#move`, with column-ownership rules enforced in `lib/backlog/cards.rb`
|
||||
(never into/out of Next; hitl cards never moved; never out of Done; →Done only for
|
||||
afk-ready cards). Autonomy semantics amended per user decision: afk-ready ships straight to
|
||||
Done (no Review sift); semi still stops at Review. capture/list SKILL.md updated to the
|
||||
"working a card means moving it" contract. Suite 68 runs/153 assertions (was 58/130).
|
||||
Sonnet-delegated build.
|
||||
|
||||
**Remaining optional items:** additional project onboarding (one at a time, per ADR-013); bulk
|
||||
vault migration. All required build steps complete as of 2026-06-15.
|
||||
|
||||
## Implemented components
|
||||
|
||||
**Global os-vault plugin** — `cc-os/plugins/os-vault/` (git-tracked, 2026-06-12); symlinked
|
||||
into `~/.claude/plugins/os-vault`
|
||||
- Hooks: `hooks/` — `session_start.py` (SessionStart; staleness check + detached graph
|
||||
rebuild, and, as of 2026-07-10, a fast-forward-only `git pull` for `cfg.vault_path` and
|
||||
`cfg.memsearch_dir` before the staleness check — silent on success, quiet one-line stderr
|
||||
note on failure, never blocking), `session_context.py` (project graph path only),
|
||||
`post_tool_use_write.py`, `session_end.py` (vault journal), `memsearch_sync.py` (second
|
||||
SessionEnd hook; memsearch auto-commit+push, 30s timeout), `vault_sync.py` (third
|
||||
SessionEnd hook; vault auto-commit+push to forgejo.swansoncloud.com/jared/SecondBrain, 30s
|
||||
timeout; mirrors memsearch_sync.py)
|
||||
- Shared modules: `config.py` (load_config → frozen Config dataclass), `hook_io.py`
|
||||
(read_input → HookInput dataclass), `session_state.py` (record_touch/read_touches;
|
||||
encapsulates `/tmp/claude-vault-touched-$SESSION_ID` contract)
|
||||
- Skills: `skills/` — query, write, reorganize, onboard-project (assessment-first
|
||||
onboarding: surveys repo, classifies excludes by type (11 categories), writes per-project
|
||||
`.graphifyignore`, confirms with user, then extracts using `qwen25-coder-7b-16k` — per
|
||||
ADR-017), design-template (routes template-design ↔ new-type-creation: 4-step
|
||||
template-design process + injection-economics filter, 9-step new-type lifecycle; backs
|
||||
Phase 1 of the SB Content Plan — see `docs/memory-system/09-sb-content-plan.md`)
|
||||
- Config: `config.yaml` — vault path, Ollama model (qwen25-coder-7b-16k), env vars
|
||||
- Hook wiring: `~/.claude/settings.json` (hook entries invoke `/usr/bin/python3` with
|
||||
absolute paths into cc-os)
|
||||
- Isolation seam (2026-07-06, WS2): `OS_VAULT_PATH` env var overrides `vault_path` for the
|
||||
write skill and every hook via `config.load_config()`; `OS_VAULT_SKIP_REBUILD` suppresses
|
||||
the SessionStart graphify rebuild. Added for the eval harness; zero production impact when
|
||||
unset.
|
||||
- Write-behavior eval (2026-07-06, WS2): `plugins/os-vault/eval/` — held-out unprompted
|
||||
vault-write discrimination eval (Eval B/C playbook: ambiguity ladder L1 explicit → L3
|
||||
conceptual, paired positives/negatives, 6 run-set scenarios on a new `reportgen` Ruby
|
||||
fixture + 6 frozen reserve twins in a different knowledge domain, isolated sandbox vault,
|
||||
headless-only runner, deterministic-first Ruby checker with narrow frozen offer-detection
|
||||
judge fallback stubbable via `OS_VAULT_EVAL_JUDGE_CMD`, model-free `bin/self-test` 21/21).
|
||||
**Scenario Task blocks are held-out — never run informally; the reserve set is never even
|
||||
read informally.** Procedure: `plugins/os-vault/eval/README.md`.
|
||||
- Untuned baseline grid (2026-07-06, run-set × sonnet/haiku × 3 reps + 1 counted canary, 37
|
||||
reps, zero harness errors): **positives 1/19, negatives 18/18**. Headline: at L1 (explicit
|
||||
cue) both tiers persist the knowledge every rep but route it to Claude Code's built-in
|
||||
auto-memory (`~/.claude/projects/<slug>/memory/`) instead of the vault; L2/L3 mostly don't
|
||||
persist at all; zero over-triggering anywhere. TSV:
|
||||
`eval/results/2026-07-06-baseline-grid.tsv`; full analysis: vault note
|
||||
[[os-vault-write-eval-baseline-grid-results]].
|
||||
- WS2 wording loop COMPLETE (2026-07-07, 4 iterations + confirm + held-out reserve; run-set
|
||||
AND reserve now contaminated): shipped destination-ladder write SKILL.md + SessionStart
|
||||
USAGE_NOTE (mechanical triggers + before-final-reply checkpoint), mechanical vault-root
|
||||
resolution (`${OS_VAULT_PATH:-$HOME/Documents/SecondBrain}` — fixed real-vault breaches
|
||||
found on iteration 1), fixture CLAUDE.md "Cross-project memory" section (**the
|
||||
real-project adoption template**, required to reach haiku at L2/L3; includes ADR-vs-vault
|
||||
disambiguation), candidates 2–4 unscored, post-reserve YAML-colon quoting rule. Confirm
|
||||
grid sonnet 18/18 / haiku 16/18; reserve (held-out) positives 8/18 strict / trigger-axis
|
||||
13/18, negatives 17/18 (vs baseline 1/19, 18/18). Residual gaps: YAML-hostile titles,
|
||||
haiku L2 no-trigger, generalization-laundering over-trigger. One checker false-positive
|
||||
fixed+rescored mid-loop (breach regex vs `2>/dev/null`); negative-criterion redesign
|
||||
pre-registered. TSVs: `eval/results/2026-07-07-wording-*.tsv`; loop log:
|
||||
`autoresearch/classic-260707-1253/loop-log.md`; analysis: vault note
|
||||
[[os-vault-write-eval-wording-loop-results]]. Next signal is production IRL audits.
|
||||
|
||||
**Global os-orchestration plugin** — `cc-os/plugins/os-orchestration/` (git-tracked,
|
||||
2026-07-03); symlinked into `~/.claude/plugins/os-orchestration`
|
||||
- Hooks: `hooks/` — `inject.py` (injects `ORCHESTRATION.md` as additionalContext to all
|
||||
sessions)
|
||||
- Behavior: SessionStart hook injects an `ORCHESTRATION.md` markdown doc (hardcoded; lives
|
||||
in plugin source) as additionalContext, carrying the canonical global
|
||||
session-orchestration rule set for Claude Code across all projects.
|
||||
- Migration: migrated from a standalone repo (`~/dev/cc-plugins/orchestration/`, 2026-07-03)
|
||||
and integrated into cc-os. Supersedes the per-project copy-pasted orchestration text
|
||||
blocks that previously existed in individual project CLAUDE.md files (including a stricter
|
||||
local override that cc-os had carried — now removed; see ADR-019).
|
||||
- Session audit (WS1, 2026-07-06): 10 stratified real sessions audited via
|
||||
`audit/bin/extract` (Ruby fact-sheet extractor, dual-use as future eval checker) +
|
||||
parallel sonnet auditors + verified synthesis. Findings + E1–E4 eval-scenario backlog:
|
||||
`docs/orchestration-audit/2026-07-06-findings.md`. Headlines: model param was silently
|
||||
overridden to haiku on all 23 verified spawns — root cause found post-audit:
|
||||
`CLAUDE_CODE_SUBAGENT_MODEL=haiku` in `~/.claude/settings.json` env block (set by an
|
||||
earlier session as a cost measure; removed 2026-07-06; the Fable-5 correlation was
|
||||
coincidental timing). Omitted model param inherits the main-loop model (opus/sonnet) —
|
||||
misses cluster in pre-rollout and ops sessions; no over-sharing/full-dump failures
|
||||
anywhere.
|
||||
- Wording fixes shipped (2026-07-06, post-audit): ORCHESTRATION.md rewritten with
|
||||
trigger-conditioned when→then phrasing for the three verified clusters — explicit
|
||||
`model:` before every Agent call (cluster 2), subagent model self-report on sonnet/opus
|
||||
spawns (cluster 1 — the E1 canary proved `resolvedModel` is NOT model-visible in the
|
||||
launch result; self-report is the only observable channel), don't re-cover your own ground
|
||||
before delegating (cluster 3). Delegation thresholds unchanged. Cache refreshed via
|
||||
`bin/refresh-plugins`. Deterministic env-override detection (warn on
|
||||
`CLAUDE_CODE_SUBAGENT_MODEL`) is delegated to WS3's status-check design.
|
||||
- Eval harness E1–E3 (2026-07-06): `plugins/os-orchestration/eval/` — scripted headless
|
||||
orchestration-behavior eval scored by the audit extractor (extended with per-segment read
|
||||
targets). 6 run-set scenarios (E1P/N downgrade detection with
|
||||
`CLAUDE_CODE_SUBAGENT_MODEL=haiku` runner stub; E2P/N explicit-model fan-out vs
|
||||
no-delegation-on-trivial; E3P/N delegate-investigation vs orienting-reads-correct) + 6
|
||||
frozen reserve twins; new Node.js "relaystation" fixture with deterministic sandbox-time
|
||||
log generation; deterministic-first checker (`eval/bin/check`, narrow frozen haiku-judge
|
||||
fallback for E1 language axes only, stubbable via `ORCH_EVAL_JUDGE_CMD`); model-free
|
||||
`bin/self-test` 21/21 green incl. shipped-instruction-compliant conformance transcripts.
|
||||
FAIL axes map only to verified audit misses; tier choice informational. **Scenario Task
|
||||
blocks are held-out — never run informally.** Procedure:
|
||||
`plugins/os-orchestration/eval/README.md`.
|
||||
- Baseline grid run (2026-07-06, run-set × sonnet/haiku × 3 reps + 2 counted canaries, all
|
||||
counted, zero harness errors): negatives 18/18 PASS both tiers (zero over-delegation, zero
|
||||
false downgrade claims); positives 1/18 on the delegate-at-all axis — but transcript
|
||||
verification shows most positive FAILs are *superior direct strategies* (E2P sonnet: one
|
||||
scripted bulk edit; E3P sonnet: surgical greps over 400KB logs), which the WS1 audit
|
||||
itself scored as justified non-delegation, vs genuine miss shapes (E3P haiku: whole 112KB
|
||||
logs into own context; E2P haiku: 20-call per-file grind). Conditional rules validated
|
||||
where engaged: 14/14 spawns explicit-model; the one delegating E1P rep flagged the stubbed
|
||||
downgrade via self-report. Criterion redesign for E2P/E3P A-axes pre-registered (anchor
|
||||
thresholds independently, NOT to this grid) before any rescore/tuning; reserve stays
|
||||
frozen. ORCHESTRATION.md gained a scripted-bulk-edit carve-out. TSV:
|
||||
`eval/results/2026-07-06-baseline-grid.tsv`; full analysis: vault note
|
||||
[[os-orchestration-eval-baseline-grid-results]].
|
||||
- WS4 economics loop COMPLETE + SHIPPED (2026-07-08; run-set AND reserve now contaminated
|
||||
for future wording tuning): ORCHESTRATION.md v3 shipped — cost-asymmetry +
|
||||
tier-conditional threshold opener, symmetric delegate-when/work-directly-when trigger
|
||||
lists (replaces "delegate only when"), batching rule (~5–8 items per grouped spawn,
|
||||
SendMessage reuse), async rule, Workflow effort dial; explicit-model/self-report/
|
||||
don't-re-cover-ground rules unchanged. Harness extended first (commit b226601): real token
|
||||
econ axes — subagent transcripts persist at
|
||||
`<projects>/<flat-cwd>/<session-id>/subagents/agent-*.jsonl` with per-message usage blocks
|
||||
(solves the mini-audit's instrumentation gap; vault note
|
||||
[[claude-code-subagent-transcripts-and-token-accounting]]) — E5 batching pair + reserve
|
||||
twins (authored by subagent, held-out content never entered the tuning session), fable
|
||||
column, pre-registered E2P/E3P criterion redesign landed (scripted-direct PASS branch;
|
||||
74KB ingestion anchor + id-keyed root-cause concepts). Results: E5P 0/6 → majority-PASS
|
||||
both tiers incl. reserve twin 5/5 (fable median mltok 43% of baseline, H3 met); negatives
|
||||
clean everywhere (36/36 across confirm+reserve). Residual gaps: E1P sonnet delegate-at-all
|
||||
(fixture too small — needs bigger fixture before meaningful), fable downgrade-flag ~1/4
|
||||
judge flicker. 15 baseline reps were session-limit-truncated and mechanically
|
||||
excluded+replaced (limit-banner sweep now a standard step, promoted to the autoresearch
|
||||
howto). TSVs: `eval/results/2026-07-08-*.tsv`; loop log:
|
||||
`autoresearch/classic-260708-1039/`; vault note
|
||||
[[os-orchestration-ws4-econ-wording-results]]. IRL re-audit of ~5 real Fable sessions
|
||||
scheduled ~2026-07-22.
|
||||
- **First IRL re-audit DONE (2026-07-10, ahead of schedule):** 19 production transcripts
|
||||
post-2026-07-08 audited by 3 parallel agents against the shipped rules (run:
|
||||
`autoresearch/improve-260710-1057/`, plan in `improvement-plan.md`). Solved surfaces:
|
||||
model-param discipline (~48/49 spawns pinned), over-delegation guardrail. Tier 1 findings
|
||||
→ ORCHESTRATION.md wording patch shipped same day (grid validation impossible —
|
||||
run-set/reserve contaminated — so this is a one-shot production patch, not a wording
|
||||
loop): (1) settled-design mid-session fan-out trigger, (2) never-sleep-poll rule, (3) own
|
||||
eval/benchmark loops named as mechanical sequences, (4) unfamiliar-tree ~5-call Explore
|
||||
trigger. SendMessage worked example deliberately skipped (no violations found;
|
||||
injected-token budget). Acceptance check = next IRL audit (~2–3 weeks; baseline:
|
||||
settled-design drift 8/19 sessions). Side finding needing separate repro: harness ignored
|
||||
explicit `model: "sonnet"` pins on general-purpose agents (env override unset — distinct
|
||||
from WS1 cause); the self-report line caught it.
|
||||
- **IRL audit semi-automated (2026-07-10):** recurring biweekly audit formalized, working
|
||||
from any cwd. Deterministic layer: `audit/bin/audit-stats` (Ruby, composes the existing
|
||||
`audit/bin/extract` fact-sheets) enumerates production transcripts since the last run
|
||||
(excludes `-tmp-orch-*`/`-tmp-claude-*` sandboxes, `*-eval` dirs, active/trivial
|
||||
sessions), writes `~/.local/state/os-orchestration/audit/run-<date>/`
|
||||
(stats.tsv/flags.md/sheets) and appends the trend ledger `metrics.tsv` (seeded with the
|
||||
2026-07-10 run: 23 sessions, 52 spawns, 1 missing model). Judgment layer: new skill
|
||||
`/os-orchestration:audit-sessions` (rubric in `references/`) — sonnet auditor fan-out
|
||||
over flagged regions only, tiered synthesis, run-over-run trend verdicts, stops before
|
||||
any wording edit. Nudge: os-status check `orchestration-audit-due` warns when the
|
||||
ledger's last row is ≥14 days old (missing ledger = machine not opted in, silent; env
|
||||
override `OS_ORCH_AUDIT_LEDGER` for tests). Planka recurrence-manifest entry skipped:
|
||||
the gem's offsets are deliberately `+1y/+6m/+3m/+1m` only (issue 11) — a `+2w` offset is
|
||||
a gem change blocked on the parked publish/versioning decisions; the os-status nudge is
|
||||
the trigger for now.
|
||||
|
||||
**Global os-status plugin** — `cc-os/plugins/os-status/` (git-tracked, 2026-07-06);
|
||||
symlinked into `~/.claude/plugins/os-status`
|
||||
- Purpose: aggregated deterministic SessionStart status checks for the cc-os plugin family —
|
||||
per-project artifacts (ADR system, vault hub note) and environment hazards (subagent model
|
||||
env override). Built via OpenSpec change `add-os-status-plugin` (supersedes the WS3 plan
|
||||
doc); decision record ADR-022.
|
||||
- Architecture: checks are **in-process** plain Python functions in `hooks/checks.py` with
|
||||
the uniform signature `check(ctx) -> CheckResult(status, message)` plus one registry entry
|
||||
(no per-plugin subprocess protocol, no per-project code copies — both rejected in ADR-022
|
||||
after perspective review). Three states: `ok` (silent), `note` (near-zero-token
|
||||
additionalContext line, never snoozed/suppressed), `warn` (aggregated into at most one
|
||||
banner; once-per-day snooze + permanent `suppress-<check>` markers in gitignored
|
||||
per-project `.cc-os/` — state only, never code; `.cc-os/config` holds optional
|
||||
`hub`/`vault_path` key=value overrides).
|
||||
- Initial checks: `subagent-model-env-override` (env + `~/.claude/settings.json` `env`
|
||||
block; runs outside git projects too — catches the WS1 Cluster 1 incident deterministically
|
||||
at session start), `adr-system-present` (verbatim port of os-adr's hook behavior incl.
|
||||
`.os-adr/suppress`), `vault-hub-note-present` (config slug first, else `type/hub` +
|
||||
`project/<name>` facet-tag scan of the vault; missing → warn naming `/os-vault:write`).
|
||||
- Added 2026-07-12 (ADR-026, issue #21): `/os-status:fix` skill — the unified project
|
||||
setup/update command; runs the registry via `python3 hooks/checks.py --json` (each result
|
||||
now carries a `remediation` pointer), drives the owning plugin's remediation per failing
|
||||
check, stamps `version=` into `.cc-os/config` (`CURRENT_CONFIG_VERSION`); idempotent, so
|
||||
re-running is the update path. New checks `project-graph-present` and
|
||||
`config-version-current`; warn banner names `/os-status:fix` as the single entry point.
|
||||
- Tests: `python3 tests/hook_test.py` (64, model-free; 36 pre-#21) incl. byte-identity of
|
||||
PRESENT_NOTE/ABSENT_NOTE against os-adr's source. Invariants in `invariants.md`.
|
||||
Smoke-tested 2026-07-06 (cache==source, real headless session fired the hook,
|
||||
env-override canary warns).
|
||||
|
||||
**Global os-doc-hygiene plugin** — `cc-os/plugins/os-doc-hygiene/` (git-tracked,
|
||||
2026-07-03); symlinked into `~/.claude/plugins/os-doc-hygiene`
|
||||
- Hooks: `hooks/hooks.json` → SessionStart hook (matcher: startup|resume) runs
|
||||
`scripts/reminder.py` via `${CLAUDE_PLUGIN_ROOT}` (5s timeout), emitting a deterministic
|
||||
zero-token reminder banner
|
||||
- Behavior: Monitors stale and bloated project documentation per-project under
|
||||
`.dochygiene/` state dir (gitignored). SessionStart reminder is deterministic (no AI
|
||||
tokens, once/day snooze). Skills (verb-first, no `commands/` dispatcher — invoked directly
|
||||
as `/os-doc-hygiene:<skill>`, per [[cc-os-plugin-skill-naming-convention]]): `check`
|
||||
(AI-assisted classification of staleness signals, emits machine+human reports), `clean`
|
||||
(AI-assisted or deterministic patch application with git-safe scoped cleanup), `status`
|
||||
(read-only lifecycle-timestamp read), `sweep` (check then clean in sequence).
|
||||
Reversion-protected via invariants.md + golden-example test fixtures.
|
||||
- Migration: migrated from standalone repo (`~/dev/cc-plugins/doc-hygiene/`, 2026-07-03) and
|
||||
integrated into cc-os. No content conflicts detected. Renamed from `doc-hygiene` to
|
||||
`os-doc-hygiene` per cc-os plugin naming convention. Skills renamed
|
||||
`hygiene-check`/`hygiene-clean` → `check`/`clean`, and the `commands/hygiene.md`
|
||||
dispatcher removed in favor of two new skills (`status`, `sweep`), aligning with the
|
||||
`os-vault`/`os-orchestration` pattern of no `commands/` directory (2026-07-03).
|
||||
|
||||
**Global os-adr plugin** — `cc-os/plugins/os-adr/` (git-tracked, 2026-07-03); symlinked into
|
||||
`~/.claude/plugins/os-adr`
|
||||
- Purpose: Architecture Decision Records, consistently in every project — one
|
||||
customized-Nygard template (frontmatter
|
||||
`id`/`date`/`status`/`supersedes`/`superseded-by`/`affected-paths`/`affected-components`;
|
||||
Context/Decision/Consequences/Alternatives-rejected sections),
|
||||
`docs/adr/NNNN-kebab-title.md` one-file-per-decision + fully-regenerated
|
||||
`docs/adr/README.md` index. Built from `docs/adr-system/` (requirements `04`, PRD `05`,
|
||||
eval sketches `06`) via OpenSpec change `add-os-adr-plugin`.
|
||||
- Core: Ruby OO library `lib/adr/` (`Record`, `Repository`, `Index`, `Template`, `Detector`,
|
||||
`Migrator`, `MigrationReport`, `Finder`; Sandi Metz style, injected paths) behind thin
|
||||
CLIs in `bin/` (`adr-new`, `adr-init`, `adr-detect`, `adr-migrate`, `adr-find`). Tests:
|
||||
`ruby tests/all.rb` (47) + `python3 tests/hook_test.py` (15), all model-free. Behavioral
|
||||
invariants in `invariants.md`.
|
||||
- Hook: **moved to os-status (2026-07-06)** — the SessionStart existence check now runs as
|
||||
the `adr-system-present` check in the os-status plugin (`hooks.json` here is empty;
|
||||
`hooks/session_start.py` stays as the wording source of record, byte-copied into
|
||||
os-status). Behavior unchanged: present → near-zero-token usage note naming
|
||||
`/os-adr:create` + `/os-adr:find`; absent → once-per-day init/migrate suggestion, silenced
|
||||
permanently by `.os-adr/suppress` (still honored); silent outside git projects.
|
||||
- Skills (verb-first, no `commands/`): `create` (one-invocation templated+numbered+indexed
|
||||
ADR with mechanical supersession), `init`, `migrate` (non-destructive: detect surveyed
|
||||
shapes → mechanical heuristic fill → LLM fills only manifest-listed interpretive fields
|
||||
via `--apply-fills` → `migration_confidence` frontmatter + `docs/adr/migration-report.md`;
|
||||
old-system deletion only as an explicit separate user-approved step), `find`
|
||||
(deterministic-first retrieval: affected-paths/components match → Accepted-only filter →
|
||||
one-hop Graphify query-path expansion with graceful degradation → AI judgment over the
|
||||
candidate set only).
|
||||
- Migration pilot passed 2026-07-03 (sandboxed project copies; gate threshold 25%
|
||||
low-confidence flags): viking-warrior-training-log 0%, delta-refinery 0%, llf-schema 8.3%.
|
||||
Retrieval verified end-to-end against the real llf-schema project graph. Resolved: ADRs
|
||||
need no Graphify tag/edge convention — retrieval layer 3 expands the *query* paths via
|
||||
graph-node `source_file`s.
|
||||
- Eval A harness (2026-07-03, ADR-021): `plugins/os-adr/eval/` — prompted skill-execution
|
||||
eval across model tiers (haiku/sonnet). Two fixtures (generated with the plugin's own
|
||||
CLIs), six scenarios (S1 create, S2 create+supersede, S3 find/conflict, S4
|
||||
find/distractor, S5 init, S6 migrate+fills), deterministic Ruby checker (`eval/bin/check`,
|
||||
structural invariants, TSV mode for autoresearch), sandbox + headless-runner scripts,
|
||||
runner-prompt template. Primary run mode: in-session Agent-tool subagents with pinned
|
||||
`model:`; optimized via the `/autoresearch` Classic loop over SKILL.md *wording only*
|
||||
(checker/fixtures/scenarios frozen during a loop). Procedure:
|
||||
`plugins/os-adr/eval/README.md`. Self-tested both directions; grid run 2026-07-03, all
|
||||
scenarios × both tiers passing. Distinct from the held-out Eval B (unprompted behavior) —
|
||||
do not conflate.
|
||||
- Eval B harness (2026-07-03, OpenSpec change `add-os-adr-eval-b-harness`):
|
||||
`plugins/os-adr/eval-b/` — held-out unprompted-behavior eval (requirements 4–5). 7
|
||||
scenarios (W1–W3 write-trigger, R1–R4 retrieval) authored from the frozen shapes in
|
||||
`docs/adr-system/06-eval-scenarios.md`; dedicated Ruby webhook-relay fixture with a 6-ADR
|
||||
history (Superseded pair + near-miss distractors, generated via the plugin's own CLIs);
|
||||
R4's one-hop graph reach uses a real `graphify update` AST build (model-free, rebuilt via
|
||||
`eval-b/bin/build-fixture-graph`, never committed). **Headless-only runner**
|
||||
(`eval-b/bin/run` — fresh `claude -p` per rep, cwd = sandbox, so the real SessionStart
|
||||
hook fires; in-session subagents are invalid here, unlike Eval A) and a two-axis
|
||||
deterministic-first checker (`eval-b/bin/check`): axis (a) unprompted consultation,
|
||||
mechanical from transcript tool_use blocks; axis (b) correct-ADR citation (R1–R4) or
|
||||
new-ADR-file with a narrow frozen-rubric haiku judge fallback (W1–W3, `judge-rubric.md`,
|
||||
stubbable via `ADR_EVAL_B_JUDGE_CMD`). `R4-nograph` is the graph-degradation variant
|
||||
(expected FAIL). Self-tested both directions model-free via `eval-b/bin/self-test`.
|
||||
**Scenario Task blocks are held-out — never run them informally.** Procedure:
|
||||
`plugins/os-adr/eval-b/README.md`. Grid run 2026-07-03 (1 rep/cell): haiku 0/8 PASS;
|
||||
sonnet 5/8 PASS (fails W3, R1). `R4-nograph` FAILed on both tiers as expected. Full
|
||||
results + observations: vault note [[os-adr-eval-b-grid-results-and-observations]].
|
||||
- **Initial iteration complete (2026-07-03):** plugin build, migration pilot, Eval A grid
|
||||
(clean pass), and Eval B grid (baseline) all done — closes the first pass on os-adr.
|
||||
- **Wording experiment complete (2026-07-04):** the follow-up `/autoresearch` loop over
|
||||
Eval B wording ran (5 iterations, checker/fixtures/scenarios/rubric frozen; fixture
|
||||
CLAUDE.md declared a wording surface upfront) and closed the gap — final full grid
|
||||
**sonnet 8/8, haiku 7/8** (from 5/8 / 0/8 baseline; haiku's one miss is a W3 axis-b
|
||||
judge-boundary flicker). Winning wording shipped in `hooks/session_start.py`
|
||||
(PRESENT_NOTE), find/create SKILL.mds, and `eval-b/fixture/project/CLAUDE.md` (new — the
|
||||
trigger-phrased "Architecture decisions" section, the template for real-project adoption).
|
||||
Confirmed mechanisms: trigger-conditioned when→then phrasing beats inventory phrasing on
|
||||
both tiers; each rule must live where its precondition is visible (the reversal→supersede
|
||||
rule in the find skill's act-on-findings step fixed W3); lower tiers need *mechanical*
|
||||
triggers — semantic triggers only reach sonnet. Open: channel ablation never run;
|
||||
R4-nograph now passes both tiers, so the graph-degradation check no longer differentiates.
|
||||
Full hypothesis→result mapping: vault note [[os-adr-eval-b-wording-experiment-hypotheses]].
|
||||
**Before designing or running any autoresearch eval, Read
|
||||
`~/Documents/SecondBrain/howto/running-autoresearch-skill-evals.md`.**
|
||||
- **Eval B W3 stability check (2026-07-06):** 7 headless reps of haiku × W3 (all reps
|
||||
counted): axis (a) consultation 7/7 PASS; axis (b) recording-offer ~5/7 (~75–85%) — one
|
||||
FAIL plausibly infra, one a genuine behavioral miss. Verdict: intermittent flicker, not a
|
||||
hard gap — grid claim stays **haiku 7/8**, W3 characterized as an ~1-in-4/5 axis-b miss.
|
||||
Haiku cleared for daily use with that caveat. Recorded in the vault good-enough gate:
|
||||
[[os-adr-eval-b-wording-experiment-hypotheses]].
|
||||
- **Eval C harness built, NOT yet run as designed (2026-07-06):** `plugins/os-adr/eval-c/` —
|
||||
ambiguity-ladder DISCRIMINATION eval (held-out; Eval B is contaminated by the wording
|
||||
loop). 3 levels (explicit → moderate → conceptual framing) × paired positive/negative
|
||||
scenarios; run-set (6, job-execution domain) + frozen reserve-set (6, notifications
|
||||
domain). New Python fixture (`taskq` async job queue, 6-ADR history generated via the
|
||||
plugin's own CLIs, trigger-phrased CLAUDE.md section copied from eval-b). Scoring:
|
||||
positives = both axes; negatives FAIL **only on unneeded ADR creation**. Model-free
|
||||
`bin/self-test` green, including a truthful-citation-must-PASS guard. **Scenario Task
|
||||
blocks are held-out — never run informally.** Methodology: vault notes
|
||||
[[eval-methodology-ladder]] and [[eval-methodology-irl-feedback-loop]]; vault also gained
|
||||
an `eval-results` note type + `_templates/eval-results.md` (2026-07-06).
|
||||
- **Eval C frozen grid run (2026-07-06):** run-set × {sonnet, haiku} × 3 reps/cell (36
|
||||
headless reps, all counted) — **12/12 cells PASS at ≥2/3 majority; haiku 18/18 reps,
|
||||
sonnet 17/18**. Zero over-trigger ADR creation on any negative at any level; every
|
||||
positive pass was via governing-ADR citation (16) or judge-recognized proposal (1), never
|
||||
duplicate creation. Sonnet's one FAIL rep (P1-L1) is a final-message-citation boundary
|
||||
artifact, not a behavioral miss. Two harness defects fixed on the first live rep, BEFORE
|
||||
the grid, with Task blocks/fixtures/rubric untouched: (1) `bin/run` pipefail bug; (2)
|
||||
positive axis-b contradiction fixed via a mechanical `cited-governing` PASS branch + two
|
||||
new self-test guards (`B-via:` now recorded in TSV). Verdict: the Eval B wording
|
||||
**generalizes** — grid saturated, ceiling not found; next signal is production (rollout +
|
||||
IRL session audits), not another lab rung. Run-set is now the baseline; wording tuning
|
||||
against it moves measurement to the reserve-set. Full results: vault note
|
||||
[[os-adr-eval-c-frozen-grid-results]].
|
||||
- **Remaining (rollout order, amended 2026-07-12):** the cc-os retrofit is done (promoted
|
||||
ahead of pilots — see the 2026-07-12 timeline entry and the ADR-020 amendment); remaining:
|
||||
real-project migration/adoption one at a time via `/os-adr:migrate` — pilot projects, then
|
||||
wider. When onboarding real projects,
|
||||
add the trigger-phrased "Architecture decisions" CLAUDE.md section (copy from
|
||||
`eval-b/fixture/project/CLAUDE.md`; candidate: emit it from `/os-adr:init`/`migrate`).
|
||||
|
||||
**Graphify** — v0.8.31 at `/home/jared/.local/bin/graphify`
|
||||
- Vault graph: `~/Documents/SecondBrain/graphify-out/` — disposable, structural index
|
||||
rebuilt by SessionStart hook; handles relational/graph queries. Distinct from
|
||||
vault-index.json (planned, Plan B Phase 1): a flat `{tag: [{path, title, summary}]}`
|
||||
lookup rebuilt at SessionEnd, queried by the `/memory-find` skill for fast tag-based SB
|
||||
note discovery.
|
||||
- Project graph: `<project-root>/graphify-out/` — same pattern; gitignore it in each project
|
||||
repo
|
||||
- Vault conventions: `~/Documents/SecondBrain/vault-conventions.md` — frontmatter contract +
|
||||
tag taxonomy (canonical name decided 2026-06-09; formerly referred to as CONVENTIONS.md)
|
||||
|
||||
**memsearch** — v0.4.6 via Claude Code plugin marketplace (`memsearch@memsearch-plugins`)
|
||||
- Hooks: Stop, SessionStart, UserPromptSubmit, SessionEnd (ship with plugin)
|
||||
- Memory store: `~/.memsearch/memory/YYYY-MM-DD.md` daily files (global, cross-project, all
|
||||
clients — one store by design)
|
||||
- Index: `~/.memsearch/milvus.db` (Milvus Lite, local); embeddings via ONNX bge-m3
|
||||
- Config: `MEMSEARCH_DIR=~/.memsearch` in `~/.zshrc` for global scope
|
||||
- Skills: `/memory-recall`, `/memory-config` (ship with plugin)
|
||||
- Git sync: `~/.memsearch` is a dedicated private Forgejo repo
|
||||
(`forgejo.swansoncloud.com/jared/memsearch`); whitelist `.gitignore` commits only
|
||||
`memory/*.md`; auto-commit+push wired into `memsearch_sync.py` SessionEnd hook (relocated
|
||||
from `session-end.sh` by ADR-016, 2026-06-12; behavior preserved — see ADR-015)
|
||||
|
||||
**Obsidian vault git sync** — `~/Documents/SecondBrain` (2026-06-15)
|
||||
- Remote: `ssh://git@forgejo.swansoncloud.com:2222/jared/SecondBrain.git` (private Forgejo;
|
||||
web: `https://forgejo.swansoncloud.com/jared/SecondBrain`)
|
||||
- 52 files tracked (notes, journal, templates, vault-conventions.md, CLAUDE.md, .obsidian
|
||||
config); `graphify-out/` excluded via `.gitignore`
|
||||
- Auto-commit+push via `vault_sync.py` SessionEnd hook (mirrors `memsearch_sync.py`;
|
||||
push-only — SessionStart pull is optional future item for multi-machine)
|
||||
|
||||
**Global os-backlog plugin** — `cc-os/plugins/os-backlog/` (git-tracked, 2026-07-10);
|
||||
symlinked into `~/.claude/plugins/os-backlog`; PRD Forgejo issue #9, slices #10–#17
|
||||
- Purpose: the AI's process-management surface over Planka — deterministic tracker routing
|
||||
(Planka = task state by default; git issues = implementation specs only), mid-session
|
||||
capture, pull-only listing, board lifecycle, and the three-value autonomy convention
|
||||
(`hitl`/`semi`/`afk-ready`).
|
||||
- Lib (Ruby, os-adr `lib/`+`bin/` pattern, installed planka-api gem only (0.2.0, typed
|
||||
`Planka::Types` returns as of 2026-07-10) — no source
|
||||
coupling): `board_spec.rb` (uniform lists/labels contract), `board_ensurer.rb`
|
||||
(idempotent create/repair; Planka 2.1.1 `type` fields, shared-project + owner-manager
|
||||
visibility fix, label color candidates with fallback; `archived--` rename convention),
|
||||
`config.rb` (`.cc-os/config` parser), `resolver.rb` (pure repo→board decision:
|
||||
`use`/`activate`/`stop-and-discuss`; no network), `cards.rb` (add-at-Backlog, snapshot,
|
||||
attach_label, comment).
|
||||
- CLI: `bin/os-backlog` — `board-ensure`, `activate`, `archive`, `resolve`, `card-add`,
|
||||
`cards`, `snapshot`, `card-label`, `card-comment`, `card-move` (2026-07-12, ADR-029 —
|
||||
ownership rules enforced in lib); every path fails soft (one-line error,
|
||||
exit 1) when the gem/Planka is unavailable.
|
||||
- Skills: `/os-backlog:capture`, `/os-backlog:list`, `/os-backlog:route` (2026-07-12, issue
|
||||
#14 — tracker onboarding conversation over new `inspect`/`config-write` subcommands and
|
||||
`lib/backlog/tracker.rb`/`inspector.rb`; destination choice + live-history migration are
|
||||
named human gates) — carry the column-ownership rules (AI
|
||||
creates at Backlog, Doing→Review at most, never Next or Review→Done, never self-assigns
|
||||
`hitl`); list is pull-only per notification policy v2.
|
||||
- Agents: `agents/card-triage.md` (priority + autonomy labels only; ambiguity → `hitl`),
|
||||
`agents/board-audit.md` (four drift classes; writes at most comments).
|
||||
- Tests: `tests/` — 54 runs/110 assertions (35 pre-#14) against an in-memory
|
||||
`FakePlankaClient`; no live
|
||||
API. Companion os-status check `tracker-configured` (issue #11) validates the `tracker`
|
||||
key grammar and nudges unconfigured git projects (daily-snoozed).
|
||||
- Outstanding: Semi slices #16/#17 (injection note + /to-issues routing, ops board + rubric
|
||||
category + routing ADR); #14 (routing skill) shipped 2026-07-12 — rollout (onboard cc-os,
|
||||
then one more project) pending. Live verification
|
||||
completed 2026-07-10 (snapshot / board-ensure / card-add / card-label against production;
|
||||
gem 0.1.0 locally installed via `rake install`); re-verified 2026-07-10 (later) against the
|
||||
installed 0.2.0 gem post-migration — see "os-backlog migrated to planka-api 0.2.0" and
|
||||
"os-backlog live verification (0.2.0)" timeline entries above.
|
||||
|
||||
### os-shortcuts (plugin)
|
||||
|
||||
Global plugin: user-invoked QOL shortcut commands (ADR-028). Skills only — never hooks;
|
||||
automatic injection belongs to os-orchestration / the planned os-context.
|
||||
|
||||
- Skills: `/os-shortcuts:wrap` — session-close ritual (git review, commits per repo
|
||||
conventions, doc/todo sweep deferring to project CLAUDE.md, handoff summary). Shipped
|
||||
2026-07-12.
|
||||
- Expected future residents: additional shortcuts as audit findings warrant (`/readonly`
|
||||
card is open — decide vs plan mode before implementing).
|
||||
- **os-vault** — vault write/query/reorganize/onboard-project/design-template skills,
|
||||
SessionStart/End hooks, vault + memsearch git sync, WS2 write-behavior eval →
|
||||
[implementation-status/os-vault.md](implementation-status/os-vault.md)
|
||||
- **os-context** (ex os-orchestration) — SessionStart prompt composer
|
||||
(`prompts/session-start/*.md`), orchestration rules, audit-sessions skill, E1–E5 eval
|
||||
harness + IRL audit machinery →
|
||||
[implementation-status/os-context.md](implementation-status/os-context.md)
|
||||
- **os-status** — aggregated in-process SessionStart checks + `/os-status:fix` unified
|
||||
setup/update command →
|
||||
[implementation-status/os-status.md](implementation-status/os-status.md)
|
||||
- **os-doc-hygiene** — stale/bloated-doc monitoring; check/clean/status/sweep skills;
|
||||
deterministic scanner incl. `file_length` signal →
|
||||
[implementation-status/os-doc-hygiene.md](implementation-status/os-doc-hygiene.md)
|
||||
- **os-adr** — ADR system (Ruby `lib/adr/` + CLIs; create/init/migrate/find skills; Eval
|
||||
A/B/C harnesses; cc-os retrofit done) →
|
||||
[implementation-status/os-adr.md](implementation-status/os-adr.md)
|
||||
- **os-backlog** — Planka process surface: board lifecycle, capture/list/route skills,
|
||||
card CLI with column-ownership rules, triage/audit agents →
|
||||
[implementation-status/os-backlog.md](implementation-status/os-backlog.md)
|
||||
- **os-shortcuts** — user-invoked QOL commands, skills only, never hooks (ADR-0028);
|
||||
resident: `/os-shortcuts:wrap` session-close ritual (shipped 2026-07-12). No leaf file
|
||||
yet — this entry is the full detail.
|
||||
- **Graphify / memsearch / vault git sync** — external memory infrastructure (versions,
|
||||
paths, sync topology) →
|
||||
[implementation-status/memory-infrastructure.md](implementation-status/memory-infrastructure.md)
|
||||
|
||||
## Operational procedures
|
||||
|
||||
### Renaming or moving a local plugin
|
||||
|
||||
Renaming a plugin directory under a directory-source marketplace (like `local-plugins`, whose
|
||||
source is `~/.claude/plugins` itself) requires updating **three** places, not just the
|
||||
directory and `settings.json`:
|
||||
|
||||
1. **Plugin directory / symlink** — the actual files (or symlink target, for plugins that
|
||||
live in a git repo like `cc-os/plugins/os-vault/`).
|
||||
2. **Marketplace manifest** — `~/.claude/plugins/.claude-plugin/marketplace.json` (for
|
||||
`local-plugins`) lists each plugin's `name` and `source` path explicitly; this does
|
||||
**not** auto-discover from the directory listing. Update the entry to match the new
|
||||
name/path.
|
||||
3. **`settings.json` `enabledPlugins`** — the `<name>@<marketplace>` key must match the
|
||||
manifest entry from step 2.
|
||||
|
||||
Then refresh the plugin manager's state so it re-resolves the marketplace and drops the
|
||||
stale install record:
|
||||
|
||||
```bash
|
||||
claude plugin marketplace update <marketplace-name> # re-validates the manifest
|
||||
claude plugin install <new-name>@<marketplace-name> # creates the new install record
|
||||
claude plugin uninstall <old-name>@<marketplace-name> # drops the stale one
|
||||
claude plugin list # verify: new name enabled, old name gone
|
||||
claude plugin details <new-name>@<marketplace-name> # verify skills/agents/hooks resolved
|
||||
```
|
||||
|
||||
Skipping step 2 is the failure mode to watch for: hooks (wired by absolute path in
|
||||
`settings.json`) keep working, masking the fact that skills/slash-commands silently stopped
|
||||
registering under the plugin's new name.
|
||||
|
||||
### Editing a local plugin (cache refresh)
|
||||
|
||||
Plugin source edits (including SKILL.md wording, hook scripts, or CLI code) do not reach
|
||||
running sessions until the cache is refreshed. Claude Code caches plugins when installed,
|
||||
copying them to `~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/`. To refresh a
|
||||
stale cache after source edits:
|
||||
|
||||
```bash
|
||||
bin/refresh-plugins [marketplace-names...] # refresh all or specific local marketplaces
|
||||
```
|
||||
|
||||
This uninstalls and reinstalls each plugin, forcing the cache to repopulate. See
|
||||
`bin/refresh-plugins --help` for usage. After refreshing,
|
||||
`claude plugin details <name>@<marketplace>` should show updated skill descriptions and hook
|
||||
counts. Directory-source marketplaces (e.g., `local-plugins` sourced from
|
||||
`~/.claude/plugins`, `cc-plugins` sourced from `~/dev/cc-plugins`) are the only ones that
|
||||
need manual refresh — public marketplaces auto-update.
|
||||
Runbooks (plugin rename/move, plugin cache refresh via `bin/refresh-plugins`) live in
|
||||
[implementation-status/operational-procedures.md](implementation-status/operational-procedures.md)
|
||||
— stable leaf, referenced from CLAUDE.md.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
# Memory infrastructure (Graphify, memsearch, vault git sync) — detail
|
||||
|
||||
_Leaf file of [../implementation-status.md](../implementation-status.md). Read on demand._
|
||||
|
||||
## Graphify — v0.8.31 at `/home/jared/.local/bin/graphify`
|
||||
|
||||
- Vault graph: `~/Documents/SecondBrain/graphify-out/` — disposable, structural index
|
||||
rebuilt by SessionStart hook; handles relational/graph queries. Distinct from
|
||||
vault-index.json (planned, Plan B Phase 1): a flat `{tag: [{path, title, summary}]}`
|
||||
lookup rebuilt at SessionEnd, queried by the `/memory-find` skill for fast tag-based SB
|
||||
note discovery.
|
||||
- Project graph: `<project-root>/graphify-out/` — same pattern; gitignore it in each project
|
||||
repo. Exempt from the `.cc-os/` state consolidation (ADR-0027).
|
||||
- Vault conventions: `~/Documents/SecondBrain/vault-conventions.md` — frontmatter contract +
|
||||
tag taxonomy (canonical name decided 2026-06-09; formerly referred to as CONVENTIONS.md).
|
||||
|
||||
## memsearch — v0.4.6 via Claude Code plugin marketplace (`memsearch@memsearch-plugins`)
|
||||
|
||||
- Hooks: Stop, SessionStart, UserPromptSubmit, SessionEnd (ship with plugin)
|
||||
- Memory store: `~/.memsearch/memory/YYYY-MM-DD.md` daily files (global, cross-project, all
|
||||
clients — one store by design; ADR-0015)
|
||||
- Index: `~/.memsearch/milvus.db` (Milvus Lite, local); embeddings via ONNX bge-m3
|
||||
- Config: `MEMSEARCH_DIR=~/.memsearch` in `~/.zshrc` for global scope
|
||||
- Skills: `/memory-recall`, `/memory-config` (ship with plugin)
|
||||
- Git sync: `~/.memsearch` is a dedicated private Forgejo repo
|
||||
(`forgejo.swansoncloud.com/jared/memsearch`); whitelist `.gitignore` commits only
|
||||
`memory/*.md`; auto-commit+push wired into os-vault's `memsearch_sync.py` SessionEnd hook
|
||||
(relocated from `session-end.sh` by ADR-0016; behavior per ADR-0015)
|
||||
|
||||
## Obsidian vault git sync — `~/Documents/SecondBrain` (2026-06-15)
|
||||
|
||||
- Remote: `ssh://git@forgejo.swansoncloud.com:2222/jared/SecondBrain.git` (private Forgejo;
|
||||
web: `https://forgejo.swansoncloud.com/jared/SecondBrain`)
|
||||
- 52 files tracked at initialization (notes, journal, templates, vault-conventions.md,
|
||||
CLAUDE.md, .obsidian config); `graphify-out/` excluded via `.gitignore`
|
||||
- Auto-commit+push via os-vault's `vault_sync.py` SessionEnd hook (mirrors
|
||||
`memsearch_sync.py`); SessionStart fast-forward pull added 2026-07-10 for multi-machine
|
||||
freshness (see [os-vault.md](os-vault.md))
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
# Operational procedures
|
||||
|
||||
_Stable runbooks referenced from CLAUDE.md. Keep this file's procedures current — they are
|
||||
the canonical copies._
|
||||
|
||||
## Renaming or moving a local plugin
|
||||
|
||||
Renaming a plugin directory under a directory-source marketplace (like `local-plugins`, whose
|
||||
source is `~/.claude/plugins` itself) requires updating **three** places, not just the
|
||||
directory and `settings.json`:
|
||||
|
||||
1. **Plugin directory / symlink** — the actual files (or symlink target, for plugins that
|
||||
live in a git repo like `cc-os/plugins/os-vault/`).
|
||||
2. **Marketplace manifest** — `~/.claude/plugins/.claude-plugin/marketplace.json` (for
|
||||
`local-plugins`) lists each plugin's `name` and `source` path explicitly; this does
|
||||
**not** auto-discover from the directory listing. Update the entry to match the new
|
||||
name/path.
|
||||
3. **`settings.json` `enabledPlugins`** — the `<name>@<marketplace>` key must match the
|
||||
manifest entry from step 2.
|
||||
|
||||
Then refresh the plugin manager's state so it re-resolves the marketplace and drops the
|
||||
stale install record:
|
||||
|
||||
```bash
|
||||
claude plugin marketplace update <marketplace-name> # re-validates the manifest
|
||||
claude plugin install <new-name>@<marketplace-name> # creates the new install record
|
||||
claude plugin uninstall <old-name>@<marketplace-name> # drops the stale one
|
||||
claude plugin list # verify: new name enabled, old name gone
|
||||
claude plugin details <new-name>@<marketplace-name> # verify skills/agents/hooks resolved
|
||||
```
|
||||
|
||||
Skipping step 2 is the failure mode to watch for: hooks (wired by absolute path in
|
||||
`settings.json`) keep working, masking the fact that skills/slash-commands silently stopped
|
||||
registering under the plugin's new name. See ADR-0018.
|
||||
|
||||
## Editing a local plugin (cache refresh)
|
||||
|
||||
Plugin source edits (including SKILL.md wording, hook scripts, or CLI code) do not reach
|
||||
running sessions until the cache is refreshed. Claude Code caches plugins when installed,
|
||||
copying them to `~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/`. To refresh a
|
||||
stale cache after source edits:
|
||||
|
||||
```bash
|
||||
bin/refresh-plugins [marketplace-names...] # refresh all or specific local marketplaces
|
||||
```
|
||||
|
||||
This uninstalls and reinstalls each plugin, forcing the cache to repopulate. See
|
||||
`bin/refresh-plugins --help` for usage. After refreshing,
|
||||
`claude plugin details <name>@<marketplace>` should show updated skill descriptions and hook
|
||||
counts. Directory-source marketplaces (e.g., `local-plugins` sourced from
|
||||
`~/.claude/plugins`, `cc-plugins` sourced from `~/dev/cc-plugins`) are the only ones that
|
||||
need manual refresh — public marketplaces auto-update.
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
# os-adr — component detail
|
||||
|
||||
_Leaf file of [../implementation-status.md](../implementation-status.md). Read on demand._
|
||||
|
||||
**Global os-adr plugin** — `cc-os/plugins/os-adr/` (git-tracked, 2026-07-03); symlinked into
|
||||
`~/.claude/plugins/os-adr`. Decision records: ADR-0020 (build + open-question resolutions,
|
||||
amended 2026-07-12 for the cc-os retrofit promotion), ADR-0021 (Eval A design).
|
||||
|
||||
## Component inventory
|
||||
|
||||
- Purpose: Architecture Decision Records, consistently in every project — one
|
||||
customized-Nygard template (frontmatter
|
||||
`id`/`date`/`status`/`supersedes`/`superseded-by`/`affected-paths`/`affected-components`;
|
||||
Context/Decision/Consequences/Alternatives-rejected sections),
|
||||
`docs/adr/NNNN-kebab-title.md` one-file-per-decision + fully-regenerated
|
||||
`docs/adr/README.md` index. Built from `docs/adr-system/` (requirements `04`, PRD `05`,
|
||||
eval sketches `06`) via OpenSpec change `add-os-adr-plugin`.
|
||||
- Core: Ruby OO library `lib/adr/` (`Record`, `Repository`, `Index`, `Template`, `Detector`,
|
||||
`Migrator`, `MigrationReport`, `Finder`; Sandi Metz style, injected paths) behind thin
|
||||
CLIs in `bin/` (`adr-new`, `adr-init`, `adr-detect`, `adr-migrate`, `adr-find`). Tests:
|
||||
`ruby tests/all.rb` (47) + `python3 tests/hook_test.py` (15), all model-free. Behavioral
|
||||
invariants in `invariants.md`.
|
||||
- Hook: **moved to os-status (2026-07-06)** — the SessionStart existence check now runs as
|
||||
the `adr-system-present` check in the os-status plugin (`hooks.json` here is empty;
|
||||
`hooks/session_start.py` stays as the wording source of record, byte-copied into
|
||||
os-status). Behavior unchanged: present → near-zero-token usage note naming
|
||||
`/os-adr:create` + `/os-adr:find`; absent → once-per-day init/migrate suggestion, silenced
|
||||
permanently by `.os-adr/suppress` (still honored); silent outside git projects.
|
||||
- Skills (verb-first, no `commands/`): `create` (one-invocation templated+numbered+indexed
|
||||
ADR with mechanical supersession), `init`, `migrate` (non-destructive: detect surveyed
|
||||
shapes → mechanical heuristic fill → LLM fills only manifest-listed interpretive fields
|
||||
via `--apply-fills` → `migration_confidence` frontmatter + `docs/adr/migration-report.md`;
|
||||
old-system deletion only as an explicit separate user-approved step), `find`
|
||||
(deterministic-first retrieval: affected-paths/components match → Accepted-only filter →
|
||||
one-hop Graphify query-path expansion with graceful degradation → AI judgment over the
|
||||
candidate set only).
|
||||
- Migration pilot passed 2026-07-03 (sandboxed project copies; gate threshold 25%
|
||||
low-confidence flags): viking-warrior-training-log 0%, delta-refinery 0%, llf-schema 8.3%.
|
||||
Retrieval verified end-to-end against the real llf-schema project graph. Resolved: ADRs
|
||||
need no Graphify tag/edge convention — retrieval layer 3 expands the *query* paths via
|
||||
graph-node `source_file`s.
|
||||
|
||||
## Evals
|
||||
|
||||
- **Eval A harness (2026-07-03, ADR-0021):** `plugins/os-adr/eval/` — prompted
|
||||
skill-execution eval across model tiers (haiku/sonnet). Two fixtures (generated with the
|
||||
plugin's own CLIs), six scenarios (S1 create, S2 create+supersede, S3 find/conflict, S4
|
||||
find/distractor, S5 init, S6 migrate+fills), deterministic Ruby checker (`eval/bin/check`,
|
||||
structural invariants, TSV mode for autoresearch), sandbox + headless-runner scripts,
|
||||
runner-prompt template. Primary run mode: in-session Agent-tool subagents with pinned
|
||||
`model:`; optimized via the `/autoresearch` Classic loop over SKILL.md *wording only*.
|
||||
Procedure: `plugins/os-adr/eval/README.md`. Grid run 2026-07-03, all scenarios × both
|
||||
tiers passing. Distinct from the held-out Eval B — do not conflate.
|
||||
- **Eval B harness (2026-07-03, OpenSpec change `add-os-adr-eval-b-harness`):**
|
||||
`plugins/os-adr/eval-b/` — held-out unprompted-behavior eval (requirements 4–5). 7
|
||||
scenarios (W1–W3 write-trigger, R1–R4 retrieval) from the frozen shapes in
|
||||
`docs/adr-system/06-eval-scenarios.md`; Ruby webhook-relay fixture with a 6-ADR history
|
||||
(Superseded pair + near-miss distractors); R4's one-hop graph reach uses a real
|
||||
`graphify update` AST build (rebuilt via `eval-b/bin/build-fixture-graph`, never
|
||||
committed). **Headless-only runner** (`eval-b/bin/run` — fresh `claude -p` per rep, cwd =
|
||||
sandbox, so the real SessionStart hook fires) and a two-axis deterministic-first checker
|
||||
(`eval-b/bin/check`): (a) unprompted consultation, mechanical; (b) correct-ADR citation
|
||||
(R1–R4) or new-ADR-file with a narrow frozen-rubric haiku judge fallback (W1–W3,
|
||||
stubbable via `ADR_EVAL_B_JUDGE_CMD`). **Scenario Task blocks are held-out — never run
|
||||
informally.** Procedure: `plugins/os-adr/eval-b/README.md`. Baseline grid 2026-07-03:
|
||||
haiku 0/8, sonnet 5/8 (fails W3, R1); `R4-nograph` degradation variant FAILed both tiers
|
||||
as expected. Results: vault note [[os-adr-eval-b-grid-results-and-observations]].
|
||||
- **Wording experiment complete (2026-07-04):** `/autoresearch` loop over Eval B wording (5
|
||||
iterations; checker/fixtures/scenarios/rubric frozen) closed the gap — final full grid
|
||||
**sonnet 8/8, haiku 7/8** (haiku's one miss is a W3 axis-b judge-boundary flicker).
|
||||
Winning wording shipped in `hooks/session_start.py` (PRESENT_NOTE), find/create SKILL.mds,
|
||||
and `eval-b/fixture/project/CLAUDE.md` (the trigger-phrased "Architecture decisions"
|
||||
section — the template for real-project adoption). Confirmed mechanisms:
|
||||
trigger-conditioned when→then phrasing beats inventory phrasing on both tiers; each rule
|
||||
must live where its precondition is visible (the reversal→supersede rule in the find
|
||||
skill's act-on-findings step fixed W3); lower tiers need *mechanical* triggers — semantic
|
||||
triggers only reach sonnet. Open: channel ablation never run; R4-nograph now passes both
|
||||
tiers so no longer differentiates. Full mapping: vault note
|
||||
[[os-adr-eval-b-wording-experiment-hypotheses]]. **Before designing or running any
|
||||
autoresearch eval, Read
|
||||
`~/Documents/SecondBrain/howto/running-autoresearch-skill-evals.md`.**
|
||||
- **Eval B W3 stability check (2026-07-06):** 7 headless reps of haiku × W3: axis (a) 7/7
|
||||
PASS; axis (b) ~5/7 — intermittent flicker, not a hard gap; grid claim stays **haiku
|
||||
7/8**; haiku cleared for daily use with that caveat. Recorded in
|
||||
[[os-adr-eval-b-wording-experiment-hypotheses]].
|
||||
- **Eval C harness (2026-07-06):** `plugins/os-adr/eval-c/` — ambiguity-ladder
|
||||
DISCRIMINATION eval (held-out; Eval B is contaminated by the wording loop). 3 levels
|
||||
(explicit → moderate → conceptual) × paired positive/negative scenarios; run-set (6,
|
||||
job-execution domain) + frozen reserve-set (6, notifications domain). Python `taskq`
|
||||
fixture with 6-ADR history and the eval-b trigger-phrased CLAUDE.md section. Scoring:
|
||||
positives = both axes; negatives FAIL **only on unneeded ADR creation**. Model-free
|
||||
`bin/self-test` green. **Scenario Task blocks are held-out — never run informally.**
|
||||
Methodology: vault notes [[eval-methodology-ladder]] and
|
||||
[[eval-methodology-irl-feedback-loop]]; vault gained an `eval-results` note type +
|
||||
template (2026-07-06).
|
||||
- **Eval C frozen grid run (2026-07-06):** run-set × {sonnet, haiku} × 3 reps/cell (36
|
||||
headless reps, all counted) — **12/12 cells PASS at ≥2/3 majority; haiku 18/18 reps,
|
||||
sonnet 17/18**. Zero over-trigger ADR creation on any negative; every positive pass via
|
||||
governing-ADR citation (16) or judge-recognized proposal (1), never duplicate creation.
|
||||
Sonnet's one FAIL rep is a final-message-citation boundary artifact. Two harness defects
|
||||
fixed on the first live rep, BEFORE the grid (run pipefail bug; mechanical
|
||||
`cited-governing` PASS branch + two self-test guards). Verdict: the Eval B wording
|
||||
**generalizes** — grid saturated; next signal is production (rollout + IRL session
|
||||
audits). Full results: vault note [[os-adr-eval-c-frozen-grid-results]].
|
||||
|
||||
## cc-os retrofit and rollout
|
||||
|
||||
- **cc-os ADR retrofit (2026-07-12, via `/os-status:fix`):** the 27-ADR monolithic
|
||||
`docs/memory-system/03-architecture-decisions.md` was migrated to `docs/adr/` with
|
||||
`/os-adr:migrate`, promoted ahead of the pilot projects (ADR-0020 amended in place).
|
||||
Mechanics: the detector only scans `docs/{adr,decisions,decision}/` + `DECISIONS.md`, so
|
||||
the source was shimmed in via a format-normalized copy under `docs/decisions/`
|
||||
(bullet-label → line-start bold-label so the parser could extract sections). Result: 27/27
|
||||
converted, flag rate 3.7% (one low: 0016), consequences filled per file from source
|
||||
material (sonnet-delegated), 0004/0005/0006 marked Superseded by 0010 with frontmatter
|
||||
links, `migration_source` repointed at the real monolith path, shim removed. The monolith
|
||||
is retained untouched as the historical source; new decisions go through `/os-adr:create`.
|
||||
- **Remaining (rollout order, amended 2026-07-12):** real-project migration/adoption one at
|
||||
a time via `/os-adr:migrate` — pilot projects, then wider. When onboarding real projects,
|
||||
add the trigger-phrased "Architecture decisions" CLAUDE.md section (copy from
|
||||
`eval-b/fixture/project/CLAUDE.md`; candidate: emit it from `/os-adr:init`/`migrate`).
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
# os-backlog — component detail
|
||||
|
||||
_Leaf file of [../implementation-status.md](../implementation-status.md). Read on demand._
|
||||
|
||||
**Global os-backlog plugin** — `cc-os/plugins/os-backlog/` (git-tracked, 2026-07-10);
|
||||
symlinked into `~/.claude/plugins/os-backlog`; PRD Forgejo issue #9 (state pointer: Planka
|
||||
card `1816234194716592051`), slices #10–#17. Decision records: ADR-0023 (own plugin, not an
|
||||
os-vault extension), ADR-0025 (standard Ruby plugin structure), ADR-0029 (card-move +
|
||||
column ownership).
|
||||
|
||||
## Component inventory
|
||||
|
||||
- Purpose: the AI's process-management surface over Planka — deterministic tracker routing
|
||||
(Planka = task state by default; git issues = implementation specs only), mid-session
|
||||
capture, pull-only listing, board lifecycle, and the three-value autonomy convention
|
||||
(`hitl`/`semi`/`afk-ready`; amended per ADR-0029: afk-ready ships straight to Done, semi
|
||||
stops at Review).
|
||||
- Lib (Ruby, os-adr `lib/`+`bin/` pattern, installed planka-api gem only — no source
|
||||
coupling): `board_spec.rb` (uniform lists/labels contract), `board_ensurer.rb`
|
||||
(idempotent create/repair; Planka 2.1.1 `type` fields, shared-project + owner-manager
|
||||
visibility fix, label color candidates with fallback; `archived--` rename convention),
|
||||
`config.rb` (`.cc-os/config` parser), `resolver.rb` (pure repo→board decision:
|
||||
`use`/`activate`/`stop-and-discuss`; no network), `cards.rb` (add-at-Backlog, snapshot,
|
||||
attach_label, comment, move with ownership rules), `tracker.rb` + `inspector.rb`
|
||||
(2026-07-12, issue #14 — tracker-format validation/remote classification + in-repo
|
||||
issue-file discovery; `Config.merge` preserves other keys).
|
||||
- CLI: `bin/os-backlog` — `board-ensure`, `activate`, `archive`, `resolve`, `card-add`,
|
||||
`cards`, `snapshot`, `card-label`, `card-comment`, `card-move` (2026-07-12, ADR-0029),
|
||||
`inspect`, `config-write`; every path fails soft (one-line error, exit 1) when the
|
||||
gem/Planka is unavailable.
|
||||
- Skills: `/os-backlog:capture`, `/os-backlog:list` (pull-only per notification policy v2),
|
||||
`/os-backlog:route` (2026-07-12, issue #14 — tracker onboarding conversation; destination
|
||||
choice + live-history migration are named human gates) — carry the column-ownership rules
|
||||
(AI creates at Backlog, Doing→Review at most, never Next, never self-assigns `hitl`;
|
||||
"working a card means moving it" contract added 2026-07-12).
|
||||
- Agents: `agents/card-triage.md` (priority + autonomy labels only; ambiguity → `hitl`),
|
||||
`agents/board-audit.md` (four drift classes; writes at most comments).
|
||||
- Tests: `tests/` — 68 runs/153 assertions as of 2026-07-12 (35 → 54 → 58 → 68 across the
|
||||
slices) against an in-memory `FakePlankaClient`; no live API. Companion os-status check
|
||||
`tracker-configured` (issue #11) validates the `tracker` key grammar and nudges
|
||||
unconfigured git projects (daily-snoozed).
|
||||
|
||||
## Build and verification history
|
||||
|
||||
- **AFK slices shipped (2026-07-10):** #10 plugin scaffold + `board-ensure` (uniform
|
||||
lists/labels incl. new `semi` autonomy label), #11 `tracker` key in `.cc-os/config` +
|
||||
os-status check, #12 pure routing resolver CLI, #13 capture/list skills + card CLI, #15
|
||||
`card-triage` + `board-audit` named agents. Live verification same day: planka-api gem
|
||||
0.1.0 installed locally (`rake install`, rbenv 4.0.2; publish/versioning parked behind gem
|
||||
issue 13); `snapshot`, `board-ensure` (created the missing `semi` label on the cc-os
|
||||
board), `card-add` + `card-label` all succeeded against production Planka with bot
|
||||
credentials sourced from ovh-vps (`~/services/planka/bot-credentials.env`) in a subshell.
|
||||
Follow-up captured as a `hitl` Backlog card: get planka_bot into credvault (no import
|
||||
path — rotate vs. add import capability is a human decision).
|
||||
- **planka-api 0.2.0 typed-returns migration (2026-07-10, commit `8df0cb7`):** 0.2.0 returns
|
||||
`Planka::Types` value objects instead of parsed-JSON hashes. `board_ensurer.rb` and
|
||||
`cards.rb` rewritten on typed accessors; `Cards#snapshot` builds explicit string-keyed
|
||||
hashes from `BoardDetail#lists`/`#cards`/`#labels`/`#card_labels`. Test fakes rebuilt to
|
||||
return real `Planka::Types` records. JSON output stays byte-compatible, so skill/agent
|
||||
contracts are unchanged. Follow-up fix (issue #18, same day): the migration had silently
|
||||
dropped one field from the per-card snapshot hash; restored the full 19-key camelCase card
|
||||
key set plus `labels`, with a regression test pinning the key set including
|
||||
`listChangedAt`.
|
||||
- **Live verification 0.2.0 (2026-07-10, issue #20):** local machine had no `PLANKA_*`
|
||||
env/credvault entry, so the plugin tree was rsynced to `ovh-vps:/tmp/os-backlog-verify`
|
||||
and exercised there with bot credentials sourced in a subshell (contents never read or
|
||||
printed). Results: tests 35 runs/0 failures on ovh-vps; `board-ensure cc-os` idempotent
|
||||
(twice `(existing)`, nothing created); card-add/card-label/card-comment/snapshot all
|
||||
correct with the full 20-key set; test card deleted via `Planka::Client#cards.delete`
|
||||
(the CLI has no delete subcommand) and confirmed gone; verification tree removed. No
|
||||
regressions vs. pre-0.2.0.
|
||||
- **Slice 7 (2026-07-13, issue #16):** SessionStart injection note shipped
|
||||
(`hooks/session_start.py` + `hooks/hooks.json`, os-adr-style plugin-local hook): WHEN→THEN
|
||||
rules for capture / routing / promotion / column ownership / autonomy labels, ADR-0029
|
||||
semantics (the issue body's "afk-ready → Review" was wrong — caught in Codex review),
|
||||
rules only, zero board state, ~447 tokens. `/to-issues` (global skill,
|
||||
`~/.agents/skills/to-issues/`) gained a destination-tracker step reading the
|
||||
`.cc-os/config` tracker key; the `planka:`/unset default publishes slices as git issues
|
||||
on the repo remote + one Planka pointer card (spec/state boundary preserved — human gate
|
||||
decided 2026-07-13). `Backlog::Tracker.issues_destination` + 6 routing tests.
|
||||
- **Board-id cache (2026-07-13, issue #22):** `Backlog::BoardResolver` with injected config
|
||||
read/write lambdas; `board_id=` cached in `.cc-os/config` beside `tracker=` (name stays
|
||||
source of truth; id honored only for `planka:` trackers and validated against the
|
||||
returned board name; 404/mismatch/any cache failure degrades to name lookup and rewrites
|
||||
the cache; write failures never fail the command). Comment-preserving `Config.set`
|
||||
mirrors os-status's writer. 14 new tests; suite 90 runs / 198 assertions / 0 failures.
|
||||
- **Slice 8 (2026-07-13, issue #17):** Operations board created in the Dev project (board
|
||||
`1818308120145822783`) with uniform lists/labels; recurring card "Biweekly orchestration
|
||||
IRL audit (recurring)" (card `1818310715438531668`, P2 + semi, first progress comment
|
||||
`1818310889929966683`; recurrence contract: comment + move back to Backlog, never Done).
|
||||
Tracker-routing convention canonized as **ADR-0033** (Planka = state / git issues = specs,
|
||||
tracker key grammar, pointer + promotion rules; cites route/to-issues/session-start
|
||||
rather than restating). os-context audit rubric gained category 8 `tracker-routing`
|
||||
(capture/route/promote compliance only). Bug found live and fixed: label color fallback
|
||||
only rescued `Planka::ValidationError`, but Planka rejects unknown colors as HTTP 400
|
||||
`Planka::BadRequestError` — fallback now rescues both (regression test; suite 91 runs /
|
||||
0 failures). os-status `/os-status:fix` step d reworded: invoke `/os-backlog:route`
|
||||
directly, never pre-ask "forgejo or planka?" (boundary rule uses both — surfaced by the
|
||||
servers-repo onboarding, cf. issue #26).
|
||||
- **Issue-shape classification (2026-07-13, issue #26):** `inspect` now fetches lightweight
|
||||
remote-issue metadata (tea `--output json` with string-index/joined-labels normalization;
|
||||
gh `--json`; fail-soft → nil) and emits an always-present `issue_shape` key
|
||||
(`sequential-chain` / `flat-adhoc` / `mixed` / null). Classification is a pure
|
||||
`Inspector.classify_issue_shape` (label allowlist with whole-token matching; cross-refs
|
||||
must hit the fetched set with dependency language within 40 chars; <3 issues or no
|
||||
metadata → null). Route SKILL.md step 2 rewritten as three branches with **split as the
|
||||
default** for spec-shaped issue sets (still exactly one tracker key — division of labor,
|
||||
not two destinations); step 3 got a never-re-trigger exception; boundary rule now cites
|
||||
ADR-0033 instead of restating it. 12 new tests incl. a CLI-contract test; suite 103 runs /
|
||||
212 assertions / 0 failures.
|
||||
- **Cross-project filing (2026-07-13, issue #27, ADR-0034):** `Backlog::ProjectIndex` —
|
||||
derived global index at `~/.cc-os/projects.json` (atomic temp+rename writes, corrupt/
|
||||
missing → empty, `CC_OS_HOME` override), upserted fail-soft by config-write (canonical
|
||||
path = realpath of the directory the config was written into — NOT the git toplevel, so
|
||||
umbrella subprojects index as themselves; corrected 2026-07-13 review), queried via new
|
||||
`projects [filter]` CLI subcommand (filter matches name, tracker, or path). Discoverer template + tea/gh/card-add mechanics in
|
||||
`skills/route/references/cross-project-filing.md`; one CROSS-PROJECT rule line added to
|
||||
the SessionStart note. 9 new tests; suite 112 runs / 233 assertions / 0 failures. Live
|
||||
smoke: cc-os row present in the real index.
|
||||
- **Wakeup design spike (2026-07-13, issue #28, ADR-0035/0036):** poll + tmux direction
|
||||
chosen at the human gate. `Backlog::Wakeup` + `bin/wakeup-poll` thin pilot (dry-run
|
||||
default, `--spawn` to create `cc-<project>-issue<N>` tmux sessions and persist
|
||||
`~/.cc-os/wakeup-state.json`; opt-in `wakeup=true` in `.cc-os/config`; afk-ready → act,
|
||||
semi → triage-only, else notify-only into pull-only `~/.cc-os/wakeup-summary.md`). No
|
||||
cron installed; 6 tests; suite 128 runs / 0 failures. Follow-up recorded: single
|
||||
designated poller machine (state stays per-host); cadence/cron mechanism and a
|
||||
`config-write` wakeup key deferred until a first project opts in.
|
||||
- **Review corrections (2026-07-13, cards #1818330518584820864 / #1818330528525321347):**
|
||||
post-ship review (Codex + verification pass) of the triage hook and project index:
|
||||
`Config#planka_board` (`tracker=planka:<board>` now counts everywhere, incl. the
|
||||
Resolver, not just the explicit `board` key); triage hook board resolution moved to pure
|
||||
`TriageCheck.board_name_for` honoring the umbrella rule (session cwd's `.cc-os/config`
|
||||
overrides the git root's; hook previously read only the root config); index keying fixed
|
||||
per above; hook timeout 8→15s; `CC_OS_DEBUG=1` surfaces swallowed hook errors on stderr.
|
||||
Known residual (captured as a Backlog card): board lookup is name-only across ALL Planka
|
||||
projects — duplicate board names in different projects can resolve to the wrong board.
|
||||
Suite 138 runs / 295 assertions / 0 failures.
|
||||
- **Outstanding:** #14 residual (onboard one more project — operational hitl); wakeup
|
||||
rollout (first opt-in project + poller machine + cadence).
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
# os-context (formerly os-orchestration) — component detail
|
||||
|
||||
_Leaf file of [../implementation-status.md](../implementation-status.md). Read on demand._
|
||||
|
||||
**Global os-context plugin** — `cc-os/plugins/os-context/` (git-tracked); symlinked into
|
||||
`~/.claude/plugins/os-context`. Migrated 2026-07-03 from a standalone repo
|
||||
(`~/dev/cc-plugins/orchestration/`), superseding per-project orchestration text (ADR-0019);
|
||||
renamed and generalized from os-orchestration 2026-07-13 (ADR-0031). Entries below predating
|
||||
the rename refer to the plugin by its former name.
|
||||
|
||||
## Component inventory
|
||||
|
||||
- Hooks: `hooks/inject.py` — SessionStart prompt composer: globs
|
||||
`prompts/session-start/*.md`, sorts by filename, concatenates into one additionalContext
|
||||
block for all sessions, with a deterministic line-count budget (warn >120 lines, refuse
|
||||
>240 lines). Byte-identical output to the old single-file `ORCHESTRATION.md` behavior when
|
||||
only `10-orchestration.md` is present (tested).
|
||||
- Prompt files (2026-07-13): `10-orchestration.md` (the canonical session-orchestration +
|
||||
delegation-economics rules; formerly `ORCHESTRATION.md`), `20-kickoff-conventions.md`,
|
||||
`30-standing-safety.md`, `40-decision-memos.md` (43 lines; total injection 102/120).
|
||||
Decision-memo auto-memory file deleted per single-source rule.
|
||||
- Skill: `/os-context:audit-sessions` (formerly `/os-orchestration:audit-sessions`).
|
||||
- State: `~/.local/state/os-context/` (old `os-orchestration` path symlinked;
|
||||
`OS_ORCH_AUDIT_LEDGER` env var name kept). os-status's `orchestration-audit-due` check
|
||||
updated to match.
|
||||
|
||||
## Session audits and wording iterations
|
||||
|
||||
- **WS1 session audit (2026-07-06):** 10 stratified real sessions audited via
|
||||
`audit/bin/extract` (Ruby fact-sheet extractor, dual-use as future eval checker) +
|
||||
parallel sonnet auditors + verified synthesis. Findings + E1–E4 eval-scenario backlog:
|
||||
`docs/orchestration-audit/2026-07-06-findings.md`. Headlines: model param silently
|
||||
overridden to haiku on all 23 verified spawns — root cause
|
||||
`CLAUDE_CODE_SUBAGENT_MODEL=haiku` in `~/.claude/settings.json` env block (removed
|
||||
2026-07-06); omitted model param inherits the main-loop model; no over-sharing/full-dump
|
||||
failures anywhere.
|
||||
- **Wording fixes shipped (2026-07-06, post-audit):** ORCHESTRATION.md rewritten with
|
||||
trigger-conditioned when→then phrasing for the three verified clusters — explicit
|
||||
`model:` before every Agent call, subagent model self-report on sonnet/opus spawns (the E1
|
||||
canary proved `resolvedModel` is NOT model-visible in the launch result; self-report is
|
||||
the only observable channel), don't re-cover your own ground before delegating.
|
||||
Delegation thresholds unchanged. Deterministic env-override detection delegated to
|
||||
os-status (WS3).
|
||||
- **Eval harness E1–E3 (2026-07-06):** `plugins/os-context/eval/` — scripted headless
|
||||
orchestration-behavior eval scored by the audit extractor (extended with per-segment read
|
||||
targets). 6 run-set scenarios (E1P/N downgrade detection with runner stub; E2P/N
|
||||
explicit-model fan-out vs no-delegation-on-trivial; E3P/N delegate-investigation vs
|
||||
orienting-reads-correct) + 6 frozen reserve twins; Node.js "relaystation" fixture with
|
||||
deterministic sandbox-time log generation; deterministic-first checker (`eval/bin/check`,
|
||||
narrow frozen haiku-judge fallback for E1 language axes only, stubbable via
|
||||
`ORCH_EVAL_JUDGE_CMD`); model-free `bin/self-test` 21/21. **Scenario Task blocks are
|
||||
held-out — never run informally.** Procedure: `plugins/os-context/eval/README.md`.
|
||||
- **Baseline grid (2026-07-06,** run-set × sonnet/haiku × 3 reps + 2 counted canaries):
|
||||
negatives 18/18 PASS both tiers (zero over-delegation, zero false downgrade claims);
|
||||
positives 1/18 on delegate-at-all — but transcript verification shows most positive FAILs
|
||||
are *superior direct strategies* (scripted bulk edit; surgical greps over 400KB logs),
|
||||
which the WS1 audit itself scored as justified non-delegation, vs genuine miss shapes
|
||||
(haiku: whole 112KB logs into own context; 20-call per-file grind). 14/14 spawns
|
||||
explicit-model; the one delegating E1P rep flagged the stubbed downgrade via self-report.
|
||||
E2P/E3P A-axis criterion redesign pre-registered; reserve stayed frozen. ORCHESTRATION.md
|
||||
gained a scripted-bulk-edit carve-out. TSV: `eval/results/2026-07-06-baseline-grid.tsv`;
|
||||
analysis: vault note [[os-orchestration-eval-baseline-grid-results]].
|
||||
- **WS4 economics loop COMPLETE + SHIPPED (2026-07-08;** run-set AND reserve now
|
||||
contaminated for future wording tuning): ORCHESTRATION.md v3 shipped — cost-asymmetry +
|
||||
tier-conditional threshold opener, symmetric delegate-when/work-directly-when trigger
|
||||
lists, batching rule (~5–8 items per grouped spawn, SendMessage reuse), async rule,
|
||||
Workflow effort dial; explicit-model/self-report/don't-re-cover-ground rules unchanged.
|
||||
Harness extended first (commit b226601): real token-econ axes — subagent transcripts
|
||||
persist at `<projects>/<flat-cwd>/<session-id>/subagents/agent-*.jsonl` with per-message
|
||||
usage blocks (vault note [[claude-code-subagent-transcripts-and-token-accounting]]) — E5
|
||||
batching pair + reserve twins (subagent-authored, held out), fable column, pre-registered
|
||||
E2P/E3P criterion redesign landed. Results: E5P 0/6 → majority-PASS both tiers incl.
|
||||
reserve twin 5/5 (fable median mltok 43% of baseline); negatives clean everywhere
|
||||
(36/36). Residual gaps: E1P sonnet delegate-at-all (fixture too small), fable
|
||||
downgrade-flag ~1/4 judge flicker. 15 truncated baseline reps mechanically
|
||||
excluded+replaced (limit-banner sweep promoted to the autoresearch howto). TSVs:
|
||||
`eval/results/2026-07-08-*.tsv`; loop log: `autoresearch/classic-260708-1039/`; vault
|
||||
note [[os-orchestration-ws4-econ-wording-results]].
|
||||
- **First IRL re-audit DONE (2026-07-10, ahead of schedule):** 19 production transcripts
|
||||
post-2026-07-08 audited by 3 parallel agents (run: `autoresearch/improve-260710-1057/`).
|
||||
Solved surfaces: model-param discipline (~48/49 spawns pinned), over-delegation guardrail.
|
||||
Tier 1 findings → same-day one-shot ORCHESTRATION.md production patch (grid validation
|
||||
impossible — sets contaminated): settled-design mid-session fan-out trigger,
|
||||
never-sleep-poll rule, own eval/benchmark loops named as mechanical sequences,
|
||||
unfamiliar-tree ~5-call Explore trigger. Acceptance check = next IRL audit (~2–3 weeks;
|
||||
baseline: settled-design drift 8/19 sessions). Side finding needing separate repro:
|
||||
harness ignored explicit `model: "sonnet"` pins on general-purpose agents (env override
|
||||
unset — distinct from WS1 cause); the self-report line caught it.
|
||||
- **IRL audit semi-automated (2026-07-10):** recurring biweekly audit formalized, working
|
||||
from any cwd. Deterministic layer: `audit/bin/audit-stats` (Ruby, composes
|
||||
`audit/bin/extract` fact-sheets) enumerates production transcripts since the last run
|
||||
(excludes sandboxes, `*-eval` dirs, active/trivial sessions), writes
|
||||
`~/.local/state/os-context/audit/run-<date>/` (stats.tsv/flags.md/sheets) and appends the
|
||||
trend ledger `metrics.tsv` (seeded 2026-07-10: 23 sessions, 52 spawns, 1 missing model).
|
||||
Judgment layer: `/os-context:audit-sessions` (rubric in `references/`) — sonnet auditor
|
||||
fan-out over flagged regions only, tiered synthesis, run-over-run trend verdicts, stops
|
||||
before any wording edit. Nudge: os-status check `orchestration-audit-due` warns when the
|
||||
ledger's last row is ≥14 days old (missing ledger = machine not opted in, silent; env
|
||||
override `OS_ORCH_AUDIT_LEDGER` for tests). Planka recurrence-manifest entry skipped
|
||||
(gem offsets are `+1y/+6m/+3m/+1m` only; a `+2w` offset is blocked on parked gem
|
||||
publish/versioning decisions).
|
||||
- **Audit-driven wording patch (2026-07-13):** IRL session audit (session fdee577b) found
|
||||
the delegation-economics rules failed to fire when a main-loop fable model implemented a
|
||||
fully-specified fix list itself via TDD (~110k tokens). Two approved additions to
|
||||
`prompts/session-start/10-orchestration.md`: (1) names a red-green TDD loop over an
|
||||
already-specified fix list as a single delegable unit (issue #29); (2) names
|
||||
review/diagnosis-agent convergence on a confirmed defect list as a fresh delegation
|
||||
decision, never a continuation of the main loop's review thread (issue #30). Applied
|
||||
directly (design pre-approved, no wording-loop re-run); `bin/refresh-plugins` run after
|
||||
the edit.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# os-doc-hygiene — component detail
|
||||
|
||||
_Leaf file of [../implementation-status.md](../implementation-status.md). Read on demand._
|
||||
|
||||
**Global os-doc-hygiene plugin** — `cc-os/plugins/os-doc-hygiene/` (git-tracked,
|
||||
2026-07-03); symlinked into `~/.claude/plugins/os-doc-hygiene`.
|
||||
|
||||
- Hooks: `hooks/hooks.json` → SessionStart hook (matcher: startup|resume) runs
|
||||
`scripts/reminder.py` via `${CLAUDE_PLUGIN_ROOT}` (5s timeout), emitting a deterministic
|
||||
zero-token reminder banner (once/day snooze).
|
||||
- Behavior: monitors stale and bloated project documentation; per-project state under
|
||||
`.cc-os/dochygiene/` (gitignored; moved from `.dochygiene/` 2026-07-12 per ADR-0027, with
|
||||
canonical-then-legacy fallback reads and auto-migration + legacy dir removal on first
|
||||
write; scanner self-excludes `.cc-os/`).
|
||||
- Skills (verb-first, no `commands/` dispatcher — invoked directly as
|
||||
`/os-doc-hygiene:<skill>`, per [[cc-os-plugin-skill-naming-convention]]): `check`
|
||||
(AI-assisted classification of staleness signals, emits machine+human reports), `clean`
|
||||
(AI-assisted or deterministic patch application with git-safe scoped cleanup), `status`
|
||||
(read-only lifecycle-timestamp read), `sweep` (check then clean in sequence).
|
||||
Reversion-protected via invariants.md + golden-example test fixtures.
|
||||
- Scanner signals: broken refs, version skew, churn, archive ratio, frontmatter markers,
|
||||
and — added 2026-07-13 (issue #25 part 1) — a deterministic `file_length` signal
|
||||
(defaults 400 lines / 4000 estimated tokens, `--max-lines` / `--max-tokens`; token
|
||||
estimate via `scripts/token_estimator.py`). Objective facts only; classification lives
|
||||
downstream. Suite 286 passed as of 2026-07-13 (279 at the ADR-0027 move).
|
||||
- Migration: migrated from standalone repo (`~/dev/cc-plugins/doc-hygiene/`, 2026-07-03) and
|
||||
integrated into cc-os; no content conflicts. Renamed `doc-hygiene` → `os-doc-hygiene` per
|
||||
the cc-os naming convention; skills renamed `hygiene-check`/`hygiene-clean` →
|
||||
`check`/`clean`; the `commands/hygiene.md` dispatcher was removed in favor of two new
|
||||
skills (`status`, `sweep`), aligning with the no-`commands/`-directory pattern
|
||||
(2026-07-03).
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# os-status — component detail
|
||||
|
||||
_Leaf file of [../implementation-status.md](../implementation-status.md). Read on demand._
|
||||
|
||||
**Global os-status plugin** — `cc-os/plugins/os-status/` (git-tracked, 2026-07-06);
|
||||
symlinked into `~/.claude/plugins/os-status`. Decision record: ADR-0022 (architecture),
|
||||
ADR-0026 (`/os-status:fix`), ADR-0027 (state location). Built via OpenSpec change
|
||||
`add-os-status-plugin` (supersedes the WS3 plan doc).
|
||||
|
||||
- Purpose: aggregated deterministic SessionStart status checks for the cc-os plugin family —
|
||||
per-project artifacts (ADR system, vault hub note) and environment hazards (subagent model
|
||||
env override).
|
||||
- Architecture: checks are **in-process** plain Python functions in `hooks/checks.py` with
|
||||
the uniform signature `check(ctx) -> CheckResult(status, message)` plus one registry entry
|
||||
(no per-plugin subprocess protocol, no per-project code copies — both rejected in
|
||||
ADR-0022). Three states: `ok` (silent), `note` (near-zero-token additionalContext line,
|
||||
never snoozed/suppressed), `warn` (aggregated into at most one banner; once-per-day snooze
|
||||
+ permanent `suppress-<check>` markers in gitignored per-project `.cc-os/status/` — state
|
||||
only, never code; shared `.cc-os/config` holds optional `hub`/`vault_path` key=value
|
||||
overrides).
|
||||
- Initial checks: `subagent-model-env-override` (env + `~/.claude/settings.json` `env`
|
||||
block; runs outside git projects too — catches the WS1 Cluster 1 incident deterministically
|
||||
at session start), `adr-system-present` (verbatim port of os-adr's hook behavior incl.
|
||||
`.os-adr/suppress`), `vault-hub-note-present` (config slug first, else `type/hub` +
|
||||
`project/<name>` facet-tag scan of the vault; missing → warn naming `/os-vault:write`).
|
||||
Later additions: `tracker-configured` (2026-07-10, os-backlog issue #11),
|
||||
`orchestration-audit-due` (2026-07-10), `project-graph-present` and
|
||||
`config-version-current` (2026-07-12).
|
||||
- Added 2026-07-12 (ADR-0026, issue #21): `/os-status:fix` skill — the unified project
|
||||
setup/update command; runs the registry via `python3 hooks/checks.py --json` (each result
|
||||
carries a `remediation` pointer), drives the owning plugin's remediation per failing
|
||||
check, stamps `version=` into `.cc-os/config` (`CURRENT_CONFIG_VERSION`); idempotent, so
|
||||
re-running is the update path. Warn banner names `/os-status:fix` as the single entry
|
||||
point. Human gates preserved: fix never fabricates a tracker value and never edits global
|
||||
settings.json.
|
||||
- Fix tune-up (2026-07-12, issues #23/#24): the credvault smoke-test of `/os-status:fix`
|
||||
(session cd1ae1cd) was on-contract but surfaced four gaps. Fixes: `clear_snooze` helper +
|
||||
self-clearing snoozes when a check evaluates ok/note (both runners); own state relocated
|
||||
to `.cc-os/status/` with legacy fallback read + migrate-on-write (ADR-0027);
|
||||
`write_config_value` normalizes the whole file to `key=value` (no spaces); fix SKILL.md
|
||||
scopes out committing, adds an explicit "`.cc-os/` in .gitignore" step, clarifies when
|
||||
re-runs are needed. Re-run of `/os-status:fix` in credvault is the pending IRL
|
||||
verification.
|
||||
- Tests: `python3 tests/hook_test.py` (80 as of 2026-07-12; 64 post-#21; 36 initial) incl.
|
||||
byte-identity of PRESENT_NOTE/ABSENT_NOTE against os-adr's source. Invariants in
|
||||
`invariants.md`. Smoke-tested 2026-07-06 (cache==source, real headless session fired the
|
||||
hook, env-override canary warns).
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
# os-vault — component detail
|
||||
|
||||
_Leaf file of [../implementation-status.md](../implementation-status.md). Read on demand._
|
||||
|
||||
**Global os-vault plugin** — `cc-os/plugins/os-vault/` (git-tracked, 2026-06-12); symlinked
|
||||
into `~/.claude/plugins/os-vault`. Renamed from `memory` 2026-07-03 (ADR-0018 records the
|
||||
marketplace-manifest lesson). Plugin sourcing/port architecture: ADR-0016.
|
||||
|
||||
## Component inventory
|
||||
|
||||
- Hooks: `hooks/` — `session_start.py` (SessionStart; staleness check + detached graph
|
||||
rebuild, and, as of 2026-07-10, a fast-forward-only `git pull` (`timeout 10 git pull
|
||||
--ff-only --quiet`) for `cfg.vault_path` and `cfg.memsearch_dir` before the staleness
|
||||
check — silent on success, quiet one-line stderr note on failure (offline machine), never
|
||||
blocking; closes the multi-machine push-only gap, mirroring the push style of the sync
|
||||
hooks in the opposite direction), `session_context.py` (project graph path only),
|
||||
`post_tool_use_write.py`, `session_end.py` (vault journal), `memsearch_sync.py` (second
|
||||
SessionEnd hook; memsearch auto-commit+push, 30s timeout), `vault_sync.py` (third
|
||||
SessionEnd hook; vault auto-commit+push to forgejo.swansoncloud.com/jared/SecondBrain, 30s
|
||||
timeout; mirrors memsearch_sync.py)
|
||||
- Shared modules: `config.py` (load_config → frozen Config dataclass), `hook_io.py`
|
||||
(read_input → HookInput dataclass), `session_state.py` (record_touch/read_touches;
|
||||
encapsulates `/tmp/claude-vault-touched-$SESSION_ID` contract)
|
||||
- Skills: `skills/` — query, write, reorganize, onboard-project (assessment-first
|
||||
onboarding: surveys repo, classifies excludes by type (11 categories), writes per-project
|
||||
`.graphifyignore`, confirms with user, then extracts using `qwen25-coder-7b-16k` — per
|
||||
ADR-0017), design-template (routes template-design ↔ new-type-creation: 4-step
|
||||
template-design process + injection-economics filter, 9-step new-type lifecycle; backs
|
||||
Phase 1 of the SB Content Plan — see `docs/memory-system/09-sb-content-plan.md`)
|
||||
- Config: `config.yaml` — vault path, Ollama model (qwen25-coder-7b-16k), env vars
|
||||
- Hook wiring: `~/.claude/settings.json` (hook entries invoke `/usr/bin/python3` with
|
||||
absolute paths into cc-os)
|
||||
- Isolation seam (2026-07-06, WS2): `OS_VAULT_PATH` env var overrides `vault_path` for the
|
||||
write skill and every hook via `config.load_config()`; `OS_VAULT_SKIP_REBUILD` suppresses
|
||||
the SessionStart graphify rebuild. Added for the eval harness; zero production impact when
|
||||
unset.
|
||||
|
||||
## Build-out history (2026-06-09 → 2026-06-30)
|
||||
|
||||
- **2026-06-09:** plugin live at `~/.claude/plugins/os-vault/`; build-plan Steps 1, 2a, 2b,
|
||||
3, 6 complete (incl. `onboard-project`). Step 1: vault-conventions.md, 6 fixture notes, 14
|
||||
Graphify handbook + design notes migrated as scaffolding. Step 4 (memsearch): installed
|
||||
via marketplace, `MEMSEARCH_DIR` global, Stop hook verified, search working. Step 5a
|
||||
(memsearch git sync): private Forgejo repo, whitelist `.gitignore` (memory/*.md only),
|
||||
auto-commit+push SessionEnd hook (ADR-0015; relocated to `memsearch_sync.py` by ADR-0016).
|
||||
- **2026-06-15:** Step 5b done — vault initialized as git repo, pushed to private Forgejo
|
||||
(52 files; git chosen over Syncthing); auto-commit+push via `vault_sync.py` (runs after
|
||||
`session_end.py` so the daily journal note is included). Step 2e pilot done — llf-schema
|
||||
(PHP 8.2 WordPress plugin) onboarded via `/os-vault:onboard-project`: 605 nodes / 930
|
||||
edges / 52 communities; Ollama doc pass lossy on WordPress docs but AST pass solid. Step
|
||||
2d closed — live vault `graphify-out/` is the baseline; fixture-only build superseded.
|
||||
All required build steps complete as of this date.
|
||||
- **2026-06-17:** onboard-project moved to assessment-first onboarding with per-project
|
||||
`.graphifyignore` (ADR-0017); extraction model `qwen25-coder-7b-16k` (16k-context build of
|
||||
qwen2.5-coder:7b — larger context window cuts chunk count per doc, the main speed lever).
|
||||
- **2026-06-30:** SB Content Plan Phase 1 complete (`docs/memory-system/09-sb-content-plan.md`,
|
||||
issues #1–#6): vault-conventions.md reconciled to one authoritative typed frontmatter
|
||||
schema (slug-only filenames; `source:` and `scope` are frontmatter fields, not tags); new
|
||||
`/os-vault:design-template` skill; three templates in `~/Documents/SecondBrain/_templates/`
|
||||
(`howto.md`, `convention.md`, `reference.md` with four-subtype variant selector), each
|
||||
dogfooded; four proof-of-concept notes patched. Phase 2 (issue #7) remains open as the
|
||||
steady-state migration/onboarding epic (no code).
|
||||
|
||||
## Write-behavior eval (WS2)
|
||||
|
||||
- Harness (2026-07-06): `plugins/os-vault/eval/` — held-out unprompted vault-write
|
||||
discrimination eval (Eval B/C playbook: ambiguity ladder L1 explicit → L3 conceptual,
|
||||
paired positives/negatives, 6 run-set scenarios on a new `reportgen` Ruby fixture + 6
|
||||
frozen reserve twins in a different knowledge domain, isolated sandbox vault,
|
||||
headless-only runner, deterministic-first Ruby checker with narrow frozen offer-detection
|
||||
judge fallback stubbable via `OS_VAULT_EVAL_JUDGE_CMD`, model-free `bin/self-test` 21/21).
|
||||
**Scenario Task blocks are held-out — never run informally; the reserve set is never even
|
||||
read informally.** Procedure: `plugins/os-vault/eval/README.md`.
|
||||
- Untuned baseline grid (2026-07-06, run-set × sonnet/haiku × 3 reps + 1 counted canary, 37
|
||||
reps, zero harness errors): **positives 1/19, negatives 18/18**. Headline: at L1 (explicit
|
||||
cue) both tiers persist the knowledge every rep but route it to Claude Code's built-in
|
||||
auto-memory instead of the vault; L2/L3 mostly don't persist at all; zero over-triggering
|
||||
anywhere. TSV: `eval/results/2026-07-06-baseline-grid.tsv`; full analysis: vault note
|
||||
[[os-vault-write-eval-baseline-grid-results]].
|
||||
- WS2 wording loop COMPLETE (2026-07-07, 4 iterations + confirm + held-out reserve; run-set
|
||||
AND reserve now contaminated): shipped destination-ladder write SKILL.md + SessionStart
|
||||
USAGE_NOTE (mechanical triggers + before-final-reply checkpoint), mechanical vault-root
|
||||
resolution (`${OS_VAULT_PATH:-$HOME/Documents/SecondBrain}` — fixed real-vault breaches
|
||||
found on iteration 1), fixture CLAUDE.md "Cross-project memory" section (**the
|
||||
real-project adoption template**, required to reach haiku at L2/L3; includes ADR-vs-vault
|
||||
disambiguation), candidates 2–4 unscored, post-reserve YAML-colon quoting rule. Confirm
|
||||
grid sonnet 18/18 / haiku 16/18; reserve (held-out) positives 8/18 strict / trigger-axis
|
||||
13/18, negatives 17/18 (vs baseline 1/19, 18/18). Residual gaps: YAML-hostile titles,
|
||||
haiku L2 no-trigger, generalization-laundering over-trigger. One checker false-positive
|
||||
fixed+rescored mid-loop (breach regex vs `2>/dev/null`); negative-criterion redesign
|
||||
pre-registered. TSVs: `eval/results/2026-07-07-wording-*.tsv`; loop log:
|
||||
`autoresearch/classic-260707-1253/loop-log.md`; analysis: vault note
|
||||
[[os-vault-write-eval-wording-loop-results]]. Next signal is production IRL audits.
|
||||
|
|
@ -5,7 +5,9 @@
|
|||
# cards, snapshot, inspect, config-write.
|
||||
#
|
||||
# Usage:
|
||||
# os-backlog board-ensure NAME --project Dev|Clients
|
||||
# os-backlog board-ensure NAME [--project PROJECT] (any Planka project name;
|
||||
# defaults to Backlog::Resolver.project_for(Dir.pwd) — Dev/Clients/Servers
|
||||
# by path, else "Dev"; creates the project if it doesn't exist yet)
|
||||
# os-backlog activate NAME
|
||||
# os-backlog archive NAME --yes
|
||||
# os-backlog resolve < input.json
|
||||
|
|
@ -15,7 +17,9 @@
|
|||
# os-backlog cards --board NAME (human-readable per-list card listing)
|
||||
# os-backlog snapshot --board NAME (full board state as JSON; read-only)
|
||||
# os-backlog inspect (detect existing tracking for cwd repo, JSON)
|
||||
# os-backlog config-write <tracker-value> (write tracker key to .cc-os/config)
|
||||
# os-backlog config-write <tracker-value> (write tracker key to .cc-os/config
|
||||
# + upsert the global project index)
|
||||
# os-backlog projects [NAME-FILTER] (print ~/.cc-os/projects.json as JSON)
|
||||
#
|
||||
# board-ensure/activate/archive talk to Planka over the network (the
|
||||
# installed planka-api gem, credentials from PLANKA_BASE_URL/USERNAME/
|
||||
|
|
@ -47,13 +51,33 @@ rescue StandardError => e
|
|||
fail_soft("could not authenticate to Planka: #{e.message}")
|
||||
end
|
||||
|
||||
# Cards wired to the cwd repo's .cc-os/config board_id cache (issue #22):
|
||||
# the BoardResolver gets injected read/write lambdas; any cache failure
|
||||
# degrades to the plain name lookup inside the resolver itself.
|
||||
def build_cards(client)
|
||||
config_path = File.join(Dir.pwd, ".cc-os", "config")
|
||||
read_config = -> { File.exist?(config_path) ? File.read(config_path) : nil }
|
||||
write_config = lambda do |contents|
|
||||
FileUtils.mkdir_p(File.dirname(config_path))
|
||||
File.write(config_path, contents)
|
||||
end
|
||||
resolver = Backlog::BoardResolver.new(client: client, read_config: read_config,
|
||||
write_config: write_config)
|
||||
Backlog::Cards.new(client: client, board_resolver: resolver)
|
||||
end
|
||||
|
||||
# --project accepts any Planka project name (Dev/Clients/Servers, or a
|
||||
# fresh one board-ensure will create). When --project is omitted, the
|
||||
# default is derived from the repo's path via
|
||||
# Backlog::Resolver.project_for(Dir.pwd), falling back to "Dev" for paths
|
||||
# outside every known root.
|
||||
def parse_project_flag(args)
|
||||
idx = args.index("--project")
|
||||
return "Dev" unless idx
|
||||
return Backlog::Resolver.project_for(Dir.pwd) || "Dev" unless idx
|
||||
|
||||
args.delete_at(idx)
|
||||
project = args.delete_at(idx)
|
||||
project || fail_soft("--project requires a value (Dev or Clients)")
|
||||
project || fail_soft("--project requires a value (a Planka project name)")
|
||||
end
|
||||
|
||||
def parse_flag(args, flag)
|
||||
|
|
@ -84,37 +108,79 @@ rescue StandardError => e
|
|||
{ "checked" => false, "reason" => e.message }
|
||||
end
|
||||
|
||||
# Best-effort open-issue count via tea (Forgejo) or gh (GitHub), whichever
|
||||
# the detected remote implies. Fails soft per-field.
|
||||
# Best-effort open-issue count + lightweight per-issue metadata via tea
|
||||
# (Forgejo) or gh (GitHub), whichever the detected remote implies. Fails
|
||||
# soft per-field: any fetch/parse failure means metadata nil. Returns
|
||||
# [issue_cli_hash, metadata_or_nil] — the metadata feeds the pure
|
||||
# Backlog::Inspector.classify_issue_shape and never appears in the JSON.
|
||||
def inspect_issue_cli(remote_info)
|
||||
unless remote_info && %i[forgejo github].include?(remote_info[:kind])
|
||||
return { "tool" => nil, "open_count" => nil, "reason" => "no forgejo/github remote detected" }
|
||||
return [{ "tool" => nil, "open_count" => nil, "reason" => "no forgejo/github remote detected" }, nil]
|
||||
end
|
||||
|
||||
repo_slug = "#{remote_info[:owner]}/#{remote_info[:repo]}"
|
||||
if remote_info[:kind] == :forgejo
|
||||
tea_issue_count(repo_slug)
|
||||
tea_issues(repo_slug)
|
||||
else
|
||||
gh_issue_count(repo_slug)
|
||||
gh_issues(repo_slug)
|
||||
end
|
||||
end
|
||||
|
||||
def tea_issue_count(repo_slug)
|
||||
return { "tool" => "tea", "open_count" => nil, "reason" => "tea CLI not found" } unless system("which tea > /dev/null 2>&1")
|
||||
def tea_issues(repo_slug)
|
||||
return [{ "tool" => "tea", "open_count" => nil, "reason" => "tea CLI not found" }, nil] unless system("which tea > /dev/null 2>&1")
|
||||
|
||||
out = `tea issues list --repo #{repo_slug} --state open --fields index,title,labels,body --output json 2>/dev/null`
|
||||
parsed = JSON.parse(out)
|
||||
issues = parsed.map do |issue|
|
||||
{ number: issue["index"].to_i, title: issue["title"],
|
||||
labels: issue["labels"].to_s.split(",").map(&:strip).reject(&:empty?),
|
||||
body: issue["body"] }
|
||||
end
|
||||
[{ "tool" => "tea", "open_count" => issues.size }, issues]
|
||||
rescue StandardError
|
||||
# JSON mode unavailable/unparsable: fall back to the plain line count.
|
||||
out = `tea issues list --repo #{repo_slug} --state open 2>/dev/null`
|
||||
lines = out.to_s.lines.map(&:strip).reject(&:empty?)
|
||||
{ "tool" => "tea", "open_count" => lines.size }
|
||||
[{ "tool" => "tea", "open_count" => lines.size }, nil]
|
||||
end
|
||||
|
||||
def gh_issue_count(repo_slug)
|
||||
return { "tool" => "gh", "open_count" => nil, "reason" => "gh CLI not found" } unless system("which gh > /dev/null 2>&1")
|
||||
def gh_issues(repo_slug)
|
||||
return [{ "tool" => "gh", "open_count" => nil, "reason" => "gh CLI not found" }, nil] unless system("which gh > /dev/null 2>&1")
|
||||
|
||||
out = `gh issue list --repo #{repo_slug} --state open --json number 2>/dev/null`
|
||||
out = `gh issue list --repo #{repo_slug} --state open --json number,title,labels,body 2>/dev/null`
|
||||
parsed = JSON.parse(out)
|
||||
{ "tool" => "gh", "open_count" => parsed.size }
|
||||
rescue JSON::ParserError
|
||||
{ "tool" => "gh", "open_count" => nil, "reason" => "could not parse gh output" }
|
||||
issues = parsed.map do |issue|
|
||||
{ number: issue["number"].to_i, title: issue["title"],
|
||||
labels: Array(issue["labels"]).map { |label| label["name"] },
|
||||
body: issue["body"] }
|
||||
end
|
||||
[{ "tool" => "gh", "open_count" => issues.size }, issues]
|
||||
rescue StandardError
|
||||
[{ "tool" => "gh", "open_count" => nil, "reason" => "could not parse gh output" }, nil]
|
||||
end
|
||||
|
||||
# Canonical path for the global project index: the realpath of cwd — the
|
||||
# directory config-write just wrote .cc-os/config into, i.e. the project
|
||||
# the row describes. Deliberately NOT the git toplevel: in an umbrella
|
||||
# repo a subdirectory's config is its own project, and keying on the
|
||||
# toplevel would make every subproject overwrite the umbrella's row.
|
||||
# Never raises.
|
||||
def canonical_repo_path
|
||||
File.realpath(Dir.pwd)
|
||||
rescue StandardError
|
||||
Dir.pwd
|
||||
end
|
||||
|
||||
# Best-effort upsert of the global project index row after a successful
|
||||
# config-write. Index failures must never fail config-write — warn at most.
|
||||
def update_project_index(tracker)
|
||||
repo_path = canonical_repo_path
|
||||
remote = `git remote get-url origin 2>/dev/null`.strip
|
||||
remote = nil if remote.empty?
|
||||
Backlog::ProjectIndex.new.upsert(path: repo_path, name: File.basename(repo_path),
|
||||
tracker: tracker, remote: remote)
|
||||
rescue StandardError => e
|
||||
warn "os-backlog: (non-fatal) could not update project index: #{e.message}"
|
||||
end
|
||||
|
||||
command, *rest = ARGV
|
||||
|
|
@ -157,19 +223,19 @@ when "card-add"
|
|||
title = require_flag(rest, "--title", "card-add")
|
||||
description = parse_flag(rest, "--description")
|
||||
client = build_client
|
||||
card = Backlog::Cards.new(client: client).add(board_name: board, title: title, description: description)
|
||||
card = build_cards(client).add(board_name: board, title: title, description: description)
|
||||
puts "created card ##{card.id} at Backlog on #{board}: #{card.name}"
|
||||
when "card-move"
|
||||
board = require_flag(rest, "--board", "card-move")
|
||||
card_id = require_flag(rest, "--card", "card-move")
|
||||
to = require_flag(rest, "--to", "card-move")
|
||||
client = build_client
|
||||
card = Backlog::Cards.new(client: client).move(board_name: board, card_id: card_id, to: to)
|
||||
card = build_cards(client).move(board_name: board, card_id: card_id, to: to)
|
||||
puts "moved card ##{card.id} to #{to} on #{board}"
|
||||
when "cards"
|
||||
board = require_flag(rest, "--board", "cards")
|
||||
client = build_client
|
||||
snapshot = Backlog::Cards.new(client: client).snapshot(board_name: board)
|
||||
snapshot = build_cards(client).snapshot(board_name: board)
|
||||
snapshot["lists"].each do |list|
|
||||
puts "#{list['name']} (#{list['cards'].size})"
|
||||
list["cards"].each do |card|
|
||||
|
|
@ -180,19 +246,19 @@ when "cards"
|
|||
when "snapshot"
|
||||
board = require_flag(rest, "--board", "snapshot")
|
||||
client = build_client
|
||||
puts JSON.pretty_generate(Backlog::Cards.new(client: client).snapshot(board_name: board))
|
||||
puts JSON.pretty_generate(build_cards(client).snapshot(board_name: board))
|
||||
when "card-label"
|
||||
board = require_flag(rest, "--board", "card-label")
|
||||
card_id = require_flag(rest, "--card", "card-label")
|
||||
label = require_flag(rest, "--label", "card-label")
|
||||
client = build_client
|
||||
Backlog::Cards.new(client: client).attach_label(board_name: board, card_id: card_id, label: label)
|
||||
build_cards(client).attach_label(board_name: board, card_id: card_id, label: label)
|
||||
puts "labeled card ##{card_id} with #{label}"
|
||||
when "card-comment"
|
||||
card_id = require_flag(rest, "--card", "card-comment")
|
||||
text = require_flag(rest, "--text", "card-comment")
|
||||
client = build_client
|
||||
comment = Backlog::Cards.new(client: client).comment(card_id: card_id, text: text)
|
||||
comment = build_cards(client).comment(card_id: card_id, text: text)
|
||||
puts "commented on card ##{card_id} (comment ##{comment.id})"
|
||||
when "inspect"
|
||||
repo_path = Dir.pwd
|
||||
|
|
@ -203,12 +269,14 @@ when "inspect"
|
|||
remote_output = `git -C #{Shellwords.escape(repo_path)} remote -v 2>/dev/null`
|
||||
remote_info = Backlog::Tracker.parse_remote(remote_output)
|
||||
|
||||
issue_cli, issue_metadata = inspect_issue_cli(remote_info)
|
||||
findings = {
|
||||
"repo_path" => repo_path,
|
||||
"tracker_configured" => config.tracker,
|
||||
"planka" => inspect_planka(repo_path),
|
||||
"git_remote" => remote_info ? remote_info.transform_keys(&:to_s) : nil,
|
||||
"issue_cli" => inspect_issue_cli(remote_info),
|
||||
"issue_cli" => issue_cli,
|
||||
"issue_shape" => Backlog::Inspector.classify_issue_shape(issue_metadata),
|
||||
"in_repo_issue_files" => Backlog::Inspector.issue_files(repo_path)
|
||||
}
|
||||
puts JSON.pretty_generate(findings)
|
||||
|
|
@ -226,13 +294,24 @@ when "config-write"
|
|||
updated = Backlog::Config.merge(existing, "tracker", value)
|
||||
FileUtils.mkdir_p(config_dir)
|
||||
File.write(config_path, updated)
|
||||
update_project_index(value)
|
||||
puts "tracker set to #{value} in #{config_path}"
|
||||
when "projects"
|
||||
filter = rest.shift
|
||||
projects = Backlog::ProjectIndex.new.all
|
||||
if filter
|
||||
projects = projects.select do |path, row|
|
||||
[row["name"], row["tracker"], path].any? { |field| field.to_s.include?(filter) }
|
||||
end
|
||||
end
|
||||
puts JSON.pretty_generate({ "projects" => projects })
|
||||
when nil, "-h", "--help"
|
||||
puts <<~USAGE
|
||||
usage: os-backlog <command> [options]
|
||||
|
||||
commands:
|
||||
board-ensure NAME --project Dev|Clients
|
||||
board-ensure NAME [--project PROJECT] (any project name; creates it if missing;
|
||||
default derived from cwd path)
|
||||
activate NAME
|
||||
archive NAME --yes
|
||||
resolve (reads {"repo_path","config","boards"} JSON from stdin)
|
||||
|
|
@ -243,7 +322,8 @@ when nil, "-h", "--help"
|
|||
card-label --board NAME --card ID --label NAME
|
||||
card-comment --card ID --text "..."
|
||||
inspect (detect existing tracking for cwd repo, JSON)
|
||||
config-write <tracker-value> (write tracker key to .cc-os/config)
|
||||
config-write <tracker-value> (write tracker key to .cc-os/config + global project index)
|
||||
projects [NAME-FILTER] (print the global project index at ~/.cc-os/projects.json as JSON)
|
||||
USAGE
|
||||
exit(command.nil? ? 1 : 0)
|
||||
else
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
#
|
||||
# wakeup-poll: one pass of the issue-triggered wakeup poller (ADR-0035).
|
||||
#
|
||||
# Reads the global project index (~/.cc-os/projects.json, ADR-0034), and for
|
||||
# each wakeup-opted-in forgejo:/github: project lists open issues (tea/gh),
|
||||
# diffs against last-seen state (~/.cc-os/wakeup-state.json), and for each
|
||||
# NEW issue either spawns a tmux session (ADR-0036) or — the default —
|
||||
# prints the command it WOULD run. hitl/unlabeled issues are notify-only:
|
||||
# appended to ~/.cc-os/wakeup-summary.md (pull-only, notification policy v2).
|
||||
#
|
||||
# Usage:
|
||||
# wakeup-poll # dry-run: print planned actions, touch no state
|
||||
# wakeup-poll --spawn # actually create tmux sessions + persist state
|
||||
#
|
||||
# Fails soft everywhere: missing index, unroutable projects, tea/gh errors,
|
||||
# and absent tmux each degrade to a printed note, never a crash. The poller
|
||||
# uses tea/gh's existing logins only and never reads ~/.credentials.
|
||||
# Pilot only — install no cron around this until ADR-0035 is Accepted.
|
||||
|
||||
require "json"
|
||||
require "open3"
|
||||
require "shellwords"
|
||||
require_relative "../lib/backlog"
|
||||
|
||||
spawn_mode = ARGV.include?("--spawn")
|
||||
|
||||
def note(msg) = puts("wakeup-poll: #{msg}")
|
||||
|
||||
def list_issues(kind, slug)
|
||||
cmd =
|
||||
case kind
|
||||
when "forgejo" then ["tea", "issues", "list", "--repo", slug, "--state", "open", "--output", "json"]
|
||||
when "github" then ["gh", "issue", "list", "--repo", slug, "--state", "open",
|
||||
"--json", "number,title,labels,body,url"]
|
||||
end
|
||||
out, err, status = Open3.capture3(*cmd)
|
||||
return nil unless status.success?
|
||||
|
||||
rows = JSON.parse(out)
|
||||
# Normalize gh's shape to tea's {index, title, labels}.
|
||||
rows.map do |r|
|
||||
{ "index" => (r["index"] || r["number"]).to_s,
|
||||
"title" => r["title"],
|
||||
"labels" => r["labels"].is_a?(Array) ? r["labels"].map { |l| l["name"] }.join(",") : r["labels"].to_s,
|
||||
"body" => r["body"].to_s,
|
||||
"url" => r["url"].to_s }
|
||||
end
|
||||
rescue StandardError => e
|
||||
warn "wakeup-poll: #{slug}: #{e.class}: #{e.message}"
|
||||
nil
|
||||
end
|
||||
|
||||
index = Backlog::ProjectIndex.new
|
||||
projects = index.all
|
||||
if projects.empty?
|
||||
note "project index empty or unreadable — nothing to do"
|
||||
exit 0
|
||||
end
|
||||
|
||||
config_reader = lambda do |path|
|
||||
file = File.join(path, ".cc-os", "config")
|
||||
File.exist?(file) ? File.read(file) : ""
|
||||
rescue StandardError
|
||||
""
|
||||
end
|
||||
|
||||
eligible = Backlog::Wakeup.eligible_projects(projects, config_reader: config_reader)
|
||||
skipped = projects.keys - eligible.map { |p| p[:path] }
|
||||
skipped.each { |path| note "skip #{path} (#{projects[path]['tracker'] || 'no tracker'}: not a wakeup-eligible forgejo/github project, or wakeup=true not set)" }
|
||||
|
||||
if eligible.empty?
|
||||
note "no opted-in forgejo/github projects — nothing to do"
|
||||
exit 0
|
||||
end
|
||||
|
||||
state = Backlog::Wakeup::State.new
|
||||
summary_path = File.join(File.dirname(Backlog::Wakeup::State.default_path), "wakeup-summary.md")
|
||||
|
||||
eligible.each do |project|
|
||||
issues = list_issues(project[:tracker_kind], project[:repo_slug])
|
||||
if issues.nil?
|
||||
note "#{project[:repo_slug]}: issue listing failed (is #{project[:tracker_kind] == 'forgejo' ? 'tea' : 'gh'} logged in?) — skipping"
|
||||
next
|
||||
end
|
||||
|
||||
last_seen = state.last_seen(project[:repo_slug])
|
||||
if last_seen.nil?
|
||||
top = Backlog::Wakeup.highest_index(issues)
|
||||
note "#{project[:repo_slug]}: first scan — baselining at ##{top}, waking nothing"
|
||||
state.record(project[:repo_slug], top) if spawn_mode
|
||||
next
|
||||
end
|
||||
|
||||
fresh = Backlog::Wakeup.new_issues(issues, last_seen)
|
||||
note "#{project[:repo_slug]}: #{fresh.size} new issue(s) since ##{last_seen}" if fresh.any?
|
||||
|
||||
fresh.each do |issue|
|
||||
tier = Backlog::Wakeup.tier(issue["labels"])
|
||||
if tier == :notify
|
||||
line = "- #{Time.now.strftime('%Y-%m-%d')} #{project[:repo_slug]}##{issue['index']} (#{tier}): #{issue['title']}"
|
||||
if spawn_mode
|
||||
File.open(summary_path, "a") { |f| f.puts(line) }
|
||||
note "notify-only -> appended to #{summary_path}"
|
||||
else
|
||||
note "would append to #{summary_path}: #{line}"
|
||||
end
|
||||
next
|
||||
end
|
||||
|
||||
prompt = Backlog::Wakeup.session_prompt(
|
||||
issue: issue, tier: tier,
|
||||
url: issue["url"].empty? ? "#{project[:tracker_kind]}:#{project[:repo_slug]}##{issue['index']}" : issue["url"]
|
||||
)
|
||||
cmd = Backlog::Wakeup.tmux_command(
|
||||
project: project[:name], path: project[:path],
|
||||
issue_index: issue["index"], prompt: prompt
|
||||
)
|
||||
if spawn_mode
|
||||
ok = system(*cmd)
|
||||
note ok ? "spawned #{cmd[4]}" : "tmux spawn failed for #{cmd[4]} (is tmux installed?)"
|
||||
else
|
||||
note "would run: #{cmd.shelljoin}"
|
||||
end
|
||||
end
|
||||
|
||||
state.record(project[:repo_slug], Backlog::Wakeup.highest_index(issues, last_seen)) if spawn_mode
|
||||
end
|
||||
|
||||
note "dry-run complete — no state written, no sessions spawned (use --spawn)" unless spawn_mode
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup|resume|compact",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session_start.py",
|
||||
"timeout": 5
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "ruby ${CLAUDE_PLUGIN_ROOT}/hooks/triage_check.rb",
|
||||
"timeout": 15
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env python3
|
||||
"""session_start.py — deterministic SessionStart injection note for os-backlog.
|
||||
|
||||
Modeled on os-adr's session_start.py (the wording-source-of-record pattern).
|
||||
Injects the backlog process rules — WHEN->THEN mechanical triggers only.
|
||||
Deliberately contains ZERO board state and no briefs (notification policy v2:
|
||||
pull beats push — board state is fetched on demand via /os-backlog:list, never
|
||||
pushed into a session).
|
||||
|
||||
Sibling hook triage_check.rb (card 1818330518584820864) is a narrow, sanctioned
|
||||
exception to that policy: it makes one bounded Planka read to decide whether to
|
||||
push a single mechanical dispatch directive — not a brief, not board contents —
|
||||
when the current project's own Backlog has unlabeled cards. The only board-
|
||||
derived fact it ever surfaces is the unlabeled count that justifies dispatching
|
||||
card-triage; it never lists card titles or otherwise renders board state. Kept
|
||||
as a separate script precisely so this file can stay a pure, network-free
|
||||
function of filesystem state.
|
||||
|
||||
git project -> additionalContext note (rules only, near-zero tokens)
|
||||
not a git repo -> silent (process rules only apply inside real projects)
|
||||
|
||||
Autonomy-label semantics in NOTE are the ADR-029 contract (which supersedes
|
||||
the wording in issue #16's body): hitl never picked up autonomously; semi
|
||||
stops at Review for human sign-off; afk-ready goes straight to Done after
|
||||
verification, skipping Review.
|
||||
|
||||
Pure function of the project's filesystem state; no model, no network, no
|
||||
Ruby, no Planka call. Always exits 0 — the hook must never block a session.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
NOTE = """\
|
||||
[os-backlog] Backlog process rules (rules only — this note never carries board state; board state is pull-only via /os-backlog:list when the user asks).
|
||||
- CAPTURE: WHEN a concrete follow-up task, bug, or piece of deferred work surfaces mid-session that will NOT be done this session -> capture it as a Backlog card via /os-backlog:capture. Never a TODO comment in code, never only a mention in chat.
|
||||
- ROUTING: WHEN process/backlog/spec-tracking work surfaces -> read the tracker key from .cc-os/config (`tracker=planka:<board>` | `forgejo:<owner>/<repo>` | `github:<owner>/<repo>` | `repo:<path>`) and send work where it says. WHEN this is a git project and no tracker key is configured -> suggest /os-backlog:route once; do not nag. A subdirectory's own .cc-os/config overrides the repo root's for any session started there — umbrella repos route each subproject to its own board/tracker this way; do not assume one repo means one board.
|
||||
- PROMOTION: WHEN a Planka-tracked effort accretes significant code or design decisions in card comments -> flag promotion to the user (never silently): carve out a repo, move the durable spec into git issues via /to-issues, and keep the Planka card as a pointer (title + link). Never duplicate spec text into a card description.
|
||||
- COLUMN OWNERSHIP (CLI-enforced per ADR-029; a violating card-move fails with the rule named): create cards at Backlog only. Never move a card into or out of Next — Next is human-curated in both directions. Never move a card labeled hitl; never move a card out of Done. Working a card MEANS moving it via card-move: Doing when work starts, Waiting + a blocker comment when blocked, and on shipping: Review (semi) or Done (afk-ready, only after verification).
|
||||
- LABELING/TRIAGE: priority (P0-P3) and autonomy (hitl/semi/afk-ready) labeling is the RESPONSIBLE project's job (it knows its own priorities) — a parent umbrella may label its own and its child boards, but never label cards on a board that belongs to a different project you're just filing onto. Triage is initiated by this project's own SessionStart check (a separate hook), not by capture — if it reports unlabeled Backlog cards, dispatch the card-triage agent (haiku, background) to clear them.
|
||||
- CROSS-PROJECT: WHEN a needed change belongs to a DIFFERENT project THEN do not edit it — query `os-backlog projects`, file an issue/card with that project's tracker using the Discoverer template (see os-backlog route references/cross-project-filing.md).
|
||||
- AUTONOMY LABELS (ADR-029): hitl -> human-owned; never picked up autonomously. semi -> run the mechanical parts, stop at named decision gates; shipped work goes to Review for human sign-off. afk-ready -> shipped + verified goes straight to Done, skipping Review.
|
||||
"""
|
||||
|
||||
|
||||
def find_project_root(cwd: Path) -> Optional[Path]:
|
||||
"""Nearest ancestor (including cwd) containing .git, else None."""
|
||||
for candidate in [cwd, *cwd.parents]:
|
||||
if (candidate / ".git").exists():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def run(root: Optional[Path], out) -> str:
|
||||
"""Pure decision: ('context'|'silent'). Emits hook JSON when in a project."""
|
||||
if root is None:
|
||||
return "silent"
|
||||
out.write(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": NOTE,
|
||||
}
|
||||
}))
|
||||
return "context"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
run(find_project_root(Path.cwd()), sys.stdout)
|
||||
except Exception:
|
||||
pass # never block a session
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
#
|
||||
# triage_check.rb — SessionStart hook (card 1818330518584820864): checks
|
||||
# whether the CURRENT project's own resolved board has unlabeled Backlog
|
||||
# cards and, if so, emits additionalContext instructing dispatch of the
|
||||
# card-triage agent (haiku, background).
|
||||
#
|
||||
# Design intent: labeling is the RESPONSIBLE project's job, not capture's
|
||||
# and not whichever project happened to file a card cross-project. This
|
||||
# hook only ever looks at the board the CURRENT session's project resolves
|
||||
# to (the same board /os-backlog:capture would file onto from here) — a
|
||||
# session run from the owning project, or a parent umbrella, naturally
|
||||
# only ever triggers triage for a board it owns; it never inspects or
|
||||
# labels a board belonging to a different project.
|
||||
#
|
||||
# This is deliberately separate from session_start.py (which stays a pure,
|
||||
# network-free function of filesystem state per its own docstring): this
|
||||
# script does one bounded Planka read to decide whether to fire, then
|
||||
# hands off the actual labeling to the card-triage agent — it never
|
||||
# labels anything itself.
|
||||
#
|
||||
# Fails soft everywhere: no git project, no board name derivable, no
|
||||
# network/credentials, any Planka error, or a board that doesn't exist yet
|
||||
# -> silent, exit 0. Never blocks a session.
|
||||
|
||||
require "json"
|
||||
require_relative "../lib/backlog"
|
||||
|
||||
def find_project_root(cwd)
|
||||
dir = File.expand_path(cwd)
|
||||
loop do
|
||||
return dir if Dir.exist?(File.join(dir, ".git"))
|
||||
|
||||
parent = File.dirname(dir)
|
||||
return nil if parent == dir
|
||||
|
||||
dir = parent
|
||||
end
|
||||
end
|
||||
|
||||
# Same config semantics as the CLI: the session cwd's own .cc-os/config
|
||||
# wins (umbrella subdir configs override the repo root's for sessions
|
||||
# started there), then the root config, then the path heuristic — and
|
||||
# `tracker=planka:<board>` counts, not just the explicit `board` key.
|
||||
# The decision itself is pure (TriageCheck.board_name_for); this only
|
||||
# reads the two candidate config files.
|
||||
def resolve_board_name(root)
|
||||
read = lambda do |dir|
|
||||
path = File.join(dir, ".cc-os", "config")
|
||||
File.exist?(path) ? File.read(path) : nil
|
||||
end
|
||||
cwd = File.expand_path(Dir.pwd)
|
||||
Backlog::TriageCheck.board_name_for(cwd: cwd, root: root,
|
||||
cwd_config: read.call(cwd),
|
||||
root_config: read.call(root))
|
||||
end
|
||||
|
||||
def build_client
|
||||
require "planka_api"
|
||||
client = Planka::Client.new
|
||||
client.login
|
||||
client
|
||||
end
|
||||
|
||||
def main
|
||||
root = find_project_root(Dir.pwd)
|
||||
return unless root
|
||||
|
||||
board_name = resolve_board_name(root)
|
||||
return unless board_name
|
||||
|
||||
client = build_client
|
||||
snapshot = Backlog::Cards.new(client: client).snapshot(board_name: board_name)
|
||||
backlog_list = snapshot["lists"].find { |l| l["name"] == "Backlog" }
|
||||
return unless backlog_list
|
||||
|
||||
labels = backlog_list["cards"].map { |c| c["labels"] }
|
||||
note = Backlog::TriageCheck.note_for(board_name, labels)
|
||||
return unless note
|
||||
|
||||
puts JSON.generate(
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "SessionStart",
|
||||
additionalContext: note
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
begin
|
||||
main
|
||||
rescue Exception => e # rubocop:disable Lint/RescueException -- a hook must never block a session
|
||||
warn "os-backlog triage_check: #{e.class}: #{e.message}" if ENV["CC_OS_DEBUG"]
|
||||
end
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
require_relative "backlog/board_spec"
|
||||
require_relative "backlog/board_ensurer"
|
||||
require_relative "backlog/board_resolver"
|
||||
require_relative "backlog/cards"
|
||||
require_relative "backlog/config"
|
||||
require_relative "backlog/inspector"
|
||||
require_relative "backlog/project_index"
|
||||
require_relative "backlog/resolver"
|
||||
require_relative "backlog/tracker"
|
||||
require_relative "backlog/triage_check"
|
||||
require_relative "backlog/wakeup"
|
||||
|
|
|
|||
|
|
@ -20,9 +20,19 @@ module Backlog
|
|||
# This class never archives a board — archiving only happens on explicit
|
||||
# human request (see BoardSpec / the resolver). It does provide the raw
|
||||
# rename primitives (+activate+ / +archive+) that a caller may invoke.
|
||||
#
|
||||
# project_name accepts ANY Planka project name, not just "Dev"/"Clients"
|
||||
# — #ensure creates the project on demand (find_or_create_project) if it
|
||||
# doesn't exist yet. There is deliberately no move-a-board-between-
|
||||
# projects command: Planka's API doesn't expose one cleanly and the need
|
||||
# is rare enough that it isn't worth building — do it by hand in the
|
||||
# Planka UI (drag the board card into the target project) instead.
|
||||
class BoardEnsurer
|
||||
Result = Struct.new(:board, :created, :project, :lists_created, :labels_created, keyword_init: true)
|
||||
|
||||
# Raised instead of silently creating a bot-only-visible Planka project.
|
||||
class MissingHumanUserIdError < StandardError; end
|
||||
|
||||
# @param client [Planka::Client] an already-authenticated client
|
||||
# @param human_user_id [String, Integer, nil] Planka user id of the
|
||||
# human who must be able to see bot-created projects. Falls back to
|
||||
|
|
@ -35,7 +45,8 @@ module Backlog
|
|||
|
||||
# @param name [String] board name (never an archived---prefixed name;
|
||||
# this method only ever targets the active board)
|
||||
# @param project_name [String] "Dev" or "Clients" (or any project name)
|
||||
# @param project_name [String] any Planka project name — created on demand
|
||||
# if it doesn't already exist
|
||||
# @return [Result]
|
||||
def ensure(name, project_name:)
|
||||
raise ArgumentError, "board name must not use the archived-- prefix" if BoardSpec.archived?(name)
|
||||
|
|
@ -86,9 +97,23 @@ module Backlog
|
|||
end
|
||||
|
||||
# Quirk 2: null the bot's sole ownerProjectManagerId and add the human
|
||||
# as a project manager, so the project shows up for them.
|
||||
# as a project manager, so the project shows up for them. Only reached
|
||||
# right after find_or_create_project creates a brand-new project — an
|
||||
# already-existing project never calls this and so never requires
|
||||
# human_user_id.
|
||||
def fix_visibility(project)
|
||||
return unless @human_user_id
|
||||
unless @human_user_id
|
||||
# The Planka API has no atomic create-with-manager call, so the
|
||||
# project already exists (bot-only-visible) by the time we can
|
||||
# detect the missing id — we deliberately don't try to roll it
|
||||
# back/delete it here; raise loudly instead and let a human fix
|
||||
# visibility (env var + rerun, or the Planka UI) after the fact.
|
||||
raise MissingHumanUserIdError,
|
||||
"cannot make new Planka project #{project.name.inspect} visible to a human: " \
|
||||
"no human_user_id given and PLANKA_HUMAN_USER_ID is unset — the project was " \
|
||||
"created bot-only; set the env var and fix visibility manually or via the " \
|
||||
"Planka UI"
|
||||
end
|
||||
|
||||
@client.projects.add_manager(project.id, @human_user_id)
|
||||
@client.projects.update(project.id, ownerProjectManagerId: nil)
|
||||
|
|
@ -140,7 +165,7 @@ module Backlog
|
|||
last_error = nil
|
||||
candidates.each do |color|
|
||||
return @client.labels.create(board.id, name: label_name, color: color, position: 65_536)
|
||||
rescue Planka::ValidationError => e
|
||||
rescue Planka::ValidationError, Planka::BadRequestError => e
|
||||
last_error = e
|
||||
next
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
require_relative "config"
|
||||
|
||||
module Backlog
|
||||
# Resolves a board name to its Planka board record, caching the resolved
|
||||
# id as `board_id=<id>` in the repo's `.cc-os/config` (issue #22).
|
||||
#
|
||||
# The tracker's board *name* stays the source of truth; the id is only a
|
||||
# cache. The cached id is honored only when the config's tracker is
|
||||
# exactly `planka:<board_name>`, and a cached `boards.get(id)` hit counts
|
||||
# only if the returned board's name still matches — a 404 or a rename
|
||||
# falls back to the projects+boards name lookup and rewrites the cache.
|
||||
# Any cache failure (unreadable config, unwritable config, stale id)
|
||||
# degrades to the name lookup; it never fails the command.
|
||||
#
|
||||
# Filesystem access is injected: `read_config` returns the raw
|
||||
# `.cc-os/config` contents (or nil), `write_config` persists updated
|
||||
# contents. Cache writes go through Config.set, the comment-preserving
|
||||
# writer — never Config.merge.
|
||||
class BoardResolver
|
||||
# @param client [Planka::Client] an already-authenticated client
|
||||
# @param read_config [#call] -> String, nil — raw .cc-os/config contents
|
||||
# @param write_config [#call] (String) -> void — persist updated contents
|
||||
def initialize(client:, read_config:, write_config:)
|
||||
@client = client
|
||||
@read_config = read_config
|
||||
@write_config = write_config
|
||||
end
|
||||
|
||||
# A resolver with no config access: always name-lookup, never caches.
|
||||
def self.null(client:)
|
||||
new(client: client, read_config: -> {}, write_config: ->(_) {})
|
||||
end
|
||||
|
||||
# @param name [String] the board name (source of truth)
|
||||
# @return [Planka::Types::Board]
|
||||
# @raise [RuntimeError] if no board with that name exists
|
||||
def board_for(name)
|
||||
cached = cached_board(name)
|
||||
return cached if cached
|
||||
|
||||
board = lookup_by_name!(name)
|
||||
cache_id(name, board.id)
|
||||
board
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cached_board(name)
|
||||
config = safe_config
|
||||
return nil unless config && config.tracker == "planka:#{name}"
|
||||
|
||||
id = config.board_id
|
||||
return nil unless id
|
||||
|
||||
board = @client.boards.get(id).board
|
||||
board && board.name == name ? board : nil
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
|
||||
def lookup_by_name!(name)
|
||||
@client.projects.list.each do |project|
|
||||
board = @client.boards.list(project.id).find { |b| b.name == name }
|
||||
return board if board
|
||||
end
|
||||
raise "no board named #{name.inspect} found (run board-ensure first)"
|
||||
end
|
||||
|
||||
def cache_id(name, id)
|
||||
contents = @read_config.call
|
||||
config = Config.new(contents)
|
||||
return unless config.tracker == "planka:#{name}"
|
||||
return if config.board_id == id.to_s
|
||||
|
||||
@write_config.call(Config.set(contents, "board_id", id))
|
||||
rescue StandardError
|
||||
nil # cache write failure never fails the command
|
||||
end
|
||||
|
||||
def safe_config
|
||||
Config.new(@read_config.call)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
require_relative "board_spec"
|
||||
require_relative "board_resolver"
|
||||
|
||||
module Backlog
|
||||
# Card-level operations over an authenticated Planka client: create a
|
||||
|
|
@ -12,8 +13,13 @@ module Backlog
|
|||
# Never deletes anything.
|
||||
class Cards
|
||||
# @param client [Planka::Client] an already-authenticated client
|
||||
def initialize(client:)
|
||||
# @param board_resolver [BoardResolver, nil] resolves board names to
|
||||
# board records (with the .cc-os/config board_id cache — issue #22);
|
||||
# defaults to a cache-less name-only resolver. Cards never touches
|
||||
# the filesystem itself — all config I/O lives in the resolver.
|
||||
def initialize(client:, board_resolver: nil)
|
||||
@client = client
|
||||
@board_resolver = board_resolver || BoardResolver.null(client: client)
|
||||
end
|
||||
|
||||
# Create a card at the board's Backlog list. Raises if the board or
|
||||
|
|
@ -163,11 +169,7 @@ module Backlog
|
|||
end
|
||||
|
||||
def find_board!(name)
|
||||
@client.projects.list.each do |project|
|
||||
board = @client.boards.list(project.id).find { |b| b.name == name }
|
||||
return board if board
|
||||
end
|
||||
raise "no board named #{name.inspect} found (run board-ensure first)"
|
||||
@board_resolver.board_for(name)
|
||||
end
|
||||
|
||||
def find_list!(board, list_name)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,24 @@ module Backlog
|
|||
@data["tracker"]
|
||||
end
|
||||
|
||||
# @return [String, nil] the effective Planka board name: the explicit
|
||||
# `board` key when present, else the board named by a
|
||||
# `tracker=planka:<board>` key (the form config-write actually
|
||||
# writes). Non-planka trackers contribute nothing.
|
||||
def planka_board
|
||||
return board if board
|
||||
return nil unless tracker&.start_with?("planka:")
|
||||
|
||||
tracker.delete_prefix("planka:")
|
||||
end
|
||||
|
||||
# @return [String, nil] the cached Planka board id, if present. Only a
|
||||
# cache — the tracker's board *name* stays the source of truth
|
||||
# (issue #22); BoardResolver decides whether to honor it.
|
||||
def board_id
|
||||
@data["board_id"]
|
||||
end
|
||||
|
||||
def configured? = !board.nil?
|
||||
def tracker_configured? = !tracker.nil?
|
||||
|
||||
|
|
@ -55,5 +73,35 @@ module Backlog
|
|||
data[key.to_s] = value.to_s
|
||||
data.map { |k, v| "#{k}=#{v}\n" }.join
|
||||
end
|
||||
|
||||
# Like .merge but comment/format-preserving: every non-key line
|
||||
# (comments, blanks) is left byte-for-byte untouched; existing
|
||||
# key=value lines are re-emitted in the normalized no-spaces form; the
|
||||
# key is replaced in place or appended if absent. Mirrors os-status's
|
||||
# write_config_value (plugins/os-status/hooks/state.py) — the shared
|
||||
# .cc-os/config contract owner. Pure — no filesystem.
|
||||
#
|
||||
# @param contents [String, nil] existing raw `.cc-os/config` contents
|
||||
# @param key [String, Symbol] key to set
|
||||
# @param value [Object] value to set it to
|
||||
# @return [String] the updated contents
|
||||
def self.set(contents, key, value)
|
||||
key = key.to_s
|
||||
found = false
|
||||
out = contents.to_s.split("\n").map do |line|
|
||||
stripped = line.strip
|
||||
next line if stripped.empty? || stripped.start_with?("#") || !stripped.include?("=")
|
||||
|
||||
k, v = stripped.split("=", 2)
|
||||
if k.strip == key
|
||||
found = true
|
||||
"#{key}=#{value}"
|
||||
else
|
||||
"#{k.strip}=#{v.strip}"
|
||||
end
|
||||
end
|
||||
out << "#{key}=#{value}" unless found
|
||||
"#{out.join("\n")}\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,6 +8,62 @@ module Backlog
|
|||
CANDIDATE_DIRS = ["docs/issues"].freeze
|
||||
CANDIDATE_FILES = ["ISSUES.md"].freeze
|
||||
|
||||
# Labels that mark an issue as part of a dependency chain. Matched as
|
||||
# whole (hyphen-preserving) tokens, case-insensitively — "not-blocked"
|
||||
# does NOT match "blocked".
|
||||
CHAIN_LABELS = %w[blocked blocked-by depends depends-on prereq
|
||||
prerequisite waiting needs].freeze
|
||||
|
||||
# Dependency language that must appear near a cross-reference for it to
|
||||
# count as chain evidence — a plain "fixed in #12" does not.
|
||||
DEPENDENCY_LANGUAGE = /\b(blocked|depends|after|requires|prereq|follows)/i
|
||||
CROSS_REF_WINDOW = 40 # chars either side of the "#N" reference
|
||||
|
||||
# Pure classifier for the shape of a repo's open git issues — no shell,
|
||||
# no network. Feeds the route skill's split-vs-migrate recommendation
|
||||
# (ADR-0033: git issues are specs; a chain of interdependent issues is
|
||||
# spec-shaped and should stay put).
|
||||
#
|
||||
# @param issues [Array<Hash>, nil] one hash per open issue:
|
||||
# {number:, title:, labels: [String], body:}
|
||||
# @return [String, nil] "sequential-chain" | "flat-adhoc" | "mixed", or
|
||||
# nil when there's no metadata or fewer than 3 issues (no evidence,
|
||||
# which is distinct from evidence of flatness)
|
||||
def self.classify_issue_shape(issues)
|
||||
return nil if issues.nil? || issues.size < 3
|
||||
|
||||
numbers = issues.map { |issue| issue[:number].to_i }
|
||||
with_evidence = issues.count { |issue| chain_evidence?(issue, numbers) }
|
||||
return "flat-adhoc" if with_evidence.zero?
|
||||
|
||||
with_evidence * 3 >= issues.size * 2 ? "sequential-chain" : "mixed"
|
||||
end
|
||||
|
||||
def self.chain_evidence?(issue, numbers)
|
||||
chain_label?(issue[:labels]) ||
|
||||
chain_cross_reference?(issue[:body], own_number: issue[:number].to_i, numbers: numbers)
|
||||
end
|
||||
|
||||
def self.chain_label?(labels)
|
||||
Array(labels).any? do |label|
|
||||
tokens = label.to_s.downcase.scan(/[a-z][a-z-]*[a-z]|[a-z]/)
|
||||
tokens.any? { |token| CHAIN_LABELS.include?(token) }
|
||||
end
|
||||
end
|
||||
|
||||
def self.chain_cross_reference?(body, own_number:, numbers:)
|
||||
text = body.to_s
|
||||
text.enum_for(:scan, /#(\d+)/).any? do
|
||||
match = Regexp.last_match
|
||||
referenced = match[1].to_i
|
||||
next false if referenced == own_number || !numbers.include?(referenced)
|
||||
|
||||
window_start = [match.begin(0) - CROSS_REF_WINDOW, 0].max
|
||||
window = text[window_start, (match.begin(0) - window_start) + match[0].length + CROSS_REF_WINDOW]
|
||||
window.match?(DEPENDENCY_LANGUAGE)
|
||||
end
|
||||
end
|
||||
|
||||
# @param repo_path [String] absolute path to the repo
|
||||
# @return [Array<String>] repo-relative paths of in-repo issue tracking
|
||||
# found (empty if none)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
require "json"
|
||||
require "fileutils"
|
||||
|
||||
module Backlog
|
||||
# The global project index at ~/.cc-os/projects.json: a derived map of
|
||||
# canonical repo path -> {name, tracker, remote, updated_at}, maintained as
|
||||
# a side effect of config-write (issue #27). Deliberately GLOBAL state
|
||||
# (distinct from per-project .cc-os/ under ADR-0027) so any session can ask
|
||||
# "which tracker does project X use" without visiting its repo.
|
||||
#
|
||||
# Pure over an injected path: callers hand in the already-canonical repo
|
||||
# path (the CLI resolves it via `git rev-parse --show-toplevel`). Read
|
||||
# paths never raise — a missing or corrupt file reads as an empty index; a
|
||||
# corrupt file is only overwritten when a new upsert happens. Writes are
|
||||
# atomic (temp file + rename in the same directory).
|
||||
class ProjectIndex
|
||||
VERSION = 1
|
||||
|
||||
# Default index path, honoring a CC_OS_HOME override (used by tests to
|
||||
# keep the real index untouched) and falling back to $HOME/.cc-os.
|
||||
def self.default_path(env: ENV)
|
||||
base = env["CC_OS_HOME"] || File.join(env.fetch("HOME", Dir.home), ".cc-os")
|
||||
File.join(base, "projects.json")
|
||||
end
|
||||
|
||||
def initialize(path: self.class.default_path, today: -> { Time.now.strftime("%Y-%m-%d") })
|
||||
@path = path
|
||||
@today = today
|
||||
end
|
||||
|
||||
# @return [Hash] the projects map (canonical path -> row); {} when the
|
||||
# file is missing or unreadable/corrupt. Never raises.
|
||||
def all
|
||||
data = read
|
||||
data["projects"].is_a?(Hash) ? data["projects"] : {}
|
||||
end
|
||||
|
||||
# Insert or update the row for a canonical repo path. Atomic write;
|
||||
# creates the parent directory if missing.
|
||||
def upsert(path:, name:, tracker:, remote:)
|
||||
projects = all
|
||||
projects[path] = {
|
||||
"name" => name,
|
||||
"tracker" => tracker,
|
||||
"remote" => remote,
|
||||
"updated_at" => @today.call
|
||||
}
|
||||
write({ "version" => VERSION, "projects" => projects })
|
||||
projects[path]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def read
|
||||
return {} unless File.exist?(@path)
|
||||
|
||||
parsed = JSON.parse(File.read(@path))
|
||||
parsed.is_a?(Hash) ? parsed : {}
|
||||
rescue StandardError
|
||||
{}
|
||||
end
|
||||
|
||||
def write(data)
|
||||
dir = File.dirname(@path)
|
||||
FileUtils.mkdir_p(dir)
|
||||
temp = File.join(dir, ".projects.json.tmp.#{Process.pid}")
|
||||
File.write(temp, JSON.pretty_generate(data) + "\n")
|
||||
File.rename(temp, @path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -18,6 +18,31 @@ module Backlog
|
|||
|
||||
DEV_ROOT = File.expand_path("~/dev")
|
||||
CLIENTS_ROOT = File.expand_path("~/clients")
|
||||
SERVERS_ROOT = File.expand_path("~/servers")
|
||||
|
||||
# Path-based Planka project heuristic, shared by the resolver's own
|
||||
# ambiguity gate and the `board-ensure` CLI's --project default: "Dev"
|
||||
# under ~/dev/*, "Clients" under ~/clients/*, "Servers" under
|
||||
# ~/servers/* (the servers umbrella repo — ADR: infrastructure is a
|
||||
# distinct domain from Dev), nil (ambiguous) otherwise. Board-ensure
|
||||
# always accepts an arbitrary --project name too — this heuristic only
|
||||
# supplies the default when none is given.
|
||||
#
|
||||
# @param repo_path [String]
|
||||
# @return [String, nil]
|
||||
def self.project_for(repo_path)
|
||||
path = File.expand_path(repo_path)
|
||||
return "Dev" if under_root?(path, DEV_ROOT)
|
||||
return "Clients" if under_root?(path, CLIENTS_ROOT)
|
||||
return "Servers" if under_root?(path, SERVERS_ROOT)
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def self.under_root?(path, root)
|
||||
path == root || path.start_with?("#{root}/")
|
||||
end
|
||||
private_class_method :under_root?
|
||||
|
||||
# @param repo_path [String] absolute path to the repo
|
||||
# @param config_contents [String, nil] raw contents of .cc-os/config, or nil/absent
|
||||
|
|
@ -31,7 +56,8 @@ module Backlog
|
|||
|
||||
# @return [Decision]
|
||||
def resolve
|
||||
return decision_for_name(@config.board) || stop_and_discuss if @config.configured?
|
||||
configured_board = @config.planka_board
|
||||
return decision_for_name(configured_board) || stop_and_discuss if configured_board
|
||||
|
||||
derived_decision || stop_and_discuss
|
||||
end
|
||||
|
|
@ -60,17 +86,6 @@ module Backlog
|
|||
|
||||
def board_name = File.basename(@repo_path)
|
||||
|
||||
# "Dev" for paths under ~/dev/*, "Clients" for paths under ~/clients/*,
|
||||
# nil (ambiguous) otherwise.
|
||||
def project
|
||||
return "Dev" if under?(DEV_ROOT)
|
||||
return "Clients" if under?(CLIENTS_ROOT)
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def under?(root)
|
||||
@repo_path == root || @repo_path.start_with?("#{root}/")
|
||||
end
|
||||
def project = self.class.project_for(@repo_path)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,6 +25,22 @@ module Backlog
|
|||
nil
|
||||
end
|
||||
|
||||
# Where /to-issues output (spec slices) should be published, given the
|
||||
# repo's tracker key. The ADR'd boundary: git issues = specs, Planka =
|
||||
# state. So forgejo:/github: keys route spec slices to that git issue
|
||||
# tracker. `planka:` / `repo:` / nil deliberately map to
|
||||
# :planka_default — what that MEANS for spec-shaped output is a named
|
||||
# decision gate at wording review (issue #16), not settled here.
|
||||
#
|
||||
# @return [Symbol] :git_issues | :planka_default | :unrouted (invalid key)
|
||||
def self.issues_destination(value)
|
||||
case kind(value)
|
||||
when :forgejo, :github then :git_issues
|
||||
when :planka, :repo then :planka_default
|
||||
else value.nil? ? :planka_default : :unrouted
|
||||
end
|
||||
end
|
||||
|
||||
# Parse the first remote line of `git remote -v` output and classify it
|
||||
# as a Forgejo or GitHub remote (or :unknown for anything else).
|
||||
#
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
module Backlog
|
||||
# Pure decision for the triage-trigger SessionStart hook (card
|
||||
# 1818330518584820864): given the label arrays already sitting on a
|
||||
# board's Backlog-column cards, does the hook need to instruct dispatch
|
||||
# of the card-triage agent? Network/board-fetch stays in the hook
|
||||
# script (hooks/triage_check.rb) — this class only classifies +
|
||||
# composes the note text, so the decision is unit-testable without
|
||||
# Planka.
|
||||
#
|
||||
# "Unlabeled" mirrors card-triage's own definition (agents/card-triage.md):
|
||||
# a card is raw if it is missing a priority label (P0-P3) OR an
|
||||
# autonomy label (hitl/semi/afk-ready) — not just totally bare.
|
||||
class TriageCheck
|
||||
PRIORITY_LABELS = %w[P0 P1 P2 P3].freeze
|
||||
AUTONOMY_LABELS = %w[hitl semi afk-ready].freeze
|
||||
|
||||
# Which board should this session's triage check inspect? Mirrors the
|
||||
# CLI's config semantics (the session cwd's own .cc-os/config wins —
|
||||
# umbrella subdir configs override the repo root's), falling back to
|
||||
# the root config, then to the path heuristic. Honors both the
|
||||
# explicit `board` key and `tracker=planka:<board>` (via
|
||||
# Config#planka_board). Pure: the hook script reads the files and
|
||||
# passes contents (or nil) in.
|
||||
#
|
||||
# @param cwd [String] the session's working directory
|
||||
# @param root [String] the enclosing git toplevel (== cwd outside umbrellas)
|
||||
# @param cwd_config [String, nil] contents of <cwd>/.cc-os/config
|
||||
# @param root_config [String, nil] contents of <root>/.cc-os/config
|
||||
# @return [String, nil] board name, or nil (don't guess) when nothing
|
||||
# is configured and the path heuristic doesn't recognize the path
|
||||
def self.board_name_for(cwd:, root:, cwd_config:, root_config:)
|
||||
config_home = cwd_config ? cwd : root
|
||||
config = Config.new(cwd_config || root_config)
|
||||
return config.planka_board if config.planka_board
|
||||
return nil unless Resolver.project_for(config_home)
|
||||
|
||||
File.basename(config_home)
|
||||
end
|
||||
|
||||
# @param labels [Array<String>] the label names on one card
|
||||
# @return [Boolean]
|
||||
def self.needs_triage?(labels)
|
||||
(labels & PRIORITY_LABELS).empty? || (labels & AUTONOMY_LABELS).empty?
|
||||
end
|
||||
|
||||
# @param backlog_cards_labels [Array<Array<String>>] label arrays,
|
||||
# one per card currently in the board's Backlog list
|
||||
# @return [Array<Array<String>>] the subset needing triage
|
||||
def self.unlabeled(backlog_cards_labels)
|
||||
backlog_cards_labels.select { |labels| needs_triage?(labels) }
|
||||
end
|
||||
|
||||
# @param board_name [String]
|
||||
# @param backlog_cards_labels [Array<Array<String>>]
|
||||
# @return [String, nil] the SessionStart additionalContext note, or
|
||||
# nil when there is nothing to triage
|
||||
def self.note_for(board_name, backlog_cards_labels)
|
||||
count = unlabeled(backlog_cards_labels).size
|
||||
return nil if count.zero?
|
||||
|
||||
card_word = count == 1 ? "card" : "cards"
|
||||
"[os-backlog] #{count} unlabeled Backlog #{card_word} on board #{board_name} -> " \
|
||||
"dispatch the card-triage agent now (model: haiku, run in background) to label " \
|
||||
"them. Labeling is this project's job for its own board (a parent umbrella may " \
|
||||
"also label its child boards) — never label cards on a board that belongs to a " \
|
||||
"different project."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
require "json"
|
||||
require "fileutils"
|
||||
|
||||
module Backlog
|
||||
# Pure logic for the issue-triggered wakeup poller (ADR-0035): which
|
||||
# projects are polled, which issues count as new, what autonomy tier a
|
||||
# label set maps to, and the tmux command a wakeup would run (ADR-0036).
|
||||
#
|
||||
# All methods are pure over injected data — the bin/wakeup-poll glue does
|
||||
# the shelling out (tea/gh, tmux) and file IO. Read paths never raise.
|
||||
module Wakeup
|
||||
module_function
|
||||
|
||||
# Autonomy tiers, from ADR-0029 label vocabulary (ADR-0035 mapping):
|
||||
# afk-ready -> acting session; semi -> triage-only session;
|
||||
# anything else (hitl / unlabeled) -> notify-only.
|
||||
def tier(labels)
|
||||
names = Array(labels).flat_map { |l| l.to_s.split(",") }.map(&:strip)
|
||||
return :afk_ready if names.include?("afk-ready")
|
||||
return :semi if names.include?("semi")
|
||||
|
||||
:notify
|
||||
end
|
||||
|
||||
# Filter global-index rows (ADR-0034 shape: path -> row) down to
|
||||
# wakeup-eligible projects: tracker forgejo:/github: AND the per-project
|
||||
# .cc-os/config carries wakeup=true (config is the authority; the
|
||||
# config_reader is injected so this stays pure).
|
||||
#
|
||||
# @return [Array<Hash>] rows as {path:, name:, tracker_kind:, repo_slug:}
|
||||
def eligible_projects(projects, config_reader:)
|
||||
projects.filter_map do |path, row|
|
||||
tracker = row["tracker"].to_s
|
||||
kind, slug = tracker.split(":", 2)
|
||||
next unless %w[forgejo github].include?(kind) && !slug.to_s.empty?
|
||||
next unless opted_in?(config_reader.call(path))
|
||||
|
||||
{ path: path, name: row["name"] || File.basename(path),
|
||||
tracker_kind: kind, repo_slug: slug }
|
||||
end
|
||||
end
|
||||
|
||||
def opted_in?(config_text)
|
||||
config_text.to_s.each_line.any? { |l| l.strip == "wakeup=true" }
|
||||
end
|
||||
|
||||
# Issues newer than the last-seen index for this repo. A nil last_seen
|
||||
# means the repo has never been scanned: baseline without waking
|
||||
# anything (ADR-0035 — no bulk backfill of pre-existing issues).
|
||||
def new_issues(issues, last_seen)
|
||||
return [] if last_seen.nil?
|
||||
|
||||
issues.select { |i| i["index"].to_i > last_seen.to_i }
|
||||
end
|
||||
|
||||
def highest_index(issues, floor = 0)
|
||||
([floor.to_i] + issues.map { |i| i["index"].to_i }).max
|
||||
end
|
||||
|
||||
# The tmux command a wakeup runs (ADR-0036 naming: cc-<project>-issue<N>).
|
||||
def tmux_command(project:, path:, issue_index:, prompt:)
|
||||
[
|
||||
"tmux", "new-session", "-d",
|
||||
"-s", "cc-#{project}-issue#{issue_index}",
|
||||
"-c", path,
|
||||
"claude", prompt
|
||||
]
|
||||
end
|
||||
|
||||
# Initial prompt for the wakened session: issue coordinates + autonomy
|
||||
# tier + the standing guardrails (ADR-0035 context handoff). The issue
|
||||
# body (with any Discoverer block) is passed through verbatim.
|
||||
def session_prompt(issue:, tier:, url:)
|
||||
action = tier == :afk_ready ? AFK_READY_ACTION : SEMI_ACTION
|
||||
<<~PROMPT
|
||||
Wakeup: new issue ##{issue['index']} — #{issue['title']}
|
||||
#{url}
|
||||
|
||||
#{action}
|
||||
File-don't-fix still applies (ADR-0034): never edit other projects.
|
||||
Before exiting, post your triage/summary as a comment on the issue —
|
||||
that comment is the durable record (ADR-0036 lifecycle).
|
||||
|
||||
Issue body:
|
||||
#{issue['body']}
|
||||
PROMPT
|
||||
end
|
||||
|
||||
AFK_READY_ACTION =
|
||||
"This issue is labeled afk-ready: triage it and, if it is actionable " \
|
||||
"within this repo, do the work."
|
||||
SEMI_ACTION =
|
||||
"This issue is labeled semi: TRIAGE ONLY — label, comment, and " \
|
||||
"propose an approach, but stop at proposing. Do not implement."
|
||||
|
||||
# Last-seen state (~/.cc-os/wakeup-state.json): repo_slug -> highest
|
||||
# issue index seen. Missing/corrupt reads as empty; writes are atomic.
|
||||
class State
|
||||
def self.default_path(env: ENV)
|
||||
base = env["CC_OS_HOME"] || File.join(env.fetch("HOME", Dir.home), ".cc-os")
|
||||
File.join(base, "wakeup-state.json")
|
||||
end
|
||||
|
||||
def initialize(path: self.class.default_path)
|
||||
@path = path
|
||||
end
|
||||
|
||||
def last_seen(repo_slug)
|
||||
read[repo_slug]
|
||||
end
|
||||
|
||||
def record(repo_slug, index)
|
||||
data = read
|
||||
data[repo_slug] = index
|
||||
write(data)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def read
|
||||
return {} unless File.exist?(@path)
|
||||
|
||||
parsed = JSON.parse(File.read(@path))
|
||||
parsed.is_a?(Hash) ? parsed : {}
|
||||
rescue StandardError
|
||||
{}
|
||||
end
|
||||
|
||||
def write(data)
|
||||
dir = File.dirname(@path)
|
||||
FileUtils.mkdir_p(dir)
|
||||
temp = File.join(dir, ".wakeup-state.json.tmp.#{Process.pid}")
|
||||
File.write(temp, JSON.pretty_generate(data) + "\n")
|
||||
File.rename(temp, @path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -11,7 +11,7 @@ Capture a piece of work as a card on the right Planka board, cheaply, without in
|
|||
- **Next stays human-curated in both directions** — never move a card into or out of Next.
|
||||
- **Never move a card labeled `hitl`** — hitl cards are human-owned, never self-assigned or moved by the AI.
|
||||
- These rules are enforced deterministically by the CLI (`card-move`), not just documented here — a violating move fails with a one-line reason naming the rule.
|
||||
- Priority/autonomy labeling is not capture's job — leave new cards unlabeled; the card-triage agent batches that later.
|
||||
- Priority/autonomy labeling is not capture's job — leave new cards unlabeled; the card-triage agent batches that later. Triage is initiated by the SessionStart hook of the board's OWNING project (the project the card was filed onto, or a parent umbrella labeling its own/child boards) — never by whichever project happened to file the card. A cross-project filer (see CROSS-PROJECT below) must never label the destination board's cards itself.
|
||||
|
||||
## Procedure
|
||||
|
||||
|
|
@ -31,9 +31,15 @@ All commands use the plugin CLI at `${CLAUDE_PLUGIN_ROOT}/bin/os-backlog`.
|
|||
- `stop-and-discuss` → do NOT create anything. Tell the user the repo doesn't map to a board and ask where the card should go.
|
||||
3. **Ensure the board exists** (idempotent — safe to run every time):
|
||||
```bash
|
||||
${CLAUDE_PLUGIN_ROOT}/bin/os-backlog board-ensure <board> --project <Dev|Clients>
|
||||
${CLAUDE_PLUGIN_ROOT}/bin/os-backlog board-ensure <board>
|
||||
```
|
||||
Project is `Dev` for repos under `~/dev/`, `Clients` for repos under `~/clients/`.
|
||||
Omit `--project` and it derives the right Planka project from the repo path: `Dev` under
|
||||
`~/dev/`, `Clients` under `~/clients/`, `Servers` under `~/servers/`. Pass `--project <name>`
|
||||
to target (or create) any other project explicitly — board-ensure creates the project too if
|
||||
it doesn't exist yet. There's no command to move an existing board between projects; that's a
|
||||
manual step in the Planka UI. Creating a NEW Planka project requires `PLANKA_HUMAN_USER_ID`
|
||||
(or an explicit human user id) — board-ensure fails loudly rather than silently creating a
|
||||
bot-only project.
|
||||
4. **Create the card at Backlog:**
|
||||
```bash
|
||||
${CLAUDE_PLUGIN_ROOT}/bin/os-backlog card-add --board <board> --title "<short imperative title>" [--description "<context: repo, file paths, why>"]
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ Register a project's issue tracker: figure out what already exists, propose wher
|
|||
|
||||
## The boundary rule (what you're deciding between)
|
||||
|
||||
- **Planka = state.** Cards track *what's happening now* — backlog/next/doing/review/done. Use `planka:<board>` when the project's work is mostly ephemeral tasks, small fixes, or process/coordination — nothing that needs a durable written spec.
|
||||
- **Git issues = specs only.** Use `forgejo:<owner>/<repo>` or `github:<owner>/<repo>` when work is specified as text that needs to survive and be referenced (features, migrations, architecture-level changes) — tracer-bullet slices from `/to-issues`, PRDs from `/to-prd`.
|
||||
- **`repo:<path>`** is the escape hatch for a project that already tracks issues in-repo (`docs/issues/`, `ISSUES.md`) and wants to keep doing so without moving to Planka or a git host's issue tracker.
|
||||
- **Card-as-pointer + issue-chain-as-spec, for code efforts:** when a body of work needs both — ongoing state tracking AND a durable spec — the Planka card is a pointer (title + link) into a chain of git issues that hold the actual spec. Don't duplicate the spec text into the card description.
|
||||
- **Promotion rule:** if a Planka-tracked effort accretes significant code/design decisions in card comments, that's a signal to promote it — open a git issue (or issue chain) holding the durable spec, and turn the Planka card into a pointer per the rule above. Flag this to the user when you see it happening; don't do it silently.
|
||||
Planka = state, git issues = specs (ADR-0033, `docs/adr/0033-tracker-routing-planka-is-state-git-issues-are-specs.md`) — the ADR is the canonical statement; the operative routing rules are:
|
||||
|
||||
- **WHEN** the project's work is mostly ephemeral tasks, small fixes, or process/coordination (state: backlog/next/doing/review/done, nothing needing a durable written spec) → `planka:<board>`.
|
||||
- **WHEN** work is specified as text that must survive and be referenced (features, migrations, architecture-level changes, tracer-bullet slices from `/to-issues`, PRDs from `/to-prd`) → `forgejo:<owner>/<repo>` or `github:<owner>/<repo>`.
|
||||
- **WHEN** a project already tracks issues in-repo (`docs/issues/`, `ISSUES.md`) and wants to stay there → `repo:<path>` (the escape hatch).
|
||||
- **WHEN** a code effort needs both state tracking and a durable spec → card-as-pointer + issue-chain-as-spec: the Planka card is a pointer (title + link) into the git issue chain holding the spec; never duplicate spec text into the card description.
|
||||
- **WHEN** a Planka-tracked effort accretes significant code/design decisions in card comments → flag promotion to the user (open a git issue chain for the spec, turn the card into a pointer); never promote silently.
|
||||
|
||||
Valid tracker key formats (exactly one, written to `.cc-os/config`'s `tracker` key): `planka:<board>` | `forgejo:<owner>/<repo>` | `github:<owner>/<repo>` | `repo:<path>`.
|
||||
|
||||
|
|
@ -26,12 +28,16 @@ All commands use the plugin CLI at `${CLAUDE_PLUGIN_ROOT}/bin/os-backlog`.
|
|||
- `tracker_configured` — an existing `tracker` key in `.cc-os/config`, if any. If this is already set, tell the user the project is already routed to it and confirm they want to re-route before continuing (re-routing is itself a decision gate — treat it as step 2).
|
||||
- `planka` — whether a Planka board already resolves for this repo (via the same resolver `/os-backlog:capture` uses), or why that check couldn't run (gem/credentials unavailable — fail-soft, not blocking).
|
||||
- `git_remote` — the parsed `git remote -v`, classified as `forgejo` (the user's self-hosted instance) / `github` / `unknown`, plus the open-issue count via `tea` or `gh` if that CLI is available (`null` with a `reason` if not — fail-soft, not blocking).
|
||||
- `issue_shape` — a classification of the open git issues' shape: `sequential-chain` (most issues carry dependency labels or blocked-by/depends-on cross-references), `flat-adhoc` (no dependency structure), `mixed` (some), or `null` (no metadata, or fewer than 3 open issues — no evidence either way).
|
||||
- `in_repo_issue_files` — `docs/issues/` and/or `ISSUES.md` if present.
|
||||
|
||||
2. **Synthesize and propose (NAMED DECISION GATE).** Given the findings, propose exactly ONE destination tracker key with a short rationale grounded in the boundary rule above — e.g. "this repo has an active Forgejo remote with 11 open issues and no Planka board; issues already carry specs, so `forgejo:jared/cc-os` fits git-issues-as-spec" or "no existing tracking found, work here is small ad hoc fixes; `planka:<repo-name>` fits Planka-as-state; a board will be created on first capture, not by this skill."
|
||||
2. **Synthesize and propose (NAMED DECISION GATE).** Exactly ONE tracker key is ever written to `.cc-os/config` — the branches below differ in *which* key and *what happens to existing issues*, never in how many keys. Branch on the findings:
|
||||
- **(a) No existing tracker found** (no open git issues, no in-repo issue files, no live board): propose one destination key with a short rationale grounded in the boundary rule — e.g. "no existing tracking found, work here is small ad hoc fixes; `planka:<repo-name>` fits Planka-as-state; a board will be created on first capture, not by this skill."
|
||||
- **(b) Existing git issues that are spec-shaped** (`issue_shape` is `sequential-chain` or `mixed`): recommend the **SPLIT as the default**. Write `planka:<board>` as the single declared tracker — it governs new/ephemeral capture only. The existing git issues are specs per ADR-0033: they stay exactly where they are, unmigrated, and remain the durable spec surface; skip migration entirely (step 3 does not apply to them). This is still exactly one tracker key — the split is a division of labor (state vs specs), not two destinations.
|
||||
- **(c) Existing git issues that are flat-adhoc** (`issue_shape` is `flat-adhoc`): migration to Planka is on the table — propose the destination key, and if confirmed, existing open issues go through the step-3 migration gate as usual. If `issue_shape` is `null` (no metadata or too few issues to tell), treat shape as unknown: ask the user which shape fits rather than assuming.
|
||||
**Stop here and wait for the user to confirm or override the destination before writing anything or moving anything.** If findings are ambiguous (e.g. both a live Planka board AND an active issue tracker with open items, and it's unclear which is authoritative), say so plainly and ask rather than guessing.
|
||||
|
||||
3. **If migration is needed (SECOND NAMED DECISION GATE).** Only applies when the confirmed destination differs from where open items currently live (e.g. moving from ad hoc `ISSUES.md` entries to Planka, or from an unrouted Planka board to git issues). Before touching any live project history:
|
||||
3. **If migration is needed (SECOND NAMED DECISION GATE).** Only applies when the confirmed destination differs from where open items currently live (e.g. moving from ad hoc `ISSUES.md` entries to Planka, or from an unrouted Planka board to git issues). **Exception — never re-trigger on the split:** a `planka:` tracker alongside by-design git spec issues (branch 2b, or any project whose git issues are specs per ADR-0033) is the intended end state, not a discrepancy; do not propose migrating those issues, now or on any later re-run of this skill. Before touching any live project history:
|
||||
- Tell the user exactly what will move (list the items) and ask for explicit go-ahead. This gate is separate from the destination gate in step 2 — confirming the destination is not confirming the migration.
|
||||
- Once confirmed, migrate mechanically using **existing machinery, not new code paths**: `card-add`/`cards` (via `/os-backlog:capture`'s CLI calls) for items moving into Planka; `tea`/`gh` issue-create commands for items moving into git issues. Do not invent bespoke migration scripts.
|
||||
- **Back-link both ways so nothing is double-tracked**: the old item (closed Planka card comment, closed in-repo issue entry, or a note in the git-host issue if migrating away from it) gets a pointer to its new home; the new item (card description or issue body) links back to the source. Close/archive the old item once the back-link is in place — don't leave both open.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
# Cross-project filing (file-don't-fix)
|
||||
|
||||
When work surfaces that belongs to a DIFFERENT project than the one this session is
|
||||
working in, do not edit that project — file the work on ITS tracker and continue. The
|
||||
owning project's own session picks it up with full context.
|
||||
|
||||
## Finding the target's tracker
|
||||
|
||||
Query the global project index (derived from every `config-write`, at
|
||||
`~/.cc-os/projects.json`):
|
||||
|
||||
```
|
||||
os-backlog projects # full index as JSON
|
||||
os-backlog projects <substring> # filter by project name
|
||||
```
|
||||
|
||||
Each row: canonical repo path -> `{name, tracker, remote, updated_at}`. If the target
|
||||
project has no row, it hasn't been routed yet — tell the user rather than guessing.
|
||||
|
||||
## Filing mechanics by tracker kind
|
||||
|
||||
- `forgejo:<owner>/<repo>` — `tea issues create --repo <owner>/<repo> --title "..." --description "..."`
|
||||
- `github:<owner>/<repo>` — `gh issue create --repo <owner>/<repo> --title "..." --body "..."`
|
||||
- `planka:<board>` (planka-only targets) — `os-backlog card-add --board <board> --title "..." [--description "..."]`
|
||||
|
||||
Routing inside the target project still follows ITS boundary rule (ADR-0033: Planka is
|
||||
state, git issues are specs) — a durable spec goes to the git-issue tracker even if the
|
||||
project also has a board.
|
||||
|
||||
## Discoverer block (append verbatim to the issue/card description)
|
||||
|
||||
```
|
||||
--------
|
||||
**Discoverer:** <filing repo or path>, session <claude-code-session-id>, <YYYY-MM-DD>. <one-line motivation>
|
||||
```
|
||||
|
||||
Session id source: the SessionStart hook receives it as the `session_id` field of its
|
||||
stdin JSON; a live session can also find it in its transcript filename. If it is
|
||||
genuinely unavailable, write `session id unavailable` — never fabricate one.
|
||||
|
|
@ -72,6 +72,17 @@ class BoardEnsurerTest < Minitest::Test
|
|||
refute_equal first_choice, p0.color
|
||||
end
|
||||
|
||||
def test_label_color_falls_back_when_server_rejects_with_400
|
||||
first_choice = Backlog::BoardSpec::LABEL_COLOR_CANDIDATES.fetch("P3").first
|
||||
client = FakePlankaClient.new(reject_colors_with_400: [first_choice])
|
||||
ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1")
|
||||
|
||||
ensurer.ensure("llf-schema", project_name: "Dev")
|
||||
|
||||
p3 = client.labels_store.find { |l| l.name == "P3" }
|
||||
refute_equal first_choice, p3.color
|
||||
end
|
||||
|
||||
|
||||
def test_activate_renames_archived_board_back
|
||||
client = FakePlankaClient.new
|
||||
|
|
@ -109,6 +120,46 @@ class BoardEnsurerTest < Minitest::Test
|
|||
refute_nil archived
|
||||
end
|
||||
|
||||
def test_ensure_creates_an_arbitrary_project_name_on_demand
|
||||
client = FakePlankaClient.new
|
||||
ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1")
|
||||
|
||||
result = ensurer.ensure("ovh-prod", project_name: "Servers")
|
||||
|
||||
assert result.created
|
||||
project = client.projects_store.first
|
||||
assert_equal "Servers", project.name
|
||||
assert_equal "Servers", result.project.name
|
||||
end
|
||||
|
||||
def test_raises_missing_human_user_id_error_when_creating_new_project_without_one
|
||||
original = ENV.delete("PLANKA_HUMAN_USER_ID")
|
||||
client = FakePlankaClient.new
|
||||
ensurer = Backlog::BoardEnsurer.new(client: client)
|
||||
|
||||
error = assert_raises(Backlog::BoardEnsurer::MissingHumanUserIdError) do
|
||||
ensurer.ensure("llf-schema", project_name: "Dev")
|
||||
end
|
||||
|
||||
assert_includes error.message, "PLANKA_HUMAN_USER_ID"
|
||||
ensure
|
||||
ENV["PLANKA_HUMAN_USER_ID"] = original
|
||||
end
|
||||
|
||||
def test_ensure_against_existing_project_does_not_raise_without_human_user_id
|
||||
original = ENV.delete("PLANKA_HUMAN_USER_ID")
|
||||
client = FakePlankaClient.new
|
||||
seeding_ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1")
|
||||
seeding_ensurer.ensure("board-a", project_name: "Dev")
|
||||
|
||||
ensurer = Backlog::BoardEnsurer.new(client: client)
|
||||
result = ensurer.ensure("board-b", project_name: "Dev")
|
||||
|
||||
refute result.project.nil?
|
||||
ensure
|
||||
ENV["PLANKA_HUMAN_USER_ID"] = original
|
||||
end
|
||||
|
||||
def test_ensure_rejects_archived_prefixed_name
|
||||
client = FakePlankaClient.new
|
||||
ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
require_relative "test_helper"
|
||||
|
||||
class BoardResolverTest < Minitest::Test
|
||||
include BacklogTestHelpers
|
||||
|
||||
def setup
|
||||
@client = FakePlankaClient.new
|
||||
ensurer = Backlog::BoardEnsurer.new(client: @client, human_user_id: "human-1")
|
||||
@board = ensurer.ensure("llf-schema", project_name: "Dev").board
|
||||
@writes = []
|
||||
end
|
||||
|
||||
def resolver_with(config)
|
||||
@config = config
|
||||
Backlog::BoardResolver.new(
|
||||
client: @client,
|
||||
read_config: -> { @config },
|
||||
write_config: ->(contents) { @writes << contents }
|
||||
)
|
||||
end
|
||||
|
||||
def config_with_cache(id = @board.id)
|
||||
"# repo config\ntracker=planka:llf-schema\nboard_id=#{id}\nversion=3\n"
|
||||
end
|
||||
|
||||
def test_cache_hit_uses_boards_get_and_skips_name_listing
|
||||
@client.projects.define_singleton_method(:list) { raise "projects.list must not be called on a cache hit" }
|
||||
resolver = resolver_with(config_with_cache)
|
||||
|
||||
board = resolver.board_for("llf-schema")
|
||||
|
||||
assert_equal @board.id, board.id
|
||||
assert_empty @writes
|
||||
end
|
||||
|
||||
def test_cache_miss_resolves_by_name_and_writes_board_id_preserving_comments
|
||||
resolver = resolver_with("# repo config\ntracker=planka:llf-schema\nversion=3\n")
|
||||
|
||||
board = resolver.board_for("llf-schema")
|
||||
|
||||
assert_equal @board.id, board.id
|
||||
assert_equal 1, @writes.size
|
||||
assert_equal "# repo config\ntracker=planka:llf-schema\nversion=3\nboard_id=#{@board.id}\n",
|
||||
@writes.last
|
||||
end
|
||||
|
||||
def test_stale_id_404_falls_back_to_name_lookup_and_rewrites_cache
|
||||
resolver = resolver_with(config_with_cache("no-such-id"))
|
||||
|
||||
board = resolver.board_for("llf-schema")
|
||||
|
||||
assert_equal @board.id, board.id
|
||||
assert_includes @writes.last, "board_id=#{@board.id}\n"
|
||||
assert_includes @writes.last, "# repo config"
|
||||
end
|
||||
|
||||
def test_renamed_board_name_mismatch_falls_back_and_rewrites_cache
|
||||
stale = @board
|
||||
@client.boards.update(stale.id, name: "something-else")
|
||||
current = @client.boards.create(stale.project_id, name: "llf-schema", position: 2)
|
||||
resolver = resolver_with(config_with_cache(stale.id))
|
||||
|
||||
board = resolver.board_for("llf-schema")
|
||||
|
||||
assert_equal current.id, board.id
|
||||
assert_includes @writes.last, "board_id=#{current.id}\n"
|
||||
end
|
||||
|
||||
def test_non_planka_tracker_ignores_board_id_and_never_writes
|
||||
resolver = resolver_with("tracker=forgejo:jared/cc-os\nboard_id=bogus-id\n")
|
||||
|
||||
board = resolver.board_for("llf-schema")
|
||||
|
||||
assert_equal @board.id, board.id
|
||||
assert_empty @writes
|
||||
end
|
||||
|
||||
def test_board_id_for_different_planka_board_name_is_ignored
|
||||
resolver = resolver_with("tracker=planka:other-board\nboard_id=bogus-id\n")
|
||||
|
||||
board = resolver.board_for("llf-schema")
|
||||
|
||||
assert_equal @board.id, board.id
|
||||
assert_empty @writes # tracker doesn't match this board name; no cache write either
|
||||
end
|
||||
|
||||
def test_unreadable_config_degrades_to_name_lookup
|
||||
resolver = Backlog::BoardResolver.new(
|
||||
client: @client,
|
||||
read_config: -> { raise Errno::EACCES, "config" },
|
||||
write_config: ->(contents) { @writes << contents }
|
||||
)
|
||||
|
||||
board = resolver.board_for("llf-schema")
|
||||
|
||||
assert_equal @board.id, board.id
|
||||
assert_empty @writes
|
||||
end
|
||||
|
||||
def test_unwritable_config_never_fails_the_command
|
||||
resolver = Backlog::BoardResolver.new(
|
||||
client: @client,
|
||||
read_config: -> { "tracker=planka:llf-schema\n" },
|
||||
write_config: ->(_) { raise Errno::EACCES, "config" }
|
||||
)
|
||||
|
||||
board = resolver.board_for("llf-schema")
|
||||
|
||||
assert_equal @board.id, board.id
|
||||
end
|
||||
|
||||
def test_matching_cached_id_is_not_rewritten
|
||||
resolver = resolver_with(config_with_cache)
|
||||
resolver.board_for("llf-schema")
|
||||
|
||||
assert_empty @writes
|
||||
end
|
||||
|
||||
def test_unknown_board_still_raises
|
||||
resolver = resolver_with(nil)
|
||||
error = assert_raises(RuntimeError) { resolver.board_for("nope") }
|
||||
assert_match(/no board named "nope"/, error.message)
|
||||
end
|
||||
|
||||
def test_cards_add_through_resolver_writes_cache_once
|
||||
resolver = resolver_with("tracker=planka:llf-schema\n")
|
||||
cards = Backlog::Cards.new(client: @client, board_resolver: resolver)
|
||||
|
||||
card = cards.add(board_name: "llf-schema", title: "Cache me")
|
||||
|
||||
assert_equal "Cache me", card.name
|
||||
assert_equal 1, @writes.size
|
||||
assert_includes @writes.last, "board_id=#{@board.id}\n"
|
||||
end
|
||||
end
|
||||
|
|
@ -93,4 +93,48 @@ class ConfigTest < Minitest::Test
|
|||
assert_equal "my-hub", updated.to_h["hub"]
|
||||
assert_equal "planka:board", updated.tracker
|
||||
end
|
||||
|
||||
def test_board_id_reader
|
||||
config = Backlog::Config.new("tracker=planka:board\nboard_id=42\n")
|
||||
assert_equal "42", config.board_id
|
||||
assert_nil Backlog::Config.new(nil).board_id
|
||||
end
|
||||
|
||||
def test_set_preserves_comments_and_blank_lines
|
||||
existing = "# managed by cc-os\ntracker=planka:board\n\n# stamped by os-status\nversion=3\n"
|
||||
updated = Backlog::Config.set(existing, "board_id", "42")
|
||||
assert_equal "# managed by cc-os\ntracker=planka:board\n\n# stamped by os-status\nversion=3\nboard_id=42\n",
|
||||
updated
|
||||
end
|
||||
|
||||
def test_set_replaces_existing_key_in_place
|
||||
existing = "# top comment\nboard_id=old\ntracker=planka:board\n"
|
||||
updated = Backlog::Config.set(existing, "board_id", "new")
|
||||
assert_equal "# top comment\nboard_id=new\ntracker=planka:board\n", updated
|
||||
end
|
||||
|
||||
def test_set_normalizes_spaced_key_value_lines_only
|
||||
existing = "# note\nversion = 1\n"
|
||||
updated = Backlog::Config.set(existing, "board_id", "42")
|
||||
assert_equal "# note\nversion=1\nboard_id=42\n", updated
|
||||
end
|
||||
|
||||
def test_set_on_nil_contents
|
||||
assert_equal "board_id=42\n", Backlog::Config.set(nil, "board_id", "42")
|
||||
end
|
||||
|
||||
def test_planka_board_prefers_explicit_board_key
|
||||
config = Backlog::Config.new("board=explicit\ntracker=planka:from-tracker\n")
|
||||
assert_equal "explicit", config.planka_board
|
||||
end
|
||||
|
||||
def test_planka_board_derives_from_planka_tracker
|
||||
config = Backlog::Config.new("tracker=planka:my-board\n")
|
||||
assert_equal "my-board", config.planka_board
|
||||
end
|
||||
|
||||
def test_planka_board_nil_for_non_planka_tracker
|
||||
assert_nil Backlog::Config.new("tracker=forgejo:me/repo\n").planka_board
|
||||
assert_nil Backlog::Config.new(nil).planka_board
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
require_relative "test_helper"
|
||||
require "tmpdir"
|
||||
require "fileutils"
|
||||
require "json"
|
||||
|
||||
class InspectorTest < Minitest::Test
|
||||
def test_detects_docs_issues_dir
|
||||
|
|
@ -23,3 +24,104 @@ class InspectorTest < Minitest::Test
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ClassifyIssueShapeTest < Minitest::Test
|
||||
def issue(number, title: "issue #{number}", labels: [], body: "")
|
||||
{ number: number, title: title, labels: labels, body: body }
|
||||
end
|
||||
|
||||
def test_nil_when_metadata_unavailable
|
||||
assert_nil Backlog::Inspector.classify_issue_shape(nil)
|
||||
end
|
||||
|
||||
def test_nil_when_fewer_than_three_issues_even_with_chain_labels
|
||||
issues = [issue(1, labels: ["blocked"]), issue(2, labels: ["blocked"])]
|
||||
assert_nil Backlog::Inspector.classify_issue_shape(issues)
|
||||
end
|
||||
|
||||
def test_flat_adhoc_when_no_issue_shows_chain_evidence
|
||||
issues = [issue(1), issue(2), issue(3, body: "fixed in #1 last week")]
|
||||
assert_equal "flat-adhoc", Backlog::Inspector.classify_issue_shape(issues)
|
||||
end
|
||||
|
||||
def test_sequential_chain_from_labels
|
||||
issues = [
|
||||
issue(1, labels: ["Blocked-By"]),
|
||||
issue(2, labels: ["depends-on"]),
|
||||
issue(3, labels: ["enhancement"])
|
||||
]
|
||||
assert_equal "sequential-chain", Backlog::Inspector.classify_issue_shape(issues)
|
||||
end
|
||||
|
||||
def test_negated_label_token_does_not_count
|
||||
issues = [issue(1, labels: ["not-blocked"]), issue(2), issue(3)]
|
||||
assert_equal "flat-adhoc", Backlog::Inspector.classify_issue_shape(issues)
|
||||
end
|
||||
|
||||
def test_cross_reference_near_dependency_language_counts
|
||||
issues = [
|
||||
issue(1, body: "This is blocked by #2 until the schema lands."),
|
||||
issue(2, body: "Must land after #3."),
|
||||
issue(3, body: "Standalone groundwork.")
|
||||
]
|
||||
assert_equal "sequential-chain", Backlog::Inspector.classify_issue_shape(issues)
|
||||
end
|
||||
|
||||
def test_plain_cross_reference_without_dependency_language_does_not_count
|
||||
issues = [
|
||||
issue(1, body: "See also #2 for background."),
|
||||
issue(2, body: "Related: #3."),
|
||||
issue(3)
|
||||
]
|
||||
assert_equal "flat-adhoc", Backlog::Inspector.classify_issue_shape(issues)
|
||||
end
|
||||
|
||||
def test_reference_to_number_outside_fetched_set_does_not_count
|
||||
issues = [
|
||||
issue(1, body: "blocked by #99 upstream"),
|
||||
issue(2),
|
||||
issue(3)
|
||||
]
|
||||
assert_equal "flat-adhoc", Backlog::Inspector.classify_issue_shape(issues)
|
||||
end
|
||||
|
||||
def test_dependency_language_too_far_from_reference_does_not_count
|
||||
padding = "x" * 60
|
||||
issues = [
|
||||
issue(1, body: "blocked #{padding} see #2"),
|
||||
issue(2),
|
||||
issue(3)
|
||||
]
|
||||
assert_equal "flat-adhoc", Backlog::Inspector.classify_issue_shape(issues)
|
||||
end
|
||||
|
||||
def test_mixed_when_some_but_not_most_show_evidence
|
||||
issues = [issue(1, labels: ["blocked"]), issue(2), issue(3)]
|
||||
assert_equal "mixed", Backlog::Inspector.classify_issue_shape(issues)
|
||||
end
|
||||
|
||||
def test_self_reference_does_not_count
|
||||
issues = [
|
||||
issue(1, body: "depends on #1 (this issue)"),
|
||||
issue(2),
|
||||
issue(3)
|
||||
]
|
||||
assert_equal "flat-adhoc", Backlog::Inspector.classify_issue_shape(issues)
|
||||
end
|
||||
end
|
||||
|
||||
class InspectCliContractTest < Minitest::Test
|
||||
BIN = File.expand_path("../bin/os-backlog", __dir__)
|
||||
|
||||
# `inspect` must always expose the issue_shape key, even when every
|
||||
# network-ish check degrades (tmpdir: no git remote, no config). Value is
|
||||
# null here because no metadata is fetchable — that's the fail-soft path.
|
||||
def test_inspect_json_always_contains_issue_shape_key
|
||||
Dir.mktmpdir do |dir|
|
||||
out = Dir.chdir(dir) { `ruby #{BIN} inspect 2>/dev/null` }
|
||||
findings = JSON.parse(out)
|
||||
assert findings.key?("issue_shape"), "inspect JSON missing issue_shape key"
|
||||
assert_nil findings["issue_shape"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
# /to-issues routing wiring (issue #16): spec-slice output honors the
|
||||
# tracker key per the ADR'd boundary (git issues = specs, Planka = state).
|
||||
require_relative "test_helper"
|
||||
|
||||
class IssuesRoutingTest < Minitest::Test
|
||||
def test_forgejo_tracker_routes_spec_slices_to_git_issues
|
||||
assert_equal :git_issues, Backlog::Tracker.issues_destination("forgejo:jared/cc-os")
|
||||
end
|
||||
|
||||
def test_github_tracker_routes_spec_slices_to_git_issues
|
||||
assert_equal :git_issues, Backlog::Tracker.issues_destination("github:jared/some-repo")
|
||||
end
|
||||
|
||||
def test_planka_tracker_is_the_gated_default
|
||||
assert_equal :planka_default, Backlog::Tracker.issues_destination("planka:cc-os")
|
||||
end
|
||||
|
||||
def test_repo_tracker_is_the_gated_default
|
||||
assert_equal :planka_default, Backlog::Tracker.issues_destination("repo:docs/issues")
|
||||
end
|
||||
|
||||
def test_absent_tracker_is_the_gated_default
|
||||
assert_equal :planka_default, Backlog::Tracker.issues_destination(nil)
|
||||
end
|
||||
|
||||
def test_invalid_tracker_is_unrouted
|
||||
assert_equal :unrouted, Backlog::Tracker.issues_destination("jira:whatever")
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
require_relative "test_helper"
|
||||
require "tmpdir"
|
||||
require "fileutils"
|
||||
require "json"
|
||||
|
||||
class ProjectIndexTest < Minitest::Test
|
||||
def with_index
|
||||
Dir.mktmpdir do |dir|
|
||||
path = File.join(dir, ".cc-os", "projects.json")
|
||||
yield Backlog::ProjectIndex.new(path: path, today: -> { "2026-07-13" }), path
|
||||
end
|
||||
end
|
||||
|
||||
def test_missing_file_reads_as_empty
|
||||
with_index do |index, _path|
|
||||
assert_equal({}, index.all)
|
||||
end
|
||||
end
|
||||
|
||||
def test_corrupt_file_reads_as_empty_and_is_not_clobbered_by_reads
|
||||
with_index do |index, path|
|
||||
FileUtils.mkdir_p(File.dirname(path))
|
||||
File.write(path, "{not json")
|
||||
assert_equal({}, index.all)
|
||||
assert_equal "{not json", File.read(path) # read paths never rewrite
|
||||
end
|
||||
end
|
||||
|
||||
def test_upsert_creates_row_with_updated_at
|
||||
with_index do |index, path|
|
||||
index.upsert(path: "/repos/foo", name: "foo", tracker: "planka:foo", remote: nil)
|
||||
data = JSON.parse(File.read(path))
|
||||
assert_equal 1, data["version"]
|
||||
row = data["projects"]["/repos/foo"]
|
||||
assert_equal "foo", row["name"]
|
||||
assert_equal "planka:foo", row["tracker"]
|
||||
assert_nil row["remote"]
|
||||
assert_equal "2026-07-13", row["updated_at"]
|
||||
end
|
||||
end
|
||||
|
||||
def test_upsert_updates_existing_row_keyed_by_path
|
||||
with_index do |index, _path|
|
||||
index.upsert(path: "/repos/foo", name: "foo", tracker: "planka:foo", remote: nil)
|
||||
index.upsert(path: "/repos/foo", name: "foo", tracker: "github:me/foo", remote: "r")
|
||||
assert_equal 1, index.all.size
|
||||
assert_equal "github:me/foo", index.all["/repos/foo"]["tracker"]
|
||||
end
|
||||
end
|
||||
|
||||
def test_name_collision_two_paths_same_basename_keeps_both
|
||||
with_index do |index, _path|
|
||||
index.upsert(path: "/a/foo", name: "foo", tracker: "planka:foo", remote: nil)
|
||||
index.upsert(path: "/b/foo", name: "foo", tracker: "github:me/foo", remote: nil)
|
||||
assert_equal %w[/a/foo /b/foo], index.all.keys.sort
|
||||
end
|
||||
end
|
||||
|
||||
def test_upsert_over_corrupt_file_leaves_valid_json_and_no_temp_files
|
||||
with_index do |index, path|
|
||||
FileUtils.mkdir_p(File.dirname(path))
|
||||
File.write(path, "{not json")
|
||||
index.upsert(path: "/repos/foo", name: "foo", tracker: "repo:.", remote: nil)
|
||||
data = JSON.parse(File.read(path))
|
||||
assert_equal ["/repos/foo"], data["projects"].keys
|
||||
leftovers = Dir.children(File.dirname(path)).reject { |f| f == "projects.json" }
|
||||
assert_empty leftovers, "atomic write left temp files: #{leftovers}"
|
||||
end
|
||||
end
|
||||
|
||||
def test_default_path_honors_cc_os_home_override
|
||||
path = Backlog::ProjectIndex.default_path(env: { "CC_OS_HOME" => "/tmp/x", "HOME" => "/home/y" })
|
||||
assert_equal "/tmp/x/projects.json", path
|
||||
end
|
||||
|
||||
def test_default_path_falls_back_to_home
|
||||
path = Backlog::ProjectIndex.default_path(env: { "HOME" => "/home/y" })
|
||||
assert_equal "/home/y/.cc-os/projects.json", path
|
||||
end
|
||||
end
|
||||
|
||||
class ProjectsCliContractTest < Minitest::Test
|
||||
BIN = File.expand_path("../bin/os-backlog", __dir__)
|
||||
|
||||
# config-write must upsert the index (redirected via CC_OS_HOME so the
|
||||
# real ~/.cc-os is never touched), and `projects` must always emit a
|
||||
# top-level "projects" key — even when the index is empty.
|
||||
def test_config_write_upserts_index_and_projects_lists_it
|
||||
Dir.mktmpdir do |home|
|
||||
Dir.mktmpdir do |repo|
|
||||
env = "CC_OS_HOME=#{home} HOME=#{home}"
|
||||
out = Dir.chdir(repo) { `#{env} ruby #{BIN} projects 2>/dev/null` }
|
||||
assert_equal({ "projects" => {} }, JSON.parse(out))
|
||||
|
||||
Dir.chdir(repo) { `#{env} ruby #{BIN} config-write planka:demo 2>/dev/null` }
|
||||
out = Dir.chdir(repo) { `#{env} ruby #{BIN} projects 2>/dev/null` }
|
||||
projects = JSON.parse(out)["projects"]
|
||||
assert_equal 1, projects.size
|
||||
row = projects.values.first
|
||||
assert_equal "planka:demo", row["tracker"]
|
||||
assert_equal File.basename(File.realpath(repo)), row["name"]
|
||||
|
||||
filtered = Dir.chdir(repo) { `#{env} ruby #{BIN} projects no-such-name 2>/dev/null` }
|
||||
assert_equal({}, JSON.parse(filtered)["projects"])
|
||||
|
||||
by_tracker = Dir.chdir(repo) { `#{env} ruby #{BIN} projects planka:demo 2>/dev/null` }
|
||||
assert_equal 1, JSON.parse(by_tracker)["projects"].size
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Umbrella repos: config-write from a subdirectory writes the config to
|
||||
# that subdirectory, so the index row must be keyed by the SUBDIRECTORY
|
||||
# (the project the config describes), never the enclosing git toplevel —
|
||||
# otherwise every subproject masquerades as the umbrella repo.
|
||||
def test_config_write_in_umbrella_subdir_indexes_the_subdir
|
||||
Dir.mktmpdir do |home|
|
||||
Dir.mktmpdir do |repo|
|
||||
`git -C #{repo} init -q 2>/dev/null`
|
||||
subdir = File.join(repo, "child-project")
|
||||
FileUtils.mkdir_p(subdir)
|
||||
env = "CC_OS_HOME=#{home} HOME=#{home}"
|
||||
|
||||
Dir.chdir(subdir) { `#{env} ruby #{BIN} config-write planka:child 2>/dev/null` }
|
||||
|
||||
projects = Dir.chdir(subdir) { JSON.parse(`#{env} ruby #{BIN} projects 2>/dev/null`)["projects"] }
|
||||
assert_equal [File.realpath(subdir)], projects.keys
|
||||
assert_equal "child-project", projects.values.first["name"]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -90,4 +90,32 @@ class ResolverTest < Minitest::Test
|
|||
|
||||
assert_equal "use llf-schema", resolver.resolve_string
|
||||
end
|
||||
|
||||
def test_unconfigured_servers_repo_uses_board_named_after_repo
|
||||
resolver = Backlog::Resolver.new(
|
||||
repo_path: "/home/jared/servers/proxmox-ubuntu",
|
||||
config_contents: nil,
|
||||
boards: %w[proxmox-ubuntu]
|
||||
)
|
||||
|
||||
assert_equal "use proxmox-ubuntu", resolver.resolve_string
|
||||
end
|
||||
|
||||
def test_planka_tracker_key_resolves_to_its_board
|
||||
resolver = Backlog::Resolver.new(
|
||||
repo_path: "/somewhere/unrecognized/repo",
|
||||
config_contents: "tracker=planka:my-board\n",
|
||||
boards: %w[my-board]
|
||||
)
|
||||
|
||||
assert_equal "use my-board", resolver.resolve_string
|
||||
end
|
||||
|
||||
def test_project_for_returns_dev_clients_servers_or_nil
|
||||
assert_equal "Dev", Backlog::Resolver.project_for("/home/jared/dev/some-repo")
|
||||
assert_equal "Clients", Backlog::Resolver.project_for("/home/jared/clients/acme-co")
|
||||
assert_equal "Servers", Backlog::Resolver.project_for("/home/jared/servers/ovh-prod")
|
||||
assert_equal "Servers", Backlog::Resolver.project_for("/home/jared/servers")
|
||||
assert_nil Backlog::Resolver.project_for("/home/jared/scratch/some-repo")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ module BacklogTestHelpers
|
|||
class FakePlankaClient
|
||||
attr_reader :projects, :boards, :lists, :labels, :cards, :comments
|
||||
|
||||
def initialize(reject_colors: [])
|
||||
def initialize(reject_colors: [], reject_colors_with_400: [])
|
||||
@next_id = 0
|
||||
@projects_store = []
|
||||
@boards_store = []
|
||||
|
|
@ -24,7 +24,8 @@ module BacklogTestHelpers
|
|||
@projects = ProjectsResource.new(self)
|
||||
@boards = BoardsResource.new(self)
|
||||
@lists = ListsResource.new(self)
|
||||
@labels = LabelsResource.new(self, reject_colors: reject_colors)
|
||||
@labels = LabelsResource.new(self, reject_colors: reject_colors,
|
||||
reject_colors_with_400: reject_colors_with_400)
|
||||
@cards = CardsResource.new(self)
|
||||
@comments = CommentsResource.new(self)
|
||||
end
|
||||
|
|
@ -111,6 +112,11 @@ module BacklogTestHelpers
|
|||
|
||||
def get(id)
|
||||
board = @client.boards_store.find { |b| b.id == id }
|
||||
unless board
|
||||
raise Planka::NotFoundError.new(status: 404,
|
||||
body: { "message" => "board #{id} not found" })
|
||||
end
|
||||
|
||||
Planka::Types::BoardDetail.new(
|
||||
board: board,
|
||||
lists: @client.lists.list(id),
|
||||
|
|
@ -174,9 +180,10 @@ module BacklogTestHelpers
|
|||
end
|
||||
|
||||
class LabelsResource
|
||||
def initialize(client, reject_colors: [])
|
||||
def initialize(client, reject_colors: [], reject_colors_with_400: [])
|
||||
@client = client
|
||||
@reject_colors = reject_colors
|
||||
@reject_colors_with_400 = reject_colors_with_400
|
||||
end
|
||||
|
||||
def list(board_id)
|
||||
|
|
@ -184,6 +191,11 @@ module BacklogTestHelpers
|
|||
end
|
||||
|
||||
def create(board_id, attrs)
|
||||
if @reject_colors_with_400.include?(attrs[:color])
|
||||
# Planka 2.x rejects unknown color names as E_MISSING_OR_INVALID_PARAMS
|
||||
raise Planka::BadRequestError.new(status: 400,
|
||||
body: { "message" => "color #{attrs[:color]} rejected" })
|
||||
end
|
||||
if @reject_colors.include?(attrs[:color])
|
||||
raise Planka::ValidationError.new(status: 422,
|
||||
body: { "message" => "color #{attrs[:color]} rejected" })
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
require_relative "test_helper"
|
||||
|
||||
class TriageCheckTest < Minitest::Test
|
||||
def test_card_missing_both_labels_needs_triage
|
||||
assert Backlog::TriageCheck.needs_triage?([])
|
||||
end
|
||||
|
||||
def test_card_missing_priority_label_needs_triage
|
||||
assert Backlog::TriageCheck.needs_triage?(["semi"])
|
||||
end
|
||||
|
||||
def test_card_missing_autonomy_label_needs_triage
|
||||
assert Backlog::TriageCheck.needs_triage?(["P1"])
|
||||
end
|
||||
|
||||
def test_fully_labeled_card_does_not_need_triage
|
||||
refute Backlog::TriageCheck.needs_triage?(%w[P1 semi])
|
||||
end
|
||||
|
||||
def test_note_for_no_unlabeled_cards_is_nil
|
||||
assert_nil Backlog::TriageCheck.note_for("cc-os", [%w[P1 semi], %w[P0 hitl]])
|
||||
end
|
||||
|
||||
def test_note_for_counts_only_unlabeled_cards
|
||||
labels = [%w[P1 semi], [], ["P2"]]
|
||||
note = Backlog::TriageCheck.note_for("cc-os", labels)
|
||||
|
||||
refute_nil note
|
||||
assert_includes note, "2 unlabeled Backlog cards on board cc-os"
|
||||
assert_includes note, "card-triage"
|
||||
assert_includes note, "haiku"
|
||||
assert_includes note, "background"
|
||||
end
|
||||
|
||||
def test_note_for_singular_card_uses_singular_wording
|
||||
note = Backlog::TriageCheck.note_for("cc-os", [[]])
|
||||
|
||||
assert_includes note, "1 unlabeled Backlog card on board cc-os"
|
||||
refute_includes note, "1 unlabeled Backlog cards"
|
||||
end
|
||||
|
||||
def test_board_name_for_subdir_config_overrides_root_config
|
||||
name = Backlog::TriageCheck.board_name_for(
|
||||
cwd: "/home/jared/servers/proxmox", root: "/home/jared/servers",
|
||||
cwd_config: "tracker=planka:proxmox-board\n", root_config: "board=servers\n"
|
||||
)
|
||||
|
||||
assert_equal "proxmox-board", name
|
||||
end
|
||||
|
||||
def test_board_name_for_falls_back_to_root_config
|
||||
name = Backlog::TriageCheck.board_name_for(
|
||||
cwd: "/home/jared/dev/cc-os/plugins", root: "/home/jared/dev/cc-os",
|
||||
cwd_config: nil, root_config: "tracker=planka:cc-os\n"
|
||||
)
|
||||
|
||||
assert_equal "cc-os", name
|
||||
end
|
||||
|
||||
def test_board_name_for_subdir_config_without_board_names_the_subdir
|
||||
name = Backlog::TriageCheck.board_name_for(
|
||||
cwd: "/home/jared/servers/proxmox", root: "/home/jared/servers",
|
||||
cwd_config: "version=3\n", root_config: nil
|
||||
)
|
||||
|
||||
assert_equal "proxmox", name
|
||||
end
|
||||
|
||||
def test_board_name_for_no_config_uses_heuristic_repo_basename
|
||||
name = Backlog::TriageCheck.board_name_for(
|
||||
cwd: "/home/jared/dev/cc-os", root: "/home/jared/dev/cc-os",
|
||||
cwd_config: nil, root_config: nil
|
||||
)
|
||||
|
||||
assert_equal "cc-os", name
|
||||
end
|
||||
|
||||
def test_board_name_for_unrecognized_path_without_config_is_nil
|
||||
name = Backlog::TriageCheck.board_name_for(
|
||||
cwd: "/home/jared/scratch/x", root: "/home/jared/scratch/x",
|
||||
cwd_config: nil, root_config: nil
|
||||
)
|
||||
|
||||
assert_nil name
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
require_relative "test_helper"
|
||||
require "tmpdir"
|
||||
require "json"
|
||||
|
||||
class WakeupTest < Minitest::Test
|
||||
W = Backlog::Wakeup
|
||||
|
||||
def test_tier_mapping
|
||||
assert_equal :afk_ready, W.tier("afk-ready")
|
||||
assert_equal :afk_ready, W.tier("bug, afk-ready")
|
||||
assert_equal :semi, W.tier("semi")
|
||||
assert_equal :notify, W.tier("hitl")
|
||||
assert_equal :notify, W.tier("")
|
||||
assert_equal :notify, W.tier(nil)
|
||||
end
|
||||
|
||||
def test_eligible_projects_requires_git_tracker_and_optin
|
||||
projects = {
|
||||
"/p/planka" => { "name" => "planka-proj", "tracker" => "planka:board" },
|
||||
"/p/forgejo-in" => { "name" => "fin", "tracker" => "forgejo:jared/fin" },
|
||||
"/p/forgejo-out" => { "name" => "fout", "tracker" => "forgejo:jared/fout" },
|
||||
"/p/github-in" => { "name" => "gin", "tracker" => "github:jared/gin" }
|
||||
}
|
||||
reader = ->(path) { path.end_with?("-in") ? "tracker=x\nwakeup=true\n" : "tracker=x\n" }
|
||||
rows = W.eligible_projects(projects, config_reader: reader)
|
||||
assert_equal %w[/p/forgejo-in /p/github-in], rows.map { |r| r[:path] }
|
||||
assert_equal "jared/fin", rows.first[:repo_slug]
|
||||
assert_equal "forgejo", rows.first[:tracker_kind]
|
||||
end
|
||||
|
||||
def test_new_issues_baselines_on_nil_last_seen
|
||||
issues = [{ "index" => "5" }, { "index" => "3" }]
|
||||
assert_equal [], W.new_issues(issues, nil)
|
||||
assert_equal [{ "index" => "5" }], W.new_issues(issues, 3)
|
||||
assert_equal 5, W.highest_index(issues)
|
||||
assert_equal 9, W.highest_index(issues, 9)
|
||||
end
|
||||
|
||||
def test_tmux_command_follows_adr_0036_naming
|
||||
cmd = W.tmux_command(project: "llf-schema", path: "/p/llf", issue_index: "12", prompt: "hi")
|
||||
assert_equal %w[tmux new-session -d -s cc-llf-schema-issue12 -c /p/llf claude hi], cmd
|
||||
end
|
||||
|
||||
def test_session_prompt_carries_tier_and_body
|
||||
issue = { "index" => "7", "title" => "T", "body" => "Body\n**Discoverer:** x" }
|
||||
semi = W.session_prompt(issue: issue, tier: :semi, url: "u")
|
||||
assert_includes semi, "TRIAGE ONLY"
|
||||
assert_includes semi, "**Discoverer:** x"
|
||||
afk = W.session_prompt(issue: issue, tier: :afk_ready, url: "u")
|
||||
assert_includes afk, "afk-ready"
|
||||
end
|
||||
|
||||
def test_state_reads_missing_as_empty_and_records_atomically
|
||||
Dir.mktmpdir do |dir|
|
||||
state = W::State.new(path: File.join(dir, "wakeup-state.json"))
|
||||
assert_nil state.last_seen("jared/x")
|
||||
state.record("jared/x", 7)
|
||||
assert_equal 7, W::State.new(path: File.join(dir, "wakeup-state.json")).last_seen("jared/x")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "os-context",
|
||||
"version": "0.1.0",
|
||||
"description": "Prompt-composer SessionStart plugin: concatenates prompts/session-start/*.md (currently session-orchestration rules — delegation threshold, explicit model routing on Agent spawns) into one additionalContext block, injected at session start and after compaction."
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Enumerates production session transcripts modified since a given date,
|
||||
# reduces each to a delegation fact-sheet (audit/bin/extract), and writes a
|
||||
# run directory under the audit state dir (~/.local/state/os-orchestration/audit):
|
||||
# run directory under the audit state dir (~/.local/state/os-context/audit):
|
||||
#
|
||||
# run-<date>/stats.tsv one row per audited session (deterministic columns)
|
||||
# run-<date>/flags.md heuristic missed-delegation candidates, per session,
|
||||
|
|
@ -31,7 +31,7 @@ require "optparse"
|
|||
load File.expand_path("extract", __dir__)
|
||||
|
||||
module OrchAudit
|
||||
DEFAULT_STATE_DIR = File.expand_path("~/.local/state/os-orchestration/audit")
|
||||
DEFAULT_STATE_DIR = File.expand_path("~/.local/state/os-context/audit")
|
||||
DEFAULT_PROJECTS_DIR = File.expand_path("~/.claude/projects")
|
||||
|
||||
class TranscriptSet
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
# os-orchestration eval — E1–E3, E5 (session-orchestration behavior)
|
||||
# os-context eval — E1–E3, E5 (session-orchestration behavior)
|
||||
|
||||
_Created: 2026-07-06. Scenarios sourced from the verified misses in
|
||||
`docs/orchestration-audit/2026-07-06-findings.md` (clusters 1–3), per the
|
||||
IRL-feedback-loop methodology. E4 (batch planning) deliberately deferred._
|
||||
|
||||
Measures whether an orchestrating session follows the shipped `ORCHESTRATION.md`
|
||||
Measures whether an orchestrating session follows the shipped `prompts/session-start/10-orchestration.md`
|
||||
(the 2026-07-06 trigger-conditioned wording) mid-task: explicit `model:` per spawn,
|
||||
resolved-model verification, delegation thresholds, and not re-covering its own
|
||||
ground. This validates the wording fixes the way Eval B/C validated os-adr's —
|
||||
|
|
@ -18,7 +18,7 @@ was wrong).**
|
|||
## Run mode (the only valid one)
|
||||
|
||||
Headless: fresh `claude -p` per rep, cwd = a sandbox copy of `fixture/project/`, so
|
||||
the global os-orchestration SessionStart hook injects ORCHESTRATION.md for real.
|
||||
the global os-context SessionStart hook injects the orchestration prompt for real.
|
||||
In-session subagents are invalid here (they inherit the parent session). The behavior
|
||||
under test is mid-session, so unlike Eval B there is no SessionStart trigger to
|
||||
protect — but the injection channel is the SessionStart hook, so the constraint is
|
||||
|
|
@ -104,7 +104,7 @@ name field); metadata gains `total_spawns` and `max_rounds`.
|
|||
## Conformance dry-run (done at design time, 2026-07-06)
|
||||
|
||||
Per the ladder anti-pattern list, each cell was checked against "would a model
|
||||
perfectly following the shipped ORCHESTRATION.md pass?" — yes on all six (E1P:
|
||||
perfectly following the shipped prompts/session-start/10-orchestration.md pass?" — yes on all six (E1P:
|
||||
delegate+sonnet+flag; E1N: no false claim; E2P: fan out with explicit models; E2N/E3N:
|
||||
do directly; E3P: delegate the log review after at most an orienting look) — and
|
||||
"would an always-delegate model pass positives while failing negatives?" — it fails
|
||||
|
|
@ -128,7 +128,7 @@ done; wait
|
|||
|
||||
- Sandboxes must live under a dot-free path (/tmp/orch-eval): the checker flattens
|
||||
the sandbox realpath to find the project transcript dir.
|
||||
- Run `cc-os/bin/refresh-plugins` after ANY ORCHESTRATION.md edit, before any grid.
|
||||
- Run `cc-os/bin/refresh-plugins` after ANY prompts/session-start/10-orchestration.md edit, before any grid.
|
||||
- TSV columns: scenario, model, rep, verdict, axes, info. The TSV is the truth —
|
||||
verify from it, not from prose reports.
|
||||
- Judge (E1 language axes only): frozen rubric in `judge-rubric.md`; stub with
|
||||
|
|
@ -141,7 +141,7 @@ The first live E1P rep (sonnet, counted: FAIL, `B:fail-mechanical`) falsified th
|
|||
premise that the orchestrator can see `resolvedModel` in the launch stub — the model-visible
|
||||
launch result carries no model info, so the first shipped wording ("compare the resolved model
|
||||
reported in the launch result") was unactionable and E1 was unwinnable by a compliant model.
|
||||
Fixed in ORCHESTRATION.md wording ONLY (subagent model self-report mechanism — subagents know
|
||||
Fixed in prompts/session-start/10-orchestration.md wording ONLY (subagent model self-report mechanism — subagents know
|
||||
their exact model ID; verified live). Scenarios, checker, and rubric were NOT touched.
|
||||
|
||||
## Fixture
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Deterministic-first checker for the os-orchestration eval (E1-E3, E5).
|
||||
# Deterministic-first checker for the os-context eval (E1-E3, E5).
|
||||
#
|
||||
# Usage: check <scenario> <sandbox> [--tsv <model> [<rep>]]
|
||||
#
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
# Headless runner for the os-orchestration eval — the ONLY valid execution mode
|
||||
# Headless runner for the os-context eval — the ONLY valid execution mode
|
||||
# (mid-session orchestration behavior under the real global SessionStart-injected
|
||||
# ORCHESTRATION.md; fresh `claude -p` per rep with cwd = sandbox).
|
||||
# prompts/session-start/10-orchestration.md; fresh `claude -p` per rep with cwd = sandbox).
|
||||
#
|
||||
# Usage: run <scenario> <model> <workdir> [--reps N] [--results FILE]
|
||||
#
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Model-free self-test for the os-orchestration eval checker. Fabricates
|
||||
# Model-free self-test for the os-context eval checker. Fabricates
|
||||
# session-format transcripts and asserts verdicts in BOTH directions per
|
||||
# scenario, including at least one SHIPPED-INSTRUCTION-COMPLIANT transcript
|
||||
# per positive scenario (the Eval C conformance lesson: self-tests that only
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Frozen judge rubrics — os-orchestration eval (E1 language axes only)
|
||||
# Frozen judge rubrics — os-context eval (E1 language axes only)
|
||||
|
||||
Used only when the mechanical scan is inconclusive. FROZEN during any wording loop.
|
||||
The judge receives one rubric section plus the transcript text, and must answer with
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
|
Can't render this file because it has a wrong number of fields in line 2.
|
|
Can't render this file because it has a wrong number of fields in line 2.
|
|
Can't render this file because it has a wrong number of fields in line 6.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue