@intutic/sync-daemon 1.9.0 → 1.10.1
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/README.md +2 -2
- package/dist/agentReporter.d.ts +205 -0
- package/dist/agentReporter.d.ts.map +1 -0
- package/dist/agentReporter.js +394 -0
- package/dist/agentReporter.js.map +1 -0
- package/dist/configReader.d.ts +39 -2
- package/dist/configReader.d.ts.map +1 -1
- package/dist/configReader.js +56 -2
- package/dist/configReader.js.map +1 -1
- package/dist/configWriter.d.ts +51 -1
- package/dist/configWriter.d.ts.map +1 -1
- package/dist/configWriter.js +490 -63
- package/dist/configWriter.js.map +1 -1
- package/dist/harness/antigravityHooks.d.ts.map +1 -1
- package/dist/harness/antigravityHooks.js +33 -55
- package/dist/harness/antigravityHooks.js.map +1 -1
- package/dist/harness/claudeCodeHooks.d.ts +47 -13
- package/dist/harness/claudeCodeHooks.d.ts.map +1 -1
- package/dist/harness/claudeCodeHooks.js +323 -55
- package/dist/harness/claudeCodeHooks.js.map +1 -1
- package/dist/harness/claudeDesktopHooks.d.ts.map +1 -1
- package/dist/harness/claudeDesktopHooks.js +35 -48
- package/dist/harness/claudeDesktopHooks.js.map +1 -1
- package/dist/harness/clineHooks.d.ts.map +1 -1
- package/dist/harness/clineHooks.js +42 -34
- package/dist/harness/clineHooks.js.map +1 -1
- package/dist/harness/codexHooks.d.ts +36 -0
- package/dist/harness/codexHooks.d.ts.map +1 -0
- package/dist/harness/codexHooks.js +227 -0
- package/dist/harness/codexHooks.js.map +1 -0
- package/dist/harness/continueHooks.d.ts +45 -5
- package/dist/harness/continueHooks.d.ts.map +1 -1
- package/dist/harness/continueHooks.js +225 -5
- package/dist/harness/continueHooks.js.map +1 -1
- package/dist/harness/cursorHooks.d.ts +31 -0
- package/dist/harness/cursorHooks.d.ts.map +1 -1
- package/dist/harness/cursorHooks.js +78 -49
- package/dist/harness/cursorHooks.js.map +1 -1
- package/dist/harness/dshHooks.d.ts +198 -0
- package/dist/harness/dshHooks.d.ts.map +1 -0
- package/dist/harness/dshHooks.js +569 -0
- package/dist/harness/dshHooks.js.map +1 -0
- package/dist/harness/gateBody.d.ts +422 -0
- package/dist/harness/gateBody.d.ts.map +1 -0
- package/dist/harness/gateBody.js +1816 -0
- package/dist/harness/gateBody.js.map +1 -0
- package/dist/harness/gateKind.d.ts +164 -0
- package/dist/harness/gateKind.d.ts.map +1 -0
- package/dist/harness/gateKind.js +143 -0
- package/dist/harness/gateKind.js.map +1 -0
- package/dist/harness/githubCopilotHooks.d.ts +54 -0
- package/dist/harness/githubCopilotHooks.d.ts.map +1 -0
- package/dist/harness/githubCopilotHooks.js +218 -0
- package/dist/harness/githubCopilotHooks.js.map +1 -0
- package/dist/harness/gooseHooks.d.ts.map +1 -1
- package/dist/harness/gooseHooks.js +67 -37
- package/dist/harness/gooseHooks.js.map +1 -1
- package/dist/harness/grokHooks.d.ts +77 -0
- package/dist/harness/grokHooks.d.ts.map +1 -0
- package/dist/harness/grokHooks.js +350 -0
- package/dist/harness/grokHooks.js.map +1 -0
- package/dist/harness/hermesHooks.d.ts.map +1 -1
- package/dist/harness/hermesHooks.js +26 -55
- package/dist/harness/hermesHooks.js.map +1 -1
- package/dist/harness/holdRedaction.d.ts +69 -0
- package/dist/harness/holdRedaction.d.ts.map +1 -0
- package/dist/harness/holdRedaction.js +211 -0
- package/dist/harness/holdRedaction.js.map +1 -0
- package/dist/harness/jetbrainsXmlConfig.d.ts +51 -0
- package/dist/harness/jetbrainsXmlConfig.d.ts.map +1 -0
- package/dist/harness/jetbrainsXmlConfig.js +151 -0
- package/dist/harness/jetbrainsXmlConfig.js.map +1 -0
- package/dist/harness/mcpAutoWrite.d.ts +100 -3
- package/dist/harness/mcpAutoWrite.d.ts.map +1 -1
- package/dist/harness/mcpAutoWrite.js +712 -90
- package/dist/harness/mcpAutoWrite.js.map +1 -1
- package/dist/harness/museHooks.d.ts +53 -0
- package/dist/harness/museHooks.d.ts.map +1 -0
- package/dist/harness/museHooks.js +273 -0
- package/dist/harness/museHooks.js.map +1 -0
- package/dist/harness/n8nHooks.d.ts +25 -9
- package/dist/harness/n8nHooks.d.ts.map +1 -1
- package/dist/harness/n8nHooks.js +174 -9
- package/dist/harness/n8nHooks.js.map +1 -1
- package/dist/harness/openCodeHooks.d.ts +93 -0
- package/dist/harness/openCodeHooks.d.ts.map +1 -0
- package/dist/harness/openCodeHooks.js +248 -0
- package/dist/harness/openCodeHooks.js.map +1 -0
- package/dist/harness/openWebuiHooks.d.ts.map +1 -1
- package/dist/harness/openWebuiHooks.js +56 -11
- package/dist/harness/openWebuiHooks.js.map +1 -1
- package/dist/harness/openclawHooks.d.ts.map +1 -1
- package/dist/harness/openclawHooks.js +30 -54
- package/dist/harness/openclawHooks.js.map +1 -1
- package/dist/harness/openhandsHooks.d.ts.map +1 -1
- package/dist/harness/openhandsHooks.js +35 -41
- package/dist/harness/openhandsHooks.js.map +1 -1
- package/dist/harness/piHooks.d.ts.map +1 -1
- package/dist/harness/piHooks.js +31 -58
- package/dist/harness/piHooks.js.map +1 -1
- package/dist/harness/protectedPaths.d.ts +353 -0
- package/dist/harness/protectedPaths.d.ts.map +1 -0
- package/dist/harness/protectedPaths.js +805 -0
- package/dist/harness/protectedPaths.js.map +1 -0
- package/dist/harness/rooCodeHooks.d.ts.map +1 -1
- package/dist/harness/rooCodeHooks.js +34 -48
- package/dist/harness/rooCodeHooks.js.map +1 -1
- package/dist/harness/windsurfHooks.d.ts +72 -0
- package/dist/harness/windsurfHooks.d.ts.map +1 -1
- package/dist/harness/windsurfHooks.js +172 -58
- package/dist/harness/windsurfHooks.js.map +1 -1
- package/dist/harness/windsurfJetBrainsProxy.d.ts +78 -0
- package/dist/harness/windsurfJetBrainsProxy.d.ts.map +1 -0
- package/dist/harness/windsurfJetBrainsProxy.js +159 -0
- package/dist/harness/windsurfJetBrainsProxy.js.map +1 -0
- package/dist/hashReporter.js +1 -1
- package/dist/hashReporter.js.map +1 -1
- package/dist/index.d.ts +12 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -6
- package/dist/index.js.map +1 -1
- package/dist/integrityStore.d.ts +0 -8
- package/dist/integrityStore.d.ts.map +1 -1
- package/dist/integrityStore.js +6 -29
- package/dist/integrityStore.js.map +1 -1
- package/dist/lib/approvedBypasses.d.ts +76 -0
- package/dist/lib/approvedBypasses.d.ts.map +1 -0
- package/dist/lib/approvedBypasses.js +140 -0
- package/dist/lib/approvedBypasses.js.map +1 -0
- package/dist/lib/complianceProbes.d.ts.map +1 -1
- package/dist/lib/complianceProbes.js +5 -0
- package/dist/lib/complianceProbes.js.map +1 -1
- package/dist/lib/decisionsDigest.d.ts +99 -0
- package/dist/lib/decisionsDigest.d.ts.map +1 -0
- package/dist/lib/decisionsDigest.js +199 -0
- package/dist/lib/decisionsDigest.js.map +1 -0
- package/dist/lib/egressPolicy.d.ts +69 -0
- package/dist/lib/egressPolicy.d.ts.map +1 -0
- package/dist/lib/egressPolicy.js +126 -0
- package/dist/lib/egressPolicy.js.map +1 -0
- package/dist/lib/gitWorktrees.d.ts +140 -0
- package/dist/lib/gitWorktrees.d.ts.map +1 -0
- package/dist/lib/gitWorktrees.js +191 -0
- package/dist/lib/gitWorktrees.js.map +1 -0
- package/dist/lib/policySnapshot.d.ts +224 -0
- package/dist/lib/policySnapshot.d.ts.map +1 -0
- package/dist/lib/policySnapshot.js +637 -0
- package/dist/lib/policySnapshot.js.map +1 -0
- package/dist/lib/processPoller.d.ts +44 -0
- package/dist/lib/processPoller.d.ts.map +1 -1
- package/dist/lib/processPoller.js +176 -1
- package/dist/lib/processPoller.js.map +1 -1
- package/dist/lib/runtimeEnv.d.ts +8 -0
- package/dist/lib/runtimeEnv.d.ts.map +1 -1
- package/dist/lib/runtimeEnv.js +3 -0
- package/dist/lib/runtimeEnv.js.map +1 -1
- package/dist/localTrajectoryHeuristics.d.ts +51 -0
- package/dist/localTrajectoryHeuristics.d.ts.map +1 -0
- package/dist/localTrajectoryHeuristics.js +90 -0
- package/dist/localTrajectoryHeuristics.js.map +1 -0
- package/dist/sessionReporter.d.ts +70 -0
- package/dist/sessionReporter.d.ts.map +1 -0
- package/dist/sessionReporter.js +140 -0
- package/dist/sessionReporter.js.map +1 -0
- package/dist/skillWriter.d.ts +1 -1
- package/dist/skillWriter.d.ts.map +1 -1
- package/dist/skillWriter.js +6 -3
- package/dist/skillWriter.js.map +1 -1
- package/dist/syncLoop.d.ts +105 -3
- package/dist/syncLoop.d.ts.map +1 -1
- package/dist/syncLoop.js +397 -65
- package/dist/syncLoop.js.map +1 -1
- package/dist/trajectoryMonitor.d.ts +2 -0
- package/dist/trajectoryMonitor.d.ts.map +1 -1
- package/dist/trajectoryMonitor.js +57 -0
- package/dist/trajectoryMonitor.js.map +1 -1
- package/dist/watcher/driftWatcher.d.ts.map +1 -1
- package/dist/watcher/driftWatcher.js +12 -3
- package/dist/watcher/driftWatcher.js.map +1 -1
- package/dist/watcher/settingsGuard.d.ts +23 -0
- package/dist/watcher/settingsGuard.d.ts.map +1 -1
- package/dist/watcher/settingsGuard.js +202 -17
- package/dist/watcher/settingsGuard.js.map +1 -1
- package/package.json +37 -6
- package/dist/brainIndexer.d.ts +0 -51
- package/dist/brainIndexer.d.ts.map +0 -1
- package/dist/brainIndexer.js +0 -148
- package/dist/brainIndexer.js.map +0 -1
- package/dist/harness/cursorHooksJson.d.ts +0 -29
- package/dist/harness/cursorHooksJson.d.ts.map +0 -1
- package/dist/harness/cursorHooksJson.js +0 -130
- package/dist/harness/cursorHooksJson.js.map +0 -1
- package/dist/harness/gooseHooksWriter.d.ts +0 -27
- package/dist/harness/gooseHooksWriter.d.ts.map +0 -1
- package/dist/harness/gooseHooksWriter.js +0 -147
- package/dist/harness/gooseHooksWriter.js.map +0 -1
- package/dist/harness/openhandsHooksWriter.d.ts +0 -23
- package/dist/harness/openhandsHooksWriter.d.ts.map +0 -1
- package/dist/harness/openhandsHooksWriter.js +0 -146
- package/dist/harness/openhandsHooksWriter.js.map +0 -1
package/dist/syncLoop.js
CHANGED
|
@@ -23,33 +23,165 @@ import * as path from 'node:path';
|
|
|
23
23
|
import * as fs from 'node:fs';
|
|
24
24
|
import * as os from 'node:os';
|
|
25
25
|
import { newIso } from '@intutic/id';
|
|
26
|
-
import {
|
|
26
|
+
import { deriveEnforcementInputs } from '@intutic/shared-types';
|
|
27
|
+
import { writeConfigFiles, HARNESS_FILES, applyConfigEdits, loadLocalSopEntries } from './configWriter.js';
|
|
28
|
+
import { injectMcpServer } from './harness/mcpAutoWrite.js';
|
|
29
|
+
import { discoverWorktrees } from './lib/gitWorktrees.js';
|
|
27
30
|
import { computeFileHashes } from './hashReporter.js';
|
|
28
31
|
import { loadIntegrity, saveIntegrity } from './integrityStore.js';
|
|
29
|
-
import { drainHookEvents } from './harness/claudeCodeHooks.js';
|
|
32
|
+
import { drainHookEvents, drainReviewRequests, REVIEW_REQUESTS_LOG, parseSopConstraints, } from './harness/claudeCodeHooks.js';
|
|
30
33
|
import { writeRuntimeEnv } from './lib/runtimeEnv.js';
|
|
34
|
+
import { refreshPolicySnapshot } from './lib/policySnapshot.js';
|
|
35
|
+
import { refreshApprovedBypasses } from './lib/approvedBypasses.js';
|
|
36
|
+
import { refreshEgressPolicy } from './lib/egressPolicy.js';
|
|
37
|
+
import { refreshDecisionsDigest } from './lib/decisionsDigest.js';
|
|
31
38
|
import { runComplianceProbes } from './lib/complianceProbes.js';
|
|
32
39
|
import { startWatcher } from './watcher/driftWatcher.js';
|
|
33
40
|
import { shouldCaptureThisIteration, captureAndUpload } from './configReader.js';
|
|
34
41
|
import { watch } from 'chokidar';
|
|
35
|
-
import
|
|
42
|
+
// Named import, not default: this package is ESM ("type": "module") and under
|
|
43
|
+
// Node16 resolution the default import of ioredis's CJS typings resolves to the
|
|
44
|
+
// module namespace, which is neither constructable nor usable as a type. The
|
|
45
|
+
// named export is the class itself. (The old `new (Redis as any)(...)` cast
|
|
46
|
+
// silenced exactly this, at the cost of also erasing the instance type.)
|
|
47
|
+
import { Redis } from 'ioredis';
|
|
36
48
|
import { TrajectoryMonitor } from './trajectoryMonitor.js';
|
|
49
|
+
import { collectAgentReport, reportAgent, fetchLocalProxyInstanceId } from './agentReporter.js';
|
|
50
|
+
import { startHarnessSession, endAllOpenSessions } from './sessionReporter.js';
|
|
51
|
+
/** Narrow an unknown thrown value to a printable message. */
|
|
52
|
+
function errorMessage(err) {
|
|
53
|
+
return err instanceof Error ? err.message : String(err);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Structural guard for trace events arriving over the `trace:live:*` Valkey
|
|
57
|
+
* channel. The payload is JSON off the wire, so it is genuinely unknown here;
|
|
58
|
+
* `handleTraceEvent` indexes `sessionId`/`workspaceId` immediately and a
|
|
59
|
+
* malformed publish would otherwise fault inside the buffer bookkeeping.
|
|
60
|
+
*/
|
|
61
|
+
function isTraceEvent(value) {
|
|
62
|
+
if (typeof value !== 'object' || value === null)
|
|
63
|
+
return false;
|
|
64
|
+
const e = value;
|
|
65
|
+
return typeof e['sessionId'] === 'string' && typeof e['workspaceId'] === 'string';
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Turns a non-clean `scanSkillContent` result — already computed as part of
|
|
69
|
+
* `collectAgentReport`'s `facets.skills` — into a `skill_flagged` hook event
|
|
70
|
+
* (TD-358).
|
|
71
|
+
*
|
|
72
|
+
* Deliberately reuses the report the daemon already built for this cycle
|
|
73
|
+
* rather than re-scanning: `agentReporter.ts`'s `collectSkills` runs
|
|
74
|
+
* `scanSkillContent` on every `.agents/skills/**\/SKILL.md` as part of
|
|
75
|
+
* building `facets.skills` already, so this only turns a result the daemon
|
|
76
|
+
* already has into a visible signal — it does not add a second scan pass.
|
|
77
|
+
*
|
|
78
|
+
* Appends directly to `.intutic/events/hook-events.jsonl`, the SAME file
|
|
79
|
+
* every emitted PreToolUse gate's `log_event`/`logEvent` writes to and the
|
|
80
|
+
* SAME mechanism `onDriftDetected` below already uses for `config_tamper` —
|
|
81
|
+
* not a new event pipeline. `drainHookEvents` (`claudeCodeHooks.ts`) picks
|
|
82
|
+
* this line up on its next pass exactly like a gate-written one, because it
|
|
83
|
+
* only ever reads the file; it does not care who appended a given line.
|
|
84
|
+
*
|
|
85
|
+
* Report-only, per `scanSkillContent`'s own doc comment on the unmeasured
|
|
86
|
+
* false-positive rate against real, benign skill markdown: this NEVER
|
|
87
|
+
* blocks, mutates, or removes a skill. `hookEvents.ts`'s
|
|
88
|
+
* `HookEventSchema.event` enum (control plane) counts `skill_flagged`
|
|
89
|
+
* exactly like `tool_flagged` — advisory telemetry, not an incident.
|
|
90
|
+
*
|
|
91
|
+
* A failure to append is caught and warned, never thrown: a governance
|
|
92
|
+
* telemetry write must not be able to abort the sync cycle it rides in on.
|
|
93
|
+
*/
|
|
94
|
+
export function emitSkillFlaggedEvents(opts) {
|
|
95
|
+
const eventsLog = path.join(opts.workspaceRoot, '.intutic', 'events', 'hook-events.jsonl');
|
|
96
|
+
for (const skill of opts.skills) {
|
|
97
|
+
if (!skill.scanned || skill.clean)
|
|
98
|
+
continue;
|
|
99
|
+
const key = `${skill.source}:${skill.name}`;
|
|
100
|
+
if (opts.alreadyEmitted.has(key))
|
|
101
|
+
continue;
|
|
102
|
+
opts.alreadyEmitted.add(key);
|
|
103
|
+
try {
|
|
104
|
+
const ts = newIso();
|
|
105
|
+
const entry = JSON.stringify({
|
|
106
|
+
event: 'skill_flagged',
|
|
107
|
+
toolName: `skill:${skill.name}`,
|
|
108
|
+
reason: `scanSkillContent found ${skill.findingsCount} finding(s) in ` +
|
|
109
|
+
`${skill.source}/${skill.name}/SKILL.md — advisory only, see skillScan.ts`,
|
|
110
|
+
workspaceId: opts.workspaceId,
|
|
111
|
+
harnessType: opts.harnessType,
|
|
112
|
+
timestamp: ts,
|
|
113
|
+
incidentId: crypto
|
|
114
|
+
.createHash('sha1')
|
|
115
|
+
.update(ts + skill.name + opts.workspaceId)
|
|
116
|
+
.digest('hex')
|
|
117
|
+
.slice(0, 16),
|
|
118
|
+
filePath: `${skill.source}/${skill.name}/SKILL.md`,
|
|
119
|
+
}) + '\n';
|
|
120
|
+
fs.appendFileSync(eventsLog, entry, { flag: 'a' });
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
console.warn('[sync-daemon] failed to write skill_flagged event (non-fatal):', err);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
37
127
|
// ─── Default constants ───────────────────────────────────────────────
|
|
38
128
|
const DEFAULT_POLL_INTERVAL_MS = 30_000;
|
|
39
|
-
// ─── Core sync loop ──────────────────────────────────────────────────
|
|
40
129
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
130
|
+
* Best-effort reachability probe for the control plane, modelled on
|
|
131
|
+
* `agentReporter.fetchEgressStatus()`'s bounded-fetch-and-swallow shape.
|
|
43
132
|
*
|
|
44
|
-
*
|
|
133
|
+
* The trajectory monitor used to start on `VALKEY_URL || apiKey` alone —
|
|
134
|
+
* never checking `controlPlaneUrl` at all, so an unset URL produced
|
|
135
|
+
* `undefined/api/v1/...` on every submit attempt. This is the missing check:
|
|
136
|
+
* a monitor that will publish its summaries to a URL that does not resolve
|
|
137
|
+
* (or does not answer) should not start believing it has somewhere to send
|
|
138
|
+
* them.
|
|
139
|
+
*/
|
|
140
|
+
async function controlPlaneIsReachable(controlPlaneUrl) {
|
|
141
|
+
if (!controlPlaneUrl)
|
|
142
|
+
return false;
|
|
143
|
+
try {
|
|
144
|
+
const res = await fetch(`${controlPlaneUrl}/healthz`, { signal: AbortSignal.timeout(2000) });
|
|
145
|
+
return res.ok;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The `review_before:` tokens the snapshot compiles into hold rules: from the
|
|
153
|
+
* synced SOPs' front matter and JSON blocks, the workspace settings, and the
|
|
154
|
+
* local `.intutic/sops`. The same parse the Claude Code writer runs for its
|
|
155
|
+
* pattern blacklist, so the two cannot disagree about what is held.
|
|
45
156
|
*/
|
|
157
|
+
export async function localHoldTokensFor(workspaceRoot, sops, settings, harnesses = []) {
|
|
158
|
+
const local = await loadLocalSopEntries(workspaceRoot, harnesses);
|
|
159
|
+
return parseSopConstraints([...sops, ...local], settings).reviewBefore;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Refresh the three gate-side caches every hook reads: the policy snapshot
|
|
163
|
+
* (`~/.intutic/hooks/policy-snapshot.rules`), the approved review-hold
|
|
164
|
+
* bypasses and the central egress policy. Called on EVERY sync cycle by both
|
|
165
|
+
* loops — this one and `intutic connect`'s — never only when the config
|
|
166
|
+
* version moved: policy changes without the version changing, and before
|
|
167
|
+
* this helper existed `connect` refreshed once at startup and never again, so
|
|
168
|
+
* a guardrail promoted mid-session reached a connected machine only on
|
|
169
|
+
* restart (TD-488). None of the three throw; the return says which of them
|
|
170
|
+
* landed.
|
|
171
|
+
*/
|
|
172
|
+
export async function refreshGateCaches(opts) {
|
|
173
|
+
const snapshot = (await refreshPolicySnapshot(opts)) !== null;
|
|
174
|
+
const bypasses = (await refreshApprovedBypasses(opts)) !== null;
|
|
175
|
+
const egress = (await refreshEgressPolicy(opts)) !== null;
|
|
176
|
+
return { snapshot, bypasses, egress };
|
|
177
|
+
}
|
|
46
178
|
export async function startSyncLoop(options) {
|
|
47
|
-
const { controlPlaneUrl, apiKey, workspaceId, workspaceRoot, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS, onSync, signal,
|
|
179
|
+
const { controlPlaneUrl, apiKey, workspaceId, workspaceRoot, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS, onSync, signal, } = options;
|
|
48
180
|
// Step -1: Start the Trajectory Monitor & Valkey Subscriber
|
|
49
181
|
let trajectoryMonitor = null;
|
|
50
182
|
let trajectorySubscriber = null;
|
|
51
|
-
|
|
52
|
-
|
|
183
|
+
const valkeyUrl = process.env.VALKEY_URL ?? 'redis://127.0.0.1:6379';
|
|
184
|
+
if ((process.env.VALKEY_URL || apiKey) && (await controlPlaneIsReachable(controlPlaneUrl))) {
|
|
53
185
|
trajectoryMonitor = new TrajectoryMonitor({
|
|
54
186
|
valkeyUrl,
|
|
55
187
|
controlPlaneUrl,
|
|
@@ -61,9 +193,13 @@ export async function startSyncLoop(options) {
|
|
|
61
193
|
await trajectoryMonitor.start();
|
|
62
194
|
trajectorySubscriber = new Redis(valkeyUrl);
|
|
63
195
|
await trajectorySubscriber.psubscribe('trace:live:*');
|
|
64
|
-
trajectorySubscriber.on('pmessage', (
|
|
196
|
+
trajectorySubscriber.on('pmessage', (_pattern, _channel, message) => {
|
|
65
197
|
try {
|
|
66
198
|
const event = JSON.parse(message);
|
|
199
|
+
if (!isTraceEvent(event)) {
|
|
200
|
+
console.warn('[sync-daemon] Discarding malformed trajectory trace event');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
67
203
|
trajectoryMonitor?.handleTraceEvent(event);
|
|
68
204
|
}
|
|
69
205
|
catch (err) {
|
|
@@ -82,14 +218,32 @@ export async function startSyncLoop(options) {
|
|
|
82
218
|
let latestSops = [];
|
|
83
219
|
let latestProxyUrl = '';
|
|
84
220
|
let latestHarnesses = [];
|
|
221
|
+
let latestGovernanceInputs = {};
|
|
85
222
|
// Step 0: Write runtime env file (hook scripts source this for credentials)
|
|
86
223
|
// Runs once at startup and then again on every iteration.
|
|
87
224
|
try {
|
|
88
|
-
|
|
225
|
+
// The Valkey URL rides along only when this loop is itself using one
|
|
226
|
+
// (the trajectory monitor connected): the MCP proxies share their session
|
|
227
|
+
// window through it (Wave 5.3, TD-437).
|
|
228
|
+
await writeRuntimeEnv({ controlPlaneUrl, apiKey, workspaceId, valkeyUrl: trajectoryMonitor ? valkeyUrl : undefined });
|
|
89
229
|
}
|
|
90
230
|
catch (err) {
|
|
91
231
|
console.warn('[sync-daemon] Could not write runtime env file (non-fatal):', err);
|
|
92
232
|
}
|
|
233
|
+
// Step 0b–0d: Refresh the three gate-side caches every hook reads —
|
|
234
|
+
// policy snapshot, approved review_before bypasses, central egress policy.
|
|
235
|
+
//
|
|
236
|
+
// Deliberately here beside writeRuntimeEnv, and deliberately NOT inside
|
|
237
|
+
// writeConfigFiles: that path is gated on the config version, so a snapshot
|
|
238
|
+
// written there would go stale by construction on every cycle where the
|
|
239
|
+
// version did not move. Policy changes without the config version changing,
|
|
240
|
+
// and a bypass an operator just approved has to reach the gate before the
|
|
241
|
+
// developer's very next retry. One helper, shared with `intutic connect`'s
|
|
242
|
+
// own loop, so the two cannot drift apart again (TD-488).
|
|
243
|
+
await refreshGateCaches({
|
|
244
|
+
controlPlaneUrl, apiKey, workspaceId,
|
|
245
|
+
localHoldTokens: await localHoldTokensFor(workspaceRoot, [], undefined),
|
|
246
|
+
});
|
|
93
247
|
// Try to sync offline traces back to PostgreSQL
|
|
94
248
|
try {
|
|
95
249
|
await syncOfflineTraces(controlPlaneUrl, apiKey);
|
|
@@ -101,6 +255,10 @@ export async function startSyncLoop(options) {
|
|
|
101
255
|
// Drains immediately when the hook-events.jsonl file changes (written by hook scripts).
|
|
102
256
|
// Falls back to a 60s safety-net interval for NFS/Docker mounts where inotify may not fire.
|
|
103
257
|
const hookEventsLog = `${workspaceRoot}/.intutic/events/hook-events.jsonl`;
|
|
258
|
+
// The review-hold log, watched alongside it. Two named files rather than the
|
|
259
|
+
// directory: a directory watch would also fire on `.rejected.jsonl`, which the
|
|
260
|
+
// drain itself writes, and a drain that retriggers itself is a loop.
|
|
261
|
+
const reviewHoldsLog = `${workspaceRoot}/${REVIEW_REQUESTS_LOG}`;
|
|
104
262
|
let fsWatcher = null;
|
|
105
263
|
let drainSafetyTimer = null;
|
|
106
264
|
const runDrain = async () => {
|
|
@@ -113,9 +271,20 @@ export async function startSyncLoop(options) {
|
|
|
113
271
|
catch (err) {
|
|
114
272
|
console.warn('[sync-daemon] Hook event drain error (non-fatal):', err);
|
|
115
273
|
}
|
|
274
|
+
// Separate try: a failure draining events must not skip the holds. They go
|
|
275
|
+
// to different endpoints and one being down says nothing about the other.
|
|
276
|
+
try {
|
|
277
|
+
const held = await drainReviewRequests(workspaceRoot, controlPlaneUrl, apiKey);
|
|
278
|
+
if (held > 0) {
|
|
279
|
+
console.log(`[sync-daemon] Drained ${held} review hold(s) to control plane`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
console.warn('[sync-daemon] Review hold drain error (non-fatal):', err);
|
|
284
|
+
}
|
|
116
285
|
};
|
|
117
286
|
try {
|
|
118
|
-
fsWatcher = watch(hookEventsLog, { ignoreInitial: true, persistent: false });
|
|
287
|
+
fsWatcher = watch([hookEventsLog, reviewHoldsLog], { ignoreInitial: true, persistent: false });
|
|
119
288
|
fsWatcher.on('change', runDrain);
|
|
120
289
|
fsWatcher.on('add', runDrain);
|
|
121
290
|
}
|
|
@@ -173,7 +342,6 @@ export async function startSyncLoop(options) {
|
|
|
173
342
|
workspaceId,
|
|
174
343
|
workspaceRoot,
|
|
175
344
|
onSync,
|
|
176
|
-
adapterId,
|
|
177
345
|
});
|
|
178
346
|
// Update cached state for drift handler
|
|
179
347
|
if (result) {
|
|
@@ -181,6 +349,7 @@ export async function startSyncLoop(options) {
|
|
|
181
349
|
latestSops = result.sops ?? [];
|
|
182
350
|
latestProxyUrl = result.proxyUrl ?? '';
|
|
183
351
|
latestHarnesses = result.harnesses ?? [];
|
|
352
|
+
latestGovernanceInputs = result.harnessGovernanceInputs ?? {};
|
|
184
353
|
await writeRuntimeEnv({
|
|
185
354
|
controlPlaneUrl,
|
|
186
355
|
apiKey,
|
|
@@ -188,7 +357,32 @@ export async function startSyncLoop(options) {
|
|
|
188
357
|
mcpProxyFailBehavior: result.settings?.mcpProxyFailBehavior,
|
|
189
358
|
mcpProxyMode: result.settings?.mcpProxyMode,
|
|
190
359
|
bypassEnforcementTier: result.settings?.bypassEnforcementTier,
|
|
360
|
+
valkeyUrl: trajectoryMonitor ? valkeyUrl : undefined,
|
|
361
|
+
});
|
|
362
|
+
// Same reasoning as Step 0b–0d: policy rides the sync cycle, not the
|
|
363
|
+
// config version.
|
|
364
|
+
await refreshGateCaches({
|
|
365
|
+
controlPlaneUrl, apiKey, workspaceId,
|
|
366
|
+
localHoldTokens: await localHoldTokensFor(workspaceRoot, latestSops, latestSettings, latestHarnesses),
|
|
191
367
|
});
|
|
368
|
+
// Governed decisions log — opt-in only (WorkspaceSettings.decisionsLogEnabled,
|
|
369
|
+
// default off: a growing context file is token spend the product must not
|
|
370
|
+
// silently impose on every workspace). No Step-0-equivalent unconditional
|
|
371
|
+
// call before the loop, unlike egress/policy/bypasses above: this gate can
|
|
372
|
+
// only be evaluated once `result.settings` is known, which is not yet true
|
|
373
|
+
// before the first iteration completes. When disabled, this is simply not
|
|
374
|
+
// called — existing files (if any, from before the workspace disabled the
|
|
375
|
+
// feature) are left as last written rather than force-deleted or overwritten
|
|
376
|
+
// with a stale/wrong "disabled" digest.
|
|
377
|
+
if (result.settings?.decisionsLogEnabled) {
|
|
378
|
+
await refreshDecisionsDigest({
|
|
379
|
+
controlPlaneUrl,
|
|
380
|
+
apiKey,
|
|
381
|
+
workspaceId,
|
|
382
|
+
workspaceRoot,
|
|
383
|
+
harnesses: latestHarnesses,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
192
386
|
}
|
|
193
387
|
// Start the drift watcher on first successful sync (once harnesses are known)
|
|
194
388
|
if (!driftWatcher && latestHarnesses.length > 0) {
|
|
@@ -216,7 +410,7 @@ export async function startSyncLoop(options) {
|
|
|
216
410
|
// 4b. Capture harness configs back to control plane (every Nth iteration)
|
|
217
411
|
if (shouldCaptureThisIteration(iterationCount)) {
|
|
218
412
|
try {
|
|
219
|
-
await captureAndUpload(controlPlaneUrl, apiKey, workspaceId, workspaceRoot, latestHarnesses);
|
|
413
|
+
await captureAndUpload(controlPlaneUrl, apiKey, workspaceId, workspaceRoot, latestHarnesses, latestGovernanceInputs);
|
|
220
414
|
}
|
|
221
415
|
catch (err) {
|
|
222
416
|
console.warn('[sync-daemon] Config capture failed (non-fatal):', err);
|
|
@@ -234,6 +428,9 @@ export async function startSyncLoop(options) {
|
|
|
234
428
|
driftWatcher?.stop();
|
|
235
429
|
if (drainSafetyTimer)
|
|
236
430
|
clearInterval(drainSafetyTimer);
|
|
431
|
+
// Mark this run's harness sessions ended so the dashboard's live list
|
|
432
|
+
// reflects reality.
|
|
433
|
+
await endAllOpenSessions(controlPlaneUrl, apiKey);
|
|
237
434
|
}
|
|
238
435
|
/**
|
|
239
436
|
* Execute a single sync iteration:
|
|
@@ -243,9 +440,16 @@ export async function startSyncLoop(options) {
|
|
|
243
440
|
* 4. Report status back to control plane
|
|
244
441
|
* 5. Update local integrity store
|
|
245
442
|
* 6. Call onSync callback
|
|
443
|
+
*
|
|
444
|
+
* Exported (in addition to being used internally by `startSyncLoop`'s
|
|
445
|
+
* polling loop) as a test seam — it is the unit that exercises exactly one
|
|
446
|
+
* cycle's worth of work, including the injectMcpServer call (step 3c),
|
|
447
|
+
* without the surrounding while-loop's extra one-time/Nth-iteration network
|
|
448
|
+
* calls (policy snapshot, approved bypasses, egress policy, compliance
|
|
449
|
+
* probes, drift watcher, decisions digest, config capture).
|
|
246
450
|
*/
|
|
247
|
-
async function runSyncIteration(ctx) {
|
|
248
|
-
const { controlPlaneUrl, apiKey, workspaceId, workspaceRoot, onSync
|
|
451
|
+
export async function runSyncIteration(ctx) {
|
|
452
|
+
const { controlPlaneUrl, apiKey, workspaceId, workspaceRoot, onSync } = ctx;
|
|
249
453
|
// 1. Fetch config from control plane
|
|
250
454
|
const config = await fetchConfig(controlPlaneUrl, apiKey, workspaceId);
|
|
251
455
|
// 2. Load local integrity store
|
|
@@ -276,8 +480,21 @@ async function runSyncIteration(ctx) {
|
|
|
276
480
|
: config.appliedEdits.filter(edit => !localAppliedIds.includes(edit.suggestionId));
|
|
277
481
|
if (toApply.length > 0) {
|
|
278
482
|
try {
|
|
279
|
-
await applyConfigEdits(workspaceRoot, toApply, config.settings.bypassEnforcementTier);
|
|
280
|
-
|
|
483
|
+
const results = await applyConfigEdits(workspaceRoot, toApply, config.settings.bypassEnforcementTier);
|
|
484
|
+
// Ack every result back to control-plane (TD-349). Best-effort per
|
|
485
|
+
// suggestion — one failed ack must not block the others or abort
|
|
486
|
+
// the sync cycle; see reportApplyResult's own non-fatal handling.
|
|
487
|
+
for (const result of results) {
|
|
488
|
+
await reportApplyResult(controlPlaneUrl, apiKey, result.suggestionId, result);
|
|
489
|
+
}
|
|
490
|
+
// Only remember ids that fully landed (every operation for that
|
|
491
|
+
// suggestion applied). Recording every ATTEMPTED id regardless of
|
|
492
|
+
// outcome (the old behavior) meant a fuzzy-match miss was silently
|
|
493
|
+
// never retried — the id was in applied-suggestions.json forever,
|
|
494
|
+
// and forceApply is the only other thing that re-attempts an id
|
|
495
|
+
// already in this file. A failed edit must remain retryable.
|
|
496
|
+
const landedIds = results.filter((r) => r.ok).map((r) => r.suggestionId);
|
|
497
|
+
const newlyAppliedIds = Array.from(new Set([...localAppliedIds, ...landedIds]));
|
|
281
498
|
try {
|
|
282
499
|
fs.mkdirSync(path.dirname(appliedSuggestionsPath), { recursive: true });
|
|
283
500
|
fs.writeFileSync(appliedSuggestionsPath, JSON.stringify(newlyAppliedIds, null, 2), 'utf-8');
|
|
@@ -291,6 +508,56 @@ async function runSyncIteration(ctx) {
|
|
|
291
508
|
}
|
|
292
509
|
}
|
|
293
510
|
}
|
|
511
|
+
// 3c. Proxy-wrap MCP servers across all supported harness configs. This is
|
|
512
|
+
// a continuous invariant, not a one-shot done only at `connect` time: a
|
|
513
|
+
// server a user adds to a harness config after their first `connect` would
|
|
514
|
+
// otherwise never get wrapped and stay invisible to governance. Safe to run
|
|
515
|
+
// every cycle because mcpAutoWrite's writeJsonFile is write-if-changed — an
|
|
516
|
+
// already-wrapped, unchanged config writes zero bytes. Non-fatal, matching
|
|
517
|
+
// step 3b above: a failure here must not abort the sync cycle.
|
|
518
|
+
try {
|
|
519
|
+
await injectMcpServer(workspaceRoot, workspaceId);
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
console.warn('[sync-daemon] injectMcpServer failed (non-fatal):', err);
|
|
523
|
+
}
|
|
524
|
+
// 3d. Worktree propagation (O1). `workspaceRoot` above only ever covers the
|
|
525
|
+
// ONE checkout this daemon was pointed at — a `git worktree add`-created
|
|
526
|
+
// sibling checkout has its own independent working tree, and every
|
|
527
|
+
// project-tier governance file this product writes is untracked, so none
|
|
528
|
+
// of it exists in a worktree by default (see gitWorktrees.ts's module doc
|
|
529
|
+
// for the full mechanics and why Xirp is what surfaced this). Discovering
|
|
530
|
+
// worktrees fresh every cycle — never caching the list across
|
|
531
|
+
// iterations — is what makes worktree REMOVAL fall out for free: a
|
|
532
|
+
// worktree `git worktree remove`d since the last cycle simply stops being
|
|
533
|
+
// returned here, with no stale-entry cleanup logic required.
|
|
534
|
+
//
|
|
535
|
+
// Config-content writes reuse the SAME `config.configVersion > localVersion`
|
|
536
|
+
// gate step 3 already used — every worktree of one repo shares the one
|
|
537
|
+
// control-plane config, so re-deriving per-worktree versioning would just
|
|
538
|
+
// reproduce a decision already made above. `injectMcpServer` runs
|
|
539
|
+
// unconditionally per worktree, same as it does for the main root: it is
|
|
540
|
+
// write-if-changed internally (mcpAutoWrite's writeJsonFile), so an
|
|
541
|
+
// unchanged worktree costs a few file reads and no writes.
|
|
542
|
+
try {
|
|
543
|
+
const worktrees = await discoverWorktrees(workspaceRoot);
|
|
544
|
+
for (const worktreeRoot of worktrees) {
|
|
545
|
+
if (worktreeRoot === workspaceRoot)
|
|
546
|
+
continue; // already covered above
|
|
547
|
+
try {
|
|
548
|
+
if (config.configVersion > localVersion) {
|
|
549
|
+
await writeConfigFiles(worktreeRoot, config.sops, config.proxyUrl, extractHarnesses(config));
|
|
550
|
+
}
|
|
551
|
+
await injectMcpServer(worktreeRoot, workspaceId);
|
|
552
|
+
}
|
|
553
|
+
catch (err) {
|
|
554
|
+
console.warn(`[sync-daemon] worktree propagation failed for ${worktreeRoot} (non-fatal):`, err);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
catch (err) {
|
|
559
|
+
console.warn('[sync-daemon] discoverWorktrees failed (non-fatal):', err);
|
|
560
|
+
}
|
|
294
561
|
// 4. Compute file hashes for drift detection
|
|
295
562
|
const harnesses = extractHarnesses(config);
|
|
296
563
|
const canonicalHashes = buildCanonicalHashMap(config);
|
|
@@ -306,6 +573,71 @@ async function runSyncIteration(ctx) {
|
|
|
306
573
|
await reportStatus(controlPlaneUrl, apiKey, workspaceId, harness, harnessHashes);
|
|
307
574
|
}
|
|
308
575
|
}
|
|
576
|
+
// 5b. Register each harness as a durable agent with its facets, so the
|
|
577
|
+
// dashboard agent graph + posture ring stay live. Best-effort: a failed
|
|
578
|
+
// report never blocks the sync loop.
|
|
579
|
+
//
|
|
580
|
+
// Skills scanned this cycle whose finding has already been emitted, keyed
|
|
581
|
+
// `source:name`. `collectAgentReport` recomputes the same workspace-wide
|
|
582
|
+
// skill scan once per harness in this loop — a skill is not harness-
|
|
583
|
+
// specific — so without this a workspace with two active harnesses would
|
|
584
|
+
// double-emit every flagged skill every cycle.
|
|
585
|
+
const skillFlaggedThisCycle = new Set();
|
|
586
|
+
// Per-harness governance-coverage enforcement signals for this cycle,
|
|
587
|
+
// derived from the same facets `collectAgentReport` just computed — see
|
|
588
|
+
// `SyncResult.harnessGovernanceInputs`'s doc comment. Uses the shared
|
|
589
|
+
// `deriveEnforcementInputs` (`@intutic/shared-types`, TD-443) so this
|
|
590
|
+
// mapping cannot drift from `harnessGradeSweep.ts`'s control-plane-side
|
|
591
|
+
// sweep the way the two hand-kept copies previously did — safe to depend
|
|
592
|
+
// on since `@intutic/shared-types` is a leaf package, not
|
|
593
|
+
// `services/control-plane` itself, so this publicly-mirrored module still
|
|
594
|
+
// never imports from the enterprise-only service.
|
|
595
|
+
const harnessGovernanceInputs = {};
|
|
596
|
+
// The local proxy's instance id, read once per iteration (not per harness):
|
|
597
|
+
// step 5c registers each harness's git/task context onto that process's own
|
|
598
|
+
// session row (TD-231, Wave 5.6). `null` — no local proxy, or a shared
|
|
599
|
+
// gateway — keeps the `ses_` fallback. Skipped when there is no harness to
|
|
600
|
+
// report, so a workspace with no SOP targets never probes the proxy.
|
|
601
|
+
const proxyInstanceId = harnesses.length > 0 ? await fetchLocalProxyInstanceId() : null;
|
|
602
|
+
for (const harness of harnesses) {
|
|
603
|
+
const filename = HARNESS_FILES[harness];
|
|
604
|
+
const configSynced = filename
|
|
605
|
+
? !fileHashes.some((f) => f.filePath === filename && f.drifted)
|
|
606
|
+
: false;
|
|
607
|
+
const report = await collectAgentReport({
|
|
608
|
+
workspaceRoot,
|
|
609
|
+
harnessType: harness,
|
|
610
|
+
configSynced,
|
|
611
|
+
dlpEnabled: true, // the daemon-managed proxy ships DLP on by default
|
|
612
|
+
policyEnforced: Boolean(controlPlaneUrl),
|
|
613
|
+
allowLocalVaults: config.settings?.allowLocalMemoryVaults,
|
|
614
|
+
});
|
|
615
|
+
await reportAgent(controlPlaneUrl, apiKey, workspaceId, report);
|
|
616
|
+
harnessGovernanceInputs[harness] = deriveEnforcementInputs(report.facets);
|
|
617
|
+
// 5b-i. Skill-content scan findings, surfaced as `skill_flagged` events
|
|
618
|
+
// (TD-358). See `emitSkillFlaggedEvents`'s own doc comment for why this
|
|
619
|
+
// rides the existing hook-events drain rather than a new pipeline.
|
|
620
|
+
emitSkillFlaggedEvents({
|
|
621
|
+
workspaceRoot,
|
|
622
|
+
workspaceId,
|
|
623
|
+
harnessType: harness,
|
|
624
|
+
skills: report.facets.skills,
|
|
625
|
+
alreadyEmitted: skillFlaggedThisCycle,
|
|
626
|
+
});
|
|
627
|
+
// 5c. Report the harness session (once per harness per proxy process —
|
|
628
|
+
// the reporter dedupes). This is what switches on branch/commit capture
|
|
629
|
+
// and the task-context cascade. With a local proxy the context lands on
|
|
630
|
+
// the proxy's own row, the one its traces are filed under; without one a
|
|
631
|
+
// `ses_` row is opened (see sessionReporter.ts).
|
|
632
|
+
await startHarnessSession({
|
|
633
|
+
controlPlaneUrl,
|
|
634
|
+
apiKey,
|
|
635
|
+
workspaceId,
|
|
636
|
+
harnessType: harness,
|
|
637
|
+
workspaceRoot,
|
|
638
|
+
...(proxyInstanceId ? { proxyInstanceId } : {}),
|
|
639
|
+
});
|
|
640
|
+
}
|
|
309
641
|
// 6. Update local integrity store
|
|
310
642
|
const newFiles = {};
|
|
311
643
|
for (const fh of fileHashes) {
|
|
@@ -318,48 +650,13 @@ async function runSyncIteration(ctx) {
|
|
|
318
650
|
configVersion: config.configVersion,
|
|
319
651
|
files: newFiles,
|
|
320
652
|
});
|
|
321
|
-
// 6b. ContextGraph
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const delta = BrainIndexer.computeDelta(scanResult, contextIntegrity);
|
|
329
|
-
if (delta.upserted.length > 0 || delta.deleted.length > 0) {
|
|
330
|
-
const syncUrl = `${controlPlaneUrl}/api/v1/context/sync`;
|
|
331
|
-
const res = await fetch(syncUrl, {
|
|
332
|
-
method: 'POST',
|
|
333
|
-
headers: {
|
|
334
|
-
'Content-Type': 'application/json',
|
|
335
|
-
Authorization: `Bearer ${apiKey}`,
|
|
336
|
-
},
|
|
337
|
-
body: JSON.stringify({
|
|
338
|
-
adapterId,
|
|
339
|
-
delta,
|
|
340
|
-
}),
|
|
341
|
-
signal: AbortSignal.timeout(15_000),
|
|
342
|
-
});
|
|
343
|
-
if (res.ok) {
|
|
344
|
-
const newContextFiles = {};
|
|
345
|
-
for (const [filePath, file] of Object.entries(scanResult.files)) {
|
|
346
|
-
newContextFiles[filePath] = file.hash;
|
|
347
|
-
}
|
|
348
|
-
await saveContextIntegrity(workspaceRoot, {
|
|
349
|
-
lastSyncAt: newIso(),
|
|
350
|
-
configVersion: config.configVersion,
|
|
351
|
-
files: newContextFiles,
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
else {
|
|
355
|
-
console.warn(`[sync-daemon] ContextGraph sync failed: ${res.status} ${res.statusText}`);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
catch (err) {
|
|
360
|
-
console.warn('[sync-daemon] ContextGraph sync error:', err);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
653
|
+
// 6b. ContextGraph indexing — REMOVED.
|
|
654
|
+
//
|
|
655
|
+
// This block scanned the whole workspace every sync cycle and POSTed the
|
|
656
|
+
// delta to /api/v1/context/sync, an ingest stripped from the control plane
|
|
657
|
+
// with the Context Graph feature (commit 9cfc0200). Worse than dead: the
|
|
658
|
+
// integrity file is only written on a 2xx, so every cycle re-scanned and
|
|
659
|
+
// re-posted the same delta forever on every developer machine.
|
|
363
660
|
// 6c. Hook event drain is now FSEvents-driven (see startSyncLoop).
|
|
364
661
|
// No per-iteration drain needed here; the chokidar watcher handles it.
|
|
365
662
|
// 7. Invoke onSync callback
|
|
@@ -372,6 +669,7 @@ async function runSyncIteration(ctx) {
|
|
|
372
669
|
sops: config.sops,
|
|
373
670
|
proxyUrl: config.proxyUrl,
|
|
374
671
|
harnesses,
|
|
672
|
+
harnessGovernanceInputs,
|
|
375
673
|
};
|
|
376
674
|
onSync?.(result);
|
|
377
675
|
return result;
|
|
@@ -424,6 +722,40 @@ async function reportStatus(controlPlaneUrl, apiKey, workspaceId, harnessType, f
|
|
|
424
722
|
console.warn(`[sync-daemon] reportStatus failed for ${harnessType}: ${res.status} ${res.statusText}`);
|
|
425
723
|
}
|
|
426
724
|
}
|
|
725
|
+
/**
|
|
726
|
+
* Ack a SkillOpt config-edit apply outcome back to the control plane
|
|
727
|
+
* (TD-349) — POST /api/v1/skillopt/:suggestionId/apply-result.
|
|
728
|
+
*
|
|
729
|
+
* Same HTTP call shape as `reportGovernanceCoverageSnapshot`
|
|
730
|
+
* (configReader.ts): `fetch` + `AbortSignal.timeout(10_000)`, fully
|
|
731
|
+
* non-fatal. This ack closing the loop is important, but it must never be
|
|
732
|
+
* allowed to abort a sync cycle that otherwise succeeded — a dropped ack
|
|
733
|
+
* just means the suggestion re-syncs and gets acked again next cycle
|
|
734
|
+
* (getSyncConfig re-pushes 'applied' and 'apply_unconfirmed' rows alike).
|
|
735
|
+
*/
|
|
736
|
+
async function reportApplyResult(controlPlaneUrl, apiKey, suggestionId, result) {
|
|
737
|
+
try {
|
|
738
|
+
const res = await fetch(`${controlPlaneUrl}/api/v1/skillopt/${suggestionId}/apply-result`, {
|
|
739
|
+
method: 'POST',
|
|
740
|
+
headers: {
|
|
741
|
+
'Content-Type': 'application/json',
|
|
742
|
+
Authorization: `Bearer ${apiKey}`,
|
|
743
|
+
},
|
|
744
|
+
body: JSON.stringify({
|
|
745
|
+
ok: result.ok,
|
|
746
|
+
perOperation: result.perOperation,
|
|
747
|
+
}),
|
|
748
|
+
signal: AbortSignal.timeout(10_000),
|
|
749
|
+
});
|
|
750
|
+
if (!res.ok) {
|
|
751
|
+
const body = await res.text().catch(() => '');
|
|
752
|
+
console.warn(`[sync-daemon] reportApplyResult failed for ${suggestionId} (${res.status}): ${body}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
catch (err) {
|
|
756
|
+
console.warn(`[sync-daemon] reportApplyResult request failed for ${suggestionId}:`, err);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
427
759
|
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
428
760
|
/**
|
|
429
761
|
* Extract unique harness types from a sync config payload.
|
|
@@ -492,7 +824,7 @@ export async function syncOfflineTraces(controlPlaneUrl, apiKey) {
|
|
|
492
824
|
fs.renameSync(originalPath, syncingPath);
|
|
493
825
|
}
|
|
494
826
|
catch (renameErr) {
|
|
495
|
-
console.error(`[sync-daemon] Failed to lock/rename file ${file}:`, renameErr
|
|
827
|
+
console.error(`[sync-daemon] Failed to lock/rename file ${file}:`, errorMessage(renameErr));
|
|
496
828
|
continue;
|
|
497
829
|
}
|
|
498
830
|
try {
|
|
@@ -526,7 +858,7 @@ export async function syncOfflineTraces(controlPlaneUrl, apiKey) {
|
|
|
526
858
|
console.log(`[sync-daemon] Successfully synced ${traces.length} offline traces back from ${file}.`);
|
|
527
859
|
}
|
|
528
860
|
catch (err) {
|
|
529
|
-
console.error(`[sync-daemon] Failed to sync offline traces back from ${file}:`, err
|
|
861
|
+
console.error(`[sync-daemon] Failed to sync offline traces back from ${file}:`, errorMessage(err));
|
|
530
862
|
// Revert rename on failure to allow retry on next cycle
|
|
531
863
|
try {
|
|
532
864
|
if (fs.existsSync(syncingPath)) {
|
|
@@ -534,13 +866,13 @@ export async function syncOfflineTraces(controlPlaneUrl, apiKey) {
|
|
|
534
866
|
}
|
|
535
867
|
}
|
|
536
868
|
catch (revertErr) {
|
|
537
|
-
console.error(`[sync-daemon] Failed to revert rename for ${file}:`, revertErr
|
|
869
|
+
console.error(`[sync-daemon] Failed to revert rename for ${file}:`, errorMessage(revertErr));
|
|
538
870
|
}
|
|
539
871
|
}
|
|
540
872
|
}
|
|
541
873
|
}
|
|
542
874
|
catch (err) {
|
|
543
|
-
console.error(`[sync-daemon] Failed to scan offline traces directory:`, err
|
|
875
|
+
console.error(`[sync-daemon] Failed to scan offline traces directory:`, errorMessage(err));
|
|
544
876
|
}
|
|
545
877
|
}
|
|
546
878
|
//# sourceMappingURL=syncLoop.js.map
|