instar 1.3.1043 → 1.3.1045
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/dashboard/index.html +355 -0
- package/dist/core/AutoUpdater.d.ts +27 -0
- package/dist/core/AutoUpdater.d.ts.map +1 -1
- package/dist/core/AutoUpdater.js +34 -1
- package/dist/core/AutoUpdater.js.map +1 -1
- package/dist/core/UpdateGate.d.ts +37 -0
- package/dist/core/UpdateGate.d.ts.map +1 -1
- package/dist/core/UpdateGate.js +41 -4
- package/dist/core/UpdateGate.js.map +1 -1
- package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
- package/dist/core/WriteDomainRegistry.js +18 -0
- package/dist/core/WriteDomainRegistry.js.map +1 -1
- package/dist/data/standards-registry.meta.json +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +353 -4
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +47 -47
- package/src/data/standards-registry.meta.json +1 -1
- package/upgrades/{1.3.1043.md → 1.3.1044.md} +32 -0
- package/upgrades/1.3.1045.md +52 -0
- package/upgrades/side-effects/dashboard-jobs-ui-phase-4.md +170 -0
- package/upgrades/side-effects/restart-safe-blocker-classification.md +88 -0
package/dashboard/index.html
CHANGED
|
@@ -3172,12 +3172,40 @@
|
|
|
3172
3172
|
</select>
|
|
3173
3173
|
</div>
|
|
3174
3174
|
<div class="tab-purpose" style="padding:0 12px 8px">Monitor and control your scheduled tasks — recurring jobs that run on their own.</div>
|
|
3175
|
+
|
|
3176
|
+
<!-- Migration banner (jobs-as-agentmd Phase 4) -->
|
|
3177
|
+
<div id="jobsMigrationBanner" style="display:none;margin:8px 12px;padding:10px 12px;background:var(--panel);border:1px solid var(--border);border-radius:8px;font-size:12px">
|
|
3178
|
+
<div id="jobsMigrationBannerBody"></div>
|
|
3179
|
+
<div id="jobsMigrationBannerActions" style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap"></div>
|
|
3180
|
+
</div>
|
|
3181
|
+
|
|
3182
|
+
<!-- Issues card (jobs-as-agentmd Phase 4) -->
|
|
3183
|
+
<div id="jobsIssuesCard" style="display:none;margin:8px 12px;padding:10px 12px;background:var(--panel);border:1px solid var(--border);border-radius:8px;font-size:12px">
|
|
3184
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
|
|
3185
|
+
<strong style="font-size:12px">Issues <span id="jobsIssuesCount" style="color:var(--text-dim);font-weight:400"></span></strong>
|
|
3186
|
+
<div style="display:flex;gap:6px;align-items:center">
|
|
3187
|
+
<select id="jobsIssuesFilter" onchange="renderIssuesCard()" style="font-size:11px;padding:2px 4px">
|
|
3188
|
+
<option value="">All kinds</option>
|
|
3189
|
+
<option value="orphan-manifest">Orphan manifest</option>
|
|
3190
|
+
<option value="shadow-md">Shadow body</option>
|
|
3191
|
+
<option value="missing-from-jobs-json">Missing migration</option>
|
|
3192
|
+
<option value="staged-new">Staged save</option>
|
|
3193
|
+
<option value="case-collision">Case collision</option>
|
|
3194
|
+
</select>
|
|
3195
|
+
<button onclick="loadIssuesCard()" title="Refresh the issues list" aria-label="Refresh the issues list" style="font-size:11px;padding:2px 6px">↻</button>
|
|
3196
|
+
</div>
|
|
3197
|
+
</div>
|
|
3198
|
+
<div id="jobsIssuesList"></div>
|
|
3199
|
+
</div>
|
|
3200
|
+
|
|
3175
3201
|
<div id="jobsSummary" class="jobs-summary"></div>
|
|
3176
3202
|
<div class="jobs-filter-bar" id="jobsFilterBar">
|
|
3177
3203
|
<button class="jobs-filter-chip active" data-filter="all" onclick="setJobFilter('all')">All</button>
|
|
3178
3204
|
<button class="jobs-filter-chip" data-filter="running" onclick="setJobFilter('running')">Running</button>
|
|
3179
3205
|
<button class="jobs-filter-chip" data-filter="failing" onclick="setJobFilter('failing')">Failing</button>
|
|
3180
3206
|
<button class="jobs-filter-chip" data-filter="disabled" onclick="setJobFilter('disabled')">Disabled</button>
|
|
3207
|
+
<button class="jobs-filter-chip" data-filter="instar" onclick="setJobFilter('instar')">Instar defaults</button>
|
|
3208
|
+
<button class="jobs-filter-chip" data-filter="user" onclick="setJobFilter('user')">Your jobs</button>
|
|
3181
3209
|
</div>
|
|
3182
3210
|
<div class="jobs-list" id="jobsList">
|
|
3183
3211
|
<div style="padding:20px;color:var(--text-dim);text-align:center">Loading jobs...</div>
|
|
@@ -7312,6 +7340,250 @@
|
|
|
7312
7340
|
document.getElementById('jobsList').innerHTML =
|
|
7313
7341
|
'<div style="padding:20px;color:var(--red);text-align:center">Failed to load jobs</div>';
|
|
7314
7342
|
}
|
|
7343
|
+
// Phase 4: refresh ancillary surfaces alongside the jobs list.
|
|
7344
|
+
loadJobsMigrationBanner();
|
|
7345
|
+
loadIssuesCard();
|
|
7346
|
+
}
|
|
7347
|
+
|
|
7348
|
+
// ─── Migration banner (jobs-as-agentmd Phase 4) ──────────────────
|
|
7349
|
+
//
|
|
7350
|
+
// Reads GET /jobs/migration-status. Surfaces three operator actions
|
|
7351
|
+
// when applicable: confirm migration complete, roll back, or "your
|
|
7352
|
+
// migration auto-ran on the last update."
|
|
7353
|
+
let migrationStatus = null;
|
|
7354
|
+
async function loadJobsMigrationBanner() {
|
|
7355
|
+
try {
|
|
7356
|
+
const data = await apiFetch('/jobs/migration-status');
|
|
7357
|
+
migrationStatus = data;
|
|
7358
|
+
renderJobsMigrationBanner();
|
|
7359
|
+
} catch {
|
|
7360
|
+
// best-effort — banner is non-load-bearing
|
|
7361
|
+
document.getElementById('jobsMigrationBanner').style.display = 'none';
|
|
7362
|
+
}
|
|
7363
|
+
}
|
|
7364
|
+
|
|
7365
|
+
function renderJobsMigrationBanner() {
|
|
7366
|
+
const banner = document.getElementById('jobsMigrationBanner');
|
|
7367
|
+
const bodyEl = document.getElementById('jobsMigrationBannerBody');
|
|
7368
|
+
const actionsEl = document.getElementById('jobsMigrationBannerActions');
|
|
7369
|
+
if (!migrationStatus) { banner.style.display = 'none'; return; }
|
|
7370
|
+
|
|
7371
|
+
const { hasLegacyJobsJson, hasMigrationComplete, hasMigrationAbandoned, canConfirm, canAbandon, scheduleEntryCount } = migrationStatus;
|
|
7372
|
+
|
|
7373
|
+
if (hasMigrationComplete) {
|
|
7374
|
+
bodyEl.innerHTML = '<strong>✓ Migration confirmed.</strong> Your jobs are running from the new per-slug layout. The legacy <code>jobs.json</code> can be cleaned up on the next release.';
|
|
7375
|
+
actionsEl.innerHTML = '';
|
|
7376
|
+
banner.style.display = 'block';
|
|
7377
|
+
return;
|
|
7378
|
+
}
|
|
7379
|
+
if (hasMigrationAbandoned) {
|
|
7380
|
+
bodyEl.innerHTML = '<strong>Migration abandoned.</strong> Running on the legacy <code>jobs.json</code>. Run <code>instar job migrate</code> to retry.';
|
|
7381
|
+
actionsEl.innerHTML = '';
|
|
7382
|
+
banner.style.display = 'block';
|
|
7383
|
+
return;
|
|
7384
|
+
}
|
|
7385
|
+
if (canConfirm) {
|
|
7386
|
+
bodyEl.innerHTML = `<strong>Migration ready to confirm.</strong> ${scheduleEntryCount} per-slug entries are in place. Confirming locks the new layout in for release-cut.`;
|
|
7387
|
+
actionsEl.innerHTML =
|
|
7388
|
+
'<button onclick="confirmMigration()" style="padding:4px 10px;background:var(--accent);color:#fff;border:none;border-radius:4px;cursor:pointer">Confirm migration complete</button>' +
|
|
7389
|
+
'<button onclick="abandonMigration()" style="padding:4px 10px;background:transparent;border:1px solid var(--border);border-radius:4px;cursor:pointer">Roll back</button>';
|
|
7390
|
+
banner.style.display = 'block';
|
|
7391
|
+
return;
|
|
7392
|
+
}
|
|
7393
|
+
if (hasLegacyJobsJson && canAbandon) {
|
|
7394
|
+
bodyEl.innerHTML = '<strong>Migration partially complete.</strong> Some jobs are on the new layout; review the Issues card below.';
|
|
7395
|
+
actionsEl.innerHTML =
|
|
7396
|
+
'<button onclick="abandonMigration()" style="padding:4px 10px;background:transparent;border:1px solid var(--border);border-radius:4px;cursor:pointer">Roll back</button>';
|
|
7397
|
+
banner.style.display = 'block';
|
|
7398
|
+
return;
|
|
7399
|
+
}
|
|
7400
|
+
banner.style.display = 'none';
|
|
7401
|
+
}
|
|
7402
|
+
|
|
7403
|
+
async function confirmMigration() {
|
|
7404
|
+
if (!confirm('Confirm migration complete? This signals the release-cut gate that your legacy jobs.json can be removed in a future release.')) return;
|
|
7405
|
+
try {
|
|
7406
|
+
await apiFetch('/jobs/migration-confirm', { method: 'POST' });
|
|
7407
|
+
showToast('Migration confirmed.');
|
|
7408
|
+
loadJobs();
|
|
7409
|
+
} catch (e) {
|
|
7410
|
+
showToast('Failed: ' + (e && e.message ? e.message : 'unknown'));
|
|
7411
|
+
}
|
|
7412
|
+
}
|
|
7413
|
+
|
|
7414
|
+
async function abandonMigration() {
|
|
7415
|
+
if (!confirm('Roll back the migration? This removes the new per-slug entries and leaves your legacy jobs.json intact. You can re-run instar job migrate later.')) return;
|
|
7416
|
+
try {
|
|
7417
|
+
await apiFetch('/jobs/migration-abandon', { method: 'POST' });
|
|
7418
|
+
showToast('Migration rolled back.');
|
|
7419
|
+
loadJobs();
|
|
7420
|
+
} catch (e) {
|
|
7421
|
+
showToast('Failed: ' + (e && e.message ? e.message : 'unknown'));
|
|
7422
|
+
}
|
|
7423
|
+
}
|
|
7424
|
+
|
|
7425
|
+
// ─── Issues card (jobs-as-agentmd Phase 4) ───────────────────────
|
|
7426
|
+
//
|
|
7427
|
+
// Consumes GET /jobs/reconcile. Surfaces orphan/shadow/missing/
|
|
7428
|
+
// staged-new/case-collision findings with per-class filter chips
|
|
7429
|
+
// and per-item dismiss (auto-undismiss on recurrence at next reload).
|
|
7430
|
+
let issuesData = null;
|
|
7431
|
+
const dismissedIssueKeys = new Set();
|
|
7432
|
+
async function loadIssuesCard() {
|
|
7433
|
+
try {
|
|
7434
|
+
const data = await apiFetch('/jobs/reconcile');
|
|
7435
|
+
issuesData = data;
|
|
7436
|
+
renderIssuesCard();
|
|
7437
|
+
} catch {
|
|
7438
|
+
document.getElementById('jobsIssuesCard').style.display = 'none';
|
|
7439
|
+
}
|
|
7440
|
+
}
|
|
7441
|
+
|
|
7442
|
+
function renderIssuesCard() {
|
|
7443
|
+
const card = document.getElementById('jobsIssuesCard');
|
|
7444
|
+
const list = document.getElementById('jobsIssuesList');
|
|
7445
|
+
const count = document.getElementById('jobsIssuesCount');
|
|
7446
|
+
const filterEl = document.getElementById('jobsIssuesFilter');
|
|
7447
|
+
|
|
7448
|
+
if (!issuesData || !Array.isArray(issuesData.findings) || issuesData.findings.length === 0) {
|
|
7449
|
+
card.style.display = 'none';
|
|
7450
|
+
return;
|
|
7451
|
+
}
|
|
7452
|
+
|
|
7453
|
+
const filter = filterEl.value;
|
|
7454
|
+
const filtered = issuesData.findings.filter(f => (!filter || f.kind === filter) && !dismissedIssueKeys.has(issueKey(f)));
|
|
7455
|
+
|
|
7456
|
+
if (filtered.length === 0) {
|
|
7457
|
+
card.style.display = 'none';
|
|
7458
|
+
return;
|
|
7459
|
+
}
|
|
7460
|
+
card.style.display = 'block';
|
|
7461
|
+
count.textContent = '(' + filtered.length + ')';
|
|
7462
|
+
|
|
7463
|
+
// Sort by severity (error > warning > info), then by slug.
|
|
7464
|
+
const sevOrder = { error: 0, warning: 1, info: 2 };
|
|
7465
|
+
filtered.sort((a, b) => (sevOrder[a.severity] - sevOrder[b.severity]) || ((a.slug || '').localeCompare(b.slug || '')));
|
|
7466
|
+
|
|
7467
|
+
list.innerHTML = filtered.map(f => {
|
|
7468
|
+
const sevColor = f.severity === 'error' ? 'var(--red)' : f.severity === 'warning' ? 'var(--yellow)' : 'var(--text-dim)';
|
|
7469
|
+
const slugDisplay = f.slug ? '<code>' + esc(f.slug) + '</code> ' : '';
|
|
7470
|
+
const key = issueKey(f);
|
|
7471
|
+
return '<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px;padding:4px 0;border-bottom:1px solid var(--border)">'
|
|
7472
|
+
+ '<div style="flex:1">'
|
|
7473
|
+
+ '<span style="color:' + sevColor + ';font-weight:600;margin-right:6px">' + esc(f.kind) + '</span>'
|
|
7474
|
+
+ slugDisplay
|
|
7475
|
+
+ '<span style="color:var(--text-dim);font-size:11px">' + esc(f.summary || '') + '</span>'
|
|
7476
|
+
+ '</div>'
|
|
7477
|
+
+ '<button onclick="dismissIssue(\'' + esc(key) + '\')" title="Dismiss until next reload" style="font-size:11px;padding:2px 6px;background:transparent;border:1px solid var(--border);border-radius:4px;cursor:pointer">×</button>'
|
|
7478
|
+
+ '</div>';
|
|
7479
|
+
}).join('');
|
|
7480
|
+
}
|
|
7481
|
+
|
|
7482
|
+
function issueKey(f) {
|
|
7483
|
+
return (f.kind || '') + '|' + (f.slug || '') + '|' + (f.filePath || '');
|
|
7484
|
+
}
|
|
7485
|
+
|
|
7486
|
+
function dismissIssue(key) {
|
|
7487
|
+
dismissedIssueKeys.add(key);
|
|
7488
|
+
renderIssuesCard();
|
|
7489
|
+
}
|
|
7490
|
+
|
|
7491
|
+
// ─── Override / Unfork / Save (jobs-as-agentmd Phase 4) ──────────
|
|
7492
|
+
async function overrideJob(slug) {
|
|
7493
|
+
if (!confirm('Make your own copy of this job. After this, you control it — instar won\'t update it automatically. You can switch back to the default later.')) return;
|
|
7494
|
+
try {
|
|
7495
|
+
await apiFetch('/jobs/' + encodeURIComponent(slug) + '/override', { method: 'POST' });
|
|
7496
|
+
showToast('Override created. Your copy is now editable in the User namespace.');
|
|
7497
|
+
loadJobs();
|
|
7498
|
+
} catch (e) {
|
|
7499
|
+
showToast('Failed: ' + (e && e.message ? e.message : 'unknown'));
|
|
7500
|
+
}
|
|
7501
|
+
}
|
|
7502
|
+
|
|
7503
|
+
async function unforkJob(slug) {
|
|
7504
|
+
if (!confirm('Switch back to the instar default. Your version is saved locally for 30 days in case you change your mind. After that it\'s deleted.')) return;
|
|
7505
|
+
try {
|
|
7506
|
+
const r = await apiFetch('/jobs/' + encodeURIComponent(slug) + '/unfork', { method: 'POST' });
|
|
7507
|
+
showToast('Unforked. Backup saved.');
|
|
7508
|
+
loadJobs();
|
|
7509
|
+
} catch (e) {
|
|
7510
|
+
showToast('Failed: ' + (e && e.message ? e.message : 'unknown'));
|
|
7511
|
+
}
|
|
7512
|
+
}
|
|
7513
|
+
|
|
7514
|
+
async function toggleJobEnabled(slug, currentlyEnabled) {
|
|
7515
|
+
const endpoint = currentlyEnabled ? '/disable' : '/enable';
|
|
7516
|
+
try {
|
|
7517
|
+
await apiFetch('/jobs/' + encodeURIComponent(slug) + endpoint, { method: 'POST' });
|
|
7518
|
+
loadJobs();
|
|
7519
|
+
} catch (e) {
|
|
7520
|
+
showToast('Failed: ' + (e && e.message ? e.message : 'unknown'));
|
|
7521
|
+
}
|
|
7522
|
+
}
|
|
7523
|
+
|
|
7524
|
+
// ─── Editor modal ────────────────────────────────────────────────
|
|
7525
|
+
let editorOpenSlug = null;
|
|
7526
|
+
let editorManifestVersion = null;
|
|
7527
|
+
function openJobEditor(slug) {
|
|
7528
|
+
const job = jobsData.find(j => j.slug === slug);
|
|
7529
|
+
if (!job) return;
|
|
7530
|
+
if (job.origin === 'instar') {
|
|
7531
|
+
if (!confirm('This is an instar default. Editing it requires forking to your User namespace first. Override now?')) return;
|
|
7532
|
+
overrideJob(slug).then(() => setTimeout(() => openJobEditor(slug), 600));
|
|
7533
|
+
return;
|
|
7534
|
+
}
|
|
7535
|
+
editorOpenSlug = slug;
|
|
7536
|
+
editorManifestVersion = job.manifestVersion || 0;
|
|
7537
|
+
document.getElementById('jobEditorSlug').textContent = slug;
|
|
7538
|
+
document.getElementById('jobEditorName').value = job.name || '';
|
|
7539
|
+
document.getElementById('jobEditorDescription').value = job.description || '';
|
|
7540
|
+
document.getElementById('jobEditorSchedule').value = job.schedule || '';
|
|
7541
|
+
document.getElementById('jobEditorPriority').value = job.priority || 'low';
|
|
7542
|
+
document.getElementById('jobEditorBody').value = job.body || '';
|
|
7543
|
+
document.getElementById('jobEditorOcc').textContent = editorManifestVersion ? 'Saved copy #' + editorManifestVersion : '';
|
|
7544
|
+
document.getElementById('jobEditorModal').style.display = 'flex';
|
|
7545
|
+
}
|
|
7546
|
+
|
|
7547
|
+
function closeJobEditor() {
|
|
7548
|
+
editorOpenSlug = null;
|
|
7549
|
+
editorManifestVersion = null;
|
|
7550
|
+
document.getElementById('jobEditorModal').style.display = 'none';
|
|
7551
|
+
}
|
|
7552
|
+
|
|
7553
|
+
async function saveJobEditor() {
|
|
7554
|
+
if (!editorOpenSlug) return;
|
|
7555
|
+
const slug = editorOpenSlug;
|
|
7556
|
+
const body = document.getElementById('jobEditorBody').value;
|
|
7557
|
+
const manifest = {
|
|
7558
|
+
slug,
|
|
7559
|
+
origin: 'user',
|
|
7560
|
+
name: document.getElementById('jobEditorName').value,
|
|
7561
|
+
description: document.getElementById('jobEditorDescription').value,
|
|
7562
|
+
schedule: document.getElementById('jobEditorSchedule').value,
|
|
7563
|
+
priority: document.getElementById('jobEditorPriority').value,
|
|
7564
|
+
execute: { type: 'agentmd' },
|
|
7565
|
+
manifestVersion: editorManifestVersion,
|
|
7566
|
+
};
|
|
7567
|
+
try {
|
|
7568
|
+
const r = await apiFetch('/jobs/' + encodeURIComponent(slug) + '/save', {
|
|
7569
|
+
method: 'POST',
|
|
7570
|
+
headers: { 'content-type': 'application/json' },
|
|
7571
|
+
body: JSON.stringify({ mdBody: body, manifest, manifestVersion: editorManifestVersion }),
|
|
7572
|
+
});
|
|
7573
|
+
showToast('Saved.');
|
|
7574
|
+
closeJobEditor();
|
|
7575
|
+
loadJobs();
|
|
7576
|
+
} catch (e) {
|
|
7577
|
+
const msg = e && e.message ? e.message : 'unknown';
|
|
7578
|
+
if (msg.indexOf('409') >= 0 || msg.indexOf('stale') >= 0) {
|
|
7579
|
+
if (confirm('Someone else saved this job since you opened the editor. Reload and lose your changes?')) {
|
|
7580
|
+
closeJobEditor();
|
|
7581
|
+
loadJobs();
|
|
7582
|
+
}
|
|
7583
|
+
} else {
|
|
7584
|
+
showToast('Save failed: ' + msg);
|
|
7585
|
+
}
|
|
7586
|
+
}
|
|
7315
7587
|
}
|
|
7316
7588
|
|
|
7317
7589
|
function renderJobsSummary() {
|
|
@@ -7348,6 +7620,8 @@
|
|
|
7348
7620
|
return (s.consecutiveFailures || 0) > 0;
|
|
7349
7621
|
}
|
|
7350
7622
|
if (jobFilter === 'disabled') return !j.enabled;
|
|
7623
|
+
if (jobFilter === 'instar') return j.origin === 'instar';
|
|
7624
|
+
if (jobFilter === 'user') return j.origin === 'user';
|
|
7351
7625
|
return true;
|
|
7352
7626
|
});
|
|
7353
7627
|
|
|
@@ -7398,6 +7672,25 @@
|
|
|
7398
7672
|
const priorityBadge = (j.priority === 'critical' || j.priority === 'high')
|
|
7399
7673
|
? '<span class="priority-badge ' + esc(j.priority) + '">' + esc(j.priority) + '</span>' : '';
|
|
7400
7674
|
|
|
7675
|
+
// Namespace badge: instar default vs user-authored vs legacy.
|
|
7676
|
+
// Per INSTAR-JOBS-AS-AGENTMD spec §Dashboard UX "namespace badge".
|
|
7677
|
+
let nsBadge = '';
|
|
7678
|
+
if (j.origin === 'instar') {
|
|
7679
|
+
nsBadge = '<span class="model-badge" title="Shipped by instar" style="background:rgba(120,180,255,0.18)">instar</span>';
|
|
7680
|
+
} else if (j.origin === 'user') {
|
|
7681
|
+
nsBadge = '<span class="model-badge" title="Your job" style="background:rgba(150,220,150,0.18)">user</span>';
|
|
7682
|
+
if (j.hasUserFork) {
|
|
7683
|
+
nsBadge += '<span class="model-badge" title="Forked from an instar default" style="background:rgba(200,180,100,0.18)">fork</span>';
|
|
7684
|
+
}
|
|
7685
|
+
}
|
|
7686
|
+
|
|
7687
|
+
// Lock-trust indicator: warn when an instar default is in a
|
|
7688
|
+
// tamper state. Per §Trust Model.
|
|
7689
|
+
let trustWarn = '';
|
|
7690
|
+
if (j.lockTrust && j.lockTrust !== 'trusted' && j.lockTrust !== 'untrusted-no-lockfile') {
|
|
7691
|
+
trustWarn = '<span class="model-badge" title="' + esc(j.lockTrust) + '" style="background:rgba(220,100,100,0.28);color:#fff">!</span>';
|
|
7692
|
+
}
|
|
7693
|
+
|
|
7401
7694
|
return '<div class="job-item' + isActive + '" onclick="selectJob(\'' + esc(j.slug) + '\')">'
|
|
7402
7695
|
+ '<div class="job-item-top">'
|
|
7403
7696
|
+ '<span class="job-status-dot ' + dotClass + '"></span>'
|
|
@@ -7409,6 +7702,8 @@
|
|
|
7409
7702
|
+ '<span>' + esc(schedule) + '</span>'
|
|
7410
7703
|
+ '<span class="model-badge">' + esc(j.model || '—') + '</span>'
|
|
7411
7704
|
+ priorityBadge
|
|
7705
|
+
+ nsBadge
|
|
7706
|
+
+ trustWarn
|
|
7412
7707
|
+ '</div>'
|
|
7413
7708
|
+ '<div class="job-item-status">'
|
|
7414
7709
|
+ '<span>' + esc(lastResult) + '</span> · <span>' + esc(lastTime) + '</span>'
|
|
@@ -7477,6 +7772,19 @@
|
|
|
7477
7772
|
}
|
|
7478
7773
|
configHtml += '</div></div>';
|
|
7479
7774
|
|
|
7775
|
+
// Phase 4 action buttons: Edit (user-origin only), Override (instar-origin),
|
|
7776
|
+
// Unfork (user-origin with hasUserFork shadowing an instar slug).
|
|
7777
|
+
let phase4Actions = '';
|
|
7778
|
+
if (job.origin === 'instar' && !job.hasUserFork) {
|
|
7779
|
+
phase4Actions += '<button onclick="overrideJob(\'' + esc(job.slug) + '\')" style="margin-right:6px" title="Make your own editable copy">Override</button>';
|
|
7780
|
+
}
|
|
7781
|
+
if (job.origin === 'user') {
|
|
7782
|
+
phase4Actions += '<button onclick="openJobEditor(\'' + esc(job.slug) + '\')" style="margin-right:6px">Edit</button>';
|
|
7783
|
+
if (job.hasUserFork) {
|
|
7784
|
+
phase4Actions += '<button onclick="unforkJob(\'' + esc(job.slug) + '\')" style="margin-right:6px" title="Switch back to the instar default (saves a backup of your version)">Unfork</button>';
|
|
7785
|
+
}
|
|
7786
|
+
}
|
|
7787
|
+
|
|
7480
7788
|
detail.innerHTML = '<div class="job-detail-header">'
|
|
7481
7789
|
+ '<div>'
|
|
7482
7790
|
+ '<button class="back-btn" onclick="jobsGoBack()" style="display:none;margin-right:8px" title="Back" aria-label="Back">←</button>'
|
|
@@ -7484,6 +7792,7 @@
|
|
|
7484
7792
|
+ '<div class="job-desc">' + esc(job.description || '') + '</div>'
|
|
7485
7793
|
+ '</div>'
|
|
7486
7794
|
+ '<div class="job-detail-actions">'
|
|
7795
|
+
+ phase4Actions
|
|
7487
7796
|
+ '<button class="job-run-btn" id="jobRunBtn" onclick="runJob(\'' + esc(job.slug) + '\')"'
|
|
7488
7797
|
+ (isRunning ? ' disabled' : '') + '>'
|
|
7489
7798
|
+ (isRunning ? 'Running...' : 'Run Now') + '</button>'
|
|
@@ -9019,5 +9328,51 @@
|
|
|
9019
9328
|
// reconnects.)
|
|
9020
9329
|
|
|
9021
9330
|
</script>
|
|
9331
|
+
|
|
9332
|
+
<!-- Job editor modal (jobs-as-agentmd Phase 4) -->
|
|
9333
|
+
<div id="jobEditorModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:100;align-items:center;justify-content:center">
|
|
9334
|
+
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;width:min(720px,92vw);max-height:90vh;display:flex;flex-direction:column;overflow:hidden">
|
|
9335
|
+
<div style="padding:12px 16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
|
|
9336
|
+
<div>
|
|
9337
|
+
<strong>Edit job</strong>
|
|
9338
|
+
<span id="jobEditorSlug" style="color:var(--text-dim);margin-left:8px;font-family:monospace;font-size:12px"></span>
|
|
9339
|
+
</div>
|
|
9340
|
+
<button onclick="closeJobEditor()" title="Close the job editor" aria-label="Close the job editor" style="background:transparent;border:none;font-size:18px;cursor:pointer">×</button>
|
|
9341
|
+
</div>
|
|
9342
|
+
<div style="padding:16px;overflow-y:auto;display:flex;flex-direction:column;gap:12px">
|
|
9343
|
+
<label style="display:flex;flex-direction:column;gap:4px;font-size:12px">
|
|
9344
|
+
Name
|
|
9345
|
+
<input id="jobEditorName" type="text" style="padding:6px 8px" />
|
|
9346
|
+
</label>
|
|
9347
|
+
<label style="display:flex;flex-direction:column;gap:4px;font-size:12px">
|
|
9348
|
+
Description
|
|
9349
|
+
<input id="jobEditorDescription" type="text" style="padding:6px 8px" />
|
|
9350
|
+
</label>
|
|
9351
|
+
<label style="display:flex;flex-direction:column;gap:4px;font-size:12px">
|
|
9352
|
+
Schedule (cron)
|
|
9353
|
+
<input id="jobEditorSchedule" type="text" placeholder="*/5 * * * *" style="padding:6px 8px;font-family:monospace" />
|
|
9354
|
+
</label>
|
|
9355
|
+
<label style="display:flex;flex-direction:column;gap:4px;font-size:12px">
|
|
9356
|
+
Priority
|
|
9357
|
+
<select id="jobEditorPriority" style="padding:6px 8px">
|
|
9358
|
+
<option value="low">low</option>
|
|
9359
|
+
<option value="medium">medium</option>
|
|
9360
|
+
<option value="high">high</option>
|
|
9361
|
+
<option value="critical">critical</option>
|
|
9362
|
+
</select>
|
|
9363
|
+
</label>
|
|
9364
|
+
<label style="display:flex;flex-direction:column;gap:4px;font-size:12px">
|
|
9365
|
+
Prompt body
|
|
9366
|
+
<textarea id="jobEditorBody" rows="12" style="padding:8px;font-family:monospace;font-size:12px;resize:vertical"></textarea>
|
|
9367
|
+
</label>
|
|
9368
|
+
<div id="jobEditorOcc" style="font-size:11px;color:var(--text-dim)"></div>
|
|
9369
|
+
</div>
|
|
9370
|
+
<div style="padding:12px 16px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:8px">
|
|
9371
|
+
<button onclick="closeJobEditor()" style="padding:6px 14px">Cancel</button>
|
|
9372
|
+
<button onclick="saveJobEditor()" style="padding:6px 14px;background:var(--accent);color:#fff;border:none;border-radius:4px;cursor:pointer">Save</button>
|
|
9373
|
+
</div>
|
|
9374
|
+
</div>
|
|
9375
|
+
</div>
|
|
9376
|
+
|
|
9022
9377
|
</body>
|
|
9023
9378
|
</html>
|
|
@@ -103,6 +103,16 @@ export interface AutoUpdaterStatus {
|
|
|
103
103
|
restartDeferral: RestartDeferralState | null;
|
|
104
104
|
/** Primary-developer mode: restarts roll onto latest immediately, never deferred for sessions/window */
|
|
105
105
|
restartImmediately: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Observability-only split of the current restart blockers: the restart-safe
|
|
108
|
+
* subset (topics that resume cleanly across a restart — resumable autonomous
|
|
109
|
+
* topics with a per-topic state file). Empty when not deferring or when no
|
|
110
|
+
* blocker is resumable. Does NOT yet change the restart decision.
|
|
111
|
+
*/
|
|
112
|
+
restartSafeSessions: string[];
|
|
113
|
+
/** Observability-only split of the current restart blockers: those with no
|
|
114
|
+
* known clean-resume path (the complement of {@link restartSafeSessions}). */
|
|
115
|
+
hardBlockingSessions: string[];
|
|
106
116
|
}
|
|
107
117
|
export interface RestartDeferralState {
|
|
108
118
|
active: boolean;
|
|
@@ -162,6 +172,23 @@ export declare class AutoUpdater {
|
|
|
162
172
|
* Get current auto-updater status.
|
|
163
173
|
*/
|
|
164
174
|
getStatus(): AutoUpdaterStatus;
|
|
175
|
+
/**
|
|
176
|
+
* Restart-safety resolver for the UpdateGate (observability-only — Step 1 of
|
|
177
|
+
* the restart-safe session work). A blocking session is "restart-safe" when
|
|
178
|
+
* its topic resumes cleanly across a server restart. The concrete signal we
|
|
179
|
+
* trust today: the topic has a per-topic autonomous state file
|
|
180
|
+
* (`.instar/autonomous/<topicId>.local.md`) — those sessions are driven by
|
|
181
|
+
* the autonomous loop, which re-injects context via CONTINUATION on the next
|
|
182
|
+
* turn, so a restart costs only a brief re-read rather than lost work.
|
|
183
|
+
*
|
|
184
|
+
* This does NOT change the restart decision — a restart-safe session still
|
|
185
|
+
* blocks exactly as before. It only classifies the blockers so a later step
|
|
186
|
+
* can choose to restart through the safe ones. Fails safe to `false` (treat
|
|
187
|
+
* as a hard blocker) on any error or missing topic id, so a bad check can
|
|
188
|
+
* never make the gate restart through a session it shouldn't.
|
|
189
|
+
* Spec: docs/specs/restart-safe-blocker-classification-spec.md.
|
|
190
|
+
*/
|
|
191
|
+
private isSessionRestartSafe;
|
|
165
192
|
/**
|
|
166
193
|
* Set the Telegram adapter (may be wired after construction).
|
|
167
194
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AutoUpdater.d.ts","sourceRoot":"","sources":["../../src/core/AutoUpdater.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAG1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,
|
|
1
|
+
{"version":3,"file":"AutoUpdater.d.ts","sourceRoot":"","sources":["../../src/core/AutoUpdater.ts"],"names":[],"mappings":"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"}
|
package/dist/core/AutoUpdater.js
CHANGED
|
@@ -96,7 +96,10 @@ export class AutoUpdater {
|
|
|
96
96
|
};
|
|
97
97
|
// Primary-developer mode: the gate inherits restartImmediately so it never
|
|
98
98
|
// defers behind active sessions (default false → fleet behavior unchanged).
|
|
99
|
-
this.gate = new UpdateGate({
|
|
99
|
+
this.gate = new UpdateGate({
|
|
100
|
+
alwaysRestartImmediately: this.config.restartImmediately,
|
|
101
|
+
restartSafetyResolver: (session) => this.isSessionRestartSafe(session),
|
|
102
|
+
});
|
|
100
103
|
this.dampener = new RestartCascadeDampener(this.config.restartCascadeDampenerWindowMs);
|
|
101
104
|
// npx cache detection is no longer needed — updates install to a local
|
|
102
105
|
// shadow directory ({stateDir}/shadow-install/) instead of globally.
|
|
@@ -166,8 +169,38 @@ export class AutoUpdater {
|
|
|
166
169
|
maxDeferralHours: gateStatus.maxDeferralHours,
|
|
167
170
|
restartDeferral: this.restartDeferral ? { ...this.restartDeferral } : null,
|
|
168
171
|
restartImmediately: gateStatus.alwaysRestartImmediately,
|
|
172
|
+
restartSafeSessions: gateStatus.restartSafeSessions,
|
|
173
|
+
hardBlockingSessions: gateStatus.hardBlockingSessions,
|
|
169
174
|
};
|
|
170
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Restart-safety resolver for the UpdateGate (observability-only — Step 1 of
|
|
178
|
+
* the restart-safe session work). A blocking session is "restart-safe" when
|
|
179
|
+
* its topic resumes cleanly across a server restart. The concrete signal we
|
|
180
|
+
* trust today: the topic has a per-topic autonomous state file
|
|
181
|
+
* (`.instar/autonomous/<topicId>.local.md`) — those sessions are driven by
|
|
182
|
+
* the autonomous loop, which re-injects context via CONTINUATION on the next
|
|
183
|
+
* turn, so a restart costs only a brief re-read rather than lost work.
|
|
184
|
+
*
|
|
185
|
+
* This does NOT change the restart decision — a restart-safe session still
|
|
186
|
+
* blocks exactly as before. It only classifies the blockers so a later step
|
|
187
|
+
* can choose to restart through the safe ones. Fails safe to `false` (treat
|
|
188
|
+
* as a hard blocker) on any error or missing topic id, so a bad check can
|
|
189
|
+
* never make the gate restart through a session it shouldn't.
|
|
190
|
+
* Spec: docs/specs/restart-safe-blocker-classification-spec.md.
|
|
191
|
+
*/
|
|
192
|
+
isSessionRestartSafe(session) {
|
|
193
|
+
if (session.topicId == null)
|
|
194
|
+
return false;
|
|
195
|
+
try {
|
|
196
|
+
const stateFile = path.join(process.cwd(), '.instar', 'autonomous', `${session.topicId}.local.md`);
|
|
197
|
+
return fs.existsSync(stateFile);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// @silent-fallback-ok — restart-safety classification fails closed to a hard blocker.
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
171
204
|
/**
|
|
172
205
|
* Set the Telegram adapter (may be wired after construction).
|
|
173
206
|
*/
|