@indigoai-us/hq-cli 5.108.1 → 5.108.2
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.
- package/CHANGELOG.md +77 -0
- package/assets/mesh-daemon/README.md +23 -0
- package/assets/mesh-daemon/launchd.plist.template +33 -0
- package/assets/mesh-daemon/systemd.system.service.template +27 -0
- package/assets/mesh-daemon/systemd.user.service.template +18 -0
- package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +267 -0
- package/dist/commands/core.js +5 -4
- package/dist/commands/mesh.d.ts +15 -0
- package/dist/commands/mesh.js +732 -4
- package/dist/commands/search.d.ts +23 -0
- package/dist/commands/search.js +42 -1
- package/dist/lib/doctor/checks/work-context.d.ts +23 -0
- package/dist/lib/doctor/checks/work-context.js +195 -0
- package/dist/lib/doctor/registry.js +4 -0
- package/dist/lib/mesh/api.d.ts +18 -0
- package/dist/lib/mesh/api.js +55 -0
- package/dist/lib/mesh/cache.js +35 -4
- package/dist/lib/mesh/client.d.ts +118 -0
- package/dist/lib/mesh/client.js +268 -0
- package/dist/lib/mesh/live/backoff.d.ts +16 -0
- package/dist/lib/mesh/live/backoff.js +20 -0
- package/dist/lib/mesh/live/daemon/board-refresh.d.ts +49 -0
- package/dist/lib/mesh/live/daemon/board-refresh.js +185 -0
- package/dist/lib/mesh/live/daemon/credentials.d.ts +71 -0
- package/dist/lib/mesh/live/daemon/credentials.js +227 -0
- package/dist/lib/mesh/live/daemon/doctor.d.ts +37 -0
- package/dist/lib/mesh/live/daemon/doctor.js +139 -0
- package/dist/lib/mesh/live/daemon/index.d.ts +24 -0
- package/dist/lib/mesh/live/daemon/index.js +14 -0
- package/dist/lib/mesh/live/daemon/install.d.ts +83 -0
- package/dist/lib/mesh/live/daemon/install.js +330 -0
- package/dist/lib/mesh/live/daemon/log.d.ts +11 -0
- package/dist/lib/mesh/live/daemon/log.js +79 -0
- package/dist/lib/mesh/live/daemon/paths.d.ts +18 -0
- package/dist/lib/mesh/live/daemon/paths.js +31 -0
- package/dist/lib/mesh/live/daemon/pid-lock.d.ts +48 -0
- package/dist/lib/mesh/live/daemon/pid-lock.js +134 -0
- package/dist/lib/mesh/live/daemon/presence.d.ts +101 -0
- package/dist/lib/mesh/live/daemon/presence.js +250 -0
- package/dist/lib/mesh/live/daemon/presign.d.ts +18 -0
- package/dist/lib/mesh/live/daemon/presign.js +65 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +74 -0
- package/dist/lib/mesh/live/daemon/run.js +400 -0
- package/dist/lib/mesh/live/daemon/state.d.ts +24 -0
- package/dist/lib/mesh/live/daemon/state.js +57 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +170 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.js +520 -0
- package/dist/lib/mesh/live/enqueue.d.ts +48 -0
- package/dist/lib/mesh/live/enqueue.js +100 -0
- package/dist/lib/mesh/live/flush.d.ts +56 -0
- package/dist/lib/mesh/live/flush.js +360 -0
- package/dist/lib/mesh/live/format-spool-line.d.ts +42 -0
- package/dist/lib/mesh/live/format-spool-line.js +106 -0
- package/dist/lib/mesh/live/index.d.ts +22 -0
- package/dist/lib/mesh/live/index.js +16 -0
- package/dist/lib/mesh/live/paths.d.ts +13 -0
- package/dist/lib/mesh/live/paths.js +30 -0
- package/dist/lib/mesh/live/session-events-client.d.ts +58 -0
- package/dist/lib/mesh/live/session-events-client.js +120 -0
- package/dist/lib/mesh/live/session-identity.d.ts +39 -0
- package/dist/lib/mesh/live/session-identity.js +91 -0
- package/dist/lib/mesh/live/spool.d.ts +52 -0
- package/dist/lib/mesh/live/spool.js +193 -0
- package/dist/lib/mesh/live/ulid.d.ts +12 -0
- package/dist/lib/mesh/live/ulid.js +39 -0
- package/dist/lib/mesh/live/validate-session-event.d.ts +39 -0
- package/dist/lib/mesh/live/validate-session-event.js +128 -0
- package/dist/lib/search-index/index.d.ts +45 -0
- package/dist/lib/search-index/index.js +76 -0
- package/dist/lib/work-context/atomic.d.ts +18 -0
- package/dist/lib/work-context/atomic.js +132 -0
- package/dist/lib/work-context/company.d.ts +103 -0
- package/dist/lib/work-context/company.js +274 -0
- package/dist/lib/work-context/config.d.ts +59 -0
- package/dist/lib/work-context/config.js +172 -0
- package/dist/lib/work-context/contract.d.ts +81 -0
- package/dist/lib/work-context/contract.js +65 -0
- package/dist/lib/work-context/errors.d.ts +27 -0
- package/dist/lib/work-context/errors.js +46 -0
- package/dist/lib/work-context/index.d.ts +23 -0
- package/dist/lib/work-context/index.js +20 -0
- package/dist/lib/work-context/migrate.d.ts +84 -0
- package/dist/lib/work-context/migrate.js +287 -0
- package/dist/lib/work-context/organize.d.ts +130 -0
- package/dist/lib/work-context/organize.js +471 -0
- package/dist/lib/work-context/outbox.d.ts +97 -0
- package/dist/lib/work-context/outbox.js +357 -0
- package/dist/lib/work-context/paths.d.ts +14 -0
- package/dist/lib/work-context/paths.js +39 -0
- package/dist/lib/work-context/project.d.ts +78 -0
- package/dist/lib/work-context/project.js +247 -0
- package/dist/lib/work-context/reconcile.d.ts +85 -0
- package/dist/lib/work-context/reconcile.js +651 -0
- package/dist/lib/work-context/repo-remote.d.ts +43 -0
- package/dist/lib/work-context/repo-remote.js +200 -0
- package/dist/lib/work-context/state.d.ts +112 -0
- package/dist/lib/work-context/state.js +162 -0
- package/dist/main.js +40 -3
- package/dist/utils/cognito-session.d.ts +85 -9
- package/dist/utils/cognito-session.js +216 -9
- package/dist/utils/qmd-model-download-error.d.ts +27 -0
- package/dist/utils/qmd-model-download-error.js +114 -0
- package/dist/utils/qmd-query-document-error.d.ts +23 -0
- package/dist/utils/qmd-query-document-error.js +117 -0
- package/dist/utils/sentry-fingerprint.js +2 -0
- package/package.json +5 -1
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hq mesh context reconcile — sole runtime-to-control-plane entrypoint
|
|
3
|
+
* (US-007A company gate + US-007B project/task organization).
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import { InvalidSessionIdentityError, normalizeSessionIdentity, } from "../mesh/live/session-identity.js";
|
|
7
|
+
import { companyCorrectionPath, resolveCompany, } from "./company.js";
|
|
8
|
+
import { WORK_CONTEXT_CONTRACT_VERSION, normalizeTaskId, } from "./contract.js";
|
|
9
|
+
import { EXIT_INVALID_IDENTITY, EXIT_NOT_TRACKING, EXIT_OK, InvalidDecisionOriginError, NotTrackingError, } from "./errors.js";
|
|
10
|
+
import { decisionFromCandidates, } from "./organize.js";
|
|
11
|
+
import { resolveProjectTask, shouldAskAfter } from "./project.js";
|
|
12
|
+
import { deriveRemoteOwnerSlug } from "./repo-remote.js";
|
|
13
|
+
import { enqueueOutbox, markOutboxAcked, markOutboxQuarantined, markOutboxQueued, replayOutbox, } from "./outbox.js";
|
|
14
|
+
import { mergeLocalSessionFields, readSessionState, writeSessionState, } from "./state.js";
|
|
15
|
+
function resultOf(parts) {
|
|
16
|
+
const out = {
|
|
17
|
+
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
18
|
+
kind: parts.kind,
|
|
19
|
+
classification: parts.classification,
|
|
20
|
+
delivery: parts.delivery,
|
|
21
|
+
lifecycle: parts.lifecycle ?? "open",
|
|
22
|
+
sessionId: parts.sessionId,
|
|
23
|
+
clientOperationId: parts.clientOperationId,
|
|
24
|
+
};
|
|
25
|
+
if (parts.companyUid)
|
|
26
|
+
out.companyUid = parts.companyUid;
|
|
27
|
+
if (parts.projectId)
|
|
28
|
+
out.projectId = parts.projectId;
|
|
29
|
+
if (parts.taskId)
|
|
30
|
+
out.taskId = parts.taskId;
|
|
31
|
+
if (parts.bindingEpisodeId)
|
|
32
|
+
out.bindingEpisodeId = parts.bindingEpisodeId;
|
|
33
|
+
if (parts.errorCode)
|
|
34
|
+
out.errorCode = parts.errorCode;
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function parseUntrackedOrigin(obs) {
|
|
38
|
+
if (obs.untrackedOrigin === "user")
|
|
39
|
+
return "user";
|
|
40
|
+
if (obs.untracked) {
|
|
41
|
+
const origin = String(obs.untracked.origin || "").toLowerCase();
|
|
42
|
+
if (origin === "user")
|
|
43
|
+
return "user";
|
|
44
|
+
if (origin === "model" || origin === "agent")
|
|
45
|
+
return "rejected";
|
|
46
|
+
return "rejected";
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
export function parseObservationJson(raw) {
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
if (!parsed || typeof parsed !== "object") {
|
|
53
|
+
throw new Error("Observation must be a JSON object");
|
|
54
|
+
}
|
|
55
|
+
if (parsed.contractVersion !== WORK_CONTEXT_CONTRACT_VERSION) {
|
|
56
|
+
throw new Error(`Unsupported observation contractVersion: ${String(parsed.contractVersion)}`);
|
|
57
|
+
}
|
|
58
|
+
if (!parsed.clientOperationId || typeof parsed.clientOperationId !== "string") {
|
|
59
|
+
throw new Error("Observation requires clientOperationId");
|
|
60
|
+
}
|
|
61
|
+
if (!parsed.identity || typeof parsed.identity !== "object") {
|
|
62
|
+
throw new Error("Observation requires identity");
|
|
63
|
+
}
|
|
64
|
+
return parsed;
|
|
65
|
+
}
|
|
66
|
+
export function loadObservationFromFile(filePath) {
|
|
67
|
+
return parseObservationJson(fs.readFileSync(filePath, "utf8"));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Mark a session untracked (user origin only). Local receipt; no server write.
|
|
71
|
+
*/
|
|
72
|
+
export function markUntracked(sessionId, deps, clientOperationId = `untracked_${sessionId}`) {
|
|
73
|
+
const now = (deps.now ?? (() => new Date()))().toISOString();
|
|
74
|
+
const state = {
|
|
75
|
+
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
76
|
+
sessionId,
|
|
77
|
+
contextStatus: "untracked",
|
|
78
|
+
untrackedReceipt: { origin: "user", at: now, noBackfill: true },
|
|
79
|
+
updatedAt: now,
|
|
80
|
+
};
|
|
81
|
+
const statePath = writeSessionState(state, deps.root);
|
|
82
|
+
return {
|
|
83
|
+
result: resultOf({
|
|
84
|
+
kind: "untracked",
|
|
85
|
+
classification: "untracked",
|
|
86
|
+
delivery: "clean",
|
|
87
|
+
sessionId,
|
|
88
|
+
clientOperationId,
|
|
89
|
+
}),
|
|
90
|
+
exitCode: EXIT_OK,
|
|
91
|
+
diagnostics: ["untracked: local consent receipt only; no server write"],
|
|
92
|
+
statePath,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export async function reconcileObservation(obs, deps) {
|
|
96
|
+
const diagnostics = [];
|
|
97
|
+
const env = deps.env ?? process.env;
|
|
98
|
+
const nowFn = deps.now ?? (() => new Date());
|
|
99
|
+
let sessionId;
|
|
100
|
+
try {
|
|
101
|
+
const normalized = normalizeSessionIdentity({
|
|
102
|
+
sessionId: obs.identity.sessionId,
|
|
103
|
+
providerSessionId: obs.identity.providerSessionId,
|
|
104
|
+
env,
|
|
105
|
+
});
|
|
106
|
+
sessionId = normalized.sessionId;
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
if (err instanceof InvalidSessionIdentityError) {
|
|
110
|
+
const sid = obs.identity.sessionId || "unknown";
|
|
111
|
+
return {
|
|
112
|
+
result: resultOf({
|
|
113
|
+
kind: "error",
|
|
114
|
+
classification: "unresolved",
|
|
115
|
+
delivery: "clean",
|
|
116
|
+
sessionId: sid,
|
|
117
|
+
clientOperationId: obs.clientOperationId,
|
|
118
|
+
errorCode: "InvalidSessionIdentityError",
|
|
119
|
+
}),
|
|
120
|
+
exitCode: EXIT_INVALID_IDENTITY,
|
|
121
|
+
diagnostics: [err.message],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
// Explicit untracked before company resolution.
|
|
127
|
+
const untracked = parseUntrackedOrigin(obs);
|
|
128
|
+
if (untracked === "rejected") {
|
|
129
|
+
return {
|
|
130
|
+
result: resultOf({
|
|
131
|
+
kind: "error",
|
|
132
|
+
classification: "unresolved",
|
|
133
|
+
delivery: "clean",
|
|
134
|
+
sessionId,
|
|
135
|
+
clientOperationId: obs.clientOperationId,
|
|
136
|
+
errorCode: "InvalidDecisionOriginError",
|
|
137
|
+
}),
|
|
138
|
+
exitCode: EXIT_OK,
|
|
139
|
+
diagnostics: [
|
|
140
|
+
new InvalidDecisionOriginError().message,
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (untracked === "user") {
|
|
145
|
+
try {
|
|
146
|
+
return markUntracked(sessionId, deps, obs.clientOperationId);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
if (err instanceof NotTrackingError) {
|
|
150
|
+
return {
|
|
151
|
+
result: resultOf({
|
|
152
|
+
kind: "error",
|
|
153
|
+
classification: "untracked",
|
|
154
|
+
delivery: "clean",
|
|
155
|
+
sessionId,
|
|
156
|
+
clientOperationId: obs.clientOperationId,
|
|
157
|
+
errorCode: "NOT_TRACKING",
|
|
158
|
+
}),
|
|
159
|
+
exitCode: EXIT_NOT_TRACKING,
|
|
160
|
+
diagnostics: [err.message],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Already untracked locally — stay untracked until explicit opt-in (no backfill).
|
|
167
|
+
const prior = readSessionState(sessionId, deps.root);
|
|
168
|
+
if (prior?.contextStatus === "untracked" && prior.untrackedReceipt?.origin === "user") {
|
|
169
|
+
return {
|
|
170
|
+
result: resultOf({
|
|
171
|
+
kind: "untracked",
|
|
172
|
+
classification: "untracked",
|
|
173
|
+
delivery: "clean",
|
|
174
|
+
sessionId,
|
|
175
|
+
clientOperationId: obs.clientOperationId,
|
|
176
|
+
}),
|
|
177
|
+
exitCode: EXIT_OK,
|
|
178
|
+
diagnostics: ["session already untracked (local receipt); no server write"],
|
|
179
|
+
statePath: undefined,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const trusted = obs.trustedContext ?? obs.explicit;
|
|
183
|
+
if (trusted?.taskId || trusted?.storyId) {
|
|
184
|
+
const norm = normalizeTaskId({
|
|
185
|
+
taskId: trusted.taskId,
|
|
186
|
+
storyId: trusted.storyId,
|
|
187
|
+
});
|
|
188
|
+
if (!norm.ok) {
|
|
189
|
+
return {
|
|
190
|
+
result: resultOf({
|
|
191
|
+
kind: "error",
|
|
192
|
+
classification: "unresolved",
|
|
193
|
+
delivery: "clean",
|
|
194
|
+
sessionId,
|
|
195
|
+
clientOperationId: obs.clientOperationId,
|
|
196
|
+
errorCode: norm.code,
|
|
197
|
+
}),
|
|
198
|
+
exitCode: EXIT_OK,
|
|
199
|
+
diagnostics: [`task alias error: ${norm.code}`],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const cwd = typeof obs.cwd === "string" ? obs.cwd : undefined;
|
|
204
|
+
const hqRoot = typeof obs.hqRoot === "string" ? obs.hqRoot : undefined;
|
|
205
|
+
// Prefer an explicit observation remoteOwnerSlug (tests / adapters); otherwise
|
|
206
|
+
// derive from the enclosing git origin + companies/manifest.yaml.
|
|
207
|
+
const remoteOwnerSlug = typeof obs.remoteOwnerSlug === "string"
|
|
208
|
+
? obs.remoteOwnerSlug
|
|
209
|
+
: deriveRemoteOwnerSlug({
|
|
210
|
+
cwd: cwd ?? process.cwd(),
|
|
211
|
+
hqRoot,
|
|
212
|
+
}) ?? undefined;
|
|
213
|
+
const company = resolveCompany({
|
|
214
|
+
root: deps.root,
|
|
215
|
+
sessionId,
|
|
216
|
+
env,
|
|
217
|
+
trusted,
|
|
218
|
+
cwd,
|
|
219
|
+
hqRoot,
|
|
220
|
+
remoteOwnerSlug,
|
|
221
|
+
existingState: prior,
|
|
222
|
+
});
|
|
223
|
+
const nowIso = nowFn().toISOString();
|
|
224
|
+
const startedAt = prior?.startedAt ?? nowIso;
|
|
225
|
+
const toolWrites = typeof obs.toolWrites === "number" && obs.toolWrites >= 0
|
|
226
|
+
? Math.floor(obs.toolWrites)
|
|
227
|
+
: prior?.toolWrites;
|
|
228
|
+
const hasTurn = Boolean(obs.hasTurn || deps.markTurn || prior?.hasTurn);
|
|
229
|
+
const transcriptFields = obs.source === "transcript"
|
|
230
|
+
? {
|
|
231
|
+
source: "transcript",
|
|
232
|
+
transcriptMarker: "from transcript",
|
|
233
|
+
}
|
|
234
|
+
: {};
|
|
235
|
+
if (company.status === "needs_company") {
|
|
236
|
+
let statePath;
|
|
237
|
+
try {
|
|
238
|
+
statePath = writeSessionState(mergeLocalSessionFields({
|
|
239
|
+
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
240
|
+
sessionId,
|
|
241
|
+
contextStatus: "needs_company",
|
|
242
|
+
updatedAt: nowIso,
|
|
243
|
+
startedAt,
|
|
244
|
+
...(typeof toolWrites === "number" ? { toolWrites } : {}),
|
|
245
|
+
...(hasTurn ? { hasTurn: true } : {}),
|
|
246
|
+
...transcriptFields,
|
|
247
|
+
}, prior), deps.root);
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
if (err instanceof NotTrackingError) {
|
|
251
|
+
return {
|
|
252
|
+
result: resultOf({
|
|
253
|
+
kind: "error",
|
|
254
|
+
classification: "needs_company",
|
|
255
|
+
delivery: "clean",
|
|
256
|
+
sessionId,
|
|
257
|
+
clientOperationId: obs.clientOperationId,
|
|
258
|
+
errorCode: "NOT_TRACKING",
|
|
259
|
+
}),
|
|
260
|
+
exitCode: EXIT_NOT_TRACKING,
|
|
261
|
+
diagnostics: [err.message],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
throw err;
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
result: resultOf({
|
|
268
|
+
kind: "needs_company",
|
|
269
|
+
classification: "needs_company",
|
|
270
|
+
delivery: "clean",
|
|
271
|
+
sessionId,
|
|
272
|
+
clientOperationId: obs.clientOperationId,
|
|
273
|
+
}),
|
|
274
|
+
exitCode: EXIT_OK,
|
|
275
|
+
diagnostics: ["needs_company: no tenant read or write"],
|
|
276
|
+
statePath,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (company.status === "company_conflict") {
|
|
280
|
+
const correctionPath = company.correctionPath ??
|
|
281
|
+
(company.afterBinding ? companyCorrectionPath(sessionId) : undefined);
|
|
282
|
+
const statePath = writeSessionState(mergeLocalSessionFields({
|
|
283
|
+
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
284
|
+
sessionId,
|
|
285
|
+
contextStatus: "company_conflict",
|
|
286
|
+
companySlug: company.defaultCompany.slug,
|
|
287
|
+
companyUid: company.defaultCompany.uid,
|
|
288
|
+
updatedAt: nowIso,
|
|
289
|
+
startedAt,
|
|
290
|
+
...(typeof toolWrites === "number" ? { toolWrites } : {}),
|
|
291
|
+
...(hasTurn ? { hasTurn: true } : {}),
|
|
292
|
+
// Preserve prior binding ids for visibility; do not auto-switch.
|
|
293
|
+
...(prior?.projectId ? { projectId: prior.projectId } : {}),
|
|
294
|
+
...(prior?.taskId ? { taskId: prior.taskId } : {}),
|
|
295
|
+
...(prior?.bindingEpisodeId
|
|
296
|
+
? { bindingEpisodeId: prior.bindingEpisodeId }
|
|
297
|
+
: {}),
|
|
298
|
+
...transcriptFields,
|
|
299
|
+
decision: {
|
|
300
|
+
decisionId: `conflict_${sessionId}`,
|
|
301
|
+
options: company.afterBinding
|
|
302
|
+
? [
|
|
303
|
+
{
|
|
304
|
+
optionId: "correct_company",
|
|
305
|
+
label: "Correct company (US-017B)",
|
|
306
|
+
kind: "override_evidence",
|
|
307
|
+
},
|
|
308
|
+
]
|
|
309
|
+
: [
|
|
310
|
+
{
|
|
311
|
+
optionId: "keep_default",
|
|
312
|
+
label: "Keep device default",
|
|
313
|
+
kind: "keep_default",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
optionId: "override_evidence",
|
|
317
|
+
label: "Use deterministic evidence",
|
|
318
|
+
kind: "override_evidence",
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
askAfter: false,
|
|
322
|
+
...(correctionPath ? { correctionPath } : {}),
|
|
323
|
+
},
|
|
324
|
+
}, prior), deps.root);
|
|
325
|
+
return {
|
|
326
|
+
result: resultOf({
|
|
327
|
+
kind: "company_conflict",
|
|
328
|
+
classification: "company_conflict",
|
|
329
|
+
delivery: "clean",
|
|
330
|
+
sessionId,
|
|
331
|
+
clientOperationId: obs.clientOperationId,
|
|
332
|
+
}),
|
|
333
|
+
exitCode: EXIT_OK,
|
|
334
|
+
diagnostics: [
|
|
335
|
+
`company_conflict: default=${company.defaultCompany.slug ?? "?"} evidence=${company.evidenceCompany.slug ?? "?"} — no tenant I/O` +
|
|
336
|
+
(correctionPath ? `; correctionPath=${correctionPath}` : ""),
|
|
337
|
+
],
|
|
338
|
+
statePath,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
// Membership-validated slug→uid when local resolution only yielded a slug.
|
|
342
|
+
let resolvedCompany = company.company;
|
|
343
|
+
if (!resolvedCompany.uid && resolvedCompany.slug && deps.validateMembership) {
|
|
344
|
+
try {
|
|
345
|
+
const membership = await deps.validateMembership(resolvedCompany.slug);
|
|
346
|
+
if (membership && typeof membership === "object" && membership.uid) {
|
|
347
|
+
resolvedCompany = {
|
|
348
|
+
...resolvedCompany,
|
|
349
|
+
uid: membership.uid,
|
|
350
|
+
};
|
|
351
|
+
diagnostics.push(`membership resolved companyUid for slug=${resolvedCompany.slug}`);
|
|
352
|
+
}
|
|
353
|
+
else if (membership === false) {
|
|
354
|
+
diagnostics.push(`membership denied for slug=${resolvedCompany.slug}; treating as needs_company`);
|
|
355
|
+
const statePath = writeSessionState(mergeLocalSessionFields({
|
|
356
|
+
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
357
|
+
sessionId,
|
|
358
|
+
contextStatus: "needs_company",
|
|
359
|
+
companySlug: resolvedCompany.slug,
|
|
360
|
+
updatedAt: nowIso,
|
|
361
|
+
startedAt,
|
|
362
|
+
...(typeof toolWrites === "number" ? { toolWrites } : {}),
|
|
363
|
+
...(hasTurn ? { hasTurn: true } : {}),
|
|
364
|
+
...transcriptFields,
|
|
365
|
+
}, prior), deps.root);
|
|
366
|
+
return {
|
|
367
|
+
result: resultOf({
|
|
368
|
+
kind: "needs_company",
|
|
369
|
+
classification: "needs_company",
|
|
370
|
+
delivery: "clean",
|
|
371
|
+
sessionId,
|
|
372
|
+
clientOperationId: obs.clientOperationId,
|
|
373
|
+
}),
|
|
374
|
+
exitCode: EXIT_OK,
|
|
375
|
+
diagnostics: [
|
|
376
|
+
...diagnostics,
|
|
377
|
+
"needs_company: slug not in active memberships",
|
|
378
|
+
],
|
|
379
|
+
statePath,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
diagnostics.push(`membership lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Company resolved — resolve project/task (US-007B order).
|
|
388
|
+
const projectResolved = resolveProjectTask({
|
|
389
|
+
sessionId,
|
|
390
|
+
env,
|
|
391
|
+
trusted,
|
|
392
|
+
cwd,
|
|
393
|
+
hqRoot,
|
|
394
|
+
existingState: prior,
|
|
395
|
+
});
|
|
396
|
+
const projectId = projectResolved?.projectId;
|
|
397
|
+
const taskId = projectResolved?.taskId;
|
|
398
|
+
// Trusted / deterministic auto-bind never asks.
|
|
399
|
+
// Transcript-sourced sessions never ask (US-018).
|
|
400
|
+
const fromTranscript = obs.source === "transcript";
|
|
401
|
+
const autoBound = Boolean(projectResolved &&
|
|
402
|
+
projectId &&
|
|
403
|
+
taskId &&
|
|
404
|
+
(projectResolved.source === "trusted_explicit" ||
|
|
405
|
+
projectResolved.source === "existing_binding"));
|
|
406
|
+
const deterministicProjectOnly = Boolean(projectResolved &&
|
|
407
|
+
projectId &&
|
|
408
|
+
!taskId &&
|
|
409
|
+
(projectResolved.source === "deterministic_cwd" ||
|
|
410
|
+
projectResolved.source === "deterministic_registry"));
|
|
411
|
+
let classification;
|
|
412
|
+
if (autoBound) {
|
|
413
|
+
classification = "bound";
|
|
414
|
+
}
|
|
415
|
+
else if (projectId && taskId) {
|
|
416
|
+
classification = "bound";
|
|
417
|
+
}
|
|
418
|
+
else if (projectId || deterministicProjectOnly) {
|
|
419
|
+
classification = "needs_task";
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
// Unresolved until work signals unlock candidates — never invent a project.
|
|
423
|
+
classification = "unresolved";
|
|
424
|
+
}
|
|
425
|
+
const effectiveClassification = classification;
|
|
426
|
+
// Prepare one-time decision from server candidates when threshold crossed
|
|
427
|
+
// and no trusted/deterministic bind applies. Soft evidence never auto-binds.
|
|
428
|
+
// Transcript fallback never asks / never invents a project.
|
|
429
|
+
let decision = prior?.decision;
|
|
430
|
+
const alreadyDecided = Boolean(prior?.decision?.decisionId);
|
|
431
|
+
const askReady = !fromTranscript &&
|
|
432
|
+
!autoBound &&
|
|
433
|
+
!alreadyDecided &&
|
|
434
|
+
(effectiveClassification === "unresolved" ||
|
|
435
|
+
effectiveClassification === "needs_task") &&
|
|
436
|
+
shouldAskAfter({
|
|
437
|
+
toolWrites,
|
|
438
|
+
startedAt,
|
|
439
|
+
hasTurn,
|
|
440
|
+
now: nowFn(),
|
|
441
|
+
});
|
|
442
|
+
if (askReady &&
|
|
443
|
+
resolvedCompany.uid &&
|
|
444
|
+
!deps.offline &&
|
|
445
|
+
deps.fetchCandidates) {
|
|
446
|
+
try {
|
|
447
|
+
const candidates = await deps.fetchCandidates({
|
|
448
|
+
sessionId,
|
|
449
|
+
companyUid: resolvedCompany.uid,
|
|
450
|
+
});
|
|
451
|
+
decision = decisionFromCandidates(sessionId, candidates);
|
|
452
|
+
diagnostics.push(`askAfter prepared decisionId=${decision.decisionId} options=${decision.options.length}`);
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
diagnostics.push(`candidates fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
else if (askReady &&
|
|
459
|
+
(deps.offline || !deps.fetchCandidates || !resolvedCompany.uid)) {
|
|
460
|
+
diagnostics.push("work-signal threshold crossed; candidates unavailable — staying unresolved (no invented project)");
|
|
461
|
+
}
|
|
462
|
+
else if (!autoBound &&
|
|
463
|
+
!alreadyDecided &&
|
|
464
|
+
effectiveClassification === "unresolved") {
|
|
465
|
+
diagnostics.push("below work-signal threshold; unresolved silently");
|
|
466
|
+
}
|
|
467
|
+
// When we have a pending ask, surface as needs_project for visibility.
|
|
468
|
+
let registerClassification = effectiveClassification;
|
|
469
|
+
if (decision?.askAfter &&
|
|
470
|
+
registerClassification === "unresolved") {
|
|
471
|
+
registerClassification = "needs_project";
|
|
472
|
+
}
|
|
473
|
+
let outboxOp;
|
|
474
|
+
try {
|
|
475
|
+
outboxOp = enqueueOutbox({
|
|
476
|
+
clientOperationId: obs.clientOperationId,
|
|
477
|
+
sessionId,
|
|
478
|
+
companyUid: resolvedCompany.uid,
|
|
479
|
+
companySlug: resolvedCompany.slug,
|
|
480
|
+
projectId,
|
|
481
|
+
taskId,
|
|
482
|
+
kind: "register",
|
|
483
|
+
now: nowFn,
|
|
484
|
+
}, deps.root);
|
|
485
|
+
}
|
|
486
|
+
catch (err) {
|
|
487
|
+
if (err instanceof NotTrackingError) {
|
|
488
|
+
return {
|
|
489
|
+
result: resultOf({
|
|
490
|
+
kind: "error",
|
|
491
|
+
classification: "unresolved",
|
|
492
|
+
delivery: "clean",
|
|
493
|
+
sessionId,
|
|
494
|
+
clientOperationId: obs.clientOperationId,
|
|
495
|
+
errorCode: "NOT_TRACKING",
|
|
496
|
+
}),
|
|
497
|
+
exitCode: EXIT_NOT_TRACKING,
|
|
498
|
+
diagnostics: [err.message],
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
throw err;
|
|
502
|
+
}
|
|
503
|
+
diagnostics.push(`outbox queued ${outboxOp.operationId}`);
|
|
504
|
+
if (projectResolved) {
|
|
505
|
+
diagnostics.push(`project source=${projectResolved.source} projectId=${projectId ?? "-"} taskId=${taskId ?? "-"}`);
|
|
506
|
+
}
|
|
507
|
+
let statePath;
|
|
508
|
+
try {
|
|
509
|
+
const stateWrite = mergeLocalSessionFields({
|
|
510
|
+
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
511
|
+
sessionId,
|
|
512
|
+
companyUid: resolvedCompany.uid,
|
|
513
|
+
companySlug: resolvedCompany.slug,
|
|
514
|
+
contextStatus: registerClassification,
|
|
515
|
+
projectId,
|
|
516
|
+
taskId,
|
|
517
|
+
updatedAt: nowIso,
|
|
518
|
+
startedAt,
|
|
519
|
+
...(typeof toolWrites === "number" ? { toolWrites } : {}),
|
|
520
|
+
...(hasTurn ? { hasTurn: true } : {}),
|
|
521
|
+
...(decision ? { decision } : {}),
|
|
522
|
+
...transcriptFields,
|
|
523
|
+
}, prior);
|
|
524
|
+
statePath = writeSessionState(stateWrite, deps.root);
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
if (err instanceof NotTrackingError) {
|
|
528
|
+
return {
|
|
529
|
+
result: resultOf({
|
|
530
|
+
kind: "error",
|
|
531
|
+
classification: registerClassification,
|
|
532
|
+
delivery: "queued",
|
|
533
|
+
sessionId,
|
|
534
|
+
clientOperationId: obs.clientOperationId,
|
|
535
|
+
companyUid: resolvedCompany.uid,
|
|
536
|
+
projectId,
|
|
537
|
+
taskId,
|
|
538
|
+
errorCode: "NOT_TRACKING",
|
|
539
|
+
}),
|
|
540
|
+
exitCode: EXIT_NOT_TRACKING,
|
|
541
|
+
diagnostics: [err.message],
|
|
542
|
+
outboxOperationId: outboxOp.operationId,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
throw err;
|
|
546
|
+
}
|
|
547
|
+
if (deps.offline || !deps.deliver) {
|
|
548
|
+
return {
|
|
549
|
+
result: resultOf({
|
|
550
|
+
kind: "queued",
|
|
551
|
+
classification: registerClassification,
|
|
552
|
+
delivery: "queued",
|
|
553
|
+
sessionId,
|
|
554
|
+
clientOperationId: obs.clientOperationId,
|
|
555
|
+
companyUid: resolvedCompany.uid,
|
|
556
|
+
projectId,
|
|
557
|
+
taskId,
|
|
558
|
+
}),
|
|
559
|
+
exitCode: EXIT_OK,
|
|
560
|
+
diagnostics: [...diagnostics, "offline/local: operation remains queued"],
|
|
561
|
+
statePath,
|
|
562
|
+
outboxOperationId: outboxOp.operationId,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
const delivered = await deps.deliver(outboxOp);
|
|
566
|
+
if (delivered.ok) {
|
|
567
|
+
markOutboxAcked(outboxOp.operationId, deps.root, delivered.receiptId, nowFn);
|
|
568
|
+
// Persist binding episode on success for bound registrations.
|
|
569
|
+
if (registerClassification === "bound") {
|
|
570
|
+
writeSessionState(mergeLocalSessionFields({
|
|
571
|
+
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
572
|
+
sessionId,
|
|
573
|
+
companyUid: resolvedCompany.uid,
|
|
574
|
+
companySlug: resolvedCompany.slug,
|
|
575
|
+
contextStatus: "bound",
|
|
576
|
+
projectId,
|
|
577
|
+
taskId,
|
|
578
|
+
bindingEpisodeId: delivered.receiptId,
|
|
579
|
+
updatedAt: nowFn().toISOString(),
|
|
580
|
+
startedAt,
|
|
581
|
+
...(typeof toolWrites === "number" ? { toolWrites } : {}),
|
|
582
|
+
...(hasTurn ? { hasTurn: true } : {}),
|
|
583
|
+
...transcriptFields,
|
|
584
|
+
}, prior), deps.root);
|
|
585
|
+
}
|
|
586
|
+
const kind = registerClassification === "bound"
|
|
587
|
+
? "bound"
|
|
588
|
+
: registerClassification === "needs_task"
|
|
589
|
+
? "needs_task"
|
|
590
|
+
: registerClassification === "needs_project"
|
|
591
|
+
? "needs_project"
|
|
592
|
+
: "queued";
|
|
593
|
+
return {
|
|
594
|
+
result: resultOf({
|
|
595
|
+
kind: kind === "queued" ? "queued" : kind,
|
|
596
|
+
classification: registerClassification,
|
|
597
|
+
delivery: "acked",
|
|
598
|
+
sessionId,
|
|
599
|
+
clientOperationId: obs.clientOperationId,
|
|
600
|
+
companyUid: resolvedCompany.uid,
|
|
601
|
+
projectId,
|
|
602
|
+
taskId,
|
|
603
|
+
bindingEpisodeId: delivered.receiptId,
|
|
604
|
+
}),
|
|
605
|
+
exitCode: EXIT_OK,
|
|
606
|
+
diagnostics: [...diagnostics, `acked receipt=${delivered.receiptId}`],
|
|
607
|
+
statePath,
|
|
608
|
+
outboxOperationId: outboxOp.operationId,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
if (delivered.retryable) {
|
|
612
|
+
markOutboxQueued(outboxOp.operationId, deps.root, delivered.code, nowFn);
|
|
613
|
+
return {
|
|
614
|
+
result: resultOf({
|
|
615
|
+
kind: "queued",
|
|
616
|
+
classification: registerClassification,
|
|
617
|
+
delivery: "queued",
|
|
618
|
+
sessionId,
|
|
619
|
+
clientOperationId: obs.clientOperationId,
|
|
620
|
+
companyUid: resolvedCompany.uid,
|
|
621
|
+
projectId,
|
|
622
|
+
taskId,
|
|
623
|
+
errorCode: delivered.code,
|
|
624
|
+
}),
|
|
625
|
+
exitCode: EXIT_OK,
|
|
626
|
+
diagnostics: [...diagnostics, `transient ${delivered.code}; remains queued`],
|
|
627
|
+
statePath,
|
|
628
|
+
outboxOperationId: outboxOp.operationId,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
markOutboxQuarantined(outboxOp.operationId, deps.root, delivered.code, nowFn);
|
|
632
|
+
return {
|
|
633
|
+
result: resultOf({
|
|
634
|
+
kind: "error",
|
|
635
|
+
classification: registerClassification,
|
|
636
|
+
delivery: "quarantined",
|
|
637
|
+
sessionId,
|
|
638
|
+
clientOperationId: obs.clientOperationId,
|
|
639
|
+
companyUid: resolvedCompany.uid,
|
|
640
|
+
projectId,
|
|
641
|
+
taskId,
|
|
642
|
+
errorCode: delivered.code,
|
|
643
|
+
}),
|
|
644
|
+
exitCode: EXIT_OK,
|
|
645
|
+
diagnostics: [...diagnostics, `quarantined: ${delivered.code}`],
|
|
646
|
+
statePath,
|
|
647
|
+
outboxOperationId: outboxOp.operationId,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
export { replayOutbox };
|
|
651
|
+
//# sourceMappingURL=reconcile.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic company ownership from the enclosing git remote (US-007A).
|
|
3
|
+
*
|
|
4
|
+
* Walks up from cwd to find a .git dir/file, reads origin's URL from .git/config
|
|
5
|
+
* (no git subprocess), normalises to owner/name, and matches against
|
|
6
|
+
* companies/manifest.yaml repos. Zero or multiple matches → no signal.
|
|
7
|
+
*/
|
|
8
|
+
export interface CompaniesManifestEntry {
|
|
9
|
+
repos?: unknown;
|
|
10
|
+
[k: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
export type CompaniesManifestMap = Record<string, CompaniesManifestEntry | null | undefined>;
|
|
13
|
+
/**
|
|
14
|
+
* Normalise a git remote URL to `owner/name`.
|
|
15
|
+
* Handles https://github.com/owner/name(.git), git@github.com:owner/name(.git),
|
|
16
|
+
* and ssh://git@github.com/owner/name(.git). Returns null when unrecognised.
|
|
17
|
+
*/
|
|
18
|
+
export declare function normalizeRemoteOwnerName(remoteUrl: string): string | null;
|
|
19
|
+
/** Walk up from startDir looking for a `.git` directory or file. */
|
|
20
|
+
export declare function findEnclosingGitDir(startDir: string): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Read `remote.origin.url` from a `.git/config` (ini-style). No subprocess.
|
|
23
|
+
*/
|
|
24
|
+
export declare function readOriginRemoteUrl(gitDir: string): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Match normalised `owner/name` against companies/manifest repos.
|
|
27
|
+
* A repo entry matches if it equals owner/name (case-insensitive) or is a URL
|
|
28
|
+
* that normalises to the same owner/name. Exactly one company → its slug;
|
|
29
|
+
* zero or more than one → null.
|
|
30
|
+
*/
|
|
31
|
+
export declare function matchCompanySlugForRepo(ownerName: string, companies: CompaniesManifestMap): string | null;
|
|
32
|
+
export declare function readCompaniesManifestMap(hqRoot: string): CompaniesManifestMap | null;
|
|
33
|
+
/**
|
|
34
|
+
* Derive the deterministic remote-owner company slug from cwd + HQ manifest.
|
|
35
|
+
* Returns null when there is no unique match.
|
|
36
|
+
*/
|
|
37
|
+
export declare function deriveRemoteOwnerSlug(opts: {
|
|
38
|
+
cwd?: string;
|
|
39
|
+
hqRoot?: string;
|
|
40
|
+
/** Injectable manifest reader (tests). */
|
|
41
|
+
readManifest?: (hqRoot: string) => CompaniesManifestMap | null;
|
|
42
|
+
}): string | null;
|
|
43
|
+
//# sourceMappingURL=repo-remote.d.ts.map
|