mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
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:
@@ -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 |
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]) + "..."
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user