@indigoai-us/hq-cli 5.192.2 → 5.193.1

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 CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.193.1] — 2026-09-25
6
+
7
+ ### Fixed
8
+
9
+ - Work Mesh project sync maps ledger states (`todo` to `queued`, `in_progress` to
10
+ `in_progress`, `in_review`/`verification`/`blocked` to `review`, and `done`/`dropped`
11
+ to `done`), with explicit `status` taking precedence and `passes` preserved. PRD
12
+ sync emits review transitions; Board readers preserve unknown status strings and
13
+ ignore unknown ledger states. `hq mesh project ensure --dry-run` previews merged
14
+ Board statuses without writing.
15
+ - Claude lane balance reports now normalize Outpost lane-list and queued show
16
+ views before strict lane-record validation, including `no_story_reason: null`.
17
+
18
+ ## [5.193.0] — 2026-09-25
19
+
20
+ ### Fixed
21
+
22
+ - `hq secrets set` now handles a closed stdin descriptor during terminal cleanup.
23
+
24
+ ### Improved
25
+
26
+ - Sentry reports for failed skill-wrapper replacement now include bounded ownership-state tags.
27
+
5
28
  ## [5.192.2] — 2026-09-25
6
29
 
7
30
  ### Fixed
@@ -5777,6 +5777,9 @@ export declare const COMMAND_CATALOG: readonly [{
5777
5777
  }, {
5778
5778
  readonly flags: "--stories-file <path>";
5779
5779
  readonly description: "PRD JSON file with userStories and metadata.repos";
5780
+ }, {
5781
+ readonly flags: "--dry-run";
5782
+ readonly description: "Read the current Board and print computed story statuses without writing";
5780
5783
  }, {
5781
5784
  readonly flags: "--json";
5782
5785
  readonly description: "Print {projectId, threadId, channelId, version, stories}";
@@ -7458,6 +7458,10 @@ export const COMMAND_CATALOG = [
7458
7458
  "flags": "--stories-file <path>",
7459
7459
  "description": "PRD JSON file with userStories and metadata.repos"
7460
7460
  },
7461
+ {
7462
+ "flags": "--dry-run",
7463
+ "description": "Read the current Board and print computed story statuses without writing"
7464
+ },
7461
7465
  {
7462
7466
  "flags": "--json",
7463
7467
  "description": "Print {projectId, threadId, channelId, version, stories}"
@@ -534,6 +534,30 @@ function ownerErrorFromOutput(stdout) {
534
534
  : `Outpost owner returned ${owner.error}`;
535
535
  return { error: owner.error, message, details };
536
536
  }
537
+ const LANE_RECORD_VIEW_FIELDS = [
538
+ "story_ids",
539
+ "hire_alive_children",
540
+ "hire",
541
+ "hire_summary",
542
+ "hire_source",
543
+ "hire_warning",
544
+ "senior_lost",
545
+ "remote_state",
546
+ ];
547
+ /** Remove presentation-only fields before validating a remote lane snapshot. */
548
+ function normalizeLaneRecordView(raw) {
549
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
550
+ return raw;
551
+ const normalized = { ...raw };
552
+ if (normalized.state === "remote" && normalized.remote_state !== undefined) {
553
+ normalized.state = normalized.remote_state;
554
+ }
555
+ for (const field of LANE_RECORD_VIEW_FIELDS)
556
+ delete normalized[field];
557
+ if (normalized.no_story_reason === null)
558
+ delete normalized.no_story_reason;
559
+ return normalized;
560
+ }
537
561
  async function ownerLaneHireFields(ctx, lane) {
538
562
  if (lane.host.kind !== "outpost") {
539
563
  throw new LanesError("outpost_lane_hire_summary_invalid", "Owner summary requires an Outpost-hosted lane", {
@@ -567,7 +591,7 @@ async function ownerLaneHireFields(ctx, lane) {
567
591
  }
568
592
  let ownerLane;
569
593
  try {
570
- ownerLane = parseLaneRecord(remote.lane);
594
+ ownerLane = parseLaneRecord(normalizeLaneRecordView(remote.lane));
571
595
  }
572
596
  catch (err) {
573
597
  throw new LanesError("outpost_lane_hire_summary_invalid", `Outpost ${outpostId} returned an invalid lane snapshot for ${lane.lane_id}`, { lane_id: lane.lane_id, outpost_id: outpostId, cause: dispatchErrorMessage(err) });
@@ -890,11 +914,8 @@ async function readOutpostLaneRecords(ctx, outpostId) {
890
914
  typeof rowHost.id === "string" && rowHost.id.trim() !== "";
891
915
  if (!belongsToOutpost && !hasLocalHostIdentity)
892
916
  continue;
893
- const normalized = raw.state === "remote" && raw.remote_state !== undefined
894
- ? { ...raw, state: raw.remote_state }
895
- : raw;
896
917
  try {
897
- lanes.push(parseLaneRecord(normalized));
918
+ lanes.push(parseLaneRecord(normalizeLaneRecordView(raw)));
898
919
  }
899
920
  catch (err) {
900
921
  throw new LanesError("outpost_lane_list_invalid", `Outpost ${outpostId} returned an invalid owned lane record: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId, index });
@@ -14,7 +14,8 @@ import chalk from "chalk";
14
14
  import { loadCachedTokens, isMachineIdentity } from "../utils/cognito-session.js";
15
15
  import * as readline from "node:readline/promises";
16
16
  import { STORY_STATUSES, appendThreadEvent, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
17
- import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard, formatStoryStatusChanges, mergeProjectStories, prdSyncEventsThatChangeBoard, } from "../lib/mesh/project-board.js";
17
+ import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard, boardStoryStatus, formatStoryStatusChanges, mergeProjectStories, prdSyncEventsThatChangeBoard, } from "../lib/mesh/project-board.js";
18
+ import { mapSourceStoryStatus } from "../lib/mesh/story-status.js";
18
19
  import { buildPreflightFixtures } from "../lib/work-context/preflight-contract.js";
19
20
  import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
20
21
  import { clearDefaultCompany, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, } from "../lib/work-context/config.js";
@@ -258,10 +259,7 @@ function projectStoriesFromJson(value, source) {
258
259
  if (!story || !id || !title) {
259
260
  fail(`${source} userStories[${index}] requires string id and title.`);
260
261
  }
261
- const status = typeof story.status === "string" ? story.status.trim() : undefined;
262
- if (status && !STORY_STATUSES.has(status)) {
263
- fail(`${source} userStories[${index}].status must be queued|in_progress|review|done.`);
264
- }
262
+ const status = mapSourceStoryStatus(story, `${source} userStories[${index}]`);
265
263
  const resolvedStatus = status || (story.passes === true ? "done" : undefined);
266
264
  return {
267
265
  id,
@@ -475,6 +473,42 @@ async function runProjectEnsure(projectId, opts) {
475
473
  if (httpStatus(err) !== 404)
476
474
  throw err;
477
475
  }
476
+ if (opts.dryRun) {
477
+ if (current && (current.companyUid !== company.companyUid || current.projectId !== projectId)) {
478
+ fail("Work mesh project ensure dry-run: Board company or project id did not match the request.");
479
+ }
480
+ const stories = current ? mergeProjectStories(current.stories, source.stories) : source.stories;
481
+ const sourceStatuses = source.stories.map((story) => ({
482
+ id: story.id,
483
+ title: story.title,
484
+ status: boardStoryStatus(story),
485
+ }));
486
+ const boardStatuses = stories.map((story) => ({
487
+ id: story.id,
488
+ title: story.title,
489
+ status: boardStoryStatus(story),
490
+ }));
491
+ if (opts.json) {
492
+ console.log(JSON.stringify({
493
+ ok: true,
494
+ action: "project.ensure.dry-run",
495
+ projectId,
496
+ companyUid: company.companyUid,
497
+ sourceStatuses,
498
+ stories: boardStatuses,
499
+ }, null, 2));
500
+ }
501
+ else {
502
+ console.log(`Work mesh: project ${projectId} dry run (${sourceStatuses.length} source stories)`);
503
+ console.log("Source statuses:");
504
+ for (const story of sourceStatuses)
505
+ console.log(`${story.id}\t${story.status}`);
506
+ console.log("Merged Board statuses:");
507
+ for (const story of boardStatuses)
508
+ console.log(`${story.id}\t${story.status}`);
509
+ }
510
+ return;
511
+ }
478
512
  // Genesis thread, then the view, then register. Register returns 403
479
513
  // PROJECT_NOT_IN_COMPANY until both exist. A missing view uses that chain
480
514
  // from ensureProjectBoard; an existing view is merged, then registered.
@@ -659,7 +693,11 @@ async function runPrdSync(opts) {
659
693
  source: "hooks",
660
694
  taskId: event.taskId,
661
695
  status: event.status,
662
- summary: event.status === "done" ? `${event.taskId} passes` : `${event.taskId} in progress`,
696
+ summary: event.status === "done"
697
+ ? `${event.taskId} passes`
698
+ : event.status === "in_progress"
699
+ ? `${event.taskId} in progress`
700
+ : `${event.taskId} in review`,
663
701
  evidence: { ...evidenceBase, task: event.taskId },
664
702
  }));
665
703
  const poster = createEmitPoster({ token, baseUrl: resolveVaultApiBase(process.env) });
@@ -1855,6 +1893,7 @@ export function registerMeshCommand(program) {
1855
1893
  .description("Idempotently register a PRD on the Board and verify the view")
1856
1894
  .requiredOption("--company <slug|uid>", "Company slug or cloud uid")
1857
1895
  .requiredOption("--stories-file <path>", "PRD JSON file with userStories and metadata.repos")
1896
+ .option("--dry-run", "Read the current Board and print computed story statuses without writing")
1858
1897
  .option("--json", "Print {projectId, threadId, channelId, version, stories}")
1859
1898
  .action((projectId, opts) => wrap(() => runProjectEnsure(projectId, opts))());
1860
1899
  project
@@ -188,12 +188,23 @@ function promptSecretInteractively() {
188
188
  let value = "";
189
189
  process.stdin.setEncoding("utf8");
190
190
  const cleanup = () => {
191
- if (process.stdin.isTTY) {
192
- process.stdin.setRawMode(false);
191
+ try {
192
+ if (process.stdin.isTTY) {
193
+ try {
194
+ process.stdin.setRawMode(false);
195
+ }
196
+ catch (error) {
197
+ if (error?.code !== "EBADF") {
198
+ throw error;
199
+ }
200
+ }
201
+ }
202
+ }
203
+ finally {
204
+ process.stdin.removeListener("data", onData);
205
+ process.stdin.removeListener("end", onEnd);
206
+ process.stdin.pause();
193
207
  }
194
- process.stdin.removeListener("data", onData);
195
- process.stdin.removeListener("end", onEnd);
196
- process.stdin.pause();
197
208
  };
198
209
  const onEnd = () => {
199
210
  cleanup();
@@ -1,4 +1,13 @@
1
1
  import * as fs from "node:fs";
2
+ export interface CompanySkillWrapperOwnershipDiagnostics {
3
+ kind: "directory" | "symlink" | "file" | "other";
4
+ bridgeState: "all_live" | "partial" | "none" | "empty" | "not_applicable";
5
+ markerState: "bound" | "missing" | "invalid" | "unbound" | "unavailable" | "not_applicable";
6
+ linkTarget: "company_skills" | "outside_company_skills" | "unavailable" | "not_applicable";
7
+ platform: "win32" | "darwin" | "linux" | "other";
8
+ nameEncoding: "percent_encoded" | "literal";
9
+ }
10
+ export declare function companySkillWrapperSentryTagsFor(error: unknown): Record<string, string> | undefined;
2
11
  export interface SurfaceCompanySkillInput {
3
12
  hqRoot: string;
4
13
  companySlug: string;
@@ -62,20 +62,6 @@ function symlinkTarget(linkPath) {
62
62
  function generatedMarkerPath(wrapperPath) {
63
63
  return path.join(path.dirname(wrapperPath), WRAPPER_MARKER_DIR, `${path.basename(wrapperPath)}.json`);
64
64
  }
65
- function hasBoundGeneratedMarker(wrapperPath, companySlug) {
66
- try {
67
- const marker = JSON.parse(fs.readFileSync(generatedMarkerPath(wrapperPath), "utf8"));
68
- const wrapper = fs.lstatSync(wrapperPath);
69
- return (marker.version === 2 &&
70
- marker.companySlug === companySlug &&
71
- marker.wrapperDev === wrapper.dev &&
72
- marker.wrapperIno === wrapper.ino &&
73
- marker.wrapperBirthtimeMs === wrapper.birthtimeMs);
74
- }
75
- catch {
76
- return false;
77
- }
78
- }
79
65
  function writeGeneratedMarker(wrapperPath, companySlug, skillSlug) {
80
66
  const markerPath = generatedMarkerPath(wrapperPath);
81
67
  try {
@@ -104,21 +90,116 @@ function removeGeneratedMarker(wrapperPath) {
104
90
  // never sufficient to surface content on its own.
105
91
  }
106
92
  }
107
- function isGeneratedNamespacedWrapper(wrapperPath, hqRoot, companySlug) {
93
+ const COMPANY_SKILL_WRAPPER_DIAGNOSTICS = Symbol("company-skill-wrapper-diagnostics");
94
+ function companySkillWrapperOwnershipError(wrapperPath, diagnostics) {
95
+ const error = new Error(`Cannot replace '${wrapperPath}' because it is not an HQ-generated skill wrapper.`);
96
+ Object.defineProperty(error, COMPANY_SKILL_WRAPPER_DIAGNOSTICS, {
97
+ value: diagnostics,
98
+ });
99
+ return error;
100
+ }
101
+ export function companySkillWrapperSentryTagsFor(error) {
102
+ if (!(error instanceof Error))
103
+ return undefined;
104
+ const diagnostics = error[COMPANY_SKILL_WRAPPER_DIAGNOSTICS];
105
+ if (!diagnostics)
106
+ return undefined;
107
+ return {
108
+ hq_cli_skill_wrapper_kind: diagnostics.kind,
109
+ hq_cli_skill_wrapper_bridge_state: diagnostics.bridgeState,
110
+ hq_cli_skill_wrapper_marker_state: diagnostics.markerState,
111
+ hq_cli_skill_wrapper_link_target: diagnostics.linkTarget,
112
+ hq_cli_skill_wrapper_platform: diagnostics.platform,
113
+ hq_cli_skill_wrapper_name_encoding: diagnostics.nameEncoding,
114
+ };
115
+ }
116
+ function inspectGeneratedNamespacedWrapper(wrapperPath, hqRoot, companySlug) {
108
117
  const companySkillsRoot = path.join(hqRoot, "companies", companySlug, "skills");
109
- const wrapperTarget = symlinkTarget(wrapperPath);
110
- if (wrapperTarget)
111
- return isWithin(companySkillsRoot, wrapperTarget);
112
- const wrapper = lstat(wrapperPath);
113
- if (!wrapper?.isDirectory())
114
- return false;
118
+ const wrapperEntry = lstat(wrapperPath);
119
+ if (wrapperEntry?.isSymbolicLink()) {
120
+ const target = symlinkTarget(wrapperPath);
121
+ const linkTarget = target === undefined
122
+ ? "unavailable"
123
+ : isWithin(companySkillsRoot, target)
124
+ ? "company_skills"
125
+ : "outside_company_skills";
126
+ return {
127
+ owned: linkTarget === "company_skills",
128
+ diagnostics: {
129
+ kind: "symlink",
130
+ bridgeState: "not_applicable",
131
+ markerState: "not_applicable",
132
+ linkTarget,
133
+ },
134
+ };
135
+ }
136
+ if (!wrapperEntry?.isDirectory()) {
137
+ return {
138
+ owned: false,
139
+ diagnostics: {
140
+ kind: wrapperEntry?.isFile() ? "file" : "other",
141
+ bridgeState: "not_applicable",
142
+ markerState: "not_applicable",
143
+ linkTarget: "not_applicable",
144
+ },
145
+ };
146
+ }
115
147
  const entries = fs.readdirSync(wrapperPath).filter((entry) => !entry.startsWith("."));
116
- const containsOnlyLiveBridges = (entries.length > 0 &&
117
- entries.every((entry) => {
118
- const target = symlinkTarget(path.join(wrapperPath, entry));
119
- return target !== undefined && isWithin(companySkillsRoot, target);
120
- }));
121
- return containsOnlyLiveBridges || hasBoundGeneratedMarker(wrapperPath, companySlug);
148
+ const liveBridgeCount = entries.reduce((count, entry) => {
149
+ const target = symlinkTarget(path.join(wrapperPath, entry));
150
+ return count + (target !== undefined && isWithin(companySkillsRoot, target) ? 1 : 0);
151
+ }, 0);
152
+ const bridgeState = entries.length === 0
153
+ ? "empty"
154
+ : liveBridgeCount === entries.length
155
+ ? "all_live"
156
+ : liveBridgeCount > 0
157
+ ? "partial"
158
+ : "none";
159
+ const markerState = generatedMarkerState(wrapperPath, companySlug);
160
+ return {
161
+ owned: bridgeState === "all_live" || markerState === "bound",
162
+ diagnostics: {
163
+ kind: "directory",
164
+ bridgeState,
165
+ markerState,
166
+ linkTarget: "not_applicable",
167
+ },
168
+ };
169
+ }
170
+ function isGeneratedNamespacedWrapper(wrapperPath, hqRoot, companySlug) {
171
+ return inspectGeneratedNamespacedWrapper(wrapperPath, hqRoot, companySlug).owned;
172
+ }
173
+ function generatedMarkerState(wrapperPath, companySlug) {
174
+ let markerContents;
175
+ try {
176
+ markerContents = fs.readFileSync(generatedMarkerPath(wrapperPath), "utf8");
177
+ }
178
+ catch (error) {
179
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT"
180
+ ? "missing"
181
+ : "unavailable";
182
+ }
183
+ let marker;
184
+ try {
185
+ marker = JSON.parse(markerContents);
186
+ }
187
+ catch {
188
+ return "invalid";
189
+ }
190
+ try {
191
+ const wrapper = fs.lstatSync(wrapperPath);
192
+ return marker.version === 2 &&
193
+ marker.companySlug === companySlug &&
194
+ marker.wrapperDev === wrapper.dev &&
195
+ marker.wrapperIno === wrapper.ino &&
196
+ marker.wrapperBirthtimeMs === wrapper.birthtimeMs
197
+ ? "bound"
198
+ : "unbound";
199
+ }
200
+ catch {
201
+ return "unavailable";
202
+ }
122
203
  }
123
204
  function isGeneratedLegacyEntry(legacyPath, sourceDir, skillFile) {
124
205
  const target = symlinkTarget(legacyPath);
@@ -224,8 +305,19 @@ export function surfaceCompanySkill(input) {
224
305
  const backupPath = `${stagingPath}.previous`;
225
306
  let previousMoved = false;
226
307
  if (currentWrapper) {
227
- if (!isGeneratedNamespacedWrapper(wrapperPath, input.hqRoot, input.companySlug)) {
228
- throw new Error(`Cannot replace '${wrapperPath}' because it is not an HQ-generated skill wrapper.`);
308
+ const ownership = inspectGeneratedNamespacedWrapper(wrapperPath, input.hqRoot, input.companySlug);
309
+ if (!ownership.owned) {
310
+ const runtimePlatform = process.platform;
311
+ const platform = win32
312
+ ? "win32"
313
+ : runtimePlatform === "darwin" || runtimePlatform === "linux"
314
+ ? runtimePlatform
315
+ : "other";
316
+ throw companySkillWrapperOwnershipError(wrapperPath, {
317
+ ...ownership.diagnostics,
318
+ platform,
319
+ nameEncoding: win32 ? "percent_encoded" : "literal",
320
+ });
229
321
  }
230
322
  renameEntry(wrapperPath, backupPath);
231
323
  previousMoved = true;
@@ -10,6 +10,7 @@ import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
11
  import { workContextSessionsDir } from "../../../work-context/paths.js";
12
12
  import { readSessionState } from "../../../work-context/state.js";
13
+ import { mapReaderStoryStatus } from "../../story-status.js";
13
14
  import { sessionBoardPath } from "./paths.js";
14
15
  export const BOARD_REFRESH_INTERVAL_MS = 60_000;
15
16
  function listBoundSessionIds(workContextRoot) {
@@ -147,7 +148,7 @@ function storiesFromProject(project) {
147
148
  : typeof row.name === "string"
148
149
  ? row.name.slice(0, 120)
149
150
  : undefined,
150
- status: typeof row.status === "string" ? row.status : undefined,
151
+ status: mapReaderStoryStatus(row),
151
152
  });
152
153
  }
153
154
  return stories;
@@ -1,4 +1,5 @@
1
1
  import { appendThreadEvent, ensureProjectThread, getProjectView, putProjectView, registerMeshProject, } from "./api.js";
2
+ import { mapReaderStoryStatus } from "./story-status.js";
2
3
  function httpStatus(err) {
3
4
  if (err && typeof err === "object" && "status" in err && typeof err.status === "number") {
4
5
  return err.status;
@@ -19,7 +20,7 @@ export function mergeProjectStories(current, planned, opts) {
19
20
  ...(live.assigneeUid ? { assigneeUid: live.assigneeUid } : {}),
20
21
  };
21
22
  }
22
- const status = live.status || story.status;
23
+ const status = mapReaderStoryStatus(live) || story.status;
23
24
  return {
24
25
  ...story,
25
26
  ...(status ? { status } : {}),
@@ -30,8 +31,9 @@ export function mergeProjectStories(current, planned, opts) {
30
31
  return [...merged, ...current.filter((story) => !plannedIds.has(story.id))];
31
32
  }
32
33
  export function boardStoryStatus(story) {
33
- if (story.status)
34
- return story.status;
34
+ const status = mapReaderStoryStatus(story);
35
+ if (status)
36
+ return status;
35
37
  return story.passes === true ? "done" : "queued";
36
38
  }
37
39
  /**
@@ -0,0 +1,22 @@
1
+ export declare const BOARD_STORY_STATUSES: readonly ["queued", "in_progress", "review", "done"];
2
+ export type BoardStoryStatus = (typeof BOARD_STORY_STATUSES)[number];
3
+ /**
4
+ * The ledger has more states than the Board. `blocked` and the review states
5
+ * need attention, while `dropped` is terminal; the Board has only one terminal
6
+ * column (`done`).
7
+ */
8
+ export declare const LEDGER_STATE_TO_BOARD_STATUS: Readonly<Record<string, BoardStoryStatus>>;
9
+ export declare function mapSourceStoryStatus(story: {
10
+ status?: unknown;
11
+ state?: unknown;
12
+ }, source: string): BoardStoryStatus | undefined;
13
+ /**
14
+ * Read a status from an existing PRD or Board response without imposing the
15
+ * stricter `--stories-file` contract. Before state mapping was added, these
16
+ * readers passed explicit status strings through and ignored state.
17
+ */
18
+ export declare function mapReaderStoryStatus(story: {
19
+ status?: unknown;
20
+ state?: unknown;
21
+ }): string | undefined;
22
+ //# sourceMappingURL=story-status.d.ts.map
@@ -0,0 +1,49 @@
1
+ export const BOARD_STORY_STATUSES = ["queued", "in_progress", "review", "done"];
2
+ const boardStoryStatusSet = new Set(BOARD_STORY_STATUSES);
3
+ /**
4
+ * The ledger has more states than the Board. `blocked` and the review states
5
+ * need attention, while `dropped` is terminal; the Board has only one terminal
6
+ * column (`done`).
7
+ */
8
+ export const LEDGER_STATE_TO_BOARD_STATUS = {
9
+ todo: "queued",
10
+ in_progress: "in_progress",
11
+ in_review: "review",
12
+ verification: "review",
13
+ blocked: "review",
14
+ done: "done",
15
+ dropped: "done",
16
+ };
17
+ export function mapSourceStoryStatus(story, source) {
18
+ if (story.status !== undefined && story.status !== null) {
19
+ if (typeof story.status !== "string" || !boardStoryStatusSet.has(story.status.trim())) {
20
+ throw new Error(`${source}.status must be queued|in_progress|review|done.`);
21
+ }
22
+ return story.status.trim();
23
+ }
24
+ if (Object.hasOwn(story, "state")) {
25
+ if (typeof story.state !== "string" ||
26
+ !Object.hasOwn(LEDGER_STATE_TO_BOARD_STATUS, story.state.trim())) {
27
+ throw new Error(`${source}.state must be todo|in_progress|in_review|verification|blocked|done|dropped.`);
28
+ }
29
+ return LEDGER_STATE_TO_BOARD_STATUS[story.state.trim()];
30
+ }
31
+ return undefined;
32
+ }
33
+ /**
34
+ * Read a status from an existing PRD or Board response without imposing the
35
+ * stricter `--stories-file` contract. Before state mapping was added, these
36
+ * readers passed explicit status strings through and ignored state.
37
+ */
38
+ export function mapReaderStoryStatus(story) {
39
+ if (story.status !== undefined && story.status !== null) {
40
+ return typeof story.status === "string" ? story.status : undefined;
41
+ }
42
+ if (typeof story.state !== "string")
43
+ return undefined;
44
+ const state = story.state.trim();
45
+ return Object.hasOwn(LEDGER_STATE_TO_BOARD_STATUS, state)
46
+ ? LEDGER_STATE_TO_BOARD_STATUS[state]
47
+ : undefined;
48
+ }
49
+ //# sourceMappingURL=story-status.js.map
@@ -8,7 +8,7 @@
8
8
  import { type SessionStateFile } from "./state.js";
9
9
  export interface PrdStatusEvent {
10
10
  taskId: string;
11
- status: "done" | "in_progress";
11
+ status: "done" | "in_progress" | "review";
12
12
  }
13
13
  export interface PrdSnapshot {
14
14
  stories: Record<string, {
@@ -8,6 +8,7 @@
8
8
  import * as fs from "node:fs";
9
9
  import * as path from "node:path";
10
10
  import { readSessionState, updateSessionState } from "./state.js";
11
+ import { mapReaderStoryStatus } from "../mesh/story-status.js";
11
12
  export function prdSnapshotPath(sessionId, root) {
12
13
  return path.join(root, "sessions", `${sessionId}.prd-snapshot.json`);
13
14
  }
@@ -87,11 +88,12 @@ function storiesFromPrd(file) {
87
88
  const id = row.id;
88
89
  if (typeof id !== "string" || !id.trim())
89
90
  continue;
90
- const status = row.status;
91
+ const rawStatus = mapReaderStoryStatus(row);
92
+ const status = rawStatus?.trim() ? rawStatus.trim() : undefined;
91
93
  out.push({
92
94
  id: id.trim(),
93
95
  passes: row.passes === true,
94
- ...(typeof status === "string" && status.trim() ? { status: status.trim() } : {}),
96
+ ...(status ? { status } : {}),
95
97
  });
96
98
  }
97
99
  return out;
@@ -144,7 +146,7 @@ export function planPrdSync(input) {
144
146
  ...(story.status ? { status: story.status } : {}),
145
147
  };
146
148
  const prior = previous ? before[story.id] : undefined;
147
- if (story.passes && !prior?.passes) {
149
+ if ((story.passes || story.status === "done") && !prior?.passes && prior?.status !== "done") {
148
150
  events.push({ taskId: story.id, status: "done" });
149
151
  continue;
150
152
  }
@@ -152,6 +154,9 @@ export function planPrdSync(input) {
152
154
  events.push({ taskId: story.id, status: "in_progress" });
153
155
  taskId = story.id;
154
156
  }
157
+ if (story.status === "review" && prior?.status !== "review") {
158
+ events.push({ taskId: story.id, status: "review" });
159
+ }
155
160
  }
156
161
  const snapshot = {
157
162
  stories: nextStories,
package/dist/sentry.js CHANGED
@@ -5,6 +5,7 @@ import { beforeBreadcrumb } from "./utils/breadcrumb-buffer.js";
5
5
  import { CLI_VERSION } from "./cli-version.js";
6
6
  import { getCachedSentryUser } from "./utils/sentry-identity.js";
7
7
  import { isEpipe } from "./utils/epipe.js";
8
+ import { companySkillWrapperSentryTagsFor } from "./lib/company-skill-wrapper.js";
8
9
  import { environmentalFsErrorMessage } from "./utils/environmental-error.js";
9
10
  import { incompleteInstallCaptureContext, incompleteInstallMessage, } from "./utils/incomplete-install-error.js";
10
11
  import { installTreeTornSentryTagsFor, sentryFingerprintFor, sentryMcpCollisionContextFor, } from "./utils/sentry-fingerprint.js";
@@ -84,6 +85,10 @@ export function epipeAwareBeforeSend(event, hint) {
84
85
  if (fingerprint)
85
86
  event.fingerprint = fingerprint;
86
87
  }
88
+ const skillWrapperTags = companySkillWrapperSentryTagsFor(hint?.originalException);
89
+ if (skillWrapperTags) {
90
+ event.tags = { ...event.tags, ...skillWrapperTags };
91
+ }
87
92
  const installTreeTags = installTreeTornSentryTagsFor(hint?.originalException);
88
93
  if (installTreeTags) {
89
94
  event.tags = { ...event.tags, ...installTreeTags };
@@ -99,5 +99,5 @@ export declare const CONTRIBUTION_KEYS: ContributionKey[];
99
99
  */
100
100
  export declare function payloadFor(key: ContributionKey, item: string): string;
101
101
  /** Keys whose contributions are wired by a host symlink (skip `merge` rows). */
102
- export declare const SYMLINK_KEYS: ("workers" | "mcp" | "knowledge" | "skills" | "commands" | "hooks" | "policies" | "scripts")[];
102
+ export declare const SYMLINK_KEYS: ("workers" | "mcp" | "skills" | "knowledge" | "commands" | "hooks" | "policies" | "scripts")[];
103
103
  //# sourceMappingURL=contribution-table.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.192.2",
3
+ "version": "5.193.1",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {