@mstar-harness/engine 3.9.2 → 3.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,170 @@
1
+ import type { ValidationResult } from "./core.js";
2
+ /** Stable refusal codes of the scoped coordination surface (spec §C4). */
3
+ export declare const COORDINATION_ERROR_CODES: readonly ["coordination.harness-not-found", "coordination.workflow-not-found", "coordination.plan-not-found", "coordination.scope-mismatch", "coordination.path-mismatch", "coordination.assignment-invalid", "coordination.assignment-stale", "coordination.not-prepared", "coordination.duplicate-holder", "coordination.session-mismatch", "coordination.session-not-found", "coordination.session-role", "coordination.version-conflict", "coordination.expected-version-required", "coordination.invalid-transition", "coordination.invalid-input", "coordination.forbidden-field", "coordination.not-in-git", "coordination.git-unavailable", "coordination.git-proof", "coordination.evidence-stale", "coordination.integration-unresolved", "coordination.integration-diverged", "coordination.local-store-required", "coordination.direct-write-refused", "coordination.scoped-writer-required", "coordination.unknown-operation", "coordination.not-implemented", "coordination.lock", "coordination.store"];
4
+ export type CoordinationErrorCode = (typeof COORDINATION_ERROR_CODES)[number];
5
+ /**
6
+ * Stable exception of the coordination surface: `code` is the consumer
7
+ * contract, `details` carries the machine-readable context (path, expected,
8
+ * actual, holder, …). Exported publicly from `coordination.ts`.
9
+ */
10
+ export declare class CoordinationError extends Error {
11
+ readonly code: CoordinationErrorCode;
12
+ readonly details: Record<string, unknown>;
13
+ constructor(code: CoordinationErrorCode, message: string, details?: Record<string, unknown>);
14
+ }
15
+ export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
16
+ /** Non-empty trimmed string predicate. */
17
+ export declare function isNonEmptyString(value: unknown): value is string;
18
+ /** `sha256:<64 lowercase hex>` of the exact bytes handed in. */
19
+ export declare function artifactVersion(bytes: Buffer | string): string;
20
+ /** SHA-256 (bare lowercase hex) of the exact bytes handed in. */
21
+ export declare function sha256Bytes(bytes: Buffer | string): string;
22
+ /** Artifact bytes read once: payload + byte version from that same read. */
23
+ export type ArtifactBytes = {
24
+ payload: unknown;
25
+ version: string;
26
+ };
27
+ /**
28
+ * Read an artifact once and derive both payload and version from the same
29
+ * bytes. Missing file → `undefined` (the caller reports `absent`).
30
+ * Malformed JSON throws (never a silent empty document).
31
+ */
32
+ export declare function readArtifactBytes(filePath: string): ArtifactBytes | undefined;
33
+ /**
34
+ * Canonicalize a target: realpath when it exists (symlinks resolved), else the
35
+ * realpath of its nearest existing ancestor with the missing tail re-attached.
36
+ * Aliases therefore collapse onto the real protected file whether or not the
37
+ * leaf has been created yet, so a `json`/symlink ref can never dodge the
38
+ * boundary. A lexical fallback would do exactly that: a symlinked parent stays
39
+ * unresolved, the alias classifies as unprotected, and `put` creates the
40
+ * protected document through it.
41
+ *
42
+ * The same rule as `path.ts#canonicalizeNearestExisting`, restated here because
43
+ * `path.ts` imports this module (importing back would close an ESM cycle).
44
+ */
45
+ export declare function canonicalTarget(target: string): string;
46
+ /** Protected document class, decided by the caller's resolved path table. */
47
+ export type ProtectedWriteKind = "root" | "snapshot" | "register";
48
+ /**
49
+ * Run `fn` inside the private authorization context for `target`. Only the
50
+ * locked coordination/writer implementation calls this; the context records
51
+ * canonical target + operation, never a caller-supplied boolean, and nested
52
+ * authorizations stack (a residual write authorizes both the snapshot and
53
+ * the register it touches).
54
+ */
55
+ export declare function withProtectedWrite<T>(target: string, operation: "put" | "delete", fn: () => T | Promise<T>): Promise<T>;
56
+ /** `true` when the current async context authorized exactly this target+operation. */
57
+ export declare function isWriteAuthorized(target: string, operation: "put" | "delete"): boolean;
58
+ /**
59
+ * Refuse an un-authorized write to a protected target (spec §C4). The
60
+ * target's class is decided by the store's resolved path table (never by
61
+ * document content, which a caller could shape); the authorization is the
62
+ * private context alone.
63
+ */
64
+ export declare function assertProtectedWriteAuthorized(target: string, operation: "put" | "delete", kind: ProtectedWriteKind): void;
65
+ /** Exact-key contract: unknown keys anywhere are rejected before mutation. */
66
+ export declare function assertExactKeys(value: Record<string, unknown>, allowed: readonly string[], what: string): void;
67
+ /** Row/progress status a plan session may report (`progress` op). */
68
+ export type PlanProgressStatus = "InProgress" | "InReview" | "Blocked";
69
+ /** Progress payload a plan session reports on its own row. */
70
+ export type PlanProgress = {
71
+ status: PlanProgressStatus;
72
+ summary: string;
73
+ /** Canonical absolute artifacts inside the plan's own plan/SDD area. */
74
+ evidence_paths: string[];
75
+ /** L2 track branches reported for this plan (never main/integration). */
76
+ track_branches?: string[];
77
+ };
78
+ /** Hash-pinned reference to a submitted evidence file. */
79
+ export type EvidenceRef = {
80
+ path: string;
81
+ sha256: string;
82
+ };
83
+ /** Handoff lifecycle state (`PlanHandoff.state`). */
84
+ export type HandoffState = "submitted" | "accepted" | "returned" | "integrating" | "merged" | "completed";
85
+ /** QC review outcome recorded on a handoff. */
86
+ export type HandoffQc = {
87
+ decision: string;
88
+ reports: EvidenceRef[];
89
+ consolidated: EvidenceRef;
90
+ };
91
+ /** QA verification outcome recorded on a handoff. */
92
+ export type HandoffQa = {
93
+ gate: string;
94
+ decision: string;
95
+ report: EvidenceRef;
96
+ };
97
+ /** The single integration attempt recorded on a handoff. */
98
+ export type HandoffIntegration = {
99
+ target_branch: string;
100
+ worktree_path: string;
101
+ base_sha: string;
102
+ started_at: string;
103
+ result_sha?: string;
104
+ verified_at?: string;
105
+ };
106
+ /** The durable review package of one plan (spec §B `PlanHandoff`). */
107
+ export type PlanHandoff = {
108
+ id: string;
109
+ attempt: number;
110
+ state: HandoffState;
111
+ submitted_by: string;
112
+ submitted_at: string;
113
+ source_branch: string;
114
+ source_sha: string;
115
+ worktree_path: string;
116
+ review_base: string;
117
+ review_head: string;
118
+ qc: HandoffQc;
119
+ qa: HandoffQa;
120
+ accepted_by?: string;
121
+ accepted_at?: string;
122
+ returned_at?: string;
123
+ return_reason?: string;
124
+ integration?: HandoffIntegration;
125
+ completed_at?: string;
126
+ };
127
+ /** Coordinator-recorded preparation of one plan (spec §D `prepare`). */
128
+ export type PreparedCoordination = {
129
+ assignment_path: string;
130
+ assignment_sha256: string;
131
+ plan_sha256: string;
132
+ qa_gate: string;
133
+ findings_cleanup: string;
134
+ prepared_by: string;
135
+ prepared_at: string;
136
+ };
137
+ /** A bound session: identity + the canonical envelope that proves it. */
138
+ export type CoordinatorBinding = {
139
+ session_id: string;
140
+ session_file: string;
141
+ bound_at: string;
142
+ };
143
+ /** Snapshot-level coordination block (the workflow's coordinator). */
144
+ export type SnapshotCoordination = {
145
+ coordinator: CoordinatorBinding;
146
+ };
147
+ /** Row-level coordination block (one plan). */
148
+ export type RowCoordination = {
149
+ revision: number;
150
+ prepared?: PreparedCoordination;
151
+ session?: CoordinatorBinding;
152
+ progress?: PlanProgress;
153
+ handoff?: PlanHandoff;
154
+ };
155
+ export declare const HANDOFF_STATES: readonly HandoffState[];
156
+ export declare const PLAN_PROGRESS_STATUSES: readonly PlanProgressStatus[];
157
+ /** Validate a stored `PlanProgress` (`status`, `summary`, `evidence_paths`, `track_branches`). */
158
+ export declare function validatePlanProgress(value: unknown, what?: string): ValidationResult[];
159
+ /** Validate a stored `PlanHandoff`, including its state/field coherence. */
160
+ export declare function validatePlanHandoff(value: unknown, what?: string): ValidationResult[];
161
+ /** Validate a stored `PreparedCoordination`. */
162
+ export declare function validatePreparedCoordination(value: unknown, what?: string): ValidationResult[];
163
+ /** Validate one plan row's `coordination` object (spec §C2). */
164
+ export declare function validateRowCoordination(value: unknown, what?: string): ValidationResult[];
165
+ /** Validate a snapshot's top `coordination` block (spec §C2). */
166
+ export declare function validateSnapshotCoordination(value: unknown, what?: string): ValidationResult[];
167
+ /** Hash-pinned evidence reference for an absolute path, read from disk. */
168
+ export declare function evidenceRefOf(filePath: string): EvidenceRef;
169
+ /** Whether `value` is a well-formed `sha256:<64 hex>` artifact version. */
170
+ export declare function isArtifactVersion(value: unknown): value is string;
@@ -0,0 +1,284 @@
1
+ import { CoordinationError, type PlanProgress, type PreparedCoordination } from "./coordination-write.js";
2
+ import { type PlanRow } from "./status.js";
3
+ import { type ArtifactRef } from "./store.js";
4
+ /**
5
+ * Re-exported from the storage layer because this module is the public entry
6
+ * point for the scoped writers: callers catch `CoordinationError` and branch
7
+ * on its `code` without importing the storage layer directly.
8
+ */
9
+ export { CoordinationError };
10
+ /** Bind roles: one coordinator per lifecycle, one plan session per plan. */
11
+ export type CoordinationRole = "plan-pm" | "coordinator";
12
+ /**
13
+ * Scope address, both forms required by spec §B: from a pinned Assignment
14
+ * path, or from a workflow/plan pair resolved through that row's `prepared`
15
+ * block. Both forms resolve to the same `ResolvedPlanScope`.
16
+ */
17
+ export type PlanScopeInput = {
18
+ assignmentPath: string;
19
+ } | {
20
+ workflowId: string;
21
+ planId: string;
22
+ harnessDir?: string;
23
+ };
24
+ /** The fully pinned plan scope (spec §B `ResolvedPlanScope`). */
25
+ export type ResolvedPlanScope = {
26
+ harnessRoot: string;
27
+ workflowId: string;
28
+ planId: string;
29
+ /** `{WORKFLOW_DIR}/<workflow-id>/snapshot.json`. */
30
+ snapshotPath: string;
31
+ /** The plan markdown pinned by the Assignment `Plan Path`. */
32
+ planPath: string;
33
+ assignmentPath: string;
34
+ worktreePath: string;
35
+ workingBranch: string;
36
+ projectId: string;
37
+ sddDir: string;
38
+ };
39
+ /**
40
+ * Session envelope persisted at
41
+ * `{WORKFLOW_DIR}/<workflow-id>/sessions/<session-id>.json` (mode `0600`).
42
+ * The envelope is the durable proof of who holds the session: the snapshot
43
+ * stores its canonical path and every later call must present the same file.
44
+ */
45
+ export type CoordinationSession = {
46
+ schema_version: 1;
47
+ role: CoordinationRole;
48
+ session_id: string;
49
+ workflow_id: string;
50
+ plan_id?: string;
51
+ harness_root: string;
52
+ };
53
+ /**
54
+ * Bind addressing (spec §B). A fresh bind never supplies a session path: the
55
+ * engine generates the session UUID and creates
56
+ * `<workflow-dir>/<workflow-id>/sessions/<session-id>.json` itself. An existing
57
+ * session is reached only through its explicit `resumePath`, which resumes
58
+ * read-only and never writes.
59
+ */
60
+ export type BindPlanSessionInput = {
61
+ scope: PlanScopeInput;
62
+ cwd: string;
63
+ } | {
64
+ coordinator: true;
65
+ workflowId: string;
66
+ harnessDir?: string;
67
+ cwd: string;
68
+ } | {
69
+ resumePath: string;
70
+ cwd: string;
71
+ };
72
+ /** One artifact read: payload plus the byte version it was read at. */
73
+ export type VersionedArtifact = {
74
+ payload: unknown;
75
+ version: string;
76
+ };
77
+ /** Everything `mstar plan show` needs, in one read. */
78
+ export type PlanCoordinationView = {
79
+ /** Row `coordination.revision` (`0` when the row is not yet coordinated). */
80
+ revision: number;
81
+ /** `sha256:…` of the snapshot bytes, or `"absent"`. */
82
+ snapshot_version: string;
83
+ /** `sha256:…` of this plan's project register bytes, or `"absent"`. */
84
+ register_version: string;
85
+ /** `null` while the row carries no `prepared` block. */
86
+ scope: ResolvedPlanScope | null;
87
+ row: PlanRow;
88
+ prepared?: PreparedCoordination;
89
+ session: CoordinationSession;
90
+ session_file: string;
91
+ /** Operations this session may run now — implemented operations only. */
92
+ allowed_operations: string[];
93
+ };
94
+ /** Success shape of a coordination call. */
95
+ export type CoordinationResult = {
96
+ ok: true;
97
+ operation: string;
98
+ session: CoordinationSession;
99
+ session_file: string;
100
+ /** `claimed` / `resumed` / `prepared` / `progressed` / `residual-added` / `residual-closed`. */
101
+ outcome?: string;
102
+ view?: PlanCoordinationView;
103
+ };
104
+ /** One residual being registered: the nine v1 fields (+ optional `detail_doc`). */
105
+ export type ResidualInput = {
106
+ id: string;
107
+ title: string;
108
+ severity: string;
109
+ source: string;
110
+ scope: string;
111
+ decision: string;
112
+ owner: string;
113
+ target: string;
114
+ tracking: string;
115
+ detail_doc?: string;
116
+ };
117
+ export type PrepareCoordinationRequest = {
118
+ kind: "prepare";
119
+ /** The plan's pinned Assignment (absolute). */
120
+ assignmentPath: string;
121
+ expectedRevision: number;
122
+ };
123
+ export type ProgressCoordinationRequest = {
124
+ kind: "progress";
125
+ progress: PlanProgress;
126
+ expectedRevision: number;
127
+ };
128
+ export type ResidualAddCoordinationRequest = {
129
+ kind: "residual-add";
130
+ entries: ResidualInput[];
131
+ expectedRegisterVersion: string;
132
+ /** Optional extra guard: the row revision must still match. */
133
+ expectedRevision?: number;
134
+ };
135
+ export type ResidualCloseCoordinationRequest = {
136
+ kind: "residual-close";
137
+ entryId: string;
138
+ note: string;
139
+ expectedRegisterVersion: string;
140
+ expectedRevision?: number;
141
+ };
142
+ /**
143
+ * Handoff evidence (spec §D). Slice A types it so the union is stable; the
144
+ * operations that consume it arrive with the handoff slice.
145
+ */
146
+ export type HandoffEvidence = {
147
+ source_sha: string;
148
+ review_base: string;
149
+ review_head: string;
150
+ qc: {
151
+ decision: "Approve" | "Approve with residuals";
152
+ reports: string[];
153
+ consolidated: string;
154
+ };
155
+ qa: {
156
+ gate: "mandatory" | "pm-acceptance";
157
+ decision: "pass";
158
+ report: string;
159
+ };
160
+ };
161
+ /**
162
+ * The operation surface (spec §B): every kind is implemented and typed, so an
163
+ * unknown shape is refused as `coordination.invalid-input` rather than
164
+ * silently accepted.
165
+ */
166
+ export type PlanCoordinationOperation = {
167
+ kind: "prepare";
168
+ assignmentPath: string;
169
+ } | {
170
+ kind: "progress";
171
+ progress: PlanProgress;
172
+ } | {
173
+ kind: "residual-add";
174
+ entries: ResidualInput[];
175
+ expectedRegisterVersion: string;
176
+ } | {
177
+ kind: "residual-close";
178
+ entryId: string;
179
+ note: string;
180
+ expectedRegisterVersion: string;
181
+ } | {
182
+ kind: "handoff";
183
+ evidence: HandoffEvidence;
184
+ } | {
185
+ kind: "accept";
186
+ handoffId: string;
187
+ } | {
188
+ kind: "return";
189
+ handoffId: string;
190
+ reason: string;
191
+ } | {
192
+ kind: "integration-start";
193
+ handoffId: string;
194
+ } | {
195
+ kind: "integration-accept";
196
+ handoffId: string;
197
+ } | {
198
+ kind: "complete";
199
+ handoffId: string;
200
+ } | {
201
+ kind: "reconcile";
202
+ handoffId: string;
203
+ };
204
+ /** One whole coordination request: one session, one operation, one precondition. */
205
+ export type CoordinationRequest = {
206
+ sessionPath: string;
207
+ /** Required for a coordinator session; never another plan for a plan session. */
208
+ planId?: string;
209
+ /** The selected row's `coordination.revision` from `show` (absent row = 0). */
210
+ expectedRevision: number;
211
+ operation: PlanCoordinationOperation;
212
+ };
213
+ /** Coordinator replacement of a coordinated artifact (spec §B). */
214
+ export type CoordinatedReplacement = {
215
+ harnessRoot: string;
216
+ ref: ArtifactRef;
217
+ payload: unknown;
218
+ /** Byte version the writer expects (`sha256:…`, or `absent` to create). */
219
+ expectedVersion: string;
220
+ /** Required for snapshot replacement (the coordinator session envelope). */
221
+ sessionPath?: string;
222
+ };
223
+ /**
224
+ * The process-wide harness root (spec §C1): resolved from the **main**
225
+ * worktree's root, never from the checkout the process happens to sit in.
226
+ * This is the fix for the process-root bug — a process inside a linked
227
+ * worktree must not resolve the worktree's own `.mstar`.
228
+ *
229
+ * Returns `null` when no harness root is resolvable (the unscoped CLI keeps
230
+ * its existing non-Git fallback). Throws `coordination.not-in-git` when the
231
+ * cwd is provably a linked checkout (a `.git` **file**) whose main worktree
232
+ * cannot be read — falling back to local artifacts there is exactly the bug.
233
+ */
234
+ export declare function resolveProcessHarnessDir(cwd?: string, harnessDir?: string): string | null;
235
+ /**
236
+ * Resolve the plan scope (spec §B). Form A pins the scope from the Assignment
237
+ * itself; form B starts from `{workflowId, planId}` and reads the pinned
238
+ * Assignment path out of that row's `prepared` block — never "the first
239
+ * unfinished row".
240
+ */
241
+ export declare function resolvePlanScope(input: PlanScopeInput, cwd?: string): Promise<ResolvedPlanScope>;
242
+ /** Read and validate a session envelope (throws `coordination.session-*`). */
243
+ export declare function readSessionEnvelope(sessionPath: string): CoordinationSession;
244
+ /**
245
+ * Read this session's plan coordination view (spec §B). A coordinator session
246
+ * must select a plan; a plan session reads only its own row. A row that is not
247
+ * yet prepared yields `scope: null` and the raw row.
248
+ */
249
+ export declare function readPlanCoordination(sessionPath: string, planId?: string, cwd?: string): Promise<PlanCoordinationView>;
250
+ /**
251
+ * Read one coordinated artifact plus its byte version, from a **single** byte
252
+ * read. `payload` is `undefined` and `version` is `"absent"` when the document
253
+ * does not exist. Snapshot payloads are validated before they are handed out.
254
+ */
255
+ export declare function readCoordinatedArtifact(harnessRoot: string, ref: ArtifactRef): Promise<VersionedArtifact>;
256
+ /**
257
+ * Bind a session (spec §C2). A fresh bind creates the envelope, claims the L1
258
+ * lease and records the binding in the same snapshot commit; an existing
259
+ * envelope is a **read-only** resume that only re-verifies the binding.
260
+ */
261
+ /**
262
+ * Bind a session (spec §B, §C2). Fresh addressing never supplies a session
263
+ * path: the engine generates the UUID and creates the envelope. `resumePath`
264
+ * names an existing envelope and resumes read-only.
265
+ */
266
+ export declare function bindPlanSession(input: BindPlanSessionInput): Promise<CoordinationResult>;
267
+ /**
268
+ * Run one coordinated mutation (spec §B/§D). Every call re-authenticates the
269
+ * session from its envelope, re-checks the Assignment hash, enforces the
270
+ * revision/register precondition under the lock, and writes through
271
+ * `withProtectedWrite`. The operation surface is a closed discriminated union:
272
+ * an unknown key anywhere is rejected before any state is touched.
273
+ */
274
+ export declare function mutatePlanCoordination(request: CoordinationRequest): Promise<CoordinationResult>;
275
+ /**
276
+ * Replace a coordinated artifact with an exact-version precondition (spec §B,
277
+ * §C4 line 156). Snapshot replacement goes through the canonical snapshot
278
+ * writer (coordinator session, phase-only delta, locked CAS); the root status
279
+ * and a project register are written with the same byte-version CAS under
280
+ * root → snapshot → destination locks, refusing any document that carries
281
+ * coordinated ownership. `review`/`json` are not coordinated artifacts, so
282
+ * they refuse explicitly instead of silently no-opping.
283
+ */
284
+ export declare function replaceCoordinatedArtifact(input: CoordinatedReplacement): Promise<VersionedArtifact>;