@quantiya/codevibe-codex-plugin 2.0.110 → 2.0.112

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 (37) hide show
  1. package/dist/native-launch.js +1 -1
  2. package/dist/server.js +1 -1
  3. package/node_modules/@quantiya/codevibe-core/dist/appsync/sync-mobile-echo.d.ts +12 -12
  4. package/node_modules/@quantiya/codevibe-core/dist/appsync/sync-shell-source.d.ts +14 -14
  5. package/node_modules/@quantiya/codevibe-core/dist/index.d.ts +1 -0
  6. package/node_modules/@quantiya/codevibe-core/dist/index.js +692 -664
  7. package/node_modules/@quantiya/codevibe-core/dist/local-executor/binary-classifier.d.ts +8 -0
  8. package/node_modules/@quantiya/codevibe-core/dist/local-executor/blob-store.d.ts +5 -0
  9. package/node_modules/@quantiya/codevibe-core/dist/local-executor/change-manifest.d.ts +5 -0
  10. package/node_modules/@quantiya/codevibe-core/dist/local-executor/local-executor-impl.d.ts +3 -0
  11. package/node_modules/@quantiya/codevibe-core/dist/local-executor/media-metadata.d.ts +16 -0
  12. package/node_modules/@quantiya/codevibe-core/dist/local-executor/shadow-protocol.d.ts +22 -0
  13. package/node_modules/@quantiya/codevibe-core/dist/local-executor/shadow-slice3-cleanups.d.ts +15 -0
  14. package/node_modules/@quantiya/codevibe-core/dist/local-executor/snapshot-merge.d.ts +4 -0
  15. package/node_modules/@quantiya/codevibe-core/dist/local-executor/task-group-types.d.ts +3 -1
  16. package/node_modules/@quantiya/codevibe-core/dist/local-executor/team-media-approval.d.ts +78 -0
  17. package/node_modules/@quantiya/codevibe-core/dist/local-executor/withheld-media.d.ts +14 -0
  18. package/node_modules/@quantiya/codevibe-core/dist/local-executor/workspace-shadow.d.ts +11 -6
  19. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/advisory-fingerprint/git-availability.d.ts +30 -0
  20. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/advisory-fingerprint/git-classify.d.ts +34 -0
  21. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/advisory-fingerprint/git-runner.d.ts +24 -0
  22. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/advisory-fingerprint/repo-record.d.ts +47 -0
  23. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/advisory-fingerprint/walk.d.ts +34 -0
  24. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/agent-turn-degrade.d.ts +14 -0
  25. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/binary-retry-brief.d.ts +8 -0
  26. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/brainstorm-quorum.d.ts +17 -5
  27. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/cli.js +6352 -3265
  28. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/context-store.d.ts +3 -58
  29. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/engine-projection.d.ts +9 -0
  30. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/media-approval.d.ts +112 -0
  31. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/quorum-loop.d.ts +3 -0
  32. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/review-bundle.d.ts +40 -0
  33. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/review-receipts.d.ts +22 -1
  34. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/types.d.ts +6 -0
  35. package/node_modules/@quantiya/codevibe-core/dist/tool-activity/types.d.ts +12 -12
  36. package/node_modules/@quantiya/codevibe-core/package.json +1 -1
  37. package/package.json +2 -2
@@ -18,6 +18,14 @@ export type FileClass = {
18
18
  kind: 'other';
19
19
  format: 'audio' | 'video' | 'archive' | 'other_binary';
20
20
  };
21
+ /** P63 Slice 3 — a captured file's class when it is not text; a diff entry without one is text (§10.1). */
22
+ export type StoredFileClass = Exclude<FileClass, {
23
+ kind: 'text';
24
+ }>;
25
+ /** The same class — `null` / absent is text (§10.1). Compares the class a capture recorded with the class of bytes read later. */
26
+ export declare function sameStoredClass(a: StoredFileClass | null | undefined, b: StoredFileClass | null | undefined): boolean;
27
+ /** A well-formed stored class, exactly as `classifyFile` returns it (records are read back from disk). */
28
+ export declare function isStoredFileClass(value: unknown): value is StoredFileClass;
21
29
  /** §10.2: per-part inflate output limit (64 MiB). */
22
30
  export declare const MAX_OFFICE_PART_BYTES: number;
23
31
  /**
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import type { BlobRef } from './change-manifest';
3
+ import { type FileSource, type StoredFileClass } from './binary-classifier';
3
4
  import { type StoreAnchor } from './store-dir-anchor';
4
5
  export declare const BLOB_CHUNK_BYTES: number;
5
6
  /** One instance's blob namespace in a task state directory. */
@@ -80,6 +81,10 @@ export declare function readBlobChunks(store: BlobStore, ref: BlobRef): AsyncGen
80
81
  * mismatch before anything is published). Opened no-follow; must be a regular file of exactly `ref.size` bytes.
81
82
  */
82
83
  export declare function openBlobForStreaming(store: BlobStore, ref: BlobRef): Promise<fs.FileHandle>;
84
+ /** P63 Slice 3 — random access to one stored blob for the duration of `fn` (the blob is verified as a regular file). */
85
+ export declare function withBlobSource<T>(store: BlobStore, ref: BlobRef, fn: (source: FileSource) => Promise<T>): Promise<T>;
86
+ /** P63 Slice 3 (§10.1) — a blob's class, read through random access to the stored bytes; null for text. */
87
+ export declare function classifyBlob(store: BlobStore, ref: BlobRef): Promise<StoredFileClass | null>;
83
88
  /**
84
89
  * P63 Slice 2 (§9.4) — the strict-text rule of `decodeStrictUtf8` (valid UTF-8, no NUL byte) checked while streaming,
85
90
  * for a file too large to decode whole. Throws the same "binary file changes are not supported safely" refusal.
@@ -1,3 +1,4 @@
1
+ import { type StoredFileClass } from './binary-classifier';
1
2
  import type { ShadowChangeKind } from './shadow-protocol';
2
3
  export declare const CHANGE_MANIFEST_VERSION = 1;
3
4
  export declare const CHANGE_MANIFEST_SHARD_ENTRIES = 10000;
@@ -20,6 +21,10 @@ export interface ChangeManifestEntry {
20
21
  * track's declared base mode); a single task's manifest omits it and its lines are unchanged.
21
22
  */
22
23
  base_mode?: number | null;
24
+ /** P63 Slice 3 (§10.1): the reviewed post-image's class when it is not text. Absent: text (every older line). */
25
+ file_class?: StoredFileClass;
26
+ /** P63 Slice 3 (§10.1): the pre-task file's class when it is not text. Absent: text. */
27
+ base_class?: StoredFileClass;
23
28
  }
24
29
  export interface ChangeManifestShard {
25
30
  /** SHA-256 (hex) of `bytes`; the shard's name in the store. */
@@ -8,6 +8,7 @@ import { TrackRegistry } from './track-registry';
8
8
  import { AgentKind, AuthorityAction, ClassBInbound, GateDispatchPayload, HookEvent, ImplementorHandle, LocalExecutor, MergeGateDispatchPayload, ProbeHandle, ProcessRole, ReviewerDispatchPayload, ReviseFeedbackPayload, QueuedWorkEndedPayload, ReviewerHandle, SpawnArgs, TrackAssignedPayload, type DurableSessionResourceDisposeOptions, type WorkspaceDisposeOptions } from './types';
9
9
  import { type TeamExecution } from './team-execution-flag';
10
10
  import { type SnapshotBundlesView, type SnapshotMergeGateResult } from './snapshot-merge';
11
+ import { type TeamMediaAsker } from './team-media-approval';
11
12
  import { type WorkspaceRootAuthority } from './workspace-authority';
12
13
  import type { RenderedConflict, TrackIntentDiff } from './wb2-review-bundle';
13
14
  import type { ConflictModelCaller } from './class2-resolver';
@@ -371,6 +372,8 @@ export interface LocalExecutorImplDeps {
371
372
  }) => Promise<void>;
372
373
  /** Optional shadow lifecycle hooks shared with implementor workspaces. */
373
374
  snapshotShadowEnv?: ShadowEnv;
375
+ /** P63 Slice 3 §9.5 — ask a team's media question once, before its merged result is applied (team-media-approval.ts). */
376
+ askTeamMediaApproval?: TeamMediaAsker;
374
377
  /** Mark a successfully accepted group terminal and reclaim its bundle state. */
375
378
  completeSnapshotGroup?: (taskGroupId: string) => Promise<void>;
376
379
  /** Normal-exit retirement of all ordinary durable resources from this session. */
@@ -0,0 +1,16 @@
1
+ /**
2
+ * P63 Slice 3 Phase B — §8.1 / §9.5: what the INDEX and the phone question say about a withheld file (audio, video,
3
+ * archives, other binary): its container type, and its duration where the container states one. Read through the same
4
+ * random-access `FileSource` as the classifier, header fields only — never the media data. Malformed or unknown input
5
+ * yields `unknown` / no duration; a read error from the source propagates (the store failed, not the file).
6
+ */
7
+ import type { FileSource } from './binary-classifier';
8
+ export interface MediaMetadata {
9
+ /** A short container name for people: "MPEG-4 (MP4)", "WAV", "ZIP", … or "unknown". */
10
+ container: string;
11
+ /** Whole seconds, when the container header states the duration (MP4/MOV/M4A `mvhd`, WAV byte rate). */
12
+ durationSeconds: number | null;
13
+ }
14
+ export declare function readMediaMetadata(source: FileSource): Promise<MediaMetadata>;
15
+ /** "0:45", "1:04", "1:02:03". */
16
+ export declare function formatDuration(seconds: number): string;
@@ -1,3 +1,4 @@
1
+ import { type StoredFileClass } from './binary-classifier';
1
2
  import * as fs from 'node:fs/promises';
2
3
  import type { BlobRef } from './change-manifest';
3
4
  import type { ChangeManifestRecord } from './change-manifest-store';
@@ -41,6 +42,8 @@ export declare const DEFAULT_MATERIALIZER_LANES = 4;
41
42
  export declare const SHADOW_UUID_SUFFIX_RE: RegExp;
42
43
  /** §6.2's per-spawn advisory records live in the v2 state root beside the task folders, never as a task. */
43
44
  export declare const SHADOW_STATE_ADVISORY_DIR = "advisory";
45
+ /** P63 §9.5: the open team media questions (`team-media-approval.ts`) live in the v2 state root, never as a task. */
46
+ export declare const SHADOW_STATE_TEAM_MEDIA_DIR = "team-media-questions";
44
47
  export declare function isStateRootInfrastructureDirectory(name: string): boolean;
45
48
  export declare function taskIdFromGeneratedShadowName(name: string): string | null;
46
49
  /** Strict lifecycle-lock residue authored by `withAnchoredExclusiveLock`. */
@@ -129,7 +132,24 @@ export interface ShadowDiffFile {
129
132
  * anchored copy. Absent for deletions and for records written before Slice 2.
130
133
  */
131
134
  blob?: BlobRef;
135
+ /** P63 Slice 3 (§10.1) — the new version's class when it is not text (created/modified). Absent: text. */
136
+ file_class?: StoredFileClass;
137
+ /** P63 Slice 3 (§10.1) — the user's version's class when it is not text (modified/deleted). Absent: text. */
138
+ base_class?: StoredFileClass;
132
139
  }
140
+ /** P63 Slice 3 — the class fields of a stored diff entry are well formed and sit on the change kinds they describe. */
141
+ export declare function assertDiffFileClasses(file: {
142
+ change_kind: unknown;
143
+ content?: unknown;
144
+ blob?: unknown;
145
+ file_class?: unknown;
146
+ base_class?: unknown;
147
+ }, label: string): void;
148
+ /** P63 Slice 3 — a change that is binary on either side: whole-file, never decoded as text or line-merged (§10.3). */
149
+ export declare function isBinaryDiffFile(file: {
150
+ file_class?: unknown;
151
+ base_class?: unknown;
152
+ }): boolean;
133
153
  /**
134
154
  * P63 Slice 2 (§9.4) — a diff entry's optional blob reference is well formed and agrees with its inline content:
135
155
  * never on a deletion, a 64-hex SHA-256 and a byte size equal to the content's UTF-8 length.
@@ -138,6 +158,8 @@ export declare function assertDiffBlobRef(file: {
138
158
  change_kind: unknown;
139
159
  content: unknown;
140
160
  blob?: unknown;
161
+ file_class?: unknown;
162
+ base_class?: unknown;
141
163
  }, label: string): void;
142
164
  /**
143
165
  * P63 Slice 2 (§9.4) — text above this many bytes is carried as a blob reference only (`content: ''`): it is never
@@ -0,0 +1,15 @@
1
+ import { type ShadowEnv } from './shadow-protocol';
2
+ import type { WorkspaceRootAuthority } from './workspace-authority';
3
+ /** F-P0-1 — remove the state directory a failed create made, by its creation identity and only when it is empty. */
4
+ export declare function removeEmptyCreatedStateDir(createdStateDir: WorkspaceRootAuthority | null, taskId: string): Promise<void>;
5
+ /**
6
+ * §11 / §16 G2 — a task's capture dirs whose task no longer exists are removed by exact path: the task has no state
7
+ * directory and is not kept by this desktop. A link is never followed; anything that is not a real directory is left.
8
+ * The task folder goes too when that leaves it empty.
9
+ */
10
+ export declare function removeOrphanCaptureDirs(keepTaskIds: ReadonlySet<string>, env?: ShadowEnv): Promise<string[]>;
11
+ /**
12
+ * The orphan reap's last step: once, against this desktop's own (v2) roots, after every pass decided what it keeps. Not
13
+ * after an aborted or failed sweep (`skip`). Never fatal to the sweep.
14
+ */
15
+ export declare function sweepOrphanCaptureDirs(keepTaskIds: ReadonlySet<string>, skip: boolean, env?: ShadowEnv): Promise<void>;
@@ -4,6 +4,8 @@ import { type MetadataBudget } from './bounded-directory';
4
4
  import type { PromoteManifestInput, RevertManifestEntry, RevertManifestStore, SnapshotPromotionJournalV1 } from './durable-store';
5
5
  import { WorkspaceShadow, type ReviewedShadowDiffFile, type ShadowEnv } from './workspace-shadow';
6
6
  import { type BlobStore } from './blob-store';
7
+ import type { ShadowDiffFile } from './shadow-protocol';
8
+ import type { WithheldMediaChange } from './withheld-media';
7
9
  import { type WorkspaceRootAuthority } from './workspace-authority';
8
10
  export declare const SNAPSHOT_TRACK_BUNDLE_VERSION: 2;
9
11
  export interface SnapshotTrackBundle {
@@ -90,6 +92,8 @@ export interface RunSnapshotMergeGateArgs {
90
92
  */
91
93
  beforePromotionMutation?: (operationHash: string, files: readonly ReviewedShadowDiffFile[], workspaceRootAuthority: WorkspaceRootAuthority) => Promise<void>;
92
94
  markPromotionApplied: (operationHash: string) => Promise<void>;
95
+ /** P63 Slice 3 §9.5 — a team's media question, before anything is journaled; false applies nothing (team-media-approval.ts). */
96
+ approveMedia?: (files: readonly ReviewedShadowDiffFile[], withheldOf?: (files: readonly ShadowDiffFile[]) => Promise<WithheldMediaChange[]>) => Promise<boolean>;
93
97
  timeoutMs?: number;
94
98
  /** @internal deterministic merge-planning aggregate-work ceiling for tests. */
95
99
  mergePlanMaxWork?: number;
@@ -158,7 +158,9 @@ export interface TrackChangedFiles {
158
158
  /** MergeGate result discriminator (§2.1). SCREAMING wire values. */
159
159
  export type MergeResult = 'PASS' | 'FAIL';
160
160
  /** The 6-class MergeGate failure classification (§2.1, #C12-8). */
161
- export type MergeFailureClass = 'SHARED_FILE_CONFLICT' | 'INTEGRATION_FAILURE' | 'REVIEWER_DISAGREEMENT' | 'SCOPE_VIOLATION' | 'DIRTY_REPO_STATE' | 'BUDGET_EXHAUSTED';
161
+ export type MergeFailureClass = 'SHARED_FILE_CONFLICT' | 'INTEGRATION_FAILURE' | 'REVIEWER_DISAGREEMENT' | 'SCOPE_VIOLATION' | 'DIRTY_REPO_STATE' | 'BUDGET_EXHAUSTED'
162
+ /** P63 Slice 3 §9.5 — the user declined the team's media question; nothing applied (engine: no escalation). */
163
+ | 'USER_DECLINED_MEDIA';
162
164
  /** Retry-target kind discriminator (§2.1). SCREAMING wire values. */
163
165
  export type MergeRetryTargetKind = 'TRACK' | 'SERIAL_OWNER' | 'NONE';
164
166
  /**
@@ -0,0 +1,78 @@
1
+ import type { BlobStore } from './blob-store';
2
+ import type { ShadowDiffFile } from './shadow-protocol';
3
+ import type { TaskGroupHaltedPayload } from './types';
4
+ import { type WithheldMediaChange } from './withheld-media';
5
+ /** Ask the team's question; resolves true only on "yes". */
6
+ export type TeamMediaAsker = (taskGroupId: string, withheld: readonly WithheldMediaChange[]) => Promise<boolean>;
7
+ export declare function takeTeamMediaDeclined(owner: object, taskGroupId: string): boolean;
8
+ /** The longest a team group lives (engine TTL, F2 TM-2: 7 days from creation; 30 days once PROMOTE is authorized,
9
+ * which a group waiting on this question has not reached). An older record names a group the engine has ended. */
10
+ export declare const TEAM_MEDIA_RECORD_MAX_AGE_MS: number;
11
+ /** The engine's halt reason for the user's "no" (core-rs `GROUP_USER_DECLINED_MEDIA`). */
12
+ export declare const GROUP_USER_DECLINED_MEDIA = "group_user_declined_media";
13
+ /**
14
+ * Engine gate D-1: the engine writes no `TaskGroupHalted` row for the user's own "no", so the desktop that submitted the
15
+ * decline runs the group's halt teardown itself — the same `haltTaskGroup` a halt packet drives, with no choices (a v2
16
+ * group's desktop notice is informational; the user has already decided).
17
+ */
18
+ export declare function declinedTeardownPayload(taskGroupId: string, decisionProtocolVersion: unknown): TaskGroupHaltedPayload;
19
+ /** The media in a merged change, from the classes the reviewed bundles carry (§10.1). */
20
+ export declare function teamWithheldMedia(files: readonly ShadowDiffFile[], store: BlobStore | undefined): Promise<WithheldMediaChange[]>;
21
+ /** The durable record of an open team question (a JSON file per group under `<state root>/team-media-questions/`). */
22
+ export interface OpenTeamMediaQuestion {
23
+ version: 1;
24
+ taskGroupId: string;
25
+ /** The session that asked; only that session (resumed) may end the group. */
26
+ sessionId: string;
27
+ /** The MergeGateDispatch the verdict answers (persisted because the engine does not re-deliver it). */
28
+ payload: Record<string, unknown>;
29
+ gateKinds: string[];
30
+ openedAt: string;
31
+ }
32
+ /** Registered as state-root infrastructure (`shadow-protocol.ts`), so no state-root walker treats it as a task folder. */
33
+ export declare function teamMediaQuestionDir(stateRoot: string): string;
34
+ /**
35
+ * Durable (SF-6, X-1 restart drill 2026-11-09: an abrupt stop within the file system's commit interval lost an unsynced
36
+ * record): the bytes are synced before the rename, and the folder (and, when it was just created, its parent) after it.
37
+ */
38
+ export declare function writeOpenTeamMediaQuestion(stateRoot: string, q: OpenTeamMediaQuestion): Promise<void>;
39
+ /** Durable too: a record that came back after a crash would end a group whose question was already answered. */
40
+ export declare function removeOpenTeamMediaQuestion(stateRoot: string, taskGroupId: string): Promise<void>;
41
+ export declare function listOpenTeamMediaQuestions(stateRoot: string): Promise<OpenTeamMediaQuestion[]>;
42
+ /**
43
+ * The merge hook. `true` = apply the merged result; `false` = apply nothing (a decline is remembered for the FAIL
44
+ * verdict; an abort is not a decline). No asker in this session: nothing is applied and it counts as declined, so the
45
+ * group ends instead of waiting for an answer that cannot come.
46
+ */
47
+ export declare function approveTeamMedia(args: {
48
+ owner: object;
49
+ taskGroupId: string;
50
+ files: readonly ShadowDiffFile[];
51
+ /** S2R1-F3: the integration snapshot's own withheld check, bound to the merge base's pre-task bytes. */
52
+ withheldOf?: (files: readonly ShadowDiffFile[]) => Promise<WithheldMediaChange[]>;
53
+ store: BlobStore | undefined;
54
+ ask: TeamMediaAsker | undefined;
55
+ signal?: AbortSignal;
56
+ record?: {
57
+ stateRoot: string;
58
+ sessionId: string;
59
+ payload: Record<string, unknown>;
60
+ gateKinds: string[];
61
+ };
62
+ }): Promise<boolean>;
63
+ /**
64
+ * At start (Hendry 2026-11-09, "Engine ends orphaned groups"; F2 TM-10 was disproven): a resumed session runs under a
65
+ * NEW session id, which the engine rejects for the old group, so the desktop never submits a verdict here. A question
66
+ * the PREDECESSOR session left open is closed locally: its record is removed and the user is told nothing was applied —
67
+ * the engine ends that group by itself once the predecessor's session has ended (`session_end_sweep`; a resume is only
68
+ * admitted after it has). Another session's record is left alone (it may still be asking) unless it is older than the
69
+ * group lifetime, when it is removed.
70
+ */
71
+ export declare function recoverOpenTeamMediaQuestions(args: {
72
+ stateRoot: string;
73
+ /** The session this one resumed from (`Session.resumedFromSessionId`); undefined for a fresh session. */
74
+ resumedFromSessionId?: string;
75
+ nowMs?: number;
76
+ notify: (text: string) => void;
77
+ warn: (message: string, details?: unknown) => void;
78
+ }): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import type { StoredFileClass } from './binary-classifier';
2
+ import { type BlobStore } from './blob-store';
3
+ import type { PreTaskHashes, ShadowDiffFile } from './shadow-protocol';
4
+ export interface WithheldMediaChange {
5
+ path: string;
6
+ change_kind: ShadowDiffFile['change_kind'];
7
+ cls: StoredFileClass;
8
+ size: number | null;
9
+ container: string;
10
+ durationSeconds: number | null;
11
+ }
12
+ export declare function withheldMediaChanges(store: BlobStore, preTaskHashes: PreTaskHashes, files: readonly ShadowDiffFile[],
13
+ /** S2R1-F3: copies the user's current original of `rel` into `store` (the shadow's anchored base read). */
14
+ readLiveBase?: (rel: string) => Promise<unknown>): Promise<WithheldMediaChange[]>;
@@ -6,6 +6,8 @@ import { type CopySpaceVerdict } from './shadow-free-space';
6
6
  import { type TaskIntent } from './shadow-task-intent';
7
7
  import { type BlobStore } from './blob-store';
8
8
  import type { BlobRef } from './change-manifest';
9
+ import { type StoredFileClass } from './binary-classifier';
10
+ import { type WithheldMediaChange } from './withheld-media';
9
11
  import type { CasRecoveryAuthority } from './shadow-protocol';
10
12
  import { RecoveredShadow } from './shadow-recovery';
11
13
  export { MAX_CHANGE_SET_FILE_COUNT, MAX_CHANGE_SET_PATH_BYTES, MAX_TRACKED_FILE_LOGICAL_BYTES, MAX_TRACKED_WORKSPACE_LOGICAL_BYTES, MAX_TRACKED_FILE_COUNT, MAX_TRACKED_PATH_BYTES, MAX_REVIEW_FILE_BYTES, MAX_PROMOTION_PREIMAGE_AGGREGATE_BYTES, MAX_STATE_MARKER_BYTES, DEFAULT_SHADOW_TTL_MS, } from './shadow-protocol';
@@ -255,13 +257,12 @@ export declare class WorkspaceShadow {
255
257
  };
256
258
  };
257
259
  /**
258
- * P63 Slice 2 R1-F1 — the user's file a change modifies or deletes is text: its current bytes are streamed through the
259
- * anchored copy into this task's blob store and checked as strict UTF-8, so a binary original (a deleted `clip.mp4`, a
260
- * `logo.png` overwritten with text) stops the task with the binary reason before any review, as the strict decode of
261
- * the promote preflight did before Slice 2 — and its bytes never reach a review bundle. `required`: the file must
262
- * exist (a deletion); for a modification a file the user removed meanwhile is left to the conflict check.
260
+ * P63 Slice 2 R1-F1, Slice 3 — the user's file a change modifies or deletes: its current bytes are streamed through the
261
+ * anchored copy into this task's blob store (where the review bundle reuses them as the base version) and classified
262
+ * (§10.1); a binary original is recorded as `base_class`, never refused. `required`: the file must exist (a deletion);
263
+ * for a modification a file the user removed meanwhile is left to the conflict check (null).
263
264
  */
264
- private assertRealBaseIsText;
265
+ private realBaseClass;
265
266
  /**
266
267
  * P63 Slice 2 (§8.1) — the verified blob sources of a review bundle for `files`: each new version from this instance's
267
268
  * blob store (`ownPostImageBlob`), each base version copied from the real file through the anchored helper and kept
@@ -288,7 +289,11 @@ export declare class WorkspaceShadow {
288
289
  size: number | null;
289
290
  changed: boolean;
290
291
  };
292
+ afterClass?: StoredFileClass;
293
+ beforeClass?: StoredFileClass;
291
294
  }>>;
295
+ /** P63 Slice 3 (§9.5) — the changes the user must approve before anything is applied (withheld-media.ts). */
296
+ withheldMediaChanges(files: readonly ShadowDiffFile[]): Promise<WithheldMediaChange[]>;
292
297
  /** §9.4 — this instance's blob store (a team bundle's source when it is published to the group's content store). */
293
298
  contentBlobStore(): BlobStore;
294
299
  /**
@@ -0,0 +1,30 @@
1
+ import { type ProcessRunner } from './git-runner';
2
+ export type HandOffGit = {
3
+ kind: 'available';
4
+ bin: string;
5
+ } | {
6
+ kind: 'unavailable';
7
+ why: string;
8
+ };
9
+ /** A transient failure (spawn pressure, signal, deadline): fail this probe, ask again next time. */
10
+ export declare class GitAvailabilityTransient extends Error {
11
+ }
12
+ export declare const MACOS_GIT_SHIM = "/usr/bin/git";
13
+ export interface AvailabilityDeps {
14
+ platform: NodeJS.Platform;
15
+ env: NodeJS.ProcessEnv;
16
+ /**
17
+ * a regular file that is executable (`stat` + `access(X_OK)`); a directory is never executable here. A definitive
18
+ * absence (ENOENT, ENOTDIR, EACCES, ELOOP, ENAMETOOLONG) is `false`; any other filesystem error throws, and the
19
+ * resolver treats it as transient (never cached).
20
+ */
21
+ isExecutable(p: string): Promise<boolean>;
22
+ realpath(p: string): Promise<string>;
23
+ readlink(p: string): Promise<string>;
24
+ }
25
+ export declare const defaultAvailabilityDeps: AvailabilityDeps;
26
+ /** Decide the hand-off git. Definitive answers resolve; transient ones throw `GitAvailabilityTransient`. */
27
+ export declare function resolveHandOffGit(runner: ProcessRunner, deps: AvailabilityDeps): Promise<HandOffGit>;
28
+ export declare function handOffGit(runner: ProcessRunner, deps?: AvailabilityDeps): Promise<HandOffGit>;
29
+ /** Tests only. */
30
+ export declare function resetHandOffGitCacheForTests(): void;
@@ -0,0 +1,34 @@
1
+ export declare const errnoCode: (err: unknown) => string;
2
+ /**
3
+ * git's own "not a repository" failure: exit status 128 with that diagnostic as git's `fatal:` line (`LC_ALL=C`) —
4
+ * the ONLY stderr line that starts with `fatal: `. `GIT_TRACE`/`GIT_TRACE2` output and `warning:` lines can come before
5
+ * it, and a path git prints can carry a newline followed by text of our choosing (a folder named
6
+ * "d\nfatal: not a git repository: x"), so a `fatal: `-prefixed line is not necessarily git's diagnostic. git's `die()`
7
+ * always prints its own `fatal: ` line, so when exactly one line has the prefix it is git's; when several do, which one
8
+ * is git's cannot be told and the failure is not classified (fail closed). git has two forms:
9
+ * `fatal: not a git repository (or any of the parent directories): .git` (discovery found nothing) and
10
+ * `fatal: not a git repository: <gitdir>` (a `.git` file, or `GIT_DIR`, naming something that is not a repository — a
11
+ * linked worktree whose repository is gone, a submodule folder copied out of its project). Matching the phrase
12
+ * anywhere in stderr would also match a path quoted inside another diagnostic or a trace line — "invalid gitfile
13
+ * format: <path>" under a folder named "not a git repository" — and a timeout or a signal is never "not a repository".
14
+ */
15
+ export declare function isNotAGitRepositoryFailure(err: unknown): boolean;
16
+ /**
17
+ * git's "detected dubious ownership in repository at '<path>'" refusal (`safe.directory`), classified by the same
18
+ * single-`fatal:`-line rule: git prints the path, so the phrase alone could be planted by a folder name.
19
+ */
20
+ export declare function isDubiousOwnershipFailure(err: unknown): boolean;
21
+ /**
22
+ * For a folder with a `.git` marker: is it really inside a repository? A `.git` that git itself does not
23
+ * accept (an empty or stale `.git` folder, a gitfile of a removed worktree) or a missing `git` binary is
24
+ * `not-a-repository`, so the folder is walked like any other; any other git failure is `error`.
25
+ */
26
+ export declare function classifyGitWorkspace(workspaceRoot: string, timeoutMs: number): Promise<'repository' | 'not-a-repository' | 'error'>;
27
+ /** Variables that make git use a repository other than the one in its working folder. */
28
+ export declare const REPOSITORY_LOCATING_GIT_ENV: readonly ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_OBJECT_DIRECTORY", "GIT_INDEX_FILE", "GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_NAMESPACE", "GIT_CEILING_DIRECTORIES", "GIT_DISCOVERY_ACROSS_FILESYSTEM"];
29
+ /** The launch folder's environment (today's, as `probeWorkspaceTreeFingerprint` uses). */
30
+ export declare function launchGitEnv(): NodeJS.ProcessEnv;
31
+ /** The environment for a repository found by the walk, or nested in one: no repository-locating variable. */
32
+ export declare function repositoryScrubbedEnv(): NodeJS.ProcessEnv;
33
+ /** Leading arguments of every advisory git call: no fsmonitor daemon, no optional index writes. */
34
+ export declare const ADVISORY_GIT_PREFIX: readonly ["-c", "core.fsmonitor=false", "--no-optional-locks"];
@@ -0,0 +1,24 @@
1
+ /** No process is started once fewer than this many milliseconds remain before the deadline. */
2
+ export declare const MIN_SPAWN_BUDGET_MS = 50;
3
+ /** A failed run: same fields `execFile` failures carry, so the git-classify readers work on both. */
4
+ export declare class ProcessRunError extends Error {
5
+ readonly code: number | string | null;
6
+ readonly signal: NodeJS.Signals | null;
7
+ readonly killed: boolean;
8
+ readonly stderr: string;
9
+ /** the deadline ended the run (or no time was left to start it) */
10
+ readonly timedOut: boolean;
11
+ constructor(message: string, code: number | string | null, signal: NodeJS.Signals | null, killed: boolean, stderr: string,
12
+ /** the deadline ended the run (or no time was left to start it) */
13
+ timedOut: boolean);
14
+ }
15
+ export interface ProcessRunner {
16
+ /** Resolves with stdout on exit 0; rejects with `ProcessRunError` otherwise. */
17
+ run(bin: string, args: readonly string[], opts: {
18
+ cwd: string;
19
+ env: NodeJS.ProcessEnv;
20
+ }): Promise<string>;
21
+ /** Process-group ids this runner spawned (diagnostics and tests). */
22
+ readonly spawnedGroups: readonly number[];
23
+ }
24
+ export declare function createProcessRunner(deadline: number, concurrency?: number): ProcessRunner;
@@ -0,0 +1,47 @@
1
+ import type { ProcessRunner } from './git-runner';
2
+ export declare const ADVISORY_FINGERPRINT_MAX_REPOSITORIES = 128;
3
+ /**
4
+ * A failure that ends the whole probe with this reason (fail closed). `dir` (absolute) and a bounded `category`
5
+ * (`timeout`, `exit-<code>`, `signal-<sig>`, `spawn-<errno>`, or a fixed step name) are for the desktop log only — never
6
+ * git's stderr.
7
+ */
8
+ export declare class ProbeFailure extends Error {
9
+ readonly reason: string;
10
+ readonly dir?: string | undefined;
11
+ readonly category?: string | undefined;
12
+ constructor(reason: string, dir?: string | undefined, category?: string | undefined);
13
+ }
14
+ export interface RecordContext {
15
+ runner: ProcessRunner;
16
+ git: string;
17
+ env: NodeJS.ProcessEnv;
18
+ /** absolute repository path → its control set, from the before-turn probe (after-turn probe only) */
19
+ before: Readonly<Record<string, string>> | null;
20
+ /** filled with every 3B repository's control set (absolute path → set) */
21
+ controlSets: Record<string, string>;
22
+ repositories: number;
23
+ }
24
+ /** A bounded failure category from a process-runner failure (never its message, which names the binary path). */
25
+ export declare function failureCategory(err: unknown): string;
26
+ /** `mode : size : mtime rounded to the nearest second : inode` — the walk's stamp, plus mode. */
27
+ export declare function stamp(p: string): Promise<string>;
28
+ export type RootTest = {
29
+ kind: 'root';
30
+ gitDir: string;
31
+ commonDir: string;
32
+ } | {
33
+ kind: 'walk';
34
+ };
35
+ /** Is `dir` (which has a `.git` entry) a repository ROOT? Not a repository / dubious ownership / not a root → walk it. */
36
+ export declare function repositoryRoot(dir: string, ctx: RecordContext): Promise<RootTest>;
37
+ /** 3B: a repository found by the walk (or nested in one). */
38
+ export declare function walkFoundRecord(dir: string, info: {
39
+ gitDir: string;
40
+ commonDir: string;
41
+ }, ctx: RecordContext, depth?: number): Promise<string>;
42
+ /**
43
+ * 3A: the launch repository (today's coverage plus HEAD). Nested records use the scrubbed environment. A nested entry
44
+ * that git does not accept as a repository root (not a repository, dubious ownership, a different top level) keeps its
45
+ * status line only — today's coverage; an unclassified git failure fails the probe (step 2).
46
+ */
47
+ export declare function launchRecord(top: string, ctx: RecordContext, nestedEnv: NodeJS.ProcessEnv, depth?: number): Promise<string>;
@@ -0,0 +1,34 @@
1
+ import { type AvailabilityDeps } from './git-availability';
2
+ /** Budget of the whole probe (walk and every git call); past it the probe fails (fail closed). */
3
+ export declare const ADVISORY_WALK_FINGERPRINT_TIMEOUT_MS = 10000;
4
+ /** Entry cap of the walk (a repository handed to git counts as one entry); past it the probe fails (fail closed). */
5
+ export declare const ADVISORY_WALK_FINGERPRINT_MAX_ENTRIES = 200000;
6
+ /** git processes the probe runs at once. */
7
+ export declare const ADVISORY_GIT_CONCURRENCY = 4;
8
+ export type AdvisoryWorkspaceFingerprintProbe = {
9
+ kind: 'fingerprint';
10
+ /** `git`: launch inside a repository; `walk`: no repository handed to git; `walk+git`: some were */
11
+ method: 'git' | 'walk' | 'walk+git';
12
+ value: string;
13
+ entries: number;
14
+ durationMs: number;
15
+ /** control sets of repositories found by the walk (absolute path → set), for the after-turn pre-check */
16
+ controlSets?: Readonly<Record<string, string>>;
17
+ } | {
18
+ kind: 'probe-failed';
19
+ method: 'git' | 'walk' | 'marker';
20
+ reason: string;
21
+ durationMs: number;
22
+ };
23
+ export interface AdvisoryProbeOptions {
24
+ timeoutMs?: number;
25
+ maxEntries?: number;
26
+ /** the before-turn result; its control sets gate any git run in a repository the walk finds */
27
+ before?: AdvisoryWorkspaceFingerprintProbe;
28
+ /** timeout of the launch-folder classification call (`git rev-parse --show-toplevel`) */
29
+ classifyTimeoutMs?: number;
30
+ /** git processes run at once (default ADVISORY_GIT_CONCURRENCY) */
31
+ gitConcurrency?: number;
32
+ availability?: AvailabilityDeps;
33
+ }
34
+ export declare function probeAdvisoryWorkspaceFingerprintImpl(workspaceRoot: string, options?: AdvisoryProbeOptions): Promise<AdvisoryWorkspaceFingerprintProbe>;
@@ -0,0 +1,14 @@
1
+ import type { BrainstormPanelActiveAgent } from './brainstorm-quorum';
2
+ import type { ShellEventEmitResult } from './emit-shell-event';
3
+ export declare const AGENT_TURN_DESKTOP_ONLY_DEGRADE_NOTICE = "Note: companion sync was unavailable for this turn; response shown on desktop only.";
4
+ export interface AgentTurnCellSection {
5
+ readonly authorAgent: BrainstormPanelActiveAgent;
6
+ readonly boundedBody: string;
7
+ }
8
+ export declare function shouldDegradeAgentTurnOnPreEmitFailure(promptResult: ShellEventEmitResult, meta: {
9
+ readonly sessionId: string;
10
+ readonly route: string;
11
+ readonly turnId: string;
12
+ }): boolean;
13
+ export declare function formatAgentTurnCellsMarkdown(cells: ReadonlyArray<AgentTurnCellSection>, formatDisplayName: (agent: BrainstormPanelActiveAgent) => string): string;
14
+ export declare function renderDegradedAgentTurnOnDesktop(cells: ReadonlyArray<AgentTurnCellSection>, formatDisplayName: (agent: BrainstormPanelActiveAgent) => string, dispatchAdvisory: (text: string) => void): void;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * P63 Slice 3 (WORKSPACE-HARDENING-DESIGN §10.1; F-S3-RETRY-BRIEF-BINARY) — in the P46a retry brief, a collided file that
3
+ * is a binary on either side has no text to show: it is named with its class and left to the copy, never decoded.
4
+ * Extracted from quorum-loop.ts (a frozen monolith), which keeps a two-line delegation.
5
+ */
6
+ import { type ShadowDiffFile } from '../local-executor/shadow-protocol';
7
+ /** The brief's lines for a binary collision, or null for text (the caller shows the reviewed text). */
8
+ export declare function binaryRetryBriefLines(filePath: string, who: string, reviewedFile: ShadowDiffFile | undefined): string[] | null;
@@ -75,7 +75,9 @@ export declare function agentIdForAuthor(agent: BrainstormPanelActiveAgent): 'cl
75
75
  * The display names come from the caller (index.ts `agentDisplayName`) so the
76
76
  * mapping stays single-sourced.
77
77
  */
78
- export declare function renderBrainstormPanelHeader(activeAgents: readonly BrainstormPanelActiveAgent[], displayName: (agent: BrainstormPanelActiveAgent) => string): string;
78
+ export declare function renderBrainstormPanelHeader(activeAgents: readonly BrainstormPanelActiveAgent[], displayName: (agent: BrainstormPanelActiveAgent) => string, options?: {
79
+ readonly hasWebContext?: boolean;
80
+ }): string;
79
81
  /**
80
82
  * Below Max: the frontier panel is fenced, but the local model still
81
83
  * brainstorms. No frontier agent was called and no code was changed.
@@ -95,12 +97,22 @@ export declare function brainstormAllQuotaWalledMessage(walled: readonly Brainst
95
97
  * `logger.warn` (the WIRE is what the ZK invariant governs, NOT the local log
96
98
  * file, which never leaves the machine). The invariant is stricter than
97
99
  * ZK-on-wire (the mirror is E2E-encrypted regardless): only a BOUNDED enum label
98
- * is surfaced on the wire, never the agent's diagnostic. Three labels only —
99
- * quota-wall vs timeout vs generic failure — so no free-form detail can leak
100
- * through. [#635] the `'quota_walled'`/`'failed'` strings are preserved VERBATIM
100
+ * is surfaced on the wire, never the agent's diagnostic. Five closed labels —
101
+ * quota-wall, timeout, the two workspace-check outcomes (repo-aware fingerprint
102
+ * design r6), and generic failure — so no free-form detail can leak through. [#635] the `'quota_walled'`/`'failed'` strings are preserved VERBATIM
101
103
  * from the prior 2-label version; only `'timed_out'` is new.
102
104
  */
103
- export type BrainstormPanelFailureReason = 'quota_walled' | 'timed_out' | 'failed';
105
+ export type BrainstormPanelFailureReason = 'quota_walled' | 'timed_out' | 'workspace_unverifiable' | 'workspace_changed' | 'failed';
106
+ /** The user-facing text for a read-only turn refused by the workspace check, or null for any other reason. */
107
+ export declare function workspaceVerificationMessage(unavailableReason: string | null | undefined): string | null;
108
+ /** Map a failed advisory result to its closed panel label: quota wall > timeout > workspace check > generic. */
109
+ export declare function brainstormPanelFailureReasonFor(result: {
110
+ quotaWalled: boolean;
111
+ timedOut: boolean;
112
+ usage: {
113
+ unavailableReason?: string | null;
114
+ };
115
+ }): BrainstormPanelFailureReason;
104
116
  export declare function brainstormPanelFailureNotice(reason: BrainstormPanelFailureReason): string;
105
117
  export declare const BRAINSTORM_PRIOR_CONTEXT_LABEL = "Prior brainstorm context (idea so far + open questions):";
106
118
  export declare const BRAINSTORM_REQUEST_LABEL = "Current brainstorm request:";