feat(ms-ai-architect): Sesjon 24 — C1 Tier 2 lokal launchd-scheduler (deteksjon-only, ToS-trygt) [skip-docs]
Tier 1 (SessionStart-hook) kjører deteksjon kun når en Claude-sesjon starter.
Tier 2 installerer en lokal launchd LaunchAgent som fyrer SAMME Claude-frie
entrypoint daglig (kl. 03:00), uavhengig av sesjoner = ekte bakgrunn mellom
sesjoner. ToS-trygt: entrypointet kan strukturelt ikke invokere claude.
Søk-først-verifisert (2026-06): launchd > cron (Apple-anbefalt; cron krever
Full Disk Access); StartCalendarInterval fanger opp etter dvale; launchd har
minimalt PATH + ekspanderer ikke ~; process.execPath = Cellar-sti som dør ved
`brew upgrade node` → plistens PATH inkluderer /usr/local/bin.
Levert (TDD, test FØR kode):
- lib/launchd-plist.mjs (REN, SKRIVER ALDRI): renderPlist (XML-escape,
StartCalendarInterval, PATH-fix), defaultLabel, resolvePaths.
- scheduler.mjs (TYNN CLI): install|uninstall|status|run-now|print|run.
Strukturelt Claude-fri (kun process.execPath på run-detection.mjs +
launchctl; kilde-grep-testet). run gater på os_scheduler_cadence →
delegerer til run-detection.mjs. uid-0-nekt; idempotent bootout.
- detection-schedule.mjs (additiv, Tier-1 urørt): os_scheduler_cadence
('daily' default | 'interval') + daysSinceLastPoll ren helper.
- commands/kb-update.md: Tier-1/Tier-2-doc + caveats (uninstall=pause;
re-install etter node-upgrade).
Operatør-valg: daglig default, kadens egen innstilling (settes i onboarding/C2).
Verifisert: launchd-plist 13/13 · detection-schedule 21/21 · kb-update 173 ·
kb-eval 100 · validate 239/0 · test-hooks 6/6 · plutil -lint OK · live
install→status→idempotent→uninstall på maskinen (gui/501, rent).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f8a160585f
commit
071601bb9e
6 changed files with 624 additions and 5 deletions
|
|
@ -19,6 +19,11 @@ export const DEFAULT_SCHEDULE_CONFIG = Object.freeze({
|
|||
enabled: false,
|
||||
interval_days: 7,
|
||||
include_skill_lifecycle: true,
|
||||
// Tier 2 (launchd) cadence. 'daily' = poll on every (daily) fire; 'interval'
|
||||
// = throttle to interval_days via the same staleness gate Tier 1 uses. Read
|
||||
// ONLY by the Tier-2 entrypoint (scheduler.mjs run); the Tier-1 hook ignores
|
||||
// it, so existing behavior is unchanged. Operator default: daily.
|
||||
os_scheduler_cadence: 'daily',
|
||||
});
|
||||
|
||||
// The ordered, Claude-FREE detection pipeline. Every step is a local node
|
||||
|
|
@ -45,6 +50,10 @@ function coerce(key, raw) {
|
|||
const n = Number.parseInt(v, 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : undefined;
|
||||
}
|
||||
if (key === 'os_scheduler_cadence') {
|
||||
// Only 'interval' opts out of daily; any other/garbage value => default daily.
|
||||
return v === 'interval' ? 'interval' : 'daily';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +121,22 @@ export function shouldRunDetection(config, lastPollDaysAgo, opts = {}) {
|
|||
return { run: false, reason: 'fresh (within interval)' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Days since the last sitemap poll, read from a parsed change-report.json.
|
||||
* Pure; mirrors the SessionStart hook's computation. Missing/invalid
|
||||
* last_poll => Infinity ("infinitely stale" => the 'interval' cadence runs).
|
||||
* @param {object|null} report parsed change-report.json ({ last_poll?: string })
|
||||
* @param {number} now Date.now()
|
||||
* @returns {number}
|
||||
*/
|
||||
export function daysSinceLastPoll(report, now) {
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
if (!report || !report.last_poll) return Infinity;
|
||||
const t = new Date(report.last_poll).getTime();
|
||||
if (!Number.isFinite(t)) return Infinity;
|
||||
return (now - t) / DAY_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a compact one-liner summarizing skill-lifecycle signals for the
|
||||
* SessionStart hook. Pure; tolerant of partial/missing reports. Returns null
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue