diff --git a/CHANGELOG.md b/CHANGELOG.md index a453e15..54544d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,24 @@ what a plugin user sees. ## [Unreleased] +### Added +- **`paad:kb-brain` — repository-native working knowledge for humans and + agents.** Initializes a `kb-brain/` tree (separate from stable `docs/`), + routes durable notes to the right section, and gives every managed task a + focused workspace with index-first retrieval. Sub-agents may append findings, + questions, failures, conflicts, and handoffs; only the lead or human owner + changes scope, assignments, confirmed decisions, or closure. Closed + workspaces are sealed (`SEAL.json`) and corrected only via amendments. + Ships templates plus stdlib `scripts/kb_brain.py` (`init` / `start` / `new` / + `index` / `check` / `close` / `amend`) and Make targets. Does not wire + automatic KBB behaviour into existing skills. +- **`paad:brief-ruminate` — brief-to-milestone-spec expansion.** Reads a + human-owned brief, repository evidence, and KB-Brain context, then expands + one milestone into a candidate specification at `review-needed`. Stops before + approval, planning, or implementation; only a human may mark + `approved-spec`. Does not auto-run `pushback`, `alignment`, or + `agentic-review`. + ## [1.24.1] — 2026-08-01 ### Fixed diff --git a/Makefile b/Makefile index 44ddaa5..c75775c 100644 --- a/Makefile +++ b/Makefile @@ -2,14 +2,17 @@ SKILLS_DIR := plugins/paad/skills SKILL_DIRS := $(wildcard $(SKILLS_DIR)/*) SKILL_NAMES := $(notdir $(SKILL_DIRS)) -.PHONY: help test validate check-versions check-skill-versions check-digraphs check-help check-readme check-frontmatter check-references check-dispatch-sites check-announce check-export-current bump-version export release tag +.PHONY: help test validate check-versions check-skill-versions check-digraphs check-help check-readme check-frontmatter check-references check-dispatch-sites check-announce check-export-current check-kb-brain bump-version export release tag help: ## Show this help @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-22s %s\n", $$1, $$2}' -test: validate check-versions check-skill-versions check-digraphs check-help check-readme check-frontmatter check-references check-dispatch-sites check-announce check-export-current ## Run all checks +test: validate check-versions check-skill-versions check-digraphs check-help check-readme check-frontmatter check-references check-dispatch-sites check-announce check-export-current check-kb-brain ## Run all checks @echo "All checks passed." +check-kb-brain: ## Run KB-Brain tooling unit tests + @python3 -m unittest discover -s plugins/paad/skills/kb-brain/tests -p 'test_*.py' -b + validate: ## Validate marketplace and all plugins @claude plugin validate . @for dir in plugins/*/; do \ diff --git a/README.md b/README.md index a5bcb71..414ca26 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ Not using Claude Code? PAAD also supports **Cursor**, **Kiro**, and | `/agentic-a11y [path]` | Accessibility audit against WCAG 2.2 AA, by disability category | | `/vibe [task]` | Small fixes, TDD guardrails still on | | `/makefile` | Creates or updates a project `Makefile` | +| `/kb-brain [action...]` | Repository-native working knowledge base for humans and agents | +| `/brief-ruminate [brief] [milestone]` | Expand one brief milestone into a candidate spec for human review | | `/paad:help [skill-name]` | Lists the skills, or explains one | | `/agentic-dedup [scope]` | Finds duplicated *meaning*, not duplicated text — experimental | | `/rethink [topic]` | Checks whether the premises under a recommendation hold — experimental | @@ -663,6 +665,52 @@ automatically and never modifies an existing target without asking first. Note: this skill might be removed in the future, or moved to a different namespace. Let me know if you rely on it. +#### `/kb-brain [action...]` + +Agents forget. Sessions end. The next run invents a different answer to a +question you already settled. `kb-brain` keeps a repository-native working +knowledge base under `kb-brain/` — mutable context, decisions, failures, +improvements, debt, and focused task workspaces — while stable architecture +stays in `docs/`. + +* **Arguments:** `/kb-brain init [level]`, `/kb-brain start [level]`, + `/kb-brain record
[title]`, `/kb-brain status [task-id]`, + `/kb-brain index`, `/kb-brain check`, `/kb-brain close [task-id]`, + `/kb-brain amend `, `/kb-brain route ` +* **Levels:** `minimal`, `standard` (default), `strict` — raise freely; lowering + below the repository default needs explicit human approval +* **Permissions:** sub-agents append findings, questions, failures, conflicts, + and handoffs; only the lead or human owner changes scope, assignments, + confirmed decisions, or closes a workspace +* **Closure:** sealed with `SEAL.json`; later corrections are amendments, not + edits to history +* **Tooling:** copies `scripts/kb_brain.py` and Make targets into the target + repo on init — no bulk ingest of existing `docs/` + +Does not automatically wire into other PAAD skills. Invoke it explicitly, or +point agents at it from `AGENTS.md`. + +#### `/brief-ruminate [brief-path] [milestone-id]` + +A human writes the project brief. `brief-ruminate` expands **one** milestone +into a repository-grounded candidate specification for human review. It stops +at `review-needed`. It does not approve the spec, plan the work, or run +`pushback` / `alignment` / `agentic-review` for you. + +* **Arguments:** `/brief-ruminate `, + `/brief-ruminate `, + `/brief-ruminate next `, `/brief-ruminate status ` +* **Storage:** `kb-brain/briefs//` for the brief and milestones; + `kb-brain/specs//` for candidate specs +* **Selection:** without a milestone id, recommends the next valuable and + sufficiently unblocked milestone — not merely the first one +* **Stop conditions:** missing controlling decisions, invented product + behaviour, unresolved ownership overlap, or an oversized milestone without an + accepted split — record the blocker and leave the work unapproved + +Requires `kb-brain/` to be present. Only a human may mark a spec +`approved-spec`. + #### `/vibe [task description]` Speed without recklessness. `vibe` supports smaller fixes and quick changes diff --git a/docs/plans/2026-08-04-kb-brain-brief-ruminate-handoff.md b/docs/plans/2026-08-04-kb-brain-brief-ruminate-handoff.md new file mode 100644 index 0000000..eed85eb --- /dev/null +++ b/docs/plans/2026-08-04-kb-brain-brief-ruminate-handoff.md @@ -0,0 +1,1040 @@ +# PAAD KB-Brain and Brief Rumination +## Implementation Handoff for a Coding Agent + +**Prepared:** 4 August 2026 +**Target repository:** `Ovid/paad` +**Delivery goal:** Produce a focused pull request adding two standalone PAAD skills without changing the behaviour of existing skills. + +--- + +## 1. Executive summary + +Implement two related but independently callable capabilities: + +1. **`kb-brain`**: a repository-native, human-readable working knowledge base shared by humans, lead agents, and sub-agents. It records context, decisions, failures, working memory, technical debt, improvements, questions, and durable lessons. It also provides a focused task workspace that reduces sub-agent drift without requiring agents to load a long shared journal. +2. **`brief-ruminate`**: a brief-to-milestone-spec workflow. A human owns and writes the high-level project brief. PAAD reads the brief, repository, and KB-Brain context, then expands one milestone into a reviewable working specification. It does not approve the specification, plan implementation, or modify existing PAAD review skills. + +The features must remain isolated. Do not add automatic KBB behaviour to `pushback`, `alignment`, `agentic-review`, or any other existing skill. Users and agents invoke the new skills explicitly or direct agents to use them through their repository `AGENTS.md`. + +--- + +## 2. Source constraints and existing PAAD conventions + +The PAAD repository is a Claude Code plugin marketplace whose canonical skill sources live under: + +```text +plugins/paad/skills//SKILL.md +``` + +Follow the current repository conventions documented in `CLAUDE.md`: + +- skill folder names become `/paad:` commands; +- each skill requires YAML frontmatter with `name` and `description`; +- positional arguments should be intuitive; +- every skill except `help` needs a complete Graphviz `dot` decision-flow diagram; +- update the root README and `/paad:help` documentation; +- keep plugin and marketplace versions synchronized; +- run the repository's complete test and plugin-validation suite; +- use the repository's existing generation process for platform mirrors rather than independently editing generated copies. + +The supplied KB prototype establishes this source-of-truth split: + +- `docs/`: stable architecture, protocol, and accepted documentation source of truth. +- `kb-brain/`: mutable working knowledge, active context, in-flight decisions, gaps, debt, and agent memory. + +Do not duplicate stable architecture documentation in `kb-brain/`. Working notes that refine or challenge stable documentation belong in `kb-brain/architecture/` and must link back to the stable source. Never invent answers for entries in `open-questions/`. + +--- + +## 3. Global product rules + +These requirements apply to both new skills. + +### 3.1 Human authority + +Agents may autonomously record observations, evidence, failures, questions, proposed improvements, technical debt, handoffs, and working context. + +Agents must not silently convert any of the following into accepted facts: + +- an inferred product requirement; +- an unapproved architectural direction; +- an answer to an unresolved question; +- a suggested improvement promoted to roadmap scope; +- a candidate specification marked as approved. + +Confirmed decisions require either explicit human confirmation or unambiguous existing repository evidence. Record the owner and evidence. + +### 3.2 Notice broadly, work narrowly + +An agent may notice useful work outside its current scope and write an atomic improvement or technical-debt record. It must not implement that work unless the active task includes it. + +### 3.3 Index-first retrieval + +Agents must not read the entire KB by default. The normal read order is: + +1. `kb-brain/work/ACTIVE.md` +2. the selected workspace's `TASK.md` +3. `INDEX.md` +4. `CONTEXT.md` +5. the agent's relevant assignment +6. only linked or relevant atomic records + +This rule exists to avoid context rot and unnecessary token use. + +### 3.4 No unsolicited bulk ingress + +Bulk ingress from `docs/` or other existing documentation is a dedicated operation. Do not dump architecture documents into the KB during initialization or ordinary work. + +### 3.5 Existing skills remain unchanged + +Do not modify existing PAAD skills to write to or read from KBB automatically. Documentation may mention that the new skills can be used alongside existing workflows, but their behaviour and execution flows must remain unchanged. + +--- + +# Part A — `kb-brain` + +## 4. Skill purpose and activation + +Create a standalone `/paad:kb-brain` skill that: + +- initializes the KBB structure in a target repository; +- routes human and agent knowledge to the correct section; +- starts, updates, closes, indexes, validates, and amends task workspaces; +- supplies reusable templates for humans and agents; +- enforces the authority, lifecycle, and immutability rules in this specification; +- supports sub-agent alignment through focused shared task files. + +Recommended frontmatter description: + +```yaml +--- +name: kb-brain +description: Maintain a repository-native working knowledge base for humans and coding agents. Use when initializing kb-brain, starting or closing task workspaces, recording durable context, decisions, findings, failures, questions, improvements, technical debt, handoffs, or amendments, and when coordinating lead agents and sub-agents through shared project memory. +--- +``` + +## 5. Arguments and callable operations + +Use one positional action followed by intuitive positional values. Avoid a large flag surface. + +```text +/paad:kb-brain init [minimal|standard|strict] +/paad:kb-brain start [minimal|standard|strict] +/paad:kb-brain record
[title] +/paad:kb-brain status [task-id] +/paad:kb-brain index [task-id] +/paad:kb-brain check [path] +/paad:kb-brain close [task-id] +/paad:kb-brain amend +/paad:kb-brain route +``` + +Natural-language invocations must work as well, for example: + +- “Use kb-brain to record the parser failure.” +- “Start a strict KBB workspace for the authentication migration.” +- “Where should this decision be stored?” +- “Close and seal the current KBB workspace.” + +If no action is supplied, infer the requested operation from conversation context. When it cannot be inferred safely, display the operation summary and ask for one choice. + +## 6. Project-local KBB structure + +Initialization creates the following project-local structure. Do not create empty atomic records. + +```text +kb-brain/ +├── README.md +├── INDEX.md +├── decisions/ +├── architecture/ +├── domains/ +├── runbooks/ +├── gotchas/ +├── briefs/ +├── specs/ +├── plans/ +├── reviews/ +├── learnings/ +├── open-questions/ +├── agents/ +├── improvements/ +├── tech-debt/ +│ ├── LEDGER.md +│ └── closed/ +├── templates/ +└── work/ + ├── ACTIVE.md + ├── active/ + └── closed/ +``` + +`README.md` should retain the supplied section descriptions and add `briefs/`, templates, workspace behaviour, closure, and amendment rules. + +### 6.1 Permanent section meanings + +- `decisions/` — accepted ADR-style decisions with owner and evidence. +- `architecture/` — mutable notes that refine, question, or compare against stable architecture in `docs/`. +- `domains/` — product and business domain knowledge. +- `runbooks/` — recurring procedures that are useful but not yet stable documentation. +- `gotchas/` — sharp edges, false starts, and reliable warnings. +- `briefs/` — human-owned briefs and atomic milestone records. +- `specs/` — active working specifications, including candidate milestone expansions. +- `plans/` — implementation plans tied to approved specs. +- `reviews/` — review outputs worth preserving beyond their originating session. +- `learnings/` — post-hoc lessons and retrospectives. +- `open-questions/` — unresolved questions; agents may add evidence but not invent answers. +- `agents/` — repository-specific agent behaviour and coordination notes. +- `improvements/` — one atomic file per noticed gap or opportunity. +- `tech-debt/` — open debt records and `LEDGER.md`; resolved entries move to `tech-debt/closed/`. + +## 7. Always-present task workspace + +Every PAAD-managed task receives a KBB workspace, regardless of size. The repository `AGENTS.md` selects the default detail level. A task may raise its level, but lowering it below the repository default requires explicit human approval. + +### 7.1 Workspace levels + +**`minimal`** + +Required: + +- task scope and status; +- compact context; +- ownership; +- final handoff; +- durable findings selected for promotion. + +**`standard` — recommended default** + +Adds: + +- assignments; +- findings and evidence; +- questions; +- failures and abandoned approaches; +- confirmed decisions; +- conflicts; +- handoffs; +- promotion tracking. + +**`strict`** + +Adds: + +- explicit assumptions; +- dependency records; +- decision ownership; +- scope-change records; +- mandatory evidence references; +- detailed conflict handling; +- completion and promotion checks. + +All levels use focused files and atomic record directories. Do not implement a single append-only workspace journal. + +### 7.2 Active workspace structure + +```text +kb-brain/work/active// +├── TASK.md +├── INDEX.md +├── CONTEXT.md +├── ASSIGNMENTS.md +├── PROMOTION.md +├── decisions/ +├── findings/ +├── questions/ +├── failures/ +├── conflicts/ +├── handoffs/ +├── assumptions/ # mandatory only in strict +├── dependencies/ # mandatory only in strict +└── scope-changes/ # mandatory only in strict +``` + +The task ID should be stable, human-readable, and collision-resistant. Recommended format: + +```text +YYYY-MM-DD- +``` + +If a second task would use the same ID, append `-2`, `-3`, and so on. + +### 7.3 File responsibilities + +**`TASK.md`** is the authoritative task card. It contains: + +- objective; +- scope; +- non-goals; +- completion criteria; +- workspace level; +- lead owner; +- lifecycle status; +- current focus; +- blockers; +- controlling brief, spec, issue, or plan links. + +**`INDEX.md`** is generated. It lists all atomic records grouped by type, status, owner, and relationship. It must include amendment markers for closed workspaces. + +**`CONTEXT.md`** is compact shared working memory needed by most participants. Keep it brief. Move detailed material to atomic records and link it. + +**`ASSIGNMENTS.md`** is lead-controlled and records each human or agent assignment, boundary, dependencies, and status. + +**`PROMOTION.md`** tracks durable information that must move into permanent KBB sections or stable `docs/` before closure. + +## 8. Agent permissions and sub-agent alignment + +Use the agreed hybrid model. + +### 8.1 Sub-agents may append + +Sub-agents may autonomously create atomic records for: + +- findings and evidence; +- questions; +- failures and abandoned approaches; +- conflicts; +- handoffs; +- suggested improvements; +- technical debt. + +### 8.2 Lead-only changes + +Only the lead agent or human task owner may change: + +- task scope or non-goals; +- task lifecycle status; +- assignments and ownership; +- confirmed decisions; +- blocker disposition; +- conflict resolution status; +- workspace closure and sealing. + +This is primarily a governed workflow, not a security sandbox. The skill instructions and validation should make unauthorized changes visible. Where practical, require `role: lead` metadata for privileged record changes and detect invalid ownership transitions during `kb-check`. + +### 8.3 Conflict protocol + +Conflicting findings are recorded, never overwritten. + +When a conflict pertains to current work, address it during the current session. The lead records one of three outcomes: + +- `resolved` — evidence or a human decision establishes the controlling conclusion; +- `deferred` — the conflict is real but safely outside the current bounded task; record why and where it will be handled; +- `blocked` — work cannot safely continue; add the blocker to `TASK.md` so it appears in `ACTIVE.md`. + +Unrelated conflicts remain recorded and do not block the current assignment. + +## 9. Repository-level active work summary + +Generate: + +```text +kb-brain/work/ACTIVE.md +``` + +This file is an inexpensive repository-wide overview. It contains only: + +- active task ID and link; +- status; +- workspace level; +- lead/owner; +- objective; +- current focus; +- blockers. + +Do not list conflicts in `ACTIVE.md`. Conflicts are reviewed inside the relevant workspace when they affect the current work. + +`ACTIVE.md` must be generated from task workspace metadata. Do not permit manually authored narrative that can drift. + +## 10. Atomic records and templates + +Provide templates that can be used by both humans and agents. Store distributable templates with the skill and copy project-local versions into `kb-brain/templates/` during initialization. + +### 10.1 Common frontmatter + +Use YAML frontmatter for atomic records: + +```yaml +--- +id: F-001 +type: finding +status: open +author: agent-parser-review +owner: task-lead +created: 2026-08-04 +updated: 2026-08-04 +related: + - questions/Q-002-import-order.md + - src/parser/imports.ts +--- +``` + +Required common fields: + +- `id` +- `type` +- `status` +- `author` +- `created` +- `updated` + +Add `owner`, `related`, `evidence`, `supersedes`, `amends`, or `decision-owner` where applicable. + +IDs are scoped to a workspace or permanent section and use prefixes: + +- Decision: `D-` +- Finding: `F-` +- Question: `Q-` +- Failure: `X-` +- Conflict: `C-` +- Handoff: `H-` +- Improvement: `I-` +- Technical debt: `TD-` +- Assumption: `A-` +- Dependency: `DEP-` +- Scope change: `SC-` +- Amendment: `AM-` +- Milestone: `M-` + +Allocate the next numeric ID by inspecting the relevant directory. Avoid a shared mutable counter file that causes concurrent-write contention. + +### 10.2 Required templates + +Provide at least: + +- task card; +- context; +- assignments; +- finding; +- decision; +- question; +- failure; +- conflict; +- handoff; +- improvement; +- technical debt; +- assumption; +- dependency; +- scope change; +- promotion; +- closeout; +- amendment; +- human project brief; +- milestone; +- candidate milestone specification. + +### 10.3 Improvement template + +```markdown +# + +## Observed during + + +## Observation + + +## Evidence + + +## Why it matters + + +## Possible direction + + +## Relationship to current work + +``` + +### 10.4 Conflict template + +```markdown +# + +## Position A + + +## Position B + + +## Evidence + + +## Impact + + +## Required resolution + + +## Resolution + +``` + +## 11. Autonomous write routing + +The skill must decide whether a note is durable enough to write. Use this test: + +> Will this information materially help a future developer understand, decide, avoid a failure, or continue unfinished work? + +If no, keep it in session context and do not create a file. + +If yes, route it by intent: + +- accepted choice and rationale → `decisions/`; +- mutable architectural reasoning → `architecture/`; +- product/domain fact → `domains/`; +- recurring procedure → `runbooks/`; +- sharp edge or false start → `gotchas/`; +- unresolved decision → `open-questions/`; +- out-of-scope opportunity → `improvements/`; +- remediation liability → `tech-debt/` and `LEDGER.md`; +- task-specific evidence or working state → current workspace; +- stable accepted documentation → propose or perform promotion to `docs/`, then link from KBB. + +Avoid duplicate records. Search indexes and relevant titles before creating a new entry. Append evidence to an active compatible record when appropriate; create a new record when the conclusion, scope, owner, or lifecycle differs. + +## 12. Tooling + +The project-local KB README promises: + +```bash +make kb-index +make kb-new SECTION=… TITLE=… +make kb-check +``` + +Implement deterministic, dependency-light tooling using the repository's established scripting language. If no convention controls this choice, use Python 3 standard library. + +Recommended project-local script surface: + +```text +scripts/kb_brain.py init [level] +scripts/kb_brain.py start [level] +scripts/kb_brain.py new
[--task <id>] +scripts/kb_brain.py index [path] +scripts/kb_brain.py check [path] +scripts/kb_brain.py close <task-id> +scripts/kb_brain.py amend <closed-task-id> <record-path> <title> +``` + +Initialization should add Makefile targets only through a clearly marked, idempotent block. If no Makefile exists, create `kb-brain/Makefile.inc` and document how to include it rather than replacing project build tooling. + +At minimum expose: + +```make +kb-index: + python3 scripts/kb_brain.py index + +kb-new: + python3 scripts/kb_brain.py new "$(SECTION)" "$(TITLE)" + +kb-check: + python3 scripts/kb_brain.py check +``` + +Additional targets may include `kb-start`, `kb-close`, and `kb-amend`. + +All operations must be idempotent where reasonable and fail clearly without destructive partial changes. + +## 13. Closing, sealing, and amendments + +### 13.1 Final cleanup + +Immediately before closing, the lead may: + +- remove empty placeholders and accidental duplicate files; +- repair metadata and broken links; +- update statuses, ownership, and final context; +- remove accidentally captured secrets or sensitive transient output; +- complete `PROMOTION.md` and `CLOSEOUT.md`; +- regenerate indexes; +- run validation. + +Do not erase substantive failures, disagreements, abandoned approaches, or evidence to make the history appear cleaner. + +### 13.2 Closure flow + +```text +active work +→ final cleanup +→ durable knowledge promotion +→ closeout generation +→ index regeneration +→ validation +→ seal creation +→ move to work/closed/ +→ immutable +``` + +A closed workspace retains the same atomic structure and additionally contains: + +```text +CLOSEOUT.md +SEAL.json +amendments/ +``` + +### 13.3 Immutability enforcement + +Generate `SEAL.json` containing SHA-256 hashes of every historical workspace file at closure, excluding: + +- `SEAL.json` itself; +- generated `INDEX.md`; +- files under `amendments/`. + +`kb-check` must report any mutation, removal, or rename of sealed files. It may regenerate `INDEX.md` and accept new valid amendment files without invalidating the seal. + +### 13.4 Amendments + +After closure, never edit, rename, or delete original records. Create an atomic amendment under: + +```text +kb-brain/work/closed/<task-id>/amendments/ +``` + +An amendment must identify what it corrects, clarifies, or supersedes, explain why, link new evidence, and state impact. Regenerated indexes must visibly mark amended records. + +--- + +# Part B — `brief-ruminate` + +## 14. Skill purpose and activation + +Create a separate `/paad:brief-ruminate` skill. It depends on a KBB structure being present but does not modify `kb-brain` skill logic or any existing PAAD skill. + +Purpose: + +- preserve the human-written project brief as controlling intent; +- track its milestones atomically; +- use repository and KBB evidence to expand one milestone at a time; +- produce a candidate specification for human review; +- record consequential questions, assumptions, dependencies, and conflicts; +- stop before approval, planning, or implementation. + +Recommended frontmatter: + +```yaml +--- +name: brief-ruminate +description: Expand a human-owned project brief or selected milestone into a repository-grounded candidate specification using KB-Brain context. Use when creating or reviewing project milestones, identifying dependencies and unresolved decisions, or preparing a milestone specification for human approval before pushback, planning, alignment, or implementation. +--- +``` + +## 15. Arguments + +```text +/paad:brief-ruminate <brief-path> +/paad:brief-ruminate <brief-path> <milestone-id> +/paad:brief-ruminate next <brief-path> +/paad:brief-ruminate status <brief-path> +``` + +Without a milestone ID, inspect the brief and milestone index, then recommend the next milestone that is both valuable and sufficiently unblocked. Do not select a milestone solely because it appears first. + +## 16. Brief and milestone storage + +Use atomic storage: + +```text +kb-brain/briefs/<brief-slug>/ +├── BRIEF.md +├── INDEX.md +└── milestones/ + ├── M-001-<slug>.md + ├── M-002-<slug>.md + └── ... + +kb-brain/specs/<brief-slug>/ +└── M-001-<slug>-spec.md +``` + +The human owns `BRIEF.md`. Agents may suggest amendments but must not silently rewrite the brief's intent, outcomes, constraints, or non-goals. + +## 17. Brief template + +A brief should remain product-level and implementation-light: + +```markdown +# <Project or feature brief> + +## Intended outcome +<What should become possible> + +## Users and stakeholders +<Who benefits or is affected> + +## Why it matters +<Value, problem, or risk addressed> + +## Constraints +<Hard boundaries> + +## Non-goals +<Explicit exclusions> + +## Success at project level +<Observable outcomes> + +## Known milestone ideas +<Initial human-authored decomposition; may be incomplete> + +## Open questions +<Questions the human has intentionally left unresolved> +``` + +## 18. Milestone lifecycle + +Use these explicit states: + +```text +brief +→ incubating +→ ready-for-expansion +→ expanding +→ review-needed +→ approved-spec +→ planned +→ in-progress +→ completed +→ superseded +``` + +The skill may move a milestone as far as `review-needed`. Only a human may mark it `approved-spec`. + +Existing PAAD skills can then be invoked separately: + +```text +approved specification +→ pushback +→ implementation planning +→ alignment +→ implementation +→ agentic-review +``` + +Do not auto-run or modify those skills. + +## 19. Rumination workflow + +For one selected milestone: + +1. Read `BRIEF.md`, its milestone record, and the brief index. +2. Read `kb-brain/work/ACTIVE.md` to identify ownership or blockers relevant to the milestone. +3. Read the relevant repository files, stable documentation, KBB indexes, decisions, questions, failures, and gotchas. +4. Check whether previous decisions constrain the milestone. +5. Identify dependencies, conflicts, assumptions, compatibility concerns, data implications, operational concerns, and non-goals. +6. Determine whether the milestone is independently valuable and reasonably scoped. +7. If it is oversized, propose an atomic milestone split without changing the human brief automatically. +8. Ask only consequential questions that materially change scope or behaviour. +9. Write a candidate milestone spec with status `review-needed`. +10. Link all supporting and unresolved KBB records. +11. Update the milestone index and stop for human review. + +Do not “ruminate” indefinitely or repeatedly rewrite a candidate spec without new evidence or human direction. + +## 20. Candidate milestone specification template + +```markdown +# <Milestone title> + +## Status +review-needed + +## Brief linkage +<Brief path and milestone ID> + +## Outcome +<Concrete result delivered by this milestone> + +## Scope +<Included behaviour> + +## Non-goals +<Excluded behaviour> + +## User and system behaviour +<Observable behaviours and flows> + +## Constraints and controlling decisions +<Hard limits and links to accepted decisions> + +## Components and boundaries +<Likely areas involved, without pretending the implementation is already planned> + +## Data and interface implications +<Schemas, APIs, compatibility, migration, or persistence concerns> + +## Failure and recovery behaviour +<Expected handling of errors and partial completion> + +## Security, privacy, accessibility, and operational concerns +<Only relevant concerns; state not applicable where justified> + +## Dependencies +<Internal and external dependencies> + +## Acceptance criteria +<Testable product-level criteria> + +## Testing expectations +<Required evidence categories, not a full implementation plan> + +## Open questions +<Unresolved consequential questions> + +## Assumptions +<Explicit assumptions requiring validation> + +## Evidence consulted +<Repository and KBB links> + +## Human approval +Unapproved candidate specification. +``` + +## 21. Brief-rumination stop conditions + +Stop and leave the milestone unapproved when: + +- the brief is contradictory or lacks a usable intended outcome; +- a controlling decision is missing; +- unresolved dependencies make a meaningful specification impossible; +- the milestone overlaps another active owner's work and the boundary is unresolved; +- the candidate would require inventing product behaviour; +- the milestone is too large and the human has not accepted a proposed split. + +Record the blocker or open question in KBB rather than guessing. + +--- + +# Part C — Repository implementation + +## 22. Expected repository changes + +The implementing agent must inspect the current repository and adjust paths to existing generation conventions. The expected canonical additions are: + +```text +plugins/paad/skills/kb-brain/ +├── SKILL.md +├── references/ +│ ├── structure.md +│ ├── routing.md +│ ├── lifecycle.md +│ └── templates.md +├── templates/ +│ └── <project-local template files> +└── scripts/ + └── <deterministic scaffold/index/check/seal tool> + +plugins/paad/skills/brief-ruminate/ +├── SKILL.md +├── references/ +│ ├── brief-format.md +│ ├── milestone-lifecycle.md +│ └── candidate-spec.md +└── templates/ + ├── BRIEF.md + ├── MILESTONE.md + └── MILESTONE-SPEC.md +``` + +Keep `SKILL.md` focused. Put detailed templates and long reference material in bundled resources so they are loaded only when needed. + +Also update: + +- `README.md` available-skills documentation; +- `plugins/paad/skills/help/SKILL.md` overview and detailed sections; +- `CHANGELOG.md`; +- plugin and marketplace manifests with a synchronized semver bump; +- repository-generated or mirrored platform skill directories using the existing generator; +- tests and validation scripts required by current repository conventions. + +Do not change prose or flows inside existing skills unless a shared generated index absolutely requires a mechanical listing update. + +## 23. AGENTS.md integration text + +Provide this as an installation snippet and include it in the `kb-brain` reference material: + +```markdown +## KB-Brain + +Use the `kb-brain` skill for section routing, templates, workspace lifecycle, and write rules. + +Every PAAD-managed task uses a focused shared workspace under: + +`kb-brain/work/active/<task-id>/` + +Repository workspace level: `standard` + +Supported levels: + +- `minimal` — scope, status, ownership, handoff, and durable findings +- `standard` — findings, questions, decisions, failures, conflicts, and handoffs +- `strict` — standard plus evidence, assumptions, dependencies, ownership, and scope-change tracking + +A task may raise its level. Lowering below the repository level requires explicit human approval. + +Sub-agents may append findings, evidence, questions, failures, conflicts, and handoffs. + +Only the lead agent or human task owner may change task scope, lifecycle status, assignments, blockers, confirmed decisions, or conflict resolution state. + +Conflicting findings must be recorded rather than overwritten. Address conflicts during the session when they affect current work. + +Agents may autonomously: + +- add one file per gap under `kb-brain/improvements/`; +- track smoothing or remediation work under `kb-brain/tech-debt/`; +- update `kb-brain/tech-debt/LEDGER.md`; +- move resolved debt into `kb-brain/tech-debt/closed/`; +- promote durable task knowledge into the appropriate permanent KBB section. + +Use the provided templates for human- and agent-authored records. + +Bulk content ingress from existing documentation is a dedicated operation. Do not copy architecture documentation into KBB unprompted. + +Closed workspaces are immutable. Add corrections or later findings as explicit amendments. +``` + +## 24. Required validation behaviour + +`kb-check` should validate at least: + +- expected top-level section names; +- required workspace files for the selected level; +- valid YAML frontmatter; +- required fields and valid status values; +- unique IDs within their scope; +- valid relative links where deterministically checkable; +- task ownership and lead-only metadata consistency; +- blockers represented consistently between `TASK.md` and generated `ACTIVE.md`; +- closed workspace seal hashes; +- amendment references point to an existing sealed record; +- no original closed file has been changed, removed, or renamed; +- no obvious secret patterns are present in files being sealed; +- `INDEX.md` and `ACTIVE.md` are up to date or can be regenerated cleanly. + +Validation should report all safe-to-collect errors in one run rather than stopping at the first issue. + +## 25. Testing requirements + +Follow the repository's existing test style. Add automated coverage for at least: + +### Initialization + +- creates the required KBB structure; +- preserves existing files; +- is idempotent; +- creates project-local templates; +- defaults to the repository-selected workspace level; +- does not ingest `docs/`. + +### Workspace lifecycle + +- creates a workspace for every task; +- creates only level-appropriate required directories/files; +- uses collision-safe task IDs; +- generates `INDEX.md` and `ACTIVE.md`; +- lists ownership and blockers but not conflicts in `ACTIVE.md`; +- updates task status without losing atomic records. + +### Routing and records + +- allocates unique atomic IDs; +- routes improvements and debt correctly; +- supports human and agent authors; +- detects invalid frontmatter and duplicate IDs; +- prevents unresolved questions from being marked answered without owner/evidence metadata. + +### Closure and immutability + +- permits final cleanup before closure; +- requires closeout and promotion checks; +- creates `SEAL.json` correctly; +- detects mutation, removal, and rename of sealed files; +- permits valid amendments; +- marks amended records in regenerated indexes; +- retains all atomic historical files. + +### Brief rumination + +- preserves the human brief unchanged; +- reads milestone and KBB context selectively; +- creates an atomic candidate spec with `review-needed` status; +- cannot mark a spec approved; +- records blockers rather than inventing missing requirements; +- updates milestone and brief indexes; +- does not invoke or alter other PAAD skills. + +### Skill package validation + +- frontmatter names match directories; +- Graphviz diagrams cover all decision branches and stop conditions; +- README and help documentation list both skills and arguments; +- manifests use the same new version; +- generated platform copies match canonical sources; +- complete repository test suite passes. + +## 26. Suggested implementation sequence + +1. Inspect current main branch, repository generation scripts, tests, manifests, and current version. +2. Add failing tests for skill discovery, docs coverage, and KBB deterministic tooling. +3. Implement shared KBB templates and project-local scaffold/index/check functions. +4. Implement workspace start, record creation, global active index, and validation. +5. Implement close, seal, immutable validation, and amendments. +6. Write the `kb-brain` skill workflow and complete Graphviz diagram. +7. Add brief and milestone templates plus candidate-spec generation support. +8. Write the `brief-ruminate` skill workflow and complete Graphviz diagram. +9. Add README, help, AGENTS snippet, and changelog documentation. +10. Regenerate all supported platform mirrors using repository tooling. +11. Bump patch version in all required manifests. +12. Run formatting, generated-file checks, repository tests, plugin validation, and local skill smoke tests. +13. Review the diff to confirm existing skills have no behavioural changes. +14. Commit in coherent units and open a pull request. + +## 27. Pull request requirements + +Suggested title: + +```text +feat: add KB-Brain memory and brief rumination skills +``` + +The PR description should explain: + +- why persistent repository-native memory is needed; +- the `docs/` versus `kb-brain/` authority boundary; +- autonomous atomic writes and lead/sub-agent permissions; +- workspace levels and index-first retrieval; +- closure sealing and amendment rules; +- brief-to-milestone-spec expansion and its human approval boundary; +- explicit confirmation that existing PAAD skills are not behaviourally modified; +- tests and validation commands run. + +Keep the PR focused. Do not include unrelated refactors. + +## 28. Definition of done + +The change is complete when: + +- both new skills are discoverable and documented; +- `/paad:kb-brain init` can scaffold a usable KBB without ingesting existing docs; +- every managed task can receive a focused atomic workspace; +- humans and agents share templates; +- sub-agents can append evidence without being able to redefine task authority silently; +- `ACTIVE.md` exposes ownership and blockers economically; +- conflicts affecting current work are surfaced in-session; +- closed workspaces are verifiably immutable and amendable; +- `/paad:brief-ruminate` can turn one human-owned milestone into an unapproved candidate spec grounded in repository and KBB evidence; +- existing PAAD skill behaviour is unchanged; +- all repository tests, generation checks, documentation checks, and plugin validations pass. + +--- + +## 29. Source basis + +- User-supplied `KB_README.md`, including the mutable KBB section model and promised Make targets. +- PAAD repository README: `https://github.com/Ovid/paad` +- PAAD contribution conventions: `https://github.com/Ovid/paad/blob/main/CLAUDE.md` + +Where repository implementation details differ from this handoff, preserve the product rules and adapt the mechanics to the current canonical generation and testing conventions. diff --git a/kiro_and_antigravity/skills/.agent/skills/brief-ruminate/SKILL.md b/kiro_and_antigravity/skills/.agent/skills/brief-ruminate/SKILL.md new file mode 100644 index 0000000..aee7ea5 --- /dev/null +++ b/kiro_and_antigravity/skills/.agent/skills/brief-ruminate/SKILL.md @@ -0,0 +1,11 @@ +--- +name: brief-ruminate +description: Expand a human-owned project brief or selected milestone into a repository-grounded candidate specification using KB-Brain context. Use when creating or reviewing project milestones, identifying dependencies and unresolved decisions, or preparing a milestone specification for human approval before pushback, planning, alignment, or implementation. +--- + +# Brief Rumination (Antigravity Wrapper) + +This is a project-specific skill. The detailed checklist and procedures are in: +**`.kiro/skills/brief-ruminate/SKILL.md`** + +Please refer to that file for the full criteria. diff --git a/kiro_and_antigravity/skills/.agent/skills/kb-brain/SKILL.md b/kiro_and_antigravity/skills/.agent/skills/kb-brain/SKILL.md new file mode 100644 index 0000000..c3456d9 --- /dev/null +++ b/kiro_and_antigravity/skills/.agent/skills/kb-brain/SKILL.md @@ -0,0 +1,11 @@ +--- +name: kb-brain +description: Maintain a repository-native working knowledge base for humans and coding agents. Use when initializing kb-brain, starting or closing task workspaces, recording durable context, decisions, findings, failures, questions, improvements, technical debt, handoffs, or amendments, and when coordinating lead agents and sub-agents through shared project memory. +--- + +# KB-Brain (Antigravity Wrapper) + +This is a project-specific skill. The detailed checklist and procedures are in: +**`.kiro/skills/kb-brain/SKILL.md`** + +Please refer to that file for the full criteria. diff --git a/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/SKILL.md b/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/SKILL.md new file mode 100644 index 0000000..ef5054b --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/SKILL.md @@ -0,0 +1,146 @@ +--- +name: brief-ruminate +description: Expand a human-owned project brief or selected milestone into a repository-grounded candidate specification using KB-Brain context. Use when creating or reviewing project milestones, identifying dependencies and unresolved decisions, or preparing a milestone specification for human approval before pushback, planning, alignment, or implementation. +--- + +**On invocation:** announce "Running paad:brief-ruminate v1.24.1" before anything else. + +# Brief Rumination + +Expand one human-owned brief milestone into a **candidate** specification +grounded in the repository and KB-Brain. The human owns the brief. This skill +stops at `review-needed`. It does **not** approve specs, plan implementation, +or modify `pushback`, `alignment`, `agentic-review`, or any other PAAD skill. + +`kb-brain` skill logic. + +Load on demand: + +- `references/brief-format.md` — brief and milestone storage and templates +- `references/milestone-lifecycle.md` — states and human approval boundary +- `references/candidate-spec.md` — candidate spec shape and stop conditions + +Templates: this skill's `templates/` (`BRIEF.md`, `MILESTONE.md`, +`MILESTONE-SPEC.md`). Optional helpers via `scripts/kb_brain.py brief-init`, +`brief-milestone`, and `brief-spec` (always write `review-needed`, never +`approved-spec`). + +**Session flow:** + +```dot +digraph brief_ruminate { + "kb-brain/ present?" [shape=diamond]; + "Brief path usable?" [shape=diamond]; + "Milestone ID given?" [shape=diamond]; + "Milestone valuable and unblocked?" [shape=diamond]; + "Human accepted split?" [shape=diamond]; + "Controlling decision missing?" [shape=diamond]; + "Would invent product behaviour?" [shape=diamond]; + "Ownership boundary unresolved?" [shape=diamond]; + "Brief contradictory / no outcome?" [shape=diamond]; + + "STOP: run kb-brain init first" [shape=box, style=bold]; + "ASK: which brief?" [shape=box]; + "Read BRIEF.md, milestones, brief INDEX" [shape=box]; + "Recommend next valuable unblocked milestone (not merely first)" [shape=box]; + "ASK: confirm milestone or choose another" [shape=box]; + "Read ACTIVE.md, selective repo + KBB evidence" [shape=box]; + "Propose atomic milestone split" [shape=box]; + "STOP: record blocker/open question; leave unapproved" [shape=box, style=bold]; + "Ask only consequential questions" [shape=box]; + "Write candidate spec status=review-needed" [shape=box]; + "Update milestone + brief indexes; stop for human review" [shape=box]; + "Announce files written or updated" [shape=box]; + "Done" [shape=box]; + + "kb-brain/ present?" -> "STOP: run kb-brain init first" [label="no"]; + "kb-brain/ present?" -> "Brief path usable?" [label="yes"]; + "Brief path usable?" -> "ASK: which brief?" [label="no"]; + "ASK: which brief?" -> "Brief path usable?"; + "Brief path usable?" -> "Brief contradictory / no outcome?" [label="yes"]; + "Brief contradictory / no outcome?" -> "STOP: record blocker/open question; leave unapproved" [label="yes"]; + "Brief contradictory / no outcome?" -> "Milestone ID given?" [label="no"]; + + "Milestone ID given?" -> "Read BRIEF.md, milestones, brief INDEX" [label="yes"]; + "Milestone ID given?" -> "Recommend next valuable unblocked milestone (not merely first)" [label="no / next"]; + "Recommend next valuable unblocked milestone (not merely first)" -> "ASK: confirm milestone or choose another"; + "ASK: confirm milestone or choose another" -> "Read BRIEF.md, milestones, brief INDEX"; + + "Read BRIEF.md, milestones, brief INDEX" -> "Read ACTIVE.md, selective repo + KBB evidence"; + "Read ACTIVE.md, selective repo + KBB evidence" -> "Milestone valuable and unblocked?"; + "Milestone valuable and unblocked?" -> "Propose atomic milestone split" [label="oversized"]; + "Propose atomic milestone split" -> "Human accepted split?"; + "Human accepted split?" -> "Read BRIEF.md, milestones, brief INDEX" [label="yes — new milestone"]; + "Human accepted split?" -> "STOP: record blocker/open question; leave unapproved" [label="no"]; + + "Milestone valuable and unblocked?" -> "Controlling decision missing?" [label="scoped ok"]; + "Controlling decision missing?" -> "STOP: record blocker/open question; leave unapproved" [label="yes"]; + "Controlling decision missing?" -> "Would invent product behaviour?" [label="no"]; + "Would invent product behaviour?" -> "STOP: record blocker/open question; leave unapproved" [label="yes"]; + "Would invent product behaviour?" -> "Ownership boundary unresolved?" [label="no"]; + "Ownership boundary unresolved?" -> "STOP: record blocker/open question; leave unapproved" [label="yes"]; + "Ownership boundary unresolved?" -> "Ask only consequential questions" [label="no"]; + + "Ask only consequential questions" -> "Write candidate spec status=review-needed"; + "Write candidate spec status=review-needed" -> "Update milestone + brief indexes; stop for human review"; + "Update milestone + brief indexes; stop for human review" -> "Announce files written or updated"; + "STOP: record blocker/open question; leave unapproved" -> "Announce files written or updated"; + "Announce files written or updated" -> "Done"; +} +``` + +## Authority + +- The human owns `BRIEF.md`. Suggest amendments; never silently rewrite intent, + outcomes, constraints, or non-goals. +- This skill may advance a milestone as far as `review-needed`. +- **Only a human** may mark `approved-spec`. +- After approval, other PAAD skills may be invoked **separately** (`pushback` → + planning → `alignment` → implementation → `agentic-review`). Do not auto-run + them. + +## Rumination steps (one milestone) + +1. Read `BRIEF.md`, the milestone record, and the brief index. +2. Read `kb-brain/work/ACTIVE.md` for ownership or blockers relevant to the + milestone. +3. Read relevant repository files, stable docs, and KBB indexes / decisions / + questions / failures / gotchas — selectively, index-first. +4. Check whether previous decisions constrain the milestone. +5. Identify dependencies, conflicts, assumptions, compatibility, data, and + operational concerns; respect non-goals. +6. Confirm the milestone is independently valuable and reasonably scoped. +7. If oversized, propose an atomic split without changing the human brief + automatically. +8. Ask only consequential questions that materially change scope or behaviour. +9. Write a candidate milestone spec with status `review-needed` under + `kb-brain/specs/<brief-slug>/`. +10. Link supporting and unresolved KBB records. +11. Update the milestone index and **stop** for human review. + +Do not ruminate indefinitely or repeatedly rewrite a candidate without new +evidence or human direction. + +## Announce What You Wrote + +``` +Files written or updated: + new kb-brain/specs/checkout/M-001-guest-cart-spec.md + updated kb-brain/briefs/checkout/milestones/M-001-guest-cart.md + updated kb-brain/briefs/checkout/INDEX.md +``` + +If the run only recorded a blocker/open question and wrote no spec, still list +those KBB paths. + +## Common Mistakes + +| Mistake | What to do instead | +|---------|-------------------| +| Marking a spec `approved-spec` | Leave `review-needed`; humans approve | +| Rewriting the brief's intent | Suggest amendments; wait for the human | +| Picking the first milestone blindly | Prefer valuable and unblocked | +| Inventing missing product behaviour | Record an open question / blocker and stop | +| Auto-running pushback or alignment | Stop after the candidate spec | +| Loading the entire KB | Index-first, selective reads | +| Changing other PAAD skill behaviour | Leave them untouched | diff --git a/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/references/brief-format.md b/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/references/brief-format.md new file mode 100644 index 0000000..11a0615 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/references/brief-format.md @@ -0,0 +1,63 @@ +# Brief and milestone format + +## Storage + +```text +kb-brain/briefs/<brief-slug>/ +├── BRIEF.md +├── INDEX.md +└── milestones/ + ├── M-001-<slug>.md + └── ... + +kb-brain/specs/<brief-slug>/ +└── M-001-<slug>-spec.md +``` + +The human owns `BRIEF.md`. Agents may suggest amendments but must not silently +rewrite intent, outcomes, constraints, or non-goals. + +## Brief template (product-level, implementation-light) + +```markdown +# <Project or feature brief> + +## Intended outcome +... + +## Users and stakeholders +... + +## Why it matters +... + +## Constraints +... + +## Non-goals +... + +## Success at project level +... + +## Known milestone ideas +... + +## Open questions +... +``` + +Canonical copies: this skill's `templates/BRIEF.md` and +`kb-brain/templates/BRIEF.md` after init. + +## Milestone records + +Atomic files under `milestones/` with prefix `M-`. Status follows +`references/milestone-lifecycle.md`. Keep each milestone independently valuable +when possible. + +## Index + +`INDEX.md` lists milestone ID, title, status, and linked candidate spec path. +Regenerate when milestones or specs change (`kb_brain.py` brief helpers, or by +hand consistently with the table format). diff --git a/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/references/candidate-spec.md b/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/references/candidate-spec.md new file mode 100644 index 0000000..c53bc80 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/references/candidate-spec.md @@ -0,0 +1,84 @@ +# Candidate milestone specification + +Status must be `review-needed`. Human approval section must remain unapproved +until a human changes it. + +```markdown +# <Milestone title> + +## Status +review-needed + +## Brief linkage +<Brief path and milestone ID> + +## Outcome +... + +## Scope +... + +## Non-goals +... + +## User and system behaviour +... + +## Constraints and controlling decisions +... + +## Components and boundaries +... + +## Data and interface implications +... + +## Failure and recovery behaviour +... + +## Security, privacy, accessibility, and operational concerns +... + +## Dependencies +... + +## Acceptance criteria +... + +## Testing expectations +... + +## Open questions +... + +## Assumptions +... + +## Evidence consulted +... + +## Human approval +Unapproved candidate specification. +``` + +Canonical template: `templates/MILESTONE-SPEC.md`. + +Tooling helper (never writes `approved-spec`): + +```bash +python3 scripts/kb_brain.py brief-spec <brief-slug> <milestone-id> +``` + +## Stop conditions (leave unapproved) + +Stop and record a blocker or open question in KBB when: + +- the brief is contradictory or lacks a usable intended outcome +- a controlling decision is missing +- unresolved dependencies make a meaningful specification impossible +- the milestone overlaps another active owner's work and the boundary is unresolved +- the candidate would require inventing product behaviour +- the milestone is too large and the human has not accepted a proposed split + +Do not ruminate indefinitely or rewrite the candidate without new evidence or +human direction. diff --git a/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/references/milestone-lifecycle.md b/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/references/milestone-lifecycle.md new file mode 100644 index 0000000..e290622 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/references/milestone-lifecycle.md @@ -0,0 +1,45 @@ +# Milestone lifecycle + +```text +brief +→ incubating +→ ready-for-expansion +→ expanding +→ review-needed +→ approved-spec +→ planned +→ in-progress +→ completed +→ superseded +``` + +## Who may advance which states + +| Transition | Actor | +|------------|-------| +| through `review-needed` | `brief-ruminate` / agents | +| to `approved-spec` | **human only** | +| `planned` and beyond | separate planning / implementation workflows | + +## After human approval (separate invocations) + +```text +approved specification +→ pushback +→ implementation planning +→ alignment +→ implementation +→ agentic-review +``` + +Do **not** auto-run or modify those skills from `brief-ruminate`. + +## Selection guidance + +When choosing the next milestone: + +- Prefer independently valuable outcomes that are sufficiently unblocked. +- Do not select solely because a milestone appears first in the list. +- If oversized, propose a split; do not rewrite the brief automatically. +- If blocked on a missing controlling decision, ownership overlap, or invented + behaviour, stop and record the blocker rather than guessing. diff --git a/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/templates/BRIEF.md b/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/templates/BRIEF.md new file mode 100644 index 0000000..69b2244 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/templates/BRIEF.md @@ -0,0 +1,35 @@ +--- +id: brief-slug +type: brief +status: active +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +--- + +# Title + +## Intended outcome +- + +## Users and stakeholders +- + +## Why it matters +- + +## Constraints +- + +## Non-goals +- + +## Success at project level +- + +## Known milestone ideas +- + +## Open questions +- diff --git a/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/templates/MILESTONE-SPEC.md b/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/templates/MILESTONE-SPEC.md new file mode 100644 index 0000000..2d91ef3 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/templates/MILESTONE-SPEC.md @@ -0,0 +1,66 @@ +--- +id: F-001-spec +type: milestone-spec +status: review-needed +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +--- + +# Title + +## Status +review-needed + +## Brief linkage +- + +## Outcome +- + +## Scope +- + +## Non-goals +- + +## User and system behaviour +- + +## Constraints and controlling decisions +- + +## Components and boundaries +- + +## Data and interface implications +- + +## Failure and recovery behaviour +- + +## Security, privacy, accessibility, and operational concerns +- + +## Dependencies +- + +## Acceptance criteria +- + +## Testing expectations +- + +## Open questions +- + +## Assumptions +- + +## Evidence consulted +- + +## Human approval +Unapproved candidate specification. diff --git a/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/templates/MILESTONE.md b/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/templates/MILESTONE.md new file mode 100644 index 0000000..b9f5f92 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/brief-ruminate/templates/MILESTONE.md @@ -0,0 +1,18 @@ +--- +id: F-001 +type: milestone +status: brief +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +--- + +# Title + +## Outcome +- + +## Notes +- diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/SKILL.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/SKILL.md new file mode 100644 index 0000000..8e93b0c --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/SKILL.md @@ -0,0 +1,209 @@ +--- +name: kb-brain +description: Maintain a repository-native working knowledge base for humans and coding agents. Use when initializing kb-brain, starting or closing task workspaces, recording durable context, decisions, findings, failures, questions, improvements, technical debt, handoffs, or amendments, and when coordinating lead agents and sub-agents through shared project memory. +--- + +**On invocation:** announce "Running paad:kb-brain v1.24.1" before anything else. + +# KB-Brain + +Repository-native working knowledge for humans, lead agents, and sub-agents. +Stable architecture and accepted documentation stay in `docs/`. Mutable +context, in-flight decisions, gaps, debt, lessons, and focused task workspaces +live under `kb-brain/`. + +This skill does **not** change the behaviour of `pushback`, `alignment`, +`agentic-review`, or any other existing PAAD skill. Invoke it explicitly (or +via repository `AGENTS.md` instructions). + +Load details on demand from: + +- `references/structure.md` — directory layout, workspace levels, file roles +- `references/routing.md` — when to write, where to put it, AGENTS.md snippet +- `references/lifecycle.md` — start, close, seal, amend, validation +- `references/templates.md` — atomic record templates and ID prefixes + +Bundled templates live in this skill's `templates/` directory. Deterministic +tooling is `scripts/kb_brain.py` (copied into the target repo on `init`). + +**Dispatch:** + +```dot +digraph kb_brain_dispatch { + "Action known?" [shape=diamond]; + "kb-brain/ exists?" [shape=diamond]; + "Which action?" [shape=diamond]; + "Durable enough to write?" [shape=diamond]; + "Level below repo default?" [shape=diamond]; + "Human approved lower level?" [shape=diamond]; + "Promotion + closeout ready?" [shape=diamond]; + "Validation clean?" [shape=diamond]; + + "Infer from conversation or show operation summary and ASK" [shape=box]; + "init scaffold (no docs ingest)" [shape=box]; + "STOP: run init first" [shape=box, style=bold]; + "start workspace" [shape=box]; + "STOP: refuse lower level without approval" [shape=box, style=bold]; + "record / route to section or workspace" [shape=box]; + "Keep in session only" [shape=box]; + "status / index / check" [shape=box]; + "close → seal → move to work/closed" [shape=box]; + "STOP: finish promotion/closeout or fix check errors" [shape=box, style=bold]; + "amend under amendments/ only" [shape=box]; + "Announce files written or updated" [shape=box]; + "Done" [shape=box]; + + "Action known?" -> "Which action?" [label="yes"]; + "Action known?" -> "Infer from conversation or show operation summary and ASK" [label="no"]; + "Infer from conversation or show operation summary and ASK" -> "Action known?"; + + "Which action?" -> "init scaffold (no docs ingest)" [label="init"]; + "Which action?" -> "kb-brain/ exists?" [label="other"]; + "kb-brain/ exists?" -> "STOP: run init first" [label="no"]; + "kb-brain/ exists?" -> "Level below repo default?" [label="yes + start"]; + "kb-brain/ exists?" -> "Durable enough to write?" [label="yes + record/route"]; + "kb-brain/ exists?" -> "status / index / check" [label="yes + status|index|check"]; + "kb-brain/ exists?" -> "Promotion + closeout ready?" [label="yes + close"]; + "kb-brain/ exists?" -> "amend under amendments/ only" [label="yes + amend"]; + + "Level below repo default?" -> "Human approved lower level?" [label="yes"]; + "Level below repo default?" -> "start workspace" [label="no"]; + "Human approved lower level?" -> "start workspace" [label="yes"]; + "Human approved lower level?" -> "STOP: refuse lower level without approval" [label="no"]; + + "Durable enough to write?" -> "record / route to section or workspace" [label="yes"]; + "Durable enough to write?" -> "Keep in session only" [label="no"]; + + "Promotion + closeout ready?" -> "Validation clean?" [label="yes"]; + "Promotion + closeout ready?" -> "STOP: finish promotion/closeout or fix check errors" [label="no"]; + "Validation clean?" -> "close → seal → move to work/closed" [label="yes"]; + "Validation clean?" -> "STOP: finish promotion/closeout or fix check errors" [label="no"]; + + "init scaffold (no docs ingest)" -> "Announce files written or updated"; + "start workspace" -> "Announce files written or updated"; + "record / route to section or workspace" -> "Announce files written or updated"; + "Keep in session only" -> "Done"; + "status / index / check" -> "Announce files written or updated"; + "close → seal → move to work/closed" -> "Announce files written or updated"; + "amend under amendments/ only" -> "Announce files written or updated"; + "Announce files written or updated" -> "Done"; +} +``` + +## Global rules + +1. **Human authority.** Agents may record observations, evidence, failures, + questions, improvements, debt, handoffs, and working context. They must + **not** silently promote inferred requirements, unapproved architecture, + invented answers to open questions, improvements-as-roadmap, or candidate + specs to accepted facts. Confirmed decisions need explicit human + confirmation or unambiguous repository evidence — record owner and evidence. +2. **Notice broadly, work narrowly.** Out-of-scope gaps become atomic + improvement or tech-debt records. Do not implement them unless the active + task includes them. +3. **Index-first retrieval.** Read `kb-brain/work/ACTIVE.md`, then the + workspace `TASK.md`, indexes, `CONTEXT.md`, assignment, then only linked + records. Never load the whole KB by default. +4. **No unsolicited bulk ingress.** Do not dump `docs/` into `kb-brain/` on + init or during ordinary work. +5. **Existing skills unchanged.** Do not wire automatic KBB reads/writes into + other PAAD skills. + +## Tooling + +After `init`, the target repo has `scripts/kb_brain.py` and Make targets +(`kb-index`, `kb-new`, `kb-check`, plus `kb-start` / `kb-close` / `kb-amend`). +Prefer: + +```bash +python3 scripts/kb_brain.py init standard +python3 scripts/kb_brain.py start <slug> [level] +python3 scripts/kb_brain.py new <section> "<title>" [--task <id>] +python3 scripts/kb_brain.py index +python3 scripts/kb_brain.py check +python3 scripts/kb_brain.py close <task-id> +python3 scripts/kb_brain.py amend <closed-task-id> <record-path> "<title>" +``` + +Until `init` has run in the target repo, invoke the bundled copy: + +`python3 <path-to-this-skill>/scripts/kb_brain.py --root <repo> …` + +If the target has no Makefile, `init` writes `kb-brain/Makefile.inc` instead +of replacing project build tooling. + +## Operations + +### init + +Scaffold `kb-brain/` (see `references/structure.md`). Copy templates. Install +script + Make block. Default level is `standard` unless overridden. **Do not** +ingest existing documentation. + +### start + +Create `kb-brain/work/active/YYYY-MM-DD-<slug>/` with level-appropriate files. +Collision-safe IDs (`-2`, `-3`, …). Raise above repo default freely; lowering +requires explicit human approval. Regenerate `ACTIVE.md` and indexes. + +### record / route + +Apply the durability test in `references/routing.md`. Search indexes before +creating duplicates. Allocate the next ID by inspecting the target directory +(no shared counter file). Sub-agents may append findings, questions, failures, +conflicts, handoffs, improvements, and tech-debt. Only the lead or human task +owner may change scope, lifecycle, assignments, confirmed decisions, blockers, +conflict resolution, or close/seal. + +### status / index / check + +`status` summarizes active work from `ACTIVE.md` / `TASK.md`. `index` +regenerates indexes. `check` validates structure, frontmatter, IDs, seals, and +amendments — report **all** safe-to-collect errors in one run. + +### close + +Final cleanup → promote durable knowledge → `CLOSEOUT.md` → index → validate → +`SEAL.json` (SHA-256 of historical files, excluding seal, generated `INDEX.md`, +and `amendments/`) → move to `work/closed/`. Do not erase substantive failures +or disagreements to “clean up” history. + +### amend + +After closure, never edit sealed originals. Create +`work/closed/<task-id>/amendments/AM-…md` that names what it corrects. +Regenerated indexes must mark amended records. + +## Conflict protocol + +Record conflicting findings; never overwrite. When a conflict affects current +work, the lead sets `resolved`, `deferred`, or `blocked` (and adds blockers to +`TASK.md` so they appear in `ACTIVE.md`). Unrelated conflicts stay recorded +and do not block the current assignment. `ACTIVE.md` lists ownership and +blockers — **not** conflicts. + +## Announce What You Wrote + +Before any summary, list every path this run created or updated: + +``` +Files written or updated: + new kb-brain/work/active/2026-08-04-auth-migration/TASK.md + updated kb-brain/work/ACTIVE.md +``` + +Source and test files outside `kb-brain/` need only a count and a pointer to +the diff when there are many. Skills that write nothing are exempt; this skill +almost always writes. + +## Common Mistakes + +| Mistake | What to do instead | +|---------|-------------------| +| Dumping `docs/` into KBB on init | Init scaffolds empty sections only | +| Reading the whole KB every turn | Index-first retrieval order | +| Inventing answers in `open-questions/` | Add evidence; leave unanswered | +| Editing a sealed closed workspace | Write an amendment | +| Lowering workspace level silently | Get explicit human approval | +| Auto-wiring KBB into other PAAD skills | Leave those skills unchanged | +| Implementing every improvement you notice | Record it; stay in task scope | diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/references/lifecycle.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/references/lifecycle.md new file mode 100644 index 0000000..3b013b0 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/references/lifecycle.md @@ -0,0 +1,87 @@ +# Lifecycle: start, close, seal, amend, check + +## Start + +1. Ensure `kb-brain/` exists (`init` if not). +2. Resolve repository default level from `AGENTS.md` or `.default-level`. +3. Refuse levels below the repo default without explicit human approval. +4. Allocate `YYYY-MM-DD-<slug>` (append `-2`, … on collision). +5. Create level-appropriate files and directories from templates. +6. Regenerate workspace `INDEX.md`, `work/ACTIVE.md`, and repo `INDEX.md`. + +## Final cleanup (before close) + +The lead may: + +- remove empty placeholders and accidental duplicates +- repair metadata and broken links +- update statuses, ownership, and final context +- remove accidentally captured secrets or sensitive transient output +- complete `PROMOTION.md` and `CLOSEOUT.md` +- regenerate indexes and run validation + +Do **not** erase substantive failures, disagreements, abandoned approaches, or +evidence to make history look cleaner. + +## Closure flow + +```text +active work +→ final cleanup +→ durable knowledge promotion +→ closeout generation +→ index regeneration +→ validation +→ seal creation +→ move to work/closed/ +→ immutable +``` + +Closed workspace retains its atomic structure and adds: + +```text +CLOSEOUT.md +SEAL.json +amendments/ +``` + +## SEAL.json + +SHA-256 hashes of every historical workspace file at closure, **excluding**: + +- `SEAL.json` itself +- generated `INDEX.md` +- files under `amendments/` + +`kb-check` reports mutation, removal, or rename of sealed files. It may +regenerate `INDEX.md` and accept new valid amendment files without invalidating +the seal. + +## Amendments + +Never edit, rename, or delete original sealed records. Create: + +```text +kb-brain/work/closed/<task-id>/amendments/AM-NNN-<slug>.md +``` + +An amendment must identify what it corrects, clarifies, or supersedes; explain +why; link new evidence; and state impact. Regenerated indexes visibly mark +amended records. + +## kb-check validates + +- expected top-level section names +- required workspace files for the selected level +- valid YAML frontmatter and required fields +- unique IDs within their scope +- valid relative links where deterministically checkable +- task ownership / lead-only metadata consistency +- blockers consistent between `TASK.md` and generated `ACTIVE.md` +- closed workspace seal hashes +- amendment references point at an existing sealed record +- no original closed file changed, removed, or renamed +- no obvious secret patterns in files being sealed +- indexes regenerable cleanly + +Report all safe-to-collect errors in one run; do not stop at the first issue. diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/references/routing.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/references/routing.md new file mode 100644 index 0000000..5bee789 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/references/routing.md @@ -0,0 +1,109 @@ +# Routing and write rules + +## Durability test + +> Will this information materially help a future developer understand, decide, +> avoid a failure, or continue unfinished work? + +If **no** — keep it in session context; do not create a file. + +If **yes** — route by intent: + +| Intent | Destination | +|--------|-------------| +| Accepted choice and rationale | `decisions/` | +| Mutable architectural reasoning | `architecture/` (link to stable `docs/`) | +| Product/domain fact | `domains/` | +| Recurring procedure | `runbooks/` | +| Sharp edge or false start | `gotchas/` | +| Unresolved decision | `open-questions/` | +| Out-of-scope opportunity | `improvements/` | +| Remediation liability | `tech-debt/` + update `LEDGER.md` | +| Task-specific evidence or working state | current workspace | +| Stable accepted documentation | propose/perform promotion to `docs/`, then link from KBB | + +Avoid duplicates: search indexes and relevant titles first. Append evidence to +an active compatible record when the conclusion, scope, owner, and lifecycle +match; otherwise create a new record. + +## Agent permissions + +### Sub-agents may append + +- findings and evidence +- questions +- failures and abandoned approaches +- conflicts +- handoffs +- suggested improvements +- technical debt + +### Lead-only (or human task owner) + +- task scope or non-goals +- task lifecycle status +- assignments and ownership +- confirmed decisions +- blocker disposition +- conflict resolution status +- workspace closure and sealing + +Prefer `role: lead` (and `decision-owner` where applicable) on privileged +records. `kb-check` flags confirmatory decisions/scope-changes that lack lead +ownership metadata. + +## Conflict protocol + +Conflicting findings are recorded, never overwritten. When a conflict pertains +to current work, address it in-session. The lead records one of: + +- `resolved` — evidence or human decision establishes the controlling conclusion +- `deferred` — real but outside the bounded task; record why and where next +- `blocked` — cannot safely continue; add the blocker to `TASK.md` (surfaces in + `ACTIVE.md`) + +Unrelated conflicts remain recorded and do not block the current assignment. + +## AGENTS.md installation snippet + +Provide this (or equivalent) when initializing KBB in a repository: + +```markdown +## KB-Brain + +Use the `kb-brain` skill for section routing, templates, workspace lifecycle, and write rules. + +Every PAAD-managed task uses a focused shared workspace under: + +`kb-brain/work/active/<task-id>/` + +Repository workspace level: `standard` + +Supported levels: + +- `minimal` — scope, status, ownership, handoff, and durable findings +- `standard` — findings, questions, decisions, failures, conflicts, and handoffs +- `strict` — standard plus evidence, assumptions, dependencies, ownership, and scope-change tracking + +A task may raise its level. Lowering below the repository level requires explicit human approval. + +Sub-agents may append findings, evidence, questions, failures, conflicts, and handoffs. + +Only the lead agent or human task owner may change task scope, lifecycle status, assignments, blockers, confirmed decisions, or conflict resolution state. + +Conflicting findings must be recorded rather than overwritten. Address conflicts during the session when they affect current work. + +Agents may autonomously: + +- add one file per gap under `kb-brain/improvements/`; +- track smoothing or remediation work under `kb-brain/tech-debt/`; +- update `kb-brain/tech-debt/LEDGER.md`; +- move resolved debt into `kb-brain/tech-debt/closed/`; +- promote durable task knowledge into the appropriate permanent KBB section. + +Use the provided templates for human- and agent-authored records. + +Bulk content ingress from existing documentation is a dedicated operation. Do not copy architecture documentation into KBB unprompted. + +Closed workspaces are immutable. Add corrections or later findings as explicit amendments. +``` diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/references/structure.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/references/structure.md new file mode 100644 index 0000000..44f6cd0 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/references/structure.md @@ -0,0 +1,121 @@ +# KB-Brain structure + +## Top-level layout + +```text +kb-brain/ +├── README.md +├── INDEX.md +├── decisions/ +├── architecture/ +├── domains/ +├── runbooks/ +├── gotchas/ +├── briefs/ +├── specs/ +├── plans/ +├── reviews/ +├── learnings/ +├── open-questions/ +├── agents/ +├── improvements/ +├── tech-debt/ +│ ├── LEDGER.md +│ └── closed/ +├── templates/ +└── work/ + ├── ACTIVE.md + ├── active/ + └── closed/ +``` + +Do not create empty atomic records during init. + +## Permanent section meanings + +| Section | Meaning | +|---------|---------| +| `decisions/` | Accepted ADR-style decisions with owner and evidence | +| `architecture/` | Mutable notes refining or challenging stable `docs/` — always link back | +| `domains/` | Product and business domain knowledge | +| `runbooks/` | Recurring procedures not yet stable documentation | +| `gotchas/` | Sharp edges, false starts, reliable warnings | +| `briefs/` | Human-owned briefs and atomic milestone records | +| `specs/` | Active working specifications, including candidate milestone expansions | +| `plans/` | Implementation plans tied to approved specs | +| `reviews/` | Review outputs worth preserving beyond their session | +| `learnings/` | Post-hoc lessons and retrospectives | +| `open-questions/` | Unresolved questions; agents may add evidence, never invent answers | +| `agents/` | Repository-specific agent behaviour and coordination notes | +| `improvements/` | One atomic file per noticed gap or opportunity | +| `tech-debt/` | Open debt + `LEDGER.md`; resolved entries move to `closed/` | + +`docs/` is the stable architecture / accepted documentation source of truth. +Do not duplicate it under `kb-brain/`. + +## Workspace levels + +Repository default is selected in `AGENTS.md` (recommended `standard`). A task +may raise its level. Lowering below the repository default requires explicit +human approval. + +### minimal + +Required: task scope and status, compact context, ownership, final handoff, +durable findings selected for promotion. + +### standard (default) + +Adds: assignments, findings and evidence, questions, failures and abandoned +approaches, confirmed decisions, conflicts, handoffs, promotion tracking. + +### strict + +Adds: explicit assumptions, dependency records, decision ownership, scope-change +records, mandatory evidence references, detailed conflict handling, completion +and promotion checks. + +All levels use focused files and atomic record directories — never a single +append-only workspace journal. + +## Active workspace layout + +```text +kb-brain/work/active/<task-id>/ +├── TASK.md +├── INDEX.md +├── CONTEXT.md +├── ASSIGNMENTS.md # standard + strict +├── PROMOTION.md +├── decisions/ +├── findings/ +├── questions/ +├── failures/ +├── conflicts/ +├── handoffs/ +├── assumptions/ # strict +├── dependencies/ # strict +└── scope-changes/ # strict +``` + +Task ID format: `YYYY-MM-DD-<short-slug>`. On collision append `-2`, `-3`, … + +## File responsibilities + +- **`TASK.md`** — authoritative task card: objective, scope, non-goals, + completion criteria, workspace level, lead owner, lifecycle status, current + focus, blockers, controlling brief/spec/issue/plan links. +- **`INDEX.md`** — generated. Lists atomic records by type, status, owner, and + relationship. Marks amendments on closed workspaces. +- **`CONTEXT.md`** — compact shared working memory. Keep brief; link detail. +- **`ASSIGNMENTS.md`** — lead-controlled assignments, boundaries, dependencies, + status. +- **`PROMOTION.md`** — durable information that must move to permanent KBB + sections or stable `docs/` before closure. + +## ACTIVE.md + +`kb-brain/work/ACTIVE.md` is generated from task workspace metadata. It +contains only: active task ID and link, status, workspace level, lead/owner, +objective, current focus, blockers. **Do not list conflicts.** Do not hand-edit +narrative that can drift — regenerate via `kb_brain.py index`. diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/references/templates.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/references/templates.md new file mode 100644 index 0000000..8d7f759 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/references/templates.md @@ -0,0 +1,121 @@ +# Templates and IDs + +Distributable templates ship in this skill's `templates/` directory and are +copied to `kb-brain/templates/` on `init`. Humans and agents use the same +files. + +## Common frontmatter + +```yaml +--- +id: F-001 +type: finding +status: open +author: agent-parser-review +owner: task-lead +created: 2026-08-04 +updated: 2026-08-04 +related: + - questions/Q-002-import-order.md + - src/parser/imports.ts +--- +``` + +Required: `id`, `type`, `status`, `author`, `created`, `updated`. + +Add `owner`, `related`, `evidence`, `supersedes`, `amends`, `decision-owner`, +or `role: lead` where applicable. + +## ID prefixes + +Allocate the next numeric ID by inspecting the relevant directory — no shared +mutable counter file. + +| Kind | Prefix | +|------|--------| +| Decision | `D-` | +| Finding | `F-` | +| Question | `Q-` | +| Failure | `X-` | +| Conflict | `C-` | +| Handoff | `H-` | +| Improvement | `I-` | +| Technical debt | `TD-` | +| Assumption | `A-` | +| Dependency | `DEP-` | +| Scope change | `SC-` | +| Amendment | `AM-` | +| Milestone | `M-` | + +## Required templates + +| File | Purpose | +|------|---------| +| `TASK.md` | Task card | +| `CONTEXT.md` | Compact shared context | +| `ASSIGNMENTS.md` | Lead-controlled assignments | +| `finding.md` | Finding / evidence | +| `decision.md` | Accepted decision | +| `question.md` | Open question | +| `failure.md` | Failure / abandoned approach | +| `conflict.md` | Conflicting positions | +| `handoff.md` | Handoff | +| `improvement.md` | Out-of-scope opportunity | +| `tech-debt.md` | Remediation liability | +| `assumption.md` | Strict-level assumption | +| `dependency.md` | Strict-level dependency | +| `scope-change.md` | Strict-level scope change | +| `PROMOTION.md` | Promotion tracking | +| `closeout.md` | Closeout | +| `amendment.md` | Post-seal correction | +| `BRIEF.md` | Human project brief | +| `MILESTONE.md` | Atomic milestone | +| `MILESTONE-SPEC.md` | Candidate milestone specification | + +## Improvement body shape + +```markdown +# <Improvement title> + +## Observed during +... + +## Observation +... + +## Evidence +... + +## Why it matters +... + +## Possible direction +... + +## Relationship to current work +... +``` + +## Conflict body shape + +```markdown +# <Conflict title> + +## Position A +... + +## Position B +... + +## Evidence +... + +## Impact +... + +## Required resolution +... + +## Resolution +unresolved | resolved | deferred | blocked +``` diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/scripts/kb_brain.py b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/scripts/kb_brain.py new file mode 100644 index 0000000..4130f65 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/scripts/kb_brain.py @@ -0,0 +1,2227 @@ +#!/usr/bin/env python3 +"""KB-Brain: deterministic scaffold, index, check, seal, and amend tooling. + +Stdlib only. Designed to be copied into a target repository's scripts/ +directory during `init`. Safe to re-run; fails clearly without destructive +partial changes where practical. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import shutil +import sys +from datetime import date, datetime, timezone +from pathlib import Path +from typing import Any, Iterable + +VERSION = "1.0.0" + +TOP_SECTIONS = [ + "decisions", + "architecture", + "domains", + "runbooks", + "gotchas", + "briefs", + "specs", + "plans", + "reviews", + "learnings", + "open-questions", + "agents", + "improvements", + "tech-debt", + "templates", + "work", +] + +WORKSPACE_LEVELS = ("minimal", "standard", "strict") +DEFAULT_LEVEL = "standard" + +# Atomic record prefixes and their default directories (relative to a workspace +# or permanent section root, depending on type). +PREFIXES = { + "decision": "D", + "finding": "F", + "question": "Q", + "failure": "X", + "conflict": "C", + "handoff": "H", + "improvement": "I", + "tech-debt": "TD", + "assumption": "A", + "dependency": "DEP", + "scope-change": "SC", + "amendment": "AM", + "milestone": "M", +} + +SECTION_TO_TYPE = { + "decisions": "decision", + "findings": "finding", + "questions": "question", + "failures": "failure", + "conflicts": "conflict", + "handoffs": "handoff", + "improvements": "improvement", + "tech-debt": "tech-debt", + "assumptions": "assumption", + "dependencies": "dependency", + "scope-changes": "scope-change", + "amendments": "amendment", + "open-questions": "question", +} + +VALID_STATUSES = { + "open", + "closed", + "resolved", + "deferred", + "blocked", + "unresolved", + "accepted", + "rejected", + "superseded", + "active", + "done", + "promoted", + "abandoned", + "review-needed", + "approved-spec", + "brief", + "incubating", + "ready-for-expansion", + "expanding", + "planned", + "in-progress", + "completed", +} + +SECRET_PATTERNS = [ + re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"), + re.compile(r"(?i)api[_-]?key\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{20,}"), + re.compile(r"(?i)secret\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{16,}"), + re.compile(r"(?i)password\s*[:=]\s*['\"]?[^\s'\"]{8,}"), + re.compile(r"ghp_[A-Za-z0-9]{36}"), + re.compile(r"sk-[A-Za-z0-9]{20,}"), + re.compile(r"xox[baprs]-[A-Za-z0-9-]{10,}"), +] + +FRONTMATTER_RE = re.compile(r"\A---\r?\n(.*?)\r?\n---\r?\n?", re.DOTALL) +ID_RE = re.compile(r"^[A-Z]+-\d+$") +SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") + +MAKEFILE_BLOCK_BEGIN = "# >>> kb-brain targets (managed by kb_brain.py; do not edit by hand)" +MAKEFILE_BLOCK_END = "# <<< kb-brain targets" + +MAKEFILE_BLOCK = f"""{MAKEFILE_BLOCK_BEGIN} +.PHONY: kb-index kb-new kb-check kb-start kb-close kb-amend + +kb-index: +\tpython3 scripts/kb_brain.py index + +kb-new: +\tpython3 scripts/kb_brain.py new "$(SECTION)" "$(TITLE)" + +kb-check: +\tpython3 scripts/kb_brain.py check + +kb-start: +\tpython3 scripts/kb_brain.py start "$(SLUG)" "$(LEVEL)" + +kb-close: +\tpython3 scripts/kb_brain.py close "$(TASK)" + +kb-amend: +\tpython3 scripts/kb_brain.py amend "$(TASK)" "$(RECORD)" "$(TITLE)" +{MAKEFILE_BLOCK_END} +""" + +README_BODY = """# KB-Brain + +Repository-native working knowledge shared by humans, lead agents, and +sub-agents. Stable architecture and accepted documentation live under `docs/`. +This tree holds mutable context: decisions in flight, gaps, debt, lessons, and +focused task workspaces. + +## Index-first retrieval + +Do not read the entire KB by default. Normal order: + +1. `work/ACTIVE.md` +2. the selected workspace's `TASK.md` +3. `INDEX.md` (workspace, then repository) +4. `CONTEXT.md` +5. the agent's assignment +6. only linked or relevant atomic records + +## Permanent sections + +| Section | Meaning | +|---------|---------| +| `decisions/` | Accepted ADR-style decisions with owner and evidence | +| `architecture/` | Mutable notes that refine or challenge stable `docs/` architecture — always link back | +| `domains/` | Product and business domain knowledge | +| `runbooks/` | Recurring procedures not yet promoted to stable docs | +| `gotchas/` | Sharp edges, false starts, reliable warnings | +| `briefs/` | Human-owned briefs and atomic milestone records | +| `specs/` | Active working specifications, including candidate milestone expansions | +| `plans/` | Implementation plans tied to approved specs | +| `reviews/` | Review outputs worth preserving beyond their session | +| `learnings/` | Post-hoc lessons and retrospectives | +| `open-questions/` | Unresolved questions — agents may add evidence, never invent answers | +| `agents/` | Repository-specific agent behaviour and coordination notes | +| `improvements/` | One atomic file per noticed gap or opportunity | +| `tech-debt/` | Open debt plus `LEDGER.md`; resolved entries move to `tech-debt/closed/` | +| `templates/` | Project-local copies of record templates | +| `work/` | Active and closed task workspaces | + +## Workspace levels + +Configured in `AGENTS.md` (default `standard`). A task may raise its level; +lowering below the repository default requires explicit human approval. + +- **minimal** — scope, status, ownership, handoff, durable findings +- **standard** — findings, questions, decisions, failures, conflicts, handoffs +- **strict** — standard plus assumptions, dependencies, ownership, scope-change tracking + +## Closure and amendments + +Closed workspaces under `work/closed/` are immutable. Corrections go in +`amendments/` and are marked in regenerated indexes. See `SEAL.json`. + +## Tooling + +```bash +make kb-index +make kb-new SECTION=improvements TITLE="Example gap" +make kb-check +make kb-start SLUG=auth-migration LEVEL=strict +make kb-close TASK=2026-08-04-auth-migration +make kb-amend TASK=2026-08-04-auth-migration RECORD=findings/F-001-parser.md TITLE="Clarify parser note" +``` + +Or call `python3 scripts/kb_brain.py --help`. + +Bulk ingress from `docs/` is a dedicated operation — never dump architecture +documents into this tree during ordinary work. +""" + + +# --------------------------------------------------------------------------- +# Frontmatter (stdlib YAML subset) +# --------------------------------------------------------------------------- + +def parse_frontmatter(text: str) -> tuple[dict[str, Any], str]: + """Parse a minimal YAML frontmatter block into a dict and body.""" + match = FRONTMATTER_RE.match(text) + if not match: + return {}, text + meta: dict[str, Any] = {} + lines = match.group(1).splitlines() + i = 0 + while i < len(lines): + line = lines[i] + if not line.strip() or line.strip().startswith("#"): + i += 1 + continue + if ":" not in line: + i += 1 + continue + key, _, raw = line.partition(":") + key = key.strip() + value = raw.strip() + if value in ("", "|", ">"): + # Multi-line scalar or empty — collect indented follow-ons as list/string + items: list[str] = [] + i += 1 + while i < len(lines) and (lines[i].startswith(" ") or lines[i].startswith("\t") or lines[i].startswith("- ")): + item = lines[i].strip() + if item.startswith("- "): + items.append(item[2:].strip().strip("'\"")) + else: + items.append(item.strip("'\"")) + i += 1 + meta[key] = items if items else "" + continue + if value.startswith("[") and value.endswith("]"): + inner = value[1:-1].strip() + meta[key] = [p.strip().strip("'\"") for p in inner.split(",") if p.strip()] if inner else [] + elif value.lower() in ("true", "false"): + meta[key] = value.lower() == "true" + elif re.fullmatch(r"-?\d+", value): + meta[key] = int(value) + else: + meta[key] = value.strip("'\"").strip('"') + i += 1 + body = text[match.end() :] + return meta, body + + +def dump_frontmatter(meta: dict[str, Any], body: str = "") -> str: + lines = ["---"] + for key, value in meta.items(): + if isinstance(value, list): + if not value: + lines.append(f"{key}: []") + else: + lines.append(f"{key}:") + for item in value: + lines.append(f" - {item}") + elif isinstance(value, bool): + lines.append(f"{key}: {'true' if value else 'false'}") + else: + lines.append(f"{key}: {value}") + lines.append("---") + lines.append("") + if body and not body.startswith("\n"): + return "\n".join(lines) + body.lstrip("\n") + return "\n".join(lines) + (body or "") + + +# --------------------------------------------------------------------------- +# Path helpers +# --------------------------------------------------------------------------- + +def repo_root_from_cwd(start: Path | None = None) -> Path: + """Walk up looking for kb-brain/ or .git; fall back to cwd.""" + cur = (start or Path.cwd()).resolve() + for candidate in [cur, *cur.parents]: + if (candidate / "kb-brain").is_dir() or (candidate / ".git").exists(): + return candidate + return cur + + +def kb_root(root: Path) -> Path: + return root / "kb-brain" + + +def skill_dir() -> Path: + """Directory that owns templates/ — skill package or project kb-brain/.""" + here = Path(__file__).resolve().parent + # Skill package: .../kb-brain/scripts/kb_brain.py + if (here.parent / "templates").is_dir() or (here.parent / "SKILL.md").exists(): + return here.parent + # After init the script lives at <root>/scripts/kb_brain.py + root = here.parent + if (root / "kb-brain" / "templates").is_dir(): + return root / "kb-brain" + return here.parent + + +def today() -> str: + return date.today().isoformat() + + +def slugify(text: str) -> str: + text = text.strip().lower() + text = re.sub(r"[^a-z0-9]+", "-", text) + text = re.sub(r"-+", "-", text).strip("-") + return text or "item" + + +def ensure_dir(path: Path) -> None: + path.mkdir(parents=True, exist_ok=True) + + +def write_if_absent(path: Path, content: str) -> bool: + """Write content only if the file does not exist. Returns True if written.""" + if path.exists(): + return False + ensure_dir(path.parent) + path.write_text(content, encoding="utf-8") + return True + + +def write_text(path: Path, content: str) -> None: + ensure_dir(path.parent) + path.write_text(content, encoding="utf-8") + + +def read_text(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def sha256_file(path: Path) -> str: + h = hashlib.sha256() + with path.open("rb") as fh: + for chunk in iter(lambda: fh.read(65536), b""): + h.update(chunk) + return h.hexdigest() + + +def relative_to_kb(path: Path, root: Path) -> str: + try: + return str(path.resolve().relative_to(kb_root(root).resolve())) + except ValueError: + return str(path) + + +# --------------------------------------------------------------------------- +# Templates +# --------------------------------------------------------------------------- + +TEMPLATE_FILES = { + "TASK.md": """--- +id: {task_id} +type: task +status: active +level: {level} +lead: {lead} +author: {author} +created: {created} +updated: {created} +related: [] +--- + +# {title} + +## Objective +{objective} + +## Scope +- + +## Non-goals +- + +## Completion criteria +- + +## Current focus +- + +## Blockers +- + +## Controlling links +- brief: +- spec: +- issue: +- plan: +""", + "CONTEXT.md": """--- +id: {task_id}-context +type: context +status: active +author: {author} +created: {created} +updated: {created} +--- + +# Shared context + +Keep this brief. Move detail into atomic records and link them. + +## What matters right now +- + +## Pointers +- +""", + "ASSIGNMENTS.md": """--- +id: {task_id}-assignments +type: assignments +status: active +author: {author} +owner: {lead} +role: lead +created: {created} +updated: {created} +--- + +# Assignments + +Lead-controlled. Record each human or agent assignment, boundary, dependencies, and status. + +| Agent / human | Boundary | Depends on | Status | +|---------------|----------|------------|--------| +| | | | | +""", + "PROMOTION.md": """--- +id: {task_id}-promotion +type: promotion +status: open +author: {author} +created: {created} +updated: {created} +--- + +# Promotion tracking + +Durable information that must move into permanent KBB sections or stable `docs/` before closure. + +| Record | Destination | Status | +|--------|-------------|--------| +| | | | +""", + "finding.md": """--- +id: {id} +type: finding +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +evidence: [] +--- + +# {title} + +## Observation +- + +## Evidence +- + +## Impact +- +""", + "decision.md": """--- +id: {id} +type: decision +status: accepted +author: {author} +owner: {owner} +decision-owner: {owner} +role: lead +created: {created} +updated: {created} +related: [] +evidence: [] +--- + +# {title} + +## Context +- + +## Decision +- + +## Rationale +- + +## Evidence +- + +## Consequences +- +""", + "question.md": """--- +id: {id} +type: question +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +evidence: [] +--- + +# {title} + +## Question +- + +## Why it matters +- + +## Evidence so far +- + +## Answer +Unanswered. Do not invent an answer. +""", + "failure.md": """--- +id: {id} +type: failure +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +evidence: [] +--- + +# {title} + +## What was tried +- + +## Why it failed +- + +## Evidence +- + +## Lesson +- +""", + "conflict.md": """--- +id: {id} +type: conflict +status: unresolved +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +evidence: [] +--- + +# {title} + +## Position A +- + +## Position B +- + +## Evidence +- + +## Impact +- + +## Required resolution +- + +## Resolution +unresolved +""", + "handoff.md": """--- +id: {id} +type: handoff +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +--- + +# {title} + +## From +- + +## To +- + +## State of play +- + +## Next actions +- + +## Blockers +- +""", + "improvement.md": """--- +id: {id} +type: improvement +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +evidence: [] +--- + +# {title} + +## Observed during +- + +## Observation +- + +## Evidence +- + +## Why it matters +- + +## Possible direction +- + +## Relationship to current work +- +""", + "tech-debt.md": """--- +id: {id} +type: tech-debt +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +evidence: [] +--- + +# {title} + +## Liability +- + +## Evidence +- + +## Suggested remediation +- + +## Urgency +- +""", + "assumption.md": """--- +id: {id} +type: assumption +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +evidence: [] +--- + +# {title} + +## Assumption +- + +## If wrong +- + +## Validation needed +- +""", + "dependency.md": """--- +id: {id} +type: dependency +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +--- + +# {title} + +## Depends on +- + +## Needed by +- + +## Status +- +""", + "scope-change.md": """--- +id: {id} +type: scope-change +status: open +author: {author} +owner: {owner} +role: lead +created: {created} +updated: {created} +related: [] +--- + +# {title} + +## Change +- + +## Why +- + +## Approved by +- +""", + "closeout.md": """--- +id: {task_id}-closeout +type: closeout +status: done +author: {author} +owner: {lead} +role: lead +created: {created} +updated: {created} +--- + +# Closeout — {task_id} + +## Outcome +- + +## What was promoted +- + +## What was not promoted (and why) +- + +## Remaining risks +- + +## Follow-ups +- +""", + "amendment.md": """--- +id: {id} +type: amendment +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +amends: {amends} +related: [] +evidence: [] +--- + +# {title} + +## What this corrects +- + +## Why +- + +## New evidence +- + +## Impact +- +""", + "BRIEF.md": """--- +id: brief-{slug} +type: brief +status: active +author: {author} +owner: {owner} +created: {created} +updated: {created} +--- + +# {title} + +## Intended outcome +- + +## Users and stakeholders +- + +## Why it matters +- + +## Constraints +- + +## Non-goals +- + +## Success at project level +- + +## Known milestone ideas +- + +## Open questions +- +""", + "MILESTONE.md": """--- +id: {id} +type: milestone +status: brief +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +--- + +# {title} + +## Outcome +- + +## Notes +- +""", + "MILESTONE-SPEC.md": """--- +id: {id}-spec +type: milestone-spec +status: review-needed +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +--- + +# {title} + +## Status +review-needed + +## Brief linkage +- + +## Outcome +- + +## Scope +- + +## Non-goals +- + +## User and system behaviour +- + +## Constraints and controlling decisions +- + +## Components and boundaries +- + +## Data and interface implications +- + +## Failure and recovery behaviour +- + +## Security, privacy, accessibility, and operational concerns +- + +## Dependencies +- + +## Acceptance criteria +- + +## Testing expectations +- + +## Open questions +- + +## Assumptions +- + +## Evidence consulted +- + +## Human approval +Unapproved candidate specification. +""", +} + + +def bundled_templates_dir() -> Path: + return skill_dir() / "templates" + + +def materialize_bundled_templates() -> None: + """Ensure skill-package templates/ exists with the canonical set.""" + dest = bundled_templates_dir() + ensure_dir(dest) + for name, content in TEMPLATE_FILES.items(): + path = dest / name + if not path.exists(): + # Store unsubstituted exemplars for humans/agents to copy. + exemplar = content + for key, sample in { + "{task_id}": "YYYY-MM-DD-slug", + "{level}": "standard", + "{lead}": "task-lead", + "{author}": "author", + "{owner}": "owner", + "{created}": "YYYY-MM-DD", + "{title}": "Title", + "{objective}": "Objective", + "{id}": "F-001", + "{slug}": "slug", + "{amends}": "path/to/record.md", + }.items(): + exemplar = exemplar.replace(key, sample) + write_text(path, exemplar) + + +def copy_templates_to_project(root: Path) -> None: + materialize_bundled_templates() + dest = kb_root(root) / "templates" + ensure_dir(dest) + for src in bundled_templates_dir().iterdir(): + if src.is_file(): + target = dest / src.name + if not target.exists(): + shutil.copy2(src, target) + + +def render_template(name: str, **kwargs: str) -> str: + materialize_bundled_templates() + # Prefer in-memory TEMPLATE_FILES for substitution fidelity. + if name in TEMPLATE_FILES: + text = TEMPLATE_FILES[name] + else: + text = read_text(bundled_templates_dir() / name) + for key, value in kwargs.items(): + text = text.replace("{" + key + "}", value) + # Leave any unused placeholders as-is only if they look intentional; + # replace common leftovers with empty/defaults. + return text + + +# --------------------------------------------------------------------------- +# Init +# --------------------------------------------------------------------------- + +def cmd_init(root: Path, level: str) -> int: + if level not in WORKSPACE_LEVELS: + print(f"FAIL: level must be one of {', '.join(WORKSPACE_LEVELS)}", file=sys.stderr) + return 2 + + kb = kb_root(root) + ensure_dir(kb) + + # Top-level sections — empty dirs with .gitkeep only (no atomic records). + for section in TOP_SECTIONS: + ensure_dir(kb / section) + + ensure_dir(kb / "tech-debt" / "closed") + write_if_absent( + kb / "tech-debt" / "LEDGER.md", + "# Technical debt ledger\n\n| ID | Title | Status | Path |\n|----|-------|--------|------|\n", + ) + + ensure_dir(kb / "work" / "active") + ensure_dir(kb / "work" / "closed") + + write_if_absent(kb / "README.md", README_BODY) + write_if_absent( + kb / "INDEX.md", + "# KB-Brain index\n\n_Generated. Run `make kb-index` or `python3 scripts/kb_brain.py index`._\n", + ) + write_if_absent( + kb / "work" / "ACTIVE.md", + "# Active work\n\n_No active KB-Brain workspaces._\n", + ) + + # Default level marker (read by start when AGENTS.md has no override). + write_if_absent( + kb / ".default-level", + level + "\n", + ) + + copy_templates_to_project(root) + install_script(root) + install_makefile_targets(root) + regenerate_repo_index(root) + regenerate_active(root) + + print(f"Initialized kb-brain/ at {kb} (default level: {level})") + print("Did not ingest docs/ or other existing documentation.") + return 0 + + +def install_script(root: Path) -> None: + src = Path(__file__).resolve() + dest_dir = root / "scripts" + ensure_dir(dest_dir) + dest = dest_dir / "kb_brain.py" + if dest.resolve() == src.resolve(): + return + shutil.copy2(src, dest) + dest.chmod(dest.stat().st_mode | 0o111) + + +def install_makefile_targets(root: Path) -> None: + makefile = root / "Makefile" + if not makefile.exists(): + inc = kb_root(root) / "Makefile.inc" + write_text( + inc, + f"# Include from your Makefile with: include kb-brain/Makefile.inc\n{MAKEFILE_BLOCK}\n", + ) + print(f"No Makefile found — wrote {inc.relative_to(root)} (include it from your build file)") + return + + text = read_text(makefile) + if MAKEFILE_BLOCK_BEGIN in text: + # Replace existing managed block idempotently. + pattern = re.compile( + re.escape(MAKEFILE_BLOCK_BEGIN) + r".*?" + re.escape(MAKEFILE_BLOCK_END), + re.DOTALL, + ) + text = pattern.sub(MAKEFILE_BLOCK.strip(), text) + write_text(makefile, text if text.endswith("\n") else text + "\n") + return + + separator = "" if text.endswith("\n") else "\n" + write_text(makefile, text + separator + "\n" + MAKEFILE_BLOCK + "\n") + + +def read_default_level(root: Path) -> str: + """Resolve repository default workspace level.""" + agents = root / "AGENTS.md" + if agents.exists(): + text = read_text(agents) + m = re.search( + r"Repository workspace level:\s*`?(minimal|standard|strict)`?", + text, + re.IGNORECASE, + ) + if m: + return m.group(1).lower() + marker = kb_root(root) / ".default-level" + if marker.exists(): + value = read_text(marker).strip().lower() + if value in WORKSPACE_LEVELS: + return value + return DEFAULT_LEVEL + + +# --------------------------------------------------------------------------- +# Workspace start +# --------------------------------------------------------------------------- + +def allocate_task_id(root: Path, slug: str) -> str: + slug = slugify(slug) + base = f"{today()}-{slug}" + active = kb_root(root) / "work" / "active" + closed = kb_root(root) / "work" / "closed" + existing = set() + for parent in (active, closed): + if parent.is_dir(): + existing.update(p.name for p in parent.iterdir() if p.is_dir()) + if base not in existing: + return base + n = 2 + while f"{base}-{n}" in existing: + n += 1 + return f"{base}-{n}" + + +def required_workspace_dirs(level: str) -> list[str]: + base = ["decisions", "findings", "questions", "failures", "conflicts", "handoffs"] + if level == "minimal": + return ["findings", "handoffs"] + if level == "strict": + return base + ["assumptions", "dependencies", "scope-changes"] + return base + + +def required_workspace_files(level: str) -> list[str]: + files = ["TASK.md", "INDEX.md", "CONTEXT.md", "PROMOTION.md"] + if level in ("standard", "strict"): + files.append("ASSIGNMENTS.md") + return files + + +def cmd_start(root: Path, slug: str, level: str | None, lead: str, author: str) -> int: + if not kb_root(root).is_dir(): + print("FAIL: kb-brain/ not found — run init first", file=sys.stderr) + return 2 + + repo_level = read_default_level(root) + chosen = (level or repo_level).lower() + if chosen not in WORKSPACE_LEVELS: + print(f"FAIL: level must be one of {', '.join(WORKSPACE_LEVELS)}", file=sys.stderr) + return 2 + + level_rank = {name: i for i, name in enumerate(WORKSPACE_LEVELS)} + if level_rank[chosen] < level_rank[repo_level]: + print( + f"FAIL: lowering workspace level below repository default " + f"({repo_level} → {chosen}) requires explicit human approval. " + f"Pass the repo default or higher, or obtain approval and re-run " + f"with --force-lower after documenting the approval.", + file=sys.stderr, + ) + return 2 + + task_id = allocate_task_id(root, slug) + ws = kb_root(root) / "work" / "active" / task_id + if ws.exists(): + print(f"FAIL: workspace already exists: {ws}", file=sys.stderr) + return 2 + + ensure_dir(ws) + created = today() + title = slug.replace("-", " ").strip().title() or task_id + + write_text( + ws / "TASK.md", + render_template( + "TASK.md", + task_id=task_id, + level=chosen, + lead=lead, + author=author, + created=created, + title=title, + objective=title, + ), + ) + write_text( + ws / "CONTEXT.md", + render_template( + "CONTEXT.md", + task_id=task_id, + author=author, + created=created, + ), + ) + write_text( + ws / "PROMOTION.md", + render_template( + "PROMOTION.md", + task_id=task_id, + author=author, + created=created, + ), + ) + if chosen in ("standard", "strict"): + write_text( + ws / "ASSIGNMENTS.md", + render_template( + "ASSIGNMENTS.md", + task_id=task_id, + author=author, + lead=lead, + created=created, + ), + ) + + for dirname in required_workspace_dirs(chosen): + ensure_dir(ws / dirname) + gitkeep = ws / dirname / ".gitkeep" + write_if_absent(gitkeep, "") + + regenerate_workspace_index(ws) + regenerate_active(root) + regenerate_repo_index(root) + + print(f"Started workspace {task_id} (level={chosen}) at {ws.relative_to(root)}") + return 0 + + +# --------------------------------------------------------------------------- +# Record creation +# --------------------------------------------------------------------------- + +def next_id(directory: Path, prefix: str) -> str: + highest = 0 + if directory.is_dir(): + for path in directory.iterdir(): + if not path.is_file() or path.name.startswith("."): + continue + meta, _ = parse_frontmatter(read_text(path)) + rid = str(meta.get("id", "")) + m = re.match(rf"^{re.escape(prefix)}-(\d+)$", rid) + if m: + highest = max(highest, int(m.group(1))) + else: + m2 = re.match(rf"^{re.escape(prefix)}-(\d+)", path.stem) + if m2: + highest = max(highest, int(m2.group(1))) + return f"{prefix}-{highest + 1:03d}" + + +def resolve_section_dir(root: Path, section: str, task_id: str | None) -> tuple[Path, str]: + """Return (directory, record_type) for a section name.""" + section = section.strip().strip("/") + if section in SECTION_TO_TYPE: + rtype = SECTION_TO_TYPE[section] + elif section in PREFIXES: + rtype = section + # Map type back to plural dir when needed + plural = { + "decision": "decisions", + "finding": "findings", + "question": "questions", + "failure": "failures", + "conflict": "conflicts", + "handoff": "handoffs", + "improvement": "improvements", + "tech-debt": "tech-debt", + "assumption": "assumptions", + "dependency": "dependencies", + "scope-change": "scope-changes", + "amendment": "amendments", + }.get(section, section) + section = plural + else: + raise ValueError(f"unknown section '{section}'") + + if section in ("improvements", "tech-debt", "open-questions", "decisions", "gotchas", "learnings"): + # Permanent sections (and decisions may also be workspace-local) + if task_id and section == "decisions": + ws = find_workspace(root, task_id) + if ws is None: + raise ValueError(f"task not found: {task_id}") + return ws / "decisions", rtype + if section == "open-questions": + return kb_root(root) / "open-questions", rtype + return kb_root(root) / section, rtype + + # Workspace-local sections + if not task_id: + active = list_active_workspaces(root) + if len(active) == 1: + task_id = active[0].name + elif not active: + raise ValueError("no active workspace — pass --task or start one") + else: + raise ValueError("multiple active workspaces — pass --task <id>") + + ws = find_workspace(root, task_id) + if ws is None: + raise ValueError(f"task not found: {task_id}") + return ws / section, rtype + + +def find_workspace(root: Path, task_id: str) -> Path | None: + for parent in ("active", "closed"): + candidate = kb_root(root) / "work" / parent / task_id + if candidate.is_dir(): + return candidate + return None + + +def list_active_workspaces(root: Path) -> list[Path]: + active = kb_root(root) / "work" / "active" + if not active.is_dir(): + return [] + return sorted(p for p in active.iterdir() if p.is_dir()) + + +def template_name_for_type(rtype: str) -> str: + return { + "decision": "decision.md", + "finding": "finding.md", + "question": "question.md", + "failure": "failure.md", + "conflict": "conflict.md", + "handoff": "handoff.md", + "improvement": "improvement.md", + "tech-debt": "tech-debt.md", + "assumption": "assumption.md", + "dependency": "dependency.md", + "scope-change": "scope-change.md", + "amendment": "amendment.md", + "milestone": "MILESTONE.md", + }[rtype] + + +def cmd_new( + root: Path, + section: str, + title: str, + task_id: str | None, + author: str, + owner: str, +) -> int: + if not kb_root(root).is_dir(): + print("FAIL: kb-brain/ not found — run init first", file=sys.stderr) + return 2 + try: + directory, rtype = resolve_section_dir(root, section, task_id) + except ValueError as exc: + print(f"FAIL: {exc}", file=sys.stderr) + return 2 + + ensure_dir(directory) + # Remove .gitkeep once real content arrives + gitkeep = directory / ".gitkeep" + if gitkeep.exists(): + gitkeep.unlink() + + prefix = PREFIXES[rtype] + rid = next_id(directory, prefix) + slug = slugify(title)[:60] + filename = f"{rid}-{slug}.md" + path = directory / filename + if path.exists(): + print(f"FAIL: refusing to overwrite {path}", file=sys.stderr) + return 2 + + created = today() + content = render_template( + template_name_for_type(rtype), + id=rid, + title=title, + author=author, + owner=owner, + created=created, + amends="", + slug=slug, + ) + write_text(path, content) + + if rtype == "tech-debt": + update_debt_ledger(root) + + # Refresh indexes + ws = directory + while ws.name not in ("active", "closed") and ws != kb_root(root) and ws.parent != ws: + if (ws / "TASK.md").exists(): + regenerate_workspace_index(ws) + break + ws = ws.parent + regenerate_repo_index(root) + regenerate_active(root) + + print(f"Created {path.relative_to(root)}") + return 0 + + +def update_debt_ledger(root: Path) -> None: + debt_dir = kb_root(root) / "tech-debt" + rows = ["# Technical debt ledger", "", "| ID | Title | Status | Path |", "|----|-------|--------|------|"] + for path in sorted(debt_dir.glob("TD-*.md")): + meta, body = parse_frontmatter(read_text(path)) + title = _heading(body) or path.stem + rows.append( + f"| {meta.get('id', path.stem)} | {title} | {meta.get('status', '')} | {path.relative_to(kb_root(root))} |" + ) + closed = debt_dir / "closed" + if closed.is_dir(): + for path in sorted(closed.glob("TD-*.md")): + meta, body = parse_frontmatter(read_text(path)) + title = _heading(body) or path.stem + rows.append( + f"| {meta.get('id', path.stem)} | {title} | closed | {path.relative_to(kb_root(root))} |" + ) + write_text(debt_dir / "LEDGER.md", "\n".join(rows) + "\n") + + +def _heading(body: str) -> str: + for line in body.splitlines(): + if line.startswith("# "): + return line[2:].strip() + return "" + + +# --------------------------------------------------------------------------- +# Index generation +# --------------------------------------------------------------------------- + +def iter_markdown_records(directory: Path) -> Iterable[Path]: + if not directory.is_dir(): + return + for path in sorted(directory.rglob("*.md")): + if path.name in ("INDEX.md", "README.md", "LEDGER.md", "ACTIVE.md", "CLOSEOUT.md"): + continue + if path.name.startswith("."): + continue + # Skip pure template copies at kb-brain/templates + if "templates" in path.parts: + continue + yield path + + +def regenerate_workspace_index(ws: Path) -> None: + amendments: dict[str, list[str]] = {} + amend_dir = ws / "amendments" + if amend_dir.is_dir(): + for path in amend_dir.glob("*.md"): + meta, _ = parse_frontmatter(read_text(path)) + target = str(meta.get("amends", "")).strip() + if target: + amendments.setdefault(target, []).append(path.name) + + lines = [ + f"# Index — {ws.name}", + "", + "_Generated by kb_brain.py. Do not edit by hand._", + "", + ] + groups: dict[str, list[str]] = {} + for path in iter_markdown_records(ws): + if path.parent.name == "amendments": + continue + meta, body = parse_frontmatter(read_text(path)) + rtype = str(meta.get("type", path.parent.name)) + status = str(meta.get("status", "")) + owner = str(meta.get("owner", meta.get("author", ""))) + rid = str(meta.get("id", path.stem)) + title = _heading(body) or path.stem + rel = path.relative_to(ws).as_posix() + marker = "" + for key, names in amendments.items(): + if key == rel or key.endswith(path.name) or key == rid: + marker = f" **[amended: {', '.join(names)}]**" + break + groups.setdefault(rtype, []).append( + f"- `{rid}` {title} — status={status or '—'}; owner={owner or '—'}; [{rel}]({rel}){marker}" + ) + + if not groups: + lines.append("_No atomic records yet._") + else: + for rtype in sorted(groups): + lines.append(f"## {rtype}") + lines.extend(groups[rtype]) + lines.append("") + + if amendments: + lines.append("## amendments") + for path in sorted(amend_dir.glob("*.md")): + meta, body = parse_frontmatter(read_text(path)) + lines.append( + f"- `{meta.get('id', path.stem)}` {_heading(body) or path.stem} — amends `{meta.get('amends', '')}`" + ) + lines.append("") + + write_text(ws / "INDEX.md", "\n".join(lines).rstrip() + "\n") + + +def regenerate_repo_index(root: Path) -> None: + kb = kb_root(root) + lines = [ + "# KB-Brain index", + "", + "_Generated by kb_brain.py. Do not edit by hand._", + "", + "## Active workspaces", + "", + ] + active = list_active_workspaces(root) + if not active: + lines.append("_None._") + else: + for ws in active: + lines.append(f"- [{ws.name}](work/active/{ws.name}/TASK.md)") + lines.append("") + lines.append("## Permanent sections") + lines.append("") + for section in TOP_SECTIONS: + if section in ("templates", "work"): + continue + directory = kb / section + records = [p for p in iter_markdown_records(directory) if p.parent == directory or section in p.parts] + # Only top-level section files for the summary (briefs/specs have nested trees) + count = len(list(directory.rglob("*.md"))) if directory.is_dir() else 0 + if section == "tech-debt": + count = len(list(directory.glob("TD-*.md"))) + len(list((directory / "closed").glob("TD-*.md"))) if directory.is_dir() else 0 + lines.append(f"- `{section}/` — {count} markdown file(s)") + lines.append("") + write_text(kb / "INDEX.md", "\n".join(lines)) + + +def parse_task_card(ws: Path) -> dict[str, Any]: + path = ws / "TASK.md" + if not path.exists(): + return {} + meta, body = parse_frontmatter(read_text(path)) + fields = { + "id": meta.get("id", ws.name), + "status": meta.get("status", "active"), + "level": meta.get("level", DEFAULT_LEVEL), + "lead": meta.get("lead", meta.get("owner", "")), + "objective": "", + "focus": "", + "blockers": "", + } + section = None + buckets: dict[str, list[str]] = {} + for line in body.splitlines(): + if line.startswith("## "): + section = line[3:].strip().lower() + buckets[section] = [] + continue + if section is not None: + buckets[section].append(line) + def section_text(name: str) -> str: + lines = [ln for ln in buckets.get(name, []) if ln.strip() and ln.strip() != "-"] + return " ".join(ln.strip("- ").strip() for ln in lines).strip() + + fields["objective"] = section_text("objective") or _heading(body) + fields["focus"] = section_text("current focus") + fields["blockers"] = section_text("blockers") + return fields + + +def regenerate_active(root: Path) -> None: + lines = [ + "# Active work", + "", + "_Generated from workspace TASK.md metadata. Do not edit by hand._", + "", + ] + active = list_active_workspaces(root) + if not active: + lines.append("_No active KB-Brain workspaces._") + else: + for ws in active: + info = parse_task_card(ws) + lines.append(f"## [{info['id']}](active/{ws.name}/TASK.md)") + lines.append("") + lines.append(f"- **Status:** {info['status']}") + lines.append(f"- **Level:** {info['level']}") + lines.append(f"- **Lead:** {info['lead'] or '—'}") + lines.append(f"- **Objective:** {info['objective'] or '—'}") + lines.append(f"- **Current focus:** {info['focus'] or '—'}") + lines.append(f"- **Blockers:** {info['blockers'] or '—'}") + lines.append("") + write_text(kb_root(root) / "work" / "ACTIVE.md", "\n".join(lines).rstrip() + "\n") + + +def cmd_index(root: Path, path: str | None) -> int: + if not kb_root(root).is_dir(): + print("FAIL: kb-brain/ not found — run init first", file=sys.stderr) + return 2 + if path: + target = Path(path) + if not target.is_absolute(): + target = root / target + if (target / "TASK.md").exists(): + regenerate_workspace_index(target) + print(f"Indexed workspace {target}") + else: + print(f"FAIL: not a workspace path: {path}", file=sys.stderr) + return 2 + else: + for ws in list_active_workspaces(root): + regenerate_workspace_index(ws) + closed = kb_root(root) / "work" / "closed" + if closed.is_dir(): + for ws in closed.iterdir(): + if ws.is_dir(): + regenerate_workspace_index(ws) + regenerate_active(root) + regenerate_repo_index(root) + update_debt_ledger(root) + print("Regenerated KB-Brain indexes") + return 0 + + +# --------------------------------------------------------------------------- +# Check / validation +# --------------------------------------------------------------------------- + +class CheckReport: + def __init__(self) -> None: + self.errors: list[str] = [] + self.warnings: list[str] = [] + + def error(self, msg: str) -> None: + self.errors.append(msg) + + def warn(self, msg: str) -> None: + self.warnings.append(msg) + + @property + def ok(self) -> bool: + return not self.errors + + +def scan_secrets(path: Path, report: CheckReport) -> None: + try: + text = read_text(path) + except UnicodeDecodeError: + return + for pattern in SECRET_PATTERNS: + if pattern.search(text): + report.error(f"possible secret pattern in {path}: {pattern.pattern[:40]}...") + + +def check_frontmatter_record(path: Path, report: CheckReport, require_owner_for_answered: bool = True) -> dict[str, Any]: + text = read_text(path) + meta, body = parse_frontmatter(text) + if not meta: + report.error(f"{path}: missing YAML frontmatter") + return {} + for field in ("id", "type", "status", "author", "created", "updated"): + if field not in meta or meta[field] in ("", None): + report.error(f"{path}: missing required frontmatter field '{field}'") + status = str(meta.get("status", "")).lower() + if status and status not in VALID_STATUSES: + report.warn(f"{path}: unusual status '{status}'") + rid = str(meta.get("id", "")) + if rid and not ID_RE.match(rid) and not rid.startswith("brief-") and "-context" not in rid and "-assignments" not in rid and "-promotion" not in rid and "-closeout" not in rid and not rid.endswith("-spec"): + # Allow task ids like 2026-08-04-slug + if not re.match(r"^\d{4}-\d{2}-\d{2}-[a-z0-9-]+$", rid): + report.warn(f"{path}: id '{rid}' does not match expected patterns") + + # Unresolved questions must not look answered without owner/evidence + if str(meta.get("type")) == "question" and status in ("answered", "resolved", "closed"): + if require_owner_for_answered: + if not meta.get("owner") and not meta.get("decision-owner"): + report.error(f"{path}: answered question lacks owner/decision-owner") + evidence = meta.get("evidence") or [] + answer_section = "## Answer" in body + if not evidence and answer_section: + # Check answer isn't the placeholder + after = body.split("## Answer", 1)[-1] + if "Unanswered" in after.split("##", 1)[0]: + report.error(f"{path}: question marked {status} but answer is still Unanswered") + elif not evidence: + report.warn(f"{path}: answered question has no evidence links") + + # Lead-only types should carry role: lead when status is confirmatory + if str(meta.get("type")) in ("decision", "scope-change") and status in ("accepted", "resolved", "done"): + if meta.get("role") != "lead" and not meta.get("decision-owner"): + report.error(f"{path}: confirmed {meta.get('type')} lacks role: lead or decision-owner") + + # Relative links + for match in re.finditer(r"\[([^\]]*)\]\(([^)]+)\)", body): + href = match.group(2) + if href.startswith(("http://", "https://", "mailto:", "#")): + continue + target = (path.parent / href).resolve() + if not target.exists(): + report.warn(f"{path}: broken relative link ({href})") + + scan_secrets(path, report) + return meta + + +def check_workspace(ws: Path, report: CheckReport, sealed: bool = False) -> None: + task = ws / "TASK.md" + if not task.exists(): + report.error(f"{ws}: missing TASK.md") + return + meta, _ = parse_frontmatter(read_text(task)) + level = str(meta.get("level", DEFAULT_LEVEL)) + if level not in WORKSPACE_LEVELS: + report.error(f"{task}: invalid level '{level}'") + level = DEFAULT_LEVEL + + for name in required_workspace_files(level): + if not (ws / name).exists(): + report.error(f"{ws}: missing required file {name} for level={level}") + + for dirname in required_workspace_dirs(level): + if not (ws / dirname).is_dir(): + report.error(f"{ws}: missing required directory {dirname}/ for level={level}") + + ids_seen: dict[str, Path] = {} + for path in iter_markdown_records(ws): + rec_meta = check_frontmatter_record(path, report) + rid = str(rec_meta.get("id", "")) + if rid: + if rid in ids_seen: + report.error(f"duplicate id '{rid}' in {ids_seen[rid]} and {path}") + else: + ids_seen[rid] = path + + if sealed: + seal = ws / "SEAL.json" + if not seal.exists(): + report.error(f"{ws}: closed workspace missing SEAL.json") + else: + verify_seal(ws, report) + if not (ws / "CLOSEOUT.md").exists(): + report.error(f"{ws}: closed workspace missing CLOSEOUT.md") + # Amendments must point at existing sealed records + amend_dir = ws / "amendments" + if amend_dir.is_dir(): + for path in amend_dir.glob("*.md"): + ameta, _ = parse_frontmatter(read_text(path)) + target = str(ameta.get("amends", "")).strip() + if not target: + report.error(f"{path}: amendment missing 'amends'") + continue + candidate = ws / target + if not candidate.exists(): + # try by basename + matches = list(ws.rglob(Path(target).name)) + if not matches: + report.error(f"{path}: amends target not found: {target}") + + +def verify_seal(ws: Path, report: CheckReport) -> None: + seal_path = ws / "SEAL.json" + try: + seal = json.loads(read_text(seal_path)) + except json.JSONDecodeError as exc: + report.error(f"{seal_path}: invalid JSON ({exc})") + return + expected = seal.get("files", {}) + if not isinstance(expected, dict): + report.error(f"{seal_path}: 'files' must be an object") + return + + current: dict[str, str] = {} + for path in ws.rglob("*"): + if not path.is_file(): + continue + rel = path.relative_to(ws).as_posix() + if rel == "SEAL.json": + continue + if rel == "INDEX.md": + continue + if rel.startswith("amendments/"): + continue + current[rel] = sha256_file(path) + + for rel, digest in expected.items(): + if rel not in current: + report.error(f"{ws}: sealed file missing or renamed: {rel}") + elif current[rel] != digest: + report.error(f"{ws}: sealed file mutated: {rel}") + for rel in current: + if rel not in expected: + report.error(f"{ws}: unsealed new file in closed workspace (use amendments/): {rel}") + + +def cmd_check(root: Path, path: str | None) -> int: + if not kb_root(root).is_dir(): + print("FAIL: kb-brain/ not found — run init first", file=sys.stderr) + return 2 + + report = CheckReport() + kb = kb_root(root) + + for section in TOP_SECTIONS: + if not (kb / section).exists(): + report.error(f"missing top-level section: {section}/") + + # Permanent section records + for section in ("decisions", "improvements", "open-questions", "gotchas", "learnings"): + directory = kb / section + if not directory.is_dir(): + continue + ids: dict[str, Path] = {} + for path in directory.glob("*.md"): + if path.name in ("INDEX.md", "README.md"): + continue + meta = check_frontmatter_record(path, report) + rid = str(meta.get("id", "")) + if rid: + if rid in ids: + report.error(f"duplicate id '{rid}' in {ids[rid]} and {path}") + ids[rid] = path + + debt = kb / "tech-debt" + if debt.is_dir(): + if not (debt / "LEDGER.md").exists(): + report.error("tech-debt/LEDGER.md missing") + for path in list(debt.glob("TD-*.md")) + list((debt / "closed").glob("TD-*.md")): + check_frontmatter_record(path, report) + + # Workspaces + for ws in list_active_workspaces(root): + check_workspace(ws, report, sealed=False) + closed_root = kb / "work" / "closed" + if closed_root.is_dir(): + for ws in closed_root.iterdir(): + if ws.is_dir(): + check_workspace(ws, report, sealed=True) + + # ACTIVE.md consistency: blockers from TASK.md should appear + active_md = kb / "work" / "ACTIVE.md" + if active_md.exists(): + active_text = read_text(active_md) + for ws in list_active_workspaces(root): + info = parse_task_card(ws) + if info.get("blockers") and info["blockers"] not in active_text and info["blockers"] != "—": + report.warn(f"ACTIVE.md missing blockers text for {ws.name}; regenerate with index") + if "conflict" in active_text.lower() and "## conflict" in active_text.lower(): + report.error("ACTIVE.md must not list conflicts") + + if path: + target = Path(path) + if not target.is_absolute(): + target = (root / target).resolve() + # path-scoped checks already covered; keep CLI compatible + _ = target + + for warn in report.warnings: + print(f"WARN: {warn}") + for err in report.errors: + print(f"ERROR: {err}") + + if report.ok: + print("kb-check: OK") + return 0 + print(f"kb-check: {len(report.errors)} error(s), {len(report.warnings)} warning(s)") + return 1 + + +# --------------------------------------------------------------------------- +# Close / seal / amend +# --------------------------------------------------------------------------- + +def build_seal(ws: Path) -> dict[str, Any]: + files: dict[str, str] = {} + for path in sorted(ws.rglob("*")): + if not path.is_file(): + continue + rel = path.relative_to(ws).as_posix() + if rel == "SEAL.json" or rel == "INDEX.md" or rel.startswith("amendments/"): + continue + files[rel] = sha256_file(path) + return { + "version": 1, + "sealed_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "task_id": ws.name, + "files": files, + } + + +def cmd_close(root: Path, task_id: str, author: str, lead: str) -> int: + ws = kb_root(root) / "work" / "active" / task_id + if not ws.is_dir(): + print(f"FAIL: active workspace not found: {task_id}", file=sys.stderr) + return 2 + + # Require promotion tracking present + if not (ws / "PROMOTION.md").exists(): + print("FAIL: PROMOTION.md missing — complete promotion tracking before close", file=sys.stderr) + return 2 + + created = today() + if not (ws / "CLOSEOUT.md").exists(): + write_text( + ws / "CLOSEOUT.md", + render_template( + "closeout.md", + task_id=task_id, + author=author, + lead=lead, + created=created, + ), + ) + + ensure_dir(ws / "amendments") + + # Mark task closed in TASK.md + task_path = ws / "TASK.md" + meta, body = parse_frontmatter(read_text(task_path)) + meta["status"] = "closed" + meta["updated"] = created + write_text(task_path, dump_frontmatter(meta, body)) + + regenerate_workspace_index(ws) + + # Validate before sealing + report = CheckReport() + check_workspace(ws, report, sealed=False) + # Soft: don't require seal yet + if report.errors: + for err in report.errors: + # Ignore seal-related (not sealed yet) + if "SEAL.json" in err or "CLOSEOUT.md" in err: + continue + print(f"ERROR: {err}") + # Still allow close if only missing seal — but surface other errors + real = [e for e in report.errors if "SEAL.json" not in e] + if real: + print("FAIL: fix validation errors before closing", file=sys.stderr) + return 1 + + seal = build_seal(ws) + write_text(ws / "SEAL.json", json.dumps(seal, indent=2, sort_keys=True) + "\n") + + dest_parent = kb_root(root) / "work" / "closed" + ensure_dir(dest_parent) + dest = dest_parent / task_id + if dest.exists(): + print(f"FAIL: closed path already exists: {dest}", file=sys.stderr) + return 2 + shutil.move(str(ws), str(dest)) + + regenerate_active(root) + regenerate_repo_index(root) + regenerate_workspace_index(dest) + + # Final seal verification + report2 = CheckReport() + check_workspace(dest, report2, sealed=True) + if report2.errors: + for err in report2.errors: + print(f"ERROR: {err}") + print("FAIL: closed workspace failed seal validation", file=sys.stderr) + return 1 + + print(f"Closed and sealed workspace {task_id} → work/closed/{task_id}") + return 0 + + +def cmd_amend(root: Path, task_id: str, record_path: str, title: str, author: str, owner: str) -> int: + ws = kb_root(root) / "work" / "closed" / task_id + if not ws.is_dir(): + print(f"FAIL: closed workspace not found: {task_id}", file=sys.stderr) + return 2 + + target = Path(record_path) + if not target.is_absolute(): + candidate = ws / record_path + if candidate.exists(): + target = candidate + else: + matches = list(ws.rglob(Path(record_path).name)) + if len(matches) == 1: + target = matches[0] + else: + print(f"FAIL: cannot resolve record path: {record_path}", file=sys.stderr) + return 2 + + try: + rel = target.resolve().relative_to(ws.resolve()).as_posix() + except ValueError: + print("FAIL: record is not inside the closed workspace", file=sys.stderr) + return 2 + + if rel == "SEAL.json" or rel.startswith("amendments/"): + print("FAIL: cannot amend SEAL.json or another amendment via this path", file=sys.stderr) + return 2 + + ensure_dir(ws / "amendments") + rid = next_id(ws / "amendments", "AM") + slug = slugify(title)[:60] + path = ws / "amendments" / f"{rid}-{slug}.md" + content = render_template( + "amendment.md", + id=rid, + title=title, + author=author, + owner=owner, + created=today(), + amends=rel, + ) + write_text(path, content) + regenerate_workspace_index(ws) + print(f"Created amendment {path.relative_to(root)} (amends {rel})") + return 0 + + +# --------------------------------------------------------------------------- +# Brief helpers (used by brief-ruminate skill; do not approve specs) +# --------------------------------------------------------------------------- + +def cmd_brief_init(root: Path, slug: str, title: str, author: str, owner: str) -> int: + if not kb_root(root).is_dir(): + print("FAIL: kb-brain/ not found — run init first", file=sys.stderr) + return 2 + slug = slugify(slug) + brief_dir = kb_root(root) / "briefs" / slug + if brief_dir.exists() and (brief_dir / "BRIEF.md").exists(): + print(f"Brief already exists at {brief_dir.relative_to(root)}") + return 0 + ensure_dir(brief_dir / "milestones") + write_text( + brief_dir / "BRIEF.md", + render_template( + "BRIEF.md", + slug=slug, + title=title or slug.replace("-", " ").title(), + author=author, + owner=owner, + created=today(), + ), + ) + write_text( + brief_dir / "INDEX.md", + f"# Brief index — {slug}\n\n_No milestones yet._\n", + ) + regenerate_repo_index(root) + print(f"Created brief at {brief_dir.relative_to(root)}") + return 0 + + +def regenerate_brief_index(brief_dir: Path) -> None: + lines = [f"# Brief index — {brief_dir.name}", "", "| ID | Title | Status | Spec |", "|----|-------|--------|------|"] + milestones = brief_dir / "milestones" + specs_root = kb_root(brief_dir.parents[1]) / "specs" / brief_dir.name if brief_dir.parents[1].name == "kb-brain" else brief_dir.parents[2] / "specs" / brief_dir.name + # brief_dir = kb-brain/briefs/<slug> + kb = brief_dir.parent.parent + specs_root = kb / "specs" / brief_dir.name + if milestones.is_dir(): + for path in sorted(milestones.glob("M-*.md")): + meta, body = parse_frontmatter(read_text(path)) + rid = str(meta.get("id", path.stem)) + status = str(meta.get("status", "")) + title = _heading(body) or path.stem + spec = "" + if specs_root.is_dir(): + matches = list(specs_root.glob(f"{rid}-*-spec.md")) + list(specs_root.glob(f"{rid}-spec.md")) + if matches: + spec = matches[0].relative_to(kb).as_posix() + lines.append(f"| {rid} | {title} | {status} | {spec or '—'} |") + if len(lines) == 4: + lines = [f"# Brief index — {brief_dir.name}", "", "_No milestones yet._"] + write_text(brief_dir / "INDEX.md", "\n".join(lines) + "\n") + + +def cmd_brief_milestone(root: Path, brief_slug: str, title: str, author: str, owner: str) -> int: + brief_dir = kb_root(root) / "briefs" / slugify(brief_slug) + if not (brief_dir / "BRIEF.md").exists(): + print(f"FAIL: brief not found: {brief_slug}", file=sys.stderr) + return 2 + ensure_dir(brief_dir / "milestones") + rid = next_id(brief_dir / "milestones", "M") + slug = slugify(title)[:60] + path = brief_dir / "milestones" / f"{rid}-{slug}.md" + write_text( + path, + render_template( + "MILESTONE.md", + id=rid, + title=title, + author=author, + owner=owner, + created=today(), + ), + ) + regenerate_brief_index(brief_dir) + print(f"Created milestone {path.relative_to(root)}") + return 0 + + +def cmd_brief_spec(root: Path, brief_slug: str, milestone_id: str, author: str, owner: str) -> int: + """Create a candidate milestone spec with status review-needed. Never approved.""" + brief_slug = slugify(brief_slug) + brief_dir = kb_root(root) / "briefs" / brief_slug + if not (brief_dir / "BRIEF.md").exists(): + print(f"FAIL: brief not found: {brief_slug}", file=sys.stderr) + return 2 + + matches = list((brief_dir / "milestones").glob(f"{milestone_id}-*.md")) + matches += list((brief_dir / "milestones").glob(f"{milestone_id}.md")) + if not matches: + print(f"FAIL: milestone not found: {milestone_id}", file=sys.stderr) + return 2 + milestone_path = matches[0] + meta, body = parse_frontmatter(read_text(milestone_path)) + title = _heading(body) or milestone_id + + # Refuse if somehow asked to write approved + specs_dir = kb_root(root) / "specs" / brief_slug + ensure_dir(specs_dir) + slug = slugify(title)[:60] + spec_path = specs_dir / f"{milestone_id}-{slug}-spec.md" + if spec_path.exists(): + existing_meta, _ = parse_frontmatter(read_text(spec_path)) + if str(existing_meta.get("status")) == "approved-spec": + print("FAIL: refusing to overwrite an approved specification", file=sys.stderr) + return 2 + print(f"Candidate spec already exists at {spec_path.relative_to(root)}") + return 0 + + content = render_template( + "MILESTONE-SPEC.md", + id=milestone_id, + title=title, + author=author, + owner=owner, + created=today(), + ) + # Hard-enforce review-needed in the written file + if "approved-spec" in content and "Human approval" not in content: + print("FAIL: template unexpectedly contains approved-spec", file=sys.stderr) + return 2 + write_text(spec_path, content) + + # Advance milestone status only as far as review-needed + meta["status"] = "review-needed" + meta["updated"] = today() + write_text(milestone_path, dump_frontmatter(meta, body)) + regenerate_brief_index(brief_dir) + regenerate_repo_index(root) + print(f"Created candidate spec {spec_path.relative_to(root)} (status=review-needed)") + print("Human approval required before planning or implementation.") + return 0 + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="KB-Brain tooling") + parser.add_argument("--root", type=Path, default=None, help="Repository root (default: auto-detect)") + parser.add_argument("--author", default="agent", help="Author metadata") + parser.add_argument("--owner", default="task-lead", help="Owner metadata") + parser.add_argument("--lead", default="task-lead", help="Lead metadata for workspaces") + sub = parser.add_subparsers(dest="command", required=True) + + p_init = sub.add_parser("init", help="Initialize kb-brain/ structure") + p_init.add_argument("level", nargs="?", default=DEFAULT_LEVEL, choices=WORKSPACE_LEVELS) + + p_start = sub.add_parser("start", help="Start a task workspace") + p_start.add_argument("slug") + p_start.add_argument("level", nargs="?", default=None, choices=WORKSPACE_LEVELS) + + p_new = sub.add_parser("new", help="Create an atomic record") + p_new.add_argument("section") + p_new.add_argument("title") + p_new.add_argument("--task", default=None) + + p_index = sub.add_parser("index", help="Regenerate indexes") + p_index.add_argument("path", nargs="?", default=None) + + p_check = sub.add_parser("check", help="Validate KB-Brain structure and seals") + p_check.add_argument("path", nargs="?", default=None) + + p_close = sub.add_parser("close", help="Close, seal, and archive a workspace") + p_close.add_argument("task_id") + + p_amend = sub.add_parser("amend", help="Amend a sealed closed workspace") + p_amend.add_argument("task_id") + p_amend.add_argument("record_path") + p_amend.add_argument("title") + + p_bi = sub.add_parser("brief-init", help="Scaffold a human-owned brief") + p_bi.add_argument("slug") + p_bi.add_argument("title", nargs="?", default="") + + p_bm = sub.add_parser("brief-milestone", help="Add a milestone under a brief") + p_bm.add_argument("brief_slug") + p_bm.add_argument("title") + + p_bs = sub.add_parser("brief-spec", help="Create a review-needed candidate milestone spec") + p_bs.add_argument("brief_slug") + p_bs.add_argument("milestone_id") + + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + root = (args.root or repo_root_from_cwd()).resolve() + + if args.command == "init": + return cmd_init(root, args.level) + if args.command == "start": + return cmd_start(root, args.slug, args.level, args.lead, args.author) + if args.command == "new": + return cmd_new(root, args.section, args.title, args.task, args.author, args.owner) + if args.command == "index": + return cmd_index(root, args.path) + if args.command == "check": + return cmd_check(root, args.path) + if args.command == "close": + return cmd_close(root, args.task_id, args.author, args.lead) + if args.command == "amend": + return cmd_amend(root, args.task_id, args.record_path, args.title, args.author, args.owner) + if args.command == "brief-init": + return cmd_brief_init(root, args.slug, args.title, args.author, args.owner) + if args.command == "brief-milestone": + return cmd_brief_milestone(root, args.brief_slug, args.title, args.author, args.owner) + if args.command == "brief-spec": + return cmd_brief_spec(root, args.brief_slug, args.milestone_id, args.author, args.owner) + parser.error(f"unknown command {args.command}") + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/ASSIGNMENTS.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/ASSIGNMENTS.md new file mode 100644 index 0000000..b5996b1 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/ASSIGNMENTS.md @@ -0,0 +1,18 @@ +--- +id: YYYY-MM-DD-slug-assignments +type: assignments +status: active +author: author +owner: task-lead +role: lead +created: YYYY-MM-DD +updated: YYYY-MM-DD +--- + +# Assignments + +Lead-controlled. Record each human or agent assignment, boundary, dependencies, and status. + +| Agent / human | Boundary | Depends on | Status | +|---------------|----------|------------|--------| +| | | | | diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/BRIEF.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/BRIEF.md new file mode 100644 index 0000000..69b2244 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/BRIEF.md @@ -0,0 +1,35 @@ +--- +id: brief-slug +type: brief +status: active +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +--- + +# Title + +## Intended outcome +- + +## Users and stakeholders +- + +## Why it matters +- + +## Constraints +- + +## Non-goals +- + +## Success at project level +- + +## Known milestone ideas +- + +## Open questions +- diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/CONTEXT.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/CONTEXT.md new file mode 100644 index 0000000..7b4dbe6 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/CONTEXT.md @@ -0,0 +1,18 @@ +--- +id: YYYY-MM-DD-slug-context +type: context +status: active +author: author +created: YYYY-MM-DD +updated: YYYY-MM-DD +--- + +# Shared context + +Keep this brief. Move detail into atomic records and link them. + +## What matters right now +- + +## Pointers +- diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/MILESTONE-SPEC.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/MILESTONE-SPEC.md new file mode 100644 index 0000000..2d91ef3 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/MILESTONE-SPEC.md @@ -0,0 +1,66 @@ +--- +id: F-001-spec +type: milestone-spec +status: review-needed +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +--- + +# Title + +## Status +review-needed + +## Brief linkage +- + +## Outcome +- + +## Scope +- + +## Non-goals +- + +## User and system behaviour +- + +## Constraints and controlling decisions +- + +## Components and boundaries +- + +## Data and interface implications +- + +## Failure and recovery behaviour +- + +## Security, privacy, accessibility, and operational concerns +- + +## Dependencies +- + +## Acceptance criteria +- + +## Testing expectations +- + +## Open questions +- + +## Assumptions +- + +## Evidence consulted +- + +## Human approval +Unapproved candidate specification. diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/MILESTONE.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/MILESTONE.md new file mode 100644 index 0000000..b9f5f92 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/MILESTONE.md @@ -0,0 +1,18 @@ +--- +id: F-001 +type: milestone +status: brief +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +--- + +# Title + +## Outcome +- + +## Notes +- diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/PROMOTION.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/PROMOTION.md new file mode 100644 index 0000000..3a3da1f --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/PROMOTION.md @@ -0,0 +1,16 @@ +--- +id: YYYY-MM-DD-slug-promotion +type: promotion +status: open +author: author +created: YYYY-MM-DD +updated: YYYY-MM-DD +--- + +# Promotion tracking + +Durable information that must move into permanent KBB sections or stable `docs/` before closure. + +| Record | Destination | Status | +|--------|-------------|--------| +| | | | diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/TASK.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/TASK.md new file mode 100644 index 0000000..b39f08d --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/TASK.md @@ -0,0 +1,37 @@ +--- +id: YYYY-MM-DD-slug +type: task +status: active +level: standard +lead: task-lead +author: author +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +--- + +# Title + +## Objective +Objective + +## Scope +- + +## Non-goals +- + +## Completion criteria +- + +## Current focus +- + +## Blockers +- + +## Controlling links +- brief: +- spec: +- issue: +- plan: diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/amendment.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/amendment.md new file mode 100644 index 0000000..be0fc21 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/amendment.md @@ -0,0 +1,26 @@ +--- +id: F-001 +type: amendment +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +amends: path/to/record.md +related: [] +evidence: [] +--- + +# Title + +## What this corrects +- + +## Why +- + +## New evidence +- + +## Impact +- diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/assumption.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/assumption.md new file mode 100644 index 0000000..ce4a175 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/assumption.md @@ -0,0 +1,22 @@ +--- +id: F-001 +type: assumption +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +evidence: [] +--- + +# Title + +## Assumption +- + +## If wrong +- + +## Validation needed +- diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/closeout.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/closeout.md new file mode 100644 index 0000000..75693ad --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/closeout.md @@ -0,0 +1,27 @@ +--- +id: YYYY-MM-DD-slug-closeout +type: closeout +status: done +author: author +owner: task-lead +role: lead +created: YYYY-MM-DD +updated: YYYY-MM-DD +--- + +# Closeout — YYYY-MM-DD-slug + +## Outcome +- + +## What was promoted +- + +## What was not promoted (and why) +- + +## Remaining risks +- + +## Follow-ups +- diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/conflict.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/conflict.md new file mode 100644 index 0000000..1c0947f --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/conflict.md @@ -0,0 +1,31 @@ +--- +id: F-001 +type: conflict +status: unresolved +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +evidence: [] +--- + +# Title + +## Position A +- + +## Position B +- + +## Evidence +- + +## Impact +- + +## Required resolution +- + +## Resolution +unresolved diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/decision.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/decision.md new file mode 100644 index 0000000..0f35d42 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/decision.md @@ -0,0 +1,30 @@ +--- +id: F-001 +type: decision +status: accepted +author: author +owner: owner +decision-owner: owner +role: lead +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +evidence: [] +--- + +# Title + +## Context +- + +## Decision +- + +## Rationale +- + +## Evidence +- + +## Consequences +- diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/dependency.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/dependency.md new file mode 100644 index 0000000..d463c5d --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/dependency.md @@ -0,0 +1,21 @@ +--- +id: F-001 +type: dependency +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +--- + +# Title + +## Depends on +- + +## Needed by +- + +## Status +- diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/failure.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/failure.md new file mode 100644 index 0000000..930e7fa --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/failure.md @@ -0,0 +1,25 @@ +--- +id: F-001 +type: failure +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +evidence: [] +--- + +# Title + +## What was tried +- + +## Why it failed +- + +## Evidence +- + +## Lesson +- diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/finding.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/finding.md new file mode 100644 index 0000000..a28920a --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/finding.md @@ -0,0 +1,22 @@ +--- +id: F-001 +type: finding +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +evidence: [] +--- + +# Title + +## Observation +- + +## Evidence +- + +## Impact +- diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/handoff.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/handoff.md new file mode 100644 index 0000000..38a71a8 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/handoff.md @@ -0,0 +1,27 @@ +--- +id: F-001 +type: handoff +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +--- + +# Title + +## From +- + +## To +- + +## State of play +- + +## Next actions +- + +## Blockers +- diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/improvement.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/improvement.md new file mode 100644 index 0000000..d63780a --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/improvement.md @@ -0,0 +1,31 @@ +--- +id: F-001 +type: improvement +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +evidence: [] +--- + +# Title + +## Observed during +- + +## Observation +- + +## Evidence +- + +## Why it matters +- + +## Possible direction +- + +## Relationship to current work +- diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/question.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/question.md new file mode 100644 index 0000000..bc9dd32 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/question.md @@ -0,0 +1,25 @@ +--- +id: F-001 +type: question +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +evidence: [] +--- + +# Title + +## Question +- + +## Why it matters +- + +## Evidence so far +- + +## Answer +Unanswered. Do not invent an answer. diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/scope-change.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/scope-change.md new file mode 100644 index 0000000..acddf50 --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/scope-change.md @@ -0,0 +1,22 @@ +--- +id: F-001 +type: scope-change +status: open +author: author +owner: owner +role: lead +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +--- + +# Title + +## Change +- + +## Why +- + +## Approved by +- diff --git a/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/tech-debt.md b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/tech-debt.md new file mode 100644 index 0000000..22ce32d --- /dev/null +++ b/kiro_and_antigravity/skills/.kiro/skills/kb-brain/templates/tech-debt.md @@ -0,0 +1,25 @@ +--- +id: F-001 +type: tech-debt +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +evidence: [] +--- + +# Title + +## Liability +- + +## Evidence +- + +## Suggested remediation +- + +## Urgency +- diff --git a/plugins/paad/skills/brief-ruminate/SKILL.md b/plugins/paad/skills/brief-ruminate/SKILL.md new file mode 100644 index 0000000..3511b99 --- /dev/null +++ b/plugins/paad/skills/brief-ruminate/SKILL.md @@ -0,0 +1,163 @@ +--- +name: brief-ruminate +description: Expand a human-owned project brief or selected milestone into a repository-grounded candidate specification using KB-Brain context. Use when creating or reviewing project milestones, identifying dependencies and unresolved decisions, or preparing a milestone specification for human approval before pushback, planning, alignment, or implementation. +--- + +**On invocation:** announce "Running paad:brief-ruminate v1.24.1" before anything else. + +# Brief Rumination + +Expand one human-owned brief milestone into a **candidate** specification +grounded in the repository and KB-Brain. The human owns the brief. This skill +stops at `review-needed`. It does **not** approve specs, plan implementation, +or modify `pushback`, `alignment`, `agentic-review`, or any other PAAD skill. + +Requires a KBB structure (`/paad:kb-brain init` if missing). Does not change +`kb-brain` skill logic. + +Load on demand: + +- `references/brief-format.md` — brief and milestone storage and templates +- `references/milestone-lifecycle.md` — states and human approval boundary +- `references/candidate-spec.md` — candidate spec shape and stop conditions + +Templates: this skill's `templates/` (`BRIEF.md`, `MILESTONE.md`, +`MILESTONE-SPEC.md`). Optional helpers via `scripts/kb_brain.py brief-init`, +`brief-milestone`, and `brief-spec` (always write `review-needed`, never +`approved-spec`). + +**Session flow:** + +```dot +digraph brief_ruminate { + "kb-brain/ present?" [shape=diamond]; + "Brief path usable?" [shape=diamond]; + "Milestone ID given?" [shape=diamond]; + "Milestone valuable and unblocked?" [shape=diamond]; + "Human accepted split?" [shape=diamond]; + "Controlling decision missing?" [shape=diamond]; + "Would invent product behaviour?" [shape=diamond]; + "Ownership boundary unresolved?" [shape=diamond]; + "Brief contradictory / no outcome?" [shape=diamond]; + + "STOP: run kb-brain init first" [shape=box, style=bold]; + "ASK: which brief?" [shape=box]; + "Read BRIEF.md, milestones, brief INDEX" [shape=box]; + "Recommend next valuable unblocked milestone (not merely first)" [shape=box]; + "ASK: confirm milestone or choose another" [shape=box]; + "Read ACTIVE.md, selective repo + KBB evidence" [shape=box]; + "Propose atomic milestone split" [shape=box]; + "STOP: record blocker/open question; leave unapproved" [shape=box, style=bold]; + "Ask only consequential questions" [shape=box]; + "Write candidate spec status=review-needed" [shape=box]; + "Update milestone + brief indexes; stop for human review" [shape=box]; + "Announce files written or updated" [shape=box]; + "Done" [shape=box]; + + "kb-brain/ present?" -> "STOP: run kb-brain init first" [label="no"]; + "kb-brain/ present?" -> "Brief path usable?" [label="yes"]; + "Brief path usable?" -> "ASK: which brief?" [label="no"]; + "ASK: which brief?" -> "Brief path usable?"; + "Brief path usable?" -> "Brief contradictory / no outcome?" [label="yes"]; + "Brief contradictory / no outcome?" -> "STOP: record blocker/open question; leave unapproved" [label="yes"]; + "Brief contradictory / no outcome?" -> "Milestone ID given?" [label="no"]; + + "Milestone ID given?" -> "Read BRIEF.md, milestones, brief INDEX" [label="yes"]; + "Milestone ID given?" -> "Recommend next valuable unblocked milestone (not merely first)" [label="no / next"]; + "Recommend next valuable unblocked milestone (not merely first)" -> "ASK: confirm milestone or choose another"; + "ASK: confirm milestone or choose another" -> "Read BRIEF.md, milestones, brief INDEX"; + + "Read BRIEF.md, milestones, brief INDEX" -> "Read ACTIVE.md, selective repo + KBB evidence"; + "Read ACTIVE.md, selective repo + KBB evidence" -> "Milestone valuable and unblocked?"; + "Milestone valuable and unblocked?" -> "Propose atomic milestone split" [label="oversized"]; + "Propose atomic milestone split" -> "Human accepted split?"; + "Human accepted split?" -> "Read BRIEF.md, milestones, brief INDEX" [label="yes — new milestone"]; + "Human accepted split?" -> "STOP: record blocker/open question; leave unapproved" [label="no"]; + + "Milestone valuable and unblocked?" -> "Controlling decision missing?" [label="scoped ok"]; + "Controlling decision missing?" -> "STOP: record blocker/open question; leave unapproved" [label="yes"]; + "Controlling decision missing?" -> "Would invent product behaviour?" [label="no"]; + "Would invent product behaviour?" -> "STOP: record blocker/open question; leave unapproved" [label="yes"]; + "Would invent product behaviour?" -> "Ownership boundary unresolved?" [label="no"]; + "Ownership boundary unresolved?" -> "STOP: record blocker/open question; leave unapproved" [label="yes"]; + "Ownership boundary unresolved?" -> "Ask only consequential questions" [label="no"]; + + "Ask only consequential questions" -> "Write candidate spec status=review-needed"; + "Write candidate spec status=review-needed" -> "Update milestone + brief indexes; stop for human review"; + "Update milestone + brief indexes; stop for human review" -> "Announce files written or updated"; + "STOP: record blocker/open question; leave unapproved" -> "Announce files written or updated"; + "Announce files written or updated" -> "Done"; +} +``` + +## Arguments + +``` +/paad:brief-ruminate <brief-path> +/paad:brief-ruminate <brief-path> <milestone-id> +/paad:brief-ruminate next <brief-path> +/paad:brief-ruminate status <brief-path> +``` + +Without a milestone ID (or with `next`), inspect the brief and milestone index, +then recommend the next milestone that is both valuable and sufficiently +unblocked. Do **not** select a milestone solely because it appears first. + +`status` reports milestone states and linked candidate specs without writing a +new spec. + +## Authority + +- The human owns `BRIEF.md`. Suggest amendments; never silently rewrite intent, + outcomes, constraints, or non-goals. +- This skill may advance a milestone as far as `review-needed`. +- **Only a human** may mark `approved-spec`. +- After approval, other PAAD skills may be invoked **separately** (`pushback` → + planning → `alignment` → implementation → `agentic-review`). Do not auto-run + them. + +## Rumination steps (one milestone) + +1. Read `BRIEF.md`, the milestone record, and the brief index. +2. Read `kb-brain/work/ACTIVE.md` for ownership or blockers relevant to the + milestone. +3. Read relevant repository files, stable docs, and KBB indexes / decisions / + questions / failures / gotchas — selectively, index-first. +4. Check whether previous decisions constrain the milestone. +5. Identify dependencies, conflicts, assumptions, compatibility, data, and + operational concerns; respect non-goals. +6. Confirm the milestone is independently valuable and reasonably scoped. +7. If oversized, propose an atomic split without changing the human brief + automatically. +8. Ask only consequential questions that materially change scope or behaviour. +9. Write a candidate milestone spec with status `review-needed` under + `kb-brain/specs/<brief-slug>/`. +10. Link supporting and unresolved KBB records. +11. Update the milestone index and **stop** for human review. + +Do not ruminate indefinitely or repeatedly rewrite a candidate without new +evidence or human direction. + +## Announce What You Wrote + +``` +Files written or updated: + new kb-brain/specs/checkout/M-001-guest-cart-spec.md + updated kb-brain/briefs/checkout/milestones/M-001-guest-cart.md + updated kb-brain/briefs/checkout/INDEX.md +``` + +If the run only recorded a blocker/open question and wrote no spec, still list +those KBB paths. + +## Common Mistakes + +| Mistake | What to do instead | +|---------|-------------------| +| Marking a spec `approved-spec` | Leave `review-needed`; humans approve | +| Rewriting the brief's intent | Suggest amendments; wait for the human | +| Picking the first milestone blindly | Prefer valuable and unblocked | +| Inventing missing product behaviour | Record an open question / blocker and stop | +| Auto-running pushback or alignment | Stop after the candidate spec | +| Loading the entire KB | Index-first, selective reads | +| Changing other PAAD skill behaviour | Leave them untouched | diff --git a/plugins/paad/skills/brief-ruminate/references/brief-format.md b/plugins/paad/skills/brief-ruminate/references/brief-format.md new file mode 100644 index 0000000..11a0615 --- /dev/null +++ b/plugins/paad/skills/brief-ruminate/references/brief-format.md @@ -0,0 +1,63 @@ +# Brief and milestone format + +## Storage + +```text +kb-brain/briefs/<brief-slug>/ +├── BRIEF.md +├── INDEX.md +└── milestones/ + ├── M-001-<slug>.md + └── ... + +kb-brain/specs/<brief-slug>/ +└── M-001-<slug>-spec.md +``` + +The human owns `BRIEF.md`. Agents may suggest amendments but must not silently +rewrite intent, outcomes, constraints, or non-goals. + +## Brief template (product-level, implementation-light) + +```markdown +# <Project or feature brief> + +## Intended outcome +... + +## Users and stakeholders +... + +## Why it matters +... + +## Constraints +... + +## Non-goals +... + +## Success at project level +... + +## Known milestone ideas +... + +## Open questions +... +``` + +Canonical copies: this skill's `templates/BRIEF.md` and +`kb-brain/templates/BRIEF.md` after init. + +## Milestone records + +Atomic files under `milestones/` with prefix `M-`. Status follows +`references/milestone-lifecycle.md`. Keep each milestone independently valuable +when possible. + +## Index + +`INDEX.md` lists milestone ID, title, status, and linked candidate spec path. +Regenerate when milestones or specs change (`kb_brain.py` brief helpers, or by +hand consistently with the table format). diff --git a/plugins/paad/skills/brief-ruminate/references/candidate-spec.md b/plugins/paad/skills/brief-ruminate/references/candidate-spec.md new file mode 100644 index 0000000..c53bc80 --- /dev/null +++ b/plugins/paad/skills/brief-ruminate/references/candidate-spec.md @@ -0,0 +1,84 @@ +# Candidate milestone specification + +Status must be `review-needed`. Human approval section must remain unapproved +until a human changes it. + +```markdown +# <Milestone title> + +## Status +review-needed + +## Brief linkage +<Brief path and milestone ID> + +## Outcome +... + +## Scope +... + +## Non-goals +... + +## User and system behaviour +... + +## Constraints and controlling decisions +... + +## Components and boundaries +... + +## Data and interface implications +... + +## Failure and recovery behaviour +... + +## Security, privacy, accessibility, and operational concerns +... + +## Dependencies +... + +## Acceptance criteria +... + +## Testing expectations +... + +## Open questions +... + +## Assumptions +... + +## Evidence consulted +... + +## Human approval +Unapproved candidate specification. +``` + +Canonical template: `templates/MILESTONE-SPEC.md`. + +Tooling helper (never writes `approved-spec`): + +```bash +python3 scripts/kb_brain.py brief-spec <brief-slug> <milestone-id> +``` + +## Stop conditions (leave unapproved) + +Stop and record a blocker or open question in KBB when: + +- the brief is contradictory or lacks a usable intended outcome +- a controlling decision is missing +- unresolved dependencies make a meaningful specification impossible +- the milestone overlaps another active owner's work and the boundary is unresolved +- the candidate would require inventing product behaviour +- the milestone is too large and the human has not accepted a proposed split + +Do not ruminate indefinitely or rewrite the candidate without new evidence or +human direction. diff --git a/plugins/paad/skills/brief-ruminate/references/milestone-lifecycle.md b/plugins/paad/skills/brief-ruminate/references/milestone-lifecycle.md new file mode 100644 index 0000000..e290622 --- /dev/null +++ b/plugins/paad/skills/brief-ruminate/references/milestone-lifecycle.md @@ -0,0 +1,45 @@ +# Milestone lifecycle + +```text +brief +→ incubating +→ ready-for-expansion +→ expanding +→ review-needed +→ approved-spec +→ planned +→ in-progress +→ completed +→ superseded +``` + +## Who may advance which states + +| Transition | Actor | +|------------|-------| +| through `review-needed` | `brief-ruminate` / agents | +| to `approved-spec` | **human only** | +| `planned` and beyond | separate planning / implementation workflows | + +## After human approval (separate invocations) + +```text +approved specification +→ pushback +→ implementation planning +→ alignment +→ implementation +→ agentic-review +``` + +Do **not** auto-run or modify those skills from `brief-ruminate`. + +## Selection guidance + +When choosing the next milestone: + +- Prefer independently valuable outcomes that are sufficiently unblocked. +- Do not select solely because a milestone appears first in the list. +- If oversized, propose a split; do not rewrite the brief automatically. +- If blocked on a missing controlling decision, ownership overlap, or invented + behaviour, stop and record the blocker rather than guessing. diff --git a/plugins/paad/skills/brief-ruminate/templates/BRIEF.md b/plugins/paad/skills/brief-ruminate/templates/BRIEF.md new file mode 100644 index 0000000..69b2244 --- /dev/null +++ b/plugins/paad/skills/brief-ruminate/templates/BRIEF.md @@ -0,0 +1,35 @@ +--- +id: brief-slug +type: brief +status: active +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +--- + +# Title + +## Intended outcome +- + +## Users and stakeholders +- + +## Why it matters +- + +## Constraints +- + +## Non-goals +- + +## Success at project level +- + +## Known milestone ideas +- + +## Open questions +- diff --git a/plugins/paad/skills/brief-ruminate/templates/MILESTONE-SPEC.md b/plugins/paad/skills/brief-ruminate/templates/MILESTONE-SPEC.md new file mode 100644 index 0000000..2d91ef3 --- /dev/null +++ b/plugins/paad/skills/brief-ruminate/templates/MILESTONE-SPEC.md @@ -0,0 +1,66 @@ +--- +id: F-001-spec +type: milestone-spec +status: review-needed +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +--- + +# Title + +## Status +review-needed + +## Brief linkage +- + +## Outcome +- + +## Scope +- + +## Non-goals +- + +## User and system behaviour +- + +## Constraints and controlling decisions +- + +## Components and boundaries +- + +## Data and interface implications +- + +## Failure and recovery behaviour +- + +## Security, privacy, accessibility, and operational concerns +- + +## Dependencies +- + +## Acceptance criteria +- + +## Testing expectations +- + +## Open questions +- + +## Assumptions +- + +## Evidence consulted +- + +## Human approval +Unapproved candidate specification. diff --git a/plugins/paad/skills/brief-ruminate/templates/MILESTONE.md b/plugins/paad/skills/brief-ruminate/templates/MILESTONE.md new file mode 100644 index 0000000..b9f5f92 --- /dev/null +++ b/plugins/paad/skills/brief-ruminate/templates/MILESTONE.md @@ -0,0 +1,18 @@ +--- +id: F-001 +type: milestone +status: brief +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +--- + +# Title + +## Outcome +- + +## Notes +- diff --git a/plugins/paad/skills/help/SKILL.md b/plugins/paad/skills/help/SKILL.md index 542decd..7c5fd82 100644 --- a/plugins/paad/skills/help/SKILL.md +++ b/plugins/paad/skills/help/SKILL.md @@ -45,6 +45,8 @@ Available skills: /paad:fix-architecture [report] Fix architectural flaws from an analysis report /paad:agentic-review [base-branch] [path] Multi-agent code review of current branch (bug hunting) /paad:alignment [files...] Requirements-to-tasks alignment + TDD rewrite + /paad:brief-ruminate [brief] [milestone] Expand one brief milestone into a candidate spec + /paad:kb-brain [action...] Repository-native working knowledge base /paad:makefile Create or update a Makefile with standard targets /paad:pushback [spec-file] Spec/PRD critic (finds issues before you build) /paad:vibe [task description] Safe vibe coding with TDD guardrails @@ -66,6 +68,9 @@ Picking between them: Have no tests, or tests you distrust? test-roadmap (experimental; the only skill that writes and commits code) Have one spec, is it any good? pushback + Have a brief, need a milestone spec? brief-ruminate (candidate only; human + approves before pushback/planning) + Need shared agent/human memory? kb-brain (workspaces + sealed history) Been handed options, are they sound? rethink (experimental; checks premises, does not invent alternatives) Have a spec AND a plan, do they match? alignment (needs both; does not read code) @@ -276,6 +281,72 @@ What it does: Works within an existing conversation — no fresh session needed. ``` +### brief-ruminate + +``` +/paad:brief-ruminate [brief-path] [milestone-id] + +Expand one human-owned brief milestone into a repository-grounded +candidate specification. Stops at review-needed. Does not approve, +plan, or implement. Does not auto-run pushback, alignment, or +agentic-review. + +Requires kb-brain/ (run /paad:kb-brain init first). + +Arguments: + /paad:brief-ruminate path/to/BRIEF.md + /paad:brief-ruminate path/to/BRIEF.md M-001 + /paad:brief-ruminate next path/to/BRIEF.md + /paad:brief-ruminate status path/to/BRIEF.md + +What it does: + 1. Reads the human-owned brief (never silently rewrites its intent) + 2. Picks or confirms a milestone that is valuable and unblocked + (not merely first in the list) + 3. Reads ACTIVE.md plus selective repo and KBB evidence + 4. Records consequential questions, assumptions, dependencies, + and conflicts instead of inventing product behaviour + 5. Writes kb-brain/specs/<slug>/M-NNN-…-spec.md at review-needed + 6. Updates the brief milestone index and stops for human review + +Only a human may mark a spec approved-spec. +``` + +### kb-brain + +``` +/paad:kb-brain [action...] + +Repository-native working knowledge base for humans and agents. +Stable docs stay in docs/; mutable working memory lives in kb-brain/. + +Arguments: + /paad:kb-brain init [minimal|standard|strict] + /paad:kb-brain start <task-slug> [minimal|standard|strict] + /paad:kb-brain record <section> [title] + /paad:kb-brain status [task-id] + /paad:kb-brain index [task-id] + /paad:kb-brain check [path] + /paad:kb-brain close [task-id] + /paad:kb-brain amend <closed-task-id> <record-path> + /paad:kb-brain route <description-of-knowledge> + +What it does: + 1. Initializes kb-brain/ (no bulk ingest of docs/) + 2. Starts a focused task workspace under work/active/ + 3. Routes durable notes to the right section or workspace + 4. Regenerates indexes and ACTIVE.md (ownership + blockers, + not conflicts) + 5. Validates frontmatter, IDs, links, and seals (kb-check) + 6. Closes with SEAL.json; amendments only after closure + +Sub-agents may append findings, questions, failures, conflicts, +handoffs, improvements, and tech-debt. Only the lead or human +owner changes scope, assignments, confirmed decisions, or closes. + +Does not change the behaviour of other PAAD skills. +``` + ### makefile ``` diff --git a/plugins/paad/skills/kb-brain/SKILL.md b/plugins/paad/skills/kb-brain/SKILL.md new file mode 100644 index 0000000..8051c5c --- /dev/null +++ b/plugins/paad/skills/kb-brain/SKILL.md @@ -0,0 +1,229 @@ +--- +name: kb-brain +description: Maintain a repository-native working knowledge base for humans and coding agents. Use when initializing kb-brain, starting or closing task workspaces, recording durable context, decisions, findings, failures, questions, improvements, technical debt, handoffs, or amendments, and when coordinating lead agents and sub-agents through shared project memory. +--- + +**On invocation:** announce "Running paad:kb-brain v1.24.1" before anything else. + +# KB-Brain + +Repository-native working knowledge for humans, lead agents, and sub-agents. +Stable architecture and accepted documentation stay in `docs/`. Mutable +context, in-flight decisions, gaps, debt, lessons, and focused task workspaces +live under `kb-brain/`. + +This skill does **not** change the behaviour of `pushback`, `alignment`, +`agentic-review`, or any other existing PAAD skill. Invoke it explicitly (or +via repository `AGENTS.md` instructions). + +Load details on demand from: + +- `references/structure.md` — directory layout, workspace levels, file roles +- `references/routing.md` — when to write, where to put it, AGENTS.md snippet +- `references/lifecycle.md` — start, close, seal, amend, validation +- `references/templates.md` — atomic record templates and ID prefixes + +Bundled templates live in this skill's `templates/` directory. Deterministic +tooling is `scripts/kb_brain.py` (copied into the target repo on `init`). + +**Dispatch:** + +```dot +digraph kb_brain_dispatch { + "Action known?" [shape=diamond]; + "kb-brain/ exists?" [shape=diamond]; + "Which action?" [shape=diamond]; + "Durable enough to write?" [shape=diamond]; + "Level below repo default?" [shape=diamond]; + "Human approved lower level?" [shape=diamond]; + "Promotion + closeout ready?" [shape=diamond]; + "Validation clean?" [shape=diamond]; + + "Infer from conversation or show operation summary and ASK" [shape=box]; + "init scaffold (no docs ingest)" [shape=box]; + "STOP: run init first" [shape=box, style=bold]; + "start workspace" [shape=box]; + "STOP: refuse lower level without approval" [shape=box, style=bold]; + "record / route to section or workspace" [shape=box]; + "Keep in session only" [shape=box]; + "status / index / check" [shape=box]; + "close → seal → move to work/closed" [shape=box]; + "STOP: finish promotion/closeout or fix check errors" [shape=box, style=bold]; + "amend under amendments/ only" [shape=box]; + "Announce files written or updated" [shape=box]; + "Done" [shape=box]; + + "Action known?" -> "Which action?" [label="yes"]; + "Action known?" -> "Infer from conversation or show operation summary and ASK" [label="no"]; + "Infer from conversation or show operation summary and ASK" -> "Action known?"; + + "Which action?" -> "init scaffold (no docs ingest)" [label="init"]; + "Which action?" -> "kb-brain/ exists?" [label="other"]; + "kb-brain/ exists?" -> "STOP: run init first" [label="no"]; + "kb-brain/ exists?" -> "Level below repo default?" [label="yes + start"]; + "kb-brain/ exists?" -> "Durable enough to write?" [label="yes + record/route"]; + "kb-brain/ exists?" -> "status / index / check" [label="yes + status|index|check"]; + "kb-brain/ exists?" -> "Promotion + closeout ready?" [label="yes + close"]; + "kb-brain/ exists?" -> "amend under amendments/ only" [label="yes + amend"]; + + "Level below repo default?" -> "Human approved lower level?" [label="yes"]; + "Level below repo default?" -> "start workspace" [label="no"]; + "Human approved lower level?" -> "start workspace" [label="yes"]; + "Human approved lower level?" -> "STOP: refuse lower level without approval" [label="no"]; + + "Durable enough to write?" -> "record / route to section or workspace" [label="yes"]; + "Durable enough to write?" -> "Keep in session only" [label="no"]; + + "Promotion + closeout ready?" -> "Validation clean?" [label="yes"]; + "Promotion + closeout ready?" -> "STOP: finish promotion/closeout or fix check errors" [label="no"]; + "Validation clean?" -> "close → seal → move to work/closed" [label="yes"]; + "Validation clean?" -> "STOP: finish promotion/closeout or fix check errors" [label="no"]; + + "init scaffold (no docs ingest)" -> "Announce files written or updated"; + "start workspace" -> "Announce files written or updated"; + "record / route to section or workspace" -> "Announce files written or updated"; + "Keep in session only" -> "Done"; + "status / index / check" -> "Announce files written or updated"; + "close → seal → move to work/closed" -> "Announce files written or updated"; + "amend under amendments/ only" -> "Announce files written or updated"; + "Announce files written or updated" -> "Done"; +} +``` + +## Arguments + +Positional action, then intuitive values. Prefer the script for deterministic +scaffold/index/check/seal work. + +``` +/paad:kb-brain init [minimal|standard|strict] +/paad:kb-brain start <task-slug> [minimal|standard|strict] +/paad:kb-brain record <section> [title] +/paad:kb-brain status [task-id] +/paad:kb-brain index [task-id] +/paad:kb-brain check [path] +/paad:kb-brain close [task-id] +/paad:kb-brain amend <closed-task-id> <record-path> +/paad:kb-brain route <description-of-knowledge> +``` + +Natural language is fine (“record the parser failure”, “close the current +workspace”). If no action can be inferred safely, show the list above and ask. + +## Global rules + +1. **Human authority.** Agents may record observations, evidence, failures, + questions, improvements, debt, handoffs, and working context. They must + **not** silently promote inferred requirements, unapproved architecture, + invented answers to open questions, improvements-as-roadmap, or candidate + specs to accepted facts. Confirmed decisions need explicit human + confirmation or unambiguous repository evidence — record owner and evidence. +2. **Notice broadly, work narrowly.** Out-of-scope gaps become atomic + improvement or tech-debt records. Do not implement them unless the active + task includes them. +3. **Index-first retrieval.** Read `kb-brain/work/ACTIVE.md`, then the + workspace `TASK.md`, indexes, `CONTEXT.md`, assignment, then only linked + records. Never load the whole KB by default. +4. **No unsolicited bulk ingress.** Do not dump `docs/` into `kb-brain/` on + init or during ordinary work. +5. **Existing skills unchanged.** Do not wire automatic KBB reads/writes into + other PAAD skills. + +## Tooling + +After `init`, the target repo has `scripts/kb_brain.py` and Make targets +(`kb-index`, `kb-new`, `kb-check`, plus `kb-start` / `kb-close` / `kb-amend`). +Prefer: + +```bash +python3 scripts/kb_brain.py init standard +python3 scripts/kb_brain.py start <slug> [level] +python3 scripts/kb_brain.py new <section> "<title>" [--task <id>] +python3 scripts/kb_brain.py index +python3 scripts/kb_brain.py check +python3 scripts/kb_brain.py close <task-id> +python3 scripts/kb_brain.py amend <closed-task-id> <record-path> "<title>" +``` + +Until `init` has run in the target repo, invoke the bundled copy: + +`python3 <path-to-this-skill>/scripts/kb_brain.py --root <repo> …` + +If the target has no Makefile, `init` writes `kb-brain/Makefile.inc` instead +of replacing project build tooling. + +## Operations + +### init + +Scaffold `kb-brain/` (see `references/structure.md`). Copy templates. Install +script + Make block. Default level is `standard` unless overridden. **Do not** +ingest existing documentation. + +### start + +Create `kb-brain/work/active/YYYY-MM-DD-<slug>/` with level-appropriate files. +Collision-safe IDs (`-2`, `-3`, …). Raise above repo default freely; lowering +requires explicit human approval. Regenerate `ACTIVE.md` and indexes. + +### record / route + +Apply the durability test in `references/routing.md`. Search indexes before +creating duplicates. Allocate the next ID by inspecting the target directory +(no shared counter file). Sub-agents may append findings, questions, failures, +conflicts, handoffs, improvements, and tech-debt. Only the lead or human task +owner may change scope, lifecycle, assignments, confirmed decisions, blockers, +conflict resolution, or close/seal. + +### status / index / check + +`status` summarizes active work from `ACTIVE.md` / `TASK.md`. `index` +regenerates indexes. `check` validates structure, frontmatter, IDs, seals, and +amendments — report **all** safe-to-collect errors in one run. + +### close + +Final cleanup → promote durable knowledge → `CLOSEOUT.md` → index → validate → +`SEAL.json` (SHA-256 of historical files, excluding seal, generated `INDEX.md`, +and `amendments/`) → move to `work/closed/`. Do not erase substantive failures +or disagreements to “clean up” history. + +### amend + +After closure, never edit sealed originals. Create +`work/closed/<task-id>/amendments/AM-…md` that names what it corrects. +Regenerated indexes must mark amended records. + +## Conflict protocol + +Record conflicting findings; never overwrite. When a conflict affects current +work, the lead sets `resolved`, `deferred`, or `blocked` (and adds blockers to +`TASK.md` so they appear in `ACTIVE.md`). Unrelated conflicts stay recorded +and do not block the current assignment. `ACTIVE.md` lists ownership and +blockers — **not** conflicts. + +## Announce What You Wrote + +Before any summary, list every path this run created or updated: + +``` +Files written or updated: + new kb-brain/work/active/2026-08-04-auth-migration/TASK.md + updated kb-brain/work/ACTIVE.md +``` + +Source and test files outside `kb-brain/` need only a count and a pointer to +the diff when there are many. Skills that write nothing are exempt; this skill +almost always writes. + +## Common Mistakes + +| Mistake | What to do instead | +|---------|-------------------| +| Dumping `docs/` into KBB on init | Init scaffolds empty sections only | +| Reading the whole KB every turn | Index-first retrieval order | +| Inventing answers in `open-questions/` | Add evidence; leave unanswered | +| Editing a sealed closed workspace | Write an amendment | +| Lowering workspace level silently | Get explicit human approval | +| Auto-wiring KBB into other PAAD skills | Leave those skills unchanged | +| Implementing every improvement you notice | Record it; stay in task scope | diff --git a/plugins/paad/skills/kb-brain/references/lifecycle.md b/plugins/paad/skills/kb-brain/references/lifecycle.md new file mode 100644 index 0000000..3b013b0 --- /dev/null +++ b/plugins/paad/skills/kb-brain/references/lifecycle.md @@ -0,0 +1,87 @@ +# Lifecycle: start, close, seal, amend, check + +## Start + +1. Ensure `kb-brain/` exists (`init` if not). +2. Resolve repository default level from `AGENTS.md` or `.default-level`. +3. Refuse levels below the repo default without explicit human approval. +4. Allocate `YYYY-MM-DD-<slug>` (append `-2`, … on collision). +5. Create level-appropriate files and directories from templates. +6. Regenerate workspace `INDEX.md`, `work/ACTIVE.md`, and repo `INDEX.md`. + +## Final cleanup (before close) + +The lead may: + +- remove empty placeholders and accidental duplicates +- repair metadata and broken links +- update statuses, ownership, and final context +- remove accidentally captured secrets or sensitive transient output +- complete `PROMOTION.md` and `CLOSEOUT.md` +- regenerate indexes and run validation + +Do **not** erase substantive failures, disagreements, abandoned approaches, or +evidence to make history look cleaner. + +## Closure flow + +```text +active work +→ final cleanup +→ durable knowledge promotion +→ closeout generation +→ index regeneration +→ validation +→ seal creation +→ move to work/closed/ +→ immutable +``` + +Closed workspace retains its atomic structure and adds: + +```text +CLOSEOUT.md +SEAL.json +amendments/ +``` + +## SEAL.json + +SHA-256 hashes of every historical workspace file at closure, **excluding**: + +- `SEAL.json` itself +- generated `INDEX.md` +- files under `amendments/` + +`kb-check` reports mutation, removal, or rename of sealed files. It may +regenerate `INDEX.md` and accept new valid amendment files without invalidating +the seal. + +## Amendments + +Never edit, rename, or delete original sealed records. Create: + +```text +kb-brain/work/closed/<task-id>/amendments/AM-NNN-<slug>.md +``` + +An amendment must identify what it corrects, clarifies, or supersedes; explain +why; link new evidence; and state impact. Regenerated indexes visibly mark +amended records. + +## kb-check validates + +- expected top-level section names +- required workspace files for the selected level +- valid YAML frontmatter and required fields +- unique IDs within their scope +- valid relative links where deterministically checkable +- task ownership / lead-only metadata consistency +- blockers consistent between `TASK.md` and generated `ACTIVE.md` +- closed workspace seal hashes +- amendment references point at an existing sealed record +- no original closed file changed, removed, or renamed +- no obvious secret patterns in files being sealed +- indexes regenerable cleanly + +Report all safe-to-collect errors in one run; do not stop at the first issue. diff --git a/plugins/paad/skills/kb-brain/references/routing.md b/plugins/paad/skills/kb-brain/references/routing.md new file mode 100644 index 0000000..5bee789 --- /dev/null +++ b/plugins/paad/skills/kb-brain/references/routing.md @@ -0,0 +1,109 @@ +# Routing and write rules + +## Durability test + +> Will this information materially help a future developer understand, decide, +> avoid a failure, or continue unfinished work? + +If **no** — keep it in session context; do not create a file. + +If **yes** — route by intent: + +| Intent | Destination | +|--------|-------------| +| Accepted choice and rationale | `decisions/` | +| Mutable architectural reasoning | `architecture/` (link to stable `docs/`) | +| Product/domain fact | `domains/` | +| Recurring procedure | `runbooks/` | +| Sharp edge or false start | `gotchas/` | +| Unresolved decision | `open-questions/` | +| Out-of-scope opportunity | `improvements/` | +| Remediation liability | `tech-debt/` + update `LEDGER.md` | +| Task-specific evidence or working state | current workspace | +| Stable accepted documentation | propose/perform promotion to `docs/`, then link from KBB | + +Avoid duplicates: search indexes and relevant titles first. Append evidence to +an active compatible record when the conclusion, scope, owner, and lifecycle +match; otherwise create a new record. + +## Agent permissions + +### Sub-agents may append + +- findings and evidence +- questions +- failures and abandoned approaches +- conflicts +- handoffs +- suggested improvements +- technical debt + +### Lead-only (or human task owner) + +- task scope or non-goals +- task lifecycle status +- assignments and ownership +- confirmed decisions +- blocker disposition +- conflict resolution status +- workspace closure and sealing + +Prefer `role: lead` (and `decision-owner` where applicable) on privileged +records. `kb-check` flags confirmatory decisions/scope-changes that lack lead +ownership metadata. + +## Conflict protocol + +Conflicting findings are recorded, never overwritten. When a conflict pertains +to current work, address it in-session. The lead records one of: + +- `resolved` — evidence or human decision establishes the controlling conclusion +- `deferred` — real but outside the bounded task; record why and where next +- `blocked` — cannot safely continue; add the blocker to `TASK.md` (surfaces in + `ACTIVE.md`) + +Unrelated conflicts remain recorded and do not block the current assignment. + +## AGENTS.md installation snippet + +Provide this (or equivalent) when initializing KBB in a repository: + +```markdown +## KB-Brain + +Use the `kb-brain` skill for section routing, templates, workspace lifecycle, and write rules. + +Every PAAD-managed task uses a focused shared workspace under: + +`kb-brain/work/active/<task-id>/` + +Repository workspace level: `standard` + +Supported levels: + +- `minimal` — scope, status, ownership, handoff, and durable findings +- `standard` — findings, questions, decisions, failures, conflicts, and handoffs +- `strict` — standard plus evidence, assumptions, dependencies, ownership, and scope-change tracking + +A task may raise its level. Lowering below the repository level requires explicit human approval. + +Sub-agents may append findings, evidence, questions, failures, conflicts, and handoffs. + +Only the lead agent or human task owner may change task scope, lifecycle status, assignments, blockers, confirmed decisions, or conflict resolution state. + +Conflicting findings must be recorded rather than overwritten. Address conflicts during the session when they affect current work. + +Agents may autonomously: + +- add one file per gap under `kb-brain/improvements/`; +- track smoothing or remediation work under `kb-brain/tech-debt/`; +- update `kb-brain/tech-debt/LEDGER.md`; +- move resolved debt into `kb-brain/tech-debt/closed/`; +- promote durable task knowledge into the appropriate permanent KBB section. + +Use the provided templates for human- and agent-authored records. + +Bulk content ingress from existing documentation is a dedicated operation. Do not copy architecture documentation into KBB unprompted. + +Closed workspaces are immutable. Add corrections or later findings as explicit amendments. +``` diff --git a/plugins/paad/skills/kb-brain/references/structure.md b/plugins/paad/skills/kb-brain/references/structure.md new file mode 100644 index 0000000..44f6cd0 --- /dev/null +++ b/plugins/paad/skills/kb-brain/references/structure.md @@ -0,0 +1,121 @@ +# KB-Brain structure + +## Top-level layout + +```text +kb-brain/ +├── README.md +├── INDEX.md +├── decisions/ +├── architecture/ +├── domains/ +├── runbooks/ +├── gotchas/ +├── briefs/ +├── specs/ +├── plans/ +├── reviews/ +├── learnings/ +├── open-questions/ +├── agents/ +├── improvements/ +├── tech-debt/ +│ ├── LEDGER.md +│ └── closed/ +├── templates/ +└── work/ + ├── ACTIVE.md + ├── active/ + └── closed/ +``` + +Do not create empty atomic records during init. + +## Permanent section meanings + +| Section | Meaning | +|---------|---------| +| `decisions/` | Accepted ADR-style decisions with owner and evidence | +| `architecture/` | Mutable notes refining or challenging stable `docs/` — always link back | +| `domains/` | Product and business domain knowledge | +| `runbooks/` | Recurring procedures not yet stable documentation | +| `gotchas/` | Sharp edges, false starts, reliable warnings | +| `briefs/` | Human-owned briefs and atomic milestone records | +| `specs/` | Active working specifications, including candidate milestone expansions | +| `plans/` | Implementation plans tied to approved specs | +| `reviews/` | Review outputs worth preserving beyond their session | +| `learnings/` | Post-hoc lessons and retrospectives | +| `open-questions/` | Unresolved questions; agents may add evidence, never invent answers | +| `agents/` | Repository-specific agent behaviour and coordination notes | +| `improvements/` | One atomic file per noticed gap or opportunity | +| `tech-debt/` | Open debt + `LEDGER.md`; resolved entries move to `closed/` | + +`docs/` is the stable architecture / accepted documentation source of truth. +Do not duplicate it under `kb-brain/`. + +## Workspace levels + +Repository default is selected in `AGENTS.md` (recommended `standard`). A task +may raise its level. Lowering below the repository default requires explicit +human approval. + +### minimal + +Required: task scope and status, compact context, ownership, final handoff, +durable findings selected for promotion. + +### standard (default) + +Adds: assignments, findings and evidence, questions, failures and abandoned +approaches, confirmed decisions, conflicts, handoffs, promotion tracking. + +### strict + +Adds: explicit assumptions, dependency records, decision ownership, scope-change +records, mandatory evidence references, detailed conflict handling, completion +and promotion checks. + +All levels use focused files and atomic record directories — never a single +append-only workspace journal. + +## Active workspace layout + +```text +kb-brain/work/active/<task-id>/ +├── TASK.md +├── INDEX.md +├── CONTEXT.md +├── ASSIGNMENTS.md # standard + strict +├── PROMOTION.md +├── decisions/ +├── findings/ +├── questions/ +├── failures/ +├── conflicts/ +├── handoffs/ +├── assumptions/ # strict +├── dependencies/ # strict +└── scope-changes/ # strict +``` + +Task ID format: `YYYY-MM-DD-<short-slug>`. On collision append `-2`, `-3`, … + +## File responsibilities + +- **`TASK.md`** — authoritative task card: objective, scope, non-goals, + completion criteria, workspace level, lead owner, lifecycle status, current + focus, blockers, controlling brief/spec/issue/plan links. +- **`INDEX.md`** — generated. Lists atomic records by type, status, owner, and + relationship. Marks amendments on closed workspaces. +- **`CONTEXT.md`** — compact shared working memory. Keep brief; link detail. +- **`ASSIGNMENTS.md`** — lead-controlled assignments, boundaries, dependencies, + status. +- **`PROMOTION.md`** — durable information that must move to permanent KBB + sections or stable `docs/` before closure. + +## ACTIVE.md + +`kb-brain/work/ACTIVE.md` is generated from task workspace metadata. It +contains only: active task ID and link, status, workspace level, lead/owner, +objective, current focus, blockers. **Do not list conflicts.** Do not hand-edit +narrative that can drift — regenerate via `kb_brain.py index`. diff --git a/plugins/paad/skills/kb-brain/references/templates.md b/plugins/paad/skills/kb-brain/references/templates.md new file mode 100644 index 0000000..8d7f759 --- /dev/null +++ b/plugins/paad/skills/kb-brain/references/templates.md @@ -0,0 +1,121 @@ +# Templates and IDs + +Distributable templates ship in this skill's `templates/` directory and are +copied to `kb-brain/templates/` on `init`. Humans and agents use the same +files. + +## Common frontmatter + +```yaml +--- +id: F-001 +type: finding +status: open +author: agent-parser-review +owner: task-lead +created: 2026-08-04 +updated: 2026-08-04 +related: + - questions/Q-002-import-order.md + - src/parser/imports.ts +--- +``` + +Required: `id`, `type`, `status`, `author`, `created`, `updated`. + +Add `owner`, `related`, `evidence`, `supersedes`, `amends`, `decision-owner`, +or `role: lead` where applicable. + +## ID prefixes + +Allocate the next numeric ID by inspecting the relevant directory — no shared +mutable counter file. + +| Kind | Prefix | +|------|--------| +| Decision | `D-` | +| Finding | `F-` | +| Question | `Q-` | +| Failure | `X-` | +| Conflict | `C-` | +| Handoff | `H-` | +| Improvement | `I-` | +| Technical debt | `TD-` | +| Assumption | `A-` | +| Dependency | `DEP-` | +| Scope change | `SC-` | +| Amendment | `AM-` | +| Milestone | `M-` | + +## Required templates + +| File | Purpose | +|------|---------| +| `TASK.md` | Task card | +| `CONTEXT.md` | Compact shared context | +| `ASSIGNMENTS.md` | Lead-controlled assignments | +| `finding.md` | Finding / evidence | +| `decision.md` | Accepted decision | +| `question.md` | Open question | +| `failure.md` | Failure / abandoned approach | +| `conflict.md` | Conflicting positions | +| `handoff.md` | Handoff | +| `improvement.md` | Out-of-scope opportunity | +| `tech-debt.md` | Remediation liability | +| `assumption.md` | Strict-level assumption | +| `dependency.md` | Strict-level dependency | +| `scope-change.md` | Strict-level scope change | +| `PROMOTION.md` | Promotion tracking | +| `closeout.md` | Closeout | +| `amendment.md` | Post-seal correction | +| `BRIEF.md` | Human project brief | +| `MILESTONE.md` | Atomic milestone | +| `MILESTONE-SPEC.md` | Candidate milestone specification | + +## Improvement body shape + +```markdown +# <Improvement title> + +## Observed during +... + +## Observation +... + +## Evidence +... + +## Why it matters +... + +## Possible direction +... + +## Relationship to current work +... +``` + +## Conflict body shape + +```markdown +# <Conflict title> + +## Position A +... + +## Position B +... + +## Evidence +... + +## Impact +... + +## Required resolution +... + +## Resolution +unresolved | resolved | deferred | blocked +``` diff --git a/plugins/paad/skills/kb-brain/scripts/kb_brain.py b/plugins/paad/skills/kb-brain/scripts/kb_brain.py new file mode 100644 index 0000000..4130f65 --- /dev/null +++ b/plugins/paad/skills/kb-brain/scripts/kb_brain.py @@ -0,0 +1,2227 @@ +#!/usr/bin/env python3 +"""KB-Brain: deterministic scaffold, index, check, seal, and amend tooling. + +Stdlib only. Designed to be copied into a target repository's scripts/ +directory during `init`. Safe to re-run; fails clearly without destructive +partial changes where practical. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import shutil +import sys +from datetime import date, datetime, timezone +from pathlib import Path +from typing import Any, Iterable + +VERSION = "1.0.0" + +TOP_SECTIONS = [ + "decisions", + "architecture", + "domains", + "runbooks", + "gotchas", + "briefs", + "specs", + "plans", + "reviews", + "learnings", + "open-questions", + "agents", + "improvements", + "tech-debt", + "templates", + "work", +] + +WORKSPACE_LEVELS = ("minimal", "standard", "strict") +DEFAULT_LEVEL = "standard" + +# Atomic record prefixes and their default directories (relative to a workspace +# or permanent section root, depending on type). +PREFIXES = { + "decision": "D", + "finding": "F", + "question": "Q", + "failure": "X", + "conflict": "C", + "handoff": "H", + "improvement": "I", + "tech-debt": "TD", + "assumption": "A", + "dependency": "DEP", + "scope-change": "SC", + "amendment": "AM", + "milestone": "M", +} + +SECTION_TO_TYPE = { + "decisions": "decision", + "findings": "finding", + "questions": "question", + "failures": "failure", + "conflicts": "conflict", + "handoffs": "handoff", + "improvements": "improvement", + "tech-debt": "tech-debt", + "assumptions": "assumption", + "dependencies": "dependency", + "scope-changes": "scope-change", + "amendments": "amendment", + "open-questions": "question", +} + +VALID_STATUSES = { + "open", + "closed", + "resolved", + "deferred", + "blocked", + "unresolved", + "accepted", + "rejected", + "superseded", + "active", + "done", + "promoted", + "abandoned", + "review-needed", + "approved-spec", + "brief", + "incubating", + "ready-for-expansion", + "expanding", + "planned", + "in-progress", + "completed", +} + +SECRET_PATTERNS = [ + re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"), + re.compile(r"(?i)api[_-]?key\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{20,}"), + re.compile(r"(?i)secret\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{16,}"), + re.compile(r"(?i)password\s*[:=]\s*['\"]?[^\s'\"]{8,}"), + re.compile(r"ghp_[A-Za-z0-9]{36}"), + re.compile(r"sk-[A-Za-z0-9]{20,}"), + re.compile(r"xox[baprs]-[A-Za-z0-9-]{10,}"), +] + +FRONTMATTER_RE = re.compile(r"\A---\r?\n(.*?)\r?\n---\r?\n?", re.DOTALL) +ID_RE = re.compile(r"^[A-Z]+-\d+$") +SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") + +MAKEFILE_BLOCK_BEGIN = "# >>> kb-brain targets (managed by kb_brain.py; do not edit by hand)" +MAKEFILE_BLOCK_END = "# <<< kb-brain targets" + +MAKEFILE_BLOCK = f"""{MAKEFILE_BLOCK_BEGIN} +.PHONY: kb-index kb-new kb-check kb-start kb-close kb-amend + +kb-index: +\tpython3 scripts/kb_brain.py index + +kb-new: +\tpython3 scripts/kb_brain.py new "$(SECTION)" "$(TITLE)" + +kb-check: +\tpython3 scripts/kb_brain.py check + +kb-start: +\tpython3 scripts/kb_brain.py start "$(SLUG)" "$(LEVEL)" + +kb-close: +\tpython3 scripts/kb_brain.py close "$(TASK)" + +kb-amend: +\tpython3 scripts/kb_brain.py amend "$(TASK)" "$(RECORD)" "$(TITLE)" +{MAKEFILE_BLOCK_END} +""" + +README_BODY = """# KB-Brain + +Repository-native working knowledge shared by humans, lead agents, and +sub-agents. Stable architecture and accepted documentation live under `docs/`. +This tree holds mutable context: decisions in flight, gaps, debt, lessons, and +focused task workspaces. + +## Index-first retrieval + +Do not read the entire KB by default. Normal order: + +1. `work/ACTIVE.md` +2. the selected workspace's `TASK.md` +3. `INDEX.md` (workspace, then repository) +4. `CONTEXT.md` +5. the agent's assignment +6. only linked or relevant atomic records + +## Permanent sections + +| Section | Meaning | +|---------|---------| +| `decisions/` | Accepted ADR-style decisions with owner and evidence | +| `architecture/` | Mutable notes that refine or challenge stable `docs/` architecture — always link back | +| `domains/` | Product and business domain knowledge | +| `runbooks/` | Recurring procedures not yet promoted to stable docs | +| `gotchas/` | Sharp edges, false starts, reliable warnings | +| `briefs/` | Human-owned briefs and atomic milestone records | +| `specs/` | Active working specifications, including candidate milestone expansions | +| `plans/` | Implementation plans tied to approved specs | +| `reviews/` | Review outputs worth preserving beyond their session | +| `learnings/` | Post-hoc lessons and retrospectives | +| `open-questions/` | Unresolved questions — agents may add evidence, never invent answers | +| `agents/` | Repository-specific agent behaviour and coordination notes | +| `improvements/` | One atomic file per noticed gap or opportunity | +| `tech-debt/` | Open debt plus `LEDGER.md`; resolved entries move to `tech-debt/closed/` | +| `templates/` | Project-local copies of record templates | +| `work/` | Active and closed task workspaces | + +## Workspace levels + +Configured in `AGENTS.md` (default `standard`). A task may raise its level; +lowering below the repository default requires explicit human approval. + +- **minimal** — scope, status, ownership, handoff, durable findings +- **standard** — findings, questions, decisions, failures, conflicts, handoffs +- **strict** — standard plus assumptions, dependencies, ownership, scope-change tracking + +## Closure and amendments + +Closed workspaces under `work/closed/` are immutable. Corrections go in +`amendments/` and are marked in regenerated indexes. See `SEAL.json`. + +## Tooling + +```bash +make kb-index +make kb-new SECTION=improvements TITLE="Example gap" +make kb-check +make kb-start SLUG=auth-migration LEVEL=strict +make kb-close TASK=2026-08-04-auth-migration +make kb-amend TASK=2026-08-04-auth-migration RECORD=findings/F-001-parser.md TITLE="Clarify parser note" +``` + +Or call `python3 scripts/kb_brain.py --help`. + +Bulk ingress from `docs/` is a dedicated operation — never dump architecture +documents into this tree during ordinary work. +""" + + +# --------------------------------------------------------------------------- +# Frontmatter (stdlib YAML subset) +# --------------------------------------------------------------------------- + +def parse_frontmatter(text: str) -> tuple[dict[str, Any], str]: + """Parse a minimal YAML frontmatter block into a dict and body.""" + match = FRONTMATTER_RE.match(text) + if not match: + return {}, text + meta: dict[str, Any] = {} + lines = match.group(1).splitlines() + i = 0 + while i < len(lines): + line = lines[i] + if not line.strip() or line.strip().startswith("#"): + i += 1 + continue + if ":" not in line: + i += 1 + continue + key, _, raw = line.partition(":") + key = key.strip() + value = raw.strip() + if value in ("", "|", ">"): + # Multi-line scalar or empty — collect indented follow-ons as list/string + items: list[str] = [] + i += 1 + while i < len(lines) and (lines[i].startswith(" ") or lines[i].startswith("\t") or lines[i].startswith("- ")): + item = lines[i].strip() + if item.startswith("- "): + items.append(item[2:].strip().strip("'\"")) + else: + items.append(item.strip("'\"")) + i += 1 + meta[key] = items if items else "" + continue + if value.startswith("[") and value.endswith("]"): + inner = value[1:-1].strip() + meta[key] = [p.strip().strip("'\"") for p in inner.split(",") if p.strip()] if inner else [] + elif value.lower() in ("true", "false"): + meta[key] = value.lower() == "true" + elif re.fullmatch(r"-?\d+", value): + meta[key] = int(value) + else: + meta[key] = value.strip("'\"").strip('"') + i += 1 + body = text[match.end() :] + return meta, body + + +def dump_frontmatter(meta: dict[str, Any], body: str = "") -> str: + lines = ["---"] + for key, value in meta.items(): + if isinstance(value, list): + if not value: + lines.append(f"{key}: []") + else: + lines.append(f"{key}:") + for item in value: + lines.append(f" - {item}") + elif isinstance(value, bool): + lines.append(f"{key}: {'true' if value else 'false'}") + else: + lines.append(f"{key}: {value}") + lines.append("---") + lines.append("") + if body and not body.startswith("\n"): + return "\n".join(lines) + body.lstrip("\n") + return "\n".join(lines) + (body or "") + + +# --------------------------------------------------------------------------- +# Path helpers +# --------------------------------------------------------------------------- + +def repo_root_from_cwd(start: Path | None = None) -> Path: + """Walk up looking for kb-brain/ or .git; fall back to cwd.""" + cur = (start or Path.cwd()).resolve() + for candidate in [cur, *cur.parents]: + if (candidate / "kb-brain").is_dir() or (candidate / ".git").exists(): + return candidate + return cur + + +def kb_root(root: Path) -> Path: + return root / "kb-brain" + + +def skill_dir() -> Path: + """Directory that owns templates/ — skill package or project kb-brain/.""" + here = Path(__file__).resolve().parent + # Skill package: .../kb-brain/scripts/kb_brain.py + if (here.parent / "templates").is_dir() or (here.parent / "SKILL.md").exists(): + return here.parent + # After init the script lives at <root>/scripts/kb_brain.py + root = here.parent + if (root / "kb-brain" / "templates").is_dir(): + return root / "kb-brain" + return here.parent + + +def today() -> str: + return date.today().isoformat() + + +def slugify(text: str) -> str: + text = text.strip().lower() + text = re.sub(r"[^a-z0-9]+", "-", text) + text = re.sub(r"-+", "-", text).strip("-") + return text or "item" + + +def ensure_dir(path: Path) -> None: + path.mkdir(parents=True, exist_ok=True) + + +def write_if_absent(path: Path, content: str) -> bool: + """Write content only if the file does not exist. Returns True if written.""" + if path.exists(): + return False + ensure_dir(path.parent) + path.write_text(content, encoding="utf-8") + return True + + +def write_text(path: Path, content: str) -> None: + ensure_dir(path.parent) + path.write_text(content, encoding="utf-8") + + +def read_text(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def sha256_file(path: Path) -> str: + h = hashlib.sha256() + with path.open("rb") as fh: + for chunk in iter(lambda: fh.read(65536), b""): + h.update(chunk) + return h.hexdigest() + + +def relative_to_kb(path: Path, root: Path) -> str: + try: + return str(path.resolve().relative_to(kb_root(root).resolve())) + except ValueError: + return str(path) + + +# --------------------------------------------------------------------------- +# Templates +# --------------------------------------------------------------------------- + +TEMPLATE_FILES = { + "TASK.md": """--- +id: {task_id} +type: task +status: active +level: {level} +lead: {lead} +author: {author} +created: {created} +updated: {created} +related: [] +--- + +# {title} + +## Objective +{objective} + +## Scope +- + +## Non-goals +- + +## Completion criteria +- + +## Current focus +- + +## Blockers +- + +## Controlling links +- brief: +- spec: +- issue: +- plan: +""", + "CONTEXT.md": """--- +id: {task_id}-context +type: context +status: active +author: {author} +created: {created} +updated: {created} +--- + +# Shared context + +Keep this brief. Move detail into atomic records and link them. + +## What matters right now +- + +## Pointers +- +""", + "ASSIGNMENTS.md": """--- +id: {task_id}-assignments +type: assignments +status: active +author: {author} +owner: {lead} +role: lead +created: {created} +updated: {created} +--- + +# Assignments + +Lead-controlled. Record each human or agent assignment, boundary, dependencies, and status. + +| Agent / human | Boundary | Depends on | Status | +|---------------|----------|------------|--------| +| | | | | +""", + "PROMOTION.md": """--- +id: {task_id}-promotion +type: promotion +status: open +author: {author} +created: {created} +updated: {created} +--- + +# Promotion tracking + +Durable information that must move into permanent KBB sections or stable `docs/` before closure. + +| Record | Destination | Status | +|--------|-------------|--------| +| | | | +""", + "finding.md": """--- +id: {id} +type: finding +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +evidence: [] +--- + +# {title} + +## Observation +- + +## Evidence +- + +## Impact +- +""", + "decision.md": """--- +id: {id} +type: decision +status: accepted +author: {author} +owner: {owner} +decision-owner: {owner} +role: lead +created: {created} +updated: {created} +related: [] +evidence: [] +--- + +# {title} + +## Context +- + +## Decision +- + +## Rationale +- + +## Evidence +- + +## Consequences +- +""", + "question.md": """--- +id: {id} +type: question +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +evidence: [] +--- + +# {title} + +## Question +- + +## Why it matters +- + +## Evidence so far +- + +## Answer +Unanswered. Do not invent an answer. +""", + "failure.md": """--- +id: {id} +type: failure +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +evidence: [] +--- + +# {title} + +## What was tried +- + +## Why it failed +- + +## Evidence +- + +## Lesson +- +""", + "conflict.md": """--- +id: {id} +type: conflict +status: unresolved +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +evidence: [] +--- + +# {title} + +## Position A +- + +## Position B +- + +## Evidence +- + +## Impact +- + +## Required resolution +- + +## Resolution +unresolved +""", + "handoff.md": """--- +id: {id} +type: handoff +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +--- + +# {title} + +## From +- + +## To +- + +## State of play +- + +## Next actions +- + +## Blockers +- +""", + "improvement.md": """--- +id: {id} +type: improvement +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +evidence: [] +--- + +# {title} + +## Observed during +- + +## Observation +- + +## Evidence +- + +## Why it matters +- + +## Possible direction +- + +## Relationship to current work +- +""", + "tech-debt.md": """--- +id: {id} +type: tech-debt +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +evidence: [] +--- + +# {title} + +## Liability +- + +## Evidence +- + +## Suggested remediation +- + +## Urgency +- +""", + "assumption.md": """--- +id: {id} +type: assumption +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +evidence: [] +--- + +# {title} + +## Assumption +- + +## If wrong +- + +## Validation needed +- +""", + "dependency.md": """--- +id: {id} +type: dependency +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +--- + +# {title} + +## Depends on +- + +## Needed by +- + +## Status +- +""", + "scope-change.md": """--- +id: {id} +type: scope-change +status: open +author: {author} +owner: {owner} +role: lead +created: {created} +updated: {created} +related: [] +--- + +# {title} + +## Change +- + +## Why +- + +## Approved by +- +""", + "closeout.md": """--- +id: {task_id}-closeout +type: closeout +status: done +author: {author} +owner: {lead} +role: lead +created: {created} +updated: {created} +--- + +# Closeout — {task_id} + +## Outcome +- + +## What was promoted +- + +## What was not promoted (and why) +- + +## Remaining risks +- + +## Follow-ups +- +""", + "amendment.md": """--- +id: {id} +type: amendment +status: open +author: {author} +owner: {owner} +created: {created} +updated: {created} +amends: {amends} +related: [] +evidence: [] +--- + +# {title} + +## What this corrects +- + +## Why +- + +## New evidence +- + +## Impact +- +""", + "BRIEF.md": """--- +id: brief-{slug} +type: brief +status: active +author: {author} +owner: {owner} +created: {created} +updated: {created} +--- + +# {title} + +## Intended outcome +- + +## Users and stakeholders +- + +## Why it matters +- + +## Constraints +- + +## Non-goals +- + +## Success at project level +- + +## Known milestone ideas +- + +## Open questions +- +""", + "MILESTONE.md": """--- +id: {id} +type: milestone +status: brief +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +--- + +# {title} + +## Outcome +- + +## Notes +- +""", + "MILESTONE-SPEC.md": """--- +id: {id}-spec +type: milestone-spec +status: review-needed +author: {author} +owner: {owner} +created: {created} +updated: {created} +related: [] +--- + +# {title} + +## Status +review-needed + +## Brief linkage +- + +## Outcome +- + +## Scope +- + +## Non-goals +- + +## User and system behaviour +- + +## Constraints and controlling decisions +- + +## Components and boundaries +- + +## Data and interface implications +- + +## Failure and recovery behaviour +- + +## Security, privacy, accessibility, and operational concerns +- + +## Dependencies +- + +## Acceptance criteria +- + +## Testing expectations +- + +## Open questions +- + +## Assumptions +- + +## Evidence consulted +- + +## Human approval +Unapproved candidate specification. +""", +} + + +def bundled_templates_dir() -> Path: + return skill_dir() / "templates" + + +def materialize_bundled_templates() -> None: + """Ensure skill-package templates/ exists with the canonical set.""" + dest = bundled_templates_dir() + ensure_dir(dest) + for name, content in TEMPLATE_FILES.items(): + path = dest / name + if not path.exists(): + # Store unsubstituted exemplars for humans/agents to copy. + exemplar = content + for key, sample in { + "{task_id}": "YYYY-MM-DD-slug", + "{level}": "standard", + "{lead}": "task-lead", + "{author}": "author", + "{owner}": "owner", + "{created}": "YYYY-MM-DD", + "{title}": "Title", + "{objective}": "Objective", + "{id}": "F-001", + "{slug}": "slug", + "{amends}": "path/to/record.md", + }.items(): + exemplar = exemplar.replace(key, sample) + write_text(path, exemplar) + + +def copy_templates_to_project(root: Path) -> None: + materialize_bundled_templates() + dest = kb_root(root) / "templates" + ensure_dir(dest) + for src in bundled_templates_dir().iterdir(): + if src.is_file(): + target = dest / src.name + if not target.exists(): + shutil.copy2(src, target) + + +def render_template(name: str, **kwargs: str) -> str: + materialize_bundled_templates() + # Prefer in-memory TEMPLATE_FILES for substitution fidelity. + if name in TEMPLATE_FILES: + text = TEMPLATE_FILES[name] + else: + text = read_text(bundled_templates_dir() / name) + for key, value in kwargs.items(): + text = text.replace("{" + key + "}", value) + # Leave any unused placeholders as-is only if they look intentional; + # replace common leftovers with empty/defaults. + return text + + +# --------------------------------------------------------------------------- +# Init +# --------------------------------------------------------------------------- + +def cmd_init(root: Path, level: str) -> int: + if level not in WORKSPACE_LEVELS: + print(f"FAIL: level must be one of {', '.join(WORKSPACE_LEVELS)}", file=sys.stderr) + return 2 + + kb = kb_root(root) + ensure_dir(kb) + + # Top-level sections — empty dirs with .gitkeep only (no atomic records). + for section in TOP_SECTIONS: + ensure_dir(kb / section) + + ensure_dir(kb / "tech-debt" / "closed") + write_if_absent( + kb / "tech-debt" / "LEDGER.md", + "# Technical debt ledger\n\n| ID | Title | Status | Path |\n|----|-------|--------|------|\n", + ) + + ensure_dir(kb / "work" / "active") + ensure_dir(kb / "work" / "closed") + + write_if_absent(kb / "README.md", README_BODY) + write_if_absent( + kb / "INDEX.md", + "# KB-Brain index\n\n_Generated. Run `make kb-index` or `python3 scripts/kb_brain.py index`._\n", + ) + write_if_absent( + kb / "work" / "ACTIVE.md", + "# Active work\n\n_No active KB-Brain workspaces._\n", + ) + + # Default level marker (read by start when AGENTS.md has no override). + write_if_absent( + kb / ".default-level", + level + "\n", + ) + + copy_templates_to_project(root) + install_script(root) + install_makefile_targets(root) + regenerate_repo_index(root) + regenerate_active(root) + + print(f"Initialized kb-brain/ at {kb} (default level: {level})") + print("Did not ingest docs/ or other existing documentation.") + return 0 + + +def install_script(root: Path) -> None: + src = Path(__file__).resolve() + dest_dir = root / "scripts" + ensure_dir(dest_dir) + dest = dest_dir / "kb_brain.py" + if dest.resolve() == src.resolve(): + return + shutil.copy2(src, dest) + dest.chmod(dest.stat().st_mode | 0o111) + + +def install_makefile_targets(root: Path) -> None: + makefile = root / "Makefile" + if not makefile.exists(): + inc = kb_root(root) / "Makefile.inc" + write_text( + inc, + f"# Include from your Makefile with: include kb-brain/Makefile.inc\n{MAKEFILE_BLOCK}\n", + ) + print(f"No Makefile found — wrote {inc.relative_to(root)} (include it from your build file)") + return + + text = read_text(makefile) + if MAKEFILE_BLOCK_BEGIN in text: + # Replace existing managed block idempotently. + pattern = re.compile( + re.escape(MAKEFILE_BLOCK_BEGIN) + r".*?" + re.escape(MAKEFILE_BLOCK_END), + re.DOTALL, + ) + text = pattern.sub(MAKEFILE_BLOCK.strip(), text) + write_text(makefile, text if text.endswith("\n") else text + "\n") + return + + separator = "" if text.endswith("\n") else "\n" + write_text(makefile, text + separator + "\n" + MAKEFILE_BLOCK + "\n") + + +def read_default_level(root: Path) -> str: + """Resolve repository default workspace level.""" + agents = root / "AGENTS.md" + if agents.exists(): + text = read_text(agents) + m = re.search( + r"Repository workspace level:\s*`?(minimal|standard|strict)`?", + text, + re.IGNORECASE, + ) + if m: + return m.group(1).lower() + marker = kb_root(root) / ".default-level" + if marker.exists(): + value = read_text(marker).strip().lower() + if value in WORKSPACE_LEVELS: + return value + return DEFAULT_LEVEL + + +# --------------------------------------------------------------------------- +# Workspace start +# --------------------------------------------------------------------------- + +def allocate_task_id(root: Path, slug: str) -> str: + slug = slugify(slug) + base = f"{today()}-{slug}" + active = kb_root(root) / "work" / "active" + closed = kb_root(root) / "work" / "closed" + existing = set() + for parent in (active, closed): + if parent.is_dir(): + existing.update(p.name for p in parent.iterdir() if p.is_dir()) + if base not in existing: + return base + n = 2 + while f"{base}-{n}" in existing: + n += 1 + return f"{base}-{n}" + + +def required_workspace_dirs(level: str) -> list[str]: + base = ["decisions", "findings", "questions", "failures", "conflicts", "handoffs"] + if level == "minimal": + return ["findings", "handoffs"] + if level == "strict": + return base + ["assumptions", "dependencies", "scope-changes"] + return base + + +def required_workspace_files(level: str) -> list[str]: + files = ["TASK.md", "INDEX.md", "CONTEXT.md", "PROMOTION.md"] + if level in ("standard", "strict"): + files.append("ASSIGNMENTS.md") + return files + + +def cmd_start(root: Path, slug: str, level: str | None, lead: str, author: str) -> int: + if not kb_root(root).is_dir(): + print("FAIL: kb-brain/ not found — run init first", file=sys.stderr) + return 2 + + repo_level = read_default_level(root) + chosen = (level or repo_level).lower() + if chosen not in WORKSPACE_LEVELS: + print(f"FAIL: level must be one of {', '.join(WORKSPACE_LEVELS)}", file=sys.stderr) + return 2 + + level_rank = {name: i for i, name in enumerate(WORKSPACE_LEVELS)} + if level_rank[chosen] < level_rank[repo_level]: + print( + f"FAIL: lowering workspace level below repository default " + f"({repo_level} → {chosen}) requires explicit human approval. " + f"Pass the repo default or higher, or obtain approval and re-run " + f"with --force-lower after documenting the approval.", + file=sys.stderr, + ) + return 2 + + task_id = allocate_task_id(root, slug) + ws = kb_root(root) / "work" / "active" / task_id + if ws.exists(): + print(f"FAIL: workspace already exists: {ws}", file=sys.stderr) + return 2 + + ensure_dir(ws) + created = today() + title = slug.replace("-", " ").strip().title() or task_id + + write_text( + ws / "TASK.md", + render_template( + "TASK.md", + task_id=task_id, + level=chosen, + lead=lead, + author=author, + created=created, + title=title, + objective=title, + ), + ) + write_text( + ws / "CONTEXT.md", + render_template( + "CONTEXT.md", + task_id=task_id, + author=author, + created=created, + ), + ) + write_text( + ws / "PROMOTION.md", + render_template( + "PROMOTION.md", + task_id=task_id, + author=author, + created=created, + ), + ) + if chosen in ("standard", "strict"): + write_text( + ws / "ASSIGNMENTS.md", + render_template( + "ASSIGNMENTS.md", + task_id=task_id, + author=author, + lead=lead, + created=created, + ), + ) + + for dirname in required_workspace_dirs(chosen): + ensure_dir(ws / dirname) + gitkeep = ws / dirname / ".gitkeep" + write_if_absent(gitkeep, "") + + regenerate_workspace_index(ws) + regenerate_active(root) + regenerate_repo_index(root) + + print(f"Started workspace {task_id} (level={chosen}) at {ws.relative_to(root)}") + return 0 + + +# --------------------------------------------------------------------------- +# Record creation +# --------------------------------------------------------------------------- + +def next_id(directory: Path, prefix: str) -> str: + highest = 0 + if directory.is_dir(): + for path in directory.iterdir(): + if not path.is_file() or path.name.startswith("."): + continue + meta, _ = parse_frontmatter(read_text(path)) + rid = str(meta.get("id", "")) + m = re.match(rf"^{re.escape(prefix)}-(\d+)$", rid) + if m: + highest = max(highest, int(m.group(1))) + else: + m2 = re.match(rf"^{re.escape(prefix)}-(\d+)", path.stem) + if m2: + highest = max(highest, int(m2.group(1))) + return f"{prefix}-{highest + 1:03d}" + + +def resolve_section_dir(root: Path, section: str, task_id: str | None) -> tuple[Path, str]: + """Return (directory, record_type) for a section name.""" + section = section.strip().strip("/") + if section in SECTION_TO_TYPE: + rtype = SECTION_TO_TYPE[section] + elif section in PREFIXES: + rtype = section + # Map type back to plural dir when needed + plural = { + "decision": "decisions", + "finding": "findings", + "question": "questions", + "failure": "failures", + "conflict": "conflicts", + "handoff": "handoffs", + "improvement": "improvements", + "tech-debt": "tech-debt", + "assumption": "assumptions", + "dependency": "dependencies", + "scope-change": "scope-changes", + "amendment": "amendments", + }.get(section, section) + section = plural + else: + raise ValueError(f"unknown section '{section}'") + + if section in ("improvements", "tech-debt", "open-questions", "decisions", "gotchas", "learnings"): + # Permanent sections (and decisions may also be workspace-local) + if task_id and section == "decisions": + ws = find_workspace(root, task_id) + if ws is None: + raise ValueError(f"task not found: {task_id}") + return ws / "decisions", rtype + if section == "open-questions": + return kb_root(root) / "open-questions", rtype + return kb_root(root) / section, rtype + + # Workspace-local sections + if not task_id: + active = list_active_workspaces(root) + if len(active) == 1: + task_id = active[0].name + elif not active: + raise ValueError("no active workspace — pass --task or start one") + else: + raise ValueError("multiple active workspaces — pass --task <id>") + + ws = find_workspace(root, task_id) + if ws is None: + raise ValueError(f"task not found: {task_id}") + return ws / section, rtype + + +def find_workspace(root: Path, task_id: str) -> Path | None: + for parent in ("active", "closed"): + candidate = kb_root(root) / "work" / parent / task_id + if candidate.is_dir(): + return candidate + return None + + +def list_active_workspaces(root: Path) -> list[Path]: + active = kb_root(root) / "work" / "active" + if not active.is_dir(): + return [] + return sorted(p for p in active.iterdir() if p.is_dir()) + + +def template_name_for_type(rtype: str) -> str: + return { + "decision": "decision.md", + "finding": "finding.md", + "question": "question.md", + "failure": "failure.md", + "conflict": "conflict.md", + "handoff": "handoff.md", + "improvement": "improvement.md", + "tech-debt": "tech-debt.md", + "assumption": "assumption.md", + "dependency": "dependency.md", + "scope-change": "scope-change.md", + "amendment": "amendment.md", + "milestone": "MILESTONE.md", + }[rtype] + + +def cmd_new( + root: Path, + section: str, + title: str, + task_id: str | None, + author: str, + owner: str, +) -> int: + if not kb_root(root).is_dir(): + print("FAIL: kb-brain/ not found — run init first", file=sys.stderr) + return 2 + try: + directory, rtype = resolve_section_dir(root, section, task_id) + except ValueError as exc: + print(f"FAIL: {exc}", file=sys.stderr) + return 2 + + ensure_dir(directory) + # Remove .gitkeep once real content arrives + gitkeep = directory / ".gitkeep" + if gitkeep.exists(): + gitkeep.unlink() + + prefix = PREFIXES[rtype] + rid = next_id(directory, prefix) + slug = slugify(title)[:60] + filename = f"{rid}-{slug}.md" + path = directory / filename + if path.exists(): + print(f"FAIL: refusing to overwrite {path}", file=sys.stderr) + return 2 + + created = today() + content = render_template( + template_name_for_type(rtype), + id=rid, + title=title, + author=author, + owner=owner, + created=created, + amends="", + slug=slug, + ) + write_text(path, content) + + if rtype == "tech-debt": + update_debt_ledger(root) + + # Refresh indexes + ws = directory + while ws.name not in ("active", "closed") and ws != kb_root(root) and ws.parent != ws: + if (ws / "TASK.md").exists(): + regenerate_workspace_index(ws) + break + ws = ws.parent + regenerate_repo_index(root) + regenerate_active(root) + + print(f"Created {path.relative_to(root)}") + return 0 + + +def update_debt_ledger(root: Path) -> None: + debt_dir = kb_root(root) / "tech-debt" + rows = ["# Technical debt ledger", "", "| ID | Title | Status | Path |", "|----|-------|--------|------|"] + for path in sorted(debt_dir.glob("TD-*.md")): + meta, body = parse_frontmatter(read_text(path)) + title = _heading(body) or path.stem + rows.append( + f"| {meta.get('id', path.stem)} | {title} | {meta.get('status', '')} | {path.relative_to(kb_root(root))} |" + ) + closed = debt_dir / "closed" + if closed.is_dir(): + for path in sorted(closed.glob("TD-*.md")): + meta, body = parse_frontmatter(read_text(path)) + title = _heading(body) or path.stem + rows.append( + f"| {meta.get('id', path.stem)} | {title} | closed | {path.relative_to(kb_root(root))} |" + ) + write_text(debt_dir / "LEDGER.md", "\n".join(rows) + "\n") + + +def _heading(body: str) -> str: + for line in body.splitlines(): + if line.startswith("# "): + return line[2:].strip() + return "" + + +# --------------------------------------------------------------------------- +# Index generation +# --------------------------------------------------------------------------- + +def iter_markdown_records(directory: Path) -> Iterable[Path]: + if not directory.is_dir(): + return + for path in sorted(directory.rglob("*.md")): + if path.name in ("INDEX.md", "README.md", "LEDGER.md", "ACTIVE.md", "CLOSEOUT.md"): + continue + if path.name.startswith("."): + continue + # Skip pure template copies at kb-brain/templates + if "templates" in path.parts: + continue + yield path + + +def regenerate_workspace_index(ws: Path) -> None: + amendments: dict[str, list[str]] = {} + amend_dir = ws / "amendments" + if amend_dir.is_dir(): + for path in amend_dir.glob("*.md"): + meta, _ = parse_frontmatter(read_text(path)) + target = str(meta.get("amends", "")).strip() + if target: + amendments.setdefault(target, []).append(path.name) + + lines = [ + f"# Index — {ws.name}", + "", + "_Generated by kb_brain.py. Do not edit by hand._", + "", + ] + groups: dict[str, list[str]] = {} + for path in iter_markdown_records(ws): + if path.parent.name == "amendments": + continue + meta, body = parse_frontmatter(read_text(path)) + rtype = str(meta.get("type", path.parent.name)) + status = str(meta.get("status", "")) + owner = str(meta.get("owner", meta.get("author", ""))) + rid = str(meta.get("id", path.stem)) + title = _heading(body) or path.stem + rel = path.relative_to(ws).as_posix() + marker = "" + for key, names in amendments.items(): + if key == rel or key.endswith(path.name) or key == rid: + marker = f" **[amended: {', '.join(names)}]**" + break + groups.setdefault(rtype, []).append( + f"- `{rid}` {title} — status={status or '—'}; owner={owner or '—'}; [{rel}]({rel}){marker}" + ) + + if not groups: + lines.append("_No atomic records yet._") + else: + for rtype in sorted(groups): + lines.append(f"## {rtype}") + lines.extend(groups[rtype]) + lines.append("") + + if amendments: + lines.append("## amendments") + for path in sorted(amend_dir.glob("*.md")): + meta, body = parse_frontmatter(read_text(path)) + lines.append( + f"- `{meta.get('id', path.stem)}` {_heading(body) or path.stem} — amends `{meta.get('amends', '')}`" + ) + lines.append("") + + write_text(ws / "INDEX.md", "\n".join(lines).rstrip() + "\n") + + +def regenerate_repo_index(root: Path) -> None: + kb = kb_root(root) + lines = [ + "# KB-Brain index", + "", + "_Generated by kb_brain.py. Do not edit by hand._", + "", + "## Active workspaces", + "", + ] + active = list_active_workspaces(root) + if not active: + lines.append("_None._") + else: + for ws in active: + lines.append(f"- [{ws.name}](work/active/{ws.name}/TASK.md)") + lines.append("") + lines.append("## Permanent sections") + lines.append("") + for section in TOP_SECTIONS: + if section in ("templates", "work"): + continue + directory = kb / section + records = [p for p in iter_markdown_records(directory) if p.parent == directory or section in p.parts] + # Only top-level section files for the summary (briefs/specs have nested trees) + count = len(list(directory.rglob("*.md"))) if directory.is_dir() else 0 + if section == "tech-debt": + count = len(list(directory.glob("TD-*.md"))) + len(list((directory / "closed").glob("TD-*.md"))) if directory.is_dir() else 0 + lines.append(f"- `{section}/` — {count} markdown file(s)") + lines.append("") + write_text(kb / "INDEX.md", "\n".join(lines)) + + +def parse_task_card(ws: Path) -> dict[str, Any]: + path = ws / "TASK.md" + if not path.exists(): + return {} + meta, body = parse_frontmatter(read_text(path)) + fields = { + "id": meta.get("id", ws.name), + "status": meta.get("status", "active"), + "level": meta.get("level", DEFAULT_LEVEL), + "lead": meta.get("lead", meta.get("owner", "")), + "objective": "", + "focus": "", + "blockers": "", + } + section = None + buckets: dict[str, list[str]] = {} + for line in body.splitlines(): + if line.startswith("## "): + section = line[3:].strip().lower() + buckets[section] = [] + continue + if section is not None: + buckets[section].append(line) + def section_text(name: str) -> str: + lines = [ln for ln in buckets.get(name, []) if ln.strip() and ln.strip() != "-"] + return " ".join(ln.strip("- ").strip() for ln in lines).strip() + + fields["objective"] = section_text("objective") or _heading(body) + fields["focus"] = section_text("current focus") + fields["blockers"] = section_text("blockers") + return fields + + +def regenerate_active(root: Path) -> None: + lines = [ + "# Active work", + "", + "_Generated from workspace TASK.md metadata. Do not edit by hand._", + "", + ] + active = list_active_workspaces(root) + if not active: + lines.append("_No active KB-Brain workspaces._") + else: + for ws in active: + info = parse_task_card(ws) + lines.append(f"## [{info['id']}](active/{ws.name}/TASK.md)") + lines.append("") + lines.append(f"- **Status:** {info['status']}") + lines.append(f"- **Level:** {info['level']}") + lines.append(f"- **Lead:** {info['lead'] or '—'}") + lines.append(f"- **Objective:** {info['objective'] or '—'}") + lines.append(f"- **Current focus:** {info['focus'] or '—'}") + lines.append(f"- **Blockers:** {info['blockers'] or '—'}") + lines.append("") + write_text(kb_root(root) / "work" / "ACTIVE.md", "\n".join(lines).rstrip() + "\n") + + +def cmd_index(root: Path, path: str | None) -> int: + if not kb_root(root).is_dir(): + print("FAIL: kb-brain/ not found — run init first", file=sys.stderr) + return 2 + if path: + target = Path(path) + if not target.is_absolute(): + target = root / target + if (target / "TASK.md").exists(): + regenerate_workspace_index(target) + print(f"Indexed workspace {target}") + else: + print(f"FAIL: not a workspace path: {path}", file=sys.stderr) + return 2 + else: + for ws in list_active_workspaces(root): + regenerate_workspace_index(ws) + closed = kb_root(root) / "work" / "closed" + if closed.is_dir(): + for ws in closed.iterdir(): + if ws.is_dir(): + regenerate_workspace_index(ws) + regenerate_active(root) + regenerate_repo_index(root) + update_debt_ledger(root) + print("Regenerated KB-Brain indexes") + return 0 + + +# --------------------------------------------------------------------------- +# Check / validation +# --------------------------------------------------------------------------- + +class CheckReport: + def __init__(self) -> None: + self.errors: list[str] = [] + self.warnings: list[str] = [] + + def error(self, msg: str) -> None: + self.errors.append(msg) + + def warn(self, msg: str) -> None: + self.warnings.append(msg) + + @property + def ok(self) -> bool: + return not self.errors + + +def scan_secrets(path: Path, report: CheckReport) -> None: + try: + text = read_text(path) + except UnicodeDecodeError: + return + for pattern in SECRET_PATTERNS: + if pattern.search(text): + report.error(f"possible secret pattern in {path}: {pattern.pattern[:40]}...") + + +def check_frontmatter_record(path: Path, report: CheckReport, require_owner_for_answered: bool = True) -> dict[str, Any]: + text = read_text(path) + meta, body = parse_frontmatter(text) + if not meta: + report.error(f"{path}: missing YAML frontmatter") + return {} + for field in ("id", "type", "status", "author", "created", "updated"): + if field not in meta or meta[field] in ("", None): + report.error(f"{path}: missing required frontmatter field '{field}'") + status = str(meta.get("status", "")).lower() + if status and status not in VALID_STATUSES: + report.warn(f"{path}: unusual status '{status}'") + rid = str(meta.get("id", "")) + if rid and not ID_RE.match(rid) and not rid.startswith("brief-") and "-context" not in rid and "-assignments" not in rid and "-promotion" not in rid and "-closeout" not in rid and not rid.endswith("-spec"): + # Allow task ids like 2026-08-04-slug + if not re.match(r"^\d{4}-\d{2}-\d{2}-[a-z0-9-]+$", rid): + report.warn(f"{path}: id '{rid}' does not match expected patterns") + + # Unresolved questions must not look answered without owner/evidence + if str(meta.get("type")) == "question" and status in ("answered", "resolved", "closed"): + if require_owner_for_answered: + if not meta.get("owner") and not meta.get("decision-owner"): + report.error(f"{path}: answered question lacks owner/decision-owner") + evidence = meta.get("evidence") or [] + answer_section = "## Answer" in body + if not evidence and answer_section: + # Check answer isn't the placeholder + after = body.split("## Answer", 1)[-1] + if "Unanswered" in after.split("##", 1)[0]: + report.error(f"{path}: question marked {status} but answer is still Unanswered") + elif not evidence: + report.warn(f"{path}: answered question has no evidence links") + + # Lead-only types should carry role: lead when status is confirmatory + if str(meta.get("type")) in ("decision", "scope-change") and status in ("accepted", "resolved", "done"): + if meta.get("role") != "lead" and not meta.get("decision-owner"): + report.error(f"{path}: confirmed {meta.get('type')} lacks role: lead or decision-owner") + + # Relative links + for match in re.finditer(r"\[([^\]]*)\]\(([^)]+)\)", body): + href = match.group(2) + if href.startswith(("http://", "https://", "mailto:", "#")): + continue + target = (path.parent / href).resolve() + if not target.exists(): + report.warn(f"{path}: broken relative link ({href})") + + scan_secrets(path, report) + return meta + + +def check_workspace(ws: Path, report: CheckReport, sealed: bool = False) -> None: + task = ws / "TASK.md" + if not task.exists(): + report.error(f"{ws}: missing TASK.md") + return + meta, _ = parse_frontmatter(read_text(task)) + level = str(meta.get("level", DEFAULT_LEVEL)) + if level not in WORKSPACE_LEVELS: + report.error(f"{task}: invalid level '{level}'") + level = DEFAULT_LEVEL + + for name in required_workspace_files(level): + if not (ws / name).exists(): + report.error(f"{ws}: missing required file {name} for level={level}") + + for dirname in required_workspace_dirs(level): + if not (ws / dirname).is_dir(): + report.error(f"{ws}: missing required directory {dirname}/ for level={level}") + + ids_seen: dict[str, Path] = {} + for path in iter_markdown_records(ws): + rec_meta = check_frontmatter_record(path, report) + rid = str(rec_meta.get("id", "")) + if rid: + if rid in ids_seen: + report.error(f"duplicate id '{rid}' in {ids_seen[rid]} and {path}") + else: + ids_seen[rid] = path + + if sealed: + seal = ws / "SEAL.json" + if not seal.exists(): + report.error(f"{ws}: closed workspace missing SEAL.json") + else: + verify_seal(ws, report) + if not (ws / "CLOSEOUT.md").exists(): + report.error(f"{ws}: closed workspace missing CLOSEOUT.md") + # Amendments must point at existing sealed records + amend_dir = ws / "amendments" + if amend_dir.is_dir(): + for path in amend_dir.glob("*.md"): + ameta, _ = parse_frontmatter(read_text(path)) + target = str(ameta.get("amends", "")).strip() + if not target: + report.error(f"{path}: amendment missing 'amends'") + continue + candidate = ws / target + if not candidate.exists(): + # try by basename + matches = list(ws.rglob(Path(target).name)) + if not matches: + report.error(f"{path}: amends target not found: {target}") + + +def verify_seal(ws: Path, report: CheckReport) -> None: + seal_path = ws / "SEAL.json" + try: + seal = json.loads(read_text(seal_path)) + except json.JSONDecodeError as exc: + report.error(f"{seal_path}: invalid JSON ({exc})") + return + expected = seal.get("files", {}) + if not isinstance(expected, dict): + report.error(f"{seal_path}: 'files' must be an object") + return + + current: dict[str, str] = {} + for path in ws.rglob("*"): + if not path.is_file(): + continue + rel = path.relative_to(ws).as_posix() + if rel == "SEAL.json": + continue + if rel == "INDEX.md": + continue + if rel.startswith("amendments/"): + continue + current[rel] = sha256_file(path) + + for rel, digest in expected.items(): + if rel not in current: + report.error(f"{ws}: sealed file missing or renamed: {rel}") + elif current[rel] != digest: + report.error(f"{ws}: sealed file mutated: {rel}") + for rel in current: + if rel not in expected: + report.error(f"{ws}: unsealed new file in closed workspace (use amendments/): {rel}") + + +def cmd_check(root: Path, path: str | None) -> int: + if not kb_root(root).is_dir(): + print("FAIL: kb-brain/ not found — run init first", file=sys.stderr) + return 2 + + report = CheckReport() + kb = kb_root(root) + + for section in TOP_SECTIONS: + if not (kb / section).exists(): + report.error(f"missing top-level section: {section}/") + + # Permanent section records + for section in ("decisions", "improvements", "open-questions", "gotchas", "learnings"): + directory = kb / section + if not directory.is_dir(): + continue + ids: dict[str, Path] = {} + for path in directory.glob("*.md"): + if path.name in ("INDEX.md", "README.md"): + continue + meta = check_frontmatter_record(path, report) + rid = str(meta.get("id", "")) + if rid: + if rid in ids: + report.error(f"duplicate id '{rid}' in {ids[rid]} and {path}") + ids[rid] = path + + debt = kb / "tech-debt" + if debt.is_dir(): + if not (debt / "LEDGER.md").exists(): + report.error("tech-debt/LEDGER.md missing") + for path in list(debt.glob("TD-*.md")) + list((debt / "closed").glob("TD-*.md")): + check_frontmatter_record(path, report) + + # Workspaces + for ws in list_active_workspaces(root): + check_workspace(ws, report, sealed=False) + closed_root = kb / "work" / "closed" + if closed_root.is_dir(): + for ws in closed_root.iterdir(): + if ws.is_dir(): + check_workspace(ws, report, sealed=True) + + # ACTIVE.md consistency: blockers from TASK.md should appear + active_md = kb / "work" / "ACTIVE.md" + if active_md.exists(): + active_text = read_text(active_md) + for ws in list_active_workspaces(root): + info = parse_task_card(ws) + if info.get("blockers") and info["blockers"] not in active_text and info["blockers"] != "—": + report.warn(f"ACTIVE.md missing blockers text for {ws.name}; regenerate with index") + if "conflict" in active_text.lower() and "## conflict" in active_text.lower(): + report.error("ACTIVE.md must not list conflicts") + + if path: + target = Path(path) + if not target.is_absolute(): + target = (root / target).resolve() + # path-scoped checks already covered; keep CLI compatible + _ = target + + for warn in report.warnings: + print(f"WARN: {warn}") + for err in report.errors: + print(f"ERROR: {err}") + + if report.ok: + print("kb-check: OK") + return 0 + print(f"kb-check: {len(report.errors)} error(s), {len(report.warnings)} warning(s)") + return 1 + + +# --------------------------------------------------------------------------- +# Close / seal / amend +# --------------------------------------------------------------------------- + +def build_seal(ws: Path) -> dict[str, Any]: + files: dict[str, str] = {} + for path in sorted(ws.rglob("*")): + if not path.is_file(): + continue + rel = path.relative_to(ws).as_posix() + if rel == "SEAL.json" or rel == "INDEX.md" or rel.startswith("amendments/"): + continue + files[rel] = sha256_file(path) + return { + "version": 1, + "sealed_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "task_id": ws.name, + "files": files, + } + + +def cmd_close(root: Path, task_id: str, author: str, lead: str) -> int: + ws = kb_root(root) / "work" / "active" / task_id + if not ws.is_dir(): + print(f"FAIL: active workspace not found: {task_id}", file=sys.stderr) + return 2 + + # Require promotion tracking present + if not (ws / "PROMOTION.md").exists(): + print("FAIL: PROMOTION.md missing — complete promotion tracking before close", file=sys.stderr) + return 2 + + created = today() + if not (ws / "CLOSEOUT.md").exists(): + write_text( + ws / "CLOSEOUT.md", + render_template( + "closeout.md", + task_id=task_id, + author=author, + lead=lead, + created=created, + ), + ) + + ensure_dir(ws / "amendments") + + # Mark task closed in TASK.md + task_path = ws / "TASK.md" + meta, body = parse_frontmatter(read_text(task_path)) + meta["status"] = "closed" + meta["updated"] = created + write_text(task_path, dump_frontmatter(meta, body)) + + regenerate_workspace_index(ws) + + # Validate before sealing + report = CheckReport() + check_workspace(ws, report, sealed=False) + # Soft: don't require seal yet + if report.errors: + for err in report.errors: + # Ignore seal-related (not sealed yet) + if "SEAL.json" in err or "CLOSEOUT.md" in err: + continue + print(f"ERROR: {err}") + # Still allow close if only missing seal — but surface other errors + real = [e for e in report.errors if "SEAL.json" not in e] + if real: + print("FAIL: fix validation errors before closing", file=sys.stderr) + return 1 + + seal = build_seal(ws) + write_text(ws / "SEAL.json", json.dumps(seal, indent=2, sort_keys=True) + "\n") + + dest_parent = kb_root(root) / "work" / "closed" + ensure_dir(dest_parent) + dest = dest_parent / task_id + if dest.exists(): + print(f"FAIL: closed path already exists: {dest}", file=sys.stderr) + return 2 + shutil.move(str(ws), str(dest)) + + regenerate_active(root) + regenerate_repo_index(root) + regenerate_workspace_index(dest) + + # Final seal verification + report2 = CheckReport() + check_workspace(dest, report2, sealed=True) + if report2.errors: + for err in report2.errors: + print(f"ERROR: {err}") + print("FAIL: closed workspace failed seal validation", file=sys.stderr) + return 1 + + print(f"Closed and sealed workspace {task_id} → work/closed/{task_id}") + return 0 + + +def cmd_amend(root: Path, task_id: str, record_path: str, title: str, author: str, owner: str) -> int: + ws = kb_root(root) / "work" / "closed" / task_id + if not ws.is_dir(): + print(f"FAIL: closed workspace not found: {task_id}", file=sys.stderr) + return 2 + + target = Path(record_path) + if not target.is_absolute(): + candidate = ws / record_path + if candidate.exists(): + target = candidate + else: + matches = list(ws.rglob(Path(record_path).name)) + if len(matches) == 1: + target = matches[0] + else: + print(f"FAIL: cannot resolve record path: {record_path}", file=sys.stderr) + return 2 + + try: + rel = target.resolve().relative_to(ws.resolve()).as_posix() + except ValueError: + print("FAIL: record is not inside the closed workspace", file=sys.stderr) + return 2 + + if rel == "SEAL.json" or rel.startswith("amendments/"): + print("FAIL: cannot amend SEAL.json or another amendment via this path", file=sys.stderr) + return 2 + + ensure_dir(ws / "amendments") + rid = next_id(ws / "amendments", "AM") + slug = slugify(title)[:60] + path = ws / "amendments" / f"{rid}-{slug}.md" + content = render_template( + "amendment.md", + id=rid, + title=title, + author=author, + owner=owner, + created=today(), + amends=rel, + ) + write_text(path, content) + regenerate_workspace_index(ws) + print(f"Created amendment {path.relative_to(root)} (amends {rel})") + return 0 + + +# --------------------------------------------------------------------------- +# Brief helpers (used by brief-ruminate skill; do not approve specs) +# --------------------------------------------------------------------------- + +def cmd_brief_init(root: Path, slug: str, title: str, author: str, owner: str) -> int: + if not kb_root(root).is_dir(): + print("FAIL: kb-brain/ not found — run init first", file=sys.stderr) + return 2 + slug = slugify(slug) + brief_dir = kb_root(root) / "briefs" / slug + if brief_dir.exists() and (brief_dir / "BRIEF.md").exists(): + print(f"Brief already exists at {brief_dir.relative_to(root)}") + return 0 + ensure_dir(brief_dir / "milestones") + write_text( + brief_dir / "BRIEF.md", + render_template( + "BRIEF.md", + slug=slug, + title=title or slug.replace("-", " ").title(), + author=author, + owner=owner, + created=today(), + ), + ) + write_text( + brief_dir / "INDEX.md", + f"# Brief index — {slug}\n\n_No milestones yet._\n", + ) + regenerate_repo_index(root) + print(f"Created brief at {brief_dir.relative_to(root)}") + return 0 + + +def regenerate_brief_index(brief_dir: Path) -> None: + lines = [f"# Brief index — {brief_dir.name}", "", "| ID | Title | Status | Spec |", "|----|-------|--------|------|"] + milestones = brief_dir / "milestones" + specs_root = kb_root(brief_dir.parents[1]) / "specs" / brief_dir.name if brief_dir.parents[1].name == "kb-brain" else brief_dir.parents[2] / "specs" / brief_dir.name + # brief_dir = kb-brain/briefs/<slug> + kb = brief_dir.parent.parent + specs_root = kb / "specs" / brief_dir.name + if milestones.is_dir(): + for path in sorted(milestones.glob("M-*.md")): + meta, body = parse_frontmatter(read_text(path)) + rid = str(meta.get("id", path.stem)) + status = str(meta.get("status", "")) + title = _heading(body) or path.stem + spec = "" + if specs_root.is_dir(): + matches = list(specs_root.glob(f"{rid}-*-spec.md")) + list(specs_root.glob(f"{rid}-spec.md")) + if matches: + spec = matches[0].relative_to(kb).as_posix() + lines.append(f"| {rid} | {title} | {status} | {spec or '—'} |") + if len(lines) == 4: + lines = [f"# Brief index — {brief_dir.name}", "", "_No milestones yet._"] + write_text(brief_dir / "INDEX.md", "\n".join(lines) + "\n") + + +def cmd_brief_milestone(root: Path, brief_slug: str, title: str, author: str, owner: str) -> int: + brief_dir = kb_root(root) / "briefs" / slugify(brief_slug) + if not (brief_dir / "BRIEF.md").exists(): + print(f"FAIL: brief not found: {brief_slug}", file=sys.stderr) + return 2 + ensure_dir(brief_dir / "milestones") + rid = next_id(brief_dir / "milestones", "M") + slug = slugify(title)[:60] + path = brief_dir / "milestones" / f"{rid}-{slug}.md" + write_text( + path, + render_template( + "MILESTONE.md", + id=rid, + title=title, + author=author, + owner=owner, + created=today(), + ), + ) + regenerate_brief_index(brief_dir) + print(f"Created milestone {path.relative_to(root)}") + return 0 + + +def cmd_brief_spec(root: Path, brief_slug: str, milestone_id: str, author: str, owner: str) -> int: + """Create a candidate milestone spec with status review-needed. Never approved.""" + brief_slug = slugify(brief_slug) + brief_dir = kb_root(root) / "briefs" / brief_slug + if not (brief_dir / "BRIEF.md").exists(): + print(f"FAIL: brief not found: {brief_slug}", file=sys.stderr) + return 2 + + matches = list((brief_dir / "milestones").glob(f"{milestone_id}-*.md")) + matches += list((brief_dir / "milestones").glob(f"{milestone_id}.md")) + if not matches: + print(f"FAIL: milestone not found: {milestone_id}", file=sys.stderr) + return 2 + milestone_path = matches[0] + meta, body = parse_frontmatter(read_text(milestone_path)) + title = _heading(body) or milestone_id + + # Refuse if somehow asked to write approved + specs_dir = kb_root(root) / "specs" / brief_slug + ensure_dir(specs_dir) + slug = slugify(title)[:60] + spec_path = specs_dir / f"{milestone_id}-{slug}-spec.md" + if spec_path.exists(): + existing_meta, _ = parse_frontmatter(read_text(spec_path)) + if str(existing_meta.get("status")) == "approved-spec": + print("FAIL: refusing to overwrite an approved specification", file=sys.stderr) + return 2 + print(f"Candidate spec already exists at {spec_path.relative_to(root)}") + return 0 + + content = render_template( + "MILESTONE-SPEC.md", + id=milestone_id, + title=title, + author=author, + owner=owner, + created=today(), + ) + # Hard-enforce review-needed in the written file + if "approved-spec" in content and "Human approval" not in content: + print("FAIL: template unexpectedly contains approved-spec", file=sys.stderr) + return 2 + write_text(spec_path, content) + + # Advance milestone status only as far as review-needed + meta["status"] = "review-needed" + meta["updated"] = today() + write_text(milestone_path, dump_frontmatter(meta, body)) + regenerate_brief_index(brief_dir) + regenerate_repo_index(root) + print(f"Created candidate spec {spec_path.relative_to(root)} (status=review-needed)") + print("Human approval required before planning or implementation.") + return 0 + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="KB-Brain tooling") + parser.add_argument("--root", type=Path, default=None, help="Repository root (default: auto-detect)") + parser.add_argument("--author", default="agent", help="Author metadata") + parser.add_argument("--owner", default="task-lead", help="Owner metadata") + parser.add_argument("--lead", default="task-lead", help="Lead metadata for workspaces") + sub = parser.add_subparsers(dest="command", required=True) + + p_init = sub.add_parser("init", help="Initialize kb-brain/ structure") + p_init.add_argument("level", nargs="?", default=DEFAULT_LEVEL, choices=WORKSPACE_LEVELS) + + p_start = sub.add_parser("start", help="Start a task workspace") + p_start.add_argument("slug") + p_start.add_argument("level", nargs="?", default=None, choices=WORKSPACE_LEVELS) + + p_new = sub.add_parser("new", help="Create an atomic record") + p_new.add_argument("section") + p_new.add_argument("title") + p_new.add_argument("--task", default=None) + + p_index = sub.add_parser("index", help="Regenerate indexes") + p_index.add_argument("path", nargs="?", default=None) + + p_check = sub.add_parser("check", help="Validate KB-Brain structure and seals") + p_check.add_argument("path", nargs="?", default=None) + + p_close = sub.add_parser("close", help="Close, seal, and archive a workspace") + p_close.add_argument("task_id") + + p_amend = sub.add_parser("amend", help="Amend a sealed closed workspace") + p_amend.add_argument("task_id") + p_amend.add_argument("record_path") + p_amend.add_argument("title") + + p_bi = sub.add_parser("brief-init", help="Scaffold a human-owned brief") + p_bi.add_argument("slug") + p_bi.add_argument("title", nargs="?", default="") + + p_bm = sub.add_parser("brief-milestone", help="Add a milestone under a brief") + p_bm.add_argument("brief_slug") + p_bm.add_argument("title") + + p_bs = sub.add_parser("brief-spec", help="Create a review-needed candidate milestone spec") + p_bs.add_argument("brief_slug") + p_bs.add_argument("milestone_id") + + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + root = (args.root or repo_root_from_cwd()).resolve() + + if args.command == "init": + return cmd_init(root, args.level) + if args.command == "start": + return cmd_start(root, args.slug, args.level, args.lead, args.author) + if args.command == "new": + return cmd_new(root, args.section, args.title, args.task, args.author, args.owner) + if args.command == "index": + return cmd_index(root, args.path) + if args.command == "check": + return cmd_check(root, args.path) + if args.command == "close": + return cmd_close(root, args.task_id, args.author, args.lead) + if args.command == "amend": + return cmd_amend(root, args.task_id, args.record_path, args.title, args.author, args.owner) + if args.command == "brief-init": + return cmd_brief_init(root, args.slug, args.title, args.author, args.owner) + if args.command == "brief-milestone": + return cmd_brief_milestone(root, args.brief_slug, args.title, args.author, args.owner) + if args.command == "brief-spec": + return cmd_brief_spec(root, args.brief_slug, args.milestone_id, args.author, args.owner) + parser.error(f"unknown command {args.command}") + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/plugins/paad/skills/kb-brain/templates/ASSIGNMENTS.md b/plugins/paad/skills/kb-brain/templates/ASSIGNMENTS.md new file mode 100644 index 0000000..b5996b1 --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/ASSIGNMENTS.md @@ -0,0 +1,18 @@ +--- +id: YYYY-MM-DD-slug-assignments +type: assignments +status: active +author: author +owner: task-lead +role: lead +created: YYYY-MM-DD +updated: YYYY-MM-DD +--- + +# Assignments + +Lead-controlled. Record each human or agent assignment, boundary, dependencies, and status. + +| Agent / human | Boundary | Depends on | Status | +|---------------|----------|------------|--------| +| | | | | diff --git a/plugins/paad/skills/kb-brain/templates/BRIEF.md b/plugins/paad/skills/kb-brain/templates/BRIEF.md new file mode 100644 index 0000000..69b2244 --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/BRIEF.md @@ -0,0 +1,35 @@ +--- +id: brief-slug +type: brief +status: active +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +--- + +# Title + +## Intended outcome +- + +## Users and stakeholders +- + +## Why it matters +- + +## Constraints +- + +## Non-goals +- + +## Success at project level +- + +## Known milestone ideas +- + +## Open questions +- diff --git a/plugins/paad/skills/kb-brain/templates/CONTEXT.md b/plugins/paad/skills/kb-brain/templates/CONTEXT.md new file mode 100644 index 0000000..7b4dbe6 --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/CONTEXT.md @@ -0,0 +1,18 @@ +--- +id: YYYY-MM-DD-slug-context +type: context +status: active +author: author +created: YYYY-MM-DD +updated: YYYY-MM-DD +--- + +# Shared context + +Keep this brief. Move detail into atomic records and link them. + +## What matters right now +- + +## Pointers +- diff --git a/plugins/paad/skills/kb-brain/templates/MILESTONE-SPEC.md b/plugins/paad/skills/kb-brain/templates/MILESTONE-SPEC.md new file mode 100644 index 0000000..2d91ef3 --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/MILESTONE-SPEC.md @@ -0,0 +1,66 @@ +--- +id: F-001-spec +type: milestone-spec +status: review-needed +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +--- + +# Title + +## Status +review-needed + +## Brief linkage +- + +## Outcome +- + +## Scope +- + +## Non-goals +- + +## User and system behaviour +- + +## Constraints and controlling decisions +- + +## Components and boundaries +- + +## Data and interface implications +- + +## Failure and recovery behaviour +- + +## Security, privacy, accessibility, and operational concerns +- + +## Dependencies +- + +## Acceptance criteria +- + +## Testing expectations +- + +## Open questions +- + +## Assumptions +- + +## Evidence consulted +- + +## Human approval +Unapproved candidate specification. diff --git a/plugins/paad/skills/kb-brain/templates/MILESTONE.md b/plugins/paad/skills/kb-brain/templates/MILESTONE.md new file mode 100644 index 0000000..b9f5f92 --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/MILESTONE.md @@ -0,0 +1,18 @@ +--- +id: F-001 +type: milestone +status: brief +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +--- + +# Title + +## Outcome +- + +## Notes +- diff --git a/plugins/paad/skills/kb-brain/templates/PROMOTION.md b/plugins/paad/skills/kb-brain/templates/PROMOTION.md new file mode 100644 index 0000000..3a3da1f --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/PROMOTION.md @@ -0,0 +1,16 @@ +--- +id: YYYY-MM-DD-slug-promotion +type: promotion +status: open +author: author +created: YYYY-MM-DD +updated: YYYY-MM-DD +--- + +# Promotion tracking + +Durable information that must move into permanent KBB sections or stable `docs/` before closure. + +| Record | Destination | Status | +|--------|-------------|--------| +| | | | diff --git a/plugins/paad/skills/kb-brain/templates/TASK.md b/plugins/paad/skills/kb-brain/templates/TASK.md new file mode 100644 index 0000000..b39f08d --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/TASK.md @@ -0,0 +1,37 @@ +--- +id: YYYY-MM-DD-slug +type: task +status: active +level: standard +lead: task-lead +author: author +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +--- + +# Title + +## Objective +Objective + +## Scope +- + +## Non-goals +- + +## Completion criteria +- + +## Current focus +- + +## Blockers +- + +## Controlling links +- brief: +- spec: +- issue: +- plan: diff --git a/plugins/paad/skills/kb-brain/templates/amendment.md b/plugins/paad/skills/kb-brain/templates/amendment.md new file mode 100644 index 0000000..be0fc21 --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/amendment.md @@ -0,0 +1,26 @@ +--- +id: F-001 +type: amendment +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +amends: path/to/record.md +related: [] +evidence: [] +--- + +# Title + +## What this corrects +- + +## Why +- + +## New evidence +- + +## Impact +- diff --git a/plugins/paad/skills/kb-brain/templates/assumption.md b/plugins/paad/skills/kb-brain/templates/assumption.md new file mode 100644 index 0000000..ce4a175 --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/assumption.md @@ -0,0 +1,22 @@ +--- +id: F-001 +type: assumption +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +evidence: [] +--- + +# Title + +## Assumption +- + +## If wrong +- + +## Validation needed +- diff --git a/plugins/paad/skills/kb-brain/templates/closeout.md b/plugins/paad/skills/kb-brain/templates/closeout.md new file mode 100644 index 0000000..75693ad --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/closeout.md @@ -0,0 +1,27 @@ +--- +id: YYYY-MM-DD-slug-closeout +type: closeout +status: done +author: author +owner: task-lead +role: lead +created: YYYY-MM-DD +updated: YYYY-MM-DD +--- + +# Closeout — YYYY-MM-DD-slug + +## Outcome +- + +## What was promoted +- + +## What was not promoted (and why) +- + +## Remaining risks +- + +## Follow-ups +- diff --git a/plugins/paad/skills/kb-brain/templates/conflict.md b/plugins/paad/skills/kb-brain/templates/conflict.md new file mode 100644 index 0000000..1c0947f --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/conflict.md @@ -0,0 +1,31 @@ +--- +id: F-001 +type: conflict +status: unresolved +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +evidence: [] +--- + +# Title + +## Position A +- + +## Position B +- + +## Evidence +- + +## Impact +- + +## Required resolution +- + +## Resolution +unresolved diff --git a/plugins/paad/skills/kb-brain/templates/decision.md b/plugins/paad/skills/kb-brain/templates/decision.md new file mode 100644 index 0000000..0f35d42 --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/decision.md @@ -0,0 +1,30 @@ +--- +id: F-001 +type: decision +status: accepted +author: author +owner: owner +decision-owner: owner +role: lead +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +evidence: [] +--- + +# Title + +## Context +- + +## Decision +- + +## Rationale +- + +## Evidence +- + +## Consequences +- diff --git a/plugins/paad/skills/kb-brain/templates/dependency.md b/plugins/paad/skills/kb-brain/templates/dependency.md new file mode 100644 index 0000000..d463c5d --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/dependency.md @@ -0,0 +1,21 @@ +--- +id: F-001 +type: dependency +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +--- + +# Title + +## Depends on +- + +## Needed by +- + +## Status +- diff --git a/plugins/paad/skills/kb-brain/templates/failure.md b/plugins/paad/skills/kb-brain/templates/failure.md new file mode 100644 index 0000000..930e7fa --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/failure.md @@ -0,0 +1,25 @@ +--- +id: F-001 +type: failure +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +evidence: [] +--- + +# Title + +## What was tried +- + +## Why it failed +- + +## Evidence +- + +## Lesson +- diff --git a/plugins/paad/skills/kb-brain/templates/finding.md b/plugins/paad/skills/kb-brain/templates/finding.md new file mode 100644 index 0000000..a28920a --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/finding.md @@ -0,0 +1,22 @@ +--- +id: F-001 +type: finding +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +evidence: [] +--- + +# Title + +## Observation +- + +## Evidence +- + +## Impact +- diff --git a/plugins/paad/skills/kb-brain/templates/handoff.md b/plugins/paad/skills/kb-brain/templates/handoff.md new file mode 100644 index 0000000..38a71a8 --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/handoff.md @@ -0,0 +1,27 @@ +--- +id: F-001 +type: handoff +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +--- + +# Title + +## From +- + +## To +- + +## State of play +- + +## Next actions +- + +## Blockers +- diff --git a/plugins/paad/skills/kb-brain/templates/improvement.md b/plugins/paad/skills/kb-brain/templates/improvement.md new file mode 100644 index 0000000..d63780a --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/improvement.md @@ -0,0 +1,31 @@ +--- +id: F-001 +type: improvement +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +evidence: [] +--- + +# Title + +## Observed during +- + +## Observation +- + +## Evidence +- + +## Why it matters +- + +## Possible direction +- + +## Relationship to current work +- diff --git a/plugins/paad/skills/kb-brain/templates/question.md b/plugins/paad/skills/kb-brain/templates/question.md new file mode 100644 index 0000000..bc9dd32 --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/question.md @@ -0,0 +1,25 @@ +--- +id: F-001 +type: question +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +evidence: [] +--- + +# Title + +## Question +- + +## Why it matters +- + +## Evidence so far +- + +## Answer +Unanswered. Do not invent an answer. diff --git a/plugins/paad/skills/kb-brain/templates/scope-change.md b/plugins/paad/skills/kb-brain/templates/scope-change.md new file mode 100644 index 0000000..acddf50 --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/scope-change.md @@ -0,0 +1,22 @@ +--- +id: F-001 +type: scope-change +status: open +author: author +owner: owner +role: lead +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +--- + +# Title + +## Change +- + +## Why +- + +## Approved by +- diff --git a/plugins/paad/skills/kb-brain/templates/tech-debt.md b/plugins/paad/skills/kb-brain/templates/tech-debt.md new file mode 100644 index 0000000..22ce32d --- /dev/null +++ b/plugins/paad/skills/kb-brain/templates/tech-debt.md @@ -0,0 +1,25 @@ +--- +id: F-001 +type: tech-debt +status: open +author: author +owner: owner +created: YYYY-MM-DD +updated: YYYY-MM-DD +related: [] +evidence: [] +--- + +# Title + +## Liability +- + +## Evidence +- + +## Suggested remediation +- + +## Urgency +- diff --git a/plugins/paad/skills/kb-brain/tests/test_kb_brain.py b/plugins/paad/skills/kb-brain/tests/test_kb_brain.py new file mode 100644 index 0000000..139b377 --- /dev/null +++ b/plugins/paad/skills/kb-brain/tests/test_kb_brain.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +"""Tests for KB-Brain deterministic tooling.""" + +from __future__ import annotations + +import json +import shutil +import sys +import tempfile +import unittest +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parents[1] / "scripts" +sys.path.insert(0, str(SCRIPT_DIR)) +import kb_brain # noqa: E402 + + +class KbBrainTestCase(unittest.TestCase): + def setUp(self) -> None: + self.tmp = Path(tempfile.mkdtemp(prefix="kb-brain-test-")) + self.root = self.tmp / "repo" + self.root.mkdir() + (self.root / "docs").mkdir() + (self.root / "docs" / "architecture.md").write_text("# Stable architecture\n", encoding="utf-8") + (self.root / "Makefile").write_text("help:\n\t@echo hi\n", encoding="utf-8") + + def tearDown(self) -> None: + shutil.rmtree(self.tmp, ignore_errors=True) + + def run_cmd(self, *argv: str) -> int: + return kb_brain.main(["--root", str(self.root), *argv]) + + def test_init_creates_structure_idempotent_no_docs_ingest(self) -> None: + self.assertEqual(self.run_cmd("init", "standard"), 0) + kb = self.root / "kb-brain" + for section in kb_brain.TOP_SECTIONS: + self.assertTrue((kb / section).is_dir(), section) + self.assertTrue((kb / "tech-debt" / "LEDGER.md").exists()) + self.assertTrue((kb / "work" / "ACTIVE.md").exists()) + self.assertTrue((kb / "templates" / "finding.md").exists()) + self.assertTrue((self.root / "scripts" / "kb_brain.py").exists()) + makefile = (self.root / "Makefile").read_text(encoding="utf-8") + self.assertIn("kb-index:", makefile) + + # docs untouched / not ingested as records + self.assertTrue((self.root / "docs" / "architecture.md").exists()) + self.assertFalse((kb / "architecture" / "architecture.md").exists()) + self.assertEqual(list((kb / "architecture").glob("*.md")), []) + self.assertNotIn( + "# Stable architecture", + (kb / "README.md").read_text(encoding="utf-8"), + ) + + # idempotent + self.assertEqual(self.run_cmd("init", "standard"), 0) + self.assertEqual(makefile.count("kb-index:"), (self.root / "Makefile").read_text(encoding="utf-8").count("kb-index:")) + + def test_start_level_dirs_and_collision_safe_ids(self) -> None: + self.run_cmd("init") + self.assertEqual(self.run_cmd("start", "auth-migration"), 0) + active = self.root / "kb-brain" / "work" / "active" + workspaces = list(active.iterdir()) + self.assertEqual(len(workspaces), 1) + ws = workspaces[0] + self.assertTrue(ws.name.endswith("auth-migration")) + for name in ("TASK.md", "INDEX.md", "CONTEXT.md", "ASSIGNMENTS.md", "PROMOTION.md"): + self.assertTrue((ws / name).exists(), name) + for dirname in ("findings", "questions", "decisions", "failures", "conflicts", "handoffs"): + self.assertTrue((ws / dirname).is_dir(), dirname) + self.assertFalse((ws / "assumptions").exists()) + + self.assertEqual(self.run_cmd("start", "auth-migration", "strict"), 0) + ids = sorted(p.name for p in active.iterdir()) + self.assertEqual(len(ids), 2) + self.assertTrue(any(name.endswith("auth-migration-2") for name in ids)) + strict_ws = next(p for p in active.iterdir() if p.name.endswith("auth-migration-2")) + self.assertTrue((strict_ws / "assumptions").is_dir()) + self.assertTrue((strict_ws / "dependencies").is_dir()) + self.assertTrue((strict_ws / "scope-changes").is_dir()) + + active_md = (self.root / "kb-brain" / "work" / "ACTIVE.md").read_text(encoding="utf-8") + self.assertIn("auth-migration", active_md) + self.assertIn("Lead:", active_md) + self.assertNotIn("## conflicts", active_md.lower()) + + def test_refuse_lower_than_repo_default(self) -> None: + self.run_cmd("init", "standard") + self.assertEqual(self.run_cmd("start", "too-low", "minimal"), 2) + + def test_routing_unique_ids_and_check(self) -> None: + self.run_cmd("init") + self.run_cmd("start", "parser") + task = next((self.root / "kb-brain" / "work" / "active").iterdir()).name + self.assertEqual( + self.run_cmd("new", "findings", "Import order bug", "--task", task), + 0, + ) + self.assertEqual( + self.run_cmd("new", "improvements", "Extract shared validator"), + 0, + ) + self.assertEqual( + self.run_cmd("new", "tech-debt", "Legacy parser dual path"), + 0, + ) + findings = list((self.root / "kb-brain" / "work" / "active" / task / "findings").glob("F-*.md")) + self.assertEqual(len(findings), 1) + improvements = list((self.root / "kb-brain" / "improvements").glob("I-*.md")) + self.assertEqual(len(improvements), 1) + self.assertTrue((self.root / "kb-brain" / "tech-debt" / "LEDGER.md").exists()) + ledger = (self.root / "kb-brain" / "tech-debt" / "LEDGER.md").read_text(encoding="utf-8") + self.assertIn("TD-001", ledger) + self.assertEqual(self.run_cmd("check"), 0) + + # duplicate id detection + dup = findings[0].parent / "F-001-duplicate.md" + dup.write_text(findings[0].read_text(encoding="utf-8"), encoding="utf-8") + self.assertEqual(self.run_cmd("check"), 1) + + def test_answered_question_without_owner(self) -> None: + self.run_cmd("init") + self.run_cmd("start", "qtest") + task = next((self.root / "kb-brain" / "work" / "active").iterdir()).name + self.run_cmd("new", "questions", "What is the timeout?", "--task", task) + q = next((self.root / "kb-brain" / "work" / "active" / task / "questions").glob("Q-*.md")) + text = q.read_text(encoding="utf-8") + text = text.replace("status: open", "status: resolved") + text = text.replace("owner: task-lead", "owner: ") + # remove owner line value + text = text.replace("\nowner: \n", "\n") + if "owner:" in text: + lines = [] + for line in text.splitlines(): + if line.startswith("owner:"): + continue + lines.append(line) + text = "\n".join(lines) + "\n" + text = text.replace("Unanswered. Do not invent an answer.", "Forty-two.") + q.write_text(text, encoding="utf-8") + self.assertEqual(self.run_cmd("check"), 1) + + def test_close_seal_mutation_and_amend(self) -> None: + self.run_cmd("init") + self.run_cmd("start", "seal-me") + task = next((self.root / "kb-brain" / "work" / "active").iterdir()).name + self.run_cmd("new", "findings", "Observed flake", "--task", task) + self.assertEqual(self.run_cmd("close", task), 0) + closed = self.root / "kb-brain" / "work" / "closed" / task + self.assertTrue((closed / "SEAL.json").exists()) + self.assertTrue((closed / "CLOSEOUT.md").exists()) + self.assertFalse((self.root / "kb-brain" / "work" / "active" / task).exists()) + seal = json.loads((closed / "SEAL.json").read_text(encoding="utf-8")) + self.assertIn("files", seal) + self.assertNotIn("SEAL.json", seal["files"]) + self.assertNotIn("INDEX.md", seal["files"]) + + finding = next(closed.joinpath("findings").glob("F-*.md")) + # mutation detected + finding.write_text(finding.read_text(encoding="utf-8") + "\nTampered\n", encoding="utf-8") + self.assertEqual(self.run_cmd("check"), 1) + + # restore from seal by rewriting original content is hard; re-close path — + # instead amend after fixing: recreate clean closed workspace + shutil.rmtree(self.root / "kb-brain") + self.run_cmd("init") + self.run_cmd("start", "seal-me") + task = next((self.root / "kb-brain" / "work" / "active").iterdir()).name + self.run_cmd("new", "findings", "Observed flake", "--task", task) + self.run_cmd("close", task) + closed = self.root / "kb-brain" / "work" / "closed" / task + finding = next(closed.joinpath("findings").glob("F-*.md")) + rel = finding.relative_to(closed).as_posix() + self.assertEqual(self.run_cmd("amend", task, rel, "Clarify flake note"), 0) + amend = list((closed / "amendments").glob("AM-*.md")) + self.assertEqual(len(amend), 1) + index = (closed / "INDEX.md").read_text(encoding="utf-8") + self.assertIn("amended", index.lower()) + self.assertEqual(self.run_cmd("check"), 0) + + # rename sealed file + finding.rename(finding.with_name("renamed.md")) + self.assertEqual(self.run_cmd("check"), 1) + + def test_brief_preserves_brief_and_candidate_not_approved(self) -> None: + self.run_cmd("init") + self.assertEqual(self.run_cmd("brief-init", "checkout", "Checkout redesign"), 0) + brief = self.root / "kb-brain" / "briefs" / "checkout" / "BRIEF.md" + original = brief.read_text(encoding="utf-8") + self.assertEqual(self.run_cmd("brief-milestone", "checkout", "Guest cart"), 0) + self.assertEqual(self.run_cmd("brief-spec", "checkout", "M-001"), 0) + self.assertEqual(brief.read_text(encoding="utf-8"), original) + specs = list((self.root / "kb-brain" / "specs" / "checkout").glob("*-spec.md")) + self.assertEqual(len(specs), 1) + meta, body = kb_brain.parse_frontmatter(specs[0].read_text(encoding="utf-8")) + self.assertEqual(meta.get("status"), "review-needed") + self.assertIn("Unapproved candidate specification", body) + self.assertNotEqual(meta.get("status"), "approved-spec") + + # milestone advanced only to review-needed + m = next((self.root / "kb-brain" / "briefs" / "checkout" / "milestones").glob("M-001-*.md")) + mmeta, _ = kb_brain.parse_frontmatter(m.read_text(encoding="utf-8")) + self.assertEqual(mmeta.get("status"), "review-needed") + + index = (self.root / "kb-brain" / "briefs" / "checkout" / "INDEX.md").read_text(encoding="utf-8") + self.assertIn("M-001", index) + self.assertIn("review-needed", index) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/convert_skills.py b/scripts/convert_skills.py index 40763a7..0c75ef5 100755 --- a/scripts/convert_skills.py +++ b/scripts/convert_skills.py @@ -203,6 +203,37 @@ def convert_skills(): shutil.copy2(ref_file, target) print(f" + {len(list(dst_refs.rglob('*.md')))} reference file(s)") + # Copy templates/ and scripts/ verbatim (md templates still neutralized + # so any paad/ output paths match the exported skill). These are part + # of the skill package for Kiro/Cursor/Antigravity users who copy the + # export tree; Claude Code and Pi load them from plugins/paad/skills. + for bundle in ("templates", "scripts"): + src_bundle = skill_path / bundle + dst_bundle = kiro_skill_dir / bundle + if dst_bundle.exists(): + shutil.rmtree(dst_bundle) + if not src_bundle.is_dir(): + continue + dst_bundle.mkdir() + count = 0 + for src_file in sorted(src_bundle.rglob("*")): + if "__pycache__" in src_file.parts or src_file.suffix in (".pyc", ".pyo"): + continue + target = dst_bundle / src_file.relative_to(src_bundle) + if src_file.is_dir(): + target.mkdir(exist_ok=True) + elif src_file.suffix == ".md": + text = neutralize(src_file.read_text(encoding="utf-8")) + text = re.sub(r"\n{3,}", "\n\n", text).strip() + "\n" + target.write_text(text, encoding="utf-8") + count += 1 + else: + ensure_parent = target.parent + ensure_parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src_file, target) + count += 1 + print(f" + {count} {bundle} file(s)") + # Write Antigravity wrapper h1_match = re.search(r'^#\s*(.*)', cleaned_content, re.MULTILINE)