specweave 0.32.10 → 0.33.3
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/CLAUDE.md +162 -1
- package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts +120 -0
- package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-ado/lib/per-us-sync.js +276 -0
- package/dist/plugins/specweave-ado/lib/per-us-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts +4 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.js +13 -3
- package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
- package/dist/plugins/specweave-github/lib/per-us-sync.d.ts +97 -0
- package/dist/plugins/specweave-github/lib/per-us-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/per-us-sync.js +274 -0
- package/dist/plugins/specweave-github/lib/per-us-sync.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/per-us-sync.d.ts +113 -0
- package/dist/plugins/specweave-jira/lib/per-us-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/per-us-sync.js +254 -0
- package/dist/plugins/specweave-jira/lib/per-us-sync.js.map +1 -0
- package/dist/src/cli/add-child-pid.d.ts +11 -0
- package/dist/src/cli/add-child-pid.d.ts.map +1 -0
- package/dist/src/cli/add-child-pid.js +42 -0
- package/dist/src/cli/add-child-pid.js.map +1 -0
- package/dist/src/cli/add-child-process.d.ts +15 -0
- package/dist/src/cli/add-child-process.d.ts.map +1 -0
- package/dist/src/cli/add-child-process.js +40 -0
- package/dist/src/cli/add-child-process.js.map +1 -0
- package/dist/src/cli/check-watchdog.d.ts +15 -0
- package/dist/src/cli/check-watchdog.d.ts.map +1 -0
- package/dist/src/cli/check-watchdog.js +47 -0
- package/dist/src/cli/check-watchdog.js.map +1 -0
- package/dist/src/cli/cleanup-zombies.d.ts +14 -0
- package/dist/src/cli/cleanup-zombies.d.ts.map +1 -0
- package/dist/src/cli/cleanup-zombies.js +268 -0
- package/dist/src/cli/cleanup-zombies.js.map +1 -0
- package/dist/src/cli/find-session-by-pid.d.ts +14 -0
- package/dist/src/cli/find-session-by-pid.d.ts.map +1 -0
- package/dist/src/cli/find-session-by-pid.js +45 -0
- package/dist/src/cli/find-session-by-pid.js.map +1 -0
- package/dist/src/cli/get-stale-sessions.d.ts +17 -0
- package/dist/src/cli/get-stale-sessions.d.ts.map +1 -0
- package/dist/src/cli/get-stale-sessions.js +36 -0
- package/dist/src/cli/get-stale-sessions.js.map +1 -0
- package/dist/src/cli/register-session.d.ts +16 -0
- package/dist/src/cli/register-session.d.ts.map +1 -0
- package/dist/src/cli/register-session.js +48 -0
- package/dist/src/cli/register-session.js.map +1 -0
- package/dist/src/cli/remove-session.d.ts +11 -0
- package/dist/src/cli/remove-session.d.ts.map +1 -0
- package/dist/src/cli/remove-session.js +36 -0
- package/dist/src/cli/remove-session.js.map +1 -0
- package/dist/src/cli/update-heartbeat.d.ts +11 -0
- package/dist/src/cli/update-heartbeat.d.ts.map +1 -0
- package/dist/src/cli/update-heartbeat.js +36 -0
- package/dist/src/cli/update-heartbeat.js.map +1 -0
- package/dist/src/config/types.d.ts +1208 -203
- package/dist/src/config/types.d.ts.map +1 -1
- package/dist/src/core/background/job-manager.d.ts +16 -0
- package/dist/src/core/background/job-manager.d.ts.map +1 -1
- package/dist/src/core/background/job-manager.js +110 -15
- package/dist/src/core/background/job-manager.js.map +1 -1
- package/dist/src/core/config/config-manager.d.ts.map +1 -1
- package/dist/src/core/config/config-manager.js +58 -0
- package/dist/src/core/config/config-manager.js.map +1 -1
- package/dist/src/core/config/types.d.ts +80 -0
- package/dist/src/core/config/types.d.ts.map +1 -1
- package/dist/src/core/config/types.js.map +1 -1
- package/dist/src/core/increment/increment-utils.d.ts +26 -1
- package/dist/src/core/increment/increment-utils.d.ts.map +1 -1
- package/dist/src/core/increment/increment-utils.js +66 -4
- package/dist/src/core/increment/increment-utils.js.map +1 -1
- package/dist/src/core/increment/status-change-sync-trigger.d.ts +3 -1
- package/dist/src/core/increment/status-change-sync-trigger.d.ts.map +1 -1
- package/dist/src/core/increment/status-change-sync-trigger.js +5 -2
- package/dist/src/core/increment/status-change-sync-trigger.js.map +1 -1
- package/dist/src/core/living-docs/cross-project-sync.d.ts +87 -15
- package/dist/src/core/living-docs/cross-project-sync.d.ts.map +1 -1
- package/dist/src/core/living-docs/cross-project-sync.js +147 -28
- package/dist/src/core/living-docs/cross-project-sync.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.d.ts.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js +48 -12
- package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.d.ts +70 -0
- package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.js +188 -0
- package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.d.ts +33 -0
- package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.js +290 -0
- package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.d.ts.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.js +114 -11
- package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.d.ts +23 -0
- package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.js +283 -0
- package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.d.ts +44 -0
- package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.js +61 -0
- package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.d.ts +126 -0
- package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.js +378 -0
- package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.d.ts.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js +57 -0
- package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.d.ts +82 -0
- package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.js +430 -0
- package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.d.ts +84 -0
- package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.js +387 -0
- package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/report-writer.d.ts +61 -0
- package/dist/src/core/living-docs/intelligent-analyzer/report-writer.d.ts.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/report-writer.js +174 -0
- package/dist/src/core/living-docs/intelligent-analyzer/report-writer.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.js +26 -22
- package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
- package/dist/src/core/living-docs/module-analyzer.d.ts +3 -0
- package/dist/src/core/living-docs/module-analyzer.d.ts.map +1 -1
- package/dist/src/core/living-docs/module-analyzer.js +40 -1
- package/dist/src/core/living-docs/module-analyzer.js.map +1 -1
- package/dist/src/core/living-docs/types.d.ts +24 -3
- package/dist/src/core/living-docs/types.d.ts.map +1 -1
- package/dist/src/core/qa/qa-runner.js +1 -1
- package/dist/src/core/qa/qa-runner.js.map +1 -1
- package/dist/src/core/scheduler/session-sync-executor.js +1 -1
- package/dist/src/core/scheduler/session-sync-executor.js.map +1 -1
- package/dist/src/core/status-line/status-line-updater.d.ts +1 -1
- package/dist/src/core/status-line/status-line-updater.d.ts.map +1 -1
- package/dist/src/core/status-line/status-line-updater.js +4 -3
- package/dist/src/core/status-line/status-line-updater.js.map +1 -1
- package/dist/src/core/types/config.d.ts +79 -0
- package/dist/src/core/types/config.d.ts.map +1 -1
- package/dist/src/core/types/config.js.map +1 -1
- package/dist/src/importers/jira-importer.d.ts.map +1 -1
- package/dist/src/importers/jira-importer.js +18 -9
- package/dist/src/importers/jira-importer.js.map +1 -1
- package/dist/src/init/architecture/types.d.ts +140 -33
- package/dist/src/init/architecture/types.d.ts.map +1 -1
- package/dist/src/init/compliance/types.d.ts +27 -30
- package/dist/src/init/compliance/types.d.ts.map +1 -1
- package/dist/src/init/repo/types.d.ts +34 -11
- package/dist/src/init/repo/types.d.ts.map +1 -1
- package/dist/src/init/research/src/config/types.d.ts +82 -15
- package/dist/src/init/research/src/config/types.d.ts.map +1 -1
- package/dist/src/init/research/types.d.ts +93 -38
- package/dist/src/init/research/types.d.ts.map +1 -1
- package/dist/src/init/team/types.d.ts +42 -4
- package/dist/src/init/team/types.d.ts.map +1 -1
- package/dist/src/sync/ado-reconciler.js +1 -1
- package/dist/src/sync/ado-reconciler.js.map +1 -1
- package/dist/src/sync/github-reconciler.js +1 -1
- package/dist/src/sync/github-reconciler.js.map +1 -1
- package/dist/src/sync/jira-reconciler.js +1 -1
- package/dist/src/sync/jira-reconciler.js.map +1 -1
- package/dist/src/sync/sync-coordinator.d.ts +20 -0
- package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
- package/dist/src/sync/sync-coordinator.js +258 -33
- package/dist/src/sync/sync-coordinator.js.map +1 -1
- package/dist/src/types/session.d.ts +65 -0
- package/dist/src/types/session.d.ts.map +1 -0
- package/dist/src/types/session.js +8 -0
- package/dist/src/types/session.js.map +1 -0
- package/dist/src/utils/lock-manager.d.ts +48 -0
- package/dist/src/utils/lock-manager.d.ts.map +1 -0
- package/dist/src/utils/lock-manager.js +195 -0
- package/dist/src/utils/lock-manager.js.map +1 -0
- package/dist/src/utils/notification-manager.d.ts +45 -0
- package/dist/src/utils/notification-manager.d.ts.map +1 -0
- package/dist/src/utils/notification-manager.js +130 -0
- package/dist/src/utils/notification-manager.js.map +1 -0
- package/dist/src/utils/platform-utils.d.ts +136 -0
- package/dist/src/utils/platform-utils.d.ts.map +1 -0
- package/dist/src/utils/platform-utils.js +366 -0
- package/dist/src/utils/platform-utils.js.map +1 -0
- package/dist/src/utils/project-resolver.d.ts +156 -0
- package/dist/src/utils/project-resolver.d.ts.map +1 -0
- package/dist/src/utils/project-resolver.js +587 -0
- package/dist/src/utils/project-resolver.js.map +1 -0
- package/dist/src/utils/session-registry.d.ts +142 -0
- package/dist/src/utils/session-registry.d.ts.map +1 -0
- package/dist/src/utils/session-registry.js +480 -0
- package/dist/src/utils/session-registry.js.map +1 -0
- package/package.json +5 -2
- package/plugins/specweave/commands/specweave-living-docs.md +42 -0
- package/plugins/specweave/hooks/hooks.json +20 -0
- package/plugins/specweave/hooks/lib/update-active-increment.sh +2 -2
- package/plugins/specweave/hooks/lib/update-status-line.sh +1 -1
- package/plugins/specweave/hooks/post-increment-status-change.sh +3 -3
- package/plugins/specweave/hooks/post-metadata-change.sh +1 -1
- package/plugins/specweave/hooks/universal/hook-wrapper.cmd +26 -26
- package/plugins/specweave/hooks/universal/session-start.cmd +16 -16
- package/plugins/specweave/hooks/universal/session-start.ps1 +16 -16
- package/plugins/specweave/hooks/user-prompt-submit.sh +107 -5
- package/plugins/specweave/hooks/v2/guards/increment-root-guard.sh +61 -0
- package/plugins/specweave/hooks/v2/guards/per-us-project-validator.sh +281 -0
- package/plugins/specweave/hooks/v2/handlers/living-specs-handler.sh +29 -0
- package/plugins/specweave/hooks/v2/session-end.sh +69 -0
- package/plugins/specweave/hooks/v2/session-start.sh +81 -0
- package/plugins/specweave/lib/vendor/sync/github-reconciler.js +1 -1
- package/plugins/specweave/lib/vendor/sync/github-reconciler.js.map +1 -1
- package/plugins/specweave/scripts/heartbeat.sh +110 -0
- package/plugins/specweave/scripts/progress.js +34 -4
- package/plugins/specweave/scripts/read-jobs.sh +1 -1
- package/plugins/specweave/scripts/read-progress.sh +50 -5
- package/plugins/specweave/scripts/read-workflow.sh +1 -1
- package/plugins/specweave/scripts/session-watchdog.sh +65 -0
- package/plugins/specweave/scripts/status.js +28 -11
- package/plugins/specweave-ado/lib/per-us-sync.js +247 -0
- package/plugins/specweave-ado/lib/per-us-sync.ts +410 -0
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +738 -0
- package/plugins/specweave-github/lib/github-client-v2.js +10 -3
- package/plugins/specweave-github/lib/github-client-v2.ts +15 -3
- package/plugins/specweave-github/lib/per-us-sync.js +241 -0
- package/plugins/specweave-github/lib/per-us-sync.ts +375 -0
- package/plugins/specweave-jira/lib/per-us-sync.js +224 -0
- package/plugins/specweave-jira/lib/per-us-sync.ts +366 -0
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +1107 -0
|
@@ -118,11 +118,24 @@ if (specificId) {
|
|
|
118
118
|
|
|
119
119
|
// Show all active increments
|
|
120
120
|
const increments = incrementFolders.map(parseIncrement);
|
|
121
|
-
const
|
|
121
|
+
const readyForReview = increments.filter(i => i.status === 'ready_for_review');
|
|
122
|
+
const active = increments.filter(i => ['active', 'planning', 'backlog'].includes(i.status));
|
|
122
123
|
const paused = increments.filter(i => i.status === 'paused');
|
|
123
124
|
|
|
124
125
|
console.log('\n📊 Increment Progress\n');
|
|
125
126
|
|
|
127
|
+
// Show ready_for_review FIRST (needs attention!)
|
|
128
|
+
if (readyForReview.length > 0) {
|
|
129
|
+
console.log(`👀 Ready for Review (${readyForReview.length}):`);
|
|
130
|
+
for (const inc of readyForReview) {
|
|
131
|
+
const bar = createProgressBar(inc.percentage, 15);
|
|
132
|
+
console.log(` ${inc.id}`);
|
|
133
|
+
console.log(` ${bar} ${inc.completedTasks}/${inc.totalTasks} (${inc.percentage}%)`);
|
|
134
|
+
console.log(` → /specweave:done ${inc.id}`);
|
|
135
|
+
}
|
|
136
|
+
console.log('');
|
|
137
|
+
}
|
|
138
|
+
|
|
126
139
|
if (active.length > 0) {
|
|
127
140
|
console.log(`🔄 Active (${active.length}):`);
|
|
128
141
|
for (const inc of active) {
|
|
@@ -141,16 +154,31 @@ if (paused.length > 0) {
|
|
|
141
154
|
console.log('');
|
|
142
155
|
}
|
|
143
156
|
|
|
144
|
-
|
|
157
|
+
// Summary section
|
|
158
|
+
if (readyForReview.length > 0 || active.length > 0 || paused.length > 0) {
|
|
159
|
+
console.log('─'.repeat(40));
|
|
160
|
+
const parts = [];
|
|
161
|
+
if (readyForReview.length > 0) parts.push(`${readyForReview.length} ready for review`);
|
|
162
|
+
if (active.length > 0) parts.push(`${active.length} active`);
|
|
163
|
+
if (paused.length > 0) parts.push(`${paused.length} paused`);
|
|
164
|
+
console.log(`Summary: ${parts.join(', ')}`);
|
|
165
|
+
console.log('');
|
|
166
|
+
|
|
167
|
+
if (readyForReview.length > 0) {
|
|
168
|
+
console.log('💡 Run /specweave:done <id> to close reviewed increments');
|
|
169
|
+
} else {
|
|
170
|
+
console.log('💡 For details: /specweave:progress <incrementId>');
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
145
173
|
console.log('No active increments.');
|
|
146
174
|
const completed = increments.filter(i => i.status === 'completed');
|
|
147
175
|
if (completed.length > 0) {
|
|
148
176
|
console.log(`${completed.length} completed increment(s).`);
|
|
149
177
|
}
|
|
178
|
+
console.log('');
|
|
179
|
+
console.log('💡 Run /specweave:increment to start new work');
|
|
150
180
|
}
|
|
151
181
|
|
|
152
|
-
console.log('💡 For details: /specweave:progress <incrementId>');
|
|
153
|
-
|
|
154
182
|
// Helpers
|
|
155
183
|
function createProgressBar(pct, width = 20) {
|
|
156
184
|
const filled = Math.round((pct / 100) * width);
|
|
@@ -162,6 +190,8 @@ function formatStatus(status) {
|
|
|
162
190
|
const icons = {
|
|
163
191
|
'active': '🔄 active',
|
|
164
192
|
'planning': '📝 planning',
|
|
193
|
+
'backlog': '📋 backlog',
|
|
194
|
+
'ready_for_review': '👀 ready for review',
|
|
165
195
|
'paused': '⏸️ paused',
|
|
166
196
|
'completed': '✅ completed',
|
|
167
197
|
'abandoned': '❌ abandoned'
|
|
@@ -110,7 +110,7 @@ if [[ -f "$CACHE_FILE" ]] && jq -e '.' "$CACHE_FILE" >/dev/null 2>&1; then
|
|
|
110
110
|
# Show active increments
|
|
111
111
|
ACTIVE=$(jq -r '
|
|
112
112
|
.increments | to_entries[] |
|
|
113
|
-
select(.value.status == "active" or .value.status == "planning" or .value.status == "
|
|
113
|
+
select(.value.status == "active" or .value.status == "planning" or .value.status == "backlog" or .value.status == "ready_for_review") |
|
|
114
114
|
"\(.key)|\(.value.status)|\(.value.tasks.completed)|\(.value.tasks.total)"
|
|
115
115
|
' "$CACHE_FILE" 2>/dev/null)
|
|
116
116
|
|
|
@@ -126,10 +126,36 @@ echo ""
|
|
|
126
126
|
echo "📊 Increment Progress"
|
|
127
127
|
echo ""
|
|
128
128
|
|
|
129
|
-
# Get
|
|
129
|
+
# Get ready_for_review increments FIRST (needs attention!)
|
|
130
|
+
READY_FOR_REVIEW=$(jq -r '
|
|
131
|
+
.increments | to_entries[] |
|
|
132
|
+
select(.value.status == "ready_for_review") |
|
|
133
|
+
"\(.key)|\(.value.tasks.completed)|\(.value.tasks.total)"
|
|
134
|
+
' "$CACHE_FILE" 2>/dev/null)
|
|
135
|
+
|
|
136
|
+
REVIEW_COUNT=0
|
|
137
|
+
if [[ -n "$READY_FOR_REVIEW" ]]; then
|
|
138
|
+
echo "👀 Ready for Review:"
|
|
139
|
+
while IFS='|' read -r id completed total; do
|
|
140
|
+
[[ -z "$id" ]] && continue
|
|
141
|
+
REVIEW_COUNT=$((REVIEW_COUNT + 1))
|
|
142
|
+
if [[ "$total" -gt 0 ]]; then
|
|
143
|
+
pct=$((completed * 100 / total))
|
|
144
|
+
else
|
|
145
|
+
pct=0
|
|
146
|
+
fi
|
|
147
|
+
bar=$(progress_bar "$pct" 15)
|
|
148
|
+
echo " $id"
|
|
149
|
+
echo " $bar $completed/$total ($pct%)"
|
|
150
|
+
echo " → /specweave:done $id"
|
|
151
|
+
done <<< "$READY_FOR_REVIEW"
|
|
152
|
+
echo ""
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
# Get active increments (excluding ready_for_review)
|
|
130
156
|
ACTIVE=$(jq -r '
|
|
131
157
|
.increments | to_entries[] |
|
|
132
|
-
select(.value.status == "active" or .value.status == "planning" or .value.status == "
|
|
158
|
+
select(.value.status == "active" or .value.status == "planning" or .value.status == "backlog") |
|
|
133
159
|
"\(.key)|\(.value.tasks.completed)|\(.value.tasks.total)"
|
|
134
160
|
' "$CACHE_FILE" 2>/dev/null)
|
|
135
161
|
|
|
@@ -173,8 +199,20 @@ if [[ -n "$PAUSED" ]]; then
|
|
|
173
199
|
echo ""
|
|
174
200
|
fi
|
|
175
201
|
|
|
176
|
-
#
|
|
177
|
-
if [[ "$ACTIVE_COUNT" -
|
|
202
|
+
# Summary section
|
|
203
|
+
if [[ "$REVIEW_COUNT" -gt 0 ]] || [[ "$ACTIVE_COUNT" -gt 0 ]] || [[ -n "$PAUSED" ]]; then
|
|
204
|
+
# Has work - show summary
|
|
205
|
+
echo "────────────────────────────────────────"
|
|
206
|
+
SUMMARY_PARTS=()
|
|
207
|
+
[[ "$REVIEW_COUNT" -gt 0 ]] && SUMMARY_PARTS+=("$REVIEW_COUNT ready for review")
|
|
208
|
+
[[ "$ACTIVE_COUNT" -gt 0 ]] && SUMMARY_PARTS+=("$ACTIVE_COUNT active")
|
|
209
|
+
PAUSED_COUNT=0
|
|
210
|
+
[[ -n "$PAUSED" ]] && PAUSED_COUNT=$(echo "$PAUSED" | grep -c '|' || echo 0)
|
|
211
|
+
[[ "$PAUSED_COUNT" -gt 0 ]] && SUMMARY_PARTS+=("$PAUSED_COUNT paused")
|
|
212
|
+
|
|
213
|
+
IFS=', '; echo "Summary: ${SUMMARY_PARTS[*]}"
|
|
214
|
+
else
|
|
215
|
+
# No work in progress
|
|
178
216
|
echo "No active increments."
|
|
179
217
|
COMPLETED_COUNT=$(jq '.summary.completed // 0' "$CACHE_FILE")
|
|
180
218
|
if [[ "$COMPLETED_COUNT" -gt 0 ]]; then
|
|
@@ -182,4 +220,11 @@ if [[ "$ACTIVE_COUNT" -eq 0 ]] && [[ -z "$PAUSED" ]]; then
|
|
|
182
220
|
fi
|
|
183
221
|
fi
|
|
184
222
|
|
|
185
|
-
echo "
|
|
223
|
+
echo ""
|
|
224
|
+
if [[ "$REVIEW_COUNT" -gt 0 ]]; then
|
|
225
|
+
echo "💡 Run /specweave:done <id> to close reviewed increments"
|
|
226
|
+
elif [[ "$ACTIVE_COUNT" -eq 0 ]]; then
|
|
227
|
+
echo "💡 Run /specweave:increment to start new work"
|
|
228
|
+
else
|
|
229
|
+
echo "💡 For details: /specweave:progress <incrementId>"
|
|
230
|
+
fi
|
|
@@ -64,7 +64,7 @@ else
|
|
|
64
64
|
# Find first active increment
|
|
65
65
|
ACTIVE_ID=$(jq -r '
|
|
66
66
|
.increments | to_entries[] |
|
|
67
|
-
select(.value.status == "active" or .value.status == "
|
|
67
|
+
select(.value.status == "active" or .value.status == "planning" or .value.status == "backlog" or .value.status == "ready_for_review") |
|
|
68
68
|
.key
|
|
69
69
|
' "$CACHE_FILE" 2>/dev/null | head -1)
|
|
70
70
|
fi
|
|
@@ -12,6 +12,9 @@ SPECWEAVE_ROOT="${SPECWEAVE_ROOT:-.specweave}"
|
|
|
12
12
|
SIGNAL_FILE="${SPECWEAVE_ROOT}/state/.session-stuck"
|
|
13
13
|
HEARTBEAT_FILE="${SPECWEAVE_ROOT}/state/.heartbeat"
|
|
14
14
|
DAEMON_MODE=false
|
|
15
|
+
PROJECT_ROOT="${PWD}"
|
|
16
|
+
SESSION_ID="watchdog-$$-$(date +%s)"
|
|
17
|
+
COORDINATION_THRESHOLD=30 # Heartbeat threshold for active watchdog detection
|
|
15
18
|
|
|
16
19
|
# Colors
|
|
17
20
|
RED='\033[0;31m'
|
|
@@ -176,15 +179,77 @@ check_session_health() {
|
|
|
176
179
|
fi
|
|
177
180
|
}
|
|
178
181
|
|
|
182
|
+
# Coordination Functions
|
|
183
|
+
check_active_watchdog() {
|
|
184
|
+
# Check if another watchdog is running via session registry
|
|
185
|
+
node "${PROJECT_ROOT}/dist/src/cli/check-watchdog.js" 2>/dev/null || echo ""
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
register_watchdog() {
|
|
189
|
+
# Register this watchdog in session registry
|
|
190
|
+
node "${PROJECT_ROOT}/dist/src/cli/register-session.js" "$SESSION_ID" $$ "watchdog" 2>&1 | \
|
|
191
|
+
grep -v "^$" | head -3
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
update_watchdog_heartbeat() {
|
|
195
|
+
# Update watchdog heartbeat
|
|
196
|
+
node "${PROJECT_ROOT}/dist/src/cli/update-heartbeat.js" "$SESSION_ID" 2>/dev/null || true
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
cleanup_watchdog() {
|
|
200
|
+
# Remove watchdog from registry on exit
|
|
201
|
+
node "${PROJECT_ROOT}/dist/src/cli/remove-session.js" "$SESSION_ID" 2>&1 | \
|
|
202
|
+
grep -v "^$" | head -3
|
|
203
|
+
log "Watchdog cleanup complete"
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
run_cleanup_service() {
|
|
207
|
+
# Run zombie process cleanup
|
|
208
|
+
node "${PROJECT_ROOT}/dist/src/cli/cleanup-zombies.js" 60 2>&1 | \
|
|
209
|
+
grep -v "^$" | head -10 || true
|
|
210
|
+
}
|
|
211
|
+
|
|
179
212
|
# Main execution
|
|
180
213
|
if [[ "$DAEMON_MODE" == "true" ]]; then
|
|
214
|
+
# Coordination check before starting daemon
|
|
215
|
+
active_watchdog=$(check_active_watchdog)
|
|
216
|
+
|
|
217
|
+
if [[ -n "$active_watchdog" ]]; then
|
|
218
|
+
log "${YELLOW}Watchdog already active (PID: $active_watchdog)${NC}"
|
|
219
|
+
log "Exiting to avoid duplicate watchdogs"
|
|
220
|
+
exit 0
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
# Register as watchdog
|
|
224
|
+
register_watchdog
|
|
225
|
+
|
|
226
|
+
# Trap signals for graceful shutdown
|
|
227
|
+
trap cleanup_watchdog SIGTERM SIGINT EXIT
|
|
228
|
+
|
|
181
229
|
log "Starting session watchdog daemon (interval: ${CHECK_INTERVAL}s, threshold: ${STUCK_THRESHOLD_SECONDS}s)"
|
|
230
|
+
log "Watchdog session: $SESSION_ID (PID: $$)"
|
|
182
231
|
log "Press Ctrl+C to stop"
|
|
183
232
|
|
|
184
233
|
while true; do
|
|
234
|
+
# Update own heartbeat
|
|
235
|
+
update_watchdog_heartbeat
|
|
236
|
+
|
|
237
|
+
# Check session health
|
|
185
238
|
check_session_health || true
|
|
239
|
+
|
|
240
|
+
# Run cleanup service
|
|
241
|
+
run_cleanup_service
|
|
242
|
+
|
|
243
|
+
# Check if parent process still exists (if we have a parent session)
|
|
244
|
+
if ! kill -0 $PPID 2>/dev/null; then
|
|
245
|
+
log "${YELLOW}Parent process died, exiting watchdog${NC}"
|
|
246
|
+
break
|
|
247
|
+
fi
|
|
248
|
+
|
|
186
249
|
sleep "$CHECK_INTERVAL"
|
|
187
250
|
done
|
|
251
|
+
|
|
252
|
+
cleanup_watchdog
|
|
188
253
|
else
|
|
189
254
|
log "Running single health check..."
|
|
190
255
|
check_session_health
|
|
@@ -86,7 +86,7 @@ for (const folder of incrementFolders) {
|
|
|
86
86
|
// Display
|
|
87
87
|
console.log('\n📋 SpecWeave Status Overview\n');
|
|
88
88
|
|
|
89
|
-
const statusOrder = ['active', 'planning', 'in-progress', '
|
|
89
|
+
const statusOrder = ['ready_for_review', 'active', 'planning', 'in-progress', 'paused', 'backlog', 'completed', 'abandoned'];
|
|
90
90
|
const statusIcons = {
|
|
91
91
|
'active': '🔄',
|
|
92
92
|
'planning': '📝',
|
|
@@ -105,12 +105,16 @@ for (const status of statusOrder) {
|
|
|
105
105
|
if (items && items.length > 0) {
|
|
106
106
|
hasOutput = true;
|
|
107
107
|
const icon = statusIcons[status] || '•';
|
|
108
|
-
console.log(`${icon} ${status.replace(
|
|
109
|
-
|
|
108
|
+
console.log(`${icon} ${status.replace(/_/g, ' ')} (${items.length}):`);
|
|
109
|
+
|
|
110
110
|
// Show first 5, summarize rest
|
|
111
111
|
const toShow = items.slice(0, 5);
|
|
112
112
|
for (const item of toShow) {
|
|
113
113
|
console.log(` ${item}`);
|
|
114
|
+
// Add action hint for ready_for_review
|
|
115
|
+
if (status === 'ready_for_review') {
|
|
116
|
+
console.log(` → /specweave:done ${item}`);
|
|
117
|
+
}
|
|
114
118
|
}
|
|
115
119
|
if (items.length > 5) {
|
|
116
120
|
console.log(` ... and ${items.length - 5} more`);
|
|
@@ -140,15 +144,28 @@ if (!hasOutput) {
|
|
|
140
144
|
|
|
141
145
|
// Summary line
|
|
142
146
|
const total = incrementFolders.length;
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
(byStatus['
|
|
147
|
+
const reviewCount = byStatus['ready_for_review']?.length || 0;
|
|
148
|
+
const activeCount = (byStatus['active']?.length || 0) +
|
|
149
|
+
(byStatus['planning']?.length || 0) +
|
|
150
|
+
(byStatus['backlog']?.length || 0);
|
|
146
151
|
const completedCount = byStatus['completed']?.length || 0;
|
|
147
152
|
|
|
148
153
|
console.log('─'.repeat(40));
|
|
149
|
-
|
|
154
|
+
const summaryParts = [`Total: ${total}`];
|
|
155
|
+
if (reviewCount > 0) summaryParts.push(`Review: ${reviewCount}`);
|
|
156
|
+
summaryParts.push(`Active: ${activeCount}`);
|
|
157
|
+
summaryParts.push(`Completed: ${completedCount}`);
|
|
158
|
+
if (archivedCount > 0) summaryParts.push(`Archived: ${archivedCount}`);
|
|
159
|
+
console.log(summaryParts.join(' | '));
|
|
150
160
|
console.log('');
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
console.log('
|
|
161
|
+
|
|
162
|
+
// Context-aware command hints
|
|
163
|
+
if (reviewCount > 0) {
|
|
164
|
+
console.log('💡 Next step: /specweave:done <id> to close reviewed increment(s)');
|
|
165
|
+
} else if (activeCount > 0) {
|
|
166
|
+
console.log('💡 Commands:');
|
|
167
|
+
console.log(' /specweave:progress Show task progress');
|
|
168
|
+
console.log(' /specweave:do Execute current tasks');
|
|
169
|
+
} else {
|
|
170
|
+
console.log('💡 Run /specweave:increment to start new work');
|
|
171
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { consoleLogger } from "../../../src/utils/logger.js";
|
|
2
|
+
class PerUSAdoSync {
|
|
3
|
+
constructor(adoClient, projectMappings, options = {}) {
|
|
4
|
+
this.adoClient = adoClient;
|
|
5
|
+
this.projectMappings = projectMappings;
|
|
6
|
+
this.logger = options.logger ?? consoleLogger;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Sync all user stories to their respective ADO projects
|
|
10
|
+
*
|
|
11
|
+
* @param userStories - User stories with explicit project/board fields
|
|
12
|
+
* @param featureId - Feature ID (e.g., "FS-137")
|
|
13
|
+
* @param options - Sync options
|
|
14
|
+
*/
|
|
15
|
+
async syncUserStories(userStories, featureId, options = {}) {
|
|
16
|
+
const synced = [];
|
|
17
|
+
const failed = [];
|
|
18
|
+
const externalRefs = {};
|
|
19
|
+
const groups = this.groupByProject(userStories, options.defaultProject);
|
|
20
|
+
this.logger.log(`\u{1F4E1} Per-US ADO Sync: ${userStories.length} USs across ${groups.size} projects`);
|
|
21
|
+
for (const [projectId, stories] of groups) {
|
|
22
|
+
const mapping = this.projectMappings[projectId]?.ado;
|
|
23
|
+
if (!mapping) {
|
|
24
|
+
this.logger.warn(` \u26A0\uFE0F No ADO mapping for project "${projectId}" - skipping ${stories.length} USs`);
|
|
25
|
+
for (const story of stories) {
|
|
26
|
+
failed.push({
|
|
27
|
+
usId: story.id,
|
|
28
|
+
projectId,
|
|
29
|
+
adoProject: "N/A",
|
|
30
|
+
areaPath: "",
|
|
31
|
+
workItemId: 0,
|
|
32
|
+
url: "",
|
|
33
|
+
action: "skipped",
|
|
34
|
+
error: `No ADO mapping for project "${projectId}"`
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
for (const story of stories) {
|
|
40
|
+
try {
|
|
41
|
+
const result = await this.syncUserStory(story, mapping, featureId, options);
|
|
42
|
+
synced.push({
|
|
43
|
+
...result,
|
|
44
|
+
projectId
|
|
45
|
+
});
|
|
46
|
+
if (!options.dryRun && result.action !== "skipped") {
|
|
47
|
+
externalRefs[story.id] = {
|
|
48
|
+
ado: {
|
|
49
|
+
provider: "ado",
|
|
50
|
+
issueNumber: result.workItemId,
|
|
51
|
+
url: result.url,
|
|
52
|
+
targetProject: projectId,
|
|
53
|
+
lastSynced: (/* @__PURE__ */ new Date()).toISOString()
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
failed.push({
|
|
59
|
+
usId: story.id,
|
|
60
|
+
projectId,
|
|
61
|
+
adoProject: mapping.project,
|
|
62
|
+
areaPath: mapping.areaPath || "",
|
|
63
|
+
workItemId: 0,
|
|
64
|
+
url: "",
|
|
65
|
+
action: "skipped",
|
|
66
|
+
error: error instanceof Error ? error.message : String(error)
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const created = synced.filter((r) => r.action === "created").length;
|
|
72
|
+
const updated = synced.filter((r) => r.action === "updated").length;
|
|
73
|
+
const skipped = synced.filter((r) => r.action === "skipped").length;
|
|
74
|
+
return {
|
|
75
|
+
success: failed.length === 0,
|
|
76
|
+
synced,
|
|
77
|
+
failed,
|
|
78
|
+
externalRefs,
|
|
79
|
+
summary: {
|
|
80
|
+
total: userStories.length,
|
|
81
|
+
created,
|
|
82
|
+
updated,
|
|
83
|
+
skipped,
|
|
84
|
+
failed: failed.length
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Sync a single user story to ADO
|
|
90
|
+
*
|
|
91
|
+
* For 2-level structures:
|
|
92
|
+
* - **Project**: maps to ADO project
|
|
93
|
+
* - **Board**: maps to area path under the project
|
|
94
|
+
*/
|
|
95
|
+
async syncUserStory(story, mapping, featureId, options) {
|
|
96
|
+
const title = `[${featureId}][${story.id}] ${story.title}`;
|
|
97
|
+
const description = this.buildWorkItemDescription(story, featureId);
|
|
98
|
+
let areaPath = mapping.areaPath || mapping.project;
|
|
99
|
+
if (story.board) {
|
|
100
|
+
areaPath = `${mapping.project}\\${story.board}`;
|
|
101
|
+
}
|
|
102
|
+
if (options.dryRun) {
|
|
103
|
+
this.logger.log(` \u{1F50D} [DRY-RUN] Would sync ${story.id} to ${mapping.project} (area: ${areaPath})`);
|
|
104
|
+
return {
|
|
105
|
+
usId: story.id,
|
|
106
|
+
projectId: story.project || "unknown",
|
|
107
|
+
adoProject: mapping.project,
|
|
108
|
+
areaPath,
|
|
109
|
+
workItemId: 0,
|
|
110
|
+
url: "",
|
|
111
|
+
action: "skipped"
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const existingItem = await this.findExistingWorkItem(mapping.project, story.id);
|
|
115
|
+
if (existingItem) {
|
|
116
|
+
await this.adoClient.updateWorkItem(
|
|
117
|
+
mapping.project,
|
|
118
|
+
existingItem.id,
|
|
119
|
+
title,
|
|
120
|
+
description,
|
|
121
|
+
areaPath
|
|
122
|
+
);
|
|
123
|
+
this.logger.log(` \u{1F504} Updated ${story.id} \u2192 ${mapping.project}/${existingItem.id}`);
|
|
124
|
+
return {
|
|
125
|
+
usId: story.id,
|
|
126
|
+
projectId: story.project || "unknown",
|
|
127
|
+
adoProject: mapping.project,
|
|
128
|
+
areaPath,
|
|
129
|
+
workItemId: existingItem.id,
|
|
130
|
+
url: this.adoClient.getWorkItemUrl(mapping.project, existingItem.id),
|
|
131
|
+
action: "updated"
|
|
132
|
+
};
|
|
133
|
+
} else {
|
|
134
|
+
const newItem = await this.adoClient.createWorkItem(
|
|
135
|
+
mapping.project,
|
|
136
|
+
"User Story",
|
|
137
|
+
title,
|
|
138
|
+
description,
|
|
139
|
+
areaPath
|
|
140
|
+
);
|
|
141
|
+
this.logger.log(` \u2705 Created ${story.id} \u2192 ${mapping.project}/${newItem.id}`);
|
|
142
|
+
return {
|
|
143
|
+
usId: story.id,
|
|
144
|
+
projectId: story.project || "unknown",
|
|
145
|
+
adoProject: mapping.project,
|
|
146
|
+
areaPath,
|
|
147
|
+
workItemId: newItem.id,
|
|
148
|
+
url: newItem.url,
|
|
149
|
+
action: "created"
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Find existing work item by US ID in title
|
|
155
|
+
*/
|
|
156
|
+
async findExistingWorkItem(project, usId) {
|
|
157
|
+
try {
|
|
158
|
+
const query = `[System.Title] Contains '[${usId}]'`;
|
|
159
|
+
const results = await this.adoClient.searchWorkItems(project, query);
|
|
160
|
+
return results.length > 0 ? { id: results[0].id } : null;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Build work item description from user story
|
|
167
|
+
*/
|
|
168
|
+
buildWorkItemDescription(story, featureId) {
|
|
169
|
+
const lines = [];
|
|
170
|
+
lines.push(`<h1>${story.title}</h1>`);
|
|
171
|
+
lines.push("");
|
|
172
|
+
if (story.description) {
|
|
173
|
+
lines.push(`<p>${story.description}</p>`);
|
|
174
|
+
lines.push("");
|
|
175
|
+
}
|
|
176
|
+
if (story.acceptanceCriteria && story.acceptanceCriteria.length > 0) {
|
|
177
|
+
lines.push("<h2>Acceptance Criteria</h2>");
|
|
178
|
+
lines.push("<ul>");
|
|
179
|
+
for (const ac of story.acceptanceCriteria) {
|
|
180
|
+
lines.push(` <li>${ac}</li>`);
|
|
181
|
+
}
|
|
182
|
+
lines.push("</ul>");
|
|
183
|
+
lines.push("");
|
|
184
|
+
}
|
|
185
|
+
lines.push("<hr/>");
|
|
186
|
+
lines.push("");
|
|
187
|
+
lines.push(`<p><strong>Feature</strong>: ${featureId}</p>`);
|
|
188
|
+
lines.push(`<p><strong>User Story</strong>: ${story.id}</p>`);
|
|
189
|
+
if (story.project) {
|
|
190
|
+
lines.push(`<p><strong>Project</strong>: ${story.project}</p>`);
|
|
191
|
+
}
|
|
192
|
+
if (story.board) {
|
|
193
|
+
lines.push(`<p><strong>Board</strong>: ${story.board}</p>`);
|
|
194
|
+
}
|
|
195
|
+
lines.push("");
|
|
196
|
+
lines.push("<p><em>Auto-generated by SpecWeave</em></p>");
|
|
197
|
+
return lines.join("\n");
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Group user stories by their explicit project field
|
|
201
|
+
*/
|
|
202
|
+
groupByProject(userStories, defaultProject) {
|
|
203
|
+
const groups = /* @__PURE__ */ new Map();
|
|
204
|
+
for (const story of userStories) {
|
|
205
|
+
const project = story.project || defaultProject || "default";
|
|
206
|
+
if (!groups.has(project)) {
|
|
207
|
+
groups.set(project, []);
|
|
208
|
+
}
|
|
209
|
+
groups.get(project).push(story);
|
|
210
|
+
}
|
|
211
|
+
return groups;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function formatPerUSSyncResults(result) {
|
|
215
|
+
const lines = [];
|
|
216
|
+
lines.push("");
|
|
217
|
+
lines.push("\u{1F4CA} Per-US ADO Sync Results");
|
|
218
|
+
lines.push("");
|
|
219
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
220
|
+
for (const r of [...result.synced, ...result.failed]) {
|
|
221
|
+
const existing = byProject.get(r.projectId) || [];
|
|
222
|
+
existing.push(r);
|
|
223
|
+
byProject.set(r.projectId, existing);
|
|
224
|
+
}
|
|
225
|
+
for (const [projectId, results] of byProject) {
|
|
226
|
+
const adoProject = results[0]?.adoProject || "N/A";
|
|
227
|
+
const areaPath = results[0]?.areaPath || "";
|
|
228
|
+
lines.push(`**${projectId}** (\u2192 ${adoProject}${areaPath ? ` [${areaPath}]` : ""}):`);
|
|
229
|
+
for (const r of results) {
|
|
230
|
+
const icon = r.action === "created" ? "\u2705" : r.action === "updated" ? "\u{1F504}" : r.error ? "\u274C" : "\u23ED\uFE0F";
|
|
231
|
+
if (r.workItemId > 0) {
|
|
232
|
+
lines.push(` ${icon} ${r.usId} \u2192 ${r.adoProject}/${r.workItemId}`);
|
|
233
|
+
} else if (r.error) {
|
|
234
|
+
lines.push(` ${icon} ${r.usId}: ${r.error}`);
|
|
235
|
+
} else {
|
|
236
|
+
lines.push(` ${icon} ${r.usId} (${r.action})`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
lines.push("");
|
|
240
|
+
}
|
|
241
|
+
lines.push(`\u{1F4C8} Summary: ${result.summary.created} created, ${result.summary.updated} updated, ${result.summary.failed} failed`);
|
|
242
|
+
return lines.join("\n");
|
|
243
|
+
}
|
|
244
|
+
export {
|
|
245
|
+
PerUSAdoSync,
|
|
246
|
+
formatPerUSSyncResults
|
|
247
|
+
};
|