Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# .gitattributes -- merge strategy and diff behaviour
#
# Policy: documentation is merged additively (union) so two branches appending
# to the same note never produce a conflict. Code is merged strictly, with
# conflict markers left intact for a human to resolve.
#
# NOTE: merge=union is only safe for files where line order does not carry
# meaning. It must NEVER be applied to code, config, or lock files.

# --- Documentation: additive merge -----------------------------------------
*.md merge=union
*.mdx merge=union
knowledge/**/*.md merge=union

# --- Code: strict merge, never union ---------------------------------------
*.py merge=text diff=python
*.js merge=text diff=javascript
*.ts merge=text diff=javascript
*.tsx merge=text diff=javascript
*.yml merge=text
*.yaml merge=text
*.json merge=text
*.toml merge=text

# --- Generated / lock files: do not attempt a content merge ----------------
*.lock merge=binary -diff
poetry.lock merge=binary -diff
package-lock.json merge=binary -diff
uv.lock merge=binary -diff

# --- Never show these in diffs or language stats ---------------------------
*.pyc -diff linguist-generated
__pycache__/ export-ignore
*.min.js -diff linguist-generated
*.min.css -diff linguist-generated

# --- Line endings -----------------------------------------------------------
* text=auto eol=lf
*.ps1 text eol=crlf
*.bat text eol=crlf

# --- Binary assets ----------------------------------------------------------
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.pdf binary
*.zip binary
38 changes: 38 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# CODEOWNERS -- who must review what
#
# Order matters: the LAST matching pattern wins. Put broad rules first and
# specific overrides below them.
#
# Owners below are placeholders pointing at the organisation. Replace them with
# the real team handles before relying on review routing -- an unresolvable
# owner means GitHub falls back to "any approver" for that path.

# --- Default: anything not matched below ------------------------------------
* @ZyntroAI

# --- Knowledge base: content requires a knowledge owner ---------------------
/knowledge/ @ZyntroAI
/knowledge/manifest.yml @ZyntroAI

# --- Agent task skills ------------------------------------------------------
/knowledge/agents/ @ZyntroAI

# --- CI and merge protection: the highest-consequence paths -----------------
/.github/workflows/ @ZyntroAI
/.github/CODEOWNERS @ZyntroAI
/.github/merge_rules.json @ZyntroAI
/.gitattributes @ZyntroAI

# --- Application code --------------------------------------------------------
/app/ @ZyntroAI
/backend/ @ZyntroAI
/deliverables/ @ZyntroAI
/skills/ @ZyntroAI

# --- Security-sensitive ------------------------------------------------------
/security/ @ZyntroAI
/.github/dependabot.yml @ZyntroAI

# --- Repo-wide policy --------------------------------------------------------
/CONTRIBUTING.md @ZyntroAI
/SECURITY.md @ZyntroAI
85 changes: 85 additions & 0 deletions .github/merge_rules.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{
"$comment": "Machine-readable merge policy for this repository. Advisory configuration: the enforcement is .github/workflows/protect-merge.yml plus the branch-protection rules on main. Keep the two in step.",
"version": 1,
"updated": "2026-09-14",

"protected_branches": ["main"],

"merge_strategy": {
"allowed": ["merge_commit"],
"default": "MERGE_COMMIT",
"squash_allowed": false,
"rebase_allowed": false,
"$comment": "History is preserved on main. Feature branches may squash internally, but the commit that lands on main is a merge commit."
},

"reviews": {
"required_approving_review_count": 2,
"dismiss_stale_reviews_on_push": true,
"require_review_from_codeowners": true,
"require_conversation_resolution": true
},

"status_checks": {
"require_branches_up_to_date": true,
"require_status_checks": true,
"required_checks": [
"protect-merge",
"CI",
"secret-scan"
]
},

"branch_rules": {
"require_linear_history": false,
"allow_force_pushes": false,
"allow_deletions": false,
"require_signed_commits": false
},

"protected_paths": [
{
"pattern": "knowledge/**",
"reason": "Curated knowledge notes are content-reviewed and sha256-indexed in knowledge/manifest.yml.",
"rules": ["require_codeowner_review", "block_force_push"]
},
{
"pattern": ".github/workflows/**",
"reason": "CI definitions can alter what runs on main. Changes here require explicit review.",
"rules": ["require_codeowner_review", "block_force_push", "require_additional_approval"]
},
{
"pattern": ".github/CODEOWNERS",
"reason": "Self-protecting: the review-routing rules themselves must be reviewed.",
"rules": ["require_codeowner_review"]
},
{
"pattern": ".github/merge_rules.json",
"reason": "Self-protecting: the merge policy itself must be reviewed.",
"rules": ["require_codeowner_review"]
},
{
"pattern": ".gitattributes",
"reason": "A wrong merge strategy can silently corrupt other files' merges.",
"rules": ["require_codeowner_review"]
},
{
"pattern": "security/**",
"reason": "Security controls and their tests.",
"rules": ["require_codeowner_review"]
}
],

"safety": {
"block_on_secret_detection": true,
"block_on_delete_of_protected_path": true,
"require_additive_only_for": ["knowledge/**", "docs/**"],
"$comment": "require_additive_only_for means files under these paths may be added or modified in a PR, but a PR that DELETES one is flagged for manual review."
},

"delete_protection": {
"enabled": true,
"patterns": ["knowledge/**", "docs/**", "security/**"],
"on_delete": "flag_for_review"
}
}
44 changes: 44 additions & 0 deletions knowledge/agents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Agent Task Skills

Six composable skills for managing a unit of work end to end -- plan it, run it,
grade it, unblock it, hand it over, and report on it.

They are deliberately small and dependency-free: pure Python, no network, no
state on disk. Each is a module with a dataclass vocabulary and one coordinator
class, so they compose in any pipeline and are trivial to unit test.

## Skills

| Skill | Responsibility | Entry point |
|---|---|---|
| [task-master](task-master/SKILL.md) | Decompose a goal, order by dependency, refuse cycles | `TaskMaster.plan()` |
| [result-orchestrator](result-orchestrator/SKILL.md) | Collect outcomes, apply quality gates, reduce to a verdict | `ResultOrchestrator.aggregate()` |
| [milestone-tracker](milestone-tracker/SKILL.md) | Grade timeline health from slip against schedule | `MilestoneTracker.health()` |
| [blocker-resolver](blocker-resolver/SKILL.md) | Classify obstacles and route escalations | `BlockerResolver.triage()` |
| [handoff-coordinator](handoff-coordinator/SKILL.md) | Gate a handoff on completeness | `HandoffCoordinator.receipt()` |
| [summary-reporter](summary-reporter/SKILL.md) | Render an executive update | `SummaryReporter.report()` |

## Lifecycle

```
goal --[task-master]--> waves
waves --[result-orchestrator]--> verdict + failure_reasons
verdict --[milestone-tracker]--> timeline health
health --[blocker-resolver]--> resolutions + escalations
work --[handoff-coordinator]--> accepted | rejected
all --[summary-reporter]--> executive update
```

## Design rules

- **Deterministic.** Same input, same output -- no clocks, no randomness.
- **Explicit failure.** A cycle, an unknown dependency, an unknown status, or an
incomplete handoff raises or is rejected; nothing is silently tolerated.
- **No hidden state.** Every skill is a plain class; construct it, call it, drop it.
- **Machine-first.** Coordinators return dicts; the reporter is the only renderer.

## Running the tests

```bash
python -m pytest knowledge/agents -q
```
49 changes: 49 additions & 0 deletions knowledge/agents/blocker-resolver/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
title: "Agent Skill: Blocker Resolver"
description: "Classify obstacles, attach a resolution playbook, and route escalations."
tags:
- agents/execution
- agents/recovery
- agents/skills
doc_kind: "skill"
status: "active"
owner: "Platform Engineering"
last_reviewed: "2026-09-13"
review_frequency: "Annual"
---

# Blocker Resolver

> Classify obstacles, attach the matching resolution playbook, and decide which ones need a human escalation before work can resume.

**Module:** `knowledge.agents.blocker_resolver.main` · **Version:** 1.0.0 · **Type:** `agents/execution/recovery`

## Contract

`Blocker Resolver` exposes one coordinator class. Classifies an obstacle against a fixed playbook and decides whether it can be cleared locally or needs a human.

## Usage

```python
from knowledge.agents.blocker_resolver.main import Blocker, BlockerResolver

out = BlockerResolver().triage([
Blocker("b1", "missing lib", "dependency"),
Blocker("b2", "no workflows scope", "permission", waiting_on="repo admin"),
])
# {"self_resolvable": 1, "escalation_ids": ["b2"]}
```

## Design notes

- Permission and unclear-requirement blockers always escalate; a machine
cannot grant itself a scope or invent a requirement.
- Any blocker escalates once its retry budget is spent, so a loop can
never spin indefinitely on a problem it cannot solve.
- An unrecognised category raises rather than guessing a playbook.

## Tests

```bash
python -m pytest knowledge/agents/blocker-resolver -q
```
106 changes: 106 additions & 0 deletions knowledge/agents/blocker-resolver/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Blocker Resolver -- obstacle classification and recovery routing."""
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Dict, List, Optional, Sequence

#: Resolution playbook keyed by blocker category.
PLAYBOOK: Dict[str, str] = {
"dependency": "pin the missing dependency or vendor the interface",
"permission": "request the scope from the resource owner; do not retry blindly",
"environment": "rebuild the environment from its recorded recipe",
"data": "locate the authoritative source and re-derive",
"unclear_requirement": "return to the requester with one specific question",
"external_service": "wait with backoff, then report the outage with evidence",
}

#: Categories that can never be cleared without a human decision.
ESCALATE_ALWAYS = frozenset({"permission", "unclear_requirement"})

#: Attempts allowed before an otherwise-resolvable blocker escalates.
MAX_SELF_ATTEMPTS = 2


@dataclass
class Blocker:
"""One obstacle holding up work."""

id: str
description: str
category: str
attempts: int = 0
waiting_on: Optional[str] = None

def __post_init__(self) -> None:
if self.category not in PLAYBOOK:
raise ValueError(
f"unknown category {self.category!r}; "
f"expected one of {sorted(PLAYBOOK)}"
)


@dataclass
class Resolution:
"""The recommended action for one blocker."""

blocker_id: str
category: str
action: str
escalate: bool
escalate_to: Optional[str] = None
owner_blocked: bool = False


class BlockerResolver:
"""Route blockers to a resolution or an escalation.

Args:
max_self_attempts: Retries before a blocker escalates anyway.
"""

def __init__(self, max_self_attempts: int = MAX_SELF_ATTEMPTS) -> None:
if max_self_attempts < 0:
raise ValueError("max_self_attempts cannot be negative")
self.max_self_attempts = max_self_attempts

def resolve(self, blocker: Blocker) -> Resolution:
"""Classify one blocker and recommend an action."""
must_escalate = blocker.category in ESCALATE_ALWAYS
exhausted = blocker.attempts >= self.max_self_attempts
escalate = must_escalate or exhausted

if escalate:
action = (
f"escalate to {blocker.waiting_on or 'the blocker owner'}: "
f"{PLAYBOOK[blocker.category]}"
)
else:
action = PLAYBOOK[blocker.category]

return Resolution(
blocker_id=blocker.id,
category=blocker.category,
action=action,
escalate=escalate,
escalate_to=blocker.waiting_on,
owner_blocked=blocker.waiting_on is not None,
)

def triage(self, blockers: Sequence[Blocker]) -> Dict[str, object]:
"""Resolve a whole set and split what you can fix from what you cannot."""
resolutions = [self.resolve(b) for b in blockers]
needs_human = [r for r in resolutions if r.escalate]
return {
"total": len(blockers),
"self_resolvable": len(resolutions) - len(needs_human),
"needs_escalation": len(needs_human),
"escalation_ids": [r.blocker_id for r in needs_human],
"resolutions": {
r.blocker_id: {
"category": r.category,
"action": r.action,
"escalate": r.escalate,
}
for r in resolutions
},
}
Loading
Loading