kibi-opencode 0.9.0 → 0.10.0

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.
Files changed (51) hide show
  1. package/README.md +36 -12
  2. package/dist/brief-intent.d.ts +15 -4
  3. package/dist/brief-intent.js +63 -25
  4. package/dist/briefing-runtime.js +2 -1
  5. package/dist/config.d.ts +3 -0
  6. package/dist/config.js +9 -0
  7. package/dist/e2e-coverage-signals.d.ts +6 -0
  8. package/dist/e2e-coverage-signals.js +186 -0
  9. package/dist/file-entity-links.d.ts +15 -0
  10. package/dist/file-entity-links.js +254 -0
  11. package/dist/file-operation-reminders.d.ts +24 -0
  12. package/dist/file-operation-reminders.js +55 -0
  13. package/dist/file-operation-state.d.ts +29 -0
  14. package/dist/file-operation-state.js +113 -0
  15. package/dist/idle-brief-audit.d.ts +36 -0
  16. package/dist/idle-brief-audit.js +186 -0
  17. package/dist/idle-brief-paths.d.ts +6 -0
  18. package/dist/idle-brief-paths.js +120 -0
  19. package/dist/idle-brief-reader.d.ts +25 -0
  20. package/dist/idle-brief-reader.js +142 -0
  21. package/dist/idle-brief-runtime.d.ts +48 -0
  22. package/dist/idle-brief-runtime.js +443 -0
  23. package/dist/idle-brief-store.d.ts +96 -0
  24. package/dist/idle-brief-store.js +209 -0
  25. package/dist/index.d.ts +14 -1
  26. package/dist/index.js +626 -50
  27. package/dist/init-kibi-alias.d.ts +14 -0
  28. package/dist/init-kibi-alias.js +38 -0
  29. package/dist/init-kibi-capability.d.ts +32 -0
  30. package/dist/init-kibi-capability.js +202 -0
  31. package/dist/logger.js +9 -3
  32. package/dist/plugin-startup.d.ts +1 -0
  33. package/dist/plugin-startup.js +11 -2
  34. package/dist/prompt.d.ts +15 -3
  35. package/dist/prompt.js +103 -33
  36. package/dist/reconcile-engine.d.ts +15 -0
  37. package/dist/reconcile-engine.js +112 -0
  38. package/dist/scheduler.d.ts +1 -0
  39. package/dist/scheduler.js +37 -1
  40. package/dist/session-edit-state.d.ts +25 -0
  41. package/dist/session-edit-state.js +177 -0
  42. package/dist/session-fingerprint.d.ts +11 -0
  43. package/dist/session-fingerprint.js +21 -0
  44. package/dist/source-linked-guidance.d.ts +1 -2
  45. package/dist/source-linked-guidance.js +5 -168
  46. package/dist/startup-notifier.js +42 -31
  47. package/dist/toast.d.ts +21 -22
  48. package/dist/toast.js +36 -14
  49. package/dist/tui-brief-delivery.d.ts +47 -0
  50. package/dist/tui-brief-delivery.js +138 -0
  51. package/package.json +4 -3
@@ -0,0 +1,254 @@
1
+ // implements REQ-opencode-file-context-guidance-v1
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import * as path from "node:path";
4
+ import { loadKbSyncPaths } from "./file-filter.js";
5
+ // ── Lightweight YAML parser (symbols.yaml subset) ───────────────────
6
+ //
7
+ // Handles:
8
+ // symbols:
9
+ // - id: SYM-xxx
10
+ // sourceFile: path/to/file
11
+ // links:
12
+ // - REQ-xxx
13
+ // relationships:
14
+ // - type: implements
15
+ // target: REQ-xxx
16
+ //
17
+ // And bare array format (no wrapping `symbols:` key):
18
+ // - id: SYM-xxx
19
+ // ...
20
+ // implements REQ-opencode-file-context-guidance-v1
21
+ export function parseSymbolsYaml(content) {
22
+ const entries = [];
23
+ const lines = content.split("\n");
24
+ let current = null;
25
+ let section = "none";
26
+ let pendingRel = null;
27
+ function flushRel() {
28
+ if (pendingRel?.type && pendingRel.target && current?.relationships) {
29
+ current.relationships.push({
30
+ type: pendingRel.type,
31
+ target: pendingRel.target,
32
+ });
33
+ }
34
+ pendingRel = null;
35
+ }
36
+ function flushEntry() {
37
+ flushRel();
38
+ if (current?.id && current?.sourceFile) {
39
+ entries.push(current);
40
+ }
41
+ current = null;
42
+ section = "none";
43
+ }
44
+ for (const raw of lines) {
45
+ if (raw.trim().startsWith("#"))
46
+ continue;
47
+ // New entry: " - id: ..."
48
+ const entryMatch = raw.match(/^\s+-\s+id:\s*(.+)$/);
49
+ if (entryMatch) {
50
+ flushEntry();
51
+ const entryId = entryMatch[1];
52
+ if (entryId === undefined)
53
+ continue;
54
+ current = { id: entryId.trim(), links: [], relationships: [] };
55
+ section = "none";
56
+ continue;
57
+ }
58
+ if (!current)
59
+ continue;
60
+ // sourceFile
61
+ const srcMatch = raw.match(/^\s+sourceFile:\s*(.+)$/);
62
+ if (srcMatch) {
63
+ const sourceFile = srcMatch[1];
64
+ if (sourceFile === undefined)
65
+ continue;
66
+ current.sourceFile = sourceFile.trim();
67
+ section = "none";
68
+ continue;
69
+ }
70
+ // links section header
71
+ if (/^\s+links:\s*$/.test(raw)) {
72
+ flushRel();
73
+ section = "links";
74
+ continue;
75
+ }
76
+ // relationships section header
77
+ if (/^\s+relationships:\s*$/.test(raw)) {
78
+ flushRel();
79
+ section = "relationships";
80
+ continue;
81
+ }
82
+ // Link item: " - REQ-xxx"
83
+ if (section === "links") {
84
+ const linkMatch = raw.match(/^\s+-\s+(REQ-[A-Za-z0-9_-]+)\s*$/);
85
+ if (linkMatch) {
86
+ const linkId = linkMatch[1];
87
+ if (linkId !== undefined && current.links) {
88
+ current.links.push(linkId);
89
+ }
90
+ continue;
91
+ }
92
+ }
93
+ // Relationship type: " - type: implements"
94
+ if (section === "relationships") {
95
+ const relTypeMatch = raw.match(/^\s+-\s+type:\s*(.+)$/);
96
+ if (relTypeMatch) {
97
+ flushRel();
98
+ const relationType = relTypeMatch[1];
99
+ if (relationType === undefined)
100
+ continue;
101
+ pendingRel = { type: relationType.trim() };
102
+ continue;
103
+ }
104
+ // Relationship target: " target: REQ-..."
105
+ const relTargetMatch = raw.match(/^\s+target:\s*(.+)$/);
106
+ if (relTargetMatch && pendingRel) {
107
+ const target = relTargetMatch[1];
108
+ if (target === undefined)
109
+ continue;
110
+ pendingRel.target = target.trim();
111
+ continue;
112
+ }
113
+ }
114
+ }
115
+ flushEntry();
116
+ return entries;
117
+ }
118
+ // ── Doc-path identity mapping ───────────────────────────────────────
119
+ const DOC_ENTITY_PATTERN = /^(REQ|SCEN|TEST|ADR|FLAG|EVT|FACT)-[A-Za-z0-9_-]+\.md$/;
120
+ // implements REQ-opencode-file-context-guidance-v1
121
+ function resolveDocPathIdentity(relPath, syncPaths) {
122
+ const basename = path.posix.basename(relPath);
123
+ if (!DOC_ENTITY_PATTERN.test(basename))
124
+ return null;
125
+ const entityId = basename.replace(/\.md$/, "");
126
+ // Check if the file lives under one of the configured doc roots
127
+ const docRootKeys = [
128
+ "requirements",
129
+ "scenarios",
130
+ "tests",
131
+ "adr",
132
+ "flags",
133
+ "events",
134
+ "facts",
135
+ ];
136
+ // Normalize the relative path for matching
137
+ const normalizedRel = relPath.split(path.sep).join("/");
138
+ for (const key of docRootKeys) {
139
+ const pattern = syncPaths[key];
140
+ if (!pattern)
141
+ continue;
142
+ // Strip glob from pattern to get the root dir prefix
143
+ // e.g. "documentation/requirements/**/*.md" → "documentation/requirements"
144
+ const rootDir = pattern.replace(/\/\*\*\/.*$/, "").replace(/\/+$/, "");
145
+ if (normalizedRel.startsWith(rootDir + "/")) {
146
+ return entityId;
147
+ }
148
+ }
149
+ // If no specific root matched but path starts with documentation/,
150
+ // still accept (covers default configuration)
151
+ return null;
152
+ }
153
+ // ── Symbols file resolution ─────────────────────────────────────────
154
+ function readSymbolsManifest(worktree, syncPaths) {
155
+ const symbolsPathRaw = syncPaths.symbols;
156
+ if (!symbolsPathRaw)
157
+ return [];
158
+ const symbolsPath = path.isAbsolute(symbolsPathRaw)
159
+ ? symbolsPathRaw
160
+ : path.join(worktree, symbolsPathRaw);
161
+ if (!existsSync(symbolsPath))
162
+ return [];
163
+ const content = readFileSync(symbolsPath, "utf8");
164
+ return parseSymbolsYaml(content);
165
+ }
166
+ function normalizeFilePath(worktree, filePath) {
167
+ // Normalize to forward-slash relative path from worktree
168
+ const absPath = path.isAbsolute(filePath)
169
+ ? filePath
170
+ : path.resolve(worktree, filePath);
171
+ return path
172
+ .relative(worktree, absPath)
173
+ .split(path.sep)
174
+ .join("/");
175
+ }
176
+ // ── Public API ──────────────────────────────────────────────────────
177
+ // implements REQ-opencode-file-context-guidance-v1
178
+ export function getFileLinkedEntityIds(worktree, filePath) {
179
+ try {
180
+ const syncPaths = loadKbSyncPaths(worktree);
181
+ const relPath = normalizeFilePath(worktree, filePath);
182
+ // Check doc-path identity first
183
+ const docId = resolveDocPathIdentity(relPath, syncPaths);
184
+ if (docId) {
185
+ return { ids: [docId], source: "doc-path" };
186
+ }
187
+ // Try symbols manifest
188
+ const symbols = readSymbolsManifest(worktree, syncPaths);
189
+ const matchedRows = symbols.filter((s) => s.sourceFile === relPath);
190
+ if (matchedRows.length === 0) {
191
+ return { ids: [], source: "none" };
192
+ }
193
+ const seen = new Set();
194
+ const orderedIds = [];
195
+ // Priority order: implements → covered_by → executable_for
196
+ const relPriority = ["implements", "covered_by", "executable_for"];
197
+ // First pass: collect relationships grouped by priority type, preserving file order within each type
198
+ for (const priorityType of relPriority) {
199
+ for (const row of matchedRows) {
200
+ for (const r of row.relationships ?? []) {
201
+ if (r.type === priorityType) {
202
+ const id = r.target;
203
+ if (!seen.has(id)) {
204
+ seen.add(id);
205
+ orderedIds.push(id);
206
+ if (orderedIds.length >= 3)
207
+ return { ids: orderedIds.slice(0, 3), source: "symbols" };
208
+ }
209
+ }
210
+ }
211
+ }
212
+ }
213
+ // Second pass: fall back to static links, preserving file order
214
+ for (const row of matchedRows) {
215
+ for (const l of row.links ?? []) {
216
+ if (!seen.has(l)) {
217
+ seen.add(l);
218
+ orderedIds.push(l);
219
+ if (orderedIds.length >= 3)
220
+ return { ids: orderedIds.slice(0, 3), source: "symbols" };
221
+ }
222
+ }
223
+ }
224
+ return { ids: orderedIds.slice(0, 3), source: "symbols" };
225
+ }
226
+ catch {
227
+ return { ids: [], source: "none" };
228
+ }
229
+ }
230
+ // implements REQ-opencode-file-context-guidance-v1
231
+ export function getFileLinkedTargetsByType(worktree, filePath, relationshipTypes) {
232
+ try {
233
+ const syncPaths = loadKbSyncPaths(worktree);
234
+ const relPath = normalizeFilePath(worktree, filePath);
235
+ const symbols = readSymbolsManifest(worktree, syncPaths);
236
+ const matchedRows = symbols.filter((s) => s.sourceFile === relPath);
237
+ if (matchedRows.length === 0)
238
+ return [];
239
+ const targets = [];
240
+ const seen = new Set();
241
+ for (const row of matchedRows) {
242
+ for (const r of row.relationships ?? []) {
243
+ if (relationshipTypes.includes(r.type) && !seen.has(r.target)) {
244
+ seen.add(r.target);
245
+ targets.push(r.target);
246
+ }
247
+ }
248
+ }
249
+ return targets;
250
+ }
251
+ catch {
252
+ return [];
253
+ }
254
+ }
@@ -0,0 +1,24 @@
1
+ import type { RepoPosture } from "./repo-posture.js";
2
+ import type { PathKind } from "./path-kind.js";
3
+ import type { RiskClass } from "./risk-classifier.js";
4
+ import type { ReminderKind } from "./file-operation-state.js";
5
+ import type { E2eCoverageSignal } from "./e2e-coverage-signals.js";
6
+ export interface LinkedEntityResult {
7
+ ids: string[];
8
+ source: "symbols" | "doc-path" | "none";
9
+ }
10
+ export interface DeriveFileOperationReminderParams {
11
+ normalizedPath: string;
12
+ lifecycle: "created" | "edited" | "deleted";
13
+ pathKind: PathKind;
14
+ linkedEntityResult: LinkedEntityResult;
15
+ e2eSignal: E2eCoverageSignal;
16
+ currentSemanticRisk: RiskClass;
17
+ posture: RepoPosture;
18
+ }
19
+ export interface DeriveFileOperationReminderResult {
20
+ lifecycleReminder: string | null;
21
+ e2eReminder: string | null;
22
+ reminderKindsToMark: ReminderKind[];
23
+ }
24
+ export declare function deriveFileOperationReminder(params: DeriveFileOperationReminderParams): DeriveFileOperationReminderResult;
@@ -0,0 +1,55 @@
1
+ // ── Lifecycle reminder text ─────────────────────────────────────
2
+ const NEW_FILE_REMINDER = "- New file detected. Add or update the necessary Kibi entities and traceability before completing this task.";
3
+ const DELETED_WITH_IDS_REMINDER = (ids) => `- Deleted file had linked Kibi entities: ${ids}. Update Kibi to keep traceability accurate.`;
4
+ const DELETED_NO_IDS_REMINDER = "- Deleted file had no linked Kibi entities. Update Kibi if this removal changes documented behavior or traceability.";
5
+ // ── Main exported function ────────────────────────────────────
6
+ // implements REQ-opencode-file-context-guidance-v1
7
+ export function deriveFileOperationReminder(params) {
8
+ const { lifecycle, pathKind, linkedEntityResult, e2eSignal, posture, } = params;
9
+ // Check if posture allows lifecycle reminders
10
+ const isAuthoritativePosture = posture === "root_active" || posture === "hybrid_root_plus_vendored";
11
+ // Derive lifecycle reminder
12
+ let lifecycleReminder = null;
13
+ const reminderKindsToMark = [];
14
+ if (isAuthoritativePosture) {
15
+ if (lifecycle === "created") {
16
+ // Only emit create reminder for code files (not documentation, not KB docs)
17
+ if (pathKind === "code") {
18
+ lifecycleReminder = NEW_FILE_REMINDER;
19
+ reminderKindsToMark.push("kibi_write");
20
+ }
21
+ }
22
+ else if (lifecycle === "edited") {
23
+ // No generic lifecycle reminder for edited files
24
+ // Existing semantic risk guidance remains primary
25
+ }
26
+ else if (lifecycle === "deleted") {
27
+ const ids = linkedEntityResult.ids;
28
+ if (ids.length > 0) {
29
+ lifecycleReminder = DELETED_WITH_IDS_REMINDER(ids.join(", "));
30
+ reminderKindsToMark.push("kibi_delete");
31
+ }
32
+ else {
33
+ lifecycleReminder = DELETED_NO_IDS_REMINDER;
34
+ reminderKindsToMark.push("kibi_delete");
35
+ }
36
+ }
37
+ }
38
+ // Derive e2e reminder (only when e2e signal exists)
39
+ // E2e reminders are NOT posture-gated - they're always relevant
40
+ let e2eReminder = null;
41
+ if (e2eSignal.level !== "none" && e2eSignal.reminderText !== null) {
42
+ e2eReminder = e2eSignal.reminderText;
43
+ if (lifecycle === "deleted") {
44
+ reminderKindsToMark.push("e2e_delete");
45
+ }
46
+ else {
47
+ reminderKindsToMark.push("e2e_write");
48
+ }
49
+ }
50
+ return {
51
+ lifecycleReminder,
52
+ e2eReminder,
53
+ reminderKindsToMark,
54
+ };
55
+ }
@@ -0,0 +1,29 @@
1
+ export type FileLifecycle = "created" | "edited" | "deleted";
2
+ export type ReminderKind = "kibi_write" | "kibi_delete" | "e2e_write" | "e2e_delete";
3
+ export interface PendingLifecycleEvent {
4
+ /** Normalized file path (relative to worktree root). */
5
+ normalizedPath: string;
6
+ /** Coalesced lifecycle event for this path. */
7
+ lifecycle: FileLifecycle;
8
+ /** Timestamp (ms) of the lifecycle event. */
9
+ timestamp: number;
10
+ }
11
+ export interface FileOperationState {
12
+ /** Normalize file path relative to worktree root. */
13
+ normalizePath(filePath: string): string;
14
+ /** Record a lifecycle event for a file, coalescing with existing events. */
15
+ recordLifecycle(filePath: string, lifecycle: FileLifecycle, timestamp?: number): void;
16
+ /** Peek at pending lifecycle event, preferring specified path if available. */
17
+ peekPending(preferredPath?: string): PendingLifecycleEvent | null;
18
+ /** Consume pending lifecycle event for a specific path. */
19
+ consumePending(filePath: string): void;
20
+ /** Check if a reminder has already been shown for a path/kind combo. */
21
+ hasShown(filePath: string, reminderKind: ReminderKind): boolean;
22
+ /** Mark a reminder as shown for a path/kind combo. */
23
+ markShown(filePath: string, reminderKind: ReminderKind): void;
24
+ }
25
+ export declare function createFileOperationState(opts: {
26
+ worktree: string;
27
+ /** Custom clock for testing. Defaults to Date.now. */
28
+ now?: () => number;
29
+ }): FileOperationState;
@@ -0,0 +1,113 @@
1
+ import * as path from "node:path";
2
+ // ---------------------------------------------------------------------------
3
+ // Factory function
4
+ // ---------------------------------------------------------------------------
5
+ export function createFileOperationState(opts) {
6
+ const worktree = opts.worktree;
7
+ const now = opts.now ?? Date.now;
8
+ // ---- Per-instance state (no module globals) ----
9
+ /**
10
+ * Pending lifecycle events keyed by normalized path.
11
+ * Each path has at most one coalesced lifecycle state.
12
+ */
13
+ const pendingLifecycleEvents = new Map();
14
+ /**
15
+ * Reminder suppression state: (normalized path + reminder kind) -> shown flag.
16
+ * Keeps path-aware, kind-aware suppression separate from GuidanceCache.
17
+ */
18
+ const reminderSuppression = new Map();
19
+ // ---- Internal helpers ----
20
+ /**
21
+ * Coalesce lifecycle events using precedence rules:
22
+ * - created + edited -> created
23
+ * - edited + edited -> edited
24
+ * - created|edited + deleted -> deleted
25
+ * - deleted + created|edited -> deleted
26
+ */
27
+ function coalesceLifecycle(existing, incoming) {
28
+ if (existing === undefined) {
29
+ return incoming;
30
+ }
31
+ // created + edited -> created
32
+ if (existing === "created" && incoming === "edited") {
33
+ return "created";
34
+ }
35
+ // edited + edited -> edited
36
+ if (existing === "edited" && incoming === "edited") {
37
+ return "edited";
38
+ }
39
+ // created|edited + deleted -> deleted
40
+ if ((existing === "created" || existing === "edited") && incoming === "deleted") {
41
+ return "deleted";
42
+ }
43
+ // deleted + created|edited -> deleted
44
+ if (existing === "deleted" && (incoming === "created" || incoming === "edited")) {
45
+ return "deleted";
46
+ }
47
+ // Fallback: use incoming
48
+ return incoming;
49
+ }
50
+ function normalizeSessionPath(filePath) {
51
+ if (path.isAbsolute(filePath)) {
52
+ const relativePath = path.relative(worktree, filePath);
53
+ // Keep absolute path if it escapes worktree
54
+ return relativePath.startsWith("..") ? filePath : relativePath;
55
+ }
56
+ // Normalize leading ./ and trailing slashes
57
+ const normalized = path.normalize(filePath);
58
+ return normalized.startsWith("./") ? normalized.slice(2) : normalized;
59
+ }
60
+ function getSuppressionKey(filePath, kind) {
61
+ const normalized = normalizeSessionPath(filePath);
62
+ return `${normalized}:${kind}`;
63
+ }
64
+ // ---- Public API ----
65
+ function normalizePath(filePath) {
66
+ return normalizeSessionPath(filePath);
67
+ }
68
+ function recordLifecycle(filePath, lifecycle, timestamp) {
69
+ const normalized = normalizeSessionPath(filePath);
70
+ const existing = pendingLifecycleEvents.get(normalized);
71
+ const coalesced = coalesceLifecycle(existing?.lifecycle, lifecycle);
72
+ pendingLifecycleEvents.set(normalized, {
73
+ normalizedPath: normalized,
74
+ lifecycle: coalesced,
75
+ timestamp: timestamp ?? now(),
76
+ });
77
+ }
78
+ function peekPending(preferredPath) {
79
+ if (preferredPath !== undefined) {
80
+ const normalized = normalizeSessionPath(preferredPath);
81
+ const preferred = pendingLifecycleEvents.get(normalized);
82
+ return preferred ?? null;
83
+ }
84
+ // No preferred path specified, return most recent pending event
85
+ let mostRecent = null;
86
+ for (const event of pendingLifecycleEvents.values()) {
87
+ if (mostRecent === null || event.timestamp > mostRecent.timestamp) {
88
+ mostRecent = event;
89
+ }
90
+ }
91
+ return mostRecent;
92
+ }
93
+ function consumePending(filePath) {
94
+ const normalized = normalizeSessionPath(filePath);
95
+ pendingLifecycleEvents.delete(normalized);
96
+ }
97
+ function hasShown(filePath, reminderKind) {
98
+ const key = getSuppressionKey(filePath, reminderKind);
99
+ return reminderSuppression.get(key) ?? false;
100
+ }
101
+ function markShown(filePath, reminderKind) {
102
+ const key = getSuppressionKey(filePath, reminderKind);
103
+ reminderSuppression.set(key, true);
104
+ }
105
+ return {
106
+ normalizePath,
107
+ recordLifecycle,
108
+ peekPending,
109
+ consumePending,
110
+ hasShown,
111
+ markShown,
112
+ };
113
+ }
@@ -0,0 +1,36 @@
1
+ export interface AuditCursor {
2
+ lastTimestamp: string;
3
+ lastOperation: string;
4
+ entryCount: number;
5
+ fileSize: number;
6
+ }
7
+ export interface AuditDelta {
8
+ hasChanges: boolean;
9
+ entries: AuditEntry[];
10
+ newCursor: AuditCursor;
11
+ contentHash: string;
12
+ }
13
+ export interface AuditEntry {
14
+ timestamp: string;
15
+ operation: string;
16
+ entityId: string;
17
+ payload?: AuditEntityPayload | AuditRelationshipPayload | null;
18
+ }
19
+ export interface AuditEntityPayload {
20
+ kind: "entity";
21
+ entityType: string;
22
+ changeKind?: "created" | "updated";
23
+ title?: string;
24
+ source?: string;
25
+ textRef?: string;
26
+ properties: Record<string, unknown>;
27
+ }
28
+ export interface AuditRelationshipPayload {
29
+ kind: "relationship";
30
+ relationshipType: string;
31
+ properties: Record<string, unknown>;
32
+ }
33
+ export declare function computeAuditDelta(workspaceRoot: string, branch: string, previousCursor: AuditCursor | null): AuditDelta;
34
+ export declare function getLatestAuditCursor(workspaceRoot: string, branch: string): AuditCursor | null;
35
+ export declare function getAuditTailCursor(workspaceRoot: string, branch: string): AuditCursor | null;
36
+ export declare function guardBranchChanged(startBranch: string, currentBranch: string): boolean;