Implement all 5 tasks: additive extension model, framework self-enforcement, changelog, project migration, dashboard review

This commit is contained in:
2026-06-13 12:26:27 -04:00
parent 46ceed0122
commit 52dcf8e309
15 changed files with 380 additions and 115 deletions
+4 -3
View File
@@ -9,9 +9,10 @@ The agent's first task in any project is to perform an "Initial Exploration" to
### Step 1: Discovery
The agent must:
1. Explore the project root using `ls` and `find`.
2. Read `.automaton/.agent.md`
3. Read `.automaton/.rules.md`
4. Read the global `~/.automaton/.agent.md`
2. Read ~/.automaton/.agent.md (global router)
3. Read ~/.automaton/.rules.md (global framework rules)
4. Read the project's `.automaton/.agent.md`
5. Read the project's `.automaton/.rules.md`
### Step 2: Reporting
The agent must report back with:
+17 -2
View File
@@ -1,5 +1,20 @@
# .rules.md
- Add one rule per observed failure mode with a concrete example.
## Task-Driven Development
- All changes must go through a task in tasks/{name}/ with SPEC.md → phases → VERDICT.md
- Never edit files directly without a corresponding task
- Never create task directories manually (mkdir tasks/) — use the Orchestrator instead
## VRAM-Aware Task Sizing
- Before creating or scoping a task, check ~/.automaton/config.md for VRAM limits
- Verify the task fits within max peak context (default: 12k tokens)
- If no config exists, default to 8k with 25% headroom
## Changelog
- When a task reaches Resolution (VERDICT.md written), append an entry to CHANGELOG.md
- Use the format: `- description (#task-name)` under the appropriate [unreleased] section
## Self-Improvement
- Add one rule per observed failure mode with a concrete example
- Consolidate contradictions monthly. Remove stale rules.
- No rule without a real example of the problem it prevents.
- No rule without a real example of the problem it prevents
+18
View File
@@ -0,0 +1,18 @@
# Changelog
## [unreleased]
### Added
- Blocked phase column between Verification and Resolution on dashboard (#additive-extension-model)
- Framework self-enforcement rules in .rules.md and system-prompt.md (#framework-self-enforcement)
- Additive extension model: projects extend via extensions/ dir, never copy framework files (#additive-extension-model)
- CHANGELOG.md for release notes tracking (#changelog)
- Framework audit: comprehensive self-consistency check with RESEARCH.md (#framework-audit)
### Changed
- prompts/orchestrate.md: always reads prompts/contracts/scripts from global, project extensions are additive (#additive-extension-model)
- prompts/onboarding.md: removed diff/merge upgrade, replaced with migration check (#additive-extension-model)
- README.md: updated upgrade docs for new additive model (#additive-extension-model)
- scripts/update.sh: simplified to plain git pull (#additive-extension-model)
- .rules.md: converted from template to concrete rules with Task-Driven Development, VRAM-aware sizing, Changelog, and Self-Improvement sections (#framework-self-enforcement)
- system-prompt.md: added instruction to read global .rules.md (#framework-self-enforcement)
+13 -9
View File
@@ -35,7 +35,9 @@ This will:
- Check for uncommitted changes and warn you
- Pull the latest updates
**Upgrading existing projects:** When the framework is updated, existing projects may need their framework files upgraded (new phases added, new prompts, etc.). To upgrade an existing project, tell the agent: "Upgrade automaton for this project." The agent will check for missing files and update them.
**Upgrading existing projects:** The framework reads prompts, contracts, and scripts from `~/.automaton/` at runtime. Projects only override `.agent.md` and `.rules.md`. This means updating the global framework (`git pull`) automatically applies to all projects. No per-project upgrade is needed.
If a project was set up under the old model (with copies of framework files), it needs migration first. Tell the agent: "Upgrade automaton for this project" to run the migration.
---
@@ -185,21 +187,23 @@ The framework uses a **layered approach** to file management, with a clear prece
### What files belong in each layer?
- **Project's `.automaton/`**: .agent.md (project-specific settings like Autopilot mode, rules override), .rules.md (project-specific constraints)
- **Project's `.automaton/`**: .agent.md (project-specific settings like Autopilot mode, rules override), .rules.md (project-specific constraints), extensions/ (optional additive overrides)
- **Global `~/.automaton/`**: All prompt files, contracts, scripts, config.md, workflow.md
### Upgrading
When you upgrade the global framework (e.g., after pushing bug fixes), existing projects may need their framework files upgraded. Tell the agent:
The framework reads all base files from `~/.automaton/` at runtime. To update the framework:
```bash
cd ~/.automaton && git pull
```
This automatically applies changes to all projects — no per-project upgrade needed.
If a project has stale framework file copies (from the old model), tell the agent:
> "Upgrade automaton for this project."
The agent will:
1. Compare the project's `.automaton/` files with the global `~/.automaton/` files
2. **Customized files** — If the project has customized a file (differs from global), **keep the project's version**
3. **Outdated files** — If the project's file is identical to the old global version, **update from global**
4. **New files** — If the global framework has new files, **add them to the project**
5. Report what was upgraded, added, and skipped
The agent will run `migrate-project.sh` to clean up stale files and move customizations to `extensions/`.
## Contact & Support
[Insert Contact Info]
+45 -2
View File
@@ -1,7 +1,7 @@
const state = {
scope: 'none', currentView: 'board', theme: 'default', selectedTask: null,
tasks: [], refreshCount: 0, autoRefresh: true, showWaves: true,
filterPhase: 'all', filterWave: 'all', searchQuery: '', filterVisible: false,
filterPhase: 'all', filterReview: 'all', filterWave: 'all', searchQuery: '', filterVisible: false,
refreshInterval: null, projectName: null,
};
@@ -77,6 +77,8 @@ function renderHeader() {
wipTasks.textContent = filtered.filter(t => wipStates.includes(t.state)).length;
doneTasks.textContent = filtered.filter(t => t.state === 'done').length;
blockedTasks.textContent = filtered.filter(t => t.state === 'blocked').length;
const pendingReview = document.getElementById('pending-review');
if (pendingReview) pendingReview.textContent = filtered.filter(t => !t.review || t.review.status === 'pending').length;
// Project name display
const projectName = state.projectName;
if (projectName) {
@@ -140,6 +142,10 @@ function renderTaskCard(task) {
const statusClass = task.state === 'done' ? 'done' : task.state === 'blocked' ? 'blocked' : 'in_progress';
const statusIcon = task.state === 'done' ? '✅' : task.state === 'blocked' ? '❌' : '🔄';
const subLabel = getSubLabel(task.state);
const reviewStatus = task.review ? task.review.status : 'pending';
const reviewBadge = reviewStatus === 'approved' ? '<span class="review-badge approved" title="Approved">✅</span>'
: reviewStatus === 'changes_requested' ? '<span class="review-badge changes" title="Changes requested">❌</span>'
: '<span class="review-badge pending" title="Pending review">🟡</span>';
const progressHtml = task.sub_tasks.length > 0
? `<span class="subtask-progress">${task.sub_tasks.filter(st => st.has_verdict && st.verdict_status === 'PASS').length}/${task.sub_tasks.length}</span>`
: '';
@@ -151,7 +157,7 @@ function renderTaskCard(task) {
}).join('')}</div>`
: '';
return `<div class="task-card" data-task="${task.name}" data-status="${statusClass}">
<div class="task-card-header"><span class="task-card-name">${task.display_name}</span><span class="task-card-status ${statusClass}">${statusIcon}</span></div>
<div class="task-card-header"><span class="task-card-name">${task.display_name}</span><span style="display:flex;align-items:center;gap:4px">${reviewBadge}<span class="task-card-status ${statusClass}">${statusIcon}</span></span></div>
<div class="task-card-sublabel">${subLabel}</div>
${progressHtml ? `<div class="task-card-footer"><span class="subtask-progress">${progressHtml}</span></div>` : ''}
${subtasksHtml}
@@ -175,9 +181,20 @@ function renderDetail(task) {
return `<span class="detail-artifact"><span class="${cls}">${icon}</span>${col.label}</span>`;
}).join('');
const phaseGroupHtml = phaseGroup ? `<span class="detail-phase-badge" style="background: ${phaseGroupColor}20; color: ${phaseGroupColor}">${PHASE_GROUPS.find(g => g.id === phaseGroup).label}</span>` : '';
const reviewStatus = task.review ? task.review.status : 'pending';
const reviewStatusText = reviewStatus === 'approved' ? '✅ Approved' : reviewStatus === 'changes_requested' ? '❌ Changes Requested' : '🟡 Pending Review';
const reviewComment = task.review && task.review.comment ? `<p class="review-comment">${escapeHtml(task.review.comment)}</p>` : '';
content.innerHTML = `
<div class="detail-section"><h4>Status</h4><span class="detail-status-badge ${statusClass}">${statusText}</span>${phaseGroupHtml}</div>
<div class="detail-section"><h4>Artifacts</h4><div class="detail-artifacts">${artifactsHtml}</div></div>
<div class="detail-section"><h4>Review</h4>
<span class="review-badge ${reviewStatus}">${reviewStatusText}</span>
${reviewComment}
<div class="review-actions">
<button class="review-btn approve" onclick="submitReview('${task.name}', 'approved')">✅ Approve</button>
<button class="review-btn changes" onclick="submitReview('${task.name}', 'changes_requested')">❌ Request Changes</button>
</div>
</div>
${task.sub_tasks.length > 0 ? `<div class="detail-section"><h4>Sub-tasks (${task.sub_tasks.filter(st => st.has_verdict).length}/${task.sub_tasks.length})</h4>
<ul class="detail-subtask-list">${task.sub_tasks.map(st => {
const stStatus = st.has_verdict && st.verdict_status === 'PASS' ? 'pass' : st.has_verdict && st.verdict_status === 'FAIL' ? 'fail' : 'incomplete';
@@ -299,9 +316,34 @@ function renderTimeline() {
panel.innerHTML = `<div class="timeline-header"><h4>Phase Legend:</h4><div class="phase-legend">${legend}</div></div>${itemsHtml}`;
}
async function submitReview(taskName, status) {
const comment = prompt(status === 'changes_requested' ? 'Describe what changes are needed:' : 'Optional approval comment:');
if (comment === null) return;
try {
const res = await fetch(`/api/task/${taskName}/review`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status, comment }),
});
const data = await res.json();
if (data.success) {
await refreshData();
const task = state.tasks.find(t => t.name === taskName);
if (task && task.name === (state.selectedTask && state.selectedTask.name)) {
renderDetail(task);
}
}
} catch (err) {
console.error('Review submission failed:', err);
}
}
function getFilteredTasks() {
let filtered = [...state.tasks];
if (state.filterPhase !== 'all') filtered = filtered.filter(t => t.state === state.filterPhase);
if (state.filterReview === 'pending') filtered = filtered.filter(t => !t.review || t.review.status === 'pending');
else if (state.filterReview === 'approved') filtered = filtered.filter(t => t.review && t.review.status === 'approved');
else if (state.filterReview === 'changes_requested') filtered = filtered.filter(t => t.review && t.review.status === 'changes_requested');
if (state.filterWave === 'has-waves') filtered = filtered.filter(t => t.sub_tasks.length > 0);
else if (state.filterWave === 'no-waves') filtered = filtered.filter(t => t.sub_tasks.length === 0);
if (state.searchQuery) {
@@ -374,6 +416,7 @@ function setupUI() {
document.getElementById('btn-close-help').addEventListener('click', () => { document.getElementById('help-modal').classList.remove('open'); });
document.getElementById('btn-close-detail').addEventListener('click', closeDetail);
document.getElementById('filter-phase').addEventListener('change', (e) => { state.filterPhase = e.target.value; renderCurrentView(); });
document.getElementById('filter-review').addEventListener('change', (e) => { state.filterReview = e.target.value; renderCurrentView(); });
document.getElementById('filter-wave').addEventListener('change', (e) => { state.filterWave = e.target.value; renderCurrentView(); });
document.getElementById('search-input').addEventListener('input', (e) => { state.searchQuery = e.target.value; renderCurrentView(); });
}
+10
View File
@@ -26,6 +26,7 @@
<span class="stat-wip">WIP: <strong id="wip-tasks">0</strong></span>
<span class="stat-done">Done: <strong id="done-tasks">0</strong></span>
<span class="stat-blocked">Blocked: <strong id="blocked-tasks">0</strong></span>
<span class="stat-pending">Pending: <strong id="pending-review">0</strong></span>
</div>
<div class="header-controls">
<button class="btn btn-icon" id="btn-refresh" title="Refresh">↻</button>
@@ -48,6 +49,15 @@
<option value="blocked">Blocked</option>
</select>
</div>
<div class="filter-group">
<label>Review:</label>
<select id="filter-review">
<option value="all">All</option>
<option value="pending">Pending</option>
<option value="approved">Approved</option>
<option value="changes_requested">Changes Requested</option>
</select>
</div>
<div class="filter-group">
<label>Waves:</label>
<select id="filter-wave">
+13
View File
@@ -52,6 +52,7 @@ body {
.stats-mini { display: flex; gap: 12px; font-size: 12px; color: var(--text-secondary); }
.stat-total strong, .stat-wip strong, .stat-done strong, .stat-blocked strong { color: var(--text-primary); }
.stat-blocked strong { color: var(--error); }
.stat-pending strong { color: var(--warning); }
.header-controls { display: flex; gap: 4px; }
.btn {
padding: 6px 12px; border: 1px solid var(--border-color);
@@ -234,6 +235,18 @@ kbd {
::-webkit-scrollbar-track { background: var(--bg-primary); }
::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--border-active); }
.review-badge { font-size: 11px; margin-left: 4px; }
.review-badge.approved { color: var(--success); }
.review-badge.changes_requested { color: var(--error); }
.review-badge.pending { color: var(--warning); }
.review-actions { display: flex; gap: 8px; margin-top: 8px; }
.review-btn { padding: 6px 12px; border: 1px solid var(--border-color); border-radius: 6px; cursor: pointer; font-size: 12px; transition: all 0.15s; background: var(--bg-card); color: var(--text-primary); }
.review-btn.approve { border-color: var(--success); color: var(--success); }
.review-btn.approve:hover { background: var(--success-bg); }
.review-btn.changes { border-color: var(--error); color: var(--error); }
.review-btn.changes:hover { background: var(--error-bg); }
.review-comment { font-size: 12px; color: var(--text-secondary); padding: 8px; background: var(--bg-primary); border-radius: 4px; margin-top: 4px; }
@media (max-width: 768px) {
.header { flex-wrap: wrap; gap: 8px; }
.header-center { order: 3; width: 100%; }
+92 -2
View File
@@ -44,12 +44,23 @@ class DashboardHandler(SimpleHTTPRequestHandler):
self._serve_project_name()
elif self.path == "/api/task/" or self.path.startswith("/api/task/"):
task_name = self.path.split("/api/task/")[1]
if task_name.endswith("/review"):
task_name = task_name[:-7]
self._serve_task_review(task_name)
else:
self._serve_task(task_name)
elif self.path == "/api/review-summary":
self._serve_review_summary()
else:
# Serve static files from dashboard HTML directory manually
# This avoids redirect loops with the root path
self._serve_static()
def do_POST(self):
if self.path.startswith("/api/task/") and self.path.endswith("/review"):
task_name = self.path.split("/api/task/")[1][:-7]
self._handle_review(task_name)
else:
self._send_error(404, "Not found")
def _serve_static(self):
"""Serve static files from the dashboard HTML directory."""
# Strip query string and fragment
@@ -113,6 +124,7 @@ class DashboardHandler(SimpleHTTPRequestHandler):
],
"verdict_content": t.verdict_content,
"bug_report_content": t.bug_report_content,
"review": self._get_review_status(t.name),
}
for t in tasks
]
@@ -171,9 +183,87 @@ class DashboardHandler(SimpleHTTPRequestHandler):
],
"verdict_content": task.verdict_content,
"bug_report_content": task.bug_report_content,
"review": self._get_review_status(task.name),
}
self._send_json(task_data)
REVIEW_FILE = "REVIEW.md"
def _get_review_status(self, task_name: str) -> dict:
project_root = find_automaton_root()
if not project_root:
return {"status": "unknown"}
review_path = project_root / ".automaton" / "tasks" / task_name / self.REVIEW_FILE
if not review_path.exists():
return {"status": "pending"}
try:
content = review_path.read_text().strip()
status = "pending"
timestamp = ""
comment = ""
for line in content.split('\n'):
line = line.strip()
if line.startswith("- **Status**"):
status = line.split("**:")[1].strip().rstrip()
elif line.startswith("- **Timestamp**"):
timestamp = line.split("**:")[1].strip().rstrip()
elif line.startswith("- **Comment**"):
comment = line.split("**:", 1)[1].strip().rstrip() if "**: " in line else ""
return {"status": status, "timestamp": timestamp, "comment": comment}
except Exception:
return {"status": "pending"}
def _write_review(self, task_name: str, status: str, comment: str = ""):
project_root = find_automaton_root()
if not project_root:
return
from datetime import datetime
review_path = project_root / ".automaton" / "tasks" / task_name / self.REVIEW_FILE
content = f"# Review\n- **Status**: {status}\n- **Timestamp**: {datetime.now().isoformat()}\n"
if comment:
content += f"- **Comment**: {comment}\n"
review_path.parent.mkdir(parents=True, exist_ok=True)
review_path.write_text(content)
def _serve_task_review(self, task_name: str):
review = self._get_review_status(task_name)
self._send_json(review)
def _handle_review(self, task_name: str):
try:
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length).decode() if content_length else "{}"
data = json.loads(body)
status = data.get("status", "pending")
comment = data.get("comment", "")
if status not in ("approved", "changes_requested"):
self._send_error(400, "Invalid status. Use 'approved' or 'changes_requested'.")
return
self._write_review(task_name, status, comment)
self._send_json({"success": True, "status": status})
except json.JSONDecodeError:
self._send_error(400, "Invalid JSON")
def _serve_review_summary(self):
project_root = find_automaton_root()
if not project_root:
self._send_json({"pending": 0, "approved": 0, "changes_requested": 0})
return
tasks_dir = project_root / ".automaton" / "tasks"
if not tasks_dir.exists():
self._send_json({"pending": 0, "approved": 0, "changes_requested": 0})
return
counts = {"pending": 0, "approved": 0, "changes_requested": 0}
for task_dir in tasks_dir.iterdir():
if task_dir.is_dir():
review = self._get_review_status(task_dir.name)
status = review.get("status", "pending")
if status in counts:
counts[status] += 1
else:
counts["pending"] += 1
self._send_json(counts)
def _send_json(self, data):
self.send_response(200)
self.send_header("Content-Type", "application/json")
+22 -67
View File
@@ -5,10 +5,11 @@ Your only job is to set up the minimal agent framework structure in the target p
## Read These Files
1. ~/.automaton/.agent.md — global framework router
2. ~/.automaton/.onboarding.md — human reference for drop-in vs from-scratch scenarios
3. {project}/.automaton/.agent.md (if it exists — project override)
4. {project}/.automaton/.rules.md (if it exists — project override)
5. ~/.automaton/scripts/vram_detect.sh (if exists — for VRAM detection)
2. ~/.automaton/.rules.md — global framework rules
3. ~/.automaton/.onboarding.md — human reference for drop-in vs from-scratch scenarios
4. {project}/.automaton/.agent.md (if it exists — project override)
5. {project}/.automaton/.rules.md (if it exists — project override)
6. ~/.automaton/scripts/vram_detect.sh (if exists — for VRAM detection)
## Task
@@ -16,16 +17,6 @@ Your only job is to set up the minimal agent framework structure in the target p
## Onboarding Ritual (Strict Sequence)
### Step 0: Check if Framework Needs Upgrade
Before proceeding, check if the project is running an older version of the framework:
1. Compare the project's `.automaton/` files with the global `~/.automaton/` files.
2. If the project's `.automaton/` has a file that differs from the current global version, the project needs an upgrade.
3. If the project's `.automaton/` is missing files that exist in the global framework (e.g., new prompt files like `doc_review.md`, `test_design.md`), the project needs an upgrade.
4. If an upgrade is needed, report it to the user and offer to upgrade the project's framework files.
**Precedence**: The project's `.automaton/` files override the global `~/.automaton/` files. The Orchestrator reads from the project's directory first, then falls back to the global directory.
### Step 1: Discovery
1. Check if {project}/.automaton/ exists. If not, create it.
@@ -39,7 +30,7 @@ Before proceeding, check if the project is running an older version of the frame
5. Explore the project root at a high level (ls, key directories, README if present).
6. Produce a short onboarding report.
**Important**: The project's `.automaton/` directory should only contain .agent.md and .rules.md. All other framework files (prompts, contracts, scripts) are read from the global `~/.automaton/` directory. The project's directory is the override layer — if a file exists in both, the project's version takes precedence.
**Important**: The project's `.automaton/` directory should only contain .agent.md and .rules.md. All other framework files (prompts, contracts, scripts) are read from the global `~/.automaton/` directory. Projects can provide additive extensions under `.automaton/extensions/` — these extend, never replace, the framework files.
### Step 2: VRAM Configuration
@@ -80,7 +71,6 @@ Then produce a file called ONBOARDING_REPORT.md at {project}/tasks/onboarding/ON
- What process this project expects
- Key observations from the project structure
- Any missing pieces the human should provide next
- **Upgrade status**: Whether the project's framework files are up to date with the global framework
- **VRAM Configuration**: Whether VRAM config was auto-detected and applied (if available), or needs manual setup
When the ritual is complete, output "CONTRACT_MET" and stop.
@@ -96,60 +86,25 @@ Do not begin any research, implementation, or bug-finding tasks.
---
## Project Upgrade
## Migration Check
When a user asks to "upgrade automaton for this project," the agent should:
When a user asks to "upgrade automaton for this project" or during onboarding, the agent should first check if the project has stale copies of framework files (from the old model where files were copied into the project):
### Upgrade Process
### Migration Detection
1. **Compare the project's `~/.automaton/` files with the global `~/.automaton/` files.**
- For each file in the global framework, check if it exists in the project's framework.
- If it exists in both, compare their content.
1. Check if `{project}/.automaton/` contains any files beyond `.agent.md` and `.rules.md`:
- `prompts/` directory with files matching global prompts
- `contracts/` directory with files matching global contracts
- `scripts/` directory with files matching global scripts
- Any other files that exist in `~/.automaton/`
2. **Identify three categories of files:**
- **Customized** — The file exists in both, but they differ. The project has customized it. **Keep the project's version.**
- **Outdated** — The file exists in both, but they are identical. The project hasn't customized it, but the global version has changed. **Update from global.**
- **New** — The file exists in the global framework but not in the project. **Add from global.**
2. If stale files are found, offer to run migration:
- Run `~/.automaton/scripts/migrate-project.sh {project}` if available
- Or manually:
- Files identical to global → delete (framework provides them)
- Files different from global → move to `.automaton/extensions/`
- `.agent.md` and `.rules.md` → keep as-is
3. **Apply upgrades:**
- For **Outdated** files: Copy from the global framework to the project's framework (update the project's version).
- For **New** files: Copy from the global framework to the project's framework (add the file).
- For **Customized** files: **Do NOT overwrite** — keep the project's version and report it as "skipped (customized)."
### After Migration
4. **Report what was upgraded and what was already up to date.**
### Upgrade Report Format
The agent should report:
- **Upgraded**: Files that were updated from the global framework (Outdated → Upgraded)
- **Added**: New files added from the global framework (New → Added)
- **Skipped**: Files that were customized in the project and not overwritten (Customized → Skipped)
- **Already up to date**: Files that were already identical (shouldn't happen, but report for completeness)
### Example Upgrade Scenarios
**Scenario 1: New file added globally**
- User says: "Upgrade automaton for this project"
- Agent detects that `prompts/test_design.md` is missing from the project's framework
- Agent copies `prompts/test_design.md` from the global framework into the project's framework
- Agent reports: "Upgraded: Added prompts/test_design.md. Your framework is now up to date."
**Scenario 2: Global file changed, project hasn't customized it**
- User says: "Upgrade automaton for this project"
- Agent detects that `prompts/orchestrate.md` has changed in the global framework, and the project's version is identical to the old global version
- Agent copies `prompts/orchestrate.md` from the global framework into the project's framework
- Agent reports: "Upgraded: Updated prompts/orchestrate.md. Your framework is now up to date."
**Scenario 3: Global file changed, project has customized it**
- User says: "Upgrade automaton for this project"
- Agent detects that `prompts/orchestrate.md` has changed in the global framework, and the project's version differs from the global version
- Agent keeps the project's version of `prompts/orchestrate.md`
- Agent reports: "Skipped: prompts/orchestrate.md (customized in your project). Your framework is now up to date."
**Scenario 4: Multiple changes**
- User says: "Upgrade automaton for this project"
- Agent detects:
- `prompts/test_design.md` is new → **Added**
- `prompts/workflow.md` has changed, project hasn't customized → **Upgraded**
- `.agent.md` has changed, project has customized → **Skipped (customized)**
- Agent reports: "Upgraded: Updated prompts/workflow.md. Added: Added prompts/test_design.md. Skipped: .agent.md (customized in your project). Your framework is now up to date."
No further upgrade steps are needed. The framework is always read from `~/.automaton/` at runtime. Simply updating the global framework (`cd ~/.automaton && ./update.sh`) automatically applies all changes to every project.
+8 -5
View File
@@ -7,16 +7,19 @@ The Orchestrator reads files using a **layered approach** with a clear precedenc
1. **Project overrides** (highest precedence): `{project}/.automaton/` — contains project-specific customizations
2. **Global framework** (default): `~/.automaton/` — contains the base framework files
**Precedence rule**: If a file exists in the project's `.automaton/` directory, the Orchestrator reads it from there. If it doesn't exist, the Orchestrator reads it from the global `~/.automaton/` directory.
**Precedence rule**: Base framework files (prompts, contracts, scripts) are always read from `~/.automaton/`. Projects provide additive extensions under `{project}/.automaton/extensions/` — never copies of framework files. Only `.agent.md` and `.rules.md` can be overridden directly in the project root.
Specifically:
1. {project}/.automaton/.agent.md (if exists — project override) OR ~/.automaton/.agent.md (global default)
2. {project}/.automaton/.rules.md (if exists — project override) OR ~/.automaton/.rules.md (global default)
3. ~/.automaton/config.md — Global framework configuration (VRAM, model settings)
4. {project}/.automaton/prompts/*.md (if exists — project overrides) OR ~/.automaton/prompts/*.md (global default)
5. {project}/.automaton/contracts/*.md (if exists — project overrides) OR ~/.automaton/contracts/*.md (global default)
6. {project}/.automaton/scripts/*.sh (if exists — project overrides) OR ~/.automaton/scripts/*.sh (global default)
7. Any existing files under {project}/tasks/
4. ~/.automaton/prompts/*.md — Always from global framework
5. {project}/.automaton/extensions/prompts/*.md — Additive extensions loaded after the corresponding global prompt
6. ~/.automaton/contracts/*.md — Always from global framework
7. {project}/.automaton/extensions/contracts/*.md — Additive extensions loaded after global contracts
8. ~/.automaton/scripts/*.sh — Always from global framework
9. {project}/.automaton/extensions/scripts/*.sh — Additive extensions loaded before global scripts (pre-processing)
10. Any existing files under {project}/tasks/
## VRAM Detection
+3 -2
View File
@@ -7,8 +7,9 @@ Copy and paste this at the very beginning of every new agent session (before giv
Read the following files in order, then wait for my instructions:
1. ~/.automaton/.agent.md (global framework router)
2. {project}/.automaton/.agent.md (project-level router, if it exists)
3. {project}/.automaton/.rules.md (if it exists)
2. ~/.automaton/.rules.md (global framework rules)
3. {project}/.automaton/.agent.md (project-level router, if it exists)
4. {project}/.automaton/.rules.md (if it exists)
After reading these files, acknowledge with: ".agent.md and .rules.md loaded. Ready."
+125
View File
@@ -0,0 +1,125 @@
#!/bin/bash
# Migrate a project from the old copy-based model to the additive extension model.
# Usage: ./migrate-project.sh /path/to/project
set -e
FRAMEWORK_DIR="$HOME/.automaton"
PROJECT_DIR="${1}"
if [ -z "$PROJECT_DIR" ]; then
echo "Usage: $0 /path/to/project"
exit 1
fi
PROJECT_AUTOMATON="$PROJECT_DIR/.automaton"
if [ ! -d "$PROJECT_AUTOMATON" ]; then
echo "No .automaton/ directory found at $PROJECT_AUTOMATON"
echo "Nothing to migrate."
exit 0
fi
EXTENSIONS_DIR="$PROJECT_AUTOMATON/extensions"
DELETED=()
MOVED=()
KEPT=()
ERRORS=()
# Directories in the framework that projects may have copied
SCAN_DIRS=("prompts" "contracts" "scripts")
# Function to diff a file with its global counterpart
diff_file() {
local project_file="$1"
local global_file="$2"
if [ ! -f "$global_file" ]; then
return 2 # file doesn't exist in global
fi
if diff -q "$project_file" "$global_file" >/dev/null 2>&1; then
return 0 # identical
else
return 1 # different
fi
}
# Check root-level framework files
echo "Scanning $PROJECT_AUTOMATON for stale framework files..."
echo ""
for dir in "${SCAN_DIRS[@]}"; do
project_dir="$PROJECT_AUTOMATON/$dir"
global_dir="$FRAMEWORK_DIR/$dir"
if [ ! -d "$project_dir" ]; then
continue
fi
# Find all files in the project's copy of this directory
while IFS= read -r -d '' project_file; do
rel_path="${project_file#$PROJECT_AUTOMATON/}"
global_file="$FRAMEWORK_DIR/$rel_path"
if diff_file "$project_file" "$global_file"; then
# Identical to global — safe to delete
rm "$project_file"
DELETED+=("$rel_path")
echo " DELETED $rel_path (identical to global)"
elif [ $? -eq 1 ]; then
# Different from global — move to extensions/
ext_path="$EXTENSIONS_DIR/$dir/$(basename "$project_file")"
mkdir -p "$(dirname "$ext_path")"
mv "$project_file" "$ext_path"
MOVED+=("$rel_path -> extensions/$dir/$(basename "$project_file")")
echo " MOVED $rel_path → extensions/$dir/$(basename "$project_file") (customized)"
fi
done < <(find "$project_dir" -type f -print0 2>/dev/null || true)
# Remove empty directories after moving/deleting files
rmdir "$project_dir" 2>/dev/null || true
done
# Check for other stale root-level files (templates/, references/, etc.)
while IFS= read -r -d '' project_file; do
rel_path="${project_file#$PROJECT_AUTOMATON/}"
global_file="$FRAMEWORK_DIR/$rel_path"
# Skip protected files
basename_file=$(basename "$rel_path")
if [ "$basename_file" = ".agent.md" ] || [ "$basename_file" = ".rules.md" ]; then
KEPT+=("$rel_path")
continue
fi
if [ ! -f "$global_file" ]; then
continue # not a framework file, skip
fi
if diff_file "$project_file" "$global_file"; then
rm "$project_file"
DELETED+=("$rel_path")
echo " DELETED $rel_path (identical to global)"
elif [ $? -eq 1 ]; then
ext_path="$EXTENSIONS_DIR/$(basename "$rel_path")"
mkdir -p "$(dirname "$ext_path")"
mv "$project_file" "$ext_path"
MOVED+=("$rel_path -> extensions/$(basename "$rel_path")")
echo " MOVED $rel_path → extensions/$(basename "$rel_path") (customized)"
fi
done < <(find "$PROJECT_AUTOMATON" -maxdepth 1 -type f -name "*.md" -o -name "*.sh" -print0 2>/dev/null || true)
# Report
echo ""
echo "=== Migration Summary ==="
echo " Deleted: ${#DELETED[@]} (identical to global, removed)"
echo " Moved: ${#MOVED[@]} (customized, moved to extensions/)"
echo " Kept: ${#KEPT[@]} (.agent.md and .rules.md preserved)"
if [ ${#ERRORS[@]} -gt 0 ]; then
echo " Errors: ${#ERRORS[@]}"
for err in "${ERRORS[@]}"; do
echo " - $err"
done
fi
echo ""
echo "Migration complete."
+2 -20
View File
@@ -19,26 +19,8 @@ if [ ! -d ".git" ]; then
exit 1
fi
# Fetch latest changes
git fetch origin
echo ""
# Check for local changes
if ! git diff --quiet HEAD; then
echo "WARNING: You have uncommitted changes in $FRAMEWORK_DIR."
echo "These will be lost when pulling updates."
read -p "Do you want to discard your local changes and update? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Update cancelled."
exit 1
fi
git reset --hard HEAD
fi
# Pull latest changes
# Fetch and pull latest changes
git pull origin main
echo ""
echo ""
echo "Update complete."
echo "You can check for breaking changes at: https://gitea.yourdomain.com/hermes/automaton"
+3 -2
View File
@@ -3,8 +3,9 @@ You are working inside the minimal agent framework.
At the very start of every session, you must:
1. Read ~/.automaton/.agent.md (global router)
2. Read the current project's .automaton/.agent.md (if it exists)
3. Read the current project's .automaton/.rules.md (if it exists)
2. Read ~/.automaton/.rules.md (global framework rules)
3. Read the current project's .automaton/.agent.md (if it exists)
4. Read the current project's .automaton/.rules.md (if it exists)
After reading these files, respond with: "Framework context loaded. Ready for task."
+4
View File
@@ -0,0 +1,4 @@
# Review
- **Status**: approved
- **Timestamp**: 2026-06-13T12:26:14.840024
- **Comment**: Looks good