agentic-orchestrator 0.1.26 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/CLAUDE.md +2 -2
- package/README.md +47 -14
- package/agentic/orchestrator/agents.yaml +13 -0
- package/agentic/orchestrator/policy.yaml +3 -0
- package/agentic/orchestrator/schemas/agents.schema.json +76 -0
- package/agentic/orchestrator/schemas/policy.schema.json +16 -0
- package/agentic/orchestrator/schemas/policy.user.schema.json +16 -0
- package/agentic/orchestrator/schemas/state.schema.json +53 -0
- package/apps/control-plane/src/application/configuration-service.ts +181 -0
- package/apps/control-plane/src/application/kernel-tool-wiring.ts +292 -0
- package/apps/control-plane/src/application/services/checkpoint-service.ts +523 -0
- package/apps/control-plane/src/application/services/feature-send-message-service.ts +132 -0
- package/apps/control-plane/src/application/services/patch-service.ts +29 -5
- package/apps/control-plane/src/application/services/repo-operations-service.ts +276 -0
- package/apps/control-plane/src/application/services/worktree-watchdog-service.ts +156 -0
- package/apps/control-plane/src/cli/cli-argument-parser.ts +12 -0
- package/apps/control-plane/src/cli/help-command-handler.ts +17 -0
- package/apps/control-plane/src/cli/init-command-handler.ts +31 -0
- package/apps/control-plane/src/cli/resume-command-handler.ts +31 -4
- package/apps/control-plane/src/cli/rollback-command-handler.ts +217 -0
- package/apps/control-plane/src/cli/run-command-handler.ts +8 -0
- package/apps/control-plane/src/cli/types.ts +3 -0
- package/apps/control-plane/src/core/kernel-types.ts +55 -0
- package/apps/control-plane/src/core/kernel.ts +61 -878
- package/apps/control-plane/src/core/tool-caller.ts +10 -0
- package/apps/control-plane/src/core/utils/field-readers.ts +38 -0
- package/apps/control-plane/src/core/utils/index-normalizer.ts +119 -0
- package/apps/control-plane/src/core/utils/path-normalizers.ts +22 -0
- package/apps/control-plane/src/interfaces/cli/bootstrap.ts +15 -0
- package/apps/control-plane/src/providers/api-worker-provider.ts +14 -12
- package/apps/control-plane/src/providers/cli-worker-provider.ts +82 -12
- package/apps/control-plane/src/providers/providers.ts +45 -24
- package/apps/control-plane/src/providers/worker-provider-factory.ts +36 -1
- package/apps/control-plane/src/supervisor/run-coordinator.ts +91 -36
- package/apps/control-plane/src/supervisor/runtime.ts +107 -1
- package/apps/control-plane/src/supervisor/types.ts +9 -0
- package/apps/control-plane/src/supervisor/worker-decision-loop.ts +253 -14
- package/apps/control-plane/test/checkpoint-service.spec.ts +537 -0
- package/apps/control-plane/test/cli-helpers.spec.ts +28 -0
- package/apps/control-plane/test/cli.unit.spec.ts +52 -0
- package/apps/control-plane/test/configuration-service.spec.ts +466 -0
- package/apps/control-plane/test/dashboard-api.integration.spec.ts +537 -0
- package/apps/control-plane/test/dashboard-client.spec.ts +233 -0
- package/apps/control-plane/test/feature-send-message-service.spec.ts +314 -0
- package/apps/control-plane/test/init-wizard.spec.ts +35 -0
- package/apps/control-plane/test/path-normalizers.spec.ts +41 -0
- package/apps/control-plane/test/repo-operations-service.spec.ts +339 -0
- package/apps/control-plane/test/resume-command.spec.ts +33 -0
- package/apps/control-plane/test/review-workspace-logic.spec.ts +130 -0
- package/apps/control-plane/test/rollback-command.spec.ts +208 -0
- package/apps/control-plane/test/run-coordinator.spec.ts +119 -0
- package/apps/control-plane/test/worker-decision-loop.spec.ts +209 -0
- package/apps/control-plane/test/worker-provider-adapters.spec.ts +102 -0
- package/apps/control-plane/test/worker-provider-factory.spec.ts +14 -0
- package/apps/control-plane/test/worktree-watchdog-service.spec.ts +147 -0
- package/config/agentic/orchestrator/agents.yaml +13 -0
- package/dist/apps/control-plane/application/configuration-service.d.ts +19 -0
- package/dist/apps/control-plane/application/configuration-service.js +123 -0
- package/dist/apps/control-plane/application/configuration-service.js.map +1 -0
- package/dist/apps/control-plane/application/kernel-tool-wiring.d.ts +39 -0
- package/dist/apps/control-plane/application/kernel-tool-wiring.js +38 -0
- package/dist/apps/control-plane/application/kernel-tool-wiring.js.map +1 -0
- package/dist/apps/control-plane/application/services/checkpoint-service.d.ts +84 -0
- package/dist/apps/control-plane/application/services/checkpoint-service.js +367 -0
- package/dist/apps/control-plane/application/services/checkpoint-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/feature-send-message-service.d.ts +25 -0
- package/dist/apps/control-plane/application/services/feature-send-message-service.js +105 -0
- package/dist/apps/control-plane/application/services/feature-send-message-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/patch-service.d.ts +6 -0
- package/dist/apps/control-plane/application/services/patch-service.js +11 -2
- package/dist/apps/control-plane/application/services/patch-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/repo-operations-service.d.ts +70 -0
- package/dist/apps/control-plane/application/services/repo-operations-service.js +213 -0
- package/dist/apps/control-plane/application/services/repo-operations-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/worktree-watchdog-service.d.ts +23 -0
- package/dist/apps/control-plane/application/services/worktree-watchdog-service.js +119 -0
- package/dist/apps/control-plane/application/services/worktree-watchdog-service.js.map +1 -0
- package/dist/apps/control-plane/cli/cli-argument-parser.js +12 -0
- package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
- package/dist/apps/control-plane/cli/help-command-handler.js +17 -0
- package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/init-command-handler.js +23 -0
- package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/resume-command-handler.js +25 -5
- package/dist/apps/control-plane/cli/resume-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/rollback-command-handler.d.ts +6 -0
- package/dist/apps/control-plane/cli/rollback-command-handler.js +177 -0
- package/dist/apps/control-plane/cli/rollback-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/run-command-handler.js +7 -1
- package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/types.d.ts +3 -0
- package/dist/apps/control-plane/cli/types.js +1 -0
- package/dist/apps/control-plane/cli/types.js.map +1 -1
- package/dist/apps/control-plane/core/configuration-service.d.ts +25 -0
- package/dist/apps/control-plane/core/configuration-service.js +130 -0
- package/dist/apps/control-plane/core/configuration-service.js.map +1 -0
- package/dist/apps/control-plane/core/kernel-tool-wiring.d.ts +50 -0
- package/dist/apps/control-plane/core/kernel-tool-wiring.js +44 -0
- package/dist/apps/control-plane/core/kernel-tool-wiring.js.map +1 -0
- package/dist/apps/control-plane/core/kernel-types.d.ts +48 -0
- package/dist/apps/control-plane/core/kernel-types.js +2 -0
- package/dist/apps/control-plane/core/kernel-types.js.map +1 -0
- package/dist/apps/control-plane/core/kernel.d.ts +17 -48
- package/dist/apps/control-plane/core/kernel.js +44 -539
- package/dist/apps/control-plane/core/kernel.js.map +1 -1
- package/dist/apps/control-plane/core/tool-caller.d.ts +10 -0
- package/dist/apps/control-plane/core/utils/error-normalizer.d.ts +2 -0
- package/dist/apps/control-plane/core/utils/error-normalizer.js +51 -0
- package/dist/apps/control-plane/core/utils/error-normalizer.js.map +1 -0
- package/dist/apps/control-plane/core/utils/field-readers.d.ts +9 -0
- package/dist/apps/control-plane/core/utils/field-readers.js +30 -0
- package/dist/apps/control-plane/core/utils/field-readers.js.map +1 -0
- package/dist/apps/control-plane/core/utils/index-normalizer.d.ts +7 -0
- package/dist/apps/control-plane/core/utils/index-normalizer.js +92 -0
- package/dist/apps/control-plane/core/utils/index-normalizer.js.map +1 -0
- package/dist/apps/control-plane/core/utils/path-normalizers.d.ts +2 -0
- package/dist/apps/control-plane/core/utils/path-normalizers.js +17 -0
- package/dist/apps/control-plane/core/utils/path-normalizers.js.map +1 -0
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js +13 -1
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
- package/dist/apps/control-plane/providers/api-worker-provider.d.ts +4 -13
- package/dist/apps/control-plane/providers/api-worker-provider.js +10 -0
- package/dist/apps/control-plane/providers/api-worker-provider.js.map +1 -1
- package/dist/apps/control-plane/providers/cli-worker-provider.d.ts +11 -13
- package/dist/apps/control-plane/providers/cli-worker-provider.js +64 -0
- package/dist/apps/control-plane/providers/cli-worker-provider.js.map +1 -1
- package/dist/apps/control-plane/providers/providers.d.ts +31 -24
- package/dist/apps/control-plane/providers/providers.js +10 -0
- package/dist/apps/control-plane/providers/providers.js.map +1 -1
- package/dist/apps/control-plane/providers/worker-provider-factory.d.ts +11 -0
- package/dist/apps/control-plane/providers/worker-provider-factory.js +20 -1
- package/dist/apps/control-plane/providers/worker-provider-factory.js.map +1 -1
- package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/run-coordinator.js +81 -33
- package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
- package/dist/apps/control-plane/supervisor/runtime.d.ts +8 -1
- package/dist/apps/control-plane/supervisor/runtime.js +90 -0
- package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
- package/dist/apps/control-plane/supervisor/types.d.ts +11 -0
- package/dist/apps/control-plane/supervisor/types.js.map +1 -1
- package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +21 -1
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js +207 -13
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
- package/package.json +1 -1
- package/packages/web-dashboard/package.json +2 -0
- package/packages/web-dashboard/src/app/analytics/page.tsx +83 -2
- package/packages/web-dashboard/src/app/api/actions/route.ts +92 -1
- package/packages/web-dashboard/src/app/api/analytics/route.ts +5 -2
- package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/[checkpointId]/diff/route.ts +43 -0
- package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/compare/route.ts +45 -0
- package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/stream/route.ts +170 -0
- package/packages/web-dashboard/src/app/api/features/[id]/file-diff/route.ts +144 -0
- package/packages/web-dashboard/src/app/api/features/[id]/log-stream/route.ts +167 -0
- package/packages/web-dashboard/src/app/api/features/[id]/raw-logs/[filename]/route.ts +65 -0
- package/packages/web-dashboard/src/app/api/features/[id]/raw-logs/route.ts +63 -0
- package/packages/web-dashboard/src/app/api/features/[id]/timeline/route.ts +60 -0
- package/packages/web-dashboard/src/app/feature/[id]/page.tsx +32 -11
- package/packages/web-dashboard/src/app/globals.css +2 -0
- package/packages/web-dashboard/src/components/detail-panel.tsx +483 -0
- package/packages/web-dashboard/src/components/review-workspace.tsx +1162 -0
- package/packages/web-dashboard/src/lib/aop-client.ts +725 -0
- package/packages/web-dashboard/src/lib/review-contracts.ts +182 -0
- package/packages/web-dashboard/src/lib/review-workspace-logic.ts +64 -0
- package/packages/web-dashboard/src/lib/types.ts +131 -0
- package/packages/web-dashboard/src/styles/dashboard.module.css +333 -0
- package/spec-files/completed/agentic_orchestrator_execution_mode_spec.md +1905 -0
- package/spec-files/outstanding/agentic_orchestrator_runtime_inspection_spec.md +940 -0
- package/spec-files/outstanding/execution_mode_critical_review.md +355 -0
- package/spec-files/outstanding/shadow_workspace_implementation_spec.md +1271 -0
- package/spec-files/outstanding/shadow_workspace_spec_summary.md +222 -0
- package/spec-files/progress.md +269 -1
|
@@ -0,0 +1,1271 @@
|
|
|
1
|
+
# Shadow Workspace Implementation Specification
|
|
2
|
+
|
|
3
|
+
**Status:** Outstanding
|
|
4
|
+
**Created:** 2026-03-05
|
|
5
|
+
**Author:** System
|
|
6
|
+
**Scope:** Detailed implementation of shadow workspace strategy for interactive execution mode
|
|
7
|
+
**Parent Spec:** [Execution Mode Specification](../completed/agentic_orchestrator_execution_mode_spec.md)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 0. Executive Summary
|
|
12
|
+
|
|
13
|
+
### 0.1 Purpose
|
|
14
|
+
|
|
15
|
+
This specification details the **shadow workspace strategy** for interactive execution mode, where agents work in an isolated copy of the feature worktree. Changes are validated before promotion to the real worktree, providing validation-before-write guarantees while maintaining the benefits of direct file system access.
|
|
16
|
+
|
|
17
|
+
### 0.2 Key Concepts
|
|
18
|
+
|
|
19
|
+
**Shadow Workspace:** An isolated copy of the feature worktree where the agent performs all file operations. Changes remain in the shadow until validated and promoted.
|
|
20
|
+
|
|
21
|
+
**Promotion:** The atomic operation of copying validated changes from shadow workspace to real worktree.
|
|
22
|
+
|
|
23
|
+
**Lifecycle:** Shadow creation → Agent execution → Checkpoint validation → Promotion (if valid) or Discard (if invalid) → Repeat
|
|
24
|
+
|
|
25
|
+
### 0.3 Benefits Over Direct Worktree
|
|
26
|
+
|
|
27
|
+
| Aspect | Direct Worktree | Shadow Workspace |
|
|
28
|
+
| -------------------- | -------------------------- | ------------------------------- |
|
|
29
|
+
| Validation timing | After write (risky) | Before promotion (safe) |
|
|
30
|
+
| Rollback complexity | Destructive revert | Simple discard |
|
|
31
|
+
| Real worktree safety | Can be corrupted | Always valid |
|
|
32
|
+
| Race conditions | Possible during checkpoint | Eliminated (shadow is isolated) |
|
|
33
|
+
| Agent restart cost | Low | Medium (must recreate shadow) |
|
|
34
|
+
| Disk overhead | None | 2x worktree size |
|
|
35
|
+
|
|
36
|
+
### 0.4 Trade-offs
|
|
37
|
+
|
|
38
|
+
**Pros:**
|
|
39
|
+
|
|
40
|
+
- Real worktree never corrupted
|
|
41
|
+
- Validation before promotion (safe)
|
|
42
|
+
- Easy rollback (discard shadow)
|
|
43
|
+
- No race conditions
|
|
44
|
+
- Atomic promotion guarantees
|
|
45
|
+
|
|
46
|
+
**Cons:**
|
|
47
|
+
|
|
48
|
+
- 2x disk space required
|
|
49
|
+
- Promotion overhead (copy operation)
|
|
50
|
+
- Agent must restart in fresh shadow after validation failure
|
|
51
|
+
- More complex implementation
|
|
52
|
+
|
|
53
|
+
### 0.5 When to Use
|
|
54
|
+
|
|
55
|
+
**Use shadow workspace for:**
|
|
56
|
+
|
|
57
|
+
- High-risk features (contract changes, schema migrations, API modifications)
|
|
58
|
+
- Features with strict validation requirements
|
|
59
|
+
- Production environments where safety > performance
|
|
60
|
+
- Features modifying protected areas
|
|
61
|
+
|
|
62
|
+
**Use direct worktree for:**
|
|
63
|
+
|
|
64
|
+
- Low-risk features (documentation, tests, internal utilities)
|
|
65
|
+
- Development environments where iteration speed matters
|
|
66
|
+
- Features with simple validation rules
|
|
67
|
+
- Trusted agents with proven track record
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 1. Architecture Overview
|
|
72
|
+
|
|
73
|
+
### 1.1 Component Diagram
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
77
|
+
│ SupervisorRuntime │
|
|
78
|
+
│ ┌──────────────────────────────────────────────────────┐ │
|
|
79
|
+
│ │ InteractiveExecutionService │ │
|
|
80
|
+
│ │ ┌────────────────────────────────────────────────┐ │ │
|
|
81
|
+
│ │ │ ShadowWorkspaceManager │ │ │
|
|
82
|
+
│ │ │ - createShadow() │ │ │
|
|
83
|
+
│ │ │ - promoteShadow() │ │ │
|
|
84
|
+
│ │ │ - discardShadow() │ │ │
|
|
85
|
+
│ │ │ - syncShadow() │ │ │
|
|
86
|
+
│ │ └────────────────────────────────────────────────┘ │ │
|
|
87
|
+
│ │ │ │ │
|
|
88
|
+
│ │ ▼ │ │
|
|
89
|
+
│ │ ┌────────────────────────────────────────────────┐ │ │
|
|
90
|
+
│ │ │ WorktreeWatchdogService │ │ │
|
|
91
|
+
│ │ │ - startWatching(shadowPath) │ │ │
|
|
92
|
+
│ │ │ - getChangedFiles() │ │ │
|
|
93
|
+
│ │ └────────────────────────────────────────────────┘ │ │
|
|
94
|
+
│ │ │ │ │
|
|
95
|
+
│ │ ▼ │ │
|
|
96
|
+
│ │ ┌────────────────────────────────────────────────┐ │ │
|
|
97
|
+
│ │ │ CheckpointService │ │ │
|
|
98
|
+
│ │ │ - validateShadow() │ │ │
|
|
99
|
+
│ │ │ - computeShadowDiff() │ │ │
|
|
100
|
+
│ │ └────────────────────────────────────────────────┘ │ │
|
|
101
|
+
│ └──────────────────────────────────────────────────────┘ │
|
|
102
|
+
└─────────────────────────────────────────────────────────────┘
|
|
103
|
+
│
|
|
104
|
+
▼
|
|
105
|
+
┌────────────────────────┐
|
|
106
|
+
│ WorkerProvider │
|
|
107
|
+
│ (Agent runs in shadow)│
|
|
108
|
+
└────────────────────────┘
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 1.2 Directory Structure
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
.worktrees/
|
|
115
|
+
├── my_feature/ # Real worktree (always valid)
|
|
116
|
+
│ ├── src/
|
|
117
|
+
│ ├── tests/
|
|
118
|
+
│ └── ...
|
|
119
|
+
└── my_feature.shadow/ # Shadow workspace (agent works here)
|
|
120
|
+
├── src/
|
|
121
|
+
├── tests/
|
|
122
|
+
└── ...
|
|
123
|
+
|
|
124
|
+
.aop/features/my_feature/
|
|
125
|
+
├── state.md
|
|
126
|
+
├── plan.json
|
|
127
|
+
├── checkpoints/
|
|
128
|
+
│ ├── ckpt-001.diff # Diff between real and shadow at checkpoint
|
|
129
|
+
│ └── ckpt-002.diff
|
|
130
|
+
└── shadow/
|
|
131
|
+
├── metadata.json # Shadow lifecycle metadata
|
|
132
|
+
└── promotion-history.jsonl # Log of all promotions
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 2. Shadow Workspace Lifecycle
|
|
138
|
+
|
|
139
|
+
### 2.1 Phase 1: Shadow Creation
|
|
140
|
+
|
|
141
|
+
**Trigger:** Feature starts in interactive mode with shadow strategy enabled
|
|
142
|
+
|
|
143
|
+
**Steps:**
|
|
144
|
+
|
|
145
|
+
1. Verify real worktree exists and is clean
|
|
146
|
+
2. Create shadow directory (`.worktrees/<feature_id>.shadow`)
|
|
147
|
+
3. Copy real worktree to shadow (fast copy with hardlinks where possible)
|
|
148
|
+
4. Initialize shadow metadata
|
|
149
|
+
5. Start watchdog on shadow directory
|
|
150
|
+
6. Return shadow path to agent
|
|
151
|
+
|
|
152
|
+
**Implementation:**
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
interface ShadowWorkspaceManager {
|
|
156
|
+
createShadow(featureId: string): Promise<ShadowInfo>;
|
|
157
|
+
promoteShadow(featureId: string, validationResult: ValidationResult): Promise<PromotionResult>;
|
|
158
|
+
discardShadow(featureId: string, reason: string): Promise<void>;
|
|
159
|
+
syncShadow(featureId: string): Promise<void>;
|
|
160
|
+
getShadowInfo(featureId: string): Promise<ShadowInfo>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface ShadowInfo {
|
|
164
|
+
feature_id: string;
|
|
165
|
+
shadow_path: string;
|
|
166
|
+
real_worktree_path: string;
|
|
167
|
+
created_at: string;
|
|
168
|
+
last_sync_at: string;
|
|
169
|
+
generation: number; // Increments on each recreate
|
|
170
|
+
size_bytes: number;
|
|
171
|
+
file_count: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
class ShadowWorkspaceManager {
|
|
175
|
+
async createShadow(featureId: string): Promise<ShadowInfo> {
|
|
176
|
+
const realPath = this.kernel.worktreePath(featureId);
|
|
177
|
+
const shadowPath = `${realPath}.shadow`;
|
|
178
|
+
|
|
179
|
+
// 1. Verify real worktree is clean
|
|
180
|
+
const status = await this.git.status(realPath);
|
|
181
|
+
if (status.modified.length > 0) {
|
|
182
|
+
throw new Error('Real worktree has uncommitted changes');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 2. Remove existing shadow if present
|
|
186
|
+
if (await fs.pathExists(shadowPath)) {
|
|
187
|
+
await fs.remove(shadowPath);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 3. Fast copy with hardlinks (copy-on-write)
|
|
191
|
+
await this.fastCopy(realPath, shadowPath);
|
|
192
|
+
|
|
193
|
+
// 4. Initialize metadata
|
|
194
|
+
const shadowInfo: ShadowInfo = {
|
|
195
|
+
feature_id: featureId,
|
|
196
|
+
shadow_path: shadowPath,
|
|
197
|
+
real_worktree_path: realPath,
|
|
198
|
+
created_at: new Date().toISOString(),
|
|
199
|
+
last_sync_at: new Date().toISOString(),
|
|
200
|
+
generation: await this.getNextGeneration(featureId),
|
|
201
|
+
size_bytes: await this.getDirectorySize(shadowPath),
|
|
202
|
+
file_count: await this.countFiles(shadowPath),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
await this.persistShadowMetadata(featureId, shadowInfo);
|
|
206
|
+
|
|
207
|
+
return shadowInfo;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private async fastCopy(src: string, dest: string): Promise<void> {
|
|
211
|
+
// Use cp with reflink for copy-on-write (fast on modern filesystems)
|
|
212
|
+
await execa('cp', ['-r', '--reflink=auto', src, dest]);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 2.2 Phase 2: Agent Execution in Shadow
|
|
218
|
+
|
|
219
|
+
**Trigger:** Shadow created, agent ready to start
|
|
220
|
+
|
|
221
|
+
**Steps:**
|
|
222
|
+
|
|
223
|
+
1. Spawn agent with `cwd` set to shadow path
|
|
224
|
+
2. Agent performs file operations in shadow
|
|
225
|
+
3. Watchdog monitors shadow for changes
|
|
226
|
+
4. Track changed files for checkpoint
|
|
227
|
+
|
|
228
|
+
**Key Points:**
|
|
229
|
+
|
|
230
|
+
- Agent is unaware it's in a shadow (transparent)
|
|
231
|
+
- All file operations are scoped to shadow
|
|
232
|
+
- Real worktree remains untouched
|
|
233
|
+
- Watchdog only monitors shadow directory
|
|
234
|
+
|
|
235
|
+
**Implementation:**
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
class InteractiveExecutionService {
|
|
239
|
+
async runWithShadowWorkspace(featureId: string): Promise<void> {
|
|
240
|
+
// 1. Create shadow
|
|
241
|
+
const shadowInfo = await this.shadowManager.createShadow(featureId);
|
|
242
|
+
|
|
243
|
+
// 2. Start watchdog on shadow
|
|
244
|
+
await this.watchdog.startWatching(featureId, shadowInfo.shadow_path);
|
|
245
|
+
|
|
246
|
+
// 3. Start checkpoint loop
|
|
247
|
+
const checkpointTimer = setInterval(async () => {
|
|
248
|
+
await this.checkpointShadow(featureId);
|
|
249
|
+
}, this.config.checkpoint_interval_ms);
|
|
250
|
+
|
|
251
|
+
// Also trigger on change threshold
|
|
252
|
+
this.watchdog.on('changeThreshold', async (fid) => {
|
|
253
|
+
if (fid === featureId) {
|
|
254
|
+
await this.checkpointShadow(featureId);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
// 4. Run agent in shadow
|
|
260
|
+
const result = await this.provider.runWorker({
|
|
261
|
+
role: 'builder',
|
|
262
|
+
feature_id: featureId,
|
|
263
|
+
working_directory: shadowInfo.shadow_path, // Agent works in shadow
|
|
264
|
+
execution_mode: 'interactive',
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// 5. Final checkpoint and promotion
|
|
268
|
+
await this.finalCheckpointAndPromote(featureId);
|
|
269
|
+
} finally {
|
|
270
|
+
clearInterval(checkpointTimer);
|
|
271
|
+
await this.watchdog.stopWatching(featureId);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### 2.3 Phase 3: Checkpoint Validation
|
|
278
|
+
|
|
279
|
+
**Trigger:** Checkpoint interval elapsed OR change threshold reached
|
|
280
|
+
|
|
281
|
+
**Steps:**
|
|
282
|
+
|
|
283
|
+
1. Pause agent
|
|
284
|
+
2. Compute diff between real worktree and shadow
|
|
285
|
+
3. Validate diff against plan/policy/locks
|
|
286
|
+
4. If valid: promote shadow changes to real worktree
|
|
287
|
+
5. If invalid: notify agent, optionally discard shadow
|
|
288
|
+
6. Resume agent (in shadow or fresh shadow)
|
|
289
|
+
|
|
290
|
+
**Implementation:**
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
class InteractiveExecutionService {
|
|
294
|
+
private async checkpointShadow(featureId: string): Promise<void> {
|
|
295
|
+
// 1. Pause agent
|
|
296
|
+
const pauseResult = await this.provider.pauseAgent(featureId, 10000);
|
|
297
|
+
if (!pauseResult.acknowledged) {
|
|
298
|
+
this.logger.warn(`Agent ${featureId} did not acknowledge pause`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 2. Wait for file writes to stabilize
|
|
302
|
+
await this.delay(100);
|
|
303
|
+
|
|
304
|
+
// 3. Compute diff between real and shadow
|
|
305
|
+
const shadowInfo = await this.shadowManager.getShadowInfo(featureId);
|
|
306
|
+
const diff = await this.computeShadowDiff(
|
|
307
|
+
shadowInfo.real_worktree_path,
|
|
308
|
+
shadowInfo.shadow_path,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// 4. Validate diff
|
|
312
|
+
const validation = await this.checkpointService.validateCheckpoint(featureId, diff);
|
|
313
|
+
|
|
314
|
+
// 5. Record checkpoint
|
|
315
|
+
await this.checkpointService.recordCheckpoint(featureId, {
|
|
316
|
+
checkpoint_id: this.generateCheckpointId(),
|
|
317
|
+
timestamp: new Date().toISOString(),
|
|
318
|
+
files_changed: this.parseDiffFiles(diff),
|
|
319
|
+
validation_status: validation.valid ? 'valid' : 'invalid',
|
|
320
|
+
violations: validation.violations,
|
|
321
|
+
severity: validation.severity,
|
|
322
|
+
diff_snapshot: await this.storeDiff(featureId, diff),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// 6. Handle validation result
|
|
326
|
+
if (validation.valid) {
|
|
327
|
+
await this.promoteShadowChanges(featureId, shadowInfo, diff);
|
|
328
|
+
} else {
|
|
329
|
+
await this.handleValidationFailure(featureId, validation);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 7. Resume agent
|
|
333
|
+
await this.provider.resumeAgent(featureId);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private async computeShadowDiff(realPath: string, shadowPath: string): Promise<string> {
|
|
337
|
+
// Use git diff to compare real and shadow
|
|
338
|
+
// This captures all changes agent made in shadow
|
|
339
|
+
const result = await execa('git', ['diff', '--no-index', '--no-color', realPath, shadowPath], {
|
|
340
|
+
reject: false,
|
|
341
|
+
}); // Exit code 1 means differences found (expected)
|
|
342
|
+
|
|
343
|
+
return result.stdout;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### 2.4 Phase 4: Promotion (Valid Changes)
|
|
349
|
+
|
|
350
|
+
**Trigger:** Checkpoint validation passed
|
|
351
|
+
|
|
352
|
+
**Steps:**
|
|
353
|
+
|
|
354
|
+
1. Verify real worktree is still clean (no external changes)
|
|
355
|
+
2. Apply shadow diff to real worktree (atomic)
|
|
356
|
+
3. Sync shadow with real worktree (reset shadow to match real)
|
|
357
|
+
4. Log promotion in history
|
|
358
|
+
5. Continue agent execution in shadow
|
|
359
|
+
|
|
360
|
+
**Promotion Strategies:**
|
|
361
|
+
|
|
362
|
+
#### Strategy A: Atomic Copy (Default)
|
|
363
|
+
|
|
364
|
+
Copy changed files from shadow to real worktree atomically.
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
class ShadowWorkspaceManager {
|
|
368
|
+
async promoteShadow(
|
|
369
|
+
featureId: string,
|
|
370
|
+
validationResult: ValidationResult,
|
|
371
|
+
): Promise<PromotionResult> {
|
|
372
|
+
const shadowInfo = await this.getShadowInfo(featureId);
|
|
373
|
+
const changedFiles = validationResult.files_changed;
|
|
374
|
+
|
|
375
|
+
// 1. Verify real worktree is clean
|
|
376
|
+
const status = await this.git.status(shadowInfo.real_worktree_path);
|
|
377
|
+
if (status.modified.length > 0) {
|
|
378
|
+
throw new Error('Real worktree modified externally during shadow execution');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 2. Atomic promotion: copy changed files
|
|
382
|
+
const tempDir = `${shadowInfo.real_worktree_path}.promotion-temp`;
|
|
383
|
+
await fs.ensureDir(tempDir);
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
// Copy changed files to temp
|
|
387
|
+
for (const file of changedFiles) {
|
|
388
|
+
const shadowFile = path.join(shadowInfo.shadow_path, file);
|
|
389
|
+
const tempFile = path.join(tempDir, file);
|
|
390
|
+
|
|
391
|
+
if (await fs.pathExists(shadowFile)) {
|
|
392
|
+
await fs.copy(shadowFile, tempFile);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Atomic move from temp to real
|
|
397
|
+
for (const file of changedFiles) {
|
|
398
|
+
const tempFile = path.join(tempDir, file);
|
|
399
|
+
const realFile = path.join(shadowInfo.real_worktree_path, file);
|
|
400
|
+
|
|
401
|
+
if (await fs.pathExists(tempFile)) {
|
|
402
|
+
await fs.move(tempFile, realFile, { overwrite: true });
|
|
403
|
+
} else {
|
|
404
|
+
// File was deleted in shadow
|
|
405
|
+
await fs.remove(realFile);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 3. Sync shadow with real (reset shadow)
|
|
410
|
+
await this.syncShadow(featureId);
|
|
411
|
+
|
|
412
|
+
// 4. Log promotion
|
|
413
|
+
await this.logPromotion(featureId, {
|
|
414
|
+
timestamp: new Date().toISOString(),
|
|
415
|
+
files_promoted: changedFiles,
|
|
416
|
+
validation_result: validationResult,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
success: true,
|
|
421
|
+
files_promoted: changedFiles.length,
|
|
422
|
+
promotion_time_ms: Date.now() - startTime,
|
|
423
|
+
};
|
|
424
|
+
} finally {
|
|
425
|
+
await fs.remove(tempDir);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async syncShadow(featureId: string): Promise<void> {
|
|
430
|
+
const shadowInfo = await this.getShadowInfo(featureId);
|
|
431
|
+
|
|
432
|
+
// Remove shadow and recreate from real worktree
|
|
433
|
+
await fs.remove(shadowInfo.shadow_path);
|
|
434
|
+
await this.fastCopy(shadowInfo.real_worktree_path, shadowInfo.shadow_path);
|
|
435
|
+
|
|
436
|
+
// Update metadata
|
|
437
|
+
shadowInfo.last_sync_at = new Date().toISOString();
|
|
438
|
+
await this.persistShadowMetadata(featureId, shadowInfo);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
#### Strategy B: Incremental Sync
|
|
444
|
+
|
|
445
|
+
Only sync changed files (faster for large worktrees).
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
async syncShadowIncremental(featureId: string, changedFiles: string[]): Promise<void> {
|
|
449
|
+
const shadowInfo = await this.getShadowInfo(featureId);
|
|
450
|
+
|
|
451
|
+
// Copy only changed files from real to shadow
|
|
452
|
+
for (const file of changedFiles) {
|
|
453
|
+
const realFile = path.join(shadowInfo.real_worktree_path, file);
|
|
454
|
+
const shadowFile = path.join(shadowInfo.shadow_path, file);
|
|
455
|
+
|
|
456
|
+
if (await fs.pathExists(realFile)) {
|
|
457
|
+
await fs.copy(realFile, shadowFile, { overwrite: true });
|
|
458
|
+
} else {
|
|
459
|
+
await fs.remove(shadowFile);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
shadowInfo.last_sync_at = new Date().toISOString();
|
|
464
|
+
await this.persistShadowMetadata(featureId, shadowInfo);
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### 2.5 Phase 5: Discard (Invalid Changes)
|
|
469
|
+
|
|
470
|
+
**Trigger:** Checkpoint validation failed
|
|
471
|
+
|
|
472
|
+
**Steps:**
|
|
473
|
+
|
|
474
|
+
1. Notify agent of violations
|
|
475
|
+
2. Decide: discard shadow OR keep shadow for agent to fix
|
|
476
|
+
3. If discard: remove shadow, recreate fresh from real worktree
|
|
477
|
+
4. If keep: agent continues in shadow with violation feedback
|
|
478
|
+
5. Resume agent
|
|
479
|
+
|
|
480
|
+
**Discard Strategies:**
|
|
481
|
+
|
|
482
|
+
#### Strategy A: Full Discard (Default for critical violations)
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
async discardShadow(featureId: string, reason: string): Promise<void> {
|
|
486
|
+
const shadowInfo = await this.getShadowInfo(featureId);
|
|
487
|
+
|
|
488
|
+
// 1. Log discard event
|
|
489
|
+
await this.logDiscard(featureId, {
|
|
490
|
+
timestamp: new Date().toISOString(),
|
|
491
|
+
reason,
|
|
492
|
+
generation: shadowInfo.generation,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// 2. Remove shadow
|
|
496
|
+
await fs.remove(shadowInfo.shadow_path);
|
|
497
|
+
|
|
498
|
+
// 3. Recreate fresh shadow from real worktree
|
|
499
|
+
await this.createShadow(featureId);
|
|
500
|
+
|
|
501
|
+
// 4. Notify agent
|
|
502
|
+
await this.provider.sendMessage(featureId, {
|
|
503
|
+
type: 'shadow_discarded',
|
|
504
|
+
severity: 'error',
|
|
505
|
+
content: `Shadow workspace discarded due to validation failure: ${reason}`,
|
|
506
|
+
structured_data: { reason, new_generation: shadowInfo.generation + 1 },
|
|
507
|
+
requires_acknowledgment: true,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
#### Strategy B: Partial Discard (Revert only violated files)
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
async discardViolatedFiles(
|
|
516
|
+
featureId: string,
|
|
517
|
+
violations: ValidationViolation[]
|
|
518
|
+
): Promise<void> {
|
|
519
|
+
const shadowInfo = await this.getShadowInfo(featureId);
|
|
520
|
+
const violatedFiles = new Set(violations.map(v => v.file_path));
|
|
521
|
+
|
|
522
|
+
// Revert only violated files in shadow (copy from real worktree)
|
|
523
|
+
for (const file of violatedFiles) {
|
|
524
|
+
const realFile = path.join(shadowInfo.real_worktree_path, file);
|
|
525
|
+
const shadowFile = path.join(shadowInfo.shadow_path, file);
|
|
526
|
+
|
|
527
|
+
if (await fs.pathExists(realFile)) {
|
|
528
|
+
await fs.copy(realFile, shadowFile, { overwrite: true });
|
|
529
|
+
} else {
|
|
530
|
+
await fs.remove(shadowFile);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Notify agent which files were reverted
|
|
535
|
+
await this.provider.sendMessage(featureId, {
|
|
536
|
+
type: 'partial_revert',
|
|
537
|
+
severity: 'warning',
|
|
538
|
+
content: `Reverted ${violatedFiles.size} files with violations`,
|
|
539
|
+
structured_data: {
|
|
540
|
+
reverted_files: Array.from(violatedFiles),
|
|
541
|
+
violations
|
|
542
|
+
},
|
|
543
|
+
requires_acknowledgment: true,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## 3. Configuration
|
|
551
|
+
|
|
552
|
+
### 3.1 Schema Changes
|
|
553
|
+
|
|
554
|
+
**File:** `agentic/orchestrator/schemas/agents.schema.json`
|
|
555
|
+
|
|
556
|
+
```json
|
|
557
|
+
{
|
|
558
|
+
"runtime": {
|
|
559
|
+
"execution_mode": "interactive",
|
|
560
|
+
"interactive": {
|
|
561
|
+
"strategy": "shadow_workspace",
|
|
562
|
+
"shadow_workspace": {
|
|
563
|
+
"enabled": true,
|
|
564
|
+
"promotion_strategy": "atomic",
|
|
565
|
+
"sync_strategy": "full",
|
|
566
|
+
"discard_strategy": "full",
|
|
567
|
+
"cleanup_on_failure": true,
|
|
568
|
+
"max_shadow_size_mb": 2048,
|
|
569
|
+
"max_shadow_age_hours": 24,
|
|
570
|
+
"hardlink_optimization": true,
|
|
571
|
+
"violation_handling": {
|
|
572
|
+
"critical": "discard_full",
|
|
573
|
+
"error": "discard_violated_files",
|
|
574
|
+
"warning": "keep_shadow",
|
|
575
|
+
"info": "keep_shadow"
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### 3.2 Configuration Options
|
|
584
|
+
|
|
585
|
+
| Option | Type | Default | Description |
|
|
586
|
+
| ----------------------- | ------- | --------- | ------------------------------------------------------------ |
|
|
587
|
+
| `enabled` | boolean | `false` | Enable shadow workspace strategy |
|
|
588
|
+
| `promotion_strategy` | enum | `atomic` | How to promote changes (`atomic` \| `incremental`) |
|
|
589
|
+
| `sync_strategy` | enum | `full` | How to sync shadow after promotion (`full` \| `incremental`) |
|
|
590
|
+
| `discard_strategy` | enum | `full` | How to discard invalid changes (`full` \| `partial`) |
|
|
591
|
+
| `cleanup_on_failure` | boolean | `true` | Delete shadow after validation failure |
|
|
592
|
+
| `max_shadow_size_mb` | number | `2048` | Maximum shadow workspace size (MB) |
|
|
593
|
+
| `max_shadow_age_hours` | number | `24` | Maximum shadow age before forced cleanup |
|
|
594
|
+
| `hardlink_optimization` | boolean | `true` | Use hardlinks for fast copy (copy-on-write) |
|
|
595
|
+
| `violation_handling` | object | See above | Per-severity discard strategy |
|
|
596
|
+
|
|
597
|
+
### 3.3 Feature-Level Override
|
|
598
|
+
|
|
599
|
+
Features can override shadow strategy in `state.md`:
|
|
600
|
+
|
|
601
|
+
```yaml
|
|
602
|
+
---
|
|
603
|
+
feature_id: my_feature
|
|
604
|
+
execution_mode: interactive
|
|
605
|
+
shadow_workspace:
|
|
606
|
+
enabled: true
|
|
607
|
+
discard_strategy: partial # Override: keep valid changes
|
|
608
|
+
---
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
## 4. Performance Optimization
|
|
614
|
+
|
|
615
|
+
### 4.1 Fast Copy with Hardlinks
|
|
616
|
+
|
|
617
|
+
**Problem:** Copying large worktrees (>1GB) is slow.
|
|
618
|
+
|
|
619
|
+
**Solution:** Use copy-on-write (reflink) or hardlinks.
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
async fastCopy(src: string, dest: string): Promise<void> {
|
|
623
|
+
if (this.config.hardlink_optimization) {
|
|
624
|
+
// Try reflink first (copy-on-write on btrfs, xfs, apfs)
|
|
625
|
+
try {
|
|
626
|
+
await execa('cp', ['-r', '--reflink=always', src, dest]);
|
|
627
|
+
return;
|
|
628
|
+
} catch {
|
|
629
|
+
// Reflink not supported, fall back to hardlinks
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Use hardlinks (fast, but files share inodes until modified)
|
|
633
|
+
await execa('cp', ['-rl', src, dest]);
|
|
634
|
+
} else {
|
|
635
|
+
// Standard copy (slow but safe)
|
|
636
|
+
await fs.copy(src, dest);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
**Benchmark:**
|
|
642
|
+
|
|
643
|
+
- Standard copy (1GB worktree): ~10s
|
|
644
|
+
- Hardlink copy (1GB worktree): ~500ms
|
|
645
|
+
- Reflink copy (1GB worktree): ~100ms
|
|
646
|
+
|
|
647
|
+
### 4.2 Incremental Sync
|
|
648
|
+
|
|
649
|
+
**Problem:** Full shadow recreation after each promotion is slow.
|
|
650
|
+
|
|
651
|
+
**Solution:** Only sync changed files.
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
async syncShadowIncremental(featureId: string, promotedFiles: string[]): Promise<void> {
|
|
655
|
+
const shadowInfo = await this.getShadowInfo(featureId);
|
|
656
|
+
|
|
657
|
+
// Only copy promoted files from real to shadow
|
|
658
|
+
await pMap(promotedFiles, async (file) => {
|
|
659
|
+
const realFile = path.join(shadowInfo.real_worktree_path, file);
|
|
660
|
+
const shadowFile = path.join(shadowInfo.shadow_path, file);
|
|
661
|
+
await fs.copy(realFile, shadowFile, { overwrite: true });
|
|
662
|
+
}, { concurrency: 10 });
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
**Benchmark:**
|
|
667
|
+
|
|
668
|
+
- Full sync (1GB worktree): ~10s
|
|
669
|
+
- Incremental sync (50 files): ~200ms
|
|
670
|
+
|
|
671
|
+
### 4.3 Lazy Shadow Creation
|
|
672
|
+
|
|
673
|
+
**Problem:** Creating shadow upfront delays agent start.
|
|
674
|
+
|
|
675
|
+
**Solution:** Create shadow in background while agent initializes.
|
|
676
|
+
|
|
677
|
+
```typescript
|
|
678
|
+
async runWithLazyShadow(featureId: string): Promise<void> {
|
|
679
|
+
// Start shadow creation in background
|
|
680
|
+
const shadowPromise = this.shadowManager.createShadow(featureId);
|
|
681
|
+
|
|
682
|
+
// Initialize agent in parallel
|
|
683
|
+
const agentPromise = this.provider.createSession('builder', featureId, systemPrompt);
|
|
684
|
+
|
|
685
|
+
// Wait for both
|
|
686
|
+
const [shadowInfo, session] = await Promise.all([shadowPromise, agentPromise]);
|
|
687
|
+
|
|
688
|
+
// Now run agent in shadow
|
|
689
|
+
await this.provider.runWorker({
|
|
690
|
+
working_directory: shadowInfo.shadow_path,
|
|
691
|
+
// ...
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### 4.4 Shadow Pooling
|
|
697
|
+
|
|
698
|
+
**Problem:** Creating/destroying shadows repeatedly is wasteful.
|
|
699
|
+
|
|
700
|
+
**Solution:** Maintain pool of pre-created shadows.
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
class ShadowPool {
|
|
704
|
+
private pool = new Map<string, ShadowInfo[]>();
|
|
705
|
+
private readonly POOL_SIZE = 3;
|
|
706
|
+
|
|
707
|
+
async getOrCreateShadow(featureId: string): Promise<ShadowInfo> {
|
|
708
|
+
const available = this.pool.get(featureId) || [];
|
|
709
|
+
|
|
710
|
+
if (available.length > 0) {
|
|
711
|
+
return available.pop()!;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return await this.shadowManager.createShadow(featureId);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async returnShadow(featureId: string, shadowInfo: ShadowInfo): Promise<void> {
|
|
718
|
+
const pool = this.pool.get(featureId) || [];
|
|
719
|
+
|
|
720
|
+
if (pool.length < this.POOL_SIZE) {
|
|
721
|
+
// Clean shadow and return to pool
|
|
722
|
+
await this.cleanShadow(shadowInfo);
|
|
723
|
+
pool.push(shadowInfo);
|
|
724
|
+
this.pool.set(featureId, pool);
|
|
725
|
+
} else {
|
|
726
|
+
// Pool full, discard
|
|
727
|
+
await fs.remove(shadowInfo.shadow_path);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
---
|
|
734
|
+
|
|
735
|
+
## 5. Error Handling and Edge Cases
|
|
736
|
+
|
|
737
|
+
### 5.1 Shadow Creation Failures
|
|
738
|
+
|
|
739
|
+
**Scenario:** Disk full, permission denied, or real worktree corrupted.
|
|
740
|
+
|
|
741
|
+
**Handling:**
|
|
742
|
+
|
|
743
|
+
```typescript
|
|
744
|
+
async createShadow(featureId: string): Promise<ShadowInfo> {
|
|
745
|
+
try {
|
|
746
|
+
// Attempt shadow creation
|
|
747
|
+
return await this._createShadow(featureId);
|
|
748
|
+
} catch (error) {
|
|
749
|
+
if (error.code === 'ENOSPC') {
|
|
750
|
+
// Disk full: cleanup old shadows and retry
|
|
751
|
+
await this.cleanupOldShadows();
|
|
752
|
+
return await this._createShadow(featureId);
|
|
753
|
+
} else if (error.code === 'EACCES') {
|
|
754
|
+
// Permission denied: escalate to human
|
|
755
|
+
throw new Error('Shadow creation failed: permission denied');
|
|
756
|
+
} else {
|
|
757
|
+
// Unknown error: fall back to direct worktree mode
|
|
758
|
+
this.logger.error(`Shadow creation failed: ${error.message}`);
|
|
759
|
+
throw new Error('Shadow creation failed, falling back to direct worktree');
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
### 5.2 Promotion Failures
|
|
766
|
+
|
|
767
|
+
**Scenario:** Real worktree modified externally during shadow execution.
|
|
768
|
+
|
|
769
|
+
**Handling:**
|
|
770
|
+
|
|
771
|
+
```typescript
|
|
772
|
+
async promoteShadow(featureId: string): Promise<PromotionResult> {
|
|
773
|
+
const shadowInfo = await this.getShadowInfo(featureId);
|
|
774
|
+
|
|
775
|
+
// Verify real worktree is clean
|
|
776
|
+
const status = await this.git.status(shadowInfo.real_worktree_path);
|
|
777
|
+
|
|
778
|
+
if (status.modified.length > 0) {
|
|
779
|
+
// Real worktree modified externally
|
|
780
|
+
this.logger.error(`Real worktree modified externally: ${status.modified}`);
|
|
781
|
+
|
|
782
|
+
// Options:
|
|
783
|
+
// 1. Abort promotion, notify human
|
|
784
|
+
// 2. Attempt 3-way merge
|
|
785
|
+
// 3. Force promotion (overwrite external changes)
|
|
786
|
+
|
|
787
|
+
// Default: abort and escalate
|
|
788
|
+
throw new Error('Promotion aborted: real worktree modified externally');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Proceed with promotion
|
|
792
|
+
return await this._promoteShadow(featureId);
|
|
793
|
+
}
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
### 5.3 Shadow Corruption
|
|
797
|
+
|
|
798
|
+
**Scenario:** Agent corrupts shadow (e.g., deletes `.git`, creates infinite loop).
|
|
799
|
+
|
|
800
|
+
**Handling:**
|
|
801
|
+
|
|
802
|
+
```typescript
|
|
803
|
+
async validateShadowIntegrity(featureId: string): Promise<boolean> {
|
|
804
|
+
const shadowInfo = await this.getShadowInfo(featureId);
|
|
805
|
+
|
|
806
|
+
// Check .git directory exists
|
|
807
|
+
const gitDir = path.join(shadowInfo.shadow_path, '.git');
|
|
808
|
+
if (!await fs.pathExists(gitDir)) {
|
|
809
|
+
this.logger.error(`Shadow corrupted: .git directory missing`);
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Check shadow size is reasonable
|
|
814
|
+
const size = await this.getDirectorySize(shadowInfo.shadow_path);
|
|
815
|
+
if (size > this.config.max_shadow_size_mb * 1024 * 1024) {
|
|
816
|
+
this.logger.error(`Shadow corrupted: size exceeds limit (${size} bytes)`);
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Check file count is reasonable
|
|
821
|
+
const fileCount = await this.countFiles(shadowInfo.shadow_path);
|
|
822
|
+
if (fileCount > 100000) {
|
|
823
|
+
this.logger.error(`Shadow corrupted: too many files (${fileCount})`);
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async checkpointShadow(featureId: string): Promise<void> {
|
|
831
|
+
// Validate shadow integrity before checkpoint
|
|
832
|
+
const isValid = await this.validateShadowIntegrity(featureId);
|
|
833
|
+
|
|
834
|
+
if (!isValid) {
|
|
835
|
+
// Shadow corrupted: discard and recreate
|
|
836
|
+
await this.discardShadow(featureId, 'shadow_corrupted');
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Proceed with normal checkpoint
|
|
841
|
+
// ...
|
|
842
|
+
}
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
### 5.4 Concurrent Promotions
|
|
846
|
+
|
|
847
|
+
**Scenario:** Multiple checkpoints trigger simultaneously (race condition).
|
|
848
|
+
|
|
849
|
+
**Handling:**
|
|
850
|
+
|
|
851
|
+
```typescript
|
|
852
|
+
class ShadowWorkspaceManager {
|
|
853
|
+
private promotionLocks = new Map<string, Promise<PromotionResult>>();
|
|
854
|
+
|
|
855
|
+
async promoteShadow(featureId: string): Promise<PromotionResult> {
|
|
856
|
+
// Serialize promotions per feature
|
|
857
|
+
const existingPromotion = this.promotionLocks.get(featureId);
|
|
858
|
+
if (existingPromotion) {
|
|
859
|
+
this.logger.warn(`Promotion already in progress for ${featureId}, waiting...`);
|
|
860
|
+
return await existingPromotion;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const promotionPromise = this._promoteShadow(featureId);
|
|
864
|
+
this.promotionLocks.set(featureId, promotionPromise);
|
|
865
|
+
|
|
866
|
+
try {
|
|
867
|
+
return await promotionPromise;
|
|
868
|
+
} finally {
|
|
869
|
+
this.promotionLocks.delete(featureId);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
---
|
|
876
|
+
|
|
877
|
+
## 6. Monitoring and Observability
|
|
878
|
+
|
|
879
|
+
### 6.1 Metrics
|
|
880
|
+
|
|
881
|
+
**Shadow lifecycle metrics:**
|
|
882
|
+
|
|
883
|
+
- Shadow creation time (p50, p95, p99)
|
|
884
|
+
- Shadow size distribution
|
|
885
|
+
- Promotion time (p50, p95, p99)
|
|
886
|
+
- Promotion success rate
|
|
887
|
+
- Discard rate (by severity)
|
|
888
|
+
- Shadow age distribution
|
|
889
|
+
|
|
890
|
+
**Resource metrics:**
|
|
891
|
+
|
|
892
|
+
- Total disk usage (all shadows)
|
|
893
|
+
- Disk usage per shadow
|
|
894
|
+
- Shadow count (active, pooled, orphaned)
|
|
895
|
+
- Hardlink effectiveness (% space saved)
|
|
896
|
+
|
|
897
|
+
**Validation metrics:**
|
|
898
|
+
|
|
899
|
+
- Validation pass rate (shadow vs direct worktree)
|
|
900
|
+
- Violations per checkpoint (shadow vs direct worktree)
|
|
901
|
+
- Time to first violation
|
|
902
|
+
|
|
903
|
+
### 6.2 Logging
|
|
904
|
+
|
|
905
|
+
**Shadow events to log:**
|
|
906
|
+
|
|
907
|
+
```typescript
|
|
908
|
+
interface ShadowEvent {
|
|
909
|
+
event_type: 'created' | 'promoted' | 'discarded' | 'synced' | 'corrupted';
|
|
910
|
+
feature_id: string;
|
|
911
|
+
timestamp: string;
|
|
912
|
+
generation: number;
|
|
913
|
+
details: Record<string, unknown>;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Example log entries
|
|
917
|
+
{
|
|
918
|
+
"event_type": "created",
|
|
919
|
+
"feature_id": "my_feature",
|
|
920
|
+
"timestamp": "2026-03-05T17:45:00.000Z",
|
|
921
|
+
"generation": 1,
|
|
922
|
+
"details": {
|
|
923
|
+
"shadow_path": ".worktrees/my_feature.shadow",
|
|
924
|
+
"size_bytes": 1048576000,
|
|
925
|
+
"file_count": 5432,
|
|
926
|
+
"creation_time_ms": 523
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
{
|
|
931
|
+
"event_type": "promoted",
|
|
932
|
+
"feature_id": "my_feature",
|
|
933
|
+
"timestamp": "2026-03-05T17:46:30.000Z",
|
|
934
|
+
"generation": 1,
|
|
935
|
+
"details": {
|
|
936
|
+
"files_promoted": 12,
|
|
937
|
+
"promotion_time_ms": 145,
|
|
938
|
+
"validation_status": "valid"
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
{
|
|
943
|
+
"event_type": "discarded",
|
|
944
|
+
"feature_id": "my_feature",
|
|
945
|
+
"timestamp": "2026-03-05T17:47:15.000Z",
|
|
946
|
+
"generation": 1,
|
|
947
|
+
"details": {
|
|
948
|
+
"reason": "validation_failed",
|
|
949
|
+
"violations": ["Path 'src/config.ts' not in allowed_areas"],
|
|
950
|
+
"severity": "error"
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
### 6.3 Alerts
|
|
956
|
+
|
|
957
|
+
**Alert conditions:**
|
|
958
|
+
|
|
959
|
+
- Shadow creation time > 5s
|
|
960
|
+
- Promotion time > 2s
|
|
961
|
+
- Discard rate > 30%
|
|
962
|
+
- Total shadow disk usage > 80% of limit
|
|
963
|
+
- Shadow age > 24 hours (orphaned shadow)
|
|
964
|
+
- Shadow corruption detected
|
|
965
|
+
|
|
966
|
+
---
|
|
967
|
+
|
|
968
|
+
## 7. Testing Strategy
|
|
969
|
+
|
|
970
|
+
### 7.1 Unit Tests
|
|
971
|
+
|
|
972
|
+
**ShadowWorkspaceManager:**
|
|
973
|
+
|
|
974
|
+
- `createShadow()` creates shadow with correct structure
|
|
975
|
+
- `promoteShadow()` copies only changed files
|
|
976
|
+
- `discardShadow()` removes shadow and recreates
|
|
977
|
+
- `syncShadow()` resets shadow to match real worktree
|
|
978
|
+
- Fast copy uses hardlinks when available
|
|
979
|
+
- Promotion is atomic (all or nothing)
|
|
980
|
+
|
|
981
|
+
**CheckpointService:**
|
|
982
|
+
|
|
983
|
+
- `computeShadowDiff()` correctly compares real and shadow
|
|
984
|
+
- `validateShadow()` enforces plan/policy/locks
|
|
985
|
+
- Validation failures trigger correct discard strategy
|
|
986
|
+
|
|
987
|
+
### 7.2 Integration Tests
|
|
988
|
+
|
|
989
|
+
**Shadow lifecycle:**
|
|
990
|
+
|
|
991
|
+
1. Create shadow → Agent modifies files → Checkpoint validates → Promote → Verify real worktree updated
|
|
992
|
+
2. Create shadow → Agent violates policy → Checkpoint fails → Discard → Verify shadow recreated
|
|
993
|
+
3. Create shadow → Agent corrupts shadow → Checkpoint detects → Discard → Verify recovery
|
|
994
|
+
|
|
995
|
+
**Concurrent features:**
|
|
996
|
+
|
|
997
|
+
1. Start 3 features in shadow mode simultaneously
|
|
998
|
+
2. Verify each has isolated shadow
|
|
999
|
+
3. Verify promotions don't interfere
|
|
1000
|
+
4. Verify disk usage is tracked correctly
|
|
1001
|
+
|
|
1002
|
+
### 7.3 Performance Tests
|
|
1003
|
+
|
|
1004
|
+
**Benchmarks:**
|
|
1005
|
+
|
|
1006
|
+
- Shadow creation time vs worktree size (100MB, 1GB, 10GB)
|
|
1007
|
+
- Promotion time vs changed file count (10, 100, 1000 files)
|
|
1008
|
+
- Sync time (full vs incremental)
|
|
1009
|
+
- Disk space overhead (hardlinks vs standard copy)
|
|
1010
|
+
|
|
1011
|
+
**Load tests:**
|
|
1012
|
+
|
|
1013
|
+
- 10 concurrent features in shadow mode
|
|
1014
|
+
- 100 checkpoints per feature
|
|
1015
|
+
- Verify no memory leaks
|
|
1016
|
+
- Verify disk cleanup works
|
|
1017
|
+
|
|
1018
|
+
---
|
|
1019
|
+
|
|
1020
|
+
## 8. Migration Path
|
|
1021
|
+
|
|
1022
|
+
### 8.1 Phase 1: Infrastructure (Week 1)
|
|
1023
|
+
|
|
1024
|
+
- Implement `ShadowWorkspaceManager` service
|
|
1025
|
+
- Add shadow configuration to `agents.schema.json`
|
|
1026
|
+
- Implement fast copy with hardlink optimization
|
|
1027
|
+
- Add shadow metadata persistence
|
|
1028
|
+
- Unit tests (>= 90% coverage)
|
|
1029
|
+
|
|
1030
|
+
### 8.2 Phase 2: Lifecycle Integration (Week 2)
|
|
1031
|
+
|
|
1032
|
+
- Integrate shadow creation into `InteractiveExecutionService`
|
|
1033
|
+
- Implement checkpoint validation for shadow
|
|
1034
|
+
- Implement promotion logic (atomic strategy)
|
|
1035
|
+
- Implement discard logic (full strategy)
|
|
1036
|
+
- Integration tests
|
|
1037
|
+
|
|
1038
|
+
### 8.3 Phase 3: Optimization (Week 3)
|
|
1039
|
+
|
|
1040
|
+
- Implement incremental sync
|
|
1041
|
+
- Implement partial discard
|
|
1042
|
+
- Add shadow pooling
|
|
1043
|
+
- Performance benchmarks
|
|
1044
|
+
- Load tests
|
|
1045
|
+
|
|
1046
|
+
### 8.4 Phase 4: Monitoring and Rollout (Week 4)
|
|
1047
|
+
|
|
1048
|
+
- Add shadow metrics and logging
|
|
1049
|
+
- Add shadow alerts
|
|
1050
|
+
- Dashboard integration (shadow status in RuntimeInspector)
|
|
1051
|
+
- Documentation
|
|
1052
|
+
- Beta rollout (opt-in via config)
|
|
1053
|
+
|
|
1054
|
+
---
|
|
1055
|
+
|
|
1056
|
+
## 9. Acceptance Criteria
|
|
1057
|
+
|
|
1058
|
+
### 9.1 Must Have
|
|
1059
|
+
|
|
1060
|
+
- [ ] `ShadowWorkspaceManager` implemented with all methods
|
|
1061
|
+
- [ ] Shadow creation uses hardlinks (< 1s for 1GB worktree)
|
|
1062
|
+
- [ ] Promotion is atomic (all files or none)
|
|
1063
|
+
- [ ] Discard recreates fresh shadow from real worktree
|
|
1064
|
+
- [ ] Validation enforces plan/policy/locks (same as direct worktree)
|
|
1065
|
+
- [ ] Real worktree never corrupted by agent
|
|
1066
|
+
- [ ] Shadow integrity checks detect corruption
|
|
1067
|
+
- [ ] Concurrent features have isolated shadows
|
|
1068
|
+
- [ ] All unit tests pass (>= 90% coverage)
|
|
1069
|
+
- [ ] All integration tests pass
|
|
1070
|
+
|
|
1071
|
+
### 9.2 Should Have
|
|
1072
|
+
|
|
1073
|
+
- [ ] Incremental sync (< 500ms for 50 files)
|
|
1074
|
+
- [ ] Partial discard (revert only violated files)
|
|
1075
|
+
- [ ] Shadow pooling (reduce creation overhead)
|
|
1076
|
+
- [ ] Metrics and logging for all shadow events
|
|
1077
|
+
- [ ] Dashboard displays shadow status
|
|
1078
|
+
- [ ] Alerts for shadow issues
|
|
1079
|
+
- [ ] Performance benchmarks documented
|
|
1080
|
+
|
|
1081
|
+
### 9.3 Nice to Have
|
|
1082
|
+
|
|
1083
|
+
- [ ] Shadow diff viewer in dashboard
|
|
1084
|
+
- [ ] Shadow history timeline
|
|
1085
|
+
- [ ] Manual shadow promotion command (`aop promote --feature-id <id>`)
|
|
1086
|
+
- [ ] Shadow inspection command (`aop shadow inspect --feature-id <id>`)
|
|
1087
|
+
- [ ] Shadow cleanup command (`aop shadow cleanup --all`)
|
|
1088
|
+
|
|
1089
|
+
---
|
|
1090
|
+
|
|
1091
|
+
## 10. Comparison with Direct Worktree
|
|
1092
|
+
|
|
1093
|
+
### 10.1 Safety
|
|
1094
|
+
|
|
1095
|
+
| Aspect | Direct Worktree | Shadow Workspace |
|
|
1096
|
+
| ----------------------------- | ------------------ | ----------------------------- |
|
|
1097
|
+
| Real worktree corruption risk | High | None |
|
|
1098
|
+
| Validation timing | After write | Before promotion |
|
|
1099
|
+
| Rollback complexity | Destructive revert | Simple discard |
|
|
1100
|
+
| Race conditions | Possible | Eliminated |
|
|
1101
|
+
| Agent can escape worktree | Yes (via symlinks) | Yes (but doesn't affect real) |
|
|
1102
|
+
|
|
1103
|
+
**Winner:** Shadow Workspace (much safer)
|
|
1104
|
+
|
|
1105
|
+
### 10.2 Performance
|
|
1106
|
+
|
|
1107
|
+
| Aspect | Direct Worktree | Shadow Workspace |
|
|
1108
|
+
| --------------------- | --------------- | -------------------------- |
|
|
1109
|
+
| Agent start time | Instant | +500ms (shadow creation) |
|
|
1110
|
+
| Checkpoint overhead | Low | Medium (diff + promotion) |
|
|
1111
|
+
| Disk usage | 1x | 2x (with hardlinks: ~1.1x) |
|
|
1112
|
+
| Validation speed | Same | Same |
|
|
1113
|
+
| Agent iteration speed | Fast | Fast (works in shadow) |
|
|
1114
|
+
|
|
1115
|
+
**Winner:** Direct Worktree (slightly faster)
|
|
1116
|
+
|
|
1117
|
+
### 10.3 Complexity
|
|
1118
|
+
|
|
1119
|
+
| Aspect | Direct Worktree | Shadow Workspace |
|
|
1120
|
+
| ---------------------------- | --------------- | ---------------- |
|
|
1121
|
+
| Implementation lines of code | ~500 | ~1500 |
|
|
1122
|
+
| Service dependencies | 2 | 3 |
|
|
1123
|
+
| Configuration options | 5 | 12 |
|
|
1124
|
+
| Error scenarios | 8 | 15 |
|
|
1125
|
+
| Testing surface | Medium | Large |
|
|
1126
|
+
|
|
1127
|
+
**Winner:** Direct Worktree (simpler)
|
|
1128
|
+
|
|
1129
|
+
### 10.4 Recommendation
|
|
1130
|
+
|
|
1131
|
+
**Use Shadow Workspace when:**
|
|
1132
|
+
|
|
1133
|
+
- Safety is paramount (production environments)
|
|
1134
|
+
- Features modify contracts, schemas, or migrations
|
|
1135
|
+
- Agent is untrusted or experimental
|
|
1136
|
+
- Validation failures are expected to be common
|
|
1137
|
+
- Rollback needs to be fast and reliable
|
|
1138
|
+
|
|
1139
|
+
**Use Direct Worktree when:**
|
|
1140
|
+
|
|
1141
|
+
- Iteration speed is critical (development environments)
|
|
1142
|
+
- Features are low-risk (docs, tests, internal utilities)
|
|
1143
|
+
- Agent is trusted and proven
|
|
1144
|
+
- Validation failures are rare
|
|
1145
|
+
- Disk space is constrained
|
|
1146
|
+
|
|
1147
|
+
**Hybrid approach:** Start with direct worktree, automatically switch to shadow workspace on first validation failure.
|
|
1148
|
+
|
|
1149
|
+
---
|
|
1150
|
+
|
|
1151
|
+
## 11. Future Enhancements
|
|
1152
|
+
|
|
1153
|
+
### 11.1 Multi-Shadow Branching
|
|
1154
|
+
|
|
1155
|
+
**Concept:** Agent can create multiple shadow branches to explore alternatives.
|
|
1156
|
+
|
|
1157
|
+
**Use case:** Agent wants to try two different implementation approaches in parallel.
|
|
1158
|
+
|
|
1159
|
+
```typescript
|
|
1160
|
+
interface ShadowBranch {
|
|
1161
|
+
branch_id: string;
|
|
1162
|
+
parent_shadow_id: string;
|
|
1163
|
+
created_at: string;
|
|
1164
|
+
description: string;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
async createShadowBranch(featureId: string, description: string): Promise<ShadowBranch> {
|
|
1168
|
+
const currentShadow = await this.getShadowInfo(featureId);
|
|
1169
|
+
const branchShadow = await this.copyShadow(currentShadow, `${featureId}.branch-${uuid()}`);
|
|
1170
|
+
|
|
1171
|
+
return {
|
|
1172
|
+
branch_id: branchShadow.shadow_path,
|
|
1173
|
+
parent_shadow_id: currentShadow.shadow_path,
|
|
1174
|
+
created_at: new Date().toISOString(),
|
|
1175
|
+
description,
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
### 11.2 Shadow Snapshots
|
|
1181
|
+
|
|
1182
|
+
**Concept:** Create lightweight snapshots of shadow at any point for easy rollback.
|
|
1183
|
+
|
|
1184
|
+
**Use case:** Agent wants to experiment but be able to revert to a known-good state.
|
|
1185
|
+
|
|
1186
|
+
```typescript
|
|
1187
|
+
async createShadowSnapshot(featureId: string, label: string): Promise<SnapshotInfo> {
|
|
1188
|
+
const shadowInfo = await this.getShadowInfo(featureId);
|
|
1189
|
+
|
|
1190
|
+
// Use git to create snapshot (lightweight)
|
|
1191
|
+
await this.git.commit(shadowInfo.shadow_path, `Snapshot: ${label}`);
|
|
1192
|
+
const commitHash = await this.git.getHeadCommit(shadowInfo.shadow_path);
|
|
1193
|
+
|
|
1194
|
+
return {
|
|
1195
|
+
snapshot_id: commitHash,
|
|
1196
|
+
label,
|
|
1197
|
+
created_at: new Date().toISOString(),
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
async rollbackToSnapshot(featureId: string, snapshotId: string): Promise<void> {
|
|
1202
|
+
const shadowInfo = await this.getShadowInfo(featureId);
|
|
1203
|
+
await this.git.reset(shadowInfo.shadow_path, snapshotId, { hard: true });
|
|
1204
|
+
}
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
### 11.3 Shadow Diff Streaming
|
|
1208
|
+
|
|
1209
|
+
**Concept:** Stream shadow changes to dashboard in real-time.
|
|
1210
|
+
|
|
1211
|
+
**Use case:** Human observer wants to watch agent work live.
|
|
1212
|
+
|
|
1213
|
+
```typescript
|
|
1214
|
+
async streamShadowChanges(featureId: string): AsyncIterableIterator<FileChange> {
|
|
1215
|
+
const shadowInfo = await this.getShadowInfo(featureId);
|
|
1216
|
+
|
|
1217
|
+
const watcher = chokidar.watch(shadowInfo.shadow_path);
|
|
1218
|
+
|
|
1219
|
+
for await (const event of watcher) {
|
|
1220
|
+
yield {
|
|
1221
|
+
event_type: event.type,
|
|
1222
|
+
file_path: event.path,
|
|
1223
|
+
timestamp: new Date().toISOString(),
|
|
1224
|
+
diff: await this.computeFileDiff(event.path),
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
### 11.4 Shadow Collaboration
|
|
1231
|
+
|
|
1232
|
+
**Concept:** Multiple agents work in same shadow with conflict resolution.
|
|
1233
|
+
|
|
1234
|
+
**Use case:** Planner and builder agents collaborate on same feature.
|
|
1235
|
+
|
|
1236
|
+
**Challenge:** Requires sophisticated merge logic and coordination.
|
|
1237
|
+
|
|
1238
|
+
---
|
|
1239
|
+
|
|
1240
|
+
## 12. References
|
|
1241
|
+
|
|
1242
|
+
### 12.1 Related Specifications
|
|
1243
|
+
|
|
1244
|
+
- **Parent Spec:** [Execution Mode Specification](../completed/agentic_orchestrator_execution_mode_spec.md) - Overall architecture for deterministic vs interactive modes
|
|
1245
|
+
- **Related:** [Runtime Inspection Specification](./agentic_orchestrator_runtime_inspection_spec.md) - Dashboard integration for shadow monitoring
|
|
1246
|
+
|
|
1247
|
+
### 12.2 External Resources
|
|
1248
|
+
|
|
1249
|
+
- **Copy-on-Write Filesystems:** [Btrfs reflink](https://btrfs.wiki.kernel.org/index.php/Reflink), [XFS reflink](https://www.kernel.org/doc/Documentation/filesystems/xfs.txt)
|
|
1250
|
+
- **Hardlinks:** [Linux hardlink documentation](https://man7.org/linux/man-pages/man2/link.2.html)
|
|
1251
|
+
- **File System Monitoring:** [chokidar](https://github.com/paulmillr/chokidar)
|
|
1252
|
+
|
|
1253
|
+
### 12.3 Implementation Files
|
|
1254
|
+
|
|
1255
|
+
**New files to create:**
|
|
1256
|
+
|
|
1257
|
+
- `apps/control-plane/src/application/services/shadow-workspace-manager.ts`
|
|
1258
|
+
- `apps/control-plane/src/application/services/shadow-pool.ts`
|
|
1259
|
+
- `apps/control-plane/test/shadow-workspace-manager.spec.ts`
|
|
1260
|
+
- `apps/control-plane/test/shadow-pool.spec.ts`
|
|
1261
|
+
|
|
1262
|
+
**Files to modify:**
|
|
1263
|
+
|
|
1264
|
+
- `apps/control-plane/src/supervisor/worker-decision-loop.ts` - Add shadow mode execution path
|
|
1265
|
+
- `apps/control-plane/src/application/services/checkpoint-service.ts` - Add shadow diff computation
|
|
1266
|
+
- `agentic/orchestrator/schemas/agents.schema.json` - Add shadow configuration
|
|
1267
|
+
- `agentic/orchestrator/schemas/state.schema.json` - Add shadow metadata
|
|
1268
|
+
|
|
1269
|
+
---
|
|
1270
|
+
|
|
1271
|
+
**End of Specification**
|