fix(sync): fresh-machine schedule install, work-matching disclosure line, named drift causes

This commit is contained in:
iamtoruk 2026-08-28 01:26:48 -07:00
parent a9c5d8b6ec
commit 9616bbd0f5
4 changed files with 114 additions and 17 deletions

View file

@ -29,6 +29,7 @@ import {
computeAcceptanceFingerprint,
buildDisclosure,
CORE_SYNC_FIELD_MEANINGS,
detectFingerprintChanges,
type FingerprintInput,
type DisclosureInput,
type Receipt,
@ -622,15 +623,23 @@ export function registerSyncCommands(program: Command): void {
cadence,
disclosure,
attribution: workMatching,
input: fingerprintInput,
},
killed: false,
}
delete config.auto.killed
writeSyncConfig(config)
await installSchedule(cadence, process.argv[1])
process.stdout.write(`Automatic sync enabled (${cadence}). Fingerprint: ${fingerprint}\n`)
try {
await installSchedule(cadence, process.argv[1])
process.stdout.write(`Automatic sync enabled (${cadence}). Fingerprint: ${fingerprint}\n`)
} catch (schedErr) {
process.stderr.write(`Warning: ${(schedErr as Error).message}\n`)
process.stderr.write(`Acceptance was stored and will take effect, but the schedule could not be installed.\n`)
process.stderr.write(`Run: codeburn sync auto enable --cadence ${cadence} --attribution${opts.attribution ? '' : ''}\n`)
process.stderr.write(`Or install manually: launchctl load ~/Library/LaunchAgents/com.codeburn.sync-auto.plist\n`)
process.exit(1)
}
} catch (err) {
process.stderr.write(`${(err as Error).message}\n`)
process.exit(1)
@ -694,12 +703,7 @@ export function registerSyncCommands(program: Command): void {
if (currentFingerprint === accepted.fingerprint) {
process.stdout.write('Current fingerprint: MATCHES\n')
} else {
// Detect what changed
const changed: string[] = []
const baseCoreKeys = Array.from(CORE_SYNC_ATTRIBUTE_KEYS).sort()
if (JSON.stringify(allKeys) !== JSON.stringify(baseCoreKeys)) {
changed.push('field set')
}
const changed = detectFingerprintChanges(accepted.input, fingerprintInput)
process.stdout.write(`Current fingerprint: DIFFERS (${changed.join(', ')})\n`)
}
} catch {
@ -749,16 +753,11 @@ export function registerSyncCommands(program: Command): void {
const currentFingerprint = computeAcceptanceFingerprint(fingerprintInput)
if (currentFingerprint !== accepted.fingerprint) {
// Detect what changed
const changed: string[] = []
const baseCoreKeys = Array.from(CORE_SYNC_ATTRIBUTE_KEYS).sort()
if (JSON.stringify(allKeys) !== JSON.stringify(baseCoreKeys)) {
changed.push('field set')
}
const changed = detectFingerprintChanges(accepted.input, fingerprintInput)
appendReceipt(buildReceipt(
at,
currentFingerprint,
{ result: 'acceptance-required', changed: changed.length > 0 ? changed : ['unknown'] }
{ result: 'acceptance-required', changed }
))
return
}

View file

@ -77,6 +77,9 @@ export function buildDisclosure(input: DisclosureInput): string {
: input.scopeSinceDays === 0
? 'today only'
: `last ${input.scopeSinceDays} days`
const workMatchingText = input.workMatching
? 'on - session-to-commit links are sent'
: 'off'
const fieldsList = input.outboundFields.length > 0
? input.outboundFields
@ -89,6 +92,7 @@ export function buildDisclosure(input: DisclosureInput): string {
`URL: ${input.destinationUrl}`,
`Cadence: ${cadenceText}`,
`Scope: ${scopeText}`,
`Work matching: ${workMatchingText}`,
'',
'Data sent to the endpoint:',
fieldsList,
@ -109,6 +113,7 @@ export interface AcceptanceRecord {
cadence: 'daily' | 'hourly'
disclosure: string
attribution: boolean
input?: FingerprintInput
}
export interface AutoSyncConfig {
@ -141,3 +146,39 @@ export function buildReceipt(at: string, fingerprint: string | undefined, data:
...data,
}
}
export function detectFingerprintChanges(stored: FingerprintInput | undefined, current: FingerprintInput): string[] {
if (!stored) return ['unknown']
const changes: string[] = []
if (stored.destination !== current.destination) {
changes.push('destination')
}
if (stored.cadence !== current.cadence) {
changes.push('cadence')
}
if (stored.scopeSinceDays !== current.scopeSinceDays) {
changes.push('scope')
}
if (stored.workMatching !== current.workMatching) {
changes.push('work matching')
}
const storedFields = new Set(stored.outboundFields)
const currentFields = new Set(current.outboundFields)
const added = Array.from(currentFields).filter(f => !storedFields.has(f)).sort()
const removed = Array.from(storedFields).filter(f => !currentFields.has(f)).sort()
if (added.length > 0 || removed.length > 0) {
const parts: string[] = []
if (added.length > 0) parts.push(`added: ${added.join(', ')}`)
if (removed.length > 0) parts.push(`removed: ${removed.join(', ')}`)
changes.push(`field set (${parts.join(' / ')})`)
}
return changes.length > 0 ? changes : ['unknown']
}

View file

@ -8,7 +8,7 @@
import { platform } from 'os'
import { homedir } from 'os'
import { join } from 'path'
import { writeFileSync, unlinkSync, existsSync } from 'fs'
import { writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs'
import { spawn } from 'child_process'
const SCHEDULE_AGENT_NAME = 'com.codeburn.sync-auto'
@ -67,8 +67,12 @@ export async function installSchedule(
const plistContent = buildLaunchAgentPlist(cadence, codeburnPath)
const plistPath = launchAgentPath()
const agentDir = launchAgentDir()
try {
// Ensure LaunchAgents directory exists
mkdirSync(agentDir, { recursive: true })
// Write the plist
writeFileSync(plistPath, plistContent, { mode: 0o644 })

View file

@ -7,6 +7,7 @@ import {
computeAcceptanceFingerprint,
buildDisclosure,
CORE_SYNC_FIELD_MEANINGS,
detectFingerprintChanges,
type FingerprintInput,
type DisclosureInput,
} from '../src/sync/consent.js'
@ -239,6 +240,58 @@ describe('Disclosure', () => {
}
}
})
it('includes work-matching line when enabled', () => {
const input: DisclosureInput = {
destination: 'test-org',
destinationUrl: 'https://endpoint.example.com',
cadence: 'daily',
outboundFields: [],
workMatching: true,
scopeSinceDays: 7,
}
const disclosure = buildDisclosure(input)
expect(disclosure).toContain('Work matching: on - session-to-commit links are sent')
})
it('includes work-matching line when disabled', () => {
const input: DisclosureInput = {
destination: 'test-org',
destinationUrl: 'https://endpoint.example.com',
cadence: 'daily',
outboundFields: [],
workMatching: false,
scopeSinceDays: 7,
}
const disclosure = buildDisclosure(input)
expect(disclosure).toContain('Work matching: off')
})
it('detects removed field set changes', () => {
const storedInput: FingerprintInput = {
org: 'test-org',
destination: 'https://endpoint.example.com',
outboundFields: ['ai.cost_usd', 'ai.model', 'plugin.custom'],
workMatching: false,
scopeSinceDays: 7,
cadence: 'daily',
}
const currentInput: FingerprintInput = {
org: 'test-org',
destination: 'https://endpoint.example.com',
outboundFields: ['ai.cost_usd', 'ai.model'],
workMatching: false,
scopeSinceDays: 7,
cadence: 'daily',
}
const changes = detectFingerprintChanges(storedInput, currentInput)
expect(changes[0]).toContain('field set')
expect(changes[0]).toContain('removed: plugin.custom')
})
})
describe('Config with auto block', () => {