@indigoai-us/hq-cli 5.201.3 → 5.202.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/commands/core-ontology.js +12 -4
- package/dist/commands/lanes.js +110 -16
- package/dist/lib/checkpoint-ontology/apply.d.ts +3 -0
- package/dist/lib/checkpoint-ontology/apply.js +5 -0
- package/dist/lib/checkpoint-ontology/company-brief.d.ts +18 -0
- package/dist/lib/checkpoint-ontology/company-brief.js +337 -0
- package/dist/lib/checkpoint-ontology/plan.d.ts +9 -0
- package/dist/lib/checkpoint-ontology/plan.js +16 -2
- package/dist/lib/lanes/claude-account-balance.d.ts +1 -0
- package/dist/lib/lanes/claude-account-balance.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.202.0] — 2026-09-26
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Behind the off-by-default checkpoint.ontology.company-brief flag, the checkpoint ontology plan writes company-brief.md from the local entities, signals and source index, with no model call; recent signals are dated through their source.
|
|
10
|
+
|
|
11
|
+
## [5.201.4] — 2026-09-25
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Claude account balancing on lane resume now respects the attached worker
|
|
16
|
+
profile's account pin, skips accounts pinned to a different model, and treats
|
|
17
|
+
`--account` naming the current account as a keep request for message/resume.
|
|
18
|
+
|
|
5
19
|
## [5.201.3] — 2026-09-25
|
|
6
20
|
|
|
7
21
|
### Fixed
|
|
@@ -44,6 +44,11 @@ function requireRunDir(runDir) {
|
|
|
44
44
|
export const registryFlagReader = (flagKey, company) => readLanesRegistryFlag(flagKey, {
|
|
45
45
|
company: company.cloudUid ? { slug: company.slug, uid: company.cloudUid } : null,
|
|
46
46
|
});
|
|
47
|
+
function companyBriefSuffix(brief) {
|
|
48
|
+
if (!brief)
|
|
49
|
+
return "";
|
|
50
|
+
return `; company brief ${brief.status}${brief.reason ? ` (${brief.reason})` : ""}`;
|
|
51
|
+
}
|
|
47
52
|
export async function runOntologyPlan(hqRoot, runDirOption, deps = {}) {
|
|
48
53
|
const runDir = requireRunDir(runDirOption);
|
|
49
54
|
const outcome = await planOntologyRun({
|
|
@@ -53,11 +58,13 @@ export async function runOntologyPlan(hqRoot, runDirOption, deps = {}) {
|
|
|
53
58
|
readFlag: deps.readFlag ?? registryFlagReader,
|
|
54
59
|
now: deps.now?.(),
|
|
55
60
|
});
|
|
56
|
-
if (outcome.status === "skipped")
|
|
57
|
-
return `ontology: skipped (${outcome.reason})`;
|
|
61
|
+
if (outcome.status === "skipped") {
|
|
62
|
+
return `ontology: skipped (${outcome.reason})${companyBriefSuffix(outcome.companyBrief)}`;
|
|
63
|
+
}
|
|
58
64
|
const { plan } = outcome;
|
|
59
65
|
return (`ontology: planned ${plan.inputs.length} input(s), ~${plan.budget.planned_tokens} tokens, ` +
|
|
60
|
-
`${plan.pending_after_run} left for later runs; plan at ${outcome.planPath}`
|
|
66
|
+
`${plan.pending_after_run} left for later runs; plan at ${outcome.planPath}` +
|
|
67
|
+
companyBriefSuffix(outcome.companyBrief));
|
|
61
68
|
}
|
|
62
69
|
export function runOntologyApply(hqRoot, runDirOption, deps = {}) {
|
|
63
70
|
const runDir = requireRunDir(runDirOption);
|
|
@@ -70,7 +77,8 @@ export function runOntologyApply(hqRoot, runDirOption, deps = {}) {
|
|
|
70
77
|
return `ontology: ${report.status.replaceAll("_", " ")}; nothing written`;
|
|
71
78
|
return (`ontology: ${report.created.length} created, ${report.updated.length} updated, ` +
|
|
72
79
|
`${report.matched} matched, ${report.rejected.length} rejected, ${report.deferred.length} deferred, ` +
|
|
73
|
-
`${report.suspect_duplicates.length} suspect duplicate(s), ${report.write_errors.length} write error(s)`
|
|
80
|
+
`${report.suspect_duplicates.length} suspect duplicate(s), ${report.write_errors.length} write error(s)` +
|
|
81
|
+
companyBriefSuffix(report.company_brief));
|
|
74
82
|
}
|
|
75
83
|
function fail(error) {
|
|
76
84
|
if (error instanceof OntologyUsageError || error instanceof ApplyError || error instanceof WriteBoundsError) {
|
package/dist/commands/lanes.js
CHANGED
|
@@ -4905,8 +4905,26 @@ function applyResumeProfileAndEffort(current, next, input) {
|
|
|
4905
4905
|
}
|
|
4906
4906
|
}
|
|
4907
4907
|
function recordResumeSetup(ctx, lane, input) {
|
|
4908
|
+
if (input.cliModel !== undefined) {
|
|
4909
|
+
const model = input.cliModel.trim();
|
|
4910
|
+
if (!model)
|
|
4911
|
+
throw new LanesError("empty_model", "--model is empty or whitespace-only", { value: input.cliModel });
|
|
4912
|
+
const config = loadLanesConfig(ctx.hqRoot());
|
|
4913
|
+
const account = lane.account === undefined
|
|
4914
|
+
? undefined
|
|
4915
|
+
: config.providers[lane.provider].accounts[lane.account];
|
|
4916
|
+
if (account === undefined) {
|
|
4917
|
+
throw new AccountResolutionError("unknown_account", `Lane ${lane.lane_id} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`, { provider: lane.provider, account: lane.account ?? "", enabled: [] });
|
|
4918
|
+
}
|
|
4919
|
+
validateLaneModel(lane.provider, model, account.home, {
|
|
4920
|
+
readFileSync: ctx.readFileSync,
|
|
4921
|
+
probe: ctx.probeProvider,
|
|
4922
|
+
env: ctx.env,
|
|
4923
|
+
});
|
|
4924
|
+
}
|
|
4908
4925
|
if (input.profile === undefined &&
|
|
4909
4926
|
input.effort === undefined &&
|
|
4927
|
+
input.cliModel === undefined &&
|
|
4910
4928
|
input.meshCompany === undefined)
|
|
4911
4929
|
return lane;
|
|
4912
4930
|
return casUpdateLane(ctx.hqRoot(), lane.lane_id, (current) => {
|
|
@@ -4915,6 +4933,10 @@ function recordResumeSetup(ctx, lane, input) {
|
|
|
4915
4933
|
}
|
|
4916
4934
|
const next = { ...current };
|
|
4917
4935
|
applyResumeProfileAndEffort(current, next, input);
|
|
4936
|
+
if (input.cliModel !== undefined) {
|
|
4937
|
+
next.model = input.cliModel.trim();
|
|
4938
|
+
next.model_source = "cli";
|
|
4939
|
+
}
|
|
4918
4940
|
if (input.meshCompany !== undefined) {
|
|
4919
4941
|
if (current.company === null)
|
|
4920
4942
|
next.mesh_company = input.meshCompany;
|
|
@@ -5147,7 +5169,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5147
5169
|
return { lane: moved, from, to: target, copy, queued };
|
|
5148
5170
|
}, ctx.sleep);
|
|
5149
5171
|
}
|
|
5150
|
-
async function selectAutomaticClaudeResumeAccount(ctx, lane) {
|
|
5172
|
+
async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
|
|
5151
5173
|
if (lane.provider !== "claude")
|
|
5152
5174
|
return undefined;
|
|
5153
5175
|
if (!await ctx.laneClaudeAccountBalanceEnabled(lane.company)) {
|
|
@@ -5171,6 +5193,16 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane) {
|
|
|
5171
5193
|
}
|
|
5172
5194
|
const hqRoot = ctx.hqRoot();
|
|
5173
5195
|
const config = loadLanesConfig(hqRoot);
|
|
5196
|
+
const profile = workerProfile?.profile ?? (lane.worker_profile !== undefined
|
|
5197
|
+
? loadWorkerProfile(hqRoot, lane.worker_profile)
|
|
5198
|
+
: undefined);
|
|
5199
|
+
const profileDefaultsApply = profile !== undefined && profile.meta.provider === lane.provider;
|
|
5200
|
+
const profileAccount = profileDefaultsApply ? profile?.meta.account : undefined;
|
|
5201
|
+
const profileAccounts = profileDefaultsApply ? profile?.meta.accounts : undefined;
|
|
5202
|
+
const profilePin = profileAccounts ?? (profileAccount === undefined ? undefined : [profileAccount]);
|
|
5203
|
+
const allowedAccounts = profilePin !== undefined && lane.admission_accounts !== undefined
|
|
5204
|
+
? profilePin.filter((account) => lane.admission_accounts.includes(account))
|
|
5205
|
+
: profilePin ?? lane.admission_accounts;
|
|
5174
5206
|
const currentConfig = lane.account === undefined
|
|
5175
5207
|
? undefined
|
|
5176
5208
|
: config.providers.claude.accounts[lane.account];
|
|
@@ -5181,20 +5213,71 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane) {
|
|
|
5181
5213
|
probe: ctx.probeProvider,
|
|
5182
5214
|
env: ctx.env,
|
|
5183
5215
|
}).logged_in;
|
|
5216
|
+
const pinnedModel = workerProfile !== undefined
|
|
5217
|
+
? lane.model_source === "cli" ? lane.model : undefined
|
|
5218
|
+
: lane.model_source === undefined || lane.model_source === "cli"
|
|
5219
|
+
? lane.model
|
|
5220
|
+
: undefined;
|
|
5221
|
+
const model = resolveLaneModel({
|
|
5222
|
+
cliModel: pinnedModel,
|
|
5223
|
+
profileModel: profileDefaultsApply ? profile?.meta.model : undefined,
|
|
5224
|
+
accountModel: lane.account === undefined ? undefined : config.providers.claude.accounts[lane.account]?.model,
|
|
5225
|
+
providerModel: config.providers.claude.model,
|
|
5226
|
+
});
|
|
5184
5227
|
const accountNeedsRepair = currentConfig?.enabled !== true || currentLogin === false;
|
|
5185
5228
|
const mayRebalance = BALANCE_MOVABLE_STATES.includes(lane.state);
|
|
5186
5229
|
if (!accountNeedsRepair && !mayRebalance)
|
|
5187
5230
|
return undefined;
|
|
5188
5231
|
const balanceHosts = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config);
|
|
5232
|
+
const targetHost = balanceHosts.snapshots.find((host) => host.capacity.host.kind === lane.host.kind && host.capacity.host.id === lane.host.id);
|
|
5233
|
+
const currentTargetAccount = lane.account === undefined
|
|
5234
|
+
? undefined
|
|
5235
|
+
: targetHost?.capacity.providers.claude.accounts[lane.account];
|
|
5236
|
+
if (profilePin !== undefined &&
|
|
5237
|
+
lane.account !== undefined &&
|
|
5238
|
+
allowedAccounts?.includes(lane.account) &&
|
|
5239
|
+
currentConfig?.enabled === true &&
|
|
5240
|
+
currentTargetAccount?.enabled === true &&
|
|
5241
|
+
currentTargetAccount.logged_in === true &&
|
|
5242
|
+
(model === undefined || currentConfig.model === undefined || currentConfig.model === model))
|
|
5243
|
+
return undefined;
|
|
5189
5244
|
const selected = selectClaudeAccount({
|
|
5190
5245
|
hosts: balanceHosts.snapshots,
|
|
5191
5246
|
targetHost: lane.host,
|
|
5192
5247
|
config,
|
|
5193
|
-
...(
|
|
5248
|
+
...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
|
|
5249
|
+
...(model !== undefined ? { model } : {}),
|
|
5194
5250
|
unavailableHosts: balanceHosts.unavailableHosts,
|
|
5195
5251
|
});
|
|
5196
5252
|
return { ...selected, selected_at: ctx.now().toISOString() };
|
|
5197
5253
|
}
|
|
5254
|
+
function accountRequestKeepsCurrentLane(lane, rawAccount) {
|
|
5255
|
+
const raw = rawAccount.trim();
|
|
5256
|
+
const colon = raw.indexOf(":");
|
|
5257
|
+
const requestedProvider = colon > 0 ? raw.slice(0, colon) : undefined;
|
|
5258
|
+
const requestedAccount = colon > 0 ? raw.slice(colon + 1) : raw;
|
|
5259
|
+
return requestedAccount.length > 0 &&
|
|
5260
|
+
requestedAccount === lane.account &&
|
|
5261
|
+
(requestedProvider === undefined || requestedProvider === lane.provider);
|
|
5262
|
+
}
|
|
5263
|
+
function assertCurrentResumeAccountAvailable(ctx, lane) {
|
|
5264
|
+
const config = loadLanesConfig(ctx.hqRoot());
|
|
5265
|
+
const account = lane.account === undefined
|
|
5266
|
+
? undefined
|
|
5267
|
+
: config.providers[lane.provider].accounts[lane.account];
|
|
5268
|
+
if (account === undefined || !account.enabled) {
|
|
5269
|
+
throw new AccountResolutionError(account === undefined ? "unknown_account" : "account_disabled", account === undefined
|
|
5270
|
+
? `Lane ${lane.lane_id} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`
|
|
5271
|
+
: `Account ${JSON.stringify(lane.account)} is disabled${account.disabled_reason ? `: ${account.disabled_reason}` : ""}`, { provider: lane.provider, account: lane.account ?? "", ...(account?.disabled_reason !== undefined ? { reason: account.disabled_reason } : {}) });
|
|
5272
|
+
}
|
|
5273
|
+
if (accountStatus(lane.provider, lane.account, config, {
|
|
5274
|
+
existsSync: ctx.existsSync,
|
|
5275
|
+
probe: ctx.probeProvider,
|
|
5276
|
+
env: ctx.env,
|
|
5277
|
+
}).logged_in !== true) {
|
|
5278
|
+
throw new LanesError("account_not_logged_in", `Account ${lane.account} for ${lane.provider} is not logged in (no credential file)`, { provider: lane.provider, account: lane.account, lane_id: lane.lane_id });
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
5198
5281
|
async function runMessage(ctx, laneId, opts) {
|
|
5199
5282
|
const hqRoot = ctx.hqRoot();
|
|
5200
5283
|
const text = opts.text;
|
|
@@ -5215,16 +5298,19 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5215
5298
|
// The final state transition repeats this check under the senior lock.
|
|
5216
5299
|
await withResumeHireAdmission(ctx, lane, () => undefined);
|
|
5217
5300
|
lane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5301
|
+
const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
|
|
5302
|
+
if (keepCurrentAccount)
|
|
5303
|
+
assertCurrentResumeAccountAvailable(ctx, lane);
|
|
5304
|
+
// A real account move is refused on its own terms first: a running lane and
|
|
5305
|
+
// a provider without session resume get the move's named errors, not the
|
|
5306
|
+
// generic delivery refusal below. Naming the current account is a keep.
|
|
5307
|
+
if (opts.account !== undefined && !keepCurrentAccount && lane.state === "running") {
|
|
5222
5308
|
throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
|
|
5223
5309
|
lane_id: laneId,
|
|
5224
5310
|
state: lane.state,
|
|
5225
5311
|
});
|
|
5226
5312
|
}
|
|
5227
|
-
if (opts.account !== undefined && !providerSupportsSessionResume(lane.provider)) {
|
|
5313
|
+
if (opts.account !== undefined && !keepCurrentAccount && !providerSupportsSessionResume(lane.provider)) {
|
|
5228
5314
|
throw new LanesError("provider_session_not_portable", `Provider ${lane.provider} has no session resume, so a lane cannot move between its accounts`, { lane_id: laneId, provider: lane.provider });
|
|
5229
5315
|
}
|
|
5230
5316
|
// One decision point for how a follow-up reaches this lane (delivery.ts).
|
|
@@ -5239,6 +5325,9 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5239
5325
|
message,
|
|
5240
5326
|
});
|
|
5241
5327
|
}
|
|
5328
|
+
if (keepCurrentAccount && opts.model !== undefined && mechanism !== "resume") {
|
|
5329
|
+
throw new LanesError("model_requires_resume", "--model with --account requires a message that resumes the lane", { lane_id: laneId, state: lane.state });
|
|
5330
|
+
}
|
|
5242
5331
|
if (opts.worker !== undefined && mechanism === "dropbox") {
|
|
5243
5332
|
throw new LanesError("worker_profile_requires_resume", `Lane ${laneId} is still running; --worker can only be applied when message starts a resumed provider turn`, { lane_id: laneId, state: lane.state });
|
|
5244
5333
|
}
|
|
@@ -5252,12 +5341,12 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5252
5341
|
throw new LanesError("model_requires_account", "--model on hq lanes message is only accepted together with --account", {});
|
|
5253
5342
|
}
|
|
5254
5343
|
const automaticAccountSelection = opts.account === undefined && mechanism === "resume"
|
|
5255
|
-
? await selectAutomaticClaudeResumeAccount(ctx, lane)
|
|
5344
|
+
? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile)
|
|
5256
5345
|
: undefined;
|
|
5257
5346
|
const resumeEffort = resolveResumeEffort(ctx, lane, {
|
|
5258
5347
|
cliEffort,
|
|
5259
5348
|
profile: resumeProfile,
|
|
5260
|
-
account: opts.account ?? automaticAccountSelection?.account,
|
|
5349
|
+
account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.account,
|
|
5261
5350
|
model: opts.model,
|
|
5262
5351
|
});
|
|
5263
5352
|
const messageId = mintMessageId();
|
|
@@ -5280,7 +5369,7 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5280
5369
|
let resumed = null;
|
|
5281
5370
|
let move = null;
|
|
5282
5371
|
let queuedOnAdmission = false;
|
|
5283
|
-
if (opts.account !== undefined) {
|
|
5372
|
+
if (opts.account !== undefined && !keepCurrentAccount) {
|
|
5284
5373
|
// A moved message is always a resume on the target account: a running
|
|
5285
5374
|
// lane (the dropbox mechanism) is refused inside prepareAccountMove.
|
|
5286
5375
|
move = await prepareAccountMove(ctx, lane, {
|
|
@@ -5321,13 +5410,14 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5321
5410
|
}
|
|
5322
5411
|
}
|
|
5323
5412
|
}
|
|
5324
|
-
if (resumeProfile !== undefined && opts.account === undefined) {
|
|
5413
|
+
if (resumeProfile !== undefined && (opts.account === undefined || keepCurrentAccount)) {
|
|
5325
5414
|
validateResumeProfileModel(ctx, lane, resumeProfile.profile);
|
|
5326
5415
|
}
|
|
5327
|
-
if (move === null && (resumeProfile !== undefined || resumeEffort !== undefined)) {
|
|
5416
|
+
if (move === null && (resumeProfile !== undefined || resumeEffort !== undefined || (keepCurrentAccount && opts.model !== undefined))) {
|
|
5328
5417
|
lane = recordResumeSetup(ctx, lane, {
|
|
5329
5418
|
...(resumeProfile !== undefined ? { profile: resumeProfile } : {}),
|
|
5330
5419
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5420
|
+
...(keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
|
|
5331
5421
|
});
|
|
5332
5422
|
}
|
|
5333
5423
|
if (move?.queued) {
|
|
@@ -5751,20 +5841,23 @@ async function runResume(ctx, laneId, opts) {
|
|
|
5751
5841
|
const resumeProfile = opts.worker !== undefined
|
|
5752
5842
|
? resolveResumeProfile(ctx, lane, opts.worker)
|
|
5753
5843
|
: undefined;
|
|
5754
|
-
|
|
5844
|
+
const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
|
|
5845
|
+
if (keepCurrentAccount)
|
|
5846
|
+
assertCurrentResumeAccountAvailable(ctx, lane);
|
|
5847
|
+
if (resumeProfile !== undefined && (opts.account === undefined || keepCurrentAccount)) {
|
|
5755
5848
|
validateResumeProfileModel(ctx, lane, resumeProfile.profile);
|
|
5756
5849
|
}
|
|
5757
5850
|
const automaticAccountSelection = opts.account === undefined
|
|
5758
|
-
? await selectAutomaticClaudeResumeAccount(ctx, lane)
|
|
5851
|
+
? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile)
|
|
5759
5852
|
: undefined;
|
|
5760
5853
|
const resumeEffort = resolveResumeEffort(ctx, lane, {
|
|
5761
5854
|
cliEffort,
|
|
5762
5855
|
profile: resumeProfile,
|
|
5763
|
-
account: opts.account ?? automaticAccountSelection?.account,
|
|
5856
|
+
account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.account,
|
|
5764
5857
|
model: opts.model,
|
|
5765
5858
|
});
|
|
5766
5859
|
let move = null;
|
|
5767
|
-
if (opts.account !== undefined) {
|
|
5860
|
+
if (opts.account !== undefined && !keepCurrentAccount) {
|
|
5768
5861
|
move = await prepareAccountMove(ctx, lane, {
|
|
5769
5862
|
account: opts.account,
|
|
5770
5863
|
cliModel: opts.model?.trim() || undefined,
|
|
@@ -5805,6 +5898,7 @@ async function runResume(ctx, laneId, opts) {
|
|
|
5805
5898
|
lane = recordResumeSetup(ctx, lane, {
|
|
5806
5899
|
...(move === null && resumeProfile !== undefined ? { profile: resumeProfile } : {}),
|
|
5807
5900
|
...(move === null && resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5901
|
+
...(move === null && keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
|
|
5808
5902
|
...(requestedMeshCompany !== undefined
|
|
5809
5903
|
? { meshCompany: requestedMeshCompany, threadId: meshThreadId }
|
|
5810
5904
|
: {}),
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* sections 4.3, 5 and 14). The model output is untrusted input: a malformed
|
|
6
6
|
* candidate is rejected and counted, never repaired.
|
|
7
7
|
*/
|
|
8
|
+
import { type CompanyBriefResult } from "./company-brief.js";
|
|
8
9
|
import { type WritableEntityType } from "./identity.js";
|
|
9
10
|
import { type EntityIndex } from "./tree.js";
|
|
10
11
|
export declare const CAPS: {
|
|
@@ -73,6 +74,8 @@ export interface ApplyReport {
|
|
|
73
74
|
path: string;
|
|
74
75
|
reason: string;
|
|
75
76
|
}>;
|
|
77
|
+
/** Present only when the plan ran the company brief and this apply created or updated entities. */
|
|
78
|
+
company_brief?: CompanyBriefResult;
|
|
76
79
|
duration_ms: number;
|
|
77
80
|
}
|
|
78
81
|
export interface ApplyOptions {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import * as path from "node:path";
|
|
11
|
+
import { writeCompanyBrief } from "./company-brief.js";
|
|
11
12
|
import { entitiesRoot, resolveOntologyCompany, safeWriteFile } from "./company.js";
|
|
12
13
|
import { appendBackRefs, parseEntityFile, patchEntityFile, previewSignalContent, renderNewEntity, validateEntityFrontmatter, } from "./entity-file.js";
|
|
13
14
|
import { canonicalizeName, entityId, isRejectedName, isWritableEntityType, normalizeEntityName, squash, toSlug, } from "./identity.js";
|
|
@@ -438,6 +439,10 @@ export function applyOntologyRun(options) {
|
|
|
438
439
|
report.updated.push(rel);
|
|
439
440
|
}
|
|
440
441
|
commitLedger(options.hqRoot, company, plan, inputs, blocked, report);
|
|
442
|
+
// The plan rendered the brief before these entity writes; render it again so it matches them.
|
|
443
|
+
if (plan.company_brief && report.created.length + report.updated.length > 0) {
|
|
444
|
+
report.company_brief = writeCompanyBrief(options.hqRoot, company, options.now ?? new Date());
|
|
445
|
+
}
|
|
441
446
|
return finish();
|
|
442
447
|
}
|
|
443
448
|
let signalRefIndex = null;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic `company-brief.md` for one company.
|
|
3
|
+
*
|
|
4
|
+
* The cloud gardener used to write this file. The checkpoint renders the same
|
|
5
|
+
* shape from the local ontology with no model call. Signal dates come from
|
|
6
|
+
* `sources/_index`, because signal files have no `created_at`.
|
|
7
|
+
*/
|
|
8
|
+
import { type OntologyCompany } from "./company.js";
|
|
9
|
+
export declare const COMPANY_BRIEF_FILE = "company-brief.md";
|
|
10
|
+
export interface CompanyBriefResult {
|
|
11
|
+
status: "written" | "unchanged" | "skipped" | "failed";
|
|
12
|
+
/** Set for skipped and failed. */
|
|
13
|
+
reason?: string;
|
|
14
|
+
}
|
|
15
|
+
/** `null` when fewer than three valid person, project, company, or concept entities exist. */
|
|
16
|
+
export declare function renderCompanyBrief(company: OntologyCompany, now: Date): string | null;
|
|
17
|
+
export declare function writeCompanyBrief(hqRoot: string, company: OntologyCompany, now: Date): CompanyBriefResult;
|
|
18
|
+
//# sourceMappingURL=company-brief.d.ts.map
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic `company-brief.md` for one company.
|
|
3
|
+
*
|
|
4
|
+
* The cloud gardener used to write this file. The checkpoint renders the same
|
|
5
|
+
* shape from the local ontology with no model call. Signal dates come from
|
|
6
|
+
* `sources/_index`, because signal files have no `created_at`.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { safeWriteFile, WriteBoundsError } from "./company.js";
|
|
11
|
+
import { parseEntityFile, validateEntityFrontmatter } from "./entity-file.js";
|
|
12
|
+
export const COMPANY_BRIEF_FILE = "company-brief.md";
|
|
13
|
+
const SIGNAL_TYPES = [
|
|
14
|
+
{ dir: "decision", heading: "Decisions" },
|
|
15
|
+
{ dir: "risk", heading: "Risks" },
|
|
16
|
+
{ dir: "question", heading: "Questions" },
|
|
17
|
+
];
|
|
18
|
+
const ENTITY_TYPES = [
|
|
19
|
+
{ dir: "person", heading: "People" },
|
|
20
|
+
{ dir: "project", heading: "Projects" },
|
|
21
|
+
{ dir: "company", heading: "Companies" },
|
|
22
|
+
{ dir: "concept", heading: "Concepts" },
|
|
23
|
+
];
|
|
24
|
+
const CHANNELS = [
|
|
25
|
+
{ id: "meeting", label: "Meeting" },
|
|
26
|
+
{ id: "email", label: "Email" },
|
|
27
|
+
{ id: "slack", label: "Slack" },
|
|
28
|
+
{ id: "slack-dm", label: "Slack DM" },
|
|
29
|
+
{ id: "linear", label: "Linear" },
|
|
30
|
+
{ id: "notion", label: "Notion" },
|
|
31
|
+
];
|
|
32
|
+
const NO_SOURCES = "<!-- source channels: no sources/_index entries in last 7d -->";
|
|
33
|
+
const SIGNAL_PREVIEW_MAX = 160;
|
|
34
|
+
const SIGNAL_TOP = 5;
|
|
35
|
+
const ENTITY_TOP = 8;
|
|
36
|
+
/** Cloud budget: `floor(2000 * 0.4)` reserved tokens, minus the source block. */
|
|
37
|
+
const SIGNAL_TOKEN_RESERVE = Math.floor(2000 * 0.4);
|
|
38
|
+
const MIN_ENTITIES = 3;
|
|
39
|
+
function byCodeUnit(a, b) {
|
|
40
|
+
if (a < b)
|
|
41
|
+
return -1;
|
|
42
|
+
if (a > b)
|
|
43
|
+
return 1;
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
function tokenCount(text) {
|
|
47
|
+
return Math.ceil(text.length / 4);
|
|
48
|
+
}
|
|
49
|
+
/** UTC dates `now`, `now − 1 day`, … from the epoch day, not the local calendar. */
|
|
50
|
+
function windowDates(now) {
|
|
51
|
+
const day = Math.floor(now.getTime() / 86_400_000);
|
|
52
|
+
const dates = [];
|
|
53
|
+
for (let i = 0; i < 7; i += 1)
|
|
54
|
+
dates.push(new Date((day - i) * 86_400_000).toISOString().slice(0, 10));
|
|
55
|
+
return dates;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* `false` when `dir` does not exist. Throws when it resolves outside the company:
|
|
59
|
+
* a symlinked directory would otherwise copy another company's files into this brief.
|
|
60
|
+
*/
|
|
61
|
+
function readableInside(companyDir, dir) {
|
|
62
|
+
let real;
|
|
63
|
+
try {
|
|
64
|
+
real = fs.realpathSync(dir);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error.code === "ENOENT")
|
|
68
|
+
return false;
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
const relative = path.relative(fs.realpathSync(companyDir), real);
|
|
72
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
73
|
+
throw new WriteBoundsError(`${path.relative(companyDir, dir)} resolves outside the company`);
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
function listDirectMd(companyDir, dir) {
|
|
78
|
+
if (!readableInside(companyDir, dir))
|
|
79
|
+
return [];
|
|
80
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
81
|
+
const names = [];
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
// Files only. A directory whose name starts with `@` or `_` is never entered.
|
|
84
|
+
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
85
|
+
continue;
|
|
86
|
+
names.push(entry.name);
|
|
87
|
+
}
|
|
88
|
+
names.sort(byCodeUnit);
|
|
89
|
+
return names;
|
|
90
|
+
}
|
|
91
|
+
/** `null` when the file is missing, unparsable, or not a JSON array. */
|
|
92
|
+
function readIndexArray(file) {
|
|
93
|
+
let raw;
|
|
94
|
+
try {
|
|
95
|
+
raw = fs.readFileSync(file, "utf8");
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error.code === "ENOENT")
|
|
99
|
+
return null;
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(raw);
|
|
104
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error instanceof SyntaxError)
|
|
108
|
+
return null;
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function indexKey(entry) {
|
|
113
|
+
const keys = entry.keys;
|
|
114
|
+
const markdown = keys && typeof keys === "object" && !Array.isArray(keys)
|
|
115
|
+
? keys.markdown
|
|
116
|
+
: undefined;
|
|
117
|
+
if (typeof markdown === "string" && markdown.length > 0)
|
|
118
|
+
return markdown;
|
|
119
|
+
const sourceRef = entry.source_ref;
|
|
120
|
+
const fallback = sourceRef && typeof sourceRef === "object" && !Array.isArray(sourceRef)
|
|
121
|
+
? sourceRef.key
|
|
122
|
+
: undefined;
|
|
123
|
+
return typeof fallback === "string" && fallback.length > 0 ? fallback : null;
|
|
124
|
+
}
|
|
125
|
+
function mapChannel(value) {
|
|
126
|
+
if (value === "meeting" || value === "meetings")
|
|
127
|
+
return "meeting";
|
|
128
|
+
if (value === "email" || value === "slack" || value === "slack-dm" || value === "linear" || value === "notion") {
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
function loadSources(companyDir, now) {
|
|
134
|
+
const times = new Map();
|
|
135
|
+
const counts = new Map();
|
|
136
|
+
const dir = path.join(companyDir, "sources", "_index");
|
|
137
|
+
// Regular files only: a symlinked index file could point at another company's index.
|
|
138
|
+
const names = readableInside(companyDir, dir)
|
|
139
|
+
? fs
|
|
140
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
141
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
142
|
+
.map((entry) => entry.name)
|
|
143
|
+
.sort(byCodeUnit)
|
|
144
|
+
: [];
|
|
145
|
+
const windowFiles = new Set(windowDates(now).map((date) => `${date}.json`));
|
|
146
|
+
for (const name of names) {
|
|
147
|
+
const entries = readIndexArray(path.join(dir, name));
|
|
148
|
+
if (!entries)
|
|
149
|
+
continue;
|
|
150
|
+
const inWindow = windowFiles.has(name);
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
153
|
+
continue;
|
|
154
|
+
const record = entry;
|
|
155
|
+
const key = indexKey(record);
|
|
156
|
+
if (key && typeof record.written_at === "string")
|
|
157
|
+
times.set(key, record.written_at);
|
|
158
|
+
if (!inWindow)
|
|
159
|
+
continue;
|
|
160
|
+
const channel = mapChannel(record.channel);
|
|
161
|
+
if (channel)
|
|
162
|
+
counts.set(channel, (counts.get(channel) ?? 0) + 1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { times, counts };
|
|
166
|
+
}
|
|
167
|
+
function loadSignals(companyDir, times) {
|
|
168
|
+
const items = [];
|
|
169
|
+
for (const type of SIGNAL_TYPES) {
|
|
170
|
+
const dir = path.join(companyDir, "signals", type.dir);
|
|
171
|
+
for (const name of listDirectMd(companyDir, dir)) {
|
|
172
|
+
const parsed = parseEntityFile(fs.readFileSync(path.join(dir, name), "utf8"));
|
|
173
|
+
if (!parsed)
|
|
174
|
+
continue;
|
|
175
|
+
const content = parsed.frontmatter.canonical_content;
|
|
176
|
+
if (typeof content !== "string" || content.length === 0)
|
|
177
|
+
continue;
|
|
178
|
+
const sourceRef = parsed.frontmatter.source_ref;
|
|
179
|
+
const time = typeof sourceRef === "string" ? times.get(sourceRef) ?? null : null;
|
|
180
|
+
items.push({ type: type.dir, rel: `signals/${type.dir}/${name}`, content, time });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return items;
|
|
184
|
+
}
|
|
185
|
+
function compareSignals(a, b) {
|
|
186
|
+
if (a.time !== null && b.time !== null) {
|
|
187
|
+
if (a.time !== b.time)
|
|
188
|
+
return a.time < b.time ? 1 : -1;
|
|
189
|
+
}
|
|
190
|
+
else if (a.time !== b.time) {
|
|
191
|
+
return a.time === null ? 1 : -1;
|
|
192
|
+
}
|
|
193
|
+
if (a.rel !== b.rel)
|
|
194
|
+
return a.rel < b.rel ? 1 : -1;
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
function previewContent(content) {
|
|
198
|
+
const collapsed = content.replace(/\s+/g, " ").trim();
|
|
199
|
+
if (collapsed.length <= SIGNAL_PREVIEW_MAX)
|
|
200
|
+
return collapsed;
|
|
201
|
+
return `${collapsed.slice(0, SIGNAL_PREVIEW_MAX).trimEnd()}…`;
|
|
202
|
+
}
|
|
203
|
+
function renderSignalBlock(items) {
|
|
204
|
+
const sections = [];
|
|
205
|
+
for (const type of SIGNAL_TYPES) {
|
|
206
|
+
const rows = items.filter((item) => item.type === type.dir);
|
|
207
|
+
if (rows.length === 0)
|
|
208
|
+
continue;
|
|
209
|
+
const lines = rows.map((item) => {
|
|
210
|
+
const base = `- [${previewContent(item.content)}](${item.rel})`;
|
|
211
|
+
return item.time ? `${base} _(${item.time.slice(0, 10)})_` : base;
|
|
212
|
+
});
|
|
213
|
+
sections.push(`### ${type.heading}\n${lines.join("\n")}`);
|
|
214
|
+
}
|
|
215
|
+
if (sections.length === 0)
|
|
216
|
+
return null;
|
|
217
|
+
return ["## Recent Signals", ...sections].join("\n\n").trimEnd();
|
|
218
|
+
}
|
|
219
|
+
function confidenceOf(frontmatter) {
|
|
220
|
+
const value = frontmatter.confidence;
|
|
221
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : 0;
|
|
222
|
+
}
|
|
223
|
+
function compareEntities(a, b) {
|
|
224
|
+
if (a.score !== b.score)
|
|
225
|
+
return b.score - a.score;
|
|
226
|
+
if (a.name !== b.name)
|
|
227
|
+
return a.name < b.name ? -1 : 1;
|
|
228
|
+
if (a.file !== b.file)
|
|
229
|
+
return a.file < b.file ? -1 : 1;
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
function loadEntities(companyDir) {
|
|
233
|
+
let total = 0;
|
|
234
|
+
const groups = [];
|
|
235
|
+
for (const type of ENTITY_TYPES) {
|
|
236
|
+
const dir = path.join(companyDir, "ontology", "entities", type.dir);
|
|
237
|
+
const items = [];
|
|
238
|
+
for (const name of listDirectMd(companyDir, dir)) {
|
|
239
|
+
const parsed = parseEntityFile(fs.readFileSync(path.join(dir, name), "utf8"));
|
|
240
|
+
if (!parsed)
|
|
241
|
+
continue;
|
|
242
|
+
const valid = validateEntityFrontmatter(parsed.frontmatter);
|
|
243
|
+
if (typeof valid === "string")
|
|
244
|
+
continue;
|
|
245
|
+
items.push({
|
|
246
|
+
file: name,
|
|
247
|
+
name: valid.canonical_name,
|
|
248
|
+
signalCount: valid.signal_count,
|
|
249
|
+
score: valid.signal_count * confidenceOf(parsed.frontmatter),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
total += items.length;
|
|
253
|
+
items.sort(compareEntities);
|
|
254
|
+
groups.push({
|
|
255
|
+
heading: type.heading,
|
|
256
|
+
lines: items.slice(0, ENTITY_TOP).map((item) => `- [${item.name}](ontology/entities/${type.dir}/${item.file}) (${item.signalCount} signals)`),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return { total, groups };
|
|
260
|
+
}
|
|
261
|
+
function renderEntityBlock(slug, groups) {
|
|
262
|
+
const parts = [
|
|
263
|
+
`# ${slug} company brief`,
|
|
264
|
+
"_Rendered by hq core checkpoint from the local ontology._",
|
|
265
|
+
"## Key Entities",
|
|
266
|
+
];
|
|
267
|
+
for (const group of groups) {
|
|
268
|
+
if (group.lines.length === 0)
|
|
269
|
+
continue;
|
|
270
|
+
parts.push(`### ${group.heading}\n${group.lines.join("\n")}`);
|
|
271
|
+
}
|
|
272
|
+
return parts.join("\n\n");
|
|
273
|
+
}
|
|
274
|
+
function renderSourceBlock(counts) {
|
|
275
|
+
const total = CHANNELS.reduce((sum, channel) => sum + (counts.get(channel.id) ?? 0), 0);
|
|
276
|
+
if (total === 0)
|
|
277
|
+
return NO_SOURCES;
|
|
278
|
+
const rows = CHANNELS.map((channel) => `| ${channel.label} | ${counts.get(channel.id) ?? 0} |`);
|
|
279
|
+
return ["## Source Channels", "", "_Last 7 days_", "", "| Channel | Count |", "| --- | ---: |", ...rows].join("\n");
|
|
280
|
+
}
|
|
281
|
+
function fitSignals(ranked, budget) {
|
|
282
|
+
const kept = ranked.slice();
|
|
283
|
+
while (kept.length > 0) {
|
|
284
|
+
const block = renderSignalBlock(kept);
|
|
285
|
+
if (block === null || tokenCount(block) <= budget)
|
|
286
|
+
break;
|
|
287
|
+
kept.pop();
|
|
288
|
+
}
|
|
289
|
+
return kept;
|
|
290
|
+
}
|
|
291
|
+
/** `null` when fewer than three valid person, project, company, or concept entities exist. */
|
|
292
|
+
export function renderCompanyBrief(company, now) {
|
|
293
|
+
const entities = loadEntities(company.dir);
|
|
294
|
+
if (entities.total < MIN_ENTITIES)
|
|
295
|
+
return null;
|
|
296
|
+
const sources = loadSources(company.dir, now);
|
|
297
|
+
const signals = loadSignals(company.dir, sources.times);
|
|
298
|
+
const ranked = [];
|
|
299
|
+
for (const type of SIGNAL_TYPES) {
|
|
300
|
+
ranked.push(...signals.filter((item) => item.type === type.dir).sort(compareSignals).slice(0, SIGNAL_TOP));
|
|
301
|
+
}
|
|
302
|
+
const sourceBlock = renderSourceBlock(sources.counts);
|
|
303
|
+
const budget = Math.max(0, SIGNAL_TOKEN_RESERVE - tokenCount(sourceBlock));
|
|
304
|
+
const signalBlock = renderSignalBlock(fitSignals(ranked, budget));
|
|
305
|
+
const blocks = [renderEntityBlock(company.slug, entities.groups)];
|
|
306
|
+
if (signalBlock)
|
|
307
|
+
blocks.push(signalBlock);
|
|
308
|
+
blocks.push(sourceBlock);
|
|
309
|
+
return `${blocks.join("\n\n")}\n`;
|
|
310
|
+
}
|
|
311
|
+
export function writeCompanyBrief(hqRoot, company, now) {
|
|
312
|
+
try {
|
|
313
|
+
const content = renderCompanyBrief(company, now);
|
|
314
|
+
if (content === null)
|
|
315
|
+
return { status: "skipped", reason: "fewer than 3 entities" };
|
|
316
|
+
const target = path.join(company.dir, COMPANY_BRIEF_FILE);
|
|
317
|
+
let existing = null;
|
|
318
|
+
try {
|
|
319
|
+
// Never follow a symlinked brief for the equality check; safeWriteFile rejects it below.
|
|
320
|
+
if (!fs.lstatSync(target).isSymbolicLink())
|
|
321
|
+
existing = fs.readFileSync(target);
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
if (error.code !== "ENOENT")
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
if (existing && existing.equals(Buffer.from(content, "utf8")))
|
|
328
|
+
return { status: "unchanged" };
|
|
329
|
+
safeWriteFile(hqRoot, [company.dir], target, content);
|
|
330
|
+
return { status: "written" };
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
const failed = error;
|
|
334
|
+
return { status: "failed", reason: `${failed.name}: ${failed.message}` };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
//# sourceMappingURL=company-brief.js.map
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* backend model to read. The model only fills a candidates file; everything
|
|
8
8
|
* that affects identity, scope and cost stays in code.
|
|
9
9
|
*/
|
|
10
|
+
import { type CompanyBriefResult } from "./company-brief.js";
|
|
10
11
|
import { type OntologyCompany } from "./company.js";
|
|
11
12
|
export declare const ONTOLOGY_FLAG = "checkpoint.ontology";
|
|
12
13
|
export declare const SHARED_INPUTS_FLAG = "checkpoint.ontology.shared-inputs";
|
|
14
|
+
export declare const COMPANY_BRIEF_FLAG = "checkpoint.ontology.company-brief";
|
|
13
15
|
export declare const PLAN_FILE = "ontology-plan.json";
|
|
14
16
|
export declare const CANDIDATES_FILE = "ontology-candidates.json";
|
|
15
17
|
export declare const REPORT_FILE = "ontology-report.json";
|
|
@@ -66,6 +68,11 @@ export interface OntologyPlan {
|
|
|
66
68
|
first_run: boolean;
|
|
67
69
|
boundary: string | null;
|
|
68
70
|
shared_inputs: boolean;
|
|
71
|
+
/** Present only when `checkpoint.ontology.company-brief` was on for this plan. */
|
|
72
|
+
company_brief?: {
|
|
73
|
+
status: CompanyBriefResult["status"];
|
|
74
|
+
reason?: string;
|
|
75
|
+
};
|
|
69
76
|
inputs: PlanInput[];
|
|
70
77
|
/** The session's summary, decisions and learnings, for the `session` input. */
|
|
71
78
|
session_text: string | null;
|
|
@@ -98,10 +105,12 @@ export interface PlanOptions {
|
|
|
98
105
|
export type PlanOutcome = {
|
|
99
106
|
status: "skipped";
|
|
100
107
|
reason: string;
|
|
108
|
+
companyBrief?: CompanyBriefResult;
|
|
101
109
|
} | {
|
|
102
110
|
status: "planned";
|
|
103
111
|
plan: OntologyPlan;
|
|
104
112
|
planPath: string;
|
|
113
|
+
companyBrief?: CompanyBriefResult;
|
|
105
114
|
};
|
|
106
115
|
export declare function ledgerPath(hqRoot: string, company: OntologyCompany): string;
|
|
107
116
|
export declare function readLedger(hqRoot: string, company: OntologyCompany): Ledger | null;
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import { writeCompanyBrief } from "./company-brief.js";
|
|
13
14
|
import { resolveOntologyCompany, safeWriteFile, stateRoot } from "./company.js";
|
|
14
15
|
import { buildEntityIndex, listKnowledgeFiles, listSignalFiles, readSignal } from "./tree.js";
|
|
15
16
|
export const ONTOLOGY_FLAG = "checkpoint.ontology";
|
|
16
17
|
export const SHARED_INPUTS_FLAG = "checkpoint.ontology.shared-inputs";
|
|
18
|
+
export const COMPANY_BRIEF_FLAG = "checkpoint.ontology.company-brief";
|
|
17
19
|
export const PLAN_FILE = "ontology-plan.json";
|
|
18
20
|
export const CANDIDATES_FILE = "ontology-candidates.json";
|
|
19
21
|
export const REPORT_FILE = "ontology-report.json";
|
|
@@ -173,6 +175,10 @@ export async function planOntologyRun(options) {
|
|
|
173
175
|
if (!(await options.readFlag(ONTOLOGY_FLAG, company)))
|
|
174
176
|
return { status: "skipped", reason: "flag off" };
|
|
175
177
|
const sharedInputs = await options.readFlag(SHARED_INPUTS_FLAG, company);
|
|
178
|
+
// Before the ledger, so a nothing-changed plan still records the brief.
|
|
179
|
+
const companyBrief = (await options.readFlag(COMPANY_BRIEF_FLAG, company))
|
|
180
|
+
? writeCompanyBrief(hqRoot, company, options.now ?? new Date())
|
|
181
|
+
: undefined;
|
|
176
182
|
const touched = sessionTouched(hqRoot, company, payload.files);
|
|
177
183
|
const { ledger, firstRun } = refreshLedger(company, readLedger(hqRoot, company), touched, sharedInputs);
|
|
178
184
|
const pending = Object.entries(ledger.entries)
|
|
@@ -214,7 +220,8 @@ export async function planOntologyRun(options) {
|
|
|
214
220
|
}
|
|
215
221
|
writeLedger(hqRoot, company, ledger);
|
|
216
222
|
if (inputs.length === 0) {
|
|
217
|
-
|
|
223
|
+
const reason = pending.length > 0 ? "over budget" : "nothing changed";
|
|
224
|
+
return companyBrief ? { status: "skipped", reason, companyBrief } : { status: "skipped", reason };
|
|
218
225
|
}
|
|
219
226
|
// Session content is scoped to the session's user (design section 6, D2).
|
|
220
227
|
// v1 records the existing entities it names as links in the report and
|
|
@@ -243,6 +250,13 @@ export async function planOntologyRun(options) {
|
|
|
243
250
|
first_run: firstRun,
|
|
244
251
|
boundary: ledger.boundary,
|
|
245
252
|
shared_inputs: sharedInputs,
|
|
253
|
+
...(companyBrief
|
|
254
|
+
? {
|
|
255
|
+
company_brief: companyBrief.reason
|
|
256
|
+
? { status: companyBrief.status, reason: companyBrief.reason }
|
|
257
|
+
: { status: companyBrief.status },
|
|
258
|
+
}
|
|
259
|
+
: {}),
|
|
246
260
|
inputs,
|
|
247
261
|
session_text: sessionText,
|
|
248
262
|
pending_after_run: remaining,
|
|
@@ -258,7 +272,7 @@ export async function planOntologyRun(options) {
|
|
|
258
272
|
fs.mkdirSync(runDir, { recursive: true });
|
|
259
273
|
const planPath = path.join(runDir, PLAN_FILE);
|
|
260
274
|
fs.writeFileSync(planPath, `${JSON.stringify(plan, null, 2)}\n`);
|
|
261
|
-
return { status: "planned", plan, planPath };
|
|
275
|
+
return companyBrief ? { status: "planned", plan, planPath, companyBrief } : { status: "planned", plan, planPath };
|
|
262
276
|
}
|
|
263
277
|
const SESSION_TEXT_MAX_BYTES = 8_000;
|
|
264
278
|
/** The session summary, decisions and learnings from the payload, capped. */
|
|
@@ -67,6 +67,7 @@ export declare function selectClaudeAccount(input: {
|
|
|
67
67
|
targetHost: LaneHost;
|
|
68
68
|
config: LanesConfig;
|
|
69
69
|
allowedAccounts?: readonly string[];
|
|
70
|
+
model?: string;
|
|
70
71
|
explicitAccount?: string;
|
|
71
72
|
unavailableHosts?: readonly BalanceHostGap[];
|
|
72
73
|
}): ClaudeAccountSelection;
|
|
@@ -91,7 +91,10 @@ function buildLoads(input) {
|
|
|
91
91
|
const loads = Object.keys(configAccounts).sort().map((account) => {
|
|
92
92
|
const hosts = input.hosts.map((host) => accountHostLoad(host, account));
|
|
93
93
|
const targetLoad = hosts.find((host) => sameHost(host.host, targetHost));
|
|
94
|
-
const
|
|
94
|
+
const configuredModel = configAccounts[account]?.model;
|
|
95
|
+
const ineligible = targetIneligibilityReason(account, configAccounts[account]?.enabled === true, input.allowedAccounts, targetLoad) ?? (input.model !== undefined && configuredModel !== undefined && configuredModel !== input.model
|
|
96
|
+
? "model_mismatch"
|
|
97
|
+
: undefined);
|
|
95
98
|
if (ineligible !== undefined)
|
|
96
99
|
skipped.push({ account, host: targetHost, reason: ineligible });
|
|
97
100
|
const knownCounts = hosts.flatMap((host) => host.live_lanes === null ? [] : [host.live_lanes]);
|