@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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* egressPolicy.ts — ships the workspace's central egress policy *to* the
|
|
3
|
+
* machine, so an admin sets `enforce`/`monitor` + an allow list once in the
|
|
4
|
+
* control plane rather than in every developer's local proxy config
|
|
5
|
+
* (LLD #63 §4). Mirrors `policySnapshot.ts` / `approvedBypasses.ts`: fetch from
|
|
6
|
+
* the control plane, write a local artifact under `.intutic/hooks` with
|
|
7
|
+
* integrity headers, and let the consumer (here the Rust proxy) read it.
|
|
8
|
+
*
|
|
9
|
+
* # The contract with the proxy
|
|
10
|
+
*
|
|
11
|
+
* `.intutic/hooks/egress-policy.json` — same directory as the policy snapshot
|
|
12
|
+
* and for the same reason: `.intutic/hooks` is in `UNIVERSAL_PROTECTED_PATHS`,
|
|
13
|
+
* so a gate already refuses an agent's own write there. The file carries its
|
|
14
|
+
* own integrity: `digest` (sha256 of a canonical `mode\nallow…` string, first
|
|
15
|
+
* 32 hex) and `workspace`. The proxy recomputes the digest and refuses a file
|
|
16
|
+
* whose digest or workspace does not match, keeping its config-derived policy
|
|
17
|
+
* instead — a corrupt or foreign file degrades to "local config", never to a
|
|
18
|
+
* weaker enforcement than the operator configured.
|
|
19
|
+
*
|
|
20
|
+
* `mode: null` means "central management not configured": the proxy keeps its
|
|
21
|
+
* local `intutic_settings.egress` / `INTUTIC_EGRESS_MODE`. A `mode` of
|
|
22
|
+
* `off`/`monitor`/`enforce` is authoritative for the mode; the `allow` list is
|
|
23
|
+
* UNIONed with the proxy's local allow entries so a developer's local infra
|
|
24
|
+
* allowances are never dropped.
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
import * as fs from 'node:fs/promises';
|
|
29
|
+
import * as path from 'node:path';
|
|
30
|
+
import * as os from 'node:os';
|
|
31
|
+
import { createHash } from 'node:crypto';
|
|
32
|
+
import { createLogger } from '@intutic/logger';
|
|
33
|
+
const log = createLogger('sync-egress-policy');
|
|
34
|
+
export const DEFAULT_EGRESS_CACHE_DIR = path.join(os.homedir(), '.intutic', 'hooks');
|
|
35
|
+
export const EGRESS_POLICY_FILE = 'egress-policy.json';
|
|
36
|
+
/**
|
|
37
|
+
* Trims trailing `/` without a regex — see the identical helper in
|
|
38
|
+
* `approvedBypasses.ts`: `/\/+$/` is flagged by CodeQL as a polynomial-time
|
|
39
|
+
* pattern on external input, and a loop sidesteps the whole category.
|
|
40
|
+
*/
|
|
41
|
+
function trimTrailingSlashes(s) {
|
|
42
|
+
let end = s.length;
|
|
43
|
+
while (end > 0 && s.charCodeAt(end - 1) === 47 /* '/' */)
|
|
44
|
+
end--;
|
|
45
|
+
return s.slice(0, end);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The canonical string the digest is computed over — identical on the daemon
|
|
49
|
+
* (here) and the proxy (Rust `load_local_egress_file`), so both derive the same
|
|
50
|
+
* digest. Mode (or empty) then each allow entry, newline-joined. Allow entries
|
|
51
|
+
* are hosts/CIDRs and never contain newlines.
|
|
52
|
+
*/
|
|
53
|
+
export function egressDigestInput(mode, allow) {
|
|
54
|
+
return [mode ?? '', ...allow].join('\n');
|
|
55
|
+
}
|
|
56
|
+
/** Fetch this workspace's central egress policy. Returns null on any failure —
|
|
57
|
+
* the caller keeps the previous cache file rather than replacing it with
|
|
58
|
+
* nothing, the same rule the policy snapshot follows. */
|
|
59
|
+
export async function fetchWorkspaceEgressPolicy(opts) {
|
|
60
|
+
const url = `${trimTrailingSlashes(opts.controlPlaneUrl)}/api/v1/workspace/egress-policy` +
|
|
61
|
+
`?workspaceId=${encodeURIComponent(opts.workspaceId)}`;
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(url, {
|
|
64
|
+
headers: { Authorization: `Bearer ${opts.apiKey}`, 'x-workspace-id': opts.workspaceId },
|
|
65
|
+
signal: AbortSignal.timeout(10_000),
|
|
66
|
+
});
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
log.warn({ action: 'egress_fetch_failed', status: res.status }, 'egress-policy returned non-OK');
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const body = (await res.json());
|
|
72
|
+
if (typeof body !== 'object' || body === null)
|
|
73
|
+
return null;
|
|
74
|
+
const rec = body;
|
|
75
|
+
const mode = rec.mode === 'off' || rec.mode === 'monitor' || rec.mode === 'enforce' ? rec.mode : null;
|
|
76
|
+
const allow = Array.isArray(rec.allow)
|
|
77
|
+
? rec.allow.filter((a) => typeof a === 'string' && a.length > 0)
|
|
78
|
+
: [];
|
|
79
|
+
return { mode, allow };
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
log.warn({ action: 'egress_fetch_failed', err }, 'egress-policy unreachable');
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Write the cache file atomically, digest + workspace header included — same
|
|
87
|
+
* read-only-by-rename posture as the policy snapshot. Exported so a test can
|
|
88
|
+
* assert the on-disk shape directly. */
|
|
89
|
+
export async function writeEgressPolicy(policy, workspaceId, cacheDir = DEFAULT_EGRESS_CACHE_DIR) {
|
|
90
|
+
const digest = createHash('sha256')
|
|
91
|
+
.update(egressDigestInput(policy.mode, policy.allow))
|
|
92
|
+
.digest('hex')
|
|
93
|
+
.slice(0, 32);
|
|
94
|
+
const doc = {
|
|
95
|
+
workspace: workspaceId,
|
|
96
|
+
digest,
|
|
97
|
+
generated: new Date().toISOString(),
|
|
98
|
+
mode: policy.mode,
|
|
99
|
+
allow: policy.allow,
|
|
100
|
+
};
|
|
101
|
+
const text = JSON.stringify(doc, null, 2) + '\n';
|
|
102
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
103
|
+
const target = path.join(cacheDir, EGRESS_POLICY_FILE);
|
|
104
|
+
const tmp = target + '.tmp';
|
|
105
|
+
await fs.writeFile(tmp, text, { encoding: 'utf-8' });
|
|
106
|
+
await fs.chmod(tmp, 0o444);
|
|
107
|
+
await fs.rename(tmp, target);
|
|
108
|
+
log.debug({ action: 'egress_policy_written', digest, mode: policy.mode, count: policy.allow.length, dir: cacheDir }, 'Egress policy cache refreshed');
|
|
109
|
+
return { digest, mode: policy.mode };
|
|
110
|
+
}
|
|
111
|
+
/** Fetch and write in one call. Safe to run on every sync cycle. Never throws —
|
|
112
|
+
* a failed poll must not take down the sync loop, and leaves the previous
|
|
113
|
+
* cache in place. */
|
|
114
|
+
export async function refreshEgressPolicy(opts) {
|
|
115
|
+
const policy = await fetchWorkspaceEgressPolicy(opts);
|
|
116
|
+
if (policy === null)
|
|
117
|
+
return null;
|
|
118
|
+
try {
|
|
119
|
+
return await writeEgressPolicy(policy, opts.workspaceId, opts.cacheDir ?? DEFAULT_EGRESS_CACHE_DIR);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
log.warn({ action: 'egress_policy_write_failed', err }, 'Could not write egress-policy cache');
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=egressPolicy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"egressPolicy.js","sourceRoot":"","sources":["../../src/lib/egressPolicy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gitWorktrees.ts — discovers every `git worktree` checkout of a repo the
|
|
3
|
+
* sync daemon is watching, so project-tier governance files can be written
|
|
4
|
+
* into each one, not just the main checkout.
|
|
5
|
+
*
|
|
6
|
+
* # The gap this closes
|
|
7
|
+
*
|
|
8
|
+
* `git worktree add <path>` creates a second, fully independent WORKING
|
|
9
|
+
* TREE that shares the main checkout's `.git` object store (via a `.git`
|
|
10
|
+
* FILE pointing back at `<main>/.git/worktrees/<name>`) but has its own
|
|
11
|
+
* untracked files. Every project-tier governance file this product writes
|
|
12
|
+
* — `.claude/settings.json`, `.cursor/hooks.json`, `.intutic/hooks/*`,
|
|
13
|
+
* `AGENTS.md`'s sibling hook registrations, the wrapped `.mcp.json` entries,
|
|
14
|
+
* etc. — is untracked by convention (SOP content is workspace-specific and
|
|
15
|
+
* synced from the control plane, not committed). That means none of it
|
|
16
|
+
* exists in a worktree's working tree by default: `writeConfigFiles`/
|
|
17
|
+
* `injectMcpServer` running against only the main checkout's `workspaceRoot`
|
|
18
|
+
* leaves every OTHER worktree of that same repo completely ungoverned,
|
|
19
|
+
* regardless of what the main checkout has configured.
|
|
20
|
+
*
|
|
21
|
+
* This was already true for any hand-rolled `git worktree` workflow before
|
|
22
|
+
* this module existed — Xirp (an orchestrator that spawns CLI agents each
|
|
23
|
+
* inside its own tmux session + git worktree, see `tools/cli/src/harness/
|
|
24
|
+
* xirp.ts`) is what surfaced it, but the fix here is general: it benefits
|
|
25
|
+
* ANY worktree-based workflow, Xirp-managed or not. See TD-390.
|
|
26
|
+
*
|
|
27
|
+
* # Verifying the mechanics for real
|
|
28
|
+
*
|
|
29
|
+
* Confirmed against a real throwaway repo during this module's development
|
|
30
|
+
* (not asserted from memory of git's docs): an untracked file written into
|
|
31
|
+
* a main checkout is NOT visible from a `git worktree add`-created sibling
|
|
32
|
+
* checkout's directory. `git worktree list --porcelain`'s block format,
|
|
33
|
+
* including `locked`/`prunable`/`bare`/`detached` fields, was captured from
|
|
34
|
+
* a real `git worktree list --porcelain` run (this module's own test suite
|
|
35
|
+
* builds the same kind of fixture, in an isolated temp directory, rather
|
|
36
|
+
* than relying on this description alone).
|
|
37
|
+
*
|
|
38
|
+
* @module
|
|
39
|
+
*/
|
|
40
|
+
/** One block of `git worktree list --porcelain` output, parsed. */
|
|
41
|
+
export interface WorktreeInfo {
|
|
42
|
+
/** Absolute path to this worktree's working directory. */
|
|
43
|
+
path: string;
|
|
44
|
+
/** Commit the worktree's HEAD points at, or null if not reported (bare). */
|
|
45
|
+
head: string | null;
|
|
46
|
+
/** Branch ref this worktree has checked out (e.g. `refs/heads/main`), or
|
|
47
|
+
* null when detached or bare. */
|
|
48
|
+
branch: string | null;
|
|
49
|
+
/** True if this worktree's HEAD is detached (no branch checked out). */
|
|
50
|
+
detached: boolean;
|
|
51
|
+
/** True for the bare-repository worktree entry — the repo root itself
|
|
52
|
+
* when it has no working tree of its own files (only ever the first
|
|
53
|
+
* entry, and only for a bare repo). */
|
|
54
|
+
bare: boolean;
|
|
55
|
+
/** True if this worktree is locked (`git worktree lock`) — git will not
|
|
56
|
+
* prune it even if its directory looks gone. Locking does not restrict
|
|
57
|
+
* reading or writing ordinary files in it, so this does not by itself
|
|
58
|
+
* change whether `discoverWorktrees` includes the path. */
|
|
59
|
+
locked: boolean;
|
|
60
|
+
/** Reason string passed to `git worktree lock --reason`, if any. */
|
|
61
|
+
lockedReason: string | null;
|
|
62
|
+
/** True if git has flagged this worktree's directory as missing/invalid
|
|
63
|
+
* and a candidate for `git worktree prune`. */
|
|
64
|
+
prunable: boolean;
|
|
65
|
+
/** Reason string git reports for why this worktree is prunable, if any. */
|
|
66
|
+
prunableReason: string | null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parses `git worktree list --porcelain` output.
|
|
70
|
+
*
|
|
71
|
+
* Real format (verified against a live `git worktree list --porcelain` run,
|
|
72
|
+
* git 2.x): one block per worktree, separated by a blank line, each line a
|
|
73
|
+
* bare keyword or `<keyword> <value>`:
|
|
74
|
+
*
|
|
75
|
+
* ```
|
|
76
|
+
* worktree /path/to/main
|
|
77
|
+
* HEAD 56bdba0a04f83da4e821a0a3e7a6208d43fcaec5
|
|
78
|
+
* branch refs/heads/master
|
|
79
|
+
*
|
|
80
|
+
* worktree /path/to/locked-wt
|
|
81
|
+
* HEAD 56bdba0a04f83da4e821a0a3e7a6208d43fcaec5
|
|
82
|
+
* branch refs/heads/feature2
|
|
83
|
+
* locked testing lock
|
|
84
|
+
*
|
|
85
|
+
* worktree /path/to/detached-wt
|
|
86
|
+
* HEAD 56bdba0a04f83da4e821a0a3e7a6208d43fcaec5
|
|
87
|
+
* detached
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* Deliberately implemented with plain string methods (`startsWith`/`slice`)
|
|
91
|
+
* rather than a line-matching regex — this parses `git`-controlled, not
|
|
92
|
+
* attacker-controlled, input, but a sibling Wave 1 phase shipped a
|
|
93
|
+
* CodeQL-flagged polynomial-regex bug from exactly this kind of "just trim
|
|
94
|
+
* a path" text handling, so the same class of construct is avoided here on
|
|
95
|
+
* principle even though the input source differs.
|
|
96
|
+
*/
|
|
97
|
+
export declare function parseWorktreeListPorcelain(output: string): WorktreeInfo[];
|
|
98
|
+
/**
|
|
99
|
+
* Discovers every worktree of the git repository containing `repoRoot`,
|
|
100
|
+
* returning the absolute paths that should be treated as additional
|
|
101
|
+
* project-tier workspace roots.
|
|
102
|
+
*
|
|
103
|
+
* Filtering decisions, made explicit rather than left implicit:
|
|
104
|
+
*
|
|
105
|
+
* - **The main checkout is included.** `git worktree list` always lists it
|
|
106
|
+
* first (confirmed against a live run) — it needs no special-casing by
|
|
107
|
+
* callers; a caller that already writes to `repoRoot` directly can simply
|
|
108
|
+
* skip a returned path equal to `repoRoot` if it wants to avoid a
|
|
109
|
+
* redundant duplicate write, but the list is uniformly correct either way.
|
|
110
|
+
* - **Locked worktrees are included.** `git worktree lock` only prevents
|
|
111
|
+
* git's own pruning of a worktree it cannot otherwise verify still
|
|
112
|
+
* exists (e.g. on removable storage) — it says nothing about whether the
|
|
113
|
+
* directory is currently present and writable, which it normally is.
|
|
114
|
+
* Excluding locked worktrees would silently under-cover a workflow (like
|
|
115
|
+
* Xirp's) that locks worktrees deliberately to protect them from being
|
|
116
|
+
* pruned out from under a long-running session — exactly the worktrees
|
|
117
|
+
* most worth covering.
|
|
118
|
+
* - **Prunable worktrees are EXCLUDED.** `prunable` means git itself has
|
|
119
|
+
* flagged the worktree's directory as missing or otherwise administratively
|
|
120
|
+
* invalid. Writing into that path would either fail or (via this
|
|
121
|
+
* codebase's `mkdir(dir, { recursive: true })` write convention) silently
|
|
122
|
+
* RECREATE a directory git considers gone — resurrecting a deleted
|
|
123
|
+
* worktree's folder with governance files nobody asked for. Skipping it
|
|
124
|
+
* is what "don't write into a directory git says isn't really there"
|
|
125
|
+
* means in practice.
|
|
126
|
+
* - **Bare-repository entries are EXCLUDED.** A bare repo's own `worktree`
|
|
127
|
+
* porcelain entry names its root path but has no working tree of ordinary
|
|
128
|
+
* files to write project-tier config into.
|
|
129
|
+
*
|
|
130
|
+
* Best-effort by design: returns `[]` (never throws) if `repoRoot` is not
|
|
131
|
+
* inside a git repository, if `git` is not on `PATH`, or on any other
|
|
132
|
+
* failure — this is additive coverage on top of the existing single-root
|
|
133
|
+
* write path, so a git failure here must not break the sync cycle that
|
|
134
|
+
* already succeeds for the main `workspaceRoot`. This also means a
|
|
135
|
+
* non-worktree, single-checkout project (the common case) is a harmless
|
|
136
|
+
* no-extra-work path: `git worktree list` still runs, still succeeds, and
|
|
137
|
+
* returns exactly one entry (the main checkout itself).
|
|
138
|
+
*/
|
|
139
|
+
export declare function discoverWorktrees(repoRoot: string): Promise<string[]>;
|
|
140
|
+
//# sourceMappingURL=gitWorktrees.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitWorktrees.d.ts","sourceRoot":"","sources":["../../src/lib/gitWorktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAOH,mEAAmE;AACnE,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAA;IACZ,4EAA4E;IAC5E,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB;sCACkC;IAClC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAA;IACjB;;4CAEwC;IACxC,IAAI,EAAE,OAAO,CAAA;IACb;;;gEAG4D;IAC5D,MAAM,EAAE,OAAO,CAAA;IACf,oEAAoE;IACpE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B;oDACgD;IAChD,QAAQ,EAAE,OAAO,CAAA;IACjB,2EAA2E;IAC3E,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE,CAuDzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAgB3E"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gitWorktrees.ts — discovers every `git worktree` checkout of a repo the
|
|
3
|
+
* sync daemon is watching, so project-tier governance files can be written
|
|
4
|
+
* into each one, not just the main checkout.
|
|
5
|
+
*
|
|
6
|
+
* # The gap this closes
|
|
7
|
+
*
|
|
8
|
+
* `git worktree add <path>` creates a second, fully independent WORKING
|
|
9
|
+
* TREE that shares the main checkout's `.git` object store (via a `.git`
|
|
10
|
+
* FILE pointing back at `<main>/.git/worktrees/<name>`) but has its own
|
|
11
|
+
* untracked files. Every project-tier governance file this product writes
|
|
12
|
+
* — `.claude/settings.json`, `.cursor/hooks.json`, `.intutic/hooks/*`,
|
|
13
|
+
* `AGENTS.md`'s sibling hook registrations, the wrapped `.mcp.json` entries,
|
|
14
|
+
* etc. — is untracked by convention (SOP content is workspace-specific and
|
|
15
|
+
* synced from the control plane, not committed). That means none of it
|
|
16
|
+
* exists in a worktree's working tree by default: `writeConfigFiles`/
|
|
17
|
+
* `injectMcpServer` running against only the main checkout's `workspaceRoot`
|
|
18
|
+
* leaves every OTHER worktree of that same repo completely ungoverned,
|
|
19
|
+
* regardless of what the main checkout has configured.
|
|
20
|
+
*
|
|
21
|
+
* This was already true for any hand-rolled `git worktree` workflow before
|
|
22
|
+
* this module existed — Xirp (an orchestrator that spawns CLI agents each
|
|
23
|
+
* inside its own tmux session + git worktree, see `tools/cli/src/harness/
|
|
24
|
+
* xirp.ts`) is what surfaced it, but the fix here is general: it benefits
|
|
25
|
+
* ANY worktree-based workflow, Xirp-managed or not. See TD-390.
|
|
26
|
+
*
|
|
27
|
+
* # Verifying the mechanics for real
|
|
28
|
+
*
|
|
29
|
+
* Confirmed against a real throwaway repo during this module's development
|
|
30
|
+
* (not asserted from memory of git's docs): an untracked file written into
|
|
31
|
+
* a main checkout is NOT visible from a `git worktree add`-created sibling
|
|
32
|
+
* checkout's directory. `git worktree list --porcelain`'s block format,
|
|
33
|
+
* including `locked`/`prunable`/`bare`/`detached` fields, was captured from
|
|
34
|
+
* a real `git worktree list --porcelain` run (this module's own test suite
|
|
35
|
+
* builds the same kind of fixture, in an isolated temp directory, rather
|
|
36
|
+
* than relying on this description alone).
|
|
37
|
+
*
|
|
38
|
+
* @module
|
|
39
|
+
*/
|
|
40
|
+
import { execFile as _execFile } from 'node:child_process';
|
|
41
|
+
import { promisify } from 'node:util';
|
|
42
|
+
const execFile = promisify(_execFile);
|
|
43
|
+
/**
|
|
44
|
+
* Parses `git worktree list --porcelain` output.
|
|
45
|
+
*
|
|
46
|
+
* Real format (verified against a live `git worktree list --porcelain` run,
|
|
47
|
+
* git 2.x): one block per worktree, separated by a blank line, each line a
|
|
48
|
+
* bare keyword or `<keyword> <value>`:
|
|
49
|
+
*
|
|
50
|
+
* ```
|
|
51
|
+
* worktree /path/to/main
|
|
52
|
+
* HEAD 56bdba0a04f83da4e821a0a3e7a6208d43fcaec5
|
|
53
|
+
* branch refs/heads/master
|
|
54
|
+
*
|
|
55
|
+
* worktree /path/to/locked-wt
|
|
56
|
+
* HEAD 56bdba0a04f83da4e821a0a3e7a6208d43fcaec5
|
|
57
|
+
* branch refs/heads/feature2
|
|
58
|
+
* locked testing lock
|
|
59
|
+
*
|
|
60
|
+
* worktree /path/to/detached-wt
|
|
61
|
+
* HEAD 56bdba0a04f83da4e821a0a3e7a6208d43fcaec5
|
|
62
|
+
* detached
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* Deliberately implemented with plain string methods (`startsWith`/`slice`)
|
|
66
|
+
* rather than a line-matching regex — this parses `git`-controlled, not
|
|
67
|
+
* attacker-controlled, input, but a sibling Wave 1 phase shipped a
|
|
68
|
+
* CodeQL-flagged polynomial-regex bug from exactly this kind of "just trim
|
|
69
|
+
* a path" text handling, so the same class of construct is avoided here on
|
|
70
|
+
* principle even though the input source differs.
|
|
71
|
+
*/
|
|
72
|
+
export function parseWorktreeListPorcelain(output) {
|
|
73
|
+
const blocks = output.split('\n\n');
|
|
74
|
+
const worktrees = [];
|
|
75
|
+
for (const block of blocks) {
|
|
76
|
+
const trimmedBlock = block.trim();
|
|
77
|
+
if (!trimmedBlock)
|
|
78
|
+
continue;
|
|
79
|
+
let path = '';
|
|
80
|
+
let head = null;
|
|
81
|
+
let branch = null;
|
|
82
|
+
let detached = false;
|
|
83
|
+
let bare = false;
|
|
84
|
+
let locked = false;
|
|
85
|
+
let lockedReason = null;
|
|
86
|
+
let prunable = false;
|
|
87
|
+
let prunableReason = null;
|
|
88
|
+
for (const rawLine of trimmedBlock.split('\n')) {
|
|
89
|
+
const line = rawLine.trim();
|
|
90
|
+
if (!line)
|
|
91
|
+
continue;
|
|
92
|
+
if (line === 'bare') {
|
|
93
|
+
bare = true;
|
|
94
|
+
}
|
|
95
|
+
else if (line === 'detached') {
|
|
96
|
+
detached = true;
|
|
97
|
+
}
|
|
98
|
+
else if (line.startsWith('worktree ')) {
|
|
99
|
+
path = line.slice('worktree '.length).trim();
|
|
100
|
+
}
|
|
101
|
+
else if (line.startsWith('HEAD ')) {
|
|
102
|
+
head = line.slice('HEAD '.length).trim();
|
|
103
|
+
}
|
|
104
|
+
else if (line.startsWith('branch ')) {
|
|
105
|
+
branch = line.slice('branch '.length).trim();
|
|
106
|
+
}
|
|
107
|
+
else if (line === 'locked') {
|
|
108
|
+
locked = true;
|
|
109
|
+
}
|
|
110
|
+
else if (line.startsWith('locked ')) {
|
|
111
|
+
locked = true;
|
|
112
|
+
lockedReason = line.slice('locked '.length).trim() || null;
|
|
113
|
+
}
|
|
114
|
+
else if (line === 'prunable') {
|
|
115
|
+
prunable = true;
|
|
116
|
+
}
|
|
117
|
+
else if (line.startsWith('prunable ')) {
|
|
118
|
+
prunable = true;
|
|
119
|
+
prunableReason = line.slice('prunable '.length).trim() || null;
|
|
120
|
+
}
|
|
121
|
+
// Any other line is a porcelain field this parser does not know about
|
|
122
|
+
// yet (git has added a few over the years) — ignored rather than
|
|
123
|
+
// treated as an error, so a future git version adding a new field
|
|
124
|
+
// degrades gracefully instead of breaking discovery entirely.
|
|
125
|
+
}
|
|
126
|
+
if (path) {
|
|
127
|
+
worktrees.push({ path, head, branch, detached, bare, locked, lockedReason, prunable, prunableReason });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return worktrees;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Discovers every worktree of the git repository containing `repoRoot`,
|
|
134
|
+
* returning the absolute paths that should be treated as additional
|
|
135
|
+
* project-tier workspace roots.
|
|
136
|
+
*
|
|
137
|
+
* Filtering decisions, made explicit rather than left implicit:
|
|
138
|
+
*
|
|
139
|
+
* - **The main checkout is included.** `git worktree list` always lists it
|
|
140
|
+
* first (confirmed against a live run) — it needs no special-casing by
|
|
141
|
+
* callers; a caller that already writes to `repoRoot` directly can simply
|
|
142
|
+
* skip a returned path equal to `repoRoot` if it wants to avoid a
|
|
143
|
+
* redundant duplicate write, but the list is uniformly correct either way.
|
|
144
|
+
* - **Locked worktrees are included.** `git worktree lock` only prevents
|
|
145
|
+
* git's own pruning of a worktree it cannot otherwise verify still
|
|
146
|
+
* exists (e.g. on removable storage) — it says nothing about whether the
|
|
147
|
+
* directory is currently present and writable, which it normally is.
|
|
148
|
+
* Excluding locked worktrees would silently under-cover a workflow (like
|
|
149
|
+
* Xirp's) that locks worktrees deliberately to protect them from being
|
|
150
|
+
* pruned out from under a long-running session — exactly the worktrees
|
|
151
|
+
* most worth covering.
|
|
152
|
+
* - **Prunable worktrees are EXCLUDED.** `prunable` means git itself has
|
|
153
|
+
* flagged the worktree's directory as missing or otherwise administratively
|
|
154
|
+
* invalid. Writing into that path would either fail or (via this
|
|
155
|
+
* codebase's `mkdir(dir, { recursive: true })` write convention) silently
|
|
156
|
+
* RECREATE a directory git considers gone — resurrecting a deleted
|
|
157
|
+
* worktree's folder with governance files nobody asked for. Skipping it
|
|
158
|
+
* is what "don't write into a directory git says isn't really there"
|
|
159
|
+
* means in practice.
|
|
160
|
+
* - **Bare-repository entries are EXCLUDED.** A bare repo's own `worktree`
|
|
161
|
+
* porcelain entry names its root path but has no working tree of ordinary
|
|
162
|
+
* files to write project-tier config into.
|
|
163
|
+
*
|
|
164
|
+
* Best-effort by design: returns `[]` (never throws) if `repoRoot` is not
|
|
165
|
+
* inside a git repository, if `git` is not on `PATH`, or on any other
|
|
166
|
+
* failure — this is additive coverage on top of the existing single-root
|
|
167
|
+
* write path, so a git failure here must not break the sync cycle that
|
|
168
|
+
* already succeeds for the main `workspaceRoot`. This also means a
|
|
169
|
+
* non-worktree, single-checkout project (the common case) is a harmless
|
|
170
|
+
* no-extra-work path: `git worktree list` still runs, still succeeds, and
|
|
171
|
+
* returns exactly one entry (the main checkout itself).
|
|
172
|
+
*/
|
|
173
|
+
export async function discoverWorktrees(repoRoot) {
|
|
174
|
+
try {
|
|
175
|
+
const { stdout } = await execFile('git', ['worktree', 'list', '--porcelain'], {
|
|
176
|
+
cwd: repoRoot,
|
|
177
|
+
encoding: 'utf-8',
|
|
178
|
+
timeout: 5000,
|
|
179
|
+
});
|
|
180
|
+
return parseWorktreeListPorcelain(stdout)
|
|
181
|
+
.filter((wt) => !wt.bare && !wt.prunable)
|
|
182
|
+
.map((wt) => wt.path);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Not a git repo, git not installed, or a transient failure — additive
|
|
186
|
+
// coverage, so this degrades to "no extra worktrees found", not a
|
|
187
|
+
// thrown error the caller must handle.
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=gitWorktrees.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitWorktrees.js","sourceRoot":"","sources":["../../src/lib/gitWorktrees.ts"],"names":[],"mappings":"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"}
|