@indigoai-us/hq-cli 5.189.0 → 5.190.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 +25 -0
- package/dist/command-catalog.generated.d.ts +25 -1
- package/dist/command-catalog.generated.js +32 -1
- package/dist/commands/lanes.d.ts +3 -1
- package/dist/commands/lanes.js +135 -16
- package/dist/lib/bot/promotion-upload.js +17 -2
- package/dist/lib/lanes/index.d.ts +2 -0
- package/dist/lib/lanes/index.js +1 -0
- package/dist/lib/lanes/store.js +129 -6
- package/dist/lib/lanes/stories.js +3 -0
- package/dist/lib/lanes/story-ledger.d.ts +78 -0
- package/dist/lib/lanes/story-ledger.js +762 -0
- package/dist/lib/lanes/types.d.ts +10 -1
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.js +2 -0
- package/dist/utils/vault-api.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.190.0] — 2026-09-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `hq lanes` validates story references against project `prd.json` files and records
|
|
10
|
+
explicit `--no-story` reasons. When `lanes.story-ledger` is enabled, lane state,
|
|
11
|
+
blockers, questions, and PR links sync back to project stories. Terminal history
|
|
12
|
+
writes are atomic and retry-safe. Stale-lock cleanup reclaims abandoned ownerless
|
|
13
|
+
locks after their directory ages while preserving fresh replacements.
|
|
14
|
+
`hq lanes status --project` summarizes workers, PRs, blockers, questions, and story
|
|
15
|
+
counts.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- `hq bot promote` accepts server-signed upload URLs without SSE headers when the bucket supplies encryption. SSE header names are checked case-insensitively, and any supplied encryption must be a complete SSE-KMS pair.
|
|
20
|
+
- Company slug lookup errors now point to the supported `hq sources entities` command.
|
|
21
|
+
|
|
5
22
|
## [5.189.0] — 2026-09-25
|
|
6
23
|
|
|
7
24
|
### Added
|
|
@@ -70,6 +87,14 @@
|
|
|
70
87
|
|
|
71
88
|
### Added
|
|
72
89
|
|
|
90
|
+
- `hq lanes` validates story references against project `prd.json` files and records
|
|
91
|
+
explicit `--no-story` reasons. When `lanes.story-ledger` is enabled, lane state,
|
|
92
|
+
blockers, questions, and PR links sync back to project stories. Terminal history
|
|
93
|
+
writes are atomic and retry-safe. Stale-lock cleanup reclaims abandoned ownerless
|
|
94
|
+
locks after their directory ages while preserving fresh replacements.
|
|
95
|
+
`hq lanes status --project` summarizes workers, PRs,
|
|
96
|
+
blockers, questions, and story counts.
|
|
97
|
+
|
|
73
98
|
- `hq doctor --json` includes stable sync `reasonCode` enums. Client-health reports add `checkId: reasonCode` pairs only when the company-scoped `client-health-sync-reason-codes` hq-flags gate is enabled. `hq doctor --fix --json` reports refresh-sync outcomes without raw error details.
|
|
74
99
|
|
|
75
100
|
- Repair known stale QMD collection paths behind the default-off `qmd.collection-path-repair` hq-flags canary while preserving collection metadata.
|
|
@@ -6868,6 +6868,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
6868
6868
|
}, {
|
|
6869
6869
|
readonly flags: "--story <id>";
|
|
6870
6870
|
readonly description: "Story id from the project's prd.json";
|
|
6871
|
+
}, {
|
|
6872
|
+
readonly flags: "--no-story <reason>";
|
|
6873
|
+
readonly description: "Create a lane without a story and record why";
|
|
6871
6874
|
}, {
|
|
6872
6875
|
readonly flags: "--additional-story <id>";
|
|
6873
6876
|
readonly description: "Additional story carried by this lane; repeatable and discouraged when active";
|
|
@@ -6998,7 +7001,28 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
6998
7001
|
readonly description: "Filter by senior ref";
|
|
6999
7002
|
}, {
|
|
7000
7003
|
readonly flags: "--json";
|
|
7001
|
-
readonly description: "JSON on stdout (
|
|
7004
|
+
readonly description: "Emit JSON on stdout (default)";
|
|
7005
|
+
}, {
|
|
7006
|
+
readonly flags: "--human";
|
|
7007
|
+
readonly description: "Emit human-readable output";
|
|
7008
|
+
}];
|
|
7009
|
+
readonly subcommands: readonly [];
|
|
7010
|
+
}, {
|
|
7011
|
+
readonly name: "status";
|
|
7012
|
+
readonly description: "Summarize project stories, lanes, PRs, blockers, and pending questions";
|
|
7013
|
+
readonly aliases: readonly [];
|
|
7014
|
+
readonly hidden: false;
|
|
7015
|
+
readonly usage: "[options]";
|
|
7016
|
+
readonly arguments: readonly [];
|
|
7017
|
+
readonly options: readonly [{
|
|
7018
|
+
readonly flags: "--project <slug>";
|
|
7019
|
+
readonly description: "HQ project slug";
|
|
7020
|
+
}, {
|
|
7021
|
+
readonly flags: "--company <slug|uid>";
|
|
7022
|
+
readonly description: "Company project scope; omit for a personal project";
|
|
7023
|
+
}, {
|
|
7024
|
+
readonly flags: "--json";
|
|
7025
|
+
readonly description: "Emit JSON on stdout";
|
|
7002
7026
|
}];
|
|
7003
7027
|
readonly subcommands: readonly [];
|
|
7004
7028
|
}, {
|
|
@@ -8874,6 +8874,10 @@ export const COMMAND_CATALOG = [
|
|
|
8874
8874
|
"flags": "--story <id>",
|
|
8875
8875
|
"description": "Story id from the project's prd.json"
|
|
8876
8876
|
},
|
|
8877
|
+
{
|
|
8878
|
+
"flags": "--no-story <reason>",
|
|
8879
|
+
"description": "Create a lane without a story and record why"
|
|
8880
|
+
},
|
|
8877
8881
|
{
|
|
8878
8882
|
"flags": "--additional-story <id>",
|
|
8879
8883
|
"description": "Additional story carried by this lane; repeatable and discouraged when active"
|
|
@@ -9044,7 +9048,34 @@ export const COMMAND_CATALOG = [
|
|
|
9044
9048
|
},
|
|
9045
9049
|
{
|
|
9046
9050
|
"flags": "--json",
|
|
9047
|
-
"description": "JSON on stdout (
|
|
9051
|
+
"description": "Emit JSON on stdout (default)"
|
|
9052
|
+
},
|
|
9053
|
+
{
|
|
9054
|
+
"flags": "--human",
|
|
9055
|
+
"description": "Emit human-readable output"
|
|
9056
|
+
}
|
|
9057
|
+
],
|
|
9058
|
+
"subcommands": []
|
|
9059
|
+
},
|
|
9060
|
+
{
|
|
9061
|
+
"name": "status",
|
|
9062
|
+
"description": "Summarize project stories, lanes, PRs, blockers, and pending questions",
|
|
9063
|
+
"aliases": [],
|
|
9064
|
+
"hidden": false,
|
|
9065
|
+
"usage": "[options]",
|
|
9066
|
+
"arguments": [],
|
|
9067
|
+
"options": [
|
|
9068
|
+
{
|
|
9069
|
+
"flags": "--project <slug>",
|
|
9070
|
+
"description": "HQ project slug"
|
|
9071
|
+
},
|
|
9072
|
+
{
|
|
9073
|
+
"flags": "--company <slug|uid>",
|
|
9074
|
+
"description": "Company project scope; omit for a personal project"
|
|
9075
|
+
},
|
|
9076
|
+
{
|
|
9077
|
+
"flags": "--json",
|
|
9078
|
+
"description": "Emit JSON on stdout"
|
|
9048
9079
|
}
|
|
9049
9080
|
],
|
|
9050
9081
|
"subcommands": []
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `hq lanes` — worker lane management and decision queue (spec §5–§7, §10–§11).
|
|
3
3
|
*
|
|
4
|
-
* Existing commands print JSON on stdout and take no interactive input, except
|
|
4
|
+
* Existing commands print JSON on stdout by default and take no interactive input, except
|
|
5
5
|
* `accounts login`, which hands the terminal to the vendor's own sign-in.
|
|
6
6
|
* `watch` and the monitor reminder are the line-oriented text surfaces.
|
|
7
7
|
*/
|
|
@@ -80,6 +80,8 @@ export interface LanesCommandDeps {
|
|
|
80
80
|
lanePlacementEnabled?: () => boolean;
|
|
81
81
|
/** Test seam for the default-off lane hire-grants rollout gate. */
|
|
82
82
|
laneHireGrantsEnabled?: (company?: LaneCompanyRef | null) => boolean | Promise<boolean>;
|
|
83
|
+
/** Test seam for the default-off story ledger flag. */
|
|
84
|
+
storyLedgerEnabled?: () => boolean;
|
|
83
85
|
/** Test seam for the SSM-only Outpost owner grant path. */
|
|
84
86
|
isSsmOwnerDispatch?: () => boolean;
|
|
85
87
|
/** Test seam for changing a lane between pre-lock and locked grant reads. */
|
package/dist/commands/lanes.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `hq lanes` — worker lane management and decision queue (spec §5–§7, §10–§11).
|
|
3
3
|
*
|
|
4
|
-
* Existing commands print JSON on stdout and take no interactive input, except
|
|
4
|
+
* Existing commands print JSON on stdout by default and take no interactive input, except
|
|
5
5
|
* `accounts login`, which hands the terminal to the vendor's own sign-in.
|
|
6
6
|
* `watch` and the monitor reminder are the line-oriented text surfaces.
|
|
7
7
|
*/
|
|
@@ -10,6 +10,7 @@ import { randomBytes } from "node:crypto";
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as os from "node:os";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import { Option } from "commander";
|
|
13
14
|
import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
14
15
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
15
16
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
@@ -29,7 +30,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
|
|
|
29
30
|
import { openMeshTransport } from "../lib/mesh/client.js";
|
|
30
31
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
|
|
31
32
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
32
|
-
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, withHireGrantAdmission, 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, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
33
|
+
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, 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, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
33
34
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
34
35
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
35
36
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
@@ -257,6 +258,7 @@ function resolveDeps(deps = {}) {
|
|
|
257
258
|
fetchText: deps.fetchText ?? defaultFetchText,
|
|
258
259
|
lanePlacementEnabled: deps.lanePlacementEnabled ?? defaultLanePlacementEnabled,
|
|
259
260
|
laneHireGrantsEnabled: deps.laneHireGrantsEnabled ?? defaultLaneHireGrantsEnabled,
|
|
261
|
+
storyLedgerEnabled: deps.storyLedgerEnabled ?? laneStoryLedgerEnabled,
|
|
260
262
|
isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
|
|
261
263
|
withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
|
|
262
264
|
laneClaudeAccountBalanceEnabled: deps.laneClaudeAccountBalanceEnabled ?? defaultLaneClaudeAccountBalanceEnabled,
|
|
@@ -2112,18 +2114,28 @@ async function runCreate(ctx, opts) {
|
|
|
2112
2114
|
const meshCompanyFlag = opts.meshCompany?.trim();
|
|
2113
2115
|
const project = opts.project?.trim();
|
|
2114
2116
|
const story = opts.story?.trim();
|
|
2117
|
+
const noStoryReason = opts.noStory?.trim();
|
|
2115
2118
|
const additionalStories = (opts.additionalStory ?? []).map((value) => value.trim());
|
|
2116
2119
|
const briefFile = opts.briefFile?.trim();
|
|
2117
2120
|
if (!project) {
|
|
2118
2121
|
throw new LanesError("missing_project", "--project is required", {});
|
|
2119
2122
|
}
|
|
2120
|
-
if (
|
|
2123
|
+
if (opts.story !== undefined && opts.noStory !== undefined) {
|
|
2124
|
+
throw new LanesError("story_selection_conflict", "Pass either --story or --no-story, not both", {});
|
|
2125
|
+
}
|
|
2126
|
+
if (opts.noStory !== undefined && !noStoryReason) {
|
|
2127
|
+
throw new LanesError("missing_no_story_reason", "--no-story requires a non-empty reason", {});
|
|
2128
|
+
}
|
|
2129
|
+
if (!story && !noStoryReason) {
|
|
2121
2130
|
throw new LanesError("missing_story", "--story is required", {});
|
|
2122
2131
|
}
|
|
2132
|
+
if (noStoryReason && additionalStories.length > 0) {
|
|
2133
|
+
throw new LanesError("story_selection_conflict", "--additional-story requires --story and cannot be used with --no-story", {});
|
|
2134
|
+
}
|
|
2123
2135
|
if (additionalStories.some((value) => value.length === 0)) {
|
|
2124
2136
|
throw new LanesError("missing_story", "--additional-story values must be non-empty", {});
|
|
2125
2137
|
}
|
|
2126
|
-
const storyIds = [story, ...additionalStories];
|
|
2138
|
+
const storyIds = story ? [story, ...additionalStories] : [];
|
|
2127
2139
|
if (new Set(storyIds).size !== storyIds.length) {
|
|
2128
2140
|
throw new LanesError("duplicate_story", "A lane cannot carry the same project/story more than once", {
|
|
2129
2141
|
project_id: project,
|
|
@@ -2174,6 +2186,9 @@ async function runCreate(ctx, opts) {
|
|
|
2174
2186
|
throw new LanesError("max_lane_depth", `Lane depth ${laneDepth} exceeds cap ${MAX_LANE_DEPTH}`, { depth: laneDepth, max: MAX_LANE_DEPTH });
|
|
2175
2187
|
}
|
|
2176
2188
|
// A no-company lane must not call company resolution (openMeshTransport).
|
|
2189
|
+
if (storyIds.length > 0 && ctx.storyLedgerEnabled()) {
|
|
2190
|
+
validateProjectStories(hqRoot, project, storyIds, company?.slug);
|
|
2191
|
+
}
|
|
2177
2192
|
let meshCompany = company;
|
|
2178
2193
|
if (meshCompanyFlag) {
|
|
2179
2194
|
const resolvedMeshCompany = await ctx.resolveCompany(meshCompanyFlag);
|
|
@@ -2189,7 +2204,7 @@ async function runCreate(ctx, opts) {
|
|
|
2189
2204
|
const seniorCompany = companyOfSenior(hqRoot, senior, ctx);
|
|
2190
2205
|
assertSameTenant(company, senior, seniorCompany);
|
|
2191
2206
|
const createStoryWarnings = [];
|
|
2192
|
-
if (meshCompany) {
|
|
2207
|
+
if (meshCompany && storyIds.length > 0) {
|
|
2193
2208
|
const warning = await storyBoardWarning(ctx, meshCompany, project, storyIds);
|
|
2194
2209
|
if (warning !== undefined)
|
|
2195
2210
|
createStoryWarnings.push(warning);
|
|
@@ -2630,7 +2645,8 @@ async function runCreate(ctx, opts) {
|
|
|
2630
2645
|
? { mesh_company: meshCompany }
|
|
2631
2646
|
: {}),
|
|
2632
2647
|
project_id: project,
|
|
2633
|
-
story_id: story,
|
|
2648
|
+
story_id: story ?? "",
|
|
2649
|
+
...(noStoryReason ? { no_story_reason: noStoryReason } : {}),
|
|
2634
2650
|
stories: storyIds.map((storyId) => ({
|
|
2635
2651
|
project_id: project,
|
|
2636
2652
|
story_id: storyId,
|
|
@@ -3416,6 +3432,9 @@ async function runPickup(ctx, opts) {
|
|
|
3416
3432
|
});
|
|
3417
3433
|
}
|
|
3418
3434
|
async function runList(ctx, opts) {
|
|
3435
|
+
if (opts.json && opts.human) {
|
|
3436
|
+
throw new LanesError("conflicting_output_format", "Use either --json or --human for hq lanes list", { json: true, human: true });
|
|
3437
|
+
}
|
|
3419
3438
|
const hqRoot = ctx.hqRoot();
|
|
3420
3439
|
const state = opts.state ? parseLaneState(opts.state) : undefined;
|
|
3421
3440
|
const senior = opts.senior ? parseSeniorRef(opts.senior) : undefined;
|
|
@@ -3430,9 +3449,11 @@ async function runList(ctx, opts) {
|
|
|
3430
3449
|
.filter((lane) => {
|
|
3431
3450
|
if (state && lane.state !== state)
|
|
3432
3451
|
return false;
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3452
|
+
const carriesFilter = laneStories(lane).some((story) => (!opts.project || story.project_id === opts.project) &&
|
|
3453
|
+
(!opts.story || story.story_id === opts.story));
|
|
3454
|
+
const matchingNoStory = !opts.story && opts.project !== undefined &&
|
|
3455
|
+
lane.no_story_reason !== undefined && lane.project_id === opts.project;
|
|
3456
|
+
if ((opts.project || opts.story) && !carriesFilter && !matchingNoStory) {
|
|
3436
3457
|
return false;
|
|
3437
3458
|
}
|
|
3438
3459
|
if (company && !recordMatchesCompany(lane, company)) {
|
|
@@ -3465,6 +3486,9 @@ async function runList(ctx, opts) {
|
|
|
3465
3486
|
return {
|
|
3466
3487
|
...lane,
|
|
3467
3488
|
...hireByLane.get(lane.lane_id),
|
|
3489
|
+
stories: laneStories(lane),
|
|
3490
|
+
story_ids: laneStories(lane).map((story) => story.story_id),
|
|
3491
|
+
no_story_reason: lane.no_story_reason ?? null,
|
|
3468
3492
|
...(remoteQueued
|
|
3469
3493
|
? { state: "remote", remote_state: lane.state, queued_reason: undefined }
|
|
3470
3494
|
: {}),
|
|
@@ -3486,12 +3510,74 @@ async function runList(ctx, opts) {
|
|
|
3486
3510
|
const host_capacity = ctx.lanePlacementEnabled()
|
|
3487
3511
|
? (await loadHostCapacitySnapshots(ctx, hqRoot)).entries
|
|
3488
3512
|
: undefined;
|
|
3489
|
-
|
|
3513
|
+
const result = {
|
|
3490
3514
|
ok: true,
|
|
3491
3515
|
action: "list",
|
|
3492
3516
|
lanes,
|
|
3493
3517
|
...(host_capacity !== undefined ? { host_capacity } : {}),
|
|
3518
|
+
};
|
|
3519
|
+
if (opts.human)
|
|
3520
|
+
console.log(formatLaneListHuman(lanes));
|
|
3521
|
+
else
|
|
3522
|
+
printJson(result);
|
|
3523
|
+
}
|
|
3524
|
+
function formatLaneListHuman(lanes) {
|
|
3525
|
+
if (lanes.length === 0)
|
|
3526
|
+
return "No lanes.";
|
|
3527
|
+
return lanes.map((lane) => {
|
|
3528
|
+
const ids = lane.story_ids;
|
|
3529
|
+
const storyLabel = ids.length > 0
|
|
3530
|
+
? "stories: " + ids.join(", ")
|
|
3531
|
+
: "no story: " + (lane.no_story_reason ?? "reason not recorded");
|
|
3532
|
+
return lane.lane_id + " " + lane.state + " " + lane.provider + " " + lane.project_id +
|
|
3533
|
+
" " + storyLabel;
|
|
3534
|
+
}).join("\n");
|
|
3535
|
+
}
|
|
3536
|
+
async function runStatus(ctx, opts) {
|
|
3537
|
+
if (!ctx.storyLedgerEnabled()) {
|
|
3538
|
+
throw new LanesError("story_ledger_disabled", "hq lanes status requires the lanes.story-ledger flag", {
|
|
3539
|
+
flag: "lanes.story-ledger",
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
const hqRoot = ctx.hqRoot();
|
|
3543
|
+
const company = opts.company ? await ctx.resolveCompany(opts.company) : undefined;
|
|
3544
|
+
const lanes = loadAllLanes(hqRoot).filter((lane) => lane.company?.slug === company?.slug);
|
|
3545
|
+
const pending = new Map();
|
|
3546
|
+
for (const lane of lanes) {
|
|
3547
|
+
const refs = [];
|
|
3548
|
+
for (const questionId of lane.pending_question_ids ?? []) {
|
|
3549
|
+
const question = readQuestion(hqRoot, questionId);
|
|
3550
|
+
const sameCompany = question.company === null
|
|
3551
|
+
? lane.company === null
|
|
3552
|
+
: lane.company !== null &&
|
|
3553
|
+
question.company.uid === lane.company.uid &&
|
|
3554
|
+
question.company.slug === lane.company.slug;
|
|
3555
|
+
if (question.lane_id !== lane.lane_id || !sameCompany) {
|
|
3556
|
+
throw new LanesError("story_ledger_question_mismatch", `Pending question ${questionId} does not belong to lane ${lane.lane_id}`, { lane_id: lane.lane_id, question_id: questionId, question_lane_id: question.lane_id });
|
|
3557
|
+
}
|
|
3558
|
+
if (question.status === "pending") {
|
|
3559
|
+
refs.push({
|
|
3560
|
+
question_id: question.question_id,
|
|
3561
|
+
project_id: question.project_id,
|
|
3562
|
+
story_id: question.story_id,
|
|
3563
|
+
});
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
pending.set(lane.lane_id, refs);
|
|
3567
|
+
}
|
|
3568
|
+
const snapshot = buildLaneStatusSnapshot({
|
|
3569
|
+
hqRoot,
|
|
3570
|
+
projectId: opts.project,
|
|
3571
|
+
...(company ? { companySlug: company.slug } : {}),
|
|
3572
|
+
...(company ? { companyUid: company.uid } : {}),
|
|
3573
|
+
lanes,
|
|
3574
|
+
generatedAt: ctx.now().toISOString(),
|
|
3575
|
+
pendingQuestions: pending,
|
|
3494
3576
|
});
|
|
3577
|
+
if (opts.json)
|
|
3578
|
+
printJson(snapshot);
|
|
3579
|
+
else
|
|
3580
|
+
console.log(formatLaneStatusHuman(snapshot));
|
|
3495
3581
|
}
|
|
3496
3582
|
async function runCapacity(ctx) {
|
|
3497
3583
|
const hqRoot = ctx.hqRoot();
|
|
@@ -4736,6 +4822,12 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
4736
4822
|
const at = ctx.now().toISOString();
|
|
4737
4823
|
const projectId = opts.project?.trim() || lane.project_id;
|
|
4738
4824
|
const storyId = opts.story?.trim() || lane.story_id;
|
|
4825
|
+
if (opts.story !== undefined && !opts.story.trim()) {
|
|
4826
|
+
throw new LanesError("missing_story", "--story must be non-empty", {});
|
|
4827
|
+
}
|
|
4828
|
+
if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
|
|
4829
|
+
validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
|
|
4830
|
+
}
|
|
4739
4831
|
const dropboxMessage = {
|
|
4740
4832
|
at,
|
|
4741
4833
|
message_id: messageId,
|
|
@@ -4889,6 +4981,7 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
4889
4981
|
...current,
|
|
4890
4982
|
project_id: projectId,
|
|
4891
4983
|
story_id: storyId,
|
|
4984
|
+
no_story_reason: storyId ? undefined : current.no_story_reason,
|
|
4892
4985
|
stories: laneStory(current, projectId, storyId)
|
|
4893
4986
|
? laneStories(current)
|
|
4894
4987
|
: [
|
|
@@ -5190,6 +5283,12 @@ async function runResume(ctx, laneId, opts) {
|
|
|
5190
5283
|
const at = ctx.now().toISOString();
|
|
5191
5284
|
const projectId = opts.project?.trim() || lane.project_id;
|
|
5192
5285
|
const storyId = opts.story?.trim() || lane.story_id;
|
|
5286
|
+
if (opts.story !== undefined && !opts.story.trim()) {
|
|
5287
|
+
throw new LanesError("missing_story", "--story must be non-empty", {});
|
|
5288
|
+
}
|
|
5289
|
+
if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
|
|
5290
|
+
validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
|
|
5291
|
+
}
|
|
5193
5292
|
const attributionChanged = projectId !== lane.project_id || storyId !== lane.story_id;
|
|
5194
5293
|
const messageId = text !== undefined || attributionChanged ? mintMessageId() : undefined;
|
|
5195
5294
|
let requestedMeshCompany;
|
|
@@ -5370,6 +5469,7 @@ async function runResume(ctx, laneId, opts) {
|
|
|
5370
5469
|
...current,
|
|
5371
5470
|
project_id: projectId,
|
|
5372
5471
|
story_id: storyId,
|
|
5472
|
+
no_story_reason: storyId ? undefined : current.no_story_reason,
|
|
5373
5473
|
stories: laneStory(current, projectId, storyId)
|
|
5374
5474
|
? laneStories(current)
|
|
5375
5475
|
: [
|
|
@@ -5460,8 +5560,7 @@ function addCarriedStory(lane, projectId, storyId, at) {
|
|
|
5460
5560
|
}
|
|
5461
5561
|
return {
|
|
5462
5562
|
...lane,
|
|
5463
|
-
project_id: projectId,
|
|
5464
|
-
story_id: storyId,
|
|
5563
|
+
...(lane.story_id.trim() ? {} : { project_id: projectId, story_id: storyId }),
|
|
5465
5564
|
stories: [
|
|
5466
5565
|
...laneStories(lane),
|
|
5467
5566
|
{ project_id: projectId, story_id: storyId, status: "in_progress", added_at: at, updated_at: at },
|
|
@@ -5521,7 +5620,7 @@ async function emitStoryTransition(ctx, lane, projectId, storyId, event, status,
|
|
|
5521
5620
|
const company = lane.mesh_company ?? lane.company;
|
|
5522
5621
|
if (!company)
|
|
5523
5622
|
return;
|
|
5524
|
-
const ready = await ensureStoryThread(ctx, lane, projectId);
|
|
5623
|
+
const ready = await ensureStoryThread(ctx, lane, projectId, storyId);
|
|
5525
5624
|
if (!ready.thread_id)
|
|
5526
5625
|
return;
|
|
5527
5626
|
await emitLaneMeshEvent(ctx, company, ready.thread_id, event, {
|
|
@@ -5593,6 +5692,9 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
|
|
|
5593
5692
|
const normalizedStoryId = storyId.trim();
|
|
5594
5693
|
if (!normalizedStoryId)
|
|
5595
5694
|
throw new LanesError("missing_story", "<story-id> is required", {});
|
|
5695
|
+
if (ctx.storyLedgerEnabled()) {
|
|
5696
|
+
validateProjectStories(hqRoot, projectId, [normalizedStoryId], lane.company?.slug);
|
|
5697
|
+
}
|
|
5596
5698
|
const company = lane.mesh_company ?? lane.company;
|
|
5597
5699
|
const warnings = [];
|
|
5598
5700
|
if (company) {
|
|
@@ -5608,7 +5710,11 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
|
|
|
5608
5710
|
const before = lane;
|
|
5609
5711
|
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
5610
5712
|
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
5611
|
-
|
|
5713
|
+
const added = addCarriedStory(current, projectId, normalizedStoryId, at);
|
|
5714
|
+
return {
|
|
5715
|
+
...added,
|
|
5716
|
+
no_story_reason: undefined,
|
|
5717
|
+
};
|
|
5612
5718
|
});
|
|
5613
5719
|
const transitionWarning = await commitStoryTransition(ctx, before, next, projectId, normalizedStoryId, "lane_started", "in_progress");
|
|
5614
5720
|
if (transitionWarning !== undefined)
|
|
@@ -6953,13 +7059,16 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
6953
7059
|
.command("lanes", { hidden: true })
|
|
6954
7060
|
.description("Create, supervise, and steer worker lanes; routed decision queue");
|
|
6955
7061
|
registerOutpostPrunerCommand(lanes);
|
|
7062
|
+
const noStoryOption = new Option("--no-story <reason>", "Create a lane without a story and record why");
|
|
7063
|
+
noStoryOption.attributeName = () => "noStory";
|
|
6956
7064
|
lanes
|
|
6957
7065
|
.command("create")
|
|
6958
7066
|
.description("Register a lane, run admission, and return immediately")
|
|
6959
7067
|
.option("--company <slug|uid>", "Company slug or cloud uid; omit for a core/personal lane")
|
|
6960
7068
|
.option("--mesh-company <slug|uid>", "Explicit Work Mesh company for a personal lane; membership is verified")
|
|
6961
7069
|
.requiredOption("--project <slug>", "HQ project slug")
|
|
6962
|
-
.
|
|
7070
|
+
.option("--story <id>", "Story id from the project's prd.json")
|
|
7071
|
+
.addOption(noStoryOption)
|
|
6963
7072
|
.option("--additional-story <id>", "Additional story carried by this lane; repeatable and discouraged when active", collectRepeatable)
|
|
6964
7073
|
.requiredOption("--brief-file <path>", "Path to the lane brief")
|
|
6965
7074
|
.option("--worker <name>", "HQ worker or reusable profile id")
|
|
@@ -7009,10 +7118,20 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7009
7118
|
.option("--story <id>", "Filter by story")
|
|
7010
7119
|
.option("--state <state>", `Filter by state (${LANE_STATES.join("|")})`)
|
|
7011
7120
|
.option("--senior <ref>", "Filter by senior ref")
|
|
7012
|
-
.option("--json", "JSON on stdout (
|
|
7121
|
+
.option("--json", "Emit JSON on stdout (default)")
|
|
7122
|
+
.option("--human", "Emit human-readable output")
|
|
7013
7123
|
.action(wrap(async (opts) => {
|
|
7014
7124
|
await runList(ctx, opts);
|
|
7015
7125
|
}));
|
|
7126
|
+
lanes
|
|
7127
|
+
.command("status")
|
|
7128
|
+
.description("Summarize project stories, lanes, PRs, blockers, and pending questions")
|
|
7129
|
+
.requiredOption("--project <slug>", "HQ project slug")
|
|
7130
|
+
.option("--company <slug|uid>", "Company project scope; omit for a personal project")
|
|
7131
|
+
.option("--json", "Emit JSON on stdout")
|
|
7132
|
+
.action(wrap(async (opts) => {
|
|
7133
|
+
await runStatus(ctx, opts);
|
|
7134
|
+
}));
|
|
7016
7135
|
lanes
|
|
7017
7136
|
.command("capacity")
|
|
7018
7137
|
.description("Report this host's lane capacity by provider and account")
|
|
@@ -7,6 +7,22 @@ function assertState(state, bundle, companyUid, operationId) {
|
|
|
7
7
|
state.phase !== "preparing")
|
|
8
8
|
throw new Error("Promotion preparation returned a different identity or phase");
|
|
9
9
|
}
|
|
10
|
+
function hasValidEncryptionHeaders(headers) {
|
|
11
|
+
const valuesFor = (name) => Object.entries(headers)
|
|
12
|
+
.filter(([key]) => key.toLowerCase() === name)
|
|
13
|
+
.map(([, value]) => value);
|
|
14
|
+
const encryptionValues = valuesFor("x-amz-server-side-encryption");
|
|
15
|
+
const keyIdValues = valuesFor("x-amz-server-side-encryption-aws-kms-key-id");
|
|
16
|
+
// Multiple differently-cased copies are ambiguous, even when their values
|
|
17
|
+
// happen to match. Accept one exact SSE-KMS pair or no effective SSE values.
|
|
18
|
+
if (encryptionValues.length > 1 || keyIdValues.length > 1)
|
|
19
|
+
return false;
|
|
20
|
+
const encryption = encryptionValues[0] ?? "";
|
|
21
|
+
const keyId = keyIdValues[0] ?? "";
|
|
22
|
+
if (!encryption.trim() && !keyId.trim())
|
|
23
|
+
return true;
|
|
24
|
+
return encryption === "aws:kms" && Boolean(keyId.trim());
|
|
25
|
+
}
|
|
10
26
|
/** Only the presigned object request receives these bytes. Owner authorization
|
|
11
27
|
* is handled by BotApi and is never forwarded to S3 or a redirect destination.
|
|
12
28
|
*/
|
|
@@ -15,8 +31,7 @@ async function uploadObject(upload, bytes, fetchImpl) {
|
|
|
15
31
|
if (url.protocol !== "https:" || url.username || url.password || url.port ||
|
|
16
32
|
!/^[a-z0-9.-]+\.s3(?:[.-][a-z0-9-]+)?\.amazonaws\.com$/.test(url.hostname) ||
|
|
17
33
|
upload.method !== "PUT" || upload.bytes !== bytes.length || upload.sha256 !== sha(bytes) ||
|
|
18
|
-
upload.headers
|
|
19
|
-
!upload.headers["x-amz-server-side-encryption-aws-kms-key-id"] ||
|
|
34
|
+
!hasValidEncryptionHeaders(upload.headers) ||
|
|
20
35
|
upload.headers["if-none-match"] !== "*" ||
|
|
21
36
|
upload.headers["x-amz-checksum-sha256"] !== Buffer.from(upload.sha256, "hex").toString("base64") ||
|
|
22
37
|
Object.keys(upload.headers).some(key => /^(authorization|cookie)$/i.test(key))) {
|
|
@@ -16,6 +16,8 @@ export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFT
|
|
|
16
16
|
export type { ConfigLoadDeps, ConfigWriteDeps } from "./config.js";
|
|
17
17
|
export { resolveLanesHqRoot } from "./hq-root.js";
|
|
18
18
|
export type { LanesHqRootOpts } from "./hq-root.js";
|
|
19
|
+
export { LANE_STORY_LEDGER_FLAG, buildLaneStatusSnapshot, formatLaneStatusHuman, laneStoryLedgerEnabled, resolveProjectStoryLedger, syncLaneToProjectStoryLedger, validateProjectStories, } from "./story-ledger.js";
|
|
20
|
+
export type { LaneStatusSnapshot, ProjectStoryLedger, StoryLedgerWriteDeps } from "./story-ledger.js";
|
|
19
21
|
export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
20
22
|
export type { OutpostDispatchResult, OutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
21
23
|
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, withLaneHireLocks, } from "./store.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HIS
|
|
|
10
10
|
export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory, storyKey, storyStatusForEnvelope, storyWarning, withCurrentStory, withLaneStories, } from "./stories.js";
|
|
11
11
|
export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerAutoCompactTokens, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
|
|
12
12
|
export { resolveLanesHqRoot } from "./hq-root.js";
|
|
13
|
+
export { LANE_STORY_LEDGER_FLAG, buildLaneStatusSnapshot, formatLaneStatusHuman, laneStoryLedgerEnabled, resolveProjectStoryLedger, syncLaneToProjectStoryLedger, validateProjectStories, } from "./story-ledger.js";
|
|
13
14
|
export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
14
15
|
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, withLaneHireLocks, } from "./store.js";
|
|
15
16
|
export { DEFAULT_LANE_WATCH_INTERVAL_MS, MAX_LANE_WATCH_INTERVAL_MS, MAX_WATCHED_LANES, MAX_WATCH_OUTPUT_LINES, runLaneWatch, } from "./watch.js";
|