instar 1.3.236 → 1.3.238

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 (62) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +35 -2
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  5. package/dist/config/ConfigDefaults.js +8 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/ParallelActivityIndex.d.ts +62 -0
  8. package/dist/core/ParallelActivityIndex.d.ts.map +1 -0
  9. package/dist/core/ParallelActivityIndex.js +129 -0
  10. package/dist/core/ParallelActivityIndex.js.map +1 -0
  11. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  12. package/dist/core/PostUpdateMigrator.js +16 -0
  13. package/dist/core/PostUpdateMigrator.js.map +1 -1
  14. package/dist/core/types.d.ts +37 -0
  15. package/dist/core/types.d.ts.map +1 -1
  16. package/dist/core/types.js.map +1 -1
  17. package/dist/monitoring/GeminiLoopDriver.d.ts +112 -0
  18. package/dist/monitoring/GeminiLoopDriver.d.ts.map +1 -0
  19. package/dist/monitoring/GeminiLoopDriver.js +111 -0
  20. package/dist/monitoring/GeminiLoopDriver.js.map +1 -0
  21. package/dist/monitoring/GeminiLoopRunner.d.ts +93 -0
  22. package/dist/monitoring/GeminiLoopRunner.d.ts.map +1 -0
  23. package/dist/monitoring/GeminiLoopRunner.js +134 -0
  24. package/dist/monitoring/GeminiLoopRunner.js.map +1 -0
  25. package/dist/monitoring/ParallelWorkOverlap.d.ts +59 -0
  26. package/dist/monitoring/ParallelWorkOverlap.d.ts.map +1 -0
  27. package/dist/monitoring/ParallelWorkOverlap.js +86 -0
  28. package/dist/monitoring/ParallelWorkOverlap.js.map +1 -0
  29. package/dist/monitoring/ParallelWorkSentinel.d.ts +59 -0
  30. package/dist/monitoring/ParallelWorkSentinel.d.ts.map +1 -0
  31. package/dist/monitoring/ParallelWorkSentinel.js +79 -0
  32. package/dist/monitoring/ParallelWorkSentinel.js.map +1 -0
  33. package/dist/monitoring/geminiLoopProduction.d.ts +62 -0
  34. package/dist/monitoring/geminiLoopProduction.d.ts.map +1 -0
  35. package/dist/monitoring/geminiLoopProduction.js +124 -0
  36. package/dist/monitoring/geminiLoopProduction.js.map +1 -0
  37. package/dist/providers/adapters/gemini-cli/transport/geminiSpawn.d.ts +14 -0
  38. package/dist/providers/adapters/gemini-cli/transport/geminiSpawn.d.ts.map +1 -1
  39. package/dist/providers/adapters/gemini-cli/transport/geminiSpawn.js +16 -0
  40. package/dist/providers/adapters/gemini-cli/transport/geminiSpawn.js.map +1 -1
  41. package/dist/scaffold/templates.d.ts.map +1 -1
  42. package/dist/scaffold/templates.js +4 -0
  43. package/dist/scaffold/templates.js.map +1 -1
  44. package/dist/server/AgentServer.d.ts +9 -0
  45. package/dist/server/AgentServer.d.ts.map +1 -1
  46. package/dist/server/AgentServer.js +83 -0
  47. package/dist/server/AgentServer.js.map +1 -1
  48. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  49. package/dist/server/CapabilityIndex.js +10 -0
  50. package/dist/server/CapabilityIndex.js.map +1 -1
  51. package/dist/server/routes.d.ts +5 -0
  52. package/dist/server/routes.d.ts.map +1 -1
  53. package/dist/server/routes.js +62 -0
  54. package/dist/server/routes.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/data/builtin-manifest.json +64 -64
  57. package/src/scaffold/templates.ts +4 -0
  58. package/upgrades/1.3.237.md +82 -0
  59. package/upgrades/1.3.238.md +68 -0
  60. package/upgrades/side-effects/gemini-loop-driver-wiring.md +59 -0
  61. package/upgrades/side-effects/gemini-multi-turn-loop-driver.md +56 -0
  62. package/upgrades/side-effects/parallel-activity-coherence.md +94 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * GeminiLoopRunner (need-gem-002, increment 2) — the budget-gated, dark-by-default
3
+ * service that makes the GeminiLoopDriver invocable.
4
+ *
5
+ * A multi-turn gemini loop can run for minutes (several turns × a turn each), so
6
+ * it CANNOT block an HTTP request. `startRun` admits a run (enabled? under
7
+ * concurrency cap? budget ok?), kicks off `driver.run()` in the background, and
8
+ * returns a `runId` immediately; the result lands in a bounded in-memory registry
9
+ * retrievable via `getRun`. Ships DARK (`enabled: false`); the `developmentAgent`
10
+ * gate turns it on for dev agents only.
11
+ *
12
+ * Subscription auth is structural: the runner's spawn dep is the production
13
+ * transport spawn (`buildGeminiChildEnv` strips every billing var), so no run can
14
+ * introduce an API key.
15
+ *
16
+ * Spec: docs/specs/gemini-multi-turn-loop-driver.md
17
+ */
18
+ import { type GeminiLoopSpawn, type GeminiLoopHandleCapture, type GeminiLoopBudgetGate, type GeminiLoopResult } from './GeminiLoopDriver.js';
19
+ export interface GeminiLoopDriverConfig {
20
+ enabled: boolean;
21
+ /** Model passed to every turn (explicit -m bypasses the router classifier). */
22
+ model: string;
23
+ /** Hard cap on turns per run. */
24
+ maxTurns: number;
25
+ /** Min ms between turns (anti-spin). */
26
+ minTurnIntervalMs: number;
27
+ /** Max concurrent runs. 1 keeps shared-cwd handle capture unambiguous. */
28
+ maxConcurrent: number;
29
+ /** Bounded registry size — oldest finished runs are evicted past this. */
30
+ maxRetainedRuns: number;
31
+ /** Sentinel the mentee emits when done. */
32
+ doneSentinel?: string;
33
+ }
34
+ export type GeminiLoopRunStatus = 'running' | 'done' | 'error';
35
+ export interface GeminiLoopRunRecord {
36
+ runId: string;
37
+ status: GeminiLoopRunStatus;
38
+ goalPrompt: string;
39
+ model: string;
40
+ maxTurns: number;
41
+ startedAt: number;
42
+ finishedAt?: number;
43
+ result?: GeminiLoopResult;
44
+ error?: string;
45
+ }
46
+ export interface GeminiLoopStartRequest {
47
+ goalPrompt: string;
48
+ model?: string;
49
+ maxTurns?: number;
50
+ }
51
+ export type GeminiLoopStartOutcome = {
52
+ ok: true;
53
+ runId: string;
54
+ } | {
55
+ ok: false;
56
+ reason: 'disabled' | 'at-capacity' | 'budget' | 'invalid';
57
+ detail?: string;
58
+ };
59
+ export interface GeminiLoopRunnerDeps {
60
+ config: GeminiLoopDriverConfig;
61
+ spawn: GeminiLoopSpawn;
62
+ captureHandle: GeminiLoopHandleCapture;
63
+ budgetGate: GeminiLoopBudgetGate;
64
+ now?: () => number;
65
+ genId?: () => string;
66
+ log?: (msg: string) => void;
67
+ }
68
+ export declare class GeminiLoopRunner {
69
+ private readonly config;
70
+ private readonly spawn;
71
+ private readonly captureHandle;
72
+ private readonly budgetGate;
73
+ private readonly now;
74
+ private readonly genId;
75
+ private readonly log;
76
+ private readonly runs;
77
+ private activeCount;
78
+ constructor(deps: GeminiLoopRunnerDeps);
79
+ isEnabled(): boolean;
80
+ activeRuns(): number;
81
+ /**
82
+ * Admit + launch a run. Returns immediately with a runId; the loop runs in the
83
+ * background. Admission order: feature enabled → under concurrency cap → valid
84
+ * goal → budget gate open.
85
+ */
86
+ startRun(req: GeminiLoopStartRequest): GeminiLoopStartOutcome;
87
+ getRun(runId: string): GeminiLoopRunRecord | null;
88
+ listRuns(): GeminiLoopRunRecord[];
89
+ private execute;
90
+ /** Keep the registry bounded — drop the oldest FINISHED runs past the cap. */
91
+ private evictOldFinished;
92
+ }
93
+ //# sourceMappingURL=GeminiLoopRunner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeminiLoopRunner.d.ts","sourceRoot":"","sources":["../../src/monitoring/GeminiLoopRunner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACtB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,0EAA0E;IAC1E,aAAa,EAAE,MAAM,CAAC;IACtB,0EAA0E;IAC1E,eAAe,EAAE,MAAM,CAAC;IACxB,2CAA2C;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;AAE/D,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,mBAAmB,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC3B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,UAAU,GAAG,aAAa,GAAG,QAAQ,GAAG,SAAS,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9F,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,sBAAsB,CAAC;IAC/B,KAAK,EAAE,eAAe,CAAC;IACvB,aAAa,EAAE,uBAAuB,CAAC;IACvC,UAAU,EAAE,oBAAoB,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyB;IAChD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA0B;IACxD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAuB;IAClD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAwB;IAE5C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA0C;IAC/D,OAAO,CAAC,WAAW,CAAK;gBAEZ,IAAI,EAAE,oBAAoB;IAUtC,SAAS,IAAI,OAAO;IAIpB,UAAU,IAAI,MAAM;IAIpB;;;;OAIG;IACH,QAAQ,CAAC,GAAG,EAAE,sBAAsB,GAAG,sBAAsB;IAoC7D,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI;IAIjD,QAAQ,IAAI,mBAAmB,EAAE;YAInB,OAAO;IA4BrB,8EAA8E;IAC9E,OAAO,CAAC,gBAAgB;CAYzB"}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * GeminiLoopRunner (need-gem-002, increment 2) — the budget-gated, dark-by-default
3
+ * service that makes the GeminiLoopDriver invocable.
4
+ *
5
+ * A multi-turn gemini loop can run for minutes (several turns × a turn each), so
6
+ * it CANNOT block an HTTP request. `startRun` admits a run (enabled? under
7
+ * concurrency cap? budget ok?), kicks off `driver.run()` in the background, and
8
+ * returns a `runId` immediately; the result lands in a bounded in-memory registry
9
+ * retrievable via `getRun`. Ships DARK (`enabled: false`); the `developmentAgent`
10
+ * gate turns it on for dev agents only.
11
+ *
12
+ * Subscription auth is structural: the runner's spawn dep is the production
13
+ * transport spawn (`buildGeminiChildEnv` strips every billing var), so no run can
14
+ * introduce an API key.
15
+ *
16
+ * Spec: docs/specs/gemini-multi-turn-loop-driver.md
17
+ */
18
+ import { randomUUID } from 'node:crypto';
19
+ import { GeminiLoopDriver, } from './GeminiLoopDriver.js';
20
+ export class GeminiLoopRunner {
21
+ config;
22
+ spawn;
23
+ captureHandle;
24
+ budgetGate;
25
+ now;
26
+ genId;
27
+ log;
28
+ runs = new Map();
29
+ activeCount = 0;
30
+ constructor(deps) {
31
+ this.config = deps.config;
32
+ this.spawn = deps.spawn;
33
+ this.captureHandle = deps.captureHandle;
34
+ this.budgetGate = deps.budgetGate;
35
+ this.now = deps.now ?? (() => Date.now());
36
+ this.genId = deps.genId ?? (() => randomUUID());
37
+ this.log = deps.log ?? (() => { });
38
+ }
39
+ isEnabled() {
40
+ return this.config.enabled === true;
41
+ }
42
+ activeRuns() {
43
+ return this.activeCount;
44
+ }
45
+ /**
46
+ * Admit + launch a run. Returns immediately with a runId; the loop runs in the
47
+ * background. Admission order: feature enabled → under concurrency cap → valid
48
+ * goal → budget gate open.
49
+ */
50
+ startRun(req) {
51
+ if (!this.isEnabled())
52
+ return { ok: false, reason: 'disabled' };
53
+ if (this.activeCount >= this.config.maxConcurrent) {
54
+ return { ok: false, reason: 'at-capacity', detail: `maxConcurrent=${this.config.maxConcurrent}` };
55
+ }
56
+ const goalPrompt = (req.goalPrompt ?? '').trim();
57
+ if (!goalPrompt)
58
+ return { ok: false, reason: 'invalid', detail: 'goalPrompt is required' };
59
+ const gate = this.budgetGate();
60
+ if (!gate.ok)
61
+ return { ok: false, reason: 'budget', detail: gate.reason };
62
+ const runId = this.genId();
63
+ const model = (req.model ?? this.config.model) || this.config.model;
64
+ const maxTurns = Number.isInteger(req.maxTurns) && req.maxTurns > 0
65
+ ? Math.min(req.maxTurns, this.config.maxTurns)
66
+ : this.config.maxTurns;
67
+ const record = {
68
+ runId,
69
+ status: 'running',
70
+ goalPrompt,
71
+ model,
72
+ maxTurns,
73
+ startedAt: this.now(),
74
+ };
75
+ this.runs.set(runId, record);
76
+ this.evictOldFinished();
77
+ this.activeCount += 1;
78
+ this.log(`[gemini-loop] run ${runId} started (model=${model}, maxTurns=${maxTurns})`);
79
+ // Fire-and-forget — do NOT await; the HTTP caller already has the runId.
80
+ void this.execute(record);
81
+ return { ok: true, runId };
82
+ }
83
+ getRun(runId) {
84
+ return this.runs.get(runId) ?? null;
85
+ }
86
+ listRuns() {
87
+ return [...this.runs.values()].sort((a, b) => b.startedAt - a.startedAt);
88
+ }
89
+ async execute(record) {
90
+ const driver = new GeminiLoopDriver({
91
+ spawn: this.spawn,
92
+ captureHandle: this.captureHandle,
93
+ budgetGate: this.budgetGate,
94
+ });
95
+ try {
96
+ const result = await driver.run({
97
+ model: record.model,
98
+ goalPrompt: record.goalPrompt,
99
+ maxTurns: record.maxTurns,
100
+ minTurnIntervalMs: this.config.minTurnIntervalMs,
101
+ doneSentinel: this.config.doneSentinel,
102
+ });
103
+ record.status = 'done';
104
+ record.result = result;
105
+ record.finishedAt = this.now();
106
+ this.log(`[gemini-loop] run ${record.runId} done (${result.stopReason}, ${result.turns.length} turns)`);
107
+ }
108
+ catch (err) {
109
+ record.status = 'error';
110
+ record.error = err instanceof Error ? err.message : String(err);
111
+ record.finishedAt = this.now();
112
+ this.log(`[gemini-loop] run ${record.runId} error: ${record.error}`);
113
+ }
114
+ finally {
115
+ this.activeCount = Math.max(0, this.activeCount - 1);
116
+ }
117
+ }
118
+ /** Keep the registry bounded — drop the oldest FINISHED runs past the cap. */
119
+ evictOldFinished() {
120
+ if (this.runs.size <= this.config.maxRetainedRuns)
121
+ return;
122
+ const finished = [...this.runs.values()]
123
+ .filter((r) => r.status !== 'running')
124
+ .sort((a, b) => (a.finishedAt ?? a.startedAt) - (b.finishedAt ?? b.startedAt));
125
+ let over = this.runs.size - this.config.maxRetainedRuns;
126
+ for (const r of finished) {
127
+ if (over <= 0)
128
+ break;
129
+ this.runs.delete(r.runId);
130
+ over -= 1;
131
+ }
132
+ }
133
+ }
134
+ //# sourceMappingURL=GeminiLoopRunner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeminiLoopRunner.js","sourceRoot":"","sources":["../../src/monitoring/GeminiLoopRunner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * ParallelWorkOverlap — the pure cross-topic overlap detector that the
3
+ * ParallelWorkSentinel (Phase B) runs on a cadence (docs/specs/parallel-activity-coherence.md).
4
+ *
5
+ * Convergence hardened this against false-positive nudges (a noisy councilor gets
6
+ * muted, which is worse than silence). The containment lives HERE, in pure logic:
7
+ * - ACTIVITY GATE: only topics worked within a recent window are compared (a dormant
8
+ * overlap is not a live duplication risk — mirrors BurnDetector's activity gate).
9
+ * - SPECIFICITY: overlap rests on shared HIGH-SPECIFICITY tags (entities/files/identifiers,
10
+ * already filtered of boilerplate by extractTags) and requires ≥ minSharedSpecific of
11
+ * them — never on generic words. Rarer shared tags weigh more (IDF over the candidate set).
12
+ * - SELF-EXCLUSION: a topic never matches itself.
13
+ * - SIGNATURE: the sorted shared-tag SET — the dedup key the sentinel uses (with hysteresis)
14
+ * so a slowly-evolving focus does not re-nag and a genuinely new overlap is not suppressed.
15
+ *
16
+ * This module is PURE (no I/O, no time except the injected nowMs) so it is exhaustively
17
+ * unit-testable. The stateful dedup/cooldown/cadence/nudge lives in the sentinel.
18
+ */
19
+ /** The subset of a TopicActivity this detector needs (structurally compatible with ParallelActivityIndex output). */
20
+ export interface OverlapCandidate {
21
+ topicId: number;
22
+ tags: string[];
23
+ updatedAt: number | null;
24
+ running?: boolean;
25
+ }
26
+ export interface OverlapPair {
27
+ /** Lower topicId first (stable pair key). */
28
+ topicA: number;
29
+ topicB: number;
30
+ /** The high-specificity tokens both topics share (the genuine overlap). */
31
+ sharedTags: string[];
32
+ /** IDF-weighted overlap strength over the candidate set; higher = rarer shared tokens. */
33
+ score: number;
34
+ /** Sorted sharedTags joined — the dedup signature (the sentinel keys cooldown on the PAIR, hysteresis on this). */
35
+ signature: string;
36
+ }
37
+ export interface DetectOverlapsOptions {
38
+ nowMs: number;
39
+ /** Only compare topics whose updatedAt is within this window (default 4h). Null updatedAt ⇒ excluded. */
40
+ activityWindowMs?: number;
41
+ /** Require at least this many shared high-specificity tags (default 1). */
42
+ minSharedSpecific?: number;
43
+ /** When true, at least ONE side of each pair must be currently running (default true — we nudge live work). */
44
+ requireRunning?: boolean;
45
+ }
46
+ /**
47
+ * Detect overlapping work across topics. Returns one OverlapPair per genuinely-overlapping
48
+ * pair, strongest first. Pure: same inputs → same output.
49
+ */
50
+ export declare function detectOverlaps(candidates: OverlapCandidate[], opts: DetectOverlapsOptions): OverlapPair[];
51
+ /** Stable pair key for sentinel-side cooldown (survives focus edits / signature drift). */
52
+ export declare function pairKey(topicA: number, topicB: number): string;
53
+ /**
54
+ * Hysteresis: should a NEW overlap signature for an already-nudged pair re-fire? Only when
55
+ * the shared-tag set has changed materially (Jaccard(old,new) < threshold) — so a one-token
56
+ * focus tweak does not re-nag, but a genuinely different overlap does.
57
+ */
58
+ export declare function signatureChangedMaterially(oldSig: string, newSig: string, jaccardThreshold?: number): boolean;
59
+ //# sourceMappingURL=ParallelWorkOverlap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ParallelWorkOverlap.d.ts","sourceRoot":"","sources":["../../src/monitoring/ParallelWorkOverlap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,qHAAqH;AACrH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC;IACd,mHAAmH;IACnH,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,yGAAyG;IACzG,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,+GAA+G;IAC/G,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAID;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,gBAAgB,EAAE,EAC9B,IAAI,EAAE,qBAAqB,GAC1B,WAAW,EAAE,CAqCf;AAED,2FAA2F;AAC3F,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,SAAM,GAAG,OAAO,CAU1G"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * ParallelWorkOverlap — the pure cross-topic overlap detector that the
3
+ * ParallelWorkSentinel (Phase B) runs on a cadence (docs/specs/parallel-activity-coherence.md).
4
+ *
5
+ * Convergence hardened this against false-positive nudges (a noisy councilor gets
6
+ * muted, which is worse than silence). The containment lives HERE, in pure logic:
7
+ * - ACTIVITY GATE: only topics worked within a recent window are compared (a dormant
8
+ * overlap is not a live duplication risk — mirrors BurnDetector's activity gate).
9
+ * - SPECIFICITY: overlap rests on shared HIGH-SPECIFICITY tags (entities/files/identifiers,
10
+ * already filtered of boilerplate by extractTags) and requires ≥ minSharedSpecific of
11
+ * them — never on generic words. Rarer shared tags weigh more (IDF over the candidate set).
12
+ * - SELF-EXCLUSION: a topic never matches itself.
13
+ * - SIGNATURE: the sorted shared-tag SET — the dedup key the sentinel uses (with hysteresis)
14
+ * so a slowly-evolving focus does not re-nag and a genuinely new overlap is not suppressed.
15
+ *
16
+ * This module is PURE (no I/O, no time except the injected nowMs) so it is exhaustively
17
+ * unit-testable. The stateful dedup/cooldown/cadence/nudge lives in the sentinel.
18
+ */
19
+ const DEFAULT_WINDOW_MS = 4 * 60 * 60 * 1000; // 4h
20
+ /**
21
+ * Detect overlapping work across topics. Returns one OverlapPair per genuinely-overlapping
22
+ * pair, strongest first. Pure: same inputs → same output.
23
+ */
24
+ export function detectOverlaps(candidates, opts) {
25
+ const windowMs = opts.activityWindowMs ?? DEFAULT_WINDOW_MS;
26
+ const minShared = opts.minSharedSpecific ?? 1;
27
+ const requireRunning = opts.requireRunning ?? true;
28
+ const cutoff = opts.nowMs - windowMs;
29
+ // Activity gate: only recently-worked topics with at least one specific tag.
30
+ const active = candidates.filter((c) => c.updatedAt !== null && c.updatedAt >= cutoff && c.tags.length > 0);
31
+ // IDF: a tag shared by FEW topics is more meaningful than one shared by many.
32
+ const docFreq = new Map();
33
+ for (const c of active)
34
+ for (const t of new Set(c.tags))
35
+ docFreq.set(t, (docFreq.get(t) ?? 0) + 1);
36
+ const idf = (tag) => {
37
+ const df = docFreq.get(tag) ?? 1;
38
+ return Math.log(1 + active.length / df); // rarer ⇒ higher
39
+ };
40
+ const pairs = [];
41
+ for (let i = 0; i < active.length; i++) {
42
+ for (let j = i + 1; j < active.length; j++) {
43
+ const a = active[i];
44
+ const b = active[j];
45
+ if (a.topicId === b.topicId)
46
+ continue; // self-exclusion (defensive)
47
+ if (requireRunning && !a.running && !b.running)
48
+ continue;
49
+ const bTags = new Set(b.tags);
50
+ const shared = [...new Set(a.tags)].filter((t) => bTags.has(t));
51
+ if (shared.length < minShared)
52
+ continue;
53
+ const score = shared.reduce((s, t) => s + idf(t), 0);
54
+ const sorted = [...shared].sort();
55
+ const [topicA, topicB] = a.topicId < b.topicId ? [a.topicId, b.topicId] : [b.topicId, a.topicId];
56
+ pairs.push({ topicA, topicB, sharedTags: sorted, score, signature: sorted.join('|') });
57
+ }
58
+ }
59
+ pairs.sort((x, y) => y.score - x.score);
60
+ return pairs;
61
+ }
62
+ /** Stable pair key for sentinel-side cooldown (survives focus edits / signature drift). */
63
+ export function pairKey(topicA, topicB) {
64
+ return topicA < topicB ? `${topicA}:${topicB}` : `${topicB}:${topicA}`;
65
+ }
66
+ /**
67
+ * Hysteresis: should a NEW overlap signature for an already-nudged pair re-fire? Only when
68
+ * the shared-tag set has changed materially (Jaccard(old,new) < threshold) — so a one-token
69
+ * focus tweak does not re-nag, but a genuinely different overlap does.
70
+ */
71
+ export function signatureChangedMaterially(oldSig, newSig, jaccardThreshold = 0.6) {
72
+ if (oldSig === newSig)
73
+ return false;
74
+ const a = new Set(oldSig ? oldSig.split('|') : []);
75
+ const b = new Set(newSig ? newSig.split('|') : []);
76
+ if (a.size === 0 && b.size === 0)
77
+ return false;
78
+ let inter = 0;
79
+ for (const t of a)
80
+ if (b.has(t))
81
+ inter++;
82
+ const union = a.size + b.size - inter;
83
+ const jaccard = union === 0 ? 1 : inter / union;
84
+ return jaccard < jaccardThreshold;
85
+ }
86
+ //# sourceMappingURL=ParallelWorkOverlap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ParallelWorkOverlap.js","sourceRoot":"","sources":["../../src/monitoring/ParallelWorkOverlap.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * ParallelWorkSentinel — the proactive overlap councilor (Parallel-Work Awareness
3
+ * Phase B; docs/specs/parallel-activity-coherence.md, Part 2).
4
+ *
5
+ * On a cadence it reads the cross-topic activity index, runs the pure overlap detector,
6
+ * and — for genuinely-fresh overlaps — emits ONE 'overlap' nudge so the agent learns its
7
+ * other hand is already on the same work (the willpower-trap fix: the signal comes TO the
8
+ * agent, it doesn't rely on the agent remembering to query). SIGNAL-ONLY — it never gates,
9
+ * blocks, or mutates anything.
10
+ *
11
+ * Convergence-mandated containment (a noisy councilor gets muted, which is worse than
12
+ * silence), all enforced here statefully on top of the pure detector's containment:
13
+ * - PAIR-KEYED COOLDOWN: at most one nudge per (topicA,topicB) per `nudgeCooldownMs`
14
+ * (default 60min) — survives focus edits, so responding to a nudge can't re-trigger it.
15
+ * - SIGNATURE HYSTERESIS: within the cooldown, a re-nudge only fires if the shared-tag SET
16
+ * changed materially (a genuinely new overlap), not on a one-token focus tweak.
17
+ * - It is the LEASE-HOLDER's job to run this (the caller gates ticks on holdsLease) so the
18
+ * same overlap isn't nudged twice from two machines.
19
+ *
20
+ * Ships DARK (the route/config default is enabled:false) — graduate after it's proven quiet.
21
+ */
22
+ import { EventEmitter } from 'node:events';
23
+ import { type OverlapCandidate, type OverlapPair } from './ParallelWorkOverlap.js';
24
+ export interface ParallelWorkSentinelOptions {
25
+ /** Pull the current cross-topic activities (e.g. ParallelActivityIndex.activities()). */
26
+ getActivities: (nowMs: number) => OverlapCandidate[];
27
+ /** Min ms between nudges for the same topic pair (default 60min). */
28
+ nudgeCooldownMs?: number;
29
+ /** Only compare topics worked within this window (passed to the detector; default 4h). */
30
+ activityWindowMs?: number;
31
+ /** Require ≥ this many shared high-specificity tags (default 1). */
32
+ minSharedSpecific?: number;
33
+ /** Audit sink for every transition (detected/nudged/deduped) — default-on housekeeping. */
34
+ audit?: (event: ParallelWorkAuditEvent) => void;
35
+ }
36
+ export interface ParallelWorkAuditEvent {
37
+ kind: 'nudged' | 'deduped-cooldown' | 'deduped-hysteresis';
38
+ pair: OverlapPair;
39
+ atMs: number;
40
+ }
41
+ /** Emitted to the agent when a fresh overlap is found. The one user-facing signal. */
42
+ export interface OverlapNudge {
43
+ pair: OverlapPair;
44
+ atMs: number;
45
+ message: string;
46
+ }
47
+ export interface ParallelWorkSentinelEvents {
48
+ overlap: [nudge: OverlapNudge];
49
+ }
50
+ export declare class ParallelWorkSentinel extends EventEmitter {
51
+ private readonly opts;
52
+ private readonly state;
53
+ constructor(opts: ParallelWorkSentinelOptions);
54
+ /** Run one cadence tick. Pure-ish: side effects are the emitted nudges + the audit sink. */
55
+ tick(nowMs: number): OverlapNudge[];
56
+ /** Test/inspection helper: how many pairs are being tracked for dedup. */
57
+ trackedPairCount(): number;
58
+ }
59
+ //# sourceMappingURL=ParallelWorkSentinel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ParallelWorkSentinel.d.ts","sourceRoot":"","sources":["../../src/monitoring/ParallelWorkSentinel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAIL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,2BAA2B;IAC1C,yFAAyF;IACzF,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,gBAAgB,EAAE,CAAC;IACrD,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0FAA0F;IAC1F,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,2FAA2F;IAC3F,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAC;CACjD;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,QAAQ,GAAG,kBAAkB,GAAG,oBAAoB,CAAC;IAC3D,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,sFAAsF;AACtF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAOD,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;CAChC;AAED,qBAAa,oBAAqB,SAAQ,YAAY;IAGxC,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;gBAEzB,IAAI,EAAE,2BAA2B;IAI9D,4FAA4F;IAC5F,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,EAAE;IA6CnC,0EAA0E;IAC1E,gBAAgB,IAAI,MAAM;CAG3B"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * ParallelWorkSentinel — the proactive overlap councilor (Parallel-Work Awareness
3
+ * Phase B; docs/specs/parallel-activity-coherence.md, Part 2).
4
+ *
5
+ * On a cadence it reads the cross-topic activity index, runs the pure overlap detector,
6
+ * and — for genuinely-fresh overlaps — emits ONE 'overlap' nudge so the agent learns its
7
+ * other hand is already on the same work (the willpower-trap fix: the signal comes TO the
8
+ * agent, it doesn't rely on the agent remembering to query). SIGNAL-ONLY — it never gates,
9
+ * blocks, or mutates anything.
10
+ *
11
+ * Convergence-mandated containment (a noisy councilor gets muted, which is worse than
12
+ * silence), all enforced here statefully on top of the pure detector's containment:
13
+ * - PAIR-KEYED COOLDOWN: at most one nudge per (topicA,topicB) per `nudgeCooldownMs`
14
+ * (default 60min) — survives focus edits, so responding to a nudge can't re-trigger it.
15
+ * - SIGNATURE HYSTERESIS: within the cooldown, a re-nudge only fires if the shared-tag SET
16
+ * changed materially (a genuinely new overlap), not on a one-token focus tweak.
17
+ * - It is the LEASE-HOLDER's job to run this (the caller gates ticks on holdsLease) so the
18
+ * same overlap isn't nudged twice from two machines.
19
+ *
20
+ * Ships DARK (the route/config default is enabled:false) — graduate after it's proven quiet.
21
+ */
22
+ import { EventEmitter } from 'node:events';
23
+ import { detectOverlaps, pairKey, signatureChangedMaterially, } from './ParallelWorkOverlap.js';
24
+ export class ParallelWorkSentinel extends EventEmitter {
25
+ opts;
26
+ state = new Map();
27
+ constructor(opts) {
28
+ super();
29
+ this.opts = opts;
30
+ }
31
+ /** Run one cadence tick. Pure-ish: side effects are the emitted nudges + the audit sink. */
32
+ tick(nowMs) {
33
+ const activities = this.opts.getActivities(nowMs);
34
+ const pairs = detectOverlaps(activities, {
35
+ nowMs,
36
+ activityWindowMs: this.opts.activityWindowMs,
37
+ minSharedSpecific: this.opts.minSharedSpecific,
38
+ requireRunning: true,
39
+ });
40
+ const cooldown = this.opts.nudgeCooldownMs ?? 60 * 60 * 1000;
41
+ const fired = [];
42
+ for (const pair of pairs) {
43
+ const key = pairKey(pair.topicA, pair.topicB);
44
+ const prev = this.state.get(key);
45
+ if (prev) {
46
+ const withinCooldown = nowMs - prev.lastNudgedMs < cooldown;
47
+ if (withinCooldown && !signatureChangedMaterially(prev.lastSignature, pair.signature)) {
48
+ this.opts.audit?.({ kind: 'deduped-hysteresis', pair, atMs: nowMs });
49
+ // refresh the stored signature (track drift) but do NOT re-nudge
50
+ this.state.set(key, { lastSignature: pair.signature, lastNudgedMs: prev.lastNudgedMs });
51
+ continue;
52
+ }
53
+ if (withinCooldown && signatureChangedMaterially(prev.lastSignature, pair.signature)) {
54
+ // Material change but still inside the cooldown window: suppress, record drift.
55
+ this.opts.audit?.({ kind: 'deduped-cooldown', pair, atMs: nowMs });
56
+ this.state.set(key, { lastSignature: pair.signature, lastNudgedMs: prev.lastNudgedMs });
57
+ continue;
58
+ }
59
+ }
60
+ // Fresh (never nudged, or past cooldown) → nudge.
61
+ this.state.set(key, { lastSignature: pair.signature, lastNudgedMs: nowMs });
62
+ const nudge = {
63
+ pair,
64
+ atMs: nowMs,
65
+ message: `Heads up: topics ${pair.topicA} and ${pair.topicB} look like overlapping work ` +
66
+ `(shared: ${pair.sharedTags.join(', ')}). You may be duplicating — worth aligning before you go further.`,
67
+ };
68
+ this.opts.audit?.({ kind: 'nudged', pair, atMs: nowMs });
69
+ this.emit('overlap', nudge);
70
+ fired.push(nudge);
71
+ }
72
+ return fired;
73
+ }
74
+ /** Test/inspection helper: how many pairs are being tracked for dedup. */
75
+ trackedPairCount() {
76
+ return this.state.size;
77
+ }
78
+ }
79
+ //# sourceMappingURL=ParallelWorkSentinel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ParallelWorkSentinel.js","sourceRoot":"","sources":["../../src/monitoring/ParallelWorkSentinel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Production wiring for the GeminiLoopDriver (need-gem-002, increment 2).
3
+ *
4
+ * Turns the injected-dependency engine (`GeminiLoopDriver`) into real
5
+ * subscription-auth gemini spawns:
6
+ * - `createGeminiLoopSpawn` — routes every turn through the gemini transport
7
+ * (`spawnGeminiAndWait` + `buildGeminiChildEnv`,
8
+ * which strips every billing env var → subscription
9
+ * OAuth is the only possible auth path).
10
+ * - `parseLatestGeminiSessionHandle` — pure parser of `gemini --list-sessions`
11
+ * output → the FRESHEST session's UUID (the one
12
+ * turn 1 just created). Min-age, not list order.
13
+ * - `createGeminiHandleCapture` — runs `--list-sessions` via the transport and
14
+ * parses it; returns null if no handle resolves
15
+ * (the engine then ABORTS rather than guessing).
16
+ * - `createQuotaBudgetGate` — wraps the existing QuotaTracker so a loop refuses
17
+ * to start / continue under load or spend pressure.
18
+ *
19
+ * Spec: docs/specs/gemini-multi-turn-loop-driver.md
20
+ */
21
+ import type { GeminiLoopSpawn, GeminiLoopHandleCapture, GeminiLoopBudgetGate } from './GeminiLoopDriver.js';
22
+ /** Default per-turn spawn timeout (a single gemini turn). */
23
+ export declare const DEFAULT_GEMINI_TURN_TIMEOUT_MS = 180000;
24
+ /**
25
+ * Parse `gemini --list-sessions` stdout and return the FRESHEST session's UUID.
26
+ *
27
+ * Each session row looks like:
28
+ * ` 3. Remember the codeword... (Just now) [ef951c6e-49b4-49df-a8f0-b8aa62b4403f]`
29
+ * The age is the parenthesised group immediately before the `[uuid]` bracket
30
+ * (anchored there so parentheses inside the title don't interfere). We pick the
31
+ * MINIMUM age rather than trusting list order, so the handle is the session turn 1
32
+ * just created. Returns null when no row parses.
33
+ */
34
+ export declare function parseLatestGeminiSessionHandle(stdout: string): string | null;
35
+ /**
36
+ * Production gemini spawn — every turn routes through the subscription-auth
37
+ * transport. `buildGeminiChildEnv()` strips all billing env vars, so no argv this
38
+ * receives can introduce an API key.
39
+ */
40
+ export declare function createGeminiLoopSpawn(geminiPath: string, turnTimeoutMs?: number): GeminiLoopSpawn;
41
+ /**
42
+ * Production handle capture — after turn 1, list sessions and pick the freshest.
43
+ * Single-concurrency (`maxConcurrent: 1`) keeps this unambiguous: turn 1 just ran,
44
+ * so its session is the "Just now" / min-age row even if other gemini one-shots
45
+ * (e.g. cross-model review) created older sessions in the shared cwd.
46
+ */
47
+ export declare function createGeminiHandleCapture(geminiPath: string, listTimeoutMs?: number): GeminiLoopHandleCapture;
48
+ /** Minimal slice of QuotaTracker the budget gate needs. */
49
+ export interface SpawnAdmissionSource {
50
+ shouldSpawnSession(): {
51
+ allowed: boolean;
52
+ reason: string;
53
+ };
54
+ }
55
+ /**
56
+ * Production budget gate — reuses the existing QuotaTracker spawn-admission
57
+ * decision (the same daily-spend / load-shedding signal every other instar spawn
58
+ * respects). Fails OPEN if no tracker is wired (the engine's own maxTurns +
59
+ * spawn-failure handling still bound the loop).
60
+ */
61
+ export declare function createQuotaBudgetGate(quota: SpawnAdmissionSource | null | undefined): GeminiLoopBudgetGate;
62
+ //# sourceMappingURL=geminiLoopProduction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geminiLoopProduction.d.ts","sourceRoot":"","sources":["../../src/monitoring/geminiLoopProduction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH,OAAO,KAAK,EACV,eAAe,EACf,uBAAuB,EACvB,oBAAoB,EACrB,MAAM,uBAAuB,CAAC;AAE/B,6DAA6D;AAC7D,eAAO,MAAM,8BAA8B,SAAU,CAAC;AAEtD;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAY5E;AAkBD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,MAAM,EAClB,aAAa,GAAE,MAAuC,GACrD,eAAe,CAQjB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,MAAM,EAClB,aAAa,GAAE,MAAe,GAC7B,uBAAuB,CAiBzB;AAED,2DAA2D;AAC3D,MAAM,WAAW,oBAAoB;IACnC,kBAAkB,IAAI;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5D;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,oBAAoB,GAAG,IAAI,GAAG,SAAS,GAC7C,oBAAoB,CAMtB"}