fix(cli+docs): tech debt sweep — UTF-8 truncation, CORS, stale refs (na-7r3)

Go fixes:
- Fix UTF-8 boundary slicing in narrative truncation (rune-safe truncateRunes helper)
- Add signal.Stop to rpi_serve signal handlers (prevent goroutine leak)
- Replace CORS wildcard with localhost-only validation (prevent cross-site reads)
- Surface SSE polling errors to client instead of silent continue
- Extract section name constants (HISTORY/INTEL/TASK) from magic strings
- Add intel_scope enum validation (none/topic/full)
- Sort resolveSkillPath glob matches for deterministic resolution
- Add random suffix to adhoc context IDs (prevent 1-second collision)
- Surface writePromptAuditTrail errors to stderr unconditionally
- Document RPI_RUN_ID env var in ao inject --help

Doc fixes:
- Replace stale /learn skill references with /retro --quick across 9 workflow docs
- Replace .agents/retros/ with .agents/council/ in post-mortem SKILL.md + references
- Reorder GOALS.md directives to sequential 1-8 numbering
- Rename post-mortem Phase 2 steps from 4.x to EX.x
- Update SKILL-TIERS.md: council + retro → council + knowledge lifecycle
- Sync all changes to skills-codex mirror

Epic: na-7r3 (6 issues, 2 waves)
This commit is contained in:
Boden Fuller
2026-03-04 10:07:01 -05:00
parent 201380a9ac
commit 2b338ee0c3
31 changed files with 248 additions and 96 deletions
+12 -12
View File
@@ -40,18 +40,6 @@ The north star claims every session is smarter than the last. Instrumented and v
**Steer:** decrease (maintain)
### 7. Enforce release cadence in pre-release gate
Policy: max 1 published release per week (security hotfixes exempt). Was violated (4 releases on 2026-02-27). Added `scripts/release-cadence-check.sh` with warn <7 days, block <1 day. Wired into `ci-local-release.sh` Phase 2. Gate: cadence check passes.
**Steer:** decrease (violation count)
### 8. Tighten cli/ complexity ceiling from 21 to 20
Two functions at CC 21 refactored: `buildHandoffContext` 21→10 (extracted renderHandoffEntry, renderDegradationWarnings, resolveNarrativeCap) and `runRPIServe` 21→12 (extracted runServeOrchestrate, runServeWatch, newDashboardServer). Gate threshold lowered from 21 to 20.
**Steer:** decrease (maintain)
### 5. Run Athena knowledge cycle daily
The flywheel captures learnings reactively. Athena mines git, `.agents/`, and code hotspots to extract signal that sessions missed, then defrags stale/duplicate learnings and flags oscillating evolve goals before they waste cycles. Gate: `ao defrag` report is ≤26 hours old and stale learning count ≤5.
@@ -64,6 +52,18 @@ Goals that alternate improved→fail for ≥3 consecutive cycles indicate the im
**Steer:** decrease
### 7. Enforce release cadence in pre-release gate
Policy: max 1 published release per week (security hotfixes exempt). Was violated (4 releases on 2026-02-27). Added `scripts/release-cadence-check.sh` with warn <7 days, block <1 day. Wired into `ci-local-release.sh` Phase 2. Gate: cadence check passes.
**Steer:** decrease (violation count)
### 8. Tighten cli/ complexity ceiling from 21 to 20
Two functions at CC 21 refactored: `buildHandoffContext` 21→10 (extracted renderHandoffEntry, renderDegradationWarnings, resolveNarrativeCap) and `runRPIServe` 21→12 (extracted runServeOrchestrate, runServeWatch, newDashboardServer). Gate threshold lowered from 21 to 20.
**Steer:** decrease (maintain)
## Gates
| ID | Check | Weight | Description |
+5 -1
View File
@@ -126,7 +126,11 @@ Examples:
ao inject --apply-decay # Apply confidence decay before ranking
ao inject --bead ag-7abc # Work-scoped injection for bead
ao inject --predecessor /path/to/handoff.md # Include predecessor context
ao inject --for=research "authentication" # Filtered by skill's context contract`,
ao inject --for=research "authentication" # Filtered by skill's context contract
Environment variables:
RPI_RUN_ID When set, --for uses this as the context artifact directory name
instead of generating an adhoc-<timestamp> ID. Set by /rpi orchestrator.`,
Args: cobra.MaximumNArgs(1),
RunE: runInject,
}
+36 -7
View File
@@ -4,11 +4,26 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"gopkg.in/yaml.v3"
)
// Section name constants for context filtering.
const (
sectionHistory = "HISTORY"
sectionIntel = "INTEL"
sectionTask = "TASK"
)
// validIntelScopes is the set of allowed intel_scope values.
var validIntelScopes = map[string]bool{
"none": true,
"topic": true,
"full": true,
}
// ContextDeclaration represents a skill's context window policy.
type ContextDeclaration struct {
Window string `yaml:"window"` // isolated, fork, inherit
@@ -113,6 +128,11 @@ func parseContextFromFrontmatter(frontmatter []byte) (*ContextDeclaration, error
return nil, err
}
}
if decl.IntelScope != "" {
if err := validateIntelScope(decl.IntelScope); err != nil {
return nil, err
}
}
return &decl, nil
}
@@ -133,6 +153,14 @@ func validateWindow(w string) error {
}
}
// validateIntelScope checks that the intel_scope value is one of the allowed values.
func validateIntelScope(scope string) error {
if validIntelScopes[scope] {
return nil
}
return fmt.Errorf("invalid context.intel_scope %q: must be none, topic, or full", scope)
}
// resolveSkillPath finds the SKILL.md for a given skill name.
// Search order: local repo, installed skills, plugin cache.
func resolveSkillPath(skillName string) (string, error) {
@@ -166,7 +194,8 @@ func resolveSkillPath(skillName string) (string, error) {
cachePattern := filepath.Join(home, ".claude", "plugins", "cache", "agentops-marketplace", "agentops", "*", "skills", skillName, "SKILL.md")
matches, _ := filepath.Glob(cachePattern)
if len(matches) > 0 {
return matches[0], nil
sort.Strings(matches)
return matches[len(matches)-1], nil // latest version (lexicographically highest)
}
return "", fmt.Errorf("skill %q not found", skillName)
@@ -192,14 +221,14 @@ func applyContextFilter(knowledge *injectedKnowledge, decl *ContextDeclaration)
for _, s := range decl.Sections.Include {
allowed[s] = true
}
if !allowed["HISTORY"] {
if !allowed[sectionHistory] {
knowledge.Sessions = nil
}
if !allowed["INTEL"] {
if !allowed[sectionIntel] {
knowledge.Learnings = nil
knowledge.Patterns = nil
}
if !allowed["TASK"] {
if !allowed[sectionTask] {
knowledge.BeadID = ""
knowledge.Predecessor = nil
}
@@ -207,12 +236,12 @@ func applyContextFilter(knowledge *injectedKnowledge, decl *ContextDeclaration)
// Exclude mode: zero sections in the exclude list.
for _, section := range decl.Sections.Exclude {
switch section {
case "HISTORY":
case sectionHistory:
knowledge.Sessions = nil
case "INTEL":
case sectionIntel:
knowledge.Learnings = nil
knowledge.Patterns = nil
case "TASK":
case sectionTask:
knowledge.BeadID = ""
knowledge.Predecessor = nil
}
+2 -1
View File
@@ -2,6 +2,7 @@ package main
import (
"fmt"
"math/rand"
"os"
"path/filepath"
"time"
@@ -12,7 +13,7 @@ import (
// Called automatically when --for is used; uses RPI_RUN_ID if set.
func contextArtifactDir(runID string) string {
if runID == "" {
runID = fmt.Sprintf("adhoc-%d", time.Now().Unix())
runID = fmt.Sprintf("adhoc-%d-%04x", time.Now().Unix(), rand.Intn(0x10000)) //nolint:gosec // non-cryptographic use
}
return filepath.Join(".agents", "context", runID)
}
+14 -6
View File
@@ -21,15 +21,23 @@ func TestContextArtifactDir_Empty(t *testing.T) {
if !strings.HasPrefix(got, prefix) {
t.Errorf("contextArtifactDir(\"\") = %q, want prefix %q", got, prefix)
}
// Verify the suffix after "adhoc-" is numeric
// Verify the suffix after "adhoc-" matches <timestamp>-<4hex>
suffix := strings.TrimPrefix(got, prefix)
if suffix == "" {
t.Errorf("contextArtifactDir(\"\") suffix is empty, expected numeric timestamp")
t.Errorf("contextArtifactDir(\"\") suffix is empty, expected timestamp-hex")
}
for _, c := range suffix {
if c < '0' || c > '9' {
t.Errorf("contextArtifactDir(\"\") suffix %q contains non-numeric character %q", suffix, string(c))
break
parts := strings.SplitN(suffix, "-", 2)
if len(parts) != 2 {
t.Errorf("contextArtifactDir(\"\") suffix %q expected format <timestamp>-<hex>, got %d parts", suffix, len(parts))
} else {
for _, c := range parts[0] {
if c < '0' || c > '9' {
t.Errorf("contextArtifactDir(\"\") timestamp part %q contains non-numeric character %q", parts[0], string(c))
break
}
}
if len(parts[1]) != 4 {
t.Errorf("contextArtifactDir(\"\") hex suffix %q expected 4 characters", parts[1])
}
}
}
+22
View File
@@ -537,3 +537,25 @@ func TestApplyContextFilter_IncludePrecedenceOverExclude(t *testing.T) {
}
}
func TestValidateIntelScope_Valid(t *testing.T) {
for _, scope := range []string{"none", "topic", "full"} {
if err := validateIntelScope(scope); err != nil {
t.Errorf("validateIntelScope(%q) = %v, want nil", scope, err)
}
}
}
func TestValidateIntelScope_Invalid(t *testing.T) {
if err := validateIntelScope("typo"); err == nil {
t.Error("validateIntelScope(\"typo\") = nil, want error")
}
}
func TestParseContextFromFrontmatter_InvalidIntelScope(t *testing.T) {
fm := []byte("context:\n window: fork\n intel_scope: invalid")
_, err := parseContextFromFrontmatter(fm)
if err == nil {
t.Error("parseContextFromFrontmatter with invalid intel_scope should error")
}
}
+2 -2
View File
@@ -463,7 +463,7 @@ func buildPromptForPhase(cwd string, phaseNum int, state *phasedState, _ *retryC
// Write provenance audit trail
if state.RunID != "" {
if err := writePromptAuditTrail(cwd, state.RunID, phaseNum, prompt.String()); err != nil {
VerbosePrintf("Warning: failed to write prompt audit trail for phase %d: %v\n", phaseNum, err)
fmt.Fprintf(os.Stderr, "Warning: failed to write prompt audit trail for phase %d: %v\n", phaseNum, err)
}
}
@@ -612,7 +612,7 @@ func buildRetryPrompt(cwd string, phaseNum int, state *phasedState, retryCtx *re
// Write provenance audit trail for retry
if state.RunID != "" {
if err := writePromptAuditTrail(cwd, state.RunID, phaseNum, prompt.String()); err != nil {
VerbosePrintf("Warning: failed to write retry audit trail for phase %d: %v\n", phaseNum, err)
fmt.Fprintf(os.Stderr, "Warning: failed to write retry audit trail for phase %d: %v\n", phaseNum, err)
}
}
+12 -2
View File
@@ -267,7 +267,7 @@ func renderHandoffEntry(sb *strings.Builder, h *phaseHandoff, manifest phaseMani
if narrativeCap > 0 && h.Narrative != "" {
narrative := h.Narrative
if len(narrative) > narrativeCap {
narrative = narrative[:narrativeCap] + "..."
narrative = truncateRunes(narrative, narrativeCap)
}
sb.WriteString(fmt.Sprintf("Narrative (from phase-%d-summary): %s\n", h.Phase, narrative))
}
@@ -325,7 +325,7 @@ func buildPhaseHandoffFromState(state *phasedState, phaseNum int, cwd string) *p
if content, err := os.ReadFile(matches[len(matches)-1]); err == nil {
narrative := string(content)
if len(narrative) > 2000 {
narrative = narrative[:2000]
narrative = truncateRunes(narrative, 2000)
}
h.Narrative = narrative
}
@@ -389,3 +389,13 @@ func discoverPhaseArtifacts(cwd string, phaseNum int) []string {
return artifacts
}
// truncateRunes truncates s to at most cap runes and appends "..." if truncated.
// Safe for multi-byte UTF-8 characters — avoids slicing mid-codepoint.
func truncateRunes(s string, cap int) string {
runes := []rune(s)
if len(runes) <= cap {
return s
}
return string(runes[:cap]) + "..."
}
+26
View File
@@ -418,6 +418,32 @@ func TestRenderHandoffField_EmptyString(t *testing.T) {
}
}
func TestTruncateRunes_MultiByteUTF8(t *testing.T) {
// Mix of ASCII and multi-byte characters (Japanese + emoji)
input := "Hello 世界! 🌍🌎🌏 more text here"
got := truncateRunes(input, 10)
// Should be first 10 runes + "..."
runes := []rune(input)
want := string(runes[:10]) + "..."
if got != want {
t.Errorf("truncateRunes(%q, 10) = %q, want %q", input, got, want)
}
// Verify result is valid UTF-8
for i, r := range got {
if r == '\uFFFD' {
t.Errorf("truncateRunes produced invalid UTF-8 at byte %d", i)
}
}
}
func TestTruncateRunes_ShortString(t *testing.T) {
input := "short"
got := truncateRunes(input, 100)
if got != input {
t.Errorf("truncateRunes(%q, 100) = %q, want %q (no truncation)", input, got, input)
}
}
func TestFormatVerdicts_Sorted(t *testing.T) {
verdicts := map[string]string{
"zebra": "FAIL",
+39 -7
View File
@@ -159,6 +159,7 @@ func runServeOrchestrate(cwd, goal string) error {
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(stop)
go func() {
<-stop
orchCancel()
@@ -224,6 +225,7 @@ func runServeWatch(cwd, watchRunID string) error {
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(stop)
go func() {
<-stop
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -275,7 +277,7 @@ func buildServeMux(root *serveMuxRoot, runID string) *http.ServeMux {
mux.HandleFunc("/", serveRPIIndex)
mux.HandleFunc("/events", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodOptions {
setCORSHeaders(w)
setCORSHeaders(w, r)
w.WriteHeader(http.StatusNoContent)
return
}
@@ -283,7 +285,7 @@ func buildServeMux(root *serveMuxRoot, runID string) *http.ServeMux {
})
mux.HandleFunc("/runs", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodOptions {
setCORSHeaders(w)
setCORSHeaders(w, r)
w.WriteHeader(http.StatusNoContent)
return
}
@@ -319,7 +321,7 @@ func serveRPIEvents(w http.ResponseWriter, r *http.Request, root, defaultRunID s
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Accel-Buffering", "no")
setCORSHeaders(w)
setCORSHeaders(w, r)
seen := 0
ticker := time.NewTicker(500 * time.Millisecond)
@@ -333,6 +335,11 @@ func serveRPIEvents(w http.ResponseWriter, r *http.Request, root, defaultRunID s
case <-ticker.C:
events, err := loadRPIC2Events(root, runID)
if err != nil {
errEvent := RPIC2Event{Type: "error", Message: err.Error(), Timestamp: time.Now().UTC().Format(time.RFC3339)}
if writeSSEEvent(w, errEvent) != nil {
return
}
flusher.Flush()
continue
}
for ; seen < len(events); seen++ {
@@ -354,20 +361,45 @@ func writeSSEEvent(w http.ResponseWriter, ev RPIC2Event) error {
return err
}
func serveRPIRuns(w http.ResponseWriter, _ *http.Request, root string) {
setCORSHeaders(w)
func serveRPIRuns(w http.ResponseWriter, r *http.Request, root string) {
setCORSHeaders(w, r)
runs := scanRegistryRuns(root)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(runs)
}
func setCORSHeaders(w http.ResponseWriter) {
w.Header().Set("Access-Control-Allow-Origin", "*")
func setCORSHeaders(w http.ResponseWriter, r ...*http.Request) {
origin := ""
if len(r) > 0 && r[0] != nil {
origin = r[0].Header.Get("Origin")
}
// Only allow localhost origins to prevent cross-site data exfiltration.
if origin == "" || isLocalhostOrigin(origin) {
if origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
} else {
w.Header().Set("Access-Control-Allow-Origin", "http://localhost")
}
}
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Max-Age", "86400")
}
// isLocalhostOrigin returns true if the origin is a localhost URL.
func isLocalhostOrigin(origin string) bool {
for _, prefix := range []string{
"http://localhost", "https://localhost",
"http://127.0.0.1", "https://127.0.0.1",
"http://[::1]", "https://[::1]",
} {
if strings.HasPrefix(origin, prefix) {
return true
}
}
return false
}
// openBrowserURL opens url in the default system browser.
func openBrowserURL(url string) {
var cmd *exec.Cmd
+21 -2
View File
@@ -135,10 +135,29 @@ func TestBuildServeMux(t *testing.T) {
}
func TestSetCORSHeaders(t *testing.T) {
// Without request, defaults to localhost
rr := httptest.NewRecorder()
setCORSHeaders(rr)
if v := rr.Header().Get("Access-Control-Allow-Origin"); v != "*" {
t.Errorf("CORS origin: want *, got %q", v)
if v := rr.Header().Get("Access-Control-Allow-Origin"); v != "http://localhost" {
t.Errorf("CORS origin (no request): want http://localhost, got %q", v)
}
// With localhost origin, echoes it back
rr = httptest.NewRecorder()
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Origin", "http://localhost:3000")
setCORSHeaders(rr, req)
if v := rr.Header().Get("Access-Control-Allow-Origin"); v != "http://localhost:3000" {
t.Errorf("CORS origin (localhost): want http://localhost:3000, got %q", v)
}
// With non-localhost origin, does not set origin header
rr = httptest.NewRecorder()
req = httptest.NewRequest("GET", "/", nil)
req.Header.Set("Origin", "http://evil.example.com")
setCORSHeaders(rr, req)
if v := rr.Header().Get("Access-Control-Allow-Origin"); v != "" {
t.Errorf("CORS origin (external): want empty, got %q", v)
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ What are you trying to do?
│
├─ "Learn from past work"
│ ├─ What do we know about X? ──► /knowledge <query>
│ ├─ Save this insight ─────────► /learn "insight"
│ ├─ Save this insight ─────────► /retro --quick "insight"
│ └─ Run a retrospective ───────► /retro
│
├─ "Parallelize work"
+3 -3
View File
@@ -163,7 +163,7 @@ Read CLAUDE.md # Reload primer context if needed
**Commands:**
```bash
/learn [topic] # Extract patterns
/retro --quick [topic] # Extract patterns
/bundle-save [topic]-learning # Share learnings
```
@@ -215,7 +215,7 @@ Read CLAUDE.md
/implement --resume [topic]-implementation-progress
# Complete implementation
/vibe recent
/learn [topic]
/retro --quick [topic]
# Context: 40-70k total (20-35%)
```
@@ -321,7 +321,7 @@ Read CLAUDE.md
**Session 4: Learn**
```bash
/learn redis-pooling-implementation
/retro --quick redis-pooling-implementation
# Agent extracts:
# Pattern: Redis Connection Pooling for Burst Traffic
# - When: High-traffic services with burst patterns
+5 -5
View File
@@ -39,7 +39,7 @@ Phase 4: Improve (10-15% context)
**Commands:**
```bash
/learn --retrospective --since "30 days ago"
/retro --retrospective --since "30 days ago"
```
**Activities:**
@@ -92,7 +92,7 @@ Low Impact, High Effort → SKIP
/plan [improvement]
/implement [improvement]-plan
/vibe recent
/learn [improvement]
/retro --quick [improvement]
```
**Output:** Improved system + new patterns
@@ -103,7 +103,7 @@ Low Impact, High Effort → SKIP
```bash
# Phase 1: Review
/learn --retrospective --since "2025-10-01"
/retro --retrospective --since "2025-10-01"
# Findings:
# - 15 commits reviewed
# - 3 patterns emerged (used 5+ times each)
@@ -124,10 +124,10 @@ Low Impact, High Effort → SKIP
/plan pre-commit-hook
/implement pre-commit-hook-plan
/vibe recent
/learn pre-commit-improvement
/retro --quick pre-commit-improvement
# Result: 90% reduction in linting issues
```
---
**Start continuous improvement with:** `/learn --retrospective`
**Start continuous improvement with:** `/retro --retrospective`
+2 -2
View File
@@ -100,7 +100,7 @@ Read CLAUDE.md
**Commands:**
```bash
/vibe recent
/learn [issue]-fix
/retro --quick [issue]-fix
```
**Activities:**
@@ -134,7 +134,7 @@ Read CLAUDE.md
# Phase 4: Verify
/vibe recent
# ✅ Load test: No failures at 3x traffic
/learn redis-pool-debugging
/retro --quick redis-pool-debugging
# Pattern: Intermittent failures + time correlation = resource exhaustion
```
+2 -2
View File
@@ -68,7 +68,7 @@ Phase 3: Document (5-10% context)
**Commands:**
```bash
/learn [topic]
/retro --quick [topic]
# Creates pattern documentation
# Updates knowledge base
```
@@ -103,7 +103,7 @@ Read CLAUDE.md
# Pattern: Middleware-based validation
# Best practice: Separate auth concerns from business logic
/learn auth-system-knowledge
/retro --quick auth-system-knowledge
# Documented for future reference
```
+1 -1
View File
@@ -115,7 +115,7 @@ Read CLAUDE.md
**Session 3: Integration**
```bash
# Both bundles loaded (technical + personal)
/learn k8s-deployment
/retro --quick k8s-deployment
# Pattern extracted:
# - Technical: K8s app deployment pattern
# - Personal: Career advancement evidence
+9 -9
View File
@@ -2,7 +2,7 @@
**Purpose:** Systematic retrospective after completing significant work
**Composes:** `/retro` -> `/learn` -> improvement creation
**Composes:** `/retro` -> `/post-mortem` -> improvement creation
**Failure Patterns Prevented:** Institutional memory loss, repeated mistakes, ecosystem gaps
@@ -17,7 +17,7 @@ Run /retro (failure pattern analysis)
↓
Identify Learnings (patterns, anti-patterns)
↓
Run /learn for each pattern
Run /retro --quick for each pattern
↓
Create Improvements (skills, workflows, agents)
↓
@@ -178,21 +178,21 @@ Extract patterns (what to repeat) and anti-patterns (what to avoid):
**Evidence:** 3 rollback commits in session
```
### Step 5: Run /learn for Each Pattern
### Step 5: Run /retro --quick for Each Pattern
Extract reusable patterns to pattern library:
```markdown
# For each pattern worth capturing:
/learn pattern="tracer-bullet-first" \
/retro --quick pattern="tracer-bullet-first" \
context="Infrastructure deployments with operators" \
what="Deploy minimal resource before full implementation" \
why="Catches admission webhook rejections early" \
when="Any deployment using CRDs with admission webhooks" \
evidence="Dify deployment 2025-11-27, saved 2h"
/learn pattern="version-check-before-docs" \
/retro --quick pattern="version-check-before-docs" \
context="Using external documentation" \
what="Check installed version before trusting docs" \
why="Docs may be for newer version with features you don't have" \
@@ -345,12 +345,12 @@ Based on gaps identified, create issues or drafts for:
→ Saves bundle
```
### With /learn Command
### With /retro Command
```markdown
# Each pattern identified gets captured
/learn pattern="name" context="..." what="..." why="..." when="..."
/retro --quick pattern="name" context="..." what="..." why="..." when="..."
# Patterns stored in:
# - Knowledge graph (Memory MCP)
@@ -381,7 +381,7 @@ infrastructure-deployment:
| Run /retro | 5 min | Failure pattern analysis |
| Ecosystem audit | 5 min | Tool usage review |
| Identify patterns | 10 min | Extract learnings |
| Run /learn | 5 min | Capture patterns |
| Run /retro --quick | 5 min | Capture patterns |
| Create improvements | 10 min | File issues/drafts |
| Save bundle | 2 min | Preserve context |
| **Total** | **~40 min** | |
@@ -418,7 +418,7 @@ Post-work retrospective is successful when:
# What tools helped? What was missing?
4. Extract patterns
/learn pattern="name" ...
/retro --quick pattern="name" ...
5. Create improvements
# File issues for skills/workflows needed
+1 -2
View File
@@ -241,7 +241,7 @@ Next time, just say "continue the caching work" and I'll pick up where you left
| Command | Purpose | When to Use |
|---------|---------|-------------|
| `/retro` | Session retrospective | After significant work |
| `/learn` | Extract reusable patterns | After solving problems |
| `/retro --quick` | Extract reusable patterns | After solving problems |
---
@@ -300,7 +300,6 @@ Next time, just say "continue the caching work" and I'll pick up where you left
/session-end
# optionally:
/retro
/learn
```
---
+2
View File
@@ -42,6 +42,8 @@ if [[ -z "$TAG_DATE" ]]; then
exit 0
fi
# Note: Uses rolling 86400-second window, not calendar-day boundaries.
# Both date +%s values are UTC epoch seconds, so subtraction is timezone-neutral.
NOW=$(date +%s)
DAYS_AGO=$(( (NOW - TAG_DATE) / 86400 ))
@@ -12,7 +12,7 @@ Defines the maturity pipeline for knowledge artifacts.
## Tier 1: Learnings (`.agents/learnings/`)
- **Source:** `/retro`, `/learn`, `/extract`, promoted from forge
- **Source:** `/retro`, `/post-mortem`, `/extract`, promoted from forge
- **Confidence:** 0.3-1.0
- **Citations:** 1+
- **Promotion criteria:** Promote to Tier 2 when confidence >= 0.8 AND cited >= 3 times AND age > 30 days
@@ -28,7 +28,7 @@ Defines the maturity pipeline for knowledge artifacts.
## Cross-Repo Promotion
- Any tier can be promoted to `~/.claude/patterns/` via `/learn --global`
- Any tier can be promoted to `~/.claude/patterns/` via `/retro --global`
- Global patterns are user-level, shared across all repositories
- Promotion is a manual decision (human judgment on cross-repo applicability)
- Global patterns are found by `/research`, `/knowledge`, and `/inject` via grep
+8 -8
View File
@@ -274,7 +274,7 @@ Enables adversarial two-round review for post-implementation validation. Use for
Inline extraction of learnings from the completed work (formerly delegated to the retro skill).
#### Step 4.1: Gather Context
#### Step EX.1: Gather Context
```bash
# Recent commits
@@ -291,7 +291,7 @@ Read relevant artifacts: research documents, plan documents, commit messages, co
**If retrospecting an epic:** Run the closure integrity quick-check from `references/context-gathering.md` (Phantom Bead Detection + Multi-Wave Regression Scan). Include any warnings in findings.
#### Step 4.2: Classify Learnings
#### Step EX.2: Classify Learnings
Ask these questions:
@@ -318,7 +318,7 @@ For each learning, capture:
- **Why it matters**: Impact on future work
- **Confidence**: high, medium, low
#### Step 4.3: Write Learnings
#### Step EX.3: Write Learnings
**Write to:** `.agents/learnings/YYYY-MM-DD-<topic>.md`
@@ -353,13 +353,13 @@ confidence: <high|medium|low>
...
```
#### Step 4.4: Classify Learning Scope
#### Step EX.4: Classify Learning Scope
For each learning extracted in Step 4.3, classify:
For each learning extracted in Step EX.3, classify:
**Question:** "Does this learning reference specific files, packages, or architecture in THIS repo? Or is it a transferable pattern that helps any project?"
- **Repo-specific** -> Write to `.agents/learnings/` (existing behavior from Step 4.3). Use `git rev-parse --show-toplevel` to resolve repo root — never write relative to cwd.
- **Repo-specific** -> Write to `.agents/learnings/` (existing behavior from Step EX.3). Use `git rev-parse --show-toplevel` to resolve repo root — never write relative to cwd.
- **Cross-cutting/transferable** -> Rewrite to remove repo-specific context (file paths, function names, package names), then:
1. Write abstracted version to `~/.agents/learnings/YYYY-MM-DD-<slug>.md` (NOT local — one copy only)
2. Run abstraction lint check:
@@ -377,7 +377,7 @@ For each learning extracted in Step 4.3, classify:
- Local: "Athena's validate package needs O_CREATE|O_EXCL for atomic claims because Zeus spawns concurrent workers"
- Global: "Use O_CREATE|O_EXCL for atomic file creation when multiple processes may race on the same path"
#### Step 4.5: Compile Constraint Templates
#### Step EX.5: Compile Constraint Templates
For each extracted learning scoring >= 4/5 on actionability AND tagged "constraint" or "anti-pattern", run `bash hooks/constraint-compiler.sh <learning-path>` to generate a constraint template.
@@ -594,7 +594,7 @@ source: "[[.agents/plans/YYYY-MM-DD-<plan-slug>]]"
**Epic:** <epic-id or "recent">
**Duration:** <elapsed time from PM_START to now>
**Cycle-Time Trend:** <compare against prior post-mortems — is this faster or slower? Check .agents/retros/ for prior Duration values>
**Cycle-Time Trend:** <compare against prior post-mortems — is this faster or slower? Check .agents/council/ for prior post-mortem Duration values>
## Council Verdict: PASS / WARN / FAIL
@@ -46,7 +46,7 @@ python3 ~/.claude/scripts/analyze-sessions.py --session=$SESSION_ID --limit=50
| Conversation Data | Retro Output |
|-------------------|--------------|
| `DecisionExtraction` | `.agents/retros/` - Decisions section |
| `DecisionExtraction` | `.agents/council/` - Decisions section |
| `QualityResult.issues` | Friction detection |
| `DocExtraction(type="warning")` | `.agents/learnings/` |
| `DecisionExtraction(type="pattern")` | `.agents/patterns/` |
@@ -230,7 +230,7 @@ mcp__ai-platform__memory_store(
## Retro Summary Template
Write to: `.agents/retros/YYYY-MM-DD-{epic}.md`
Write to: `.agents/council/YYYY-MM-DD-{epic}.md`
```markdown
# Retro: [Epic Title]
@@ -19,7 +19,7 @@ See `.claude/includes/tag-vocabulary.md` for the complete tag vocabulary.
## Retro Summary Template
Write to `.agents/retros/YYYY-MM-DD-{topic}.md`:
Write to `.agents/council/YYYY-MM-DD-{topic}.md`:
```markdown
---
@@ -329,7 +329,7 @@ Cross-references added (not superseded):
-> Added to "Related" section
No action needed:
- .agents/retros/2025-10-01-unrelated.md
- .agents/council/2025-10-01-unrelated.md
-> Different topic, no relationship
Ready for Phase 5: User Review
@@ -340,7 +340,7 @@ Ready for Phase 5: User Review
```
Retro complete:
- Summary: .agents/retros/YYYY-MM-DD-topic.md
- Summary: .agents/council/YYYY-MM-DD-topic.md
- Learnings: .agents/learnings/YYYY-MM-DD-topic.md (if applicable)
- Patterns: [updated/created files] (if applicable)
+1 -1
View File
@@ -402,7 +402,7 @@ Orchestrators, interactive skills, and single-task executors stay in the main se
| implement | Single-task | Single issue, medium duration |
| bug-hunt | Investigator | Hypothesis loop, need to see reasoning |
| vibe | Orchestrator | Orchestrates council, reports verdict |
| post-mortem | Orchestrator | Orchestrates council + retro |
| post-mortem | Orchestrator | Orchestrates council + knowledge lifecycle |
| pre-mortem | Orchestrator | Default inline, orchestrates if --deep |
### Tier 2: FORK (subagent/worktree via `context: fork`)
@@ -12,7 +12,7 @@ Defines the maturity pipeline for knowledge artifacts.
## Tier 1: Learnings (`.agents/learnings/`)
- **Source:** `/retro`, `/learn`, `/extract`, promoted from forge
- **Source:** `/retro`, `/post-mortem`, `/extract`, promoted from forge
- **Confidence:** 0.3-1.0
- **Citations:** 1+
- **Promotion criteria:** Promote to Tier 2 when confidence >= 0.8 AND cited >= 3 times AND age > 30 days
@@ -28,7 +28,7 @@ Defines the maturity pipeline for knowledge artifacts.
## Cross-Repo Promotion
- Any tier can be promoted to `~/.claude/patterns/` via `/learn --global`
- Any tier can be promoted to `~/.claude/patterns/` via `/retro --global`
- Global patterns are user-level, shared across all repositories
- Promotion is a manual decision (human judgment on cross-repo applicability)
- Global patterns are found by `/research`, `/knowledge`, and `/inject` via grep
+8 -8
View File
@@ -274,7 +274,7 @@ Enables adversarial two-round review for post-implementation validation. Use for
Inline extraction of learnings from the completed work (formerly delegated to the retro skill).
#### Step 4.1: Gather Context
#### Step EX.1: Gather Context
```bash
# Recent commits
@@ -291,7 +291,7 @@ Read relevant artifacts: research documents, plan documents, commit messages, co
**If retrospecting an epic:** Run the closure integrity quick-check from `references/context-gathering.md` (Phantom Bead Detection + Multi-Wave Regression Scan). Include any warnings in findings.
#### Step 4.2: Classify Learnings
#### Step EX.2: Classify Learnings
Ask these questions:
@@ -318,7 +318,7 @@ For each learning, capture:
- **Why it matters**: Impact on future work
- **Confidence**: high, medium, low
#### Step 4.3: Write Learnings
#### Step EX.3: Write Learnings
**Write to:** `.agents/learnings/YYYY-MM-DD-<topic>.md`
@@ -353,13 +353,13 @@ confidence: <high|medium|low>
...
```
#### Step 4.4: Classify Learning Scope
#### Step EX.4: Classify Learning Scope
For each learning extracted in Step 4.3, classify:
For each learning extracted in Step EX.3, classify:
**Question:** "Does this learning reference specific files, packages, or architecture in THIS repo? Or is it a transferable pattern that helps any project?"
- **Repo-specific** -> Write to `.agents/learnings/` (existing behavior from Step 4.3). Use `git rev-parse --show-toplevel` to resolve repo root — never write relative to cwd.
- **Repo-specific** -> Write to `.agents/learnings/` (existing behavior from Step EX.3). Use `git rev-parse --show-toplevel` to resolve repo root — never write relative to cwd.
- **Cross-cutting/transferable** -> Rewrite to remove repo-specific context (file paths, function names, package names), then:
1. Write abstracted version to `~/.agents/learnings/YYYY-MM-DD-<slug>.md` (NOT local — one copy only)
2. Run abstraction lint check:
@@ -377,7 +377,7 @@ For each learning extracted in Step 4.3, classify:
- Local: "Athena's validate package needs O_CREATE|O_EXCL for atomic claims because Zeus spawns concurrent workers"
- Global: "Use O_CREATE|O_EXCL for atomic file creation when multiple processes may race on the same path"
#### Step 4.5: Compile Constraint Templates
#### Step EX.5: Compile Constraint Templates
For each extracted learning scoring >= 4/5 on actionability AND tagged "constraint" or "anti-pattern", run `bash hooks/constraint-compiler.sh <learning-path>` to generate a constraint template.
@@ -594,7 +594,7 @@ source: "[[.agents/plans/YYYY-MM-DD-<plan-slug>]]"
**Epic:** <epic-id or "recent">
**Duration:** <elapsed time from PM_START to now>
**Cycle-Time Trend:** <compare against prior post-mortems — is this faster or slower? Check .agents/retros/ for prior Duration values>
**Cycle-Time Trend:** <compare against prior post-mortems — is this faster or slower? Check .agents/council/ for prior post-mortem Duration values>
## Council Verdict: PASS / WARN / FAIL
@@ -46,7 +46,7 @@ python3 ~/.claude/scripts/analyze-sessions.py --session=$SESSION_ID --limit=50
| Conversation Data | Retro Output |
|-------------------|--------------|
| `DecisionExtraction` | `.agents/retros/` - Decisions section |
| `DecisionExtraction` | `.agents/council/` - Decisions section |
| `QualityResult.issues` | Friction detection |
| `DocExtraction(type="warning")` | `.agents/learnings/` |
| `DecisionExtraction(type="pattern")` | `.agents/patterns/` |
@@ -230,7 +230,7 @@ mcp__ai-platform__memory_store(
## Retro Summary Template
Write to: `.agents/retros/YYYY-MM-DD-{epic}.md`
Write to: `.agents/council/YYYY-MM-DD-{epic}.md`
```markdown
# Retro: [Epic Title]
@@ -19,7 +19,7 @@ See `.claude/includes/tag-vocabulary.md` for the complete tag vocabulary.
## Retro Summary Template
Write to `.agents/retros/YYYY-MM-DD-{topic}.md`:
Write to `.agents/council/YYYY-MM-DD-{topic}.md`:
```markdown
---
@@ -329,7 +329,7 @@ Cross-references added (not superseded):
-> Added to "Related" section
No action needed:
- .agents/retros/2025-10-01-unrelated.md
- .agents/council/2025-10-01-unrelated.md
-> Different topic, no relationship
Ready for Phase 5: User Review
@@ -340,7 +340,7 @@ Ready for Phase 5: User Review
```
Retro complete:
- Summary: .agents/retros/YYYY-MM-DD-topic.md
- Summary: .agents/council/YYYY-MM-DD-topic.md
- Learnings: .agents/learnings/YYYY-MM-DD-topic.md (if applicable)
- Patterns: [updated/created files] (if applicable)