@telora/daemon 0.19.31 → 0.19.34

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.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `telora-daemon harness add <target>` subcommand.
3
+ *
4
+ * A token-free, idempotent way to install or repair any operator harness on an
5
+ * ALREADY-connected workstation. Unlike `connect`, it never redeems a token,
6
+ * never rewrites `~/.telora/daemon.json`, and never starts the daemon -- it
7
+ * reads the identity `connect` already persisted (`readConnectedIdentity`) and
8
+ * writes exactly the same harness scaffolding (`writeHarnessScaffolding`), so
9
+ * `harness add <h>` and `connect --harness <h>` produce byte-identical files
10
+ * for a given identity.
11
+ *
12
+ * On a repo the workstation is not connected to, it refuses with an actionable
13
+ * message directing the operator to `connect` first.
14
+ */
15
+ import { type ConnectIO, type OperatorHarness } from './connect.js';
16
+ export interface HarnessAddOptions {
17
+ /** The harness target to install or repair. */
18
+ target: OperatorHarness;
19
+ /** Repo path to scaffold (default: cwd). */
20
+ repoPath?: string;
21
+ /** Override IO (used by tests). */
22
+ io?: Partial<ConnectIO>;
23
+ }
24
+ export interface HarnessAddResult {
25
+ ok: boolean;
26
+ /** Human-facing message (success detail, or the not-connected remediation). */
27
+ message: string;
28
+ }
29
+ /**
30
+ * Install or repair the `target` harness scaffolding for a connected repo.
31
+ *
32
+ * Idempotent: the underlying writers skip the write when content is unchanged,
33
+ * so a second run leaves identical files and returns ok. Returns `ok: false`
34
+ * (no files touched) when the repo is not connected.
35
+ */
36
+ export declare function runHarnessAdd(opts: HarnessAddOptions): HarnessAddResult;
37
+ //# sourceMappingURL=harness-add.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness-add.d.ts","sourceRoot":"","sources":["../../src/cli/harness-add.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,EAML,KAAK,SAAS,EACd,KAAK,eAAe,EACrB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,MAAM,EAAE,eAAe,CAAC;IACxB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,EAAE,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,gBAAgB,CA4BvE"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * `telora-daemon harness add <target>` subcommand.
3
+ *
4
+ * A token-free, idempotent way to install or repair any operator harness on an
5
+ * ALREADY-connected workstation. Unlike `connect`, it never redeems a token,
6
+ * never rewrites `~/.telora/daemon.json`, and never starts the daemon -- it
7
+ * reads the identity `connect` already persisted (`readConnectedIdentity`) and
8
+ * writes exactly the same harness scaffolding (`writeHarnessScaffolding`), so
9
+ * `harness add <h>` and `connect --harness <h>` produce byte-identical files
10
+ * for a given identity.
11
+ *
12
+ * On a repo the workstation is not connected to, it refuses with an actionable
13
+ * message directing the operator to `connect` first.
14
+ */
15
+ import { resolve } from 'node:path';
16
+ import { defaultIO, ensureGitignoreEntries, harnessGitignoreEntries, readConnectedIdentity, writeHarnessScaffolding, } from './connect.js';
17
+ /**
18
+ * Install or repair the `target` harness scaffolding for a connected repo.
19
+ *
20
+ * Idempotent: the underlying writers skip the write when content is unchanged,
21
+ * so a second run leaves identical files and returns ok. Returns `ok: false`
22
+ * (no files touched) when the repo is not connected.
23
+ */
24
+ export function runHarnessAdd(opts) {
25
+ const io = defaultIO(opts.io);
26
+ const repoPath = resolve(opts.repoPath ?? process.cwd());
27
+ const identity = readConnectedIdentity(repoPath);
28
+ if (!identity) {
29
+ return {
30
+ ok: false,
31
+ message: `This workstation is not connected to Telora for ${repoPath}. ` +
32
+ `Run \`telora-daemon connect <token> --harness ${opts.target}\` to connect first.`,
33
+ };
34
+ }
35
+ const inv = {
36
+ productName: null,
37
+ teloraUrl: identity.teloraUrl,
38
+ organizationId: identity.organizationId,
39
+ productId: identity.productId,
40
+ };
41
+ ensureGitignoreEntries(repoPath, io, harnessGitignoreEntries(opts.target));
42
+ writeHarnessScaffolding(repoPath, opts.target, inv, io);
43
+ return {
44
+ ok: true,
45
+ message: `Installed ${opts.target} harness scaffolding for product ${identity.productId}.`,
46
+ };
47
+ }
48
+ //# sourceMappingURL=harness-add.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness-add.js","sourceRoot":"","sources":["../../src/cli/harness-add.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EACL,SAAS,EACT,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,uBAAuB,GAGxB,MAAM,cAAc,CAAC;AAiBtB;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,IAAuB;IACnD,MAAM,EAAE,GAAG,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC9B,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAEzD,MAAM,QAAQ,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IACjD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EACL,mDAAmD,QAAQ,IAAI;gBAC/D,iDAAiD,IAAI,CAAC,MAAM,sBAAsB;SACrF,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GAAgC;QACvC,WAAW,EAAE,IAAI;QACjB,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,cAAc,EAAE,QAAQ,CAAC,cAAc;QACvC,SAAS,EAAE,QAAQ,CAAC,SAAS;KAC9B,CAAC;IAEF,sBAAsB,CAAC,QAAQ,EAAE,EAAE,EAAE,uBAAuB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3E,uBAAuB,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IAExD,OAAO;QACL,EAAE,EAAE,IAAI;QACR,OAAO,EAAE,aAAa,IAAI,CAAC,MAAM,oCAAoC,QAAQ,CAAC,SAAS,GAAG;KAC3F,CAAC;AACJ,CAAC"}
@@ -0,0 +1,171 @@
1
+ /**
2
+ * `telora-daemon open <focus>` subcommand.
3
+ *
4
+ * Resolves a focus (by UUID or by focus key like "f3") to its git branch,
5
+ * reuses or creates an OPERATOR-OWNED worktree on that branch, and launches the
6
+ * operator's editor there -- for a focus that is still in flight OR already
7
+ * merged/completed.
8
+ *
9
+ * Two invariants shape the design:
10
+ *
11
+ * 1. The operator worktree is NEVER the team's live worktree. Team worktrees
12
+ * live at `join(worktreeDir, branch.replace(/\//g,'-'))`; operator worktrees
13
+ * live under a distinct `operator-` prefixed namespace
14
+ * (`join(worktreeDir, 'operator-' + branch.replace(/\//g,'-'))`). The
15
+ * prefix keeps them provably a different path AND out of reach of the
16
+ * team-lifecycle sweeps: `cleanupOrphanFocusWorktrees` only reaps dirs whose
17
+ * name starts with `agent-`, so an `operator-agent-...` dir is never reaped.
18
+ *
19
+ * 2. A focus resolves to its branch even after merge. The `focus_worktrees`
20
+ * ROW survives a merge (only the worktree DIRECTORY is removed), so the row
21
+ * is the primary branch source. When no row exists we fall back to deriving
22
+ * the branch name and verifying it exists in git; a focus with neither is
23
+ * the actionable no-branch error path.
24
+ *
25
+ * All side-effecting pieces (record lookup, git ops, editor launch, IO) are
26
+ * injected so `runOpen` is testable without a network, a real git remote, or a
27
+ * real editor. `runOpenCommand` wires the real defaults for the CLI.
28
+ */
29
+ /** Default editor command. `cursor` since this verb is the IDE bridge. */
30
+ export declare const DEFAULT_EDITOR = "cursor";
31
+ /** True when `ref` is a focus UUID (vs. an ephemeral focus key like "f3"). */
32
+ export declare function looksLikeUuid(ref: string): boolean;
33
+ /** True when `ref` is an ephemeral focus key like "f3". */
34
+ export declare function looksLikeFocusKey(ref: string): boolean;
35
+ /**
36
+ * Derive a focus branch name. Mirrors `generateFocusBranchName` in
37
+ * focus-executor.ts (`agent/<role>/<focus>-<shortId>`), reusing the same
38
+ * canonical `sanitizeGitSegment`. Used only as the fallback when a focus has no
39
+ * `focus_worktrees` row; the derived branch is git-verified before use.
40
+ */
41
+ export declare function deriveFocusBranchName(roleName: string, focusName: string, focusId: string): string;
42
+ /**
43
+ * The team's worktree path for a branch. Mirrors `createWorktree` in
44
+ * worktree.ts. This is the LIVE worktree an executing team owns -- `open`
45
+ * never creates, removes, or writes to it.
46
+ */
47
+ export declare function teamWorktreePath(worktreeDir: string, branchName: string): string;
48
+ /**
49
+ * The OPERATOR worktree path for a branch: a distinct namespace under the same
50
+ * worktree dir, prefixed `operator-`. Distinct from `teamWorktreePath` by
51
+ * construction, and invisible to `cleanupOrphanFocusWorktrees` (which only
52
+ * reaps `agent-*` dirs).
53
+ */
54
+ export declare function operatorWorktreePath(worktreeDir: string, branchName: string): string;
55
+ /**
56
+ * Resolve the editor command with precedence: explicit `--editor` > `TELORA_EDITOR`
57
+ * env > `DEFAULT_EDITOR`.
58
+ */
59
+ export declare function resolveEditor(optEditor?: string, env?: NodeJS.ProcessEnv): string;
60
+ /** The subset of a `focus_worktrees` row the resolver needs. */
61
+ export interface WorktreeLite {
62
+ focusId: string;
63
+ branchName: string;
64
+ worktreePath: string;
65
+ productId?: string | null;
66
+ }
67
+ /** The subset of a `focus_list` entry the resolver needs. */
68
+ export interface FocusLite {
69
+ id: string;
70
+ focusKey?: string | null;
71
+ name?: string | null;
72
+ assignedAgentRoleName?: string | null;
73
+ productId?: string | null;
74
+ }
75
+ /** Record-level resolution of a focus ref to a branch (no product paths yet). */
76
+ export interface FocusRecordResolution {
77
+ focusId: string;
78
+ focusName: string;
79
+ branchName: string;
80
+ productId: string | null;
81
+ /** True when the branch came from a focus_worktrees row; false when derived. */
82
+ fromWorktreeRow: boolean;
83
+ }
84
+ /** Full resolution including the product's repo + worktree directory. */
85
+ export interface FocusResolution extends FocusRecordResolution {
86
+ repoPath: string;
87
+ worktreeDir: string;
88
+ }
89
+ /**
90
+ * Pure resolution of a focus ref (UUID or focus key) to its branch, from
91
+ * already-fetched worktree rows + focus list. No I/O -- hermetic to test.
92
+ *
93
+ * Returns null when the ref is neither a UUID nor a key, when a key does not
94
+ * match any focus, or when there is no branch to open (no row and no derivable
95
+ * branch).
96
+ */
97
+ export declare function resolveFocusWorktreeFromData(params: {
98
+ focusRef: string;
99
+ worktrees: WorktreeLite[];
100
+ focuses: FocusLite[];
101
+ }): FocusRecordResolution | null;
102
+ /** Git/worktree operations, injectable for tests. */
103
+ export interface OpenGitOps {
104
+ /** Whether a path exists on disk. */
105
+ pathExists: (path: string) => boolean;
106
+ /** Whether a local git branch exists in the repo. */
107
+ branchExists: (branchName: string, repoPath: string) => boolean;
108
+ /** `git worktree add <worktreePath> <branch>`. Throws on failure. */
109
+ addWorktree: (repoPath: string, worktreePath: string, branchName: string) => void;
110
+ }
111
+ /** Launch an editor at a path. Injectable for tests. */
112
+ export type LaunchEditor = (editorCmd: string, worktreePath: string) => void;
113
+ /** Message sink, injectable for tests. */
114
+ export interface OpenIO {
115
+ out: (msg: string) => void;
116
+ err: (msg: string) => void;
117
+ }
118
+ export interface RunOpenParams {
119
+ focusRef: string;
120
+ /** The resolved editor command (see resolveEditor). */
121
+ editor: string;
122
+ /** Injected focus-ref -> resolution lookup (network in real wiring). */
123
+ resolveWorktreeRecord: (focusRef: string) => Promise<FocusResolution | null>;
124
+ git: OpenGitOps;
125
+ launchEditor: LaunchEditor;
126
+ io: OpenIO;
127
+ }
128
+ export type OpenFailure = 'no_branch' | 'worktree_create_failed';
129
+ export interface RunOpenResult {
130
+ ok: boolean;
131
+ editor: string;
132
+ message: string;
133
+ failure?: OpenFailure;
134
+ focusName?: string;
135
+ branchName?: string;
136
+ operatorWorktreePath?: string;
137
+ teamWorktreePath?: string;
138
+ /** True when an existing operator worktree was reused (not created). */
139
+ reused?: boolean;
140
+ /** True when a new operator worktree was created. */
141
+ createdWorktree?: boolean;
142
+ /** True when the team's worktree still exists on disk (focus in flight). */
143
+ inFlight?: boolean;
144
+ }
145
+ /**
146
+ * Resolve a focus to an operator worktree and launch the editor there.
147
+ *
148
+ * Returns a result object; the CLI wrapper maps `ok === false` to a non-zero
149
+ * exit. Never touches the team's live worktree.
150
+ */
151
+ export declare function runOpen(params: RunOpenParams): Promise<RunOpenResult>;
152
+ /** Default git ops backed by the real filesystem + git. */
153
+ export declare const defaultGitOps: OpenGitOps;
154
+ /** Default editor launcher: detached spawn so the CLI can return immediately. */
155
+ export declare function defaultLaunchEditor(editorCmd: string, worktreePath: string): void;
156
+ export interface RunOpenCommandOptions {
157
+ focusRef: string;
158
+ /** Explicit editor override (from --editor). */
159
+ editor?: string;
160
+ /** Path to daemon.json (from --config). */
161
+ config?: string;
162
+ /** IO override (tests). */
163
+ io?: Partial<OpenIO>;
164
+ }
165
+ /**
166
+ * CLI entry: load config, wire the real defaults, run, print, and return an
167
+ * exit code (0 success, 1 failure). The `index.ts` action wrapper calls
168
+ * `process.exit` with the returned code.
169
+ */
170
+ export declare function runOpenCommand(opts: RunOpenCommandOptions): Promise<number>;
171
+ //# sourceMappingURL=open.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open.d.ts","sourceRoot":"","sources":["../../src/cli/open.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,323 @@
1
+ /**
2
+ * `telora-daemon open <focus>` subcommand.
3
+ *
4
+ * Resolves a focus (by UUID or by focus key like "f3") to its git branch,
5
+ * reuses or creates an OPERATOR-OWNED worktree on that branch, and launches the
6
+ * operator's editor there -- for a focus that is still in flight OR already
7
+ * merged/completed.
8
+ *
9
+ * Two invariants shape the design:
10
+ *
11
+ * 1. The operator worktree is NEVER the team's live worktree. Team worktrees
12
+ * live at `join(worktreeDir, branch.replace(/\//g,'-'))`; operator worktrees
13
+ * live under a distinct `operator-` prefixed namespace
14
+ * (`join(worktreeDir, 'operator-' + branch.replace(/\//g,'-'))`). The
15
+ * prefix keeps them provably a different path AND out of reach of the
16
+ * team-lifecycle sweeps: `cleanupOrphanFocusWorktrees` only reaps dirs whose
17
+ * name starts with `agent-`, so an `operator-agent-...` dir is never reaped.
18
+ *
19
+ * 2. A focus resolves to its branch even after merge. The `focus_worktrees`
20
+ * ROW survives a merge (only the worktree DIRECTORY is removed), so the row
21
+ * is the primary branch source. When no row exists we fall back to deriving
22
+ * the branch name and verifying it exists in git; a focus with neither is
23
+ * the actionable no-branch error path.
24
+ *
25
+ * All side-effecting pieces (record lookup, git ops, editor launch, IO) are
26
+ * injected so `runOpen` is testable without a network, a real git remote, or a
27
+ * real editor. `runOpenCommand` wires the real defaults for the CLI.
28
+ */
29
+ import { existsSync } from 'node:fs';
30
+ import { join } from 'node:path';
31
+ import { spawn } from 'node:child_process';
32
+ import { loadConfig, configForProduct, findProduct } from '../config.js';
33
+ import { sanitizeGitSegment } from '../git-utils.js';
34
+ import { runGitSync } from '../git-types.js';
35
+ import { listWorktreeRecords } from '../queries/worktrees.js';
36
+ import { callApi, initSupabase } from '../queries/shared.js';
37
+ /** Default editor command. `cursor` since this verb is the IDE bridge. */
38
+ export const DEFAULT_EDITOR = 'cursor';
39
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
40
+ const FOCUS_KEY_RE = /^f\d+$/;
41
+ /** True when `ref` is a focus UUID (vs. an ephemeral focus key like "f3"). */
42
+ export function looksLikeUuid(ref) {
43
+ return UUID_RE.test(ref);
44
+ }
45
+ /** True when `ref` is an ephemeral focus key like "f3". */
46
+ export function looksLikeFocusKey(ref) {
47
+ return FOCUS_KEY_RE.test(ref);
48
+ }
49
+ /**
50
+ * Derive a focus branch name. Mirrors `generateFocusBranchName` in
51
+ * focus-executor.ts (`agent/<role>/<focus>-<shortId>`), reusing the same
52
+ * canonical `sanitizeGitSegment`. Used only as the fallback when a focus has no
53
+ * `focus_worktrees` row; the derived branch is git-verified before use.
54
+ */
55
+ export function deriveFocusBranchName(roleName, focusName, focusId) {
56
+ return `agent/${sanitizeGitSegment(roleName)}/${sanitizeGitSegment(focusName)}-${focusId.slice(0, 8)}`;
57
+ }
58
+ /**
59
+ * The team's worktree path for a branch. Mirrors `createWorktree` in
60
+ * worktree.ts. This is the LIVE worktree an executing team owns -- `open`
61
+ * never creates, removes, or writes to it.
62
+ */
63
+ export function teamWorktreePath(worktreeDir, branchName) {
64
+ return join(worktreeDir, branchName.replace(/\//g, '-'));
65
+ }
66
+ /**
67
+ * The OPERATOR worktree path for a branch: a distinct namespace under the same
68
+ * worktree dir, prefixed `operator-`. Distinct from `teamWorktreePath` by
69
+ * construction, and invisible to `cleanupOrphanFocusWorktrees` (which only
70
+ * reaps `agent-*` dirs).
71
+ */
72
+ export function operatorWorktreePath(worktreeDir, branchName) {
73
+ return join(worktreeDir, `operator-${branchName.replace(/\//g, '-')}`);
74
+ }
75
+ /**
76
+ * Resolve the editor command with precedence: explicit `--editor` > `TELORA_EDITOR`
77
+ * env > `DEFAULT_EDITOR`.
78
+ */
79
+ export function resolveEditor(optEditor, env = process.env) {
80
+ if (optEditor && optEditor.trim())
81
+ return optEditor.trim();
82
+ const fromEnv = env.TELORA_EDITOR;
83
+ if (fromEnv && fromEnv.trim())
84
+ return fromEnv.trim();
85
+ return DEFAULT_EDITOR;
86
+ }
87
+ /**
88
+ * Pure resolution of a focus ref (UUID or focus key) to its branch, from
89
+ * already-fetched worktree rows + focus list. No I/O -- hermetic to test.
90
+ *
91
+ * Returns null when the ref is neither a UUID nor a key, when a key does not
92
+ * match any focus, or when there is no branch to open (no row and no derivable
93
+ * branch).
94
+ */
95
+ export function resolveFocusWorktreeFromData(params) {
96
+ const { focusRef, worktrees, focuses } = params;
97
+ // 1. Resolve the ref to a focus id (+ carry the focus row for name/role).
98
+ let focusId = null;
99
+ let focus;
100
+ if (looksLikeUuid(focusRef)) {
101
+ focusId = focusRef;
102
+ focus = focuses.find((f) => f.id === focusRef);
103
+ }
104
+ else if (looksLikeFocusKey(focusRef)) {
105
+ focus = focuses.find((f) => f.focusKey === focusRef);
106
+ focusId = focus?.id ?? null;
107
+ }
108
+ else {
109
+ return null; // Neither a UUID nor a focus key.
110
+ }
111
+ if (!focusId)
112
+ return null; // Key matched no focus.
113
+ const focusName = focus?.name ?? focusId;
114
+ // 2. Prefer the focus_worktrees row -- it survives a merge, so this is the
115
+ // branch source for BOTH in-flight and already-merged focuses.
116
+ const row = worktrees.find((w) => w.focusId === focusId);
117
+ if (row) {
118
+ return {
119
+ focusId,
120
+ focusName,
121
+ branchName: row.branchName,
122
+ productId: row.productId ?? focus?.productId ?? null,
123
+ fromWorktreeRow: true,
124
+ };
125
+ }
126
+ // 3. Fallback: derive the branch when we know the focus name + assigned role.
127
+ // runOpen verifies the branch exists in git before using it, so a derived
128
+ // branch git does not have degrades to the no-branch error path.
129
+ if (focus?.name && focus.assignedAgentRoleName) {
130
+ return {
131
+ focusId,
132
+ focusName,
133
+ branchName: deriveFocusBranchName(focus.assignedAgentRoleName, focus.name, focusId),
134
+ productId: focus.productId ?? null,
135
+ fromWorktreeRow: false,
136
+ };
137
+ }
138
+ return null; // No row, no derivable branch.
139
+ }
140
+ /**
141
+ * Resolve a focus to an operator worktree and launch the editor there.
142
+ *
143
+ * Returns a result object; the CLI wrapper maps `ok === false` to a non-zero
144
+ * exit. Never touches the team's live worktree.
145
+ */
146
+ export async function runOpen(params) {
147
+ const { focusRef, editor, resolveWorktreeRecord, git, launchEditor, io } = params;
148
+ const resolution = await resolveWorktreeRecord(focusRef);
149
+ if (!resolution) {
150
+ return {
151
+ ok: false,
152
+ editor,
153
+ failure: 'no_branch',
154
+ message: `Focus "${focusRef}" has no branch to open: no focus_worktrees row and no ` +
155
+ `derivable branch. It has never been provisioned by the daemon (no delivery ` +
156
+ `has entered coding yet). Nothing to open.`,
157
+ };
158
+ }
159
+ const { branchName, focusName, repoPath, worktreeDir } = resolution;
160
+ const teamPath = teamWorktreePath(worktreeDir, branchName);
161
+ const operatorPath = operatorWorktreePath(worktreeDir, branchName);
162
+ // Reuse an existing operator worktree on this branch -- do not recreate.
163
+ if (git.pathExists(operatorPath)) {
164
+ io.out(`Reusing operator worktree for "${focusName}" at ${operatorPath} (branch ${branchName}).\n`);
165
+ launchEditor(editor, operatorPath);
166
+ return {
167
+ ok: true,
168
+ editor,
169
+ message: `Opened ${operatorPath} in ${editor}.`,
170
+ focusName,
171
+ branchName,
172
+ operatorWorktreePath: operatorPath,
173
+ teamWorktreePath: teamPath,
174
+ reused: true,
175
+ createdWorktree: false,
176
+ inFlight: git.pathExists(teamPath),
177
+ };
178
+ }
179
+ // The branch must exist to base a worktree on it. A merged focus whose branch
180
+ // was deleted, or a never-provisioned focus, lands here.
181
+ if (!git.branchExists(branchName, repoPath)) {
182
+ return {
183
+ ok: false,
184
+ editor,
185
+ failure: 'no_branch',
186
+ focusName,
187
+ branchName,
188
+ message: `Focus "${focusName}" resolves to branch ${branchName}, but that branch does ` +
189
+ `not exist in ${repoPath} (it may have been deleted after merge). Nothing to open.`,
190
+ };
191
+ }
192
+ // Create an OPERATOR-owned worktree on the focus branch. This is a distinct
193
+ // path from the team's worktree (${teamPath}) and never touches it.
194
+ try {
195
+ git.addWorktree(repoPath, operatorPath, branchName);
196
+ }
197
+ catch (err) {
198
+ return {
199
+ ok: false,
200
+ editor,
201
+ failure: 'worktree_create_failed',
202
+ focusName,
203
+ branchName,
204
+ message: `Failed to create operator worktree at ${operatorPath}: ` +
205
+ `${err instanceof Error ? err.message : String(err)}`,
206
+ };
207
+ }
208
+ const inFlight = git.pathExists(teamPath);
209
+ io.out(inFlight
210
+ ? `Focus "${focusName}" is in flight; opened a separate operator worktree at ` +
211
+ `${operatorPath} (branch ${branchName}). The team's worktree at ${teamPath} is untouched.\n`
212
+ : `Focus "${focusName}" branch ${branchName} checked out at operator worktree ${operatorPath}.\n`);
213
+ launchEditor(editor, operatorPath);
214
+ return {
215
+ ok: true,
216
+ editor,
217
+ message: `Opened ${operatorPath} in ${editor}.`,
218
+ focusName,
219
+ branchName,
220
+ operatorWorktreePath: operatorPath,
221
+ teamWorktreePath: teamPath,
222
+ reused: false,
223
+ createdWorktree: true,
224
+ inFlight,
225
+ };
226
+ }
227
+ // ── Real CLI wiring ────────────────────────────────────────────────────────
228
+ /** Default git ops backed by the real filesystem + git. */
229
+ export const defaultGitOps = {
230
+ pathExists: (path) => existsSync(path),
231
+ branchExists: (branchName, repoPath) => {
232
+ const res = runGitSync(['rev-parse', '--verify', '--quiet', `refs/heads/${branchName}`], repoPath);
233
+ return res.success && res.output.trim().length > 0;
234
+ },
235
+ addWorktree: (repoPath, worktreePath, branchName) => {
236
+ const res = runGitSync(['worktree', 'add', worktreePath, branchName], repoPath);
237
+ if (!res.success) {
238
+ throw new Error(res.error || 'git worktree add failed');
239
+ }
240
+ },
241
+ };
242
+ /** Default editor launcher: detached spawn so the CLI can return immediately. */
243
+ export function defaultLaunchEditor(editorCmd, worktreePath) {
244
+ const child = spawn(editorCmd, [worktreePath], {
245
+ detached: true,
246
+ stdio: 'ignore',
247
+ });
248
+ child.on('error', (err) => {
249
+ process.stderr.write(`Could not launch editor "${editorCmd}": ${err.message}. ` +
250
+ `Open ${worktreePath} manually, or pass --editor / set TELORA_EDITOR.\n`);
251
+ });
252
+ child.unref();
253
+ }
254
+ /** Fetch focus id/key/name/role across all configured products (best-effort). */
255
+ async function fetchFocusLiteAcrossProducts(config) {
256
+ const out = [];
257
+ for (const product of config.products) {
258
+ try {
259
+ const res = await callApi('focus_list', { productId: product.id, limit: 500 });
260
+ for (const f of res.focuses ?? []) {
261
+ out.push({
262
+ id: f.id,
263
+ focusKey: f.focusKey ?? null,
264
+ name: f.name ?? null,
265
+ assignedAgentRoleName: f.assignedAgentRoleName ?? null,
266
+ productId: product.id,
267
+ });
268
+ }
269
+ }
270
+ catch {
271
+ // Best-effort per product: a failure resolving one product's focuses must
272
+ // not block opening a focus in another.
273
+ }
274
+ }
275
+ return out;
276
+ }
277
+ /**
278
+ * Real resolver wiring: hits the Telora API for worktree rows + focuses, runs
279
+ * the pure resolver, then attaches the resolved product's repo + worktree dir.
280
+ */
281
+ async function resolveFocusWorktreeReal(focusRef, config) {
282
+ initSupabase(config);
283
+ const worktrees = await listWorktreeRecords();
284
+ const focuses = await fetchFocusLiteAcrossProducts(config);
285
+ const base = resolveFocusWorktreeFromData({ focusRef, worktrees, focuses });
286
+ if (!base)
287
+ return null;
288
+ const product = base.productId ? findProduct(config, base.productId) : undefined;
289
+ const scoped = product ? configForProduct(config, product) : config;
290
+ return {
291
+ ...base,
292
+ repoPath: scoped.repoPath,
293
+ worktreeDir: scoped.worktreeDir,
294
+ };
295
+ }
296
+ /**
297
+ * CLI entry: load config, wire the real defaults, run, print, and return an
298
+ * exit code (0 success, 1 failure). The `index.ts` action wrapper calls
299
+ * `process.exit` with the returned code.
300
+ */
301
+ export async function runOpenCommand(opts) {
302
+ const io = {
303
+ out: opts.io?.out ?? ((m) => process.stdout.write(m)),
304
+ err: opts.io?.err ?? ((m) => process.stderr.write(m)),
305
+ };
306
+ const config = loadConfig(opts.config);
307
+ const editor = resolveEditor(opts.editor);
308
+ const result = await runOpen({
309
+ focusRef: opts.focusRef,
310
+ editor,
311
+ resolveWorktreeRecord: (ref) => resolveFocusWorktreeReal(ref, config),
312
+ git: defaultGitOps,
313
+ launchEditor: defaultLaunchEditor,
314
+ io,
315
+ });
316
+ if (!result.ok) {
317
+ io.err(result.message.endsWith('\n') ? result.message : `${result.message}\n`);
318
+ return 1;
319
+ }
320
+ io.out(result.message.endsWith('\n') ? result.message : `${result.message}\n`);
321
+ return 0;
322
+ }
323
+ //# sourceMappingURL=open.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open.js","sourceRoot":"","sources":["../../src/cli/open.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"focus-engine.d.ts","sourceRoot":"","sources":["../src/focus-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,YAAY,EACZ,aAAa,EACb,UAAU,EACX,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAA2B,gBAAgB,EAAmD,MAAM,qBAAqB,CAAC;AA8DjI;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,MAAM,EAClB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAOT;AAMD,qBAAa,WAAY,YAAW,eAAe;IACjD,QAAQ,CAAC,IAAI,WAAW;IAExB,wCAAwC;IACxC,OAAO,CAAC,MAAM,CAA6B;IAE3C,kEAAkE;IAClE,OAAO,CAAC,QAAQ,CAAiC;IAEjD,sEAAsE;IACtE,OAAO,CAAC,SAAS,CAAwC;IAEzD,8EAA8E;IAC9E,OAAO,CAAC,qBAAqB,CAA6B;IAE1D;;;OAGG;IACH,WAAW,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI;IAUlC,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAkEjE,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAsEjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAmXd,aAAa;YA8Bb,wBAAwB;IA+BtC,IAAI,IAAI,IAAI;IAeN,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IA2G/B,WAAW,IAAI,YAAY;IA0D3B,gBAAgB,IAAI,aAAa;IAwBjC;;;OAGG;IACH,OAAO,CAAC,aAAa;CAStB"}
1
+ {"version":3,"file":"focus-engine.d.ts","sourceRoot":"","sources":["../src/focus-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,YAAY,EACZ,aAAa,EACb,UAAU,EACX,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAA2B,gBAAgB,EAAmD,MAAM,qBAAqB,CAAC;AA+DjI;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,MAAM,EAClB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAOT;AAMD,qBAAa,WAAY,YAAW,eAAe;IACjD,QAAQ,CAAC,IAAI,WAAW;IAExB,wCAAwC;IACxC,OAAO,CAAC,MAAM,CAA6B;IAE3C,kEAAkE;IAClE,OAAO,CAAC,QAAQ,CAAiC;IAEjD,sEAAsE;IACtE,OAAO,CAAC,SAAS,CAAwC;IAEzD,8EAA8E;IAC9E,OAAO,CAAC,qBAAqB,CAA6B;IAE1D;;;OAGG;IACH,WAAW,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI;IAUlC,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAkEjE,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAsEjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YA6Yd,aAAa;YA8Bb,wBAAwB;IA+BtC,IAAI,IAAI,IAAI;IAeN,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IA2G/B,WAAW,IAAI,YAAY;IA0D3B,gBAAgB,IAAI,aAAa;IAwBjC;;;OAGG;IACH,OAAO,CAAC,aAAa;CAStB"}
@@ -43,6 +43,7 @@ import { runCascadeProseTick, runNegativeBranchTick } from './cascade-restatemen
43
43
  import { runPrdControllerTickAll } from './prd-controller.js';
44
44
  import { runMergeBackTick } from './merge-back-loop.js';
45
45
  import { runDriftEvalTick, runPendingDriftEvalRequestsTick } from './drift-eval-loop.js';
46
+ import { runSensorSweepTick } from './sensor-sweep.js';
46
47
  import { reconcileDaemonProducts, buildReconcileDeps } from './config-reconcile.js';
47
48
  import { buildDefaultSecurityScanDeps, registerScanner, runSecurityScanTick, } from './security-scan-engine.js';
48
49
  import { depsScanner } from './scanners/deps.js';
@@ -444,6 +445,29 @@ export class FocusEngine {
444
445
  else {
445
446
  console.log('Drift-eval loop tick: disabled (TELORA_DRIFT_EVAL_LOOP=0)');
446
447
  }
448
+ // Artifact-health sensor sweep (30 minutes). Deterministic whole-repo mining
449
+ // (churn/hotspot mass, lint-ratchet counts, dependency freshness) into the
450
+ // code_metrics DC baseline. Drift-proportional: skips a product whose repo
451
+ // HEAD + manifest hash are unchanged since its last snapshot. Opt-out via
452
+ // TELORA_SENSOR_SWEEP_LOOP=0; default ON.
453
+ if (shouldRunLoop('TELORA_SENSOR_SWEEP_LOOP')) {
454
+ console.log('Sensor-sweep loop tick: enabled (set TELORA_SENSOR_SWEEP_LOOP=0 to disable)');
455
+ this.intervals.push(safeInterval(async () => {
456
+ try {
457
+ const counts = await runSensorSweepTick(config);
458
+ if (counts.snapshotsWritten > 0) {
459
+ console.log(`[sensor-sweep] tick: productsScanned=${counts.productsScanned} ` +
460
+ `snapshotsWritten=${counts.snapshotsWritten} skippedUnchanged=${counts.skippedUnchanged}`);
461
+ }
462
+ }
463
+ catch (err) {
464
+ console.warn('[sensor-sweep] tick failed:', err.message);
465
+ }
466
+ }, 1_800_000, 'sensor-sweep-loop'));
467
+ }
468
+ else {
469
+ console.log('Sensor-sweep loop tick: disabled (TELORA_SENSOR_SWEEP_LOOP=0)');
470
+ }
447
471
  // Security scan loop (per-product cron cadence inside).
448
472
  // Pluggable scanner engine: deps / signatures / workflow.
449
473
  // Opt-out via TELORA_SECURITY_SCAN_LOOP=0; default ON.