@yemi33/minions 0.1.2147 → 0.1.2149
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/bin/minions.js +8 -0
- package/dashboard/js/settings.js +9 -0
- package/dashboard/slim/body.html +2 -1
- package/dashboard/slim/js/command-send.js +3 -2
- package/dashboard/slim/js/history.js +9 -2
- package/dashboard/slim/styles.css +88 -10
- package/dashboard.js +5 -0
- package/docs/README.md +1 -0
- package/docs/completion-reports.md +44 -12
- package/docs/project-skills.md +133 -0
- package/engine/ado.js +19 -0
- package/engine/discover-project-skills.js +490 -0
- package/engine/discover-review-skills.js +15 -269
- package/engine/dispatch.js +2 -0
- package/engine/github.js +14 -0
- package/engine/lifecycle.js +17 -0
- package/engine/playbook-intents.js +76 -0
- package/engine/playbook.js +59 -20
- package/engine/shared.js +288 -0
- package/engine/worktree-gc.js +241 -21
- package/engine.js +294 -32
- package/package.json +1 -1
- package/playbooks/fix.md +4 -0
- package/playbooks/implement-shared.md +4 -0
- package/playbooks/implement.md +4 -0
- package/playbooks/plan-to-prd.md +4 -0
- package/playbooks/plan.md +4 -0
- package/playbooks/review.md +3 -3
- package/bin/minions.js.rej +0 -16
- package/dashboard/slim/body.html.rej +0 -11
- package/dashboard/slim/js/command-send.js.rej +0 -12
- package/dashboard/slim/js/history.js.rej +0 -26
- package/dashboard/slim/styles.css.rej +0 -124
- package/docs/README.md.rej +0 -9
- package/docs/onboarding.md.rej +0 -10
package/engine/worktree-gc.js
CHANGED
|
@@ -79,18 +79,60 @@ function _markStuckSuccess(resolvedPath, opts = {}) {
|
|
|
79
79
|
try {
|
|
80
80
|
const writer = typeof opts.writeToInbox === 'function' ? opts.writeToInbox : shared.writeToInbox;
|
|
81
81
|
const basename = path.basename(resolvedPath);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
// W-mq6f2fe0000557fa — when the recovery is the direct result of an
|
|
83
|
+
// auto-reap kill, write a distinct slug + body so operators can see
|
|
84
|
+
// which orphan-sweep escalations were resolved by the engine itself
|
|
85
|
+
// vs. naturally by an external holder release.
|
|
86
|
+
const viaReap = opts.recoveryReason === 'holder-reap';
|
|
87
|
+
const reapedPids = Array.isArray(opts.reapedPids) ? opts.reapedPids : [];
|
|
88
|
+
const slug = viaReap
|
|
89
|
+
? `worktree-recovered-${basename}-via-holder-reap`
|
|
90
|
+
: `worktree-recovered-${basename}`;
|
|
91
|
+
const bodyLines = viaReap
|
|
92
|
+
? [
|
|
93
|
+
`# Worktree recovered via holder-reap: ${basename}`,
|
|
94
|
+
'',
|
|
95
|
+
`Previously stuck worktree dir **${resolvedPath}** was successfully removed`,
|
|
96
|
+
`after the engine auto-killed the orphan spawn-agent holder(s)`,
|
|
97
|
+
`${reapedPids.length > 0 ? `(PID${reapedPids.length > 1 ? 's' : ''} ${reapedPids.join(', ')})` : ''}`.trim(),
|
|
98
|
+
`under the \`engine.autoReapOrphanWorktreeHolders\` flag (W-mq6f2fe0000557fa).`,
|
|
99
|
+
`Suppression cleared.`,
|
|
100
|
+
]
|
|
101
|
+
: [
|
|
102
|
+
`# Worktree recovered: ${basename}`,
|
|
103
|
+
'',
|
|
104
|
+
`Previously stuck worktree dir **${resolvedPath}** has been successfully removed`,
|
|
105
|
+
`after escalation. Suppression cleared.`,
|
|
106
|
+
];
|
|
107
|
+
writer('engine', slug, bodyLines.join('\n'));
|
|
89
108
|
} catch { /* note best-effort */ }
|
|
90
109
|
}
|
|
91
110
|
return wasEscalated;
|
|
92
111
|
}
|
|
93
112
|
|
|
113
|
+
// W-mq6f2fe0000557fa — gate: is this holder an unambiguous orphan spawn-agent
|
|
114
|
+
// process we can safely kill? Requires ALL of:
|
|
115
|
+
// (a) cmdline contains 'spawn-agent.js' (only minions agent wrappers qualify)
|
|
116
|
+
// (b) cmdline references the worktree basename (extra confirmation it's
|
|
117
|
+
// OUR spawn-agent and not an unrelated process that happens to mention
|
|
118
|
+
// the path)
|
|
119
|
+
// (c) ageMs > agentTimeoutMs * 2 (the worktree was definitely abandoned by
|
|
120
|
+
// its dispatch; agentTimeout default is 5h, so 10h)
|
|
121
|
+
//
|
|
122
|
+
// The orphan-sweep code path already guarantees no live dispatch row claims
|
|
123
|
+
// the path — we'd never be here otherwise — so the "no live dispatch" check
|
|
124
|
+
// is implicit, not duplicated here.
|
|
125
|
+
function _isSafeReapHolder(holder, basename, minAgeMs) {
|
|
126
|
+
if (!holder || !holder.pid || !holder.cmdline) return false;
|
|
127
|
+
const cmd = String(holder.cmdline);
|
|
128
|
+
if (!/spawn-agent\.js/i.test(cmd)) return false;
|
|
129
|
+
if (basename && !cmd.includes(basename)) return false;
|
|
130
|
+
const age = Number(holder.ageMs) || 0;
|
|
131
|
+
if (age <= 0) return false; // unknown age — refuse to kill
|
|
132
|
+
if (age < minAgeMs) return false;
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
94
136
|
function _maybeEscalateStuck(resolvedPath, errorMsg, opts = {}) {
|
|
95
137
|
const cfgEng = (opts.config && opts.config.engine) || {};
|
|
96
138
|
const threshold = cfgEng.worktreeStuckThreshold || shared.ENGINE_DEFAULTS.worktreeStuckThreshold || 10;
|
|
@@ -100,7 +142,7 @@ function _maybeEscalateStuck(resolvedPath, errorMsg, opts = {}) {
|
|
|
100
142
|
rec.failures++;
|
|
101
143
|
rec.lastAttempt = now;
|
|
102
144
|
rec.lastError = errorMsg || rec.lastError || 'unknown';
|
|
103
|
-
if (rec.failures < threshold) return { escalated: false, alreadyEscalated: false };
|
|
145
|
+
if (rec.failures < threshold) return { escalated: false, alreadyEscalated: false, holders: [], reapedPids: [] };
|
|
104
146
|
|
|
105
147
|
const alreadyEscalated = rec.escalatedAt > 0;
|
|
106
148
|
if (!alreadyEscalated) {
|
|
@@ -112,15 +154,57 @@ function _maybeEscalateStuck(resolvedPath, errorMsg, opts = {}) {
|
|
|
112
154
|
rec.suppressedUntil = now + suppressMs;
|
|
113
155
|
}
|
|
114
156
|
|
|
157
|
+
// W-mq6f2fe0000557fa — orphan-sweep enrichment + opt-in auto-reap. Scans
|
|
158
|
+
// and reap-attempts run on EVERY escalated tick (no per-day dedup) so that
|
|
159
|
+
// a fresh holder appearing after the first escalation still gets killed.
|
|
160
|
+
// The inbox NOTE is still dedup'd per UTC day below.
|
|
161
|
+
let holders = [];
|
|
162
|
+
let reapedPids = [];
|
|
163
|
+
const isOrphanSweep = opts.reason === 'orphan-sweep';
|
|
164
|
+
if (isOrphanSweep) {
|
|
165
|
+
try {
|
|
166
|
+
const findHoldersFn = typeof opts.findProcessesWithCwdInside === 'function'
|
|
167
|
+
? opts.findProcessesWithCwdInside
|
|
168
|
+
: shared.findProcessesWithCwdInside;
|
|
169
|
+
const scanTimeoutMs = cfgEng.orphanHolderScanTimeoutMs
|
|
170
|
+
?? shared.ENGINE_DEFAULTS.orphanHolderScanTimeoutMs
|
|
171
|
+
?? 5000;
|
|
172
|
+
holders = findHoldersFn(resolvedPath, { timeoutMs: scanTimeoutMs }) || [];
|
|
173
|
+
} catch { holders = []; }
|
|
174
|
+
|
|
175
|
+
if (cfgEng.autoReapOrphanWorktreeHolders === true && holders.length > 0) {
|
|
176
|
+
const basename = path.basename(resolvedPath);
|
|
177
|
+
const agentTimeoutMs = Number(cfgEng.agentTimeout)
|
|
178
|
+
|| Number(shared.ENGINE_DEFAULTS.agentTimeout)
|
|
179
|
+
|| (5 * 60 * 60 * 1000);
|
|
180
|
+
const minAgeMs = agentTimeoutMs * 2;
|
|
181
|
+
const killFn = typeof opts.killImmediate === 'function'
|
|
182
|
+
? opts.killImmediate
|
|
183
|
+
: shared.killImmediate;
|
|
184
|
+
for (const h of holders) {
|
|
185
|
+
if (!_isSafeReapHolder(h, basename, minAgeMs)) continue;
|
|
186
|
+
try {
|
|
187
|
+
killFn({ pid: h.pid });
|
|
188
|
+
reapedPids.push(h.pid);
|
|
189
|
+
} catch { /* fail-open — keep trying other holders */ }
|
|
190
|
+
}
|
|
191
|
+
if (reapedPids.length > 0) {
|
|
192
|
+
try { shared.bumpWorktreeGcMetric('orphanHoldersReaped'); } catch { /* optional */ }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
115
197
|
// Dedup inbox note per UTC day.
|
|
116
198
|
const today = new Date(now).toISOString().slice(0, 10);
|
|
117
|
-
if (rec.lastNoteDate === today)
|
|
199
|
+
if (rec.lastNoteDate === today) {
|
|
200
|
+
return { escalated: !alreadyEscalated, alreadyEscalated, holders, reapedPids };
|
|
201
|
+
}
|
|
118
202
|
rec.lastNoteDate = today;
|
|
119
203
|
|
|
120
204
|
try {
|
|
121
205
|
const writer = typeof opts.writeToInbox === 'function' ? opts.writeToInbox : shared.writeToInbox;
|
|
122
206
|
const basename = path.basename(resolvedPath);
|
|
123
|
-
const
|
|
207
|
+
const bodyParts = [
|
|
124
208
|
`# Worktree stuck: ${basename}`,
|
|
125
209
|
'',
|
|
126
210
|
`Worktree dir **${resolvedPath}** has failed removal ${rec.failures} consecutive times.`,
|
|
@@ -153,16 +237,116 @@ function _maybeEscalateStuck(resolvedPath, errorMsg, opts = {}) {
|
|
|
153
237
|
'',
|
|
154
238
|
`Per-tick warn spam is suppressed for the next`,
|
|
155
239
|
`\`ENGINE_DEFAULTS.worktreeStuckSuppressMs\` (default 60min).`,
|
|
156
|
-
]
|
|
157
|
-
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
// W-mq6f2fe0000557fa — append the holder section for orphan-sweep callers.
|
|
243
|
+
if (isOrphanSweep) {
|
|
244
|
+
bodyParts.push('', '## Live holders');
|
|
245
|
+
bodyParts.push('');
|
|
246
|
+
if (holders.length === 0) {
|
|
247
|
+
bodyParts.push(
|
|
248
|
+
'_No holder process found via cwd scan._',
|
|
249
|
+
'',
|
|
250
|
+
'If the dir is still stuck, the holder may be a non-process resource (',
|
|
251
|
+
'antivirus scan-in-progress, OneDrive sync, indexer); try again after',
|
|
252
|
+
'~60s, or run `handle.exe` interactively for a deeper search.'
|
|
253
|
+
);
|
|
254
|
+
} else {
|
|
255
|
+
for (const h of holders) {
|
|
256
|
+
const tail = String(h.cmdline || '').replace(/\r?\n/g, ' ').slice(0, 300);
|
|
257
|
+
const ageStr = h.ageMs ? `${Math.floor(h.ageMs / 1000)}s` : 'unknown';
|
|
258
|
+
bodyParts.push(`- **PID ${h.pid}** (age: ${ageStr})`);
|
|
259
|
+
bodyParts.push(' ```');
|
|
260
|
+
bodyParts.push(` ${tail}`);
|
|
261
|
+
bodyParts.push(' ```');
|
|
262
|
+
}
|
|
263
|
+
bodyParts.push('');
|
|
264
|
+
if (cfgEng.autoReapOrphanWorktreeHolders === true) {
|
|
265
|
+
if (reapedPids.length > 0) {
|
|
266
|
+
bodyParts.push(
|
|
267
|
+
`## Auto-reap`,
|
|
268
|
+
'',
|
|
269
|
+
`Engine auto-killed ${reapedPids.length} unambiguous orphan`,
|
|
270
|
+
`spawn-agent process${reapedPids.length === 1 ? '' : 'es'}: PID${reapedPids.length === 1 ? '' : 's'} ${reapedPids.join(', ')}.`,
|
|
271
|
+
`If the kill released the cwd lock, the next sweep tick will`,
|
|
272
|
+
`successfully remove the worktree and emit a`,
|
|
273
|
+
`\`worktree-recovered-${basename}-via-holder-reap\` recovery note.`
|
|
274
|
+
);
|
|
275
|
+
} else {
|
|
276
|
+
bodyParts.push(
|
|
277
|
+
`## Auto-reap`,
|
|
278
|
+
'',
|
|
279
|
+
`\`engine.autoReapOrphanWorktreeHolders\` is ON but no holder`,
|
|
280
|
+
`qualified for auto-kill (cmdline must match \`spawn-agent.js\`,`,
|
|
281
|
+
`reference the worktree basename, and process age must exceed`,
|
|
282
|
+
`\`engine.agentTimeout * 2\`). Manual intervention may be needed.`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
bodyParts.push(
|
|
287
|
+
`_To auto-kill orphan spawn-agent processes holding this worktree,_`,
|
|
288
|
+
`_flip \`engine.autoReapOrphanWorktreeHolders\` ON in dashboard Settings._`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
writer('engine', `worktree-stuck-${basename}`, bodyParts.join('\n'));
|
|
158
295
|
} catch { /* note best-effort */ }
|
|
159
296
|
|
|
160
|
-
return { escalated: !alreadyEscalated, alreadyEscalated };
|
|
297
|
+
return { escalated: !alreadyEscalated, alreadyEscalated, holders, reapedPids };
|
|
161
298
|
}
|
|
162
299
|
|
|
163
300
|
// Test-only reset hook. Production code never calls this.
|
|
164
301
|
function _resetStuckPathsForTesting() { _stuckPaths.clear(); }
|
|
165
302
|
|
|
303
|
+
// W-mq6f2fe0000557fa — post-auto-reap retry. Called only when
|
|
304
|
+
// `_maybeEscalateStuck` reports `reapedPids.length > 0`. Clears the per-path
|
|
305
|
+
// failure cooldown (otherwise the post-3-strike suppression in
|
|
306
|
+
// `shared.removeWorktree` silently skips the retry), waits briefly for the
|
|
307
|
+
// OS to release the killed processes' file handles, then attempts ONE more
|
|
308
|
+
// removal. On success, writes the recovered-via-holder-reap note via
|
|
309
|
+
// `_markStuckSuccess` with the reaped-pid attribution.
|
|
310
|
+
function _postReapRetry(wtPath, gitRoot, parentDir, resolvedPath, _removeWorktree, opts, projStats, result, log, reapedPids) {
|
|
311
|
+
try { shared.clearWorktreeFailureCache(resolvedPath); } catch { /* optional */ }
|
|
312
|
+
// Brief settle window for OS to release file handles from killed processes.
|
|
313
|
+
try {
|
|
314
|
+
const sleepFn = typeof opts.sleepSyncFn === 'function'
|
|
315
|
+
? opts.sleepSyncFn
|
|
316
|
+
: (ms) => {
|
|
317
|
+
try {
|
|
318
|
+
require('child_process').execFileSync(process.execPath, ['-e', `setTimeout(()=>process.exit(0), ${Number(ms) || 0})`], { stdio: 'ignore' });
|
|
319
|
+
} catch { /* timeout / spawn fail — proceed anyway */ }
|
|
320
|
+
};
|
|
321
|
+
sleepFn(2000);
|
|
322
|
+
} catch { /* sleep failures should not block the retry */ }
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const removed = _removeWorktree(wtPath, gitRoot, parentDir);
|
|
326
|
+
if (removed) {
|
|
327
|
+
// Down-count the failure we recorded just before; this dispatch
|
|
328
|
+
// ultimately succeeded after the auto-reap.
|
|
329
|
+
if (projStats) { projStats.failed = Math.max(0, projStats.failed - 1); projStats.evicted++; }
|
|
330
|
+
if (result) { result.failed = Math.max(0, result.failed - 1); result.evicted++; }
|
|
331
|
+
_markStuckSuccess(resolvedPath, {
|
|
332
|
+
writeToInbox: opts.writeToInbox,
|
|
333
|
+
recoveryReason: 'holder-reap',
|
|
334
|
+
reapedPids,
|
|
335
|
+
});
|
|
336
|
+
try { shared.bumpWorktreeGcMetric('recoveredViaHolderReap'); } catch { /* optional */ }
|
|
337
|
+
if (typeof log === 'function') {
|
|
338
|
+
log('info', `worktree-gc: removed ${wtPath} after auto-reaping holder(s) ${reapedPids.join(', ')}`);
|
|
339
|
+
}
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
} catch (retryErr) {
|
|
343
|
+
if (typeof log === 'function') {
|
|
344
|
+
log('warn', `worktree-gc: post-reap retry threw for ${wtPath}: ${retryErr && retryErr.message}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
166
350
|
/**
|
|
167
351
|
* Decide whether a dispatch-end worktree should be GC'd.
|
|
168
352
|
*
|
|
@@ -494,19 +678,39 @@ function pruneOrphanWorktrees(opts) {
|
|
|
494
678
|
log('info', `worktree-gc: boot-evicted orphan ${name} for project ${project.name || 'default'}`);
|
|
495
679
|
} else {
|
|
496
680
|
projStats.failed++; result.failed++;
|
|
497
|
-
const
|
|
498
|
-
|
|
681
|
+
const escResult = _maybeEscalateStuck(wtResolved, 'remove returned false', {
|
|
682
|
+
config: opts.config, writeToInbox: opts.writeToInbox,
|
|
683
|
+
reason: 'orphan-sweep',
|
|
684
|
+
findProcessesWithCwdInside: opts.findProcessesWithCwdInside,
|
|
685
|
+
killImmediate: opts.killImmediate,
|
|
686
|
+
});
|
|
687
|
+
if (!escResult.alreadyEscalated && !suppressed) {
|
|
499
688
|
log('warn', `worktree-gc: boot-evict returned false for ${wtPath}`);
|
|
500
689
|
}
|
|
690
|
+
// W-mq6f2fe0000557fa — if auto-reap killed any holders, retry the
|
|
691
|
+
// removal once after a brief settle window. Clear the failure
|
|
692
|
+
// cache entry so the post-reap retry isn't suppressed by the
|
|
693
|
+
// 3-strike cooldown.
|
|
694
|
+
if (escResult.reapedPids && escResult.reapedPids.length > 0) {
|
|
695
|
+
_postReapRetry(wtPath, rootDir, wtParent, wtResolved, _removeWorktree, opts, projStats, result, log, escResult.reapedPids);
|
|
696
|
+
}
|
|
501
697
|
}
|
|
502
698
|
} catch (rmErr) {
|
|
503
699
|
projStats.failed++; result.failed++;
|
|
504
700
|
const wtResolved = path.resolve(wtPath);
|
|
505
701
|
const suppressed = _isStuckPathSuppressed(wtResolved);
|
|
506
|
-
const
|
|
507
|
-
|
|
702
|
+
const escResult = _maybeEscalateStuck(wtResolved, rmErr && rmErr.message, {
|
|
703
|
+
config: opts.config, writeToInbox: opts.writeToInbox,
|
|
704
|
+
reason: 'orphan-sweep',
|
|
705
|
+
findProcessesWithCwdInside: opts.findProcessesWithCwdInside,
|
|
706
|
+
killImmediate: opts.killImmediate,
|
|
707
|
+
});
|
|
708
|
+
if (!escResult.alreadyEscalated && !suppressed) {
|
|
508
709
|
log('warn', `worktree-gc: boot-evict threw for ${wtPath}: ${rmErr.message}`);
|
|
509
710
|
}
|
|
711
|
+
if (escResult.reapedPids && escResult.reapedPids.length > 0) {
|
|
712
|
+
_postReapRetry(wtPath, rootDir, wtParent, wtResolved, _removeWorktree, opts, projStats, result, log, escResult.reapedPids);
|
|
713
|
+
}
|
|
510
714
|
}
|
|
511
715
|
}
|
|
512
716
|
result.perProject[project.name || rootDir] = projStats;
|
|
@@ -693,17 +897,33 @@ function pruneOrphanWorktreesFromGitRegistry(opts) {
|
|
|
693
897
|
log('info', `worktree-gc: out-of-root evicted ${wtAbs} for project ${project.name || 'default'}`);
|
|
694
898
|
} else {
|
|
695
899
|
projStats.failed++; result.failed++;
|
|
696
|
-
const
|
|
697
|
-
|
|
900
|
+
const escResult = _maybeEscalateStuck(wtAbs, 'remove returned false', {
|
|
901
|
+
config: opts.config, writeToInbox: opts.writeToInbox,
|
|
902
|
+
reason: 'orphan-sweep',
|
|
903
|
+
findProcessesWithCwdInside: opts.findProcessesWithCwdInside,
|
|
904
|
+
killImmediate: opts.killImmediate,
|
|
905
|
+
});
|
|
906
|
+
if (!escResult.alreadyEscalated && !suppressed) {
|
|
698
907
|
log('warn', `worktree-gc: out-of-root remove returned false for ${wtAbs}`);
|
|
699
908
|
}
|
|
909
|
+
if (escResult.reapedPids && escResult.reapedPids.length > 0) {
|
|
910
|
+
_postReapRetry(wtAbs, rootDir, parentDir, wtAbs, _removeWorktree, opts, projStats, result, log, escResult.reapedPids);
|
|
911
|
+
}
|
|
700
912
|
}
|
|
701
913
|
} catch (rmErr) {
|
|
702
914
|
projStats.failed++; result.failed++;
|
|
703
|
-
const
|
|
704
|
-
|
|
915
|
+
const escResult = _maybeEscalateStuck(wtAbs, rmErr && rmErr.message, {
|
|
916
|
+
config: opts.config, writeToInbox: opts.writeToInbox,
|
|
917
|
+
reason: 'orphan-sweep',
|
|
918
|
+
findProcessesWithCwdInside: opts.findProcessesWithCwdInside,
|
|
919
|
+
killImmediate: opts.killImmediate,
|
|
920
|
+
});
|
|
921
|
+
if (!escResult.alreadyEscalated && !suppressed) {
|
|
705
922
|
log('warn', `worktree-gc: out-of-root remove threw for ${wtAbs}: ${rmErr.message}`);
|
|
706
923
|
}
|
|
924
|
+
if (escResult.reapedPids && escResult.reapedPids.length > 0) {
|
|
925
|
+
_postReapRetry(wtAbs, rootDir, parentDir, wtAbs, _removeWorktree, opts, projStats, result, log, escResult.reapedPids);
|
|
926
|
+
}
|
|
707
927
|
}
|
|
708
928
|
}
|
|
709
929
|
|