@indigoai-us/hq-cli 5.238.0 → 5.240.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 +21 -0
- package/dist/command-catalog.generated.d.ts +41 -0
- package/dist/command-catalog.generated.js +54 -0
- package/dist/commands/lanes.js +187 -16
- package/dist/lib/checkpoint-ontology/apply.js +26 -1
- package/dist/lib/checkpoint-ontology/plan.d.ts +12 -0
- package/dist/lib/checkpoint-ontology/plan.js +12 -0
- package/dist/lib/lanes/approval-gate.d.ts +8 -0
- package/dist/lib/lanes/approval-gate.js +90 -0
- package/dist/lib/lanes/errors.d.ts +1 -1
- package/dist/lib/lanes/index.d.ts +3 -1
- package/dist/lib/lanes/index.js +2 -1
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/outpost-registry-worker-approval.d.ts +18 -0
- package/dist/lib/lanes/outpost-registry-worker-approval.js +109 -0
- package/dist/lib/lanes/profile.d.ts +10 -0
- package/dist/lib/lanes/profile.js +88 -0
- package/dist/lib/lanes/reconcile.js +38 -11
- package/dist/lib/lanes/registry-persona.d.ts +20 -0
- package/dist/lib/lanes/registry-persona.js +329 -0
- package/dist/lib/lanes/registry.d.ts +4 -1
- package/dist/lib/lanes/registry.js +8 -3
- package/dist/lib/lanes/spawn.d.ts +1 -1
- package/dist/lib/lanes/spawn.js +28 -7
- package/dist/lib/lanes/store.js +90 -0
- package/dist/lib/lanes/types.d.ts +29 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.d.ts +2 -0
- package/dist/lib/lanes/watcher.js +46 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.240.0] — 2026-09-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `hq lanes` worker profiles can pin a registry persona from `worker.yaml`.
|
|
10
|
+
The composed prompt includes the worker's instructions, skill paths, done
|
|
11
|
+
criteria, and stop checkpoints. Registry approval requirements are passed to
|
|
12
|
+
workers, enforced on external-action envelopes, and recorded through the new
|
|
13
|
+
senior-only `hq lanes approve` command.
|
|
14
|
+
|
|
15
|
+
## [5.239.0] — 2026-09-27
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `hq core checkpoint` can skip a knowledge link to a concept entity whose
|
|
20
|
+
name is also the name of an entity of another type, such as a concept
|
|
21
|
+
`hq-secrets` beside a project `HQ Secrets`. The report lists the skipped
|
|
22
|
+
link as rejected with reason `ambiguous_type`. Signal and session inputs,
|
|
23
|
+
other links and new entities are unchanged. Behind the hq-flags flag
|
|
24
|
+
`checkpoint.ontology.skip-ambiguous-concept-links`, off by default.
|
|
25
|
+
|
|
5
26
|
## [5.238.0] — 2026-09-27
|
|
6
27
|
|
|
7
28
|
### Added: Lane orphan reporting
|
|
@@ -7507,6 +7507,25 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
7507
7507
|
readonly description: "JSON on stdout (always)";
|
|
7508
7508
|
}];
|
|
7509
7509
|
readonly subcommands: readonly [];
|
|
7510
|
+
}, {
|
|
7511
|
+
readonly name: "approve-target";
|
|
7512
|
+
readonly description: "";
|
|
7513
|
+
readonly aliases: readonly [];
|
|
7514
|
+
readonly hidden: true;
|
|
7515
|
+
readonly usage: "[options] <lane-id>";
|
|
7516
|
+
readonly arguments: readonly [{
|
|
7517
|
+
readonly name: "lane-id";
|
|
7518
|
+
readonly required: true;
|
|
7519
|
+
readonly variadic: false;
|
|
7520
|
+
}];
|
|
7521
|
+
readonly options: readonly [{
|
|
7522
|
+
readonly flags: "--approval-context <base64url>";
|
|
7523
|
+
readonly description: "Primary-authorized registry worker approval";
|
|
7524
|
+
}, {
|
|
7525
|
+
readonly flags: "--json";
|
|
7526
|
+
readonly description: "JSON on stdout (always)";
|
|
7527
|
+
}];
|
|
7528
|
+
readonly subcommands: readonly [];
|
|
7510
7529
|
}, {
|
|
7511
7530
|
readonly name: "reparent-target";
|
|
7512
7531
|
readonly description: "";
|
|
@@ -8193,6 +8212,28 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
8193
8212
|
readonly description: "JSON on stdout (always)";
|
|
8194
8213
|
}];
|
|
8195
8214
|
readonly subcommands: readonly [];
|
|
8215
|
+
}, {
|
|
8216
|
+
readonly name: "approve";
|
|
8217
|
+
readonly description: "Record senior approval for a registry worker checkpoint";
|
|
8218
|
+
readonly aliases: readonly [];
|
|
8219
|
+
readonly hidden: false;
|
|
8220
|
+
readonly usage: "[options] <lane-id>";
|
|
8221
|
+
readonly arguments: readonly [{
|
|
8222
|
+
readonly name: "lane-id";
|
|
8223
|
+
readonly required: true;
|
|
8224
|
+
readonly variadic: false;
|
|
8225
|
+
}];
|
|
8226
|
+
readonly options: readonly [{
|
|
8227
|
+
readonly flags: "--checkpoint <name>";
|
|
8228
|
+
readonly description: "Exact registry worker checkpoint name";
|
|
8229
|
+
}, {
|
|
8230
|
+
readonly flags: "--reason <text>";
|
|
8231
|
+
readonly description: "Why the checkpoint is approved";
|
|
8232
|
+
}, {
|
|
8233
|
+
readonly flags: "--json";
|
|
8234
|
+
readonly description: "JSON on stdout (always)";
|
|
8235
|
+
}];
|
|
8236
|
+
readonly subcommands: readonly [];
|
|
8196
8237
|
}, {
|
|
8197
8238
|
readonly name: "ask";
|
|
8198
8239
|
readonly description: "Queue a mid-run question without blocking the lane";
|
|
@@ -9705,6 +9705,31 @@ export const COMMAND_CATALOG = [
|
|
|
9705
9705
|
],
|
|
9706
9706
|
"subcommands": []
|
|
9707
9707
|
},
|
|
9708
|
+
{
|
|
9709
|
+
"name": "approve-target",
|
|
9710
|
+
"description": "",
|
|
9711
|
+
"aliases": [],
|
|
9712
|
+
"hidden": true,
|
|
9713
|
+
"usage": "[options] <lane-id>",
|
|
9714
|
+
"arguments": [
|
|
9715
|
+
{
|
|
9716
|
+
"name": "lane-id",
|
|
9717
|
+
"required": true,
|
|
9718
|
+
"variadic": false
|
|
9719
|
+
}
|
|
9720
|
+
],
|
|
9721
|
+
"options": [
|
|
9722
|
+
{
|
|
9723
|
+
"flags": "--approval-context <base64url>",
|
|
9724
|
+
"description": "Primary-authorized registry worker approval"
|
|
9725
|
+
},
|
|
9726
|
+
{
|
|
9727
|
+
"flags": "--json",
|
|
9728
|
+
"description": "JSON on stdout (always)"
|
|
9729
|
+
}
|
|
9730
|
+
],
|
|
9731
|
+
"subcommands": []
|
|
9732
|
+
},
|
|
9708
9733
|
{
|
|
9709
9734
|
"name": "reparent-target",
|
|
9710
9735
|
"description": "",
|
|
@@ -10601,6 +10626,35 @@ export const COMMAND_CATALOG = [
|
|
|
10601
10626
|
],
|
|
10602
10627
|
"subcommands": []
|
|
10603
10628
|
},
|
|
10629
|
+
{
|
|
10630
|
+
"name": "approve",
|
|
10631
|
+
"description": "Record senior approval for a registry worker checkpoint",
|
|
10632
|
+
"aliases": [],
|
|
10633
|
+
"hidden": false,
|
|
10634
|
+
"usage": "[options] <lane-id>",
|
|
10635
|
+
"arguments": [
|
|
10636
|
+
{
|
|
10637
|
+
"name": "lane-id",
|
|
10638
|
+
"required": true,
|
|
10639
|
+
"variadic": false
|
|
10640
|
+
}
|
|
10641
|
+
],
|
|
10642
|
+
"options": [
|
|
10643
|
+
{
|
|
10644
|
+
"flags": "--checkpoint <name>",
|
|
10645
|
+
"description": "Exact registry worker checkpoint name"
|
|
10646
|
+
},
|
|
10647
|
+
{
|
|
10648
|
+
"flags": "--reason <text>",
|
|
10649
|
+
"description": "Why the checkpoint is approved"
|
|
10650
|
+
},
|
|
10651
|
+
{
|
|
10652
|
+
"flags": "--json",
|
|
10653
|
+
"description": "JSON on stdout (always)"
|
|
10654
|
+
}
|
|
10655
|
+
],
|
|
10656
|
+
"subcommands": []
|
|
10657
|
+
},
|
|
10604
10658
|
{
|
|
10605
10659
|
"name": "ask",
|
|
10606
10660
|
"description": "Queue a mid-run question without blocking the lane",
|
package/dist/commands/lanes.js
CHANGED
|
@@ -24,6 +24,7 @@ import { resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
|
24
24
|
import { defaultTrackerActor, LANES_TRACKER_FLAG, registerLaneTrackerCommands, } from "./lanes-tracker.js";
|
|
25
25
|
import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
|
|
26
26
|
import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } from "../lib/lanes/outpost-profile-repin.js";
|
|
27
|
+
import { createOutpostRegistryWorkerApprovalContext, parseOutpostRegistryWorkerApprovalContext, } from "../lib/lanes/outpost-registry-worker-approval.js";
|
|
27
28
|
import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
|
|
28
29
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
29
30
|
import { buildOutpostCapacityCommand, buildOutpostBalanceMoveCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostQuestionAnswerCommand, buildOutpostQuestionListCommand, buildOutpostQuestionShowCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
@@ -40,7 +41,7 @@ import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
|
40
41
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
|
|
41
42
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
42
43
|
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
43
|
-
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
44
|
+
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
44
45
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
45
46
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
46
47
|
import { keepAliveExclusion, LEAD_LANE_KEEP_ALIVE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
@@ -3059,6 +3060,9 @@ async function runCreate(ctx, opts) {
|
|
|
3059
3060
|
worker_profile: profile.id,
|
|
3060
3061
|
worker_profile_hash: profile.contentHash,
|
|
3061
3062
|
worker_profile_ignored: ignoredWorkerProfileFields(provider),
|
|
3063
|
+
...(profile.registryWorker !== undefined
|
|
3064
|
+
? { registry_worker: registryWorkerRecord(profile.registryWorker) }
|
|
3065
|
+
: {}),
|
|
3062
3066
|
}
|
|
3063
3067
|
: {}),
|
|
3064
3068
|
keep_alive: keepAlive,
|
|
@@ -3470,7 +3474,12 @@ function verifyOutpostProfileContent(hqRoot, lane) {
|
|
|
3470
3474
|
if (lane.host.kind !== "outpost" || lane.worker_profile === undefined)
|
|
3471
3475
|
return;
|
|
3472
3476
|
const expectedHash = lane.worker_profile_hash;
|
|
3473
|
-
const profile =
|
|
3477
|
+
const profile = loadWorkerProfileWithRegistry(hqRoot, lane.worker_profile, {
|
|
3478
|
+
laneCompany: lane.company?.slug ?? lane.mesh_company?.slug,
|
|
3479
|
+
...(lane.registry_worker !== undefined
|
|
3480
|
+
? { registryWorkerRecord: lane.registry_worker }
|
|
3481
|
+
: {}),
|
|
3482
|
+
});
|
|
3474
3483
|
if (expectedHash !== profile.contentHash) {
|
|
3475
3484
|
throw new LanesError("worker_profile_hash_mismatch", `Worker profile ${JSON.stringify(lane.worker_profile)} on Outpost ${lane.host.id} does not match the hash recorded by the creator`, {
|
|
3476
3485
|
lane_id: lane.lane_id,
|
|
@@ -3692,7 +3701,9 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3692
3701
|
? "grant-target"
|
|
3693
3702
|
: action === "reparent"
|
|
3694
3703
|
? "reparent-target"
|
|
3695
|
-
: action
|
|
3704
|
+
: action === "approve"
|
|
3705
|
+
? "approve-target"
|
|
3706
|
+
: action,
|
|
3696
3707
|
laneId: lane.lane_id,
|
|
3697
3708
|
hqBinary: action === "grant" || action === "reparent"
|
|
3698
3709
|
? outpostCliOverride(ctx, outpostId)
|
|
@@ -3712,6 +3723,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3712
3723
|
grantContext: contexts.grantContext,
|
|
3713
3724
|
profileRepinContext: contexts.profileRepinContext,
|
|
3714
3725
|
failedResumeContext: contexts.failedResumeContext,
|
|
3726
|
+
approvalContext: options.approvalContext,
|
|
3715
3727
|
compact: options.compact,
|
|
3716
3728
|
}),
|
|
3717
3729
|
});
|
|
@@ -3820,17 +3832,20 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3820
3832
|
? remote.accepted === true
|
|
3821
3833
|
: action === "grant"
|
|
3822
3834
|
? remote.hire_grant === Number(options.hire)
|
|
3823
|
-
: action === "
|
|
3824
|
-
? remote.
|
|
3825
|
-
Array.isArray(remote.
|
|
3826
|
-
:
|
|
3827
|
-
remote.
|
|
3828
|
-
|
|
3829
|
-
(remote.delivery_status === "
|
|
3830
|
-
remote.
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3835
|
+
: action === "approve"
|
|
3836
|
+
? remote.checkpoint === options.checkpoint &&
|
|
3837
|
+
remote.lane !== null && typeof remote.lane === "object" && !Array.isArray(remote.lane)
|
|
3838
|
+
: action === "reparent"
|
|
3839
|
+
? remote.lane !== null && typeof remote.lane === "object" &&
|
|
3840
|
+
Array.isArray(remote.changed_descendant_ids)
|
|
3841
|
+
: (((remote.delivery_status === "worker_spawned" ||
|
|
3842
|
+
remote.delivery_status === "queued_on_target_account") &&
|
|
3843
|
+
remote.lane !== undefined) ||
|
|
3844
|
+
(remote.delivery_status === "queued_on_admission" &&
|
|
3845
|
+
remote.lane !== null &&
|
|
3846
|
+
typeof remote.lane === "object" &&
|
|
3847
|
+
!Array.isArray(remote.lane) &&
|
|
3848
|
+
remote.lane.state === "queued")));
|
|
3834
3849
|
if (!resultConfirmed) {
|
|
3835
3850
|
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without a confirmed ${action} result for lane ${lane.lane_id}`, {
|
|
3836
3851
|
lane_id: lane.lane_id,
|
|
@@ -3866,7 +3881,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3866
3881
|
});
|
|
3867
3882
|
return;
|
|
3868
3883
|
}
|
|
3869
|
-
if (action === "reparent" || action === "resume") {
|
|
3884
|
+
if (action === "approve" || action === "reparent" || action === "resume") {
|
|
3870
3885
|
const ownerLane = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, remote.lane, undefined, { allowNonTerminal: true });
|
|
3871
3886
|
const descendants = [];
|
|
3872
3887
|
if (action === "reparent") {
|
|
@@ -5189,6 +5204,141 @@ async function runGrant(ctx, laneId, opts) {
|
|
|
5189
5204
|
const updated = await applyHireGrant(ctx, laneId, hireGrant, "senior");
|
|
5190
5205
|
printGrantResult(hqRoot, updated);
|
|
5191
5206
|
}
|
|
5207
|
+
async function runApprove(ctx, laneId, opts) {
|
|
5208
|
+
const checkpoint = opts.checkpoint?.trim();
|
|
5209
|
+
const reason = opts.reason?.trim();
|
|
5210
|
+
if (!checkpoint) {
|
|
5211
|
+
throw new LanesError("registry_worker_checkpoint_required", "--checkpoint requires a non-empty checkpoint name", { lane_id: laneId });
|
|
5212
|
+
}
|
|
5213
|
+
if (!reason) {
|
|
5214
|
+
throw new LanesError("registry_worker_approval_reason_required", "--reason requires a non-empty explanation", { lane_id: laneId });
|
|
5215
|
+
}
|
|
5216
|
+
assertNoSecretText("approval reason", reason);
|
|
5217
|
+
const hqRoot = ctx.hqRoot();
|
|
5218
|
+
const initial = readLane(hqRoot, laneId);
|
|
5219
|
+
const remoteOutpost = initial.host.kind === "outpost" && !laneIsHostedHere(ctx, initial);
|
|
5220
|
+
if (!initial.registry_worker ||
|
|
5221
|
+
(!initial.registry_worker.approval_required && initial.registry_worker.human_checkpoints.length === 0)) {
|
|
5222
|
+
throw new LanesError("registry_worker_approval_unavailable", `Lane ${laneId} has no registry worker approval gate`, { lane_id: laneId });
|
|
5223
|
+
}
|
|
5224
|
+
const allowed = initial.registry_worker.human_checkpoints.length > 0
|
|
5225
|
+
? initial.registry_worker.human_checkpoints
|
|
5226
|
+
: ["external_publish"];
|
|
5227
|
+
if (!allowed.includes(checkpoint)) {
|
|
5228
|
+
throw new LanesError("registry_worker_checkpoint_unknown", `Checkpoint ${JSON.stringify(checkpoint)} is not declared by the lane's registry worker`, { lane_id: laneId, checkpoint, accepted: allowed });
|
|
5229
|
+
}
|
|
5230
|
+
if (!callerMatchesSenior(ctx, initial.senior)) {
|
|
5231
|
+
throw new LanesError("registry_worker_approval_unauthorized", `Only senior ${seniorKey(initial.senior)} may approve lane ${laneId}'s registry worker checkpoint`, { lane_id: laneId, senior: initial.senior });
|
|
5232
|
+
}
|
|
5233
|
+
if (remoteOutpost) {
|
|
5234
|
+
const approvalContext = createOutpostRegistryWorkerApprovalContext({
|
|
5235
|
+
lane_id: initial.lane_id,
|
|
5236
|
+
expected_senior: initial.senior,
|
|
5237
|
+
actor: resumeProfileRepinActor(ctx),
|
|
5238
|
+
checkpoint,
|
|
5239
|
+
reason,
|
|
5240
|
+
approval_id: randomBytes(16).toString("hex"),
|
|
5241
|
+
registry_worker_id: initial.registry_worker.id,
|
|
5242
|
+
worker_yaml_sha256: initial.registry_worker.worker_yaml_sha256,
|
|
5243
|
+
at: ctx.now().toISOString(),
|
|
5244
|
+
});
|
|
5245
|
+
await dispatchOutpostLaneCommand(ctx, initial, "approve", {
|
|
5246
|
+
approvalContext,
|
|
5247
|
+
checkpoint,
|
|
5248
|
+
});
|
|
5249
|
+
return;
|
|
5250
|
+
}
|
|
5251
|
+
assertLaneRunsHereForOperatorMutation(ctx, initial, "approve");
|
|
5252
|
+
const approvalId = randomBytes(16).toString("hex");
|
|
5253
|
+
const at = ctx.now().toISOString();
|
|
5254
|
+
const actor = resumeProfileRepinActor(ctx);
|
|
5255
|
+
const updated = retryUpdateLane(hqRoot, laneId, "command", (current) => {
|
|
5256
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "approve");
|
|
5257
|
+
if (!callerMatchesSenior(ctx, current.senior)) {
|
|
5258
|
+
throw new LanesError("registry_worker_approval_unauthorized", `Only senior ${seniorKey(current.senior)} may approve lane ${laneId}'s registry worker checkpoint`, { lane_id: laneId, senior: current.senior });
|
|
5259
|
+
}
|
|
5260
|
+
if (JSON.stringify(current.registry_worker) !== JSON.stringify(initial.registry_worker) ||
|
|
5261
|
+
!current.registry_worker ||
|
|
5262
|
+
!current.registry_worker.human_checkpoints.concat(current.registry_worker.approval_required && current.registry_worker.human_checkpoints.length === 0
|
|
5263
|
+
? ["external_publish"]
|
|
5264
|
+
: []).includes(checkpoint)) {
|
|
5265
|
+
throw new LanesError("registry_worker_approval_stale", `Lane ${laneId}'s registry worker approval contract changed before the checkpoint was recorded`, { lane_id: laneId });
|
|
5266
|
+
}
|
|
5267
|
+
return {
|
|
5268
|
+
...current,
|
|
5269
|
+
registry_worker_approval_history: appendCappedHistory(current.registry_worker_approval_history ?? [], {
|
|
5270
|
+
event: "approved",
|
|
5271
|
+
approval_id: approvalId,
|
|
5272
|
+
checkpoint,
|
|
5273
|
+
registry_worker_id: current.registry_worker.id,
|
|
5274
|
+
worker_yaml_sha256: current.registry_worker.worker_yaml_sha256,
|
|
5275
|
+
reason,
|
|
5276
|
+
actor,
|
|
5277
|
+
at,
|
|
5278
|
+
}),
|
|
5279
|
+
};
|
|
5280
|
+
});
|
|
5281
|
+
printJson({ ok: true, action: "approve", lane_id: laneId, checkpoint, lane: updated });
|
|
5282
|
+
}
|
|
5283
|
+
async function runApproveOwnerTarget(ctx, laneId, opts) {
|
|
5284
|
+
if (!ctx.isSsmOwnerDispatch()) {
|
|
5285
|
+
throw new LanesError("registry_worker_approval_dispatch_unauthorized", "The Outpost approval target is available only to an SSM Run Command dispatch", { lane_id: laneId });
|
|
5286
|
+
}
|
|
5287
|
+
if (opts.approvalContext === undefined) {
|
|
5288
|
+
throw new LanesError("registry_worker_approval_context_invalid", "--approval-context is required on the Outpost owner target", { lane_id: laneId });
|
|
5289
|
+
}
|
|
5290
|
+
const authorization = parseOutpostRegistryWorkerApprovalContext(opts.approvalContext, laneId);
|
|
5291
|
+
assertNoSecretText("approval reason", authorization.reason);
|
|
5292
|
+
const hqRoot = ctx.hqRoot();
|
|
5293
|
+
const validateOwnerState = (current) => {
|
|
5294
|
+
if (current.host.kind !== "outpost" || !laneIsHostedHere(ctx, current)) {
|
|
5295
|
+
throw new LanesError("lane_remote", `Outpost approval target cannot mutate lane ${laneId} on this host`, { lane_id: laneId, host: current.host });
|
|
5296
|
+
}
|
|
5297
|
+
if (!sameHireGrantSenior(current.senior, authorization.expected_senior) ||
|
|
5298
|
+
current.registry_worker?.id !== authorization.registry_worker_id ||
|
|
5299
|
+
current.registry_worker.worker_yaml_sha256 !== authorization.worker_yaml_sha256) {
|
|
5300
|
+
throw new LanesError("registry_worker_approval_stale", `Lane ${laneId}'s senior or registry worker changed after approval was authorized`, { lane_id: laneId });
|
|
5301
|
+
}
|
|
5302
|
+
if (!current.registry_worker ||
|
|
5303
|
+
(!current.registry_worker.approval_required && current.registry_worker.human_checkpoints.length === 0)) {
|
|
5304
|
+
throw new LanesError("registry_worker_approval_unavailable", `Lane ${laneId} has no registry worker approval gate`, { lane_id: laneId });
|
|
5305
|
+
}
|
|
5306
|
+
const allowed = current.registry_worker.human_checkpoints.length > 0
|
|
5307
|
+
? current.registry_worker.human_checkpoints
|
|
5308
|
+
: ["external_publish"];
|
|
5309
|
+
if (!allowed.includes(authorization.checkpoint)) {
|
|
5310
|
+
throw new LanesError("registry_worker_approval_stale", `Lane ${laneId}'s registry worker no longer declares checkpoint ${JSON.stringify(authorization.checkpoint)}`, { lane_id: laneId, checkpoint: authorization.checkpoint });
|
|
5311
|
+
}
|
|
5312
|
+
};
|
|
5313
|
+
validateOwnerState(readLane(hqRoot, laneId));
|
|
5314
|
+
const updated = retryUpdateLane(hqRoot, laneId, "command", (current) => {
|
|
5315
|
+
validateOwnerState(current);
|
|
5316
|
+
const history = current.registry_worker_approval_history ?? [];
|
|
5317
|
+
if (history.some((entry) => entry.event === "approved" && entry.approval_id === authorization.approval_id)) {
|
|
5318
|
+
return current;
|
|
5319
|
+
}
|
|
5320
|
+
return {
|
|
5321
|
+
...current,
|
|
5322
|
+
registry_worker_approval_history: appendCappedHistory(history, {
|
|
5323
|
+
event: "approved",
|
|
5324
|
+
approval_id: authorization.approval_id,
|
|
5325
|
+
checkpoint: authorization.checkpoint,
|
|
5326
|
+
registry_worker_id: authorization.registry_worker_id,
|
|
5327
|
+
worker_yaml_sha256: authorization.worker_yaml_sha256,
|
|
5328
|
+
reason: authorization.reason,
|
|
5329
|
+
actor: authorization.actor,
|
|
5330
|
+
at: authorization.at,
|
|
5331
|
+
}),
|
|
5332
|
+
};
|
|
5333
|
+
});
|
|
5334
|
+
printJson({
|
|
5335
|
+
ok: true,
|
|
5336
|
+
action: "approve",
|
|
5337
|
+
lane_id: laneId,
|
|
5338
|
+
checkpoint: authorization.checkpoint,
|
|
5339
|
+
lane: updated,
|
|
5340
|
+
});
|
|
5341
|
+
}
|
|
5192
5342
|
async function runGrantOwnerTarget(ctx, laneId, opts) {
|
|
5193
5343
|
if (!ctx.isSsmOwnerDispatch()) {
|
|
5194
5344
|
throw new LanesError("hire_grant_dispatch_unauthorized", "The Outpost grant target is available only to an SSM Run Command dispatch", { lane_id: laneId });
|
|
@@ -5350,7 +5500,9 @@ async function runAdmit(ctx, laneId, opts) {
|
|
|
5350
5500
|
}, ctx.sleep);
|
|
5351
5501
|
}
|
|
5352
5502
|
function resolveResumeProfile(ctx, lane, workerId) {
|
|
5353
|
-
const worker = resolveLaneWorker(ctx.hqRoot(), workerId.trim(), lane.company?.slug ?? lane.mesh_company?.slug)
|
|
5503
|
+
const worker = resolveLaneWorker(ctx.hqRoot(), workerId.trim(), lane.company?.slug ?? lane.mesh_company?.slug, undefined, lane.worker_profile === workerId.trim() && lane.registry_worker !== undefined
|
|
5504
|
+
? { registryWorkerRecord: lane.registry_worker }
|
|
5505
|
+
: {});
|
|
5354
5506
|
if (worker.profile === undefined) {
|
|
5355
5507
|
throw new LanesError("worker_profile_required", `Worker ${JSON.stringify(workerId)} resolves to a registry worker, not a reusable provider profile; resume --worker requires personal/workers/<id>/meta.yaml`, { worker: workerId, path: worker.path });
|
|
5356
5508
|
}
|
|
@@ -5393,6 +5545,9 @@ function applyResumeProfileAndEffort(current, next, input) {
|
|
|
5393
5545
|
next.worker_pack = input.profile.pack;
|
|
5394
5546
|
next.worker_profile = profile.id;
|
|
5395
5547
|
next.worker_profile_hash = profile.contentHash;
|
|
5548
|
+
next.registry_worker = profile.registryWorker !== undefined
|
|
5549
|
+
? registryWorkerRecord(profile.registryWorker)
|
|
5550
|
+
: undefined;
|
|
5396
5551
|
if (input.profileRepin !== undefined &&
|
|
5397
5552
|
current.host.kind === "outpost" &&
|
|
5398
5553
|
current.worker_profile_hash !== profile.contentHash) {
|
|
@@ -9282,6 +9437,13 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9282
9437
|
.action(wrap(async (opts) => {
|
|
9283
9438
|
await runCreate(ctx, opts);
|
|
9284
9439
|
}));
|
|
9440
|
+
lanes
|
|
9441
|
+
.command("approve-target", { hidden: true })
|
|
9442
|
+
.argument("<lane-id>", "Lane id")
|
|
9443
|
+
.requiredOption("--approval-context <base64url>", "Primary-authorized registry worker approval")
|
|
9444
|
+
.action(wrap(async (laneId, opts) => {
|
|
9445
|
+
await runApproveOwnerTarget(ctx, laneId, opts);
|
|
9446
|
+
}));
|
|
9285
9447
|
lanes
|
|
9286
9448
|
.command("reparent-target", { hidden: true })
|
|
9287
9449
|
.argument("<lane-id>", "Lane id")
|
|
@@ -9581,6 +9743,15 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9581
9743
|
.action(wrap(async (laneId, options) => {
|
|
9582
9744
|
await runStop(ctx, laneId, options.reason);
|
|
9583
9745
|
}));
|
|
9746
|
+
lanes
|
|
9747
|
+
.command("approve")
|
|
9748
|
+
.description("Record senior approval for a registry worker checkpoint")
|
|
9749
|
+
.argument("<lane-id>", "Lane id")
|
|
9750
|
+
.requiredOption("--checkpoint <name>", "Exact registry worker checkpoint name")
|
|
9751
|
+
.requiredOption("--reason <text>", "Why the checkpoint is approved")
|
|
9752
|
+
.action(wrap(async (laneId, opts) => {
|
|
9753
|
+
await runApprove(ctx, laneId, opts);
|
|
9754
|
+
}));
|
|
9584
9755
|
lanes
|
|
9585
9756
|
.command("ask")
|
|
9586
9757
|
.description("Queue a mid-run question without blocking the lane")
|
|
@@ -262,6 +262,25 @@ export function applyOntologyRun(options) {
|
|
|
262
262
|
}
|
|
263
263
|
report.candidates = rawCandidates.length;
|
|
264
264
|
const index = buildEntityIndex(company.dir);
|
|
265
|
+
// checkpoint.ontology.skip-ambiguous-concept-links: canonical names held by entities of more than one type.
|
|
266
|
+
// Names compare in the non-organization form for every type (lowercase, keeps + and #, no squashing),
|
|
267
|
+
// so "C++" and "C", or "AB" and "A B", stay distinct.
|
|
268
|
+
const crossTypeKey = (name) => normalizeEntityName(name, "concept");
|
|
269
|
+
const crossTypeNames = new Set();
|
|
270
|
+
if (plan.skip_ambiguous_concept_links === true) {
|
|
271
|
+
const typesByName = new Map();
|
|
272
|
+
for (const entity of index.byRel.values()) {
|
|
273
|
+
const name = crossTypeKey(entity.canonical_name);
|
|
274
|
+
if (name === "")
|
|
275
|
+
continue;
|
|
276
|
+
const types = typesByName.get(name) ?? new Set();
|
|
277
|
+
types.add(entity.type);
|
|
278
|
+
typesByName.set(name, types);
|
|
279
|
+
}
|
|
280
|
+
for (const [name, types] of typesByName)
|
|
281
|
+
if (types.size > 1)
|
|
282
|
+
crossTypeNames.add(name);
|
|
283
|
+
}
|
|
265
284
|
const inputs = liveInputs(options.hqRoot, company, plan);
|
|
266
285
|
const blocked = new Set();
|
|
267
286
|
const updates = new Map();
|
|
@@ -313,8 +332,14 @@ export function applyOntologyRun(options) {
|
|
|
313
332
|
continue;
|
|
314
333
|
}
|
|
315
334
|
if (resolved.tier === 1 || resolved.tier === 2) {
|
|
316
|
-
report.matched += 1;
|
|
317
335
|
const linked = index.byRel.get(resolved.target);
|
|
336
|
+
if (input.kind === "knowledge" &&
|
|
337
|
+
linked.type === "concept" &&
|
|
338
|
+
crossTypeNames.has(crossTypeKey(linked.canonical_name))) {
|
|
339
|
+
note(report.rejected, "ambiguous_type");
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
report.matched += 1;
|
|
318
343
|
report.links.push({ input: candidate.input, action: "link", type: linked.type, canonical_name: linked.canonical_name, path: resolved.target });
|
|
319
344
|
if (resolved.tier === 1 && resolved.duplicates.length > 0) {
|
|
320
345
|
report.suspect_duplicates.push({ name: canonical, type: candidate.type, paths: [resolved.target, ...resolved.duplicates] });
|
|
@@ -68,6 +68,16 @@ export declare const NO_LINK_ALIASES_FLAG = "checkpoint.ontology.no-link-aliases
|
|
|
68
68
|
* the model cannot see that `John Smith` already exists.
|
|
69
69
|
*/
|
|
70
70
|
export declare const FIRST_NAME_PEOPLE_FLAG = "checkpoint.ontology.first-name-people";
|
|
71
|
+
/**
|
|
72
|
+
* A knowledge input does not link to a concept entity whose canonical name is
|
|
73
|
+
* also the canonical name of an entity of another type (`concept/hq-secrets`
|
|
74
|
+
* beside `project/hq-secrets`). The plan carries
|
|
75
|
+
* `skip_ambiguous_concept_links: true`, and apply rejects the link as
|
|
76
|
+
* `ambiguous_type`. CO-46: 34 of the 73 false positives on the knowledge
|
|
77
|
+
* fixtures were such concept links; skipping them raised precision from 0.733
|
|
78
|
+
* to 0.803 and cost recall 0.023 on a replay of six runs.
|
|
79
|
+
*/
|
|
80
|
+
export declare const SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG = "checkpoint.ontology.skip-ambiguous-concept-links";
|
|
71
81
|
export declare const PLAN_FILE = "ontology-plan.json";
|
|
72
82
|
export declare const CANDIDATES_FILE = "ontology-candidates.json";
|
|
73
83
|
export declare const REPORT_FILE = "ontology-report.json";
|
|
@@ -138,6 +148,8 @@ export interface OntologyPlan {
|
|
|
138
148
|
link_aliases?: false;
|
|
139
149
|
/** Set by checkpoint.ontology.first-name-people: apply defers a one-word person who may be an existing fuller name. */
|
|
140
150
|
first_name_people?: true;
|
|
151
|
+
/** Set by checkpoint.ontology.skip-ambiguous-concept-links: apply rejects a knowledge link to a concept whose name another type also holds. */
|
|
152
|
+
skip_ambiguous_concept_links?: true;
|
|
141
153
|
inputs: PlanInput[];
|
|
142
154
|
/** The session's summary, decisions and learnings, for the `session` input. */
|
|
143
155
|
session_text: string | null;
|
|
@@ -72,6 +72,16 @@ export const NO_LINK_ALIASES_FLAG = "checkpoint.ontology.no-link-aliases";
|
|
|
72
72
|
* the model cannot see that `John Smith` already exists.
|
|
73
73
|
*/
|
|
74
74
|
export const FIRST_NAME_PEOPLE_FLAG = "checkpoint.ontology.first-name-people";
|
|
75
|
+
/**
|
|
76
|
+
* A knowledge input does not link to a concept entity whose canonical name is
|
|
77
|
+
* also the canonical name of an entity of another type (`concept/hq-secrets`
|
|
78
|
+
* beside `project/hq-secrets`). The plan carries
|
|
79
|
+
* `skip_ambiguous_concept_links: true`, and apply rejects the link as
|
|
80
|
+
* `ambiguous_type`. CO-46: 34 of the 73 false positives on the knowledge
|
|
81
|
+
* fixtures were such concept links; skipping them raised precision from 0.733
|
|
82
|
+
* to 0.803 and cost recall 0.023 on a replay of six runs.
|
|
83
|
+
*/
|
|
84
|
+
export const SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG = "checkpoint.ontology.skip-ambiguous-concept-links";
|
|
75
85
|
export const PLAN_FILE = "ontology-plan.json";
|
|
76
86
|
export const CANDIDATES_FILE = "ontology-candidates.json";
|
|
77
87
|
export const REPORT_FILE = "ontology-report.json";
|
|
@@ -254,6 +264,7 @@ export async function planOntologyRun(options) {
|
|
|
254
264
|
const hintsOn = await options.readFlag(ENTITY_HINTS_FLAG, company);
|
|
255
265
|
const noLinkAliases = await options.readFlag(NO_LINK_ALIASES_FLAG, company);
|
|
256
266
|
const firstNamePeople = await options.readFlag(FIRST_NAME_PEOPLE_FLAG, company);
|
|
267
|
+
const skipAmbiguousConceptLinks = await options.readFlag(SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG, company);
|
|
257
268
|
const touched = sessionTouched(hqRoot, company, payload.files);
|
|
258
269
|
// A first run takes its boundary from the entity tree, so the ledger is
|
|
259
270
|
// refreshed before merge-back moves any `last_updated` forward.
|
|
@@ -368,6 +379,7 @@ export async function planOntologyRun(options) {
|
|
|
368
379
|
...(multiWriter ? { multi_writer: multiWriter } : {}),
|
|
369
380
|
...(noLinkAliases ? { link_aliases: false } : {}),
|
|
370
381
|
...(firstNamePeople ? { first_name_people: true } : {}),
|
|
382
|
+
...(skipAmbiguousConceptLinks ? { skip_ambiguous_concept_links: true } : {}),
|
|
371
383
|
inputs,
|
|
372
384
|
session_text: sessionText,
|
|
373
385
|
pending_after_run: remaining,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LaneEnvelope, LaneRecord } from "./types.js";
|
|
2
|
+
export interface RegistryApprovalGateResult {
|
|
3
|
+
envelope: LaneEnvelope;
|
|
4
|
+
approvedIdsToConsume: string[];
|
|
5
|
+
violation: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function enforceRegistryApprovalGate(lane: LaneRecord, envelope: LaneEnvelope): RegistryApprovalGateResult;
|
|
8
|
+
//# sourceMappingURL=approval-gate.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
function record(value) {
|
|
2
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
3
|
+
? value
|
|
4
|
+
: undefined;
|
|
5
|
+
}
|
|
6
|
+
function text(value) {
|
|
7
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : undefined;
|
|
8
|
+
}
|
|
9
|
+
function externalActionCheckpoint(artifactValue) {
|
|
10
|
+
const artifact = record(artifactValue);
|
|
11
|
+
if (!artifact)
|
|
12
|
+
return undefined;
|
|
13
|
+
const kind = (text(artifact.kind) ?? text(artifact.type) ?? "").toLowerCase().replace(/[\s-]+/g, "_");
|
|
14
|
+
const status = (text(artifact.status) ?? text(artifact.state) ?? "").toLowerCase();
|
|
15
|
+
const hasDate = (...keys) => keys.some((key) => text(artifact[key]) !== undefined);
|
|
16
|
+
const url = text(artifact.url) ?? "";
|
|
17
|
+
const mergedPr = (["pr", "pull_request", "merged_pr"].includes(kind) ||
|
|
18
|
+
/\/(?:pull|pulls|merge_requests)\/\d+(?:\/|$)/i.test(url)) &&
|
|
19
|
+
(status === "merged" || artifact.merged === true || hasDate("merged_at")) ||
|
|
20
|
+
artifact.merged === true || status === "merged";
|
|
21
|
+
const pushedTag = (["tag", "git_tag"].includes(kind) ||
|
|
22
|
+
text(artifact.tag) !== undefined || text(artifact.tag_name) !== undefined) &&
|
|
23
|
+
(status === "pushed" || artifact.pushed === true || hasDate("pushed_at"));
|
|
24
|
+
const publishedPackage = (["package", "npm_package", "published_package"].includes(kind) ||
|
|
25
|
+
text(artifact.package) !== undefined || text(artifact.package_name) !== undefined) &&
|
|
26
|
+
(status === "published" || artifact.published === true || hasDate("published_at"));
|
|
27
|
+
const deployment = ["deploy", "deployment", "deployed"].includes(kind) &&
|
|
28
|
+
(status === "" || ["deployed", "success", "succeeded", "successful", "complete", "completed", "live"].includes(status) ||
|
|
29
|
+
artifact.deployed === true || hasDate("deployed_at"));
|
|
30
|
+
if (!mergedPr && !pushedTag && !publishedPackage && !deployment)
|
|
31
|
+
return undefined;
|
|
32
|
+
return text(artifact.checkpoint) ?? null;
|
|
33
|
+
}
|
|
34
|
+
function approvedEvents(history, workerId, workerYamlSha256) {
|
|
35
|
+
const consumed = new Set(history.flatMap((entry) => entry.event === "consumed" ? [entry.approval_id] : []));
|
|
36
|
+
return history.filter((entry) => entry.event === "approved" &&
|
|
37
|
+
entry.registry_worker_id === workerId &&
|
|
38
|
+
entry.worker_yaml_sha256 === workerYamlSha256 &&
|
|
39
|
+
!consumed.has(entry.approval_id));
|
|
40
|
+
}
|
|
41
|
+
export function enforceRegistryApprovalGate(lane, envelope) {
|
|
42
|
+
const registryWorker = lane.registry_worker;
|
|
43
|
+
const gateEnabled = registryWorker !== undefined &&
|
|
44
|
+
(registryWorker.approval_required || registryWorker.human_checkpoints.length > 0);
|
|
45
|
+
if (!gateEnabled || envelope.decision !== "done") {
|
|
46
|
+
return { envelope, approvedIdsToConsume: [], violation: false };
|
|
47
|
+
}
|
|
48
|
+
const actionCheckpoints = envelope.artifacts
|
|
49
|
+
.map(externalActionCheckpoint)
|
|
50
|
+
.filter((checkpoint) => checkpoint !== undefined);
|
|
51
|
+
if (actionCheckpoints.length === 0) {
|
|
52
|
+
return { envelope, approvedIdsToConsume: [], violation: false };
|
|
53
|
+
}
|
|
54
|
+
const missingCheckpoint = actionCheckpoints.includes(null);
|
|
55
|
+
const requiredCheckpoints = [...new Set(actionCheckpoints.filter((checkpoint) => checkpoint !== null))];
|
|
56
|
+
const configuredCheckpoints = registryWorker.human_checkpoints.length > 0
|
|
57
|
+
? new Set(registryWorker.human_checkpoints)
|
|
58
|
+
: new Set(["external_publish"]);
|
|
59
|
+
const approvals = approvedEvents(lane.registry_worker_approval_history ?? [], registryWorker.id, registryWorker.worker_yaml_sha256);
|
|
60
|
+
const used = [];
|
|
61
|
+
const missing = missingCheckpoint ? ["(checkpoint missing)"] : [];
|
|
62
|
+
for (const checkpoint of requiredCheckpoints) {
|
|
63
|
+
if (!configuredCheckpoints.has(checkpoint)) {
|
|
64
|
+
missing.push(checkpoint);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const approval = approvals.find((entry) => entry.checkpoint === checkpoint && !used.includes(entry.approval_id));
|
|
68
|
+
if (!approval)
|
|
69
|
+
missing.push(checkpoint);
|
|
70
|
+
else
|
|
71
|
+
used.push(approval.approval_id);
|
|
72
|
+
}
|
|
73
|
+
if (missing.length === 0) {
|
|
74
|
+
return { envelope, approvedIdsToConsume: used, violation: false };
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
envelope: {
|
|
78
|
+
...envelope,
|
|
79
|
+
decision: "blocked",
|
|
80
|
+
summary: `External action requires senior approval at checkpoint: ${missing.join(", ")}.`,
|
|
81
|
+
story_status: "review",
|
|
82
|
+
blocked_reason: "approval_gate_violated",
|
|
83
|
+
story_complete: false,
|
|
84
|
+
story_complete_reason: "The reported external action has no matching recorded approval.",
|
|
85
|
+
},
|
|
86
|
+
approvedIdsToConsume: [],
|
|
87
|
+
violation: true,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=approval-gate.js.map
|